---
name: ao-llm-export
description: "Export skills, agents, and prompts to llm.txt or JSON for LLM consumption"
category: utility
invokes: []
invoked_by: []
state_files:
  read: []
  write: []
---

# LLM Export Skill

## Purpose

Export all AO capabilities (skills, agents, prompts) into a structured format for external LLM consumption. Generates either markdown (llm.txt) or JSON output containing metadata and descriptions of all available artifacts.

**Use cases:**
- Enable external LLMs to understand AO capabilities
- Generate documentation for capability discovery
- Create API reference for AO integration
- Support automated capability mapping

## Input Arguments

| Argument | Values | Default | Description |
|----------|--------|---------|-------------|
| `--format` | `markdown`, `json` | `markdown` | Output format |
| `--include` | `skills`, `agents`, `prompts`, `all` | `all` | What to include (comma-separated) |
| `--content` | `metadata`, `summary`, `full` | `metadata` | Content depth to include |
| `--output` | `<path>` | `.agent/ops/llm.txt` or `.agent/ops/llm.json` | Custom output path |

**Content Modes:**
- `metadata` — Frontmatter fields only (name, description, category, etc.)
- `summary` — Metadata + first 50 lines or until first `---` separator
- `full` — Complete file content (uses Python script for large exports)

## Procedure

### Step 0: Detect Available Tools (for full content mode)

**Check `.agent/ops/tools.json`** for Python availability:

```
IF --content full specified:
    IF .agent/ops/tools.json exists:
        Read tools.json
        python_available = find tool where name="Python" AND available=true
        IF python_available:
            python_path = tool.path
            Log: ✓ Python detected: {python_path}
        ELSE:
            Log: ⚠️ Warning: Python not available, falling back to metadata mode
            content_mode = metadata
    ELSE:
        Log: ⚠️ Warning: No tools.json found, falling back to metadata mode
        content_mode = metadata
```

**Why Python?** Full content export can exceed LLM context windows. Python scripts can process files directly without context limits.

### Step 1: Parse Arguments

Extract and validate command arguments:

```
IF --format specified:
    format = markdown | json
ELSE:
    format = markdown

IF --include specified:
    include_list = parse_comma_separated(include_value)
    validate_values(include_list, [skills, agents, prompts, all])
ELSE:
    include_list = [all]

IF --output specified:
    output_path = custom_path
ELSE:
    output_path = .agent/ops/llm.txt (if markdown) or .agent/ops/llm.json (if json)

IF --content specified:
    content_mode = metadata | summary | full
    validate_value(content_mode, [metadata, summary, full])
ELSE:
    content_mode = metadata
```

Set scanning flags:
- `scan_skills = "skills" in include_list OR "all" in include_list`
- `scan_agents = "agents" in include_list OR "all" in include_list`
- `scan_prompts = "prompts" in include_list OR "all" in include_list`

### Step 1.5: Generate Python Script (if content=full and Python available)

**When `--content full` is specified and Python is available:**

Generate a Python script that will:
1. Scan all skill/agent/prompt files
2. Parse YAML frontmatter
3. Read complete file content
4. Generate structured output with collapsible sections
5. Write directly to output file

**Python Script Template:**

```python
#!/usr/bin/env python3
"""AO Capabilities Export - Full Content Mode"""
import os
import re
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

def parse_frontmatter(content: str) -> tuple[dict[str, Any], str]:
    """Extract YAML frontmatter and remaining content."""
    if not content.startswith('---'):
        return {}, content
    parts = content.split('---', 2)
    if len(parts) < 3:
        return {}, content
    frontmatter_text = parts[1].strip()
    body = parts[2].strip() if len(parts) > 2 else ''
    # Simple YAML parsing (no external deps)
    meta = {}
    for line in frontmatter_text.split('\n'):
        if ':' in line:
            key, val = line.split(':', 1)
            key = key.strip()
            val = val.strip().strip('"').strip("'")
            if val.startswith('[') and val.endswith(']'):
                val = [v.strip().strip('"').strip("'") for v in val[1:-1].split(',') if v.strip()]
            meta[key] = val
    return meta, body

def scan_files(pattern: str, base_path: Path) -> list[Path]:
    """Find files matching glob pattern."""
    return sorted(base_path.glob(pattern))

def generate_export(base_path: Path, output_path: Path, format: str, include: list[str], content_mode: str):
    """Generate the capabilities export."""
    scan_skills = 'skills' in include or 'all' in include
    scan_agents = 'agents' in include or 'all' in include
    scan_prompts = 'prompts' in include or 'all' in include
    
    skills, agents, prompts = [], [], []
    
    # Scan skills
    if scan_skills:
        for f in scan_files('.ao/skills/*/SKILL.md', base_path):
            try:
                content = f.read_text(encoding='utf-8')
                meta, body = parse_frontmatter(content)
                skills.append({
                    'name': meta.get('name', f.parent.name),
                    'description': meta.get('description', ''),
                    'category': meta.get('category', ''),
                    'file': str(f.relative_to(base_path)).replace('\\\\', '/'),
                    'invokes': meta.get('invokes', []) or [],
                    'invoked_by': meta.get('invoked_by', []) or [],
                    'full_content': content if content_mode == 'full' else (body[:2000] if content_mode == 'summary' else '')
                })
            except Exception as e:
                print(f'Warning: Could not parse {f}: {e}')
    
    # Scan agents
    if scan_agents:
        seen = {}
        for pattern in ['.github/agents/*.agent.md', 'tools/*/agents/*.agent.md']:
            for f in scan_files(pattern, base_path):
                if '.venv' in str(f) or '_bundle' in str(f):
                    continue
                try:
                    content = f.read_text(encoding='utf-8')
                    meta, body = parse_frontmatter(content)
                    name = meta.get('name', f.stem.replace('.agent', ''))
                    if name not in seen or '.github/agents' in str(f):
                        seen[name] = {
                            'name': name,
                            'description': meta.get('description', ''),
                            'file': str(f.relative_to(base_path)).replace('\\\\', '/'),
                            'argumentHint': meta.get('argumentHint', ''),
                            'full_content': content if content_mode == 'full' else (body[:2000] if content_mode == 'summary' else '')
                        }
                except Exception as e:
                    print(f'Warning: Could not parse {f}: {e}')
        agents = list(seen.values())
    
    # Scan prompts
    if scan_prompts:
        for f in scan_files('.github/prompts/*.prompt.md', base_path):
            try:
                content = f.read_text(encoding='utf-8')
                meta, body = parse_frontmatter(content)
                prompts.append({
                    'name': meta.get('name', f.stem.replace('.prompt', '')),
                    'description': meta.get('description', ''),
                    'agent': meta.get('agent', ''),
                    'file': str(f.relative_to(base_path)).replace('\\\\', '/'),
                    'full_content': content if content_mode == 'full' else (body[:2000] if content_mode == 'summary' else '')
                })
            except Exception as e:
                print(f'Warning: Could not parse {f}: {e}')
    
    # Sort all alphabetically
    skills.sort(key=lambda x: x['name'].lower())
    agents.sort(key=lambda x: x['name'].lower())
    prompts.sort(key=lambda x: x['name'].lower())
    
    # Generate output
    timestamp = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
    
    if format == 'json':
        output = {
            'generated': timestamp,
            'content_mode': content_mode,
            'stats': {'skills': len(skills), 'agents': len(agents), 'prompts': len(prompts)}
        }
        if scan_skills: output['skills'] = skills
        if scan_agents: output['agents'] = agents
        if scan_prompts: output['prompts'] = prompts
        output_path.parent.mkdir(parents=True, exist_ok=True)
        output_path.write_text(json.dumps(output, indent=2), encoding='utf-8')
    else:
        lines = ['# AO Capabilities Export', '', f'Generated: {timestamp}', f'Content Mode: {content_mode}', '', '---', '']
        
        if scan_skills:
            lines.extend([f'## Skills ({len(skills)} total)', ''])
            for s in skills:
                lines.extend([
                    f"### {s['name']}", '',
                    f"**Description**: {s['description']}",
                    f"**Category**: {s['category']}",
                    f"**File**: {s['file']}",
                    f"**Invokes**: {', '.join(s['invokes']) if s['invokes'] else 'none'}",
                    f"**Invoked By**: {', '.join(s['invoked_by']) if s['invoked_by'] else 'none'}", ''
                ])
                if s['full_content']:
                    lines.extend([
                        '<details>',
                        '<summary>Full Content (click to expand)</summary>', '',
                        '```markdown',
                        s['full_content'],
                        '```', '',
                        '</details>', ''
                    ])
                lines.extend(['---', ''])
        
        if scan_agents:
            lines.extend(['', f'## Agents ({len(agents)} total)', ''])
            for a in agents:
                lines.extend([
                    f"### {a['name']}", '',
                    f"**Description**: {a['description']}",
                    f"**File**: {a['file']}",
                    f"**Argument Hint**: {a['argumentHint'] or 'N/A'}", ''
                ])
                if a['full_content']:
                    lines.extend([
                        '<details>',
                        '<summary>Full Content (click to expand)</summary>', '',
                        '```markdown',
                        a['full_content'],
                        '```', '',
                        '</details>', ''
                    ])
                lines.extend(['---', ''])
        
        if scan_prompts:
            lines.extend(['', f'## Prompts ({len(prompts)} total)', ''])
            for p in prompts:
                lines.extend([
                    f"### {p['name']}", '',
                    f"**Description**: {p['description']}",
                    f"**Target Agent**: {p['agent'] or 'N/A'}",
                    f"**File**: {p['file']}", ''
                ])
                if p['full_content']:
                    lines.extend([
                        '<details>',
                        '<summary>Full Content (click to expand)</summary>', '',
                        '```markdown',
                        p['full_content'],
                        '```', '',
                        '</details>', ''
                    ])
                lines.extend(['---', ''])
        
        output_path.parent.mkdir(parents=True, exist_ok=True)
        output_path.write_text('\n'.join(lines), encoding='utf-8')
    
    print(f'✓ Export complete: {output_path}')
    print(f'  Skills: {len(skills)}, Agents: {len(agents)}, Prompts: {len(prompts)}')
    print(f'  Size: {output_path.stat().st_size / 1024:.1f} KB')

if __name__ == '__main__':
    import sys
    base = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
    output = Path(sys.argv[2]) if len(sys.argv) > 2 else base / '.agent' / 'llm.txt'
    fmt = sys.argv[3] if len(sys.argv) > 3 else 'markdown'
    include = sys.argv[4].split(',') if len(sys.argv) > 4 else ['all']
    content = sys.argv[5] if len(sys.argv) > 5 else 'full'
    generate_export(base, output, fmt, include, content)
```

**Execution:**
```
1. Locate colocated script: .ao/skills/ao-llm-export/export.py
2. Execute: python {skill_dir}/export.py {repo_root} {output_path} {format} {include} {content_mode}
3. Verify output file was created
```

**Note:** The export.py script is colocated with this SKILL.md file. Do not create temporary files elsewhere.

**If Python execution fails:**
```
Log: ⚠️ Python script failed, falling back to metadata mode
content_mode = metadata
Continue with Step 2 (normal scanning)
```

### Step 2: Scan for Skills (if enabled)

**Pattern**: `.ao/skills/*/SKILL.md`

For each skill file found:

1. **Read frontmatter** (YAML between `---` delimiters)
   ```yaml
   ---
   name: ao-planning
   description: "Produce thorough plans..."
   category: core
   invokes: [ao-state, ao-interview]
   invoked_by: []
   ---
   ```

2. **Extract metadata:**
   - `name` (string)
   - `description` (string)
   - `category` (string)
   - `invokes` (array of strings, default: [])
   - `invoked_by` (array of strings, default: [])
   - `file` (relative path from repo root)

3. **Handle errors:**
   - If file doesn't exist: skip silently
   - If YAML invalid: log warning, skip file
   - If required fields missing: use defaults

4. **Sort results** alphabetically by `name`

### Step 3: Scan for Agents (if enabled)

**Patterns**:
- `.github/agents/*.agent.md`
- `tools/*/agents/*.agent.md`

For each agent file found:

1. **Read frontmatter**
   ```yaml
   ---
   name: AO Worker
   description: "Plan→Implement→Review→Retrospective"
   argumentHint: "Task description or issue ID"
   ---
   ```

2. **Extract metadata:**
   - `name` (string)
   - `description` (string, default: "")
   - `argumentHint` (string, default: "")
   - `file` (relative path from repo root)

3. **Deduplicate:**
   - If same `name` found in multiple locations
   - Prefer `.github/agents/` over `tools/*/agents/`
   - Log: `ℹ️ Deduplicated agent '{name}' - using {chosen_path}`

4. **Sort results** alphabetically by `name`

**Handle missing directories:**
```
IF no agents/ directories found:
    Log: ⚠️ Warning: No agents/ directory found, skipping agent scanning
    Return empty agents list
```

### Step 4: Scan for Prompts (if enabled)

**Pattern**: `.github/prompts/*.prompt.md`

For each prompt file found:

1. **Read frontmatter**
   ```yaml
   ---
   name: ao-task
   description: "Create, refine, or manage issues"
   agent: AO
   ---
   ```

2. **Extract metadata:**
   - `name` (string)
   - `description` (string, default: "")
   - `agent` (string, default: "")
   - `file` (relative path from repo root)

3. **Sort results** alphabetically by `name`

**Handle missing directory:**
```
IF .github/prompts/ doesn't exist:
    Log: ⚠️ Warning: No prompts/ directory found, skipping prompt scanning
    Return empty prompts list
```

### Step 5: Generate Output

#### Markdown Format (llm.txt)

```markdown
# AO Capabilities Export

Generated: {ISO 8601 timestamp}
Repository: {git remote URL or local path}

---

## Skills ({count} total)

{for each skill in sorted order:}

### {skill.name}

**Description**: {skill.description}
**Category**: {skill.category}
**File**: {skill.file}
**Invokes**: {comma-separated list or "none"}
**Invoked By**: {comma-separated list or "none"}

---

{end for}

---

## Agents ({count} total)

{for each agent in sorted order:}

### {agent.name}

**Description**: {agent.description}
**File**: {agent.file}
**Argument Hint**: {agent.argumentHint or "N/A"}

---

{end for}

---

## Prompts ({count} total)

{for each prompt in sorted order:}

### {prompt.name}

**Description**: {prompt.description}
**Target Agent**: {prompt.agent or "N/A"}
**File**: {prompt.file}

---

{end for}
```

**Omit sections** if include filter excludes them:
- If skills not included: omit Skills section entirely
- If agents not included: omit Agents section entirely
- If prompts not included: omit Prompts section entirely

#### JSON Format

```json
{
  "generated": "{ISO 8601 timestamp}",
  "repository": "{git remote URL or local path}",
  "stats": {
    "skills": {count},
    "agents": {count},
    "prompts": {count}
  },
  "skills": [
    {
      "name": "ao-planning",
      "description": "Produce thorough plans before implementation...",
      "category": "core",
      "file": ".ao/skills/ao-planning/SKILL.md",
      "invokes": ["ao-state", "ao-interview"],
      "invoked_by": []
    }
  ],
  "agents": [
    {
      "name": "AO Worker",
      "description": "Plan→Implement→Review→Retrospective",
      "file": ".github/agents/ao-worker.agent.md",
      "argumentHint": "Task description or issue ID"
    }
  ],
  "prompts": [
    {
      "name": "ao-task",
      "description": "Create, refine, or manage issues",
      "agent": "AO",
      "file": ".github/prompts/ao-task.prompt.md"
    }
  ]
}
```

**Omit sections** if include filter excludes them:
- If skills not included: omit `skills` array
- If agents not included: omit `agents` array
- If prompts not included: omit `prompts` array
- Update stats to reflect included sections only

### Step 6: Write Output File

1. **Ensure output directory exists**
   ```
   IF parent directory of output_path doesn't exist:
       Create parent directory
   ```

2. **Write file**
   ```
   TRY:
       Write content to output_path
       Log: ✓ Export complete: {output_path}
   CATCH permission_error:
       Error: ❌ Cannot write to {output_path} - check permissions
       Exit with error code
   ```

3. **Report statistics**
   ```
   📊 Export Summary:
   - Skills: {count}
   - Agents: {count}
   - Prompts: {count}
   - Output: {output_path} ({file_size} KB)
   ```

## Error Handling

| Error | Behavior |
|-------|----------|
| Missing .ao/skills/ | Warn, continue with empty skills list |
| Missing .github/agents/ | Warn, continue with empty agents list |
| Missing .github/prompts/ | Warn, continue with empty prompts list |
| Invalid YAML frontmatter | Warn with file path, skip that file |
| Missing required frontmatter field | Use default value, continue |
| Cannot write output file | Error and exit with code 1 |
| Invalid --include value | Error with usage message, exit |
| Invalid --format value | Error with usage message, exit |

## Usage Examples

### Example 1: Default Export (markdown, all capabilities)

**Command:**
```
/ao-llm-export
```

**Output:**
- File: `.agent/ops/llm.txt`
- Contents: All skills, agents, prompts in markdown format
- Sorted alphabetically within each section

### Example 2: JSON Export

**Command:**
```
/ao-llm-export --format json
```

**Output:**
- File: `.agent/ops/llm.json`
- Contents: JSON structure with all capabilities
- Proper JSON formatting with indentation

### Example 3: Export Only Skills and Prompts

**Command:**
```
/ao-llm-export --include skills,prompts
```

**Output:**
- File: `.agent/ops/llm.txt`
- Contents: Skills and Prompts sections only (no Agents)

### Example 4: Custom Output Path

**Command:**
```
/ao-llm-export --output docs/capabilities.md
```

**Output:**
- File: `docs/capabilities.md`
- Contents: All capabilities in markdown format
- Creates docs/ directory if missing

### Example 5: JSON Skills Only to Custom Path

**Command:**
```
/ao-llm-export --format json --include skills --output .agent/ops/skills-only.json
```

**Output:**
- File: `.agent/ops/skills-only.json`
- Contents: JSON with skills array only

### Example 6: Full Content Export (uses Python)

**Command:**
```
/ao-llm-export --content full --output output/llm-full.txt
```

**Output:**
- File: `output/llm-full.txt`
- Contents: All capabilities with FULL file content in collapsible `<details>` sections
- Uses Python script to bypass context limits
- Typical size: 200-500 KB depending on codebase

### Example 7: Summary Content Export

**Command:**
```
/ao-llm-export --content summary
```

**Output:**
- File: `.agent/ops/llm.txt`
- Contents: Metadata + first 50 lines of each file
- Smaller than full export but more context than metadata-only

### Example 8: Full JSON Export with All Content

**Command:**
```
/ao-llm-export --format json --content full --output docs/full-capabilities.json
```

**Output:**
- File: `docs/full-capabilities.json`
- Contents: JSON with complete file content in `full_content` field
- Suitable for programmatic processing

## Completion Checklist

- [ ] Arguments parsed and validated
- [ ] Scanning flags set based on --include
- [ ] Skills scanned if enabled (or skipped with warning)
- [ ] Agents scanned if enabled (or skipped with warning)
- [ ] Prompts scanned if enabled (or skipped with warning)
- [ ] All results sorted alphabetically
- [ ] Output format generated (markdown or JSON)
- [ ] Output file written successfully
- [ ] Statistics reported to user
- [ ] No crashes on missing directories
- [ ] Clear warnings for skipped files

## Anti-patterns (avoid)

- ❌ Crashing when directories don't exist — handle gracefully
- ❌ Unsorted output — always alphabetize within sections
- ❌ Silent failures on YAML parse errors — warn user with file path
- ❌ Overwriting existing files without confirmation (in automated runs)
- ❌ Hardcoded paths — resolve relative to repo root dynamically
- ❌ Using full content mode without Python — check tools.json first
- ❌ Ignoring encoding issues — use UTF-8 with proper error handling

## Implementation Notes

**YAML Parsing:**
- Frontmatter is between first `---` and second `---`
- Parse as YAML dictionary
- Handle missing fields gracefully with defaults

**File Discovery:**
- Use glob patterns for file matching
- Recursive search for `tools/*/agents/*.agent.md`
- Filter out directories, only process files

**Deduplication (agents):**
- Build dictionary with agent name as key
- On collision, keep `.github/agents/` version
- Log deduplication for transparency

**Relative Paths:**
- Always resolve paths relative to repo root
- Use forward slashes `/` in output (cross-platform)
- Example: `.ao/skills/ao-planning/SKILL.md`

**Timestamp:**
- ISO 8601 format: `2026-01-25T14:30:00Z`
- Use UTC timezone

**Repository Detection:**
- Try `git remote get-url origin` for remote URL
- Fallback to local absolute path if not git repo
- Example: `https://github.com/user/ao-ops.git`
