feat(dev): add sub-agent orchestration for tmux#420
Open
basnijholt wants to merge 22 commits intomainfrom
Open
feat(dev): add sub-agent orchestration for tmux#420basnijholt wants to merge 22 commits intomainfrom
basnijholt wants to merge 22 commits intomainfrom
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
dev poll,dev output,dev send,dev waitfor agent lifecycle management.claude/DONEsentinel) with fallback to output quiescence (SHA-256 hash comparison)~/.cache/agent-cli/<repo-slug>/agents.jsonNew modules
tmux_ops.py— low-level tmux pane operations (open, capture, send, exists)agent_state.py— JSON-based state tracking, hook injection, name generationpoller.py— background daemon +poll_once()andwait_for_agent()business logicModified
_launch_agent()tracks agents in tmux via stable pane IDs (%N), auto-starts pollerdev agentgains--tab(launch in new tmux tab) and--nameflagsTest plan
tests/dev/test_orchestration.pycovering tmux_ops, agent_state, all CLI commands, hook injection