Per reviewer feedback (code-yeongyu), keep the 'skill' tool as the main tool and merge slashcommand functionality INTO it, rather than the reverse. Changes: - skill/tools.ts: Add command discovery (discoverCommandsSync) support; handle both SKILL.md skills and .omo/commands/ slash commands in a single tool; show combined listing in tool description - skill/types.ts: Add 'commands' option to SkillLoadOptions - skill/constants.ts: Update description to mention both skills and commands - plugin/tool-registry.ts: Replace createSlashcommandTool with createSkillTool; register tool as 'skill' instead of 'slashcommand' - tools/index.ts: Export createSkillTool instead of createSlashcommandTool - plugin/tool-execute-before.ts: Update tool name checks from 'slashcommand' to 'skill'; update arg name from 'command' to 'name' - agents/dynamic-agent-prompt-builder.ts: Categorize 'skill' tool as 'command' - tools/skill-mcp/tools.ts: Update hint message to reference 'skill' tool - hooks/auto-slash-command/executor.ts: Update error message The slashcommand/ module files are kept (they provide shared utilities used by the skill tool), but the slashcommand tool itself is no longer registered.
422 lines
14 KiB
TypeScript
422 lines
14 KiB
TypeScript
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 customSkillNames = customSkills.map((s) => `"${s.name}"`).join(", ")
|
|
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 ${customSkillNames} 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_Usage>
|
|
## 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.
|
|
</Oracle_Usage>`
|
|
}
|
|
|
|
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")
|
|
}
|