From c6f5a2bec0746a7bf87af17ce5c461d5f4080291 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues <44656907+Rodriguespn@users.noreply.github.com> Date: Fri, 13 Feb 2026 17:14:49 +0000 Subject: [PATCH] chore: build system skill body (#42) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: extract SKILL.md body into AGENTS.md with H1 title and Overview section Build system now parses SKILL.md body to extract H1 heading as the AGENTS.md title and places remaining content under an Overview section. Adds validation that SKILL.md body starts with H1, directory name is kebab-case, and name field matches directory name. Co-Authored-By: Claude Opus 4.6 * feat: AGENTS.md is now SKILL.md body with frontmatter stripped Build now generates AGENTS.md by extracting the SKILL.md markdown body (everything after YAML frontmatter). CLAUDE.md remains a symlink to AGENTS.md. Removes content generation logic (Structure, Usage, Overview, Reference Categories, Available References sections) — SKILL.md is the single source of truth for agent instructions. Co-Authored-By: Claude Opus 4.6 * feat: add Structure and Usage sections to AGENTS.md, validate H1 title matches directory name Build now generates AGENTS.md as: H1 Title > Structure > Usage > rest of SKILL.md body. Validates that SKILL.md body starts with H1 heading and that the title in kebab-case matches the directory name. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- AGENTS.md | 2 +- packages/skills-build/src/build.ts | 192 ++++-------------- packages/skills-build/src/validate.ts | 94 ++++++++- .../AGENTS.md | 91 ++++----- 4 files changed, 172 insertions(+), 207 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4bc4def..9bfa676 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ Guidance for AI coding agents working with this repository. skills/ {skill-name}/ SKILL.md # Required: skill manifest (Agent Skills spec) - AGENTS.md # Generated: navigation guide for agents + AGENTS.md # Generated: SKILL.md body (frontmatter stripped) CLAUDE.md # Generated: symlink to AGENTS.md references/ _sections.md # Required: section definitions diff --git a/packages/skills-build/src/build.ts b/packages/skills-build/src/build.ts index 03b06c8..13272c2 100644 --- a/packages/skills-build/src/build.ts +++ b/packages/skills-build/src/build.ts @@ -8,7 +8,7 @@ import { unlinkSync, writeFileSync, } from "node:fs"; -import { basename, join } from "node:path"; +import { join } from "node:path"; import { discoverSkills, getSkillPaths, @@ -77,78 +77,19 @@ function parseSections(rulesDir: string): Section[] { } /** - * Parse SKILL.md frontmatter to extract metadata + * Extract the markdown body from SKILL.md (everything after frontmatter) */ -function parseSkillFrontmatter(content: string): Record { +function extractSkillBody(content: string): string { if (!content.startsWith("---")) { - return {}; + return content.trim(); } const endIndex = content.indexOf("---", 3); if (endIndex === -1) { - return {}; + return content.trim(); } - const frontmatterContent = content.slice(3, endIndex).trim(); - const result: Record = {}; - let inMetadata = false; - const metadataObj: Record = {}; - - for (const line of frontmatterContent.split("\n")) { - // Check for metadata block start - if (line.trim() === "metadata:") { - inMetadata = true; - continue; - } - - // Handle metadata nested values - if (inMetadata && line.startsWith(" ")) { - const colonIndex = line.indexOf(":"); - if (colonIndex !== -1) { - const key = line.slice(0, colonIndex).trim(); - let value = line.slice(colonIndex + 1).trim(); - if ( - (value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'")) - ) { - value = value.slice(1, -1); - } - metadataObj[key] = value; - } - continue; - } - - // End metadata block when we hit a non-indented line - if (inMetadata && !line.startsWith(" ") && line.trim()) { - inMetadata = false; - result.metadata = metadataObj; - } - - // Handle top-level key-value - const colonIndex = line.indexOf(":"); - if (colonIndex === -1) continue; - - const currentKey = line.slice(0, colonIndex).trim(); - let value = line.slice(colonIndex + 1).trim(); - - if ( - (value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'")) - ) { - value = value.slice(1, -1); - } - - if (value) { - result[currentKey] = value; - } - } - - // Ensure metadata is captured if file ends in metadata block - if (inMetadata && Object.keys(metadataObj).length > 0) { - result.metadata = metadataObj; - } - - return result; + return content.slice(endIndex + 3).trim(); } /** @@ -219,22 +160,29 @@ function getReferenceFiles(referencesDir: string): string[] { } /** - * Convert skill name to title (e.g., "postgres-best-practices" -> "Postgres Best Practices") + * Parse the SKILL.md body into its H1 title and the content after it. + * Returns the title text and the remaining body content. */ -function skillNameToTitle(skillName: string): string { - return skillName - .split("-") - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(" "); +function parseSkillBodySections(body: string): { + title: string | null; + content: string; +} { + const lines = body.split("\n"); + const firstLine = lines[0]?.trim() ?? ""; + + const h1Match = firstLine.match(/^#\s+(.+)$/); + if (!h1Match) { + return { title: null, content: body }; + } + + const content = lines.slice(1).join("\n").trim(); + return { title: h1Match[1].trim(), content }; } /** - * Create CLAUDE.md symlink pointing to AGENTS.md + * Create a symlink, removing any existing file or symlink at the target path */ -function createClaudeSymlink(paths: SkillPaths): void { - const symlinkPath = paths.claudeSymlink; - - // Remove existing symlink or file if it exists +function createSymlink(symlinkPath: string, target: string): void { if (existsSync(symlinkPath)) { const stat = lstatSync(symlinkPath); if (stat.isSymbolicLink() || stat.isFile()) { @@ -242,46 +190,33 @@ function createClaudeSymlink(paths: SkillPaths): void { } } - // Create symlink (relative path so it works across environments) - symlinkSync("AGENTS.md", symlinkPath); - console.log(` Created symlink: CLAUDE.md -> AGENTS.md`); + symlinkSync(target, symlinkPath); } /** * Build AGENTS.md for a specific skill * - * AGENTS.md is a concise navigation guide for AI agents, NOT a comprehensive - * documentation dump. It helps agents understand the skill directory structure - * and how to find information. + * Structure: H1 Title > Structure > Usage > rest of SKILL.md body + * CLAUDE.md = symlink to AGENTS.md. */ function buildSkill(paths: SkillPaths): void { console.log(`[${paths.name}] Building AGENTS.md...`); - // Read SKILL.md for metadata + // Read SKILL.md and strip frontmatter const skillContent = existsSync(paths.skillFile) ? readFileSync(paths.skillFile, "utf-8") : ""; - const frontmatter = parseSkillFrontmatter(skillContent); - const skillTitle = skillNameToTitle(paths.name); - const description = - (frontmatter.description as string) || `${skillTitle} skill for AI agents.`; + const body = extractSkillBody(skillContent); + const { title, content: skillBodyContent } = parseSkillBodySections(body); - // Parse sections if available - const sections = parseAllSections(paths.referencesDir); - const referenceFiles = getReferenceFiles(paths.referencesDir); - - // Generate concise AGENTS.md const output: string[] = []; - // Header - output.push(`# ${paths.name}\n`); - output.push(`> **Note:** \`CLAUDE.md\` is a symlink to this file.\n`); + // 1. Title (from SKILL.md H1) + if (title) { + output.push(`# ${title}\n`); + } - // Brief description - output.push(`## Overview\n`); - output.push(`${description}\n`); - - // Directory structure + // 2. Structure output.push(`## Structure\n`); output.push("```"); output.push(`${paths.name}/`); @@ -293,7 +228,7 @@ function buildSkill(paths: SkillPaths): void { } output.push("```\n"); - // How to use + // 3. Usage output.push(`## Usage\n`); output.push(`1. Read \`SKILL.md\` for the main skill instructions`); output.push( @@ -303,59 +238,19 @@ function buildSkill(paths: SkillPaths): void { `3. Reference files are loaded on-demand - read only what you need\n`, ); - // Reference sections (if available) - if (sections.length > 0) { - output.push(`## Reference Categories\n`); - output.push(`| Priority | Category | Impact | Prefix |`); - output.push(`|----------|----------|--------|--------|`); - for (const section of sections.sort((a, b) => a.number - b.number)) { - output.push( - `| ${section.number} | ${section.title} | ${section.impact} | \`${section.prefix}-\` |`, - ); - } + // 4. Rest of SKILL.md body (after H1 title) + if (skillBodyContent) { + output.push(skillBodyContent); output.push(""); - output.push( - `Reference files are named \`{prefix}-{topic}.md\` (e.g., \`query-missing-indexes.md\`).\n`, - ); } - // Reference file list (just filenames, not content) - if (referenceFiles.length > 0) { - output.push(`## Available References\n`); - const grouped = new Map(); - - for (const file of referenceFiles) { - const name = basename(file, ".md"); - const prefix = name.split("-")[0]; - const group = grouped.get(prefix) || []; - group.push(name); - grouped.set(prefix, group); - } - - for (const [prefix, files] of grouped) { - const section = sections.find((s) => s.prefix === prefix); - const title = section ? section.title : prefix; - output.push(`**${title}** (\`${prefix}-\`):`); - for (const file of files.sort()) { - output.push(`- \`references/${file}.md\``); - } - output.push(""); - } - } - - // Stats - output.push(`---\n`); - output.push( - `*${referenceFiles.length} reference files across ${sections.length} categories*`, - ); - // Write AGENTS.md writeFileSync(paths.agentsOutput, output.join("\n")); console.log(` Generated: ${paths.agentsOutput}`); - console.log(` Total references: ${referenceFiles.length}`); - // Create CLAUDE.md symlink - createClaudeSymlink(paths); + // Create CLAUDE.md -> AGENTS.md symlink + createSymlink(paths.claudeSymlink, "AGENTS.md"); + console.log(` Created symlink: CLAUDE.md -> AGENTS.md`); } // Run build when executed directly @@ -397,11 +292,12 @@ if (isMainModule) { export { buildSkill, + extractSkillBody, generateSectionMap, getMarkdownFiles, getMarkdownFilesRecursive, // deprecated, use getMarkdownFiles getReferenceFiles, parseAllSections, + parseSkillBodySections, parseSections, - skillNameToTitle, }; diff --git a/packages/skills-build/src/validate.ts b/packages/skills-build/src/validate.ts index 10830f1..41694da 100644 --- a/packages/skills-build/src/validate.ts +++ b/packages/skills-build/src/validate.ts @@ -1,10 +1,12 @@ -import { existsSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { basename } from "node:path"; import { + extractSkillBody, generateSectionMap, getMarkdownFiles, parseAllSections, parseSections, + parseSkillBodySections, } from "./build.js"; import { discoverSkills, @@ -142,12 +144,102 @@ export function validateRuleFile( }; } +/** + * Extract the `name` field from SKILL.md frontmatter + */ +function extractSkillName(skillFilePath: string): string | null { + if (!existsSync(skillFilePath)) return null; + + const content = readFileSync(skillFilePath, "utf-8"); + if (!content.startsWith("---")) return null; + + const endIndex = content.indexOf("---", 3); + if (endIndex === -1) return null; + + const frontmatter = content.slice(3, endIndex); + for (const line of frontmatter.split("\n")) { + const match = line.match(/^name:\s*(.+)/); + if (match) return match[1].trim(); + } + return null; +} + +/** + * Convert a title to kebab-case (e.g., "Supabase Postgres Best Practices" -> "supabase-postgres-best-practices") + */ +function titleToKebab(title: string): string { + return title + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-|-$/g, ""); +} + +/** + * Validate SKILL.md structure: + * - `name` field matches directory name (kebab-case) + * - Body starts with an H1 heading + * - H1 title in kebab-case matches directory name + */ +function validateSkillStructure(paths: SkillPaths): string[] { + const errors: string[] = []; + const skillName = extractSkillName(paths.skillFile); + + if (!skillName) { + errors.push("SKILL.md is missing the `name` field in frontmatter"); + return errors; + } + + if (skillName !== paths.name) { + errors.push( + `SKILL.md name "${skillName}" does not match directory name "${paths.name}"`, + ); + } + + // Validate kebab-case format + if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(paths.name)) { + errors.push( + `Directory name "${paths.name}" is not valid kebab-case (lowercase alphanumeric and hyphens only, no leading/trailing/consecutive hyphens)`, + ); + } + + // Validate body starts with H1 and title matches directory name in kebab-case + if (existsSync(paths.skillFile)) { + const content = readFileSync(paths.skillFile, "utf-8"); + const body = extractSkillBody(content); + const { title } = parseSkillBodySections(body); + + if (!title) { + errors.push( + "SKILL.md body must start with an H1 heading (e.g., `# Skill Title`)", + ); + } else { + const kebabTitle = titleToKebab(title); + if (kebabTitle !== paths.name) { + errors.push( + `H1 title "${title}" in kebab-case is "${kebabTitle}", but directory name is "${paths.name}"`, + ); + } + } + } + + return errors; +} + /** * Validate all reference files for a skill */ function validateSkill(paths: SkillPaths): boolean { console.log(`[${paths.name}] Validating...`); + // Validate skill structure (name, kebab-case, H1 heading) + const nameErrors = validateSkillStructure(paths); + if (nameErrors.length > 0) { + for (const error of nameErrors) { + console.log(` ERROR: ${error}`); + } + return false; + } + // Check if references directory exists if (!existsSync(paths.referencesDir)) { console.log(` No references directory found.`); diff --git a/skills/supabase-postgres-best-practices/AGENTS.md b/skills/supabase-postgres-best-practices/AGENTS.md index a744cc8..a7baf44 100644 --- a/skills/supabase-postgres-best-practices/AGENTS.md +++ b/skills/supabase-postgres-best-practices/AGENTS.md @@ -1,10 +1,4 @@ -# supabase-postgres-best-practices - -> **Note:** `CLAUDE.md` is a symlink to this file. - -## Overview - -Postgres performance optimization and best practices from Supabase. Use this skill when writing, reviewing, or optimizing Postgres queries, schema designs, or database configurations. +# Supabase Postgres Best Practices ## Structure @@ -22,7 +16,19 @@ supabase-postgres-best-practices/ 2. Browse `references/` for detailed documentation on specific topics 3. Reference files are loaded on-demand - read only what you need -## Reference Categories +Comprehensive performance optimization guide for Postgres, maintained by Supabase. Contains rules across 8 categories, prioritized by impact to guide automated query optimization and schema design. + +## When to Apply + +Reference these guidelines when: +- Writing SQL queries or designing schemas +- Implementing indexes or query optimization +- Reviewing database performance issues +- Configuring connection pooling or scaling +- Optimizing for Postgres-specific features +- Working with Row-Level Security (RLS) + +## Rule Categories by Priority | Priority | Category | Impact | Prefix | |----------|----------|--------|--------| @@ -35,57 +41,28 @@ supabase-postgres-best-practices/ | 7 | Monitoring & Diagnostics | LOW-MEDIUM | `monitor-` | | 8 | Advanced Features | LOW | `advanced-` | -Reference files are named `{prefix}-{topic}.md` (e.g., `query-missing-indexes.md`). +## How to Use -## Available References +Read individual rule files for detailed explanations and SQL examples: -**Advanced Features** (`advanced-`): -- `references/advanced-full-text-search.md` -- `references/advanced-jsonb-indexing.md` +``` +references/query-missing-indexes.md +references/schema-partial-indexes.md +references/_sections.md +``` -**Connection Management** (`conn-`): -- `references/conn-idle-timeout.md` -- `references/conn-limits.md` -- `references/conn-pooling.md` -- `references/conn-prepared-statements.md` +Each rule file contains: +- Brief explanation of why it matters +- Incorrect SQL example with explanation +- Correct SQL example with explanation +- Optional EXPLAIN output or metrics +- Additional context and references +- Supabase-specific notes (when applicable) -**Data Access Patterns** (`data-`): -- `references/data-batch-inserts.md` -- `references/data-n-plus-one.md` -- `references/data-pagination.md` -- `references/data-upsert.md` +## References -**Concurrency & Locking** (`lock-`): -- `references/lock-advisory.md` -- `references/lock-deadlock-prevention.md` -- `references/lock-short-transactions.md` -- `references/lock-skip-locked.md` - -**Monitoring & Diagnostics** (`monitor-`): -- `references/monitor-explain-analyze.md` -- `references/monitor-pg-stat-statements.md` -- `references/monitor-vacuum-analyze.md` - -**Query Performance** (`query-`): -- `references/query-composite-indexes.md` -- `references/query-covering-indexes.md` -- `references/query-index-types.md` -- `references/query-missing-indexes.md` -- `references/query-partial-indexes.md` - -**Schema Design** (`schema-`): -- `references/schema-constraints.md` -- `references/schema-data-types.md` -- `references/schema-foreign-key-indexes.md` -- `references/schema-lowercase-identifiers.md` -- `references/schema-partitioning.md` -- `references/schema-primary-keys.md` - -**Security & RLS** (`security-`): -- `references/security-privileges.md` -- `references/security-rls-basics.md` -- `references/security-rls-performance.md` - ---- - -*31 reference files across 8 categories* \ No newline at end of file +- https://www.postgresql.org/docs/current/ +- https://supabase.com/docs +- https://wiki.postgresql.org/wiki/Performance_Optimization +- https://supabase.com/docs/guides/database/overview +- https://supabase.com/docs/guides/auth/row-level-security