Content engine: the build plan
Content engine + Studio Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Ship an app-resident content engine (blocks + document runtime + logic graph + tiny formulas), an agent-first Studio (headless CLI + Mac UI), a gated publish pipeline (sbapi registry, content.stickbash.com, signed packs, staged rollout) and an exact-match proof set. All new skins, maps, kill films and taunts then ship from our server with no app update.
Build order (the user, 6 Nov: "u build one by one, first map engine, then animation engine, like that"): the engines are built one after another, each finished end to end before the next starts:
- Stage 1: the shared core, only what maps need.
- Stage 2: the MAP engine.
- Stage 3: the ANIMATION (kill film) engine.
- Stage 4: the SKIN engine.
- Stage 5: the TAUNT / EMOTE engine.
- Stage 6: the shared catalogue, Studio polish and launch readiness.
Within an engine, tasks marked [par] can split between two lanes.
Architecture:
- Game. A Unity-free core
(
Scripts/Engine/Core/) holds the document model, the compiler, quantisation, the logic graph, fixed-point formulas and the pack format, and is unit-tested headless in seconds. On top of it, Unity-side block families wrap today's tested code paths: the page-film engine,ArenaDef/MapScript, cosmetics recipes and the taunt rig. The Studio is the internal player build with-scenario studio:*. - Server. sbapi gains a
contentmodule, and Caddy behind Cloudflare serves immutable content-addressed files.
Tech Stack:
- Unity
6000.3.26f1(C#, IL2CPP), Newtonsoft JSON (com.unity.nuget.newtonsoft-json3.2.2), Unity's bundled Roslyn + NetCoreRuntime for headless core tests. - Go (sbapi), Caddy, Cloudflare.
- zsh tooling (
unity.sh,build_test.sh,cap.sh,net_test.sh,tools/csc_check.sh,tools/locked.sh).
Spec:
docs/superpowers/specs/2026-11-06-content-engine-design.md
(APPROVED 6 Nov; all 15 decisions made). Section numbers below (§x)
refer to it. Executors read both.
Global Constraints
- No downloaded executable code. Packs are
declarative data, strings, audio and images. There is no scripting
language. The only expression type is the tiny formula set of §5.5a: ops
add sub mul div(÷0 = 0) min max clamp abs lerp step, ≤ 32 nodes, no loops, recursion, variables, functions or strings, and a server kill switch to each slot's mandatory fallback constant. - Bundles hold only
AudioClip,Texture2D(andFont). NoMonoScript, shader, custom-shader material or component prefab. Same Unity version6000.3.26f1. - Logic graph bounds per document:
- ≤ 256 nodes, ≤ 1024 wires, ≤ 32 state nodes;
- ≤ 64 sinks firing per tick, logic state ≤ 64 B;
- one evaluation pass per tick, actions visible to triggers on the next tick;
- counters i16, saturating.
- Formula budgets: ≤ 64 formulas per document. Per
LOW frame ≤ 2,048
lookformula nodes; per fixed step ≤ 1,024simformula nodes.simformulas use Q16.16 int32, saturating, division rounding toward zero. - Determinism:
- Content numbers are quantised once at compile: ticks, mm, 1/65536 turn, 1e-4 for dimensionless numbers.
- Engine wire messages use
EngineWriter:ContentRef(u8 room-table index + u16 local index),Tick,Seed, small ints, enums andPos(int16 mm). It has no F32 / Half. - Random draws use seeded streams with an FNV-1a seed. Never
string.GetHashCode(except the proof maps' director-compat stream).
- Do not touch legacy content paths.
FilmStart,PageKillWire(append-only), emote ids 0-33 and cosmetic u8 slot indexes stay byte-for-byte as they are. Existing content stays as it is. - Protocol:
- 144 → 145 in Stage 2 (the map engine: room table,
pack-set hash, arena
ContentRef, the generic snapshot block, the relay), withNfOldRoomProtocol-style refusal. - Each later engine adds its messages with one more bump:
EngineFilmStart→ 146 (Stage 3), the loadout extension → 147 (Stage 4), the tauntContentRef→ 148 (Stage 5). - This is allowed only because no player builds exist before launch. The launch build carries the last number.
- 144 → 145 in Stage 2 (the map engine: room table,
pack-set hash, arena
- Performance:
- Frame p95 ≤ 16.7 ms, max ≤ 33 ms, begin ≤ 16.7 ms, 0 B GC per frame
in steady state, on LOW / MED / HIGH and with
-perfslow 6. - GPU numbers come from the Mac / M2 only; the VM reports CPU ms only.
- The budget meter never drops or changes a
simpart.
- Frame p95 ≤ 16.7 ms, max ≤ 33 ms, begin ≤ 16.7 ms, 0 B GC per frame
in steady state, on LOW / MED / HIGH and with
- Gore LOW 0 / MEDIUM 2 / EXTREME 5. Every new film authors all three levels (D11). New films must be the same tick length and have the same online state at every level. Imported legacy takes use the legacy ladder mode.
- Packs:
- ≤ 5 MB per pack per platform and ≤ 150 MB cache, with LRU eviction; owned items' current and previous live packs are pinned (D6).
- Content identity = sha256 of the content only. The envelope
(
.sbe: gate report, Unity version, texture formats, approval, Ed25519 signature) sits beside it.
- Approval: only a human dashboard account can move a
pack to
live(D2). Rollout is internal → test → 10 % for a day → everyone, with a one-tap pull-back (D9). - Phone download: the splash downloads required / new packs before the menu with "Downloading new content… X / Y MB". Offline, it retries 3 times over ~10 s, then continues with the cache plus a "new content waiting" note; it never blocks forever. Rooms download a missing playable pack with a progress bar before joining (D4, D5).
- Exact match (proof set): identical, zero difference per frame in shapes, sounds and positions, same machine, same gore level, same seed (D7).
- Proof set:
- Maps: Backyard, Belt Line, Wrecking Yard.
- Films: SIX-SHOOTER, THE X, DRIP, HIGH NOON, COLD-BLOODED, THE OFFERED HAND.
- Cosmetics: ICE revolver, one PLAIN skin, HOODIE, CROWN, FIRE aura, RAINBOW trail.
- Taunts: LOSER, CHAI BREAK, DAB, BHANGRA (D8).
- Repos:
- Game code: branches off
live-v4train7inStickFight-online-train7. - Server: branches off
maininstickbash-online. - Content: new private repo
farisbasha/stickbash-content(D15). - Commit by path, never
commit -a, no stash. - Game builds rewrite
ProjectSettings.asset: restore it after builds.
- Game code: branches off
- Lab code sits under
#if !SB_STORE. The Studio never ships in a store build. - Mac load rules: heavy steps go to the M2 / VM
first, under the load bands (< 20 normal, 20-35 no new heavy local
steps, > 35 hold). One Unity editor at a time through
tools/locked.sh.
Review Focus
- A phone that is offline or slow at app open. The
splash must give up after ~10 s and enter the menu with cached packs. A
half-downloaded pack must never register. Pinned packs must survive an
LRU sweep. Test: Task C.3
partial_download_never_registers,lru_keeps_pinned; Task C.10splash_offline_continues. - A joiner whose app is older (
ENGINE_SIMdiffers) or outside the rollout bucket. The room must still form, with engine maps not offered and nobody refused. Test: Task M.8older_engine_sim_room_plays_builtins,rollout_bucket_outsider_can_fetch_room_pack. - A document that is valid but pathological.
Examples: a logic graph at every cap at once, a formula dividing by a
signal that is 0, a counter at i16 max that keeps adding. It must run
within budget with no exception and give deterministic output. Test:
Task C.4
logic_caps_all_at_once,counter_i16_saturates; Task C.5formula_div_zero_is_zero. - A pack pulled while a room is playing its map, or while
players own its items. The match finishes unchanged, dependents
cascade, and nothing crashes on the next fetch. Owned items fall back or
hide. Test: Task C.9
kill_list_cascades_dependents; Task M.8pulled_map_finishes_round; Task K.6pulled_owned_item_falls_back. - The same document on ARM (phones, M1 Mac) and x64
(VM). Replicated state bytes, the gore-independent film hash
and Q16.16 formula bits must be identical. Test: Task 2
detspike_cross_arch_digest_equal; Task C.5fixed_golden_vectors.
Stages, sizes and lanes
Sizes are lane-days of build. "Effective" adds ~15 % for review rounds. Each stage ends with its engine's done definition passing.
| Stage | What | Build (lane-days) | Effective | Can split into 2 lanes? |
|---|---|---|---|---|
| 1 Shared core for maps | the 3 map de-risk spikes, the document model + schema generator, the pack format + hashes + store, the loader / validator / budget meter, the logic graph, the formula set (map slots), seeded streams, the agent CLI (validate / lint / explain / preview / play / gate / diff / publish-to-test), the server registry + content list + kill list + approval, content.stickbash.com, the phone fetcher + splash | 36-42 | 41-48 | yes: core (B) / CLI + server (C) |
| 2 MAP engine | today's map blocks + the 23 new map blocks + the shared T1 blocks, bots on data maps, protocol 145 + online pack checks + relay, the bundle lane (map sounds), proof maps identical, the map gate, Studio map editing (agent-first, then the visual editor), the map playbook | 50-60 | 58-69 | yes: blocks / proof (B) and net / Studio (C) |
| 3 ANIMATION engine | the film runtime spike, the film blocks on the page-film engine, the 16 new film blocks, the kill-page → Studio motion import, the 3 gore levels + online film state, protocol 146, the 6 proof films identical, the Studio film timeline, the film playbook | 42-52 | 48-60 | yes: runtime + proof films (A) and new blocks / import / Studio (C) |
| 4 SKIN engine | the Shapes.shader material / pattern feature, cosmetic
blocks, the 8 new skin blocks, premium attachments (film / ammo / sparks
/ burst / feed), the loadout extension (protocol 147), owned-item
pinning + fallback, the 6 proof cosmetics identical, Studio skin
editing, the skin playbook |
22-27 | 25-31 | yes |
| 5 TAUNT / EMOTE engine | performance blocks, the 6 new taunt blocks, the taunt
ContentRef (protocol 148), the 4 proof performances
identical, Studio taunt editing, the playbook |
12-15 | 14-17 | partly |
| 6 Catalogue + polish + launch | the shared catalogue (shop / pass / rooms / customize / kill shop / album / CatalogExport / sbapi auto-pick), Studio polish across kinds, the reference-phone pipeline step, store notes + the first submission, drills, the end-to-end agent rehearsal | 12-16 | 14-18 (+ a store-review week) | partly |
| Total | 174-212 | 200-243 |
Building one engine at a time costs a little more than the old interleaved plan, about +10 %. Each engine gets its own Studio editing and its own end-to-end done pass, and some core work comes back when a later engine first needs it. In return, each engine is usable as soon as it is finished.
Calendar (honest; agents run daily, review latency included)
| Engine lanes | Map engine done (end of Stage 2) | Launch-ready (end of Stage 6) |
|---|---|---|
| 1 lane | ≈ 14-17 weeks | ≈ 30-36 weeks (7-8 months) |
| 2 lanes (split inside each engine) | ≈ 9-11 weeks | ≈ 19-23 weeks (4½-5½ months) |
| 3 lanes | ≈ 8-9 weeks | ≈ 16-19 weeks (3¾-4½ months) |
- 2 lanes is the sweet spot under the coordinator's cap of 4 shared subagents. That leaves room for a reviewer and other work.
- A third lane helps less than it did in the interleaved plan, because one engine at a time leaves fewer independent tasks. The third lane mostly takes the Studio UI and server work of each stage.
- Each "done" definition is a hard gate: the next engine starts only after it passes.
Lanes inside each stage
- Lane B (core / maps / net) and Lane C (agent CLI / server / Studio) run Stages 1-2.
- In Stages 3-5, Lane A (films / cosmetics) replaces B as the content lane, and C stays on the CLI, Studio and server side.
- Tasks are tagged [seq] (waits for the listed deps) or [par] (can run beside the stage's other [par] tasks once its deps are met).
Writing the detail. The first 3 tasks are written to full step detail below. Every other task carries its files, interfaces, tests and acceptance. The lane that starts a stage first runs a short writing-plans pass to expand that stage's tasks into numbered steps in the same style. No stage starts without its expanded steps committed to this file.
File map
(the game repo, StickBrawl/Assets/_Game/Scripts/)
| Path | Responsibility |
|---|---|
Engine/Core/ |
Unity-free, no using UnityEngine:
Units.cs, Fixed.cs (Q16.16),
Doc.cs, Compiler.cs,
PackFormat.cs, Logic.cs,
Timeline.cs, Formula.cs,
Streams.cs (seeded rng, FNV-1a), Validate.cs
(errors with JSON paths + hints), Schema.cs (block
parameter declarations → JSON Schema), Ed25519.cs (verify
only) |
Engine/Runtime/ |
Unity side: PackStore.cs, EngineLoader.cs,
BudgetMeter.cs, EngineWriter.cs,
RoomTable.cs, ContentFetch.cs (splash +
background), later ContentCatalog.cs |
Engine/Blocks/Map/, Shared/, later
Film/, Cos/, Perf/,
Common/ |
one file per block or small block group; each block is a class with
[Block("id")] and typed Param<T>
fields |
Engine/Map/EngineMap.cs,
EngineMapScript.cs |
map document → ArenaDef + a MapScript
subclass driven by blocks and the logic graph |
later Engine/Film/EngineFilm.cs |
film document → PageKill.Def + hooks driving the
page-film engine (Stage 3) |
Engine/Lab/ (#if !SB_STORE) |
EngineProbe.cs (per-frame digest), the spikes' labs,
Studio*.cs (CLI scenarios + the JSON-RPC server) |
Core/Game.cs:~1504 |
scenario dispatch: add engine:* and
studio:* cases beside case "killsys" |
repo root tools/ |
tools/engine/coretest.sh (headless core tests with
Unity's Roslyn + NetCoreRuntime, the paths of
tools/csc_check.sh:19-32),
tools/engine/tests/*.cs, tools/studio.sh,
tools/engine/studio_smoke.sh |
Server (stickbash-online):
server/sbapi/content/ (new),
server/sbapi/admin/ + dash/ (approval),
server/relay/ (pack-set hash),
deploy/stack/content-site.caddy.tmpl (new). Content repo
(stickbash-content, new): the layout of spec §6.4.
Stage 1: the shared core, only what maps need
What Stage 1 builds and what it defers
| Core part | Stage 1 | Deferred to |
|---|---|---|
| Units + quantisation, document model, compiler, schema generator | ✅ | none |
| Pack format, sha256 identity, envelope, Ed25519 verify, pack store, LRU + pinning (pins for "in a room's table" and "current content list") | ✅ | owned-item pins: Stage 4 |
Loader, validator (paths + hints), budget meter (sim
never dropped) |
✅ | none |
| Logic graph (all node kinds), timeline, counters, state machines | ✅ | none |
Formula set: parser, validator, the
sim fixed-point evaluator, map inputs
(time, alive, score, phase, counters / timers, own anchors' distances,
tier), the kill switch |
✅ | the look evaluator + wearer inputs: Stage 4 (skins);
film inputs: Stage 3 |
| Seeded streams + director-compat | ✅ | none |
EngineWriter / EngineReader |
✅ (used by Stage 2's snapshot and join) | EngineFilmStart: Stage 3 |
EngineProbe digest (shapes + positions + replicated
state) |
✅ | voices + film joints: Stage 3 |
Agent CLI:
schema validate lint explain fmt preview play gate(map checks) diff pack publish --channel internal|test,
serve + hot reload |
✅ | the film / skin / taunt preview modes and gate checks: their stages |
sbapi content: packs, item history, reverse deps,
roles, content list, kill list, the minimal approval page |
✅ | owned-item rules on the dashboard: Stage 4 |
content.stickbash.com (Caddy template + the human
Cloudflare runbook), phone fetcher + splash progress |
✅ | none |
| Unity Editor bundle lane | ❌ | Stage 2 (Task M.9: the first map sounds) |
| ContentCatalog (shop / pass / customize) | ❌ (maps only need the room / vote listing: Stage 2) | Stage 6 |
| Gore rules, legacy ladder, film online state | ❌ | Stage 3 |
Shapes.shader material feature, loadout extension |
❌ | Stage 4 |
Task 1: Map document + loader spike: Backyard rebuilt from a document, identical [par] (Lane B, 4-5 days)
Retires: can a map document reproduce a code-built
arena exactly, with the same ArenaDef, the same bot
navigation and the same frames (D7)? The map export is lossy
(MapExportLab.cs:29), so this proves the exact-geometry
path.
Files:
- Create:
Engine/Lab/EngineProbe.cs,Engine/Lab/MapSpikeLab.cs,Engine/Map/SpikeMapDoc.cs(a throw-away JSON →ArenaDefadapter formap.frame,map.solidfloor / one-way,map.spawnsincl. extra spawns, the theme id),tools/engine/spike_docs/backyard.json - Modify:
Core/Game.cs(case "engine:mapspike"),Render/ShapeBatch.cs(a#if !SB_STOREstaticProbecallback once per batch upload: batch name, vertex count, the verts span; null in normal runs),Level/Arenas.cs(register the doc's arena only in lab runs, appended after every built-in so no index shifts,hidden = true) - Test: the lab (
./cap.sh "engine:mapspike"), output lines[ENGMAP]
Interfaces:
- Produces:
EngineProbe.Begin(string runId),EngineProbe.FrameDigest() -> ulong(FNV-1a over everyShapeBatchvertex quantised to 1e-4 in batch order, every fighter's root + 13 joints ×1e2, everyMapPlatpose in mm),EngineProbe.End() -> ProbeReport { int frames; ulong[] digests; int firstDiff; }. This is the exact-match harness of every later proof task. - Produces:
SpikeMapDoc.Build(string jsonPath) -> ArenaDef(ideng.spike.backyard).
-
- (a)
ArenaDeffield by field: everyPlatDef(rect, kind, ice, conveyor, look, rot, a, b), spawns, extra spawns, bounds, camArea, killArea, focusArea, fitSize, middle, skyY, dropRange, theme. Printed as[ENGMAP] def <field> equal|DIFF <a> <b>. - (b)
NavGraph: the same spans and links (kind, take / land x, cost, air). Printed as[ENGMAP] nav spans=<n> links=<m> equal|DIFF. - (c) A frame-by-frame run: a seeded 3-bot
deathmatch, 20 s, seed 11, on each arena, at
-devicetier low|medium|high. Printed as[ENGMAP] run q=<tier> frames=<n> firstDiff=<i|-1>.
It ends with
[ENGMAP] PASSonly when all are equal. - (a)
git add StickBrawl/Assets/_Game/Scripts/Engine StickBrawl/Assets/_Game/Scripts/Core/Game.cs StickBrawl/Assets/_Game/Scripts/Render/ShapeBatch.cs StickBrawl/Assets/_Game/Scripts/Level/Arenas.cs tools/engine/spike_docs/backyard.json
git commit -m "engine spike M1: Backyard from a map document, identical ArenaDef / NavGraph / frames; EngineProbe"Acceptance: [ENGMAP] PASS at all three
tiers, and the results note committed.
Task 2: Logic-graph determinism spike: fixed point, the graph, the float-free writer, across ARM and x64 [par] (Lane B or a second lane, 4-5 days)
Retires: can replicated state and formulas be
bit-identical across the Mac (ARM64), the VM (x64) and phones, and can a
generic payload replace hand-written Write /
Read (spec §5.7, MapWreck.cs:341-352)?
Files:
- Create:
Engine/Core/Fixed.cs,Engine/Core/Logic.cs(spike subset: sourcestick,zone; logicAND,NOT,toggle,delay,timer; sinkdoor.open),Engine/Core/Streams.cs(FNV-1a seed + xorshift128+),Engine/Runtime/EngineWriter.cs,Engine/Lab/DetSpikeLab.cs,tools/engine/coretest.sh,tools/engine/tests/Run.cs,FixedTests.cs,LogicTests.cs,StreamsTests.cs,tools/engine/spike_docs/det-door.json(two zones → AND → a sliding door piece, plus a formula-driven mover speed) - Modify:
Core/Game.cs(case "engine:detspike")
Interfaces:
- Produces:
struct Fx: Q16.16,Fx.FromRaw(int),Fx.Of(int milli),+ - * /saturating,/by 0 = 0,Min Max Clamp Abs Lerp Step.static class Fnv { ulong Hash(ReadOnlySpan<byte>) }.sealed class Stream { Stream(ulong seed); uint Next(); int Range(int lo, int hi); int Draws { get; } }.sealed class LogicGraph { static LogicGraph Compile(LogicDoc d, out List<Error> errs); void Tick(ISignals inputs, ISinks outputs); int StateBytes { get; } void Write(EngineWriter w); void Read(EngineReader r); }.sealed class EngineWriter { U8 U16 U32 I16 Tick(uint) Seed(uint) Ref(ContentRef) Pos(int mmX, int mmY) }andEngineReadermirroring it. No float methods.
- Consumed by: Tasks C.4-C.6 (they harden these into the full core).
-
fixed_golden_vectors: 64 (a, b, op) → raw results written in the test file, covering saturation at ±32767.99998,/0 = 0, negative rounding toward zero,Lerpclamping t.fixed_saturates.formula_div_zero_is_zero(through a 3-node AST).streams_fnv_seed_stable:Fnv.Hash(utf8("wreck"))equals the constant printed by an independent FNV-1a 64 implementation (Python one-liner in the test comment).streams_draw_count.logic_one_pass: a toggle wired to itself throughdelay 1flips once per tick.logic_caps_all_at_once: 256 nodes, 1024 wires, 32 state nodes, 64 sinks per tick. It runs,StateBytes ≤ 64, no exception.writer_has_no_float: reflection finds noEngineWritermethod takingfloat,doubleorHalf.
Run
tools/engine/coretest.sh. Expected: FAIL (types missing).
Acceptance: coretest green on Mac + VM with equal
digests, the phone digest equal, [ENGDET] PASS host +
client, and writer_has_no_float green.
Task
3: Agent CLI skeleton: studio.sh serve, validate, preview
of a map, < 30 s edit → still [par] (Lane C, 3-4 days)
Retires: can agents iterate on maps without Unity builds (spec §6.5), and do the RPC + capture paths work on Mac, M2 and VM?
Files:
- Create:
tools/studio.sh,Engine/Lab/StudioServe.cs(scenariostudio:serve:TcpListeneron127.0.0.1:<port>, newline-delimited JSON-RPC 2.0,FileSystemWatcheron--content <dir>),Engine/Lab/StudioCommands.cs(ping,validate,preview),Engine/Core/Validate.cs(spike subset:Error { string path; string rule; string found; string allowed; string hint; }),tools/engine/studio_smoke.sh,tools/engine/spike_docs/tiny-arena.json(a one-screen arena: a floor, two ledges, four spawns, one ladder) - Modify:
Core/Game.cs(case "studio:serve","studio:validate","studio:preview")
Interfaces:
- Produces the CLI contract every later command follows:
studio.sh <cmd> [args]prints exactly one JSON object on stdout:{ "ok": bool, "cmd": str, "errors": [Error], "warnings": [Error], "outputs": [path], "ms": int, "seed": int|null }.- Exit codes: 0 ok, 1 errors, 2 warnings only, 3 tool failure.
- When
serveruns (port file~/.stickbash-studio/<worktree>.port), the CLI forwards over RPC. Otherwise it launches a one-shot batchmode player, ascap.shdoes.
- Produces:
StudioCommands.Preview(DocRef doc, PreviewOpts o) -> List<string> pngPaths(o.atin s or ticks,o.cam fit|follow,o.seed,o.w / o.h). - Task 1's
SpikeMapDoc.Buildis reused for the tiny arena (coordinate on the file if both run at once; Task 3 may copy the adapter and merge later).
-
- Start
studio.sh serve --content tools/engine/spike_docsin the background. ping→ok:true.validate tiny-arena.json→ok:true.- A copy with the ladder's top below its bottom →
ok:false, exit 1, an error at path/blocks/3/p/y1with a non-emptyhint. preview tiny-arena.json --at 1s --cam fit --seed 3→ one PNG listed, and the file exists.- Move a ledge in the doc and time the file write → the new
PNG on disk; assert
< 30000 ms. - Stop the server.
Run. Expected: FAIL (no
studio.sh). - Start
Acceptance: the smoke test passes on Mac, M2 and VM, edit → still < 30 s on the Mac and M2, and the contract documented.
Stage 1 core tasks (after the spikes)
| Task | What | Files | Key tests | Tag / lane | Size |
|---|---|---|---|---|---|
| C.0 | Freeze the contracts from the spikes: the document + block-param
schema model ([Block], Param<T> with
name type unit grid min max default expr inputs), the pack
/ envelope format, the room table + join fields, the CLI / RPC
contract |
Engine/Core/Schema.cs;
stickbash-online/docs/contract/engine-{docs,pack,wire}.md,
studio-cli.md |
schema_roundtrip_tiny_arena,
schema_expr_flag_on_every_param, golden byte fixtures
committed |
seq, B + C | 3 |
| C.1 | Units + quantisation
(Units.Parse(string lit, Dim want) -> Q; tick 1/60 s, 1
mm, 1/65536 turn, 1e-4) |
Engine/Core/Units.cs |
units_1_2s_is_72t,
units_below_one_tick_is_error,
units_reject_bare_length,
units_mismatch_hint |
seq, B | 1 |
| C.2 | Document model + compiler + schema generator
(Doc.Parse,
Compiler.Compile(Doc, IRefResolver, out errs) -> CompiledDoc,
Schema.ToJsonSchema(kind)) |
Doc.cs, Compiler.cs,
Schema.cs |
compile_is_deterministic_bytes (Mac = VM sha256),
compile_strips_studio_block,
refs_builtin_and_pinned_deps_only,
refs_reject_cycles |
seq, B | 4 |
| C.3 | Pack format, sha256 identity, envelope, Ed25519 verify, pack store, LRU + pins | PackFormat.cs, Ed25519.cs,
Engine/Runtime/PackStore.cs |
ed25519_rfc8032_vectors,
pack_hash_excludes_envelope,
tampered_content_rejected,
partial_download_never_registers,
lru_keeps_pinned, pack_over_5mb_rejected |
par, B | 3 |
| C.4 | Timeline + the full logic graph (every node kind of spec §5.5) + state machines | Logic.cs, Timeline.cs |
logic_every_node_truth_table,
logic_cycle_without_state_node_is_error,
logic_caps_all_at_once,
timeline_action_visible_next_tick,
counter_i16_saturates |
par, B | 3 |
| C.5 | Formula set, map slots: parser (≤ 32 nodes), validator (ops, the
slot's allowed inputs, units), the sim Q16.16 evaluator,
the map inputs, the kill switch → else |
Formula.cs |
formula_33_nodes_rejected,
formula_unknown_input_path_pos_hint,
formula_div_zero_is_zero,
formula_units_checked,
formula_kill_switch_uses_else,
fixed_golden_vectors (cross-arch digest) |
par, B | 2 |
| C.6 | Seeded streams + director-compat (reproduces
MapEvents.cs:314's seed and the shared draw order, for
proof maps only) |
Streams.cs,
Engine/Runtime/DirectorCompat.cs |
streams_seed_from_ids_fnv,
compat_stream_matches_map_director (lab: 1,000 draws equal
on wreck) |
par, B | 1-2 |
| C.7 | Loader (off the main thread), the full validator (all errors, path /
rule / found / allowed / hint), the budget meter (sim never
dropped) |
EngineLoader.cs, Validate.cs,
BudgetMeter.cs |
validate_reports_all_errors,
budget_meter_never_drops_sim,
loader_zero_gc_after_warm |
par, C | 3 |
| C.8 | Agent CLI complete for maps:
schema lint explain fmt play gate(map) diff pack publish --channel internal|test;
block reference pages generated with examples |
Engine/Lab/Studio*.cs,
tools/studio.sh |
the spec §6.2 lint rules (one fixture each),
explain_mentions_every_cue, fmt_idempotent,
play_same_seed_same_report,
play_report_has_all_metrics,
diff_params_cost_logic_visual,
gate_fails_with_block_name_over_budget,
gate_runs_formulas_off_too |
par, C | 6-7 |
| C.9 | sbapi content: packs, item history, reverse deps, roles
(agent tokens ≤ gated / test; only human
accounts approve live), the content list and the
only-growing kill list (separate versioned documents), the minimal
dashboard approval page |
server/sbapi/content/, admin/,
dash/ |
go test ./server/sbapi/...:
TestAgentTokenCannotGoLive,
TestItemHistoryOrder,
TestContentRollbackLeavesConfig,
TestKillListNeverRollsBack,
kill_list_cascades_dependents,
TestDashApproveRequiresHuman |
par, C | 6-7 |
| C.10 | content.stickbash.com + phone fetch:
deploy/stack/content-site.caddy.tmpl (Origin CA;
/p/ /e/ /b/ only;
Cache-Control: public, max-age=31536000, immutable; else
404) + the runbook for the human Cloudflare steps
(proxied DNS record, cache rule: cache everything, edge TTL ≥ 1 month).
Agents never change DNS or Cloudflare. ContentFetch handles
the splash progress ("Downloading new content… X / Y MB"), the 3 retries
over ~10 s and the "new content waiting" note |
deploy/stack/,
Engine/Runtime/ContentFetch.cs, the splash UI |
caddy validate; after the human steps
curl -sI shows cf-cache-status: HIT on a
second fetch; splash_offline_continues,
splash_progress_text_exact,
splash_never_blocks_over_15s |
par, C | 3-4 |
| C.11 | Bootstrap stickbash-content. Coordinator
step: create the private repo
farisbasha/stickbash-content (D15). Then the layout of spec
§6.4, the generated schema, templates (one-screen arena, MEDIUM follow
map, hazard arena, logic-puzzle arena) and make check |
the content repo | every_template_validates |
par, C | 1 |
Stage 1 done when coretest is green on Mac + VM +
phone, every C task's tests pass, and an agent can
publish --channel test a tiny static map pack that a test
phone downloads at the splash and plays offline. Online play comes in
Stage 2.
Stage 2: the MAP engine, complete
Done definition (the coordinator, 6 Nov): an agent makes a brand-new map from the CLI, previews it, bot-tests it, passes the gates, publishes it to the test phones, and it plays online. In addition, the proof maps Backyard / Belt Line / Wrecking Yard are identical (D7), and the map Studio editing works agent-first, then visual.
| Task | What | Key tests | Tag / lane | Size |
|---|---|---|---|---|
| M.1 | Static map blocks → ArenaDef:
map.frame solid traverse liquid spawns prop (from Task 1's
adapter, made real) |
engine_backyard_arenadef_equal,
navgraph_equal_backyard, the same for a lifts / pads /
low-g fixture vs Military Base's pieces |
seq, B | 3 |
| M.2 | EngineMapScript + map.piece (path / pivot
/ weight seesaw / breakable / crumble) + map.hazard
(pendulum / kill zone / damage / push / magnet) +
map.timeline + map.event + the generic
payload |
generic_payload_roundtrip,
hazards_off_disables_all,
quiet_stretch_holds_cues,
pace_calm_skips_chaos_only |
seq, B | 5-6 |
| M.3 | map.art map.look map.elements + nav hints for pieces;
the 58k JSON by-product |
elements_export_equals_MapExportLab_for_proof_maps,
nav_hint_avoid_ride_wait |
par, B | 3 |
| M.4 | New map blocks, group 1:
map.zone switch door plate mover rotor (spec §7.2 rows 2-6,
19) |
per block: <b>_predictable_on_client,
<b>_bot_rule_navgraph,
<b>_budget |
par, B or C | 4-5 |
| M.5 | New map blocks, group 2:
map.laser saw crusher spikes dropper spawner (rows
7-12) |
the above + <b>_warning_at_least_0_6s,
<b>_hazards_off |
par | 5-6 |
| M.6 | New map blocks, group 3:
map.level field belt phase tiles bumper portal (rows 13-18,
20) |
the above + portal_own_fighter_predicted,
tiles_bitmask_snapshot |
par | 5-6 |
| M.7 | New map blocks, group 4: map.weather camrule ambience +
the shared T1 blocks a map uses
(curve rng.stream cond path strings palette.set variance shape.lib) |
weather_night_lock_respected,
variance_seeded |
par | 3-4 |
| M.8 | Net, protocol 145: RoomTable, join fields
(packSetHash, engineApi,
engineSim, formulas), the arena
ContentRef in room settings / vote / match start, the
generic snapshot block, the relay server change,
fetch-before-join with a progress bar, the no-internet Wi-Fi rule,
refusal of 144 |
room_table_same_bytes_all_members,
proto144_refused_cleanly,
older_engine_sim_room_plays_builtins,
rollout_bucket_outsider_can_fetch_room_pack,
wifi_no_internet_intersection,
pulled_map_finishes_round,
go test ./server/relay/...
TestJoinPackSetMismatchRefused,
net_test.sh engmap 3 all [ENGNET] PASS |
seq after M.2, C | 6-7 |
| M.9 | The Unity Editor bundle lane for map sounds
(ProjectSetup.BuildContentBundles: iOS / Android / Mac /
Linux; Mac / M2 first, then the VM's editor) + the bundle checker |
bundle_with_monoscript_rejected,
bundle_envelope_records_unity_and_formats |
par, C | 3 |
| M.10 | Engine maps in the room picker / vote / presets (the map slice of the catalogue; the full catalogue is Stage 6) | engine_map_listed_and_votable,
hidden_when_engine_sim_differs |
par, C | 1-2 |
| M.11 | Proof maps identical, one task per map: Backyard, Belt Line, Wrecking Yard (director-compat stream) | engine:proof:map:<id>: per-tick full state +
EngineProbe zero diff, 3 rounds × 2 / 4 / 8 bots × CALM / NORMAL / CHAOS
× HAZARDS on / off × LOW / MED / HIGH; then host + client online
equal |
par per map, B | 6-8 |
| M.12 | The map gate complete (spec §5.12 for maps: coverage, look checks, fairness, the reference-phone run step) | gate_map_spawn_near_hazard_fails,
gate_map_unreachable_spawn_fails, phone run digest
equal |
par, C | 2-3 |
| M.13 | Studio map editing, visual: the shell + library, the inspector with the expr editor, the logic-graph view with live signals, try-it vs bots (all over the CLI's documents and commands) | inspector_edit_writes_same_json_as_fmt,
graph_view_roundtrip_no_change,
try_it_loads_without_unity_build |
par, C | 6-8 |
| M.14 | The map playbook + recipes (door with two plates, a timed hazard
cycle, rising lava at two left, a three-target vault, a seeded random
event) + the annotated proof maps in examples/proof/ |
every_recipe_validates,
every_recipe_gates_fast |
par, C | 1-2 |
| M.15 | The done rehearsal: an agent gets a one-line brief
("a MEDIUM map: two rotating wheels over a rising-water pit, a switch
that reverses them"). It follows the playbook to
publish --channel test, the test phones download it at the
splash, and a 2-phone online room plays it |
the rehearsal report: CLI transcript, stills, play metrics, gate
report, the online [ENGNET] PASS |
seq (last), all | 1-2 |
Stage 3: the ANIMATION (kill film) engine
Done definition: an agent makes a brand-new film from a kill-page take plus Studio blocks at all three gore levels, previews it, gates it on every fitting arena, publishes it to test phones, and it plays online (host + client hash equal). The 6 proof films are identical.
| Task | What | Key tests | Tag / lane | Size |
|---|---|---|---|---|
| F.0 | Film runtime spike (formerly Task 1 of the
interleaved plan): SIX-SHOOTER from a document + a DRIP hybrid (its slab
and drip line as film.prop / film.mark blocks)
drive the page-film engine with zero difference;
EngineProbe gains voices + film joints |
[ENGSPIKE] PASS on 18 cases (2 films × 3 gore × 3
tiers) |
seq (first), A | 4-5 |
| F.1 | The tag pass of all 155 film scripts' hooks against the block list; GAPs become blocks or "legacy only" (a spec update on this branch) | the results table | par, A + C | 2-3 |
| F.2 | film.take (incl. the legacy ladder mode)
cast fit |
cast_gun_class_track_of,
fit_refusals_equal_builtin |
seq, A | 3 |
| F.3 | film.weaponprop prop mark cut blood soul trackedit posefix order handoff |
one golden lab scene per block | par, A | 5-6 |
| F.4 | Common film-side blocks: look grade camera clock, the
film formula inputs (film time, time since trigger) |
golden scenes | par, A | 2-3 |
| F.5 | Film online state + EngineFilmStart (protocol 146) |
film_online_state_gore_independent,
legacy_ladder_hash_matches_builtin,
engine_film_host_client_hash_equal (net_test) |
seq, A | 3 |
| F.6 | The 16 new film blocks (spec §7.3 rows 1-16) in two groups: camera /
time (cam.* time.stutter) and fx / sound
(fx.* snd.*) |
per block: golden scene, budget,
<b>_never_changes_online_state |
par, C + A | 9-11 |
| F.7 | The kill-page → Studio motion import:
studio.sh import take <bake dir> wraps
tools/killport (pk_pack + pk_pack2) and writes
takes/*.bytes + a draft film document at three gore
levels |
import_roundtrip_equals_pk_pack2_verify |
par, C | 2-3 |
| F.8 | Proof films identical, one task per film: SIX-SHOOTER, THE X, DRIP, HIGH NOON (every shipping class), COLD-BLOODED, THE OFFERED HAND | engine:proof:film:<key>: zero diff × gore 0 / 2 /
5 × LOW / MED / HIGH × home + 3 fitting arenas + the GLOAT hand-off |
par per film, A | 10-14 |
| F.9 | The film gate + preview modes (every fitting arena, every level, the look checks of PORT_GATE §4) | gate_film_all_arenas_report |
par, C | 2 |
| F.10 | The Studio film timeline (tracks per block, scrubber, gore / tier / mirror toggles) | timeline_edit_writes_same_json_as_fmt |
par, C | 3-4 |
| F.11 | The film playbook + the done rehearsal | the rehearsal report | seq (last) | 1-2 |
Stage 4: the SKIN engine (+ attachments)
Done definition: an agent makes a brand-new premium weapon skin (look + an attached engine film + ammo + sparks + burst + feed icon) and a new body cosmetic. They gate, publish to test, show in matches, films and previews, travel online in the loadout, and a pulled pack falls back correctly. The 6 proof cosmetics are identical.
| Task | What | Key tests | Tag / lane | Size |
|---|---|---|---|---|
| K.1 | The Shapes.shader material + pattern feature
(app-shipped, precompiled) +
mat.layers mat.pattern mat.animate |
shader_variants_unchanged_count, per-block golden
scenes, perf on LOW |
seq, A | 5-6 |
| K.2 | cos.item art premium filmrule + the look
formula evaluator + wearer inputs (own state only) |
formula_cosmetic_cannot_read_other_player,
premium_attachments_resolve |
par, A | 3-4 |
| K.3 | New skin blocks
cos.anchors+ react emitter ribbon aura |
per block + <b>_no_advantage_caps |
par, C | 4-5 |
| K.4 | Loadout extension (protocol 147): 192 B cap, ContentRef
per engine slot, the drop order, announced cosmetic packs in the room
table |
loadout_worst_case_fits,
drop_order_plain_first,
friend_outfit_default_until_fetched |
seq, A | 2 |
| K.5 | Proof cosmetics identical: ICE revolver (with COLD-BLOODED as its film), PLAIN skin, HOODIE, CROWN, FIRE aura, RAINBOW trail | engine:proof:cos:<id>: zero diff idle / run /
jump / a film / a preview card × tiers |
par per item, A | 5-6 |
| K.6 | Owned-item rules: pin current + previous live, the fallback, hide-and-return, the dashboard's owned view | pulled_owned_item_falls_back,
lru_keeps_owned,
engine_item_owned_survives_revision,
old_save_loads_unchanged |
par, C | 2 |
| K.7 | Studio skin editing + the skin playbook + the done rehearsal | the rehearsal report | seq (last) | 2-3 |
Stage 5: the TAUNT / EMOTE engine
Done definition: an agent makes a brand-new taunt with a prop and a gloat variant, and a new emote. They gate, publish to test and play online for everyone in the room. The 4 proof performances are identical.
| Task | What | Key tests | Tag / lane | Size |
|---|---|---|---|---|
| T.1 | perf.pose face words prop clip gloat kind |
perf_track_equals_TauntRig_build (LOSER's keys =
TauntDefs.cs's) |
seq, A | 3 |
| T.2 | New taunt blocks
perf.blend layer toss fx aim sting |
per block | par, C | 3-4 |
| T.3 | The taunt ContentRef in Ev.Taunt (protocol
148) |
engine_taunt_plays_on_every_client |
seq, A | 1 |
| T.4 | Proof performances identical: LOSER (with the lasting L), CHAI BREAK, DAB, BHANGRA | engine:proof:perf:<id> zero diff |
par per item, A | 3-4 |
| T.5 | Studio taunt editing + the playbook + the done rehearsal | the rehearsal report | seq (last) | 2-3 |
Stage 6: the shared catalogue, Studio polish, launch readiness
| Task | What | Key tests | Tag / lane | Size |
|---|---|---|---|---|
| X.1 | ContentCatalog + built-in adapters across shop / pass /
customize / kill shop / album / previews; CatalogExport
with engine; sbapi season auto-pick by
ENGINE_API + live-on-platform |
catalog_lists_builtins_unchanged (same ids, order and
prices as today's CatalogExport.Json()),
TestAutopickSkipsUnplayableEngineItems |
seq, A | 5-6 |
| X.2 | Studio polish across kinds (one library, cross-kind search, consistent inspectors, error panels) | studio_ui_opens_every_proof_doc |
par, C | 3-4 |
| X.3 | Hardening: the reference-phone run as a step for every pack (iOS + a
LOW-tier Android the coordinator names); drills: rollback, kill-list
cascade, formulas: off, a bad-signature pack, the CDN
origin down |
the drill reports | par, all | 2-3 |
| X.4 | Store: review notes (spec §5.15, §5.5a) and the first submission with the engine and only built-in content live; budget one resubmission week | approved builds | seq, coordinator | 1 + the review wait |
| X.5 | The launch rehearsal: one agent per kind from a one-line brief to
test, and a person approves one item to live
at 10 % |
spec §11 items 1-6 green | seq (last) | 1-2 |
Self-review (done)
- Spec coverage:
- §5.1-5.4 → C.* / M.* / F.* / K.* / T.*;
- §5.5 / 5.5a → C.4 / C.5 (+ K.2 for
lookformulas); - §5.6 → F.5; §5.7 → Task 2, C.6, M.8, F.5;
- §5.8 → C.0, M.8, F.5, K.4, T.3; §5.9 → M.9;
- §5.10 → M.13, F.10, K.7, T.5, X.2; §5.11 / §6 → Task 3, C.8, C.11, M.14;
- §5.12 → C.8, M.12, F.9; §5.13 → C.9, C.10, K.6; §5.14 → M.10, X.1; §5.15 → X.4;
- §7 → M.4-M.7, F.6, K.1 / K.3, T.2; §8 → M.11, F.8, K.5, T.4; §11 → X.5.
- Types:
EngineProbe,Fx,LogicGraph,EngineWriter/EngineReader,ContentRef,Errorand the CLI JSON result are defined once (Tasks 1-3, C.0) and only referenced later. - Review Focus: each of the 5 lines names its test in its owning task.
- Proportion: full steps only for the three map de-risk spikes; each stage is expanded by its lane before it starts.