Skip to content

`crimes@0.10.0` — Front-door redesign (Release A)

Draft release notes for the GitHub Release tagged v0.10.0. The body below is what should go in the Releases page when you cut the tag — that triggers .github/workflows/release.yml and publishes to npm via Trusted Publishing.

crimes@0.10.0 is a minor release that redesigns the crimes scan default output and the first-run experience so the first screen tells you what to fix rather than listing everything at once. crimes context now leads in every entry-point — banner, --help, README, agent docs.

Two changes require agent attention:

  1. Default crimes scan is now file-grouped, not severity-grouped. Use --flat to get the old layout back.
  2. scores.test_gap is now a repo-relative quartile (0 / 0.25 / 0.5 / 0.75 / 1.0), not the prior fixed {0, 0.5, 1.0} mapping. Agents that compared test_gap === 1 must switch to >= 0.75.

Both are additive with respect to schema_version — it stays at "0.1.0". The new fields (Finding.tier, Finding.scores.recency, ContextReport.clues) are optional and absent on output from earlier versions.

Design rationale: docs/superpowers/specs/2026-05-20-release-a-front-door-design.md.


The default crimes scan human output now groups findings by file and sorts files by aggregate risk — a blend of churn, test-gap quartile, blast radius, and recency. The prior layout (severity-grouped flat list) is available via --flat.

Default view (human output):

CRIME SCENE REPORT
repo: acme-app · 173 findings across 47 files
Top files by risk
🚨 src/billing/invoice.ts 4 findings · 2 high
1. God Function · generateInvoice() 214 lines, 6 awaits, 3 tables
2. Duplicated Policy Logic · isOwner role check in 4 sites
3. Temporal Recklessness UTC + local mixed
4. Direct Date.now() in domain ln 187
Risk: churn high · test gap top-quartile · blast 0.72
id=crime_01982 … crime_01985
⚠️ src/api/users.ts 3 findings · 1 high
...
Showing top 5 files of 47. Run with --all to see every finding.

The JSON wire format is unchanged: findings[] is still a flat array on ScanReport, sorted by rank score. The human reporter folds them into the per-file view.

New crimes scan flags:

FlagWhat it does
--top <n>Show top N files (default 5). Overrides scan.topFiles config.
--flatRevert to severity-grouped flat output.
--no-recencyDisable recency weighting in file ranking.

New config keys:

KeyTypeDefaultWhat it does
scan.topFilesnumber5Default number of files shown in file-grouped output. Override per-invocation with --top N.
scopeTiers.nonDomainstring[][7-pattern list, see below]Glob patterns whose findings appear in a separate “Also flagged elsewhere” footer. Empty array opts out.

The default scopeTiers.nonDomain list (applied at scan time when the key is absent from crimes.config.json):

[
"scripts/**",
"examples/**",
"fixtures/**",
"public/**",
"**/__tests__/**",
"**/*.test.{ts,tsx,js,jsx}",
"**/*.spec.{ts,tsx,js,jsx}"
]

Finding.scores.test_gap was previously one of three fixed values (0 = covered, 0.5 = weakly covered, 1.0 = uncovered). From 0.10.0 it is a quartile rank computed against the distribution of test-file proximity across all source files in the repo:

Quartiletest_gap valueMeaning
Bottom0.00Well-tested relative to this repo
Q10.25Modestly covered
Q2 (median)0.50Average for this repo
Q30.75Weakly covered relative to this repo
Top1.00Among the least-tested files in the repo

The human reporter renders one of four labels — top-quartile, ~median, bottom-quartile, unknown — instead of the raw float. The JSON clues.test_gap.label enum drops the tilde (top-quartile | median | bottom-quartile | unknown).

Migration note for agents. If your agent code compares scores.test_gap === 1 (or === 1.0) to detect “no tests”, change the comparison to scores.test_gap >= 0.75. The 1.0 value now means “top-quartile test gap in this specific repo”, not “unconditionally uncovered”.

Finding.scores.recency is a new optional 0–1 float:

  • 1.0 — file last committed in the past 7 days
  • Linear decay from 1.0 to 0.0 between 7 and 14 days
  • 0.0 — file untouched for 14 days or more, or latestChange absent (non-git directory, shallow clone window excludes the file, etc.)

The rank score that drives file ordering in the default scan layout multiplies recency in (rank_score = agent_risk × (1 + recency × 0.5)), so files that are both risky and recently touched surface first. Use --no-recency to collapse the multiplier to 1 and rank by agent_risk alone — recency still ships on FindingScores for JSON consumers, the CLI just doesn’t apply it to ordering.

Each finding is now tagged with tier: "domain" | "nonDomain", computed from the finding’s file path matched against config.scopeTiers.nonDomain (defaults documented above). Domain findings are the ones the default scan layout shows in its top-N file list; non-domain findings are grouped under an “Also flagged elsewhere” footer with per-prefix counts (scripts/ 6 findings examples/ 3 findings tests/ 12 findings).

tier is optional and absent on output from earlier versions; treat absence as "domain" for backwards-compatible reads. Set scopeTiers.nonDomain: [] in crimes.config.json to disable tiering entirely (every finding becomes "domain").

Frozen contract for downstream consumers (e.g. PreToolUse hooks). ContextReport gains an optional clues object with three substantive sub-blocks plus a reserved key:

{
"clues": {
"churn": {
"commits_90d": 14,
"last_commit_at": "2026-05-18T12:30:00Z",
"unique_authors_90d": 3
},
"suppressions": [
{
"fingerprint": "large_function::src/billing.ts::renderInvoice",
"detector": "large_function",
"reason": "Legacy billing module, rewrite planned",
"pinned_version": "0.9",
"matches_current_finding": false
}
],
"test_gap": {
"raw": 1,
"percentile": 0.85,
"label": "top-quartile"
},
"related_signals": []
}
}

Omission rules:

  • clues.churn is omitted when git is unavailable.
  • clues.suppressions is omitted when no entries scope to this file.
  • clues.test_gap.percentile is omitted in repos with fewer than 4 source files; in that case label is "unknown" and raw carries the raw 0 / 0.5 / 1 value.
  • clues.test_gap.label enum is exactly "top-quartile" | "median" | "bottom-quartile" | "unknown" (the human reporter prints ~median with a tilde; JSON does not).
  • clues.related_signals is always present, always [] in 0.10.0 — reserved for future use.
  • clues itself is omitted when every substantive sub-block would be empty (rare in practice since test_gap is almost always populated).

The human crimes context output renders a “Clues” block between “Likely tests” and “Findings” with one line per sub-block.

Running any crimes subcommand (other than init, feedback, ignore, unignore, baseline) on a repo with no crimes.config.json triggers a short interactive setup instead of silently proceeding. Up to two prompts:

  1. Config: “No crimes.config.json found. Generate one for this repo? [Y/n]” — accept and crimes writes a config tuned to the repo (TS-only include if no JS files exist, **/.next/** exclude for Next.js projects, **/dist/** for Vite, and scopeTiers.nonDomain populated from directories that actually exist plus the standard test globs).
  2. Agent skill (only when an agent is detected): “Write .claude/skills/crimes/SKILL.md so Claude Code discovers crimes for future sessions? [Y/n]” — or the equivalent Codex prompt for .agents/.

Agent detection priority (first match wins):

  1. process.env.CLAUDECODE or CLAUDE_CODEclaude
  2. process.env.OPENAI_CODEX or CODEX_AGENTcodex
  3. .claude/ directory exists → claude
  4. .agents/ directory exists → codex
  5. otherwise → no skill prompt

Skip conditions (the whole prompt block is suppressed):

  • process.env.CI set
  • stdout is not a TTY (piped invocations, runCli-style tests)
  • --no-init global flag
  • .crimes/.skip-init marker file exists (written when the user declines a prompt without accepting either)

--init global flag re-enters the prompt block even when crimes.config.json already exists.

New flags:

FlagWhereWhat it does
--init (global)Any cmdForce the auto-init prompt block even if a config already exists.
--no-init (global)Any cmdSuppress the auto-init prompt block entirely.
--no-detectinitOn explicit crimes init, skip repo-shape detection; write static config.

The welcome banner (printed on bare crimes) and the --help output now lead with crimes context <file> as the first suggested command. The README Quick start and docs/agent-usage.md are restructured to the same order: context first, then scan gates, then verdict.


  • No schema_version bump. The Finding wire format is backwards-compatible: tier, scores.recency, and ContextReport.clues are additive optional fields. schema_version stays "0.1.0".
  • No new detectors. Detector count unchanged at 48. Detector taxonomy is frozen for Release A per the design spec.
  • No crimes ask / LLM-assisted modes. Still deferred to v1+.

Terminal window
npm install -g crimes@0.10.0
crimes --version # 0.10.0
crimes context src/billing.ts --format json # new clues block
crimes scan . # new file-grouped layout
crimes scan . --flat # old severity-grouped layout

If your agent code reads scores.test_gap:

Change any === 1 or === 1.0 comparison to >= 0.75. Values shifted from the fixed three-point scale to repo-relative quartiles. The 1.0 value is preserved (top-quartile files still score 1.0) so the absolute ceiling is unchanged, but everything between 0 and 1 may move.

If your agent code reads crimes scan --format json and parses findings by severity group:

The JSON wire format is unchanged — findings[] is still a flat array on ScanReport. Only the human renderer changed to file-grouped. If your code consumes --format json, no changes are needed.

Auto-init on first run:

If you run crimes in a fresh repo (no crimes.config.json) in a CI environment, the auto-init step detects CI is set and skips silently — no prompt, no files written. Use --no-init to suppress the prompt unconditionally (CI or local), or --init to force the prompt block to re-enter when a config already exists.