feat: implement ao start command for unified orchestrator startup
Adds `ao start` and `ao stop` commands to unify orchestrator and dashboard startup. Key features: - Generates CLAUDE.orchestrator.md with project-specific context - Auto-imports orchestrator prompt via CLAUDE.local.md - Creates orchestrator tmux session with agent - Starts Next.js dashboard server - Supports --no-dashboard, --no-orchestrator, --regenerate flags - Idempotent operation (safe to run multiple times) - Computes orchestrator ID from config (not session search) - Dashboard button always visible for orchestrator terminal Components: - packages/core/src/orchestrator-prompt.ts: Prompt generator - packages/cli/src/commands/start.ts: Start/stop commands - Modified exports and dashboard UI for orchestrator support Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
de662dc042
commit
3ca65a271c
|
|
@ -0,0 +1,375 @@
|
|||
/**
|
||||
* `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.
|
||||
*/
|
||||
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
import { resolve, join, dirname } from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import chalk from "chalk";
|
||||
import ora from "ora";
|
||||
import type { Command } from "commander";
|
||||
import {
|
||||
loadConfig,
|
||||
generateOrchestratorPrompt,
|
||||
hasTmuxSession,
|
||||
newTmuxSession,
|
||||
tmuxSendKeys,
|
||||
writeMetadata,
|
||||
deleteMetadata,
|
||||
type OrchestratorConfig,
|
||||
type ProjectConfig,
|
||||
} from "@composio/ao-core";
|
||||
import { exec, getTmuxSessions } from "../lib/shell.js";
|
||||
import { getAgent } from "../lib/plugins.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
/**
|
||||
* Locate the @composio/ao-web package directory.
|
||||
* Reuses the logic from dashboard.ts.
|
||||
*/
|
||||
function findWebDir(): string {
|
||||
try {
|
||||
const pkgJson = require.resolve("@composio/ao-web/package.json");
|
||||
return resolve(pkgJson, "..");
|
||||
} catch {
|
||||
const candidates = [
|
||||
resolve(__dirname, "../../../web"),
|
||||
resolve(__dirname, "../../../../packages/web"),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (existsSync(resolve(candidate, "package.json"))) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return candidates[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Otherwise, error with helpful message.
|
||||
*/
|
||||
function resolveProject(
|
||||
config: OrchestratorConfig,
|
||||
projectArg?: string,
|
||||
): { projectId: string; project: ProjectConfig } {
|
||||
const projectIds = Object.keys(config.projects);
|
||||
|
||||
if (projectIds.length === 0) {
|
||||
throw new Error("No projects configured. Add a project to agent-orchestrator.yaml.");
|
||||
}
|
||||
|
||||
// Explicit project argument
|
||||
if (projectArg) {
|
||||
const project = config.projects[projectArg];
|
||||
if (!project) {
|
||||
throw new Error(
|
||||
`Project "${projectArg}" not found. Available projects:\n ${projectIds.join(", ")}`,
|
||||
);
|
||||
}
|
||||
return { projectId: projectArg, project };
|
||||
}
|
||||
|
||||
// Only one project — use it
|
||||
if (projectIds.length === 1) {
|
||||
const projectId = projectIds[0];
|
||||
return { projectId, project: config.projects[projectId] };
|
||||
}
|
||||
|
||||
// Multiple projects, no argument — error
|
||||
throw new Error(
|
||||
`Multiple projects configured. Specify which one to start:\n ${projectIds.map((id) => `ao start ${id}`).join("\n ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start dashboard server in the background.
|
||||
* Returns the child process handle for cleanup.
|
||||
*/
|
||||
function startDashboard(port: number, webDir: string): ChildProcess {
|
||||
const child = spawn("npx", ["next", "dev", "-p", String(port)], {
|
||||
cwd: webDir,
|
||||
stdio: "inherit",
|
||||
detached: false,
|
||||
});
|
||||
|
||||
child.on("error", (err) => {
|
||||
console.error(chalk.red("Dashboard failed to start:"), err);
|
||||
});
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop dashboard server.
|
||||
* Uses pkill to find and kill the Next.js dev server.
|
||||
* Best effort — if it fails, just warn the user.
|
||||
*/
|
||||
async function stopDashboard(port: number): Promise<void> {
|
||||
try {
|
||||
await exec("pkill", ["-f", `next dev -p ${port}`]);
|
||||
console.log(chalk.green("Dashboard stopped"));
|
||||
} catch {
|
||||
console.log(chalk.yellow("Could not stop dashboard (may not be running)"));
|
||||
}
|
||||
}
|
||||
|
||||
export function registerStart(program: Command): void {
|
||||
program
|
||||
.command("start [project]")
|
||||
.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 },
|
||||
) => {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const { projectId, project } = resolveProject(config, projectArg);
|
||||
const sessionId = `${project.sessionPrefix}-orchestrator`;
|
||||
const port = config.port;
|
||||
|
||||
console.log(chalk.bold(`\nStarting orchestrator for ${chalk.cyan(project.name)}\n`));
|
||||
|
||||
// Check if orchestrator session already exists
|
||||
const exists = await hasTmuxSession(sessionId);
|
||||
if (exists && opts?.orchestrator !== false) {
|
||||
console.log(chalk.yellow(`Orchestrator session "${sessionId}" is already running.`));
|
||||
console.log(chalk.dim(` Attach: tmux attach -t ${sessionId}`));
|
||||
console.log(chalk.dim(` Dashboard: http://localhost:${port}\n`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure CLAUDE.orchestrator.md exists
|
||||
const spinner = ora("Generating orchestrator prompt").start();
|
||||
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");
|
||||
try {
|
||||
ensureOrchestratorImport(project.path);
|
||||
spinner.succeed("CLAUDE.local.md configured");
|
||||
} catch (err) {
|
||||
spinner.fail("Could not write CLAUDE.local.md");
|
||||
throw new Error(
|
||||
`Failed to update CLAUDE.local.md: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Start dashboard (unless --no-dashboard)
|
||||
let dashboardProcess: ChildProcess | null = null;
|
||||
if (opts?.dashboard !== false) {
|
||||
spinner.start("Starting dashboard");
|
||||
const webDir = findWebDir();
|
||||
if (!existsSync(resolve(webDir, "package.json"))) {
|
||||
spinner.fail("Dashboard not found");
|
||||
throw new Error(
|
||||
"Could not find @composio/ao-web package. Run: pnpm install",
|
||||
);
|
||||
}
|
||||
|
||||
dashboardProcess = startDashboard(port, webDir);
|
||||
spinner.succeed(`Dashboard starting on http://localhost:${port}`);
|
||||
console.log(chalk.dim(" (Dashboard will be ready in a few seconds)\n"));
|
||||
}
|
||||
|
||||
// Create orchestrator tmux session (unless --no-orchestrator)
|
||||
if (opts?.orchestrator !== false && !exists) {
|
||||
spinner.start("Creating orchestrator session");
|
||||
|
||||
// Get agent launch command
|
||||
const agent = getAgent(config, projectId);
|
||||
const launchCmd = agent.getLaunchCommand({
|
||||
sessionId,
|
||||
projectConfig: project,
|
||||
permissions: project.agentConfig?.permissions ?? "default",
|
||||
model: project.agentConfig?.model,
|
||||
});
|
||||
|
||||
// Determine environment variables
|
||||
const envVarName = `${project.sessionPrefix.toUpperCase().replace(/[^A-Z0-9_]/g, "_")}_SESSION`;
|
||||
const environment: Record<string, string> = {
|
||||
[envVarName]: sessionId,
|
||||
DIRENV_LOG_FORMAT: "",
|
||||
};
|
||||
|
||||
// Merge agent-specific environment
|
||||
const agentEnv = agent.getEnvironment({
|
||||
sessionId,
|
||||
projectConfig: project,
|
||||
permissions: project.agentConfig?.permissions ?? "default",
|
||||
model: project.agentConfig?.model,
|
||||
});
|
||||
Object.assign(environment, agentEnv);
|
||||
|
||||
// Create tmux session
|
||||
await newTmuxSession({
|
||||
name: sessionId,
|
||||
cwd: project.path,
|
||||
environment,
|
||||
});
|
||||
|
||||
// Launch agent
|
||||
await tmuxSendKeys(sessionId, launchCmd, true);
|
||||
|
||||
spinner.succeed("Orchestrator session created");
|
||||
|
||||
// Write metadata
|
||||
const runtimeHandle = JSON.stringify({
|
||||
id: sessionId,
|
||||
runtimeName: "tmux",
|
||||
data: {},
|
||||
});
|
||||
|
||||
writeMetadata(config.dataDir, sessionId, {
|
||||
worktree: project.path,
|
||||
branch: project.defaultBranch,
|
||||
status: "working",
|
||||
project: projectId,
|
||||
createdAt: new Date().toISOString(),
|
||||
runtimeHandle,
|
||||
});
|
||||
}
|
||||
|
||||
// Print summary
|
||||
console.log(chalk.bold.green("\n✓ Orchestrator started\n"));
|
||||
console.log(chalk.cyan("Dashboard:"), `http://localhost:${port}`);
|
||||
console.log(chalk.cyan("Session:"), `tmux attach -t ${sessionId}`);
|
||||
console.log(chalk.dim(`Config: ${config.dataDir}\n`));
|
||||
|
||||
// Keep dashboard process alive if it was started
|
||||
if (dashboardProcess) {
|
||||
dashboardProcess.on("exit", (code) => {
|
||||
if (code !== 0 && code !== null) {
|
||||
console.error(chalk.red(`Dashboard exited with code ${code}`));
|
||||
}
|
||||
process.exit(code ?? 0);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
if (err.message.includes("No agent-orchestrator.yaml found")) {
|
||||
console.error(chalk.red("\nNo config found. Run:"));
|
||||
console.error(chalk.cyan(" ao init\n"));
|
||||
} else {
|
||||
console.error(chalk.red("\nError:"), err.message);
|
||||
}
|
||||
} else {
|
||||
console.error(chalk.red("\nError:"), String(err));
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function registerStop(program: Command): void {
|
||||
program
|
||||
.command("stop [project]")
|
||||
.description("Stop orchestrator agent and dashboard for a project")
|
||||
.action(async (projectArg?: string) => {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const { projectId, project } = resolveProject(config, projectArg);
|
||||
const sessionId = `${project.sessionPrefix}-orchestrator`;
|
||||
const port = config.port;
|
||||
|
||||
console.log(chalk.bold(`\nStopping orchestrator for ${chalk.cyan(project.name)}\n`));
|
||||
|
||||
// Kill orchestrator session
|
||||
const sessions = await getTmuxSessions();
|
||||
if (sessions.includes(sessionId)) {
|
||||
const spinner = ora("Stopping orchestrator session").start();
|
||||
await exec("tmux", ["kill-session", "-t", sessionId]);
|
||||
spinner.succeed("Orchestrator session stopped");
|
||||
|
||||
// Archive metadata
|
||||
deleteMetadata(config.dataDir, sessionId, true);
|
||||
} else {
|
||||
console.log(chalk.yellow(`Orchestrator session "${sessionId}" is not running`));
|
||||
}
|
||||
|
||||
// Stop dashboard
|
||||
await stopDashboard(port);
|
||||
|
||||
console.log(chalk.bold.green("\n✓ Orchestrator stopped\n"));
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
console.error(chalk.red("\nError:"), err.message);
|
||||
} else {
|
||||
console.error(chalk.red("\nError:"), String(err));
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import { registerSend } from "./commands/send.js";
|
|||
import { registerReviewCheck } from "./commands/review-check.js";
|
||||
import { registerDashboard } from "./commands/dashboard.js";
|
||||
import { registerOpen } from "./commands/open.js";
|
||||
import { registerStart, registerStop } from "./commands/start.js";
|
||||
|
||||
const program = new Command();
|
||||
|
||||
|
|
@ -18,6 +19,8 @@ program
|
|||
.version("0.1.0");
|
||||
|
||||
registerInit(program);
|
||||
registerStart(program);
|
||||
registerStop(program);
|
||||
registerStatus(program);
|
||||
registerSpawn(program);
|
||||
registerBatchSpawn(program);
|
||||
|
|
|
|||
|
|
@ -48,5 +48,9 @@ export type { LifecycleManagerDeps } from "./lifecycle-manager.js";
|
|||
export { buildPrompt, BASE_AGENT_PROMPT } from "./prompt-builder.js";
|
||||
export type { PromptBuildConfig } from "./prompt-builder.js";
|
||||
|
||||
// Orchestrator prompt — generates CLAUDE.orchestrator.md
|
||||
export { generateOrchestratorPrompt } from "./orchestrator-prompt.js";
|
||||
export type { OrchestratorPromptConfig } from "./orchestrator-prompt.js";
|
||||
|
||||
// Shared utilities
|
||||
export { shellEscape, escapeAppleScript, validateUrl } from "./utils.js";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,209 @@
|
|||
/**
|
||||
* Orchestrator Prompt Generator — generates CLAUDE.orchestrator.md content.
|
||||
*
|
||||
* This file is imported into CLAUDE.local.md (gitignored) in the main checkout
|
||||
* to provide orchestrator-specific context when the orchestrator agent runs.
|
||||
*/
|
||||
|
||||
import type { OrchestratorConfig, ProjectConfig } from "./types.js";
|
||||
|
||||
export interface OrchestratorPromptConfig {
|
||||
config: OrchestratorConfig;
|
||||
projectId: string;
|
||||
project: ProjectConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate markdown content for CLAUDE.orchestrator.md.
|
||||
* Provides orchestrator agent with context about available commands,
|
||||
* session management workflows, and project configuration.
|
||||
*/
|
||||
export function generateOrchestratorPrompt(opts: OrchestratorPromptConfig): string {
|
||||
const { config, projectId, project } = opts;
|
||||
const sections: string[] = [];
|
||||
|
||||
// Header
|
||||
sections.push(`# CLAUDE.orchestrator.md - ${project.name} Orchestrator
|
||||
|
||||
You are the **orchestrator agent** for the ${project.name} project.
|
||||
|
||||
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.`);
|
||||
|
||||
// Project Info
|
||||
sections.push(`## Project Info
|
||||
|
||||
- **Name**: ${project.name}
|
||||
- **Repository**: ${project.repo}
|
||||
- **Default Branch**: ${project.defaultBranch}
|
||||
- **Session Prefix**: ${project.sessionPrefix}
|
||||
- **Local Path**: ${project.path}
|
||||
- **Data Directory**: ${config.dataDir}
|
||||
- **Worktree Directory**: ${config.worktreeDir}
|
||||
- **Dashboard Port**: ${config.port}`);
|
||||
|
||||
// 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}
|
||||
\\\`\\\`\\\``);
|
||||
|
||||
// Available Commands
|
||||
sections.push(`## Available Commands
|
||||
|
||||
| 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 |`);
|
||||
|
||||
// Session Management
|
||||
sections.push(`## Session Management
|
||||
|
||||
### Spawning Sessions
|
||||
|
||||
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 \`${config.dataDir}\`
|
||||
|
||||
### 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"
|
||||
\\\`\\\`\\\`
|
||||
|
||||
### Cleanup
|
||||
|
||||
Remove completed sessions:
|
||||
\\\`\\\`\\\`bash
|
||||
ao session cleanup -p ${projectId} # Kill sessions where PR is merged or issue is closed
|
||||
\\\`\\\`\\\``);
|
||||
|
||||
// Dashboard
|
||||
sections.push(`## Dashboard
|
||||
|
||||
The web dashboard runs at **http://localhost:${config.port}**.
|
||||
|
||||
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`);
|
||||
|
||||
// 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"})`);
|
||||
} else if (reaction.auto && reaction.action === "notify") {
|
||||
reactionLines.push(`- **${event}**: Notifies human (priority: ${reaction.priority ?? "info"})`);
|
||||
}
|
||||
}
|
||||
|
||||
if (reactionLines.length > 0) {
|
||||
sections.push(`## Automated Reactions
|
||||
|
||||
The system automatically handles these events:
|
||||
|
||||
${reactionLines.join("\n")}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Workflows
|
||||
sections.push(`## Common Workflows
|
||||
|
||||
### 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 files in \`${config.dataDir}\` track branch, PR, status, etc.
|
||||
|
||||
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** — Check \`${config.dataDir}/events.jsonl\` for full system activity history.
|
||||
|
||||
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
|
||||
|
||||
${project.orchestratorRules}`);
|
||||
}
|
||||
|
||||
return sections.join("\n\n");
|
||||
}
|
||||
|
|
@ -12,9 +12,12 @@ export default async function Home() {
|
|||
try {
|
||||
const { config, registry, sessionManager } = await getServices();
|
||||
const allSessions = await sessionManager.list();
|
||||
// Find and filter out orchestrator sessions — they get their own button, not a card
|
||||
const orchSession = allSessions.find((s) => s.id.endsWith("-orchestrator"));
|
||||
if (orchSession) orchestratorId = orchSession.id;
|
||||
|
||||
// Compute expected orchestrator ID from config
|
||||
const firstProject = Object.values(config.projects)[0];
|
||||
orchestratorId = firstProject ? `${firstProject.sessionPrefix}-orchestrator` : null;
|
||||
|
||||
// Filter out orchestrator from worker sessions
|
||||
const coreSessions = allSessions.filter((s) => !s.id.endsWith("-orchestrator"));
|
||||
sessions = coreSessions.map(sessionToDashboard);
|
||||
|
||||
|
|
|
|||
|
|
@ -87,14 +87,12 @@ export function Dashboard({ sessions, stats, orchestratorId }: DashboardProps) {
|
|||
<span className="text-[#7c8aff]">Agent</span> Orchestrator
|
||||
</h1>
|
||||
<div className="flex items-baseline gap-4">
|
||||
{orchestratorId && (
|
||||
<a
|
||||
href={`/sessions/${encodeURIComponent(orchestratorId)}`}
|
||||
className="rounded-md border border-[var(--color-border-default)] px-3 py-1 text-[11px] text-[var(--color-text-secondary)] transition-colors hover:border-[var(--color-accent-blue)] hover:text-[var(--color-accent-blue)]"
|
||||
>
|
||||
orchestrator terminal
|
||||
</a>
|
||||
)}
|
||||
<a
|
||||
href={`/sessions/${encodeURIComponent(orchestratorId ?? "ao-orchestrator")}`}
|
||||
className="rounded-md border border-[var(--color-border-default)] px-3 py-1 text-[11px] text-[var(--color-text-secondary)] transition-colors hover:border-[var(--color-accent-blue)] hover:text-[var(--color-accent-blue)]"
|
||||
>
|
||||
orchestrator terminal
|
||||
</a>
|
||||
<ClientTimestamp />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue