# /setup-engine — Section 5.5: Populate project.yaml > Part of `/setup-engine`. `SKILL.md` names when to read this file; the settings it > resolved and the rules under its later headings still apply here. ## 5.5 Populate `project.yaml` (Primary Config Store) **In this file:** - 5.5.1 Build the four blocks - 5.5.2 Write the blocks to `project.yaml` - 5.5.3 Verify the dual-write > **v1.1:** `project.yaml` at the repo root is the **primary** store for project > configuration. The `.claude/docs/technical-preferences.md` file written in > Section 5 is kept as a **legacy mirror** for backward compatibility. This step > is a **dual-write** — the same engine facts go into both files, and > `project.yaml` wins when a skill reads a key present in both. After `technical-preferences.md` is written and approved, dual-write the structured config into `project.yaml`. Four blocks are written: `engine`, `specialists`, `naming`, and `commands`. ### 5.5.1 Build the four blocks Derive every value from the engine and language already chosen in Sections 3–4. Do NOT re-ask for the **engine, version or language** — reuse those decisions. > **That is not a blanket "never ask".** `rendering` and `physics` below depend on > two further inputs — target platform and 2D-vs-3D — which **Section 2 collects on > only one of the four entry paths**. When they were never collected, ask; the > unavailable-branch under the project-shape table says how. **`engine` block:** ```yaml engine: name: "" version: "" language: "" rendering: "" physics: "" ``` **On Godot these two values depend on the answers Section 2 already collected — the target platform and 2D-vs-3D — so do not pick them from the engine name alone.** On Unity and Unreal the table has a single `any` row and the engine name *is* sufficient; take those rows directly. | Engine | Project shape | `rendering` default | `physics` default | |--------|---------------|---------------------|-------------------| | Godot 4.x | 2D · web target | Compatibility | Godot Physics 2D | | Godot 4.x | 2D · non-web | Forward+ | Godot Physics 2D | | Godot 4.x | 3D · web target | Compatibility | Jolt | | Godot 4.x | 3D · non-web | Forward+ | Jolt | | Unity | any | URP | PhysX | | Unreal | any | Lumen + Nanite (deferred) | Chaos | > **Why this table has a shape column and the others do not.** Both Godot > defaults were wrong for a 2D browser game, and both were wrong from data this > skill had already collected and then discarded. > - **Jolt is Godot's default *3D* engine.** `docs/engine-reference/godot/modules/physics.md`: > "Jolt Physics is the DEFAULT 3D engine" and "2D physics UNCHANGED (still > Godot Physics 2D)". Writing `Jolt` on a 2D project sends the implementer to > tune a backend their game does not use. > - **Forward+ is desktop-focused.** `docs/engine-reference/godot/modules/rendering.md`: > Forward+ is "Full featured, desktop-focused"; Compatibility is > "OpenGL 3.3 / WebGL 2, broadest hardware support" — the browser path. > > If Section 2's platform answer was `Multiple platforms` **including** web, use > the web row: the renderer must work everywhere the game ships. > **Godot only — Unity and Unreal have one row each, keyed `any`.** Shape cannot > change `rendering` or `physics` for those two, so do **not** run the resolution > below for them: asking an operator a question whose answer you will discard is > exactly what §5.5.1 opens by forbidding. On Unity write `URP`/`PhysX` and on > Unreal write `Lumen + Nanite (deferred)`/`Chaos` straight from the table. > > **Section 2 did not necessarily run — check before reading its answers.** > Target platform and 2D-vs-3D are collected under *Questions 2–6, only if no > prior engine experience*. **Three of the four entry paths never reach them:** > `/setup-engine ` and `/setup-engine ` skip Section 2 > outright, and guided mode skips the matrix as soon as the user names an engine > they have worked in before. Only guided mode answered `None` or `Multiple` > collects both. > > Resolve each input in this order and stop at the first that answers: > 1. **Section 2's answers**, if that path ran. > 2. **The design artifact** — `design/gdd/game-concept.md` at `standard`/`full`, > `design/game-brief.md` at `minimal`. Section 2 already extracts platform > targets from these; extract 2D-vs-3D the same way when it is stated. > 3. **Ask.** Do not infer the shape from the engine name, and do not write a row > you could not justify: > - Prompt: "`rendering` and `physics` depend on the project's shape, which > hasn't been established yet. Is this 2D or 3D, and does it ship to web?" > - Options: `2D · non-web` / `2D · web` / `3D · non-web` / `3D · web`, plus > `Leave as [TO BE CONFIGURED]` — a legitimate answer here, revisited via > `/settings` once the concept exists. > > `[TO BE CONFIGURED]` is a valid value for both keys. A wrong `Jolt` on a 2D > project is worse than an unset one, because nothing downstream questions it. - `rendering` and `physics` are engine-typical defaults — tell the user they can be changed later via `/settings` if the project uses a different setup. - For Godot **Both**, write `language: "GDScript"` (the primary gameplay language); the C# usage is recorded in `technical-preferences.md`. - For Unreal, write `language: "C++"` — or `"Blueprint"` only if the project is Blueprint-primary. Blueprint as a secondary tool does not change this value. **`specialists` block** — copy directly from the routing table chosen in Section 5 (**A3** in `references/godot-language-config.md` for Godot): | Engine / language | `code` | `shader` | `ui` | `additional` | |-------------------|--------|----------|------|--------------| | Godot — GDScript | godot-gdscript-specialist | godot-shader-specialist | godot-specialist | [godot-gdextension-specialist] | | Godot — C# | godot-csharp-specialist | godot-shader-specialist | godot-specialist | [godot-gdextension-specialist] | | Godot — Both | godot-gdscript-specialist | godot-shader-specialist | godot-specialist | [godot-csharp-specialist, godot-gdextension-specialist] | | Unity | unity-specialist | unity-shader-specialist | unity-ui-specialist | [unity-dots-specialist, unity-addressables-specialist] | | Unreal | unreal-specialist | unreal-specialist | ue-umg-specialist | [ue-gas-specialist, ue-blueprint-specialist, ue-replication-specialist] | ```yaml specialists: code: "" shader: "" ui: "" additional: ["<...>"] ``` **`naming` block** — enum values (NOT the prose form used in `technical-preferences.md`). Use the column matching the engine and, for Godot, the language: | Field | Godot GDScript | Godot C# | Unity | Unreal | |-------|----------------|----------|-------|--------| | `classes` | PascalCase | PascalCase | PascalCase | PascalCase | | `variables` | snake_case | camelCase | camelCase | **PascalCase** | | `constants` | SCREAMING_SNAKE | PascalCase | SCREAMING_SNAKE | SCREAMING_SNAKE | | `signals` | past_tense | on_event_name | **PascalCase `On`** | **PascalCase `On`** | | `files` | snake_case | PascalCase | PascalCase | PascalCase | | `scenes` | snake_case | PascalCase | PascalCase | PascalCase | > **The Unreal column is sourced, not inherited from the table's generic shape.** > `variables: camelCase` and `signals: on_event_name` are the two rows that shape > would produce, and both are wrong for Unreal. Checked against this repo's > pinned reference: `docs/engine-reference/unreal/` contains **zero** camelCase > variable declarations and uses PascalCase throughout (`int32 Health;`, > `FVector ServerPosition;`), and the word "signal" **never appears in it at all** — > it is Godot vocabulary. Unreal has delegates and events, and the reference's > handlers are PascalCase `On`. > > Two caveats kept rather than smoothed over. (1) `classes: PascalCase` is > **incomplete** for Unreal, not wrong: UE requires a mandatory type prefix > (`U`/`A`/`F`/`E`/`I`/`T`) that this enum cannot express, so an Unreal project's > coding standards must state it separately. (2) `scenes` is a **category > mismatch** — Unreal has Levels and Maps under World Partition, not scenes — left > in place because the key is shared across engines and renaming it is a schema > change, not a value fix. > > The blast radius is limited: `naming.*` is currently a > **documented-but-unread** setting, so wrong values misinform a human reading > `project.yaml` rather than driving any automated check — today. That is a > reason to get them right now, while it is cheap. > **Three further rules for this table.** > > **The Unity `build` row must not hardcode `-buildTarget StandaloneLinux64`.** > A hardcoded target gives every Unity project a **Linux** build command, whatever > platform it actually ships on. The row reads ``: **ask the operator for > the build target and write the value they give.** The correct `-buildTarget` identifiers > are not sourceable from `docs/engine-reference/unity/`, so this skill must not > emit one from recall — the same call made for `/security-audit`'s save > patterns and for Unity's release dates. A wrong-but-plausible default is worse > than an explicit question: it produces a `commands.build` that runs, succeeds, > and builds the wrong artifact. > > **`-buildTarget` alone builds nothing.** It switches the active platform and > exits 0 (verified on 6000.3.23f1). The row also needs a player-build flag, > `-buildPlayer ` — for Windows 64-bit, > `-buildWindows64Player Builds/Win64/.exe` (verified: exit 0 and an > `.exe`; exit 1 on a compile error or when no scene is in the build settings). > Ask which platform, and confirm any other platform's flag against Unity's > command-line documentation rather than recalling one. > > **The Godot `build` row must not hardcode `'Linux/X11'` either — same defect, > same remedy.** A Godot export preset name is **project-defined**: it is whatever > string the operator typed into `export_presets.cfg`, and the shipped defaults are > per-platform (`Windows Desktop`, `macOS`, `Web`, `Linux/X11`). Writing > `'Linux/X11'` gives a Windows-targeting project a build command that fails with > `Unknown export preset`, and gives a project that *does* have a Linux preset a > command that silently builds the wrong platform. The row reads ``: > **ask the operator which export preset to build and write the value they give**, > exactly as the Unity row does. If no presets exist yet — common, since > `/setup-engine` usually runs before the Godot project has any — write > `[TO BE CONFIGURED]` and say so, rather than guessing a name that does not exist. > > **The `constants` row must not contradict this skill's own prose.** The table says > `SCREAMING_SNAKE` for Unity; the Unity section of this same file says > "Constants: PascalCase or UPPER_SNAKE_CASE". Both cannot be authoritative, and > an agent following one produces config the other rejects. The row is **left > unchanged pending sourcing** rather than picked by preference — neither > `docs/engine-reference/unity/` nor `unreal/` contains a SCREAMING_SNAKE constant > to confirm or refute it, and the Unreal run flagged the same row as wrong for > Unreal on the same grounds. Resolve by adding a naming section to the engine > references, then correct table and prose together. Until then, treat > `naming.constants` as unverified for Unity and Unreal. > > **Write that caveat into `project.yaml`, do not leave it here.** On Unity and > Unreal, emit the `constants` line with a trailing comment: > ```yaml > naming: > constants: SCREAMING_SNAKE # UNVERIFIED — not sourceable from > # docs/engine-reference//; confirm before relying on it > ``` > Without it, a value the skill *knows* is unsourced is recorded as flatly as > `classes: PascalCase`, which is sourced, and the reader of the primary config > store cannot tell them apart. The `commands` block nine lines below already does > exactly this with its `# TODO: confirm these` line — this is the same pattern, > applied to the value that needs it just as much. For Godot **Both**, use the GDScript column — `.gd` files dominate gameplay scripting; `.cs` files follow C# conventions per-file (see **A2** in `references/godot-language-config.md`). ```yaml naming: classes: variables: constants: signals: files: scenes: ``` **`commands` block** — engine-default shell commands, simple-string form: | Engine | `build` | `test` | `run` | `smoke` | |--------|---------|--------|-------|---------| | Godot | `godot --headless --export-debug ''` **(ASK — do not default)** | `godot --headless -s -d --remote-debug tcp://127.0.0.1:0 res://addons/gdUnit4/bin/GdUnitCmdTool.gd -a res://tests --ignoreHeadlessMode` | `godot --path . --windowed --resolution 1280x720` | `godot --headless --quit-after 5` | | Unity | `"" -batchmode -quit -projectPath . -buildTarget -buildPlayer Builds//.exe` **(ASK — do not default)** | `"" -batchmode -runTests -projectPath . -testPlatform EditMode -testResults test-results/editmode.xml` | `Builds//.exe -screen-width 1280 -screen-height 720 -screen-fullscreen 0` | `"" -batchmode -quit -projectPath . -logFile -` | > **What each exit code proves — every row verified on the pinned engines.** > Godot `test` exits 0 when all pass, 100 on a failure, 101 when all pass but > nodes leaked (a warning, not a failure), 105 when a test script does not > parse, and 103 / 104 when gdUnit4 cannot run at all (headless refused, Godot > older than 4.3). Keep `--remote-debug tcp://127.0.0.1:0`: without it a script > error opens Godot's interactive debugger and the run waits at a `debug>` > prompt forever instead of exiting. Run `godot --headless --path . --import` > before it on a fresh clone: with no `.godot/` class cache, > `GdUnitCmdTool.gd` did not load and the run exits 1 having run nothing. Unity `test` > exits 0 / 2, and a results file with `testcasecount="0"` means no test was > compiled — not a pass. It runs Edit Mode only: Play Mode tests under > `Assets/Tests/PlayMode/` need a second run into their own results file > (`-testPlatform PlayMode -testResults test-results/playmode.xml`), which > `/smoke-check` makes whenever that folder holds tests and CI runs as its own > step. Unity `smoke` exits 1 on a compile error, 0 when clean. > **Godot `smoke` exits 0 even on a parse error** — it is a boot check: read its > output for `SCRIPT ERROR`. It also needs `run/main_scene` set: without one it > prints `Can't run project: no main scene defined` and never exits, so run it > under a timeout. Unity `smoke` compiles only the editor-side assemblies; an > error that exists only in a player build (an `Editor/` script without its own > assembly) shows up in `build`, not here. The real Godot parse check is > `godot --headless --path . --import`, then > `godot --headless --path . -s res://.claude/scripts/godot-parse-check.gd -- res://.gd …` > (exit 1 when a script does not load). `--check-only` is not one: it fails > valid code that names an autoload. `` is the **editor** > executable's full path, quoted. Never write bare `Unity`: on `PATH` that name > may be Unity's separate CLI, which rejects `-batchmode` with exit 2 — the same > code as a failed test. > **`commands.run` is the one the run-and-observe step depends on.** It must > launch the **game**, windowed, at a fixed resolution — never the editor, and > never with a headless / batch / null-RHI flag, which exist to skip rendering. > `/dev-story` Phase 6 step 4 appends the per-engine capture flags to it > (`.claude/docs/run-and-observe.md`). `smoke` is Unity's parse check, `test` > feeds `/smoke-check`; `build` is the one row you must ask for. > **YAML quoting rule — applies to EVERY command value.** `yaml-helper.sh` — the > parser every config read in this framework goes through — decodes **no** > escape sequences: neither `\"` inside a double-quoted scalar nor `''` inside a > single-quoted scalar. It takes the first matching quote character as the end of > the value and discards the rest, so `"he said \"hi\" now"` parses as > `he said \`. Verified against the parser, not inferred. The rule is > load-bearing: `/smoke-check` and `/dev-story` run these values, `/settings` > reads and writes them through this parser, and the same holds for every other > quoted value in `project.yaml`. > Pick the quote style that needs **no escaping**: > - value contains a single quote `'` (e.g. the Godot `build` above) → wrap it in > a **double-quoted** YAML scalar: `"... 'Linux/X11' ..."` > - value contains a double quote `"` (e.g. the Unreal `test`/`smoke` below) → > wrap it in a **single-quoted** YAML scalar: `'... "Quit" ...'` > - value contains neither → use a double-quoted scalar for consistency. > Never use a quote style that forces an escape (`\"` or `''`) — the parser > truncates the value at the escape. A value containing BOTH quote types cannot > be represented for this parser; rewrite the command to drop one. **Godot / Unity** — write the table values directly, substituting the operator's answer for `` / `` / ``, and the editor's full path for ``. Godot's `build` contains a single quote, so it takes a double-quoted scalar (`Windows Desktop` below is the *example* answer, not a default — write what the operator said): ```yaml commands: build: "godot --headless --export-debug 'Windows Desktop'" test: "godot --headless -s -d --remote-debug tcp://127.0.0.1:0 res://addons/gdUnit4/bin/GdUnitCmdTool.gd -a res://tests --ignoreHeadlessMode" run: "godot --path . --windowed --resolution 1280x720" smoke: "godot --headless --quit-after 5" ``` **Write bare `godot` only when Section 3's `godot --version` ran** — that is, `godot` is on `PATH`. Otherwise (the usual case on Windows and macOS) substitute the executable the probe found or the user gave — a Windows `Godot_v-stable_win64.exe`, macOS `/Applications/Godot.app/Contents/MacOS/Godot` — as its full path, in double quotes, for the leading `godot` in all four values, exactly as `` is. The value then holds double quotes, so it takes a single-quoted scalar, and `build` quotes its preset with double quotes too, so no value holds both quote types. Godot not on PATH, installed under `C:/Program Files/Godot/`: ```yaml commands: build: '"C:/Program Files/Godot/Godot_v4.6.1-stable_win64.exe" --headless --export-debug "Windows Desktop"' test: '"C:/Program Files/Godot/Godot_v4.6.1-stable_win64.exe" --headless -s -d --remote-debug tcp://127.0.0.1:0 res://addons/gdUnit4/bin/GdUnitCmdTool.gd -a res://tests --ignoreHeadlessMode' run: '"C:/Program Files/Godot/Godot_v4.6.1-stable_win64.exe" --path . --windowed --resolution 1280x720' smoke: '"C:/Program Files/Godot/Godot_v4.6.1-stable_win64.exe" --headless --quit-after 5' ``` If the probe found no executable and the user does not know where it is, keep bare `godot`, put `# TODO: godot is not on PATH here — set the editor's full path via /settings` above the block, and say so: those commands will not run until one of the two is fixed. For Unity, probe the Hub install (`C:/Program Files/Unity/Hub/Editor//Editor/Unity.exe` on Windows, `/Applications/Unity/Hub/Editor//Unity.app/Contents/MacOS/Unity` on macOS, `/home//Unity/Hub/Editor//Editor/Unity` on Linux) or ask. Write the expanded absolute path — a `~` inside quotes is not expanded, and the command then exits 127. The quoted path puts double quotes in the command, so the value takes a single-quoted scalar: ```yaml commands: test: '"C:/Program Files/Unity/Hub/Editor/6000.3.23f1/Editor/Unity.exe" -batchmode -runTests -projectPath . -testPlatform EditMode -testResults test-results/editmode.xml' smoke: '"C:/Program Files/Unity/Hub/Editor/6000.3.23f1/Editor/Unity.exe" -batchmode -quit -projectPath . -logFile -' ``` > **Also write `engine.path` if the editor is not on `PATH`.** Agents and > `/smoke-check` read it to find the editor, but a command is run as written — > `engine.path` is never spliced into `commands.*` at run time, which is why the > full path goes into the commands above — and on Windows and macOS none of > the three engines installs onto `PATH` by default (on macOS the editor is inside an app > bundle, e.g. `/Applications/Godot.app/Contents/MacOS/Godot`). Ask for or probe the > install location and record it — the editor executable for Godot and Unity, > the engine folder (``) for Unreal: > > ```yaml > engine: > path: "C:/Program Files/Epic Games/UE_5.7" # omit if the editor is on PATH > ``` > > **This is not cosmetic.** Without it, an agent reports *"no editor on this > machine"* and ships C++ it never compiled — on a machine where that engine WAS > installed. Nothing in the project told it where to look, so absence of a path read > as absence of an engine. For **Unreal**, UE build/test commands vary by version and project setup. Write best-effort values and add a `# TODO` comment so the user knows to confirm them. Substitute the engine's install folder for `` (the value you record as `engine.path`) — none of the editor binaries is on `PATH`. The commands embed double quotes, so they take single-quoted scalars. Write the block for the machine the project is developed on (`uname -s`: `Linux`, `Darwin` = macOS, anything else = Windows). The Windows block was run on UE 5.7; the Linux and macOS lines come from Epic's documentation, recorded with their sources in `docs/engine-reference/unreal/current-best-practices.md` ("Command Line"). Windows: ```yaml commands: # TODO: confirm these for your UE version and project — adjust via /settings build: '"/Engine/Binaries/DotNET/AutomationTool/AutomationTool.exe" BuildCookRun -project="$(pwd -W 2>/dev/null || pwd)/.uproject" -platform=Win64 -build -cook' test: '"/Engine/Binaries/Win64/UnrealEditor-Cmd.exe" "$(pwd -W 2>/dev/null || pwd)/.uproject" -ExecCmds="Automation RunTests .; Quit" -unattended -nullrhi -stdout -FullStdOutLogOutput' run: '"/Engine/Binaries/Win64/UnrealEditor.exe" "$(pwd -W 2>/dev/null || pwd)/.uproject" -game -windowed -ResX=1280 -ResY=720' smoke: '"/Engine/Binaries/Win64/UnrealEditor-Cmd.exe" "$(pwd -W 2>/dev/null || pwd)/.uproject" -game -nullrhi -unattended -stdout -ExecCmds="Quit"' ``` Linux — the editor binary is `Engine/Binaries/Linux/UnrealEditor`, the build scripts are shell scripts: ```yaml commands: # TODO: confirm these for your UE version and project — adjust via /settings build: '"/Engine/Build/BatchFiles/RunUAT.sh" BuildCookRun -project="$(pwd -W 2>/dev/null || pwd)/.uproject" -platform=Linux -build -cook' test: '"/Engine/Binaries/Linux/UnrealEditor" "$(pwd -W 2>/dev/null || pwd)/.uproject" -ExecCmds="Automation RunTests .; Quit" -unattended -nullrhi -stdout -FullStdOutLogOutput' run: '"/Engine/Binaries/Linux/UnrealEditor" "$(pwd -W 2>/dev/null || pwd)/.uproject" -game -windowed -ResX=1280 -ResY=720' smoke: '"/Engine/Binaries/Linux/UnrealEditor" "$(pwd -W 2>/dev/null || pwd)/.uproject" -game -nullrhi -unattended -stdout -ExecCmds="Quit"' ``` macOS — only the build is sourced. Epic documents the `UnrealEditor.app` bundle but no command-line editor inside it, so write `build` and leave the other three as TODO lines rather than a guessed path; `/smoke-check` reports NOT ASSESSED for tests until the user sets `commands.test`: ```yaml commands: # TODO: confirm this for your UE version and project — adjust via /settings build: '"/Engine/Build/BatchFiles/RunUAT.sh" BuildCookRun -project="$(pwd -W 2>/dev/null || pwd)/.uproject" -platform=Mac -build -cook' # TODO: test, run and smoke — no documented command-line editor on macOS. # Set them via /settings to the editor command you run for this project. ``` On Windows, `build` calls AutomationTool directly: from Git Bash, `RunUAT.bat` fails (`'C:\Program' is not recognized`) whenever the project path has a space. AutomationTool.exe runs on the machine's .NET 8 runtime; `RunUAT.bat` brings its own SDK, so with no space in the path and no .NET 8 installed, use it. `-build` builds the editor target too (verified on 5.7). **Three details are load-bearing.** The project path is absolute: UE 5.7 does not find a relative `.uproject` and exits 1 (`Project file not found`) before anything runs. `$(pwd -W 2>/dev/null || pwd)` gives the `C:/…` form in Git Bash and the plain path elsewhere, run from the project root; `$PWD` alone fails when `MSYS_NO_PATHCONV` is set. `smoke` needs `-game`: without it `UnrealEditor-Cmd` boots the *editor*, where a bare `Quit` console command only ends a play-in-editor session — the process stays resident and the command never returns; with `-game` the same line boots headless and exits in seconds. `test` needs the `; Quit` inside the `Automation` string: the automation runner handles that trailing `Quit` itself and exits the editor once the tests finish, and without it the run also never returns. Its filter runs every test whose full name *contains* `.` — a substring match, not a prefix — so name tests `.[System].[Scenario]` as `qa-tester` does. If the project's name is also an engine area — `Audio`, `Core`, `Input`, `Test`, `System`, `Engine`, `Editor`, `AI`, `Math` — `.` also runs hundreds to thousands of the engine's own tests; give the tests a distinct root (say `Game.`) and put the same root in the filter. This form exits 255 on a failing test AND on a filter that matches nothing (`No automation tests matched`, verified on 5.7), so `/smoke-check` reads the output, not the exit code. `smoke` carries `-stdout` for the same reason: without it a failed boot prints nothing. Do not swap in `-TestExit="Automation Test Queue Empty"`: that form exits **0** on a failing test unless its report JSON is parsed. ### 5.5.2 Write the blocks to `project.yaml` Show the user the full set of four blocks and ask: > "May I write the `engine`, `specialists`, `naming`, and `commands` blocks to `project.yaml`?" Wait for confirmation, then apply based on the file's current state: - **`project.yaml` does not exist** — create it with the Write tool using this v1.1 template (the four blocks appended after `framework`; replace `<...>` and the date): ```yaml # CCGS project configuration — single source of truth for project settings. # Schema: grep the `## ` section of .claude/docs/effects-map.md — # it is ~31k tokens whole, ~900 per section. Do not open it entire. schema_version: 1 framework: version: 1.1.3 last_upgraded: engine: name: "<...>" version: "<...>" language: "<...>" rendering: "<...>" physics: "<...>" specialists: code: "<...>" shader: "<...>" ui: "<...>" additional: ["<...>"] naming: classes: <...> variables: <...> constants: <...> signals: <...> files: <...> scenes: <...> commands: build: "<...>" test: "<...>" run: "<...>" smoke: "<...>" ``` Do not seed `modes.review_mode` here. It is a rigor-fronted knob — `modes.rigor` supplies its value, so an explicit value would shadow the rigor expansion and pin the review mode regardless of the project's rigor. - **`project.yaml` exists** — Read it first (the Edit tool requires the file to have been read this session), then, processing the blocks in the order `engine` → `specialists` → `naming` → `commands`, for each block: - **Block absent** — insert the whole block, appended after the last existing top-level block. - **Block already present** (a re-run of `/setup-engine`) — Edit the existing keys in place, and add any keys the block is missing (a partial block must end up with its full key set). Do NOT add a second copy of the block. **Unreal on macOS is the exception in both cases:** `commands.test`, `run` and `smoke` are written as the TODO comment lines from the macOS block — never as `<...>` placeholders or a guessed path — and a re-run leaves them unset until the user sets them. Preserve all other content in `project.yaml` (`schema_version`, `framework`, `modes`, `project`, etc.) — only the four blocks are touched. ### 5.5.3 Verify the dual-write Read `project.yaml` back and confirm the four blocks are present and that `engine.name`, `engine.language`, and the naming values match what was written to `technical-preferences.md`. If they diverge, report the discrepancy to the user and stop — a split config corrupts skill and hook reads. ---