Skip to content

mcp(refactor[instructions]): slim base instructions to a 117-word visibility-aware card#29

Open
tony wants to merge 10 commits intomainfrom
simplify-instructions
Open

mcp(refactor[instructions]): slim base instructions to a 117-word visibility-aware card#29
tony wants to merge 10 commits intomainfrom
simplify-instructions

Conversation

@tony
Copy link
Copy Markdown
Member

@tony tony commented Apr 26, 2026

Summary

Slim _BASE_INSTRUCTIONS from a 305-word, 6-segment monolith to a
117-word "three handles" card (62% reduction) by pushing per-tool
rules down into tool descriptions where agents read them on every
list_tools call, and making the card responsive to mcp.enable
visibility so it never names a tool the agent cannot call.

The change is split into four landed commits — Phase 1 (additive,
prerequisite), Phase 2 (additive, independent), Phase 3 (irreversible
cut), Phase 4 (additive feature on top of the cut). Phase 5
(LIBTMUX_TOOLSETS env var) is deferred per plan guidance until
there is concrete demand for narrower tool surfaces.

Phase Commit Type What it does
1 393cef4 refactor (additive) Promote rules from invisible module docstrings / global card into tool descriptions
2 5ef52c9 feat Add tmux://reference/format-strings markdown resource
3 72abdae refactor (one-way) Collapse 6 _INSTR_* segments into _INSTR_CARD + _INSTR_HANDLES; rewrite contract tests; update prompting.md excerpt
4 883cd80 feat _build_instructions filters handle hints by visible_tool_names; run_server populates it from mcp.list_tools() after mcp.enable

Why

Verification of the prior 6-segment monolith found an asymmetry:

  • METADATA_VS_CONTENT, READ_TOOLS, WAIT_NOT_POLL were
    already duplicated in agent-visible tool descriptions
    (window_tools.py, pane_tools/meta.py, pane_tools/wait.py,
    etc.) — keeping them in the card was redundant.
  • HOOKS_GAP and BUFFERS_GAP rationale lived only in module
    docstrings, which FastMCP does not surface to agents — keeping them
    only in the card meant they vanished from context once the prompt
    rolled out of the conversation history.

So the migration is asymmetric: 3 segments were redundant duplicates to
delete, 2 were rationale to promote into tool docstrings (visible at
every call). The card now only carries cross-cutting orientation:
server identity, the socket_name exception, and pointers to the three
handles (Tools / Resources / Prompts).

Numbers

  • _BASE_INSTRUCTIONS: 305 words → 117 words (62% reduction)
  • Live _build_instructions(mutating) outside tmux: 357 words → 215 words, comfortably inside MCP's 150-300 word recommendation
  • Tests: 410 → 414 (added 13 new contract tests, deleted 6 obsolete substring tests)

Behavioral changes

  • The agent receives a fundamentally different prompt at session start —
    shorter and pointing at tools/resources/prompts instead of inlining
    per-tool rules.
  • Under LIBTMUX_SAFETY=readonly, the card now drops hints for
    hidden mutating tools (send_keys-driven flows like
    wait_for_text) — the card was naming tools the agent could not
    call.
  • capture_pane's registration carries a description= override
    cross-referencing snapshot_pane, wait_for_text,
    wait_for_content_change, and search_panes. Function docstring
    stays focused on parameters for Sphinx readers; the override carries
    agent-facing cross-references without bloating the human docstring.
  • New static resource tmux://reference/format-strings (text/markdown)
    cataloging the format strings agents most commonly encounter via
    display_message.

Tests

  • New parametrized test_tool_description_includes (9 rows) asserts
    each tool is registered AND its description carries the cross-reference
    — catches future renames that would silently drop the rule.
  • New CardContract NamedTuple + test_card_contracts (3 rows)
    pins server identity, socket_name exception, and three-handles
    orientation. must_exclude=("All tools accept",) guards against
    drift back to the pre-refactor lie.
  • New test_card_length_budget enforces a soft 200-word ceiling
    against creeping re-bloat.
  • New test_card_omits_invisible_tools (parametrized) asserts the
    visibility filter actually drops hints for hidden tools, with paired
    sanity check that hints ARE present when the tool IS visible.
  • New test_format_string_reference_* smoke tests for the new
    resource (body, MIME, registered URI).

Test plan

  • CI passes: ruff check, ruff format, mypy --strict, pytest (414 tests, --reruns 0), just build-docs
  • Manual: start the server with LIBTMUX_SAFETY=readonly, connect with an MCP client, verify the global instructions block (a) is under 200 words and (b) does not name send_keys / wait_for_text / mutating tools in its handles section
  • Manual: with LIBTMUX_SAFETY=mutating (default), verify all three hint phrases (snapshot_pane, wait_for_text, search_panes) ARE present
  • Manual: pull tmux://reference/format-strings from an MCP client and confirm it renders as markdown with the expected headings

tony added 4 commits April 26, 2026 17:57
…ions

Follow-up to 6432646 which split ``_BASE_INSTRUCTIONS`` into named
gap-explainer / positive-guidance segments and named the "prefer the
tool description" decision. Phase 1 of the slim-down acts on it:
per-tool rules move from the global card (or invisible module
docstrings) into tool descriptions an agent sees on every
``list_tools`` call.

* ``show_hooks``: docstring now carries the no-set_hook rationale
  (write-hooks survive process death, so they belong in the tmux
  config file, not a transient MCP session). Previously only in the
  ``hook_tools`` module docstring — FastMCP doesn't surface those.
* ``load_buffer``: docstring carries the no-list_buffers /
  clipboard-privacy rationale. Same module-docstring-only problem.
* ``capture_pane``: registered with a ``description=`` override
  pointing at ``snapshot_pane``, ``wait_for_text``, and
  ``search_panes``. The function docstring stays focused on
  parameters for Sphinx; the override carries the agent-facing
  cross-references without bloating the human docstring.
* ``send_keys``: explicit anti-poll guidance naming ``wait_for_text``
  as the server-side blocking primitive.
* ``list_panes`` / ``list_windows``: sharpened metadata-vs-content
  phrasing with the user-trigger language ("panes that contain X").

New parametrized ``test_tool_description_includes`` asserts each tool
is registered AND its description carries the cross-reference, so a
future rename that drops the rule fails loudly instead of silently.

Pure addition — ``_BASE_INSTRUCTIONS`` is unchanged. The redundant
card-level segments come out in a later phase once the call-site
copies have shipped.
Static markdown reference resource cataloging the tmux format strings
agents most commonly encounter via ``display_message`` and friends:
pane / window / session / server fields, plus the conditional and
string-operation forms (``#{?cond,then,else}``, ``#{=N:expr}``,
``#{s/from/to/:expr}``, etc.).

Why a resource and not a tool: tmux format strings are a closed
catalog, not a query. Agents that hit a ``#{...}`` field they don't
recognize need a fixed lookup, not a server round-trip. Pulling the
reference is cheaper than guessing a name and burning a
``display_message`` call to confirm it.

The new module follows the ``hierarchy.register(mcp)`` shape: closures
decorated with ``@mcp.resource(...)`` and a trailing ``_ = (fn,)``
tuple to silence type-checker unused-name warnings without exporting
the local name. Concrete URI (no template params), so it registers
under ``mcp.list_resources()`` rather than ``list_resource_templates()``.

Tests cover both layers: a closure-capture fixture verifies the body
contains the spot-check format strings (``#{pane_id}``, ``#{window_id}``,
``#{session_id}``, ``#{?cond,then,else}``), and a real-FastMCP test
verifies the registered resource advertises ``text/markdown`` so
clients render it correctly.
… card

Phase 3 of the BASE_INSTRUCTIONS slim-down: delete the six
gap-explainer / positive-guidance segments now that their content
lives in tool descriptions (Phase 1). The card shrinks from a
305-word, 6-paragraph monolith to a 117-word "three handles"
overview that answers (1) what is this server and (2) where do I
look for the rest.

Server.py changes
-----------------
* Delete ``_INSTR_HIERARCHY``, ``_INSTR_METADATA_VS_CONTENT``,
  ``_INSTR_READ_TOOLS``, ``_INSTR_WAIT_NOT_POLL``, ``_INSTR_HOOKS_GAP``,
  ``_INSTR_BUFFERS_GAP``.
* Add ``_INSTR_CARD`` (server identity, hierarchy, socket_name
  exception) and ``_INSTR_HANDLES`` (Tools / Resources / Prompts).
* Rewrite the module-level comment to name the cross-cutting vs.
  tool-specific boundary explicitly, pointing future contributors at
  tool descriptions for any rule that names a specific tool.

Live ``_build_instructions(mutating)`` output drops from 357 words to
215 — comfortably inside MCP's 150-300 word recommendation for global
instructions.

Tests
-----
* Delete the six standalone ``test_base_instructions_*`` functions —
  every substring they pinned has either moved into a tool description
  (covered by Phase 1's ``test_tool_description_includes``) or been
  deliberately removed from the card.
* Add ``CardContract`` ``NamedTuple`` + parametrized
  ``test_card_contracts`` with three rows: ``server_identity``,
  ``socket_name_exception`` (with ``must_exclude=("All tools accept",)``
  guarding against drift back to the pre-refactor lie), and
  ``three_handles``.
* Add ``test_card_length_budget`` enforcing a soft 200-word ceiling so
  the card cannot regress into the monolith it just shrank from.
* The ``BuildInstructionsFixture`` matrix is unchanged — its
  assertions cover the dynamic safety / agent-context blocks of
  ``_build_instructions``, not ``_BASE_INSTRUCTIONS``, and those still
  build identically.

Docs
----
``docs/topics/prompting.md`` previously excerpted only the first two
old segments; updated to mirror the new slim card so readers see what
agents actually receive.
Phase 4 of the BASE_INSTRUCTIONS slim-down. The card names specific
tools by hint phrase (``snapshot_pane over capture_pane + get_pane_info``,
``wait_for_text over capture_pane in a retry loop``,
``search_panes over list_panes...``); under
``LIBTMUX_SAFETY=readonly`` some of those tools are hidden by
``mcp.enable(tags=..., only=True)``, so naming them in the card was
misleading.

Composition
-----------
* New ``_HANDLE_HINTS: tuple[tuple[str, str], ...]`` keyed by tool name.
* New ``_format_handles_section(visible_tool_names)`` renders the
  three-bullet handles list, filtering the Tools-handle hints by
  visibility. ``visible_tool_names=None`` keeps every hint
  (backward-compat for tests that build instructions without invoking
  ``mcp.enable``, and for the module-import-time placeholder).
* ``_INSTR_HANDLES`` is now ``_format_handles_section(None)`` — the
  unfiltered baseline used by ``_BASE_INSTRUCTIONS``.

Wiring
------
* ``_build_instructions`` gains a ``visible_tool_names: set[str] | None``
  parameter. When provided it rebuilds the base from ``_INSTR_CARD`` +
  filtered handles; otherwise it uses ``_BASE_INSTRUCTIONS`` directly.
  Env-driven ``is_caller`` block is unchanged.
* ``run_server`` now collects visible tool names via
  ``asyncio.run(mcp.list_tools())`` after ``mcp.enable(tags=..., only=True)``
  has applied the safety-tier filter, then overwrites ``mcp.instructions``
  with the visibility-filtered card. The module-import-time
  ``instructions=`` set on the FastMCP constructor is now an explicitly
  documented placeholder.

Tests
-----
* ``test_card_omits_invisible_tools`` (parametrized) — drops one tool
  from the visible set and asserts its hint phrase disappears, with a
  paired sanity check that the phrase IS present when the tool is
  visible.
* ``test_build_instructions_default_visible_tool_names_emits_full_card``
  — pins the backward-compat behavior so a future contributor doesn't
  silently break the placeholder by changing the default.

End-to-end smoke: simulating ``LIBTMUX_SAFETY=readonly`` (wait_for_text
hidden), the card drops the wait_for_text hint while keeping
snapshot_pane and search_panes. Default placeholder still emits all
three at 215 total words.
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 26, 2026

Codecov Report

❌ Patch coverage is 90.62500% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.83%. Comparing base (6534251) to head (2b985dd).

Files with missing lines Patch % Lines
src/libtmux_mcp/server.py 85.71% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #29      +/-   ##
==========================================
+ Coverage   83.62%   83.83%   +0.20%     
==========================================
  Files          39       40       +1     
  Lines        2113     2134      +21     
  Branches      270      273       +3     
==========================================
+ Hits         1767     1789      +22     
  Misses        266      266              
+ Partials       80       79       -1     

☔ View full report in Codecov by Sentry.
📢 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 April 26, 2026 20:18
``_format_handles_section`` filters card hints by ``tool in
visible_tool_names`` — a tool name in ``_HANDLE_HINTS`` that no longer
matches a registered tool produces the worst of both worlds: agents
see a phantom tool name during the brief import-time placeholder
window (where ``visible_tool_names is None`` and every hint is
emitted unconditionally), then lose the guidance entirely once
``run_server`` filters by real visibility.

Phase 1's ``test_tool_description_includes`` already catches drift in
tool descriptions. This test catches drift in the parallel
``_HANDLE_HINTS`` table — the two together close the rename-without-
update loophole.

Pure addition; no source changes.
``_format_handles_section`` has two output branches: with hints
(``"... prefer (e.g. ...)."``) and without (``"... prefer."``). The
no-hints branch fires when ``visible_tool_names`` is non-None but no
hint tool is in it.

With every current hint tool tagged ``TAG_READONLY`` this branch is
unreachable by the existing tool set — even ``LIBTMUX_SAFETY=readonly``
keeps ``snapshot_pane`` / ``wait_for_text`` / ``search_panes`` visible.
Codecov flagged it as uncovered, and the upcoming Phase 5
(``LIBTMUX_TOOLSETS``) makes it reachable: a user setting
``LIBTMUX_TOOLSETS=server,session`` would hide every pane tool, leaving
``visible_tool_names`` disjoint from every ``_HANDLE_HINTS`` key. Pin
the branch's output now so a regression that emits broken syntax
(trailing ", ).", duplicate periods, etc.) fails loudly.

Pure addition; no source changes. Pulls the empty-set branch from 0%
to 100% coverage.
``test_registered_tools_accept_socket_name`` constructs a fresh
``FastMCP("socket-name-contract")`` and asserts every registered tool
(except ``SOCKET_NAME_EXEMPT``) takes a ``socket_name`` parameter. This
exercises the unfiltered registered set, not the production singleton
at ``server.py:222`` (which has middleware + tier filtering applied
by ``run_server``).

The behavior is correct: any subset of an all-pass set is also
all-pass for the same property, so verifying on the unfiltered set
covers every tier-filtered subset by implication. Adding the scope
note to the docstring prevents a future maintainer from misreading
the test as covering the singleton — and flags that tier-specific
socket_name behavior, if ever added, would need a parallel test.

Documentation only; no test logic changes.
When ``capture_pane`` truncates, the returned string is prefixed with a
literal ``[... truncated K lines ...]`` first line so the caller can
detect the cap. This is documented in the function docstring at
``pane_tools/io.py:174-178``, but FastMCP only surfaces the
``description=`` override registered at ``pane_tools/__init__.py:83-95``,
not the docstring — and the override only said "tail-preserving
truncation at max_lines, default 500". Agents reading the description
had no signal that the first line of a truncated response is a marker
rather than terminal content, and could mishandle the output.

Extends the override to name the marker and tell the agent it's a
literal prefix line to skip when parsing. Also surfaces the
``max_lines=None`` opt-out.

Test: adds a row to ``test_tool_description_includes`` asserting
``capture_pane``'s description contains ``"truncated"``. The contract
suite already pinned ``snapshot_pane`` / ``wait_for_text`` /
``search_panes`` cross-references; this extends the same drift
guard to the truncation behavior.
…imer

The ``tmux://reference/format-strings`` resource lists ~25 of tmux's
~200 format variables. Without a clear "this is a subset" signal at
the top, an agent that reads the body and doesn't find e.g.
``#{history_size}`` (one of the 175 omitted strings) may erroneously
conclude that string is unsupported and skip a ``display_message``
call that would have worked. The closing ``man tmux`` line existed
but came after the substantive content — too easy to miss in a
top-down skim.

Adds a leading note declaring the resource a curated cheat sheet,
not the complete catalog, and points readers at ``man tmux`` for
anything not listed. Also softens the ``get_format_string_reference``
docstring's "avoids hallucinated format names" claim — the resource
is now honest about being a subset, so the docstring framing
matches.

Test: ``test_format_string_reference_leads_with_subset_disclaimer``
asserts both that the disclaimer keywords are present AND that the
disclaimer precedes the first format-variable example, so a future
edit can't quietly move the disclaimer to the bottom (recreating the
original problem).
…d tools

``test_card_omits_invisible_tools`` proves the visibility filter's
*logic* using a synthetic 3-element ``visible_tool_names`` set. The
production wiring uses ~20+ names returned by
``asyncio.run(mcp.list_tools())`` after ``mcp.enable(tags=..., only=True)``
runs in ``run_server``. The two paths share code but are not the same
exercise.

Adds an integration test that registers the real tool surface, applies
the same ``mcp.enable(tags={TAG_READONLY}, only=True)`` call
``run_server`` makes, collects the visible names from
``mcp.list_tools()``, and asserts every hint phrase still appears in
the card. All three current hint tools (``snapshot_pane``,
``wait_for_text``, ``search_panes``) carry ``TAG_READONLY``, so even
the most restrictive tier keeps them visible — the test pins this
invariant. A future change that moves a hint tool out of the readonly
tier will fail this test and force a deliberate decision: retag the
tool, or drop the hint from ``_HANDLE_HINTS``.

The check is paired: each iteration asserts both that the hint tool is
actually in the visible set (catches an unexpected tier change) and
that the corresponding phrase is in the produced card (catches a
wiring regression).

Pure addition; no source changes.
tony added a commit that referenced this pull request May 3, 2026
why: Verifies the in-flight `sphinx-vite-builder` PEP 517 backend
(git-pull/gp-sphinx#29) end-to-end as a real
downstream consumer. libtmux-mcp consumes `gp-sphinx` (and
transitively `gp-furo-theme`), so installing this branch's deps
exercises the new backend's `build_editable` hook against the actual
vite + pnpm toolchain in CI.

what:
- pyproject.toml:
  - Bump `gp-sphinx` (and matching helper packages) constraint from
    `==0.0.1a13` (the latest PyPI release) to `==0.0.1a15` (the
    workspace-internal version on the gp-sphinx PR branch).
  - Add `[tool.uv.sources]` entries pointing every gp-sphinx
    workspace package at the `sphinx-vite-builder` branch via git
    URL + subdirectory. uv resolves each package directly from the
    PR branch rather than from PyPI.
- uv.lock: locked against commit 1ab7554a on git-pull/gp-sphinx
  (the latest sphinx-vite-builder branch tip at write time).
- .github/workflows/docs.yml:
  - Trigger on push to `sphinx-vite-builder` so the deploy workflow
    runs on this branch (not just main). The S3/CloudFront publish
    steps still gate on the `LIBTMUX_MCP_DOCS_*` secrets being
    populated for the `docs` environment.
  - Add `pnpm/action-setup@v6` (pnpm 10) and `actions/setup-node@v6`
    (Node 22) before `uv sync`. Without pnpm on PATH, the new
    backend fast-fails with `PnpmMissingError` at editable-install
    time — these steps satisfy the toolchain requirement so the
    backend can run vite and populate the gitignored `static/` tree
    that gp-furo-theme advertises in `theme.conf`.

Verified locally:
- `uv lock` resolves the 11 gp-sphinx packages from the branch
- `uv sync --all-extras --dev` succeeds (vite runs during the
  gp-furo-theme editable install via the new backend)
- `just build-docs` produces a clean docs site

Once gp-sphinx#29 merges and v0.0.1a16 (or v0.0.1a15 re-publish)
ships to PyPI, this commit is reverted: the `[tool.uv.sources]`
entries go away, the version pin returns to PyPI, and the docs.yml
trigger drops the branch entry. Track via `git log --grep="DO NOT MERGE"`.

References:
- gp-sphinx PR #29: git-pull/gp-sphinx#29
- gp-sphinx issue #28 (architecture): git-pull/gp-sphinx#28
tony added a commit that referenced this pull request May 3, 2026
…nstall)

why: Verifies the **wheel-install path** of the new
sphinx-vite-builder backend (gp-sphinx PR #29, dev release tagged
v0.0.1a16.dev0 and published to PyPI). End users `pip install`
gp-furo-theme from PyPI; the wheel ships with vite-built `static/`
already baked in by the backend at release time, so the install
path never invokes the backend and never needs pnpm or Node.

This commit is the canonical proof of that property, demonstrated
end-to-end:

- Drop every `[tool.uv.sources]` git URL added in commit 91ad21a;
  uv resolves every gp-sphinx package straight from PyPI now.
- Bump version pins from `==0.0.1a15` to `==0.0.1a16.dev0` (matches
  the dev release).
- Drop pnpm/Node setup steps from `docs.yml` and `tests.yml`.
  Toolchain not needed for wheel-install path; their presence here
  would obscure the proof.
- Refresh `uv.lock` (`uv lock --refresh --prerelease=allow` since
  PEP 440 dev releases need explicit prerelease policy).

Verified locally with `PATH` stripped of pnpm/node/corepack:

  $ PATH=$(echo "$PATH" | tr : '\n' | grep -v -E "(pnpm|node|corepack)" | paste -sd:) which pnpm
  pnpm not found
  $ PATH=... uv sync --all-extras --dev    # succeeds
  $ PATH=... just build-docs                # succeeds; 60K furo-tw.css + 8K furo.js render

Once gp-sphinx v0.0.1a16 stable ships to PyPI (after dev releases
are verified), this commit is replaced by a normal pin bump.

References:
- gp-sphinx PR #29: git-pull/gp-sphinx#29
- gp-sphinx issue #28 (architecture): git-pull/gp-sphinx#28
- v0.0.1a16.dev0 wheel: https://pypi.org/project/gp-furo-theme/0.0.1a16.dev0/
tony added a commit that referenced this pull request May 3, 2026
Final QA permutation — verifies that the wheel-install path's
companion (sdist install) ALSO works without pnpm/Node when the sdist
is downloaded from PyPI.

For gp-furo-theme specifically:
- The sdist excludes `web/` (per `[tool.hatch.build.targets.sdist]
  exclude = ["web/"]` in gp-sphinx PR #29)
- The unpacked sdist therefore has no `web/` directory
- sphinx_vite_builder.build's wheel-from-sdist phase invokes its
  vite-orchestration helper, which short-circuits when `web/` is
  absent (the documented unpacked-sdist case)
- Hatchling then packs the pre-baked `static/` (carried by the sdist
  via `[tool.hatch.build] artifacts`) into the wheel
- Net: sdist install succeeds with only Python on PATH

`[tool.uv] no-binary-package` forces uv to prefer sdist over wheel
for the listed gp-sphinx workspace packages. Verified locally:
toolchain-stripped `uv sync --all-extras --dev` completes cleanly.
This commit pushes the same scenario to the libtmux-mcp CI runners
for end-to-end confirmation against real GitHub-hosted infrastructure.

Reverted alongside the rest of the [DO NOT MERGE] integration
testing once gp-sphinx v0.0.1a16 stable ships.
tony added a commit that referenced this pull request May 3, 2026
why: gp-sphinx PR #29 cut a second dev release (v0.0.1a16.dev1) on
top of dev0, carrying the docs polish + drift-proofing pass. This
re-tests the wheel + sdist install paths against the latest dev
artifacts on PyPI, exercising any incremental regressions before the
v0.0.1a16 stable cut. Same scenario as the dev0 commit: tests +
docs jobs run against PyPI-installed wheels with `[tool.uv]
no-binary-package` forcing the gp-sphinx workspace packages to come
through the sdist→wheel chain (which exercises the
sphinx_vite_builder backend's `web/`-absent short-circuit).

what:
- pyproject.toml: bump every `==0.0.1a16.dev0` pin to
  `==0.0.1a16.dev1` (3 entries each in `[dependency-groups] dev`
  and `[dependency-groups] docs`)
- pyproject.toml: add `sphinx-vite-builder = false` to
  `[tool.uv.exclude-newer-package]`. The new package isn't on the
  workspace's existing exclude-newer-package list (it's brand new
  this release cycle), and its absence caused `uv sync` to refuse
  to resolve it because of the user's global exclude-newer cutoff.
  This `false` exempts it like the other gp-sphinx workspace
  packages.
- uv.lock: refreshed via `uv lock --refresh --prerelease=allow`
  (PEP 440 dev releases need explicit prerelease policy)

Verified locally:
- toolchain-stripped `uv sync --all-extras --dev` succeeds (sdist
  install path + backend short-circuit working as designed)
- `just build-docs` succeeds against the PyPI-installed dev1 theme

References:
- gp-sphinx v0.0.1a16.dev1 wheel:
  https://pypi.org/project/gp-furo-theme/0.0.1a16.dev1/
- gp-sphinx PR #29: git-pull/gp-sphinx#29
tony added a commit that referenced this pull request May 3, 2026
why: gp-sphinx workspace just published v0.0.1a16.dev2 to PyPI
(third dev release of PR #29 — sphinx-vite-builder backend). The
libtmux-mcp QA branch needs to track the latest dev tag so the
next CI cycle exercises the exact wheels the eventual stable will
ship.

what:
- pyproject.toml: gp-sphinx, sphinx-autodoc-api-style,
  sphinx-autodoc-fastmcp pins bumped from ==0.0.1a16.dev1 to
  ==0.0.1a16.dev2 in both [dependency-groups.dev] and
  [project.optional-dependencies.docs]
- uv.lock: refreshed via `uv lock --refresh --prerelease=allow`;
  11 transitive gp-sphinx workspace packages updated to dev2
  (sphinx-vite-builder, sphinx-gp-theme, sphinx-fonts, etc.)

Verified locally:
- uv sync --all-extras --group dev → clean install
- uv run sphinx-build -b html docs docs/_build/html → succeeds
  (49 pre-existing warnings; not introduced by the bump)
- uv run py.test --reruns 0 → 401 passed
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