studio — the build renders its own construction site
Stage: Phase 5 — Scaffold (right after scaffold; skipped at sketch tier) - Reads: design/PROGRESS.md, design/QA.md, design/REVIEWS.md, design/screenshots/, design/studio-log.jsonl — all off disk, at request time; design/CONTEXT-HANDOFF.md and the qa/*.log files are deliberately not rendered (the page shows PROGRESS, QA, REVIEWS, screenshots and the activity log, nothing else) - Writes: app/(studio)/studio/* (page, feed handler, shot handler, one client leaf)
A long build is a long silence, and the usual fix is narration — tokens spent describing the work instead of doing it, and a transcript the user has to read. This route replaces narration with a window: the files the pipeline already writes, served by the dev server it already runs, at zero tokens per refresh. The client hired a studio, not a file browser — let them stand in the doorway.
Standard
- Free to watch. Every update comes from
fs at request time — no model call, no cached snapshot, no agent-authored "status update" on the page. Two hundred reloads cost what zero do.
- Dead in production. Two independent locks:
notFound() on NODE_ENV === "production" in every file of the route group, and ship's smoke test fetching /studio against npm run start expecting 404. A 200 is a ship blocker, not a note.
- Un-designed on purpose. System monospace, system colors, borders not shadows, no tokens, no motion, nothing from
components/ui. A styled /studio competes with the work and teaches the user to grade the wrong surface.
- Honest when empty. Phase 5 runs before QA.md, REVIEWS.md, or a single screenshot exists. Every panel renders its own absence in plain words — "nothing yet — Phase 11 writes this" — never an error, a permanent spinner, or a fake zero.
- Render, don't parse. PROGRESS.md and REVIEWS.md go on the page as their own text; only QA.md's gate rows get pattern-matched, for PASS / FAIL / UNVERIFIED coloring. A markdown parser here is scope creep with a dependency attached.
- Exempt from the gates.
gate-antislop does not sweep it, gate-visual does not judge it, gate-responsive does not screenshot it. The gates measure the site and this is not the site — say so in QA.md's first entry so no later run "discovers" it.
- Thirty minutes, ceiling. It is a plywood viewing platform. Time spent styling it is stolen from what the client pays for.
Process
- Confirm the tier. Sketch skips this skill — a one-pager is over before a dashboard pays for itself.
- Create the route group
app/(studio)/ so the feature is one deletable folder inheriting nothing from the marketing layout. Its own bare layout.tsx: no header, no footer, no fonts.
- Write the production gate first, in the page and in every route handler under the group. A route that renders before it refuses is a leak waiting for a deadline.
- Read the artifacts defensively. One helper taking a path and returning
string | null, swallowing ENOENT and nothing else, resolving from process.cwd() — after scaffold's move, that is where design/ lives. Every panel accepts null and renders a "nothing yet" line naming the phase that will fill it.
- Build five panels top to bottom, in the order the user asks the questions: (1) Now / Waiting on you — PROGRESS.md's first two sections verbatim, the waiting line loudest on the page; (2) Gates — QA.md's rows as a table: green PASS, red FAIL, amber UNVERIFIED; (3) Checkpoints — REVIEWS.md's blocks and their Approved / Auto-passed lines; (4) Contact sheet — every image under
design/screenshots/ and the gates' qa/, newest first, filename beneath; (5) Activity — the live feed.
- Serve screenshots through a dev-only route handler —
shot/route.ts, taking a ?f= basename, rejecting anything that does not resolve inside the screenshot directories, streaming bytes no-store.
- Add the feed as the one client leaf.
feed.tsx ("use client") polls feed/route.ts every 2000ms; the handler tails the last ~40 lines of design/studio-log.jsonl as JSON. Malformed lines are skipped, never thrown on — a hook is appending mid-build and the read can catch a half-flushed line.
- Force dynamic rendering, then prove it once.
export const dynamic = "force-dynamic" on page and handlers; load /studio, confirm it renders against Phase 5 reality (PROGRESS.md alone, the rest empty), print the URL to the terminal exactly once. Nothing links to it — no nav entry, no sitemap.ts, no line in SITEMAP.md.
Route contract
The whole feature, and the two lines that make it safe:
// app/(studio)/layout.tsx ← bare workshop chrome
// app/(studio)/studio/page.tsx ← the five panels (server component)
// app/(studio)/studio/feed.tsx ← "use client", polls feed/ every 2s
// app/(studio)/studio/feed/route.ts ← tails design/studio-log.jsonl
// app/(studio)/studio/shot/route.ts ← streams one screenshot by basename
import { notFound } from "next/navigation";
export const dynamic = "force-dynamic";
export default async function StudioPage() {
if (process.env.NODE_ENV === "production") notFound();
// ...read design/* off disk, render the five panels
}
The feed's input, appended by the plugin's hooks/studio-log.sh on PostToolUse(Task) and SubagentStop — one line per agent launch and finish, no model in the loop:
{"ts":"2026-08-14T15:22:07Z","event":"PostToolUse","agent":"general-purpose","summary":"Build /pricing per SITEMAP.md"}
{"ts":"2026-08-14T15:41:33Z","event":"SubagentStop","agent":"","summary":""}
Empty fields render blank, unknown keys are ignored, corrupt lines dropped; the page never writes here. The hook tracks agents, so the feed is busy in fan-out mode and quiet in solo — by design, the other four panels carry the page.
Anti-patterns
- A
/studio that renders in production because the env check sat below the first await — gate on line one or it is not gated
- Designing it: tokens, shadcn components, motion, the site's own fonts. The workshop must look like a workshop
- The agent writing status prose INTO the page — narration with extra steps, priced per update
- Polling a server action, or revalidating the whole page, instead of one small JSON handler
- Symlinking
design/screenshots/ into public/ — public/ ships, and the evidence would ship with it
- A panel that 500s or prints "undefined" because its artifact does not exist yet
- Linking it from the nav, or letting it into
sitemap.ts — nothing on the site knows it exists
Worked example — Tidepool, port-logistics SaaS built in fan-out mode
Moved to references/example.md — read only when this build's case is genuinely ambiguous; the sections above are the decision material.
Composes with
Moved to references/composes.md — the handoff map; load it when orchestrating this skill against its neighbors.
1---2name: studio3description: Scaffold the dev-only /studio route — the construction-site window onto a running ultraweb build. A force-dynamic page, hard-gated to 404 in production, that reads design/PROGRESS.md, design/QA.md, design/REVIEWS.md, design/screenshots/ and design/studio-log.jsonl off disk at request time and renders phase progress, the gate table, the checkpoint ledger, a screenshot contact sheet, and a 2-second-polled activity feed — so the user watches the build live at zero token cost per update. Invoke in Phase 5 immediately after scaffold (skipped at sketch tier), whenever the user asks to "watch the build", "see it live", "show me what you're doing", "is there a dashboard", "open the studio", or when a resumed build finds app/(studio)/ missing. Deliberately un-designed workshop chrome; the gates ignore it and handoff documents deleting it.4---56# studio — the build renders its own construction site78**Stage:** Phase 5 — Scaffold (right after `scaffold`; skipped at sketch tier) - **Reads:** design/PROGRESS.md, design/QA.md, design/REVIEWS.md, design/screenshots/, design/studio-log.jsonl — all off disk, at request time; `design/CONTEXT-HANDOFF.md` and the `qa/*.log` files are deliberately not rendered (the page shows PROGRESS, QA, REVIEWS, screenshots and the activity log, nothing else) - **Writes:** app/(studio)/studio/* (page, feed handler, shot handler, one client leaf)910A long build is a long silence, and the usual fix is narration — tokens spent describing the work instead of doing it, and a transcript the user has to read. This route replaces narration with a window: the files the pipeline already writes, served by the dev server it already runs, at zero tokens per refresh. The client hired a studio, not a file browser — let them stand in the doorway.1112## Standard1314- **Free to watch.** Every update comes from `fs` at request time — no model call, no cached snapshot, no agent-authored "status update" on the page. Two hundred reloads cost what zero do.15- **Dead in production.** Two independent locks: `notFound()` on `NODE_ENV === "production"` in every file of the route group, and `ship`'s smoke test fetching `/studio` against `npm run start` expecting 404. A 200 is a ship blocker, not a note.16- **Un-designed on purpose.** System monospace, system colors, borders not shadows, no tokens, no motion, nothing from `components/ui`. A styled `/studio` competes with the work and teaches the user to grade the wrong surface.17- **Honest when empty.** Phase 5 runs before QA.md, REVIEWS.md, or a single screenshot exists. Every panel renders its own absence in plain words — "nothing yet — Phase 11 writes this" — never an error, a permanent spinner, or a fake zero.18- **Render, don't parse.** PROGRESS.md and REVIEWS.md go on the page as their own text; only QA.md's gate rows get pattern-matched, for PASS / FAIL / UNVERIFIED coloring. A markdown parser here is scope creep with a dependency attached.19- **Exempt from the gates.** `gate-antislop` does not sweep it, `gate-visual` does not judge it, `gate-responsive` does not screenshot it. The gates measure the site and this is not the site — say so in QA.md's first entry so no later run "discovers" it.20- **Thirty minutes, ceiling.** It is a plywood viewing platform. Time spent styling it is stolen from what the client pays for.2122## Process23241. **Confirm the tier.** Sketch skips this skill — a one-pager is over before a dashboard pays for itself.252. **Create the route group** `app/(studio)/` so the feature is one deletable folder inheriting nothing from the marketing layout. Its own bare `layout.tsx`: no header, no footer, no fonts.263. **Write the production gate first**, in the page and in every route handler under the group. A route that renders before it refuses is a leak waiting for a deadline.274. **Read the artifacts defensively.** One helper taking a path and returning `string | null`, swallowing ENOENT and nothing else, resolving from `process.cwd()` — after `scaffold`'s move, that is where `design/` lives. Every panel accepts `null` and renders a "nothing yet" line naming the phase that will fill it.285. **Build five panels top to bottom**, in the order the user asks the questions: (1) **Now / Waiting on you** — PROGRESS.md's first two sections verbatim, the waiting line loudest on the page; (2) **Gates** — QA.md's rows as a table: green PASS, red FAIL, amber UNVERIFIED; (3) **Checkpoints** — REVIEWS.md's blocks and their Approved / Auto-passed lines; (4) **Contact sheet** — every image under `design/screenshots/` and the gates' `qa/`, newest first, filename beneath; (5) **Activity** — the live feed.296. **Serve screenshots through a dev-only route handler** — `shot/route.ts`, taking a `?f=` basename, rejecting anything that does not resolve inside the screenshot directories, streaming bytes `no-store`.307. **Add the feed as the one client leaf.** `feed.tsx` (`"use client"`) polls `feed/route.ts` every 2000ms; the handler tails the last ~40 lines of `design/studio-log.jsonl` as JSON. Malformed lines are skipped, never thrown on — a hook is appending mid-build and the read can catch a half-flushed line.318. **Force dynamic rendering, then prove it once.** `export const dynamic = "force-dynamic"` on page and handlers; load `/studio`, confirm it renders against Phase 5 reality (PROGRESS.md alone, the rest empty), print the URL to the terminal exactly once. Nothing links to it — no nav entry, no `sitemap.ts`, no line in SITEMAP.md.3233## Route contract3435The whole feature, and the two lines that make it safe:3637```tsx38// app/(studio)/layout.tsx ← bare workshop chrome39// app/(studio)/studio/page.tsx ← the five panels (server component)40// app/(studio)/studio/feed.tsx ← "use client", polls feed/ every 2s41// app/(studio)/studio/feed/route.ts ← tails design/studio-log.jsonl42// app/(studio)/studio/shot/route.ts ← streams one screenshot by basename43import { notFound } from "next/navigation";4445export const dynamic = "force-dynamic";4647export default async function StudioPage() {48 if (process.env.NODE_ENV === "production") notFound();49 // ...read design/* off disk, render the five panels50}51```5253The feed's input, appended by the plugin's `hooks/studio-log.sh` on PostToolUse(Task) and SubagentStop — one line per agent launch and finish, no model in the loop:5455```jsonl56{"ts":"2026-08-14T15:22:07Z","event":"PostToolUse","agent":"general-purpose","summary":"Build /pricing per SITEMAP.md"}57{"ts":"2026-08-14T15:41:33Z","event":"SubagentStop","agent":"","summary":""}58```5960Empty fields render blank, unknown keys are ignored, corrupt lines dropped; the page never writes here. The hook tracks agents, so the feed is busy in fan-out mode and quiet in solo — by design, the other four panels carry the page.6162## Anti-patterns6364- A `/studio` that renders in production because the env check sat below the first `await` — gate on line one or it is not gated65- Designing it: tokens, shadcn components, motion, the site's own fonts. The workshop must look like a workshop66- The agent writing status prose INTO the page — narration with extra steps, priced per update67- Polling a server action, or revalidating the whole page, instead of one small JSON handler68- Symlinking `design/screenshots/` into `public/` — `public/` ships, and the evidence would ship with it69- A panel that 500s or prints "undefined" because its artifact does not exist yet70- Linking it from the nav, or letting it into `sitemap.ts` — nothing on the site knows it exists7172## Worked example — Tidepool, port-logistics SaaS built in fan-out mode7374Moved to `references/example.md` — read only when this build's case is genuinely ambiguous; the sections above are the decision material.7576## Composes with7778Moved to `references/composes.md` — the handoff map; load it when orchestrating this skill against its neighbors.