Skip to content

fix(ruff): recognize current format --check wording - #1

Open
cestercian wants to merge 507 commits into
developfrom
fix/ruff-format-check-filter-wording
Open

cestercian wants to merge 507 commits into
developfrom
fix/ruff-format-check-filter-wording

Conversation

@cestercian

@cestercian cestercian commented Sep 9, 2026 •

Copy link
Copy Markdown
Owner

Mirror of upstream — use rtk-ai/rtk PR for review. Fixes rtk-ai#3930.

KuSh and others added 30 commits September 18, 2026 01:31
…net-trx-mtime

fix(tests): make dotnet_trx mtime-dependent tests deterministic
…sition

`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>
…ning-config

feat(config): add suppress_hook_warning option
std::process::Command wraps an argument in quotes only when it contains a
space or a tab, so an argument like a"b reaches the raw command line bare.
MSVCRT and UCRT parse that back correctly, Cygwin and MSYS do not, which is
why `rtk grep -c '"type"'` silently matched nothing (rtk-ai#3727). Re-encode with
libuv's quote_cmd_arg rules, and only when the argument contains a quote, so
every argument that works today keeps its exact bytes. Batch programs stay
on the old path: raw_arg would bypass std's CVE-2024-24576 mitigation, and
gradlew.bat and mvnw.cmd both reach run_passthrough.

tree forwards the user's own arguments the same way (rtk-ai#4101). MSYS2 ships an
MSYS-linked tree, so it belongs with ls, wc and find rather than with the
native-only spawn sites, which are deliberately left alone.
…by command

Matching the RTK command alone settled the round-1 defect, where a matcher or
timeout the user had edited made uninstall report "nothing to remove" and a
reinstall append a duplicate. It also went one step too far in both directions,
and this restores the two distinctions that were lost with it.

Presence now only counts a registration that would run for the tool RTK serves:
a group with no matcher, or one naming RunCommand among its `|`-separated
tools. Anything unrecognised counts as not covering RunCommand, so an install
adds an entry that fires rather than skipping one that never would. Removal
stays matcher-blind, which is what round 1 asked for, but skips an entry whose
explicit type is not `command`: a `prompt` entry is by construction the user's,
and deleting it also took the group and its `matcher` and `description` keys.

The "Already updated" list was also per-run, so the rerun the documentation
recommends after a write failure reported "none" while a target was fully
installed. It is now seeded from the preflight, and the uninstall path — which
has the same two-target failure mode — reports it too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ild-arg-quoting

fix(core): quote child arguments so MSYS children receive them intact
`git show --oneline -p <sha>` printed the requested commit's header and then
HEAD's stat and full patch. The stat step drops `--oneline`, which shifts every
later argument down one index, then strips patch-shape flags using the token
indices of the unshifted args: it deleted `<sha>` and forwarded a bare `-p`,
which outranked RTK's `--no-patch`. Re-tokenize the shifted args. Same path for
`--patch`, `-U<n>` and `-W`.

`git log -p` caps the walk at 10 commits, and the notice that says so was driven
by counting `commit <hex>` headers in RTK's own output. `--oneline` emits no such
header, `log.decorate` and `--graph` disfigure the one there is, `-z` runs the
walk onto one line, `--line-prefix` puts something in front of it, colour puts an
escape there, and a SHA-256 name is not 40 characters -- on each of those the cap
took 15 of 25 commits with nothing on stderr. Ask git for the one commit just
past the limit instead and read only whether anything came back, which no output
shape can disfigure.

What that probe forwards is pruned to what bounds the walk: the format flags,
because RTK's own is written first and would lose the arbitration; `--skip`,
which RTK's has to absorb; `--exit-code`, which would report failure on a walk
git was happy with; `--output`, a redirect that would truncate the file the
command just wrote; and the patch-shape flags, which are work with no answer in
them and one extra run of the user's `diff.external` program.

`--patch-with-stat` and `--patch-with-raw` now count as patch requests, which
they always were -- `git show`'s stat header was carrying a full patch under
both spellings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`rtk read <file> --head-lines N` read the whole file before slicing, so on a
source with no end there was nothing to slice: `head -n 5 /dev/urandom` returns
five lines instantly, and the rewritten form never returned at all. That path is
reachable now that `head -n N` and `head --lines N` rewrite to it.

Read in chunks and stop at the Nth newline, as head does, retrying an
interrupted read the way the `fs::read` this replaces did. Byte-for-byte the
same answer as before on a regular file -- across chunk boundaries, on CRLF
endings and on an unterminated last line; the savings baseline comes from the
size on disk, and a source that has no meaningful size claims none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Forwarding stderr verbatim keeps a golangci-lint config error from vanishing,
but it also hands back every `go: downloading …` line a cold module cache
produces: a passing `go test` went from 3220 raw bytes to 3216 through rtk,
where the stdout filter alone returns 24.

Stderr still goes through whole whenever it is the report -- the command failed,
or the filter had nothing to show for stdout -- and now also whenever capping
would not pay: below the recovery store's floor, when there is no store to point
at, and whenever the note and the hint would cost more than the lines they
replace. Otherwise the last lines are kept and the rest goes behind the
`[full output: …]` handle, which puts that same `go test` at 354 bytes with its
warnings intact.

The end, not the head: a tool resolves and downloads before it builds, so a cap
on the head keeps the chatter and drops the two lines worth forwarding. The kept
lines are sliced out of the original rather than re-joined, so CRLF endings
survive.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…subcommands

The caps counted the lines they held back inside a shown file, but a file the
total cap skipped outright was reported as a file and nothing else: 255 of 387
lines on the module's own fixture were in no tally at all, with no way to read
them. Count those lines too, and point at the full output with the same
`[full output: …]` handle every other capping filter emits -- decided on the
text that handle is part of, so a search that ends up printing raw never leaves
a stored file with nothing pointing at it.

Only `run` produces the `path:line:content` shape this module parses. `scan
--report-style short` happens to parse as well and was being capped as if it
were a match list, and `lsp` speaks a protocol over the stdin that capturing
closes, so it served an editor an empty document. The subcommand is now read in
two passes -- the options that may precede one, then `run`'s own grammar once
the line is known to be a `run` -- and a line that names any other subcommand,
or a `--stdin` run, goes through the passthrough, which inherits stdin. A line
this module cannot identify is not filtered: `--color always lsp` reading as a
`run` is what closed that stdin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A windowed `git show <rev>:<path>` points at the rest of the file with
`| tail -n +N`. The hook rewrites the bare `git show` in that hint back into
`rtk git show`, which windows the dump a second time, so `tail` had only the
hint itself left to print: 1 line where the reader asked for 1434. Name the
command the hook leaves alone, as the `diff` size hints already do.

The differential test now runs the command the hint names instead of
simulating it, so a hint that cannot return the blob fails CI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(read): stop the head window reading past the lines it was asked for
fix(core): cap the stderr a stdout-only filter forwards on a clean run
fix(ast-grep): account for every match line and stop capturing other subcommands
fix(git): make the blob recovery hint work under RTK's own hook
…sions

fix(git): stop show reporting on HEAD and log capping in silence
…'s files

`--codex` is the one init mode whose paths are the project root, where `RTK.md`
is a name RTK does not own. Uninstall removed any file sitting there, so a
project holding its own `RTK.md` lost it with no content check, backup or
prompt; init overwrote one just as quietly. The file now says whose it is: init
writes an `rtk-owned` line above the awareness payload and both sides read it
on the first non-blank line, so a payload from another release is still
recognised as RTK's -- comparing against the running build's copy would orphan
RTK's own file on the first upgrade and leave a numbered backup on every one
after -- while a marker a user pasted into the middle of their own notes does
not hand the rest of the file over with it. A file without the line is the
user's: init moves it to a free `.bak` sibling, uninstall keeps it.

Installs predating the line are recognised too, so the change does not strand
them. Up to v0.48.0 the payload opened with a heading naming RTK and the mode,
which is enough on its own. The releases after it wrote the shared awareness
text, whose headings name neither and which a user's own notes may open with,
so those are matched whole instead, by a digest of the bytes that shipped --
frozen, because rewording the awareness text must not change which files
uninstall recognises as RTK's own.

That test is for the project root only. Under `--global` the file sits in the
Codex home RTK created it in, where nothing else claims the name and every
release before the marker wrote it there unmarked; reading the marker there
would keep all of those files forever and back one up on the next install.

The project paths are relative names RTK joins itself, so a symlinked `.codex`
sent the new `hooks.json` write out of the project entirely -- and a planted
`hooks.json.bak` took the existing hooks out through `fs::copy`, which follows
a symlink at the destination, even with `.codex` a real directory. Both paths
are now resolved link by link, up to a bound that makes a cycle terminate, and
refused when the end of the chain lands outside. Resolving one link was not
enough: `canonicalize` gives up at the first target that does not exist --
which it will, since RTK creates these files -- and hands back the unresolved
path as if it sat where the chain broke, so neither a second hop nor a link
above the file was ever looked at. A link standing where the file itself goes
also has to sit inside the project rather than merely point there, since a
write that cannot resolve its path replaces that link instead of following it.
Only the hook is given
up when the check refuses: `AGENTS.md` and `RTK.md` are demonstrably where
uninstall left them, and refusing to clean them leaves artifacts with no
command that removes them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nt-aware

The containment walk resolved symlink chains with a loop of its own, and the
`.bak` picker chose a slot by name alone. Both are shaped here so the Pi/OMP
ownership work can build on them rather than growing a second copy of a
security-relevant walk that can drift out of agreement with this one.

A bare `Option` conflates three facts: "not a symlink", "a link I could not
read", and "I gave up". Conflating the first two lets a `readlink` failure on a
path `lstat` just called a symlink be compared as if it had resolved, so
`SymlinkHop` and `SymlinkChain` keep them apart and `Resolution` carries the
distinction out to the caller. The hop bound is inclusive: it counts hops
followed, and an exclusive range stopped one short of the length the
documentation promised.

Containment keeps a check the resolver cannot make. `atomic_write` cannot
canonicalize a chain whose end does not exist, so it writes at the path as the
filesystem reads it, replacing the last link rather than following it -- which
makes every link along the site's own chain a place the write can land, not just
the far end. A link outside the project that currently points back in is one an
attacker may own and re-aim, so the chain is judged link by link. Ancestor
components are traversed rather than sited: whole directory trees hang off one
on macOS, and siting those would refuse every absolute target under `/var`.

`free_backup_slot` probes with `fs::read` so an unreadable slot counts as taken
rather than being overwritten, and reuses a backup whose content already matches
so a provisioning loop cannot consume a slot per run. Names are built by
appending to the `OsString`: `with_extension` replaces an extension instead of
extending it. `BackupSlot` keeps an unreadable source apart from a preserved
one, because only a caller that copies needs that answer -- a rename carries
content RTK cannot read.

`CwdGuard` restores the working directory on the way out. Restoring by hand
needs the call under test to return rather than panic, so one failing assertion
left every later test inside a deleted `TempDir`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(hooks): keep rtk init --codex inside the project and off the user's files
Its match_command, `^(gradle|gradlew|\./)gradlew?\b`, needs a doubled
token (`gradlegradle`) or a literal `./gradle…`. Both call sites reduce
argv[0] to its basename before the lookup — `run_fallback` in main.rs and
`strip_absolute_path` on the hook rewrite path — so the `./` branch is
unreachable too. The filter has matched nothing since it landed: the
basename logic arrived in adda253 (2026-03-10), the file in 8fae5b0
(2026-03-18).

Deleting it changes no output. `src/discover/rules.rs` already routes the
whole gradle family (`gradle`, `gradlew`, `./gradlew`, `gradlew.bat`) to
`rtk gradlew` and the Rust gradlew_cmd, and `gradle … bootRun` is filtered
by spring-boot.toml. Widening the regex instead would have shadowed
spring-boot.toml — first match wins, ordered by filter name — and pulled
gradle-profiler and gradle.bat into a head-truncating filter.

Making TOML filters reachable from the default passthrough is a separate
design question, tracked in rtk-ai#3904.

Refs rtk-ai#2708
`rtk grep -l`, `grep -L`, `rg -l`, and `rg --files` print every path in
full and record 0/0 savings. Agents search from absolute roots, so each
line repeats the same long directory prefix.

Fold that prefix into a `<prefix> (N files)` header and print each
path's tail. The transform is lossless, keeps engine order, cuts only on
whole components, and runs under `never_worse`. It applies only to a
bare file list: any other shape flag (`-c`, `-Z`, `--null`, `--json`,
`-q`) changes the line and leaves the list verbatim. Streaming stdin
passthrough is untouched.

Track the fold with real sizes so it shows in `rtk gain`; unchanged
passthrough keeps recording 0/0.

Follow-up to rtk-ai#2630, which targeted the pre-rtk-ai#2641 grep_cmd.rs.
KuSh and others added 29 commits October 8, 2026 17:46
26b96ec pinned pr-target-check.yml, and next-release.yml matches it. Both are
pull_request_target workflows that read secrets.APP_PRIVATE_KEY and mint a
write-scoped token, which is where a repointed tag does the most damage. Two
workflows in the same position were never covered:

  cd.yml       contents: write, pull-requests: write, reads APP_PRIVATE_KEY,
               and calls googleapis/release-please-action on a floating tag
  release.yml  contents: write, builds and publishes the binaries users install,
               through softprops/action-gh-release and dtolnay/rust-toolchain

A tag is mutable, so whoever owns the action repository can repoint it and have
the new code run against that key, or against the build that produces the
release artifacts. A commit SHA cannot be repointed.

  googleapis/release-please-action  5c625bf  # v4.4.1
  softprops/action-gh-release       3bb1273  # v2.6.2
  dtolnay/rust-toolchain            4360b52  # stable branch, untagged
  actions/create-github-app-token   bcd2ba4  # v3.2.0

Each SHA was resolved from the ref it replaces and annotated with the version
that actually points at it. rust-toolchain is the exception: `stable` is a
branch selecting the Rust toolchain, and the repository's only tag, v1, points
at a different commit, so there is no version to name.

ci.yml is left on tags on purpose. It runs on pull_request with contents: read
and no secrets, so pinning it would buy little and stop the toolchain and cache
actions receiving fixes.

actions/* is not exempt either; the line is privilege, not who owns the
repository. 26b96ec already pins create-github-app-token and github-script,
the two that mint a write-scoped token or run script with secrets in reach,
while checkout, upload-artifact, download-artifact, setup-go and stale stay on
tags because none of them touch a credential. cd.yml and release.yml hold the
three create-github-app-token references 26b96ec did not reach, so one action,
one key and one set of scopes were split across two treatments. v3 and v3.2.0
both point at bcd2ba4 today, so this pins the code already running.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`extract_identifier_and_extra_args` picked the PR/issue/run identifier with a
hand-rolled walker: a hardcoded eight-entry list of value-taking flags, a
`skip_next` bool, and `arg.starts_with('-')`. Anything the list missed had its
value read as the identifier, and `pr_action` did not consult the list at all.

Verified against gh 2.46.0 and 2.100.0:

    rtk gh pr checks --interval 30   ->  gh pr checks 30 --interval
    rtk gh run view -j 12345         ->  gh run view 12345 -j
    rtk gh pr edit --add-label bug   ->  "ok edited #bug"

Replace it with `arg_tokenizer::tokenize_grammar` plus one `takes_value`
predicate per gh subcommand, transcribed from each subcommand's own `--help`,
and take the first free positional. `pr_action` now uses the same tokenization,
and `run()` restores the `--` that clap's `trailing_var_arg` consumes so gh
receives the boundary the user typed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`trailing_var_arg` only ever lets clap strip one `--`, the one at the head of
the region it hands to `gh` — before the subcommand, or right after it. That
one is rtk's option terminator and was never part of the gh command line, so
restoring it fabricated an argument: `gh -- pr view 42` is `unknown command
"pr" for "gh"` and `gh api -- repos/o/r --jq .name` is `accepts 1 arg(s),
received 3` on gh 2.46.0 and 2.100.0 alike. Every later `--` clap passes
through untouched and still reaches gh verbatim.

Rebuilding the region before the drop keeps the subcommand from being
reinjected, so `gh -- pr view 42` runs `gh pr view 42`, not `gh pr pr view 42`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`split_identifier` reads positionals past `--` because gh does too
(`gh pr view -- 42` views PR 42), but the callers re-emit the identifier ahead
of the boundary. When the escaped region held more than the identifier, that
promoted a flag the user had escaped back into flag position:
`rtk gh pr checks -- --watch 42` ran gh with `--watch` live, blocking on a
watch the user had asked gh to treat as a positional.

gh takes at most one positional for these subcommands, so an escaped region
holding more than one token is something gh rejects whatever rtk sends. Look
past the boundary only while it escapes a single token, and hand the rest to
gh untouched.

Also isolate HOME and RTK_DB_PATH in the argv test, which otherwise read the
developer's own config and wrote to their real tracking database.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Refusing to look past `--` only when two or more tokens trailed it still unescaped
the single-token case, which is the one a user is most likely to type: `rtk gh pr
checks -- --watch` hoisted `--watch` into flag position and ran the blocking watch
the user had escaped, where develop passed the command through untouched. `gh issue
view -- --web` and `gh run view -- --log` went the same way.

What makes hoisting safe is not how much the boundary escapes but whether unescaping
it is a no-op: reach past `--` only for one token that is still read as a positional
without it, so `gh pr view -- 42` keeps gaining `--json`. gh reads everything past
the boundary as a positional -- `gh pr view -R o/r -- --web` resolves `--web` as the
PR selector -- so a flag left there is a flag gh never acts on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`dirs::config_dir()` prefers `XDG_CONFIG_HOME` over `HOME`, so on a machine that sets
it the argv assertions still ran against the developer's real `~/.config/rtk`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The `--` tests started rtk from CARGO_BIN_EXE_rtk and pointed HOME,
XDG_CONFIG_HOME and RTK_DB_PATH at a tempdir by hand. A child spawned
that way is not covered by the isolation guard, which fails the suite for
any test file that names the binary directly, because it also writes the
tee spool and recall database under the developer's real data directory.

`common::rtk_command()` pins all of those, plus telemetry, so the manual
settings and the comment justifying them go. The PATH that puts the
argv-recording `gh` stub first is not covered by it and stays.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
ci: pin the third-party actions that run with secrets and write access
fix(gh): tokenize gh args instead of walking them by hand
Matching the last dot-segment also accepted any plugin tool ending in
.bash (e.g. myplugin.bash). Accept only bash and file_system.bash.
# Conflicts:
#	docs/contributing/TECHNICAL.md
…wline

fix(core): emit one trailing newline, not one on top of the output's own
`;;`, `;&` and `;;&` each terminate a `case` arm, but the lexer emitted a
separate `Operator(";")` per character. `rewrite_compound` splits on every
operator and rebuilds the text around it, so the second half of a terminator
came back as its own empty segment and the arm was re-emitted with the two
halves apart:

    case x in a) echo 1;; esac    ->  case x in a) echo 1; ; esac
    case x in a) echo 1;& esac    ->  case x in a) echo 1;  & esac

The first is a bash syntax error, so any command mixing a `case` statement
with a rewritable command never ran at all. The second is worse than an
error: `; &` is valid, and turns a fall-through into a background job.

The `;` arm now folds `;;`, `;&` and `;;&` into one operator token, matching
bash's own maximal munch — `;;;` is `;;` then `;`, and `;&&` is `;&` then
`&`, which bash's syntax errors name explicitly. The emitter's existing
no-padding branch for `;` covers every terminator starting with one, so the
terminator itself is reproduced verbatim instead of being padded to ` ;; `.

Whitespace after a terminator is not preserved: that branch appends a space
when the operator is not the last token, so `echo 1;;esac` comes back as
`echo 1;; esac`. Both parse identically, and the behaviour predates this
change — it applied to bare `;` already.

No existing test changes: the old two-token form produced an empty segment
between the halves, which the segmenters already dropped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review follow-up: the segmenter doc comments and the discover README
still named only `&&`, `||` and `;` as split points; `;;`, `;&` and
`;;&` are now one operator token each and split the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(hook): keep `case` terminators intact when rewriting (rtk-ai#3197)
Exit 0 needs allow rules covering every command of a chain, only
RTK_REWRITE_HOST=openclaw relaxes the default 3, and exit 1 also covers
excluded commands, `$(...)` and file redirects. Say so in --help and on
the rewrite_cmd::run table, update the outdated FEATURES.md section, and
test that --help gives each exit code its own paragraph.

Co-authored-by: tandede <1090179959@qq.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-exit-codes

docs(hook): document all four rtk rewrite exit codes in --help
fix(hook): match namespaced bash tool name in the Vibe hook
…them

Round 2 blocker 1. Four arms of the same class: a config shape where rtk saw
fewer rules than OpenCode does, and a rewrite moved the command out from under
an ask or a deny.

`execute.before` runs before permission evaluation, so the engine judges the
string the plugin hands back. If that string no longer matches a rule the
original did, the rule is gone. djbclark reproduced exactly that on 2.0.26: a
`deny` on `*PERMTEST-REWRITTEN*` rejected `echo PERMTEST-ORIGINAL` after a
rewrite, which proves the engine evaluated the rewritten form. Our defence is
comparing the verdict before and after and answering `{}` when they differ, and
that defence is only as good as our model of the rules.

The ordering. OpenCode reads the root rules of every file first, then the agent
rules of every file. rtk looped over the files and read root-then-agent inside
each, so a project `allow` outranked a global agent `ask`. Now two passes.

The agent block. Both spellings are legacy maps and both precede the 2.x list,
but rtk appended `agent.<name>.permission` after `agents.<name>.permissions`.
One file declaring both is now read map-first, so its ask wins.

Wildcard actions. OpenCode matches `action` as a glob. `sh*` was dropped by a
comparison against the two exact spellings, which is the same as not reading the
rule at all.

Markdown agents. An agent can be a file under `.opencode/agents/` with
permission rules in its frontmatter, and rtk read none of them. Their contents
are still not parsed; `agent_rules_unreadable` reports that a key is present and
`rtk hook opencode` answers `{}`, which is the safe direction. An agent file
with no such key keeps its rewrites, since most carry only a prompt and a model.

Five tests, one per arm. I reverted each fix to confirm the suite goes red
without it: the ordering and the agent-block swaps and the wildcard test all
fail on the old code, and the markdown one failed until a test exercised the
call site rather than the helper — the helper test passed with the call site
disabled, so `a_markdown_agents_rules_stop_the_rewrite_entirely` now goes
through `opencode_answer_for`.

The README described the order as "Global before project, agent last", which
was never what the code did.
… load it

Round 2 blocker 2. `rtk init -g --opencode` printed a warning that the plugin
would not load, then wrote it anyway — so on 1.1.4 the user got an OpenCode that
exits 1 at startup, having been told exactly that would happen. The warning and
the write contradicted each other.

Below the floor nothing is written now, and an existing file is left as it is
rather than replaced, so somebody who already has a working plugin keeps it. The
message says which of the two happened, since "installed nothing" and "left your
plugin in place" call for different follow-ups.

The banner is now reported as `x.y.z` rather than the whole `opencode --version`
line, which on some builds carries more than the number and wrapped the message
mid-sentence.

The version arrives as a parameter instead of being spawned here. That is what
makes the no-write branch testable, and it also fixes the optional note from the
review: `test_opencode_plugin_install_and_update` ran with `dry_run=false`, so
its result depended on whichever OpenCode the developer had installed. Three
tests now cover the branch directly — an old version leaves an existing file
alone and does not create a missing one, a new or unreadable version still gets
the plugin, and the install/update test passes `None` explicitly. Removing the
early return turns the suite red.
feat(rewrite): rewrite inner commands of a quoted fish -c wrapper
clippy runs with -D warnings over --all-targets, so a function left alive only
for a test fails the build. `too_old_opencode` absorbed `is_too_old` when it
started taking the banner as a parameter and needed the parsed triple, and the
test kept calling the old name.

The test now calls `too_old_opencode` itself, which is what it should have been
doing anyway: the old pair of assertions went through a helper that shared no
code with the branch that actually runs. It also picks up the trimmed-output
check while it is there, since that was the reason the function was split at
all.
…init honest

- A 2.x list rule whose action only names `bash` is no longer counted: OpenCode
  ignores it, and a counted allow outranked an ask it applies.
- A legacy map key the two generations read differently (`shell` and `sh*`
  apply only on 2.x, `b*` only on 1.x) now stops the rewrite: rtk cannot tell
  which generation it is serving. Configs are read once per call.
- Agent frontmatter keys are found quoted, after a byte-order mark, or in an
  unclosed block, and agent files in the singular `agent/` directory too.
- Below the floor, init no longer prints the plugin path or an OpenCode restart
  hint for a plugin it did not write, and its message and the docs no longer
  claim the plugin cannot load from 1.3.4 on.
- Tests and dry runs no longer spawn the developer's own `opencode --version`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bility

feat(hook/opencode): support OpenCode V2, Desktop, and Windows (rtk-ai#3463, rtk-ai#3326)
ruff 0.12+ `format --check` prints `unformatted:` per file and
`N files already formatted` as the summary. The filter still keyed
on pre-0.12 `Would reformat:` / `left unchanged`, so the check path
fell through to identity (0% compression).

Keep both spellings, parse paths after the first colon (Windows-safe),
and collect files from the default full diagnostic (` --> path:line:col`)
and concise `path:line:col: unformatted:` lines.

Fixes rtk-ai#3930

# Conflicts:
#	src/cmds/python/ruff_cmd.rs
@cursor
cursor Bot force-pushed the fix/ruff-format-check-filter-wording branch from 693fe83 to 91b04b5 Compare October 12, 2026 07:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

filter_ruff_format matches pre-0.12 ruff wording, so rtk ruff format --check compresses nothing