Agentic orchestrator for parallel coding agents — plans tasks, spawns agents, and autonomously handles CI fixes, merge conflicts, and code reviews.
Go to file
Harsh Batheja df8395c3ec
feat: standardize agent plugins — shared hooks, activity JSONL, backfill aider/opencode (#755)
* feat: standardize agent plugins with shared hooks, activity JSONL, and CLAUDE.md

- Add CLAUDE.md with full project context and agent plugin implementation standards
- Extract shared PATH-wrapper metadata hooks into @composio/ao-core (agent-workspace-hooks.ts)
- Backfill Aider + OpenCode: setupWorkspaceHooks, postLaunchSetup, getSessionInfo, getRestoreCommand
- Add recordActivity method to Agent interface for terminal-derived JSONL writing
- Create activity-log.ts in core: appendActivityEntry, readLastActivityEntry
- Lifecycle manager calls recordActivity before getActivityState for agents that implement it
- Upgrade detectActivity in Aider/OpenCode with real terminal prompt/permission patterns
- Upgrade Codex getActivityState to parse JSONL entry types (6 states, up from 2)
- Replace duplicated normalizePermissionMode with shared normalizeAgentPermissionMode from core
- Remove ~200 lines of duplicated shell wrapper code from Codex plugin
- Add git wrapper detection for existing branch switches (parity with Claude Code hooks)
- 484 tests passing across all 4 agent plugins

* chore: unignore CLAUDE.md and AGENTS.md, slim down AGENTS.md to pointer

- Remove CLAUDE.md and AGENTS.md from .gitignore (both should be tracked)
- Slim AGENTS.md from 96 to 30 lines — commands, TL;DR, key files
- Full context now lives in CLAUDE.md; AGENTS.md points there

* fix: eliminate redundant double file read in readLastActivityEntry

Remove the readLastJsonlEntry call that was only used as a null-check,
then immediately discarded before re-reading the same file from scratch.
Now performs a single open + stat + tail-read per poll cycle.

* fix: remove duplicate case-insensitive regex in OpenCode detectActivity

The \(y\)es.*\(n\)o pattern with /i flag was identical to the preceding
\(Y\)es.*\(N\)o/i check — remove the redundant line.

* fix: use zero-initialized buffer and slice to bytesRead in readLastActivityEntry

Replace Buffer.allocUnsafe with Buffer.alloc and slice the result to
actual bytesRead, preventing uninitialized heap data from being parsed
if the file shrinks between stat() and read().

* fix: add staleness cap for waiting_input/blocked and deduplicate recordActivity

- Add ACTIVITY_INPUT_STALENESS_MS (5 min) cap so stale waiting_input/blocked
  entries don't keep sessions stuck in needs_input on the dashboard forever.
- Extract checkActivityLogState() into core — shared by aider, opencode, codex.
- Extract classifyTerminalActivity() into core — deduplicates the identical
  recordActivity logic across all three plugins.

* fix: prioritize native Codex JSONL over AO activity log in getActivityState

Reorder detection so Codex's native 6-state JSONL (approval_request,
error, tool_call, etc.) is checked first. AO activity JSONL from
terminal parsing is now a fallback only for waiting_input/blocked states
that the native JSONL may not have captured. Previously the AO log was
always fresh (written every poll cycle by recordActivity) and shadowed
the richer native detection entirely.

* fix: restrict AO activity JSONL to waiting_input/blocked only

checkActivityLogState now only returns results for waiting_input/blocked
states. Non-critical states (active/ready/idle) return null, forcing
callers to fall through to their native signals (git commits, chat
history, OpenCode API, Codex native JSONL). This prevents the lifecycle
manager's recordActivity writes (which refresh mtime every poll cycle)
from shadowing richer detection methods and breaking stuck-detection.

* fix: prevent stale idle timestamp in aider and skip flags in git wrapper

- Remove Aider's fallback that returned idle with activityResult.modifiedAt
  (always fresh due to recordActivity writes). Now returns null when no
  git commits or chat history are found, letting the lifecycle manager
  handle stuck-detection correctly.
- Fix git wrapper catch-all case to skip flag arguments (e.g. -B) and
  look at $3 for the actual branch name.

* fix: add mtime fallback for empty Codex JSONL and remove unused exports

- When the native Codex session file exists but readLastJsonlEntry returns
  null (empty/unparseable), fall back to stat-based mtime detection instead
  of losing activity detection entirely.
- Remove unused exports getActivityLogPath and ACTIVITY_INPUT_STALENESS_MS
  from @composio/ao-core barrel — they are only used internally.

* fix: opencode activity state detection and CLAUDE.md agent plugin standards

- Fix session ID capture to handle both session_id (snake_case) and
  sessionID (camelCase) from OpenCode 1.3.x JSON responses
- Replace broken --command true with -- noop for session creation
  (true is not a valid OpenCode command since 1.3.x)
- Add JSONL mtime fallback in getActivityState so active/ready/idle
  states work even when findOpenCodeSession returns null
- Rewrite CLAUDE.md activity detection section with the full
  getActivityState contract, mandatory JSONL mtime fallback pattern,
  and 8 required tests every agent plugin must implement

* fix: opencode --command true flag and activity JSONL mtime staleness

- Fix opencode getLaunchCommand to use `--command true` instead of `-- noop`
  (aligns with test expectations and opencode CLI docs)
- Fix checkActivityLogState to use entry.ts instead of file mtime for
  staleness checking — recordActivity refreshes mtime every poll cycle,
  which prevented stale waiting_input/blocked entries from being detected
- Fix opencode getActivityState fallback to use entry state directly
  instead of re-deriving from mtime, which always returned "active"
  because recordActivity refreshes the file every cycle
- Update tests to reflect new entry-state-based fallback behavior

* fix: deduplicate recordActivity writes and restore mtime-based fallback

recordActivity was writing to the JSONL every poll cycle (~30s), which
kept refreshing the file mtime and prevented the JSONL mtime fallback
in getActivityState from ever reaching "ready" or "idle".

Fix: skip writes when the state hasn't changed and the last entry is
<20s old. This keeps mtime fresh during active work (writes every
20-30s, within the 30s activeWindow) but lets it age naturally when
the agent goes quiet.

Also restores the mtime-based age classification in the JSONL fallback
(active/ready/idle by mtime age) instead of returning the entry state
directly, which was always "active" since that's what recordActivity
writes.

Applied to both OpenCode and Aider plugins. Updated CLAUDE.md with
the dedup pattern and rationale.

* fix: align integration tests with opencode `-- noop` launch command

Update 9 test expectations from `--command true` to `-- noop` to match
the reverted getLaunchCommand implementation.

* fix: add JSONL mtime fallback to Aider getActivityState

When git commits and chat history are both unavailable (e.g. early
session startup), Aider's getActivityState now falls back to the AO
activity JSONL mtime for active/ready/idle classification — matching
OpenCode's existing step 3 fallback. Previously it returned null,
leaving the dashboard with no activity signal.

* fix: add write deduplication to Codex recordActivity

Add the same dedup logic that Aider and OpenCode already have — skip
writes when the state hasn't changed and the last entry is recent
(<20s). Prevents unbounded file growth and stale mtime refreshes.

* refactor: extract shared recordTerminalActivity into core

Move the duplicated recordActivity logic (classify + dedup + append)
from all three plugins into a shared `recordTerminalActivity` function
in `@composio/ao-core/activity-log`. Each plugin's `recordActivity`
is now a thin wrapper that delegates to the shared function.

Add core tests for classifyTerminalActivity, checkActivityLogState,
and recordTerminalActivity (10 tests).

* fix: validate JSONL entry fields, handle invalid dates, consistent operators

- Validate required fields (ts, state, source) before casting parsed
  JSON to ActivityLogEntry — prevents malformed entries from propagating
- Guard against invalid Date parsing in checkActivityLogState — returns
  null instead of comparing against NaN
- Use <= instead of < in Aider chat-history threshold comparisons to
  match OpenCode and Aider's own JSONL fallback path

* fix: use --command true for opencode run, validate sed key parameter

- Replace `-- noop` with `--command true` in opencode getLaunchCommand
  so the bootstrap uses a valid command
- Validate metadata key against [a-zA-Z0-9_-]+ in the git wrapper's
  update_ao_metadata to prevent sed metacharacter injection

* fix: extract DEFAULT_ACTIVE_WINDOW_MS constant, clarify Codex recordActivity

- Extract magic number 30_000 into DEFAULT_ACTIVE_WINDOW_MS constant in
  core types, used by Aider and OpenCode for active/ready thresholds
- Clarify in CLAUDE.md that Codex implements recordActivity as a safety
  net for when its native JSONL is missing/unparseable, not redundantly

* fix: add JSONL mtime fallback to Codex getActivityState

When native Codex session file is missing but AO JSONL has data,
derive active/ready/idle from JSONL mtime instead of returning null.
Matches the fallback pattern already in Aider (step 4) and OpenCode
(step 3).

* fix: validate ActivityState and source values when parsing JSONL entries

Validate that `state` is one of the known ActivityState values and
`source` is "terminal" or "native" before constructing the entry.
Construct the entry explicitly instead of using unsafe double cast.

* fix: wrap getOutput + recordActivity in try-catch to protect getActivityState

If runtime.getOutput() throws (e.g. tmux unresponsive), the error
previously propagated to the outer catch, skipping getActivityState
entirely. Now the entire recordActivity preamble is wrapped in its
own try-catch so getActivityState always runs.

* chore: add .ao/ to gitignore

* chore: clean up gitignore comment

* test: add coverage for activity-log and agent-workspace-hooks

- activity-log: test readLastActivityEntry (missing file), invalid
  entry.ts in checkActivityLogState, blocked state path
- agent-workspace-hooks: test buildAgentPath (dedup, defaults,
  ordering), setupPathWrapperWorkspace (create/skip wrappers,
  AGENTS.md create/skip)

Raises diff coverage from 39% toward 80% threshold.

* test: add real file I/O tests for readLastActivityEntry and recordTerminalActivity

Test readLastActivityEntry with actual temp files: valid entries,
empty file, invalid JSON, invalid state, missing fields, multi-line.
Test recordTerminalActivity dedup logic and actionable state bypass.

* fix: add activeWindow threshold to Codex native JSONL state detection

Action entries (tool_call, user_input, exec_command) now use the 30s
active window: <=30s is "active", 30s-5min is "ready", >5min is "idle".
Previously these skipped "ready" entirely, going straight from "active"
to "idle" at the 5min threshold.

* fix: add activeWindow threshold to Claude Code native JSONL state detection

Same fix as Codex — action entries (user, tool_use, progress) now use
the 30s active window for consistent 3-state classification across
all agent plugins: <=30s active, 30s-5min ready, >5min idle.

* fix: handle truncated JSONL, add exited state, fix session lookup and dedup window

- readLastActivityEntry: increase tail buffer to 4KB, skip truncated
  first line when reading from offset, try lines from end on parse error
- Add "exited" to valid ActivityState set in JSONL validation
- findOpenCodeSession: pick most recently updated session when multiple
  title matches exist, preventing stale session binding
- Increase dedup window from 20s to 60s so mtime can age past the 30s
  active window between writes, allowing "ready" state to be reached

* fix: use entry state for JSONL fallback, write AGENTS.md to .ao/, revert dedup to 20s

- Replace mtime-based active/ready/idle derivation in all 3 plugin
  fallbacks with direct entry.state + entry.ts usage. This eliminates
  the fundamental conflict between write deduplication and mtime
  freshness — the entry already has the correct detected state.
- Revert dedup window to 20s (purely I/O optimization, no longer
  affects state detection since mtime is not used)
- Write AO session context to .ao/AGENTS.md (gitignored) instead of
  modifying the repo-tracked AGENTS.md, preventing dirty worktree state

* fix: reorder Codex fallback chain so AO JSONL is checked before stat mtime

When native JSONL exists but can't be parsed, the stat() fallback
previously returned early, skipping AO JSONL waiting_input/blocked
detection and the ready state. Now it falls through to AO JSONL first,
then uses stat mtime as a last resort with proper 3-state classification.

* fix: re-validate canonicalized ao_dir against trusted roots

After resolving symlinks with pwd -P, re-check real_ao_dir against
the trusted root allowlist. Prevents paths like /tmp/../../home/user
from passing the pre-canonicalization check then escaping to arbitrary
directories after symlink resolution.

* fix: update tests for .ao/AGENTS.md location and remove unused vi import

- Update Codex setupWorkspaceHooks tests to expect .ao/AGENTS.md
  instead of workspace root AGENTS.md
- Remove unused vi import from activity-log test (lint error)

* fix: add age-based decay to JSONL entry fallback via getActivityFallbackState

Extract getActivityFallbackState in core — reclassifies entry state
based on entry.ts age (active→ready→idle) so old entries don't stay
as "active" forever when recordActivity stops being called. All three
plugins now use this shared helper for their JSONL fallback paths.

* fix: apply staleness cap to actionable states in getActivityFallbackState

Stale waiting_input/blocked entries (older than ACTIVITY_INPUT_STALENESS_MS)
are now treated as idle in the fallback, preventing them from bypassing
the staleness filtering in checkActivityLogState.

* fix: respect entry state as ceiling in getActivityFallbackState

Age-based decay can only demote (active→ready→idle), never promote.
A fresh "idle" entry stays "idle" instead of being reclassified as
"active" — the detected state from terminal output is authoritative.

* docs: update CLAUDE.md to match current activity detection implementation

- Replace inline recordActivity dedup example with recordTerminalActivity delegation
- Replace mtime-based fallback example with getActivityFallbackState
- Update step 4 description: entry state + age-based decay, not mtime
- Add new core exports to utilities section
- Document .ao/AGENTS.md location and setupPathWrapperWorkspace
- Update required test list for entry-based fallback

* fix: skip metadata helper rewrite when version marker matches

Move ao-metadata-helper.sh write inside the needsUpdate check so it's
only rewritten when wrapper scripts are outdated, not on every call.

* fix: update Codex test for metadata helper skip when version matches

Metadata helper is now inside the needsUpdate check, so when the
version marker matches, no wrapper writes occur (including helper).
2026-04-01 21:50:07 +05:30
.changeset chore: version packages 2026-03-29 09:21:26 +00:00
.cursor feat: publish to npm under @composio scope (#32) 2026-02-15 04:28:57 +05:30
.github/workflows ci: fix SSH fingerprint to use RSA host key 2026-04-01 04:18:51 +05:30
.husky feat: implement comprehensive security audit and secret leak prevention (#67) 2026-02-16 10:33:50 +05:30
artifacts chore: remove static CLAUDE.orchestrator.md (#143) 2026-02-22 14:29:17 +05:30
changelog fix: migrate to hash-based project isolation architecture 2026-02-18 00:19:55 +05:30
docs Merge pull request #836 from ChiragArora31/feat/status-watch-mode 2026-04-01 12:28:32 +05:30
examples docs: update port references to reflect configurability (#122) 2026-02-19 07:37:00 +05:30
openclaw-plugin fix: minor name update 2026-03-28 02:42:16 +05:30
packages feat: standardize agent plugins — shared hooks, activity JSONL, backfill aider/opencode (#755) 2026-04-01 21:50:07 +05:30
scripts Fix ao launcher linking and mobile toast offset 2026-03-26 22:47:25 +05:30
skills/agent-orchestrator feat: harden plugin, rewrite skill for ClawHub 2026-03-25 20:05:57 +05:30
tests/integration 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
.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: standardize agent plugins — shared hooks, activity JSONL, backfill aider/opencode (#755) 2026-04-01 21:50:07 +05:30
.gitignore-template feat: OpenCode session lifecycle and CLI controls (#315) 2026-03-08 09:55:44 +05:30
.gitleaks.toml feat: implement comprehensive security audit and secret leak prevention (#67) 2026-02-16 10:33:50 +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 feat: standardize agent plugins — shared hooks, activity JSONL, backfill aider/opencode (#755) 2026-04-01 21:50:07 +05:30
ARCHITECTURE.md fix: migrate to hash-based project isolation architecture 2026-02-18 00:19:55 +05:30
CLAUDE.md feat: standardize agent plugins — shared hooks, activity JSONL, backfill aider/opencode (#755) 2026-04-01 21:50:07 +05:30
CONTRIBUTING.md add plugin spec and update docs 2026-03-31 04:02:25 +05:30
DASHBOARD_FIXES_SUMMARY.md feat: seamless onboarding with enhanced documentation (#66) 2026-02-16 22:22:13 +05:30
DESIGN-OPENCLAW-PLUGIN.md feat(ao): add OpenClaw notifier plugin for AO escalations 2026-03-09 18:24:30 +05:30
DESIGN.md feat: add comprehensive design system (DESIGN.md) 2026-03-28 23:55:24 +05:30
LICENSE feat: publish to npm under @composio scope (#32) 2026-02-15 04:28:57 +05:30
README.md add plugin spec and update docs 2026-03-31 04:02:25 +05:30
SECURITY.md fix: migrate to hash-based project isolation architecture 2026-02-18 00:19:55 +05:30
SETUP.md add plugin spec and update docs 2026-03-31 04:02:25 +05:30
TROUBLESHOOTING.md fix: migrate to hash-based project isolation architecture 2026-02-18 00:19:55 +05:30
agent-orchestrator.yaml.example add plugin spec and update docs 2026-03-31 04:02:25 +05:30
eslint.config.js Merge main into chore/remove-mobile-app - resolve conflicts 2026-03-30 18:06:08 +05:30
package.json Publish ao-web on npm, harden error handling, deduplicate code 2026-03-14 19:01:49 +05:30
pnpm-lock.yaml ci: add diff coverage workflow enforcing 80% on changed code only 2026-03-31 09:26:36 +05:30
pnpm-workspace.yaml chore: remove React Native/Expo mobile app 2026-03-25 12:00:59 +05:30
test-ao-config.yaml feat: seamless onboarding with enhanced documentation (#66) 2026-02-16 22:22:13 +05:30
test-ao-config2.yaml feat: seamless onboarding with enhanced documentation (#66) 2026-02-16 22:22:13 +05:30
tsconfig.base.json feat: scaffold TypeScript monorepo with all plugin interfaces 2026-02-13 17:02:42 +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
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, 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+, tmux, gh CLI. Install tmux via brew install tmux (macOS) or sudo apt install tmux (Linux).

Install

npm install -g @composio/ao
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

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
# Runtime data is auto-derived under ~/.agent-orchestrator/{hash}-{projectId}/
port: 3000

defaults:
  runtime: tmux
  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.

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

Plugin Architecture

Seven plugin slots. Lifecycle stays in core.

Slot Default Alternatives
Runtime tmux process
Agent claude-code codex, aider, opencode
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