diff --git a/apps/debug-frontend/.gitignore b/apps/debug-frontend/.gitignore
new file mode 100644
index 000000000..aa2168ae1
--- /dev/null
+++ b/apps/debug-frontend/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+dist
+.vitest-attachments
+src/**/__screenshots__
diff --git a/apps/debug-frontend/.oxfmtignore b/apps/debug-frontend/.oxfmtignore
new file mode 100644
index 000000000..928e4ae2f
--- /dev/null
+++ b/apps/debug-frontend/.oxfmtignore
@@ -0,0 +1,2 @@
+dist
+src/routeTree.gen.ts
diff --git a/apps/debug-frontend/.oxlintignore b/apps/debug-frontend/.oxlintignore
new file mode 100644
index 000000000..928e4ae2f
--- /dev/null
+++ b/apps/debug-frontend/.oxlintignore
@@ -0,0 +1,2 @@
+dist
+src/routeTree.gen.ts
diff --git a/apps/debug-frontend/README.md b/apps/debug-frontend/README.md
new file mode 100644
index 000000000..70e66e7ca
--- /dev/null
+++ b/apps/debug-frontend/README.md
@@ -0,0 +1,34 @@
+# @plannotator/debug-frontend
+
+Debug/development harness UI for the Plannotator daemon runtime. **Not production code** — this is a
+testbed for exercising daemon sessions, verifying event streams, and testing session lifecycle actions.
+
+## Shape
+
+- `src/routes` is only TanStack Router wiring.
+- `src/daemon` owns the typed daemon API client and contracts.
+- `src/sessions` owns session ids, session state, the dashboard, and mode dispatch.
+- `src/plan`, `src/review`, `src/annotate`, `src/archive`, and `src/setup-goal` own product views.
+- `src/testing` owns contract fixtures and browser helpers.
+
+The shell talks to session APIs through `/s/:sessionId/api`, never root `/api`.
+
+The build is intentionally single-file HTML for daemon serving. Separate static asset
+routes are deferred until the full UI migration needs code splitting or cacheable chunks.
+
+## Commands
+
+```bash
+bun run --cwd apps/debug-frontend dev
+bun run --cwd apps/debug-frontend build
+bun run --cwd apps/debug-frontend check
+bun run --cwd apps/debug-frontend test:browser
+```
+
+Or from the repo root:
+
+```bash
+bun run dev:debug-frontend
+bun run build:debug-frontend
+bun run check:debug-frontend
+```
diff --git a/apps/debug-frontend/index.html b/apps/debug-frontend/index.html
new file mode 100644
index 000000000..a975c0f7a
--- /dev/null
+++ b/apps/debug-frontend/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Plannotator
+
+
+
+
+
+
diff --git a/apps/debug-frontend/package.json b/apps/debug-frontend/package.json
new file mode 100644
index 000000000..2bb66326e
--- /dev/null
+++ b/apps/debug-frontend/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "@plannotator/debug-frontend",
+ "description": "Debug/development harness UI for the Plannotator daemon runtime. Not production code.",
+ "private": true,
+ "version": "0.0.1",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build && bun run scripts/verify-single-file-build.ts",
+ "typecheck": "tsc --noEmit -p tsconfig.json",
+ "lint": "oxlint .",
+ "lint:fix": "oxlint . --fix",
+ "fmt": "oxfmt --ignore-path .oxfmtignore --write .",
+ "fmt:check": "oxfmt --ignore-path .oxfmtignore --check .",
+ "test": "vitest run",
+ "test:browser": "vitest run --config vitest.browser.config.ts",
+ "check": "bun run typecheck && bun run lint && bun run fmt:check && bun run test"
+ },
+ "dependencies": {
+ "@plannotator/shared": "workspace:*",
+ "@plannotator/ui": "workspace:*",
+ "@tanstack/react-router": "^1.141.0",
+ "immer": "^10.2.0",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3",
+ "zustand": "^5.0.13"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.1.18",
+ "@tanstack/router-plugin": "^1.141.0",
+ "@types/node": "^22.14.0",
+ "@types/react": "^19.2.0",
+ "@types/react-dom": "^19.2.0",
+ "@vitejs/plugin-react": "^5.0.0",
+ "@vitest/browser-playwright": "^4.0.16",
+ "oxfmt": "^0.17.0",
+ "oxlint": "^1.31.0",
+ "tailwindcss": "^4.1.18",
+ "typescript": "~5.8.2",
+ "vite": "^6.2.0",
+ "vite-plugin-singlefile": "^2.0.3",
+ "vitest": "^4.0.16"
+ }
+}
diff --git a/apps/debug-frontend/scripts/verify-single-file-build.ts b/apps/debug-frontend/scripts/verify-single-file-build.ts
new file mode 100644
index 000000000..137408b26
--- /dev/null
+++ b/apps/debug-frontend/scripts/verify-single-file-build.ts
@@ -0,0 +1,56 @@
+import { existsSync, readdirSync, readFileSync } from "fs";
+import { join, relative } from "path";
+
+const distDir = join(import.meta.dirname, "..", "dist");
+const indexPath = join(distDir, "index.html");
+
+function listFiles(dir: string): string[] {
+ if (!existsSync(dir)) return [];
+
+ const files: string[] = [];
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
+ const entryPath = join(dir, entry.name);
+ if (entry.isDirectory()) {
+ files.push(...listFiles(entryPath));
+ } else if (entry.isFile()) {
+ files.push(entryPath);
+ }
+ }
+ return files;
+}
+
+if (!existsSync(indexPath)) {
+ throw new Error("Expected apps/debug-frontend/dist/index.html to exist after build.");
+}
+
+const html = readFileSync(indexPath, "utf-8");
+
+const outputFiles = listFiles(distDir)
+ .map((file) => relative(distDir, file))
+ .sort();
+const extraFiles = outputFiles.filter((file) => file !== "index.html");
+
+if (extraFiles.length > 0) {
+ throw new Error(
+ `Frontend daemon shell build must be single-file; found outputs: ${extraFiles.join(", ")}`,
+ );
+}
+
+const htmlWithoutInlineCode = html
+ .replace(/")
+ .replace(/");
+
+const externalScriptPattern = /Shell