Build DEC-0064 Option D into running code — the v0 agency governance dashboard is a non-prerendered `(operator)` route group that reuses dispose.ts, gated behind DOSSIER_OPERATOR
0068-operator-dashboard-route-group
- Reversibility
- two-way door
DEC-0068 — The v0 agency governance dashboard: a non-prerendered (operator) route group over dispose.ts
Reversibility: two-way door. The change is additive — one route group, one server-only bridge module, one workspace dependency, one vitest $lib alias. The disposition mechanism (dispose.ts) is untouched; this is a second front door onto it, exactly as DEC-0064 framed it. Delete the (operator) group and the platform is unchanged (the CLI front door still disposes). The public deploy is unaffected — it never sets DOSSIER_OPERATOR and nothing prerendered links to the group.
Context — DEC-0064 decided the posture; this builds it
DEC-0064 disposed the topology question: the public /board deploy holds no mutation authority (read-only, prerender=true, sovereignty contract — DEC-0001), and governed approve/reject lives with the operator control plane. It adopted Option D — a local-only operator dashboard ("the app run locally against a local tenant repo, never the Vercel deploy") as the optional browser home, and assigned the Principal Forward Deployed Engineer its wiring: "reuse dispose.ts, no new mechanism." The operator greenlit it as the next move. This DEC records the concrete build of Option D as the v0 agency governance dashboard that makes DEC-0052 Phase 0 real — the human approve / ship / watch surface over the loop (Spec the v0 agency dashboard surface (Phase 0 dogfood — Dossier's own .claude/agents team on Dossier's own OKF; daily-standup / approve-ship loop)).
Decision — the concrete topology
A SvelteKit (operator) route group at /operator, structurally segregated from the read-only public surfaces:
(operator)/+layout.tsisprerender = false— the structural opposite of the(app)/boardroute'sprerender = true. The dashboard reads + mutates the live tenant repo at request time; it cannot be a static file. It builds as a server function, never a prerendered page (verified:board.htmlis still emitted; nooperatorappears underprerendered/).(operator)/operator/+page.server.ts— theloadreuseslistReviewQueue; the approve/reject form actions reuseapproveTask/rejectTask. These are the EXACT@dossier/runtimefunctionspackages/runtime/src/cli.tscalls (cmdReviewQueue/cmdApprove/cmdReject). The disposition logic is not forked — single source of truth, so the browser and the CLI can never drift and Inv 3 (only this human-invoked path reachesdone) is non-bypassable by construction regardless of front door.$lib/server/operator.ts— the server-only bridge holding all@dossier/runtimecalls + the gate. SvelteKit never bundles$lib/server/*to the client, so the tenant repo path + git + the mutation seam stay server-side, never in the browser (the DEC-0064 §Context posture).
Three consequential choices
- New
@dossier/app → @dossier/runtimeworkspace dependency. The app gains a server-side dependency on the runtime control plane. Confined to$lib/server/*(client-unreachable). This is the first time the serve layer (app) depends on the orchestration layer (runtime) — deliberate, because the dashboard is a control-plane front door. - The
DOSSIER_OPERATORenv gate. The mutating actions refuse unlessDOSSIER_OPERATOR=1. The public Vercel deploy never sets it, so it can never gain mutation authority — the read-only contract holds literally, by a single switch, not by hoping no one wires a button. The read (review queue) is always allowed (read-only-then-governed).DOSSIER_OPERATOR_ROOT/_CLIENTlocate the tenant (the CLI's--root/--clientanalogue), defaulting to the dogfood RBA tenant. - The proposed-diff lenient join. The drain-emitted
proposed_atoms(off the strictTaskSchema, DEC-0053 Inv 2 / DEC-0064) is stripped by the validatedlistReviewQueue. To surface it without forking the queue authority,loadReviewQueuejoins it from a lenientreadKbAtomsread by atom id — the SAME read the public board loader uses — keeping the two surfaces in lock-step on the diff whilelistReviewQueuestays the single authority for which tasks are inreview.
Why a route group, not a button on the public board (rejected)
DEC-0064 already rejected Option B (a mutating server action on the public app) outright — it would put a credential-bearing, git-mutating endpoint on the replaceable public cache and tunnel the sovereign repo through Vercel. This DEC honors that: the operator surface is a distinct group with its own prerender=false + env gate, so the public board's one clean property — static prerenderability + zero mutation authority — is preserved untouched.
Proven this session (no fabricated status)
- Live browser round-trip: Playwright drove
/operatoragainst the provisioned RBA tenant — the review queue rendered the realtask-data-estate-assessment-northwindwith full context; clicking Approve & ship drove the realapproveTask→ the task transitionedreview → donewithapproved-by:humanprovenance and a real tenant-git commit (dossier(board): approve … → done — compounded by human (DEC-0053 Inv 3)); the CLIreview-queuewent 1 → 0; the after-state rendered the empty queue. The tenant was then restored toreview(the proof is repeatable;clients/is gitignored so the main repo is untouched). Screenshots underscreenshots/operator-0{1,2,3}-*.png. - Offline integration test (
packages/app/test/operator-dashboard.test.ts, 7 cases):loadreads the live queue + the lenient proposed diff;approvedrives review→done+commit and clears the queue;rejectdrives review→backlog with a recorded reason; the gate refuses a non-reviewtask (409 — the CLI exit-2 analogue) and a missing id (400); and withDOSSIER_OPERATORunset the read still works but mutation is refused (403) — the public-deploy posture. - Gates:
pnpm typecheckclean ·pnpm --filter @dossier/app check0 errors ·pnpm test520 passed / 2 skipped ·pnpm buildclean (board.html still prerendered, operator is a server function) ·pnpm kb:checkclean ·pnpm plugin:checkin sync.
Two bugs found + fixed while proving it (recorded for the next person)
- Client bundle pulled
node:path. The page component importedstatusLabelfrom the@dossier/okf-viewbarrel, which drags inroutes.js(usesnode:path) — fine at SSR, but it threw on client hydration afterinvalidateAll()(a 500 on the post-disposition reload). Fix: the component imports NO Node-touching module; the gate label is a client-side constant, and any status-label need comes server-side via the load. The integration test guards this class going forward. proposed_atomswas empty becauselistReviewQueue's validated parse strips off-schema fields — hence the lenient-join choice above.
Consequences
- DEC-0052 Phase 0's named dashboard surface now exists as running, verified code: the org (Dossier, dogfooding the RBA tenant) sees its agentic team's
reviewwork and runs the human approve/ship loop in a browser. Spec the v0 agency dashboard surface (Phase 0 dogfood — Dossier's own .claude/agents team on Dossier's own OKF; daily-standup / approve-ship loop) moves toreviewfor product-owner acceptance of the v0 (the spec task asked for the surface; the v0 surface is built). - The
dispose.tsseam is confirmed as the single mutation authority across all three hosts (CLI / local dashboard / future deployed service — DEC-0064 Option C), keeping Inv 3 non-bypassable regardless of front door. - Carried forward / NOT built: the autonomy dial (per-team/per-action deterministic→self-guided) and the daily-report digest from Spec the v0 agency dashboard surface (Phase 0 dogfood — Dossier's own .claude/agents team on Dossier's own OKF; daily-standup / approve-ship loop) are not in v0 (v0 is the approve/ship/watch queue). The deployed, authenticated, per-tenant-siloed control-plane service stays deferred behind DEC-0064's trigger (a non-CLI operator disposing in a browser against a hosted repo).
vite previewdoes not serve the SSR function (adapter-vercel) — the local dashboard runs underpnpm dev(or a deployed runtime); this is the expected Option-D local posture.