fix(skill): unify skill resolution to support user custom skills

sisyphus_task was only loading builtin skills via resolveMultipleSkills().
Now uses resolveMultipleSkillsAsync() which merges discoverSkills() + builtin skills.

- Add getAllSkills(), extractSkillTemplate(), resolveMultipleSkillsAsync()
- Update sisyphus_task to use async skill resolution
- Refactor skill tool to reuse unified getAllSkills()
- Add async skill resolution tests
This commit is contained in:
justsisyphus
2026-01-16 01:57:57 +09:00
parent 5de3d4fb7d
commit 207a39b17a
4 changed files with 198 additions and 12 deletions

View File

@@ -1,10 +1,64 @@
import { createBuiltinSkills } from "../builtin-skills/skills"
import { discoverSkills } from "./loader"
import type { LoadedSkill } from "./types"
import { parseFrontmatter } from "../../shared/frontmatter"
import { readFileSync } from "node:fs"
import type { GitMasterConfig } from "../../config/schema"
export interface SkillResolutionOptions {
gitMasterConfig?: GitMasterConfig
}
let cachedSkills: LoadedSkill[] | null = null
function clearSkillCache(): void {
cachedSkills = null
}
async function getAllSkills(): Promise<LoadedSkill[]> {
if (cachedSkills) return cachedSkills
const [discoveredSkills, builtinSkillDefs] = await Promise.all([
discoverSkills({ includeClaudeCodePaths: true }),
Promise.resolve(createBuiltinSkills()),
])
const builtinSkillsAsLoaded: LoadedSkill[] = builtinSkillDefs.map((skill) => ({
name: skill.name,
definition: {
name: skill.name,
description: skill.description,
template: skill.template,
model: skill.model,
agent: skill.agent,
subtask: skill.subtask,
},
scope: "builtin" as const,
license: skill.license,
compatibility: skill.compatibility,
metadata: skill.metadata as Record<string, string> | undefined,
allowedTools: skill.allowedTools,
mcpConfig: skill.mcpConfig,
}))
const discoveredNames = new Set(discoveredSkills.map((s) => s.name))
const uniqueBuiltins = builtinSkillsAsLoaded.filter((s) => !discoveredNames.has(s.name))
cachedSkills = [...discoveredSkills, ...uniqueBuiltins]
return cachedSkills
}
async function extractSkillTemplate(skill: LoadedSkill): Promise<string> {
if (skill.path) {
const content = readFileSync(skill.path, "utf-8")
const { body } = parseFrontmatter(content)
return body.trim()
}
return skill.definition.template || ""
}
export { clearSkillCache, getAllSkills, extractSkillTemplate }
function injectGitMasterConfig(template: string, config?: GitMasterConfig): string {
if (!config) return template
@@ -60,3 +114,53 @@ export function resolveMultipleSkills(skillNames: string[], options?: SkillResol
return { resolved, notFound }
}
export async function resolveSkillContentAsync(
skillName: string,
options?: SkillResolutionOptions
): Promise<string | null> {
const allSkills = await getAllSkills()
const skill = allSkills.find((s) => s.name === skillName)
if (!skill) return null
const template = await extractSkillTemplate(skill)
if (skillName === "git-master" && options?.gitMasterConfig) {
return injectGitMasterConfig(template, options.gitMasterConfig)
}
return template
}
export async function resolveMultipleSkillsAsync(
skillNames: string[],
options?: SkillResolutionOptions
): Promise<{
resolved: Map<string, string>
notFound: string[]
}> {
const allSkills = await getAllSkills()
const skillMap = new Map<string, LoadedSkill>()
for (const skill of allSkills) {
skillMap.set(skill.name, skill)
}
const resolved = new Map<string, string>()
const notFound: string[] = []
for (const name of skillNames) {
const skill = skillMap.get(name)
if (skill) {
const template = await extractSkillTemplate(skill)
if (name === "git-master" && options?.gitMasterConfig) {
resolved.set(name, injectGitMasterConfig(template, options.gitMasterConfig))
} else {
resolved.set(name, template)
}
} else {
notFound.push(name)
}
}
return { resolved, notFound }
}