Merge pull request #1206 from yyovil/yyovil/fix-issue-1175
refactor(core): templated orchestrator prompt
This commit is contained in:
commit
8b7a54ba5a
|
|
@ -1,7 +1,21 @@
|
|||
import { readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
{
|
||||
name: "raw-markdown",
|
||||
enforce: "pre",
|
||||
async load(id) {
|
||||
if (!id.endsWith(".md")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `export default ${JSON.stringify(await readFile(id, "utf8"))};`;
|
||||
},
|
||||
},
|
||||
],
|
||||
test: {
|
||||
include: ["__tests__/**/*.test.ts"],
|
||||
testTimeout: 10000,
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@
|
|||
"node": ">=20.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"build": "tsx ./node_modules/rollup/dist/bin/rollup -c rollup.config.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
|
|
@ -91,8 +91,12 @@
|
|||
"zod": "^3.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"@rollup/plugin-typescript": "^12.3.0",
|
||||
"@types/node": "^25.2.3",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"rollup": "^4.60.1",
|
||||
"tsx": "^4.21.0",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
import { readFile, rm } from "node:fs/promises";
|
||||
import { builtinModules } from "node:module";
|
||||
import type { Plugin, RollupOptions } from "rollup";
|
||||
import typescript from "@rollup/plugin-typescript";
|
||||
|
||||
const externalPackages = new Set(["yaml", "zod"]);
|
||||
const nodeBuiltins = new Set([
|
||||
...builtinModules,
|
||||
...builtinModules.map((moduleName) => `node:${moduleName}`),
|
||||
]);
|
||||
|
||||
function getPackageName(importId: string): string | null {
|
||||
if (importId.startsWith(".") || importId.startsWith("/")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parts = importId.split("/");
|
||||
return importId.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
|
||||
}
|
||||
|
||||
function rawMarkdown(): Plugin {
|
||||
return {
|
||||
name: "raw-markdown",
|
||||
async load(id: string) {
|
||||
if (!id.endsWith(".md")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `export default ${JSON.stringify(await readFile(id, "utf8"))};`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function cleanDist(): Plugin {
|
||||
return {
|
||||
name: "clean-dist",
|
||||
async buildStart() {
|
||||
await rm("dist", { force: true, recursive: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const config: RollupOptions = {
|
||||
input: "src/index.ts",
|
||||
output: {
|
||||
dir: "dist",
|
||||
format: "es",
|
||||
preserveModules: true,
|
||||
preserveModulesRoot: "src",
|
||||
sourcemap: true,
|
||||
},
|
||||
external(id: string) {
|
||||
if (nodeBuiltins.has(id) || id.startsWith("node:")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const packageName = getPackageName(id);
|
||||
return packageName ? externalPackages.has(packageName) : false;
|
||||
},
|
||||
plugins: [
|
||||
cleanDist(),
|
||||
rawMarkdown(),
|
||||
typescript({
|
||||
compilerOptions: {
|
||||
declaration: true,
|
||||
declarationMap: true,
|
||||
module: "Node16",
|
||||
},
|
||||
tsconfig: "./tsconfig.build.json",
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OrchestratorConfig } from "../types.js";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const packageRoot = resolve(__dirname, "../..");
|
||||
const distModuleUrl = pathToFileURL(resolve(packageRoot, "dist/orchestrator-prompt.js")).href;
|
||||
const pnpmCommand = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
|
||||
|
||||
const config: OrchestratorConfig = {
|
||||
configPath: "/tmp/agent-orchestrator.yaml",
|
||||
port: 3000,
|
||||
power: { preventIdleSleep: false },
|
||||
defaults: {
|
||||
runtime: "tmux",
|
||||
agent: "claude-code",
|
||||
workspace: "worktree",
|
||||
notifiers: ["desktop"],
|
||||
},
|
||||
projects: {
|
||||
"my-app": {
|
||||
name: "My App",
|
||||
repo: "org/my-app",
|
||||
path: "/tmp/my-app",
|
||||
defaultBranch: "main",
|
||||
sessionPrefix: "app",
|
||||
},
|
||||
},
|
||||
notifiers: {},
|
||||
notificationRouting: {
|
||||
urgent: ["desktop"],
|
||||
action: ["desktop"],
|
||||
warning: [],
|
||||
info: [],
|
||||
},
|
||||
reactions: {},
|
||||
readyThresholdMs: 300_000,
|
||||
};
|
||||
|
||||
describe("generateOrchestratorPrompt dist smoke test", () => {
|
||||
it("imports the built artifact and loads the bundled markdown template at runtime", async () => {
|
||||
execFileSync(pnpmCommand, ["build"], {
|
||||
cwd: packageRoot,
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
const { generateOrchestratorPrompt } = await import(`${distModuleUrl}?t=${Date.now()}`);
|
||||
const prompt = generateOrchestratorPrompt({
|
||||
config,
|
||||
projectId: "my-app",
|
||||
project: {
|
||||
...config.projects["my-app"]!,
|
||||
orchestratorRules: "First block\n\n\nSecond block",
|
||||
},
|
||||
});
|
||||
|
||||
expect(prompt).toContain("# My App Orchestrator");
|
||||
expect(prompt).toContain("ao session ls -p my-app");
|
||||
expect(prompt).toContain("First block\n\n\nSecond block");
|
||||
}, 15000);
|
||||
});
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { generateOrchestratorPrompt } from "../orchestrator-prompt.js";
|
||||
import type { OrchestratorConfig } from "../types.js";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OrchestratorConfig, ProjectConfig } from "../types.js";
|
||||
|
||||
const config: OrchestratorConfig = {
|
||||
configPath: "/tmp/agent-orchestrator.yaml",
|
||||
|
|
@ -32,8 +31,29 @@ const config: OrchestratorConfig = {
|
|||
readyThresholdMs: 300_000,
|
||||
};
|
||||
|
||||
async function loadGenerateOrchestratorPrompt() {
|
||||
vi.resetModules();
|
||||
return (await import("../orchestrator-prompt.js")).generateOrchestratorPrompt;
|
||||
}
|
||||
|
||||
async function loadGenerateOrchestratorPromptWithTemplate(template: string) {
|
||||
vi.resetModules();
|
||||
vi.doMock("../prompts/orchestrator.md", () => ({
|
||||
default: template,
|
||||
}));
|
||||
|
||||
return (await import("../orchestrator-prompt.js")).generateOrchestratorPrompt;
|
||||
}
|
||||
|
||||
describe("generateOrchestratorPrompt", () => {
|
||||
it("requires read-only investigation from the orchestrator session", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.doUnmock("../prompts/orchestrator.md");
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("requires read-only investigation from the orchestrator session", async () => {
|
||||
const generateOrchestratorPrompt = await loadGenerateOrchestratorPrompt();
|
||||
const prompt = generateOrchestratorPrompt({
|
||||
config,
|
||||
projectId: "my-app",
|
||||
|
|
@ -44,7 +64,8 @@ describe("generateOrchestratorPrompt", () => {
|
|||
expect(prompt).toContain("do not edit repository files or implement fixes");
|
||||
});
|
||||
|
||||
it("mandates ao send and bans raw tmux access", () => {
|
||||
it("mandates ao send and bans raw tmux access", async () => {
|
||||
const generateOrchestratorPrompt = await loadGenerateOrchestratorPrompt();
|
||||
const prompt = generateOrchestratorPrompt({
|
||||
config,
|
||||
projectId: "my-app",
|
||||
|
|
@ -56,7 +77,8 @@ describe("generateOrchestratorPrompt", () => {
|
|||
expect(prompt).toContain("ao send --no-wait");
|
||||
});
|
||||
|
||||
it("shows 'not configured' when repo is omitted", () => {
|
||||
it("shows 'not configured' and omits repo-only guidance when repo is omitted", async () => {
|
||||
const generateOrchestratorPrompt = await loadGenerateOrchestratorPrompt();
|
||||
const noRepoProject = { ...config.projects["my-app"]!, repo: undefined };
|
||||
const prompt = generateOrchestratorPrompt({
|
||||
config,
|
||||
|
|
@ -66,9 +88,14 @@ describe("generateOrchestratorPrompt", () => {
|
|||
|
||||
expect(prompt).toContain("not configured");
|
||||
expect(prompt).not.toContain("undefined");
|
||||
expect(prompt).not.toContain("ao batch-spawn");
|
||||
expect(prompt).not.toContain("ao session claim-pr");
|
||||
expect(prompt).not.toContain("PR state (open/merged/closed)");
|
||||
expect(prompt).toContain("No repository remote is configured.");
|
||||
});
|
||||
|
||||
it("pushes implementation and PR claiming into worker sessions", () => {
|
||||
it("pushes implementation and PR claiming into worker sessions", async () => {
|
||||
const generateOrchestratorPrompt = await loadGenerateOrchestratorPrompt();
|
||||
const prompt = generateOrchestratorPrompt({
|
||||
config,
|
||||
projectId: "my-app",
|
||||
|
|
@ -79,4 +106,158 @@ describe("generateOrchestratorPrompt", () => {
|
|||
expect(prompt).toContain("Never claim a PR into `app-orchestrator`");
|
||||
expect(prompt).toContain("Delegate implementation, test execution, or PR claiming");
|
||||
});
|
||||
|
||||
it("expands markdown template placeholders with typed render data", async () => {
|
||||
const generateOrchestratorPrompt = await loadGenerateOrchestratorPrompt();
|
||||
const prompt = generateOrchestratorPrompt({
|
||||
config,
|
||||
projectId: "my-app",
|
||||
project: config.projects["my-app"]!,
|
||||
});
|
||||
|
||||
expect(prompt).toContain("# My App Orchestrator");
|
||||
expect(prompt).toContain("- **Repository**: org/my-app");
|
||||
expect(prompt).toContain("ao session ls -p my-app");
|
||||
expect(prompt).toContain("http://localhost:3000");
|
||||
});
|
||||
|
||||
it("throws when the markdown template contains an unresolved snake_case placeholder", async () => {
|
||||
const generateOrchestratorPrompt =
|
||||
await loadGenerateOrchestratorPromptWithTemplate("Hello {{missing_placeholder}}");
|
||||
|
||||
expect(() =>
|
||||
generateOrchestratorPrompt({
|
||||
config,
|
||||
projectId: "my-app",
|
||||
project: config.projects["my-app"]!,
|
||||
}),
|
||||
).toThrow("Unresolved template placeholder: missing_placeholder");
|
||||
});
|
||||
|
||||
it("throws when the markdown template placeholder matches a prototype property", async () => {
|
||||
const generateOrchestratorPrompt =
|
||||
await loadGenerateOrchestratorPromptWithTemplate("Hello {{toString}}");
|
||||
|
||||
expect(() =>
|
||||
generateOrchestratorPrompt({
|
||||
config,
|
||||
projectId: "my-app",
|
||||
project: config.projects["my-app"]!,
|
||||
}),
|
||||
).toThrow("Unresolved template placeholder: toString");
|
||||
});
|
||||
|
||||
it("throws when the markdown template leaves behind a malformed placeholder", async () => {
|
||||
const generateOrchestratorPrompt =
|
||||
await loadGenerateOrchestratorPromptWithTemplate("Hello {{ projectName }}");
|
||||
|
||||
expect(() =>
|
||||
generateOrchestratorPrompt({
|
||||
config,
|
||||
projectId: "my-app",
|
||||
project: config.projects["my-app"]!,
|
||||
}),
|
||||
).toThrow("Unresolved template placeholder: {{ projectName }}");
|
||||
});
|
||||
|
||||
it("throws when the markdown template contains an unmatched optional-section marker", async () => {
|
||||
const generateOrchestratorPrompt =
|
||||
await loadGenerateOrchestratorPromptWithTemplate(
|
||||
"{{AUTOMATED_REACTIONS_SECTION_START}}\n{{automatedReactionsSection}}",
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
generateOrchestratorPrompt({
|
||||
config,
|
||||
projectId: "my-app",
|
||||
project: config.projects["my-app"]!,
|
||||
}),
|
||||
).toThrow(
|
||||
"Malformed optional section block: expected {{AUTOMATED_REACTIONS_SECTION_START}} before {{AUTOMATED_REACTIONS_SECTION_END}}",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws when the markdown template nests the same optional section type", async () => {
|
||||
const generateOrchestratorPrompt =
|
||||
await loadGenerateOrchestratorPromptWithTemplate(
|
||||
"{{REPO_CONFIGURED_SECTION_START}}outer {{REPO_CONFIGURED_SECTION_START}}inner{{REPO_CONFIGURED_SECTION_END}}{{REPO_CONFIGURED_SECTION_END}}",
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
generateOrchestratorPrompt({
|
||||
config,
|
||||
projectId: "my-app",
|
||||
project: config.projects["my-app"]!,
|
||||
}),
|
||||
).toThrow(
|
||||
"Nested optional section blocks are not supported: {{REPO_CONFIGURED_SECTION_START}} before {{REPO_CONFIGURED_SECTION_END}}",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders optional sections only when project data is present", async () => {
|
||||
const generateOrchestratorPrompt = await loadGenerateOrchestratorPrompt();
|
||||
const projectWithOptionalSections: ProjectConfig = {
|
||||
...config.projects["my-app"]!,
|
||||
reactions: {
|
||||
ci_failed: {
|
||||
auto: true,
|
||||
action: "send-to-agent",
|
||||
retries: 2,
|
||||
escalateAfter: 3,
|
||||
},
|
||||
},
|
||||
orchestratorRules: "Escalate production incidents immediately.",
|
||||
};
|
||||
|
||||
const promptWithOptionalSections = generateOrchestratorPrompt({
|
||||
config,
|
||||
projectId: "my-app",
|
||||
project: projectWithOptionalSections,
|
||||
});
|
||||
|
||||
const promptWithoutOptionalSections = generateOrchestratorPrompt({
|
||||
config,
|
||||
projectId: "my-app",
|
||||
project: config.projects["my-app"]!,
|
||||
});
|
||||
|
||||
expect(promptWithOptionalSections).toContain("## Automated Reactions");
|
||||
expect(promptWithOptionalSections).toContain("ci_failed");
|
||||
expect(promptWithOptionalSections).toContain("## Project-Specific Rules");
|
||||
expect(promptWithOptionalSections).toContain("Escalate production incidents immediately.");
|
||||
expect(promptWithoutOptionalSections).not.toContain("## Automated Reactions");
|
||||
expect(promptWithoutOptionalSections).not.toContain("## Project-Specific Rules");
|
||||
});
|
||||
|
||||
it("preserves intentional blank lines inside project-specific rules", async () => {
|
||||
const generateOrchestratorPrompt = await loadGenerateOrchestratorPrompt();
|
||||
const projectWithSpacedRules: ProjectConfig = {
|
||||
...config.projects["my-app"]!,
|
||||
orchestratorRules: "First block\n\n\nSecond block",
|
||||
};
|
||||
|
||||
const prompt = generateOrchestratorPrompt({
|
||||
config,
|
||||
projectId: "my-app",
|
||||
project: projectWithSpacedRules,
|
||||
});
|
||||
|
||||
expect(prompt).toContain("First block\n\n\nSecond block");
|
||||
});
|
||||
|
||||
it("allows literal handlebars inside project-specific rules", async () => {
|
||||
const generateOrchestratorPrompt = await loadGenerateOrchestratorPrompt();
|
||||
const projectWithHandlebarsRules: ProjectConfig = {
|
||||
...config.projects["my-app"]!,
|
||||
orchestratorRules: "Use {{example}} literally in the docs.",
|
||||
};
|
||||
|
||||
const prompt = generateOrchestratorPrompt({
|
||||
config,
|
||||
projectId: "my-app",
|
||||
project: projectWithHandlebarsRules,
|
||||
});
|
||||
|
||||
expect(prompt).toContain("Use {{example}} literally in the docs.");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
declare module "*.md" {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
/**
|
||||
* Orchestrator Prompt Generator — generates orchestrator prompt content.
|
||||
* Orchestrator Prompt Generator - generates orchestrator prompt content.
|
||||
*
|
||||
* This is injected via `ao start` to provide orchestrator-specific context
|
||||
* when the orchestrator agent runs.
|
||||
*/
|
||||
|
||||
import orchestratorTemplate from "./prompts/orchestrator.md";
|
||||
import type { OrchestratorConfig, ProjectConfig } from "./types.js";
|
||||
|
||||
export interface OrchestratorPromptConfig {
|
||||
|
|
@ -13,287 +14,186 @@ export interface OrchestratorPromptConfig {
|
|||
project: ProjectConfig;
|
||||
}
|
||||
|
||||
interface OrchestratorPromptRenderData {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
projectRepo: string;
|
||||
projectDefaultBranch: string;
|
||||
projectSessionPrefix: string;
|
||||
projectPath: string;
|
||||
dashboardPort: string;
|
||||
automatedReactionsSection: string;
|
||||
projectSpecificRulesSection: string;
|
||||
repoConfiguredSection: string;
|
||||
repoNotConfiguredSection: string;
|
||||
}
|
||||
|
||||
type OrchestratorPromptRenderKey = keyof OrchestratorPromptRenderData;
|
||||
|
||||
function buildAutomatedReactionsSection(project: ProjectConfig): string {
|
||||
const markdownBold = String.fromCharCode(42).repeat(2);
|
||||
const bold = (text: string): string => `${markdownBold}${text}${markdownBold}`;
|
||||
|
||||
const reactionLines: string[] = [];
|
||||
|
||||
for (const [event, reaction] of Object.entries(project.reactions ?? {})) {
|
||||
if (reaction.auto && reaction.action === "send-to-agent") {
|
||||
reactionLines.push(
|
||||
`- ${bold(event)}: Auto-sends instruction to agent (retries: ${reaction.retries ?? "none"}, escalates after: ${reaction.escalateAfter ?? "never"})`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (reaction.auto && reaction.action === "notify") {
|
||||
reactionLines.push(
|
||||
`- ${bold(event)}: Notifies human (priority: ${reaction.priority ?? "info"})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (reactionLines.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return reactionLines.join("\n");
|
||||
}
|
||||
|
||||
function buildProjectSpecificRulesSection(project: ProjectConfig): string {
|
||||
const rules = project.orchestratorRules?.trim();
|
||||
if (!rules) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return rules;
|
||||
}
|
||||
|
||||
function removeOptionalSectionBlocks(
|
||||
template: string,
|
||||
data: OrchestratorPromptRenderData,
|
||||
): string {
|
||||
const templates = [
|
||||
["REPO_CONFIGURED_SECTION_START", "REPO_CONFIGURED_SECTION_END", data.repoConfiguredSection],
|
||||
["REPO_NOT_CONFIGURED_SECTION_START", "REPO_NOT_CONFIGURED_SECTION_END", data.repoNotConfiguredSection],
|
||||
["AUTOMATED_REACTIONS_SECTION_START", "AUTOMATED_REACTIONS_SECTION_END", data.automatedReactionsSection],
|
||||
["PROJECT_SPECIFIC_RULES_SECTION_START", "PROJECT_SPECIFIC_RULES_SECTION_END", data.projectSpecificRulesSection],
|
||||
] as const;
|
||||
|
||||
let interpolated = template;
|
||||
for (const [startKey, endKey, section] of templates) {
|
||||
const startMarker = `{{${startKey}}}`;
|
||||
const endMarker = `{{${endKey}}}`;
|
||||
|
||||
while (true) {
|
||||
const start = interpolated.indexOf(startMarker);
|
||||
const end = interpolated.indexOf(endMarker);
|
||||
|
||||
if (start === -1 && end === -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (start === -1 || end === -1 || end < start) {
|
||||
throw new Error(
|
||||
`Malformed optional section block: expected ${startMarker} before ${endMarker}`,
|
||||
);
|
||||
}
|
||||
|
||||
const fullStart = start;
|
||||
const fullEnd = end + endMarker.length;
|
||||
const blockContent = interpolated.slice(start + startMarker.length, end);
|
||||
// Optional sections are flat by design. Reject nesting of the same block
|
||||
// type so future template edits fail loudly instead of matching ambiguously.
|
||||
if (blockContent.includes(startMarker)) {
|
||||
throw new Error(
|
||||
`Nested optional section blocks are not supported: ${startMarker} before ${endMarker}`,
|
||||
);
|
||||
}
|
||||
|
||||
const replacement = section ? blockContent : "";
|
||||
const before = interpolated.slice(0, fullStart);
|
||||
const after = interpolated.slice(fullEnd);
|
||||
|
||||
interpolated = replacement
|
||||
? before + replacement + after
|
||||
: collapseOptionalGap(before, after);
|
||||
}
|
||||
}
|
||||
|
||||
return interpolated;
|
||||
}
|
||||
|
||||
function collapseOptionalGap(before: string, after: string): string {
|
||||
const trailingNewlines = before.match(/\n*$/)?.[0] ?? "";
|
||||
const leadingNewlines = after.match(/^\n*/)?.[0] ?? "";
|
||||
const totalNewlines = trailingNewlines.length + leadingNewlines.length;
|
||||
const boundary = totalNewlines >= 2 ? "\n\n" : trailingNewlines + leadingNewlines;
|
||||
|
||||
return (
|
||||
before.slice(0, before.length - trailingNewlines.length) +
|
||||
boundary +
|
||||
after.slice(leadingNewlines.length)
|
||||
);
|
||||
}
|
||||
|
||||
function hasRenderDataKey(
|
||||
data: OrchestratorPromptRenderData,
|
||||
key: string,
|
||||
): key is OrchestratorPromptRenderKey {
|
||||
return Object.prototype.hasOwnProperty.call(data, key);
|
||||
}
|
||||
|
||||
function createRenderData(opts: OrchestratorPromptConfig): OrchestratorPromptRenderData {
|
||||
const { config, projectId, project } = opts;
|
||||
const hasRepo = Boolean(project.repo);
|
||||
|
||||
return {
|
||||
projectId,
|
||||
projectName: project.name,
|
||||
projectRepo: project.repo ?? "not configured",
|
||||
projectDefaultBranch: project.defaultBranch,
|
||||
projectSessionPrefix: project.sessionPrefix,
|
||||
projectPath: project.path,
|
||||
dashboardPort: String(config.port ?? 3000),
|
||||
automatedReactionsSection: buildAutomatedReactionsSection(project),
|
||||
projectSpecificRulesSection: buildProjectSpecificRulesSection(project),
|
||||
repoConfiguredSection: hasRepo ? "true" : "",
|
||||
repoNotConfiguredSection: hasRepo ? "" : "true",
|
||||
};
|
||||
}
|
||||
|
||||
function renderTemplate(template: string, data: OrchestratorPromptRenderData): string {
|
||||
const unresolvedPlaceholder = template
|
||||
.replace(/\{\{([a-zA-Z0-9_]+)\}\}/g, "")
|
||||
.match(/\{\{[^}]+\}\}/);
|
||||
if (unresolvedPlaceholder) {
|
||||
throw new Error(`Unresolved template placeholder: ${unresolvedPlaceholder[0]}`);
|
||||
}
|
||||
|
||||
return template.replace(/\{\{([a-zA-Z0-9_]+)\}\}/g, (_match, rawKey: string) => {
|
||||
if (!hasRenderDataKey(data, rawKey)) {
|
||||
throw new Error(`Unresolved template placeholder: ${rawKey}`);
|
||||
}
|
||||
|
||||
return data[rawKey];
|
||||
});
|
||||
}
|
||||
|
||||
function finalizeRenderedPrompt(prompt: string): string {
|
||||
return prompt.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate orchestrator prompt content.
|
||||
* 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(`# ${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.`);
|
||||
|
||||
sections.push(`## Non-Negotiable Rules
|
||||
|
||||
- Investigations from the orchestrator session are **read-only**. Inspect status, logs, metadata, PR state, and worker output, but do not edit repository files or implement fixes from the orchestrator session.
|
||||
- Any code change, test run tied to implementation, git branch work, or PR takeover must be delegated to a **worker session**.
|
||||
- The orchestrator session must never own a PR. Never claim a PR into the orchestrator session, and never treat the orchestrator as the worker responsible for implementation.
|
||||
- If an investigation discovers follow-up work, either spawn a worker session or direct an existing worker session with clear instructions.
|
||||
- **Always use \`ao send\` to communicate with sessions** — never use raw \`tmux send-keys\` or \`tmux capture-pane\`. Direct tmux access bypasses busy detection, retry logic, and input sanitization, and breaks multi-line input for some agents (e.g. Codex).
|
||||
- When a session might be busy, use \`ao send --no-wait <session> <message>\` to send without waiting for the session to become idle.`);
|
||||
|
||||
// Project Info
|
||||
sections.push(`## Project Info
|
||||
|
||||
- **Name**: ${project.name}
|
||||
- **Repository**: ${project.repo ?? "not configured"}
|
||||
- **Default Branch**: ${project.defaultBranch}
|
||||
- **Session Prefix**: ${project.sessionPrefix}
|
||||
- **Local Path**: ${project.path}
|
||||
- **Dashboard Port**: ${config.port ?? 3000}`);
|
||||
|
||||
// Quick Start — include issue/PR commands only when repo is configured
|
||||
if (project.repo) {
|
||||
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 INT-1234
|
||||
ao spawn --claim-pr 123
|
||||
ao batch-spawn INT-1 INT-2 INT-3
|
||||
|
||||
# Spawn a session without a tracker issue (prompt-driven)
|
||||
ao spawn --prompt "Refactor the auth module to use JWT"
|
||||
|
||||
# List sessions
|
||||
ao session ls -p ${projectId}
|
||||
|
||||
# Send message to a session
|
||||
ao send ${project.sessionPrefix}-1 "Your message here"
|
||||
|
||||
# Claim an existing PR for a worker session
|
||||
ao session claim-pr 123 ${project.sessionPrefix}-1
|
||||
|
||||
# Kill a session
|
||||
ao session kill ${project.sessionPrefix}-1
|
||||
|
||||
# Open all sessions in terminal tabs
|
||||
ao open ${projectId}
|
||||
\`\`\``);
|
||||
} else {
|
||||
sections.push(`## Quick Start
|
||||
|
||||
\`\`\`bash
|
||||
# See all sessions at a glance
|
||||
ao status
|
||||
|
||||
# Spawn a session (prompt-driven, no issue tracker configured)
|
||||
ao spawn --prompt "Refactor the auth module to use JWT"
|
||||
|
||||
# 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
|
||||
\`\`\`
|
||||
|
||||
> **Note:** No repository remote is configured. Issue tracking, PR, and CI features are unavailable.
|
||||
> Add a \`repo\` field (owner/repo) to \`agent-orchestrator.yaml\` to enable them.`);
|
||||
}
|
||||
|
||||
// Available Commands — omit PR/issue commands when no repo configured
|
||||
const cmdRows = [
|
||||
`| \`ao status\` | Show all sessions${project.repo ? " with PR/CI/review status" : ""} |`,
|
||||
`| \`ao spawn [issue] [--prompt <text>]${project.repo ? " [--claim-pr <pr>]" : ""}\` | Spawn a worker session${project.repo ? "; use issue ID or --prompt for freeform tasks" : " with --prompt for freeform tasks"} |`,
|
||||
...(project.repo ? [`| \`ao batch-spawn <issues...>\` | Spawn multiple sessions in parallel (project auto-detected) |`] : []),
|
||||
`| \`ao session ls [-p project]\` | List all sessions (optionally filter by project) |`,
|
||||
...(project.repo ? [`| \`ao session claim-pr <pr> [session]\` | Attach an existing PR to a worker session |`] : []),
|
||||
`| \`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 send --no-wait <session> <message>\` | Send without waiting for session to become idle |`,
|
||||
`| \`ao dashboard\` | Start the web dashboard (http://localhost:${config.port ?? 3000}) |`,
|
||||
`| \`ao open <project>\` | Open all project sessions in terminal tabs |`,
|
||||
];
|
||||
|
||||
sections.push(`## Available Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
${cmdRows.join("\n")}`);
|
||||
|
||||
// Session Management
|
||||
const sessionMgmt = [`## 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\` for issues, \`session/<id>\` for prompt-driven)
|
||||
3. A tmux session is started (e.g., \`${project.sessionPrefix}-1\`)
|
||||
4. The agent is launched with context about the issue or prompt
|
||||
5. Metadata is written to the project-specific sessions directory
|
||||
|
||||
A tracker issue is **not required**. Use \`--prompt\` to spawn freeform sessions:
|
||||
\`\`\`bash
|
||||
ao spawn --prompt "Add rate limiting to the /api/upload endpoint"
|
||||
\`\`\`
|
||||
|
||||
### Monitoring Progress
|
||||
|
||||
Use \`ao status\` to see:
|
||||
- Current session status (working, pr_open, review_pending, etc.)${project.repo ? `
|
||||
- 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"
|
||||
\`\`\``];
|
||||
|
||||
if (project.repo) {
|
||||
sessionMgmt.push(`
|
||||
### PR Takeover
|
||||
|
||||
If a worker session needs to continue work on an existing PR:
|
||||
\`\`\`bash
|
||||
ao session claim-pr 123 ${project.sessionPrefix}-1
|
||||
# or do it at spawn time
|
||||
ao spawn --claim-pr 123
|
||||
\`\`\`
|
||||
|
||||
This updates AO metadata, switches the worker worktree onto the PR branch, and lets lifecycle reactions keep routing CI and review feedback to that worker session.
|
||||
|
||||
Never claim a PR into \`${project.sessionPrefix}-orchestrator\`. If a PR needs implementation or takeover, delegate it to a worker session instead.`);
|
||||
}
|
||||
|
||||
sessionMgmt.push(`
|
||||
### Investigation Workflow
|
||||
|
||||
When debugging or triaging from the orchestrator session:
|
||||
1. Inspect with read-only commands such as \`ao status\`, \`ao session ls\`, \`ao session attach\`, and SCM/tracker lookups.
|
||||
2. Decide whether a worker already owns the work or a new worker is needed.
|
||||
3. Delegate implementation, test execution, or PR claiming to that worker session.
|
||||
4. Return to monitoring and coordination once the worker has the task.
|
||||
|
||||
### Cleanup
|
||||
|
||||
Remove completed sessions:
|
||||
\`\`\`bash
|
||||
ao session cleanup -p ${projectId} # Kill sessions where PR is merged or issue is closed
|
||||
\`\`\``);
|
||||
|
||||
sections.push(sessionMgmt.join("\n"));
|
||||
|
||||
// Dashboard
|
||||
sections.push(`## Dashboard
|
||||
|
||||
The web dashboard runs at **http://localhost:${config.port ?? 3000}**.
|
||||
|
||||
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
|
||||
const workflows = [`## Common Workflows`];
|
||||
|
||||
if (project.repo) {
|
||||
workflows.push(`
|
||||
### 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`);
|
||||
}
|
||||
|
||||
workflows.push(`
|
||||
### 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`);
|
||||
|
||||
if (project.repo) {
|
||||
workflows.push(`
|
||||
### 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)`);
|
||||
}
|
||||
|
||||
workflows.push(`
|
||||
### 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 the human-only action yourself${project.repo ? " (merge PR, close issue, etc.)" : ""} while keeping implementation in worker sessions.`);
|
||||
|
||||
sections.push(workflows.join("\n"));
|
||||
|
||||
// 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
|
||||
|
||||
${project.orchestratorRules}`);
|
||||
}
|
||||
|
||||
return sections.join("\n\n");
|
||||
const data = createRenderData(opts);
|
||||
const templateWithOptionalSections = removeOptionalSectionBlocks(
|
||||
orchestratorTemplate.trim(),
|
||||
data,
|
||||
);
|
||||
|
||||
return finalizeRenderedPrompt(
|
||||
renderTemplate(templateWithOptionalSections, data),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,223 @@
|
|||
# {{projectName}} Orchestrator
|
||||
|
||||
You are the **orchestrator agent** for the {{projectName}} 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.
|
||||
|
||||
## Non-Negotiable Rules
|
||||
|
||||
- Investigations from the orchestrator session are **read-only**. Inspect status, logs, metadata, PR state, and worker output, but do not edit repository files or implement fixes from the orchestrator session.
|
||||
- Any code change, test run tied to implementation, git branch work, or PR takeover must be delegated to a **worker session**.
|
||||
- The orchestrator session must never own a PR. Never claim a PR into the orchestrator session, and never treat the orchestrator as the worker responsible for implementation.
|
||||
- If an investigation discovers follow-up work, either spawn a worker session or direct an existing worker session with clear instructions.
|
||||
- **Always use `ao send` to communicate with sessions** - never use raw `tmux send-keys` or `tmux capture-pane`. Direct tmux access bypasses busy detection, retry logic, and input sanitization, and breaks multi-line input for some agents (e.g. Codex).
|
||||
- When a session might be busy, use `ao send --no-wait <session> <message>` to send without waiting for the session to become idle.
|
||||
|
||||
## Project Info
|
||||
|
||||
- **Name**: {{projectName}}
|
||||
- **Repository**: {{projectRepo}}
|
||||
- **Default Branch**: {{projectDefaultBranch}}
|
||||
- **Session Prefix**: {{projectSessionPrefix}}
|
||||
- **Local Path**: {{projectPath}}
|
||||
- **Dashboard Port**: {{dashboardPort}}
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# See all sessions at a glance
|
||||
ao status
|
||||
|
||||
{{REPO_CONFIGURED_SECTION_START}}# Spawn sessions for issues (GitHub: #123, Linear: INT-1234, etc.)
|
||||
ao spawn INT-1234
|
||||
ao spawn --claim-pr 123
|
||||
ao batch-spawn INT-1 INT-2 INT-3
|
||||
|
||||
{{REPO_CONFIGURED_SECTION_END}}# Spawn a session without a tracker issue (prompt-driven)
|
||||
ao spawn --prompt "Refactor the auth module to use JWT"
|
||||
|
||||
# List sessions
|
||||
ao session ls -p {{projectId}}
|
||||
|
||||
# Send message to a session
|
||||
ao send {{projectSessionPrefix}}-1 "Your message here"
|
||||
|
||||
{{REPO_CONFIGURED_SECTION_START}}# Claim an existing PR for a worker session
|
||||
ao session claim-pr 123 {{projectSessionPrefix}}-1
|
||||
|
||||
{{REPO_CONFIGURED_SECTION_END}}# Kill a session
|
||||
ao session kill {{projectSessionPrefix}}-1
|
||||
{{REPO_CONFIGURED_SECTION_START}}
|
||||
# Open all sessions in terminal tabs
|
||||
ao open {{projectId}}{{REPO_CONFIGURED_SECTION_END}}
|
||||
```
|
||||
|
||||
{{REPO_NOT_CONFIGURED_SECTION_START}}
|
||||
> **Note:** No repository remote is configured. Issue tracking, PR, and CI features are unavailable.
|
||||
> Add a `repo` field (owner/repo) to `agent-orchestrator.yaml` to enable them.
|
||||
{{REPO_NOT_CONFIGURED_SECTION_END}}
|
||||
|
||||
## Available Commands
|
||||
|
||||
- `ao status`: Show all sessions{{REPO_CONFIGURED_SECTION_START}} with PR/CI/review status{{REPO_CONFIGURED_SECTION_END}}
|
||||
- `ao spawn [issue] [--prompt <text>]{{REPO_CONFIGURED_SECTION_START}} [--claim-pr <pr>]{{REPO_CONFIGURED_SECTION_END}}`: Spawn a worker session{{REPO_CONFIGURED_SECTION_START}}; use issue ID or --prompt for freeform tasks{{REPO_CONFIGURED_SECTION_END}}{{REPO_NOT_CONFIGURED_SECTION_START}} with --prompt for freeform tasks{{REPO_NOT_CONFIGURED_SECTION_END}}
|
||||
{{REPO_CONFIGURED_SECTION_START}}- `ao batch-spawn <issues...>`: Spawn multiple sessions in parallel (project auto-detected)
|
||||
{{REPO_CONFIGURED_SECTION_END}}- `ao session ls [-p project]`: List all sessions (optionally filter by project)
|
||||
{{REPO_CONFIGURED_SECTION_START}}- `ao session claim-pr <pr> [session]`: Attach an existing PR to a worker session
|
||||
{{REPO_CONFIGURED_SECTION_END}}- `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 send --no-wait <session> <message>`: Send without waiting for session to become idle
|
||||
- `ao dashboard`: Start the web dashboard (http://localhost:{{dashboardPort}})
|
||||
- `ao open <project>`: Open all project sessions in terminal tabs
|
||||
|
||||
## Session Management
|
||||
|
||||
### Spawning Sessions
|
||||
|
||||
When you spawn a session:
|
||||
|
||||
1. A git worktree is created from `{{projectDefaultBranch}}`
|
||||
2. A feature branch is created (e.g., `feat/INT-1234` for issues, `session/<id>` for prompt-driven)
|
||||
3. A tmux session is started (e.g., `{{projectSessionPrefix}}-1`)
|
||||
4. The agent is launched with context about the issue or prompt
|
||||
5. Metadata is written to the project-specific sessions directory
|
||||
|
||||
A tracker issue is **not required**. Use `--prompt` to spawn freeform sessions:
|
||||
|
||||
```bash
|
||||
ao spawn --prompt "Add rate limiting to the /api/upload endpoint"
|
||||
```
|
||||
|
||||
### Monitoring Progress
|
||||
|
||||
Use `ao status` to see:
|
||||
|
||||
- Current session status (working, pr_open, review_pending, etc.)
|
||||
{{REPO_CONFIGURED_SECTION_START}}- PR state (open/merged/closed)
|
||||
- CI status (passing/failing/pending)
|
||||
- Review decision (approved/changes_requested/pending)
|
||||
- Unresolved comments count
|
||||
{{REPO_CONFIGURED_SECTION_END}}
|
||||
|
||||
### Sending Messages
|
||||
|
||||
Send instructions to a running agent:
|
||||
|
||||
```bash
|
||||
ao send {{projectSessionPrefix}}-1 "Please address the review comments on your PR"
|
||||
```
|
||||
|
||||
{{REPO_CONFIGURED_SECTION_START}}### PR Takeover
|
||||
|
||||
If a worker session needs to continue work on an existing PR:
|
||||
|
||||
```bash
|
||||
ao session claim-pr 123 {{projectSessionPrefix}}-1
|
||||
# or do it at spawn time
|
||||
ao spawn --claim-pr 123
|
||||
```
|
||||
|
||||
This updates AO metadata, switches the worker worktree onto the PR branch, and lets lifecycle reactions keep routing CI and review feedback to that worker session.
|
||||
|
||||
Never claim a PR into `{{projectSessionPrefix}}-orchestrator`. If a PR needs implementation or takeover, delegate it to a worker session instead.
|
||||
{{REPO_CONFIGURED_SECTION_END}}
|
||||
|
||||
### Investigation Workflow
|
||||
|
||||
When debugging or triaging from the orchestrator session:
|
||||
|
||||
1. Inspect with read-only commands such as `ao status`, `ao session ls`, `ao session attach`, and SCM/tracker lookups.
|
||||
2. Decide whether a worker already owns the work or a new worker is needed.
|
||||
3. Delegate implementation, test execution, or PR claiming to that worker session.
|
||||
4. Return to monitoring and coordination once the worker has the task.
|
||||
|
||||
### Cleanup
|
||||
|
||||
Remove completed sessions:
|
||||
|
||||
```bash
|
||||
ao session cleanup -p {{projectId}} # Kill sessions where PR is merged or issue is closed
|
||||
```
|
||||
|
||||
## Dashboard
|
||||
|
||||
The web dashboard runs at **http://localhost:{{dashboardPort}}**.
|
||||
|
||||
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
|
||||
|
||||
{{AUTOMATED_REACTIONS_SECTION_START}}
|
||||
|
||||
## Automated Reactions
|
||||
|
||||
The system automatically handles these events:
|
||||
|
||||
{{automatedReactionsSection}}
|
||||
{{AUTOMATED_REACTIONS_SECTION_END}}
|
||||
|
||||
## Common Workflows
|
||||
|
||||
{{REPO_CONFIGURED_SECTION_START}}### 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
|
||||
|
||||
{{REPO_CONFIGURED_SECTION_END}}### 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
|
||||
|
||||
{{REPO_CONFIGURED_SECTION_START}}### 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)
|
||||
|
||||
{{REPO_CONFIGURED_SECTION_END}}### 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 the human-only action yourself{{REPO_CONFIGURED_SECTION_START}} (merge PR, close issue, etc.){{REPO_CONFIGURED_SECTION_END}} while keeping implementation in worker sessions.
|
||||
|
||||
## 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_SECTION_START}}
|
||||
|
||||
## Project-Specific Rules
|
||||
|
||||
{{projectSpecificRulesSection}}
|
||||
{{PROJECT_SPECIFIC_RULES_SECTION_END}}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["src/__tests__"]
|
||||
"exclude": ["src/__tests__/**"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,24 @@
|
|||
import { defineConfig } from "vitest/config";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
{
|
||||
name: "raw-markdown",
|
||||
enforce: "pre",
|
||||
async load(id) {
|
||||
if (!id.endsWith(".md")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `export default ${JSON.stringify(await readFile(id, "utf8"))};`;
|
||||
},
|
||||
},
|
||||
],
|
||||
test: {
|
||||
alias: {
|
||||
// Integration tests import real plugins. These aliases resolve
|
||||
|
|
|
|||
|
|
@ -1,9 +1,23 @@
|
|||
import { readFile } from "node:fs/promises";
|
||||
import { defineConfig } from "vitest/config";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
plugins: [
|
||||
react(),
|
||||
{
|
||||
name: "raw-markdown",
|
||||
enforce: "pre",
|
||||
async load(id) {
|
||||
if (!id.endsWith(".md")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `export default ${JSON.stringify(await readFile(id, "utf8"))};`;
|
||||
},
|
||||
},
|
||||
],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "jsdom",
|
||||
|
|
|
|||
|
|
@ -154,12 +154,24 @@ importers:
|
|||
specifier: ^3.24.0
|
||||
version: 3.25.76
|
||||
devDependencies:
|
||||
'@rollup/plugin-typescript':
|
||||
specifier: ^12.3.0
|
||||
version: 12.3.0(rollup@4.60.1)(tslib@2.8.1)(typescript@5.9.3)
|
||||
'@types/node':
|
||||
specifier: ^25.2.3
|
||||
version: 25.6.0
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^4.0.18
|
||||
version: 4.1.4(vitest@4.1.4)
|
||||
rollup:
|
||||
specifier: ^4.60.1
|
||||
version: 4.60.1
|
||||
tslib:
|
||||
specifier: ^2.8.1
|
||||
version: 2.8.1
|
||||
tsx:
|
||||
specifier: ^4.21.0
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5.7.0
|
||||
version: 5.9.3
|
||||
|
|
@ -1685,6 +1697,28 @@ packages:
|
|||
'@rolldown/pluginutils@1.0.0-beta.27':
|
||||
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
|
||||
|
||||
'@rollup/plugin-typescript@12.3.0':
|
||||
resolution: {integrity: sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
rollup: ^2.14.0||^3.0.0||^4.0.0
|
||||
tslib: '*'
|
||||
typescript: '>=3.7.0'
|
||||
peerDependenciesMeta:
|
||||
rollup:
|
||||
optional: true
|
||||
tslib:
|
||||
optional: true
|
||||
|
||||
'@rollup/pluginutils@5.3.0':
|
||||
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
|
||||
peerDependenciesMeta:
|
||||
rollup:
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.60.1':
|
||||
resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==}
|
||||
cpu: [arm]
|
||||
|
|
@ -2616,6 +2650,9 @@ packages:
|
|||
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
|
||||
engines: {node: '>=4.0'}
|
||||
|
||||
estree-walker@2.0.2:
|
||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||
|
||||
estree-walker@3.0.3:
|
||||
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||
|
||||
|
|
@ -2871,6 +2908,10 @@ packages:
|
|||
resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
is-core-module@2.16.1:
|
||||
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-docker@2.2.1:
|
||||
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -3426,6 +3467,9 @@ packages:
|
|||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
path-parse@1.0.7:
|
||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||
|
||||
path-root-regex@0.1.2:
|
||||
resolution: {integrity: sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -3567,6 +3611,11 @@ packages:
|
|||
resolve-pkg-maps@1.0.0:
|
||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||
|
||||
resolve@1.22.12:
|
||||
resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
hasBin: true
|
||||
|
||||
restore-cursor@5.1.0:
|
||||
resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -3748,6 +3797,10 @@ packages:
|
|||
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
symbol-tree@3.2.4:
|
||||
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
|
||||
|
||||
|
|
@ -5148,6 +5201,23 @@ snapshots:
|
|||
|
||||
'@rolldown/pluginutils@1.0.0-beta.27': {}
|
||||
|
||||
'@rollup/plugin-typescript@12.3.0(rollup@4.60.1)(tslib@2.8.1)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
resolve: 1.22.12
|
||||
typescript: 5.9.3
|
||||
optionalDependencies:
|
||||
rollup: 4.60.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@rollup/pluginutils@5.3.0(rollup@4.60.1)':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
estree-walker: 2.0.2
|
||||
picomatch: 4.0.4
|
||||
optionalDependencies:
|
||||
rollup: 4.60.1
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.60.1':
|
||||
optional: true
|
||||
|
||||
|
|
@ -6145,6 +6215,8 @@ snapshots:
|
|||
|
||||
estraverse@5.3.0: {}
|
||||
|
||||
estree-walker@2.0.2: {}
|
||||
|
||||
estree-walker@3.0.3:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
|
|
@ -6394,6 +6466,10 @@ snapshots:
|
|||
|
||||
ip-address@10.1.0: {}
|
||||
|
||||
is-core-module@2.16.1:
|
||||
dependencies:
|
||||
hasown: 2.0.2
|
||||
|
||||
is-docker@2.2.1: {}
|
||||
|
||||
is-extglob@2.1.1: {}
|
||||
|
|
@ -6918,6 +6994,8 @@ snapshots:
|
|||
|
||||
path-key@3.1.1: {}
|
||||
|
||||
path-parse@1.0.7: {}
|
||||
|
||||
path-root-regex@0.1.2: {}
|
||||
|
||||
path-root@0.1.1:
|
||||
|
|
@ -7029,6 +7107,13 @@ snapshots:
|
|||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
|
||||
resolve@1.22.12:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
is-core-module: 2.16.1
|
||||
path-parse: 1.0.7
|
||||
supports-preserve-symlinks-flag: 1.0.0
|
||||
|
||||
restore-cursor@5.1.0:
|
||||
dependencies:
|
||||
onetime: 7.0.0
|
||||
|
|
@ -7241,6 +7326,8 @@ snapshots:
|
|||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
symbol-tree@3.2.4: {}
|
||||
|
||||
tailwindcss@4.2.2: {}
|
||||
|
|
|
|||
Loading…
Reference in New Issue