Skip to content

Commit 39bfe1a

Browse files
committed
feat: add RTK guidance and shell rewrites
1 parent 36bd2e4 commit 39bfe1a

5 files changed

Lines changed: 319 additions & 2 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,7 @@ Agent-facing install steps live in [`CODEX_INSTALL.md`](CODEX_INSTALL.md).
2727
it can suggest a safer RTK-shaped command. Ambiguous, unsupported, malformed, or
2828
mutating commands fail open with no output.
2929

30+
Repo-tested Codex usage guidance lives in [`RTK.md`](RTK.md).
31+
3032
Developer docs, release steps, debugging, and full rewrite details live in
3133
[`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md).

RTK.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# RTK Guidance For Codex
2+
3+
RTK saves context by filtering noisy command output before it reaches Codex.
4+
Use it deliberately for commands that usually produce many tokens.
5+
6+
## Default
7+
8+
Prefer `rtk` for supported noisy commands:
9+
10+
```bash
11+
rtk git status --short
12+
rtk git diff -- src tests
13+
rtk cargo test
14+
rtk cargo clippy --all-targets --all-features -- -D warnings
15+
rtk npm run build
16+
rtk pytest -q
17+
rtk gh run view <id>
18+
```
19+
20+
Do not blindly prefix every shell command. Keep shell/control/device tools raw
21+
when RTK does not support them, or use `rtk proxy <cmd>` as an escape hatch.
22+
Common raw tools: `pwsh`, `powershell`, `bash`, `sh`, `adb`, `sqlite3`, `jq`,
23+
`node`, `where.exe`, `which`, direct scripts, and direct local binaries.
24+
25+
## Read Files
26+
27+
Use `rtk read` instead of `cat`, `Get-Content`, `sed -n`, `head`, `tail`, or
28+
`nl -ba` when inspecting known files:
29+
30+
```bash
31+
rtk read src/main.rs
32+
rtk read -n src/main.rs
33+
rtk read src/main.rs --max-lines 120
34+
rtk read app.log --tail-lines 80
35+
```
36+
37+
Real flags: `--line-numbers`/`-n`, `--max-lines`, `--tail-lines`, and
38+
`--level none|minimal|aggressive`. Do not invent `--range`, `--line`, or
39+
`--lines`.
40+
41+
## Filter Levels And Smart Reads
42+
43+
Use read levels intentionally. The flag is `--level <level>` or `-l <level>`;
44+
do not use standalone `--minimal` or `--aggressive` unless RTK adds those flags.
45+
46+
```bash
47+
rtk read src/main.rs --level none
48+
rtk read src/main.rs --level minimal
49+
rtk read -n src/main.rs --level aggressive
50+
rtk smart src/main.rs
51+
```
52+
53+
Use `none` or omit `--level` when exact text matters, especially short files,
54+
line-sensitive code, configs, or useful file headers/comments. Add
55+
`--max-lines` or `--tail-lines` to keep exact top/tail windows small.
56+
57+
Use `minimal` for compact first reads of medium or large files when comments and
58+
blank lines are less important than code shape.
59+
60+
Use `aggressive` for structure scans of large unfamiliar files, then follow with
61+
targeted `rtk grep` or exact `rtk read` windows.
62+
63+
Use `rtk smart <file>` for a quick heuristic summary of an unfamiliar source
64+
file before deciding what to read next. Do not use it as a substitute for exact
65+
code when editing or reviewing line-level behavior.
66+
67+
## Search And Discovery
68+
69+
Use `rtk grep` for content search and `rtk find` for file discovery:
70+
71+
```bash
72+
rtk grep -n "pattern" src tests
73+
rtk grep -n "pattern|other" .
74+
rtk find .
75+
rtk find src -name "*.rs"
76+
```
77+
78+
Use `rtk find`, not `rtk rg --files` or `rtk grep --files`. If raw `rg` is
79+
needed for a complex mode, keep it raw or use `rtk proxy rg ...`.
80+
81+
## Shell Wrappers
82+
83+
Avoid shell wrappers for simple commands:
84+
85+
```bash
86+
rtk git status
87+
rtk cargo test
88+
```
89+
90+
Use a wrapper only when shell features are needed, such as environment setup,
91+
compound commands, redirects, or here-docs. Keep the wrapper raw and put `rtk`
92+
on the noisy inner command when safe:
93+
94+
```bash
95+
pwsh -NoProfile -Command '$env:PATH="$env:APPDATA\luarocks\bin;$env:PATH"; rtk busted spec'
96+
bash -lc 'PATH="$HOME/.luarocks/bin:$PATH" rtk busted spec'
97+
env PATH="$HOME/.luarocks/bin:$PATH" rtk luacheck --codes spec
98+
```
99+
100+
For structured or already-bounded output, raw may be better than RTK:
101+
102+
```bash
103+
jq . file.json
104+
where.exe rtk
105+
which rtk
106+
adb devices
107+
```
108+
109+
## Check Savings
110+
111+
```bash
112+
rtk gain
113+
rtk gain --history
114+
rtk gain --failures
115+
rtk --help
116+
rtk proxy <cmd>
117+
```
118+
119+
High-value habits from observed runs: use `rtk grep` for broad recursive
120+
searches, `rtk read` for memory/session/source files, `rtk find` for recursive
121+
file lists, and `rtk cargo test`/test wrappers for noisy test output.

docs/DEVELOPMENT.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ Rewrite order:
120120
- PowerShell wrappers around `busted` and `luacheck` keep the `pwsh` wrapper
121121
for environment setup while applying `rtk` to the noisy inner tool. Existing
122122
unsupported `rtk pwsh ...` commands are rewritten back to `pwsh ...`.
123+
- Unix `bash -c`/`bash -lc` and `env VAR=...` wrappers around `busted` and
124+
`luacheck` keep the wrapper or environment setup raw while applying `rtk` to
125+
the noisy inner tool. Complex shell control flow, pipes, redirects, and syntax
126+
checks such as `bash -n` are left alone.
123127
- Direct and wrapped PowerShell `Select-String`/`sls` becomes `rtk grep -n` for
124128
simple `-Path`/`-Pattern` forms, with optional `-Context N` mapped to `-C N`.
125129
Other switches are left alone.

src/rewrite.rs

Lines changed: 162 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ pub fn suggest(command: &str) -> Option<String> {
1212
if command.is_empty()
1313
|| starts_with_rtk(command) && preferred_rtk_command(command)
1414
|| is_preferred_pwsh_wrapper(command)
15+
|| is_preferred_bash_wrapper(command)
1516
{
1617
return None;
1718
}
1819

1920
direct_powershell_redirect(command)
2021
.or_else(|| powershell_redirect(command))
22+
.or_else(|| env_redirect(command))
23+
.or_else(|| bash_redirect(command))
2124
.or_else(|| posix_redirect(command))
2225
.or_else(|| rg_redirect(command))
2326
.or_else(|| safe_external_rtk_rewrite(command))
@@ -56,6 +59,7 @@ fn preferred_rtk_command(command: &str) -> bool {
5659
| "curl"
5760
)
5861
) || is_preferred_pwsh_wrapper(command)
62+
|| is_preferred_bash_wrapper(command)
5963
}
6064

6165
fn starts_with_rtk(command: &str) -> bool {
@@ -146,6 +150,23 @@ fn is_preferred_pwsh_wrapper(command: &str) -> bool {
146150
})
147151
}
148152

153+
fn is_preferred_bash_wrapper(command: &str) -> bool {
154+
if starts_with_rtk(command) {
155+
return false;
156+
}
157+
if tokenize(command)
158+
.first()
159+
.is_none_or(|token| command_name(&token.text) != "bash")
160+
{
161+
return false;
162+
}
163+
164+
inner_bash_command(command).is_some_and(|(_, inner)| {
165+
let tokens = tokenize(&inner);
166+
find_test_tool_invocation(&tokens).is_some_and(|(_, _, _, already_rtk)| already_rtk)
167+
})
168+
}
169+
149170
fn inner_powershell_command(command: &str) -> Option<String> {
150171
let tokens = tokenize(command);
151172
if !tokens
@@ -167,6 +188,25 @@ fn inner_powershell_command(command: &str) -> Option<String> {
167188
Some(inner)
168189
}
169190

191+
fn inner_bash_command(command: &str) -> Option<(String, String)> {
192+
let tokens = tokenize(command);
193+
if tokens
194+
.first()
195+
.is_none_or(|token| command_name(&token.text) != "bash")
196+
{
197+
return None;
198+
}
199+
200+
let command_token = tokens.iter().position(|token| {
201+
matches!(token.text.as_str(), "-c" | "-lc" | "-cl")
202+
|| token.text.starts_with('-') && token.text.contains('c') && !token.text.contains('n')
203+
})?;
204+
let option = tokens[command_token].text.clone();
205+
let start = tokens.get(command_token + 1)?.start;
206+
let inner = strip_outer_quotes(command[start..].trim()).to_string();
207+
Some((option, inner))
208+
}
209+
170210
fn strip_outer_quotes(value: &str) -> &str {
171211
let bytes = value.as_bytes();
172212
if bytes.len() >= 2
@@ -209,6 +249,117 @@ fn contains_output_redirection(command: &str) -> bool {
209249
false
210250
}
211251

252+
fn env_redirect(command: &str) -> Option<String> {
253+
let had_rtk_prefix = strip_rtk_prefix(command).is_some();
254+
let without_rtk = strip_rtk_prefix(command).unwrap_or(command);
255+
let tokens = tokenize(without_rtk);
256+
if tokens
257+
.first()
258+
.is_none_or(|token| command_name(&token.text) != "env")
259+
{
260+
return None;
261+
}
262+
263+
let mut index = 1;
264+
while tokens
265+
.get(index)
266+
.is_some_and(|token| is_env_assignment(&token.text))
267+
{
268+
index += 1;
269+
}
270+
if index == 1 || index >= tokens.len() {
271+
return None;
272+
}
273+
274+
let (command_index, tool_index, tool, already_rtk) = find_test_tool_invocation(
275+
&tokens[index..],
276+
)
277+
.map(|(command_index, tool_index, tool, already_rtk)| {
278+
(command_index + index, tool_index + index, tool, already_rtk)
279+
})?;
280+
if command_index != index || already_rtk && !had_rtk_prefix {
281+
return None;
282+
}
283+
if tokens[tool_index + 1..]
284+
.iter()
285+
.any(|token| matches!(token.text.as_str(), "|" | ";"))
286+
{
287+
return None;
288+
}
289+
290+
let assignments = tokens[1..index]
291+
.iter()
292+
.map(|token| token.text.as_str())
293+
.collect::<Vec<_>>()
294+
.join(" ");
295+
let args = join_args(&tokens[tool_index + 1..]);
296+
Some(format!("env {assignments} rtk {tool}{args}"))
297+
}
298+
299+
fn bash_redirect(command: &str) -> Option<String> {
300+
let had_rtk_prefix = strip_rtk_prefix(command).is_some();
301+
let without_rtk = strip_rtk_prefix(command).unwrap_or(command);
302+
let (option, inner) = inner_bash_command(without_rtk)?;
303+
let rewritten = shell_inner_test_tool_redirect(&inner)?;
304+
if rewritten == inner && !had_rtk_prefix {
305+
return None;
306+
}
307+
Some(format!(
308+
"bash {option} {}",
309+
quote_bash_command_arg(&rewritten)
310+
))
311+
}
312+
313+
fn shell_inner_test_tool_redirect(inner: &str) -> Option<String> {
314+
if shell_inner_has_unsupported_control(inner) {
315+
return None;
316+
}
317+
let tokens = tokenize(inner);
318+
let (command_index, tool_index, tool, already_rtk) = find_test_tool_invocation(&tokens)?;
319+
if tokens[..command_index]
320+
.iter()
321+
.any(|token| !is_env_assignment(&token.text))
322+
{
323+
return None;
324+
}
325+
326+
let prefix = inner[..tokens[command_index].start].trim_end();
327+
let args = join_args(&tokens[tool_index + 1..]);
328+
let mut rewritten = String::new();
329+
if !prefix.is_empty() {
330+
rewritten.push_str(prefix);
331+
rewritten.push(' ');
332+
}
333+
rewritten.push_str("rtk ");
334+
rewritten.push_str(&tool);
335+
rewritten.push_str(&args);
336+
if already_rtk && rewritten == inner {
337+
return Some(inner.to_string());
338+
}
339+
Some(rewritten)
340+
}
341+
342+
fn shell_inner_has_unsupported_control(inner: &str) -> bool {
343+
inner.contains('\n')
344+
|| inner.contains('|')
345+
|| inner.contains("&&")
346+
|| inner.contains("||")
347+
|| inner.contains('<')
348+
|| inner.contains('>')
349+
|| inner.contains(';')
350+
}
351+
352+
fn is_env_assignment(value: &str) -> bool {
353+
let Some((name, _)) = value.split_once('=') else {
354+
return false;
355+
};
356+
!name.is_empty()
357+
&& name
358+
.chars()
359+
.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
360+
&& !name.as_bytes()[0].is_ascii_digit()
361+
}
362+
212363
fn content_redirect(command: &str) -> Option<String> {
213364
let tokens = tokenize(command);
214365
if !tokens
@@ -334,17 +485,22 @@ fn test_tool_redirect(inner: &str) -> Option<String> {
334485
}
335486

336487
fn find_test_tool(tokens: &[Token]) -> Option<(usize, String)> {
488+
let (_, tool_index, tool, _) = find_test_tool_invocation(tokens)?;
489+
Some((tool_index, tool))
490+
}
491+
492+
fn find_test_tool_invocation(tokens: &[Token]) -> Option<(usize, usize, String, bool)> {
337493
for (index, token) in tokens.iter().enumerate() {
338494
let name = command_name(&token.text);
339495
if matches!(name.as_str(), "busted" | "luacheck") {
340-
return Some((index, name));
496+
return Some((index, index, name, false));
341497
}
342498
if name == "rtk"
343499
&& let Some(next) = tokens.get(index + 1)
344500
{
345501
let next_name = command_name(&next.text);
346502
if matches!(next_name.as_str(), "busted" | "luacheck") {
347-
return Some((index + 1, next_name));
503+
return Some((index, index + 1, next_name, true));
348504
}
349505
}
350506
}
@@ -1102,6 +1258,10 @@ fn quote_powershell_command_arg(value: &str) -> String {
11021258
format!("'{}'", value.replace('\'', "''"))
11031259
}
11041260

1261+
fn quote_bash_command_arg(value: &str) -> String {
1262+
format!("'{}'", value.replace('\'', r#"'\''"#))
1263+
}
1264+
11051265
#[cfg(test)]
11061266
mod tests {
11071267
use super::*;

0 commit comments

Comments
 (0)