import type { AgentPromptMetadata } from "./types" import { truncateDescription } from "../shared/truncate-description" export interface AvailableAgent { name: string description: string metadata: AgentPromptMetadata } export interface AvailableTool { name: string category: "lsp" | "ast" | "search" | "session" | "command" | "other" } export interface AvailableSkill { name: string description: string location: "user" | "project" | "plugin" } export interface AvailableCategory { name: string description: string model?: string } export function categorizeTools(toolNames: string[]): AvailableTool[] { return toolNames.map((name) => { let category: AvailableTool["category"] = "other" if (name.startsWith("lsp_")) { category = "lsp" } else if (name.startsWith("ast_grep")) { category = "ast" } else if (name === "grep" || name === "glob") { category = "search" } else if (name.startsWith("session_")) { category = "session" } else if (name === "skill") { category = "command" } return { name, category } }) } function formatToolsForPrompt(tools: AvailableTool[]): string { const lspTools = tools.filter((t) => t.category === "lsp") const astTools = tools.filter((t) => t.category === "ast") const searchTools = tools.filter((t) => t.category === "search") const parts: string[] = [] if (searchTools.length > 0) { parts.push(...searchTools.map((t) => `\`${t.name}\``)) } if (lspTools.length > 0) { parts.push("`lsp_*`") } if (astTools.length > 0) { parts.push("`ast_grep`") } return parts.join(", ") } export function buildKeyTriggersSection(agents: AvailableAgent[], _skills: AvailableSkill[] = []): string { const keyTriggers = agents .filter((a) => a.metadata.keyTrigger) .map((a) => `- ${a.metadata.keyTrigger}`) if (keyTriggers.length === 0) return "" return `### Key Triggers (check BEFORE classification): ${keyTriggers.join("\n")} - **"Look into" + "create PR"** → Not just research. Full implementation cycle expected.` } export function buildToolSelectionTable( agents: AvailableAgent[], tools: AvailableTool[] = [], _skills: AvailableSkill[] = [] ): string { const rows: string[] = [ "### Tool & Agent Selection:", "", ] if (tools.length > 0) { const toolsDisplay = formatToolsForPrompt(tools) rows.push(`- ${toolsDisplay} — **FREE** — Not Complex, Scope Clear, No Implicit Assumptions`) } const costOrder = { FREE: 0, CHEAP: 1, EXPENSIVE: 2 } const sortedAgents = [...agents] .filter((a) => a.metadata.category !== "utility") .sort((a, b) => costOrder[a.metadata.cost] - costOrder[b.metadata.cost]) for (const agent of sortedAgents) { const shortDesc = agent.description.split(".")[0] || agent.description rows.push(`- \`${agent.name}\` agent — **${agent.metadata.cost}** — ${shortDesc}`) } rows.push("") rows.push("**Default flow**: explore/librarian (background) + tools → oracle (if required)") return rows.join("\n") } export function buildExploreSection(agents: AvailableAgent[]): string { const exploreAgent = agents.find((a) => a.name === "explore") if (!exploreAgent) return "" const useWhen = exploreAgent.metadata.useWhen || [] const avoidWhen = exploreAgent.metadata.avoidWhen || [] return `### Explore Agent = Contextual Grep Use it as a **peer tool**, not a fallback. Fire liberally. **Use Direct Tools when:** ${avoidWhen.map((w) => `- ${w}`).join("\n")} **Use Explore Agent when:** ${useWhen.map((w) => `- ${w}`).join("\n")}` } export function buildLibrarianSection(agents: AvailableAgent[]): string { const librarianAgent = agents.find((a) => a.name === "librarian") if (!librarianAgent) return "" const useWhen = librarianAgent.metadata.useWhen || [] return `### Librarian Agent = Reference Grep Search **external references** (docs, OSS, web). Fire proactively when unfamiliar libraries are involved. **Contextual Grep (Internal)** — search OUR codebase, find patterns in THIS repo, project-specific logic. **Reference Grep (External)** — search EXTERNAL resources, official API docs, library best practices, OSS implementation examples. **Trigger phrases** (fire librarian immediately): ${useWhen.map((w) => `- "${w}"`).join("\n")}` } export function buildDelegationTable(agents: AvailableAgent[]): string { const rows: string[] = [ "### Delegation Table:", "", ] for (const agent of agents) { for (const trigger of agent.metadata.triggers) { rows.push(`- **${trigger.domain}** → \`${agent.name}\` — ${trigger.trigger}`) } } return rows.join("\n") } /** * Renders the "User-Installed Skills (HIGH PRIORITY)" block used across multiple agent prompts. * Extracted to avoid duplication between buildCategorySkillsDelegationGuide, buildSkillsSection, etc. */ export function formatCustomSkillsBlock( customRows: string[], customSkills: AvailableSkill[], headerLevel: "####" | "**" = "####" ): string { const header = headerLevel === "####" ? `#### User-Installed Skills (HIGH PRIORITY)` : `**User-Installed Skills (HIGH PRIORITY):**` return `${header} **The user has installed these custom skills. They MUST be evaluated for EVERY delegation.** Subagents are STATELESS — they lose all custom knowledge unless you pass these skills via \`load_skills\`. ${customRows.join("\n")} > **CRITICAL**: Ignoring user-installed skills when they match the task domain is a failure. > The user installed custom skills for a reason — USE THEM when the task overlaps with their domain.` } export function buildCategorySkillsDelegationGuide(categories: AvailableCategory[], skills: AvailableSkill[]): string { if (categories.length === 0 && skills.length === 0) return "" const categoryRows = categories.map((c) => { const desc = c.description || c.name return `- \`${c.name}\` — ${desc}` }) const builtinSkills = skills.filter((s) => s.location === "plugin") const customSkills = skills.filter((s) => s.location !== "plugin") const builtinRows = builtinSkills.map((s) => { const desc = truncateDescription(s.description) return `- \`${s.name}\` — ${desc}` }) const customRows = customSkills.map((s) => { const desc = truncateDescription(s.description) const source = s.location === "project" ? "project" : "user" return `- \`${s.name}\` (${source}) — ${desc}` }) const customSkillBlock = formatCustomSkillsBlock(customRows, customSkills) let skillsSection: string if (customSkills.length > 0 && builtinSkills.length > 0) { skillsSection = `#### Built-in Skills ${builtinRows.join("\n")} ${customSkillBlock}` } else if (customSkills.length > 0) { skillsSection = customSkillBlock } else { skillsSection = `#### Available Skills (Domain Expertise Injection) Skills inject specialized instructions into the subagent. Read the description to understand when each skill applies. ${builtinRows.join("\n")}` } return `### Category + Skills Delegation System **task() combines categories and skills for optimal task execution.** #### Available Categories (Domain-Optimized Models) Each category is configured with a model optimized for that domain. Read the description to understand when to use it. ${categoryRows.join("\n")} ${skillsSection} --- ### MANDATORY: Category + Skill Selection Protocol **STEP 1: Select Category** - Read each category's description - Match task requirements to category domain - Select the category whose domain BEST fits the task **STEP 2: Evaluate ALL Skills (Built-in AND User-Installed)** For EVERY skill listed above, ask yourself: > "Does this skill's expertise domain overlap with my task?" - If YES → INCLUDE in \`load_skills=[...]\` - If NO → You MUST justify why (see below) ${customSkills.length > 0 ? ` > **User-installed skills get PRIORITY.** The user explicitly installed them for their workflow. > When in doubt about a user-installed skill, INCLUDE it rather than omit it.` : ""} **STEP 3: Justify Omissions** If you choose NOT to include a skill that MIGHT be relevant, you MUST provide: \`\`\` SKILL EVALUATION for "[skill-name]": - Skill domain: [what the skill description says] - Task domain: [what your task is about] - Decision: OMIT - Reason: [specific explanation of why domains don't overlap] \`\`\` **WHY JUSTIFICATION IS MANDATORY:** - Forces you to actually READ skill descriptions - Prevents lazy omission of potentially useful skills - Subagents are STATELESS - they only know what you tell them - Missing a relevant skill = suboptimal output --- ### Delegation Pattern \`\`\`typescript task( category="[selected-category]", load_skills=["skill-1", "skill-2"], // Include ALL relevant skills — ESPECIALLY user-installed ones prompt="..." ) \`\`\` **ANTI-PATTERN (will produce poor results):** \`\`\`typescript task(category="...", load_skills=[], run_in_background=false, prompt="...") // Empty load_skills without justification \`\`\`` } export function buildOracleSection(agents: AvailableAgent[]): string { const oracleAgent = agents.find((a) => a.name === "oracle") if (!oracleAgent) return "" const useWhen = oracleAgent.metadata.useWhen || [] const avoidWhen = oracleAgent.metadata.avoidWhen || [] return ` ## Oracle — Read-Only High-IQ Consultant Oracle is a read-only, expensive, high-quality reasoning model for debugging and architecture. Consultation only. ### WHEN to Consult (Oracle FIRST, then implement): ${useWhen.map((w) => `- ${w}`).join("\n")} ### WHEN NOT to Consult: ${avoidWhen.map((w) => `- ${w}`).join("\n")} ### Usage Pattern: Briefly announce "Consulting Oracle for [reason]" before invocation. **Exception**: This is the ONLY case where you announce before acting. For all other work, start immediately without status updates. ### Oracle Background Task Policy: **You MUST collect Oracle results before your final answer. No exceptions.** - Oracle may take several minutes. This is normal and expected. - When Oracle is running and you finish your own exploration/analysis, your next action is \`background_output(task_id="...")\` on Oracle — NOT delivering a final answer. - Oracle catches blind spots you cannot see — its value is HIGHEST when you think you don't need it. - **NEVER** cancel Oracle. **NEVER** use \`background_cancel(all=true)\` when Oracle is running. Cancel disposable tasks (explore, librarian) individually by taskId instead. ` } export function buildHardBlocksSection(): string { const blocks = [ "- Type error suppression (`as any`, `@ts-ignore`) — **Never**", "- Commit without explicit request — **Never**", "- Speculate about unread code — **Never**", "- Leave code in broken state after failures — **Never**", "- `background_cancel(all=true)` when Oracle is running — **Never.** Cancel tasks individually by taskId.", "- Delivering final answer before collecting Oracle result — **Never.** Always `background_output` Oracle first.", ] return `## Hard Blocks (NEVER violate) ${blocks.join("\n")}` } export function buildAntiPatternsSection(): string { const patterns = [ "- **Type Safety**: `as any`, `@ts-ignore`, `@ts-expect-error`", "- **Error Handling**: Empty catch blocks `catch(e) {}`", "- **Testing**: Deleting failing tests to \"pass\"", "- **Search**: Firing agents for single-line typos or obvious syntax errors", "- **Debugging**: Shotgun debugging, random changes", "- **Background Tasks**: `background_cancel(all=true)` — always cancel individually by taskId", "- **Oracle**: Skipping Oracle results when Oracle was launched — ALWAYS collect via `background_output`", ] return `## Anti-Patterns (BLOCKING violations) ${patterns.join("\n")}` } export function buildUltraworkSection( agents: AvailableAgent[], categories: AvailableCategory[], skills: AvailableSkill[] ): string { const lines: string[] = [] if (categories.length > 0) { lines.push("**Categories** (for implementation tasks):") for (const cat of categories) { const shortDesc = cat.description || cat.name lines.push(`- \`${cat.name}\`: ${shortDesc}`) } lines.push("") } if (skills.length > 0) { const builtinSkills = skills.filter((s) => s.location === "plugin") const customSkills = skills.filter((s) => s.location !== "plugin") if (builtinSkills.length > 0) { lines.push("**Built-in Skills** (combine with categories):") for (const skill of builtinSkills) { const shortDesc = skill.description.split(".")[0] || skill.description lines.push(`- \`${skill.name}\`: ${shortDesc}`) } lines.push("") } if (customSkills.length > 0) { lines.push("**User-Installed Skills** (HIGH PRIORITY - user installed these for their workflow):") for (const skill of customSkills) { const shortDesc = skill.description.split(".")[0] || skill.description lines.push(`- \`${skill.name}\`: ${shortDesc}`) } lines.push("") } } if (agents.length > 0) { const ultraworkAgentPriority = ["explore", "librarian", "plan", "oracle"] const sortedAgents = [...agents].sort((a, b) => { const aIdx = ultraworkAgentPriority.indexOf(a.name) const bIdx = ultraworkAgentPriority.indexOf(b.name) if (aIdx === -1 && bIdx === -1) return 0 if (aIdx === -1) return 1 if (bIdx === -1) return -1 return aIdx - bIdx }) lines.push("**Agents** (for specialized consultation/exploration):") for (const agent of sortedAgents) { const shortDesc = agent.description.length > 120 ? agent.description.slice(0, 120) + "..." : agent.description const suffix = agent.name === "explore" || agent.name === "librarian" ? " (multiple)" : "" lines.push(`- \`${agent.name}${suffix}\`: ${shortDesc}`) } } return lines.join("\n") }