feat(web): reshape workshop from 'tune a harness' to 'design a domain node'

The board is now a Claude-powered ZeroClaw agent with expert skills that teams
talk to directly (its own dashboard / Telegram / voice). The frontend was built
on the older sensor+harness model; this repoints it.

- State/DTO: AddLayers remapped to the 5 design layers (Domain/Skills/Policies/
  Harness/Loops); added session.domain (+ mirrored through TeamSnapshot, sync,
  and the SQLite store with a guarded migration); dropped Harness/Provider/RunMode;
  persist v2 migrate resets stale state.
- Removed the harness/sim/failover machinery (HarnessProviderSelect, HarnessTuner,
  HarnessTomlPreview, ResiliencePanel, TriggerButtons, harness.ts, useSerial,
  LiveFeed/serial classifier) and every sim-vs-live branch.
- Onboarding: EnvSetup rebuilt into 'Meet your node' — Open-your-node hero (new
  reusable OpenYourNode CTA), say-hi-to-your-agent, a free-text DomainPicker
  (domain drives L1-L5), Telegram/voice pointers, and the open->locked lockdown
  policy step. Domain is required to proceed.
- Modules repointed: M1 Domain & events (L1), M2 Skills & Policies (L2/L3, skills
  reference + actuation-gate framing), M3 Harness (reasoning+tiering) & Loops
  (cadence) (L4/L5). BuildFlash reframed as a guided 'ask your node to build X'
  that hands off to the node dashboard.
- Judge rubric -> Domain fit/Skills/Policies/Harness/Loops; Lecture + Landing
  re-storied to the Claude-node + talk-to-your-node narrative.

Verified: web tsc + 181 tests, api tsc + 58 tests, dead-ref sweep clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-16 15:33:38 -07:00
co-authored by Claude Opus 4.8
parent 3be44ec215
commit 6d94833bb9
52 changed files with 649 additions and 1833 deletions
+13 -47
View File
@@ -14,6 +14,7 @@ vi.mock('@/lib/api', async (orig) => ({
return () => {}
},
getNodeStatus: vi.fn().mockResolvedValue({ teamId: 't', online: false }),
sendPrompt: vi.fn().mockResolvedValue(undefined),
}))
function renderPage() {
@@ -32,63 +33,28 @@ describe('Module2', () => {
it('renders the phase strip set to m2 and the heading', () => {
renderPage()
expect(screen.getByRole('heading', { name: /harness engineering/i })).toBeInTheDocument()
expect(screen.getByRole('heading', { name: /skills.*policies/i })).toBeInTheDocument()
expect(
screen.getByTestId('phase-strip').querySelector('[data-phase="m2"]'),
).toHaveAttribute('data-state', 'active')
})
it('tunes a threshold and reflects it in the TOML preview', async () => {
const user = userEvent.setup()
it('shows the live board feed and the build & flash panel', () => {
renderPage()
const input = screen.getByLabelText(/threshold · g/i)
await user.clear(input)
await user.type(input, '1.2')
expect(useSession.getState().harness.thresholdG).toBe(1.2)
expect(screen.getByTestId('harness-toml')).toHaveTextContent('threshold_g = 1.2')
expect(screen.getByTestId('live-board-feed')).toBeInTheDocument()
expect(screen.getByTestId('activity-log')).toBeInTheDocument()
})
it('fires a trigger that classifies against the tuned threshold', async () => {
const user = userEvent.setup()
renderPage()
// tune low so the impact frame is unambiguously critical
const g = screen.getByLabelText(/threshold · g/i)
await user.clear(g)
await user.type(g, '0.5')
await user.click(screen.getByRole('button', { name: /impact/i }))
expect(useSession.getState().stats.critical).toBe(1)
})
it('gates Proceed until a trigger fired and L2 + L3 are filled', async () => {
const user = userEvent.setup()
it('gates Proceed until the board acted and L2 + L3 are filled', async () => {
useSession.getState().setAddLayer('L2', 'escalate on critical')
useSession.getState().setAddLayer('L3', 'drive damper on critical')
renderPage()
const proceed = screen.getByRole('button', { name: /proceed/i })
expect(proceed).toBeDisabled()
await user.type(screen.getByLabelText(/layer 2/i), 'escalate on critical')
await user.type(screen.getByLabelText(/layer 3/i), 'drive damper on critical')
expect(proceed).toBeDisabled() // no trigger fired yet
await user.click(screen.getByRole('button', { name: /shake/i }))
expect(proceed).toBeEnabled()
})
describe('live mode (real node feed)', () => {
it('replaces triggers with the live board feed and gates on real activity', async () => {
useSession.getState().setMode('live')
useSession.getState().setAddLayer('L2', 'escalate on critical')
useSession.getState().setAddLayer('L3', 'drive damper on critical')
renderPage()
expect(await screen.findByTestId('live-board-feed')).toBeInTheDocument()
expect(screen.queryByTestId('trigger-buttons')).toBeNull() // no synthetic triggers in live mode
const proceed = screen.getByRole('button', { name: /proceed/i })
expect(proceed).toBeDisabled()
// the board acts (via Build & flash) → activity streams from the node
act(() => emit({ type: 'node:activity', teamId: 'x', kind: 'response', label: 'Agent finished', ts: '' }))
await waitFor(() => expect(proceed).toBeEnabled())
})
// the board acts (via Build & flash) → activity streams from the node
act(() => emit({ type: 'node:activity', teamId: 'x', kind: 'response', label: 'Agent finished', ts: '' }))
await waitFor(() => expect(proceed).toBeEnabled())
})
it('marks m2 complete on Proceed', async () => {
@@ -96,8 +62,8 @@ describe('Module2', () => {
renderPage()
await user.type(screen.getByLabelText(/layer 2/i), 'L2 text')
await user.type(screen.getByLabelText(/layer 3/i), 'L3 text')
await user.click(screen.getByRole('button', { name: /impact/i }))
await user.click(screen.getByRole('button', { name: /proceed/i }))
act(() => emit({ type: 'node:activity', teamId: 'x', kind: 'response', label: 'Agent finished', ts: '' }))
await user.click(await screen.findByRole('button', { name: /proceed/i }))
expect(useSession.getState().phases.m2).toBe(true)
})
})