Agentic orchestrator for parallel coding agents — plans tasks, spawns agents, and autonomously handles CI fixes, merge conflicts, and code reviews.
Go to file
Harshit Singh Bhandari 8c71bdebbd
fix(agent-claude-code): harden activity detection against 5 real-world edge cases (#1932)
* fix(agent-claude-code): map bookkeeping JSONL types to ready, not active

Claude writes several types AFTER finishing a turn — `file-history-snapshot`, `attachment`, `pr-link`, `queue-operation`, `permission-mode`, `last-prompt`, `ai-title`, `agent-color`, `agent-name`, `custom-title`. Until now they fell through to the `default` switch branch and looked `active` for 30s, making finished sessions appear busy. This was almost certainly the root cause of "Claude looks like it's still working when it's done" reports (the #1908 family).

Add explicit cases mapping each to `ready`/`idle` by age, same as `assistant`/`summary`. Three existing tests that asserted these returned `active` were testing the buggy behavior — updated to the correct behavior, plus four new tests covering `attachment`, `permission-mode`, and `ai-title`.

* fix(agent-claude-code): broaden process regex to match real install variants

`(?:^|\/)claude(?:\s|$)` rejected several legitimate Claude installs, causing AO to declare sessions `exited` while Claude was still running:
- `claude-code` (some Anthropic CLI variants)
- `claude.exe` / `claude.js` / `claude.cjs` (Windows / shim installs)
- `.claude` (dot-prefix shim)
- `node /opt/.../@anthropic-ai/claude-code/cli.js` (npm shim — path-contains-claude case)

New regex `(?:^|\/)(?:\.)?claude(?:[-.][\w-]+)*(?:[\s/]|$)` allows optional dot prefix, hyphen/dot suffix chains, and `/` terminator (for paths with claude as a path component). Still anchored at `/` or start-of-line so `claudia`/`claudine` etc. don't false-match.

Tests: 7-case `it.each` for each install shape returning true, dedicated rejection test for `claudia`/`claudine`. Old strict test (`does not match claude-code`) deleted — it was testing the bug.

* fix(agent-claude-code): warn on non-ENOENT failures reading ~/.claude/projects/

Previously `findLatestSessionFile` swallowed every readdir error silently, so a permission-denied or fd-exhausted misconfig on `~/.claude/projects/<slug>` would leave the session looking permanently `idle` on the dashboard with zero telemetry — debugging this took hours in the past (filed as one of the gotchas in #1927's description).

Now: ENOENT (dir hasn't been created yet) stays silent because that's normal during the early lifecycle. Every other errno (EACCES, EPERM, EMFILE, etc.) emits a single console.warn naming the dir and the errno, plus noting that activity will fall back to the AO JSONL only. Caller behavior unchanged (still returns null).

Tests: one for EACCES (via chmod 0o000) confirming warn fires with the errno, one for the missing-dir case confirming warn does NOT fire.

* fix(agent-claude-code): resolve symlinked workspace paths before slugifying

If AO records `session.workspacePath` as a symlink (e.g. `/Users/me/symlinks/repo`) and Claude resolves the target before computing its on-disk slug (e.g. `/Users/me/code/repo`), the two slugs diverge and `findLatestSessionFile` looks in an empty `~/.claude/projects/<wrong-slug>/` dir forever. Session looks permanently `idle` on the dashboard with no telemetry.

Add `resolveWorkspaceForClaude(workspacePath)` (try realpathSync, fall back to literal on error). Use it in all three sites that slugify a workspace path: `getClaudeActivityState`, `getSessionInfo`, `getRestoreCommand`. Re-exported from `index.ts` for downstream consumers.

Kept `toClaudeProjectPath` as a pure string transform — the realpath happens in the caller, so the slug function stays trivially testable.

New test confirms: write JSONL under the target's slug, call getActivityState with the symlink path, expect `ready` (was `null` before the fix).

Test-setup also needed `realpathSync(mkdtempSync(...))` because /var/folders is itself a symlink on macOS, otherwise existing tests would set up JSONL under one slug and the code would now look under another.

* fix(agent-claude-code): disambiguate multi-session via claudeSessionUuid

When two Claude sessions are running in the same workspace, `findLatestSessionFile` picked newest-mtime — which is the WRONG session's JSONL whenever its sibling has just written. The `getSessionInfo` method already captures `session.metadata.claudeSessionUuid` (the UUID Claude uses as its JSONL filename), but `getClaudeActivityState` was ignoring it.

`findLatestSessionFile` now accepts an optional `preferredUuid`. If provided, it checks `<projectDir>/<preferredUuid>.jsonl` first via `stat()` and returns that path on success. Falls back to newest-mtime when the UUID is missing or the named file doesn't exist yet (fresh session not yet introspected, or file rotated/removed).

`getClaudeActivityState` reads `session.metadata.claudeSessionUuid` (coerces unknown→string, trims, treats empty as undefined) and passes it through.

Tests:
- prefers UUID-named JSONL when set, ignoring newer sibling file
- falls back to newest-mtime when UUID-named file doesn't exist

* chore: changeset for claude-activity-edge-cases

* fix(agent-claude-code): use ES import for realpathSync in test setup

CI lint rejected the `require()` form. Move realpathSync to the top-level node:fs import.

* refactor(agent-claude-code): drop dead tool_use and result switch cases

Both types were inherited from the Agent-interface spec but never actually emitted by Claude (verified on disk for #1927). They now fall to the `default` branch.

Behavior change:
- `tool_use` → previously in user/progress branch → identical to default. No real-world effect.
- `result` → previously in assistant/summary branch (never active) → now default (can be active when fresh). No real-world effect because Claude doesn't emit `result`.

If Claude ever introduces these types and we want different semantics, we add explicit cases back. Until then, the dead switch entries were noise.

Tests:
- Removed `returns 'active' for recent 'tool_use' entry` (default branch does the same)
- Removed `returns 'ready' for recent 'result' entry` (was testing dead behavior)
- Added `unknown types fall through to default branch — fresh → active` to lock the default-branch semantics for any future Claude type addition.

* feat(agent-claude-code): detect blocked from terminal regex too

Until now `blocked` was only sourced from native JSONL (`{type:"system", subtype:"api_error", level:"error"}` from #1927). The terminal-regex pipeline (`detectActivity` → `recordTerminalActivity` → AO activity-JSONL → `checkActivityLogState`) only emitted `waiting_input`/`idle`/`active`, never `blocked`. So when Claude's native JSONL was unreadable (slug drift, fresh session pre-introspection, etc.) the blocked state was unreachable via the safety net.

Patterns observed empirically by capturing tmux output during a real api.anthropic.com block (api blocked via /etc/hosts, fresh `ao spawn` session, `tmux capture-pane` after retries started):

  ⎿  Unable to connect to API (ConnectionRefused)
     Retrying in 19s · attempt 7/10

Add two matches to `classifyTerminalOutput`:
  - /Unable to connect to API/i — primary error wording
  - /Retrying in \\d+s.*attempt \\d+\\/\\d+/i — retry counter (fires even when error scrolled off)

Placed BEFORE the existing waiting_input checks because Claude's static UI footer contains `bypass permissions on (shift+tab to cycle)` which the existing `bypass.*permissions` regex matches — without this ordering, a real blocked state would lose to that incidental match.

Tests cover: real captured output, alternate error code (FailedToOpenSocket), retry counter alone, and the bypass-permissions-footer precedence case.

* fix(agent-claude-code): address review feedback on PR #1932

Three issues raised by @greptile-apps review:

1. console.warn flooded on every poll (P1). getClaudeActivityState runs on
   a polling interval — without a dedupe, a single EACCES path would log
   60+ lines/minute indefinitely. Added module-level Set<string> keyed by
   projectDir; warn only on first miss per path for the process lifetime.
   Exported resetWarnedReaddirPaths for test isolation.

2. realpathSync blocked the event loop in fully-async callers (P2).
   Switched to async realpath from node:fs/promises. Updated the three
   call sites (getClaudeActivityState, getSessionInfo, getRestoreCommand)
   to await. resolveWorkspaceForClaude is now Promise<string>.

3. Test regex /failed to read.*EACCES|EPERM/ parsed as
   (failed to read.*EACCES) | (EPERM) because | has lowest precedence.
   Wrapped the alternation in a non-capturing group: (?:EACCES|EPERM).

Also added a new test confirming the dedupe works: three consecutive polls
against an EACCES path produce exactly one warn.

* fix(agent-claude-code): skip UI-noise JSONL types when reading last entry

Regression caused by the earlier bookkeeping-types fix on this branch (commit 39c2b1dd). That commit moved `permission-mode`, `ai-title`, `agent-color`, `agent-name`, `custom-title` into the explicit ready/idle bookkeeping case alongside `file-history-snapshot` etc. — but these five types are UI-state snapshots Claude writes at random times (session attach, permission-mode change, title regeneration). They are NOT correlated with real activity.

Empirical proof: ao-144 had 73 trailing `permission-mode` + 73 trailing `ai-title` entries written over 6 dormant days. Last real assistant message was 2026-05-13 12:57. With the earlier fix in place, the dashboard oscillated between `ready` (recent noise mtime) and `idle` (older noise mtime) instead of staying `idle`. User reported "I am not seeing active but ready for all session" — this is what they were seeing.

Fix: define `NOISE_JSONL_TYPES` containing the five UI-state types. When the literal last entry is one of these, skip the native-JSONL classification and fall through to the AO activity-JSONL pipeline (terminal-derived). If that's also empty, the existing `staleNativeState` fallback returns `idle` from `session.createdAt`, which is correct for dormant sessions.

For sessions that ARE actively working but happen to have a noise type as the latest line, the AO JSONL fallback's terminal scrape catches the active state.

Removed the five noise types from the explicit bookkeeping case in the switch. Kept `file-history-snapshot`, `attachment`, `pr-link`, `queue-operation`, `last-prompt` — these plausibly correlate with real activity (file edits, PR creation, prompt submission).

Tests: updated `permission-mode → ready` to `→ idle`, same for `ai-title`. Added explicit tests for the other three noise types and a test confirming the AO JSONL pipeline still wins when last native entry is noise but AO has actionable state.

* fix(agent-claude-code): tighten waiting_input regex to ignore static UI footer

The existing `/bypass.*permissions/i` regex matched Claude's persistent UI footer `⏵⏵ bypass permissions on (shift+tab to cycle)` — which is visible on EVERY active Claude session. As long as native JSONL classification returned a definitive answer, this didn't matter. The previous noise-skip commit on this branch (faf43042) made many more sessions fall through to the AO JSONL pipeline, which exposed the bug: ao-143/144/151 all flipped to waiting_input even though they were genuinely dormant.

Tightened the regex to require "all future" — matching the actual permission-bypass prompt ("bypass all future permissions for this session") rather than the footer toggle ("bypass permissions on"). The other two waiting_input regexes (`Do you want to proceed?`, `(Y)es...(N)o`) are unchanged.

Verified: dashboard's stored AO activity-JSONL entries had `state: "waiting_input"` with trigger snippets containing the persistent footer text. With this fix, classifyTerminalOutput on the exact same terminal capture returns active instead.

Tests: existing `bypass all future permissions for this session` test still passes (the prompt phrase contains the new tighter pattern). Added a regression test confirming the static footer alone does NOT match waiting_input.

* fix(agent-claude-code): treat pr-link as re-snapshot noise too

Empirical evidence from production: ao-160's JSONL had the same PR (#1911) written as a `pr-link` entry 33 times in the last 200 lines, alternating with permission-mode/ai-title in the same trio pattern. Internal timestamps showed re-emissions ~1 minute apart (15:24:40, 15:26:27, 15:27:42 — same PR, three writes within ~3 minutes).

User-visible bug: ao-139 (last real work 11h ago) and ao-160 (last real work over a day ago) were both showing as `ready` on the dashboard because the file mtime kept moving forward via these pr-link re-snapshots, and pr-link was in the explicit ready/idle bookkeeping case.

Fix: move `pr-link` from the bookkeeping case into NOISE_JSONL_TYPES so the cascade skips it and falls through to the AO JSONL pipeline (and ultimately to the stale-native idle fallback for genuinely dormant sessions). Real PR creation is still observable via the assistant message that asked for it and via the gh-tracker.

Verified: 200/200 plugin tests pass. The existing pr-link test updated to expect idle (matches new behavior); added comment with the empirical evidence.

* fix(agent-claude-code): tighten terminal classification — default to idle, gerund+ellipsis = active

Root cause of dashboard showing ready for dormant sessions ao-143/144/151/154/160/139: classifyTerminalOutput defaulted to "active" for any output that didn't match a specific idle/blocked/waiting_input pattern. Claude's persistent UI footer + empty input area always look the same between "just finished" and "currently working", and the existing lastLine idle-check missed dormant sessions because the LAST line is the static footer, not the ❯ prompt.

So every poll cycle, recordTerminalActivity wrote "active" entries to AO activity-JSONL for dormant sessions. The cascade's age-decayed fallback then surfaced those as ready (30s–5min decay) forever. Pr-link being treated as turn-end bookkeeping in 4e0e64ab made things even worse (sessions oscillated between ready and idle).

Fix — invert the default:
- classifyTerminalOutput now defaults to "idle" and only returns "active"
  when an explicit active-work indicator is present.
- Strongest active signal: gerund-form status word followed by trailing
  ellipsis "…". Claude rotates many spinner glyphs (✻ ✽ · ⠁ ⠈ etc.) and
  status words (Germinating, Fluttering, Pondering, Mulling, Crafting,
  Thinking, Reasoning…), but the gerund+ellipsis combo is consistent.
  Past-tense lines like "✻ Worked for 11s" or "✻ Crunched for 11s" lack
  the ellipsis and stay idle.
- Removed reliance on:
  - bare "esc to interrupt" (in static footer, not just active)
  - "Working" without ellipsis (false-matches "working on issue #N")
  - "⎿" line prefix (also appears in past tool-result content)
- Word-based fallbacks kept for specific cases: Reading file / Writing to
  / Searching codebase / Press up to edit queued / Germinating /
  Crunched for Ns / Thinking…|Working…
- Widened tail window from 5 to 12 lines because Claude's spinner line
  sits 6-8 lines above the bottom (above input area + footer).

Verified live on tmux captures from 6 dormant sessions (ao-143/144/151/154/160/139) and 1 active session (ao-161): correct classification for all 7. Previous code returned "active" for all 6 dormant ones; new code returns "idle".

Tests: 203/203 pass. Updated the "non-empty output with no patterns" test to expect idle (matches new behavior — was testing the bug). Added dormant-pane and active-pane regression tests using real captured strings.

* fix(agent-claude-code): tighten terminal classification — default to idle, gerund+ellipsis = active

Followup to f1fc21bc: also drop the "/\bCrunched\s+for\s+\d+\s*s/i" pattern. It's past-tense like "Worked for Ns" — Claude shows "✻ Crunched for 22s" as a turn-complete summary, not an active indicator. ao-154's terminal had exactly this line, making every poll write "active" to AO activity-JSONL.

After this commit the only past-tense traps left would also lack the gerund+ellipsis signal, so the gerund+ellipsis check above handles all real active spinners. The remaining word-based fallbacks (Germinating, Thinking…/Working…, Reading file, Writing to, Searching codebase, Press up to edit queued) all stay because they're either gerund-form or specific enough to be unambiguous.

Live-verified on tmux captures from all 7 sessions:
- ao-161 (active turn, "Fluttering…") → active 
- ao-143 ("Worked for 11s") → idle 
- ao-154 ("Crunched for 22s") → idle  (was returning active before this commit)
- ao-144/151/160/139 (no active indicators in pane) → idle 

Tests: 203/203.

* fix(agent-claude-code): bound blocked detection to wideTail, not full buffer

Per @greptile-apps P1 review on PR #1932: `Unable to connect to API` / `Retrying in Ns · attempt N/M` were being matched against the entire `terminalOutput` rather than the last-12-lines `wideTail`. If Claude hit an api_error, retried successfully, and then generated enough subsequent output to push the error off the visible window but not out of full scrollback, every subsequent poll would re-detect "blocked" forever.

Move the `wideTail = lines.slice(-12).join("\n")` definition earlier in the function and use it for the two blocked patterns too. All existing blocked test fixtures are ≤12 lines so they pass unchanged.

Added a regression test that simulates the recovered-and-continued scenario: error text early in the input, then 15+ lines of subsequent work, then an active spinner at the bottom. Old code returned "blocked" (saw the error in the full buffer); new code returns "active" (wideTail only sees the spinner).
2026-05-19 22:55:05 +05:30
.changeset fix(agent-claude-code): harden activity detection against 5 real-world edge cases (#1932) 2026-05-19 22:55:05 +05:30
.cursor feat(windows): complete Windows support (#1025) 2026-05-09 00:10:53 +05:30
.github refactor(release): split publish into two-repo model — GitHub release public, npm publish private (#1815) 2026-05-13 03:29:04 +05:30
.husky feat: implement comprehensive security audit and secret leak prevention (#67) 2026-02-16 10:33:50 +05:30
.issue-assets feat(notifier): make notifier system robust with manual harness and desktop setup (#1736) 2026-05-19 14:48:40 +05:30
artifacts chore: rename @composio scope to @aoagents across all packages 2026-04-09 15:59:33 +00:00
changelog chore: rename @composio scope to @aoagents across all packages 2026-04-09 15:59:33 +00:00
completions add zsh completion support for ao (#1374) 2026-04-24 03:48:25 +05:30
docs feat(notifier): make notifier system robust with manual harness and desktop setup (#1736) 2026-05-19 14:48:40 +05:30
examples feat: add $schema support to agent-orchestrator.yaml (#1373) 2026-04-26 19:42:45 +05:30
handoff/pr-1466 refactor(core): storage redesign — projectId-based paths, JSON metadata (#1466) 2026-04-28 17:55:53 +05:30
openclaw-plugin refactor: remove --decompose feature entirely 2026-04-10 13:02:52 +05:30
packages fix(agent-claude-code): harden activity detection against 5 real-world edge cases (#1932) 2026-05-19 22:55:05 +05:30
schema feat(notifier): make notifier system robust with manual harness and desktop setup (#1736) 2026-05-19 14:48:40 +05:30
scripts feat(release): weekly release train — channels, onboarding, dashboard banner, cron (#1781) 2026-05-12 23:11:09 +05:30
skills chore: add bug-triage skill for agent-driven issue triage (#1725) 2026-05-12 20:51:00 +05:30
tests/integration chore(cli): remove deprecated 'ao init' command (#1438) 2026-05-05 18:43:55 +05:30
website feat(notifier): make notifier system robust with manual harness and desktop setup (#1736) 2026-05-19 14:48:40 +05:30
.eslintignore fix: added GraphQL batch PRfeat: add GraphQL batch PR enrichment for orchestrator polling enrichment for orchestrator polling (fixes #608) (#637) 2026-03-30 00:31:04 +05:30
.gitignore feat(windows): complete Windows support (#1025) 2026-05-09 00:10:53 +05:30
.gitleaks.toml refactor(core): storage redesign — projectId-based paths, JSON metadata (#1466) 2026-04-28 17:55:53 +05:30
.npmrc feat: publish to npm under @composio scope (#32) 2026-02-15 04:28:57 +05:30
.prettierignore chore: add ESLint, Prettier, CI workflow, and comprehensive CLAUDE.md conventions 2026-02-13 18:01:52 +05:30
.prettierrc chore: add ESLint, Prettier, CI workflow, and comprehensive CLAUDE.md conventions 2026-02-13 18:01:52 +05:30
AGENTS.md chore: add bug-triage skill for agent-driven issue triage (#1725) 2026-05-12 20:51:00 +05:30
ARCHITECTURE.md feat(windows): complete Windows support (#1025) 2026-05-09 00:10:53 +05:30
CLAUDE.md fix(core): keep actionable activity sticky (#1902) 2026-05-17 20:03:56 +05:30
CONTRIBUTING.md refactor(release): split publish into two-repo model — GitHub release public, npm publish private (#1815) 2026-05-13 03:29:04 +05:30
DESIGN.md refactor(core): storage redesign — projectId-based paths, JSON metadata (#1466) 2026-04-28 17:55:53 +05:30
LICENSE feat: publish to npm under @composio scope (#32) 2026-02-15 04:28:57 +05:30
README.md feat(release): weekly release train — channels, onboarding, dashboard banner, cron (#1781) 2026-05-12 23:11:09 +05:30
SECURITY.md fix: migrate to hash-based project isolation architecture 2026-02-18 00:19:55 +05:30
SETUP.md feat(windows): complete Windows support (#1025) 2026-05-09 00:10:53 +05:30
TROUBLESHOOTING.md fix: migrate to hash-based project isolation architecture 2026-02-18 00:19:55 +05:30
agent-orchestrator.yaml.example feat(windows): complete Windows support (#1025) 2026-05-09 00:10:53 +05:30
eslint.config.js fix(cli): reap daemon children on stop+SIGINT, sweep orphans on start (closes #1848) (#1849) 2026-05-15 03:38:09 +05:30
package.json fix(deps): bump vulnerable dependencies so pnpm audit passes cleanly (#1338) 2026-04-26 17:01:07 +05:30
pnpm-lock.yaml feat(notifier): make notifier system robust with manual harness and desktop setup (#1736) 2026-05-19 14:48:40 +05:30
pnpm-workspace.yaml chore: remove React Native/Expo mobile app 2026-03-25 12:00:59 +05:30
tsconfig.base.json fix: scope node types to node packages 2026-04-13 18:25:21 +05:30
tsconfig.node.json fix: scope node types to node packages 2026-04-13 18:25:21 +05:30

README.md

Agent Orchestrator — The Orchestration Layer for Parallel AI Agents

Agent Orchestrator banner

Spawn parallel AI coding agents, each in its own git worktree. Agents autonomously fix CI failures, address review comments, and open PRs — you supervise from one dashboard.

GitHub stars
npm version
License: MIT
PRs merged
Tests
Discord


Agent Orchestrator manages fleets of AI coding agents working in parallel on your codebase. Each agent gets its own git worktree, its own branch, and its own PR. When CI fails, the agent fixes it. When reviewers leave comments, the agent addresses them. You only get pulled in when human judgment is needed.

Agent-agnostic (Claude Code, Codex, Aider) · Runtime-agnostic (tmux, ConPTY/process, Docker) · Tracker-agnostic (GitHub, Linear)

See it in action

Agent Orchestrator demo — AI agents building their own orchestrator

Watch the Demo on X


The Self-Improving AI System That Built Itself

Read the Full Article on X

Quick Start

Prerequisites: Node.js 20+, Git 2.25+, gh CLI, and:

  • macOS / Linux: tmux — install via brew install tmux or sudo apt install tmux.
  • Windows: PowerShell 7+ recommended. tmux is not required — AO uses native ConPTY via the runtime-process plugin (the default on Windows). Set AO_SHELL=bash if you have Git Bash and prefer it.

Install

npm install -g @aoagents/ao

Nightly builds (latest main, daily FriTue): npm install -g @aoagents/ao@nightly
Back to stable: npm install -g @aoagents/ao@latest

Permission denied? Install from source?

If npm install -g fails with EACCES, prefix with sudo or fix your npm permissions.

To install from source (for contributors):

git clone https://github.com/ComposioHQ/agent-orchestrator.git
cd agent-orchestrator && bash scripts/setup.sh

Zsh Completion

Generate the completion file from the installed CLI:

mkdir -p ~/.zsh/completions
ao completion zsh > ~/.zsh/completions/_ao

Then make sure the directory is on your fpath before compinit runs:

fpath=(~/.zsh/completions $fpath)
autoload -Uz compinit
compinit

For Oh My Zsh, install the same generated file into a custom plugin directory and add ao to your plugin list:

mkdir -p "${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/ao"
ao completion zsh > "${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/ao/_ao"

If you are contributing from a source checkout, you can also symlink the repo copy at completions/_ao.

Start

Point it at any repo — it clones, configures, and launches the dashboard in one command:

ao start https://github.com/your-org/your-repo

Or from inside an existing local repo:

cd ~/your-project && ao start

That's it. The dashboard opens at http://localhost:3000 and the orchestrator agent starts managing your project.

Add more projects

ao start ~/path/to/another-repo

How It Works

  1. You startao start launches the dashboard and an orchestrator agent
  2. Orchestrator spawns workers — each issue gets its own agent in an isolated git worktree
  3. Agents work autonomously — they read code, write tests, create PRs
  4. Reactions handle feedback — CI failures and review comments are automatically routed back to the agent
  5. You review and merge — you only get pulled in when human judgment is needed

The orchestrator agent uses the AO CLI internally to manage sessions. You don't need to learn or use the CLI — the dashboard and orchestrator handle everything.

Configuration

ao start auto-generates agent-orchestrator.yaml with sensible defaults. You can edit it afterwards to customize behavior:

# agent-orchestrator.yaml
$schema: https://raw.githubusercontent.com/ComposioHQ/agent-orchestrator/main/schema/config.schema.json
# Runtime data is auto-derived under ~/.agent-orchestrator/{hash}-{projectId}/
port: 3000

defaults:
  runtime: tmux       # default on macOS / Linux; on Windows the default is `process` (ConPTY)
  agent: claude-code
  workspace: worktree
  notifiers: [desktop]

projects:
  my-app:
    repo: owner/my-app
    path: ~/my-app
    defaultBranch: main
    sessionPrefix: app

reactions:
  ci-failed:
    auto: true
    action: send-to-agent
    retries: 2
  changes-requested:
    auto: true
    action: send-to-agent
    escalateAfter: 30m
  approved-and-green:
    auto: false # flip to true for auto-merge
    action: notify

CI fails → agent gets the logs and fixes it. Reviewer requests changes → agent addresses them. PR approved with green CI → you get a notification to merge.

Keep the $schema line so editors can autocomplete and validate against schema/config.schema.json.

See agent-orchestrator.yaml.example for the full reference, or run ao config-help for the complete schema.

Remote Access

AO keeps your Mac awake while running, so you can access the dashboard remotely (e.g., via Tailscale from your phone) without the machine going to sleep.

How it works: On macOS, AO automatically holds an idle-sleep prevention assertion using caffeinate. When AO exits, the assertion is released.

# agent-orchestrator.yaml
$schema: https://raw.githubusercontent.com/ComposioHQ/agent-orchestrator/main/schema/config.schema.json
power:
  preventIdleSleep: true  # Default on macOS; no-op on Linux and Windows

Set to false if you want to allow idle sleep while AO runs.

Lid-close limitation: macOS enforces lid-close sleep at the hardware level — no userspace assertion can override it. If you need remote access while traveling with the lid closed, use clamshell mode (external power + display + input device).

Linux / Windows: AO does not currently hold a wake assertion on these platforms. On Linux, idle-sleep behaviour is governed by your desktop environment / systemd-logind; configure that directly. On Windows, set the OS power plan if remote access matters while idle.

Plugin Architecture

Seven plugin slots. Lifecycle stays in core.

Slot Default Alternatives
Runtime tmux (macOS/Linux) / process (Windows) process, docker
Agent claude-code codex, aider, cursor, opencode, kimicode
Workspace worktree clone
Tracker github linear, gitlab
SCM github gitlab
Notifier desktop slack, discord, composio, webhook, openclaw
Terminal iterm2 web

All interfaces defined in packages/core/src/types.ts. A plugin implements one interface and exports a PluginModule. That's it.

Why Agent Orchestrator?

Running one AI agent in a terminal is easy. Running 30 across different issues, branches, and PRs is a coordination problem.

Without orchestration, you manually: create branches, start agents, check if they're stuck, read CI failures, forward review comments, track which PRs are ready to merge, clean up when done.

With Agent Orchestrator, you: ao start and walk away. The system handles isolation, feedback routing, and status tracking. You review PRs and make decisions — the rest is automated.

Documentation

Doc What it covers
Setup Guide Detailed installation, configuration, and troubleshooting
CLI Reference All ao commands (mostly used by the orchestrator agent)
Examples Config templates (GitHub, Linear, multi-project, auto-merge)
Development Guide Architecture, conventions, plugin pattern
Contributing How to contribute, build plugins, PR process

Development

pnpm install && pnpm build    # Install and build all packages
pnpm test                      # Run tests (3,288 test cases)
pnpm dev                       # Start web dashboard dev server

See docs/DEVELOPMENT.md for code conventions and architecture details.

Contributing

Contributions welcome. The plugin system makes it straightforward to add support for new agents, runtimes, trackers, and notification channels. Every plugin is an implementation of a TypeScript interface — see CONTRIBUTING.md and the Development Guide for the pattern.

License

MIT