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
| option | removes the conflict | removes the CI silence | verdict |
|---|---|---|---|
Merge driver for dist/* (take either side, rebuild) | no | no | GitHub 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 merge | briefly | no | Still a race, now automated; every rebase re-triggers 18 required checks. Rejected. |
Stop tracking dist/ entirely, build at install | yes | yes | The 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 both | yes | yes | One 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
| file | change | role |
|---|---|---|
.github/workflows/ci.yml | Build 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.yml | new 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.yml | heal roster excludes dist | a heal commit must not fail the gate it heals for |
bin/git-hooks/pre-commit | section 9 refuses staged dist (ORK_DIST_COMMIT=1 for automation) | local, before CI |
bin/bump-version.sh | no longer stages dist | legacy path kept consistent |
CLAUDE.md, CONTRIBUTING.md, src/hooks/README.md | the exception spelled out, registry changelog entry | docs |
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.