Governed disposition lives with the operator control plane (CLI now, local dashboard optional, deployed siloed service deferred) — the public `/board` deploy stays read-only and holds no mutation authority
0064-governed-disposition-topology-read-only-board
- Reversibility
- two-way door
DEC-0064 — Governed disposition lives with the operator control plane; the public /board stays read-only
Reversibility: two-way door. The disposition mechanism (dispose.ts — approveTask/rejectTask/listReviewQueue, the only path that reaches done, every write confineToTenant-gated) is identical in every option; only which host serves the second front door onto it changes. The public deploy stays static (reversible by deleting a component); the mutation host moves CLI → optional local dashboard → eventual deployed control plane without re-architecting the seam. The single deliberate forward door — standing up a deployed control-plane service — is gated on an explicit trigger (below).
Context — the constraint that blocks the naive path is real and verified
DEC-0063 gave the agentic loop a CLI front door and explicitly routed the remaining "approve/ship view" half here as a topology call. Ground truth, verified:
packages/app/src/routes/(app)/board/+page.server.tsisexport const prerender = true(statically prerendered, no request-time server runtime) and READ-ONLY by explicit sovereignty contract — its header states the surface is "a derived, replaceable view… there is no claim/transition/create/edit affordance. A client could delete this whole surface and lose nothing."- The app is the primary Vercel deploy (
packages/app/vercel.jsoncatch-all rewrite); the tenant OKF repo + git (whereapproveTaskcompounds thereview → donecommit) lives server-side / on the operator's machine, addressed by--root/--client— never in the browser.
So a browser-initiated governed, git-mutating approve/reject cannot be a button on a prerendered static page: it needs a server runtime with access to the tenant git, and where that runtime lives has sovereignty implications.
The reframe that decides it: disposition is already built, tested, and operator-reachable (dispose.ts → dossier-runtime approve/reject/review-queue, proven committing into a real temp tenant repo this session, DEC-0063). "Browser approve/reject" is therefore not a new capability — it is a second front door onto an existing seam. The decision is not whether to build git-mutation-from-a-button; it is which host serves it. The answer: anything but the public read-only cache.
Options considered
- Public deploy stays a pure read-only mirror; disposition is CLI/operator-only (Option A). Sovereignty-purest, zero new attack surface, the contract holds literally. Cost: the human disposes in a terminal, not a browser. Adopted for the public deploy.
- An authenticated server action /
+server.tson the public app (dropprerenderfor that route) callingdispose.ts. Best one-click DX — but rejected: it puts a credential-bearing, git-mutating endpoint on the replaceable public cache whose contract is "delete me and lose nothing," and forces the sovereign tenant repo (the SSOT) to be resident on or tunneled through Vercel's serverless runtime. Violates the spirit of DEC-0001 (the index/app is a replaceable cache; mutation authority belongs to the sovereign, not the cache) and the DEC-0008 confinement model, and breaks the board's one clean property — static prerenderability. - A separate deployed operator control-plane service that owns ALL mutation; the public app stays strictly read-only (Option C). The correct eventual topology — mutation authority is a distinct, authenticated, per-tenant-siloed (DEC-0008) control plane, never the public deploy. Cost: a real service to design, auth, host, and isolate — premature now. Deferred behind a trigger.
- A local-only operator dashboard: the app run locally against a local tenant repo, never the Vercel deploy (Option D). Fastest honest path to a browser governed loop; sidesteps every sovereignty objection (runs on the operator's machine, against the operator's repo, same auth posture as the CLI, never deployed). Effectively "Option C, but the control plane is
localhost." Adopted as the optional browser home now.
Decision
- The public Vercel
/boarddeploy holds NO mutation authority — read-only, forever.prerender = trueand the sovereignty header are the correct contract, not a limitation to route around. (Constraint ratified — cheap to state now, expensive to discover later.) - Governed, browser-initiated approve/reject lives with the operator control plane that holds the tenant repo: the proven CLI today (DEC-0063); a local-only operator dashboard (Option D) if a browser loop is wanted before a hosted service exists. Mutation authority always sits with the sovereign-adjacent control plane, never the public cache.
- A deployed, authenticated, per-tenant-siloed control-plane service (Option C) is the eventual home, DEFERRED behind the trigger below.
- Reject Option B outright.
The /board v0 that ships now (read-only-then-governed, zero one-way doors)
A read-only review-queue + proposed-diff enhancement to the prerendered /board, with the actual disposition routed to the working CLI:
- A review lane / "N awaiting your disposition" panel built mostly from facts the loader already computes —
reviewCount, theisGatereview column,isReviewper card, theboardDetailsquick-look blob. v0 is largely presentation of existing data. - Each review card surfaces the exact governed command to dispose it —
dossier-runtime approve --root … --client … --task <id>(andreject). The honest bridge: the surface shows what needs a decision and the precise governed command; the act happens through the proven, confined, committing path. No fake button that 404s on a static page. - The proposed diff is read from drain-emitted provenance (the
added|updateddiff the worker writes on transition toreview— DEC-0053 Inv 2), NOT reconstructed via build-time git introspection (that couples the static build to live, mutable git state, goes stale the instant a drain runs, and duplicates what the loop already knows). If that provenance is not yet persisted in a board-readable form, that enabling seam is the v0 boundary: ship the review queue + CLI disposition now; show the rich diff once the loop persists it as a board-readable artifact.
This satisfies 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)'s "read-only-then-governed" invariant literally — the dashboard reads the board and the human disposes through a governed path; the governed write is simply hosted on the CLI/control plane, not a public-deploy button.
The deferred one-way door + its trigger (DEC-0062-style)
Defer Option C (the deployed control plane). The genuinely consequential, harder-to-reverse call is standing up a deployed, authenticated, per-tenant-siloed mutation service (auth, hosting, and service-level isolation commitments — its own boundary, not assumed). Revisit when: the first time a non-CLI operator (an agency PM, or a client) must dispose review work in a browser against a hosted tenant repo they do not run locally. Until then, the CLI (operator) + optional local dashboard (Option D) cover every actual user. When it fires, the control plane is designed siloed-per-tenant (DEC-0008), and the public /board still never mutates — it points at the control plane.
Ownership
- sveltekit-engineer — the v0 read-only review-queue + proposed-diff presentation surface + per-card "dispose via this CLI command" affordance; keeps
prerender = true(packages/app/src/routes/(app)/board/). - forward-deployed-engineer — the diff-as-provenance seam: ensure the drain persists the emitted
added|updateddiff in a board-readable, atomic form on transition toreview, so the read-only loader renders it without build-time git introspection (cross-layer glue between the worker/dispose.tsand@dossier/okf-view/the loader). Owns Option D wiring if/when greenlit (reusedispose.ts, no new mechanism). - principal-architect — owns the deferred Option C topology decision when its trigger fires; ratified the "public deploy holds no mutation authority" constraint here.
- product-owner — owns
task-agency-dashboard-v0-specacceptance: confirm the read-only-then-governed v0 meets the dashboard surface's Phase-0 done-definition even though the governed write is CLI/local for now.
Consequences
- The board-view half of DEC-0063's highest-leverage move is de-risked and specced: the blocking topology is decided, the v0 is bounded, the one-way door is deferred with a trigger.
- No code changed by this decision — it ratifies a constraint, defines a v0 boundary, and defers a service. The correct cost for a topology disposition.
- The
dispose.tsseam is confirmed as the single mutation authority across every host (CLI / local / future service), keeping Inv 3 non-bypassable by construction regardless of which front door is used.