Releasing `crimes`
How a new crimes@X.Y.Z reaches npm
and crimes.sh, written so that a collaborator who has
never cut one, or an AI coding agent working on their behalf, can do it
without asking. Every command is meant to be pasted as written. Facts below
were verified against the repository, GitHub and npm on 4 September 2026;
anything not verified says so.
The short version. One file carries the version
(packages/cli/package.json). One commit, titled Prep crimes@X.Y.Z,
bumps it and every surface that mirrors it. One human action, publishing a
GitHub Release tagged vX.Y.Z, fires
.github/workflows/release.yml, which
builds, tests, smoke-tests, checks the tag against the version, and runs
npm publish using npm Trusted Publishing. Nothing is published from a
laptop and no secret is involved.
1. What publishes what
Section titled “1. What publishes what”| Surface | Mechanism | Trigger | Human involvement |
|---|---|---|---|
npm package crimes | .github/workflows/release.yml, workflow name Release, job publish-npm | A GitHub Release with event type published (drafts do not fire it, bare tags do not fire it) | Publishing the release. That is the whole trigger. |
| GitHub Release page (the canonical changelog) | Created by hand or with gh release create; body is docs/releases/vX.Y.Z.md | n/a | Writing and approving the notes, clicking Publish |
| crimes.sh | Vercel production deploy from main, building apps/website/ | Every push to main | None. Note the consequence in section 9: the site shows whatever main says the version is, whether or not npm has it yet. |
CI workflow (.github/workflows/ci.yml) | Jobs build and publish-smoke | Push or pull request to main | None. Must be green on the prep commit before the release is cut. |
evals-pr-replay (.github/workflows/evals-pr.yml) | Replays pinned eval results | Push or PR to main touching detector, scoring, language, CLI or evals/ paths; also workflow_dispatch | None |
Only packages/cli is published. @crimes/core, @crimes/reporter,
@crimes/language-js and @crimes/language-py are workspace packages at
version 0.0.0, bundled into dist/index.js by tsup, and never released on
their own. packages/cli/scripts/prepack.mjs strips devDependencies and
every scripts entry from the packed manifest, and postpack.mjs restores
them, so the tarball declares no lifecycle script and no workspace:*
reference.
There is no CHANGELOG.md, on purpose. The GitHub Release page is the
changelog; docs/releases/vX.Y.Z.md is its in-repo draft and stays in the
tree afterwards. .changeset/ is not wired to anything (no
@changesets/cli, no config, no CI step); read
.changeset/README.md before adding a file
there, and delete any file whose release has shipped.
2. Who can do what (verified 4 September 2026)
Section titled “2. Who can do what (verified 4 September 2026)”- GitHub
ortomate/crimes:andrewfantasticandtimcopelandnzboth holdadmin. Either can push tomain, create and publish a Release, and re-run a workflow.mainhas no branch protection (the API returns 404 for it), so nothing stops a direct push; the discipline is procedural. - npm
crimes: the only maintainer isandrewfantastic. Because the workflow authenticates with OIDC rather than a personal token, cutting a release does not need an npm account at all. What does need one, with its 2FA:npm deprecate,npm unpublish,npm dist-tag, and changing the Trusted Publisher configuration on npmjs.com. Today only Andrew can do those. - Vercel: not verified from this repository. The site has deployed from
mainon every push through 0.27.0’s prep commit, so the wiring works; who holds the Vercel project is not recorded here.
3. One-time setup (already done; here so it can be checked or redone)
Section titled “3. One-time setup (already done; here so it can be checked or redone)”npm Trusted Publisher. On npmjs.com, as a maintainer of crimes:
Package settings, Publishing access, Trusted Publishers, Add Trusted
Publisher. Provider GitHub Actions, organisation ortomate, repository
crimes, workflow filename release.yml, environment blank. This is in
place: every Release run from v0.22.0 through v0.26.0 succeeded, and
npm view crimes@0.26.0 dist.attestations shows a SLSA provenance
attestation, which only Trusted Publishing produces.
No NPM_TOKEN secret. release.yml references no secrets and asks for
permissions: contents: read, id-token: write only. Do not add a token
“just in case”; it widens what a tampered workflow file could do, and the
workflow would not use it.
Vercel. Project linked to this repository, production branch main,
build command node ./scripts/build.mjs in apps/website/, output
apps/website/dist. Nothing per release.
Branch protection. Recommended and not configured. If someone turns it
on, require the CI checks (install · build · typecheck · test and
publish smoke (pack + install + run)) on main.
4. Preconditions
Section titled “4. Preconditions”Confirm all of these before starting the prep commit.
cd /path/to/crimesgit switch main && git pull --ff-onlygit status --porcelain # must print nothingnode --version # must match .nvmrc (26.7.0 as of 0.27.0)pnpm --version # 10.14.0, from packageManager in package.jsongh auth status # logged in with access to ortomate/crimesnpm view crimes version # the version currently on npmnode -p "require('./packages/cli/package.json').version" # the version in the treeThe tree version and the npm version normally match here. If the tree is already ahead of npm, a prep commit has landed and not been released; go to section 7 rather than preparing a second one.
Decide the new version. Semver as this project applies it:
- patch (
X.Y.Z+1): detector bug fixes and copy changes that do not alter the JSON schema. Note that patch numbers are also consumed by eval baseline bumps between releases (evals/README.md, section “Versioning policy”), so the next release version is whatever follows the highest patch already used, not necessarily.1. - minor (
X.Y+1.0): new detectors, new commands, additive schema fields, or any change to what a detector means (averdictorbaseline checkgate would see different results). Every release since 0.16.0 has been a minor. - major: a breaking change to the wire format. Also bump
SCHEMA_VERSIONinpackages/core/src/finding.ts. To date, schema bumps have shipped inside minors (0.27.0 movedschema_versionto0.8.0), because the package itself is still0.x.
5. The prep commit
Section titled “5. The prep commit”The last two prep commits, 364a333 (Prep crimes@0.26.0) and 928034c
(Prep crimes@0.27.0), are the models. Reproduce their shape:
git show --stat 928034cChecklist. Every box is a file the commit must touch, or a reason it need not.
-
packages/cli/package.json:"version": "X.Y.Z". The only place the version is authoritative.release.ymlcompares the release tag to this field and refuses to publish on a mismatch. -
docs/releases/vX.Y.Z.md: the release notes, new file. This becomes the GitHub Release body verbatim. If the release changes fingerprints or the schema, lead with an “Upgrading” section (0.27.0 is the example). List what was planned and not done under a heading such as “Still unsettled”, so the next plan starts from the truth. -
README.mdandpackages/cli/README.md: describe the current workflow and link to release notes. Keep history indocs/releases/, not a growing stack of release headlines in onboarding docs. -
docs/roadmap.md: record the prepared version separately from the published version. Verify npm before claiming publication. - After building, run
pnpm docs:generate. It derivesdocs/reference.md,apps/website/landing/llms.txt, landing versions and live output examples from the CLI and registry.pnpm verifychecks for drift. Update surrounding capability copy when behavior changes, and run websiteverify-build. -
docs/fixtures/messy-ts-app.json: regenerated bypnpm docs:generatefrom a source-only copy without Git history; the root is normalized. This keeps examples identical in shallow CI and a maintainer checkout. -
docs/json-schema.md: only if the schema changed and the change that introduced it did not already document it. -
evals/results/<X.Y.Z>/: the eval baseline is keyed by the version inpackages/cli/package.json, so the directory moves with it. Either runpnpm run evals:rankingto seedranking.json(as 0.26.0 did), or, when the release changed only finding identity, add a row to the carry-forward table inevals/README.md(“Identity-only bumps”) saying why no run was needed. One or the other; not neither. -
.changeset/: delete any file describing a release that has shipped. -
.planning/SPRINT-X.Y.md: if a sprint plan exists for this release, record the outcome and the corrections in it (see.planning/SPRINT-0.26.mdsections 8 and 9 for the shape), then move it to.planning/archive/per.planning/README.md. This can be a separate commit but should land before the release.
Do not edit PRD.md for a release. Do not touch .nvmrc, the workflows,
or biome.jsonc in the prep commit; formatting settings change what
crimes reports about itself (section 12).
Commit message: first line Prep crimes@X.Y.Z; body lists the files and
why, records the pre-flight results with the Node version, and states that
publishing is not part of this commit. 928034c is the template.
6. Pre-flight, run locally before pushing
Section titled “6. Pre-flight, run locally before pushing”pnpm install --frozen-lockfilepnpm verify # format:check, lint, build, typecheck, testpnpm scan:example # human report against the bundled fixturepnpm scan:example:json # JSON report against the bundled fixturepnpm --filter crimes smoke # npm pack, install into a temp dir, exercise core commands and Python packagingpnpm --filter @crimes/website build && node apps/website/scripts/verify-build.mjsAll six must exit 0. If format:check fails, run pnpm format and commit
the result rather than hand-editing. verify-build runs against the built
dist/, so it must follow the website build or it checks a stale page.
Optional tarball inspection:
cd packages/cli && npm pack --dry-run && cd -Expect package.json, dist/index.js, other dist/*.js, dist/*.wasm,
README.md, LICENSE. No sourcemaps, no src/, no scripts/, no
workspace:* in the packed manifest.
Then push and wait:
git push origin maingh run watch --repo ortomate/crimes --exit-status $(gh run list --repo ortomate/crimes --branch main --workflow CI --limit 1 --json databaseId --jq '.[0].databaseId')7. Cut the release
Section titled “7. Cut the release”Two steps, deliberately split so the irreversible one is a separate act.
7a. Draft it. This records the exact commit in target_commitish, but
does not create the tag or fire the release workflow. GitHub creates the
tag when the draft is published. Verify the draft’s target now and the
actual tag after publication. This sequence was verified during 0.29.0.
git fetch origin main --tagsVERSION=$(node -p "require('./packages/cli/package.json').version")TARGET=$(git rev-parse origin/main)test -z "$(git ls-remote origin "refs/tags/v${VERSION}")" || exit 1gh release create "v${VERSION}" \ --repo ortomate/crimes \ --draft \ --target "${TARGET}" \ --title "crimes v${VERSION}" \ --notes-file "docs/releases/v${VERSION}.md"test "$(gh release view "v${VERSION}" --repo ortomate/crimes \ --json targetCommitish --jq .targetCommitish)" = "${TARGET}"Review the draft at https://github.com/ortomate/crimes/releases. Check
the tag reads vX.Y.Z with a lower-case v and matches
packages/cli/package.json exactly. The command is not repeatable while the
draft exists. An existing tag must be investigated before creating the
draft: GitHub uses that tag’s commit even when a different target is given.
To abandon an unpublished draft safely, remove it and any leftover tag with
gh release delete "v${VERSION}" --repo ortomate/crimes --cleanup-tag, then
start section 7 again.
7b. Publish it. A human does this. Either click Publish release on the draft, or:
gh release edit "v${VERSION}" --repo ortomate/crimes --draft=falsegit fetch origin --tagstest "$(git rev-parse "v${VERSION}^{commit}")" = "${TARGET}"Publishing creates the tag at the draft’s recorded target and fires
release.yml. Confirm the tag points to TARGET immediately. From here
there is no undo short of section 10.
Titles on past releases follow crimes vX.Y.Z optionally followed by a
short theme; either is fine.
8. What release.yml does on its own
Section titled “8. What release.yml does on its own”In order, from .github/workflows/release.yml:
- Checkout; set up pnpm (version from
packageManager) and Node (from.nvmrc, registryhttps://registry.npmjs.org). pnpm install --frozen-lockfile=false.pnpm run build,pnpm run typecheck,pnpm run test.pnpm --filter crimes smoke.- Verify
github.event.release.tag_nameequalsv+packages/cli/package.jsonversion. Mismatch fails the job before anything is published. - Print the OIDC token claims (payload only) for debugging.
npm publish --provenance --access publicfrompackages/cli.
Past runs take a little over two minutes. Watch it:
gh run watch --repo ortomate/crimes --exit-status $(gh run list --repo ortomate/crimes --workflow Release --limit 1 --json databaseId --jq '.[0].databaseId')If it fails before step 7, nothing was published: fix main, then either
re-run the job (gh run rerun <id>; it checks out the tag, so a fix on
main needs a new release) or delete the release and tag and start again
from section 7. If it fails at step 7 itself, read the log for the npm error
before doing anything; a Trusted Publisher mismatch shows up here and is
fixed on npmjs.com, not in the repo.
9. Verify afterwards
Section titled “9. Verify afterwards”npm view crimes version # X.Y.Znpm view crimes dist-tags # latest: X.Y.Znpm view crimes@X.Y.Z dist.attestations # provenance block presentgh release view vX.Y.Z --repo ortomate/crimescd "$(mktemp -d)" && npx --yes crimes@X.Y.Z --version && cd -curl -s https://crimes.sh/ | grep -o '<span class="pill">v[0-9.]*'The last line checks the site. Because Vercel deploys from main on push,
the site advertises the new version from the moment the prep commit lands,
which may be before npm has it. Keep the gap between prep and publish
short for that reason.
10. Rolling back or deprecating
Section titled “10. Rolling back or deprecating”npm allows unpublish within 72 hours and discourages it. Prefer shipping a
fixed patch. All of the commands below need an npm maintainer login with
2FA; as of 4 September 2026 that is only Andrew.
npm deprecate crimes@X.Y.Z "Broken: install X.Y.Z+1 instead" # preferrednpm unpublish crimes@X.Y.Z # within 72h onlynpm dist-tag add crimes@<good version> latest # if latest points at a bad buildThen prepare and release the fix as a new version through sections 5 to 9. Do not reuse a version number; npm will not accept it even after an unpublish.
A GitHub Release can be deleted (gh release delete vX.Y.Z --cleanup-tag),
but doing so does not affect npm, and the release workflow has already run.
11. If you are an agent
Section titled “11. If you are an agent”Read AGENTS.md first; its safety rules apply. Within
them, this is the division of labour.
You may do, without asking:
- Every item in section 5 (the prep commit) and section 6 (pre-flight), on
a branch or on
mainwhen the user has asked for the prep. - Draft the release notes in
docs/releases/vX.Y.Z.md. - Create the GitHub Release as a draft (section 7a). Verify its recorded target commit immediately. Saving a draft creates no tag and triggers no workflow; the draft remains removable before publication.
- Watch workflow runs and report their result.
- Run every read-only verification in section 9.
- Open a pull request.
A human must do these, and you must not do them even if your credentials would allow it:
- Publish the GitHub Release (section 7b), by clicking Publish or with
gh release edit --draft=false. This is the single irreversible action in the process and it is what runsnpm publish. One exception. Andrew granted the Hobbes executor release authority on crimes on 4 September 2026; the grant is recorded per product inortomate/hobbesatdocs/portfolio.toml(release = "hobbes") and shown byhobbes portfolioasrelease: hobbes. When you are that executor working a crimes card, you may publish the release, and you must then verify withnpm view crimes versionbefore the card is done. Every other agent, and the executor on every other product, still stops at the draft. The npm-as-a-user items below stay human for everyone. - Anything against the npm registry as a user:
npm publish,npm deprecate,npm unpublish,npm dist-tag,npm login. These require the maintainer account and its 2FA, which you do not have and should not be given. - Change the Trusted Publisher configuration on npmjs.com, or add any
secret to the GitHub repository.
release.ymlneeds none. git tagorgit push --tags. Publishing the GitHub Release creates the tag.- Force-push, reset, or rewrite
main.
When you have done everything you may do, stop and say exactly what remains:
“Draft release vX.Y.Z is at npm view crimes version returns the new number.
12. Things that have bitten past releases
Section titled “12. Things that have bitten past releases”The hero pill. apps/website/landing/index.html carries a visible
vX.Y.Z pill linking to the release notes. It sat at v0.12.0 through two
releases, and at 0.26.0 it still linked to v0.25.0’s notes.
apps/website/scripts/verify-build.mjs now fails the build on either
mistake, and on these as well, each derived from the repository rather
than hard-coded:
| Claim on the page | Derived from |
|---|---|
JSON-LD softwareVersion | packages/cli/package.json |
| Hero pill version and its release-notes link | same, plus the built dist/docs/releases/ |
| ”N detectors across M families” | the built detector registry and docs/finding-types/*.md |
| Every finding-type family linked | docs/finding-types/*.md |
| Languages named in the prose, packages listed | packages/language-* |
Adding a language pack fails the build until LANGUAGE_NAMES in
verify-build.mjs and the page are both updated. That is intended.
Biome and the self-scan. large_function, large_file,
exact_duplicate_block, near_duplicate_block and
magic_domain_literal_scatter read line counts or line text, so
lineWidth in biome.jsonc (pinned at 90) changes what crimes reports
about itself. Do not change formatter settings in a release commit.
Known lint debt. apps/website/landing/index.html is excluded from
the linter in biome.jsonc for 90 real a11y diagnostics in two
<div role="table"> blocks. It does not block a release. Do not let the
exclusion grow.
Publishing from a dirty tree. The tarball reflects the disk, not git.
git status --porcelain must be empty before npm pack or smoke.
The changeset directory. Nothing consumes it and nothing empties it. Files for 0.19.0, 0.20.0, 0.21.0 and 0.25.0 were still present on 4 September 2026, all describing shipped releases.
13. What not to do
Section titled “13. What not to do”- Do not run
npm publishlocally. Trusted Publishing only works inside the workflow. - Do not create the tag by hand. A bare tag does not fire the workflow, and a tag that already exists when the release is created can point at the wrong commit.
- Do not add
NPM_TOKEN. - Do not deploy the website with the Vercel CLI; it races the automatic deploy.
- Do not leave a prep commit on
mainunreleased for long. crimes.sh will advertise a version npm does not have.
Historical 0.27 preparation gap
Section titled “Historical 0.27 preparation gap”The 4 September audit found a prep commit and website version ahead of publication, plus stale instruction/status files and four shipped changesets. That audit is preserved in git history. Its unverified registry state is not current release guidance. 0.28 consolidates current references and records prepared versus published status explicitly; check npm and GitHub at the time of each release rather than relying on an old snapshot.