odysseus-dev/odysseus · warning · HTTPException

Skill source unavailable (legacy entry?)

Error message

Skill source unavailable (legacy entry?)

What it means

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.

Source

Thrown at routes/skills_routes.py:1380

        skills = skills_manager.load(owner=user)
        for sk in skills:
            if sk.get("name") == skill_id or sk.get("id") == skill_id:
                return sk
        raise HTTPException(404, "Skill not found")

    @router.get("/{skill_id}/markdown")
    async def get_skill_markdown(request: Request, skill_id: str):
        """Return the raw SKILL.md text — used by the slash-invocation flow
        and the editor's 'view source' affordance."""
        user = _owner(request)
        skills = skills_manager.load(owner=user)
        match = next((s for s in skills if s.get("name") == skill_id or s.get("id") == skill_id), None)
        if not match:
            raise HTTPException(404, "Skill not found")
        _verify_owner(match, user)
        md = skills_manager.read_skill_md(match.get("name"), owner=user)
        if md is None:
            raise HTTPException(404, "Skill source unavailable (legacy entry?)")
        return {"name": match.get("name"), "markdown": md}

    @router.post("/{skill_id}/test")
    async def test_skill(request: Request, skill_id: str):
        """Kick off a background skill test (agent run + LLM judge). Returns
        immediately; the run executes server-side so it survives the modal being
        closed. Poll GET /{skill_id}/test-status for progress + verdict.
        On completion it records the verdict and nudges the skill's confidence
        to match (pass→0.95, needs_work→0.6, fail→0.4; inconclusive/unknown leave
        it untouched). It never changes the skill's published/draft STATUS."""
        import time as _time
        import asyncio as _asyncio
        from src.endpoint_resolver import resolve_endpoint

        user = _owner(request)
        body = await request.json()
        task = (body.get("task") or "").strip()

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Check the skill directory on disk for SKILL.md; if absent, the entry is legacy.
  2. 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.
  3. If the file exists but is unreadable, fix permissions and retry.
  4. Alternatively re-create the skill via POST /add so it is stored in the current format.

Example fix

# before
md = client.get(f"/api/skills/{sid}/markdown").json()["markdown"]

# after
r = client.get(f"/api/skills/{sid}/markdown")
if r.status_code == 404 and "legacy" in r.text:
    meta = client.get(f"/api/skills/{sid}").json()
    reconstructed = f"---\nname: {meta['name']}\ndescription: {meta['description']}\n---\n"
    client.post(f"/api/skills/{sid}/markdown", json={"markdown": reconstructed})
    r = client.get(f"/api/skills/{sid}/markdown")
md = r.json()["markdown"]
Defensive patterns

Strategy: fallback

Validate before calling

meta = client.get(f"{base}/api/skills/{sid}").json()
# legacy entries carry structured fields but no markdown source
needs_backfill = "markdown" not in meta and not skill_dir_has_skill_md(meta["name"])

Type guard

def is_legacy_entry(skill: dict) -> bool:
    """Heuristic: entry predates markdown storage when source is unset and status fields exist."""
    return not skill.get("source") and bool(skill.get("description"))

Try / catch

try:
    md = client.get(f"{base}/api/skills/{sid}/markdown").json()["markdown"]
except KeyError if client_last_status == 404 else None:
    pass
# better: check status explicitly
r = client.get(f"{base}/api/skills/{sid}/markdown")
if r.status_code == 404 and "legacy" in str(r.json().get("detail", "")):
    md = render_markdown_from_fields(client.get(f"{base}/api/skills/{sid}").json())

Prevention

When it happens

Trigger: 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.

Common situations: Upgrading an installation that predates the markdown-backed skill format; partial migrations where only some skills were converted; hand-edited skills directories.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/a1119b9e0930a522. Report an issue: GitHub.