9.2 KiB
CLAUDE.md — Agent Orchestrator
What This Project Is
An open-source, agent-agnostic system for orchestrating parallel AI coding agents. Any coding agent, any repo, any issue tracker, any runtime. The system manages session lifecycle, tracks PR/CI/review state, auto-handles routine issues (CI failures, review comments), and pushes notifications to humans only when their judgment is needed.
Core principle: Push, not pull. The human spawns agents, walks away, and gets notified when needed.
Tech Stack
- Language: TypeScript throughout (ESM, Node 20+, strict mode)
- Monorepo: pnpm workspaces
- Web: Next.js 15 (App Router) + Tailwind CSS
- CLI: Commander.js
- Config: YAML + Zod validation
- Real-time: Server-Sent Events
- State: Flat metadata files + JSONL event log
- Linting: ESLint (flat config) + Prettier
- Testing: vitest
Architecture
8 plugin slots — every abstraction is swappable:
| Slot | Interface | Default Plugin |
|---|---|---|
| Runtime | Runtime |
tmux |
| Agent | Agent |
claude-code |
| Workspace | Workspace |
worktree |
| Tracker | Tracker |
github |
| SCM | SCM |
github |
| Notifier | Notifier |
desktop |
| Terminal | Terminal |
iterm2 |
| Lifecycle | (core, not pluggable) | — |
All interfaces are defined in packages/core/src/types.ts. Read this file first — it is the source of truth for all abstractions.
Directory Structure
packages/
core/ — @agent-orchestrator/core (types, config, services)
cli/ — @agent-orchestrator/cli (the `ao` command)
web/ — @agent-orchestrator/web (Next.js dashboard)
plugins/
runtime-tmux/ — tmux session runtime
runtime-process/ — child process runtime
agent-claude-code/ — Claude Code adapter
agent-codex/ — Codex CLI adapter
agent-aider/ — Aider adapter
workspace-worktree/ — git worktree isolation
workspace-clone/ — git clone isolation
tracker-github/ — GitHub Issues tracker
tracker-linear/ — Linear tracker
scm-github/ — GitHub PRs, CI, reviews
notifier-desktop/ — OS desktop notifications
notifier-slack/ — Slack notifications
notifier-webhook/ — Generic webhook notifications
terminal-iterm2/ — macOS iTerm2 tab management
terminal-web/ — xterm.js web terminal
Code Conventions
TypeScript (MUST follow)
- ESM modules — all packages use
"type": "module" .jsextensions in imports — required for ESM:import { foo } from "./bar.js"node:prefix for builtins —import { readFileSync } from "node:fs", never bare"fs"- Strict mode —
"strict": truein all tsconfig typeimports — useimport type { Foo }for type-only imports (enforced by ESLint)- No
any— useunknownand narrow with type guards.anyis an ESLint error - No
as unknown as Tcasts — validate data instead of unsafe casting - Prefer
const—letonly when reassignment is needed, nevervar - Semicolons — always use them
- Double quotes — for strings (enforced by Prettier)
- 2-space indentation — (enforced by Prettier)
Plugin Pattern (MUST follow)
Every plugin exports a PluginModule with type-safe satisfies:
import type { PluginModule, Runtime } from "@agent-orchestrator/core";
const manifest = {
name: "tmux",
slot: "runtime" as const,
description: "Runtime plugin: tmux sessions",
version: "0.1.0",
};
function create(): Runtime {
return {
name: "tmux",
// ... implement interface methods
};
}
export default { manifest, create } satisfies PluginModule<Runtime>;
Do NOT use const plugin = { ... }; export default plugin; — always use inline satisfies for compile-time type checking.
Shell Command Execution (MUST follow)
- Always use
execFile(orspawn) fromnode:child_process— NEVERexec execruns through a shell and is vulnerable to injectionexecFilepasses args as an array, bypassing shell interpretation- Always add timeouts —
{ timeout: 30_000 }for external commands - Escape user-provided values — never interpolate into command strings
- Shell escaping: Do NOT use
JSON.stringifyfor shell escaping — it is not a shell escaping function
// GOOD
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
const { stdout } = await execFileAsync("git", ["branch", "--show-current"], { timeout: 30_000 });
// BAD — shell injection risk
import { exec } from "node:child_process";
exec(`git checkout ${branchName}`); // branchName could contain ; rm -rf /
Error Handling
- Throw typed errors, don't return error codes
- Plugin methods should throw if they can't do their job
- The core services catch and handle plugin errors
- Always wrap
JSON.parsein try/catch — corrupted metadata should not crash the system - Guard external data — validate types from API/CLI/file inputs before using them
Naming
- Files:
kebab-case.ts - Types/Interfaces:
PascalCase - Functions/variables:
camelCase - Constants:
UPPER_SNAKE_CASEonly for true constants (env vars, regex patterns) - Plugin names:
kebab-casematching directory name - Test files:
*.test.tsco-located with source, or in__tests__/directory
Imports Order
- Node builtins (
node:fs,node:path, etc.) - External packages (
commander,chalk,yaml, etc.) - Workspace packages (
@agent-orchestrator/core) - Relative imports (
./foo.js)
Testing
- Use vitest for all tests
- Mock external dependencies (
child_process,fs, HTTP calls) - Co-locate test files:
src/foo.test.tsorsrc/__tests__/foo.test.ts - Test edge cases: corrupted data, missing files, timeout, concurrent access
Building & Development
pnpm install # install all deps
pnpm build # build all packages
pnpm typecheck # typecheck all packages
pnpm lint # ESLint check
pnpm lint:fix # ESLint auto-fix
pnpm format # Prettier format
pnpm format:check # Prettier check (CI)
pnpm test # run all tests
Before Committing
Always run lint and typecheck:
pnpm lint && pnpm typecheck
Fix any issues before pushing. CI will reject PRs that fail lint or typecheck.
Config
- Config is loaded from
agent-orchestrator.yaml(see.yaml.example) - All paths support
~expansion - Per-project overrides for plugins and reactions
- Validated with Zod schema at load time
Reference Implementation
The scripts/ directory contains the original bash scripts that this TypeScript codebase replaces. Use them as behavioral specifications:
| Script | What It Specifies |
|---|---|
claude-ao-session |
Session lifecycle (spawn, list, kill, cleanup) |
claude-dashboard |
Web dashboard, API, activity detection |
claude-batch-spawn |
Batch spawning with duplicate detection |
send-to-session |
Smart message delivery (busy detection, wait-for-idle) |
claude-status |
JSONL introspection, live branch detection |
claude-review-check |
PR review scanning, fix prompt generation |
claude-bugbot-fix |
Automated comment detection + fixes |
claude-session-status |
Activity classification (working/idle/blocked) |
get-claude-session-info |
Agent introspection (session ID, summary extraction) |
Key Design Decisions
- Stateless orchestrator — no database, just flat metadata files + event log
- Plugins implement interfaces — every plugin is a pure implementation of an interface from
types.ts - Push notifications — the Notifier is the primary human interface, not the dashboard
- Two-tier event handling — auto-handle routine issues (CI, reviews), notify human only when judgment is needed
- Backwards-compatible metadata — flat key=value files matching the existing bash script format
- Security first —
execFilenotexec, validate all external input, escape strings for target context
Common Mistakes to Avoid
- Using
execinstead ofexecFile— security vulnerability - Using
JSON.stringifyfor shell escaping — does not escape$, backticks,$() - Missing
.jsextension in local imports — will fail at runtime with ESM - Using bare
"fs"instead of"node:fs"— inconsistent, may break in edge cases - Casting with
as unknown as T— bypasses type safety, crashes on bad data export default pluginwithoutsatisfies PluginModule<T>— loses type checking- Interpolating user input into shell commands, AppleScript, or GraphQL queries
- Forgetting to clean up setInterval/setTimeout on disconnect/destroy
- Using
on("exit")instead ofonce("exit")for one-time handlers