Skip to content

feat: tmuxinator/teamocil feature parity#1025

Open
tony wants to merge 143 commits into
masterfrom
parity
Open

feat: tmuxinator/teamocil feature parity#1025
tony wants to merge 143 commits into
masterfrom
parity

Conversation

@tony
Copy link
Copy Markdown
Member

@tony tony commented Mar 17, 2026

Summary

Bring tmuxp to feature parity with tmuxinator and teamocil. This adds the missing CLI commands, config keys, lifecycle hooks, config templating, and importer improvements that users migrating from those tools expect — plus a full feature comparison page and documentation for every new command, flag, and config key.

New CLI commands

tmuxp stop — kill a session with cleanup

$ tmuxp stop mysession

Runs the on_project_stop lifecycle hook before killing the session, giving projects a chance to tear down background services, save state, etc.

tmuxp new — create a workspace config

$ tmuxp new myproject

Creates a new workspace config from a minimal template and opens it in $EDITOR.

tmuxp copy — copy a workspace config

$ tmuxp copy myproject myproject-backup

Copies an existing workspace config to a new name. Source is resolved using the same logic as tmuxp load.

tmuxp delete — delete workspace configs

$ tmuxp delete old-project

Deletes workspace config files. Prompts for confirmation unless -y is passed.

Lifecycle hooks

Workspace configs now support four hooks, matching tmuxinator's hook system:

session_name: myproject
on_project_start: docker compose up -d
on_project_exit: docker compose down
on_project_stop: docker compose down -v
on_project_restart: echo "Reattaching..."
windows:
  - window_name: editor
    panes:
      - vim
Hook When it runs
on_project_start Before session build (new session creation only)
on_project_restart When reattaching to an existing session (confirmed attach only)
on_project_exit When the last client detaches (via tmux client-detached hook)
on_project_stop Before tmuxp stop kills the session

Config templating

Workspace configs now support {{ variable }} placeholders with values passed via --set. Given mytemplate.yaml:

session_name: "{{ project }}"
start_directory: "~/code/{{ project }}"
windows:
  - window_name: editor
    panes:
      - vim
$ tmuxp load --set project=myapp mytemplate.yaml

New config keys

Pane titles

session_name: dashboard
enable_pane_titles: true
pane_title_position: top
pane_title_format: "#{pane_title}"
windows:
  - window_name: main
    panes:
      - title: logs
        shell_command: tail -f /var/log/syslog
      - title: editor
        shell_command: vim

synchronize shorthand

windows:
  - window_name: multi-server
    synchronize: before    # or: after, true
    panes:
      - ssh server1
      - ssh server2

Desugars synchronize: beforeoptions: {synchronize-panes: on} and synchronize: afteroptions_after: {synchronize-panes: on}. true is equivalent to before.

shell_command_after and clear

windows:
  - window_name: dev
    shell_command_after:
      - echo "Window ready"
    clear: true
    panes:
      - vim
      - npm run dev

New tmuxp load flags

Flag Description
--here Reuse the current tmux window instead of creating a new session
--no-shell-command-before Skip all shell_command_before entries
--debug Show tmux commands as they execute (disables progress spinner)
--set KEY=VALUE Pass template variables for config templating

Importer improvements

tmuxinator

  • preon_project_start, pre_windowshell_command_before
  • cli_args (-f, -S, -L) parsed into tmuxp equivalents
  • synchronize window key converted
  • startup_window / startup_panefocus: true on the target
  • Named panes (hash-key syntax) → title on the pane
  • Window names coerced to str (fixes numeric/emoji YAML keys)

teamocil

  • v1.x format support (windows at top level, commands key in panes)
  • focus: true on windows and panes converted
  • Window options passed through

Design decisions

  • Template syntax is plain {{ var }} substitution, not Jinja2 — no
    conditionals or loops. Keeps configs declarative and avoids a template-engine
    dependency; --set values are validated against YAML-unsafe characters.
  • --here provisions the reused pane with respawn-pane -k rather than
    typing cd/export into the running shell — no POSIX-shell assumption, no
    keystrokes landing in foreground programs. tmuxp warns first if the pane has
    running child processes, since -k kills them.
  • on_project_exit rides tmux's client-detached hook, so it fires on any
    client detach — not only tmuxp-initiated ones.

Documentation

  • Feature comparison page (docs/comparison.md): Side-by-side of tmuxp vs tmuxinator vs teamocil — architecture, config keys, CLI flags, hooks
  • Top-level config docs (docs/configuration/top-level.md): New keys, lifecycle hooks, synchronize, pane titles
  • Config examples (docs/configuration/examples.md): Working examples for each new feature
  • CLI docs: Pages for stop, new, copy, delete, plus updated load docs
  • Import docs: Updated with notes on new importer capabilities

Related issues

Test plan

  • uv run py.test passes
  • Tests for every new feature: stop, new, copy, delete, load flags, lifecycle hooks, config templating, builder, importers
  • Importer fixtures for edge cases (numeric names, YAML aliases, named panes, v1.x teamocil format)
  • Manual: tmuxp load with lifecycle hooks, --here, --debug, --set
  • Manual: tmuxp stop, tmuxp new, tmuxp copy, tmuxp delete
  • Manual: Import from tmuxinator/teamocil configs with new features

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 17, 2026

Codecov Report

❌ Patch coverage is 88.97281% with 73 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.13%. Comparing base (700a9b9) to head (13dc17e).

Files with missing lines Patch % Lines
src/tmuxp/cli/load.py 75.00% 22 Missing and 6 partials ⚠️
src/tmuxp/workspace/builder.py 86.88% 9 Missing and 7 partials ⚠️
src/tmuxp/workspace/importers.py 90.90% 3 Missing and 10 partials ⚠️
src/tmuxp/cli/copy.py 87.80% 3 Missing and 2 partials ⚠️
src/tmuxp/cli/delete.py 92.10% 2 Missing and 1 partial ⚠️
src/tmuxp/util.py 90.32% 1 Missing and 2 partials ⚠️
src/tmuxp/cli/import_config.py 90.00% 2 Missing ⚠️
src/tmuxp/cli/new.py 96.29% 1 Missing and 1 partial ⚠️
src/tmuxp/workspace/loader.py 97.95% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1025      +/-   ##
==========================================
+ Coverage   81.98%   84.13%   +2.14%     
==========================================
  Files          28       32       +4     
  Lines        2548     3120     +572     
  Branches      485      629     +144     
==========================================
+ Hits         2089     2625     +536     
- Misses        328      340      +12     
- Partials      131      155      +24     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

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

@tony tony force-pushed the parity branch 3 times, most recently from d64904e to db303db Compare March 21, 2026 10:53
@tony
Copy link
Copy Markdown
Member Author

tony commented Mar 21, 2026

Code review

Found 4 issues:

  1. Shell command injection via start_directory in --here mode. The --here path constructs f'cd "{start_directory}"' and sends it as keystrokes via send_keys. A start_directory containing " or ; can inject arbitrary shell commands into the pane. The non---here path safely passes start_directory as a structured argument to new_window(). Consider using shlex.quote() or passing the directory through tmux's -c flag instead.

if start_directory:
active_pane = window.active_pane
if active_pane is not None:
active_pane.send_keys(
f'cd "{start_directory}"',
enter=True,
)

  1. Missing doctests on command_copy, command_delete, command_new, command_stop (CLAUDE.md says "All functions and methods MUST have working doctests.")

def command_copy(
source: str,
destination: str,
parser: argparse.ArgumentParser | None = None,
color: CLIColorModeLiteral | None = None,
) -> None:
"""Entrypoint for ``tmuxp copy``, copy a workspace config to a new name."""

def command_delete(
workspace_names: list[str],
answer_yes: bool = False,
parser: argparse.ArgumentParser | None = None,
color: CLIColorModeLiteral | None = None,
) -> None:
"""Entrypoint for ``tmuxp delete``, remove workspace config files."""
color_mode = get_color_mode(color)

def command_new(
workspace_name: str,
parser: argparse.ArgumentParser | None = None,
color: CLIColorModeLiteral | None = None,
) -> None:
"""Entrypoint for ``tmuxp new``, create a new workspace config from template."""
color_mode = get_color_mode(color)
colors = Colors(color_mode)

def command_stop(
args: CLIStopNamespace,
parser: argparse.ArgumentParser | None = None,
) -> None:
"""Entrypoint for ``tmuxp stop``, kill a tmux session."""
color_mode = get_color_mode(args.color)
colors = Colors(color_mode)

  1. Missing doctest on _load_here_in_current_session (CLAUDE.md says "All functions and methods MUST have working doctests.")

tmuxp/src/tmuxp/cli/load.py

Lines 326 to 334 in db303db

def _load_here_in_current_session(builder: WorkspaceBuilder) -> None:
"""Load workspace reusing current window for first window.
Parameters
----------
builder: :class:`workspace.builder.WorkspaceBuilder`
"""
current_attached_session = builder.find_current_attached_session()
builder.build(current_attached_session, here=True)

  1. socket_name extracted from cli_args: "-L mysocket" is silently overwritten if the tmuxinator config also has an explicit socket_name key. The -L value is parsed at line 103, then unconditionally replaced at lines 105-106. Consider only overwriting if the explicit key exists and differs, or logging a warning about the conflict.

raw_args = workspace_dict.get("cli_args") or workspace_dict.get("tmux_options")
if raw_args:
tokens = shlex.split(raw_args)
flag_map = {"-f": "config", "-L": "socket_name", "-S": "socket_path"}
it = iter(tokens)
for token in it:
if token in flag_map:
value = next(it, None)
if value is not None:
tmuxp_workspace[flag_map[token]] = value
if "socket_name" in workspace_dict:
tmuxp_workspace["socket_name"] = workspace_dict["socket_name"]

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@tony tony force-pushed the parity branch 3 times, most recently from 60cd8fc to 92ba6f4 Compare March 23, 2026 01:28
@tony
Copy link
Copy Markdown
Member Author

tony commented Mar 23, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

Review scope (5 parallel agents):

  1. CLAUDE.md compliance audit — 4 minor findings, all below confidence threshold after scoring
  2. Shallow bug scan — 3 candidates; 1 debunked (shlex.quote + replace is valid POSIX quoting), 1 confirmed as correct tmuxinator parity, 1 edge case with adequate logging
  3. Git history context — no regressions found vs. prior commits
  4. Prior PR comments — color hierarchy and prompt patterns from PRs CLI Colors #1006/feat(load): animated progress spinner for tmuxp load #1020 verified compliant
  5. Code comments compliance — 1 stale docstring (pre-existing, not introduced by this PR)

Prior review rounds: 3 rounds of 3-model (Claude/Gemini/GPT) loom reviews found 16 issues, all fixed in subsequent commits on this branch.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@tony
Copy link
Copy Markdown
Member Author

tony commented Mar 28, 2026

Code review

Found 1 issue:

  1. _load_here_in_current_session and _dispatch_build use callable() as a doctest placeholder, providing zero documentation or testing value (CLAUDE.md says "All functions and methods MUST have working doctests. Doctests serve as both documentation and tests." and "If you cannot create a working doctest, STOP and ask for help")

tmuxp/src/tmuxp/cli/load.py

Lines 334 to 337 in aa69986

--------
>>> from tmuxp.cli.load import _load_here_in_current_session
>>> callable(_load_here_in_current_session)
True

tmuxp/src/tmuxp/cli/load.py

Lines 403 to 406 in aa69986

--------
>>> from tmuxp.cli.load import _dispatch_build
>>> callable(_dispatch_build)
True

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@tony
Copy link
Copy Markdown
Member Author

tony commented Mar 29, 2026

Code review

Found 2 issues:

  1. on_project_start fires unconditionally in --here mode even when the session already exists. The guard at line 793 (not here) causes the session-exists block to be skipped entirely, so execution always falls through to line 815 which runs on_project_start unconditionally. This contradicts commit 54afda7 which deliberately tightened on_project_start to fire on new session creation only.

tmuxp/src/tmuxp/cli/load.py

Lines 792 to 819 in 395b1e1

# Session-exists check — outside spinner so prompt_yes_no is safe
if builder.session_exists(session_name) and not append and not here:
_confirmed = not detached and (
answer_yes
or prompt_yes_no(
f"{cli_colors.highlight(session_name)} is already running. Attach?",
default=True,
color_mode=cli_colors.mode,
)
)
# Run on_project_restart hook — only when actually reattaching
if _confirmed:
if "on_project_restart" in expanded_workspace:
_hook_cwd = expanded_workspace.get("start_directory")
util.run_hook_commands(
expanded_workspace["on_project_restart"],
cwd=_hook_cwd,
)
_reattach(builder, cli_colors)
_cleanup_debug()
return None
# Run on_project_start hook — fires before new session build
if "on_project_start" in expanded_workspace:
_hook_cwd = expanded_workspace.get("start_directory")
util.run_hook_commands(
expanded_workspace["on_project_start"],
cwd=_hook_cwd,

  1. _validate_template_values doctest uses ... in the expected ValueError output without # doctest: +ELLIPSIS. It passes under pytest (global ELLIPSIS in pyproject.toml) but fails under python3 -m doctest. CLAUDE.md requires "Ellipsis for variable output: # doctest: +ELLIPSIS".

>>> _validate_template_values({"key": "foo: bar"})
Traceback (most recent call last):
...
ValueError: --set value for 'key' contains YAML-unsafe characters ...
"""

Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@tony tony force-pushed the parity branch 2 times, most recently from c8dae37 to 3246cb1 Compare April 4, 2026 18:53
@tony tony force-pushed the parity branch 5 times, most recently from 067bc1f to ac94a3d Compare June 6, 2026 16:02
@tony
Copy link
Copy Markdown
Member Author

tony commented Jun 6, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

@tony
Copy link
Copy Markdown
Member Author

tony commented Jun 6, 2026

Code review

Found 1 issue:

  1. _running_inside_pane compares the TMUX env socket (always str) against server.socket_path, which libtmux types as str | pathlib.Path | None. When a programmatic caller constructs Server(socket_path=pathlib.Path(...)), str != Path is always True, so the genuine self-pane is misclassified as not-self and the code falls through to respawn-pane -k — killing the tmuxp process mid-build, the exact failure this guard exists to prevent. Fix: compare against str(socket_path) (and/or normalize at the call site).

tmux = environ.get("TMUX")
tmux_pane = environ.get("TMUX_PANE")
if not tmux or not tmux_pane or pane_id is None:
return False
env_socket = tmux.split(",")[0]
if socket_path is not None and env_socket != socket_path:
return False
return tmux_pane == pane_id

Call site passing the un-normalized attribute:

if _here_pane is not None and _running_inside_pane(
_here_pane.pane_id,
socket_path=getattr(self.server, "socket_path", None),
environ=os.environ,
):

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@tony
Copy link
Copy Markdown
Member Author

tony commented Jun 6, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

tony added 4 commits June 7, 2026 06:28
…eamocil

Side-by-side comparison covering architecture, config keys,
CLI commands, hooks, and config file discovery across all three tools.
- Remove duplicate 'Attach on create' row in comparison table, keep
  corrected version with '(default: true)' near socket_path
- Annotate pre_tab as (deprecated) in comparison table
- Annotate startup_window as accepting name or index
- Fix pre_tab description: deprecated predecessor, not alias (it was
  renamed in tmuxinator, not aliased)
- Clarify startup_window renders as "#{name}:#{value}"
- tmuxinator min tmux is 1.8 (recommended), not 1.5; tmux 2.5 is
  explicitly unsupported
- teamocil has no documented min tmux version
- tmuxinator detach is via `attach: false` config or `--no-attach`
  CLI flag, not `-d` (which doesn't exist in tmuxinator)
- Fix "1.5+" to "1.8+" in architecture description (was already
  fixed in overview table but missed in prose)
- Clarify YAML anchors: tmuxinator enables via YAML.safe_load
  aliases param, not a config key
- Clarify tmuxinator edit is alias of new command
tony added 28 commits June 7, 2026 06:28
Support both space-separated (-L mysocket) and attached (-Lmysocket)
forms for -f, -L, and -S flags when parsing tmuxinator cli_args.
Previously only the space-separated form was recognized; attached
forms were silently dropped.
…cters

render_template() now rejects values containing colons, braces,
brackets, or newlines before substitution. These characters can
corrupt YAML document structure when injected as raw text before
parsing.
…broader

Add note explaining that tmuxp's --no-shell-command-before strips at
all levels (session/window/pane), which is intentionally broader than
tmuxinator's --no-pre-window that only targets the window chain.
Wrap the client-detached hook command in a #{session_attached} guard
so it only fires when the last client detaches. This prevents
premature cleanup in multi-client scenarios (pair programming, SSH
drops) where other clients are still attached.
The dict was re-created inside import_tmuxinator() on every call.
Move to module-level constant per Python convention for immutable
lookup tables.
Add require_pane_resolution parameter to get_session(). When True,
raises SessionNotFound instead of falling back to server.sessions[0]
when TMUX_PANE is unset/stale. tmuxp stop now uses strict mode to
prevent killing an unrelated session. Non-destructive commands
(shell, freeze) retain the fallback behavior.
…end_keys

Replace send_keys("export ...") and send_keys(window_shell) with tmux
primitives in --here mode:

- Environment: session.set_environment() + respawn-pane -e (inherited
  by new panes, no POSIX shell assumption)
- Shell replacement: respawn-pane -k (kills current process, starts
  fresh shell — no typing into foreground programs)
- Directory: respawn-pane -c (tmux primitive, no send_keys cd)

This eliminates all send_keys usage for infrastructure setup in --here
mode, matching teamocil's approach of using tmux primitives over
send_keys.  Fixes the fish/nu shell incompatibility and the "types
into vim" failure mode.

Closes #1031
Before calling respawn-pane -k, check pgrep -P <pane_pid> for child
processes. If the shell has running children (background jobs,
foreground programs), log a WARNING so users know their processes
will be terminated. Gracefully handles missing pgrep.
…recovery

New builder tests (NamedTuple + test_id pattern):
- HereRespawnFixture: parametrized over 4 scenarios (dir-only, env-only,
  dir-and-env, nothing-to-provision) verifying PID changes on respawn,
  directory provisioning, and session environment
- test_here_mode_respawn_multiple_env_vars: 3 env vars via set_environment
- test_here_mode_respawn_warns_on_running_processes: background sleep job
  triggers pgrep WARNING before respawn-pane -k
- test_here_mode_no_warning_when_pane_idle: idle pane produces no warning

New load CLI tests (NamedTuple + test_id pattern):
- HereErrorRecoveryFixture: parametrized over 2 scenarios verifying
  --here mode skips (k)ill option (choices=[a,d], default=d) while
  normal mode retains it (choices=[k,a,d], default=k)
why: on_project_start had been triggered before dispatch, so it also ran for paths that reused an existing session. That made --here rebuilds and interactive append flows execute a hook documented as new-session-only.

what:
- Move on_project_start execution into the attached and detached new-session load paths
- Keep --here rebuilds inside tmux and append flows from invoking the hook
- Preserve the outside-tmux --here fallback behavior, which still creates a new session
- Add dispatch tests for attached, detached, append, and here routing
- Add an on_project_exit guard assertion and fix the loader doctest ellipsis
- Update the related load, comparison, and configuration docs to match current behavior
why: Keep the changelog aligned with the lifecycle behavior shipped on
this branch so readers do not infer broader hook semantics than the
code implements.

what:
- Document on_project_exit as running when the last client detaches
- Match the guarded client-detached hook behavior in WorkspaceBuilder
…ures

why: --here and --append reuse an existing tmux session. Startup failures
must abort without destroying that live session, and duplicate target names
must stop before any plugin hooks or before_script side effects run.
what:
- track whether build created the session before cleaning it up on failure
- move the --here duplicate-session check ahead of startup hooks and script execution
- add builder coverage for reused-session failures and pre-hook rename conflicts
why: --here is a current-window workflow. Accepting multiple workspace files
silently changes behavior for earlier entries and leaves behind unexpected
sessions instead of failing fast.
what:
- reject --here when more than one workspace file is provided
- clarify the parser help text for the single-workspace contract
- add CLI coverage that exits before any workspace is loaded
- document the single-workspace restriction in the load guide
…port

why: tmuxinator numeric startup_window and startup_pane values are tmux
indices, not Python list offsets. Importing them as list positions changes
which window or pane receives focus and breaks compatibility with existing
configs, especially when base-index or pane-base-index are nonzero.
what:
- resolve numeric startup targets against tmux base-index and pane-base-index
- read live tmux index settings in the tmuxinator import CLI path
- add importer and CLI coverage for base-index aware conversion and fallback
…nd env intact

why: append and --here reuse a live tmux session rather than creating a tmuxp
owned session. Writing lifecycle hooks or stop metadata onto that reused
session can overwrite unrelated teardown behavior, and copying first-pane
environment into session state makes later windows inherit variables they were
never meant to see.
what:
- limit on_project_exit, on_project_stop, and start_directory session metadata
to sessions created by the current build
- keep --here first-pane provisioning local to respawn-pane instead of the
session environment
- add reused-session and non-leaking here-mode tests around hooks and env
why: The unreleased entry removed the KEEP THIS PLACEHOLDER insertion
anchor and skipped the lead paragraph and fixed subheadings that every
published release uses, while the importer section leaked internal
key-mapping mechanics into user-facing notes.
what:
- Restore the placeholder block; entries land below the END marker
- Open with a release lead paragraph; group deliverables under What's new
- Collapse importer key-mapping bullets into one prose deliverable
- Cross-reference cli-stop, top-level, cli-import, and comparison docs
- Drop tmux mechanism asides (select-pane -T, client-detached hook)
why: Dated "as of" claims rot immediately (AGENTS.md brittle-references
rule); the page read as three months stale despite ongoing updates.
what:
- Remove the Last updated line; the Version table row remains the
  durable anchor for which tmuxinator/teamocil releases were compared
…rror recovery test

why: AGENTS.md testing guidelines require monkeypatch over unittest.mock;
the MagicMock builder also hid which attributes _dispatch_build actually
touches, weakening the test's contract.
what:
- Stub the failing load paths with typed raising functions matching the
  real loader signatures, registered via monkeypatch.setattr
- Stand in for the builder with a minimal DummyBuilder exposing only
  session, mirroring the dispatch-loaders test's existing pattern
- Promote the exc import to module level alongside cli
why: AGENTS.md requires working doctests; both entry points were
substantially rewritten for parity (new keyword params, v1.x support)
yet only their helpers gained examples, and the new base_index /
pane_base_index parameters were undocumented.
what:
- Add Examples covering session_name, window, and pane conversion for
  both importers, plus the pre -> on_project_start hook mapping
- Document base_index and pane_base_index in the Parameters section
…ost-build fan-out

why: synchronize: after desugars to options_after synchronize-panes=on,
and tmux mirrors send-keys input across panes while that option is on.
Applying options_after first made every shell_command_after entry (and
clear) run once per pane per send — four executions in a two-pane
window instead of two.
what:
- Reorder config_after_window: shell_command_after, then clear, then
  options_after
- Add test asserting each pane runs the after-command exactly once
  while synchronize-panes still ends up enabled
why: --here is normally invoked from the pane being reused, so
respawn-pane -k killed the foreground tmuxp process mid-build: pane
commands were never sent and remaining windows never built. The
existing tests drove the builder from outside the pane, so the
self-kill path was unreachable in CI.
what:
- Add _running_inside_pane() predicate (TMUX/TMUX_PANE plus socket
  comparison when the server socket path is known)
- Self-pane fallback: environment via session set-environment
  (inherited by the panes the build creates), directory via quoted cd
  send-keys, window_shell skipped with a warning; other panes keep
  the respawn-pane path
- Scrub ambient TMUX_PANE in builder tests so the developer's own
  pane id cannot collide with fresh test-server pane ids
- Document both provisioning paths in the --here note
…omparison

why: libtmux Server.socket_path may hold a pathlib.Path, and str != Path
is always True, so _running_inside_pane misclassified a genuine
self-pane as not-self for programmatic callers — falling through to
respawn-pane -k and killing the tmuxp process mid-build, the exact
failure the guard exists to prevent.
what:
- Compare the TMUX env socket against str(socket_path); widen the
  parameter to accept pathlib.Path and add a Path doctest case
- Document that socket_path=None skips the cross-server check and errs
  toward the kill-free provisioning path
- Rescope the respawn rationale comment to the respawn branch; the
  block header no longer claims "no typing into foreground programs"
  above the self-pane branch that sends a cd
why: find_workspace_file resolves any existing direct path, so
`tmuxp delete -y README.md` unlinked an arbitrary file. The command's
contract is deleting workspace configs; destruction happened before
any validation.
what:
- Require the resolved path's extension to be .yaml/.yml/.json before
  os.remove; refuse with a warning and exit code 1 otherwise
- Add parametrized refusal tests (markdown, plain text) asserting the
  file survives and the exit code
- Add a refusal doctest and a docs note on the constraint
why: The 120s subprocess timeout silently killed long-running hooks
(database setup, docker compose teardown) and continued anyway —
on_project_stop could leave cleanup half-done while the session was
still killed afterward. The hook docs promise the commands run at
lifecycle boundaries with no mention of a limit, and tmuxinator (the
parity target) imposes none.
what:
- Drop the timeout and the TimeoutExpired handler; OSError handling
  and the non-zero-exit warning stay
- Log a structured INFO record when hooks start, so long waits are
  attributable
- Add a test pinning the no-time-limit contract and a docs note that
  hooks block until completion (output captured; Ctrl-C interrupts)
…sts handling

why: The session-exists guard skipped its prompt-and-reattach path for
here=True unconditionally, but the outside-tmux fallback to a normal
attach happened later in _dispatch_build — so a running session reached
Server.new_session() and crashed with libtmux's TmuxSessionExists,
which the TmuxpException recovery prompt does not catch.
what:
- Normalize here=False (with the relocated warning) at the top of
  load_workspace when no TMUX client is present, so the existing-session
  prompt, on_project_restart, and attach flow behave like a normal load
- Drop the now-unreachable outside-tmux fallback from _dispatch_build
  and document that its here path requires a tmux client
- Replace the dispatch-level fallback fixture with a load_workspace-level
  test: existing session + --here outside tmux now prompts to attach
  instead of crashing
…ively

why: The new deletion guard compared extensions case-sensitively while
finders, search, and ls all lowercase before matching, so an
explicitly-typed .YAML path was refused on case-insensitive
filesystems even though tmuxp load accepts it.
what:
- Lowercase the splitext result before checking
  VALID_WORKSPACE_DIR_FILE_EXTENSIONS, matching the discovery code's
  established pattern
- Add parametrized tests deleting uppercase .YAML/.JSON paths
why: The hook start record tagged a user shell string with tmux_cmd,
whose schema contract is "tmux command line" and which downstream
consumers may filter on; the message also used progressive tense
against the past-tense-for-events standard.
what:
- Log "hook commands started" with a dedicated tmux_hook_cmd key
- Register tmux_hook_cmd in the AGENTS.md core key table
- Test the INFO record's key, value, message, and the absence of
  tmux_cmd on the hook path
why: The help text claimed resolution parity with tmuxp load without
mentioning that delete refuses non-workspace files, implying
identical accept/reject behavior between the two commands.
what:
- State in the --help intro that only .yaml/.yml/.json files are
  deleted, matching the docs page note
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.

here mode level 1: match teamocil — use set_environment + respawn-pane, keep send_keys cd only

1 participant