Skip to content

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
mainfrom
scope-overhaul
Open

Phase zero of the capability model: close the format sinks, bound the matcher, audit the hints#128
tony wants to merge 48 commits into
mainfrom
scope-overhaul

Conversation

@tony

@tony tony commented Aug 29, 2026

Copy link
Copy Markdown
Member

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 #H becomes 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, and window_name. The rest reproduced as corruption — rename_window turned w#Sx into wplain-namex, and set_pane_title turned #H into 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 one pipe_pane already shipped: a #-run followed by [ is a style sequence tmux copies through verbatim, so doubling it corrupts the value.

At the mutating tier send_keys already 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_directory additionally 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_panes could hang the server indefinitely

Caller patterns compiled with the standard library's re, which has no execution ceiling. (a|a)+$ against a pane line of 60 a characters never returned, and the tool is readonly, so the hang was reachable at the most restrictive tier.

Matching now runs on regex under 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_message blocked a literal #(

That check cannot see what tmux expands next. #{E:x} re-expands x's value, and format_cb_current_path returns 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 to run_command.

set_option and set_environment schedule execution

Both advertised a closed world. default-command set through the tool ran in the next pane spawned, and status-right holding 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: false as 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-for is a consuming latch, so neither signal_channel nor wait_for_channel is 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), and just build-docs are 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. CHANGES carries the user-facing detail.

Note for review

Two commits are labelled [DO NOT MERGE] and come off before merge. One sources sphinx-autodoc-fastmcp from git-pull/gp-sphinx#77, which makes the docs badge vocabulary configurable; one adds scope-overhaul to the docs workflow's push branches so the result could be seen on the live site.

tony added 30 commits August 29, 2026 11:53
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-commenter

codecov-commenter commented Aug 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.74172% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.86%. Comparing base (dd16e3c) to head (e4acf4d).

Files with missing lines Patch % Lines
src/libtmux_mcp/server.py 63.88% 7 Missing and 6 partials ⚠️
src/libtmux_mcp/middleware.py 88.23% 1 Missing and 1 partial ⚠️
src/libtmux_mcp/prompts/__init__.py 85.71% 1 Missing and 1 partial ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

tony added 6 commits August 29, 2026 15:32
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
tony added 10 commits August 29, 2026 20:25
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants