* feat: implement runtime and workspace plugins (tmux, process, worktree, clone)
Implement all 4 runtime/workspace plugins for the agent orchestrator,
replacing the stub files with full implementations of the Runtime and
Workspace interfaces from @agent-orchestrator/core.
- runtime-tmux: tmux session lifecycle with busy detection, wait-for-idle
message delivery, capture-pane output, and load-buffer for long messages
- runtime-process: child process management with rolling output buffer,
graceful SIGTERM/SIGKILL shutdown, and stdin message delivery
- workspace-worktree: git worktree create/destroy/list with symlink support
for shared resources and postCreate hook execution
- workspace-clone: git clone --reference for fast isolated clones with
postCreate hooks
Closes INT-1328
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address all PR review comments on runtime/workspace plugins
Fixes for all 13 review issues:
1. Move processes Map inside create() for per-instance isolation (#1)
2. Wrap stdin.write in promise with error/backpressure handling (#2)
3. Use child.once("exit") instead of on("exit") to prevent leaks (#3)
4. Use child.once("exit") in destroy() timeout to prevent leaks (#4)
5. Add assertValidSessionId() with /^[a-zA-Z0-9_-]+$/ regex (#5)
6. Single capture-pane call in isBusy() to avoid TOCTOU (#6)
7. Throw error when sendMessage sends to busy session after timeout (#7)
8. Use crypto.randomUUID() for temp file names to avoid collisions (#8)
9. Document that postCreate commands run with full shell privileges (#9)
10. Inspect worktree add error — only retry on "already exists" (#10)
11. Delete orphaned branch after worktree remove in destroy() (#11)
12. Log warning for corrupted clones in list() instead of silent skip (#12)
13. Return "not running" attach info for exited processes (#13)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: harden runtime/workspace plugins (spawn errors, stdin races, git injection)
- runtime-process: catch spawn errors with setImmediate pattern, prevent
double resolve/reject in sendMessage with done-flag, add late error
handler to prevent unhandled crashes, eliminate non-null assertions
- runtime-tmux: guard config.environment with ?? {} fallback
- workspace-worktree: add -- separator to git checkout/branch commands
to prevent option injection, guard branch deletion to feature branches
only (must contain /), ensure parent dirs exist before symlink creation
- workspace-clone: add -- separator to git checkout commands, suppress
no-console lint for expected diagnostic warning
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address codex review — spawn race, path traversal, temp file perms
- runtime-process: replace setImmediate with spawn/error event listeners
to fix race where create() returns handle for failed process; document
intentional shell:true usage
- runtime-tmux: restrict temp file permissions to 0o600
- workspace-worktree: validate projectId/sessionId as safe path segments,
reject absolute/traversal symlink paths, verify resolved targets stay
within workspace
- workspace-clone: validate projectId/sessionId as safe path segments
in both create() and list()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address all review comments and fix CI
- workspace-clone/worktree: remove incorrect -- from git checkout
(switches to path mode, breaking branch operations)
- workspace-worktree: fix prefix collision in list() by appending /
to startsWith check (foo no longer matches foobar)
- runtime-tmux: use named tmux buffers (-b flag) to prevent concurrent
sendMessage calls from overwriting each other's paste content
- runtime-process: check signalCode in addition to exitCode for
accurate liveness detection of signal-killed processes
- runtime-process: reject duplicate session IDs to prevent orphaned
child processes from overwritten map entries
- runtime-tmux: anchor $ in busy detection to line start (^\$\s) to
avoid false idle detection from dollar signs in output
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: forward plugin config through registry so workspace dirs take effect
The plugin registry was calling plugin.create() with no arguments,
silently ignoring configured worktreeDir/cloneDir settings. Added
extractPluginConfig() to map orchestrator config to per-plugin config,
and updated register()/loadBuiltins() to pass config through.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: clean up orphaned worktrees/clones on checkout failure, remove risky branch -D
- Worktree: remove git branch -D from destroy() — the "/" heuristic
could delete pre-existing local branches unrelated to the workspace
- Worktree: wrap fallback checkout in try/catch, clean up orphaned
worktree if checkout fails
- Clone: wrap fallback checkout in try/catch, rmSync the orphaned clone
directory if both checkout attempts fail
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: clean up partial clone directory when git clone fails
Wrap the git clone call in try/catch so a failed clone removes any
partial clonePath left on disk, preventing orphaned directories that
block retries for the same sessionId.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: remove busy detection from tmux runtime
Busy/idle detection is an agent-layer concern, not a runtime concern.
Each agent (Claude Code, Codex, Aider) has its own native mechanism for
checking activity status. The tmux runtime should only manage sessions
and send messages immediately without polling.
Removed: isBusy() heuristics, sleep-based polling loop, sentWhileBusy
error. sendMessage() now sends immediately.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: clean up tmux session when launch command send-keys fails
If send-keys fails after new-session succeeds, the orphaned tmux
session was left running and unmanaged. Now wraps send-keys in
try/catch that kills the session before rethrowing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: prevent clone error handler from deleting pre-existing workspace
Add early existence check before git clone so create() fails
immediately if clonePath already exists, rather than cloning into it,
failing, and then deleting the pre-existing workspace in the catch block.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: clean up leaked tmux buffer when paste-buffer fails
The -d flag on paste-buffer only deletes the named buffer on success.
If paste fails, the buffer persists in the tmux server. Added
delete-buffer in the finally block to ensure cleanup regardless of
paste outcome.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: prevent orphan processes on duplicate sessionId, auto-remove exited sessions
- Move processes.has() check before spawn() so duplicate sessionIds are
rejected before any child process is created
- Auto-remove exited sessions from the process map in the exit handler
so the sessionId can be reused without manual destroy()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: register process in map before exit handler to prevent stale entries
A fast-exiting child could fire the exit event before processes.set()
ran, causing delete() to no-op and set() to insert a permanently stale
entry. Moving set() before the exit handler registration ensures
delete() always finds the entry.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: atomic session reservation and immediate exit handler registration
- Reserve map slot synchronously (no await gap between has() and set())
so concurrent create() calls cannot both pass the duplicate check
- Register exit handler immediately after spawn(), before any await,
so fast-exiting processes cannot miss cleanup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: kill process group on destroy, use literal mode for tmux send-keys
- runtime-process: use process.kill(-pid) to kill the entire process
group, not just the shell. Added detached:true so child gets its own
process group. Falls back to child.kill() if group kill fails.
- runtime-tmux: add -l flag to send-keys for short messages so text
like "Enter" or "Space" is sent literally, not as key names.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add comprehensive unit and integration tests for all 4 plugins
Add 136 unit tests and 35 integration tests covering runtime-tmux,
runtime-process, workspace-worktree, and workspace-clone plugins.
Also adds 13 unit tests for the core plugin-registry.
Unit tests (136 total):
- runtime-tmux (25): create/destroy, send-keys modes, getOutput, isAlive, getMetrics
- runtime-process (40): spawn lifecycle, SIGKILL escalation, output buffering, isolation
- workspace-worktree (40): create/destroy, tilde expansion, branch fallback, symlinks
- workspace-clone (31): create/destroy, --reference clone, pre-existence check, cleanup
Integration tests (35 total):
- runtime-tmux (8): full lifecycle with real tmux sessions
- runtime-process (11): full lifecycle with real child processes
- workspace-worktree (8): real git worktree operations
- workspace-clone (8): real git clone operations
Plugin-registry tests (13):
- register/get/list, config forwarding, slot isolation, loadBuiltins, loadFromConfig
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: guard against null process in runtime-process methods
ProcessEntry.process is null between reservation and spawn completion.
Add explicit null checks in destroy(), sendMessage(), isAlive(), and
getAttachInfo() to prevent TypeError if called during that window.
Addresses bugbot review comment about transient null process crash.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: buffer partial lines in runtime-process output capture
Stream chunks split at arbitrary boundaries, so splitting on "\n" and
pushing each piece as a complete line corrupts output when a line spans
two chunks. Buffer the trailing partial line and only push complete
(newline-terminated) lines. Flush remaining partial on process exit.
Addresses bugbot review comment about incorrect output chunk splitting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: correct vitest peer dep resolution in pnpm-lock.yaml
The lockfile referenced vitest@3.2.4 without jiti/jsdom/lightningcss
peer deps, but that resolution didn't exist in the packages section.
Update to match the full resolution available on main.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use per-stream partial buffers in runtime-process output capture
stdout and stderr shared one partial-line buffer, so interleaved chunks
from different streams could be concatenated into corrupted lines. Each
stream now gets its own closure with an independent partial buffer.
Addresses bugbot review comment about mixed stream chunk corruption.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Phase 0 complete. Establishes:
- pnpm workspace with 18 packages (core, cli, web, 15 plugins)
- Complete type definitions in packages/core/src/types.ts defining
all 8 plugin slot interfaces (Runtime, Agent, Workspace, Tracker,
SCM, Notifier, Terminal) + core service interfaces
- YAML config loader with Zod validation and sensible defaults
- Plugin registry with built-in discovery
- CLAUDE.md with conventions for spawned agents
All agents can now branch from main and implement their assigned
packages against the interfaces defined in types.ts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>