docs: comprehensively optimize CLAUDE.md for agent effectiveness (#38)
* docs: condense CLAUDE.md for token efficiency Reduced CLAUDE.md from 223 to 169 lines (24% reduction). Changes: - Removed verbose sections (reference implementation table, redundant explanations) - Added "Key Files" section highlighting types.ts and plugin examples upfront - Condensed tech stack, conventions, and commands into scannable format - Kept critical content: plugin pattern, shell security, common mistakes Agents spawned by the orchestrator don't need special documentation - they just read the repo's existing CLAUDE.md. The orchestrator is transparent. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * docs: comprehensively optimize CLAUDE.md and add package READMEs Major improvements to agent effectiveness on this codebase: ## Enhanced CLAUDE.md (169 → 444 lines) **New sections:** - Quick Start — get oriented fast (links to common tasks) - Looking for X? — quick reference table for finding code - Monorepo Tools — pnpm workspace commands (filter, watch mode, scoped builds) - Common Tasks — step-by-step guides (add plugin, add Session field, add event type) - Plugin Development — pattern explanation + examples (notifier-desktop, agent-claude-code) - Architecture Deep Dive — data flow diagram + state machine + key abstractions **Improved sections:** - Commands — added watch mode, filtering, scoped operations - Shell Security — added exploit example showing actual injection - Common Mistakes — added 5 code examples (BAD vs GOOD with explanations) - Design Decisions — added "Why" for each decision ## New Package READMEs (Progressive Disclosure) **packages/core/README.md:** - Explains core services (SessionManager, LifecycleManager, PluginRegistry) - Key files guide (types.ts, session-manager.ts, lifecycle-manager.ts) - Common tasks specific to core - Architecture notes (why flat metadata, why polling, why plugin slots) **packages/plugins/runtime-tmux/README.md:** - How the plugin works (creating sessions, sending messages, getting output) - Security considerations (session ID validation) - Common issues (tmux not installed, detached sessions persist) - Limitations (macOS/Linux only, no resource limits) - Architecture notes (why tmux over raw processes) ## Impact Agents working on this codebase now have: 1. **Faster discovery** — "Looking for X?" table + Quick Start links 2. **Actionable guides** — step-by-step for common tasks 3. **Concrete examples** — code showing actual mistakes and fixes 4. **Progressive disclosure** — package READMEs for deep dives 5. **Monorepo fluency** — pnpm workspace commands documented 6. **Architecture understanding** — data flow + state machine + "why" explanations Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
baaabe537a
commit
8db5f2b161
473
CLAUDE.md
473
CLAUDE.md
|
|
@ -1,5 +1,11 @@
|
|||
# CLAUDE.md — Agent Orchestrator
|
||||
|
||||
## Quick Start
|
||||
|
||||
- **Adding a plugin?** → [Plugin Development](#plugin-development)
|
||||
- **Modifying core types?** → Read `packages/core/src/types.ts` first, then [Architecture](#architecture-deep-dive)
|
||||
- **First contribution?** → Read [What This Is](#what-this-is), [Key Files](#key-files), [Monorepo Tools](#monorepo-tools)
|
||||
|
||||
## What This Is
|
||||
|
||||
Open-source system for orchestrating parallel AI coding agents. Agent-agnostic (Claude Code, Codex, Aider), runtime-agnostic (tmux, docker, k8s), tracker-agnostic (GitHub, Linear, Jira). Manages session lifecycle, tracks PR/CI/review state, auto-handles routine issues (CI failures, review comments), pushes notifications to humans only when needed.
|
||||
|
|
@ -14,44 +20,273 @@ TypeScript (ESM), Node 20+, pnpm workspaces. Next.js 15 (App Router) + Tailwind.
|
|||
|
||||
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) | — |
|
||||
| Slot | Interface | Default Plugin | Purpose |
|
||||
| --------- | ----------- | -------------- | --------------------------------- |
|
||||
| Runtime | `Runtime` | tmux | Where sessions execute |
|
||||
| Agent | `Agent` | claude-code | AI coding tool adapter |
|
||||
| Workspace | `Workspace` | worktree | Code isolation (worktree, clone) |
|
||||
| Tracker | `Tracker` | github | Issue tracking (GitHub, Linear) |
|
||||
| SCM | `SCM` | github | PR/CI/reviews |
|
||||
| Notifier | `Notifier` | desktop | Push notifications |
|
||||
| Terminal | `Terminal` | iterm2 | Human interaction UI |
|
||||
| Lifecycle | (core) | — | State machine + reactions (core) |
|
||||
|
||||
**All interfaces defined in `packages/core/src/types.ts` — read this file first.**
|
||||
|
||||
## Directory Structure
|
||||
## Key Files
|
||||
|
||||
```
|
||||
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,process}/
|
||||
agent-{claude-code,codex,aider,opencode}/
|
||||
workspace-{worktree,clone}/
|
||||
tracker-{github,linear}/
|
||||
scm-github/
|
||||
notifier-{desktop,slack,composio,webhook}/
|
||||
terminal-{iterm2,web}/
|
||||
1. **`packages/core/src/types.ts`** — source of truth for all interfaces
|
||||
2. **`packages/core/src/services/session-manager.ts`** — session CRUD + spawn logic
|
||||
3. **`packages/core/src/services/lifecycle-manager.ts`** — state machine + reactions
|
||||
4. **`packages/core/src/services/plugin-registry.ts`** — plugin discovery + loading
|
||||
5. **`agent-orchestrator.yaml.example`** — config format
|
||||
|
||||
## Looking for X?
|
||||
|
||||
| You want to... | Look here |
|
||||
| ---------------------------------- | ------------------------------------------------------------ |
|
||||
| Add a new plugin | `packages/plugins/`, follow `notifier-desktop` pattern |
|
||||
| Add a field to Session | `packages/core/src/types.ts` → `Session` interface |
|
||||
| Add an event type | `packages/core/src/types.ts` → `EventType` union |
|
||||
| Modify spawn logic | `packages/core/src/services/session-manager.ts` → `spawn()` |
|
||||
| Modify state machine | `packages/core/src/services/lifecycle-manager.ts` |
|
||||
| Add a CLI command | `packages/cli/src/commands/` |
|
||||
| Modify web dashboard | `packages/web/src/` |
|
||||
| Add a reaction | `packages/core/src/services/lifecycle-manager.ts` → handlers |
|
||||
| Test a plugin | `packages/plugins/<plugin>/src/__tests__/` |
|
||||
| Modify config schema | `packages/core/src/config.ts` → Zod schemas |
|
||||
|
||||
## Monorepo Tools
|
||||
|
||||
```bash
|
||||
# Install all dependencies
|
||||
pnpm install
|
||||
|
||||
# Build all packages
|
||||
pnpm build
|
||||
|
||||
# Build one package (builds dependencies automatically)
|
||||
pnpm --filter @agent-orchestrator/core build
|
||||
|
||||
# Run all tests
|
||||
pnpm test
|
||||
|
||||
# Run tests in one package
|
||||
pnpm --filter @agent-orchestrator/core test
|
||||
|
||||
# Run tests in watch mode
|
||||
pnpm --filter @agent-orchestrator/core test -- --watch
|
||||
|
||||
# Add a dependency to a package
|
||||
pnpm --filter @agent-orchestrator/core add <package-name>
|
||||
|
||||
# Lint all
|
||||
pnpm lint
|
||||
|
||||
# Typecheck all
|
||||
pnpm typecheck
|
||||
|
||||
# Before committing (MUST pass)
|
||||
pnpm lint && pnpm typecheck
|
||||
```
|
||||
|
||||
## Key Files (Read These First)
|
||||
## Common Tasks
|
||||
|
||||
1. `packages/core/src/types.ts` — all interfaces (Runtime, Agent, Workspace, Tracker, SCM, Notifier, Terminal)
|
||||
2. `agent-orchestrator.yaml.example` — config format
|
||||
3. Plugin examples:
|
||||
- `packages/plugins/runtime-tmux/src/index.ts` — Runtime implementation
|
||||
- `packages/plugins/agent-claude-code/src/index.ts` — Agent implementation
|
||||
4. This file (CLAUDE.md) — code conventions
|
||||
### Adding a New Plugin
|
||||
|
||||
1. **Create plugin directory**: `packages/plugins/<slot>-<name>/`
|
||||
```bash
|
||||
mkdir -p packages/plugins/runtime-docker/src
|
||||
cd packages/plugins/runtime-docker
|
||||
```
|
||||
|
||||
2. **Create package.json**:
|
||||
```json
|
||||
{
|
||||
"name": "@agent-orchestrator/plugin-runtime-docker",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agent-orchestrator/core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Create tsconfig.json** (copy from another plugin)
|
||||
|
||||
4. **Implement plugin** in `src/index.ts`:
|
||||
```typescript
|
||||
import type { PluginModule, Runtime } from "@agent-orchestrator/core";
|
||||
|
||||
export const manifest = {
|
||||
name: "docker",
|
||||
slot: "runtime" as const,
|
||||
description: "Runtime plugin: Docker containers",
|
||||
version: "0.1.0",
|
||||
};
|
||||
|
||||
export function create(): Runtime {
|
||||
return {
|
||||
name: "docker",
|
||||
async create(config) { /* ... */ },
|
||||
async destroy(handle) { /* ... */ },
|
||||
async sendMessage(handle, message) { /* ... */ },
|
||||
async getOutput(handle, lines) { /* ... */ },
|
||||
async isAlive(handle) { /* ... */ },
|
||||
};
|
||||
}
|
||||
|
||||
export default { manifest, create } satisfies PluginModule<Runtime>;
|
||||
```
|
||||
|
||||
5. **Build and test**:
|
||||
```bash
|
||||
pnpm --filter @agent-orchestrator/plugin-runtime-docker build
|
||||
pnpm --filter @agent-orchestrator/plugin-runtime-docker test
|
||||
```
|
||||
|
||||
6. **Register in core** (if built-in): `packages/core/src/services/plugin-registry.ts` → `loadBuiltins()`
|
||||
|
||||
### Adding a Field to Session
|
||||
|
||||
1. **Update Session interface**: `packages/core/src/types.ts`
|
||||
```typescript
|
||||
export interface Session {
|
||||
// ... existing fields
|
||||
newField: string | null; // Add your field
|
||||
}
|
||||
```
|
||||
|
||||
2. **Update SessionManager**: `packages/core/src/services/session-manager.ts`
|
||||
- Initialize the field in `spawn()`
|
||||
- Update metadata read/write if needed
|
||||
|
||||
3. **Update web dashboard** (if displayed): `packages/web/src/components/`
|
||||
|
||||
4. **Rebuild core**:
|
||||
```bash
|
||||
pnpm --filter @agent-orchestrator/core build
|
||||
```
|
||||
|
||||
### Adding an Event Type
|
||||
|
||||
1. **Add to EventType union**: `packages/core/src/types.ts`
|
||||
```typescript
|
||||
export type EventType =
|
||||
| "session.spawned"
|
||||
// ... existing events
|
||||
| "your.new_event"; // Add here
|
||||
```
|
||||
|
||||
2. **Emit the event**: In the relevant service, use `eventEmitter.emit()`
|
||||
|
||||
3. **Add reaction handler** (optional): `packages/core/src/services/lifecycle-manager.ts`
|
||||
|
||||
## Plugin Development
|
||||
|
||||
### The Plugin Pattern
|
||||
|
||||
Every plugin exports:
|
||||
- **`manifest`** — metadata (name, slot, description, version)
|
||||
- **`create()`** — factory function that returns the interface implementation
|
||||
- **`default export`** — `{ manifest, create } satisfies PluginModule<T>`
|
||||
|
||||
**Why `satisfies`?** Compile-time type checking. Using `const plugin = { ... }; export default plugin;` loses type safety.
|
||||
|
||||
### Simplest Example: notifier-desktop
|
||||
|
||||
See `packages/plugins/notifier-desktop/src/index.ts` — ~150 lines, implements `Notifier` interface, uses `osascript` (macOS) or `notify-send` (Linux).
|
||||
|
||||
### Most Complete Example: agent-claude-code
|
||||
|
||||
See `packages/plugins/agent-claude-code/src/index.ts` — implements `Agent` interface, includes:
|
||||
- Process detection (ps, TTY lookup)
|
||||
- JSONL parsing (session info extraction)
|
||||
- Activity classification (terminal output patterns)
|
||||
- Post-launch setup (hook injection)
|
||||
|
||||
### Testing Plugins
|
||||
|
||||
1. **Create test file**: `packages/plugins/<plugin>/src/__tests__/index.test.ts`
|
||||
2. **Mock dependencies**: Use vitest `vi.mock()` for `child_process`, `fs`, etc.
|
||||
3. **Test edge cases**: timeouts, corrupted data, missing files, concurrent access
|
||||
|
||||
Example structure:
|
||||
```typescript
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { create } from "../index.js";
|
||||
|
||||
describe("my-plugin", () => {
|
||||
it("should handle timeout", async () => {
|
||||
const plugin = create();
|
||||
// ... test timeout scenario
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Architecture Deep Dive
|
||||
|
||||
### Data Flow: Spawn → Execute
|
||||
|
||||
```
|
||||
CLI: ao spawn my-app issue-42
|
||||
↓
|
||||
SessionManager.spawn()
|
||||
↓ reads config
|
||||
↓ generates prompt via Tracker.generatePrompt()
|
||||
↓
|
||||
Workspace.create() → creates worktree/clone
|
||||
↓
|
||||
Agent.getLaunchCommand() → builds command
|
||||
Agent.getEnvironment() → sets env vars
|
||||
↓
|
||||
Runtime.create() → starts session (tmux/docker/k8s)
|
||||
↓ sends launch command
|
||||
↓
|
||||
Agent starts executing in workspace
|
||||
↓
|
||||
LifecycleManager polls → detects state changes
|
||||
↓
|
||||
Reactions trigger (CI failures, review comments)
|
||||
↓
|
||||
Notifier.notify() → pushes to human
|
||||
```
|
||||
|
||||
### State Machine: Session Lifecycle
|
||||
|
||||
```
|
||||
spawning
|
||||
↓ agent starts
|
||||
working
|
||||
↓ PR created
|
||||
pr_open
|
||||
↓ CI fails → ci_failed (reaction: send fix prompt)
|
||||
↓ CI passes + review pending → review_pending
|
||||
↓ changes requested → changes_requested (reaction: send review comments)
|
||||
↓ approved + CI passing → approved (reaction: notify human)
|
||||
↓ ready to merge → mergeable (reaction: notify or auto-merge)
|
||||
↓ merged
|
||||
merged
|
||||
↓ cleanup
|
||||
(session killed)
|
||||
```
|
||||
|
||||
### Key Abstractions
|
||||
|
||||
- **Session** — a running agent instance (state, metadata, runtime handle)
|
||||
- **RuntimeHandle** — opaque handle to communicate with a session (tmux session name, container ID, pod name)
|
||||
- **PluginModule** — what every plugin exports (`manifest` + `create()`)
|
||||
- **OrchestratorEvent** — events emitted by lifecycle manager (session.spawned, pr.created, ci.failing, etc.)
|
||||
- **ReactionConfig** — rules for auto-responding to events (send-to-agent, notify, auto-merge)
|
||||
|
||||
## TypeScript Conventions (MUST follow)
|
||||
|
||||
|
|
@ -65,50 +300,107 @@ packages/
|
|||
- **Prefer `const`** — `let` only when reassignment needed, never `var`
|
||||
- **Semicolons, double quotes, 2-space indent** — enforced by Prettier
|
||||
|
||||
## Plugin Pattern (MUST follow)
|
||||
|
||||
Every plugin exports a `PluginModule` with inline `satisfies` for compile-time type checking:
|
||||
|
||||
```typescript
|
||||
import type { PluginModule, Runtime } from "@agent-orchestrator/core";
|
||||
|
||||
export const manifest = {
|
||||
name: "tmux",
|
||||
slot: "runtime" as const,
|
||||
description: "Runtime plugin: tmux sessions",
|
||||
version: "0.1.0",
|
||||
};
|
||||
|
||||
export function create(): Runtime {
|
||||
return {
|
||||
name: "tmux",
|
||||
async create(config) { /* ... */ },
|
||||
async destroy(handle) { /* ... */ },
|
||||
// ... implement interface methods
|
||||
};
|
||||
}
|
||||
|
||||
export default { manifest, create } satisfies PluginModule<Runtime>;
|
||||
```
|
||||
|
||||
**Do NOT** use `const plugin = { ... }; export default plugin;` — always inline `satisfies`.
|
||||
|
||||
## Shell Command Execution (MUST follow — security critical)
|
||||
|
||||
- **Always use `execFile`** (or `spawn`) — NEVER `exec` (shell injection risk)
|
||||
- **Always add timeouts** — `{ timeout: 30_000 }` for external commands
|
||||
- **Never interpolate user input** — pass as array args, not string template
|
||||
- **Do NOT use `JSON.stringify` for shell escaping** — not a shell escaping function
|
||||
**Always use `execFile` or `spawn`, NEVER `exec`**
|
||||
|
||||
### Why? exec is vulnerable to shell injection
|
||||
|
||||
**Exploit example:**
|
||||
```typescript
|
||||
// GOOD
|
||||
// VULNERABLE
|
||||
import { exec } from "node:child_process";
|
||||
const branchName = "feat/add-feature; rm -rf /"; // malicious input
|
||||
exec(`git checkout ${branchName}`); // executes: git checkout feat/add-feature; rm -rf /
|
||||
```
|
||||
|
||||
**Safe:**
|
||||
```typescript
|
||||
// SAFE
|
||||
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
|
||||
exec(`git checkout ${branchName}`); // branchName could contain ; rm -rf /
|
||||
const branchName = "feat/add-feature; rm -rf /"; // malicious input
|
||||
await execFileAsync("git", ["checkout", branchName], { timeout: 30_000 });
|
||||
// git receives the string literally, no shell interpretation
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- **Always use `execFile`** (or `spawn`) — args as array, bypasses shell
|
||||
- **Always add timeouts** — `{ timeout: 30_000 }` for external commands
|
||||
- **Never interpolate user input** — pass as array args, not string template
|
||||
- **Do NOT use `JSON.stringify` for shell escaping** — it doesn't escape `$`, backticks, `$()`
|
||||
|
||||
## Common Mistakes with Examples
|
||||
|
||||
### 1. Missing `.js` extension in imports
|
||||
|
||||
```typescript
|
||||
// BAD — runtime error with ESM
|
||||
import { foo } from "./bar";
|
||||
|
||||
// GOOD
|
||||
import { foo } from "./bar.js";
|
||||
```
|
||||
|
||||
**Why:** ESM requires explicit file extensions. Node won't auto-resolve.
|
||||
|
||||
### 2. Unsafe type casting
|
||||
|
||||
```typescript
|
||||
// BAD — crashes on unexpected data
|
||||
const data = JSON.parse(input) as MyType;
|
||||
data.requiredField.toUpperCase(); // TypeError if field is missing
|
||||
|
||||
// GOOD — validate before using
|
||||
const parsed: unknown = JSON.parse(input);
|
||||
if (
|
||||
typeof parsed === "object" &&
|
||||
parsed !== null &&
|
||||
"requiredField" in parsed &&
|
||||
typeof parsed.requiredField === "string"
|
||||
) {
|
||||
const data = parsed as MyType;
|
||||
data.requiredField.toUpperCase(); // safe
|
||||
}
|
||||
```
|
||||
|
||||
### 3. `export default plugin` without `satisfies`
|
||||
|
||||
```typescript
|
||||
// BAD — loses type checking
|
||||
const plugin = { manifest, create };
|
||||
export default plugin; // no compile-time verification
|
||||
|
||||
// GOOD — compile-time type checking
|
||||
export default { manifest, create } satisfies PluginModule<Runtime>;
|
||||
```
|
||||
|
||||
### 4. Using `on("exit")` instead of `once("exit")`
|
||||
|
||||
```typescript
|
||||
// BAD — handler called multiple times if event emits multiple times
|
||||
process.on("exit", cleanup);
|
||||
|
||||
// GOOD — handler called once
|
||||
process.once("exit", cleanup);
|
||||
```
|
||||
|
||||
### 5. Forgetting cleanup on disconnect
|
||||
|
||||
```typescript
|
||||
// BAD — interval keeps running after session dies
|
||||
const interval = setInterval(poll, 1000);
|
||||
|
||||
// GOOD — cleanup on destroy
|
||||
const interval = setInterval(poll, 1000);
|
||||
return {
|
||||
// ... interface methods
|
||||
async destroy() {
|
||||
clearInterval(interval);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
|
@ -125,45 +417,28 @@ exec(`git checkout ${branchName}`); // branchName could contain ; rm -rf /
|
|||
- Types/Interfaces: `PascalCase`
|
||||
- Functions/variables: `camelCase`
|
||||
- Constants: `UPPER_SNAKE_CASE` (only true constants: env vars, regex patterns)
|
||||
- Test files: `*.test.ts` (co-located or in `__tests__/`)
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
pnpm install # install deps
|
||||
pnpm build # build all packages
|
||||
pnpm typecheck # typecheck
|
||||
pnpm lint # ESLint check
|
||||
pnpm lint:fix # ESLint auto-fix
|
||||
pnpm format # Prettier format
|
||||
pnpm format:check # Prettier check (CI)
|
||||
pnpm test # run tests
|
||||
|
||||
# Before committing
|
||||
pnpm lint && pnpm typecheck
|
||||
```
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
- Using `exec` instead of `execFile` — security vulnerability
|
||||
- Using `JSON.stringify` for shell escaping — does not escape `$`, backticks, `$()`
|
||||
- Missing `.js` extension in local imports — runtime error with ESM
|
||||
- Using bare `"fs"` instead of `"node:fs"` — inconsistent
|
||||
- Casting with `as unknown as T` — bypasses type safety, crashes on bad data
|
||||
- `export default plugin` without `satisfies 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 of `once("exit")` for one-time handlers
|
||||
- Test files: `*.test.ts` (co-located in `__tests__/`)
|
||||
|
||||
## Config
|
||||
|
||||
Config loaded from `agent-orchestrator.yaml` (see `agent-orchestrator.yaml.example`). Paths support `~` expansion. Validated with Zod at load time. Per-project overrides for plugins and reactions.
|
||||
|
||||
## Design Decisions
|
||||
## Design Decisions (The "Why")
|
||||
|
||||
1. **Stateless orchestrator** — no database, flat metadata files + event log
|
||||
- **Why:** Debuggability (cat metadata file), no database dependency, survives crashes
|
||||
|
||||
2. **Plugins implement interfaces** — pure implementation of interface from `types.ts`
|
||||
- **Why:** Swappability (tmux → docker), testability (mock plugins), extensibility
|
||||
|
||||
3. **Push notifications** — Notifier is primary human interface, not dashboard
|
||||
4. **Two-tier event handling** — auto-handle routine issues, notify human when judgment needed
|
||||
5. **Backwards-compatible metadata** — flat key=value files
|
||||
- **Why:** Human doesn't poll. Spawn agents, walk away, get notified when needed.
|
||||
|
||||
4. **Two-tier event handling** — auto-handle routine issues (CI, reviews), notify human when judgment needed
|
||||
- **Why:** Reduce noise, scale to many agents, only interrupt human for decisions
|
||||
|
||||
5. **Flat key=value metadata files** — `branch=feat/foo` not JSON
|
||||
- **Why:** Backwards-compatible with bash scripts, easy to parse/debug
|
||||
|
||||
6. **Security first** — `execFile` not `exec`, validate all external input
|
||||
- **Why:** Orchestrator runs user-provided code. Shell injection is real threat.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,174 @@
|
|||
# @agent-orchestrator/core
|
||||
|
||||
Core services, types, and configuration for the Agent Orchestrator system.
|
||||
|
||||
## What's Here
|
||||
|
||||
- **`src/types.ts`** — All TypeScript interfaces (Runtime, Agent, Workspace, Tracker, SCM, Notifier, Terminal, Session, events)
|
||||
- **`src/services/`** — Core services (SessionManager, LifecycleManager, PluginRegistry)
|
||||
- **`src/config.ts`** — Configuration loading + Zod schemas
|
||||
- **`src/utils/`** — Shared utilities (shell escaping, metadata parsing, etc.)
|
||||
|
||||
## Key Files
|
||||
|
||||
### `src/types.ts` — The Source of Truth
|
||||
|
||||
Every interface the system uses is defined here. If you're working on any part of the orchestrator, start by reading this file.
|
||||
|
||||
**Main interfaces:**
|
||||
- `Runtime` — where sessions execute (tmux, docker, k8s)
|
||||
- `Agent` — AI coding tool adapter (claude-code, codex, aider)
|
||||
- `Workspace` — code isolation (worktree, clone)
|
||||
- `Tracker` — issue tracking (GitHub Issues, Linear)
|
||||
- `SCM` — PR/CI/reviews (GitHub, GitLab)
|
||||
- `Notifier` — push notifications (desktop, Slack, webhook)
|
||||
- `Terminal` — human interaction UI (iTerm2, web)
|
||||
- `Session` — running agent instance (state, metadata, handles)
|
||||
- `OrchestratorEvent` — events emitted by lifecycle manager
|
||||
- `PluginModule` — what every plugin exports
|
||||
|
||||
### `src/services/session-manager.ts` — Session CRUD
|
||||
|
||||
Handles session lifecycle:
|
||||
- `spawn(config)` — create new session (workspace + runtime + agent)
|
||||
- `list(projectId?)` — list all sessions
|
||||
- `get(sessionId)` — get session details
|
||||
- `kill(sessionId)` — terminate session
|
||||
- `cleanup(projectId?)` — kill completed/merged sessions
|
||||
- `send(sessionId, message)` — send message to agent
|
||||
|
||||
**Data flow in `spawn()`:**
|
||||
1. Load project config
|
||||
2. Generate prompt via `Tracker.generatePrompt()`
|
||||
3. Create workspace via `Workspace.create()`
|
||||
4. Build launch command via `Agent.getLaunchCommand()`
|
||||
5. Create runtime session via `Runtime.create()`
|
||||
6. Send launch command
|
||||
7. Run `Agent.postLaunchSetup()` (optional)
|
||||
8. Write metadata file
|
||||
9. Return Session object
|
||||
|
||||
### `src/services/lifecycle-manager.ts` — State Machine + Reactions
|
||||
|
||||
Polls sessions, detects state changes, triggers reactions:
|
||||
|
||||
**State machine:**
|
||||
```
|
||||
spawning → working → pr_open → ci_failed/review_pending/approved → mergeable → merged
|
||||
```
|
||||
|
||||
**Reactions:**
|
||||
- `ci-failed` → send fix prompt to agent
|
||||
- `changes-requested` → send review comments to agent
|
||||
- `approved-and-green` → notify human (or auto-merge)
|
||||
- `agent-stuck` → notify human
|
||||
|
||||
**Polling loop:**
|
||||
1. For each session: check if agent is processing (`Agent.isProcessing()`)
|
||||
2. If PR exists: check CI status (`SCM.getCISummary()`), review state (`SCM.getReviewDecision()`)
|
||||
3. Update session status based on state
|
||||
4. Trigger reactions if state changed
|
||||
5. Emit events
|
||||
|
||||
### `src/services/plugin-registry.ts` — Plugin Discovery + Loading
|
||||
|
||||
Loads plugins and provides access to them:
|
||||
- `register(plugin, config?)` — register a plugin instance
|
||||
- `get<T>(slot, name)` — get plugin by slot + name
|
||||
- `list(slot)` — list all plugins for a slot
|
||||
- `loadBuiltins(config?)` — load built-in plugins (runtime-tmux, agent-claude-code, etc.)
|
||||
- `loadFromConfig(config)` — load plugins from config (npm packages, local paths)
|
||||
|
||||
**Built-in plugins** (loaded by default):
|
||||
- runtime-tmux, runtime-process
|
||||
- agent-claude-code, agent-codex, agent-aider, agent-opencode
|
||||
- workspace-worktree, workspace-clone
|
||||
- tracker-github, tracker-linear
|
||||
- scm-github
|
||||
- notifier-desktop, notifier-slack, notifier-composio, notifier-webhook
|
||||
- terminal-iterm2, terminal-web
|
||||
|
||||
### `src/config.ts` — Configuration Loading
|
||||
|
||||
Loads and validates `agent-orchestrator.yaml`:
|
||||
|
||||
**Main config sections:**
|
||||
- `dataDir` — where session metadata lives (~/.agent-orchestrator)
|
||||
- `worktreeDir` — where workspaces are created (~/.worktrees)
|
||||
- `port` — web dashboard port (default 3000)
|
||||
- `defaults` — default plugins (runtime, agent, workspace, notifiers)
|
||||
- `projects` — per-project config (repo, path, branch, symlinks, reactions, agentRules)
|
||||
- `notifiers` — notification channel config (Slack webhooks, etc.)
|
||||
- `notificationRouting` — which notifiers get which priority events
|
||||
- `reactions` — auto-response config (ci-failed, changes-requested, approved-and-green, etc.)
|
||||
|
||||
**Zod schemas** validate all config at load time.
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Adding a Field to Session
|
||||
|
||||
1. Edit `src/types.ts` → `Session` interface
|
||||
2. Edit `src/services/session-manager.ts` → initialize field in `spawn()`
|
||||
3. Rebuild: `pnpm --filter @agent-orchestrator/core build`
|
||||
|
||||
### Adding an Event Type
|
||||
|
||||
1. Edit `src/types.ts` → `EventType` union
|
||||
2. Emit the event: `eventEmitter.emit()` in relevant service
|
||||
3. Add reaction handler (optional): `src/services/lifecycle-manager.ts`
|
||||
|
||||
### Adding a Reaction
|
||||
|
||||
1. Edit `src/services/lifecycle-manager.ts` → add handler function
|
||||
2. Wire it up in the polling loop
|
||||
3. Add config schema in `src/config.ts` if new reaction type
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run all core tests
|
||||
pnpm --filter @agent-orchestrator/core test
|
||||
|
||||
# Run in watch mode
|
||||
pnpm --filter @agent-orchestrator/core test -- --watch
|
||||
|
||||
# Run specific test
|
||||
pnpm --filter @agent-orchestrator/core test -- session-manager.test.ts
|
||||
```
|
||||
|
||||
Tests are in `src/__tests__/`:
|
||||
- `session-manager.test.ts` — session CRUD, spawn, cleanup
|
||||
- `lifecycle-manager.test.ts` — state machine, reactions
|
||||
- `plugin-registry.test.ts` — plugin loading, resolution
|
||||
- `tmux.test.ts` — tmux utility functions (not a plugin test)
|
||||
- `prompt-builder.test.ts` — prompt generation utilities
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
# Build core
|
||||
pnpm --filter @agent-orchestrator/core build
|
||||
|
||||
# Typecheck
|
||||
pnpm --filter @agent-orchestrator/core typecheck
|
||||
```
|
||||
|
||||
This package is a dependency of all other packages. Build it first if working on the codebase.
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
**Why flat metadata files?**
|
||||
- Debuggability: `cat ~/.agent-orchestrator/my-app-3` shows full state
|
||||
- No database dependency (survives crashes, easy to inspect)
|
||||
- Backwards-compatible with bash script orchestrator
|
||||
|
||||
**Why polling instead of webhooks?**
|
||||
- Simpler (no webhook setup, no ngrok for local dev)
|
||||
- Works offline (CI/review state is fetched, not pushed)
|
||||
- Survives orchestrator restarts (no missed events)
|
||||
|
||||
**Why plugin slots?**
|
||||
- Swappability: use tmux locally, docker in CI, k8s in prod
|
||||
- Testability: mock plugins for tests
|
||||
- Extensibility: users can add custom plugins (e.g., company-specific notifier)
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
# @agent-orchestrator/plugin-runtime-tmux
|
||||
|
||||
Runtime plugin for executing agent sessions in tmux.
|
||||
|
||||
## What This Does
|
||||
|
||||
Creates isolated tmux sessions for each agent. Each session runs in a separate tmux session with:
|
||||
- Working directory set to workspace path
|
||||
- Environment variables from config
|
||||
- Agent launch command executed automatically
|
||||
|
||||
## How It Works
|
||||
|
||||
### Creating a Session
|
||||
|
||||
```typescript
|
||||
const handle = await runtime.create({
|
||||
sessionId: "my-app-3",
|
||||
workspacePath: "/Users/dev/.worktrees/my-app/my-app-3",
|
||||
launchCommand: "claude -p 'Fix bug in auth module'",
|
||||
environment: {
|
||||
AO_SESSION_ID: "my-app-3",
|
||||
AO_PROJECT_ID: "my-app",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
1. Validates `sessionId` (only alphanumeric, dash, underscore allowed)
|
||||
2. Creates detached tmux session: `tmux new-session -d -s my-app-3 -c /path/to/workspace`
|
||||
3. Sets environment variables: `tmux ... -e KEY=VALUE`
|
||||
4. Sends launch command: `tmux send-keys -t my-app-3 "claude -p '...'" Enter`
|
||||
5. Returns RuntimeHandle with tmux session name
|
||||
|
||||
### Sending Messages
|
||||
|
||||
```typescript
|
||||
await runtime.sendMessage(handle, "Fix the test failure in auth.test.ts");
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
1. Clears partial input: `tmux send-keys -t my-app-3 C-u`
|
||||
2. For short messages (<200 chars, no newlines): sends directly with `-l` flag (literal mode)
|
||||
3. For long/multiline messages: writes to temp file → `tmux load-buffer` → `tmux paste-buffer`
|
||||
4. Waits 300ms (let tmux process the text)
|
||||
5. Sends Enter: `tmux send-keys -t my-app-3 Enter`
|
||||
|
||||
**Why the complexity?**
|
||||
- `send-keys` without `-l` interprets special strings ("Enter", "Space") as key names
|
||||
- Long strings can overflow tmux's command buffer
|
||||
- Multiline strings need special handling
|
||||
|
||||
### Getting Output
|
||||
|
||||
```typescript
|
||||
const output = await runtime.getOutput(handle, 50); // last 50 lines
|
||||
```
|
||||
|
||||
Uses `tmux capture-pane -t my-app-3 -p -S -50` to capture terminal buffer.
|
||||
|
||||
### Checking if Alive
|
||||
|
||||
```typescript
|
||||
const alive = await runtime.isAlive(handle);
|
||||
```
|
||||
|
||||
Uses `tmux has-session -t my-app-3` (exit code 0 = exists, 1 = doesn't exist).
|
||||
|
||||
### Destroying
|
||||
|
||||
```typescript
|
||||
await runtime.destroy(handle);
|
||||
```
|
||||
|
||||
Kills tmux session: `tmux kill-session -t my-app-3` (ignores errors if already dead).
|
||||
|
||||
## Attaching to Sessions
|
||||
|
||||
For Terminal plugins (iTerm2, web):
|
||||
|
||||
```typescript
|
||||
const attachInfo = await runtime.getAttachInfo(handle);
|
||||
// Returns: { type: "tmux", target: "my-app-3", command: "tmux attach -t my-app-3" }
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
**Session ID validation:**
|
||||
```typescript
|
||||
const SAFE_SESSION_ID = /^[a-zA-Z0-9_-]+$/;
|
||||
```
|
||||
|
||||
Only allows safe characters. Prevents shell injection via session name (used in tmux commands).
|
||||
|
||||
## Error Handling
|
||||
|
||||
- **Session creation fails** → cleans up (kills session) before throwing
|
||||
- **Message send fails** → throws (caller should handle)
|
||||
- **Session already dead** → `destroy()` silently succeeds (idempotent)
|
||||
|
||||
## Metrics
|
||||
|
||||
```typescript
|
||||
const metrics = await runtime.getMetrics(handle);
|
||||
// Returns: { uptimeMs: 123456 }
|
||||
```
|
||||
|
||||
Tracks uptime (stored in RuntimeHandle.data.createdAt).
|
||||
|
||||
## Testing
|
||||
|
||||
This plugin is tested indirectly via `packages/core/src/__tests__/tmux.test.ts` (utility functions) and integration tests.
|
||||
|
||||
To test manually:
|
||||
```bash
|
||||
# Start a test session
|
||||
tmux new-session -d -s test-session -c /tmp
|
||||
tmux send-keys -t test-session "echo hello" Enter
|
||||
|
||||
# Capture output
|
||||
tmux capture-pane -t test-session -p
|
||||
|
||||
# Kill session
|
||||
tmux kill-session -t test-session
|
||||
```
|
||||
|
||||
## Common Issues
|
||||
|
||||
### tmux not installed
|
||||
If tmux is not in PATH, all operations fail. Install via:
|
||||
- macOS: `brew install tmux`
|
||||
- Linux: `apt-get install tmux` or `yum install tmux`
|
||||
|
||||
### Session name conflicts
|
||||
If a session with the same ID already exists, `create()` fails. The orchestrator should ensure unique session IDs.
|
||||
|
||||
### Detached sessions persist after orchestrator crashes
|
||||
tmux sessions keep running even if the orchestrator dies. Use `tmux list-sessions` to find orphans, `tmux kill-session -t <name>` to clean up.
|
||||
|
||||
## Limitations
|
||||
|
||||
- **macOS/Linux only** — tmux is not available on Windows (use WSL)
|
||||
- **No Windows native support** — use runtime-process instead on Windows
|
||||
- **Terminal buffer size** — `getOutput()` limited by tmux buffer size (default 2000 lines)
|
||||
- **No resource limits** — agents can consume unlimited CPU/memory (use docker/k8s runtimes for isolation)
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
**Why tmux over raw processes?**
|
||||
- Sessions persist across orchestrator restarts
|
||||
- Easy to attach for debugging: `tmux attach -t session-name`
|
||||
- Terminal emulation (colors, ANSI codes work)
|
||||
- Works well with interactive AI tools (Claude Code, Aider)
|
||||
|
||||
**Why detached mode?**
|
||||
- Orchestrator doesn't block waiting for agent
|
||||
- Multiple agents can run in parallel
|
||||
- Humans can attach later without interrupting agent
|
||||
Loading…
Reference in New Issue