add agent reference and restructure plan

CLAUDE.md documents repository layout, the Unity command line, the dialogue
content pipeline, validation, conventions, and known traps. AGENTS.md points at
it for non-Claude agents.

docs/restructure-plan.md covers both halves of the restructure: moving character
and voice prose out of JSON string fields into a writer-facing writing/ tree, and
migrating off the single-scene DialogueTest scaffold into Bootstrap/Systems/Level
scenes. Phase 0 is recorded as executed; everything from Phase 1 on is proposal.

Two findings in it were corrected after first being got wrong: the Unity CLI is
installed (just not on PATH for non-interactive shells) and does have build and
test commands, and the binary scene is not a stale save but a standing Unity
behaviour where Force Text converts only when the setting changes.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-25 19:12:49 +02:00
co-authored by Claude Opus 5
parent 4f97fc704c
commit 5da782d21b
3 changed files with 915 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
# AGENTS.md
This repository's agent instructions live in [`CLAUDE.md`](./CLAUDE.md). It is
tool-agnostic — repository layout, the Unity command line, the dialogue content
pipeline, validation, conventions, and known traps.
Read `CLAUDE.md` before making changes, regardless of which agent you are.
Design and migration plans are in [`docs/`](./docs/), most relevantly
[`docs/restructure-plan.md`](./docs/restructure-plan.md).
+361
View File
@@ -0,0 +1,361 @@
# Nightclub Arcadia — agent reference
Reference for coding agents (Claude Code and others) working in this repository.
Human-facing design docs live in `docs/`. This file is the operational one: where
things are, what is generated, and how to build/test/validate from the terminal.
Companion: `AGENTS.md` (pointer file, same content applies).
---
## 1. Repository layout
Repo root is `nightlcub-arcadia` (the typo is in the directory name, not a mistake
to fix — it is the checkout path). The Unity project is a **subdirectory**:
```
/Users/lennart/Dev/nightlcub-arcadia/ # git root
├── CLAUDE.md, AGENTS.md # this reference
├── docs/ # design + plan docs (markdown)
├── tools/YarnCheck/ # .NET 9 Yarn compiler/player, no Unity needed
├── .gitignore # macOS / IDE / toolchain rules
└── NightclubArcadia/ # ← the Unity project (-projectPath)
├── .gitignore # Unity rules (anchored, must stay here)
├── Assets/
├── Packages/ # incl. embedded dev.yarnspinner.unity
└── ProjectSettings/
```
**Two `.gitignore` files, deliberately.** The Unity one anchors every rule with a
leading `/`, so it only works at the root of the Unity project folder. Do not merge them.
Unity **6000.5.8f1**, URP 17.5.0, Cinemachine 3.1.7, Input System 1.20.0,
AI Navigation 2.0.14, ProBuilder 6.1.2, Test Framework 1.7.0. Yarn Spinner is an
**embedded package** at `NightclubArcadia/Packages/dev.yarnspinner.unity` (plus the
`textanimator` addon) — it is committed, not resolved from a registry.
---
## 2. Ground rules
1. **Never hand-edit generated `.asset` files.** `Assets/Skills/Definitions/*.asset`,
`Assets/Skills/SkillDatabase.asset`, `Assets/Candidates/Definitions/*.asset` and
`Assets/Candidates/CandidateDatabase.asset` are *build output*. Their sources are
`Assets/Skills/skill_bible.json` and `Assets/Candidates/candidates.json`. Edits are
overwritten by the Editor setup scripts. Edit the JSON.
2. **Never hand-edit scene or prefab YAML unless you have read the surrounding block.**
GUID/fileID surgery in `.unity` / `.prefab` files silently detaches components.
3. **One Unity instance per project path.** Unity refuses to open a project in batch
mode while the Editor has it open — a single instance at a time. Always check before
any `-batchmode` invocation (§3.1).
4. **Prefer YarnCheck over Unity for dialogue validation.** It is seconds, not minutes,
and needs no Editor lock (§5.3).
5. **Do not commit on the user's behalf** unless asked. Unity writes to tracked files
(`ProjectSettings/*`, `.meta`, scenes) as a side effect of simply being open, so
`git status` noise is expected and is not always yours.
---
## 3. Unity from the command line
### 3.0 Two different tools, both present
| | What it is | Status |
|---|---|---|
| `/Users/lennart/.unity/bin/unity` | The **Unity CLI** (v1.0.0-beta.6). Wraps the Editor for CI-shaped work: `build`, `test`, `run`, `open`, plus editor/license/cloud management. | Installed. **Prefer this.** |
| `/Applications/Unity/Hub/Editor/6000.5.8f1/Unity.app/Contents/MacOS/Unity` | The **Editor executable**. Raw `-batchmode` flags. | Installed. Fallback, for flags the CLI does not expose. |
The CLI is **not on `PATH` for non-interactive shells** — `which unity` fails even though the
binary exists, and `unity doctor` reports `check.binary-on-path warn`. Always invoke it by
absolute path, or export it:
```bash
export UNITY_CLI="/Users/lennart/.unity/bin/unity"
export UNITY="/Applications/Unity/Hub/Editor/6000.5.8f1/Unity.app/Contents/MacOS/Unity"
export UNITY_PROJECT="/Users/lennart/Dev/nightlcub-arcadia/NightclubArcadia"
```
`unity doctor --no-banner` confirms the environment: signed in via OAuth keyring, editor
`6000.5.8f1` arm64 resolved from `ProjectVersion.txt`. Add `--json` to any command for
machine-readable output and `--no-banner` to keep logs clean.
### 3.1 Always check the lock first
Only one Unity instance per project path. Check before any headless run:
```bash
pgrep -fl "Unity.app/Contents/MacOS/Unity" | grep -v AssetImportWorker
```
`AssetImportWorker*` processes are children of the main Editor — filter them out or you will
misread the check. `Temp/UnityLockfile` and `unity status` are the other two signals.
### 3.2 Run tests
```bash
"$UNITY_CLI" test "$UNITY_PROJECT" --no-banner --mode EditMode \
--output /tmp/editmode-results.xml \
--report-format both --junit-output /tmp/editmode-junit.xml
```
Useful flags: `--filter <pattern>`, `--retries <0-10>` (reports tests that pass on retry as
flaky), `--rerun-failed`, `--shard N/M` for parallel CI, `--coverage`, `--timeout <seconds>`.
`--mode PlayMode` runs the PlayMode suite. **There are none yet** — `Assets/Tests/` is EditMode
only, so that command reports zero tests. A PlayMode suite needs a new
`Assets/Tests/PlayMode/*.asmdef` referencing `UnityEngine.TestRunner` with `includePlatforms`
left empty.
Raw-Editor equivalent, if you need a flag the CLI does not expose:
```bash
"$UNITY" -batchmode -nographics -projectPath "$UNITY_PROJECT" \
-runTests -testPlatform EditMode \
-testResults /tmp/editmode-results.xml -logFile /tmp/unity-editmode.log
```
**Do not add `-quit` to a `-runTests` run** — the runner terminates the Editor itself, and
`-quit` on top of async work can hang it.
**Parse the results XML; do not trust the exit code.** Unity's own docs state there is no common
exit-code definition across the components under test. Read `total`/`passed`/`failed` off the
root element and list any `test-case` whose `result` is not `Passed`.
### 3.3 Build a player
```bash
"$UNITY_CLI" build "$UNITY_PROJECT" --no-banner \
--target StandaloneOSX -o /tmp/NightclubArcadia.app
```
Without `--execute-method` the CLI uses Unity's built-in build — a Build Profile on Unity 6+, or
the legacy desktop player flags — and builds whatever scenes `EditorBuildSettings` enables.
`--profile <name>` selects a profile from `Assets/Settings/Build Profiles` (the profile defines
the target). `-l/--log-file` overrides the log path; the log streams to console unless `--no-tail`.
**No build script exists yet.** When one is added, put it at `Assets/Editor/BuildPipeline.cs` and
call `--execute-method NightclubArcadia.Editor.BuildPipeline.BuildMacOS`. The CLI forwards the
output path as `-buildOutput` and **your method is responsible for honouring it**. A build method
must call `EditorApplication.Exit(1)` on failure, or Unity exits 0 and CI reports a green build
that produced nothing.
### 3.4 Run an arbitrary Editor method
```bash
"$UNITY_CLI" run "$UNITY_PROJECT" --no-banner --timeout 900 -- \
-executeMethod SomeClass.SomeStaticMethod
```
**`unity run` owns `-batchmode`, `-nographics`, `-quit` and `-logFile`.** Passing any of them
after `--` fails with a reserved-flag error. Pass only your own arguments.
Output does **not** stream to stdout. The log lands at `$UNITY_PROJECT/Logs/Editor.log` (the
previous run rotates to `Editor-prev.log`), so tag your `Debug.Log` calls and grep for them:
```bash
grep -n "\[YourTag\]" "$UNITY_PROJECT/Logs/Editor.log" | tail -20
```
This is the mechanism for the JSON → ScriptableObject regeneration in
`Assets/Editor/SkillSystemSetup.cs` and `CandidateSystemSetup.cs`. Read the actual class and
method names out of those files before invoking — do not guess them.
## 4. The dialogue content pipeline
### 4.1 Where content lives
```
NightclubArcadia/Assets/Dialogue/
├── NightclubArcadia.yarnproject # globs **/*.yarn — new files need no registration
├── Common.yarn # every <<declare>>, project-wide. Never played.
├── Commentary.yarn # passive skill barks, one node per (context, skill)
├── Scenes/ SC101, SC102, Debug
├── Characters/ Bartender, Bradford Kane, Chevalier Cassian Thal
└── Objects/ Chair
```
**Dialogue is already plain text in the repo.** It is authored in `.yarn` files, not inside
the Unity Editor, and any new `.yarn` file dropped anywhere under `Assets/Dialogue/` is picked
up automatically by the `**/*.yarn` glob in the `.yarnproject`. Adding a scene or a character
requires **no C#, no Unity, and no `.yarnproject` edit**.
The one caveat: the `.yarnproject` also stores `editorOptions.yarnScriptEditor.projectGraphPositions`
— per-file x/y coordinates for the Yarn graph editor. These churn whenever anyone opens the
graph view. Treat that block as editor state; do not fight over it in review.
### 4.2 The variable contract
Every variable used anywhere must be `<<declare>>`d in `Common.yarn`. This is the whole point
of that file: the compiler then catches `$reputaton` instead of silently creating a second
variable at runtime. Adding a scene flag means adding a `<<declare>>` line — that is a writer
action, not a code action.
Naming families that are load-bearing (enforced or lint-checked):
| Family | Meaning | Checked by |
|---|---|---|
| `$self_lean_<candidate_id>` | identity-lean counter | `CandidateYarnLintTests` — id must exist in `candidates.json` |
| `$found_<clue_id>` | clue coverage | `CandidateYarnLintTests` — id must exist in `candidates.json` |
| `$visited_<location_id>` | first arrival | convention; `unlockFlag` in `candidates.json` may only name these |
| `$errand_<errand_id>` | errand state | same |
| `$seen_<scene_id>` | scene witnessed | convention |
| `$check_*` | written by `<<check>>` | **never `<<set>>` by hand** |
### 4.3 Writer-facing Yarn vocabulary
Commands and functions registered by game code, i.e. the full set a writer may use:
| Syntax | Defined in | Notes |
|---|---|---|
| `<<check <skill> <Band>>>` | `Skills/Yarn/YarnSkillCommands.cs` | writes `$check_result`, `$check_roll`, `$check_total`, `$check_dc`, `$check_modifier`, `$check_degree`, `$check_skill` |
| `<<skill_mod <source> <skill> <n>>>` | same | `source` = bucket key, e.g. `scene`, `drunk` |
| `<<clear_skill_mods <source>>>` | same | |
| `skill_rank(<skill>)` | same | function |
| `top_skill(a, b, …)` | `Dialogue/DialougueSkillComparison.cs` | **returns its FIRST argument on a tie** — argument order is a design decision, not incidental |
| `alias_name(<name>)` | `Dialogue/AliasNameGenerator.cs` | deterministic; `$player_alias` is a smart variable derived from it |
| `<<enter_environment>>` | `Dialogue/DialogueTransitionController.cs` | hands control back to the world |
(The filename `DialougueSkillComparison.cs` is misspelled in the repo. Left alone deliberately —
renaming it changes a `.meta` GUID. Fold it into a rename batch, not a drive-by fix.)
### 4.4 Flow control discipline
`<<detour>>` returns to the caller; `<<jump>>` does not and clears the return stack.
The rule the existing files follow, and which new content must follow:
- A node started directly by `DialogueInteractable` has nothing waiting on it → its branches
`<<jump>>`, and each destination calls `<<enter_environment>>` itself.
- A sub-conversation that must resume its caller → `<<detour>>`, and **never** `<<jump>>` inside it.
Nodes sharing a `title:` form a **node group**; Yarn runs the most specific variation whose
`when:` conditions pass. Prefer adding a variation over growing an `<<if>>` chain.
### 4.5 How content reaches Unity
There is **no manual sync step** for dialogue. The chain is:
```
.yarn file saved → Unity's YarnProjectImporter recompiles the .yarnproject on focus
→ DialogueRunner plays the new nodes on next Play
```
The only things that need a human/Editor step are the *data* files, not the dialogue:
```
skill_bible.json → Tools ▸ Nightclub Arcadia ▸ (SkillSystemSetup) → Skills/Definitions/*.asset + SkillDatabase.asset
candidates.json → Tools ▸ Nightclub Arcadia ▸ (CandidateSystemSetup) → Candidates/Definitions/*.asset + CandidateDatabase.asset
```
Those setup scripts also register a `[DidReloadScripts]` callback, so in practice the assets
regenerate on script reload too. The JSON is the source of truth either way.
### 4.6 Code-owned vs writer-owned
| Writer-owned (edit freely, no Unity) | Code-owned (needs an engineer) |
|---|---|
| `Assets/Dialogue/**/*.yarn` | `Assets/Scripts/**/*.cs` |
| `<<declare>>` lines in `Common.yarn` | `Assets/Editor/**/*.cs` |
| `Assets/Skills/skill_bible.json` (prose fields) | Yarn command/function *implementations* |
| `Assets/Candidates/candidates.json` (`status`, `sceneId`, `notes`, `unlockFlag`) | `.asset`, `.prefab`, `.unity`, `.meta` |
| `docs/` writing docs | `ProjectSettings/**` |
**A writer must never need to open Unity or touch C# to write or revise a scene, a character,
or a branch.** If a writing task requires either, that is a pipeline bug — file it.
---
## 5. Validation
### 5.1 EditMode test suite
`NightclubArcadia/Assets/Tests/EditMode/` — 7 files, ~870 LOC, two assemblies:
- `NightclubArcadia.Skills.Tests` — `SkillRosterTests`, `SkillCheckSystemTests`, `DcBandTests`,
`CommentaryPickerTests`, `CandidateRegistryTests`, `CandidateYarnLintTests`
- `NightclubArcadia.Locomotion.Math.Tests` — `MotionInputMathTests`
`CandidateYarnLintTests` is a **regex lint over Yarn source**: it asserts every
`$self_lean_*` / `$found_*` in any `.yarn` file resolves to a real id in `CandidateDatabase`.
It is a tripwire, not a proof.
### 5.2 One-click in-Editor run
`Tools ▸ Nightclub Arcadia ▸ Run Skill EditMode Tests` (`Assets/Editor/SkillSystemTestRunner.cs`).
### 5.3 YarnCheck — validate dialogue without Unity ⭐
`tools/YarnCheck` is a .NET 9 console app that compiles **and plays** the Yarn scripts using
the exact Yarn Spinner DLLs shipped in `Packages/dev.yarnspinner.unity/Runtime/DLLs`, so it
can never drift from the compiler the game uses. This is the fastest correctness signal in the
repo and it needs no Editor lock. Use it on every dialogue change.
```bash
cd /Users/lennart/Dev/nightlcub-arcadia/tools/YarnCheck
# compile everything — exit 1 + diagnostics on failure
dotnet run -- ../../NightclubArcadia/Assets/Dialogue
# compile, then play a node, choosing option 2, 2, 0, 0
dotnet run -- ../../NightclubArcadia/Assets/Dialogue Bartender_Talk 2 2 0 0
```
Leftover picks restart the node — that deliberately models the player walking away and coming
back, which is how these scenes are actually played.
**What it does not model** (read traces with this in mind): Unity commands are printed, not
executed, so `<<check>>` / `<<enter_environment>>` / `<<skill_mod>>` are stepped over and
`$check_result` stays `false` — every check-gated branch shows its *failure* side. Yarn
functions are stubbed: `top_skill` mirrors the real first-argument-wins tie behaviour,
`alias_name` returns a placeholder, `skill_rank` returns 0. Variables reset to their
`<<declare>>` defaults each run; there is no save state.
Requires the .NET 9 SDK. `tools/YarnCheck/bin/` and `obj/` are currently **committed** and
should not be — see `docs/restructure-plan.md`.
---
## 6. Code conventions
- Namespaces: `NightclubArcadia.<Area>` — `Skills`, `Dialogue`, `UI`, `UI.Dialogue`,
`UI.HUD`, `UI.Menu`, `Cinematics`, `Interaction`, `Player`, `Player.Math`.
**Exception:** `NPCStandIn.cs` sits in the global namespace. That is a scaffold tell, not a
convention to copy — new runtime types always get a namespace.
- Assembly definitions exist for `NightclubArcadia.Skills` and `NightclubArcadia.Locomotion.Math`
only. Camera, Player, UI, Dialogue, NPCs and Interaction all currently compile into the
default `Assembly-CSharp`. Widening asmdef coverage is planned, not done.
- Comments in this codebase carry *decision history* — why a flag is written at the bottom of a
node rather than the top, why four voices in SC-101 breaks the three-voice cap on purpose.
Match that density. Do not strip such comments as "noise"; they are the design record.
- Editor-only code lives under `Assets/Editor/`. Anything there is stripped from builds and may
reference `UnityEditor`.
---
## 7. Traps
1. **`Assets/Scenes/DialogueTest.unity` is serialized as binary**, and Unity will not convert it
from a headless run. The project is set to Force Text (`m_SerializationMode: 2`) and the API
agrees — `EditorSettings.serializationMode` reads `ForceText` — but Force Text governs
conversion **at the moment the setting changes**, not on every save. A file that entered the
project as binary stays binary. Four approaches were tried and all failed silently:
`AssetDatabase.ForceReserializeAssets` does not rewrite scene files; `OpenScene` +
`MarkSceneDirty` + `SaveScene` to the same path returns `true` and writes nothing, because
`MarkSceneDirty` does not take in batch mode; `SaveScene` to a *new* path writes a fresh file
that is also binary; and re-assigning `serializationMode` (Mixed → ForceText) from script
does not trigger a reserialize pass. **The fix is Editor-UI only:** Project Settings ▸ Editor
▸ Asset Serialization, switch the mode and switch it back, which reserializes on apply.
Until then the scene is not diffable or mergeable. It is committed and intact (14 roots).
2. **`unity run` reserves `-batchmode`, `-nographics`, `-quit`, `-logFile`.** Passing any of them
after `--` is a hard error, and its output does not reach stdout — read `Logs/Editor.log` (§3.4).
3. **`-quit` with `-runTests`** hangs or truncates. Omit it (§3.2).
4. **Exit code alone is not a test verdict.** Parse the results XML (§3.2).
5. **`which unity` fails** in non-interactive shells even though the CLI is installed. Use the
absolute path `/Users/lennart/.unity/bin/unity` (§3.0).
6. **Editor lock** — check before every headless run, filtering out `AssetImportWorker` children (§3.1).
7. **Regenerated assets** — hand edits to `Skills/Definitions/*`, `Candidates/Definitions/*`
and the two `*Database.asset` files are silently discarded (§2.1).
8. **`top_skill` ties** resolve to the first argument. Reordering arguments changes the game.
9. **No `.gitattributes`** — no Unity YAML merge driver, no LFS rule for `Assets/Art/`.
Scene and prefab merges will conflict destructively under branching.
10. **Template leftovers still present**: `Assets/Scenes/SampleScene.unity`, `Assets/Readme.asset`,
`Assets/TutorialInfo/`. `SampleScene` is in the build scene list. Not load-bearing.
+544
View File
@@ -0,0 +1,544 @@
# Restructure Plan — writer-facing content pipeline + migration off the test scaffold
Status: **proposal, not executed.** Nothing in this document has been applied to the
repository. Two files were written as part of producing it — `CLAUDE.md` and `AGENTS.md`
at the repo root — because they were requested as deliverables. No asset, script, scene
or setting was moved, renamed or altered.
Companion reading: `docs/skill-system-refactor-plan.md`, `docs/candidate-system-plan.md`,
`docs/candidate-system-implementation-plan.md`.
---
## 0. Status — Phase 0 is complete
Executed 2026-08-25. The working tree is clean and tagged `pre-restructure`.
All previously uncommitted work is now in git across five commits: camera/locomotion/interaction,
the UI layer, player control setup, the dialogue-vs-menu interaction fix, and the scene itself.
A full pre-flight backup was taken first (`.tar.gz` of the tree excluding `Library/`, `Temp/`,
`Logs/`, and build output) plus a separate copy of the scene file.
Everything below from §1 onward is unchanged proposal. Nothing in §2–§5 has been applied.
### 0.1 The binary scene — diagnosis corrected
The original reading of this was **wrong**, and the correction matters for Phase 3.
The theory was that the scene had been saved during a temporary Force Binary session and merely
needed re-saving. It isn't that. Unity **re-writes this scene as binary even now**, with the
project set to Force Text and `EditorSettings.serializationMode` reporting `ForceText` from the
API. Force Text governs conversion **at the moment the setting changes**, not on every
subsequent save — so a file that entered the project as binary stays binary indefinitely.
Four approaches were tried headlessly, all failing silently with success return codes:
| Approach | Result |
|---|---|
| `AssetDatabase.ForceReserializeAssets(scenePath)` | Opens and imports the scene; does not rewrite scene files at all |
| `OpenScene` + `MarkSceneDirty` + `SaveScene(samePath)` | Returns `true`, writes nothing — `MarkSceneDirty` does not take in batch mode |
| `OpenScene` + `SaveScene(newPath)` | Writes a fresh file that is **also binary**, byte-identical in size |
| `serializationMode = Mixed` → `ForceText` | Mode changes as logged; no reserialize pass occurs |
**The remaining fix is Editor-UI only** and takes about thirty seconds:
> Project Settings ▸ Editor ▸ Asset Serialization → set **Mixed**, let it apply, then set
> **Force Text** again. Changing the value in the dialog is what triggers the reserialize pass.
Then verify from the terminal — you want `%YAML 1.1`:
```bash
head -c 20 NightclubArcadia/Assets/Scenes/DialogueTest.unity
```
Worth knowing: this will reserialize **every** asset the setting touches, so expect a wide diff.
That is now safe to inspect and revert per-file, because everything is committed.
The scene itself is healthy — it opens with 14 roots (`Room_101`, `Dialogue System`,
`Skill System`, the three UI canvases, `CinemachineBrain`, `CinemachineCamera`, `Navigation`,
`CameraAnchor`, `CM_Reveal_Room101`, `RevealVolume_Room101`, `Directional Light`, `UI System`).
Its md5 was verified unchanged across every failed attempt. The cost of it staying binary is
that it is **not diffable and not mergeable** — which is an argument for bringing Phase 3's
scene split forward, since the split rebuilds these files anyway.
### 0.2 The Unity CLI — finding corrected
The initial survey reported the standalone `unity` CLI as not installed and lacking build/test
commands. It **is** installed, at `/Users/lennart/.unity/bin/unity` (v1.0.0-beta.6), and it has
both `build` and `test`, plus `run`, `open`, and editor management. The original check failed
because the binary is not on `PATH` for non-interactive shells — `unity doctor` flags this
itself as `check.binary-on-path warn`.
It is the better tool for CI work than raw `-batchmode`: `unity test` handles NUnit **and**
JUnit reports, `--retries` with flaky detection, `--rerun-failed`, and `--shard N/M`. See
`CLAUDE.md` §3 for the full command set and the gotchas (`unity run` reserves `-batchmode`,
`-nographics`, `-quit` and `-logFile`; its output goes to `Logs/Editor.log`, not stdout).
---
## 1. What the survey actually found
The brief described a proof-of-concept where dialogue is authored inside the Editor. That is
not what is in the repository. The state is considerably more advanced, and that changes what
the work is.
### 1.1 Already correct — do not "fix" these
| Thing | State |
|---|---|
| **Dialogue is already plain text** | 679 lines across 9 `.yarn` files under `Assets/Dialogue/{Scenes,Characters,Objects}/`. No Editor authoring anywhere. |
| **The `.yarnproject` glob** | `**/*.yarn`. A new scene or character file is picked up with zero registration. Nothing to build here. |
| **A JSON → ScriptableObject codegen pipeline exists** | `skill_bible.json` and `candidates.json` are the source of truth; `Assets/Editor/SkillSystemSetup.cs` and `CandidateSystemSetup.cs` regenerate the `.asset` files. This is exactly the right pattern and it is already load-bearing. |
| **A headless Yarn validator exists** | `tools/YarnCheck` compiles *and plays* Yarn against the game's own Yarn Spinner DLLs, no Unity needed. |
| **A real EditMode suite exists** | ~870 LOC / 7 files, including `CandidateYarnLintTests`, which lints Yarn source against the candidate registry. |
| **Variable discipline exists** | `Common.yarn` declares everything project-wide so typos are compile errors. |
### 1.2 The real gap for a Cowork writer
It is not the `.yarn` files. It is three things:
1. **The writing handbook does not exist as a file.** `SC101.yarn` cites a *"cap introductions
at three voices"* rule and `docs/candidate-system-implementation-plan.md` says outright that
the handbook is not in this repo. So the rules a writer must follow are partly in someone's
head, partly scattered across code comments. A Cowork session cannot read a handbook that
isn't checked in — which is precisely the "re-explaining the world each time" problem.
2. **The best character material is trapped inside JSON string fields.** `skill_bible.json`
holds, per skill, a `notes` field containing an entire embedded prose sheet — domain, *what
it wants for you*, *what it's wrong about*, register, verbal tics, SUCCESS/FAILURE/PASSIVE
voice samples, firing frequency, forbidden domains. It is excellent writing. It is stored as
a single `\n`-escaped string in a machine file. A writer cannot comfortably read or revise it,
and every edit risks breaking the JSON.
3. **NPC characters have no sheets at all.** The Bartender, Bradford Kane and Chevalier Cassian
Thal exist only as their `.yarn` lines. There is no record of who they are, what they know,
or how they speak.
### 1.3 Scaffold vs. load-bearing
| Verdict | Items |
|---|---|
| **Load-bearing** | `Scripts/Skills/**` (25 files, asmdef'd), `Scripts/UI/**` (13 files, ~1660 LOC), `Scripts/Camera/**` (7 files, ~727 LOC), `Scripts/Player/**` (5 files, ~654 LOC + `Locomotion.Math` asmdef), `Scripts/Interaction/**`, `Scripts/Dialogue/**`, `Assets/Dialogue/**`, `Assets/Skills/**`, `Assets/Candidates/**`, `Assets/Prefabs/UI/**`, `Assets/Tests/**`, `tools/YarnCheck` |
| **Scaffold — replace** | `NPCStandIn.cs` (global namespace, sphere-primitive placeholder, hardwired `[F]` prompt, "drop this on a sphere" in its own docstring), `Assets/Editor/YarnDemoSetup.cs` (targets `DialogueTest.unity` by literal path), `Assets/Scenes/DialogueTest.unity` itself |
| **Scaffold — keep for now** | `Assets/Editor/UILayerSetup.cs` (818 lines), `PlayerControlSetup.cs` (316), `UILayerSetupAutoRun.cs`. These are one-shot scene/prefab builders. They are how the scene got wired and they still work; retiring them is a later step, not this one. |
| **Template leftovers — delete** | `Assets/Scenes/SampleScene.unity`, `Assets/Readme.asset`, `Assets/TutorialInfo/` |
| **Should not be committed** | `tools/YarnCheck/bin/`, `tools/YarnCheck/obj/`, `.DS_Store` files |
### 1.4 Infrastructure gaps
- **No `.gitattributes`.** No Unity YAML merge driver, no LFS rule for `Assets/Art/`. Scene and
prefab merges will conflict destructively the first time work happens on two branches.
- **asmdef coverage is partial.** Only `Skills` and `Locomotion.Math`. Camera, Player, UI,
Dialogue, NPCs and Interaction all land in `Assembly-CSharp`, so every C# edit recompiles
everything.
- **No PlayMode tests.** `Assets/Tests/` is EditMode only.
- **No build entry point.** No `-executeMethod` build script; `EditorBuildSettings` lists
`SampleScene` and `DialogueTest`.
- **Single branch, no remote work in flight.** `main` only, `origin/main` in sync.
---
## 2. Part 1 — externalizing dialogue and character writing
### 2.1 The principle
**Prose lives in markdown. Machine-readable structure lives in JSON. Playable content lives in
`.yarn`. Nothing that a writer needs to read is stored as an escaped string inside a data file.**
The existing JSON-is-source-of-truth pattern is kept and extended — not replaced. What changes
is that the *prose* half moves out of the JSON into markdown with frontmatter, and the JSON keeps
only the fields code actually reads.
### 2.2 Proposed structure
A new top-level `writing/` directory at the **repo root**, deliberately outside `Assets/`:
```
writing/ ← the Cowork working directory
├── README.md # "start here" for a writing session
├── STYLE.md # the missing writing handbook, finally checked in
├── GLOSSARY.md # world terms, factions, place names, in-fiction jargon
├── YARN-PRIMER.md # the ~1 page of Yarn syntax a writer needs
├── characters/
│ ├── _TEMPLATE.md
│ ├── bartender.md
│ ├── bradford-kane.md
│ └── chevalier-cassian-thal.md
├── voices/ # the eleven skills, as readable sheets
│ ├── _TEMPLATE.md
│ ├── provenance.md
│ ├── confluence.md
│ └── … (facework, the-float, room-tone, amnesty, house-pour,
│ long-shift, placement, standing-order, undisclosed)
├── scenes/
│ ├── _TEMPLATE.md
│ ├── sc101-the-card-at-the-desk.md # brief, beats, tone — NOT the dialogue
│ └── sc102-….md
└── lore/
├── candidates.md # the three readings, in prose
└── world.md # the club, the conference, the night
```
Playable dialogue stays exactly where it is, in `NightclubArcadia/Assets/Dialogue/`. **Do not
move the `.yarn` files.** They are already correct, Unity's importer watches that path, and the
`.meta` files there carry GUIDs that scene references depend on.
So the writer's day looks like: read `writing/`, write `.yarn`. Two directories, no Unity.
**Why root and not `Assets/writing/`:** anything under `Assets/` gets a `.meta` file, gets
imported, and shows up in the Unity project window. Markdown notes should not be Unity assets.
Root-level also means a Cowork session can be pointed at `writing/` alone.
### 2.3 File format: markdown + YAML frontmatter
Frontmatter carries the few fields a script may want to check; the body is unconstrained prose.
**`writing/characters/_TEMPLATE.md`:**
```markdown
---
id: bartender # matches the Yarn speaker name, lowercased/kebabed
name: The Bartender # exact string used as the Yarn speaker prefix
yarn_file: Assets/Dialogue/Characters/Bartender.yarn
status: authored # planned | drafted | authored
scenes: [SC101]
knows: [doss, the-estate-drink]
---
## Who they are
One paragraph. What they want tonight.
## How they speak
Register, sentence length, contractions, what they never say.
## What they know
Bullet list. For each: does the player learn it, under what condition, and which
`$flag` records it.
## What they must never do
The negative constraints. Usually more useful than the positive ones.
## Sample lines
Three to five. Not for shipping — for calibration.
```
**`writing/voices/_TEMPLATE.md`** mirrors the structure already present in
`skill_bible.json`'s `notes` field, promoted to real headings — `domain`, *what it wants for
you*, *what it's wrong about*, *how it addresses you*, register, verbal tics, *what it never
does*, opposed/allied, SUCCESS / FAILURE / PASSIVE voice, firing frequency, forbidden domains.
That structure was already good; it just needs to be readable.
**`writing/scenes/_TEMPLATE.md`** carries the brief and the beat list, plus the constraints that
are currently buried in `.yarn` header comments — the evidence-balance rule, the beat order, the
passive-pattern rule, any deliberate exception. `SC101.yarn`'s 25-line header comment is the
model; it should live here and be *referenced* from the `.yarn` file, not duplicated.
### 2.4 Migrating the Skill Bible without losing the JSON pipeline
`skill_bible.json` currently holds both machine fields (`axis`, `opposedTo`, `alliedWith`,
`targetFiringsPerHour`, `accentColor`, `startingRank`, `primaryChannel`, `clueYield`) and the
prose blob (`notes`, plus a duplicated `domain`).
Proposed split, once:
1. Write a one-shot script that reads `skill_bible.json` and emits `writing/voices/<id>.md` —
frontmatter from the machine fields, body from the parsed `notes`.
2. `SkillDefinition.notes` stays in the C# type and keeps rendering wherever it renders. The
generator that produces it now reads the markdown body instead of the JSON string.
3. `skill_bible.json` remains the machine source. Either it keeps a `notes` field regenerated
*from* the markdown, or `SkillSystemSetup.cs` learns to read `writing/voices/*.md` directly.
**Recommendation: the first.** `writing/voices/*.md` is the human source; a small script folds
prose back into `skill_bible.json`; `SkillSystemSetup.cs` is untouched. That keeps the existing,
tested Editor pipeline unchanged and confines the new machinery to one script — which matters
because `SkillRosterTests` asserts against the generated assets.
The direction of truth must be written down and never ambiguous:
```
writing/voices/*.md ──(sync script)──▶ skill_bible.json ──(SkillSystemSetup)──▶ *.asset
human writes here machine mirror build output
```
An EditMode test should assert the mirror is in sync, so a JSON hand-edit fails CI instead of
silently winning.
### 2.5 The flow back into Unity
| Change | What the writer does | Sync step | Verify |
|---|---|---|---|
| New/edited dialogue line, branch, node | edit `.yarn`, save | **none** — importer picks it up | `dotnet run` YarnCheck |
| New scene or character `.yarn` file | create file under `Assets/Dialogue/` | **none** — `**/*.yarn` glob | YarnCheck |
| New variable | add `<<declare>>` to `Common.yarn` | **none** | YarnCheck |
| Voice prose | edit `writing/voices/*.md` | run sync script → JSON | EditMode tests |
| Character sheet, scene brief, lore, style | edit `writing/*.md` | **none** — docs only | — |
| Candidate/clue status, new clue id | edit `candidates.json` | Editor menu or script reload | `CandidateYarnLintTests` |
The everyday writing loop has **no manual sync step at all**. That is the design goal and it is
already met for `.yarn`; the work is to reach the same property for character and voice material.
### 2.6 Validation a writer can run alone
```bash
cd tools/YarnCheck
dotnet run -- ../../NightclubArcadia/Assets/Dialogue # does it compile?
dotnet run -- ../../NightclubArcadia/Assets/Dialogue Bartender_Talk 2 2 0 0 # does it play?
```
Requires only the .NET 9 SDK — no Unity, no license, no Editor lock. This should be the first
thing `writing/README.md` tells a new session to run, and the last thing it tells them to run
before handing work back.
Caveat that must be stated plainly in the README, because it will otherwise cause confusion:
YarnCheck stubs `<<check>>`, so `$check_result` is always `false` and **every check-gated branch
shows its failure side**. A writer seeing only failure prose has not found a bug.
### 2.7 Scope boundary
Writer-owned: `writing/**`, `Assets/Dialogue/**/*.yarn`, prose fields in the two JSON files.
Code-owned: all C#, all `.asset` / `.prefab` / `.unity` / `.meta`, all `ProjectSettings/`,
and the *implementation* of any Yarn command or function.
A writer needing a new Yarn command is a request to engineering, not a task they can complete —
and that is the only such boundary. Everything else about writing a scene must be self-serve.
---
## 3. Part 2 — target project structure
### 3.1 Scenes
The current model — one scene holding everything — is what makes `DialogueTest.unity` both
irreplaceable and unmergeable. The target is additive scene loading, which is also the standard
answer to Unity scene merge conflicts.
```
Assets/Scenes/
├── Bootstrap.unity # entry point. Systems only, no geometry. First in build list.
├── Systems/
│ └── Systems.unity # DialogueRunner, UIStateController, PlayerSkills, Commentary,
│ # camera director. Loaded additively by Bootstrap, never unloaded.
├── Levels/
│ └── SC101_ConferenceHall.unity # geometry, lights, nav mesh, NPC + interactable placement
└── Dev/
└── Sandbox.unity # scratch scene, explicitly disposable
```
`Bootstrap` loads `Systems` then the requested level. The payoff is direct: two people (or a
person and an agent) can edit level content and systems wiring without touching the same file,
and a corrupted level scene no longer takes the systems layer with it.
`DialogueTest.unity` becomes `Levels/SC101_ConferenceHall.unity` — **renamed and split, not
rebuilt**. Its contents are the asset; only its scope changes.
### 3.2 Folders
Mostly already right. The deltas:
```
Assets/
├── Art/ ← exists (placeholder portrait). Add Environment/, Characters/, VFX/
├── Audio/ ← new: Music/, SFX/, VO/
├── Dialogue/ ← unchanged. Do not move.
├── Candidates/ Skills/ ← unchanged (JSON + generated assets)
├── Prefabs/
│ ├── UI/ ← exists
│ ├── Characters/ ← new: real NPC prefabs, replacing sphere stand-ins
│ ├── Interactables/ ← new: chair, door, object interactables
│ └── Systems/ ← new: Bootstrap/Systems prefabs
├── Scenes/ ← per §3.1
├── Scripts/
│ ├── Core/ ← new: bootstrap, scene loading, save/load, service locator
│ ├── Camera/ Player/ Interaction/ Dialogue/ UI/ Skills/ ← unchanged
│ └── NPCs/ ← NPCStandIn replaced by a real NPC component (§4, step 6)
├── Settings/ ← unchanged (URP)
└── Tests/
├── EditMode/ ← unchanged
└── PlayMode/ ← new
```
### 3.3 Conventions
- **Scenes:** `PascalCase`. Levels prefixed with their scene id — `SC101_ConferenceHall`.
- **Prefabs:** `PascalCase`, no `_Prefab` suffix. Variants suffixed `_Variant`.
- **Scripts:** one public type per file, filename = type name. Namespace `NightclubArcadia.<Area>`,
matching the folder. **No global-namespace runtime types.**
- **Yarn nodes:** the existing conventions hold and are already consistent —
`<Character>_Talk`, `SC###_Hook_<Subject>`, `Commentary_<Context>_<skill_id>`.
- **Yarn variables:** the families in `CLAUDE.md` §4.2. These are lint-enforced; treat them as API.
- **asmdefs:** one per top-level `Scripts/` area, named `NightclubArcadia.<Area>`.
- **`.yarn` filenames** may contain spaces (`Bradford Kane.yarn`) — the glob handles it, but
every shell command touching them needs quoting. Worth normalising to kebab-case during a
rename batch; low priority.
---
## 4. The migration plan
Incremental, ordered, each step independently committable and revertible. **Steps 0–2 are the
safety work and should happen before anything is moved.**
Effort marks are rough: **S** ≲1h, **M** a half day, **L** a day or more.
### Phase 0 — checkpoint ✅ DONE (2026-08-25)
| # | Step | Status |
|---|---|---|
| 0.1 | Close the Unity Editor | done |
| 0.2 | Full backup of the tree + separate copy of the scene | done |
| 0.3 | Commit all uncommitted work in logical slices (§5) | done — 5 commits |
| 0.4 | Convert `DialogueTest.unity` to text | **blocked** — Editor-UI only, see §0.1 |
| 0.5 | Tag `pre-restructure` | done |
| 0.6 | Push `main` and the tag to `origin` | pending |
0.4 is the one open item and it does not block Phase 1 or 2.
### Phase 1 — repo hygiene (no Unity changes)
| # | Step | Detail | Effort |
|---|---|---|---|
| 1.1 | Add `.gitattributes` | Unity YAML merge driver for `*.unity` / `*.prefab` / `*.asset`; LFS for `Assets/Art/**` binaries; `* text=auto eol=lf` | S |
| 1.2 | Untrack `tools/YarnCheck/{bin,obj}` | `git rm -r --cached`, add ignore rules | S |
| 1.3 | Remove committed `.DS_Store` files, confirm ignore rule | S |
| 1.4 | Add a `Makefile` or `scripts/` wrapping the `CLAUDE.md` commands | `make test`, `make yarncheck`, `make build` | S |
`.gitattributes` before any restructuring is deliberate — the moment work happens on a branch,
scene merges without a merge driver will corrupt files.
### Phase 2 — writing pipeline (no Unity changes, fully parallel to Phase 3+)
| # | Step | Detail | Effort |
|---|---|---|---|
| 2.1 | Create `writing/` skeleton + templates | §2.2, §2.3 | S |
| 2.2 | **Write `writing/STYLE.md`** | The missing handbook. Recover the rules from `.yarn` header comments and the three `docs/` plans. Highest-value single item in this plan. | M |
| 2.3 | Extract `skill_bible.json` `notes` → `writing/voices/*.md` | one-shot script, 11 files | M |
| 2.4 | Write the sync script md → JSON + an EditMode drift test | §2.4 | M |
| 2.5 | Author the three character sheets | Bartender, Bradford Kane, Chevalier Cassian Thal | M |
| 2.6 | Write `writing/README.md` + `YARN-PRIMER.md` | The Cowork session entry point, incl. the YarnCheck caveat (§2.6) | S |
| 2.7 | Move `SC101.yarn`'s header rules into `writing/scenes/sc101-*.md` | leave a one-line pointer in the `.yarn` | S |
Phase 2 touches **no Unity asset at all**. It can proceed while the Editor is open and while
Phase 3 is in flight, and it is what unblocks Cowork. Consider doing it first.
### Phase 3 — scene restructure (the risky part)
| # | Step | Detail | Effort |
|---|---|---|---|
| 3.1 | Delete template leftovers | `SampleScene.unity`, `Readme.asset`, `TutorialInfo/`; fix `EditorBuildSettings` | S |
| 3.2 | Create `Bootstrap.unity` + `Systems/Systems.unity` | Systems empty at first | M |
| 3.3 | **Copy** `DialogueTest.unity` → `Levels/SC101_ConferenceHall.unity` | Copy, do not move. Keep the original until 3.6. | S |
| 3.4 | Move systems objects out of the level scene into `Systems.unity` | dialogue runner, UI layer, skills, commentary, camera director | L |
| 3.5 | Wire additive loading in `Scripts/Core/` | Bootstrap → Systems → level | M |
| 3.6 | Verify parity, then delete `DialogueTest.unity` | full playthrough of SC-101 vs. pre-migration behaviour | M |
| 3.7 | Retarget `YarnDemoSetup.cs` or retire it | it hardcodes `Assets/Scenes/DialogueTest.unity` | S |
Step 3.4 is where things break. Do it in small commits — one system per commit, playtest between
each. Do **not** batch it.
### Phase 4 — code structure
| # | Step | Detail | Effort |
|---|---|---|---|
| 4.1 | Add asmdefs per area | Camera, Player, Interaction, Dialogue, UI, NPCs, Core | M |
| 4.2 | Add `Assets/Tests/PlayMode/` + asmdef | first test: bootstrap loads and reaches playable state | M |
| 4.3 | Add `Assets/Editor/BuildPipeline.cs` | `-executeMethod` entry, `EditorApplication.Exit(1)` on failure (`CLAUDE.md` §3.4) | M |
| 4.4 | Rename batch | `DialougueSkillComparison.cs` → `DialogueSkillComparison.cs`; kebab-case `.yarn` filenames. Rename **in the Unity Editor** so `.meta` GUIDs are preserved. | S |
### Phase 5 — replace the scaffold
| # | Step | Detail | Effort |
|---|---|---|---|
| 5.1 | Design the real NPC component | data-driven from a character definition asset, not public fields on a sphere | M |
| 5.2 | Build `Prefabs/Characters/` prefabs | one per named character | M |
| 5.3 | Migrate scene NPCs off `NPCStandIn`, delete it | proximity prompt + label logic is worth keeping — extract, don't discard | M |
| 5.4 | Retire the `Assets/Editor/*Setup.cs` one-shot builders | only once prefabs are authored and committed; `UILayerSetup.cs` alone is 818 lines of accumulated wiring knowledge | L |
### 4.6 Keep / relocate / replace, at a glance
| Verdict | Items |
|---|---|
| **Keep as-is** | `Assets/Dialogue/**`, `Assets/Skills/**`, `Assets/Candidates/**`, `Scripts/Skills/**`, `Scripts/UI/**`, `Scripts/Camera/**`, `Scripts/Player/**`, `Scripts/Interaction/**`, `Scripts/Dialogue/**`, `Assets/Tests/EditMode/**`, `Assets/Prefabs/UI/**`, `tools/YarnCheck`, `Assets/Settings/**` |
| **Rename / relocate** | `DialogueTest.unity` → `Levels/SC101_ConferenceHall.unity` (+ split); `skill_bible.json` prose → `writing/voices/*.md`; `SC101.yarn` header rules → `writing/scenes/`; `DialougueSkillComparison.cs` spelling; `.yarn` filenames with spaces |
| **Replace** | `NPCStandIn.cs` → real NPC component + prefabs; `YarnDemoSetup.cs` → retargeted or retired; single-scene model → Bootstrap/Systems/Level |
| **Delete** | `SampleScene.unity`, `Readme.asset`, `TutorialInfo/`, committed `bin/` `obj/` `.DS_Store` |
| **Add** | `.gitattributes`, `writing/**`, `Scripts/Core/**`, `Tests/PlayMode/**`, `Editor/BuildPipeline.cs`, per-area asmdefs, `Assets/Audio/` |
---
## 5. Checkpointing — what was done
Roughly **3,100 lines of C#** across untracked directories, 291 changed lines in tracked
scripts, 5 UI prefabs and the scene were uncommitted. All of it is now in git.
**Order used**, which differs from the original draft on one point: the safe text work was
committed *before* any Unity run, so that a headless Editor invocation could not endanger it.
That turned out to matter — four Unity runs followed, one ending in a `SIGILL` on teardown.
1. Verified the Editor was closed and no lockfile was held.
2. Took a full backup: a `.tar.gz` of the tree excluding `Library/`, `Temp/`, `Logs/` and build
output, plus a separate copy of the scene file.
3. Committed in five slices, in dependency order:
| Commit | Contents |
|---|---|
| `add camera rig, click-to-move locomotion, and click interaction` | `Scripts/{Camera,Player,Interaction}`, locomotion math tests, Cinemachine package, `Interactable`/`Ground` tags, NavMesh agent dimensions |
| `add UI layer: dialogue stream, HUD, player menu, and skills panel` | `Scripts/UI`, Skills UI views + asmdef reference, `Prefabs/UI`, placeholder art, `UILayerSetup` |
| `wire player control setup and tune the third-person controller` | `PlayerControlSetup.cs`, StarterAssets input/armature/controller |
| `suppress world interaction while a menu panel is open` | `Scripts/Dialogue`, `Scripts/NPCs`, yarnproject graph positions |
| `check in the DialogueTest scene and add it to the build settings` | the scene (binary, deliberately) + `EditorBuildSettings` |
4. Attempted the binary→text conversion four ways; all failed (§0.1). Committed the scene as
binary rather than leave it untracked — a valid, intact, opening scene in an awkward format
beats days of wiring living only on disk.
5. Tagged `pre-restructure`.
Slices rather than one lump, because the restructure will want to revert *part* of this later.
**Still to do:** push `main` and the tag to `origin`, and the Editor-UI serialization fix (§0.1).
### 5.1 Residual risks after checkpointing
| Risk | Mitigation |
|---|---|
| Scene re-save produces a text file that differs semantically from the binary one | Play SC-101 end to end before committing; compare against the pre-migration run |
| Renaming assets outside Unity orphans `.meta` GUIDs | Rename inside the Editor, always |
| Scene merge conflicts once branching starts | `.gitattributes` merge driver in Phase 1.1, before any branch |
| Deleting `UILayerSetup.cs` too early loses wiring knowledge | Phase 5.4 is last, and only after prefabs are authored and committed |
| Binary scene state recurs | Verify `EditorSettings` shows Force Text; add a CI check that `*.unity` files start with `%YAML` |
---
## 6. Sequencing recommendation
**Phase 0 today.** It is under an hour and it is the difference between a recoverable project
and a lost week.
**Then Phase 2, before Phase 3.** The writing pipeline touches no Unity asset, cannot break the
game, and is what actually unblocks the stated goal of writing with Cowork. Phase 3 is the risky
work and it does not need to come first. `writing/STYLE.md` (2.2) is the single highest-value
item in this document — it is the artifact whose absence is currently forcing you to re-explain
the world every session.
**Phase 1 alongside Phase 2**, since `.gitattributes` must land before any branching.
**Phase 3–5 after**, in order, small commits, playtest between each.
---
## 7. Open questions
1. **Does a writing handbook exist outside the repo?** `docs/candidate-system-implementation-plan.md`
says it exists but is not checked in. If there is a document somewhere, importing it beats
reconstructing it (Phase 2.2 assumes reconstruction).
2. ~~How did the scene become binary?~~ **Answered** (§0.1): Force Text only converts when the
setting changes, so anything that entered binary stays binary. The open part is *how* it
first entered binary — most likely created or imported during a Force Binary window. Adding a
CI check that every `*.unity` starts with `%YAML` would catch a recurrence cheaply.
3. **Is `SC101_ConferenceHall` the right level granularity,** or should the conference hall split
further (entrance / hall / bar / back office) given the `$visited_*` flag families already
declared in `Common.yarn`? Those flags imply at least seven distinct locations.
4. **Target platform(s)?** `EditorBuildSettings` and the URP settings carry both PC and Mobile
render pipeline assets. It affects whether Phase 4.3's build script targets one platform or several.
5. **Should `writing/` be a separate repository** so Cowork sessions never see Unity? Recommendation:
**no** — the coupling to `.yarn` files and `candidates.json` is tight enough that split repos
would drift. A single repo with a clear directory boundary is the better trade.