Skip to content

Workspace: add the tmux-workspace command - #15

Open
tony wants to merge 73 commits into
masterfrom
workspace-cli
Open

Workspace: add the tmux-workspace command#15
tony wants to merge 73 commits into
masterfrom
workspace-cli

Conversation

@tony

@tony tony commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Adds tmux-workspace, a Go command under workspace/cmd/tmux-workspace that manages tmuxp YAML and JSON workspaces through native Go services. The command tree follows tmuxp (repo, v1.74.0 release notes, PyPI): load, ls, search, edit, freeze, convert, import teamocil, import tmuxinator, shell and debug-info, each accepting inherited --json, --ndjson and --color.

Cobra and the presentation dependencies stay in the workspace module, outside the core tmux module, and the workspace module builds with GOWORK=off against the published core. Discovery, conversion, import, help and documentation generation do not need tmux.

What the CLI does

  • load creates or appends sessions natively, honours inherited tmux options, the initial base-index and terminal size controls, waits for shell prompts only on panes that receive commands, and converts boolean tmux options to on and off.
  • Native imports preserve source command grouping, pane order, focus, synchronization phases and saved directory context. Unsupported fields, ERB templates and invalid converted models fail before output or overwrite; generic conversion preserves arbitrary document fields.
  • Native loads reject unknown workspace, window, pane, command and readiness keys before opening logs, running scripts or changing tmux. description metadata, open option/environment names and Python extension fields remain supported; conversion preserves source fields.
  • -s renames only the final input, so later workspaces are not skipped by reusing the first session.
  • Appending verifies that the selected socket matches TMUX, authenticates the inherited daemon and PID, and retains the borrowed session across hooks and moved panes.
  • before_script argv is parsed during all-input preflight, so malformed quoting fails before any session exists. Owned sessions are cleaned up when a script or a completion record fails.
  • Human loads offer tmuxp's inside-tmux choices, attach or switch through the controlling terminal, and refuse independent client focus or a replacement client before handoff.
  • search uses Go regexp by default; Python matching sits behind an explicit engine option.
  • Python plugins run through a checked bridge that receives the same append mode as native load. A scripted bridge append is rejected in preflight, because the Python builder deletes a borrowed session when before_script fails. Empty plugin lists and builder paths stay native.
  • Human load renders progress from lifecycle events; --log-file appends severity-filtered JSON records to a regular file opened before backend work.
  • -8 is refused before input or runtime lookup; -2 still reaches tmux.
  • Command metadata, manuals and shell completion are generated from the command tree.

Core library changes

tmux.Color88 is removed. NewServer now rejects mode 88 with a ColorError wrapping ErrUnknownColor and ErrInvalidServerOptions, and neither process nor control-mode clients forward -8. tmux 3.2a and later reject that flag, so accepting the mode only deferred the error to the first command. Callers that named Color88 stop compiling.

Verification

Integration tests under workspace/internal/integration run the CLI against private tmux sockets, including a controlling PTY for attach, detach and client switching. workspace/scripts/verify_cli.py audits an installed binary: the pinned command inventory, every leaf, streaming, progress, round-trip topology and matched timings against tmuxp.

$ python3 workspace/scripts/verify_cli.py \
    --binary "$(go env GOPATH)/bin/tmux-workspace" \
    --output workspace-results.json \
    --reference "$TMUXP_REFERENCE_CHECKOUT"

Remaining work

The CLI has no published release; install it from a checkout with go install ./workspace/cmd/tmux-workspace. The installed audit reports a missing example plugin as a dependency gap, and its topology checks do not assert that external applications start.

tony added 30 commits September 12, 2026 06:49
why: Workspace documents need a command interface that preserves full
configuration data and separates machine output from terminal work.

what:
- Add the tmuxp command tree with inherited JSON and NDJSON output.
- Load, append, capture and convert through native workspace services.
- Preserve Python expression and shell behavior through checked bridges.
- Cover parsing, process streams and isolated tmux lifecycle regressions.
why: Review found that streamed Unicode, importer discovery and pane
readiness diverged from the reference behavior.

what:
- Decode child UTF-8 incrementally across pipe writes.
- Resolve tmuxinator names through TMUXINATOR_CONFIG.
- Validate readiness policies and wait for default-shell prompts.
- Cover split characters, directory overrides and delayed real prompts.
why: Initial windows and before-script execution must use the resolved
workspace policy without disturbing a borrowed append session.

what:
- Distinguish inherited script cwd from explicit start_directory.
- Create a blank pane when the pane list is omitted.
- Read inherited tmux options and honor the initial base-index.
- Verify script failure preserves borrowed sessions on private sockets.
why:
- Keep the CLI independently buildable and its reference reproducible.

what:
- Generate annotated command metadata, manuals and shell completion.
- Document native execution and explicit Python compatibility boundaries.
- Use the published core dependency and verify real Python bridges.
why:
- Optimization and closed pipes must not bypass compatibility safeguards.

what:
- Check Python and tmuxp versions without removable assertions.
- Cancel owned process groups when output or log drains fail.
- Preserve drain errors and cover both failures under the race detector.
why:
- Load progress controls must affect terminal behavior without changing
  machine output or losing child output failures.

what:
- Render presets and custom tokens from native lifecycle events.
- Bound script panels by Unicode width and terminal height.
- Preserve write errors, clear frames before raw script output, and keep
  warnings visible without progress.
- Verify streamed events and progress using the installed CLI and a PTY.
why:
- Pane IDs are only unique within one tmux server.

what:
- Verify the selected socket matches TMUX before appending windows.
- Resolve implicit current panes only on their originating server.
- Cover identical pane IDs on two isolated servers without mutating the
  unrelated session.
why:
- Root actions and parse failures must honor the requested output mode.

what:
- Preserve all accepted boolean spellings in structured parse errors.
- Reject a bare machine root invocation and envelope completion output.
- Parse JSON files as JSON instead of silently accepting YAML syntax.
why:
- Valid boolean tmux options failed and bridge stdout diagnostics vanished.

what:
- Convert boolean tmux options to on/off without changing environment data.
- Retain bounded script output, status and truncation in load summaries.
- Include both child streams in Python workspace failures.
- Verify boolean options and before-script capture on an isolated server.
why:
- Invalid dimensions must fail before execution and explicit detection
  controls must reach session creation.

what:
- Validate fallback dimensions before resolving workspace inputs.
- Respect terminal detection, terminal size and environment overrides.
- Cover malformed dimensions without an available tmux binary.
why:
- Foreground attach needs the real terminal stdin and human choices must
  control whether load switches, detaches or appends.

what:
- Preserve terminal stdin separately from buffered prompt input.
- Implement inside-tmux choices and existing-session confirmation.
- Honor --yes and reject foreground loads without terminal stdin.
- Verify native attach, detach and client switching with an installed CLI
  and a controlling PTY.
why:
- Grouping word-boundary expressions and formatting command dictionaries
  in Go changed reference search results.

what:
- Preserve the pinned ungrouped word-boundary expression.
- Pass raw command values to Python for reference string conversion.
- Match Python's omission of empty commands and cover both regressions.
why:
- Installed behavior and performance need reproducible evidence beyond
  parser and in-process tests.

what:
- Verify the pinned command inventory and every command leaf.
- Exercise streaming, terminal progress, attach, switching and round-trip
  topology on private sockets.
- Retain matched tmuxp timing samples and per-example outcomes without
  treating unavailable plugin dependencies as passes.
- Document the verifier and remaining local installation boundary.
why: Ordinary workspace search must not require a Python process.

what:
- Use Go regexp by default and group whole-word alternatives
- Keep Python matching behind an explicit engine option
- Verify missing-runtime behavior and installed search timings
Malformed quoting was detected after session creation, leaving an owned
session alive and allowing earlier inputs to mutate borrowed state.

Parse native script argv during all-input normalization and retain it for
execution. Verify preflight with no backend and unchanged live topology;
keep Python-owned script parsing and runtime cleanup unchanged.
Forwarding detached mode with append made the Python bridge create a
separate session while the native command reported append success.

Pass one bridge mode, preserving native append precedence. Exercise both
flag forms with the installed checked plugin runtime and verify borrowed
session identity, pane retention, window growth and plugin targeting.
The checked Python builder deletes borrowed sessions when a document
before_script fails, with no injectable cleanup ownership policy.

Reject that bridge append combination during preflight, before runtime
checks or backend access. Verify the actual destructive script regression
and retained session/pane state; keep safe native and bridge modes.
why: Applying -s to every input reuses the first session and skips
later workspaces.

what:
- Override the final document name during all-input preflight
- Pass each normalized session name to the Python bridge
- Verify native and plugin loads retain distinct input sessions
why: Supported tmux releases reject -8, so starting workspace work
only produces backend failures.

what:
- Refuse 88-color requests before input and runtime lookup
- Remove native and Python bridge forwarding for -8
- Verify refusal preserves sessions and -2 still reaches tmux
why: tmux 3.2a and later reject -8, so accepting mode 88 defers a
configuration error until a command runs.

what:
- Remove Color88 and legacy process/control client forwarding
- Reject numeric 88 before executable lookup with typed errors
- Preserve default and 256-color behavior
why: Append could follow a stale daemon or moved pane and run hooks on
the wrong workspace.

what:
- Authenticate inherited socket/PID and retain a core-bound session.
- Recheck native build entry and Python extension boundaries.
- Preserve borrowed IDs on failure and cover real socket retargets.
why: Optional diagnostics must not cancel workspace scripts or hide
mandatory results, and invalid destinations must fail before backend
work.

what:
- Filter private JSON log records and optional warnings by severity
- Open regular append files early and contain write or close failures
- Preserve script status and verify file, filtering and failure behavior
why: Output failures must not hide sessions a load already changed.

what:
- Record input outcomes before publishing completion events
- Include observed results in JSON and human failure diagnostics
- Cover final write, flush, cancellation and earlier event failures
why: Completion record failures must not keep failed owned sessions
alive.

what:
- Preserve script errors through completion event failures
- Finish owned cleanup before returning joined errors
- Cover prior effects, borrowed sessions and successful child controls
why: Invalid terminal context could mutate workspaces before attachment
failed, and implicit switching could select a replacement client.

what:
- Validate inherited daemon, pane, input tty and client before building
- Retain the selected client and recheck its identity before switching
- Acquire the controlling tty early and observe output/prompt flushes
- Cover preflight, independent focus, replacement and terminal cleanup
why: Empty plugin lists and builder paths alone unnecessarily require
Python and block native scripted append.

what:
- Select the bridge only when extension values require it
- Preserve native scripts and borrowed sessions with neutral metadata
- Cover native topology, Python launch avoidance and extension controls
why: A literal newline triggers the interactive shell continuation
prompt and can prefix the capture fixture marker.

what:
- Let printf interpret the newline after the complete command is typed.
why: Killing an attached client can return before tmux restores the
terminal, leaving cancellation diagnostics unwritable.

what:
- Request graceful attachment cancellation before bounded forced cleanup.
- Restore borrowed terminal state and descriptor flags before diagnostics.
- Preserve cancellation status and report secondary restoration failures.
- Verify delivery and terminal recovery with paused-daemon controls.
why: The drain failure fixture must exercise an established process group
without racing its output-triggered cancellation against a later fork.

what: Start the sleeping descendant before emitting output and wait for
it in the shell. Keep the failure status and prompt-return assertions.
why: Resolving the full before_script command before tokenization splits
workspace paths containing spaces and can alter script arguments.

what: Parse arguments first, then resolve only a dot-relative executable
against the workspace directory. Exercise inherited and explicit script
working directories with a workspace parent and marker paths containing
spaces, retaining borrowed-session cleanup coverage.
tony added 30 commits September 13, 2026 04:36
Read progress environment defaults only when the presenter is active.
Machine output, redirected stderr and explicit disabling no longer reject
a load because of an unused default; explicit flags remain validated.
Describe when progress defaults apply and retain explicit flag validation.
Only consume script stdout in the panel when stdout and stderr identify
the same terminal. Pipes, files and separate terminals keep their output;
writer failures remain visible to process cancellation.
Describe the shared-terminal panel policy and preserve explicit stdout
destinations while progress renders on stderr.
Share terminal capability checks between progress defaults and rendering.
Unset or dumb TERM values, unavailable dimensions and tiny terminals no
longer activate the presenter or validate unused environment defaults.
Describe when terminal capabilities disable progress and its defaults.
Route termination through the existing cancellation context so setup
processes stop and the CLI publishes its final interruption result.
Document the shared SIGINT/SIGTERM cleanup path and interruption status.
Match tmuxp's disabled startup default while preserving last-occurrence
precedence for explicit startup and vi-mode toggles.
Document the explicit flag required to load Python startup files.
why: Open terminal or pipe input keeps confirmation prompts blocked after
SIGINT or SIGTERM, and buffering hides input from later consumers.

what:
- Wake Unix input polling through a joined context cancellation callback
- Preserve caller input identity, descriptor flags and terminal settings
- Read one answer without consuming later input
- Verify cancellation, answer boundaries and partial EOF under race tests
why: Interactive users need to know how waiting prompts respond to signals.

what:
- Document Unix prompt cancellation and preserved terminal input
- Record the observable behaviour in the workspace changelog
why: A shell prompt can precede output from queued commands, making
exact-line assertions miss bytes the observation already delivered.

what:
- Keep both Writer commands and delimit their output markers
- Capture raw lines and pane state only when the reader fails
why: Start redacts native stderr before removing its failed window,
leaving hosted failures without the daemon refusal needed to diagnose.

what:
- Observe failed subprocess results through the private test runner
- Record respawn stderr before typed error redaction and cleanup
why: Stale dimensions leave progress outside the resized terminal
and can erase output using invalid cursor rows.

what:
- Remeasure the active terminal without changing descriptor flags
- Preserve requested line limits through shrink and growth
- Forward output while the panel cannot fit and resume on a fresh line
- Exercise native PTY resizing, raw routing and cursor recovery
why: Terminal changes affect panel size and script output routing.

what:
- Document resize recovery and fixed or automatic line limits
- Record the visible progress behavior in the changelog
why: Connect native workspace loading to the MCP inspection workflow.

what:
- Document the shared socket, command installation and inspect toolset
- Explain tool discovery, pane identities and bounded text waits
why: Ignored command controls can execute unintended actions after
earlier inputs have already changed tmux.

what:
- Validate native workspace, window, pane, command and readiness keys
- Preserve description metadata, open dictionaries and extension fields
- Prove all-input refusal before scripts, logs and topology changes
why: Native execution needs a clear boundary for unknown configuration.

what:
- Document supported command and readiness fields and metadata handling
- Explain conversion, Python extension and endpoint selection behavior
why: Input sent before shell startup can be echoed before its prompt,
so the prompt prefixes the output marker and defeats exact matching.

what:
- Emit a leading newline before the literal capture marker
- Preserve exact-line matching and the leading-dash input assertion
why: Imports could discard source behavior or fail their own loader.

what:
- Preserve command groups, pane order, focus and directory context
- Map synchronization phases to native sequential pane delivery
- Refuse unsupported fields and templates before publication
- Validate converted models and exercise owned native round trips
why: Source formats contain behavior the native loader cannot express.

what:
- Describe grouping, synchronization, focus and relocated roots
- State field, template and alias refusal before overwrite
- Keep generic conversion separate from native import validation
why: The suite root is created under /tmp, which macOS reaches through a
     link to /private/tmp, so tmux answered a pane's physical directory
     while a test compared it against the logical path built from the
     root. The imported-workspace directory test failed only on macOS.

what:
- Resolve the created root before the suite environment adopts it
- Cover a linked base and a physical one, both against the same root

evidence: The new case fails on Linux without the resolution step, and
the imported-workspace test passes locally under a linked base.
why: Kill timed out on a hosted runner at the test's own deadline, but
     this site lacked the diagnostics its two sibling kill sites carry,
     so the run recorded no daemon, pane or context state to read.

what:
- Record the failed kill's state at the streaming site

evidence: Twenty local runs under the race detector pass in 35s, about
1.75s each against a 15s budget, so slowness alone does not explain the
hosted failure and the next occurrence needs its state captured.
why: macOS reaches the suite root through /tmp, a link to /private/tmp,
     so tmux answered a pane's physical directory while the test wanted
     the logical path. Canonicalizing the root itself, as 67a5cfe did,
     fixed this but left the macOS control-client tests failing across
     two runs; the suite root also names every socket, and its literal
     spelling is what keeps those paths short.

what:
- Restore the suite root the harness creates
- Resolve the imported workspace directory in the test that compares it,
  as the sibling integration tests already do

evidence: 67a5cfe made the macOS job fail in tmux/internal/integration,
which had passed on the run before it, and a rerun failed there again.
why: Tmuxinator expands ERB through Ruby before parsing, and no native
     reader does, so markup was copied into the native document and
     reached the shell as a literal. Teamocil evaluates no templates, so
     the same text there is ordinary and must survive.

what:
- Refuse template markup in tmuxinator keys and values before conversion
- Cover refused sources beside a teamocil source that keeps the markup

evidence: Before the fix, the teamocil case with an ERB-like pane command
exited 1 with empty stdout and "import does not evaluate ERB templates";
after scoping the check to tmuxinator, it exits 0 and the saved workspace
carries "false; echo <%= literal %>" verbatim. Removing the mapping-key
check in importTemplates makes only the new tmuxinator/ERB#02 case fail,
confirming that case exercises the key path specifically.
why: Which importer refuses templates, and why the other does not, was
     unstated. The existing changelog entry also lumped ERB refusal into
     a format-neutral list, which is no longer accurate.

what:
- Record the tmuxinator refusal and teamocil's literal handling in CLI.md
- Drop the format-neutral ERB claim from the earlier import entry
- Add a changelog entry naming both halves of the boundary
why: The page said the module builds with GOWORK=off against its
     published core, without qualification. On a branch that adds to the
     core it does not: this one fails on Server.ValidateLayouts and
     SelectLayoutRequest.Validate, which no core release names yet.

what:
- State that a released version builds against the core release it
  requires, and route to the publishing order CONTRIBUTING sets out
why: A suite root reached through a link makes tmux answer a pane's
     physical directory while a test compares the logical path built
     from it. macOS reaches /tmp that way. 67a5cfe fixed this and was
     reverted in 0e45cf8 on the theory that the longer canonical
     spelling overflowed the socket-path guard, and that the theory
     explained two macOS control-client failures.

     A spike disproved it. A socket path is the root plus about 14
     bytes, so 41 against a guard of 103; the length cannot overflow.
     Pointing the suite root at a short symbolic link reproduces the
     original failure on Linux for the first time, and under that base
     the canonical root passes tmux/internal/integration, tmux/tmuxtest
     and every workspace package, including the imported-directory test
     with its own resolution removed.

what:
- Resolve the created root before the suite environment adopts it
- Cover a linked base and a physical one against the same root

evidence: Neutralizing the resolution fails the linked case with root
"…/002/link/ltg-389070043" against the physical base "…/001", while the
physical case stays green.
why: Canonicalizing it fails macOS a third time, on the same two tests:
     TestNewSessionConnectionKeepsTheCreatingControlProcess and
     ...SurvivesCreatedSessionDestruction, both "control client is
     closed". Three runs with the resolution fail, every run without it
     passes, so the correlation is the evidence even though the
     mechanism is unknown.

     A spike ruled out the socket-path guard: a socket path is the root
     plus about 14 bytes, well under 103, and pointing the root at a
     short link on Linux reproduces the original directory mismatch
     while leaving the control-client tests green. Whatever macOS does
     with /private/tmp here, Linux does not reproduce it.

what:
- Restore the root the harness creates, and drop its resolution test
- Leave the resolution in the one test that compares tmux's answer

evidence: macOS failed at 30071e7 exactly as at 3230e2f, and passed at
9cf7eb7 and 66d537e without the resolution.
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.

1 participant