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.
Supervised workers
Roles describe responsibility, not model brands. Record the actual supervisor, worker and harness for each dispatch.
judgevet has three verified worker harnesses: pi, through the delegate-to-pi
skill; Claude Code sub agents, through the Agent tool and the definitions in
.claude/agents/; and the Cursor CLI in print mode, guarded by
.cursor/cli.json.
The Codex CLI has a launch recipe in scripts/harness_build.sh, and the
delegate-to-harness skill routes a mechanical slice to Cursor or Codex by
quota headroom. Codex is unverified until a run lands.
- The supervising session selects work, decides boundaries, accepts the result and commits.
- A worker follows its brief and skips session bookkeeping.
- A worker never commits, pushes, edits the verification ledger or changes policy unless the brief assigns that action.
- Each checkout has one writer. Workers preserve unrelated changes.
Use the delegation procedure for sizing, acceptance, repairs and the brief. Use the worker run contract for launch evidence and commit trailers.
Bounded execution
The supervisor owns the cost of the whole assignment, including workers and repeated context. Explicit user scope and required gates still govern.
- Define done first. State the decision, necessary evidence, allowed repairs and stopping condition on the existing issue. Keep it under 150 words. Link existing specifications instead of rewriting them.
- Require a reason for each action. Advance the decision, repair a demonstrated blocker, or satisfy a required gate. Skip actions that serve none of these.
- Bound delegation. Default to one implementation dispatch, one independent acceptance review and one repair dispatch per behaviour. Before exceeding these limits, report the unresolved assertion and why another dispatch could resolve it.
- Use one validation path. The gate table and the hooks are that path. Do not add a second review pipeline. Reuse passing checks for unchanged inputs under the validation ownership rules. Assign one mechanical validation owner. Do not duplicate hook commands by hand.
- Keep delivery small. Use the existing issue and one commit. Create no extra report or dashboard unless the deliverable requires it.
- Stop at the agreed outcome. Report the result and remaining evidence gaps. Do not turn a worker outage or harness failure into another project. Report unavailable usage counters as unknown.
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.
The verification ledger carries the
line-by-line table. Moving a row 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. scripts/check_loc.py counts code lines under src, excluding
comments and docstrings. A module at 300 passes and 301 fails. The same
script fails a function whose body passes 50 code lines; a nested function
counts toward its parent and is also checked on its own.
- 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. The assigned validation owner
covers the whole inventory through the complete hook stages before acceptance.
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 |
| size | uv run python scripts/check_loc.py src |
| docs | uv run docvet check |
| tests | uv run pytest -q -n auto --cov |
| hooks | uv run pre-commit run --files <changed files> then uv run pre-commit run --hook-stage pre-push --files <changed files> |
The table names gates, not a second sequence to run beside the hooks. The assigned owner runs the complete commit and push stages once per relevant input snapshot. Required hooks still run at actual commit and push. See validation ownership for reuse, freshness and repair rules.
.pre-commit-config.yaml is the complete gate inventory. The hooks row runs
the checks the other rows omit: plain-english, terminology, docs-build and
doc-example-inventory at the commit stage, then doc-python-examples,
doc-example-schemas and dependency-audit at the push stage.
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, e2e and live.
live touches the real API and is excluded from the default run. e2e drives
a real entry point: the installed console script or the MCP server over stdio.
It is orthogonal to live. The default run includes offline e2e tests.
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 PostToolUse hook in .claude/settings.json runs scripts/vet_file.sh
after every Write, Edit or Bash call. It runs ruff format, ruff check and
docvet on each changed Python file, plus check_loc on files under src,
and returns their findings as context. A clean file returns nothing, so
silence means those gates are green for the files touched. Do not spend tool calls re-running them by hand; read what
the hook reports. ty, lint-imports and pytest do not run in this hook.
They run in the pre-commit hook and the gate table above. The whole table is
still the report of record.
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. Workers return their assigned deliverable and stop. The supervisor lands each accepted slice and continues the authorized multi-slice goal until its agreed stopping condition.
- Edit the verification ledger only when a call changes a claim. The
ledger records what a call has verified against the live service. Test
counts, coverage and gate state come from the gate run, not a file. The
retired
STATUS.mdis frozen underdocs/history/; do not edit it. - 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.
Claude sub agents carry their own trailers. A commit a Claude sub agent
wrote carries Generated-By: <resolved model id> (via Claude Code Agent tool,
<agent name>). A specification it wrote carries Specified-By: <resolved model
id> (via Claude Code Agent tool, specifier). The resolved model ID comes from
the agent's return, never from the requested alias. If the agent does not
report it, record unknown. The supervisor's own commits carry no
Generated-By trailer.
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, or receives the exact issue-hosted contract
through the restricted-worker evidence packet.
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.