onspec
v1.2.0
Published
Specs that refuse to drift. Git-native verification layer for spec-driven development.
Downloads
449
Maintainers
Readme
onspec
Specs that refuse to drift. A git-native verification layer for spec-driven development — works with any generator (Claude Code, Cursor, Kiro, Spec Kit output, or humans).
Everyone builds the front half of spec-driven development: spec → generated code. onspec is the back half: proving the code stays true to the spec, PR after PR, hotfix after hotfix.
- Specs are files in the repo (
specs/*.spec.md) — versioned, diffed, and approved through normal PRs. - Every change is checked against its governing spec — each acceptance criterion gets a met / unmet / uncertain verdict with evidence.
- Drift is refused, not documented — hand-edits that no spec governs get flagged: write the spec or revert the code.
Quick start
npm install
npm run build
# Grade your specs for verifiability
onspec lint
# Check a change against its governing specs
npm run test:junit # any runner that emits JUnit XML works
onspec verify --base origin/main --test-results test-results.xml
# Flag unspecced changes
onspec drift --base origin/mainSpec format
Markdown with YAML frontmatter — human-writable, agent-writable, machine-checkable:
---
id: SPEC-0042
title: CSV export includes archived records
status: approved # draft | approved | superseded
refs: # optional: trace to your issue tracker
- PROJ-123 # Jira key, GitHub issue, RFC URL, ...
covers: # globs the spec governs
- src/export/**
criteria:
- id: C1
text: Archived records appear in exports when include_archived=true
verify: test # test | assertion | manual
evidence: tests/export.test.ts::includes archived records
- id: C2
text: Export format constant stays RFC 4180
verify: assertion # met iff the file contains the snippet
evidence: src/export/csv.ts#FORMAT = "RFC4180"
invariants:
- Export format remains RFC 4180 compliant
non_goals:
- Bulk archive/unarchive operations
---
Free-form context and rationale below the frontmatter.How verdicts are reached
Two layers, deterministic first:
- Deterministic anchors.
verify: testevidence (file::test name) resolves through your CI's JUnit report — the test result is the verdict, no LLM opinion involved.verify: assertionevidence (file#snippet) is met exactly when the file contains the snippet. - LLM only for the gap. Criteria without deterministic evidence get an LLM assessment of the diff (Claude, via your
ANTHROPIC_API_KEY). Every met/unmet verdict must citefile:lineevidence — uncited verdicts are downgraded to uncertain. No key → those criteria stay uncertain and say so.
verify: manual is allowed but visible: the report surfaces it every time, and onspec lint warns about it.
Data handling: what leaves your machine
Short version: nothing, unless you provide an API key — and then only the minimum, under your own account.
- No key set: onspec makes no network calls. All verdicts come from your repo and your test results. Criteria without deterministic evidence are reported as
uncertainand say why. - Key set (
ANTHROPIC_API_KEY): for each criterion that lacks deterministic evidence, onspec sends the Anthropic API: the git diff being verified, the governing spec's text (title, criterion, context body, non-goals), and nothing else. Requests go directly from your machine or CI runner to Anthropic under your key and your account's data terms; onspec has no server and never sees your code or credentials. - What is never sent: your full repository, test results, environment variables, or anything for criteria that resolved deterministically. A fully test-anchored spec suite verifies with zero LLM calls (this repo does).
onspec reverseis the one command that sends more when invoked with a key: the source and test files matched by your configured globs, since drafting specs requires reading the code. Prefer not to?onspec reverse --prompt-onlyprints the prompt for you to run through any agent you already trust, and--from-jsoningests the result; onspec then does all validation locally.- In CI, pass the key as a secret (see below). Omit it and the Action runs fully offline with
--no-llm.
Commands
| Command | What it does | Blocking? |
|---|---|---|
| onspec verify | Criterion-by-criterion conformance verdicts for the current diff | Advisory; --strict exits 1 on unmet |
| onspec drift | Flags changed code with no approved governing spec | Advisory; --strict exits 1 on findings |
| onspec lint | Readiness grade (A–F) per spec: ambiguity, dead globs, missing evidence | Advisory; --strict exits 1 on errors |
| onspec reverse | Reverse-generates draft specs from existing code + tests (brownfield on-ramp) | Writes status: draft only |
Common flags: --base <ref>, --head <ref>, --format markdown, --output <file>.
Config (optional onspec.config.json):
{ "specDir": "specs", "code": ["src/**"], "base": "HEAD~1" }code globs define what counts as "code that must be specced" for drift detection.
GitHub Action
- uses: Avant-Concepts-LLC/onspec@v1
with:
test-results: test-results.xml
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} # optional
strict: "false" # advisory first — earn the right to blockPosts a single, self-updating conformance comment on the PR and writes the report to the job summary. See .github/workflows/onspec.yml for the full example.
GitLab CI
include:
- remote: https://raw.githubusercontent.com/Avant-Concepts-LLC/onspec/v1/templates/onspec.gitlab-ci.ymlRuns on merge requests. Configure via CI/CD variables: ONSPEC_TEST_RESULTS (JUnit artifact path), ANTHROPIC_API_KEY (optional), ONSPEC_GITLAB_TOKEN (project access token with api scope, enables a self-updating MR note), ONSPEC_STRICT. See templates/onspec.gitlab-ci.yml.
Dogfood
This repo verifies itself: specs/ governs src/, and every criterion is anchored to this repo's own tests. Try the canonical demos:
# The 2 a.m. hotfix — an unspecced file
echo 'export const x = 1;' > src/hotfix.ts
onspec drift --base HEAD # ⚠ [unspecced-change] src/hotfix.ts …
# The broken PR — sabotage covered code, watch the criterion fail
# (edit src/drift.ts, run npm run test:junit, then:)
onspec verify --base HEAD --test-results test-results.xml
# ❌ unmet C2 A changed code file whose only coverage is a non-approved spec …Reverse-spec generation (brownfield on-ramp)
For codebases with no specs at all, onspec reverse recovers the implicit spec from existing code and tests:
onspec reverse # drafts via the Claude API (needs ANTHROPIC_API_KEY)
onspec reverse --prompt-only # print the drafting prompt to drive any agent by hand
onspec reverse --from-json out.json # ingest drafts an agent produced (draftsSchema shape)The LLM only drafts. Everything that must be true is enforced deterministically afterwards: criteria anchored to test names that don't actually exist are stripped and reported as test gaps, ids are assigned in sequence, and output is always status: draft — approval stays a human act in a reviewed PR. Validated against unjs/defu: 4 specs, 20 criteria, 19 anchored to existing tests, 0 hallucinated pointers admitted, and verify then scored the repo 19 met / 1 uncertain (a real test gap it surfaced).
Status
Weekend-scale MVP. Zero server-side state — everything derives from the repo. Roadmap: blast-radius tracing, autonomy policies (change-class → merge-gate mapping), and a verifier eval corpus.
