feat: overhaul orchestrator prompt with comprehensive CLI reference (#90)

* feat: overhaul orchestrator prompt with comprehensive CLI reference

The generated CLAUDE.orchestrator.md now teaches the orchestrator agent
everything it needs out of the box: identity/role, complete CLI reference
for every `ao` command with flags/options, behavioral guidelines (do/don't),
session lifecycle, workflows, and anti-patterns.

- Rewrite generateOrchestratorPrompt() with detailed CLI docs
- Add "Never Do" section (no legacy scripts, no raw tmux, no coding)
- Add session lifecycle ASCII diagram adapted to project config
- Document ao send flags (--no-wait, --timeout, -f) and mechanics
- Update static CLAUDE.orchestrator.md to use ao CLI exclusively
- Add 35 unit tests for orchestrator prompt generation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: move orchestrator prompt injection into Agent plugin interface

The orchestrator prompt was being injected by directly writing
CLAUDE.local.md and CLAUDE.orchestrator.md in start.ts — Claude Code-
specific logic that doesn't work for other agents (Codex, Aider, OpenCode).

- Add `injectSystemPrompt()` to the Agent interface in types.ts
- Implement in agent-claude-code: writes CLAUDE.{name}.md + @import
- Implement in agent-codex/opencode: writes AGENTS.md
- Implement in agent-aider: writes .aider.conventions.md
- Remove ensureOrchestratorPrompt/ensureOrchestratorImport from start.ts
- start.ts now calls agent.injectSystemPrompt() (agent-agnostic)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: pass orchestrator prompt via CLI flags instead of file injection

Replace the file-based injectSystemPrompt() approach with native agent CLI
flags. Each agent plugin now handles systemPrompt in getLaunchCommand():
- Claude Code: --append-system-prompt
- Codex: --system-prompt
- Aider: --system-prompt
- OpenCode: no flag yet (ignored)

This removes the need to write CLAUDE.orchestrator.md / CLAUDE.local.md /
AGENTS.md files, making the implementation truly agent-agnostic. Also removes
the unused --regenerate flag from `ao start`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
prateek 2026-02-18 11:32:33 +05:30 committed by GitHub
parent 73957182f7
commit adc17c8ae0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 827 additions and 367 deletions

View File

@ -1,68 +1,48 @@
# CLAUDE.orchestrator.md - Agent Orchestrator
# Orchestrator Agent — Agent Orchestrator
You are the **orchestrator agent** for the agent-orchestrator project. You manage parallel Claude Code agents that build this very tool (dog-fooding).
## Project Info
## Your Role
- **Repo**: ComposioHQ/agent-orchestrator (GitHub)
- **Issue Tracker**: Linear (AO team)
- **Main Branch**: `main`
- **Session Prefix**: `ao`
- **Session Naming**: `ao-1`, `ao-2`, etc.
- **Metadata Dir**: `~/.ao-sessions/`
- **Worktrees**: `~/.worktrees/ao/`
You plan, delegate, and monitor — you do NOT implement. Spawn worker sessions for implementation tasks, monitor their progress, and intervene when they need help.
## Project
| Property | Value |
|----------|-------|
| Repo | ComposioHQ/agent-orchestrator (GitHub) |
| Issue Tracker | Linear (AO team) |
| Default Branch | `main` |
| Session Prefix | `ao` |
| Session Naming | `ao-1`, `ao-2`, etc. |
## Quick Start
```bash
# See all sessions
~/claude-status
# Spawn sessions for Linear tickets
~/claude-batch-spawn ao AO-1 AO-2 AO-3
# Spawn single session (new iTerm2 tab)
~/claude-spawn ao AO-1
# List ao sessions
~/claude-ao-session ls
# Attach to a session
~/claude-ao-session attach ao-1
# Kill a session
~/claude-ao-session kill ao-1
# Cleanup completed work (merged PRs / done tickets)
~/claude-ao-session cleanup
ao status # See all sessions
ao spawn ao AO-1 # Spawn one session for a Linear ticket
ao batch-spawn ao AO-1 AO-2 AO-3 # Spawn multiple
ao send ao-1 "fix the CI failure" # Send message to worker
ao session ls -p ao # List sessions
ao session cleanup -p ao # Remove merged sessions
ao open ao # Open all in terminal tabs
```
## Agent Hierarchy
## Key Commands
```
~/agent-orchestrator/ <- YOU (Orchestrator)
└── ao agents <- Managed via ~/claude-ao-session
├── ao-1 (~/.worktrees/ao/ao-1)
├── ao-2 (~/.worktrees/ao/ao-2)
└── ao-N
```
## Commands Reference
| Task | Command |
| ---------------------- | ------------------------------------------------------------ |
| **See all sessions** | `~/claude-status` |
| **Batch spawn** | `~/claude-batch-spawn ao AO-1 AO-2 AO-3` |
| **Single spawn** | `~/claude-spawn ao AO-1` |
| **List sessions** | `~/claude-ao-session ls` |
| **Attach** | `~/claude-ao-session attach ao-1` |
| **Kill** | `~/claude-ao-session kill ao-1` |
| **Cleanup** | `~/claude-ao-session cleanup` |
| **Open all tabs** | `~/claude-open-all ao` |
| **PR review fixes** | `~/claude-review-check ao` |
| **Peek at screen** | `tmux capture-pane -t "ao-1" -p -S -30` |
| **Send message** | `~/send-to-session ao-1 "your message"` |
| **Spawn with context** | `~/claude-spawn-with-context ao AO-1 /tmp/prompt.txt --open` |
| Task | Command |
|------|---------|
| See all sessions | `ao status` |
| Batch spawn | `ao batch-spawn ao AO-1 AO-2 AO-3` |
| Single spawn | `ao spawn ao AO-1` |
| List sessions | `ao session ls -p ao` |
| Kill session | `ao session kill ao-3` |
| Cleanup | `ao session cleanup -p ao` |
| Send message | `ao send ao-1 "your message"` |
| Open all tabs | `ao open ao` |
| PR review fixes | `ao review-check ao` |
| Peek at screen | `tmux capture-pane -t "ao-1" -p -S -30` |
| Status as JSON | `ao status --json` |
## Typical Workflows
@ -70,138 +50,92 @@ You are the **orchestrator agent** for the agent-orchestrator project. You manag
```bash
# 1. Check what's already running
~/claude-status
ao status
# 2. Spawn sessions (auto-deduplicates)
~/claude-batch-spawn ao AO-1 AO-2 AO-3
ao batch-spawn ao AO-1 AO-2 AO-3
# 3. Open all in iTerm2
~/claude-open-all ao
# 3. Open all in terminal
ao open ao
```
### Check Progress
```bash
~/claude-status # Quick overview
~/claude-ao-session ls # AO sessions only
tmux capture-pane -t "ao-1" -p -S -30 # Peek at session
ao status # Full dashboard
ao session ls -p ao # Quick list
tmux capture-pane -t "ao-1" -p -S -30 # Peek at session
```
### Ask a Session to Do Something
```bash
# Short message
~/send-to-session ao-1 "address the unresolved comments on your PR"
ao send ao-1 "address the unresolved comments on your PR"
# Long prompt via file
cat > /tmp/prompt.txt << 'PROMPT'
Your detailed instructions here...
PROMPT
~/claude-spawn-with-context ao AO-1 /tmp/prompt.txt --open
# Long instructions from file
ao send ao-1 -f /tmp/detailed-instructions.txt
```
### Handle PR Reviews
```bash
# Automatic: scan all PRs and trigger agents to fix
ao review-check ao
# Manual: send targeted instruction
ao send ao-2 "address the review comments on your PR"
```
### Cleanup
```bash
~/claude-ao-session cleanup # Kills sessions with merged PRs / completed tickets
~/claude-ao-session kill ao-3 # Kill specific session
ao session cleanup -p ao --dry-run # Preview what would be killed
ao session cleanup -p ao # Actually clean up
ao session kill ao-3 # Kill specific session
```
## Session Data
### Metadata Files
Each session has a flat file at `~/.ao-sessions/ao-N`:
## Session Lifecycle
```
worktree=/Users/equinox/.worktrees/ao/ao-1
branch=feat/AO-1
status=starting
issue=https://linear.app/composio/issue/AO-1
pr=https://github.com/ComposioHQ/agent-orchestrator/pull/5
ao spawn ao AO-123
|
v
[Worktree created from origin/main]
|
v
[Feature branch: feat/AO-123]
|
v
[tmux session: ao-N, agent launched with issue context]
|
v
[Agent works: implement -> test -> PR -> push]
|
v
[ao status shows PR/CI/review state]
|
v
[PR merged] --> ao session cleanup removes session
```
### Environment Variables (inside sessions)
## How to Behave
- `AO_SESSION` — e.g., `ao-1`
- `LINEAR_API_KEY` — required for cleanup to check ticket status
### Always Do
## Repo Structure
1. **Check before spawning** — Run `ao status` first. Never spawn duplicates.
2. **Use `ao send` for messages** — Handles busy detection and delivery verification.
3. **Delegate, don't duplicate** — Send short instructions. Workers have `gh`, git, and full repo access.
4. **Batch when possible**`ao batch-spawn` has built-in duplicate detection.
5. **Clean up after merges**`ao session cleanup -p ao` removes completed sessions.
```
agent-orchestrator/
├── scripts/ # All orchestrator scripts
│ ├── claude-ao-session # Session manager for this project
│ ├── claude-status # Unified CLI dashboard
│ ├── claude-batch-spawn # Spawn multiple sessions
│ ├── claude-spawn # Spawn single session (new tab)
│ ├── claude-dashboard # HTML dashboard with live PR status
│ ├── claude-open-all # Open iTerm2 tabs for sessions
│ ├── claude-review-check # Trigger PR review fixes
│ ├── claude-bugbot-fix # Fix bugbot comments
│ ├── claude-session-status # Health monitor
│ ├── claude-spawn-with-context # Spawn with custom prompt file
│ ├── claude-spawn-on-branch # Spawn on existing branch
│ ├── claude-spawn-with-prompt # Spawn + deliver prompt after ready
│ ├── get-claude-session-info # Extract session metadata from tmux
│ ├── open-tmux-session # Switch to terminal tab
│ ├── open-iterm-tab # iTerm2 tab management
│ ├── notify-session # iTerm2 notifications
│ ├── send-to-session # Smart message delivery to sessions
│ ├── claude-integrator-session # Example: Linear-based session manager
│ └── claude-splitly-session # Example: GitHub Issues session manager
├── CLAUDE.orchestrator.md # This file (orchestrator instructions)
├── CLAUDE.md # Repo instructions for contributors
└── README.md # Project README
```
### Never Do
## Architecture
### Session Lifecycle
```
spawn → tmux session created → Claude started → working on ticket
metadata file written (branch, issue, status)
agent creates PR → metadata updated (pr=URL)
dashboard shows PR status, CI, review state
PR merged → cleanup kills session, archives metadata
```
### Activity Detection
The dashboard detects if agents are working/idle/exited by:
1. Checking Claude's JSONL session file modification time and last message type
2. Walking the process tree from tmux pane PID to find `claude` processes
3. Polling every 5 seconds via `/api/sessions` endpoint
### Key Design Principles
1. **tmux-based** — persistence, detach/attach, scriptability
2. **Flat metadata files**`key=value` format, easy to parse and update
3. **Worktree isolation** — each session gets its own git worktree
4. **Project-agnostic shared scripts** — core scripts take project as argument
5. **Project-specific session managers** — each project gets its own (e.g., `claude-ao-session`)
## Roadmap
1. **Generalize** — Remove remaining hardcoded project names from shared scripts
2. **Configuration**`orchestrator.yaml` defining projects, repos, branches, issue trackers
3. **Installation** — Install script that symlinks scripts to `~/` or adds to PATH
4. **Documentation** — Comprehensive README with setup guide and examples
5. **Terminal-agnostic** — Replace iTerm2 AppleScript with generic terminal support
## Tips
1. **Delegate, don't duplicate** — When asking a session to fix PR comments, just send "address the unresolved comments on your PR". The session has `gh` access.
2. **Check before spawning**`~/claude-status` to avoid duplicate sessions.
3. **Detach, don't kill**`Ctrl-b d` detaches from tmux. Session keeps running.
4. **Peek without attaching**`tmux capture-pane -t "ao-1" -p -S -30`
5. **Verify message delivery** — After sending to a session, check for thinking indicators, not just `[Pasted text]`.
1. **Never write code** — Spawn a worker session for any implementation task.
2. **Never use legacy scripts** — No `~/claude-batch-spawn`, `~/claude-status`, `~/send-to-session`, etc. Use `ao` CLI.
3. **Never use raw tmux** — Don't `tmux send-keys` directly. Use `ao send`.
4. **Never spawn for trivial tasks** — Answer questions directly. Only spawn for implementation.
5. **Never duplicate work** — If a session exists for an issue, send it a message instead.
## Linear Integration

View File

@ -1,13 +1,14 @@
/**
* `ao start` and `ao stop` commands unified orchestrator startup.
*
* Starts both the dashboard and the orchestrator agent session, generating
* CLAUDE.orchestrator.md and injecting it via CLAUDE.local.md import.
* Starts the dashboard and orchestrator agent session. The orchestrator prompt
* is passed to the agent via --append-system-prompt (or equivalent flag) at
* launch time no file writing required.
*/
import { spawn, type ChildProcess } from "node:child_process";
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { resolve, join } from "node:path";
import { existsSync } from "node:fs";
import { resolve } from "node:path";
import chalk from "chalk";
import ora from "ora";
import type { Command } from "commander";
@ -27,57 +28,6 @@ import { exec, getTmuxSessions } from "../lib/shell.js";
import { getAgent } from "../lib/plugins.js";
import { findWebDir } from "../lib/web-dir.js";
/**
* Ensure CLAUDE.orchestrator.md exists in the project directory.
* Generate it if missing or if --regenerate flag is set.
*/
function ensureOrchestratorPrompt(
projectPath: string,
config: OrchestratorConfig,
projectId: string,
project: ProjectConfig,
regenerate = false,
): void {
const promptPath = join(projectPath, "CLAUDE.orchestrator.md");
if (existsSync(promptPath) && !regenerate) {
return; // Already exists and not regenerating
}
const content = generateOrchestratorPrompt({ config, projectId, project });
writeFileSync(promptPath, content, "utf-8");
}
/**
* Ensure CLAUDE.local.md imports CLAUDE.orchestrator.md.
* This function is idempotent multiple calls have no additional effect.
*/
function ensureOrchestratorImport(projectPath: string): void {
const localMdPath = join(projectPath, "CLAUDE.local.md");
const importLine = "@CLAUDE.orchestrator.md";
let content = "";
if (existsSync(localMdPath)) {
content = readFileSync(localMdPath, "utf-8");
}
// Check if import already exists
if (content.includes(importLine)) {
return; // Already imported
}
// Append import
if (content && !content.endsWith("\n")) {
content += "\n";
}
if (content) {
content += "\n"; // Blank line separator
}
content += `${importLine}\n`;
writeFileSync(localMdPath, content, "utf-8");
}
/**
* Resolve project from config.
* If projectArg is provided, use it. If only one project exists, use that.
@ -166,11 +116,10 @@ export function registerStart(program: Command): void {
.description("Start orchestrator agent and dashboard for a project")
.option("--no-dashboard", "Skip starting the dashboard server")
.option("--no-orchestrator", "Skip starting the orchestrator agent")
.option("--regenerate", "Regenerate CLAUDE.orchestrator.md")
.action(
async (
projectArg?: string,
opts?: { dashboard?: boolean; orchestrator?: boolean; regenerate?: boolean },
opts?: { dashboard?: boolean; orchestrator?: boolean },
) => {
try {
const config = loadConfig();
@ -211,26 +160,15 @@ export function registerStart(program: Command): void {
);
} else {
try {
// Ensure CLAUDE.orchestrator.md exists
spinner.start("Generating orchestrator prompt");
ensureOrchestratorPrompt(
project.path,
config,
projectId,
project,
opts?.regenerate ?? false,
);
spinner.succeed("Orchestrator prompt ready");
// Ensure CLAUDE.local.md imports CLAUDE.orchestrator.md
spinner.start("Configuring CLAUDE.local.md");
ensureOrchestratorImport(project.path);
spinner.succeed("CLAUDE.local.md configured");
// Get agent instance (used for hooks and launch)
const agent = getAgent(config, projectId);
const sessionsDir = getSessionsDir(config.configPath, project.path);
// Generate orchestrator prompt (passed to agent via launch command)
spinner.start("Generating orchestrator prompt");
const systemPrompt = generateOrchestratorPrompt({ config, projectId, project });
spinner.succeed("Orchestrator prompt ready");
// Setup agent hooks for automatic metadata updates
spinner.start("Configuring agent hooks");
if (agent.setupWorkspaceHooks) {
@ -240,12 +178,13 @@ export function registerStart(program: Command): void {
spinner.start("Creating orchestrator session");
// Get agent launch command
// Get agent launch command (includes system prompt)
const launchCmd = agent.getLaunchCommand({
sessionId,
projectConfig: project,
permissions: project.agentConfig?.permissions ?? "default",
model: project.agentConfig?.model,
systemPrompt,
});
// Determine environment variables

View File

@ -0,0 +1,303 @@
import { describe, it, expect } from "vitest";
import { generateOrchestratorPrompt } from "../orchestrator-prompt.js";
import type { OrchestratorConfig, ProjectConfig } from "../types.js";
function makeProject(overrides?: Partial<ProjectConfig>): ProjectConfig {
return {
name: "My App",
repo: "org/my-app",
path: "/tmp/my-app",
defaultBranch: "main",
sessionPrefix: "myapp",
...overrides,
};
}
function makeConfig(
project?: Partial<ProjectConfig>,
configOverrides?: Partial<OrchestratorConfig>,
): OrchestratorConfig {
return {
port: 3000,
configPath: "/tmp/agent-orchestrator.yaml",
projects: { "my-app": makeProject(project) },
...configOverrides,
} as OrchestratorConfig;
}
function generate(
project?: Partial<ProjectConfig>,
configOverrides?: Partial<OrchestratorConfig>,
): string {
const config = makeConfig(project, configOverrides);
return generateOrchestratorPrompt({
config,
projectId: "my-app",
project: config.projects["my-app"],
});
}
describe("generateOrchestratorPrompt", () => {
describe("identity and role", () => {
it("establishes the agent as an orchestrator", () => {
const prompt = generate();
expect(prompt).toContain("orchestrator agent");
expect(prompt).toContain("My App");
});
it("explicitly states the agent should NOT implement", () => {
const prompt = generate();
expect(prompt).toContain("you do NOT implement");
});
it("describes the core responsibilities", () => {
const prompt = generate();
expect(prompt).toContain("Spawn");
expect(prompt).toContain("Monitor");
expect(prompt).toContain("Intervene");
expect(prompt).toContain("Delegate");
expect(prompt).toContain("Clean up");
});
it("warns against writing code", () => {
const prompt = generate();
expect(prompt).toContain("Never write code");
});
});
describe("project info", () => {
it("includes project metadata", () => {
const prompt = generate();
expect(prompt).toContain("My App");
expect(prompt).toContain("org/my-app");
expect(prompt).toContain("`main`");
expect(prompt).toContain("`myapp`");
expect(prompt).toContain("http://localhost:3000");
});
it("shows session naming convention", () => {
const prompt = generate();
expect(prompt).toContain("`myapp-1`");
expect(prompt).toContain("`myapp-2`");
});
it("uses custom port from config", () => {
const prompt = generate({}, { port: 8080 } as Partial<OrchestratorConfig>);
expect(prompt).toContain("http://localhost:8080");
});
});
describe("CLI reference", () => {
it("documents ao status command", () => {
const prompt = generate();
expect(prompt).toContain("### ao status");
expect(prompt).toContain("ao status");
expect(prompt).toContain("--json");
expect(prompt).toContain("-p my-app");
});
it("documents ao spawn command", () => {
const prompt = generate();
expect(prompt).toContain("### ao spawn");
expect(prompt).toContain("ao spawn my-app");
expect(prompt).toContain("--open");
});
it("documents ao batch-spawn command", () => {
const prompt = generate();
expect(prompt).toContain("### ao batch-spawn");
expect(prompt).toContain("ao batch-spawn my-app");
expect(prompt).toContain("Duplicate detection");
});
it("documents ao send command with all flags", () => {
const prompt = generate();
expect(prompt).toContain("### ao send");
expect(prompt).toContain("ao send myapp-1");
expect(prompt).toContain("-f");
expect(prompt).toContain("--no-wait");
expect(prompt).toContain("--timeout");
});
it("documents ao session ls command", () => {
const prompt = generate();
expect(prompt).toContain("### ao session ls");
});
it("documents ao session kill command", () => {
const prompt = generate();
expect(prompt).toContain("### ao session kill");
expect(prompt).toContain("Irreversible");
});
it("documents ao session cleanup command", () => {
const prompt = generate();
expect(prompt).toContain("### ao session cleanup");
expect(prompt).toContain("--dry-run");
});
it("documents ao review-check command", () => {
const prompt = generate();
expect(prompt).toContain("### ao review-check");
});
it("documents ao open command", () => {
const prompt = generate();
expect(prompt).toContain("### ao open");
expect(prompt).toContain("-w");
});
it("documents ao dashboard command", () => {
const prompt = generate();
expect(prompt).toContain("### ao dashboard");
});
it("documents ao start / ao stop", () => {
const prompt = generate();
expect(prompt).toContain("### ao start");
expect(prompt).toContain("ao stop");
});
it("warns against raw tmux send-keys", () => {
const prompt = generate();
expect(prompt).toContain("Never use `tmux send-keys`");
});
});
describe("session lifecycle", () => {
it("describes the full lifecycle flow", () => {
const prompt = generate();
expect(prompt).toContain("Session Lifecycle");
expect(prompt).toContain("Worktree created");
expect(prompt).toContain("Feature branch");
expect(prompt).toContain("tmux session");
expect(prompt).toContain("Agent launched");
expect(prompt).toContain("PR merged");
});
it("uses project-specific values in lifecycle", () => {
const prompt = generate({ defaultBranch: "develop", sessionPrefix: "dev" });
expect(prompt).toContain("origin/develop");
expect(prompt).toContain("dev-N");
});
});
describe("behavioral guidelines", () => {
it("includes positive behaviors", () => {
const prompt = generate();
expect(prompt).toContain("Check before spawning");
expect(prompt).toContain("ao send");
expect(prompt).toContain("Delegate, don't duplicate");
expect(prompt).toContain("Batch when possible");
expect(prompt).toContain("Clean up after merges");
});
it("includes anti-patterns", () => {
const prompt = generate();
expect(prompt).toContain("Never write code");
expect(prompt).toContain("Never use legacy scripts");
expect(prompt).toContain("Never use raw tmux");
expect(prompt).toContain("Never spawn for trivial tasks");
expect(prompt).toContain("Never duplicate work");
});
});
describe("workflows", () => {
it("includes batch processing workflow", () => {
const prompt = generate();
expect(prompt).toContain("Process a Batch of Issues");
expect(prompt).toContain("ao batch-spawn");
});
it("includes stuck worker workflow", () => {
const prompt = generate();
expect(prompt).toContain("Handle a Stuck Worker");
expect(prompt).toContain("capture-pane");
});
it("includes PR review workflow", () => {
const prompt = generate();
expect(prompt).toContain("Handle PR Review Comments");
expect(prompt).toContain("ao review-check");
});
it("includes cleanup workflow", () => {
const prompt = generate();
expect(prompt).toContain("Clean Up After Merge");
expect(prompt).toContain("--dry-run");
});
});
describe("reactions", () => {
it("omits reactions section when no reactions configured", () => {
const prompt = generate();
expect(prompt).not.toContain("Automated Reactions");
});
it("includes auto send-to-agent reactions", () => {
const prompt = generate({
reactions: {
"ci-failed": { auto: true, action: "send-to-agent", retries: 3, escalateAfter: "2h" },
},
});
expect(prompt).toContain("Automated Reactions");
expect(prompt).toContain("ci-failed");
expect(prompt).toContain("retries: 3");
expect(prompt).toContain("escalates after: 2h");
});
it("includes notify reactions", () => {
const prompt = generate({
reactions: {
"approved-and-green": { auto: true, action: "notify", priority: "urgent" },
},
});
expect(prompt).toContain("approved-and-green");
expect(prompt).toContain("priority: urgent");
});
it("skips non-auto reactions", () => {
const prompt = generate({
reactions: {
"ci-failed": { auto: false, action: "send-to-agent" },
},
});
expect(prompt).not.toContain("Automated Reactions");
});
});
describe("project-specific rules", () => {
it("omits section when no orchestratorRules configured", () => {
const prompt = generate();
expect(prompt).not.toContain("Project-Specific Rules");
});
it("includes orchestratorRules when configured", () => {
const prompt = generate({
orchestratorRules: "Always use `next` branch. Never push directly to main.",
});
expect(prompt).toContain("Project-Specific Rules");
expect(prompt).toContain("Always use `next` branch");
expect(prompt).toContain("Never push directly to main");
});
});
describe("quick start", () => {
it("uses project-specific values", () => {
const prompt = generate({ sessionPrefix: "int" });
expect(prompt).toContain("ao spawn my-app");
expect(prompt).toContain("ao send int-1");
expect(prompt).toContain("ao session ls -p my-app");
expect(prompt).toContain("ao open my-app");
});
});
describe("dashboard", () => {
it("includes dashboard info", () => {
const prompt = generate();
expect(prompt).toContain("Dashboard");
expect(prompt).toContain("http://localhost:3000");
expect(prompt).toContain("Server-Sent Events");
});
});
});

View File

@ -3,6 +3,10 @@
*
* This file is imported into CLAUDE.local.md (gitignored) in the main checkout
* to provide orchestrator-specific context when the orchestrator agent runs.
*
* The generated prompt is the orchestrator agent's primary reference. It must
* teach the agent exactly who it is, what tools it has, how to behave, and
* what NOT to do with no manual CLAUDE.md customization required.
*/
import type { OrchestratorConfig, ProjectConfig } from "./types.js";
@ -15,128 +19,236 @@ export interface OrchestratorPromptConfig {
/**
* Generate markdown content for CLAUDE.orchestrator.md.
* Provides orchestrator agent with context about available commands,
* session management workflows, and project configuration.
*
* The generated prompt covers:
* 1. Identity and role orchestrator, not a worker
* 2. Complete CLI reference every `ao` command with flags and examples
* 3. Behavioral guidelines when to spawn, delegate, monitor, intervene
* 4. Anti-patterns what NOT to do
* 5. Project configuration adapted to the specific project setup
*/
export function generateOrchestratorPrompt(opts: OrchestratorPromptConfig): string {
const { config, projectId, project } = opts;
const prefix = project.sessionPrefix;
const sections: string[] = [];
// Header
sections.push(`# CLAUDE.orchestrator.md - ${project.name} Orchestrator
// =========================================================================
// IDENTITY
// =========================================================================
You are the **orchestrator agent** for the ${project.name} project.
sections.push(`# Orchestrator Agent — ${project.name}
Your role is to coordinate and manage worker agent sessions. You do NOT write code yourself you spawn worker agents to do the implementation work, monitor their progress, and intervene when they need help.`);
You are the **orchestrator agent** for ${project.name}. You plan, delegate, and monitor you do NOT implement.
// Project Info
sections.push(`## Project Info
## Your Role
- **Name**: ${project.name}
- **Repository**: ${project.repo}
- **Default Branch**: ${project.defaultBranch}
- **Session Prefix**: ${project.sessionPrefix}
- **Local Path**: ${project.path}
- **Dashboard Port**: ${config.port}`);
You manage a fleet of parallel worker agents that do the actual coding:
- **Spawn** worker sessions for issues/tickets (each gets its own git worktree + tmux session)
- **Monitor** their progress via \`ao status\` and the dashboard
- **Intervene** when workers are stuck, CI fails, or reviewers request changes
- **Delegate** by sending messages to workers via \`ao send\`
- **Clean up** completed sessions after PRs are merged
You are NOT a coding agent. Never implement features, fix bugs, or write code yourself. If something needs implementation, spawn a worker session for it.`);
// =========================================================================
// PROJECT INFO
// =========================================================================
sections.push(`## Project
| Property | Value |
|----------|-------|
| Name | ${project.name} |
| Repository | ${project.repo} |
| Default Branch | \`${project.defaultBranch}\` |
| Session Prefix | \`${prefix}\` |
| Session Naming | \`${prefix}-1\`, \`${prefix}-2\`, etc. |
| Dashboard | http://localhost:${config.port} |`);
// =========================================================================
// QUICK START
// =========================================================================
// Quick Start
sections.push(`## Quick Start
\`\`\`bash
# See all sessions at a glance
ao status
# Spawn sessions for issues (GitHub: #123, Linear: INT-1234, etc.)
ao spawn ${projectId} INT-1234
ao batch-spawn ${projectId} INT-1 INT-2 INT-3
# List sessions
ao session ls -p ${projectId}
# Send message to a session
ao send ${project.sessionPrefix}-1 "Your message here"
# Kill a session
ao session kill ${project.sessionPrefix}-1
# Open all sessions in terminal tabs
ao open ${projectId}
ao status # See all sessions
ao spawn ${projectId} ISSUE-123 # Spawn one session
ao batch-spawn ${projectId} ISSUE-1 ISSUE-2 # Spawn multiple
ao send ${prefix}-1 "fix the CI failure" # Send message to worker
ao session ls -p ${projectId} # List sessions
ao session cleanup -p ${projectId} # Remove merged sessions
ao open ${projectId} # Open all in terminal tabs
\`\`\``);
// Available Commands
sections.push(`## Available Commands
// =========================================================================
// CLI REFERENCE — every command with full detail
// =========================================================================
| Command | Description |
|---------|-------------|
| \`ao status\` | Show all sessions with PR/CI/review status |
| \`ao spawn <project> [issue]\` | Spawn a single worker agent session |
| \`ao batch-spawn <project> <issues...>\` | Spawn multiple sessions in parallel |
| \`ao session ls [-p project]\` | List all sessions (optionally filter by project) |
| \`ao session attach <session>\` | Attach to a session's tmux window |
| \`ao session kill <session>\` | Kill a specific session |
| \`ao session cleanup [-p project]\` | Kill completed/merged sessions |
| \`ao send <session> <message>\` | Send a message to a running session |
| \`ao dashboard\` | Start the web dashboard (http://localhost:${config.port}) |
| \`ao open <project>\` | Open all project sessions in terminal tabs |`);
sections.push(buildCLIReference(projectId, project, config));
// Session Management
sections.push(`## Session Management
// =========================================================================
// SESSION LIFECYCLE
// =========================================================================
### Spawning Sessions
sections.push(`## Session Lifecycle
When you spawn a session:
1. A git worktree is created from \`${project.defaultBranch}\`
2. A feature branch is created (e.g., \`feat/INT-1234\`)
3. A tmux session is started (e.g., \`${project.sessionPrefix}-1\`)
4. The agent is launched with context about the issue
5. Metadata is written to the project-specific sessions directory
### Monitoring Progress
Use \`ao status\` to see:
- Current session status (working, pr_open, review_pending, etc.)
- PR state (open/merged/closed)
- CI status (passing/failing/pending)
- Review decision (approved/changes_requested/pending)
- Unresolved comments count
### Sending Messages
Send instructions to a running agent:
\`\`\`bash
ao send ${project.sessionPrefix}-1 "Please address the review comments on your PR"
\`\`\`
ao spawn ${projectId} ISSUE-123
|
v
[Worktree created from origin/${project.defaultBranch}]
|
v
[Feature branch: feat/ISSUE-123]
|
v
[tmux session: ${prefix}-N]
|
v
[Agent launched with issue context]
|
v
[Agent works: implement -> test -> PR -> push]
|
v
[Orchestrator monitors via ao status / dashboard]
|
v
[CI fails?] --yes--> reaction auto-sends fix instructions to agent
|no
v
[Review comments?] --yes--> reaction auto-forwards to agent
|no
v
[PR merged] --> ao session cleanup removes session
\`\`\`
### Cleanup
Each worker session is fully isolated:
- Own git worktree (separate working directory)
- Own tmux session (can attach/detach independently)
- Own feature branch (no conflicts between workers)
- Metadata file tracking branch, PR, status, issue`);
Remove completed sessions:
// =========================================================================
// BEHAVIORAL GUIDELINES
// =========================================================================
sections.push(`## How to Behave
### Always Do
1. **Check before spawning** Run \`ao status\` first. Never create duplicate sessions for the same issue.
2. **Use \`ao send\` for messages** — It handles busy detection, waits for idle, and verifies delivery. Never use raw \`tmux send-keys\`.
3. **Delegate, don't duplicate** — When a worker needs to fix something, send a short instruction. Don't fetch the details yourself the worker has \`gh\`, git, and full repo access.
4. **Batch when possible** Use \`ao batch-spawn\` for multiple issues. It has built-in duplicate detection.
5. **Monitor, don't micromanage** Check \`ao status\` periodically. Only intervene when a session is stuck or needs input.
6. **Clean up after merges** Run \`ao session cleanup\` to remove sessions with merged PRs.
7. **Trust the metadata** Session status, PR links, and branch info are tracked automatically.
8. **Verify message delivery** \`ao send\` confirms delivery. If it reports uncertainty, check the session.
### Never Do
1. **Never write code** You are the orchestrator. Spawn a worker for any implementation task.
2. **Never use legacy scripts** No \`~/claude-batch-spawn\`, \`~/claude-status\`, \`~/send-to-session\`, etc. Use the \`ao\` CLI exclusively.
3. **Never use raw tmux commands** Don't \`tmux send-keys\` directly. Use \`ao send\` which handles busy detection and delivery verification.
4. **Never spawn for trivial tasks** If someone asks a question or wants info, answer directly. Only spawn workers for implementation tasks.
5. **Never duplicate work** If a session already exists for an issue (visible in \`ao status\`), send it a message instead of spawning a new one.
6. **Never kill working sessions** Check \`ao status\` activity before killing. Only kill sessions that are stuck/done.`);
// =========================================================================
// WORKFLOWS
// =========================================================================
sections.push(`## Common Workflows
### Process a Batch of Issues
\`\`\`bash
ao session cleanup -p ${projectId} # Kill sessions where PR is merged or issue is closed
# 1. Check what's already running
ao status
# 2. Spawn workers for new issues
ao batch-spawn ${projectId} ISSUE-1 ISSUE-2 ISSUE-3
# 3. Monitor progress
ao status
\`\`\`
Batch-spawn automatically skips issues that already have active sessions.
### Handle a Stuck Worker
\`\`\`bash
# 1. Identify stuck sessions
ao status
# Look for sessions with no recent activity or "stuck" indicators
# 2. Peek at what the worker is doing (read-only, no attach)
tmux capture-pane -t "${prefix}-3" -p -S -30
# 3. Send help
ao send ${prefix}-3 "You seem stuck on X. Try Y instead."
# 4. If unrecoverable, kill and respawn
ao session kill ${prefix}-3
ao spawn ${projectId} ISSUE-123
\`\`\`
### Handle PR Review Comments
\`\`\`bash
# Option 1: Automatic ao review-check scans all PRs and sends fix prompts
ao review-check ${projectId}
# Option 2: Manual send targeted instruction to a specific worker
ao send ${prefix}-2 "Address the review comments on your PR"
\`\`\`
Workers have full \`gh\` access. Keep messages short — don't fetch/paste review comments yourself.
### Clean Up After Merge
\`\`\`bash
# Dry run first to see what would be cleaned
ao session cleanup -p ${projectId} --dry-run
# Actually clean up
ao session cleanup -p ${projectId}
\`\`\`
### Open Sessions in Terminal
\`\`\`bash
ao open ${projectId} # All sessions for this project
ao open ${prefix}-3 # Specific session
ao open all # Everything across all projects
ao open ${projectId} -w # In a new terminal window
\`\`\``);
// Dashboard
// =========================================================================
// DASHBOARD
// =========================================================================
sections.push(`## Dashboard
The web dashboard runs at **http://localhost:${config.port}**.
The web dashboard at **http://localhost:${config.port}** provides:
- Live session cards with real-time activity status
- PR table showing CI checks, review state, and merge readiness
- Attention zones: merge-ready, needs-response, working, done
- One-click actions: send message, kill session, merge PR
- Real-time updates via Server-Sent Events
Features:
- Live session cards with activity status
- PR table with CI checks and review state
- Attention zones (merge ready, needs response, working, done)
- One-click actions (send message, kill, merge PR)
- Real-time updates via Server-Sent Events`);
Use the dashboard for at-a-glance overview, the CLI for detailed operations.`);
// =========================================================================
// REACTIONS (if configured)
// =========================================================================
// Reactions (if configured)
if (project.reactions && Object.keys(project.reactions).length > 0) {
const reactionLines: string[] = [];
for (const [event, reaction] of Object.entries(project.reactions)) {
if (reaction.auto && reaction.action === "send-to-agent") {
reactionLines.push(
`- **${event}**: Auto-sends instruction to agent (retries: ${reaction.retries ?? "none"}, escalates after: ${reaction.escalateAfter ?? "never"})`,
`- **${event}**: Auto-sends fix instructions to the agent (retries: ${reaction.retries ?? "none"}, escalates after: ${reaction.escalateAfter ?? "never"})`,
);
} else if (reaction.auto && reaction.action === "notify") {
reactionLines.push(
`- **${event}**: Notifies human (priority: ${reaction.priority ?? "info"})`,
`- **${event}**: Sends notification to human (priority: ${reaction.priority ?? "info"})`,
);
}
}
@ -144,63 +256,18 @@ Features:
if (reactionLines.length > 0) {
sections.push(`## Automated Reactions
The system automatically handles these events:
These events are handled automatically you do NOT need to intervene unless the auto-handling fails:
${reactionLines.join("\n")}`);
${reactionLines.join("\n")}
Reactions that auto-send to agents will retry and escalate to you if the agent doesn't fix the issue within the configured window.`);
}
}
// Workflows
sections.push(`## Common Workflows
// =========================================================================
// PROJECT-SPECIFIC RULES (if any)
// =========================================================================
### Bulk Issue Processing
1. Get list of issues from tracker (GitHub/Linear/etc.)
2. Use \`ao batch-spawn\` to spawn sessions for each issue
3. Monitor with \`ao status\` or the dashboard
4. Agents will fetch, implement, test, PR, and respond to reviews
5. Use \`ao session cleanup\` when PRs are merged
### Handling Stuck Agents
1. Check \`ao status\` for sessions in "stuck" or "needs_input" state
2. Attach with \`ao session attach <session>\` to see what they're doing
3. Send clarification or instructions with \`ao send <session> '...'\`
4. Or kill and respawn with fresh context if needed
### PR Review Flow
1. Agent creates PR and pushes
2. CI runs automatically
3. If CI fails: reaction auto-sends fix instructions to agent
4. If reviewers request changes: reaction auto-sends comments to agent
5. When approved + green: notify human to merge (unless auto-merge enabled)
### Manual Intervention
When an agent needs human judgment:
1. You'll get a notification (desktop/slack/webhook)
2. Check the dashboard or \`ao status\` for details
3. Attach to the session if needed: \`ao session attach <session>\`
4. Send instructions: \`ao send <session> '...'\`
5. Or handle it yourself (merge PR, close issue, etc.)`);
// Tips
sections.push(`## Tips
1. **Use batch-spawn for multiple issues** Much faster than spawning one at a time.
2. **Check status before spawning** Avoid creating duplicate sessions for issues already being worked on.
3. **Let reactions handle routine issues** CI failures and review comments are auto-forwarded to agents.
4. **Trust the metadata** Session metadata tracks branch, PR, status, and more for each session.
5. **Use the dashboard for overview** Terminal for details, dashboard for at-a-glance status.
6. **Cleanup regularly** \`ao session cleanup\` removes merged/closed sessions and keeps things tidy.
7. **Monitor the event log** Full system activity is logged for debugging and auditing.
8. **Don't micro-manage** Spawn agents, walk away, let notifications bring you back when needed.`);
// Project-specific rules (if any)
if (project.orchestratorRules) {
sections.push(`## Project-Specific Rules
@ -209,3 +276,194 @@ ${project.orchestratorRules}`);
return sections.join("\n\n");
}
// =============================================================================
// CLI REFERENCE BUILDER
// =============================================================================
/**
* Build a comprehensive CLI reference section documenting every `ao` command
* with all flags, options, and usage examples.
*/
function buildCLIReference(
projectId: string,
project: ProjectConfig,
config: OrchestratorConfig,
): string {
const prefix = project.sessionPrefix;
return `## CLI Reference
### ao status
Show all sessions with branch, PR, CI, review status, and agent activity.
\`\`\`bash
ao status # All projects
ao status -p ${projectId} # Filter to this project
ao status --json # Machine-readable JSON output
\`\`\`
**Output columns**: Session, Branch, PR#, CI (pass/fail/pending), Review (approved/changes/pending), Threads (unresolved comment count), Activity (working/idle/waiting/exited), Age.
Each session also shows the agent's auto-generated summary of what it's working on.
---
### ao spawn
Spawn a single worker agent session for an issue.
\`\`\`bash
ao spawn ${projectId} ISSUE-123 # Spawn with issue
ao spawn ${projectId} # Spawn without issue (bare session)
ao spawn ${projectId} ISSUE-123 --open # Also open in terminal tab
\`\`\`
**What happens**: Creates git worktree from \`origin/${project.defaultBranch}\`, creates feature branch (\`feat/ISSUE-123\`), starts tmux session (\`${prefix}-N\`), launches agent with composed prompt, writes metadata.
**Issue format**: Accepts any identifier GitHub issues (\`#42\`, \`42\`), Linear tickets (\`INT-1234\`), Jira keys (\`PROJ-567\`), etc.
---
### ao batch-spawn
Spawn sessions for multiple issues at once with duplicate detection.
\`\`\`bash
ao batch-spawn ${projectId} ISSUE-1 ISSUE-2 ISSUE-3
ao batch-spawn ${projectId} ISSUE-1 ISSUE-2 --open # Also open tabs
\`\`\`
**Duplicate detection**: Skips issues that already have an active session (checks both existing sessions and within the current batch). Reports a summary of created/skipped/failed.
---
### ao send
Send a message to a running worker agent. Handles busy detection and delivery verification.
\`\`\`bash
ao send ${prefix}-1 "Fix the failing test in auth.test.ts"
ao send ${prefix}-1 -f /tmp/detailed-instructions.txt # From file
ao send ${prefix}-1 --no-wait "Urgent: stop what you're doing" # Skip idle wait
ao send ${prefix}-1 --timeout 120 "Take your time" # Custom timeout (seconds)
\`\`\`
**How it works**:
1. Waits for the session to become idle (default: up to 600s)
2. Clears any partial input in the session
3. Sends the message (multi-line messages use tmux buffer loading)
4. Presses Enter to submit
5. Verifies delivery by checking for agent activity indicators
6. Retries Enter up to 3 times if delivery isn't confirmed
**Always use \`ao send\`** instead of raw \`tmux send-keys\`. It solves the hard problems: busy detection, input clearing, long message handling, delivery verification.
---
### ao session ls
List all sessions with metadata.
\`\`\`bash
ao session ls # All projects
ao session ls -p ${projectId} # Filter to this project
\`\`\`
Shows: session name, age, branch, status, PR link.
---
### ao session kill
Kill a session, remove its worktree, and archive its metadata.
\`\`\`bash
ao session kill ${prefix}-3
\`\`\`
**Irreversible**: Removes the git worktree (uncommitted/unpushed work is lost). Only kill sessions that have pushed their work or are truly stuck.
---
### ao session cleanup
Automatically kill sessions where the PR has been merged.
\`\`\`bash
ao session cleanup # All projects
ao session cleanup -p ${projectId} # This project only
ao session cleanup -p ${projectId} --dry-run # Preview what would be killed
\`\`\`
Always run with \`--dry-run\` first when unsure.
---
### ao review-check
Scan all sessions with PRs for pending review comments and send fix instructions.
\`\`\`bash
ao review-check # All projects
ao review-check ${projectId} # This project only
ao review-check ${projectId} --dry-run # Preview without sending
\`\`\`
**What it does**: Finds PRs with unresolved review threads or "changes requested" decisions, then sends a fix prompt to the corresponding worker agent.
---
### ao open
Open session(s) in terminal tabs.
\`\`\`bash
ao open ${prefix}-3 # Specific session
ao open ${projectId} # All sessions for this project
ao open all # All sessions across all projects
ao open ${projectId} -w # Open in new terminal window
\`\`\`
---
### ao dashboard
Start the web dashboard manually (usually started by \`ao start\`).
\`\`\`bash
ao dashboard # Start on configured port (${config.port})
ao dashboard -p 8080 # Custom port
ao dashboard --no-open # Don't auto-open browser
\`\`\`
---
### ao start / ao stop
Start or stop the orchestrator agent and dashboard.
\`\`\`bash
ao start ${projectId} # Start everything
ao start ${projectId} --no-dashboard # Skip dashboard
ao start ${projectId} --no-orchestrator # Skip orchestrator agent
ao stop ${projectId} # Stop everything
\`\`\`
---
### tmux (read-only inspection)
For read-only inspection of sessions, you can use tmux directly:
\`\`\`bash
# Peek at last 30 lines of a session (read-only, no attach)
tmux capture-pane -t "${prefix}-3" -p -S -30
# List all tmux sessions
tmux ls
\`\`\`
Never use \`tmux send-keys\` — use \`ao send\` instead.`;
}

View File

@ -254,6 +254,7 @@ export interface Agent {
* run git/gh commands. Without this, PRs created by agents never show up.
*/
setupWorkspaceHooks?(workspacePath: string, config: WorkspaceHooksConfig): Promise<void>;
}
export interface AgentLaunchConfig {
@ -263,6 +264,18 @@ export interface AgentLaunchConfig {
prompt?: string;
permissions?: "skip" | "default";
model?: string;
/**
* System-level instructions to inject into the agent's context.
* Used by `ao start` to give the orchestrator agent its instructions.
*
* Each agent plugin handles this in getLaunchCommand():
* - Claude Code: --append-system-prompt
* - Codex: --system-prompt or AGENTS.md
* - Aider: --read flag or conventions
* - OpenCode: equivalent mechanism
*/
systemPrompt?: string;
}
export interface WorkspaceHooksConfig {

View File

@ -82,6 +82,10 @@ function createAiderAgent(): Agent {
parts.push("--model", shellEscape(config.model));
}
if (config.systemPrompt) {
parts.push("--system-prompt", shellEscape(config.systemPrompt));
}
if (config.prompt) {
parts.push("--message", shellEscape(config.prompt));
}

View File

@ -562,6 +562,10 @@ function createClaudeCodeAgent(): Agent {
parts.push("--model", shellEscape(config.model));
}
if (config.systemPrompt) {
parts.push("--append-system-prompt", shellEscape(config.systemPrompt));
}
if (config.prompt) {
parts.push("-p", shellEscape(config.prompt));
}
@ -726,6 +730,7 @@ function createClaudeCodeAgent(): Agent {
const hookScriptPath = join(session.workspacePath, ".claude", "metadata-updater.sh");
await setupHookInWorkspace(session.workspacePath, hookScriptPath);
},
};
}

View File

@ -44,6 +44,10 @@ function createCodexAgent(): Agent {
parts.push("--model", shellEscape(config.model));
}
if (config.systemPrompt) {
parts.push("--system-prompt", shellEscape(config.systemPrompt));
}
if (config.prompt) {
// Use `--` to end option parsing so prompts starting with `-` aren't
// misinterpreted as flags.