chore: build system skill body (#42)

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Pedro Rodrigues
2026-02-13 17:14:49 +00:00
committed by GitHub
parent 9b236f3ebd
commit c6f5a2bec0
4 changed files with 172 additions and 207 deletions

View File

@@ -10,7 +10,7 @@ Guidance for AI coding agents working with this repository.
skills/ skills/
{skill-name}/ {skill-name}/
SKILL.md # Required: skill manifest (Agent Skills spec) 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 CLAUDE.md # Generated: symlink to AGENTS.md
references/ references/
_sections.md # Required: section definitions _sections.md # Required: section definitions

View File

@@ -8,7 +8,7 @@ import {
unlinkSync, unlinkSync,
writeFileSync, writeFileSync,
} from "node:fs"; } from "node:fs";
import { basename, join } from "node:path"; import { join } from "node:path";
import { import {
discoverSkills, discoverSkills,
getSkillPaths, 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<string, unknown> { function extractSkillBody(content: string): string {
if (!content.startsWith("---")) { if (!content.startsWith("---")) {
return {}; return content.trim();
} }
const endIndex = content.indexOf("---", 3); const endIndex = content.indexOf("---", 3);
if (endIndex === -1) { if (endIndex === -1) {
return {}; return content.trim();
} }
const frontmatterContent = content.slice(3, endIndex).trim(); return content.slice(endIndex + 3).trim();
const result: Record<string, unknown> = {};
let inMetadata = false;
const metadataObj: Record<string, string> = {};
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;
} }
/** /**
@@ -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 { function parseSkillBodySections(body: string): {
return skillName title: string | null;
.split("-") content: string;
.map((word) => word.charAt(0).toUpperCase() + word.slice(1)) } {
.join(" "); 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 { function createSymlink(symlinkPath: string, target: string): void {
const symlinkPath = paths.claudeSymlink;
// Remove existing symlink or file if it exists
if (existsSync(symlinkPath)) { if (existsSync(symlinkPath)) {
const stat = lstatSync(symlinkPath); const stat = lstatSync(symlinkPath);
if (stat.isSymbolicLink() || stat.isFile()) { if (stat.isSymbolicLink() || stat.isFile()) {
@@ -242,46 +190,33 @@ function createClaudeSymlink(paths: SkillPaths): void {
} }
} }
// Create symlink (relative path so it works across environments) symlinkSync(target, symlinkPath);
symlinkSync("AGENTS.md", symlinkPath);
console.log(` Created symlink: CLAUDE.md -> AGENTS.md`);
} }
/** /**
* Build AGENTS.md for a specific skill * Build AGENTS.md for a specific skill
* *
* AGENTS.md is a concise navigation guide for AI agents, NOT a comprehensive * Structure: H1 Title > Structure > Usage > rest of SKILL.md body
* documentation dump. It helps agents understand the skill directory structure * CLAUDE.md = symlink to AGENTS.md.
* and how to find information.
*/ */
function buildSkill(paths: SkillPaths): void { function buildSkill(paths: SkillPaths): void {
console.log(`[${paths.name}] Building AGENTS.md...`); console.log(`[${paths.name}] Building AGENTS.md...`);
// Read SKILL.md for metadata // Read SKILL.md and strip frontmatter
const skillContent = existsSync(paths.skillFile) const skillContent = existsSync(paths.skillFile)
? readFileSync(paths.skillFile, "utf-8") ? readFileSync(paths.skillFile, "utf-8")
: ""; : "";
const frontmatter = parseSkillFrontmatter(skillContent); const body = extractSkillBody(skillContent);
const skillTitle = skillNameToTitle(paths.name); const { title, content: skillBodyContent } = parseSkillBodySections(body);
const description =
(frontmatter.description as string) || `${skillTitle} skill for AI agents.`;
// Parse sections if available
const sections = parseAllSections(paths.referencesDir);
const referenceFiles = getReferenceFiles(paths.referencesDir);
// Generate concise AGENTS.md
const output: string[] = []; const output: string[] = [];
// Header // 1. Title (from SKILL.md H1)
output.push(`# ${paths.name}\n`); if (title) {
output.push(`> **Note:** \`CLAUDE.md\` is a symlink to this file.\n`); output.push(`# ${title}\n`);
}
// Brief description // 2. Structure
output.push(`## Overview\n`);
output.push(`${description}\n`);
// Directory structure
output.push(`## Structure\n`); output.push(`## Structure\n`);
output.push("```"); output.push("```");
output.push(`${paths.name}/`); output.push(`${paths.name}/`);
@@ -293,7 +228,7 @@ function buildSkill(paths: SkillPaths): void {
} }
output.push("```\n"); output.push("```\n");
// How to use // 3. Usage
output.push(`## Usage\n`); output.push(`## Usage\n`);
output.push(`1. Read \`SKILL.md\` for the main skill instructions`); output.push(`1. Read \`SKILL.md\` for the main skill instructions`);
output.push( output.push(
@@ -303,59 +238,19 @@ function buildSkill(paths: SkillPaths): void {
`3. Reference files are loaded on-demand - read only what you need\n`, `3. Reference files are loaded on-demand - read only what you need\n`,
); );
// Reference sections (if available) // 4. Rest of SKILL.md body (after H1 title)
if (sections.length > 0) { if (skillBodyContent) {
output.push(`## Reference Categories\n`); output.push(skillBodyContent);
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}-\` |`,
);
}
output.push(""); 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<string, string[]>();
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 // Write AGENTS.md
writeFileSync(paths.agentsOutput, output.join("\n")); writeFileSync(paths.agentsOutput, output.join("\n"));
console.log(` Generated: ${paths.agentsOutput}`); console.log(` Generated: ${paths.agentsOutput}`);
console.log(` Total references: ${referenceFiles.length}`);
// Create CLAUDE.md symlink // Create CLAUDE.md -> AGENTS.md symlink
createClaudeSymlink(paths); createSymlink(paths.claudeSymlink, "AGENTS.md");
console.log(` Created symlink: CLAUDE.md -> AGENTS.md`);
} }
// Run build when executed directly // Run build when executed directly
@@ -397,11 +292,12 @@ if (isMainModule) {
export { export {
buildSkill, buildSkill,
extractSkillBody,
generateSectionMap, generateSectionMap,
getMarkdownFiles, getMarkdownFiles,
getMarkdownFilesRecursive, // deprecated, use getMarkdownFiles getMarkdownFilesRecursive, // deprecated, use getMarkdownFiles
getReferenceFiles, getReferenceFiles,
parseAllSections, parseAllSections,
parseSkillBodySections,
parseSections, parseSections,
skillNameToTitle,
}; };

View File

@@ -1,10 +1,12 @@
import { existsSync } from "node:fs"; import { existsSync, readFileSync } from "node:fs";
import { basename } from "node:path"; import { basename } from "node:path";
import { import {
extractSkillBody,
generateSectionMap, generateSectionMap,
getMarkdownFiles, getMarkdownFiles,
parseAllSections, parseAllSections,
parseSections, parseSections,
parseSkillBodySections,
} from "./build.js"; } from "./build.js";
import { import {
discoverSkills, 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 * Validate all reference files for a skill
*/ */
function validateSkill(paths: SkillPaths): boolean { function validateSkill(paths: SkillPaths): boolean {
console.log(`[${paths.name}] Validating...`); 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 // Check if references directory exists
if (!existsSync(paths.referencesDir)) { if (!existsSync(paths.referencesDir)) {
console.log(` No references directory found.`); console.log(` No references directory found.`);

View File

@@ -1,10 +1,4 @@
# supabase-postgres-best-practices # 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.
## Structure ## Structure
@@ -22,7 +16,19 @@ supabase-postgres-best-practices/
2. Browse `references/` for detailed documentation on specific topics 2. Browse `references/` for detailed documentation on specific topics
3. Reference files are loaded on-demand - read only what you need 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 | | Priority | Category | Impact | Prefix |
|----------|----------|--------|--------| |----------|----------|--------|--------|
@@ -35,57 +41,28 @@ supabase-postgres-best-practices/
| 7 | Monitoring & Diagnostics | LOW-MEDIUM | `monitor-` | | 7 | Monitoring & Diagnostics | LOW-MEDIUM | `monitor-` |
| 8 | Advanced Features | LOW | `advanced-` | | 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/query-missing-indexes.md
- `references/advanced-jsonb-indexing.md` references/schema-partial-indexes.md
references/_sections.md
```
**Connection Management** (`conn-`): Each rule file contains:
- `references/conn-idle-timeout.md` - Brief explanation of why it matters
- `references/conn-limits.md` - Incorrect SQL example with explanation
- `references/conn-pooling.md` - Correct SQL example with explanation
- `references/conn-prepared-statements.md` - Optional EXPLAIN output or metrics
- Additional context and references
- Supabase-specific notes (when applicable)
**Data Access Patterns** (`data-`): ## References
- `references/data-batch-inserts.md`
- `references/data-n-plus-one.md`
- `references/data-pagination.md`
- `references/data-upsert.md`
**Concurrency & Locking** (`lock-`): - https://www.postgresql.org/docs/current/
- `references/lock-advisory.md` - https://supabase.com/docs
- `references/lock-deadlock-prevention.md` - https://wiki.postgresql.org/wiki/Performance_Optimization
- `references/lock-short-transactions.md` - https://supabase.com/docs/guides/database/overview
- `references/lock-skip-locked.md` - https://supabase.com/docs/guides/auth/row-level-security
**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*