{"record":{"id":"9fa9b63dc6e54f30","repo":"odysseus-dev/odysseus","slug":"invalid-backup-file-e-message","errorCode":null,"errorMessage":"Invalid backup file: ' + e.message","messagePattern":"Invalid backup file: ' \\+ e\\.message","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"static/js/admin.js","lineNumber":2822,"sourceCode":"    } catch (e) { msg.textContent = 'Export failed: ' + e.message; msg.className = 'admin-error'; }\n    btn.disabled = false; btn.textContent = 'Export Data';\n  });\n\n  const fileInput = el('adm-importFile');\n  el('adm-importDataBtn').addEventListener('click', () => { fileInput.value = ''; fileInput.click(); });\n  fileInput.addEventListener('change', async () => {\n    const file = fileInput.files[0];\n    if (!file) return;\n    const msg = el('adm-backupMsg');\n    const btn = el('adm-importDataBtn');\n    btn.disabled = true; btn.textContent = 'Importing...'; msg.textContent = '';\n    try {\n      const text = (await file.text()).replace(/^\\uFEFF/, '').trim();\n      let data;\n      try {\n        data = JSON.parse(text);\n      } catch (e) {\n        throw new Error('Invalid backup file: ' + e.message);\n      }\n      const res = await fetch('/api/import', {\n        method: 'POST', credentials: 'same-origin',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify(data),\n      });\n      const result = await res.json().catch(() => null);\n      if (!result) {\n        throw new Error(`Import failed: server returned ${res.status}`);\n      }\n      if (res.ok && result.ok) {\n        msg.textContent = result.message || 'Import successful.'; msg.className = 'admin-success';\n      } else {\n        msg.textContent = result.message || result.detail || 'Import failed'; msg.className = 'admin-error';\n      }\n    } catch (e) { msg.textContent = 'Import failed: ' + e.message; msg.className = 'admin-error'; }\n    btn.disabled = false; btn.textContent = 'Import Data';\n  });","sourceCodeStart":2804,"sourceCodeEnd":2840,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/static/js/admin.js#L2804-L2840","documentation":"Raised by pick_skill_md when no key in the files dict ends (case-insensitively) in 'skill.md'. In the normal flow fetch_skill_bundle already guarantees a SKILL.md exists (error 985), so this fires only when pick_skill_md or install_bundle is called directly with a caller-constructed file map that lacks one.","triggerScenarios":"Calling skills install_bundle(files) / pick_skill_md(files) with a hand-built dict like {'README.md': '...', 'prompt.txt': '...'}; or a caller that filtered/renamed fetch_skill_bundle's output and dropped the skill.md entry before installing.","commonSituations":"Programmatic integrations that assemble bundles from local directories or templates and forget the naming contract (path must end in 'skill.md'); post-processing steps that rewrite keys (e.g. stripping directories) and break the suffix.","solutions":["Ensure the files dict passed to install_bundle/pick_skill_md contains at least one key whose path ends in 'skill.md' (any directory prefix is fine, e.g. 'my-skill/SKILL.md')","Prefer using fetch_skill_bundle's output unmodified — it validates this invariant before returning","If building bundles locally, copy your skill manifest to '<name>/SKILL.md' in the dict before calling install","Check for key-mangling middleware (path normalization, lowercasing that renames the file) between fetch and install"],"exampleFix":"# before\nfiles = {\"guide.md\": md_text, \"prompt.txt\": prompt}\nskills.install_bundle(files)  # 'bundle has no SKILL.md'\n\n# after\nfiles = {\"my-skill/SKILL.md\": md_text, \"my-skill/prompt.txt\": prompt}\nskills.install_bundle(files)","handlingStrategy":"type-guard","validationCode":"def bundle_has_skill_md(files: dict[str, str]) -> bool:\n    return any(p.lower().endswith(\"skill.md\") for p in files)","typeGuard":"from typing import TypeGuard\n\ndef is_valid_bundle(files: dict[str, str]) -> TypeGuard[dict[str, str]]:\n    \"\"\"Narrows to a bundle install_bundle can process.\"\"\"\n    return bool(files) and any(p.lower().endswith(\"skill.md\") for p in files)","tryCatchPattern":"from services.memory.skill_importer import SkillImportError\n\nif not is_valid_bundle(files):\n    raise ValueError(\"bundle must contain a path ending in skill.md\")\ntry:\n    skills.install_bundle(files)\nexcept SkillImportError as e:\n    if \"no SKILL.md\" in str(e):\n        fix_bundle_naming(files)  # e.g. rename primary doc to '<name>/SKILL.md'\n    else:\n        raise","preventionTips":["Prefer passing fetch_skill_bundle output straight to install_bundle without key rewrites","Add is_valid_bundle as an assert/CI check in custom bundle builders","Keep the SKILL.md entry's path suffix intact through any normalization"],"tags":["skill-import","validation","naming-contract"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}