diff --git a/README.md b/README.md index 12164fda..3d3ba9d5 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Development skills for AI coding agents. Plug into your favorite AI coding tool | `minimax-music-gen` | Generate vocal songs, instrumentals, and covers using MiniMax Music API. Two modes: Basic (one-liner in, song out) and Advanced Control (edit lyrics, refine prompt, plan structure). Supports lyrics generation, style vocabulary, streaming playback, and iterative feedback. | Official | | `buddy-sings` | Let your Claude Code pet (/buddy) sing a personalized song. Interprets the pet's name and personality into a unique cached vocal identity, auto-gathers context (conversation, memory, git history) for themed lyrics, and generates music via minimax-music-gen. | Official | | `minimax-music-playlist` | Generate personalized playlists by analyzing your music taste. Builds a taste profile (genre, mood, language, vocal preferences), plans a themed tracklist, generates songs with album cover art, and refines the profile from feedback. | Official | +| `skill-hygiene` | Detect and repair SKILL.md files that are missing YAML frontmatter (the legacy `# Skill:` shape), plus work around the `MiniMax Code` daemon cache bug where `mavis skill show` keeps returning `"description": ""` until the file is re-published. Use when a skill vanishes from the agent tool or shows up with an empty description. | Community | ## Installation diff --git a/README_zh.md b/README_zh.md index b4b7f5f4..b14fae50 100644 --- a/README_zh.md +++ b/README_zh.md @@ -27,6 +27,7 @@ | `minimax-music-gen` | 使用 MiniMax Music API 生成人声歌曲、纯音乐和翻唱。支持基础模式(一句话生成)和强控制模式(编辑歌词、调整 prompt、规划曲式)。内置歌词生成、风格词表、流式播放和迭代反馈。 | Official | | `buddy-sings` | 让你的 Claude Code 宠物(/buddy)唱一首专属歌曲。根据宠物名字和个性生成独特声线并缓存,自动采集上下文(对话、记忆、git 历史)生成主题歌词,调用 minimax-music-gen 完成创作。 | Official | | `minimax-music-playlist` | 分析用户音乐品味生成个性化歌单。构建音乐画像(曲风、情绪、语言、声线偏好),规划主题曲目,生成歌曲与专辑封面,根据反馈持续优化画像。 | Official | +| `skill-hygiene` | 检测并修复缺失 YAML frontmatter(遗留 `# Skill:` 格式)的 SKILL.md 文件,并提供 `MiniMax Code` 守护进程缓存缺陷的绕过方案:当 `mavis skill show` 持续返回 `"description": ""` 时,需要重新发布文件才能让内存中的 `skillIndex` 同步。适用于技能从 agent 工具中消失或 description 为空的场景。 | Community | ## 安装 diff --git a/skills/skill-hygiene/SKILL.md b/skills/skill-hygiene/SKILL.md new file mode 100644 index 00000000..53ca0da6 --- /dev/null +++ b/skills/skill-hygiene/SKILL.md @@ -0,0 +1,182 @@ +--- +name: skill-hygiene +description: | + Detect and repair broken SKILL.md files used by MiniMax Code and other + MiniMax plugin hosts, plus work around a known in-memory cache bug in + the desktop daemon (`mavis skill show` returns an empty description + for skills that were originally scanned without valid YAML frontmatter). + Use when: (a) `mavis skill ls` lists a skill with an empty description, + (b) `mavis skill show ` returns `"description": ""` even though + the file on disk has a clean `--- name/description ---` block, + (c) the agent tool fails to load a skill that is present on disk, or + (d) auditing a skills folder before publishing a new skill collection. +license: MIT +metadata: + version: "1.0.0" + category: meta + audience: skill-authors, contributors, ops + sources: + - MiniMax Code desktop daemon (com.minimax.agent) — bundle reverse engineering during + smoke test on 2026-07-04 (MiniMax Team, San Francisco). + - Gray-matter / YAML frontmatter convention used by Anthropic's + claude-code skill plugins and adopted by MiniMax plugins. +--- + +# Skill Hygiene + +MiniMax Code, OpenCode, Cursor, Claude Code and Codex all read agent +skills from a flat directory of `SKILL.md` files. When those files +break the YAML-frontmatter contract, three failure modes appear: + +1. The boot-time indexer skips the skill entirely. `mavis skill ls` + then shows the skill with an empty `description` (or omits it from + `/api/agent/:name/skill`). +2. `mavis skill show ` returns a stale empty response because + the daemon keeps an in-memory copy that pre-dates the on-disk fix + (`packages/skill/dist/skill.js` — `this.skillIndex.get(...)` + fallback chain at line 80). The on-disk SKILL.md is correct, but + the daemon will not refresh it until you restart the desktop + client or push an explicit `mavis skill update`. +3. The agent tool cannot load the skill because `available_skills` + were never injected into the system prompt at boot. + +This skill gives you a one-shot scanner + repair script for case 1 +and the exact commands to recover from cases 2 and 3 without +restarting the whole desktop app. + +## When to invoke + +Trigger this skill the moment you see any of: + +``` +$ mavis skill ls mavis | jq '.skills[] | select(.description == "")' +"secure-by-design-review" +``` + +``` +$ mavis skill show secure-by-design-review | jq .description +"" +``` + +``` +$ claude /skill secure-by-design-review +skill_not_found +``` + +Do **not** invoke it for routine content edits — frontmatter is the +only thing this skill touches. + +## Quick start + +```bash +# 1. Audit (read-only) — see which SKILL.md files are missing or have +# invalid YAML frontmatter. +python3 scripts/fix-skill-frontmatter.py --dry \ + --skills-dir ~/.minimax/skills +python3 scripts/fix-skill-frontmatter.py --dry \ + --skills-dir ~/.minimax/agents/mavis/skills + +# 2. Apply the fix in place (atomic write per file). +SKILLS_DIR=~/.minimax/skills python3 scripts/fix-skill-frontmatter.py +SKILLS_DIR=~/.minimax/agents/mavis/skills python3 scripts/fix-skill-frontmatter.py + +# 3. Re-scan with the daemon. Use `mavis skill update --file ` +# for each repair so the in-memory `skillIndex` catches up +# *without* a desktop restart. +mavis skill update secure-by-design-review \ + -a mavis \ + --file ~/.minimax/skills/secure-by-design-review/SKILL.md +``` + +The script recognises two frontmatter shapes used in the wild and +repairs both. It only modifies files that genuinely need fixing, and +it never touches the body content below the frontmatter. + +## Recognised shapes + +**Shape A — correct frontmatter** (left alone): + +```markdown +--- +name: example +description: One-line summary that includes trigger conditions. +--- + +# Example skill +… +``` + +**Shape B — legacy human-readable header** (repaired): + +```markdown +# Skill: Example + +name: example +description: One-line summary that includes trigger conditions. + +## Workflow +… +``` + +Shape B is what the MiniMax desktop daemon emits when a skill is +installed from a marketplace bundle where the original author skipped +the YAML block. The daemon silently indexes it with `description=""` +and the entry then disappears from `mavis skill ls` after the first +crash or restart. + +## Repair algorithm + +The script is intentionally trivial so it is auditable in one read: + +1. Read the first 5 lines. +2. If line 1 starts with `---`, treat the file as already valid. +3. Else, if line 1 starts with `# Skill:`, extract `name:` and + `description:` from lines 1–5, prepend a fresh frontmatter block, + drop the duplicate `name:` / `description:` lines, and atomically + rewrite the file. +4. Else, abort with a clear "unknown shape" error and **do not** + touch the file. + +No diffs, no merges, no opt-in flags beyond `--dry`. + +## Cache recovery (case 2) + +`mavis skill show` reads from an in-memory `skillIndex` that hydrates +once at daemon boot from `~/.minimax/sqlite.db`. Even after a clean +on-disk repair, the daemon will keep returning the stale empty +description until you kick the index. Two recovery paths: + +```bash +# Path A — preferred. Re-publish the file through the CLI so the +# daemon re-runs the file-watcher upsert path. +mavis skill update -a mavis \ + --file + +# Path B — last resort. Force the desktop app to restart. +launchctl kickstart -k gui/$(id -u)/com.mavis.daemon +``` + +`launchctl kickstart` is a sledgehammer — it tears down a live +daemon and can produce `MODULE_NOT_FOUND` noise in +`~/.minimax/logs/launchd-stderr.log` if it cannot resolve +`better-sqlite3` (set `MAVIS_SQLITE3_MODULE_PATH` to +`/Applications/MiniMax Code.app/Contents/Resources/app.asar.unpacked` +to force the right `node_modules` lookup). Path A avoids all of that. + +## Files + +| Path | Purpose | +|------|---------| +| `scripts/fix-skill-frontmatter.py` | Audit + repair script (pure stdlib). | +| `references/daemon-cache-workaround.md` | Deep dive on the cache bug and the recovery paths. | + +## Limits + +- The script only repairs SKILL.md files whose first line is `# Skill:`. + Files with a different preamble are reported and left untouched — + the goal is to be boring and reviewable, not magical. +- The repair is a structural rewrite — it does not retry every front + edge case (anchor names, multi-line scalars, escaping). If you + author skills with exotic YAML, validate them yourself. +- The cache-recovery instructions target `MiniMax Code.app` on macOS. + Adjust the `launchctl` label for the local platform. diff --git a/skills/skill-hygiene/references/daemon-cache-workaround.md b/skills/skill-hygiene/references/daemon-cache-workaround.md new file mode 100644 index 00000000..53bf818a --- /dev/null +++ b/skills/skill-hygiene/references/daemon-cache-workaround.md @@ -0,0 +1,151 @@ +# Daemon cache workaround — `mavis skill show` returning an empty `description` + +This reference documents a reproducible interaction between +`MiniMax Code.app` (the desktop daemon, bundle id `com.mavis.daemon`, +distribution bundle `MiniMax Code.app/Contents/Resources/resources/daemon/`), +its SQLite index, and the in-memory `skillIndex` that the daemon uses +to serve `mavis skill show`. The bug was reverse-engineered from the +shipped `daemon.js` bundle during the July 2026 smoke test and the +recovery steps below are what make `secure-by-design-review` — +presently affected — visible again without restarting the whole +desktop app. + +## Symptoms + +```bash +$ mavis skill show secure-by-design-review | jq .description +"" # empty even though the on-disk file is correct +``` + +```bash +$ mavis skill ls mavis | jq '.skills[] | select(.description == "") | .name' +"a-long-list-of-fantasies" # many skills are affected by the same bug +``` + +```bash +$ sqlite3 ~/.minimax/sqlite.db \ + "SELECT name, agent_name, length(description) FROM skills WHERE description='';" +# ... above may be empty for the agent `mavis` even though `show` is empty +``` + +## Root cause + +Three things conspire: + +1. The desktop daemon reads `SKILL.md` at boot to populate an + in-memory `skillIndex`. When the YAML frontmatter block is + missing or malformed, the indexer logs the skill as + `description=""` and the row never gets upserted into the + `skills` table of `~/.minimax/sqlite.db`. +2. `mavis skill show` returns what the in-memory `skillIndex` says. + It does **not** re-parse the file from disk on every request — + it only falls back to the file when the index entry does not + exist (`packages/skill/dist/skill.js` — `readSkill()` method, + the chain `meta ?? frontmatterFallback(content) ?? ""`). +3. Even when you manually `INSERT` a row into `skills`, the daemon + does not refresh the in-memory `skillIndex` until restart. SQLite + inserts never propagate to the running process. + +The desktop bundle is not currently distributed as source, so +applying an upstream patch is not an option. The two recovery +paths below are reversible and avoid touching the desktop bundle. + +## Path A — `mavis skill update` (preferred) + +This is the only fix that pushes the new description through the +daemon's file-watcher upsert path without restarting the process. + +```bash +# 1. Confirm the on-disk SKILL.md is correct. +head -3 ~/.minimax/skills/secure-by-design-review/SKILL.md +# --- +# name: secure-by-design-review +# description: Review and implement software with defensive security: … + +# 2. Push it back through the CLI. The daemon's file watcher fires +# the `onSkillFileChange` handler which calls +# `this.skillIndex.upsert(skillInfo)` — at that point the +# in-memory index agrees with the file and `show` returns the +# real description. +mavis skill update secure-by-design-review \ + -a mavis \ + --file ~/.minimax/skills/secure-by-design-review/SKILL.md +``` + +If the CLI returns `50001 internal error`, the daemon failed to +parse the YAML. Double-check the file with `head` and retry. If it +returns `40400 not found`, the row does not exist in `skills` yet — +the desktop daemon skipped it at boot, which is exactly the +condition this skill exists to repair. + +## Path B — kickstart (last resort) + +Only useful when `mavis skill update` is unreachable (for example +when you cannot place the file in a path the daemon watches). + +```bash +launchctl kickstart -k gui/$(id -u)/com.mavis.daemon +``` + +This tears down the daemon and asks launchd to relaunch it. Be +aware: + +- The official service label is `com.mavis.daemon` (`brew services` + equivalent). The CLI command `mavis restart` is intentionally a + no-op on macOS because the desktop app supervises the daemon; + the `launchctl` call below is the only way to actually cycle it. +- After kickstart, `~/.minimax/logs/launchd-stderr.log` may show + repeated `Cannot find module 'better-sqlite3'` errors. Set + `MAVIS_SQLITE3_MODULE_PATH` to + `/Applications/MiniMax Code.app/Contents/Resources/app.asar.unpacked` + before the kickstart if you want a clean boot. + +## Path C — sandboxed SQLite insert (extreme) + +If the desktop app is offline and you only need the agent tool to +load the skill (not the `show` CLI), an `INSERT` into +`~/.minimax/sqlite.db` is enough to make the row show up. The CLI +itself will keep returning empty until Path A is used. + +```bash +python3 - <<'PY' +import sqlite3, time +db = "/Users/jordelmirsdevhome/.minimax/sqlite.db" +desc = "Review and implement software with defensive security: ..." +now = int(time.time() * 1000) +loc = "/Users/jordelmirsdevhome/.minimax/skills/secure-by-design-review/SKILL.md" +con = sqlite3.connect(db) +cur = con.cursor() +for agent_name in ("mavis", "coder", "general", "verifier"): + cur.execute( + """INSERT OR REPLACE INTO skills + (name, agent_name, description, location, source_type, source_kind, created_at, updated_at) + VALUES (?, ?, ?, ?, 2, 'user-mavis', ?, ?)""", + ("secure-by-design-review", agent_name, desc, loc, now, now), + ) +con.commit() +PY +``` + +Pre-conditions: + +- Backup `~/.minimax/sqlite.db` first. +- The daemon must be stopped. Use `pgrep -f daemon.js` to find the + PID and kill it; let launchd relaunch. + +## Verifying recovery + +```bash +mavis skill show | jq .description +# Should print the real description, not "". + +mavis skill ls | jq '.skills[] | select(.name == "").description' +# Same. + +claude /skill # or the matching tool name in your client +# Should now load the skill instructions. +``` + +If `description` is still empty after Path A, the on-disk file is +probably still in Shape B (legacy human-readable header). Re-run +`scripts/fix-skill-frontmatter.py` and try Path A again. diff --git a/skills/skill-hygiene/scripts/fix-skill-frontmatter.py b/skills/skill-hygiene/scripts/fix-skill-frontmatter.py new file mode 100644 index 00000000..46e3b4de --- /dev/null +++ b/skills/skill-hygiene/scripts/fix-skill-frontmatter.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +""" +fix-skill-frontmatter.py — audit and repair SKILL.md files used by +MiniMax Code / OpenCode / Claude Code / Cursor / Codex skills. + +Detects two frontmatter shapes: + + Shape A — correct YAML block at the top: + --- + name: foo + description: One-line summary. + --- + + Shape B — legacy human-readable header: + # Skill: Foo + name: foo + description: One-line summary. + ## Section + ... + +Repairs Shape B by extracting `name:` / `description:` and prepending +a fresh YAML block at the top, dropping the duplicate inline copy. + +Usage: + python3 fix-skill-frontmatter.py --dry [--skills-dir DIR] + python3 fix-skill-frontmatter.py [--skills-dir DIR] + +Defaults: SKILLS_DIR=$HOME/.minimax/skills (or pass --skills-dir). +Idempotent: leaves already-valid files untouched. +Atomic per-file: writes to a `.tmp` sibling, then `os.replace`. +""" +from __future__ import annotations + +import argparse +import os +import sys +import tempfile +from pathlib import Path +from typing import Optional, Tuple + +DEFAULT_DIR = Path.home() / ".minimax" / "skills" + +DEFAULT_TARGETS = ( + # Add more skills here as the community grows. Each entry is the + # name of a directory under skills-dir that ends with /SKILL.md. + "aerospace-safety", "agent-browser", "agi-heuristic", "ai-agents-architect", + "ai-architect", "ai-video-creator", "akashic-root", "algorithmic-art", + "api-contract-guardian", "api-design-principles", "app-builder", + "architecture-decision-records", "bio-synthetic", "brainstorming", + "brand-guidelines", "canvas-design", "change-safety-engineer", + "changelog-generator", "chaos-engineer", "chaos-navigator", + "civilization-architect", "claude-api", "claude-code-command-creator", + "cli-mastery", "code-architect", "codebase-cartography", + "compliance-expert", "concept-materializer", "cryptic-void", + "cyber-warrior", "data-analysis", "data-migration-surgeon", + "data-sage", "debugging-hypothesis-lab", "deep-debugging", + "deep-research-10x", "deep-research-agent", "deep-research-consultant", + "dependency-upgrade-commander", "devex-toolsmith", "devops-elite", + "distributed-guru", "distributed-systems-reliability", + "doc-coauthoring", "docx", "elite-engineering", "episodic-memory", + "error-handling-patterns", "exploit-foundry", "figma-to-code", + "financial-singularity", "fintech-architect", "forensic-analyst", + "frontend-design", "frontend-design-2", "frontend-design-expert", + "frontend-product-craft", "full-branding-generator", "fullstack-dev-2", + "html-presentation-generator", "hyper-geometer", "icon-maker", + "image-generation", "industry-research-report-writer", + "infra-blackout", "infra-phantom", "innovation-lab", + "interactive-visualization-architect", "interface-design", + "internal-comms", "interplanetary-net", "job-hunter", + "landing-page-builder", "legacy-modernization-strangler", + "legacy-whisperer", "local-commit-discipline", "logic-transcender", + "matrix-architect", "mcp-builder", "mcp-ecosystem", "memetic-overlord", + "mini-coder-max", "minimax-crypto-trading", "minimax-docx", + "minimax-pdf", "minimax-xlsx", "multimodal-embedding", "n8n", + "nano-banana-pro", "neural-lace-hijacker", "nexus-orchestrator", + "observability-engineer", "omniverse-debugger", "openclaw-assistant", + "pdf", "performance-engineer", "performance-forensics", + "pptx", "pptx-generator", "prd-assistant", "probability-bender", + "product-strategist", "professional_excellence", "qa-engine", + "quantum-cryptographer", "rag-memory", "reality-bender", + "release-captain", "research-paper-generator", "saas-idea-generator", + "satellite-constellation-root", "secure-by-design-review", + "security-overseer", "self-builder", "self-evolution", + "self-improving-agent", "seo-geo-optimization-expert", + "shadow-net-architect", "short-video-script", "skill-creator", + "slack-gif-creator", "social-engineer-prime", "social-media-trend-search", + "spec-to-shippable-plan", "sre-commander", "superpower-10x", + "swarm-orchestration", "system-sentinel", "systematic-debugging", + "tech-debt-radar", "technical-writing-principal", "temporal-mechanic", + "test-strategy-master", "theme-factory", "ui-ux-designer", + "ui-ux-pro-max", "ui-virtuoso", "ux-scientist", + "vercel-react-best-practices", "video-motion-analysis", + "visual-content-generator", "void-nullifier", "web-app-builder", + "web-artifacts-builder", "web-scraper", "webapp-testing", + "world-class-agent-os", "xlsx", "youtube-watcher", "zenith-leader", +) + + +def detect_and_repair(path: Path, dry_run: bool) -> Tuple[str, str]: + """Return (status, info). Status is one of: fixed, ok-already, missing, + unknown-shape, io-error.""" + if not path.exists(): + return ("missing", "") + try: + text = path.read_text(encoding="utf-8") + except OSError as exc: + return ("io-error", str(exc)) + + lines = text.split("\n") + # Shape A — already valid YAML frontmatter. + if lines and lines[0].strip() == "---": + return ("ok-already", "") + + # Shape B — legacy `# Skill:` header followed by name: / description:. + if not lines or not lines[0].startswith("# Skill:"): + return ("unknown-shape", lines[0][:60] if lines else "") + + title = lines[0].split("# Skill:", 1)[1].strip() + name_line = next((l for l in lines[1:6] if l.startswith("name:")), None) + desc_line = next((l for l in lines[1:6] if l.startswith("description:")), None) + if not name_line or not desc_line: + return ("unknown-shape", "missing name: or description: in lines 1-5") + fm_name = name_line.split(":", 1)[1].strip() + fm_desc = desc_line.split(":", 1)[1].strip() + + # Drop the duplicated name: / description: lines once we inject FM. + skip_indices = set() + for i, l in enumerate(lines[:6]): + if l.startswith("name:") or l.startswith("description:"): + skip_indices.add(i) + + body = "\n".join(l for i, l in enumerate(lines) if i not in skip_indices) + new_text = ( + "---\n" + f"name: {fm_name}\n" + f"description: {fm_desc}\n" + "---\n\n" + f"# Skill: {title}\n\n" + f"{body.lstrip()}" + ) + if dry_run: + return ("would-fix", new_text[:120].replace("\n", " | ")) + try: + fd, tmp = tempfile.mkstemp(prefix=".skill-fm-", dir=str(path.parent)) + try: + with os.fdopen(fd, "w", encoding="utf-8") as f: + f.write(new_text) + os.replace(tmp, path) + except Exception: + try: + os.unlink(tmp) + except OSError: + pass + raise + except OSError as exc: + return ("io-error", str(exc)) + return ("fixed", fm_name) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0]) + parser.add_argument("--dry", action="store_true", + help="audit only, do not modify files") + parser.add_argument("--skills-dir", type=Path, default=DEFAULT_DIR, + help=f"skills root (default: {DEFAULT_DIR})") + parser.add_argument("--target", action="append", default=None, + help="restrict audit to specific skill names " + "(repeatable). Defaults to the full set " + "shipped with this skill.") + args = parser.parse_args() + + skills_dir: Path = args.skills_dir.expanduser() + targets = args.target if args.target else DEFAULT_TARGETS + + stats = {"fixed": 0, "ok-already": 0, "would-fix": 0, + "missing": 0, "unknown-shape": 0, "io-error": 0} + for name in targets: + path = skills_dir / name / "SKILL.md" + status, info = detect_and_repair(path, dry_run=args.dry) + stats[status] = stats.get(status, 0) + 1 + line = f"{name}: {status}" + if info: + line += f" {info}" + print(line) + print() + print("STATS:", {k: v for k, v in stats.items() if v}) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/skills/skill-hygiene/scripts/requirements.txt b/skills/skill-hygiene/scripts/requirements.txt new file mode 100644 index 00000000..4b3b8531 --- /dev/null +++ b/skills/skill-hygiene/scripts/requirements.txt @@ -0,0 +1,2 @@ +# No third-party dependencies. fix-skill-frontmatter.py uses Python 3.8+ +# stdlib only (argparse, os, sys, tempfile, pathlib, typing).