159 Commits

Author SHA1 Message Date
lvdeqing ceae215ff7 test(hooks): scope Trae audit assertion to Unix as reviewed 2026-09-18 15:23:56 +08:00
lvdeqing 0184ba2425 fix(hooks): address Trae portability and partial install diagnostics 2026-09-18 15:08:49 +08:00
lvdeqing 651441cb52 chore: merge latest develop into Trae hook integration 2026-09-18 14:47:34 +08:00
lvdeqing dbecb980f9 fix(hooks): honor audit directory override for portable Trae tests 2026-09-18 14:26:58 +08:00
Nicolas Le Cam d2e906a0fe fix(gain): honour suppress_hook_warning, and pin the env/config composition
`rtk gain` printed its own missing-hook line from a bare `status()` match,
outside `maybe_warn`, so neither `hooks.suppress_hook_warning` nor
`RTK_SUPPRESS_HOOK_WARNING` reached it — on the report the no-hook audience
reads most. Gate that arm on the same helper; the outdated-hook prompt stays
visible, as it does everywhere else.

The new unit tests exercise the parser alone, so folding env and config
together with `||` — which drops the falsy force-off — left the whole suite
green. Drive the composition through the binary instead, and give the env
table the `=0` row that overrides the config flag.

The suite pins `XDG_CONFIG_HOME`/`XDG_DATA_HOME` to the temporary home and
seeds the config in both the XDG and the macOS location, so the loader cannot
resolve past it: `dirs::config_dir` is `~/.config` on Linux but
`~/Library/Application Support` on macOS, and a runner that exports either
`XDG_*` reached the developer's own config before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 02:16:12 +02:00
Matt Van Horn 4e53f760fe fix: scope suppress_hook_warning to missing hooks and parse the env override
Parse RTK_SUPPRESS_HOOK_WARNING as a truthy/falsy override that falls
back to config when unset or unrecognised, keep the HookStatus::Ok fast
path free of config loading, and suppress only the missing-hook warning
so the outdated-hook upgrade prompt stays visible.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CNmJZVV9u6pBhcwVhwkPp6
2026-09-16 07:46:02 -07:00
Matt Van Horn 0e7a41eb52 feat(config): add suppress_hook_warning option
Add hooks.suppress_hook_warning config option and RTK_SUPPRESS_HOOK_WARNING
env var to disable "No hook installed" and "Hook outdated" warnings.

Users running rtk via CLAUDE.md instructions instead of hooks, or with
tools like OpenCode, get these warnings on every command. The warnings
waste tokens and confuse AI agents since rtk is working correctly.

RTK_SUPPRESS_HOOK_WARNING=1 suppresses; any other value forces the warning
on; unset falls back to the config file. Both default to false so existing
behavior is unchanged. The env parse follows the repo's
as_deref() == Ok("1") convention.

The suppression check runs before status() so the suppressed path does not
pay for the hook probe.

Documented in configuration.md (the hooks block and the env var table) and
README.md.

Fixes #682

Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-09-16 07:27:24 -07:00
lvdeqing ca24862484 chore: merge latest develop into Trae hook integration 2026-09-15 20:24:01 +08:00
Nicolas Le Cam 7c18567155 chore: migrate to Rust edition 2024
Bump edition 2021 -> 2024. rust-version stays at 1.91, already well above
the 1.85 floor the edition needs; docs/guide/resources/troubleshooting.md
still claimed 1.70+, which is where a failed `cargo install --git` lands.

Four things the edition forces:

- std::env::{set_var,remove_var} are unsafe in 2024 with no safe std
  replacement. Rather than wrap the test call sites in unsafe -- which the
  crate denies and .semgrep.yml flags -- route them through temp-env, a
  dev-only dependency whose closure API is safe and which restores the
  previous value even when the body panics. The hand-rolled CLAUDE_DIR_LOCK
  and PI_DIR_LOCK guards existed only to serialise those mutations and are
  now redundant; CWD_LOCK and TEST_ENV_LOCK stay, they order more than the
  env var itself.

- unsafe_op_in_unsafe_fn is on by default, so the libc calls in the proxy
  signal handler and in stream.rs's relay handler need explicit unsafe
  blocks, scoped to the libc calls themselves.

- `gen` is a reserved keyword, so the closure by that name in diff_cmd.rs
  becomes make_lines.

- Tightened tail-expression temporary scopes let clippy prove the binding in
  setup_test_env is inlinable, so let_and_return now fires there.

if_let_rescope changes when the scrutinee temporary drops in an if let/else.
The two sites in show_claude_config take cargo fix --edition's match rewrite,
which keeps the 2021 drop timing.

rustfmt.toml is kept rather than dropped: cargo fmt passes --edition from
Cargo.toml, but a bare rustfmt invocation has no crate context and falls
back to edition 2015, which cannot parse the let-chains the next commit
introduces. Pinning it there keeps format-on-save and pre-commit hooks in
agreement with CI.

clippy::collapsible_if is allowed crate-wide for now; the follow-up commit
adopts let-chains and removes the allow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 01:26:43 +02:00
Nicolas Le Cam 70ec493d0d chore: list command modules explicitly instead of via automod
Every ecosystem mod.rs under src/cmds/ generated its module list with
automod::dir!(). rustfmt only follows literal `mod` items and cannot expand
macros, so `cargo fmt` never reached any of those files -- and neither did
CI's `cargo fmt --all -- --check` gate, which has been silently passing
over half the crate.

Demonstrated by appending `fn   __probe( )->u8{let    x=1;x}` to a file
under src/cmds/: with automod, `cargo fmt --all -- --check` reports no
diff; with an explicit `pub mod`, it reports the diff and `cargo fmt --all`
fixes it. The preceding commit had to invoke rustfmt directly for the same
reason; from here `cargo fmt` covers the crate on its own.

Listing modules by hand trades one failure mode for another, so build.rs
now guards the new one: automod compiled any stray .rs file, whereas an
explicit list silently drops a file whose `pub mod` line is forgotten --
never compiled, never linted, tests never run, and check-test-presence.sh
still reporting PASS because it only greps the file for #[cfg(test)]. The
build now fails with the missing declaration named.

Declaring the modules explicitly also exposed clippy::module_inception on
cmds::git::git, which clippy skips inside macro-generated code. git.rs is
renamed to git_cmd.rs, which resolves the lint and matches the convention
its siblings already follow -- diff_cmd, gh_cmd, glab_cmd, gt_cmd. Docs
that point at the module are updated; sample command output that merely
shows a "git.rs" path is left alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 01:26:43 +02:00
Nicolas Le Cam cfe248c1f6 Merge pull request #3941 from iliaal/fix/faithful-head-tail-windows
fix(read): make head/tail rewrites faithful to the native commands
2026-09-14 00:59:57 +02:00
Nicolas Le Cam e224b159af docs(read): point the head/tail rewrite docs at --head-lines
The pipeline diagram, the discover walkthrough and the `rtk read` option
table still described the `--max-lines` mapping this branch replaced, and
the repo's own suggest hook still proposed `--max-lines N` for `head -N`
— the mapping this branch exists to correct.

Drop the `head -` disjunct in the rewrite guard: every string with that
prefix also has the `head ` prefix that sits next to it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 00:45:35 +02:00
Nicolas Le Cam cb5e5994e1 fix(tracking): align the telemetry rates with rtk gain and pin them in memory
`get_by_command` moved to the volume-weighted rate while this branch was
open (#891), summing every call and guarding the division at group level.
The two telemetry queries now use the same shape: the row-level
`WHERE input_tokens > 0` is gone, so a call with no input that still
printed something counts against its command exactly as it does in the
By Command table, and `HAVING SUM(input_tokens) > 0` keeps commands that
never had any input out of both, as before.

`low_savings_commands` lists net-regressing commands: `sav > 0.0` became
`sav <> 0.0`, so only exact 0% stays with `passthrough_top`. A command
that emits more than it saves is the filter to fix first, and with the
weighted rate the old bound also dropped commands the unweighted query
used to list.

The two regression tests opened the on-disk tracker, so every `cargo test`
wrote ten synthetic rows (one at 100k input tokens) into whichever history
database `Tracker::new()` resolves to, on a developer machine the real one.
They now use `Tracker::new_in_memory()` like the rest of the module, and
`test_avg_savings_per_command_inner_weighted` only asserted a `0..=100`
range that the unweighted query also satisfied. Both tests now assert the
exact weighted figures, check each listed rate against `get_summary()`,
and fail when either query goes back to `AVG`, to the row filter, or to
the positive bound.

Tests are numbered 18 and 19 after the ones develop already carries as
14-17. The `low_savings_commands` doc no longer points at a PR, and both
telemetry field tables describe the new semantics.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 21:05:32 +02:00
Nicolas Le Cam 4b6c0f00ef Merge remote-tracking branch 'upstream/develop' into pr-1298-merge 2026-09-13 20:35:13 +02:00
Nicolas Le Cam 3d2d244406 fix(ast-grep): account for every dropped line and stop wrecking scan output
Two defects in `filter_ast_grep`, both verified against ast-grep 0.45.3.

`ast-grep scan` diagnostics parse only on their locator line, so filtering
line by line kept `  ┌─ a.rs:2:13` and dropped the rule id, severity,
message and source line — at exit code 0, so it read as success. The
whole-output fallback never fired because `order` was not empty.
`unparsed_signal()` now passes any shape through untouched when a single
non-blank line fails to parse, which is what `search.rs` already does for
grep/rg. This also covers `--heading` mode and Windows drive-letter paths,
where `[^:]+` cannot match `C:\src\a.rs`.

The per-file overflow hint was computed as `entries.len() - max_per_file`,
which ignores `max_total` cutting a file short: a file under its own cap
lost its remainder with no hint at all, and a file over it under-reported
the drop. Both now count against what was actually printed.

The hint said "matches" while counting lines. ast-grep prints one line per
matched source line and a structural match spans several, so a repo search
reported "19 more matches" where five matches remained.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 12:16:33 +02:00
Nicolas Le Cam 8b4c445815 Merge branch 'develop' into feat/ast-grep-support 2026-09-13 12:11:52 +02:00
Yijie Xu 285fb68717 fix(hooks): address Codex integration review feedback 2026-09-13 16:50:19 +08:00
Yijie Xu 7f2788a1b4 feat(hooks): add direct Codex command rewrite 2026-09-13 16:50:19 +08:00
Nicolas Le Cam 6e16ad847f Merge upstream/develop into feat/arg-tokenizer
develop rewrote `run_show`'s routing underneath this branch (#3265, eight
commits) into `ShowRoute` with a `cat-file` probe for blob detection. That
structure is newer and better than what this branch had, so it is kept whole
and this branch's contribution is folded into it rather than the other way
round.

`commit_or_stat_route` now classifies with the tokenizer instead of scanning
strings. develop matched `a == "--stat"` and `a.starts_with("--pretty")`,
which reads a pathspec named `--stat` past the boundary as the flag and
`--prettyish` as `--pretty`, and covers three stat spellings where the set of
shapes the compaction cannot render is larger. `git show -- --stat` takes the
compact path again.

`consumes_next_token_as_value` is reimplemented on `log_takes_value` rather
than restored as its own table. develop's copy is a subset -- it omits
`--max-count`, `--ignore-matching-lines`, `--min-age`, `--max-age` and
`--stat-graph-width` -- and keeping both would be the two-lists-drift this
branch exists to remove. Its callers are unchanged, so blob-show keeps its
own grammar rules; the attachment distinction (`-M50` attaches, `-M 50` does
not) is what the tokenizer adds.

This branch's `is_blob_show_arg` and free-positional blob pre-filter are
dropped in favour of develop's, which also handles index and merge-stage
blobs (`:file`, `:2:file`) and probes with `cat-file` rather than trusting
the shape of the argument.

Verified: seven blob-show invocations byte-identical to develop, including
the `-wG a:b HEAD:blob` cluster their walker exists for; 3536 unit tests and
every integration suite green; clippy clean.

develop's `show_positionals` carries a `TODO(after #3681)` to replace its
hand-rolled short-cluster walk with the ValueSpec factorization. Left alone
deliberately -- that is follow-up on develop, as its author intended, not
something to change inside this merge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 22:49:00 +02:00
Nicolas Le Cam 66eeb10bdf Merge pull request #3265 from breisnerlopez/feat/git-show-large-blob-preview
feat(git): filter large git show blob dumps with recovery and Latin-1 decoding
2026-09-11 17:21:52 +02:00
Nicolas Le Cam 22d49e327f Merge pull request #1318 from kerta1n/feat/winget-automation
feat(ci): add winget manifest automation
2026-09-11 12:37:41 +02:00
kerta1n 7be287958e docs: list winget in the installation page description
The frontmatter description renders as the page's meta description and
search snippet, so it advertised an install method list that omitted the
one the page now documents.
2026-09-09 14:21:43 -04:00
Adrien Eppling 7c10f7791c fix(benchmark): ignore the recall hints when counting find entries
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNqtCagEUryc1fgw62APkm
2026-09-08 16:53:02 +02:00
breisnerlopez ed106db06f docs(git): warn that a hand-written 'git show rev:path > file' can truncate
A large blob is windowed to a preview with a recovery hint, so redirecting a
manual 'rtk git show HEAD:x > file' can silently truncate (exit stays 0). Note
it in the git-show user docs and point at plain 'git show' / the recovery hint.
2026-09-08 16:19:21 +02:00
Adrien Eppling feb8aeb644 revert(recall): drop tee_on_success, recovery stays failure and truncation driven
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNqtCagEUryc1fgw62APkm
2026-09-08 14:12:27 +02:00
Adrien Eppling 8969fa49c5 feat(recall): tee_on_success restores legacy always behavior, accurate migration notices
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNqtCagEUryc1fgw62APkm
2026-09-08 14:12:25 +02:00
Adrien Eppling b158152ef7 feat(telemetry): report recall efficiency counters per filter family 2026-09-08 14:12:16 +02:00
Adrien Eppling a53373191d feat(recall): content-addressed recall store with selectable [retriever] mode
sqlite (default) queried by 'rtk recall'; tee (legacy files) and disabled modes retained.
2026-09-08 14:12:15 +02:00
Nicolas Le Cam c81c09775d docs: move the prompt-caching answer into the published guide
The section landed in docs/TROUBLESHOOTING.md, a flat file removed in
a94e9493 when the docs were consolidated. Content there does not reach
the published guide, and the copy reintroduced install guidance that
develop had already pinned to --branch master.

Move the answer into docs/guide/resources/troubleshooting.md and drop the
duplicated Type Kit collision section, which that guide already covers.

The cache write/read breakdown is reported by `rtk cc-economics`, not by
`rtk gain`, so point readers at the command that actually shows it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:54:14 +02:00
Nicolas Le Cam faaa446b9f Merge upstream/develop into docs/prompt-caching-faq 2026-09-08 01:54:00 +02:00
kerta1n 2f905f7b62 docs: complete winget coverage, normalise install headings
- README_pt.md: add the winget section (only translation missing it)
- docs/guide/getting-started/installation.md: add winget install and
  uninstall entries
- README_{es,fr,ja,ko,zh}.md: drop the superlative from the winget
  heading so all seven READMEs read "winget (Windows)", matching the
  existing "Homebrew (recommended)" convention
- README.md: shorten the Native Windows heading to "(manual install)",
  move the winget pointer into prose

Deliberately out of scope: docs/guide/getting-started/installation.md
still tells Windows users to use WSL "for full hook support". README.md
contradicts this - native Windows hooks have worked since v0.37.2.
Correcting it means rewriting text this PR does not otherwise touch, so
it is left for a separate change.
2026-09-07 17:26:55 -04:00
Adrien Eppling 280d0918b5 Merge origin/develop into fix/pipe-safe-consummer-and-slim-instructions
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UAVhpAvWDmsEWuLehm9RAo
2026-09-07 18:29:15 +02:00
Adrien Eppling 148e0d443e fix(rewrite): PipelineSafety enum, quote-aware consumer args, fd-dup redirects, stage rewrite consolidation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UAVhpAvWDmsEWuLehm9RAo
2026-09-07 14:44:15 +02:00
Nicolas Le Cam 66901427f0 docs: require arg_tokenizer for new commands, flags and arg fixes
The module landed with no statement of when to use it, so the next command or
flag can still be written with a string scan -- which is how every bug this
migration fixed got in.

States the rule where each audience meets it: the non-negotiable list agents
load (`.claude/rules/rust-patterns.md`), the checklist for adding a filter
(`src/cmds/README.md`), and the architecture map (`ARCHITECTURE.md`), all
pointing at one place -- `src/core/README.md` -- for the four rules the
migration paid for: one grammar per subcommand, scope the lookup to the region
the tool parses, inject before the boundary, detect and act with one rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 12:14:09 +02:00
Adrien Eppling 9bc4323916 Merge origin/develop into clean/awareness-file
Resolved: awareness constants vs Pi/OMP rework, InitContext destructures,
bun/deno CLI additions, write_if_changed split.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DVD4ZD5wiSjSsNpKCK6h7q
2026-09-07 11:06:29 +02:00
Christian Marbach ba802ac82d docs(ast-grep): document rtk ast-grep in README and usage guides
Adds the ast-grep row/section to README.md, what-rtk-covers.md, and
FEATURES.md, matching how rtk grep/rg are documented there.
2026-09-07 10:34:00 +02:00
Nicolas Le Cam e53ec1cf18 Merge pull request #3707 from alvins82/omp-shared
feat(omp)!: add Oh My Pi (OMP) support
2026-09-05 01:54:17 +02:00
patrick a8bb67a853 fix(rewrite): stop rewriting sudo commands (pass them through)
The env-prefix stripper treated `sudo` like `env` / `VAR=val` and rewrote
`sudo docker ps` into `sudo rtk docker ps`. That breaks at runtime: `rtk`
lives in ~/.local/bin, which is not on sudo's secure_path, so the rewritten
command fails with "rtk: command not found" under root (reported in #146).
And where rtk *is* on secure_path, `sudo rtk` would run the whole rtk binary
as root — an unnecessary-privilege footgun.

Drop `sudo` from the env-prefix regex so sudo commands pass through
untouched. The permission verdict path is unaffected (it never used this
regex and already matches sudo commands as-is, e.g. `sudo:*` rules). env /
VAR= prefixes and transparent builtins (noglob, command, …) still rewrite
normally.

Verified: `sudo docker ps` / `sudo -u root docker ps` / `sudo noglob git
status` are no longer rewritten; `env FOO=bar docker ps`, `FOO=bar docker
ps`, `noglob git status` still are. fmt/clippy clean, full test suite green.

Refs #146

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-03 22:35:01 +02:00
alvins82 d302797c51 fix(hooks)!: harden aliased Pi and OMP extension lifecycle
Treat pre-existing extensions without ownership state as uncertain, cover project-scope aliases, clean canonical sidecars after symlink removal, and classify protected extension overwrites as breaking.

BREAKING CHANGE: non-interactive installs of modified or unrelated Pi/OMP extensions now require --auto-patch to approve overwrites.
2026-09-03 11:04:19 +12:00
alvins82 ba6a1f57a3 fix: harden shared extension ownership handling 2026-09-03 09:35:36 +12:00
aesoft 988c019ce0 Merge pull request #3803 from KuSh/docs/exclude-commands-known-gaps
docs(config): scope the exclude_commands coverage claim to what is peeled
2026-09-01 13:51:43 +02:00
Nicolas Le Cam 976a1d7630 docs(config): scope the exclude_commands coverage claim to what is peeled
`configuration.md` said an excluded tool "is covered however it is invoked".
The stated limits then covered only tools without their own filter, and exactness
— leaving three invocation forms of *filtered* tools promised but not delivered.

Scope the sentence to the wrapper, interpreter and path spellings that are
actually peeled, and table the three that are not, each with its cause:

- `head -20 f` / `tail -n 5 f` — the line-range fast path returns before the
  exclusion is consulted (#2823); `head f` is excluded normally
- `gradlew.bat build` / `mvnw.cmd test` — path stripping splits on `/`, so a
  `.bat`/`.cmd` spelling never reduces to the tool name (#3617)
- `golangci run ./...` — `golangci run` is one of the rule's own aliases and is
  kept whole, so it misses a `golangci-lint` entry

Each row verified against a build on `develop` at e533c40, including the
documented workaround: `["golangci"]` excludes the alias form but not
`golangci-lint run`, so both entries are needed.

Docs only, no behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 12:42:04 +02:00
Ilia Alshanetsky 844d6fa3d1 fix(git-diff): end hunks at their declared length, keep context adjacent
A hunk ran until the next header, so any trailing text was read as its body.
`gh pr diff --patch` routes here, and GitHub's `.patch` is an mbox: each patch
carries a bare `---` before the diffstat and a `-- ` signature after the diff,
both at column 0. With hunk bodies now anchoring, a two-commit mbox with two
real deletions reported `grep -c "^-"` = 5. `@@ -a,b +c,d @@` declares its own
line counts, so the hunk now ends when they are consumed and anything after it
is outside every hunk. That covers trailing prose and truncated input too,
rather than the shapes we happen to know about, and a heuristic on `---` / `-- `
could not have worked: a deleted line whose content is `- ` renders as `-- `.

`--patch` also joins `has_non_diff_format_flag`, so an mbox passes through
whole. Someone asking for a patch wants one that applies.

Leading context keeps the last lines before the change rather than the first.
With `-U10` or `--function-context` the first three sit ten lines above it, and
emitting those tells the reader that ctx3 precedes the deletion when ctx10 does.
A three-slot ring buffer costs the same lines. The diff-wide budget is charged
when the buffer drains, so a line the ring evicted costs nothing.

Combined diffs carry one marker column per parent, so a line changed against
only one of them holds its marker in column 2. Those were counted as context:
neither in the `+N -M` tally nor reachable by an anchored grep. The tally reads
both columns now. The grep gap is real and cannot be closed without rewriting
the line, so FEATURES.md states it alongside the per-hunk cap. Against
`git diff --numstat` on three complete commit ranges the tally is now exact,
where master under-counted one of them by +2 -1.

`diff_header_path` keeps paths containing spaces: git does not quote them, so
`diff --cc my file.txt` needs the whole remainder, not the last token.

FEATURES.md said 15 lines for a 24-line block and showed the stat's first line
with a leading space it does not have. It now also carries a measured example
with real compression, since the two-file one saves 11% in a document
advertising 60-90%.
2026-09-01 12:41:51 +02:00
Ilia Alshanetsky 5d4b5492cf fix(git-diff): reset hunk state on combined diffs, exempt leading context
Dropping the `+++` / `---` guards assumed those headers only appear before the
first `@@`. That holds for `diff --git` sections. It does not hold for the
`diff --cc` sections git emits for unmerged paths: `diff --cc` never matched
the `diff --git` branch, so `in_hunk` stayed set from the previous section and
the `--- a/z.txt` / `+++ b/z.txt` headers of every later file were printed at
column 0 and counted in the tally. On a two-file conflict with no deletions,
`rtk git diff | grep -c "^-"` returned 1. The state reset now fires on any
`diff --` prefix, and the path falls back to the trailing token for the
`diff --cc <path>` shape, so the headers land before the first `@@` again.
Same repo now returns 0.

Leading context no longer counts against `max_lines`, so it cannot displace
change lines. Its previous justification was wrong: before this branch, leading
context was never emitted, so it could not have crowded anything out of
`max_hunk_lines`. The cost it does carry is on the global budget, and on a
five-commit diff of this repo it cost 57 change lines (144 shown, against 201
with the exemption). A diff-wide cap of `max_lines / 10` bounds what the
exemption gives back: without it, a diff of many small hunks would spend three
exempt lines on every one of them. The same diff comes out at 71.7% byte
reduction against 71.8% on master, which showed zero anchorable change lines.

`docs/usage/FEATURES.md` showed change lines indented under a
`src/main.rs (+5/-2)` header, which no version of rtk emits. It now carries
real captured output, plus the two limits of the anchored audit: the 100-line
per-hunk cap, and that the output is not an applyable patch.
2026-09-01 12:41:51 +02:00
aesoft e533c40901 Merge pull request #3749 from KuSh/fix/exclude-commands-wrapper-forms
fix(hooks): match exclude_commands against the peeled command form
2026-09-01 10:04:32 +02:00
Nicolas Le Cam 2a49e529bb Merge pull request #3125 from derrik-fleming/docs/update-config-example
docs(config): update example to include `max_file_size`
2026-08-31 20:23:52 +02:00
Nicolas Le Cam eb3f814872 refactor(grep): trim comments and align the siblings of the flag change
Condense the Grep variant and its tests to the comment rules in
CONTRIBUTING.md (explain why, not what), and carry the -l/-t change
through the files that still assumed the old behavior.

- scripts/test-aristote.sh: add -r to the two grep assertions, since
  `grep PATTERN <dir>` exits 2, and move the type filter to `rtk rg`,
  the engine where -t is valid.
- docs/usage/FEATURES.md: extra args go to the engine actually invoked,
  not always to rg; drop the per-row shortcut justifications.
- tests/search_compress_test.rs: guard grep-engine tests on
  grep_available() instead of rg_available(), which skipped them into a
  false green on a box without ripgrep.
- .claude/hooks/rtk-suggest.sh: reverted; the rg -> rtk rg fix is
  correct but belongs in its own PR.

BREAKING CHANGE: `rtk grep --file-type` and its `-t` short are removed.
The option never reached the engine, so it was a silent no-op; `-t` now
flows through, which means `rtk rg -t rust` filters by type while
`rtk grep -t rust` returns grep's own `invalid option -- 't'`. `-l` and
`-m` under `rtk grep` are likewise the native grep flags now rather than
rtk's --max-len and --max.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 01:58:35 +02:00
Brandon Harper 187228dc50 fix(grep): repair the smoke assertion this PR invalidated
scripts/test-all.sh asserted `rtk grep "pub fn" src/ -t rust` exits 0. That
only held because --file-type swallowed `-t` and never passed it to the
engine, so the assertion was pinning a silent no-op. With --file-type gone,
`-t` reaches GNU grep, which has no such flag:

    $ rtk grep "pub fn" src/ -t rust
    grep: invalid option -- t
    exit=2

Replaced with an assert_fails pinning that rejection, plus an assert_ok
against rg (which does have -t), guarded on rg being installed. Verified:

    rtk grep "pub fn" src/ -t rust  -> exit=2  (assert_fails wants non-zero)
    rtk rg   "pub fn" src/ -t rust  -> exit=0  (assert_ok wants 0)
    $ bash -n scripts/test-all.sh
    syntax OK

Also drops a `--line-numbers | -n` row from the FEATURES.md grep table. No
such option exists on Commands::Grep -- its fields are max_len, max,
context_only, extra_args. The `line_numbers` field at src/main.rs:118
belongs to Read. The row predates this PR but sits in the table it edits.

Note, NOT fixed here as it is pre-existing and unrelated: the adjacent
`assert_ok "rtk grep pattern" rtk grep "pub fn" src/` on the line above
also fails, because grep without -r on a directory exits 2. The smoke
suite is not wired into CI (referenced only from
.github/copilot-instructions.md), which is how it rotted unnoticed.
2026-08-30 11:22:22 -06:00
Brandon Harper 16228b74b0 fix(grep): stop -l and -t shadowing native grep flags
rtk's own tuning options on `Commands::Grep` declared short forms that
collide with native grep/rg flags of the same letter. This removes the two
that remain, and deletes one option outright.

`--max-len` loses `-l`. `-l` is grep's --files-with-matches, and the old
binding split behavior on whether the PATTERN parsed as a usize:

  - Non-numeric: clap rejected it, run_fallback re-ran raw grep, and the
    user got the right answer. Wasteful, not harmful.
  - Numeric: clap ACCEPTED it, and the result was silently wrong.

        # hit.txt contains "listen on port 8080 today"; miss.txt does not
        $ rtk grep -l 8080 hit.txt miss.txt
        before:  (no output)   exit=1     <- claims nothing matched
        after:   hit.txt       exit=0

    `-l 8080` set max_len=8080, leaving hit.txt as the pattern and miss.txt
    as the only path. With a single file the filename becomes the pattern,
    no path is left, and rtk falls back to stdin: exit 1 against /dev/null,
    hangs on a terminal. Port numbers, years and error codes are ordinary
    patterns, and the Claude Code hook rewrites plain `grep` into `rtk grep`
    transparently -- so this reached users who never opted in.

    Note this is not a savings story. `-l` is a has_format_flag passthrough
    on the fixed path too (search.rs:548), so there is no compaction for
    this flag either way. The bug was a wrong answer.

`--file-type` is removed entirely, with its `-t` short, rather than left as
a parse-only no-op. It was destructured as `file_type: _` at the call site
and never reached search::run, so it parsed and then did nothing.
`rtk rg -t rust` is unaffected: Rg has no such field and forwards `-t` to
real ripgrep.

Deliberately typed `fix`, not `fix!`, even though a documented long option
disappears. The option was inert -- it never filtered anything -- so no
working behavior is lost, and #3259 set the precedent for removing a
colliding grep short under plain `fix`. Recording the choice here because
silence would read as an oversight.

User-visible consequence: `rtk grep -t rust` goes from silently returning
unfiltered results to surfacing the engine's own error. BSD/macOS grep
prints `invalid option -- t`; GNU grep prints `invalid option -- 't'`.

`--max`/`-m` is NOT part of this change. #3259 removed that short already;
this branch is rebased onto it and preserves its rationale comment verbatim.

The struct NOTE deliberately does not claim to finish the job. clap's auto
`-h` still shadows grep/rg's --no-filename (that is #2532's scope), and
removing a clap short does not make a letter safe end to end: search.rs
shares one VALUE_FLAGS_SHORT table across both engines, so a letter taking
a value in rg but not grep still eats the next token inside a cluster --
`rtk grep -rt FOO .` consumes FOO as -t's value regardless of clap.

Adds clap-layer tests pinning the routing, including characterization tests
for behavior that was previously untested.
2026-08-30 11:22:22 -06:00
Nicolas Le Cam 9ba523960b fix(hooks): match exclude_commands against the peeled command form
`exclude_commands` entries name a tool, but a command can spell that tool with a
wrapper (`npx playwright test`), an interpreter (`python3 -m pytest tests/`) or a
path (`vendor/bin/phpunit tests/`). Those spellings are absorbed by each rule's own
pattern rather than stripped beforehand, so the anchored `^playwright($|\s)` never
matched and the exclusion silently did nothing — the README shipped
`exclude_commands = ["curl", "playwright"]` as the example, and `playwright` is a
tool almost nobody invokes bare.

Peel the wrapper off the command and match what remains, alongside the existing
check on the typed command. The peeled form keeps the arguments, so an anchored
entry still narrows the way it was written: `"^ls$"` excludes a bare `ls` without
swallowing `ls -la`.

Peeling uses the rule's own `rewrite_prefixes`, taking the shortest token-suffix of
the matched prefix that is itself a prefix of that rule. That drops `npx` and
`python3 -m` while keeping a subcommand the rule treats as part of the tool, so
`golangci-lint run` does not collapse to `run`.

Peeling reuses the PHP normalization the rewrite path already applies (`php`
wrapper and ini flags, leading `./`, vendor/composer bin dir), extracted into
`php_tool_form` and shared by both, so `php vendor/bin/phpunit tests/` is excluded
by `["phpunit"]` the same way `vendor/bin/phpunit tests/` is.

The peeled check is gated on a non-empty `exclude_commands`, keeping the default
config off the `RULES` scan on the hook rewrite path.

Matching the resolved `rtk` target instead would have been shorter but wrong in
both directions: it misses tools whose target differs from the binary (`["eslint"]`
would still rewrite `npx eslint .`, since the target is `lint`), and it leaks
across tools sharing a target (`["read"]` would exclude `cat`, `["git"]` would
exclude `yadm`). Peeling has neither failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 15:28:30 +02:00