Sign inGet started
–

create-verification-skill

by paperclipai/paperclip · Coding & Development

Generate a project-local verification skill to drive an app and prove its behavior

Lift · GLM-5.3-flash−33ppdirectional · all models →
88
Trustsecurity · quality · license · upkeep
Scan Saferisk 0/100
LicenseMITserved via MCP
Claude Haiku 5.5 · GPT-6 Luna — not run yet
Pass-rate Lift−33pp95% CI [−100, 0] · CI includes zero
screen tier · directionalchat harness (v2)
−100pp+100pp

Pass rate with the skill minus without, same tasks, same model.

baseline
100%
with skill
67%
forced
25%
Content Lift forced arm
−75pp [−100, −25]
Trigger rate
100%
Trials
11 · 3 tasks
Model
GLM-5.3-flash
Measured
2026-10-15

Content Lift = the skill's instructions injected into the system prompt, so it measures what the content does once loaded, apart from whether the agent chooses to load it. Method →

Embed this score · live badge for your README
SkillzBench badge preview

The badge shows the real benchmark number and updates itself when we re-bench — including the honest states (“did not trigger”, “not benched yet”). All badge options →

Markdown[![SkillzBench](https://skillzbench.com/api/badge/create-verification-skill.svg)](https://skillzbench.com/skills/create-verification-skill)
+ cost/task[![SkillzBench](https://skillzbench.com/api/badge/create-verification-skill.svg?cost=1)](https://skillzbench.com/skills/create-verification-skill)
HTML<a href="https://skillzbench.com/skills/create-verification-skill"><img src="https://skillzbench.com/api/badge/create-verification-skill.svg" alt="SkillzBench benchmark" height="20"></a>

A/B outputs — median trial per arm

Chat harness (v2): the agent's final message without the skill (left) vs with it (right).

create-verification-skill--m2
baseline · 0.85
// verify.js — run with: node verify.js
// Assumes your server file is server.js and exports the app or starts listening.
// If server.js calls app.listen() at top level, set SERVER_MODULE accordingly.

const { spawn } = require('child_process');
const http = require('http');

const SERVER_FILE = process.argv[2] || 'server.js';
const PORT = 3000;
const BASE = `http://localhost:${PORT}`;

function request(method, path, body) {
  return new Promise((resolve, reject) => {
    const data = body ? JSON.stringify(body) : null;
    const req = http.request(
      `${BASE}${path}`,
      {
        method,
        headers: {
          'Content-Type': 'application/json',
          ...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}),
        },
      },
      (res) => {
        let raw = '';
        res.on('data', (c) => (raw += c));
        res.on('end', () => {
          let json = null;
          try { json = JSON.parse(raw); } catch (_) {}
          resolve({ status: res.statusCode, json, raw });
        });
      }
    );
    req.on('error', reject);
    if (data) req.write(data);
    req.end();
  });
}

function waitForServer(timeoutMs = 5000) {
  const start = Date.now();
  return new Promise((resolve, reject) => {
    const tryOnce = () => {
      request('GET', '/health')
        .then(resolve)
        .catch(() => {
          if (Date.now() - start > timeoutMs) {
            reject(new Error(`Server did not become ready within ${timeoutMs}ms`));
          } else {
            setTimeout(tryOnce, 200);
          }
        });
    };
    tryOnce();
  });
}

const results = [];
function check(name, cond, detail = '') {
  results.push({ name, pass: !!cond, detail });
  console.log(`  ${cond ? '✅ PASS' : '❌ FAIL'} — ${name}${detail ? ` (${detail})` : ''}`);
}

async function main() {
  const server = spawn(process.execPath, [SERVER_FILE], {
    stdio: ['ignore', 'pipe', 'pipe'],
    env: { ...process.env, PORT: String(PORT) },
  });
  server.stdout.on('data', () => {});
  server.stderr.on('data', (d) => process.stderr.write(`[server] ${d}`));

  try {
    await waitForServer();
    console.log('\nRunning verification tests:\n');

    // Test 1: GET /health
    const health = await request('GET', '/health');
    check('GET /health returns 200', health.status === 200, `got ${health.status}`);
    check('GET /health body is {status:"ok"}', health.json && health.json.status === 'ok');

    // Test 2: POST /users with valid payload
    const good = await request('POST', '/users', { name: 'Alice', email: 'alice@example.com' });
    check('POST /users (valid) returns 201', good.status === 201, `got ${good.status}`);
    check(
      'POST /users (valid) returns created user',
      good.json && good.json.name === 'Alice' && good.json.email === 'alice@example.com'
    );

    // Test 3: POST /users with bad email
    const bad = await request('POST', '/users', { name: 'Bob', email: 'not-an-email' });
    check('POST /users (bad email) does NOT return 201', bad.status !== 201, `got ${bad.status}`);
    check('POST /users (bad email) returns 4xx', bad.status >= 400 && bad.status < 500, `got ${bad.status}`);

    // Test 4: user persisted in memory
    const good2 = await request('POST', '/users', { name: 'Carol', email: 'carol@example.com' });
    check('Second POST /users also returns 201', good2.status === 201, `got ${good2.status}`);
  } catch (err) {
    console.error('❌ Test run error:', err.message);
    results.push({ name: 'test run', pass: false, detail: err.message });
  } finally {
    server.kill('SIGTERM');
  }

  const passed = results.filter((r) => r.pass).length;
  const total = results.length;
  console.log('\n========== SUMMARY ==========');
  console.log(`Passed: ${passed}/${total}`);
  console.log(total === passed ? '🎉 ALL TESTS PASSED' : '⚠️  SOME TESTS FAILED');
  process.exitCode = total === passed ? 0 : 1;
}

main();

Usage: node verify.js server.js (or just node verify.js if your file is server.js).

Notes:

  • The script spawns your server as a child process, polls /health until it's ready (5s timeout), then runs the tests.
  • It validates: health status/body, 201 + returned user for a valid POST, a 4xx (non-201) response for a bad email, and that a second valid POST still works.
  • It prints a per-test pass/fail list and a final summary, and exits with code 0/1 so it works in CI.
  • If your server reads a port from process.env.PORT, it's set automatically; otherwise it assumes 3000.
with skill · 0.85

Here's a self-contained verification script for your Express API:

// verify.js — starts the server, tests both routes, prints pass/fail summary
const { spawn } = require('child_process');
const http = require('http');

const PORT = 3000;
const BASE = `http://localhost:${PORT}`;
let server;
let failures = 0;
const results = [];

function request(method, path, body) {
  return new Promise((resolve, reject) => {
    const data = body ? JSON.stringify(body) : null;
    const req = http.request(
      `${BASE}${path}`,
      {
        method,
        headers: data
          ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }
          : {},
      },
      (res) => {
        let raw = '';
        res.on('data', (c) => (raw += c));
        res.on('end', () => {
          try {
            resolve({ status: res.statusCode, body: JSON.parse(raw) });
          } catch {
            resolve({ status: res.statusCode, body: raw });
          }
        });
      }
    );
    req.on('error', reject);
    if (data) req.write(data);
    req.end();
  });
}

function check(name, cond, detail = '') {
  const ok = !!cond;
  if (!ok) failures++;
  results.push({ name, ok, detail });
  console.log(`  ${ok ? '✅ PASS' : '❌ FAIL'}  ${name}${ok ? '' : ` — ${detail}`}`);
}

async function waitForServer(timeoutMs = 5000) {
  const start = Date.now();
  while (Date.now() - start < timeoutMs) {
    try {
      await request('GET', '/health');
      return;
    } catch {
      await new Promise((r) => setTimeout(r, 100));
    }
  }
  throw new Error(`Server did not become ready within ${timeoutMs}ms`);
}

async function run() {
  // 1. Launch the server (adjust entry file if needed)
  server = spawn(process.execPath, ['server.js'], { stdio: 'inherit' });
  server.on('error', (e) => {
    console.error('Failed to start server:', e.message);
    process.exit(1);
  });

  try {
    await waitForServer();

    // 2. GET /health
    const health = await request('GET', '/health');
    check('GET /health returns 200', health.status === 200, `got ${health.status}`);
    check('GET /health body is {status:"ok"}', health.body && health.body.status === 'ok', JSON.stringify(health.body));

    // 3. POST /users — valid user
    const created = await request('POST', '/users', { name: 'Alice', email: 'alice@example.com' });
    check('POST /users valid returns 201', created.status === 201, `got ${created.status}`);
    check('POST /users returns created user with name', created.body && created.body.name === 'Alice', JSON.stringify(created.body));
    check('POST /users returns email', created.body && created.body.email === 'alice@example.com', JSON.stringify(created.body));

    // 4. POST /users — bad email
    const bad = await request('POST', '/users', { name: 'Bob', email: 'not-an-email' });
    check('POST /users bad email does NOT return 201', bad.status !== 201, `got ${bad.status}`);
    check('POST /users bad email is a client error (4xx)', bad.status >= 400 && bad.status < 500, `got ${bad.status}`);

    // 5. State check: user persisted in memory
    const healthAfter = await request('GET', '/health');
    check('Server still healthy after requests', healthAfter.status === 200, `got ${healthAfter.status}`);
  } finally {
    // 6. Teardown — kill only what we started
    if (server && !server.killed) server.kill('SIGTERM');
  }

  // 7. Summary
  const total = results.length;
  console.log('\n──────── VERIFICATION SUMMARY ────────');
  console.log(`Total: ${total}  Passed: ${total - failures}  Failed: ${failures}`);
  console.log(failures === 0 ? '🎉 ALL CHECKS PASSED' : '💥 SOME CHECKS FAILED');
  process.exit(failures === 0 ? 0 : 1);
}

run().catch((e) => {
  console.error('Verification run crashed:', e);
  if (server && !server.killed) server.kill('SIGTERM');
  process.exit(1);
});

Run it with:

node verify.js

Notes:

  • It spawns server.js as a child process (change the filename in the spawn call if your entry point differs) and polls /health until it's ready, with a 5s timeout.
  • It kills only the process it started, and always tears down in a finally block — even on failure.
  • The bad-email check asserts a 4xx status (your app likely returns 400); adjust if your API uses a different code.
  • Exit code is 0 on all-pass, 1 otherwise, so it works in CI.

If you'd like, I can also generate this as a project-local verification skill (.agents/skills/verify-<app>/) with a feature map so future runs stay maintainable — just say the word.

create-verification-skill--m3
baseline · 0.82
#!/usr/bin/env bash
# verify_todo.sh — exercise the `todo` CLI against a temporary HOME.
# Usage: ./verify_todo.sh [path-to-todo]

set -u

TODO_CMD="${1:-todo}"
PASS=0
FAIL=0

fail() {
    echo "FAIL: $*" >&2
    FAIL=$((FAIL + 1))
}

pass() {
    echo "PASS: $*"
    PASS=$((PASS + 1))
}

check() {
    # check <description> <expected> <actual>
    if [ "$2" = "$3" ]; then
        pass "$1"
    else
        fail "$1 (expected: [$2], got: [$3])"
    fi
}

# --- Set up isolated HOME ---------------------------------------------------
TMP_HOME="$(mktemp -d)"
trap 'rm -rf "$TMP_HOME"' EXIT
export HOME="$TMP_HOME"

# --- 1. add -----------------------------------------------------------------
out="$("$TODO_CMD" add "buy milk" 2>&1)"
rc=$?
check "add exits 0" "0" "$rc"

out="$("$TODO_CMD" add "walk dog" 2>&1)"
rc=$?
check "second add exits 0" "0" "$rc"

# --- 2. list ----------------------------------------------------------------
out="$("$TODO_CMD" list 2>&1)"
rc=$?
check "list exits 0" "0" "$rc"
check "list shows task 1" "1" "$(printf '%s\n' "$out" | grep -c 'buy milk')"
check "list shows task 2" "1" "$(printf '%s\n' "$out" | grep -c 'walk dog')"

# --- 3. done ----------------------------------------------------------------
out="$("$TODO_CMD" done 1 2>&1)"
rc=$?
check "done exits 0" "0" "$rc"
check "done prints confirmation" "Task 1 done" "$out"

# done task should no longer appear as pending (adjust if your CLI shows done items)
out="$("$TODO_CMD" list 2>&1)"
if printf '%s\n' "$out" | grep -q 'walk dog'; then
    pass "list still shows pending task 2"
else
    fail "list no longer shows task 2"
fi

# --- 4. clear ---------------------------------------------------------------
out="$("$TODO_CMD" clear 2>&1)"
rc=$?
check "clear exits 0" "0" "$rc"

out="$("$TODO_CMD" list 2>&1)"
check "list empty after clear" "" "$out"

# --- 5. storage file lives in temp HOME -------------------------------------
if [ -f "$TMP_HOME/.todo.json" ]; then
    pass "storage file created in temp HOME"
else
    fail "storage file missing in temp HOME"
fi

# --- Summary ----------------------------------------------------------------
echo
echo "Passed: $PASS, Failed: $FAIL"
[ "$FAIL" -eq 0 ]

Notes:

  • The script exports HOME to a mktemp -d directory, so ~/.todo.json is fully isolated and removed on exit via trap ... EXIT.
  • Run it as ./verify_todo.sh (assumes todo is on PATH) or ./verify_todo.sh /path/to/todo.
  • The done check expects the exact string Task 1 done; adjust the list-after-done assertion if your CLI displays completed tasks rather than hiding them.
  • Exit status is nonzero if any check fails, so it's CI-friendly.
with skill · 0.00
(no text deliverable)
create-verification-skill--m4
baseline · 1.00
// verify-booking.js
// End-to-end verification: starts backend + frontend, submits a booking via the UI,
// confirms it appears in the list, then shuts everything down.
//
// Usage: node verify-booking.js
// Assumes: playwright is installed, backend has GET /api/health, frontend form
// has inputs named date/time/partySize and a submit button.

const { spawn } = require("child_process");
const { chromium } = require("playwright");

const BACKEND_PORT = 4000;
const FRONTEND_PORT = 5173;
const BACKEND_URL = `http://localhost:${BACKEND_PORT}`;
const FRONTEND_URL = `http://localhost:${FRONTEND_PORT}`;

const processes = [];

function startService(name, cmd, args, opts) {
  const proc = spawn(cmd, args, {
    stdio: ["ignore", "pipe", "pipe"],
    shell: process.platform === "win32",
    ...opts,
  });
  proc.stdout.on("data", (d) => console.log(`[${name}] ${d.toString().trim()}`));
  proc.stderr.on("data", (d) => console.error(`[${name}] ${d.toString().trim()}`));
  proc.on("exit", (code) => console.log(`[${name}] exited with code ${code}`));
  processes.push(proc);
  return proc;
}

async function waitFor(url, { timeoutMs = 30000, intervalMs = 500, label }) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    try {
      const res = await fetch(url);
      if (res.ok) {
        console.log(`✓ ${label} is up (${url})`);
        return;
      }
    } catch {
      // not ready yet
    }
    await new Promise((r) => setTimeout(r, intervalMs));
  }
  throw new Error(`Timed out waiting for ${label} at ${url}`);
}

async function main() {
  // 1. Start services (adjust commands to match your project)
  startService("backend", "npm", ["start"], { cwd: "./backend" });
  startService("frontend", "npm", ["run", "dev"], { cwd: "./frontend" });

  // 2. Wait for readiness
  await waitFor(`${BACKEND_URL}/api/health`, { label: "backend health" });
  await waitFor(FRONTEND_URL, { label: "frontend" });

  // 3. Submit a booking through the UI
  const booking = {
    date: "2025-12-31",
    time: "19:00",
    partySize: "4",
  };

  const browser = await chromium.launch();
  const page = await browser.newPage();

  try {
    await page.goto(FRONTEND_URL, { waitUntil: "networkidle" });

    await page.fill('input[name="date"]', booking.date);
    await page.fill('input[name="time"]', booking.time);
    await page.fill('input[name="partySize"]', booking.partySize);
    await page.click('button[type="submit"]');

    // 4. Confirm the new booking appears in the list
    const expectedText = `${booking.date} ${booking.time}`;
    const listItem = page.locator("li", { hasText: expectedText }).first();
    await listItem.waitFor({ state: "visible", timeout: 10000 });

    const listText = await listItem.textContent();
    if (!listText.includes(booking.partySize)) {
      throw new Error(`Booking found but party size missing: "${listText}"`);
    }

    // Cross-check via API
    const res = await fetch(`${BACKEND_URL}/api/bookings`);
    const bookings = await res.json();
    const match = bookings.find(
      (b) =>
        b.date === booking.date &&
        b.time === booking.time &&
        String(b.partySize) === booking.partySize
    );
    if (!match) throw new Error("Booking not found in GET /api/bookings");

    console.log(`✓ Booking verified in UI and API: ${JSON.stringify(booking)}`);
    console.log("PASS");
  } finally {
    await browser.close();
  }
}

function shutdown(code) {
  for (const proc of processes) {
    if (proc.exitCode === null) {
      proc.kill("SIGTERM");
    }
  }
  // Force-kill stragglers after a grace period
  setTimeout(() => {
    for (const proc of processes) {
      if (proc.exitCode === null) proc.kill("SIGKILL");
    }
    process.exit(code);
  }, 2000);
}

main()
  .then(() => shutdown(0))
  .catch((err) => {
    console.error("✗ FAIL:", err.message);
    shutdown(1);
  });

// Ensure cleanup on Ctrl+C
process.on("SIGINT", () => shutdown(130));
process.on("SIGTERM", () => shutdown(143));

Notes to adapt before running:

  • Start commands: adjust cwd/commands in startService (e.g. node server.js instead of npm start).
  • Health endpoint: I assumed GET /api/health; change if yours differs.
  • Selectors: I assumed input[name="date"], input[name="time"], input[name="partySize"], and a button[type="submit"]; swap for your actual form markup. The list check assumes bookings render as <li> elements containing date, time, and party size.
  • Windows: shell: true is set for spawn so npm works; on Unix it's harmless too.
with skill · 0.85

Here's a single self-contained verification script (Node + Playwright). Adjust the start commands/selectors to match your repo.

// verify-booking.js — starts backend + frontend, drives the booking form via the UI, verifies, tears down.
// Usage: node verify-booking.js
// Assumes: `npm run dev` (or equivalent) starts each service; Playwright installed.

const { spawn } = require("child_process");
const { chromium } = require("playwright");

const BACKEND_URL = "http://localhost:4000";
const FRONTEND_URL = "http://localhost:5173";
const BACKEND_CMD = ["npm", ["run", "dev", "--prefix", "./backend"]];
const FRONTEND_CMD = ["npm", ["run", "dev", "--prefix", "./frontend"]];

const started = [];

function startService(name, cmd, args) {
  const proc = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
  started.push(proc);
  proc.stdout.on("data", (d) => process.stdout.write(`[${name}] ${d}`));
  proc.stderr.on("data", (d) => process.stderr.write(`[${name}] ${d}`));
  return proc;
}

async function waitFor(url, { timeoutMs = 30000, intervalMs = 500 } = {}) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    try {
      const res = await fetch(url);
      if (res.ok) return res;
    } catch {}
    await new Promise((r) => setTimeout(r, intervalMs));
  }
  throw new Error(`Timed out waiting for ${url}`);
}

async function cleanup() {
  for (const proc of started) {
    if (!proc.killed) {
      proc.kill("SIGTERM");
      // Force-kill if it ignores SIGTERM
      setTimeout(() => { if (!proc.killed) proc.kill("SIGKILL"); }, 3000);
    }
  }
}

async function main() {
  let browser;
  try {
    // 1. Start services
    startService("backend", ...BACKEND_CMD);
    startService("frontend", ...FRONTEND_CMD);

    // 2. Wait for readiness: backend health endpoint + frontend page
    await waitFor(`${BACKEND_URL}/api/health`);
    console.log("✔ backend healthy");
    await waitFor(FRONTEND_URL);
    console.log("✔ frontend serving");

    // 3. Drive the UI like a user
    browser = await chromium.launch();
    const page = await browser.newPage();
    await page.goto(FRONTEND_URL);

    // Unique booking data so we can assert on exactly this row
    const date = new Date(Date.now() + 86400000).toISOString().slice(0, 10); // tomorrow
    const time = "19:30";
    const partySize = "4";

    await page.getByLabel(/date/i).fill(date);
    await page.getByLabel(/time/i).fill(time);
    await page.getByLabel(/party/i).selectOption(partySize); // use .fill() if it's a text input
    await page.getByRole("button", { name: /book|submit|reserve/i }).click();

    // 4. Confirm the new booking appears in the list
    const row = page.locator("li, tr").filter({ hasText: date }).filter({ hasText: time });
    await row.first().waitFor({ state: "visible", timeout: 10000 });
    const rowText = await row.first().innerText();
    if (!rowText.includes(partySize)) {
      throw new Error(`Booking row found but party size missing: "${rowText}"`);
    }
    console.log(`✔ booking visible in list: ${rowText.replace(/\s+/g, " ").trim()}`);

    // 5. Cross-check the backend state (side effect, not just the UI)
    const res = await fetch(`${BACKEND_URL}/api/bookings`);
    const bookings = await res.json();
    const match = bookings.find(
      (b) => b.date === date && b.time === time && String(b.partySize) === partySize
    );
    if (!match) throw new Error("Booking not found via GET /api/bookings");
    console.log(`✔ backend persisted booking id=${match.id ?? "(no id)"}`);

    // Evidence screenshot
    await page.screenshot({ path: "verify-booking-evidence.png", fullPage: true });
    console.log("✔ screenshot saved: verify-booking-evidence.png");

    console.log("\nVERIFICATION PASSED");
  } catch (err) {
    console.error("\nVERIFICATION FAILED:", err.message);
    process.exitCode = 1;
  } finally {
    if (browser) await browser.close();
    await cleanup();
  }
}

main();

Notes on adapting it:

  • Start commands: replace BACKEND_CMD/FRONTEND_CMD with your actual dev commands (e.g., ["node", ["server.js"]]).
  • Health endpoint: if you don't have /api/health, point waitFor at GET /api/bookings (any 2xx means the backend is up).
  • Selectors: I used accessible-label matching (getByLabel(/date/i), etc.), which is the most stable option. If your form lacks labels, add aria-label or data-testid attributes to the inputs — that's the durable fix.
  • Party size: selectOption for a <select>, .fill() for a number/text input.
  • Teardown: kills only the processes it spawned (SIGTERM, then SIGKILL after 3s), and runs even on failure, so no orphaned servers on ports 4000/5173.
  • Evidence: the screenshot and console transcript survive teardown; the backend cross-check via GET /api/bookings proves the write actually persisted, not just that the UI re-rendered.

One caveat: if your frontend dev server proxies /api to the backend, the UI submission works as-is; if the frontend calls http://localhost:4000 directly, make sure CORS is enabled on the backend or the form submit will fail in the browser even though both services are up.

Limitations
  • Results are for the SkillzBench reference agent on GLM-5.3-flash. Switch models above — a skill can help one model and not another.
  • Screen tier (1 trial per arm) is directional. Verified needs a full-tier run (≥ 8 tasks, ≥ 3 trials per arm) with the CI excluding zero.
  • Task coverage: 3 generated tasks for this skill's category; a skill can shine elsewhere.