{"record":{"id":"1aa1930d41d6057e","repo":"can1357/oh-my-pi","slug":"unknown-skill-skillname-available-available","errorCode":null,"errorMessage":"Unknown skill: ${skillName}\nAvailable: ${availableStr}","messagePattern":"Unknown skill: (.+?)\nAvailable: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/internal-urls/skill-protocol.ts","lineNumber":63,"sourceCode":" * Handler for skill:// URLs.\n */\nexport class SkillProtocolHandler implements ProtocolHandler {\n\treadonly scheme = \"skill\";\n\treadonly immutable = true;\n\n\tasync resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {\n\t\tconst skills = context?.skills ?? getActiveSkills();\n\n\t\tconst skillName = url.rawHost || url.hostname;\n\t\tif (!skillName) {\n\t\t\tthrow new Error(\"skill:// URL requires a skill name: skill://<name>\");\n\t\t}\n\n\t\tconst skill = skills.find(s => s.name === skillName);\n\t\tif (!skill) {\n\t\t\tconst available = skills.map(s => s.name);\n\t\t\tconst availableStr = available.length > 0 ? available.join(\", \") : \"none\";\n\t\t\tthrow new Error(`Unknown skill: ${skillName}\\nAvailable: ${availableStr}`);\n\t\t}\n\n\t\tlet targetPath: string;\n\t\tconst urlPath = url.pathname;\n\t\tconst hasRelativePath = urlPath && urlPath !== \"/\" && urlPath !== \"\";\n\n\t\tif (hasRelativePath) {\n\t\t\tconst relativePath = decodeURIComponent(urlPath.slice(1));\n\t\t\tvalidateRelativePath(relativePath);\n\t\t\ttargetPath = path.join(skill.baseDir, relativePath);\n\n\t\t\tconst resolvedPath = path.resolve(targetPath);\n\t\t\tconst resolvedBaseDir = path.resolve(skill.baseDir);\n\t\t\tif (!resolvedPath.startsWith(resolvedBaseDir + path.sep) && resolvedPath !== resolvedBaseDir) {\n\t\t\t\tthrow new Error(\"Path traversal is not allowed\");\n\t\t\t}\n\t\t\t// Agent Plugin skills (§4.1): the resource must canonically resolve\n\t\t\t// within the plugin root; a dangling or unresolvable path fails closed.","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/internal-urls/skill-protocol.ts#L45-L81","documentation":"After extracting the skill name from the URL host, resolve() searches the active skill registry (context.skills or getActiveSkills()) for a matching name. If no skill with that exact name exists, the error lists all currently available skill names (or 'none' if the registry is empty) so the caller can correct the URL. Names are matched exactly — no fuzzy or case-insensitive fallback.","triggerScenarios":"resolve('skill://typo-name/...') where 'typo-name' is not in the active skills list; resolving a skill that was removed, renamed, or not yet loaded; resolving in a context where no skills are registered (empty registry).","commonSituations":"Typos or wrong casing in the skill name (matching is exact); referencing a skill from a plugin that failed to load or was disabled; stale links to renamed skills; running in an environment (test, SDK embed) where skills were never discovered.","solutions":["Use one of the names listed in the error's 'Available:' line","Check exact spelling and casing — matching is case-sensitive and exact","Verify the skill's plugin/extension is installed and loaded (check discovery logs)","Call getActiveSkills() (or the completion API) to enumerate valid names before resolving","If the skill was renamed, update the stored URL"],"exampleFix":"// before\nresolve('skill://Code-Review/README.md')\n// after\nresolve('skill://code-review/README.md') // exact registered name","handlingStrategy":"fallback","validationCode":"import { getActiveSkills } from '../extensibility/skills';\nconst names = new Set(getActiveSkills().map(s => s.name));\nif (!names.has(skillName)) {\n  throw new Error(`Skill '${skillName}' not loaded. Available: ${[...names].join(', ') || 'none'}`);\n}","typeGuard":"function skillExists(name: string, skills: { name: string }[]): skills is [{ name: string }, ...{ name: string }[]] {\n  return skills.some(s => s.name === name);\n}","tryCatchPattern":"try {\n  return await handler.resolve(url, ctx);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith('Unknown skill:')) {\n    const available = err.message.split('Available: ')[1] ?? '';\n    // pick closest match or surface the available list to the user\n  }\n  throw err;\n}","preventionTips":["Copy skill names from the 'Available:' list or getActiveSkills(), never from memory","Remember name matching is exact and case-sensitive","Verify the owning plugin/extension is installed and loaded before linking","After renaming a skill, update all persisted skill:// URLs"],"tags":["url","skills","not-found"],"backgroundTag":"unknown-resource-name","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}