Skip to content

Next Release - #4399

Open
rtk-release-bot[bot] wants to merge 143 commits into
masterfrom
develop
Open

rtk-release-bot[bot] wants to merge 143 commits into
masterfrom
develop

Conversation

@rtk-release-bot

@rtk-release-bot rtk-release-bot Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Feats

Fix

Other

  • docs(hook): document all four rtk rewrite exit codes in --help #4204
  • ci: pin the third-party actions that run with secrets and write access #3775
  • docs: fix doc references that never resolved, and gate cargo doc in CI #3834
  • test(isolation): scan the resolvers for the variables they name #4459
  • refactor(tokenizer): make Dialect a struct of independent grammar axes #4008
  • refactor(js): decouple vitest/jest runner from CLI Commands enum #2418
  • docs: update native Windows hook troubleshooting #4350
  • test(pipeline): tolerate broken pipes from early child exits #3832 — Closes #3739 (to verify)
  • chore(deps): bump ignore from 0.4.25 to 0.4.32 #3824
  • chore(deps): bump dirs from 5.0.1 to 7.0.0 #3822
  • chore(deps): bump serde_json from 1.0.149 to 1.0.151 #3820
  • chore(deps): bump anyhow from 1.0.102 to 1.0.104 #3818
  • chore(deps): bump libc from 0.2.182 to 0.2.189 #3817

aeppling and others added 30 commits June 12, 2026 18:27
run_test(&Commands) → run_vitest/run_jest over a shared helper, dropping the
module dependency on the CLI Commands enum and the unreachable!() arm. Also fix
two doctests that surface once the crate is bundled as a lib: correct
join_with_overflow (Unicode ellipsis) and mark the TimedExecution example ignore.
Command::output() gives the child an immediate-EOF stdin, so stdin-based
bodies (-d @-, --data-binary @-, -T -, -K -) arrived empty: the server
received Content-Length: 0. Inherit rtk's own stdin before capturing
stdout/stderr, mirroring RunOptions::inherit_stdin (npm, wc).

Fixes #4084
`show_positionals` hand-rolled git's short-flag cluster grammar to find which
`git show` arguments are objects: a per-char walk that re-queried the flag table
through a `format!("-{c}")` allocation, plus its own `--`-boundary slice. The
tokenizer's `ValueSpec` table models exactly that — attached value, separate
value, and the solo-only cluster rule — so the walk is now `tokenize_grammar` +
`before_dashdash` + `Token::is_free_positional`.

It reads `git show` under diff's grammar rather than log's, matching the route
classifier beside it: `git show -wl 100` consumes the 100 as the rename limit,
where log's `-l` is solo-only and would leave it looking like a second object and
silently drop the blob window.

Verified byte-identical (stdout, stderr and exit code) against a binary built
from the merge-base over 94 `git show` invocations: blob and commit shows, the
`-wG a:b HEAD:blob` cluster, `-pS a:b`, `-S 'url:1' HEAD`, `-L 1,2:file`,
`-- <pathspec>`, `--textconv`/`--filters`/`--ext-diff`, the `:(exclude)`/`:!`/
`:^`/`:/` magic pathspecs, and every short flag whose log and diff spellings
disagree.

The `cat-file` probe keeps its subprocess: it only runs once a free positional
already looks like `rev:path`, which is the one case where nothing but git can
answer, so no flag pre-filter can skip it without risking the blob path.

Also drops a doc link on `log_wants_raw_shape` pointing at a `#[cfg(test)]`
helper, which rustdoc cannot resolve outside a test build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`extract_identifier_and_extra_args` picked the MR/issue 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 the list itself was a mix of flags from
different subcommands.

Verified against glab 1.36.0 and 1.117.0:

    rtk glab mr view --page 2      ->  glab mr view 2 -F json --page
    rtk glab issue view -P 50      ->  glab issue view 50 -F json -P
    rtk glab mr view --jq .iid     ->  glab mr view .iid -F json --jq

Replace it with `arg_tokenizer::tokenize_grammar` plus one `takes_value`
predicate per glab subcommand, transcribed from each subcommand's own `--help`,
and take the first free positional. `mr merge`, `mr approve`, `mr note` and
`mr update` each get their own table: `-s` is the boolean `--squash` on merge
but the value-taking `--sha` on approve, and `-d` is `--remove-source-branch`
on merge but `--description` on update.

`run()` also restores the `--` that clap's `trailing_var_arg` consumes, over
the reassembled `[subcommand] + args` region rather than `args` alone, and
moves rtk's own `-R`/`-g` from the end of the args to the tokenizer's
injection point — glab reads everything past `--` as a positional, so an
appended `-R` arrived as two extra arguments instead of the repo flag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two fallouts of classifying `--jq` and restoring `--`.

`--jq` now reaches glab with its value intact, so glab returns the user's
projection — and `run_glab_json` reformatted it as an MR summary of fields the
projection does not have (`? MR !0: ???`). Previously the value was hoisted out
as the MR number and glab failed loudly, so this turned a visible error into a
silent wrong answer. `has_output_flag` now counts `--jq`.

A `--` at the head of the region ended rtk's own `-R`/`-g` parsing, not glab's.
Forwarding it makes glab stop looking for a subcommand: `glab -- mr view 42`
prints root help on 1.117.0. Drop it instead.

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 glab — before the subcommand, or right after it, since `-R`/`-g`
are parsed before the subcommand positional either way. Only checking index 0 let
the index-1 case through: `glab mr -- view 42` prints the `mr` help instead of
viewing the MR, and `glab api -- projects/1 --paginate` is `Accepts 1 arg(s),
received 2` on glab 1.117. Every later `--` clap passes through untouched and still
reaches glab verbatim.

`split_identifier` reads positionals past `--` because glab does too
(`glab mr view -- 42` views MR 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 glab mr view -- --web 42`
ran glab with `--web` live, opening a browser the user had asked glab to treat as a
positional. glab takes at most one positional for these subcommands, so an escaped
region holding more than one token is something glab rejects whatever rtk sends.

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 the user is most likely to type:
`rtk glab mr view -- --web` hoisted `--web` into identifier position and ran glab
with the browser flag live, where develop passed the command through untouched.

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 `glab mr view -- 42` keeps gaining `-F json`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`escapes_a_bare_positional` landed between the header and the function it describes,
so rustdoc rendered the helper under `split_identifier`'s summary and left
`split_identifier` undocumented.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`rtk rewrite --help` still described a two-code protocol (0 rewritten,
1 no equivalent) and suggested `$(rtk rewrite "$CMD") || exit 0`. The
command returns four codes, and with no permission rules it exits 3 by
design (#1155), so a hook following the help text drops every default
rewrite. List all four codes in the help, note that the no-rule default
is 3, and align the table on `rewrite_cmd::run` with the protocol tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WcTsSr2cKxstnvgXk2bP88
Address #3808 review: only accept sudo/env/KEY=VALUE/-flag wrappers,
never search past non-Arg tokens for RTK_DISABLED=, and stop at the
first command word instead of scanning into wrapper arguments.

Fixes false positives from docker -e, && chains, ssh/xargs/watch, and
value-taking sudo flags like -u root.
…up logic

- Consolidate command mutation logic between V1 and V2 entrypoints into a shared helper
- Unify missing binary warning between setup() and server()
- Simplify expandHome and path discovery loops
- Add fs::create_dir_all for .claude dir in patch_settings_json_command and run_default_mode (fixes #2519, #4046 Windows regression)
- Hoist OpenCode plugin installation before Unchanged early return in run_claude_md_mode_with
- Fixes #4046 regressions.
The previous fix created the .claude directory unconditionally, but dry-run
mode must not create directories. Only create the directory when not in
dry-run mode.

Fixes test: test_run_default_mode_dry_run_does_not_create_missing_claude_dir
categorize_command mapped pip/pytest/ruff/mypy/sqlfluff to python but
missed uv, so every rtk uv … telemetry row fell through to other.
After the pip uv-fallback labelled those rows rtk pip …, the same
invocation was bucketed differently depending on entry path.

Adds uv to the python arm and pins the mapping in a unit test.

Fixes #4316
Keep wrappers (sudo/env/transparent_prefixes) when asking whether the
hook would have rewritten the line without RTK_DISABLED=. Peel only the
RTK_DISABLED= assignments for that gate; leave gain on the syntactic
strip_disabled_prefix so it does not inflate without a coverage check.

Addresses KuSh round-3 CHANGES_REQUESTED on #3808.
… filter

Successful fuzz runs collapsed to a bare "N passed", dropping the final "fuzz: elapsed:" line. Non-JSON stdout (e.g. setup errors) was discarded, so users saw "Go test: No tests found" instead of the real error.
- README.md: change --opencode description to 'Claude setup + OpenCode plugin'
- supported-agents.md: clarify OpenCode installs Claude setup + plugin
- troubleshooting.md: change --opencode label to 'Claude Code + OpenCode plugin'
- main.rs: install Claude even when --opencode is passed (additive)
- mod.rs: update help text to 'Claude setup + OpenCode plugin'

Ports docs edits from #4052 to #4323.
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.182 to 0.2.189.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.189/CHANGELOG.md)
- [Commits](rust-lang/libc@0.2.182...0.2.189)

---
updated-dependencies:
- dependency-name: libc
  dependency-version: 0.2.189
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps dirs from 5.0.1 to 7.0.0.

---
updated-dependencies:
- dependency-name: dirs
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.102 to 1.0.104.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](dtolnay/anyhow@1.0.102...1.0.104)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-version: 1.0.104
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [ignore](https://github.com/BurntSushi/ripgrep) from 0.4.25 to 0.4.32.
- [Release notes](https://github.com/BurntSushi/ripgrep/releases)
- [Changelog](https://github.com/BurntSushi/ripgrep/blob/master/CHANGELOG.md)
- [Commits](BurntSushi/ripgrep@ignore-0.4.25...ignore-0.4.32)

---
updated-dependencies:
- dependency-name: ignore
  dependency-version: 0.4.32
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.149 to 1.0.151.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](serde-rs/json@v1.0.149...v1.0.151)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-version: 1.0.151
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
KuSh and others added 22 commits October 8, 2026 17:53
`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
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 (#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>
…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.
KuSh and others added 2 commits October 12, 2026 00:46
…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>
feat(hook/opencode): support OpenCode V2, Desktop, and Windows (#3463, #3326)
@aeppling

Copy link
Copy Markdown
Contributor

Blocker: regression vs master from #4126

.claude/settings.json:

{"permissions":{"allow":["Bash(env:*)"],"deny":["Bash(git push:*)"]}}
$ rtk rewrite "env bash -c 'git push'"; echo "exit=$?"
env bash -c 'rtk git push'
exit=0
  • master (v0.51.0) and develop before feat(rewrite): rewrite inner commands of a quoted fish -c wrapper #4126 (0eddb84b): exit=1, no rewrite.
  • rtk hook claude now returns "permissionDecision":"allow" for the same command.
  • Unprefixed bash -c 'git push' is correctly denied (exit 2). A prefix (env, timeout N, nice, time, FOO=1) hides the wrapper from the inner-deny check in src/hooks/permissions.rs.
  • Affects rtk rewrite and every hook except OpenCode.
  • Suggested fix: don't rewrite a wrapper that sits behind a prefix.

aeppling pushed a commit that referenced this pull request Oct 12, 2026
#4126 made the rewrite descend through prefixes (env, assignments,
timeout, nice, nohup, time, exec, command, uv run, configured
transparent_prefixes) to a quoted shell wrapper, but the permission gate
still looked for the wrapper at the segment start only. A denied inner
command rode the outer allow: `env bash -c 'git push'` under
`allow env:*` + `deny git push:*` was rewritten and auto-allowed.

The gate now checks deny rules against the wrapper's script at every
position the rewrite reaches (`registry::shell_wrapper_positions`, the
rewrite's own prefix walk), on the text the rewrite reads: continuations
joined, segments not cut at a redirect (`lexer::split_for_wrappers`). A
match returns the new `PermissionVerdict::Defer`: no rewrite, no
decision, the host sees the command as typed — master's behaviour,
instead of #4126's Deny. Nothing else changes: a prefixed wrapper
otherwise keeps the outer command's verdict, and a wrapper at the
segment start keeps develop's rule.

Fixes the release blocker reported on #4399.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.