* rebase: forward branch onto main + resolve activity-events kind union conflict * feat(core): wire scm/runtime/agent plugin-call failure events Adds activity-event evidence for previously-silent failure paths in lifecycle-manager.ts so the RCA agent can answer 'why did X happen?': - scm.batch_enrich_failed (line 617 catch) - scm.detect_pr_succeeded (line 658 success path) - scm.detect_pr_failed (line 664 catch) - scm.review_fetch_failed (line 1517 catch) - scm.poll_pr_failed (line 1132 catch) - runtime.probe_failed (line 938 catch) - agent.process_probe_failed (lines 1054 + 1139 catches, with where field) - agent.activity_probe_failed (line 1062 outer catch) Plus 6 new tests covering the call shapes. Invariants preserved (per CLAUDE.md): - B1 state-mutate-before-emit: each emit follows existing observer call - B2 never throws: recordActivityEvent best-effort by design - B3 re-entrancy guard unchanged - B4 Promise.allSettled semantics unchanged * feat(core): wire reaction lifecycle activity events Adds AE evidence around reaction triggers, escalations, and failures so RCA can answer 'did AO try to auto-fix this? did it succeed?': - reaction.action_succeeded (combined for send-to-agent / notify / auto-merge, with data.action variant) — fires after each successful reaction action - reaction.send_to_agent_failed — fires in the previously-silent catch when sessionManager.send throws inside a send-to-agent reaction - reaction.escalated — fires alongside the existing notifyHuman escalation with data.escalationCause = 'max_retries' | 'max_duration' Plus 3 new tests covering the call shapes. Invariants preserved: emits land after the existing notifyHuman/return paths so state mutation order is unchanged. * feat(core): wire auto-cleanup, poll-cycle, detecting escalation events Adds AE evidence around session destruction, poll loop failures, and the detecting→stuck transition so RCA can answer 'when did my session get cleaned up?', 'did the polling loop crash?', and 'why did AO mark this session stuck?': - session.auto_cleanup_deferred — agent busy, cleanup deferred - session.auto_cleanup_completed — kill succeeded, runtime + worktree gone - session.auto_cleanup_failed (level=error) — kill threw, session stays merged - lifecycle.poll_failed (level=error) — pollAll outer catch fired - detecting.escalated — first cycle that promotes detecting→stuck, with cause = max_attempts | max_duration. Guarded by detectingEscalatedAt metadata so it fires once per escalation, not on every poll while stuck. Plus 5 new tests covering the call shapes and the idempotency guard. Invariants preserved: - Auto-cleanup events fire AFTER existing observer.recordOperation (B1) - detecting.escalated emits ONCE per escalation (invariant B9 in .context/lifecycle-manager-instrumentation.md) - poll_failed emits inside the existing pollAll catch — flow unchanged * feat(core): wire report_watcher.triggered activity event Adds AE evidence when the report watcher fires (no_acknowledge / stale_report / agent_needs_input). RCA: 'AO thinks my agent is stuck — why?' - report_watcher.triggered (level=warn) — emitted alongside the existing observer.recordOperation, only when a trigger is non-null (per invariant in .context/lifecycle-manager-instrumentation.md §B9) Plus 1 test exercising the no_acknowledge trigger path. * fix(core): one-shot guard on report_watcher.triggered AE emit Live-observed regression: report_watcher.triggered fired 116 times in production over a few hours because the emit was unguarded and re-fired every 30s poll while a trigger stayed active. Symptom was massive event flood for stuck/no-acknowledge/stale conditions. Fix: gate the emit on the existing isNewTrigger variable (same one-shot guard pattern used for detecting.escalated). The observer.recordOperation above remains unguarded by design (it's a metric/heartbeat); the AE trail is for actionable evidence only. Adds a regression test that drives the same trigger across two polls and asserts the AE event fires only on the first. * fix(core): address Greptile feedback on PR #1620 Two findings from Greptile (issue same as Codex P2 #1): 1. scm.batch_enrich_failed omitted projectId/sessionId — when the lifecycle worker is project-scoped (deps.projectId set), this event is effectively project-scoped too. Without projectId, queries like `ao events list --project todo-app --type scm.batch_enrich_failed` return zero results, defeating the purpose of the instrumentation. Fix: pass scopedProjectId when set. Unscoped (multi-project) supervisors still leave projectId null because the batch crosses project boundaries. 2. Misleading field name pendingSinceMs in session.auto_cleanup_deferred data — the local variable of the same name is a Unix epoch timestamp, but the data field stored `Date.now() - pendingSinceMs` (an elapsed duration). RCA agents would mis-interpret it as a timestamp and compute a 1970-era "pending since" date. Renamed to pendingElapsedMs. * fix(core): address Codex review on PR #1620 - lifecycle.poll_failed: keep summary generic, route raw error text through `data.errorMessage` only. sanitizeSummary just truncates; sanitizeData redacts credential URLs. Since FTS5 indexes summary, interpolating subprocess error output (which can include https://x-oauth-basic:TOKEN@github.com/... from git/gh) made credentials persistently searchable. - reaction.escalated: expand escalationCause to "max_retries" | "max_attempts" | "max_duration" and mirror the trigger checks. Numeric escalateAfter is an attempt-count gate, not a duration; previously got misattributed to "max_duration" whenever retries was unset (built-in defaults use {escalateAfter: 2}). Adds two regression tests as guards for both behaviors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(core): replace import() type annotation with import type to satisfy lint CI's @typescript-eslint/consistent-type-imports rule rejects inline `typeof import("../activity-events.js")` inside the vi.mock factory. Hoist it to a top-level `import type * as ActivityEventsModule` so the type lives in a proper import declaration; vi.mock factory resolution is unaffected (type-only imports emit no runtime code). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(core): keep report_watcher.triggered summary generic to plug FTS leak auditResult.message for the agent_needs_input trigger embeds the free-form report.note supplied via `ao report --note "..."`. Since sanitizeSummary only truncates and FTS5 indexes the summary column, a note containing a credential URL would be persistently searchable from the events DB. Same class of bug as the prior poll_failed fix. Summary becomes generic ("<trigger> triggered"); the full message continues to flow through `data.message` where sanitizeData redacts credential URLs. Adds a regression test that seeds a needs_input report with a credential-bearing note and asserts the summary stays clean. Reported by @ashish921998 in PR #1620. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(core): redact token-shaped secrets in activity-event data (P1) Both `summary` and `data` columns are FTS5-indexed (events-db.ts:58-59). Prior fixes moved raw error/report text from `summary` to `data.message` / `data.errorMessage`, on the assumption that sanitizeData() would scrub it. That assumption was incomplete: sanitizeData only redacted credential URLs and entire values under sensitive *key* names. Token-shaped substrings (`Bearer …`, `ghp_…`, `sk-…`, JWTs, `AKIA…`, ALL_CAPS_TOKEN=value) under non-sensitive keys like `message`/`errorMessage` were stored as-is and made searchable via FTS. Adds a TOKEN_PATTERNS array applied to every string value during sanitization, plus a 500-char per-string cap (matching sanitizeSummary's existing precedent — limits blast radius if a new token format slips past the patterns). Patterns cover: Bearer headers, GitHub PATs (classic + fine-grained), OpenAI/Anthropic sk- keys, Slack xox- tokens, AWS access key IDs, JWTs, and ENV-style assignments scoped to ALL_CAPS keys ending in TOKEN/PASSWORD/SECRET/etc. Tests: - 10 new sanitizeString unit tests (one per token shape + prose-preservation regression guard + 500-char cap + nested array/object recursion) - 1 new FTS5 integration test that drives recordActivityEvent → real SQLite → both direct row read and FTS MATCH must return zero token leakage Test fixtures use string concatenation across the prefix boundary so literal token shapes don't appear in source (gitleaks pre-commit guard). Reported by @ashish921998 in PR #1620. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(core): bound credential-URL regex to prevent ReDoS (CodeQL alert) CodeQL flagged CREDENTIAL_URL_RE as polynomial: input shaped like `http://http://http://...` with no terminating `@` caused O(n²) backtracking because the unbounded `[^@\s]+` greedily spanned multiple `http://` prefixes before failing at end-of-string and walking back. Two-part fix: 1. Exclude `/` from the userinfo character class — this is also semantically correct since RFC 3986 userinfo cannot contain unencoded `/`. 2. Add a hard length cap (200 chars) on the userinfo segment as a belt-and- braces guard against future pathological inputs. The fix is observable: 14KB pathological input completes in single-digit ms post-fix vs multiple seconds pre-fix. Adds a regression test that runs the pathological input through the full sanitize pipeline and asserts <100ms completion. Reported by GitHub Advanced Security on PR #1620. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(core): replace CREDENTIAL_URL_RE regex with linear scan The bounded {1,200} quantifier in CREDENTIAL_URL_RE let credential URLs with >200-char userinfo pass through unredacted. Since data is FTS5-indexed, those credentials became searchable (P1 from PR #1620 review). Replace the regex with a simple linear scan (redactCredentialUrls) that: - Has no length limit — scans until @, space, or / - Is O(n) with no regex backtracking (fixes CodeQL polynomial-regex alert) - Matches http:// and https:// case-insensitively (preserves old /gi behavior) Adds regression tests for: - >200-char userinfo bypass - URLs without userinfo (no false positives) - Multiple credential URLs in one string - Pathological ReDoS-shaped input still completes in <100ms --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: AO Bot <ao-bot@composio.dev> |
||
|---|---|---|
| .changeset | ||
| .cursor | ||
| .github | ||
| .husky | ||
| .issue-assets | ||
| artifacts | ||
| changelog | ||
| completions | ||
| docs | ||
| examples | ||
| handoff/pr-1466 | ||
| openclaw-plugin | ||
| packages | ||
| schema | ||
| scripts | ||
| skills | ||
| tests/integration | ||
| website | ||
| .eslintignore | ||
| .gitignore | ||
| .gitleaks.toml | ||
| .npmrc | ||
| .prettierignore | ||
| .prettierrc | ||
| AGENTS.md | ||
| ARCHITECTURE.md | ||
| CLAUDE.md | ||
| CONTRIBUTING.md | ||
| DESIGN.md | ||
| LICENSE | ||
| README.md | ||
| SECURITY.md | ||
| SETUP.md | ||
| TROUBLESHOOTING.md | ||
| agent-orchestrator.yaml.example | ||
| eslint.config.js | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| tsconfig.base.json | ||
| tsconfig.node.json | ||
README.md
Agent Orchestrator — The Orchestration Layer for Parallel AI Agents
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.
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)
Quick Start
Prerequisites: Node.js 20+, Git 2.25+, tmux,
ghCLI. Install tmux viabrew install tmux(macOS) orsudo apt install tmux(Linux).
Install
npm install -g @aoagents/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
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
- You start —
ao startlaunches the dashboard and an orchestrator agent - Orchestrator spawns workers — each issue gets its own agent in an isolated git worktree
- Agents work autonomously — they read code, write tests, create PRs
- Reactions handle feedback — CI failures and review comments are automatically routed back to the agent
- 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
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
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).
Plugin Architecture
Seven plugin slots. Lifecycle stays in core.
| Slot | Default | Alternatives |
|---|---|---|
| Runtime | tmux | process |
| 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