Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ee94d6a
Pane(fix[wait_for_text]): anchor on baseline so stale scrollback no l…
tony May 15, 2026
24d732e
Pane(test[wait_for_text]): regress stale-scrollback no-match contract…
tony May 15, 2026
7cb0525
Pane(refactor[wait_for_text]): adopt Pane.display_message wrapper
tony May 15, 2026
ee12d67
Pane(fix[wait_for_text]): guard against bottom-row capture clip
tony May 15, 2026
ad1f61a
Pane(fix[wait_for_text]): include baseline read inside the timeout bu…
tony May 15, 2026
eab8ca8
Pane(fix[wait_for_text]): reject footgun inputs at the door
tony May 15, 2026
ed7b407
Pane(fix[wait_for_text]): surface respawn and pane-death as ToolError
tony May 15, 2026
859ecb6
Pane(docs[wait_for_text]): fix reverse-index claim and surface the sa…
tony May 15, 2026
2a0bef8
docs(CHANGES[wait_for_text]): note the baseline-anchor fix in AGENTS.…
tony May 15, 2026
bc16a27
Pane(docs[wait_for_text]): correct asyncio.gather references that don…
tony May 15, 2026
7ec75bf
Pane(test[wait_for_text]): bump _stale_settled budget to match projec…
tony May 15, 2026
c378e16
Pane(fix[wait_for_text]): raise on history rollover so the baseline a…
tony May 15, 2026
67eed15
Pane(fix[wait_for_text]): join wrapped lines so long-line patterns match
tony May 15, 2026
6bfabc3
Pane(docs[wait_for_text]): cross-link wait_for_channel as the determi…
tony May 15, 2026
692858a
Pane(fix[wait_for_text]): exempt resize-grow from the rollover guard
tony May 16, 2026
8c7a152
Pane(docs[wait_for_text]): walk back rollover-detection contract; the…
tony May 16, 2026
2bb6b80
Pane(fix[wait_for_text]): warn when polling in the history-limit risk…
tony May 16, 2026
28a2fdf
Pane(test[wait_for_text]): cover history-limit trim during continuous…
tony May 16, 2026
2aa7199
Pane(docs[wait_for_text]): describe the shipped wait-for-text contrac…
tony May 16, 2026
62a2cef
Pane(fix[wait_for_text]): warn on entry-in-risk-band and cache histor…
tony May 16, 2026
82cc9cd
Pane(docs[wait_for_text]): point system-prompt fragment and troublesh…
tony May 16, 2026
dc0389b
docs(CHANGES[wait_for_text]): refine unreleased section to AGENTS.md …
tony May 16, 2026
61f42ef
Pane(fix[wait_for_channel]): stop killing the parent shell in run_and…
tony May 16, 2026
5927faa
Pane(fix[wait_for_text]): filter stale below-cursor content captured …
tony May 16, 2026
2b6124f
Pane(docs[wait_for_text]): finish the wait-for-channel re-framing in …
tony May 16, 2026
85c0c97
Pane(docs[wait_for_text]): correct timeout-budget comment to describe…
tony May 16, 2026
aad2821
Pane(docs[wait_for_content_change]): note weaker baseline-loss detect…
tony May 16, 2026
94ec865
Pane(docs[wait_for_channel]): drop dangerous exit-$status pattern fro…
tony May 16, 2026
0ded558
Pane(docs[run_and_wait]): drop stale "status-preserving" claims acros…
tony May 16, 2026
f7d6edf
Pane(refactor[wait]): drop redundant timed_out field from result models
tony May 16, 2026
f79625c
docs(CHANGES[wait]): tighten timed_out subheading to name the deliver…
tony May 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,67 @@
_Notes on upcoming releases will be added here_
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->

### Breaking changes

**{tooliconl}`wait-for-text` waits for new output, not stale scrollback**

{tooliconl}`wait-for-text` now matches lines written *after* the call begins. The previous behaviour returned `found=True` on the first poll whenever the pattern already lived in the pane, so agents synchronising on command output got the wrong result. For the synchronous "is the pattern in the pane right now?" case, call {tooliconl}`search-panes` instead.

Baseline-loss events surface as `ToolError`: pane respawn, pane death, `clear-history`, and any other event that drops history below the entry baseline. Pane resize that pulls lines back from history into the visible region is exempted — the anchor stays valid.

Trim during heavy output near `history-limit` can't be reliably detected from polling alone. When polling approaches that limit, the tool emits a `notifications/message` warning so MCP clients can decide whether to keep waiting, retry, or switch to {tooliconl}`wait-for-channel`. For deterministic command completion, compose `tmux wait-for -S <channel>` into the shell command and call {tooliconl}`wait-for-channel`. (#45)

**{tooliconl}`wait-for-text` drops `content_start` / `content_end`**

The new baseline anchor follows the pane's grid position automatically, so the manual capture-range parameters have no remaining purpose. Drop them from call sites. (#45)

```python
# Before
wait_for_text(pattern="OK", content_start=-100)

# After
wait_for_text(pattern="OK")
```

**Wait result models drop `timed_out`**

{class}`~libtmux_mcp.models.WaitForTextResult` and {class}`~libtmux_mcp.models.ContentChangeResult` drop the `timed_out` field. It was mechanically the boolean negation of the primary result (`not found` / `not changed`) and carried no information beyond that. Callers should switch to `not result.found` / `not result.changed`. (#47)

```python
# Before
result = wait_for_text(pattern="OK")
if result.timed_out:
...

# After
result = wait_for_text(pattern="OK")
if not result.found:
...
```

### Dependencies

**Minimum `libtmux>=0.56.0`** (was `>=0.55.1`). Unlocks the new tmux-command wrappers shipped in libtmux 0.56.0 — {meth}`~libtmux.Pane.respawn`, {meth}`~libtmux.Pane.copy_mode`, {meth}`~libtmux.Pane.pipe`, {meth}`~libtmux.Pane.swap`, {meth}`~libtmux.Pane.paste_buffer`, {meth}`~libtmux.Pane.clear_history`, {meth}`~libtmux.Pane.display_message`, {meth}`~libtmux.Server.delete_buffer`, and the {meth}`~libtmux.Session.next_window` / {meth}`~libtmux.Session.previous_window` / {meth}`~libtmux.Session.last_window` trio — so the MCP no longer falls back to raw `cmd()` calls for tmux commands the upstream API now covers. (#46)
**Minimum `libtmux>=0.56.0`** (was `>=0.55.1`). Picks up libtmux 0.56's typed wrappers for the tmux commands the server invokes — the MCP now uses libtmux's public API instead of raw command-line escapes for pane lifecycle, scrollback, and session navigation. (#46)

### Fixes

**{tooliconl}`wait-for-channel` recipe no longer exits the parent shell**

The `run_and_wait` prompt template previously appended `exit $__mcp_status` to its shell payload to preserve the command's exit status. In an interactive shell that exits the shell itself, destroying single-pane sessions. The recipe now signals completion via `tmux wait-for -S` without exiting, and the equivalent example in {doc}`/tools/pane/wait-for-channel` is similarly fixed. Exit-status preservation in interactive shells is documented as out-of-scope; agents that need it should inspect the captured output for command-specific success markers. (#47)

**{tooliconl}`wait-for-text` matches patterns across visually-wrapped lines**

Long patterns like `"Build failed: module not found"` that tmux wraps at the pane's column width are now matched against the joined logical line. Previously the wrap split the pattern across two captured rows and neither row matched. The joined line is returned in `matched_lines` and can exceed the pane width. (#45)

**{tooliconl}`wait-for-text` rejects misused `pattern` / `interval` / `timeout`**

Empty `pattern`, `interval` below 10 ms, and non-positive `timeout` each raise `ToolError` at entry. Previously they silently matched every line, spun the tmux server in a tight loop, or completed a surprise single probe. (#45)

### Documentation

**Wait family is re-framed around {tooliconl}`wait-for-channel` as the deterministic primitive**

The {tooliconl}`send-keys` docstring, server system instructions, {tooliconl}`wait-for-text` docstring, and the user-facing quickstart, gotchas, prompting, troubleshooting, recipes, and send-keys topics now point agents at {tooliconl}`wait-for-channel` with composed `tmux wait-for -S` for command completion. {tooliconl}`wait-for-text` and {tooliconl}`wait-for-content-change` are reframed as the fallbacks for output the agent does not author. The `run_and_wait` recipe shows the canonical safe-completion pattern. (#45)

## libtmux-mcp 0.1.0a6 (2026-05-09)

Expand Down
6 changes: 3 additions & 3 deletions docs/demo.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,11 @@ These are the actual tool headings as they render on tool pages:

### In prose

Use {tooliconl}`search-panes` to find text across all panes. If you know which pane, use {tooliconl}`capture-pane` instead. After running a command with {tooliconl}`send-keys`, always {tooliconl}`wait-for-text` before capturing.
Use {tooliconl}`search-panes` to find text across all panes. If you know which pane, use {tooliconl}`capture-pane` instead. After running a command with {tooliconl}`send-keys`, compose `tmux wait-for -S` and call {tooliconl}`wait-for-channel` before capturing.

### Dense inline (toolref, no badges)

The fundamental pattern: {toolref}`send-keys` → {toolref}`wait-for-text` → {toolref}`capture-pane`. For discovery: {toolref}`list-sessions` → {toolref}`list-panes` → {toolref}`get-pane-info`.
The fundamental pattern: {toolref}`send-keys` → {toolref}`wait-for-channel` → {toolref}`capture-pane`. For discovery: {toolref}`list-sessions` → {toolref}`list-panes` → {toolref}`get-pane-info`.

## Environment variable references

Expand All @@ -87,7 +87,7 @@ Use {tooliconl}`search-panes` before {tooliconl}`capture-pane` when you don't kn
```

```{warning}
Do not call {toolref}`capture-pane` immediately after {toolref}`send-keys` — there is a race condition. Use {toolref}`wait-for-text` between them.
Do not call {toolref}`capture-pane` immediately after {toolref}`send-keys` — there is a race condition. Compose `tmux wait-for -S` into the command and use {toolref}`wait-for-channel` between them.
```

```{note}
Expand Down
19 changes: 13 additions & 6 deletions docs/prompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,12 @@ channel is signalled — strictly cheaper in agent turns than a

````markdown
Run this shell command in tmux pane %1 and block
until it finishes, preserving the command's exit status:
until it finishes:

```python
send_keys(
pane_id='%1',
keys='pytest; __mcp_status=$?; tmux wait-for -S libtmux_mcp_wait_<uuid>; exit $__mcp_status',
keys='pytest; tmux wait-for -S libtmux_mcp_wait_<uuid>',
)
wait_for_channel(channel='libtmux_mcp_wait_<uuid>', timeout=60.0)
capture_pane(pane_id='%1', max_lines=100)
Expand All @@ -83,12 +83,19 @@ capture_pane(pane_id='%1', max_lines=100)
After the channel signals, read the last ~100 lines to verify the
command's behaviour. Do NOT use a `capture_pane` retry loop —
`wait_for_channel` is strictly cheaper in agent turns.

The payload does not preserve the command's exit status: doing so
in an interactive shell would require exiting the shell (which kills
the pane) or routing through an out-of-band file or tmux variable.
If you need the status, inspect the captured output for
command-specific success markers.
````

The ``__mcp_status=$?`` capture and ``exit $__mcp_status`` mean the
agent observes the command's real exit code via shell-conventional
``$?`` — even though the wait-for signal fires regardless of
success or failure.
Shell ``;`` semantics fire the ``wait-for -S`` whether ``pytest``
succeeded or failed, so the edge-triggered signal never deadlocks the
agent on a crashed command. Status preservation is intentionally
omitted: chaining ``exit $status`` after the signal would exit the
interactive shell itself, destroying single-pane sessions.

---

Expand Down
6 changes: 3 additions & 3 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ Search all my panes for the word "error".

When you say "run `make test` and show me the output", the agent executes a three-step pattern:

1. {tool}`send-keys` — send the command to a tmux pane
2. {tool}`wait-for-text` — wait for the shell prompt to return (command finished)
1. {tool}`send-keys` — send the command (composed with `tmux wait-for -S <channel>`) to a tmux pane
2. {tool}`wait-for-channel` — block deterministically until the command signals completion
3. {tool}`capture-pane` — read the terminal output

This **send → wait → capture** sequence is the fundamental workflow. Most agent interactions with tmux follow this pattern or a variation of it.
This **send → wait → capture** sequence is the fundamental workflow. For commands the agent authors, the channel pattern is deterministic; for output the agent does not author (third-party log lines, daemon prompts, interactive supervisors), substitute {tool}`wait-for-text` for step 2.

## Next steps

Expand Down
15 changes: 10 additions & 5 deletions docs/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,11 @@ agent calls {tooliconl}`send-keys` in the original pane:
```{warning}
Calling {toolref}`capture-pane` immediately after {toolref}`send-keys` is a
race condition. {toolref}`send-keys` returns the moment tmux accepts the
keystrokes, not when the command finishes. Always use {toolref}`wait-for-text`
between them.
keystrokes, not when the command finishes. For commands the agent authors,
compose `tmux wait-for -S <channel>` into the command and call
{toolref}`wait-for-channel` — deterministic, race-free. For output the
agent does not author (server-startup banners, test-result lines like
the ones above), use {toolref}`wait-for-text` instead.
```

### The non-obvious part
Expand Down Expand Up @@ -391,9 +394,11 @@ long-lived process, I would not hijack it -- I would use a different pane.
### Act

The agent calls {tooliconl}`clear-pane`, then {tooliconl}`send-keys` with
`keys: "pytest"`, then {tooliconl}`wait-for-text` with
`pattern: "passed|failed|error"` and `regex: true`, then
{tooliconl}`capture-pane` to read the fresh output.
`keys: "pytest; tmux wait-for -S pytest_done"`, then
{tooliconl}`wait-for-channel` with `channel: "pytest_done"`, then
{tooliconl}`capture-pane` to read the fresh output. Composing the
`tmux wait-for -S` signal directly into the shell command is the
deterministic path for authored commands.

### The non-obvious part

Expand Down
3 changes: 2 additions & 1 deletion docs/tools/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ All tools accept an optional `socket_name` parameter for multi-server support. I
- Already know the `pane_id` → use it directly

**Running a command?**
- {tool}`send-keys` — then {tool}`wait-for-text` + {tool}`capture-pane`
- {tool}`send-keys` (with `tmux wait-for -S <channel>` composed into the keys) → {tool}`wait-for-channel` → {tool}`capture-pane` — the deterministic path for commands the agent authors
- For output the agent does not author (third-party logs, daemon prompts), use {tool}`wait-for-text` or {tool}`wait-for-content-change` between `send-keys` and `capture-pane`
- Pasting multi-line text? → {tool}`paste-text`

**Creating workspace structure?**
Expand Down
5 changes: 4 additions & 1 deletion docs/tools/pane/send-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
terminal. This is the primary way to execute commands in tmux panes.

**Avoid when** you need to run something and immediately capture the result —
send keys first, then use {tooliconl}`capture-pane` or {tooliconl}`wait-for-text`.
compose `tmux wait-for -S <channel>` into the keys and call
{tooliconl}`wait-for-channel` for deterministic completion, or fall back to
{tooliconl}`wait-for-text` / {tooliconl}`wait-for-content-change` when you
must observe output the agent does not author.

**Side effects:** Sends keystrokes to the pane. If `enter` is true (default),
the command executes.
Expand Down
10 changes: 6 additions & 4 deletions docs/tools/pane/wait-for-channel.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,26 @@

tmux's `wait-for` command exposes named, server-global channels that clients can signal and block on. These give agents an explicit synchronization primitive — strictly cheaper in agent turns than polling pane content via {tooliconl}`capture-pane` or {tooliconl}`wait-for-text`.

The composition pattern: {tooliconl}`send-keys` a command that emits the signal on its exit, then `wait_for_channel`. The signal MUST fire on both success and failure paths or the wait will block until the timeout.
The composition pattern: {tooliconl}`send-keys` a command followed by `; tmux wait-for -S NAME`, then call `wait_for_channel`. Shell `;` semantics fire the second statement whether the first succeeds or fails, so the edge-triggered signal never deadlocks the agent on a crashed command.

```python
send_keys(
pane_id="%1",
keys="pytest; status=$?; tmux wait-for -S tests_done; exit $status",
keys="pytest; tmux wait-for -S tests_done",
)
wait_for_channel("tests_done", timeout=60)
```

The `; status=$?; tmux wait-for -S NAME; exit $status` idiom is the load-bearing safety contract — `wait-for` is edge-triggered, so a crash before the signal would deadlock until the wait's `timeout`.
The `; tmux wait-for -S NAME` suffix is the load-bearing safety contract — `wait-for` is edge-triggered, so a crash before the signal would deadlock until the wait's `timeout`. The shell separator `;` runs the next statement unconditionally, so the signal fires on both success and failure paths.

The payload deliberately does not append `exit $?` — in an interactive shell that exits the shell itself, taking single-pane sessions down with it. If exit-status preservation matters, capture the status out-of-band (e.g. write it to a file the agent reads later, or use a dedicated scratch pane).

```{fastmcp-tool} wait_for_tools.wait_for_channel
```

**Use when** the shell command can reliably emit the signal (single
test runs, build scripts, dev-server boot, anything composable with
`; status=$?; tmux wait-for -S name; exit $status`).
`; tmux wait-for -S name`).

**Avoid when** the signal cannot be guaranteed — for example, when
the command might be killed externally. Use {tooliconl}`wait-for-text`
Expand Down
3 changes: 1 addition & 2 deletions docs/tools/pane/wait-for-content-change.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ Response:
{
"changed": true,
"pane_id": "%0",
"elapsed_seconds": 1.234,
"timed_out": false
"elapsed_seconds": 1.234
}
```

Expand Down
3 changes: 1 addition & 2 deletions docs/tools/pane/wait-for-text.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ Response:
"Server listening on port 8000"
],
"pane_id": "%2",
"elapsed_seconds": 0.002,
"timed_out": false
"elapsed_seconds": 0.002
}
```

Expand Down
8 changes: 4 additions & 4 deletions docs/topics/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ The `enter` parameter defaults to `true`, which is correct for commands (`make t
{"tool": "capture_pane", "arguments": {"pane_id": "%0"}}
```

The capture above may return the terminal state **before** pytest runs. Use {tooliconl}`wait-for-text` between them:
The capture above may return the terminal state **before** pytest runs. Compose `tmux wait-for -S <channel>` into the command and block on {tooliconl}`wait-for-channel` — deterministic, race-free:

```json
{"tool": "send_keys", "arguments": {"keys": "pytest", "pane_id": "%0"}}
{"tool": "wait_for_text", "arguments": {"pattern": "passed|failed|error", "pane_id": "%0", "regex": true}}
{"tool": "send_keys", "arguments": {"keys": "pytest; tmux wait-for -S pytest_done", "pane_id": "%0"}}
{"tool": "wait_for_channel", "arguments": {"channel": "pytest_done", "timeout": 60}}
{"tool": "capture_pane", "arguments": {"pane_id": "%0"}}
```

See {ref}`recipes` for the complete pattern.
For output the agent does not author (third-party logs, daemon prompts, interactive supervisors), substitute {tooliconl}`wait-for-text` for `wait_for_channel`. See {ref}`recipes` for the complete pattern.

## Window names are not unique across sessions

Expand Down
17 changes: 11 additions & 6 deletions docs/topics/prompting.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ These natural-language prompts reliably trigger the right tool sequences:

| Prompt | Agent interprets as |
|--------|-------------------|
| [Run `pytest` in my build pane and show results]{.prompt} | {toolref}`send-keys` → {toolref}`wait-for-text` → {toolref}`capture-pane` |
| [Start the dev server and wait until it's ready]{.prompt} | {toolref}`send-keys` → {toolref}`wait-for-text` (for "listening on") |
| [Spin up the dev server in the bottom-right pane]{.prompt} | {toolref}`find-pane-by-position` (corner=bottom-right) → {toolref}`send-keys` → {toolref}`wait-for-text` |
| [Run `pytest` in my build pane and show results]{.prompt} | {toolref}`send-keys` (with `tmux wait-for -S` composed in) → {toolref}`wait-for-channel` → {toolref}`capture-pane` |
| [Start the dev server and wait until it's ready]{.prompt} | {toolref}`send-keys` → {toolref}`wait-for-text` (for "listening on" — third-party output the agent doesn't author) |
| [Spin up the dev server in the bottom-right pane]{.prompt} | {toolref}`find-pane-by-position` (corner=bottom-right) → {toolref}`send-keys` → {toolref}`wait-for-text` (for the server's readiness banner) |
| [Check if any pane has errors]{.prompt} | {toolref}`search-panes` with pattern "error" |
| [Set up a workspace with editor, server, and tests]{.prompt} | {toolref}`create-session` → {toolref}`split-window` (x2) → {toolref}`set-pane-title` (x3) |
| [What's running in my tmux sessions?]{.prompt} | {toolref}`list-sessions` → {toolref}`list-panes` → {toolref}`capture-pane` |
Expand All @@ -90,8 +90,13 @@ Copy these into your agent's system instructions (`AGENTS.md`, `CLAUDE.md`, `.cu

When executing long-running commands (servers, builds, test suites),
use tmux via the libtmux MCP server rather than running them directly.
This keeps output accessible for later inspection. Use the pattern:
send_keys → wait_for_text (for completion signal) → capture_pane.
This keeps output accessible for later inspection.

For command completion, compose `tmux wait-for -S <channel>` into the
shell command and call wait_for_channel — deterministic, no polling.
Use wait_for_text or wait_for_content_change for observation flows
(third-party logs, daemon prompts). Never capture_pane immediately
after send_keys — the command may still be running.
```

### For safe agent behavior
Expand Down Expand Up @@ -134,6 +139,6 @@ When an agent is unsure which tool to use, these rules help:

1. **Discovery first**: Call {toolref}`list-sessions` or {toolref}`list-panes` before acting on specific targets
2. **Prefer IDs**: Once you have a `pane_id`, use it for all subsequent calls — it never changes during the pane's lifetime
3. **Wait, don't poll**: Use {toolref}`wait-for-text` instead of repeatedly calling {toolref}`capture-pane` in a loop
3. **Wait, don't poll**: For commands the agent authors, prefer {toolref}`wait-for-channel` with `tmux wait-for -S <channel>` composed into the command — deterministic and race-free. Fall back to {toolref}`wait-for-text` or {toolref}`wait-for-content-change` for output the agent doesn't author. Never call {toolref}`capture-pane` in a retry loop.
4. **Content vs. metadata**: If looking for text *in* a terminal, use {toolref}`search-panes`. If looking for pane *properties* (name, PID, path), use {toolref}`list-panes` or {toolref}`get-pane-info`
5. **Destructive tools are opt-in**: Never kill sessions, windows, or panes unless the user explicitly asks
2 changes: 1 addition & 1 deletion docs/topics/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Symptom-based guide. Find your problem, follow the steps.

2. **Special characters**: tmux interprets some key names (e.g. `C-c`, `Enter`). If sending literal text, use `literal=true`.

3. **Timing**: After `send_keys`, use `wait_for_text` to wait for the command to complete before capturing output. Don't `capture_pane` immediately — the command may still be running.
3. **Timing**: After `send_keys`, prefer composing `tmux wait-for -S <channel>` into the shell command and calling `wait_for_channel` for deterministic completion. Use `wait_for_text` or `wait_for_content_change` only when waiting on output you do not author. Don't `capture_pane` immediately — the command may still be running.

## Silent startup failure

Expand Down
Loading