diff --git a/Makefile b/Makefile
index e5ba3ff0eee..0333c4375fd 100644
--- a/Makefile
+++ b/Makefile
@@ -98,7 +98,7 @@ include fmt.mk
.PHONY: vscode-ext vscode-ext-install
.PHONY: docs-server check-docs-links
.PHONY: storybook storybook-run storybook-build storybook-flake-check test-storybook storybook-budget
-.PHONY: benchmark-terminal bench bench-compare first-load-js page-timing bug-bash mcp-apps-e2e
+.PHONY: benchmark-terminal bench bench-compare first-load-js page-timing first-run-view-parity bug-bash mcp-apps-e2e
.PHONY: ensure-deps mux
.PHONY: check-startup-imports check-startup-imports-runtime check-react-compiler check-test-routing check-test-seam-comments test-bench-scripts
@@ -826,6 +826,9 @@ first-load-js: ## Report first-load JS bytes of dist/ (run make build-renderer f
page-timing: node_modules/.installed ## Paired mobile UCS/IR timing of two built trees (PAGE_TIMING_ARGS; see scripts/perf/pageTiming.ts)
@node --import tsx scripts/perf/pageTiming.ts $(PAGE_TIMING_ARGS)
+first-run-view-parity: node_modules/.installed ## Static vs live first-run view boxes and screenshots, 4 widths x 2 themes (FIRST_RUN_VIEW_PARITY_ARGS; see scripts/perf/firstRunViewParity.ts)
+ @node --import tsx scripts/perf/firstRunViewParity.ts $(FIRST_RUN_VIEW_PARITY_ARGS)
+
benchmark-terminal: ## Run Terminal-Bench 2.0 with Harbor (use TB_HARBOR_PACKAGE/TB_HARBOR_DAYTONA_PACKAGE/TB_DATASET/TB_CONCURRENCY/TB_TIMEOUT/TB_ENV/TB_MODEL/TB_ARGS to customize)
@# Pin Harbor with the Daytona extra so scheduled ingestion does not break on future CLI or adapter API drift.
@# Force the Daytona SDK to the cursor-pagination API while keeping Harbor stable.
diff --git a/scripts/perf/firstRunViewParity.test.ts b/scripts/perf/firstRunViewParity.test.ts
new file mode 100644
index 00000000000..fa6f046f2ba
--- /dev/null
+++ b/scripts/perf/firstRunViewParity.test.ts
@@ -0,0 +1,30 @@
+import { expect, test } from "bun:test";
+import { Window } from "happy-dom";
+import { FIRST_RUN_VIEW_ID } from "@/common/constants/firstRunView";
+import { FIRST_RUN_VIEW_HTML } from "@/node/orpc/firstRunView";
+import { parityFailures, withFirstRunView } from "./firstRunViewParity";
+
+// Catches a hand-made test page that puts the view inside #root (React clears #root, so the view
+// would vanish before the handoff), adds it twice, or patches an HTML it cannot place it in.
+test("the test page gets the constant once, outside #root", async () => {
+ const served = '
Loading
\n';
+ const window = new Window();
+ window.document.write(withFirstRunView(served, FIRST_RUN_VIEW_HTML));
+ const views = window.document.querySelectorAll(`#${FIRST_RUN_VIEW_ID}`);
+ expect(views).toHaveLength(1);
+ expect(views[0].closest("#root") === null).toBe(true); // a compact failure, not an element dump
+ expect(window.document.getElementById("root")?.textContent).toBe("Loading");
+ expect(() => withFirstRunView("", FIRST_RUN_VIEW_HTML)).toThrow("");
+ await window.happyDOM.close();
+});
+
+// Catches a rule that checks only the position (a paragraph 3 px wider passes), uses a tolerance
+// other than 2 px, or lets a second landmark through.
+test("parity allows 2 px of drift in position and size, and exactly one main", () => {
+ const live = { x: 20, y: 300, width: 351, height: 60 };
+ expect(parityFailures({ ...live, x: 22, height: 58 }, live, 1)).toEqual([]);
+ expect(parityFailures({ ...live, y: 302.5 }, live, 1)).toEqual(["y off by 2.5 px"]);
+ expect(parityFailures({ ...live, width: 354 }, live, 1)).toEqual(["width off by 3 px"]);
+ expect(parityFailures(live, live, 2)).toEqual(["2 main landmarks in the static phase"]);
+ expect(parityFailures(live, live, 0)).toEqual(["0 main landmarks in the static phase"]);
+});
diff --git a/scripts/perf/firstRunViewParity.ts b/scripts/perf/firstRunViewParity.ts
new file mode 100644
index 00000000000..1a703909baa
--- /dev/null
+++ b/scripts/perf/firstRunViewParity.ts
@@ -0,0 +1,222 @@
+/**
+ * First-run view parity (T3 #5971, Option C step 2, plan section 5.6: PR 3b, gate 8 of PR 4b).
+ * make first-run-view-parity FIRST_RUN_VIEW_PARITY_ARGS="--tree [--out ] [--chrome ]"
+ * Serves the tree's `xum server` on a fresh empty root (the first-run state) and opens "/" in a real
+ * Chrome at 390, 744, 1200 and 1900 px, in the light and the dark system theme. Each run holds the
+ * app's main chunk, so the static view (#xum-pre) stays alone on screen, measures it, releases the
+ * chunk and measures the live wizard's first frame after the handoff.
+ * Rule: the static "OSS contributors..." paragraph is within 2 px of the live one in x, y, width and
+ * height, and the static phase has exactly one . It saves a full-page screenshot of the static
+ * phase and of the live first frame per run, for PR 4b's shell check (owner decision 10-14).
+ * Until PR 4b serves the view, the served "/" has no #xum-pre: the tool then inserts #6444's
+ * FIRST_RUN_VIEW_HTML before of the served "/" by hand (Playwright route), as a test page.
+ * Exit: 0 every run passes, 1 a run fails the rule, 2 bad input, 3 an invalid run.
+ */
+import { spawn } from "node:child_process";
+import { once } from "node:events";
+import fs from "node:fs";
+import net from "node:net";
+import os from "node:os";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import { parseArgs } from "node:util";
+import { chromium, type Browser, type Page } from "playwright";
+import { FIRST_RUN_VIEW_ID } from "@/common/constants/firstRunView";
+import { FIRST_RUN_VIEW_HTML } from "@/node/orpc/firstRunView";
+
+export interface Box {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+}
+
+/** The served "/" with the static view inserted once, before