crimes v0.20.0 — scope it to the work
Theme: the most valuable way to run crimes was the least
discoverable one, and the default view was describing a report it wasn’t
printing.
Everything here comes from one outside artefact: a field report from an
agent using crimes@latest on a real Next.js project
(docs/dogfooding/2026-08-05-choreograph-field-notes.md), plus the
re-verification that checked every complaint against main before any
of it was acted on
(docs/dogfooding/2026-08-05-choreograph-reverify.md).
That re-verification is why this release is shaped the way it is. Two of the report’s five concrete asks turned out to be already shipped, and a third would not have worked as proposed. Building all five would have meant a second mechanism for something that already had one.
The headline: crimes scan has a working set
Section titled “The headline: crimes scan has a working set”Bare crimes scan audits the whole repository. On the 209-file project
in the field report that is 499 findings, which is not a work list —
it is an invitation to either over-fix into unrelated files or dismiss
the tool. Both are worse than not running it.
Almost every agent task is about a handful of files. Now you can say so:
# You know which files the change touches.crimes scan --files src/lib/api.ts,src/lib/types.ts
# You know one file and want its neighbourhood.crimes scan --related-to src/lib/api.tscrimes scan --related-to src/lib/api.ts --related-depth 2
# You have already made the edits.crimes scan --changed --base mainOn the same repo, --related-to src/lib/creative-pass.ts returns 28
findings across 8 files where the bare scan returns 491. --files
with the three files the task actually touched returns 25.
--related-to walks the graph both ways
Section titled “--related-to walks the graph both ways”What a file imports can break it; what imports it is what it can break.
“What should I look at before changing this” has both halves, so the
walk is symmetric. --related-depth widens it; the default is 1.
The import graph this uses already existed and already carries Python edges, so this works on a mixed repo without anything new underneath.
The scope is always checkable
Section titled “The scope is always checkable”The resolved set — for --related-to, the result of the walk, not the
seeds — comes back on the JSON report:
"working_set": { "selector": "related-to", "seeds": ["src/lib/creative-pass.ts"], "depth": 1, "files": ["src/lib/audio-generator.ts", "src/lib/creative-pass.ts", …]}A graph walk that silently included or excluded a file, with no way to check, is a shape this codebase keeps getting bitten by. An agent has to be able to confirm what was looked at.
And a path that matched nothing says so, loudly:
$ crimes scan --files src/lib/nope.tscrimes: --files src/lib/nope.ts matched no file in this repo. The scan is narrower than you asked for.Without that breadcrumb the visible output of a typo’d path is “No
crimes detected. Suspiciously clean.” — the same failure shape as the
detectors.enable allowlist bug 0.19.0 fixed, and it fires even under
--no-color for the same reason: it reports a gap between what was
asked for and what happened, not a preference.
Cross-file indexes still see the whole repo
Section titled “Cross-file indexes still see the whole repo”blast_radius on a working-set scan is the blast radius of the file
within the repository, not within the set. A scan scoped to one file
that reported it as having no importers would be worse than useless.
--fail-on accepts any of the three
Section titled “--fail-on accepts any of the three”It used to require --changed, which left the CI story unavailable to
anyone scoping by path. Selectors are mutually exclusive — combining
them would make a report’s scope unguessable from the command that
produced it.
--changed is the post-edit selector, and the docs now say so
Section titled “--changed is the post-edit selector, and the docs now say so”The field report’s own conclusion was “--changed --base main is the
answer, and it was not reached for”. The re-verification found the
sharper version of that:
scan --changed --base HEAD~1 → 11 findings across 7 filesscan --changed → clean tree: nothing--changed works, and works well — 491 → 11 is exactly the compression
the complaint asks for. But it only works after the edits exist. The
field report describes crimes being used mid-design, to scope cleanup
before writing code: at that moment the tree is clean and sitting on
main, so --changed --base main returns zero findings.
So documenting --changed as “the” agent default would have sent an
agent in that position to a command that prints nothing. It is
documented as the post-edit default, paired with --files /
--related-to for the half of the loop that comes first.
The --help tips block now leads with scoping. It previously named
init --agents and context <file> and said nothing about narrowing
a scan at all — which is the one-line version of the whole problem.
The headline number now counts what the report shows
Section titled “The headline number now counts what the report shows”This is the part the re-verification changed most.
The field report asked for scaffolding: ["scripts/**"] globs in
crimes.config.json, applied at scan time, because roughly a third of
its findings were in one-off diagnostic scripts that are quick and dirty
by design.
That mechanism already ships, on by default, and scripts/** is
literally the first entry in DEFAULT_NON_DOMAIN_PATTERNS. All 148
scripts/ findings on that repo already carried tier: "nonDomain",
were already excluded from the top-file ranking, already
confidence-damped, and already collapsed into a one-line “Also flagged
elsewhere” footer.
What was actually broken is narrower and worse — the header was counting findings the report then declined to show:
| header said | body listed | |
|---|---|---|
| findings | 491 | 339 |
| files | 208 | 137 |
| high | 35 | 22 |
A third of the demoralising number was findings crimes had already
decided were not the point. The summary line now counts the domain
findings the body lists, and states the rest:
repo: choreo · 339 findings across 137 files · 22 high 178 medium 139 low · +152 in non-domain paths--all still reports 491, because --all lists all of them.
summary.total in the JSON is untouched — the renderer is a view
and the JSON is the contract, and teaching summary to withhold
findings would be fixing a readability problem in the machine interface.
Adding a scaffolding glob would have been a second way to say what
scopeTiers.nonDomain already says. docs/configuration.md now names
it as the scaffolding knob, which is what was missing.
The totals survive to the end of the report
Section titled “The totals survive to the end of the report”scan --top 15 on that repo emits 296 lines, so the summary header —
line 6 — scrolled off the buffer. A second run at --top 3 was needed
just to read the totals.
The summary is now repeated immediately above the closing line on any
report long enough to have lost it. Not after: the 0.10.0 front-door
redesign deliberately ends the report on → Start with crimes context <file>, which is the most actionable single line an agent gets, and
that decision stands. Both now survive in the last two lines.
Not a flag, either. A --summary-last flag would have exactly the
discoverability problem --changed had: an agent finds it only by
reading --help, which it only does if it already suspects there is
something to find.
crimes init --agents is now the loudest thing in the README
Section titled “crimes init --agents is now the loudest thing in the README”The field report records four steps to first run: searching
~/.claude/skills, then ~/.claude/commands, then ~/.claude/plugins,
then a filesystem find that turned up an unrelated directory, then
which crimes (nothing), before finally guessing npx crimes@latest.
That search order is real data about how an agent looks for a tool.
crimes init --agents is the fix and it existed — as a tip at the
bottom of --help, which an agent reads only if it has already decided
to run --help. It is now the first thing in the README’s agent
section, with the reason attached.
The skill that init --agents writes leads with a scoping table.
Schema
Section titled “Schema”schema_version 0.6.0 → 0.7.0. Purely additive:
- New optional field on
ScanReport:working_set. - New
coverage.warnings[].kind:working_set_path_unmatched.
No fingerprints change, so every .crimes/baseline.json,
.crimes/suppressions.json and .crimes/triage.json entry carries over
untouched — nothing to re-record. A consumer that ignores unknown keys
needs no change at all; additionalProperties: false validators and
schema_version === "0.6.0" hard-checks need updating.
Full notes:
Migrating from 0.6.0 to 0.7.0.
The pinned example at docs/fixtures/messy-ts-app.json is regenerated.
It had been stale at 0.4.0 — two schema versions behind — since the
0.5.0 bump.
One fix found while cutting the release
Section titled “One fix found while cutting the release”crimes feedback recheck looked up its per-detector migration note by
the current minor exactly, so every note went unreachable the moment
the next minor shipped. All fifteen 0.17 notes had been orphaned since
0.18.0. A release is not the unit a user upgrades across; a span is.
The lookup now takes the pin as well as the target and returns every
note in between. Nine 0.18-era detector entries were added at the same
time — the map had only ever been written for fingerprint changes, so
detectors that changed what they fire on had no note at all.
This shipped in 0.19.0. It is repeated here because it is the third
instance of the same shape in this codebase — apparatus that fails
closed on correct input, after the eval scorer’s extension list and
the biome guard’s summary regex.
What the field report asked for and did not get, and why
Section titled “What the field report asked for and did not get, and why”crimes context --files a,b,c. The report asked for either a
multi-file context or scan --related-to; this release ships the
second. context() builds every cross-file index per call, so
nineteen files would mean nineteen index builds — the wrong shape for a
working set. crimes context stays deliberately single-file: it is the
deep per-file briefing, and scan --files is the working-set view.
That boundary is now documented rather than implied.
Exempting type-only modules from fan-in. Deferred to 0.21.0 with
the four other precision items, and the rule as proposed will not be the
one that ships: src/lib/types.ts exports 24 types and one
constant, so an exports-are-type-only test fails on the very file that
prompted the ask. Three candidate rules and the argument between them
are in docs/dogfooding/2026-08-05-r3-premeasurements.md.
Eval baseline
Section titled “Eval baseline”Unchanged at evals/results/0.18.4/. Nothing in this release moves a
finding for the same input code: the working-set flags change which
files are scanned only when passed, the summary line is a human
renderer change, and schema_version is a version string. A default
crimes scan --format json on the same tree is byte-identical apart
from that string.
Per evals/README.md that is explicitly not a
bump-triggering change, and re-running the suite to produce an identical
number would be measurement theatre.
Verification
Section titled “Verification”pnpm verify # format:check + lint + build + typecheck + testpnpm --filter crimes smoke # pack + install in a temp dir + run every commandNew coverage: eleven core tests for the working set (both selectors,
absolute and relative paths, depth, symmetry, the unmatched-path
warning, and a guard that blast_radius still measures against the
whole repo), thirteen CLI tests for the flags and their unhappy paths,
and sixteen reporter tests pinning the summary’s placement and what it
counts.
One incidental fix: the SCHEMA_VERSION pin test was titled is 0.5.0
while asserting 0.6.0, so a failure reported the wrong expectation to
whoever hit it. The title no longer restates the version.