- Shell 60%
- Rust 29.6%
- Just 10.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .claude | ||
| docs/harness | ||
| example | ||
| scripts | ||
| specs | ||
| tests | ||
| .gitignore | ||
| .typos.toml | ||
| CLAUDE.md | ||
| gate.just | ||
| justfile | ||
| README.md | ||
A harness for spec- and quality-driven work with Claude Code
Coding guidelines that only live in a document get politely acknowledged by the agent and then violated. So every rule here is bound to an executable check: a lint, a structure check, or a review entry. All of them run under one command, and what that command cannot check does not exist as a rule.
Not using Rust? Read docs/harness/concept.md first. The idea is not
language-specific but the tooling in this repository entirely is. That document
is what to hand someone working in another language.
What is in here
This repository is the harness. It contains no production code of its own.
docs/harness/ the rule set, and the reasoning behind it
concept.md what a harness is, language-independent
rust-style.md RS-xx coding rules
architecture.md AR-xx structure rules
language.md RS-70: this repository is English
review-checklist.md RC-xx: what only a manual review can find
spec-guide.md requirement format and traceability
workflow.md the process, and who decides what
scripts/ the checks themselves
harness-lib.sh path and configuration resolution
arch-check.sh layer edges plus forbidden symbols
spec-trace.sh requirements against tests, both directions
language-check.sh RS-70, this repository's own rule
rules-check.sh the rule tables against each other
suite.sh runs tests/, enforces fixture hygiene and budget
install-global.sh agents and commands into ~/.claude
german-words.txt word list, shared by both language checks
git-hooks/commit-msg RS-72: English commit messages
tests/ the behavioural self-test suite (bats): every check
proven red on a violation, traced via # covers:
.claude/ agents, slash commands, hooks
justfile checks the harness itself, and drives example/
example/ a complete project that uses the harness
Cargo.toml its own [workspace.lints]
arch.json its own layers
clippy.toml rustfmt.toml .typos.toml
justfile the project gate, and what a real one looks like
specs/ its requirements
crates/ domain -> app -> adapters -> cli
The split follows one question: who owns the file?
- Configuration is project-specific.
arch.json,clippy.tomlandCargo.toml [lints]belong to each project separately. - Tooling is copied in.
scripts/,docs/harness/and.claude/go into the project, so a clone has a working gate without anything else installed.
That makes example/ the integration test. Its gate is green while its scripts
come from the parent directory, which is exactly what happens when you move the
harness into a project of your own.
The three kinds of check
| Tool | Covers | Cost per rule | |
|---|---|---|---|
| Lints | Clippy and rustc, configured in Cargo.toml [lints] |
around 70 % of style rules | one line |
| Structure | Cargo crate boundaries plus arch.json |
architecture, determinism, dependencies | one line or one JSON block |
| Review | harness-auditor against review-checklist.md |
the remainder, unreliably | one paragraph |
The share of review rules is the harness's outstanding debt. /harness-gap is
the command that pays it down.
What Rust needs no extra tooling for: Clippy ships more than 700 lints.
The layer check emerges from the workspace structure, because Cargo cannot
build a dependency from an inner crate to an outer one unless it is
written in a Cargo.toml. arch-check.sh only verifies that nobody adds
the edge afterwards.
The harness is committed with the project
A project carries its rules, its checks and its documentation. just verify in
a fresh clone needs nothing but the usual Rust toolchain, which is what makes a
green gate an honest statement: whatever your machine happens to have installed
cannot change the answer, and CI runs the same thing you ran.
The price is that a fix in a check reaches other projects only when they update. That is visible drift rather than silent divergence, which is the better of the two.
./scripts/install-global.sh --with-hooks # agents and commands into ~/.claude
The agents and slash commands can also be installed globally, which is
convenient while working on the harness itself. A project that carries the
harness gets its own copy in .claude/.
Getting started
just verify # the harness itself, then example/'s own gate
just example # only example/, as a real project would run it
just example demo # watch the retry backoff grow until the attempts run out
just harness # what the rule set currently covers
just setup # optional tooling: nextest, deny, mutants, llvm-cov, machete, typos
Moving this into an existing project of your own
Not all at once. The order matters, because step 2 can produce hundreds of hits in a grown codebase.
-
Copy the tooling:
scripts/,docs/harness/,.claude/. Then copyexample/'s configuration into your project root:arch.json,clippy.toml,justfile, and the[workspace.lints]block.rustfmt.toml,.typos.toml,rust-toolchain.tomlanddeny.tomlare decisions of your own; leaving them out is a legitimate answer. Replace the layers inarch.jsonwith your own crates, initially with the existing edges so the check starts green.clippy.tomland[workspace.lints]belong together: theallow-*-in-testsentries are what keepunwrap_used = "deny"from making every test unwritable. -
Lints in stages. First put
[workspace.lints]on"warn"forallandpedantic, then count:cargo clippy --workspace --all-targets 2>&1 | grep -c '^warning'Raise individual lints to
"deny"once their hit count reaches zero. A harness that shows 400 errors the moment you switch it on gets switched off. -
Sharpen
arch.json. Remove the edges you no longer want frommay_depend_on, together with the refactoring. -
Hooks last. The
Stophook is convenient once the gate finishes in seconds, and a nuisance when it takes minutes. -
A language rule is yours to add. This repository keeps itself English with
scripts/language-check.sh. Wire it into your gate and adapt the word list if you want the same rule, or leave it out ofverify.
The example
A retry policy with exponential backoff: validated parameters, a ceiling on the
delay, retries driven through a Clock port, and the policy loaded from a text
file. Four requirements, 24 acceptance criteria, 25 tests.
It exists so the checks have something to bite on, and it earned its keep
during development. The architecture check rejected the first version of the
Clock port, because it handed out std::time::Instant, a type with no public
constructor. A fake clock would have been impossible to write without calling
Instant::now(), the very thing AR-10 forbids outside the adapters. A port has
to abstract the types as well as the operations. That note is preserved in
example/specs/REQ-0003.
Known limits
- The layer check works at crate granularity, not inside a crate. When a crate develops an internal architecture, that is the signal to split it.
- The symbol rules in
arch.jsonare text search, not parsing. They catch the ordinary case and not a deliberate detour. spec-trace.shchecks that a test exists. Whether it covers the criterion is beyond it.just example mutants(objective, slow) and RC-16 in review (fast, unreliable) close that gap.language-check.shcatches any letter beyond ASCII and German prose. An all-ASCII foreign noun in an otherwise English sentence slips through.harness-gate.shgives up after three blocked attempts. An infinite loop would be worse than a red harness.