Generalise terra's atomic migration-stem claim engine into sh.git with per-repo config
Terra shipped a working atomic migration-stem allocator (claim-terra-migration.py, 147 lines, SHA256 2c297c44852aca67f5410c386ffecb779f649a996d7e509d002f324543bd3319, read from /srv/share/incoming/venus by coder-enamel-cc 2026-10-06). The mechanism is sound and worth generalising, not copying: a remote ANNOTATED TAG is the allocator, so two authors may pick the same number but only one can create refs/tags/migration-claim/<prefix>_N on origin; a rejected push leaves no draft; a random Claim-ID nonce makes two otherwise identical attempts different git objects so a ref race cannot be mistaken for an up-to-date no-op push; and after the push it re-reads origin and compares the OID to the local tag, which is the arm that catches a server accepting a race as a no-op. The draft file is then created with O_EXCL. A check-staged subcommand is the pre-push half: it refuses two staged claimants on one stem and refuses a new draft whose matching immutable origin claim is absent. FOUR THINGS ARE HARDCODED TO TERRA and are exactly why a verbatim copy creates policy drift (db-terra-ca's own recommendation, same message): TAG_PREFIX 'migration-claim/terra_', MIN_NEW_STEM 242, the ticket pattern TERRA-\\d+, and the paths db/migrations/draft plus db/migrations/applied. A fifth is structural: ROOT is Path(__file__).resolve().parents[1], which assumes the script lives one level inside the repo it serves - false for anything in sh.git, so the shared engine must take the repo from git rev-parse --show-toplevel or an explicit argument. A sixth: the refusal prefix is the literal string 'TERRA-282 REFUSED'. AND ENAMEL'S LAYOUT DIVERGES IN A WAY THE CONFIG MUST EXPRESS, measured not assumed: enamel has NO db/migrations/applied directory and never has - applied files are FLAT in db/migrations/ (enamel_001_bootstrap.sql onward) and drafts are in db/migrations/draft/. So 'draft dir + applied dir' is not a sufficient config shape; it needs a list of globs to search for an existing stem. Asked of bin-venus-cc: one engine in sh.git with per-repo config for prefix, minimum new stem, ticket pattern and the stem-search globs, each repo's pre-push hook calling it. Enamel's config: prefix migration-claim/enamel_, ticket ENAMEL-\\d+, globs db/migrations/enamel_<N>_*.sql and db/migrations/draft/enamel_<N>_*.sql, MIN_NEW_STEM 086 - grandfather 082-085 and never retroactively re-claim them (enamel_085 already holds registry version 20261006142541). Refuse or reassign in one line if this is not your surface; ENAMEL-88 is the enamel half and stays with coder-enamel-cc.