118 KiB
Fusion Architecture
This document describes the actual architecture of Fusion as implemented in this repository (gsxdsm/fusion). It is intended as a practical onboarding map for developers and AI agents.
1) Overview
Fusion is an AI-orchestrated task board. It takes tasks through a structured lifecycle (planning → todo → in-progress → in-review → done → archived) and automates planning, execution, review, merge, and operational recovery.
At a high level, Fusion is split into:
- Core domain + persistence (
@fusion/core) - Execution engine (
@fusion/engine) - Dashboard API + SPA (
@fusion/dashboard) - CLI + Pi extension (
@runfusion/fusion) - Desktop shell (
@fusion/desktop) - Mobile shell (
@fusion/mobile) - Terminal dashboard (part of
@runfusion/fusion— seepackages/cli/src/commands/dashboard-tui/)
Native shells expose a shared host-neutral bridge at window.fusionShell for first-run shell onboarding, connection profile persistence, and active shell mode/profile state. The dashboard consumes window.fusionShell when present and degrades cleanly in plain web/PWA mode.
The dashboard also has a canonical host-context bootstrap layer (packages/dashboard/app/shell-host.ts) that normalizes launch metadata into one discriminated union:
{ kind: "browser" }{ kind: "desktop-shell", mode?, connectionId?, serverUrl?, canOpenConnectionManager? }{ kind: "mobile-shell", mode?, connectionId?, serverUrl?, canOpenConnectionManager? }
Detection priority is deterministic: explicit bootstrapped global from shell handoff → shell handoff query params → desktop fallback via window.fusionAPI presence → browser fallback. Shell-only query params are stripped at bootstrap via history.replaceState.
React consumers read this through ShellHostProvider / useShellHostContext (packages/dashboard/app/context/ShellHostContext.tsx). Do not add ad-hoc host checks in components.
Dashboard chrome now resolves connection-management capabilities through packages/dashboard/app/shell-native.ts (getShellConnectionNativeResult) and renders status/actions via ShellConnectionStatus. Components should receive derived props from App-level wiring, not read window.fusionAPI/window.fusionShell directly.
Important distinction: NodeContext.isRemote indicates browsing a remote mesh node inside the current dashboard instance; shell host mode: "remote" indicates how native desktop/mobile launched into this dashboard server. These are separate axes and must not be conflated in UI or routing logic.
window.fusionShell bridge contract
Canonical dashboard-side types live in packages/dashboard/app/types/native-shell.d.ts.
Shared bridge methods used by dashboard/mobile/desktop flows:
getState()listProfiles()saveProfile(profile)deleteProfile(profileId)setActiveProfile(profileId)setDesktopMode(mode)startQrScan()openConnectionManager()subscribe(listener)
Shared shell state contract (ShellConnectionState):
host("web" | "mobile-shell" | "desktop-shell")desktopMode("local" | "remote", optional)activeProfileIdprofileslocalServer(status, optionalport, optionalerror)
Desktop-specific bootstrap extension:
- Electron preload also exposes
getDesktopModeState()for first-run desktop mode selection ({ isFirstRun, desktopMode }). - Electron preload exposes
window.fusionAPI.openConnectionManager()as the renderer-safe desktop entry point for opening native connection management. - The dashboard itself does not depend on that preload-only helper for steady-state rendering; it consumes shared shell state via
ShellContext(packages/dashboard/app/context/ShellContext.tsx).
Persistence ownership by host:
- Mobile shell persists connection profiles + active profile with Capacitor Preferences (
packages/mobile/src/plugins/connection-profiles.ts). - Desktop shell persists shell settings in app-owned JSON at
app.getPath("userData")/shell-connections.json(packages/desktop/src/shell-settings.ts).
These are shell-owned persistence layers, intentionally separate from Fusion project/global settings.
Shell contract regression matrix (FN-3409)
Cross-package automated tests now lock:
- Mobile shell: first-run remote onboarding inputs (QR/manual + optional token), saved-profile edit/switch, and restore-on-reinit persistence.
- Desktop shell: first-run/last-used mode restore, local-vs-remote startup behavior, and preload bridge channel compatibility for connection management.
- Dashboard shell awareness: canonical per-viewport connection-manager entry placement, browser-safe fallback (no shell-only controls), and host-context/native-helper resolution without ad-hoc window bridge access.
- Sensitive data handling: dashboard-facing native status surfaces expose profile label/origin metadata only; auth tokens are not surfaced.
High-level runtime diagram
┌──────────────────────────────┐
│ Human + AI Interactions │
│ (Dashboard SPA, CLI, Pi) │
└──────────────┬───────────────┘
│
┌──────────────────────┼──────────────────────┐
│ │ │
┌─────────▼─────────┐ ┌─────────▼─────────┐ ┌─────────▼─────────┐
│ Dashboard (API) │ │ CLI `fn` router │ │ Pi extension tools │
│ + React SPA │ │ + TUI component │ │ (extension.ts) │
│ (lazy-loaded) │ │ (commands/*) │ │ │
└─────────┬─────────┘ └─────────┬─────────┘ └─────────┬─────────┘
└──────────────┬────────┴──────────────┬───────┘
│ │
┌────────▼───────────────────────▼───────┐
│ Engine Runtime │
│ Scheduler / Planning / Executor / Merger │
│ Heartbeat / Self-healing / Autopilot │
└────────┬───────────────────────┬────────┘
│ │
┌───────────▼──────────┐ ┌────────▼─────────────┐
│ @fusion/core │ │ External systems │
│ stores + types │ │ git, GitHub, models │
└───────┬──────────────┘ └───────────────────────┘
│
┌────────────────▼────────────────┐
│ Persistence │
│ - .fusion/fusion.db (SQLite/WAL)
│ - .fusion/tasks/* (PROMPT/logs)
│ - ~/.fusion/fusion-central.db │
└──────────────────────────────────┘
2) Monorepo Structure
| Package | Published | Role | Key files |
|---|---|---|---|
@fusion/core |
Private | Domain model, stores, SQLite adapters, settings, shared types | packages/core/src/types.ts, store.ts, db.ts, central-core.ts, agent-store.ts |
@fusion/engine |
Private | AI orchestration runtime (planning, scheduler, executor, merger, recovery) | planning processor, scheduler.ts, executor.ts, merger.ts, project-runtime.ts |
@fusion/dashboard |
Private | Express API server + React app | packages/dashboard/src/server.ts, routes.ts, sse.ts, websocket.ts, packages/dashboard/app/App.tsx |
@runfusion/fusion |
Published | CLI binary (fn) + Pi extension |
packages/cli/src/bin.ts, commands/*, project-resolver.ts, extension.ts |
@fusion/desktop |
Private | Electron shell around Fusion dashboard/client | packages/desktop/src/main.ts, ipc.ts, preload.ts, scripts/build.ts |
@fusion/mobile |
Private | Capacitor + PWA mobile packaging of dashboard assets | packages/mobile/capacitor.config.ts, packages/mobile/src/* |
@fusion/plugin-sdk |
Private | Plugin SDK for building Fusion extensions | packages/plugin-sdk/src/* |
3) Package Dependencies
Workspace dependency graph
A ──▶ B means A depends on B.
@fusion/engine ───────────────▶ @fusion/core
@fusion/dashboard ────────────▶ @fusion/core
@fusion/dashboard ────────────▶ @fusion/engine
@runfusion/fusion (CLI) ─────────▶ @fusion/core
@runfusion/fusion (CLI) ─────────▶ @fusion/engine
@runfusion/fusion (CLI) ─────────▶ @fusion/dashboard
@fusion/plugin-sdk (peerDep) ─▶ @fusion/core
@fusion/desktop: no workspace package dependencies
@fusion/mobile: no workspace package dependencies
Concrete references:
@fusion/enginehas a workspace dependency on@fusion/core(packages/engine/package.json)@fusion/dashboardhas workspace dependencies on@fusion/coreand@fusion/engine(packages/dashboard/package.json)@runfusion/fusionhas workspace development dependencies on@fusion/core,@fusion/engine, and@fusion/dashboardfor composition/build packaging (packages/cli/package.json)@fusion/plugin-sdkdeclares a peer dependency on@fusion/core(packages/plugin-sdk/package.json)@fusion/desktopembeds dashboard assets at build time via script (packages/desktop/scripts/build.ts) but does not declare workspace deps inpackage.json@fusion/mobiletriggers dashboard build/sync via scripts (packages/mobile/package.json) but does not declare workspace deps inpackage.json
4) Core Package (@fusion/core)
Responsibility
@fusion/core is the shared domain and persistence layer.
Main components
- Types and constants:
packages/core/src/types.ts- Columns:
COLUMNS - Transition map:
VALID_TRANSITIONS - Settings defaults:
DEFAULT_GLOBAL_SETTINGS,DEFAULT_PROJECT_SETTINGS - Workflow types (
WorkflowStep,WorkflowStepPhase, etc.)
- Columns:
- TaskStore:
packages/core/src/store.ts- Main task CRUD + lifecycle store
- Emits board events (
task:created,task:moved,task:updated, ...) - Hybrid model: SQLite metadata + filesystem blobs under
.fusion/tasks/{id}
- Database adapter:
packages/core/src/db.ts- SQLite (
node:sqlite) with WAL mode + foreign keys - JSON helpers:
toJson,toJsonNullable,fromJson - Core schema tables include:
tasks,config,workflow_steps,activityLog,archivedTasks,automations,agents,agentHeartbeats, approval tables (approval_requests,approval_request_audit_events),task_documents,task_document_revisions, mission hierarchy tables (missions,milestones,slices,mission_features,mission_events), plugin/routine tables (plugins,routines), roadmap tables (roadmaps,roadmap_milestones,roadmap_features), insight tables (project_insights,project_insight_runs), research tables (research_runs,research_exports,research_run_events), eval tables (eval_runs,eval_task_results,eval_run_events), todo tables (todo_lists,todo_items),__meta - Migration-created tables include:
ai_sessions,messages,agentRatings,chat_sessions,chat_messages,runAuditEvents,mission_contract_assertions,mission_feature_assertions,mission_validator_runs,mission_validator_failures,mission_fix_feature_lineage ai_sessions.statuslifecycle includesdraft(pre-start planning session), thengenerating,awaiting_input, terminalcomplete/error
- SQLite (
- Roadmap feature ownership: roadmap contracts, ordering/handoff helpers, persistence, routes, and dashboard UI live in
plugins/fusion-plugin-roadmap(package@fusion-plugin-examples/roadmap, plugin idfusion-plugin-roadmap) rather than dashboard/core ownership. - CentralCore:
packages/core/src/central-core.ts- Global project registry, health, central activity feed, global concurrency
- Backed by
packages/core/src/central-db.ts(~/.fusion/fusion-central.db)
- Specialized stores:
AgentStore(agent-store.ts) — filesystem-based agent metadata + heartbeat run historyMissionStore(mission-store.ts) — mission/milestone/slice/feature hierarchyAutomationStore(automation-store.ts) — scheduled jobs with global/project scope isolationMessageStore(message-store.ts) — SQLite-backed mailbox/inbox/outbox messagingApprovalRequestStore(approval-request-store.ts) — durable approval request lifecycle + append-only audit eventsChatStore(chat-store.ts) — session/message persistence for agent chatInsightStore(insight-store.ts) — project insight persistence + dedupe/run trackingReflectionStore(reflection-store.ts) — agent reflection records and performance snapshotsPluginStore(plugin-store.ts) — plugin registry/state/settings persistenceRoutineStore(routine-store.ts) — recurring routine definitions and run historyTodoStore(todo-store.ts) — project-scoped todo lists/items with completion, reorder, and composite list+items queriesEvalStore(eval-store.ts) — eval run persistence, per-task eval results with durable snapshots, and append-only run event trails
Approval request system (ApprovalRequestStore)
Schema (migration 68 in db.ts) adds two tables:
approval_requests- Identity/lifecycle:
id,status,requestedAt,decidedAt,completedAt,createdAt,updatedAt - Requester snapshot:
requesterActorId,requesterActorType,requesterActorName - Target action payload:
targetActionCategory,targetActionOperation,targetActionSummary,targetResourceType,targetResourceId,targetContext(JSON text) - Optional runtime linkage:
taskId,runId - Indexes:
idxApprovalRequestsStatusCreatedAt (status, createdAt),idxApprovalRequestsRequesterCreatedAt (requesterActorId, createdAt),idxApprovalRequestsTaskCreatedAt (taskId, createdAt)
- Identity/lifecycle:
approval_request_audit_eventsid,requestId,eventType, actor snapshot (actorId,actorType,actorName), optionalnote,createdAtrequestIdis a foreign key toapproval_requests(id)withON DELETE CASCADE- Index:
idxApprovalRequestAuditRequestCreatedAt (requestId, createdAt, id)
Store API (packages/core/src/approval-request-store.ts):
Dashboard approval endpoints (packages/dashboard/src/routes/register-approval-routes.ts):
GET /api/approvalsGET /api/approvals/:idPOST /api/approvals/:id/decision
Runtime flow: engine action gate creates/reuses request → pauses task/agent with pauseReason="awaiting-approval" → approver calls decision endpoint (decision: approve|deny) → request transitions (pending→approved|denied) → route resumes matching paused task/agent best-effort → next tool retry consumes approved exactly once (then completed) or returns structured denial.
Provisioning note: durable fn_agent_create / fn_agent_delete approvals use agent_provisioning policy handling on this same decision route; fn_spawn_agent stays under action-gate task_agent_mutation because spawned children are ephemeral runtime workers.
create(input: ApprovalRequestCreateInput)— inserts apendingrequest and appends acreatedaudit eventget(id)— returns one request ornulllist(input?: ApprovalRequestListInput)— filters bystatus,requesterActorId,taskId,runId; orderedcreatedAt DESC, id DESC; paginated bylimit/offsetdecide(requestId, status, input: ApprovalRequestDecisionInput)— appliespending -> approved|denied, stampsdecidedAt, appendsapproved/deniedaudit eventmarkCompleted(requestId, input: ApprovalRequestCompletionInput)— appliesapproved -> completed, stampscompletedAt, appendscompletedaudit eventgetAuditHistory(requestId)— returns append-only audit rows orderedcreatedAt ASC, rowid ASC
Lifecycle contract (types.ts isValidApprovalRequestTransition):
- Primary forward paths:
pending -> approved -> completedandpending -> denied - Direct
pending -> completedand all transitions fromdenied/completed(except no-op self-transition) are rejected - Same-state transitions (
from === to) are treated as valid by the helper even though the intended lifecycle is forward-only
Mesh state read path for dashboard topology
GET /api/mesh/stateinpackages/dashboard/src/routes/register-mesh-routes.tsis the authoritative dashboard/API read path for topology.- Default behavior aggregates a deduped cluster snapshot from the local node plus reachable peers (
includeRemote !== false) while preserving node-local last-known entries when peers are unreachable. includeRemote=falseis the non-recursive local-only path used for peer fan-out, so cross-node aggregation never recursively calls remote aggregated endpoints.- Route registration reuses the shared
options?.centralCoreinstance when available instead of creating per-requestCentralCoreinstances, preserving shared mesh state continuity. - Nodes UI topology consumes a dedicated
useMeshStatehook that unwraps the/api/mesh/statesnapshot intoNodeMeshState[];MeshTopologyrenders peer relationships directly from each node'sknownPeers(including remote↔remote links) without fabricating local-star fallback edges.
Shared mesh-state snapshot helpers
packages/core/src/shared-mesh-state.ts defines a common snapshot envelope for non-task mesh state export/apply:
- Envelope fields:
version,exportedAt,checksum,payload - Checksum rule:
sha256(JSON.stringify(payloadWithoutChecksum)) - Payload families:
TaskMetadataSnapshot(tasksstructured metadata only)MissionHierarchySnapshot(missions,milestones,slices,features,missionEvents,assertions,featureAssertionLinks)AgentSnapshot(agents,blockedStates)AgentRunSnapshot(runs)ActivityLogSnapshot(entries)RunAuditSnapshot(entries)ProjectSettingsSnapshot(global,projects)AuthMaterialSnapshot(providerAuth, with API-key and OAuth credential shapes)
Intentional exclusions from shared snapshots:
- Task/agent blob contents (
PROMPT.md, task document bodies, attachment bytes, JSONL run logs) - Instruction-bundle file contents
- Node-local runtime handles and paths (for example worktree/session-file handles)
Chat System
ChatStore(packages/core/src/chat-store.ts) andchat-types.tsprovide session-oriented chat state (chat_sessions,chat_messagestables)- Dashboard chat UX lives in
packages/dashboard/app/components/ChatView.tsxand hooksuseChat.ts/useQuickChat.ts - Main
useChatsession restore/recovery must not reset the active thread during session-list refresh orchat:session:updatedmetadata churn while a response is in flight. chat_sessions.inFlightGenerationstores a durable JSON snapshot while generation is active: latest streamed text/thinking, tool-call state, andreplayFromEventIdfor SSE resume.ChatManager.sendMessage()updates that snapshot during streaming (debounced) and clears it on done/error/cancel so stale partial state does not survive completion.- When the active session is still generating after reload/reconnect (
isGenerating: true),useChat/useQuickChathydrate the UI frominFlightGenerationimmediately, then reconnect/api/chat/sessions/:id/streamwithLast-Event-ID = replayFromEventIdto avoid re-appending already-known deltas. - Chat message submission uses SSE streaming responses from dashboard chat routes.
- Direct-chat terminal failures now persist as a distinct assistant message with
metadata.failureInfo(summary, optionalerrorClass, optionalcode, optionaldetail, optional reference metadata) so the chat thread remains the durable primary failure surface after reload/reconnect. ChatManager.sendMessage()preserves any interrupted partial assistant output as its own message, then appends a separate persisted failure bubble instead of overwriting the partial reply.- Main-chat optimistic user sends are reconciled against persisted SSE user echoes by content + temp-id replacement, so one user send cannot survive as a duplicate history entry after stream completion.
useChat.loadMessages()/session restore map persistedmetadata.failureInfoback intoChatMessageInfo.failureInfo, and live stream failures append the same assistant-style bubble client-side unless the error is classified as a tab-suspension false positive.ChatViewrenders failure bubbles inline with shared error-surface tokens; mailbox references deep-link into the mailbox view, while other failure references keep an inline "View failure details" affordance so reload/reconnect does not strand users in agent logs.ChatViewrenders those failure bubbles with inline assistant attribution even for model-only__fn_agent__chats, so provider/model failures still read as a response from the active model instead of an anonymous system alert.streamChatResponse()must flush trailing buffered SSE data on EOF even without a final newline, so terminaldone/errorevents are not dropped at chunk boundaries.- Chat generation ownership is isolated by
generationId(ChatManager.beginGeneration+ChatStreamManagersubscription filters + route preallocation), preventing stale generation terminal events from leaking into a newer active request.
Chat Rooms (Dashboard)
- The Rooms tab in
packages/dashboard/app/components/ChatView.tsxis wired throughuseChatRooms(packages/dashboard/app/hooks/useChatRooms.ts). useChatRoomsowns room list fetch/sort, active-room selection, member+message hydration, room creation/deletion, and room message sends.- The hook subscribes to
/api/eventsand consumeschat:room:created,chat:room:updated,chat:room:deleted,chat:room:member:added,chat:room:member:removed,chat:room:message:added,chat:room:message:updated, andchat:room:message:deletedto keep UI state in sync. - Room messages persist through
POST /api/chat/rooms/:id/messages; the route persists the user message first, then callsChatManager.sendRoomMessage(...)to orchestrate room-member responders and persist assistant room replies withchatStore.addRoomMessage(...)(includingsenderAgentIdfor each responder). sendRoomMessage(...)uses existing room-member + mention resolution rules: mentioned members are direct responders, non-mentioned members are ambient responders (capped byROOM_AMBIENT_MAX_RESPONDERS), and non-member mentions are handled explicitly by the manager instead of silently disappearing.- Room responder prompt context is compacted deterministically: the newest 12 room messages stay verbatim, while older fetched history is summarized into a structured header (span, participants, and ranked highlights) before prompt size caps are enforced.
- Room-reply generation is now non-silent on failure: if a room has members but no active responders can be resolved, or all responder generations fail/return empty output,
sendRoomMessage(...)throwsRoomReplyGenerationErrorand the route surfaces HTTP 502 instead of returning a silent user-only success. useChatRooms.sendRoomMessage()now follows direct-chat style optimistic UX: append a temporary local user room message beforePOST /api/chat/rooms/:id/messages, reconcile that temp entry to the persisted user message on success, then refresh authoritative transcript state while continuingchat:room:message:*live SSE updates.- On failures,
useChatRooms.sendRoomMessage()performs state reconciliation (rollback temp entry or replace with persisted transcript when POST partially succeeded) and rethrows;ChatViewclears the composer immediately when dispatching a room send, restores the exact prior text only if the send rejects, and owns the single user-facing error toast. - Mention UI in rooms keeps direct-chat behavior unchanged while adding room affordances:
AgentMentionPopupreceives room membership context and shows members first with astatus-dotmember indicator (aria-label="Room member").- With an empty mention filter in room mode, only room members are listed; a hint row prompts the user to type to search non-members.
- Mention chips rendered in room messages (
ChatViewandQuickChatFAB) mark non-members viachat-mention-chip--non-member, includingtitle/aria-labeltext (Not a member of {roomName}) and muted warning-token styling.
Agent Companies
- Import/export utilities:
agent-companies-parser.ts,agent-companies-exporter.ts,agent-companies-types.ts - Supports YAML-frontmatter manifests for company/team/agent/project/task/skill definitions
- Includes conversion helpers from parsed manifests to
AgentCreateInputand export helpers for directory bundles
Project Insights
InsightStore(insight-store.ts,insight-types.ts) persists extracted project learnings- Uses fingerprint-based deduplication and run tracking
- Run lifecycle is hardened through
insight-run-executor.ts+InsightStoretransition guards:- single active run per
projectId + trigger(pending|runningconflict) - terminal-state immutability for run rows
- persisted failure classification (
cancelled,timed_out,retryable_transient,non_retryable) and retry lineage metadata - append-only durable event trail in
project_insight_run_events
- single active run per
- Dashboard routes (
insights-routes.ts) consume the core executor/store APIs for run start, cancel, retry, and event inspection (/api/insights/runs/:id/events) POST /api/insights/runpreserves the single-active-run guarantee while adding orphan recovery for stalepending|runningrows:- a conflicting active row is only auto-recovered when there is no in-memory controller ownership (
activeRunControllershas no entry) and run age (startedAt ?? createdAt) exceeds the grace window (ORPHAN_GRACE_MS = 30_000) - recovered rows are durably marked
failedwith lifecycle terminal metadata (terminalReason=failed,terminalCause=orphaned_active_run_recovered,failureClass=non_retryable,retryable=false) and warning/status events appended toproject_insight_run_events - true live conflicts continue returning HTTP 409 with structured payload details
{ code: "ACTIVE_RUN_CONFLICT", activeRunId, activeRunStatus, trigger }so the dashboard can hydrate and display the existing active run instead of surfacing a raw backend exception
- a conflicting active row is only auto-recovered when there is no in-memory controller ownership (
POST /api/insights/:id/create-taskremains a draft-payload endpoint (returnssuggestedTitle/suggestedDescription); the dashboardInsightsViewnow uses that payload to create a real task through the normal app task-creation path (column: triage,sourceType: dashboard_ui, source metadata indicating insights origin)- Backed by
project_insights,project_insight_runs, andproject_insight_run_events
Research Runs
ResearchStore(research-store.ts,research-types.ts,research-settings.ts) persists bounded research runs, sources/events, exports, lifecycle metadata, and retry/cancel state transitions.- Backed by
research_runs,research_exports, andresearch_run_events. - Engine orchestration is implemented in
packages/engine/src/research-orchestrator.ts+research-step-runner.ts. - Dashboard/API surface is implemented under
/api/research(packages/dashboard/src/research-routes.ts) withResearchView.tsxin the app. - CLI surface is implemented in
packages/cli/src/commands/research.tswith six subcommands (create, list, show, export, cancel, retry). - Agent tool surface is exposed via
packages/cli/src/extension.ts(fn_research_run,fn_research_list,fn_research_get,fn_research_cancel,fn_research_retry). - Boundary contract (FN-3292):
ResearchStoreowns persistence and lifecycle writes (status transitions, lifecycle event log rows, sources/results snapshots).ResearchStepRunnerowns provider I/O concerns only (provider selection, timeout/abort/provider-error classification, synthesis call execution); it does not read/write run state.ResearchOrchestratorowns sequencing and failure policy (phase progression, provider fallback, partial-step continuation, terminal status choice) and interacts with store only through public store methods.- Provider substitution must remain data-driven: source metadata can carry provider identity, and fetching should resolve providers per source rather than relying on provider ordering.
- Boundary note: research and insights are parallel subsystems sharing host infrastructure, not one table/store family.
Task Evaluations
EvalStore(eval-store.ts,eval-types.ts) persists eval runs and task-level eval outcomes.- Dashboard/API surface is implemented under
/api/evals(packages/dashboard/src/evals-routes.ts) withEvalsView.tsxin the app. - Backed by
eval_runs,eval_task_results, andeval_run_events. - Data model stores structured scoring/evidence/signal payloads plus durable
taskSnapshotmetadata so historical eval results remain readable even if the live task row later changes or is removed. - Lifecycle safeguards mirror other core stores: deterministic list ordering, transition guards, terminal immutability for run rows, and active-run conflict protection for scheduled/task-completion triggers.
eval_task_resultsenforces one row per(runId, taskId)via a unique index; store writes use upsert semantics to keep reruns idempotent.- Canonical scoring contract is documented in
docs/evals.md; authoritative score computation is centralized inpackages/core/src/eval-scoring.ts.
Scoring authority boundary:
- Authoritative fields:
categoryScores[].finalScore,categoryScores[].band,categoryScores[].weight, andoverallScore(derived bycomputeOverallScore). - Advisory/model-authored fields: category
aiScore, categoryrationale, categoryevidence, andoverallRationaletext. - Evaluator code (
packages/engine/src/evaluator.ts) may provide AI category inputs, but must route final score computation through core helpers (normalizeCategoryScore,computeOverallScore) and must not persist AI-provided overall numbers as source of truth.
Hybrid evaluator pipeline (FN-3389/FN-3391):
- Batch selection:
runScheduledEvalBatchin core computes a deterministic completed-task window (windowStartExclusive→windowEndInclusive) from the last completed scheduled run. - Signal summary:
collectDeterministicSignals(eval-signal-collector.ts) normalizes timing/workflow/review/log/commit summaries with stable fallbacks for missing metadata. - Evidence harvesting:
collectTaskEvaluationEvidence(packages/engine/src/evaluator-evidence.ts) reads existing task-store/git surfaces (workflowStepResults, documents, task activity log, agent logs, run-audit events, merge/PR metadata) and emits a boundedTaskEvaluationEvidenceBundlewith fixed source-group ordering. - AI review:
HybridEvaluatorService(packages/engine/src/evaluator.ts) injects deterministic signals plus a dedicated## Evidencebundle section into a strict JSON prompt, runs a read-only AI session, validates the JSON payload, and merges AI advisory fields into persisted eval output while preserving core score authority. - Follow-up policy engine:
packages/engine/src/eval-followups.tsnormalizes raw evaluator drafts into canonical follow-up suggestions, applies deterministic suppression/dedupe rules, and (policy permitting) materializes triage tasks throughTaskStore.createTask()with source provenance back to the parent task and eval run/suggestion IDs. - Persistence boundary: eval rows persist normalized evidence refs plus bounded excerpts/IDs (not full raw logs or unbounded command output) and structured follow-up lifecycle state (
suggested/suppressed/created) including suppression reason or created task linkage. Source drill-down stays in original task/agent/run-audit stores and git history. - Model resolution (temporary): evaluator model selection first uses an explicit run override pair (
provider+modelIdtogether only), then falls back to the existing validator lane (resolveValidatorSettingsModel) until FN-3393 introduces dedicated evaluator settings. - Scheduled execution wiring: CronRunner intercepts the sentinel command
fn eval --scheduled-batchand executes in-process, invokingrunScheduledEvalBatchwithHybridEvaluatorService;ProjectEnginesyncs scheduled eval automation on startup and on relevant settings changes.
Plugin System
PluginStore(plugin-store.ts) is a facade over two persistence scopes:- Global install metadata in central DB table
plugin_installs(~/.fusion/fusion-central.db) including manifest/path/settings/schema/dependencies - Per-project runtime state in central DB table
project_plugin_stateskeyed by normalized project path (enabled,state,error)
- Global install metadata in central DB table
- Legacy project-local
pluginsrows in.fusion/fusion.dbare migrated lazily on plugin-store init/read; migration is idempotent and keeps newestupdatedAtinstall metadata as global canonical data while preserving per-project enablement rows - Post-FN-3722, the project-local
pluginstable is legacy read-only migration input; any new install writer targeting it is a bug TaskStore.getPluginStore()now propagates the configuredglobalSettingsDir/central directory so all CLI and dashboard install paths resolve the same central DBPluginLoader(plugin-loader.ts) loads/unloads plugin modules using the effective per-project plugin state- Plugin contributions now include both embedded
uiSlotsand top-leveldashboardViews - Executor runtime contributions can be provided via
executorRuntimeEnv(taskCtx, ctx); see the canonical plugin-authoring contract indocs/PLUGIN_AUTHORING.md§4 "executorRuntimeEnv: task-scoped executor subprocess environment". The engine applies these task-scoped overlays only to executor-spawned user commands, never to git plumbing subprocesses. - Discovery endpoints:
GET /api/plugins/ui-slotsGET /api/plugins/dashboard-views
- Dashboard management routes are implemented in
packages/dashboard/src/plugin-routes.ts
Prompt Overrides
prompt-overrides.tsdefines prompt key catalogs and per-role override validation- Provides override resolution/validation helpers (
resolvePrompt,resolveRolePrompts,assertValidPromptOverrideMap)
Plugin Prompt Contributions
- Plugin prompt contributions are filtered per surface through
PluginRunner.getPromptContributionsForSurface(surface). - Prompt assembly uses
buildPluginPromptSection(surface, pluginRunner)inpackages/engine/src/agent-instructions.ts. - Supported prompt surfaces:
executor-systemexecutor-tasktriagereviewerheartbeat
- Integration points append the built plugin section to the role-specific system/task prompt only when contributions exist, preserving existing prompts when no plugins contribute.
Agent Permissions
agent-permissions.tsnormalizes permissions and computes effective access state- Core helpers:
normalizePermissions,computeAccessState,ROLE_DEFAULT_PERMISSIONS
Standalone roadmap model
Fusion now has two planning models in core:
- Roadmap hierarchy —
Roadmap → RoadmapMilestone → RoadmapFeature - Mission hierarchy —
Mission → Milestone → Slice → Feature → Task
The roadmap model is intentionally lightweight and independent from MissionStore/mission lifecycle semantics. It is meant for standalone planning, ordering, drag-and-drop moves, and future conversion flows into missions or tasks without coupling roadmap data to slice activation, autopilot, or mission status rollups.
Roadmap persistence (FN-1690/FN-1691):
RoadmapStoreprovides CRUD operations with atomic reorder/move semantics- All list queries use deterministic ordering:
ORDER BY orderIndex ASC, createdAt ASC, id ASC - Covering indexes ensure efficient ordered reads without temp B-tree sorts
- Cross-milestone feature moves atomically renumber both source and destination milestone scopes
- FK cascade integrity: deleting a roadmap removes milestones and features
- Export/handoff DTO methods for integration with downstream systems:
getRoadmapExport()→RoadmapExportBundle(flat export payload)getMissionPlanningHandoff()→RoadmapMissionPlanningHandoff(mission conversion)listFeatureTaskPlanningHandoffs()→RoadmapFeatureTaskPlanningHandoff[](all features as task handoffs)getRoadmapFeatureHandoff()→RoadmapFeatureTaskPlanningHandoff(single feature task handoff)
- Pure handoff mapping helpers in
roadmap-handoff.tsfor read-only transformations
Roadmap handoff contract boundary (FN-1674):
- Handoffs are read-only transformations — no mission/task records are created
- Source lineage is preserved on every emitted item (roadmapId, milestoneId, featureId, titles, order indices)
- Ordering is deterministic using
normalizeRoadmapMilestoneOrderandnormalizeRoadmapFeatureOrder - Not-found semantics: store handoff methods throw when roadmapId is unknown; routes map to HTTP 404
- The combined handoff endpoint (
GET /:roadmapId/handoff) returns both mission and task handoffs
Key roadmap invariants:
- milestone ordering is scoped to a single roadmap and must remain contiguous + 0-based
- feature ordering is scoped to a single milestone and must remain contiguous + 0-based
- repair/normalization uses deterministic tie-breakers:
orderIndex ASC,createdAt ASC,id ASC - cross-milestone feature moves must renumber both the source and destination milestone deterministically
Roadmap frontend API contract (plugin namespace):
- Canonical frontend namespace:
/api/plugins/fusion-plugin-roadmap/roadmaps - Roadmaps:
GET /,POST /,GET /:roadmapId,PATCH /:roadmapId,DELETE /:roadmapId - Milestones:
GET /:roadmapId/milestones,POST /:roadmapId/milestones,PATCH /milestones/:milestoneId,DELETE /milestones/:milestoneId,POST /:roadmapId/milestones/reorder - Features:
GET /milestones/:milestoneId/features,POST /milestones/:milestoneId/features,PATCH /features/:featureId,DELETE /features/:featureId,POST /milestones/:milestoneId/features/reorder,POST /features/:featureId/move - Export/Handoff:
GET /:roadmapId/export,GET /:roadmapId/handoff,GET /:roadmapId/handoff/mission,GET /:roadmapId/milestones/:milestoneId/features/:featureId/handoff/task - Canonical roadmap REST namespace is plugin-scoped (
/api/plugins/fusion-plugin-roadmap/...), while dashboard maintains a temporary/api/roadmapscompatibility mount that delegates to plugin-owned handlers during migration.
Database schema:
roadmaps— roadmap metadata (id, title, description, timestamps)roadmap_milestones— milestone data withroadmapIdFKroadmap_features— feature data withmilestoneIdFKidxRoadmapMilestonesRoadmapOrder— covering index for deterministic milestone orderingidxRoadmapFeaturesMilestoneOrder— covering index for deterministic feature ordering
Shared utilities
From packages/core/src/index.ts exports (selected high-impact modules):
- Memory + knowledge:
memory-backend.ts,memory-compaction.ts,memory-dreams.ts,project-memory.ts,memory-insights.ts,insight-store.ts,insight-types.ts - Stores and plugin/routine helpers:
chat-store.ts,routine-store.ts,plugin-store.ts,plugin-loader.ts,reflection-store.ts - Execution/runtime helpers:
run-command.ts,board.ts,task-merge.ts,archive-db.ts - Settings + prompts + permissions:
settings-schema.ts,prompt-overrides.ts,agent-permissions.ts,agent-prompts.ts - Node/system infrastructure:
node-connection.ts,node-discovery.ts,system-metrics.ts,migration-orchestrator.ts - Identity/version/extensions:
daemon-token.ts,app-version.ts,pi-extensions.ts - Agent companies import/export:
agent-companies-parser.ts,agent-companies-exporter.ts,agent-companies-types.ts
Docker Node Provisioning
Fusion has a managed Docker node provisioning subsystem spanning @fusion/core services and dashboard routes.
Core services:
DockerClientService(packages/core/src/docker-client.ts)- Creates Dockerode clients from host settings.
- Supports default local daemon, named Docker
context, or explicithostwith optional TLS fields. - Host/TLS inputs:
context,host,tlsVerify,tlsCaPath,tlsCertPath,tlsKeyPath.
DockerProvisioningService(packages/core/src/docker-provisioning.ts)- Handles initial container lifecycle actions (provision/deprovision/start/stop/restart/status).
- Provisioning creates and starts a container first, then route-level orchestration registers metadata/node records.
MeshConfigGenerator(packages/core/src/mesh-config-generator.ts)- Generates mesh env/config, applies config by recreating the container, registers the node into mesh state, then health-checks until online or timeout.
Route boundary (dashboard):
register-docker-provisioning-routes.tsowns initial container lifecycle endpoints (/api/docker/provision,/api/docker/deprovision, and per-container start/stop/restart/status).register-docker-node-routes.tsowns managed-node metadata + mesh configuration endpoints (for example/api/docker/nodes/:managedId/apply-mesh-configand mesh-status checks) after a container is provisioned.
Provisioning lifecycle (implemented flow):
- Container provisioning: dashboard provisioning route calls
DockerProvisioningService.provision()to create/start a managed container. - Mesh config generation:
MeshConfigGenerator.generateConfig()resolves API key, reachable URL, and mesh env vars. - Mesh config application:
MeshConfigGenerator.applyConfig()callsDockerClientService.recreateContainer()so env vars are applied to a recreated container. - Node registration:
MeshConfigGenerator.registerInMesh()creates/links a remoteNodeConfigentry. - Health check: mesh registration flow polls
checkNodeHealth()until online or timeout.
Port convention:
- Managed Docker mesh-node containers default to
4041(DEFAULT_CONTAINER_PORTinmesh-config-generator.ts). 4040remains reserved for the production dashboard and should not be documented as the managed mesh-node default.
Memory System
Fusion uses OpenClaw-style project memory files and separates memory into two responsibilities:
- Layered backend runtime memory (
memory-backend.ts,project-memory.ts)- canonical long-term + layered memory access used by agents and dashboard APIs
- Insight extraction automation (
memory-insights.ts,InsightStore)- scheduled extraction/pruning workflows over project memory plus insight/audit artifacts
Both systems currently use .fusion/memory/MEMORY.md as the canonical working source-of-truth.
Primary memory files:
- Long-term:
.fusion/memory/MEMORY.md - Daily notes:
.fusion/memory/YYYY-MM-DD.md - Dream processing:
.fusion/memory/DREAMS.md
Memory subsystems:
memory-backend.ts— backend contracts + file/readonly/qmd implementationsmemory-compaction.ts— summarization/compaction automationmemory-dreams.ts— background dream processing for agent and project memorymemory-insights.ts+InsightStore— extracted insight synthesis and persistent insight/run storage
Pluggable backends (memory-backend.ts):
| Backend | Type | Capabilities |
|---|---|---|
FileMemoryBackend |
file |
Read/Write, Atomic writes, Persistent |
ReadOnlyMemoryBackend |
readonly |
Read only, Non-persistent |
QmdMemoryBackend |
qmd |
Read/Write, Persistent, CLI-based with file fallback |
Backend registration:
import { registerMemoryBackend, resolveMemoryBackend } from "@fusion/core";
// Register custom backend
registerMemoryBackend(customBackend);
// Resolve based on settings
const backend = resolveMemoryBackend(settings);
Settings integration:
memoryEnabled: Toggle controls whether memory instructions are injected into promptsmemoryBackendType: Select which backend to use (file,readonly,qmd, or custom). Unknown types are accepted and persisted verbatim; runtime resolution falls back toDEFAULT_MEMORY_BACKEND(qmd).
QMD Backend Behavior:
The QMD backend (qmd) delegates read/write I/O to the file backend and schedules background QMD index refreshes. For search, it attempts QMD query first and falls back to local .fusion/memory/ file search when QMD is unavailable, errors, or returns no matches.
QMD-backed memory behavior also applies to agent-private memory workspaces under .fusion/agent-memory/{agentId}/:
- Agent memory search normalizes QMD hit paths (including
qmd://..., absolute paths, and relative filenames) into canonical readable workspace paths (MEMORY.md,DREAMS.md,YYYY-MM-DD.md) so results can be passed directly intofn_memory_get. - Agent-memory writes from tool and non-tool paths (including
processAgentMemoryDreams()) schedule agent-specific QMD refreshes so new dreams/long-term updates remain discoverable without manual reindexing.
Dashboard API:
GET /api/memory/backend— Returns current backend status and capabilities
See Memory Plugin Contract for the full plan.
5) Engine Package (@fusion/engine)
@fusion/engine executes the autonomous workflow.
Agent roles
- Planning: the planning processor generates task plans (
PROMPT.md) and selects eligible planning tasks by priority first, then FIFO (createdAtascending) within each priority tier. - Executor:
TaskExecutor(executor.ts) implements tasks in worktrees - Reviewer:
reviewStep()(reviewer.ts) performs plan/code/spec reviews - Merger:
aiMergeTask()(merger.ts) merges approved work
Reviewer verdict recovery contract (FN-4092)
- Reviewer verdicts are
APPROVE,REVISE,RETHINK, orUNAVAILABLE. - For non-pause
UNAVAILABLEor non-context reviewer prompt errors,reviewStep()retries once:- Prefer configured validator fallback model (
validatorFallbackProvider+validatorFallbackModelId, including project overrides), or - Retry once on the same model with stricter
Verdict:output instructions when no fallback model is configured.
- Prefer configured validator fallback model (
- Pause/engine-pause short-circuits still return
UNAVAILABLEimmediately and do not spawn/retry reviewer sessions. - Executor handling in
createReviewStepTool()is now explicit:plan/specUNAVAILABLEis advisory after retry exhaustion (UNAVAILABLE (advisory)), and execution proceeds.codeUNAVAILABLEremains blocking; step completion must wait for a usable review verdict.- Advisory and blocking paths are both logged to task logs for operator visibility.
Scheduling and execution
Scheduler(scheduler.ts) — dependency-aware task scheduling that dispatches eligible todo tasks by priority first, then FIFO (createdAtascending) within each priority tier.blockedByinvariant (FN-3924/FN-4091): the field is only durable when it references a current unresolved explicit dependency (or, for dependency-free tasks, an active overlap blocker). Completion gating now validatesblockedBythrough live task resolution: missing blockers and blockers already indone/archivedare treated as stale, while only still-active blockers continue to preventfn_task_done. If no current blocker remains, scheduler/event reconciliation clearsblockedBytonulland re-evaluates from live task state.
BlockedBy stamping invariants
- Scheduler writes overlap-based
blockedByonly when overlap gating is active and there is a live overlapping active scope; otherwise overlap logic does not stamp blockers. - Active overlap scopes exclude permanently-failed
in-reviewtasks (status === "failed", typically produced bycheckStuckBudget()afterstuckKillCount > maxStuckKills) so superseding re-implementation tasks are not indefinitely queued behind work that will never merge. (FN-4200) - Stamping is sticky when valid (FN-3899): if a todo task is already
queuedbehind a blocker that is still active and still overlaps, the scheduler preserves that blocker and skips rewrites. - When the blocker must change, selection is deterministic: active overlap candidates are ordered by task ID and the first overlapping task is chosen, removing tick-order churn.
- Writes are idempotent: scheduler updates
status/blockedByonly when values change, reducing per-tick churn and audit noise. - Self-healing remains responsible for terminal/missing blocker cleanup (
clearStaleBlockedBy()), while scheduler overlap stamping now focuses on stable active-overlap attribution. StepSessionExecutor(step-session-executor.ts) — per-step sessions + parallel wave executioncreateTaskUpdateTool()(executor.ts) emits a diagnostic warning when an agent marks step Nin-progresswhile another step on the same task is alreadyin-progress; the update still proceeds so operators get evidence without changing task semantics.TaskCompletion(task-completion.ts) — completion gate helpersSpecStaleness(spec-staleness.ts) — stale spec detection utilitiesMissionExecutionLoop(mission-execution-loop.ts) — validator/fix loop orchestrationMissionFeatureSync(mission-feature-sync.ts) — feature↔task status synchronizationMissionAutopilot(mission-autopilot.ts) — mission slice auto-progression
Routine + cron automation
RoutineRunner(routine-runner.ts) — executes routine stepsRoutineScheduler(routine-scheduler.ts) — schedules due routinesCronRunner(cron-runner.ts) — cron-based AI/script jobs
Execution context + skills
SkillResolver(skill-resolver.ts) — resolves active skill sets for sessionsSessionSkillContext(session-skill-context.ts) — skill context materialization per runContextLimitDetector(context-limit-detector.ts) — context-window pressure checksTokenCapDetector(token-cap-detector.ts) — token-cap enforcement checksPluginRunner(plugin-runner.ts) — runtime plugin callback executionAgentRuntime(agent-runtime.ts) — runtime adapter interface contractRuntimeResolution(runtime-resolution.ts) — runtime selection and fallback logicAgentSessionHelpers(agent-session-helpers.ts) — runtime-aware session creation helpersAgentActionGate(agent-action-gate.ts) — permanent-agent runtime action classification + policy disposition decisions (shared classification source:packages/engine/src/gating-classifications.ts)
Runtime action-gate flow (v1):
- Tool execution wrappers in
pi.tscomposewrapToolsWithBoundary()andwrapToolsWithActionGate(). - Non-ephemeral agents receive
AgentActionGateContextfrom executor/heartbeat session creation. blockandrequire-approvaldispositions intercept before tool side effects.require-approvalpersists durable requests viaApprovalRequestStore, reusing pending requests by dedupe key intargetAction.context.approvalDedupeKey.
Concurrency, recovery, and resiliency
AgentSemaphore(concurrency.ts) — slot acquisitionRecoveryPolicy(recovery-policy.ts) — retry/recovery decision policyStuckTaskDetector(stuck-task-detector.ts) — inactivity/loop stall detectionGridlockDetector(gridlock-detector.ts) — detects all-blocked todo pipelines and emits notification events (plus explicit clear signals when gridlock resolves)TransientErrorDetector(transient-error-detector.ts) — retriable error classificationSelfHealingManager(self-healing.ts) — auto-unpause/maintenance recovery actionsrecoverGhostReviewTasks()is a fallback only for idle, non-terminalin-reviewstates. Terminal/actionable states (notablystatus: "failed") are preserved and not auto-kicked back totodo.
Stuck-loop exhaustion terminal contract
When stuck-kill retries are exhausted, checkStuckBudget() marks the task status: "failed", moves it to in-review, and writes an error that starts with STUCK_LOOP_EXHAUSTED:. The error and final task-log line both include the kill count/max and last stuck reason (loop or inactivity). StuckTaskDetector also untracks the task and refuses to re-track it while that failed terminal error remains, preventing further automatic kill/requeue churn. The final log line explicitly states that no further automatic retries will run and directs operators to manually retry, pause, or move the task back to triage to resume work.
recoverMissingWorktreeReviewFailures()is a narrow failed-review recovery: onlystatus: "failed"in-reviewtasks with the explicit session-start signatureRefusing to start coding agent in missing worktree:(fromassertValidWorktreeSession()) are requeued. Recovery clears stale session metadata (worktree,branch,sessionFile, transient failure state), preserves valid step progress/retry counters, logs the auto-recovery reason, and moves the task back totodofor a clean retry.recoverMergeableReviewTasks()only re-enqueues truly eligible tasks; retry-exhausted review tasks are skipped to avoid re-enqueue/no-op loops that keep refreshingupdatedAt.recoverAlreadyMergedReviewTasks()auto-finalizes retry-exhaustedin-reviewtasks when self-healing can prove their work already landed on the merge target, but it still honorsgetTaskMergeBlocker()beforein-review→done. If a blocker remains (for example, incomplete steps), the task is surfaced in stablein-review/failedstate with a blocker error instead of entering an auto-finalize loop.
Orphan-only scope-violation auto-recovery
recoverOrphanOnlyScopeViolations() handles the narrow FN-4350 shape without weakening the file-scope invariant: it runs only when all of these predicates hold — task is column === "in-review"; task is failed (status === "failed", not paused, and engine/global pause are both off); error evidence is a FileScopeViolation (tool_error agent-log payload from formatFileScopeViolationAgentLog, with task.error prefix fallback); task.scopeOverride !== true; task is not actively executing and mergeDetails.mergeConfirmed !== true. It then verifies the task's specific work is already on main using findAlreadyMergedTaskCommit (Fusion-Task-Id trailer / ancestry / patch-id / tree-equality proof). Only when staged files are orphan-only (no declared-scope overlap after excluding .changeset/*) and main-branch proof is positive does it finalize as a no-op (resolutionStrategy: "orphan-discard-no-op"), append an explicit auto-recovery log line, and tear down the task worktree so orphan staging is discarded.
Guardrails: this routine does not retry merges, does not apply to mixed/non-orphan staging, and does not run when no landed-work proof exists (FN-4280 class protection).
- FN-4285 decision: add a follow-up for a tree-equality recovery strategy (
rev-parse <base>^{tree}==<task-branch>^{tree}) infindAlreadyMergedTaskCommit. This closes stranded already-merged branches that evade trailer/ancestry/patch-id matching, with guardrails limited to retry-exhausted review tasks to avoid false positives during transient post-rebase parity windows. - No-
fn_task_donerecovery classification is normalized across executor, restart recovery, and self-healing: detection keys on executor-emitted"without calling fn_task_done"strings (while still tolerating legacytask_donewording), then applies the bounded ladder deterministically (in-session retries → bounded todo requeues with preserved progress when appropriate → terminal surfaced failure when budget is exhausted). clearStaleBlockedBy()clearsblockedBy(and transientstatus) on todo tasks when their blocker is missing, done, archived, paused in-review, or failed in-review with merge retries exhausted. FN-3924 extends this with a dependency-integrity guard: if a task has explicit dependencies andblockedByis not one of the currently unresolved deps, the stale marker is cleared. FN-4091 broadens the sweep to activein-progressand un-pausedin-reviewtasks as well, but those repairs only nullblockedBy(they do not rewrite scheduler-owned queued state). This repairs rows corrupted by historical overlap re-stamping and lets scheduler re-evaluate from live dependency state. The ad-hocscripts/recover-stale-blocked-by.mjsremains a manual backstop for filesystem/db audits, not the primary repair path.- Together,
recoverAlreadyMergedReviewTasks(),clearStaleBlockedBy(), and paused-aware in-review scheduling prevent merge-deadlock loops by finalizing already-landed work, clearing stale dependency blockers, and avoiding paused review cards re-blocking overlap dispatch. - Merge commit attribution is ownership-aware: a
mergeDetails.commitShais trusted only when reachable fromHEADand attributable to the task viaFusion-Task-Idtrailer or task-ID-bearing subject. Reachable-but-unowned SHAs are rejected to prevent sibling done tasks from sharing misleading merge metadata. ProjectEnginesettings lifecycle handlers (project-engine.ts) treatenginePausedas a soft pause: clearing it dispatches runtime resume and, whenautoMergeis enabled, performs anin-revieweligibility sweep to requeue mergeable review tasks.UsageLimitPauser(usage-limit-detector.ts) andwithRateLimitRetry(rate-limit-retry.ts)
Worktree and naming helpers
WorktreePool(worktree-pool.ts) — idle worktree reuseWorktreeNames(worktree-names.ts) — deterministic worktree/branch naming
Observability and reflection
AgentLogger(agent-logger.ts) — structured per-agent run loggingRunAudit(run-audit.ts) — mutation audit tracking (DB/git/filesystem)Notifier(notifier.ts) — legacy ntfy compatibility shim (NtfyNotifier) plus shared ntfy helpers- Runtime ownership:
NtfyNotifierno longer owns an independent task-lifecycle listener graph;ProjectEngineinjects the canonicalNotificationServiceinstance so task lifecycle notifications (task:moved,task:updated,task:merged) are emitted through a single path. - Merge dedup safety:
ProjectEngine.start()is idempotent, so repeated start calls do not wire a secondNotificationService/NtfyNotifierpair. A successful merge therefore emits exactly one canonicalmergedntfy lifecycle notification per task. - Compatibility scope:
NtfyNotifierremains responsible for gridlock-only compatibility notifications (notifyGridlock) and legacy helper APIs. - Legacy gridlock ntfy delivery is cooldown-throttled: first detection notifies immediately, subsequent detections are suppressed for 15 minutes (even if blocked-task membership changes), and the cooldown resets as soon as gridlock fully clears.
- Runtime ownership:
NotificationService(notification/notification-service.ts) — provider lifecycle + event dispatch orchestration- Subscribes to task lifecycle events plus mailbox and memory events.
message:sentdispatchesmessage:agent-to-userandmessage:agent-to-agentnotification events (with message metadata for deep-links), and manualPOST /api/memory/dreamprocessing emitsstore.emit("memory:dreams-processed", payload)when new DREAMS content is written.
- Subscribes to task lifecycle events plus mailbox and memory events.
NotificationProviderinterface (@fusion/corenotification/provider.ts) — pluggable provider contract- Built-in providers:
NtfyNotificationProvider(notification/ntfy-provider.ts),WebhookNotificationProvider(notification/webhook-provider.ts) AgentReflection(agent-reflection.ts) — reflection extraction and persistence
Heartbeat execution
Implemented in agent-heartbeat.ts:
HeartbeatMonitorHeartbeatTriggerScheduler(timer, assignment, on-demand triggers)WakeContext/ per-agent runtime config support
Node/mesh runtime services
NodeHealthMonitor(node-health-monitor.ts) — remote node liveness/metrics checksPeerExchangeService(peer-exchange-service.ts) — peer sync orchestrationMeshLeaseManager(mesh-lease-manager.ts) — canonical abandoned-lease detection + recovery path
Outage ownership boundaries (degraded reads + queued write replay)
CentralCoreowns durable outage state in central persistence (meshSharedSnapshots+meshWriteQueue) and exposes stable assertion methods:recordMeshSnapshot,getLatestMeshSnapshot,enqueueMeshWrite,listPendingMeshWrites,markMeshWriteReplayStarted,markMeshWriteApplied,markMeshWriteFailed, andgetMeshDegradedReadState.PeerExchangeServiceowns retryability classification for sync/apply failures, queue insertion for retryable failures, replay execution (replayPendingWritesForNode(targetNodeId)), and observable sync results (queuedWriteId,replaySummary) for partition/replay assertions.NodeHealthMonitorprovides liveness transitions as replay hints only via deterministic recovery callbackonNodeRecovered(nodeId, previousStatus);onlineis a trigger to attempt replay, not proof that replay succeeded.- Dashboard mesh routes (
register-mesh-routes.ts) preserveGET /api/mesh/statearray shape and attach per-node degradedreadStatemetadata so stale fallback data is explicit during partitions.
Mesh task lease ownership and recovery
Task ownership is persisted in shared task metadata so all nodes agree on one canonical lease view. The persisted lease fields are:
checkedOutBy— owning agent id (compatibility field)checkedOutAt— lease acquisition timestamp (compatibility field)checkoutNodeId— owning node idcheckoutRunId— active owning heartbeat/executor run id when knowncheckoutLeaseRenewedAt— last successful lease renewal timestampcheckoutLeaseEpoch— monotonic fencing generation used to reject stale owners after recovery
AgentStore.checkoutTask() remains the compatibility entrypoint for ownership claims, but lease replacement is fenced by epoch semantics: only the same live owner can renew idempotently, and stale owner replacement is performed only through the recovery path.
MeshLeaseManager.recoverAbandonedLease(taskId, reason, context) is the single canonical abandoned-work path used by scheduler/self-healing/runtime orchestration. Recovery validates staleness, bumps checkoutLeaseEpoch, clears active-owner fields, logs the reason, and re-queues work for scheduler visibility.
A lease is recoverable only when there is no active local executor session for that task and either:
- the owning node is
offlineorerror, or - the owner heartbeat/run age exceeds
max(agentHeartbeatTimeoutMs * 2, 120_000)measured against the most recent lease renewal timestamp.
- Canonical replication/write-coordination contract:
docs/shared-mesh-protocol.md- Defines protocol versioning, write classes, quorum/ack semantics, lease epochs/fencing, offline queue/replay, reconciliation outcomes, restart recovery hooks, and degraded-read staleness metadata.
- Existing
/api/mesh/syncand settings-sync payloads remain the active exchange primitives while follow-on runtime tasks implement full v1 coordinator/quorum behavior.
- Distributed task-ID allocation (
packages/core/src/distributed-task-id.ts) is the first mesh-aware coordinated write primitive.- Durable state lives in SQLite tables
distributed_task_id_state(prefix sequence + authoritative committed count) anddistributed_task_id_reservations(reservation lifecycle rows). - Reserve/commit/abort execute under a process-local lock and a single SQLite transaction. Lazy reservation expiry cleanup runs inside those same transactions.
- Default reservation TTL is
15 * 60 * 1000ms (15 minutes). Expired/aborted reservations are burned IDs and are never reissued. committedClusterTaskCountfrom allocator state is the only authoritative cluster-wide committed-task count. Local task-row counts and ID suffix math are not authoritative.- Store open reconciles every known prefix in
distributed_task_id_statetomax(current nextSequence, max(tasks suffix)+1, max(archivedTasks suffix)+1, max(reservation sequence)+1). This self-heals stale counters before ordinary task creation resumes. - Mesh allocator write routes (
/api/mesh/task-ids/reserve|commit|abort) return503when the coordinator node is unreachable; they never fall back to local-only cluster ID issuance.
- Durable state lives in SQLite tables
- Cluster task creation now uses a strong-write reserve → create → replicate → commit/abort sequence.
- Ordinary local task creation (
TaskStore.createTask(), duplicate, and refine flows) now allocates IDs through the same distributed reserve/commit/abort lifecycle owned byTaskStore. POST /api/tasksuses the store-owned allocator path for local creates rather than maintaining a separate route-local allocator implementation.POST /api/tasksreserves a distributed ID, creates the authoritative local task with that reserved ID, then POSTs authenticated replication payloads to peer nodes.- All create-class writes now use conflict-raising inserts, not SQLite
ON CONFLICT ... DO UPDATE. Existing task rows and.fusion/tasks/{id}contents always win over stale counters or colliding reservations. - Local create paths perform a final active+archived existence check immediately before insert. If a reserved
FN-*still collides, the reservation is aborted/burned and the create fails loudly instead of rewriting the existing task. - Creation self-heals stale overlap state at the route layer: if a reserved
FN-*collides with an existing task (Task ID already exists...or replicated-create collision), the route aborts that reservation, cleans up partial local state, reserves the next ID, and retries up to a bounded limit. - Replica apply uses
TaskStore.applyReplicatedTaskCreate(...), which is idempotent by task ID: replaying the same payload returns the existing task without creating duplicates. - If an incoming replicated payload conflicts with a different existing task record for the same ID, the apply path returns a deterministic collision error instead of overwriting data.
- Any replication/coordinator failure aborts the reservation and returns write failure (
503), so this path does not report success for local-only partial writes.
- Ordinary local task creation (
- Process lifecycle ownership:
fn serve/fn dashboardstart a single process-levelPeerExchangeServiceand stop it during shutdown.CentralCore.startDiscovery()is invoked from CLI startup only after HTTP bind completes so discovery advertises the actual listening port.InProcessRuntimestays project-scoped and intentionally does not own mesh startup/shutdown.
Remote access runtime
Operator setup + troubleshooting guide: Remote Access runbook.
remote-access/tunnel-process-manager.tsowns tunnel lifecycle orchestration withspawn-based, non-blocking process supervision.remote-access/types.tsdefines the runtime contract used by downstream API/TUI/headless layers:- Providers:
"tailscale" | "cloudflare" - Lifecycle states:
"stopped" | "starting" | "running" | "stopping" | "failed" - Error codes:
invalid_config,start_failed,stop_failed,switch_failed,readiness_timeout,process_exit, etc.
- Providers:
remote-access/provider-adapters.tsprovides provider-specific command composition + readiness parsing while enforcing config validation.- Cloudflare has two command variants:
- Named tunnel mode:
cloudflared tunnel --no-autoupdate run <tunnelName>(token from env) - Quick tunnel mode:
cloudflared tunnel --url http://localhost:<dashboardPort>(ephemeraltrycloudflare.comURL, no token)
- Named tunnel mode:
- Credential inputs are reference-based (
tokenEnvVar,credentialsPath) and validated without logging raw secret values. - Redaction is applied to command previews and emitted log lines before publishing status/log events.
- Deterministic stop semantics: graceful shutdown (
SIGTERM) first, bounded wait, then force-kill fallback (SIGKILL). - Safe provider switching is stop-first: active provider fully stops before target start is attempted; failed starts emit
switch_failedterminal status. ProjectEngine.start()instantiates a per-project tunnel manager and applies startup restore policy fromremoteAccess.lifecycle:- restore is attempted only when
rememberLastRunningis true, a prior-running marker exists, provider config is valid, and runtime prerequisites are available. - restore skips/failures are non-fatal to engine startup and clear stale running markers to avoid restart loops.
- restore is attempted only when
- Manual lifecycle remains explicit: only
startRemoteTunnel()/stopRemoteTunnel()transitions mutate runtime state; provider/settings updates do not auto-start tunnels. ProjectEngineexposes restore diagnostics viagetRemoteTunnelRestoreDiagnostics()(applied|skipped|failed+ machine-readable reason).
Multi-runtime support + IPC
- Runtime contracts:
project-runtime.ts - Orchestration:
ProjectManagerandHybridExecutor - Runtime implementations:
runtimes/in-process-runtime.tsruntimes/child-process-runtime.tsruntimes/remote-node-runtime.ts
- IPC protocol/transport:
ipc/ipc-protocol.tsipc/ipc-host.tsipc/ipc-worker.ts- worker entrypoint:
runtimes/child-process-worker.ts
6) Dashboard Package (@fusion/dashboard)
Server layer
- Entry exports:
packages/dashboard/src/index.ts - Main server factory:
createServer()inpackages/dashboard/src/server.ts - Primary API router:
createApiRoutes()inpackages/dashboard/src/routes.ts
Key server capabilities:
- REST APIs for tasks, git, GitHub, agents, missions, planning, automations/routines, settings
- System stats snapshot and vitest process controls APIs (
GET /api/system-stats,POST /api/kill-vitest) exposing dashboard process/system telemetry (including app CPU percentage and host memory rendered as numeric values with visual usage bars in the System Stats modal), task/agent aggregates, and manual vitest process termination - Remote access APIs (
/api/remote/*) for provider config, activation, tunnel lifecycle, status, token issuance, authenticated URL generation, and QR payload generation- Operational runbook (prereqs/security/troubleshooting):
docs/remote-access.md /api/remote/tunnel/start,/api/remote/tunnel/stop, and/api/remote/tunnel/kill-externalcover tunnel lifecycle and external funnel cleanup./api/remote/statusincludes tunnel status, external funnel detection (externalTunnelwhen managed tunnel is stopped), plus restore diagnostics (restore.outcome+restore.reason) with parity between dashboard and headlessfn serveruntimes.
- Operational runbook (prereqs/security/troubleshooting):
- Remote auth handoff endpoints:
POST /api/remote-access/auth/login-url(daemon-auth protected) issues a tokenized phone-login URL for eitherpersistentorshort-livedmode.GET /remote-login?rt=<token>(public) validates remote token strategy and redirects to dashboard auth handoff (/?token=<daemonToken>when daemon auth is enabled, otherwise/).- Invalid/missing/expired remote tokens return
401JSON with deterministic codes:remote_token_invalid,remote_token_missing,remote_token_expired.
- Chat APIs (
/api/chat/*) with streaming response support (routes.ts,chat.ts) - Dev-server lifecycle + persistence APIs (
/api/dev-server/*) backed by:dev-server-routes.ts(router factory + per-project runtime registry)dev-server-process.ts(DevServerProcessManagerfor spawn/stop/restart/url-detection)dev-server-store.ts(durable.fusion/dev-server.jsonstate + log ring buffer)dev-server-detect.ts(project/workspace script auto-detection + confidence scoring)- Note: this hyphenated
dev-server-*family is the canonical runtime owner today; seedocs/dev-server-module-boundary-audit.mdfor the FN-2212 boundary/consolidation audit covering paralleldevserver-*modules.
- Plugin management routes (
plugin-routes.ts) - Insights routes (
insights-routes.ts) - Evals routes (
evals-routes.ts) —/api/evalsread surface for eval result listing/filtering, drill-down detail, and eval run metadata - Research routes (
research-routes.ts) —/api/researchsurface for runs, details, cancel/retry, exports, create-task, and attach-task actions; supports graceful degradation envelopes via availability payloads when capabilities are unavailable - Plugin-defined roadmap routes under
plugin-routes.tsdispatch (/api/plugins/fusion-plugin-roadmap/...) - Project-scoped store reuse via
project-store-resolver.ts - Rate limiting (
rate-limit.ts) - Static SPA hosting (Vite build output)
Runtime diagnostics logging contract
- Dashboard/server runtime diagnostics use the shared
RuntimeLoggercontract (packages/dashboard/src/runtime-logger.ts) instead of ad hocconsole.*calls. createServer()acceptsServerOptions.runtimeLogger; when omitted it defaults to a console-backed logger, preserving readable output in non-TTY/headless modes.- CLI TTY dashboard sessions inject a logger backed by
DashboardLogSink, so runtime diagnostics from server/routes are captured in the TUI log buffer. - Sensitive remote-auth material is never logged raw; route/UI responses mask persistent token values unless explicitly requested by token-generation actions.
- Short-lived remote auth tokens are runtime-ephemeral (in-memory only, cleared on process restart) and TTL-enforced server-side against persisted
remoteAccess.tokenStrategy.shortLived.ttlMsplus issued expiry metadata. - Remote login links carry auth material in query params (
rtthentokenon redirect). Treat links/QR screenshots as secrets: they can leak through history, screenshots, and chat logs; prefer short-lived mode for sharing. - Intentional startup/banner text in
fn dashboardandfn serveremains direct plain output for readability and backward-compatible scripting behavior.
Headless Node Mode (fn serve / fn daemon)
- Headless runtimes now auto-register the current working directory as a project when it is missing from central registry metadata, then continue normal engine startup.
- First-run auto-bootstrap logs one line:
[serve] Auto-registered project "<name>" at <cwd>(or[daemon] ...). - This enables first-run startup in CI/Docker/cron without requiring a prior
fn initorfn project add. - Pass
--no-auto-registerto either command to preserve legacy strict behavior.
Real-time channels
- SSE:
/api/events(sse.ts)- Emits
task:*, mission events, AI session updates, automation schedule events (schedule:created,schedule:updated,schedule:deleted,schedule:run), and research run lifecycle events (research:run:created,research:run:updated,research:run:completed,research:run:failed,research:run:cancelled) when available - Project-scoped: resolves project context from query param or engine manager
- Canonical maintainer contract (ownership/lifecycle/scoping/pitfalls and shared-vs-dedicated stream boundaries):
docs/dashboard-realtime.md
- Emits
- Chat streaming:
/api/chat/sessions/:id/messages(routes.ts+chat.ts)- Streams assistant responses as SSE events for chat sessions
doneevents include the authoritative persisted assistant message snapshot (message) so clients can render final output even when incrementaltextdeltas are absenterrorevents now allow either the legacy string payload or a structured failure payload matching persistedmetadata.failureInfo; direct-chat clients normalize both shapes and render failures inline in the thread
- Chat session queries:
/api/chat/sessions(routes.ts)- Existing list behavior is unchanged (
status=active|archived|allreturns an array) - Quick Chat resume uses targeted lookup params:
agentId, optionalmodelProvider+modelId, plusresume=1 - Validation requires
modelProviderandmodelIdtogether; partial model pairs return400 - Targeted lookup returns only the newest matching active session (or
null) to avoid scanning every active session client-side
- Existing list behavior is unchanged (
- Chat Room API:
/api/chat/rooms*(register-chat-room-routes.ts)GET /api/chat/rooms→200 { rooms }; query supportsprojectId,status, andagentIdPOST /api/chat/rooms→201 { room, members }; validatesname, returns409on slug collisionsGET/PATCH/DELETE /api/chat/rooms/:id→ room read/update/delete (404for unknown room)GET/POST/DELETE /api/chat/rooms/:id/members[/:agentId]→ member list/add/remove (400for invalid body,404for unknown room/member)GET /api/chat/rooms/:id/messages+POST /api/chat/rooms/:id/messages+DELETE /api/chat/rooms/:id/messages/:messageId- Room message POST persists the user room message (
201 { message }), rejects non-nullsenderAgentIdfor user submissions, then triggers server-side room responder execution that persists assistant room replies viachatStore.addRoomMessage(...)
- Room message POST persists the user room message (
POST /api/chat/rooms/:id/messages/:messageId/attachmentsrecords attachment metadata on an existing room message- Error contract follows existing API patterns:
400validation failures,404missing resources,409duplicate-slug conflicts,503when chat store is unavailable - SSE fan-out on
/api/eventsnow includes:chat:room:created,chat:room:updated,chat:room:deleted,chat:room:member:added,chat:room:member:removed,chat:room:message:added,chat:room:message:updated,chat:room:message:deleted - Room test coverage (planned): FN-3812 tracks the contract-first test matrix for room creation/switching, persisted history, mention routing, and hybrid responder behavior. See
.fusion/tasks/FN-3812/test-plan.mdplus scaffold files:packages/core/src/__tests__/chat-store.rooms.test.ts,packages/dashboard/src/__tests__/chat.rooms.test.ts,packages/dashboard/src/__tests__/chat-routes.rooms.test.ts, andpackages/dashboard/app/components/__tests__/ChatView.rooms.test.tsx.
- Task log stream:
/api/tasks/:id/logs/stream(server.ts)- SSE endpoint for live task log streaming with project scope resolution
- Dev-server stream:
/api/dev-server/logs/stream(dev-server-routes.ts)- SSE stream emits
history,log,stopped, andfailedevents - initial connection replays persisted
logHistoryand then follows live process output - companion endpoints:
/api/dev-server/detect,/config,/status,/start,/stop,/restart,/preview-url
- SSE stream emits
- Badge WebSocket:
/api/ws(server.ts,websocket.ts)- Scope-keyed channels (
badge:{scopeKey}:{taskId}) prevent cross-project collisions
- Scope-keyed channels (
- Terminal WebSocket:
/api/terminal/ws(server.ts,terminal-service.ts)- Project-scoped terminal session validation + safe unscoped fallback
Frontend SPA layer
- App entry:
packages/dashboard/app/main.tsx - Root composition:
packages/dashboard/app/App.tsx - Core board components:
Board.tsx,Column.tsx,TaskCard.tsx,TaskDetailModal.tsx,ListView.tsx - Board column ordering (board view only):
todocards mirror scheduler pickup order (priority descending, thencreatedAtascending/FIFO within each priority tier, then task ID ascending).triage,in-progress, andarchiveduse priority descending then task ID ascending, with missing/invalid priority normalized tonormal.doneis completion-recency ordered (columnMovedAt, thenupdatedAt, thencreatedAt, newest first). Inin-review, merge-active tasks (status === "merging","merging-pr", or"merging-fix") are pinned above non-merging tasks, with priority-then-ID ordering within each group. - Task detail surface is shared through
TaskDetailContent(exported fromTaskDetailModal.tsx): desktop/tabletListViewrenders it inline in the split right pane, while mobile and non-list entry points continue usingTaskDetailModal. - In desktop split mode,
ListViewnow uses a compact sidebar-first control layout (count/actions/summary chips + collapsible "View options" panel) to keep list controls dense alongside the inline detail pane; mobile keeps the card-first flow with a toolbar "View options" entry point for the same visibility/filter toggles. - Chat system UI:
ChatView.tsx,QuickChatFAB.tsx - Planning/insight UI:
MissionManager.tsx,TodoView.tsx,InsightsView.tsx,DocumentsView.tsx(roadmap view is plugin-owned) - Dev server UI:
DevServerView.tsx(controls + status/log panel + embedded preview with iframe fallback messaging)
CSS Architecture
The dashboard's CSS is split between a consolidated global stylesheet and modular per-component files:
- Global stylesheet (
packages/dashboard/app/styles.css, ~4,500 lines)- Design tokens (spacing, colors, shadows, transitions, fonts)
- Primitive component classes (
.btn,.card,.modal,.form-input) - Cross-component
@mediaoverrides and breakpoint definitions
- Per-component stylesheets (56+ files in
packages/dashboard/app/components/)- Each component has a co-located
ComponentName.cssfile - Each
ComponentName.tsximports its stylesheet:import "./ComponentName.css"; - Component-specific CSS rules live in the component's
.cssfile, not in the root stylesheet
- Each component has a co-located
Lazy-loaded views (bundle size optimization):
The following 15 views are lazy-loaded via React.lazy() with <Suspense fallback={null}>:
AgentsView,TodoView,NodesView,ChatView,MemoryView,ResearchViewDevServerView,InsightsView,DocumentsView,SkillsViewSetupWizardModal,PluginManager,PiExtensionsManager, `AgentDetailView
A prefetchLazyViews() function runs once on mount via requestIdleCallback to warm chunks. Do not make these views eager — bundle size is carefully managed.
Key hooks
- Task + realtime:
useTasks.ts,useBadgeWebSocket.ts,useAiSessionSync.ts - Chat:
useChat.ts,useQuickChat.ts - Documents/insights/memory:
useDocuments.ts,useInsights.ts,useMemoryBackendStatus.ts,useMemoryData.ts - Plugin roadmap state/hooks: owned by
plugins/fusion-plugin-roadmap/src/dashboard/* - Dev server:
useDevServer.ts(status hydration, command controls, reconnect stream handling, project-scope reset) - Project/agents/setup:
useProjects.ts,useCurrentProject.ts,useAgents.ts,useSetupReadiness.ts - UX/platform helpers:
useFavorites.ts,useAuthOnboarding.ts,useDeepLink.ts,useTerminal.ts
Planning and decomposition features
- Backend planners:
planning.ts,subtask-breakdown.ts(roadmap suggestion generation is plugin-owned) - UI modals:
PlanningModeModal.tsx,SubtaskBreakdownModal.tsx, milestone interview flows - Multi-task creation endpoints are wired under planning/subtask routes in
routes.ts
Health and monitoring endpoints
- Health check:
GET /api/health- Returns liveness status for load balancers and monitoring
- Response:
{ status: "ok" | "degraded", version: string, uptime: number, database: { healthy: boolean, isRunning: boolean, lastCheckedAt: string | null } } - Startup does not block on full
PRAGMA integrity_check(100); Fusion schedules it in the background shortly after boot. - Background integrity checks are deduplicated process-wide per on-disk SQLite path: multiple
Databaseinstances sharing the samefusion.dbjoin one shared run, and each instance still updates the underlying integrity state (integrityCheckPending,integrityCheckLastRunAt,corruptionDetected) that maps todatabase.isRunning,database.lastCheckedAt, anddatabase.healthy. - No authentication required
Custom Provider endpoints
Custom-provider settings routes are registered in register-custom-provider-routes.ts.
| Method | Path | Description |
|---|---|---|
| GET | /api/custom-providers |
List configured custom providers from global settings with API keys masked in the response payload. |
| POST | /api/custom-providers |
Create a custom provider (name, apiType, baseUrl, optional apiKey and models) and return the new provider with masked API key. |
| PUT | /api/custom-providers/:id |
Update an existing custom provider by ID (partial updates supported) and return the sanitized provider payload. |
| DELETE | /api/custom-providers/:id |
Delete a custom provider by ID and return a success envelope. |
Project/node path-mapping endpoints
| Method | Path | Description |
|---|---|---|
| GET | /api/projects/:id/path-mappings |
List persisted per-node absolute paths for a project (projectNodePathMappings rows keyed by projectId + nodeId). |
| GET | /api/projects/:id/path-mappings/:nodeId |
Fetch one project↔node mapping row. |
| PUT | /api/projects/:id/path-mappings/:nodeId |
Upsert one mapping row (path body field must be absolute). |
| DELETE | /api/projects/:id/path-mappings/:nodeId |
Delete one mapping row if present. |
| GET | /api/nodes/:id/path-mappings |
List all project mappings known for a node. |
This API surface is intentionally separate from projects.nodeId (runtime host placement metadata) and from task-level routing defaults (defaultNodeId / Task.nodeId).
Dashboard node onboarding (AddNodeModal → useNodes.register) uses a two-phase flow:
- Register node metadata first via
POST /api/nodes. - Persist selected project↔node path mappings with per-project
PUT /api/projects/:id/path-mappings/:nodeIdupserts.
The client treats mapping persistence as part of onboarding success. If mapping writes fail after node creation, onboarding attempts rollback via DELETE /api/nodes/:id and refreshes node state to avoid a silent half-configured node.
Node settings sync and update-check endpoints
| Method | Path | Description |
|---|---|---|
| GET | /api/nodes/:id/settings |
Fetch settings from a remote node. |
| POST | /api/nodes/:id/settings/push |
Push local settings to a remote node. |
| POST | /api/nodes/:id/settings/pull |
Pull settings from a remote node. |
| GET | /api/nodes/:id/settings/sync-status |
Get sync status and diff summary. |
| POST | /api/nodes/:id/auth/sync |
Sync model auth snapshots (push/pull, checksum/version validated). |
| POST | /api/settings/sync-receive |
Receive pushed settings (inbound). |
| POST | /api/settings/auth-receive |
Receive AuthMaterialSnapshot and persist via auth storage. |
| GET | /api/settings/auth-export |
Export local AuthMaterialSnapshot. |
| GET | /api/update-check |
Read cached/TTL-guarded npm update status for @runfusion/fusion (respects updateCheckEnabled). |
| POST | /api/update-check/refresh |
Clear cached update data and force a fresh npm update check. |
| GET | /api/updates/check |
Perform an on-demand npm registry check for the latest @runfusion/fusion version (no cache). |
Agent stats endpoint
| Method | Path | Description |
|---|---|---|
| GET | /api/agents/stats |
Aggregate agent/task stats used by operator summaries and capacity-risk signaling. Returns activeCount, assignedTaskCount, completedRuns, failedRuns, successRate, plus idleNonEphemeralCount (idle agents excluding ephemeral/runtime workers via isEphemeralAgent) and todoTaskCount (tasks currently in Todo). |
Docker provisioning endpoints
Initial container provisioning and lifecycle routes are registered by register-docker-provisioning-routes.ts.
| Method | Path | Description |
|---|---|---|
| POST | /api/docker/provision |
Provision and start a managed Docker container. |
| POST | /api/docker/deprovision |
Stop/remove a managed Docker container. |
| POST | /api/docker/containers/:containerId/start |
Start an existing container. |
| POST | /api/docker/containers/:containerId/stop |
Stop a running container. |
| POST | /api/docker/containers/:containerId/restart |
Restart a container. |
| GET | /api/docker/containers/:containerId/status |
Read runtime status for a container. |
Mesh configuration and post-provision managed-node operations are registered separately in register-docker-node-routes.ts (for example /api/docker/nodes/:managedId/apply-mesh-config and /api/docker/nodes/:managedId/mesh-status).
Run Audit API
The run-audit system records every mutation performed by the engine across three domains:
- Database — task:create, task:update, task:move, etc.
- Git — worktree:create, commit:create, merge:resolve, etc.
- Filesystem — file:write, prompt:write, attachment:create, etc.
Events are tied to specific run IDs for end-to-end traceability.
Run audit endpoint:
GET /api/agents/:id/runs/:runId/audit— Returns audit trail for a specific agent run- Query params:
?domain=database|git|filesystemfor filtering - Requires agent ownership or admin access
- Query params:
7) CLI Package (@runfusion/fusion)
Command entrypoint
packages/cli/src/bin.ts- Bootstraps environment
- Parses global flags (including
--project) - Routes subcommands (
task,project,settings,git,backup,mission,agent,message, etc.)
Command modules
packages/cli/src/commands/*- Task operations, settings, git wrappers, backup operations, project/node management
- TUI component (
packages/cli/src/commands/dashboard-tui/)- Ink-based terminal UI (status panel, logs, cursor visibility, tail-follow)
- Merged from former
@fusion/tuipackage - Invoked as part of the
fncommand (no separate package orpnpm tuicommand)
Project selection
packages/cli/src/project-resolver.ts- Resolution order: explicit
--project→ CWD detection (.fusion) → default/fallback logic - Integrates
CentralCoreandProjectManager
- Resolution order: explicit
Pi extension
packages/cli/src/extension.ts- Registers tool set for in-chat task/mission operations
- Uses
TaskStoredirectly for extension-side actions
Binary identity
- Published package defines
fnbinary (packages/cli/package.json) - Running
fnwith no arguments defaults to dashboard (web UI by default)
8) Storage Architecture
Fusion uses a hybrid storage model.
Per-project storage
- SQLite DB:
.fusion/fusion.db - Filesystem blobs (task-local artifacts):
.fusion/tasks/{TASK_ID}/PROMPT.md.fusion/tasks/{TASK_ID}/agent.log.fusion/tasks/{TASK_ID}/attachments/*
SQLite schema is initialized in packages/core/src/db.ts and uses:
- WAL mode (
PRAGMA journal_mode = WAL) - Foreign keys (
PRAGMA foreign_keys = ON) __meta.lastModifiedfor change detection/polling
Central storage (multi-project)
- Central DB:
~/.fusion/fusion-central.db - Schema in
packages/core/src/central-db.tsprojects,projectHealth,centralActivityLog,globalConcurrency,nodes,peerNodes,projectNodePathMappings,settingsSyncState,__meta
Memory files
- OpenClaw-style memory workspace:
.fusion/memory/MEMORY.md.fusion/memory/YYYY-MM-DD.md.fusion/memory/DREAMS.md
- The legacy top-level memory file is migration-compatibility only (seed/alias behavior) and is not canonical storage.
File-based side stores
Some data remains intentionally filesystem-based:
- Agent instruction bundles and heartbeat markdown:
.fusion/agents/*(AgentStore)
Agent/message/approval metadata and history now persist in SQLite tables.
Migration from legacy file storage
- Detection + migration:
packages/core/src/db-migrate.ts - Migrates legacy task/config/log/archive/automation/agent data into SQLite
- Creates
.bakbackups (for exampletask.json.bak,config.json.bak,archive.jsonl.bak)
Archive system
- Archived task snapshots are stored in SQLite
archivedTasks TaskStorearchive helpers:archiveTaskAndCleanup()cleanupArchivedTasks()readArchiveLog()/findInArchive()unarchiveTask()with restore behavior
9) Task Lifecycle
Lifecycle constants are defined in packages/core/src/types.ts:
- Columns:
planning,todo,in-progress,in-review,done,archived - Transition rules via
VALID_TRANSITIONS
Lifecycle flow
planning
│ (Planning processor writes PROMPT.md)
▼
todo
│ (Scheduler selects task, dependencies satisfied)
▼
in-progress
│ (TaskExecutor runs in worktree)
▼
in-review
│ (implementation complete + pre-merge workflow steps)
▼
done
│
└──────────────▶ archived
Execution detail
- Planning phase: the planning processor generates an executable plan
- Execution phase:
TaskExecutorperforms implementation, tool calls, tests/build commands - Review phase: optional
reviewStep()workflow depending on prompt review level (bypassed in fast mode) - Merge phase:
aiMergeTask()handles merge strategy and post-merge workflow steps
Fast Mode: Tasks with
executionMode: "fast"bypass thereview_steptool injection and pre-merge workflow steps. Completion blockers (tests, build, typecheck from PROMPT.md) and post-merge workflow steps remain enforced.
Step status model
Task steps use statuses: pending, in-progress, done, skipped.
Workflow steps
- Defined in project config as
WorkflowStep - Pre-merge steps run in executor (
runWorkflowSteps()) — bypassed in fast mode - Post-merge steps run in merger (
runPostMergeWorkflowSteps())
Stalled review detection
@fusion/core computes a heuristic task.stalledReview signal during task hydration (both slim board listings and full/detail reads in TaskStore) by scanning recent task log activity.
Current heuristics (see packages/core/src/stalled-review-detector.ts):
- Reenqueue churn (
heuristic: "reenqueue-churn"): at leastSTALLED_REVIEW_REENQUEUE_THRESHOLD(3) matches ofSTALLED_REVIEW_REENQUEUE_PATTERNwithinSTALLED_REVIEW_WINDOW_MS(60 minutes). - Invalid-transition loop (
heuristic: "invalid-transition-loop"): at leastSTALLED_REVIEW_INVALID_TRANSITION_THRESHOLD(2) matches ofSTALLED_REVIEW_INVALID_TRANSITION_PATTERNin logaction/outcomewithin the same window.
Detection is visibility-only: no scheduler/self-healing actions are triggered by this field. The dashboard TaskCard renders a Stalled badge for in-review tasks when task.stalledReview is present, with the heuristic reason in the tooltip.
Tune sensitivity by adjusting the exported constants in stalled-review-detector.ts. Increase thresholds to reduce noise; decrease thresholds only with incident evidence, because lower values can over-flag transient recovery bursts.
10) Agent System
Fusion has two complementary agent models:
- Task pipeline agents (planning/executor/reviewer/merger) managed by engine runtime
- Persistent registered agents managed by
AgentStore
Persistent agent storage
packages/core/src/agent-store.ts persists to:
.fusion/agents/{id}.json.fusion/agents/{id}-heartbeats.jsonl.fusion/agents/{id}-keys.jsonl.fusion/agents/{id}-revisions.jsonl.fusion/agents/{id}/avatar.{ext}(uploaded avatar image file, served via/api/agents/:id/avatar)
Agent spawning from executor
TaskExecutor supports hierarchical child agents via:
createSpawnAgentTool()runSpawnedChild()terminateChildAgent()/terminateAllChildren()
Limits are controlled by project settings (maxSpawnedAgentsPerParent, maxSpawnedAgentsGlobal).
Heartbeat monitoring and triggers
agent-heartbeat.ts provides:
- Health monitoring and run tracking (
HeartbeatMonitor) - Trigger scheduling (
HeartbeatTriggerScheduler) for:- timer
- task assignment
- on-demand runs
Custom instructions
packages/engine/src/agent-instructions.ts resolves per-agent instruction text/path with path-traversal and extension validation.
11) Multi-Project Architecture
Multi-project orchestration spans core + engine.
Core control plane
CentralCore(packages/core/src/central-core.ts) maintains:- Project registry
- Health metrics
- Unified central activity feed
- Global concurrency state
- Node registry (
local/remote) - Per-project/per-node working-directory mappings (
projectNodePathMappings)
Engine orchestration
HybridExecutor(packages/engine/src/hybrid-executor.ts) is the top-level orchestratorProjectManagerinstantiates per-project runtimes and forwards events with project attribution- Runtime startup/update resolves
ProjectRuntimeConfig.workingDirectorythroughCentralCore.resolveLocalProjectWorkingDirectory()/resolveProjectWorkingDirectory(projectId,nodeId)using exactprojectNodePathMappingsrows for the active node; missing mappings are hard failures (no fallback toRegisteredProject.path).
Runtime abstraction
Defined in project-runtime.ts:
ProjectRuntimeinterfaceRuntimeStatusandRuntimeMetrics
Implementations:
InProcessRuntimeChildProcessRuntimeRemoteNodeRuntime
IPC protocol (child-process mode)
In packages/engine/src/ipc/ipc-protocol.ts:
- Host commands:
START_RUNTIME,STOP_RUNTIME,GET_STATUS,GET_METRICS,PING - Worker events:
TASK_CREATED,TASK_MOVED,TASK_UPDATED,ERROR_EVENT,HEALTH_CHANGED
Multi-project runtime diagram
HybridExecutor
│
┌───────┴────────┐
│ ProjectManager│
└───┬─────────┬───┘
│ │
┌───────────▼───┐ ┌──▼──────────────┐
│InProcessRuntime│ │ChildProcessRuntime│
│(local process) │ │(fork + IPC host) │
└──────┬─────────┘ └──┬───────────────┘
│ │
TaskStore/Scheduler │
▼
child-process-worker
+ InProcessRuntime
Task Routing Architecture
Task dispatch routing is resolved in two layers:
- Task routing resolution (
packages/engine/src/effective-node.ts)resolveEffectiveNode(task, settings)applies precedence:Task.nodeId→task-overrideProjectSettings.defaultNodeId→project-default- no node set →
local
- Runtime selection (
packages/engine/src/project-manager.ts)child-processisolation always usesChildProcessRuntimein-processisolation usesRemoteNodeRuntimewhen the registered project host node is remote- otherwise uses
InProcessRuntime
Dispatch flow in scheduler
Within Scheduler.schedule() dispatch for todo tasks now runs node gates in this order:
resolveEffectiveNode()chooses routing source (task-override,project-default,local).- If a node is selected,
validateNodeDispatchchecks for a persisted(projectId, nodeId)working-directory mapping (CentralCore.getProjectNodePath). - Missing/blank mappings block dispatch (task stays in
todo) and logExecution blocked: project has no path mapping for node <id>. - Only after mapping validation passes does
applyUnavailableNodePolicy()evaluate node health and optionalfallback-localbehavior.
This preserves a clear separation between configuration correctness (mapping exists) and runtime health/failover policy.
Unavailable-node policy
unavailableNodePolicy is a validated/stored project setting (block default, fallback-local allowed) and is enforced during scheduler dispatch when both conditions are true:
- effective routing selected a remote node, and
SchedulerOptions.nodeHealthMonitoris configured.
Behavior summary:
block(default): unhealthy node status (offline,error,connecting) blocks dispatch for that poll cycle and keeps the task intodo.fallback-local: unhealthy remote node reroutes dispatch to local execution (effectiveNodeId: null,effectiveNodeSource: "local").- unknown node health (
undefined) is treated as allow/continue.
Active-task node-override guard
packages/core/src/node-override-guard.ts enforces immutable routing overrides for active tasks:
validateNodeOverrideChange()blocks node override updates while task column isin-progress- returns reason
task-in-progress
TaskStore.updateTask() applies this guard before persisting nodeId changes.
Task commit-association API (GET /api/tasks/:id/commit-associations)
Dashboard session-diff route registration (packages/dashboard/src/routes/register-session-diff-routes.ts) now exposes lineage commit associations for task detail views:
- Route:
GET /api/tasks/:id/commit-associations - Project scoping: uses
getProjectContext(req)so reads are project-aware like adjacent task diff endpoints. - 404 behavior: returns
{ error: "Task not found" }for unknown task ids. - Response contract:
{
"taskId": "FN-1234",
"lineageId": "uuid-or-null",
"associations": [
{
"commitSha": "abc123...",
"commitSubject": "feat(FN-1234): ...",
"authoredAt": "2026-05-11T02:00:00.000Z",
"matchedBy": "canonical-lineage-trailer | legacy-task-id-trailer | legacy-subject | manual-reconciliation",
"confidence": "canonical | legacy | ambiguous",
"taskIdSnapshot": "FN-1234",
"note": "optional reconciliation note"
}
]
}
confidence is a consumer-facing interpretation aid:
canonical= immutable lineage trailer match (highest confidence)legacy= recovered via legacy task-id/subject matchingambiguous= manual reconciliation where historical task-id attribution could be misleading
Done-task changed-file aggregation provenance
Done-task files changed data shown on Task Cards and Task Changes tabs comes from the dashboard diff endpoints (/api/tasks/:id/diff and /api/tasks/:id/file-diffs) using this precedence:
- If the task has a
lineageId, aggregate the union of file changes across reachable commits fromtask_commit_associationsfor that lineage (plusmergeDetails.commitShawhen missing from associations). - If no lineage commits are usable, fall back to legacy single-commit behavior based on
mergeDetails.commitSha^..mergeDetails.commitSha. - If no commit SHA is available, return merge summary numbers only (
mergeDetails.filesChanged/insertions/deletions) without file patches.
This keeps multi-commit landed tasks accurate while preserving compatibility for legacy/self-healed done tasks.
Task branch field plumbing (branch + baseBranch)
Task create/update now preserves both branch fields end-to-end:
- Request validation/normalization (dashboard route layer):
packages/dashboard/src/routes/register-task-workflow-routes.tsPOST /api/tasksacceptsbranchandbaseBranchas string values.PATCH /api/tasks/:idacceptsbranchandbaseBranchasstring | nullfor PATCH-style updates, trims string inputs, and treats empty strings as clears (null).- Route handlers reject non-string/non-null payloads with
400.
- Durable persistence (core store layer):
packages/core/src/store.tsTaskStore.createTask()persists bothbranchandbaseBranchon task creation.TaskStore.updateTask()preserves existing PATCH semantics where explicitnullclears either field.- Fields round-trip through JSON and SQLite persistence via the shared task contract in
packages/core/src/types.ts.
Routing activity visibility
Routing decisions are visible in task activity/log entries and in task metadata (effectiveNodeId, effectiveNodeSource), and surfaced in dashboard routing UI + fn task show output.
See also:
- Settings Reference → Node Routing settings
- Task Management → Node Routing
- Multi-Project → Node Routing
12) Settings Hierarchy
Settings are split by scope.
Global scope
- File:
~/.fusion/settings.json - Managed by
GlobalSettingsStore(packages/core/src/global-settings.ts) - Examples:
themeMode,colorTheme, default model/provider, notification preferences (ntfy*legacy fields andnotificationProviders)
Project scope
- Stored in per-project config (
configtable + compatibility file.fusion/config.json) - Includes engine/runtime controls (
maxConcurrent,autoMerge, worktree and workflow behavior, etc.)
Merged view
Settingscombines global + project values- Defaults in
DEFAULT_GLOBAL_SETTINGSandDEFAULT_PROJECT_SETTINGS - Scope key lists in
GLOBAL_SETTINGS_KEYSandPROJECT_SETTINGS_KEYS
Model controls
- Per-task model overrides on task fields:
modelProvider/modelIdvalidatorModelProvider/validatorModelIdplanningModelProvider/planningModelIdthinkingLevel
- Reusable presets via
ModelPreset - Agent prompt template overrides via
agentPrompts
13) Git Integration
Git behavior is implemented primarily in engine executor/merger + dashboard/CLI git APIs.
Git REST API endpoints
Git dashboard routes are registered in register-git-github.ts.
| Method | Path | Description |
|---|---|---|
| GET | /api/git/remotes |
List GitHub remotes parsed from git remote -v output. |
| GET | /api/git/remotes/detailed |
List all remotes with fetch/push URLs. |
| POST | /api/git/remotes |
Add a new remote (name, url). |
| DELETE | /api/git/remotes/:name |
Remove an existing remote by name. |
| PATCH | /api/git/remotes/:name |
Rename a remote (newName). |
| PUT | /api/git/remotes/:name/url |
Update a remote URL. |
| GET | /api/git/status |
Return branch, short commit, dirty state, and ahead/behind counts. |
| GET | /api/git/commits |
Return recent commits (?limit= capped at 100). |
| GET | /api/git/commits/:hash/diff |
Return commit stat + patch for a validated commit hash. |
| GET | /api/git/commits/ahead |
Return local commits ahead of upstream (empty when upstream is not configured). |
| GET | /api/git/remotes/:name/commits |
Return commits for a remote ref (?ref= optional, ?limit= max 50, with remote HEAD/main/master fallback resolution). |
| GET | /api/git/branches |
List local branches with current/tracking metadata and last commit date. |
| GET | /api/git/branches/:name/commits |
Return commits for a branch (?limit= default 10, max 100). |
| GET | /api/git/worktrees |
List worktrees with branch/path metadata and task association when available. |
| POST | /api/git/branches |
Create a branch from HEAD or an optional base ref. |
| POST | /api/git/branches/:name/checkout |
Checkout an existing branch. |
| DELETE | /api/git/branches/:name |
Delete a branch (?force=true allows deleting unmerged branches). |
| POST | /api/git/fetch |
Fetch from a remote (remote defaults to origin). |
| POST | /api/git/pull |
Pull the current branch (rebase boolean optional) and return structured conflict metadata on merge/rebase conflicts. |
| POST | /api/git/push |
Push the current branch. |
| GET | /api/git/stashes |
List stash entries. |
| GET | /api/git/stashes/:index/diff |
Return stash stat + patch for a validated stash index (404 when missing). |
| POST | /api/git/stashes |
Create a stash with an optional message. |
| POST | /api/git/stashes/:index/apply |
Apply a stash by index (optionally drop after apply via drop: true). |
| DELETE | /api/git/stashes/:index |
Drop a stash by index. |
| GET | /api/git/diff |
Return unstaged working-tree diff text. |
| GET | /api/git/diff/file |
Return staged or unstaged diff for one file (path + `staged=true |
| GET | /api/git/changes |
Return staged and unstaged file change summary. |
| POST | /api/git/stage |
Stage specified files. |
| POST | /api/git/unstage |
Unstage specified files. |
| POST | /api/git/commit |
Create a commit from staged changes with a required message. |
| POST | /api/git/discard |
Discard working-tree changes for specified files. |
GitHub tracking lifecycle (task creation + existing-task edits)
Fusion attempts GitHub issue creation when per-task tracking is explicitly enabled (task.githubTracking.enabled === true) and the task is currently unlinked. This runs during task creation flows and during existing-task PATCH updates whenever the post-update task is still enabled+unlinked (not only githubTracking field edits), allowing previously skipped/unlinked tasks to recover on later updates. The lifecycle resolves the repo in priority order: task override (repoOverride) → project githubTrackingDefaultRepo → global githubTrackingDefaultRepo. If no repo resolves, task create/update still succeeds and an activity entry records the skip reason. GitHub API/CLI failures are best-effort only (swallowed with warning), so task updates are never blocked by GitHub availability.
When Fusion does create a tracking issue, it formats the title as [FN-XXXX] Task title and sends a short plain-text body prefixed with Fusion task: FN-XXXX. The body is a bounded summary snippet (not full task prompt content), and Fusion does not include any hyperlink back to the local dashboard. Manual unlink requests (githubTracking.issue: null) do not recreate an issue in that same PATCH request, and disable updates do not create issues. Auth resolution remains strict-mode (token vs gh-cli) but now defensively accepts merged settings shapes where auth keys may appear in global-merged payloads.
When a tracked task later moves to in-progress or done, Fusion posts one short lifecycle comment on the linked tracking issue. These comments always include the Fusion task ID as plain text (Fusion task: FN-XXXX) and never link back to the Fusion app. The in-progress comment stays plain-text; the done comment can additionally include GitHub commit/PR markdown links plus branch, file-change, and merge-timestamp details when that merge context is available on the task. No comment is posted for any other transition.
When a tracked task transitions into done, Fusion closes the linked GitHub issue with state_reason: completed. When a task transitions out of done into any active column (triage, todo, in-progress, in-review), Fusion reopens it with state_reason: reopened. When a tracked task is permanently deleted, Fusion closes the linked GitHub issue with state_reason: not_planned. Moves from done to archived leave the issue closed. Tasks without githubTracking.enabled or without a linked issue are unaffected, and GitHub failures are logged to task activity without blocking the move.
Worktree model
- Each active task runs in isolated worktree under
.worktrees/* - Executor creates branches like
fusion/{task-id}(executor.ts) WorktreePoolcan recycle idle worktrees when enabled
Merge strategies
- Setting type:
MergeStrategy = "direct" | "pull-request"(types.ts) aiMergeTask()inmerger.tsperforms merge flowmerger.tsalso exposes a test-only__test__helper object for internal merger unit/integration coverage (for example autostash orphan cleanup behavior)- Supports workflow-step execution after merge (post-merge phase)
- Deterministic verification now runs a bootstrap preamble (
node scripts/ensure-test-artifacts.mjs) before configuredtestCommand/buildCommand, then self-heals ViteFailed to resolve entry for package "@fusion/..."workspace-entry faults by rebuilding the missing package once and retrying the failed command. If that retry still reports the same missing-entry fault, merger raises a typed environment fault andProjectEngineleaves the task in-review (no verificationFailureCount increment or in-progress bounce) so the next recovery sweep can retry after other runs rebuild artifacts.
Autostash lifecycle
- Before destructive merge prep,
stashUnrelatedRootDirChanges()snapshots dirty root-dir edits intofusion-merger-autostash:<taskId>:<ts>(plus optionalrace-rescue-*stashes for late writes). - During verification-fix finalize fallback,
commitOrAmendMergeWithFixes()now snapshots any still-dirty root-dir state intofusion-merger-autostash:<taskId>:finalize-reset:<ts>before its hard reset/clean recovery path, preventing silent mixed-worktree leftovers from being discarded. - In
aiMergeTaskcleanup,restoreUnrelatedRootDirChanges()attempts restore; thendropAutostashHandle()runs on every terminal path and drops primary + race-rescue stashes when restoration succeeded or content is no longer live. - If restore fails with unresolved developer work (
failed/conflict-needs-manual), cleanup uses a keep-if-live rule so still-live stashes are preserved for manual recovery. sweepAutostashOrphans()keeps its subsumed/live classification for prior-run leftovers, andsweepStaleAutostashes()adds an age-based backstop that dropsfusion-merger-autostash:*entries older than the configured threshold (default 24h).
Stash Recovery surface
- Orphans are typically residual
fusion-merger-autostash:*entries from older merge runs where restore could not safely complete. - Existing task-scoped surfacing remains: merger warnings still log to
mergerLog.warnandstore.logEntryfor the active merge task. - New global surfacing adds
merger:autostashOrphansTaskStore events, engine helpers (listAutostashOrphans,getAutostashDiff,applyAutostashBySha,dropAutostashBySha), and dashboard API endpoints under/api/stash-recovery/*. merger:autostashOrphansrecords now include provenance fields (sourcePhase,detectedByTaskId,detectedAt) so operators can attribute leftovers to the merge phase and surfacing task/session.ProjectEngineconsumes the orphan event stream and auto-creates deduplicatedsourceType: "recovery"follow-up tasks keyed bysourceParentTaskIdfor live leftovers, so repeated detections do not spam the board.- Dashboard operators can inspect orphan counts, review diffs, apply stashes, and explicitly drop entries with confirmation.
- Decision: recovery stays user-gated. Auto-apply was rejected because clean-tree checks are racy, stash placement is ambiguous after source task merge, and apply conflicts can produce hard-to-untangle state.
sweepAutostashOrphanscontinues to auto-drop only subsumed entries while preserving live developer work.
Conflict handling
merger.ts includes conflict classification and auto-resolution helpers:
- lock files (
LOCKFILE_PATTERNS) - generated files (
GENERATED_PATTERNS) - whitespace-trivial conflicts
PR and badge integration
- Engine PR monitor:
pr-monitor.tsandpr-comment-handler.ts - Dashboard GitHub APIs + webhook route in
routes.ts - Badge snapshots are streamed via
/api/wsanduseBadgeWebSocket.ts
14) Key Design Decisions
-
SQLite + WAL for local-first reliability
- Chosen for simple deployment and strong transactional behavior
- WAL mode enables concurrent readers/writers with low ops overhead
-
Hybrid persistence (DB + filesystem blobs)
- Structured metadata in SQLite, large text/artifacts in task directories
- Keeps DB efficient while preserving inspectable task artifacts
-
Git worktree isolation as core execution primitive
- Prevents cross-task interference
- Makes concurrent task execution safer
- Enables deterministic cleanup/retry/recovery
-
Agent-as-tool-caller pattern
- Engine tools (
task_update,task_log,review_step,spawn_agent, etc.) create explicit, auditable state transitions - Prompts are role-specific (
TRIAGE_SYSTEM_PROMPT,EXECUTOR_SYSTEM_PROMPT, etc.)
- Engine tools (
-
Separation of real-time channels by concern
- SSE for broad board/missions/session state updates (
/api/events) - Dedicated badge WebSocket (
/api/ws) for lightweight PR/issue badge snapshots
- SSE for broad board/missions/session state updates (
-
Multi-project control plane with runtime abstraction
CentralCoredecouples registry/health/concurrency from per-project executionProjectRuntimeinterface allows multiple isolation strategies (in-process, child-process, remote node)
Source Map (quick navigation)
- Core exports:
packages/core/src/index.ts - Engine exports:
packages/engine/src/index.ts - Dashboard exports:
packages/dashboard/src/index.ts - CLI entry:
packages/cli/src/bin.ts - Pi extension:
packages/cli/src/extension.ts - Runtime abstraction:
packages/engine/src/project-runtime.ts - Multi-project orchestrator:
packages/engine/src/hybrid-executor.ts - Task routing resolver:
packages/engine/src/effective-node.ts - Node override guard:
packages/core/src/node-override-guard.ts
PR-backed Review tab state and same-task revision flow
Pull-request auto-merge tasks persist structured review metadata on the task as reviewState.
reviewState.source:"pull-request"or"reviewer-agent"reviewState.summary: review decision, reviewer states, required checks, and blocking reasonsreviewState.items: normalized per-review/per-comment records keyed by stable GitHub IDsreviewState.addressing: per-item lifecycle records (queued,in-progress,addressed,failed) with timestamps and optionalstale
API flow:
GET /api/tasks/:id/reviewreturns canonicalTaskReviewData(mode,refreshable,fetchedAt,summary,items[]) for modal load.POST /api/tasks/:id/review/refreshreturns the sameTaskReviewDatashape after re-fetching source data (GitHub PR mode or reviewer-agent direct mode).POST /api/tasks/:id/review/addressrecords selected review items as queued, appends a deterministic**PR Review Revision Request**steering comment payload, clears transient failure/session state, and requeues the same task totodofor same-task revision.
UI contract boundary:
PrSectionowns branch/PR lifecycle metadata and automation status.TaskReviewTabowns review decisions, detailed review items, selection, and addressing progress.TaskCommentsremains separate for general discussion.