Skip to content

feat(dev): add sub-agent orchestration for tmux#420

Open
basnijholt wants to merge 22 commits intomainfrom
read-dev-md
Open

feat(dev): add sub-agent orchestration for tmux#420
basnijholt wants to merge 22 commits intomainfrom
read-dev-md

Conversation

@basnijholt
Copy link
Owner

Summary

  • Add orchestration primitives for spawning, monitoring, and interacting with AI coding agents in tmux panes
  • New commands: dev poll, dev output, dev send, dev wait for agent lifecycle management
  • Two-tier completion detection: Claude Code Stop hooks (.claude/DONE sentinel) with fallback to output quiescence (SHA-256 hash comparison)
  • Agent state tracking via JSON files at ~/.cache/agent-cli/<repo-slug>/agents.json
  • Background poller daemon for pre-computing agent status

New modules

  • tmux_ops.py — low-level tmux pane operations (open, capture, send, exists)
  • agent_state.py — JSON-based state tracking, hook injection, name generation
  • poller.py — background daemon + poll_once() and wait_for_agent() business logic

Modified

  • _launch_agent() tracks agents in tmux via stable pane IDs (%N), auto-starts poller
  • dev agent gains --tab (launch in new tmux tab) and --name flags
  • Multiple agents per worktree supported (e.g., implementer + reviewer)

Test plan

  • 36 new tests in tests/dev/test_orchestration.py covering tmux_ops, agent_state, all CLI commands, hook injection
  • All 1006 tests pass
  • pre-commit (ruff, mypy, jscpd, pylint) passes

basnijholt and others added 22 commits February 9, 2026 17:58
Add primitives for spawning, monitoring, and interacting with AI coding
agents in tmux panes. This enables workflows like: spawn an implementer,
wait for completion, spawn a reviewer in the same worktree, read review
output, and send corrections back.

New modules:
- tmux_ops.py: low-level tmux pane operations (open, capture, send, exists)
- agent_state.py: JSON-based state tracking for spawned agents
- poller.py: background daemon for polling agent status

New CLI commands:
- dev poll: show status of all tracked agents (table or --json)
- dev output: capture and display agent's terminal output
- dev send: send text/commands to an agent's tmux pane
- dev wait: block until an agent reaches done/dead status

Modified:
- _launch_agent(): tracks agents in tmux via pane IDs, auto-starts poller
- dev agent: added --tab (launch in new tmux tab) and --name flags
- Completion detection via Claude Code Stop hooks (.claude/DONE sentinel)
  with fallback to output quiescence (SHA-256 hash comparison)
Move business logic out of cli.py into appropriate modules:

- inject_completion_hook → agent_state.py (config/state management)
- is_tmux() → agent_state.py (environment check)
- wait_for_agent() → poller.py (blocking wait with timeout)
- _update_agent_status() → poller.py (shared by poll_once and wait)

poll_cmd now calls poller.poll_once() instead of duplicating the
polling logic inline. wait_cmd delegates to poller.wait_for_agent()
which raises TimeoutError/KeyError instead of mixing business logic
with CLI exit codes.
Move agent/editor resolution, launching, and config helpers into
launch.py. Move worktree cleanup business logic into cleanup.py.
This reduces cli.py by ~400 lines, keeping it focused on CLI
presentation (option parsing, table/JSON formatting) while business
logic lives in dedicated modules.
… cli.py

- Create _output.py with shared console helpers (_error, _success, _info, _warn),
  fixing the circular import between launch.py and cli.py
- Move orchestration commands (poll, output, send, wait) to orchestration.py
- Decompose `new` command with _resolve_branch_name and _setup_worktree_env helpers
- Update launch.py to import from _output instead of cli
- Update test mock paths to match new module locations
- Stop aliasing imports with underscores in cli.py (import directly)
- Make launch.py internal helpers private: _is_ssh_session,
  _format_env_prefix, _create_prompt_wrapper_script
- Update test mock paths and imports to match
Rename _error/_success/_info/_warn to error/success/info/warn in
_output.py. Fix local variables in cli.py that shadowed the newly
public names (success, error) causing TypeError at runtime.
Keep top-level imports for cleanup and _output helpers (from main),
retain --tab agent tracking feature (from read-dev-md branch).
- Change `track` default to False in launch_agent to prevent dev new
  from silently attempting tmux tracking
- Fix follow mode in dev output to show only new lines instead of
  reprinting the entire buffer
- Extract shared _check_agent_status helper to deduplicate polling logic
  between poll_once and wait_for_agent
- Remove dead daemon code (start_poller, run_poller_daemon, stop_poller,
  is_poller_running, __main__ block)
- Use repo_root from _lookup_agent in wait_cmd instead of calling
  _ensure_git_repo twice
Just switch to the tmux pane instead.
Organizes the dev help output into logical sections:
Environments, Launch, Info, Setup, Agent Orchestration.
- Fix inject_completion_hook to use new Claude Code hook format
  (hooks entries must be {type, command} objects, not strings)
- Fix is_tmux() to also detect reachable tmux server (not just $TMUX env)
- Fix inject_completion_hook to handle non-dict JSON in settings file
- Fix quiescence counter reset on transient capture_pane failures
- Increase quiescence threshold from 2 to 6 polls (30s at default interval)
- Remove unused unregister_agent function
Use .claude/settings.local.json instead of .claude/settings.json to
avoid dirtying tracked files in the worktree. The local settings file
is automatically gitignored by Claude Code.
…rites

Fix save_state race condition where concurrent writers used the same
temp file name, causing FileNotFoundError. Use PID-suffixed temp files
instead. Guard load_state and inject_completion_hook against non-dict
JSON values that would crash on .items()/.setdefault() calls.
Track output hash and consecutive unchanged polls in the state file so
poll_once can detect when an agent's output has stabilized. Agents
without completion sentinels (e.g., codex) now transition to "quiet"
status after 6 consecutive polls with identical output, instead of
showing "running" forever.
Replace poll-count-based quiescence with time-based: track when output
last changed and require 10s of unchanged output before marking quiet.
Prevents false positives from rapid polling.
…rminal

Use DONE-<agent-name> sentinel files instead of a shared DONE file to
avoid false positives when multiple Claude sessions share a worktree.
Also decouple tracked launch from terminal type — when track=True, always
use tmux_ops directly (is_tmux() already validated at the CLI layer),
so agents launched from zellij/kitty are still tracked via tmux.
…ve agent history

- Move is_tmux() from agent_state.py to tmux_ops.py where it belongs
  alongside other tmux operations
- Inline _state_dir() into _state_file_path() (was a single-use wrapper)
- Stop purging terminal agents in register_agent() so completed agents
  remain visible in dev poll after launching new ones
- Add noqa: S607 to open_window_with_pane_id for consistency
- Restore deep-nesting config test for _flatten_nested_sections
- Revert unrelated pyproject.toml ruff ignore removals (COM812, D203, D213)
Address Codex review findings:

1. (High) load_state now skips non-dict agent entries instead of
   crashing with AttributeError on malformed cache files.
2. (Medium) inject_completion_hook skips non-dict entries in the Stop
   hooks list instead of crashing with AttributeError.
3. (Medium) dev agent --tab now checks TMUX env var (inside a session)
   instead of is_tmux() (any reachable server), preventing silent
   untracked launches from non-tmux terminals.
4. (Low) Add docs for dev poll, dev output, dev send, dev wait to
   docs/commands/dev.md.
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