@jaguilar87/gaia 5.0.0-rc.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 (621) 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 +1298 -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 +111 -0
  16. package/agents/gaia-planner.md +53 -0
  17. package/agents/gaia-system.md +71 -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 +651 -0
  26. package/bin/cli/history.py +305 -0
  27. package/bin/cli/memory.py +483 -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 +919 -0
  45. package/bin/pre-publish-validate.js +610 -0
  46. package/bin/python-detect.js +60 -0
  47. package/bin/validate-sandbox.sh +601 -0
  48. package/commands/README.md +64 -0
  49. package/commands/gaia.md +37 -0
  50. package/commands/scan-project.md +67 -0
  51. package/config/README.md +71 -0
  52. package/config/cloud/aws.json +134 -0
  53. package/config/cloud/gcp.json +139 -0
  54. package/config/context-contracts.json +158 -0
  55. package/config/crons-schema.md +81 -0
  56. package/config/git_standards.json +72 -0
  57. package/config/surface-routing.json +417 -0
  58. package/config/universal-rules.json +102 -0
  59. package/dist/gaia-ops/.claude-plugin/plugin.json +24 -0
  60. package/dist/gaia-ops/README.md +80 -0
  61. package/dist/gaia-ops/agents/cloud-troubleshooter.md +73 -0
  62. package/dist/gaia-ops/agents/developer.md +65 -0
  63. package/dist/gaia-ops/agents/gaia-operator.md +64 -0
  64. package/dist/gaia-ops/agents/gaia-orchestrator.md +111 -0
  65. package/dist/gaia-ops/agents/gaia-planner.md +53 -0
  66. package/dist/gaia-ops/agents/gaia-system.md +71 -0
  67. package/dist/gaia-ops/agents/gitops-operator.md +61 -0
  68. package/dist/gaia-ops/agents/terraform-architect.md +63 -0
  69. package/dist/gaia-ops/commands/gaia.md +37 -0
  70. package/dist/gaia-ops/config/README.md +71 -0
  71. package/dist/gaia-ops/config/cloud/aws.json +134 -0
  72. package/dist/gaia-ops/config/cloud/gcp.json +139 -0
  73. package/dist/gaia-ops/config/context-contracts.json +158 -0
  74. package/dist/gaia-ops/config/crons-schema.md +81 -0
  75. package/dist/gaia-ops/config/git_standards.json +72 -0
  76. package/dist/gaia-ops/config/surface-routing.json +417 -0
  77. package/dist/gaia-ops/config/universal-rules.json +102 -0
  78. package/dist/gaia-ops/hooks/adapters/__init__.py +52 -0
  79. package/dist/gaia-ops/hooks/adapters/base.py +219 -0
  80. package/dist/gaia-ops/hooks/adapters/channel.py +17 -0
  81. package/dist/gaia-ops/hooks/adapters/claude_code.py +1890 -0
  82. package/dist/gaia-ops/hooks/adapters/types.py +194 -0
  83. package/dist/gaia-ops/hooks/adapters/utils.py +25 -0
  84. package/dist/gaia-ops/hooks/hooks.json +192 -0
  85. package/dist/gaia-ops/hooks/modules/__init__.py +15 -0
  86. package/dist/gaia-ops/hooks/modules/agents/__init__.py +29 -0
  87. package/dist/gaia-ops/hooks/modules/agents/contract_validator.py +647 -0
  88. package/dist/gaia-ops/hooks/modules/agents/response_contract.py +496 -0
  89. package/dist/gaia-ops/hooks/modules/agents/skill_injection_verifier.py +120 -0
  90. package/dist/gaia-ops/hooks/modules/agents/state_tracker.py +267 -0
  91. package/dist/gaia-ops/hooks/modules/agents/task_info_builder.py +74 -0
  92. package/dist/gaia-ops/hooks/modules/agents/transcript_analyzer.py +458 -0
  93. package/dist/gaia-ops/hooks/modules/agents/transcript_reader.py +152 -0
  94. package/dist/gaia-ops/hooks/modules/audit/__init__.py +28 -0
  95. package/dist/gaia-ops/hooks/modules/audit/event_detector.py +168 -0
  96. package/dist/gaia-ops/hooks/modules/audit/logger.py +131 -0
  97. package/dist/gaia-ops/hooks/modules/audit/metrics.py +134 -0
  98. package/dist/gaia-ops/hooks/modules/audit/workflow_auditor.py +611 -0
  99. package/dist/gaia-ops/hooks/modules/audit/workflow_recorder.py +296 -0
  100. package/dist/gaia-ops/hooks/modules/context/__init__.py +11 -0
  101. package/dist/gaia-ops/hooks/modules/context/agentic_loop_detector.py +165 -0
  102. package/dist/gaia-ops/hooks/modules/context/anchor_tracker.py +317 -0
  103. package/dist/gaia-ops/hooks/modules/context/compact_context_builder.py +218 -0
  104. package/dist/gaia-ops/hooks/modules/context/context_freshness.py +145 -0
  105. package/dist/gaia-ops/hooks/modules/context/context_injector.py +558 -0
  106. package/dist/gaia-ops/hooks/modules/context/context_writer.py +530 -0
  107. package/dist/gaia-ops/hooks/modules/context/contracts_loader.py +161 -0
  108. package/dist/gaia-ops/hooks/modules/core/__init__.py +40 -0
  109. package/dist/gaia-ops/hooks/modules/core/hook_entry.py +78 -0
  110. package/dist/gaia-ops/hooks/modules/core/paths.py +160 -0
  111. package/dist/gaia-ops/hooks/modules/core/plugin_mode.py +149 -0
  112. package/dist/gaia-ops/hooks/modules/core/plugin_setup.py +577 -0
  113. package/dist/gaia-ops/hooks/modules/core/state.py +179 -0
  114. package/dist/gaia-ops/hooks/modules/core/stdin.py +24 -0
  115. package/dist/gaia-ops/hooks/modules/events/__init__.py +1 -0
  116. package/dist/gaia-ops/hooks/modules/events/event_writer.py +210 -0
  117. package/dist/gaia-ops/hooks/modules/memory/__init__.py +8 -0
  118. package/dist/gaia-ops/hooks/modules/memory/episode_writer.py +216 -0
  119. package/dist/gaia-ops/hooks/modules/orchestrator/__init__.py +1 -0
  120. package/dist/gaia-ops/hooks/modules/orchestrator/delegate_mode.py +122 -0
  121. package/dist/gaia-ops/hooks/modules/scanning/__init__.py +8 -0
  122. package/dist/gaia-ops/hooks/modules/scanning/scan_trigger.py +84 -0
  123. package/dist/gaia-ops/hooks/modules/security/__init__.py +120 -0
  124. package/dist/gaia-ops/hooks/modules/security/approval_cleanup.py +87 -0
  125. package/dist/gaia-ops/hooks/modules/security/approval_constants.py +23 -0
  126. package/dist/gaia-ops/hooks/modules/security/approval_grants.py +1638 -0
  127. package/dist/gaia-ops/hooks/modules/security/approval_messages.py +71 -0
  128. package/dist/gaia-ops/hooks/modules/security/approval_scopes.py +222 -0
  129. package/dist/gaia-ops/hooks/modules/security/blocked_commands.py +595 -0
  130. package/dist/gaia-ops/hooks/modules/security/blocked_message_formatter.py +87 -0
  131. package/dist/gaia-ops/hooks/modules/security/command_semantics.py +181 -0
  132. package/dist/gaia-ops/hooks/modules/security/composition_rules.py +547 -0
  133. package/dist/gaia-ops/hooks/modules/security/flag_classifiers.py +873 -0
  134. package/dist/gaia-ops/hooks/modules/security/gitops_validator.py +179 -0
  135. package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +1131 -0
  136. package/dist/gaia-ops/hooks/modules/security/network_hosts.py +481 -0
  137. package/dist/gaia-ops/hooks/modules/security/prompt_validator.py +40 -0
  138. package/dist/gaia-ops/hooks/modules/security/shell_unwrapper.py +165 -0
  139. package/dist/gaia-ops/hooks/modules/security/tiers.py +196 -0
  140. package/dist/gaia-ops/hooks/modules/session/__init__.py +10 -0
  141. package/dist/gaia-ops/hooks/modules/session/pending_scanner.py +174 -0
  142. package/dist/gaia-ops/hooks/modules/session/session_context_writer.py +100 -0
  143. package/dist/gaia-ops/hooks/modules/session/session_event_injector.py +160 -0
  144. package/dist/gaia-ops/hooks/modules/session/session_manager.py +31 -0
  145. package/dist/gaia-ops/hooks/modules/session/session_registry.py +333 -0
  146. package/dist/gaia-ops/hooks/modules/tools/__init__.py +29 -0
  147. package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +1008 -0
  148. package/dist/gaia-ops/hooks/modules/tools/cloud_pipe_validator.py +231 -0
  149. package/dist/gaia-ops/hooks/modules/tools/hook_response.py +55 -0
  150. package/dist/gaia-ops/hooks/modules/tools/shell_parser.py +227 -0
  151. package/dist/gaia-ops/hooks/modules/tools/stage_decomposer.py +315 -0
  152. package/dist/gaia-ops/hooks/modules/tools/task_validator.py +294 -0
  153. package/dist/gaia-ops/hooks/modules/validation/__init__.py +23 -0
  154. package/dist/gaia-ops/hooks/modules/validation/commit_validator.py +380 -0
  155. package/dist/gaia-ops/hooks/post_compact.py +43 -0
  156. package/dist/gaia-ops/hooks/post_tool_use.py +54 -0
  157. package/dist/gaia-ops/hooks/pre_compact.py +60 -0
  158. package/dist/gaia-ops/hooks/pre_tool_use.py +413 -0
  159. package/dist/gaia-ops/hooks/session_end_hook.py +77 -0
  160. package/dist/gaia-ops/hooks/session_start.py +81 -0
  161. package/dist/gaia-ops/hooks/stop_hook.py +70 -0
  162. package/dist/gaia-ops/hooks/subagent_start.py +71 -0
  163. package/dist/gaia-ops/hooks/subagent_stop.py +295 -0
  164. package/dist/gaia-ops/hooks/task_completed.py +70 -0
  165. package/dist/gaia-ops/hooks/user_prompt_submit.py +246 -0
  166. package/dist/gaia-ops/settings.json +72 -0
  167. package/dist/gaia-ops/skills/README.md +158 -0
  168. package/dist/gaia-ops/skills/agent-creation/SKILL.md +87 -0
  169. package/dist/gaia-ops/skills/agent-creation/examples.md +170 -0
  170. package/dist/gaia-ops/skills/agent-creation/reference.md +191 -0
  171. package/dist/gaia-ops/skills/agent-protocol/SKILL.md +93 -0
  172. package/dist/gaia-ops/skills/agent-protocol/examples.md +223 -0
  173. package/dist/gaia-ops/skills/agent-response/SKILL.md +69 -0
  174. package/dist/gaia-ops/skills/agentic-loop/SKILL.md +80 -0
  175. package/dist/gaia-ops/skills/agentic-loop/reference.md +378 -0
  176. package/dist/gaia-ops/skills/blog-writing/SKILL.md +98 -0
  177. package/dist/gaia-ops/skills/blog-writing/reference.md +130 -0
  178. package/dist/gaia-ops/skills/brief-spec/SKILL.md +185 -0
  179. package/dist/gaia-ops/skills/command-execution/SKILL.md +64 -0
  180. package/dist/gaia-ops/skills/command-execution/reference.md +83 -0
  181. package/dist/gaia-ops/skills/context-updater/SKILL.md +87 -0
  182. package/dist/gaia-ops/skills/context-updater/examples.md +71 -0
  183. package/dist/gaia-ops/skills/developer-patterns/SKILL.md +50 -0
  184. package/dist/gaia-ops/skills/developer-patterns/reference.md +112 -0
  185. package/dist/gaia-ops/skills/execution/SKILL.md +99 -0
  186. package/dist/gaia-ops/skills/fast-queries/SKILL.md +43 -0
  187. package/dist/gaia-ops/skills/gaia-compact/SKILL.md +74 -0
  188. package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +108 -0
  189. package/dist/gaia-ops/skills/gaia-patterns/reference.md +395 -0
  190. package/dist/gaia-ops/skills/gaia-planner/SKILL.md +37 -0
  191. package/dist/gaia-ops/skills/gaia-planner/reference.md +107 -0
  192. package/dist/gaia-ops/skills/gaia-release/SKILL.md +85 -0
  193. package/dist/gaia-ops/skills/gaia-release/reference.md +92 -0
  194. package/dist/gaia-ops/skills/gaia-self-check/SKILL.md +114 -0
  195. package/dist/gaia-ops/skills/gaia-self-check/reference.md +453 -0
  196. package/dist/gaia-ops/skills/gaia-verify/SKILL.md +77 -0
  197. package/dist/gaia-ops/skills/gaia-verify/reference.md +80 -0
  198. package/dist/gaia-ops/skills/git-conventions/SKILL.md +47 -0
  199. package/dist/gaia-ops/skills/gitops-patterns/SKILL.md +60 -0
  200. package/dist/gaia-ops/skills/gitops-patterns/reference.md +183 -0
  201. package/dist/gaia-ops/skills/gmail-policy/SKILL.md +200 -0
  202. package/dist/gaia-ops/skills/gmail-policy/reference.md +150 -0
  203. package/dist/gaia-ops/skills/gmail-triage/SKILL.md +100 -0
  204. package/dist/gaia-ops/skills/gws-setup/SKILL.md +99 -0
  205. package/dist/gaia-ops/skills/gws-setup/reference.md +73 -0
  206. package/dist/gaia-ops/skills/investigation/SKILL.md +100 -0
  207. package/dist/gaia-ops/skills/memory-curation/SKILL.md +83 -0
  208. package/dist/gaia-ops/skills/memory-search/SKILL.md +88 -0
  209. package/dist/gaia-ops/skills/orchestrator-approval/SKILL.md +160 -0
  210. package/dist/gaia-ops/skills/orchestrator-approval/reference.md +174 -0
  211. package/dist/gaia-ops/skills/pending-approvals/SKILL.md +72 -0
  212. package/dist/gaia-ops/skills/pending-approvals/reference.md +214 -0
  213. package/dist/gaia-ops/skills/readme-writing/SKILL.md +71 -0
  214. package/dist/gaia-ops/skills/readme-writing/reference.md +188 -0
  215. package/dist/gaia-ops/skills/reference.md +135 -0
  216. package/dist/gaia-ops/skills/request-approval/SKILL.md +140 -0
  217. package/dist/gaia-ops/skills/request-approval/examples.md +140 -0
  218. package/dist/gaia-ops/skills/request-approval/reference.md +57 -0
  219. package/dist/gaia-ops/skills/schedule-task/SKILL.md +64 -0
  220. package/dist/gaia-ops/skills/schedule-task/reference.md +233 -0
  221. package/dist/gaia-ops/skills/security-tiers/SKILL.md +141 -0
  222. package/dist/gaia-ops/skills/security-tiers/destructive-commands-reference.md +623 -0
  223. package/dist/gaia-ops/skills/security-tiers/reference.md +39 -0
  224. package/dist/gaia-ops/skills/session-reflection/SKILL.md +69 -0
  225. package/dist/gaia-ops/skills/skill-creation/SKILL.md +92 -0
  226. package/dist/gaia-ops/skills/skill-creation/reference.md +29 -0
  227. package/dist/gaia-ops/skills/terraform-patterns/SKILL.md +89 -0
  228. package/dist/gaia-ops/skills/terraform-patterns/reference.md +93 -0
  229. package/dist/gaia-ops/tools/__init__.py +9 -0
  230. package/dist/gaia-ops/tools/agentic-loop/decide-status.py +210 -0
  231. package/dist/gaia-ops/tools/agentic-loop/parse-metric.py +106 -0
  232. package/dist/gaia-ops/tools/agentic-loop/record-iteration.py +221 -0
  233. package/dist/gaia-ops/tools/context/README.md +132 -0
  234. package/dist/gaia-ops/tools/context/__init__.py +42 -0
  235. package/dist/gaia-ops/tools/context/_paths.py +20 -0
  236. package/dist/gaia-ops/tools/context/context_provider.py +721 -0
  237. package/dist/gaia-ops/tools/context/context_section_reader.py +342 -0
  238. package/dist/gaia-ops/tools/context/deep_merge.py +159 -0
  239. package/dist/gaia-ops/tools/context/pending_updates.py +760 -0
  240. package/dist/gaia-ops/tools/context/surface_router.py +278 -0
  241. package/dist/gaia-ops/tools/fast-queries/README.md +65 -0
  242. package/dist/gaia-ops/tools/fast-queries/__init__.py +30 -0
  243. package/dist/gaia-ops/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
  244. package/dist/gaia-ops/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
  245. package/dist/gaia-ops/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
  246. package/dist/gaia-ops/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
  247. package/dist/gaia-ops/tools/fast-queries/run_triage.sh +59 -0
  248. package/dist/gaia-ops/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
  249. package/dist/gaia-ops/tools/gaia_simulator/__init__.py +33 -0
  250. package/dist/gaia-ops/tools/gaia_simulator/cli.py +354 -0
  251. package/dist/gaia-ops/tools/gaia_simulator/extractor.py +457 -0
  252. package/dist/gaia-ops/tools/gaia_simulator/reporter.py +258 -0
  253. package/dist/gaia-ops/tools/gaia_simulator/routing_simulator.py +334 -0
  254. package/dist/gaia-ops/tools/gaia_simulator/runner.py +539 -0
  255. package/dist/gaia-ops/tools/gaia_simulator/skills_mapper.py +264 -0
  256. package/dist/gaia-ops/tools/memory/README.md +0 -0
  257. package/dist/gaia-ops/tools/memory/__init__.py +20 -0
  258. package/dist/gaia-ops/tools/memory/backfill_fts5.py +107 -0
  259. package/dist/gaia-ops/tools/memory/conflict_detector.py +295 -0
  260. package/dist/gaia-ops/tools/memory/episodic.py +1210 -0
  261. package/dist/gaia-ops/tools/memory/git_invalidator.py +262 -0
  262. package/dist/gaia-ops/tools/memory/paths.py +102 -0
  263. package/dist/gaia-ops/tools/memory/scoring.py +193 -0
  264. package/dist/gaia-ops/tools/memory/search_store.py +375 -0
  265. package/dist/gaia-ops/tools/persist_transcript_analysis.py +85 -0
  266. package/dist/gaia-ops/tools/review/__init__.py +1 -0
  267. package/dist/gaia-ops/tools/review/review_engine.py +157 -0
  268. package/dist/gaia-ops/tools/scan/__init__.py +35 -0
  269. package/dist/gaia-ops/tools/scan/config.py +247 -0
  270. package/dist/gaia-ops/tools/scan/merge.py +212 -0
  271. package/dist/gaia-ops/tools/scan/orchestrator.py +549 -0
  272. package/dist/gaia-ops/tools/scan/registry.py +127 -0
  273. package/dist/gaia-ops/tools/scan/scanners/__init__.py +18 -0
  274. package/dist/gaia-ops/tools/scan/scanners/base.py +137 -0
  275. package/dist/gaia-ops/tools/scan/scanners/environment.py +349 -0
  276. package/dist/gaia-ops/tools/scan/scanners/git.py +570 -0
  277. package/dist/gaia-ops/tools/scan/scanners/infrastructure.py +875 -0
  278. package/dist/gaia-ops/tools/scan/scanners/orchestration.py +600 -0
  279. package/dist/gaia-ops/tools/scan/scanners/stack.py +1085 -0
  280. package/dist/gaia-ops/tools/scan/scanners/tools.py +260 -0
  281. package/dist/gaia-ops/tools/scan/setup.py +686 -0
  282. package/dist/gaia-ops/tools/scan/tests/__init__.py +1 -0
  283. package/dist/gaia-ops/tools/scan/tests/conftest.py +796 -0
  284. package/dist/gaia-ops/tools/scan/tests/test_environment.py +323 -0
  285. package/dist/gaia-ops/tools/scan/tests/test_git.py +419 -0
  286. package/dist/gaia-ops/tools/scan/tests/test_infrastructure.py +382 -0
  287. package/dist/gaia-ops/tools/scan/tests/test_integration.py +920 -0
  288. package/dist/gaia-ops/tools/scan/tests/test_merge.py +269 -0
  289. package/dist/gaia-ops/tools/scan/tests/test_orchestration.py +304 -0
  290. package/dist/gaia-ops/tools/scan/tests/test_stack.py +604 -0
  291. package/dist/gaia-ops/tools/scan/tests/test_tools.py +349 -0
  292. package/dist/gaia-ops/tools/scan/ui.py +624 -0
  293. package/dist/gaia-ops/tools/scan/verify.py +270 -0
  294. package/dist/gaia-ops/tools/scan/walk.py +118 -0
  295. package/dist/gaia-ops/tools/scan/workspace.py +85 -0
  296. package/dist/gaia-ops/tools/validation/README.md +244 -0
  297. package/dist/gaia-ops/tools/validation/__init__.py +17 -0
  298. package/dist/gaia-ops/tools/validation/approval_gate.py +321 -0
  299. package/dist/gaia-ops/tools/validation/validate_skills.py +189 -0
  300. package/dist/gaia-security/.claude-plugin/plugin.json +24 -0
  301. package/dist/gaia-security/README.md +90 -0
  302. package/dist/gaia-security/config/universal-rules.json +102 -0
  303. package/dist/gaia-security/hooks/adapters/__init__.py +52 -0
  304. package/dist/gaia-security/hooks/adapters/base.py +219 -0
  305. package/dist/gaia-security/hooks/adapters/channel.py +17 -0
  306. package/dist/gaia-security/hooks/adapters/claude_code.py +1890 -0
  307. package/dist/gaia-security/hooks/adapters/types.py +194 -0
  308. package/dist/gaia-security/hooks/adapters/utils.py +25 -0
  309. package/dist/gaia-security/hooks/hooks.json +113 -0
  310. package/dist/gaia-security/hooks/modules/__init__.py +15 -0
  311. package/dist/gaia-security/hooks/modules/agents/__init__.py +29 -0
  312. package/dist/gaia-security/hooks/modules/agents/contract_validator.py +647 -0
  313. package/dist/gaia-security/hooks/modules/agents/response_contract.py +496 -0
  314. package/dist/gaia-security/hooks/modules/agents/skill_injection_verifier.py +120 -0
  315. package/dist/gaia-security/hooks/modules/agents/state_tracker.py +267 -0
  316. package/dist/gaia-security/hooks/modules/agents/task_info_builder.py +74 -0
  317. package/dist/gaia-security/hooks/modules/agents/transcript_analyzer.py +458 -0
  318. package/dist/gaia-security/hooks/modules/agents/transcript_reader.py +152 -0
  319. package/dist/gaia-security/hooks/modules/audit/__init__.py +28 -0
  320. package/dist/gaia-security/hooks/modules/audit/event_detector.py +168 -0
  321. package/dist/gaia-security/hooks/modules/audit/logger.py +131 -0
  322. package/dist/gaia-security/hooks/modules/audit/metrics.py +134 -0
  323. package/dist/gaia-security/hooks/modules/audit/workflow_auditor.py +611 -0
  324. package/dist/gaia-security/hooks/modules/audit/workflow_recorder.py +296 -0
  325. package/dist/gaia-security/hooks/modules/context/__init__.py +11 -0
  326. package/dist/gaia-security/hooks/modules/context/agentic_loop_detector.py +165 -0
  327. package/dist/gaia-security/hooks/modules/context/anchor_tracker.py +317 -0
  328. package/dist/gaia-security/hooks/modules/context/compact_context_builder.py +218 -0
  329. package/dist/gaia-security/hooks/modules/context/context_freshness.py +145 -0
  330. package/dist/gaia-security/hooks/modules/context/context_injector.py +558 -0
  331. package/dist/gaia-security/hooks/modules/context/context_writer.py +530 -0
  332. package/dist/gaia-security/hooks/modules/context/contracts_loader.py +161 -0
  333. package/dist/gaia-security/hooks/modules/core/__init__.py +40 -0
  334. package/dist/gaia-security/hooks/modules/core/hook_entry.py +78 -0
  335. package/dist/gaia-security/hooks/modules/core/paths.py +160 -0
  336. package/dist/gaia-security/hooks/modules/core/plugin_mode.py +149 -0
  337. package/dist/gaia-security/hooks/modules/core/plugin_setup.py +577 -0
  338. package/dist/gaia-security/hooks/modules/core/state.py +179 -0
  339. package/dist/gaia-security/hooks/modules/core/stdin.py +24 -0
  340. package/dist/gaia-security/hooks/modules/events/__init__.py +1 -0
  341. package/dist/gaia-security/hooks/modules/events/event_writer.py +210 -0
  342. package/dist/gaia-security/hooks/modules/memory/__init__.py +8 -0
  343. package/dist/gaia-security/hooks/modules/memory/episode_writer.py +216 -0
  344. package/dist/gaia-security/hooks/modules/orchestrator/__init__.py +1 -0
  345. package/dist/gaia-security/hooks/modules/orchestrator/delegate_mode.py +122 -0
  346. package/dist/gaia-security/hooks/modules/scanning/__init__.py +8 -0
  347. package/dist/gaia-security/hooks/modules/scanning/scan_trigger.py +84 -0
  348. package/dist/gaia-security/hooks/modules/security/__init__.py +120 -0
  349. package/dist/gaia-security/hooks/modules/security/approval_cleanup.py +87 -0
  350. package/dist/gaia-security/hooks/modules/security/approval_constants.py +23 -0
  351. package/dist/gaia-security/hooks/modules/security/approval_grants.py +1638 -0
  352. package/dist/gaia-security/hooks/modules/security/approval_messages.py +71 -0
  353. package/dist/gaia-security/hooks/modules/security/approval_scopes.py +222 -0
  354. package/dist/gaia-security/hooks/modules/security/blocked_commands.py +595 -0
  355. package/dist/gaia-security/hooks/modules/security/blocked_message_formatter.py +87 -0
  356. package/dist/gaia-security/hooks/modules/security/command_semantics.py +181 -0
  357. package/dist/gaia-security/hooks/modules/security/composition_rules.py +547 -0
  358. package/dist/gaia-security/hooks/modules/security/flag_classifiers.py +873 -0
  359. package/dist/gaia-security/hooks/modules/security/gitops_validator.py +179 -0
  360. package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +1131 -0
  361. package/dist/gaia-security/hooks/modules/security/network_hosts.py +481 -0
  362. package/dist/gaia-security/hooks/modules/security/prompt_validator.py +40 -0
  363. package/dist/gaia-security/hooks/modules/security/shell_unwrapper.py +165 -0
  364. package/dist/gaia-security/hooks/modules/security/tiers.py +196 -0
  365. package/dist/gaia-security/hooks/modules/session/__init__.py +10 -0
  366. package/dist/gaia-security/hooks/modules/session/pending_scanner.py +174 -0
  367. package/dist/gaia-security/hooks/modules/session/session_context_writer.py +100 -0
  368. package/dist/gaia-security/hooks/modules/session/session_event_injector.py +160 -0
  369. package/dist/gaia-security/hooks/modules/session/session_manager.py +31 -0
  370. package/dist/gaia-security/hooks/modules/session/session_registry.py +333 -0
  371. package/dist/gaia-security/hooks/modules/tools/__init__.py +29 -0
  372. package/dist/gaia-security/hooks/modules/tools/bash_validator.py +1008 -0
  373. package/dist/gaia-security/hooks/modules/tools/cloud_pipe_validator.py +231 -0
  374. package/dist/gaia-security/hooks/modules/tools/hook_response.py +55 -0
  375. package/dist/gaia-security/hooks/modules/tools/shell_parser.py +227 -0
  376. package/dist/gaia-security/hooks/modules/tools/stage_decomposer.py +315 -0
  377. package/dist/gaia-security/hooks/modules/tools/task_validator.py +294 -0
  378. package/dist/gaia-security/hooks/modules/validation/__init__.py +23 -0
  379. package/dist/gaia-security/hooks/modules/validation/commit_validator.py +380 -0
  380. package/dist/gaia-security/hooks/post_tool_use.py +54 -0
  381. package/dist/gaia-security/hooks/pre_tool_use.py +413 -0
  382. package/dist/gaia-security/hooks/session_end_hook.py +77 -0
  383. package/dist/gaia-security/hooks/session_start.py +81 -0
  384. package/dist/gaia-security/hooks/stop_hook.py +70 -0
  385. package/dist/gaia-security/hooks/user_prompt_submit.py +246 -0
  386. package/dist/gaia-security/settings.json +58 -0
  387. package/git-hooks/commit-msg +41 -0
  388. package/hooks/README.md +100 -0
  389. package/hooks/adapters/__init__.py +52 -0
  390. package/hooks/adapters/base.py +219 -0
  391. package/hooks/adapters/channel.py +17 -0
  392. package/hooks/adapters/claude_code.py +1890 -0
  393. package/hooks/adapters/types.py +194 -0
  394. package/hooks/adapters/utils.py +25 -0
  395. package/hooks/elicitation_result.py +179 -0
  396. package/hooks/hooks.json +84 -0
  397. package/hooks/modules/README.md +189 -0
  398. package/hooks/modules/__init__.py +15 -0
  399. package/hooks/modules/agents/__init__.py +29 -0
  400. package/hooks/modules/agents/contract_validator.py +647 -0
  401. package/hooks/modules/agents/response_contract.py +496 -0
  402. package/hooks/modules/agents/skill_injection_verifier.py +120 -0
  403. package/hooks/modules/agents/state_tracker.py +267 -0
  404. package/hooks/modules/agents/task_info_builder.py +74 -0
  405. package/hooks/modules/agents/transcript_analyzer.py +458 -0
  406. package/hooks/modules/agents/transcript_reader.py +152 -0
  407. package/hooks/modules/audit/__init__.py +28 -0
  408. package/hooks/modules/audit/event_detector.py +168 -0
  409. package/hooks/modules/audit/logger.py +131 -0
  410. package/hooks/modules/audit/metrics.py +134 -0
  411. package/hooks/modules/audit/workflow_auditor.py +611 -0
  412. package/hooks/modules/audit/workflow_recorder.py +296 -0
  413. package/hooks/modules/context/__init__.py +11 -0
  414. package/hooks/modules/context/agentic_loop_detector.py +165 -0
  415. package/hooks/modules/context/anchor_tracker.py +317 -0
  416. package/hooks/modules/context/compact_context_builder.py +218 -0
  417. package/hooks/modules/context/context_freshness.py +145 -0
  418. package/hooks/modules/context/context_injector.py +558 -0
  419. package/hooks/modules/context/context_writer.py +530 -0
  420. package/hooks/modules/context/contracts_loader.py +161 -0
  421. package/hooks/modules/core/__init__.py +40 -0
  422. package/hooks/modules/core/hook_entry.py +78 -0
  423. package/hooks/modules/core/paths.py +160 -0
  424. package/hooks/modules/core/plugin_mode.py +149 -0
  425. package/hooks/modules/core/plugin_setup.py +577 -0
  426. package/hooks/modules/core/state.py +179 -0
  427. package/hooks/modules/core/stdin.py +24 -0
  428. package/hooks/modules/events/__init__.py +1 -0
  429. package/hooks/modules/events/event_writer.py +210 -0
  430. package/hooks/modules/evidence/__init__.py +34 -0
  431. package/hooks/modules/evidence/assertions.py +137 -0
  432. package/hooks/modules/evidence/index_writer.py +57 -0
  433. package/hooks/modules/evidence/loader.py +126 -0
  434. package/hooks/modules/evidence/runner.py +241 -0
  435. package/hooks/modules/memory/__init__.py +8 -0
  436. package/hooks/modules/memory/episode_writer.py +216 -0
  437. package/hooks/modules/orchestrator/__init__.py +1 -0
  438. package/hooks/modules/orchestrator/delegate_mode.py +122 -0
  439. package/hooks/modules/scanning/__init__.py +8 -0
  440. package/hooks/modules/scanning/scan_trigger.py +84 -0
  441. package/hooks/modules/security/__init__.py +120 -0
  442. package/hooks/modules/security/approval_cleanup.py +87 -0
  443. package/hooks/modules/security/approval_constants.py +23 -0
  444. package/hooks/modules/security/approval_grants.py +1638 -0
  445. package/hooks/modules/security/approval_messages.py +71 -0
  446. package/hooks/modules/security/approval_scopes.py +222 -0
  447. package/hooks/modules/security/blocked_commands.py +595 -0
  448. package/hooks/modules/security/blocked_message_formatter.py +87 -0
  449. package/hooks/modules/security/command_semantics.py +181 -0
  450. package/hooks/modules/security/composition_rules.py +547 -0
  451. package/hooks/modules/security/flag_classifiers.py +873 -0
  452. package/hooks/modules/security/gitops_validator.py +179 -0
  453. package/hooks/modules/security/mutative_verbs.py +1131 -0
  454. package/hooks/modules/security/network_hosts.py +481 -0
  455. package/hooks/modules/security/prompt_validator.py +40 -0
  456. package/hooks/modules/security/shell_unwrapper.py +165 -0
  457. package/hooks/modules/security/tiers.py +196 -0
  458. package/hooks/modules/session/__init__.py +10 -0
  459. package/hooks/modules/session/pending_scanner.py +174 -0
  460. package/hooks/modules/session/session_context_writer.py +100 -0
  461. package/hooks/modules/session/session_event_injector.py +160 -0
  462. package/hooks/modules/session/session_manager.py +31 -0
  463. package/hooks/modules/session/session_registry.py +333 -0
  464. package/hooks/modules/tools/__init__.py +29 -0
  465. package/hooks/modules/tools/bash_validator.py +1008 -0
  466. package/hooks/modules/tools/cloud_pipe_validator.py +231 -0
  467. package/hooks/modules/tools/hook_response.py +55 -0
  468. package/hooks/modules/tools/shell_parser.py +227 -0
  469. package/hooks/modules/tools/stage_decomposer.py +315 -0
  470. package/hooks/modules/tools/task_validator.py +294 -0
  471. package/hooks/modules/validation/__init__.py +23 -0
  472. package/hooks/modules/validation/commit_validator.py +380 -0
  473. package/hooks/post_compact.py +43 -0
  474. package/hooks/post_tool_use.py +54 -0
  475. package/hooks/pre_compact.py +60 -0
  476. package/hooks/pre_tool_use.py +413 -0
  477. package/hooks/session_end_hook.py +77 -0
  478. package/hooks/session_start.py +81 -0
  479. package/hooks/stop_hook.py +70 -0
  480. package/hooks/subagent_start.py +71 -0
  481. package/hooks/subagent_stop.py +295 -0
  482. package/hooks/task_completed.py +70 -0
  483. package/hooks/user_prompt_submit.py +246 -0
  484. package/index.js +83 -0
  485. package/package.json +103 -0
  486. package/pyproject.toml +32 -0
  487. package/skills/README.md +158 -0
  488. package/skills/agent-creation/SKILL.md +87 -0
  489. package/skills/agent-creation/examples.md +170 -0
  490. package/skills/agent-creation/reference.md +191 -0
  491. package/skills/agent-protocol/SKILL.md +93 -0
  492. package/skills/agent-protocol/examples.md +223 -0
  493. package/skills/agent-response/SKILL.md +69 -0
  494. package/skills/agentic-loop/SKILL.md +80 -0
  495. package/skills/agentic-loop/reference.md +378 -0
  496. package/skills/blog-writing/SKILL.md +98 -0
  497. package/skills/blog-writing/reference.md +130 -0
  498. package/skills/brief-spec/SKILL.md +185 -0
  499. package/skills/command-execution/SKILL.md +64 -0
  500. package/skills/command-execution/reference.md +83 -0
  501. package/skills/context-updater/SKILL.md +87 -0
  502. package/skills/context-updater/examples.md +71 -0
  503. package/skills/developer-patterns/SKILL.md +50 -0
  504. package/skills/developer-patterns/reference.md +112 -0
  505. package/skills/execution/SKILL.md +99 -0
  506. package/skills/fast-queries/SKILL.md +43 -0
  507. package/skills/gaia-compact/SKILL.md +74 -0
  508. package/skills/gaia-patterns/SKILL.md +108 -0
  509. package/skills/gaia-patterns/reference.md +395 -0
  510. package/skills/gaia-planner/SKILL.md +37 -0
  511. package/skills/gaia-planner/reference.md +107 -0
  512. package/skills/gaia-release/SKILL.md +85 -0
  513. package/skills/gaia-release/reference.md +92 -0
  514. package/skills/gaia-self-check/SKILL.md +114 -0
  515. package/skills/gaia-self-check/reference.md +453 -0
  516. package/skills/gaia-verify/SKILL.md +77 -0
  517. package/skills/gaia-verify/reference.md +80 -0
  518. package/skills/git-conventions/SKILL.md +47 -0
  519. package/skills/gitops-patterns/SKILL.md +60 -0
  520. package/skills/gitops-patterns/reference.md +183 -0
  521. package/skills/gmail-policy/SKILL.md +200 -0
  522. package/skills/gmail-policy/reference.md +150 -0
  523. package/skills/gmail-triage/SKILL.md +100 -0
  524. package/skills/gws-setup/SKILL.md +99 -0
  525. package/skills/gws-setup/reference.md +73 -0
  526. package/skills/investigation/SKILL.md +100 -0
  527. package/skills/memory-curation/SKILL.md +83 -0
  528. package/skills/memory-search/SKILL.md +88 -0
  529. package/skills/orchestrator-approval/SKILL.md +160 -0
  530. package/skills/orchestrator-approval/reference.md +174 -0
  531. package/skills/pending-approvals/SKILL.md +72 -0
  532. package/skills/pending-approvals/reference.md +214 -0
  533. package/skills/readme-writing/SKILL.md +71 -0
  534. package/skills/readme-writing/reference.md +188 -0
  535. package/skills/reference.md +135 -0
  536. package/skills/request-approval/SKILL.md +140 -0
  537. package/skills/request-approval/examples.md +140 -0
  538. package/skills/request-approval/reference.md +57 -0
  539. package/skills/schedule-task/SKILL.md +64 -0
  540. package/skills/schedule-task/reference.md +233 -0
  541. package/skills/security-tiers/SKILL.md +141 -0
  542. package/skills/security-tiers/destructive-commands-reference.md +623 -0
  543. package/skills/security-tiers/reference.md +39 -0
  544. package/skills/session-reflection/SKILL.md +69 -0
  545. package/skills/skill-creation/SKILL.md +92 -0
  546. package/skills/skill-creation/reference.md +29 -0
  547. package/skills/terraform-patterns/SKILL.md +89 -0
  548. package/skills/terraform-patterns/reference.md +93 -0
  549. package/templates/README.md +69 -0
  550. package/templates/managed-settings.template.json +43 -0
  551. package/tools/__init__.py +9 -0
  552. package/tools/agentic-loop/decide-status.py +210 -0
  553. package/tools/agentic-loop/parse-metric.py +106 -0
  554. package/tools/agentic-loop/record-iteration.py +221 -0
  555. package/tools/context/README.md +132 -0
  556. package/tools/context/__init__.py +42 -0
  557. package/tools/context/_paths.py +20 -0
  558. package/tools/context/context_provider.py +721 -0
  559. package/tools/context/context_section_reader.py +342 -0
  560. package/tools/context/deep_merge.py +159 -0
  561. package/tools/context/pending_updates.py +760 -0
  562. package/tools/context/surface_router.py +278 -0
  563. package/tools/fast-queries/README.md +65 -0
  564. package/tools/fast-queries/__init__.py +30 -0
  565. package/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
  566. package/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
  567. package/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
  568. package/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
  569. package/tools/fast-queries/run_triage.sh +59 -0
  570. package/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
  571. package/tools/gaia_simulator/__init__.py +33 -0
  572. package/tools/gaia_simulator/cli.py +354 -0
  573. package/tools/gaia_simulator/extractor.py +457 -0
  574. package/tools/gaia_simulator/reporter.py +258 -0
  575. package/tools/gaia_simulator/routing_simulator.py +334 -0
  576. package/tools/gaia_simulator/runner.py +539 -0
  577. package/tools/gaia_simulator/skills_mapper.py +264 -0
  578. package/tools/memory/README.md +0 -0
  579. package/tools/memory/__init__.py +20 -0
  580. package/tools/memory/backfill_fts5.py +107 -0
  581. package/tools/memory/conflict_detector.py +295 -0
  582. package/tools/memory/episodic.py +1210 -0
  583. package/tools/memory/git_invalidator.py +262 -0
  584. package/tools/memory/paths.py +102 -0
  585. package/tools/memory/scoring.py +193 -0
  586. package/tools/memory/search_store.py +375 -0
  587. package/tools/persist_transcript_analysis.py +85 -0
  588. package/tools/review/__init__.py +1 -0
  589. package/tools/review/review_engine.py +157 -0
  590. package/tools/scan/__init__.py +35 -0
  591. package/tools/scan/config.py +247 -0
  592. package/tools/scan/merge.py +212 -0
  593. package/tools/scan/orchestrator.py +549 -0
  594. package/tools/scan/registry.py +127 -0
  595. package/tools/scan/scanners/__init__.py +18 -0
  596. package/tools/scan/scanners/base.py +137 -0
  597. package/tools/scan/scanners/environment.py +349 -0
  598. package/tools/scan/scanners/git.py +570 -0
  599. package/tools/scan/scanners/infrastructure.py +875 -0
  600. package/tools/scan/scanners/orchestration.py +600 -0
  601. package/tools/scan/scanners/stack.py +1085 -0
  602. package/tools/scan/scanners/tools.py +260 -0
  603. package/tools/scan/setup.py +686 -0
  604. package/tools/scan/tests/__init__.py +1 -0
  605. package/tools/scan/tests/conftest.py +796 -0
  606. package/tools/scan/tests/test_environment.py +323 -0
  607. package/tools/scan/tests/test_git.py +419 -0
  608. package/tools/scan/tests/test_infrastructure.py +382 -0
  609. package/tools/scan/tests/test_integration.py +920 -0
  610. package/tools/scan/tests/test_merge.py +269 -0
  611. package/tools/scan/tests/test_orchestration.py +304 -0
  612. package/tools/scan/tests/test_stack.py +604 -0
  613. package/tools/scan/tests/test_tools.py +349 -0
  614. package/tools/scan/ui.py +624 -0
  615. package/tools/scan/verify.py +270 -0
  616. package/tools/scan/walk.py +118 -0
  617. package/tools/scan/workspace.py +85 -0
  618. package/tools/validation/README.md +244 -0
  619. package/tools/validation/__init__.py +17 -0
  620. package/tools/validation/approval_gate.py +321 -0
  621. package/tools/validation/validate_skills.py +189 -0
@@ -0,0 +1,740 @@
1
+ """
2
+ gaia approvals -- Approval System v2 Track 1 CLI subcommand.
3
+
4
+ Subcommands:
5
+ list [--json] [--session SESSION_ID] [--orphans-only]
6
+ -- list pending approvals
7
+ (--orphans-only filters to
8
+ pendings from dead sessions)
9
+ show APPROVAL_ID [--json] -- show full detail of one approval
10
+ reject NONCE [--reason REASON] -- reject a pending approval
11
+ reject --all [--reason REASON] -- reject ALL pending approvals in one call
12
+ clean [--dry-run] -- remove expired/stale approvals
13
+ stats [--json] -- approval system statistics
14
+
15
+ All subcommands exit 0 on success, 1 on error.
16
+ """
17
+
18
+ import argparse
19
+ import json
20
+ import os
21
+ import sys
22
+ import time
23
+ from pathlib import Path
24
+
25
+ # Ensure hooks/ is on sys.path so approval_grants resolves correctly.
26
+ # This mirrors the pattern used in bin/gaia-scan.py.
27
+ _SCRIPT_DIR = Path(__file__).resolve().parent
28
+ _BIN_DIR = _SCRIPT_DIR.parent
29
+ _PLUGIN_ROOT = _BIN_DIR.parent
30
+ _HOOKS_DIR = _PLUGIN_ROOT / "hooks"
31
+
32
+ for _p in [str(_HOOKS_DIR), str(_PLUGIN_ROOT)]:
33
+ if _p not in sys.path:
34
+ sys.path.insert(0, _p)
35
+
36
+
37
+ def _import_approval_grants():
38
+ """Import approval_grants lazily to allow mocking in tests."""
39
+ from modules.security.approval_grants import (
40
+ cleanup_expired_grants,
41
+ get_pending_approvals_for_session,
42
+ load_pending_by_nonce_prefix,
43
+ reject_pending,
44
+ )
45
+ return {
46
+ "cleanup_expired_grants": cleanup_expired_grants,
47
+ "get_pending_approvals_for_session": get_pending_approvals_for_session,
48
+ "load_pending_by_nonce_prefix": load_pending_by_nonce_prefix,
49
+ "reject_pending": reject_pending,
50
+ }
51
+
52
+
53
+ def _import_grants_dir():
54
+ """Get the grants directory path for approval files.
55
+
56
+ Resolution order mirrors get_plugin_data_dir() in paths.py:
57
+ 1. CLAUDE_PLUGIN_DATA env var (set by Claude Code at runtime) -- data
58
+ lives at <CLAUDE_PLUGIN_DATA>/cache/approvals/.
59
+ 2. Delegate to the approval_grants module which calls get_plugin_data_dir(),
60
+ which in turn walks up from CWD to find .claude/.
61
+
62
+ Keeping CLAUDE_PLUGIN_DATA as the first check ensures the CLI finds the
63
+ same approvals directory the hooks use when invoked from any working
64
+ directory (e.g. from inside gaia-ops-dev/ during development).
65
+ """
66
+ import os
67
+ plugin_data = os.environ.get("CLAUDE_PLUGIN_DATA")
68
+ if plugin_data:
69
+ return Path(plugin_data) / "cache" / "approvals"
70
+ from modules.security.approval_grants import _get_grants_dir
71
+ return _get_grants_dir()
72
+
73
+
74
+ def _import_approval_grants_module():
75
+ """Return the approval_grants module object for direct attribute access.
76
+
77
+ Separate from _import_approval_grants() so cmd_clean can reset
78
+ _last_cleanup_time and call cleanup_expired_grants atomically on the
79
+ same module reference. Kept as a separate injectable function so tests
80
+ can mock it without touching sys.modules.
81
+ """
82
+ import modules.security.approval_grants as ag_mod
83
+ return ag_mod
84
+
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # Formatting helpers
88
+ # ---------------------------------------------------------------------------
89
+
90
+ def _format_age(seconds: float) -> str:
91
+ """Format seconds into a human-readable age string."""
92
+ if seconds < 60:
93
+ return f"{int(seconds)}s"
94
+ if seconds < 3600:
95
+ return f"{int(seconds / 60)}m"
96
+ if seconds < 86400:
97
+ return f"{int(seconds / 3600)}h"
98
+ return f"{int(seconds / 86400)}d"
99
+
100
+
101
+ def _nonce_short(nonce: str) -> str:
102
+ """Return the 8-char short form used in P-XXXX display."""
103
+ return nonce[:8] if nonce else "?"
104
+
105
+
106
+ def _approval_id_label(nonce: str) -> str:
107
+ """Return the P-XXXX label for display."""
108
+ return f"P-{_nonce_short(nonce)}"
109
+
110
+
111
+ def _pending_to_display(p: dict) -> dict:
112
+ """Convert a raw pending dict to a display-friendly dict."""
113
+ nonce = p.get("nonce", "")
114
+ ts = float(p.get("timestamp", 0))
115
+ age_secs = time.time() - ts if ts else 0
116
+ ctx = p.get("context") or {}
117
+ return {
118
+ "approval_id": _approval_id_label(nonce),
119
+ "nonce_prefix": _nonce_short(nonce),
120
+ "command": p.get("command", ""),
121
+ "verb": p.get("danger_verb", ""),
122
+ "category": p.get("danger_category", ""),
123
+ "age": _format_age(age_secs),
124
+ "age_seconds": round(age_secs),
125
+ "session_id": p.get("session_id", ""),
126
+ "source": ctx.get("source", ""),
127
+ "description": ctx.get("description", ""),
128
+ "risk": ctx.get("risk", ""),
129
+ "rollback": ctx.get("rollback", ""),
130
+ "branch": ctx.get("branch", ""),
131
+ "files_changed": ctx.get("files_changed", []),
132
+ "scope_type": p.get("scope_type", ""),
133
+ "timestamp": ts,
134
+ }
135
+
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # Subcommand: list
139
+ # ---------------------------------------------------------------------------
140
+
141
+ def _scan_pending_shared(exclude_live_sessions: bool = False) -> list:
142
+ """Return all non-expired, non-rejected pending approvals across all sessions.
143
+
144
+ Thin wrapper around the shared ``scan_pending_approvals`` in
145
+ ``modules.session.pending_scanner`` so CLI and hook consumers share one
146
+ implementation of pending discovery + liveness filtering.
147
+
148
+ When ``exclude_live_sessions=True``, only pendings whose owning session
149
+ is NOT currently alive (orphans) are returned — this backs the
150
+ ``--orphans-only`` flag.
151
+
152
+ Raises:
153
+ Exception: propagated from ``_import_grants_dir()`` so ``cmd_list``
154
+ can catch it and return exit code 1 consistently.
155
+ """
156
+ # Let ImportError / other failures from _import_grants_dir propagate up.
157
+ grants_dir = _import_grants_dir()
158
+
159
+ from modules.session.pending_scanner import scan_pending_approvals
160
+
161
+ scanned = scan_pending_approvals(
162
+ grants_dir, exclude_live_sessions=exclude_live_sessions
163
+ )
164
+
165
+ # scan_pending_approvals returns a display-ish shape; we rehydrate each
166
+ # scanned result back into the on-disk pending dict keys that
167
+ # _pending_to_display expects. Single source of truth for the scan,
168
+ # but the CLI's display contract is preserved unchanged.
169
+ results = []
170
+ for s in scanned:
171
+ results.append({
172
+ "nonce": s.get("nonce_full") or s.get("nonce_short", ""),
173
+ "session_id": s.get("pending_session_id", ""),
174
+ "command": s.get("command", ""),
175
+ "danger_verb": s.get("verb", ""),
176
+ "danger_category": s.get("category", ""),
177
+ "scope_type": s.get("scope_type", ""),
178
+ "timestamp": s.get("timestamp", 0),
179
+ "context": s.get("context", {}),
180
+ })
181
+
182
+ results.sort(key=lambda d: d.get("timestamp", 0), reverse=True)
183
+ return results
184
+
185
+
186
+ def cmd_list(args) -> int:
187
+ """List pending approvals.
188
+
189
+ Without ``--session``, all sessions are shown so the CLI is useful as a
190
+ cross-session review tool. With ``--session SESSION_ID``, only that
191
+ session's approvals are shown.
192
+
193
+ With ``--orphans-only``, only pendings whose owning session is no longer
194
+ alive (per session_registry) are shown. This is the operator-facing
195
+ tool for diagnosing cross-session drift after the T11/T12 liveness
196
+ plumbing landed.
197
+ """
198
+ session_id = getattr(args, "session", None)
199
+ orphans_only = getattr(args, "orphans_only", False)
200
+
201
+ try:
202
+ if session_id is None:
203
+ # All sessions -- scan directly so we don't filter by current session.
204
+ raw = _scan_pending_shared(exclude_live_sessions=orphans_only)
205
+ else:
206
+ ag = _import_approval_grants()
207
+ raw = ag["get_pending_approvals_for_session"](session_id)
208
+ except Exception as exc:
209
+ _print_error(f"Failed to load approvals: {exc}", args)
210
+ return 1
211
+
212
+ items = [_pending_to_display(p) for p in raw]
213
+
214
+ if getattr(args, "json", False):
215
+ print(json.dumps({"pending": items, "count": len(items)}, indent=2))
216
+ return 0
217
+
218
+ if not items:
219
+ print("No pending approvals.")
220
+ return 0
221
+
222
+ # Table output
223
+ print(f"{'ID':<12} {'AGE':<6} {'VERB':<10} {'SOURCE':<16} COMMAND")
224
+ print("-" * 70)
225
+ for item in items:
226
+ cmd_preview = item["command"][:40]
227
+ source = item["source"][:14] if item["source"] else "-"
228
+ print(
229
+ f"{item['approval_id']:<12} "
230
+ f"{item['age']:<6} "
231
+ f"{item['verb']:<10} "
232
+ f"{source:<16} "
233
+ f"{cmd_preview}"
234
+ )
235
+ print(f"\n{len(items)} pending approval(s).")
236
+ return 0
237
+
238
+
239
+ # ---------------------------------------------------------------------------
240
+ # Subcommand: show
241
+ # ---------------------------------------------------------------------------
242
+
243
+ def cmd_show(args) -> int:
244
+ """Show full details of a specific pending approval."""
245
+ approval_id: str = args.approval_id.lstrip("P-").lstrip("p-")
246
+ # Strip leading 'P-' prefix if present
247
+ if approval_id.upper().startswith("P-"):
248
+ approval_id = approval_id[2:]
249
+
250
+ try:
251
+ ag = _import_approval_grants()
252
+ raw = ag["load_pending_by_nonce_prefix"](approval_id)
253
+ except Exception as exc:
254
+ _print_error(f"Failed to load approval: {exc}", args)
255
+ return 1
256
+
257
+ if raw is None:
258
+ _print_error(f"No pending approval found for ID: P-{approval_id}", args)
259
+ return 1
260
+
261
+ item = _pending_to_display(raw)
262
+ env = raw.get("environment") or {}
263
+ cwd = raw.get("cwd", "")
264
+
265
+ if getattr(args, "json", False):
266
+ detail = dict(item)
267
+ detail["environment"] = env
268
+ detail["cwd"] = cwd
269
+ print(json.dumps(detail, indent=2))
270
+ return 0
271
+
272
+ # Human-readable detail
273
+ lines = [
274
+ f"Approval {item['approval_id']}",
275
+ "",
276
+ f" Command : {item['command']}",
277
+ f" Verb : {item['verb']} ({item['category']})",
278
+ f" Age : {item['age']}",
279
+ f" Session : {item['session_id']}",
280
+ f" Scope type: {item['scope_type']}",
281
+ ]
282
+ if item["source"]:
283
+ lines.append(f" Source : {item['source']}")
284
+ if item["description"] and item["description"] != item["command"]:
285
+ lines.append(f" Desc : {item['description']}")
286
+ if item["risk"]:
287
+ lines.append(f" Risk : {item['risk']}")
288
+ if item["rollback"]:
289
+ lines.append(f" Rollback : {item['rollback']}")
290
+ if item["branch"]:
291
+ lines.append(f" Branch : {item['branch']}")
292
+ if item["files_changed"]:
293
+ lines.append(f" Files : {', '.join(item['files_changed'])}")
294
+ if cwd:
295
+ lines.append(f" CWD : {cwd}")
296
+ if env:
297
+ lines.append(f" Env keys : {', '.join(sorted(env.keys()))}")
298
+ lines.append("")
299
+ lines.append(f" To reject : gaia approvals reject {approval_id}")
300
+ print("\n".join(lines))
301
+ return 0
302
+
303
+
304
+ # ---------------------------------------------------------------------------
305
+ # Subcommand: reject
306
+ # ---------------------------------------------------------------------------
307
+
308
+ def cmd_reject(args) -> int:
309
+ """Reject a pending approval by nonce prefix, or all pending approvals.
310
+
311
+ With ``--all``: rejects every non-expired pending approval across all
312
+ sessions. Exits 0 whether or not any approvals existed.
313
+
314
+ Without ``--all``: rejects the single approval identified by NONCE
315
+ (P-XXXX label or raw hex prefix). Exits 1 when not found.
316
+ """
317
+ reject_all = getattr(args, "all", False)
318
+ reason = getattr(args, "reason", None)
319
+
320
+ if reject_all:
321
+ return _cmd_reject_all(args, reason)
322
+
323
+ # Single-reject path (original behavior)
324
+ nonce = getattr(args, "nonce", None)
325
+ if nonce is None:
326
+ _print_error("NONCE is required when --all is not specified.", args)
327
+ return 1
328
+
329
+ nonce = nonce.strip()
330
+ # Accept P-XXXX or raw hex prefix
331
+ if nonce.upper().startswith("P-"):
332
+ nonce = nonce[2:]
333
+
334
+ try:
335
+ ag = _import_approval_grants()
336
+ ok = ag["reject_pending"](nonce)
337
+ except Exception as exc:
338
+ _print_error(f"Failed to reject approval: {exc}", args)
339
+ return 1
340
+
341
+ if ok:
342
+ msg = f"Rejected P-{nonce}"
343
+ if reason:
344
+ msg += f" (reason: {reason})"
345
+ if getattr(args, "json", False):
346
+ print(json.dumps({"status": "rejected", "nonce_prefix": nonce, "reason": reason}))
347
+ else:
348
+ print(msg)
349
+ return 0
350
+ else:
351
+ _print_error(f"No pending approval found for P-{nonce}", args)
352
+ return 1
353
+
354
+
355
+ def _cmd_reject_all(args, reason: str | None) -> int:
356
+ """Reject all pending approvals across all sessions.
357
+
358
+ Scans the same queue that ``gaia approvals list`` shows, then calls
359
+ ``reject_pending`` for each non-expired, non-rejected pending approval.
360
+ Exits 0 always -- an empty queue is not an error.
361
+ """
362
+ try:
363
+ # Bulk reject operates on the full queue regardless of liveness;
364
+ # we intentionally pass exclude_live_sessions=False so the operator
365
+ # can clear orphaned and live-session pendings in one call.
366
+ raw = _scan_pending_shared(exclude_live_sessions=False)
367
+ except Exception as exc:
368
+ _print_error(f"Failed to load approvals: {exc}", args)
369
+ return 1
370
+
371
+ if not raw:
372
+ if getattr(args, "json", False):
373
+ print(json.dumps({"status": "ok", "rejected": 0, "ids": []}))
374
+ else:
375
+ print("No pending approvals to reject.")
376
+ return 0
377
+
378
+ try:
379
+ ag = _import_approval_grants()
380
+ reject_fn = ag["reject_pending"]
381
+ except Exception as exc:
382
+ _print_error(f"Failed to load approval module: {exc}", args)
383
+ return 1
384
+
385
+ rejected_ids = []
386
+ failed_ids = []
387
+ for pending in raw:
388
+ nonce = pending.get("nonce", "")
389
+ nonce_prefix = _nonce_short(nonce)
390
+ try:
391
+ ok = reject_fn(nonce_prefix)
392
+ if ok:
393
+ rejected_ids.append(f"P-{nonce_prefix}")
394
+ else:
395
+ failed_ids.append(f"P-{nonce_prefix}")
396
+ except Exception:
397
+ failed_ids.append(f"P-{nonce_prefix}")
398
+
399
+ n = len(rejected_ids)
400
+ if getattr(args, "json", False):
401
+ payload: dict = {
402
+ "status": "ok" if not failed_ids else "partial",
403
+ "rejected": n,
404
+ "ids": rejected_ids,
405
+ }
406
+ if reason:
407
+ payload["reason"] = reason
408
+ if failed_ids:
409
+ payload["failed"] = failed_ids
410
+ print(json.dumps(payload))
411
+ else:
412
+ summary = f"Rejected {n} approval(s): {', '.join(rejected_ids)}"
413
+ if reason:
414
+ summary += f" (reason: {reason})"
415
+ print(summary)
416
+ if failed_ids:
417
+ _print_error(f"Failed to reject: {', '.join(failed_ids)}", args)
418
+
419
+ return 0 if not failed_ids else 1
420
+
421
+
422
+ # ---------------------------------------------------------------------------
423
+ # Subcommand: clean
424
+ # ---------------------------------------------------------------------------
425
+
426
+ def cmd_clean(args) -> int:
427
+ """Remove expired and stale approvals."""
428
+ dry_run = getattr(args, "dry_run", False)
429
+
430
+ if dry_run:
431
+ # Inspect without deleting -- count files that would be removed
432
+ try:
433
+ grants_dir = _import_grants_dir()
434
+ except Exception as exc:
435
+ _print_error(f"Cannot access approvals directory: {exc}", args)
436
+ return 1
437
+
438
+ if not grants_dir.exists():
439
+ msg = "Approvals directory does not exist. Nothing to clean."
440
+ if getattr(args, "json", False):
441
+ print(json.dumps({"dry_run": True, "would_remove": 0, "message": msg}))
442
+ else:
443
+ print(msg)
444
+ return 0
445
+
446
+ would_remove = _count_stale_files(grants_dir)
447
+ if getattr(args, "json", False):
448
+ print(json.dumps({"dry_run": True, "would_remove": would_remove}))
449
+ else:
450
+ print(f"Dry run: {would_remove} expired/stale file(s) would be removed.")
451
+ return 0
452
+
453
+ # Real cleanup -- reset throttle to force run
454
+ try:
455
+ ag_mod = _import_approval_grants_module()
456
+ ag_mod._last_cleanup_time = 0.0
457
+ cleaned = ag_mod.cleanup_expired_grants()
458
+ except Exception as exc:
459
+ _print_error(f"Cleanup failed: {exc}", args)
460
+ return 1
461
+
462
+ if getattr(args, "json", False):
463
+ print(json.dumps({"status": "ok", "cleaned": cleaned}))
464
+ else:
465
+ print(f"Cleaned {cleaned} expired/stale approval file(s).")
466
+ return 0
467
+
468
+
469
+ def _count_stale_files(grants_dir: Path) -> int:
470
+ """Count expired grant and pending files without deleting them."""
471
+ count = 0
472
+ now = time.time()
473
+
474
+ for f in grants_dir.glob("grant-*.json"):
475
+ try:
476
+ data = json.loads(f.read_text())
477
+ granted_at = float(data.get("granted_at", 0))
478
+ ttl = int(data.get("ttl_minutes", 5))
479
+ if ttl > 0 and (now - granted_at) / 60 > ttl:
480
+ count += 1
481
+ except Exception:
482
+ count += 1
483
+
484
+ for f in grants_dir.glob("pending-*.json"):
485
+ if "index" in f.name:
486
+ continue
487
+ try:
488
+ data = json.loads(f.read_text())
489
+ if data.get("status") == "rejected":
490
+ count += 1
491
+ continue
492
+ ts = float(data.get("timestamp", 0))
493
+ ttl = int(data.get("ttl_minutes", 5))
494
+ if ttl > 0 and (now - ts) / 60 > ttl:
495
+ count += 1
496
+ except Exception:
497
+ count += 1
498
+
499
+ return count
500
+
501
+
502
+ # ---------------------------------------------------------------------------
503
+ # Subcommand: stats
504
+ # ---------------------------------------------------------------------------
505
+
506
+ def cmd_stats(args) -> int:
507
+ """Show approval system statistics."""
508
+ try:
509
+ ag = _import_approval_grants()
510
+ grants_dir = _import_grants_dir()
511
+ except Exception as exc:
512
+ _print_error(f"Failed to access approval system: {exc}", args)
513
+ return 1
514
+
515
+ # Gather data
516
+ all_sessions_pending = []
517
+ active_grants = []
518
+ rejected_count = 0
519
+ expired_pending_count = 0
520
+ now = time.time()
521
+
522
+ if grants_dir.exists():
523
+ for f in grants_dir.glob("pending-*.json"):
524
+ if "index" in f.name:
525
+ continue
526
+ try:
527
+ data = json.loads(f.read_text())
528
+ if data.get("status") == "rejected":
529
+ rejected_count += 1
530
+ continue
531
+ ts = float(data.get("timestamp", 0))
532
+ ttl = int(data.get("ttl_minutes", 5))
533
+ if ttl > 0 and (now - ts) / 60 > ttl:
534
+ expired_pending_count += 1
535
+ continue
536
+ all_sessions_pending.append(data)
537
+ except Exception:
538
+ pass
539
+
540
+ for f in grants_dir.glob("grant-*.json"):
541
+ try:
542
+ data = json.loads(f.read_text())
543
+ granted_at = float(data.get("granted_at", 0))
544
+ ttl = int(data.get("ttl_minutes", 5))
545
+ if ttl == 0 or (now - granted_at) / 60 <= ttl:
546
+ active_grants.append(data)
547
+ except Exception:
548
+ pass
549
+
550
+ # Current session pending
551
+ session_pending = ag["get_pending_approvals_for_session"]()
552
+
553
+ # Verb breakdown
554
+ verb_counts: dict = {}
555
+ for p in all_sessions_pending:
556
+ verb = p.get("danger_verb", "unknown")
557
+ verb_counts[verb] = verb_counts.get(verb, 0) + 1
558
+
559
+ stats = {
560
+ "pending_current_session": len(session_pending),
561
+ "pending_all_sessions": len(all_sessions_pending),
562
+ "active_grants": len(active_grants),
563
+ "rejected": rejected_count,
564
+ "expired_pending": expired_pending_count,
565
+ "verb_breakdown": verb_counts,
566
+ }
567
+
568
+ if getattr(args, "json", False):
569
+ print(json.dumps(stats, indent=2))
570
+ return 0
571
+
572
+ print("Approval System Stats")
573
+ print("---------------------")
574
+ print(f" Pending (this session) : {stats['pending_current_session']}")
575
+ print(f" Pending (all sessions) : {stats['pending_all_sessions']}")
576
+ print(f" Active grants : {stats['active_grants']}")
577
+ print(f" Rejected (pending) : {stats['rejected']}")
578
+ print(f" Expired (pending) : {stats['expired_pending']}")
579
+ if verb_counts:
580
+ print(" Verb breakdown:")
581
+ for verb, cnt in sorted(verb_counts.items(), key=lambda x: -x[1]):
582
+ print(f" {verb:<16} {cnt}")
583
+ return 0
584
+
585
+
586
+ # ---------------------------------------------------------------------------
587
+ # Error helper
588
+ # ---------------------------------------------------------------------------
589
+
590
+ def _print_error(msg: str, args=None) -> None:
591
+ """Print error in the appropriate format."""
592
+ if args and getattr(args, "json", False):
593
+ print(json.dumps({"error": msg}))
594
+ else:
595
+ print(f"Error: {msg}", file=sys.stderr)
596
+
597
+
598
+ # ---------------------------------------------------------------------------
599
+ # Plugin registration (called by bin/gaia dispatcher)
600
+ # ---------------------------------------------------------------------------
601
+
602
+ def register(subparsers) -> None:
603
+ """Register the 'approvals' subcommand group with the root parser."""
604
+ p = subparsers.add_parser(
605
+ "approvals",
606
+ help="Manage T3 pending approvals",
607
+ description="View, reject, and clean up Gaia approval requests.",
608
+ )
609
+ sub = p.add_subparsers(dest="approvals_cmd", metavar="SUBCOMMAND")
610
+ sub.required = True
611
+
612
+ # list
613
+ p_list = sub.add_parser("list", help="List pending approvals")
614
+ p_list.add_argument("--json", action="store_true", help="JSON output")
615
+ p_list.add_argument("--session", metavar="SESSION_ID", help="Filter by session ID")
616
+ p_list.add_argument(
617
+ "--orphans-only",
618
+ action="store_true",
619
+ dest="orphans_only",
620
+ help="Show only pendings from sessions no longer alive (via session_registry)",
621
+ )
622
+ p_list.set_defaults(func=cmd_list)
623
+
624
+ # show
625
+ p_show = sub.add_parser("show", help="Show detail for a specific approval")
626
+ p_show.add_argument("approval_id", metavar="APPROVAL_ID", help="P-XXXX identifier or nonce prefix")
627
+ p_show.add_argument("--json", action="store_true", help="JSON output")
628
+ p_show.set_defaults(func=cmd_show)
629
+
630
+ # reject
631
+ p_reject = sub.add_parser(
632
+ "reject",
633
+ help="Reject a pending approval (or all with --all)",
634
+ description=(
635
+ "Reject a pending T3 approval.\n\n"
636
+ "Single reject: provide NONCE (P-XXXX or raw hex prefix).\n"
637
+ "Bulk reject: use --all to reject every pending approval in one call."
638
+ ),
639
+ )
640
+ p_reject.add_argument(
641
+ "nonce",
642
+ metavar="NONCE",
643
+ nargs="?",
644
+ help="P-XXXX identifier or nonce prefix (omit when using --all)",
645
+ )
646
+ p_reject.add_argument(
647
+ "--all",
648
+ action="store_true",
649
+ dest="all",
650
+ help="Reject ALL pending approvals (ignores NONCE)",
651
+ )
652
+ p_reject.add_argument("--reason", metavar="REASON", help="Rejection reason applied to all rejected approvals")
653
+ p_reject.add_argument("--json", action="store_true", help="JSON output")
654
+ p_reject.set_defaults(func=cmd_reject)
655
+
656
+ # clean
657
+ p_clean = sub.add_parser("clean", help="Remove expired/stale approvals")
658
+ p_clean.add_argument("--dry-run", action="store_true", dest="dry_run",
659
+ help="Show what would be removed without deleting")
660
+ p_clean.add_argument("--json", action="store_true", help="JSON output")
661
+ p_clean.set_defaults(func=cmd_clean)
662
+
663
+ # stats
664
+ p_stats = sub.add_parser("stats", help="Show approval system statistics")
665
+ p_stats.add_argument("--json", action="store_true", help="JSON output")
666
+ p_stats.set_defaults(func=cmd_stats)
667
+
668
+ p.set_defaults(func=_approvals_default)
669
+
670
+
671
+ def cmd_approvals(args) -> int:
672
+ """Top-level dispatcher for 'gaia approvals'.
673
+
674
+ Called by bin/gaia which invokes cmd_{subcommand}(args). For grouped
675
+ subcommands like approvals, this function delegates to the specific
676
+ handler set via set_defaults(func=...) in register().
677
+ """
678
+ func = getattr(args, "func", None)
679
+ if func is not None and func is not _approvals_default:
680
+ return func(args)
681
+ return _approvals_default(args)
682
+
683
+
684
+ def _approvals_default(args) -> int:
685
+ """Default handler when no sub-subcommand is given."""
686
+ print("Usage: gaia approvals {list,show,reject,clean,stats} [options]")
687
+ print(" gaia approvals reject --all [--reason TEXT] # bulk reject")
688
+ print("Run 'gaia approvals --help' for more information.")
689
+ return 0
690
+
691
+
692
+ # ---------------------------------------------------------------------------
693
+ # Standalone shim (for development/testing without bin/gaia)
694
+ # ---------------------------------------------------------------------------
695
+
696
+ def _build_standalone_parser() -> argparse.ArgumentParser:
697
+ parser = argparse.ArgumentParser(
698
+ prog="python bin/cli/approvals.py",
699
+ description="Gaia approvals subcommand (standalone mode)",
700
+ )
701
+ subparsers = parser.add_subparsers(dest="approvals_cmd", metavar="SUBCOMMAND")
702
+ subparsers.required = True
703
+
704
+ p_list = subparsers.add_parser("list", help="List pending approvals")
705
+ p_list.add_argument("--json", action="store_true")
706
+ p_list.add_argument("--session", metavar="SESSION_ID")
707
+ p_list.add_argument(
708
+ "--orphans-only", action="store_true", dest="orphans_only",
709
+ help="Show only pendings from sessions no longer alive",
710
+ )
711
+ p_list.set_defaults(func=cmd_list)
712
+
713
+ p_show = subparsers.add_parser("show", help="Show approval detail")
714
+ p_show.add_argument("approval_id", metavar="APPROVAL_ID")
715
+ p_show.add_argument("--json", action="store_true")
716
+ p_show.set_defaults(func=cmd_show)
717
+
718
+ p_reject = subparsers.add_parser("reject", help="Reject a pending approval (or all with --all)")
719
+ p_reject.add_argument("nonce", metavar="NONCE", nargs="?")
720
+ p_reject.add_argument("--all", action="store_true", dest="all", help="Reject all pending approvals")
721
+ p_reject.add_argument("--reason", metavar="REASON")
722
+ p_reject.add_argument("--json", action="store_true")
723
+ p_reject.set_defaults(func=cmd_reject)
724
+
725
+ p_clean = subparsers.add_parser("clean", help="Remove expired approvals")
726
+ p_clean.add_argument("--dry-run", action="store_true", dest="dry_run")
727
+ p_clean.add_argument("--json", action="store_true")
728
+ p_clean.set_defaults(func=cmd_clean)
729
+
730
+ p_stats = subparsers.add_parser("stats", help="Approval system stats")
731
+ p_stats.add_argument("--json", action="store_true")
732
+ p_stats.set_defaults(func=cmd_stats)
733
+
734
+ return parser
735
+
736
+
737
+ if __name__ == "__main__":
738
+ parser = _build_standalone_parser()
739
+ parsed = parser.parse_args()
740
+ sys.exit(parsed.func(parsed))