Merges FN-3396's full Cursor CLI provider integration (Steps 1–4): defines a CLI-backed provider contract, adds the `fusion-plugin-cursor-runtime` plugin package with process management and runtime probes, wires dashboard auth flows and UI (ProviderCard, onboarding modal, settings), and bundles the
Fusion-Task-Id: FN-3396
- Add core plugin AI security scan module and schema support for scan toggle/state metadata
- Enforce scan checks during CLI and dashboard plugin install flows, with preserved API error status on scan failures
- Expose plugin scan toggle and rescan actions in dashboard/plugin manager with route and UI coverage
- Update plugin authoring and CLI/dashboard docs, plus add changeset for published CLI package
Fusion-Task-Id: FN-3077
Implements a modular dependency graph feature for the Fusion dashboard plugin, replacing the monolithic `DependencyGraphView` component with a factored architecture: graph types and filtering (Step 1), a data hook (Step 2), auto-layout engine (Step 3), SVG edge rendering (Step 4), an interaction hoo
Fusion-Task-Id: FN-3082
Adds documentation for graph navigation canonicalization (FN-3080) covering plugin authoring and dashboard usage, along with a changeset for the `@runfusion/fusion` package release.
Fusion-Task-Id: FN-3080
Codex returns weekly window reset_at in milliseconds in some cases,
which was being multiplied by 1000 and producing nonsensical reset
countdowns and pace calculations. Route through _parseResetTimestamp
so both formats work, and add a regression test.
Also tightens comments in useMobileKeyboard / ChatView around why
visualViewport scroll events skip offsetTop updates.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When ChatView remounted (e.g. tab switch with keyboard still up), the
hook started with keyboardOpen=false and corrected itself only after
the effect ran. That single stale-state render briefly unhid the
executor status bar, which appeared as a blank pane covering half the
input box before the next state update settled it.
useState initializers now call getKeyboardMetrics() lazily on first
render so the very first paint already reflects the live keyboard
state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two mobile chat regressions addressed together:
1) ChatView send button: preventDefault now fires on pointerdown for
touch pointers, before iOS blurs the textarea. The previous
onMouseDown.preventDefault was too late on iOS (mousedown is
synthesized after touchend, by which point the keyboard has
already started dismissing). Click still runs the action so quick
taps remain reliable.
2) ExecutorStatusBar: hidden on mobile while keyboard is open,
mirroring MobileNavBar. The bar is position:fixed against the
layout viewport, which iOS leaves anchored below the keyboard;
during a swipe/pan it would slide over the messages list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The imperative useLayoutEffect approach made mobile worse — first tap
flickered and didn't bring up the keyboard, while the original
swipe-overlap symptom remained. Restoring the previous React-state
flow until a better fix is identified. Removes the changeset that
shipped with the failed attempt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The mobile composer/footer slid over the message list when the user
swiped with the keyboard up. Cause: --vv-height / --vv-offset-top were
routed through React state via useMobileKeyboard, so on iOS — which
fires visualViewport scroll/resize on the same frame as its keyboard
animation — the .chat-thread translation lagged by one paint, visible
as the composer momentarily floating over messages.
Now those two vars are written imperatively in a useLayoutEffect
directly to the .chat-thread DOM node on every visualViewport event,
mirroring the working pattern at QuickChatFAB.tsx:1032-1052 (which
already works correctly on mobile). Only --keyboard-overlap (a
structural open/close signal, not per-frame) still flows through
React state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The mobile send button used pointerdown + touchstart with preventDefault
and a focus-preservation dance to keep the keyboard up while sending.
That path silently failed on quick taps on iOS — only a long press
registered. Switching to plain onClick (with touch-action: manipulation
to skip the click delay) fires reliably on tap. The soft keyboard may
dismiss on send now, which is a minor regression vs. the previous
intent but vastly preferable to silent failure.
QuickChat is unchanged because it already works on mobile.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The ChatView and QuickChat send/stop buttons each had both an
onPointerDown and an onTouchStart handler invoking the action. On a
quick mobile tap both fire, so handleSend / handleSendMessage /
stopStreaming ran twice in rapid succession. The second invocation
closed the first's SSE stream (streamRef.current.close()), the server
treated that as a cancel via beginGeneration, and the chat ended with
no output — exactly matching the reported "tap silently fails, long
press works" symptom (long press happened to suppress one of the two
events).
Both handlers now early-return when the existing handledMobile*Ref
flag is already set, so only the first event for a given tap fires the
action. The send button additionally gets touch-action: manipulation
(removes the click delay that lets the textarea blur win the race) and
an expanded invisible hit area via ::before so slightly-off taps don't
land on the surrounding textarea and dismiss the keyboard without
sending.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the dashboard TUI collapses to single-pane mode (narrow width or
short terminal), show a horizontal log strip at the bottom of the screen
while the active section renders on top. The split is content-driven:
the top pane gets exactly the rows it needs to render its section
without truncating (computed from SystemPanel's chip wrap at the current
width, or each panel's known row count for Stats/Utilities/Settings),
and the log strip absorbs every remaining row to maximize log
visibility. Down-arrow shifts sub-focus into the log strip with the
same key bindings as the dedicated Logs section (j/k, Home/G, Enter to
expand, w to wrap, c to copy, f to filter, mouse wheel). Up-arrow at
the top of the strip or Esc returns focus to the main pane.
Right/Left/Tab continue to cycle sections, so the dedicated full-screen
Logs view is unchanged. The split auto-disables if the log strip would
get fewer than 6 rows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
P0 — parsePorcelainZ rename/copy handling
Git's -z porcelain emits `R <new>\0<old>\0` for renames (and
C for copies). The naive split-and-slice treated <old> as an
independent dirty path, which made runObservedDestructiveSyncOp
warn about phantom "cleared paths" whenever a rename was in
flight. Now we detect R/C status and skip the trailing entry.
P1 — race-rescue loop unstages between attempts
`git stash create` snapshots the index without clearing it, so
iteration 2's `git add -A` would re-stage atop iteration 1's
leftovers. Tree differences inside the loop then reflected stale
staging rather than genuine new writes. Added a `git reset` at
the top of each iteration so every attempt starts from a clean
index baseline.
P1 — writeActiveMergerStatus is now atomic
Switched from in-place writeFileSync to temp-file + renameSync.
POSIX guarantees rename atomicity on the same filesystem, so a
reader can no longer catch the file mid-flush and return a
false-negative "no merger active" advisory.
P2 — Step regex em-dash clarity
`[—\-:]` is functionally fine but obscures intent; switched to
`(?:—|-|:)` so the em-dash branch is obvious. Added a test case
for the em-dash separator.
New tests:
- parse-porcelain-z.test.ts (8 cases including renames + copies)
- em-dash case added to derive-subject-summary.test.ts
247/247 merger-suite tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The race-rescue loop was firing on every merger run because
`git add -A && git stash create` does not clean the working tree —
files stay dirty post-stash, so a subsequent `snapshotDirtyFiles` saw
the SAME paths the primary stash had just captured and stashed them
again, producing identical-tree race-rescue duplicates (visible in
git stash list as `fusion-merger-autostash:FN-XXXX:race-rescue-0`
sitting next to its identical `fusion-merger-autostash:FN-XXXX:`).
Fix: list the path set captured by the primary stash via
`git stash show --name-only`, and only rescue paths in the current
dirty snapshot that are NOT in that set — those are genuine
late-dirty writes from concurrent dev edits or interleaved ops.
Also drop any rescue whose tree-SHA exactly equals the primary,
as a defensive belt-and-braces.
Existing duplicate race-rescue stashes are harmless (identical
content to their primaries) and can be dropped manually.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- writeActiveMergerStatus: writes .git/.fusion-merger-active.json
(taskId, pid, hostname, startedAt) at merge entry, deleted in finally.
Not a lock — purely informational so dashboards / status lines /
pre-Edit hooks can warn devs that rootDir is volatile during the run.
readActiveMergerStatus(rootDir) is exported for consumers.
- runObservedDestructiveSyncOp: snapshot-before/after wrapper around
destructive rootDir ops that are *supposed* to preserve unrelated
working-tree edits. resetMergeWithWarn now uses it — any future
silent wipe of dirty paths surfaces as an actionable warning instead
of going unnoticed. Not applied to the autostash's own reset
--hard / clean -fd; those are intentionally destructive and already
protected by the race-rescue stash.
- Race-rescue stashes from stashUnrelatedRootDirChanges are now
attached to the AutostashHandle and surfaced via store.logEntry so
the recovery command lands on the task feed instead of only
mergerLog.warn.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- deriveDeterministicSubjectSummary now picks the lowest-numbered
`complete Step N` headline (or the oldest commit) instead of the most
recent commit, so trailing quality-gate revisions stop hijacking the
squash-merge subject (FN-3617 landed as "align mailbox modal css..."
when 4 of 5 commits were the actual Claude OAuth fix).
- AI subject + body system prompts in ai-summarize.ts now weight by
commit theme rather than file size, so a small token cleanup that
touches a large CSS file no longer dominates the summary.
- stashUnrelatedRootDirChanges adds a bounded re-snapshot loop after
the primary stash is persisted but before \`git reset --hard\`. Any
late-dirty paths (concurrent dev edits during a long merger run,
parallel merger runs racing on rootDir, late test/build artifacts)
get captured in labeled \`race-rescue-N\` stashes recoverable from
\`git stash list\`, instead of being wiped by the destructive reset.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The monolithic `TaskDetailModal` test suite (6,745 lines) was split into six focused test files covering attachments/tabs, definition/actions, inline editing/integrations, models/progress/workflow, rendering, and responsive/dependencies, with a shared test helpers module added for common utilities.
Fusion-Task-Id: FN-3591
Merged FN-3602: Compact tool-call summaries in the ChatView with responsive mobile layout improvements, including new regression tests and documentation in the dashboard guide.
Fusion-Task-Id: FN-3602
Merges five commits implementing centralized runtime skill forwarding that preserves Fusion context across the Hermes runtime layer. The engine's `agent-runtime` and `agent-session-helpers` were updated to forward skills at runtime, with `runtime-adapter.ts` and its types extended to carry context.
Fusion-Task-Id: FN-3612
Two cuts to wasted work in the merge verification loop:
1. After the in-merge fix agent runs, fingerprint the working tree
(`git diff HEAD` + `git status --porcelain`, sha256). If the post-fix
fingerprint matches pre-fix and is non-empty, the agent didn't actually
change anything — re-running the same failing command can only yield
the same failure, so log and report the attempt as unsuccessful without
paying the test/build cost. Empty fingerprints (snapshot tooling failed)
fall through to the existing re-run path so we never silently swallow a
real fix.
2. Inside `syncDependenciesForMerge`, hash the active lockfile and compare
against `node_modules/.fusion-install-marker` (written after each
successful install). When they match, skip `pnpm install
--frozen-lockfile` even if `package.json` is staged. Covers the common
case where `package.json` changes but the lockfile doesn't, and
amortizes install across auto-recovery re-enqueues that hit the same
worktree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /_perf/dashboard-load server route no longer exists, so every
reportDashboardPerf() call was a silently-swallowed 404. Remove the helper
in legacy.ts plus its five call sites in App.tsx / useProjects.ts (the
companion console.log lines stay), and drop the dashboard-perf.log entry
from the test-isolation runtime ignore list since nothing writes that file
anymore.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Live fusion instances running on a shared HOME write to tasks/, messages/,
memory-insights.md, test-cache.json, HEARTBEAT.md, kb.db.backup-*, and
fusion.db.pre-* snapshots; none were in the runtime ignore list, so the
script flagged them as test-driven mutations and failed merge build
verification. Recursion in collectFusionSignature only filters via top-level
matches, so adding these top-level patterns short-circuits descent into
live-app dirs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This merge introduces eval score categorization with a new `eval-scoring.ts` module (FN-3601, FN-3390), hardened bundled plugin entry resolution to prioritize workspace source over installed copies (FN-3590), and documented reply-link threading behavior in the mailbox (FN-3598). It also adds mobile
Fusion-Task-Id: FN-3590
Local `pnpm test:isolated` was failing because a concurrently-running
fusion app continually mutates `~/.fusion` (databases, agent sessions,
memory, automations, plugins, logs). Filter those runtime-owned paths
from the protected-dir signature, widen the baseline-stability sampling
window, and re-sample on suspected violations so transient app activity
doesn't masquerade as test pollution.
Tests still cannot legitimately write into these paths — they're skipped
because the *running app* is expected to.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This merge restores the canonical agent lifecycle with termination scoped at the run level (FN-3580, 4 steps), adds sender-side wake recipient override for messages, and introduces test isolation CI enforcement with a stuck-requeue race fix. UI changes remove terminated-agent indicators from AgentDe
Fusion-Task-Id: FN-3580
Documents the live reviewer override behavior in the settings reference and task management guides, adding two lines to each file for a total of 4 lines of documentation.
Fusion-Task-Id: FN-3595
This merge lands five FN-3593 commits establishing a test isolation contract with a new `scripts/check-test-isolation.mjs` guard that scans for accidental `beforeEach`/`afterEach`/`beforeAll`/`afterAll` in setup helpers, plus per-package `setup-test-isolation.ts` bootstraps that canonicalize the pat
Fusion-Task-Id: FN-3593
Senders can now force the recipient agent to wake on receipt regardless
of the recipient's `messageResponseMode`. Surfaced as a "Wake recipient
immediately" checkbox in MessageComposer and as a `wake_recipient`
boolean param on the `fn_send_message` agent tool. Carried as
`metadata.wakeRecipient: true` on the message; the heartbeat hook
treats forced wakes as `message_received_urgent` in the wake delta so
agents can distinguish them from normal `messageResponseMode: immediate`
wakes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When SelfHealingManager.recoverCompletedTasks moved a task from
in-progress to in-review, the executor's stuck-kill cleanup running in
execute()'s finally block could fire 20s later, see a stale captured
task.column = "in-progress", and overwrite the recovery by tearing down
the worktree and moving the task back to todo with all step progress
reset. Both the outer-finally and step-session requeue blocks (and the
force-requeue setTimeout in markStuckAborted) now re-read the latest
column and skip cleanup entirely if the task has moved past
in-progress/todo.
Adds a new preserveProgressOnStuckRequeue setting (default: true,
toggle in Settings near the Stuck Task Timeout) so stuck-requeue passes
{ preserveProgress: true } to moveTask. Completed step statuses now
survive the bounce so the agent resumes from where it left off instead
of restarting every step from pending.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
POST /api/agents/:id/runs previously awaited resolvedMonitor.executeHeartbeat
end-to-end before sending the response. For real provider runs that take
tens of seconds to minutes, Safari (and intermediate proxies) drop the
client socket and the dashboard surfaces "Failed to start heartbeat run:
load failed" — the run is actually in flight, but the toast suggests it
failed to start.
The route now kicks off executeHeartbeat in the background, polls briefly
for the active-run record (created synchronously inside executeHeartbeat
→ startRun), and returns 201 with that record. Synchronous failures of
executeHeartbeat are still surfaced to the client; background failures
are logged via runtimeLogger.child("heartbeat"). The 409 active-run
conflict contract is preserved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Overview dropdown previously rendered "X active · Y running" where
both X (stats.activeCount) and Y (activeAgents.length) counted agents
whose state was either "active" or "running" — so an agent that was
merely enabled but idle would still inflate the "running" tally. The
label now counts each state distinctly so "running" only reflects
agents that are mid-heartbeat. Adds AgentsOverviewBar.test.tsx to lock
in the new contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the dashboard user browses another agent's mailbox (e.g. the CEO's
inbox), opening a message no longer triggers POST /messages/:id/read.
The previous behavior silently consumed the agent's unread state, so the
agent's heartbeat never surfaced the message and fn_read_messages (which
defaults to unread_only=true) returned nothing. Auto-mark-read now only
fires on the dashboard user's own inbox tab. Adds regression tests in
MailboxView.test.tsx and MailboxModal.test.tsx.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The InProcessRuntime constructed its AgentStore with only `rootDir`, leaving
task-claim/checkout/release operations unconfigured. As a result, the
heartbeat auto-claim scan logged "TaskStore not configured for task-claim
operations" whenever a relevant todo was found. Pass the runtime's TaskStore
through to the AgentStore so claimTaskForAgent succeeds.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This merge introduces a memory file markdown preview feature (FN-3584) with corresponding documentation, refines the AgentDetailView and AgentLogViewer components in the dashboard, and adds defensive collision handling for worktree operations during manual task moves (FN-3583).
Fusion-Task-Id: FN-3584
Two related bugs let two in-progress tasks share a single
.worktrees/<name> directory:
1. The dashboard POST /tasks/:id/move route promoted tasks to
in-progress without allocating a fresh worktree path, so a queued
task carrying a stale worktree field from a prior preserveResumeState
requeue could land in-progress on a directory already held by another
active task.
2. moveTask({preserveResumeState:true}) kept the worktree pointer on
requeue. When the on-disk checkout was later removed or reassigned,
the next dispatch collided with a worktree the scheduler had handed
to another task.
moveTask now releases the worktree pointer on every reopen-to-todo hop
(branch is kept so committed progress survives via git worktree add
<path> <branch>). A new preserveWorktree option opts internal bounces
out of the release. moveTask also accepts an allocateWorktree callback
that runs under a new cross-task allocation lock in TaskStore, so two
concurrent moves cannot pick the same name from a stale snapshot. Both
the manual-move route and the scheduler dispatch path flow through the
allocator and share the lock.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert the dynamic await import("./agent-session-helpers.js") and
await import("./session-skill-context.js") calls inside the heartbeat
executor to static top-level imports, matching the rationale of
38933c770 (which already made the sibling pi.js import static).
This surfaces ERR_MODULE_NOT_FOUND at engine load time rather than
mid-heartbeat when a worktree is on a branch missing the helpers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This merge adds a complete plugin management system (FN-3565) with CLI commands, a loader, runner, and dashboard routes, along with project-scoped auth storage (FN-3544), native shell connection support for mobile (FN-3400) spanning onboarding, connection manager, and remote desktop handoff, and ref
Fusion-Task-Id: FN-3400
This merge adds a complete plugin management system to Fusion: a new `fn plugin` CLI command for installing/removing plugins, a plugin loader in core, a plugin runner in engine, and dashboard routes for plugin management UI, along with a plugin management guide in docs. It also documents task evalua
Fusion-Task-Id: FN-3565
- Promote the sidebar "Plan New Mission" CTA to a btn-primary (matching the
chat sidebar's "New Chat") and drop the dashed icon buttons; full-width
progress bar and Activity row each get their own line in the card.
- Auto-select the first mission in the inline desktop view so users land
in detail rather than the empty placeholder. Skipped in mobile and the
standalone modal so existing flows and unit tests stay intact.
- Replace the bare "No missions yet" line with a richer empty state that
explains what missions are and offers an inline Plan New Mission CTA.
- Guard loadMissionDetail against malformed responses (missing milestones)
so racing fetch fallbacks don't crash the detail render.
- Update three MissionManager tests to match the new copy/structure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TriageProcessor.stop() previously only halted the polling loop, so
in-flight specify sessions and their reviewer subagents kept streaming
past shutdown. Extracted the existing global-pause teardown into
abortAndDisposeActiveSessions() and call it from stop() too.
aiMergeTask creates three sessions during a merge — autostash resolver,
in-merge verification fix agent, and pull-rebase conflict resolver — but
only the autostash one was registered via onSession. The other two are
now registered (with onSession threaded through pushToRemoteAfterMerge
into the rebase resolver chain), so ProjectEngine.stop() actually
disposes whichever merger session is running when shutdown lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Recovers cleanups from stash@{2}/stash@{3} (FN-3530 merger autostashes)
that the merger never restored: 4 dead `[data-state="terminated"]`
selectors in AgentListModal.css, 3 CSS-class assertions in
agent-css-classes.test.ts targeting classes the runtime no longer emits,
and a `state: "terminated"` fixture in routes-agents.test.ts now flipped
to `paused` so the "invalid state transitions" test exercises a real
rejection (paused→paused is not in AGENT_VALID_TRANSITIONS).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>