From 02c04e3d32a2c16e0fa5e526d2d522ae3f7edc62 Mon Sep 17 00:00:00 2001 From: Ben Fairless Date: Sat, 1 Aug 2026 10:33:23 +0800 Subject: [PATCH 1/2] fix(frontmatter): repair 14 unloadable YAML blocks Twelve files had frontmatter that yaml.safe_load rejects, all from the same cause: an unquoted plain scalar containing ": " inside description. Claude Code responds by loading the body with empty metadata, so the skill keeps working via /name but has no description for the model to match against, and the seven affected agents (where name and description are required) may not load at all. Eleven are fixed by quoting the existing scalar, leaving the text byte-identical. design-system carries both ' and " so quoting would defeat the repo's regex extractors in generate-docs.py and sync-codex-skills.py; its one colon-space is reworded instead ("Precedence:" -> "Precedence is"). Two agents had no frontmatter at all and were being listed with a placeholder description; both now declare name and description. tools is deliberately omitted so they keep inheriting the full set, as before. Assisted-by: Claude Code:claude-opus-5 --- .claude/commands/focused-fix.md | 2 +- business-operations/commands/cs-bizops.md | 2 +- c-level-advisor/arquiteto-de-empresa/agents/cs-arquiteto.md | 2 +- c-level-advisor/executive-mentor/agents/devils-advocate.md | 5 +++++ commercial/commands/cs-commercial.md | 2 +- compliance-os/agents/cs-ai-act-compliance.md | 2 +- compliance-os/agents/cs-aims-iso42001.md | 2 +- compliance-os/agents/cs-fda-qsr-auditor.md | 2 +- engineering/autoresearch-agent/agents/experiment-runner.md | 5 +++++ markdown-html/skills/design-system/SKILL.md | 2 +- markdown-html/skills/md-slides/SKILL.md | 2 +- productivity/andreessen/agents/cs-andreessen.md | 2 +- productivity/deep-work/agents/cs-deep-work.md | 2 +- research/notebooklm/agents/cs-notebooklm.md | 2 +- 14 files changed, 22 insertions(+), 12 deletions(-) diff --git a/.claude/commands/focused-fix.md b/.claude/commands/focused-fix.md index 0f15a49c..db1b0614 100644 --- a/.claude/commands/focused-fix.md +++ b/.claude/commands/focused-fix.md @@ -1,5 +1,5 @@ --- -description: Deep-dive feature repair — systematically fix an entire feature/module. Usage: /focused-fix +description: "Deep-dive feature repair — systematically fix an entire feature/module. Usage: /focused-fix " --- Systematically repair the feature/module at `$ARGUMENTS` using the focused-fix 5-phase protocol. diff --git a/business-operations/commands/cs-bizops.md b/business-operations/commands/cs-bizops.md index 23b9fe57..6a8d7272 100644 --- a/business-operations/commands/cs-bizops.md +++ b/business-operations/commands/cs-bizops.md @@ -1,5 +1,5 @@ --- -description: Top-level Business Operations router. Routes the inquiry to one of six BizOps sub-skills (process, vendor, capacity, comms, knowledge, procurement) and returns a digest. Invokes the business-operations-skills orchestrator (context: fork). +description: "Top-level Business Operations router. Routes the inquiry to one of six BizOps sub-skills (process, vendor, capacity, comms, knowledge, procurement) and returns a digest. Invokes the business-operations-skills orchestrator (context: fork)." argument-hint: "" --- diff --git a/c-level-advisor/arquiteto-de-empresa/agents/cs-arquiteto.md b/c-level-advisor/arquiteto-de-empresa/agents/cs-arquiteto.md index cb7c9eb1..cf672cb8 100644 --- a/c-level-advisor/arquiteto-de-empresa/agents/cs-arquiteto.md +++ b/c-level-advisor/arquiteto-de-empresa/agents/cs-arquiteto.md @@ -1,6 +1,6 @@ --- name: cs-arquiteto -description: Company Architect — a senior chief of staff who builds a business from scratch as an OKF (Open Knowledge Format) bundle: a tree of version-controllable .md files with frontmatter type, links forming a graph, and reserved index.md/log.md. Guides the founder through a 12-phase interview (foundation, strategy, market, financial, sales, marketing, product, operations, tech, people, legal, governance), one phase at a time, at most 3-5 questions per block, confirming before generating each concept. Trigger when the user wants to create, structure, or document an entire company as folders and markdown files, or mentions company as code, company knowledge base for AI, OKF, or knowledge bundle. Works in English. Never dumps the company all at once — it interviews, validates, and builds phase by phase. +description: "Company Architect — a senior chief of staff who builds a business from scratch as an OKF (Open Knowledge Format) bundle: a tree of version-controllable .md files with frontmatter type, links forming a graph, and reserved index.md/log.md. Guides the founder through a 12-phase interview (foundation, strategy, market, financial, sales, marketing, product, operations, tech, people, legal, governance), one phase at a time, at most 3-5 questions per block, confirming before generating each concept. Trigger when the user wants to create, structure, or document an entire company as folders and markdown files, or mentions company as code, company knowledge base for AI, OKF, or knowledge bundle. Works in English. Never dumps the company all at once — it interviews, validates, and builds phase by phase." skills: c-level-advisor/arquiteto-de-empresa/skills/arquiteto-de-empresa domain: c-level model: opus diff --git a/c-level-advisor/executive-mentor/agents/devils-advocate.md b/c-level-advisor/executive-mentor/agents/devils-advocate.md index 8b7fe872..5379ee36 100644 --- a/c-level-advisor/executive-mentor/agents/devils-advocate.md +++ b/c-level-advisor/executive-mentor/agents/devils-advocate.md @@ -1,3 +1,8 @@ +--- +name: devils-advocate +description: "Adversarial reviewer for executive plans, proposals, and decisions. Returns exactly three specific concerns, each severity-rated CRITICAL / HIGH / MEDIUM, with the evidence that would confirm or kill it. Use before committing resources to a plan, before a board or investor presentation, or when feedback so far has been one-sidedly positive. Not a code reviewer." +--- + # Devil's Advocate Agent **Role:** Adversarial thinker. Finds what's wrong before others do. diff --git a/commercial/commands/cs-commercial.md b/commercial/commands/cs-commercial.md index c31e0fdc..ef494855 100644 --- a/commercial/commands/cs-commercial.md +++ b/commercial/commands/cs-commercial.md @@ -1,5 +1,5 @@ --- -description: Top-level Commercial router. Routes the inquiry to one of seven Commercial sub-skills (pricing, deal, partner, channel, policy, RFP, forecast) and returns a digest. Invokes the commercial-skills orchestrator (context: fork). +description: "Top-level Commercial router. Routes the inquiry to one of seven Commercial sub-skills (pricing, deal, partner, channel, policy, RFP, forecast) and returns a digest. Invokes the commercial-skills orchestrator (context: fork)." argument-hint: "" --- diff --git a/compliance-os/agents/cs-ai-act-compliance.md b/compliance-os/agents/cs-ai-act-compliance.md index 6b0634a3..672856d1 100644 --- a/compliance-os/agents/cs-ai-act-compliance.md +++ b/compliance-os/agents/cs-ai-act-compliance.md @@ -1,6 +1,6 @@ --- name: cs-ai-act-compliance -description: EU AI Act (Regulation (EU) 2024/1689) Article-cited compliance operator. Three decisions: AI system risk tier (Article 5 / 6+ Annex III / 50 / minimal), conformity assessment routing (Article 43 Module A vs H + Annex IV docs), per-role obligation matrix (provider/deployer/importer/distributor + GPAI). NOT executive AI strategy (see cs-caio-advisor). NOT a legal substitute (engage counsel for novel cases). +description: "EU AI Act (Regulation (EU) 2024/1689) Article-cited compliance operator. Three decisions: AI system risk tier (Article 5 / 6+ Annex III / 50 / minimal), conformity assessment routing (Article 43 Module A vs H + Annex IV docs), per-role obligation matrix (provider/deployer/importer/distributor + GPAI). NOT executive AI strategy (see cs-caio-advisor). NOT a legal substitute (engage counsel for novel cases)." skills: ra-qm-team/skills/eu-ai-act-specialist domain: compliance-os model: opus diff --git a/compliance-os/agents/cs-aims-iso42001.md b/compliance-os/agents/cs-aims-iso42001.md index 0bec3f4f..880b42b3 100644 --- a/compliance-os/agents/cs-aims-iso42001.md +++ b/compliance-os/agents/cs-aims-iso42001.md @@ -1,6 +1,6 @@ --- name: cs-aims-iso42001 -description: ISO/IEC 42001:2023 AI Management System (AIMS) implementation + internal audit operator. Three decisions: AIMS gaps against Clauses 4-10, AI risk register per Annex A + ISO 23894, Clause 9.2 internal audit plan. NOT executive AI strategy (see cs-caio-advisor). NOT EU AI Act conformity (see cs-ai-act-compliance). +description: "ISO/IEC 42001:2023 AI Management System (AIMS) implementation + internal audit operator. Three decisions: AIMS gaps against Clauses 4-10, AI risk register per Annex A + ISO 23894, Clause 9.2 internal audit plan. NOT executive AI strategy (see cs-caio-advisor). NOT EU AI Act conformity (see cs-ai-act-compliance)." skills: ra-qm-team/skills/iso42001-specialist domain: compliance-os model: opus diff --git a/compliance-os/agents/cs-fda-qsr-auditor.md b/compliance-os/agents/cs-fda-qsr-auditor.md index 3bcaa986..ee9e1f38 100644 --- a/compliance-os/agents/cs-fda-qsr-auditor.md +++ b/compliance-os/agents/cs-fda-qsr-auditor.md @@ -1,6 +1,6 @@ --- name: cs-fda-qsr-auditor -description: FDA 21 CFR 820 (QSR / QMSR) auditor persona. Substantially harmonized with ISO 13485 post-Feb 2026 via FDA Final Rule incorporating ISO 13485 by reference. Adds FDA-specific overlays: labeling (21 CFR 801), complaint handling (21 CFR 820.198), MDR reporting (21 CFR 803), 510(k) / PMA submissions. NOT FDA submission strategy (route to fda-consultant-specialist for that). +description: "FDA 21 CFR 820 (QSR / QMSR) auditor persona. Substantially harmonized with ISO 13485 post-Feb 2026 via FDA Final Rule incorporating ISO 13485 by reference. Adds FDA-specific overlays: labeling (21 CFR 801), complaint handling (21 CFR 820.198), MDR reporting (21 CFR 803), 510(k) / PMA submissions. NOT FDA submission strategy (route to fda-consultant-specialist for that)." skills: ra-qm-team/skills/fda-consultant-specialist domain: compliance-os model: opus diff --git a/engineering/autoresearch-agent/agents/experiment-runner.md b/engineering/autoresearch-agent/agents/experiment-runner.md index 120d81eb..92056286 100644 --- a/engineering/autoresearch-agent/agents/experiment-runner.md +++ b/engineering/autoresearch-agent/agents/experiment-runner.md @@ -1,3 +1,8 @@ +--- +name: experiment-runner +description: "Runs one iteration of an autoresearch experiment loop. Reads experiment state from .autoresearch/{domain}/{name}/, makes exactly ONE change to the target file, commits it, evaluates via run_experiment.py, and reports KEEP / DISCARD / CRASH. Spawned per iteration by /ar:run and /ar:loop. Never modifies the evaluator. Not for general refactoring or multi-change edits." +--- + # Experiment Runner Agent You are an autonomous experimenter. Your job is to optimize a target file by a measurable metric, one change at a time. diff --git a/markdown-html/skills/design-system/SKILL.md b/markdown-html/skills/design-system/SKILL.md index 16b54cad..d52846ff 100644 --- a/markdown-html/skills/design-system/SKILL.md +++ b/markdown-html/skills/design-system/SKILL.md @@ -1,6 +1,6 @@ --- name: design-system -description: Captures the user's brand identity once via a 10-question onboarding wizard (primary/accent HEX + heading + body Google Fonts + design style editorial/technical/minimal/playful + default output directory + syntax theme + TOC behavior + optional logo/company), validates body-text and link contrast against WCAG 2.2 AA, derives 12 CSS custom properties in HSL space, and stores the result for every markdown-html converter to consume. Use before any markdown-html conversion. Triggers on first-run onboarding ("set up the brand", "configure markdown-html", "run onboarding"), on explicit reset ("reset the design system", "re-onboard"), and is checked by every converter via config_loader.py before rendering. Refuses to save if body-text contrast fails AA 4.5:1 or the output dir isn't writable. Precedence: project (./.markdown-html/) > global (~/.config/markdown-html/) > built-in defaults; MARKDOWN_HTML_NO_CONFIG=1 bypasses. +description: Captures the user's brand identity once via a 10-question onboarding wizard (primary/accent HEX + heading + body Google Fonts + design style editorial/technical/minimal/playful + default output directory + syntax theme + TOC behavior + optional logo/company), validates body-text and link contrast against WCAG 2.2 AA, derives 12 CSS custom properties in HSL space, and stores the result for every markdown-html converter to consume. Use before any markdown-html conversion. Triggers on first-run onboarding ("set up the brand", "configure markdown-html", "run onboarding"), on explicit reset ("reset the design system", "re-onboard"), and is checked by every converter via config_loader.py before rendering. Refuses to save if body-text contrast fails AA 4.5:1 or the output dir isn't writable. Precedence is project (./.markdown-html/) > global (~/.config/markdown-html/) > built-in defaults; MARKDOWN_HTML_NO_CONFIG=1 bypasses. version: 2.10.0 author: Alireza Rezvani license: MIT diff --git a/markdown-html/skills/md-slides/SKILL.md b/markdown-html/skills/md-slides/SKILL.md index 2b762aa8..a31560a0 100644 --- a/markdown-html/skills/md-slides/SKILL.md +++ b/markdown-html/skills/md-slides/SKILL.md @@ -1,6 +1,6 @@ --- name: md-slides -description: Converts a markdown deck (slides separated by `---` HR boundaries or by `# ` H1 headings, with optional `` presenter notes blocks) into a single-file HTML presentation with arrow-key / space / PgDn / PgUp / Home / End / P / Esc keyboard navigation, presenter mode (split view with current slide + speaker notes + clock + next-slide preview), URL-hash deep linking, and `@media print` page-per-slide for PDF export. Triggers when the markdown-html-orchestrator classifies an input as SLIDES, or when invoked directly via /cs:md-slides. Reuses md-document's markdown parser for slide-body rendering and reads design-system tokens via config_loader.py. Refuses if input has no clear slide boundaries, produces a 1-slide deck, or `--strict-notes` is on with < 50% notes coverage. Use after orchestrator routing. +description: "Converts a markdown deck (slides separated by `---` HR boundaries or by `# ` H1 headings, with optional `` presenter notes blocks) into a single-file HTML presentation with arrow-key / space / PgDn / PgUp / Home / End / P / Esc keyboard navigation, presenter mode (split view with current slide + speaker notes + clock + next-slide preview), URL-hash deep linking, and `@media print` page-per-slide for PDF export. Triggers when the markdown-html-orchestrator classifies an input as SLIDES, or when invoked directly via /cs:md-slides. Reuses md-document's markdown parser for slide-body rendering and reads design-system tokens via config_loader.py. Refuses if input has no clear slide boundaries, produces a 1-slide deck, or `--strict-notes` is on with < 50% notes coverage. Use after orchestrator routing." version: 2.10.3 author: Alireza Rezvani license: MIT diff --git a/productivity/andreessen/agents/cs-andreessen.md b/productivity/andreessen/agents/cs-andreessen.md index bd9b2fab..03e1d2cf 100644 --- a/productivity/andreessen/agents/cs-andreessen.md +++ b/productivity/andreessen/agents/cs-andreessen.md @@ -1,6 +1,6 @@ --- name: cs-andreessen -description: Marc Andreessen-mode operator. Runs on a fixed anti-sycophancy operating prompt — leads with the strongest counterargument, never validates premises or praises the question, no disclaimers, no morals lectures, explicit confidence levels (high/moderate/low/unknown), never apologizes for disagreeing, never capitulates without new evidence. Pressure-tests ventures/ideas/features/career bets through Andreessen's documented frameworks: market dominates team and product, product/market fit is the only milestone that matters, bias to build. Issues hard verdicts (BUILD-POUR-FUEL / MARKET-FIRST-DERISK / KILL-OR-REPICK-MARKET) backed by deterministic tools. Also runs the 3x5-card + Anti-Todo daily routine. Refuses to soften a dead-market verdict. Refuses to cite a quote without a confidence level. +description: "Marc Andreessen-mode operator. Runs on a fixed anti-sycophancy operating prompt — leads with the strongest counterargument, never validates premises or praises the question, no disclaimers, no morals lectures, explicit confidence levels (high/moderate/low/unknown), never apologizes for disagreeing, never capitulates without new evidence. Pressure-tests ventures/ideas/features/career bets through Andreessen's documented frameworks: market dominates team and product, product/market fit is the only milestone that matters, bias to build. Issues hard verdicts (BUILD-POUR-FUEL / MARKET-FIRST-DERISK / KILL-OR-REPICK-MARKET) backed by deterministic tools. Also runs the 3x5-card + Anti-Todo daily routine. Refuses to soften a dead-market verdict. Refuses to cite a quote without a confidence level." skills: productivity/andreessen/skills/andreessen domain: productivity model: opus diff --git a/productivity/deep-work/agents/cs-deep-work.md b/productivity/deep-work/agents/cs-deep-work.md index 97533fab..cd6d1c50 100644 --- a/productivity/deep-work/agents/cs-deep-work.md +++ b/productivity/deep-work/agents/cs-deep-work.md @@ -1,6 +1,6 @@ --- name: cs-deep-work -description: Plans a deep work day the Cal Newport way — audits a task list deep vs shallow against a 30-50% shallow budget, builds an energy-first time-blocked schedule (deep blocks of at least 90 minutes in the earliest hours, a hard 4-hour deep ceiling, shallow work batched into at most two windows, 10-minute buffers, an immovable hard stop), logs focus sessions against a weekly target, and closes the day with a shutdown ritual. Respects the scripts' exit-2 refusals: deep demand past the cap and overflow past the hard stop are named and deferred, never squeezed in. Use to plan or re-plan a focused day, cut shallow work, or track deep-hours streaks. +description: "Plans a deep work day the Cal Newport way — audits a task list deep vs shallow against a 30-50% shallow budget, builds an energy-first time-blocked schedule (deep blocks of at least 90 minutes in the earliest hours, a hard 4-hour deep ceiling, shallow work batched into at most two windows, 10-minute buffers, an immovable hard stop), logs focus sessions against a weekly target, and closes the day with a shutdown ritual. Respects the scripts' exit-2 refusals: deep demand past the cap and overflow past the hard stop are named and deferred, never squeezed in. Use to plan or re-plan a focused day, cut shallow work, or track deep-hours streaks." skills: productivity/deep-work/skills/deep-work domain: productivity model: opus diff --git a/research/notebooklm/agents/cs-notebooklm.md b/research/notebooklm/agents/cs-notebooklm.md index 0b8542f1..b5b29e6f 100644 --- a/research/notebooklm/agents/cs-notebooklm.md +++ b/research/notebooklm/agents/cs-notebooklm.md @@ -1,6 +1,6 @@ --- name: cs-notebooklm -description: NotebookLM browser-automation persona. Walks 2-4 forcing intake questions (Q1 action: read / add source / Studio output / create new; Q2-Q4 branch per action). Refuses to start without action commitment. Refuses to handle login flows automatically. Refuses to wait synchronously for Studio generations (fire-and-notify). Always opens Studio customization menu and writes detailed custom prompt (default prompts produce mediocre output). Screenshot-first discipline. find()-before-click. Fails fast when browser automation unavailable. +description: "NotebookLM browser-automation persona. Walks 2-4 forcing intake questions (Q1 action: read / add source / Studio output / create new; Q2-Q4 branch per action). Refuses to start without action commitment. Refuses to handle login flows automatically. Refuses to wait synchronously for Studio generations (fire-and-notify). Always opens Studio customization menu and writes detailed custom prompt (default prompts produce mediocre output). Screenshot-first discipline. find()-before-click. Fails fast when browser automation unavailable." skills: research/notebooklm/skills/notebooklm domain: research model: opus From 5b85956d637d376e3c60e07dd09472e2ed62e91c Mon Sep 17 00:00:00 2001 From: Ben Fairless Date: Sat, 1 Aug 2026 10:33:36 +0800 Subject: [PATCH 2/2] feat(ci): add frontmatter YAML validator as gate G10 Every existing gate reads frontmatter with a regex or a line scan (generate-docs.py, sync-codex-skills.py, check_paths.py), so a block that is not valid YAML passed CI while Claude Code loaded the skill with no metadata. The 14 files fixed in the previous commit had drifted that way unnoticed. check_frontmatter.py parses each block with yaml.safe_load and enforces what Claude Code actually reads: errors - unparseable YAML, non-mapping frontmatter, missing description, missing agent name, an agent name containing ':' (refused since CC 2.1.218), or a missing frontmatter block warnings - keys outside the current skill/agent frontmatter spec, and a combined description + when_to_use over the 1536-char cap that the skill listing truncates at Warnings are non-blocking so this lands without requiring the wider metadata cleanup; --strict flips them fatal. The run also tallies the off-spec keys no runtime reads (license 172, metadata 125, domain 76, compatible_tools 37, triggers 14), which gives that cleanup a worklist regenerated on every run. Clean on the current tree: 593 files, 0 errors, 17 warnings. Assisted-by: Claude Code:claude-opus-5 --- .github/workflows/ci-quality-gate.yml | 7 + scripts/check_frontmatter.py | 242 ++++++++++++++++++++++++++ 2 files changed, 249 insertions(+) create mode 100644 scripts/check_frontmatter.py diff --git a/.github/workflows/ci-quality-gate.yml b/.github/workflows/ci-quality-gate.yml index 8e0b9fcd..0e4cb119 100644 --- a/.github/workflows/ci-quality-gate.yml +++ b/.github/workflows/ci-quality-gate.yml @@ -102,6 +102,13 @@ jobs: run: | python3 scripts/check_paths.py --all + # Every other gate reads frontmatter with a regex, so malformed YAML used + # to pass CI while Claude Code silently loaded the skill with no + # description. G10 parses it properly. + - name: Frontmatter YAML validator (gate G10 — blocking) + run: | + python3 scripts/check_frontmatter.py --all + - name: Dual-publish drift guard (gate G4 — blocking) run: | python3 scripts/check_dual_publish.py diff --git a/scripts/check_frontmatter.py b/scripts/check_frontmatter.py new file mode 100644 index 00000000..852a43f0 --- /dev/null +++ b/scripts/check_frontmatter.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +"""check_frontmatter.py — YAML frontmatter validator for skills, agents, and commands. + +Every other gate in this repo extracts frontmatter with a regex or a line scan +(see generate-docs.py and sync-codex-skills.py), so a block that is not valid +YAML sails through CI unnoticed. Claude Code parses it properly: when the YAML +is malformed it loads the body with EMPTY metadata, which silently removes the +skill's `description` from the skill listing and makes it invisible to +model invocation. Agents, where `name` and `description` are required, may fail +to load entirely. + +This gate parses each block with yaml.safe_load and enforces the fields Claude +Code actually reads. + +Errors (exit 1): + - frontmatter block present but not parseable, or not a YAML mapping + - missing `description` (skills/commands) or `name`/`description` (agents) + - an agent `name` containing ':' (rejected by Claude Code >= 2.1.218) + - no frontmatter block at all in a file that should have one + +Warnings (exit 0 unless --strict): + - keys outside the current Claude Code frontmatter spec + - combined description + when_to_use over the 1536-char skill-listing cap + +Exit codes: 0 = clean, 1 = at least one error (or a warning under --strict). +Intended as CI gate G10. + +Usage: + python3 scripts/check_frontmatter.py --all # scan canonical dirs repo-wide + python3 scripts/check_frontmatter.py FILE [FILE ...] # scan specific files + python3 scripts/check_frontmatter.py --all --json # machine-readable output + python3 scripts/check_frontmatter.py --all --strict # warnings become errors +""" + +import argparse +import fnmatch +import json +import os +import re +import sys + +try: + import yaml +except ImportError: # pragma: no cover - CI installs yamllint, which vendors PyYAML + sys.stderr.write( + "check_frontmatter.py needs PyYAML (pip install pyyaml). " + "Parsing frontmatter with a regex is what this gate exists to prevent.\n" + ) + sys.exit(2) + +EXCLUDED_DIRS = { + ".git", ".codex", ".gemini", ".hermes", ".vibe", "docs", "audit", + "node_modules", ".github", ".claude-plugin", +} + +FRONTMATTER_RE = re.compile(r"^---\r?\n(.*?)\r?\n---\r?\n", re.DOTALL) + +# Files that legitimately carry no frontmatter: documentation that happens to +# live inside a canonical dir, and the skill-tester's deliberate bare fixture. +DOC_BASENAMES = {"README.md", "CLAUDE.md", "TEMPLATE.md"} +NO_FRONTMATTER_OK = ("*/assets/sample-skill/SKILL.md",) + +# Claude Code skill frontmatter. Commands are skills, so they share this set. +# https://code.claude.com/docs/en/skills#frontmatter-reference +SKILL_SPEC_KEYS = { + "name", "description", "when_to_use", "argument-hint", "arguments", + "disable-model-invocation", "user-invocable", "allowed-tools", + "disallowed-tools", "model", "effort", "context", "agent", "background", + "hooks", "paths", "shell", +} + +# Claude Code subagent frontmatter. +# https://code.claude.com/docs/en/sub-agents#write-subagent-files +AGENT_SPEC_KEYS = { + "name", "description", "tools", "disallowedTools", "model", + "permissionMode", "maxTurns", "skills", "mcpServers", "hooks", "memory", + "background", "effort", "isolation", "color", "initialPrompt", +} + +# Pre-existing repo conventions that no runtime reads. Reported separately from +# genuine typos so the migration in the v5 plan has a worklist rather than noise. +KNOWN_REPO_EXTRAS = { + "license", "metadata", "version", "author", "tags", "compatible_tools", + "triggers", "command", "domain", "title", "emoji", "vibe", "tier", + "category", "dependencies", "not_for", "agents", "source", "attribution", +} + +# Combined description + when_to_use budget per entry in the skill listing. +DESC_CAP = 1536 + + +def kind_for(rel_path): + """skill | agent | command, based on where the file lives.""" + if os.path.basename(rel_path) == "SKILL.md": + return "skill" + parts = rel_path.split(os.sep) + if "agents" in parts: + return "agent" + return "command" + + +def no_frontmatter_ok(rel_path): + if os.path.basename(rel_path) in DOC_BASENAMES: + return True + posix = rel_path.replace(os.sep, "/") + return any(fnmatch.fnmatch(posix, pat) for pat in NO_FRONTMATTER_OK) + + +def check_file(path, repo_root): + """Return (errors, warnings, offspec_keys) for one file.""" + rel = os.path.relpath(path, repo_root) + kind = kind_for(rel) + errors, warnings, offspec = [], [], [] + + text = open(path, encoding="utf-8", errors="replace").read() + match = FRONTMATTER_RE.match(text) + if not match: + if not no_frontmatter_ok(rel): + errors.append("no YAML frontmatter block") + return errors, warnings, offspec + + try: + data = yaml.safe_load(match.group(1)) + except yaml.YAMLError as exc: + detail = str(exc).replace("\n", " ") + errors.append(f"frontmatter is not valid YAML: {detail}") + return errors, warnings, offspec + + if not isinstance(data, dict): + errors.append(f"frontmatter is not a YAML mapping (parsed as {type(data).__name__})") + return errors, warnings, offspec + + if not str(data.get("description") or "").strip(): + errors.append("missing `description` — Claude Code has nothing to match the skill against") + + if kind == "agent": + name = str(data.get("name") or "").strip() + if not name: + errors.append("missing `name` — required for subagents") + elif ":" in name: + errors.append(f"`name: {name}` contains ':', which Claude Code >= 2.1.218 refuses to load") + + spec = AGENT_SPEC_KEYS if kind == "agent" else SKILL_SPEC_KEYS + for key in data: + if key in spec: + continue + if key in KNOWN_REPO_EXTRAS: + offspec.append(key) + else: + warnings.append(f"unrecognized key `{key}` (not in the {kind} frontmatter spec)") + + listing = len(str(data.get("description") or "")) + len(str(data.get("when_to_use") or "")) + if kind != "agent" and listing > DESC_CAP: + warnings.append( + f"description + when_to_use is {listing} chars; the skill listing truncates at {DESC_CAP}" + ) + + return errors, warnings, offspec + + +def collect_canonical(repo_root): + """All SKILL.md + *.md under any agents/ or commands/ dir, excluding sync/doc trees.""" + targets = [] + for dirpath, dirnames, filenames in os.walk(repo_root): + dirnames[:] = [d for d in dirnames if d not in EXCLUDED_DIRS] + parts = os.path.relpath(dirpath, repo_root).split(os.sep) + in_canonical_dir = "agents" in parts or "commands" in parts + for fn in filenames: + if fn == "SKILL.md" or (in_canonical_dir and fn.endswith(".md")): + targets.append(os.path.join(dirpath, fn)) + return sorted(targets) + + +def main(): + ap = argparse.ArgumentParser( + description="Validate YAML frontmatter on SKILL.md / agents / commands files." + ) + ap.add_argument("files", nargs="*", help="Specific markdown files to scan") + ap.add_argument("--all", action="store_true", + help="Scan all SKILL.md + agents/*.md + commands/*.md in the repo") + ap.add_argument("--json", action="store_true", help="Emit JSON instead of human-readable output") + ap.add_argument("--strict", action="store_true", help="Treat warnings as errors") + ap.add_argument("--root", default=os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + help="Repo root (default: parent of this script)") + args = ap.parse_args() + + repo_root = os.path.abspath(args.root) + if args.all: + targets = collect_canonical(repo_root) + elif args.files: + targets = [os.path.abspath(f) for f in args.files] + else: + ap.print_help() + return 0 + + errors, warnings = {}, {} + offspec_counts = {} + for path in targets: + errs, warns, offspec = check_file(path, repo_root) + rel = os.path.relpath(path, repo_root) + if errs: + errors[rel] = errs + if warns: + warnings[rel] = warns + for key in offspec: + offspec_counts[key] = offspec_counts.get(key, 0) + 1 + + n_err = sum(len(v) for v in errors.values()) + n_warn = sum(len(v) for v in warnings.values()) + + if args.json: + print(json.dumps({ + "files_scanned": len(targets), + "errors": errors, + "warnings": warnings, + "total_errors": n_err, + "total_warnings": n_warn, + "off_spec_key_counts": dict(sorted(offspec_counts.items(), + key=lambda kv: -kv[1])), + }, indent=2)) + else: + for rel in sorted(errors): + print(f"{rel}:") + for msg in errors[rel]: + print(f" ERROR: {msg}") + for rel in sorted(warnings): + print(f"{rel}:") + for msg in warnings[rel]: + print(f" WARN: {msg}") + if offspec_counts: + print("\nOff-spec keys still in use (no runtime reads these):") + for key, count in sorted(offspec_counts.items(), key=lambda kv: -kv[1]): + print(f" {count:4d} {key}") + print(f"\nScanned {len(targets)} files; {n_err} errors, {n_warn} warnings.") + + if n_err: + return 1 + return 1 if (args.strict and n_warn) else 0 + + +if __name__ == "__main__": + sys.exit(main())