{"record":{"id":"a1119b9e0930a522","repo":"odysseus-dev/odysseus","slug":"skill-source-unavailable-legacy-entry","errorCode":null,"errorMessage":"Skill source unavailable (legacy entry?)","messagePattern":"Skill source unavailable \\(legacy entry\\?\\)","errorType":"http","errorClass":"HTTPException","httpStatus":404,"severity":"warning","filePath":"routes/skills_routes.py","lineNumber":1380,"sourceCode":"        skills = skills_manager.load(owner=user)\n        for sk in skills:\n            if sk.get(\"name\") == skill_id or sk.get(\"id\") == skill_id:\n                return sk\n        raise HTTPException(404, \"Skill not found\")\n\n    @router.get(\"/{skill_id}/markdown\")\n    async def get_skill_markdown(request: Request, skill_id: str):\n        \"\"\"Return the raw SKILL.md text — used by the slash-invocation flow\n        and the editor's 'view source' affordance.\"\"\"\n        user = _owner(request)\n        skills = skills_manager.load(owner=user)\n        match = next((s for s in skills if s.get(\"name\") == skill_id or s.get(\"id\") == skill_id), None)\n        if not match:\n            raise HTTPException(404, \"Skill not found\")\n        _verify_owner(match, user)\n        md = skills_manager.read_skill_md(match.get(\"name\"), owner=user)\n        if md is None:\n            raise HTTPException(404, \"Skill source unavailable (legacy entry?)\")\n        return {\"name\": match.get(\"name\"), \"markdown\": md}\n\n    @router.post(\"/{skill_id}/test\")\n    async def test_skill(request: Request, skill_id: str):\n        \"\"\"Kick off a background skill test (agent run + LLM judge). Returns\n        immediately; the run executes server-side so it survives the modal being\n        closed. Poll GET /{skill_id}/test-status for progress + verdict.\n        On completion it records the verdict and nudges the skill's confidence\n        to match (pass→0.95, needs_work→0.6, fail→0.4; inconclusive/unknown leave\n        it untouched). It never changes the skill's published/draft STATUS.\"\"\"\n        import time as _time\n        import asyncio as _asyncio\n        from src.endpoint_resolver import resolve_endpoint\n\n        user = _owner(request)\n        body = await request.json()\n        task = (body.get(\"task\") or \"\").strip()\n","sourceCodeStart":1362,"sourceCodeEnd":1398,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/skills_routes.py#L1362-L1398","documentation":"404 from GET /{skill_id}/markdown raised when the skill WAS found and ownership verified, but skills_manager.read_skill_md returned None — the index entry exists without a readable SKILL.md behind it. The '(legacy entry?)' hint marks entries created before skills stored a SKILL.md file, which carry only structured metadata and no raw source.","triggerScenarios":"Viewing source of a legacy skill created by an older version that stored fields without a SKILL.md file; a skill whose file was deleted or stranded by a rename while the entry remained; a directory whose SKILL.md lost read permission.","commonSituations":"Upgrading an installation that predates the markdown-backed skill format; partial migrations where only some skills were converted; hand-edited skills directories.","solutions":["Check the skill directory on disk for SKILL.md; if absent, the entry is legacy.","Regenerate the source: reconstruct a SKILL.md from the entry's structured fields (description, procedure, pitfalls, verification) and POST it to /{skill_id}/markdown to backfill.","If the file exists but is unreadable, fix permissions and retry.","Alternatively re-create the skill via POST /add so it is stored in the current format."],"exampleFix":"# before\nmd = client.get(f\"/api/skills/{sid}/markdown\").json()[\"markdown\"]\n\n# after\nr = client.get(f\"/api/skills/{sid}/markdown\")\nif r.status_code == 404 and \"legacy\" in r.text:\n    meta = client.get(f\"/api/skills/{sid}\").json()\n    reconstructed = f\"---\\nname: {meta['name']}\\ndescription: {meta['description']}\\n---\\n\"\n    client.post(f\"/api/skills/{sid}/markdown\", json={\"markdown\": reconstructed})\n    r = client.get(f\"/api/skills/{sid}/markdown\")\nmd = r.json()[\"markdown\"]","handlingStrategy":"fallback","validationCode":"meta = client.get(f\"{base}/api/skills/{sid}\").json()\n# legacy entries carry structured fields but no markdown source\nneeds_backfill = \"markdown\" not in meta and not skill_dir_has_skill_md(meta[\"name\"])","typeGuard":"def is_legacy_entry(skill: dict) -> bool:\n    \"\"\"Heuristic: entry predates markdown storage when source is unset and status fields exist.\"\"\"\n    return not skill.get(\"source\") and bool(skill.get(\"description\"))","tryCatchPattern":"try:\n    md = client.get(f\"{base}/api/skills/{sid}/markdown\").json()[\"markdown\"]\nexcept KeyError if client_last_status == 404 else None:\n    pass\n# better: check status explicitly\nr = client.get(f\"{base}/api/skills/{sid}/markdown\")\nif r.status_code == 404 and \"legacy\" in str(r.json().get(\"detail\", \"\")):\n    md = render_markdown_from_fields(client.get(f\"{base}/api/skills/{sid}\").json())","preventionTips":["After upgrades, run a migration that materializes SKILL.md for every legacy entry.","Test 'view source' across the whole skill list right after upgrading.","Keep structured fields and markdown in sync going forward via the /markdown save route."],"tags":["skills","http-404","legacy","markdown","migration"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}