{"record":{"id":"6ff897bd11f2910b","repo":"different-ai/openwork","slug":"skill-not-found","errorCode":"skill_not_found","errorMessage":"Skill not found: ${trimmed}","messagePattern":"Skill not found: (.+?)","errorType":"error_code","errorClass":"ApiError","httpStatus":404,"severity":"error","filePath":"apps/server/src/skills.ts","lineNumber":291,"sourceCode":"  return { path: skillPath, action: existed ? \"updated\" : \"added\" };\n}\n\nexport async function deleteSkill(workspaceRoot: string, name: string): Promise<{ path: string }> {\n  const trimmed = name.trim();\n  validateSkillName(trimmed);\n  const baseDir = projectSkillsDir(workspaceRoot);\n  const flatDir = join(baseDir, trimmed);\n  if (await exists(join(flatDir, \"SKILL.md\"))) {\n    await rm(flatDir, { recursive: true, force: true });\n    return { path: flatDir };\n  }\n  // Nested layout: skills/<domain>/<name>/SKILL.md (e.g. skills installed by\n  // marketplace plugin bundles are namespaced under a plugin folder). Listing\n  // supports this layout, so deletion must resolve it the same way.\n  const items = await listSkills(workspaceRoot, false);\n  const item = items.find((skill) => skill.name === trimmed && skill.scope === \"project\");\n  if (!item) {\n    throw new ApiError(404, \"skill_not_found\", `Skill not found: ${trimmed}`);\n  }\n  const skillDir = dirname(item.path);\n  await rm(skillDir, { recursive: true, force: true });\n  return { path: skillDir };\n}\n","sourceCodeStart":273,"sourceCodeEnd":297,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/apps/server/src/skills.ts#L273-L297","documentation":"This 404 skill_not_found error is thrown by deleteSkill when no project-scope skill matching the requested name exists in the workspace's skills directory. Deletion resolves the skill via listSkills and requires an exact name match with scope \"project\"; nested layouts (skills/<domain>/<name>/SKILL.md) are searched too, but user/global-scope skills will not match.","triggerScenarios":"DELETEing a skill whose name doesn't match any installed project skill — wrong name, wrong casing, the skill is user-scope instead of project-scope, or it was already deleted.","commonSituations":"Typos in skill names; case-sensitivity mismatch (Linux filesystem); attempting to delete a user-level skill via the project endpoint; a previous delete already removed it and the client retried.","solutions":["List project skills first and use the exact name returned (skill.name)","Check the skill's scope — user-scope skills need the user-scope deletion path","Handle 404 idempotently (treat already-deleted as success) in retry logic","Verify filename casing matches the requested name"],"exampleFix":"// before\nawait deleteSkill(\"My-Skill\"); // 404: actual name is \"my-skill\"\n// after\nconst skills = await listSkills(root);\nconst target = skills.find(s => s.scope === \"project\" && s.name === \"my-skill\");\nif (target) await deleteSkill(target.name);","handlingStrategy":"try-catch","validationCode":"const skills = await listSkills(root);\nconst exists = skills.some(s => s.scope === \"project\" && s.name === name);\nif (!exists) throw new Error(`skill \"${name}\" not found in project scope`);","typeGuard":null,"tryCatchPattern":"try {\n  await deleteSkill(name);\n} catch (e) {\n  if (e.code === \"skill_not_found\") return { deleted: false }; // idempotent\n  throw e;\n}","preventionTips":["Look up exact skill.name via listSkills before deleting","Check skill scope — user-scope skills aren't deletable via this path","Treat 404 as idempotent success in deletion retries","Match name casing exactly on case-sensitive filesystems"],"tags":["not-found","skills","http-404"],"backgroundTag":"resource-not-found","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}