---
phase: 20-real-time-subscriptions
plan: 02
type: execute
wave: 2
depends_on: ["20-01"]
files_modified:
  - packages/web/src/lib/trpc.ts
  - packages/web/src/routes/initiatives.index.tsx
  - packages/web/src/routes/initiatives.$id.tsx
  - packages/web/src/routes/inbox.tsx
  - packages/web/package.json
autonomous: true
---

<objective>
Wire frontend tRPC client with SSE subscriptions and add live update hooks to all three UI pages.

Purpose: Currently, the dashboard, initiative detail, and inbox pages fetch data once and only refresh on manual user action. This plan adds `httpSubscriptionLink` via `splitLink` so subscription procedures route to SSE, then wires `useSubscription` hooks into each page to auto-refresh data when domain events arrive.

Output: All three UI pages update in real-time when agents spawn/stop, tasks complete, or questions arrive.
</objective>

<execution_context>
@~/.claude/get-shit-done/workflows/execute-plan.md
@~/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md

# Prior plan output (genuinely needed — subscription procedures defined there):
@.planning/phases/20-real-time-subscriptions/20-01-SUMMARY.md

# Key source files:
@packages/web/src/lib/trpc.ts
@packages/web/src/main.tsx
@packages/web/src/routes/initiatives.index.tsx
@packages/web/src/routes/initiatives.$id.tsx
@packages/web/src/routes/inbox.tsx
@packages/web/vite.config.ts
</context>

<tasks>

<task type="auto">
  <name>Task 1: Update tRPC client with splitLink for SSE subscriptions</name>
  <files>packages/web/src/lib/trpc.ts, packages/web/package.json</files>
  <action>
**1. Update the tRPC client configuration (`packages/web/src/lib/trpc.ts`):**

Replace the single `httpBatchLink` with `splitLink` that routes subscriptions to `httpSubscriptionLink` and everything else to `httpBatchLink`:

```typescript
import { createTRPCReact } from '@trpc/react-query';
import { httpBatchLink, splitLink } from '@trpc/client';
import { httpSubscriptionLink } from '@trpc/client';
import type { AppRouter } from '@codewalk-district/shared';

export const trpc = createTRPCReact<AppRouter>();

export function createTRPCClient() {
  return trpc.createClient({
    links: [
      splitLink({
        condition: (op) => op.type === 'subscription',
        true: httpSubscriptionLink({ url: '/trpc' }),
        false: httpBatchLink({ url: '/trpc' }),
      }),
    ],
  });
}
```

Key points:
- `httpSubscriptionLink` uses SSE (Server-Sent Events) over HTTP — NOT WebSocket. It reuses the same `/trpc` URL.
- The Vite dev proxy at `/trpc` already forwards to the backend. SSE works over standard HTTP, so no WebSocket proxy config needed.
- No `createWSClient` or `wsLink` needed — SSE is simpler and sufficient for this use case (server-to-client push only).
- `httpSubscriptionLink` is already included in `@trpc/client@^11.9.0` — no new packages needed.

**2. Verify no new packages needed:**

Check that `@trpc/client@^11.9.0` already exports `httpSubscriptionLink` and `splitLink`. These are part of the core `@trpc/client` package in v11. Do NOT install any additional packages unless the import fails.
  </action>
  <verify>
1. `npm run build` in `packages/web/` succeeds
2. TypeScript can resolve `httpSubscriptionLink` and `splitLink` from `@trpc/client`
3. No type errors
  </verify>
  <done>
- tRPC client routes subscriptions to SSE via httpSubscriptionLink
- Queries and mutations still use httpBatchLink (no regression)
- splitLink correctly discriminates by operation type
  </done>
</task>

<task type="auto">
  <name>Task 2: Add live update hooks to all three UI pages</name>
  <files>packages/web/src/routes/initiatives.index.tsx, packages/web/src/routes/initiatives.$id.tsx, packages/web/src/routes/inbox.tsx</files>
  <action>
**Pattern for all pages:** Use `trpc.{subscription}.useSubscription()` with an `onData` callback that invalidates the relevant React Query cache, causing the existing `useQuery` hooks to refetch. This is the simplest approach — no local state management for events, just invalidate-on-event.

```typescript
const utils = trpc.useUtils();

trpc.onTaskUpdate.useSubscription(undefined, {
  onData: () => {
    // Invalidate relevant queries to trigger refetch
    utils.listPhases.invalidate();
    utils.listTasks.invalidate();
  },
});
```

**1. Initiative Dashboard (`packages/web/src/routes/initiatives.index.tsx`):**

Add `onTaskUpdate` subscription. On any task/phase event:
- Invalidate `listInitiatives` (status may have changed)
- Invalidate `listPhases` (progress bars use phase counts)

This makes the dashboard phase progress bars and status badges update live.

**2. Initiative Detail (`packages/web/src/routes/initiatives.$id.tsx`):**

Add `onTaskUpdate` subscription. On any task/phase event:
- Invalidate `listPhases` for the current initiative
- Invalidate `listTasks` for visible plans
- Invalidate `listPlans` for visible phases

This makes the phase accordion and task rows update live.

Also add `onAgentUpdate` subscription to update agent assignment badges:
- Invalidate `listAgents` on agent events

**3. Agent Inbox (`packages/web/src/routes/inbox.tsx`):**

Add `onAgentUpdate` subscription. On any agent event:
- Invalidate `listWaitingAgents` (new question arrived or agent resumed)
- Invalidate `listMessages` (message status may have changed)

This is the most impactful — when an agent asks a question, the inbox lights up without manual refresh.

**Important implementation notes:**
- Use `undefined` as subscription input (no `lastEventId` filtering needed for v1 — just invalidate on any event)
- The `useSubscription` hook auto-reconnects on connection loss — tRPC v11's `httpSubscriptionLink` handles this
- Don't accumulate events in state — just invalidate queries. Keep it simple.
- If the subscription connection fails (server not running), it should fail silently — the pages still work via polling/manual refresh. Do NOT show error UI for subscription failures.
- Add `onError: () => {}` to suppress subscription error logs in development (server may not be running during frontend-only development)
  </action>
  <verify>
1. `npm run build` in `packages/web/` succeeds
2. No TypeScript errors on subscription hook usage
3. Start backend (`npx tsx src/bin/cw.ts --server`), start frontend (`npm run dev` in packages/web/), open dashboard — SSE connection established (check Network tab in browser DevTools for `onTaskUpdate.subscribe` request with `text/event-stream` response)
  </verify>
  <done>
- Dashboard auto-refreshes initiative status and phase progress on events
- Initiative detail auto-refreshes task/phase status on events
- Inbox auto-refreshes agent questions on agent events
- Subscription failures are silent — pages degrade to manual refresh
- No new state management — subscriptions just invalidate React Query caches
  </done>
</task>

</tasks>

<verification>
Before declaring plan complete:
- [ ] `npm run build` succeeds for all packages
- [ ] Frontend connects SSE subscription on page load (visible in DevTools Network tab)
- [ ] Triggering a backend event (e.g., spawning agent via CLI) causes frontend data to refresh without manual action
- [ ] Closing/reopening the SSE connection auto-reconnects
- [ ] Pages still work when backend is not running (graceful degradation)
</verification>

<success_criteria>

- All tasks completed
- All verification checks pass
- No errors or warnings introduced
- All three UI pages (dashboard, detail, inbox) receive live updates via SSE
- Subscription failures degrade gracefully (no error UI, pages still work)
</success_criteria>

<output>
After completion, create `.planning/phases/20-real-time-subscriptions/20-02-SUMMARY.md`
</output>
