{"record":{"id":"e586a187cbbb0a5c","repo":"odysseus-dev/odysseus","slug":"skill-is-not-available-for-slash-invocation","errorCode":null,"errorMessage":"Skill is not available for slash invocation","messagePattern":"Skill is not available for slash invocation","errorType":"http","errorClass":"HTTPException","httpStatus":404,"severity":"warning","filePath":"routes/skills_routes.py","lineNumber":1338,"sourceCode":"        \"\"\"Build a skill-pinned prompt for slash-command invocation.\n\n        This is intentionally server-side so availability, ownership, and usage\n        accounting use the same rules as the SkillsManager.\n        \"\"\"\n        user = _owner(request)\n        try:\n            body = await request.json()\n        except Exception:\n            body = {}\n        request_text = (body.get(\"request\") or \"\").strip() if isinstance(body, dict) else \"\"\n\n        invokable = {\n            s.get(\"name\"): s for s in skills_manager.index_for(owner=user)\n            if (s.get(\"name\") or \"\").strip()\n        }\n        match = invokable.get(skill_id)\n        if not match:\n            raise HTTPException(404, \"Skill is not available for slash invocation\")\n\n        name = match.get(\"name\")\n        md = skills_manager.read_skill_md(name, owner=user)\n        if md is None:\n            raise HTTPException(404, \"Skill source unavailable\")\n\n        skills_manager.record_use(name, owner=user)\n        message = (\n            \"Apply the skill below to my request, following its Procedure / Pitfalls / Verification.\\n\\n\"\n            f\"--- BEGIN SKILL ---\\n{md}\\n--- END SKILL ---\\n\\n\"\n            + (f\"Request: {request_text}\" if request_text else \"Request: (use the skill as appropriate)\")\n        )\n        return {\n            \"ok\": True,\n            \"type\": \"skill\",\n            \"name\": name,\n            \"command\": f\"/{name}\",\n            \"message\": message,","sourceCodeStart":1320,"sourceCodeEnd":1356,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/skills_routes.py#L1320-L1356","documentation":"404 from the slash-invocation endpoint: the requested skill_id is not a key in the dict built from skills_manager.index_for(owner). Only skills that appear in the owner's invocable index (and have a non-blank name) can be slash-invoked. This is an ownership/scope filter, not just existence — a skill that exists for another owner is indistinguishable from one that never existed.","triggerScenarios":"POST to /{skill_id}/invoke (slash flow) with a name that is misspelled, has different casing/whitespace, was deleted after the UI list was rendered, belongs to a different owner, or exists on disk but is not present in index_for(user) (e.g. filtered out as a non-invokable/published-draft entry).","commonSituations":"Stale client-side skill list after another tab or the skills audit pipeline deleted or renamed the skill; multi-user deployments where the skill id was copied from someone else's workspace; trailing whitespace or shell-style escaping of the /name typed by the user.","solutions":["Refresh the client's skill list (GET the skills index) and re-check the exact name before invoking.","Confirm the skill still exists for the CURRENT owner: GET /{skill_id} — if that also 404s, the skill is gone or owned by someone else.","If the skill exists via GET but invoke 404s, it is excluded from index_for (blank name or index filter) — fix or re-save the skill so it has a valid name.","Guard the slash UX: on 404 show 'skill no longer available' and offer to reload skills instead of retrying blindly."],"exampleFix":"# before\nr = requests.post(f\"{base}/api/skills/{name}/invoke\", json={\"request\": req})\nr.raise_for_status()\n\n# after\nr = requests.post(f\"{base}/api/skills/{name}/invoke\", json={\"request\": req})\nif r.status_code == 404:\n    print(f\"Skill '{name}' not invokable — refreshing list\")\n    skills = requests.get(f\"{base}/api/skills\").json()\n    name = next((s[\"name\"] for s in skills if s[\"name\"] == name), None)\n    if name is None:\n        raise SystemExit(f\"Skill '{name}' was deleted or belongs to another owner\")","handlingStrategy":"validation","validationCode":"names = {s[\"name\"] for s in client.get(f\"{base}/api/skills\").json()}\nif skill_name not in names:\n    skill_name = next((n for n in names if n.lower() == skill_name.lower().strip()), None)\n    if skill_name is None:\n        raise LookupError(f\"'{skill_name}' not invokable for this owner\")","typeGuard":"def is_invokable(skill_name: str, skills: list[dict]) -> bool:\n    \"\"\"True when the name appears with a non-blank name in the owner's index.\"\"\"\n    return any((s.get(\"name\") or \"\").strip() == skill_name.strip() for s in skills)","tryCatchPattern":null,"preventionTips":["Fetch the invocable index immediately before invoking, never from a cached session list.","Normalize whitespace and casing when matching user-typed slash names.","On any 404 from invoke, refresh the skill list once instead of retrying."],"tags":["skills","http-404","slash-invocation","ownership","fastapi"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}