Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/core/prompts/sections/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export { getToolUseGuidelinesSection } from "./tool-use-guidelines"
export { getCapabilitiesSection } from "./capabilities"
export { getModesSection } from "./modes"
export { markdownFormattingSection } from "./markdown-formatting"
export { getSkillsSection } from "./skills"
71 changes: 71 additions & 0 deletions src/core/prompts/sections/skills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { SkillsManager, SkillMetadata } from "../../../services/skills/SkillsManager"

/**
* Get a display-friendly relative path for a skill.
* Converts absolute paths to relative paths to avoid leaking sensitive filesystem info.
*
* @param skill - The skill metadata
* @returns A relative path like ".roo/skills/name/SKILL.md" or "~/.roo/skills/name/SKILL.md"
*/
function getDisplayPath(skill: SkillMetadata): string {
const basePath = skill.source === "project" ? ".roo" : "~/.roo"
const skillsDir = skill.mode ? `skills-${skill.mode}` : "skills"
return `${basePath}/${skillsDir}/${skill.name}/SKILL.md`
}

/**
* Generate the skills section for the system prompt.
* Only includes skills relevant to the current mode.
* Format matches the modes section style.
*
* @param skillsManager - The SkillsManager instance
* @param currentMode - The current mode slug (e.g., 'code', 'architect')
*/
export async function getSkillsSection(
skillsManager: SkillsManager | undefined,
currentMode: string | undefined,
): Promise<string> {
if (!skillsManager || !currentMode) return ""

// Get skills filtered by current mode (with override resolution)
const skills = skillsManager.getSkillsForMode(currentMode)
if (skills.length === 0) return ""

// Separate generic and mode-specific skills for display
const genericSkills = skills.filter((s) => !s.mode)
const modeSpecificSkills = skills.filter((s) => s.mode === currentMode)

let skillsList = ""

if (modeSpecificSkills.length > 0) {
skillsList += modeSpecificSkills
.map(
(skill) =>
` * "${skill.name}" skill (${currentMode} mode) - ${skill.description} [${getDisplayPath(skill)}]`,
)
.join("\n")
}

if (genericSkills.length > 0) {
if (skillsList) skillsList += "\n"
skillsList += genericSkills
.map((skill) => ` * "${skill.name}" skill - ${skill.description} [${getDisplayPath(skill)}]`)
.join("\n")
}

return `====

AVAILABLE SKILLS

Skills are pre-packaged instructions for specific tasks. When a user request matches a skill description, read the full SKILL.md file to get detailed instructions.

- These are the currently available skills for "${currentMode}" mode:
${skillsList}

To use a skill:
1. Identify which skill matches the user's request based on the description
2. Use read_file to load the full SKILL.md file from the path shown in brackets
3. Follow the instructions in the skill file
4. Access any bundled files (scripts, references, assets) as needed
`
}
10 changes: 8 additions & 2 deletions src/core/prompts/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { isEmpty } from "../../utils/object"

import { McpHub } from "../../services/mcp/McpHub"
import { CodeIndexManager } from "../../services/code-index/manager"
import { SkillsManager } from "../../services/skills/SkillsManager"

import { PromptVariables, loadSystemPromptFile } from "./sections/custom-system-prompt"

Expand All @@ -34,6 +35,7 @@ import {
getModesSection,
addCustomInstructions,
markdownFormattingSection,
getSkillsSection,
} from "./sections"

// Helper function to get prompt component, filtering out empty objects
Expand Down Expand Up @@ -69,6 +71,7 @@ async function generatePrompt(
settings?: SystemPromptSettings,
todoList?: TodoItem[],
modelId?: string,
skillsManager?: SkillsManager,
): Promise<string> {
if (!context) {
throw new Error("Extension context is required for generating system prompt")
Expand All @@ -91,7 +94,7 @@ async function generatePrompt(
// Determine the effective protocol (defaults to 'xml')
const effectiveProtocol = getEffectiveProtocol(settings?.toolProtocol)

const [modesSection, mcpServersSection] = await Promise.all([
const [modesSection, mcpServersSection, skillsSection] = await Promise.all([
getModesSection(context),
shouldIncludeMcp
? getMcpServersSection(
Expand All @@ -101,6 +104,7 @@ async function generatePrompt(
!isNativeProtocol(effectiveProtocol),
)
: Promise.resolve(""),
getSkillsSection(skillsManager, mode as string),
])

// Build tools catalog section only for XML protocol
Expand Down Expand Up @@ -147,7 +151,7 @@ ${mcpServersSection}
${getCapabilitiesSection(cwd, shouldIncludeMcp ? mcpHub : undefined)}

${modesSection}

${skillsSection ? `\n${skillsSection}` : ""}
${getRulesSection(cwd, settings)}

${getSystemInfoSection(cwd)}
Expand Down Expand Up @@ -183,6 +187,7 @@ export const SYSTEM_PROMPT = async (
settings?: SystemPromptSettings,
todoList?: TodoItem[],
modelId?: string,
skillsManager?: SkillsManager,
): Promise<string> => {
if (!context) {
throw new Error("Extension context is required for generating system prompt")
Expand Down Expand Up @@ -255,5 +260,6 @@ ${customInstructions}`
settings,
todoList,
modelId,
skillsManager,
)
}
1 change: 1 addition & 0 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3538,6 +3538,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
},
undefined, // todoList
this.api.getModel().id,
provider.getSkillsManager(),
)
})()
}
Expand Down
1 change: 1 addition & 0 deletions src/core/task/__tests__/Task.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,7 @@ describe("Cline", () => {
apiConfiguration: mockApiConfig,
}),
getMcpHub: vi.fn().mockReturnValue(undefined),
getSkillsManager: vi.fn().mockReturnValue(undefined),
say: vi.fn(),
postStateToWebview: vi.fn().mockResolvedValue(undefined),
postMessageToWebview: vi.fn().mockResolvedValue(undefined),
Expand Down
14 changes: 14 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import { ShadowCheckpointService } from "../../services/checkpoints/ShadowCheckp
import { CodeIndexManager } from "../../services/code-index/manager"
import type { IndexProgressUpdate } from "../../services/code-index/interfaces/manager"
import { MdmService } from "../../services/mdm/MdmService"
import { SkillsManager } from "../../services/skills/SkillsManager"

import { fileExistsAtPath } from "../../utils/fs"
import { setTtsEnabled, setTtsSpeed } from "../../utils/tts"
Expand Down Expand Up @@ -137,6 +138,7 @@ export class ClineProvider
private codeIndexManager?: CodeIndexManager
private _workspaceTracker?: WorkspaceTracker // workSpaceTracker read-only for access outside this class
protected mcpHub?: McpHub // Change from private to protected
protected skillsManager?: SkillsManager
private marketplaceManager: MarketplaceManager
private mdmService?: MdmService
private taskCreationCallback: (task: Task) => void
Expand Down Expand Up @@ -197,6 +199,12 @@ export class ClineProvider
this.log(`Failed to initialize MCP Hub: ${error}`)
})

// Initialize Skills Manager for skill discovery
this.skillsManager = new SkillsManager(this)
this.skillsManager.initialize().catch((error) => {
this.log(`Failed to initialize Skills Manager: ${error}`)
})

this.marketplaceManager = new MarketplaceManager(this.context, this.customModesManager)

// Forward <most> task events to the provider.
Expand Down Expand Up @@ -603,6 +611,8 @@ export class ClineProvider
this._workspaceTracker = undefined
await this.mcpHub?.unregisterClient()
this.mcpHub = undefined
await this.skillsManager?.dispose()
this.skillsManager = undefined
this.marketplaceManager?.cleanup()
this.customModesManager?.dispose()
this.log("Disposed all disposables")
Expand Down Expand Up @@ -2443,6 +2453,10 @@ export class ClineProvider
return this.mcpHub
}

public getSkillsManager(): SkillsManager | undefined {
return this.skillsManager
}

/**
* Check if the current state is compliant with MDM policy
* @returns true if compliant or no MDM policy exists, false if MDM policy exists and user is non-compliant
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ function makeProviderStub() {
rooIgnoreController: { getInstructions: () => undefined },
}),
getMcpHub: () => undefined,
getSkillsManager: () => undefined,
// State must enable browser tool and provide apiConfiguration
getState: async () => ({
apiConfiguration: {
Expand Down
3 changes: 3 additions & 0 deletions src/core/webview/generateSystemPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ export const generateSystemPrompt = async (provider: ClineProvider, message: Web
toolProtocol,
isStealthModel: modelInfo?.isStealthModel,
},
undefined, // todoList
undefined, // modelId
provider.getSkillsManager(),
)

return systemPrompt
Expand Down
Loading
Loading