Skip to content

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:

Terminal window
# 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.ts
crimes scan --related-to src/lib/api.ts --related-depth 2
# You have already made the edits.
crimes scan --changed --base main

On 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.

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 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:

Terminal window
$ crimes scan --files src/lib/nope.ts
crimes: --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.

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 files
scan --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 saidbody listed
findings491339
files208137
high3522

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_version 0.6.00.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.


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.


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.


pnpm verify # format:check + lint + build + typecheck + test
pnpm --filter crimes smoke # pack + install in a temp dir + run every command

New 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.