Four files every hooks PR rewrote, and the CI that went silent

Issue #3578. Measured on the 30 days before 2026-09-02: 88 of 372 commits on main touched the hook bundles. The fix makes those bundles release-owned.

The defect, as a sequence

A hook bundle is one minified file per event (pretool.mjs, stop.mjs, ...) plus bundle-stats.json, mirrored into plugins/ork/hooks/dist/. Any source change rewrites the whole file, so two PRs touching any two hooks always conflict on the same four paths. Rebasing is a race against the next hooks merge.

PR #3570, 2026-08-14
  rebase onto origin/main, rebuild, push   9c1f8218c   mergeable, 0 dirty
  main advances (a hooks fix, a4da29fa9)   +7 files    4 of them are the bundles
  PR state                                 dirty       no merge ref
  pull_request runs on the new head        0           (14 on the previous head)
  pull_request_target runs                 2           builds against BASE, unaffected

A red check is a signal. Zero runs is silence, and it is indistinguishable from a slow queue unless you know to look at pull_request_target surviving while pull_request goes to zero.

Simulate two hooks PRs

Pick the policy, then land PR A while PR B is still open. What each PR commits decides whether GitHub can still compute B's merge ref.

PR A (merged first)

PR B (still open)


Options that were on the table

optionremoves the conflictremoves the CI silenceverdict
Merge driver for dist/* (take either side, rebuild)nonoGitHub computes mergeability with its own merge, custom drivers are local only, so the PR stays dirty on the server. Rejected.
Rebase bot that re-rebases every open hooks PR after each mergebrieflynoStill a race, now automated; every rebase re-triggers 18 required checks. Rejected.
Stop tracking dist/ entirely, build at installyesyesThe plugin is installed from a git ref and nothing builds at install time, so a tree without bundles is a plugin whose hooks silently no-op. Rejected.
Release-owned dist: feature PRs never commit it, the release-please branch rebuilds it, CI gates bothyesyesOne writer per path, the same shape hq-ext used for CHANGELOG.md. Release tags, the only refs installs pin, always carry fresh bundles. Chosen.

What changes, file by file

filechangerole
.github/workflows/ci.ymlBuild drift roster excludes both dist paths; new required step "Check hook bundles are release-owned"gate: feature PR must not change dist vs base; release PR must equal a fresh build
.github/workflows/release-please.ymlnew step "Rebuild hook bundles on the release PR"the single writer; refuses unless the branch differs from main only in release-owned paths
.github/workflows/skill-autobuild.ymlheal roster excludes dista heal commit must not fail the gate it heals for
bin/git-hooks/pre-commitsection 9 refuses staged dist (ORK_DIST_COMMIT=1 for automation)local, before CI
bin/bump-version.shno longer stages distlegacy path kept consistent
CLAUDE.md, CONTRIBUTING.md, src/hooks/README.mdthe exception spelled out, registry changelog entrydocs

Consequences worth knowing

main's bundles are stale between releases, by design. Every CI job already builds from source before testing (install-hooks-deps: 'true'), so tests never read the committed copy. Installs pin release tags, and the release PR is required by the gate to carry a fresh build, so a release cannot ship stale bundles even if the bot step fails.

Transition. An open hooks PR that already commits bundles fails the new gate once this lands; the fix is one command, printed in the failure: restore the two dist paths from the base and commit.

Local workflow. npm run build still writes dist. Leave it unstaged; the pre-commit hook refuses it if you stage by accident.