{"record":{"id":"e0569c077a075976","repo":"odysseus-dev/odysseus","slug":"skill-source-unavailable","errorCode":null,"errorMessage":"Skill source unavailable","messagePattern":"Skill source unavailable","errorType":"http","errorClass":"HTTPException","httpStatus":404,"severity":"error","filePath":"routes/skills_routes.py","lineNumber":1343,"sourceCode":"        user = _owner(request)\n        try:\n            body = await request.json()\n        except Exception:\n            body = {}\n        request_text = (body.get(\"request\") or \"\").strip() if isinstance(body, dict) else \"\"\n\n        invokable = {\n            s.get(\"name\"): s for s in skills_manager.index_for(owner=user)\n            if (s.get(\"name\") or \"\").strip()\n        }\n        match = invokable.get(skill_id)\n        if not match:\n            raise HTTPException(404, \"Skill is not available for slash invocation\")\n\n        name = match.get(\"name\")\n        md = skills_manager.read_skill_md(name, owner=user)\n        if md is None:\n            raise HTTPException(404, \"Skill source unavailable\")\n\n        skills_manager.record_use(name, owner=user)\n        message = (\n            \"Apply the skill below to my request, following its Procedure / Pitfalls / Verification.\\n\\n\"\n            f\"--- BEGIN SKILL ---\\n{md}\\n--- END SKILL ---\\n\\n\"\n            + (f\"Request: {request_text}\" if request_text else \"Request: (use the skill as appropriate)\")\n        )\n        return {\n            \"ok\": True,\n            \"type\": \"skill\",\n            \"name\": name,\n            \"command\": f\"/{name}\",\n            \"message\": message,\n        }\n\n    @router.get(\"/{skill_id}\")\n    async def get_skill(request: Request, skill_id: str):\n        user = _owner(request)","sourceCodeStart":1325,"sourceCodeEnd":1361,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/skills_routes.py#L1325-L1361","documentation":"404 raised when skills_manager.read_skill_md(name, owner) returns None: the skill is in the invocable index but its SKILL.md file could not be read from disk. The route has already confirmed the skill exists (821 passed), so this is specifically a missing-or-unreadable source file, not a missing skill.","triggerScenarios":"POST /{skill_id}/invoke for a skill whose index entry survived but whose SKILL.md was deleted, moved, or left unreadable on disk — e.g. the directory was partially deleted, a save was interrupted, the file lives under a renamed directory (the orphaned-id rename bug shape referenced by #1333), or file permissions changed after the index was built.","commonSituations":"Skills directory edited by hand or by another tool while the server was running; a crashed import/save leaving an index entry without its markdown; skills migrated between machines with an index/cache copied but files skipped; containerized deployments where the volume mount dropped files.","solutions":["Check the skill's directory on disk: does SKILL.md exist and is it readable by the server process?","If the file is genuinely gone, re-import the skill or re-create it via POST /add, then re-save metadata.","If files exist but under a different directory name (rename/orphan case), restore the directory to match the stored skill name or fix the entry to point at the real name.","Restart the service if the index is cached in memory so it re-scans disk state."],"exampleFix":"# before\nresp = client.post(f\"/api/skills/{name}/invoke\", json={})\nassert resp.status_code == 200\n\n# after\nresp = client.post(f\"/api/skills/{name}/invoke\", json={})\nif resp.status_code == 404 and \"source unavailable\" in resp.text:\n    # index entry exists but SKILL.md is missing on disk — re-import or re-add\n    skill_md = rebuild_or_fetch_skill_md(name)\n    client.post(f\"/api/skills/{name}/markdown\", json={\"markdown\": skill_md})\n    resp = client.post(f\"/api/skills/{name}/invoke\", json={})","handlingStrategy":"validation","validationCode":"# probe source availability via the markdown route before invoking\nr = client.get(f\"{base}/api/skills/{name}/markdown\")\nif r.status_code == 404:\n    raise FileNotFoundError(f\"SKILL.md missing for '{name}' — index entry is orphaned\")","typeGuard":"def has_source(name: str, skills_manager, owner: str) -> bool:\n    \"\"\"Skill is invokable only if its markdown source is readable.\"\"\"\n    return skills_manager.read_skill_md(name, owner=owner) is not None","tryCatchPattern":null,"preventionTips":["Never delete SKILL.md by hand; use the DELETE route so index and disk stay consistent.","After renames, verify the directory name still matches the stored skill name.","Monitor for index entries whose files are missing (reconciliation job)."],"tags":["skills","http-404","filesystem","missing-file","slash-invocation"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}