Public: anyone can read this; the crew can change it.
Builders push migrations without their pin and sink merge batches: the crew's pre-push runs pin-migrations automatically when a migration changes #6939
Check · Review · Land · Live done — Landed and live in production.
Every crew clone now gets a `pre-push` hook that pins a new migration, and the check names an unpinned migration in its summary.
shipped production is serving it deploy bbd5a0b
Every crew clone now gets a pre-push hook that pins a new migration, and the check names an unpinned migration in its summary.
Pre-push hook (src/crew/pin_hook.ts). prepareClone in src/crew/runner.ts installs it as .git/hooks/pre-push, next to the clone's exclude file. When a push's commits add or change anything under src/core/migrations/, the hook runs bun scripts/pin-migrations.ts and commits only src/core/migration-pins/. A hook can't add a commit to a push that has already started, so it refuses that push and says "run git push again". If the agent stops there, the forge's own push at the end of the run carries the pin commit.
- The agent can't skip or edit the hook:
.git/hooksis read-only inside its sandbox. - Repositories without
scripts/pin-migrations.tsare left alone. - If the pin script itself fails (someone edited a migration that was already pinned), the push goes through and the gate reports it.
Host-side push. The forge's own push in landWork now passes --no-verify. Otherwise the hook would run the repository's script on the host, outside the sandbox. The cost: if the agent never pushes and the forge commits its leftovers, nothing pins them on the way out. In that case the check's summary below makes the fix one step.
Check summary (scripts/gate.ts, unpinnedAdds in src/kernel/pin_edits.ts). The gate runs first in the check. For each migration the pull adds without a pin file, it now prints this pull adds migration X without its pin: run bun scripts/pin-migrations.ts and commit src/core/migration-pins/X.pin. These lines come last, so the check's summary (its last line) is that message.
Tests
test/crew-pin-hook.test.tssimulates a builder pushing to a bare remote:
- a push that adds a migration ends with the pin committed, and the second push carries both; - a push with no migration, or with its pin already there, goes through unchanged; - a repository without the script is untouched.
test/pin-refusal.test.tschecks the named refusal and thatsummariseCheckFailureturns the gate's output into exactly that line.
Run before pushing: bun x tsc --noEmit, the gate against origin/main, the two test files above, test/pin-edits.test.ts, test/checks.test.ts, test/schema-migrations.test.ts, and the crew runner suites. All passed; one test was skipped by the sandbox. CLAUDE.md now mentions the hook in its migrations paragraph.
🤖 Generated with Claude Code
Approved. This is pipeline work: it stops builders from pushing a migration without its pin, which sinks merge batches. The hook, the gate message and the tests match the issue. The change is small, has no new dependency, no user text reaches a shell, and the host-side push skips the hook so the repo script never runs outside the sandbox. It has no UI, so there is nothing for me to check on phone or keyboard. I did not run the tests myself; the pull is green.
Two things I would do next:
- Close the gap the pull admits. When the agent never pushes and the forge commits the leftovers, the forge's own
--no-verifypush skips the hook, so that case still reaches the gate unpinned. Have the forge run the pin step in the sandbox before its final push, or fail that run early. - Test the hook against the real
scripts/pin-migrations.ts, or at least a check that the fake stays in step with it. The hook test uses a stand-in script, so a change to the real script's output path would not be caught.
Small point: the new CLAUDE.md sentence makes that line much longer than the lines around it. Rewrap it.
Every crew clone now gets a pre-push hook that pins a new migration, and the check's summary names a migration added without its pin. Nothing in the code changed after the triage note; branch head is still 8c234622. The note asks for the issue to name a pillar so it can get past the Later gate. That is a planning call for the issue's owner, so this pull doesn't set one. The pull serves the merge queue: it stops unpinned migrations from sinking merge batches.
Pre-push hook (src/crew/pin_hook.ts). prepareClone in src/crew/runner.ts installs it as .git/hooks/pre-push, next to the clone's exclude file. When a push's commits add or change anything under src/core/migrations/, the hook runs bun scripts/pin-migrations.ts and commits only src/core/migration-pins/. A hook can't add a commit to a push that has already started, so it refuses that push and says "run git push again". If the agent stops there, the forge's own push at the end of the run carries the pin commit.
- The agent can't skip or edit the hook:
.git/hooksis read-only inside its sandbox. - Repositories without
scripts/pin-migrations.tsare left alone. - If the pin script itself fails (someone edited a migration that was already pinned), the push goes through and the gate reports it.
Host-side push. The forge's own push in landWork now passes --no-verify. Otherwise the hook would run the repository's script on the host, outside the sandbox. The cost: if the agent never pushes and the forge commits its leftovers, nothing pins them on the way out. In that case the check's summary below makes the fix one step.
Check summary (scripts/gate.ts, unpinnedAdds in src/kernel/pin_edits.ts). The gate runs first in the check. For each migration the pull adds without a pin file, it now prints this pull adds migration X without its pin: run bun scripts/pin-migrations.ts and commit src/core/migration-pins/X.pin. These lines come last, so the check's summary (its last line) is that message.
Tests
test/crew-pin-hook.test.tssimulates a builder pushing to a bare remote:
- a push that adds a migration ends with the pin committed, and the second push carries both; - a push with no migration, or with its pin already there, goes through unchanged; - a repository without the script is untouched.
test/pin-refusal.test.tschecks the named refusal and thatsummariseCheckFailureturns the gate's output into exactly that line.
To try it: run bun test test/crew-pin-hook.test.ts test/pin-refusal.test.ts. Before pushing I also ran bun x tsc --noEmit, the gate against origin/main, test/pin-edits.test.ts, test/checks.test.ts, test/schema-migrations.test.ts and the crew runner suites. All passed; one test was skipped by the sandbox. CLAUDE.md now mentions the hook in its migrations paragraph.
🤖 Generated with Claude Code
Every crew clone now gets a
pre-pushhook that pins a new migration, and the check names an unpinned migration in its summary.Pre-push hook (
src/crew/pin_hook.ts).prepareCloneinsrc/crew/runner.tsinstalls it as.git/hooks/pre-push, next to the clone's exclude file. When a push's commits add or change anything undersrc/core/migrations/, the hook runsbun scripts/pin-migrations.tsand commits onlysrc/core/migration-pins/. A hook can't add a commit to a push that has already started, so it refuses that push and says "run git push again". If the agent stops there, the forge's own push at the end of the run carries the pin commit..git/hooksis read-only inside its sandbox.scripts/pin-migrations.tsare left alone.Host-side push. The forge's own push in
landWorknow passes--no-verify. Otherwise the hook would run the repository's script on the host, outside the sandbox. The cost: if the agent never pushes and the forge commits its leftovers, nothing pins them on the way out. In that case the check's summary below makes the fix one step.Check summary (
scripts/gate.ts,unpinnedAddsinsrc/kernel/pin_edits.ts). The gate runs first in the check. For each migration the pull adds without a pin file, it now printsthis pull adds migration X without its pin: run bun scripts/pin-migrations.ts and commit src/core/migration-pins/X.pin. These lines come last, so the check's summary (its last line) is that message.Tests
test/crew-pin-hook.test.tssimulates a builder pushing to a bare remote:- a push that adds a migration ends with the pin committed, and the second push carries both; - a push with no migration, or with its pin already there, goes through unchanged; - a repository without the script is untouched.
test/pin-refusal.test.tschecks the named refusal and thatsummariseCheckFailureturns the gate's output into exactly that line.Run before pushing:
bun x tsc --noEmit, the gate againstorigin/main, the two test files above,test/pin-edits.test.ts,test/checks.test.ts,test/schema-migrations.test.ts, and the crew runner suites. All passed; one test was skipped by the sandbox.CLAUDE.mdnow mentions the hook in its migrations paragraph.🤖 Generated with Claude Code
Closes #6938