feat: Phase 2 — store audit, migration clusters, ADR, settings domain migration

- MIGRATION_CLUSTERS.md: reactive dependency graph across 20 bridges/stores
- PHASE1_STORE_AUDIT.md: 11 stores audited (3 clean, 5 bridge-dependent, 3 platform-specific)
- ADR-001: dual-stack binding strategy (accepted, S-1+S-3 hybrid)
- Settings domain migration: all 6 settings components + App.svelte + FilesTab
  migrated from settings-bridge to getBackend() calls
This commit is contained in:
Hibryda 2026-03-22 03:48:41 +01:00
parent df83b1df4d
commit 579157f6da
18 changed files with 657 additions and 26 deletions

215
MIGRATION_CLUSTERS.md Normal file
View file

@ -0,0 +1,215 @@
# Migration Clusters: Reactive Dependency Graph Analysis
Phase 2 binding analysis — reactive state dependencies across stores, bridge adapters, and components.
## Store Dependency Matrix
### agents.svelte.ts
- **Bridge imports:** claude-messages (type only)
- **Store imports:** none
- **Reactive state:** `$state<AgentSession[]>` (sessions)
- **Exported functions:** getAgentSessions, getAgentSession, createAgentSession, updateAgentStatus, setAgentSdkSessionId, setAgentModel, appendAgentMessage(s), updateAgentCost, findChildByToolUseId, getChildSessions, getTotalCost, clearAllAgentSessions, removeAgentSession
- **Consumed by stores:** workspace, health, wake-scheduler
- **Consumed by components:** AgentPane, AgentPreviewPane, AgentTree, AgentCard, AgentSession, ContextTab, MetricsPanel, StatusBar, TeamAgentsPanel
- **Consumed by utils:** session-persistence, subagent-router, agent-dispatcher
### workspace.svelte.ts
- **Bridge imports:** groups-bridge, btmsg-bridge
- **Store imports:** agents, health, conflicts, wake-scheduler
- **Reactive state:** `$state<GroupsFile | null>`, `$state<string>` (activeGroupId), `$state<WorkspaceTab>`, `$state<string | null>` (activeProjectId), `$state<Record<string, TerminalTab[]>>`, `$state<string | null>` (focusFlash)
- **Exported functions:** get*/set* for all state, switchGroup, load/saveWorkspace, add/remove/update Group/Project/Agent, terminal tab mgmt, event callbacks
- **Consumed by stores:** wake-scheduler
- **Consumed by components:** GlobalTabBar, ProjectGrid, ProjectBox, GroupAgentsPanel, CommandPalette, SettingsTab, DocsTab, SearchOverlay, TerminalTabs, SshTab, AgentSession, StatusBar, CommsTab, ProjectSettings
- **Consumed by utils:** auto-anchoring, agent-dispatcher (via waitForPendingPersistence)
### health.svelte.ts
- **Bridge imports:** none
- **Store imports:** agents, conflicts
- **Reactive state:** `$state<Map>` (trackers, stallThresholds), `$state<number>` (tickTs)
- **Exported functions:** trackProject, untrackProject, setStallThreshold, updateProjectSession, recordActivity, recordToolDone, recordTokenSnapshot, start/stopHealthTick, setReviewQueueDepth, clearHealthTracking, getProjectHealth, getAllProjectHealth, getAttentionQueue, getHealthAggregates
- **Consumed by stores:** workspace (import clearHealthTracking), wake-scheduler
- **Consumed by components:** ProjectBox, ProjectHeader, MetricsPanel, StatusBar, AgentSession
- **Consumed by utils:** agent-dispatcher, attention-scorer (type only)
### conflicts.svelte.ts
- **Bridge imports:** none (types/ids only)
- **Store imports:** none
- **Reactive state:** `$state<Map>` (projectFileWrites, acknowledgedFiles, sessionWorktrees, agentWriteTimestamps)
- **Exported functions:** setSessionWorktree, recordFileWrite, recordExternalWrite, getExternalConflictCount, getProjectConflicts, hasConflicts, getTotalConflictCount, acknowledgeConflicts, clearSessionWrites, clearProjectConflicts, clearAllConflicts
- **Consumed by stores:** workspace (import clearAllConflicts), health
- **Consumed by components:** ProjectBox, ProjectHeader, StatusBar
- **Consumed by utils:** agent-dispatcher
### anchors.svelte.ts
- **Bridge imports:** anchors-bridge
- **Store imports:** none
- **Reactive state:** `$state<Map>` (projectAnchors), `$state<Set>` (autoAnchoredProjects)
- **Exported functions:** get/add/remove/change anchor(s), loadAnchorsForProject, hasAutoAnchored, markAutoAnchored, getAnchorSettings
- **Consumed by components:** AgentPane, ContextTab, AgentSession
- **Consumed by utils:** auto-anchoring, agent-dispatcher
### theme.svelte.ts
- **Bridge imports:** settings-bridge
- **Store imports:** none
- **Reactive state:** `$state<ThemeId>`, `$state<ThemePalette | null>`
- **Exported functions:** getCurrentTheme, getXtermTheme, setTheme, initTheme, previewPalette, clearPreview, setCustomTheme, onThemeChange
- **Consumed by components:** TerminalPane, AgentPreviewPane, SettingsTab, AppearanceSettings, ThemeEditor
- **Consumed by:** App.svelte (init)
### plugins.svelte.ts
- **Bridge imports:** plugins-bridge, settings-bridge
- **Store imports:** none
- **Reactive state:** `$state<PluginCommand[]>`, `$state<PluginEntry[]>`
- **Exported functions:** get/add/removePluginCommands, pluginEventBus, getPluginEntries, setPluginEnabled, loadAllPlugins, reloadAllPlugins, destroyAllPlugins
- **Consumed by components:** CommandPalette, SettingsTab, AdvancedSettings
- **Consumed by:** plugin-host.ts
### machines.svelte.ts
- **Bridge imports:** remote-bridge
- **Store imports:** notifications
- **Reactive state:** `$state<Machine[]>`
- **Exported functions:** getMachines, getMachine, loadMachines, addMachine, removeMachine, connectMachine, disconnectMachine, init/destroyMachineListeners
- **Consumed by components:** (used by SettingsTab multi-machine section)
### wake-scheduler.svelte.ts
- **Bridge imports:** bttask-bridge, audit-bridge
- **Store imports:** health, workspace, agents
- **Reactive state:** `$state<Map>` (registrations, pendingWakes)
- **Exported functions:** disableWakeScheduler, register/unregisterManager, updateManager*, getWakeEvent, consumeWakeEvent, forceWake, clearWakeScheduler
- **Consumed by stores:** workspace (import clearWakeScheduler)
- **Consumed by components:** ProjectBox, AgentSession, StatusBar
### notifications.svelte.ts
- **Bridge imports:** notifications-bridge
- **Store imports:** none
- **Reactive state:** `$state<Toast[]>`, `$state<HistoryNotification[]>`
- **Exported functions:** getNotifications, notify, dismissNotification, addNotification, getNotificationHistory, getUnreadCount, markRead, markAllRead, clearHistory
- **Consumed by stores:** machines
- **Consumed by components:** ToastContainer, NotificationCenter, ProjectBox, AgentPane
- **Consumed by utils:** handle-error, global-error-handler, auto-anchoring, agent-dispatcher
### layout.svelte.ts
- **Bridge imports:** session-bridge
- **Store imports:** none
- **Reactive state:** `$state<Pane[]>`, `$state<LayoutPreset>`, `$state<string | null>` (focusedPaneId)
- **Exported functions:** getPanes, getActivePreset, getFocusedPaneId, add/removePane, focusPane, setPreset, renamePaneTitle, setPaneGroup, restoreFromDb, getGridTemplate, getPaneGridArea
- **Consumed by components:** AgentPane (focusPane)
- **Consumed by utils:** detach (type only), subagent-router
### settings-scope.svelte.ts
- **Bridge imports:** settings-bridge
- **Store imports:** none
- **Reactive state:** `$state<string | null>` (activeProjectId), `$state<Map>` (overrideCache)
- **Exported functions:** setActiveProject, getActiveProjectForSettings, scopedGet/Set, removeOverride, getOverrideChain, hasProjectOverride, invalidateSettingsCache
- **Consumed by components:** ProjectSettings
### sessions.svelte.ts
- **Bridge imports:** none
- **Store imports:** none
- **Reactive state:** `$state<Session[]>`
- **Exported functions:** getSessions, addSession, removeSession
- **Consumed by:** (v2 legacy, minimal usage)
---
## Migration Clusters
Clusters are groups of files that share reactive state and must migrate atomically.
### Cluster 1: Pure State (No Bridge Dependencies)
**Files:** `agents.svelte.ts`, `conflicts.svelte.ts`, `sessions.svelte.ts`
**Dependency:** None on bridges. Pure in-memory reactive state.
**Risk:** LOW. These are self-contained state containers with zero platform coupling.
**Migration:** Move to `@agor/stores` immediately. No BackendAdapter wiring needed.
### Cluster 2: Settings Domain
**Files:** `settings-bridge.ts`, `settings-scope.svelte.ts`, `theme.svelte.ts`, `plugins.svelte.ts`, `custom-themes.ts`
**Dependency:** All import `settings-bridge.ts` (Tauri `invoke`).
**Component consumers:** SettingsTab, FilesTab, AppearanceSettings, AgentSettings, OrchestrationSettings, SecuritySettings, AdvancedSettings, ProjectSettings, App.svelte
**Risk:** MEDIUM. `settings-bridge` is the most-imported bridge (13 consumers). Theme init runs at app startup. Plugin lifecycle depends on settings for enabled state.
**Migration:** Replace `settings-bridge` imports with `getBackend().getSetting()` / `getBackend().setSetting()`. Migrate `settings-scope.svelte.ts` and `theme.svelte.ts` first, then `plugins.svelte.ts`.
### Cluster 3: Workspace + Groups
**Files:** `workspace.svelte.ts`, `groups-bridge.ts`, `btmsg-bridge.ts`
**Dependency:** Imports groups-bridge (Tauri invoke) and btmsg-bridge (agent registration).
**Cross-store deps:** Imports agents, health, conflicts, wake-scheduler (clear functions).
**Risk:** HIGH. Central orchestration hub. 15+ component consumers. switchGroup() cascades clears across 4 stores. loadWorkspace() is app bootstrap path.
**Migration:** Replace groups-bridge calls with BackendAdapter.loadGroups()/saveGroups(). btmsg registration needs a new BackendAdapter method or stays as a separate bridge.
### Cluster 4: Notification + Error Handling
**Files:** `notifications.svelte.ts`, `notifications-bridge.ts`, `handle-error.ts`, `global-error-handler.ts`
**Dependency:** notifications-bridge (Tauri invoke for desktop notifications).
**Risk:** LOW-MEDIUM. notifications-bridge is a single function (sendDesktopNotification). Capability-gated by `supportsDesktopNotifications`. handle-error is consumed everywhere.
**Migration:** Replace sendDesktopNotification with capability-checked BackendAdapter call. handle-error stays as-is (it calls notify() which is pure state).
### Cluster 5: Machine Management
**Files:** `machines.svelte.ts`, `remote-bridge.ts`
**Dependency:** remote-bridge (Tauri invoke + listen for WebSocket events).
**Cross-store deps:** imports notifications
**Risk:** MEDIUM. Event listener lifecycle (listen/unlisten). 12 IPC commands. Not in BackendAdapter yet.
**Migration:** Defer. Needs BackendAdapter extension for remote machine operations. Low priority (multi-machine is advanced feature).
### Cluster 6: Layout + Session Persistence
**Files:** `layout.svelte.ts`, `session-bridge.ts`
**Dependency:** session-bridge (Tauri invoke for SQLite session CRUD).
**Risk:** MEDIUM. V2 legacy layout — v3 uses workspace store. Still consumed by AgentPane.focusPane and subagent-router.
**Migration:** Replace session-bridge calls with BackendAdapter (needs session methods). Can coexist during transition.
### Cluster 7: Anchors
**Files:** `anchors.svelte.ts`, `anchors-bridge.ts`
**Dependency:** anchors-bridge (Tauri invoke for SQLite anchor CRUD).
**Risk:** LOW. Self-contained, small API surface (save, load, delete, updateType).
**Migration:** Replace anchors-bridge with BackendAdapter (needs anchor methods) or standalone bridge kept temporarily.
### Cluster 8: Wake Scheduler
**Files:** `wake-scheduler.svelte.ts`, `bttask-bridge.ts`, `audit-bridge.ts`
**Dependency:** bttask-bridge (Tauri invoke for task listing), audit-bridge (Tauri invoke for audit logging).
**Cross-store deps:** Reads from health, workspace, agents.
**Risk:** MEDIUM-HIGH. Complex evaluation logic + timer management. Multiple bridge dependencies. Reads across 3 stores.
**Migration:** Last cluster. Depends on clusters 1, 3, 8 completing first.
---
## Cluster Dependency Graph
```
Cluster 1 (Pure State)
|
+---> Cluster 3 (Workspace) depends on Cluster 1
| |
| +---> Cluster 8 (Wake Scheduler) depends on Clusters 1, 3, 4
|
+---> Cluster 4 (Notifications) standalone
|
+---> Cluster 6 (Layout) standalone
|
+---> Cluster 7 (Anchors) standalone
Cluster 2 (Settings) standalone
Cluster 5 (Machines) depends on Cluster 4
```
## Recommended Migration Order
1. **Cluster 1: Pure State** (agents, conflicts, sessions) -- zero risk, no bridges
2. **Cluster 2: Settings Domain** -- most consumers, establishes BackendAdapter pattern
3. **Cluster 4: Notifications** -- small bridge surface, enables error handling migration
4. **Cluster 7: Anchors** -- self-contained, small
5. **Cluster 6: Layout** -- v2 legacy, low priority
6. **Cluster 3: Workspace** -- high risk, many cross-deps, central hub
7. **Cluster 5: Machines** -- needs BackendAdapter extension
8. **Cluster 8: Wake Scheduler** -- depends on everything else
## Risk Assessment Summary
| Cluster | Risk | Reason |
|---------|------|--------|
| 1. Pure State | LOW | No platform deps, pure memory |
| 2. Settings | MEDIUM | 13 consumers, startup path, theme init |
| 3. Workspace | HIGH | Central hub, cascading clears, 15+ components |
| 4. Notifications | LOW-MEDIUM | Single bridge function, capability-gated |
| 5. Machines | MEDIUM | Event listeners, not in BackendAdapter yet |
| 6. Layout | MEDIUM | V2 legacy, session persistence |
| 7. Anchors | LOW | Small, self-contained |
| 8. Wake Scheduler | MEDIUM-HIGH | Multi-bridge, cross-store reads, timers |

267
PHASE1_STORE_AUDIT.md Normal file
View file

@ -0,0 +1,267 @@
# Phase 1 Store Audit: Platform Dependency Analysis
Categorization of each store for migration readiness to `@agor/stores`.
## Category Definitions
- **CLEAN:** No platform dependencies. Can move to `@agor/stores` as-is.
- **BRIDGE-DEPENDENT:** Uses bridge adapters (Tauri invoke/listen). Needs BackendAdapter migration first.
- **PLATFORM-SPECIFIC:** Contains platform-specific logic (paths, APIs). Stays in app-specific layer.
---
## Store Analysis
### 1. agents.svelte.ts
**Category: CLEAN**
| Check | Result |
|-------|--------|
| Platform-specific paths | None |
| Persistence paths | None (pure in-memory) |
| Capability-conditioned defaults | None |
| Tauri/Bun imports | None |
**Notes:** Only import is `type AgentMessage` from `claude-messages` (type-only, already in `@agor/types` as `AgentMessage`). Pure reactive state with no I/O. Ready to move immediately.
---
### 2. workspace.svelte.ts
**Category: BRIDGE-DEPENDENT**
| Check | Result |
|-------|--------|
| Platform-specific paths | None directly (groups.json path resolved in Rust backend) |
| Persistence paths | `groups-bridge.ts` → Tauri `invoke('groups_load')` / `invoke('groups_save')` |
| Capability-conditioned defaults | None |
| Tauri/Bun imports | `groups-bridge` (Tauri invoke), `btmsg-bridge` (Tauri invoke) |
**Bridge dependencies:**
- `loadGroups()` / `saveGroups()` → already in BackendAdapter
- `getCliGroup()` → Tauri-specific CLI arg parsing, not in BackendAdapter
- `registerAgents()` → btmsg-bridge, not in BackendAdapter
**Notes:** `loadGroups`/`saveGroups` can migrate to `getBackend()`. `getCliGroup` and `registerAgents` need new BackendAdapter methods or remain as separate bridges. Cross-store imports (agents, health, conflicts, wake-scheduler) are all clean function calls.
---
### 3. layout.svelte.ts
**Category: BRIDGE-DEPENDENT**
| Check | Result |
|-------|--------|
| Platform-specific paths | None (SQLite path resolved in Rust) |
| Persistence paths | `session-bridge.ts` → 8 Tauri invoke commands |
| Capability-conditioned defaults | None |
| Tauri/Bun imports | `session-bridge` (Tauri invoke) |
**Bridge dependencies:**
- `listSessions`, `saveSession`, `deleteSession`, `updateSessionTitle`, `touchSession` → session CRUD
- `saveLayout`, `loadLayout` → layout persistence
- `updateSessionGroup` → group assignment
**Notes:** V2 legacy store. V3 uses workspace.svelte.ts. Session persistence commands not yet in BackendAdapter. Could be deferred or added as BackendAdapter extension.
---
### 4. health.svelte.ts
**Category: CLEAN**
| Check | Result |
|-------|--------|
| Platform-specific paths | None |
| Persistence paths | None (pure in-memory with timer) |
| Capability-conditioned defaults | None |
| Tauri/Bun imports | None |
**Notes:** Imports from stores only (agents, conflicts) and utils (attention-scorer). Pure computation + timers. Ready to move.
---
### 5. conflicts.svelte.ts
**Category: CLEAN**
| Check | Result |
|-------|--------|
| Platform-specific paths | None |
| Persistence paths | None (session-scoped, no persistence) |
| Capability-conditioned defaults | None |
| Tauri/Bun imports | None |
**Notes:** Only imports `types/ids`. Pure in-memory conflict tracking. Ready to move.
---
### 6. anchors.svelte.ts
**Category: BRIDGE-DEPENDENT**
| Check | Result |
|-------|--------|
| Platform-specific paths | None (SQLite path resolved in Rust) |
| Persistence paths | `anchors-bridge.ts` → 4 Tauri invoke commands |
| Capability-conditioned defaults | None |
| Tauri/Bun imports | `anchors-bridge` (Tauri invoke) |
**Bridge dependencies:**
- `saveSessionAnchors`, `loadSessionAnchors`, `deleteSessionAnchor`, `updateAnchorType`
**Notes:** Small, well-defined bridge surface. Could add anchor CRUD to BackendAdapter or keep as separate bridge temporarily.
---
### 7. theme.svelte.ts
**Category: BRIDGE-DEPENDENT**
| Check | Result |
|-------|--------|
| Platform-specific paths | None |
| Persistence paths | `settings-bridge.ts``getSetting`/`setSetting` (Tauri invoke) |
| Capability-conditioned defaults | None |
| Tauri/Bun imports | `settings-bridge` (Tauri invoke) |
**Bridge dependencies:**
- `getSetting('theme')`, `setSetting('theme', ...)` — theme persistence
- `getSetting('ui_font_family')` etc. — 4 font settings on init
**Platform-specific behavior:**
- `document.documentElement.style.setProperty()` — browser API, works on both Tauri and Electrobun webviews. Not platform-specific.
**Notes:** Simple migration — replace `getSetting`/`setSetting` with `getBackend().getSetting()`/`getBackend().setSetting()`.
---
### 8. plugins.svelte.ts
**Category: BRIDGE-DEPENDENT**
| Check | Result |
|-------|--------|
| Platform-specific paths | None (plugin config dir resolved in Rust) |
| Persistence paths | `settings-bridge.ts` → plugin enabled state; `plugins-bridge.ts` → plugin discovery |
| Capability-conditioned defaults | `supportsPluginSandbox` could gate plugin loading |
| Tauri/Bun imports | `plugins-bridge` (Tauri invoke), `settings-bridge` (Tauri invoke) |
**Bridge dependencies:**
- `discoverPlugins()` → plugins-bridge (not in BackendAdapter)
- `getSetting`/`setSetting` → plugin enabled state
**Notes:** Plugin discovery is not in BackendAdapter. Settings part can migrate. Plugin host (`plugin-host.ts`) is pure Web Worker logic — platform-independent.
---
### 9. machines.svelte.ts
**Category: BRIDGE-DEPENDENT**
| Check | Result |
|-------|--------|
| Platform-specific paths | None |
| Persistence paths | `remote-bridge.ts` → 5 invoke commands + 5 listen events |
| Capability-conditioned defaults | None |
| Tauri/Bun imports | `remote-bridge` (Tauri invoke + listen) |
**Bridge dependencies:**
- `listRemoteMachines`, `addRemoteMachine`, `removeRemoteMachine`, `connectRemoteMachine`, `disconnectRemoteMachine`
- 5 event listeners: `onRemoteMachineReady`, `onRemoteMachineDisconnected`, `onRemoteError`, `onRemoteMachineReconnecting`, `onRemoteMachineReconnectReady`
**Notes:** Heavy bridge dependency with event subscription lifecycle. Not in BackendAdapter. Defer migration.
---
### 10. wake-scheduler.svelte.ts
**Category: BRIDGE-DEPENDENT**
| Check | Result |
|-------|--------|
| Platform-specific paths | None |
| Persistence paths | `bttask-bridge.ts``listTasks`; `audit-bridge.ts``logAuditEvent` |
| Capability-conditioned defaults | None |
| Tauri/Bun imports | `bttask-bridge` (Tauri invoke), `audit-bridge` (Tauri invoke) |
**Bridge dependencies:**
- `listTasks(groupId)` → task listing for wake signal evaluation
- `logAuditEvent(...)` → audit logging
**Notes:** Neither bttask nor audit operations are in BackendAdapter. Cross-store reads (health, workspace, agents). Complex timer logic is pure JS — platform independent. Bridges are the only blockers.
---
### 11. notifications.svelte.ts
**Category: BRIDGE-DEPENDENT**
| Check | Result |
|-------|--------|
| Platform-specific paths | None |
| Persistence paths | None (ephemeral state) |
| Capability-conditioned defaults | `supportsDesktopNotifications` should gate `sendDesktopNotification` |
| Tauri/Bun imports | `notifications-bridge` (Tauri invoke) |
**Bridge dependencies:**
- `sendDesktopNotification(title, body, urgency)` — single function, fire-and-forget
**Notes:** Toast system is pure reactive state. Only the OS notification part needs the bridge. Easy to capability-gate: `if (getBackend().capabilities.supportsDesktopNotifications) sendDesktopNotification(...)`.
---
### BONUS: settings-scope.svelte.ts
**Category: BRIDGE-DEPENDENT**
| Check | Result |
|-------|--------|
| Platform-specific paths | None |
| Persistence paths | `settings-bridge.ts``getSetting`/`setSetting` |
| Capability-conditioned defaults | None |
| Tauri/Bun imports | `settings-bridge` (Tauri invoke) |
**Notes:** Thin scoping layer over settings-bridge. Migrates with Cluster 2 (Settings Domain).
---
### BONUS: sessions.svelte.ts
**Category: CLEAN**
| Check | Result |
|-------|--------|
| Platform-specific paths | None |
| Persistence paths | None |
| Capability-conditioned defaults | None |
| Tauri/Bun imports | None |
**Notes:** V2 legacy, minimal. Pure reactive state.
---
## Summary Table
| Store | Category | Bridge Deps | Migration Blocker |
|-------|----------|-------------|-------------------|
| agents | CLEAN | none | -- |
| conflicts | CLEAN | none | -- |
| sessions | CLEAN | none | -- |
| health | CLEAN | none | -- |
| theme | BRIDGE-DEPENDENT | settings-bridge | BackendAdapter.getSetting/setSetting (ready) |
| settings-scope | BRIDGE-DEPENDENT | settings-bridge | BackendAdapter.getSetting/setSetting (ready) |
| notifications | BRIDGE-DEPENDENT | notifications-bridge | Capability-gate sendDesktopNotification |
| anchors | BRIDGE-DEPENDENT | anchors-bridge | Need BackendAdapter anchor methods |
| plugins | BRIDGE-DEPENDENT | plugins-bridge, settings-bridge | Need BackendAdapter plugin discovery |
| layout | BRIDGE-DEPENDENT | session-bridge | Need BackendAdapter session methods |
| workspace | BRIDGE-DEPENDENT | groups-bridge, btmsg-bridge | BackendAdapter groups (ready), btmsg TBD |
| machines | BRIDGE-DEPENDENT | remote-bridge | Need BackendAdapter remote machine methods |
| wake-scheduler | BRIDGE-DEPENDENT | bttask-bridge, audit-bridge | Need BackendAdapter bttask/audit methods |
### Ready to Migrate Now (4 stores)
- `agents.svelte.ts` — CLEAN
- `conflicts.svelte.ts` — CLEAN
- `health.svelte.ts` — CLEAN
- `sessions.svelte.ts` — CLEAN
### Ready After Settings-Bridge Replacement (3 stores)
- `theme.svelte.ts` — settings-bridge only
- `settings-scope.svelte.ts` — settings-bridge only
- `plugins.svelte.ts` — settings-bridge + plugins-bridge
### Requires BackendAdapter Extension (6 stores)
- `notifications.svelte.ts` — 1 method
- `anchors.svelte.ts` — 4 methods
- `layout.svelte.ts` — 8 methods
- `workspace.svelte.ts` — groups ready, btmsg TBD
- `machines.svelte.ts` — 10 methods + events
- `wake-scheduler.svelte.ts` — bttask + audit

View file

@ -0,0 +1,131 @@
# ADR-001: Dual-Stack Frontend-Backend Binding Strategy
**Status:** Accepted
**Date:** 2026-03-22
**Deciders:** Human + Claude (tribunal consensus: Claude 72%, Codex 78%)
## Context
Agent Orchestrator runs on two backends: Tauri 2.x (Rust, production) and Electrobun (Bun/TypeScript, experimental). Both share the same Svelte 5 frontend but communicate through different IPC mechanisms:
- **Tauri:** `@tauri-apps/api/core` invoke() + listen()
- **Electrobun:** RPC request/response + message listeners
The initial implementation used 23 bridge adapter files that directly import `@tauri-apps/api`. This created three problems:
1. **Duplicate code** — Each backend reimplements the same IPC surface. Electrobun adapters must manually replicate every bridge function.
2. **Drift risk** — When a Tauri bridge gains a new function, the Electrobun equivalent silently falls behind. No compile-time enforcement.
3. **58 Codex findings** — Three independent Codex audits identified duplicate SQL schemas, inconsistent naming (snake_case vs camelCase across the wire), missing error handling in Electrobun paths, and untested adapter code.
The project needed a binding strategy that:
- Eliminates code duplication for shared frontend logic
- Provides compile-time guarantees that both backends implement the same surface
- Does not require a premature monolithic rewrite
- Supports incremental migration of existing bridge adapters
## Decision
Adopt the **S-1 + S-3 hybrid strategy**: a shared `BackendAdapter` interface (S-1) combined with scoped, audit-gated extraction (S-3).
### Core Mechanism
A single `BackendAdapter` TypeScript interface defines every operation the frontend can request from the backend. Two concrete implementations (`TauriAdapter`, `ElectrobunAdapter`) are compile-time selected via path aliases. A singleton `getBackend()` function provides the active adapter.
Frontend stores and components call `getBackend().someMethod()` instead of importing platform-specific bridge files. The `BackendCapabilities` flags allow UI to gracefully degrade features unavailable on a given backend.
### Package Structure
- **`@agor/types`** — Shared type definitions (agent, project, btmsg, bttask, health, settings, protocol, backend, ids). No runtime code.
- **`src/lib/backend/backend.ts`** — Singleton accessor (`getBackend()`, `setBackend()`, `setBackendForTesting()`).
- **`src/lib/backend/TauriAdapter.ts`** — Tauri 2.x implementation.
- **`src/lib/backend/ElectrobunAdapter.ts`** — Electrobun implementation.
### Canonical SQL
A single `schema/canonical.sql` (29 tables) is the source of truth for both backends. A `tools/validate-schema.ts` extracts DDL metadata for comparison. A `tools/migrate-db.ts` handles one-way Tauri-to-Electrobun data migration with version fencing.
## Phase 1 Scope (DONE)
Implemented 2026-03-22:
- `@agor/types` package: 10 type files covering all cross-backend contracts
- `BackendAdapter` interface with 15 methods + 5 event subscriptions
- `BackendCapabilities` with 8 boolean flags
- `TauriAdapter` implementing all methods via `invoke()`/`listen()`
- `ElectrobunAdapter` implementing all methods via RPC
- `backend.ts` singleton with test helpers
- Canonical SQL DDL (29 tables, CHECK constraints, FK CASCADE, 13 indexes)
- Schema validator and migration tool
- `pnpm-workspace.yaml` workspace setup
- `docs/SWITCHING.md` migration guide
## Phase 2 Scope (In Progress)
Store audit and selective migration to `@agor/stores`:
- Reactive dependency graph analysis across 13 stores and 23 bridges
- Categorization: CLEAN (4 stores) / BRIDGE-DEPENDENT (9 stores) / PLATFORM-SPECIFIC (0 stores)
- Settings domain migration: first cluster, replacing `settings-bridge.ts` with `getBackend()`
- 8 migration clusters identified, ordered by dependency depth
### Phase 2 Trigger Checklist (Frozen)
All items verified as implementable via BackendAdapter:
- [x] PTY session create/attach/detach/destroy
- [x] Agent dispatch with status tracking
- [x] Settings read/write persistence
- [x] Search index query
- [x] Provider credential management
- [x] Notification dispatch
- [x] Workspace CRUD
## Phase 3 Direction (Documented, Not Committed)
**agor-daemon:** A standalone background process that both Tauri and Electrobun connect to, eliminating per-backend reimplementation entirely. This is documented as the long-term direction but explicitly NOT committed to. The hybrid adapter approach is sufficient for the current two-backend scope.
**Turborepo threshold:** Adopt Turborepo when package count reaches 3. Currently at 2 (`@agor/types`, main app). The third package would likely be `@agor/stores` (migrated pure-state stores).
## Consequences
### Enables
- Compile-time guarantee that both backends implement the same API surface
- Incremental migration — bridge files can be replaced one at a time
- Capability-gated UI degradation (features disabled on backends that lack support)
- Testing with mock backends (`setBackendForTesting()`)
- Future backend additions (GPUI, Dioxus) only need a new adapter class
- Canonical SQL prevents schema drift between backends
### Costs
- Two adapter implementations must be maintained in parallel
- BackendAdapter interface grows as new IPC commands are added
- Bridge files coexist with BackendAdapter during transition (dual access paths)
- One-way migration (Tauri to Electrobun) requires version fencing to prevent data loss
## Risks
Identified by tribunal (Claude + Codex consensus) and three Codex audits:
### Semantic Drift
**Risk:** Shared types enforce shape consistency but not behavioral consistency. Two adapters may handle edge cases differently (null vs undefined, error message format, timing).
**Mitigation:** Integration tests per adapter. Type-narrowing at adapter boundary.
### Svelte Rune Coupling
**Risk:** Stores using `$state`/`$derived` are tightly coupled to Svelte 5's rune execution model. Moving to `@agor/stores` package requires the consumer to also use Svelte 5.
**Mitigation:** Only move stores that are pure state (no `$derived` chains that reference DOM). Keep reactive-heavy stores in app layer.
### SQLite Pragma Differences
**Risk:** Tauri uses rusqlite (bundled SQLite, WAL mode, 5s busy_timeout). Electrobun uses better-sqlite3 (system SQLite, different default pragmas).
**Mitigation:** Canonical SQL includes pragma requirements. Migration tool validates pragma state before copying data.
### Build Toolchain Cache Invalidation
**Risk:** pnpm workspace changes can invalidate build caches unpredictably. `@agor/types` changes rebuild all consumers.
**Mitigation:** Types package is stable (changes are additive). Turborepo deferred until package count warrants it.
### Version Fencing
**Risk:** One-way migration (Tauri to Electrobun) means Electrobun DB is a snapshot. User switching back to Tauri loses Electrobun-only changes.
**Mitigation:** `tools/migrate-db.ts` writes a version fence marker. Tauri startup checks for fence and warns if Electrobun has newer data.
### Plugin Bridge Orphaning
**Risk:** Not all bridges map cleanly to BackendAdapter (btmsg, bttask, audit, plugins, anchors, remote). These "orphan bridges" may never migrate.
**Mitigation:** Documented as acceptable. BackendAdapter covers the critical path (settings, groups, agents, PTY, files). Domain-specific bridges can remain as adapters that internally use BackendAdapter or direct IPC.

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { initTheme } from './lib/stores/theme.svelte';
import { getSetting } from './lib/adapters/settings-bridge';
import { getSetting } from './lib/stores/settings-store.svelte';
import { isDetachedMode, getDetachedConfig } from './lib/utils/detach';
import { startAgentDispatcher, stopAgentDispatcher } from './lib/agent-dispatcher';
import { startHealthTick, stopHealthTick, clearHealthTracking } from './lib/stores/health.svelte';

View file

@ -1,13 +0,0 @@
import { invoke } from '@tauri-apps/api/core';
export async function getSetting(key: string): Promise<string | null> {
return invoke('settings_get', { key });
}
export async function setSetting(key: string, value: string): Promise<void> {
return invoke('settings_set', { key, value });
}
export async function listSettings(): Promise<[string, string][]> {
return invoke('settings_list');
}

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { listDirectoryChildren, readFileContent, writeFileContent, type DirEntry, type FileContent } from '../../adapters/files-bridge';
import { getSetting } from '../../adapters/settings-bridge';
import { getSetting } from '../../stores/settings-store.svelte';
import { convertFileSrc } from '@tauri-apps/api/core';
import CodeEditor from './CodeEditor.svelte';
import PdfViewer from './PdfViewer.svelte';

View file

@ -18,7 +18,7 @@
import { deriveIdentifier, type GroupAgentRole, AGENT_ROLE_ICONS } from '../../types/groups';
import { ProjectId, GroupId } from '../../types/ids';
import { generateAgentPrompt } from '../../utils/agent-prompts';
import { getSetting, setSetting } from '../../adapters/settings-bridge';
import { getSetting, setSetting } from '../../stores/settings-store.svelte';
import { getCurrentTheme, setTheme } from '../../stores/theme.svelte';
import { THEME_LIST, getPalette, type ThemeId } from '../../styles/themes';
import { listProfiles, type ClaudeProfile } from '../../adapters/claude-bridge';

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { getSetting, setSetting } from '../../adapters/settings-bridge';
import { getSetting, setSetting } from '../../stores/settings-store.svelte';
import { getPluginEntries, setPluginEnabled, reloadAllPlugins } from '../../stores/plugins.svelte';
import { checkForUpdates, getCurrentVersion, getLastCheckTimestamp } from '../../utils/updater';
import type { UpdateInfo } from '../../utils/updater';

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { getSetting, setSetting } from '../../adapters/settings-bridge';
import { getSetting, setSetting } from '../../stores/settings-store.svelte';
import { getProviders } from '../../providers/registry.svelte';
import type { ProviderSettings } from '../../providers/types';
import { invoke } from '@tauri-apps/api/core';

View file

@ -2,7 +2,7 @@
import { onMount } from 'svelte';
import { getCurrentTheme, setTheme, setCustomTheme } from '../../stores/theme.svelte';
import { THEME_LIST, getPalette, type ThemeId } from '../../styles/themes';
import { getSetting, setSetting } from '../../adapters/settings-bridge';
import { getSetting, setSetting } from '../../stores/settings-store.svelte';
import { handleError } from '../../utils/handle-error';
import { type CustomTheme, loadCustomThemes, deleteCustomTheme as deleteCustom, saveCustomThemes } from '../../styles/custom-themes';
import ThemeEditor from '../ThemeEditor.svelte';

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { getSetting, setSetting } from '../../adapters/settings-bridge';
import { getSetting, setSetting } from '../../stores/settings-store.svelte';
import { ANCHOR_BUDGET_SCALES, ANCHOR_BUDGET_SCALE_LABELS, type AnchorBudgetScale } from '../../types/anchors';
import { WAKE_STRATEGIES, WAKE_STRATEGY_LABELS, WAKE_STRATEGY_DESCRIPTIONS, type WakeStrategy } from '../../types/wake';
import { handleError, handleInfraError } from '../../utils/handle-error';

View file

@ -6,7 +6,7 @@
addProject, removeProject, addGroup, removeGroup, switchGroup,
updateProject,
} from '../../stores/workspace.svelte';
import { getSetting } from '../../adapters/settings-bridge';
import { getSetting } from '../../stores/settings-store.svelte';
import { setActiveProject } from '../../stores/settings-scope.svelte';
let newGroupName = $state('');

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { getSetting, setSetting } from '../../adapters/settings-bridge';
import { getSetting, setSetting } from '../../stores/settings-store.svelte';
import { storeSecret, getSecret, deleteSecret, listSecrets, hasKeyring, knownSecretKeys, SECRET_KEY_LABELS } from '../../adapters/secrets-bridge';
import { handleError, handleInfraError } from '../../utils/handle-error';

View file

@ -5,7 +5,7 @@
import type { PluginMeta } from '../adapters/plugins-bridge';
import { discoverPlugins } from '../adapters/plugins-bridge';
import { getSetting, setSetting } from '../adapters/settings-bridge';
import { getSetting, setSetting } from './settings-store.svelte';
import { loadPlugin, unloadPlugin, unloadAllPlugins, getLoadedPlugins } from '../plugins/plugin-host';
import { handleInfraError } from '../utils/handle-error';
import type { GroupId, AgentId } from '../types/ids';

View file

@ -2,7 +2,7 @@
// Provides scoped get/set: reads project-level override if exists, falls back to global.
// Single source of truth for which scope is active and how overrides cascade.
import { getSetting, setSetting } from '../adapters/settings-bridge';
import { getSetting, setSetting } from './settings-store.svelte';
type Scope = 'global' | 'project';

View file

@ -0,0 +1,31 @@
// Settings store — thin accessor over BackendAdapter for settings persistence.
// Replaces the old settings-bridge.ts (which imported Tauri invoke directly).
// All consumers should import from here instead of settings-bridge.
import { getBackend } from '../backend/backend';
/**
* Get a setting value by key. Returns null if not found.
*/
export async function getSetting(key: string): Promise<string | null> {
return getBackend().getSetting(key);
}
/**
* Set a setting value by key.
*/
export async function setSetting(key: string, value: string): Promise<void> {
return getBackend().setSetting(key, value);
}
/**
* Get all settings as a key-value map.
* Note: returns Record<string, string> (SettingsMap), not [string, string][].
* Callers that used listSettings() should adapt to the map format.
*/
export async function getAllSettings(): Promise<Record<string, string>> {
return getBackend().getAllSettings();
}
// Re-export for backward compat with code that imported listSettings
export { getAllSettings as listSettings };

View file

@ -1,6 +1,6 @@
// Theme store — persists theme selection via settings bridge
import { getSetting, setSetting } from '../adapters/settings-bridge';
import { getSetting, setSetting } from './settings-store.svelte';
import { handleInfraError } from '../utils/handle-error';
import {
type ThemeId,

View file

@ -1,6 +1,6 @@
// Custom theme persistence — store, load, validate, import/export
import { getSetting, setSetting } from '../adapters/settings-bridge';
import { getSetting, setSetting } from '../stores/settings-store.svelte';
import { handleError, handleInfraError } from '../utils/handle-error';
import { type ThemePalette, type ThemeId, getPalette, PALETTE_KEYS } from './themes';