Skip to content

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.

  • Store build number == Sim_Const.BUILD (from 207; store build 37 = BUILD 206 was the last on the old separate counter). scripts/bump-build.sh is the ONLY writer of BUILD and all 7 Apple presets’ application/version — a hand edit of sim/constants.gd fails tests/test_release_presets.gd at 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 tags store-N. Dev-only modes are runtime DevFlags (Debug Settings, dev builds only), not main.gd consts. Full how-to: docs/architecture/dev-tools.md § Build channels. Any player-visible merge to main adds a build: null item to the Remote Console’s What’s new (control/src/whats_new.js); bump-build.sh stamps it on main — commit it with the bump, regenerate remote-config/manifest/build-N.json at the same time, and redeploy the console Worker after merges touching control/: from the branch it was last deployed from (often a test-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 main whose tree didn’t pass scripts/gate.sh (2026-10-15, after a gate.sh …; git push chain pushed a GATE FAIL merge). gate.sh stamps 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, any CLAUDE.md/README.md/AGENTS.md, docs/, .claude/skills|agents/, .factory/; NOT other *.md, which GUT may read) and a consistent bump-build.sh bump alone, which get a ~1 s light check. Strict: merging a moved origin/main in 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-verify is emergency-only and goes in the commit message.
  • Prune finished worktrees with scripts/dev/prune-worktrees.sh (dry run; --apply acts). It removes only worktrees whose HEAD is in origin/main, clean, idle 24 h and with no process inside, and MOVES their non-cache ignored files (.superpowers/, marketing/raw, builds/, tool out/) to .claude/worktree-archive/<name>/ first, because git worktree remove deletes 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 each in-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) or unique (real unmerged work: merge, park as an issue via scripts/dev/file-issue.sh, or drop), shows files:L/N on every row and lists the files a picked/unique row has NOT landed, and flags LOCAL-ONLY work no origin branch holds. Trust files: over commit matching: git cherry and 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-jitter as 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 or bump-build.sh, run ListAgents, 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’s isolation: "worktree" made for it (harness-blocked): never pre-create one and point it there; it commits to its own worktree-agent-* dir.
  • Unmerged branches burn BUILD numbers too (each device install bumps on its branch), so main can lag numbers already installed and in telemetry. Before a dev install from main, check the highest BUILD on live branches (git show <branch>:sim/constants.gd) and bump past it (bump-build.sh only does +1, so loop it). Looping on main stamps What’s new with the FIRST bump’s number (the build: null entry is gone after bump 1), so hand-fix that entry’s build: 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 merge origin/main into 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 throwaway test-N-combined branch 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, templates 4.8.dev/{tvos,ios}.zip; Chris, 2026-10-12, after the BUILD 310 ATV verdict). install-dev.sh (BH_TVOS_GODOT / BH_IOS_GODOT) and build-{tvos,ios}.sh (GODOT_TVOS / BH_IOS_GODOT) default to it; the overrides are the 4.7 rollback (~/Claude/godot-tvos/godot fork for tvOS, /opt/homebrew/bin/godot for iOS). The Xcode shell’s libgodot.a must be the exporting editor’s engine: scripts/lib/shell-engine.sh refuses a mismatch before the BUILD bump, and the refusal names the full-export recipe that regenerates the shell.
  • release.sh / build-macos.sh pre-flight what used to fail only after a burned build: a worktree release seeds the gitignored platform/tvos/build{,-ios}/ inputs from the main checkout (seed_build_dirs), and asc.py check-version refuses a short_version Apple 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.sh runs asc.py mac-profile: the Mac build signs MANUALLY with a downloaded profile, which goes INVALID after any capability change on digital.lumara.bulletheaven (App Groups for the clip, 2026-10-18); iOS/tvOS sign automatically and self-heal.
  • scripts/gate.sh (and economy-careers.sh, scripts/dev/install-dev.sh) snapshot project.godot at start and restore it at the end — a Godot-rewritten file is reverted loudly, and an intentional uncommitted edit survives the run (the old git checkout silently discarded it, 2026-09-28). A raw windowed godot --path . run has no such guard and dirties BOTH project.godot and audio/bus_layout.tres — after running any tool page under tools/, ui-sweep or soak, check git status and restore both before committing.
  • .claude/{agents,hooks,scripts,skills,workflows}/ + .claude/settings.json ARE tracked (the repo .gitignore re-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 (skillOverrides for skills/commands, enabledPlugins for plugin skills, which skillOverrides can’t reach), never settings.local.json, which worktree sessions don’t load. scripts/test/skills-paths.test.mjs fails on any repo path, test_*.gd or bh-* 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 the bh-* tooling is mirrored there. Edit .claude/skills/<name>/SKILL.md, run node scripts/port-claude-to-factory.mjs, commit both sides — scripts/test/skills-parity.test.mjs regenerates and diffs on every gate run, so a direct .factory/ edit fails, as does a NEW skill until it is classified into exactly one of DISABLE_MODEL_INVOCATION / MODEL_INVOCABLE in scripts/lib/claude-to-factory.mjs. A skill must be <name>/SKILL.md; a bare .md at 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: memory bullet-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.mjs and telemetry/test/*.test.mjs (the directory form fails MODULE_NOT_FOUND on Node 22).