OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / .pear-git / ci.yaml

1758 lines · yaml ref:
steps:
  - name: write-version
    run: |
      # CI runs in a `git archive | tar -x` extraction with no .git/
      # directory, so `git rev-list` returns empty here. The orchestrator
      # injects git metadata via env vars instead — use those.
      COUNT="${GIT_COMMIT_COUNT:-0}"
      SHORT="${GIT_COMMIT_SHORT:-unknown}"
      BUILD_TS=$(date -u +%Y%m%d-%H%M)
      echo "v${COUNT}-${BUILD_TS}-${SHORT}" > version.txt
      sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"0.${COUNT}.0\"/" package.json
  - name: overlay-sync
    # The git archive contains pointer files where binary assets used to
    # be (hyper-overlay's git-LFS-style routing). Pull real bytes from
    # the publisher's pgh-sidecar before godot imports.
    #
    # 108 pointer files at ~1MB/s aggregate (sequential walk, per-file
    # Hyperswarm setup overhead) → ~10 min minimum, often more on cold
    # connection setup. Bump to 30 min until pgh overlay sync gains
    # parallel fetch (filed separately).
    run: pgh overlay sync
    timeout: 1800
  - name: import-godot
    # Engine is nvidia-rtx-godot-v3 @ linux-rtx-v3-dev, at 4480abf76b (4.8.dev + upstream
    # #99404 VisualShader node groups). Editor 15:14, linux template 15:23, windows
    # template 15:36 on 2026-09-29, all rebuilt from that commit.
    # Since b7b5b330f3 that adds, in order: GODOT_NGX_NO_DLSS=1 as a runtime DLSS
    # kill switch, use_ngx_dlss defaulting ON, the DLSS-RR sky albedo guide
    # tone-map (the sun disc was drawn as a black square by the demodulation
    # seam), and explicit NGX render presets (RR=D, SR=K -- the Linux path set
    # NONE before, so presets silently did nothing while they worked on Windows).
    # ⚠ The TEMPLATES are what the shipped game runs, not the editor. Rebuilt
    # 2026-09-21: linux 16:49, windows 16:54, both after the last engine edit.
    # A stale template ships old behaviour while every check here still passes.
    # ⛔ VERIFY THE WINDOWS TEMPLATE MTIME, do not trust scons' exit code: on
    # this rebuild scons recompiled the changed objects, relinked BOTH static
    # libraries, printed "done building targets" and exited 0 -- without ever
    # linking godot.windows.template_release.x86_64.exe. The .exe stayed 22
    # minutes older than the source it supposedly contained. Deleting the .exe
    # forces the link. It only ships on v* tags, so a stale one hides until a
    # tagged release.
    # NOTE THE DIFFERENT CHECKOUT: nvidia-rtx-godot-v3/, not nvidia-rtx-godot/.
    # The v2 tree (Godot 4.7-beta, linux-rtx-v2-dev) still exists but nothing in
    # this file uses it any more -- Linux AND Windows both build from v3.
    #
    # The BLAS fix is still load-bearing for RT in RELEASE builds -- max_index is
    # populated only under DEBUG_ENABLED, so template_release stores an "unknown"
    # sentinel and blas_create's unconditional assert failed EVERY indexed mesh,
    # and editor builds never reproduce it (do not "verify" an RT change in the
    # editor). But on THIS engine it no longer comes from our reapplied commit:
    # NVIDIA landed the same fix independently in the 4.8 base, spelled
    # 0xFFFFFFFFu rather than our 0xFFFFFFFF, so our three BLAS commits
    # (fix + revert + reapply) were dropped from the port as redundant. Grepping
    # for one spelling will falsely report the other missing.
    # Verified on this engine 2026-09-21: a template_release export path-traces a
    # scene with geometry and a cast shadow, i.e. the release-build failure this
    # paragraph exists to prevent does not occur.
    #
    # NOTE: the engine binaries are untracked build artifacts, so this comment is the
    # ONLY record of which engine built a release. Update it when you rebuild.
    #
    # Both hyper-games #10 blockers are FIXED as of this engine (do not re-diagnose
    # them from a stale comment — this one said they were open for a week after they
    # were closed):
    #   - the RT bindless table now has typed texture2DArray/3D/Cube/CubeArray
    #     entries, so sampler2DArray materials (liquid_warp, dc_level) compile
    #   - build_tlas's null uniform set is guarded by uniform_set_is_valid() before
    #     the trace dispatch, instead of binding it and crashing
    # Path tracing is exposed in settings and defaults OFF; Render Scale defaults 100%.
    #
    # godot-rtx (our NVIDIA-RTX/godot fork), NOT the system godot4 (4.6.stable).
    # The fork is now Godot 4.8-dev, rebuilt on NVIDIA-RTX/godot's own 4.8 release
    # branch (nvidia-pt-dlss @ 135dff3887) with our 28 commits replayed on top --
    # the Linux NGX DLSS backend, the RT correctness fixes, and the Vulkan
    # device-creation guards. It still provides AreaLight3D (4.7+, PR #108219) and
    # Environment.pathtracing_enabled.
    # project.godot still declares config/features=["4.7"]. That is cosmetically
    # stale, not load-bearing: a 4.8 run does not rewrite it, and the full gate
    # suite passes on this engine (27/28, the one failure -- gate-nav-end-to-end --
    # fails identically on the 4.7 engine and is unrelated to the port).
    # All five GDExtensions load unmodified; their compatibility_minimum is 4.3/4.4,
    # so no godot-cpp rebuild was needed.
    # Absolute path required: CI runs from a `git archive` extraction with no PATH
    # guarantees, which is why every other host path in this file is absolute too.
    #
    # ⛔ `|| true` is for the ERRORS a first import reports about assets it is still
    # generating -- NOT for the engine dying. This engine's headless import aborts
    # intermittently (SIGABRT in a worker thread right after the benchmark CSVs:
    # 1 of 3 fresh imports of 17fde44 on 2026-09-29; the other 2 exited 0). The
    # swallowed crash left assets unimported and gate-map-wiring failed 30 steps
    # later on "non-existent resource", which reads as a broken map, not a crash.
    # So: exit >= 128 (killed by a signal) is retried once, and a second crash fails
    # HERE, naming itself.
    run: |
      GODOT=/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64
      for attempt in 1 2; do
        "$GODOT" --headless --import; rc=$?
        [ "$rc" -lt 128 ] && exit 0
        echo "import-godot: the engine was killed by signal $((rc - 128)) (attempt $attempt)"
      done
      echo "ERROR: headless import crashed twice -- see the backtrace above"; exit 1
    timeout: 1200
  - name: gate-proc-graphs
    # Every res://graphs/*.tres must load with all nodes present and bake to a
    # mesh. graphs/terrain.tres shipped from 2026-03-27 to 2026-09-04 with
    # `nodes` holding null in slot 3 while `connections` still routed two links
    # into node 3 — it loaded, looked right in the inspector, and evaluated to
    # nothing. A null node is not an error to Godot: ProcNodeGraph::_set("nodes")
    # pushes whatever the array holds, so only baking catches it.
    #
    # HERE, and not later: this is the first step at which the project is
    # imported and the GDExtensions are loaded, and it is upstream of the
    # ~10-minute export. A broken graph aborts the deploy before that cost, and
    # before pgh-publish, so the live version is untouched.
    #
    # --headless is correct for this one (unlike export-godot, which needs a
    # real Vulkan device for the shader baker): the gate sets use_gpu = false
    # and bakes on the CPU path, which is the path being verified.
    #
    # No `|| true`. import-godot swallows its status because a first import
    # reports errors for assets it is in the middle of generating; this step has
    # no such excuse and is meant to stop the build.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scripts/gate_terrain_graph.gd"
    timeout: 300
  - name: gate-containment
    # An entity must die with its container. "Survive" means three things declared
    # in three different places -- statebase keys, scene-tree node, and whoever runs
    # the brain -- and this checks all three against ONE map change.
    #
    # It guards a regression that already shipped once: creature visuals were
    # parented to /root/Game while their listener lived in the map scene, so they
    # outlived every map change AND became unreclaimable, because _creatures (the
    # only handle) died with the listener while _remove_visual stayed the sole path
    # that frees one. The statebase rows were map-scoped the whole time -- scoping
    # controls the key NAME, not node lifetime. See spec/bot-testbed.md (e1).
    #
    # Cheap to run (no GPU, no import of a terrain map) and it fails loudly, so it
    # belongs in front of every deploy rather than in a manual checklist.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_containment.gd"
    timeout: 300
  - name: gate-bot-registry
    # Containment must stay DERIVED from one declaration rather than remembered in
    # three places. Most of what this guards is the REFUSALS: an unknown binding
    # silently means "this container never tears me down", which is the creature
    # leak returning through the front door, and a body we cannot build must not
    # register as though we could.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_bot_registry.gd"
    timeout: 300
  - name: gate-bot-end-to-end
    # The pieces are unit-covered; this is the only thing asserting they COMPOSE --
    # registering a bot compiles and runs a brain, the brain's writes reach shared
    # state, and a map change tears the whole chain down INCLUDING the state the bot
    # claimed. That last assertion is the one worth the build time: containment on
    # the roster is easy, containment on the consequence is what the creature leak
    # actually violated.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_bot_end_to_end.gd"
    timeout: 300
  - name: gate-input-sync-root
    # player_procedural.tscn's InputSynchronizer shipped with NO root_path, so it
    # defaulted to ".." -- the InputSync node it hangs under -- while its config
    # addressed "InputSync:input_direction". It therefore looked for a child named
    # InputSync INSIDE InputSync, failed, and logged every frame for as long as a
    # peer was connected. player.tscn had the line and was fine, so the defect only
    # showed with use_procedural_player, which is to say always.
    #
    # A missing line in a .tscn is invisible to every other check we have: the
    # scene loads, the game runs, and the only symptom is console spam that looks
    # like a netcode problem.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_input_sync_root.gd"
    timeout: 300
  - name: gate-reconnect-rearm
    # A peer that restarts must be able to dial the room again. It could not:
    # public keys were learned ONLY in _on_peer_connected, held in the in-memory
    # _peer_keys, and cleared by reset_for_rejoin, so swarm_join_peer was
    # reachable only for a peer already met this session. After a restart there
    # was nothing to call it with.
    #
    # The retry was never the missing part -- SwarmManager::_select_timer sends
    # EXPLICIT peers to the long retry timer for ever and drops non-explicit ones,
    # and swarm_join_peer is what marks a peer explicit. So the fix publishes the
    # stable DHT key into durable presence and re-arms from there, which also lets
    # a peer dial members it has NEVER met.
    #
    # Checks the key survives into the presence entry and that game.gd re-arms
    # from somewhere other than the disconnect handler. Unlike its neighbours this
    # gate exits non-zero on failure.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_reconnect_rearm.gd"
    timeout: 300
  - name: gate-script-host-res
    # GDScript exports as .gdc binary tokens, so ScriptHost's FileAccess+compile
    # read of a res:// path returned error 7 for every shipped brain in an
    # exported build. bots/*.gd reached the pck in 0.1141.0 and still could not
    # run. hyper-games #77 item 3.
    #
    # ⛔ The real condition exists ONLY in an exported build: from the source
    # tree res://bots/*.gd are ordinary .gd files, so the functional half of this
    # gate passes with or without the fix. The structural half -- that run_file
    # has a res:// branch using load() -- is what actually guards it, and it
    # carries a self-check. Read the gate's header before weakening it.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_script_host_res.gd"
    timeout: 300
  - name: gate-headless-map
    # A headless peer's map was a hardcoded branch loading map_server, and
    # spec/bot-testbed.md (e1) lists "a map argument for headless, so a peer-bot's
    # map is a declaration rather than a hardcoded branch" as an unbuilt
    # obligation of the containment work.
    #
    # The second defect in the same six lines was silent and worse:
    # current_map_path was never updated, so GameState.current_map_id was seeded
    # from a map that was NOT loaded, and every map-scoped key on a headless peer
    # -- world:bots: included -- was namespaced to the wrong map. Bot containment
    # is map-scoped, so it landed on the peer the bots run on.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_headless_map.gd"
    timeout: 300
  - name: gate-admin-cmd-applied
    # admin:cmd: is a DURABLE route, so the room replicates every command ever
    # issued, and _seen_seqs (the re-dispatch guard) is in-memory and empty in a
    # fresh process. Whoever became authority on join re-executed the entire
    # history -- ashatron replayed stortron's walker and probe commands against
    # files that only exist on stortron. hyper-games #77 item 5.
    #
    # ⛔ The gate's seq fixtures are copied VERBATIM from the live room. A TTL
    # was implemented first and covered 0 of the 9 real commands, while passing
    # a gate written with synthetic TUI-shaped seqs. Do not replace them with
    # invented ones.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_admin_cmd_ttl.gd"
    timeout: 300
  - name: gate-bot-call-scope
    # BotRegistry declared every admin method "local", so every peer that saw the
    # admin:cmd: ran it. A body=none brain driving the local player therefore
    # walked EVERY peer's avatar and took every player's keyboard. It was wrong
    # for body=position too: remote peers already build the marker from the
    # replicated rows, so running register per-peer starts a second simulator for
    # one bot. hyper-games #77 item 6.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_bot_call_scope.gd"
    timeout: 300
  - name: gate-bot-key-scope
    # world:bots: is map-scoped unconditionally, so a room- or session-bound bot
    # got a MAP-SCOPED durable row: its node survives a map change while the row
    # announcing it is dropped with the map. Binding and namespace disagreeing for
    # exactly the non-map bindings — the three-lifetimes problem spec/bot-testbed.md
    # (e2) exists to prevent, in the one case it did not cover.
    #
    # Found by standing up the bot matrix in a live room, not by a gate.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_bot_key_scope.gd"
    timeout: 300
  - name: gate-bot-marker
    # Every position body was the same grey box, so a room of bots was a room of
    # identical boxes and the testbed could not be read by looking at it. Colour
    # carries the containment binding (what a map change is meant to prove),
    # emission carries "a brain is running", shape derives from the body, and a
    # Label3D spells out the declaration.
    #
    # ⚠ Arity is checked before calling _make_marker: has_method() is true for the
    # old one-argument version, and calling it with four raises a SCRIPT ERROR
    # that aborts the gate before any FAIL is recorded — which is exactly how the
    # first run of this gate printed PASS against a registry with none of it.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_bot_marker.gd"
    timeout: 300
  - name: gate-bot-spawn-spread
    # register() wrote [0,0,0] for every position body and never touched the
    # node's transform, so bots with brain=none stacked into one box at the
    # container origin and the whole matrix read as a single bot.
    #
    # The offset is derived from the id so every peer agrees without a round trip:
    # a remote peer builds the marker BEFORE any position row arrives, and if the
    # two sides chose independently the body would jump when the first row landed.
    #
    # This gate earned its place twice while being written: it caught a ring
    # placement putting two ids 0.31 apart, and then an exact cell collision
    # between "a" and "b" from taking both lattice axes out of FNV's low bits.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_bot_spawn_spread.gd"
    timeout: 300
  - name: gate-objects-without-level
    # refold() returned {} before touching `objects` whenever there was no DCLevel,
    # conflating "this map has no brush level" with "there is nothing to fold". It
    # never showed because map_dc_level is the only map that instantiates
    # SceneObjects and it also has a DCLevel. Bringing the object system to
    # map_flat -- a bare ground plane -- makes every object.create journal,
    # replicate, and then silently never build, on every peer.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_objects_without_level.gd"
    timeout: 300
  - name: gate-bevelled-block
    # The block a builder bot stacks: 10 x 10 x 20, chamfered, and ONE ArrayMesh
    # shared by every instance. "Instanced" is the load-bearing word — building the
    # mesh per instance looks identical on screen and is a copy per block in
    # memory, which is invisible without asking.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_bevelled_block.gd"
    timeout: 300
  - name: gate-map-wiring
    # Every map loads, every teleporter's target_map names a map that EXISTS,
    # and no two pads in a map overlap. target_map is a STRING: a renamed or
    # moved map breaks nothing until a player walks into the pad. Also pins the
    # rule that map_flat carries no DCTerrain -- a second child answering
    # nav_find_path with a disjoint graph is what left the bots inert.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_map_wiring.gd"
    timeout: 180

  - name: gate-settings-dump
    # Ctrl+Shift+F5's SettingsDump must report exactly what was tuned at runtime --
    # a node property and a property INSIDE a resource, each with the file to edit
    # -- and nothing untouched. A cached load of the scene would return the very
    # instance that was edited and report no changes at all.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_settings_dump.gd"
    timeout: 180

  - name: gate-map-flat-objects
    # map_dc_level was the only map that instantiated SceneObjects, so "building"
    # and "the Quake brush level" were one thing by accident of where the wiring
    # lived. Instances the REAL map_flat.tscn: the wiring under test is in that
    # scene's script, so a fixture would assert against itself.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_map_flat_objects.gd"
    timeout: 300
  - name: gate-builder-bot
    # The builder stacks one block per interval, each a course above the last, and
    # STOPS at its bound — an unbounded builder writing a durable journal is a leak
    # that replays on every join, not a testbed.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_builder_bot.gd"
    timeout: 300
  - name: gate-brain-params
    # BotRegistry handed ScriptHost exactly {"bot_id": id}, so every published
    # brain was hard-coded and changing a builder's block count meant editing the
    # brain rather than the registration. The brain dictionary gains `params`.
    #
    # bot_id is applied LAST and wins: params arrive from an admin:cmd, so a
    # caller could otherwise pass {"bot_id": "someone-else"} and have the brain
    # act as another bot, since move() and despawn() are keyed on it.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_brain_params.gd"
    timeout: 300
  - name: gate-chat-names
    # keet-daemon's `read` is flat {memberId, name, text}; the overlay read
    # msg.sender.displayName and showed "?" for every received line, and echoed
    # its own sends under a hard-coded "Player" that the poll then duplicated.
    # A nameless member reads "anon-<4 of member id>", never "?", "Player" or a
    # key fragment (the daemon still fills a missing name with the id prefix).
    # HYPERTPS_DAEMON_TCP points KeetService at a dead port: no daemon needed. (#89)
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_chat_names.gd"
    timeout: 300
  - name: gate-purge-confirm
    # The room list's "Purge" called room.purge (= core.leaveRoom: rejoining
    # needs a new invite) on the first click. It now opens a confirmation that
    # names the room; only OK purges, cancel does nothing. KeetService.room_mgr
    # is a recording stub, so no daemon is touched. (#93 §1)
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_purge_confirm.gd"
    timeout: 300
  - name: gate-home-button
    # Resume is now HOME: this identity's own keet room (keet-daemon home.get,
    # docs/home-world-design.md §3). Button states (waiting / daemon down /
    # unavailable with the error / Home), the home room on the OFFLINE peer with
    # no mesh, leaving home ending that session, no Purge for home.
    # KeetService.room_mgr is a recording stub; no daemon or network is touched.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_home_button.gd"
    timeout: 300
  - name: gate-save-banner
    # With no keet-daemon, edits live only in GameState's memory and are lost on
    # quit. The HUD now says so while a room (Home included) is selected, and
    # names a durable write that was refused. Signals emitted by hand; no daemon.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_save_banner.gd"
    timeout: 300
  - name: gate-home-starter
    # A release's starter map (res://home/<map>.home.json, made by the dev's
    # tools/release-home-map.sh) is copied into a player's HOME once per map:
    # positions/undone dropped, only absent keys written, the marker only when
    # every write landed, deferred until the role (admin at Home) is known, never
    # into another room. docs/home-world-design.md §6.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_home_starter.gd"
    timeout: 300
  - name: gate-param-shapes
    # Parametric shapes (docs/parametric-shapes-design.md): stairs / spiral stairs /
    # tunnel generators yield pieces the REAL brush store accepts (control: an
    # inside-out box is refused), and the fold turns shape.* records into brushes —
    # deterministic, per-param last-write-wins, xform, Make Real (detach), undo.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_param_shapes.gd"
    timeout: 300
  - name: gate-shape-journal
    # The same shapes through the REAL journal: persist.record(shape.*) ->
    # _to_record -> ShapeFold -> DCLevel. Catches a kind/field _to_record drops,
    # and the by-name restore trap (pieces must be renamed when the shape changes).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_shape_journal.tscn"
    timeout: 300
  - name: gate-identity-flow
    # In-game keet identity (#89 §3.2/§5): an unnamed identity gets a required
    # first-run step (name + vault password) → vault.setup, identity.setName;
    # Back up / change password, Recovery phrase, Restore (identity-manager.import
    # — a NEW identity, the current one kept; never vault.import), rename on
    # Enter. A headless run is never blocked. Scripted stub client, no daemon.
    # Live counterpart: scenes/test/_probe_identity_flow_live.gd (real daemon
    # over TCP, keet-daemon test/harness-tcp-scratch.mjs).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_identity_flow.gd"
    timeout: 300
  - name: gate-nameplate
    # Nameplates show the VERIFIED keet member's ROSTER name (#89 §3.3): each
    # peer proves its mesh key belongs to its keet member (daemon identity.attest),
    # the receiver verifies against the handshake-authenticated key
    # (identity.verify) — sent per connection, so late joiners get it; forged and
    # replayed proofs stay anon; renames follow the roster. The any_peer
    # _set_player_name RPC, player:name: and the "Player" default are gone. Bots
    # bound to a peer read "<peer>.bot", others "<id>.bot". Stub daemon; live
    # counterpart: scenes/test/_probe_peer_identity_live.gd (two real daemons).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_nameplate.gd"
    timeout: 300
  - name: gate-graphics-presets
    # Settings: a Low / High preset row (an action — writes its keys as overrides,
    # reads back only while all match; never stored) and a Volumetric Clouds
    # switch that toggles SunshineCloudsGD.enabled, never forcing on an effect the
    # addon disabled itself (#93 §2). Measured on a 2070: clouds on 9.5 ms GPU,
    # off 0.2 ms (scenes/test/_probe_clouds_switch_cost.gd, needs a display).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_graphics_presets.gd"
    timeout: 300
  - name: gate-mountain-stats-worst
    # [MountainStats] worst= took max(delta), and the engine clamps delta to
    # max_physics_steps_per_frame × physics step (~133 ms): a 4117 ms stall printed
    # 135 ms. It now uses the wall clock, like the hitch logger. (#93 §2)
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_mountain_stats_worst.gd"
    timeout: 300
  - name: gate-brush-maps
    # Every demo map builds brushes (user decision 2026-10-02): Mountain, the hub
    # (map_test), Erosion and Flat all run map_dc_level with level_import off -- an
    # empty brush level -- and carry a DCLevel; in the tree, each journal folds into
    # its own DCLevel. Flat's own journal/objects pair became the inherited one.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_brush_maps.gd"
    timeout: 300
  - name: gate-terrain-panel-restore
    # Saved / remote / map-change terrain knobs reach the GENERATOR on generator
    # maps. The panel saved through `_target` (the generator) but restored onto the
    # DCTerrain node, which has none of those knobs: Erosion's terrain settings
    # moved the slider and never reached the world. Fails if < 6 checks ran.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_terrain_panel_restore.gd"
    timeout: 300
  - name: gate-journal-texture
    # Texture intent through the REAL journal path (#94 P1): record -> GameState ->
    # LevelPersistence._to_record -> DCLevel decode -> fold -> get_brush and the
    # derived projection table (import rows first, pack layers, missing layer,
    # tile size, dedup). Also guards brush.create's clearance + group, which
    # _to_record silently dropped until 2026-10-02 (refolds made clearance volumes
    # solid). Scene gate: level_persistence.gd names GameState at compile time.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/journal_texture_gate.tscn"
    timeout: 300
  - name: gate-operators
    # The operator contract (nothing written before confirm, escape first-wins,
    # switching tools cancels) and every shape the box tool makes. Since #94 P1
    # step 4 also gate 6: a new box carries the map's CURRENT texture on every
    # plane, in the record AND on the local brush; arch clearance stays untextured.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/operator_gate.tscn"
    timeout: 300
  - name: gate-editor-e0
    # Editor redesign E0 (docs/editor-redesign.md §1-2): a focused text field is an
    # exclusive `text_input` context (no builder key fires, movement stops reading
    # the keyboard); OperatorStack pushes `tool`, and while it is up without RMB the
    # REAL fly camera ignores Space/Ctrl/Shift (measured: rises 0 m vs ~7 m); one
    # Shortcut per action shows the live key; Keymap.run reaches the same dispatch.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_editor_e0.gd"
    timeout: 300
  - name: gate-editor-e1
    # Editor redesign E1 on a real map_flat: four collapsible regions (top / bottom /
    # left / right, ▸/▾ each, saved per player), the top bar (menus generated from the
    # keymap with live accelerators, select modes, grid, texture lock — all acting via
    # Keymap.run, read back from the map), the toolbar (tooltips = label + live key) and
    # the generated status bar (idle hints, tool-up hints, grid / selection).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_editor_e1.gd"
    timeout: 300
  - name: gate-editor-e2
    # Editor redesign E2 (snapping) on a real map_flat: grid 0.25-256u; snap toggle
    # (top-bar magnet + Shift+Tab), OFF = 1-unit steps; while a tool is up Ctrl
    # inverts and Shift = 1/8 — and only then; operators and placement snaps read
    # the effective units; units u|m drive the grid dropdown and the status bar.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_editor_e2.gd"
    timeout: 300
  - name: gate-editor-e3
    # Editor redesign E3 helpers on a real map_flat. E3a: the F3 command palette lists
    # every action reachable now (from the keymap: label, menu, live key), fuzzy search,
    # Enter runs it through Keymap.run (the key path), Escape closes, recents remembered.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_editor_e3.gd"
    timeout: 300
  - name: gate-uv-controls
    # UV controls (#94 §9, docs/uv-controls-design.md) on a real map_flat: fit / justify /
    # align / nudge / mode / freeze / reset each change only their masked fields, land
    # locally AND in the journal (refold agrees), brush selections group records; the
    # face_sel keys through the real handler; the Face panel's buttons and read-outs.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_uv_controls.gd"
    timeout: 300
  - name: gate-hotspot
    # Hotspot faces (#94 P2): Fit rect picks a fitting strip of the composed industrial
    # sheet, re-roll cycles the near-equal strips, pin holds through re-rolls AND a refold
    # (level_persistence keeps rect/seed), textures without rects are skipped, panel buttons.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_hotspot.gd"
    timeout: 300
  - name: gate-selection-outline
    # Selection is outlined, not washed: no fill for brushes, active #ffa028 3 px vs other
    # selected #ed5700 2 px with faint hidden copies, face outline + depth-tested tint, grey wireframe.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_selection_outline.gd"
    timeout: 300
  - name: gate-move-proxy
    # Move shows the actual brush: hidden + proxy mesh with the level material, proxy and
    # outline follow the drag, texture lock shifts the projection, confirm/cancel restore.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_move_proxy.gd"
    timeout: 300
  - name: gate-rotate
    # R turns the selection about its union centre: ] 15° / Shift 1°, proxies + outline
    # turn with it, one brush.rotate per brush about ONE pivot, refold agrees, cancel restores.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_rotate.gd"
    timeout: 300
  - name: gate-editor-e4
    # Tool settings row generated from tool_settings(): every tool has a page, keys show in
    # it, controls drive the tool, typed values hold, sticky prefs, readout, plugin shape.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_editor_e4.gd"
    timeout: 300
  - name: gate-editor-e5
    # Gestures: a stairs / group rotate undo+redo in ONE press; Adjust Last re-runs as a new
    # gesture; a new edit discards the redo branch; History lists + seeks gestures.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_editor_e5.gd"
    timeout: 300
  - name: gate-scene-objects
    # The object fold: create / transform / set_prop. set_prop's names and values come
    # from peers, so the catalogue allow-list, clamping, per-prop last-write-wins and
    # undo-to-default are all asserted (and each prefab's defaults against its .tscn).
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/scene_objects_gate.gd"
    timeout: 300
  - name: gate-object-panel
    # Object panel on a real map_flat: one record per change ("Set <Label>"), slider drags
    # preview without writing, undo takes it back, every widget kind reaches the node.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_object_panel.gd"
    timeout: 300
  - name: gate-glb-check
    # A peer's .glb is checked BEFORE Godot parses it: no uris (GLTFDocument reads local
    # disk through them), node / texture / triangle limits, image sizes from headers.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_glb_check.gd"
    timeout: 300
  - name: gate-admin-role
    # Admin commands run with the AUTHOR'S keet room role (admin:cmd:<memberId>:<seq>),
    # never a role the command claims. Interim: the daemon-side writer check is the
    # hyper-games #92 follow-up.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_admin_role.gd"
    timeout: 300
  - name: gate-shapes
    # Create-tool shapes (#31 BasicShapes): cylinder / cone / sphere are ONE convex brush
    # each, standing on the aimed surface with the requested footprint.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_shapes.gd"
    timeout: 300
  - name: gate-editor-grid
    # The active grid on the aimed surface (#31 GridRenderer): world-lattice lines, rebuilt
    # per cell, Level editor only, apostrophe toggles the per-player pref.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_editor_grid.gd"
    timeout: 300
  - name: gate-scale
    # The scale tool (O): grid / ratio steps about the base or centre, axis lock, one
    # brush.scale per brush about ONE pivot; models scale uniformly via their Scale prop.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_scale.gd"
    timeout: 300
  - name: gate-undersea-geiss-graph
    # Our Geiss-style undersea density graph (§4 P1): compiles to GLSL, the bridged scene
    # blender/undersea_geiss.tscn holds a graph that compiles and carries every knob,
    # untouched above the coast band, altitude coast mask, floors at random 10-100 m
    # spacings, Geiss term present below, detail_of cuts exactly G, install_detail works.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_undersea_geiss_graph.gd"
    timeout: 300
  - name: gate-undersea-gpu-parity
    # The undersea field as dc-terrain runs it on the GPU (bounded generator ->
    # proc_density) equals the graph sampled on the CPU, at three blocks; also the
    # sign (positive = solid). WINDOWED like export-godot: needs a RenderingDevice.
    run: "DISPLAY=:0 HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --path . --script res://scenes/test/gate_undersea_gpu_parity.gd"
    timeout: 300
  - name: gate-undersea-erosion
    # Our undersea term inside the erosion / diffusion generator on the REAL maps:
    # compiles, the combined shader runs (two graphs in one shader — needs proc-nodes'
    # guarded globals), nothing changes above the coast band, the term is present below
    # it where there is sea (diffusion has none, by design). WINDOWED: needs a device.
    run: "DISPLAY=:0 HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --path . --script res://scenes/test/gate_undersea_erosion.gd"
    timeout: 600
  - name: gate-glsl-publish-race
    # A generator recompile never hands the GPU thread a half-built shader: two
    # recompiles per frame for 150 frames while the erosion map streams; zero empty
    # bodies and zero density compile failures (DCTerrain.get_gpu_shader_stats).
    # Red before dc-terrain cd3d776 (3 empty, 2 failed). WINDOWED: needs a device.
    run: "DISPLAY=:0 HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --path . --script res://scenes/test/gate_glsl_publish_race.gd"
    timeout: 600
  - name: gate-undersea-bridge
    # The undersea scene in a REAL blender-bridge session, Godot leading (what "Open in
    # Blender" starts): the scene crosses as a density preview with its 22 knobs, the
    # preview meshes the seabed, a knob tuned on its Blender node reaches Godot's live
    # graph and nothing else changes. Needs Blender 5.2 + the blender-bridge addon on
    # this host; SKIPs without them.
    run: "/home/fleeky/3d-prog/blender/blender-5.2.0-linux-x64/blender --background --factory-startup --python scripts/gate_undersea_bridge.py"
    timeout: 600
  - name: gate-state-routes
    # The route table: every row complete, the derived map/author lists exactly as
    # declared, keys scope and attribute per their row. Was not in CI, so the admin:cmd:
    # author row (2026-10-04) left it red unnoticed.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/state_routes_gate.gd"
    timeout: 300
  - name: gate-ghost-creatures
    # Ghost bots (demo sprint §5) on the REAL wiring (game.tscn + change_map), creatures
    # as bots (#114 phase 5): body inside the map, no authority twin, a declared row
    # revived (not frozen), hand-off on losing / taking authority, nothing left after a
    # map change, and a bot row held for the old map is not revived in the new one.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_ghost_creatures.gd"
    timeout: 300
  - name: gate-no-default-creatures
    # No shipped map spawns creatures (user 2026-10-13: a room adds them as placed bots);
    # an older session's world:creatures: rows draw nothing (kept, not deleted).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_no_default_creatures.gd"
    timeout: 300
  - name: gate-bot-tiers
    # Creatures → bots phase 1 (#114): a placed bot's body is ticked by BotRegistry's one
    # scheduler — near ticks, every observer far = asleep (zero ticks), body swaps clean.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_bot_tiers.gd"
    timeout: 300
  - name: gate-roam-brain
    # Phase 2: the roam brain (default for a placed bot) publishes targets within its
    # radius of home, moving on every pause; its settings reach it.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_roam_brain.gd"
    timeout: 300
  - name: gate-bot-creature-body
    # Phase 3: registered bots with crawler / hopper / serpent bodies — placed at their
    # row's position, scheduled, steered, revived, rendered remotely, freed unscheduled.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_bot_creature_body.gd"
    timeout: 300
  - name: gate-spawner-object
    # Phase 4: the placeable spawner keeps roaming creature bots near players on the
    # authority, despawns far ones, retires its left-overs, and its bots go with it.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_spawner_object.gd"
    timeout: 300
  - name: gate-object-parent
    # Reparenting (object.parent): parent-local transforms, cycles / bot children refused,
    # delete takes children, Ctrl+P / Alt+P, a selected child moves once, Outliner nesting
    # + drop, undoing the parent's create keeps the child.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_object_parent.gd"
    timeout: 300
  - name: gate-creature-target
    # Bot bodies: crawler / hopper / serpent walk (fly) to a TARGET with their own gait
    # and stop / circle there; far targets teleport; no target = wander as before.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_creature_target.gd"
    timeout: 300
  - name: gate-bot-object
    # Bots as scene objects: attach / brain on the authority, settings restart it,
    # move -> body, render-peer steering, delete/undo (incl. same frame), allow-listed
    # brains, the same creature on every peer.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_bot_object.gd"
    timeout: 300
  - name: gate-bot-editor
    # The Level / Bot editor tabs: workspace panels, Place bot as one gesture, the bot
    # list, brush keys ignored in the Bot editor (with a level control).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_bot_editor.gd"
    timeout: 300
  - name: gate-object-pick
    # Click selects scene OBJECTS (lights have no collider: bounds pick, nearer wins) and
    # the Outliner follows; a brush pick drops the object (G used to move a stale one).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_object_pick.gd"
    timeout: 300
  - name: gate-gltf-packer
    # .gltf -> one .glb at import: side files packed, nothing read outside the model's
    # folder ('..', absolute paths, URLs, symlinks refused). Peers only ever see .glb.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_gltf_packer.gd"
    timeout: 300
  - name: gate-model-collision
    # The editor's -col / -convcol / -colonly / -convcolonly rules, ported, and the
    # convex / trimesh / decompose (VHACD) / named / none modes with scale baked in.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_model_collision.gd"
    timeout: 300
  - name: gate-model-object
    # ModelCache + the model prefab through the fold: shared meshes, refused files say
    # why, placeholder -> model, collision layers, per-copy -colonly removal.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_model_object.gd"
    timeout: 300
  - name: gate-model-import
    # Import -> content cache -> room library -> placed as one gesture; Place from the
    # library; a refused file publishes nothing; the frame loop alone finishes a load.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_model_import.gd"
    timeout: 300
  - name: gate-bot-smoothing
    # Bot markers glide to published positions (first row / > 5 m snap, bounded extrapolation).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_bot_smoothing.gd"
    timeout: 300
  - name: gate-settings-panel
    # The Settings panel is usable at 1280x720: panel + Apply/Cancel on screen (they
    # were off screen before the redesign), every SCHEMA row on a tab with its widget,
    # real value names, collapsed groups below the visible rows, RTX rows greyed while
    # path tracing is off, Custom preset state, help on focus, widgets write Settings.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_settings_panel.gd"
    timeout: 300
  - name: gate-keymap
    # The keymap layer's own rules: every action registered, no two actions in one
    # context share a key, plain keys don't fire under a modifier, inactive contexts
    # dispatch nothing, rebinds refuse collisions. Caught the F3 change the other
    # gates missed. Runs as a SCENE (autoloads aren't registered under --script).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/keymap_gate.tscn"
    timeout: 300
  - name: gate-keys-with-prefix
    # GameState.keys_with_prefix is linear (was quadratic: `k not in result` on an
    # Array), checked as a time RATIO 4000 vs 1000 keys so load cannot flake it.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_keys_with_prefix.gd"
    timeout: 300
  - name: gate-flatten-history
    # Flatten one map's history: the minimum non-undoable BASE records that rebuild
    # what is there now, old records erased after a backup. Geometry and objects
    # identical, History empty, Ctrl+Z inert, backup complete, source key kept.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_flatten_history.tscn"
    timeout: 300
  - name: gate-flatten-room
    # Flatten the WHOLE room: visit every map with level history, flatten each while it
    # is loaded, report a map that cannot load, return to the starting map.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_flatten_room.gd"
    timeout: 120
  - name: gate-history-role
    # The admin-only Flatten buttons follow KeetService.role_changed, not only the
    # journal's folds: at Home the role lands after the last fold (#109, greyed out).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_history_role.gd"
    timeout: 120
  - name: gate-outliner-locked
    # System rows (NavVolume, EntityLinks, the command palette, DCLevel) are LOCKED with
    # a note saying what they are; Delete refuses with that note; their eye hides them and
    # a mode change does not undo it (#109).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_outliner_locked.gd"
    timeout: 120
  - name: gate-pick-range
    # Build mode: object pick and the aim ray reach the camera's far plane (120-130 m
    # targets hit, past-far not). Play mode and the K carve ray keep 50 m (#109).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_pick_range.gd"
    timeout: 120
  - name: gate-confirm-focus
    # Enter through the viewport (push_input) confirms the active tool even when the
    # Outliner's Tree holds the keyboard from the row you clicked (#109).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_confirm_focus.gd"
    timeout: 120
  - name: gate-click-through
    # A click on any dock panel (padding, centre, a Label, the gap between two panels)
    # never selects what is behind it; a click in the open still does (#109).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_click_through.gd"
  - name: gate-areas-layout
    # #124 B1: opaque areas in a split layout around one free 3D rectangle; borders drag,
    # sizes saved, collapse grows R and expand restores it, play mode hides the areas.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_areas_layout.gd"
  - name: gate-tool-owns-mouse
    # #124: while a tool is up, panels neither click nor hover; LMB confirms / RMB cancels
    # over a panel; the shield drops while looking (pointer captured).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_tool_owns_mouse.gd"
  - name: gate-view3d-rect
    # #124: overlays (membership diag, peer/FPS labels) follow the 3D View's corner, not
    # the window's, so they never draw over the areas.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_view3d_rect.gd"
    timeout: 120
  - name: gate-editor-theme
    # #124 spacing audit: Blender-sized widgets (<= 22 px, 12 px text) scoped to the areas;
    # 2 px gaps, no area borders, a thin status bar.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_editor_theme.gd"
    timeout: 120
  - name: gate-areas-types
    # #124 B3: right-click a panel header -> Move to ▸ a column; saved per player; reset.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_areas_types.gd"
    timeout: 120
  - name: gate-outliner-tree
    # #124 B4: the organizer is ONE tree (collections first, members listed once), collapse
    # / expand all, and the Tree fills the organizer area.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_outliner_tree.tscn"
    timeout: 240
  - name: gate-chat-key
    # T toggles chat in play only: build mode suppresses it, Ctrl+T is not chat.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_chat_key.gd"
    timeout: 180
  - name: gate-param-builtins
    # box / cylinder / cone / sphere are parametric types that generate exactly the baked
    # pieces; ParamShapes in Tab order; stairs auto_riser default keeps gv 1 (§13).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_param_builtins.gd"
    timeout: 120
  - name: gate-create-param
    # The create tool makes ONLY parametric shapes: one shape.create, old prefs migrate,
    # tunnel wall from the arch rule, Auto steps riser/grid stored in the record (§13).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_create_param.gd"
    timeout: 120
  - name: gate-shape-texture
    # A shape is textured at birth on every solid plane (clearance bare); a texture on a
    # selected shape is ONE shape.set; it survives a param edit; undo restores (§13.3).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_shape_texture.gd"
    timeout: 120
  - name: gate-unbounded-scale
    # No ceiling on create size/height or model scale (floors kept); the Object panel's
    # Scale slider shows a value past its drag range instead of clamping it (#109).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_unbounded_scale.gd"
    timeout: 120
  - name: gate-shape-child-names
    # A parametric shape's Outliner children are named by role (step N, landing N,
    # voussoir N, floor, column, box) in piece order, the cut is "passage" (#109, §13.4).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_shape_child_names.gd"
    timeout: 120
  - name: gate-pointer-wrap
    # G / element move / extrude: the mouse wraps at the window edge and the drag carries
    # on through a virtual pointer with no jump; Rotate and play mode never wrap (#109).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_pointer_wrap.gd"
    timeout: 120
  - name: gate-move-plane
    # Move: X = axis, X again = X locked (move in the YZ plane), X again = axis; the plane
    # delta is on the grid and commits as one brush.translate (#109).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_move_plane.gd"
    timeout: 120
  - name: gate-editor-view
    # Orthographic + Blender numpad views (5, 1/3/7, 2/4/6/8, 9, .), auto-perspective,
    # wheel zoom in ortho, picking in ortho, on a bare camera and the player rig (#109).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_editor_view.gd"
    timeout: 120
  - name: gate-editor-cursor
    # The 3D cursor: Shift+RMB places it on the surface (view orientation), drag slides it
    # and saves once; to selected (median) / active / grid / origin; menu-only actions run
    # through Keymap.run; Shift+S opens its menu only when not moving (#109).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_editor_cursor.gd"
    timeout: 120
  - name: gate-refold-skip
    # An edit that changes no BRUSH skips the brush replay + full re-mesh (was 705 ms
    # at 2000 brushes, on every fold, deleting a teleporter included). Skipped only
    # when the brush records AND the store fingerprint are unchanged, so drift behind
    # the journal's back still gets a full fold.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_refold_skip.tscn"
    timeout: 600
  - name: gate-map-change-keys
    # A map change must not kill the builder keys: the old map's _exit_tree pop ran
    # after the new map's _ready push and removed `builder` (N / Shift+Space / Delete
    # dead). Contexts are now held per owner. Real map_test, change_map's order.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_map_change_keys.gd"
    timeout: 300
  - name: gate-pick-level-only
    # A pick names a brush only when the ray hit the LEVEL's body: a scene box's
    # shape 0 used to name the level's brush 0 (an unrelated brush that click
    # selected and Delete removed). Real physics on map_test.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_pick_level_only.gd"
    timeout: 300
  - name: gate-delete-selection
    # Delete acts on the SELECTION (objects, then brushes; the aimed brush only when
    # nothing is selected). One piece of a parametric shape deletes the whole shape
    # (shape.remove). One gesture each; Ctrl+Z restores. Real map_test + journal.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_delete_selection.tscn"
    timeout: 300
  - name: gate-scene-boxes-base
    # A level map's scene boxes (StaticBody3D + one BoxShape3D) become BASE brushes at
    # load (keys i:1:<n> in node-name order) so they can be picked, moved and deleted
    # through the journal; rotation kept; non-collision children (HubLabel) survive.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_scene_boxes_base.tscn"
    timeout: 300
  - name: gate-home-reset
    # Home reset to the release starter (the flatten pattern): seeded, edited, flattened,
    # edited again, reset through the History panel -> exactly the starter's content +
    # marker; player position and level source kept; backup holds every erased key;
    # refused outside Home and for a non-admin.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_home_reset.tscn"
    timeout: 300
  - name: gate-nav-follows-edits
    # The bot nav graph is rebuilt after level edits (debounced): built once at load, not
    # during an edit burst, once after it; a platform joins the navigable set, deleting it
    # restores the original graph.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_nav_follows_edits.tscn"
    timeout: 300
  - name: gate-step-up
    # Player step-up (docs/player-step-up-design.md): ledges <= 0.395 m walked up with
    # no airborne frame; 0.45-0.79 m one single-leg hop each (STEP_HOP, never FALL/LAND,
    # lead leg alternating up a flight, derived on a remote copy); 0.85 m blocked; a
    # 0.224 m flight up and down never airborne; drawn height continuous; no ramp lag.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_step_up.tscn"
    timeout: 300
  - name: gate-shape-editor
    # Parametric shapes in the editor: the box tool creates one (ONE shape.create); a
    # click on any piece selects the shape (face mode too — pieces are read-only); the
    # Shape panel's param change is one shape.set; move/scale/rotate write shape.xform /
    # shape.size; Make Real hands the same geometry over as plain brushes; undo restores.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_shape_editor.tscn"
    timeout: 300
  - name: gate-extrude-shape-prompt
    # E on a parametric shape's piece asks "Make Real, then extrude / Cancel" (pieces
    # are read-only): Cancel writes nothing; Make Real converts and starts Extrude; the
    # commit pushes the TILTED face along its own normal (#109, 2026-10-13).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_extrude_shape_prompt.tscn"
    timeout: 300
  - name: gate-rotate-cursor
    # Rotating about the 3D cursor keeps each thing's offset (it orbits, never collapses
    # onto the cursor): a plain brush, a parametric shape, and a second turn of the shape.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_rotate_cursor.tscn"
    timeout: 300
  - name: gate-element-rotate
    # R on a vertex / edge / face turns THAT element, not the brush: a corner lands at
    # R(v - p) + p round the 3D cursor, an edge's two endpoints likewise, a face's plane
    # turns about its centroid; the rest of the cube does not move (#109, 2026-10-13).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_element_rotate.tscn"
    timeout: 300
  - name: gate-knife-local
    # The knife's axis key twice cuts on the brush's OWN axis (Blender X X): world X,
    # then the turned cube's own X, then world again; a local cut lands on the cube's grid.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_knife_local.tscn"
    timeout: 300
  - name: gate-axis-local
    # X X = the thing's own axis in Move, Rotate, element Move and Scale (shapes): a
    # turned cube moves along / turns about its own axes; a turned shape scales its own X.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_axis_local.tscn"
    timeout: 300
  - name: gate-circle-select
    # C = Blender's circle select: LMB drag paints brushes in, Shift paints out, the
    # wheel resizes the ring, RMB leaves with the selection kept (#109 step 6).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_circle_select.tscn"
    timeout: 300
  - name: gate-select-mode-menu
    # Ctrl+Tab = Blender's select-mode menu (vertex / edge / face / brush), the same
    # actions as 1 2 3 4 with their keys shown; picking one sets the mode (#109 step 6).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_select_mode_menu.tscn"
    timeout: 300
  - name: gate-individual-origins
    # Ctrl+. = Blender's Individual Origins pivot: rotate / scale turn each unit (a brush,
    # or a shape's pieces as one) about its own centre with its own grid step, models
    # about their own origins; one undo (#109 build tools).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_individual_origins.tscn"
    timeout: 300
  - name: gate-shape-parts
    # A shape's parts (#109 "select, prompt on edit"): vertex / edge / face mode select
    # the part; G / R / the [ ] nudge / the knife ask "Make Real, then go on"; Make Real
    # carries the selected part onto the plain brush and the tool runs on it.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_shape_parts.tscn"
    timeout: 300
  - name: gate-proportional
    # Proportional editing (O, #109): Shift+O falloff, Alt+O connected; G moves the units
    # near the selection by a falloff of their distance (brushes, a shape as one, models),
    # one gesture; PgUp / PgDn size the radius.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_proportional.gd"
    timeout: 300
  - name: gate-proportional-vertex
    # Proportional, element level (#109): G / [ ] on a vertex — nearby corners follow by
    # the falloff, re-hulled, on neighbouring brushes too unless Alt+O; one undo.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_proportional_vertex.tscn"
    timeout: 300
  - name: gate-mirror
    # Mirror (Ctrl+M, #109): X / Y / Z about the editor pivot; brushes reflect exactly
    # (individual origins too), a shape asks Make Real first, an object mirrors its
    # placement (position + facing, never a negative scale); one undo.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_mirror.gd"
    timeout: 300
  - name: gate-creature-tiers
    # Creature SIMULATION TIERS + SLEEP (CreatureScheduler, #114): by the NEAREST player
    # in the mesh — near 60 Hz, mid 10 Hz, far 2 Hz, beyond asleep (zero ticks, frozen);
    # each tick gets the time it missed; a creature by a second player stays near; a far
    # hopper keeps its raised ground; only ticked creatures are synced.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_creature_tiers.tscn"
    timeout: 300
  - name: gate-op-journal
    # LIVE CUTS through the journal (docs/cutting-and-operations.md §9a): op.create
    # opens a door in a box shape, the cutter is not solid; moving the cutter moves
    # the door, resizing the target re-cuts it; op.set -> intersect; undo restores
    # the wall and a solid cutter; a second peer folds the same pieces; flatten refuses.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_op_journal.tscn"
    timeout: 300
  - name: gate-op-editor
    # Live cuts in the editor: _csg subtract with a shape cutter writes ONE op.create;
    # the crosshair picks the wire cutter; plain brushes still cut destructively;
    # Make Real refuses on a cutter, bakes the target and retires the op; Ctrl+Z restores.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_op_editor.tscn"
    timeout: 300
  - name: gate-op-inset
    # #120 L1 §12.3: inset = a pocket `depth` deep where the cutter meets the skin;
    # outset = a boss `depth` proud, never overlapping; depth edits re-cut; undo.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_op_inset.tscn"
    timeout: 240
  - name: gate-stack-panel
    # The Stack panel (cutting-and-operations.md §9b): generator + one row per live cut in
    # order; the eye switches a cut off (its cutter stays wire), the kind dropdown writes
    # op.set, ✕ op.remove — one Ctrl+Z each; a selected cutter lists what it cuts.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_stack_panel.tscn"
    timeout: 300
  - name: gate-fly-modal
    # Keymap step 1 (keymap-blender-279.md §2a): no permanent WASD — the build camera moves
    # only while flying (RMB held / Shift+F), the exclusive `fly` context suppresses every
    # command then; Shift+F Esc returns to the start view, Enter keeps; Ctrl+Numpad CSG keys.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_fly_modal.gd"
    timeout: 300
  - name: gate-keymap-blender
    # Keymap steps 2-3 (keymap-blender-279.md §1, §5): our build-mode keys vs Blender 2.7x's
    # OWN key-map (tools/keymap/blender_279_left.json, generated by blender_dump.py), command
    # by command (tools/keymap/command_map.json); a stale exception fails too.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_keymap_blender.gd"
    timeout: 300
  - name: gate-keymap-step3
    # Keymap step 3's new commands through their keys: A / Ctrl+I / B selection, Shift+A add
    # menu -> create tool, U texture menu, N sidebar, Tab element/brush, Z wireframe, Shift+R.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_keymap_step3.tscn"
    timeout: 300
  - name: gate-help-contexts
    # Keymap step 4 (§4a): F1 lists the keys of the contexts active NOW (snapshot taken
    # before the search box takes focus), "show every context" marks the rest, Blender
    # names on mapped rows; the status-bar context strip (BUILD / FLYING / tool).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_help_contexts.gd"
    timeout: 300
  - name: gate-keymap-layouts
    # Keymap step 5 (§6): letters bind by CHARACTER, everything else by POSITION — AZERTY,
    # Dvorak and German events shaped as each layout sends them.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_keymap_layouts.gd"
    timeout: 300
  - name: gate-view-commands
    # Keymap step 6.1 (§6a): Home view all, Shift+C + cursor to origin, Alt+Home centre on
    # the 3D cursor; Home yields to UV fit while faces are selected.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_view_commands.tscn"
    timeout: 300
  - name: gate-pivot
    # Keymap step 6.2: the pivot keys (, Ctrl+, . Alt+.) and rotate / scale about it.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_pivot.tscn"
    timeout: 300
  - name: gate-select-linked
    # Keymap steps 6.3-6.4: Ctrl+L select linked (touching, transitively), Shift+G select
    # grouped, L's unit is the whole shape; X = the confirming delete popup.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_select_linked.tscn"
    timeout: 300
  - name: gate-overlays
    # Keymap step 6.5 (§6a.5): one switch per overlay on its key + the Shift+Alt+Z master
    # (each comes back as it was); outlines off keeps clearance / cutters; states survive
    # a mode change; the Outliner eye agrees; the top bar's Overlays toggle + ▾ rows.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_overlays.tscn"
    timeout: 300
  - name: gate-shape-hotspot
    # Hotspot on a parametric shape survives its edits: Fit / Re-roll write the SHAPE's tex
    # (no face.tex on renamed pieces); lengthening the shape keeps the face on the same
    # strip (dc_face_hash ignores @<ver>); Pin on a whole shape is refused; undo works.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_shape_hotspot.gd"
    timeout: 300
  - name: gate-q-menu
    # The Q menu and H = hide (cutting-and-operations.md §9c): rows by selection
    # (nothing / shape / cutter / two things), a pick runs its op (keyless actions reach
    # the map as InputEventAction); H / Shift+H / Alt+H through the Outliner's rows,
    # ungrouped brushes by their member id. Saves under its OWN outliner key.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_q_menu.tscn"
    timeout: 300
  # gate-handspring SKIPPED for now (user, 2026-10-07): the handspring is in-progress work,
  # and the committed version fails check B ("1 of 5 top frames tucked"). Re-enable with
  # the handspring work. Run by hand meanwhile:
  #   HYPERTPS_DAEMON_TCP=127.0.0.1:1 $GODOT --headless --path . res://scenes/test/gate_handspring.tscn
  - name: gate-faces-to-brushes
    # Faces of different brushes -> new brushes: Alt+J hulls them into one (fills the
    # gap between facing walls), Alt+E extrudes each one grid step; the slab is a working
    # CSG cutter. Shift+G duplicates brushes, shapes and objects and starts Move.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_faces_to_brushes.tscn"
    timeout: 300
  - name: gate-level-collision-scale
    # DCLevel collision at 2000 brushes: the body joins the physics space AFTER its shapes
    # (a Jolt body in a space rebuilt its compound on every add — 1149 ms of a 1172 ms
    # rebuild at 2480 brushes); picking's shape -> brush map still exact on 25 rays.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_level_collision_scale.tscn"
    timeout: 300
  - name: gate-editor-highlights
    # The crosshair's auto highlight is dimmed; leaving build mode hides every highlight
    # (selection outlines, element, face tint, aim) and keeps the selection for return.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_editor_highlights.tscn"
    timeout: 300
  - name: gate-journal-index
    # The journal INDEX (LevelPersistence): parsed once, updated per key from
    # state_changed. After 210 seeded random steps (records, objects, undo/redo/seek,
    # a peer's record, erases, source changes, map_changing) it must answer exactly what
    # a from-scratch rebuild answers; an object move parses ONE record and skips the
    # brush fold; a 40-record gesture undoes and redoes as one.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_journal_index.tscn"
    timeout: 300
  - name: gate-outliner-delete
    # Outliner Delete acts on anything saved in the room (objects, a group's brushes) as
    # ONE gesture; plain scene nodes are refused; a group row opens to list its brushes.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_outliner_delete.tscn"
  - name: gate-outliner-sync
    # The tree never disagrees with the viewport: a group row the viewport moved away
    # from is dropped, so the outliner's Delete cannot take the whole group (#120 audit).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_outliner_sync.tscn"
  - name: gate-pcg
    # Pcg32 (the fold's randomness) gives pcg-c-basic's numbers exactly, on every peer:
    # golden values from an independent reference, seeding via SHA-256 (#120 F4).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_pcg.tscn"
  - name: gate-face-primitives
    # FaceKit: a face's polygon (ordered, convex), prisms over it that share the face
    # exactly, the hull of two sections (watertight segments), grid cells to rectangles.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_face_primitives.tscn"
  - name: gate-collection-fold
    # col.* records -> the room's collection tree: per-field last-write-wins, cycles
    # ignored and reported, remove keeping contents vs with hierarchy, links (#120 F1).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_collection_fold.tscn"
  - name: gate-collection-persist
    # Collections are room state: col.* through the journal come back as the tree, never
    # re-fold the level, never reach DCLevel (nothing skipped), and undo (#120 F1).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_collection_persist.tscn"
  - name: gate-collection-edit
    # Editing collections, one gesture each: new, from selection, link (Shift+M), move
    # (M), unlink (Ctrl+Alt+G), rename, delete keeping contents, Delete Hierarchy (#120 F1).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_collection_edit.tscn"
  - name: gate-brush-regroup
    # Brush objects (needs dc-terrain BrushRegroup): a loose brush becomes its own object
    # on Ctrl+G, a later split stays in it, undo restores it, Ctrl+J joins, P separates
    # (keeping its collections), shape pieces are refused (#120 F1).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_brush_regroup.tscn"
  - name: gate-collection-keys
    # Ctrl+G, M / Shift+M / Ctrl+Alt+G pickers, Ctrl+J, P and the Object menu, through
    # Keymap.run (the menu's path) (#120 F1).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_collection_keys.tscn"
  - name: gate-meta-collection
    # Several collection rows select all their brushes; Ctrl+G / + Collection wraps the
    # selected collections in a new parent where they were, subtrees intact (2026-10-18).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_meta_collection.tscn"
  - name: gate-collection-focus
    # M and Ctrl+G through Viewport.push_input, the organizer's Tree focused or not; the
    # new collection is named in a prompt first (2026-10-18).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_collection_focus.gd"
  - name: gate-duplicate-structure
    # Shift+D keeps structure: one brush object's copies form one new object, a copy keeps
    # its parent (or the parent's copy), every copy joins its source's collections (#120 F1).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_duplicate_structure.tscn"
  - name: gate-cable-gen
    # The cable generator (#120 L3 §11.1): segments = hulls of profile rings (static
    # DCLevel.hull_of), watertight, angle / length rules, square + tilt, taper, strip,
    # connectors, rings, closed curves, determinism.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_cable_gen.tscn"
  - name: gate-cable-journal
    # Cables in the fold (#120 L3 §11.2): add_cable is one gesture, curve edits / params /
    # removal re-fold the pieces, arrival order, mirror, cuts, flatten keeps the curve.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_cable_journal.tscn"
  - name: gate-cable-tool
    # The cable tools (#120 L3 §11.3): Add → Cable, slack sag, G / R move the curve, X
    # removes the curve with the cable unless something else reads it; clean hulls at 320 m.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_cable_tool.tscn"
  - name: gate-cable-profile
    # Custom cable profiles (#120 L3 §14.2): a closed curve's convex outline as the
    # cross-section, live; the Shape panel's Profile curve row; fallback to round ("r" names).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_cable_profile.tscn"
  - name: gate-cable-drop
    # Drop to the ground (#120 L3 §14.3): CurveDrop on a fake floor; real physics with the
    # cable's own pieces skipped; Ctrl+Shift+D tool, one gesture, Adjust Last.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_cable_drop.tscn"
  - name: gate-edge-set
    # The edge set (#120 L3 §14.4): Shift-click edges across brushes, held as brush key +
    # two plane keys, resolved from the planes, live brushes only, highlighted.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_edge_set.tscn"
  - name: gate-cable-edges
    # Cables from edges (#120 L3 §14.5): chains (stop at a 3-way vertex, close a loop),
    # vertices / centres, offset along the mean normal, corners; Alt+C, one gesture.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_cable_edges.tscn"
  - name: gate-cable-bundle
    # A bundle between two face sets (#120 L3 §14.6): islands, seeded ends on the faces,
    # free end handles, slack; N cables + a Bundle collection in one gesture; Shift+Ctrl+C.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_cable_bundle.tscn"
  - name: gate-brush-narrow-reads
    # DCLevel.get_brushes_bounds / count_clearance answer bit-for-bit what the get_brush
    # loops they replaced did (hyper-games #127: the organizer's per-fold reads).
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_brush_narrow_reads.gd"
  - name: gate-asset-doc
    # #120 L4 step 1: a collection's contents as an asset blob — round trip, canonical
    # bytes, local dots, cuts carried with their cutter, decode refuses untrusted input.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_asset_doc.tscn"
  - name: gate-asset-fold
    # #120 L4 step 2: placed assets fold as live instances — at their pivot the originals'
    # windings bit-identical; moved / turned / undone / missing blob / tail fold.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_asset_fold.tscn"
  - name: gate-asset-library
    # #120 L4 step 3: Save as asset into the room's + personal libraries; a re-save moves
    # my placements, others' wait for Update placements; personal published on place; fetch.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_asset_library.tscn"
  - name: gate-asset-edit
    # #120 L4 step 4: a placed asset is one thing in the editor — select, G/R/X/Shift+D,
    # read-only parts, Make Real (same windings), outliner rows; no edit names a synthetic piece.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_asset_edit.tscn"
  - name: gate-asset-browser
    # #120 L4 step 5: the Assets strip — tiles, filters, Save to switches, Update N, the
    # drop frame (floor / wall / ceiling), the 3D view armed only during a tile's drag.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_asset_browser.tscn"
  - name: gate-asset-thumb
    # #120 L4 step 6: a save renders a 128 px thumbnail of the collection alone (real
    # pixels), named by the overlay's hash, in both libraries and on the tile; a late one
    # never clobbers a newer save. WINDOWED: needs a device (the headless dummy draws nothing).
    run: "DISPLAY=:0 HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --path . res://scenes/test/gate_asset_thumb.tscn"
  - name: gate-curve-overlay
    # Curves (wire, points, held handles) are an editor overlay: hidden in play mode, the
    # Curves switch (Shift+Alt+K) and the master switch hide them in build (2026-10-18).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_curve_overlay.tscn"
  - name: gate-curve-member
    # #120 §16 step 1: a curve is a collection member ("c:<dot>"): Ctrl+G / M / Shift+M /
    # Ctrl+Alt+G, Delete Hierarchy keeps a used curve, flatten remaps, assets carry it.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_curve_member.tscn"
  - name: gate-outliner-curves
    # #120 §16 steps 2-5: curve rows (a cable's own curve under the cable), rename,
    # select both ways, X (refused while used), drawn selected, hide, Shift+D, G / R,
    # click the wire to pick (nearer wins over the brush behind it).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_outliner_curves.tscn"
  - name: gate-curve-g-mode
    # #120 §17 step 1: G on a selected curve by select mode — vertex mode edits the point
    # nearest the pointer, brush mode moves the whole curve; a mode switch keeps the curve.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_curve_g_mode.tscn"
  - name: gate-modal-drag
    # #120 §17 step 2: the mouse drags a tool's focused value (Blender's): px x step / 25,
    # Shift finer, Ctrl snap, accumulates on integers; Rotate drags, Move does not.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_modal_drag.tscn"
  - name: gate-shape-handles
    # #120 §17 step 3: a selected shape's six face dots; press-drag-release resizes one
    # side (the opposite stays), grid-snapped, one gesture; Esc nothing; Ctrl+Z restores.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_shape_handles.tscn"
  - name: gate-tool-handles
    # #120 §17 step 4: a tool's dots (BoxCutter depth, cable slack): a press on one grabs it
    # (no confirm), the drag sets the value, the release lets go; a press elsewhere confirms.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_tool_handles.tscn"
  - name: gate-union-select
    # #120 §17 step 5a: objects, brushes and curves held together — Shift adds across
    # kinds, A selects all three (and toggles off), the outliner lights every row.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_union_select.tscn"
  - name: gate-union-ops
    # #120 §17 step 5b: X and H on objects + brushes + curves together — one gesture, one
    # Ctrl+Z; a cable's curve selected with its cable goes with it (not refused).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_union_ops.tscn"
  - name: gate-union-move
    # #120 §17 step 5c: G and Shift+D on objects + brushes + curves together — one delta,
    # one gesture, Esc restores all; Shift+D copies every kind and Moves the copies.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_union_move.tscn"
  - name: gate-union-rotate
    # #120 §17 step 5d: R on objects + brushes + curves together — one pivot (their
    # combined centre), one gesture; Esc restores all.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_union_rotate.tscn"
  - name: gate-face-set-live
    # #129 deferred bug: the face set (Alt+J / Alt+E / cable bundle) drops a deleted
    # brush's face (a tombstone brush_by_key still returns), as the edge set does.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_face_set_live.tscn"
  - name: gate-curve-point-handles
    # #129 deferred bug: set_curve_point moving `p` re-derives auto / vector handles (the
    # moved point's and its neighbours'), one gesture; free / aligned keep theirs.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_curve_point_handles.tscn"
  - name: gate-duplicate-linked
    # Alt+D (Blender's Duplicate Linked): the copy shares the shape's data (an edit on
    # either reaches both), keeps its own place / size / modifiers; a link of a link names
    # the root; the root deleted, links still share; flatten keeps the set linked.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_duplicate_linked.tscn"
  - name: gate-make-parametric
    # Any brush -> a parametric shape: box when it is its own box, else hull (its planes
    # in the unit box, per-face textures kept); resizable; collections kept; undo (#120 F2).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_make_parametric.tscn"
  - name: gate-assign-generator
    # Box -> stairs with the box as passage; params kept across a switch; Remove generator;
    # hull round trip; undo; Shift+D and flatten keep the passage (#120 F2, #109).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_assign_generator.tscn"
  - name: gate-mod-copies
    # Copy modifiers on a shape: mirror / array / radial, a stack, edits rename the copies,
    # on / off / remove / undo, determinism, a cut mirrored with its target, Make Real
    # bakes the copies, flatten keeps the modifiers (#120 F2c).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_mod_copies.tscn"
  - name: gate-mod-curve
    # Follow-curve: the object and its copies onto a curve (rigid, convex), fit, a curve
    # edit moves them (only a followed curve re-folds), curve removed -> back, determinism,
    # a cut carried, the nearest curve, flatten remaps the curve (#120 F2 + F5).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_mod_curve.tscn"
  - name: gate-mod-chamfer
    # The chamfer modifier (dc-terrain BrushChamfer, after the cuts): box, width edit,
    # on / off / undo, no groove at a mirror seam, cut pieces chamfered too (#120 F2d).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_mod_chamfer.tscn"
  - name: gate-mod-dice
    # #120 L1 §12.4: dice splits a shape into even grid cells (convex, no overlap), BEFORE
    # the copies whatever the record order; a cut reaches the cells; stable names.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_mod_dice.tscn"
    timeout: 240
  - name: gate-param-prism
    # #120 L2 §13.1: the prism cutter — one convex piece, taper / shear, ngon, concave -> hull.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_param_prism.gd"
    timeout: 120
  - name: gate-cut-through
    # #120 L2 §13.3: through-depth = the targets' deepest reach below the plane + a step.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_cut_through.gd"
    timeout: 120
  - name: gate-box-cutter
    # #120 L2 §13.2: BoxCutter — rect on a face, through by default, X / I modes, E depth,
    # one gesture (prism shape + live op), one Ctrl+Z.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_box_cutter.tscn"
    timeout: 240
  - name: gate-q-stack
    # #120 L2 §13.4–13.5: Change cut, Extract (+ BoxCutter Y), Apply up to here (later cuts
    # stay live on the baked pieces), the Q rows + keys, the LOCAL stack scroll preview.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_q_stack.tscn"
    timeout: 300
  - name: gate-mod-keys
    # #120 L1 §12.5: Shift+Ctrl+M opens Add modifier with letters; G / D add grid / dice.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_mod_keys.tscn"
    timeout: 240
  - name: gate-modal-controls
    # One modal convention: a tool's key table is its status line and its H overlay; bound
    # values step with [ ] (Shift finer) and Tab; Mirror migrated, through the real key
    # path, with a summary card on confirm (#120 F3).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_modal_controls.tscn"
  - name: gate-curve-math
    # Curve evaluation (pure): Bezier vs analytic (line, quarter circle), arc length,
    # rotation-minimising frames (no flips on a helix), tilt, radius, auto handles off the
    # surface normal, closed curves, determinism (#120 F5).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_curve_math.tscn"
  - name: gate-curve-journal
    # Curves are room state: curve.create / set (whole or one point) / remove through the
    # journal, never re-folding the level; undo; arrival order; the view; flatten (#120 F5).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_curve_journal.tscn"
  - name: gate-curve-tool
    # The curve tool: Add menu, P places points on surfaces (lifted along the normal, which
    # each point keeps), Backspace, ] lift, C closed, Enter = one curve.create, Esc
    # writes nothing (#120 F5).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_curve_tool.tscn"
    timeout: 300
  - name: gate-scene-objects-base
    # A level map's teleporters and spawn points are BASE objects ("scene:<Name>"):
    # movable, relabelable and deletable through the journal; Ctrl+Z restores the
    # authored transform/props; a deleted one is detached, so undo brings the same node.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_scene_objects_base.tscn"
    timeout: 300
  - name: gate-texture-strip
    # The texture strip (#94 P1 §9) on a real map_flat: a BOTTOM-row dock panel,
    # search / pack filter, a click sets the current texture, U applies it to a
    # brush or face selection LOCALLY and as one face.tex record per brush (texture
    # only — alignment kept; untextured faces become paraxial), a refold agrees,
    # Y picks a face's texture, U/Y bound with no conflicts.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_texture_strip.gd"
    timeout: 300
  - name: gate-pack-texture
    # Texture packs reach the level (#94 P1 step 3): pack.json catalog (counts per
    # pack), TexturePacks.build at 1024 and 512, missing checker, flat-normal / plain
    # ORM defaults, Settings texture_res follows the preset, and LevelPackMaterial on
    # a real DCLevel through the journal: on-demand load, faces -> their layer,
    # unknown id -> missing layer, 512 switch keeps layers. Scene gate (autoloads).
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/pack_texture_gate.tscn"
    timeout: 300
  - name: gate-presence-null-identity
    # presence.gd stored identity fields with str(): str(null) is "<null>", so a
    # fresh (unnamed) keet identity was announced with name "<null>", and a null
    # memberId passed the `== ""` guard and announced under presence:<null>. (#89)
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_presence_null_identity.gd"
    timeout: 300
  - name: gate-rpc-timeout
    # KeetUdsClient released a callback only on a reply or a disconnect, so a daemon
    # that accepted a call and never answered left the caller waiting for ever (the
    # menu sat on "identity" silently). A local TCP server that accepts and never
    # replies reproduces it; calls now fail with "Timed out" after rpc_timeout_sec. (#89)
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_rpc_timeout.gd"
    timeout: 300
  - name: gate-map-visibility
    # Player nodes hang off Game, not off the map, so every peer's body existed in
    # every map: a peer parked in map_flat was visible from map_test and followed
    # you through a teleporter. Nothing filtered by map because change_map assumed
    # the whole room moved together — and it never did. world:settings:current_map
    # is read once at game start and nothing subscribes to it, so a live peer
    # never follows someone else's map change.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_map_visibility.gd"
    timeout: 300
  - name: gate-room-state-seed
    # #131: a timed-out room-state load is UNKNOWN, not absent — the default world is never
    # seeded then (it overwrote a slow room's saved map as admin); the world restores late
    # once state arrives; the timeout reports only errors from its own wait.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_room_state_seed.gd"
  - name: gate-room-state-fault
    # The room-state timeout used to say "(daemon down, room not opening)" — two
    # candidates, neither distinguished. It fired once on stortron after a pgh app
    # update and four controlled attempts failed to reproduce it, so the single
    # occurrence had to carry its own evidence and did not.
    #
    # Gated as a PURE mapping from observations to message, which is the only part
    # that can be wrong in a way that costs anything, plus a structural check that
    # the timeout path actually calls it.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_room_state_fault.gd"
    timeout: 300
  - name: gate-nav-debug
    # DCTerrain.debug_nav_chunk — the instrument from the chunk/portal nav design
    # (dc-terrain docs/plans/2026-09-15). Makes the portal graph visible BEFORE
    # anything paths over it, because a pathfinder on a wrong graph produces bad
    # routes that look like a bad pathfinder.
    #
    # ⛔ Its load-bearing assertion is that CARVING CHANGES THE ANSWER. A nav view
    # built from the generator alone would be identical before and after an edit and
    # would look healthy while being blind to every change in the world —
    # DCCollisionSDF has exactly that bug. The first version of this gate guessed a
    # method name, skipped that check, and reported PASS.
    #
    # It also fails if the GDExtension .so is stale, which is a known trap: the
    # symlinked binary is untracked and CI never rebuilds it. All three targets
    # (editor, template_debug, template_release) must be built.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_nav_debug.gd"
    timeout: 300
  - name: gate-pathwalker
    # A bot given a route walks it: advances through waypoints in order, converges
    # rather than orbiting, appends the destination so it does not stop in the last
    # doorway, and does not move at all when there is no route.
    #
    # Driven through a STUB terrain on purpose. The search has its own gate in
    # libdc; driving the real DCTerrain here would need a meshed world and would
    # leave the thing under test — does the bot converge or orbit — dependent on
    # whatever terrain happened to generate.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_pathwalker.gd"
    timeout: 300
  - name: gate-bot-declaration-survives
    # A map change ends the BOT, not its DECLARATION. despawn() erased the durable
    # registry row for every position body, so leaving a map deleted the
    # declarations of the bots that belong to it and re-entering had nothing to
    # revive. The position row is still erased, which is what other peers render.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_bot_declaration_survives.gd"
    timeout: 300
  - name: gate-bot-revive
    # Entering a map must bring its DECLARED bots back to life, not just to sight.
    # Teardown was wired (map_changing despawns map-bound bots) and setup never
    # was, so a declared bot could never run again after its first map change --
    # the map with the nav volume was the one no bot lived in. Asserts the brain
    # actually RAN (beacon writes state:bot:beacon:alive) and that a row arriving
    # before its map exists is held and retried rather than dropped.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_bot_revive.gd"
    timeout: 300
  - name: gate-poll-no-reemit
    # The 3 s room poll emits only what changed. Step 4 stages the CSG full-rebuild
    # race (hyper-games #108): a poll sent BEFORE a local write but answered after it
    # must not report that key erased, which re-folded the whole terrain journal after
    # the player's own carve. A poll in flight during a local erase must not bring the
    # key back either.
    run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_poll_no_reemit.gd"
    timeout: 120
  - name: gate-durable-load-order
    # A bulk durable load must hand handlers a COMPLETE snapshot. Store-and-emit
    # in one pass gave them a half-filled mirror, so AdminFacade could not see the
    # admin:result:<seq> marker for the very command it was being handed and
    # re-executed the room's whole command history on every cold load.
    #
    # The gate registers a bot as the side effect, because the marker's VALUE is
    # destroyed by the load itself and a state_changed observer runs too late.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_durable_load_order.gd"
    timeout: 300
  - name: gate-nav-end-to-end
    # THE CHAIN, on map_flat: real map -> real box colliders -> nav graph -> route
    # -> a bot that walks it. Every link had its own gate and the chain did not,
    # which is the gap this closes.
    #
    # map_flat on purpose: it has NO DCTerrain and no density field anywhere, so if
    # navigation worked only on terrain it would work nowhere the bots actually are.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_nav_end_to_end.gd"
    timeout: 400
  - name: gate-level-nav
    # The STARTING map (map_test) gives bots navigation: map_dc_level builds a
    # DCNavVolume for every level map (it had none -- bots walked through walls and
    # the A* brain stood still), thin floors are found, and a route around Wall1
    # goes AROUND it (intra-region cell routing). Verified red before the change.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_level_nav.gd"
    timeout: 300
  - name: gate-walker-acquire
    # walker.gd looked its player up once in _bot_init and gave up for ever.
    # hyper-games #77 item 4. Its existing gate, gate_walker_bot, builds its own
    # Game/Players and attaches stub_local_player.gd, whose is_local_player()
    # returns true unconditionally — it asserts against a tree constructed to
    # satisfy the code under test and cannot see this. THIS gate instances the
    # real player_procedural.tscn and uses the real PlayerPosition autoload.
    # Verified red against the pre-fix walker before it was added.
    run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_walker_acquire.gd"
    timeout: 300
  - name: export-godot
    # Export with a REAL Vulkan RenderingDevice (NOT --headless) so the Shader
    # Baker (export_presets.cfg: shader_baker/enabled=true) actually runs. The
    # baker bakes the SPIR-V shader cache into the PCK as res://.godot/shader_cache,
    # so installed clients load warm shaders instead of cold-recompiling on first
    # launch (incl. after every OTA, since each version ships its own bake).
    # `--headless` uses the dummy renderer → no RenderingDevice → _is_active()
    # is false → the baker silently bakes nothing. The build box (tabertron) has
    # an RTX GPU on DISPLAY=:0. A failed export aborts the deploy safely: pgh-publish
    # never runs, so the live version is untouched.
    # NOTE: godot 4.6 segfaults on EXIT (dangling GDExtension atexit handler) — harmless
    # AFTER the .pck is written, but a `test -f` on the binary alone would mask a crash
    # that happened BEFORE the pack finished, shipping a truncated .pck. So validate the
    # .pck exists AND is a plausible size (real pack ~391MB). See hyper-games #1.
    # Exports with the godot-rtx EDITOR against the godot-rtx TEMPLATE
    # (export_presets.cfg custom_template/release). Both must come from the same
    # build or the engine version-mismatches. The template is a build artifact of
    # nvidia-rtx-godot and is NOT in git, so assert it exists FIRST: without this
    # test a missing template silently falls back to the installed 4.6.stable
    # templates and ships a build with no AreaLight3D, which looks identical to a
    # successful deploy.
    run: |
      GODOT=/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64
      TEMPLATE=/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.template_release.x86_64
      test -x "$GODOT" || { echo "ERROR: godot-rtx editor missing at $GODOT"; exit 1; }
      test -f "$TEMPLATE" || { echo "ERROR: godot-rtx export template missing at $TEMPLATE — build it with 'scons platform=linuxbsd target=template_release'"; exit 1; }
      # $TEMPLATE is only an ASSERTION -- the export uses whatever
      # export_presets.cfg custom_template/release names. Those drifted apart on
      # 2026-09-21: ci.yaml moved to the 4.8 engine while the preset still named the
      # 4.7 template, so the export ran a 4.8 EDITOR against a 4.7 TEMPLATE and
      # shipped a 4.8 .pck next to a 4.7 binary. It published green; the failure
      # only appeared on a user's machine as
      #   "Pack created with a newer version of the engine: 4.8.0"
      # Assert they agree, so the pair can never silently diverge again.
      grep -qF "custom_template/release=\"$TEMPLATE\"" export_presets.cfg || {
        echo "ERROR: export_presets.cfg custom_template/release does not match \$TEMPLATE ($TEMPLATE)."
        echo "       The export would use the preset's template, not this one. Preset says:"
        grep -n 'custom_template/release' export_presets.cfg
        exit 1; }
      echo "engine: $("$GODOT" --version)"
      mkdir -p build/linux
      DISPLAY=:0 "$GODOT" --rendering-driver vulkan --export-release "Linux" build/linux/hypertps
      test -f build/linux/hypertps && test "$(stat -c%s build/linux/hypertps.pck 2>/dev/null || echo 0)" -gt 100000000
    timeout: 600
  - name: copy-ngx-dlss
    # DLSS needs its RUNTIME snippet libraries at run time; libnvsdk_ngx.a is
    # link-time only, so a binary can carry NGX symbols and still fail to create
    # the feature. dlss.cpp:679 searches exactly two directories:
    #   1) the executable's own dir  2) <exe_dir>/../thirdparty/ngx/lib/linux_x86_64
    # The editor satisfies (1) because the .so sit in bin/ next to it. A shipped
    # game satisfies NEITHER unless we put them beside hypertps — which is why
    # every exported build until now failed with
    #   [NGX DLSS] DLSS-SR feature creation failed: 0xbad0000b
    # = NVSDK_NGX_Result_FAIL_UnableToInitializeFeature ("the library for that
    # feature could not be found"). Both are needed: -dlss is SR, -dlssd is Ray
    # Reconstruction, which is the denoiser path tracing uses.
    # Filenames are copied verbatim — the versioned names are what NGX matches
    # in the editor case that demonstrably works.
    run: |
      NGX=/storage/projects/code/godot/nvidia-rtx-godot-v3/thirdparty/ngx/lib/linux_x86_64
      for so in libnvidia-ngx-dlss.so.310.5.3 libnvidia-ngx-dlssd.so.310.5.3; do
        test -f "$NGX/$so" || { echo "ERROR: NGX runtime lib missing at $NGX/$so — run 'python misc/scripts/install_dlss_linux.py' in nvidia-rtx-godot"; exit 1; }
        cp "$NGX/$so" build/linux/
      done
      ls -la build/linux/libnvidia-ngx-*.so*
  - name: copy-gdextension
    run: |
      mkdir -p build/linux/bin/linux
      # *.so* (not *.so) so versioned runtime libs like libomp.so.5 are included —
      # libomp.so.5 ends in .so.5 and was silently dropped by a bare *.so glob.
      for f in bin/linux/*.so*; do
        if [ -L "$f" ]; then
          cp "$(readlink -f "$f")" "build/linux/bin/linux/$(basename "$f")"
        else
          cp "$f" "build/linux/bin/linux/"
        fi
      done
      # dc_terrain GPU compute shaders — shipped next to libdc_terrain…so so the
      # extension resolves them via dladdr at runtime (relative to its own .so),
      # not a hardcoded dev path. Without these the GPU mesh pipeline fails on
      # other machines and CPU-falls-back to dual-contouring → ~4fps. -rL derefs
      # the bin/linux/shaders symlink into the real shader tree (incl generators/).
      if [ -e bin/linux/shaders ]; then
        cp -rL bin/linux/shaders build/linux/bin/linux/shaders
        echo "Shipped dc_terrain shaders: $(find build/linux/bin/linux/shaders -name '*.glsl' | wc -l) .glsl files"
      else
        echo "WARNING: bin/linux/shaders missing — dc_terrain GPU meshing will CPU-fall-back"
      fi
  - name: verify-gdextension
    run: |
      echo "GDExtension .so files in build:"
      ls -la build/linux/bin/linux/*.so*
      file build/linux/bin/linux/*.so* | grep -v "ELF" && echo "ERROR: non-ELF .so files!" && exit 1 || echo "All .so files are ELF binaries"
      # GLIBC CEILING: 2.35, ashatron's. A plain `scons` on this host links against
      # its glibc 2.39 and produces a library that needs GLIBC_2.38 -- it builds,
      # every gate here passes, and on ashatron the extension silently does not
      # load. dc-terrain and hyperswarm-peer ship ZIG-built (tools/zig-cc, target
      # glibc 2.28, libomp.so.5); 2026-09-29 a plain rebuild of dc-terrain reached
      # a published release this way. Build them with:
      #   CC=$PWD/tools/zig-cc CXX=$PWD/tools/zig-cxx scons platform=linux target=template_release
      # proc_nodes is zig-built the same way since 0.1230.0 (it shipped GLIBC_2.38
      # before, and loads: proc_nodes.gdextension is in the export include_filter).
      # THE ENGINE TOO: build/linux/hypertps is the export template, and a template
      # built on this host needs GLIBC_2.38 -- ashatron refused to start 0.1229.0
      # ("libm.so.6: version `GLIBC_2.38' not found"). It is built in an ubuntu:22.04
      # container (glibc 2.35) from a `git archive` of the engine branch.
      # blender_bridge alone WARNs: its .so ships but never loads, because
      # bin/blender_bridge.gdextension is in the export exclude_filter.
      FAIL=0
      for so in build/linux/hypertps build/linux/bin/linux/*template_release*.so; do
        need=$(objdump -T "$so" | grep -oE 'GLIBC_[0-9]+\.[0-9]+' | sort -V | tail -1)
        ok=$(printf '%s\nGLIBC_2.35\n' "$need" | sort -V | tail -1)
        if [ "$ok" = "GLIBC_2.35" ]; then echo "  glibc ok   $need  $(basename "$so")"; continue; fi
        case "$(basename "$so")" in
          libblender_bridge.*) echo "  WARN $need > 2.35 (not loaded: blender_bridge.gdextension is excluded): $(basename "$so")" ;;
          hypertps) echo "  ERROR $need > 2.35: the engine template -- rebuild it in the ubuntu:22.04 container"; FAIL=1 ;;
          *) echo "  ERROR $need > 2.35: $(basename "$so") -- rebuild it through tools/zig-cc"; FAIL=1 ;;
        esac
      done
      [ "$FAIL" = 0 ] || exit 1
  - name: gate-flatten-node-modules
    # The launcher flattens pgh's node_modules on first start. It used to leave
    # every nested `../../<hash>` link dangling (1310/1310), so each package
    # resolved the APP's top-level deps instead of its own: 0.1217.0 aborted in
    # keet-core on blind-peering 2.1.2 (needs ^2.5.0). Nested links must land on
    # the one path their hash loads from.
    run: node scripts/test/flatten-node-modules.test.cjs
    timeout: 60
  - name: install-public-deps
    # `npm ci` — strictly installs from package-lock.json, refuses if lockfile
    # disagrees with package.json. Catches transitive drift at CI time instead
    # of crash time on user devices. Lockfile MUST be committed alongside
    # package.json. --omit=dev (modern replacement for --production) skips
    # devDependencies. --ignore-scripts blocks postinstall hooks.
    run: npm ci --omit=dev --ignore-scripts
    timeout: 600
  - name: overlay-private-deps
    run: rsync -a --ignore-existing /storage/projects/code/hypercore-kb/hyper-fleeky/keet-cli/node_modules/ node_modules/
  - name: overlay-keet-core
    # 2026-05-11: @holepunchto/keet-core-hyperdb 1.22.0 was retired
    # upstream — its bundle/abi*.bundle.js files reference targets not
    # actually inlined, causing the cold-install crash in keet-daemon
    # (hyper-fleeky-dev#20). Replacement is @holepunchto/keet-core
    # 4.14.4 (no eager bundle loading; bundle protocol is migration-
    # only). We overlay the new package set from holepunchto-deps
    # directly because keet-cli (the rsync source above) hasn't been
    # bumped yet and stays on 1.22.0 as legacy fallback. The new
    # package set + keet-daemon's matching import swap together
    # resolve the bundle crash. Audit at hyper-fleeky/keet-audit-4.x/.
    run: |
      DEPS=/storage/projects/code/hypercore-kb/hyper-fleeky/holepunchto-deps/@holepunchto
      rm -rf node_modules/@holepunchto/keet-core-hyperdb
      # 2026-06-29: keet-core 4.17.3 (Keet 4.17.2). Overlay the FULL @holepunchto
      # set from holepunchto-deps — keet-cli's rsync (overlay-private-deps) only
      # provides STALE siblings and is missing keet-room-registry (a NEW keet-core
      # 4.17.x dep), so we must overlay every keet-core sibling, not just 3.
      # 2026-09-30: keet-core 4.22.28 (Keet 4.22.5) adds @holepunchto/call-swarm (calls moved
      # out of keet-core api/v1/calls).
      # 2026-10-11: keet-core 4.23.8 (Keet 4.23.0) — no new @holepunchto package. Its new deps
      # (ble-swarm, bare-hrtime, speedometer) ship inside keet-daemon 1.1.27's own node_modules,
      # which copy-keet-daemon brings along. Lockstep reason: the network's autoupgrade is 37
      # and 4.22.28's ceiling was 35 — every new room interrupted (room.invite: null anchor).
      PKGS="keet-core call-swarm keet-room-registry keet-call keet-unread-anchors keet-core-api keet-core-schemas keet-indexing keet-push keet-registry keet-registry-client keet-registry-schema hyperdiscovery hyperindex keet-default-config keet-debug-helper legacy-keet-core-schemas"
      for pkg in $PKGS; do
        if [ -d "$DEPS/$pkg" ]; then
          rm -rf "node_modules/@holepunchto/$pkg"
          cp -r "$DEPS/$pkg" "node_modules/@holepunchto/$pkg"
        else
          echo "  WARN: $pkg not in holepunchto-deps"
        fi
      done
      echo "Overlay versions:"
      for pkg in $PKGS; do
        [ -d "node_modules/@holepunchto/$pkg" ] || continue
        v=$(node -e "console.log(require('./node_modules/@holepunchto/$pkg/package.json').version)")
        echo "  @holepunchto/$pkg: $v"
      done
  - name: copy-keet-daemon
    run: rm -rf node_modules/keet-daemon && cp -r /storage/projects/code/hypercore-kb/hyper-fleeky/packages/keet-daemon node_modules/keet-daemon
  - name: verify-deps
    run: |
      FAIL=0
      for mod in keet-daemon framed-stream b4a @holepunchto/keet-core hyperswarm; do
        if [ ! -d "node_modules/$mod" ]; then echo "MISSING: $mod"; FAIL=1; fi
      done
      test $FAIL -eq 0 && echo "All critical deps present" || exit 1
  - name: tune-rocksdb-memory
    run: sed -i 's/maxWriteBufferNumber = 2/maxWriteBufferNumber = 1/' node_modules/rocksdb-native/lib/column-family.js && sed -i 's/maxOpenFiles = -1/maxOpenFiles = 256/' node_modules/rocksdb-native/lib/state.js
  - name: stage-fat-tree
    run: |
      # Multi-arch layout: host-independent files at top level, platform
      # binaries under by-arch/<host>/build/<platformDir>/. The rootTree
      # references all arches; pgh app install's skipPath drops foreign
      # by-arch/<other-host>/ during materialize so each user only fetches
      # their own host's binaries.
      RAW=$(uname -m)
      case "$RAW" in x86_64) ARCH=x64 ;; aarch64) ARCH=arm64 ;; *) ARCH="$RAW" ;; esac
      HOST="linux-${ARCH}"
      STAGE=stage
      rm -rf "$STAGE"
      mkdir -p "$STAGE/by-arch/${HOST}/build/linux/bin/linux"

      # Host-independent at top level
      cp index.js package.json version.txt "$STAGE/"
      cp -r daemon "$STAGE/"
      cp -r lib "$STAGE/"
      cp -r scripts "$STAGE/"
      cp -r node_modules "$STAGE/"

      # Host-specific: godot binary + pck + native GDExtension .so
      cp build/linux/hypertps "$STAGE/by-arch/${HOST}/build/linux/"
      cp build/linux/hypertps.pck "$STAGE/by-arch/${HOST}/build/linux/"
      # NGX DLSS runtime libs must land BESIDE hypertps, not in bin/linux/ —
      # dlss.cpp searches the executable's own dir (see copy-ngx-dlss).
      cp build/linux/libnvidia-ngx-*.so* "$STAGE/by-arch/${HOST}/build/linux/"
      cp -L build/linux/bin/linux/*.so* "$STAGE/by-arch/${HOST}/build/linux/bin/linux/"
      # dc_terrain GPU compute shaders, alongside the .so (dladdr-resolved at runtime)
      if [ -d build/linux/bin/linux/shaders ]; then
        cp -rL build/linux/bin/linux/shaders "$STAGE/by-arch/${HOST}/build/linux/bin/linux/shaders"
      fi
  - name: strip-foreign-prebuilds
    run: |
      RAW=$(uname -m)
      case "$RAW" in x86_64) ARCH=x64 ;; aarch64) ARCH=arm64 ;; *) ARCH="$RAW" ;; esac
      HOST="linux-${ARCH}"
      TARGET="stage/node_modules"
      if [ -d "$TARGET" ]; then
        BEFORE=$(du -sm "$TARGET" | cut -f1)
        find "$TARGET" -path "*/prebuilds/*" -mindepth 1 -maxdepth 1 -type d ! -name "$HOST" -exec rm -rf {} +
        AFTER=$(du -sm "$TARGET" | cut -f1)
        echo "Stripped foreign prebuilds: ${BEFORE}MB -> ${AFTER}MB (saved $((BEFORE - AFTER))MB)"
      fi
  - name: pgh-publish
    run: |
      cd stage
      pgh publish . --accept-conflict "automated CI publish; dep version splits + non-deterministic builds inherited from per-app lockfiles across the pgh ecosystem"
    timeout: 300
  - name: bundle-windows
    # Windows is on the SAME engine as every Linux step: nvidia-rtx-godot-v3
    # (4.8.dev, linux-rtx-v3-dev @ b7b5b330f3). Both paths below must move
    # together -- this step exports Windows using the LINUX editor against the
    # WINDOWS template, and as export-godot states, a mismatched pair
    # version-mismatches. That failure would surface ONLY on a tagged release,
    # because every other build takes the stub branch below and never exports.
    #
    # The 4.8 Windows template was cross-built 2026-09-21 (14m09s, clean):
    #   cd /storage/projects/code/godot/nvidia-rtx-godot-v3
    #   scons platform=windows target=template_release use_mingw=yes d3d12=no \
    #         winrt=no accesskit=no angle=no
    # That build needs df60317884 ("windows: _fread_nolock is MSVC-only"), which
    # the port carries: NVIDIA's 4.8 base uses _fread_nolock, MSVC-only, which
    # breaks under mingw.
    # use_ngx_dlss is linuxbsd-only in SConstruct, so the Windows build has no NGX
    # DLSS -- same as on v2, not a regression from this migration.
    #
    # The standalone Windows build, for TAGGED RELEASES ONLY.
    #
    # It is 450MB zipped and the release dir stores plain files with no
    # content-addressing or dedup, so one per master push would be ~450MB of
    # permanent disk each — the accumulation problem that produced a 9.5GB
    # pear-runtime store, in a different place.
    #
    # WHY A STUB ON EVERY OTHER BUILD, rather than skipping the step:
    # collectArtifacts THROWS "artifact not found after build" on a missing path
    # (ci-runner.js), and the artifacts list below cannot be conditional. So the
    # path always exists; on a non-tag build it is a ~200 byte zip that says so,
    # which is honest about what it is and costs nothing.
    #
    # CHANNEL is set by the runner from the channels map below, so "production"
    # means this build came from a v* tag.
    #
    # Prerequisites are BUILD ARTIFACTS ON THIS HOST, not built here: the fork's
    # Windows template (~9min) and the three Windows GDExtension DLLs (dc-terrain,
    # hyperswarm-peer, proc-nodes). The script
    # fails loudly if the export is missing; the same assert-don't-build pattern
    # ci.yaml already uses for the Linux template.
    run: |
      mkdir -p dist
      rm -f dist/hypertps-win64.zip
      if [ "$CHANNEL" = "production" ]; then
        GODOT=/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64
        TEMPLATE=/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.windows.template_release.x86_64.exe
        test -x "$GODOT" || { echo "ERROR: godot-rtx editor missing at $GODOT"; exit 1; }
        test -f "$TEMPLATE" || { echo "ERROR: WINDOWS export template missing at $TEMPLATE — build it with 'scons platform=windows target=template_release use_mingw=yes d3d12=no winrt=no accesskit=no angle=no'"; exit 1; }
        # The EXPORT HAPPENS HERE, not in a prior step: build/ is gitignored, so
        # the git-archive extraction CI runs in has no build/windows/ at all and
        # the bundle script's precondition would fail. Same reasoning as
        # export-godot above — DISPLAY=:0 and a real Vulkan device, so the shader
        # baker actually runs rather than silently baking nothing.
        mkdir -p build/windows
        DISPLAY=:0 "$GODOT" --rendering-driver vulkan --export-release "Windows" "$PWD/build/windows/hypertps.exe"
        test -f build/windows/hypertps.exe || { echo "ERROR: windows export produced no exe"; exit 1; }
        test "$(stat -c%s build/windows/hypertps.pck 2>/dev/null || echo 0)" -gt 100000000 || { echo "ERROR: windows .pck missing or implausibly small"; exit 1; }
        # No GDExtension DLL may be packed INTO the .pck: FileAccess::exists() then
        # finds the packed copy of the relative bin/windows/<dll>, the exe-dir
        # fallback never runs, and LoadLibraryExW fails with Error 126 for every
        # extension (v0.1207.0). The Windows bundle keeps DLLs beside the exe, so a
        # packed one is always wrong here. (Linux packs its .so too, but its install
        # tree ships a real bin/linux/ beside the binary, so the paths line up.)
        python3 tools/pck-ls.py build/windows/hypertps.pck --forbid '*.dll' || { echo "ERROR: a DLL is packed into the Windows .pck — check export_presets.cfg include_filter"; exit 1; }
        ./tools/build-windows-bundle.sh "$PWD/dist/hypertps-win64"
        (cd dist && zip -rq hypertps-win64.zip hypertps-win64)
      else
        mkdir -p dist/_stub
        echo "The Windows bundle is built for tagged releases only (v*). This build was CHANNEL='${CHANNEL:-none}'." > dist/_stub/NOTE.txt
        (cd dist/_stub && zip -q ../hypertps-win64.zip NOTE.txt)
      fi
      ls -la dist/hypertps-win64.zip
    timeout: 1800

# Files only — collectArtifacts has no dir walk and no glob, which is why the
# bundle is a single zip rather than a tree.
artifacts:
  - dist/hypertps-win64.zip

# Tags publish to `production`; branches stay on the default (none). This is what
# gates the Windows bundle above, and what pins a version against GC: the release
# manager's gc() keeps every version a channel points at.
channels:
  production:
    tags: ["v*"]