Skill: athola/nm-sanctum-tutorial-updates (version 1.0.2)

--- name: tutorial-updates description: | Generate tutorials from VHS tapes and Playwright specs with dual-tone markdown and GIF recording version: 1.9.5 triggers: - tutorial - gif - vhs - playwright - documentation - demo metadata: {"openclaw": {"homepage": "https://github.com/athola/claude-night-market/tree/master/plugins/sanctum", "emoji": "\ud83e\udd9e", "requires": {"config": ["night-market.sanctum:shared", "night-market.sanctum:git-workspace-review", "night-market.scry:vhs-recording", "night-market.scry:browser-recording", "night-market.scry:gif-generation", "night-market.scry:media-composition"]}}} source: claude-night-market source_plugin: sanctum --- > **Night Market Skill** — ported from [claude-night-market/sanctum](https://github.com/athola/claude-night-market/tree/master/plugins/sanctum). For the full experience with agents, hooks, and commands, install the Claude Code plugin. ## Table of Contents - [Overview](#overview) - [Command Options](#command-options) - [Required TodoWrite Items](#required-todowrite-items) - [Phase 1: Discovery (`tutorial-updates:discovery`)](#phase-1:-discovery-(tutorial-updates:discovery)) - [Step 1.1: Locate Tutorial Assets](#step-11:-locate-tutorial-assets) - [Step 1.2: Parse Manifests](#step-12:-parse-manifests) - [Step 1.3: Handle Options](#step-13:-handle-options) - [Phase 1.5: Validation (`tutorial-updates:validation`)](#phase-15:-validation-(tutorial-updates:validation)) - [Step 1.5.1: VHS Syntax Validation](#step-151:-vhs-syntax-validation) - [Step 1.5.2: Extract and Validate CLI Commands](#step-152:-extract-and-validate-cli-commands) - [Step 1.5.3: Verify Demo Data Exists](#step-153:-verify-demo-data-exists) - [Step 1.5.4: Test Commands Locally](#step-154:-test-commands-locally) - [Validation Flags](#validation-flags) - [Validation Exit Criteria](#validation-exit-criteria) - [Phase 1.6: Binary Rebuild (`tutorial-updates:rebuild`)](#phase-16:-binary-rebuild-(tutorial-updates:rebuild)) - [Step 1.6.1: Detect Build System](#step-161:-detect-build-system) - [Step 1.6.2: Check Binary Freshness](#step-162:-check-binary-freshness) - [Step 1.6.3: Rebuild Binary](#step-163:-rebuild-binary) - [Step 1.6.4: Verify Binary Accessibility](#step-164:-verify-binary-accessibility) - [Rebuild Flags](#rebuild-flags) - [Rebuild Exit Criteria](#rebuild-exit-criteria) - [Phase 2: Recording (`tutorial-updates:recording`)](#phase-2:-recording-(tutorial-updates:recording)) - [Step 2.1: Process Tape Components](#step-21:-process-tape-components) - [Step 2.2: Process Browser Components](#step-22:-process-browser-components) - [Step 2.3: Handle Multi-Component Tutorials](#step-23:-handle-multi-component-tutorials) - [Phase 3: Generation (`tutorial-updates:generation`)](#phase-3:-generation-(tutorial-updates:generation)) - [Step 3.1: Parse Tape Annotations](#step-31:-parse-tape-annotations) - [Step 3.2: Generate Dual-Tone Markdown](#step-32:-generate-dual-tone-markdown) - [Step 3.3: Generate README Demo Section](#step-33:-generate-readme-demo-section) - [Demos](#demos) - [Quickstart](#quickstart) - [Phase 4: Integration (`tutorial-updates:integration`)](#phase-4:-integration-(tutorial-updates:integration)) - [Step 4.1: Verify All Outputs](#step-41:-verify-all-outputs) - [Step 4.2: Update SUMMARY.md (Book)](#step-42:-update-summarymd-(book)) - [Step 4.3: Report Results](#step-43:-report-results) - [Exit Criteria](#exit-criteria) - [Error Handling](#error-handling) - [Scaffold Mode](#scaffold-mode) # Tutorial Updates Skill Orchestrate tutorial generation with GIF recordings from VHS tape files and Playwright browser specs. ## When To Use - Generating or updating user-facing tutorials - Creating VHS and Playwright tutorial recordings ## When NOT To Use - Internal documentation without user-facing tutorials - API reference docs - use scribe:doc-generator instead ## Overview This skill coordinates the complete tutorial generation pipeline: 1. Discover tape files and manifests in the project 2. Validate tape commands and check binary freshness 3. Rebuild binaries if stale so demos reflect latest code 4. Record terminal sessions using VHS (scry:vhs-recording) 5. Record browser sessions using Playwright (scry:browser-recording) 6. Generate optimized GIFs (scry:gif-generation) 7. Compose multi-component tutorials (scry:media-composition) 8. Generate dual-tone markdown for docs/ and book/ ## Command Options ```bash /update-tutorial quickstart # Single tutorial by name /update-tutorial sync mcp # Multiple tutorials /update-tutorial --all # All tutorials with manifests /update-tutorial --list # Show available tutorials /update-tutorial --scaffold # Create structure without recording ``` **Verification:** Run the command with `--help` flag to verify availability. ## Required TodoWrite Items Create todos with these prefixes for progress tracking: ``` **Verification:** Run the command with `--help` flag to verify availability. - tutorial-updates:discovery - tutorial-updates:validation - tutorial-updates:rebuild - tutorial-updates:recording - tutorial-updates:generation - tutorial-updates:integration ``` **Verification:** Run the command with `--help` flag to verify availability. ## Phase 1: Discovery (`tutorial-updates:discovery`) ### Step 1.1: Locate Tutorial Assets Find tape files and manifests in the project: ```bash # Find manifest files find . -name "*.manifest.yaml" -type f \ -not -path "*/.venv/*" -not -path "*/__pycache__/*" \ -not -path "*/node_modules/*" -not -path "*/.git/*" \ 2>/dev/null | head -20 # Find tape files find . -name "*.tape" -type f \ -not -path "*/.venv/*" -not -path "*/__pycache__/*" \ -not -path "*/node_modules/*" -not -path "*/.git/*" \ 2>/dev/null | head -20 # Find browser specs find . -name "*.spec.ts" -path "*/browser/*" -type f \ -not -path "*/.venv/*" -not -path "*/__pycache__/*" \ -not -path "*/node_modules/*" -not -path "*/.git/*" \ 2>/dev/null | head -20 ``` **Verification:** Run the command with `--help` flag to verify availability. ### Step 1.2: Parse Manifests For each manifest file, extract: - Tutorial name and title - Component list (tape files, playwright specs) - Output paths for GIFs - Composition rules (layout, combine options) See `modules/manifest-parsing.md` for manifest schema details. ### Step 1.3: Handle Options | Option | Behavior | |--------|----------| | `--list` | Display discovered tutorials and exit | | `--all` | Process all discovered manifests | | `--scaffold` | Create directory structure and empty files without recording | | `<names>` | Process only specified tutorials | When `--list` is specified: ``` **Verification:** Run the command with `--help` flag to verify availability. Available tutorials: quickstart assets/tapes/quickstart.tape sync assets/tapes/sync.tape (manifest) mcp assets/tapes/mcp.manifest.yaml (terminal + browser) skill-debug assets/tapes/skill-debug.tape ``` **Verification:** Run the command with `--help` flag to verify availability. ## Phase 1.5: Validation (`tutorial-updates:validation`) **CRITICAL**: Validate tape commands BEFORE running VHS to avoid expensive regeneration cycles. See `modules/tape-validation.md` for detailed validation logic. ### Step 1.5.1: VHS Syntax Validation Check each tape file for valid VHS syntax: ```bash # Required: Output directive exists grep -q '^Output ' "$tape_file" || echo "ERROR: Missing Output directive" # Check for balanced quotes in Type directives grep '^Type ' "$tape_file" | while read -r line; do quote_count=$(echo "$line" | tr -cd '"' | wc -c) if [ $((quote_count % 2)) -ne 0 ]; then echo "ERROR: Unbalanced quotes: $line" fi done ``` **Verification:** Run the command with `--help` flag to verify availability. ### Step 1.5.2: Extract and Validate CLI Commands For each `Type` directive, extract the command and validate flags: ```bash # Extract commands from Type directives grep '^Type ' "$tape_file" | sed 's/^Type "//' | sed 's/"$//' | while read -r cmd; do # Skip comments, clear, and echo commands [[ "$cmd" =~ ^# ]] && continue [[ "$cmd" == "clear" ]] && continue # For skrills commands, validate flags exist if [[ "$cmd" =~ ^skrills ]]; then base_cmd=$(echo "$cmd" | awk '{print $1, $2}') flags=$(echo "$cmd" | grep -oE '\-\-[a-zA-Z0-9-]+' || true) for flag in $flags; do if ! $base_cmd --help 2>&1 | grep -q -- "$flag"; then echo "ERROR: Invalid flag '$flag' in command: $cmd" fi done fi done ``` **Verification:** Run the command with `--help` flag to verify availability. ### Step 1.5.3: Verify Demo Data Exists If the tape uses demo data, verify it exists and is populated: ```bash # Check SKRILLS_SKILL_DIR if set skill_dir=$(grep '^Env SKRILLS_SKILL_DIR' "$tape_file" | sed 's/.*"\(.*\)"/\1/') if [ -n "$skill_dir" ]; then if [ ! -d "$skill_dir" ]; then echo "ERROR: Demo skill directory missing: $skill_dir" else skill_count=$(find "$skill_dir" -name "SKILL.md" 2>/dev/null | wc -l) if [ "$skill_count" -eq 0 ]; then echo "ERROR: No skills in demo directory: $skill_dir" else echo "OK: Found $skill_count demo skills in $skill_dir" fi fi fi ``` **Verification:** Run the command with `--help` flag to verify availability. ### Step 1.5.4: Test Commands Locally **CRITICAL**: Run each extracted command locally to verify it produces expected output: ```bash # For each command in the tape, do a quick sanity check # This catches issues like: # - Commands that exit with non-zero status # - Commands that produce no output (won't show anything in GIF) # - Commands that require user input (will hang VHS) for cmd in $(extract_commands "$tape_file"); do # Run with timeout to catch hanging commands if ! timeout 5s bash -c "$cmd" &>/dev/null; then echo "WARNING: Command may fail or hang: $cmd" fi done ``` **Verification:** Run the command with `--help` flag to verify availability. ### Validation Flags | Flag | Behavior | |------|----------| | `--validate-only` | Run validation without generating GIF | | `--skip-validation` | Bypass validation for rapid regeneration | ### Validation Exit Criteria - [ ] VHS tape syntax is valid (Output directive, balanced quotes) - [ ] All CLI flags in commands are valid (verified against --help) - [ ] Demo data directories exist and are populated - [ ] Commands execute successfully with expected output **If validation fails**: Stop immediately, report errors, and do NOT proceed to VHS recording. ## Phase 1.6: Binary Rebuild (`tutorial-updates:rebuild`) **CRITICAL**: Ensure the binary being tested in tapes matches the latest source code. Stale binaries produce misleading demos. ### Step 1.6.1: Detect Build System Identify the project's build system: ```bash # Check for Cargo (Rust) if [ -f "Cargo.toml" ]; then BUILD_SYSTEM="cargo" BINARY_NAME=$(grep '^name = ' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/') echo "Detected Cargo project: $BINARY_NAME" # Check for Makefile elif [ -f "Makefile" ]; then BUILD_SYSTEM="make" echo "Detected Make project" # Unknown else echo "WARNING: Unknown build system, skipping binary check" BUILD_SYSTEM="unknown" fi ``` **Verification:** Run `make --dry-run` to verify build configuration. ### Step 1.6.2: Check Binary Freshness Compare binary modification time against Git HEAD: ```bash check_binary_freshness() { local binary_name="$1" # Locate binary (check cargo install location first, then PATH) local binary_path=$(which "$binary_name" 2>/dev/null) if [ -z "$binary_path" ]; then echo "WARNING: Binary '$binary_name' not found in PATH" return 1 fi # Get binary modification time (Linux/macOS compatible) local binary_mtime if command -v stat >/dev/null 2>&1; then # Linux binary_mtime=$(stat -c %Y "$binary_path" 2>/dev/null || \ # macOS stat -f %m "$binary_path" 2>/dev/null) else echo "WARNING: stat command not available, skipping freshness check" return 2 fi # Get Git HEAD commit time local git_head_time=$(git log -1 --format=%ct 2>/dev/null) if [ -z "$git_head_time" ]; then echo "WARNING: Not a git repository, skipping freshness check" return 2 fi # Compare timestamps if [ "$binary_mtime" -lt "$git_head_time" ]; then echo "STALE: Binary is older than Git HEAD" echo " Binary: $(date -d @$binary_mtime 2>/dev/null || date -r $binary_mtime)" echo " HEAD: $(date -d @$git_head_time 2>/dev/null || date -r $git_head_time)" return 1 else echo "OK: Binary is up-to-date" return 0 fi } ``` **Verification:** Run `git status` to confirm working tree state. ### Step 1.6.3: Rebuild Binary Rebuild using the detected build system: ```bash rebuild_binary() { local build_system="$1" local binary_name="$2" case "$build_system" in cargo) echo "Rebuilding with Cargo..." # Use cargo install for CLI binaries if [ -d "crates/cli" ]; then cargo install --path crates/cli --locked --quiet else cargo install --path . --locked --quiet fi ;; make) echo "Rebuilding with Make..." make build --quiet ;; *) echo "ERROR: Cannot rebuild, unknown build system" return 1 ;; esac echo "Build complete: $binary_name" } ``` **Verification:** Run `make --dry-run` to verify build configuration. ### Step 1.6.4: Verify Binary Accessibility Ensure the rebuilt binary is accessible: ```bash verify_binary() { local binary_name="$1" if ! command -v "$binary_name" >/dev/null 2>&1; then echo "ERROR: Binary '$binary_name' not found after rebuild" echo " Check PATH includes: $HOME/.cargo/bin" return 1 fi # Test binary can execute if ! "$binary_name" --version >/dev/null 2>&1; then echo "WARNING: Binary exists but --version failed" else echo "OK: Binary is accessible and functional" "$binary_name" --version fi } ``` **Verification:** Run `pytest -v` to verify tests pass. ### Rebuild Flags | Flag | Behavior | |------|----------| | `--skip-rebuild` | Skip binary freshness check and rebuild | | `--force-rebuild` | Force rebuild even if binary is fresh | ### Rebuild Exit Criteria - [ ] Build system detected (Cargo, Make, or explicitly skipped) - [ ] Binary freshness checked against Git HEAD - [ ] Binary rebuilt if stale (or forced) - [ ] Rebuilt binary is accessible in PATH - [ ] Binary executes successfully (--version test) **If rebuild fails**: Stop immediately, report build errors, and do NOT proceed to tape validation or VHS recording. ## Phase 2: Recording (`tutorial-updates:recording`) ### Step 2.1: Process Tape Components For each tape file component: 1. Parse tape file for metadata annotations (@step, @docs-brief, @book-detail) 2. Validate Output directive exists 3. Invoke `Skill(scry:vhs-recording)` with tape file path 4. Verify GIF output was created ### Step 2.2: Process Browser Components For each playwright spec component: 1. Check `requires` field for prerequisite commands (e.g., start server) 2. Launch any required background processes 3. Invoke `Skill(scry:browser-recording)` with spec path 4. Stop background processes 5. Invoke `Skill(scry:gif-generation)` to convert WebM to GIF ### Step 2.3: Handle Multi-Component Tutorials For manifests with `combine` section: 1. Verify all component GIFs exist 2. Invoke `Skill(scry:media-composition)` with manifest 3. Verify combined output was created ## Phase 3: Generation (`tutorial-updates:generation`) ### Step 3.1: Parse Tape Annotations Extract documentation content from tape files: ```tape # @step Install skrills # @docs-brief Install via cargo # @book-detail The recommended installation method uses cargo... Type "cargo install skrills" ``` **Verification:** Run the command with `--help` flag to verify availability. Annotations: - `@step` - Step title/heading - `@docs-brief` - Concise text for project docs (docs/ directory) - `@book-detail` - Extended text for technical book (book/ directory) ### Step 3.2: Generate Dual-Tone Markdown Generate two versions of each tutorial: 1. **Project docs** (`docs/tutorials/<name>.md`) - Brief, action-oriented - Uses @docs-brief content - Focuses on commands and quick results 2. **Technical book** (`book/src/tutorials/<name>.md`) - Detailed, educational - Uses @book-detail content - Explains concepts and rationale See `modules/markdown-generation.md` for formatting details. ### Step 3.3: Generate README Demo Section Create or update demo section in README.md: ```markdown ## Demos ### Quickstart ![Quickstart demo](assets/gifs/quickstart.gif) *Install, validate, analyze, and serve in under a minute. [Full tutorial](docs/tutorials/quickstart.md)* ``` **Verification:** Run the command with `--help` flag to verify availability. ## Phase 4: Integration (`tutorial-updates:integration`) ### Step 4.1: Verify All Outputs Confirm all expected files exist: ```bash # Check GIF files for gif in assets/gifs/*.gif; do if [[ -f "$gif" ]]; then echo "OK: $gif ($(du -h "$gif" | cut -f1))" else echo "MISSING: $gif" fi done # Check markdown files ls -la docs/tutorials/*.md 2>/dev/null ls -la book/src/tutorials/*.md 2>/dev/null ``` **Verification:** Run the command with `--help` flag to verify availability. ### Step 4.2: Update SUMMARY.md (Book) If the project has an mdBook structure, update `book/src/SUMMARY.md`: ```markdown - [Tutorials](./tutorials/README.md) - [Quickstart](./tutorials/quickstart.md) - [Sync Workflow](./tutorials/sync.md) - [MCP Integration](./tutorials/mcp.md) - [Skill Debugging](./tutorials/skill-debug.md) ``` **Verification:** Run the command with `--help` flag to verify availability. ### Step 4.3: Report Results Summarize the update: ``` **Verification:** Run the command with `--help` flag to verify availability. Tutorial Update Complete ======================== Tutorials processed: 4 GIFs generated: 5 - quickstart.gif (1.2MB) - sync.gif (980KB) - mcp-terminal.gif (1.5MB) - mcp-browser.gif (2.1MB) - skill-debug.gif (890KB) Markdown generated: - docs/tutorials/ (4 files) - book/src/tutorials/ (4 files) README demo section updated ``` **Verification:** Run the command with `--help` flag to verify availability. ## Exit Criteria - [ ] All specified tutorials processed (or all if --all) - [ ] GIF files created at manifest-specified paths - [ ] Dual-tone markdown generated for each tutorial - [ ] README demo section updated with GIF embeds - [ ] Book SUMMARY.md updated (if applicable) - [ ] All TodoWrite items completed ## Error Handling | Error | Resolution | |-------|------------| | VHS not installed | `go install github.com/charmbracelet/vhs@latest` | | Playwright not installed | `npm install -D @playwright/test && npx playwright install chromium` | | Tape file missing Output | Add `Output assets/gifs/<name>.gif` directive | | Browser spec requires server | Start server before running spec | | GIF too large | Adjust fps/scale in gif-generation | ## Scaffold Mode When `--scaffold` is specified, create structure without recording: 1. Create `assets/tapes/` directory 2. Create `assets/gifs/` directory 3. Create `assets/browser/` directory (if browser tutorials planned) 4. Create template tape file with metadata annotations 5. Create template manifest file 6. Create empty markdown files in docs/tutorials/ and book/src/tutorials/ Template tape file: ```tape # @title: Tutorial Name # @description: Brief description of the tutorial Output assets/gifs/tutorial-name.gif Set FontSize 14 Set Width 1200 Set Height 600 Set Theme "Catppuccin Mocha" # @step Step 1 Title # @docs-brief Brief docs text # @book-detail Extended book text with more context and explanation Type "command here" Enter Sleep 2s ``` **Verification:** Run the command with `--help` flag to verify availability. ## Troubleshooting ### Common Issues **Command not found** Ensure all dependencies are installed and in PATH **Permission errors** Check file permissions and run with appropriate privileges **Unexpected behavior** Enable verbose logging with `--verbose` flag

path: modules/manifest-parsing.md
content: # Manifest Parsing Module

Parse `.manifest.yaml` files and tape file annotations for tutorial orchestration.

## Manifest Schema

Tutorial manifests define multi-component tutorials with composition rules:

```yaml
# Full manifest schema
name: string              # Required: identifier for the tutorial
title: string             # Optional: human-readable title
description: string       # Optional: brief description

components:               # Required: list of media components
  - type: tape            # Component type: tape, playwright, static
    source: path/to.tape  # Path to source file (relative to manifest)
    output: path/to.gif   # Path for generated output
    options:              # Optional: component-specific options
      fps: 10
      width: 800

  - type: playwright
    source: browser/spec.ts
    output: assets/gifs/browser.gif
    requires:             # Optional: commands to run before
      - "npm run serve"

  - type: static          # Pre-existing asset (no generation)
    source: existing.gif
    output: existing.gif

combine:                  # Optional: composition rules
  output: combined.gif    # Path for combined output
  layout: vertical        # Layout: vertical, horizontal, sequential, grid, pip
  options:
    padding: 10
    background: "#1a1a2e"
```

## Component Types

### Tape Components

VHS tape files for terminal recordings:

```yaml
- type: tape
  source: quickstart.tape
  output: assets/gifs/quickstart.gif
  options:
    # Override tape file settings if needed
    width: 1000
    height: 500
```

### Playwright Components

Browser automation specs:

```yaml
- type: playwright
  source: browser/mcp-dashboard.spec.ts
  output: assets/gifs/mcp-browser.gif
  requires:
    - "skrills serve"
  options:
    fps: 12
    width: 1280
```

The `requires` array specifies commands to run before the spec. These run as background processes and are terminated after recording.

### Static Components

Pre-existing assets that don't need generation:

```yaml
- type: static
  source: diagrams/architecture.gif
  output: diagrams/architecture.gif
```

## Parsing Tape File Annotations

Tape files contain inline annotations for documentation generation:

### Annotation Format

```tape
# @title: Tutorial Title
# @description: Brief description for README

# @step Step Name
# @docs-brief Concise text for project docs
# @book-detail Extended explanation for technical book
Type "command"
Enter
```

### Annotation Types

| Annotation | Scope | Purpose |
|------------|-------|---------|
| `@title` | File | Tutorial title |
| `@description` | File | Brief description |
| `@step` | Block | Step heading |
| `@docs-brief` | Block | Concise docs text |
| `@book-detail` | Block | Extended book text |

### Parsing Algorithm

```python
def parse_tape_annotations(tape_content: str) -> dict:
    """Parse tape file for documentation annotations."""
    result = {
        "title": None,
        "description": None,
        "steps": []
    }

    current_step = None

    for line in tape_content.splitlines():
        line = line.strip()

        # File-level annotations
        if line.startswith("# @title:"):
            result["title"] = line.split(":", 1)[1].strip()
        elif line.startswith("# @description:"):
            result["description"] = line.split(":", 1)[1].strip()

        # Step-level annotations
        elif line.startswith("# @step"):
            # Save previous step
            if current_step:
                result["steps"].append(current_step)
            # Start new step
            step_name = line.replace("# @step", "").strip()
            current_step = {
                "name": step_name,
                "docs_brief": None,
                "book_detail": None,
                "commands": []
            }
        elif line.startswith("# @docs-brief"):
            if current_step:
                current_step["docs_brief"] = line.replace("# @docs-brief", "").strip()
        elif line.startswith("# @book-detail"):
            if current_step:
                current_step["book_detail"] = line.replace("# @book-detail", "").strip()

        # Command lines (Type, Enter, etc.)
        elif current_step and line.startswith("Type"):
            # Extract command text
            match = re.match(r'Type(?:@\d+ms)?\s+"(.+)"', line)
            if match:
                current_step["commands"].append(match.group(1))

    # Don't forget last step
    if current_step:
        result["steps"].append(current_step)

    return result
```

## Manifest Validation

### Required Fields

```bash
# Validate manifest has required fields
yq eval '.name' manifest.yaml >/dev/null || echo "ERROR: missing name"
yq eval '.components | length > 0' manifest.yaml | grep -q true || echo "ERROR: no components"

# Validate each component
for i in $(seq 0 $(($(yq eval '.components | length' manifest.yaml) - 1))); do
  yq eval ".components[$i].type" manifest.yaml >/dev/null || echo "ERROR: component $i missing type"
  yq eval ".components[$i].source" manifest.yaml >/dev/null || echo "ERROR: component $i missing source"
  yq eval ".components[$i].output" manifest.yaml >/dev/null || echo "ERROR: component $i missing output"
done
```

### Source File Validation

```bash
# Check all source files exist
for source in $(yq eval '.components[].source' manifest.yaml); do
  if [[ ! -f "$source" ]]; then
    echo "ERROR: Source file not found: $source"
  fi
done
```

## Discovery Patterns

### Find All Manifests

```bash
# Find manifest files in common locations
find . -name "*.manifest.yaml" -type f \
  -not -path "*/.venv/*" -not -path "*/__pycache__/*" \
  -not -path "*/node_modules/*" -not -path "*/.git/*" \
  2>/dev/null
find assets -name "*.manifest.yaml" -type f 2>/dev/null
find tutorials -name "*.manifest.yaml" -type f 2>/dev/null
```

### Find Standalone Tape Files

Tape files without manifests (single-component tutorials):

```bash
# Find tape files
find . -name "*.tape" -type f \
  -not -path "*/.venv/*" -not -path "*/__pycache__/*" \
  -not -path "*/node_modules/*" -not -path "*/.git/*" \
  2>/dev/null

# Filter out those with manifests
for tape in $(find . -name "*.tape" -type f \
  -not -path "*/.venv/*" -not -path "*/__pycache__/*" \
  -not -path "*/node_modules/*" -not -path "*/.git/*" \
  2>/dev/null); do
  manifest="${tape%.tape}.manifest.yaml"
  if [[ ! -f "$manifest" ]]; then
    echo "Standalone: $tape"
  fi
done
```

### Build Tutorial Index

```bash
# Create index of all tutorials
echo "Tutorials:"
echo "=========="

# From manifests
for manifest in $(find . -name "*.manifest.yaml" -type f \
  -not -path "*/.venv/*" -not -path "*/__pycache__/*" \
  -not -path "*/node_modules/*" -not -path "*/.git/*" \
  2>/dev/null); do
  name=$(yq eval '.name' "$manifest")
  title=$(yq eval '.title // .name' "$manifest")
  components=$(yq eval '.components | length' "$manifest")
  echo "  $name: $title ($components components) [manifest]"
done

# Standalone tapes
for tape in $(find . -name "*.tape" -type f \
  -not -path "*/.venv/*" -not -path "*/__pycache__/*" \
  -not -path "*/node_modules/*" -not -path "*/.git/*" \
  2>/dev/null); do
  manifest="${tape%.tape}.manifest.yaml"
  if [[ ! -f "$manifest" ]]; then
    name=$(basename "$tape" .tape)
    echo "  $name: $tape [standalone]"
  fi
done
```

## Error Handling

| Error | Resolution |
|-------|------------|
| Manifest parse error | Validate YAML syntax with `yq eval '.' manifest.yaml` |
| Missing source file | Check path is relative to manifest location |
| Unknown component type | Use `tape`, `playwright`, or `static` |
| Missing combine output | Add `combine.output` field if combine section exists |
| Circular requires | validate background processes don't depend on each other |

## Example Manifests

### Simple Tape Tutorial

```yaml
name: quickstart
title: "Quickstart Guide"
components:
  - type: tape
    source: quickstart.tape
    output: assets/gifs/quickstart.gif
```

### Multi-Component Tutorial

```yaml
name: mcp
title: "MCP Server Integration"
description: "Terminal and browser demo of MCP server"
components:
  - type: tape
    source: mcp-terminal.tape
    output: assets/gifs/mcp-terminal.gif
  - type: playwright
    source: browser/mcp-dashboard.spec.ts
    output: assets/gifs/mcp-browser.gif
    requires:
      - "skrills serve"
combine:
  output: assets/gifs/mcp-combined.gif
  layout: vertical
  options:
    padding: 10
    background: "#0d1117"
```

### Tutorial with Static Assets

```yaml
name: architecture
title: "Architecture Overview"
components:
  - type: tape
    source: arch-demo.tape
    output: assets/gifs/arch-demo.gif
  - type: static
    source: diagrams/system-overview.gif
    output: diagrams/system-overview.gif
combine:
  output: assets/gifs/architecture-full.gif
  layout: sequential
```


path: modules/markdown-generation.md
content: # Markdown Generation Module

Generate dual-tone markdown documentation from tape file annotations and manifest metadata.

## Dual-Tone System

Tutorials are generated in two tones for different audiences:

| Tone | Location | Audience | Style |
|------|----------|----------|-------|
| **Project Docs** | `docs/tutorials/` | Users getting started | Concise, action-oriented |
| **Technical Book** | `book/src/tutorials/` | Developers learning deeply | Detailed, educational |

## Annotation Sources

Content comes from tape file annotations:

```tape
# @step Install the CLI
# @docs-brief Install via cargo with a single command
# @book-detail The recommended installation method uses cargo, Rust's package manager. This validates you get the latest stable release with all dependencies properly resolved. For development builds or specific versions, you can also install from source.
Type "cargo install skrills"
```

- `@docs-brief` - Used for project docs (brief, focused)
- `@book-detail` - Used for technical book (extended, contextual)
- If only one is present, use it for both
- If neither is present, generate minimal text from step name

## Project Docs Format

### Template Structure

```markdown
# {Tutorial Title}

{Description from @description}

![Demo]({relative-path-to-gif})

## Prerequisites

- Prerequisite 1
- Prerequisite 2

## Steps

### {Step 1 Name}

{@docs-brief content}

```bash
{command from Type directive}
```

### {Step 2 Name}

{@docs-brief content}

```bash
{command}
```

## Next Steps

- Link to related tutorial 1
- Link to related tutorial 2
```

### Example Output

```markdown
# Quickstart

Install, validate, analyze, and serve in under a minute.

![Quickstart demo](../../assets/gifs/quickstart.gif)

## Prerequisites

- Rust toolchain installed (`rustup`)
- Terminal with UTF-8 support

## Steps

### Install skrills

Install via cargo with a single command.

```bash
cargo install skrills
```

### Validate Skills

Validate and auto-fix missing frontmatter.

```bash
skrills validate --target codex --autofix
```

### Analyze Token Usage

Analyze skills for token optimization opportunities.

```bash
skrills analyze --min-tokens 500 --suggestions
```

### Start MCP Server

Start the MCP server.

```bash
skrills serve
```

## Next Steps

- [Sync Workflow](./sync.md) - Bidirectional sync between Claude Code and Codex CLI
- [MCP Integration](./mcp.md) - Use skrills as an MCP server
```

## Technical Book Format

### Template Structure

```markdown
# {Tutorial Title}

{Extended description}

## Overview

{Context and learning objectives}

![Demo]({relative-path-to-gif})

## {Step 1 Name}

{@book-detail content - multiple paragraphs allowed}

```bash
{command from Type directive}
```

{Additional explanation of what the command does}

## {Step 2 Name}

{@book-detail content}

```bash
{command}
```

{Explanation of output and next steps}

## Summary

{Key takeaways}

## Further Reading

- Internal link 1
- External reference 1
```

### Example Output

```markdown
# Quickstart

This guide walks through the complete skrills workflow: installation, validation, analysis, and serving skills via MCP.

## Overview

By the end of this tutorial, you'll understand:
- How to install skrills using cargo
- The difference between Claude Code and Codex CLI validation targets
- How to analyze skills for token optimization
- How to expose skills via the MCP protocol

![Quickstart demo](../assets/gifs/quickstart.gif)

## Install skrills

The recommended installation method uses cargo, Rust's package manager. This validates you get the latest stable release with all dependencies properly resolved. For development builds or specific versions, you can also install from source.

```bash
cargo install skrills
```

The binary will be placed in `~/.cargo/bin/`, which should be in your PATH if you installed Rust using rustup.

## Validate Skills

Skrills validates skills against two targets with different strictness levels. Claude Code accepts any markdown file as a skill, while Codex CLI requires YAML frontmatter with specific fields.

```bash
skrills validate --target codex --autofix
```

The `--autofix` flag automatically derives missing frontmatter from the file path and content:
1. Parses the skill filename to derive `name`
2. Extracts the first paragraph as `description`
3. Inserts YAML frontmatter at the file start

This makes migration from Claude Code to Codex straightforward.

## Analyze Token Usage

The analyzer reports token counts for each skill and suggests optimizations for large skills that may consume excessive context.

```bash
skrills analyze --min-tokens 500 --suggestions
```

Skills exceeding the threshold are flagged with specific recommendations:
- Split into multiple focused skills
- Extract reusable modules
- Remove redundant content

## Start MCP Server

When running as an MCP server, skrills exposes tools for skill discovery, validation, and analysis to any MCP-compatible client.

```bash
skrills serve
```

The server listens on the default MCP port and responds to tool invocations from Claude Code or other clients.

## Summary

- Install skrills with `cargo install skrills`
- Validate skills for Codex compatibility with `--target codex`
- Use `--autofix` to automatically add required frontmatter
- Analyze token usage to optimize context consumption
- Serve skills via MCP for integration with AI assistants

## Further Reading

- [Sync Workflow](./sync.md) - Bidirectional synchronization between skill repositories
- [MCP Integration](./mcp.md) - Advanced MCP server configuration
- [Skill Debugging](./skill-debug.md) - Troubleshooting skill loading issues
```

## GIF Embedding

### Relative Path Calculation

GIFs are embedded with paths relative to the markdown file:

| Markdown Location | GIF Location | Relative Path |
|-------------------|--------------|---------------|
| `docs/tutorials/quickstart.md` | `assets/gifs/quickstart.gif` | `../../assets/gifs/quickstart.gif` |
| `book/src/tutorials/quickstart.md` | `assets/gifs/quickstart.gif` | `../../../assets/gifs/quickstart.gif` |
| `README.md` | `assets/gifs/quickstart.gif` | `assets/gifs/quickstart.gif` |

### Path Calculation Algorithm

```python
def relative_gif_path(markdown_path: str, gif_path: str) -> str:
    """Calculate relative path from markdown file to GIF."""
    from pathlib import Path

    md = Path(markdown_path)
    gif = Path(gif_path)

    # Get common ancestor
    common = Path(*os.path.commonprefix([md.parts, gif.parts]))

    # Calculate relative path
    md_depth = len(md.parent.relative_to(common).parts)
    gif_relative = gif.relative_to(common)

    return "../" * md_depth + str(gif_relative)
```

### Embedding Format

```markdown
![{Alt text}]({relative-path})
```

For multi-component tutorials with combined GIF:

```markdown
![{Tutorial title} demo]({relative-path-to-combined-gif})

*This demo shows both terminal and browser interactions.*
```

## README Integration

### Demo Section Template

```markdown
## Demos

### {Tutorial 1 Title}
![{Tutorial 1 title} demo]({gif-path})
*{Description}. [Full tutorial]({docs-tutorial-path})*

### {Tutorial 2 Title}
![{Tutorial 2 title} demo]({gif-path})
*{Description}. [Full tutorial]({docs-tutorial-path})*
```

### Generating README Section

```python
def generate_readme_demos(tutorials: list) -> str:
    """Generate demo section for README."""
    lines = ["## Demos", ""]

    for tutorial in tutorials:
        lines.extend([
            f"### {tutorial['title']}",
            f"![{tutorial['title']} demo]({tutorial['gif_path']})",
            f"*{tutorial['description']}. [Full tutorial]({tutorial['docs_path']})*",
            ""
        ])

    return "\n".join(lines)
```

### Updating README

Replace the existing demo section or append if not present:

```bash
# Check if demo section exists
if grep -q "^## Demos" README.md; then
  # Replace section (between ## Demos and next ##)
  sed -i '/^## Demos/,/^## [^D]/{ /^## [^D]/!d }' README.md
  # Insert new content after ## Demos
fi
```

## Book SUMMARY.md Integration

### Template

```markdown
- [Tutorials](./tutorials/README.md)
  - [{Tutorial 1 Title}](./tutorials/{name1}.md)
  - [{Tutorial 2 Title}](./tutorials/{name2}.md)
```

### Detection and Update

```bash
# Check if tutorials section exists in SUMMARY.md
if [[ -f "book/src/SUMMARY.md" ]]; then
  if grep -q "Tutorials" book/src/SUMMARY.md; then
    echo "Tutorials section exists - update entries"
  else
    echo "Add Tutorials section to SUMMARY.md"
  fi
fi
```

## Content Guidelines

### Project Docs Style

- Action-oriented imperatives: "Install", "Run", "Configure"
- One paragraph per step maximum
- Focus on commands and results
- Minimal explanation of "why"
- Include prerequisites section

### Technical Book Style

- Educational tone: "This guide explains..."
- Multiple paragraphs allowed per step
- Explain rationale and context
- Include troubleshooting tips
- Reference related concepts
- Add summary and further reading sections

### Common Rules (Both)

- No filler phrases ("simply", "just", "easily")
- No emojis or decorative elements
- Grounded, specific language
- Code blocks for all commands
- Consistent heading hierarchy
- Prose text wraps at 80 chars per line (hybrid wrapping:
  prefer sentence/clause boundaries over arbitrary breaks)
- Blank line before and after every heading
- ATX headings only (`#` prefix, no setext underlines)
- Blank line before every list
- Reference-style links when inline links push past 80 chars
- Full formatting spec: `Skill(leyline:markdown-formatting)`

## Error Handling

| Issue | Resolution |
|-------|------------|
| Missing @docs-brief | Fall back to @book-detail or step name |
| Missing @book-detail | Fall back to @docs-brief or generate minimal text |
| No Output directive | Skip GIF embed, log warning |
| Invalid path calculation | Verify markdown and GIF paths are relative to project root |
| README section conflict | Preserve user content outside ## Demos section |


path: modules/tape-validation.md
content: # Tape Validation Module

Pre-flight validation for VHS tape files before GIF generation. Validate commands work correctly and demo data exists BEFORE running the time-consuming VHS recording.

## Overview

This module catches errors early:
- Stale binaries detected before recording (Phase 0)
- Invalid CLI flags discovered before GIF generation
- Missing demo data detected before recording starts
- VHS syntax issues reported before execution

## Validation Phases

### Phase 0: Binary Freshness Check

**CRITICAL**: Verify the CLI binary matches the latest source code. Stale binaries produce misleading demos.

```bash
# Check if binary is older than Git HEAD
check_binary_freshness() {
  local binary_name="$1"

  # Locate binary in PATH
  local binary_path=$(which "$binary_name" 2>/dev/null)

  if [ -z "$binary_path" ]; then
    echo "WARNING: Binary '$binary_name' not found in PATH"
    return 1
  fi

  # Get binary modification time (Linux/macOS compatible)
  local binary_mtime
  binary_mtime=$(stat -c %Y "$binary_path" 2>/dev/null || \
                 stat -f %m "$binary_path" 2>/dev/null)

  # Get Git HEAD commit time
  local git_head_time=$(git log -1 --format=%ct 2>/dev/null)

  if [ -z "$git_head_time" ]; then
    echo "WARNING: Not a git repository, skipping freshness check"
    return 2
  fi

  # Compare timestamps
  if [ "$binary_mtime" -lt "$git_head_time" ]; then
    echo "STALE: Binary is older than Git HEAD"
    echo "  Binary: $(date -d @$binary_mtime 2>/dev/null || date -r $binary_mtime)"
    echo "  HEAD:   $(date -d @$git_head_time 2>/dev/null || date -r $git_head_time)"
    echo ""
    echo "RECOMMENDATION: Rebuild binary before recording tape"
    echo "  cargo install --path crates/cli --locked"
    return 1
  else
    echo "OK: Binary is up-to-date with Git HEAD"
    return 0
  fi
}
```

**Error handling**:
- Exit code 0: Binary is fresh
- Exit code 1: Binary is stale (older than HEAD)
- Exit code 2: Cannot determine (not in git repo or binary not found)

**Integration**:
```bash
# Run before tape validation
check_binary_freshness "skrills"
if [ $? -eq 1 ]; then
  echo "ERROR: Stale binary detected"
  echo "Run 'cargo install --path crates/cli --locked' to rebuild"
  exit 1
fi
```

## Command Validation Phases

### 1. VHS Syntax Validation

Check that the tape file has valid VHS syntax:

```bash
# Required: Output directive
grep -q '^Output ' "$tape_file" || echo "ERROR: Missing Output directive"

# Check for balanced quotes in Type directives
grep '^Type ' "$tape_file" | while read -r line; do
  # Count quotes (should be even)
  quote_count=$(echo "$line" | tr -cd '"' | wc -c)
  if [ $((quote_count % 2)) -ne 0 ]; then
    echo "ERROR: Unbalanced quotes in: $line"
  fi
done
```

**Error examples**:
- "Missing Output directive in quickstart.tape:1"
- "Unbalanced quotes in Type directive at quickstart.tape:15"

### 2. Command Extraction

Parse `Type` directives to extract shell commands:

```bash
# Extract commands from Type directives
extract_commands() {
  local tape_file="$1"
  grep '^Type ' "$tape_file" | \
    sed 's/^Type "//' | \
    sed 's/"$//' | \
    grep -v '^#' | \
    grep -v '^clear$' | \
    grep -v '^$'
}
```

**Example**:
```vhs
Type "skrills validate --errors-only"
```
Extracts: `skrills validate --errors-only`

### 3. CLI Flag Validation

For each extracted command, validate flags exist:

```bash
validate_command_flags() {
  local cmd="$1"
  local line_num="$2"

  # Extract the base command (e.g., "skrills validate")
  local base_cmd=$(echo "$cmd" | awk '{print $1, $2}')

  # Get help output for flag discovery
  local help_output=$($base_cmd --help 2>&1)

  # Extract flags from the command
  local flags=$(echo "$cmd" | grep -oE '\-\-[a-zA-Z0-9-]+')

  for flag in $flags; do
    if ! echo "$help_output" | grep -q -- "$flag"; then
      echo "ERROR: Invalid flag '$flag' at line $line_num"
      echo "  Command: $cmd"
      echo "  Run '$base_cmd --help' to see available flags"
    fi
  done
}
```

**Example validation**:
```bash
# Command from tape line 12
skrills create demo --sample basic

# Validation
$ skrills create --help | grep -- '--sample'
# (no output - flag doesn't exist)

# Error output
ERROR: Invalid flag '--sample' at line 12
  Command: skrills create demo --sample basic
  Run 'skrills create --help' to see available flags
```

### 4. Demo Data Verification

Check that demo directories and skills exist:

```bash
verify_demo_data() {
  local tape_file="$1"

  # Extract Env SKRILLS_SKILL_DIR if set
  local skill_dir=$(grep '^Env SKRILLS_SKILL_DIR' "$tape_file" | \
    sed 's/.*"\(.*\)"/\1/')

  if [ -n "$skill_dir" ]; then
    if [ ! -d "$skill_dir" ]; then
      echo "ERROR: Demo skill directory does not exist: $skill_dir"
      return 1
    fi

    # Check it has content
    local skill_count=$(find "$skill_dir" -name "SKILL.md" 2>/dev/null | wc -l)
    if [ "$skill_count" -eq 0 ]; then
      echo "ERROR: Demo skill directory is empty: $skill_dir"
      echo "  Expected at least one SKILL.md file"
      return 1
    fi

    echo "OK: Demo skills found: $skill_count skills in $skill_dir"
  fi

  # Check for other referenced directories
  grep '^Type.*mkdir\|^Type.*cp\|^Type.*cd' "$tape_file" | while read -r line; do
    # Extract directory paths and verify parent exists
    # This is optional - catches obvious path errors
    true
  done
}
```

### 5. Expected Output Verification

For commands that should produce visible output, verify they will:

```bash
verify_expected_output() {
  local cmd="$1"

  # Skip echo commands (always produce output)
  echo "$cmd" | grep -q '^echo ' && return 0

  # For skrills commands, do a dry-run to verify output
  if echo "$cmd" | grep -q '^skrills '; then
    # Run with --help to verify command exists
    local base=$(echo "$cmd" | awk '{print $1, $2}')
    if ! $base --help &>/dev/null; then
      echo "WARNING: Command may not produce output: $cmd"
    fi
  fi
}
```

## Complete Validation Script

```bash
#!/bin/bash
# validate_tape.sh - Pre-flight validation for VHS tape files

validate_tape() {
  local tape_file="$1"
  local errors=0

  echo "=== Validating: $tape_file ==="

  # Phase 1: Syntax
  echo "Phase 1: VHS Syntax..."
  if ! grep -q '^Output ' "$tape_file"; then
    echo "  ERROR: Missing Output directive"
    ((errors++))
  else
    echo "  OK: Output directive found"
  fi

  # Phase 2: Extract commands
  echo "Phase 2: Extracting commands..."
  local line_num=0
  local cmd_count=0
  while IFS= read -r line; do
    ((line_num++))
    if [[ "$line" =~ ^Type\ \" ]]; then
      cmd=$(echo "$line" | sed 's/^Type "//' | sed 's/"$//')
      ((cmd_count++))

      # Phase 3: Validate CLI flags
      if [[ "$cmd" =~ ^skrills ]]; then
        base_cmd=$(echo "$cmd" | awk '{print $1, $2}')
        flags=$(echo "$cmd" | grep -oE '\-\-[a-zA-Z0-9-]+' || true)

        if [ -n "$flags" ]; then
          help_output=$($base_cmd --help 2>&1 || echo "")
          for flag in $flags; do
            if ! echo "$help_output" | grep -q -- "$flag"; then
              echo "  ERROR: Invalid flag '$flag' at line $line_num"
              echo "         Command: $cmd"
              ((errors++))
            fi
          done
        fi
      fi
    fi
  done < "$tape_file"
  echo "  Extracted $cmd_count commands"

  # Phase 4: Demo data
  echo "Phase 4: Demo data verification..."
  skill_dir=$(grep '^Env SKRILLS_SKILL_DIR' "$tape_file" | sed 's/.*"\(.*\)"/\1/' || true)
  if [ -n "$skill_dir" ]; then
    if [ ! -d "$skill_dir" ]; then
      echo "  ERROR: Demo skill directory missing: $skill_dir"
      ((errors++))
    else
      skill_count=$(find "$skill_dir" -name "SKILL.md" 2>/dev/null | wc -l)
      if [ "$skill_count" -eq 0 ]; then
        echo "  ERROR: No skills in demo directory: $skill_dir"
        ((errors++))
      else
        echo "  OK: Found $skill_count demo skills"
      fi
    fi
  fi

  # Summary
  echo "=== Validation Complete ==="
  if [ "$errors" -eq 0 ]; then
    echo "PASSED: No errors found"
    return 0
  else
    echo "FAILED: $errors error(s) found"
    return 1
  fi
}

# Run if called directly
if [ -n "$1" ]; then
  validate_tape "$1"
fi
```

## Integration with Workflow

Before running VHS, execute validation:

```bash
# In the tutorial-updates skill Phase 1.5
validate_tape "$tape_file"
if [ $? -ne 0 ]; then
  echo "Validation failed. Fix errors before running VHS."
  exit 1
fi

# Proceed to VHS recording
vhs "$tape_file"
```

## Flags

### `--validate-only`

Run validation without generating GIF:

```bash
# Validate all tapes without recording
for tape in assets/tapes/*.tape; do
  validate_tape "$tape"
done
```

### `--skip-validation`

Bypass validation for rapid regeneration:

```bash
# Skip validation when commands are known-good
if [ "$SKIP_VALIDATION" != "true" ]; then
  validate_tape "$tape_file" || exit 1
fi
vhs "$tape_file"
```

## Exit Codes

| Code | Meaning |
|------|---------|
| 0 | Validation passed |
| 1 | Validation failed (errors found) |
| 2 | Validation skipped (--skip-validation) |

## Error Message Format

Standardized format for actionable errors:

```
ERROR: <short-description> at line <number>
  Command: <full-command>
  <hint-or-available-options>
```

Example:
```
ERROR: Invalid flag '--sample' at line 12
  Command: skrills validate --sample 5
  Run 'skrills validate --help' to see available flags
```


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