CLAUDE.md
Guidance for coding agents in this repository. AGENTS.md is the same file.
What this repo is
judgevet is a typed client for TypeSafe's Jev (System One) judgment model, plus two inbound adapters over it: a CLI and an MCP server.
The library is the artifact. Scripts, pre-commit hooks and CI can call the typed library directly. The CLI and MCP server are two inbound adapters over one contract-tested core. The official TypeSafe Python SDK also provides synchronous and asynchronous clients.
Conventions are inherited from the sister projects automarket, vramfit and docvet.
Trust levels
Docs pages carry status: sketch | draft | stable. Do not write code against a
sketch page without flagging it. Promote a page in the change that lands the
code proving it.
Non-negotiables
- The domain model is verified in part, and only in part. This rule used
to read "no call has been made". Calls have now been made: a
live-marked test passes against the real API, and three probe calls on 2026-09-21 settled the success shape and the 401 and 422 error bodies.README.mdanddocs/reference/api.mdmoved fromsketchtodrafton that evidence, for those parts only.
What the rule becomes, rather than disappears: promote a claim only when a
call has exercised it. The 429 and 529 bodies are unseen — one needs
abusing the service, the other cannot be provoked — every model other than
jev-1.13.0 is untested, and any field no call touched is still inference.
STATUS.md carries the line-by-line table, and moving a line from inferred
to verified without a call that did it is the one change this repo will not
accept.
Nothing reaches stable while a documented status code has never been
seen.
- No API detail without a citation. A field name, an endpoint or a status
code carries the URL it came from, in the docstring or the docs page. An
uncited detail is an invention.
- Sources disagree, and that is reportable. When two pages describe the
response differently, record both and mark it an open question. Do not pick
one silently.
- The domain is pure. No IO, no serialization, no HTTP, no CLI or server
framework. import-linter enforces it.
- MCP stays in its adapter. mcp is an optional extra so the library and
CLI install without an MCP runtime. An import-linter contract forbids it
everywhere else. If that contract fails, the import is in the wrong layer —
move the code, never weaken the contract.
- Modules cap at 300 code lines, functions at 50. Over the limit means
decompose.
- Never silence a gate. Fix the cause. Do not add per-file-ignores,
# noqa, # type: ignore, --no-verify, or a narrowed scope. If a type
checker rejects a test double, the fix is a better double — a small class
that satisfies the protocol — not a cast to Any.
- Fixing one gate must not break another. Run the whole table before you
report. Adding a docstring to satisfy ruff D earns a docvet enrichment
finding unless it carries the Args:, Returns:, Raises: and
Attributes: sections the case needs.
- Every new module needs its docstring sections on the first pass. docvet
fails on enrichment, so a module docstring needs an Examples: block with
runnable code and a See Also: list of [judgevet.module.path][]
cross-references. A class needs Attributes:, a function that returns needs
Returns:, one that raises needs Raises:. Writing them afterwards has cost
three extra sessions already.
Architecture
Hexagonal, enforced by import-linter:
adapters/inbound/ cli.py (typer) · mcp.py (stdio server)
adapters/outbound/ http.py — the Jev API
ports/ SystemOnePort protocol
domain/ Noul · Choice · Score · their answers · Usage — pure
Both inbound adapters take a SystemOnePort. Neither constructs an HTTP client
itself. That is what makes the contract test meaningful and the MCP server
testable without a network.
Build and gates
uv sync # library and CLI
uv sync --extra mcp # adds the MCP runtime
uv run pre-commit install
uv run judgevet --help
| gate | command |
|---|---|
| lint | uv run ruff check . |
| format | uv run ruff format --check . |
| types | uv run ty check |
| layers | uv run lint-imports |
| docs | uv run docvet check |
| tests | uv run pytest -q --cov |
docvet runs in two modes: the pre-commit hook vets only the files a commit
touches (uv run docvet check, diff mode), and the pre-push hook vets the
whole repo (uv run docvet check --all), so commits never fail on doc debt
they did not introduce and no stale docstring can leave the branch.
Coverage floor is 90. Test markers are unit, contract and live; live
touches the real API and is excluded from the default run.
A contract test proves a fake port and the real adapter agree on the same
fixtures. When the outbound adapter changes, that test is the one that must
still pass.
Gates run themselves
A turn-end hook runs these gates after any turn that edits Python and reports only failures. Silence means green. Do not spend tool calls re-running them by hand; read what the hook reports. A gate slower than two seconds runs every fifth turn rather than every turn.
A tool that fails to spawn is a failure, not an environment detail — it means a tool is configured but not installed, and installing it is part of the work.
A summary is not evidence
Never report work complete while a gate is red. Report what is red and say why you think it should be accepted. That is a conversation you can win; a false "complete" is one you cannot.
This has happened. A session reported "Implementation complete" with five
properties ticked while check_suppressions.py exited 1, having added four
per-file-ignores codes and edited that gate's own test to assert the
inflated number. The work underneath was correct. The claim was not.
A green gate table is not an audit either. Gates catch the mechanical class. They cannot see:
- a test that passes whether or not the behaviour under test happens — a bare
try/exceptaround a call that must raise, where nothing raised means the handler never runs; - a helper that raises where the specification said return, which makes its own return annotation false and the call site unreachable;
- an unwrapped secret bound to a local, which
--showlocalsprints on any failure.
All three shipped with every gate green. #94 and #95 exist to convert the mechanically decidable half into gates; the rest is read by eye.
Prove a property with a command, not a sentence. "Confirm that X" is answered by a claim. A command whose output either exhibits the property or does not is answered by the world. For a test, the command is: break the precondition and show it go red. A test that cannot be made to fail is not evidence.
Vocabulary
Use the canonical glossary for definitions and preferred wording. Its vocabulary table is the single source for terminology. Preserve exact public identifiers, wire fields and quoted diagnostics. Apply prose preferences in their stated context; do not rename supported APIs.
Writing system
Short declarative sentences. Active voice, name the actor. One instruction per sentence. No hedging stacks, no marketing adjectives (seamless, robust, powerful, blazing). State a fact or write an explicit Open question.
Applies to docstrings, docs pages, error messages, CLI help and commit messages.
Working rounds
- Behavioral changes start red. Write the acceptance test first and record its failing command and output on the issue. Verify it fails for the missing behavior, not a broken fixture. Give the coder that exact test contract, then require green gates and an independent failure proof. Do not split a production-only change from tests promised in a later round.
- One deliverable per round. Land one thing, run the gates, stop.
STATUS.mdis rewritten in the commit that changes what it says. It names the test count, the coverage figure, the gate state and what is verified against the live service. A commit that moves any of those and leaves STATUS alone publishes a claim the next session has to discover is false. automarket lost three sessions to exactly that and gated it; see #28.- Read a file once. Use
git diffbetween reads rather than re-reading. - Never poll. Wait on a watcher, not a loop of empty checks.
- Do not write summary documents.
docs/holdsreference/api.mdand the Diátaxis tree. The README is the only summary. NoPROJECT_SUMMARY.md, noFINAL_REPORT.md.
Commits
Conventional Commits 1.0.0. The type comes from the closed vocabulary
(feat, fix, docs, refactor, test, chore, perf, build, ci).
A commit that finishes an issue closes it from the footer. Write
Closes #N — spec rule 8's <space># separator — and GitHub closes the issue
on push and leaves a permanent link from the issue to the commit. Do not close
issues by hand with gh issue close: a hand-written "landed in Refs #N for an issue the commit touches but does not finish.
Footers follow the git trailer convention the spec is built on: the token
takes - in place of whitespace (Generated-By, Co-Authored-By), and a
value may wrap onto further lines because parsing only stops at the next
token: or token # pair.
Attribution is per-commit and factual. A commit a local model wrote carries
Generated-By: <model> (local, via pi), with a trailing clause naming any part
of it that someone else wrote. A commit written by hand carries no such
trailer — the trailer is evidence, and this repo is partly an evaluation of
those models, so decorating an unearned commit corrupts the record.
Two models, two trailers. When a reasoning model wrote the specification and a coder implemented it, both are named and the roles are not merged:
Specified-By: Qwen3.8-27B-UD-Q4_K_M (local, via pi, --thinking medium)
Generated-By: Qwen3-Coder-Next-UD-IQ4_XS (local, via pi)
Specified-By means that model produced the definition of ready and done the
coder worked from — not that it wrote code. Collapsing the two into one
trailer would destroy the only comparison that makes the pipeline falsifiable:
whether a refined specification changes what the coder lands. The same
distinction is recorded as spec_refined in the delegation log, so the commit
history and the log agree.
A commit with no Specified-By went to the coder from an issue and a prompt.
That absence is data too, so do not add the trailer to make a commit look more
rigorous than it was.
The specification lives on the issue, not on disk. A reasoning pass posts
its definition of ready and done as an issue comment, bylined with the model
that wrote it, before any coder sees it. The coder then reads it with
gh issue view <N> --comments. This is not bookkeeping: a local scratch file
is invisible to review, dies with the machine, and has already been deleted
twice — once by a crashing session and once by the coder itself. Keeping the
original issue body intact beside the comment also shows what the reasoning
pass added, which is the comparison being measured.
Never destroy work you did not create
A delegated session does not run git checkout, git restore, git reset,
git stash, git clean, or rm -rf against any path, and does not modify a
file its task did not name. Unrelated modifications in the tree belong to
someone else; leave them and mention them in the summary.
This is not hypothetical. A session working one issue ran
git checkout CLAUDE.md && rm -rf scratchpad/ after inspecting the diff and
the git log of a file that was not its concern, destroying an uncommitted
change and a specification. Every gate here examines files that were
changed; nothing notices a file that was reverted, because a revert leaves
no diff and a clean-looking tree. See #82.
Default branch is main. The repo is public. No pull requests — commit a
finished piece of work directly to main and push. The pre-commit and pre-push
hooks are the only gate between a change and the branch, so
uv run pre-commit install --install-hooks -t pre-commit -t pre-push -t commit-msg
is the
first thing a clone does. Never pass --no-verify.
Do not describe work as complete while any gate is red. If a requirement was not met, name it.