User Story Writer
deanpeters/Product-Manager-Skills
Writes user stories in Mike Cohn's format with Gherkin acceptance criteria, turning user needs into development-ready work with testable conditions.
Verify any change surface against approved acceptance criteria, run the real system, and preserve the report and test artifacts.
$ npx skills add opslane/verify --skill verify -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install opslane/verify verify --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/opslane/verify.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/verify .claude/skills/verify && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "verify" agent skill from https://github.com/opslane/verify/tree/main/skills/verify into .claude/skills/verify/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "verify", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/opslane/verify/tree/main/skills/verifyType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add opslane/verify --skill verify -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install opslane/verify verify --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/opslane/verify.git skills-src && mkdir -p .agents/skills && cp -r skills-src/skills/verify .agents/skills/verify && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "verify" agent skill from https://github.com/opslane/verify/tree/main/skills/verify into .agents/skills/verify/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "verify", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add opslane/verify --skill verify -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install opslane/verify verify --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/opslane/verify.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/skills/verify .cursor/skills/verify && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "verify" agent skill from https://github.com/opslane/verify/tree/main/skills/verify into .cursor/skills/verify/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "verify", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/opslane/verify.git --path skills/verify--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add opslane/verify --skill verify -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install opslane/verify verify --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/opslane/verify.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/skills/verify .gemini/skills/verify && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "verify" agent skill from https://github.com/opslane/verify/tree/main/skills/verify into .gemini/skills/verify/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "verify", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install opslane/verify verifyInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add opslane/verify --skill verify -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/opslane/verify.git skills-src && mkdir -p .github/skills && cp -r skills-src/skills/verify .github/skills/verify && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "verify" agent skill from https://github.com/opslane/verify/tree/main/skills/verify into .github/skills/verify/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "verify", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add opslane/verify --skill verify -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install opslane/verify verify --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/opslane/verify.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/skills/verify .opencode/skills/verify && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "verify" agent skill from https://github.com/opslane/verify/tree/main/skills/verify into .opencode/skills/verify/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "verify", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
verifyVerify any change surface against approved acceptance criteria, run the real system, and preserve the report and test artifacts.
Verify is an agent skill from opslane/verify. Verify any change surface against approved acceptance criteria, run the real system, and preserve the report and test artifacts.
Its SKILL.md is about 11k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Product & Project Management, covering User stories. The repository describes itself as: Verification Skill for Claude Code. The licence is MIT.
12 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit 25dc3a9. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
bashnpxgitjqnpmgopython3curlbrewclaudeFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md. Its commands use npx, git, npm and curl, which can reach the network depending on how they are called.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Verify loads about 11k tokens when it runs. Until then it costs about 34 tokens; SKILL.md has 5,578 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check noted patterns worth knowing about, such as sudo or a known installer.
t most name one of the repo's own local `.env`Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from opslane/verify at commit 25dc3a9, republished under its MIT licence (© opslane). 5,578 words, ~10,846 tokens.
.claude/skills/verify/SKILL.md (or your agent's skills folder).Verify that a change does what its plan said. Report what you observe and keep the receipts. Never fix the code being judged.
This workflow has exactly two halves. Half one creates acceptance criteria and stops for approval. Half two runs only after the user approves those criteria by saying go or an equally explicit instruction.
Not checked is always printed, even when empty.Not checked reason states why it was not driven. It never asserts that something else covers it, unless it names that thing and says plainly this run did not re-run it.tests/ directory until the user explicitly chooses to check them in.The engine is a local TypeScript package with no installed verify binary. Every invocation must be self-contained because shell variables and working directories do not survive between tool calls.
Use this exact resolution rule at every call site, replacing the verb and arguments as needed:
VERIFY_PIPELINE="${VERIFY_PIPELINE:-$CLAUDE_PLUGIN_ROOT/pipeline}"
(cd "$VERIFY_PIPELINE" && npx --no-install tsx src/cli.ts <verb> [arguments])CLAUDE_PLUGIN_ROOT is set by Claude Code for an installed plugin. Set VERIFY_PIPELINE
yourself only when running from a development checkout. Never hardcode a path to someone's
home directory; if neither variable resolves, stop and say so rather than guessing.
The environment helpers are bash scripts resolved the same way:
VERIFY_SCRIPTS="${VERIFY_SCRIPTS:-$CLAUDE_PLUGIN_ROOT/scripts}"
bash "$VERIFY_SCRIPTS/<name>.sh" [arguments]Once per machine, install the engine's dependencies. The plugin ships TypeScript source
and no node_modules, so the first call fails without this. It is a lockfile-pinned install
inside the plugin's own directory, it touches nothing in the target repository, and it is
safe to re-run:
VERIFY_PIPELINE="${VERIFY_PIPELINE:-$CLAUDE_PLUGIN_ROOT/pipeline}"
if [ ! -x "$VERIFY_PIPELINE/node_modules/.bin/tsx" ]; then
(cd "$VERIFY_PIPELINE" && npm ci)
fiRun that before the first engine call of a session. Do not fall back to plain npx, which
would fetch an unpinned package from the network.
Optional recorder garnish. Driven criteria get an engine-recorded transcript from their
receipts; no PTY is involved. asciinema and agg are attempted only for hand-driven
flows, under hard wall-clock limits. A missing, hanging, or broken recorder never blocks
verification: record it under Not checked and continue. On a fresh box:
brew install asciinema agg, or the equivalent for the platform.
The skill runs in the target repository, not the plugin repository. Resolve every target-repository path with pwd -P and pass absolute paths for --repo, --dir, --criteria, --results, and --claims.
First pull anything a fresh worktree is missing from the per-repo shared store (captured login state; a fallback env file stays in the store and is picked up automatically by the environment scripts):
VERIFY_SCRIPTS="${VERIFY_SCRIPTS:-$CLAUDE_PLUGIN_ROOT/scripts}"
bash "$VERIFY_SCRIPTS/shared-store.sh" pull.verify/setup.json records how this repo boots, seeds, and reports health
(written once by /verify-setup from sniffed candidates). If it is missing,
ask once: "No setup contract found. Run /verify-setup (recommended), or
continue in plain-command mode without a managed stack?" On plain-command
consent, write a minimal contract inline:
{"mode": "none", "compose_file": null, "boot": "", "teardown": "", "seed": [],
"seed_data_files": [], "health_url": "", "base_url": "", "env_file": "",
"observe": {}, "probes": {}}Never silently proceed without one. Verify never asks for or stores sensitive
credentials; the contract may at most name one of the repo's own local .env
files.
Look in this order:
/verify.docs/plans/..omx/plans/.Use the newest plan that clearly describes the current change. If no plan is available, ask the user for one and stop. Do not derive criteria from the diff alone.
The diff raises questions. It never answers them.
This is the line that keeps verification black box. An expectation comes from the plan, or
from the behavior that existed before the change, or from an assumption you label
invented so the user can correct it. Never from the implementation you are verifying.
That is what the source field records, and why invented is a legitimate value rather
than an admission of failure. A plan that says "field values persist" without naming a
field leaves you a choice. Making it and flagging it loudly is honest. Resolving it by
opening the code and testing whatever it happens to do is not, because that criterion
cannot fail.
So do not go reading the implementation for things to test. If the plan says "bound every
outbound call with a timeout" and does not say what the budget is, the criterion is not
"raises at 30 seconds" because you found DEFAULT_TIMEOUT = (5, 30) in the diff. That
criterion passes by construction. Ask what the budget should be.
What the diff is for. Once the criteria are drafted from the plan, read the diff once to find gaps. Mark two things:
Each one goes in the approval artifact as a question, naming the change that prompted
it and asking what the behavior should be. You do not answer it yourself. A question the
user answers becomes a criterion with {"kind": "inferred", "from": "..."} whose
expectation is theirs. A question they wave off goes in the uncovered list, where it stays
visible.
The difference is between asking "the /confluence/* route was deleted and the plan never
mentions it, was that intended?" and deciding for yourself what that route ought to do. The
first found a production regression. The second writes a criterion that agrees with
whatever the code now does.
Two real examples of what one pass catches. A diff added two clear() calls, in an early
return and an error handler, that the plan never asked for; one of them was the bug, and it
was on screen while the criteria were being drafted. A different diff deleted an ingress
route in the same hunk that added a new one, while the backend still served it; nothing in
the plan mentioned the deletion, because a plan records what someone meant to add and is
silent on what went out with it.
If an earlier run on the current branch has a criteria.md, show its path and offer to start from it. Stop for the user's choice before replacing or reusing it. A run drafted before criteria carried source.quote and why still reports, with the gap printed where the citation would be, but its criteria cannot be approved again as they stand: add both, from the spec, to every criterion you carry forward.
Ensure .verify/ is ignored by the target repository. Add .verify/ to its .gitignore when missing.
Create the run and persist its identity in one tool call:
TARGET_REPO="$(pwd -P)"
RUN_ID="$(date +%Y%m%d-%H%M%S)"
mkdir -p "$TARGET_REPO/.verify/runs/$RUN_ID/tests"
printf '%s\n' "$RUN_ID" > "$TARGET_REPO/.verify/current-run"
printf '%s\n' "<path to the plan chosen in step 1>" > "$TARGET_REPO/.verify/.spec_path"The empty tests/ directory is intentional and must exist even when no test is generated.
.spec_path is how the engine and the second-opinion reviewer get the spec: the
engine looks every quote up in it, and the reviewer reads it. When the plan is not a
file (it came from the conversation or a pull request body), write it verbatim to
$TARGET_REPO/.verify/runs/$RUN_ID/spec.md first and record that path.
Snapshot the working tree now — before any model-driven step (including the
second-opinion reviewer) runs with permissions — so the run can later prove
nothing outside .verify/ changed (staged changes included):
git diff HEAD > .verify/pre-run.diff
git ls-files -o --exclude-standard | grep -v '^\.verify/' | while IFS= read -r f; do printf '%s %s\n' "$(git hash-object "$f" 2>/dev/null || echo missing)" "$f"; done > .verify/pre-run-untracked.txtChoose the branch's merge base. Prefer the PR base or upstream merge base; do not guess a different branch when repository metadata supplies one.
Run:
TARGET_REPO="$(pwd -P)"
RUN_ID="$(cat "$TARGET_REPO/.verify/current-run")"
VERIFY_PIPELINE="${VERIFY_PIPELINE:-$CLAUDE_PLUGIN_ROOT/pipeline}"
(cd "$VERIFY_PIPELINE" && npx --no-install tsx src/cli.ts changed-files \
--repo "$TARGET_REPO" --base <merge-base>) \
> "$TARGET_REPO/.verify/runs/$RUN_ID/changed-files.json"The result includes committed branch changes, staged and unstaged changes, and untracked behavior files. Test files are excluded.
Translate the plan into concrete, observable criteria. Each criterion has:
id: stable AC1, AC2, and so on.title: one behavior.plain: reader-facing claim, one sentence with no implementation jargon. Draft it for
every new criterion: this is the report-card headline the user approves. The engine
keeps the field schema-optional only so older runs can re-render, falling back to
title.doIt: the intent of the real action, kept short because an approved drive plan is
the execution authority when one exists.expectIt: a measurable observation.source: { "kind": "plan", "ref": "...", "quote": "..." },
{ "kind": "inferred", "from": "..." }, or { "kind": "invented", "note": "..." }.
For a plan source, ref says where in the spec (a requirement id, a heading, a line)
and quote is the spec's own words, copied verbatim, that the criterion was read
from. Keep the quote to the sentence or clause that carries the requirement. The
engine looks every quote up in the spec and lists the ones it cannot find under the
table, so a paraphrase has to be reworded or the criterion relabelled. For an
inferred source, from names the diff observation and the answer the user gave. For
an invented one, note states the assumption.why: one sentence on why this check exists: the bug it would catch, or what would
go wrong if the behaviour did not hold. Not a restatement of the title. "The retry
schedule is the whole feature" and "over-eager code clears valid selections too" are
reasons; "checks that retries work" is the title again. The engine rejects a why
that is the title or the plain claim word for word.intent: "changes" or "preserves". What the criterion is for.baseline: "fail", "pass", "not-applicable", or "unknown". What you expect the
base commit to do with it.witness: "success" or "refusal". Does this criterion show something working, or
show something correctly turned away?dependsOn: the parts of the system the criterion drives or observes, from exactly
"api", "db", "worker", "browser", "sink", "storage". Half two probes each
named part once before judging; a down part marks only its dependents "could not run".proof: how a reader will know the check actually ran. One of
{"kind": "marker-in-data", "detail": "..."} (the run marker woven into created data —
the strongest form), {"kind": "marked-request-rejected", "detail": "..."} (the
rejection paired with the marked request), or {"kind": "live-read", "detail": "..."}
(a value read fresh during the run, not a stale capture). A criterion you cannot name
a proof for is defective: move it to skipped with the reason "no way to prove it ran".drive: for criteria reachable through generic command, HTTP, or read-only database
surfaces, an ordered plan of exactly these verbs: http, db, wait, run. Each
step is { "verb": "...", "args": ["..."], "timeoutSeconds": 60 }; omit the
timeout for the 60-second default. Plans are execution authority and run verbatim, so
never put shell strings, database writes, assertions, or app-specific verbs in them.
UI criteria remain plan-less and hand-driven.marker-in-data proof also declares "step": N and optional
"expect": "present" | "absent" (default present). The designated step is the
only output the engine uses for mechanical proof. expectIt remains the judge's
behavioral rubric.intent is what the criterion is for. baseline is a separate claim about the base
commit. Keeping them apart matters, because the obvious shortcut of defining changes as
"fails on base" is wrong for real criteria:
intent: changes, baseline: not-applicable.intent: preserves, baseline: not-applicable.changes means the criterion exists to prove the change did something. Its normal baseline
is fail.
preserves means it guards something the change could have broken. Its normal baseline is
pass, and that is the point rather than a defect. A preserves criterion names what it
guards against: the changed route, setting, or file that could plausibly have broken it.
The check is one line: intent: changes with baseline: pass is a defect. That
criterion passed before the change existed, so it proves nothing. Everything else is a
question of honesty about the baseline, not a rule violation.
Declaring a baseline obliges you to picture the base commit while you are drafting. That is the whole mechanism. Most free passes are visible the moment you ask "would this have passed last week?" and invisible three steps later when the run comes back green.
unknown is a real answer and a cheap one. It goes in the approval artifact as a question.
An honest unknown is a much smaller failure than a confident fail that turns out to be
a free pass.
witness exists because a set can be all-refusal and look complete. Six criteria that
all check something is rejected are satisfied by an implementation that rejects everything.
Count the boxes before you submit:
| preserves | changes | |
|---|---|---|
| success | the old path still works | the new capability works |
| refusal | the old guard still holds | the new guard holds |
Every new capability needs at least one success + changes criterion: the thing works,
end to end, driven the way a user drives it. That box being empty is the most common way a
criteria set passes while proving nothing.
Write every criterion as a user would experience it, not as the code is structured. A criterion names a request, a command, a message, or a screen, and the observable it produces. It never names a function, a class, or an internal call. If a criterion cannot be checked without reading source, it is the wrong criterion: rewrite it as something a customer could do.
Good: POST /api/v1/events with a staging key, then read the row: environment_id is the staging environment.
Bad: resolvePayloadEnvironment returns the key-bound environment.
This is why the workflow does not inspect the repository's existing tests. Whether a unit test exists, and whether it mocks something, is a question about the test suite. This workflow answers one question only: does the change work when driven the way a user drives it.
Ask what the laziest implementation that passes would look like. For every criterion, before it goes in the table: could a stub, a constant, or a function that always does the same thing pass this? If yes, the criterion is an observation, not a check.
Real example. A criterion read "a stale selection is cleared from state, URL, and local storage." It passed. Code that clears the selection unconditionally, always passes it just as cleanly, and that was the bug: a valid selection was being thrown away on every page load. The criterion tested the rule in one direction only, and the broken build satisfied it.
So every rule with an on and an off gets both criteria. If a criterion says something is cleared, hidden, rejected, disabled, filtered, or logged out, write its opposite: a valid one is kept, shown, accepted, enabled, passed through, stayed logged in.
The negative side is nearly always where the bug lives, because over-eager code satisfies the positive side by accident:
| the plan says | the obvious criterion | the one that catches bugs |
|---|---|---|
| clear a stale selection | a stale one is cleared | a valid one survives |
| reject an invalid key | an invalid key gets 401 | a valid key still works |
| hide the panel when empty | empty hides it | non-empty still shows it |
| retry on failure | a failure retries | a success does not retry |
A plan describes what a change adds. Criteria copied from a plan inherit that blind spot, and nothing in the plan describes what must not break. Writing the opposite criterion is how you get it back.
Write .verify/runs/<id>/claims.json as a mapping from criterion id to changed files it claims, for example:
{
"AC1": ["src/assets.ts"],
"AC2": ["src/auth.ts"]
}Then compute uncovered files from that artifact:
TARGET_REPO="$(pwd -P)"
RUN_ID="$(cat "$TARGET_REPO/.verify/current-run")"
RUN_DIR="$TARGET_REPO/.verify/runs/$RUN_ID"
VERIFY_PIPELINE="${VERIFY_PIPELINE:-$CLAUDE_PLUGIN_ROOT/pipeline}"
(cd "$VERIFY_PIPELINE" && npx --no-install tsx src/cli.ts changed-files \
--repo "$TARGET_REPO" --base <merge-base> --claims "$RUN_DIR/claims.json") \
> "$RUN_DIR/coverage.json"Write .verify/runs/<id>/criteria.json in this shape, copying coverage.json.uncovered into uncoveredFiles:
{
"criteria": [],
"uncoveredFiles": []
}Render the approval artifact:
TARGET_REPO="$(pwd -P)"
RUN_ID="$(cat "$TARGET_REPO/.verify/current-run")"
RUN_DIR="$TARGET_REPO/.verify/runs/$RUN_ID"
VERIFY_PIPELINE="${VERIFY_PIPELINE:-$CLAUDE_PLUGIN_ROOT/pipeline}"
(cd "$VERIFY_PIPELINE" && npx --no-install tsx src/cli.ts criteria \
--criteria "$RUN_DIR/criteria.json" \
--spec "$(cat "$TARGET_REPO/.verify/.spec_path")") > "$RUN_DIR/criteria.md"The rendered artifact shows each plain claim alongside expectIt and includes a
Drive plans (what will actually run) section; its verbatim steps and designated proof
line are part of what the user approves.
If any criterion needs volume or precondition data (thresholds, grouping, "with 6
users..."), write a literal $RUN_DIR/seed.sh now: real commands (curl, CLI) that
create that data through the application's front door, taking the run marker as $1
and weaving it into every entity. No LLM runs at seed time; this script IS the seed,
and the user reviews it. An entry that genuinely needs browser interaction is flagged
"needs an agent — approve separately?".
Then get the second opinion — a reviewer that did not write these criteria:
VERIFY_SCRIPTS="${VERIFY_SCRIPTS:-$CLAUDE_PLUGIN_ROOT/scripts}"
VERIFY_ALLOW_DANGEROUS=1 bash "$VERIFY_SCRIPTS/review.sh"It uses Codex when installed (a different vendor, different blind spots), else a fresh
claude -p that sees only the spec and the criteria, and writes
$RUN_DIR/review.json (keep/why/codify per criterion plus a missing list).
Its output is advice; the user is the tiebreaker. If the reviewer is unavailable,
say so verbatim: the criteria were reviewed only by the model that wrote them.
Also compute which relied-on parts have no probe, so the user sees it before approving:
jq -r --slurpfile s .verify/setup.json '
[.criteria[].dependsOn[]?] | unique
| map(select(IN("worker","sink","storage") and (($s[0].probes[.] // "") == "")))
| if length > 0 then "Unprobed parts this run relies on: " + join(", ") else empty end' \
"$RUN_DIR/criteria.json"Print criteria.md, the seed script (verbatim), the reviewer's table and missing
list, and the unprobed-parts line, including invented specifics and uncovered
changed files. Ask the user to edit, correct, or say go.
The table carries each criterion's citation and reason so the user can check every
row against the spec without opening it. The engine prints a NOT IN THE SPEC block
for any quote the spec does not contain, and rejects a criterion that omits a quote or
a why, or whose why is the title restated. A why that says nothing the title did
not, in different words, is still a reason to send the criterion back.
Before approval, a plan may be inspected without counting as evidence. Dry-run the
fully substituted plan, or exercise one original step into the isolated drafts/
folder:
VERIFY_PIPELINE="${VERIFY_PIPELINE:-$CLAUDE_PLUGIN_ROOT/pipeline}"
(cd "$VERIFY_PIPELINE" && npx --no-install tsx src/cli.ts drive AC1 \
--repo-root "$TARGET_REPO" --run-dir "$RUN_DIR" \
--dry-run --criteria "$RUN_DIR/criteria.json")
(cd "$VERIFY_PIPELINE" && npx --no-install tsx src/cli.ts drive AC1 \
--repo-root "$TARGET_REPO" --run-dir "$RUN_DIR" \
--draft --criteria "$RUN_DIR/criteria.json" --step 1)Draft receipts never participate in reconciliation. If an exercise changes the plan, re-render the approval artifact so the user signs the exact steps that will run.
Stop here. Apart from the explicit isolated drafting helpers above, run no verification command and perform no system mutation until the user approves the criteria.
Enter this half only after explicit approval. Read the persisted run id; never rely on an earlier shell variable:
TARGET_REPO="$(pwd -P)"
RUN_ID="$(cat "$TARGET_REPO/.verify/current-run")"
RUN_DIR="$TARGET_REPO/.verify/runs/$RUN_ID"
test -f "$RUN_DIR/criteria.json" && test -f "$RUN_DIR/criteria.md"If the user requested changes to the criteria instead of approving them, update and re-render half one, then stop again.
Boot a throwaway environment and arm teardown before anything else can fail:
VERIFY_SCRIPTS="${VERIFY_SCRIPTS:-$CLAUDE_PLUGIN_ROOT/scripts}"
bash "$VERIFY_SCRIPTS/env.sh" upFrom this point every exit path must run bash "$VERIFY_SCRIPTS/env.sh" down —
run the rest of half two inside a subshell opening with
trap 'bash "$VERIFY_SCRIPTS/env.sh" down' EXIT, or tear down explicitly on
every failure branch. Boot and seed are separate verbs precisely so a seed
failure cannot leak the stack. up writes .verify/run-env.json with this
run's marker and rotates old runs (newest 5 kept).
Seed, in order:
bash "$VERIFY_SCRIPTS/env.sh" seed # repo seed scripts + user data filesIf the approved $RUN_DIR/seed.sh exists, run it with the marker — a failure
aborts before judging, because judging against a half-seeded system is how
wrong verdicts happen:
MARKER=$(jq -r '.marker' .verify/run-env.json)
VERIFY_SCRIPTS="${VERIFY_SCRIPTS:-$CLAUDE_PLUGIN_ROOT/scripts}"
VERIFY_BASE_URL="$(jq -r '.base_url // empty' .verify/setup.json | bash "$VERIFY_SCRIPTS/expand.sh" --load-env .verify/setup.json)" \
bash "$RUN_DIR/seed.sh" "$MARKER" 2>&1 | tee "$RUN_DIR/seed.log"Then prove the pipes work before judging anything:
bash "$VERIFY_SCRIPTS/precheck.sh"Every part named by any criterion's dependsOn gets one probe (a marker
round-trip for the database; any HTTP response for the API; configured
commands for worker/sink/storage). A down part taints only its dependent
criteria: do NOT drive those — record each as could-not-run with the
pre-check's evidence ($RUN_DIR/prechecks/<part>.log). A part with no probe
reports unknown and never taints; a FAIL on a criterion relying on an
unknown part gets "the failure may be environmental: <part> was never
health-checked" appended to its observation.
Driven criteria need no live recorder: their finalized receipts produce the step list and
terminal transcript in report.html. If the run has no hand-driven criteria, skip
asciinema entirely.
For hand-driven criteria, check asciinema, agg, and GNU timeout, then perform one
bounded start-check. Never retry or debug a recorder during the run:
command -v asciinema >/dev/null || echo "asciinema missing: brew install asciinema"
command -v agg >/dev/null || echo "agg missing: brew install agg"
command -v timeout >/dev/null || echo "timeout missing: recorder stages cannot be bounded"
timeout --signal=TERM --kill-after=2s 10s \
asciinema rec --overwrite --command "printf recorder-ok" "$RUN_DIR/recorder-check.cast"If the start-check hangs or fails, skip recording loudly, add the exact failure under
Not checked, and continue. When it succeeds, generate one bounded hand-drive script
under the run directory. Give each operation its own timeout, then set
RECORDING_BUDGET_SECONDS to their total plus a small shutdown allowance. Bound the whole
recorder process group as well:
VERIFY_HAND_DRIVE="$RUN_DIR/hand-drive.sh" \
timeout --signal=TERM --kill-after=5s "${RECORDING_BUDGET_SECONDS}s" \
asciinema rec --overwrite --command 'exec bash "$VERIFY_HAND_DRIVE"' "$RUN_DIR/run.cast"
timeout --signal=TERM --kill-after=5s 60s agg "$RUN_DIR/run.cast" "$RUN_DIR/run.gif"If recording fails, continue the hand-driven checks without it and record the failure in
Not checked. If agg fails, keep run.cast, omit run.gif, and record that failure.
No recorder stage may run without its own wall-clock timeout.
For every approved criterion that has a drive plan, invoke the engine exactly once:
VERIFY_PIPELINE="${VERIFY_PIPELINE:-$CLAUDE_PLUGIN_ROOT/pipeline}"
(cd "$VERIFY_PIPELINE" && npx --no-install tsx src/cli.ts drive AC1 \
--repo-root "$TARGET_REPO" --run-dir "$TARGET_REPO/.verify/runs/$RUN_ID")Replace AC1 with that criterion's id; do not batch or repeat planned criteria.
The JSON manifest is the neutral execution summary. Carry its completed, per-step
completed / command-error / timeout / not-attempted states, and proof result into
the result. The report reads the newest finalized attempt itself and synthesizes its
numbered step list and transcript; do not name receipt files in evidence. The judge maps
command errors and timeouts to the behavioral rubric using the receipts and prechecks.
Report, don't rescue. Never rerun a driven criterion or tweak its approved arguments mid-run. A plan that needs changes goes back through criteria approval. Criteria without a plan remain hand-driven using the guidance below.
Choose the cheapest way to actually check each criterion, using the repository's own commands. Cheapest sufficient proof means the least setup that still observes the behaviour. It does not mean skipping a criterion because driving it is inconvenient.
Weave the run marker into everything you create. .verify/run-env.json
holds this run's marker. Every entity a criterion creates carries it (form
fields, payloads, names). Each criterion's declared proof says which
artifact must show the check ran: capture that artifact verbatim in the
evidence — the marker-bearing row, the rejection paired with the marked
request, or the fresh live read. Status codes alone and absence-of-error are
never evidence.
UI criteria, in detail. A web interface is the surface where "drive it the way a user
does" is most literal, so it gets a real browser rather than an HTTP client. The plugin
declares Playwright MCP, so mcp__playwright__* tools are available once the plugin is
installed.
could-not-run with the reason, exactly as with a missing recorder. Never substitute
a curl against the page and call it a UI check: fetching HTML does not prove a user
can complete the flow.evidence list. The
screenshot is evidence, not decoration.A UI criterion that cannot be judged without reading the page source is the wrong criterion. Rewrite it as something a person could confirm by looking.
Arrive at state, do not plant it. It is tempting to set local storage, cookies, or query parameters and then load the page you care about. That is faster, and it hides every bug that lives in the transition: what the page does on its first pass, before something has resolved, is exactly where these sit.
At least one criterion per run should walk a real path end to end in a single session — land on the first page, click through, and observe on the last one — rather than loading each page cold with values planted by hand.
Real example. Every check for one change opened a single page with local storage set directly. The bug was in what the sessions page did on its first pass, before the project id resolved, on the way in from the issues page. Nobody ever made that journey, so nobody saw it.
Mutation in a disposable local system is allowed. Before writing to a shared or staging system, describe the exact mutation and obtain a specific yes. Before provisioning anything that costs money, obtain a specific yes. If permission is not given, record the criterion as could-not-run; do not count it as a behavior failure.
Some criteria are drivable but costly: they need a failure induced, a long-running job, a fixture built, or a service stood up that nothing else in the run needs.
Cost is the user's decision, not yours. Do not quietly move a criterion to
Not checked because it would take a while. Present it, with what it would cost, and let
them choose:
AC5 needs a job that fails with a key in its error text before the admin endpoint
shows anything. Roughly 15 minutes on the local stack: seed a job, force it to
fail with a key in the message, then read it back through the admin surface.
Drive it, or record it as not checked?Note what that does not say. It does not say "needs production". The cost is the fifteen minutes of setup, and naming the setup is what lets the user judge it. Naming an environment instead hides the real reason and sounds like a wall.
Ask before half two finishes, while the stack is still up and the answer is still cheap
to act on. If the user declines, the Not checked reason is what it would have taken,
not a judgement that it did not matter.
If several criteria are expensive, list them together with their costs and let the user pick which ones are worth it. Say which one you would drive if they only pick one, and why — usually the one whose failure would be worst.
Before writing that a criterion needs production, staging, a live service, or "a real failure", ask what actually stops you from causing that locally. Usually nothing does.
A job that fails with a key in its error text is a job you can make fail. A worker run is a worker you can start. A queue message is a message you can publish. The stack is already up, because half two brought it up.
Only these are real blockers, and each has to be named specifically:
"Needs production" is almost never one of these. If the reason you wrote names an environment, replace it with the specific thing that environment has and yours does not. If you cannot name that thing, the blocker is effort, and effort is the user's decision (see above).
A Not checked reason explains why the criterion was not driven here. That is all it is
required to do.
If you also believe something else covers it, that claim has to be checkable by a reader who was not present. Name it, and state plainly that this run did not re-run it:
GOOD not driven: the user declined the 15 minutes of fixture setup it needed.
A test exists: TestRedactAdminErrorSwallowsEndpointBearingProjectKey
(Go handler suite). This run did not execute it.
BAD covered by unit canaries
BAD needs a production job to fail with a key in its error text
(an environment standing in for setup work; see the section above)Never write that something is covered by tests you did not run without naming them. An unnamed claim of coverage cannot be checked, reads as reassurance, and is exactly the thing this report exists to avoid. A named test that turns out to be inadequate is a finding; an unnamed one is noise.
Record exactly one result for every approved criterion:
{
"id": "AC1",
"outcome": "pass",
"proofSeen": true,
"observed": "50 rows containing marker verify-<run> (was HTTP 500)",
"evidence": ["ac1-rows.json", "screens/ac1-result.png"]
}outcome is pass, fail, or could-not-run. observed describes only what
the command showed. proofSeen is true only when the criterion's declared
proof is quotable from the evidence; a pass without it renders "not proven"
and never counts toward the headline. Never set it optimistically.
evidence is an optional array of paths relative to the run folder. It is required by
the workflow for a hand-driven pass or fail: name at least one nonempty file that
backs the observation. Screenshots, logs, JSON, and text may live anywhere inside the run
folder; there is no folder-layout convention. A driven pass or fail is substantiated
only by its finalized drive attempt, so its evidence list is optional and may contain
extra screenshots or logs but can never replace the receipt trail. A could-not-run
result names no evidence and must put the concrete blocker in observed.
The renderer opens each named path, rejects absolute paths, symlinks, escapes from the run
folder, empty or non-regular files, the run's own report/input outputs, and reserved drive
attempt folders. Missing and rejected items stay visible on the card. The same valid file
may support several criteria, but the report calls out every reuse. When precheck taint
blocked a criterion, its existing prechecks/<part>.log attaches automatically.
If a useful regression test is generated, write it only to .verify/runs/<id>/tests/. Never put it in the source tree during verification. Preserve an empty tests/ directory when no test is generated.
Before rendering anything, verify the clean-repo promise — the violation flag must exist BEFORE the report renders, so no already-served PASS ever survives a detected mutation:
git diff HEAD > .verify/post-run.diff
git ls-files -o --exclude-standard | grep -v '^\.verify/' | while IFS= read -r f; do printf '%s %s\n' "$(git hash-object "$f" 2>/dev/null || echo missing)" "$f"; done > .verify/post-run-untracked.txt
if ! diff -q .verify/pre-run.diff .verify/post-run.diff > /dev/null \
|| ! diff -q .verify/pre-run-untracked.txt .verify/post-run-untracked.txt > /dev/null; then
touch "$RUN_DIR/clean-repo-violation"
echo "✗ verify modified your repo during the run — this is a verify bug."
fiThe flag makes the report headline read "CANNOT TRUST THIS RUN" and the exit status non-zero; the report still renders because the diff is exactly what the user needs to file the bug.
Write .verify/runs/<id>/results.json:
{
"results": [],
"coverage": { "filesWithoutCriterion": 0 },
"notChecked": []
}There must be one result per approved criterion. Set coverage.filesWithoutCriterion from the persisted coverage.json, not from memory. Include every skipped surface, permission denial, harness problem, recorder problem, and uncovered file in notChecked. Keep the list present even when empty.
Render:
TARGET_REPO="$(pwd -P)"
RUN_ID="$(cat "$TARGET_REPO/.verify/current-run")"
RUN_DIR="$TARGET_REPO/.verify/runs/$RUN_ID"
VERIFY_PIPELINE="${VERIFY_PIPELINE:-$CLAUDE_PLUGIN_ROOT/pipeline}"
(cd "$VERIFY_PIPELINE" && npx --no-install tsx src/cli.ts report \
--results "$RUN_DIR/results.json" --criteria "$RUN_DIR/criteria.json" \
--precheck "$RUN_DIR/precheck.json" --repo-root "$TARGET_REPO" \
--run-dir "$RUN_DIR") > "$RUN_DIR/report.md"The --precheck flag enforces taint mechanically. In run-directory mode the engine
reconciles results, applies receipted proofs, resolves evidence, applies taint, and then
classifies every criterion once as proven, failed, not-proven, or blocked.
Every output consumes that classification. A hand-driven pass/fail without valid named
evidence, or a driven pass/fail without a qualifying finalized attempt, renders not
proven. A could-not-run without a nonblank reason also renders not proven. Checks that
did not run never disappear, and PASS appears only when every criterion is proven.
Then render and serve the visual page:
(cd "$VERIFY_PIPELINE" && npx --no-install tsx src/cli.ts html \
--criteria "$RUN_DIR/criteria.json" --results "$RUN_DIR/results.json" \
--precheck "$RUN_DIR/precheck.json" --review "$RUN_DIR/review.json" \
--repo-root "$TARGET_REPO" --run-dir "$RUN_DIR" --run-id "$RUN_ID")Serve it so the user opens a browser, not a file path — kill the previous run's server first, fall back through ports, and confirm the new server actually answers before printing the URL:
[ -f .verify/server.pid ] && kill "$(cat .verify/server.pid)" 2>/dev/null || true
for PORT in 8123 8124 8125; do
python3 -m http.server "$PORT" --bind 127.0.0.1 --directory "$RUN_DIR" > /dev/null 2>&1 &
echo $! > .verify/server.pid
sleep 1
curl -sf "http://127.0.0.1:$PORT/report.html" -o /dev/null && { echo "Report: http://127.0.0.1:$PORT/report.html"; break; }
kill "$(cat .verify/server.pid)" 2>/dev/null || true
done(On a remote box, forward the port or bind explicitly if the user asks.)
Print report.md and the artifact paths. When recording succeeded, confirm run.cast
exists and contains real commands and output; when GIF rendering also succeeded, confirm
run.gif. Confirm criteria.json, criteria.md, claims.json, coverage.json,
results.json, report.md, report.html, and tests/ exist. Confirm the target
repository has no verification changes outside .verify/.
For each generated test, print its artifact path and the source-tree path where it would belong. Ask separately whether the user wants that test checked in. Do not move or commit any test without that explicit choice.
When a criterion fails and the user questions the verdict, offer the manual comparison — it settles "the change broke this" vs "the harness or spec is wrong" by running the OLD code through the identical checks:
VERIFY_SCRIPTS="${VERIFY_SCRIPTS:-$CLAUDE_PLUGIN_ROOT/scripts}"
bash "$VERIFY_SCRIPTS/compare.sh" up <merge-base>
# drive the chosen criteria in the printed worktree exactly as on the candidate
bash "$VERIFY_SCRIPTS/compare.sh" downThe base stack is separately seeded (setup contract, auth state, and the
reviewed seed script are carried over) and never reuses the candidate's
environment. Read the results side by side: fails on base too → not this
change's fault; fails only on the candidate → likely a regression; passes on
base too for an intent: changes criterion → it would have passed before the
change and proves nothing; base could not run → say exactly that, never
reinterpret. Never run this automatically.
After the report renders (never before — "never fix what you judge" survives because a regression test for a delivered verdict is not tampering):
$RUN_DIR/review.json. Criteria the reviewer marked codify: true
are candidates; a criterion that caught a real bug this run is an automatic
candidate.Cross-stage state (.verify/current-run, run-env.json, the setup contract)
is repo-global: run one verification per repository at a time. A second
concurrent run would repoint the shared state mid-run.
© opslane, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in skills/verify of opslane/verify.
Open the folder on GitHubat commit 25dc3a9
Verify next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Verify this skillopslane/verify | 115 | — | ~11k | Automated safety check: Notes | MIT | |
| User Story Writerdeanpeters/Product-Manager-Skills | 7.2k | 2 repos | ~2.9k | Automated safety check: Pass | Custom licence | |
| Ralph Tui Create Beadssubsy/ralph-tui | 2.5k | 1 repos | ~2.6k | Automated safety check: Pass | MIT | |
| Agile Product Owneralirezarezvani/claude-skills | 28k | 3 repos | ~3.2k | Automated safety check: Pass | MIT | |
| Ralph Tui Create Beads Rustsubsy/ralph-tui | 2.5k | 1 repos | ~2.8k | Automated safety check: Pass | MIT | |
| Ralph Tui Create JSONsubsy/ralph-tui | 2.5k | 1 repos | ~2.6k | Automated safety check: Pass | MIT |
deanpeters/Product-Manager-Skills
Writes user stories in Mike Cohn's format with Gherkin acceptance criteria, turning user needs into development-ready work with testable conditions.
subsy/ralph-tui
Convert PRDs to beads for ralph-tui execution. An agent skill from subsy/ralph-tui.
alirezarezvani/claude-skills
Writes INVEST-checked user stories with acceptance criteria, splits epics, plans sprints from velocity and ranks the backlog with a weighted score.
subsy/ralph-tui
Convert PRDs to beads for ralph-tui execution using beads-rust (br CLI).
subsy/ralph-tui
Convert PRDs to prd.json format for ralph-tui execution. An agent skill from subsy/ralph-tui.
VibiumDev/vibium
Independently check application acceptance criteria in a live browser or saved recording with the Vibium CLI.
opslane/verify
One-time setup for /verify and /break. An agent skill from opslane/verify.
opslane/verify
Try to break a change the way the real world will - network faults, restarts, two things at once, users doing things out of order - on a disposable local stack, and report what broke with a…
Categories
Verify any change surface against approved acceptance criteria, run the real system, and preserve the report and test artifacts. Verify is an agent skill from opslane/verify. Verify any change surface against approved acceptance criteria, run the real system, and preserve the report and test artifacts.
Verify fits situations like: tasks that involve User stories.
Run `npx skills add opslane/verify --skill verify -a claude-code`. Or copy the skill folder (skills/verify in opslane/verify) into .claude/skills/verify in your project. Claude Code loads it when a task matches its description.
Run `npx skills add opslane/verify --skill verify -a codex`. Or copy the skill folder (skills/verify in opslane/verify) into .agents/skills/verify in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add opslane/verify --skill verify -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/verify, .gemini/skills/verify, .github/skills/verify and .opencode/skills/verify in your project.
Going by SKILL.md and its folder, Verify needs the command-line tools its instructions call (bash, npx, git, jq, npm and go). Our summary lists: Node.js.
SKILL.md contains no URLs. Its commands use npx, git, npm and curl, which can reach the network depending on how they are called. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found notes only (mentions a .env file), nothing it rates as a warning. It is not a guarantee. Review the folder before installing.
Verify is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 11k tokens (SKILL.md is roughly 43k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Verify: User Story Writer (deanpeters/Product-Manager-Skills, 7.2k stars), Ralph Tui Create Beads (subsy/ralph-tui, 2.5k stars), Agile Product Owner (alirezarezvani/claude-skills, 28k stars) and Ralph Tui Create Beads Rust (subsy/ralph-tui, 2.5k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
opslane (a GitHub organization) maintains it in opslane/verify, which has 115 GitHub stars. The repository holds 3 skills in this directory. The repository was last updated on September 28, 2026.
Source: opslane/verify on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.