# Originmain — Claude Code Build Prompt ## Layer-by-Layer Engineering Guide for AI Coding Agents **Product:** Originmain — AI-Native Design Engineering Platform **Stack:** React 19 · Next.js 15 · Fluent UI v9 · TypeScript 5 · PostgreSQL · Supabase · Webpack Module Federation · MCP · Claude Sonnet 4 **Renderer:** @pierre/diffs (code diff display) · @pierre/trees (codebase file browser) **Tooling:** pnpm workspaces · Vitest · Playwright · ESLint strict · Zod **Date:** April 2026 --- ## How to Read This Document This prompt is structured as a strict sequence of layers. Each layer has a **goal**, a set of **files to create or modify**, a **verification gate** you must pass before proceeding, and **critical constraints** that must not be violated. Do not begin a layer until the previous layer's gate passes. Do not skip gates under any circumstances. When you see `[FILE]`, create or modify that file. When you see `[VERIFY]`, run the specified command and confirm it passes before continuing. When you see `[CRITICAL]`, treat that constraint as a hard requirement — violations will cause downstream failure. --- ## Repository Bootstrap (Before Layer 0) ```bash # Initialise the pnpm monorepo mkdir originmain && cd originmain git init pnpm init echo "packages:\n - 'packages/*'" > pnpm-workspace.yaml # Create all package directories mkdir -p packages/{app,renderer,diff-engine,origin-graph,ai-layer,agent-bridge,ui,integrations} mkdir -p .github/workflows ``` The root `package.json` sets `"type": "module"` and declares the pnpm workspace. All packages share TypeScript 5 strict config via a root `tsconfig.base.json`. All packages run tests via a root `vitest.config.ts` with `projects` pointing to each package. --- ## Layer 0 — Infrastructure & DevOps **Goal:** A working monorepo skeleton that CI can lint, type-check, build, and test. No product logic yet. ### 0.1 Root Configuration **[FILE] `tsconfig.base.json`** ```json { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "exactOptionalPropertyTypes": true, "noUncheckedIndexedAccess": true, "jsx": "react-jsx", "paths": { "@originmain/ui": ["./packages/ui/src/index.ts"], "@originmain/diff-engine": ["./packages/diff-engine/src/index.ts"], "@originmain/origin-graph": ["./packages/origin-graph/src/index.ts"], "@originmain/agent-bridge": ["./packages/agent-bridge/src/index.ts"], "@originmain/ai-layer": ["./packages/ai-layer/src/index.ts"], "@originmain/renderer": ["./packages/renderer/src/index.ts"], "@originmain/integrations": ["./packages/integrations/src/index.ts"] } } } ``` **[FILE] `.github/workflows/ci.yml`** Define a GitHub Actions workflow named `CI` that runs on every pull request to `main`. It must execute these steps in strict sequence: 1. `pnpm install --frozen-lockfile` 2. `pnpm run typecheck` (runs `tsc --noEmit` in every package) 3. `pnpm run lint` (ESLint with `@typescript-eslint/strict` ruleset, zero warnings policy) 4. `pnpm run test` (Vitest across all packages, minimum 80% coverage on `diff-engine` and `origin-graph`) 5. `pnpm run build` (Next.js production build for `packages/app`) 6. `pnpm run migration:dry-run` (Supabase migration dry-run against production schema snapshot) No step may be skipped or made non-blocking. The CI pipeline is the product's quality gate. **[FILE] `.github/workflows/preview.yml`** A separate workflow that deploys a Vercel preview URL on every PR and posts it as a PR comment. ### 0.2 Supabase Project Setup Create a `supabase/` directory at the monorepo root. Initialise it with `supabase init`. The `supabase/config.toml` sets: - `project_id = "originmain-local"` - `db.port = 54322` - `studio.port = 54323` - `api.port = 54321` The local Supabase instance is started with `supabase start` and used by all local development. Staging and production use separate Supabase cloud projects. ### 0.3 Environment Variables **[FILE] `.env.example`** — document every required variable: ``` NEXT_PUBLIC_SUPABASE_URL= NEXT_PUBLIC_SUPABASE_ANON_KEY= SUPABASE_SERVICE_ROLE_KEY= ANTHROPIC_API_KEY= CLERK_PUBLISHABLE_KEY= CLERK_SECRET_KEY= LIVEBLOCKS_SECRET_KEY= LINEAR_WEBHOOK_SECRET= SLACK_BOT_TOKEN= SLACK_SIGNING_SECRET= AGENT_BRIDGE_PORT=3001 NEXT_PUBLIC_APP_URL=http://localhost:3000 ``` Never commit `.env.local`. Add it to `.gitignore`. The CI pipeline uses GitHub Actions secrets injected as environment variables. **[VERIFY]** Run `pnpm install` and `pnpm run typecheck`. Both must exit 0. --- ## Layer 1 — Canvas UI Shell **Goal:** A navigable Next.js app with the Fluent 2 chrome, a working infinite canvas viewport, and the two-surface navigation architecture (artboard navigator + codebase file browser). No artboard content yet — placeholder divs only. ### 1.1 Package Setup (`packages/app`) Install core dependencies: ```bash cd packages/app pnpm add next@15 react@19 react-dom@19 pnpm add @fluentui/react-components @fluentui/react-icons pnpm add @pierre/trees pnpm add zustand immer pnpm add @tanstack/react-query pnpm add -D typescript @types/react @types/node ``` The Next.js app uses the App Router (`app/` directory). No Pages Router. ### 1.2 Root Layout and Fluent Provider **[FILE] `packages/app/src/app/layout.tsx`** ```tsx import { FluentProvider } from '@fluentui/react-components'; import { originmainTheme } from '@originmain/ui'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` **[FILE] `packages/ui/src/themes/originmain-theme.ts`** Create the Originmain brand theme using `createLightTheme` from `@fluentui/react-components`. The brand palette anchors at `#0F52BA`. Map this colour to all 16 `BrandVariants` slots (10 through 160 in increments of 10), interpolating from a near-white tint at the 10 end to a near-black shade at the 160 end. Export as `originmainTheme` and also export `originmainDarkTheme` using `createDarkTheme` with the same brand variants. **[CRITICAL]** The `FluentProvider` wraps the entire application. No component outside it uses Fluent 2 hooks or tokens. This is a hard constraint — Fluent 2's context system requires it. ### 1.3 Canvas Viewport **[FILE] `packages/app/src/store/viewport.store.ts`** A Zustand store that manages: ```typescript interface ViewportState { panX: number; // pixels panY: number; // pixels zoom: number; // 0.1 to 4.0 setPan: (x: number, y: number) => void; setZoom: (level: number, originX: number, originY: number) => void; resetViewport: () => void; } ``` The `setZoom` function adjusts `panX` and `panY` to keep the zoom origin stationary on screen (the same point under the cursor before and after zooming). **[FILE] `packages/app/src/components/canvas/Canvas.tsx`** A full-viewport div with `overflow: hidden` and a pointer-event handler that: - On mouse drag (middle button or space+left): updates `setPan` - On scroll wheel: calls `setZoom` with the wheel delta and current cursor position - On pinch gesture (via `onPointerDown` multi-touch detection): calls `setZoom` Inside, render a transform div: ```tsx
{children}
``` **[CRITICAL]** The canvas transform is applied via a CSS matrix to a single container. Never apply individual transforms to artboard elements. This is the only way to achieve 60fps pan/zoom without re-rendering artboard content. ### 1.4 Navigation Architecture **[FILE] `packages/app/src/components/navigator/ArtboardNavigator.tsx`** Uses Fluent 2's `Tree` and `TreeItem` components to render the workspace hierarchy: - Workspace (root) - Project folders - Artboard groups - Individual artboards Each `TreeItem` renders the artboard's origin badge (Linear issue, Git commit, User feedback, Manual) alongside its name. Clicking selects the artboard and centres the canvas viewport on it. ```typescript // Data shape this component expects: interface NavigatorNode { id: string; label: string; type: 'workspace' | 'folder' | 'group' | 'artboard'; originType?: 'linear' | 'git' | 'feedback' | 'manual'; children?: NavigatorNode[]; } ``` **[FILE] `packages/app/src/components/codebase/CodebaseFileTree.tsx`** Uses `@pierre/trees` to render the connected application's repository file tree. This component is entirely separate from the artboard navigator — it lives in a collapsible side panel on the opposite side of the canvas. ```tsx import { FileTree } from '@pierre/trees'; interface CodebaseFileTreeProps { nodes: FileTreeNode[]; // from the renderer's component tree extraction onFileSelect: (path: string) => void; } export function CodebaseFileTree({ nodes, onFileSelect }: CodebaseFileTreeProps) { return (
node.type === 'file' && onFileSelect(node.path)} />
); } ``` **[CRITICAL]** The CSS custom property names above match the ones @pierre/trees exposes for theming. Verify against the @pierre/trees documentation before shipping. Never hardcode colour values — always reference Fluent 2 tokens. ### 1.5 Chrome Layout **[FILE] `packages/app/src/app/(workspace)/layout.tsx`** A three-column layout: left panel (ArtboardNavigator, 240px), centre (Canvas, flex 1), right panel (Inspector + CodebaseFileTree, 320px). All panels use Fluent 2's `makeStyles` from Griffel for styling. No external CSS files. Fluent 2 Toolbar across the top with these tool groups (left to right): Workspace name, Select tool, Pan tool, Artboard tools (new, duplicate, fork), AI tools (Completion Zone trigger), Export, Settings. **[VERIFY]** `pnpm run build` passes. `pnpm run dev` opens the canvas in a browser with visible toolbar, left panel, and right panel. Panning and zooming work. --- ## Layer 2 — Live Rendering Engine **Goal:** Originmain can connect to a running Next.js application, render one of its routes as a Live Artboard in the canvas, and extract the component tree from the rendered iframe. ### 2.1 Package Setup (`packages/renderer`) ```bash cd packages/renderer pnpm add react@19 react-dom@19 pnpm add -D webpack@5 @module-federation/enhanced pnpm add -D typescript zod ``` ### 2.2 The Renderer Protocol **[FILE] `packages/renderer/src/protocol.ts`** Define the message contract between the iframe and the host application: ```typescript export type RendererMessage = | { type: 'COMPONENT_TREE_READY'; payload: ComponentTreeNode } | { type: 'COMPONENT_SELECTED'; payload: { componentId: string; rect: DOMRect } } | { type: 'THEME_INJECTED'; payload: { success: boolean } } | { type: 'ROUTE_CHANGED'; payload: { path: string } } | { type: 'RENDER_ERROR'; payload: { message: string; stack?: string } }; export type HostMessage = | { type: 'SELECT_COMPONENT'; payload: { componentId: string } } | { type: 'INJECT_THEME'; payload: { tokens: Record } } | { type: 'NAVIGATE'; payload: { path: string } } | { type: 'REQUEST_TREE' }; export interface ComponentTreeNode { id: string; // stable ID derived from fiber key + display name displayName: string; // React component display name filePath?: string; // source file path (from sourcemaps) props: Record; // serialisable props only rect: DOMRect; // bounding rect at render time designTokens?: Record; // resolved Fluent 2 token values children: ComponentTreeNode[]; } ``` All messages are validated with Zod schemas before processing. Invalid messages are silently dropped. ### 2.3 The Fiber Tree Injector **[FILE] `packages/renderer/src/fiber-injector.ts`** A script injected into the iframe before the remote application initialises. It hooks into React's DevTools global hook (`__REACT_DEVTOOLS_GLOBAL_HOOK__`) to intercept the Fiber tree after each render commit. From the Fiber tree, it extracts `ComponentTreeNode` records by walking the `child` / `sibling` / `return` fiber links. It posts the extracted tree to the parent frame via `window.parent.postMessage`. **[CRITICAL]** This injector must never import React or any application code — it runs in the iframe's global scope and must be a plain IIFE. It reads from the global hook only, produces a serialisable data structure, and posts it. Do not access `fiber.stateNode` beyond reading its bounding rect. ### 2.4 The Artboard Iframe Wrapper **[FILE] `packages/app/src/components/artboard/ArtboardFrame.tsx`** ```tsx interface ArtboardFrameProps { artboardId: string; remoteUrl: string; // URL of the connected app's Module Federation entry route: string; // path to render (e.g., '/dashboard') width: number; height: number; } ``` The component renders an `