Changelog
All notable changes to pairmux are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
0.5.2 - 2026-08-28
Fixed
-
wait --humancould not be armed on a prompt it had just been handed (#7). The documented hand-off loop iswait --pattern "<the question>", notify the human, thenwait --human— and the pattern wait returns the instant the question is printed, which is inside the settle window the prompt classifier required before it would call a terminalawaiting-input. The handoff therefore armed no answer-watch at all; on a--cmdterminal (ssh, a login prompt) there is no completion mark to fall back on either, so the wait could only run out its full deadline while the human answered in the pane — reporting a finished hand-off as still pending, forever. The settle gate exists to stop a busy command's prompt-shaped line from being called a question; it was never meant to hide a prompt from a caller that had already been shown it. Arming now uses the same evidence without that gate, and — because a hand-off may also be armed before the command has got as far as asking — the decision is re-taken on every poll until it is made, so a prompt that arrives later is picked up too. The two branches that gate on a silence threshold of their own keep them, so nothing that was not a prompt before becomes one now. -
A resolved hand-off on a program terminal pointed at a command that only errors. It offered
wait --donefor following the command that is still running; a--cmdterminal emits no completion marks andwaitrejects that flag outright. It now offerswait --idle, which is the same advice the rejection itself gives. -
The documentation site had not deployed since 2026-08-01. The Docs workflow ran
npm audit --audit-level=moderateahead of the build, a transitive dependency picked up an advisory with no published fix, and because the audit gates the build every deploy stopped with it — so 0.4.0, 0.5.0 and 0.5.1 never reached the site. Everything with a fixed version is now pinned throughoverrides, and the gate is a script that still fails on anything actionable but accepts a short list of reviewed exceptions, re-checking on every run that each one is still genuinely unfixable and still reported. An exception that cannot expire is what turns one stuck advisory into a month of silent non-deployment.
0.5.1 - 2026-08-26
Added
-
A one-line installer at the top of the README, for both platforms it can serve:
curl -fsSL https://raw.githubusercontent.com/treeleaves30760/pairmux/main/install.sh | shirm https://raw.githubusercontent.com/treeleaves30760/pairmux/main/install.ps1 | iexThe POSIX one is the installer that already shipped, promoted out of the last subsection of the install chapter into the first thing anyone reads. The PowerShell one is new and does not pretend to install a Windows binary: tmux has no Windows build, so it finds WSL, checks it has a distribution and something to download with, and runs the POSIX installer inside it — refusing with the command that fixes it rather than half-installing. Configure it through
PAIRMUX_VERSION/PAIRMUX_WSL_DISTRO/PAIRMUX_INSTALL_DIR, since a piped script takes no arguments.Both are checked in CI now: the PowerShell one is parsed and analysed on every push, because a syntax error in a script people run as
irm ... | iexis a syntax error in their terminal.
Changed
- The
pairmux.v1journal gained an event type in 0.5.0 without the release saying so.index.jsonlnow also carriessent, recorded whensenddelivers input. Anything parsing that file against a closed set ofcreated | cmd_start | cmd_end | noteshould accept it. It stores only the shape of a send (text+enter) and never its content — a human answering a handoff types the credential throughsend, and that file outlives the screen. The journal layout in the concepts documentation now lists the full vocabulary.
Fixed
- Documentation still said a note is marked seen only by a completed
run. Since 0.5.0 asendmarks it seen too, which is the whole reason a driving loop over a long-lived program terminates; the human-collaboration guide described the old rule and would have led anyone reasoning from it to the wrong conclusion about why their wait returned.
0.5.0 - 2026-08-26
This release is about one workload: an agent driving other agents, with a human able to step into any of them. Every change below came out of running that end to end against real Claude Code and Codex terminals and fixing what broke.
Added
-
A terminal that holds the keyboard is now recognised as waiting for input. A pager, an editor, an ssh session at a remote prompt, another agent's terminal UI — all of them turn
ICANONoff and then say whatever their author chose, which left them invisible to both the line-discipline check and the pattern list.ICANONoff is itself a declaration that the program reads keystrokes, so a program that has claimed the keyboard, printed nothing, and is running no code at all has nothing left to be doing but waiting on it. Whether it is running is asked of the kernel —KERN_PROC_TTYplus Mach task info on macOS,/procon Linux — never of the screen. Measured against the previous release: a raw-mode prompt in an invented script went from a 15s timeout reported asrunningtoawaiting-inputin 2.5s, vim from invisible to 2.3s, and Claude Code's own trust prompt and input box to 2.5s and 1.4s.It is reported as an inference, not a recognition, because "running nothing" is not "blocked on a read": a TUI waiting on the network looks identical. The reply says so and offers
peek --screenfirst. -
waitaccepts several terminals (pairmux wait a,b,corpairmux wait a b c) and ends on the first to satisfy a condition, naming it in the reply. Waiting on five sub-agents was five processes polling five deadlines, none of which could wake the others. -
wait --noteresolves on a note event and nothing else. A shell terminal completes commands and--donecan wait for the mark; a terminal holding an agent's UI never completes anything, so the only honest turn boundary is one the pane itself declares. Have it callpairmux note "$PAIRMUX_NAME" "..."from a stop hook and the wait ends exactly there, with no screen-reading and no heuristics.--humankeeps its handoff extras;--noteis the primitive underneath, without--human's habit of returning early on a terminal already sitting at a prompt. -
Nested layers. Each pane is now handed its own tmux endpoint, so a pairmux run inside a pairmux terminal builds its own layer. Everything pairmux scopes is already keyed by socket, so this separates the session, the state directory, the name grammar and the write locks in one move: three layers can now each have a terminal called
build.lsmarks a terminal driving a layer with[layer:SOCKET]and spells out the command to descend into it.
Changed
-
killtears down the nested layers a terminal was driving, deepest first, and reports which endpoints went with it. Each layer is its own tmux server, so a grandchild does not die with its parent's; left behind, a sub-agent keeps running and keeps spending, unlisted at every layer anyone would think to look at. -
sendtakes a short lock of its own. Not the write lock — that means "a command is running here", which is exactly when an interactive answer most needs to get through — but one that stops two senders interleaving keystrokes into a line belonging to neither. No lock reaches a human at a keyboard, so when one is attached and watching that window,sendnow says you are sharing its input line. -
A terminal that has been quiet mid-line but is burning CPU is no longer called a question. The weakest inference now asks the kernel before it speaks:
printf 'Building... 'followed by real work used to be reported asawaiting-inputafter ten seconds. -
awaiting-inputon a terminal held by a full-screen program no longer suggests--done. There is no command underneath it to complete, so that hint blocked until the program itself exited — for an agent's UI, never. It now points at--screenand--pattern.
Fixed
wait --humanreplayed the same note forever on a terminal driven bysend. A note counted as answered only once a command completed, and a terminal holding a long-lived program completes none, so every wait after the first returned the first note instantly and an agent looping on "instruct the sub-agent, wait for its reply" never blocked at all. Delivering input now counts as answering it, which is what it always meant.
0.4.0 - 2026-08-10
Changed
-
Prompt detection now reads the pane's terminal, not just the words on it. Echo suppressed while the kernel is still assembling a line is exactly what
getpass()does — and what sudo, ssh, git, gpg, pinentry and npm therefore do — so a credential prompt is identified as one whatever language it is written in and whichever tool asked, including tools no pattern covers. Of eighteen real prompts, the pattern list recognised twelve; the ssh host-key question, terraform'sEnter a value:,[yes/No],Username:and a type-the-name confirmation were all invisible, which meant the terminal sat atrunningand a handoff over one could only end in a timeout. Every one of them is now detected, and every echo-suppressed prompt is refused rather than guessed at — the case where guessing wrong is worst is the one case that is now certain rather than heuristic.Wording remains a second source of evidence, and still promotes a prompt to secret for tools that ask in plain sight. A terminal that only goes quiet mid-line is reported as a third, weaker kind: it is how an unrecognised question surfaces at all, but it also describes a command that printed
Building...and went to work, so it needs ten seconds of silence and its reply says to look before answering and offers--donefor carrying on.runis unaffected below that threshold, and never kills anything either way.The reading costs one ioctl on a device recorded at creation — no subprocess and no tmux round-trip — because several agents watching one terminal each pay for every poll.
Fixed
-
A program terminal could lose the first thing it printed. tmux starts a pane's command as soon as the window exists, which is before
pipe-panecan be attached, so a program whose opening act is a prompt could print into a window nothing was recording and then sit there invisible for its whole life. Linux lost that race routinely while macOS won it. The pane now holds on a FIFO until the journal is capturing, turning the race into an ordering; the holding shellexecs the real command, so what runs in the pane, whatpane_current_commandreports, and how the terminal dies are all unchanged. -
The release job validated every credential except the one it used last. The Homebrew tap token was first exercised after PyPI — which cannot be republished for a tag — and after the GitHub release went public, so v0.3.0's
403 Resource not accessible by personal access tokencould only surface in the one position where recovery has to be manual. The preflight now proves the tap token can reach the cask for stable tags, before anything publishes, and a manual Tap credential workflow proves read and write after a rotation: only Actions can read the secret, so a value mistyped intogh secret setis otherwise invisible until a release depends on it.
0.3.0 - 2026-08-09
Added
pairmux wait <name> --doneblocks until the terminal's running command finishes — or until the next one does, if the terminal is idle right now — and reports itsexit_code.waittakes no lock and records nothing, so any number of agents can hold a--donewait on one terminal and every one of them wakes on the same completion mark: a completion broadcast with nothing to register with and no daemon involved. It also covers a command a human typed into the pane directly. Shell terminals only; a--cmdprogram terminal emits no completion marks and getsE_BAD_ARGSwith a workable alternative.detect.CompletionWatcher, a pollable form of the C-correlated completion detectionrunalready used.waithas to watch a completion alongside notes, patterns and quiescence, so it drives the watcher from its own loop instead of blocking inside one.
Fixed
wait --humancould only end in its own timeout. The handoff the docs teach — hit a secret prompt,wait --human --notify, let a human type the password in the pane — resolved only on apairmux note, and a human who answers the prompt writes nothing to the journal: attaching records no event, and typing into a pane is not one either. The command would finish and the agent would keep blocking for the full 300s.--humannow also resolves when the human is finished: the prompt is answered and the terminal is visibly moving again (status: running, whosenextoffers--donefor following the command the rest of the way), or the command finishes outright (status: donewith itsexit_code). Reporting resumption rather than completion is the point — a password answered at second two can be followed by a five-minute migration, and the agent is released at second two. A rejected answer that re-prompts keeps the wait blocked, because the terminal is still awaiting input. A completion that landed before the wait started resolves it immediately, the same way an unseen note already does — an agent that hands off, does other work, and only then waits is not left blocking for a human who has gone.- Adding
--idletowait --humanwas not a workaround:awaiting-inputcounts as a terminal state for an idle wait, so it returned instantly with the very prompt being handed off, and the agent's only move was to hand off again. A prompt no longer resolves a--humanwait at all. - A timed-out wait's retry hint dropped every condition flag:
wait <name> --human --notifycame back suggesting a barewait <name> --timeout 2x, which at the handoff prompt is an idle wait that returnsawaiting-inputinstantly — so an agent following its ownnextspun instead of waiting. The hint now reproduces the wait exactly (--idle/--pattern/--done/--human/--notify) with double the deadline, floored at 300s for a handoff so it cannot undercut the skill's own rule, and a--humantimeout leads with "the human has not answered yet — do NOT type the secret". - A handoff is released by the human's answer, not by their keystrokes. Typed characters echo into the journal one at a time, and an echoed answer stops the last line looking like a prompt well before the human commits to it — so "output appeared past the prompt" alone reported a human who was still deciding as a human who had finished. The line the prompt sat on must now be terminated (the newline an Enter produces, echoed by the tty or written by the program) before any verdict is reached, and the verdict is then held briefly so a rejected answer's re-prompt wins over the echo that preceded it.
- A resolved
--humanhandoff returns nooutput. The span it would quote is the span a human was summoned to type into, so a program that echoes what it should not — or a secret prompt the heuristics never recognized — would have put the credential straight into the agent's context. The agent gets the fact and the exit code, andpeeks if it wants the output.
0.2.0 - 2026-08-01
Added
pairmux prune [name] [--older-than 7d] [--dry-run]reclaims retained journals: dead terminals' state directories and.prevrotation archives (including orphaned ones), never a live terminal's journal, and never a directory whose write lock is held. This is the documented exit forkill's retain-the-journal default;killand the large-journal guard now teach it.doctorreports the bytes retained under the state namespace with the largest terminal directories, and suggestspruneonce the total passes the large-journal threshold.- The large-journal warning is now also appended to
waitreplies. Program terminals (dev servers,tail -f) are driven bysend/wait/peekand previously never saw the guard even though they grow journals the fastest.
Changed
-
README, the docs site, and the Agent Skill now lead with what only pairmux provides — a real PTY for interactive programs, persistent shell state, live human handoff, and shared observation — and include an explicit "when to use it and when not to" table that sends short commands to the agent's own shell tool and plain long non-interactive commands to the harness's background mode. The install docs now cover the live signed APT repository (pairmux-apt) instead of calling it unavailable, and state the tmux ≥ 3.2 requirement up front.
-
Secret-prompt recognition covers far more than English password prompts: PIN, OTP/MFA/2FA, verification/security/access codes, API/encryption/private keys, tokens and client secrets, plus the standard sudo password translations (zh-TW/zh-CN 密碼/密码, de Passwort, fr mot de passe, ja パスワード, es contraseña, pt senha, ko 암호, ru пароль) and fullwidth-colon endings. Multi-option confirmations (
[y/N/a],(y/N/q)) are now recognized too. A newPAIRMUX_SECRET_PROMPT_REenvironment variable extends — never replaces — the builtin patterns;doctorvalidates it. Documentation now states plainly that recognition is best-effort and that a known credential prompt sitting quiet atrunningshould be treated as a handoff candidate. -
A Homebrew cask (binary + man page,
depends_on formula: tmux, quarantine-stripping postflight) is staged in the GoReleaser configuration, and the channel is now active: the release workflow preserves the rendered cask during validation and pushesCasks/pairmux.rbto thetreeleaves30760/homebrew-pairmuxtap after the release goes public (prereleases skipped, content read back and byte-compared). RELEASING.md documents the wiring, and the APT channel is recorded as live via the pairmux-apt repository.
Fixed
- Documentation builds pick up the patched
fast-urirelease and override the vulnerable transitivebrace-expansionversions with 5.0.9;npm auditreports zero known vulnerabilities again. - The commit-subject validator pins
LC_ALL=Cso its[a-z]pattern brackets match bytes rather than locale collation. Under locales such aszh_TW.UTF-8the ranges also matched uppercase letters, silently accepting invalid subjects (and failing--self-test) on contributor machines while CI's C locale rejected them. runnow takes the command as exactly one argument, matching the MCPpairmux_runcontract. The previous variadic form re-joined tokens on single spaces and typed the result into the shell, silently discarding quoting the caller's shell had already consumed (pairmux run t git commit -m "two words"committed with the messagetwo). Multiple command tokens now returnE_BAD_ARGSwith a hint that reconstructs the correctly quoted single-argument command.
0.1.1 - 2026-07-20
Changed
- PyPI wheels now use a dedicated Markdown project description with copyable installation and quickstart commands, precise platform requirements, Agent Skill and MCP entry points, and links to the documentation, changelog, and issue tracker. Core Metadata also advertises tmux as an external requirement and adds discovery keywords; the release workflow rejects wheels whose description fails PyPI's strict renderer check.
- The README and documentation now use reproducible JSON examples, distinguish direct
.deb/.rpmdownloads from the not-yet-implemented APT repository, and document prompt detection, future-only pattern waits, program terminals, notes, and human handoff without overstating their guarantees. The note command's self-teaching hint now distinguishesrun/peeknote fields fromwait --human'soutputresult, and the nested-tmux attach hint no longer recommends an impossible cross-serverswitch-client.
0.1.0 - 2026-07-20
Fixed
- Tag releases now build the complete GoReleaser archive/package/checksum set once, verify and preserve those exact bytes with the four wheels, then promote them without recompilation. GitHub remains a draft until PyPI succeeds, tag commits and workflow syntax are checked, and concurrent runs serialize.
- The wheel builder recognizes GoReleaser's dotted arm64 variant directories such as
v8.0, validates both Mach-O architecture/deployment targets and ELF64 architecture, and rejects a binary whose native header does not match its wheel platform tag. Only canonical three-part SemVer tags are accepted; supported prerelease tags are normalized to PEP 440 wheel metadata. install.shnow stages the verified binary beside its destination and atomically renames it, replacing a pre-existing symlink instead of following it; it also requires the installed version to match the tag.- Unsigned and unnotarized Homebrew casks are no longer advertised or published. That channel remains deferred until macOS signing and a real Gatekeeper installation test are available.
- Terminal names and tmux socket labels are validated before any filesystem or tmux operation. State
now lives in an endpoint-specific hash of canonical
TMUX_TMPDIR, uid, and socket label, preventing traversal and cross-server metadata collisions while retaining an in-place, non-migrating fallback for the historical conventional default-socket layout. - Concurrent terminal creation uses a per-name reservation; journal preparation and command dispatch
now close their failure paths so a partial tmux operation cannot leak a pane or leave a permanent
E_BUSYcommand record. - Fish 4+ uses its native OSC 133 integration. Fish installations that emit no ready mark degrade to
the sentinel path using fish's
$status, rather than the POSIX-only$?expansion. wait --idlenow treats output silence as a prompt to refresh terminal state, not as proof that a command completed. Quiet running commands continue waiting; prompts and dead panes surface their actual status.- Managed-pane discovery now uses a printable tmux format separator. Alpine's C locale can sanitize control-character tabs into underscores, which previously made a newly created live pane look dead.
- Bounded
peekand default-logresponses now report skipped raw prefixes astruncated.omitted_bytesand provide the executablepairmux log NAME --range 1:endrecovery. Explicitlog --grep,--range, and--cmdselections read the complete requested history;--range A:endselects through the final shaped line. - Documentation builds override vulnerable transitive
serialize-javascriptanduuidreleases with patched versions;npm auditnow reports zero known vulnerabilities.
Added
pairmux mcp serveexposes the existing terminal commands as typed MCP2025-11-25stdio tools, preserving eachpairmux.v1envelope as structured and text content without invoking a wrapper shell.pairmux skill installembeds the canonical Agent Skill and installs it atomically for Claude Code, Codex, Gemini CLI, Cursor, OpenCode, GitHub Copilot, Windsurf, Kiro, Amp, or the cross-agentagentslocation. Codex andagentsshare the standard~/.agents/skillsdestination, and--target allupdates each existing agent directory only once.- Terminal lifecycle and core loop:
pairmux new(optional--name,--cwd,--cmd) opens a tmux-backed terminal;runblocks for command completion;peekreturns a bounded recent view;senddelivers interactive input;logreturns a bounded recent view or complete explicitly selected history with--cmd,--grep, and--range A:B/A:end;lslists status and activity; andkillends terminals while retaining their journals. waitfamily:pairmux wait <name>blocks until the shell is truly idle (--idle MS, the default), a regex matches new output (--pattern RE), or a human note arrives (--human), with--timeoutand a--notifydesktop ping.- Notes and human handoff:
pairmux note <name> <text>records a message that surfaces in the driving agent's nextrun/peekenvelope;wait --humaninstead returnshuman-donewith the note text inoutput. - Awaiting-input detection: a quiet command whose last screen line matches an interactive prompt
(
[y/N],password:, pager--More--/(END), "press any key") makesrunreturnawaiting-inputbefore its overall timeout. Password/passphrase/passcode prompts are classified as secrets and answered with a human-handoff hint rather than a guess. - Human-facing commands:
pairmux attach [name]takes over the live tmux session;pairmux watch [--interval 2s]renders a self-refreshing dashboard;pairmux doctorprobes tmux version, per-shell completion tier, state-dir writability and the notification backend. - Completion detection: injected OSC 133 integration for bash/zsh and native fish 4 marks are the
primary signal; a shell-specific sentinel marker is the fallback.
doctordistinguishes the diagnostichooks-no-Ctier from the stored terminal modes (hooksorsentinel). - Journal and locking: each terminal streams raw output through
tmux pipe-paneinto a per-terminal journal (raw.log+index.jsonl, mode 0600); reads are lock-free, writes take a per-terminal writerflockand return anE_BUSYenvelope naming the holder when contended. Attaching is a native tmux operation and records no event; humans leave an explicitnotewhen handing back. - Output shaping and
pairmux.v1JSON envelopes: every non-interactive command replies through a self-describing envelope (--json) carryingstatus, ANSI-stripped and carriage-return-collapsedoutput,truncatedrecovery metadata when output is elided,notes, orderednexthints, and stableerror.codevalues (E_NO_TERMINAL,E_EXISTS,E_BUSY,E_DEAD,E_BAD_ARGS,E_TMUX,E_INTERNAL).