From 06cdf3657066a8a018b62d17f1f87a7f0c3adc1f Mon Sep 17 00:00:00 2001 From: Yibo Cai Date: Sun, 18 Oct 2026 22:47:35 -0700 Subject: [PATCH] fix(nano): list on-disk skills using /SKILL.md as the location nano located skills via `skill.paths`, which is the conditional-activation glob list from frontmatter, not a file location. Ordinary on-disk skills have no `paths`, so they were never listed; skills that did declare `paths` were listed with a glob as their "location", which Read cannot load. Use `/SKILL.md` and skip skills with no skill_root (bundled/MCP). Co-Authored-By: Claude Sonnet 5.5 --- src/nano/prompt.py | 10 +++++++--- tests/nano/test_nano_prompt.py | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/nano/prompt.py b/src/nano/prompt.py index b3dbb961c..46a148d4b 100644 --- a/src/nano/prompt.py +++ b/src/nano/prompt.py @@ -25,6 +25,8 @@ from __future__ import annotations +import os + from pathlib import Path from typing import Any @@ -126,13 +128,15 @@ def _skills_section(cwd: str) -> str: # advertised but unusable (and would only bloat the prompt). Drop them. entries: list[tuple[str, str, str]] = [] for skill in skills: - paths = getattr(skill, "paths", None) or [] - if not paths: + # ``paths`` is the conditional-activation glob list, not a file + # location; the loadable file is ``/SKILL.md``. + root = getattr(skill, "skill_root", None) + if not root: continue entries.append(( _xml_escape(str(getattr(skill, "name", ""))), _xml_escape(str(getattr(skill, "description", "") or "")), - _xml_escape(str(paths[0])), + _xml_escape(os.path.join(str(root), "SKILL.md")), )) if not entries: return "" diff --git a/tests/nano/test_nano_prompt.py b/tests/nano/test_nano_prompt.py index 6180f7d34..963241234 100644 --- a/tests/nano/test_nano_prompt.py +++ b/tests/nano/test_nano_prompt.py @@ -120,3 +120,17 @@ def test_default_path_unchanged_when_nano_off(no_skills, tmp_path): # The maximal prompt keeps its signature sections. assert "# Doing tasks" in joined assert "nano mode" not in joined + + +def test_skills_section_lists_skill_by_root_not_paths(monkeypatch): + """A skill is located at /SKILL.md; ``paths`` is only the + conditional-activation glob list and is usually empty.""" + import src.command_system as cs + from src.nano.prompt import _skills_section + + skill = SimpleNamespace( + name="demo", description="d", paths=[], skill_root="/x/skills/demo" + ) + monkeypatch.setattr(cs, "get_skill_tool_commands", lambda cwd: (skill,)) + out = _skills_section("/tmp") + assert "/x/skills/demo/SKILL.md" in out