From 3ce1f303101d6b81eb11d8c26068d559a1ea2141 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 3 Apr 2026 19:16:57 +0900 Subject: [PATCH 1/3] refactor(tools): split delegate-task constants into category-specific modules --- .../delegate-task/anthropic-categories.ts | 54 ++++ src/tools/delegate-task/builtin-categories.ts | 33 ++ .../builtin-category-definition.ts | 8 + src/tools/delegate-task/constants.ts | 305 +----------------- src/tools/delegate-task/google-categories.ts | 122 +++++++ src/tools/delegate-task/kimi-categories.ts | 36 +++ src/tools/delegate-task/openai-categories.ts | 116 +++++++ 7 files changed, 374 insertions(+), 300 deletions(-) create mode 100644 src/tools/delegate-task/anthropic-categories.ts create mode 100644 src/tools/delegate-task/builtin-categories.ts create mode 100644 src/tools/delegate-task/builtin-category-definition.ts create mode 100644 src/tools/delegate-task/google-categories.ts create mode 100644 src/tools/delegate-task/kimi-categories.ts create mode 100644 src/tools/delegate-task/openai-categories.ts diff --git a/src/tools/delegate-task/anthropic-categories.ts b/src/tools/delegate-task/anthropic-categories.ts new file mode 100644 index 000000000..e6b0894e3 --- /dev/null +++ b/src/tools/delegate-task/anthropic-categories.ts @@ -0,0 +1,54 @@ +import type { BuiltinCategoryDefinition } from "./builtin-category-definition" + +const UNSPECIFIED_LOW_CATEGORY_PROMPT_APPEND = ` +You are working on tasks that don't fit specific categories but require moderate effort. + + +BEFORE selecting this category, VERIFY ALL conditions: +1. Task does NOT fit: quick (trivial), visual-engineering (UI), ultrabrain (deep logic), artistry (creative), writing (docs) +2. Task requires more than trivial effort but is NOT system-wide +3. Scope is contained within a few files/modules + +If task fits ANY other category, DO NOT select unspecified-low. +This is NOT a default choice - it's for genuinely unclassifiable moderate-effort work. + + + + +THIS CATEGORY USES A MID-TIER MODEL (claude-sonnet-4-6). + +**PROVIDE CLEAR STRUCTURE:** +1. MUST DO: Enumerate required actions explicitly +2. MUST NOT DO: State forbidden actions to prevent scope creep +3. EXPECTED OUTPUT: Define concrete success criteria +` + +const UNSPECIFIED_HIGH_CATEGORY_PROMPT_APPEND = ` +You are working on tasks that don't fit specific categories but require substantial effort. + + +BEFORE selecting this category, VERIFY ALL conditions: +1. Task does NOT fit: quick (trivial), visual-engineering (UI), ultrabrain (deep logic), artistry (creative), writing (docs) +2. Task requires substantial effort across multiple systems/modules +3. Changes have broad impact or require careful coordination +4. NOT just "complex" - must be genuinely unclassifiable AND high-effort + +If task fits ANY other category, DO NOT select unspecified-high. +If task is unclassifiable but moderate-effort, use unspecified-low instead. + +` + +export const ANTHROPIC_CATEGORIES: BuiltinCategoryDefinition[] = [ + { + name: "unspecified-low", + config: { model: "anthropic/claude-sonnet-4-6" }, + description: "Tasks that don't fit other categories, low effort required", + promptAppend: UNSPECIFIED_LOW_CATEGORY_PROMPT_APPEND, + }, + { + name: "unspecified-high", + config: { model: "anthropic/claude-opus-4-6", variant: "max" }, + description: "Tasks that don't fit other categories, high effort required", + promptAppend: UNSPECIFIED_HIGH_CATEGORY_PROMPT_APPEND, + }, +] diff --git a/src/tools/delegate-task/builtin-categories.ts b/src/tools/delegate-task/builtin-categories.ts new file mode 100644 index 000000000..f8da8ecf1 --- /dev/null +++ b/src/tools/delegate-task/builtin-categories.ts @@ -0,0 +1,33 @@ +import type { CategoryConfig } from "../../config/schema" +import { ANTHROPIC_CATEGORIES } from "./anthropic-categories" +import type { BuiltinCategoryDefinition } from "./builtin-category-definition" +import { GOOGLE_CATEGORIES } from "./google-categories" +import { KIMI_CATEGORIES } from "./kimi-categories" +import { OPENAI_CATEGORIES } from "./openai-categories" + +const BUILTIN_CATEGORIES: BuiltinCategoryDefinition[] = [ + ...GOOGLE_CATEGORIES, + ...OPENAI_CATEGORIES, + ...ANTHROPIC_CATEGORIES, + ...KIMI_CATEGORIES, +] + +function buildCategoryRecord( + selector: (definition: BuiltinCategoryDefinition) => TValue +): Record { + return Object.fromEntries( + BUILTIN_CATEGORIES.map((definition) => [definition.name, selector(definition)]) + ) +} + +export const DEFAULT_CATEGORIES: Record = buildCategoryRecord( + (definition) => definition.config +) + +export const CATEGORY_PROMPT_APPENDS: Record = buildCategoryRecord( + (definition) => definition.promptAppend +) + +export const CATEGORY_DESCRIPTIONS: Record = buildCategoryRecord( + (definition) => definition.description +) diff --git a/src/tools/delegate-task/builtin-category-definition.ts b/src/tools/delegate-task/builtin-category-definition.ts new file mode 100644 index 000000000..d9c853b63 --- /dev/null +++ b/src/tools/delegate-task/builtin-category-definition.ts @@ -0,0 +1,8 @@ +import type { CategoryConfig } from "../../config/schema" + +export type BuiltinCategoryDefinition = { + name: string + config: CategoryConfig + description: string + promptAppend: string +} diff --git a/src/tools/delegate-task/constants.ts b/src/tools/delegate-task/constants.ts index c0cc9ca42..510bcf80d 100644 --- a/src/tools/delegate-task/constants.ts +++ b/src/tools/delegate-task/constants.ts @@ -1,308 +1,13 @@ -import type { CategoryConfig } from "../../config/schema" import type { AvailableCategory, AvailableSkill, } from "../../agents/dynamic-agent-prompt-builder" import { truncateDescription } from "../../shared/truncate-description" - -export const VISUAL_CATEGORY_PROMPT_APPEND = ` -You are working on VISUAL/UI tasks. - - -## YOU ARE A VISUAL ENGINEER. FOLLOW THIS WORKFLOW OR YOUR OUTPUT IS REJECTED. - -**YOUR FAILURE MODE**: You skip design system analysis and jump straight to writing components with hardcoded colors, arbitrary spacing, and ad-hoc font sizes. The result is INCONSISTENT GARBAGE that looks like 5 different people built it. THIS STOPS NOW. - -**EVERY visual task follows this EXACT workflow. VIOLATION = BROKEN OUTPUT.** - -### PHASE 1: ANALYZE THE DESIGN SYSTEM (MANDATORY FIRST ACTION) - -**BEFORE writing a SINGLE line of CSS, HTML, JSX, Svelte, or component code — you MUST:** - -1. **SEARCH for the design system.** Use Grep, Glob, Read — actually LOOK: - - Design tokens: colors, spacing, typography, shadows, border-radii - - Theme files: CSS variables, Tailwind config, \`theme.ts\`, styled-components theme, design tokens file - - Shared/base components: Button, Card, Input, Layout primitives - - Existing UI patterns: How are pages structured? What spacing grid? What color usage? - -2. **READ at minimum 5-10 existing UI components.** Understand: - - Naming conventions (BEM? Atomic? Utility-first? Component-scoped?) - - Spacing system (4px grid? 8px? Tailwind scale? CSS variables?) - - Color usage (semantic tokens? Direct hex? Theme references?) - - Typography scale (heading levels, body, caption — how many? What font stack?) - - Component composition patterns (slots? children? compound components?) - -**DO NOT proceed to Phase 2 until you can answer ALL of these. If you cannot, you have not explored enough. EXPLORE MORE.** - -### PHASE 2: NO DESIGN SYSTEM? BUILD ONE. NOW. - -If Phase 1 reveals NO coherent design system (or scattered, inconsistent patterns): - -1. **STOP. Do NOT build the requested UI yet.** -2. **Extract what exists** — even inconsistent patterns have salvageable decisions. -3. **Create a minimal design system FIRST:** - - Color palette: primary, secondary, neutral, semantic (success/warning/error/info) - - Typography scale: heading levels (h1-h4 minimum), body, small, caption - - Spacing scale: consistent increments (4px or 8px base) - - Border radii, shadows, transitions — systematic, not random - - Component primitives: the reusable building blocks -4. **Commit/save the design system, THEN proceed to Phase 3.** - -A design system is NOT optional overhead. It is the FOUNDATION. Building UI without one is like building a house on sand. It WILL collapse into inconsistency. - -### PHASE 3: BUILD WITH THE SYSTEM. NEVER AROUND IT. - -**NOW and ONLY NOW** — implement the requested visual work: - -| Element | CORRECT | WRONG (WILL BE REJECTED) | -|---------|---------|--------------------------| -| Color | Design token / CSS variable | Hardcoded \`#3b82f6\`, \`rgb(59,130,246)\` | -| Spacing | System value (\`space-4\`, \`gap-md\`, \`var(--spacing-4)\`) | Arbitrary \`margin: 13px\`, \`padding: 7px\` | -| Typography | Scale value (\`text-lg\`, \`heading-2\`, token) | Ad-hoc \`font-size: 17px\` | -| Component | Extend/compose from existing primitives | One-off div soup with inline styles | -| Border radius | System token | Random \`border-radius: 6px\` | - -**IF the design requires something OUTSIDE the current system:** -- **Extend the system FIRST** — add the new token/primitive -- **THEN use the new token** in your component -- **NEVER one-off override.** That is how design systems die. - -### PHASE 4: VERIFY BEFORE CLAIMING DONE - -BEFORE reporting visual work as complete, answer these: - -- [ ] Does EVERY color reference a design token or CSS variable? -- [ ] Does EVERY spacing use the system scale? -- [ ] Does EVERY component follow the existing composition pattern? -- [ ] Would a designer see CONSISTENCY across old and new components? -- [ ] Are there ZERO hardcoded magic numbers for visual properties? - -**If ANY answer is NO — FIX IT. You are NOT done.** - - - - -Design-first mindset (AFTER design system is established): -- Bold aesthetic choices over safe defaults -- Unexpected layouts, asymmetry, grid-breaking elements -- Distinctive typography (avoid: Arial, Inter, Roboto, Space Grotesk) -- Cohesive color palettes with sharp accents -- High-impact animations with staggered reveals -- Atmosphere: gradient meshes, noise textures, layered transparencies - -AVOID: Generic fonts, purple gradients on white, predictable layouts, cookie-cutter patterns. - -` - -export const ULTRABRAIN_CATEGORY_PROMPT_APPEND = ` -You are working on DEEP LOGICAL REASONING / COMPLEX ARCHITECTURE tasks. - -**CRITICAL - CODE STYLE REQUIREMENTS (NON-NEGOTIABLE)**: -1. BEFORE writing ANY code, SEARCH the existing codebase to find similar patterns/styles -2. Your code MUST match the project's existing conventions - blend in seamlessly -3. Write READABLE code that humans can easily understand - no clever tricks -4. If unsure about style, explore more files until you find the pattern - -Strategic advisor mindset: -- Bias toward simplicity: least complex solution that fulfills requirements -- Leverage existing code/patterns over new components -- Prioritize developer experience and maintainability -- One clear recommendation with effort estimate (Quick/Short/Medium/Large) -- Signal when advanced approach warranted - -Response format: -- Bottom line (2-3 sentences) -- Action plan (numbered steps) -- Risks and mitigations (if relevant) -` - -export const ARTISTRY_CATEGORY_PROMPT_APPEND = ` -You are working on HIGHLY CREATIVE / ARTISTIC tasks. - -Artistic genius mindset: -- Push far beyond conventional boundaries -- Explore radical, unconventional directions -- Surprise and delight: unexpected twists, novel combinations -- Rich detail and vivid expression -- Break patterns deliberately when it serves the creative vision - -Approach: -- Generate diverse, bold options first -- Embrace ambiguity and wild experimentation -- Balance novelty with coherence -- This is for tasks requiring exceptional creativity -` - -export const QUICK_CATEGORY_PROMPT_APPEND = ` -You are working on SMALL / QUICK tasks. - -Efficient execution mindset: -- Fast, focused, minimal overhead -- Get to the point immediately -- No over-engineering -- Simple solutions for simple problems - -Approach: -- Minimal viable implementation -- Skip unnecessary abstractions -- Direct and concise - - - -THIS CATEGORY USES A SMALLER/FASTER MODEL (gpt-5.4-mini). - -The model executing this task is optimized for speed over depth. Your prompt MUST be: - -**EXHAUSTIVELY EXPLICIT** - Leave NOTHING to interpretation: -1. MUST DO: List every required action as atomic, numbered steps -2. MUST NOT DO: Explicitly forbid likely mistakes and deviations -3. EXPECTED OUTPUT: Describe exact success criteria with concrete examples - -**WHY THIS MATTERS:** -- Smaller models benefit from explicit guardrails -- Vague instructions may lead to unpredictable results -- Implicit expectations may be missed -**PROMPT STRUCTURE (MANDATORY):** -\`\`\` -TASK: [One-sentence goal] - -MUST DO: -1. [Specific action with exact details] -2. [Another specific action] -... - -MUST NOT DO: -- [Forbidden action + why] -- [Another forbidden action] -... - -EXPECTED OUTPUT: -- [Exact deliverable description] -- [Success criteria / verification method] -\`\`\` - -If your prompt lacks this structure, REWRITE IT before delegating. -` - -export const UNSPECIFIED_LOW_CATEGORY_PROMPT_APPEND = ` -You are working on tasks that don't fit specific categories but require moderate effort. - - -BEFORE selecting this category, VERIFY ALL conditions: -1. Task does NOT fit: quick (trivial), visual-engineering (UI), ultrabrain (deep logic), artistry (creative), writing (docs) -2. Task requires more than trivial effort but is NOT system-wide -3. Scope is contained within a few files/modules - -If task fits ANY other category, DO NOT select unspecified-low. -This is NOT a default choice - it's for genuinely unclassifiable moderate-effort work. - - - - -THIS CATEGORY USES A MID-TIER MODEL (claude-sonnet-4-6). - -**PROVIDE CLEAR STRUCTURE:** -1. MUST DO: Enumerate required actions explicitly -2. MUST NOT DO: State forbidden actions to prevent scope creep -3. EXPECTED OUTPUT: Define concrete success criteria -` - -export const UNSPECIFIED_HIGH_CATEGORY_PROMPT_APPEND = ` -You are working on tasks that don't fit specific categories but require substantial effort. - - -BEFORE selecting this category, VERIFY ALL conditions: -1. Task does NOT fit: quick (trivial), visual-engineering (UI), ultrabrain (deep logic), artistry (creative), writing (docs) -2. Task requires substantial effort across multiple systems/modules -3. Changes have broad impact or require careful coordination -4. NOT just "complex" - must be genuinely unclassifiable AND high-effort - -If task fits ANY other category, DO NOT select unspecified-high. -If task is unclassifiable but moderate-effort, use unspecified-low instead. - -` - -export const WRITING_CATEGORY_PROMPT_APPEND = ` -You are working on WRITING / PROSE tasks. - -Wordsmith mindset: -- Clear, flowing prose -- Appropriate tone and voice -- Engaging and readable -- Proper structure and organization - -Approach: -- Understand the audience -- Draft with care -- Polish for clarity and impact -- Documentation, READMEs, articles, technical writing - -ANTI-AI-SLOP RULES (NON-NEGOTIABLE): -- NEVER use em dashes (—) or en dashes (–). Use commas, periods, ellipses, or line breaks instead. Zero tolerance. -- Remove AI-sounding phrases: "delve", "it's important to note", "I'd be happy to", "certainly", "please don't hesitate", "leverage", "utilize", "in order to", "moving forward", "circle back", "at the end of the day", "robust", "streamline", "facilitate" -- Pick plain words. "Use" not "utilize". "Start" not "commence". "Help" not "facilitate". -- Use contractions naturally: "don't" not "do not", "it's" not "it is". -- Vary sentence length. Don't make every sentence the same length. -- NEVER start consecutive sentences with the same word. -- No filler openings: skip "In today's world...", "As we all know...", "It goes without saying..." -- Write like a human, not a corporate template. -` - -export const DEEP_CATEGORY_PROMPT_APPEND = ` -You are working on GOAL-ORIENTED AUTONOMOUS tasks. - -You are NOT an interactive assistant. You are an autonomous problem-solver. - -BEFORE making ANY changes: -1. Silently explore the codebase extensively (5-15 minutes of reading is normal) -2. Read related files, trace dependencies, understand the full context -3. Build a complete mental model of the problem space -4. Do not ask clarifying questions - the goal is already defined - -You receive a GOAL. When the goal includes numbered steps or phases, treat them as one atomic task broken into sub-steps, not as separate independent tasks. Figure out HOW to achieve it yourself. Thorough research before any action. - -Sub-steps of ONE goal = execute all steps as phases of one atomic task. -Genuinely independent tasks = flag and refuse, require separate delegations. - -Approach: explore extensively, understand deeply, then act decisively. Prefer comprehensive solutions over quick patches. If the goal is unclear, make reasonable assumptions and proceed. - -Minimal status updates. Focus on results, not play-by-play. Report completion with summary of changes. -` - - - -export const DEFAULT_CATEGORIES: Record = { - "visual-engineering": { model: "google/gemini-3.1-pro", variant: "high" }, - ultrabrain: { model: "openai/gpt-5.4", variant: "xhigh" }, - deep: { model: "openai/gpt-5.4", variant: "medium" }, - artistry: { model: "google/gemini-3.1-pro", variant: "high" }, - quick: { model: "openai/gpt-5.4-mini" }, - "unspecified-low": { model: "anthropic/claude-sonnet-4-6" }, - "unspecified-high": { model: "anthropic/claude-opus-4-6", variant: "max" }, - writing: { model: "kimi-for-coding/k2p5" }, -} - -export const CATEGORY_PROMPT_APPENDS: Record = { - "visual-engineering": VISUAL_CATEGORY_PROMPT_APPEND, - ultrabrain: ULTRABRAIN_CATEGORY_PROMPT_APPEND, - deep: DEEP_CATEGORY_PROMPT_APPEND, - artistry: ARTISTRY_CATEGORY_PROMPT_APPEND, - quick: QUICK_CATEGORY_PROMPT_APPEND, - "unspecified-low": UNSPECIFIED_LOW_CATEGORY_PROMPT_APPEND, - "unspecified-high": UNSPECIFIED_HIGH_CATEGORY_PROMPT_APPEND, - writing: WRITING_CATEGORY_PROMPT_APPEND, -} - -export const CATEGORY_DESCRIPTIONS: Record = { - "visual-engineering": "Frontend, UI/UX, design, styling, animation", - ultrabrain: "Use ONLY for genuinely hard, logic-heavy tasks. Give clear goals only, not step-by-step instructions.", - deep: "Goal-oriented autonomous problem-solving. Thorough research before action. For hairy problems requiring deep understanding.", - artistry: "Complex problem-solving with unconventional, creative approaches - beyond standard patterns", - quick: "Trivial tasks - single file changes, typo fixes, simple modifications", - "unspecified-low": "Tasks that don't fit other categories, low effort required", - "unspecified-high": "Tasks that don't fit other categories, high effort required", - writing: "Documentation, prose, technical writing", -} +export { + CATEGORY_DESCRIPTIONS, + CATEGORY_PROMPT_APPENDS, + DEFAULT_CATEGORIES, +} from "./builtin-categories" /** * System prompt prepended to plan agent invocations. diff --git a/src/tools/delegate-task/google-categories.ts b/src/tools/delegate-task/google-categories.ts new file mode 100644 index 000000000..53f6cb8ad --- /dev/null +++ b/src/tools/delegate-task/google-categories.ts @@ -0,0 +1,122 @@ +import type { BuiltinCategoryDefinition } from "./builtin-category-definition" + +const VISUAL_CATEGORY_PROMPT_APPEND = ` +You are working on VISUAL/UI tasks. + + +## YOU ARE A VISUAL ENGINEER. FOLLOW THIS WORKFLOW OR YOUR OUTPUT IS REJECTED. + +**YOUR FAILURE MODE**: You skip design system analysis and jump straight to writing components with hardcoded colors, arbitrary spacing, and ad-hoc font sizes. The result is INCONSISTENT GARBAGE that looks like 5 different people built it. THIS STOPS NOW. + +**EVERY visual task follows this EXACT workflow. VIOLATION = BROKEN OUTPUT.** + +### PHASE 1: ANALYZE THE DESIGN SYSTEM (MANDATORY FIRST ACTION) + +**BEFORE writing a SINGLE line of CSS, HTML, JSX, Svelte, or component code — you MUST:** + +1. **SEARCH for the design system.** Use Grep, Glob, Read — actually LOOK: + - Design tokens: colors, spacing, typography, shadows, border-radii + - Theme files: CSS variables, Tailwind config, \`theme.ts\`, styled-components theme, design tokens file + - Shared/base components: Button, Card, Input, Layout primitives + - Existing UI patterns: How are pages structured? What spacing grid? What color usage? + +2. **READ at minimum 5-10 existing UI components.** Understand: + - Naming conventions (BEM? Atomic? Utility-first? Component-scoped?) + - Spacing system (4px grid? 8px? Tailwind scale? CSS variables?) + - Color usage (semantic tokens? Direct hex? Theme references?) + - Typography scale (heading levels, body, caption — how many? What font stack?) + - Component composition patterns (slots? children? compound components?) + +**DO NOT proceed to Phase 2 until you can answer ALL of these. If you cannot, you have not explored enough. EXPLORE MORE.** + +### PHASE 2: NO DESIGN SYSTEM? BUILD ONE. NOW. + +If Phase 1 reveals NO coherent design system (or scattered, inconsistent patterns): + +1. **STOP. Do NOT build the requested UI yet.** +2. **Extract what exists** — even inconsistent patterns have salvageable decisions. +3. **Create a minimal design system FIRST:** + - Color palette: primary, secondary, neutral, semantic (success/warning/error/info) + - Typography scale: heading levels (h1-h4 minimum), body, small, caption + - Spacing scale: consistent increments (4px or 8px base) + - Border radii, shadows, transitions — systematic, not random + - Component primitives: the reusable building blocks +4. **Commit/save the design system, THEN proceed to Phase 3.** + +A design system is NOT optional overhead. It is the FOUNDATION. Building UI without one is like building a house on sand. It WILL collapse into inconsistency. + +### PHASE 3: BUILD WITH THE SYSTEM. NEVER AROUND IT. + +**NOW and ONLY NOW** — implement the requested visual work: + +| Element | CORRECT | WRONG (WILL BE REJECTED) | +|---------|---------|--------------------------| +| Color | Design token / CSS variable | Hardcoded \`#3b82f6\`, \`rgb(59,130,246)\` | +| Spacing | System value (\`space-4\`, \`gap-md\`, \`var(--spacing-4)\`) | Arbitrary \`margin: 13px\`, \`padding: 7px\` | +| Typography | Scale value (\`text-lg\`, \`heading-2\`, token) | Ad-hoc \`font-size: 17px\` | +| Component | Extend/compose from existing primitives | One-off div soup with inline styles | +| Border radius | System token | Random \`border-radius: 6px\` | + +**IF the design requires something OUTSIDE the current system:** +- **Extend the system FIRST** — add the new token/primitive +- **THEN use the new token** in your component +- **NEVER one-off override.** That is how design systems die. + +### PHASE 4: VERIFY BEFORE CLAIMING DONE + +BEFORE reporting visual work as complete, answer these: + +- [ ] Does EVERY color reference a design token or CSS variable? +- [ ] Does EVERY spacing use the system scale? +- [ ] Does EVERY component follow the existing composition pattern? +- [ ] Would a designer see CONSISTENCY across old and new components? +- [ ] Are there ZERO hardcoded magic numbers for visual properties? + +**If ANY answer is NO — FIX IT. You are NOT done.** + + + + +Design-first mindset (AFTER design system is established): +- Bold aesthetic choices over safe defaults +- Unexpected layouts, asymmetry, grid-breaking elements +- Distinctive typography (avoid: Arial, Inter, Roboto, Space Grotesk) +- Cohesive color palettes with sharp accents +- High-impact animations with staggered reveals +- Atmosphere: gradient meshes, noise textures, layered transparencies + +AVOID: Generic fonts, purple gradients on white, predictable layouts, cookie-cutter patterns. + +` + +const ARTISTRY_CATEGORY_PROMPT_APPEND = ` +You are working on HIGHLY CREATIVE / ARTISTIC tasks. + +Artistic genius mindset: +- Push far beyond conventional boundaries +- Explore radical, unconventional directions +- Surprise and delight: unexpected twists, novel combinations +- Rich detail and vivid expression +- Break patterns deliberately when it serves the creative vision + +Approach: +- Generate diverse, bold options first +- Embrace ambiguity and wild experimentation +- Balance novelty with coherence +- This is for tasks requiring exceptional creativity +` + +export const GOOGLE_CATEGORIES: BuiltinCategoryDefinition[] = [ + { + name: "visual-engineering", + config: { model: "google/gemini-3.1-pro", variant: "high" }, + description: "Frontend, UI/UX, design, styling, animation", + promptAppend: VISUAL_CATEGORY_PROMPT_APPEND, + }, + { + name: "artistry", + config: { model: "google/gemini-3.1-pro", variant: "high" }, + description: "Complex problem-solving with unconventional, creative approaches - beyond standard patterns", + promptAppend: ARTISTRY_CATEGORY_PROMPT_APPEND, + }, +] diff --git a/src/tools/delegate-task/kimi-categories.ts b/src/tools/delegate-task/kimi-categories.ts new file mode 100644 index 000000000..041a4ae54 --- /dev/null +++ b/src/tools/delegate-task/kimi-categories.ts @@ -0,0 +1,36 @@ +import type { BuiltinCategoryDefinition } from "./builtin-category-definition" + +const WRITING_CATEGORY_PROMPT_APPEND = ` +You are working on WRITING / PROSE tasks. + +Wordsmith mindset: +- Clear, flowing prose +- Appropriate tone and voice +- Engaging and readable +- Proper structure and organization + +Approach: +- Understand the audience +- Draft with care +- Polish for clarity and impact +- Documentation, READMEs, articles, technical writing + +ANTI-AI-SLOP RULES (NON-NEGOTIABLE): +- NEVER use em dashes (—) or en dashes (–). Use commas, periods, ellipses, or line breaks instead. Zero tolerance. +- Remove AI-sounding phrases: "delve", "it's important to note", "I'd be happy to", "certainly", "please don't hesitate", "leverage", "utilize", "in order to", "moving forward", "circle back", "at the end of the day", "robust", "streamline", "facilitate" +- Pick plain words. "Use" not "utilize". "Start" not "commence". "Help" not "facilitate". +- Use contractions naturally: "don't" not "do not", "it's" not "it is". +- Vary sentence length. Don't make every sentence the same length. +- NEVER start consecutive sentences with the same word. +- No filler openings: skip "In today's world...", "As we all know...", "It goes without saying..." +- Write like a human, not a corporate template. +` + +export const KIMI_CATEGORIES: BuiltinCategoryDefinition[] = [ + { + name: "writing", + config: { model: "kimi-for-coding/k2p5" }, + description: "Documentation, prose, technical writing", + promptAppend: WRITING_CATEGORY_PROMPT_APPEND, + }, +] diff --git a/src/tools/delegate-task/openai-categories.ts b/src/tools/delegate-task/openai-categories.ts new file mode 100644 index 000000000..028ade55e --- /dev/null +++ b/src/tools/delegate-task/openai-categories.ts @@ -0,0 +1,116 @@ +import type { BuiltinCategoryDefinition } from "./builtin-category-definition" + +const ULTRABRAIN_CATEGORY_PROMPT_APPEND = ` +You are working on DEEP LOGICAL REASONING / COMPLEX ARCHITECTURE tasks. + +**CRITICAL - CODE STYLE REQUIREMENTS (NON-NEGOTIABLE)**: +1. BEFORE writing ANY code, SEARCH the existing codebase to find similar patterns/styles +2. Your code MUST match the project's existing conventions - blend in seamlessly +3. Write READABLE code that humans can easily understand - no clever tricks +4. If unsure about style, explore more files until you find the pattern + +Strategic advisor mindset: +- Bias toward simplicity: least complex solution that fulfills requirements +- Leverage existing code/patterns over new components +- Prioritize developer experience and maintainability +- One clear recommendation with effort estimate (Quick/Short/Medium/Large) +- Signal when advanced approach warranted + +Response format: +- Bottom line (2-3 sentences) +- Action plan (numbered steps) +- Risks and mitigations (if relevant) +` + +const DEEP_CATEGORY_PROMPT_APPEND = ` +You are working on GOAL-ORIENTED AUTONOMOUS tasks. + +You are NOT an interactive assistant. You are an autonomous problem-solver. + +BEFORE making ANY changes: +1. Silently explore the codebase extensively (5-15 minutes of reading is normal) +2. Read related files, trace dependencies, understand the full context +3. Build a complete mental model of the problem space +4. Do not ask clarifying questions - the goal is already defined + +You receive a GOAL. When the goal includes numbered steps or phases, treat them as one atomic task broken into sub-steps, not as separate independent tasks. Figure out HOW to achieve it yourself. Thorough research before any action. + +Sub-steps of ONE goal = execute all steps as phases of one atomic task. +Genuinely independent tasks = flag and refuse, require separate delegations. + +Approach: explore extensively, understand deeply, then act decisively. Prefer comprehensive solutions over quick patches. If the goal is unclear, make reasonable assumptions and proceed. + +Minimal status updates. Focus on results, not play-by-play. Report completion with summary of changes. +` + +const QUICK_CATEGORY_PROMPT_APPEND = ` +You are working on SMALL / QUICK tasks. + +Efficient execution mindset: +- Fast, focused, minimal overhead +- Get to the point immediately +- No over-engineering +- Simple solutions for simple problems + +Approach: +- Minimal viable implementation +- Skip unnecessary abstractions +- Direct and concise + + + +THIS CATEGORY USES A SMALLER/FASTER MODEL (gpt-5.4-mini). + +The model executing this task is optimized for speed over depth. Your prompt MUST be: + +**EXHAUSTIVELY EXPLICIT** - Leave NOTHING to interpretation: +1. MUST DO: List every required action as atomic, numbered steps +2. MUST NOT DO: Explicitly forbid likely mistakes and deviations +3. EXPECTED OUTPUT: Describe exact success criteria with concrete examples + +**WHY THIS MATTERS:** +- Smaller models benefit from explicit guardrails +- Vague instructions may lead to unpredictable results +- Implicit expectations may be missed +**PROMPT STRUCTURE (MANDATORY):** +\`\`\` +TASK: [One-sentence goal] + +MUST DO: +1. [Specific action with exact details] +2. [Another specific action] +... + +MUST NOT DO: +- [Forbidden action + why] +- [Another forbidden action] +... + +EXPECTED OUTPUT: +- [Exact deliverable description] +- [Success criteria / verification method] +\`\`\` + +If your prompt lacks this structure, REWRITE IT before delegating. +` + +export const OPENAI_CATEGORIES: BuiltinCategoryDefinition[] = [ + { + name: "ultrabrain", + config: { model: "openai/gpt-5.4", variant: "xhigh" }, + description: "Use ONLY for genuinely hard, logic-heavy tasks. Give clear goals only, not step-by-step instructions.", + promptAppend: ULTRABRAIN_CATEGORY_PROMPT_APPEND, + }, + { + name: "deep", + config: { model: "openai/gpt-5.4", variant: "medium" }, + description: "Goal-oriented autonomous problem-solving. Thorough research before action. For hairy problems requiring deep understanding.", + promptAppend: DEEP_CATEGORY_PROMPT_APPEND, + }, + { + name: "quick", + config: { model: "openai/gpt-5.4-mini" }, + description: "Trivial tasks - single file changes, typo fixes, simple modifications", + promptAppend: QUICK_CATEGORY_PROMPT_APPEND, + }, +] From 3689ecd5b0660d12a141048e982ada209074e661 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 3 Apr 2026 19:17:03 +0900 Subject: [PATCH 2/3] refactor(tools): decompose skill/tools.ts into focused tool creators --- src/tools/skill/description-formatter.ts | 61 ++++ src/tools/skill/mcp-capability-formatter.ts | 96 +++++++ src/tools/skill/native-skills.ts | 62 ++++ src/tools/skill/scope-priority.ts | 17 ++ src/tools/skill/skill-body.ts | 26 ++ src/tools/skill/skill-matcher.ts | 40 +++ src/tools/skill/tools.ts | 300 +++----------------- 7 files changed, 336 insertions(+), 266 deletions(-) create mode 100644 src/tools/skill/description-formatter.ts create mode 100644 src/tools/skill/mcp-capability-formatter.ts create mode 100644 src/tools/skill/native-skills.ts create mode 100644 src/tools/skill/scope-priority.ts create mode 100644 src/tools/skill/skill-body.ts create mode 100644 src/tools/skill/skill-matcher.ts diff --git a/src/tools/skill/description-formatter.ts b/src/tools/skill/description-formatter.ts new file mode 100644 index 000000000..c51c10a23 --- /dev/null +++ b/src/tools/skill/description-formatter.ts @@ -0,0 +1,61 @@ +import { TOOL_DESCRIPTION_NO_SKILLS, TOOL_DESCRIPTION_PREFIX } from "./constants" +import { sortByScopePriority } from "./scope-priority" +import type { SkillInfo } from "./types" +import type { CommandInfo } from "../slashcommand/types" + +function formatSkillCommand(skill: SkillInfo): string { + const lines = [ + " ", + ` /${skill.name}`, + ` ${skill.description}`, + ` ${skill.scope}`, + ] + + if (skill.compatibility) { + lines.push(` ${skill.compatibility}`) + } + + lines.push(" ") + return lines.join("\n") +} + +function formatSlashCommand(command: CommandInfo): string { + const argumentHint = typeof command.metadata.argumentHint === "string" + ? command.metadata.argumentHint.trim() + : undefined + const lines = [ + " ", + ` /${command.name}`, + ` ${command.metadata.description || "(no description)"}`, + ` ${command.scope}`, + ] + + if (argumentHint) { + lines.push(` ${argumentHint}`) + } + + lines.push(" ") + return lines.join("\n") +} + +export function formatCombinedDescription(skills: SkillInfo[], commands: CommandInfo[]): string { + if (skills.length === 0 && commands.length === 0) { + return TOOL_DESCRIPTION_NO_SKILLS + } + + const availableItems = [ + ...sortByScopePriority(skills).map(formatSkillCommand), + ...sortByScopePriority(commands).map(formatSlashCommand), + ] + + if (availableItems.length === 0) { + return TOOL_DESCRIPTION_PREFIX + } + + return `${TOOL_DESCRIPTION_PREFIX} + +Priority: project > user > opencode > builtin/plugin | Skills listed before commands +Invoke via: skill(name="item-name") — omit leading slash for commands. +${availableItems.join("\n")} +` +} diff --git a/src/tools/skill/mcp-capability-formatter.ts b/src/tools/skill/mcp-capability-formatter.ts new file mode 100644 index 000000000..a7371480f --- /dev/null +++ b/src/tools/skill/mcp-capability-formatter.ts @@ -0,0 +1,96 @@ +import type { Prompt, Resource, Tool } from "@modelcontextprotocol/sdk/types.js" +import { sanitizeJsonSchema } from "../../plugin/normalize-tool-arg-schemas" +import type { + SkillMcpClientInfo, + SkillMcpManager, + SkillMcpServerContext, +} from "../../features/skill-mcp-manager" +import type { LoadedSkill } from "../../features/opencode-skill-loader" + +export async function formatMcpCapabilities( + skill: LoadedSkill, + manager: SkillMcpManager, + sessionID: string +): Promise { + if (!skill.mcpConfig || Object.keys(skill.mcpConfig).length === 0) { + return null + } + + const sections: string[] = ["", "## Available MCP Servers", ""] + + for (const [serverName, config] of Object.entries(skill.mcpConfig)) { + const info: SkillMcpClientInfo = { + serverName, + skillName: skill.name, + sessionID, + } + const context: SkillMcpServerContext = { + config, + skillName: skill.name, + } + + sections.push(`### ${serverName}`, "") + + try { + const [tools, resources, prompts] = await Promise.all([ + manager.listTools(info, context).catch(() => []), + manager.listResources(info, context).catch(() => []), + manager.listPrompts(info, context).catch(() => []), + ]) + + appendToolSections(sections, tools as Tool[]) + appendResourceSection(sections, resources as Resource[]) + appendPromptSection(sections, prompts as Prompt[]) + + if (tools.length === 0 && resources.length === 0 && prompts.length === 0) { + sections.push("*No capabilities discovered*") + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + sections.push(`*Failed to connect: ${errorMessage.split("\n")[0]}*`) + } + + sections.push("", `Use \`skill_mcp\` tool with \`mcp_name=\"${serverName}\"\` to invoke.`, "") + } + + return sections.join("\n") +} + +function appendToolSections(sections: string[], tools: Tool[]): void { + if (tools.length === 0) { + return + } + + sections.push("**Tools:**", "") + + for (const toolDefinition of tools) { + sections.push(`#### \`${toolDefinition.name}\``) + if (toolDefinition.description) { + sections.push(toolDefinition.description) + } + sections.push( + "", + "**inputSchema:**", + "```json", + JSON.stringify(sanitizeJsonSchema(toolDefinition.inputSchema), null, 2), + "```", + "" + ) + } +} + +function appendResourceSection(sections: string[], resources: Resource[]): void { + if (resources.length === 0) { + return + } + + sections.push(`**Resources**: ${resources.map((resource) => resource.uri).join(", ")}`) +} + +function appendPromptSection(sections: string[], prompts: Prompt[]): void { + if (prompts.length === 0) { + return + } + + sections.push(`**Prompts**: ${prompts.map((prompt) => prompt.name).join(", ")}`) +} diff --git a/src/tools/skill/native-skills.ts b/src/tools/skill/native-skills.ts new file mode 100644 index 000000000..67bc463b3 --- /dev/null +++ b/src/tools/skill/native-skills.ts @@ -0,0 +1,62 @@ +import type { SkillInfo } from "./types" +import type { LoadedSkill } from "../../features/opencode-skill-loader" + +export type NativeSkillEntry = { + name: string + description: string + location: string + content: string +} + +export function loadedSkillToInfo(skill: LoadedSkill): SkillInfo { + return { + name: skill.name, + description: skill.definition.description || "", + location: skill.path, + scope: skill.scope, + license: skill.license, + compatibility: skill.compatibility, + metadata: skill.metadata, + allowedTools: skill.allowedTools, + } +} + +function nativeSkillToLoadedSkill(native: NativeSkillEntry): LoadedSkill { + return { + name: native.name, + path: native.location, + definition: { + name: native.name, + description: native.description, + template: native.content, + }, + scope: "config", + } +} + +export function mergeNativeSkills(skills: LoadedSkill[], nativeSkills: NativeSkillEntry[]): void { + const knownNames = new Set(skills.map((skill) => skill.name)) + for (const native of nativeSkills) { + if (knownNames.has(native.name)) continue + skills.push(nativeSkillToLoadedSkill(native)) + knownNames.add(native.name) + } +} + +export function mergeNativeSkillInfos(skillInfos: SkillInfo[], nativeSkills: NativeSkillEntry[]): void { + const knownNames = new Set(skillInfos.map((skill) => skill.name)) + for (const native of nativeSkills) { + if (knownNames.has(native.name)) continue + skillInfos.push({ + name: native.name, + description: native.description, + location: native.location, + scope: "config", + }) + knownNames.add(native.name) + } +} + +export function isPromiseLike(value: TValue | Promise): value is Promise { + return typeof value === "object" && value !== null && "then" in value +} diff --git a/src/tools/skill/scope-priority.ts b/src/tools/skill/scope-priority.ts new file mode 100644 index 000000000..29364d24d --- /dev/null +++ b/src/tools/skill/scope-priority.ts @@ -0,0 +1,17 @@ +export const SCOPE_PRIORITY: Record = { + project: 4, + user: 3, + opencode: 2, + "opencode-project": 2, + plugin: 1, + config: 1, + builtin: 1, +} + +export function sortByScopePriority(items: TItem[]): TItem[] { + return [...items].sort((left, right) => { + const leftPriority = SCOPE_PRIORITY[left.scope] || 0 + const rightPriority = SCOPE_PRIORITY[right.scope] || 0 + return rightPriority - leftPriority + }) +} diff --git a/src/tools/skill/skill-body.ts b/src/tools/skill/skill-body.ts new file mode 100644 index 000000000..fa05f6c8e --- /dev/null +++ b/src/tools/skill/skill-body.ts @@ -0,0 +1,26 @@ +import type { LoadedSkill } from "../../features/opencode-skill-loader" +import { extractSkillTemplate } from "../../features/opencode-skill-loader/skill-content" + +const SKILL_INSTRUCTION_PATTERN = /([\s\S]*?)<\/skill-instruction>/ + +function trimSkillInstruction(template: string): string { + const templateMatch = template.match(SKILL_INSTRUCTION_PATTERN) + return templateMatch ? templateMatch[1].trim() : template +} + +export async function extractSkillBody(skill: LoadedSkill): Promise { + if (skill.lazyContent) { + const fullTemplate = await skill.lazyContent.load() + return trimSkillInstruction(fullTemplate) + } + + if (skill.scope === "config" && skill.definition.template) { + return trimSkillInstruction(skill.definition.template) + } + + if (skill.path) { + return extractSkillTemplate(skill) + } + + return trimSkillInstruction(skill.definition.template || "") +} diff --git a/src/tools/skill/skill-matcher.ts b/src/tools/skill/skill-matcher.ts new file mode 100644 index 000000000..9634d3c3b --- /dev/null +++ b/src/tools/skill/skill-matcher.ts @@ -0,0 +1,40 @@ +import { sortByScopePriority } from "./scope-priority" +import type { CommandInfo } from "../slashcommand/types" +import type { LoadedSkill } from "../../features/opencode-skill-loader" + +export function matchSkillByName(skills: LoadedSkill[], requestedName: string): LoadedSkill | undefined { + const normalizedName = requestedName.toLowerCase() + const exactMatch = skills.find((skill) => skill.name.toLowerCase() === normalizedName) + if (exactMatch) { + return exactMatch + } + + const shortNameMatches = skills.filter((skill) => { + const parts = skill.name.split("/") + const shortName = parts[parts.length - 1] + return parts.length > 1 && shortName?.toLowerCase() === normalizedName + }) + + if (shortNameMatches.length === 1) { + return shortNameMatches[0] + } + + return undefined +} + +export function matchCommandByName(commands: CommandInfo[], requestedName: string): CommandInfo | undefined { + const normalizedName = requestedName.toLowerCase() + return sortByScopePriority(commands).find((command) => command.name.toLowerCase() === normalizedName) +} + +export function findPartialMatches( + skills: LoadedSkill[], + commands: CommandInfo[], + requestedName: string +): string[] { + const normalizedName = requestedName.toLowerCase() + return [ + ...skills.map((skill) => skill.name), + ...commands.map((command) => `/${command.name}`), + ].filter((name) => name.toLowerCase().includes(normalizedName)) +} diff --git a/src/tools/skill/tools.ts b/src/tools/skill/tools.ts index 34d31cb2e..f15e90410 100644 --- a/src/tools/skill/tools.ts +++ b/src/tools/skill/tools.ts @@ -1,258 +1,52 @@ import { dirname } from "node:path" import { tool, type ToolDefinition } from "@opencode-ai/plugin" import type { ToolContext } from "@opencode-ai/plugin/tool" -import { TOOL_DESCRIPTION_NO_SKILLS, TOOL_DESCRIPTION_PREFIX } from "./constants" -import type { SkillArgs, SkillInfo, SkillLoadOptions } from "./types" +import { TOOL_DESCRIPTION_PREFIX } from "./constants" +import type { SkillArgs, SkillLoadOptions } from "./types" import type { LoadedSkill } from "../../features/opencode-skill-loader" -import { getAllSkills, extractSkillTemplate, clearSkillCache } from "../../features/opencode-skill-loader/skill-content" +import { getAllSkills, clearSkillCache } from "../../features/opencode-skill-loader/skill-content" import { injectGitMasterConfig } from "../../features/opencode-skill-loader/skill-content" -import type { SkillMcpManager, SkillMcpClientInfo, SkillMcpServerContext } from "../../features/skill-mcp-manager" -import type { Tool, Resource, Prompt } from "@modelcontextprotocol/sdk/types.js" -import { sanitizeJsonSchema } from "../../plugin/normalize-tool-arg-schemas" import { discoverCommandsSync } from "../slashcommand/command-discovery" import type { CommandInfo } from "../slashcommand/types" import { formatLoadedCommand } from "../slashcommand/command-output-formatter" - -type NativeSkillEntry = { - name: string - description: string - location: string - content: string -} -// Priority: project > user > opencode/opencode-project > builtin/config -const scopePriority: Record = { - project: 4, - user: 3, - opencode: 2, - "opencode-project": 2, - plugin: 1, - config: 1, - builtin: 1, -} - -function loadedSkillToInfo(skill: LoadedSkill): SkillInfo { - return { - name: skill.name, - description: skill.definition.description || "", - location: skill.path, - scope: skill.scope, - license: skill.license, - compatibility: skill.compatibility, - metadata: skill.metadata, - allowedTools: skill.allowedTools, - } -} - -function nativeSkillToLoadedSkill(native: NativeSkillEntry): LoadedSkill { - return { - name: native.name, - path: native.location, - definition: { - name: native.name, - description: native.description, - template: native.content, - }, - scope: "config", - } -} - -function mergeNativeSkills(skills: LoadedSkill[], nativeSkills: NativeSkillEntry[]): void { - const knownNames = new Set(skills.map(skill => skill.name)) - for (const native of nativeSkills) { - if (knownNames.has(native.name)) continue - skills.push(nativeSkillToLoadedSkill(native)) - knownNames.add(native.name) - } -} - -function mergeNativeSkillInfos(skillInfos: SkillInfo[], nativeSkills: NativeSkillEntry[]): void { - const knownNames = new Set(skillInfos.map(skill => skill.name)) - for (const native of nativeSkills) { - if (knownNames.has(native.name)) continue - skillInfos.push({ - name: native.name, - description: native.description, - location: native.location, - scope: "config", - }) - knownNames.add(native.name) - } -} - -function isPromiseLike(value: T | Promise): value is Promise { - return typeof value === "object" && value !== null && "then" in value -} - -function formatCombinedDescription(skills: SkillInfo[], commands: CommandInfo[]): string { - const lines: string[] = [] - - if (skills.length === 0 && commands.length === 0) { - return TOOL_DESCRIPTION_NO_SKILLS - } - - // Uses module-level scopePriority for consistent priority ordering - - const allItems: string[] = [] - - // Skills rendered as command items (skills are also slash-invocable) - if (skills.length > 0) { - const sortedSkills = [...skills].sort((a, b) => { - const priorityA = scopePriority[a.scope] || 0 - const priorityB = scopePriority[b.scope] || 0 - return priorityB - priorityA - }) - sortedSkills.forEach(skill => { - const parts = [ - " ", - ` /${skill.name}`, - ` ${skill.description}`, - ` ${skill.scope}`, - ] - if (skill.compatibility) { - parts.push(` ${skill.compatibility}`) - } - parts.push(" ") - allItems.push(parts.join("\n")) - }) - } - - // Sort and add commands second (commands after skills) - if (commands.length > 0) { - const sortedCommands = [...commands].sort((a, b) => { - const priorityA = scopePriority[a.scope] || 0 - const priorityB = scopePriority[b.scope] || 0 - return priorityB - priorityA // Higher priority first - }) - sortedCommands.forEach(cmd => { - const hint = cmd.metadata.argumentHint ? ` ${cmd.metadata.argumentHint}` : "" - const parts = [ - " ", - ` /${cmd.name}`, - ` ${cmd.metadata.description || "(no description)"}`, - ` ${cmd.scope}`, - ] - if (hint) { - parts.push(` ${hint.trim()}`) - } - parts.push(" ") - allItems.push(parts.join("\n")) - }) - } - - if (allItems.length > 0) { - lines.push(`\n\nPriority: project > user > opencode > builtin/plugin | Skills listed before commands\nInvoke via: skill(name="item-name") — omit leading slash for commands.\n${allItems.join("\n")}\n`) - } - - return TOOL_DESCRIPTION_PREFIX + lines.join("") -} - -async function extractSkillBody(skill: LoadedSkill): Promise { - if (skill.lazyContent) { - const fullTemplate = await skill.lazyContent.load() - const templateMatch = fullTemplate.match(/([\s\S]*?)<\/skill-instruction>/) - return templateMatch ? templateMatch[1].trim() : fullTemplate - } - - if (skill.scope === "config" && skill.definition.template) { - const templateMatch = skill.definition.template.match(/([\s\S]*?)<\/skill-instruction>/) - return templateMatch ? templateMatch[1].trim() : skill.definition.template - } - - if (skill.path) { - return extractSkillTemplate(skill) - } - - const templateMatch = skill.definition.template?.match(/([\s\S]*?)<\/skill-instruction>/) - return templateMatch ? templateMatch[1].trim() : skill.definition.template || "" -} - -async function formatMcpCapabilities( - skill: LoadedSkill, - manager: SkillMcpManager, - sessionID: string -): Promise { - if (!skill.mcpConfig || Object.keys(skill.mcpConfig).length === 0) { - return null - } - - const sections: string[] = ["", "## Available MCP Servers", ""] - - for (const [serverName, config] of Object.entries(skill.mcpConfig)) { - const info: SkillMcpClientInfo = { - serverName, - skillName: skill.name, - sessionID, - } - const context: SkillMcpServerContext = { - config, - skillName: skill.name, - } - - sections.push(`### ${serverName}`) - sections.push("") - - try { - const [tools, resources, prompts] = await Promise.all([ - manager.listTools(info, context).catch(() => []), - manager.listResources(info, context).catch(() => []), - manager.listPrompts(info, context).catch(() => []), - ]) - - if (tools.length > 0) { - sections.push("**Tools:**") - sections.push("") - for (const t of tools as Tool[]) { - sections.push(`#### \`${t.name}\``) - if (t.description) { - sections.push(t.description) - } - sections.push("") - sections.push("**inputSchema:**") - sections.push("```json") - sections.push(JSON.stringify(sanitizeJsonSchema(t.inputSchema), null, 2)) - sections.push("```") - sections.push("") - } - } - if (resources.length > 0) { - sections.push(`**Resources**: ${resources.map((r: Resource) => r.uri).join(", ")}`) - } - if (prompts.length > 0) { - sections.push(`**Prompts**: ${prompts.map((p: Prompt) => p.name).join(", ")}`) - } - - if (tools.length === 0 && resources.length === 0 && prompts.length === 0) { - sections.push("*No capabilities discovered*") - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) - sections.push(`*Failed to connect: ${errorMessage.split("\n")[0]}*`) - } - - sections.push("") - sections.push(`Use \`skill_mcp\` tool with \`mcp_name="${serverName}"\` to invoke.`) - sections.push("") - } - - return sections.join("\n") -} +import { formatCombinedDescription } from "./description-formatter" +import { formatMcpCapabilities } from "./mcp-capability-formatter" +import { + findPartialMatches, + matchCommandByName, + matchSkillByName, +} from "./skill-matcher" +import { extractSkillBody } from "./skill-body" +import { + isPromiseLike, + loadedSkillToInfo, + mergeNativeSkillInfos, + mergeNativeSkills, +} from "./native-skills" export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition { let cachedDescription: string | null = null const getSkills = async (): Promise => { clearSkillCache() - const discovered = await getAllSkills({disabledSkills: options?.disabledSkills, browserProvider: options?.browserProvider}) + const discovered = await getAllSkills({ + disabledSkills: options?.disabledSkills, + browserProvider: options?.browserProvider, + }) const allSkills = !options.skills ? discovered - : [...discovered, ...options.skills.filter(s => !new Set(discovered.map(d => d.name)).has(s.name))] + : [ + ...discovered, + ...options.skills.filter( + (skill) => !new Set(discovered.map((discoveredSkill) => discoveredSkill.name)).has(skill.name) + ), + ] if (options.nativeSkills) { try { const nativeAll = await options.nativeSkills.all() mergeNativeSkills(allSkills, nativeAll) } catch { - // Native skill discovery may not be available } } @@ -289,7 +83,6 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition mergeNativeSkillInfos(skillInfos, nativeAll) } } catch { - // Native skill discovery may not be available } } @@ -323,21 +116,7 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition cachedDescription = formatCombinedDescription(skills.map(loadedSkillToInfo), commands) const requestedName = args.name.replace(/^\//, "") - - // Check skills first (exact match, case-insensitive) - let matchedSkill = skills.find(s => s.name.toLowerCase() === requestedName.toLowerCase()) - - // Fallback: try matching by short name (basename) for namespaced skills - // e.g. "systematic-debugging" matches "superpowers/systematic-debugging" - if (!matchedSkill) { - const shortNameMatches = skills.filter(s => { - const parts = s.name.split("/") - return parts.length > 1 && parts[parts.length - 1].toLowerCase() === requestedName.toLowerCase() - }) - if (shortNameMatches.length === 1) { - matchedSkill = shortNameMatches[0] - } - } + const matchedSkill = matchSkillByName(skills, requestedName) if (matchedSkill) { if (matchedSkill.definition.agent && (!ctx?.agent || matchedSkill.definition.agent !== ctx.agent)) { @@ -380,27 +159,13 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition return output.join("\n") } - // Check commands (exact match, case-insensitive) - sort by priority first - const sortedCommands = [...commands].sort((a, b) => { - const priorityA = scopePriority[a.scope] || 0 - const priorityB = scopePriority[b.scope] || 0 - return priorityB - priorityA // Higher priority first - }) - const matchedCommand = sortedCommands.find(c => c.name.toLowerCase() === requestedName.toLowerCase()) + const matchedCommand = matchCommandByName(commands, requestedName) if (matchedCommand) { return await formatLoadedCommand(matchedCommand, args.user_message) } - // No match found — provide helpful error with partial matches - const allNames = [ - ...skills.map(s => s.name), - ...commands.map(c => `/${c.name}`), - ] - - const partialMatches = allNames.filter(n => - n.toLowerCase().includes(requestedName.toLowerCase()) - ) + const partialMatches = findPartialMatches(skills, commands, requestedName) if (partialMatches.length > 0) { throw new Error( @@ -408,7 +173,10 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition ) } - const available = allNames.join(", ") + const available = [ + ...skills.map((skill) => skill.name), + ...commands.map((command) => `/${command.name}`), + ].join(", ") throw new Error( `Skill or command "${args.name}" not found. Available: ${available || "none"}` ) From 22283fca6d25f8bc403cb9754fc7313e0792d890 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 3 Apr 2026 19:17:09 +0900 Subject: [PATCH 3/3] refactor(tools): fix empty catches, remove AI slop from code comments --- src/tools/delegate-task/subagent-resolver.ts | 4 --- .../hashline-edit/edit-operations.test.ts | 6 ++-- .../hashline-edit/formatter-trigger.test.ts | 8 +++--- src/tools/hashline-edit/validation.test.ts | 12 ++++---- src/tools/hashline-edit/validation.ts | 1 - src/tools/lsp/config.test.ts | 3 +- src/tools/lsp/lsp-manager-process-cleanup.ts | 28 +++++++++++++------ src/tools/lsp/lsp-process.ts | 3 -- src/tools/skill/tools.test.ts | 6 ++-- src/tools/task/todo-sync.test.ts | 2 +- 10 files changed, 38 insertions(+), 35 deletions(-) diff --git a/src/tools/delegate-task/subagent-resolver.ts b/src/tools/delegate-task/subagent-resolver.ts index 0baedf552..88117ca0f 100644 --- a/src/tools/delegate-task/subagent-resolver.ts +++ b/src/tools/delegate-task/subagent-resolver.ts @@ -141,8 +141,6 @@ Create the work plan directly - that's your job as the planning agent.`, categoryModel = variantToUse ? { ...normalized, variant: variantToUse } : normalized } } else if (resolutionSkipped && (agentOverride?.model ?? agentCategoryModel)) { - // Cold cache: resolution was skipped but user explicitly configured a model. - // Honor the user override directly — don't fall through to hardcoded fallback chain. const normalized = normalizeModelFormat((agentOverride?.model ?? agentCategoryModel)!) if (normalized) { const agentCategoryVariant = agentOverride?.category @@ -164,8 +162,6 @@ Create the work plan directly - that's your job as the planning agent.`, normalizedAgentFallbackModels, defaultProviderID, ) - // Don't assign hardcoded fallback chain when resolution was skipped (cold cache) - // — the chain may contain model IDs that don't exist in the provider yet. fallbackChain = configuredFallbackChain ?? (resolutionSkipped ? undefined : agentRequirement?.fallbackChain) // Only promote fallback-only settings when resolution actually selected a fallback model. diff --git a/src/tools/hashline-edit/edit-operations.test.ts b/src/tools/hashline-edit/edit-operations.test.ts index 40585210f..9be6dabae 100644 --- a/src/tools/hashline-edit/edit-operations.test.ts +++ b/src/tools/hashline-edit/edit-operations.test.ts @@ -227,10 +227,10 @@ describe("hashline edit operations", () => { }) it("preserves blank lines and indentation in range replace (no false unwrap)", () => { - //#given — reproduces the 애국가 bug where blank+indented lines collapse + //#given, reproduces the 애국가 bug where blank+indented lines collapse const lines = ["", "동해물과 백두산이 마르고 닳도록", "하느님이 보우하사 우리나라 만세", "", "무궁화 삼천리 화려강산", "대한사람 대한으로 길이 보전하세", ""] - //#when — replace the range with indented version (blank lines preserved) + //#when, replace the range with indented version (blank lines preserved) const result = applyReplaceLines( lines, anchorFor(lines, 1), @@ -238,7 +238,7 @@ describe("hashline edit operations", () => { ["", " 동해물과 백두산이 마르고 닳도록", " 하느님이 보우하사 우리나라 만세", "", " 무궁화 삼천리 화려강산", " 대한사람 대한으로 길이 보전하세", ""] ) - //#then — all 7 lines preserved with indentation, not collapsed to 3 + //#then, all 7 lines preserved with indentation, not collapsed to 3 expect(result).toEqual(["", " 동해물과 백두산이 마르고 닳도록", " 하느님이 보우하사 우리나라 만세", "", " 무궁화 삼천리 화려강산", " 대한사람 대한으로 길이 보전하세", ""]) }) diff --git a/src/tools/hashline-edit/formatter-trigger.test.ts b/src/tools/hashline-edit/formatter-trigger.test.ts index c631ae079..050c2b2f6 100644 --- a/src/tools/hashline-edit/formatter-trigger.test.ts +++ b/src/tools/hashline-edit/formatter-trigger.test.ts @@ -350,10 +350,10 @@ describe("runFormattersForFile", () => { }, }) - //#when — run for a .go file, but only .ts formatters registered + //#when, run for a .go file, but only .ts formatters registered await runFormattersForFile(client, "/project", "/src/main.go") - //#then — no error thrown + //#then, no error thrown }) it("runs formatter for matching extension", async () => { @@ -367,10 +367,10 @@ describe("runFormattersForFile", () => { }, }) - //#when — echo is a safe no-op command + //#when, echo is a safe no-op command await runFormattersForFile(client, "/tmp", "/tmp/test.ts") - //#then — should complete without error + //#then, should complete without error expect(client.config.get).toHaveBeenCalledTimes(1) }) }) diff --git a/src/tools/hashline-edit/validation.test.ts b/src/tools/hashline-edit/validation.test.ts index 739def9fa..c3c531bef 100644 --- a/src/tools/hashline-edit/validation.test.ts +++ b/src/tools/hashline-edit/validation.test.ts @@ -23,10 +23,10 @@ describe("parseLineRef", () => { }) it("gives specific hint when literal text is used instead of line number", () => { - //#given — model sends "LINE#HK" instead of "1#HK" + //#given, model sends "LINE#HK" instead of "1#HK" const ref = "LINE#HK" - //#when / #then — error should mention that LINE is not a valid number + //#when / #then, error should mention that LINE is not a valid number expect(() => parseLineRef(ref)).toThrow(/not a line number/i) }) @@ -39,10 +39,10 @@ describe("parseLineRef", () => { }) it("extracts valid line number from mixed prefix like LINE42 without throwing", () => { - //#given — normalizeLineRef extracts 42#VK from LINE42#VK + //#given, normalizeLineRef extracts 42#VK from LINE42#VK const ref = "LINE42#VK" - //#when / #then — should parse successfully as line 42 + //#when / #then, should parse successfully as line 42 const result = parseLineRef(ref) expect(result.line).toBe(42) expect(result.hash).toBe("VK") @@ -144,11 +144,11 @@ describe("validateLineRef", () => { }) it("suggests correct line number when hash matches a file line", () => { - //#given — model sends LINE#XX where XX is the actual hash for line 1 + //#given, model sends LINE#XX where XX is the actual hash for line 1 const lines = ["function hello() {", " return 42", "}"] const hash = computeLineHash(1, lines[0]) - //#when / #then — error should suggest the correct reference + //#when / #then, error should suggest the correct reference expect(() => validateLineRefs(lines, [`LINE#${hash}`])).toThrow(new RegExp(`1#${hash}`)) }) }) diff --git a/src/tools/hashline-edit/validation.ts b/src/tools/hashline-edit/validation.ts index aa9166c16..f09b8fb8b 100644 --- a/src/tools/hashline-edit/validation.ts +++ b/src/tools/hashline-edit/validation.ts @@ -48,7 +48,6 @@ export function parseLineRef(ref: string): LineRef { hash: match[2], } } - // normalized equals ref.trim() in all error paths — extraction only succeeds for valid refs const hashIdx = normalized.indexOf('#') if (hashIdx > 0) { const prefix = normalized.slice(0, hashIdx) diff --git a/src/tools/lsp/config.test.ts b/src/tools/lsp/config.test.ts index 85de82c37..59459cde8 100644 --- a/src/tools/lsp/config.test.ts +++ b/src/tools/lsp/config.test.ts @@ -20,8 +20,7 @@ describe("isServerInstalled", () => { afterEach(() => { try { rmSync(tempDir, { recursive: true, force: true }) - } catch (e) { - // cleanup failed — ignored + } catch { } if (process.platform === "win32") { diff --git a/src/tools/lsp/lsp-manager-process-cleanup.ts b/src/tools/lsp/lsp-manager-process-cleanup.ts index df9f299e9..4bf6b14f7 100644 --- a/src/tools/lsp/lsp-manager-process-cleanup.ts +++ b/src/tools/lsp/lsp-manager-process-cleanup.ts @@ -1,3 +1,5 @@ +import { log } from "../../shared/logger" + type ManagedClientForCleanup = { client: { stop: () => Promise; @@ -22,23 +24,32 @@ export type LspProcessCleanupHandle = { export function registerLspManagerProcessCleanup(options: ProcessCleanupOptions): LspProcessCleanupHandle { const handlers: RegisteredHandler[] = []; - // Synchronous cleanup for 'exit' event (cannot await) + const logCleanupError = (phase: string, error: unknown): void => { + log(`[lsp-manager-process-cleanup] ${phase}`, { + error: error instanceof Error ? error.message : String(error), + }); + }; + const syncCleanup = () => { for (const [, managed] of options.getClients()) { try { - // Fire-and-forget during sync exit - process is terminating - void managed.client.stop().catch(() => {}); - } catch {} + void managed.client.stop().catch((error) => { + logCleanupError("stop failed during exit cleanup", error); + }); + } catch (error) { + logCleanupError("failed to schedule exit cleanup", error); + } } options.clearClients(); options.clearCleanupInterval(); }; - // Async cleanup for signal handlers - properly await all stops const asyncCleanup = async () => { const stopPromises: Promise[] = []; for (const [, managed] of options.getClients()) { - stopPromises.push(managed.client.stop().catch(() => {})); + stopPromises.push(managed.client.stop().catch((error) => { + logCleanupError("stop failed during signal cleanup", error); + })); } await Promise.allSettled(stopPromises); options.clearClients(); @@ -52,8 +63,9 @@ export function registerLspManagerProcessCleanup(options: ProcessCleanupOptions) registerHandler("exit", syncCleanup); - // Don't call process.exit() here; other handlers (background-agent manager) handle final exit. - const signalCleanup = () => void asyncCleanup().catch(() => {}); + const signalCleanup = () => void asyncCleanup().catch((error) => { + logCleanupError("signal cleanup failed", error); + }); registerHandler("SIGINT", signalCleanup); registerHandler("SIGTERM", signalCleanup); if (process.platform === "win32") { diff --git a/src/tools/lsp/lsp-process.ts b/src/tools/lsp/lsp-process.ts index 358c5c7e5..3f7b769a2 100644 --- a/src/tools/lsp/lsp-process.ts +++ b/src/tools/lsp/lsp-process.ts @@ -2,11 +2,9 @@ import { spawn as bunSpawn } from "bun" import { spawn as nodeSpawn, type ChildProcess } from "node:child_process" import { existsSync, statSync } from "fs" import { log } from "../../shared/logger" -// Bun spawn segfaults on Windows (oven-sh/bun#25798) — unfixed as of v1.3.8+ function shouldUseNodeSpawn(): boolean { return process.platform === "win32" } -// Prevents segfaults when libuv gets a non-existent cwd (oven-sh/bun#25798) export function validateCwd(cwd: string): { valid: boolean; error?: string } { try { if (!existsSync(cwd)) { @@ -24,7 +22,6 @@ export function validateCwd(cwd: string): { valid: boolean; error?: string } { interface StreamReader { read(): Promise<{ done: boolean; value: Uint8Array | undefined }> } -// Bridges Bun Subprocess and Node.js ChildProcess under a common API export interface UnifiedProcess { stdin: { write(chunk: Uint8Array | string): void } stdout: { getReader(): StreamReader } diff --git a/src/tools/skill/tools.test.ts b/src/tools/skill/tools.test.ts index 5007857ff..bf7ebf451 100644 --- a/src/tools/skill/tools.test.ts +++ b/src/tools/skill/tools.test.ts @@ -732,14 +732,14 @@ describe("skill tool - short name resolution", () => { ] const tool = createSkillTool({ skills: loadedSkills }) - // when / then — should not resolve (ambiguous), should suggest both + // when / then, should not resolve (ambiguous), should suggest both await expect(tool.execute({ name: "debugging" }, mockContext)).rejects.toThrow( "not found" ) }) it("prefers exact match over short name match", async () => { - // given — "debugging" exists as both exact and as part of a namespace + // given, "debugging" exists as both exact and as part of a namespace const loadedSkills = [ createMockSkill("debugging"), createMockSkill("superpowers/debugging"), @@ -749,7 +749,7 @@ describe("skill tool - short name resolution", () => { // when const result = await tool.execute({ name: "debugging" }, mockContext) - // then — should match "debugging" exactly, not "superpowers/debugging" + // then, should match "debugging" exactly, not "superpowers/debugging" expect(result).toContain("## Skill: debugging") }) }) diff --git a/src/tools/task/todo-sync.test.ts b/src/tools/task/todo-sync.test.ts index d6c87c3df..bf83b732b 100644 --- a/src/tools/task/todo-sync.test.ts +++ b/src/tools/task/todo-sync.test.ts @@ -535,7 +535,7 @@ describe("syncAllTasksToTodos", () => { // when await syncAllTasksToTodos(mockCtx, tasks, "session-1", writer); - // then — no duplicates + // then, no duplicates const matching = writtenTodos.filter((t: TodoInfo) => t.content === "Task 1 (updated)"); expect(matching.length).toBe(1); expect(matching[0].status).toBe("in_progress");