Builds and releases: the load-bearing rules
Builds and releases: the load-bearing rules
Section titled “Builds and releases: the load-bearing rules”Moved verbatim from CLAUDE.md on 2026-10-09 so it stops loading into every session. Read before ANY merge to main, BUILD bump, device install, store release, or .claude/ / .factory/ skill edit.
The rules
Section titled “The rules”- Store build number ==
Sim_Const.BUILD(from 207; store build 37 = BUILD 206 was the last on the old separate counter).scripts/bump-build.shis the ONLY writer ofBUILDand all 7 Apple presets’application/version— a hand edit ofsim/constants.gdfailstests/test_release_presets.gdat the gate. Device playtests install the separate Dark Cosmos Dev app (scripts/dev/install-dev.sh, bundle…bulletheaven.dev), never the store bundle id. Store uploads:scripts/release/release.sh(no arg), which tagsstore-N. Dev-only modes are runtimeDevFlags(Debug Settings, dev builds only), notmain.gdconsts. Full how-to:docs/architecture/dev-tools.md§ Build channels. Any player-visible merge to main adds abuild: nullitem to the Remote Console’s What’s new (control/src/whats_new.js);bump-build.shstamps it on main — commit it with the bump, regenerateremote-config/manifest/build-N.jsonat the same time, and redeploy the console Worker after merges touchingcontrol/: from the branch it was last deployed from (often atest-N-combined; check memory) with main merged in, as a plain main deploy drops that branch’s tunable docs. - A pre-push hook refuses any push to
mainwhose tree didn’t passscripts/gate.sh(2026-10-15, after agate.sh …; git pushchain pushed a GATE FAIL merge).gate.shstamps HEAD’s tree in<git common dir>/gate-ok/on a PASS from a clean tree; the hook (scripts/githooks/pre-push→scripts/lib/push-guard.mjs) accepts a push only when the pushed tree differs from a stamped tree (or from origin’s main) in doc paths (top-level*.md, anyCLAUDE.md/README.md/AGENTS.md,docs/,.claude/skills|agents/,.factory/; NOT other*.md, which GUT may read) and a consistentbump-build.shbump alone, which get a ~1 s light check. Strict: merging a movedorigin/mainin means re-gating — two separately-gated changes are never assumed to compose. Install once per clone, Toby’s included:scripts/dev/install-hooks.sh(git hooks don’t travel with the repo, and a GitHub web merge bypasses it).--no-verifyis emergency-only and goes in the commit message. - Prune finished worktrees with
scripts/dev/prune-worktrees.sh(dry run;--applyacts). It removes only worktrees whose HEAD is inorigin/main, clean, idle 24 h and with no process inside, and MOVES their non-cache ignored files (.superpowers/,marketing/raw,builds/, toolout/) to.claude/worktree-archive/<name>/first, becausegit worktree removedeletes ignored files silently. A SessionStart hook nags above 25 worktrees. What it keeps as “has commits not on origin/main” is not safe to ignore: run--triage(read-only) before removing any of those by hand. It labels eachin-main(merging changes nothing),inside:<wt>(an earlier stage of another branch),landed(every file it changed is on main at its version, now or earlier in main’s history: reached main by another commit, maybe edited since),picked:N/M(commit subjects already on main) orunique(real unmerged work: merge, park as an issue viascripts/dev/file-issue.sh, or drop), showsfiles:L/Non every row and lists the files a picked/unique row has NOT landed, and flagsLOCAL-ONLYwork no origin branch holds. Trustfiles:over commit matching:git cherryand subject matching miss content that landed via a different commit. Push a LOCAL-ONLY branch before removing its worktree: on 2026-10-16 a manual cleanup left 27 unmerged branches only on this Mac, invisible to Toby. - One session merges to main at a time. Several bullet-heaven sessions often run at once (2026-10-08: one merged
lan-jitteras BUILD 279 while this session was still gating the same branch, and a third had already used 280 on an unmerged branch). Before any merge to main orbump-build.sh, runListAgents, SendMessage the other bullet-heaven sessions to claim merge ownership, and have them hand over branch + reviewer verdict instead. A subagent cannot run git in any worktree but the one the Agent tool’sisolation: "worktree"made for it (harness-blocked): never pre-create one and point it there; it commits to its ownworktree-agent-*dir. - Unmerged branches burn BUILD numbers too (each device install bumps on its branch), so
maincan lag numbers already installed and in telemetry. Before a dev install frommain, check the highestBUILDon live branches (git show <branch>:sim/constants.gd) and bump past it (bump-build.shonly does +1, so loop it). Looping on main stamps What’s new with the FIRST bump’s number (thebuild: nullentry is gone after bump 1), so hand-fix that entry’sbuild:to the final N before committing (2026-10-12: stamped 296, shipped 307). Reusing a number mixes two builds’ telemetry (Chris, 2026-10-01: main went 236→242). It also breaks LAN co-op: the join handshake hashes the tunables registry + bible, so two devices on the “same” BUILD from different branches are rejected with “config mismatch” (2026-10-07). Also mergeorigin/maininto a long-lived branch before each device install from it: a branch cut before a main feature ships builds without it, and the Remote Console (deployed from main) then greys those controls out as “needs a newer game build” (fx-v2 BUILD 243/244 lacked Console v2). The Dev app is ONE slot on each device: when two branches each await Chris’s on-device verdict, build a throwawaytest-N-combinedbranch merging both (never merged to main) rather than alternating installs that overwrite each other. - tvOS and iOS both export on the patched Godot 4.8 P3 fork (
~/Claude/godot-tvos/godot-48, templates4.8.dev/{tvos,ios}.zip; Chris, 2026-10-12, after the BUILD 310 ATV verdict).install-dev.sh(BH_TVOS_GODOT/BH_IOS_GODOT) andbuild-{tvos,ios}.sh(GODOT_TVOS/BH_IOS_GODOT) default to it; the overrides are the 4.7 rollback (~/Claude/godot-tvos/godotfork for tvOS,/opt/homebrew/bin/godotfor iOS). The Xcode shell’slibgodot.amust be the exporting editor’s engine:scripts/lib/shell-engine.shrefuses a mismatch before the BUILD bump, and the refusal names the full-export recipe that regenerates the shell. release.sh/build-macos.shpre-flight what used to fail only after a burned build: a worktree release seeds the gitignoredplatform/tvos/build{,-ios}/inputs from the main checkout (seed_build_dirs), andasc.py check-versionrefuses ashort_versionApple already approved on a target platform (an approved version closes its train) or a platform with no App Store record. altool failures print every error (altool_run, full log beside the ipa). A new release-path script must resolve paths from its own location, never~/Claude/bullet-heaven(that patched the main checkout from a worktree, ITMS-90513).build-macos.shrunsasc.py mac-profile: the Mac build signs MANUALLY with a downloaded profile, which goes INVALID after any capability change ondigital.lumara.bulletheaven(App Groups for the clip, 2026-10-18); iOS/tvOS sign automatically and self-heal.scripts/gate.sh(andeconomy-careers.sh,scripts/dev/install-dev.sh) snapshotproject.godotat start and restore it at the end — a Godot-rewritten file is reverted loudly, and an intentional uncommitted edit survives the run (the oldgit checkoutsilently discarded it, 2026-09-28). A raw windowedgodot --path .run has no such guard and dirties BOTHproject.godotandaudio/bus_layout.tres— after running any tool page undertools/, ui-sweep or soak, checkgit statusand restore both before committing..claude/{agents,hooks,scripts,skills,workflows}/+.claude/settings.jsonARE tracked (the repo.gitignorere-includes them over the global.claude/ignore, which is for client repos only). Commit skill edits like code; the rest of.claude/(worktrees, scratch, local settings) stays ignored. Skill visibility lives in the tracked.claude/settings.json(skillOverridesfor skills/commands,enabledPluginsfor plugin skills, whichskillOverridescan’t reach), neversettings.local.json, which worktree sessions don’t load.scripts/test/skills-paths.test.mjsfails on any repo path,test_*.gdorbh-*name a skill/agent cites that no longer exists..factory/is GENERATED from.claude/— never hand-edit a file in it. Droid loads skills from.factory/skills/and custom droids from.factory/droids/, so thebh-*tooling is mirrored there. Edit.claude/skills/<name>/SKILL.md, runnode scripts/port-claude-to-factory.mjs, commit both sides —scripts/test/skills-parity.test.mjsregenerates and diffs on every gate run, so a direct.factory/edit fails, as does a NEW skill until it is classified into exactly one ofDISABLE_MODEL_INVOCATION/MODEL_INVOCABLEinscripts/lib/claude-to-factory.mjs. A skill must be<name>/SKILL.md; a bare.mdat the skills root is discovered by neither harness. The 4 banner-ed skills (bh-overnight-audit-loop,bh-overnight-feature-loop,bh-overnight-swarm,bh-cloud-offload) do NOT run under Droid — they need Claude-only primitives. Detail: memorybullet-heaven-local-skills-worktrees.- Run the per-build report card at each BUILD bump (
scripts/dev/report-card.sh, then--post): the whole headless suite (balance/feel/difficulty, soak, sim_bench, story_trace) diffed against the previous build, one summary posted to the tracker hub issue. WARN is a flag, never a block. Manual:docs/reference/fidelity-report-card.md(#115). - Script tests:
node --test scripts/test/*.test.mjsandtelemetry/test/*.test.mjs(the directory form fails MODULE_NOT_FOUND on Node 22).