@miller-tech/uap 1.221.0 → 2.0.0

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 (238) hide show
  1. package/README.md +19 -9
  2. package/config/exl3-profiles/qwen38-27b-exl3.env +68 -0
  3. package/config/llama-profiles/qwen38-27b-dflash2.env +6 -1
  4. package/dist/.tsbuildinfo +1 -1
  5. package/dist/benchmarks/paired/adapter.d.ts +15 -0
  6. package/dist/benchmarks/paired/adapter.d.ts.map +1 -1
  7. package/dist/benchmarks/paired/adapter.js +22 -1
  8. package/dist/benchmarks/paired/adapter.js.map +1 -1
  9. package/dist/bin/cli.js +20 -24
  10. package/dist/bin/cli.js.map +1 -1
  11. package/dist/cli/deliver.d.ts +34 -0
  12. package/dist/cli/deliver.d.ts.map +1 -1
  13. package/dist/cli/deliver.js +147 -20
  14. package/dist/cli/deliver.js.map +1 -1
  15. package/dist/cli/guided-setup.d.ts.map +1 -1
  16. package/dist/cli/guided-setup.js +15 -0
  17. package/dist/cli/guided-setup.js.map +1 -1
  18. package/dist/cli/init.d.ts.map +1 -1
  19. package/dist/cli/init.js +3 -0
  20. package/dist/cli/init.js.map +1 -1
  21. package/dist/cli/plan.d.ts.map +1 -1
  22. package/dist/cli/plan.js +8 -2
  23. package/dist/cli/plan.js.map +1 -1
  24. package/dist/cli/policy.d.ts.map +1 -1
  25. package/dist/cli/policy.js +104 -2
  26. package/dist/cli/policy.js.map +1 -1
  27. package/dist/cli/quality.d.ts +11 -0
  28. package/dist/cli/quality.d.ts.map +1 -0
  29. package/dist/cli/quality.js +213 -0
  30. package/dist/cli/quality.js.map +1 -0
  31. package/dist/cli/wizard-config.d.ts.map +1 -1
  32. package/dist/cli/wizard-config.js +19 -9
  33. package/dist/cli/wizard-config.js.map +1 -1
  34. package/dist/cli/worktree.d.ts.map +1 -1
  35. package/dist/cli/worktree.js +8 -1
  36. package/dist/cli/worktree.js.map +1 -1
  37. package/dist/dashboard/savings.d.ts +1 -4
  38. package/dist/dashboard/savings.d.ts.map +1 -1
  39. package/dist/dashboard/savings.js +2 -52
  40. package/dist/dashboard/savings.js.map +1 -1
  41. package/dist/delivery/acceptance-judge.d.ts +10 -0
  42. package/dist/delivery/acceptance-judge.d.ts.map +1 -1
  43. package/dist/delivery/acceptance-judge.js +45 -3
  44. package/dist/delivery/acceptance-judge.js.map +1 -1
  45. package/dist/delivery/agentic-executor.d.ts +12 -1
  46. package/dist/delivery/agentic-executor.d.ts.map +1 -1
  47. package/dist/delivery/agentic-executor.js +25 -5
  48. package/dist/delivery/agentic-executor.js.map +1 -1
  49. package/dist/delivery/bash-sweep.d.ts +12 -0
  50. package/dist/delivery/bash-sweep.d.ts.map +1 -1
  51. package/dist/delivery/bash-sweep.js +8 -1
  52. package/dist/delivery/bash-sweep.js.map +1 -1
  53. package/dist/delivery/convergence-loop.d.ts.map +1 -1
  54. package/dist/delivery/convergence-loop.js +6 -4
  55. package/dist/delivery/convergence-loop.js.map +1 -1
  56. package/dist/delivery/execution-gate.d.ts +1 -1
  57. package/dist/delivery/execution-gate.d.ts.map +1 -1
  58. package/dist/delivery/execution-gate.js +248 -5
  59. package/dist/delivery/execution-gate.js.map +1 -1
  60. package/dist/delivery/snapshot.d.ts +16 -0
  61. package/dist/delivery/snapshot.d.ts.map +1 -1
  62. package/dist/delivery/snapshot.js +113 -2
  63. package/dist/delivery/snapshot.js.map +1 -1
  64. package/dist/delivery/user-validation.d.ts.map +1 -1
  65. package/dist/delivery/user-validation.js +55 -10
  66. package/dist/delivery/user-validation.js.map +1 -1
  67. package/dist/delivery/verifier-ladder.d.ts +24 -0
  68. package/dist/delivery/verifier-ladder.d.ts.map +1 -1
  69. package/dist/delivery/verifier-ladder.js +119 -2
  70. package/dist/delivery/verifier-ladder.js.map +1 -1
  71. package/dist/delivery/visual-gate.d.ts +24 -0
  72. package/dist/delivery/visual-gate.d.ts.map +1 -1
  73. package/dist/delivery/visual-gate.js +70 -9
  74. package/dist/delivery/visual-gate.js.map +1 -1
  75. package/dist/generators/claude-md.js +17 -4
  76. package/dist/generators/claude-md.js.map +1 -1
  77. package/dist/policies/liveness.d.ts +105 -0
  78. package/dist/policies/liveness.d.ts.map +1 -0
  79. package/dist/policies/liveness.js +251 -0
  80. package/dist/policies/liveness.js.map +1 -0
  81. package/dist/policies/policy-memory.d.ts.map +1 -1
  82. package/dist/policies/policy-memory.js +3 -4
  83. package/dist/policies/policy-memory.js.map +1 -1
  84. package/dist/quality/baseline.d.ts +29 -0
  85. package/dist/quality/baseline.d.ts.map +1 -0
  86. package/dist/quality/baseline.js +79 -0
  87. package/dist/quality/baseline.js.map +1 -0
  88. package/dist/quality/complexity.d.ts +38 -0
  89. package/dist/quality/complexity.d.ts.map +1 -0
  90. package/dist/quality/complexity.js +239 -0
  91. package/dist/quality/complexity.js.map +1 -0
  92. package/dist/quality/config.d.ts +40 -0
  93. package/dist/quality/config.d.ts.map +1 -0
  94. package/dist/quality/config.js +77 -0
  95. package/dist/quality/config.js.map +1 -0
  96. package/dist/quality/crap.d.ts +11 -0
  97. package/dist/quality/crap.d.ts.map +1 -0
  98. package/dist/quality/crap.js +54 -0
  99. package/dist/quality/crap.js.map +1 -0
  100. package/dist/quality/languages.d.ts +25 -0
  101. package/dist/quality/languages.d.ts.map +1 -0
  102. package/dist/quality/languages.js +150 -0
  103. package/dist/quality/languages.js.map +1 -0
  104. package/dist/quality/report.d.ts +38 -0
  105. package/dist/quality/report.d.ts.map +1 -0
  106. package/dist/quality/report.js +251 -0
  107. package/dist/quality/report.js.map +1 -0
  108. package/dist/quality/scanner.d.ts +32 -0
  109. package/dist/quality/scanner.d.ts.map +1 -0
  110. package/dist/quality/scanner.js +127 -0
  111. package/dist/quality/scanner.js.map +1 -0
  112. package/dist/quality/tools.d.ts +28 -0
  113. package/dist/quality/tools.d.ts.map +1 -0
  114. package/dist/quality/tools.js +189 -0
  115. package/dist/quality/tools.js.map +1 -0
  116. package/dist/types/config.d.ts +148 -0
  117. package/dist/types/config.d.ts.map +1 -1
  118. package/dist/types/config.js +35 -0
  119. package/dist/types/config.js.map +1 -1
  120. package/docs/ATTRIBUTION.md +0 -1
  121. package/docs/INDEX.md +38 -4
  122. package/docs/analysis/MARKET_POSITION.md +179 -0
  123. package/docs/architecture/OVERVIEW.md +82 -18
  124. package/docs/architecture/PROTOCOL.md +1 -1
  125. package/docs/benchmarks/PAIRED_FINDINGS.md +106 -0
  126. package/docs/benchmarks/PAIRED_HARNESS.md +16 -0
  127. package/docs/benchmarks/README.md +59 -0
  128. package/docs/design/UAP_REACTOR.md +0 -1
  129. package/docs/getting-started/INSTALLATION.md +1 -1
  130. package/docs/guides/AUTOMATIC.md +0 -14
  131. package/docs/guides/AUTOMATIC_FEATURES.md +1 -9
  132. package/docs/guides/COORDINATION.md +1 -1
  133. package/docs/guides/DELIVERY_PIPELINE.md +5 -0
  134. package/docs/guides/DEPLOY_BATCHING.md +1 -1
  135. package/docs/guides/DROIDS_AND_SKILLS.md +1 -1
  136. package/docs/guides/LOCAL_MODELS.md +6 -1
  137. package/docs/guides/MCP_ROUTER.md +1 -1
  138. package/docs/guides/MEMORY.md +1 -1
  139. package/docs/guides/MULTI_MODEL.md +1 -1
  140. package/docs/guides/ORCHESTRATOR.md +1 -1
  141. package/docs/guides/POLICIES.md +19 -3
  142. package/docs/guides/QWEN38_EXL3.md +114 -0
  143. package/docs/guides/WORKTREE_WORKFLOW.md +45 -1
  144. package/docs/integrations/MCP_ROUTER.md +1 -4
  145. package/docs/plans/deliver-hardening-plan-2026-07-13.md +173 -0
  146. package/docs/plans/deliver-hardening-review-response-2026-07-13.md +180 -0
  147. package/docs/plans/k3-uap-uplift-validation.md +216 -0
  148. package/docs/plans/qwen38-uap-retest-plan.md +196 -0
  149. package/docs/reference/API.md +1 -1
  150. package/docs/reference/CLI.md +49 -14
  151. package/docs/reference/COMPARISON.md +248 -0
  152. package/docs/reference/CONFIGURATION.md +1 -3
  153. package/docs/reference/DATABASE_SCHEMA.md +1 -1
  154. package/docs/reference/FEATURES.md +3 -3
  155. package/docs/reference/PATTERNS.md +1 -1
  156. package/docs/reference/PLATFORMS.md +1 -1
  157. package/package.json +2 -2
  158. package/scripts/maintenance/update-skills.py +0 -22
  159. package/scripts/maintenance/verify-compliance.sh +3 -3
  160. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  161. package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-312.pyc +0 -0
  162. package/src/policies/enforcers/adr_guard.py +0 -0
  163. package/src/policies/enforcers/bearer_lockdown.py +0 -0
  164. package/src/policies/enforcers/branch_freshness.py +0 -0
  165. package/src/policies/enforcers/design_token_gate.py +0 -0
  166. package/src/policies/enforcers/enforcement_self_protect.py +70 -0
  167. package/src/policies/enforcers/expert_review_required.py +22 -0
  168. package/src/policies/enforcers/iac_plan_destruction_check.py +0 -0
  169. package/src/policies/enforcers/quality_metrics_gate.py +447 -0
  170. package/src/policies/enforcers/ship_loop_gate.py +0 -0
  171. package/src/policies/enforcers/validate_plan_on_change.py +0 -0
  172. package/src/policies/enforcers/visual_verification.py +0 -0
  173. package/src/policies/enforcers/workdir_scope.py +0 -0
  174. package/src/policies/schemas/policies/expert-review-required.md +9 -1
  175. package/src/policies/schemas/policies/quality-metrics-gate.md +74 -0
  176. package/templates/CLAUDE.template.md +1 -1
  177. package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
  178. package/templates/hooks/pre-tool-use-bash.sh +50 -3
  179. package/templates/hooks/pre-tool-use-edit-write.sh +31 -0
  180. package/templates/hooks/session-start.sh +15 -0
  181. package/templates/hooks/uap-policy-gate.sh +187 -5
  182. package/templates/stryker.config.json +14 -0
  183. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
  184. package/tools/agents/scripts/anthropic_proxy.py +254 -28
  185. package/tools/agents/tests/run_acceptance.py +0 -0
  186. package/tools/agents/tests/test_client_disconnect.py +0 -0
  187. package/tools/agents/tests/test_cycle_break_exploration.py +0 -0
  188. package/tools/agents/tests/test_deferral_break.py +0 -0
  189. package/tools/agents/tests/test_delivery_enforcement_all_langs.py +0 -0
  190. package/tools/agents/tests/test_delivery_enforcement_exemptions.py +0 -0
  191. package/tools/agents/tests/test_delivery_enforcement_filepath.py +0 -0
  192. package/tools/agents/tests/test_delivery_enforcement_web_and_bash.py +0 -0
  193. package/tools/agents/tests/test_delivery_enforcement_worktree.py +0 -0
  194. package/tools/agents/tests/test_delivery_enforcement_write_complexity.py +0 -0
  195. package/tools/agents/tests/test_disconnect_watcher.py +0 -0
  196. package/tools/agents/tests/test_doubling_break.py +0 -0
  197. package/tools/agents/tests/test_empty_maxtokens_recovery.py +0 -0
  198. package/tools/agents/tests/test_empty_tool_loop_break.py +0 -0
  199. package/tools/agents/tests/test_enforcement_self_protect.py +90 -0
  200. package/tools/agents/tests/test_enforcer_escape_hatches.py +0 -0
  201. package/tools/agents/tests/test_error_loop_break.py +0 -0
  202. package/tools/agents/tests/test_expert_review_pr_scope.py +13 -5
  203. package/tools/agents/tests/test_fastpath_config.py +0 -0
  204. package/tools/agents/tests/test_finalize_suppression.py +0 -0
  205. package/tools/agents/tests/test_gate_evidence.py +0 -0
  206. package/tools/agents/tests/test_gate_failclosed_indirection.py +0 -0
  207. package/tools/agents/tests/test_gate_integrity.py +0 -0
  208. package/tools/agents/tests/test_live_deliver_kill.py +0 -0
  209. package/tools/agents/tests/test_malformed_unclosed_think.py +0 -0
  210. package/tools/agents/tests/test_mandate_beats_recon.py +0 -0
  211. package/tools/agents/tests/test_mandate_deliver.py +0 -0
  212. package/tools/agents/tests/test_non_stream_resp_log.py +0 -0
  213. package/tools/agents/tests/test_output_token_ceilings.py +0 -0
  214. package/tools/agents/tests/test_overflow_truncate_count_tokens.py +0 -0
  215. package/tools/agents/tests/test_path_containment.py +0 -0
  216. package/tools/agents/tests/test_path_normalizer_hardened.py +0 -0
  217. package/tools/agents/tests/test_repeat_call_guard.py +0 -0
  218. package/tools/agents/tests/test_repeat_guard.py +0 -0
  219. package/tools/agents/tests/test_session_admission.py +0 -0
  220. package/tools/agents/tests/test_stream_heartbeat.py +0 -0
  221. package/tools/agents/tests/test_stream_thinking_deltas.py +405 -0
  222. package/tools/agents/tests/test_stuck_break_hard.py +0 -0
  223. package/tools/agents/tests/test_stuck_break_reattach.py +0 -0
  224. package/tools/agents/tests/test_turn_count_breaker_periodic.py +0 -0
  225. package/tools/agents/tests/test_upstream_chokepoint.py +0 -0
  226. package/tools/agents/tests/test_upstream_model_aliases.py +0 -0
  227. package/tools/agents/tests/test_validate_plan_gate.py +0 -0
  228. package/tools/agents/tests/test_vision_passthrough.py +0 -0
  229. package/tools/agents/tests/test_workdir_scope_enforcer.py +0 -0
  230. package/dist/cli/rtk.d.ts +0 -34
  231. package/dist/cli/rtk.d.ts.map +0 -1
  232. package/dist/cli/rtk.js +0 -401
  233. package/dist/cli/rtk.js.map +0 -1
  234. package/docs/integrations/RTK.md +0 -113
  235. package/scripts/setup/install-rtk.sh +0 -184
  236. package/src/policies/enforcers/rtk_wrap.py +0 -205
  237. package/src/policies/schemas/policies/rtk-wrap.md +0 -50
  238. package/tools/agents/tests/test_rtk_wrap.py +0 -151
@@ -1,184 +0,0 @@
1
- #!/usr/bin/env bash
2
- # RTK (Rust Token Killer) Installation Script for UAP
3
- # Auto-detects OS and installs RTK via appropriate method
4
-
5
- set -euo pipefail
6
-
7
- echo "🔧 Installing RTK (Rust Token Killer)..."
8
- echo " Reduces LLM token consumption by 60-90% on CLI commands"
9
- echo ""
10
-
11
- # Detect OS and architecture
12
- detect_os() {
13
- if [[ "$(uname -s)" == "Darwin" ]]; then
14
- echo "macOS"
15
- elif [[ "$(uname -s)" == "Linux" ]]; then
16
- echo "Linux"
17
- elif [[ "$(uname -s)" =~ MINGW.* || "$(uname -s)" =~ MSYS* ]]; then
18
- echo "Windows"
19
- else
20
- echo "Unknown"
21
- fi
22
- }
23
-
24
- detect_arch() {
25
- local arch=$(uname -m)
26
- case $arch in
27
- x86_64) echo "x86_64" ;;
28
- arm64|aarch64) echo "arm64" ;;
29
- *) echo "$arch" ;;
30
- esac
31
- }
32
-
33
- OS=$(detect_os)
34
- ARCH=$(detect_arch)
35
-
36
- echo "Detected: $OS ($ARCH)"
37
- echo ""
38
-
39
- # Check if Homebrew is available (macOS/Linux)
40
- check_brew() {
41
- command -v brew &> /dev/null && echo "yes" || echo "no"
42
- }
43
-
44
- # Check if cargo is installed
45
- check_cargo() {
46
- command -v cargo &> /dev/null && echo "yes" || echo "no"
47
- }
48
-
49
- # Check if RTK is already installed
50
- check_rtk_installed() {
51
- command -v rtk &> /dev/null && echo "yes" || echo "no"
52
- }
53
-
54
- RTK_INSTALLED=$(check_rtk_installed)
55
-
56
- if [[ "$RTK_INSTALLED" == "yes" ]]; then
57
- CURRENT_VERSION=$(rtk --version 2>&1 | head -1)
58
- echo "ℹ RTK is already installed: $CURRENT_VERSION"
59
- echo ""
60
- read -p "Do you want to upgrade to the latest version? [y/N] " -n 1 -r
61
- echo ""
62
- if [[ ! $REPLY =~ ^[Yy]$ ]]; then
63
- echo "Skipping installation."
64
- exit 0
65
- fi
66
- fi
67
-
68
- # Installation methods (in order of preference)
69
- install_methods=(
70
- "homebrew"
71
- "cargo"
72
- "curl_install"
73
- )
74
-
75
- INSTALL_SUCCESS=false
76
-
77
- for method in "${install_methods[@]}"; do
78
- echo "📦 Trying installation method: $method"
79
-
80
- case $method in
81
- homebrew)
82
- if [[ "$OS" == "macOS" ]] || (check_brew == "yes"); then
83
- echo " Installing via Homebrew..."
84
- if brew install rtk &> /dev/null; then
85
- INSTALL_SUCCESS=true
86
- break
87
- fi
88
- fi
89
- ;;
90
-
91
- cargo)
92
- if check_cargo == "yes"; then
93
- echo " Installing via Cargo..."
94
- if cargo install --git https://github.com/rtk-ai/rtk --quiet 2>&1; then
95
- INSTALL_SUCCESS=true
96
- break
97
- fi
98
- fi
99
- ;;
100
-
101
- curl_install)
102
- if [[ "$OS" == "macOS" ]] || [[ "$OS" == "Linux" ]]; then
103
- echo " Installing via curl..."
104
- TEMP_DIR=$(mktemp -d)
105
- cd "$TEMP_DIR"
106
-
107
- # Download appropriate binary based on OS/arch
108
- if [[ "$ARCH" == "arm64" ]]; then
109
- BINARY="rtk-aarch64-apple-darwin.tar.gz"
110
- else
111
- BINARY="rtk-x86_64-apple-darwin.tar.gz"
112
- fi
113
-
114
- if [[ "$OS" == "Linux" ]]; then
115
- if [[ "$ARCH" == "arm64" ]]; then
116
- BINARY="rtk-aarch64-unknown-linux-gnu.tar.gz"
117
- else
118
- BINARY="rtk-x86_64-unknown-linux-musl.tar.gz"
119
- fi
120
- fi
121
-
122
- echo " Downloading: $BINARY"
123
-
124
- if curl -fsSL "https://github.com/rtk-ai/rtk/releases/latest/download/$BINARY" -o rtk.tar.gz 2>&1; then
125
- tar -xzf rtk.tar.gz
126
- chmod +x rtk
127
-
128
- # Move to local bin or PATH
129
- if [[ -d "$HOME/.local/bin" ]] || echo "$PATH" | grep -q "$HOME/.local/bin"; then
130
- mkdir -p "$HOME/.local/bin"
131
- mv rtk "$HOME/.local/bin/"
132
- echo " Installed to: $HOME/.local/bin/rtk"
133
- else
134
- sudo mkdir -p /usr/local/bin
135
- sudo mv rtk /usr/local/bin/
136
- echo " Installed to: /usr/local/bin/rtk"
137
- fi
138
-
139
- INSTALL_SUCCESS=true
140
- cd ..
141
- rm -rf "$TEMP_DIR"
142
- break
143
- fi
144
- fi
145
- ;;
146
- esac
147
-
148
- echo " ✗ Failed: $method"
149
- done
150
-
151
- # Verify installation
152
- if [[ "$INSTALL_SUCCESS" == "true" ]]; then
153
- if command -v rtk &> /dev/null; then
154
- VERSION=$(rtk --version 2>&1 | head -1)
155
- echo ""
156
- echo "✅ RTK installed successfully!"
157
- echo " Version: $VERSION"
158
- echo ""
159
- echo "📚 Next steps:"
160
- echo " 1. Initialize hook for Claude Code:"
161
- echo " rtk init --global"
162
- echo ""
163
- echo " 2. Verify installation:"
164
- echo " rtk gain"
165
- echo ""
166
- echo " 3. View token savings:"
167
- echo " rtk gain --graph"
168
- echo ""
169
- else
170
- echo "❌ Installation completed but RTK binary not found in PATH"
171
- echo " Please ensure $HOME/.local/bin is in your PATH:"
172
- echo ' echo '\''export PATH="$HOME/.local/bin:$PATH"'\'' >> ~/.bashrc'
173
- exit 1
174
- fi
175
- else
176
- echo ""
177
- echo "❌ All installation methods failed"
178
- echo ""
179
- echo "Manual installation options:"
180
- echo " 1. Homebrew (macOS/Linux): brew install rtk"
181
- echo " 2. Cargo: cargo install --git https://github.com/rtk-ai/rtk"
182
- echo " 3. Pre-built binaries: https://github.com/rtk-ai/rtk/releases"
183
- exit 1
184
- fi
@@ -1,205 +0,0 @@
1
- #!/usr/bin/env python3
2
- """rtk-wrap enforcer: heavy CLIs must be invoked via rtk."""
3
- from __future__ import annotations
4
- import re
5
- import sys
6
- from pathlib import Path
7
-
8
- sys.path.insert(0, str(Path(__file__).parent))
9
- from _common import emit, parse_cli # noqa: E402
10
-
11
- WRAPPED = ("git", "kubectl", "docker", "docker-compose", "npm", "pnpm", "yarn", "helm", "terraform")
12
- RTK_META = re.compile(r"^\s*rtk\s+(gain|discover|proxy|--version|-V|--help)\b")
13
- ALREADY_WRAPPED = re.compile(r"^\s*rtk\s+\S+")
14
-
15
-
16
- # `rtk npm`/`rtk pnpm`/`rtk yarn` map to `<pm> run` (filtered script output). The
17
- # subcommands below are NOT script runners, so `rtk npm view` would mangle to
18
- # `npm run view`. Route them through `rtk proxy` instead (still tracked).
19
- PM_BUILTINS = {
20
- "install", "i", "ci", "add", "view", "info", "show", "publish", "pack",
21
- "audit", "outdated", "ls", "list", "link", "unlink", "dedupe", "prune",
22
- "exec", "dlx", "init", "create", "ping", "whoami", "login", "logout",
23
- "version", "deprecate", "dist-tag", "owner", "access", "search", "update",
24
- "uninstall", "remove", "rm", "rebuild", "root", "bin", "config", "cache",
25
- "doctor", "fund", "why", "store", "set", "get",
26
- }
27
- PMS = ("npm", "pnpm", "yarn")
28
-
29
-
30
- # Flags that say "I am going to PARSE this". rtk rewrites output for human
31
- # reading and does not notice these, so the caller gets prose where it asked
32
- # for records. Verified against real git in this repo:
33
- # worktree list --porcelain 46 entries -> 0 through rtk
34
- # branch --format=... 55 refs -> 142 lines, wrong set
35
- # diff --name-only 3 paths -> 3 paths + a "--- Changes ---" block
36
- # status --porcelain 12 lines -> 11
37
- # `rtk proxy` runs it unfiltered and is byte-exact on every one.
38
- MACHINE_FLAGS = (
39
- "--porcelain",
40
- "--format",
41
- "--pretty",
42
- "--name-only",
43
- "--name-status",
44
- "--numstat",
45
- "--raw",
46
- "-z",
47
- )
48
- # NOT --oneline: it is a reading convenience, not a parse signal, and
49
- # test/rtk-wrap.test.ts contracts it as accepted. Anything meaning to parse log
50
- # output uses --format or --pretty, which are covered above. rtk does reformat
51
- # `log --oneline`, so a caller that parses it gets altered text -- a narrow
52
- # residual, accepted rather than taxing one of the most common human git calls.
53
-
54
- # Subcommand forms rtk restructures even without one of those flags. Kept to
55
- # what was actually measured rather than guessed, because every entry here is
56
- # friction for a human-readable call that would have been fine.
57
- MACHINE_FORMS = {
58
- "branch": ("-r", "-a", "--merged", "--no-merged"),
59
- "worktree": ("list",),
60
- "stash": ("list",),
61
- }
62
-
63
- # rtk's docker handlers only recognise the BARE subcommand. Any flag on
64
- # `docker ps` or `docker logs` makes rtk print "[rtk: parse failed, running
65
- # raw]" ahead of otherwise-correct raw output. The DATA is fine -- but that
66
- # line reads as a tool error, and a model that cannot act on it retries: it
67
- # drove a live ERROR-LOOP on 2026-08-25 ("same failure x3,
68
- # sig='[rtk: parse failed, running raw]'"). `rtk proxy` runs the same command
69
- # unfiltered with no such line, and is still counted in the savings ledger.
70
- #
71
- # Measured against rtk 0.27.0 on 2026-08-25 -- listed rather than guessed,
72
- # because every entry here is friction for a call that would have been fine:
73
- # docker ps ok
74
- # docker ps -a parse failed
75
- # docker ps --filter name=uap parse failed
76
- # docker ps --format '{{.Names}}' parse failed
77
- # docker images ok
78
- # docker logs --tail 1 <id> parse failed
79
- # docker inspect <id> ok
80
- # So the trigger is the SUBCOMMAND plus any flag at all, not a specific flag.
81
- DOCKER_FLAG_INTOLERANT = ("ps", "logs")
82
-
83
-
84
- def docker_wants_proxy(tokens: list[str]) -> bool:
85
- """Is this a docker call rtk will fail to parse (bare form only)?"""
86
- args = [t for t in tokens if t.split("/")[-1] != "docker"]
87
- sub_cmd = next((t for t in args if not t.startswith("-")), "")
88
- if sub_cmd not in DOCKER_FLAG_INTOLERANT:
89
- return False
90
- return any(t.startswith("-") for t in args)
91
-
92
-
93
- def wants_machine_output(tokens: list[str]) -> bool:
94
- """Is this git invocation asking for output something will parse?"""
95
- args = [t for t in tokens if t != "git"]
96
- for tok in args:
97
- if tok in MACHINE_FLAGS or tok.split("=")[0] in MACHINE_FLAGS:
98
- return True
99
- sub_cmd = next((t for t in args if not t.startswith("-")), "")
100
- return any(t in MACHINE_FORMS.get(sub_cmd, ()) for t in args)
101
-
102
-
103
- # A command is a sequence of STATEMENTS; each one invokes its own binary.
104
- _STATEMENT_SPLIT_RE = re.compile(r"&&|\|\||[;|&\n]")
105
-
106
-
107
- def _leading_binary(statement: str) -> tuple[str, list[str]] | None:
108
- """The binary a statement invokes, plus its tokens — or None.
109
-
110
- Skips env assignments (`FOO=bar cmd`) and shell grouping, then reads the
111
- FIRST real word. That word is what the shell executes; a wrapped CLI
112
- appearing later is an argument, not an invocation.
113
- """
114
- tokens = statement.replace("(", " ").replace("{", " ").split()
115
- idx = 0
116
- while idx < len(tokens) and "=" in tokens[idx].split("/")[0]:
117
- idx += 1 # env assignment prefix
118
- if idx >= len(tokens):
119
- return None
120
- return tokens[idx].split("/")[-1], tokens[idx:]
121
-
122
-
123
- def main() -> None:
124
- op, args = parse_cli()
125
- cmd = (args.get("command") or args.get("cmd") or "").strip()
126
- if not cmd or op.lower() != "bash":
127
- emit(True, "not a Bash command")
128
-
129
- # Evaluate each statement on its own.
130
- #
131
- # Whole-command matching got this wrong in BOTH directions. It only accepted
132
- # `rtk` at the very start, so `cd /srv/app && rtk git log` — already wrapped —
133
- # was refused, with a suggestion that prefixed rtk to the entire line and
134
- # produced nonsense like `rtk cd /srv/app; git log`. And it scanned only the
135
- # first three tokens, so `cd /srv/app && git log` slipped through entirely:
136
- # `git` sat at index 3. That is a bypass, not just noise — any bare invocation
137
- # could be hidden behind a `cd`. Reading the leading binary of each statement
138
- # fixes both, because that is what the shell actually runs.
139
- for raw in _STATEMENT_SPLIT_RE.split(cmd):
140
- statement = raw.strip()
141
- if not statement:
142
- continue
143
- found = _leading_binary(statement)
144
- if not found:
145
- continue
146
- bin_name, tokens = found
147
-
148
- # A statement led by `rtk` is normally fine: in `rtk git log` the
149
- # leading binary is rtk and `git` is its argument. One exception --
150
- # `rtk git <machine-readable>` is the corrupting combination, and
151
- # skipping every rtk-led statement is what let it through the gate.
152
- if bin_name == "rtk":
153
- rest = [t for t in tokens[1:] if not t.startswith("-")]
154
- if rest[:1] == ["docker"] and docker_wants_proxy(tokens[1:]):
155
- emit(
156
- False,
157
- "rtk-wrap: rtk only parses the bare form of this docker "
158
- "subcommand; with any flag it prefixes "
159
- "'[rtk: parse failed, running raw]' to the output, which "
160
- "reads as an error and has driven retry loops. Use: "
161
- "rtk proxy " + " ".join(tokens[1:]),
162
- bin="rtk",
163
- )
164
- if rest[:1] == ["git"] and wants_machine_output(tokens[1:]):
165
- emit(
166
- False,
167
- "rtk-wrap: rtk reformats output for reading, so a git command "
168
- "that asked for machine-readable output comes back as prose "
169
- "(measured: `worktree list --porcelain` returns 0 entries "
170
- "through rtk, 46 through git). Use: rtk proxy "
171
- + " ".join(tokens[1:]),
172
- bin="rtk",
173
- )
174
- continue
175
-
176
- if bin_name not in WRAPPED:
177
- continue
178
-
179
- wrapper = "rtk"
180
- if bin_name == "git" and wants_machine_output(tokens):
181
- # Still through rtk, so the call is still tracked -- just unfiltered.
182
- wrapper = "rtk proxy"
183
- elif bin_name == "docker" and docker_wants_proxy(tokens):
184
- wrapper = "rtk proxy"
185
- if bin_name in PMS:
186
- sub = ""
187
- for nxt in tokens[1:]:
188
- if not nxt.startswith("-"):
189
- sub = nxt.split("/")[-1]
190
- break
191
- if sub in PM_BUILTINS:
192
- wrapper = "rtk proxy"
193
- # Suggest fixing THIS statement, not the whole line.
194
- emit(
195
- False,
196
- f"rtk-wrap: '{bin_name}' must be invoked via rtk. "
197
- f"Use: {wrapper} {statement}",
198
- bin=bin_name,
199
- )
200
-
201
- emit(True, "no unwrapped CLI in any statement")
202
-
203
-
204
- if __name__ == "__main__":
205
- main()
@@ -1,50 +0,0 @@
1
- # rtk-wrap
2
-
3
- **Category**: custom
4
- **Level**: RECOMMENDED
5
- **Default**: off
6
- **Enforcement Stage**: pre-exec
7
- **Tags**: rtk, tokens, efficiency
8
-
9
- ## Rule
10
-
11
- Opt-in. Enable with `uap policy enable <id>`; disable with `uap policy disable <id>`.
12
-
13
- When enabled, these commands SHOULD be invoked via the `rtk` wrapper rather than directly: `git`, `kubectl`, `docker`, `docker-compose`, `npm`, `pnpm`, `yarn`, `helm`, `terraform`.
14
-
15
- Exceptions: `rtk` meta-commands (`rtk gain`, `rtk discover`, `rtk proxy`, `rtk --version`), and any invocation whose output will be PARSED — those must use `rtk proxy` (see below).
16
-
17
- ## Why
18
-
19
- RTK delivers 60–90% token reduction on dev ops (`~/.claude/RTK.md`). Missing the wrap = proportional context waste.
20
-
21
- ## Why it is off by default
22
-
23
- rtk rewrites command output for reading, and does not detect when the caller
24
- asked for machine-readable output. Measured against real git in this repo:
25
-
26
- | command | git | via rtk |
27
- |---|---|---|
28
- | `worktree list --porcelain` | 46 entries | **0 entries** |
29
- | `branch --format='%(refname:short)'` | 55 refs | 142 lines, wrong set |
30
- | `diff --name-only HEAD` | 3 paths | 3 paths + a `--- Changes ---` block |
31
- | `status --porcelain` | 12 lines | 11 |
32
-
33
- `rtk proxy git ...` is byte-exact on all of them.
34
-
35
- While this policy was REQUIRED, that mangled output was what every agent read.
36
- An agent parsing `^worktree ` out of `rtk git worktree list --porcelain` sees
37
- nothing and concludes there are no worktrees — during a prune, the difference
38
- between "skip" and "delete". The enforcer now routes parsed invocations to
39
- `rtk proxy`, but the underlying trade is a judgement call, so it is offered
40
- rather than imposed.
41
-
42
- ## Enforcement
43
-
44
- Python enforcer `rtk_wrap.py` inspects the Bash command string and blocks if a wrapped binary is invoked without the `rtk ` prefix.
45
-
46
- ```rules
47
- - title: "Wrap heavy CLIs with rtk"
48
- keywords: [bash, shell, git, kubectl, docker, npm, pnpm, yarn, helm, terraform]
49
- antiPatterns: [raw-kubectl, raw-git, raw-docker, raw-npm]
50
- ```
@@ -1,151 +0,0 @@
1
- """rtk-wrap: heavy CLIs go through rtk, but PARSED output must not.
2
-
3
- rtk rewrites command output for token savings and does not notice when the
4
- caller asked for machine-readable output. Measured against real git in this
5
- repo:
6
-
7
- git worktree list --porcelain 46 entries -> rtk: 0 entries
8
- git branch --format='%(refname:short)' 55 refs -> rtk: 142 lines, wrong set
9
- git diff --name-only HEAD 3 paths -> rtk: 3 paths + "--- Changes ---"
10
- git status --porcelain 12 lines -> rtk: 11
11
- git branch -r / --merged, stash list, log --oneline : all differ
12
-
13
- `rtk proxy` runs the command unfiltered and was byte-exact on all eight.
14
-
15
- This matters because the policy makes rtk mandatory, so the mangled output is
16
- what every agent in this repo reads. Parsing `^worktree ` out of
17
- `rtk git worktree list --porcelain` yields NOTHING, and an agent pruning
18
- worktrees on that basis concludes there are none to protect.
19
-
20
- The enforcer had no tests before this file, which is how `rtk git
21
- <machine-readable>` -- the corrupting combination -- stayed invisible: every
22
- statement led by `rtk` was skipped without looking at what followed.
23
- """
24
-
25
- from __future__ import annotations
26
-
27
- import json
28
- import subprocess
29
- import sys
30
- import unittest
31
- from pathlib import Path
32
-
33
- REPO = Path(__file__).resolve().parents[3]
34
- ENFORCER = REPO / "src" / "policies" / "enforcers" / "rtk_wrap.py"
35
-
36
-
37
- def gate(command: str) -> tuple[bool, str]:
38
- r = subprocess.run(
39
- [sys.executable, str(ENFORCER), "--operation", "Bash",
40
- "--args", json.dumps({"command": command})],
41
- capture_output=True, text=True, timeout=60,
42
- )
43
- try:
44
- payload = json.loads(r.stdout or "{}")
45
- except json.JSONDecodeError:
46
- return True, f"unparseable: {r.stdout[:120]}"
47
- return payload.get("allowed", True), payload.get("reason", "")
48
-
49
-
50
- class MachineReadableGitMustBypassTheFilter(unittest.TestCase):
51
- """The verdicts that stop an agent reading prose as records."""
52
-
53
- MACHINE = [
54
- "git worktree list --porcelain",
55
- "git status --porcelain",
56
- "git branch --format='%(refname:short)'",
57
- "git diff --name-only HEAD",
58
- "git log --pretty=%H",
59
- # NOT `log --oneline`: a reading convenience, not a parse signal.
60
- # test/rtk-wrap.test.ts contracts it as accepted, and anything that
61
- # means to parse log output uses --format or --pretty.
62
- "git diff --numstat HEAD",
63
- "git ls-files -z",
64
- "git branch -r",
65
- "git branch --merged origin/master",
66
- "git stash list",
67
- ]
68
-
69
- def test_bare_git_asking_for_records_is_sent_to_rtk_proxy(self):
70
- for cmd in self.MACHINE:
71
- with self.subTest(cmd=cmd):
72
- allowed, reason = gate(cmd)
73
- self.assertFalse(allowed, cmd)
74
- self.assertIn("rtk proxy", reason, f"{cmd}: must name the unfiltered form")
75
-
76
- def test_rtk_git_asking_for_records_is_refused(self):
77
- """The combination that used to sail through.
78
-
79
- Statements led by `rtk` were skipped without inspecting the rest, so
80
- the one form that actually corrupts data was the one the gate ignored.
81
- """
82
- for cmd in self.MACHINE:
83
- with self.subTest(cmd=cmd):
84
- allowed, reason = gate("rtk " + cmd)
85
- self.assertFalse(allowed, f"rtk {cmd} returns mangled output")
86
- self.assertIn("rtk proxy", reason)
87
-
88
- def test_rtk_proxy_is_accepted(self):
89
- for cmd in self.MACHINE:
90
- with self.subTest(cmd=cmd):
91
- allowed, _ = gate("rtk proxy " + cmd)
92
- self.assertTrue(allowed, f"rtk proxy {cmd} is the correct form")
93
-
94
-
95
- class HumanReadableGitIsUnaffected(unittest.TestCase):
96
- """rtk's entire purpose is these, and the rule must not tax them."""
97
-
98
- def test_ordinary_git_still_wants_plain_rtk(self):
99
- for cmd in ("git status", "git log -5", "git diff", "git branch", "git show HEAD"):
100
- with self.subTest(cmd=cmd):
101
- allowed, reason = gate(cmd)
102
- self.assertFalse(allowed, cmd)
103
- self.assertIn("rtk", reason)
104
- self.assertNotIn(
105
- "rtk proxy", reason,
106
- f"{cmd} is human-readable; sending it to proxy forfeits the token saving",
107
- )
108
-
109
- def test_already_wrapped_human_forms_pass(self):
110
- for cmd in ("rtk git status", "rtk git log -5", "rtk git diff", "rtk git branch"):
111
- with self.subTest(cmd=cmd):
112
- allowed, _ = gate(cmd)
113
- self.assertTrue(allowed, cmd)
114
-
115
-
116
- class ExistingBehaviourIsPreserved(unittest.TestCase):
117
- """The regressions this enforcer already carries scars from."""
118
-
119
- def test_a_wrapped_cli_behind_a_cd_is_still_caught(self):
120
- allowed, _ = gate("cd /srv/app && git log")
121
- self.assertFalse(allowed, "a bare invocation hidden behind cd is a bypass")
122
-
123
- def test_an_already_wrapped_statement_behind_a_cd_passes(self):
124
- allowed, _ = gate("cd /srv/app && rtk git log")
125
- self.assertTrue(allowed)
126
-
127
- def test_npm_builtins_still_route_to_proxy(self):
128
- allowed, reason = gate("npm view lodash")
129
- self.assertFalse(allowed)
130
- self.assertIn("rtk proxy", reason, "rtk npm view would mangle to `npm run view`")
131
-
132
- def test_npm_scripts_still_route_to_plain_rtk(self):
133
- allowed, reason = gate("npm test")
134
- self.assertFalse(allowed)
135
- self.assertNotIn("rtk proxy", reason)
136
-
137
- def test_a_wrapped_cli_as_an_argument_is_not_an_invocation(self):
138
- allowed, _ = gate("echo git status")
139
- self.assertTrue(allowed, "git here is an argument to echo, not a command")
140
-
141
- def test_non_bash_operations_are_ignored(self):
142
- r = subprocess.run(
143
- [sys.executable, str(ENFORCER), "--operation", "Edit",
144
- "--args", json.dumps({"command": "git status --porcelain"})],
145
- capture_output=True, text=True, timeout=60,
146
- )
147
- self.assertTrue(json.loads(r.stdout)["allowed"])
148
-
149
-
150
- if __name__ == "__main__":
151
- unittest.main()