@am_shork/attest
v1.6.0
Published
TDD-native spec framework: tests are the source of truth for verification, ID-bound requirements the source of truth for intent.
Maintainers
Readme
A TDD-native spec framework. Tests are the source of truth for verification; ID-bound requirements are the source of truth for intent. Attest binds the two by a stable ID and continuously detects drift.
- Coverage — does every requirement have ≥1 scenario? (statically, or from the run)
- Result — is every test green? (from the test runner)
- Drift — do intent and assertions still agree? (static + runtime cross-check)
The killer move against drift: values a requirement promises (timeouts,
limits, budgets) live once in its params, and tests read them from there —
so a number is physically impossible to drift between the spec and the assertion.
Values that merely tune behaviour stay ordinary constants; nothing is owed to
anyone when a tuning knob changes. A param may be any JSON value, so a
composite constant — a vendor blacklist, a kind -> payload table — gets the
same single source as a lone number, which is where drift is worst.
Why this shape, when an agent is writing the code
Everything above holds whoever the author is. Three of the properties change character when the author is not a person, and they are the reason the framework is shaped this way rather than as a linter:
- Intent outlives the context window. A requirement is a file with a stable id, not a paragraph in a conversation. A summarised or truncated context cannot drop it, and the next session reads the same one — so "what did we agree this should do" is answered by the tree rather than by recall.
- "Done" is not the author's to declare.
attest archiveis the definition of done, and green tests do not clear it. Every scenario of a requirement the change adds must have been observed failing at least once — recorded in a file the gate trusts and cannot regenerate from a green tree. An assertion that never discriminated is the cheapest way to finish a task, and it is the one thing this gate exists to refuse. - Every diagnostic is a next action. Issues carry a stable
code, a link to the section explaining that code, and a file and line wherever the finding has a location — on stdout and in--jsonalike. Consumers branch oncode, never on wording, so a report is something to act on rather than something to interpret, and rewording a message breaks nobody's CI.
None of that asks you to hand the work over. It is the same gate whether a person or an agent is on the other side of it, which is the point: the evidence a reviewer reads does not depend on who wrote the code. Working with an agent is the setup, and it is one command.
Prerequisites
- Node ≥ 20.19
- pnpm, plus
vitest≥ 4 andvite≥ 8 (peer dependencies)
The peer range is exactly the one CI runs, which is the point of it. Older majors
are not merely untested: verify relies on a startVitest signature that Vitest
narrowed by 4, and ATX-36 — reading a registry opens no listening socket — is
asserted against the Vite that is installed, so on any other major it is a claim
rather than a measurement. If you are on Vitest 2 or 3, stay on 0.3.x.
The TypeScript compiler is a bundled dependency, not a peer — Attest reads
your registries and specs through the compiler API, and typescript@7 no longer
exposes one (its AST moved behind typescript/unstable/*). So the supported
range is ^5.5.0 || ^6.0.0, both ends run in CI, and your own compiler is not
involved: on a project already using TypeScript 5 or 6 the two resolve to one
copy, and on TypeScript 7 you will simply have a second one that only Attest
uses. Your project's TypeScript version is yours to choose either way.
Getting started
Install (the framework plus its vitest + vite peers):
pnpm add -D @am_shork/attest vitest vite1. Declare a requirement (requirements/auth.reqs.ts) — import the intent API
from the vitest-free /define subpath so registry loading never touches the
runtime. A registry is a literal: every value is written where you can read
it, because the commands below read it without executing it
(why).
import { defineRequirements } from '@am_shork/attest/define';
export default defineRequirements({
'AUTH-3': {
statement:
'The system SHALL expire a session after {idleTimeoutMin} minutes of inactivity.',
rationale: 'Security: limit the exposure window of an unattended session.',
params: { idleTimeoutMin: 30 }, // the single source for this number
},
});2. Attest it with scenarios (session.spec.ts):
import { expect } from 'vitest';
import { requirement, scenario } from '@am_shork/attest';
import reqs from './requirements/auth.reqs.js';
import { createSession, advance, touch, isValid } from './session.js';
requirement('AUTH-3', () => {
scenario('idle timeout invalidates the session', () => {
const t = reqs['AUTH-3'].params.idleTimeoutMin; // single source, typed `30` — no cast
const s = createSession();
advance(s, t + 1, 'minutes');
expect(isValid(s, t)).toBe(false);
});
scenario('activity resets the idle timer', () => {
const t = reqs['AUTH-3'].params.idleTimeoutMin;
const s = createSession();
advance(s, t - 1, 'minutes');
touch(s);
advance(s, t - 1, 'minutes');
expect(isValid(s, t)).toBe(true);
});
});Both files above are quoted from fixtures/consumer/, which the packaging test
installs from a real tarball and runs — a test asserts the quotes are byte-equal
to the files, so a sample the engine would now reject cannot survive here.
3. Run the engine:
attest check # static: orphan tests, uncovered requirements, unbound params
# (reads your registry; runs none of your code)
attest verify # run tests + coverage + drift, graded report
# (runs only the spec files that declare a requirement())
attest cover # which requirements lack a scenario
attest render # the requirements as Markdown, for people who don't read TS
attest archive <change> # gate a proposed change: green + covered + no drift
attest status <change> # per added id: scenario written? seen red? (part of that
# gate, without running anything — never a verdict)Every command takes the project root as an optional last argument, and --json
for exactly one machine-readable document on stdout. Flags, per-command
behaviour and the JSON shape are in the
CLI reference.
Reading a param is necessary and not sufficient — an assertion that recomputes its expectation from the same param the code just read has no independent term, and a scenario that loops over a list param covers exactly that list. Both have a known shape and a known repair, and both are in Judging your own intent layer, along with the decision table — the shape a composite param is best at.
Working with an agent
The engine above is only half the framework. The other half is the workflow — agree on intent, write the delta, drive the scenarios red, then implement to green — and it is written for an agent to follow:
attest init # .claude/skills/attest/SKILL.md
attest init --target cursor # .cursor/rules/attest.mdc
attest init --target copilot # .github/instructions/attest.instructions.md
attest init --target agents # .agents/skills/attest/SKILL.md — Codex, Gemini CLI, …"Every scenario has been seen to fail" is enforced rather than advised:
archive records how each of a change's scenarios ended in every run it
observes, into changes/<name>/first-run.json, and blocks with never-red on
any requirement the delta adds whose scenarios were never seen to fail. Commit
first-run.json with the change — it is the evidence, and CI has to reach the
same verdict as you do.
It does not require you to write the test first. A recorded failure is
permanent and a recorded pass is not, so a failure observed after the
implementation exists satisfies the gate exactly like one observed before it:
if you wrote the intent, the scenario and the code together, remove the
implementation, run archive, and put it back. What is enforced is that the
assertion can fail — not the order you worked in.
Before any of that, in Claude Code, there is a plugin. This repository is its own plugin marketplace, and what it hosts is a setup helper — not a second copy of the workflow:
/plugin marketplace add https://gitlab.com/Pseudorca/attest.git
/plugin install attest-setup@attestIt knows what Attest is, what to install, and to run attest init — and then it
says so and stops. The workflow is deliberately not shipped as a plugin. A
plugin installs per-user, so a teammate who does not have it would see nothing
and no file in the repository would record which workflow was followed. The
document init writes is committed to your project, which is the whole point of
writing it there.
An agent loads the document on its own — its description is already in the
agent's context, or its path matches what you have open — so nothing has to be
found or pasted. See
attest init
for the targets and what init deliberately does not write.
When something goes wrong
Every diagnostic carries a code, and every code has a section in
Troubleshooting — which the diagnostic itself links to:
ERROR registry-not-static (requirements/upload.reqs.ts:5)
Value is not a literal.
→ https://gitlab.com/Pseudorca/attest/-/blob/v1.6.0/docs/en/troubleshooting.md#registry-not-staticThe anchor is the code, so the link cannot point somewhere the section
isn't. In --json the same link is on each issue as docsUrl.
Documentation
| Document | English | 中文 | |---|---|---| | CLI reference — every command, flag and JSON field | en | 中文 | | Troubleshooting — one section per issue code | en | 中文 | | Design — the authoritative design of the framework | en | 中文 | | Judging your own intent layer — what no gate checks, and a method for it | en | 中文 | | Feedback template — report how adoption actually went | en | 中文 |
The change workflow is not here: it is what attest init writes into your
project, as a skill, a rule or an instructions file, depending on which agent
reads it.
Development
pnpm install
pnpm build # tsc -> dist/
pnpm test # vitest run (the framework's own unit tests)
pnpm typecheck
pnpm lintDogfooding
Attest describes its own behaviour under self/ and verifies itself:
pnpm verify:self # attest verify self -> all greenPackaging test
pnpm test:consumer packs a real tarball, npm installs it into a throwaway
project outside the repo, and drives the installed CLI against
fixtures/consumer/. It is the only test that exercises
the published surface — the files allowlist, the exports map, the bin
launcher, peer resolution from a foreign node_modules — so it is what catches
"green in-repo, broken once installed" bugs. It needs network and is excluded
from pnpm test; it runs in CI and as prepublishOnly.
Feedback
Adoption reports drive the roadmap. After using Attest on a real project, fill in the feedback template (linked above) and open it as an issue — it is also the Usage feedback issue template. It covers greenfield and mid-project adoption alike, and ends with a prompt to run inside the adopting repo that fills most of the report automatically and probes for problems you haven't hit yet. The highest-value part is false negatives: drift the engine should have caught and didn't.
Acknowledgments
Attest's architecture — the four-stage truth engine (AST parse → graded
validation → diff-first delta apply → archive gate) and its diff-first change
model — is adapted from OpenSpec (MIT),
swapping two parts: parsing moves to the TypeScript Compiler API, and verify
becomes running the tests. The design is re-implemented from scratch; no
OpenSpec source code is included.
