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:
prateek 2026-02-15 03:34:35 +05:30 committed by GitHub
parent baaabe537a
commit 8db5f2b161
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 706 additions and 99 deletions

473
CLAUDE.md
View File

@ -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.

174
packages/core/README.md Normal file
View File

@ -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)

View File

@ -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