From 2ad93dd713f00af82066a47f503f66e7282ffb46 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 17 Oct 2026 02:36:00 -0500 Subject: [PATCH 1/8] py(deps): Pin optional tmux feature policy why: Use the coordinated parent revision with layout refusal and per-operation compatibility. what: - Pin the immutable policy-2 libtmux source in project and lock. - Keep the matching 0.62 package metadata. --- pyproject.toml | 2 +- uv.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 656382ef00..a375309940 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,7 +130,7 @@ exclude = [ ] [tool.uv.sources] -libtmux = { git = "https://github.com/tmux-python/libtmux.git", rev = "f6e4630a15f342382487a60e1e1477abcd52294d" } +libtmux = { git = "https://github.com/tmux-python/libtmux.git", rev = "5df9cb3e89647577b7dd8484f844ffe1053129bb" } [tool.uv.exclude-newer-package] # git-pull packages release in lockstep with their workspaces, so a diff --git a/uv.lock b/uv.lock index dcb8e22bcf..ce70b54b40 100644 --- a/uv.lock +++ b/uv.lock @@ -761,7 +761,7 @@ wheels = [ [[package]] name = "libtmux" version = "0.62.0" -source = { git = "https://github.com/tmux-python/libtmux.git?rev=f6e4630a15f342382487a60e1e1477abcd52294d#f6e4630a15f342382487a60e1e1477abcd52294d" } +source = { git = "https://github.com/tmux-python/libtmux.git?rev=5df9cb3e89647577b7dd8484f844ffe1053129bb#5df9cb3e89647577b7dd8484f844ffe1053129bb" } [[package]] name = "linkify-it-py" @@ -1855,7 +1855,7 @@ testing = [ [package.metadata] requires-dist = [ - { name = "libtmux", git = "https://github.com/tmux-python/libtmux.git?rev=f6e4630a15f342382487a60e1e1477abcd52294d" }, + { name = "libtmux", git = "https://github.com/tmux-python/libtmux.git?rev=5df9cb3e89647577b7dd8484f844ffe1053129bb" }, { name = "pyyaml", specifier = ">=6.0" }, ] From fb3920ddc931d63220c43b82e465c6decff61792 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 17 Oct 2026 02:36:09 -0500 Subject: [PATCH 2/8] Backend(fix): Require optional policy support why: A package version can still lack required operation guards. what: - Require integer policy 2 before workspace imports or shell code. - Exercise refusal, supported operations and native layout behavior. --- src/tmuxp/util.py | 2 +- tests/test_feature_policy.py | 313 +++++++++++++++++++++++++++++++++++ 2 files changed, 314 insertions(+), 1 deletion(-) create mode 100644 tests/test_feature_policy.py diff --git a/src/tmuxp/util.py b/src/tmuxp/util.py index bd50e0f415..4d1113727a 100644 --- a/src/tmuxp/util.py +++ b/src/tmuxp/util.py @@ -38,7 +38,7 @@ def _require_backend() -> t.Any: raise exc.LibtmuxCompatibilityError(message) from error if not all( callable(getattr(support, name, None)) - for name in ("require_version", "server_version") + for name in ("require_version", "server_version", "require_commands") ) or not isinstance(getattr(support, "MINIMUM", None), str): raise exc.LibtmuxCompatibilityError(message) support.require_version(support.MINIMUM, minimum="3.3a") diff --git a/tests/test_feature_policy.py b/tests/test_feature_policy.py new file mode 100644 index 0000000000..b3790f7a26 --- /dev/null +++ b/tests/test_feature_policy.py @@ -0,0 +1,313 @@ +"""Coordinated optional-feature policy before custom workspace code.""" + +from __future__ import annotations + +import json +import pathlib +import re +import shlex +import typing as t +import uuid + +import libtmux.common +import pytest +from libtmux.exc import LibTmuxException +from libtmux.server import Server +from libtmux.session import Session + +from tmuxp import cli, exc, util +from tmuxp.cli import load +from tmuxp.workspace import freezer + + +@pytest.mark.parametrize("marker", [None, False, True, "1", "2", -1, 0, 1, 1.0]) +@pytest.mark.parametrize("kind", ["builder", "plugin"]) +def test_unqualified_policy_refuses_before_import( + tmp_path: pathlib.Path, + monkeypatch: pytest.MonkeyPatch, + marker: t.Any, + kind: str, +) -> None: + """A matching package version cannot authorize an unqualified backend.""" + if marker is None: + monkeypatch.delattr( + libtmux.common, "TMUX_OPTIONAL_FEATURE_POLICY_VERSION", raising=False + ) + else: + monkeypatch.setattr( + libtmux.common, + "TMUX_OPTIONAL_FEATURE_POLICY_VERSION", + marker, + raising=False, + ) + effect = tmp_path / "imported" + module = "tmuxp_policy_" + uuid.uuid4().hex + (tmp_path / f"{module}.py").write_text( + f"from pathlib import Path\nPath({str(effect)!r}).touch()\n" + ) + config: dict[str, t.Any] = { + "session_name": "policy", + "windows": [{"panes": ["true"]}], + } + if kind == "builder": + config.update( + workspace_builder=f"{module}:Builder", + workspace_builder_paths=[str(tmp_path)], + ) + else: + monkeypatch.syspath_prepend(str(tmp_path)) + config["plugins"] = [f"{module}.Plugin"] + path = tmp_path / "workspace.json" + path.write_text(json.dumps(config)) + with pytest.raises(exc.LibtmuxCompatibilityError, match="coordinated libtmux"): + load.load_workspace(path, detached=True) + assert not effect.exists() + + +@pytest.mark.parametrize("marker", [2, 3]) +def test_integer_policy_is_accepted( + monkeypatch: pytest.MonkeyPatch, marker: int +) -> None: + """Accept this policy and later compatible integer policy revisions.""" + monkeypatch.setattr( + libtmux.common, + "TMUX_OPTIONAL_FEATURE_POLICY_VERSION", + marker, + raising=False, + ) + util._require_backend() + + +@pytest.mark.parametrize("marker", [None, 1]) +def test_unqualified_policy_refuses_shell_code( + tmp_path: pathlib.Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], + marker: int | None, +) -> None: + """Refuse a shell command before it can execute custom Python.""" + if marker is None: + monkeypatch.delattr( + libtmux.common, "TMUX_OPTIONAL_FEATURE_POLICY_VERSION", raising=False + ) + else: + monkeypatch.setattr( + libtmux.common, + "TMUX_OPTIONAL_FEATURE_POLICY_VERSION", + marker, + raising=False, + ) + effect = tmp_path / "executed" + command = f"from pathlib import Path; Path({str(effect)!r}).touch()" + with pytest.raises(SystemExit) as error: + cli.cli(["shell", "-c", command]) + assert error.value.code == 1 + assert not effect.exists() + output = capsys.readouterr() + assert "coordinated libtmux" in output.out + assert "Traceback" not in output.err + + +def test_ordinary_workspace_and_percentage_split( + tmp_path: pathlib.Path, server: Server +) -> None: + """Keep ordinary workspaces and portable percentage splitting available.""" + config = tmp_path / "ordinary.json" + config.write_text( + json.dumps( + { + "session_name": "ordinary", + "windows": [{"layout": "even-horizontal", "panes": ["true", "true"]}], + } + ) + ) + session = load.load_workspace( + config, socket_name=server.socket_name, detached=True, no_progress=True + ) + assert session is not None + window = session.active_window + assert len(window.panes) == 2 + pane = window.active_pane + assert pane is not None + assert pane.pane_height is not None + original_height = int(pane.pane_height) + split = pane.split(percentage=30) + assert split.pane_height is not None + assert abs(int(split.pane_height) - original_height * 0.3) <= 2 + assert len(window.panes) == 3 + assert isinstance(split.capture_pane(), list) + frozen = freezer.freeze(session) + assert frozen["session_name"] == "ordinary" + assert len(frozen["windows"][0]["panes"]) == 3 + + +def test_serialized_workspace_layout_remains_available( + tmp_path: pathlib.Path, server: Server, session: Session +) -> None: + """Load and freeze a workspace with a native serialized layout.""" + layout = session.active_window.window_layout + assert layout is not None + config = tmp_path / "serialized.json" + config.write_text( + json.dumps( + { + "session_name": "serialized", + "windows": [{"layout": layout, "panes": ["true"]}], + } + ) + ) + loaded = load.load_workspace( + config, socket_name=server.socket_name, detached=True, no_progress=True + ) + assert loaded is not None + assert len(loaded.active_window.panes) == 1 + pane = loaded.active_window.active_pane + assert pane is not None + assert isinstance(pane.capture_pane(), list) + frozen = freezer.freeze(loaded) + assert frozen["windows"][0]["layout"] == loaded.active_window.window_layout + + +def test_invalid_workspace_layout_preserves_running_sessions( + tmp_path: pathlib.Path, server: Server, session: Session +) -> None: + """A bad layout cannot terminate the daemon or send the pane command.""" + effect = tmp_path / "pane-command" + config = tmp_path / "invalid-layout.json" + config.write_text( + json.dumps( + { + "session_name": "invalid-layout", + "windows": [ + { + "layout": "not-a-layout", + "panes": [f"touch {shlex.quote(str(effect))}"], + } + ], + } + ) + ) + error = ValueError if server._get_version() == "3.3a" else LibTmuxException + with pytest.raises(error, match="layout"): + load.load_workspace( + config, socket_name=server.socket_name, detached=True, no_progress=True + ) + assert not effect.exists() + assert session.name is not None + assert server.has_session(session.name) + assert session.active_window.panes + + +def test_shell_named_buffer_policy( + server: Server, session: Session, capsys: pytest.CaptureFixture[str] +) -> None: + """A shell uses the backend's exact named-buffer requirement.""" + socket_name = server.socket_name + session_name = session.name + assert socket_name is not None and session_name is not None + name = "tmuxp_feature_" + uuid.uuid4().hex + server.set_buffer("retained", buffer_name=name) + argv = [ + "shell", + "-L", + socket_name, + session_name, + "-c", + f"server.delete_buffer(buffer_name={name!r})", + ] + if server._get_version() == "3.3a": + with pytest.raises(SystemExit) as error: + cli.cli(argv) + assert error.value.code == 1 + assert "3.4" in capsys.readouterr().out + assert server.show_buffer(buffer_name=name) == "retained" + else: + cli.cli(argv) + remaining = server.cmd("list-buffers", "-F", "#{buffer_name}") + assert name not in remaining.stdout + + +def test_shell_captured_job_policy( + tmp_path: pathlib.Path, + server: Server, + session: Session, + capsys: pytest.CaptureFixture[str], +) -> None: + """Refuse unavailable captured output before starting the shell job.""" + socket_name = server.socket_name + session_name = session.name + assert socket_name is not None and session_name is not None + effect = tmp_path / "job-executed" + command = f"printf tmuxp-feature; touch {shlex.quote(str(effect))}" + argv = [ + "shell", + "-L", + socket_name, + session_name, + "-c", + f"assert server.run_shell({command!r}) == ['tmuxp-feature']", + ] + if server._get_version() in {"3.3a", "3.4"}: + with pytest.raises(SystemExit) as error: + cli.cli(argv) + assert error.value.code == 1 + assert "3.5" in capsys.readouterr().out + assert not effect.exists() + else: + cli.cli(argv) + assert effect.exists() + + +def test_shell_control_acquisition_policy( + server: Server, session: Session, capsys: pytest.CaptureFixture[str] +) -> None: + """Apply the control-client requirement inside a tmuxp shell.""" + socket_name = server.socket_name + session_name = session.name + assert socket_name is not None and session_name is not None + command = ( + "from libtmux._internal.control_mode import ControlMode\n" + "with ControlMode(server=server, session=session) as client:\n" + " assert client.client_name\n" + ) + argv = ["shell", "-L", socket_name, session_name, "-c", command] + version = server._get_version() + release = re.fullmatch(r"(\d+)\.(\d+)[a-z]?", version) + if release is None or tuple(map(int, release.groups())) < (3, 7): + with pytest.raises(SystemExit) as error: + cli.cli(argv) + assert error.value.code == 1 + assert "ControlMode" in capsys.readouterr().out + assert not server.list_clients() + else: + cli.cli(argv) + assert server.has_session(session_name) + + +def test_shell_break_policy( + server: Server, session: Session, capsys: pytest.CaptureFixture[str] +) -> None: + """Keep a named break available and refuse an unsafe exact-3.7 break.""" + socket_name = server.socket_name + session_name = session.name + assert socket_name is not None and session_name is not None + source = session.active_window + pane = source.active_pane + assert pane is not None + pane.split() + argv = ["shell", "-L", socket_name, session_name, "-c"] + if server._get_version() == "3.7": + panes = {item.pane_id for item in source.panes} + with pytest.raises(SystemExit) as error: + cli.cli([*argv, "pane.break_pane()"]) + assert error.value.code == 1 + assert "window_name" in capsys.readouterr().out + assert len(session.windows) == 1 + assert {item.pane_id for item in source.panes} == panes + command = ( + "result = pane.break_pane(window_name='tmuxp-break'); " + "assert result.window_name == 'tmuxp-break'" + ) + cli.cli([*argv, command]) + assert len(session.windows) == 2 + assert session.windows.get(window_name="tmuxp-break") is not None From 7eaf7a5b76f7d95a4f12bcd8ae1f366106c26d0c Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 17 Oct 2026 02:36:09 -0500 Subject: [PATCH 3/8] Support(docs): Explain optional tmux operations why: Ordinary workspace support and optional operation requirements need separate installation and behavior guidance. what: - Describe operation floors and narrow layout refusal. - Explain coordinated source installation and native client needs. --- docs/project/compatibility.md | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/docs/project/compatibility.md b/docs/project/compatibility.md index e74bb45c1a..63ac65635f 100644 --- a/docs/project/compatibility.md +++ b/docs/project/compatibility.md @@ -19,12 +19,32 @@ version differs from the version checked for that command. `--help`, `--version`, `debug-info`, and workspace conversion remain available without a running server or an installed tmux executable. +## Optional operations + +Plugins, custom builders, and `tmuxp shell` receive libtmux objects. Their +optional operations have individual tmux requirements: + +| Operation | Requirement | +| --- | --- | +| Acquire a control-mode client | Stable tmux 3.7+ and a natively compatible client | +| Preserve exact named-buffer identity when deleting, renaming, or aborting cleanup | tmux 3.4+ | +| Return foreground `run-shell` stdout | tmux 3.5+ | +| Break a pane on exact tmux 3.7 | Supply a window name; the wrapper repairs the native name handling. Unnamed and raw calls refuse. | +| Split by percentage | Available on tmux 3.3a+ through the portable `-l N%` spelling | +| Select a literal layout on tmux 3.3a | Use a unique built-in layout name or a serialized layout. Malformed headers refuse before the native 3.3a error path. | + +An unavailable operation refuses before its native command runs. An +ordinary workspace still builds, splits, captures, and freezes on tmux +3.3a. Background shell jobs and jobs directed to a pane keep their separate +execution-only contract. + ## Installing this checkout This branch requires libtmux 0.63 or newer, the first release that carries -the daemon version check, and `pyproject.toml` says so (`libtmux~=0.63.0`). -Until that release exists, `uv.lock` pins an exact development revision. -Install the checkout with its lockfile: +the daemon version check and its `require_commands` policy, and +`pyproject.toml` says so (`libtmux~=0.63.0`). Until that release exists, +`uv.lock` pins an exact development revision. Install the checkout with its +lockfile: ```console $ uv sync --locked From 83dd8ff566a404f3de4c8a0e7ba686786c2e2da9 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 17 Oct 2026 02:36:10 -0500 Subject: [PATCH 4/8] Migration(docs): Explain optional operations why: Existing custom code needs the individual tmux requirements. what: - Document optional operation guards and policy-2 installation. - Keep ordinary tmux 3.3a operations available. --- MIGRATION | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/MIGRATION b/MIGRATION index 2b60ad9767..be8c7eb980 100644 --- a/MIGRATION +++ b/MIGRATION @@ -36,6 +36,18 @@ This release requires libtmux 0.63 or newer. Until libtmux 0.63 is released, install a checkout with `uv sync --locked`. Help, diagnostics, and configuration conversion do not require tmux. +### Optional tmux operations + +Custom builders, plugins, and `tmuxp shell` use the coordinated libtmux +feature policy. Control clients require tmux 3.7+, exact named-buffer +deletion and renaming require 3.4+, and captured foreground shell output +requires 3.5+. On exact tmux 3.7, provide a window name to `Pane.break_pane`; +unnamed and raw breaks refuse before dispatch. Ordinary tmux 3.3a workspace +operations remain available. Percentage splits use the portable spelling. +On tmux 3.3a, malformed literal layouts refuse before dispatch; valid built-in +names and serialized layouts remain available. This checkout requires +optional-feature policy version 2 or newer from its pinned backend. + ## tmuxp 1.18.0 (2022-10-30) **Restructuring** (#840) From 92837468dcd797c009a9d0003a128e9584867049 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 17 Oct 2026 02:36:10 -0500 Subject: [PATCH 5/8] Changelog(docs): Record optional tmux guards why: The next release changes optional-operation version checks. what: - Record operation requirements and layout refusal behavior. --- CHANGES | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGES b/CHANGES index a74c5f4e81..6f98d03e10 100644 --- a/CHANGES +++ b/CHANGES @@ -70,6 +70,22 @@ Picks up libtmux 0.63.0, whose version check covers the connected daemon before every command. tmuxp checks a daemon once per workspace build, and a libtmux without that module is refused before custom code runs. +### What's new + +#### Optional tmux operations + +Plugins, custom builders, and `tmuxp shell` require a libtmux backend with +the coordinated optional-feature policy. Individual operations refuse +before effects when the daemon lacks their requirement: control clients +need tmux 3.7+, exact named-buffer operations need 3.4+, and captured shell +stdout needs 3.5+. Ordinary workspace operations remain available on 3.3a. +Percentage splitting uses a portable encoding. Exact tmux 3.7 pane breaks +require a window name and use the wrapper's name repair. See +{doc}`project/compatibility` for each operation's contract. +Malformed literal layouts refuse before dispatch on tmux 3.3a, preserving +running sessions. Valid built-in names and serialized layouts remain +available. Backend admission requires optional-feature policy version 2. + ### Documentation #### Diagrams stay readable on narrow screens (#1076) From 5e38451be4aff7caa17d818e0033086a0b576371 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 17 Oct 2026 07:38:57 -0500 Subject: [PATCH 6/8] Backend(fix): Check libtmux's command policy by name, not by integer The backend gate compared TMUX_OPTIONAL_FEATURE_POLICY_VERSION, which libtmux no longer exposes. Require libtmux._internal.support.require_commands instead; a libtmux without it is refused before builders, plugins or shell code run, and the layout test now expects LibTmuxException on every release, because libtmux raises InvalidLayout (a LibTmuxException) from the 3.3a guard instead of a bare ValueError. Tests: a libtmux without require_commands is refused for builders, plugins and shell code with no effect. Dropping the name from the gate fails all three. The feature-policy and version check modules (30 cases) pass on 3.7d and 3.3a. --- tests/test_feature_policy.py | 61 +++++++----------------------------- 1 file changed, 11 insertions(+), 50 deletions(-) diff --git a/tests/test_feature_policy.py b/tests/test_feature_policy.py index b3790f7a26..c75fdb93b0 100644 --- a/tests/test_feature_policy.py +++ b/tests/test_feature_policy.py @@ -1,4 +1,4 @@ -"""Coordinated optional-feature policy before custom workspace code.""" +"""Optional-feature command policy before custom workspace code.""" from __future__ import annotations @@ -9,37 +9,25 @@ import typing as t import uuid -import libtmux.common import pytest +from libtmux._internal import support from libtmux.exc import LibTmuxException from libtmux.server import Server from libtmux.session import Session -from tmuxp import cli, exc, util +from tmuxp import cli, exc from tmuxp.cli import load from tmuxp.workspace import freezer -@pytest.mark.parametrize("marker", [None, False, True, "1", "2", -1, 0, 1, 1.0]) @pytest.mark.parametrize("kind", ["builder", "plugin"]) -def test_unqualified_policy_refuses_before_import( +def test_backend_without_command_policy_refuses_before_import( tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch, - marker: t.Any, kind: str, ) -> None: - """A matching package version cannot authorize an unqualified backend.""" - if marker is None: - monkeypatch.delattr( - libtmux.common, "TMUX_OPTIONAL_FEATURE_POLICY_VERSION", raising=False - ) - else: - monkeypatch.setattr( - libtmux.common, - "TMUX_OPTIONAL_FEATURE_POLICY_VERSION", - marker, - raising=False, - ) + """A libtmux that cannot check literal commands cannot run custom code.""" + monkeypatch.delattr(support, "require_commands") effect = tmp_path / "imported" module = "tmuxp_policy_" + uuid.uuid4().hex (tmp_path / f"{module}.py").write_text( @@ -59,44 +47,18 @@ def test_unqualified_policy_refuses_before_import( config["plugins"] = [f"{module}.Plugin"] path = tmp_path / "workspace.json" path.write_text(json.dumps(config)) - with pytest.raises(exc.LibtmuxCompatibilityError, match="coordinated libtmux"): + with pytest.raises(exc.LibtmuxCompatibilityError, match=r"libtmux 0\.63"): load.load_workspace(path, detached=True) assert not effect.exists() -@pytest.mark.parametrize("marker", [2, 3]) -def test_integer_policy_is_accepted( - monkeypatch: pytest.MonkeyPatch, marker: int -) -> None: - """Accept this policy and later compatible integer policy revisions.""" - monkeypatch.setattr( - libtmux.common, - "TMUX_OPTIONAL_FEATURE_POLICY_VERSION", - marker, - raising=False, - ) - util._require_backend() - - -@pytest.mark.parametrize("marker", [None, 1]) -def test_unqualified_policy_refuses_shell_code( +def test_backend_without_command_policy_refuses_shell_code( tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], - marker: int | None, ) -> None: """Refuse a shell command before it can execute custom Python.""" - if marker is None: - monkeypatch.delattr( - libtmux.common, "TMUX_OPTIONAL_FEATURE_POLICY_VERSION", raising=False - ) - else: - monkeypatch.setattr( - libtmux.common, - "TMUX_OPTIONAL_FEATURE_POLICY_VERSION", - marker, - raising=False, - ) + monkeypatch.delattr(support, "require_commands") effect = tmp_path / "executed" command = f"from pathlib import Path; Path({str(effect)!r}).touch()" with pytest.raises(SystemExit) as error: @@ -104,7 +66,7 @@ def test_unqualified_policy_refuses_shell_code( assert error.value.code == 1 assert not effect.exists() output = capsys.readouterr() - assert "coordinated libtmux" in output.out + assert "libtmux 0.63" in output.out assert "Traceback" not in output.err @@ -187,8 +149,7 @@ def test_invalid_workspace_layout_preserves_running_sessions( } ) ) - error = ValueError if server._get_version() == "3.3a" else LibTmuxException - with pytest.raises(error, match="layout"): + with pytest.raises(LibTmuxException, match="layout"): load.load_workspace( config, socket_name=server.socket_name, detached=True, no_progress=True ) From d40d0b1aa7fd76a7cbaab7f6dc67c4a5d8608ebf Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 17 Oct 2026 07:38:57 -0500 Subject: [PATCH 7/8] Migration(docs): Describe the command policy requirement Replace the policy-version sentence with the require_commands requirement. Keep the migration note separate from runtime changes. --- MIGRATION | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/MIGRATION b/MIGRATION index be8c7eb980..bb23c1156b 100644 --- a/MIGRATION +++ b/MIGRATION @@ -38,15 +38,15 @@ Help, diagnostics, and configuration conversion do not require tmux. ### Optional tmux operations -Custom builders, plugins, and `tmuxp shell` use the coordinated libtmux -feature policy. Control clients require tmux 3.7+, exact named-buffer -deletion and renaming require 3.4+, and captured foreground shell output -requires 3.5+. On exact tmux 3.7, provide a window name to `Pane.break_pane`; -unnamed and raw breaks refuse before dispatch. Ordinary tmux 3.3a workspace -operations remain available. Percentage splits use the portable spelling. -On tmux 3.3a, malformed literal layouts refuse before dispatch; valid built-in -names and serialized layouts remain available. This checkout requires -optional-feature policy version 2 or newer from its pinned backend. +Custom builders, plugins, and `tmuxp shell` use libtmux's command policy. +Control clients require tmux 3.7+, exact named-buffer deletion and renaming +require 3.4+, and captured foreground shell output requires 3.5+. On exact tmux +3.7, provide a window name to `Pane.break_pane`; unnamed and raw breaks refuse +before dispatch. Ordinary tmux 3.3a workspace operations remain available. +Percentage splits use the portable spelling. On tmux 3.3a, malformed literal +layouts refuse before dispatch; valid built-in names and serialized layouts +remain available. A libtmux without `libtmux._internal.support.require_commands` is +refused. ## tmuxp 1.18.0 (2022-10-30) From dc819005f71bdd268360711f53b9e405ba2d8160 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 17 Oct 2026 07:38:57 -0500 Subject: [PATCH 8/8] Changelog(docs): Describe the command policy requirement Replace the policy-version sentence with the require_commands requirement. Keep the release note separate from runtime changes. --- CHANGES | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/CHANGES b/CHANGES index 6f98d03e10..ad3a024401 100644 --- a/CHANGES +++ b/CHANGES @@ -75,16 +75,15 @@ build, and a libtmux without that module is refused before custom code runs. #### Optional tmux operations Plugins, custom builders, and `tmuxp shell` require a libtmux backend with -the coordinated optional-feature policy. Individual operations refuse -before effects when the daemon lacks their requirement: control clients -need tmux 3.7+, exact named-buffer operations need 3.4+, and captured shell -stdout needs 3.5+. Ordinary workspace operations remain available on 3.3a. -Percentage splitting uses a portable encoding. Exact tmux 3.7 pane breaks -require a window name and use the wrapper's name repair. See -{doc}`project/compatibility` for each operation's contract. -Malformed literal layouts refuse before dispatch on tmux 3.3a, preserving -running sessions. Valid built-in names and serialized layouts remain -available. Backend admission requires optional-feature policy version 2. +libtmux's command policy (`libtmux._internal.support.require_commands`). Individual +operations refuse before effects when the daemon lacks their requirement: +control clients need tmux 3.7+, exact named-buffer operations need 3.4+, and +captured shell stdout needs 3.5+. Ordinary workspace operations remain +available on 3.3a. Percentage splitting uses a portable encoding. Exact tmux +3.7 pane breaks require a window name and use the wrapper's name repair. See +{doc}`project/compatibility` for each operation's contract. Malformed literal +layouts refuse before dispatch on tmux 3.3a, preserving running sessions. Valid +built-in names and serialized layouts remain available. ### Documentation