Skip to content

Next Release#2774

Open
github-actions[bot] wants to merge 116 commits into
masterfrom
develop
Open

Next Release#2774
github-actions[bot] wants to merge 116 commits into
masterfrom
develop

Conversation

@github-actions

@github-actions github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Feats

  • feat(hooks): add Factory Droid integration #2267Closes #2099 (to verify)
  • feat: add git checkout support #2859
  • feat(hook): wire TOML filters + better tee tail hint #2748
  • feat(php): consolidated PHP tooling (php, artisan, phpunit, phpstan, pest, paratest, ecs, pint) #1649

Fix

  • fix: detect absolute rtk path in Claude hook settings #2885Closes #2884 (to verify)
  • fix(ccusage): accept period key from current ccusage #2732Closes #2731 (to verify)
  • fix(cargo): surface --message-format=json build errors instead of "compiled" #2422Closes #2419 (to verify)
  • fix(grep): preserve -- separator between non-adjacent match blocks #2836Closes #2795 (to verify)
  • fix(git): compact git stash show instead of forcing -p #2801
  • fix(analytics): truncate display strings on UTF-8 char boundaries #2789Closes #2787 (to verify)
  • fix(parser): use byte offsets instead of char indices in extract_json_object #2751Closes #2509 (to verify)
  • fix(cicd): next release pr target fork compatible #2771

Other

  • chore(docs): clean contributing outdated #2911
  • chore(docs): add core contributor #2853
  • docs(windows): document native binary hook support #2791
  • chore(scripts): switch benches to mockhttp.org as httpbin.org is struggling at the moment #2775

sasha and others added 30 commits June 8, 2026 20:16
…mpiled"

With --message-format=json, cargo emits diagnostics as NDJSON, so the block
handler that keys on human `error[..]` lines counted zero errors and
summarized a failing build as "cargo build (1 crates compiled)" — the exact
same wording as a successful build. The error was dropped from the inline
output and only the exit code (easily swallowed by an && / || chain) hinted
at failure.

Recover the NDJSON `compiler-message` diagnostics from the raw stream when
building the summary: count them and render them. A failed json build now
shows the real error[E..] plus "N errors", and the exit code is used as a
backstop so a non-zero build is never reported as compiled. Non-json output
is untouched (the helper returns nothing for it).

Closes #2419
Parse the --message-format=json stream once into errors and warnings
instead of scanning it twice per call site, and move the success and
failure summaries into two small helpers shared by the streaming and
batch paths. Also render json warnings, not just errors, so json mode
matches how the human path already surfaces warning blocks.
The --message-format=json failure summary rendered every error and
warning in full, while the human-text path bounds blocks at CAP_ERRORS.
A json build with many errors could therefore print an unbounded wall of
diagnostics — exactly what RTK normally caps.

Cap the json error/warning lists at CAP_ERRORS/CAP_WARNINGS and append a
'… +N more' hint on overflow. The summary line still reports the real
total counts.
cargo_cmd had no token-savings test despite the repo's >=60% rule, so
nothing locked in the gain from extracting rendered diagnostics out of
the verbose json envelope and capping them. Add a savings assertion on a
large failing --message-format=json build.
The json-aware summary had tests for the failure and capped paths but
none for a clean --message-format=json build, where artifacts and
build-finished:true must still report crates compiled.
…pest, paratest, ecs, pint)

Consolidates the PHP-tooling work from three upstream PRs plus a new
Pint module, leaving phpt to its own PR (#1503).

- rtk php / rtk artisan: syntax check (-l) and Laravel artisan wrapper.
- rtk phpunit: structured-state parser, aggregate counts, bounded
  failure list. Uses runner::run_filtered.
- rtk phpstan: typed serde::Deserialize parser for --error-format=json,
  groups errors by file, sorts by count desc. Utility commands
  (--version, list, clear-result-cache) pass through unchanged.
- rtk pest / rtk paratest: shared test_output helper.
- rtk ecs / rtk pint: code-style fixers; pint uses --format=json for
  structured per-file rule counts.

Composer custom-bin-dir detection: composer_bin_dirs() reads
COMPOSER_BIN_DIR and composer.json config.bin-dir, so tools/bin/phpunit
classifies identically to vendor/bin/phpunit. registry.rs normalizes
tool paths before matching.

Sources:
- #1246 (aaronflorey, self-closed): php, artisan, ecs, pest,
  paratest, test_output, utils, composer_bin_dirs, registry normalization.
- #874 (Beninho, open): phpunit state-machine parser.
- #1110 (LucianoVandi, open): phpstan typed parser.
- New: pint_cmd.rs.

Tests: discover::registry 253 pass; cmds::php 36 pass; cargo build
--release 0 errors; cargo fmt --check clean.
`Commands::Run` used `status.code().unwrap_or(1)` which silently
maps signal kills (SIGTERM, SIGKILL, OOM) to exit code 1 instead
of the POSIX-conventional 128+signal. Use the existing
`exit_code_from_status()` helper already used by Commands::Proxy.

Fixes #2680
fix(run): propagate signal exit code instead of unwrap_or(1)
Upstream develop changed `rewrite_command` to take 3 args
(cmd, excluded, transparent_prefixes). The PHP tooling tests that
don't care about transparent prefixes now use the existing
`rewrite_command_no_prefixes` helper instead.
The PHP runners (phpunit, pest/paratest, ecs, phpstan, pint) only
applied their compact filters when rtk itself launched the process.
Output produced elsewhere — most commonly a tool run inside a Docker
container and piped back to the host — bypassed them entirely, since
the visible command is `docker ...`, not the PHP tool.

Wire the existing filter functions into `rtk pipe`:

- resolve_filter: phpunit, pest|paratest|php-test, ecs, phpstan, pint
- phpstan/pint pipe wrappers sniff JSON-vs-text by content, since the
  runners force --format=json but piped output may be either
- auto_detect_filter: route the "by Sebastian Bergmann" banner to the
  phpunit filter (no -f needed)
- bump the four backing fns to pub(crate)

Also fix filter_phpstan_text matching the summary line case-sensitively
("found"), which missed phpstan's actual "[ERROR] Found N errors".

Tests: phpunit banner auto-detect, phpstan case-insensitive summary.
pint: Pint >=1.14 renamed JSON keys name->path and appliedFixers->fixers.
The struct fields were required with no aliases, so serde rejected current
output and the filter fell back to raw (no compression). Add backward-
compatible aliases so both schemas parse.

phpstan: the "ok" gate and summary line read totals.errors, which counts
only non-file-specific (global) errors. A normal failing run reports
errors=0 with the count in file_errors, so runs with real errors were
reported as "phpstan: ok", silently hiding failures. Gate on both counts
and report file_errors in the summary.

Both regressions slipped past the suite because the fixtures set
errors == file_errors (phpstan) and used the old key names (pint). Added
regression tests using the current-version schemas.

Reported by @evaldnet (verified against Pint 1.27.1, PHPStan 2.1.40).

Co-authored-by: Aaron Florey <azza@jcaks.net>
Co-authored-by: Eli White <1153183+EliW@users.noreply.github.com>
Co-authored-by: Benjamin LETELLIER <bletellier@audencia.com>
Co-authored-by: Luciano <vandi.luciano@gmail.com>
…ecs/pint

`./vendor/bin/<tool>` is the common Laravel invocation form. classify_command
normalizes the leading `./`, so these classify as supported, but the rewrite
strips literal `rewrite_prefixes` from the raw command and the five rules only
carried `vendor/bin/<tool>` and bare `<tool>`. So `./vendor/bin/pint` ran raw
with no compression while `vendor/bin/pint` rewrote to `rtk pint`. phpstan
already carried `./vendor/bin/phpstan`. Add the `./vendor/bin/` prefix to the
other five, with a regression test.

Reported by @evaldnet (verified against pint 1.29.1, phpstan 2.1.40).

Co-authored-by: Aaron Florey <azza@jcaks.net>
Co-authored-by: Eli White <1153183+EliW@users.noreply.github.com>
Co-authored-by: Benjamin LETELLIER <bletellier@audencia.com>
Co-authored-by: Luciano <vandi.luciano@gmail.com>
develop's `test_every_subcommand_is_classified` requires every CLI subcommand
to appear in `RTK_META_COMMANDS` or `PASSTHROUGH`. The consolidated php
subcommands (php, phpunit, phpstan, pest, paratest, ecs, pint) wrap real
tools, so they belong in `PASSTHROUGH`. Without this the PR-into-develop merge
fails the test.
The `.semgrep.yml` `dynamic-command-execution` rule forbids `Command::new` on a
variable. `php_tool_command` built the command directly from the resolved tool
path; route it through `resolved_command` (the sanctioned PATHEXT-aware
constructor) instead. Clears both blocking findings with no behavior change.
… path compaction)

- run() now uses php_tool_command("phpstan") instead of hardcoding
  vendor/bin/phpstan, so COMPOSER_BIN_DIR and composer.json's config.bin-dir
  are respected, matching every other php tool in this module.
- filter_phpstan_text only treats shell-level "binary missing" lines as a
  failure. The previous contains("not found") matched real analysis messages
  ("Class X not found") and swallowed the summary; also dropped a stray Ruby
  LoadError string ("cannot load such file"). Adds a regression test.
- compact_php_path reduced to last-two-components for all frameworks, dropping
  the Laravel-specific prefix list. Tests updated to the compacted output.
is_numbered_failure_heading matched any line starting with a digit and
containing ')', which split a failure block on detail lines like
"5 of 10 assertions passed in Foo::bar()". Anchor to `^\d+\) \S` via a
lazy_static regex. Adds a regression test.
short_path() called current_dir() on every file (up to MAX_FILES_SHOWN per
invocation). Hoist the cwd prefix out of the loop and strip it inline.
…ewrite

The six Composer-tool rules had inconsistent patterns, and their
rewrite_prefixes only worked because classify normalizes the command while
rewrite stripped literal prefixes off the raw text. A form the pattern
accepted but the prefix list missed (e.g. ./bin/phpunit) would classify yet
silently fail to rewrite.

rewrite_segment_inner now normalizes the leading invocation for these tools
(php wrapper, ./, vendor/bin, composer bin-dir) the same way classify does, so
the prefix list collapses to the residual canonical forms: bin/<tool> + bare
name for phpunit/phpstan, bare name for pest/paratest/ecs/pint. Patterns
standardized to ^(?:php\s+)?(?:\./)?(?:(?:vendor/)?bin/)?<tool> for
phpunit/phpstan and ^(?:\./)?(?:vendor/bin/)?<tool> for the rest. Adds a
form-coverage test asserting every accepted spelling maps to one rewrite.
When no explicit permission rules exist in ~/.cursor/cli-config.json
(the default for fresh installs), every command gets Default verdict
which maps to AskRewrite. The Cursor hook only handled AllowRewrite,
silently dropping all rewrites and making RTK non-functional.

Now treat AskRewrite as allow when no rules are configured — Cursor
has no ask-the-user UX, so deferring is indistinguishable from
dropping. When explicit rules exist, preserve the conservative
behavior of deferring mixed/unmatched commands.

Also fix the legacy shell script (rtk-rewrite.sh) which treated
exit code 3 (ask) as failure via || short-circuit.

Fixes #2372
Align cursor_has_explicit_rules() with the inline has_rules check in
run_cursor_inner_with_rules() — both now consider deny, ask, and allow
rules when determining whether the user has configured explicit
permission policies.
Address PR #2609 review feedback:
- RC=3 in shell script now emits "permission": "ask" (future-proof)
- Add cursor_ask() and use it for all AskRewrite decisions
- Remove has_rules guard (unnecessary with ask semantics)
- Remove dead cursor_has_explicit_rules() function
…cation

Wire TOML-covered commands into the transparent hook rewrite path, and make
TOML truncation reversible via the shared tee hints.

Part 1 — hook wiring (#2179):
The hook's rewrite decision only consulted the static RULES table, so a command
covered solely by a TOML filter (jj, jq, just, ssh, ty, nx, turbo, …) was passed
through unfiltered by the hook even though `rtk <cmd>` filtered it directly.
`rewrite_segment_inner` now consults the TOML registry on a RULES miss
(Unsupported) and rewrites to `rtk <cmd>`, which re-enters via run_fallback →
the TOML tier. Guards: honors RTK_NO_TOML, respects the exclude list, never
rewrites Ignored (denylisted) commands, and a collision guard
(is_rtk_reserved_command) prevents rewriting to a name Clap would route to its
own subcommand. RTK_META_COMMANDS moved to core::constants as the shared source.

Part 2 — reversibility:
TOML truncation on success previously dropped lines with a bare "... omitted"
marker and no recovery pointer. apply_filter_with_info now reports a Lossiness
(Tail / Whole / None); run_fallback emits the line-offset tail hint
(`[see remaining: tail -n +N …]`) for a contiguous tail-drop, the full-output
hint otherwise, and — when tee is unavailable — shows the full raw rather than an
unrecoverable marker. Fixes the symmetric <500B failure-path gap and an
inaccurate MIN_TEE_SIZE comment in curl_cmd.

All hosts (claude/gemini/copilot/cursor) and `rtk rewrite` are covered via the
shared rewrite_command. No hook JSON / integrity-hash changes. The mirrored RULES
rows are kept (they feed discover/session/gain metadata). Builds on the approach
of #2226.

Note: the TOML registry load now lands on the Unsupported hook hot path
(~3-8ms native, one-time per process); the Supported path is unaffected. A
match-only RegexSet to cut that cost is a documented follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ccusage renamed the per-record key from month/date/week to `period`, so
the deserializer's required old field names no longer match and
`rtk cc-economics` fails with "missing field `month`".

Add `#[serde(alias = "period")]` to the daily/weekly/monthly key fields
so both legacy and current ccusage output parse. Adds period-shape tests
for all three granularities; existing legacy-key tests stay green.
PHP-CS-Fixer omits the fixers key in dry-run/diff modes (it is optional
in the JSON reporter), so a valid report like {"files":[{"name":"x.php"}]}
failed the whole parse and fell back to raw output. Mark the field
#[serde(default)] so a missing key deserializes to an empty vec.
Interpret all RTK rewrite exit codes and respond appropriately:
- Exit 0 (Allow): auto-apply rewrite
- Exit 1 (Passthrough): no RTK equivalent, pass through unchanged
- Exit 2 (Deny): block the call entirely
- Exit 3 (Ask): rewrite available, require user approval via
  requireApproval with allow-once/deny decisions

Uses execFileSync (not execSync) to avoid shell injection. Returns
a [string | null, RewriteVerdict?] tuple from tryRewrite so the
before_tool_call hook can react to each verdict type.

"allow-always" omitted from allowedDecisions because OpenClaw does
not auto-persist approval for plugin hooks — see:
https://docs.openclaw.ai/plugins/plugin-permission-requests#troubleshooting

Closes #2202
filter_phpunit_output matched on raw bytes, so `--colors=always` output
(colorized "OK ("/"FAILURES!"/"Tests:" lines) defeated every anchor and
counts were lost. Strip ANSI first, mirroring the other PHP filters.

Also report PHPUnit errors (thrown exceptions) distinctly from failures
(assertion mismatches) instead of lumping both under "failures".
…format

`phpstan -c phpstan.neon analyse src/` put a global option before the
subcommand, so the first-arg-only check missed it and the run fell back
to unfiltered passthrough. Scan all args for analyse/analyze.

Replace the has_custom_format bool with a 3-state ErrorFormat enum so an
explicit `--error-format=json` is preserved without appending a second
`--error-format json`. Strip ANSI in the text fallback for `--ansi`.
Pest has no root `pest.php` file — its bootstrap lives at tests/Pest.php
and its canonical marker is the vendor/bin/pest binary. The root-level
check never matched real Pest projects and false-positived on unrelated
utility files, misrouting PHPUnit-only projects to the Pest filter.
aeppling and others added 30 commits July 3, 2026 16:51
feat(hook): wire TOML filters + better tee tail hint
Deserialize each compiler-message line into typed structs instead of
indexing serde_json::Value, mirroring filter_go_test_json. Route the
internal-compiler-error level into the errors bucket so an ICE is no longer
dropped.
Register RTK with Factory Droid so shell commands the agent runs are
transparently rewritten to their token-saving `rtk` equivalents.

- `rtk init --agent droid`: install a native PreToolUse hook into
  ~/.factory/settings.json (matcher "Execute"); supports global (-g) and
  project scope, the FACTORY_HOME override, idempotent install with backup +
  atomic write, and a clean uninstall round-trip.
- `rtk hook droid`: process Droid's PreToolUse payload (BOM strip, stdin cap,
  JSON-parse fallback), mirroring the existing provider hooks.
- Deny verdict steps aside (no output) so Droid's native deny handling fires,
  matching Claude/Cursor/Copilot and the PermissionVerdict::Deny contract.
- Rewrites are auto-allowed: Droid only applies a hook's updatedInput when the
  decision is "allow", so we mirror run_cursor to avoid silently dropping the
  rewrite (and its savings) on the default verdict.
Droid 0.140.0 applies a PreToolUse hook's updatedInput regardless of the
permission decision (verified empirically + via binary decompile), so the
forced permissionDecision:"allow" was unnecessary. Worse, Droid resolves
the decision as the first hook result carrying one (no deny-over-allow
priority), so an unconditional allow could suppress another PreToolUse
hook's deny/ask and bypass Droid's native prompt.

Now omit permissionDecision by default and only assert allow on an explicit
allow rule, mirroring process_claude_payload. The rewrite still lands via
Droid's decision-independent "updated input result" path. Updated the
inaccurate comment and the test that required allow.
Adopt the decide_from_verdict/HookDecision flow introduced on develop so
the Droid handler gains the same safety behavior as Claude/Cursor/Copilot:
commands with substitution or file redirects now Defer (no rewrite) instead
of being rewritten, and deny/allow/ask mapping is shared instead of
reimplemented. Also removes an unwrap() in the response builder.

Claude-Session: https://claude.ai/code/session_01Vp4p5YroP2cfFvThhQE4Rv
Adversarial verification against Droid v0.164.0 (shipped code + docs)
found two broken assumptions in the original integration:

- Droid's canonical hooks file is hooks.json (root event map, no wrapper);
  the hooks key of settings.json is only a fallback that hooks.json shadows
  per event key. Droid's own /hooks UI writes hooks.json, so an RTK entry
  in settings.json silently dies as soon as the user adds any PreToolUse
  hook there. Install now targets the file whose PreToolUse array Droid
  actually reads (live hooks.json > live settings.json fallback > create
  canonical hooks.json), migrates stale copies, and uninstall sweeps root
  hooks.json, legacy hooks/hooks.json, and settings.json.

- The config-home env var is FACTORY_HOME_OVERRIDE (replaces $HOME, with
  .factory appended), not FACTORY_HOME pointing at the config dir.

Also corrects the 'legacy Bash matcher' comment (Droid never had a Bash
tool; the acceptance is purely defensive) and records that the
omit-permissionDecision rewrite path is confirmed in v0.164.0: decisions
resolve as find(first result with a decision) and updatedInput applies
via a separate path even when no decision is set.

Claude-Session: https://claude.ai/code/session_01Vp4p5YroP2cfFvThhQE4Rv
The Droid hook consulted Claude Code's settings (Host::Claude), leaking
one agent's permission config into another: a Bash(...) allow rule in
~/.claude/settings.json could auto-allow commands in Droid, bypassing
its native prompt.

Add permissions::Host::Droid backed by ~/.factory/settings.json
(honoring $FACTORY_HOME_OVERRIDE as a home-dir override, matching the
shipped Droid CLI):

- commandAllowlist -> Allow: the rewrite carries permissionDecision
  "allow", exactly the verdict Droid would emit natively.
- commandDenylist (always confirm) and commandBlocklist (never runs)
  -> Deny: RTK steps aside entirely so Droid's native confirm/block
  fires on the original command. Rewriting first would dodge Droid's
  own pattern matching ("rtk git log" no longer matches a "git log"
  denylist entry).
- Built-in defaults are mirrored from the shipped Droid CLI (verified
  against v0.153.1); a user list replaces its default outright, same
  as Droid's own accessor.

Also trims the wire-format comment per review feedback.

Verified live against droid 0.153.1: git status/git log rewrite and
auto-run at --auto low via the default allowlist; a denylisted git log
is left untouched and natively refused; uninstall round-trip clean.

Claude-Session: https://claude.ai/code/session_015CvKLxKMnmcvwY5CjHVTF5
Propagates the README Droid documentation to
docs/guide/getting-started/supported-agents.md per review feedback:
frontmatter description, agents table row, and an installation section
covering hooks.json placement and Droid-native permission handling.

Claude-Session: https://claude.ai/code/session_015CvKLxKMnmcvwY5CjHVTF5
When context flags (-A/-B/-C) are used, grep prints -- between
non-adjacent match blocks. parse_match_line() returned None for
this separator, and both output paths silently dropped it, merging
distinct matches into one contiguous block.

Fixes #2795
Native grep only prints -- between non-adjacent match blocks when
-A/-B/-C context is requested. The grouped output path was inserting
-- on any line-number gap unconditionally, so `rtk grep -rn todo`
invented separators between every match that grep never emits.

Gate the insertion on has_context_flag() which checks for -A/-B/-C
and their long-form equivalents.
fix(grep): preserve -- separator between non-adjacent match blocks
chore(docs): add core contributor
…scopes

Drop the hardcoded mirrors of Droid's built-in command lists and stop
emitting permissionDecision entirely. RTK now steps aside only on
explicit commandDenylist/commandBlocklist entries, unioned across all
four settings scopes (user + project, settings.json +
settings.local.json); every other command is rewritten via updatedInput
and the decision is left to Droid's native flow.

Verified against Droid CLI 0.153.1: updatedInput is applied after
Droid's own permission evaluation of the original command, so native
allowlist decisions are unaffected and deny entries fire natively.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpSp3Arj7fG1XJYhXoCMGC
…lure-2419

fix(cargo): surface --message-format=json build errors instead of "compiled"
fix(ccusage): accept `period` key from current ccusage
feat(hooks): add Factory Droid integration
chore(docs): clean contributing outdated
fix: detect absolute rtk path in Claude hook settings
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.