Skip to content

Conflicted Sync: upstream/develop 2026-09-11 (95 commits) - #167

Merged
kylehgc merged 96 commits into
developfrom
sync/upstream-2026-09-11
Sep 11, 2026
Merged

kylehgc merged 96 commits into
developfrom
sync/upstream-2026-09-11

Conversation

@kylehgc

@kylehgc kylehgc commented Sep 11, 2026

Copy link
Copy Markdown
Owner

Conflicted Sync of upstream/develop (head fc19d7a, tag dev-0.50.0-rc.427, 95 commits since the 2026-09-07 sync) into fork develop, per the CONTEXT.md Conflicted Sync policy: topic branch, upstream wins, only proven-additive fork code survives.

Full writeup: claudedocs/sync-conflict-2026-09-11.md.

Conflict and how it was resolved

File Colliding upstream work Resolution
src/hooks/hook_cmd.rs Hook decision consolidation (rtk-ai#3955, KuSh: d6ce8f7, 6eb915b): decide/decide_for_agent/suppress_identity and HookDecision move to a new src/hooks/decision.rs; decide_from_verdict becomes a shim Both hunks upstream's verbatim. The fork's local get_rewritten and enum HookDecision go (upstream owns them now). The fork's three already-rtk assert arms go: AllowRewrite(cmd) when every segment is already rtk and the verdict is Allow (30557a6, 371e058), AskRewrite(cmd) for an already-rtk Ask or an unattestable already-rtk shape (505e355, 31fceff). Upstream's suppress_identity is an explicit policy for the identity-rewrite case — Defer, with the Gemini trade-off documented and the contract pinned in tests/hook_decision_protocol_test.rs — so the fork's arms are a competing opinion, not an addition.

Consequential removals outside the hunks, all dead or unreachable once the arms are gone: all_segments_already_rtk and permissions::is_wrapped_invocation (only callers were the Allow arm; -D dead-code flagged both), and the rewritten == cmd early-returns in the Copilot IDE, Vibe and Droid renderers (31fceff) that suppress_identity now makes unreachable for every hook.

Additive fork code kept, proved absent upstream (git show upstream/develop:src/hooks/permissions.rs | grep -c 'normalize_for_matching\|is_rtk_prefixed\|COMMAND_WRAPPERS' → 0): the rtk-aware permission matcher from the still-open upstream rtk-ai#3195 adoption (30557a6, albatrossflyon-coder; 8adbeba, 505e355 amendments) and the already-rtk Deny assertion in the Claude hook. Deny is checked before suppress_identity can run, so rtk rm -rf / and rtk proxy rm -rf / still come back denied against a Bash(rm:*) rule.

Cargo.toml in the range: version 0.42.4 → 0.48.0 and a direct toml_edit = "0.22" (already in the lock via toml; lock diff is the version line only). No build.rs change.

Fork tests removed

Eighteen hook_cmd.rs tests that pinned the removed arms or renderer guards, or could no longer fail without them — full list in the writeup. test_probe_shapes_fail_safe's two contains_already_rtk_segment asserts moved into test_contains_already_rtk_segment_ignores_original_form. permissions.rs: test_is_wrapped_invocation_contract dropped with its subject. Kept and still green under upstream's decide: the deny-through-prefix and wrapper see-through tests, the already-rtk deny payload test, and every matcher test.

Fork delta

371e058 and 31fceff added to HEALED_SHAS: their headline behavior is superseded by upstream d6ce8f7. 30557a6, 8adbeba, 505e355, d9e7458 stay — the deny path, wrapper see-through and rtk run wrapper entry are live, even though two of those commit subjects still name the removed assert behavior.

User-visible change

With an allow rule for the underlying tool and nothing to rewrite, an already-rtk command (rtk grep foo under Bash(grep:*)) used to get an explicit permissionDecision: "allow" from the Claude hook; it now gets no hook output, and Claude Code's own matcher decides. Same for an already-rtk ask rule or redirect. Deny through the rtk prefix is unchanged.

Repro, real binaries (before = installed develop build of 2026-09-08, after = this branch)

Sandboxed CLAUDE_CONFIG_DIR with {"permissions":{"deny":["Bash(rm:*)"],"allow":["Bash(grep:*)"],"ask":["Bash(git push:*)"]}}, scratch RTK_DB_PATH.

$ printf '{"tool_name":"Bash","tool_input":{"command":"<cmd>"}}' | rtk hook claude

rtk grep foo
before: {..,"updatedInput":{"command":"rtk grep foo"},"permissionDecision":"allow"}
after:  <silent>

rtk git push origin main
before: {..,"updatedInput":{"command":"rtk git push origin main"},"permissionDecision":"ask"}
after:  <silent>

rtk git log > /tmp/out.txt
before: {..,"updatedInput":{"command":"rtk git log > /tmp/out.txt"},"permissionDecision":"ask"}
after:  <silent>

rtk rm -rf /tmp/x
before/after: {..,"permissionDecision":"deny","permissionDecisionReason":"RTK: matches a configured deny rule"}

rtk proxy rm -rf /tmp/x
before/after: {..,"permissionDecision":"deny","permissionDecisionReason":"RTK: matches a configured deny rule"}

git status
before/after: {..,"updatedInput":{"command":"rtk git status"},"permissionDecision":"ask"}

Review round

Independent read-only reviewer pass over the resolution against the writeup's claims: verbatim-vs-upstream on the conflict region, additive proofs by grep on upstream's copy, guard reachability under suppress_identity, dropped/kept test lists, stale references, HEALED_SHAS notes. All nine checks passed (conflict span byte-identical to upstream by cmp; decision.rs byte-identical; every non-test hunk vs upstream accounted for). Four findings, all taken: the unattestable-ask arm and is_wrapped_invocation were introduced by fork d9e7458, not 505e355/31fceff — attributions corrected, and d9e7458 stays in the delta because its rtk run wrapper entry is live; test_decide_defer_for_already_rtk_command_with_no_matching_rule could no longer fail under suppress_identity — dropped; the 371e058 healed note pointed at the wrong commit for the surviving strip_token rule — fixed; a caveat on the generated README rows for 505e355/d9e7458 added to the writeup. Gate re-run after the changes.

Quality gate

x64 host toolchain (scripts/win-dev-env.ps1): cargo fmt --all clean · cargo clippy --all-targets 0 warnings · cargo test --all 3540 unit tests passed, 0 failed, 8 ignored; all integration suites green, including upstream's new hook_decision_protocol_test, git_show_blob_differential_test and signal_flush_test. git diff --check clean.

Deliberately not done

🤖 Generated with Claude Code

kerta1n and others added 30 commits April 14, 2026 21:59
Auto-submit updated manifests to microsoft/winget-pkgs on stable releases
using vedantmgoyal9/winget-releaser. Windows users can now install via
`winget install rtk-ai.rtk`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move placement to make winget command easier to find
Add winget commands into other documentation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…master--components--rtk

chore(master): release 0.43.0
…master--components--rtk

chore(master): release 0.44.0
…master--components--rtk

chore(master): release 0.44.1
…master--components--rtk

chore(master): release 0.44.2
…master--components--rtk

chore(master): release 0.45.0
…master--components--rtk

chore(master): release 0.46.0
…master--components--rtk

chore(master): release 0.47.0
`filter_migrate_status` looked for whitespace after the `202...` id and fell
back to a hardcoded 20 bytes when there was none -- so an id that runs to the
end of the line slices past it:

    Migration could not be applied at 2024-01-01T12:00:00
    len 53, pos 34, end 20 -> line[34..54] -> end byte index 54 is out of bounds

Release builds set `panic = "abort"`, so `rtk prisma migrate status` dies and
the user loses the command output entirely -- the one thing a filter must never
do. A multibyte character straddling that offset panics the same way, on the
char boundary instead.

The fallback is the rest of the line, which is what the sibling
`filter_migrate_dev` has always used 70 lines above.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…master--components--rtk

chore(master): release 0.48.0
breisnerlopez and others added 28 commits September 8, 2026 16:19
Hermetic temp repo with objects of every relevant type (large/small UTF-8,
Latin-1 and UTF-16 blobs, a tree, commits). A fixed-seed PRNG generates 600
'git show <random flags> <arg>' invocations and checks rtk's classification
(windowed / passthrough / commit-diff) against 'git cat-file -t' ground truth:
0 misroutes (never window a non-blob), 0 silent losses (a large recoverable
UTF-8 blob always windows), and byte-identical passthrough for declined blobs.
A companion test proves the recovery hint reconstructs a windowed blob
byte-for-byte.
…r-candidate probe

The blob-show classifier still had a selection gap for short-flag CLUSTERS. git
re-parses a cluster's tail (`-wG x:y` == `-w -G x:y`), so `git show -wG x:y
HEAD:big` feeds `x:y` to -G and dumps only `HEAD:big`. But `show_positionals`
knew only single-letter value flags, so it kept `x:y` as the first positional; the
cat-file probe rejected it and the real blob fell through to the commit-diff path —
either dropping the windowing savings (large UTF-8 blob) or lossily decoding a Latin-1
blob into U+FFFD mojibake.

Two changes, neither of which mirrors git's flag grammar:

* Cluster-aware walker. `flag_token_consumes_next` extends the existing flag/value
  table to a short cluster's tail: the first value-taking short flag takes the rest of
  the cluster inline (`-Sfoo`) or, if it is the last char, the next token (`-wG x`).
  `is_short_value_flag` derives its char set FROM `consumes_next_token_as_value` so
  the table stays the single source of truth. (TODO points at rtk-ai#3681's ValueSpec.)

* Probe every candidate, keep any blob on the byte-safe path. `run_show` now
  cat-file-probes EVERY `rev:path` positional, not just the first, and takes the
  byte-safe path whenever ANY resolves to a blob — so even a walker miss can't misroute
  a blob's raw bytes through the lossy commit-diff decode. Windowing itself stays gated
  on a single sole positional, which also keeps `git show <commit> <rev:path>` (two
  real objects git concatenates) as byte-identical passthrough.

Also force byte-identical passthrough (never window) when a content-transforming flag
(--textconv/--filters/--ext-diff) or a trailing `-- <pathspec>` is present: the
`git show rev:path | tail` recovery hint omits them, so windowing could not reconstruct
git's bytes exactly. Narrows the fidelity-invariant comment accordingly.

`blob_show_target` (first-positional-only) is replaced by `blob_candidates` (all
candidates); unit tests migrated and extended to cover the cluster shapes.
…ial fuzzer

The blocker hid for three rounds because the fuzzer only ever paired a colon-carrying
flag cluster (`-wG x:y`, `-pS url:1`, …) with a blob-LESS `HEAD` (the CommitMisroute
trap). It never placed such a cluster BEFORE a real blob, which is exactly where the
walker gap bit.

Adds four crossed kinds:
* ClusterLargeUtf8 — cluster + large UTF-8 blob → MUST window (0 silent loss);
* ClusterLatin1 — cluster + Latin-1 blob → byte-identical to `git show <same args>`,
  no U+FFFD mojibake;
* TextconvLargeUtf8 / PathspecLargeUtf8 — `--textconv` or a trailing `-- <path>` →
  never windowed, byte-identical passthrough.

Plus a dedicated end-to-end test that `-wG x:y HEAD:large.txt` windows and that its
recovery hint (which names only the blob arg) reconstructs the blob byte-for-byte. The
extended fuzzer FAILS on the pre-fix code (verified: cluster windowing regression +
Latin-1 fidelity break) and passes on the fix. Seed and iteration count unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNqtCagEUryc1fgw62APkm
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNqtCagEUryc1fgw62APkm
… gain --reset

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNqtCagEUryc1fgw62APkm
The usage guard returns early when no hash and no --list is given, and
--list returns before this match, so the arm could never run. Removing it
keeps the usage string in one place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A non-numeric pattern stops at clap's parse error and falls back to raw
grep, so rtk and grep agree byte-for-byte whether or not `-l` is bound to
--max-len: the three existing cases still pass with the bug reintroduced.
Only a numeric pattern reaches the wrong answer -- rtk printed nothing and
exited 1 where grep listed the file.

Drop the clap-level test and the comment sentence with it. Develop's
test_grep_parse_files_with_matches_l already fails under the same mutation,
and -L never had a short binding on --max-len, so src/main.rs is untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…collision

test(grep): pin -l/-L parity with grep, including numeric patterns
`rtk rewrite`'s exit-code protocol is a public contract -- the claude and
cursor shell hooks, the opencode and pi TypeScript plugins, the hermes
Python adapter and openclaw all branch on it -- but no Rust test ever
called `run()`. `rewrite_cmd`'s own `exit_code_protocol` module asserts
against a locally re-implemented `expected_exit_code()` table, so the real
mapping could change without a single failure, including the rtk-ai#1155
invariant that a `Default` verdict must exit 3 and never 0.

Add an integration test that spawns the binary in a sandboxed
HOME/XDG_CONFIG_HOME/CLAUDE_CONFIG_DIR with project-level permission rules,
and pins the actual (exit code, stdout) pairs for allow, ask, deny,
compound deny, passthrough, default, compound rewrite, fd-dup redirect,
unattestable constructs and heredocs.

Alongside it, pin the two decision paths against each other on one corpus.
`rtk rewrite` and `rtk hook claude` answer the same question through two
independently written flows; the corpus asserts they agree, and a separate
test pins the one place they don't -- an already-RTK-prefixed command,
which the in-process hook defers on and `rtk rewrite` reports as an
ask-rewrite with the command unchanged. Modelled on registry.rs's
`segmenter_consistency` module.

Also pin `rtk hook check`, which had no test at all. It calls
`rewrite_command` directly with no verdict and none of the hooks' gates, so
it reports a rewrite for command substitutions and file redirects that both
hook paths refuse to touch.

No source change: this characterizes today's behavior so the decision-flow
consolidation can be shown to preserve it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The winget job sat in the release workflow's critical path: cd.yml gates
update-latest-tag on build-release without `if: always()`, so a failed
manifest submission would fail release.yml and leave the 'latest' tag on
the previous version. Mark the job continue-on-error so a submission
failure cannot hold back the release.

Pin winget-releaser to 4ffc7888bffd451b357355dc214d43bb9f23917e (v2)
rather than the mutable major tag, matching the pinning underway across
the repo. The step receives a classic PAT with write access to public
repositories, so the resolved ref should not be able to move underneath
it.
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.
Re-raise SIGINT/SIGTERM once output is printed so callers still see signal death, not a plain exit code.
The v2 tag is the action's only tag and has not moved since 2024-11-27,
while development continued on main. Four fixes sit between them, one of
which matters here: the action runs cargo binstall komac, and cargo is
missing on ubuntu-slim runners without it.

Pin to the most recent functional commit rather than to main, whose two
newer commits are documentation only. Its inputs are a superset of the
tag's, so the step needs no other change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The signed record() change (9da3270) lets a net-regressing filter store a
negative savings_pct, so both overall_savings_pct and avg_savings_per_command
can now be negative. Two telemetry tests still asserted `(0.0..=100.0)` against
the real user DB, so they panicked deterministically on any machine whose DB
held a net-negative row -- which is why CI failed on all three runners after the
approval, on test_enriched_stats_returns_valid_data.

Widen both assertions (test_enriched_stats and its twin test_get_stats_returns_tuple)
to the real invariant -- a saving never exceeds 100% -- dropping the stale 0
floor, and add a hermetic in-memory test pinning that a worsening command yields
a negative aggregate instead of a fake 0.
feat(ci): add winget manifest automation
`rtk hook <agent>` and `rtk rewrite` asked the same question -- may this
command be rewritten, and may the rewrite be auto-allowed? -- through two
independently written copies of the same four steps, over two isomorphic
enums (`HookDecision` and `RewriteOutcome`). A fix to the gate order, or a
new construct to refuse, had to be made twice. rtk-ai#3704 consolidated raw-command
lexing and named this duplication as its follow-up.

Move the decision into `hooks::decision`. What legitimately differs between
the callers stays outside it: `decide_with_params` takes both the permission
verdict and the rewrite parameters, so each host consults its own rules and no
test answers differently on a machine whose config.toml excludes a command
(rtk-ai#3146); `decide` is the wrapper that reads config for production callers.
The identity-rewrite policy is likewise applied by the caller that wants it:
`decide_for_agent` is the composition every hook shares, while `decide`
alone is what the `rtk rewrite` CLI renders.

That policy is the one place the two paths disagree. `get_rewritten`
suppressed a rewrite that changed nothing; `rewrite_cmd` had no such check and
reported it as a normal rewrite. Rather than silently picking a side, the
suppression is now an explicit `suppress_identity` applied at `hook_cmd`'s
single seam, with the difference and its one observable consequence documented
where it lives. Resolving it is a deliberate behavior change and is not part
of this refactor.

`get_rewritten`'s heredoc check is dropped as a duplicate rather than as the
guarantee itself: `rewrite_command` refuses heredocs through its own
`has_heredoc` (registry.rs:601,616), which is what the hook path was asking a
second time. It was also unreachable for the common forms, since a `<<`
operand reads as a file target and `contains_unattestable_construct` returns
first -- the exception being `<< /dev/null`, which that gate lets past and the
registry still refuses (rtk-ai#3980).

`rewrite_cmd::run` is reduced to exit-code rendering and `hook_cmd`'s eight
response builders are unchanged -- `HookDecision` keeps its name, so they
match on the shared type without edits. `rewrite_cmd`'s unattestable-construct
tests are dropped as verbatim duplicates of the shared module's that exercised
nothing in that file.

No behavior change. The characterization tests added in the previous commit
pass unmodified, and the out-of-crate suites are unchanged from their
pre-refactor baselines: hooks/claude/test-rtk-rewrite.sh 58/66 (the 8 are the
pre-existing audit-log gap, `rtk rewrite` never having logged), hermes 18
passed, scripts/test-all.sh 105/13/5. `rtk rewrite "git status"` benchmarks at
6.7ms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`rtk hook check` called `registry::rewrite_command` directly, with no
permission verdict and none of the gates the hooks apply. It therefore
reported a rewrite for command substitutions, file redirects and heredocs
that both hook paths refuse to touch -- the diagnostic disagreed with the
thing it exists to diagnose, and did so in the direction that matters, by
claiming RTK would rewrite a command it deliberately leaves alone.

Route it through `hooks::decision` so it answers the same question, and
report a deny rule distinctly from "no rewrite" rather than collapsing both
into one message. Both still exit 1.

That makes the answer agent-dependent, so `--agent` stops being discarded.
`AgentPath` records what actually differs between agents, which is whose
permission rules their hook consults: the six that decide in-process via
`rtk hook <agent>` use their own host's rules, the five whose plugin shells
out to `rtk rewrite` get Claude's (that entry point cannot be told who is
asking), and the six that install only a rules file have no hook and so no
rules at all. Every install target resolves -- including `codex` and
`openclaw`, which are install flags rather than `AgentTarget` variants --
and only a genuine typo is rejected.

What does *not* differ is a rewrite that changed nothing: every agent
discards it, the in-process hosts in `hook_cmd` and the others in their own
plugin, since `hooks/opencode/rtk.ts`, `hooks/pi/rtk.ts` (shared with omp)
and hermes' `__init__.py` all gate on `rewritten != command`. `AgentPath`
suppresses it for every variant. Only the bare `rtk rewrite` CLI reports the
no-op, and no agent consumes that answer raw.

Consulting no rules and ignoring `--agent` is what made the diagnostic
contradict every host: under a Claude deny rule for `git status` it reported
`rtk git status` while `rtk hook claude` refused the command outright; it
reported a rewrite for `rtk git status`, which no agent applies; and since
`--agent` selected nothing, the answer described no host in particular.

The expectations pinned in the characterization commit are updated here, in
the same commit, so the behavior change is visible as a diff rather than as a
test that quietly stopped asserting. That harness now also asserts the hook
exits 0 and never panics: a crash produces empty stdout, which would let every
"expect no output" assertion pass vacuously. The agent list is derived from
`AgentTarget::value_variants()` so a new variant fails the test instead of
silently becoming unanswerable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lidation

refactor(hooks): consolidate the hook decision behind one shared decide
`tokens_saved_24h` and `total_tokens_saved` both return `i64` from an unclamped
`SUM(saved_tokens)` -- deliberately, unlike the `as usize` readers that take
`.max(0)` for their unsigned API. A window whose filters emitted more than they
saved sums honestly negative, so `assert!(saved_24h >= 0)` panics on any DB
holding a net-negative row.

No ordering invariant replaces the floor: with a positive row inside the 24h
window and a larger negative one outside it, saved_24h (90) exceeds saved_total
(-110). Nothing about either value is assertable, so the test stops binding them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-blob-preview

feat(git): filter large git show blob dumps with recovery and Latin-1 decoding
test(diff): pin the shape detectors, the structural index lines and the GNU roots
fix(prisma): stop migrate status panicking on a trailing migration id
…2026-09-11

One conflict, src/hooks/hook_cmd.rs: upstream rtk-ai#3955 moved the hook decision
into src/hooks/decision.rs with suppress_identity; the fork's already-rtk
Allow/Ask assertion arms are a competing opinion on the same case and go,
with their dead helpers, renderer guards and tests. The rtk-aware matcher
and the already-rtk Deny assertion (rtk-ai#3195 adoption) are additive and stay.
Writeup: claudedocs/sync-conflict-2026-09-11.md

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@kylehgc
kylehgc merged commit d2a41e7 into develop Sep 11, 2026
4 checks passed
@kylehgc
kylehgc deleted the sync/upstream-2026-09-11 branch September 11, 2026 21:31
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.

5 participants