RTK could emit more tokens than the underlying command on small inputs:
filters that add headers, summaries, re-indentation, or a tee hint, plus
synthetic no-result messages ("0 matches", "No stashes", "[docker] 0
containers") printed where the raw command emitted nothing. Both break the
Transparency principle and inflate tokens instead of saving them.
- core::guard::never_worse(raw, filtered) returns raw when the filtered form
has more tokens (reuses tracking::estimate_tokens), so RTK output is never
larger than the real command.
- runner::emit_guarded(filtered, hint, raw) composes body + tee hint, guards
the whole, prints, and returns what was shown so printed == tracked.
- run_captured_filter guards the run_filtered* family centrally; per-site
guards cover the remaining single-string filters.
- On empty raw, emit empty and preserve the exit code instead of a synthetic
no-result message (the messages were cosmetic with no dependents; #2461
reports the grep one as actively harmful).
- git stash show now propagates its exit code instead of masking a real
failure as Ok(0).
Resolves#2551.
run_worktree list mode never checked result.success() — a failed
`git worktree list` (e.g. run outside a repo) was flattened to empty
output + exit 0. The has_action branch already guards on success; apply
the same guard to list mode and surface git's error with its exit code.
Part of #2497
The compact status path only caught the "not a git repository" error and
let every other failure (corrupt index, lock contention, broken refs)
fall through to a formatted "Clean working tree" + Ok(0), masking a real
git error. The non-compact path already guards on result.success(); apply
the same guard to the compact path, keeping the friendly not-a-repo
message and propagating git's real exit code otherwise.
Part of #2497
run_commit had a branch that printed 'ok (nothing to commit)' and fell
through to Ok(0) whenever git exited non-zero with a 'nothing to commit'
message — so a no-op or hook-aborted commit was reported as success with
exit 0. Gate strictly on output.status.success() via a CommitOutcome
helper: any failure surfaces git's message and propagates the real exit
code, matching native git. Same bug class as #1581 (push) and #1535
(stash).
Fixes#2494
Separator lines (═══, ---) and an emoji status marker cost tokens
without adding signal for the LLM — RTK output must never add noise
over raw. Semantic labels are kept; the emoji is swapped for plain
monochrome unicode.
The compact `git status` path ran `git status --porcelain -b -uall`. The
`-uall` flag expands fully-untracked directories into every file, while raw
`git status` collapses them (e.g. `node_modules/`). This made rtk output
larger than raw — measured ~29x on a 200-file untracked dir (5500B vs 191B) —
violating RTK's compress-or-match-raw invariant and inflating tokens.
Remove `-uall` so untracked directories collapse exactly like raw. This keeps
#991's actual fix intact: all modified/staged/renamed/conflict paths are still
shown with no grouped summaries or `... +N more` overflow markers (`-uall`
never affected those lines). Untracked files in partially-tracked dirs and any
paths git itself expands are still preserved by the formatter.
Measured (rtk vs raw git status):
- node_modules/ (200 files): 5500B (-2780%) -> 25B (+87% savings)
- normal (8 mod + 2 untracked): +71% -> +76%
- 17 modified (#991 case): unchanged at +58%, all 17 shown, 0 overflow markers
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When user runs 'git log -1 --format='%H' HEAD' where HEAD is a merge
commit, rtk was adding --no-merges which filtered out the merge commit
itself and returned the second parent instead. This made 'git log -1'
return wrong SHAs for merge commits.
Fix: don't add --no-merges when user explicitly passes -n N or
--max-count=N. When a user specifies an exact count they expect exactly
that many commits, not filtered results. Also skip --no-merges if user
already passed --merges or --no-merges explicitly.
Fixesrtk-ai/rtk#2009.
- fix signal truncation gaps + kush reviews
- added a new tee and hints function to give an hint with tail (avoid re-ingest of the head)
- extracted patterns in constants
## Problem (#963)
`rtk git push` reportedly times out: users see
`bash tool terminated command after exceeding timeout 30000 ms` while
plain `git push` to the same remote completes fine. P1-critical because
every Claude Code git push goes through rtk.
## Root cause
`run_push` used `cmd.stdin(Stdio::inherit()).output()`. `Command::output()`
captures both stdout and stderr until the child exits. Git push prints
its progress (`Counting objects` / `Compressing objects` / `Writing
objects`) to stderr and may prompt for SSH passphrases or HTTPS
credentials. With stderr captured, Claude Code's bash tool saw zero
output for 30+ seconds and killed the command — exactly the 30000 ms
message in the issue.
## Fix
Rewrite `run_push` on top of the streaming infrastructure that already
exists for this exact purpose (`stream::run_streaming` +
`FilterMode::Streaming`, added in 0.37.0).
Add a counterpart to `BlockStreamFilter<H: BlockHandler>` in
`src/core/stream.rs`: `LineStreamFilter<H: LineHandler>`. Where
`BlockStreamFilter` defaults to DROP and emits only collected blocks,
`LineStreamFilter` defaults to KEEP and lets handlers opt into dropping
noise. Trait surface mirrors `BlockHandler`:
- `should_skip(&mut self, line: &str) -> bool` — default false
- `observe_line(&mut self, line: &str)` — default no-op
- `format_summary(&self, exit_code, raw) -> Option<String>`
This lets future streaming commands reuse the line-oriented pattern.
`GitPushLineHandler` then becomes a tiny `LineHandler` impl:
- `should_skip` drops the high-volume progress phases (Enumerating /
Counting / Compressing / Writing objects, Delta compression, Total)
and blank lines.
- `observe_line` captures the up-to-date sentinel and the first ref
update target (e.g. `master`) for the summary.
- `format_summary` emits `ok <ref>` / `ok (up-to-date)` / `ok` on
success; nothing on failure (raw error lines already flowed through).
Stdin is inherited (`StdinMode::Inherit`) so SSH passphrase and HTTPS
credential prompts still reach the user. Tracking now records the real
raw output and the filtered output.
## Test plan
- [x] `cargo fmt --all -- --check`
- [x] `cargo clippy --all-targets -- -D warnings` — clean
- [x] `cargo test --all` — 1880 passed, 0 failed, 6 ignored
- Six unit tests cover the push handler: progress-prefix drop,
up-to-date summary, remote message passthrough, no-summary-on-failure,
first-ref-wins, and token-savings (>=60% on a representative payload).
- Four unit tests cover the new `LineStreamFilter` trait:
default-keep-all, skip-drops-matching, summary-propagates-exit-code,
observe-only-called-for-kept-lines.
## Notes
- Behaviour change: users now see git's native output line-by-line
(with progress phases stripped) plus a final `ok <ref>` summary,
instead of just the compact summary. This matches plain `git push`
more closely and is what the issue reporter expects.
- No regression for other filters: `run_pull`, `run_fetch`, `run_clone`
are untouched; only `run_push` is modified.
Closes#963🤖 Generated with [Claude Code](https://claude.com/claude-code)
---
_Vibe Coded by Ousama Ben Younes_
_Developed With Ora Studio (Claude Code)_
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Several filters dropped or rewrote information an agent needs, producing
output that was misleading rather than merely compressed. This fixes the
clear-cut cases surfaced by the TheDecipherist/rtk-test suite:
- ls: stop hiding the `.env` file (removed `.env`/`env` from NOISE_DIRS).
- git status: restore the explicit "HEAD detached at <sha>" line instead of
the opaque "* HEAD (no branch)".
- log/docker logs: recognise CRITICAL/FATAL/ALERT/EMERGENCY/SEVERE (and
NOTICE) as severities so those lines are no longer silently filtered;
expose `log` as a `rtk pipe` filter and accept a positional filter name.
- wc: forward stdin to the child so `cat file | rtk wc` counts the pipe
instead of reporting 0 (new RunOptions::inherit_stdin).
- jest/vitest: include skipped/pending test count in the compact summary.
- pytest: surface xfailed/xpassed counts and list XFAIL/XPASS entries with
their reasons (adds `-rxX`).
- ruff/eslint: list individual violations with file:line:col, not just
rule/file group counts.
- pnpm/npm list: render the actual package list instead of the false-positive
"All packages up-to-date" when there's no upgrade info.
- git add: stay silent on a no-op instead of printing an ambiguous "ok".
- git stash: pass git's own message through instead of collapsing to "ok".
- docker ps: keep the Status column (health/restart info) and list
stopped/exited containers; docker images: show all images with full names;
docker compose ps: use `-a` so crashed services stay visible.
All 1870 unit tests pass; cargo fmt + clippy clean.
KuSh: the in-progress state line ("rebase in progress", "merge in
progress. unresolved conflicts", etc.) is enough — LLMs know which git
commands resolve each state, so the per-state hint list was noise.
Removes STATE_HINTS and extract_state_hint, and reduces extract_state_header
to a short walk that returns the first detected state summary or None.
Existing per-state tests now assert the exact compact summary.
- Drop redundant LANG=C env (LC_ALL=C is sufficient).
- Replace nested if/else chains in detect_status_state and
extract_state_hint with const slices + iter for readability.
- Drop the 2-space indent prefix on hint lines — adds nothing for
machine consumption.
Default `rtk git status` runs `git status --porcelain -b` to build its
compact view, but porcelain v1 omits the state header git prints when a
rebase, merge, cherry-pick, revert, bisect, am, or sparse checkout is in
progress. That header is correctness-critical — hiding "You are currently
editing a commit while rebasing..." leaves the user thinking the repo is
clean when it isn't.
`run_status` already captures plain `git status` output as `raw_output`
for tracking, so this fix adds `extract_state_header` to pull the state
block out of it and prepends the block to the compact output. Returns
`None` when nothing is in progress so clean repos are unchanged.
Covered states: interactive / regular rebase, merge-with-unmerged-paths,
"still merging after conflicts fixed", cherry-pick, revert, bisect, am,
sparse checkout. Preserves the directive hints git prints alongside
(`git rebase --continue`, `git commit --amend`, `git bisect reset`,
etc.), while still filtering generic `(use "git add")` / `(use "git
restore")` noise.
Design principle: Correctness vs Token Savings — never hide information
that changes the user's understanding of repo state.
Consolidate stash default handlers to ensure "no local changes"
is properly detected across all stash operations, not just the
default push case.
Signed-off-by: Nicolas Le Cam <niko.lecam@gmail.com>
Branch names containing '/' (e.g. feature/user-auth, main...feature/auth)
were incorrectly treated as file paths by the string heuristic in
normalize_diff_args, causing '--' to be injected before them and making
git treat them as pathspecs — resulting in silent empty output (exit 0).
Replace looks_like_path() with a three-tier detection strategy:
- Explicit path prefixes (. ~) → always a path, no filesystem check needed
- Contains path separator (/ \) → use filesystem existence to distinguish
branch names (feature/auth, not on disk) from real paths (src/main.rs)
- Bare word with no separator → never inject '--', regardless of filesystem
state (avoids misfire when a file shares a name with a branch/ref)
Introduce normalize_diff_args_impl with injectable path-checker for
testability. Update all existing tests to use mock existence checks.
Add three regression tests: branch-with-slash, range-with-slash, and
bare-word-that-exists-on-disk.
Fixes: #1431
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extracts parse_commit_output into a testable helper function and adds 7 comprehensive unit tests covering:
- Normal branch names
- Root-commit annotations
- Multibyte branch names (Chinese, Thai) — regression test for the panic fix
- Edge cases (no bracket, short hash, empty input)
Tests verify the fix handles UTF-8 safely by anchoring on ']' and using char-safe slicing.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
git commit on an initial commit under a Chinese locale outputs:
[master(根提交) fb597ec] Initial commit
The old code found the first space and sliced line[1..hash_start],
yielding "master(根提交)" (21 bytes). hash.len() >= 7 was true,
so it tried &hash[..7] — but byte 7 falls inside the 3-byte '('
character, causing a panic.
Two bugs were present:
1. Logic: the code grabbed the token *before* the first space
(branch name), not the commit hash. The hash is always the last
whitespace-separated token inside the [...] brackets.
2. Safety: raw byte slicing on a potentially non-ASCII string.
Fix: find ']', extract the bracket content, split_whitespace() and
take next_back() to get the hash. Use chars().take(7) for the
short-hash slice so it is always char-boundary-safe.
clap's trailing_var_arg=true silently drops -- when it appears as the first
positional argument. This caused `rtk git diff -- <path>` to arrive in
run_diff without the separator, making git treat the path as a revision and
emit "fatal: ambiguous argument". Adds normalize_diff_args() which re-inserts
-- before the first path-like argument (contains /, \, starts with . or ~)
when -- is absent from the args vec. The fix is a no-op when -- is already
present (e.g. rtk git diff HEAD -- file preserves the separator correctly).
Generated by Claude Code
Vibe coded by ousamabenyounes
Co-Authored-By: Claude <noreply@anthropic.com>