Specs that refuse
to drift.

onspec checks every code change against the spec that governs it. Verdicts come from tests and evidence, not vibes.

$ npm install -g onspec
View on npm
onspec verify · pull request #142
$ onspec verify --test-results results.xml

SPEC-0002  Prototype pollution resistance
  ✓ met       C1  Merging untrusted payloads cannot pollute Object.prototype
              test passed: should not override Object prototype
  ✓ met       C2  Inherited enumerable properties are never copied
              test passed: should ignore inherited enumerable properties

SPEC-0004  Plain-object detection
  ✗ unmet     C3  ESM namespace objects merge like ordinary objects
              test failed: works with asterisk-import
  ? uncertain C4  isPlainObject accepts [object Module] values
              no evidence pointer on criterion

Summary: 2 met · 1 unmet · 1 uncertain (4 criteria)

Every criterion gets a verdict. Every verdict cites its evidence.

Specs live in your repo as reviewable files. On every pull request, onspec finds the specs that govern the changed code and checks each acceptance criterion.

Deterministic evidence first

A criterion that names a test resolves through your CI's JUnit report. The test result is the verdict. Assertion evidence resolves by file content. No model involved, no opinion, same answer every run.

An LLM only for the gap

Criteria without hard evidence get a Claude assessment of the diff, and it must cite file:line or the verdict downgrades to uncertain. Falsifiable claims, never bare confidence.

Drift is refused, not documented

Changed code that no approved spec governs gets flagged: update the spec or revert the code. Documentation drifts silently. A source of truth refuses to.

How a change is verified.

One pull request, left to right: the spec that governs it, the diff and your test report, the verdict per criterion, and the report that decides whether it merges.

onspec · how a change is verified · v1.2
Specs that refuse to drift.
Left to right is the life of one pull request. Green verdicts come from tests and evidence, amber ones from a model that must cite its source, red is refused.
1 · The spec · a file in the repo
specs/csv-export.spec.md
idSPEC-0042
statusapproved
refsPROJ-123
coverssrc/export/**
criteria
C1verify: test
tests/export.test.ts::archived
C2verify: assertion
src/export/csv.ts#FORMAT = "RFC4180"
C3verify: manual
invariantsRFC 4180 stays
non_goalsbulk archive
draft→approved→superseded
Approval is a human act in a reviewed PR. Versioned and diffed like code.
onspec lint · readiness gradeA B C D F
manual criterion −10 · missing evidence · vague wording −3 per word · dead cover globs. A = no penalty, F above 45.
2 · The change
git diff base…head--base origin/main · any branch, hotfix or agent PR
JUnit XML from your runnernpm run test:junit · vitest, jest, pytest, anything
onspec.config.jsoncode: ["src/**"] = what must be specced
Governing specs = every approved spec whose covers glob matches a changed file.
Runs where the change is
GitHub Action @v1GitLab CI templatelocal CLI
Zero server-side state. Everything derives from the repo and the CI report.
3 · The verdict · deterministic first, a model only for the gap
For every criterion in every governing spec:
verify: testevidence file::test name resolves through the JUnit report. The test result is the verdict.
met · unmet
verify: assertionevidence file#snippet — met exactly when the file contains the snippet.
met · unmet
no deterministic evidencethe diff + the spec text go to Claude (claude-opus-5). Every verdict must cite file:line — uncited verdicts are downgraded.
met · unmet · uncertain
verify: manual · no keynever checked automatically; surfaced in every report and warned by lint. No API key → the model lane stays uncertain and says so.
uncertain
onspec drift · in parallel, every changed code file
unspecced-change — no spec covers it: write the spec or revert the code
stale-approval — only a draft covers it: someone is shipping against a draft
superseded-coverage — only a superseded spec covers it: approve a successor
4 · The report
✓ met    C1 archived records export
✓ met    C2 format constant RFC4180
? uncertain C3 verify: manual
⚠ unspecced-change src/hotfix.ts
2 met · 1 uncertain · 1 finding
terminalmarkdownjson
One self-updating PR commentGitHub Action posts it and writes the job summary; GitLab gets an MR note
--strict exits 1on unmet criteria or drift findings. Advisory by default — earn the right to block.
Drift is refused, not documented. The verdict is on the criterion, not the vibe of the PR.
dogfooded: this repo verifies itself
Brownfield on-ramp · onspec reverse · the model drafts, code validates, a human approves
code + testsmatched by your globs
Claude drafts specsor --prompt-only for any agent you trust
deterministic validationmissing test pointers stripped and reported · ids sequenced
status: draftalways. Approval stays a reviewed PR
What leaves your machine
No key: nothingno network calls; verdicts come from your repo and your test report
Key: the minimumthe diff and the governing spec's text, per unanchored criterion, under your account
Neverthe full repo, test results, env vars, or anything already decided deterministically
covers ∩ diff
diff · JUnit
verdicts · findings
a draft spec lands in specs/ for review
the PR merges only when the report says so
onspec lint
grade the specs A–F
onspec verify --base origin/main --test-results test-results.xml
criterion verdicts for the diff
onspec drift --base origin/main
changed code with no approved spec
onspec reverse
draft specs from code + tests
deterministic — tests and evidence decidea model decides, with a cited file:linerefused — unmet or drifta file in your repo

Built for the 2 a.m. hotfix.

Someone patches production directly. Next CI run, the spec notices.

The patch ships tonight. But it never becomes invisible: the repo stays in a state where spec and code visibly disagree until a human reconciles them.

$ onspec drift --base origin/main

⚠ [unspecced-change] src/export/csv.ts changed
  but no spec covers it. Write the spec or
  revert the change.

1 drift finding

A spec is a file in your repo.

Markdown with YAML frontmatter. Humans review it in a normal pull request. Agents read it as a work order. onspec checks it forever after. Non-engineers draft them with any chat assistant.

# specs/csv-export.spec.md
id: SPEC-0042
title: CSV export includes archived records
status: approved
covers:
  - src/export/**
criteria:
  - id: C1
    text: Archived records appear when include_archived=true
    verify: test
    evidence: tests/export.test.ts::includes archived records
  - id: C2
    text: Export format constant stays RFC 4180
    verify: assertion
    evidence: src/export/csv.ts#FORMAT = "RFC4180"
non_goals:
  - Bulk archive operations

covers maps specs to code

Globs decide which spec governs a change. That mapping powers both conformance checks and drift detection.

evidence makes criteria checkable

Each criterion names its own proof: a test, a code assertion, or an honest manual check that the report will keep surfacing.

status is the human gate

Only approved specs govern code. Approval happens by editing the file in a reviewed pull request, nowhere else.

Four commands. Zero infrastructure.

A CLI that runs locally and in any CI. Everything derives from the repo: no server, no account, no state anywhere else. No API key, no network calls; with a key, only the diff leaves, under your own account.

onspec verify

Criterion-by-criterion conformance verdicts for the current diff, posted as one PR comment.

onspec drift

Flags changed code that no approved spec governs. Advisory by default, blocking when you have earned it.

onspec lint

Grades each spec A to F on verifiability: ambiguous wording, dead globs, missing evidence.

The other half of spec-driven development.

Plan-to-code is the new shape of software work: anyone who can state intent precisely writes the spec, and agents deliver the feature. A crowded field of tools already handles the front half, turning specs into code.

onspec is the back half: proof that the delivery matches the plan, on the day it ships and after every hotfix that follows. Generation made plans cheap to execute. Verification is what makes them safe to trust.

And the two halves close into a loop, because an approved spec with unmet criteria is a machine-readable work order: spec merges, an agent implements, onspec verifies the PR, humans approve the delivery. The spec catalog is the backlog. Nobody maintains a second list.

In your CI in one commit.

Write one spec

Or point onspec reverse at your existing code and review its drafts.

Add the workflow

The Action posts a self-updating conformance comment on every pull request.

Stay advisory until trusted

Default exit code is 0. Flip strict when the team wants the gate.

# .github/workflows/onspec.yml
on: pull_request
jobs:
  onspec:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run test:junit
      - uses: Avant-Concepts-LLC/onspec@v1
        with:
          test-results: test-results.xml