DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path phases/phase-46-operator-layer-frontend/phase-46-cr-operator-layer-frontend-v0_1.md

Phase 46 — Change Request — Operator Layer Frontend

CR identifier. CR-2026-061 Version. 0.1 Date. 2026-05-06 Status. Approved for execution. Author. Claude.ai (CR drafting layer). Operator: Marvin Percival. Executing agent. Claude Code (CC) on DUNIN7-M4. Depends on. Phase 45 complete (tag phase-45-delegation-contract-and-approval-cards). Namespace rename complete (CR-2026-060, tag loomworks-namespace-rename-v0_1). Substrate: 1,773 tests, 26 skipped, Alembic 0061. Workshop frontend (loomworks-ui): 282 vitest, 29 files, eslint/tsc/build clean. Informed by. Phase 46 scoping note v0.1 (twelve decisions settled). Operator Layer Discovery v0.4 (§§8–10, §15 Arc 3). Product identity standing note v0.1. Agent fabric investigation v0.1 (data-driven rendering caveat). Brand guide v0.15 + DESIGN.md v0.1. Phase 40 CR v0.2 (Orchestration API schemas). Phase 44 CR v0.1 (SSE, notification surface). Phase 45 CR v0.1 (approval cards). Phase 42 CR v0.1 (converse pipeline, conversation turns).


1. Purpose

Phase 46 builds the Operator Layer frontend — the product surface described in the Operator Layer Discovery v0.4. A new Next.js application in the freed DUNIN7/loomworks namespace. Four surfaces: Companion chat, Dashboard, Inbox (notifications + approval cards), Library. The engine is invisible. The Companion is the product.

This is Arc 3 from the Discovery document §15. The engine prerequisites (Phases 34–40) are complete. The Companion brain (Phases 41–45, Arc 2) is complete. The Orchestration API (Phase 40) is the consumption contract. Phase 46 builds the experience surface that consumes it.

One narrow substrate addition: a read-only conversation history endpoint (GET /operator/conversation-history) over the existing conversation_turns table. No migration. No new model. This is the sole substrate exception — justified by the product identity standing note's "the companion is glad to see you when you return."

Repos affected:


2. Scoping decisions adopted

All twelve decisions from the scoping note v0.1 are settled.

| Decision | Resolution | |----------|-----------| | D1 — Layout | Reading C: Companion as landing, Dashboard as home. First visit / return-after-absence lands on Companion (welcome, summary). In-flow, Dashboard is persistent home with Companion always reachable. | | D2 — Scaffold | Mirror Workshop stack: Next.js (App Router), TypeScript, Vitest + RTL, Playwright, ESLint, TailwindCSS. | | D3 — Design system | Option A: fresh components from DESIGN.md tokens. No Workshop dependency. Purpose-built for Operator surfaces. | | D4 — Auth | Dev-auth endpoint + CORS. Operator Layer on localhost:3001, engine on localhost:8000. Session cookie with credentials: 'include'. | | D5 — Chat rendering | Turn-based. "Thinking…" indicator while request in flight. No simulated streaming. | | D6 — Notifications | Data-driven rendering. Single NotificationCard renders from schema. Unknown kinds render as generic cards. | | D7 — Dashboard updates | SSE-triggered refetch. Any SSE event triggers dashboard re-fetch. 30-second polling fallback when SSE drops. | | D8 — Library scope | Minimum viable: list, download, project filter, "make a new version" → chat. Version history and preview deferred. | | D9 — Conversation history | Path B: add read-only endpoint in substrate. Narrow exception to "no engine changes." | | D10 — Mobile | Desktop-first, responsive from start. Single-column on mobile, collapsible navigation. | | D11 — Companion name | Chat header + author labels. Renaming conversational only (Phase 42 rename_companion intent). | | D12 — Phase scope | Monolithic. One phase, one CR, one tag. |


3. Pre-flight and baseline

3.1 Baseline

3.2 Pre-flight checklist

Step 0 confirms:

  1. /Users/dunin7/loomworks does not exist (Phase 46 creates it).
  2. GitHub namespace DUNIN7/loomworks is available (freed by CR-2026-060).
  3. Substrate main is at or ahead of Phase 45 head. [CC verifies: git log on loomworks-engine main.]
  4. Orchestration API endpoints exist and respond:

[CC verifies: using dev-auth session cookie and curl or httpie. The dev server must be running.]

  1. GET /me returns current person with companion_name field.
  2. Workshop frontend still builds: cd /Users/dunin7/loomworks-ui && npm run lint && npx tsc --noEmit && npm run build && npm run test. [CC runs.]
  3. Node.js version ≥ 18. npm version ≥ 9. [CC verifies.]
  4. conversation_turns table exists in the substrate database. [CC verifies: via alembic or direct query on loomworks-engine.]

3.3 CR archival

Archive this CR to docs/phase-crs/phase-46-cr-operator-layer-frontend-v0_1.md in the loomworks-engine repo at Step 0. (Substrate is the CR archive location even though Phase 46 is primarily a frontend phase.)


4. The consumption contract

Phase 46 consumes these endpoints exclusively. No engine API calls cross the vocabulary wall.

4.1 Orchestration API (Phase 40)

| Endpoint | Method | Purpose | |----------|--------|---------| | /operator/dashboard | GET | Aggregated cross-engagement state | | /operator/inbox | GET | Actionable items with typed verbs | | /operator/inbox/{item_id}/respond | POST | Execute Operator action | | /operator/library | GET | Paginated artifacts | | /operator/library/{artifact_id}/download | GET | Download artifact binary | | /operator/converse | POST | Companion brain — classify, route, respond | | /operator/projects/{id}/render_with_review | POST | Composition creation | | /operator/projects/{id}/story | GET | Stub (501) | | /operator/projects/{id}/implied_specifications | GET | Stub (501) |

4.2 Notification / SSE (Phases 44–45)

| Endpoint | Method | Purpose | |----------|--------|---------| | /operator/notifications/stream | GET (SSE) | Push channel | | /operator/notifications | GET | List notifications | | /operator/notifications/{id}/seen | PATCH | Mark seen | | /operator/notifications/{id}/dismiss | PATCH | Dismiss | | /operator/notifications/{id}/approve | POST | Approve Companion action | | /operator/notifications/{id}/decline | POST | Decline Companion action |

4.3 Person (Phases 14, 41)

| Endpoint | Method | Purpose | |----------|--------|---------| | /me | GET | Current person (display_name, companion_name) | | /me/companion-name | PATCH | Rename Companion |

4.4 Auth (dev mode)

| Endpoint | Method | Purpose | |----------|--------|---------| | /auth/dev/issue-session | POST | Mint session cookie (development only) |

4.5 Conversation history (new — Phase 46 substrate addition)

| Endpoint | Method | Purpose | |----------|--------|---------| | /operator/conversation-history | GET | Recent conversation turns for the authenticated person |

This is the narrow substrate addition (§12).


5. Layout and navigation (D1 settled)

5.1 The D1 synthesis: Companion as landing, Dashboard as home

Landing state (first visit or return-after-absence). The Operator sees the Companion. Full-screen conversation. The Companion greets, summarizes what happened while the Operator was away (using notification data), and offers to help. Navigation to Dashboard, Inbox, Library is available but secondary.

In-flow state. Once the Operator navigates away from the Companion, the Dashboard becomes the home surface. The Companion is always reachable — a persistent chat trigger in the navigation chrome opens the Companion as a side panel or navigates to the full chat view.

Route structure:


/                    → Landing / Companion (full-screen conversation)
/dashboard           → Dashboard (three zones: Active, Needs You, Recent)
/inbox               → Inbox (notifications + approval cards)
/library             → Library (artifacts)
/chat                → Companion chat (full-screen, for returning to conversation)
/chat?project={id}   → Companion chat with project context

Navigation bar. Persistent across all routes except the landing. Contains:

The landing route (/) does not show the navigation bar — the Companion fills the screen. After the first navigation event, the bar appears and persists.

5.2 Companion placement

Two modes for the Companion chat:

Full-screen mode (/ and /chat). The conversation fills the available space. Message input at the bottom. Conversation history scrolls above.

Side-panel mode (from any other route). The Companion opens as a slide-over panel from the right. The underlying surface (Dashboard, Inbox, Library) remains visible. The panel is dismissible. This lets the Operator ask the Companion questions while looking at the Dashboard or Library without navigating away.

[CC determines: whether the side panel is a React portal, a route-level layout component, or a context-driven overlay. The requirement is: it works from any route, it shows the same conversation, and it doesn't lose state when opened/closed.]


6. Repo scaffold (D2 settled)

6.1 Create the repo


mkdir /Users/dunin7/loomworks
cd /Users/dunin7/loomworks
npx create-next-app@latest . --typescript --eslint --tailwind --app --src-dir --no-import-alias

[CC verifies: the create-next-app version and flags. If the installed version differs, adjust flags accordingly. The goal is: App Router, TypeScript, TailwindCSS, src/ directory, ESLint.]

6.2 Testing setup


npm install --save-dev vitest @testing-library/react @testing-library/jest-dom @vitejs/plugin-react jsdom
npm install --save-dev @playwright/test

Add vitest.config.ts and playwright.config.ts matching the Workshop's configuration pattern. [CC references: /Users/dunin7/loomworks-ui/vitest.config.ts and /Users/dunin7/loomworks-ui/playwright.config.ts for the patterns.]

6.3 Package.json scripts


{
  "scripts": {
    "dev": "next dev -p 3001",
    "build": "next build",
    "start": "next start -p 3001",
    "lint": "next lint",
    "test": "vitest run",
    "test:watch": "vitest",
    "test:e2e": "playwright test"
  }
}

Port 3001 (Workshop is on 3000).

6.4 README


# Loomworks — Operator Layer

The Operator's product surface. Consumes the Orchestration API (`/operator/...` endpoints).
The engine is invisible. The Companion is the product.

## Development

npm install npm run dev # localhost:3001



Requires: loomworks-engine running on localhost:8000.
Dev auth: `curl -X POST http://localhost:8000/auth/dev/issue-session -H 'Content-Type: application/json' -d '{"person_id": "<uuid>"}' -c cookies.txt`

## Testing

npm run test # Vitest component tests npm run test:e2e # Playwright E2E (requires engine running) npm run lint # ESLint npx tsc --noEmit # Type check npm run build # Production build


6.5 Git and GitHub


git init
git add .
git commit -m "Phase 46 step 1: repo scaffold"
gh repo create DUNIN7/loomworks --private --source=. --push

[CC verifies: gh CLI is authenticated for the DUNIN7 org.]


7. Design system (D3 settled)

7.1 CSS custom properties

Create src/styles/tokens.css with all DESIGN.md tokens as CSS custom properties:


:root {
  /* Press & ink */
  --color-press-ink: #1A1814;
  --color-type-metal: #4A4843;
  --color-ink-faint: #8A847A;
  --color-brass: #A89B7E;

  /* Accent */
  --color-iron-oxide: #B55537;
  --color-iron-oxide-hot: #9A4A30;

  /* Paper */
  --color-cartridge: #F2EDE0;
  --color-bleached: #FFFCF5;
  --color-vellum: #E8E1CE;

  /* Rules */
  --color-rule: #E0DACC;
  --color-rule-soft: #EDE7D7;

  /* Typography */
  --font-serif: 'IBM Plex Serif', Georgia, serif;
  --font-sans: 'Inter', -apple-system, sans-serif;
  --font-mono: 'IBM Plex Mono', monospace;

  /* Spacing */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.25rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  --space-10: 2.5rem;
  --space-12: 3rem;
  --space-16: 4rem;
  --space-20: 5rem;

  /* Radii */
  --rounded-sm: 3px;
  --rounded-md: 4px;

  /* Motion */
  --duration-fast: 150ms;
  --duration-normal: 250ms;
  --duration-slow: 400ms;
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --ease-in: cubic-bezier(0.4, 0, 1, 1);
}

7.2 Font loading

Load IBM Plex Serif (400, 400 italic, 500), Inter (400, 500), and IBM Plex Mono (400, 500) via next/font/google or @fontsource. [CC determines: the best loading strategy for Next.js App Router. next/font/google is preferred for automatic optimization.]

7.3 Tailwind configuration

Extend Tailwind config to use the CSS custom properties as colors and spacing values. This lets components use Tailwind classes that map to the Loomworks design system.


// tailwind.config.ts (excerpt)
theme: {
  extend: {
    colors: {
      'press-ink': 'var(--color-press-ink)',
      'type-metal': 'var(--color-type-metal)',
      'ink-faint': 'var(--color-ink-faint)',
      'brass': 'var(--color-brass)',
      'iron-oxide': 'var(--color-iron-oxide)',
      'cartridge': 'var(--color-cartridge)',
      'bleached': 'var(--color-bleached)',
      'vellum': 'var(--color-vellum)',
      'rule': 'var(--color-rule)',
    },
    fontFamily: {
      serif: ['var(--font-serif)'],
      sans: ['var(--font-sans)'],
      mono: ['var(--font-mono)'],
    },
    borderRadius: {
      sm: 'var(--rounded-sm)',
      md: 'var(--rounded-md)',
    },
  },
}

7.4 Base components

Build from DESIGN.md token definitions. Each component is a React component in src/components/ui/:

Button — four variants: primary (press-ink bg, cartridge text), secondary (bleached bg, rule border), ghost (transparent), amend (iron-oxide bg, cartridge text — amend/escalate actions only). Hover states per DESIGN.md.

Card — bleached bg, rule border, rounded-md. No shadow (depth through paper-ground hierarchy).

Badge — filled (vellum bg, press-ink text) and outlined (transparent, rule border, type-metal text). Optional state dot (6px circle).

Input — bleached bg, rule border. Focus: press-ink border, no ring.

NotificationCard — the data-driven renderer (§10). Built as a composition of Card + conditional content.

[CC builds: each component as a separate file. Props typed with TypeScript. No external component library — these are the components.]

7.5 Global styles


8. Auth flow (D4 settled)

8.1 API client

Create src/lib/api.ts — a thin fetch wrapper that:


const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';

export async function api<T>(path: string, options?: RequestInit): Promise<T> {
  const res = await fetch(`${API_BASE}${path}`, {
    ...options,
    credentials: 'include',
    headers: {
      'Content-Type': 'application/json',
      ...options?.headers,
    },
  });
  if (res.status === 401) {
    window.location.href = '/auth';
    throw new Error('Unauthorized');
  }
  if (!res.ok) throw new Error(`API error: ${res.status}`);
  return res.json();
}

8.2 CORS configuration (substrate side)

[CC verifies: the existing CORS configuration in the engine's app factory. If CORS is not configured for localhost:3001, CC adds it. The CORS configuration must allow credentials (cookies) from localhost:3001.]

The engine must accept:

8.3 Dev auth page

A minimal /auth route for development:


Enter your person UUID: [________]  [Sign In]

On submit: calls POST /auth/dev/issue-session with the UUID, receives the session cookie, redirects to /. This page exists only in development. Production auth (passkey/SSO) is a future phase.

[CC determines: whether to gate this page behind a NEXT_PUBLIC_DEV_MODE env var or detect the dev-auth endpoint's availability. The simplest approach is: always show the page, and if the dev-auth endpoint doesn't exist (production), it 404s and the user sees an error message.]

8.4 Auth context

A React context provider that loads GET /me on mount and exposes the current person (display_name, companion_name) to all components. If the request 401s, redirect to /auth.


// src/providers/AuthProvider.tsx
interface PersonContext {
  displayName: string;
  companionName: string;
  isLoading: boolean;
}

9. SSE connection (D7 settled)

9.1 SSE provider

Create src/providers/SSEProvider.tsx — a React context that manages the SSE connection.

Behavior:

Event types consumed:

9.2 SSE-triggered dashboard refetch

When any notification_created or notification_updated event arrives, the SSE provider fires a "dashboard-stale" signal. The Dashboard component subscribes to this signal and refetches GET /operator/dashboard.

9.3 Polling fallback

If the SSE connection drops and reconnection fails after 3 attempts, the provider switches to 30-second polling of GET /operator/notifications and GET /operator/dashboard. When SSE reconnects, polling stops.


10. Companion chat (D5, D9, D11 settled)

10.1 Chat component

src/components/chat/ChatView.tsx — the primary Companion interaction surface.

Structure:

Turn rendering:

10.2 Conversation flow

Sending a message:

  1. Append the Operator's message to the local conversation state.
  2. Show "[Companion] is thinking…" indicator.
  3. Call POST /operator/converse with { message, project_id }.
  4. On response: append the companion message to conversation state. Show side effects as inline status messages. Show suggested actions as tappable chips below the companion message.
  5. If the response includes project_id (new project created): update the URL to /chat?project={id}.

Suggested actions: The suggested_actions list from ConverseResponse renders as tappable text chips below the companion message. Tapping a chip sends it as the next message.

Side effects: The side_effects list renders as subtle status messages between turns: "Started a new project" or "Project finalized." Styled as caption text in ink-faint.

Draft specification: When draft_specification is present in the response (during project creation), render it as an expandable card below the companion message — the live seed document in Operator vocabulary. Styled as a vellum card (set-aside ground).

10.3 Project context

The chat supports project-scoped conversation via ?project={id}. When a project ID is present:

When no project ID is present:

10.4 Conversation history (D9 — substrate addition)

On page load (or when switching project context), the chat fetches conversation history from GET /operator/conversation-history?project_id={id}&limit=50. This endpoint is the narrow substrate addition (§12).

History loads above the current conversation. Older messages are dimmed slightly (ink-faint text) to distinguish them from the current session.


11. Dashboard (D7 settled)

11.1 Dashboard component

src/app/dashboard/page.tsx — the three-zone layout.

Zone 1 — Active. "What's happening now." Cards for in-progress work across all projects. Each card shows: project name, activity label ("Writing your story about the lost duckling…"), started_at as relative time ("30 seconds ago"), progress hint if present. Styled as bleached cards on cartridge ground.

Zone 2 — Needs You. "Items waiting for you." Cards for actionable items. Each card shows: project name, item label, detail, arrived_at as relative time. Tapping a card navigates to the Inbox with that item focused. Shows count: "3 items need your attention."

Zone 3 — Recently Finished. "Completed artifacts." Cards for finished work. Each card shows: project name, artifact label, completed_at as relative time, download button (if download_url present). Tapping a card navigates to the Library.

Empty states. Each zone has an empty state:

Per the companion identity: empty states are warm, not clinical. The Companion voice appears even in empty states.

11.2 Dashboard greeting

At the top of the Dashboard, a greeting line: "Welcome back, [display_name]." Below it, a one-line summary derived from the dashboard data: "2 things in progress, 1 item needs your attention." Styled as body text in type-metal.

11.3 SSE-triggered refetch

The Dashboard subscribes to the SSE provider's "dashboard-stale" signal. When triggered, it refetches GET /operator/dashboard and updates all three zones.


12. Narrow substrate addition — conversation history endpoint

12.1 The endpoint

Add to the Orchestration API:

GET /operator/conversation-history

Query params:

Response:


class ConversationTurn(BaseModel):
    turn_id: UUID
    role: Literal["operator", "companion"]  # Operator vocabulary
    message: str
    classified_intent: str | None = None
    created_at: datetime

class ConversationHistoryResponse(BaseModel):
    turns: list[ConversationTurn]
    has_more: bool

12.2 Implementation

  1. Authenticate via get_current_person.
  2. Query conversation_turns table filtered by person_id and optionally engagement_id (resolved from project_id via membership check).
  3. Order by created_at descending, apply limit and before-cursor.
  4. Translate role values: the conversation_turns table stores roles as "operator" and "companion" already (Phase 42 records them this way). [CC verifies: the actual role values stored in conversation_turns.]
  5. Return ConversationHistoryResponse.

12.3 Router

Add to src/loomworks/orchestration/routers/ as conversation_history.py. Register in the app factory alongside existing orchestration routers.

12.4 Tests

Estimated: 4 new substrate tests.

12.5 Vocabulary wall

The ConversationTurn and ConversationHistoryResponse schemas live in orchestration/schemas.py. The vocabulary-wall test covers them automatically.


13. Inbox / notification surface (D6 settled)

13.1 Data-driven notification rendering

The core rendering principle from the agent fabric investigation: the frontend does not switch on specific notification kinds. It renders from structured data.

src/components/notifications/NotificationCard.tsx:


interface NotificationCardProps {
  title: string;
  body: string;
  timestamp: string;
  projectId?: string;
  projectName?: string;
  verbs?: string[];        // Rendered as action buttons
  approvalStatus?: string; // 'pending' | 'approved' | 'declined' | 'executing' | 'complete'
  downloadUrl?: string;
  onDismiss: () => void;
  onVerb: (verb: string) => void;
}

Rendering rules:

Unknown data: fields not in the interface are ignored. Notifications with kinds the frontend has never seen render as generic cards with title + body + timestamp + dismiss. This is the extensibility guarantee.

13.2 Inbox page

src/app/inbox/page.tsx — full paginated notification list.

Features:

13.3 Notification badge

In the navigation bar: a numeric badge showing the count of unseen notifications. Hidden when zero ("only show what is available").

Source: initial count from GET /operator/notifications (count of items where status is pending or delivered). Incremented on SSE notification_created. Decremented on seen/dismiss.

13.4 Notification drawer (side-panel access)

Tapping the notification badge from any route opens a notification drawer (slide-over from right, similar to the Companion side panel). Shows the most recent notifications without navigating to /inbox. "See all" link navigates to /inbox.


14. Library (D8 settled)

14.1 Library page

src/app/library/page.tsx — artifact browser.

Features:

Empty state: "No artifacts yet. Start a conversation to create your first project."

14.2 Artifact card


interface ArtifactCardProps {
  artifactId: string;
  title: string;
  projectName: string;
  format: string;
  status: 'ready' | 'archived';
  createdAt: string;
  downloadUrl: string | null;
}

Styled as a bleached card. Format shown as a badge (outlined variant). Archived artifacts shown with reduced opacity.


15. Navigation and layout

15.1 App layout

src/app/layout.tsx — the root layout:

15.2 Navigation bar

src/components/nav/NavBar.tsx:

Background: bleached. Bottom border: 1px rule color. Padding: space-4.

15.3 Responsive behavior (D10)

Desktop (≥1024px): Navigation bar horizontal. Dashboard zones side-by-side or stacked. Chat full-width. Library grid layout.

Tablet (768–1023px): Navigation bar horizontal, condensed. Dashboard zones stacked. Chat full-width.

Mobile (<768px): Navigation collapses to a bottom tab bar (Dashboard, Inbox, Library, Chat). Notification badge on the Inbox tab. No side-panel Companion — chat is always full-screen.

[CC determines: breakpoint implementation — Tailwind responsive prefixes (md:, lg:) are the natural approach. The bottom tab bar on mobile is the main structural divergence.]


16. Test suite

16.1 Substrate tests (conversation history endpoint)

4 tests per §12.4.

16.2 Frontend component tests (Vitest + RTL)

Chat:

  1. test_chat_renders_conversation_turns — mock conversation history, verify turns displayed with correct author labels.
  2. test_chat_sends_message — type message, submit, verify API call to /operator/converse.
  3. test_chat_shows_thinking_indicator — submit message, verify indicator visible before response.
  4. test_chat_displays_companion_response — mock API response, verify companion message rendered.
  5. test_chat_renders_suggested_actions — mock response with suggested_actions, verify chips rendered.
  6. test_chat_renders_draft_specification — mock response with draft_specification, verify expandable card.

Dashboard:

  1. test_dashboard_renders_three_zones — mock dashboard response, verify Active, Needs You, Recent zones.
  2. test_dashboard_empty_states — mock empty response, verify empty state messages.
  3. test_dashboard_active_card_content — verify project name, activity label, relative time.
  4. test_dashboard_needs_you_navigates_to_inbox — click needs-you card, verify navigation.

Inbox/Notifications:

  1. test_notification_card_renders_data_driven — pass arbitrary fields, verify rendering.
  2. test_notification_card_unknown_kind — notification with unknown kind renders title + body + dismiss.
  3. test_approval_card_approve_button — click Approve, verify API call to /operator/notifications/{id}/approve.
  4. test_approval_card_decline_button — click Decline, verify API call.
  5. test_notification_badge_hidden_when_zero — zero unseen, badge not rendered.
  6. test_notification_badge_shows_count — nonzero unseen, badge visible with count.

Library:

  1. test_library_renders_artifacts — mock library response, verify artifact cards.
  2. test_library_download_button — verify download link points to correct endpoint.
  3. test_library_filter_by_project — select project filter, verify API call includes project_id.
  4. test_library_make_new_version — click button, verify navigation to /chat?project={id}.

Auth:

  1. test_auth_redirect_on_401 — mock 401 response, verify redirect to /auth.
  2. test_auth_context_provides_person — mock /me, verify displayName and companionName available.

Navigation:

  1. test_nav_renders_links — verify Dashboard, Inbox, Library links.
  2. test_nav_companion_trigger — verify companion trigger exists.

Estimated: 24 frontend component tests.

16.3 Vocabulary wall (frontend)

  1. test_no_engine_terms_in_ui_text — scan all component files for forbidden terms (engagement, assertion, shape_event, render_event, manifestation, shaping, specialist, materializer, seed, normative_force). This is a static analysis test, not a runtime test.

16.4 Responsive

  1. test_mobile_viewport_renders — render each page at 375px width, verify no overflow or breakage. (Vitest with viewport mock or Playwright.)

Estimated total: ~26 frontend tests + 4 substrate tests = ~30 new tests.


17. Step structure

Auto-mode posture: Steps 0–14 auto-mode-proceed. Checkpoint A halts for Operator evaluation. Checkpoint B halts for tag.

| Step | What | Mode | |------|------|------| | 0 | Pre-flight + CR archival (substrate repo). CORS check/addition. | Auto | | 1 | Repo scaffold (§6). Create repo, install deps, configure tooling. Push to GitHub. | Auto | | 2 | Design system foundation (§7). CSS tokens, fonts, Tailwind config, base components (Button, Card, Badge, Input). | Auto | | 3 | Auth flow (§8). API client, dev auth page, auth context provider. | Auto | | 4 | SSE connection (§9). SSE provider, event distribution, reconnect logic, polling fallback. | Auto | | 5 | Substrate: conversation history endpoint (§12). Router, schema, tests. Push to loomworks-engine. | Auto | | 6 | Companion chat (§10). ChatView, conversation flow, history loading, project context, thinking indicator, suggested actions, draft specification card. | Auto | | 7 | Dashboard (§11). Three zones, greeting, empty states, SSE-triggered refetch. | Auto | | 8 | Inbox / notifications (§13). NotificationCard (data-driven), notification list, badge, drawer, approve/decline. | Auto | | 9 | Library (§14). Artifact list, download, project filter, "make a new version." | Auto | | 10 | Landing page (§5.1). Companion full-screen landing. Greeting logic. | Auto | | 11 | Navigation (§15). NavBar, route structure, Companion side panel, responsive layout. | Auto | | 12 | Frontend tests (§16.2–16.4). | Auto | | 13 | Frontend verification: npm run lint && npx tsc --noEmit && npm run build && npm run test. | Auto | | 14 | Substrate verification: cd /Users/dunin7/loomworks-engine && uv run pytest -v. | Auto | | A | Checkpoint A — Operator evaluates all four surfaces. | Checkpoint | | B | Checkpoint B — Tag DUNIN7/loomworks as phase-46-operator-layer-frontend. Tag substrate loomworks-engine as phase-46-conversation-history. Implementation notes. | Checkpoint |


18. Acceptance gate

  1. Repo DUNIN7/loomworks exists on GitHub. npm run build succeeds.
  2. Design system: DESIGN.md tokens as CSS custom properties. IBM Plex Serif, Inter, IBM Plex Mono loaded. Warm paper grounds. No pure white (#FFFFFF) anywhere.
  3. Auth: dev-auth page works. Session cookie flows to API. Auth context provides person.
  4. SSE: connection opens on auth. Events distributed to subscribers. Reconnects on drop. Falls back to polling.
  5. Companion chat: send message → "[Companion] is thinking…" → response rendered. Conversation history loads from substrate endpoint. Suggested actions render as chips. Draft specification renders as expandable card. Project context via URL param.
  6. Dashboard: three zones populate from GET /operator/dashboard. SSE events trigger refetch. Empty states render warmly. Greeting shows display name.
  7. Inbox: notifications render as data-driven cards. Unknown kinds render generically. Approval cards have Approve/Decline. Badge shows unseen count. Badge hidden when zero.
  8. Library: artifacts listed. Download works. Project filter works. "Make a new version" navigates to chat.
  9. Landing: first visit shows Companion full-screen. Navigation bar hidden on landing.
  10. Navigation: all four surfaces reachable. Companion side panel accessible from any surface.
  11. Responsive: surfaces render without breakage at 375px.
  12. Vocabulary wall: no engine terms visible in any rendered surface.
  13. Substrate: conversation history endpoint works. 4 new tests pass. Total substrate tests: 1,777+.
  14. Frontend: all vitest tests pass. lint + tsc + build clean.
  15. Workshop unchanged: cd /Users/dunin7/loomworks-ui && npm run lint && npx tsc --noEmit && npm run build && npm run test still clean.

19. Post-CR state


20. What this CR does not build


21. Kickoff prompt for the Claude Code session


Read the Change Request document at the path I supply below. This is
CR-2026-061 v0.1, the Phase 46 Change Request. You are the executing
agent named in the CR.

CR path: ~/Downloads/phase-46-cr-operator-layer-frontend-v0_1.md

Phase 46 builds the Operator Layer frontend — the product surface
the Operator uses. A new Next.js application in the freed
DUNIN7/loomworks namespace. Four surfaces: Companion chat, Dashboard,
Inbox (notifications + approval cards), Library. Consumes only
/operator/... endpoints. The engine is invisible. The Companion is
the product.

Three repos involved:
  - DUNIN7/loomworks — CREATED by this phase. Operator Layer frontend.
  - DUNIN7/loomworks-engine — narrow substrate addition (one read-only
    conversation history endpoint + 4 tests). No migration.
  - DUNIN7/loomworks-ui — UNCHANGED.

Key points:
  - Repo scaffold: Next.js App Router, TypeScript, Vitest + RTL,
    Playwright, TailwindCSS. Port 3001.
  - Design system: DESIGN.md tokens as CSS custom properties. Fresh
    component library (Button, Card, Badge, Input, NotificationCard).
    IBM Plex Serif + Inter + IBM Plex Mono. Warm paper, no pure white.
  - Auth: session cookie with credentials:include. Dev-auth page for
    development. CORS for localhost:3001 on the engine.
  - SSE: EventSource with withCredentials. Reconnect with backoff.
    30s polling fallback. Dashboard refetch on notification events.
  - Chat: turn-based rendering. "[Companion] is thinking..." indicator.
    Conversation history from new substrate endpoint. Suggested actions
    as chips. Draft specification as expandable card.
  - Dashboard: three zones (Active, Needs You, Recently Finished).
    SSE-triggered refetch. Warm empty states.
  - Inbox: data-driven NotificationCard renders from schema. Unknown
    kinds render generically (extensibility guarantee). Approval cards
    with Approve/Decline. Badge hidden when zero.
  - Library: artifact list, download, project filter. "Make a new
    version" navigates to chat.
  - Layout: Reading C — Companion as landing, Dashboard as home.
    NavBar on all routes except landing. Companion side panel from
    any route. Responsive (mobile bottom tab bar).
  - Substrate: one new endpoint (GET /operator/conversation-history),
    4 new tests, no migration. CORS addition if needed.
  - ~26 frontend tests + 4 substrate tests.
  - Two checkpoints: A for Operator evaluation, B for tag.

Run pre-flight (Step 0) per Section 3.2. Eight pre-flight items.
Archive this CR to docs/phase-crs/ in the engine repo.

Per Section 17, fifteen steps with two checkpoints. Step 5 is the
substrate work (conversation history endpoint + CORS). All other
steps are frontend work on the new repo.

Pre-flight surprises stop at Step 0 and drive a CR amendment.

Implementation notes at Checkpoint B:
docs/phase-impl-notes/phase-46-implementation-notes-v0_1.md
(in the engine repo — implementation notes for all phases live there)

DUNIN7 — Done In Seven LLC — Miami, Florida Phase 46 CR — v0.1 — 2026-05-06