onspec checks every code change against the spec that governs it. Verdicts come from tests and evidence, not vibes.
$ 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)
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.
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.
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.
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.
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
Markdown with YAML frontmatter. Humans review it in a normal pull request. Agents read it as a work order. onspec checks it forever after.
# 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 codeGlobs decide which spec governs a change. That mapping powers both conformance checks and drift detection.
evidence makes criteria checkableEach 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 gateOnly approved specs govern code. Approval happens by editing the file in a reviewed pull request, nowhere else.
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.
The brownfield on-ramp. Reverse-generates draft specs from existing code and tests, anchoring every criterion it can to a test that already exists. Hallucinated evidence pointers are stripped before they ever reach a file.
$ onspec reverse ✓ wrote specs/defaults-merging-semantics.spec.md ✓ wrote specs/prototype-pollution-resistance.spec.md 4 draft specs, 20 criteria, 19 anchored to existing tests, 0 hallucinated pointers admitted
Criterion-by-criterion conformance verdicts for the current diff, posted as one PR comment.
Flags changed code that no approved spec governs. Advisory by default, blocking when you have earned it.
Grades each spec A to F on verifiability: ambiguous wording, dead globs, missing evidence.
Or point onspec reverse at your existing code and review its drafts.
The Action posts a self-updating conformance comment on every pull request.
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: bensubercaseaux/onspec@main with: test-results: test-results.xml