Conversation
why: uv's global `exclude-newer = "3 days"` supply-chain cooldown hides ruff 0.16.0 (released 2026-07-23) from the resolver, so the version floor in the next commit cannot resolve. what: - Add `ruff = false` to `[tool.uv.exclude-newer-package]` Temporary. Revert before merge - the cooldown clears on its own and the `ruff>=0.16` floor is what actually holds the version.
why: ruff 0.16.0 stabilizes rules inside prefixes this project already selects and starts formatting Python code blocks in Markdown. Pinning a floor keeps contributors and CI on the same diagnostics instead of splitting on whatever ruff each machine resolved. what: - Raise `ruff` to `>=0.16.0` in the `dev` and `lint` dependency groups - Relock `uv.lock`: ruff 0.15.22 -> 0.16.0 https://astral.sh/blog/ruff-v0.16.0
why: Contributors reading the changelog need to know the dev toolchain floor moved and that `ruff format` now reaches Markdown. what: - Add a `Minimum `ruff>=0.16.0`` entry under `### Development` (#113)
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #113 +/- ##
=======================================
Coverage 86.08% 86.08%
=======================================
Files 46 46
Lines 3593 3593
Branches 516 516
=======================================
Hits 3093 3093
Misses 355 355
Partials 145 145 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
why: ruff 0.16.0 published 2026-07-23T19:10Z and has now cleared uv's 3-day supply-chain cooldown, so the resolver reaches it unaided. The `ruff>=0.16.0` floor is what holds the version; leaving the exemption would permanently opt ruff out of the cooldown guard. what: - Drop `ruff = false` from `[tool.uv.exclude-newer-package]` - Relock: the setting is recorded in `uv.lock`, so removing it forces a re-resolve. ruff stays at 0.16.0 and no other package moves. This reverts commit afa3e20.
why: ruff 0.16 ships a curated 413-rule default set as its recommended baseline. An explicit `select` replaces that set rather than extending it, so this project was running 351 rules and silently opting out of the default set. `extend-select` layers the project's own linters on top of the default set instead of in place of it. what: - Replace `select` with `extend-select`, same entries, one per line - Note in the file why `select` stays unset Enabled rules go from 351 to 565. https://docs.astral.sh/ruff/linter/#rule-selection
why: ruff's default rule set flags the single-letter `re` flag aliases. `re.S` is terse to the point of being cryptic at the call site; the long name says what the flag does without a trip to the `re` docs. what: - Use `re.DOTALL` instead of the `re.S` alias https://docs.astral.sh/ruff/rules/regex-flag-alias/
why: ruff's default rule set flags hand-rolled suffix stripping. The conditional slice restated the suffix twice and depended on `-len()` arithmetic staying in sync with the `endswith` guard; `str.removesuffix` expresses the same intent in one term. what: - Replace the conditional slice in `_derive_server_and_entry` with `str.removesuffix` https://docs.astral.sh/ruff/rules/slice-to-remove-prefix-or-suffix/
why: ruff's default rule set flags `sorted(...)[0]`, which sorts a whole collection to read one element. Both sites want the lowest registered server name to suggest via `--server`; `min` says that directly and drops the throwaway sort. what: - Use `min` instead of `sorted(...)[0]` in `_naming_hint` and the status report's name-mismatch warning https://docs.astral.sh/ruff/rules/sorted-min-max/
why: ruff's default rule set flags joining a static tuple, since the separator and the operands are all known at author time. ruff's own fix collapses the seven sections onto one 187-char line and trades the finding for an E501; interpolating one section per line keeps the sections scannable and stays inside the line limit. what: - Compose `_BASE_INSTRUCTIONS` from f-string fragments instead of `"\n\n".join(...)` https://docs.astral.sh/ruff/rules/static-join-to-f-string/
why: ruff's default rule set flags unparenthesized implicit string concatenation inside a collection, because a missing comma reads identically to a deliberate join and silently merges two elements. These fixtures model captured pane lines, where element boundaries are the thing under test. what: - Wrap the multi-line marker strings in `FILTER_DROP_FIXTURES` in parentheses, matching the form already used by the targeted-marker fixture https://docs.astral.sh/ruff/rules/implicit-string-concatenation-in-collection-literal/
why: ruff's default rule set flags `exec`. `docs/conf.py` execs the package's `__about__.py` to read version and project metadata without importing `libtmux_mcp` — the standard Sphinx idiom, on a repo-local file, with no user input reaching it. Scoped to that one file so a stray `exec` anywhere else still fails the lint. what: - Add a `docs/conf.py` per-file ignore for S102 with the reason inline https://docs.astral.sh/ruff/rules/exec-builtin/
why: ruff's default rule set flags every `except Exception`. All nine sites are deliberate boundaries — the MCP error middleware, the batch runners' per-operation isolation, the best-effort tmux probes and buffer GC, and mcp_swap's config rollback. tmux and the agent CLIs' config files fail in more ways than a narrowed clause can enumerate, so narrowing would trade a logged degradation for a crash in code whose whole job is not to crash. what: - Add per-file ignores for BLE001 in the six files that hold those boundaries, with the reason for each recorded inline - Leave BLE001 enforced everywhere else https://docs.astral.sh/ruff/rules/blind-except/
why: The ruff entry covered the version floor but not the config change that came with it, which is the part that alters what CI enforces. what: - Describe the `select` to `extend-select` move, the enabled-rule count change, the fixes it produced, and the two scoped ignores
why: ruff's default rule set flags a shebang on a non-executable file. The bit was never set, and the interpreter named was wrong besides: this is a PEP 723 script whose inline metadata declares tomlkit, so running it under bare `python3` raises ModuleNotFoundError. `uv run --script` honours that metadata. The rule is skipped on WSL, so this only surfaced on CI. what: - Point the shebang at `uv run --script` - Set the executable bit in the index `uv run scripts/mcp_swap.py` and the `just mcp-*` recipes are unchanged. https://docs.astral.sh/ruff/rules/shebang-not-executable/
why: The default rule set entry listed the fixes it produced but not the one that only surfaced on CI, which is the one with a user-visible effect: the script can now be run directly. what: - Mention the executable bit and the `uv run --script` shebang
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
rufffloor to>=0.16.0in thedevandlintdependency groups so contributors and CI see the same diagnostics instead of splitting on whatever ruff each machine happened to resolve.[tool.ruff.lint]fromselecttoextend-select. An explicitselectreplaces the default set rather than adding to it, so this project had been silently opting out of every rule it did not name itself. Enabled rules go from 351 to 565.pyproject.toml.Why
extend-selectruff 0.16 ships a curated default rule set as its recommended baseline. There is no token that names that set from inside
select— the selector grammar hasALL, prefixes, and individual codes, and nothing else — so a config with an explicitselectcannot say "the defaults, plus these". Leavingselectunset and layering the project's own linters on withextend-selectis the only way to get defaults-plus-extras. The file carries a comment saying so, because the natural instinct on reading it is to "tidy"extend-selectback intoselect.Expanding to whole prefixes instead was considered and rejected: it drags in all of
D,PL,S, and friends rather than ruff's curated subset, and is one to two orders of magnitude noisier for no corresponding gain in signal.Findings fixed
FURB167—re.Sspelled out asre.DOTALLin the docs-drift test.FURB188—mcp_swapderived the server name with a conditional slice that restated the-mcpsuffix twice and depended on-len()arithmetic staying in sync with its ownendswithguard. Nowstr.removesuffix.FURB192—mcp_swapsorted a whole set of registered server names to read the first element, twice, both times to suggest a--servervalue. Nowmin.FLY002— the server's base instruction blob joined a static tuple of sections. ruff's own fix collapses the sections onto a single over-long line and trades the finding for anE501, so this one is hand-written instead: an f-string with one section per line, which reads the same as the tuple did.ISC004— therun_commandmarker-filtering fixtures build captured pane lines as multi-line implicit concatenations inside a list. Element boundaries are exactly what those fixtures test, and unparenthesized concatenation makes a dropped comma indistinguishable from a deliberate join. Parenthesized, matching the form one fixture in the same literal already used.EXE001—scripts/mcp_swap.pycarried a shebang without the executable bit, and the interpreter it named was wrong besides: it is a PEP 723 script whose inline metadata declarestomlkit, so direct execution under barepython3raisedModuleNotFoundError. The shebang now points atuv run --script, which honours that metadata, and the bit is set in the index.uv run scripts/mcp_swap.pyand thejust mcp-*recipes are unchanged. Worth flagging for anyone reproducing this locally: ruff does not enforceEXE001on WSL, so this one surfaced only on CI.Findings scoped as ignores
S102indocs/conf.py— the Sphinx idiom ofexecing the package's__about__.pyto read version metadata without importing the package into the docs build. A repo-local file, no user input. Scoped to that one file so a strayexecanywhere else still fails the lint.BLE001in six files — every site was read individually, and all of them are deliberate boundaries: the MCP error middleware that renders any tool exception as anis_errorresult, the per-operation isolation in the tool and send-keys batch runners, the best-effort tmux version probes and lifespan buffer GC, andmcp_swap's rollback to a config's original bytes when a write fails. tmux and the agent CLIs' config files fail in more ways than a narrowed clause can enumerate, so narrowing here would trade a logged degradation for a crash in code whose whole job is not to crash.BLE001stays enforced everywhere else.Design decisions
Floor, not pin.
>=0.16.0is what holds the version: it guarantees every contributor is at or past the release that changes formatting behavior, while still letting them take later patch releases.uv.lockpins the exact version CI resolves.One commit per rule. Each fixed rule and each added ignore is its own commit, so a rule can be reverted or re-argued without unpicking the rest, and each commit body carries the rule's documentation link.
No
noqa, no explanatory code comments. Intentional idioms are declared in the ruff config, where the next reader of the config finds them, rather than annotated at the call site.Markdown formatting is a no-op here. 0.16.0 makes
ruff formatformat Python and pycon code blocks inside Markdown by default, so the README and thedocs/tree are now in scope. Every Markdown file in this repo is already formatted the way ruff wants it, soruff format --check .passes across the wider file set with no churn.Test plan
uv run ruff check .— all checks passeduv run ruff format --check .— every file already formatted (Python plus Markdown code blocks)uv run mypy— no issues founduv run pytest— full suite greenruff check --show-settings .before and after the config change