Scrubbed extract of
Spatial-Cognition-Platform/ADR/ADR-SCP-007-Executor-Tiers-And-Verification-Ladder.md— cited by the Chronicle (07-the-long-march). Redactions: phone / service detail / secrets / clearance / personal / internal hosts.
type: project project: Spatial-Cognition-Platform status: accepted created: 2026-08-27 updated: 2026-09-22 amended: 2026-08-28 tags:
- adr
- agents
- dispatch
- verification
ADR-SCP-007: Executor tiers and the verification ladder
Status: Accepted
Date: 2026-08-27 · Amended: 2026-08-28 (verification classes; scheduling rule; R2 provisionality; R2 via Editor MCP server) · 2026-09-17 (R3 via Quest Link) · 2026-09-21 (R2 in CI on unity-k8s; compact evidence — ADR-SCP-015) · 2026-09-22 (false-premise annotation, quoting the sentences ADR-SCP-015 §1/A1 corrects)
Deciders: Solo (Cory)
Context Project: [[Spatial-Cognition-Platform]]
[!note] Amended 2026-09-21 by [[ADR-SCP-015-R2-In-CI-And-Compact-Evidence|ADR-SCP-015]] The premise “CI has no Unity licence” is no longer true: a repository-scoped, licensed Unity runner exists in the K3s fleet (IP-CICD-36, label
unity-k8s). R2-batchmode now runs there on every pull request through.gitea/workflows/unity-r2.yml, using the same harness and invariants. The sentence below that R2-batchmode “is the only R2 form CI can use — CI is headless with no Editor” stays correct for R2-MCP only. ADR-SCP-015 also replaces committed raw NUnit XML with a per-test summary as the committed evidence form, and binds the runner’s editor version toProjectVersion.txt. The tier table, R3 and R4 are untouched.
Context
The POC is built by a mixed workforce: a local qwen3.8 (RTX 5090, via llm-proxy) running around the clock, Claude agents for a minimal set of complex items, and Cory (+ Claude interactively) for editor/device work. Unity Editor CLI (batchmode) is available to the local agent box. Work must be cut so each executor only receives tasks it can verify — an agent that cannot run its own acceptance check produces review debt, not progress.
Decision
Executor tiers (IP frontmatter model: + tags)
| Tier | Executor | IP marking | Owns | Constraint |
|---|---|---|---|---|
| A | qwen3.8 local | model: qwen |
Default executor. Sized IPs 1–2 pts in engine-free core, contracts, exporter, fixtures; Unity-side code changes explicitly flagged batchmode-verifiable | Must be verifiable at rung 1, or rung 2 when the IP says so |
| B | Claude agent | model: claude |
Complex/cross-cutting work only, kept to a minimum: Meta SDK integration seams, IL2CPP/serialization traps, composition root | Every tier-B IP states why tier A was insufficient |
| C | Operator (Cory + Claude interactive) | model: null + tag operator |
Unity scene authoring, Building Blocks wiring, device builds/deploys, simulator sessions, demo capture | Scheduled work — lanes must never block on it for >1 day |
Verification ladder (every IP names its highest required rung in DoD)
-
R1 —
dotnet teston the engine-free solution + schema validation of fixtures. Headless, seconds, no Unity. Mandatory for any IP touchingcore/orcontracts/. -
R2 — Unity EditMode verification: either
-batchmode -runTests(EditMode; PlayMode where scriptable), or the same checks driven against a resident Editor through the AnkleBreaker MCP server (unity_testing_run_tests,unity_component_get_properties,unity_console_log,unity_get_compilation_errors). Available to tier A; required for Unity-side asmdef code an agent touches. See the “R2 via the Editor MCP server” amendment for which form to use when.[!warning] Never pass
-quitalongside-runTests— amended 2026-09-07 (S14 pass H01) Do not pass-quitalongside-runTests: the editor exits before the test runner is invoked, so the run ends at exit code 0 with zero tests and no results XML — a green-looking run that proved nothing. The runner drops the editor itself when the suite completes.This is a documentation correction, not a change to the ladder’s meaning; the tier table, R3 and R4 are untouched. The clause previously prescribed
+ -quit, and the failure is not hypothetical:tools/validate_editmode_evidence.pyrecords it as an observed project failure, including a handoff note claiming104/104for a 148-test suite. Treat any R2 result with zero tests or a missing results XML as a refusal, never as a pass. -
R3 — interactive session in a real XR runtime. Two forms; an IP’s DoD names which. R3-sim — Meta XR Simulator (standalone app): interaction smoke tests, passthrough composition sanity, headset-free and agent-drivable. R3-link — Unity Editor play mode streamed to the Quest 3 over Meta Quest Link: real optics, real head and hand tracking, real angular size. Human in the headset. Tier B/C. See the “R3 via Quest Link” amendment.
-
R4 — Quest 3 device: performance (72 Hz floor), passthrough legibility, hand interaction feel, IL2CPP round-trip. Tier C; gates slice completion, per the tracker. The 72 Hz floor and the IL2CPP round-trip cannot be substituted by R3-link — see that amendment’s cannot-prove table.
Verification classes and the scheduling rule (amendment, 2026-08-28)
The tier ladder above says who executes an IP. It does not say when an IP can run. Two questions decide that, and nothing else gates dispatch:
- Does this IP need Unity to run?
- Is the work it depends on complete?
VRAM, model choice, and lane width are operator-side concerns set at execution time. They are not IP properties and must not appear in scheduling documents.
Question 1 is answered by the IP’s verification class, recorded in frontmatter as verification_class, verification_rung and needs_unity:
| Class | Correctness established by | Needs Unity |
|---|---|---|
| B1 | R1 — dotnet test, schema validation, artefact read-back |
No |
| B2 | R2 — Unity batchmode / EditMode | Yes |
| O | R3 simulator / R4 device — a human or capture pipeline looking at output | Yes |
One IP, one class. An IP that mixes classes cannot be scheduled, because it wants Unity resident and fast local inference simultaneously. B1 work must never wait on a headset; O work must never hide inside a green CI run. Decompose on this boundary per verifiability-decomposition, which is the sizing rule that sits beside architectural decomposition.
The classes remain three-way rather than a simple Unity/not-Unity split because they size differently: an O IP is sized by its number of observations, never by deliverable count, and must name in its Definition of Done the literal artefact that constitutes proof. If a DoD can be satisfied by reading a log, the IP is misclassified.
Rung R2 was provisional; as of 2026-08-28 it is real via the Editor MCP server. A rung is only real if it can actually run. Batchmode Unity in this project exits 1 from a shutdown timeout after completing and saving, and configuration writes are silently clobbered by asynchronous Building Block installs — so batchmode R2 still gates on a re-read result artefact, never on an exit code. The Editor MCP server ([[adopt-a-unity-editor-mcp-server-for-the-r2-r3-loop]]) landed and was verified against the passthrough scene the same day: it returns structured EditMode results synchronously and supports write-then-immediate-read-back, which removes both the exit-code and the silent-clobber problems for the MCP form. See the amendment below.
Evidence for this amendment. The 2026-08-28 wave lost four of five lanes because Unity was consuming GPU VRAM while local lanes ran: the Editor took the 5090 to ~96% of 32.6 GiB, evicting the dense model’s KV cache to ~19 tok/s until requests could no longer finish inside llm-proxy’s 120 s window. The original text framed lane-core as running “around-the-clock … without headset or human in the loop” concurrently with lane-unity. On a single-GPU host that is not a scheduling property to be tuned — it is a decomposition failure, because the IPs in flight contained both classes at once. Nine such IPs were found and split the same day.
R2 via the Editor MCP server (amendment, 2026-08-28)
AnkleBreaker Studio’s Unity MCP (com.anklebreaker.unity-mcp v2.39.5, bridge on 127.0.0.1:7890) was adopted and verified on-device by Liam against Unity 6000.5.10f1 and the live passthrough scene (Assets/Scenes/SpatialCognition.unity). Full record: [[adopt-a-unity-editor-mcp-server-for-the-r2-r3-loop]].
What R2 now means. R2 has two forms, and an IP’s Definition of Done should name which:
- R2-batchmode — unchanged. Still exits
1on a shutdown-timeout after saving; still gates on a re-read artefact, never an exit code. This is the only R2 form CI can use — CI is headless with no Editor, sotools/verify-scene.pystays as the permanent CI-side scene gate. - R2-MCP — checks driven against a resident Editor.
unity_testing_run_tests(EditMode) returns a structured{passed, failed, skipped}job result synchronously;unity_get_compilation_errorsand typedunity_console_logentries replace log-grepping;unity_component_get_propertiesreads live component state, and a write followed immediately by a read-back makes async Building-Block clobbering observable instead of silent. All four of this ADR’s rung-2-relevant acceptance checks passed against the real scene.
What shifts toward tier A. The verification half of class-B2 work. Previously an agent could write Unity-side asmdef code but could not confirm it without the operator opening the Editor and looking; the “agent writes, operator checks” split was the tier-C tax on B2. With R2-MCP an agent runs the EditMode suite and inspects the resulting scene/component state itself, so a B2 IP whose Unity-side work is code the agent authored is now genuinely agent-verifiable end to end. The specific ScpPassthroughFixup-style check — “read the config value back after the async install lands” — moves from an out-of-process file re-read (or an operator eyeball) to an in-session MCP read.
What does not move. Scene authoring, Building-Blocks wiring, device builds/deploys, simulator sessions and demo capture stay tier C / class O. R2-MCP reads and pokes an existing scene; it does not supply authoring judgment, and it does not touch R3/R4. The console app-side-vs-system-side distinction it provides is by message-prefix convention, not a structured source field — the on-device app-PID-vs-system-PID confusion that caused two false positives is an R4 concern and is unaffected.
New scheduling constraint. R2-MCP needs Unity resident for the whole check, not just a batchmode spin-up-and-quit. On the single-GPU host that is continuous VRAM occupancy, so an R2-MCP session is itself class-B2-exclusive under the scheduling rule above: it must not overlap B1 local-inference lanes. A batchmode R2 run at least ends; an MCP R2 session holds the GPU until the agent closes it.
R3 via Quest Link (amendment, 2026-09-17)
Context. Getting one scene-layout change in front of a wearer cost an R2 pass, a scene rebuild, a PR, CI, a merge, a ~20-minute IL2CPP build, an install and a launch — and then failed for a reason that was neither code nor gate (the room anchored to a desk at 14.6 cm because the app places content in front of the head at boot). None of that pipeline buys anything for layout, legibility, scale, reachability and interaction feel, which need a real headset on a real head and nothing else. Quest Link runs the app from the Editor and presents it in the headset; the loop becomes seconds.
What R3 now means. R3 has two forms, and an IP’s Definition of Done should name which:
- R3-sim — unchanged. Meta XR Simulator, headset-free, capture-based, agent-drivable without a human. Stays the rung for unattended regression and evidence capture.
- R3-link — Unity Editor play mode over Meta Quest Link, driven by
tools/run-link-play.ps1. A human wears the headset. This is the inner loop for anything judged by eye.
Both are kept. They answer different questions and neither replaces the other.
What R3-link proves. Real optics and stereo scale · real head tracking · real hand tracking · reachability and ergonomics · text legibility at true angular size · layout, spacing and occlusion · passthrough composition sanity · interaction feel (plus Link latency).
What R3-link CANNOT prove — these stay R4, without exception:
| Cannot prove | Why |
|---|---|
| IL2CPP round-trip | Editor play mode is Mono on a PC. AOT generic failures, async void differences and NDK/clang build breaks are all invisible. One such break left main unbuildable for three days in 2026-09. |
| 72 Hz floor / frame budget | PC GPU plus Link encode/decode. [SCP-PERF] figures gathered over Link are not device evidence and must never be recorded as such. |
| Fixed foveated rendering | ScpXrRigEntryPoint guards FFR behind #if UNITY_ANDROID && !UNITY_EDITOR; there is no foveation hardware in the loop. |
| On-device anchors / entitlement | OVRSpatialAnchor behaviour and the debug-signing limitation are device-only. |
| Thermal / sustained performance | No Quest SoC involved. |
“R3-link green” therefore never means “ready to ship.” It means the thing is worth building to a device.
Scheduling constraint, exactly parallel to R2-MCP’s: R3-link holds the Editor and its VRAM resident for the whole session, so it is class-B2-exclusive on the single-GPU host and must not overlap B1 local-inference lanes.
Three mutual exclusions, each of which presents as “the app rendered nothing” and is therefore indistinguishable from a real bug until named:
- An active Link session holds the XR runtime, so a sideloaded native app on the headset renders only black until Link disconnects. Already recorded under README “Deploy gotchas”; it now applies to the default loop.
- The simulator and Link cannot both be active. The simulator is activated by three
process-scope environment variables and by the machine-level OpenXR
ActiveRuntimekey; either one pointing at the simulator silently prevents Link from binding. - An open Editor holds the project lock, so batchmode R2 (
tools/run-editmode-r2.ps1) cannot run until it is closed.
tools/run-link-play.ps1 fails closed on all three with a named reason rather than launching
into a black headset, and tools/test-link-preflight.ps1 is its inverted proof — a preflight
that only ever passes is not a gate.
False-premise annotation (amendment, 2026-09-22 — correcting authority: ADR-SCP-015 §1 and its Amendment A1)
The 2026-09-21 callout at the head of this ADR records that the R2-local premise is dead. This annotation pins which sentences were false, so a reader who lands mid-document cannot re-derive the dead premise from the original text, which stands unmodified above per the vault’s append-and-annotate rule.
[!warning] Premise 1 — false as of 2026-09-21 Original text, “R2 via the Editor MCP server” amendment, R2-batchmode bullet (line ~82 above): “R2-batchmode — unchanged. Still exits
1on a shutdown-timeout after saving; still gates on a re-read artefact, never an exit code. This is the only R2 form CI can use — CI is headless with no Editor, sotools/verify-scene.pystays as the permanent CI-side scene gate.”The bolded clause is false in both halves. CI does run R2-batchmode:
.gitea/workflows/unity-r2.ymlexecutestools/run-editmode-r2.ps1on the licensedunity-k8srunner on every pull request ([[ADR-SCP-015-R2-In-CI-And-Compact-Evidence|ADR-SCP-015]] §1; runner live per ADR-SCP-015 A1 — DockerContainers PR 95, homelab-stacks PR 410, merged 2026-09-21). “CI is headless with no Editor” survives only as a statement about R2-MCP: no resident interactive Editor exists in CI, so the MCP form remains local-only. Theverify-scene.pyconsequence is unaffected — static scene gates still run inci.yml; the R2 lane does not replace them.The R2-batchmode harness semantics quoted in the same sentence (exit-1-after-save, gate on a re-read artefact, never on an exit code) remain true everywhere the harness runs, desktop or CI — ADR-SCP-015 A6 keeps the harness invariants unchanged.
[!warning] Premise 2 — false as of 2026-09-21 The Context section’s framing that Unity verification is local-only because CI has no Unity capability — echoed verbatim in
spatial-cognition-xr/.gitea/workflows/ci.yml(“CI has no Unity licence”) and intools/validate_health_report.py(“ADR-SCP-007 keeps rung 2 local: CI cannot run Unity”). ADR-SCP-015 §1 is the correcting authority: a repository-scoped Personal-entitlement runner exists, its editor version is bound toProjectVersion.txt(ADR-SCP-015 §2), and IP-CICD-36’s “the project does NOT change its editor version” decision is superseded. Repo-side stale copies of this sentence are inventoried for correction in [[../Reports/CI-Pipeline-Cleanup-Audit-2026-09-22|CI-Pipeline-Cleanup-Audit-2026-09-22]] (P11, P12).
What this annotation does not change: the executor-tier table, the verification-class/scheduling
rule, R3 (both forms), R4, and the cannot-prove table all stand exactly as written. The required-status
flip for the R2 lane has not happened (ADR-SCP-015 A1’s record rows are unfilled as of 2026-09-22);
until it does, ci.yml’s presence gate stands in for the lane, and no IP may cite an unity-r2.yml
run as satisfying its DoD and skip the committed-evidence rules still in force.
- All dispatchable IPs use [[Sized-IP-Template]] and score 1–3 pts on [[IP-Sizing-Rubric]]; anything larger is decomposed into
IP-SCP-N.Msub-IPs before dispatch, not during execution. - Sub-IP decomposition is performed by planning subagents against the epic README’s decomposition brief; the architecture (this ADR set) is input to decomposition, never re-decided by it. Decomposition remains carved out of local-model routing (
wave-orchestration§1.1) — it is orchestrator or frontier work. - An IP is cut on the verification-class boundary before it is cut on anything else. A mixed-class IP is not dispatchable at any size, however well it scores on the sizing rubric.
- Each sub-IP re-derives its own precedence from its own deliverables rather than inheriting the parent’s. A build-provable half typically depends on less than its parent did; inheriting the parent’s edges re-creates the blockage the split removed.
parallel_grouplanes:lane-core(tier A),lane-unity(tier B/C),lane-content(overlay/inventory authoring). Precedence edges live in frontmatterprecedence:, nowhere else.- Vault status machine per
AGENTS.md:backlog → in-progress (claim) → review → done (human only).
Consequences
Positive
- Around-the-clock throughput on the core without headset or human in the loop, provided the IP is class B1 — the ADR-SCP-002 seam is what makes that set large. This is a property of correctly-cut IPs, not of the ladder: see the amendment above.
- Rung numbering gives DoD lines teeth (“R2 green” is unambiguous).
- Tier B stays auditable and minimal by requiring justification.
- R2-MCP (2026-08-28 amendment) closes the “agent writes Unity code, operator verifies it” gap for class-B2 IPs, taking that verification load off the single human.
Negative / Risks
- Batchmode PlayMode tests are flaky under XR packages; rung 2 defaults to EditMode + compile/build, with PlayMode-in-simulator promoted to rung 3.
- Tier C is a single human; the tracker’s day gates exist to surface operator-blocking early.
- Class O work serialises on the operator and must be counted that way when partitioning parallel streams. Batch observations so Unity comes up once per batch of looks, not once per deliverable.
- R2-MCP holds the Editor (and its VRAM) resident for the whole check, which is class-B2-exclusive on the single-GPU host — it must not overlap B1 local-inference lanes. See the R2-MCP amendment’s scheduling constraint.
Alternatives Considered
Alternative 1 — Single-executor (Claude or qwen) for everything
Rejected because: wastes the 24/7 local capacity (Claude) or exceeds reliable capability on integration seams (qwen); the tiering is the cost-quality frontier.
Alternative 2 — Agents verify only by code review (no runnable rungs)
Rejected because: review-only verification is how plausible-but-wrong code lands; every tier-A IP must be machine-checkable by construction.
Related
- [[ADR-SCP-002-Engine-As-Adapter-Unity-Client]] — the seam that defines tier A’s surface
- [[ADR-SCP-008-POC-Scope-Freeze]] — what the workforce is pointed at
- [[adopt-a-unity-editor-mcp-server-for-the-r2-r3-loop]] — the R2-MCP adoption and its per-criterion verification record