@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,760 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Pending Update Store for GAIA-OPS Agent Context Feedback Loop
4
+
5
+ This module manages pending update suggestions to project-context.json from agents.
6
+ It provides deduplication, approval workflow, and automatic application of changes.
7
+
8
+ Architecture:
9
+ - JSONL append-only audit trail for all events
10
+ - JSON index for fast queries and deduplication
11
+ - Content-based hashing for deduplication
12
+ - Atomic file operations for safety
13
+ - Automatic backup before applying changes
14
+
15
+ Storage layout:
16
+ pending-updates/
17
+ ├── pending-updates.jsonl # Append-only audit trail
18
+ ├── pending-index.json # Mutable index for fast queries
19
+ └── applied/ # Archive of applied updates
20
+ └── update-<id>.json
21
+ """
22
+
23
+ import json
24
+ import hashlib
25
+ import sys
26
+ from datetime import datetime, timezone
27
+ from pathlib import Path
28
+ from typing import Dict, List, Any, Optional, Union
29
+ from dataclasses import dataclass, asdict, field
30
+ from enum import Enum
31
+
32
+
33
+ class DiscoveryCategory(str, Enum):
34
+ """Categories of discoveries that can be made by agents."""
35
+ NEW_RESOURCE = "new_resource"
36
+ CONFIGURATION_ISSUE = "configuration_issue"
37
+ DRIFT_DETECTED = "drift_detected"
38
+ DEPENDENCY_DISCOVERED = "dependency_discovered"
39
+ TOPOLOGY_CHANGE = "topology_change"
40
+
41
+
42
+ class UpdateStatus(str, Enum):
43
+ """Status of a pending update."""
44
+ PENDING = "pending"
45
+ APPROVED = "approved"
46
+ REJECTED = "rejected"
47
+ APPLIED = "applied"
48
+
49
+
50
+ # Mapping of categories to valid target sections
51
+ CATEGORY_TO_SECTIONS = {
52
+ "new_resource": ["application_services", "cluster_details", "infrastructure_topology"],
53
+ "configuration_issue": ["infrastructure", "terraform_infrastructure", "gitops_configuration", "application_services"],
54
+ "drift_detected": ["application_services", "cluster_details", "gitops_configuration", "terraform_infrastructure"],
55
+ "dependency_discovered": ["application_services", "infrastructure_topology"],
56
+ "topology_change": ["infrastructure_topology", "cluster_details"],
57
+ }
58
+
59
+
60
+ @dataclass
61
+ class DiscoveryResult:
62
+ """Input DTO for creating a pending update."""
63
+ category: str
64
+ target_section: str
65
+ proposed_change: dict
66
+ summary: str
67
+ confidence: float
68
+ source_agent: str
69
+ source_task: str = ""
70
+ source_episode_id: str = ""
71
+
72
+
73
+ @dataclass
74
+ class PendingUpdate:
75
+ """Represents a pending update to project-context.json."""
76
+ update_id: str
77
+ content_hash: str
78
+ source_agent: str
79
+ source_task: str
80
+ source_episode_id: str
81
+ category: str
82
+ confidence: float
83
+ target_section: str
84
+ proposed_change: dict
85
+ summary: str
86
+ status: str
87
+ created_at: str
88
+ updated_at: str
89
+ seen_count: int
90
+ last_seen_at: str
91
+ seen_by_agents: List[str]
92
+
93
+ def to_dict(self) -> Dict[str, Any]:
94
+ """Convert to dictionary, removing None values."""
95
+ data = asdict(self)
96
+ return {k: v for k, v in data.items() if v is not None}
97
+
98
+
99
+ class PendingUpdateStore:
100
+ """
101
+ Manages pending updates to project-context.json.
102
+
103
+ This class provides methods to:
104
+ - Create and deduplicate update suggestions
105
+ - Approve/reject updates
106
+ - Apply approved updates with automatic backup
107
+ - Track update statistics
108
+ - Maintain an efficient index for fast retrieval
109
+ """
110
+
111
+ def __init__(self, base_path: Optional[Union[str, Path]] = None):
112
+ """
113
+ Initialize PendingUpdateStore with specified or default path.
114
+
115
+ Args:
116
+ base_path: Base directory for pending updates storage.
117
+ Defaults to .claude/project-context/pending-updates/
118
+ """
119
+ if base_path:
120
+ self.base_path = Path(base_path)
121
+ else:
122
+ # Default location
123
+ self.base_path = Path(".claude/project-context/pending-updates")
124
+
125
+ self.updates_jsonl = self.base_path / "pending-updates.jsonl"
126
+ self.index_file = self.base_path / "pending-index.json"
127
+ self.applied_dir = self.base_path / "applied"
128
+
129
+ # Auto-create directories
130
+ self._ensure_directories()
131
+
132
+ def _ensure_directories(self):
133
+ """Create required directories if they don't exist."""
134
+ self.base_path.mkdir(parents=True, exist_ok=True)
135
+ self.applied_dir.mkdir(parents=True, exist_ok=True)
136
+
137
+ if not self.index_file.exists():
138
+ self._save_index({
139
+ "version": "1.0.0",
140
+ "last_updated": datetime.now(timezone.utc).isoformat(),
141
+ "total_count": 0,
142
+ "pending_count": 0,
143
+ "updates": {},
144
+ "hash_index": {}
145
+ })
146
+
147
+ def _save_index(self, index_data: Dict[str, Any]):
148
+ """Save index to JSON file."""
149
+ with open(self.index_file, 'w') as f:
150
+ json.dump(index_data, f, indent=2)
151
+
152
+ def _load_index(self) -> Dict[str, Any]:
153
+ """Load index from JSON file."""
154
+ if not self.index_file.exists():
155
+ return {
156
+ "version": "1.0.0",
157
+ "last_updated": datetime.now(timezone.utc).isoformat(),
158
+ "total_count": 0,
159
+ "pending_count": 0,
160
+ "updates": {},
161
+ "hash_index": {}
162
+ }
163
+
164
+ try:
165
+ with open(self.index_file, 'r') as f:
166
+ return json.load(f)
167
+ except (json.JSONDecodeError, IOError):
168
+ # Return empty index if file is corrupted
169
+ return {
170
+ "version": "1.0.0",
171
+ "last_updated": datetime.now(timezone.utc).isoformat(),
172
+ "total_count": 0,
173
+ "pending_count": 0,
174
+ "updates": {},
175
+ "hash_index": {}
176
+ }
177
+
178
+ def _compute_hash(self, target_section: str, proposed_change: dict) -> str:
179
+ """
180
+ Compute content hash for deduplication.
181
+
182
+ Args:
183
+ target_section: Target section in project-context.json
184
+ proposed_change: Proposed change dictionary
185
+
186
+ Returns:
187
+ SHA-256 hash (first 12 characters)
188
+ """
189
+ content = json.dumps({
190
+ "section": target_section,
191
+ "change": proposed_change
192
+ }, sort_keys=True)
193
+
194
+ hash_full = hashlib.sha256(content.encode('utf-8')).hexdigest()
195
+ return hash_full[:12]
196
+
197
+ def _log_event(self, event: Dict[str, Any]):
198
+ """Append event to JSONL audit trail."""
199
+ with open(self.updates_jsonl, 'a') as f:
200
+ f.write(json.dumps(event) + '\n')
201
+
202
+ def _validate_discovery(self, discovery: DiscoveryResult) -> bool:
203
+ """
204
+ Validate discovery result.
205
+
206
+ Args:
207
+ discovery: Discovery result to validate
208
+
209
+ Returns:
210
+ True if valid, False otherwise
211
+ """
212
+ if discovery.confidence < 0.7:
213
+ print(f"Error: Confidence {discovery.confidence} is below threshold 0.7", file=sys.stderr)
214
+ return False
215
+
216
+ if discovery.category not in CATEGORY_TO_SECTIONS:
217
+ print(f"Error: Invalid category '{discovery.category}'", file=sys.stderr)
218
+ return False
219
+
220
+ valid_sections = CATEGORY_TO_SECTIONS[discovery.category]
221
+ if discovery.target_section not in valid_sections:
222
+ print(f"Error: Invalid target section '{discovery.target_section}' for category '{discovery.category}'", file=sys.stderr)
223
+ print(f"Valid sections: {valid_sections}", file=sys.stderr)
224
+ return False
225
+
226
+ return True
227
+
228
+ def create(self, discovery: DiscoveryResult) -> str:
229
+ """
230
+ Create a new pending update or deduplicate with existing.
231
+
232
+ Args:
233
+ discovery: Discovery result from agent
234
+
235
+ Returns:
236
+ Update ID (new or deduplicated)
237
+
238
+ Raises:
239
+ ValueError: If discovery is invalid
240
+ """
241
+ if not self._validate_discovery(discovery):
242
+ raise ValueError("Invalid discovery result")
243
+
244
+ content_hash = self._compute_hash(discovery.target_section, discovery.proposed_change)
245
+
246
+ index = self._load_index()
247
+
248
+ existing_id = index["hash_index"].get(content_hash)
249
+
250
+ now = datetime.now(timezone.utc).isoformat()
251
+
252
+ if existing_id and existing_id in index["updates"]:
253
+ # Deduplicate - increment seen_count
254
+ existing = index["updates"][existing_id]
255
+ existing["seen_count"] += 1
256
+ existing["last_seen_at"] = now
257
+ existing["updated_at"] = now
258
+
259
+ # Add source agent to seen_by_agents if not already present
260
+ if discovery.source_agent not in existing["seen_by_agents"]:
261
+ existing["seen_by_agents"].append(discovery.source_agent)
262
+
263
+ self._save_index(index)
264
+
265
+ self._log_event({
266
+ "event": "dedup_increment",
267
+ "update_id": existing_id,
268
+ "timestamp": now,
269
+ "seen_count": existing["seen_count"],
270
+ "source_agent": discovery.source_agent
271
+ })
272
+
273
+ print(f"Deduplicated update: {existing_id} (seen_count={existing['seen_count']})", file=sys.stderr)
274
+ return existing_id
275
+
276
+ update_id = f"pu_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{content_hash[:4]}"
277
+
278
+ update = PendingUpdate(
279
+ update_id=update_id,
280
+ content_hash=content_hash,
281
+ source_agent=discovery.source_agent,
282
+ source_task=discovery.source_task,
283
+ source_episode_id=discovery.source_episode_id,
284
+ category=discovery.category,
285
+ confidence=discovery.confidence,
286
+ target_section=discovery.target_section,
287
+ proposed_change=discovery.proposed_change,
288
+ summary=discovery.summary,
289
+ status=UpdateStatus.PENDING.value,
290
+ created_at=now,
291
+ updated_at=now,
292
+ seen_count=1,
293
+ last_seen_at=now,
294
+ seen_by_agents=[discovery.source_agent]
295
+ )
296
+
297
+ index["updates"][update_id] = update.to_dict()
298
+ index["hash_index"][content_hash] = update_id
299
+ index["total_count"] += 1
300
+ index["pending_count"] += 1
301
+ index["last_updated"] = now
302
+
303
+ self._save_index(index)
304
+
305
+ self._log_event({
306
+ "event": "created",
307
+ "update_id": update_id,
308
+ "timestamp": now,
309
+ "data": update.to_dict()
310
+ })
311
+
312
+ print(f"Created pending update: {update_id}", file=sys.stderr)
313
+ return update_id
314
+
315
+ def get(self, update_id: str) -> Optional[PendingUpdate]:
316
+ """
317
+ Get a specific pending update by ID.
318
+
319
+ Args:
320
+ update_id: Update ID to retrieve
321
+
322
+ Returns:
323
+ PendingUpdate or None if not found
324
+ """
325
+ index = self._load_index()
326
+ update_data = index["updates"].get(update_id)
327
+
328
+ if not update_data:
329
+ return None
330
+
331
+ return PendingUpdate(**update_data)
332
+
333
+ def list_pending(self) -> List[PendingUpdate]:
334
+ """
335
+ List all pending updates, ordered by created_at descending.
336
+
337
+ Returns:
338
+ List of PendingUpdate objects with status=pending
339
+ """
340
+ index = self._load_index()
341
+ pending = [
342
+ PendingUpdate(**data)
343
+ for data in index["updates"].values()
344
+ if data["status"] == UpdateStatus.PENDING.value
345
+ ]
346
+
347
+ # Sort by created_at descending
348
+ pending.sort(key=lambda x: x.created_at, reverse=True)
349
+ return pending
350
+
351
+ def list_all(self, status: Optional[str] = None) -> List[PendingUpdate]:
352
+ """
353
+ List all updates with optional status filter.
354
+
355
+ Args:
356
+ status: Optional status filter
357
+
358
+ Returns:
359
+ List of PendingUpdate objects
360
+ """
361
+ index = self._load_index()
362
+ updates = []
363
+
364
+ for data in index["updates"].values():
365
+ if status is None or data["status"] == status:
366
+ updates.append(PendingUpdate(**data))
367
+
368
+ # Sort by created_at descending
369
+ updates.sort(key=lambda x: x.created_at, reverse=True)
370
+ return updates
371
+
372
+ def approve(self, update_id: str) -> PendingUpdate:
373
+ """
374
+ Approve a pending update.
375
+
376
+ Args:
377
+ update_id: Update ID to approve
378
+
379
+ Returns:
380
+ Updated PendingUpdate
381
+
382
+ Raises:
383
+ ValueError: If update not found or not pending
384
+ """
385
+ index = self._load_index()
386
+ update_data = index["updates"].get(update_id)
387
+
388
+ if not update_data:
389
+ raise ValueError(f"Update {update_id} not found")
390
+
391
+ if update_data["status"] != UpdateStatus.PENDING.value:
392
+ raise ValueError(f"Update {update_id} is not pending (status={update_data['status']})")
393
+
394
+ old_status = update_data["status"]
395
+ update_data["status"] = UpdateStatus.APPROVED.value
396
+ update_data["updated_at"] = datetime.now(timezone.utc).isoformat()
397
+
398
+ index["pending_count"] -= 1
399
+ index["last_updated"] = update_data["updated_at"]
400
+
401
+ self._save_index(index)
402
+
403
+ self._log_event({
404
+ "event": "status_change",
405
+ "update_id": update_id,
406
+ "timestamp": update_data["updated_at"],
407
+ "old_status": old_status,
408
+ "new_status": UpdateStatus.APPROVED.value
409
+ })
410
+
411
+ print(f"Approved update: {update_id}", file=sys.stderr)
412
+ return PendingUpdate(**update_data)
413
+
414
+ def reject(self, update_id: str) -> PendingUpdate:
415
+ """
416
+ Reject a pending update.
417
+
418
+ Args:
419
+ update_id: Update ID to reject
420
+
421
+ Returns:
422
+ Updated PendingUpdate
423
+
424
+ Raises:
425
+ ValueError: If update not found or not pending
426
+ """
427
+ index = self._load_index()
428
+ update_data = index["updates"].get(update_id)
429
+
430
+ if not update_data:
431
+ raise ValueError(f"Update {update_id} not found")
432
+
433
+ if update_data["status"] != UpdateStatus.PENDING.value:
434
+ raise ValueError(f"Update {update_id} is not pending (status={update_data['status']})")
435
+
436
+ old_status = update_data["status"]
437
+ update_data["status"] = UpdateStatus.REJECTED.value
438
+ update_data["updated_at"] = datetime.now(timezone.utc).isoformat()
439
+
440
+ index["pending_count"] -= 1
441
+ index["last_updated"] = update_data["updated_at"]
442
+
443
+ self._save_index(index)
444
+
445
+ self._log_event({
446
+ "event": "status_change",
447
+ "update_id": update_id,
448
+ "timestamp": update_data["updated_at"],
449
+ "old_status": old_status,
450
+ "new_status": UpdateStatus.REJECTED.value
451
+ })
452
+
453
+ print(f"Rejected update: {update_id}", file=sys.stderr)
454
+ return PendingUpdate(**update_data)
455
+
456
+ def apply(self, update_id: str, context_path: Optional[Union[str, Path]] = None) -> dict:
457
+ """
458
+ Apply an approved update to project-context.json.
459
+
460
+ Args:
461
+ update_id: Update ID to apply
462
+ context_path: Path to project-context.json (defaults to standard location)
463
+
464
+ Returns:
465
+ Dict with result: {success: bool, update_id: str, target_section: str, backup_path: str}
466
+
467
+ Raises:
468
+ ValueError: If update not found or not approved
469
+ """
470
+ index = self._load_index()
471
+ update_data = index["updates"].get(update_id)
472
+
473
+ if not update_data:
474
+ raise ValueError(f"Update {update_id} not found")
475
+
476
+ if update_data["status"] != UpdateStatus.APPROVED.value:
477
+ raise ValueError(f"Update {update_id} is not approved (status={update_data['status']})")
478
+
479
+ if context_path:
480
+ context_file = Path(context_path)
481
+ else:
482
+ context_file = Path(".claude/project-context/project-context.json")
483
+
484
+ if not context_file.exists():
485
+ raise ValueError(f"Project context file not found: {context_file}")
486
+
487
+ try:
488
+ with open(context_file, 'r') as f:
489
+ context_data = json.load(f)
490
+
491
+ if "sections" not in context_data:
492
+ raise ValueError("Invalid project-context.json: missing 'sections' key")
493
+
494
+ target_section = update_data["target_section"]
495
+ if target_section not in context_data["sections"]:
496
+ raise ValueError(f"Target section '{target_section}' not found in project-context.json")
497
+
498
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
499
+ backup_path = context_file.parent / f"project-context.backup.{timestamp}.json"
500
+ with open(backup_path, 'w') as f:
501
+ json.dump(context_data, f, indent=2)
502
+
503
+ section_data = context_data["sections"][target_section]
504
+ proposed_change = update_data["proposed_change"]
505
+
506
+ # Simple merge: update keys from proposed_change
507
+ self._merge_dicts(section_data, proposed_change)
508
+
509
+ if "metadata" not in context_data:
510
+ context_data["metadata"] = {}
511
+ context_data["metadata"]["last_updated"] = datetime.now(timezone.utc).isoformat()
512
+
513
+ # Atomic write: write to temp file then rename
514
+ temp_file = context_file.parent / f".{context_file.name}.tmp"
515
+ with open(temp_file, 'w') as f:
516
+ json.dump(context_data, f, indent=2)
517
+ temp_file.rename(context_file)
518
+
519
+ old_status = update_data["status"]
520
+ update_data["status"] = UpdateStatus.APPLIED.value
521
+ update_data["updated_at"] = datetime.now(timezone.utc).isoformat()
522
+
523
+ index["last_updated"] = update_data["updated_at"]
524
+ self._save_index(index)
525
+
526
+ applied_file = self.applied_dir / f"update-{update_id}.json"
527
+ with open(applied_file, 'w') as f:
528
+ json.dump(update_data, f, indent=2)
529
+
530
+ self._log_event({
531
+ "event": "status_change",
532
+ "update_id": update_id,
533
+ "timestamp": update_data["updated_at"],
534
+ "old_status": old_status,
535
+ "new_status": UpdateStatus.APPLIED.value
536
+ })
537
+
538
+ print(f"Applied update {update_id} to {target_section}", file=sys.stderr)
539
+ print(f"Backup saved: {backup_path}", file=sys.stderr)
540
+
541
+ return {
542
+ "success": True,
543
+ "update_id": update_id,
544
+ "target_section": target_section,
545
+ "backup_path": str(backup_path)
546
+ }
547
+
548
+ except Exception as e:
549
+ print(f"Error applying update {update_id}: {e}", file=sys.stderr)
550
+ raise
551
+
552
+ def _merge_dicts(self, target: dict, source: dict):
553
+ """
554
+ Recursively merge source dict into target dict.
555
+
556
+ Args:
557
+ target: Target dictionary to merge into
558
+ source: Source dictionary to merge from
559
+ """
560
+ for key, value in source.items():
561
+ if isinstance(value, dict) and key in target and isinstance(target[key], dict):
562
+ # Recursive merge for nested dicts
563
+ self._merge_dicts(target[key], value)
564
+ else:
565
+ # Overwrite or add key
566
+ target[key] = value
567
+
568
+ def get_statistics(self) -> dict:
569
+ """
570
+ Get statistics about pending updates.
571
+
572
+ Returns:
573
+ Dict with counts by status, category, and agent
574
+ """
575
+ index = self._load_index()
576
+
577
+ stats = {
578
+ "total_count": index["total_count"],
579
+ "pending_count": index["pending_count"],
580
+ "by_status": {},
581
+ "by_category": {},
582
+ "by_agent": {}
583
+ }
584
+
585
+ for update_data in index["updates"].values():
586
+ status = update_data["status"]
587
+ category = update_data["category"]
588
+ agent = update_data["source_agent"]
589
+
590
+ stats["by_status"][status] = stats["by_status"].get(status, 0) + 1
591
+ stats["by_category"][category] = stats["by_category"].get(category, 0) + 1
592
+ stats["by_agent"][agent] = stats["by_agent"].get(agent, 0) + 1
593
+
594
+ return stats
595
+
596
+ def get_pending_count(self) -> int:
597
+ """
598
+ Get count of pending updates (fast path from index).
599
+
600
+ Returns:
601
+ Number of pending updates
602
+ """
603
+ index = self._load_index()
604
+ return index["pending_count"]
605
+
606
+
607
+ # CLI interface for testing and management
608
+ if __name__ == "__main__":
609
+ import argparse
610
+
611
+ parser = argparse.ArgumentParser(description="Pending Update Store Management")
612
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
613
+
614
+ # Create command
615
+ create_parser = subparsers.add_parser("create", help="Create a new pending update")
616
+ create_parser.add_argument("--category", required=True, choices=list(CATEGORY_TO_SECTIONS.keys()), help="Discovery category")
617
+ create_parser.add_argument("--section", required=True, help="Target section")
618
+ create_parser.add_argument("--change", required=True, help="Proposed change (JSON string)")
619
+ create_parser.add_argument("--summary", required=True, help="Summary of change")
620
+ create_parser.add_argument("--confidence", type=float, default=0.8, help="Confidence score")
621
+ create_parser.add_argument("--agent", required=True, help="Source agent")
622
+ create_parser.add_argument("--task", default="", help="Source task")
623
+
624
+ # List command
625
+ list_parser = subparsers.add_parser("list", help="List pending updates")
626
+ list_parser.add_argument("--status", choices=["pending", "approved", "rejected", "applied"], help="Filter by status")
627
+
628
+ # Get command
629
+ get_parser = subparsers.add_parser("get", help="Get a specific update")
630
+ get_parser.add_argument("update_id", help="Update ID")
631
+
632
+ # Approve command
633
+ approve_parser = subparsers.add_parser("approve", help="Approve a pending update")
634
+ approve_parser.add_argument("update_id", help="Update ID")
635
+
636
+ # Reject command
637
+ reject_parser = subparsers.add_parser("reject", help="Reject a pending update")
638
+ reject_parser.add_argument("update_id", help="Update ID")
639
+
640
+ # Apply command
641
+ apply_parser = subparsers.add_parser("apply", help="Apply an approved update")
642
+ apply_parser.add_argument("update_id", help="Update ID")
643
+ apply_parser.add_argument("--context", help="Path to project-context.json")
644
+
645
+ # Stats command
646
+ stats_parser = subparsers.add_parser("stats", help="Show statistics")
647
+
648
+ args = parser.parse_args()
649
+
650
+ store = PendingUpdateStore()
651
+
652
+ if args.command == "create":
653
+ try:
654
+ proposed_change = json.loads(args.change)
655
+ discovery = DiscoveryResult(
656
+ category=args.category,
657
+ target_section=args.section,
658
+ proposed_change=proposed_change,
659
+ summary=args.summary,
660
+ confidence=args.confidence,
661
+ source_agent=args.agent,
662
+ source_task=args.task
663
+ )
664
+ update_id = store.create(discovery)
665
+ print(f"Created update: {update_id}")
666
+ except Exception as e:
667
+ print(f"Error: {e}", file=sys.stderr)
668
+ sys.exit(1)
669
+
670
+ elif args.command == "list":
671
+ if args.status:
672
+ updates = store.list_all(status=args.status)
673
+ else:
674
+ updates = store.list_pending()
675
+
676
+ if not updates:
677
+ print("No updates found")
678
+ else:
679
+ print(f"\nFound {len(updates)} update(s):\n")
680
+ for update in updates:
681
+ print(f"ID: {update.update_id}")
682
+ print(f" Status: {update.status}")
683
+ print(f" Category: {update.category}")
684
+ print(f" Section: {update.target_section}")
685
+ print(f" Agent: {update.source_agent}")
686
+ print(f" Confidence: {update.confidence}")
687
+ print(f" Seen: {update.seen_count} time(s)")
688
+ print(f" Summary: {update.summary}")
689
+ print(f" Created: {update.created_at}")
690
+ print()
691
+
692
+ elif args.command == "get":
693
+ update = store.get(args.update_id)
694
+ if not update:
695
+ print(f"Update {args.update_id} not found", file=sys.stderr)
696
+ sys.exit(1)
697
+
698
+ print(f"\nUpdate: {update.update_id}")
699
+ print(f" Status: {update.status}")
700
+ print(f" Category: {update.category}")
701
+ print(f" Section: {update.target_section}")
702
+ print(f" Agent: {update.source_agent}")
703
+ print(f" Task: {update.source_task}")
704
+ print(f" Confidence: {update.confidence}")
705
+ print(f" Seen: {update.seen_count} time(s) by {update.seen_by_agents}")
706
+ print(f" Summary: {update.summary}")
707
+ print(f" Created: {update.created_at}")
708
+ print(f" Updated: {update.updated_at}")
709
+ print(f"\n Proposed change:")
710
+ print(f" {json.dumps(update.proposed_change, indent=4)}")
711
+
712
+ elif args.command == "approve":
713
+ try:
714
+ update = store.approve(args.update_id)
715
+ print(f"Approved update: {update.update_id}")
716
+ except ValueError as e:
717
+ print(f"Error: {e}", file=sys.stderr)
718
+ sys.exit(1)
719
+
720
+ elif args.command == "reject":
721
+ try:
722
+ update = store.reject(args.update_id)
723
+ print(f"Rejected update: {update.update_id}")
724
+ except ValueError as e:
725
+ print(f"Error: {e}", file=sys.stderr)
726
+ sys.exit(1)
727
+
728
+ elif args.command == "apply":
729
+ try:
730
+ result = store.apply(args.update_id, context_path=args.context)
731
+ print(f"Successfully applied update: {result['update_id']}")
732
+ print(f" Section: {result['target_section']}")
733
+ print(f" Backup: {result['backup_path']}")
734
+ except Exception as e:
735
+ print(f"Error: {e}", file=sys.stderr)
736
+ sys.exit(1)
737
+
738
+ elif args.command == "stats":
739
+ stats = store.get_statistics()
740
+ print("\nPending Update Statistics:")
741
+ print(f" Total updates: {stats['total_count']}")
742
+ print(f" Pending: {stats['pending_count']}")
743
+
744
+ if stats["by_status"]:
745
+ print("\n By status:")
746
+ for status, count in stats["by_status"].items():
747
+ print(f" {status}: {count}")
748
+
749
+ if stats["by_category"]:
750
+ print("\n By category:")
751
+ for category, count in stats["by_category"].items():
752
+ print(f" {category}: {count}")
753
+
754
+ if stats["by_agent"]:
755
+ print("\n By agent:")
756
+ for agent, count in stats["by_agent"].items():
757
+ print(f" {agent}: {count}")
758
+
759
+ else:
760
+ parser.print_help()