Skill: erergb/auto-doc-index (version 1.2.0)

--- name: auto-doc-index description: "Auto-generate document index tables (ADR, RFC, Pitfall, etc.) from file frontmatter. In real-world testing, hand-maintained indexes had a 62% error rate — titles truncated, statuses fabricated, dates invented. This skill eliminates that silent drift. Use when creating doc directories, adding ADRs/RFCs, or setting up documentation governance." --- # Auto Doc Index — Derived Indexes from Frontmatter Replaces hand-maintained index tables in `README.md` with auto-generated tables derived from structured frontmatter in individual doc files. ## Why This Matters — Real Evidence In a real project with 13 ADR files, comparing hand-maintained index vs auto-generated index revealed **8 discrepancies (62% error rate)**: | Issue Type | Example | Count | |------------|---------|-------| | **Title truncated** | "activate none" vs actual "activate none **by default**" | 2 | | **Status fabricated** | Index said "Decided" but file said "Accepted" | 3 | | **Date invented** | Index showed "2026-01-28" but file had no Date field | 1 | | **Metadata lost** | "(revised 2026-01-28)" stripped from status | 1 | | **Case "normalized"** | `decided` silently changed to `Decided` | 4 | These aren't hypothetical risks — they were **already present and invisible** in a well-maintained project. Hand-editing creates a false sense of correctness while the index silently diverges from its source files. ## When to Use - Setting up a new documentation directory (ADR, RFC, Pitfall, Design Doc, etc.) - Adding a new document to an existing indexed directory - Onboarding a project that has hand-maintained doc indexes showing signs of drift - Resolving recurring merge conflicts in shared `README.md` index tables - Migrating from hand-maintained indexes to auto-generated ones ## Boundaries - This skill generates **index tables only** — it does not create or modify the content of individual documents. - The generator script replaces content **only between `<!-- INDEX:START -->` and `<!-- INDEX:END -->` markers**. All other README.md content is preserved verbatim. - Do NOT use this for indexes that require editorial curation (e.g., "recommended reading order"). Auto-generation is for factual, exhaustive catalogs. - Do NOT introduce YAML frontmatter parsing libraries — the regex-based approach is intentional to keep the script zero-dependency. - This skill targets file-system-based documentation. It does not apply to wiki-style or database-backed doc systems. ## Problem A hand-maintained index in `README.md` is **shared mutable state** — every new document requires editing the same file, same table, often the same diff hunk. In multi-agent or multi-contributor workflows this creates: - **Silent data loss**: titles get shortened, statuses get "corrected" - **Merge conflicts**: semantically independent changes collide in the same hunk - **Stale indexes**: contributors forget to update, nobody notices - **Normalization illusion**: edits look "cleaner" but diverge from source ## Solution Each document is self-describing via frontmatter. A generator script scans the directory, parses frontmatter, and injects the index table between `<!-- INDEX:START -->` / `<!-- INDEX:END -->` markers in README.md. **Write ops become N:N (each file independent). Index becomes a stateless pure function.** ## Setup Steps ### 1. Define frontmatter convention Choose a frontmatter format for your doc type. Two common patterns: **Pattern A — Inline metadata (ADR/RFC style):** ```markdown # ADR-001: Title Here Status: Decided Date: 2026-01-28 ## Context ... ``` **Pattern B — Bold-field metadata (Pitfall/Postmortem style):** ```markdown # PIT-001: Title Here **Date:** 2026-01-28 **Area:** engine **Severity:** high **Status:** resolved ``` ### 2. Add markers to README.md Wrap the existing index table (or create a placeholder) with markers: ```markdown ## Index <!-- INDEX:START --> | ADR | Title | Status | Date | |-----|-------|--------|------| <!-- INDEX:END --> ## Other Sections (preserved) ... ``` Content outside markers is **never touched** by the generator. ### 3. Create the generator script Copy `template/generate-doc-index.ts` from this skill's template directory, or generate a new one following the pattern below. **Core architecture (zero external dependencies):** ```typescript // 1. Scan directory for matching files (e.g. /^\d{3}-.*\.md$/) // 2. Parse frontmatter from each file (regex-based, no YAML lib needed) // 3. Sort entries by ID/number // 4. Generate markdown table string // 5. Inject between <!-- INDEX:START --> and <!-- INDEX:END --> markers ``` See [template/generate-doc-index.ts](template/generate-doc-index.ts) for a working implementation that handles both Pattern A and Pattern B. ### 4. Run the generator ```bash npx tsx scripts/generate-doc-index.ts all ``` ### 5. Update documentation governance Add to your project's AGENTS.md or CONTRIBUTING.md: > **Rule**: Never hand-edit the index table between `<!-- INDEX:START/END -->` > markers. To add a new document, create the `.md` file with proper > frontmatter, then run the generator. ## Workflow Comparison ``` OLD: Write doc → Hand-edit README.md index → Conflict risk NEW: Write doc → Run generator → Idempotent rebuild, zero conflicts ``` ## Adding a New Doc Type To support a new document category (e.g. RFCs, Design Docs): 1. Define the frontmatter convention 2. Add a parser function (regex for title, status, date, etc.) 3. Add a table generator function (column layout) 4. Add `<!-- INDEX:START/END -->` markers to the README.md 5. Register in the script's `main()` dispatcher ## Anti-patterns - Do NOT use YAML frontmatter libraries — regex is sufficient and avoids deps. - Do NOT generate the entire README.md — only the index section. Preserve manually-written intro, templates, and notes via the marker pattern. - Do NOT require contributors to run the generator before committing. Run it in CI or as a pre-commit hook for enforcement. ## Checklist ``` - [ ] Frontmatter convention defined for each doc type - [ ] README.md has <!-- INDEX:START --> and <!-- INDEX:END --> markers - [ ] Generator script created and tested - [ ] Documentation governance updated (AGENTS.md / CONTRIBUTING.md) - [ ] (Optional) Pre-commit hook or CI step added ```

path: CLAUDE.md
content: # Claude Code Configuration



path: README.md
content: # auto-doc-index

**Auto-generate document index tables from file frontmatter.**

AI agent skill that replaces hand-maintained `README.md` index tables with auto-generated ones derived from structured frontmatter in individual doc files. Eliminates merge conflicts, silent data drift, and stale indexes in multi-agent / multi-contributor documentation workflows.

## The Problem

Hand-maintained index tables in `README.md` are **shared mutable state**. In a real project with 13 ADR files, we measured an **62% error rate** in the hand-maintained index — titles truncated, statuses fabricated, dates invented. Nobody noticed because the index *looked* correct.

## The Solution

Each document is self-describing via frontmatter. A zero-dependency generator script scans the directory, parses frontmatter, and injects a fresh index table between `<!-- INDEX:START -->` / `<!-- INDEX:END -->` markers. The index becomes a **derived view** — a stateless pure function of the source files.

```
OLD: Write doc → Hand-edit README.md → Merge conflict risk
NEW: Write doc → Run generator → Idempotent rebuild, zero conflicts
```

## Quick Start

1. **Install as a Cursor skill:**
   ```bash
   skillkit install zjlpaul/auto-doc-index
   ```

2. **Or install via ClawHub:**
   ```bash
   openclaw install auto-doc-index
   ```

3. **Or just copy the template script:**
   Copy `template/generate-doc-index.ts` to your project's `scripts/` directory and run:
   ```bash
   npx tsx scripts/generate-doc-index.ts all
   ```

## Supported Formats

| Format | Agent |
|--------|-------|
| SKILL.md | Claude Code, Codex, Gemini CLI, 30+ agents |
| Cursor MDC | Cursor |
| Markdown Rules | Windsurf, GitHub Copilot |

Use [SkillKit](https://github.com/nicholasbarger/skillkit) to translate between formats:
```bash
skillkit translate --to cursor SKILL.md
skillkit translate --to windsurf SKILL.md
```

## Frontmatter Patterns

**ADR/RFC style (inline metadata):**
```markdown
# ADR-001: Title Here
Status: Decided
Date: 2026-01-28
```

**Pitfall/Postmortem style (bold fields):**
```markdown
# PIT-001: Title Here
**Area:** engine
**Severity:** high
**Status:** resolved
```

## License

MIT


path: skillkit.yaml
content: version: 1
agent: claude-code
autoSync: true


path: template/generate-doc-index.ts
content: #!/usr/bin/env npx tsx
/**
 * Auto-generates the index table in ADR and Pitfall README.md files.
 *
 * Scans *.md files in the target directory, parses frontmatter from each,
 * and replaces the content between <!-- INDEX:START --> / <!-- INDEX:END -->
 * markers in README.md. Content outside the markers is preserved.
 *
 * Usage:
 *   npx tsx scripts/generate-doc-index.ts adr
 *   npx tsx scripts/generate-doc-index.ts pitfall
 *   npx tsx scripts/generate-doc-index.ts all       # both
 *
 * Zero external dependencies — uses only Node.js built-ins.
 */

import { readdirSync, readFileSync, writeFileSync, existsSync } from 'fs';
import { join, basename } from 'path';

const DOC_ROOT = join(__dirname, '..', 'doc');

// ---------------------------------------------------------------------------
// Parsers
// ---------------------------------------------------------------------------

interface AdrEntry {
  num: string;
  title: string;
  status: string;
  date: string;
  file: string;
}

interface PitEntry {
  id: string;
  title: string;
  area: string;
  severity: string;
  status: string;
  file: string;
}

function parseAdr(filePath: string): AdrEntry | null {
  const content = readFileSync(filePath, 'utf-8');
  const name = basename(filePath);

  // Extract number from filename: 001-slug.md → "001"
  const numMatch = name.match(/^(\d+)-/);
  if (!numMatch) return null;

  // Title from H1: # ADR-NNN: Title
  const titleMatch = content.match(/^#\s+ADR-\d+:\s*(.+)$/m);
  const title = titleMatch?.[1]?.trim() ?? name;

  // Status: try "Status: value" line first, then "## Status\n\nvalue"
  let status = 'unknown';
  const statusLineMatch = content.match(/^Status:\s*(.+)$/im);
  if (statusLineMatch) {
    status = statusLineMatch[1].trim();
  } else {
    const statusSectionMatch = content.match(/^##\s+Status\s*\n+(\w+)/m);
    if (statusSectionMatch) {
      status = statusSectionMatch[1].trim();
    }
  }

  // Date: try "Date: value" line
  let date = '—';
  const dateMatch = content.match(/^Date:\s*(\d{4}-\d{2}-\d{2})/m);
  if (dateMatch) {
    date = dateMatch[1];
  }

  return { num: numMatch[1], title, status, date, file: name };
}

function parsePitfall(filePath: string): PitEntry | null {
  const content = readFileSync(filePath, 'utf-8');
  const name = basename(filePath);

  const idMatch = name.match(/^(PIT-\d+)/);
  if (!idMatch) return null;

  const titleMatch = content.match(/^#\s+PIT-\d+:\s*(.+)$/m);
  const title = titleMatch?.[1]?.trim() ?? name;

  const field = (key: string): string => {
    const m = content.match(new RegExp(`^\\*\\*${key}:\\*\\*\\s*(.+)$`, 'mi'));
    return m?.[1]?.trim() ?? '—';
  };

  return {
    id: idMatch[1],
    title,
    area: field('Area'),
    severity: field('Severity'),
    status: field('Status'),
    file: name,
  };
}

// ---------------------------------------------------------------------------
// Table generators
// ---------------------------------------------------------------------------

function generateAdrTable(entries: AdrEntry[]): string {
  const sorted = entries.sort((a, b) => a.num.localeCompare(b.num));
  const rows = sorted.map(
    (e) => `| ${e.num} | [${e.title}](${e.file}) | ${e.status} | ${e.date} |`,
  );
  return [
    '| ADR | Title | Status | Date |',
    '|-----|-------|--------|------|',
    ...rows,
  ].join('\n');
}

function generatePitfallTable(entries: PitEntry[]): string {
  const sorted = entries.sort((a, b) => a.id.localeCompare(b.id));
  const rows = sorted.map(
    (e) =>
      `| [${e.id}](${e.file}) | ${e.title} | ${e.area} | ${e.severity} | ${e.status} |`,
  );
  return [
    '| ID | Title | Area | Severity | Status |',
    '|----|-------|------|----------|--------|',
    ...rows,
  ].join('\n');
}

// ---------------------------------------------------------------------------
// README injection (marker-based)
// ---------------------------------------------------------------------------

const START_MARKER = '<!-- INDEX:START -->';
const END_MARKER = '<!-- INDEX:END -->';

function injectIndex(readmePath: string, table: string): void {
  if (!existsSync(readmePath)) {
    console.error(`README not found: ${readmePath}`);
    process.exit(1);
  }

  const content = readFileSync(readmePath, 'utf-8');
  const startIdx = content.indexOf(START_MARKER);
  const endIdx = content.indexOf(END_MARKER);

  let updated: string;
  if (startIdx !== -1 && endIdx !== -1) {
    const before = content.slice(0, startIdx + START_MARKER.length);
    const after = content.slice(endIdx);
    updated = `${before}\n${table}\n${after}`;
  } else {
    console.error(
      `Markers not found in ${readmePath}. Add ${START_MARKER} and ${END_MARKER} around the index section.`,
    );
    process.exit(1);
  }

  writeFileSync(readmePath, updated, 'utf-8');
  console.log(`✅ Updated ${readmePath}`);
}

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------

function processAdr(): void {
  const dir = join(DOC_ROOT, 'adr');
  const files = readdirSync(dir)
    .filter((f) => /^\d{3}-.*\.md$/.test(f))
    .map((f) => join(dir, f));

  const entries = files.map(parseAdr).filter(Boolean) as AdrEntry[];
  const table = generateAdrTable(entries);
  injectIndex(join(dir, 'README.md'), table);
  console.log(`   ${entries.length} ADR entries indexed`);
}

function processPitfall(): void {
  const dir = join(DOC_ROOT, 'pitfall');
  const files = readdirSync(dir)
    .filter((f) => /^PIT-\d+.*\.md$/.test(f))
    .map((f) => join(dir, f));

  const entries = files.map(parsePitfall).filter(Boolean) as PitEntry[];
  const table = generatePitfallTable(entries);
  injectIndex(join(dir, 'README.md'), table);
  console.log(`   ${entries.length} Pitfall entries indexed`);
}

const mode = process.argv[2] ?? 'all';
if (mode === 'adr' || mode === 'all') processAdr();
if (mode === 'pitfall' || mode === 'all') processPitfall();
if (!['adr', 'pitfall', 'all'].includes(mode)) {
  console.error('Usage: generate-doc-index.ts [adr|pitfall|all]');
  process.exit(1);
}


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)