PROBLEM:
- Agents completing with questions were incorrectly marked as "crashed"
- Race condition: polling handler AND crash handler both called handleCompletion()
- Caused database corruption and lost pending questions
SOLUTION:
- Add completion mutex in OutputHandler to prevent concurrent processing
- Remove duplicate completion call from crash handler
- Only one handler executes completion logic per agent
TESTING:
- Added mutex-completion.test.ts with 4 test cases
- Verified mutex prevents concurrent access
- Verified lock cleanup on exceptions
- Verified different agents can process concurrently
FIXES: residential-cuckoo and 12+ other agents stuck in crashed state
Replace the setState-during-render pattern in PlanTasksFetcher with a
useEffect hook. The onTasks callback was being called directly in the
render body when tasksQuery.isSuccess was true, which could cause
infinite re-render loops when the parent state update triggered a
re-render. Now data flows through useEffect with proper dependencies.
Enable autoCodeSplitting in TanStackRouterVite plugin to split the monolithic
582 KB bundle into route-level chunks. Main vendor chunk drops to 454 KB with
initiative detail (~13 KB), inbox (~13 KB), and dashboard (~65 KB) as separate
lazy-loaded chunks. Eliminates the Vite 500 KB chunk size warning.
- SpawnArchitectDropdown: toast.success on spawn, toast.error on failure
- ActionMenu: toast.success on archive, toast.error on failure
- CreateInitiativeDialog: toast.success on creation
- Inbox: toast.success/error for resumeAgent and respondToMessage mutations
- Replace all console.error-only mutation error handling with user-visible toasts
Replace plain "Loading initiative..." and "Loading phases..." text with
structured skeleton placeholders matching the page layout: header with
back arrow/title/badge, two-column grid with phase accordions and
sidebar panels.
- Hide inbox list on mobile when agent selected (drill-down pattern)
- Add "Back to list" button visible only on mobile (lg:hidden)
- Clicking back clears selection and returns to list view
- Create ErrorBoundary class component with recovery UI (reload button)
- Create Sonner Toaster wrapper (bottom-right, richColors)
- Wire ErrorBoundary around Outlet in root route to catch render errors
- Add Toaster as sibling to AppLayout in root route
- Update notFoundComponent with Back to Dashboard link button
- Remove disabled nav stubs (Agents, Tasks, Settings) from AppLayout
- Make TaskRow agent names clickable links to inbox page
- Add task ID link and "View in context" link in inbox detail panel
Create reusable Skeleton component with animate-pulse styling. Replace
plain "Loading..." text with structured skeleton placeholders matching
card layouts in InitiativeList and inbox page.
Add useSubscription hooks to all three UI pages that invalidate React
Query caches on domain events:
- Dashboard: onTaskUpdate invalidates listInitiatives + listPhases
- Detail: onTaskUpdate invalidates phases/tasks/plans, onAgentUpdate
invalidates listAgents
- Inbox: onAgentUpdate invalidates listWaitingAgents + listMessages
Subscription failures are silent (onError: () => {}) so pages degrade
gracefully to manual refresh when the backend is not running.
Replace single httpBatchLink with splitLink that routes subscription
operations to httpSubscriptionLink (SSE) while keeping queries/mutations
on httpBatchLink. No new packages needed — both exports are part of
@trpc/client v11.
Add SubscriptionEvent interface for frontend consumption of SSE
subscription data in onData callbacks. Vite proxy confirmed compatible
with SSE streaming (http-proxy passes chunked responses by default).
Replaces stub inbox page with full implementation connecting InboxList
and QuestionForm components to tRPC backend. Two-column layout with
agent list on left and detail panel on right. Handles answer submission
via resumeAgent mutation, notification dismissal via respondToMessage,
and loading/error states following patterns from initiative detail page.
- Renders mixed question types from questions array
- Submit disabled until all questions answered
- onSubmit called with Record<string, string> mapping questionId to answer
InboxList joins agents with their latest messages, provides filter
(all/waiting/completed) and sort (newest/oldest) controls, renders
MessageCard instances, and shows empty state when no messages match.
- OptionGroup renders radio or checkbox based on multiSelect prop
- "Other" field auto-selects when user types into it
- FreeTextInput renders Input or Textarea based on multiline prop
MessageCard displays agent name with status, message preview (truncated
to 80 chars), relative timestamp, and response indicator (filled/empty
circle). Uses shadcn Card base with selected state highlighting.
Replace placeholder with full initiative detail page: tRPC data fetching
(getInitiative, listPhases, listPlans, listTasks), PhaseWithTasks helper
component pattern for stable hooks, two-column layout with phases on left
and progress/decisions on right, TaskDetailModal with selectedTask state,
queue actions for phases and tasks, loading/error states.
Expandable/collapsible phase container with chevron toggle, phase
number + name header, task count (completed/total), StatusBadge,
phase-level DependencyIndicator, and TaskRow list when expanded.
Controlled dialog showing full task metadata, dependencies, dependents,
and action buttons (Queue Task / Stop Task) with proper enable/disable
logic based on task status and dependency completion.
Renders a single task row with Unicode tree connectors (├── / └──),
StatusBadge, inline agent name, and DependencyIndicator for blocked
tasks. Entire row is clickable with hover feedback.
Collapsible list of key decisions with expand/collapse per item and
show more/less toggle when exceeding maxVisible threshold. Renders
in a Card with empty-state placeholder.
- Back button with ChevronLeft icon for dashboard navigation
- Card displaying initiative name, status badge, and dates
- Disabled Actions placeholder button for future Plan 04 work
- Pure presentational component receiving props
The dashboard page now owns the create action via its own header button
and empty state CTA. This keeps AppLayout generic — other pages like
Inbox shouldn't show initiative-specific actions.
Replace placeholder DashboardPage with fully functional initiative dashboard:
- Page header with title, status filter dropdown, and New Initiative button
- InitiativeList with statusFilter prop for backend-driven filtering
- CreateInitiativeDialog controlled by local state
- Navigation to /initiatives/$id via TanStack Router useNavigate
- All card callbacks wired (view, spawn architect, delete placeholder)
- Fetches initiatives via trpc.listInitiatives with optional status filter
- Handles loading, error, empty, and populated states
- Empty state shows "No initiatives yet" with CTA button
- Error state shows message with retry button
- Renders vertical stack of InitiativeCards with space-y-3 gap
- Fix: SerializedInitiative type for tRPC Date→string serialization
- SpawnArchitectDropdown: discuss/breakdown modes via tRPC mutations
- Brief success state on button text after spawn
- ActionMenu: archive with browser confirm, disabled edit/duplicate/delete
- No deleteInitiative tRPC procedure exists, so delete is placeholder
- Both components invalidate listInitiatives on success
- Install shadcn Dialog, Input, Label, Textarea components
- Move from @/ literal dir to src/components/ui/ (known shadcn CLI issue)
- CreateInitiativeDialog: controlled dialog with name/description fields
- tRPC createInitiative mutation with loading/error states
- Form resets on open, validates name non-empty
- Renders initiative name, StatusBadge, ProgressBar, and phase count
- Fetches phase stats per-card via trpc.listPhases (self-contained)
- Spawn Architect dropdown with discuss/breakdown modes
- More actions menu with edit/duplicate/archive/delete
- Responsive: stacks vertically on mobile, hides phase count text
- Card clickable with stopPropagation on action buttons
- Percentage-based horizontal bar with completed/total props
- Gray track (bg-muted), blue fill for partial, green fill for 100%
- Text label showing percentage
- Handles 0/0 total gracefully (empty bar)
Create route structure with __root, index (redirect to /initiatives),
initiatives/index (dashboard stub), initiatives/$id (detail stub with
type-safe params), and inbox (stub). Add AppLayout with persistent nav
header matching wireframe: title, New Initiative button, navigation tabs
with active highlighting. Disabled tabs for Agents/Tasks/Settings (out
of scope). Replace temporary health-check App with RouterProvider.
Route tree auto-generated by TanStack Router Vite plugin.
Create tRPC client with httpBatchLink targeting /trpc (Vite proxy).
Wrap app in trpc.Provider and QueryClientProvider with sensible
defaults. Add health check query to App.tsx for connection
verification. Add vite-env.d.ts for CSS module types. Remove
unused Plan import from backend router.
Add @trpc/client, @trpc/react-query, @tanstack/react-query to web
package. Link @codewalk-district/shared as workspace dependency.
Add project reference to shared in tsconfig.app.json.
- Tailwind CSS v3 with postcss and autoprefixer
- shadcn/ui components.json and cn() utility
- Path alias @/ -> ./src/ in vite.config.ts and tsconfig.app.json
- CSS custom properties for shadcn theming (light/dark)
- tailwindcss-animate and @tailwindcss/typography plugins
- App.tsx uses cn() with Tailwind classes to verify integration
- packages/web/ with Vite dev server, React 19, TypeScript
- Server proxy /trpc -> http://127.0.0.1:3847 for CORS-free dev
- Root npm workspaces configured (packages/*)
- dev:web script for workspace dev server