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).
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:
DUNIN7/loomworks — created by this phase. The Operator Layer frontend.DUNIN7/loomworks-engine — narrow addition (one read-only endpoint + tests). No migration.DUNIN7/loomworks-ui — unchanged.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. |
loomworks-engine): 1,773 tests passed, 26 skipped. Alembic 0061. Tag: loomworks-namespace-rename-v0_1 at 8dc55f8. Main at 86e3536 (fast-forwarded to Phase 45 head).loomworks-ui): 282 vitest, 29 files. eslint 0 errors / 1 pre-existing warning. tsc/build clean.loomworks): does not exist. Phase 46 creates it./Users/dunin7/loomworks-engine (substrate), /Users/dunin7/loomworks-ui (Workshop). /Users/dunin7/loomworks does not exist.Step 0 confirms:
/Users/dunin7/loomworks does not exist (Phase 46 creates it).DUNIN7/loomworks is available (freed by CR-2026-060).main is at or ahead of Phase 45 head. [CC verifies: git log on loomworks-engine main.]GET /operator/dashboard returns 200.GET /operator/inbox returns 200.GET /operator/library returns 200.POST /operator/converse returns 200 with a message body.GET /operator/notifications returns 200.GET /operator/notifications/stream returns SSE event stream.
[CC verifies: using dev-auth session cookie and curl or httpie. The dev server must be running.]
GET /me returns current person with companion_name field.cd /Users/dunin7/loomworks-ui && npm run lint && npx tsc --noEmit && npm run build && npm run test. [CC runs.][CC verifies.]conversation_turns table exists in the substrate database. [CC verifies: via alembic or direct query on loomworks-engine.]
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.)
Phase 46 consumes these endpoints exclusively. No engine API calls cross the vocabulary wall.
| 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) |
| 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 |
| Endpoint | Method | Purpose |
|----------|--------|---------|
| /me | GET | Current person (display_name, companion_name) |
| /me/companion-name | PATCH | Rename Companion |
| Endpoint | Method | Purpose |
|----------|--------|---------|
| /auth/dev/issue-session | POST | Mint session cookie (development only) |
| Endpoint | Method | Purpose |
|----------|--------|---------|
| /operator/conversation-history | GET | Recent conversation turns for the authenticated person |
This is the narrow substrate addition (§12).
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:
/chat).
The landing route (/) does not show the navigation bar — the Companion fills the screen. After the first navigation event, the bar appears and persists.
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.]
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.]
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.]
{
"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).
# 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
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.]
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);
}
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.]
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)',
},
},
}
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.]
--color-cartridge (not white — "do not use pure white anywhere").--color-press-ink.prefers-reduced-motion: collapse transitions to zero.
Create src/lib/api.ts — a thin fetch wrapper that:
credentials: 'include' on every request (sends session cookie).Content-Type: application/json on POST/PATCH.
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();
}
[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:
http://localhost:3001
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.]
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;
}
Create src/providers/SSEProvider.tsx — a React context that manages the SSE connection.
Behavior:
EventSource to ${API_BASE}/operator/notifications/stream with withCredentials: true on auth.Event types consumed:
notification_created — new notification. Increments badge count. Triggers dashboard refetch.notification_updated — notification state changed (seen, dismissed, approved, declined). Refreshes notification list.heartbeat — keep-alive. No UI action.
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.
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.
src/components/chat/ChatView.tsx — the primary Companion interaction surface.
Structure:
Turn rendering:
Sending a message:
POST /operator/converse with { message, project_id }.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).
The chat supports project-scoped conversation via ?project={id}. When a project ID is present:
project_id in the request.When no project ID is present:
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.
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.
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.
The Dashboard subscribes to the SSE provider's "dashboard-stale" signal. When triggered, it refetches GET /operator/dashboard and updates all three zones.
Add to the Orchestration API:
GET /operator/conversation-history
Query params:
?project_id (optional UUID) — filter to a specific project's conversation. If omitted, returns general (non-project) conversation turns.?limit (int, default 50, max 100) — number of turns to return.?before (datetime, optional) — pagination cursor.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
get_current_person.conversation_turns table filtered by person_id and optionally engagement_id (resolved from project_id via membership check).created_at descending, apply limit and before-cursor.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.]ConversationHistoryResponse.
Add to src/loomworks/orchestration/routers/ as conversation_history.py. Register in the app factory alongside existing orchestration routers.
test_conversation_history_returns_recent_turns — create conversation turns, fetch, verify order and content.test_conversation_history_project_filter — turns from other projects excluded.test_conversation_history_pagination — ?before cursor returns older turns.test_conversation_history_auth_required — 401 without session.Estimated: 4 new substrate tests.
The ConversationTurn and ConversationHistoryResponse schemas live in orchestration/schemas.py. The vocabulary-wall test covers them automatically.
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:
projectId is present. Navigates to /chat?project={id}.verbs array. Button label IS the verb text, capitalized. Button style: secondary.approvalStatus === 'pending'. Two buttons: "Approve" (primary) and "Decline" (secondary). When approved or declined, the card shows a completion state.downloadUrl is present. Styled as a ghost button with download icon.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.
src/app/inbox/page.tsx — full paginated notification list.
Features:
GET /operator/notifications on mount.NotificationCard.?cursor from the response.PATCH /operator/notifications/{id}/seen for all visible unseen notifications.PATCH /operator/notifications/{id}/dismiss, removes card from list.POST /operator/notifications/{id}/approve, updates card to executing/complete state.POST /operator/notifications/{id}/decline, updates card to declined state.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.
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.
src/app/library/page.tsx — artifact browser.
Features:
GET /operator/library on mount.?project_id to the query.GET /operator/library/{artifact_id}/download (binary file download)./chat?project={projectId} and sends an initial message: "I'd like to revise [artifact title]."Empty state: "No artifacts yet. Start a conversation to create your first project."
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.
src/app/layout.tsx — the root layout:
AuthProvider — loads person, gates authenticated routes.SSEProvider — opens SSE connection on auth./ landing and /auth).
src/components/nav/NavBar.tsx:
Background: bleached. Bottom border: 1px rule color. Padding: space-4.
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.]
4 tests per §12.4.
Chat:
test_chat_renders_conversation_turns — mock conversation history, verify turns displayed with correct author labels.test_chat_sends_message — type message, submit, verify API call to /operator/converse.test_chat_shows_thinking_indicator — submit message, verify indicator visible before response.test_chat_displays_companion_response — mock API response, verify companion message rendered.test_chat_renders_suggested_actions — mock response with suggested_actions, verify chips rendered.test_chat_renders_draft_specification — mock response with draft_specification, verify expandable card.Dashboard:
test_dashboard_renders_three_zones — mock dashboard response, verify Active, Needs You, Recent zones.test_dashboard_empty_states — mock empty response, verify empty state messages.test_dashboard_active_card_content — verify project name, activity label, relative time.test_dashboard_needs_you_navigates_to_inbox — click needs-you card, verify navigation.Inbox/Notifications:
test_notification_card_renders_data_driven — pass arbitrary fields, verify rendering.test_notification_card_unknown_kind — notification with unknown kind renders title + body + dismiss.test_approval_card_approve_button — click Approve, verify API call to /operator/notifications/{id}/approve.test_approval_card_decline_button — click Decline, verify API call.test_notification_badge_hidden_when_zero — zero unseen, badge not rendered.test_notification_badge_shows_count — nonzero unseen, badge visible with count.Library:
test_library_renders_artifacts — mock library response, verify artifact cards.test_library_download_button — verify download link points to correct endpoint.test_library_filter_by_project — select project filter, verify API call includes project_id.test_library_make_new_version — click button, verify navigation to /chat?project={id}.Auth:
test_auth_redirect_on_401 — mock 401 response, verify redirect to /auth.test_auth_context_provides_person — mock /me, verify displayName and companionName available.Navigation:
test_nav_renders_links — verify Dashboard, Inbox, Library links.test_nav_companion_trigger — verify companion trigger exists.Estimated: 24 frontend component tests.
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.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.
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 |
DUNIN7/loomworks exists on GitHub. npm run build succeeds.#FFFFFF) anywhere.GET /operator/dashboard. SSE events trigger refetch. Empty states render warmly. Greeting shows display name.cd /Users/dunin7/loomworks-ui && npm run lint && npx tsc --noEmit && npm run build && npm run test still clean.DUNIN7/loomworks (Operator Layer frontend): new repo. Next.js application. Four surfaces: Companion chat, Dashboard, Inbox, Library. ~26 vitest tests. lint/tsc/build clean. Tag: phase-46-operator-layer-frontend.DUNIN7/loomworks-engine (substrate): 1,777+ tests (1,773 + 4 new), 26 skips. One new endpoint (GET /operator/conversation-history). No migration. Tag: phase-46-conversation-history.DUNIN7/loomworks-ui (Workshop): unchanged.snoozed_until; frontend snooze deferred.
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