Topic 4 · Verify & test
Agent compile check
The agent proves its code compiles before it claims a change is done.
Never ask the user to compile, refresh, or check the Console for script errors. After any C# / asmdef / package / define change that can affect compile, you drive compile and read the result via tools. Handing “please compile in Unity and paste errors” is a rule violation unless the Editor/MCP is unreachable (see Exceptions).
When you must compile
Trigger a self-owned compile check after you:
- Create, edit, move, or delete
.cs/.asmdef/.asmref - Change
Packages/manifest.json, packages-lock, or project scripting defines - Add/remove assembly references that other code depends on
- Finish a multi-file slice and claim the work “done” / ready for play smoke
Skip a full cycle only for pure docs, comments, or non-code assets that cannot affect ScriptAssemblies.
How (Unity projects with MCP)
Use unity_* MCP tools only — never raw HTTP to the bridge. Multi-instance: unity_list_instances → unity_select_instance first.
- Refresh / request compile (pick what the session supports):
- Prefer forcing asset DB refresh + script compile via
unity_execute_codewhen available, e.g.:csharp UnityEditor.AssetDatabase.Refresh; UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation;- Unfocused Editor (HARD, UNFOCUSED-COMPILE-IS-A-REQUESTED-TICK): Auto Refresh off plus an unfocused window keeps serving the pre-edit project.isCompiling: falseand a 0-error query are the last cycle until you request one. Do not steal focus. Keep the editor ticking, then Refresh, then recompile, then poll:unity command set_autotick --enable true # AssetDatabase.Refresh in its OWN eval/exec — a reload wipes a combined call unity command recompile unity command recompile_status # until completed; up_to_date BEFORE Refresh is not a compilePipelinerecompilewith no preceding Refresh reportsup_to_dateand skips the import. Prove the new type loaded (Type.GetType/ assembly name), not file mtime. MCP equivalent: Refresh in its own exec, thenRequestScriptCompilation, then poll until the bridge goes unreachable (reload started) and returns. See MCP-FORCE-COMPILE-NEEDS-FOCUS in the Live play testing via Unity MCP page and AUTO-REFRESH-OFF below. - Wait for idle — poll
unity_editor_stateuntil compilation is finished (do not assume zero-latency). - Read errors from the pipeline, not the user —
unity_get_compilation_errorswithseverity: "error"(use"all"only if diagnosing noise). Prefer this overunity_console_logfor script failures (console can be cleared / flooded). - Fix and re-run — stay in the loop until errors from your change set are gone, or you have a documented blocker (missing package, Latios parked, Editor not open).
- Report — in the agent reply, state compile outcome briefly: clean, N errors fixed, or blocked + why. Do not dump full logs unless needed.
Never ask the user to click Unity — take the foreground yourself (HARD)
"Please focus the Editor so it refreshes" is the same rule violation as "please compile". Unity only runs an asset refresh when its window is activated, so a background Editor sits on stale files indefinitely — but you can activate it yourself. Do not wait on a human click.
Windows blocks a naive focus call, and one of the failures lies to you:
SetForegroundWindowalone — no effect, focus does not move.WScript.Shell.AppActivate($pid)— returnsTruewhile focus never moves. Never treat its return value as evidence.
What actually works — clear the foreground lock timeout, then attach to the current foreground thread's input queue so the OS treats the call as user-initiated:
# 0x2001 = SPI_SETFOREGROUNDLOCKTIMEOUT -> 0
[FG]::SystemParametersInfo(0x2001, 0, [IntPtr]::Zero, 3)
$fgThread = GetWindowThreadProcessId(GetForegroundWindow) # thread that owns focus now
[FG]::AttachThreadInput($myThread, $fgThread, $true)
[FG]::ShowWindow($unity, 9); [FG]::BringWindowToTop($unity); [FG]::SetForegroundWindow($unity)
[FG]::AttachThreadInput($myThread, $fgThread, $false)
Verify by reading GetForegroundWindow back and comparing the PID — never by the return
value of the focus call. The return value lies in BOTH directions: AppActivate says True while focus
never moves, and an in-process SetForegroundWindow sequence reported false while the compile it wrapped
ran anyway (the compile was requested by RequestScriptCompilation before the focus step, so the flag
described the window, not the work — never return a focus bool from a "force compile" helper).
Verified cause of the false: the same in-editor Focus called on its own returned true twice, so a
focus grab issued right after RequestScriptCompilation lands while the editor is already tearing down
for the reload — order it BEFORE the compile request, or focus from outside. An external PowerShell
process (tools/focus-unity.ps1 shape: registry PID, else a Win32_Process scan filtered to the
windowed Unity.exe — AssetImportWorkers share the -projectPath) took the foreground reliably and is
the only path in the bootstrap case, where no bridge exists. (a tower-defense game.) Confirm the refresh actually started (Editor.log grows / Library/PackageCache
hash changes), because focus succeeding does not prove the import ran.
Bootstrap case — the one the bridge cannot solve. When the MCP plugin is not yet imported,
or its own assembly failed to compile, there is no unity_execute_code to call and no bridge port.
Force-focus is then the ONLY way to make Unity import the plugin. Ordering:
write files -> force focus -> poll for the bridge port -> use
unity_execute_codefor every later cycle.
A requested compile drops the bridge — that is success, not failure.
CompilationPipeline.RequestScriptCompilation triggers a domain reload, so the in-flight MCP call
usually returns a transport error (Error polling queue: fetch failed). Expected. Poll until the port
answers again, then read unity_editor_state + unity_get_compilation_errors. Do not retry the
compile request on that error — you would loop reloading the domain.
Once the plugin is live, prefer the in-editor path and keep focus-stealing rare — it interrupts whatever the user is doing.
Edit source as bytes, not lines (Windows)
sed -i in Git-Bash rewrites the whole file in text mode: a CRLF file comes back LF-only, so a
one-line fix lands as an every-line diff in Plastic/git and buries the real change. grep -c $'\r'
does not catch it — the shell can eat the pattern and match every line in both files, reporting
identical bogus counts.
Use a binary read/replace/write (open(p,'rb') -> bytes.replace -> open(p,'wb')) for source
edits, and prove it with a byte census: size delta must equal exactly the characters you intended,
with CRLF/LF/BOM counts unchanged. Quick gate before git add: file <path> must still say "CRLF line
terminators"; a sed-edited C# source reads plain "ASCII text" (added).
Git plumbing from Python on Windows: BINARY stdin, then assert the tree changed (HARD)
subprocess.run(..., text=True) translates the stdin payload's \n to \r\n on Windows, so
every path fed to git update-index --add --remove --stdin arrives CR-terminated, matches
nothing on disk or in the index, and is silently ignored — exit 0, write-tree returns the
parent's tree, commit-tree + update-ref happily create an EMPTY commit whose message claims
a change that is not in it. Nothing errors anywhere; only reading the commit back exposes it.
(a mobile strategy game: a 3,700-file IP-purge "landed" twice as empty shells before a
git show --stat caught it.)
- Feed git plumbing bytes:
input=('\n'.join(paths) + '\n').encodewith NOtext=Trueon that call (or use-z+ NUL separators). - Never
update-refbefore proving the tree moved: assertwrite-tree != rev-parse HEAD^{tree}ANDgit diff-tree -rcount is in the expected order of magnitude. A surgical-commit script without these two asserts will eventually ship an empty commit with a confident message. - Same family as
cm ciabove and AudioImporter.threeD: trust a READ-BACK, never the write's exit code.
Plastic: cm ci with no paths silently skips "Changed" files (HARD)
Plastic separates Changed (modified on disk) from Checked-out and Added. cm ci with no
path argument commits the Added + Checked-out set and leaves plain Changed files behind, while
still printing a cheerful Created changeset cs:N. The per-file list it prints is the truth; the
changeset line is not.
Measured (a tic-tac-toe game): a checkin reported cs:184 created, and two .cs files carrying the
entire slice were still pending afterwards. Pushing that to another machine would have shipped the
docs without the code.
- Commit by explicit path when files are merely Changed:
cm ci "<path>" "<path>" -c "..."(Plastic auto-checkouts them). - Always re-run
cm statusafter a checkin and require it to be empty. Never report "committed" off theCreated changesetline alone — diff the printed per-file list against what you intended. - Same shape as any partial-write trap: the tool succeeded, just not at what you asked.
Cost discipline (pairs with context-efficiency.md)
- One refresh → wait → one error fetch; not thrashing.
- Don’t re-fetch full console with stack traces by default.
- Don’t ask the user to “confirm it compiles” after a clean
unity_get_compilation_errors.
Non-Unity / no Editor
- Prefer the project’s normal CLI (
dotnet build, solution build, package test) when Unity is not in play. - If neither Unity MCP nor a CLI build is available, say so once and list the exact command the user would run — still try MCP/CLI first.
Exceptions (only)
- Unity Editor not open / MCP bridge down / wrong instance and you cannot connect after
unity_list_instancesand force-focus (above) failed to bring it back. A merely unfocused Editor is NOT an exception — focus it yourself. - Compile blocked by a known external state the agent must not fix alone (e.g. L0-owned Latios parked, license, network package resolve) — still run the check, paste the blocker from tools, and stop.
Anti-patterns
- “Compile in the Editor and tell me what errors you get.”
- Shipping a code slice as done without a tool-backed compile read when Editor is available.
- Ignoring
unity_get_compilation_errorsand relying only on “files look right.” - Asking the user to click/focus the Unity window — compile is not a focus job; keep it ticking and request Refresh+recompile. Report blocked only if the editor is unreachable.
- Treating
unity_editor_state isCompiling: falseorrecompileup_to_dateas a compile you requested, when you never Refresh'd. -
Treating
AppActivatereturningTrue, or a domain-reload transport error, as proof of anything. -
PORT-CLOSURE-FIRST (HARD) — a bulk file port lands with its complete reference closure in ONE batch. Before writing ported files into a live Unity project: grep the sources for type refs, statics, and
[UpdateAfter(typeof(...))]targets; diff against the destination; port or API-identical-stub every missing symbol in the same drop (netcode usings stripped, inline[GhostField]attrs stripped — they share the field's line). Iterating console errors instead costs ~90 s per pass with a broken project on the developer's screen. And never gate waits on ScriptAssemblies dll mtimes (compile can finish before the marker is taken → infinite loop that looks like a hung agent) — poll the compilation-pipeline error API instead.
EXECUTEMENUITEM-SILENTLY-NOOPS — verify with the CLI, not with a menu item
EditorApplication.ExecuteMenuItem(...) returns true while doing nothing on an unfocused
editor, and for any item whose handler defers to an interactive importer. Observed twice in one
session on 6000.5.0f1: Window → TextMeshPro → Import TMP Essential Resources returned true and
imported no files, and a [MenuItem] self-test returned true and logged nothing. Its return value is
worth exactly as much as AppActivate's — see AGENT-COMPILE-FORCE-FOCUS above.
For anything you intend to VERIFY, drive it headlessly instead. The CLI gives a real exit code, a log you can grep, and needs no focus at all:
Unity.exe -projectPath <proj> -batchmode -nographics -quit \
-executeMethod Namespace.Class.StaticMethod -logFile <log>
Grep the log for the method's own output, not just the exit code — a batchmode run exits 0 whether your method logged 92 passes or was never reached. Confirm the invocation itself appears in the log.
The GUI editor holds the project lock, so close it first (EditorApplication.Exit(0) over the
bridge — the in-flight MCP call will fail with a transport error, which is the process dying, not a
failure) and relaunch after. Prefer this shape for any assertion you plan to report as evidence;
keep the focused-GUI path for visual work, where you need frames flowing anyway.
EDITOR-MODAL-FROM-INSIDE-A-BUILD-DEADLOCKS — and it looks exactly like a crashed bridge
A vendor hook that opens EditorUtility.DisplayDialog from inside BuildPipeline.BuildPlayer
deadlocks the editor: the main thread is blocked in the build, so the dialog it raised cannot pump
its own input. Clicking the dialog does nothing — the thread that would process the click is the
one waiting on it. Measured symptom set, all at once:
- the MCP/automation bridge stops answering while its TCP port still listens (it runs on the editor update loop, which no longer ticks),
- the editor log stops dead — 20 minutes of no output is the tell,
- the Gradle
javaprocess sits idle at low CPU, - a "Building Player (busy for NN:NN)" progress window that never advances.
The bridge going quiet reads as "Unity crashed". It has not. Enumerate the process's visible
windows before concluding anything — EnumWindows filtered by PID names the modal outright:
45980 | Building Player (busy for 16:51)...
45980 | Enable Android Auto-resolution? <-- the deadlock
Fix the cause in settings so the prompt cannot appear, and run any build you intend to REPORT through
-batchmode -executeMethod (EXECUTEMENUITEM-SILENTLY-NOOPS), where no dialog can block. Dismissing
the modal is not a fix: a vendor that raises it once raises it every build, and dismissing without
answering leaves the underlying setting unchanged.
Second known trigger, Unity's own (a mobile card game): "Can not sign the application" — an
Android build with a custom keystore configured and no password in the editor session. The password
is session state, not project state, so a fresh editor + a scripted build reliably hits it, and the
log's last line is the Android SDK/NDK banner, which reads as "build in progress". Gate on
PlayerSettings.Android.keystorePass / keyaliasPass being non-empty and abort if not. Do not
flip useCustomKeystore off as a workaround — the build pipeline saves PlayerSettings mid-build, so
the flip lands in ProjectSettings.asset before your finally restores it. Ask the developer to type
the password into Publishing Settings; never handle it yourself.
Amendment (v286, EDITOR-MODAL-LOOKS-LIKE-A-HANG): the same shape outside a build is NOT a deadlock and must
not be killed. one shipped project: after a Play stop the bridge timed out, the editor log froze 14 min, CPU
sat at 0 and the registry heartbeat went stale - every symptom of the documented kill + lockfile + relaunch hang -
but Process.Responding was True and EnumWindows by pid listed a native modal ("Save changes made in Play
Mode", Cinemachine SaveDuringPlay, buttons Keep / Don't Keep). One click resumed the editor; the kill would have
taken three lanes down. Before any relaunch recipe: enumerate the process's visible top-level windows and read
Responding; a second window = a modal, answer it (play-mode changes: Don't Keep), never kill.
SYNTHESIZED-CLICKS-MISS-UNDER-DPI-SCALING — capture and click must share DPI awareness
SetCursorPos and mouse_event take physical pixels, but a DPI-unaware process has its
coordinates rescaled by Windows, and Screen.PrimaryScreen.Bounds reports logical pixels there.
On a 3440x1440 display at 125%, a screenshot came back 2752x1152 and every coordinate read off it was
1.25x short — the clicks landed in empty space, and "the dialog is still up" read as "the click was
rejected" rather than "the click missed".
Call SetProcessDPIAware once, in the same process that both captures and clicks, then check
the numbers agree: GetSystemMetrics(0/1) against the captured bitmap's size. If they differ, every
coordinate is scaled and nothing you click is where you think. Verify a click by re-capturing and
looking, never by whether the target window is still present — a window that re-raises itself is
indistinguishable from one that ignored you.
- BOOTSTRAP-ENTRY-APPLY-PLUS-RUN. Every bootstrap / setup entry point is two methods:
Apply(a MenuItem, no exit) for the editor, andRun(Apply thenEditorApplication.Exit) for-executeMethod. One method that exits kills the interactive editor; one that does not hangs batchmode. (a card game.)
Synthetic pointer input leaves device state behind (a tower-defense game)
- A queued mouse press with a dropped release sticks the editor Mouse device on
pressed, across play sessions.InputSystem.QueueStateEvent(Mouse.current, down)and anupthat never processes (unfocused editor resets, the developer clicking meanwhile) leaveleftButton.isPressed == true; every real press after that is "no change", so no UITKClickable/uGUI Button completes and the developer reports "nothing is clickable" — whilePointerDownstill logs on every button. -
Do not inject pointer state into an editor the developer is using. If a synthetic click is unavoidable, finish with
InputSystem.ResetDevice(Mouse.current)and assertMouse.current.leftButton.isPressed == falsebefore the turn ends. Prove buttons withEventSystem.RaycastAllat the centre +panel.Pick+ aclicked/ClickEventlog of the developer's OWN click instead. -
DEVICE-SIMULATOR-SPLITS-SCREEN-PARAMS — a docked Device Simulator next to the Game view breaks every runtime click in the editor. It installs a "Device Simulator Touchscreen" device and its screen params leak into the play view:
Screen.currentResolutionread the simulated device,Screen.safeAreasat outside the Game view, andScreen.heightflipped between two values on consecutive calls, so UI Toolkit scaled the panel from one height and the pointer from the other. Close the window (Resources.FindObjectsOfTypeAll(Type.GetType("UnityEditor.DeviceSimulation. SimulatorWindow, UnityEditor.DeviceSimulatorModule"))), or guard it closed from[InitializeOnLoad]; test aspect ratios with Game view presets. Also: a synthetic click cannot prove the UITK path while the editor app is unfocused —InputSystemUIInputModuledrops events whenEventSystem.isFocusedis false — so the developer's real press is the only click evidence. (a tower-defense game, third "nothing is clickable" report.) -
DOMAIN-RELOAD-HANGS-AFTER-BULK-ASSET-OPS — a domain reload right after a big asset batch can deadlock at
Reloading Domain (busy for …)with zero CPU; nothing recovers it. Seen three times in one session (after 86 generated skin prefabs, after 71 deletions, and after a two-file edit) — every time an MCP bridge request was in flight whenOnBeforeAssemblyReloadstopped the bridge server, so treat the reload window as radio silence: nounity_*call or bridge ping for 60 s after Refresh / Play / Stop, then poll. Recipe: after ~5 minutes at zero CPUStop-Process -Force, moveTemp/UnityLockfileaside, relaunchUnity.exe -projectPath, wait for the bridge,unity_select_instanceagain, and re-pinEditorSceneManager.playModeStartScene(session state, lost with the process). Pre-warm entity-scene artifacts before Play (EntityScenesPaths.GetSubSceneArtifactHash(sceneGuid, config, true, ImportMode.Synchronous)via reflection for the client GUID and the default GUID) and batch asset writes so only one reload follows them. (a tower-defense game.) Zero CPU in the PARENT is not the signal - measure the CHILDREN before you kill anything (a space sandbox game). A Unity player build farms shader work toUnityShaderCompilerchild processes, soUnity.exesits at ~1 s of CPU per wall-minute while the title readsCompiling compute shader (busy for 13:18)...- identical on every surface to the deadlock above. Measured on a healthy build: parent 9799 -> 9800 s CPU in a minute (looks wedged), while the children moved 4415 -> 4544 s across 25 -> 29 processes in 45 s, about three cores busy. Applying the five-minute kill recipe there destroys a live 15-minute build. Before declaring a hang, sum CPU across the child processes (UnityShaderCompiler,Bee, asset import workers) over a 30-60 s window; a rising sum means working, a flat sum across parent AND children means wedged. The same care applies to the log: a single long operation buffers, so a staleEditor.logmtime is not evidence of death either.
AUTO-REFRESH-OFF-SERVES-YOU-THE-PRE-EDIT-PROJECT (HARD)
a space sandbox game. Unity's Preferences > Asset Pipeline > Auto Refresh can be off
(EditorPrefs.GetInt("kAutoRefresh") returns 0). With it off, a .cs / .asmdef write under
Assets/ is not imported - not on write, and not when the Editor regains focus. The
AssetDatabase keeps serving the OLD project, and every signal an agent normally trusts agrees with
it: EditorApplication.isCompiling is false, the compilation-errors query returns count: 0, and
the console is clean. That reads exactly like "my edit compiled green". It is the pre-edit project
answering.
Measured: five files git mvd into a new assembly plus a new .asmdef. A refresh scheduled with
EditorApplication.delayCall += => AssetDatabase.Refresh did not take;
AssetDatabase.LoadAssetAtPath still resolved the OLD paths and did not resolve the new .asmdef,
three minutes later, while the error query said zero.
Do this. Call AssetDatabase.Refresh directly (a handful of files is not the mass reimport
that BUILD-AND-TEST-RUNS-ARE-NOT-UPDATE-TICK-WORK in the Live play testing via Unity MCP page warns about), then prove
the database moved before you read any compile result:
AssetDatabase.Refresh(ImportAssetOptions.Default);
// new path resolves AND old path is gone - either alone can pass on a stale database
AssetDatabase.LoadAssetAtPath<Object>(newPath) != null
AssetDatabase.LoadAssetAtPath<Object>(oldPath) == null
Then verify the code by TYPE LOAD, not by file mtime or a green error count:
typeof(TheType).Assembly.GetName.Name, or grep the built DLL under Library/ScriptAssemblies/
for a symbol that exists only after your edit.
Second-order. With Auto Refresh off, SCRIPT-WRITE-ENDS-A-PEER-PLAY-SESSION (the Live play testing via Unity MCP page) cannot fire until somebody explicitly refreshes. That is a mitigation, not a licence: the next lane to refresh detonates every queued write at once, so keep the freeze.
Same family as UNCHANGED-SAVE-STILL-REIMPORTS (the editor tooling page) and the zero-error
assembly drop in the compile assemblies page: a failure that presents as its own success.
Check the setting once per session before you trust a compile - it is one EditorPrefs read.
EditorPrefs are per-USER, not per-project (HKCU\Software\Unity Technologies\Unity Editor 5.x).
Another project's Auto Refresh setting is already yours. Measured: kAutoRefresh = 0 on a
machine where a DIFFERENT project had turned it off, silently applying to a second project that never
touched the setting - HotReloadWindow.* keys sitting beside it is how it was spotted. Read the pref
before assuming your project set it. Same shared-per-user-store shape as PER-USER-EDITOR-LOG-IS-SHARED
in the Live play testing via Unity MCP page.
Corrected v283 - those keys are a LIVE HOLDER, not a leftover. v277 shipped this as "keys present
from a plugin not installed in that project"; a direct registry read of
HKCU\Software\Unity Technologies\Unity Editor 5.x says otherwise:
kAutoRefresh_h2404942332 REG_DWORD 0x0
kAutoRefreshMode_h2874646975 REG_DWORD 0x2
HotReloadWindow.ShouldDoAutoRefreshFixup REG_DWORD 0x0
HotReloadWindow.AllowDisableUnityAutoRefresh REG_DWORD 0x1
The Hot Reload tool holds Auto Refresh off BY DESIGN. Consequence: kAutoRefresh = 1 "fixes" nothing
- the tool fights it and the setting silently reverts, so an agent that repairs the pref and then
trusts an automatic import is back to reading the pre-edit project. The explicit-Refresh path
(ForceCompile, then prove the AssetDatabase moved, then verify by type load) is the PERMANENT shape
here, not a stopgap. Read the keys, not just kAutoRefresh: a holder key beside it tells you whether
the setting is yours to change at all. Measured one shipped project lane B.
CALL-THE-OFFLINE-IL-FROM-A-CONSOLE - verify pure-math runtime code with no Editor
An offline compile that writes real assemblies can be referenced by a throwaway dotnet console project
that CALLS the shipped methods: solvers, rule structs, scoring, with no lease and no Editor.
- Reference the lane's freshly compiled assemblies,
Unity.Mathematics.dll,Unity.Collections.dll, and the editor'sUnityEngine.CoreModule.dllplusUnityEngine.MathematicsModule.dll(float3and friends are type-forwarded there;Unity.Mathematics.dllalone does not resolve them). - Never reference
Library/ScriptAssemblies/*for the code under test: that is the Editor's last build and is stale whenever the Editor has not compiled your branch. - Symptom: a before/after A/B prints the same numbers twice. Cause: the rebuild kept the referenced DLL it had already copied, so the "after" run tested the old code. Fix: give each variant its own output folder with its own copy of the assembly, and print the assembly's hash in the output.
- Symptom:
SecurityException: ECall methods must be packaged into a system modulefrom a Unity struct'sToString/ToFixedString(a NetCode tick). Cause: it reaches an engine internal call that exists only inside Unity. Fix: print the raw fields (a NetCode tick's index isSerializedData >> 1). - (HARNESS-RUNS-UNDER-PWSH) Symptom: a reflection harness over the offline IL (an
AssemblyResolvescriptblock over the offline and ScriptAssemblies folders) died withStackOverflowExceptionunderpowershell.exe5.1. Cause: not isolated; the same script ran clean underpwsh7. Fix: run harness scripts withpwsh, neverpowershell.exe. - (HARNESS-RESOLVES-ENGINE-MODULES) Symptom: "Could not load file or assembly UnityEngine.AudioModule"
when a test touched a MonoBehaviour class, while a plain struct in the same DLL ran. Cause: loading the class
resolves every engine module its members reference, and the console does not probe the editor's folders. Fix: an
AssemblyResolvehandler over the editor'sData/ManagedandData/Managed/UnityEnginefolders plus the offline output folder; reference only the modules you call.
TIME-BOTH-SIDES-AT-ONE-OPTIMISATION-LEVEL - offline IL is debug; time the source compiled Release
The Editor compiles scripts in Debug code optimisation by default, and an offline compile that replays its
response file carries no -optimize, so those assemblies are debug-level IL. A harness that times that IL
against a copy of the old code compiled Release inside the harness is comparing two optimisation levels.
- For timing, compile the source file itself into a Release harness (
<Compile Include="...">with<Optimize>true</Optimize>) and put both variants there. Keep the offline-IL reference for correctness only. - Drop the first round: tier-0 JIT ran both variants at 1.7 us per call in round 0, against 1.3 and 0.8 in the steady rounds.
- One level makes the comparison fair, not the gap smaller: in the case measured the gap held (1.3 vs 0.8 us per call; inline copies of the new code in the harness ran 0.9). Report the steady numbers of both sides and the allocation per call.
GIT-SHOW-IN-POWERSHELL-EATS-THE-BOM - a captured blob is text, not bytes
An offline compile that filled a sparse worktree's missing sources with git show HEAD:<file> captured in
PowerShell failed the Editor assembly with CS1056 '' (and CS1040) at column 1 of every copied file.
PowerShell decodes a native command's stdout with the console (OEM) code page, so the UTF-8 BOM EF BB BF came
back as three characters and was written out as text.
- Write a blob as bytes:
git show HEAD:<file> > outundercmd /c,git cat-file blob <sha>redirected by a byte-safe shell, or set[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)before capturing. - Until the script is fixed, a failure made only of CS1056/CS1040 at (1,1-4) on copied files is this defect, not the branch under test.
POLL-MATCHES-THE-KEY-AND-ITS-VALUE - a bare word in a JSON poll matches its own false
A compile poll read a finished compile as failed: grep failed matched the key in "failed":false of the bridge's
recompile status.
- Match the key with its value (
"status":"completed", then"failed":true), never a bare word; the same holds forerror,successand every other boolean key in a status poll.
DOTNET-PATH-IGNORES-SET-LOCATION - a relative path in a .NET call resolves against the process
PowerShell [IO.File]::ReadAllText('Assets/...') threw FileNotFound right after cd into a worktree: .NET resolves
relative paths against the process working directory, which Set-Location/cd does not change.
- Give .NET APIs absolute paths (
(Resolve-Path x).Path, orJoin-Path $PWD x), or edit with the file tool.
RENAME-COMPILES-THE-EDITOR-CALLERS - a runtime-only compile passes a rename that breaks tools
Renaming a replicated field passed every runtime assembly in an offline compile and failed only the Editor assembly (CS0117) on a preview tool that still set the old name. The code graph had no cross-file call edges, and a runtime compile never sees Editor or test callers.
- Grep the old member across runtime, Editor and test folders before calling a rename done.
- Compile the Editor and test assemblies too, not only the runtime ones.