@jaguilar87/gaia 5.0.0-rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (609) hide show
  1. package/.claude-plugin/marketplace.json +33 -0
  2. package/.claude-plugin/plugin.json +26 -0
  3. package/ARCHITECTURE.md +335 -0
  4. package/CHANGELOG.md +1212 -0
  5. package/CODE_OF_CONDUCT.md +11 -0
  6. package/CONTRIBUTING.md +146 -0
  7. package/INSTALL.md +436 -0
  8. package/LICENSE +21 -0
  9. package/README.md +222 -0
  10. package/SECURITY.md +47 -0
  11. package/agents/README.md +78 -0
  12. package/agents/cloud-troubleshooter.md +73 -0
  13. package/agents/developer.md +65 -0
  14. package/agents/gaia-operator.md +64 -0
  15. package/agents/gaia-orchestrator.md +237 -0
  16. package/agents/gaia-planner.md +53 -0
  17. package/agents/gaia-system.md +70 -0
  18. package/agents/gitops-operator.md +61 -0
  19. package/agents/terraform-architect.md +63 -0
  20. package/bin/README.md +106 -0
  21. package/bin/cli/__init__.py +1 -0
  22. package/bin/cli/approvals.py +740 -0
  23. package/bin/cli/cleanup.py +562 -0
  24. package/bin/cli/context.py +283 -0
  25. package/bin/cli/doctor.py +628 -0
  26. package/bin/cli/history.py +305 -0
  27. package/bin/cli/memory.py +464 -0
  28. package/bin/cli/metrics.py +1068 -0
  29. package/bin/cli/plans.py +515 -0
  30. package/bin/cli/status.py +302 -0
  31. package/bin/cli/update.py +382 -0
  32. package/bin/gaia +112 -0
  33. package/bin/gaia-cleanup.js +531 -0
  34. package/bin/gaia-doctor.js +635 -0
  35. package/bin/gaia-evidence +126 -0
  36. package/bin/gaia-history.js +251 -0
  37. package/bin/gaia-metrics.js +1278 -0
  38. package/bin/gaia-review.js +269 -0
  39. package/bin/gaia-scan +44 -0
  40. package/bin/gaia-scan.py +589 -0
  41. package/bin/gaia-skills-diagnose.js +929 -0
  42. package/bin/gaia-status.js +278 -0
  43. package/bin/gaia-uninstall.js +111 -0
  44. package/bin/gaia-update.js +816 -0
  45. package/bin/pre-publish-validate.js +610 -0
  46. package/bin/python-detect.js +60 -0
  47. package/commands/README.md +64 -0
  48. package/commands/gaia.md +37 -0
  49. package/commands/scan-project.md +67 -0
  50. package/config/README.md +71 -0
  51. package/config/cloud/aws.json +134 -0
  52. package/config/cloud/gcp.json +139 -0
  53. package/config/context-contracts.json +158 -0
  54. package/config/crons-schema.md +81 -0
  55. package/config/git_standards.json +72 -0
  56. package/config/surface-routing.json +421 -0
  57. package/config/universal-rules.json +102 -0
  58. package/dist/gaia-ops/.claude-plugin/plugin.json +24 -0
  59. package/dist/gaia-ops/README.md +80 -0
  60. package/dist/gaia-ops/agents/cloud-troubleshooter.md +73 -0
  61. package/dist/gaia-ops/agents/developer.md +65 -0
  62. package/dist/gaia-ops/agents/gaia-operator.md +64 -0
  63. package/dist/gaia-ops/agents/gaia-orchestrator.md +237 -0
  64. package/dist/gaia-ops/agents/gaia-planner.md +53 -0
  65. package/dist/gaia-ops/agents/gaia-system.md +70 -0
  66. package/dist/gaia-ops/agents/gitops-operator.md +61 -0
  67. package/dist/gaia-ops/agents/terraform-architect.md +63 -0
  68. package/dist/gaia-ops/commands/gaia.md +37 -0
  69. package/dist/gaia-ops/config/README.md +71 -0
  70. package/dist/gaia-ops/config/cloud/aws.json +134 -0
  71. package/dist/gaia-ops/config/cloud/gcp.json +139 -0
  72. package/dist/gaia-ops/config/context-contracts.json +158 -0
  73. package/dist/gaia-ops/config/crons-schema.md +81 -0
  74. package/dist/gaia-ops/config/git_standards.json +72 -0
  75. package/dist/gaia-ops/config/surface-routing.json +421 -0
  76. package/dist/gaia-ops/config/universal-rules.json +102 -0
  77. package/dist/gaia-ops/hooks/adapters/__init__.py +52 -0
  78. package/dist/gaia-ops/hooks/adapters/base.py +219 -0
  79. package/dist/gaia-ops/hooks/adapters/channel.py +17 -0
  80. package/dist/gaia-ops/hooks/adapters/claude_code.py +1890 -0
  81. package/dist/gaia-ops/hooks/adapters/types.py +194 -0
  82. package/dist/gaia-ops/hooks/adapters/utils.py +25 -0
  83. package/dist/gaia-ops/hooks/hooks.json +163 -0
  84. package/dist/gaia-ops/hooks/modules/__init__.py +15 -0
  85. package/dist/gaia-ops/hooks/modules/agents/__init__.py +29 -0
  86. package/dist/gaia-ops/hooks/modules/agents/contract_validator.py +647 -0
  87. package/dist/gaia-ops/hooks/modules/agents/response_contract.py +496 -0
  88. package/dist/gaia-ops/hooks/modules/agents/skill_injection_verifier.py +120 -0
  89. package/dist/gaia-ops/hooks/modules/agents/state_tracker.py +267 -0
  90. package/dist/gaia-ops/hooks/modules/agents/task_info_builder.py +74 -0
  91. package/dist/gaia-ops/hooks/modules/agents/transcript_analyzer.py +458 -0
  92. package/dist/gaia-ops/hooks/modules/agents/transcript_reader.py +152 -0
  93. package/dist/gaia-ops/hooks/modules/audit/__init__.py +28 -0
  94. package/dist/gaia-ops/hooks/modules/audit/event_detector.py +168 -0
  95. package/dist/gaia-ops/hooks/modules/audit/logger.py +131 -0
  96. package/dist/gaia-ops/hooks/modules/audit/metrics.py +134 -0
  97. package/dist/gaia-ops/hooks/modules/audit/workflow_auditor.py +611 -0
  98. package/dist/gaia-ops/hooks/modules/audit/workflow_recorder.py +296 -0
  99. package/dist/gaia-ops/hooks/modules/context/__init__.py +11 -0
  100. package/dist/gaia-ops/hooks/modules/context/agentic_loop_detector.py +165 -0
  101. package/dist/gaia-ops/hooks/modules/context/anchor_tracker.py +317 -0
  102. package/dist/gaia-ops/hooks/modules/context/compact_context_builder.py +218 -0
  103. package/dist/gaia-ops/hooks/modules/context/context_freshness.py +145 -0
  104. package/dist/gaia-ops/hooks/modules/context/context_injector.py +558 -0
  105. package/dist/gaia-ops/hooks/modules/context/context_writer.py +530 -0
  106. package/dist/gaia-ops/hooks/modules/context/contracts_loader.py +161 -0
  107. package/dist/gaia-ops/hooks/modules/core/__init__.py +40 -0
  108. package/dist/gaia-ops/hooks/modules/core/hook_entry.py +78 -0
  109. package/dist/gaia-ops/hooks/modules/core/paths.py +160 -0
  110. package/dist/gaia-ops/hooks/modules/core/plugin_mode.py +149 -0
  111. package/dist/gaia-ops/hooks/modules/core/plugin_setup.py +577 -0
  112. package/dist/gaia-ops/hooks/modules/core/state.py +179 -0
  113. package/dist/gaia-ops/hooks/modules/core/stdin.py +24 -0
  114. package/dist/gaia-ops/hooks/modules/events/__init__.py +1 -0
  115. package/dist/gaia-ops/hooks/modules/events/event_writer.py +210 -0
  116. package/dist/gaia-ops/hooks/modules/memory/__init__.py +8 -0
  117. package/dist/gaia-ops/hooks/modules/memory/episode_writer.py +216 -0
  118. package/dist/gaia-ops/hooks/modules/orchestrator/__init__.py +1 -0
  119. package/dist/gaia-ops/hooks/modules/orchestrator/delegate_mode.py +122 -0
  120. package/dist/gaia-ops/hooks/modules/scanning/__init__.py +8 -0
  121. package/dist/gaia-ops/hooks/modules/scanning/scan_trigger.py +84 -0
  122. package/dist/gaia-ops/hooks/modules/security/__init__.py +120 -0
  123. package/dist/gaia-ops/hooks/modules/security/approval_cleanup.py +87 -0
  124. package/dist/gaia-ops/hooks/modules/security/approval_constants.py +23 -0
  125. package/dist/gaia-ops/hooks/modules/security/approval_grants.py +1638 -0
  126. package/dist/gaia-ops/hooks/modules/security/approval_messages.py +71 -0
  127. package/dist/gaia-ops/hooks/modules/security/approval_scopes.py +222 -0
  128. package/dist/gaia-ops/hooks/modules/security/blocked_commands.py +595 -0
  129. package/dist/gaia-ops/hooks/modules/security/blocked_message_formatter.py +87 -0
  130. package/dist/gaia-ops/hooks/modules/security/command_semantics.py +181 -0
  131. package/dist/gaia-ops/hooks/modules/security/composition_rules.py +547 -0
  132. package/dist/gaia-ops/hooks/modules/security/flag_classifiers.py +873 -0
  133. package/dist/gaia-ops/hooks/modules/security/gitops_validator.py +179 -0
  134. package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +1131 -0
  135. package/dist/gaia-ops/hooks/modules/security/network_hosts.py +481 -0
  136. package/dist/gaia-ops/hooks/modules/security/prompt_validator.py +40 -0
  137. package/dist/gaia-ops/hooks/modules/security/shell_unwrapper.py +165 -0
  138. package/dist/gaia-ops/hooks/modules/security/tiers.py +196 -0
  139. package/dist/gaia-ops/hooks/modules/session/__init__.py +10 -0
  140. package/dist/gaia-ops/hooks/modules/session/pending_scanner.py +174 -0
  141. package/dist/gaia-ops/hooks/modules/session/session_context_writer.py +100 -0
  142. package/dist/gaia-ops/hooks/modules/session/session_event_injector.py +160 -0
  143. package/dist/gaia-ops/hooks/modules/session/session_manager.py +31 -0
  144. package/dist/gaia-ops/hooks/modules/session/session_registry.py +232 -0
  145. package/dist/gaia-ops/hooks/modules/tools/__init__.py +29 -0
  146. package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +1008 -0
  147. package/dist/gaia-ops/hooks/modules/tools/cloud_pipe_validator.py +231 -0
  148. package/dist/gaia-ops/hooks/modules/tools/hook_response.py +55 -0
  149. package/dist/gaia-ops/hooks/modules/tools/shell_parser.py +227 -0
  150. package/dist/gaia-ops/hooks/modules/tools/stage_decomposer.py +315 -0
  151. package/dist/gaia-ops/hooks/modules/tools/task_validator.py +294 -0
  152. package/dist/gaia-ops/hooks/modules/validation/__init__.py +23 -0
  153. package/dist/gaia-ops/hooks/modules/validation/commit_validator.py +380 -0
  154. package/dist/gaia-ops/hooks/post_compact.py +43 -0
  155. package/dist/gaia-ops/hooks/post_tool_use.py +54 -0
  156. package/dist/gaia-ops/hooks/pre_compact.py +60 -0
  157. package/dist/gaia-ops/hooks/pre_tool_use.py +413 -0
  158. package/dist/gaia-ops/hooks/session_start.py +81 -0
  159. package/dist/gaia-ops/hooks/stop_hook.py +82 -0
  160. package/dist/gaia-ops/hooks/subagent_start.py +71 -0
  161. package/dist/gaia-ops/hooks/subagent_stop.py +295 -0
  162. package/dist/gaia-ops/hooks/task_completed.py +70 -0
  163. package/dist/gaia-ops/hooks/user_prompt_submit.py +246 -0
  164. package/dist/gaia-ops/settings.json +72 -0
  165. package/dist/gaia-ops/skills/README.md +154 -0
  166. package/dist/gaia-ops/skills/agent-protocol/SKILL.md +93 -0
  167. package/dist/gaia-ops/skills/agent-protocol/examples.md +223 -0
  168. package/dist/gaia-ops/skills/agent-response/SKILL.md +69 -0
  169. package/dist/gaia-ops/skills/agentic-loop/SKILL.md +80 -0
  170. package/dist/gaia-ops/skills/agentic-loop/reference.md +378 -0
  171. package/dist/gaia-ops/skills/blog-writing/SKILL.md +98 -0
  172. package/dist/gaia-ops/skills/blog-writing/reference.md +130 -0
  173. package/dist/gaia-ops/skills/brief-spec/SKILL.md +182 -0
  174. package/dist/gaia-ops/skills/command-execution/SKILL.md +64 -0
  175. package/dist/gaia-ops/skills/command-execution/reference.md +83 -0
  176. package/dist/gaia-ops/skills/context-updater/SKILL.md +87 -0
  177. package/dist/gaia-ops/skills/context-updater/examples.md +71 -0
  178. package/dist/gaia-ops/skills/developer-patterns/SKILL.md +50 -0
  179. package/dist/gaia-ops/skills/developer-patterns/reference.md +112 -0
  180. package/dist/gaia-ops/skills/execution/SKILL.md +99 -0
  181. package/dist/gaia-ops/skills/fast-queries/SKILL.md +43 -0
  182. package/dist/gaia-ops/skills/gaia-compact/SKILL.md +74 -0
  183. package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +108 -0
  184. package/dist/gaia-ops/skills/gaia-patterns/reference.md +395 -0
  185. package/dist/gaia-ops/skills/gaia-planner/SKILL.md +37 -0
  186. package/dist/gaia-ops/skills/gaia-planner/reference.md +107 -0
  187. package/dist/gaia-ops/skills/gaia-release/SKILL.md +82 -0
  188. package/dist/gaia-ops/skills/gaia-release/reference.md +102 -0
  189. package/dist/gaia-ops/skills/gaia-self-check/SKILL.md +114 -0
  190. package/dist/gaia-ops/skills/gaia-self-check/reference.md +453 -0
  191. package/dist/gaia-ops/skills/gaia-verify/SKILL.md +77 -0
  192. package/dist/gaia-ops/skills/gaia-verify/reference.md +80 -0
  193. package/dist/gaia-ops/skills/git-conventions/SKILL.md +47 -0
  194. package/dist/gaia-ops/skills/gitops-patterns/SKILL.md +60 -0
  195. package/dist/gaia-ops/skills/gitops-patterns/reference.md +183 -0
  196. package/dist/gaia-ops/skills/gmail-policy/SKILL.md +200 -0
  197. package/dist/gaia-ops/skills/gmail-policy/reference.md +150 -0
  198. package/dist/gaia-ops/skills/gmail-triage/SKILL.md +100 -0
  199. package/dist/gaia-ops/skills/gws-setup/SKILL.md +99 -0
  200. package/dist/gaia-ops/skills/gws-setup/reference.md +73 -0
  201. package/dist/gaia-ops/skills/investigation/SKILL.md +100 -0
  202. package/dist/gaia-ops/skills/memory-curation/SKILL.md +83 -0
  203. package/dist/gaia-ops/skills/memory-search/SKILL.md +88 -0
  204. package/dist/gaia-ops/skills/orchestrator-approval/SKILL.md +160 -0
  205. package/dist/gaia-ops/skills/orchestrator-approval/reference.md +174 -0
  206. package/dist/gaia-ops/skills/pending-approvals/SKILL.md +72 -0
  207. package/dist/gaia-ops/skills/pending-approvals/reference.md +214 -0
  208. package/dist/gaia-ops/skills/readme-writing/SKILL.md +71 -0
  209. package/dist/gaia-ops/skills/readme-writing/reference.md +188 -0
  210. package/dist/gaia-ops/skills/reference.md +135 -0
  211. package/dist/gaia-ops/skills/request-approval/SKILL.md +140 -0
  212. package/dist/gaia-ops/skills/request-approval/examples.md +140 -0
  213. package/dist/gaia-ops/skills/request-approval/reference.md +57 -0
  214. package/dist/gaia-ops/skills/schedule-task/SKILL.md +64 -0
  215. package/dist/gaia-ops/skills/schedule-task/reference.md +233 -0
  216. package/dist/gaia-ops/skills/security-tiers/SKILL.md +141 -0
  217. package/dist/gaia-ops/skills/security-tiers/destructive-commands-reference.md +623 -0
  218. package/dist/gaia-ops/skills/security-tiers/reference.md +39 -0
  219. package/dist/gaia-ops/skills/skill-creation/SKILL.md +92 -0
  220. package/dist/gaia-ops/skills/skill-creation/reference.md +29 -0
  221. package/dist/gaia-ops/skills/terraform-patterns/SKILL.md +89 -0
  222. package/dist/gaia-ops/skills/terraform-patterns/reference.md +93 -0
  223. package/dist/gaia-ops/tools/__init__.py +9 -0
  224. package/dist/gaia-ops/tools/agentic-loop/decide-status.py +210 -0
  225. package/dist/gaia-ops/tools/agentic-loop/parse-metric.py +106 -0
  226. package/dist/gaia-ops/tools/agentic-loop/record-iteration.py +221 -0
  227. package/dist/gaia-ops/tools/context/README.md +132 -0
  228. package/dist/gaia-ops/tools/context/__init__.py +42 -0
  229. package/dist/gaia-ops/tools/context/_paths.py +20 -0
  230. package/dist/gaia-ops/tools/context/context_provider.py +721 -0
  231. package/dist/gaia-ops/tools/context/context_section_reader.py +342 -0
  232. package/dist/gaia-ops/tools/context/deep_merge.py +159 -0
  233. package/dist/gaia-ops/tools/context/pending_updates.py +760 -0
  234. package/dist/gaia-ops/tools/context/surface_router.py +278 -0
  235. package/dist/gaia-ops/tools/fast-queries/README.md +65 -0
  236. package/dist/gaia-ops/tools/fast-queries/__init__.py +30 -0
  237. package/dist/gaia-ops/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
  238. package/dist/gaia-ops/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
  239. package/dist/gaia-ops/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
  240. package/dist/gaia-ops/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
  241. package/dist/gaia-ops/tools/fast-queries/run_triage.sh +59 -0
  242. package/dist/gaia-ops/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
  243. package/dist/gaia-ops/tools/gaia_simulator/__init__.py +33 -0
  244. package/dist/gaia-ops/tools/gaia_simulator/cli.py +354 -0
  245. package/dist/gaia-ops/tools/gaia_simulator/extractor.py +457 -0
  246. package/dist/gaia-ops/tools/gaia_simulator/reporter.py +258 -0
  247. package/dist/gaia-ops/tools/gaia_simulator/routing_simulator.py +334 -0
  248. package/dist/gaia-ops/tools/gaia_simulator/runner.py +539 -0
  249. package/dist/gaia-ops/tools/gaia_simulator/skills_mapper.py +264 -0
  250. package/dist/gaia-ops/tools/memory/README.md +0 -0
  251. package/dist/gaia-ops/tools/memory/__init__.py +20 -0
  252. package/dist/gaia-ops/tools/memory/backfill_fts5.py +107 -0
  253. package/dist/gaia-ops/tools/memory/conflict_detector.py +295 -0
  254. package/dist/gaia-ops/tools/memory/episodic.py +1210 -0
  255. package/dist/gaia-ops/tools/memory/git_invalidator.py +262 -0
  256. package/dist/gaia-ops/tools/memory/paths.py +102 -0
  257. package/dist/gaia-ops/tools/memory/scoring.py +193 -0
  258. package/dist/gaia-ops/tools/memory/search_store.py +360 -0
  259. package/dist/gaia-ops/tools/persist_transcript_analysis.py +85 -0
  260. package/dist/gaia-ops/tools/review/__init__.py +1 -0
  261. package/dist/gaia-ops/tools/review/review_engine.py +157 -0
  262. package/dist/gaia-ops/tools/scan/__init__.py +35 -0
  263. package/dist/gaia-ops/tools/scan/config.py +247 -0
  264. package/dist/gaia-ops/tools/scan/merge.py +212 -0
  265. package/dist/gaia-ops/tools/scan/orchestrator.py +549 -0
  266. package/dist/gaia-ops/tools/scan/registry.py +127 -0
  267. package/dist/gaia-ops/tools/scan/scanners/__init__.py +18 -0
  268. package/dist/gaia-ops/tools/scan/scanners/base.py +137 -0
  269. package/dist/gaia-ops/tools/scan/scanners/environment.py +349 -0
  270. package/dist/gaia-ops/tools/scan/scanners/git.py +570 -0
  271. package/dist/gaia-ops/tools/scan/scanners/infrastructure.py +875 -0
  272. package/dist/gaia-ops/tools/scan/scanners/orchestration.py +600 -0
  273. package/dist/gaia-ops/tools/scan/scanners/stack.py +1085 -0
  274. package/dist/gaia-ops/tools/scan/scanners/tools.py +260 -0
  275. package/dist/gaia-ops/tools/scan/setup.py +686 -0
  276. package/dist/gaia-ops/tools/scan/tests/__init__.py +1 -0
  277. package/dist/gaia-ops/tools/scan/tests/conftest.py +796 -0
  278. package/dist/gaia-ops/tools/scan/tests/test_environment.py +323 -0
  279. package/dist/gaia-ops/tools/scan/tests/test_git.py +419 -0
  280. package/dist/gaia-ops/tools/scan/tests/test_infrastructure.py +382 -0
  281. package/dist/gaia-ops/tools/scan/tests/test_integration.py +920 -0
  282. package/dist/gaia-ops/tools/scan/tests/test_merge.py +269 -0
  283. package/dist/gaia-ops/tools/scan/tests/test_orchestration.py +304 -0
  284. package/dist/gaia-ops/tools/scan/tests/test_stack.py +604 -0
  285. package/dist/gaia-ops/tools/scan/tests/test_tools.py +349 -0
  286. package/dist/gaia-ops/tools/scan/ui.py +624 -0
  287. package/dist/gaia-ops/tools/scan/verify.py +270 -0
  288. package/dist/gaia-ops/tools/scan/walk.py +118 -0
  289. package/dist/gaia-ops/tools/scan/workspace.py +85 -0
  290. package/dist/gaia-ops/tools/validation/README.md +244 -0
  291. package/dist/gaia-ops/tools/validation/__init__.py +17 -0
  292. package/dist/gaia-ops/tools/validation/approval_gate.py +321 -0
  293. package/dist/gaia-ops/tools/validation/validate_skills.py +189 -0
  294. package/dist/gaia-security/.claude-plugin/plugin.json +24 -0
  295. package/dist/gaia-security/README.md +90 -0
  296. package/dist/gaia-security/config/universal-rules.json +102 -0
  297. package/dist/gaia-security/hooks/adapters/__init__.py +52 -0
  298. package/dist/gaia-security/hooks/adapters/base.py +219 -0
  299. package/dist/gaia-security/hooks/adapters/channel.py +17 -0
  300. package/dist/gaia-security/hooks/adapters/claude_code.py +1890 -0
  301. package/dist/gaia-security/hooks/adapters/types.py +194 -0
  302. package/dist/gaia-security/hooks/adapters/utils.py +25 -0
  303. package/dist/gaia-security/hooks/hooks.json +84 -0
  304. package/dist/gaia-security/hooks/modules/__init__.py +15 -0
  305. package/dist/gaia-security/hooks/modules/agents/__init__.py +29 -0
  306. package/dist/gaia-security/hooks/modules/agents/contract_validator.py +647 -0
  307. package/dist/gaia-security/hooks/modules/agents/response_contract.py +496 -0
  308. package/dist/gaia-security/hooks/modules/agents/skill_injection_verifier.py +120 -0
  309. package/dist/gaia-security/hooks/modules/agents/state_tracker.py +267 -0
  310. package/dist/gaia-security/hooks/modules/agents/task_info_builder.py +74 -0
  311. package/dist/gaia-security/hooks/modules/agents/transcript_analyzer.py +458 -0
  312. package/dist/gaia-security/hooks/modules/agents/transcript_reader.py +152 -0
  313. package/dist/gaia-security/hooks/modules/audit/__init__.py +28 -0
  314. package/dist/gaia-security/hooks/modules/audit/event_detector.py +168 -0
  315. package/dist/gaia-security/hooks/modules/audit/logger.py +131 -0
  316. package/dist/gaia-security/hooks/modules/audit/metrics.py +134 -0
  317. package/dist/gaia-security/hooks/modules/audit/workflow_auditor.py +611 -0
  318. package/dist/gaia-security/hooks/modules/audit/workflow_recorder.py +296 -0
  319. package/dist/gaia-security/hooks/modules/context/__init__.py +11 -0
  320. package/dist/gaia-security/hooks/modules/context/agentic_loop_detector.py +165 -0
  321. package/dist/gaia-security/hooks/modules/context/anchor_tracker.py +317 -0
  322. package/dist/gaia-security/hooks/modules/context/compact_context_builder.py +218 -0
  323. package/dist/gaia-security/hooks/modules/context/context_freshness.py +145 -0
  324. package/dist/gaia-security/hooks/modules/context/context_injector.py +558 -0
  325. package/dist/gaia-security/hooks/modules/context/context_writer.py +530 -0
  326. package/dist/gaia-security/hooks/modules/context/contracts_loader.py +161 -0
  327. package/dist/gaia-security/hooks/modules/core/__init__.py +40 -0
  328. package/dist/gaia-security/hooks/modules/core/hook_entry.py +78 -0
  329. package/dist/gaia-security/hooks/modules/core/paths.py +160 -0
  330. package/dist/gaia-security/hooks/modules/core/plugin_mode.py +149 -0
  331. package/dist/gaia-security/hooks/modules/core/plugin_setup.py +577 -0
  332. package/dist/gaia-security/hooks/modules/core/state.py +179 -0
  333. package/dist/gaia-security/hooks/modules/core/stdin.py +24 -0
  334. package/dist/gaia-security/hooks/modules/events/__init__.py +1 -0
  335. package/dist/gaia-security/hooks/modules/events/event_writer.py +210 -0
  336. package/dist/gaia-security/hooks/modules/memory/__init__.py +8 -0
  337. package/dist/gaia-security/hooks/modules/memory/episode_writer.py +216 -0
  338. package/dist/gaia-security/hooks/modules/orchestrator/__init__.py +1 -0
  339. package/dist/gaia-security/hooks/modules/orchestrator/delegate_mode.py +122 -0
  340. package/dist/gaia-security/hooks/modules/scanning/__init__.py +8 -0
  341. package/dist/gaia-security/hooks/modules/scanning/scan_trigger.py +84 -0
  342. package/dist/gaia-security/hooks/modules/security/__init__.py +120 -0
  343. package/dist/gaia-security/hooks/modules/security/approval_cleanup.py +87 -0
  344. package/dist/gaia-security/hooks/modules/security/approval_constants.py +23 -0
  345. package/dist/gaia-security/hooks/modules/security/approval_grants.py +1638 -0
  346. package/dist/gaia-security/hooks/modules/security/approval_messages.py +71 -0
  347. package/dist/gaia-security/hooks/modules/security/approval_scopes.py +222 -0
  348. package/dist/gaia-security/hooks/modules/security/blocked_commands.py +595 -0
  349. package/dist/gaia-security/hooks/modules/security/blocked_message_formatter.py +87 -0
  350. package/dist/gaia-security/hooks/modules/security/command_semantics.py +181 -0
  351. package/dist/gaia-security/hooks/modules/security/composition_rules.py +547 -0
  352. package/dist/gaia-security/hooks/modules/security/flag_classifiers.py +873 -0
  353. package/dist/gaia-security/hooks/modules/security/gitops_validator.py +179 -0
  354. package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +1131 -0
  355. package/dist/gaia-security/hooks/modules/security/network_hosts.py +481 -0
  356. package/dist/gaia-security/hooks/modules/security/prompt_validator.py +40 -0
  357. package/dist/gaia-security/hooks/modules/security/shell_unwrapper.py +165 -0
  358. package/dist/gaia-security/hooks/modules/security/tiers.py +196 -0
  359. package/dist/gaia-security/hooks/modules/session/__init__.py +10 -0
  360. package/dist/gaia-security/hooks/modules/session/pending_scanner.py +174 -0
  361. package/dist/gaia-security/hooks/modules/session/session_context_writer.py +100 -0
  362. package/dist/gaia-security/hooks/modules/session/session_event_injector.py +160 -0
  363. package/dist/gaia-security/hooks/modules/session/session_manager.py +31 -0
  364. package/dist/gaia-security/hooks/modules/session/session_registry.py +232 -0
  365. package/dist/gaia-security/hooks/modules/tools/__init__.py +29 -0
  366. package/dist/gaia-security/hooks/modules/tools/bash_validator.py +1008 -0
  367. package/dist/gaia-security/hooks/modules/tools/cloud_pipe_validator.py +231 -0
  368. package/dist/gaia-security/hooks/modules/tools/hook_response.py +55 -0
  369. package/dist/gaia-security/hooks/modules/tools/shell_parser.py +227 -0
  370. package/dist/gaia-security/hooks/modules/tools/stage_decomposer.py +315 -0
  371. package/dist/gaia-security/hooks/modules/tools/task_validator.py +294 -0
  372. package/dist/gaia-security/hooks/modules/validation/__init__.py +23 -0
  373. package/dist/gaia-security/hooks/modules/validation/commit_validator.py +380 -0
  374. package/dist/gaia-security/hooks/post_tool_use.py +54 -0
  375. package/dist/gaia-security/hooks/pre_tool_use.py +413 -0
  376. package/dist/gaia-security/hooks/session_start.py +81 -0
  377. package/dist/gaia-security/hooks/stop_hook.py +82 -0
  378. package/dist/gaia-security/hooks/user_prompt_submit.py +246 -0
  379. package/dist/gaia-security/settings.json +58 -0
  380. package/git-hooks/commit-msg +41 -0
  381. package/hooks/README.md +100 -0
  382. package/hooks/adapters/__init__.py +52 -0
  383. package/hooks/adapters/base.py +219 -0
  384. package/hooks/adapters/channel.py +17 -0
  385. package/hooks/adapters/claude_code.py +1890 -0
  386. package/hooks/adapters/types.py +194 -0
  387. package/hooks/adapters/utils.py +25 -0
  388. package/hooks/elicitation_result.py +179 -0
  389. package/hooks/hooks.json +84 -0
  390. package/hooks/modules/README.md +189 -0
  391. package/hooks/modules/__init__.py +15 -0
  392. package/hooks/modules/agents/__init__.py +29 -0
  393. package/hooks/modules/agents/contract_validator.py +647 -0
  394. package/hooks/modules/agents/response_contract.py +496 -0
  395. package/hooks/modules/agents/skill_injection_verifier.py +120 -0
  396. package/hooks/modules/agents/state_tracker.py +267 -0
  397. package/hooks/modules/agents/task_info_builder.py +74 -0
  398. package/hooks/modules/agents/transcript_analyzer.py +458 -0
  399. package/hooks/modules/agents/transcript_reader.py +152 -0
  400. package/hooks/modules/audit/__init__.py +28 -0
  401. package/hooks/modules/audit/event_detector.py +168 -0
  402. package/hooks/modules/audit/logger.py +131 -0
  403. package/hooks/modules/audit/metrics.py +134 -0
  404. package/hooks/modules/audit/workflow_auditor.py +611 -0
  405. package/hooks/modules/audit/workflow_recorder.py +296 -0
  406. package/hooks/modules/context/__init__.py +11 -0
  407. package/hooks/modules/context/agentic_loop_detector.py +165 -0
  408. package/hooks/modules/context/anchor_tracker.py +317 -0
  409. package/hooks/modules/context/compact_context_builder.py +218 -0
  410. package/hooks/modules/context/context_freshness.py +145 -0
  411. package/hooks/modules/context/context_injector.py +558 -0
  412. package/hooks/modules/context/context_writer.py +530 -0
  413. package/hooks/modules/context/contracts_loader.py +161 -0
  414. package/hooks/modules/core/__init__.py +40 -0
  415. package/hooks/modules/core/hook_entry.py +78 -0
  416. package/hooks/modules/core/paths.py +160 -0
  417. package/hooks/modules/core/plugin_mode.py +149 -0
  418. package/hooks/modules/core/plugin_setup.py +577 -0
  419. package/hooks/modules/core/state.py +179 -0
  420. package/hooks/modules/core/stdin.py +24 -0
  421. package/hooks/modules/events/__init__.py +1 -0
  422. package/hooks/modules/events/event_writer.py +210 -0
  423. package/hooks/modules/evidence/__init__.py +34 -0
  424. package/hooks/modules/evidence/assertions.py +137 -0
  425. package/hooks/modules/evidence/index_writer.py +57 -0
  426. package/hooks/modules/evidence/loader.py +126 -0
  427. package/hooks/modules/evidence/runner.py +241 -0
  428. package/hooks/modules/memory/__init__.py +8 -0
  429. package/hooks/modules/memory/episode_writer.py +216 -0
  430. package/hooks/modules/orchestrator/__init__.py +1 -0
  431. package/hooks/modules/orchestrator/delegate_mode.py +122 -0
  432. package/hooks/modules/scanning/__init__.py +8 -0
  433. package/hooks/modules/scanning/scan_trigger.py +84 -0
  434. package/hooks/modules/security/__init__.py +120 -0
  435. package/hooks/modules/security/approval_cleanup.py +87 -0
  436. package/hooks/modules/security/approval_constants.py +23 -0
  437. package/hooks/modules/security/approval_grants.py +1638 -0
  438. package/hooks/modules/security/approval_messages.py +71 -0
  439. package/hooks/modules/security/approval_scopes.py +222 -0
  440. package/hooks/modules/security/blocked_commands.py +595 -0
  441. package/hooks/modules/security/blocked_message_formatter.py +87 -0
  442. package/hooks/modules/security/command_semantics.py +181 -0
  443. package/hooks/modules/security/composition_rules.py +547 -0
  444. package/hooks/modules/security/flag_classifiers.py +873 -0
  445. package/hooks/modules/security/gitops_validator.py +179 -0
  446. package/hooks/modules/security/mutative_verbs.py +1131 -0
  447. package/hooks/modules/security/network_hosts.py +481 -0
  448. package/hooks/modules/security/prompt_validator.py +40 -0
  449. package/hooks/modules/security/shell_unwrapper.py +165 -0
  450. package/hooks/modules/security/tiers.py +196 -0
  451. package/hooks/modules/session/__init__.py +10 -0
  452. package/hooks/modules/session/pending_scanner.py +174 -0
  453. package/hooks/modules/session/session_context_writer.py +100 -0
  454. package/hooks/modules/session/session_event_injector.py +160 -0
  455. package/hooks/modules/session/session_manager.py +31 -0
  456. package/hooks/modules/session/session_registry.py +232 -0
  457. package/hooks/modules/tools/__init__.py +29 -0
  458. package/hooks/modules/tools/bash_validator.py +1008 -0
  459. package/hooks/modules/tools/cloud_pipe_validator.py +231 -0
  460. package/hooks/modules/tools/hook_response.py +55 -0
  461. package/hooks/modules/tools/shell_parser.py +227 -0
  462. package/hooks/modules/tools/stage_decomposer.py +315 -0
  463. package/hooks/modules/tools/task_validator.py +294 -0
  464. package/hooks/modules/validation/__init__.py +23 -0
  465. package/hooks/modules/validation/commit_validator.py +380 -0
  466. package/hooks/post_compact.py +43 -0
  467. package/hooks/post_tool_use.py +54 -0
  468. package/hooks/pre_compact.py +60 -0
  469. package/hooks/pre_tool_use.py +413 -0
  470. package/hooks/session_start.py +81 -0
  471. package/hooks/stop_hook.py +82 -0
  472. package/hooks/subagent_start.py +71 -0
  473. package/hooks/subagent_stop.py +295 -0
  474. package/hooks/task_completed.py +70 -0
  475. package/hooks/user_prompt_submit.py +246 -0
  476. package/index.js +83 -0
  477. package/package.json +99 -0
  478. package/pyproject.toml +32 -0
  479. package/skills/README.md +154 -0
  480. package/skills/agent-protocol/SKILL.md +93 -0
  481. package/skills/agent-protocol/examples.md +223 -0
  482. package/skills/agent-response/SKILL.md +69 -0
  483. package/skills/agentic-loop/SKILL.md +80 -0
  484. package/skills/agentic-loop/reference.md +378 -0
  485. package/skills/blog-writing/SKILL.md +98 -0
  486. package/skills/blog-writing/reference.md +130 -0
  487. package/skills/brief-spec/SKILL.md +182 -0
  488. package/skills/command-execution/SKILL.md +64 -0
  489. package/skills/command-execution/reference.md +83 -0
  490. package/skills/context-updater/SKILL.md +87 -0
  491. package/skills/context-updater/examples.md +71 -0
  492. package/skills/developer-patterns/SKILL.md +50 -0
  493. package/skills/developer-patterns/reference.md +112 -0
  494. package/skills/execution/SKILL.md +99 -0
  495. package/skills/fast-queries/SKILL.md +43 -0
  496. package/skills/gaia-compact/SKILL.md +74 -0
  497. package/skills/gaia-patterns/SKILL.md +108 -0
  498. package/skills/gaia-patterns/reference.md +395 -0
  499. package/skills/gaia-planner/SKILL.md +37 -0
  500. package/skills/gaia-planner/reference.md +107 -0
  501. package/skills/gaia-release/SKILL.md +82 -0
  502. package/skills/gaia-release/reference.md +102 -0
  503. package/skills/gaia-self-check/SKILL.md +114 -0
  504. package/skills/gaia-self-check/reference.md +453 -0
  505. package/skills/gaia-verify/SKILL.md +77 -0
  506. package/skills/gaia-verify/reference.md +80 -0
  507. package/skills/git-conventions/SKILL.md +47 -0
  508. package/skills/gitops-patterns/SKILL.md +60 -0
  509. package/skills/gitops-patterns/reference.md +183 -0
  510. package/skills/gmail-policy/SKILL.md +200 -0
  511. package/skills/gmail-policy/reference.md +150 -0
  512. package/skills/gmail-triage/SKILL.md +100 -0
  513. package/skills/gws-setup/SKILL.md +99 -0
  514. package/skills/gws-setup/reference.md +73 -0
  515. package/skills/investigation/SKILL.md +100 -0
  516. package/skills/memory-curation/SKILL.md +83 -0
  517. package/skills/memory-search/SKILL.md +88 -0
  518. package/skills/orchestrator-approval/SKILL.md +160 -0
  519. package/skills/orchestrator-approval/reference.md +174 -0
  520. package/skills/pending-approvals/SKILL.md +72 -0
  521. package/skills/pending-approvals/reference.md +214 -0
  522. package/skills/readme-writing/SKILL.md +71 -0
  523. package/skills/readme-writing/reference.md +188 -0
  524. package/skills/reference.md +135 -0
  525. package/skills/request-approval/SKILL.md +140 -0
  526. package/skills/request-approval/examples.md +140 -0
  527. package/skills/request-approval/reference.md +57 -0
  528. package/skills/schedule-task/SKILL.md +64 -0
  529. package/skills/schedule-task/reference.md +233 -0
  530. package/skills/security-tiers/SKILL.md +141 -0
  531. package/skills/security-tiers/destructive-commands-reference.md +623 -0
  532. package/skills/security-tiers/reference.md +39 -0
  533. package/skills/skill-creation/SKILL.md +92 -0
  534. package/skills/skill-creation/reference.md +29 -0
  535. package/skills/terraform-patterns/SKILL.md +89 -0
  536. package/skills/terraform-patterns/reference.md +93 -0
  537. package/templates/README.md +69 -0
  538. package/templates/managed-settings.template.json +43 -0
  539. package/tools/__init__.py +9 -0
  540. package/tools/agentic-loop/decide-status.py +210 -0
  541. package/tools/agentic-loop/parse-metric.py +106 -0
  542. package/tools/agentic-loop/record-iteration.py +221 -0
  543. package/tools/context/README.md +132 -0
  544. package/tools/context/__init__.py +42 -0
  545. package/tools/context/_paths.py +20 -0
  546. package/tools/context/context_provider.py +721 -0
  547. package/tools/context/context_section_reader.py +342 -0
  548. package/tools/context/deep_merge.py +159 -0
  549. package/tools/context/pending_updates.py +760 -0
  550. package/tools/context/surface_router.py +278 -0
  551. package/tools/fast-queries/README.md +65 -0
  552. package/tools/fast-queries/__init__.py +30 -0
  553. package/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
  554. package/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
  555. package/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
  556. package/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
  557. package/tools/fast-queries/run_triage.sh +59 -0
  558. package/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
  559. package/tools/gaia_simulator/__init__.py +33 -0
  560. package/tools/gaia_simulator/cli.py +354 -0
  561. package/tools/gaia_simulator/extractor.py +457 -0
  562. package/tools/gaia_simulator/reporter.py +258 -0
  563. package/tools/gaia_simulator/routing_simulator.py +334 -0
  564. package/tools/gaia_simulator/runner.py +539 -0
  565. package/tools/gaia_simulator/skills_mapper.py +264 -0
  566. package/tools/memory/README.md +0 -0
  567. package/tools/memory/__init__.py +20 -0
  568. package/tools/memory/backfill_fts5.py +107 -0
  569. package/tools/memory/conflict_detector.py +295 -0
  570. package/tools/memory/episodic.py +1210 -0
  571. package/tools/memory/git_invalidator.py +262 -0
  572. package/tools/memory/paths.py +102 -0
  573. package/tools/memory/scoring.py +193 -0
  574. package/tools/memory/search_store.py +360 -0
  575. package/tools/persist_transcript_analysis.py +85 -0
  576. package/tools/review/__init__.py +1 -0
  577. package/tools/review/review_engine.py +157 -0
  578. package/tools/scan/__init__.py +35 -0
  579. package/tools/scan/config.py +247 -0
  580. package/tools/scan/merge.py +212 -0
  581. package/tools/scan/orchestrator.py +549 -0
  582. package/tools/scan/registry.py +127 -0
  583. package/tools/scan/scanners/__init__.py +18 -0
  584. package/tools/scan/scanners/base.py +137 -0
  585. package/tools/scan/scanners/environment.py +349 -0
  586. package/tools/scan/scanners/git.py +570 -0
  587. package/tools/scan/scanners/infrastructure.py +875 -0
  588. package/tools/scan/scanners/orchestration.py +600 -0
  589. package/tools/scan/scanners/stack.py +1085 -0
  590. package/tools/scan/scanners/tools.py +260 -0
  591. package/tools/scan/setup.py +686 -0
  592. package/tools/scan/tests/__init__.py +1 -0
  593. package/tools/scan/tests/conftest.py +796 -0
  594. package/tools/scan/tests/test_environment.py +323 -0
  595. package/tools/scan/tests/test_git.py +419 -0
  596. package/tools/scan/tests/test_infrastructure.py +382 -0
  597. package/tools/scan/tests/test_integration.py +920 -0
  598. package/tools/scan/tests/test_merge.py +269 -0
  599. package/tools/scan/tests/test_orchestration.py +304 -0
  600. package/tools/scan/tests/test_stack.py +604 -0
  601. package/tools/scan/tests/test_tools.py +349 -0
  602. package/tools/scan/ui.py +624 -0
  603. package/tools/scan/verify.py +270 -0
  604. package/tools/scan/walk.py +118 -0
  605. package/tools/scan/workspace.py +85 -0
  606. package/tools/validation/README.md +244 -0
  607. package/tools/validation/__init__.py +17 -0
  608. package/tools/validation/approval_gate.py +321 -0
  609. package/tools/validation/validate_skills.py +189 -0
@@ -0,0 +1,873 @@
1
+ """
2
+ Flag-dependent command classifiers for 15 command families.
3
+
4
+ This module runs in the classify phase BEFORE detect_mutative_command(). When a
5
+ classifier returns a FlagClassifierResult, it overrides verb-based classification.
6
+ When it returns None, the caller falls through to the existing mutative_verbs pipeline.
7
+
8
+ Classification outcomes:
9
+ READ_ONLY -- safe by elimination, no approval required
10
+ MUTATIVE -- state-modifying, requires user approval (T3 nonce)
11
+ BLOCKED -- permanently blocked (exit 2), maps to the same path as blocked_commands
12
+
13
+ Note on BLOCKED overlap with blocked_commands.py:
14
+ blocked_commands.py already permanently blocks:
15
+ - git push --force / -f
16
+ - git reset --hard
17
+ The classifiers for git push and git reset are still present here for
18
+ consistency (they return BLOCKED with the same reason), but blocked_commands.py
19
+ will catch these first in the pipeline. Having both layers is intentional:
20
+ flag_classifiers is the semantic-aware layer; blocked_commands is the
21
+ pattern-level safety net.
22
+
23
+ Dependencies: Python stdlib only.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import re
29
+ import shlex
30
+ from dataclasses import dataclass
31
+ from typing import Callable, Dict, List, Optional, Tuple
32
+
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Outcome constants
36
+ # ---------------------------------------------------------------------------
37
+
38
+ OUTCOME_READ_ONLY = "READ_ONLY"
39
+ OUTCOME_MUTATIVE = "MUTATIVE"
40
+ OUTCOME_BLOCKED = "BLOCKED"
41
+
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Result dataclass
45
+ # ---------------------------------------------------------------------------
46
+
47
+ @dataclass(frozen=True)
48
+ class FlagClassifierResult:
49
+ """Structured result of flag-dependent classification.
50
+
51
+ Attributes:
52
+ outcome: One of OUTCOME_READ_ONLY, OUTCOME_MUTATIVE, or OUTCOME_BLOCKED.
53
+ reason: Human-readable explanation.
54
+ matched_pattern: The specific flag or pattern that triggered this
55
+ classification (e.g. "--force", "-i", "-exec").
56
+ command_family: The command family that handled classification
57
+ (e.g. "git_push", "sed", "curl").
58
+ """
59
+ outcome: str
60
+ reason: str
61
+ matched_pattern: str
62
+ command_family: str
63
+
64
+ @property
65
+ def is_blocked(self) -> bool:
66
+ return self.outcome == OUTCOME_BLOCKED
67
+
68
+ @property
69
+ def is_mutative(self) -> bool:
70
+ return self.outcome in (OUTCOME_MUTATIVE, OUTCOME_BLOCKED)
71
+
72
+ @property
73
+ def is_read_only(self) -> bool:
74
+ return self.outcome == OUTCOME_READ_ONLY
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Token helpers
79
+ # ---------------------------------------------------------------------------
80
+
81
+ def _tokenize(command: str) -> List[str]:
82
+ """Tokenize a shell command using shlex, fallback to split on error."""
83
+ if not command or not command.strip():
84
+ return []
85
+ try:
86
+ return shlex.split(command.strip())
87
+ except ValueError:
88
+ return command.strip().split()
89
+
90
+
91
+ def _has_flag(args: List[str], *flags: str) -> Optional[str]:
92
+ """Return the first matching flag found in args, or None."""
93
+ flag_set = set(flags)
94
+ for a in args:
95
+ if a in flag_set:
96
+ return a
97
+ return None
98
+
99
+
100
+ def _has_short_flag(args: List[str], letter: str) -> bool:
101
+ """Return True if args contain a bundled or standalone short flag.
102
+
103
+ Handles both standalone ("-f") and clustered ("-xf", "-fx") forms.
104
+ """
105
+ needle = f"-{letter}"
106
+ for a in args:
107
+ if a == needle:
108
+ return True
109
+ # Bundled short flags: -xvf contains 'f'
110
+ if len(a) >= 2 and a[0] == "-" and a[1] != "-":
111
+ if letter in a[1:]:
112
+ return True
113
+ return False
114
+
115
+
116
+ # ---------------------------------------------------------------------------
117
+ # Individual classifiers (one per command family)
118
+ # ---------------------------------------------------------------------------
119
+ # Each classifier receives (tokens: List[str]) and returns
120
+ # Optional[FlagClassifierResult]. tokens[0] is the base command (or
121
+ # "git" for git sub-commands).
122
+ #
123
+ # Convention:
124
+ # - The function is named _classify_<family>
125
+ # - It is registered in _CLASSIFIER_REGISTRY below
126
+ # ---------------------------------------------------------------------------
127
+
128
+
129
+ # 1. git push
130
+ def _classify_git_push(tokens: List[str]) -> Optional[FlagClassifierResult]:
131
+ if len(tokens) < 2 or tokens[0] != "git" or tokens[1] != "push":
132
+ return None
133
+ args = tokens[2:]
134
+
135
+ # Force push / history rewrite forms (also caught by blocked_commands.py)
136
+ force_flag = _has_flag(args, "--force", "--mirror", "--prune", "--delete", "-d")
137
+ if force_flag:
138
+ return FlagClassifierResult(
139
+ outcome=OUTCOME_BLOCKED,
140
+ reason=f"git push {force_flag} rewrites/destroys remote history; use --force-with-lease",
141
+ matched_pattern=force_flag,
142
+ command_family="git_push",
143
+ )
144
+ if _has_short_flag(args, "f"):
145
+ return FlagClassifierResult(
146
+ outcome=OUTCOME_BLOCKED,
147
+ reason="git push -f rewrites remote history; use --force-with-lease",
148
+ matched_pattern="-f",
149
+ command_family="git_push",
150
+ )
151
+ # +refspec or :refspec
152
+ for a in args:
153
+ if (a.startswith("+") or a.startswith(":")) and len(a) > 1:
154
+ return FlagClassifierResult(
155
+ outcome=OUTCOME_BLOCKED,
156
+ reason=f"git push {a!r} force-pushes or deletes a remote ref",
157
+ matched_pattern=a,
158
+ command_family="git_push",
159
+ )
160
+
161
+ # Plain push: mutative (needs T3 approval)
162
+ return FlagClassifierResult(
163
+ outcome=OUTCOME_MUTATIVE,
164
+ reason="git push modifies the remote repository",
165
+ matched_pattern="git push",
166
+ command_family="git_push",
167
+ )
168
+
169
+
170
+ # 2. git reset
171
+ def _classify_git_reset(tokens: List[str]) -> Optional[FlagClassifierResult]:
172
+ if len(tokens) < 2 or tokens[0] != "git" or tokens[1] != "reset":
173
+ return None
174
+ args = tokens[2:]
175
+
176
+ if "--hard" in args:
177
+ return FlagClassifierResult(
178
+ outcome=OUTCOME_BLOCKED,
179
+ reason="git reset --hard permanently discards uncommitted changes",
180
+ matched_pattern="--hard",
181
+ command_family="git_reset",
182
+ )
183
+ # --soft and --mixed are recoverable rewrites
184
+ return FlagClassifierResult(
185
+ outcome=OUTCOME_MUTATIVE,
186
+ reason="git reset modifies HEAD or the index",
187
+ matched_pattern="git reset",
188
+ command_family="git_reset",
189
+ )
190
+
191
+
192
+ # 3. git checkout
193
+ def _classify_git_checkout(tokens: List[str]) -> Optional[FlagClassifierResult]:
194
+ if len(tokens) < 2 or tokens[0] != "git" or tokens[1] != "checkout":
195
+ return None
196
+ args = tokens[2:]
197
+
198
+ _DISCARD_FLAGS = {".", "--", "HEAD", "--force", "-f", "--ours", "--theirs"}
199
+ flag = _has_flag(args, *_DISCARD_FLAGS)
200
+ if flag:
201
+ return FlagClassifierResult(
202
+ outcome=OUTCOME_BLOCKED,
203
+ reason=f"git checkout {flag} discards uncommitted changes",
204
+ matched_pattern=flag,
205
+ command_family="git_checkout",
206
+ )
207
+ if _has_short_flag(args, "f"):
208
+ return FlagClassifierResult(
209
+ outcome=OUTCOME_BLOCKED,
210
+ reason="git checkout -f discards uncommitted changes",
211
+ matched_pattern="-f",
212
+ command_family="git_checkout",
213
+ )
214
+
215
+ return FlagClassifierResult(
216
+ outcome=OUTCOME_MUTATIVE,
217
+ reason="git checkout switches branches or restores files",
218
+ matched_pattern="git checkout",
219
+ command_family="git_checkout",
220
+ )
221
+
222
+
223
+ # 4. git stash
224
+ def _classify_git_stash(tokens: List[str]) -> Optional[FlagClassifierResult]:
225
+ if len(tokens) < 2 or tokens[0] != "git" or tokens[1] != "stash":
226
+ return None
227
+ # No sub-command = implicit "push"
228
+ args = tokens[2:]
229
+ sub = args[0].lower() if args else "push"
230
+
231
+ if sub in ("drop", "clear"):
232
+ return FlagClassifierResult(
233
+ outcome=OUTCOME_BLOCKED,
234
+ reason=f"git stash {sub} permanently removes stashed changes",
235
+ matched_pattern=sub,
236
+ command_family="git_stash",
237
+ )
238
+ if sub in ("list", "show"):
239
+ return FlagClassifierResult(
240
+ outcome=OUTCOME_READ_ONLY,
241
+ reason=f"git stash {sub} is read-only",
242
+ matched_pattern=sub,
243
+ command_family="git_stash",
244
+ )
245
+ # push, pop, apply, branch, save
246
+ return FlagClassifierResult(
247
+ outcome=OUTCOME_MUTATIVE,
248
+ reason=f"git stash {sub} modifies the stash or working tree",
249
+ matched_pattern=sub,
250
+ command_family="git_stash",
251
+ )
252
+
253
+
254
+ # 5. git rebase
255
+ def _classify_git_rebase(tokens: List[str]) -> Optional[FlagClassifierResult]:
256
+ if len(tokens) < 2 or tokens[0] != "git" or tokens[1] != "rebase":
257
+ return None
258
+ args = tokens[2:]
259
+
260
+ if "--abort" in args:
261
+ return FlagClassifierResult(
262
+ outcome=OUTCOME_READ_ONLY,
263
+ reason="git rebase --abort cancels in-progress rebase without modifying history",
264
+ matched_pattern="--abort",
265
+ command_family="git_rebase",
266
+ )
267
+ if "--continue" in args or "--skip" in args:
268
+ return FlagClassifierResult(
269
+ outcome=OUTCOME_MUTATIVE,
270
+ reason="git rebase --continue/--skip advances an in-progress rebase",
271
+ matched_pattern="--continue" if "--continue" in args else "--skip",
272
+ command_family="git_rebase",
273
+ )
274
+ if "-i" in args or "--interactive" in args:
275
+ return FlagClassifierResult(
276
+ outcome=OUTCOME_MUTATIVE,
277
+ reason="git rebase -i rewrites commit history interactively",
278
+ matched_pattern="-i" if "-i" in args else "--interactive",
279
+ command_family="git_rebase",
280
+ )
281
+ # Plain rebase
282
+ return FlagClassifierResult(
283
+ outcome=OUTCOME_MUTATIVE,
284
+ reason="git rebase rewrites commit history",
285
+ matched_pattern="git rebase",
286
+ command_family="git_rebase",
287
+ )
288
+
289
+
290
+ # 6. git tag
291
+ def _classify_git_tag(tokens: List[str]) -> Optional[FlagClassifierResult]:
292
+ if len(tokens) < 2 or tokens[0] != "git" or tokens[1] != "tag":
293
+ return None
294
+ args = tokens[2:]
295
+
296
+ if not args:
297
+ return FlagClassifierResult(
298
+ outcome=OUTCOME_READ_ONLY,
299
+ reason="git tag with no arguments lists tags",
300
+ matched_pattern="git tag",
301
+ command_family="git_tag",
302
+ )
303
+
304
+ # Delete or force -- blocked
305
+ has_force = "--force" in args or _has_short_flag(args, "f")
306
+ has_delete = "--delete" in args or _has_short_flag(args, "d")
307
+ if has_force:
308
+ return FlagClassifierResult(
309
+ outcome=OUTCOME_BLOCKED,
310
+ reason="git tag --force rewrites an existing tag",
311
+ matched_pattern="--force",
312
+ command_family="git_tag",
313
+ )
314
+ if has_delete:
315
+ return FlagClassifierResult(
316
+ outcome=OUTCOME_BLOCKED,
317
+ reason="git tag --delete removes a tag",
318
+ matched_pattern="--delete",
319
+ command_family="git_tag",
320
+ )
321
+
322
+ # Listing / verification flags
323
+ _LIST_FLAGS = {"-l", "--list", "-v", "--verify", "--contains", "--no-contains",
324
+ "--merged", "--no-merged", "--points-at"}
325
+ if any(a in _LIST_FLAGS or a.startswith("-n") for a in args):
326
+ return FlagClassifierResult(
327
+ outcome=OUTCOME_READ_ONLY,
328
+ reason="git tag with listing/verification flags is read-only",
329
+ matched_pattern=next(
330
+ (a for a in args if a in _LIST_FLAGS or a.startswith("-n")), "-l"
331
+ ),
332
+ command_family="git_tag",
333
+ )
334
+
335
+ # Creating a tag
336
+ return FlagClassifierResult(
337
+ outcome=OUTCOME_MUTATIVE,
338
+ reason="git tag creates a new tag",
339
+ matched_pattern="git tag",
340
+ command_family="git_tag",
341
+ )
342
+
343
+
344
+ # 7. git clean
345
+ def _classify_git_clean(tokens: List[str]) -> Optional[FlagClassifierResult]:
346
+ if len(tokens) < 2 or tokens[0] != "git" or tokens[1] != "clean":
347
+ return None
348
+ args = tokens[2:]
349
+
350
+ if "--dry-run" in args or _has_short_flag(args, "n"):
351
+ return FlagClassifierResult(
352
+ outcome=OUTCOME_READ_ONLY,
353
+ reason="git clean --dry-run/-n shows what would be removed without deleting",
354
+ matched_pattern="--dry-run" if "--dry-run" in args else "-n",
355
+ command_family="git_clean",
356
+ )
357
+
358
+ # All other forms are destructive (removes untracked files)
359
+ return FlagClassifierResult(
360
+ outcome=OUTCOME_BLOCKED,
361
+ reason="git clean permanently deletes untracked files; use --dry-run first",
362
+ matched_pattern="git clean",
363
+ command_family="git_clean",
364
+ )
365
+
366
+
367
+ # 8. git remote
368
+ def _classify_git_remote(tokens: List[str]) -> Optional[FlagClassifierResult]:
369
+ if len(tokens) < 2 or tokens[0] != "git" or tokens[1] != "remote":
370
+ return None
371
+ args = tokens[2:]
372
+ sub = args[0].lower() if args else ""
373
+
374
+ if sub in ("remove", "rm", "rename", "set-url", "set-head", "set-branches"):
375
+ return FlagClassifierResult(
376
+ outcome=OUTCOME_MUTATIVE,
377
+ reason=f"git remote {sub} modifies remote configuration",
378
+ matched_pattern=sub,
379
+ command_family="git_remote",
380
+ )
381
+ if sub in ("show", "get-url", ""):
382
+ return FlagClassifierResult(
383
+ outcome=OUTCOME_READ_ONLY,
384
+ reason=f"git remote {sub or '(list)'} is read-only",
385
+ matched_pattern=sub or "git remote",
386
+ command_family="git_remote",
387
+ )
388
+ # "add" is mutative
389
+ if sub == "add":
390
+ return FlagClassifierResult(
391
+ outcome=OUTCOME_MUTATIVE,
392
+ reason="git remote add registers a new remote",
393
+ matched_pattern="add",
394
+ command_family="git_remote",
395
+ )
396
+ if sub in ("-v", "--verbose"):
397
+ return FlagClassifierResult(
398
+ outcome=OUTCOME_READ_ONLY,
399
+ reason="git remote -v lists remotes",
400
+ matched_pattern=sub,
401
+ command_family="git_remote",
402
+ )
403
+
404
+ # Unknown sub-command -- fall through
405
+ return None
406
+
407
+
408
+ # 9. sed
409
+ def _classify_sed(tokens: List[str]) -> Optional[FlagClassifierResult]:
410
+ if not tokens or tokens[0] != "sed":
411
+ return None
412
+ args = tokens[1:]
413
+
414
+ # -i / -I / --in-place mean in-place file editing
415
+ flag = _has_flag(args, "-i", "-I", "--in-place")
416
+ if flag:
417
+ return FlagClassifierResult(
418
+ outcome=OUTCOME_MUTATIVE,
419
+ reason=f"sed {flag} edits files in-place",
420
+ matched_pattern=flag,
421
+ command_family="sed",
422
+ )
423
+ # Bundled short flags: -ni (where i is in-place), -in, etc.
424
+ # Also handle -i.bak form (flag with inline backup suffix)
425
+ for a in args:
426
+ if a.startswith("-i") and a != "-i" and not a.startswith("--"):
427
+ # -i.bak, -ibak, etc. -- sed in-place with backup suffix
428
+ return FlagClassifierResult(
429
+ outcome=OUTCOME_MUTATIVE,
430
+ reason=f"sed {a} edits files in-place (with backup suffix)",
431
+ matched_pattern=a,
432
+ command_family="sed",
433
+ )
434
+ if len(a) >= 2 and a[0] == "-" and a[1] != "-" and "i" in a[1:]:
435
+ return FlagClassifierResult(
436
+ outcome=OUTCOME_MUTATIVE,
437
+ reason=f"sed {a} contains -i (in-place editing)",
438
+ matched_pattern=a,
439
+ command_family="sed",
440
+ )
441
+
442
+ return FlagClassifierResult(
443
+ outcome=OUTCOME_READ_ONLY,
444
+ reason="sed without -i writes to stdout, does not modify files",
445
+ matched_pattern="sed",
446
+ command_family="sed",
447
+ )
448
+
449
+
450
+ # 10. awk
451
+ # Pattern matching for awk programs that perform side-effecting operations.
452
+ _AWK_MUTATIVE_PATTERNS = re.compile(
453
+ r"""
454
+ system\s*\( # system("cmd")
455
+ | \|\s*getline # pipe into getline
456
+ | print\s.*> # print expr > file (redirect)
457
+ | print\s.*>> # print expr >> file (append)
458
+ | close\s*\( # close(file/pipe) implies file I/O
459
+ | \|& # two-way pipe (gawk)
460
+ """,
461
+ re.VERBOSE,
462
+ )
463
+
464
+
465
+ def _classify_awk(tokens: List[str]) -> Optional[FlagClassifierResult]:
466
+ if not tokens or tokens[0] not in ("awk", "gawk", "mawk", "nawk"):
467
+ return None
468
+
469
+ # Scan all tokens for the awk program text (first non-flag non-value argument)
470
+ args = tokens[1:]
471
+ i = 0
472
+ while i < len(args):
473
+ a = args[i]
474
+ # Flags that take a value argument: -F, -v, -f, etc.
475
+ if a in ("-F", "-v", "-f", "-i", "-l", "-M", "-m", "-o"):
476
+ i += 2
477
+ continue
478
+ if a.startswith("-") and not a.startswith("--"):
479
+ i += 1
480
+ continue
481
+ if a == "--":
482
+ i += 1
483
+ break
484
+ # First non-flag argument is the program
485
+ program = a
486
+ m = _AWK_MUTATIVE_PATTERNS.search(program)
487
+ if m:
488
+ matched = m.group().strip()
489
+ return FlagClassifierResult(
490
+ outcome=OUTCOME_MUTATIVE,
491
+ reason=f"awk program contains side-effecting construct: {matched!r}",
492
+ matched_pattern=matched,
493
+ command_family="awk",
494
+ )
495
+ # Found the program text but no mutative pattern
496
+ return FlagClassifierResult(
497
+ outcome=OUTCOME_READ_ONLY,
498
+ reason="awk program does not contain file/system side-effects",
499
+ matched_pattern="awk",
500
+ command_family="awk",
501
+ )
502
+
503
+ # Could not identify program text -- treat as read-only (conservative)
504
+ return FlagClassifierResult(
505
+ outcome=OUTCOME_READ_ONLY,
506
+ reason="awk with no identifiable program text is read-only",
507
+ matched_pattern="awk",
508
+ command_family="awk",
509
+ )
510
+
511
+
512
+ # 11. tar
513
+ def _classify_tar(tokens: List[str]) -> Optional[FlagClassifierResult]:
514
+ if not tokens or tokens[0] != "tar":
515
+ return None
516
+ args = tokens[1:]
517
+
518
+ # Long-form operation flags
519
+ long_mutative = _has_flag(args, "--create", "--extract", "--append", "--update",
520
+ "--concatenate", "--delete")
521
+ if long_mutative:
522
+ return FlagClassifierResult(
523
+ outcome=OUTCOME_MUTATIVE,
524
+ reason=f"tar {long_mutative} creates or modifies an archive",
525
+ matched_pattern=long_mutative,
526
+ command_family="tar",
527
+ )
528
+ if _has_flag(args, "--list"):
529
+ return FlagClassifierResult(
530
+ outcome=OUTCOME_READ_ONLY,
531
+ reason="tar --list reads archive contents without extracting",
532
+ matched_pattern="--list",
533
+ command_family="tar",
534
+ )
535
+
536
+ # Short-form operation letters in bundled flags (e.g. -czvf, -tf, -xvf)
537
+ for a in args:
538
+ if len(a) >= 2 and a[0] == "-" and a[1] != "-":
539
+ letters = a[1:]
540
+ if any(c in letters for c in "cxrua"):
541
+ return FlagClassifierResult(
542
+ outcome=OUTCOME_MUTATIVE,
543
+ reason=f"tar {a} creates or modifies an archive",
544
+ matched_pattern=a,
545
+ command_family="tar",
546
+ )
547
+ if "t" in letters:
548
+ return FlagClassifierResult(
549
+ outcome=OUTCOME_READ_ONLY,
550
+ reason=f"tar {a} lists archive contents without extracting",
551
+ matched_pattern=a,
552
+ command_family="tar",
553
+ )
554
+ # GNU tar also accepts bare operation letters without leading dash
555
+ # as the first non-flag argument (e.g. "tar czf out.tar dir")
556
+ if not a.startswith("-") and len(a) >= 1 and a[0] in "cxtrua":
557
+ if any(c in a for c in "cxrua"):
558
+ return FlagClassifierResult(
559
+ outcome=OUTCOME_MUTATIVE,
560
+ reason=f"tar operation '{a[0]}' creates or modifies an archive",
561
+ matched_pattern=a[0],
562
+ command_family="tar",
563
+ )
564
+ if "t" in a:
565
+ return FlagClassifierResult(
566
+ outcome=OUTCOME_READ_ONLY,
567
+ reason="tar operation 't' lists archive contents",
568
+ matched_pattern="t",
569
+ command_family="tar",
570
+ )
571
+ break
572
+
573
+ # Could not determine operation -- conservative: treat as mutative
574
+ return FlagClassifierResult(
575
+ outcome=OUTCOME_MUTATIVE,
576
+ reason="tar with unrecognized operation flags",
577
+ matched_pattern="tar",
578
+ command_family="tar",
579
+ )
580
+
581
+
582
+ # 12. find
583
+ def _classify_find(tokens: List[str]) -> Optional[FlagClassifierResult]:
584
+ if not tokens or tokens[0] != "find":
585
+ return None
586
+ args = tokens[1:]
587
+
588
+ # Actions that execute external commands or delete files
589
+ mutative_actions = ("-exec", "-execdir", "-delete", "-ok", "-okdir", "-fprint",
590
+ "-fprint0", "-fprintf")
591
+ flag = _has_flag(args, *mutative_actions)
592
+ if flag:
593
+ return FlagClassifierResult(
594
+ outcome=OUTCOME_MUTATIVE,
595
+ reason=f"find {flag} executes commands or modifies the filesystem",
596
+ matched_pattern=flag,
597
+ command_family="find",
598
+ )
599
+
600
+ return FlagClassifierResult(
601
+ outcome=OUTCOME_READ_ONLY,
602
+ reason="find without -exec/-delete is read-only",
603
+ matched_pattern="find",
604
+ command_family="find",
605
+ )
606
+
607
+
608
+ # 13. curl
609
+ # HTTP methods that write data to a remote server
610
+ _CURL_WRITE_METHODS = {"POST", "PUT", "DELETE", "PATCH"}
611
+
612
+ def _classify_curl(tokens: List[str]) -> Optional[FlagClassifierResult]:
613
+ if not tokens or tokens[0] != "curl":
614
+ return None
615
+ args = tokens[1:]
616
+ i = 0
617
+ while i < len(args):
618
+ a = args[i]
619
+ # -X / --request METHOD
620
+ if a in ("-X", "--request"):
621
+ if i + 1 < len(args) and args[i + 1].upper() in _CURL_WRITE_METHODS:
622
+ method = args[i + 1].upper()
623
+ return FlagClassifierResult(
624
+ outcome=OUTCOME_MUTATIVE,
625
+ reason=f"curl -X {method} sends a write request",
626
+ matched_pattern=f"-X {method}",
627
+ command_family="curl",
628
+ )
629
+ i += 2
630
+ continue
631
+ # --request=METHOD
632
+ if a.startswith("--request="):
633
+ method = a.split("=", 1)[1].upper()
634
+ if method in _CURL_WRITE_METHODS:
635
+ return FlagClassifierResult(
636
+ outcome=OUTCOME_MUTATIVE,
637
+ reason=f"curl --request={method} sends a write request",
638
+ matched_pattern=f"--request={method}",
639
+ command_family="curl",
640
+ )
641
+ i += 1
642
+ continue
643
+ # Data flags (imply POST)
644
+ if a in ("-d", "--data", "--data-binary", "--data-raw", "--data-urlencode",
645
+ "-F", "--form", "--form-string",
646
+ "-T", "--upload-file", "--json"):
647
+ return FlagClassifierResult(
648
+ outcome=OUTCOME_MUTATIVE,
649
+ reason=f"curl {a} sends data to the server (implies write)",
650
+ matched_pattern=a,
651
+ command_family="curl",
652
+ )
653
+ i += 1
654
+
655
+ # No write indicators found -- network read
656
+ return FlagClassifierResult(
657
+ outcome=OUTCOME_READ_ONLY,
658
+ reason="curl without write flags performs a network read (GET)",
659
+ matched_pattern="curl",
660
+ command_family="curl",
661
+ )
662
+
663
+
664
+ # 14. wget
665
+ _WGET_WRITE_METHODS = {"POST", "PUT", "DELETE", "PATCH"}
666
+
667
+ def _classify_wget(tokens: List[str]) -> Optional[FlagClassifierResult]:
668
+ if not tokens or tokens[0] != "wget":
669
+ return None
670
+ args = tokens[1:]
671
+ i = 0
672
+ while i < len(args):
673
+ a = args[i]
674
+ # --post-data or --post-file
675
+ if a in ("--post-data", "--post-file"):
676
+ return FlagClassifierResult(
677
+ outcome=OUTCOME_MUTATIVE,
678
+ reason=f"wget {a} sends a POST request",
679
+ matched_pattern=a,
680
+ command_family="wget",
681
+ )
682
+ if a.startswith("--post-data=") or a.startswith("--post-file="):
683
+ return FlagClassifierResult(
684
+ outcome=OUTCOME_MUTATIVE,
685
+ reason=f"wget {a.split('=')[0]} sends a POST request",
686
+ matched_pattern=a.split("=")[0],
687
+ command_family="wget",
688
+ )
689
+ # --method=POST/PUT/DELETE/PATCH
690
+ if a == "--method":
691
+ if i + 1 < len(args) and args[i + 1].upper() in _WGET_WRITE_METHODS:
692
+ method = args[i + 1].upper()
693
+ return FlagClassifierResult(
694
+ outcome=OUTCOME_MUTATIVE,
695
+ reason=f"wget --method {method} sends a write request",
696
+ matched_pattern=f"--method {method}",
697
+ command_family="wget",
698
+ )
699
+ i += 2
700
+ continue
701
+ if a.startswith("--method="):
702
+ method = a.split("=", 1)[1].upper()
703
+ if method in _WGET_WRITE_METHODS:
704
+ return FlagClassifierResult(
705
+ outcome=OUTCOME_MUTATIVE,
706
+ reason=f"wget --method={method} sends a write request",
707
+ matched_pattern=f"--method={method}",
708
+ command_family="wget",
709
+ )
710
+ i += 1
711
+ continue
712
+ # --body-data / --body-file (wget2)
713
+ if a in ("--body-data", "--body-file"):
714
+ return FlagClassifierResult(
715
+ outcome=OUTCOME_MUTATIVE,
716
+ reason=f"wget {a} sends request body data",
717
+ matched_pattern=a,
718
+ command_family="wget",
719
+ )
720
+ if a.startswith("--body-data=") or a.startswith("--body-file="):
721
+ return FlagClassifierResult(
722
+ outcome=OUTCOME_MUTATIVE,
723
+ reason=f"wget {a.split('=')[0]} sends request body data",
724
+ matched_pattern=a.split("=")[0],
725
+ command_family="wget",
726
+ )
727
+ i += 1
728
+
729
+ return FlagClassifierResult(
730
+ outcome=OUTCOME_READ_ONLY,
731
+ reason="wget without write flags performs a network read (GET/download)",
732
+ matched_pattern="wget",
733
+ command_family="wget",
734
+ )
735
+
736
+
737
+ # 15. httpie (http / https commands)
738
+ # httpie uses positional method as the first argument: http POST url ...
739
+ # Data items: key=value (string), key:=json (raw JSON), key@file (file)
740
+ _HTTPIE_WRITE_METHODS = {"POST", "PUT", "DELETE", "PATCH"}
741
+
742
+ # Regex to detect httpie data items (key=value, key:=json, key@file)
743
+ _HTTPIE_DATA_ITEM = re.compile(r"^[A-Za-z_][A-Za-z0-9_\-]*(:=|==|=@|:@|@|:=@|=)")
744
+
745
+
746
+ def _classify_httpie(tokens: List[str]) -> Optional[FlagClassifierResult]:
747
+ if not tokens or tokens[0] not in ("http", "https"):
748
+ return None
749
+ args = tokens[1:]
750
+
751
+ # Skip flags (start with -)
752
+ non_flag_args = [a for a in args if not a.startswith("-")]
753
+
754
+ if not non_flag_args:
755
+ # No non-flag arguments at all -- treat as read-only
756
+ return FlagClassifierResult(
757
+ outcome=OUTCOME_READ_ONLY,
758
+ reason="httpie with no positional arguments is read-only",
759
+ matched_pattern="http",
760
+ command_family="httpie",
761
+ )
762
+
763
+ first = non_flag_args[0].upper()
764
+ # If the first positional arg is an HTTP method
765
+ if first in _HTTPIE_WRITE_METHODS:
766
+ return FlagClassifierResult(
767
+ outcome=OUTCOME_MUTATIVE,
768
+ reason=f"httpie {first} sends a write request",
769
+ matched_pattern=first,
770
+ command_family="httpie",
771
+ )
772
+ # HEAD, GET are read-only explicit methods
773
+ if first in ("GET", "HEAD", "OPTIONS"):
774
+ return FlagClassifierResult(
775
+ outcome=OUTCOME_READ_ONLY,
776
+ reason=f"httpie {first} is a read-only method",
777
+ matched_pattern=first,
778
+ command_family="httpie",
779
+ )
780
+
781
+ # No explicit method: check for data items (imply POST)
782
+ for a in non_flag_args[1:]:
783
+ if _HTTPIE_DATA_ITEM.match(a):
784
+ return FlagClassifierResult(
785
+ outcome=OUTCOME_MUTATIVE,
786
+ reason=f"httpie data item {a!r} implies a POST request",
787
+ matched_pattern=a,
788
+ command_family="httpie",
789
+ )
790
+
791
+ # GET or first arg is URL with no data
792
+ return FlagClassifierResult(
793
+ outcome=OUTCOME_READ_ONLY,
794
+ reason="httpie GET request (no write method or data items)",
795
+ matched_pattern="http",
796
+ command_family="httpie",
797
+ )
798
+
799
+
800
+ # ---------------------------------------------------------------------------
801
+ # Registry: maps (base_cmd, optional_subcommand) -> classifier function
802
+ # ---------------------------------------------------------------------------
803
+ # git sub-commands share the "git" base command; they are dispatched via a
804
+ # single "git" entry that delegates to sub-command classifiers.
805
+
806
+ _GIT_SUBCOMMAND_CLASSIFIERS: Dict[str, Callable[[List[str]], Optional[FlagClassifierResult]]] = {
807
+ "push": _classify_git_push,
808
+ "reset": _classify_git_reset,
809
+ "checkout": _classify_git_checkout,
810
+ "stash": _classify_git_stash,
811
+ "rebase": _classify_git_rebase,
812
+ "tag": _classify_git_tag,
813
+ "clean": _classify_git_clean,
814
+ "remote": _classify_git_remote,
815
+ }
816
+
817
+
818
+ def _classify_git_dispatch(tokens: List[str]) -> Optional[FlagClassifierResult]:
819
+ """Dispatch to the appropriate git sub-command classifier."""
820
+ if len(tokens) < 2 or tokens[0] != "git":
821
+ return None
822
+ sub = tokens[1].lower()
823
+ classifier = _GIT_SUBCOMMAND_CLASSIFIERS.get(sub)
824
+ if classifier is None:
825
+ return None
826
+ return classifier(tokens)
827
+
828
+
829
+ # Top-level registry: base command -> classifier
830
+ _CLASSIFIER_REGISTRY: Dict[str, Callable[[List[str]], Optional[FlagClassifierResult]]] = {
831
+ "git": _classify_git_dispatch,
832
+ "sed": _classify_sed,
833
+ "awk": _classify_awk,
834
+ "gawk": _classify_awk,
835
+ "mawk": _classify_awk,
836
+ "nawk": _classify_awk,
837
+ "tar": _classify_tar,
838
+ "find": _classify_find,
839
+ "curl": _classify_curl,
840
+ "wget": _classify_wget,
841
+ "http": _classify_httpie,
842
+ "https": _classify_httpie,
843
+ }
844
+
845
+
846
+ # ---------------------------------------------------------------------------
847
+ # Public API
848
+ # ---------------------------------------------------------------------------
849
+
850
+ def classify_by_flags(command: str) -> Optional[FlagClassifierResult]:
851
+ """Classify a command based on flags and sub-commands.
852
+
853
+ This is the primary entry point. Call this BEFORE detect_mutative_command()
854
+ in the classify phase. If this returns a result, it overrides verb-based
855
+ classification. If it returns None, fall through to the existing pipeline.
856
+
857
+ Args:
858
+ command: The full shell command string (already unwrapped if applicable).
859
+
860
+ Returns:
861
+ FlagClassifierResult if the command belongs to a known family, else None.
862
+ """
863
+ if not command or not command.strip():
864
+ return None
865
+ tokens = _tokenize(command)
866
+ if not tokens:
867
+ return None
868
+
869
+ base_cmd = tokens[0]
870
+ classifier = _CLASSIFIER_REGISTRY.get(base_cmd)
871
+ if classifier is None:
872
+ return None
873
+ return classifier(tokens)