* feat: implement CLI with all commands (init, status, spawn, session, send, review-check, dashboard, open)
Implement the full `ao` CLI using Commander.js with 9 commands:
- ao init: Interactive setup wizard that creates agent-orchestrator.yaml
- ao status: Colored terminal table of all sessions (branch, activity, PR, CI)
- ao spawn <project> [issue]: Spawn single agent session (worktree + tmux + agent)
- ao batch-spawn <project> <issues...>: Batch spawn with duplicate detection
- ao session ls|kill|cleanup: Session management subcommands
- ao send <session> <message>: Smart message delivery with busy detection/retry
- ao review-check [project]: Scan PRs for review comments, trigger agent fixes
- ao dashboard: Start the Next.js web dashboard
- ao open [target]: Open session(s) in terminal tabs
Shared libraries:
- lib/shell.ts: Shell command helpers (tmux, git, gh wrappers)
- lib/metadata.ts: Flat metadata file read/write (backwards-compatible format)
- lib/format.ts: Terminal formatting (colors, tables, banners)
All commands code against core interfaces and flat metadata files,
matching the behavior of the reference bash scripts in scripts/.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address PR review — extract shared helpers, fix bugs, add tests
- Extract getTmuxSessions/getTmuxActivity to lib/shell.ts (was duplicated in 5 files)
- Fix readMetadata unsafe cast: return Partial<SessionMetadata> instead
- Fix dashboard webDir path: resolve from package root, not dist/
- Fix dashboard spawn error handling: add child.on("error")
- Fix getNextSessionNumber: escape regex metacharacters in prefix
- Fix fallback worktree creation: use existing branch name, not detached HEAD
- Fix post-create hooks: run before agent launch, not after
- Fix open --new-window: pass option through to openInTerminal
- Fix cleanup dry-run: separate summary message for dry-run mode
- Add Claude session introspection to status command (TTY→PID→CWD→JSONL)
- Add vitest test suite: 72 tests covering commands and lib utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address all PR review issues, add lint/format compliance
Review fixes:
- Extract getTmuxSessions/getTmuxActivity to lib/shell.ts (was duped in 5 files)
- Fix readMetadata: return Partial<SessionMetadata> instead of unsafe cast
- Fix dashboard webDir: use createRequire + __dirname for ESM compat
- Fix dashboard spawn: add child.on("error") handler
- Fix getNextSessionNumber: escape regex metacharacters in prefix
- Fix fallback worktree: use existing branch name, not detached HEAD
- Fix post-create hooks: run before agent launch, not after
- Fix open --new-window: pass option through to openInTerminal
- Fix cleanup dry-run: separate summary message for dry-run mode
- Add Claude session introspection to status (TTY→PID→CWD→JSONL)
- Add port validation in init wizard
- Add clarifying comment for tmux C-u send-keys
Lint/format:
- Add no-console override for CLI package in eslint config
- Fix duplicate imports (node:fs, @agent-orchestrator/core)
- Fix unused variable in send test
- Apply prettier formatting across all files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address codex review — fd leak, input validation, security hardening
- Wrap openSync/readSync in try/finally to prevent fd leak on error
- Add timeout NaN/<=0 guard in send command
- Add port validation (1-65535) in dashboard command
- Sanitize issueId for git-ref-safe branch names
- Add --project validation in session ls/cleanup
- Fix batch-spawn same-run duplicate detection
- Replace shell interpolation with safe ps + JS filtering
- Wrap temp file usage in try/finally for cleanup on error
- Read only tail of JSONL files to avoid large file loads
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address remaining PR review comments
- Use `=== null` instead of falsy check for git worktree result (spawn.ts)
- Wrap spinner in try/catch so it stops on error (spawn.ts)
- Use exact match instead of substring for issue duplicate detection (metadata.ts)
- Check git worktree remove result before printing success (session.ts)
- Skip banner/headers/footer when --json is passed (status.ts)
- Add error handler on browser-open spawn (dashboard.ts)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address bugbot review — interrupt before send, error resilience, worktree validation
- review-check: send C-c + C-u to interrupt busy agent and clear input before
delivering fix prompt, matching the reference bash script behavior
- review-check: wrap per-session send in try/catch so one failure doesn't abort
remaining sessions
- spawn: check worktree creation return value and throw on failure instead of
continuing with non-existent worktree path
- Update test mock for detached worktree to return success
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address 4 bugbot findings — status type, TTY match, retry safety, comment counting
- spawn: write status "spawning" (not "starting") to match SessionStatus type
- status: use column-based exact TTY match to prevent pts/1 matching pts/10
- send: use null-safe tmux() helper in retry loop instead of throwing exec()
- review-check: use GraphQL reviewThreads to count only unresolved comments,
preventing fix prompts on PRs with all threads resolved
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: GraphQL variable passing and env var name sanitization
- review-check: use -F flags for GraphQL variables instead of string
interpolation to prevent injection via repo config values
- spawn: sanitize env var name by replacing non-alphanumeric chars with
underscores (my-app → MY_APP_SESSION, not MY-APP_SESSION)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add -l flag to tmux send-keys and remove dead activityIndicator code
- Add literal (-l) flag to send-keys so user text isn't interpreted as
tmux key names (e.g. "Enter", "Escape")
- Remove unused activityIndicator function from format.ts and its tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move agent-specific logic from CLI into agent plugins
detectActivity, introspect, and getLaunchCommand now live in the agent
plugins (claude-code, codex, aider) instead of being hardcoded in the
CLI commands. The Agent interface's detectActivity takes a plain string
of terminal output instead of a Session object, making plugins trivially
unit-testable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use -f for string GraphQL vars and -l flag for tmux send-keys
Addresses two bugbot findings in review-check.ts:
1. Use -f (string) instead of -F (typed) for owner/name GraphQL
variables to prevent numeric coercion of repo names.
2. Use -l (literal) flag with send-keys and separate Enter call,
matching the established tmux safety pattern in send.ts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: guard empty terminal output in lifecycle-manager and deduplicate send.ts
- lifecycle-manager: skip detectActivity when terminal output is empty
to prevent false "killed" state when runtime probe returns no data
- send.ts: collapse identical isBusy/isProcessing into single isActive
- tests: use non-empty mock terminal output so detectActivity is exercised
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: validate message before side effects, remove dead updateMetadataField
- send.ts: move message validation before idle-wait loop and C-u clear
so running `ao send session` with no message exits immediately without
touching the tmux session
- metadata.ts: remove exported updateMetadataField (unused in production)
- metadata.test.ts: remove corresponding tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add -l flag and separate Enter for spawn prompt send-keys
Use literal flag (-l) when sending the initial prompt via tmux
send-keys in spawn.ts, and send Enter as a separate call. This
matches the pattern used in send.ts and review-check.ts and prevents
tmux from interpreting special characters in the issue ID.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: strict session prefix matching and validate project before use
- session-utils: add matchesPrefix() using regex ^prefix-\d+$ to prevent
cross-project misattribution with overlapping prefixes (e.g. "app" vs
"app-v2")
- Replace all startsWith(`${prefix}-`) calls across status.ts,
review-check.ts, session.ts, and open.ts with matchesPrefix()
- status.ts, review-check.ts: move project validation before the
projects map construction so invalid project IDs exit before any
access to undefined config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: deduplicate escapeRegex and add -l flag to spawn send-keys
- Export escapeRegex from session-utils.ts, remove duplicate from spawn.ts
- Add -l (literal) flag to tmux send-keys for postCreate hooks and
launch command, with Enter sent separately
- Prevents tmux from interpreting key names in user config or agent
commands
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: update detectActivity to sync string signature across all plugins
After rebase on main, all agent plugins and their tests still had the
old async detectActivity(session: Session) signature. Updated to the
new sync detectActivity(terminalOutput: string): ActivityState across:
- agent-claude-code, agent-codex, agent-aider, agent-opencode plugins
- All plugin unit tests (test terminal output patterns, not process state)
- All integration tests (capture tmux pane output before calling)
- status.ts: use getSessionInfo() instead of nonexistent introspect()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: integration tests expect idle (not exited) from detectActivity after process exit
detectActivity is a pure terminal-text classifier that returns "idle" for
empty/shell-prompt output. Process exit detection is handled by isProcessRunning.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: check last-line prompt before historical activity patterns in detectActivity
Reorder classifyTerminalOutput to check the last line for a prompt
character before scanning the full buffer for activity indicators like
"Reading" or "Thinking". This prevents historical output in the capture
buffer from causing false "active" results when the agent is idle.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: update pnpm-lock.yaml for web dashboard dependencies
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: preserve state on getOutput failure, check permission prompts before activity indicators
- lifecycle-manager: remove .catch(() => "") from getOutput so failures
reach the outer catch block that preserves stuck/needs_input states
- classifyTerminalOutput: check waiting_input prompts against bottom of
buffer before full-buffer active indicators, preventing historical
"Thinking"/"Reading" text from overriding current permission prompts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: detect agent exit via isProcessRunning, remove redundant active checks
- lifecycle-manager: replace dead "exited"/"blocked" branches with
isProcessRunning check when detectActivity returns "idle" — correctly
detects agent process exit inside a still-alive tmux session
- classifyTerminalOutput: remove redundant explicit active-indicator
checks that returned the same "active" as the unconditional default
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: wrap killSession in try-catch so cleanup loop continues on failure
One session's kill failure (e.g. archiveMetadata fs error) no longer
aborts the entire cleanup loop, matching the pattern used by batch-spawn
and review-check.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add comprehensive unit and integration tests for CLI
Unit tests (6 new files, 61 tests):
- session-utils: escapeRegex, matchesPrefix, findProjectForSession
- plugins: getAgent, getAgentByName lookup and error cases
- shell: exec, execSilent, tmux, git, gh, getTmuxSessions, getTmuxActivity
- review-check: GraphQL review checking, dry-run, fix prompt delivery
- open: target resolution (all/project/session), fallback, --new-window
- init: rejects existing config file
Integration tests (2 new files, 8 tests):
- spawn-send-kill: real tmux session create, send-keys, kill, graceful re-kill
- session-ls: multi-session listing, per-session capture, activity timestamps
CLI vitest config updated with explicit thread pool parallelization.
Total CLI tests: 68 → 129. Total integration tests: 20 → 28.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address 5 bugbot findings — symlink type, timeout cleanup, file validation, repo format
- spawn.ts: pass `type` param to symlinkSync based on lstat (dir vs file)
- spawn.ts: document TOCTOU gap in getNextSessionNumber (tmux rejects dupes)
- dashboard.ts: store browser timeout and clear it on child exit
- send.ts: wrap file read in try-catch with user-friendly error
- review-check.ts: validate owner/name split before GraphQL query
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: implement SCM and tracker plugins (github, linear)
Implement three plugin interfaces from packages/core/src/types.ts:
- scm-github: Full GitHub PR lifecycle via `gh` CLI — PR detection by
branch, state tracking, CI checks, reviews, review decision, pending
comments, automated bot comment detection (cursor[bot], codecov, etc.),
and merge readiness (CI + reviews + conflicts + draft).
- tracker-github: GitHub Issues tracker via `gh` CLI — issue CRUD,
completion check, branch naming (feat/issue-N), prompt generation,
list/filter/update/create with label and assignee support.
- tracker-linear: Linear issue tracker via GraphQL API (LINEAR_API_KEY) —
issue fetch, completion check, branch naming (feat/IDENTIFIER), prompt
generation, list with team/state/label filters, state transitions via
workflow state resolution, issue creation with teamId config.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address PR review — GraphQL injection, N+1 queries, timeouts, tests
- tracker-linear: use GraphQL variables in listIssues to prevent injection
- tracker-linear: add 30s HTTP timeout to linearQuery
- tracker-linear: fix issueUrl to use workspace slug from project config
- tracker-linear: add labels/assignee support to createIssue
- scm-github: rewrite getPendingComments to use GraphQL reviewThreads
with real isResolved status instead of hardcoding false
- scm-github: simplify getAutomatedComments to single API call (N+1 fix)
- scm-github: add 30s timeout to gh CLI calls
- scm-github: fix parseDate to return epoch instead of fabricating dates
- scm-github: add repo format validation in detectPR
- scm-github: fix getCISummary to not count all-skipped as passing
- tracker-github: add 30s timeout to gh CLI calls
- tracker-github: fix createIssue to not use unsupported --json flag
- Add comprehensive vitest tests for scm-github and tracker-github
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address Codex review — assignee lookup, GraphQL vars, status checks
Iteration 1 fixes (8 issues from Codex review):
- tracker-linear: remove invalid assigneeDisplayName, resolve assignee
by display name to ID via users query after creation
- tracker-linear: add HTTP status code check in linearQuery
- tracker-linear: throw on missing workflow state in updateIssue
- scm-github: use GraphQL variables ($owner, $name, $number) in
getPendingComments instead of string interpolation
- scm-github: guard against empty comment nodes in review threads
- scm-github: use mergeStateStatus in getMergeability (BEHIND, BLOCKED)
- tracker-github: default description to "" in createIssue
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address PR review + lint — error context, GraphQL vars, mergeability
- scm-github/tracker-github: wrap gh() errors with command context + cause
- scm-github: use GraphQL variables ($owner, $name, $number) in
getPendingComments to prevent injection
- scm-github: guard against empty review thread nodes
- scm-github: incorporate mergeStateStatus (BEHIND/BLOCKED) in getMergeability
- tracker-linear: add identifier vs UUID comment in updateIssue
- Fix ESLint preserve-caught-error violations
- Remove unused mockGhRaw in scm-github tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add pagination to getAutomatedComments and increase thread limit
- getAutomatedComments: add --paginate flag to REST API call to fetch
all review comments beyond the default 30-item first page
- getPendingComments: increase GraphQL reviewThreads limit from 100 to
250 (GitHub's max for connections)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use per_page=100 instead of --paginate for JSON-safe pagination
Replace --paginate with -F per_page=100 in getAutomatedComments to
avoid concatenated JSON arrays that break JSON.parse on multi-page
responses. 100 is GitHub's max per_page value.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: filter out resolved threads in getPendingComments
The method name implies it should only return unresolved/pending review
threads. The isResolved data was already fetched via GraphQL but was not
being filtered on, causing resolved threads to be included in results.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add comprehensive tracker-linear test suite (53 tests)
Covers all Tracker interface methods: getIssue, isCompleted, issueUrl,
branchName, generatePrompt, listIssues, updateIssue, createIssue.
Mocks node:https request to simulate Linear GraphQL API responses.
Tests state mapping, error handling, assignee/label resolution, and
GraphQL variable injection.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address 6 bugbot review comments on PR #4
- GraphQL reviewThreads(first: 250) → first: 100 (GitHub max)
- noConflicts false when mergeable is UNKNOWN (not just CONFLICTING)
- updateIssue now handles labels and assignee (not just state/comment)
- Add res.on("error") handler in linearQuery to prevent crashes
- createIssue returns only actually-applied labels, not all requested
- Tests added/updated for all fixes (144 total across 3 plugins)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make Linear updateIssue labels additive to match GitHub behavior
Linear's issueUpdate replaces all labels, while GitHub's --add-label is
additive. Now fetches existing label IDs first and merges with new ones
before sending to issueUpdate, ensuring consistent behavior across both
tracker implementations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: default listIssues to open state in tracker-linear
When no state filter is specified, tracker-github defaults to "open"
but tracker-linear was returning all issues. Now defaults to excluding
completed/canceled issues, matching tracker-github behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add Composio SDK support to tracker-linear
Allow users with a COMPOSIO_API_KEY to use the Linear tracker without
a separate LINEAR_API_KEY. The plugin auto-detects which key is available
and routes through either the direct Linear GraphQL API or Composio's
LINEAR_RUN_QUERY_OR_MUTATION tool. All existing queries and response
parsing are reused via a GraphQLTransport abstraction.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address 2 bugbot review comments on PR #4
1. getCIChecks now throws on error instead of silently returning [],
preventing a fail-open where CI appears healthy when we can't fetch
check status. getCISummary catches the error and returns "failing".
2. Handle GitHub's UNSTABLE mergeStateStatus (required checks failing)
as a merge blocker in getMergeability.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: prevent unhandled promise rejection in Composio timeout race
When the timeout wins Promise.race in the Composio transport, the
resultPromise is left without a rejection handler. If the SDK call
later rejects, it becomes an unhandled promise rejection that crashes
Node.js 20+ with --unhandled-rejections=throw.
Attach no-op .catch() to both resultPromise and timeoutPromise before
the race so whichever promise loses has its rejection silently handled.
Also adds plugin integration tests (core -> real plugins -> mocked gh CLI).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: chain error cause in getCIChecks and fix core build
- getCIChecks catch now preserves the original error via { cause: err }
instead of discarding it, matching the pattern used by the gh() helper
- Add tsconfig.build.json to core that excludes __tests__/ from build
compilation, preventing TS5055 errors from circular workspace deps
(integration tests import plugin packages that depend back on core)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: remove circular devDeps, address bugbot comments
- Remove plugin devDependencies from core/package.json that created a
circular dependency (core -> plugins -> core), breaking CI build order
- Document in_progress state as intentional no-op in tracker-github
updateIssue (GitHub Issues only supports open/closed)
- Remove @composio/core optional peerDependency from tracker-linear;
the dynamic import() already handles the missing package gracefully,
and the peer dep was pulling composio + transitive deps into lockfile
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add real integration test for tracker-linear against Linear API
Creates a test that exercises the full tracker-linear plugin lifecycle
against the real Linear API — createIssue, getIssue, isCompleted,
listIssues, updateIssue (comment, close, reopen), generatePrompt,
branchName, issueUrl. Each run creates a throwaway test issue and
trashes it in cleanup. Skipped when LINEAR_API_KEY is not set.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* ci: pass LINEAR_API_KEY and LINEAR_TEAM_ID to integration tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: support both LINEAR_API_KEY and COMPOSIO_API_KEY in integration test
The tracker-linear integration test now runs with either credential:
- LINEAR_API_KEY: direct Linear API (full cleanup via trash)
- COMPOSIO_API_KEY: via Composio SDK (cleanup falls back to closing)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: don't pass COMPOSIO_API_KEY to CI integration tests
When both LINEAR_API_KEY and COMPOSIO_API_KEY are set, the plugin
prefers the Composio transport which requires @composio/core SDK.
The SDK isn't installed in CI, so use the direct LINEAR_API_KEY
transport which has no extra dependencies.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use import.meta.url in vitest config, default createIssue description
- Replace __dirname with import.meta.url-derived dirname in ESM config
- Default createIssue description to "" for defensive safety
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: map CANCELLED and ACTION_REQUIRED CI conclusions to failed
Terminal check conclusions (CANCELLED, ACTION_REQUIRED) were falling
through to "pending", causing the lifecycle manager to wait forever.
Also changed the default else branch to "failed" (fail-closed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* 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>
* feat: implement agent plugins (claude-code, codex, aider)
Implement the Agent interface from @agent-orchestrator/core for three
AI coding tools, enabling the orchestrator to launch, detect activity,
and introspect agent sessions.
Claude Code plugin (full implementation):
- JSONL session introspection (summary, session ID, cost, last message type)
- Activity detection via terminal output pattern matching
- Process detection via tmux pane TTY → ps lookup chain
- Launch command generation with permissions/model/prompt support
Codex and Aider plugins (functional stubs):
- Launch command generation with tool-specific flags
- Process detection via TTY lookup
- Stub introspection (no JSONL support yet)
Closes INT-1329
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address PR review — shell escaping, activity detection, tests
- Replace JSON.stringify with POSIX shell escaping (single quotes) for
all agent launch commands to prevent command injection
- Remove `unset CLAUDECODE &&` shell syntax from getLaunchCommand (breaks
execFile); moved to getEnvironment as empty string
- Fix detectActivity returning "exited" for empty pane when process is
confirmed alive — now returns "idle"
- Tighten BLOCKED_PATTERNS to specific error framing (^Error:, ENOENT:,
APIError:, etc.) to avoid false positives on code output
- Check blocked patterns before input patterns to prevent "EACCES:
permission denied" matching INPUT_PATTERNS on the word "permission"
- Remove pgrep fallback in findClaudeProcess (could match wrong session)
- Fix extractCost double-counting: prefer costUSD, fall back to
estimatedCostUsd only when costUSD is absent
- Trim ACTIVE_PATTERNS to stable indicators only
- Add comprehensive vitest tests (127 tests across 3 plugins)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address Codex review + lint fixes
- Use `ps -eo pid,tty,args` instead of `comm` for process detection so
CLI wrappers (node, python) are correctly identified
- Coerce PID from handle.data with Number() + isFinite() to handle
string-serialized PIDs
- Fix token double-counting: prefer `usage` object, only fall back to
flat `inputTokens`/`outputTokens` when `usage` is absent
- Add `--` before Codex prompt to prevent prompts starting with `-`
from being parsed as CLI flags
- Check blocked patterns before input patterns so "EACCES: permission
denied" isn't misclassified as waiting_input
- Fix lint: merge duplicate imports, strict equality, remove non-null
assertions
- Format with prettier
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: multi-pane tmux, EPERM handling, strict process matching
Codex review iteration 1 fixes:
- Iterate all tmux pane TTYs instead of only the first — prevents
false "exited" in multi-pane sessions
- Handle EPERM from process.kill(pid, 0) as "process exists" — EPERM
means the process is alive but we lack permission to signal it
- Use word-boundary regex for process name matching — prevents false
positives on similar names like "claude-code" or paths containing
the substring
- Add tests for EPERM handling, multi-pane detection, and strict
name matching (134 tests total)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: exclude next-env.d.ts from eslint
Auto-generated Next.js type file triggers triple-slash-reference rule.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address Bugbot review — JSON.parse guard, detectActivity pattern priority
- Guard parseJsonlFile against non-object JSON values (null, arrays,
primitives) that would cause TypeError in downstream extractors
- Split null output (non-tmux → "active") from empty output (tmux → "idle")
so non-tmux runtimes don't falsely report idle
- Reorder pattern matching: idle before blocked so stale errors in the
tmux scrollback don't mask an idle prompt
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract shellEscape to @agent-orchestrator/core
Move the duplicated POSIX shell-escaping utility from all three agent
plugins into packages/core/src/utils.ts and re-export from the package
entry point. Plugins now import { shellEscape } from the shared package.
Addresses Bugbot review comment on PR #5.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add execFileAsync timeouts, use satisfies for plugin exports
- Add { timeout: 30_000 } to all execFileAsync calls (tmux, ps) per
CLAUDE.md convention to prevent hanging on stuck processes
- Replace `const plugin: PluginModule<Agent>` with inline
`export default { ... } satisfies PluginModule<Agent>` to preserve
narrow literal types on manifest fields
Addresses Bugbot review comments on PR #5.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: rename introspect → getSessionInfo, detectActivity uses JSONL
- Rename Agent.introspect() → getSessionInfo(), AgentIntrospection → AgentSessionInfo
- Claude Code detectActivity: replace tmux screen-scraping with JSONL-based
detection (file mtime + last entry type)
- Remove terminal pattern constants (ACTIVE_PATTERNS, IDLE_PATTERNS, etc.)
- Update all tests: JSONL-based activity tests, renamed introspect blocks
- waiting_input/blocked states deferred to hooks-based implementation (#16)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add OpenCode agent plugin, integration tests, and CI workflow
- Add agent-opencode plugin with getLaunchCommand, detectActivity,
isProcessRunning, getSessionInfo (27 unit tests)
- Add integration test suite for all 4 agents (claude-code, codex,
aider, opencode) using real binaries in tmux sessions
- Add GitHub Actions CI workflow for integration tests
- Add test:integration script to root package.json
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: update core test mocks — introspect → getSessionInfo + isProcessing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address Bugbot review — Windows paths, bytesRead, hardcoded paths
- toClaudeProjectPath: handle Windows backslashes and drive letters
- readLastJsonlEntry: use bytesRead to avoid null character corruption
- Remove duplicate next-env.d.ts eslint ignore entry
- Remove hardcoded developer-specific binary paths from integration tests
- Add isProcessing limitation comments with issue references (#17-19)
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>