* 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> |
||
|---|---|---|
| .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, ConPTY/process, Docker) · Tracker-agnostic (GitHub, Linear)
Quick Start
Prerequisites: Node.js 20+, Git 2.25+,
ghCLI, and:
- macOS / Linux: tmux — install via
brew install tmuxorsudo apt install tmux.- Windows: PowerShell 7+ recommended. tmux is not required — AO uses native ConPTY via the
runtime-processplugin (the default on Windows). SetAO_SHELL=bashif you have Git Bash and prefer it.
Install
npm install -g @aoagents/ao
Nightly builds (latest
main, daily Fri–Tue):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
- 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 # 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