Phase zero of the capability model: close the format sinks, bound the matcher, audit the hints - #128
Open
tony wants to merge 48 commits into
Open
Phase zero of the capability model: close the format sinks, bound the matcher, audit the hints#128tony wants to merge 48 commits into
tony wants to merge 48 commits into
Conversation
why: tmux expands `-c` as a format before using it as a working
directory (spawn.c), so `#(...)` in `start_directory` ran a shell
job, and a directory whose real name held a `#` silently landed the
pane in `$HOME` instead. All four spawn tools carried the sink while
advertising `destructiveHint: false` and `openWorldHint: false`.
what:
- Resolve `start_directory` to an existing directory before tmux
sees it, then escape the result for tmux's format pass
- Refuse `#[`, which tmux hands to the style parser with no escaped
form, rather than silently redirecting the pane
- Fail on a path that does not exist instead of falling back to the
client's directory
- Cover `create_session`, `create_window`, `split_window` and
`respawn_pane`, including directories named `#(id)` and `#{x}`
- State the parameter's contract where it previously restated its
own name
why: A tool behaviour change needs a changelog entry. what: - Record that `start_directory` is resolved and escaped, that `#` directory names now work, and that `#[` is refused
why: `search_panes` compiled a caller's pattern with the stdlib `re`,
which has no execution ceiling. A pattern of eleven characters against
an ordinary pane line hung the server indefinitely — `(a|a)+$` over 60
`a` characters never returned — and the tool is `readonly`, so the
hang was reachable at the most restrictive safety tier.
what:
- Compile the caller's pattern with `regex`, which accepts a
per-search wall-clock budget
- Share one 2 s deadline across every line of every captured pane, so
the ceiling covers the call rather than each line
- Report exhaustion as a correctable error naming the cause
- Keep the tmux-side `#{C:...}` fast path untouched; it never runs a
Python match
A literal scan of 20,000 lines costs ~3.5 ms, so 2 s is a ceiling a
real search never approaches.
why: A tool behaviour change needs a changelog entry. what: - Record the `regex` engine, the two-second call deadline, and that the tmux-side fast path is untouched
why: MCP defines `destructiveHint: false` as a positive claim that a tool performs only additive updates. Six tools that hand a caller's payload to a program advertised it: `send_keys`, `send_keys_batch`, `run_command`, `paste_text`, `paste_buffer`, and `pipe_pane`. Typed input can overwrite a file or end a process, so the claim was false to every connected client. what: - Advertise `destructiveHint: true` on the six input-delivering tools - Move `load_buffer` off that preset: it allocates a fresh buffer and delivers nothing, so it is additive and closed-world, which is what the module docstring already said while the code did otherwise - Assert the claim per tool against the registered surface, and drop the five tests that asserted the presets' contents instead — one of them pinned `destructiveHint: false` in place - Require every tool to advertise all four hints, so a client never falls back on a protocol default
why: The four spawn tools advertised `openWorldHint: false`, but a new pane runs a process with the user's full authority. `split_window` also claimed `destructiveHint: false` while accepting a `shell` command that replaces what the pane would otherwise have run. what: - Advertise `openWorldHint: true` on `create_session`, `create_window`, `split_window` and `respawn_pane` - Move `split_window` and `respawn_pane` onto the payload-carrying hints; both take an authored command - Add a preset for the two spawns that carry no command payload, so `create_session` and `create_window` keep their additive claim - Fold the single-use mutating-destructive preset into the destructive one; the tier tag at the registration site already carried the distinction the name existed to document - Retire the pane-scoped hint tests the surface-wide invariants now cover, including a docstring narrating an earlier preset refactor
why: MCP reserves `destructiveHint: false` for tools that perform only additive updates. Renames, resizes, selections, moves, layouts, titles, options, and environment writes all replace a value tmux already held, and `swap_pane` and `delete_buffer` were additionally mis-preset — a swap exchanges two panes, a delete removes a buffer. what: - Advertise `destructiveHint: true` on the replacement tools, and move `swap_pane`, `enter_copy_mode` and `delete_buffer` onto the removal hints - Give `signal_channel` and `wait_for_channel` an additive preset: a `wait-for` channel latches, replacing nothing - Rename the create preset to say what its one remaining user does, now that the session and window spawns have their own - Assert the claim as a closed set: only five named tools may advertise additive-only updates, so a new tool cannot join them without saying so The coarser hint is accepted, not worked around. A client that gates on `destructiveHint` now prompts for a rename; the signal that separates a rename from a shell command is the tool name and description.
why: The tools that return terminal content advertised `openWorldHint: false`, which tells a client the result came from a closed domain. A pane holds whatever was printed into it — an SSH session, a package manager, another agent — so the text crossed a trust boundary before this server read it. Being read-only says nothing about where the bytes came from. what: - Advertise `openWorldHint: true` on `capture_pane`, `capture_since`, `snapshot_pane`, `search_panes`, `wait_for_text` and `show_buffer` - Carry the same hint on `call_readonly_tools_batch`, which can invoke any of them under its own name - Leave structural reads closed: listings, info, options, hooks and the tmux environment report tmux's own state
why: The advertised surface changed for most tools. what: - Record the six annotation groups, the per-tool tests that replaced the preset assertions, and the added prompting a client that gates on `destructiveHint` will see
why: The hint table was hand-maintained, listed 29 of 56 tools, had no `openWorldHint` column, and went stale the moment the annotations were corrected. Nothing checked it. A paragraph also named a preset that no longer exists. what: - List every tool with all four hints, generated from the registered surface - Assert the table against a freshly registered server, so a hint change that skips the docs fails the suite - Say what `destructiveHint: false` and `openWorldHint: true` claim, and that hints are presentation, not enforcement - Register into a fresh server rather than the production one, whose tier filter is fixed at import and would make the check depend on test ordering
why: The registration list named three of the four MCP hints, and the suite now requires every tool to advertise all four. what: - List `openWorldHint` alongside the other three
why: `pipe_pane` already carried a correct tmux-format escaper, measured against the expander's actual rule: a `#`-run followed by `[` is a style sequence that tmux copies through verbatim, so doubling it corrupts the value. Resolving `start_directory` added a second escaper under the same name that doubled every `#` and refused `#[` because doubling broke it. The refusal was a workaround for the wrong rule. what: - Move the run-aware escape into `_utils` as the one implementation, and have `pipe_pane` call it - Leave `pipe_pane` only the half that is its own: `%` doubling for the `strftime` pass `format_expand_time` adds, which `-c` does not get - Drop the `#[` refusal; a directory named `style#[x]` or `run##[x]` now reaches the pane intact - Cover both forms in the round-trip matrix, which goes red under the naive rule
why: The entry said `#[` is refused. The run-aware escape reproduces it. what: - Say the escape is the rule `pipe_pane` already used, and that `#[` paths work
why: `start_directory` was one of eight arguments tmux runs through `format_single`. The rest were missed. `create_session`'s `session_name` and `window_name`, and `create_window`'s `window_name`, execute a `#(...)` job — reproduced on tmux 3.7d at the default safety tier. `rename_session`, `rename_window` and `set_pane_title` corrupt an ordinary name instead: `w#Sx` became `wplain-namex` and a pane titled `title #H here` became `title d here`. what: - Escape the caller's text on `create_session`, `create_window`, `rename_session`, `rename_window` and `set_pane_title` - Escape the option name on `set_option` and `show_option` together; tmux expands it on both, so escaping one alone would stop a write and its read agreeing - Cover every expanding argument in one file, named for the defect rather than for the spawn tools it started with - Correct the fallback the resolver's docstring described: a cwd that cannot be entered falls back to `$HOME`, then `/`, not the client's directory - Give `search_panes` the `Raises` section its deadline needs, since the docstring is what a calling model reads Ruled out by measurement, not assumption: `-e` environment values and `set-option`'s value expand only under `-F`, which is never passed.
why: The entry named `start_directory` alone. Eight arguments reached tmux's format expander, and the deadline entry overstated its scope. what: - Name every escaped argument and both failure modes: a `#(...)` value ran, an ordinary name was rewritten - Say the search deadline covers matching across the call, not capture - Rewrap a line past the 80-column limit
why: The generated schema block picked up the new docstrings, but the hand-written prose an agent reads first said nothing about either new failure mode. what: - Add a gotcha covering the whole class: names are stored literally, option values are not - Say on the four spawn pages that `start_directory` must exist - Say on the search page that matching shares a two-second ceiling and which patterns exhaust it
why: The annotation invariants read the production server, whose tier filter is fixed at `LIBTMUX_SAFETY` at import. Under `LIBTMUX_SAFETY=readonly` 13 of them failed on missing keys rather than on any annotation defect. The docs table gate had already solved this three commits earlier; the fix was not carried across. what: - Register into a fresh server, as `test_topic_contracts` does - Assert `idempotentHint` on the spawn tools, which nothing named: the retired per-tool test had covered `respawn_pane`, and the closed-set invariant checks only `destructiveHint`
why: `signal_channel` and `wait_for_channel` advertised additive, idempotent behaviour. `tmux wait-for` is a consuming latch, measured on 3.7d: after `wait-for -S ch`, the first wait returns and the second blocks, and signalling twice removes the channel so a later wait blocks too. Repeating either call changes what a subsequent wait does, which is neither additive nor idempotent. what: - Advertise both with the removal hints - Narrow the closed additive set to the three tools that earn it, and drop the preset that now has no users - Regenerate the safety table, which the docs gate caught
why: The spawn preset was inserted directly above the payload preset's `#:` block, so Sphinx read the whole run as one docstring on the spawn preset — opening with a rationale about payloads it does not take — and the payload preset rendered with none. what: - Move the payload preset's comment down to sit above it
why: `display_message` refused a literal `#(` in the caller's text. That
cannot see what tmux expands next: `#{E:x}` runs x's *value* through the
expander again, and `format_cb_current_path` returns a pane's working
directory unsanitized, so any process in any pane can put `#(cmd)` where
the caller never typed one. tmux neuters `#(` for names taken from pane
output (`clean_name` with untrusted set) but not for a path.
`display-message` does not set `FORMAT_NOJOBS`, so the expander reaches
`format_job_get` on that path.
what:
- Accept literal text and `#{variable}` references, and refuse anything
else; the name grammar has no `:`, which every modifier needs, so a
second expansion cannot be requested
- Advertise `openWorldHint: true`: returned values can carry text a pane
chose, such as its working directory or running command
- Say both in the docstring, since that is what a calling model reads
Validating what is allowed replaces scanning for what is not, so a
format construct nobody has thought of yet is refused by default.
why: Two more tool behaviour changes need entries. what: - Record that `display_message` validates by grammar and why a blocklist could not see a second expansion - Record that the channel tools are not idempotent
why: The page still offered "any `#{format}` string", which the grammar
made false — the one place this branch moved a description away from
accuracy instead of toward it. It also left callers with nowhere to go
for the modifiers and conditionals that were removed.
what:
- Promise variables, not formats, in the heading and the "Use when"
- Route raw format syntax to `run_command`, where the caller supplies
it on the surface labelled execution
- Say the same in the docstring, which is what a calling model reads
why: The strict-existence rule was a clause inside the escaping entry. It is a behaviour change on its own — a spawn that used to appear to succeed now fails — and reads as a regression unless the entry says what it buys. what: - Give `start_directory`'s new failure its own paragraph, and say that tmux previously started the pane in `$HOME` and reported success
why: I ruled out `set-option`'s value as a format sink because tmux expands it only under `-F`. That was true and too narrow: it rules the value out as an *immediate* sink, not as code. Measured on 3.7d — `default-command` set through this tool ran in the next pane spawned, and `status-right` holding a `#(...)` job ran under an attached client and repeated on the status interval. `set_environment` has the same shape: a shell reads `BASH_ENV` and `PROMPT_COMMAND` as code. Both advertised `openWorldHint: false`, which is the class of claim this branch exists to remove. what: - Advertise both open-world, keeping `idempotentHint: true`: the call lands on the same stored state, and it is the state that reaches out - Say in each docstring which values execute and when - Widen the gotcha: setting an option schedules execution, it does not only configure
why: The match deadline starts after compilation, so a pattern's own size was unbounded. The tool also read as if the ceiling covered the call; capturing each pane is a tmux round-trip outside it, and the work still grows with the panes in scope. what: - Cap the pattern length, which is what bounds compilation - Say in the docstring what the deadline covers and what it does not - Record why the shared budget is asserted by construction: the first pane to exhaust it aborts the call under a per-pane budget too, so no test separates them A test asserting the shared budget was written and then removed: it passed with the budget deliberately reset per pane, so it proved nothing.
why: One escaper is not one contract. `pipe_pane` doubles `%` because `pipe-pane` runs its argument through `strftime`; `-c` and the name arguments do not, so the same doubling there would corrupt a literal percent. Nothing held that line, and the function is not idempotent, so a later upstream change could double-escape without a gate noticing. what: - Assert the hash escaper leaves `%` alone, doubles a run once, and compounds when applied twice
why: Two behaviour descriptions changed and one claim was narrowed. what: - Record that `set_option` and `set_environment` advertise open-world, and why their values are still stored verbatim - Say the search ceiling bounds matching, not capture, and that pattern length is capped separately
why: The docs badges resolve a tool's safety tier from three hardcoded tags, so the toolset rename this branch leads to would render every tool read-only. git-pull/gp-sphinx#77 makes that vocabulary configurable. Building the docs against it is how we find out before either side merges. what: - Source the three gp-sphinx docs packages from the `scope-overhaul` branch; the pinned `0.1.0a37` still matches, so no version moves Revert once #77 lands and a release is cut.
why: The docs job runs only on `main`, so nothing on this branch reaches the live site. Publishing from here is how we see the gp-sphinx badge change rendered before either PR merges. what: - Add `scope-overhaul` to the docs workflow's push branches Remove before merging.
why: Sourcing the whole gp-sphinx workspace from git made CI build `gp-furo-theme`, whose backend is `sphinx-vite-builder` and needs pnpm that the runner does not carry. Every matrix leg failed in under 20s. It resolved locally because pnpm is installed here, so the local success was not evidence. what: - Override only `sphinx-autodoc-fastmcp`; its siblings stay on released wheels. The three that still resolve from git as its own workspace sources all build with hatchling.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #128 +/- ##
==========================================
- Coverage 86.98% 86.86% -0.13%
==========================================
Files 46 46
Lines 3834 3844 +10
Branches 577 579 +2
==========================================
+ Hits 3335 3339 +4
- Misses 350 354 +4
- Partials 149 151 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
why: `readonly` / `mutating` / `destructive` read as a permission ladder and was not one. It ranked unlike powers on one axis, so running a shell command and deleting a window differed by degree rather than by kind, and the tiers accumulated upward: the kill tools could not be enabled without also enabling the typing tools. `readonly` was the worst of the three — a capture returns whatever a pane holds, secrets included, so the name promised a safety the tool never had. what: - Group tools into four unordered toolsets by what they do: `inspect`, `manage`, `execute`, `teardown` - Put anything that hands input to a program or stores a value tmux later runs in `execute`, `set_option` and `set_environment` included - Replace the tier gate with set membership, still fail-closed: a tool carrying no toolset is refused - Delete `LIBTMUX_SAFETY` with a startup error naming its replacements; add `LIBTMUX_TOOLSETS`, `LIBTMUX_TOOLS`, `LIBTMUX_EXCLUDE_TOOLS`, and fail startup on an unknown name rather than falling back - Default to `inspect,manage,execute`: this server still reaches whichever tmux server the environment points at, so deletion stays something an operator asks for by name - Rename the annotation presets off tier words onto tmux effects, and `ReadonlyRetryMiddleware` onto the toolset it actually keys on `LIBTMUX_TOOLSETS=inspect,teardown` is now a legal surface.
why: The tests and docs still taught the ladder the code no longer has. A reader who found `readonly` in the glossary or the landing grid would have learned the wrong model, and the docs contract asserted the old table, so the vocabulary could not be half-removed. what: - Rewrite the safety page as a trust page: what the toolsets group, that `inspect` means "does not interpret your input as a command" rather than "safe", and that dropping a toolset is not containment - Retire the mutating and destructive batch pages, and rename the read batch to match the tool - Sweep the glossary, landing grid, configuration, architecture, troubleshooting, logging, prompting, gotchas and demo pages - Point `LIBTMUX_SAFETY`'s entry at the three variables that replaced it, and the section badge map at the toolsets - Rewrite the tests that encoded a ladder: membership instead of a ceiling, and a spawn refused by the read batch instead of a spawn batched through a wrapper that no longer exists
why: The sweep was done by reading, so it missed 39 files — the README inventory, the contributor guide, thirty tool pages saying "Readonly." under Side effects. A word this easy to reintroduce by habit needs a gate, not a careful reviewer. what: - Add a test asserting no source or page names the retired tiers, with the four MCP hint fields excluded because those are the protocol's vocabulary and stay - Exempt only what must name what it replaced: `CHANGES`, `MIGRATION`, the startup error refusing `LIBTMUX_SAFETY`, the test proving it fires, and the docs redirect - Fix everything it found: README, AGENTS, the per-tool side-effect lines, the batch index, the glossary term, the badge demo, and the test names and docstrings that still described a ladder The gate found more than the sweep did, which is the argument for it.
why: The tier vocabulary is gone and two tools with it. what: - Record why the ladder was wrong, the four unordered toolsets, the three variables replacing `LIBTMUX_SAFETY`, and that it now fails startup rather than being ignored - Say the MCP annotation hints are untouched: those are the protocol's fields, and only this project's own tiers are withdrawn - Record the two removed batch wrappers and the trust page - Retarget historical entries at the page that replaced the one they linked
why: Two a17 entry contracts pinned the label the trust page replaced. what: - Assert the reference those entries now carry
why: A rename has to reach every instance, and an operator upgrading needs the old-to-new map in one place rather than assembled from a changelog entry. what: - Add `MIGRATION.md`: the three environment variables, the tool names, which toolset each tool is in, and a surface the ladder could not express - Say why `set_option` and `set_environment` are in `execute`: tmux runs some stored values later - Say what did not change, so nobody withdraws the MCP hints too
why: gp-sphinx no longer ships a default vocabulary, so a project that declares none renders every tool without a toolset badge. That is the right default there — a docs tool should not badge a project's tools with words it never used — and it makes declaring one this project's job. The pin also moves to an exact rev. A branch ref resolves to whatever that branch pointed at when the cache was warmed, which is not a thing a lockfile should depend on. what: - Declare `fastmcp_toolsets` in `docs/conf.py`, in precedence order, with the tooltip and icon each badge carries - Retitle the hand-written tool groups from Inspect/Act/Destroy to the four toolsets, splitting Act into `manage` and `execute` - Pin `sphinx-autodoc-fastmcp` to a rev rather than a branch
why: The badges rendered transparent with inherited link-blue. The stylesheet keyed its colours on the old tag names, so nothing matched once the tags were renamed. gp-sphinx now ships tones and a project maps its tags onto them. what: - Declare a tone per toolset: inspect green, manage blue, execute amber, teardown red - Move the gp-sphinx pin to the commit carrying tones
why: MCP annotations describe the whole call. A target command alias can replace argv, and an after-hook can extend it. Direct operation hints promised more than an inherited tmux server can guarantee. what: - Give each tmux-requesting tool conservative whole-call annotations - Preserve direct semantics in its toolset classification - Prove list_panes can activate a command alias and an after-hook - Pin tmux wait-for signals as a consuming toggle
why: A tmux alias or hook may act before the command reports an error. Retrying an inspect call could repeat that ambient effect. what: - Remove InspectRetryMiddleware and its retry policy - Keep failed production-wire calls to one attempt - Let the client or operator decide whether to retry
why: A process-wide prefix does not prove buffer ownership. Shutdown cleanup could delete another instance's buffer and trigger configured tmux behavior. what: - Remove automatic buffer deletion from lifespan shutdown - Keep cache cleanup local to the MCP process - Prove shutdown issues no tmux commands
why: Tool filtering must govern both discovery and invocation. Unknown names and unclassified tools must fail closed instead of widening the surface. what: - Validate named includes and exclusions at startup - Make FastMCP visibility authoritative on the wire - Keep middleware as a classified defense-in-depth check - Retire misleading safety-tier wording
why: FastMCP generated list_prompts and get_prompt after visibility classified the catalog. Enabled adapters then disappeared, and direct calls returned Unknown tool. what: - Decorate generated prompt adapters after PromptsAsTools runs - Classify them as inspect with pure, closed-world annotations - Prove both adapters stay visible and callable on the production wire
why: tmux 3.6 can briefly report the relaunched shell before the child crosses exec(2). An immediate pane_current_command assertion therefore flaked while the behavior was correct. what: - Poll the refreshed pane for at most one second - Require the requested sleep process after the transient shell exits
why: Tool filtering was described more strongly than the architecture allows. tmux aliases and hooks can extend a call. Status jobs run without one, and a socket selects an endpoint without confining its processes. what: - Assign non-surprise, consent, configuration, and confinement ownership - Document aliases, hooks, resources, status jobs, and server provenance - Rebuild the catalog around the four direct-semantics toolsets - State visibility, stdio, prompt, buffer, retry, and channel contracts - Pin catalog and topic claims with derived documentation tests
why: The earlier gate matched ordinary words and skipped contributor guidance, skills, scripts, and project configuration. Stale tier language could therefore survive a tree-wide rename. what: - Match retired identifiers, keys, headings, and exact taxonomy phrases - Cover active guidance, skills, scripts, code, tests, and config - Update remaining active guidance and fixtures to the toolset model - Keep CHANGES and MIGRATION as the historical vocabulary homes
why: Unreleased notes still described direct effects as whole-call guarantees. They omitted ambient tmux behavior, one-attempt failures, command-free shutdown, and prompt-adapter filtering. what: - Record conservative annotations and their client consequences - Add concrete toolset and batch migration examples - Note retry, buffer, prompt-adapter, and trust-documentation changes - Remove fragile counts and repair links to removed middleware
Render the classifications with their configured icon and tone. Keep only these labels selectable so copied prose retains the names without changing badge behavior elsewhere.
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.
Phase zero of ADR 0001. The organizing principle is the ADR's own: this server's job is telling the truth about what it can do. Every change either removes a standing misstatement to connected clients or makes a true claim cheaper to keep true.
Twelve argument sites reached tmux's format expander
tmux expands several argument values as formats, where
#(cmd)runs a shell job and#Hbecomes the hostname. Twelve caller-supplied argument sites across nine tools reached that expander unescaped, at the default safety tier, on every supported tmux from 3.2a.Four were runtime-reproduced as command execution on 3.7d:
start_directory,session_name, andwindow_name. The rest reproduced as corruption —rename_windowturnedw#Sxintowplain-namex, andset_pane_titleturned#Hinto the hostname — with job dispatch source-reachable on the same code path but not observed.Escaping for fidelity makes execution impossible as a corollary, so no
#(pattern-match appears anywhere in the fix. The rule is the run-aware onepipe_panealready shipped: a#-run followed by[is a style sequence tmux copies through verbatim, so doubling it corrupts the value.At the
mutatingtiersend_keysalready runs anything the pane's shell accepts, so this grants a caller no capability the tier withheld. It is still a contract violation, a confused deputy, and a bypass of per-tool client confirmation.start_directoryadditionally changes behaviour: it is resolved to an existing directory, and a path that does not resolve is now an error rather than a silent landing in$HOME.search_panescould hang the server indefinitelyCaller patterns compiled with the standard library's
re, which has no execution ceiling.(a|a)+$against a pane line of 60acharacters never returned, and the tool isreadonly, so the hang was reachable at the most restrictive tier.Matching now runs on
regexunder a single two-second deadline shared across the call, with a separate pattern-length cap because compilation happens before the deadline applies. A literal scan of 20,000 lines costs about 3.5 ms, so the ceiling is not a budget a real search spends. It bounds matching, not capture.display_messageblocked a literal#(That check cannot see what tmux expands next.
#{E:x}re-expandsx's value, andformat_cb_current_pathreturns a pane's working directory unsanitized, so any process in any pane can put a job where the caller never typed one. Runtime-reproduced under a real pty.It now accepts literal text and
#{variable}references only. The name grammar has no:, which every tmux format modifier needs, so a second expansion cannot be requested. Raw format syntax routes torun_command.set_optionandset_environmentschedule executionBoth advertised a closed world.
default-commandset through the tool ran in the next pane spawned, andstatus-rightholding a#(...)job ran under an attached client and repeated on the status interval. Values are still stored verbatim, because a status format is supposed to keep its#{...}; what changed is the description.Every tool's hints audited per tool
MCP defines
destructiveHint: falseas a positive claim of additive-only updates. Shared presets applied that claim to tools that do not behave alike. The surface now divides into six groups, and only three named tools may advertise additive-only updates, enforced as a closed set.Channel waits are part of that:
tmux wait-foris a consuming latch, so neithersignal_channelnorwait_for_channelis idempotent.Verification
uv run ruff check .,uv run ruff format . --check,uv run mypy .(69 source files),uv run pytest -n 5 --reruns 0(939 passed, 6 skipped), andjust build-docsare all clean across repeated runs.Every gate was shown red before being trusted. One test that passed with its bug deliberately reinstated was deleted rather than shipped, and the code records why that property is asserted by construction instead.
Three independent review rounds found four defects before merge; each became a permanent test.
CHANGEScarries the user-facing detail.Note for review
Two commits are labelled
[DO NOT MERGE]and come off before merge. One sourcessphinx-autodoc-fastmcpfrom git-pull/gp-sphinx#77, which makes the docs badge vocabulary configurable; one addsscope-overhaulto the docs workflow's push branches so the result could be seen on the live site.