{"record":{"id":"b9f9b74474691a68","repo":"zylon-ai/private-gpt","slug":"missing-skill-md-b9f9b7","errorCode":"MISSING_SKILL_MD","errorMessage":"Upload must include a SKILL.md file.","messagePattern":"Upload must include a SKILL\\.md file\\.","errorType":"error_code","errorClass":"SkillDomainError","httpStatus":null,"severity":"error","filePath":"private_gpt/server/skills/skills_files.py","lineNumber":98,"sourceCode":"            }\n\n    for value in resolved.values():\n        if not value.mime_type:\n            mime_type, _ = (\n                (\"text/markdown\", None)\n                if value.path.endswith(\".md\")\n                else mimetypes.guess_type(value.path)\n            )\n            value.mime_type = mime_type or default_mime_type\n\n        if not value.mime_type:\n            raise SkillDomainError(\n                SkillErrorCode.MIME_TYPE_UNKNOWN,\n                f\"Could not determine MIME type for file: {value.path}\",\n            )\n\n    if \"SKILL.md\" not in resolved:\n        raise SkillDomainError(\n            SkillErrorCode.MISSING_SKILL_MD, \"Upload must include a SKILL.md file.\"\n        )\n\n    return list(resolved.values())\n\n\ndef _normalize_path(path: str) -> str:\n    # Normalize backslashes to forward slashes\n    path = path.replace(\"\\\\\", \"/\")\n\n    parsed = PurePosixPath(path)\n\n    if parsed.is_absolute():\n        raise SkillDomainError(\n            SkillErrorCode.UNSAFE_PATH_ABSOLUTE,\n            f\"Unsafe file path (absolute): {path!r}\",\n        )\n","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/zylon-ai/private-gpt/blob/4a030776a31a901ad80b1bf4d7faa2c1a367efbb/private_gpt/server/skills/skills_files.py#L80-L116","documentation":"SkillDomainError with code MISSING_SKILL_MD: after all uploads are read (zips extracted, paths normalized), the pipeline requires a resolved file at exactly 'SKILL.md' as the skill manifest per the Agent Skills spec. If 'SKILL.md' is not among the resolved paths, this error is raised before storing the version.","triggerScenarios":"Uploading a zip that lacks SKILL.md anywhere; uploading loose files without a SKILL.md part; a zip whose SKILL.md sits in a wrapper directory that _flatten_wrapper_directory could not strip (e.g. multiple candidate roots or SKILL.md nested deeper than the detected root); case variants like 'skill.md' are normalized by _normalize_path, so genuine absence is the usual cause.","commonSituations":"Packaging the wrong directory (parent of the skill folder) into the zip; zipping generated artifacts without the manifest; CI building archives with a script that excludes *.md; collaborators renaming SKILL.md locally.","solutions":["Verify the archive: `unzip -l skill.zip` must show SKILL.md at the root or under a single wrapper directory.","Re-zip from inside the skill directory so SKILL.md is at the archive root (`cd my-skill && zip -r ../skill.zip .`).","If uploading loose files, add a separate part named SKILL.md.","Ensure the zip-flattening logic finds exactly one SKILL.md; restructure nested archives (zip-in-zip is not expanded)."],"exampleFix":"# before\ncd skills-repo && zip -r my-skill.zip my-skill   # SKILL.md ends up under my-skill/ only if missing\n# after\ncd skills-repo/my-skill && zip -r ../my-skill.zip .  # SKILL.md at archive root","handlingStrategy":"validation","validationCode":"import zipfile\nfrom pathlib import PurePosixPath\n\ndef zip_has_skill_md(path: str) -> bool:\n    with zipfile.ZipFile(path) as zf:\n        return any(PurePosixPath(n).name.lower() == 'skill.md' for n in zf.namelist())","typeGuard":"const zipHasSkillMd = (names: string[]): boolean =>\n  names.some((n) => n.split('/').pop()?.toLowerCase() === 'skill.md');","tryCatchPattern":"try {\n  await uploadSkill(zipBlob);\n} catch (e: any) {\n  if (e?.code === 'MISSING_SKILL_MD') {\n    zipBlob = await rebuildZipWithSkillMd(zipBlob);\n    await uploadSkill(zipBlob);\n  } else throw e;\n}","preventionTips":["Always run `unzip -l` (or list entries in code) before uploading a skill archive.","Automate packaging with a script that asserts SKILL.md presence and fails the build otherwise.","Keep SKILL.md at the skill root in the repo so naive zips work."],"tags":["skills","upload","zip","manifest","domain-error"],"backgroundTag":null,"analyzedSha":"4a030776a31a901ad80b1bf4d7faa2c1a367efbb","analyzedAt":"2026-08-15T03:51:26.951Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}