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
This commit is contained in:
Donchitos
2026-10-08 17:52:48 +11:00
co-authored by Claude Opus 5.5
parent b21fa0f7f2
commit be8993bbc5
231 changed files with 12000 additions and 10527 deletions
@@ -0,0 +1,520 @@
# /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.
---