Skill: rikisann/video-proof-skill (version 1.0.1)

--- name: video-proof description: Record video proof of implemented features after coding tasks complete. Use when a coding agent finishes work and needs to visually verify and demonstrate that the feature works. Generates screen recordings, screenshots, and test logs as PR artifacts. Integrates with any coding agent workflow — run after code is written to produce video evidence of working software. Trigger on "video proof", "record demo", "prove it works", "show me it working", or when PRDs include a proof/demo step. license: Apache-2.0 compatibility: Requires Node.js 18+, Chromium (installed via Playwright). Optional ffmpeg for mp4 conversion. Works on Linux, macOS, and WSL. metadata: author: rikisann version: "1.0" --- # Video Proof Record video demos of implemented features using Playwright's built-in screen recording. After a coding task completes, this skill starts your app, runs a scripted walkthrough, and captures video + screenshots + console logs as proof artifacts. Works with any stack — Next.js, Vite, Django, Rails, Go, Docker, static files, or an already-running server. You provide the start command; the skill handles the rest. ## Prerequisites Run once per machine: ```bash bash scripts/setup.sh ``` Installs Playwright (Chromium), ffmpeg, and the `yaml` npm package. ## Quick Start ### Option A: YAML Proof Spec (recommended) Create a `proof-spec.yaml`: ```yaml proof: start_command: "npm run dev" # any shell command that starts your app start_port: 3000 # port to poll before recording begins base_url: "http://localhost:3000" steps: - goto: "/dashboard" - wait: 2000 - screenshot: "dashboard-loaded" - click: "text=Create New" - wait: 1000 - assert_visible: "text=New Item" - screenshot: "item-created" ``` Run it: ```bash node scripts/record-proof.js --spec proof-spec.yaml --output ./proof-artifacts ``` ### Option B: Inline CLI (simple cases) ```bash node scripts/record-proof.js \ --start "python3 -m http.server 8080" \ --port 8080 \ --url http://localhost:8080 \ --goto "/" \ --screenshot "home" \ --output ./proof-artifacts ``` ### Option C: Already-running server (no start_command) Omit `start_command` — the script skips server startup and goes straight to recording: ```yaml proof: base_url: "https://staging.myapp.com" steps: - goto: "/login" - screenshot: "login-page" ``` ## Artifacts Produced ``` proof-artifacts/ ├── video.webm # Full screen recording ├── video.mp4 # Chat-friendly version (auto-converted via ffmpeg) ├── screenshots/ # Named screenshots from steps │ ├── dashboard-loaded.png │ └── item-created.png ├── console.log # Browser console output (errors, warnings, logs) └── proof-summary.md # Markdown report with pass/fail status per step ``` Exit code: `0` if all steps pass, `1` if any step fails. On failure, a `FAILURE.png` full-page screenshot is captured automatically. ## Start Command Examples The `start_command` field accepts any shell command. Examples: | Stack | start_command | start_port | |-------|--------------|------------| | Next.js | `npm run dev` | 3000 | | Vite / React | `npm run dev` | 5173 | | Django | `python manage.py runserver 0.0.0.0:8000` | 8000 | | Flask | `flask run --port 5000` | 5000 | | Rails | `bin/rails server -p 3000` | 3000 | | Go | `go run . -addr :8080` | 8080 | | Rust (Actix/Axum) | `cargo run` | 8080 | | Docker Compose | `docker compose up` | 3000 | | Static files | `python3 -m http.server 8080` | 8080 | | Already running | *(omit field)* | — | ## Step Actions | Action | Value | Description | |--------|-------|-------------| | `goto` | `"/path"` or full URL | Navigate to a page | | `click` | Playwright selector | Click an element (`text=Submit`, `#btn`, `.class`) | | `fill` | `{selector, value}` | Clear and fill an input field | | `type` | `{selector, text}` | Type into an element keystroke by keystroke | | `wait` | milliseconds | Pause (let animations/data load) | | `screenshot` | `"name"` | Save `screenshots/<name>.png` | | `scroll` | `"down"` or `"up"` | Scroll 500px in direction | | `assert_visible` | Playwright selector | Fail the proof if element isn't visible | | `assert_url` | string | Fail if current URL doesn't contain string | ## API-Only Proof (No Browser) For backend/API changes with no UI, use `api-proof.js`: ```bash node scripts/api-proof.js --spec api-spec.yaml --output ./proof-artifacts ``` ```yaml proof: start_command: "npm start" start_port: 3000 base_url: "http://localhost:3000" requests: - method: GET path: /api/health assert_status: 200 assert_body_contains: "ok" - method: POST path: /api/users headers: Content-Type: application/json body: '{"name": "test"}' assert_status: 201 ``` Produces `api-proof.md` and `api-results.json` instead of video. ## Integration with Coding Agents Append to any coding task prompt: ``` After completing the implementation, create a proof-spec.yaml that: 1. Starts the app with the appropriate command for this project 2. Navigates to the relevant pages 3. Demonstrates the feature with clicks/fills as needed 4. Asserts the expected outcome is visible 5. Takes before/after screenshots Then run: node <skill-path>/scripts/record-proof.js --spec proof-spec.yaml --output ./proof-artifacts Commit proof-artifacts/ with your changes. ``` The agent writes the proof spec based on what it built, runs it, and the video becomes part of the deliverable. ## Spec Reference See `references/proof-spec.md` for the full YAML schema, API proof schema, and a copy-paste PRD template. ## Tips - Use `assert_visible` to make proofs fail when the feature doesn't work — video of a broken page isn't useful proof - Keep specs focused on the specific feature, not the whole app - `wait` steps between actions let data load and animations settle — 1-2s is usually enough - The video captures real load times — doubles as a basic performance check - If ffmpeg isn't available, the script still produces `.webm` (just skips mp4 conversion)

path: references/proof-spec.md
content: # Proof Spec Reference

## UI Proof Schema (record-proof.js)

```yaml
proof:
  # --- Server startup (optional) ---
  # Omit start_command entirely if the app is already running.
  # Accepts any shell command: npm, python, docker, go, cargo, etc.
  start_command: "npm run dev"
  start_port: 3000                    # Port to poll before recording (default: 3000)
  start_timeout: 30000                # Max ms to wait for server (default: 30000)

  # --- Browser config ---
  base_url: "http://localhost:3000"   # Base for relative goto paths
  viewport:
    width: 1280                       # Default: 1280
    height: 720                       # Default: 720

  # --- Steps (executed sequentially) ---
  steps:
    - goto: "/path"                   # Navigate (relative or absolute URL)
    - click: "text=Button"            # Playwright selector
    - fill:                           # Clear + fill input
        selector: "#email"
        value: "[REDACTED_SECRET]"
    - type:                           # Keystroke-by-keystroke input
        selector: "#search"
        text: "query"
    - wait: 2000                      # Pause in milliseconds
    - screenshot: "name"              # Save screenshots/<name>.png
    - scroll: "down"                  # "up" or "down" (500px)
    - assert_visible: "text=Success"  # Fail proof if not visible within 10s
    - assert_url: "/dashboard"        # Fail if URL doesn't contain string

  timeout: 60000                      # Overall proof timeout (default: 60000)
```

## API Proof Schema (api-proof.js)

```yaml
proof:
  start_command: "npm start"          # Optional — same as UI proof
  start_port: 3000
  base_url: "http://localhost:3000"
  timeout: 30000

  requests:
    - method: GET
      path: /api/health
      assert_status: 200
      assert_body_contains: "ok"

    - method: POST
      path: /api/users
      headers:
        Content-Type: application/json
      body: '{"name": "test"}'
      assert_status: 201

    - method: DELETE
      path: /api/users/1
      assert_status: 204
```

## PRD Integration Template

Copy into any task PRD:

```markdown
## Proof of Work

After implementation, create `proof-spec.yaml` in the task directory:

\`\`\`yaml
proof:
  start_command: "<command to start the app>"
  start_port: <port>
  base_url: "http://localhost:<port>"
  steps:
    - goto: "/<feature-page>"
    - wait: 2000
    - screenshot: "before"
    - click: "text=<feature-action>"
    - wait: 1000
    - assert_visible: "text=<expected-result>"
    - screenshot: "after"
\`\`\`

Then run:
\`\`\`bash
node <skill-dir>/scripts/record-proof.js --spec proof-spec.yaml --output ./proof-artifacts
\`\`\`

Commit the `proof-artifacts/` directory with your changes.
```


path: scripts/api-proof.js
content: #!/usr/bin/env node
/**
 * api-proof.js — Record proof for API/backend changes (no browser needed)
 *
 * Usage:
 *   node api-proof.js --spec api-spec.yaml --output ./proof-artifacts
 *   node api-proof.js --start "npm start" --port 3000 --request "GET /api/health" --output ./artifacts
 */

const fs = require('fs');
const path = require('path');
const { execSync, spawn } = require('child_process');
const yaml = require('yaml');
const http = require('http');
const https = require('https');

const args = process.argv.slice(2);

function getArg(name) {
  const idx = args.indexOf(`--${name}`);
  return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null;
}

function sleep(ms) {
  return new Promise(r => setTimeout(r, ms));
}

function httpRequest(method, url, body, headers) {
  return new Promise((resolve, reject) => {
    const parsed = new URL(url);
    const mod = parsed.protocol === 'https:' ? https : http;
    const opts = {
      hostname: parsed.hostname,
      port: parsed.port,
      path: parsed.pathname + parsed.search,
      method,
      headers: headers || {},
    };
    const req = mod.request(opts, res => {
      let data = '';
      res.on('data', c => (data += c));
      res.on('end', () => resolve({ statusCode: res.statusCode, body: data }));
    });
    req.on('error', reject);
    req.setTimeout(10000, () => { req.destroy(new Error('Request timeout')); });
    if (body) req.write(typeof body === 'string' ? body : JSON.stringify(body));
    req.end();
  });
}

async function waitForPort(port, timeoutMs) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    try {
      execSync(`curl -sf -o /dev/null http://localhost:${port}`, { timeout: 5000 });
      return true;
    } catch {
      await sleep(1000);
    }
  }
  return false;
}

async function main() {
  const specPath = getArg('spec');
  const outputDir = getArg('output') || './proof-artifacts';
  fs.mkdirSync(outputDir, { recursive: true });
  let spec;

  if (specPath) {
    spec = yaml.parse(fs.readFileSync(specPath, 'utf8'));
    if (spec.proof) spec = spec.proof;
  } else {
    spec = {
      start_command: getArg('start'),
      start_port: parseInt(getArg('port') || '3000'),
      base_url: getArg('url') || `http://localhost:${getArg('port') || 3000}`,
      requests: [],
      timeout: parseInt(getArg('timeout') || '30000'),
    };
    for (let i = 0; i < args.length; i++) {
      if (args[i] === '--request' && args[i + 1]) {
        const parts = args[++i].split(' ');
        spec.requests.push({ method: parts[0], path: parts.slice(1).join(' ') });
      }
    }
  }

  // --- Start server ---
  let serverProc = null;
  if (spec.start_command) {
    const port = spec.start_port || 3000;
    const timeout = spec.start_timeout || spec.timeout || 30000;

    console.log(`[api-proof] Starting: ${spec.start_command}`);
    serverProc = spawn('sh', ['-c', spec.start_command], {
      stdio: 'pipe',
      detached: true,
      env: { ...process.env, PORT: String(port) },
    });
    serverProc.stdout?.on('data', d => process.stdout.write(`[server] ${d}`));
    serverProc.stderr?.on('data', d => process.stderr.write(`[server] ${d}`));

    console.log(`[api-proof] Waiting for port ${port}...`);
    const ready = await waitForPort(port, timeout);
    if (!ready) {
      console.error(`[api-proof] Server not ready within ${Math.round(timeout / 1000)}s`);
      if (serverProc) try { process.kill(-serverProc.pid, 'SIGTERM'); } catch {}
      process.exit(1);
    }
    console.log(`[api-proof] Server ready`);
  }

  const baseUrl = spec.base_url || `http://localhost:${spec.start_port || 3000}`;
  const results = [];

  // --- Execute requests ---
  for (let i = 0; i < (spec.requests || []).length; i++) {
    const req = spec.requests[i];
    const method = (req.method || 'GET').toUpperCase();
    const url = `${baseUrl}${req.path}`;
    const n = i + 1;

    console.log(`[api-proof] Request ${n}: ${method} ${req.path}`);
    try {
      const res = await httpRequest(method, url, req.body, req.headers);
      const result = {
        request: n,
        method,
        path: req.path,
        status: res.statusCode,
        body: res.body.slice(0, 2000),
        ok: res.statusCode < 400,
      };

      if (req.assert_status && res.statusCode !== req.assert_status) {
        result.ok = false;
        result.error = `Expected status ${req.assert_status}, got ${res.statusCode}`;
      }
      if (req.assert_body_contains && !res.body.includes(req.assert_body_contains)) {
        result.ok = false;
        result.error = `Response body missing: "${req.assert_body_contains}"`;
      }

      results.push(result);
      console.log(`[api-proof] ${result.ok ? '✅' : '❌'} ${method} ${req.path} → ${res.statusCode}`);
    } catch (err) {
      results.push({ request: n, method, path: req.path, ok: false, error: err.message });
      console.log(`[api-proof] ❌ ${method} ${req.path} → ${err.message}`);
    }
  }

  // --- Generate summary ---
  const failed = results.some(r => !r.ok);

  let summary = `# API Proof\n\n`;
  summary += `**Status:** ${failed ? '❌ FAILED' : '✅ PASSED'}\n`;
  summary += `**Date:** ${new Date().toISOString()}\n`;
  summary += `**Requests:** ${results.length}\n\n`;
  summary += `## Results\n\n`;
  for (const r of results) {
    summary += `### ${r.ok ? '✅' : '❌'} ${r.method} ${r.path}\n\n`;
    summary += `- Status: ${r.status || 'N/A'}\n`;
    if (r.error) summary += `- Error: ${r.error}\n`;
    if (r.body) summary += `\n\`\`\`json\n${r.body}\n\`\`\`\n`;
    summary += '\n';
  }

  fs.writeFileSync(path.join(outputDir, 'api-proof.md'), summary);
  fs.writeFileSync(path.join(outputDir, 'api-results.json'), JSON.stringify(results, null, 2));

  console.log(`[api-proof] Summary: ${path.join(outputDir, 'api-proof.md')}`);
  console.log(`[api-proof] ${failed ? 'FAILED' : 'PASSED'} — ${results.length} requests`);

  if (serverProc) try { process.kill(-serverProc.pid, 'SIGTERM'); } catch {}
  process.exit(failed ? 1 : 0);
}

main().catch(err => {
  console.error('[api-proof] Fatal:', err.message);
  process.exit(1);
});


path: scripts/package.json
content: {
  "name": "scripts",
  "version": "1.0.0",
  "description": "",
  "main": "api-proof.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "commonjs",
  "dependencies": {
    "playwright": "^1.58.2",
    "yaml": "^2.8.2"
  }
}


path: scripts/record-proof.js
content: #!/usr/bin/env node
/**
 * record-proof.js — Record video proof of a working feature
 *
 * Usage:
 *   node record-proof.js --spec proof-spec.yaml --output ./proof-artifacts
 *   node record-proof.js --start "npm run dev" --port 3000 --url http://localhost:3000 --goto "/" --screenshot "home" --output ./artifacts
 */

const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
const { execSync, spawn } = require('child_process');
const yaml = require('yaml');

const args = process.argv.slice(2);

function getArg(name) {
  const idx = args.indexOf(`--${name}`);
  return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null;
}

function getAllArgs(name) {
  const results = [];
  for (let i = 0; i < args.length; i++) {
    if (args[i] === `--${name}` && i + 1 < args.length) results.push(args[++i]);
  }
  return results;
}

function sleep(ms) {
  return new Promise(r => setTimeout(r, ms));
}

function cleanup(serverProc) {
  if (serverProc) {
    try { process.kill(-serverProc.pid, 'SIGTERM'); } catch {}
  }
}

async function waitForPort(port, timeoutMs) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    try {
      execSync(`curl -sf -o /dev/null http://localhost:${port}`, { timeout: 5000 });
      return true;
    } catch {
      await sleep(1000);
    }
  }
  return false;
}

async function main() {
  const specPath = getArg('spec');
  const outputDir = getArg('output') || './proof-artifacts';
  let spec;

  if (specPath) {
    const raw = fs.readFileSync(specPath, 'utf8');
    spec = yaml.parse(raw);
    if (spec.proof) spec = spec.proof;
  } else {
    spec = {
      start_command: getArg('start') || null,
      start_port: parseInt(getArg('port') || '3000'),
      base_url: getArg('url') || `http://localhost:${getArg('port') || '3000'}`,
      steps: [],
      timeout: parseInt(getArg('timeout') || '60000'),
    };
    for (const g of getAllArgs('goto')) spec.steps.push({ goto: g });
    for (const s of getAllArgs('screenshot')) spec.steps.push({ screenshot: s });
    for (const c of getAllArgs('click')) spec.steps.push({ click: c });
  }

  const screenshotDir = path.join(outputDir, 'screenshots');
  fs.mkdirSync(screenshotDir, { recursive: true });

  // --- Start server (if configured) ---
  let serverProc = null;
  if (spec.start_command) {
    const port = spec.start_port || 3000;
    const startTimeout = spec.start_timeout || spec.timeout || 60000;

    console.log(`[proof] Starting: ${spec.start_command}`);
    serverProc = spawn('sh', ['-c', spec.start_command], {
      stdio: 'pipe',
      detached: true,
      env: { ...process.env, BROWSER: 'none', PORT: String(port) },
    });
    serverProc.stdout?.on('data', d => process.stdout.write(`[server] ${d}`));
    serverProc.stderr?.on('data', d => process.stderr.write(`[server] ${d}`));
    serverProc.on('error', err => console.error(`[server] spawn error: ${err.message}`));

    console.log(`[proof] Waiting for port ${port} (timeout: ${Math.round(startTimeout / 1000)}s)...`);
    const ready = await waitForPort(port, startTimeout);
    if (!ready) {
      console.error(`[proof] Server did not become ready on port ${port} within ${Math.round(startTimeout / 1000)}s`);
      cleanup(serverProc);
      process.exit(1);
    }
    console.log(`[proof] Server ready on port ${port}`);
  }

  // --- Launch browser ---
  const vp = {
    width: spec.viewport?.width || 1280,
    height: spec.viewport?.height || 720,
  };

  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext({
    viewport: vp,
    recordVideo: { dir: outputDir, size: vp },
  });
  const page = await context.newPage();

  const consoleLogs = [];
  page.on('console', msg => consoleLogs.push(`[${msg.type()}] ${msg.text()}`));
  page.on('pageerror', err => consoleLogs.push(`[error] ${err.message}`));

  const baseUrl = spec.base_url || `http://localhost:${spec.start_port || 3000}`;
  const steps = spec.steps || [];
  const results = [];

  console.log(`[proof] Running ${steps.length} steps (viewport: ${vp.width}x${vp.height})...`);

  // --- Execute steps ---
  try {
    for (let i = 0; i < steps.length; i++) {
      const step = steps[i];
      const n = i + 1;

      if (step.goto) {
        const url = step.goto.startsWith('http') ? step.goto : `${baseUrl}${step.goto}`;
        console.log(`[proof] Step ${n}: goto ${url}`);
        await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
        results.push({ step: n, action: 'goto', target: step.goto, status: 'ok' });

      } else if (step.click) {
        console.log(`[proof] Step ${n}: click "${step.click}"`);
        await page.click(step.click, { timeout: 10000 });
        results.push({ step: n, action: 'click', target: step.click, status: 'ok' });

      } else if (step.fill) {
        const { selector, value } = step.fill;
        console.log(`[proof] Step ${n}: fill ${selector}`);
        await page.fill(selector, value, { timeout: 10000 });
        results.push({ step: n, action: 'fill', target: selector, status: 'ok' });

      } else if (step.type) {
        const { selector, text } = step.type;
        console.log(`[proof] Step ${n}: type into ${selector}`);
        await page.type(selector, text, { timeout: 10000 });
        results.push({ step: n, action: 'type', target: selector, status: 'ok' });

      } else if (step.wait) {
        console.log(`[proof] Step ${n}: wait ${step.wait}ms`);
        await sleep(step.wait);
        results.push({ step: n, action: 'wait', target: `${step.wait}ms`, status: 'ok' });

      } else if (step.screenshot) {
        const name = step.screenshot;
        const filepath = path.join(screenshotDir, `${name}.png`);
        console.log(`[proof] Step ${n}: screenshot → ${name}.png`);
        await page.screenshot({ path: filepath, fullPage: false });
        results.push({ step: n, action: 'screenshot', target: name, status: 'ok', path: filepath });

      } else if (step.scroll) {
        const px = step.scroll === 'up' ? -500 : 500;
        console.log(`[proof] Step ${n}: scroll ${step.scroll}`);
        await page.mouse.wheel(0, px);
        await sleep(500);
        results.push({ step: n, action: 'scroll', target: step.scroll, status: 'ok' });

      } else if (step.assert_visible) {
        console.log(`[proof] Step ${n}: assert_visible "${step.assert_visible}"`);
        await page.waitForSelector(step.assert_visible, { state: 'visible', timeout: 10000 });
        results.push({ step: n, action: 'assert_visible', target: step.assert_visible, status: 'ok' });

      } else if (step.assert_url) {
        const current = page.url();
        console.log(`[proof] Step ${n}: assert_url "${step.assert_url}" (current: ${current})`);
        if (!current.includes(step.assert_url)) {
          throw new Error(`URL assertion failed: expected "${step.assert_url}" in "${current}"`);
        }
        results.push({ step: n, action: 'assert_url', target: step.assert_url, status: 'ok' });

      } else {
        console.warn(`[proof] Step ${n}: unknown action`, JSON.stringify(step));
        results.push({ step: n, action: 'unknown', target: JSON.stringify(step), status: 'skipped' });
      }
    }
  } catch (err) {
    console.error(`[proof] Step failed: ${err.message}`);
    try {
      await page.screenshot({ path: path.join(screenshotDir, 'FAILURE.png'), fullPage: true });
    } catch {}
    results.push({ action: 'error', message: err.message, status: 'failed' });
  }

  // --- Finalize ---
  await context.close();
  await browser.close();

  fs.writeFileSync(path.join(outputDir, 'console.log'), consoleLogs.join('\n'));

  // Rename Playwright's hashed video file
  const videoFiles = fs.readdirSync(outputDir).filter(f => f.endsWith('.webm'));
  let hasVideo = false;
  if (videoFiles.length > 0) {
    const src = path.join(outputDir, videoFiles[0]);
    const dest = path.join(outputDir, 'video.webm');
    if (src !== dest) fs.renameSync(src, dest);
    hasVideo = true;
    console.log(`[proof] Video saved: ${dest}`);

    try {
      const mp4 = path.join(outputDir, 'video.mp4');
      execSync(`ffmpeg -y -i "${dest}" -c:v libx264 -crf 23 -preset fast -an "${mp4}" 2>/dev/null`);
      console.log(`[proof] MP4 converted: ${mp4}`);
    } catch {
      console.warn('[proof] ffmpeg not available or conversion failed — .webm still available');
    }
  }

  // --- Generate summary ---
  const failed = results.some(r => r.status === 'failed');
  const screenshots = fs.readdirSync(screenshotDir).filter(f => f.endsWith('.png'));

  let summary = `# Proof of Work\n\n`;
  summary += `**Status:** ${failed ? '❌ FAILED' : '✅ PASSED'}\n`;
  summary += `**Date:** ${new Date().toISOString()}\n`;
  summary += `**Steps:** ${results.length}\n\n`;
  summary += `## Steps\n\n`;
  for (const r of results) {
    const icon = r.status === 'ok' ? '✅' : r.status === 'failed' ? '❌' : '⏭️';
    summary += `${icon} **${r.action}** → ${r.target || r.message || ''}\n`;
  }
  summary += `\n## Artifacts\n\n`;
  if (hasVideo) summary += `- Video: \`video.webm\` / \`video.mp4\`\n`;
  for (const s of screenshots) summary += `- Screenshot: \`screenshots/${s}\`\n`;
  summary += `- Console log: \`console.log\`\n`;

  fs.writeFileSync(path.join(outputDir, 'proof-summary.md'), summary);
  console.log(`[proof] Summary: ${path.join(outputDir, 'proof-summary.md')}`);
  console.log(`[proof] ${failed ? 'FAILED' : 'PASSED'} — ${results.length} steps executed`);

  cleanup(serverProc);
  process.exit(failed ? 1 : 0);
}

main().catch(err => {
  console.error('[proof] Fatal:', err.message);
  process.exit(1);
});


path: scripts/setup.sh
content: #!/bin/bash
# video-proof skill — one-time dependency setup
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

echo "==> Installing Node dependencies..."
cd "$SCRIPT_DIR"
if [ ! -f package.json ]; then
  npm init -y --silent > /dev/null 2>&1
fi
npm install --save playwright@latest yaml@latest 2>&1 | tail -3

echo "==> Installing Playwright Chromium..."
npx playwright install chromium 2>&1 | tail -3

echo "==> Installing Playwright system dependencies..."
npx playwright install-deps chromium 2>&1 | tail -5

echo "==> Checking for ffmpeg..."
if command -v ffmpeg &> /dev/null; then
  echo "    ffmpeg found: $(ffmpeg -version 2>&1 | head -1)"
else
  echo "    ffmpeg not found — attempting install..."
  if command -v apt-get &> /dev/null; then
    sudo apt-get update -qq && sudo apt-get install -y -qq ffmpeg
  elif command -v brew &> /dev/null; then
    brew install ffmpeg
  elif command -v dnf &> /dev/null; then
    sudo dnf install -y ffmpeg
  elif command -v pacman &> /dev/null; then
    sudo pacman -S --noconfirm ffmpeg
  else
    echo "    ⚠ Could not install ffmpeg automatically. Install manually for mp4 conversion."
    echo "    Video recording still works (produces .webm without ffmpeg)."
  fi
fi

echo ""
echo "==> Setup complete! Run a test:"
echo "    node $SCRIPT_DIR/record-proof.js --url http://example.com --goto / --screenshot test --output /tmp/proof-test"


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