From 6625112e51200733de9dde988e3ad59a57259628 Mon Sep 17 00:00:00 2001 From: McAmner Date: Sat, 8 Aug 2026 19:37:29 +0200 Subject: [PATCH 1/3] chore(skills): make the repo's skills discoverable by Claude Code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MQ convention keeps skills in /skills/, but Claude Code only discovers them under /.claude/skills/. Every repo in the stack therefore carried a full set of skills that no agent could load — 63 across the eight of them. mq-skills.py reported them all "ok, indexed", because it validates the MQ convention and knows nothing about Claude Code's search path. Relative symlinks, so a fresh clone is wired the same way. This mirrors how .agents/skills entries were already linked by hand in mq-agent and macos-scripts. Co-Authored-By: Claude Opus 5 --- .claude/skills/audit-macos-scripts-menus | 1 + .claude/skills/docs-maintainer | 1 + .claude/skills/mqlaunch-command-surface | 1 + .claude/skills/mqlaunch-menu-template | 1 + .claude/skills/release-readiness | 1 + .claude/skills/repo-health-brief | 1 + .claude/skills/terminal-ui-polisher | 1 + .claude/skills/vector-store-maintainer | 1 + 8 files changed, 8 insertions(+) create mode 120000 .claude/skills/audit-macos-scripts-menus create mode 120000 .claude/skills/docs-maintainer create mode 120000 .claude/skills/mqlaunch-command-surface create mode 120000 .claude/skills/mqlaunch-menu-template create mode 120000 .claude/skills/release-readiness create mode 120000 .claude/skills/repo-health-brief create mode 120000 .claude/skills/terminal-ui-polisher create mode 120000 .claude/skills/vector-store-maintainer diff --git a/.claude/skills/audit-macos-scripts-menus b/.claude/skills/audit-macos-scripts-menus new file mode 120000 index 00000000..de955f5d --- /dev/null +++ b/.claude/skills/audit-macos-scripts-menus @@ -0,0 +1 @@ +../../.agents/skills/audit-macos-scripts-menus \ No newline at end of file diff --git a/.claude/skills/docs-maintainer b/.claude/skills/docs-maintainer new file mode 120000 index 00000000..c345fce9 --- /dev/null +++ b/.claude/skills/docs-maintainer @@ -0,0 +1 @@ +../../skills/docs-maintainer \ No newline at end of file diff --git a/.claude/skills/mqlaunch-command-surface b/.claude/skills/mqlaunch-command-surface new file mode 120000 index 00000000..66d08d08 --- /dev/null +++ b/.claude/skills/mqlaunch-command-surface @@ -0,0 +1 @@ +../../skills/mqlaunch-command-surface \ No newline at end of file diff --git a/.claude/skills/mqlaunch-menu-template b/.claude/skills/mqlaunch-menu-template new file mode 120000 index 00000000..f98685b3 --- /dev/null +++ b/.claude/skills/mqlaunch-menu-template @@ -0,0 +1 @@ +../../skills/mqlaunch-menu-template \ No newline at end of file diff --git a/.claude/skills/release-readiness b/.claude/skills/release-readiness new file mode 120000 index 00000000..b79e6f2a --- /dev/null +++ b/.claude/skills/release-readiness @@ -0,0 +1 @@ +../../skills/release-readiness \ No newline at end of file diff --git a/.claude/skills/repo-health-brief b/.claude/skills/repo-health-brief new file mode 120000 index 00000000..fa2469eb --- /dev/null +++ b/.claude/skills/repo-health-brief @@ -0,0 +1 @@ +../../skills/repo-health-brief \ No newline at end of file diff --git a/.claude/skills/terminal-ui-polisher b/.claude/skills/terminal-ui-polisher new file mode 120000 index 00000000..158f220f --- /dev/null +++ b/.claude/skills/terminal-ui-polisher @@ -0,0 +1 @@ +../../skills/terminal-ui-polisher \ No newline at end of file diff --git a/.claude/skills/vector-store-maintainer b/.claude/skills/vector-store-maintainer new file mode 120000 index 00000000..3a4ebd0f --- /dev/null +++ b/.claude/skills/vector-store-maintainer @@ -0,0 +1 @@ +../../skills/vector-store-maintainer \ No newline at end of file From 25af08b9022dbf4a7120b5d8b8447bb38209f15a Mon Sep 17 00:00:00 2001 From: McAmner Date: Sat, 8 Aug 2026 20:13:00 +0200 Subject: [PATCH 2/3] feat(skills): audit whether a skill is loadable, not just indexed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mq-skills.py reported all 63 skills in the stack as "ok, indexed" while not one of them could be loaded. It validated the MQ convention — skills/ plus a SKILLS.md entry — and knew nothing about Claude Code's search path, which is .claude/skills/. Being indexed says a human can find the file; it says nothing about whether a tool can run it. audit now prints a third column and validate warns on two new states: not-discoverable (no entry under .claude/skills) and broken-link (an entry with no SKILL.md behind it). The fix hints name the actual remedy. The smoke test builds a scratch repo with one skill, asserts it is reported while unlinked and not reported once linked — so the check tracks reality instead of always complaining. Co-Authored-By: Claude Opus 5 --- tests/skills-repos-smoke.sh | 57 ++++++++++++++++++++++++++++++------- tools/scripts/mq-skills.py | 42 ++++++++++++++++++++++++--- 2 files changed, 85 insertions(+), 14 deletions(-) diff --git a/tests/skills-repos-smoke.sh b/tests/skills-repos-smoke.sh index d8dacb87..20a852fa 100755 --- a/tests/skills-repos-smoke.sh +++ b/tests/skills-repos-smoke.sh @@ -12,32 +12,32 @@ DOC="$ROOT/docs/COMMANDS.md" echo "SMOKE: skills and repos command surface" -echo "[1/10] scripts exist and are executable" +echo "[1/12] scripts exist and are executable" test -x "$SKILLS" test -x "$REPOS" -echo "[2/10] script syntax checks" +echo "[2/12] script syntax checks" PYTHONPYCACHEPREFIX="${TMPDIR:-/tmp}/mqlaunch-pycache" python3 -m py_compile "$SKILLS" "$REPOS" -echo "[3/10] command-mode syntax check" +echo "[3/12] command-mode syntax check" bash -n "$CMD" -echo "[4/10] launcher syntax check" +echo "[4/12] launcher syntax check" zsh -n "$LAUNCHER" -echo "[5/10] tools menu syntax check" +echo "[5/12] tools menu syntax check" bash -n "$TOOLS_MENU" -echo "[6/10] command-mode routes skills and repos" +echo "[6/12] command-mode routes skills and repos" grep -q "mq-skills.py" "$CMD" grep -q "mq-repos.py" "$CMD" # Same as brain-bridge: the skills/repos case arms live in command mode (step 6), # and mqlaunch.sh reaches them by sourcing that module. -echo "[7/10] main launcher reaches that routing (sources command mode)" +echo "[7/12] main launcher reaches that routing (sources command mode)" grep -q 'source "\$BASE_DIR/terminal/launchers/mqlaunch-command-mode.sh"' "$LAUNCHER" -echo "[8/10] tools menu exposes ecosystem actions" +echo "[8/12] tools menu exposes ecosystem actions" # Asserted as reachable actions rather than as label text. The labels were # "Skills audit" and "Repos diff" while those sat flat in the Tools menu; they # are "Audit" and "Diff summary" inside the Skills and Repos submenus now, and @@ -50,13 +50,13 @@ for handler in run_mq_skills_audit run_mq_repos_diff_summary; do } done -echo "[9/10] docs mention commands" +echo "[9/12] docs mention commands" grep -q "mqlaunch skills audit" "$DOC" grep -q "mqlaunch skills validate --ecosystem" "$DOC" grep -q "mqlaunch repos status" "$DOC" grep -q "mqlaunch repos diff-summary" "$DOC" -echo "[10/10] scripts run read-only summaries" +echo "[10/12] scripts run read-only summaries" "$SKILLS" validate >/tmp/mq-skills-validate.out "$SKILLS" validate --ecosystem >/tmp/mq-skills-validate-ecosystem.out "$REPOS" list >/tmp/mq-repos-list.out @@ -79,4 +79,41 @@ else echo " skip: sibling MQ repos not checked out; listing assertions need them" fi +echo "[11/12] audit reports whether each skill is discoverable by Claude Code" +# The blind spot this closes: mq-skills.py called all 63 skills in the stack +# "ok, indexed" while not one of them was loadable. It validated the MQ +# convention (skills/ plus a local index) and knew nothing about Claude Code's +# search path, which is .claude/skills/. An index nobody reads is not discovery. +out="$("$SKILLS" audit --repo macos-scripts)" +grep -q "discoverable" <<<"$out" + +echo "[12/12] an unlinked skill is reported, and a linked one is not" +# A scratch repo, so the assertion is about the checker rather than about +# whichever skills happen to be wired up in the real tree today. +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +mkdir -p "$work/repo/skills/ghost-skill" "$work/repo/.claude/skills" +cat >"$work/repo/skills/ghost-skill/SKILL.md" <<'SKILL' +--- +name: ghost-skill +description: Exists on disk, indexed, and reachable by nobody. +--- +SKILL +printf 'skills/ghost-skill/SKILL.md\n' >"$work/repo/SKILLS.md" + +out="$("$SKILLS" audit --repo "$work/repo" 2>&1)" +grep -q "not-discoverable" <<<"$out" || { + echo "FAIL: an unlinked skill was not reported as undiscoverable" >&2 + printf '%s\n' "$out" >&2 + exit 1 +} + +ln -s ../../skills/ghost-skill "$work/repo/.claude/skills/ghost-skill" +out="$("$SKILLS" audit --repo "$work/repo" 2>&1)" +grep -q "not-discoverable" <<<"$out" && { + echo "FAIL: a linked skill is still reported as undiscoverable" >&2 + printf '%s\n' "$out" >&2 + exit 1 +} + echo "OK: skills and repos command surface smoke test passed" diff --git a/tools/scripts/mq-skills.py b/tools/scripts/mq-skills.py index 1d1d9513..ead5eaa5 100755 --- a/tools/scripts/mq-skills.py +++ b/tools/scripts/mq-skills.py @@ -140,6 +140,26 @@ def index_state(repo: Path, skill: Skill) -> str: return "not-indexed" +def discovery_state(repo: Path, skill: Skill) -> str: + """Can an agent actually load this skill? + + The MQ convention keeps skills in /skills/ and indexes them in + SKILLS.md. Claude Code discovers skills somewhere else entirely: under + /.claude/skills/. Those two facts were never checked against each + other, so the whole stack — 63 skills across eight repos — audited as "ok, + indexed" while not one of them could be loaded. Being indexed says a human + can find the file; this says a tool can run it. + """ + entry = repo / ".claude" / "skills" / skill.path.parent.name + if entry.is_symlink() and not entry.exists(): + return "broken-link" + if not entry.exists(): + return "not-discoverable" + if not (entry / "SKILL.md").is_file(): + return "broken-link" + return "discoverable" + + def roadmap_text(repo: Path) -> str: parts = [] for path in (repo / "ROADMAP.md", repo / "docs" / "ROADMAP.md"): @@ -153,12 +173,17 @@ def audit(args: argparse.Namespace) -> int: for repo in repo_paths(args.repo): skills = find_skills(repo) states = [index_state(repo, skill) for skill in skills] + discovery = [discovery_state(repo, skill) for skill in skills] indexed_count = sum(1 for state in states if state == "indexed") - print(f"{repo.name}: {len(skills)} skill(s), {indexed_count} indexed") - for skill, idx in zip(skills, states): + loadable = sum(1 for state in discovery if state == "discoverable") + print( + f"{repo.name}: {len(skills)} skill(s), {indexed_count} indexed, " + f"{loadable} discoverable" + ) + for skill, idx, disc in zip(skills, states, discovery): status = "ok" if skill.name and skill.description else "frontmatter-missing" - print(f" - {skill.path.parent.name}: {status}, {idx}") - if status != "ok" or idx != "indexed": + print(f" - {skill.path.parent.name}: {status}, {idx}, {disc}") + if status != "ok" or idx != "indexed" or disc != "discoverable": any_warn = True text = roadmap_text(repo) @@ -201,6 +226,11 @@ def validate(args: argparse.Namespace) -> int: warnings.append(f"{repo.name}/{folder}: not referenced by local skill index") elif idx == "no-index-file": warnings.append(f"{repo.name}/{folder}: repo has no local skill index") + disc = discovery_state(repo, skill) + if disc == "not-discoverable": + warnings.append(f"{repo.name}/{folder}: not linked into .claude/skills") + elif disc == "broken-link": + warnings.append(f"{repo.name}/{folder}: .claude/skills entry does not resolve") if args.ecosystem: by_name: dict[str, list[str]] = {} @@ -243,6 +273,10 @@ def fix_suggestion(message: str) -> str: return "add the skill to SKILLS.md or skills/platform-skills.md" if "repo has no local skill index" in message: return "create SKILLS.md and list the repo skills" + if "not linked into .claude/skills" in message: + return "ln -s ../../skills/ .claude/skills/ — indexed is not the same as loadable" + if ".claude/skills entry does not resolve" in message: + return "repoint or remove the .claude/skills entry; it has no SKILL.md behind it" if "repo not found" in message: return "clone the repo locally or pass --repo with the correct path" if "duplicate skill name" in message: From 36d49dc81d43f778def0ab85620a9fee0c4f4fec Mon Sep 17 00:00:00 2001 From: McAmner Date: Sat, 8 Aug 2026 21:02:31 +0200 Subject: [PATCH 3/3] fix(tests): point the discoverability step at this checkout Step 11 passed `--repo macos-scripts`, and a bare name resolves under $HOME. On a CI runner the repo lives in $GITHUB_WORKSPACE, so the audit found no repos, printed nothing, and the grep failed against a checker that was working correctly. Step 10 already skips its listing assertions for the same reason. Reproduced by running the suite with HOME pointed at a scratch dir, which fails before this change and passes after. Co-Authored-By: Claude Opus 5 --- tests/skills-repos-smoke.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/skills-repos-smoke.sh b/tests/skills-repos-smoke.sh index 20a852fa..f9a7aff7 100755 --- a/tests/skills-repos-smoke.sh +++ b/tests/skills-repos-smoke.sh @@ -84,7 +84,10 @@ echo "[11/12] audit reports whether each skill is discoverable by Claude Code" # "ok, indexed" while not one of them was loadable. It validated the MQ # convention (skills/ plus a local index) and knew nothing about Claude Code's # search path, which is .claude/skills/. An index nobody reads is not discovery. -out="$("$SKILLS" audit --repo macos-scripts)" +# --repo takes a path, and it has to be this checkout: a bare name resolves +# under $HOME, which on a CI runner is not where the repo lives. Step 10 already +# skips its listing assertions for exactly that reason. +out="$("$SKILLS" audit --repo "$ROOT")" grep -q "discoverable" <<<"$out" echo "[12/12] an unlinked skill is reported, and a linked one is not"