{"record":{"id":"d1803473e172a5b0","repo":"odysseus-dev/odysseus","slug":"could-not-parse-skill-md-e","errorCode":null,"errorMessage":"Could not parse SKILL.md: {e}","messagePattern":"Could not parse SKILL\\.md: (.+?)","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"routes/skills_routes.py","lineNumber":1583,"sourceCode":"\n    @router.post(\"/{skill_id}/markdown\")\n    async def save_skill_markdown(request: Request, skill_id: str):\n        \"\"\"Replace SKILL.md with new raw content. Parses + validates first.\"\"\"\n        from services.memory.skill_format import Skill\n        user = _owner(request)\n        body = await request.json()\n        new_content = body.get(\"markdown\")\n        if not isinstance(new_content, str) or not new_content.strip():\n            raise HTTPException(400, \"markdown is required\")\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        try:\n            sk = Skill.from_markdown(new_content)\n        except Exception as e:\n            raise HTTPException(400, f\"Could not parse SKILL.md: {e}\")\n        # Never rename on save: a changed `name` in the markdown would move\n        # the skill dir (update_skill) and orphan the original id, so a later\n        # delete 404s (#1333). Pin to the stored name, like _apply_skill_md.\n        sk.name = match.get(\"name\")\n        if not sk.owner:\n            sk.owner = match.get(\"owner\") or user\n        ok = skills_manager.update_skill(match.get(\"name\"), {\n            \"name\": sk.name,\n            \"description\": sk.description,\n            \"version\": sk.version,\n            \"category\": sk.category,\n            \"tags\": sk.tags,\n            \"platforms\": sk.platforms,\n            \"requires_toolsets\": sk.requires_toolsets,\n            \"fallback_for_toolsets\": sk.fallback_for_toolsets,\n            \"status\": sk.status,\n            \"confidence\": sk.confidence,\n            \"source\": sk.source,","sourceCodeStart":1565,"sourceCodeEnd":1601,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/skills_routes.py#L1565-L1601","documentation":"400 from POST /{skill_id}/markdown: services.memory.skill_format.Skill.from_markdown raised while parsing the submitted text. The endpoint saves nothing on parse failure — the existing SKILL.md is untouched. The detail embeds the parser's message, so the exact front-matter or structure defect is named in the response.","triggerScenarios":"Saving markdown whose YAML front-matter is malformed (bad indent, unclosed quotes, tabs), missing required front-matter keys (name/description), missing the '---' delimiters entirely, or containing YAML types the parser rejects (unquoted colons, duplicate keys).","commonSituations":"Hand-editing SKILL.md and breaking YAML syntax; AI-generated markdown with mangled front-matter delimiters; pasting content that uses '···' or smart dashes instead of plain '---'.","solutions":["Read the detail after 'Could not parse SKILL.md:' — it pinpoints the YAML/token error and position.","Validate the front-matter locally with a YAML parser before POSTing.","Ensure the file starts with a '---' line, has name/description keys, and closes the front-matter with a second '---'.","Quote scalar values containing colons or special characters."],"exampleFix":"# before\nmd = open(\"SKILL.md\").read()  # broken front-matter\nclient.post(f\"/api/skills/{sid}/markdown\", json={\"markdown\": md})  # 400\n\n# after\nimport yaml\nmd = open(\"SKILL.md\").read()\nfm = md.split(\"---\")[1]\nyaml.safe_load(fm)  # raises locally with a clear error before the request\nassert {\"name\", \"description\"} <= set(yaml.safe_load(fm))\nclient.post(f\"/api/skills/{sid}/markdown\", json={\"markdown\": md})","handlingStrategy":"validation","validationCode":"import yaml\nparts = new_content.split(\"---\", 2)\nassert len(parts) >= 3 and parts[0].strip() == \"\", \"file must start with a '---' front-matter block\"\nfm = yaml.safe_load(parts[1])\nassert isinstance(fm, dict) and \"name\" in fm and \"description\" in fm, \"front-matter needs name and description\"","typeGuard":"def parses_as_skill_md(text: str) -> bool:\n    \"\"\"Cheap client-side gate mirroring Skill.from_markdown.\"\"\"\n    try:\n        parts = text.split(\"---\", 2)\n        if len(parts) < 3 or parts[0].strip():\n            return False\n        fm = yaml.safe_load(parts[1])\n        return isinstance(fm, dict) and bool(fm.get(\"name\")) and bool(fm.get(\"description\"))\n    except yaml.YAMLError:\n        return False","tryCatchPattern":null,"preventionTips":["Lint the front-matter locally with a YAML parser before every save.","Use plain ASCII '---' delimiters, exactly two, with the block at byte 0.","Quote values containing colons; avoid tabs for indentation.","Surface the server detail verbatim in the editor — it names the exact parse error."],"tags":["skills","http-400","yaml","markdown","parsing","validation"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}