diff --git a/.changeset/FN-7813-worktree-terminal-tab.md b/.changeset/FN-7813-worktree-terminal-tab.md new file mode 100644 index 0000000000..ca330a9eb0 --- /dev/null +++ b/.changeset/FN-7813-worktree-terminal-tab.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add an interactive worktree-rooted Terminal tab to the task detail view. +category: feature +dev: TaskDetailModal embeds TerminalModal in a new `embedded` mode; useTerminalSessions gains task-scoped storage + defaultCwd. The pre-existing agent-session tab is relabeled "Session". diff --git a/.changeset/FN-7824-terminal-first-launch-autoreconnect.md b/.changeset/FN-7824-terminal-first-launch-autoreconnect.md new file mode 100644 index 0000000000..58bd689cd9 --- /dev/null +++ b/.changeset/FN-7824-terminal-first-launch-autoreconnect.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Terminal now auto-reconnects on first launch instead of getting stuck on "Disconnected". +category: fix +dev: useTerminal tracks whether the socket has ever opened; a never-connected initial connect keeps retrying at capped backoff (staying "reconnecting") until it opens, while mid-session drops and 4000/4004 permanent closes are unchanged. diff --git a/.changeset/FN-7826-worktree-terminal-always-available.md b/.changeset/FN-7826-worktree-terminal-always-available.md new file mode 100644 index 0000000000..cb2dd6375b --- /dev/null +++ b/.changeset/FN-7826-worktree-terminal-always-available.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: The Task Detail Terminal tab is now always available, falling back to the project root when a task has no worktree. +category: feature +dev: Relaxes the TaskDetailModal `showWorktreeTerminalTab` gate to always render and passes `defaultCwd` = worktree when present else undefined (project-root auto-create via useTerminalSessions). Covers no-worktree and multi-repo workspace tasks. Sessions stay task-scoped via `scopeId`. diff --git a/.changeset/artifact-pipeline-and-gallery.md b/.changeset/artifact-pipeline-and-gallery.md new file mode 100644 index 0000000000..4e73008f5c --- /dev/null +++ b/.changeset/artifact-pipeline-and-gallery.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Agents save screenshots, videos, HTML mockups, and PDFs as artifacts, shown in a new category gallery with doc editing. +category: feature +dev: fn_artifact_register gains a `path` payload source (file copied into managed storage, MIME inference, image/video/PDF signature validation) and is now always exposed to executor sessions (previously missing in ephemeral mode) with worktree-relative path resolution and executing-task default taskId; executor/planning prompts instruct agents to register visual/media deliverables (images, videos, HTML mockups, PDFs); the media route serves HTTP byte ranges for video/audio seeking; video attachments (100MB cap) bridge into the registry like images; HTML docs render as live sandboxed previews; new `GET`/`PATCH /api/artifacts/:id` routes plus `TaskStore.updateArtifact` and the `artifact:updated` SSE event power in-place doc editing in the new ArtifactsGallery (Images/Docs/PDFs/Videos/Audio/Other sections with per-category viewers, mobile-responsive); viewers open in draggable/resizable FloatingWindows, Artifacts is the first/landing tab of the view, and mobile tab buttons render at the uniform 44px control height. diff --git a/.changeset/artifacts-doc-editing-and-comment-fix.md b/.changeset/artifacts-doc-editing-and-comment-fix.md new file mode 100644 index 0000000000..d7557a167e --- /dev/null +++ b/.changeset/artifacts-doc-editing-and-comment-fix.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Edit task documents and project files in Artifacts, with Markdown previews and working add-comment controls. +category: feature +dev: DocumentsView embeds the shared CodeMirror FileEditor for task-document (PUT /tasks/:id/documents/:key) and project-file (project workspace file API) edits. The Add comment no-op was a CSS bundle-order regression — `.btn:active` out-ordered the equal-specificity trigger rule; the `:active` rules now use `.btn.selection-comment-trigger` (0,3,0) with a test asserting the prefix. diff --git a/.changeset/badge-snapshot-archive-eviction.md b/.changeset/badge-snapshot-archive-eviction.md new file mode 100644 index 0000000000..bb527b56cf --- /dev/null +++ b/.changeset/badge-snapshot-archive-eviction.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix a slow dashboard memory leak where archived tasks were never evicted from the in-memory badge cache. +category: performance +dev: The badge-snapshot cache (packages/dashboard/src/server.ts) only removed a task on hard-delete, so archiving a task re-cached it via the task:updated listener and it was retained for the daemon's lifetime — unbounded growth over long uptimes with task churn. A new `isBadgeEligibleTask` predicate (column !== "archived") gates both the create and update listeners so archived tasks are evicted, matching the startup prime's `includeArchived:false`. An unarchive re-primes the entry. diff --git a/.changeset/bright-cost-subtotals.md b/.changeset/bright-cost-subtotals.md new file mode 100644 index 0000000000..fdfa3591a1 --- /dev/null +++ b/.changeset/bright-cost-subtotals.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Show partial estimated costs across all Command Center cost views when some model pricing is unavailable. +category: fix +dev: Priced subtotals use a trailing plus sign; entirely unpriced usage remains unavailable. diff --git a/.changeset/calm-settings-navigation.md b/.changeset/calm-settings-navigation.md new file mode 100644 index 0000000000..dbbe4b0d91 --- /dev/null +++ b/.changeset/calm-settings-navigation.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add a persistent Advanced settings toggle that keeps uncommon Settings sections and controls hidden by default. +category: feature +dev: The browser-local disclosure applies to navigation, search, and field-level controls without changing saved settings. diff --git a/.changeset/clean-compound-pipeline.md b/.changeset/clean-compound-pipeline.md new file mode 100644 index 0000000000..c0f2de9fc5 --- /dev/null +++ b/.changeset/clean-compound-pipeline.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Guide repeatable Compound Engineering cycles from product grounding through reusable learnings. +category: feature +dev: Adds project-scoped sessions, collection-aware stages, Work quality gates, and terminal Compound progression. diff --git a/.changeset/daemon-signal-nonzero-exit.md b/.changeset/daemon-signal-nonzero-exit.md new file mode 100644 index 0000000000..39bd68aa04 --- /dev/null +++ b/.changeset/daemon-signal-nonzero-exit.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Daemon exits non-zero on signal termination so Restart=on-failure restarts it after a memory-pressure kill. +category: fix +dev: `fn daemon` and `fn serve` (packages/cli/src/commands/daemon.ts, serve.ts) now exit with the POSIX 128+signal code (SIGTERM=143, SIGINT=130) on signal-initiated graceful shutdown instead of 0. Previously a memory-pressure SIGTERM produced exit 0, which `Restart=on-failure` treated as a clean stop, leaving the daemon dead. A deliberate `systemctl stop` still won't restart (systemd honors the requested inactive state regardless of exit code); a non-signal shutdown still exits 0. The interactive TUI launcher (`fn dashboard`) is intentionally unchanged — it has its own signal-name-keyed restart supervisor. diff --git a/.changeset/embedded-postgres-lifecycle.md b/.changeset/embedded-postgres-lifecycle.md new file mode 100644 index 0000000000..fe024e0481 --- /dev/null +++ b/.changeset/embedded-postgres-lifecycle.md @@ -0,0 +1,8 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Bundle embedded PostgreSQL for zero-system-install local storage when DATABASE_URL is unset. +category: feature +dev: Adds `embedded-postgres` lifecycle manager (initdb/pg_ctl start/stop, graceful SIGTERM/SIGINT shutdown, data persistence across restarts). Platform binaries bundled for macOS/Linux/Windows arm64/x64. Used by `createTaskStoreForBackend` when DATABASE_URL is unset. + diff --git a/.changeset/embedded-postgres-macos-dylib-links.md b/.changeset/embedded-postgres-macos-dylib-links.md new file mode 100644 index 0000000000..411d3033ba --- /dev/null +++ b/.changeset/embedded-postgres-macos-dylib-links.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Repair macOS embedded PostgreSQL dylib compatibility links before startup. +category: fix +dev: Adds an idempotent embedded-postgres macOS preflight that creates missing ABI-name symlinks such as `libpq.5.dylib` and `libzstd.1.dylib` from bundled versioned dylibs before `initdb`/`postgres` spawn, fixing zero-config startup when package symlink hydration is absent or incomplete. diff --git a/.changeset/fix-agent-runs-route-backend-agentstore.md b/.changeset/fix-agent-runs-route-backend-agentstore.md new file mode 100644 index 0000000000..bc05819c3a --- /dev/null +++ b/.changeset/fix-agent-runs-route-backend-agentstore.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix manual agent-run creation failing on PostgreSQL when a heartbeat executor is attached. +category: fix +dev: POST /api/agents/:id/runs built its AgentStore without the scoped store's AsyncDataLayer on the heartbeat-executor branch, hitting the removed SQLite runtime in backend mode; it now borrows the layer like the record-only branch. diff --git a/.changeset/fix-stale-planning-background-task.md b/.changeset/fix-stale-planning-background-task.md new file mode 100644 index 0000000000..5318ec316f --- /dev/null +++ b/.changeset/fix-stale-planning-background-task.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Prevent stale Planning notifications from pointing to missing sessions. +category: fix +dev: Background task sync now defers to the authoritative server session list. diff --git a/.changeset/flip-embedded-pg-default.md b/.changeset/flip-embedded-pg-default.md new file mode 100644 index 0000000000..cbb4e70c27 --- /dev/null +++ b/.changeset/flip-embedded-pg-default.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Default local backend is now embedded PostgreSQL; set FUSION_NO_EMBEDDED_PG=1 for legacy SQLite. +category: feature +dev: `createTaskStoreForBackend` now boots embedded PostgreSQL by default when DATABASE_URL is unset (previously required FUSION_EMBEDDED_PG=1). FUSION_EMBEDDED_PG=1 is now a no-op alias; FUSION_NO_EMBEDDED_PG=1 is the opt-out back to legacy SQLite. `embedded-postgres` is now a direct dependency of @runfusion/fusion so the bundled CLI can resolve the platform binary at runtime. Boot smoke exercises the embedded path by default (initdb-aware 180s health timeout). Also hardens three backend-mode gaps the flip exposed: ResearchStore/insights router/watch() now degrade gracefully instead of crashing `fn serve` when the sync SQLite satellite stores are unavailable in PG backend mode. diff --git a/.changeset/fn-7798-usage-configured-providers.md b/.changeset/fn-7798-usage-configured-providers.md new file mode 100644 index 0000000000..4e24c4e3dc --- /dev/null +++ b/.changeset/fn-7798-usage-configured-providers.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Usage view now shows meters only for AI providers you have configured. +category: fix +dev: fetchAllProviderUsage() in packages/dashboard/src/usage.ts filters providers with no resolved credentials and no meterable entitlement (e.g. GitHub 404 "No Copilot subscription found" reclassified error→no-auth); configured-but-failing providers (auth expired / HTTP 5xx / timeout) remain visible. diff --git a/.changeset/fn-7799-git-detection-false-negative.md b/.changeset/fn-7799-git-detection-false-negative.md new file mode 100644 index 0000000000..646bd0fc19 --- /dev/null +++ b/.changeset/fn-7799-git-detection-false-negative.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix a false "Project directory is not a Git repository" error that blocked all task execution in valid repos. +category: fix +dev: Git detection is now tri-state (repo/not-repo/error) via detectGitRepository(); dubious-ownership/PATH/timeout git failures no longer masquerade as "not a Git repository". FN-7799. diff --git a/.changeset/fn-7801-grok-xai-logo.md b/.changeset/fn-7801-grok-xai-logo.md new file mode 100644 index 0000000000..4d55de01ad --- /dev/null +++ b/.changeset/fn-7801-grok-xai-logo.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Show the xAI logo for Grok model IDs across dashboard provider surfaces. +category: fix +dev: ProviderIcon now falls back through inferProviderIconKey before the generic CPU icon. diff --git a/.changeset/fn-7802-phantom-worktree-merge-active-recovery.md b/.changeset/fn-7802-phantom-worktree-merge-active-recovery.md new file mode 100644 index 0000000000..b8e8560ed4 --- /dev/null +++ b/.changeset/fn-7802-phantom-worktree-merge-active-recovery.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Recover tasks stranded by missing worktrees during merge/review and allow retry. +category: fix +dev: Adds merge-active missing-worktree self-healing with no-action audits and signature-only retry resets across CLI, extension, and dashboard. diff --git a/.changeset/fn-7804-mobile-settings-version.md b/.changeset/fn-7804-mobile-settings-version.md new file mode 100644 index 0000000000..d917cc8403 --- /dev/null +++ b/.changeset/fn-7804-mobile-settings-version.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Mobile Settings footer shows the compact "v0.x" version instead of the full word. +category: fix +dev: SettingsModal picks settings.footer.versionShort ("v{{version}}") when viewportMode === "mobile". diff --git a/.changeset/fn-7805-quick-add-priority-indicators.md b/.changeset/fn-7805-quick-add-priority-indicators.md new file mode 100644 index 0000000000..e7e0d28d50 --- /dev/null +++ b/.changeset/fn-7805-quick-add-priority-indicators.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Quick-add composer now shows icon-only priority/Fast controls with GitHub tracking beside attach. +category: feature +dev: QuickEntryBox + TaskForm reuse a shared priorityIndicator glyph helper; GitHub + Priority relocated into .quick-entry-primary-group; no test-id/payload changes. diff --git a/.changeset/fn-7806-gemini-usage.md b/.changeset/fn-7806-gemini-usage.md new file mode 100644 index 0000000000..1a12bd2cde --- /dev/null +++ b/.changeset/fn-7806-gemini-usage.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Usage view now hides Gemini when it isn't configured for metering or its login has expired. +category: fix +dev: fetchGeminiUsage() in packages/dashboard/src/usage.ts reclassifies the unsupported-auth-type (api-key/vertex-ai) and HTTP 401/403 outcomes from error→no-auth so fetchAllProviderUsage omits Gemini; transient failures (HTTP 5xx/network/timeout) of a configured token remain visible as error. diff --git a/.changeset/fn-7809-list-tablet-single-pane.md b/.changeset/fn-7809-list-tablet-single-pane.md new file mode 100644 index 0000000000..26382ca1ca --- /dev/null +++ b/.changeset/fn-7809-list-tablet-single-pane.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix the List view controls and quick-add box being cut off on tablet-width screens. +category: fix +dev: ListView collapses to a single-pane layout at the `useViewportMode()` "tablet" tier (769–1024px) instead of the desktop two-pane split, which lacked horizontal room and clipped the primary action cluster and expanded QuickEntryBox. Split-vs-single now keys off a shared narrow gate; touch-only long-press stays gated on mobile. diff --git a/.changeset/fn-7811-task-documents-sidebar.md b/.changeset/fn-7811-task-documents-sidebar.md new file mode 100644 index 0000000000..e833d8be68 --- /dev/null +++ b/.changeset/fn-7811-task-documents-sidebar.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Artifacts view — Task Documents now uses a left-sidebar list with a right-pane content viewer. +category: feature +dev: DocumentsView Task Documents tab reuses the Project Files `documents-project-layout` sidebar/right-pane pattern with a separate selection state and desktop/mobile gating; the markdown/plain toggle is preserved. Select-to-comment stays Project-Files-only (tracked as a follow-up). diff --git a/.changeset/fn-7812-task-documents-select-to-comment.md b/.changeset/fn-7812-task-documents-select-to-comment.md new file mode 100644 index 0000000000..03b350ae17 --- /dev/null +++ b/.changeset/fn-7812-task-documents-select-to-comment.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Artifacts view — select text in a Task Document's content pane to comment and send it to a new task. +category: feature +dev: DocumentsView Task Documents right pane reuses the Project Files `useSelectionComment`/`SelectionCommentPopover` pattern (markdown + plain refs following the render toggle, composer-open lock, popover gated on the task-document selection + `onSendSelectionToTask`). Project Files behavior and the markdown/plain toggle are unchanged. Depends on FN-7811. diff --git a/.changeset/fn-7814-grok-usage.md b/.changeset/fn-7814-grok-usage.md new file mode 100644 index 0000000000..6f87dfc55c --- /dev/null +++ b/.changeset/fn-7814-grok-usage.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Show a Grok (xAI) card in the Usage dropdown for configured Grok API keys. +category: feature +dev: usage.ts adds fetchGrokUsage (env GROK_API_KEY -> ~/.grok/user-settings.json -> grok-cli auth key) validating GET https://api.x.ai/v1/api-key and registered in fetchAllProviderUsage. xAI exposes no subscription usage meter to the inference key, so the card is auth-validity (ok/no-auth/error) with a real usage window only when confirmed data exists; no fabricated windows. Real usage field found: no — validity-only. diff --git a/.changeset/fn-7816-cursor-usage.md b/.changeset/fn-7816-cursor-usage.md new file mode 100644 index 0000000000..e8635853a1 --- /dev/null +++ b/.changeset/fn-7816-cursor-usage.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Show Cursor subscription usage in the Usage dropdown. +category: feature +dev: usage.ts adds fetchCursorUsage via Cursor Admin API POST https://api.cursor.com/teams/spend with Basic auth API_KEY:, resolving the Admin API key from documented env `CURSOR_ADMIN_API_KEY` (or `CURSOR_API_KEY` alias) before internal test/auth-storage fallbacks. It maps teamMemberSpend overallSpendCents/spendCents plus hardLimitOverrideDollars/monthlyLimitDollars and subscriptionCycleStart; fetchAllProviderUsage wraps it with withTimeout and no-auth demotion, while UsageIndicator maps "Cursor" to cursor-cli. No personal Cursor CLI usage endpoint confirmed; CLI session only supplies userEmail/subscriptionTier metadata. diff --git a/.changeset/fn-7817-cursor-api-key.md b/.changeset/fn-7817-cursor-api-key.md new file mode 100644 index 0000000000..e84f4576e0 --- /dev/null +++ b/.changeset/fn-7817-cursor-api-key.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add a documented CURSOR_API_KEY credential path for Cursor usage metering. +category: feature +dev: usage.ts adds readCursorApiKey (CURSOR_API_KEY env var → cursor authStorage entry, mirroring readGrokApiKey); settings-reference.md documents it and clarifies cursor-cli runtime OAuth vs the usage/admin API key. Unblocks FN-7816. Cursor usage-API specifics confirmed via Cursor Admin API docs: POST /teams/spend with Basic auth using an admin:* API key as the username. diff --git a/.changeset/fn-7818-cursor-logo.md b/.changeset/fn-7818-cursor-logo.md new file mode 100644 index 0000000000..34064c73e1 --- /dev/null +++ b/.changeset/fn-7818-cursor-logo.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Use the real Cursor logo in the usage dropdown, model selection, and other provider surfaces. +category: fix +dev: Replaces the placeholder CursorCliIcon SVG with the Cursor brand mark and adds a `cursor` → `cursor-cli` mapping in inferProviderIconKey. diff --git a/.changeset/fn-7820-cost-tab-and-card-badge.md b/.changeset/fn-7820-cost-tab-and-card-badge.md new file mode 100644 index 0000000000..7f73697a38 --- /dev/null +++ b/.changeset/fn-7820-cost-tab-and-card-badge.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add a Cost tab to task detail and an optional per-card cost badge (default off). +category: feature +dev: New shared taskTokenCost helper (read-time costFor derivation) powers the Summary tab, the new Cost tab, and a card badge gated by the default-off project setting showCostBadgeOnCards. diff --git a/.changeset/fn-7821-oauth-expiry-notification-banner-consistency.md b/.changeset/fn-7821-oauth-expiry-notification-banner-consistency.md new file mode 100644 index 0000000000..3b5baabbdf --- /dev/null +++ b/.changeset/fn-7821-oauth-expiry-notification-banner-consistency.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Stop false "OAuth token expired" push notifications for providers that silently refresh (e.g. GitHub Copilot). +category: fix +dev: OAuthExpiryMonitor.check() now attempts a best-effort getApiKey refresh and re-checks the credential before dispatching oauth-token-expired, mirroring /api/auth/status's refresh-then-recheck that drives OAuthReloginBanner. The FN-7574 start-refresher-first ordering only covered the startup check; short-lived auto-refreshing tokens still fired on interval ticks with no matching banner. diff --git a/.changeset/fn-7823-terminal-header-nowrap.md b/.changeset/fn-7823-terminal-header-nowrap.md new file mode 100644 index 0000000000..aa22625ab2 --- /dev/null +++ b/.changeset/fn-7823-terminal-header-nowrap.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix terminal header wrapping and spacing when the panel is narrow. +category: fix +dev: Header shortcut/status text now stays nowrap so terminal actions scroll horizontally instead of wrapping. diff --git a/.changeset/fn-7825-settings-nav-resizable.md b/.changeset/fn-7825-settings-nav-resizable.md new file mode 100644 index 0000000000..2462c6701f --- /dev/null +++ b/.changeset/fn-7825-settings-nav-resizable.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add a resizable Settings navigation rail that remembers its width. +category: feature +dev: Removes the Settings rail divider and keeps nav labels single-line across modal and embedded Settings. diff --git a/.changeset/fuzzy-ai-merge-recovery.md b/.changeset/fuzzy-ai-merge-recovery.md new file mode 100644 index 0000000000..0a8ea39da9 --- /dev/null +++ b/.changeset/fuzzy-ai-merge-recovery.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Make stranded AI merge recovery bind to the reviewed clean-room commit. +category: fix +dev: Avoids ambiguous same-task clean-room recovery and honors cancellation before pre-prune landing. diff --git a/.changeset/fuzzy-notification-padding.md b/.changeset/fuzzy-notification-padding.md new file mode 100644 index 0000000000..22118d9b44 --- /dev/null +++ b/.changeset/fuzzy-notification-padding.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix cramped padding on the Notifications "Failure notification mode" settings card. +category: fix +dev: Wrap the failure-notification card fields in `.notification-provider-body` in NotificationsSection so it matches sibling provider-card padding on desktop and mobile. diff --git a/.changeset/grok-cli-chat-routing-and-stale-bundle.md b/.changeset/grok-cli-chat-routing-and-stale-bundle.md index 47007f87c4..d258036a0d 100644 --- a/.changeset/grok-cli-chat-routing-and-stale-bundle.md +++ b/.changeset/grok-cli-chat-routing-and-stale-bundle.md @@ -4,4 +4,4 @@ summary: Fix Grok CLI chat returning errors or empty replies in the dashboard. category: fix -dev: Two independent defects. (1) The default (no-project) ChatManager received a bare PluginLoader as its runner; Grok CLI routing (deriveGrokRuntimeHintForNoVisibleKey → resolveRuntime) calls getRuntimeById/createRuntimeContext, which only exist on PluginRunner, so a grok-cli/* chat with no Fusion-visible GROK_API_KEY threw "getRuntimeById is not a function" and surfaced the misleading "requires the bundled Grok CLI runtime" error. New resolveChatManagerPluginRunner(options) prefers the engine's PluginRunner (the runner the project-scoped chat path already uses), falling back to the loader only in UI-only mode. (2) The CLI-bundled Grok plugin (packages/cli/dist/plugins/fusion-plugin-grok-runtime/bundled.js) was stale relative to the FN-7796 single-JSON adapter source, so project-scoped grok chat produced empty replies; a `pnpm build` (tsup) regenerates it. The bundled-plugin freshness guard only warns — the dev prebuild does not rebuild the CLI tsup bundle. +dev: Two independent defects. (1) The default (no-project) ChatManager received a bare PluginLoader as its runner; Grok CLI routing (deriveGrokRuntimeHintForNoVisibleKey → resolveRuntime) calls getRuntimeById/createRuntimeContext, which only exist on PluginRunner, so a grok-cli/* chat with no Fusion-visible GROK_API_KEY threw "getRuntimeById is not a function" and surfaced the misleading "requires the bundled Grok CLI runtime" error. New resolveChatManagerPluginRunner(options) prefers the engine's PluginRunner (the runner the project-scoped chat path already uses), falling back to the loader only in UI-only mode. (2) In a source checkout the running dashboard resolved the staged CLI tsup bundle (packages/cli/dist/plugins/fusion-plugin-grok-runtime/bundled.js), which resolvePluginEntryPath prefers verbatim with no freshness check; that bundle was stale vs the FN-7796 single-JSON adapter source (the FN-7779 dev prebuild rebuilds each plugin's own dist but never the staged bundle), so project-scoped grok chat produced empty replies. Fixed durably: getCandidatePluginDirs now probes the live workspace source dir (/plugins/) before the staged bundle, so dev loads the freshness-checked live plugin (self-healing even when the prebuild is skipped). Published installs are unaffected (no workspace dir). A one-time `pnpm build` refreshes any already-stale staged bundle. diff --git a/.changeset/memory-insights-todo-ui-cleanup.md b/.changeset/memory-insights-todo-ui-cleanup.md new file mode 100644 index 0000000000..5da4f9dd22 --- /dev/null +++ b/.changeset/memory-insights-todo-ui-cleanup.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix Memory insights parsing and modernize the Memory, Insights, Todos, and agent Memory views. +category: fix +dev: parseInsightsContent filtered bullets after stripping their prefix, collapsing every category into one blob; useMemoryData drops the dead GET /memory and /memory/stats mount fetches and no longer refetches the file list on selection; Engines tab is a 2-column card grid; Todo items are single-row with a quiet inline action cluster; the agent Memory tab uses the shared FileEditor with per-section save actions and fixes the agents.memoryFileMeta {{date}} interpolation. diff --git a/.changeset/mission-lifecycle-invariants.md b/.changeset/mission-lifecycle-invariants.md new file mode 100644 index 0000000000..787adf19ee --- /dev/null +++ b/.changeset/mission-lifecycle-invariants.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Reconcile completed and stale generated-fix mission invariants. +category: fix +dev: Completed missions now normalize autopilot/auto-advance to inactive during autopilot completion, polling, and restart recovery. Mission reconciliation also supersedes generated fix features whose own validator state is already passed, and the scheduler startup sweep runs stale generated-fix reconciliation before trying to relink or retriage active slice features. This prevents complete missions from remaining watched and prevents stale generated fix rows from keeping otherwise-drained missions administratively active. diff --git a/.changeset/pg-artifacts-documents-evals.md b/.changeset/pg-artifacts-documents-evals.md new file mode 100644 index 0000000000..3698df6624 --- /dev/null +++ b/.changeset/pg-artifacts-documents-evals.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix Artifacts, Documents, and Evals dashboard views returning 500 in PostgreSQL mode. +category: fix +dev: listArtifactsImpl/getAllDocumentsImpl now branch on store.backendMode and delegate to AsyncDataLayer helpers (listArtifacts/getAllDocuments in async-comments-attachments.ts); getEvalStore() returns a new AsyncEvalStore (async-eval-store.ts) in backend mode. evals-routes await the store calls; eval-automation/eval-followups handle the EvalStore | AsyncEvalStore union (instanceof guard / await). diff --git a/.changeset/pg-cli-agent-tools-backend.md b/.changeset/pg-cli-agent-tools-backend.md new file mode 100644 index 0000000000..6adfdcb1b3 --- /dev/null +++ b/.changeset/pg-cli-agent-tools-backend.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: CLI agent tools now boot PostgreSQL instead of the removed SQLite runtime. +category: fix +dev: The extension's getStore(cwd) path constructed a legacy SQLite TaskStore (runtime removed under VAL-REMOVAL-005), and the fn_agent_* tools constructed AgentStore without an asyncLayer — both threw "SQLite Database class body has been removed" in PG mode. getStore now routes through createTaskStoreForBackend (mirroring fn serve) and caches the boot result for deterministic shutdown; a new getAgentStore(cwd) helper injects the project store's asyncLayer into AgentStore so agent data lives in PostgreSQL. CLI extension tests were migrated to a shared PG harness (pg-extension-harness.ts) backed by an isolated test database with a test-only store-injection hook (__setCachedStoreForTesting). diff --git a/.changeset/pg-command-center-analytics.md b/.changeset/pg-command-center-analytics.md new file mode 100644 index 0000000000..2bf155fe7f --- /dev/null +++ b/.changeset/pg-command-center-analytics.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Command Center productivity, team, token, and tool analytics work on the PostgreSQL backend. +category: feature +dev: Ports aggregateProductivityAnalytics/aggregateTeamAnalytics/aggregateTokenAnalytics/aggregateToolAnalytics to accept Database | AsyncDataLayer, adding a PG branch ("ping" in dbOrLayer) that runs schema-qualified raw SQL over project.tasks/task_commit_associations/pull_requests/agents/usage_events/approval_request_audit_events with snake_case columns and the same aggregation semantics as the SQLite path. The command-center tokens/tools/productivity/team routes pass getAsyncLayer() ?? getDatabase() and await; the interim 503 guards are removed. GitHub-issue, signal, and live-snapshot analytics remain 503 in PG mode (follow-up). Adds command-center-analytics.pg.test.ts to test:pg-gate. diff --git a/.changeset/pg-command-center-remaining-analytics.md b/.changeset/pg-command-center-remaining-analytics.md new file mode 100644 index 0000000000..b901094a87 --- /dev/null +++ b/.changeset/pg-command-center-remaining-analytics.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Command Center workflow, GitHub-issue, signal, and live-snapshot analytics now work on the PostgreSQL backend. +category: feature +dev: Ports aggregateWorkflowAnalytics/aggregateGithubIssueAnalytics/aggregateSignalsAnalytics/composeLiveSnapshot to accept Database | AsyncDataLayer, adding a PG branch ("ping" in dbOrLayer) that runs schema-qualified raw SQL over project.tasks/task_workflow_selection/workflows/incidents/cli_sessions/agent_runs with snake_case columns and the same aggregation semantics as the SQLite path. The command-center workflows/github/signals/live routes pass getAsyncLayer() ?? getDatabase() and await; the interim 503 guards are removed. Every /api/command-center/* route now functions in backend mode. Adds command-center-remaining-analytics.pg.test.ts to test:pg-gate. diff --git a/.changeset/pg-cutover-remaining-surfaces.md b/.changeset/pg-cutover-remaining-surfaces.md new file mode 100644 index 0000000000..e4ceef6776 --- /dev/null +++ b/.changeset/pg-cutover-remaining-surfaces.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Standalone CLI, GitLab analytics, and plugin stores now run on PostgreSQL. +category: fix +dev: Orchestrated audit + fix of remaining un-migrated SQLite surfaces. CLI: project-context/project-resolver + the fn task/agent/git/research/settings/desktop/experiment commands now boot via createTaskStoreForBackend and inject asyncLayer into AgentStore; fn_agent_update/fn_mission_list/mission-list gained backendMode branches. Core: 15 task-store Impls (merge-request record, commit-association upsert/read, stale-branch cleanup, run-audit-events read, task-document delete/revisions, github-tracking reconcile, activity/run-audit snapshots, occupants, stranded-refinements, orphaned-task-dir reconcile) gained backendMode branches (8 real Drizzle, 7 graceful sync-safe-defaults following the getTaskWorkflowSelection precedent); AgentStore gained backendMode branches for 9 snapshot/blocked-state/config-revision methods. Dashboard: GitLab analytics gained an async variant (aggregateGitlabIssueAnalyticsAsync) + the agent-token-totals + OTLP exporter paths now use the async layer. Plugins (compound-engineering pipeline-store, reports, cli-printing-press) gained isBackendMode degrade-guards. Added the missing project.chat_token_usage PG table (schema + migration + registry + created_at index) that the upstream merge referenced but never defined. Merge gate green (engine-core 287 + core pg-gate 94 + ci-shape 63); all packages typecheck clean. diff --git a/.changeset/pg-fork-review-fixes.md b/.changeset/pg-fork-review-fixes.md new file mode 100644 index 0000000000..3bcb5c531b --- /dev/null +++ b/.changeset/pg-fork-review-fixes.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Root project-scoped PostgreSQL stores and merges at the project directory, and fix backend-mode agent watching. +category: fix +dev: createTaskStoreForBackend honors an explicit rootDir over projectId re-resolution (stale bootstrap PROMPT.md pinned cards "unplanned"); drainMergeQueue roots git operations at store.getRootDir() (merges aborted with branch-missing in in-process dashboards); AgentStore.startWatching no longer trips the sqlite getLastModified gate in backend mode. diff --git a/.changeset/pg-goal-store-port.md b/.changeset/pg-goal-store-port.md new file mode 100644 index 0000000000..0963572193 --- /dev/null +++ b/.changeset/pg-goal-store-port.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Goals work on the PostgreSQL backend — the Goals view and mission goal-links load instead of erroring. +category: feature +dev: Ports GoalStore to the AsyncDataLayer. Adds AsyncGoalStore (over the existing async-goal-store.ts helpers; ACTIVE_GOAL_LIMIT enforced atomically in the helpers' transactionImmediate, same as sync). getGoalStoreImpl returns it in backend mode; the dashboard /api/goals routes await it and the interim 503 is removed. Reverts the PG-mode goal-resolution degradations added earlier — mission routes and `fn mission` now resolve/validate real linked goals on both backends. CLI goals/mission/extension and engine agent-tools converted to await; goal-injection-diagnostics stays on its instanceof-guarded sync fallback. Adds goal-store.pg.test.ts to test:pg-gate. diff --git a/.changeset/pg-insight-run-execution.md b/.changeset/pg-insight-run-execution.md new file mode 100644 index 0000000000..4660d0db1c --- /dev/null +++ b/.changeset/pg-insight-run-execution.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Generating insights works on the PostgreSQL backend — the insight run executor and stale-run sweeper run in PG mode. +category: feature +dev: Await-converts the insight run executor (insight-run-executor.ts) and the stale-run sweeper (insight-run-sweeper.ts) and widens their store type to InsightStore | AsyncInsightStore, so POST /api/insights/run and /runs/:id/retry drive the async store instead of throwing 503 (getSyncInsightStore removed). The startup/background/drive-by sweeper is now enabled for both backends. The AI extraction step still needs a configured provider at runtime; a run without one records a clean failed run rather than 503. Adds insight-run-execution.pg.test.ts (create→complete, create→fail, retry-with-lineage against embedded PG) to test:pg-gate. diff --git a/.changeset/pg-insight-store-port.md b/.changeset/pg-insight-store-port.md new file mode 100644 index 0000000000..833b7ed9c5 --- /dev/null +++ b/.changeset/pg-insight-store-port.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Insights work on the PostgreSQL backend — the Insights dashboard loads instead of erroring. +category: feature +dev: Ports InsightStore to the AsyncDataLayer. Adds AsyncInsightStore (wrapping async-insight-store.ts helpers, incl. 6 new helpers — updateInsight, updateInsightRun [faithful run-lifecycle state machine: terminal-immutable, transition validation, auto completed/cancelled timestamps], listInsightRunEvents, countInsights, countInsightRuns, listStalePendingRuns); getInsightStoreImpl returns it in backend mode; dashboard insights routes await it and the interim 503 is removed for the read/write/cancel surface. The 3 engine reporters stay on graceful fallback (instanceof-gated). Known partial: AI insight-run generation/retry (POST /run, /runs/:id/retry) and the stale-run sweeper remain sync-only and still 503 in PG mode until the run executor is ported. Adds insight-store.pg.test.ts to test:pg-gate. diff --git a/.changeset/pg-mailbox-send-fix.md b/.changeset/pg-mailbox-send-fix.md new file mode 100644 index 0000000000..3d1bf2774b --- /dev/null +++ b/.changeset/pg-mailbox-send-fix.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Mailbox — sending a message to an agent works in PG mode instead of erroring. +category: fix +dev: POST /api/messages to an agent 500'd in embedded-PG mode: MessageStore.sendMessage persisted the message via the async layer, then synchronously invoked the agent-delivery hook (agent-heartbeat.handleMessageToAgent), which reads the not-yet-ported sync AgentStore and throws. The persisted send must not fail on a notification side-effect, so the onMessageToAgent hook call is now wrapped — a hook failure logs and degrades (agent wake-on-message stays disabled in PG mode until AgentStore is ported) instead of failing the send. Adds message-store.pg.test.ts to test:pg-gate. diff --git a/.changeset/pg-mission-autopilot.md b/.changeset/pg-mission-autopilot.md new file mode 100644 index 0000000000..6a8d191b6c --- /dev/null +++ b/.changeset/pg-mission-autopilot.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Mission autopilot runs on the PostgreSQL backend — missions advance automatically instead of autopilot being disabled. +category: feature +dev: Await-converts MissionAutopilot to drive MissionStore | AsyncMissionStore (every this.missionStore.* call awaited; watchMission/unwatchMission/getAutopilotStatus and helpers async) and removes the instanceof MissionStore gates in InProcessRuntime (construction + recover paths) so the autopilot loop watches/recomputes/recovers in both backends. Slice execution + validator-loop methods stay scheduler-gated (degrade gracefully in PG). getAutopilotStatus async ripples through mission-routes/server. Adds mission-autopilot.pg.test.ts to test:pg-gate. diff --git a/.changeset/pg-mission-store-port.md b/.changeset/pg-mission-store-port.md new file mode 100644 index 0000000000..185ae5aecb --- /dev/null +++ b/.changeset/pg-mission-store-port.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Missions work on the PostgreSQL backend — the Missions dashboard and goal→mission links load instead of erroring. +category: feature +dev: Ports MissionStore (dashboard surface) to the AsyncDataLayer. Adds AsyncMissionStore (63 methods over the 71 existing async helpers + 8 new primitives), assembling the composites (getMissionWithHierarchy, listMissionsWithSummaries, mission/milestone health rollups, computeMissionStatus + the feature→slice→milestone→mission recompute cascade, triageFeature, getFeatureLoopSnapshot) by mirroring the sync store. getMissionStoreImpl returns it in backend mode; mission-routes + goal→mission routes await it and the interim 503 is removed (the GoalStore 503 stays — GoalStore is still deferred). Mission AUTOPILOT, live SSE mission events, mesh hierarchy snapshot apply/collect, and engine validator-loop methods stay degraded in PG mode behind instanceof guards. Also fixes the mission-create path which resolved linked goals via the unported sync GoalStore: goal resolution now degrades to empty in backend mode (links live in MissionStore; full Goal objects return once GoalStore is ported). Adds mission-store.pg.test.ts to test:pg-gate. diff --git a/.changeset/pg-mode-route-degradation.md b/.changeset/pg-mode-route-degradation.md new file mode 100644 index 0000000000..1d1488800c --- /dev/null +++ b/.changeset/pg-mode-route-degradation.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Not-yet-ported features (missions, insights, research, goals) degrade cleanly in PG mode instead of erroring. +category: fix +dev: Adds backendMode guards to the dashboard route choke-points that call satellite stores not yet on the AsyncDataLayer (getResearchStore/getInsightStore/getMissionStore/getGoalStore). They now return HTTP 503 "not yet available in PG backend mode" (matching the existing command-center team/productivity/token guards) instead of letting the store getter throw an unhandled 500. The SSE handler also degrades: ResearchStore access is wrapped so the event stream still serves every other event type instead of failing the whole connection when research run-events cannot be subscribed in PG mode. Full PG ports of these stores remain (TodoStore is done); these guards are the correct interim state until each lands. diff --git a/.changeset/pg-monitor-trait-agent-wake.md b/.changeset/pg-monitor-trait-agent-wake.md new file mode 100644 index 0000000000..9dc309bbd1 --- /dev/null +++ b/.changeset/pg-monitor-trait-agent-wake.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": fix +--- + +summary: Regression storm-guard and agent wake-on-message work on the PostgreSQL backend. +category: fix +dev: monitor-trait runMonitorOnRegression drops its backend-mode early return and routes the storm guard (countRecentAutoFixTasksAsync/claimIncidentForFixTaskAsync/attachFixTaskAsync/releaseIncidentFixTaskClaimAsync) through the AsyncDataLayer in PG, preserving the claim→createTask→attach→release semantics. The agent wake hook handleMessageToAgent becomes async and reads via AgentStore.getAgent (async) instead of the sync getCachedAgent that threw in PG; the onMessageToAgent hook type widens to allow a Promise and message-store awaits it inside its existing send-never-fails try/catch. diff --git a/.changeset/pg-production-readiness-fixes.md b/.changeset/pg-production-readiness-fixes.md new file mode 100644 index 0000000000..9e3d2e68f3 --- /dev/null +++ b/.changeset/pg-production-readiness-fixes.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix PostgreSQL-mode merge recovery, lost task-field writes, first-boot SQLite auto-migration, and backup tool discovery. +category: fix +dev: recoverStaleTransitionPending ported to backend mode (async-transition-pending.ts); backend moves now write/clear the crash-safe transitionPending marker; atomicWriteTaskJson/WithAudit write changed columns instead of full-row upserts (lost-update class behind stuck "unplanned" cards); createTaskStoreForBackend auto-migrates legacy fusion.db into an empty PG database on first boot (loud failure, SQLite kept as backup); PgBackupManager resolves pg_dump/pg_restore from common install locations when not on PATH. diff --git a/.changeset/pg-remove-node-settings-sync.md b/.changeset/pg-remove-node-settings-sync.md new file mode 100644 index 0000000000..7386a12a57 --- /dev/null +++ b/.changeset/pg-remove-node-settings-sync.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Remove node settings sync on the PostgreSQL backend — nodes share the database, so settings are already shared. +category: feature +dev: In backend mode the mesh sync route ignores inbound settings payloads and returns none; PeerExchangeService force-disables settings gossip; /nodes/:id/settings (fetch/push/pull/sync-status) and /settings/sync-receive answer 409 code settings-sync-disabled-postgres; the NodesView sync hook treats that 409 as a quiet steady state (no chips, no polling). Provider auth sync (/nodes/:id/auth/sync, auth-receive/auth-export) is intentionally kept — auth material is per-machine file state, not database state. diff --git a/.changeset/pg-research-execution.md b/.changeset/pg-research-execution.md new file mode 100644 index 0000000000..563fab63f8 --- /dev/null +++ b/.changeset/pg-research-execution.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Research runs actually execute on the PostgreSQL backend instead of staying queued forever. +category: feature +dev: Await-converts the engine ResearchOrchestrator + ResearchRunDispatcher to drive InsightStore | AsyncResearchStore (every this.store.* call awaited; addEvent→appendEvent for union compatibility) and removes the instanceof ResearchStore gate in ProjectEngine.start that disabled the orchestrator/dispatcher in PG mode. Exports AsyncResearchStore from @fusion/core. A queued run now advances queued→running→completed/failed in PG; the AI/web step still needs runtime providers (a run with none fails cleanly). Adds research-execution.pg.test.ts to test:pg-gate. diff --git a/.changeset/pg-research-store-port.md b/.changeset/pg-research-store-port.md new file mode 100644 index 0000000000..90fecc1e86 --- /dev/null +++ b/.changeset/pg-research-store-port.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Research works on the PostgreSQL backend — the Research dashboard loads and runs CRUD instead of erroring. +category: feature +dev: Ports ResearchStore to the AsyncDataLayer. Adds AsyncResearchStore (12 new helpers incl. faithful replicas of the run-lifecycle state machine — updateResearchStatus per-status auto-lifecycle fields, terminal-immutability, transition validation — and the retry gate/lineage in createResearchRetryRun); getResearchStoreImpl returns it in backend mode; dashboard research routes await it and the interim 503 is removed. AI research EXECUTION (engine ResearchOrchestrator/dispatcher, agent-tools research tools, CLI research run) stays degraded in PG mode behind instanceof guards — same boundary as the insight run executor. Adds research-store.pg.test.ts (13 tests incl. lifecycle machine + retry gate) to test:pg-gate. diff --git a/.changeset/pg-review-perf-fixes.md b/.changeset/pg-review-perf-fixes.md new file mode 100644 index 0000000000..807719da8f --- /dev/null +++ b/.changeset/pg-review-perf-fixes.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Speed up board listing and agent chat on PostgreSQL with SQL-side pagination and a conversation history cap. +category: performance +dev: Closes the two open PR #1793 review findings — readLiveTaskRows now pushes column filters + ORDER BY (created_at, numeric id suffix) + LIMIT/OFFSET into SQL instead of scanning and hydrating the whole task table per listTasks; getConversation is capped to the most recent 200 messages by default (options.limit overrides, oldest-first order preserved) in both the async and sqlite paths. diff --git a/.changeset/pg-signal-ingestion.md b/.changeset/pg-signal-ingestion.md new file mode 100644 index 0000000000..be0dc0ab72 --- /dev/null +++ b/.changeset/pg-signal-ingestion.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": fix +--- + +summary: Incident-signal ingestion records incidents on the PostgreSQL backend instead of being skipped. +category: fix +dev: ingestIncidentSignal now accepts Database | AsyncDataLayer and branches to ingestIncidentSignalAsync (project.incidents upsert by grouping key — absorb-or-create, occurrences/firstFiredAt preserved) in PG mode; the signal route awaits it instead of warn-skipping. monitor-trait's storm-guard helpers remain sync-only (async equivalents exist; follow-up). diff --git a/.changeset/pg-sse-live-push.md b/.changeset/pg-sse-live-push.md new file mode 100644 index 0000000000..7e90e7ba10 --- /dev/null +++ b/.changeset/pg-sse-live-push.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Live dashboard updates (SSE) work on the PostgreSQL backend for missions, research, and insights. +category: feature +dev: The async store wrappers (AsyncMissionStore/AsyncResearchStore/AsyncInsightStore) now extend EventEmitter and emit the same events as their sync counterparts at the same mutation points (after the persistence await), so the SSE handler's subscriptions fire in PG mode instead of no-op'ing. sse.ts/server.ts drop the instanceof-sync narrowing and subscribe to the union store in both backends. Live push for mission/milestone/slice/feature/assertion/validator-start, research run lifecycle, and insight create/update events. Validator-loop-completed and fix-feature emits remain sync-only (those methods aren't in AsyncMissionStore yet). diff --git a/.changeset/pg-workflow-definitions-read.md b/.changeset/pg-workflow-definitions-read.md new file mode 100644 index 0000000000..ae96cc1196 --- /dev/null +++ b/.changeset/pg-workflow-definitions-read.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Workflow definitions load in PG mode — /api/workflows no longer errors. +category: fix +dev: readAllWorkflowDefinitions/getWorkflowDefinition read custom rows from project.workflows via the AsyncDataLayer in backend mode (the sync store.db SELECT threw, 500'ing /api/workflows). New async-workflow-store.ts helpers re-stringify jsonb ir/layout for the shared toWorkflowDefinition mapper; builtins still come from code constants. Every caller already awaited these reads, so no consumer changes. Adds workflow-definitions.pg.test.ts to test:pg-gate. diff --git a/.changeset/pg-workflow-editing.md b/.changeset/pg-workflow-editing.md new file mode 100644 index 0000000000..fcbfbd0dd0 --- /dev/null +++ b/.changeset/pg-workflow-editing.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Creating, editing, and deleting custom workflows works on the PostgreSQL backend. +category: feature +dev: Completes the workflow-definition write path in PG. Adds a next_workflow_definition_id counter to project.config (schema + 0000_initial.sql baseline) with an async counter (nextWorkflowDefinitionIdAsyncImpl) that preserves project settings on bump; createWorkflowDefinitionImpl gains a backend branch that INSERTs into project.workflows via Drizzle (ir/layout as jsonb objects). Complements the update/delete/select backend branches in workflow-ops.ts. Adds workflow-create.pg.test.ts to test:pg-gate. diff --git a/.changeset/planner-overseer-eye-badge-docs.md b/.changeset/planner-overseer-eye-badge-docs.md new file mode 100644 index 0000000000..d0cfc89cb6 --- /dev/null +++ b/.changeset/planner-overseer-eye-badge-docs.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Document the planner-overseer eye badge on task cards. +category: internal +dev: Clarifies that the eye icon reflects non-idle plannerOverseerState, not a human view marker. \ No newline at end of file diff --git a/.changeset/postgres-backend-runtime-fixes.md b/.changeset/postgres-backend-runtime-fixes.md new file mode 100644 index 0000000000..f31ee1c91f --- /dev/null +++ b/.changeset/postgres-backend-runtime-fixes.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix PostgreSQL-mode crashes — agent-log flush no longer kills the server, and Command Center activity loads. +category: fix +dev: The agent-log buffer flush/append path (flushAgentLogBufferImpl, appendAgentLogBatchImpl, appendAgentLogImpl) dereferenced the SQLite-only `store.db` getter — which throws in PG backend mode — on an unref'd retry-timer and inside catch handlers, so a handled flush error became an uncaught exception that exited `fn serve` (~35s uptime). Guarded the deleted-task pre-filter and `bumpLastModified` with `!store.backendMode` and replaced every `store.db.path` log interpolation with the mode-safe `store.fusionDir`. Also schema-qualified raw async SQL that referenced project-schema tables unqualified / with camelCase columns: `project.deployments` + `project.incidents` with snake_case `deployed_at`/`opened_at`/`resolved_at` (the deployments read sat outside the try/catch and 500'd `/api/command-center/activity`), `project.experiment_session_records` (+ `::jsonb` cast on the payload update), and `project.agent_runs`. Adds a backend-mode regression test pinning the no-`store.db`-deref invariant across all three agent-log entry points. diff --git a/.changeset/postgres-create-workflow-selection-parity.md b/.changeset/postgres-create-workflow-selection-parity.md new file mode 100644 index 0000000000..7a2a65cb5c --- /dev/null +++ b/.changeset/postgres-create-workflow-selection-parity.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix task creation dropping the workflow selection when a workflow and step toggles are submitted together. +category: fix +dev: PostgreSQL create paths in task-creation.ts predated the SQLite-side FNXC:WorkflowCreation 2026-06-28 fix; they now record task_workflow_selection with explicit stepIds, and serialization.ts hydrates an explicit empty enabledWorkflowSteps as [] (not undefined). Store-integration coverage in builtin-workflows.test.ts ported to the shared PG harness (pgDescribe). diff --git a/.changeset/postgres-custom-column-create-and-move-parity.md b/.changeset/postgres-custom-column-create-and-move-parity.md new file mode 100644 index 0000000000..332cbcf194 --- /dev/null +++ b/.changeset/postgres-custom-column-create-and-move-parity.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix custom workflow columns on PostgreSQL: tasks land in their workflow's intake column and can move out of it. +category: fix +dev: Backend create paths now thread resolvedEntryColumn (workflow manual intake, e.g. Coding (Ideas) "ideas") into task creation and the bootstrap-prompt gate; move validation resolves the task workflow IR via getTaskWorkflowSelectionAsync in backend mode (the sync resolver silently fell back to builtin:coding and rejected every move out of a custom column). diff --git a/.changeset/postgres-cutover-residual-sqlite-call-sites.md b/.changeset/postgres-cutover-residual-sqlite-call-sites.md new file mode 100644 index 0000000000..a7eb821cac --- /dev/null +++ b/.changeset/postgres-cutover-residual-sqlite-call-sites.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix residual SQLite store constructions so chat, messages, backups, MCP secrets, and project setup work on PostgreSQL. +category: fix +dev: Routes remaining `new TaskStore`/`new AgentStore`/`createDatabase` call sites through `createTaskStoreForBackend`/`resolveAgentStoreBase` (chat.ts, message.ts, task.ts, pr.ts, backup.ts, memory-backup.ts, branch-group.ts, mcp.ts, project.ts, dashboard.ts getProjectStore, dashboard register-project-routes). Also fixes cli-printing-press plugin Drizzle row typing. diff --git a/.changeset/postgres-perf-and-standards.md b/.changeset/postgres-perf-and-standards.md new file mode 100644 index 0000000000..956c6ecb18 --- /dev/null +++ b/.changeset/postgres-perf-and-standards.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix PostgreSQL performance and credential-redaction gaps surfaced by the migration review. +category: performance +dev: Adds missing index on tasks.source_parent_task_id (lineage gate was a full scan) and a partial index for the live kanban `WHERE deleted_at IS NULL AND column = ?` read. Batches merge-queue stale-row cleanup to remove an N+1 on lease acquire. Pushes LIMIT into SQL for audit/activity-log queries. Drops the heavy `log` jsonb column from slim board hydration. Fixes the monitor-store backend discriminator (`"ping" in db`, not the ambiguous `"transactionImmediate" in db`), awaits the now-async resolveIncident in signal routes, and redacts `?password=` query-param URLs. diff --git a/.changeset/postgres-slim-listing-stall-signal-parity.md b/.changeset/postgres-slim-listing-stall-signal-parity.md new file mode 100644 index 0000000000..59123874cb --- /dev/null +++ b/.changeset/postgres-slim-listing-stall-signal-parity.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Restore stalled-review badges, timed-execution totals, and fresh-agent-log stall suppression on board listings. +category: fix +dev: Backend listTasks no longer excludes the log column in slim mode (stalledReview and timedExecutionMs derive from it before the log is stripped), and hasFreshAgentLogActivitySinceTaskUpdate is ported into all task-store read hydration paths so streaming merge/review agents suppress Stalled/Merge-stalled badges (mirrors main's FNXC:WorkflowLifecycle 2026-07-01 behavior lost in the PG cutover store split). diff --git a/.changeset/simplified-workflow-view.md b/.changeset/simplified-workflow-view.md new file mode 100644 index 0000000000..a8500ea820 --- /dev/null +++ b/.changeset/simplified-workflow-view.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add a simplified workflow editor view with a modern vertical canvas, plus Simple/Advanced/List mode toggle. +category: feature +dev: New WorkflowSimpleCanvas + WorkflowAddStepModal components; view mode persists in localStorage (`fusion:wf-editor-view-mode`, mobile `fusion:wf-mobile-graph-style`); insert-on-edge helpers live in workflow-simple-layout.ts. The old "Show simple editor" compact layout is now the List mode; the advanced canvas is unchanged. diff --git a/.changeset/sqlite-connection-auto-reopen.md b/.changeset/sqlite-connection-auto-reopen.md new file mode 100644 index 0000000000..81b6c30022 --- /dev/null +++ b/.changeset/sqlite-connection-auto-reopen.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Auto-heal wedged SQLite connections in place instead of failing every request until restart. +category: fix +dev: The sqlite adapter now classifies connection-corruption errors (SQLITE_NOTADB "file is not a database" / "database disk image is malformed"), reopens the connection on the same path, replays assignment-style PRAGMAs, verifies with PRAGMA quick_check, and retries the failed operation once when outside an explicit transaction. Statements are generation-tracked so ones prepared before the reopen re-prepare transparently; mid-transaction unwind (ROLLBACK/RELEASE) after a reopen is absorbed as no-ops. Covers fusion.db, fusion-central.db, and archive.db. On-disk corruption (quick_check failure) still defers to the open-time recovery machinery. diff --git a/.changeset/task-api-500-diagnostics.md b/.changeset/task-api-500-diagnostics.md new file mode 100644 index 0000000000..445e462755 --- /dev/null +++ b/.changeset/task-api-500-diagnostics.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Task API operations no longer fail with 500 when a task's PROMPT.md can't be read; server also logs 500 causes. +category: fix +dev: getTask (the shared load for GET/DELETE/PATCH/retry/reset/archive) and the mutation helpers updateTaskUnlocked, updateStep, readPromptForArchive, and resetPromptCheckboxes (packages/core/src/store.ts) read PROMPT.md unguarded, so an unreadable file (root-owned from a prior `sudo` run → EACCES, PROMPT.md being a directory → EISDIR, transient FS error) 500'd every per-task op while the PROMPT.md-free board list/create kept working. These reads are now best-effort (degrade + log). Diagnosability: rethrowAsApiError preserves the original error as Error `cause` and the /api boundary logs stack + cause for 5xx (packages/dashboard/src/api-error.ts, server.ts); client body stays generic in production. diff --git a/.changeset/todo-store-postgres-port.md b/.changeset/todo-store-postgres-port.md new file mode 100644 index 0000000000..6689e7a153 --- /dev/null +++ b/.changeset/todo-store-postgres-port.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Todo lists now work on the embedded-PostgreSQL backend instead of erroring. +category: feature +dev: Ports TodoStore to the AsyncDataLayer. Adds an `AsyncTodoStore` class (in async-todo-store.ts) wrapping the already-tested async CRUD helpers over project.todo_lists/project.todo_items; `getTodoStoreImpl` returns it in backend mode instead of throwing "TodoStore is not available in PG backend mode" (which 500'd every /api/todos route). The dashboard todo routes now await the store methods so the same code path serves both the sync SQLite store and the async PG store. Adds todo-store.pg.test.ts to the blocking test:pg-gate lane. Known gap: the async store does not yet emit list/item events for SSE live-refresh (updates land on next read). diff --git a/.changeset/update-now-eacces-guidance.md b/.changeset/update-now-eacces-guidance.md new file mode 100644 index 0000000000..0d0e142c2c --- /dev/null +++ b/.changeset/update-now-eacces-guidance.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: "Update now" now explains permission (EACCES) failures and how to fix them instead of showing raw npm errors. +category: fix +dev: `performUpdateInstall` (packages/dashboard/src/update-check.ts) detects EACCES/EPERM install failures (by error code or stderr text) and returns actionable remediation — run `sudo fn update`, reinstall without sudo, or `brew upgrade fusion` for Homebrew installs — rather than the raw `npm error EACCES … rename '/usr/lib/node_modules/@runfusion/fusion'`. Occurs when Fusion was installed via `sudo npm i -g` (root-owned global dir); `--force` is not retried for this class since it cannot grant write permission. diff --git a/.changeset/usage-dropdown-fable-grok-cli.md b/.changeset/usage-dropdown-fable-grok-cli.md new file mode 100644 index 0000000000..b4d6bb9688 --- /dev/null +++ b/.changeset/usage-dropdown-fable-grok-cli.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Usage dropdown now shows the Claude Fable weekly window and Grok CLI subscription credit usage. +category: feature +dev: Claude per-model weekly usage is parsed generically from the OAuth payload's `limits[]` scoped entries (the `seven_day_fable` key guess was disproven by a live probe); Grok prefers `~/.grok/auth.json` OIDC credentials against `cli-chat-proxy.grok.com/v1/billing?format=credits`, falling back to the xAI API-key validity card. diff --git a/.github/workflows/full-suite.yml b/.github/workflows/full-suite.yml index 7a3b00f91d..cd5158ef57 100644 --- a/.github/workflows/full-suite.yml +++ b/.github/workflows/full-suite.yml @@ -33,6 +33,27 @@ jobs: test-shards: name: Test shard ${{ matrix.shard }}/4 runs-on: ubuntu-latest + # FNXC:FixPgTestsAndCi 2026-06-26-09:10: + # Provision a PostgreSQL service container so the postgres/*.pg.test.ts + # suites run in the non-blocking full suite too (parity with the gate). + # The pg-test-harness probe skips gracefully if unreachable. + services: + postgres: + image: postgres:15 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -h localhost -p 5432 -U postgres" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + env: + FUSION_PG_TEST_URL_BASE: "postgresql://postgres:postgres@localhost:5432" + PGPASSWORD: "postgres" # Backstop for a wedged shard. The per-invocation watchdog (L2, # scripts/lib/run-vitest-watchdog.mjs) kills any single hung invocation at # its budget ceiling (<=30min), so this job budget only fires if L2 itself diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 7da29fd619..e9e2412df3 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -83,6 +83,33 @@ jobs: gate: name: Gate runs-on: ubuntu-latest + # FNXC:FixPgTestsAndCi 2026-06-26-09:10: + # Provision a PostgreSQL service container so the postgres/*.pg.test.ts + # suites (pgDescribe) run in the merge gate. The pg-test-harness probe + # detects reachability via a TCP probe on localhost:5432 and skips when + # unavailable, so this service is what makes the 57 PG twin tests actually + # execute instead of being silently skipped. + services: + postgres: + image: postgres:15 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: + - 5432:5432 + # Mark the service healthy only when pg_isready succeeds on the mapped + # port, so job steps don't start before Postgres accepts connections. + options: >- + --health-cmd "pg_isready -h localhost -p 5432 -U postgres" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + env: + # Point the PG test harness at the service container. psql admin DDL + # (CREATE/DROP DATABASE) runs against this URL's maintenance database. + FUSION_PG_TEST_URL_BASE: "postgresql://postgres:postgres@localhost:5432" + PGPASSWORD: "postgres" # The gate's value is speed; without a job timeout a hung build or # deadlocked vitest worker blocks every PR for GitHub's default 6 hours. # Expected runtime is ~3-5 min. diff --git a/AGENTS.md b/AGENTS.md index 147c00ee37..5815d029b2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -224,6 +224,7 @@ Scoped exception (FN-5819): shared-branch-group members (`branchContext.assignme - FN-7158: agent performance reflections emit `reflection:generated`, `reflection:skipped`, and `reflection:failed` with ids/counts/outcomes-only metadata; never persist reflection prose or prompt text in run-audit. - FN-7528: a deterministic, non-LLM post-task performance capture (`AgentReflectionService.captureTaskPerformance`) runs once per completed task and emits `reflection:captured` with ids/counts/outcomes-only metadata (`retryReworkCount?`, `filesTouchedCount?`, `packagesTouchedCount?`, `verificationFileScoped?`, `durationMs?`); never persists `verificationScopeReason` free-text or summary prose in run-audit. - FN-7787: `createResolvedAgentSession` enriches `session:runtime-resolved` with `noModelResolved: true` and `runtimeBuiltInFallbackModel` when a non-mock/non-test session reaches runtime creation without a complete provider/model pair; this is a visibility signal for runtime built-in fallback usage, not a fabricated model-resolution verdict. +- FN-7802: self-healing emits `task:reconcile-missing-worktree-merge-active` when it proves an `in-review` merge-active task (`merging`/`merging-pr`/`merging-fix`) is stranded by an unusable-worktree session-start failure, clears stale `worktree`/`branch`/`sessionFile`, resets the worktree-session retry budget, increments `recoveryRetryCount` as the bounded stale-metadata clear counter, and requeues to `todo`; it emits `task:reconcile-missing-worktree-merge-active-no-action` when `autoMerge:false`, workspace-task ownership, or triple-proof blocks the backward move. - FN-7011: self-healing emits `task:reconcile-engine-downtime-active-timing` when startup recovery shifts active task segment anchors to exclude proven engine-process downtime, and `task:reconcile-engine-downtime-active-timing-no-action` when no active task qualifies. - FN-5419: git run-audit now includes `pull:fast-forward` and `stash:pop-conflict`; dashboard git surfaces now include the extended `POST /api/git/pull` integration-worktree path plus companion `POST /api/git/stash-resolve`, `POST /api/git/stash-drop`, and `POST /api/git/stash-apply` routes. - FN-6292: self-healing emits `task:reconcile-dependency-blocking-lease` when it rebounds an in-progress holder whose stale file-scope lease blocks an unmet dependency, and `task:reconcile-dependency-blocking-lease-no-action` when triple-proof blocks that backward move. @@ -289,7 +290,7 @@ Keep this AGENTS inventory in sync with App lazy imports, AppModals lazy modal i - `PiExtensionsManager` - `AgentDetailView` -Note: the embedded main-content views Workflows (`_WorkflowEditorView`), Import Tasks (`_ImportTasksView`), Automations (`_AutomationsView`), and Settings (`_SettingsView`) in App.tsx are `_`-prefixed lazy splits that reuse already-documented chunks. Task session terminals, onboarding-internal modals, duplicate `AgentDetailView` imports, and right-dock overflow re-imports of already-counted views are also intentionally excluded. These exclusions stay out of the curated list and count; `lazy-loaded-views-docs.test.ts` asserts them explicitly, so do not add them as bullets. +Note: the embedded main-content views Workflows (`_WorkflowEditorView`), Import Tasks (`_ImportTasksView`), Automations (`_AutomationsView`), and Settings (`_SettingsView`) in App.tsx are `_`-prefixed lazy splits that reuse already-documented chunks. Task session terminals, the Task Detail embedded terminal, onboarding-internal modals, duplicate `AgentDetailView` imports, and right-dock overflow re-imports of already-counted views are also intentionally excluded. These exclusions stay out of the curated list and count; `lazy-loaded-views-docs.test.ts` asserts them explicitly, so do not add them as bullets. ## FNXC_LOG comments: - Please whenever you're working on a codebase. I want you to add comments describing the date of the change (must be in this format yyyy-MM-dd-hh:mm) and describing the requirements or the change in requirements that made you implement certain functionality. diff --git a/ROADMAP.md b/ROADMAP.md index c7d6227e12..460e0c56e3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,7 +1,9 @@ # Planned work - Postgres migration -- Coding (Ideas) workflow - Import from Beads -- Improved core engine reliability - Orca integration - Paperclip Fusion plugin + +# Done +- Coding (Ideas) workflow +- Improved core engine reliability diff --git a/docs/agents.md b/docs/agents.md index 2aac9b9566..d21a2ce852 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -44,7 +44,7 @@ fn chat [message…] [--once] [--non-interactive] [--poll-ms ] Artifact tools operate on the shared artifact registry, so artifacts are visible across agents and tasks when the caller has the artifact ID or can discover it through filters. -- `fn_artifact_register` registers a `document`, `image`, `video`, `audio`, or `other` artifact with `title`, optional `description`, optional `mimeType`, optional inline text `content`, optional `uri`/path reference, and optional `taskId`. Tool callers should provide either inline `content` or a `uri`/path reference for media stored elsewhere. Executor/heartbeat sessions infer the registering agent as `authorId`; dashboard chat uses the `dashboard-chat` author and requires `task_id` because chat has no ambient task. +- `fn_artifact_register` registers a `document`, `image`, `video`, `audio`, or `other` artifact with `title`, optional `description`, optional `mimeType`, and exactly one payload source: inline text `content`, a local file `path` (preferred for media the agent saved to disk — screenshots, wireframes, mockups, screen recordings, PDF exports; the file is copied into managed artifact storage with MIME inference and image/video/PDF signature validation), base64 `dataBase64` image bytes, or a `uri` reference for media stored elsewhere. HTML mockups register as `type="document"` + `mimeType="text/html"` (inline `content` or `path`) and render as live sandboxed previews in the Artifacts view; PDFs (`mimeType="application/pdf"`, `path`) open in an embedded viewer; videos stream with range-request seeking. Executor sessions resolve relative `path` values against the task worktree and default `taskId` to the executing task; task-scoped heartbeat sessions resolve relative `path` values against the acquired heartbeat worktree and default `taskId` to the assigned task; dashboard chat uses the `dashboard-chat` author and requires `task_id` because chat has no ambient task. `path` values are containment-checked before any file read: the realpath-canonicalized file must stay inside the session's workspace directory (`baseDir`) or the OS temp directory (where browser/screenshot tooling writes captures); relative paths are rejected outright in lanes without a workspace directory (dashboard chat, no-task heartbeats), which are bounded to tmpdir-only absolute paths. - `fn_artifact_list` lists artifacts across agents and tasks with optional `type`, `authorId`, `taskId`, `search`, `limit`, and `offset` filters. Dashboard chat's scoped variant requires `task_id` and otherwise supports `type`, `authorId`, `search`, `limit`, and `offset` for that task. - `fn_artifact_view` fetches one artifact by `id`, returning registry metadata plus inline `content` when present or the stored `uri`/path reference for media artifacts. - Successful registration emits a best-effort `system` → `user` inbox notification to `DASHBOARD_USER_ID` with `artifactId`, `artifactType`, `title`, `authorId`, and optional `taskId` metadata. Notification delivery failures are logged and must never fail or roll back the artifact registration. diff --git a/docs/architecture.md b/docs/architecture.md index 6b3ad56a37..ffb9f6d9cd 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2157,7 +2157,7 @@ This section preserves the detailed lifecycle/self-healing contracts that were f - **Worktree metadata reconcile ordering (FN-4962)**: `reconcile-task-worktree-metadata` must run before `reclaim-stale-active-branches`; stale `task.worktree` metadata is rebound to live `fusion/` worktrees when present (`task:auto-recover-worktree-metadata-rebound`) or cleared (`task:auto-recover-worktree-metadata-cleared`) when absent. - **Completion fan-out is synchronous**: `SelfHealingManager.reconcileCompletedTask()` runs on `in-review → done`. Downstream stale `blockedBy` links and residual `fusion/` branch/worktree artifacts are reconciled immediately, not on a periodic sweep. - **In-review stall deadlock**: identical stalls (same code + reason) repeated past `inReviewStallDeadlockThreshold` (default 3) auto-pause with `pausedReason: "in-review-stall-deadlock"` and `status: "failed"`. User-initiated retry paths (dashboard retry, `fn_task_retry`, and CLI `task retry`) clear that automatic deadlock pause so the retry can execute, but they never override explicit/manual pauses or unrelated automatic pause reasons. -- **Restart recovery**: `RestartRecoveryCoordinator` classifies interrupted `in-progress` runs. Unusable-worktree session-start failures (`missing`, `incomplete`, `unregistered git worktree`) are recoverable; retries are capped at `MAX_WORKTREE_SESSION_RETRIES=3` before escalating. +- **Restart recovery**: `RestartRecoveryCoordinator` classifies interrupted `in-progress` runs. Unusable-worktree session-start failures (`missing`, `incomplete`, `unregistered git worktree`) are recoverable; retries are capped at `MAX_WORKTREE_SESSION_RETRIES=3` before escalating. `recoverMissingWorktreeReviewFailures` also owns durable unusable-worktree session-start failures that reached `in-review` with a merge-active status (`merging`, `merging-pr`, or `merging-fix`): before interrupted/deadlocked merge sweeps can re-drive the same phantom path, it applies auto-merge eligibility, workspace-task exclusion, and triple-proof, clears stale `worktree`/`branch`/`sessionFile`, resets the exhausted worktree-session retry budget, increments `recoveryRetryCount` as the bounded merge-active stale-metadata clear counter, and requeues to `todo` preserving progress. Operator retry surfaces (`fn_task_retry`, CLI `task retry`, dashboard retry) have the same signature-only reset primitive so a missing-worktree failure does not require a valid `merging` transition. - **Executor pre-session liveness gate (FN-4935/FN-6861)**: the gate now skips for fresh acquisitions (`acquisition.source === "fresh"`), emits structured `not_usable_task_worktree:` diagnostics (including canonicalized registered-path snapshots) and a `worktree:incomplete-detected` audit event with `source: "executor-liveness-gate"`, while preserving the existing `taskDoneRetryCount` / `MAX_TASK_DONE_REQUEUE_RETRIES` requeue contract. The project repo root is never a usable task worktree even though it is a legitimately registered Git worktree; `classifyTaskWorktree` returns `repo-root` for canonical root-equal paths, and resume acquisition treats that as self-healable stale metadata by clearing `task.worktree` and creating a fresh checkout under the configured worktrees directory. FN-5772 adds a bounded nested-root self-heal: when `task.worktree` points at a strict descendant of a registered worktree root inside the configured worktrees dir, executor re-anchors `task.worktree` to the git top-level, emits `worktree:reanchored` (`fromPath`, `toPath`, `source`), and proceeds; repo-root/outside-dir/unregistered top-level mismatches still fail. FN-4651 `worktreeSessionRetryCount` remains scoped to the in-review/session-start recovery path. - **Stale self-owned active-session reconcile on conflict cleanup (FN-4973)**: when executor worktree-conflict cleanup finds only a same-task stale `activeSessionRegistry` entry and no live in-memory `activeWorktrees` binding for that task/path, it must unregister the stale entry before `removeWorktree` (plus one-shot backstop reconcile on same-task `ActiveSessionWorktreeRemovalError` races). Foreign-task entries remain protected by FN-4811 and must never be reconciled by the requesting task. - **Same-task stale removal canonical helper (FN-5346)**: executor same-task cleanup paths now route pre-removal reconciliation through `reconcileSelfOwnedActiveSessionForRemoval` (via executor helper wiring), so stale self-owned `activeSessionRegistry` residues are cleared only when no live in-memory binding exists, while FN-4811 foreign-owner refusals and live-owner protections remain intact. @@ -2177,7 +2177,7 @@ This section preserves the detailed lifecycle/self-healing contracts that were f - **Empty-commit refusal + early empty-own-diff finalize (FN-5345/FN-5377)**: Fusion task worktrees install a `prepare-commit-msg` hook that refuses `git commit --allow-empty` and other zero-staged-diff commits, preventing verification-only tasks from manufacturing empty handoff commits that defeat the merger's no-op classifier. The hook allows legitimate empty-tree paths (amend, merge, squash, cherry-pick, revert, rebase). Amend detection tokenizes the parent process command line (`ps -o args=` with `/proc/$PPID/cmdline` fallback for Alpine/busybox) and stops at the first message-supplying flag (`-m`/`-F`/`--message`/`--file`) so a commit message containing the substring `--amend` cannot bypass the guard. In `aiMergeTask`, an early empty-own-diff fast-path runs BEFORE any reuse-handoff acquisition: when integration mode is `reuse-task-worktree`, the branch exists, `git rev-list --count ..` is > 0, and `git diff --quiet ..` exits 0, the task auto-finalizes as no-op with `mergeDetails.noOpMerge: true` and emits `task:auto-recover-finalize-already-on-main` with `reason: "empty-own-diff-early-fast-path"`. The fast-path best-effort removes the stranded worktree (FN-4811 same-task/foreign-owner guard) and deletes the `fusion/` branch so empty-own-diff residuals do not accumulate. This unsticks tasks where a stale empty handoff commit combined with drifted worktree↔branch mapping would otherwise wedge the handoff gate with `registered-branch-mismatch`. The explicit `cwd-integration-branch` mode is unchanged (`cwd-main` remains a deprecated alias normalized to it). `classifyOwnedLandedEvidence` also detects empty-own-diff (aheadCount > 0, zero net diff) and returns `proven-no-op` so downstream self-healing and post-handoff finalize paths benefit too. Additionally, merger's reuse-fallback path now consults `git worktree list --porcelain` before creating a new worktree: extant usable registrations of `fusion/` are reused directly (rather than blindly `git worktree add -f` producing a duplicate registration), and stale registrations are pruned first. The direct-reuse shortcut is guarded by FN-4811 (refuses paths owned by a different task in `activeSessionRegistry`) and FN-4954 (skipped when `recycleWorktrees=true` with a pool attached, so `WorktreePool.acquire` lease bookkeeping stays consistent). Two audit subtypes — `merge:reuse-fallback-pruned-stale-registration` and `merge:reuse-fallback-reused-existing-registration` — replace the prior overloading of `merge:reuse-fallback-new-worktree` for these cases. - **Verified no-op/duplicate executor completion (FN-6275/FN-7488)**: explicit `fn_task_done` may complete with zero branch commits only when the summary starts with a recognized sentinel (`PREMISE STALE:`, `NO-OP:`, `NOOP:`, `DUPLICATE: FN-NNNN ...`, or `REDUNDANT:`), the task already carries a no-commit contract, or the PROMPT declares a source-free gitignored task-artifact delivery. The source-free path is intentionally narrow: File Scope must be populated and limited to board/task artifacts such as `.fusion/tasks/...`, task documents/logs, or attachments; the prompt must forbid force-adding ignored `.fusion/` artifacts and fabricating empty commits or equivalently state that source-free/gitignored task artifacts are the only deliverables; and any tracked source/docs/config/test/changeset scope keeps the `no_commits` refusal active (even if `.fusion/` artifacts are also listed). These exemptions only relax the `no_commits` invariant; `wrong_toplevel`, `wrong_branch`, pending-step/review refusals, and scope-leak guards still run. Accepted sentinel completions persist `noCommitsExpected: true`, write task-log audit details with marker kind/reason/raw summary/run/agent IDs, and add a task timeline activity so the no-code terminal path remains explainable. Prompt-derived source-free completions log `prompt-derived source-free task-artifact contract` for operator audit. Ordinary zero-commit implementation completions without one of these contracts are still refused. - **In-review branch-binding self-heal (FN-5083/FN-6695)**: `reconcile-in-review-branch-rebind` runs after `reconcile-task-worktree-metadata` and before `reclaim-stale-active-branches`. It restores `task.branch` (and clears `task.worktree` for fresh acquisition) for `in-review` tasks when exactly one case-insensitive `fusion/` candidate branch has unique commits versus the integration base. Ambiguous candidates emit `task:auto-rebind-skipped` (`reason: "ambiguous-candidates"`) and are never auto-resolved. Unsafe metadata repair is also skipped with `task:auto-rebind-skipped`: `userPaused` preserves authoritative user intent, and `checkedOutBy` preserves live agent checkout ownership. Branch construction across executor/worktree-pool/worktree-acquisition/merger/self-healing canonicalizes to lowercase via `canonicalFusionBranchName`; `fn_task_done` wrong-branch checks now auto-canonicalize case-only mismatches and emit `branch:auto-canonicalize-case`. -- **In-review is terminal-until-merged under `autoMerge: false` (FN-5147)**: when a project sets `settings.autoMerge: false`, `in-review` is the intended resting state until a human merges the PR. No lifecycle-mutating self-healing sweep (`reclaimSelfOwnedBranchConflicts`, `recoverGhostReviewTasks`, `recoverStaleIncompleteReviewTasks`, `recoverInterruptedMergingTasks`, `recoverStuckMergeDeadlocks`, `recoverMissingWorktreeReviewFailures`, `recoverPartialProgressNoTaskDoneFailures`, `recoverCompletionHandoffLimbo`, `recoverPostDoneNonContinuableWedge`, `recoverMergeableReviewTasks`, `recoverMergedReviewTasks`, `recoverAlreadyMergedReviewTasks`, `recoverOrphanOnlyScopeViolations`, `recoverForeignOnlyContaminatedInReviewTasks`, `recoverReviewTasksWithFailedPreMergeSteps`, `finalizeNoOpReviewTasks`, `surfaceInReviewStalls`, `surfaceInReviewStalled`) may move the task out of `in-review`, mark it `paused`/`failed`, or re-enqueue it for execution. Explicit per-task overrides are distinguished by `task.autoMergeProvenance: "user"`; ambiguous legacy rows stamped `autoMerge: true` by the pre-FN-6245 review-entry path are marked `"legacy-stamp"` once and surfaced in run-audit/logs, but are only cleared by the operator-driven `reconcileLegacyAutoMergeStamps({ apply: true })` action. Scoped FN-5819 exception: shared-group members (`branchContext.assignmentMode === "shared"`) are still allowed through the member→`branch_groups.branchName` integration step while `autoMerge` is off; this is a soft pre-integration only and does not permit shared-branch → default-branch promotion. FN-7182 applies the same human-gated treatment to an open `PrInfo.manual` PR created or linked from the dashboard **Create PR** action: automatic merge queues and self-healing stand down until the PR is closed/merged or handled manually, while pipeline-created PRs without `manual` remain auto-merge eligible. RECONCILE-ONLY sweeps (branch rebind, blocker fan-out, stale-status clears, contamination metadata cleanup, attribution restore, PR refresh, misclassified-failure error clearing) continue to run. +- **In-review is terminal-until-merged under `autoMerge: false` (FN-5147)**: when a project sets `settings.autoMerge: false`, `in-review` is the intended resting state until a human merges the PR. No lifecycle-mutating self-healing sweep (`reclaimSelfOwnedBranchConflicts`, `recoverGhostReviewTasks`, `recoverStaleIncompleteReviewTasks`, `recoverInterruptedMergingTasks`, `recoverStuckMergeDeadlocks`, `recoverMissingWorktreeReviewFailures`, `recoverPartialProgressNoTaskDoneFailures`, `recoverCompletionHandoffLimbo`, `recoverPostDoneNonContinuableWedge`, `recoverMergeableReviewTasks`, `recoverMergedReviewTasks`, `recoverAlreadyMergedReviewTasks`, `recoverOrphanOnlyScopeViolations`, `recoverForeignOnlyContaminatedInReviewTasks`, `recoverReviewTasksWithFailedPreMergeSteps`, `finalizeNoOpReviewTasks`, `surfaceInReviewStalls`, `surfaceInReviewStalled`) may move the task out of `in-review`, mark it `paused`/`failed`, or re-enqueue it for execution. That includes merge-active unusable-worktree recovery: automation emits `task:reconcile-missing-worktree-merge-active-no-action` with `reason:"auto-merge-off"` instead of moving the row, while explicit operator retry remains supported because it is a manual reset request. Explicit per-task overrides are distinguished by `task.autoMergeProvenance: "user"`; ambiguous legacy rows stamped `autoMerge: true` by the pre-FN-6245 review-entry path are marked `"legacy-stamp"` once and surfaced in run-audit/logs, but are only cleared by the operator-driven `reconcileLegacyAutoMergeStamps({ apply: true })` action. Scoped FN-5819 exception: shared-group members (`branchContext.assignmentMode === "shared"`) are still allowed through the member→`branch_groups.branchName` integration step while `autoMerge` is off; this is a soft pre-integration only and does not permit shared-branch → default-branch promotion. FN-7182 applies the same human-gated treatment to an open `PrInfo.manual` PR created or linked from the dashboard **Create PR** action: automatic merge queues and self-healing stand down until the PR is closed/merged or handled manually, while pipeline-created PRs without `manual` remain auto-merge eligible. RECONCILE-ONLY sweeps (branch rebind, blocker fan-out, stale-status clears, contamination metadata cleanup, attribution restore, PR refresh, misclassified-failure error clearing) continue to run. - **Auto-merge integration-root default (FN-5279)**: direct auto-merge now defaults `mergeIntegrationWorktree` to `reuse-task-worktree`; merger must pass the reuse handoff gates or emit `merge:reuse-handoff-refused` and leave the task in `in-review` without silently falling back to `cwd-integration-branch` (`cwd-main` remains a deprecated alias normalized to that mode). - **Orphaned execution sweep is observation-only (FN-5337)**: `recoverOrphanedExecutions` only annotates stale in-progress candidates with `task:orphan-detected-no-action` and `[orphan-detected] ... no action (operator-decides)` logs. It must never move `in-progress`/`in-review` backward to `todo` or mutate lease/worktree metadata. Proof-based backward recovery remains exclusively in `recoverInProgressLimbo` (FN-5219), `RestartRecoveryCoordinator`, `recoverMissingWorktreeReviewFailures`, and explicit executor/merger failure paths. Reintroducing lifecycle mutation here requires hard git/session proof gating plus CEO+CTO+PM sign-off. - **Self-owned reclaim resume-limbo escalation (FN-5704)**: `reclaimSelfOwnedBranchConflicts` tracks `resumeLimboCount`, `resumeLimboTipSha`, and `resumeLimboStepSignature` for in-progress reclaim/unpause loops. If reclaim finds no progress (same tip, same step-status signature, and no active-session signal) for `MAX_NO_PROGRESS_RESUME_ATTEMPTS` consecutive sweeps, self-healing escalates by moving the task to `todo` with `preserveWorktree: true`, `preserveProgress: true`, and `preserveResumeState: true` instead of endlessly re-arming resume. Escalation emits `task:resume-limbo-escalated` run-audit metadata (`frozenTipSha`, `idleMs`, `resumeAttemptCount`, `currentStep`) and resets the limbo counter. diff --git a/docs/assets/memory-ui-review-2026-07/after-agent-memory-mid.jpg b/docs/assets/memory-ui-review-2026-07/after-agent-memory-mid.jpg new file mode 100644 index 0000000000..72ca71d945 Binary files /dev/null and b/docs/assets/memory-ui-review-2026-07/after-agent-memory-mid.jpg differ diff --git a/docs/assets/memory-ui-review-2026-07/after-agent-memory2.jpg b/docs/assets/memory-ui-review-2026-07/after-agent-memory2.jpg new file mode 100644 index 0000000000..7d9533cb03 Binary files /dev/null and b/docs/assets/memory-ui-review-2026-07/after-agent-memory2.jpg differ diff --git a/docs/assets/memory-ui-review-2026-07/after-insights.jpg b/docs/assets/memory-ui-review-2026-07/after-insights.jpg new file mode 100644 index 0000000000..92dbf27596 Binary files /dev/null and b/docs/assets/memory-ui-review-2026-07/after-insights.jpg differ diff --git a/docs/assets/memory-ui-review-2026-07/after-memory-engines.jpg b/docs/assets/memory-ui-review-2026-07/after-memory-engines.jpg new file mode 100644 index 0000000000..44d72e4fe9 Binary files /dev/null and b/docs/assets/memory-ui-review-2026-07/after-memory-engines.jpg differ diff --git a/docs/assets/memory-ui-review-2026-07/after-memory-insights-multiline.jpg b/docs/assets/memory-ui-review-2026-07/after-memory-insights-multiline.jpg new file mode 100644 index 0000000000..9718c7a53c Binary files /dev/null and b/docs/assets/memory-ui-review-2026-07/after-memory-insights-multiline.jpg differ diff --git a/docs/assets/memory-ui-review-2026-07/after-memory-insights.jpg b/docs/assets/memory-ui-review-2026-07/after-memory-insights.jpg new file mode 100644 index 0000000000..027c91bd92 Binary files /dev/null and b/docs/assets/memory-ui-review-2026-07/after-memory-insights.jpg differ diff --git a/docs/assets/memory-ui-review-2026-07/after-todos.jpg b/docs/assets/memory-ui-review-2026-07/after-todos.jpg new file mode 100644 index 0000000000..febe7aeaf5 Binary files /dev/null and b/docs/assets/memory-ui-review-2026-07/after-todos.jpg differ diff --git a/docs/assets/memory-ui-review-2026-07/before-agent-memory.jpg b/docs/assets/memory-ui-review-2026-07/before-agent-memory.jpg new file mode 100644 index 0000000000..e7e2b2363f Binary files /dev/null and b/docs/assets/memory-ui-review-2026-07/before-agent-memory.jpg differ diff --git a/docs/assets/memory-ui-review-2026-07/before-insights.jpg b/docs/assets/memory-ui-review-2026-07/before-insights.jpg new file mode 100644 index 0000000000..06c0a8c9ce Binary files /dev/null and b/docs/assets/memory-ui-review-2026-07/before-insights.jpg differ diff --git a/docs/assets/memory-ui-review-2026-07/before-memory-engines.jpg b/docs/assets/memory-ui-review-2026-07/before-memory-engines.jpg new file mode 100644 index 0000000000..4d59465296 Binary files /dev/null and b/docs/assets/memory-ui-review-2026-07/before-memory-engines.jpg differ diff --git a/docs/assets/memory-ui-review-2026-07/before-memory-insights.jpg b/docs/assets/memory-ui-review-2026-07/before-memory-insights.jpg new file mode 100644 index 0000000000..9a78753ed7 Binary files /dev/null and b/docs/assets/memory-ui-review-2026-07/before-memory-insights.jpg differ diff --git a/docs/assets/memory-ui-review-2026-07/before-todos.jpg b/docs/assets/memory-ui-review-2026-07/before-todos.jpg new file mode 100644 index 0000000000..6766ce347d Binary files /dev/null and b/docs/assets/memory-ui-review-2026-07/before-todos.jpg differ diff --git a/docs/assets/simplified-workflow-view/add-step-dialog.png b/docs/assets/simplified-workflow-view/add-step-dialog.png new file mode 100644 index 0000000000..67ddf71e51 Binary files /dev/null and b/docs/assets/simplified-workflow-view/add-step-dialog.png differ diff --git a/docs/assets/simplified-workflow-view/advanced-mode.png b/docs/assets/simplified-workflow-view/advanced-mode.png new file mode 100644 index 0000000000..57071683cc Binary files /dev/null and b/docs/assets/simplified-workflow-view/advanced-mode.png differ diff --git a/docs/assets/simplified-workflow-view/insert-on-edge-inspector.png b/docs/assets/simplified-workflow-view/insert-on-edge-inspector.png new file mode 100644 index 0000000000..b0d88634d9 Binary files /dev/null and b/docs/assets/simplified-workflow-view/insert-on-edge-inspector.png differ diff --git a/docs/assets/simplified-workflow-view/lifecycle-warnings-fix-all.png b/docs/assets/simplified-workflow-view/lifecycle-warnings-fix-all.png new file mode 100644 index 0000000000..0af09b2401 Binary files /dev/null and b/docs/assets/simplified-workflow-view/lifecycle-warnings-fix-all.png differ diff --git a/docs/assets/simplified-workflow-view/lifecycle-warnings-fixed.png b/docs/assets/simplified-workflow-view/lifecycle-warnings-fixed.png new file mode 100644 index 0000000000..51b54f69cc Binary files /dev/null and b/docs/assets/simplified-workflow-view/lifecycle-warnings-fixed.png differ diff --git a/docs/assets/simplified-workflow-view/list-mode.png b/docs/assets/simplified-workflow-view/list-mode.png new file mode 100644 index 0000000000..b89ea5ad2c Binary files /dev/null and b/docs/assets/simplified-workflow-view/list-mode.png differ diff --git a/docs/assets/simplified-workflow-view/mobile-add-sheet.png b/docs/assets/simplified-workflow-view/mobile-add-sheet.png new file mode 100644 index 0000000000..097ac61ec0 Binary files /dev/null and b/docs/assets/simplified-workflow-view/mobile-add-sheet.png differ diff --git a/docs/assets/simplified-workflow-view/mobile-canvas.png b/docs/assets/simplified-workflow-view/mobile-canvas.png new file mode 100644 index 0000000000..a2dffc56b6 Binary files /dev/null and b/docs/assets/simplified-workflow-view/mobile-canvas.png differ diff --git a/docs/assets/simplified-workflow-view/mobile-list-fallback.png b/docs/assets/simplified-workflow-view/mobile-list-fallback.png new file mode 100644 index 0000000000..d37f64429c Binary files /dev/null and b/docs/assets/simplified-workflow-view/mobile-list-fallback.png differ diff --git a/docs/assets/simplified-workflow-view/mobile-node-config.png b/docs/assets/simplified-workflow-view/mobile-node-config.png new file mode 100644 index 0000000000..38ec25bb97 Binary files /dev/null and b/docs/assets/simplified-workflow-view/mobile-node-config.png differ diff --git a/docs/assets/simplified-workflow-view/saved-graph.png b/docs/assets/simplified-workflow-view/saved-graph.png new file mode 100644 index 0000000000..11864d5ace Binary files /dev/null and b/docs/assets/simplified-workflow-view/saved-graph.png differ diff --git a/docs/assets/simplified-workflow-view/simple-view-builtin.png b/docs/assets/simplified-workflow-view/simple-view-builtin.png new file mode 100644 index 0000000000..5f61ec3695 Binary files /dev/null and b/docs/assets/simplified-workflow-view/simple-view-builtin.png differ diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 9f7671eadd..a327b3994e 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -16,6 +16,9 @@ Use **Search settings** at the top of Settings to find the section that contains On mobile, the search row starts collapsed behind a compact toggle icon beside the **Settings Section** dropdown to save vertical space; tap it to reveal the search input and tap again to hide it. An in-progress search query is preserved across collapse/expand. Desktop and tablet always show the search row with no toggle. + +On desktop and tablet, the Settings navigation rail has no hard divider between navigation and content; section rows stay on one line and use ellipsis for unusually long labels. Drag the thin handle between the navigation rail and content pane to widen or narrow the rail. Fusion remembers that width in browser storage and restores it for both the standalone Settings modal and embedded Settings page. Mobile keeps the stacked **Settings Section** picker and does not show the resize handle. + Every user-editable setting's help text (the `.settings-description`/`` hint under a field) states its own default value — for example “Default: 3.”, “Default: enabled.”, or “No default — unset (inherits the global setting).” for values that fall back to another scope. Canonical default values come from `DEFAULT_GLOBAL_SETTINGS` / `DEFAULT_PROJECT_SETTINGS` in `packages/core/src/settings-schema.ts`; the dashboard copy never invents a number. A guard test (`settings-default-descriptions.test.tsx`) enforces that every surfaced setting states its default and that every `DEFAULT_SETTINGS` key is either documented or explicitly allowlisted as not surfaced in the Settings UI. @@ -194,12 +197,16 @@ Features: - GitLab tracking badges on task cards for linked GitLab project issues, group issues, and merge requests; stale GitLab metadata uses a warning-colored badge while GitHub badges remain unchanged. - GitHub provenance marker on task cards imported from GitHub (`sourceType: github_import`), shown in the footer with other external-source metadata - Task card header meta badges group priority and fast mode in the header; agent-created provenance renders in a dedicated bottom-left row ahead of workflow identity so the ID/status/actions header does not wrap on narrow cards. Agent labels prefer `sourceMetadata.agentName` over raw agent IDs. +- **Settings → Appearance → Show cost badges on task cards** is default off. When enabled, board cards with recorded positive token usage show a compact derived-cost badge beside the execution-time badge; unpriced models display `—`, and cards with no usage render no badge shell. + - Task cards show a read-only **oversight-level badge** (`Observe`, `Steer`, or `Auto-recovery`) in the meta-badges cluster reflecting the effective planner-oversight level, but only when oversight is *meaningfully configured* — an explicit per-task override (including an explicit `autonomous` override), or a resolved workflow/effective tier of `observe`/`steer` (`data-testid="card-oversight-badge"`). A card that merely **inherits** the schema default `autonomous` tier (no per-task override, no non-default workflow tier) renders no badge and no empty `.card-meta-badges` shell. The badge is also absent when the effective level is explicitly "off", **and** while an inherited (no per-task-override) workflow tier is still being resolved (in flight or not yet fetched) — it never shows a guessed default during that window. + +- Task cards show a compact **planner-overseer eye badge** (`data-testid="planner-overseer-state-badge"`) when the engine reports a non-idle transient `plannerOverseerState`. The eye badge is an active-overseer state marker, not a human-read/view indicator: `watching` means passive monitoring, `steering`/`recovering` mean active guidance or recovery is underway, and `awaiting-confirmation` means a human decision is required before the overseer can continue. Hover exposes the composed tooltip with the overseer's reason, watched stage/signal, and pending-confirmation note when present. @@ -259,8 +266,10 @@ Features: - Bulk selection + batch model updates - Bulk Pause / Unpause / Archive actions from the selection toolbar (`Pause selected`, `Unpause selected`, `Archive selected`) for fast batch task state management. - Bulk delete from the selection toolbar (`Delete selected`): archived selections are skipped automatically, and dependency-conflict failures can be force-deleted per task after a danger confirmation that removes dependency references. -- List rows and mobile cards support the same task context menu as Board cards from right-click, keyboard context menu / Shift+F10, or touch long-press without changing ordinary row selection or tap-to-open behavior. Selecting an action applies that exact action once and dismisses the menu, including **Refine** for completed tasks. - @@ -462,7 +471,8 @@ Create requests never send an explicit `column`. The task store resolves the lan Optional workflow steps declared by the active workflow are available from the quick-add action row and the **New Task** dialog's inline quick buttons. For example, the coding workflow's browser verification option appears as a quick drop-down when that workflow is active; each option is seeded from the workflow step's `defaultOn` setting and is sent with the task's `enabledWorkflowSteps` payload at creation time. -Quick Add image attachments use the paperclip icon button in the action row. Supported image files (`png`, `jpeg`, `gif`, `webp`) can be selected from that control, pasted into the Quick Add input, or dragged onto the Quick Add box; all three paths show pending previews before task creation and upload the images to the created task afterward. + +Quick Add image attachments use the paperclip icon button in the action row. Supported image files (`png`, `jpeg`, `gif`, `webp`) can be selected from that control, pasted into the Quick Add input, or dragged onto the Quick Add box; all three paths show pending previews before task creation and upload the images to the created task afterward. The same bottom action row places the GitHub tracking override beside the paperclip; Priority is an icon-only control whose glyph changes by selected level (down arrow for low, flag for normal, up arrow for high, alert for urgent), and Fast is an icon-only lightning control. These icon-only controls keep accessible labels and the same create-payload behavior as the previous text chips. Quick entry, inline quick-create, and the full **New Task** dialog all check for similar active tasks before creating. When possible duplicates exist, the warning lists each match by task description (falling back to title, then “No description”) and lets you open an existing task, cancel, or create anyway with the duplicates acknowledged. @@ -618,6 +628,12 @@ Mailbox view shows inbox/outbox communication threads and unread state. Fusion embeds a terminal using xterm.js. Desktop and tablet use the footer status bar as the terminal launcher; mobile keeps the full-screen terminal path. + +On first launch or first open, the terminal keeps reconnecting automatically until its initial WebSocket opens; it should show **Reconnecting...** during that cold-start recovery rather than requiring a manual **Reconnect** click. If an already-connected terminal drops and exhausts its bounded reconnect budget, Fusion then parks it as **Disconnected** and surfaces the manual **Reconnect** control. + + +Task Detail has two terminal-adjacent tabs when both are applicable: **Session** shows the pre-existing CLI agent session transcript/control surface, while **Terminal** embeds the interactive multi-tab terminal inside the task detail body. The interactive **Terminal** tab is always available in Task Detail; its first shell starts in the task worktree when one is recorded, otherwise it starts in the project base directory (project root), including for multi-repo workspace tasks that have no single task worktree. Its terminal tabs are stored separately from the footer/global project terminal tabs. + On Windows, the embedded terminal starts a supported shell inside Fusion, such as Command Prompt (`cmd.exe`) or Windows PowerShell. Windows Terminal (`wt.exe`) is an external terminal host and is not required or launched for the embedded panel, so Fusion should not show native Windows Terminal help/version popups while starting a terminal. If embedded terminal startup fails, Fusion shows an inline error with **Retry** instead of a blocking native dialog; install or repair Windows Terminal separately with `winget install Microsoft.WindowsTerminal` only if you want to use Windows Terminal outside Fusion. Use the terminal on desktop/tablet: @@ -758,11 +774,11 @@ Branch names are dynamic from merge/audit payloads; the banner is not hardcoded The global OAuth re-login banner clears a provider row immediately after that provider successfully re-authenticates (from Settings → Authentication or Model Onboarding), instead of waiting for the next `GET /auth/status` poll interval. -For Claude/Anthropic OAuth credentials, the same `/auth/status` poll also attempts an automatic refresh when the stored OAuth credential has a refresh token and the access token is expired or within the refresh buffer. Anthropic banner state is keyed to `anthropic-subscription` (including legacy Anthropic OAuth rows), not Claude CLI state. When that refresh succeeds, the banner clears for the subscription provider without manual re-login and without waiting for a separate model request. +For OAuth credentials, the same `/auth/status` poll also attempts an automatic refresh when the stored credential has a refresh token and the access token is expired or within the refresh buffer. Anthropic banner state is keyed to `anthropic-subscription` (including legacy Anthropic OAuth rows), not Claude CLI state. When that refresh succeeds, the banner clears for the provider without manual re-login and without waiting for a separate model request. -If the OAuth credential has no refresh token, the refresh request fails, or the provider is not Anthropic, the provider stays expired and the banner remains visible. Re-authenticate with manual re-login from **Settings → Authentication** or Model Onboarding. On Fusion desktop, OAuth login URLs open in the operating system browser rather than an in-app Electron child window; the Settings/Onboarding UI keeps polling until the provider authenticates or the login truly stops. +If the OAuth credential has no refresh token or the refresh request fails/leaves the credential expired, the provider stays expired and the banner remains visible. Re-authenticate with manual re-login from **Settings → Authentication** or Model Onboarding. On Fusion desktop, OAuth login URLs open in the operating system browser rather than an in-app Electron child window; the Settings/Onboarding UI keeps polling until the provider authenticates or the login truly stops. - + @@ -808,30 +824,36 @@ For per-run aggregation, `GET /api/agents/:id/runs/:runId/cited-goals` returns ` ## Artifacts View -Artifacts view aggregates project markdown files, task documents, and registered artifacts. The dashboard title is **Artifacts**; the internal tab bar keeps the shipped **Project Files**, **Task Documents**, and **Artifacts** labels. +Artifacts view aggregates registered artifacts, project markdown files, and task documents. The dashboard title is **Artifacts**; the internal tab bar leads with **Artifacts** (the landing tab), followed by **Project Files** and **Task Documents**. On mobile the tab buttons render at the uniform 44px control height with non-wrapping labels in a horizontally scrollable row. Features: -- Group task documents by task ID (with revision history metadata) and show the parent task status badge in each task group header when status metadata is available +- Browse **Task Documents** in the same left-sidebar/right-pane pattern as **Project Files**: the sidebar groups document entries by task ID with revision metadata and parent task status badges when available, while the right pane loads the selected document content - Search documents across tasks -- Open project markdown files with inline preview +- Open project markdown files and task documents with inline preview - Browse the **Artifacts** tab for registry media registered by any agent, dashboard chat/user action, or system tool across tasks - Already-open global and task-detail artifact lists refresh live from the artifact registry event when an agent, dashboard chat session, user action, or system tool registers a new artifact, while preserving active search filters and task scoping - Use the tab-count badges to see the current counts for Project Files, Task Documents, and Artifacts; the Artifacts badge reflects the loaded `GET /api/artifacts` result set, including active search filters -- Use the responsive media gallery to scan thumbnail-first image and video cards with consistent framing, while audio, document, and generic artifacts remain readable cards in the same grid -- Expand image and video artifact thumbnails into a full-size lightbox; dismiss it with the close button, backdrop click, or Escape while non-previewable artifact cards keep their normal controls and links -- Preview artifact images inline, play video and audio with native controls, read document previews from inline content/description, and open generic `other` artifacts through their media URL (`GET /api/artifacts/:id/media`) -- Read artifact metadata on each card: type badge (`Image`, `Video`, `Audio`, `Document`, or `Other`), title, optional description/content preview, author ID, timestamp, and linked task title/ID when present -- Use **Open task** on an artifact card to jump back to the originating task when the artifact has a `taskId`; inside task detail, the **Artifacts** tab shows that task's documents and registered media artifacts together +- Browse the category-driven gallery: artifacts are broken down into **Images**, **Docs**, **PDFs**, **Videos**, **Audio**, and **Other** content categories (PDFs are detected by MIME type/extension regardless of registry type). "All" renders one section per present category; the chip row filters to a single category, and chips only appear for categories that exist +- Each category has a tailored experience: Images/Videos use a visual-first tile grid with hover metadata and a full-size lightbox; Docs open a full document viewer with rendered markdown; PDFs open an embedded viewer with an open-in-new-tab action; Audio renders inline player rows; Other renders compact download rows +- Video artifacts (agent-registered recordings, `path`-ingested MP4/WebM/MOV, and bridged video attachments) play with working seek because the media route serves HTTP byte ranges +- HTML doc artifacts (`mimeType: text/html`) render as **live sandboxed previews** by default in the document viewer (scripts allowed, same-origin denied), with a Preview/Source toggle and the same Edit mode as other docs +- **Edit any inline-content doc in place**: the document viewer's **Edit** button switches to an editor whose **Save** persists through `PATCH /api/artifacts/:id` and live-refreshes open galleries via the `artifact:updated` registry event; binary-backed documents stay read-only with a media link +- Every viewer (image/video lightbox, PDF viewer, document viewer) opens in a draggable, resizable floating window (drag by the viewer header, resize by any edge/corner; geometry persists per viewer kind); dismiss with the close button or Escape. Windows are non-blocking, so the gallery behind them stays interactive +- Read artifact metadata on cards, rows, and viewer footers: title, optional description, author ID, timestamp, size, and linked task ID when present +- Use the task link on a card/row or viewer footer to jump back to the originating task when the artifact has a `taskId`; inside task detail, the **Artifacts** tab shows that task's documents and registered media artifacts together +- The gallery scales down at the mobile breakpoint (including landscape phones): category chips scroll horizontally, visual grids collapse to two columns, cards and rows go single-column, and viewer windows clamp to the viewport - Loading state: the Artifacts tab shows `Loading artifacts…` while the first artifact list request is pending and no artifact results are loaded - Empty states: with no search query it shows `No artifacts yet.` plus the hint that artifacts are created by agents, users, and system tools; with a search query it shows `No artifacts match "".` - Error state: a failed artifact list request uses the shared `Failed to load artifacts: ` panel with a **Retry** action that re-runs the artifact fetch - Toggle between raw text and rendered markdown using the **Markdown/Plain** button -- Highlight text in raw or rendered project-file previews, choose **Add comment**, and send the file path, selected snippet, and your comment to the **New Task** dialog +- Highlight text in raw or rendered project-file previews or the selected Task Document's right pane, choose **Add comment**, and send the source path/key, selected snippet, and your comment to the **New Task** dialog Agent registrations also surface through the [Mailbox View](#mailbox-view): successful `fn_artifact_register` calls send a best-effort system inbox notification so users can discover new media even before opening the gallery. Artifact list live-refresh does not depend on that best-effort message; it listens to the registry registration event. -![Artifacts view](./screenshots/documents-view.png) +![Artifacts gallery](./screenshots/artifacts-gallery.png) + +![Artifact document viewer with edit mode](./screenshots/artifacts-doc-edit.png) ## Reports View @@ -860,7 +882,7 @@ Artifacts view supports toggling between raw text and formatted markdown when vi The toggle button is accessible with `aria-pressed` for screen readers. Toggle state is scoped per-document, so switching between documents resets the view to raw mode. -Project-file previews also support selection comments in both raw and rendered markdown modes. Select text, click **Add comment**, enter a short note, and Fusion opens **New Task** with a seeded description containing the file path, snippet, and comment. +Project-file previews and selected Task Documents also support selection comments in both raw and rendered markdown modes. Select text, click **Add comment**, enter a short note, and Fusion opens **New Task** with a seeded description containing the file path or task-document key, snippet, and comment. ## Todo View @@ -1224,6 +1246,8 @@ Inspect task definition, logs, review feedback, comments, artifacts, workflow ou - The top-level **Chat** tab appears first for active task details and is the default landing tab for non-`done` tasks. It uses the task's effective planning model, but opening the tab is lookup-only: Fusion creates the task-scoped planner chat only after you send a composer message, starter prompt, or planner-question answer. Once a user message exists, the resumable planner chat can appear in the global Chat list; interacted chats are kept when the task reaches `done` and removed when the task is archived. Each send includes bounded server-built task context so the planner can answer current status, progress, recent activity, dependency, and task definition questions. It shows starter prompts for common planning questions, can render structured planner questions, and converts only explicit operator steering intent through the scoped steering tool. The composer stays pinned while the transcript, loading, error, starter, history, and streaming states scroll internally; on mobile/narrow task detail, the default focused Chat layout hides nonessential title/metadata/tab/action rows until you collapse it from the in-view expand control. - The **Activity → Live**, **Feed**, and **Raw Logs** segments remain immediately after **Chat** and share an expand/collapse control that lets the active Activity segment fill the task-detail modal, then restores the normal header, tabs, and action footer when collapsed. - The **Summary** tab appears for `done` tasks and remains their default landing tab. It shows the recorded completion summary, changed-file/merge stats when available, completed steps, workflow results, retry counts, and a token usage & cost section broken down by model from the already-loaded task detail; unpriced models show cost as unavailable rather than `$0`. +- The **Cost** tab is available for tasks in every column. It shows the read-time derived per-model cost breakdown (input, output, cached, cache-write, total tokens, derived USD) and a task total; no token usage shows an explicit empty state, while unpriced or zero-usage rows use `—` instead of a guessed `$0`. + - Task-detail Activity steering comments are persisted as user comments/steering guidance and surfaced to every relevant agent lane: live executor sessions receive steering injection, while planner, reviewer (spec/plan/code), and merger agents (standard and clean-room AI merge/review) receive the latest user comments in their next prompt/pass. - The priority chip in task metadata is an inline picker: you can change priority directly without entering full edit mode. - Execution mode has a read-mode inline lightning-bolt toggle for Fast mode on/off without opening the full edit form. diff --git a/docs/plans/2026-06-23-001-feat-migrate-sqlite-to-postgres-plan.md b/docs/plans/2026-06-23-001-feat-migrate-sqlite-to-postgres-plan.md new file mode 100644 index 0000000000..475a3f71de --- /dev/null +++ b/docs/plans/2026-06-23-001-feat-migrate-sqlite-to-postgres-plan.md @@ -0,0 +1,416 @@ +--- +title: "feat: Migrate storage from SQLite to PostgreSQL (embedded + external)" +type: feat +date: 2026-06-23 +--- + +# Migrate storage from SQLite to PostgreSQL (embedded + external) + +## Summary + +Replace the SQLite storage layer with PostgreSQL following the Paperclip model: a bundled embedded Postgres binary (npm `embedded-postgres`) provides zero-config local storage, `DATABASE_URL` switches to an external server, and SQLite is removed after a dual-read cutover. The data layer is rewritten on Drizzle ORM (schema-as-code, type-safe), which also forces the entire synchronous `DatabaseSync` data-access surface to become async. + +## Problem Frame + +Fusion persists all project, central, and archive state in three SQLite files (`fusion.db`, `fusion-central.db`, `archive.db`) accessed through a synchronous `DatabaseSync` adapter over `node:sqlite`/`bun:sqlite`. This works for single-machine, multi-process use under WAL, but it couples the application tightly to SQLite-specific features (FTS5 + triggers, JSON1 functions, PRAGMAs, `ATTACH DATABASE`, corruption self-healing) and blocks any multi-host or managed-database deployment. The goal is a single PostgreSQL backend that preserves zero-config local operation while enabling an external server, matching the architecture Paperclip (`github.com/paperclipai/paperclip`) uses: embedded Postgres by default, `DATABASE_URL` to point elsewhere. + +The dominant cost is not dialect conversion but the **sync-to-async conversion**: the `DatabaseSync` interface is synchronous and every Postgres client is async, so every database call site across the ~17k-line `store.ts` and ~5.9k-line `db.ts` must become awaited, independent of the query layer. + +--- + +## Requirements + +### Backend topology and packaging + +- R1. When `DATABASE_URL` is unset, the application starts an embedded PostgreSQL instance (real Postgres process via `embedded-postgres`) into a local data directory, runs migrations, and serves with no external setup required. +- R2. When `DATABASE_URL` is set, the application connects to the specified external PostgreSQL server (local Docker, managed/hosted, or any reachable server) and does not start an embedded instance. +- R3. The embedded PostgreSQL binaries are bundled/shipped so `fn` works fully offline with zero system Postgres install on supported platforms (macOS, Linux, Windows; arm64 and x64). +- R4. A separate `DATABASE_MIGRATION_URL` is honored for startup schema work when the runtime `DATABASE_URL` uses a transaction-pooling connection (Supavisor/PgBouncer), mirroring the Paperclip split. + +### Data layer + +- R5. All schema is defined as Drizzle ORM code (schema-as-code) and all data access goes through Drizzle against a PostgreSQL backend. +- R6. The synchronous `DatabaseSync` data-access surface is replaced with an async data layer; no blocking/synchronous bridge to PostgreSQL remains. +- R7. Existing behavioral invariants are preserved through the rewrite: soft-delete visibility (`deletedAt IS NULL` filtering across all live readers), task-ID allocator reconciliation on store open, lineage-integrity gates, document/artifact parent-task scoping, and the handoff-to-review `mergeQueue` transactional invariant. + +### Full-text search + +- R8. The FTS5-backed task and archive search is replaced with PostgreSQL full-text search (`tsvector`/`tsquery`, GIN indexes) preserving search-result parity and the automatic index-sync-on-write behavior that today's FTS5 triggers provide. + +### Migration and compatibility + +- R9. A migration tool moves existing SQLite data (all three databases) into PostgreSQL idempotently and verifiably. +- R10. A dual-read cutover period is supported: during transition, SQLite is read-only and PostgreSQL is the write target, so deployments can migrate without downtime windows. +- R11. After cutover, SQLite is fully removed (no dual-dialect abstraction retained long-term, no `better-sqlite3`/`node:sqlite`/`bun:sqlite` data-path dependency). + +### Health and maintenance + +- R12. SQLite-specific health and maintenance surfaces are reworked for PostgreSQL: corruption detection (`PRAGMA integrity_check`/`quick_check`) and the startup rebuild-on-malformed guard, compaction (`VACUUM`), WAL checkpointing, and the schema self-heal via `PRAGMA table_info`/fingerprint reconciliation. + +--- + +## Key Technical Decisions + +- **Drizzle ORM for the full data-layer rewrite.** User-confirmed. The existing code is ~700KB+ of hand-written SQL against a sync `prepare()` interface with zero ORM; Drizzle gives schema-as-code, type safety, and a migration system. This is a near-total data-layer rewrite rather than a dialect conversion. Adopted over raw-SQL `postgres.js` (which would have preserved the architecture but offered no schema model). + +- **Sync-to-async conversion is mandatory and load-bearing.** The entire data layer is synchronous; every PostgreSQL client is async. Every `db.prepare(sql).get()` call site becomes `await`. Store methods are already `async`, so the boundary exists, but every internal database call must be awaited. This dwarfs all other conversion work and drives sequencing. + +- **Bundle embedded PostgreSQL binaries for zero-config default.** User-confirmed. `embedded-postgres` manages `initdb`/`pg_ctl` lifecycle over platform-specific Postgres binaries (~30-50MB per platform). True offline zero-config like SQLite today, at the cost of heavier distribution and known platform edge cases (WSL2, unprivileged LXC containers, macOS dyld loading) that Paperclip also encounters. + +- **Backend resolution by `DATABASE_URL` (Paperclip model).** Unset = embedded (real Postgres process, supports multiple concurrent connections and thus preserves the existing multi-process access pattern that PGlite/WASM cannot). Set = external server. `DATABASE_MIGRATION_URL` splits schema work off pooled runtime connections. + +- **Snapshot final SQLite schema as the PostgreSQL baseline + fresh Drizzle migrations.** Reimplementing the 128 hand-rolled SQLite migrations (`SCHEMA_VERSION = 128`) in PostgreSQL dialect is pointless for a greenfield Postgres schema. The migration tool materializes the current final schema into PostgreSQL, and Drizzle's migration history starts fresh from that snapshot. The version-gate testing discipline (the institutional learning that fresh-DB tests cannot catch a skipped-on-upgrade migration) is carried forward into the Drizzle migration tests. + +- **Dual-read = SQLite read-only + PostgreSQL write target.** During cutover, writes go to PostgreSQL; reads fall back to SQLite for any path not yet ported or for verification. This is lower-risk than a dual-routing query abstraction and avoids two-writer contention. The institutional learning that two engines race task leases over the shared central SQLite DB is respected: the cutover must not run two writers against SQLite, and PostgreSQL's MVCC structurally removes the single-writer contention. + +- **Three-database topology preserved as PostgreSQL schemas or databases.** The project/central/archive separation is retained (project state, global registry, cold-storage archive), mapping each to a PostgreSQL schema or database rather than collapsing them. + +--- + +## High-Level Technical Design + +```mermaid +flowchart TB + subgraph Resolution["Backend resolution (startup)"] + D{DATABASE_URL set?} + end + D -- no --> E[Embedded Postgres lifecycle manager] + D -- yes --> X[External Postgres server] + E --> EP[initdb if needed
pg_ctl start
local data dir] + EP --> CONN + X --> CONN + CONN[Drizzle connection pool
runtime URL + DATABASE_MIGRATION_URL] --> SCHEMA[Drizzle schema
schema-as-code] + SCHEMA --> STORES[Async data layer
store.ts + satellite stores] + STORES --> FTS[tsvector/GIN search] + STORES --> HEALTH[Postgres health
autovacuum, integrity] + MIG[SQLite to Postgres
migration tool] --> SCHEMA + DUAL[Dual-read cutover harness
SQLite RO + Postgres RW] --> STORES +``` + +### Sync-to-async conversion shape + +The current layering is: async store methods (`async createTask`) calling a synchronous DB layer (`this.db.prepare(sql).get()`). The rewrite inverts the inner layer to async Drizzle calls (`await db.select()...` / `await tx.insert()`). Because the store boundary is already async, callers above `TaskStore` are unaffected; the change is contained to the data layer's internal call sites. Transaction semantics move from SQLite `BEGIN IMMEDIATE` + `SAVEPOINT` to Drizzle transaction callbacks (`db.transaction(async (tx) => ...)`), which must preserve the per-mutation atomicity the current `transactionImmediate()` path guarantees. + +### Migration and cutover sequence + +```mermaid +sequenceDiagram + participant Op as Operator + participant App as Application + participant ST as SQLite (RO) + participant PG as PostgreSQL + participant Tool as Migration tool + Op->>Tool: Run SQLite→Postgres migration + Tool->>ST: Snapshot final schema + bulk copy data + Tool->>PG: Materialize schema + load data + build tsvector + Tool->>Op: Report row-count verification + Op->>App: Enable dual-read mode + App->>PG: All writes + App->>ST: Read fallback (unported paths / verification) + Op->>App: Confirm parity, disable SQLite + App->>ST: Remove SQLite data path + deps +``` + +--- + +## Scope Boundaries + +### In scope + +- PostgreSQL connection layer with embedded/external resolution and lifecycle management. +- Drizzle schema definition for all existing tables across project, central, and archive databases. +- Async rewrite of the data layer (`store.ts`, `db.ts`, `central-db.ts`, `archive-db.ts`, and satellite `*-store.ts` files). +- Full-text search replacement (FTS5 to `tsvector`/GIN). +- Health/maintenance surface rework. +- SQLite-to-PostgreSQL data migration tool. +- Dual-read cutover harness and SQLite removal. + +### Deferred to Follow-Up Work + +- Performance benchmarking and query-plan tuning against production-scale data (after the rewrite lands and real workloads run). +- Managed-host deployment guides (Supabase/RDS connection string specifics beyond the `DATABASE_URL`/`DATABASE_MIGRATION_URL` contract). +- Read-replica or connection-pooler deployment topology recommendations. +- Central-DB multi-host replication across machines (the mesh/node replication that already exists is out of scope; only its storage backend changes). + +--- + +## System-Wide Impact + +- **All `@fusion/*` packages** consume the data layer; the async conversion ripples into `@fusion/engine` (worktree DB hydration, self-healing) and `@fusion/dashboard` (health endpoint, DB-corruption banner, routes). +- **Plugin stores** instantiate core's `Database`. The `fusion-plugin-roadmap` plugin has its own store layer on core's `Database` and pins schema versions. The backend swap must stay behind a stable data-layer interface so plugin stores keep working. +- **Backup/restore** changes fundamentally: SQLite file-copy backups become PostgreSQL logical dumps (`pg_dump`/restore). `backup.ts` and the `BackupManager` pairing behavior (project + central pair) are reworked. +- **CLI** (`fn db ...` commands, `--vacuum`, run-audit surfaces) changes surface and behavior. +- **Distribution** grows by ~30-50MB per platform for bundled Postgres binaries; the desktop build (`packages/desktop`) and CLI bundling are affected. +- **Concurrency model** shifts from SQLite WAL multi-process-over-one-file to a PostgreSQL server process, structurally resolving the documented central-DB task-lease race but introducing connection-pool and server-lifecycle management. + +--- + +## Risks & Dependencies + +- **Async-conversion correctness.** Missed `await`s, transaction isolation drift from `BEGIN IMMEDIATE`, and changed lock semantics are the highest-severity regression vectors. Mitigation: characterization coverage of current transactional paths before rewrite; the merge gate (`pnpm test:gate`) as the authoritative signal. +- **embedded-postgres platform failures.** Paperclip reports initdb failures on WSL2, unprivileged LXC, and macOS dyld. Mitigation: graceful fallback messaging; document unsupported environments; consider external-server fallback guidance. +- **FTS search parity.** `tsvector` ranking and tokenization differ from FTS5; result ordering and recall may shift. Mitigation: capture current search result fixtures as characterization baselines before replacing. +- **Data-migration fidelity.** Soft-delete visibility, JSON column fidelity (SQLite text-JSON to JSONB), FTS index rebuild, and `AUTOINCREMENT` sequence continuity must survive the copy. Mitigation: idempotent, row-count-verified migration with a dry-run mode. +- **Plugin-store contract drift.** If the data-layer interface narrows, plugin stores break. Mitigation: keep the store contract stable; schema-version pinning continues to work against the new migration history. +- **Distribution size and CI.** Bundled binaries change install size and may affect CI image caching; the desktop build pipeline must fetch/verify platform binaries. +- **Per the standing rule, flaky tests are quarantined on sight.** The rewrite will surface pre-existing flakiness; quarantine, do not appease. + +--- + +## Implementation Units + +### Phase 1 — Foundation: backend, connection, schema + +### U1. PostgreSQL connection layer and backend resolution + +- **Goal:** Resolve the backend at startup (embedded vs external via `DATABASE_URL`) and expose a Drizzle connection pool with the `DATABASE_MIGRATION_URL` split. +- **Requirements:** R1, R2, R4 +- **Dependencies:** none +- **Files:** `packages/core/src/postgres/connection.ts` (new), `packages/core/src/postgres/backend-resolver.ts` (new); touches startup wiring in `packages/core/src/central-core.ts` / `packages/dashboard/src/server.ts` +- **Approach:** A resolver reads `DATABASE_URL` (external) or signals embedded mode (U2). Runtime queries use the resolved URL; schema/migration work uses `DATABASE_MIGRATION_URL` when present, else the runtime URL. Connection pooling defaults to a small pool; document the transaction-pooling caveat (prepared-statement incompatibility) that motivates the migration-URL split. **Precondition (de-risk before Phase 2):** validate the chosen Drizzle driver bundles cleanly under the desktop Bun `--compile` build by probing both `postgres.js` and `pg` against the real `packages/desktop` build — the current `sqlite-adapter.ts` exists precisely because Bun `--compile` mishandles certain native modules, so this must be confirmed before the rewrite depends on it. +- **Patterns to follow:** Paperclip `DATABASE.md` connection-mode table; the existing settings-resolution hierarchy in `packages/core/src/settings-schema.ts`. +- **Test scenarios:** + - Happy path: unset `DATABASE_URL` resolves to embedded mode; set `DATABASE_URL` resolves to external and skips embedded start. + - `DATABASE_MIGRATION_URL` present routes schema work to it while runtime uses `DATABASE_URL`. + - Invalid/unreachable `DATABASE_URL` fails loudly with an actionable message. + - Pooled runtime URL with no `DATABASE_MIGRATION_URL` warns about prepared-statement risk. + - Security: the connection string (including any password in `DATABASE_URL`) is never written to logs, and connection-error messages redact credentials. +- **Verification:** Startup logs the resolved backend and connection target; a health probe succeeds against the resolved backend. + +### U2. Embedded PostgreSQL lifecycle manager + +- **Goal:** Manage an embedded Postgres process (`initdb`, ensure database exists, `pg_ctl` start/stop) over a local data directory using `embedded-postgres`. +- **Requirements:** R1, R3 +- **Dependencies:** U1 +- **Files:** `packages/core/src/postgres/embedded-lifecycle.ts` (new); bundled binary acquisition in `packages/desktop/scripts/build.ts` and `package.json` (`optionalDependencies`/postinstall) +- **Approach:** On first start, `initdb` into the data directory, create the application database, run migrations, then serve. Persist across restarts; deleting the directory resets local state (mirroring the current SQLite reset behavior). Acquire platform/arch binaries (`embedded-postgres` supports macOS/Linux/Windows, arm64/x64). Handle graceful shutdown (`pg_ctl stop`) on process exit. +- **Patterns to follow:** Paperclip embedded flow (`~/.paperclip/instances/default/db/`); the existing process-supervision discipline (`superviseSpawn` from `@fusion/core` — do not use raw detached spawn/nohup per AGENTS.md). +- **Test scenarios:** + - Happy path: first start runs `initdb`, creates DB, runs migrations; second start reuses the directory without re-init. + - Existing data directory with prior schema starts without re-running init. + - Graceful shutdown stops the Postgres process; no orphaned process remains. + - Corrupt/locked data directory surfaces a clear error rather than hanging. +- **Verification:** The application serves with no external Postgres installed; the data directory persists state across restarts. + +### U3. Drizzle schema definition (schema-as-code baseline) + +- **Goal:** Define the complete PostgreSQL schema in Drizzle for all existing tables across project, central, and archive databases, materialized from the current final SQLite schema (snapshot, not the 128 incremental migrations). +- **Requirements:** R5 +- **Dependencies:** U1 +- **Files:** `packages/core/src/postgres/schema/` (new, organized by database: project, central, archive); Drizzle config (`drizzle.config.ts`); fresh migration directory +- **Approach:** Translate every existing table (tasks, branch_groups, mergeQueue, config, workflow_steps, activityLog, task_commit_associations, archivedTasks, automations, agents, agentHeartbeats, approval_requests(+audit), secrets, task_documents(+revisions), artifacts, __meta, goals, missions hierarchy, plugins, routines, roadmaps, todos, chat tables, runAuditEvents, research/eval/experiment tables, etc.) into Drizzle table definitions. Map SQLite types: `INTEGER PRIMARY KEY AUTOINCREMENT` to identity/serial, JSON text columns to `jsonb`, the FTS5 tables to U7's tsvector design. Preserve all CHECK constraints, foreign keys with cascade rules, and unique indexes. +- **Patterns to follow:** Existing schema declarations in `packages/core/src/db.ts` (`SCHEMA_SQL`, `MIGRATION_ONLY_TABLE_SCHEMAS`) as the source of truth for the snapshot; Drizzle schema conventions. +- **Test scenarios:** + - Happy path: applying the fresh Drizzle migration to an empty database yields a schema matching the current final SQLite schema (column-by-column, constraint-by-constraint). + - Every foreign-key cascade rule and unique index from the SQLite schema is present. + - JSON columns round-trip as JSONB with the same shape. + - Plugin-owned tables (roadmap milestones/features) are included via the plugin schema-init hook. +- **Verification:** A schema-diff between a migrated PostgreSQL database and a fresh-Drizzle-applied database shows no structural differences. + +--- + +### Phase 2 — Data-layer rewrite (sync to async, Drizzle) + +### U4. Async data-layer foundation (replace DatabaseSync) + +- **Goal:** Replace the synchronous `DatabaseSync` adapter with an async Drizzle-backed connection and the core CRUD/transaction primitives the stores depend on. +- **Requirements:** R5, R6, R7 +- **Dependencies:** U1, U3 +- **Files:** `packages/core/src/postgres/data-layer.ts` (new); removes the sync `DatabaseSync`/`Statement` surface in `packages/core/src/db.ts`; `packages/core/src/sqlite-adapter.ts` (retained only for the dual-read period, then removed in U11) +- **Approach:** Provide the async primitives stores need: prepared-statement-equivalent query helpers, `db.transaction(async (tx) => ...)` preserving the atomicity of the current `transactionImmediate()` path, and the run-audit-event-within-transaction behavior (`recordRunAuditEvent` inside the shared transaction). Define the stable data-layer interface plugin stores consume so the backend swap is invisible to them. The `getDatabase()` accessor's contract changes: it must return an async-capable connection rather than the synchronous `Database` (U15 converts the direct-`prepare()` consumers that relied on the sync shape). +- **Patterns to follow:** Current transaction helpers (`Database.transaction()`, `transactionImmediate()`) in `packages/core/src/db.ts`; the run-audit-within-transaction pattern. +- **Test scenarios:** + - Happy path: an insert + matching audit insert commit or roll back together. + - A failing mutation inside a transaction rolls back all writes including the audit row. + - Concurrent transactions do not observe partial writes. + - The plugin-facing data-layer contract compiles against `fusion-plugin-roadmap`'s store usage. +- **Verification:** The foundation supports a representative store mutation (create task + audit) atomically and async. + +### U5. Decompose `store.ts` into cohesive modules + +- **Goal:** Break the ~17k-line `TaskStore` god-class into cohesive per-responsibility modules behind the existing `TaskStore` facade, as a pure behavior-invariant refactor that makes each subsequent migration independently landable. +- **Requirements:** R5, R7 +- **Dependencies:** none (pure refactor, no backend change) +- **Files:** `packages/core/src/store.ts` (extract); new modules under `packages/core/src/task-store/` (e.g. persistence, allocator, settings, lifecycle, merge-coordination, archive-lineage, branch-groups, workflow-workitems, audit, search, comments) +- **Approach:** Extract the distinct responsibility areas into separate modules without changing behavior or the backend: task persistence + allocator reconciliation, settings, task lifecycle/moves + workflow transitions, soft-delete/archive/lineage, merge-queue + merge, branch-groups + PR-entities/threads, workflow work-items + completion handoff, audit/activity-log/run-audit, search, comments/attachments, goal/usage/plugin events, file-watching, task-ID-integrity. Keep the `TaskStore` class as a facade composing the modules so callers are unaffected. No async or Drizzle changes yet. +- **Execution note:** Behavior-invariant by design — the existing gate (`pnpm test:gate`) plus `store-concurrent-writes` / `checkout-claim-mutex` tests verify the extraction for free. Per the mass-migration learning, this is a no-two-agents-share-a-file extraction, not a backend swap. +- **Patterns to follow:** `docs/solutions/architecture-patterns/mass-migration-agent-fleet-orchestration.md` (verification-invariance for mechanical extraction). +- **Test scenarios:** + - Test expectation: none -- behavior-invariant refactor; the existing gate and concurrent-write/mutex tests are the verification surface. +- **Verification:** `pnpm test:gate` passes with no behavior change; the facade preserves every public method signature. + +### U6. Satellite stores and databases rewrite + +- **Goal:** Rewrite the central database (`central-db.ts`), archive database (`archive-db.ts`), and satellite stores (`message-store.ts`, `chat-store.ts`, `mission-store.ts`, `insight-store.ts`, `research-store.ts`, `eval-store.ts`, `experiment-session-store.ts`, `routine-store.ts`, `plugin-store.ts`, `goal-store.ts`, `todo-store.ts`, `reflection-store.ts`, `automation-store.ts`, `approval-request-store.ts`, `secrets-store.ts`, `agent-store.ts`) to async Drizzle, plus `worktree-db-hydrate.ts`. +- **Requirements:** R5, R6, R7 +- **Dependencies:** U4 +- **Files:** the `*-store.ts` files in `packages/core/src/`; `packages/core/src/central-db.ts`, `packages/core/src/archive-db.ts`; `packages/engine/src/worktree-db-hydrate.ts` +- **Approach:** Same sync-to-async, dialect-to-Drizzle conversion as U5, applied per store. The archive database (cold storage, append-only FTS) maps to its PostgreSQL schema with the lighter-touch tsvector maintenance. Worktree DB hydration copies task-scoped metadata into the worktree's connection (now a scoped query against the shared PostgreSQL backend rather than a separate SQLite file hydration). +- **Patterns to follow:** Each store's current SQLite implementation; the central-DB concurrency note from the learnings (two engines racing leases — the new backend removes single-writer contention). +- **Test scenarios:** + - Happy path per store: representative create/read/update/delete. + - Central DB: secret encryption round-trips; access-policy CHECK constraints hold. + - Archive: archived task snapshots persist and are searchable. + - Worktree hydration: task + dependency metadata is copied for the active graph; binary artifact files are not copied. +- **Verification:** Each store's existing tests pass against PostgreSQL; the worktree-hydrate test passes. + +### U12. Migrate TaskStore persistence, allocator, and settings modules + +- **Goal:** Migrate the decomposed task-persistence, ID-allocator-reconciliation, and settings modules (from U5) from sync SQLite to async Drizzle. +- **Requirements:** R5, R6, R7 +- **Dependencies:** U4, U5 +- **Files:** `packages/core/src/task-store/persistence.ts`, `packages/core/src/task-store/allocator.ts`, `packages/core/src/task-store/settings.ts` (from U5); `packages/core/src/distributed-task-id.ts`, `packages/core/src/task-id-integrity.ts` +- **Approach:** Convert the persistence-module call sites to awaited Drizzle queries. Preserve soft-delete visibility (`deletedAt IS NULL`) across all live readers, create-class non-destructive inserts, and allocator reconciliation bumping each prefix sequence to `max(current, max(task suffix)+1, max(archived suffix)+1, max(reservation)+1)` on store open. Settings reads/writes move to Drizzle against the `config` table. Carry FNXC comments forward. +- **Execution note:** Characterization coverage of allocator reconciliation before migration; the merge gate is the authoritative signal. +- **Patterns to follow:** Current allocator reconciliation and soft-delete invariants in `docs/storage.md`. +- **Test scenarios:** + - Happy path: create/read/update/delete a task end to end. + - Soft-delete: live readers hide `deletedAt` rows; forensic reads surface them. + - Allocator reconciliation: stale sequences self-heal to max suffix; soft-deleted/archived IDs stay reserved. + - Settings: read/update project and global settings round-trip. +- **Verification:** Persistence, allocator, and settings tests pass against PostgreSQL. + +### U13. Migrate TaskStore lifecycle and merge-coordination modules + +- **Goal:** Migrate the task-lifecycle/moves/workflow-transitions and merge-queue/merge modules (from U5) to async Drizzle, preserving the transactional invariants. +- **Requirements:** R5, R6, R7 +- **Dependencies:** U5, U12 +- **Files:** `packages/core/src/task-store/lifecycle.ts`, `packages/core/src/task-store/merge-coordination.ts` (from U5) +- **Approach:** Convert move/handoff/merge call sites to awaited Drizzle. Preserve the handoff-to-review `mergeQueue` invariant: the column move, `mergeQueue` insert, and handoff audit fan-out run in one Drizzle transaction (`db.transaction`), so observers never see `column = "in-review"` without the matching queue row. Merge-queue leasing (priority-first + FIFO within priority, recoverable expired leases) maps to Drizzle transactions with row-level locking. +- **Patterns to follow:** The handoff invariant and merge-queue lease semantics in `docs/storage.md` and `packages/core/src/store.ts`. +- **Test scenarios:** + - Happy path: move a task through columns; hand off to review; acquire/release a merge-queue lease. + - Handoff invariant: column move + `mergeQueue` insert + audit are atomic; a failure rolls back all three. + - Merge-queue lease: priority-first ordering; expired leases recover without incrementing attempts. +- **Verification:** Lifecycle and merge-coordination tests pass against PostgreSQL; the checkout-claim-mutex test passes. + +### U14. Migrate TaskStore remaining modules (archive/lineage, branch-groups, workflow work-items, audit, comments) + +- **Goal:** Migrate the remaining decomposed TaskStore modules (archive/lineage, branch-groups/PR-entities, workflow work-items/completion-handoff, audit/activity-log/run-audit, comments/attachments, goal/usage/plugin events) to async Drizzle. +- **Requirements:** R5, R6, R7 +- **Dependencies:** U5, U12 +- **Files:** `packages/core/src/task-store/archive-lineage.ts`, `packages/core/src/task-store/branch-groups.ts`, `packages/core/src/task-store/workflow-workitems.ts`, `packages/core/src/task-store/audit.ts`, `packages/core/src/task-store/comments.ts` (from U5) +- **Approach:** Convert each module's call sites to awaited Drizzle. Preserve lineage-integrity gates (live children block parent delete/archive; `removeLineageReferences` clears them), document/artifact parent-task scoping under soft-delete, and run-audit-event-within-transaction behavior. The search module is migrated here for query structure, paired with U7's tsvector index. File-watching and task-ID-integrity detection move to PostgreSQL-backed reads. +- **Patterns to follow:** Lineage children, documents under soft-deleted tasks, and the artifact registry semantics in `docs/storage.md`. +- **Test scenarios:** + - Lineage: deleting a parent with live children throws; `removeLineageReferences` clears them; archived/soft-deleted children do not block. + - Archive: archived snapshots persist and are searchable; unarchive restores. + - Audit: a mutation and its run-audit event commit or roll back together. + - Comments/attachments: add/update/delete round-trip on an active task. +- **Verification:** Remaining TaskStore module tests pass against PostgreSQL. + +### U15. Migrate engine and dashboard direct-`prepare()` consumers + +- **Goal:** Convert the `@fusion/engine` and `@fusion/dashboard` consumers that bypass store methods and call the sync `Database`/`prepare()` surface directly, once `getDatabase()` returns an async connection (U4). +- **Requirements:** R5, R6 +- **Dependencies:** U4, U6, U12 +- **Files:** `packages/dashboard/src/monitor-store.ts`, `packages/dashboard/src/server.ts` (store-construction sites passing `getDatabase()`), `packages/dashboard/src/routes/register-*.ts` (store-construction sites), `packages/engine/src` callers of `store.getDatabase()` and direct `prepare()` (self-healing, worktree hydration); the `packages/engine/src/worktree-db-hydrate.ts` path already covered by U6 +- **Approach:** Replace direct `db.prepare(sql).run/get/all` calls in dashboard stores (notably `monitor-store.ts`) and route handlers with awaited Drizzle queries or routed through the relevant async store. Update store-construction sites that pass the raw `Database` (`new ChatStore(store.getDatabase())`, `new AiSessionStore(...)`, `new ApprovalRequestStore(...)`) to pass the async connection or the owning store. Convert engine test/self-healing direct-`prepare()` sites to async Drizzle. +- **Patterns to follow:** The async store-method boundary established in U4/U6; existing route store-construction patterns. +- **Test scenarios:** + - Happy path: dashboard monitor deployments/incidents/metrics read and write via the async path. + - Each migrated route store constructs against the async connection and serves requests. + - Engine self-healing mutations that previously used direct `prepare()` persist via async Drizzle. +- **Verification:** Dashboard and engine tests pass against PostgreSQL; no direct sync `prepare()` call sites remain in `packages/dashboard/src` or `packages/engine/src`. + +--- + +### Phase 3 — SQLite-specific surfaces + +### U7. Full-text search replacement (FTS5 to tsvector/GIN) + +- **Goal:** Replace the FTS5 external-content tables and triggers (`tasks_fts`, `archived_tasks_fts`) with PostgreSQL `tsvector`/GIN full-text search, preserving result parity and automatic sync-on-write. +- **Requirements:** R8 +- **Dependencies:** U3, U5, U6 +- **Files:** `packages/core/src/postgres/schema/` (fts columns/indexes); search-query paths in `packages/core/src/store.ts` (`searchTasks`) and the archive store; the FTS maintenance step in self-healing +- **Approach:** Use generated `tsvector` columns over the indexed text columns with GIN indexes, kept in sync via PostgreSQL generated columns/triggers (preserving the automatic sync that today's FTS5 `ai`/`au`/`ad` triggers provide). The value-aware partial-update optimization (only changed text columns touch the index) maps to PostgreSQL only re-generating the tsvector when source text columns change. Replace the FTS5 corruption/maintenance self-healing step with PostgreSQL index health (`REINDEX`/autovacuum) and the bounded rebuild-on-bloat threshold logic. +- **Patterns to follow:** Current FTS5 design and the `rebuildFts5Index()`/merge/optimize thresholds in `packages/core/src/db.ts`; the documented defer rationale in `docs/storage.md` (attached live-FTS investigation). +- **Test scenarios:** + - Happy path: search returns the same tasks for a representative query set as the FTS5 baseline. + - Insert/update/delete keep the tsvector in sync automatically. + - Non-text mutation does not needlessly re-generate the index. + - Index rebuild on bloat threshold restores search without data loss. +- **Verification:** Search-result fixtures captured pre-rewrite pass post-rewrite. + +### U8. Health and maintenance surface rework + +- **Goal:** Rework the SQLite-specific health and maintenance surfaces for PostgreSQL: corruption detection, startup rebuild-on-malformed, compaction, WAL checkpointing, and schema self-heal. +- **Requirements:** R12 +- **Dependencies:** U4, U5 +- **Files:** `packages/core/src/db.ts` (integrity/VACUUM/WAL-checkpoint paths); `packages/dashboard/app/components/DbCorruptionBanner.tsx`; `packages/dashboard/src/routes` (health endpoint `taskIdIntegrity`); `packages/engine/src/__tests__/self-healing-db-corruption.test.ts` +- **Approach:** Replace `PRAGMA integrity_check`/`quick_check` and the startup rebuild-on-malformed guard with PostgreSQL health checks (`pg_stat`/connection liveness) and a restore-from-backup path on corruption. Replace `VACUUM`/WAL checkpoint with autovacuum tuning plus an explicit `VACUUM`/`ANALYZE` operator command. Replace the schema self-heal via `PRAGMA table_info`/fingerprint reconciliation with an `information_schema`/`pg_catalog`-based check driven by Drizzle's known schema. Preserve the task-ID-integrity detector (duplicate IDs, cross-table collisions, sequence drift) against PostgreSQL. +- **Patterns to follow:** Current integrity/VACUUM paths and the schema self-heal fingerprint mechanism in `packages/core/src/db.ts`. +- **Test scenarios:** + - Happy path: healthy database reports green health. + - Task-ID integrity anomalies (duplicate IDs, sequence drift) are detected and surface the banner. + - Schema drift detection catches a missing column and reconciles it. + - Explicit compaction command runs `VACUUM`/`ANALYZE` and reports stats. +- **Verification:** The health endpoint and corruption banner behave as before; the self-healing-db-corruption test passes in its PostgreSQL form. + +--- + +### Phase 4 — Migration, cutover, removal + +### U9. SQLite-to-PostgreSQL data migration tool + +- **Goal:** Build a tool that snapshots the current final SQLite schema into PostgreSQL and bulk-copies all data (all three databases), idempotently and with verification. +- **Requirements:** R9 +- **Dependencies:** U3, U5, U6, U7 +- **Files:** `scripts/migrate-sqlite-to-postgres.mjs` (new); `packages/core/src/db-migrate.ts` (snapshot reference) +- **Approach:** Read each SQLite database, map types (text-JSON to JSONB, integers to appropriate types), stream rows into the PostgreSQL schema via Drizzle, rebuild the tsvector indexes, and verify row counts per table. Support a dry-run mode. Handle the soft-delete/deletedAt rows, JSON column fidelity, and `AUTOINCREMENT` sequence continuity (set sequences to max(id)+1). The tool targets the embedded or external PostgreSQL backend via `DATABASE_URL`. +- **Patterns to follow:** The existing one-shot reconciliation scripts in `scripts/` (e.g. `reconcile-leaked-soft-deletes.mjs`) for the bounded, idempotent, dry-run-default shape. +- **Test scenarios:** + - Happy path: a populated SQLite database migrates to PostgreSQL with matching row counts per table. + - Idempotency: re-running against an already-migrated PostgreSQL database is a no-op or a clean re-sync. + - JSON columns round-trip with identical shape. + - Sequences are set to max(id)+1 so new inserts do not collide. + - Dry-run reports the planned copy without writing. +- **Verification:** A migrated PostgreSQL database passes the same store tests as a natively-created one. + +### U10. Dual-read cutover harness + +- **Goal:** Support a transition window where SQLite is read-only and PostgreSQL is the write target, so deployments migrate without a downtime window. +- **Requirements:** R10 +- **Dependencies:** U9 +- **Files:** `packages/core/src/postgres/dual-read-harness.ts` (new); backend wiring touched in U1 +- **Approach:** A mode flag routes all writes to PostgreSQL while reads fall back to SQLite solely for parity verification (all live data paths are already on PostgreSQL by this point — U10 runs after U5/U6/U7 ported every store). Enforce SQLite read-only (reject writes) to prevent two-writer contention that the learnings warn races task leases. Provide a parity-check command that compares SQLite vs PostgreSQL read results for a sample of queries. The parity check must exclude search-result ordering — FTS5 (SQLite) and tsvector (PostgreSQL, from U7) rank and tokenize differently, so strict search ordering comparison would report false failures; search parity is validated separately against captured fixtures in U7, and the dual-read parity check compares row membership only for search. Document the operator sequence: migrate (U9) → enable dual-read → verify parity → disable SQLite (U11). +- **Patterns to follow:** The dual-engine safety guidance in `docs/solutions/developer-experience/browser-testing-dashboard-from-worktree-safely.md` (the daemon/lease-race hazard). +- **Test scenarios:** + - Happy path: in dual-read mode, a write lands in PostgreSQL and is readable from PostgreSQL. + - A write attempt against SQLite in dual-read mode is rejected. + - Parity check reports matching row membership for sampled queries, excluding search-result ordering. +- **Verification:** A deployment can run in dual-read mode serving live traffic with PostgreSQL as the sole writer. + +### U11. SQLite removal, fresh migration baseline, and cleanup + +- **Goal:** Remove SQLite entirely after cutover: drop the SQLite data path and dependencies, establish the fresh Drizzle migration history as authoritative, and rework backup/restore for PostgreSQL. +- **Requirements:** R11, R12 +- **Dependencies:** U10 +- **Files:** `packages/core/src/sqlite-adapter.ts` (remove), `packages/core/src/sqlite-validation.ts` (remove), SQLite paths in `db.ts`/`store.ts` (remove); `packages/core/src/backup.ts` (rework to `pg_dump`/restore); `package.json` (remove `better-sqlite3`); `plugins/fusion-plugin-even-realities-glasses/package.json`, `packages/desktop/scripts/build.ts`; `docs/storage.md`, `AGENTS.md` (SQLite-specific sections) +- **Approach:** Delete the SQLite adapter and validation, the FTS5 probe, the `ATTACH DATABASE` archive path, and SQLite-specific maintenance. Make the fresh Drizzle migration history the sole schema authority with the version-gate testing discipline carried forward. Rework `BackupManager` to PostgreSQL logical dumps (project + central pairing preserved as separate dumps). Update operator docs to reflect the `DATABASE_URL`/embedded model. +- **Patterns to follow:** The version-gate regression-test learning (seed-at-previous-version tests for skipped-on-upgrade detection), applied to Drizzle migrations. +- **Test scenarios:** + - Happy path: the application starts, runs, and passes the full gate with no SQLite code path reachable. + - No `better-sqlite3`/`node:sqlite`/`bun:sqlite` import remains in the data path. + - Backup produces a restorable PostgreSQL dump; restore round-trips. + - Fresh Drizzle migration history applies cleanly to an empty database. +- **Verification:** `pnpm verify:workspace` passes; grep for SQLite symbols in the data path returns nothing. + +--- + +## Open Questions + +- **Project/central/archive as separate databases or schemas in one database.** Both are valid; separate databases mirror today's separate files most closely and simplify backup pairing, while schemas-in-one-database simplify embedded single-instance management. Resolve during U3; the data layer abstracts the choice either way. + +- **embedded-postgres version pin and checksum verification.** The bundled Postgres binaries need a pinned version and (per the external-integration evidence rule) a checksum or `upstream-pending-verification` marker. Confirm during U2. + +--- + +## Sources & Research + +- Paperclip database model: `github.com/paperclipai/paperclip` `doc/DATABASE.md` — embedded default, `DATABASE_URL` switching, `DATABASE_MIGRATION_URL` split, plugin database namespaces. +- `embedded-postgres` package: `github.com/leinelissen/embedded-postgres`, `npmjs.com/package/embedded-postgres` — `initdb`/`pg_ctl` lifecycle, platform/arch binaries; known failure modes (WSL2, unprivileged LXC, macOS dyld) tracked in `paperclipai/paperclip` issues #1032, #828, #3583. +- Current storage architecture: `docs/storage.md` (hybrid storage model, FTS5 maintenance, attached-FTS defer rationale, write-path lock recovery). +- Migration engine: `packages/core/src/db.ts` (`SCHEMA_VERSION = 128`, `applyMigration`, `SCHEMA_COMPAT_FINGERPRINT`); `docs/solutions/database-issues/schema-version-constant-must-equal-highest-migration.md` (version-gate invariant). +- Concurrency hazard: `docs/solutions/developer-experience/browser-testing-dashboard-from-worktree-safely.md` (two engines racing task leases over the central SQLite DB). +- Plugin store coupling: `docs/solutions/test-failures/schema-version-sweep-must-include-plugin-workspaces.md` (`fusion-plugin-roadmap` instantiates core's `Database`). diff --git a/docs/plans/2026-06-23-001-fix-workflow-runtime-cutover-plan.md b/docs/plans/2026-06-23-001-fix-workflow-runtime-cutover-plan.md new file mode 100644 index 0000000000..8d98514835 --- /dev/null +++ b/docs/plans/2026-06-23-001-fix-workflow-runtime-cutover-plan.md @@ -0,0 +1,100 @@ +--- +title: Fix Workflow Runtime Cutover +date: 2026-06-23 +status: planned +--- + +# Fix Workflow Runtime Cutover + +## Problem + +The workflow graph and workflow-column runtime paths are being made default, but the first cutover review found that the new dispatch path is not yet equivalent to the legacy scheduler/executor invariants. The work must move the cutover onto an isolated branch and make the new path safe before opening a PR. + +## Requirements + +- R1: Keep unrelated dashboard/cosmetic changes out of the workflow cutover branch. +- R2: The workflow hold/release scheduler path must preserve dispatch safety: dependency, mission, filesystem/spec, pause, lease, node-routing, permanent-agent, overlap, oscillation, `maxWorktrees`, `maxConcurrent`, and semaphore behavior. +- R3: `TaskExecutor.execute()` must prove the graph-default entrypoint preserves legacy recovery behavior, including inner executor requeues and mismatched store-row protection. +- R4: The gate must be self-contained: every test referenced by `packages/engine/vitest.config.ts` must be tracked and committed. +- R5: Legacy workflow flags should not remain user-facing experimental kill switches, but stale persisted values must be tolerated. +- R6: Remove or neutralize unreachable legacy scheduler dispatch code so future fixes do not land in dead paths. +- R7: Validate with lint, typecheck, build, gate, and targeted engine tests before PR. + +## Implementation Units + +### U1. Isolate Branch State + +Files: +- `packages/dashboard/app/components/ScriptsModal.css` +- `packages/dashboard/app/components/__tests__/ScheduledTasksModal.test.tsx` +- `docs/plans/2026-06-23-001-fix-workflow-runtime-cutover-plan.md` + +Approach: +- Commit the dashboard/cosmetic automations spacing changes on `main`. +- Preserve workflow cutover work on a dedicated branch for review and rollback. +- Ensure `main` is not left carrying uncommitted workflow cutover edits. + +Tests: +- `pnpm --filter @fusion/dashboard exec vitest run app/components/__tests__/ScheduledTasksModal.test.tsx` + +### U2. Scheduler Dispatch Equivalence + +Files: +- `packages/engine/src/scheduler.ts` +- `packages/engine/src/hold-release.ts` +- `packages/engine/src/__tests__/scheduler-workflow-cutover.test.ts` +- `packages/engine/vitest.config.ts` + +Approach: +- Move all live pre-dispatch gates into the workflow hold/release reservation path or a shared helper used by that path. +- Fix capacity ordering so no task is marked starting or status-cleared until all reservation checks pass. +- Preserve `maxConcurrent` and shared semaphore semantics without double-acquiring the executor semaphore. +- Make the replacement gate test tracked and broad enough to cover the migrated invariants. + +Tests: +- `pnpm --filter @fusion/engine exec vitest run src/__tests__/scheduler-workflow-cutover.test.ts` +- `pnpm --filter @fusion/engine test:core` + +### U3. Executor Graph Entry And Recovery + +Files: +- `packages/engine/src/executor.ts` +- `packages/engine/src/__tests__/workflow-graph-task-runner.test.ts` +- Targeted executor tests under `packages/engine/src/__tests__/` + +Approach: +- Ensure graph execution preserves the original dispatched task identity. +- Fix graph failure handling so inner executor self-heal/requeue is not overwritten by outer graph parking. +- Ensure graph `prepareWorktree` does not pre-acquire or pass the repo root as a task worktree. +- Restore direct `TaskExecutor.execute()` coverage for default-on graph behavior and recovery semantics. + +Tests: +- Focused executor recovery/worktree/liveness tests affected by graph-default behavior. +- `pnpm --filter @fusion/engine test:core` + +### U4. Remove Dead Legacy Dispatch Surface + +Files: +- `packages/engine/src/scheduler.ts` +- `packages/engine/vitest.config.ts` + +Approach: +- After U2 coverage is in place, remove unreachable legacy todo dispatcher code or reduce it to any still-needed shared helpers. +- Keep reporter emission and non-dispatch scheduler duties intact. + +Tests: +- `pnpm --filter @fusion/engine typecheck` +- `pnpm --filter @fusion/engine test:core` + +## Verification + +- `pnpm lint` +- `pnpm typecheck` +- `pnpm test` +- `pnpm build` +- `compound-engineering:ce-code-review mode:agent plan:docs/plans/2026-06-23-001-fix-workflow-runtime-cutover-plan.md` + +## Risks + +- The workflow path is central engine infrastructure; green gate alone is not enough if broad affected tests still show executor/scheduler invariant regressions. +- Semaphore handling must avoid both failure modes found in review: bypassing capacity entirely and double-acquiring before the executor can run. diff --git a/docs/plans/2026-06-27-001-feat-pg-satellite-store-ports-plan.md b/docs/plans/2026-06-27-001-feat-pg-satellite-store-ports-plan.md new file mode 100644 index 0000000000..918a4a65a8 --- /dev/null +++ b/docs/plans/2026-06-27-001-feat-pg-satellite-store-ports-plan.md @@ -0,0 +1,291 @@ +--- +title: "feat: Port remaining satellite stores to PostgreSQL AsyncDataLayer" +status: completed +date: 2026-06-27 +type: feat +branch: feature/postgres +--- + +# feat: Port remaining satellite stores to PostgreSQL AsyncDataLayer + +## Summary + +The embedded-PostgreSQL backend is the default local store, but several satellite stores were never ported off the synchronous SQLite path. Their getters throw `" is not available in PG backend mode"`, so the dashboard features 500 (now interim-503-guarded). This plan ports the remaining stores so their dashboard surfaces work against real Postgres, sequenced cleanest-first as independent per-store units: **workflow definitions → mailbox (MessageStore) → InsightStore → ResearchStore → MissionStore**. TodoStore (already shipped this session) is the reference pattern. + +Each unit lands as its own commit, removes the interim 503/throw for that store, and adds a `*.pg.test.ts` to the blocking `test:pg-gate` lane. Mission autopilot/SSE and CLI-only paths may remain partial — only the dashboard read/write surface is in scope per store. + +--- + +## Problem Frame + +In PG backend mode (`store.backendMode === true`), the synchronous `store.db` getter throws. Satellite-store getters (`getInsightStoreImpl`/`getResearchStoreImpl` in `packages/core/src/task-store/remaining-ops-10.ts`, `getMissionStoreImpl` in `packages/core/src/task-store/remaining-ops-8.ts`) construct their store with `store.db` and therefore throw. Dashboard routes were given interim `503` guards this session; `/api/workflows` still 500s because `readAllWorkflowDefinitionsImpl` does a raw `store.db.prepare("SELECT * FROM workflows")`. + +Each store already has a partial `async-*-store.ts` helper module targeting the existing `project.*` Postgres tables, plus a shared PG test harness (`packages/core/src/__test-utils__/pg-test-harness.ts`). The work is: fill helper gaps (faithfully replicating stateful lifecycle logic), wrap them in an async store exposing the sync method names, return that wrapper from the getter in backend mode, convert the dashboard routes (and any unconditional non-fallback consumers) to `await`, and remove the interim guard. + +--- + +## Requirements + +- **R1.** Each ported store's dashboard routes return HTTP 200 with real data against a live embedded-PG instance (no 500/503). +- **R2.** The server stays up — no uncaught throws from store getters or async misuse on engine/SSE paths. +- **R3.** `test:pg-gate` stays green and gains one `*.pg.test.ts` per ported store covering the dashboard-critical methods, including lifecycle/state-machine behavior where present. +- **R4.** Stateful logic (status-transition validation, terminal-immutability, auto-timestamps, retry gates, fingerprint dedup, auto-seq) is replicated faithfully — the PG path must match the SQLite path's observable semantics. +- **R5.** Legacy SQLite mode is unaffected — the sync store remains the path when `!store.backendMode`. +- **R6.** Interim 503 guards added this session are removed for each store as it is genuinely ported. +- **R7.** Consumers that already wrap the getter in try/catch graceful fallback are left as-is; only unconditional sync consumers reachable in PG mode are converted to `await`. + +--- + +## Key Technical Decisions + +- **KTD1 — Async-wrapper-class pattern (mirror TodoStore).** For each store, add an `Async` class (in the existing `async-*-store.ts`) that holds the `AsyncDataLayer` and exposes the **same public method names** as the sync store, delegating to the module's helper functions. The getter returns it in backend mode; the sync class stays for SQLite. Consumers `await` the result (harmless on sync returns). Rationale: proven this session with `AsyncTodoStore`; keeps a single call path across both backends. +- **KTD2 — Getter return type becomes a union.** `getStore(): | Async`; the cached field widens to ` | Async | null`. Callers `await`. Rationale: avoids forcing the sync store to become async and avoids an interface extraction. +- **KTD3 — Replicate lifecycle logic in the new helpers, not the routes.** `updateInsightRun`/`updateResearchRun`/`updateResearchStatus`/`createResearchRetryRun` carry the state machines (`VALID_*_TRANSITIONS`, `TERMINAL_*_STATUSES`, auto-timestamps, retry gate). The async helpers must reproduce these checks and throw the same `*LifecycleError` types. Rationale: R4; the routes already assume the store enforces invariants. +- **KTD4 — Leave engine/CLI graceful-fallback consumers untouched; convert only unconditional reachable ones.** Insight's 3 engine reporters and Research's `project-engine` orchestrator init already try/catch — leave them (they degrade). Convert dashboard routes always; convert CLI/agent-tools calls that are unconditional and async-reachable. Rationale: R7, bounds blast radius. +- **KTD5 — Sequence cleanest-first, one commit per store.** Workflows (1 method, 0 consumer changes) → Mailbox (mostly wired) → Insight (6 helpers, engine on fallback) → Research (12 helpers + machines + ~24 consumers) → Mission (71 helpers, 54 route methods, partial). Rationale: ship value early, isolate risk, keep each PR reviewable. +- **KTD6 — Raw async SQL must schema-qualify `project.*` and use snake_case columns.** Per this session's earlier fixes (the connection does not put `project` on `search_path`). New helpers go through Drizzle schema objects (auto-qualified) where possible; raw `sql` must qualify. Rationale: avoids the `relation does not exist` / wrong-column class already fixed once. + +--- + +## High-Level Technical Design + +Per-store porting pipeline (applies to U3–U5; U1/U2 are reduced cases): + +```mermaid +flowchart TD + A[Sync store method surface] --> B{Async helper exists?} + B -- yes --> D[Async<Store> wrapper method delegates to helper] + B -- no --> C[Write async helper: replicate lifecycle logic faithfully] + C --> D + D --> E[get<Store>StoreImpl: backendMode ? new Async<Store>(layer) : new Sync(db)] + E --> F[Dashboard routes: await store methods; remove 503 guard] + E --> G{Other consumer} + G -- try/catch fallback --> H[Leave as-is] + G -- unconditional + async-reachable --> I[Convert to await] + F --> J[pg-gate test via shared harness] + I --> J + H --> J +``` + +Store complexity ranking (drives sequence): + +```mermaid +graph LR + W[Workflows: 1 method, 0 consumers] --> M[Mailbox: dual-path already wired] + M --> I[Insight: 6 helpers, engine fallback] + I --> R[Research: 12 helpers + 2 state machines + ~24 consumers] + R --> MI[Mission: 71 helpers, 54 route methods, autopilot partial] +``` + +--- + +## Implementation Units + +### U1. Port workflow-definitions read to the async layer + +**Goal:** `/api/workflows` returns 200 in PG mode (lists builtin + custom workflow definitions). + +**Requirements:** R1, R2, R5, R6. + +**Dependencies:** none. + +**Files:** +- `packages/core/src/task-store/remaining-ops-8.ts` (`readAllWorkflowDefinitionsImpl`) +- `packages/core/src/async-workflow-store.ts` *(new, or add a helper to an existing async module)* — `listWorkflowDefinitions(layer)` reading `project.workflows` +- `packages/core/src/__tests__/postgres/workflow-definitions.pg.test.ts` *(new)* +- `packages/core/package.json` (`test:pg-gate` list) + +**Approach:** `readAllWorkflowDefinitionsImpl` is the ONLY sync method in the workflow-definition read path — every caller (`register-workflow-routes.ts`, `board-workflows.ts`, `executor.ts`, `agent-tools.ts`, CLI) already `await`s `listWorkflowDefinitions`/`getWorkflowDefinition`. Add a `store.backendMode` branch that reads rows from `project.workflows` (ordered `created_at ASC`) via the async layer and maps them to `WorkflowDefinition` exactly as the sync branch does (parse `ir`/`layout` jsonb, default `kind`). Builtins are merged from code constants downstream — unchanged. No consumer conversion needed. + +**Patterns to follow:** `AsyncTodoStore` row-mapping; the existing sync row→definition mapping in `readAllWorkflowDefinitionsImpl`; schema object `schema.project.workflows`. + +**Test scenarios:** +- Happy path: seed two custom workflows via the layer; `store.listWorkflowDefinitions()` in backend mode returns them plus enabled builtins, ordered by `createdAt`. +- Empty: no custom rows → returns builtins only, no throw. +- `kind` filter: `listWorkflowDefinitions({ kind })` filters correctly. +- jsonb round-trip: `ir`/`layout` parse back to the stored object shape. +- Covers R1: `GET /api/workflows` handler resolves (integration-level via the store method). + +**Verification:** `GET /api/workflows` → 200 with builtin workflows on a fresh embedded-PG instance; custom workflow created via API then listed; `test:pg-gate` green. + +--- + +### U2. Close the mailbox (MessageStore) PG gap + +**Goal:** Mailbox/chat-send routes work in PG mode (the reported "mailbox send error" is gone). + +**Requirements:** R1, R2, R4, R5. + +**Dependencies:** none. + +**Files:** +- `packages/core/src/message-store.ts` (the `isBackendMode()` branches) +- `packages/core/src/async-message-store.ts` (only if a helper is missing) +- `packages/dashboard/src/routes/register-messaging-scripts.ts` / `register-chat-room-routes.ts` (verify await; no expected change) +- `packages/core/src/__tests__/postgres/message-store.pg.test.ts` *(new or extend satellite coverage)* +- `packages/core/package.json` (`test:pg-gate`) + +**Approach:** MessageStore is already engine-runtime-owned with **dual-path construction** — `in-process-runtime.ts` builds it with `{ asyncLayer }` in PG mode, the class branches on `isBackendMode()`, and the 11 async helpers exist; consumers already `await`. This is NOT a full port — it is gap-closure. **Execution note:** Start by reproducing the exact mailbox-send failure against a live embedded-PG instance and capture the error; the fix is whichever specific `MessageStore` method still routes to `this.db` (or an unimplemented `isBackendMode()` branch) on the send path. Wire that one method through the matching async helper. + +**Patterns to follow:** existing `isBackendMode()` branches in `message-store.ts`; `async-message-store.ts` `sendMessage`/`getConversation` helpers. + +**Test scenarios:** +- Happy path: `sendMessage` → `getMailbox`/`getConversation` round-trip in backend mode returns the sent message. +- Read state: `markAsRead`/`markAllAsRead` then `getUnreadAgentToAgentCount` reflects the change. +- Edge: empty inbox returns `[]`, no throw. +- Covers R1: the mailbox send route path returns 200. + +**Verification:** Reproduce-then-confirm: the captured send error no longer occurs; mailbox round-trip via API returns 200; `test:pg-gate` green. + +--- + +### U3. Port InsightStore + +**Goal:** Insights dashboard (list, runs, run events, cancel, retry, CRUD) works in PG mode. + +**Requirements:** R1–R7. + +**Dependencies:** none (independent of U1/U2). + +**Files:** +- `packages/core/src/async-insight-store.ts` — add 6 helpers + `AsyncInsightStore` class +- `packages/core/src/task-store/remaining-ops-10.ts` (`getInsightStoreImpl`) +- `packages/core/src/store.ts` (`insightStore` field type, `getInsightStore()` return type, import) +- `packages/dashboard/src/insights-routes.ts` (await calls; remove 503 guard at ~L326–333) +- `packages/cli/src/extension.ts` (4 insight tool calls → await) +- `packages/core/src/__tests__/postgres/insight-store.pg.test.ts` *(new)* +- `packages/core/package.json` (`test:pg-gate`) + +**Approach:** Write the missing async helpers: `updateInsight`, `updateInsightRun` (**replicate** terminal-immutable check, `VALID_RUN_STATUS_TRANSITIONS`, auto-`completedAt`/`cancelledAt`, `run:completed` semantics), `listInsightRunEvents`, `countInsights`, `listStalePendingRuns`, `countInsightRuns`. Build `AsyncInsightStore` exposing sync names (`getInsight`, `listInsights`, `upsertInsight`, `updateInsight`, `deleteInsight`, `createRun`, `getRun`, `listRuns`, `updateRun`, `findActiveRun`, `appendRunEvent`, `listRunEvents`, `countInsights`, `countRuns`) delegating to helpers; generate ids/timestamps in the wrapper where the sync store does. Wire `getInsightStoreImpl` (backend → `AsyncInsightStore(getAsyncLayer())`). Convert `insights-routes.ts` handlers to `await` and delete the 503 guard. Convert the 4 unconditional CLI tool calls in `extension.ts` to `await`. **Leave** the 3 engine reporters (`backlog-pressure`/`dependency-blocked-todo`/`unlinked-missions`) on their existing try/catch fallback. + +**Patterns to follow:** `AsyncTodoStore` (`getTodoStoreImpl` union return, store.ts field widening); the sync `updateRun` lifecycle block in `insight-store.ts` (lines ~537–626) is the spec for `updateInsightRun`. + +**Test scenarios:** +- Happy path: `createRun` → `appendRunEvent` → `listRunEvents` (auto-seq 1,2,3) → `updateRun` to `completed` sets `completedAt`. +- Lifecycle: updating a terminal run throws `InsightLifecycleError("terminal_immutable")`; an invalid status transition throws `invalid_transition`. +- Dedup: `upsertInsight` twice with the same `(projectId, fingerprint)` updates in place (same id, preserved `createdAt`). +- Counts: `countInsights`/`listInsights` agree on a filtered set; `findActiveRun` returns the pending/running run. +- Edge: `getInsight`/`getRun` on a missing id → `undefined`; empty list → `[]`. +- Covers R1: `GET /api/insights` and `GET /api/insights/runs` return 200 with seeded data. + +**Verification:** `/api/insights`, `/api/insights/runs`, run-events, cancel, retry all 200 on a live embedded-PG instance; create→cancel→verify terminal; server survives; 503 guard gone; `test:pg-gate` green (incl. lifecycle assertions). + +--- + +### U4. Port ResearchStore + +**Goal:** Research dashboard (runs CRUD, events, sources, results, status machine, exports, retry, search, stats) works in PG mode. + +**Requirements:** R1–R7. + +**Dependencies:** none (independent), but sequence after U3 — it reuses the same wrapper/lifecycle pattern and is the highest-friction store. + +**Files:** +- `packages/core/src/async-research-store.ts` — add 12 helpers + `AsyncResearchStore` class +- `packages/core/src/task-store/remaining-ops-10.ts` (`getResearchStoreImpl`) +- `packages/core/src/store.ts` (`researchStore` field/return type/import) +- `packages/dashboard/src/research-routes.ts` (await; remove 503 at ~L157–161) +- `packages/dashboard/src/sse.ts` (research subscription — already optional-chained this session; upgrade to real subscription if the async store exposes events, else leave optional) +- `packages/engine/src/agent-tools.ts` (5 research calls → await) +- `packages/cli/src/extension.ts` (research tool calls → await), `packages/cli/src/commands/research.ts` (async-ify handlers) +- `packages/core/src/__tests__/postgres/research-store.pg.test.ts` *(new)* +- `packages/core/package.json` (`test:pg-gate`) + +**Approach:** Write 12 helpers: `updateResearchRun`, `listResearchRuns`, `deleteResearchRun`, `appendResearchEvent` (**dual-write**: `run.events` jsonb array + `research_run_events` table), `addResearchSource`, `updateResearchSource`, `setResearchResults`, `updateResearchStatus` (**replicate the full lifecycle machine** — status validation, per-status auto-lifecycle fields, auto-timestamps, lifecycle-event append, status-changed/completed/failed/cancelled/timed_out semantics), `requestResearchCancellation`, `createResearchRetryRun` (**replicate retry gate + lineage**: source must be failed/timed_out, attempt cap → `retry_exhausted`+`not_retryable`, `rootRunId`/`retryOfRunId`), `searchResearchRuns`, `getResearchExport`. Compose via `persistResearchRun` where the sync store does (source/results/status mutate-then-persist). Build `AsyncResearchStore` with all ~23 route method names; wire `getResearchStoreImpl`; convert `research-routes.ts` (await + remove 503). Convert `agent-tools.ts` (5) and CLI (`extension.ts` + `research.ts`) unconditional calls to await; **leave** `project-engine.ts` orchestrator init on its try/catch fallback. + +**Execution note:** Implement `updateResearchStatus` and `createResearchRetryRun` test-first against the sync semantics — they are the riskiest (the lifecycle machine spans `research-store.ts` ~377–448 and retry ~570–625). + +**Patterns to follow:** U3's `AsyncInsightStore`; the sync `updateStatus`/`createRetryRun` blocks in `research-store.ts` are the spec; `appendResearchRunEvent` helper for the table side of the dual-write. + +**Test scenarios:** +- Happy path: `createRun` (status `queued`) → `updateStatus("running")` sets `startedAt` → `updateStatus("completed")` sets `completedAt` + `retryable=false`. +- Lifecycle machine: each status sets its documented auto-lifecycle fields; invalid transition throws `ResearchLifecycleError`; terminal run is immutable for non-event fields; `"pending"` normalizes to `"queued"`. +- Retry gate: retry on a `failed` run within cap creates a lineage-linked `retry_waiting` run; exceeding cap sets source `retry_exhausted` and throws `not_retryable`; retry on non-failed throws. +- Dual-write events: `appendResearchEvent` appears in both `getRun().events` and `listRunEvents`. +- Sources/results: `addSource`/`updateSource`/`setResults` round-trip via `getRun`. +- Search/stats/exports: `searchRuns` matches query/topic/summary; `getStats` groups by status; `createExport`→`getExports`→`getExport` round-trip. +- Covers R1: `GET /api/research/runs`, `PATCH /runs/:id/status`, retry, search all 200. + +**Verification:** Full research route surface 200 on live embedded-PG; a queued→running→completed run with events/sources/results persists and reloads; retry produces a lineage child; server survives; 503 gone; `test:pg-gate` green (machine + retry assertions). + +--- + +### U5. Port MissionStore (dashboard surface; autopilot/SSE partial) + +**Goal:** Missions dashboard (list/summaries/health, mission+milestone+slice+feature CRUD, reorder, links, contract assertions, validator runs) works in PG mode. Autopilot and SSE mission events may remain disabled. + +**Requirements:** R1–R7 (scoped to the dashboard surface). + +**Dependencies:** U3, U4 (reuses the established wrapper/lifecycle pattern; largest surface, do last). + +**Files:** +- `packages/core/src/async-mission-store.ts` — add `AsyncMissionStore` class over the existing 71 helpers; write any helper gaps for the 54 route methods (e.g. `getMissionWithHierarchy`, `listMissionsWithSummaries`, health rollups, `computeMissionStatus`, interview-state, `triageFeature`, `activateSlice`, `findNextPendingSlice`, `backfillFeatureAssertions` — confirm coverage during implementation) +- `packages/core/src/task-store/remaining-ops-8.ts` (`getMissionStoreImpl`) +- `packages/core/src/store.ts` (`missionStore` field/return type/import) +- `packages/dashboard/src/mission-routes.ts` (await; remove 503 at ~L308), `packages/dashboard/src/goals-routes.ts` (remove mission 503 at ~L57; goal→mission routes) +- `packages/cli/src/extension.ts` (~13 mission tool calls → await) +- `packages/core/src/__tests__/postgres/mission-store.pg.test.ts` *(new)* +- `packages/core/package.json` (`test:pg-gate`) + +**Approach:** The 71 helpers cover the entity surface (missions/milestones/slices/features/events/goal-links/assertions/validator-runs/lineage). Build `AsyncMissionStore` exposing the 54 route method names; many are composites (`getMissionWithHierarchy` = mission + milestones + slices + features assembled; `listMissionsWithSummaries` = missions + counts; health rollups = event/validator aggregation) — assemble these in the wrapper from helper reads, mirroring the sync store's composition. Wire `getMissionStoreImpl`; convert `mission-routes.ts` + `goals-routes.ts` (await + remove guards); convert the ~13 unconditional CLI mission calls in `extension.ts`. **Explicitly leave partial:** engine autopilot (`in-process-runtime.ts` try/catch fallback) and SSE mission events (`sse.ts`) — document that mission autopilot/live-SSE stay disabled in PG mode; only request/response dashboard reads/writes are in scope. + +**Execution note:** Confirm helper coverage for the composite/health/interview/triage methods before wiring; write missing helpers faithfully (reorder ordering, interview-state transitions, validator-run staleness). Given the surface, consider splitting U5 into read-surface (list/get/hierarchy/health) and write-surface (CRUD/reorder/links/validators) commits if it aids review. + +**Patterns to follow:** U3/U4 wrappers; existing `async-mission-store.ts` helper signatures; the sync `mission-store.ts` composition for hierarchy/summaries/health. + +**Test scenarios:** +- Happy path: create mission → add milestone → add slice → add feature; `getMissionWithHierarchy` returns the assembled tree; `listMissionsWithSummaries` returns counts. +- Reorder: `reorderMilestones`/`reorderSlices` produce the new order deterministically. +- Links: `linkGoal`/`unlinkGoal` and `listGoalIdsForMission` round-trip; `linkFeatureToTask`/`unlinkFeatureFromTask`. +- Assertions/validators: `addContractAssertion`→`listContractAssertions`; `startValidatorRun`→`getValidatorRunsByFeature`. +- Health/status: `computeMissionStatus`/`getMissionHealth` reflect feature/validator state. +- Edge: empty mission list → `[]`; missing mission → `undefined`/404. +- Covers R1: `GET /api/missions`, `GET /api/missions/:id` (hierarchy), goal→mission routes all 200. + +**Verification:** Mission list + a created mission with full hierarchy 200 on live embedded-PG; reorder/link/assertion/validator round-trips; server survives; 503 guards gone from mission + goals routes; `test:pg-gate` green. Autopilot/SSE-partial documented. + +--- + +## Scope Boundaries + +**In scope:** Dashboard request/response surfaces for workflow defs, mailbox, Insight, Research, Mission, against embedded/external Postgres; the async helpers and wrappers they require; PG-gate test coverage; removal of the interim 503 guards. + +### Deferred to Follow-Up Work +- **GoalStore full port.** Goals routes keep their interim 503 (GoalStore has ~10 sync CLI consumers — `extension.ts`, `commands/mission.ts` — that would need async conversion). Out of this plan's dashboard-surface scope; `async-goal-store.ts` exists for a later unit. +- **Mission autopilot + live SSE mission events in PG mode.** Engine autopilot and `sse.ts` mission subscriptions stay on graceful fallback (U5 ships read/write dashboard only). +- **CLI command full async-ification beyond what each unit's reachable tool calls require** (e.g. `commands/research.ts` sync handler conversion is included only as needed for U4; broader CLI parity is follow-up). +- **`listStalePendingRuns` background sweepers** beyond providing the helper (wiring the sweeper to the async path if it isn't already). +- **SSE live-refresh events from the async wrappers** (Todo/Insight/Research/Mission async stores do not emit store events; UI updates land on next read). Matches the documented TodoStore gap. + +--- + +## System-Wide Impact + +- **`store.ts` getter signatures widen to unions** (` | Async`) for insight/research/mission (todo already done). Only dashboard routes and the listed CLI calls consume these; engine consumers are on fallback. TypeScript will surface any missed sync consumer at compile time. +- **Engine/CLI behavior in PG mode:** reporters and orchestrator inits continue to degrade gracefully (unchanged); converted CLI tool calls begin actually working in PG mode. +- **Test gate grows** by one `*.pg.test.ts` per store; gate runtime increases modestly (shared harness, per-test DB). + +--- + +## Risks & Dependencies + +- **R-RISK1 — Lifecycle-logic drift (high).** `updateInsightRun`, `updateResearchStatus`, `createResearchRetryRun` reimplement state machines; a subtle divergence corrupts run state silently. *Mitigation:* test-first against the sync spec; assert transition rejections and auto-field population explicitly (R4 scenarios). +- **R-RISK2 — Missed sync consumer breaks at runtime, not compile (medium).** A consumer using the union store without `await` gets a `Promise` where it expects a value. *Mitigation:* the union return type makes most misuse a type error; grep each getter's callers per unit; the engine/CLI categorization (convert-vs-fallback) is enumerated in the porting maps. +- **R-RISK3 — Raw SQL schema-qualification regression (medium).** New helpers must qualify `project.*`/snake_case (KTD6). *Mitigation:* go through Drizzle schema objects; reuse the harness; the earlier `deployments`/`agent_runs` fixes are the cautionary precedent. +- **R-RISK4 — Mission surface underestimation (medium).** 54 route methods incl. composites; some helpers may be missing despite the 71-count. *Mitigation:* confirm coverage before wiring; allow U5 read/write split. +- **Dependency:** Live verification needs a fresh embedded-PG instance (force-killing the cluster corrupts `postmaster.pid` → wipe `~/.fusion/embedded-postgres` before relaunch — observed this session). Docker `postgres:15` on a non-5432 port for `test:pg-gate` (a local Postgres already holds 5432). + +--- + +## Verification Strategy + +Per unit: (1) `test:pg-gate` green with the new `*.pg.test.ts` (run against Docker `postgres:15`); (2) build, launch the sandboxed embedded-PG dashboard, hit the store's routes and confirm 200 with real data; (3) confirm the server survives past startup (no uncaught throw); (4) confirm the interim 503/throw is gone for that store. The pipeline's browser test (`ce-test-browser`) exercises the dashboard surfaces end-to-end. + +--- + +## Sources & Research + +- Reference port (this session): TodoStore — `AsyncTodoStore` in `packages/core/src/async-todo-store.ts`, `getTodoStoreImpl` in `remaining-ops-10.ts`, `todo-store.pg.test.ts` in `test:pg-gate`. +- Porting maps (two reconnaissance sub-agents, 2026-06-27): per-store sync API, async-helper coverage gaps, dashboard route method usage, consumer convert-vs-fallback categorization, and PG schema confirmation for Insight/Research/Mission/Message/workflows. +- Prior session fixes informing KTD6: schema-qualification of `project.deployments`/`project.incidents`/`project.agent_runs`/`project.experiment_session_records`. +- Shared PG test harness: `packages/core/src/__test-utils__/pg-test-harness.ts` (`createSharedPgTaskStoreTestHarness`, `pgDescribe`). diff --git a/docs/postgres-migration-review-2026-06-26.md b/docs/postgres-migration-review-2026-06-26.md new file mode 100644 index 0000000000..f049d3be32 --- /dev/null +++ b/docs/postgres-migration-review-2026-06-26.md @@ -0,0 +1,149 @@ +# Code Review — SQLite → PostgreSQL Storage Migration + +**Date:** 2026-06-26 +**Branch:** `feature/postgres` reviewed against `origin/main` (merge-base `7d13f880b`) +**HEAD:** `387cec1a7` — `feat: migrate storage from SQLite to PostgreSQL (squash)` +**Reviewers:** 13 persona agents (ce-code-review multi-agent pipeline) + learnings researcher + deployment verification +**Run artifacts:** `/tmp/compound-engineering/ce-code-review/20260626-084137-41a91d02/` (per-reviewer JSON) +**Plan:** `docs/plans/2026-06-23-001-feat-migrate-sqlite-to-postgres-plan.md` + +--- + +## Scope + +- 714 files changed, **+64,470 / −173,769**. +- **42,858 lines** of new code under `packages/core/src/postgres/`, `packages/core/src/task-store/` (63 files), and 19 `async-*` satellite stores. +- **388 deleted test files** (167 core, 123 dashboard, 73 engine, plugins); 53 new `__tests__/postgres/*.pg.test.ts` added; `scripts/lib/test-quarantine.json` +175 lines. + +## Verdict: **NOT READY TO MERGE** + +A well-architected migration that honors the plan's design (R1–R12 are all honored in *design*), but as a single 42k-line squash it ships with **7 P0 and ~27 P1 findings**. Three structural facts dominate: + +1. **The async rewrite repeatedly dropped guards the sync path still enforces.** Soft-delete write-conflict guards, handoff atomicity, and — most severely — entire merge-critical store methods were never given a `backendMode` branch, so they **throw on every merge in the default embedded-PG backend**. +2. **The tests that protected those invariants were deleted, and the new PG tests do not run in CI.** No Postgres service is provisioned and the skip logic is inverted, so 42k lines of new data-layer code is effectively uncovered. This is the FN-5893 "deleted the repro, kept the bug" failure mode. +3. **This is a mid-migration (dual-path) branch, not post-cutover.** Both SQLite and Postgres paths are live behind **289 `backendMode` branches**; R11 (SQLite removed) is intentionally incomplete. The unguarded methods below are un-migrated leftovers of an incomplete flip. + +The backup subsystem is independently broken three ways in the default embedded mode, and there is no first-class migration entry point. + +--- + +## P0 — Critical (must fix before merge) + +| # | File:Line | Issue | Reviewer(s) | Conf | +|---|-----------|-------|-------------|------| +| 1 | `task-store/remaining-ops-6.ts:441` | **`getActiveMergingTask` throws in PG mode.** Calls `store.db.prepare(...)` with no `backendMode` guard; the `db` getter throws *"SQLite Database is not available in backend mode"*. The merge concurrency guard (callers `merger.ts:9755`, `project-engine.ts:2247`) fails on every merge. **Verified.** | api-contract | 100 | +| 2 | `task-store/remaining-ops-6.ts:818` | **`upsertMergeRequestRecord` throws in PG mode** — unguarded `store.db`. Callers `merger.ts:8466`, `executor.ts:1970`, `self-healing.ts:828`, `project-engine.ts:1866`. Method must become async + all callsites awaited. | api-contract | 100 | +| 3 | `task-store/remaining-ops-6.ts:845` | **`transitionMergeRequestState` throws in PG mode** — unguarded `store.db`. ~12 callers in `merger.ts`/`project-engine.ts`. The merge state machine cannot advance. | api-contract | 100 | +| 4 | `.github/workflows/full-suite.yml` · `__test-utils__/pg-test-harness.ts:81` | **New PG tests don't run in CI.** No `postgres` service is provisioned and `PG_AVAILABLE` is always truthy (`PG_TEST_URL_BASE` defaults non-empty, `FUSION_PG_TEST_SKIP` never set), so the 57 `pgDescribe` suites fail with `ECONNREFUSED` or are dead. 42k lines of new data-layer code has no integration coverage in CI. | testing | 100 | +| 5 | `postgres/pg-backup.ts:261` | **`pg_dump` connects to the wrong DB.** Connection string passed via `PG_CONNECTION_STRING` — not a libpq variable. With no `--dbname`/`PG*` vars it hits the system default (localhost:5432, current user); in embedded mode (random port) backups fail or target an empty DB. The FNXC comment documents the (good) intent but the env var is non-functional. **Verified.** | reliability | 100 | +| 6 | `postgres/pg-backup.ts:302` | Same gap for **`pg_restore`** — restore targets the wrong server. **Verified.** | reliability | 100 | +| 7 | `task-store/remaining-ops-1.ts:132` | **Soft-delete resurrection.** The `backendMode` branch of `atomicWriteTaskJsonWithAudit` blind-upserts the row with no `deletedAt` re-read and no `throwSoftDeletedWriteBlocked` — the guard the sync branch has (lines 144-167). A write to / racing a soft-deleted task silently resurrects it (R7 / VAL-DATA-005/006). **Verified the guard is absent.** | adversarial (corrob. correctness, learnings, testing) | 75 | + +> Note: #1–#3 and #7 are the same root cause as the structural P1 below (#13) — an incomplete sync→async flip — manifesting as hard runtime failures and data-integrity regressions on critical paths. + +--- + +## P1 — High + +### Unguarded `store.db` on async-converted paths (all throw in PG mode, confidence 100, `api-contract`) +| # | File:Line | Method / impact | +|---|-----------|-----------------| +| 8 | `task-store/remaining-ops-2.ts:438` | `renewCheckoutLeaseImpl` — checkout lease renewal throws; silently escalates to checkout expiry during active execution. | +| 9 | `task-store/remaining-ops-2.ts:871` | `registerArtifactImpl` — preliminary taskId check at :871 sits *outside* the `register()` guard at :890; throws whenever `input.taskId` is set. | +| 10 | `task-store/remaining-ops-6.ts:618, :662, :699` · `remaining-ops-2.ts:489, :509` · `workflow-ops.ts:24` | Workflow settings read/write (×6) + workflow-step creation — engine agent-tools and dashboard workflow/settings routes throw in PG mode. | +| 11 | `task-store/remaining-ops-6.ts:460` | `findRecentTasksByContentFingerprint` — unguarded **and** uses SQLite-only `json_extract(...)`; near-duplicate intake breaks. | + +### Other P1 +| # | File:Line | Issue | Reviewer(s) | Conf | +|---|-----------|-------|-------------|------| +| 12 | `task-store/moves.ts:187, :702` | **Handoff-to-review atomicity broken.** `createCompletionHandoffWorkflowWork` runs its workflow-work cancel/upsert in their own fresh-pool transactions, not the outer handoff `tx`; an outer rollback leaves committed workflow-work / orphaned merge-gate rows (R7 mergeQueue invariant). Pool-exhaustion deadlock risk via nested `transactionImmediate` (`workflow-workitems-ops-2.ts:20`). | correctness | 75 | +| 13 | `store.ts` (289 sites) | **The flip never completed.** 19 `async-*` stores added *alongside* unchanged sync stores with 289 `backendMode` branches; `agent-store.ts` (3202 L), `mission-store.ts` (4390 L), `central-core.ts` (4374 L) carry both paths. Every feature written twice; the SQLite-fallback path (`in-process-runtime.ts:239`, `asyncLayer` null) runs untested. Root cause of #1–#3, #7–#11. | maintainability (corrob. correctness, testing) | 100 | +| 14 | `postgres/sqlite-migrator.ts:369` | **Migration data-corruption risk.** `resolveColumnMapping` joins `information_schema.columns` by column name only (no table predicate); `data` is `text` in `archived_tasks` but `jsonb` in 5+ tables → nondeterministic type classification → batch aborts on `::jsonb` mismatch. Fixtures pass, prod fails. | data-migration | 75 | +| 15 | `postgres/sqlite-migrator.ts:596` | **Content-blind verification.** `targetRows >= sourceRows` with `ON CONFLICT DO NOTHING` cannot detect under-migration or content divergence on re-run; reports `verified` regardless. | data-migration + adversarial (agree) | 100 | +| 16 | `dashboard/routes/register-signal-routes.ts:222` | `resolveIncident()` became async but the caller was not updated — **floating Promise**, incident-resolution errors silently dropped. | api-contract | 100 | +| 17 | `dashboard/monitor-store.ts:170` | **Broken backend discriminator.** `'transactionImmediate' in db` always routes SQLite `Database` instances (which also expose `transactionImmediate`, `db.ts:5746`) to the async path → `resolveIncidentAsync` runs with a `DatabaseSync` as the Drizzle arg. | api-contract | 75 | +| 18 | `postgres/migrations/0000_initial.sql:1436` | **Missing index on `source_parent_task_id`** → the lineage gate (`findLiveLineageChildren`/`removeLineageReferences`, run on every archive/delete) is a full `tasks`-table scan. | performance | 100 | +| 19 | `task-store/async-merge-coordination.ts:255` | **N+1 in merge-queue lease acquire** — 2 round-trips per stale row inside the tx, on every merge attempt (20 stale rows = 40 sequential round-trips before the first lease). | performance | 100 | +| 20 | `task-store/async-audit.ts:120, :252` | **`LIMIT` applied in JS, not SQL** — audit/activity queries pull the entire matching set then `.slice()`; `activity_log` has no rotation. | performance | 100 | +| 21 | `task-store/async-persistence.ts:280` | `readLiveTaskRows` does an unbounded `SELECT * FROM tasks WHERE deleted_at IS NULL` (80+ cols, jsonb) on every board hydration — MB/request over the wire. | performance | 100 | +| 22 | `postgres/credential-redact.ts:39` | Redaction misses `?password=` query-param URLs; logged verbatim by `DatabaseConnectionError`/`describeBackendForLog`. | security | 75 | +| 23 | `postgres/embedded-lifecycle.ts:414` | SIGTERM/SIGINT handler `await this.stop()` but never re-raises → process hangs alive until SIGKILL after the cluster stops. | reliability | 100 | +| 24 | `postgres/startup-factory.ts:292` | No timeout on `embeddedLifecycle.start()` — a stalled `initdb`/`pg_ctl` hangs startup forever. | reliability | 75 | +| 25 | `postgres/pg-backup.ts:130` | Partial backup not cleaned up — central dump failure orphans the project dump; `listBackups()` counts it as a pair, skewing retention. | reliability | 75 | +| 26 | `postgres/pg-backup.ts` (packaging) | **Backup broken end-to-end in embedded mode**: `pg_dump`/`pg_restore` not bundled with `@embedded-postgres/*` (only `initdb`/`pg_ctl`/`postgres`); `BackupManager` also throws standalone because the embedded URL resolves only at daemon start. Compounds #5/#6. | deployment + agent-native (agree) | 100 | +| 27 | `cli/src/commands/db.ts` | **No `fn db migrate` command and no auto-migrate at startup.** First boot on the new embedded-PG default produces an *empty database*; existing SQLite data is invisible until a hand-written script runs `migrateSqliteToPostgres`. Silent data-loss trap. | agent-native + deployment (agree) | 100 | +| 28 | `__tests__/postgres/create-task-reserved-id.pg.test.ts` | `TombstonedTaskResurrectionError` (FN-5208/FN-5233, an AGENTS.md repeat-regression incident) has zero PG coverage; 13 engine reliability-interaction tests + `soft-delete-stickiness-FN-5233.test.ts` deleted (they used the removed `inMemoryDb` option, not deleted code). This is the test that would catch #7. | testing | 100 | +| 29 | `async-central-core.ts:1424+` | FNXC gap: 1789-line file, 3 FNXC comments; the concurrency-slot + mesh-state sections (the "important technical decisions" AGENTS.md requires marked) are unmarked. | project-standards | 75 | +| 30 | `task-store/remaining-ops-1.ts`…`-10.ts` | `remaining-ops-1..10` (~9000 L) are explicitly un-categorized overflow modules (mixed domains, several >1000 L); `lifecycle-ops.ts` is a new 1241-line file mixing DB open, FS watching, and settings migration. | maintainability | 100 | + +--- + +## P2 — Moderate + +- `moves.ts:626` — soft-delete guard also missing on `moveTaskInternal` backend path (sibling of #7). *(adversarial, 50)* +- `moves.ts:629` — WIP capacity limit overrun: two concurrent backend moves into one slot both commit under READ COMMITTED. *(adversarial, 50)* +- `task-store/audit-ops.ts:59` — `taskRow as unknown as TaskDetail` **bypasses deserialization**; hook consumers get raw JSON-string columns. *(maintainability, 100)* +- `postgres/connection.ts:46` — default pool `max=10` may starve under `maxWorktrees`-level concurrent `transactionImmediate` holders. *(performance, 75)* +- `postgres/postgres-health.ts:329` — `healSchemaDrift` `catch {}` swallows ALTER TABLE errors silently. *(reliability, 100; safe_auto)* +- `postgres-health.ts:354/389` — `validateAndHealSchema` ALTER and `vacuumAnalyze` VACUUM run on the runtime pool, not the migration connection → fail under a transaction-mode pooler. +- `sqlite-migrator.ts:471` — empty-string → NULL for `jsonb`; `NOT NULL jsonb` columns (`data`/`ir`/`step_ids`) abort the batch on legacy `''` rows. *(data-migration)* +- `__test-utils__/pg-test-harness.ts:128` — `execSync('psql …')` violates the AGENTS.md execSync ban (not git plumbing; no timeout → can hang the vitest worker). *(project-standards)* +- `0000_initial.sql:1425` — no partial index for the hot `WHERE deleted_at IS NULL AND column = ?` kanban read (forces bitmap-AND). *(performance)* +- **9 quarantine entries are migration-caused mock drift, not flakes** (CE orchestrator, desktop `local-server`, dashboard `research-api`) — AGENTS.md forbids quarantining tests that fail *because of* the change; 14-day deletion clock expires **2026-07-09**. *(testing)* +- `index.ts` — `detectLegacyData`/`migrateFromLegacy`/`getMigrationStatus` removed from the `@fusion/core` public index with no deprecation; `dist/index.d.ts` still referenced them. *(api-contract)* +- `store.ts:389` / `plugin-store.ts:130` — `inMemoryDb` constructor option removed from `TaskStore`/`PluginStore` → TypeScript compile break for any external/plugin caller. +- `.changeset/embedded-postgres-lifecycle.md` — freeform body, missing `summary:`/`category:`/`dev:` (gate warns; `--strict` fails). *(project-standards; safe_auto)* + +## P3 — Low +- `.returning()` would collapse insert-then-select double round-trips (`async-branch-groups.ts:120`, `async-monitor.ts:203`, …). *(safe_auto)* +- `searchTasks*` return unbounded result sets with no default cap (`async-search.ts:159`). *(safe_auto)* +- Repeated `as unknown as Record` settings casts (`settings-ops.ts:63`). +- `flip-embedded-pg-default.md` filed `minor`/`feature` — a default-backend swap is arguably `major`/`breaking`. + +--- + +## Learnings & Past Solutions (all honored in design, at risk in execution) + +- **`docs/soft-delete-verification-matrix.md`** — the acceptance contract for R7. Findings #7, #28 are direct hits; re-run the matrix GREEN against the async store before cutover. +- **`docs/solutions/database-issues/schema-version-constant-must-equal-highest-migration.md`** — carry the version-gate discipline to the Drizzle journal; add a *seed-at-previous-state* upgrade test (not fresh-DB only). +- **`docs/solutions/database-issues/task-field-silently-dropped-without-sqlite-column-mapping.md`** — round-trip every `Task` field through `updateTask→getTask→reopen` (the `audit-ops.ts:59` cast is this risk realized). +- **`docs/solutions/integration-issues/engine-already-running-is-not-no-engine.md`** — the `taskClaims` two-write lease release must keep `BEGIN IMMEDIATE`-equivalent isolation (`SELECT FOR UPDATE`/serializable), not drift to plain READ COMMITTED. +- **`docs/solutions/test-failures/schema-version-sweep-must-include-plugin-workspaces.md`** — sweep plugin version pins from repo root after the first Drizzle migration bump; the roadmap plugin's snake_case vs camelCase column mismatch (api-contract residual) is unaudited. + +--- + +## Deployment — Go/No-Go (blocking items) + +1. No `fn db migrate` CLI (#27). +2. No automated pre-migration SQLite backup (operator must manually `cp` `fusion.db`, `archive.db`, `fusion-central.db`). +3. `pg_dump`/`pg_restore` not bundled (#26). +4. No auto-migrate → empty-DB-on-first-boot data-loss for naive upgraders. + +The full checklist (pre-migration baseline row-count queries, dry-run, FTS parity spot-check, post-migrate verification, rollback via `FUSION_NO_EMBEDDED_PG=1`, 24h monitoring of pool/process/disk) is in the deployment-verification agent output under the run artifact directory. + +--- + +## Residual Risks + +- Embedded mode hard-codes superuser password `"password"` (local-only, 127.0.0.1 + random port — parity with prior local SQLite trust; consider a random per-instance password at 0600). +- Fixed `project`/`central`/`archive` schema names → two projects sharing one external `DATABASE_URL` clobber each other (no isolation). +- `tsvector GENERATED ALWAYS AS STORED` adds write amplification on every unrelated task update (heartbeat/timing writes recompute the vector). +- No `DATABASE_URL` format validation (`backend-resolver.ts:92`) — malformed URL fails only at connect. +- `pgRowToTaskRow` shim re-serializes parsed jsonb back to strings for `fromJson()`; any new async path skipping it feeds parsed objects to `JSON.parse` → `'[object Object]'` garbage (not enumerated across all helpers). + +## Coverage + +- Confidence gate: no findings suppressed below anchor 75 except retained P0@75 (#7); ~4 testing/maintainability P2/P3 advisory items demoted to soft buckets. +- All 13 reviewers returned results; 0 failures/timeouts. +- Testing gaps: no concurrency tests for the atomicity/lost-update paths (#7, #12, WIP); no perf benchmark for the N+1 hot paths at realistic volume; migrator untested for cross-table type collision, non-superuser FK-order fallback, pre-populated-target verification, and jsonb round-trip. + +--- + +## Suggested Fix Order + +1. **Restore the safety net:** #4 (provision Postgres in CI + fix `PG_AVAILABLE` probe) and #28 (rescue the deleted invariant tests) — so everything below is verifiable. +2. **Unblock the default backend:** #1, #2, #3 and the #8–#11 unguarded `store.db` methods — complete the `backendMode` branches (this is finding #13, the incomplete flip). +3. **Data-integrity guards:** #7, #12, #14, #15. +4. **Backup / lifecycle:** #5, #6, #23, #25, #26, #27. +5. **Performance:** #18, #19, #20, #21. +6. **Standards / structure:** #16, #17, #22, #29, #30. diff --git a/docs/screenshots/artifacts-doc-edit.png b/docs/screenshots/artifacts-doc-edit.png new file mode 100644 index 0000000000..356b0a2ac0 --- /dev/null +++ b/docs/screenshots/artifacts-doc-edit.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1cf752e605f935e58a6f87d24744a61329d2fb8efdaea58ad0af4bd60744111d +size 146601 diff --git a/docs/screenshots/artifacts-doc-viewer.png b/docs/screenshots/artifacts-doc-viewer.png new file mode 100644 index 0000000000..076eb369df --- /dev/null +++ b/docs/screenshots/artifacts-doc-viewer.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04f7a1c82605eba16e0900eb2c01d00122e10084d63e71f8e0a5cfa47ca789e2 +size 142244 diff --git a/docs/screenshots/artifacts-gallery.png b/docs/screenshots/artifacts-gallery.png new file mode 100644 index 0000000000..9c72782021 --- /dev/null +++ b/docs/screenshots/artifacts-gallery.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3e5594d78abad19040108f444067126f069594fa171981a5558b00f611024ea +size 128061 diff --git a/docs/screenshots/artifacts-html-preview.png b/docs/screenshots/artifacts-html-preview.png new file mode 100644 index 0000000000..d18a348be2 --- /dev/null +++ b/docs/screenshots/artifacts-html-preview.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a7fbf7a7e9f1ded015fcc022337f1f97215199ce26a23c004bfa0a691a5dcb5 +size 95961 diff --git a/docs/screenshots/artifacts-lightbox.png b/docs/screenshots/artifacts-lightbox.png new file mode 100644 index 0000000000..444af1a1fe --- /dev/null +++ b/docs/screenshots/artifacts-lightbox.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a546fd75b6102dc29a40c49cd56409481f782034833da283d35ca41ed426ccf +size 93188 diff --git a/docs/screenshots/artifacts-mobile.png b/docs/screenshots/artifacts-mobile.png new file mode 100644 index 0000000000..75a4261fde --- /dev/null +++ b/docs/screenshots/artifacts-mobile.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0f9ddf5a9194d206874a15421a69207cc80d0e2158ddf7310ae8f9ed28df613 +size 55378 diff --git a/docs/screenshots/artifacts-pdf-viewer.png b/docs/screenshots/artifacts-pdf-viewer.png new file mode 100644 index 0000000000..bb531b2bc0 --- /dev/null +++ b/docs/screenshots/artifacts-pdf-viewer.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7edd139230f97b412daf186659bd6352d4e2980a47cadd6dc6aa62d5dbacc2b1 +size 96273 diff --git a/docs/screenshots/artifacts-video-lightbox.png b/docs/screenshots/artifacts-video-lightbox.png new file mode 100644 index 0000000000..f804e9c23b --- /dev/null +++ b/docs/screenshots/artifacts-video-lightbox.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49fee3eee4a8eea0f7e55760717dd05f31fe6397197ee6192112fb27f8721a60 +size 135050 diff --git a/docs/screenshots/compound-engineering/pipeline-desktop.png b/docs/screenshots/compound-engineering/pipeline-desktop.png new file mode 100644 index 0000000000..f13368fc77 --- /dev/null +++ b/docs/screenshots/compound-engineering/pipeline-desktop.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06d35e52a88966234de0aef50cc471a474a80422789cfaba5e3b8133f189e2d8 +size 251273 diff --git a/docs/screenshots/compound-engineering/pipeline-mobile.png b/docs/screenshots/compound-engineering/pipeline-mobile.png new file mode 100644 index 0000000000..1eaa719035 --- /dev/null +++ b/docs/screenshots/compound-engineering/pipeline-mobile.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ddba7235bdf54679910eae60d9716ea631fec8ef5e0a8be651e197fca243225 +size 98300 diff --git a/docs/screenshots/settings-advanced/desktop-advanced.png b/docs/screenshots/settings-advanced/desktop-advanced.png new file mode 100644 index 0000000000..d00a690059 --- /dev/null +++ b/docs/screenshots/settings-advanced/desktop-advanced.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12c082b2882338073e152ed0b71a18f012b10e62093c1159929ff75bb266c503 +size 183641 diff --git a/docs/screenshots/settings-advanced/desktop-basic.png b/docs/screenshots/settings-advanced/desktop-basic.png new file mode 100644 index 0000000000..50e9ceb182 --- /dev/null +++ b/docs/screenshots/settings-advanced/desktop-basic.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7289f83cbb6a864ab22238f38afdf0ee51535926e8d00c4cfcbea2b2cb1d66a4 +size 169853 diff --git a/docs/screenshots/settings-advanced/mobile-advanced.png b/docs/screenshots/settings-advanced/mobile-advanced.png new file mode 100644 index 0000000000..063d284c70 --- /dev/null +++ b/docs/screenshots/settings-advanced/mobile-advanced.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c45abb7912a0c30e3602fb63bdc8c4ffae33beb2f59f506b3c2f406a5060f13f +size 233362 diff --git a/docs/screenshots/settings-advanced/mobile-basic.png b/docs/screenshots/settings-advanced/mobile-basic.png new file mode 100644 index 0000000000..30932d6e87 --- /dev/null +++ b/docs/screenshots/settings-advanced/mobile-basic.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ddc6e921529039b61239a771d4ba5426fcffbc803f24ece719d22d4825a2fe1f +size 233374 diff --git a/docs/screenshots/settings-advanced/sections/desktop-advanced-appearance.png b/docs/screenshots/settings-advanced/sections/desktop-advanced-appearance.png new file mode 100644 index 0000000000..55ff29c20c --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/desktop-advanced-appearance.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a04396e38a65f6017f660bca82dfbf5f9724c8488939b158f07763cc0ef29e21 +size 94336 diff --git a/docs/screenshots/settings-advanced/sections/desktop-advanced-general.png b/docs/screenshots/settings-advanced/sections/desktop-advanced-general.png new file mode 100644 index 0000000000..0e375b44a9 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/desktop-advanced-general.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e53726d071495cdaf4b24bfabee555bf9462e3eac8f0ff26eb2279a48bf1d42 +size 107406 diff --git a/docs/screenshots/settings-advanced/sections/desktop-advanced-global-general.png b/docs/screenshots/settings-advanced/sections/desktop-advanced-global-general.png new file mode 100644 index 0000000000..fb3ea856d4 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/desktop-advanced-global-general.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e01b09743bc68b7fd2f33d6c7c977987270abc19ef1daae7764668675f2656d4 +size 138436 diff --git a/docs/screenshots/settings-advanced/sections/desktop-advanced-global-models.png b/docs/screenshots/settings-advanced/sections/desktop-advanced-global-models.png new file mode 100644 index 0000000000..6a5417aad6 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/desktop-advanced-global-models.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22087aaa57a929e81068718c977b71a9873b9839cbfcf14f9e2524ef3b4a6705 +size 131699 diff --git a/docs/screenshots/settings-advanced/sections/desktop-advanced-merge.png b/docs/screenshots/settings-advanced/sections/desktop-advanced-merge.png new file mode 100644 index 0000000000..c9878d12e8 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/desktop-advanced-merge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0402d3f1d8d02b329a3154f69d341607ad4ee7b34a26fd52177228fc76d11a4 +size 134893 diff --git a/docs/screenshots/settings-advanced/sections/desktop-advanced-notifications.png b/docs/screenshots/settings-advanced/sections/desktop-advanced-notifications.png new file mode 100644 index 0000000000..57f4bde0c6 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/desktop-advanced-notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eefe585ba1b1fe2f1acd107548e9be6476a1947de70751c44e8e5c0fed870cd5 +size 124381 diff --git a/docs/screenshots/settings-advanced/sections/desktop-advanced-project-models.png b/docs/screenshots/settings-advanced/sections/desktop-advanced-project-models.png new file mode 100644 index 0000000000..e22e21cccb --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/desktop-advanced-project-models.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6f2a8921fee63f767fe42483318b7241611e01b0665481af48bf08e36bee13f7 +size 165485 diff --git a/docs/screenshots/settings-advanced/sections/desktop-advanced-scheduling.png b/docs/screenshots/settings-advanced/sections/desktop-advanced-scheduling.png new file mode 100644 index 0000000000..261f713466 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/desktop-advanced-scheduling.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dfd06c5bf1e8b0e7fc51f8a8dc6fed9a5c1a34fdb079cd7b81ebc226b25d508e +size 120267 diff --git a/docs/screenshots/settings-advanced/sections/desktop-advanced-worktrees.png b/docs/screenshots/settings-advanced/sections/desktop-advanced-worktrees.png new file mode 100644 index 0000000000..1dcc494812 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/desktop-advanced-worktrees.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80611bf16a243ce6fcd4676d8b0c99dee75d17895344a7b5de11533b32bb29dd +size 152834 diff --git a/docs/screenshots/settings-advanced/sections/desktop-basic-appearance.png b/docs/screenshots/settings-advanced/sections/desktop-basic-appearance.png new file mode 100644 index 0000000000..dfa36bfc0c --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/desktop-basic-appearance.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37ef3746ae5c571c43c8ee47e8028131505b849cc3e6966f5dcf8bbf53db9d18 +size 88056 diff --git a/docs/screenshots/settings-advanced/sections/desktop-basic-general.png b/docs/screenshots/settings-advanced/sections/desktop-basic-general.png new file mode 100644 index 0000000000..e2922dca47 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/desktop-basic-general.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1716f6e65c0843b38f570464d7401c93ca46f95671ed2c25a9563527530ff754 +size 101616 diff --git a/docs/screenshots/settings-advanced/sections/desktop-basic-global-general.png b/docs/screenshots/settings-advanced/sections/desktop-basic-global-general.png new file mode 100644 index 0000000000..54add7fd77 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/desktop-basic-global-general.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e810d99687139383a6b3ef4509e3ab1e9833f079b6fbc40f25ddf59537c0d4a +size 124578 diff --git a/docs/screenshots/settings-advanced/sections/desktop-basic-global-models.png b/docs/screenshots/settings-advanced/sections/desktop-basic-global-models.png new file mode 100644 index 0000000000..411834c952 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/desktop-basic-global-models.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3bbd53114ad3f59b9dec97c7b557654027b2d1e7aa002d5d309d1099189821f +size 125657 diff --git a/docs/screenshots/settings-advanced/sections/desktop-basic-merge.png b/docs/screenshots/settings-advanced/sections/desktop-basic-merge.png new file mode 100644 index 0000000000..de51e48895 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/desktop-basic-merge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8735ab3dbabb0d4ff2af949c49a090734eb2381ae788c19dfdc40fa39ade1515 +size 124466 diff --git a/docs/screenshots/settings-advanced/sections/desktop-basic-notifications.png b/docs/screenshots/settings-advanced/sections/desktop-basic-notifications.png new file mode 100644 index 0000000000..c16c735884 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/desktop-basic-notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38675052555d74b5c65aa5af93233ea735a3e3bc42b0c5e9f98c089e1b2f1275 +size 118465 diff --git a/docs/screenshots/settings-advanced/sections/desktop-basic-project-models.png b/docs/screenshots/settings-advanced/sections/desktop-basic-project-models.png new file mode 100644 index 0000000000..e78e1912f5 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/desktop-basic-project-models.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d27385b968f4d757c0f4171e03a171144269fff7abbf641e550343292436693 +size 159137 diff --git a/docs/screenshots/settings-advanced/sections/desktop-basic-scheduling.png b/docs/screenshots/settings-advanced/sections/desktop-basic-scheduling.png new file mode 100644 index 0000000000..6d947605a1 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/desktop-basic-scheduling.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0eeb8728d506efeab3809e3128da241b34877f9900ce957635848a9c0c1e1add +size 126849 diff --git a/docs/screenshots/settings-advanced/sections/desktop-basic-worktrees.png b/docs/screenshots/settings-advanced/sections/desktop-basic-worktrees.png new file mode 100644 index 0000000000..a45e3d45b7 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/desktop-basic-worktrees.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c2f09cdc31046c196521bc5795a6f35523b9379978d9c930c6c46c08266ee7f +size 108473 diff --git a/docs/screenshots/settings-advanced/sections/mobile-advanced-appearance.png b/docs/screenshots/settings-advanced/sections/mobile-advanced-appearance.png new file mode 100644 index 0000000000..ac00fde1f2 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/mobile-advanced-appearance.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20f7d4b3c8ff9dd053d5d4d4539230adec83dbb66e6767adf57eb43e9287891d +size 123278 diff --git a/docs/screenshots/settings-advanced/sections/mobile-advanced-general.png b/docs/screenshots/settings-advanced/sections/mobile-advanced-general.png new file mode 100644 index 0000000000..11aabae225 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/mobile-advanced-general.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f918640b65b5b6beb73e4640001743a1781477ba574f1b7281758786a10ad9e4 +size 171614 diff --git a/docs/screenshots/settings-advanced/sections/mobile-advanced-global-general.png b/docs/screenshots/settings-advanced/sections/mobile-advanced-global-general.png new file mode 100644 index 0000000000..cad458ab8d --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/mobile-advanced-global-general.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c11dac99a107f2329f1541d61c307ddcf2e9f830f665f1feddcab3da00461f5 +size 194862 diff --git a/docs/screenshots/settings-advanced/sections/mobile-advanced-global-models.png b/docs/screenshots/settings-advanced/sections/mobile-advanced-global-models.png new file mode 100644 index 0000000000..bf0ceab88e --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/mobile-advanced-global-models.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68e8e37002f3892f9539142ddb89c05fe73bda2f511dfa8466f26810d502eb46 +size 211497 diff --git a/docs/screenshots/settings-advanced/sections/mobile-advanced-merge.png b/docs/screenshots/settings-advanced/sections/mobile-advanced-merge.png new file mode 100644 index 0000000000..7324d38e58 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/mobile-advanced-merge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d6d3c8f6936f83becdd3d06363d7de1fd915ce425eae8fd7d1782c4ea9b0ae3 +size 204443 diff --git a/docs/screenshots/settings-advanced/sections/mobile-advanced-notifications.png b/docs/screenshots/settings-advanced/sections/mobile-advanced-notifications.png new file mode 100644 index 0000000000..64f885b86d --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/mobile-advanced-notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f7acc16a71c886bbc5b0584cfe158c56458788935535ee2e58451a343ad841e +size 188354 diff --git a/docs/screenshots/settings-advanced/sections/mobile-advanced-project-models.png b/docs/screenshots/settings-advanced/sections/mobile-advanced-project-models.png new file mode 100644 index 0000000000..d22ae42db5 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/mobile-advanced-project-models.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7a753edc173648db83af62df461ee3d52269c5852ca632ee8080f6be7b09aed +size 247748 diff --git a/docs/screenshots/settings-advanced/sections/mobile-advanced-scheduling.png b/docs/screenshots/settings-advanced/sections/mobile-advanced-scheduling.png new file mode 100644 index 0000000000..1c67058650 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/mobile-advanced-scheduling.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a5f3c0ef73df24812cdc15f7d315d31e63272377c4c0310501785df73c40adf +size 170224 diff --git a/docs/screenshots/settings-advanced/sections/mobile-advanced-worktrees.png b/docs/screenshots/settings-advanced/sections/mobile-advanced-worktrees.png new file mode 100644 index 0000000000..1a7cda5e80 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/mobile-advanced-worktrees.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:005b3c638dd1384fac783f88a0fd7d7563507e1f564bdb33cc4a07a13e4f06ba +size 220766 diff --git a/docs/screenshots/settings-advanced/sections/mobile-basic-appearance.png b/docs/screenshots/settings-advanced/sections/mobile-basic-appearance.png new file mode 100644 index 0000000000..757f3adbb2 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/mobile-basic-appearance.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4985432762bc12542a8fdc3c5704e16518fd10201bced86b78dc11f1029db602 +size 123186 diff --git a/docs/screenshots/settings-advanced/sections/mobile-basic-general.png b/docs/screenshots/settings-advanced/sections/mobile-basic-general.png new file mode 100644 index 0000000000..8a97127628 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/mobile-basic-general.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56218ab5ac0262ebc92158ffafa2028565b99b340e2e014aeb684141af626bdd +size 171533 diff --git a/docs/screenshots/settings-advanced/sections/mobile-basic-global-general.png b/docs/screenshots/settings-advanced/sections/mobile-basic-global-general.png new file mode 100644 index 0000000000..3356bb418f --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/mobile-basic-global-general.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85d07da69d3b172b37186c157fab5ba2a13706eceb2a17f694a5c386bbb76ef0 +size 194898 diff --git a/docs/screenshots/settings-advanced/sections/mobile-basic-global-models.png b/docs/screenshots/settings-advanced/sections/mobile-basic-global-models.png new file mode 100644 index 0000000000..e94f4fc250 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/mobile-basic-global-models.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be187aec01a38ab23ab71d3a9334bab39bec823468ab14bbbc39f777fdcab28a +size 211484 diff --git a/docs/screenshots/settings-advanced/sections/mobile-basic-merge.png b/docs/screenshots/settings-advanced/sections/mobile-basic-merge.png new file mode 100644 index 0000000000..1ba8f44e77 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/mobile-basic-merge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a60f1d32fe537c9de049b840747854247cb82be40c8a11af798b12f7b01a951 +size 199685 diff --git a/docs/screenshots/settings-advanced/sections/mobile-basic-notifications.png b/docs/screenshots/settings-advanced/sections/mobile-basic-notifications.png new file mode 100644 index 0000000000..70449753b8 --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/mobile-basic-notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d599befc59e68183d530905440945bf59df29209d49713ffd49d57d9f0d2b080 +size 188528 diff --git a/docs/screenshots/settings-advanced/sections/mobile-basic-project-models.png b/docs/screenshots/settings-advanced/sections/mobile-basic-project-models.png new file mode 100644 index 0000000000..429e857e2a --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/mobile-basic-project-models.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bd56a6b889118186e6446055d34da83d1655390aa575684d9b661eff6539018 +size 247678 diff --git a/docs/screenshots/settings-advanced/sections/mobile-basic-scheduling.png b/docs/screenshots/settings-advanced/sections/mobile-basic-scheduling.png new file mode 100644 index 0000000000..4c7332681d --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/mobile-basic-scheduling.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d73c6ccfb86888b54b862fdb0e57aa4f2d94e5db9de062e7263e931e2e747a3 +size 170259 diff --git a/docs/screenshots/settings-advanced/sections/mobile-basic-worktrees.png b/docs/screenshots/settings-advanced/sections/mobile-basic-worktrees.png new file mode 100644 index 0000000000..ed1850e44b --- /dev/null +++ b/docs/screenshots/settings-advanced/sections/mobile-basic-worktrees.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9c1ef1a2ea7ad087e2fe153610a1282c3aa2616118c4e72662cf80496a43e43 +size 233380 diff --git a/docs/self-healing-backward-move-audit.md b/docs/self-healing-backward-move-audit.md index aef06faec6..67c8a13e9e 100644 --- a/docs/self-healing-backward-move-audit.md +++ b/docs/self-healing-backward-move-audit.md @@ -55,7 +55,7 @@ Stages that cannot satisfy all three must either (a) tighten predicate to requir | recoverOrphanedAgents | 6180 | dead parent/direct-report linkage | n/a | org topology checks | pause/delete/reparent decisions | RECONCILE-ONLY | keep | n/a | n/a | | recoverStaleHeartbeatRuns | 6372 | stale heartbeat run records | run age thresholds | pid/age mismatch | terminate run records | RECONCILE-ONLY | keep | n/a | n/a | | recoverNoProgressNoTaskDoneFailures | 6451 | in-progress failed no-task-done no progress | implicit (no explicit grace) | no-step-progress + no git work + not executing | clear metadata + move to todo | BACKWARD | tighten | triple proof + no-progress checks + recent liveness-audit absence | gate move; emit `task:no-progress-no-task-done-no-action` | -| recoverMissingWorktreeReviewFailures | 6516 | in-review failed session-start missing/unusable worktree | classifier-based | error classifier proof only | autoRecover requeue to todo | BACKWARD | tighten | triple proof + classifier proof | gate requeue; emit `task:missing-worktree-review-no-action` | +| recoverMissingWorktreeReviewFailures | 6516 | in-review failed OR merge-active (`merging`/`merging-pr`/`merging-fix`) session-start missing/unusable worktree | classifier-based | error classifier proof only | autoRecover requeue to todo | BACKWARD | tightened | triple proof + classifier proof + `allowsAutoMergeProcessing` + workspace-task exclusion | gate requeue; emit `task:missing-worktree-review-no-action` or `task:reconcile-missing-worktree-merge-active-no-action`; successful merge-active recovery clears `worktree`/`branch`/`sessionFile`, resets the worktree-session retry budget, increments `recoveryRetryCount` as the bounded stale-metadata clear counter, emits `task:reconcile-missing-worktree-merge-active`, and requeues to todo preserving progress | | recoverPartialProgressNoTaskDoneFailures | 6586 | in-review failed no-task-done with partial progress | bounded by `MAX_TASK_DONE_RETRIES` | no-task-done + partial progress + retry budget | clear error + move to todo preserveProgress | BACKWARD | tighten | triple proof + retry-budget predicates | gate move; emit `task:partial-progress-no-task-done-no-action` | | recoverApprovedTriageTasks | 6706 | triage planning specified stale | `APPROVED_TRIAGE_RECOVERY_GRACE_MS` | planning idle + valid PROMPT.md | recoverApprovedTriageTask callback | FORWARD | keep | n/a | n/a | | recoverStarvedRefinementTriageTasks | 6827 | refinement planning stale | `STARVED_REFINEMENT_RECOVERY_GRACE_MS` | no progress idle | requeue/annotation in triage | RECONCILE-ONLY | keep | n/a | n/a | @@ -89,7 +89,8 @@ Stages that cannot satisfy all three must either (a) tighten predicate to requir ### recoverMissingWorktreeReviewFailures - Existing classifier proof is necessary but not sufficient for backward movement. -- Tighten: require full triple-proof before auto requeue. +- Tightened: require full triple-proof before auto requeue, and keep `allowsAutoMergeProcessing` as the first lifecycle gate so `autoMerge:false` remains terminal-until-merged. +- Merge-active rows (`merging`, `merging-pr`, `merging-fix`) with the same unusable-worktree session-start signature are now owned here before interrupted/deadlocked merge sweeps can re-drive the phantom path. Workspace tasks are annotation-only in this path; per-repo land recovery remains workspace-owned. ### recoverPartialProgressNoTaskDoneFailures - Retry-budget alone can still reopen tasks under active/residual execution races. diff --git a/docs/settings-reference.md b/docs/settings-reference.md index d42b1dfae2..77f2fb19c2 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -106,7 +106,7 @@ Fusion automatically falls back to ntfy's JSON publish format when a notificatio | `gitlabAuthTokenType` | `"personal" \| "project" \| "group"` | `undefined` (effective `"personal"` when a token exists) | Global fallback GitLab token family label for operator clarity. Project tokens and group tokens remain limited to their associated project/group and role membership; this label does not expand authorization. Unsupported values are rejected by the GitLab auth resolver. | | `autoReloadOnVersionChange` | `boolean` | `true` | When enabled (default), the dashboard automatically reloads when a new build version is detected via `/version.json` polling or service worker activation. Set to `false` to suppress automatic reloads — the user must manually refresh to pick up updates. | | `modelOnboardingComplete` | `boolean` | `undefined` | Whether AI onboarding has been completed or dismissed. | -| `useCursorCli` | `boolean` | `undefined` | Enables the `cursor-cli` provider in model pickers after Cursor CLI status validation. Toggle from Settings → Authentication. | +| `useCursorCli` | `boolean` | `undefined` | Enables the `cursor-cli` provider in model pickers after Cursor CLI status validation. Toggle from Settings → Authentication. This runtime auth is OAuth/session-based; Cursor usage metering is separate and reads a Cursor Admin API key from the dashboard process `CURSOR_API_KEY` env var. | | `cursorCliBinaryPath` | `string` | `undefined` | Optional global, machine-local Cursor CLI executable override used by Settings → Authentication, status/enable validation, probes, and model discovery. Leave unset/blank to auto-detect `cursor-agent` then `cursor` on PATH. Use this when PATH points at the wrong Cursor install or Windows exposes a specific `.cmd`/`.bat` shim; invalid non-empty saves are rejected with bounded diagnostics. | | `useGrokCli` | `boolean` | `undefined` | Enables the `grok-cli` provider in model pickers after Grok CLI status validation. Toggle from Settings → Authentication. Grok's direct xAI endpoint uses API-key auth (`GROK_API_KEY` env var or `~/.grok/user-settings.json` `apiKey`); when a `grok-cli/*` execution model has no Fusion-visible key, Fusion falls back to the `grok` CLI runtime if registered so the CLI can use its own auth store. | | `grokCliBinaryPath` | `string` | `undefined` | Optional global, machine-local Grok CLI executable override used by Settings → Authentication, status/enable validation, probes, and model discovery. Leave unset/blank to auto-detect `grok` on PATH. Invalid non-empty saves are rejected with bounded diagnostics. | @@ -508,6 +508,7 @@ When `pushAfterMerge` is enabled, a completed direct merge first runs `git pull | `showWorktreeGrouping` | `boolean` | `false` | Default: off. When off, WIP/processing columns render plain task cards without worktree group shells or worktree-name labels in both legacy and workflow-mode boards. When on, every WIP/processing column groups tasks by worktree and shows worktree names, including workflow-mode columns flagged as counting toward WIP. | | `openTasksInRightSidebar` | `boolean` | `false` | Default: off. When off, board task-card clicks keep the existing full-panel task detail that replaces the board. When on and the right dock is active on desktop/tablet, board task-card clicks open the task detail in the right sidebar so the board stays visible; mobile or hidden/inactive right-dock states automatically fall back to the full-panel behavior. Non-board task-open paths, including list split detail, floating pop-outs, graph/plugin opens, and deep `changes`/`retries`/`workflow` opens, keep their existing behavior; ordinary right-dock Tasks-list cards are governed by `openMobileTasksInPopup` first and otherwise use embedded dock detail. | | `openMobileTasksInPopup` | `boolean` | `false` | Default: off. When off, ordinary board task-card clicks keep the existing fallback behavior: the full-panel task detail, or the right dock when `openTasksInRightSidebar` is on and the dock is active; ordinary right-dock Tasks-list clicks open embedded dock detail with the normal back-to-list controls. When on, ordinary board task-card clicks and right-dock Tasks-list clicks open the task in the existing task popup/FloatingWindow surface on desktop, tablet, and mobile so the board or dock list remains visible; this popup route takes precedence over right-dock routing for those ordinary clicks. Desktop/tablet task popups restore the last saved popup size and position across task IDs and use the board/task-detail layer rather than the global utility layer, while their Activity dropdown stays above and attached during popup drag/resize; mobile task popups remain full-screen sheets. Deep `changes`/`retries`/`workflow` opens, list/detail opens outside the right-dock Tasks list, graph/plugin opens, nested task-detail opens, and explicit pop-out actions keep their existing behavior. | +| `showCostBadgeOnCards` | `boolean` | `false` | Default: off. When enabled from Settings → Appearance, board cards with positive recorded token usage show a read-time derived model-cost badge beside the execution-time badge. Unpriced models display `—`, and tasks with no token usage render no badge shell. | | `executorAllowSiblingBranchRename` | `boolean` | `false` | Opt back into the legacy executor behavior that silently allocates sibling branches (`fusion/-2`, `-2-2`, …) when the canonical task branch is already checked out elsewhere. When disabled (default), branch conflicts fail loudly and leave the task in `todo` with `status: "failed"` so operators can resolve conflicting branches/worktrees with git tooling before retrying. See [Task Management → Branch conflict handling](./task-management.md#branch-conflict-handling). The dashboard Settings modal exposes the same toggle with warning copy because this legacy mode is discouraged. | | `worktreeNaming` | `"random" \| "task-id" \| "task-title"` | `"random"` | Naming mode for new worktree directories. | @@ -777,13 +778,13 @@ Recovery entrypoints in the dashboard: ### OAuth credential refresh -Fusion automatically refreshes Claude/Anthropic OAuth credentials before reporting auth status when the stored OAuth credential includes a refresh token and the access token is expired or within the refresh buffer. For Anthropic, this status path is the `anthropic-subscription` surface (including legacy `anthropic` OAuth rows), not Claude CLI state. A successful refresh updates auth storage and prevents `oauth-token-expired` notifications or startup warnings for that provider, so users usually do not need manual re-login after the initial Claude OAuth login. +Fusion automatically refreshes OAuth credentials before reporting auth status when the stored credential includes a refresh token and the access token is expired or within the refresh buffer. For Anthropic, this status path is the `anthropic-subscription` surface (including legacy `anthropic` OAuth rows), not Claude CLI state. A successful refresh updates auth storage and prevents `oauth-token-expired` notifications or startup warnings for that provider, so users usually do not need manual re-login after the initial OAuth login. -Manual re-login is still required when no refresh token is stored, the refresh request fails, or the expired OAuth credential belongs to a non-Anthropic provider. In those cases the credential remains expired, `oauth-token-expired` notifications/startup warnings may fire subject to their 12-hour provider throttle, and users should re-authenticate from **Settings → Authentication** or Model Onboarding. The top-level dashboard re-login banner suppresses only the urgent **Anthropic Subscription** entry when **Anthropic API Key** or **Anthropic — via Claude CLI** is already authenticated, so the banner does not imply all Anthropic agent execution is blocked; Settings still shows the subscription OAuth state as expired/not connected until it is refreshed or re-logged-in. +Manual re-login is still required when no refresh token is stored or the refresh request fails/leaves the credential expired. In those cases the credential remains expired, `oauth-token-expired` notifications/startup warnings may fire subject to their 12-hour provider throttle, and users should re-authenticate from **Settings → Authentication** or Model Onboarding. The top-level dashboard re-login banner suppresses only the urgent **Anthropic Subscription** entry when **Anthropic API Key** or **Anthropic — via Claude CLI** is already authenticated, so the banner does not imply all Anthropic agent execution is blocked; Settings still shows the subscription OAuth state as expired/not connected until it is refreshed or re-logged-in. - + ### Anthropic API-key authentication @@ -990,7 +991,11 @@ When the Hermes Runtime plugin (`fusion-plugin-hermes-runtime`) is installed and When the Cursor Runtime plugin (`fusion-plugin-cursor-runtime`) is installed and the `useCursorCli` toggle is enabled (Settings → Authentication), Cursor CLI-discovered models (`cursor-agent models --json`, with text/`model list` fallbacks) are surfaced additively in `/api/models` under the `cursor-cli` provider — id/name derived from the discovered model id/label. This surfacing is fetched through a short-TTL, single-flight cache so the model picker never spawns `cursor-agent` on every request; a missing/failed/unavailable Cursor CLI binary simply yields zero `cursor-cli` rows without affecting other providers. Disabling `useCursorCli` hides all `cursor-cli` rows. -When the Grok Runtime plugin (`fusion-plugin-grok-runtime`) is installed and the `useGrokCli` toggle is enabled (Settings → Authentication), Grok CLI-discovered models (`grok models`) are surfaced additively in `/api/models` under the `grok-cli` provider — id/name derived from the discovered model id/label. This surfacing is fetched through a short-TTL, single-flight cache so the model picker never spawns `grok` on every request; a missing/failed/unavailable Grok CLI binary simply yields zero `grok-cli` rows without affecting other providers. Disabling `useGrokCli` hides all `grok-cli` rows. Unlike Cursor (OAuth/session auth), Grok direct-endpoint auth is API-key based, but CLI-routed execution lets the `grok` binary use any auth source it supports; the Settings card still surfaces Fusion-visible key detection only as an informational hint. + + +The Usage dropdown can show a Cursor card when the dashboard process has a Cursor Admin API key in `CURSOR_API_KEY`. Fusion calls Cursor Admin API `POST https://api.cursor.com/teams/spend` with Basic auth (`API_KEY:`) and maps documented team spend fields into the generic usage-window UI. Cursor documents API key creation at `cursor.com/dashboard` → **API Keys** with `admin:*` scope for Admin API access, but does not document a local Admin API key file or an env-var name; `CURSOR_API_KEY` is Fusion's operator-facing convention. If only the `cursor-agent` session/OAuth login is present, Fusion omits the Cursor usage card because Cursor has not documented a personal/session usage endpoint; expired Admin API keys remain visible as an error card so operators can rotate the environment secret. + +When the Grok Runtime plugin (`fusion-plugin-grok-runtime`) is installed and the `useGrokCli` toggle is enabled (Settings → Authentication), Grok CLI-discovered models (`grok models`) are surfaced additively in `/api/models` under the `grok-cli` provider — id/name derived from the discovered model id/label. This surfacing is fetched through a short-TTL, single-flight cache so the model picker never spawns `grok` on every request; a missing/failed/unavailable Grok CLI binary simply yields zero `grok-cli` rows without affecting other providers. Disabling `useGrokCli` hides all `grok-cli` rows. Grok direct-endpoint auth is API-key based (`GROK_API_KEY` or the Grok CLI settings-file fallback), while Cursor has two distinct paths: `cursor-cli` model execution uses OAuth/session auth and Cursor usage metering uses Fusion's `CURSOR_API_KEY` Admin API key env var. CLI-routed Grok execution lets the `grok` binary use any auth source it supports; the Settings card still surfaces Fusion-visible key detection only as an informational hint. The three GPT-5.6 codenamed OpenAI Codex variants (`gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`) are additively surfaced under the `openai-codex` provider (FN-7745/FN-7754/FN-7759, mirroring the Anthropic/Z.ai supplemental-merge pattern above) so they appear both in dashboard `/api/models` and the engine/pi `createFnAgent` registry-seeding surface whenever `openai-codex` is configured — deduped against any pinned pi-ai catalog row that already carries one of the ids. FN-7759 specifically keeps the supplemental registration compatible with the real pi-coding-agent `ModelRegistry` by preserving the OpenAI Codex OAuth provider during dynamic full-provider replacement, so legacy catalogs without native 5.6 rows still survive `getAvailable()` auth filtering and remain executable. diff --git a/docs/solutions/logic-errors/git-detection-false-not-repo.md b/docs/solutions/logic-errors/git-detection-false-not-repo.md new file mode 100644 index 0000000000..e1ad59c12e --- /dev/null +++ b/docs/solutions/logic-errors/git-detection-false-not-repo.md @@ -0,0 +1,54 @@ +--- +title: "Git probe failures falsely reported as not a repository" +date: 2026-07-10 +category: docs/solutions/logic-errors +module: "engine Git detection + task executor preflight" +problem_type: logic_error +component: engine +symptoms: + - "Every task fails immediately with Project directory is not a Git repository" + - "The project is a valid Git repo, but git rev-parse fails for an environmental reason" + - "Restarting the engine does not clear the failure because the environment condition persists" +root_cause: error_classification_collapse +resolution_type: code_fix +severity: high +related_components: + - "packages/engine/src/worktree-pool.ts (detectGitRepository)" + - "packages/engine/src/executor.ts (dispatch preflight guard)" + - "packages/engine/src/runtimes/in-process-runtime.ts (startup warning)" +tags: + - git + - worktrees + - executor + - dubious-ownership + - false-negative +--- + +# Git probe failures falsely reported as not a repository + +## Problem + +A boolean Git repository probe collapses every `git rev-parse --git-dir` failure into `false`. That makes a positive non-repo response indistinguishable from environmental failures such as `fatal: detected dubious ownership`, `spawn ENOENT`, or a hung Git command. The executor then tells operators to run `git init` even when the checkout is already a valid repository, blocking all task execution until the underlying Git environment is fixed. + +## Solution + +Use tri-state Git detection: `repo`, `not-repo`, or `error`. Only the `not-repo` state may produce the existing "Project directory is not a Git repository" / `git init` guidance. Environmental failures surface the original Git error instead; dubious ownership also includes the explicit safe-directory command: + +```bash +git config --global --add safe.directory "" +``` + +The probe remains async and bounded with a timeout, so a hung Git process cannot silently become a false non-repo verdict. + +## Verification + +Cover the invariant at every consumer of repository detection: + +- Detection helper: genuine repo → `repo`; genuine `fatal: not a git repository` → `not-repo`; dubious ownership, missing Git, and timeout → `error`. +- Executor guard: `not-repo` preserves the legacy log/error strings; `error` logs and throws a distinct message without `git init` guidance and does not attempt `git worktree add`. +- Runtime startup: `not-repo` preserves the startup warning; `error` warns with the real Git failure and any safe-directory remedy. +- Worktree-add conflict parsing: `detected dubious ownership` stays `unknown`, not `not-git-repo`, so it does not become a non-retryable `git init` error. + +## Prevention + +Do not use boolean wrappers at guardrails that need operator-facing diagnosis. Keep positive semantic states separate from probe failures, preserve stderr in the result, and add tests for both a POSIX path and a Windows path with spaces passed through `cwd` rather than interpolated into the shell command. diff --git a/docs/storage.md b/docs/storage.md index cd989b059a..8a245f5ca4 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -76,9 +76,12 @@ - `artifacts` is the first-class metadata registry for generated or uploaded task artifacts. Rows store ID, `type` (`document`, `image`, `video`, `audio`, or `other`), title/description, MIME type, size, author identity/type, optional task linkage, metadata JSON, textual `content`, a relative `uri`, and timestamps; binary bytes are not stored in SQLite. - `TaskStore.registerArtifact()` writes task-scoped binary payloads under `/.fusion/tasks/{ID}/artifacts/` and task-less registry payloads under `/.fusion/artifacts/`, then records a relative `artifacts/` URI in SQLite. If the DB insert fails after a binary write, the store removes the orphaned file before surfacing the error. -- Image task attachments (`image/png`, `image/jpeg`, `image/gif`, `image/webp`) are bridged into the artifact registry by `TaskStore.addAttachment()` as `image` rows with `metadata.source: "attachment"` and a relative `attachments/` URI. This keeps one copy of the bytes under `/.fusion/tasks/{ID}/attachments/` while making the image discoverable through artifact list APIs and the Documents/Task Artifacts galleries. Non-image attachments remain attachment-only. Deleting an attachment also deletes its bridged artifact row before removing the attachment file so `/api/artifacts/:id/media` does not point at a deleted attachment. +- Image task attachments (`image/png`, `image/jpeg`, `image/gif`, `image/webp`) and video task attachments (`video/mp4`, `video/webm`, `video/quicktime`; 100MB cap vs 5MB for other attachments) are bridged into the artifact registry by `TaskStore.addAttachment()` as `image`/`video` rows with `metadata.source: "attachment"` and a relative `attachments/` URI. This keeps one copy of the bytes under `/.fusion/tasks/{ID}/attachments/` while making the image discoverable through artifact list APIs and the Documents/Task Artifacts galleries. Non-image attachments remain attachment-only. Deleting an attachment also deletes its bridged artifact row before removing the attachment file so `/api/artifacts/:id/media` does not point at a deleted attachment. - Inline text/document artifacts may store `content` directly in SQLite and therefore have no media file. The dashboard media route streams `GET /api/artifacts/:id/media` from disk when `uri` is present, accepting task-scoped artifact URIs under `artifacts/` and bridged image-attachment URIs under `attachments/`, or returns inline `content` with the persisted MIME type when no `uri` exists. - `getArtifact(id)` returns metadata by ID, `getArtifacts(taskId)` returns active-task artifacts newest-first, and `listArtifacts(...)` is the cross-agent query path with type/author/task/search filters and pagination. List reads hide artifacts whose parent task is soft-deleted while preserving task-less artifacts. +- `updateArtifact(id, { title?, description?, content? })` powers the dashboard Artifacts view's in-place doc editing (`GET`/`PATCH /api/artifacts/:id`). Content edits are only allowed on inline-content rows (no `uri`); binary-backed rows accept metadata edits only, archived-task artifacts stay read-only, and successful updates emit `artifact:updated` for live gallery refresh. +- `fn_artifact_register` accepts a local file `path` (in addition to inline `content`/`dataBase64`): the tool reads the file (50 MB cap), infers the MIME type from the extension when omitted, signature-validates image payloads (PNG/JPEG/GIF/WebP magic bytes, SVG text sniff), video payloads (mp4/mov `ftyp` box, WebM EBML header), and PDF payloads (`%PDF-` prefix), and persists the bytes through `registerArtifact()`'s managed storage path so the registry row keeps a servable URI after worktrees are cleaned up. Executor-lane registrations resolve relative paths against the task worktree and default `taskId` to the executing task. Every `path` is containment-checked before stat/read: the realpath-canonicalized file (symlinks and `../` segments resolved) must remain inside the session's `baseDir` or the OS temp directory; relative paths require a configured `baseDir`, and lanes without one (dashboard chat, no-task heartbeats) accept only absolute paths under the OS temp directory. HTML mockups register as `type="document"` + `mimeType="text/html"` (via `content` or `path`) and render as live sandboxed previews in the Artifacts view. +- `GET /api/artifacts/:id/media` serves HTTP byte ranges (`Accept-Ranges: bytes`, 206 + `Content-Range` for single ranges, 416 for unsatisfiable ranges) so `