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 7d9b862e93
refactor(agent-claude-code): replace terminal-regex activity detection with hooks (closes #1941) (#1945)
* feat(core): allow source: "hook" in ActivityLogEntry (#1941)

First step of the activity-detection hook refactor: extend the AO
activity-JSONL schema so platform-event hooks (Claude Code's
PermissionRequest / Stop / StopFailure / Notification / ...) can write
entries with explicit provenance, distinct from terminal-derived
("terminal") and agent-native-JSONL ("native") writes.

No behaviour change yet — hook writers come in subsequent commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(agent-claude-code): add activity-updater hook scripts (#1941)

Adds bash + Node script source strings (`ACTIVITY_UPDATER_SCRIPT` /
`ACTIVITY_UPDATER_SCRIPT_NODE`) that translate Claude Code lifecycle
hooks into AO activity-JSONL entries with `source: "hook"`.

Event mapping is intentional and verified against the live Claude Code
hooks reference (code.claude.com/docs/en/hooks, not the older anthropic.com
URL the RFC referenced):

- SessionStart / Stop / SubagentStop                 → ready
- UserPromptSubmit / PreToolUse / PostToolUse /
  PostToolUseFailure / PreCompact / PostCompact /
  SubagentStart / PostToolBatch                      → active
- PermissionRequest                                  → waiting_input
- Notification(permission_prompt | idle_prompt)      → waiting_input
- Notification(auth_success | elicitation_*)         → no-op (the RFC's
  blanket "Notification → waiting_input" would false-fire here)
- StopFailure                                        → blocked
- everything else (SessionEnd, TaskCreated, ...)     → no-op

Event name comes from the stdin JSON payload's `hook_event_name`
field — the RFC's proposed `$CLAUDE_HOOK_EVENT_NAME` env var does not
exist in Claude Code. The script never blocks Claude (`exit 0` on every
path, including parse failures and disk-full).

Bash variant uses `node -p 'new Date().toISOString()'` for the timestamp
because BSD date doesn't support `%3N`. Node is a hard runtime dep of
Claude Code so this is always available.

Plugin wiring + regex-layer removal come in subsequent commits — this
commit only adds the scripts and their 52-test (bash × node) parity
suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(agent-claude-code): register activity-updater on every relevant hook (#1941)

`setupWorkspaceHooks` now installs the activity-updater script alongside
the existing metadata-updater and registers it on every Claude Code
event that carries activity information:

  SessionStart, UserPromptSubmit, PreToolUse, PostToolUse,
  PostToolUseFailure, PostToolBatch, Notification, PermissionRequest,
  Stop, StopFailure, SubagentStart, SubagentStop, PreCompact, PostCompact

The metadata-updater stays registered on PostToolUse(Bash) only — git/gh
side-effect detection is unrelated to activity classification, and
splitting them keeps each script tight.

Implementation notes:
- Hook registration is now a declarative table (`HookRegistration[]`)
  fed through a shared `upsertHookEntry` helper. Calling
  `setupWorkspaceHooks` twice updates our entries in place; any
  user-installed Stop/PreToolUse/... hook is preserved alongside ours.
- Activity-updater hooks register with matcher "" — Claude Code's
  empty-string matcher fires on every variant of the event (e.g. every
  Notification regardless of `notification_type`). Variant filtering
  happens inside the script.
- Timeout for activity-updater is 2000ms (vs metadata-updater's 5000ms)
  — the script does a single JSON parse + append.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(agent-claude-code): retire terminal-regex layer (#1941)

`classifyTerminalOutput` was the source of the 15-commit churn in #1932:
every Claude UI tweak (footer wording, status verb, spinner glyph) broke
a heuristic and needed a tightening pattern. With Claude lifecycle hooks
now writing authoritative state directly to `.ao/activity.jsonl`
(`source: "hook"`), the regex layer is structurally obsolete.

This commit removes the patterns and reduces `classifyTerminalOutput` to
a stable `return "idle"` stub:

- `recordActivity` is no longer implemented on the Claude agent — the
  hooks ARE the activity producer. Lifecycle manager guards
  `agent.recordActivity?` so this is a clean drop.
- `detectActivity` is kept on the Agent interface (still required by
  Aider/OpenCode/Codex fallback) but on Claude is now a constant
  "idle" — the lifecycle's terminal-output fallback path therefore
  records a neutral signal and the JSONL cascade is the only source
  of truth for active/ready/waiting_input/blocked.
- Native Claude JSONL handling and `NOISE_JSONL_TYPES` are unchanged —
  those operate on Claude's own session files, not on terminal pixels.

Tests that exercised the retired heuristics (~200 LOC of regex-pattern
assertions in `detectActivity` + `recordActivity` integration tests) are
replaced with:
  - One it.each guarding that every previously-classified input now
    returns "idle" (locks in the no-signal contract).
  - A direct assertion that `agent.recordActivity` is undefined.
  - New tests that write hook-sourced JSONL entries
    (`source: "hook"`, trigger like "PermissionRequest (Bash)") and
    verify the cascade surfaces them correctly.

Net: −200 LOC of heuristic, ditto of tests, zero regression risk because
the cascade already accepted any `source` value as long as the entry
parsed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(changeset): claude activity hooks (#1941)

ao-core: minor (extends ActivityLogEntry.source / ActivitySignalSource
with "hook" — new value, no consumer break).
ao-plugin-agent-claude-code: minor (new activity-updater hook scripts,
terminal-regex layer retired).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(agent-claude-code): address copilot review on #1945 (#1941)

Four reviewer concerns, all valid:

1. `upsertHookEntry` assumed `hooks[event]` is always an array. A
   malformed/legacy settings.json with a non-array value there would
   crash on `.push`. Normalize via `Array.isArray(existing) ? existing : []`
   and start fresh on bad input. New test: malformed object instead of
   array round-trips cleanly with our entry added.

2. `upsertHookEntry` unconditionally overwrote `entry.matcher` on
   updates. If a user has co-located their own hook def in the same
   `{ matcher, hooks: [...] }` object as ours, resetting the matcher
   changes when the user's def fires. Now only refresh matcher when
   the entry contains a single hook def (ours). New test: user hook
   sharing an entry with us keeps its `Edit|Write` matcher across
   re-setup calls.

3. Claude agent's `detectActivity` comment claimed the lifecycle
   manager would "override" its result via the JSONL cascade. Not
   accurate — lifecycle calls `detectActivity` only when
   `getActivityState` returned null, and that path doesn't write to
   .ao/activity.jsonl. Reworded to describe the actual behaviour:
   `detectActivity` is the no-signal fallback when there's no JSONL
   and no hook entry yet, and "idle" is the conservative answer.

4. `classifyTerminalOutput`'s docstring promised that the Claude
   agent's `detectActivity` would delegate to it "rather than inlining
   `() => "idle"`", but `detectActivity` actually inlined `return "idle"`.
   Restored the delegation so the rationale matches the code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(agent-claude-code): skip bash activity-updater suite on Windows (#1941)

Greptile review on #1945: `execSync('bash "..."')` throws ENOENT on
Windows because bash isn't a native shell there, and the catch block
leaves `lastEntry = null` — making all ~26 bash-variant cases fail on
Windows CI.

Skip the bash suite on Windows via `describe.skipIf(... isWindows())`
(matches the convention used in
packages/core/src/__tests__/migration-storage-v2.test.ts). The Node
variant suite runs on every platform and is the canonical Windows path
for the activity-updater anyway, so parity between the two
implementations is still verified on Linux/macOS CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(agent-claude-code): escape control chars in bash trigger output (#1941)

i-trytoohard's PR review flagged that bash's escape_json only handles \
and " — not \n / \r / \t / \b / \f — creating asymmetry with the Node
variant where JSON.stringify covers everything. Bounded today by
Claude's event/tool/error-name enums never containing control chars,
but adds latent risk if a future trigger source isn't equally clean.

Five-line fix: extend escape_json with the five common JSON control-char
escapes so both implementations stay in lockstep against any future
trigger payload shape.

Locks the parity with a new round-trip test that smuggles
\n / \t / \r / \\ / " through error_type — confirms exactly one JSONL
line is written (no literal newline splitting one entry into two) and
the parsed trigger round-trips bit-for-bit on both bash and Node.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(agent-claude-code): drop useless \$ escape in bash heredoc (#1941)

Lint job on #1945 failed with five `no-useless-escape` errors after the
escape-control-chars fix. The five new lines in escape_json wrote
`\$'\\n'` inside the JS template literal, but `$` is only special in JS
template literals when followed by `{` — outside of interpolation it
needs no backslash. Bash output is byte-identical (still emits `$'\n'`
for ANSI-C quoting), so the 54 round-trip tests stay green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:17:35 +05:30
.changeset refactor(agent-claude-code): replace terminal-regex activity detection with hooks (closes #1941) (#1945) 2026-05-20 18:17:35 +05:30
.cursor feat(windows): complete Windows support (#1025) 2026-05-09 00:10:53 +05:30
.github fix(canary): list all 27 linked packages in dummy changeset (#1882) 2026-05-20 04:14:44 +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 refactor(agent-claude-code): replace terminal-regex activity detection with hooks (closes #1941) (#1945) 2026-05-20 18:17:35 +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