create-verification-skill
Generate a project-local verification skill to drive an app and prove its behavior
Pass rate with the skill minus without, same tasks, same model.
- 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
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 →
[](https://skillzbench.com/skills/create-verification-skill)[](https://skillzbench.com/skills/create-verification-skill)<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
// 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
/healthuntil 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.
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.jsas a child process (change the filename in thespawncall if your entry point differs) and polls/healthuntil it's ready, with a 5s timeout. - It kills only the process it started, and always tears down in a
finallyblock — 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
#!/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
HOMEto amktemp -ddirectory, so~/.todo.jsonis fully isolated and removed on exit viatrap ... EXIT. - Run it as
./verify_todo.sh(assumestodois on PATH) or./verify_todo.sh /path/to/todo. - The
donecheck expects the exact stringTask 1 done; adjust thelist-after-doneassertion if your CLI displays completed tasks rather than hiding them. - Exit status is nonzero if any check fails, so it's CI-friendly.
(no text deliverable)
create-verification-skill--m4
// 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 instartService(e.g.node server.jsinstead ofnpm 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 abutton[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: trueis set for spawn sonpmworks; on Unix it's harmless too.
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_CMDwith your actual dev commands (e.g.,["node", ["server.js"]]). - Health endpoint: if you don't have
/api/health, pointwaitForatGET /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, addaria-labelordata-testidattributes to the inputs — that's the durable fix. - Party size:
selectOptionfor 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/bookingsproves 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.
- 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.