Files
Claude-Code-Game-Studios/.claude/skills/setup-engine/references/5.5-project-yaml.md
T
DonchitosandClaude Opus 5.5 be8993bbc5 Release 1.1.3 — long skills keep their later steps
Fix release for 1.1.2: the 14 longest skills keep their later phases
after a compaction, a question nobody answered is never taken as an
answer, settings read the same from a subfolder, the commit check works
for a project in a subfolder of a larger repository, every skill opens
with a title, and the shipped allow rules no longer approve deleting a
branch or writing files without asking. Details in CHANGELOG.md; upgrade
steps in UPGRADING.md (v1.1.2 → v1.1.3).

Refs #83
Refs #133

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0185en4t76fPn7315q4c3nko
2026-10-08 17:52:48 +11:00

521 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# /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: "<Godot|Unity|Unreal>"
version: "<version from Section 3>"
language: "<GDScript|C#|C++|Blueprint>"
rendering: "<engine default — see table>"
physics: "<engine default — see table>"
```
**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 <engine> <version>` and `/setup-engine <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: "<code specialist>"
shader: "<shader specialist>"
ui: "<ui specialist>"
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<Event>`** | **PascalCase `On<Event>`** |
| `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<Event>`.
>
> 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 `<TARGET>`: **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,
> `-build<PLATFORM>Player <output path>` — for Windows 64-bit,
> `-buildWindows64Player Builds/Win64/<Game>.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 `<PRESET>`:
> **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/<engine>/; 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: <value>
variables: <value>
constants: <value>
signals: <value>
files: <value>
scenes: <value>
```
**`commands` block** — engine-default shell commands, simple-string form:
| Engine | `build` | `test` | `run` | `smoke` |
|--------|---------|--------|-------|---------|
| Godot | `godot --headless --export-debug '<PRESET>'` **(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 | `"<Unity editor>" -batchmode -quit -projectPath . -buildTarget <TARGET> -build<PLATFORM>Player Builds/<Target>/<Game>.exe` **(ASK — do not default)** | `"<Unity editor>" -batchmode -runTests -projectPath . -testPlatform EditMode -testResults test-results/editmode.xml` | `Builds/<Target>/<Game>.exe -screen-width 1280 -screen-height 720 -screen-fullscreen 0` | `"<Unity editor>" -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://<file>.gd …`
> (exit 1 when a script does not load). `--check-only` is not one: it fails
> valid code that names an autoload. `<Unity editor>` 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 `<PRESET>` / `<TARGET>` / `<PLATFORM>`, and the editor's full path for
`<Unity editor>`. 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<version>-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 `<Unity editor>` 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/<version>/Editor/Unity.exe`
on Windows, `/Applications/Unity/Hub/Editor/<version>/Unity.app/Contents/MacOS/Unity`
on macOS, `/home/<user>/Unity/Hub/Editor/<version>/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 (`<UE root>`) 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 `<UE root>` (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: '"<UE root>/Engine/Binaries/DotNET/AutomationTool/AutomationTool.exe" BuildCookRun -project="$(pwd -W 2>/dev/null || pwd)/<project>.uproject" -platform=Win64 -build -cook'
test: '"<UE root>/Engine/Binaries/Win64/UnrealEditor-Cmd.exe" "$(pwd -W 2>/dev/null || pwd)/<project>.uproject" -ExecCmds="Automation RunTests <project>.; Quit" -unattended -nullrhi -stdout -FullStdOutLogOutput'
run: '"<UE root>/Engine/Binaries/Win64/UnrealEditor.exe" "$(pwd -W 2>/dev/null || pwd)/<project>.uproject" -game -windowed -ResX=1280 -ResY=720'
smoke: '"<UE root>/Engine/Binaries/Win64/UnrealEditor-Cmd.exe" "$(pwd -W 2>/dev/null || pwd)/<project>.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: '"<UE root>/Engine/Build/BatchFiles/RunUAT.sh" BuildCookRun -project="$(pwd -W 2>/dev/null || pwd)/<project>.uproject" -platform=Linux -build -cook'
test: '"<UE root>/Engine/Binaries/Linux/UnrealEditor" "$(pwd -W 2>/dev/null || pwd)/<project>.uproject" -ExecCmds="Automation RunTests <project>.; Quit" -unattended -nullrhi -stdout -FullStdOutLogOutput'
run: '"<UE root>/Engine/Binaries/Linux/UnrealEditor" "$(pwd -W 2>/dev/null || pwd)/<project>.uproject" -game -windowed -ResX=1280 -ResY=720'
smoke: '"<UE root>/Engine/Binaries/Linux/UnrealEditor" "$(pwd -W 2>/dev/null || pwd)/<project>.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: '"<UE root>/Engine/Build/BatchFiles/RunUAT.sh" BuildCookRun -project="$(pwd -W 2>/dev/null || pwd)/<project>.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 `<project>.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* `<project>.` — a substring match, not a prefix — so
name tests `<project>.[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` — `<project>.` also runs hundreds
to thousands of the engine's own tests; give the tests a distinct root (say
`<project>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 `## <key>` 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: <today's date>
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.
---