From afa3e20505267fd16ccbde13c9cdab6cd7c89374 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 13:22:51 -0500 Subject: [PATCH 01/15] chore(uv[cooldown]): Exempt ruff from release cooldown 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. --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 42a9f94..691a73c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,6 +136,10 @@ sphinx-gp-sitemap = false sphinx-gp-theme = false sphinx-ux-autodoc-layout = false sphinx-ux-badges = false +# TEMPORARY - REVERT BEFORE MERGE. ruff 0.16.0 released 2026-07-23 and is +# still inside the 3-day cooldown, so the resolver cannot see it. Drop this +# line once the cooldown clears; the version floor is what holds ruff 0.16. +ruff = false [tool.mypy] strict = true From 0bdb4ddedab632ab292c2ea79fc150ff23d12b7f Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 13:23:07 -0500 Subject: [PATCH 02/15] py(deps[dev]): Require ruff>=0.16.0 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 --- pyproject.toml | 4 ++-- uv.lock | 47 ++++++++++++++++++++++++----------------------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 691a73c..070662f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ dev = [ "coverage", "pytest-cov", # Lint - "ruff", + "ruff>=0.16.0", "mypy", ] @@ -103,7 +103,7 @@ coverage =[ ] lint = [ "typing-extensions; python_version < '3.11'", - "ruff", + "ruff>=0.16.0", "mypy", ] diff --git a/uv.lock b/uv.lock index c623c7c..2bc6263 100644 --- a/uv.lock +++ b/uv.lock @@ -28,6 +28,7 @@ sphinx-ux-badges = false gp-libs = false sphinx-autodoc-docutils = false gp-furo-theme = false +ruff = false sphinx-gp-sitemap = false sphinx-fonts = false sphinx-autodoc-typehints-gp = false @@ -1265,7 +1266,7 @@ dev = [ { name = "pytest-rerunfailures" }, { name = "pytest-watcher" }, { name = "pytest-xdist" }, - { name = "ruff" }, + { name = "ruff", specifier = ">=0.16.0" }, { name = "sphinx-autobuild" }, { name = "sphinx-autodoc-api-style", specifier = "==0.0.1a36" }, { name = "sphinx-autodoc-fastmcp", specifier = "==0.0.1a36" }, @@ -1282,7 +1283,7 @@ docs = [ ] lint = [ { name = "mypy" }, - { name = "ruff" }, + { name = "ruff", specifier = ">=0.16.0" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] testing = [ @@ -2417,27 +2418,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/06/ae069393fc66e8ff33036d4b368003833bf6e88ccf182e17e7a2f1c754fd/ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809", size = 4785063, upload-time = "2026-07-16T15:14:13.244Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/23/18/ee54b7ae1e121be7a28ea6da4b67564ebb0530e183a54415ab7e3bcd2c4e/ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8", size = 10781258, upload-time = "2026-07-16T15:13:19.452Z" }, - { url = "https://files.pythonhosted.org/packages/2f/d2/2520cb14761ddbeaf57642a76942fc36adcbdbe53b4532241995f6fc485c/ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697", size = 10999477, upload-time = "2026-07-16T15:13:23.318Z" }, - { url = "https://files.pythonhosted.org/packages/c9/10/74e53572aa758dfaa678c2a2646b5c5515d884b7ca56be4d2ce03ca4b560/ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f", size = 10466716, upload-time = "2026-07-16T15:13:26.162Z" }, - { url = "https://files.pythonhosted.org/packages/1e/cc/44eaaf0844e028182f2d0a8f2190d0f359159aed0a9e5ab861d892f1ae2a/ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576", size = 10892644, upload-time = "2026-07-16T15:13:29.229Z" }, - { url = "https://files.pythonhosted.org/packages/9f/21/8edf559014d2b0f82beea19cfb713993ad802ccda16868769979c6090a84/ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178", size = 10576719, upload-time = "2026-07-16T15:13:32.35Z" }, - { url = "https://files.pythonhosted.org/packages/bf/1e/3a13abd392a3b50b62e5938a831f9ab6e588358cacad5c18545b716d2182/ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224", size = 11376494, upload-time = "2026-07-16T15:13:35.958Z" }, - { url = "https://files.pythonhosted.org/packages/bf/3e/422d3d95bcf04dd78e1aeac22184d4f9a8fb2c01865d39d44618484a0317/ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a", size = 12208370, upload-time = "2026-07-16T15:13:39.185Z" }, - { url = "https://files.pythonhosted.org/packages/1e/91/5d065a0e0a02bf4813f5119ad278462eed081d2b832eb7c021ade0ec9e65/ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e", size = 11581098, upload-time = "2026-07-16T15:13:42.132Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f9/a0d4871d12fae702eb1f41b686caf05f1f8b124dc6db6f784f53d74918fa/ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb", size = 11399422, upload-time = "2026-07-16T15:13:45.2Z" }, - { url = "https://files.pythonhosted.org/packages/18/80/c843a5176cddbceb0b7e8dd41cf9993490796c1c469348d384f5a5c13c56/ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74", size = 11381683, upload-time = "2026-07-16T15:13:48.46Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/8485de0ae92239438a36cfc51350db9b9e85c9ebdfaea91b18e422706662/ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c", size = 10850295, upload-time = "2026-07-16T15:13:51.655Z" }, - { url = "https://files.pythonhosted.org/packages/fa/91/24977ec2ec72eaf15e4394ace2959fdff2dd1e14f03e005e838023407169/ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296", size = 10579640, upload-time = "2026-07-16T15:13:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/9c/47/9b51216951974df1f263ac19da550d34252e0ed7218c25f10c5ef9ed7517/ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262", size = 11105077, upload-time = "2026-07-16T15:13:57.915Z" }, - { url = "https://files.pythonhosted.org/packages/c2/47/20e9d4a3b8016778acea5fc32bb50d35d207500a17ddb529ffa6996feef8/ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64", size = 11490980, upload-time = "2026-07-16T15:14:01.032Z" }, - { url = "https://files.pythonhosted.org/packages/4d/76/3f72d8fc38c1cb77b38c56a70da9d0c17700cc1cc50f9649c9d3c8f5ba71/ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf", size = 10789165, upload-time = "2026-07-16T15:14:04.16Z" }, - { url = "https://files.pythonhosted.org/packages/cb/46/4965251734c2b6fcdca1b1b187d20bcac3af0ee5b083b89c910bb961ce3a/ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde", size = 11938297, upload-time = "2026-07-16T15:14:07.316Z" }, - { url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172, upload-time = "2026-07-16T15:14:10.51Z" }, +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" }, + { url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" }, + { url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" }, + { url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" }, + { url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" }, + { url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" }, + { url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" }, + { url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" }, + { url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" }, + { url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" }, ] [[package]] From 2edc729b2e78267d920765828f3335db6855e985 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 13:26:49 -0500 Subject: [PATCH 03/15] docs(CHANGES[unreleased]): Note ruff 0.16.0 floor 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) --- CHANGES | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES b/CHANGES index af452ca..055f502 100644 --- a/CHANGES +++ b/CHANGES @@ -23,6 +23,15 @@ Workflow actions moved to their current major releases: `actions/checkout` v7, and `dorny/paths-filter` v4. Workflow behavior is unchanged, though setup-uv no longer prunes the uv cache, so the first run after this repopulates it. +**Minimum `ruff>=0.16.0`** (was unpinned) + +The `dev` and `lint` dependency groups now require ruff 0.16.0 or newer, so +contributors and CI see the same diagnostics rather than whichever ruff each +machine resolved. From this release on, `ruff format` also formats Python code +blocks inside Markdown, so `uv run ruff format .` reaches the README and the +`docs/` tree. Neither the lint nor the format change rewrites anything here. +(#113) + ## libtmux-mcp 0.1.0a19 (2026-07-25) libtmux-mcp 0.1.0a19 makes {tooliconl}`wait-for-text` see the output it was asked to watch for, and puts a ceiling under every wait. The tool anchored one row below the cursor's position at entry, which on a quiescent pane is exactly where the next line lands, so the case it exists for — output you did not author, a daemon printing a single `ready` line — could not match at all. Waits are now bounded by a server ceiling, cancellable without orphaning their tmux child, and report an `outcome` that distinguishes a command that never ran from output that did not match from a pager owning the pane. The wait API changes shape in the process: `pattern` becomes `patterns`, `stop` markers end a wait early, and `wait_for_content_change` is removed in favour of `wait_for_text(patterns=null)`. From 8399624437fd2c70e61d0929677b5e148ebc34e5 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 14:22:43 -0500 Subject: [PATCH 04/15] Revert "chore(uv[cooldown]): Exempt ruff from release cooldown" 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 afa3e20505267fd16ccbde13c9cdab6cd7c89374. --- pyproject.toml | 4 ---- uv.lock | 1 - 2 files changed, 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 070662f..08eb656 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,10 +136,6 @@ sphinx-gp-sitemap = false sphinx-gp-theme = false sphinx-ux-autodoc-layout = false sphinx-ux-badges = false -# TEMPORARY - REVERT BEFORE MERGE. ruff 0.16.0 released 2026-07-23 and is -# still inside the 3-day cooldown, so the resolver cannot see it. Drop this -# line once the cooldown clears; the version floor is what holds ruff 0.16. -ruff = false [tool.mypy] strict = true diff --git a/uv.lock b/uv.lock index 2bc6263..608c521 100644 --- a/uv.lock +++ b/uv.lock @@ -28,7 +28,6 @@ sphinx-ux-badges = false gp-libs = false sphinx-autodoc-docutils = false gp-furo-theme = false -ruff = false sphinx-gp-sitemap = false sphinx-fonts = false sphinx-autodoc-typehints-gp = false From 6edd03fe22d20e95efdd9c59f45474cb4591ed49 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:04:11 -0500 Subject: [PATCH 05/15] chore(ruff[lint]): Adopt ruff's default rule set 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 --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 08eb656..3846b9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -188,7 +188,10 @@ exclude_lines = [ target-version = "py310" [tool.ruff.lint] -select = [ +# `select` is deliberately unset: ruff 0.16 enables a curated default rule +# set, and an explicit `select` would replace it rather than extend it. +# `extend-select` layers this project's additional linters on top. +extend-select = [ "E", # pycodestyle "F", # pyflakes "I", # isort From a08f9929e9a370d69cd2b1488b95e236003729da Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:05:53 -0500 Subject: [PATCH 06/15] style(tests[test_prompts]): Spell out re.DOTALL 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/ --- tests/test_prompts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_prompts.py b/tests/test_prompts.py index a11db55..c3ed30d 100644 --- a/tests/test_prompts.py +++ b/tests/test_prompts.py @@ -438,7 +438,7 @@ async def _render() -> str: block = re.search( r'\*\*Sample render\*\* \(``pane_id="%1"``\):\n\n````markdown\n(.*?)````', page.read_text(), - re.S, + re.DOTALL, ) assert block is not None, "interrupt_gracefully sample-render block not found" From 5b9015df64040f417b5d569ef2258942daa7a12f Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:06:09 -0500 Subject: [PATCH 07/15] refactor(scripts[mcp_swap]): Derive server name with removesuffix 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/ --- scripts/mcp_swap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index 214efa8..0117564 100644 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -648,7 +648,7 @@ def resolve_repo_meta(repo: pathlib.Path) -> tuple[str, str]: msg = f"{pyproject} has no [project] table" raise RuntimeError(msg) name = str(project["name"]) - server = name[: -len("-mcp")] if name.endswith("-mcp") else name + server = name.removesuffix("-mcp") scripts = project.get("scripts") or {} if not scripts: msg = f"{pyproject} has no [project.scripts] — cannot derive entry" From 8bd78eec41444e9562421f6e8df54d59016e46e3 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:06:30 -0500 Subject: [PATCH 08/15] refactor(scripts[mcp_swap]): Pick suggested name with min() 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/ --- scripts/mcp_swap.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py index 0117564..e4e648c 100644 --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -1187,7 +1187,7 @@ def _naming_hint(repo: pathlib.Path, server: str) -> str | None: names.add(name) if server_points or not names: return None - pick = sorted(names)[0] + pick = min(names) return ( f"note: nothing is registered under server {server!r}, but this repo is " f"registered as {sorted(names)} — pass --server {pick} to target it" @@ -1237,7 +1237,7 @@ def cmd_doctor(args: argparse.Namespace) -> int: print(" (no CLI currently points at this repo)") if all_repo_names and server not in all_repo_names: - pick = sorted(all_repo_names)[0] + pick = min(all_repo_names) print( f" ! server name mismatch: this repo is registered as " f"{sorted(all_repo_names)}, not {server!r} — use --server {pick}" From 81418f067d98a3775fb3b7141a5235cf44d7fe98 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:06:55 -0500 Subject: [PATCH 09/15] refactor(mcp[server]): Build base instructions as an f-string 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/ --- src/libtmux_mcp/server.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/libtmux_mcp/server.py b/src/libtmux_mcp/server.py index 10d2670..c5772f1 100644 --- a/src/libtmux_mcp/server.py +++ b/src/libtmux_mcp/server.py @@ -128,16 +128,14 @@ "clipboard history." ) -_BASE_INSTRUCTIONS = "\n\n".join( - ( - _INSTR_HIERARCHY, - _INSTR_SCOPE, - _INSTR_METADATA_VS_CONTENT, - _INSTR_READ_TOOLS, - _INSTR_WAIT_NOT_POLL, - _INSTR_HOOKS_GAP, - _INSTR_BUFFERS_GAP, - ) +_BASE_INSTRUCTIONS = ( + f"{_INSTR_HIERARCHY}\n\n" + f"{_INSTR_SCOPE}\n\n" + f"{_INSTR_METADATA_VS_CONTENT}\n\n" + f"{_INSTR_READ_TOOLS}\n\n" + f"{_INSTR_WAIT_NOT_POLL}\n\n" + f"{_INSTR_HOOKS_GAP}\n\n" + f"{_INSTR_BUFFERS_GAP}" ) _INSTRUCTIONS_MAX_BYTES = 2048 From e3a095928f1fcaadf66fbc1b3d3d59c6a40968c5 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:08:33 -0500 Subject: [PATCH 10/15] test(pane_tools[fixtures]): Parenthesize wrapped marker lines 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/ --- tests/test_pane_tools.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/tests/test_pane_tools.py b/tests/test_pane_tools.py index 9db1979..1e3d2ff 100644 --- a/tests/test_pane_tools.py +++ b/tests/test_pane_tools.py @@ -980,10 +980,14 @@ class FilterCurrentSyncLineFixture(t.NamedTuple): "current_wrapped_long_marker", [ "RUN_OK", - "∙ }; __libtmux_mcp_status=$?; tmux set-option -p " - f"@libtmux_mcp_status_{_CURRENT_ID[:10]}", - f'{_CURRENT_ID[10:]} "$__libtmux_mcp_status"; ' - "tmux wait-for -S libtmux_mcp_run_", + ( + "∙ }; __libtmux_mcp_status=$?; tmux set-option -p " + f"@libtmux_mcp_status_{_CURRENT_ID[:10]}" + ), + ( + f'{_CURRENT_ID[10:]} "$__libtmux_mcp_status"; ' + "tmux wait-for -S libtmux_mcp_run_" + ), _CURRENT_ID, ], f"libtmux_mcp_run_{_CURRENT_ID}", @@ -993,10 +997,14 @@ class FilterCurrentSyncLineFixture(t.NamedTuple): "previous_wrapped_long_marker", [ "RUN_OK", - "∙ }; __libtmux_mcp_status=$?; tmux set-option -p " - f"@libtmux_mcp_status_{_PREVIOUS_ID[:10]}", - f'{_PREVIOUS_ID[10:]} "$__libtmux_mcp_status"; ' - "tmux wait-for -S libtmux_mcp_run_", + ( + "∙ }; __libtmux_mcp_status=$?; tmux set-option -p " + f"@libtmux_mcp_status_{_PREVIOUS_ID[:10]}" + ), + ( + f'{_PREVIOUS_ID[10:]} "$__libtmux_mcp_status"; ' + "tmux wait-for -S libtmux_mcp_run_" + ), _PREVIOUS_ID, ], f"libtmux_mcp_run_{_CURRENT_ID}", @@ -1006,8 +1014,10 @@ class FilterCurrentSyncLineFixture(t.NamedTuple): "current_short_marker", [ "RUN_OK", - f'∙ }}; s=$?; tmux set-option -p @s_{_SHORT_CURRENT_ID} "$s"; ' - f"tmux wait-for -S r_{_SHORT_CURRENT_ID}", + ( + f'∙ }}; s=$?; tmux set-option -p @s_{_SHORT_CURRENT_ID} "$s"; ' + f"tmux wait-for -S r_{_SHORT_CURRENT_ID}" + ), ], f"r_{_SHORT_CURRENT_ID}", f"@s_{_SHORT_CURRENT_ID}", From d9b5bcdf490cb51fd3222957ca31022c96f48cbf Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:08:53 -0500 Subject: [PATCH 11/15] chore(ruff[lint]): Ignore S102 in docs/conf.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/ --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 3846b9a..d17f310 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -240,6 +240,10 @@ convention = "numpy" [tool.ruff.lint.per-file-ignores] "*/__init__.py" = ["F401"] +# The Sphinx conf idiom: `exec` the package's `__about__.py` to read +# version metadata without importing the package (and its dependencies) +# into the docs build. The path is a repo-local file, not user input. +"docs/conf.py" = ["S102"] [tool.pytest.ini_options] addopts = [ From 4c3d45c944278f5b92f26a4e2a92bbf56eaa7729 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:09:17 -0500 Subject: [PATCH 12/15] chore(ruff[lint]): Ignore BLE001 at the boundary catch-alls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/ --- pyproject.toml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index d17f310..0efebd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -244,6 +244,26 @@ convention = "numpy" # version metadata without importing the package (and its dependencies) # into the docs build. The path is a repo-local file, not user input. "docs/conf.py" = ["S102"] +# Boundary catch-alls. Every `except Exception` in these files converts +# an arbitrary failure into a reported result or a debug log instead of +# letting it escape a place that cannot report it: +# middleware.py the MCP error boundary, renders any tool exception +# as an `is_error` result +# batch_tools.py isolates one failed operation from its batch +# pane_tools/io.py same, per send_keys operation +# server.py best-effort buffer GC during lifespan shutdown +# server_tools.py best-effort tmux metadata probes over sockets that +# may be stale, denied, or running an old tmux +# mcp_swap.py restores a CLI config's original bytes when a +# write fails +# tmux and the CLI configs fail in more ways than a narrowed clause can +# enumerate, so narrowing here trades a logged degradation for a crash. +"scripts/mcp_swap.py" = ["BLE001"] +"src/libtmux_mcp/middleware.py" = ["BLE001"] +"src/libtmux_mcp/server.py" = ["BLE001"] +"src/libtmux_mcp/tools/batch_tools.py" = ["BLE001"] +"src/libtmux_mcp/tools/pane_tools/io.py" = ["BLE001"] +"src/libtmux_mcp/tools/server_tools.py" = ["BLE001"] [tool.pytest.ini_options] addopts = [ From 3543a9d7d206ed75d0f4bc8456ee0ea7538dfe40 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:11:40 -0500 Subject: [PATCH 13/15] docs(CHANGES[unreleased]): Note default rule set adoption 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 --- CHANGES | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGES b/CHANGES index 055f502..c9c730f 100644 --- a/CHANGES +++ b/CHANGES @@ -32,6 +32,21 @@ blocks inside Markdown, so `uv run ruff format .` reaches the README and the `docs/` tree. Neither the lint nor the format change rewrites anything here. (#113) +**ruff's curated default rule set is now enabled** + +`[tool.ruff.lint]` layers this project's linters on with `extend-select` instead +of `select`. An explicit `select` replaces ruff's default set rather than adding +to it, so the project had been silently opting out of every rule it did not name +itself; enabled rules go from 351 to 565. The findings that surfaced are fixed — +`re.DOTALL` spelled out in place of `re.S`, `str.removesuffix` and `min()` in +`scripts/mcp_swap.py`, the server instruction blob built as an f-string, and the +wrapped marker fixtures parenthesized so a dropped comma can no longer pass for +a deliberate join. Two rules are scoped ignores with their reasons recorded in +`pyproject.toml`: `exec` in `docs/conf.py`, which is how Sphinx reads version +metadata, and the `except Exception` boundaries in the error middleware, the +batch runners, the best-effort tmux probes, and `mcp_swap`'s config rollback. +(#113) + ## libtmux-mcp 0.1.0a19 (2026-07-25) libtmux-mcp 0.1.0a19 makes {tooliconl}`wait-for-text` see the output it was asked to watch for, and puts a ceiling under every wait. The tool anchored one row below the cursor's position at entry, which on a quiescent pane is exactly where the next line lands, so the case it exists for — output you did not author, a daemon printing a single `ready` line — could not match at all. Waits are now bounded by a server ceiling, cancellable without orphaning their tmux child, and report an `outcome` that distinguishes a command that never ran from output that did not match from a pager owning the pane. The wait API changes shape in the process: `pattern` becomes `patterns`, `stop` markers end a wait early, and `wait_for_content_change` is removed in favour of `wait_for_text(patterns=null)`. From d7c968697adf6a0da9b4c0e5c85e8ca21d411fb9 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:14:57 -0500 Subject: [PATCH 14/15] fix(scripts[mcp_swap]): Make the script's shebang truthful 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/ --- scripts/mcp_swap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 scripts/mcp_swap.py diff --git a/scripts/mcp_swap.py b/scripts/mcp_swap.py old mode 100644 new mode 100755 index e4e648c..5b4355e --- a/scripts/mcp_swap.py +++ b/scripts/mcp_swap.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.10" # dependencies = ["tomlkit>=0.13"] From 64ca1cf89ac8ddd5718248f6e5eeb6e72a056b14 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 26 Jul 2026 15:17:03 -0500 Subject: [PATCH 15/15] docs(CHANGES[unreleased]): Note the mcp_swap shebang fix 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 --- CHANGES | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index c9c730f..bd33a90 100644 --- a/CHANGES +++ b/CHANGES @@ -41,7 +41,10 @@ itself; enabled rules go from 351 to 565. The findings that surfaced are fixed `re.DOTALL` spelled out in place of `re.S`, `str.removesuffix` and `min()` in `scripts/mcp_swap.py`, the server instruction blob built as an f-string, and the wrapped marker fixtures parenthesized so a dropped comma can no longer pass for -a deliberate join. Two rules are scoped ignores with their reasons recorded in +a deliberate join. `scripts/mcp_swap.py` is now executable and its shebang points +at `uv run --script`, so running it directly honours the PEP 723 metadata instead +of failing to import `tomlkit` under a bare `python3`. Two rules are scoped +ignores with their reasons recorded in `pyproject.toml`: `exec` in `docs/conf.py`, which is how Sphinx reads version metadata, and the `except Exception` boundaries in the error middleware, the batch runners, the best-effort tmux probes, and `mcp_swap`'s config rollback.