New files:
- project-state.types.ts: all per-project state interfaces
- project-state.svelte.ts: unified per-project state with version counter
- app-state.svelte.ts: root facade re-exporting all stores as appState.*
Rewired components (no more local $state):
- ProjectCard: reads via appState.agent.* and appState.project.tab.*
- TerminalTabs: state in appState.project.terminals.*
- FileBrowser: state in appState.project.files.*
- CommsTab: state in appState.project.comms.*
- TaskBoardTab: state in appState.project.tasks.*
All follow Rule 57 (no $derived with new objects) and Rule 58
(state tree architecture, components are pure renderers).
TaskBoardTab: $effect called loadTasks() which wrote ++pollToken ($state)
→ triggered $effect re-run → infinite loop. Fix: onMount instead.
Also tasksByCol $derived created new objects via .reduce/.filter.
FileBrowser: $effect read openDirs (via new Set(openDirs)) AND wrote to
it (openDirs = s) → infinite loop. Fix: onMount with fresh Set.
CommsTab: $effect called loadChannels()/loadAgents() which wrote $state
→ potential cycle. Fix: onMount instead.
Rule: NEVER use $effect for initialization that writes to $state.
Always use onMount for async init + side effects.