diff --git a/packages/cli/vitest.config.ts b/packages/cli/vitest.config.ts index cf0b05683..fda3d709d 100644 --- a/packages/cli/vitest.config.ts +++ b/packages/cli/vitest.config.ts @@ -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, diff --git a/packages/core/package.json b/packages/core/package.json index c101f98cf..d80d59a90 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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" } diff --git a/packages/core/rollup.config.ts b/packages/core/rollup.config.ts new file mode 100644 index 000000000..6adee8d18 --- /dev/null +++ b/packages/core/rollup.config.ts @@ -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; diff --git a/packages/core/src/__tests__/orchestrator-prompt.dist.test.ts b/packages/core/src/__tests__/orchestrator-prompt.dist.test.ts new file mode 100644 index 000000000..712680075 --- /dev/null +++ b/packages/core/src/__tests__/orchestrator-prompt.dist.test.ts @@ -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); +}); diff --git a/packages/core/src/__tests__/orchestrator-prompt.test.ts b/packages/core/src/__tests__/orchestrator-prompt.test.ts index c9fcf26c8..268ce1124 100644 --- a/packages/core/src/__tests__/orchestrator-prompt.test.ts +++ b/packages/core/src/__tests__/orchestrator-prompt.test.ts @@ -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."); + }); }); diff --git a/packages/core/src/markdown.d.ts b/packages/core/src/markdown.d.ts new file mode 100644 index 000000000..c94d67b1a --- /dev/null +++ b/packages/core/src/markdown.d.ts @@ -0,0 +1,4 @@ +declare module "*.md" { + const content: string; + export default content; +} diff --git a/packages/core/src/orchestrator-prompt.ts b/packages/core/src/orchestrator-prompt.ts index 7daaab4cc..fa8058bd1 100644 --- a/packages/core/src/orchestrator-prompt.ts +++ b/packages/core/src/orchestrator-prompt.ts @@ -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 \` 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 ]${project.repo ? " [--claim-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 \` | 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 [session]\` | Attach an existing PR to a worker session |`] : []), - `| \`ao session attach \` | Attach to a session's tmux window |`, - `| \`ao session kill \` | Kill a specific session |`, - `| \`ao session cleanup [-p project]\` | Kill completed/merged sessions |`, - `| \`ao send \` | Send a message to a running session |`, - `| \`ao send --no-wait \` | Send without waiting for session to become idle |`, - `| \`ao dashboard\` | Start the web dashboard (http://localhost:${config.port ?? 3000}) |`, - `| \`ao open \` | 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/\` 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 \` to see what they're doing -3. Send clarification or instructions with \`ao send '...'\` -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 \` -4. Send instructions: \`ao send '...'\` -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), + ); } diff --git a/packages/core/src/prompts/orchestrator.md b/packages/core/src/prompts/orchestrator.md new file mode 100644 index 000000000..941fda018 --- /dev/null +++ b/packages/core/src/prompts/orchestrator.md @@ -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 ` 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 ]{{REPO_CONFIGURED_SECTION_START}} [--claim-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 `: 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 [session]`: Attach an existing PR to a worker session +{{REPO_CONFIGURED_SECTION_END}}- `ao session attach `: Attach to a session's tmux window +- `ao session kill `: Kill a specific session +- `ao session cleanup [-p project]`: Kill completed/merged sessions +- `ao send `: Send a message to a running session +- `ao send --no-wait `: Send without waiting for session to become idle +- `ao dashboard`: Start the web dashboard (http://localhost:{{dashboardPort}}) +- `ao open `: 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/` 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 ` to see what they're doing +3. Send clarification or instructions with `ao send '...'` +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 ` +4. Send instructions: `ao send '...'` +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}} diff --git a/packages/core/tsconfig.build.json b/packages/core/tsconfig.build.json index 4dc23fb9c..6dc6b9013 100644 --- a/packages/core/tsconfig.build.json +++ b/packages/core/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", - "exclude": ["src/__tests__"] + "exclude": ["src/__tests__/**"] } diff --git a/packages/core/vitest.config.ts b/packages/core/vitest.config.ts index e0b2771ee..9654a19dc 100644 --- a/packages/core/vitest.config.ts +++ b/packages/core/vitest.config.ts @@ -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 diff --git a/packages/web/vitest.config.ts b/packages/web/vitest.config.ts index d1c3f7a87..7da000157 100644 --- a/packages/web/vitest.config.ts +++ b/packages/web/vitest.config.ts @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index abd3d743a..f9d7a39ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {}