@fusion/core
Minor Changes
-
a201f56: feat(core): add
mergeAdvanceAutoSyncproject setting ("off" | "ff-only" | "stash-and-ff")Adds the schema for a new project setting that controls what happens in other worktrees still checked out on the integration branch when the merger advances the branch ref. Previously the merger only updated
refs/heads/<branch>and left every other checkout's index and working tree pinned at the old tip, sogit statusin the user's project-root checkout reported the new commits as inverted "staged changes to be committed."Modes (default
"stash-and-ff"):"off"— preserve the legacy behavior; user mustgit pullor click the Merge Advance Notice banner Pull button."ff-only"— auto-fast-forward only clean worktrees; dirty worktrees stay untouched and the banner still surfaces."stash-and-ff"— run the Smart Pull pipeline (stash → fast-forward → pop). Pop conflicts emitmerge:auto-syncaudit events withoutcome: "stash-pop-conflict"and surface through the existing dashboard stash-conflict modal.
Schema-only in this changeset; the merger hook that consumes the setting lands in the follow-up engine change.
-
51fc826: fix(engine,core): dedup heartbeat-spawned follow-ups by parent task
Heartbeat agents create follow-up tasks via
fn_task_create. Until
now, the intake similarity guard scoped candidates bysourceAgentId
only, so the same parent task could spawn many sibling tasks across
heartbeats whenever triage rewrote their titles enough to dodge the
title-fingerprint guard.The task-scoped heartbeat now stamps
sourceParentTaskId(and
sourceRunId) on everyfn_task_create, and the intake duplicate
matcher treats a candidate as a sibling when it shares either the
caller's agent ID or the caller's parent task ID. Same-parent
siblings with similar descriptions are auto-archived as before.Tool description and heartbeat prompts also now instruct agents to
scan existing open tasks before creating, as a belt-and-suspenders
layer above the deterministic dedup.
Patch Changes
-
408e20b: fix(merger): two root-cause fixes for tasks landing in Done with no commit on main
Bug 1: sibling fusion/fn-* branch as merge target —
resolveTaskMergeTarget
previously returnedtask.baseBranchunconditionally before falling back to the
project default. When a task was dispatched as a sibling/dependent off another
in-flight task's worktree,baseBranchended up as the upstream's
fusion/fn-<id>branch. The merger then detached onto that sibling, squashed
on top of it, and advancedrefs/heads/fusion/fn-<id>— never main. FN-5233's
squash (84563e549) stranded onfusion/fn-5339; FN-5530's
(4140a3e0a) stranded onfusion/fn-5543. The resolver now refuses any
fusion/fn-\*candidate as a merge destination and falls through to the
project default. The merger emits a newmerge:merge-target-rejected-fusion-sibling
audit event so the upstreambaseBranch-propagation bug stays observable.Bug 2: deadlock-recovery mis-attributed tasks to unrelated commits —
findLandedTaskCommitstep (4) usedgit log --grep=FN-XXXXwhich matches the
entire commit message (not just the subject) and blindly accepted the first
hit. FN-5441 and FN-5446 were both marked done againste3dbfaae— an
FN-5483 commit whose body merely mentioned them by name in a paragraph about
a refusal. The grep fallback now fetches each candidate's body and re-verifies
ownership via a tightenedcommitOwnedByTask: trailers must be line-anchored
((?:^|\n)Fusion-Task-Id: <id>(?:\n|$)), and the subject fallback must match
a conventional-commit form (<type>(<id>):or<id>:), not a substring.
Prose mentions can no longer claim a task.The historical recovery for FN-5233 has been cherry-picked to main as
2d2e5b809. The other 11 affected tasks (FN-5441, FN-5446, FN-5472, FN-5484,
FN-5487, FN-5490, FN-5515, FN-5517, FN-5526, FN-5539, FN-5540, FN-5542)
remain in Done but need separate triage — 3 look like legitimate
verification-only no-ops, the remaining 9 likely lost real work. -
ec6643e: fix(test-utils): cancel subprocess tracking timer for every proc in afterEach
The vitest subprocess guard registered a 60 s "command timed out" timer for
each tracked child process and relied onafterEachto cancel it. Under
concurrent load (pnpmrecursive test runs) the timer could outlive the
originating test and fire during a later test'safterEach, surfacing as
spurious "Test subprocess guard detected unsafe child-process usage:
Timed out after 60000ms" failures attributed to a different test name.The cleanup loop now scopes "Left running" failure reporting + SIGKILL to
processes spawned by the current test, but unconditionally clears each
tracked subprocess's timer so the 60 s timeout cannot fire after the
afterEach completes. The grace period before declaring a process leaked
is also raised from 200 ms to 1 s to absorb event-loop contention from
slow git shells under recursive test load. -
4c31e88: feat(engine): merger auto-syncs project-root checkout after advancing integration-branch ref
Wires
mergeAdvanceAutoSyncinto the merger's post-ref-advance code path. AfteradvanceIntegrationBranchRefff-updatesrefs/heads/<integrationBranch>, the merger now enumerates other worktrees still on that branch (typically the user's project-root checkout) and reconciles each one's index + working tree to the new tip viasyncWorktreeToHead.The reconciliation primitive is not a
git pull— origin may still be at the previous tip (nopushAfterMerge), in which casegit pull --ff-onlyis a no-op and a naivestash → pull → popends with the worktree restored to the old state. InsteadsyncWorktreeToHead:- Diffs the worktree against the previous tip to isolate real user edits from the stale-index "phantom diff" that looks like inverted commits.
- When the worktree is clean against the previous tip, runs
git reset --hard HEADto snap index + files forward. - In
stash-and-ffmode with real edits, captures them as a binary patch against the previous tip, snaps to HEAD, thengit apply --3wayto restore. Untracked files are copied to a temp dir and restored after the snap. Patch conflicts surface assynced-with-pop-conflictwith the patch left on disk for manual recovery.
Each per-worktree attempt emits a
merge:auto-syncaudit event (newGitMutationType) with the outcome; the per-steppull:fast-forward,stash:push,stash:pop, andstash:pop-conflictevents that pass through the auditor are taggedmetadata.autoSync = trueso downstream consumers can attribute them.The user-facing effect: with the default
mergeAdvanceAutoSync: "stash-and-ff", after a Fusion task merges the user'sgit statusin the project-root checkout becomes clean and the working tree shows the new commits' content — no manualgit resetor Pull-button click required. SetmergeAdvanceAutoSync: "off"to restore the legacy behavior (the Merge Advance Notice banner still surfaces and the user pulls by hand).Backstopped by
merger-auto-sync.slow.test.tscovering: clean-sync snaps both index and files forward, ff-only with real edits is a no-op, stash-and-ff preserves untracked local files across the snap, task worktrees onfusion/fn-*branches are correctly skipped, and an empty branch map emits nothing.
@fusion/dashboard
Minor Changes
-
6e7f1e5: feat(dashboard): explain "Recent integration-branch advances" and add a one-click "Sync working tree" fix
Two additions to Git Manager → Status:
Info disclosure — an
[i]button next to the "Recent integration-branch advances (N need action)" header toggles an inline explainer. Covers what an "advance" is, what eachautoSyncOutcomevalue means (clean-sync,synced-with-edits-restored,off / not run,stash-failed,would-conflict, …), and where to enablemergeAdvanceAutoSyncfor the permanent fix.Sync working tree button — when ≥1 advance shows
needsAction, a button surfaces in the same header that calls the existingPOST /api/git/pull(FN-5358 Smart Pull machinery: auto-stash dirty edits, fast-forward pull, restore stash, surface conflicts). On success the extended git status auto-refetches and the "need action" count drops; on conflict, the existing error toast fires.No new state machine —
handlePull/remoteLoading === "pull"is the same plumbing the existing Pull button uses. -
85786e7: feat(dashboard): show extended integration-branch + working-tree state in Git Manager
Repository Status panel now answers "what is the actual state of my project root vs the integration branch?" so operators can be sure of the picture even when the Merge Advance Notice banner has been dismissed.
GET /api/git/statusaccepts a new?extended=1query and returns additional optional fields:- integrationBranch + integrationBranchSource — the canonical branch (resolved via
settings.integrationBranch→ legacybaseBranch→origin/HEAD→main) and where the value came from. - integrationTipSha / originIntegrationTipSha — SHAs at both ends, so operators can spot when local main has been advanced by the merger but origin/main hasn't caught up.
- aheadOfIntegration / behindIntegration — HEAD vs local integration tip (useful when on a non-integration branch).
- aheadOfOriginIntegration / behindOriginIntegration — local integration tip vs
origin/<branch>. - dirtyDetails — staged/modified/untracked/conflicted counts + a 12-line porcelain sample.
- indexStaleVsHead — true when the index reflects a previous tip and the worktree is clean against the index but not against HEAD. Surfaces the exact "phantom staged changes" scenario that
mergeAdvanceAutoSyncexists to fix. - stashCount — for at-a-glance recovery awareness.
- recentMergeAdvances — up to 5 recent
merge:integration-ref-advanceaudit events for the project root, joined with theirmerge:auto-syncoutcomes; entries whose auto-sync didn't successfully bring this worktree forward are flaggedneedsAction: true.
GitManagerModalnow renders all of this:- The existing Branch / Commit / Working Tree / Remote Sync cards gain sub-text — Working Tree shows staged/modified/untracked/conflicted breakdown; Branch shows whether you're on the integration branch.
- A second row of cards adds Integration branch (with resolution source + tip SHA), HEAD-vs-integration ahead/behind, local-integration-vs-origin ahead/behind, and stash count.
- A yellow warning panel appears when
indexStaleVsHeadis true, telling the operator to enablemergeAdvanceAutoSyncor rungit reset --hard HEAD. - A "Recent integration-branch advances" list shows the last few merger advances with their per-advance auto-sync outcome, color-coded by whether they still need action.
All
fetchGitStatus(projectId)calls insideGitManagerModalnow pass{ extended: true }. Other callers in the app are unaffected — the extra fields are optional and the un-extended response shape is unchanged. - integrationBranch + integrationBranchSource — the canonical branch (resolved via
Patch Changes
-
60a0012: fix(dashboard): stop main-chat and quick-chat composers from instantly dismissing the Android soft keyboard
Two layered Android-specific fixes for the chat composers:
-
The body scroll-lock applied while the keyboard is open in main chat was an iOS-specific workaround for visualViewport drift. On Android Chrome it does the opposite of what we want — mutating
body { position: fixed; ... }while the keyboard is opening causes Chrome to treat it as a focus-target relayout and immediately dismisses the keyboard.useMobileScrollLockis now gated to iOS UAs. -
ChatView and QuickChatFAB both had an iOS-specific
onTouchStarton the textarea that calledevent.preventDefault()and then programmatically refocused the input (to suppress iOS's visualViewport auto-scroll on re-focus). On Android,preventDefaulton a textarea touchstart prevents the soft keyboard from opening — programmaticfocus()alone does not raise the Android keyboard. Result: tapping the composer focused the input but the keyboard never appeared, looking like an instant dismiss. The touchstart workaround is now gated to iOS UAs viaisIOS().
-
-
a10fc56: fix(dashboard): keep Android keyboard open in main chat; disable kanban pinch-zoom
Two Android-specific fixes:
-
Keyboard dismissing in main chat.
mobileKeyboardOpeninApp.tsx(derived fromuseMobileKeyboard) gatesproject-content--with-mobile-nav/--with-footerclassName assignment and MobileNavBar rendering. When the soft keyboard opened, those classes were removed and the nav unmounted, shrinking padding-bottom by ~80px in a single render. Android Chrome treats the resulting jump of the focused chat input as the focus target moving and instantly dismisses the keyboard. Withinteractive-widget=resizes-contentset on Android, the layout viewport itself shrinks with the keyboard, so the hide-nav-on-keyboard behavior was redundant on Android (and harmful). The whole pattern is now gated to iOS viaisIOS(). iOS path is unchanged. -
Pinch-zoom on kanban. Android Chrome ignores
user-scalable=nofor accessibility, and the kanban board'soverflow-x: autocolumns combined with the inflated ICB produce a broken visual when the user zooms out. Addstouch-action: pan-x pan-ytohtml, bodyinside the mobile media query, which keeps scroll panning but disables pinch-zoom (Chat and MissionManager were unaffected because they don't expose a wide horizontal scrollable region).
-
-
de67c51: fix(dashboard): pull syncs the worktree to local integration tip, not just to origin
The integration-mode
POST /api/git/pull(used by the merge-advance-notice banner) only rangit merge --ff-only origin/<branch>after fetching. When the merger had advanced localrefs/heads/<integrationBranch>viaupdate-refbut the user hadn't pushed yet, the worktree's HEAD already resolved to the new sha (symbolic ref follow) but the working tree and index were still at the old state. The fast-forward step short-circuited (already up to date with origin) and the user saw "Pull completed" withfromSha === toShawhile their files visibly stayed behind.Pull now explicitly resets the worktree to
refs/heads/<integrationBranch>after the origin fast-forward step. The autostash above protects user edits, so the reset is safe regardless of whether the origin FF ran. -
5d35b64: fix(dashboard): remove duplicate integration-advances UI; Sync working tree is now pure-local (no origin fetch)
Removed duplicate UI — Git Manager → Status had two overlapping sections rendering the same data: a
Sync local tipbutton + aRecent integration advanceslist, sitting above the highlightedRecent integration-branch advancesblock (the one with the lost-work warnings). Deleted the duplicate (gm-integration-actions+gm-recent-advances) along with the deadmergeAdvanceEventsstate, fetcher, and SSE subscription that only fed it.Sync working tree is now pure-local — for the "N need action" case the merger has already advanced
refs/heads/<integration>locally and the worktree just needs to follow. Previously the button called the integration-mode pull which rantryFastForwardFromOriginfirst, silently pulling in unrelated remote commits. NewskipOriginFetchoption onPullGitBranchOptions.integration(and the matchingPOST /api/git/pullbody field) skips the origin step entirely. The Sync button passesskipOriginFetch: true, so the sequence is: auto-stash →git reset --hard refs/heads/<integration>→ restore stash. Origin is not touched.Help disclosure updated to match the new behavior.
-
4f38ed1: fix(dashboard): clear
needs actionon recent integration-branch advances after manual syncThe Git Manager's "Recent integration-branch advances" list derived
needsActionpurely from the originalmerge:auto-syncaudit-event outcome. When the operator clicked "Sync working tree" — or fixed up the worktree by hand — the worktree caught up to the integration tip, but the list kept showing "(N need action)" because the historical audit events still recorded the original failure/disabled state.collectRecentMergeAdvancesnow also checks whether each advance'stoShais reachable from the current HEAD. If it is, the worktree already contains that advance andneedsActionis false regardless of what the audit trail recorded. -
ef12df4: fix(dashboard): close 8 review findings on extended Git Manager status + Integration branch setting
Settings persistence (data-loss) — the project-settings patch builder now applies null-as-delete to all non-model keys, matching the global-settings branch. Previously, clearing the Integration branch field (picking
(auto-detect)or clickingUse dropdown) setintegrationBranch: undefined, whichJSON.stringifysilently dropped — the server retained the stale explicit value and the operator could not un-pin the branch from the UI.isIndexStalewas wrong both directions — the heuristic (diff --cached --name-onlynon-empty ANDdiff --name-onlyempty) fired false-positive on benigngit addand false-negative whenever the worktree had any unrelated edit. Replaced with a reflog-anchored check: stale iffrefs/heads/<integrationBranch>@{1}exists, HEAD is a descendant of it, andgit diff-index --cached <prevTip>is empty (i.e. the index exactly matches the pre-advance state).Auto-sync attribution — two fixes to
collectRecentMergeAdvancesinregister-git-github.ts:- Auto-sync events are now matched by
(taskId, newSha)instead oftaskId-only. A task that produced multiple advances over time no longer has all its older entries mislabeled with the most-recent outcome. worktreePathcomparison now runs both sides throughfs.realpathSyncfirst. On macOS the merger emits canonicalized paths (viacanonicalizePathinworktree-pool.ts) while the route was called with the store's rawrootDir; symlinked project paths caused every advance to be markedneedsAction: trueindefinitely.
Extended path no longer 500s on git failure — the
?extended=1branch wrapscomputeExtendedGitStatusin its own try/catch and falls back to the basic status shape on any unhandled failure. Previously an unguardedgit branch --show-currentthrow escaped to the route's outer catch and returned HTTP 500, while the basic path returned 200 with the swallowed-failure shape — surface parity matters because the dashboard always passesextended=1and would otherwise render an error toast where it should render the degraded panel. Also wrapped the same call insidecomputeExtendedGitStatusso detached-HEAD / non-git states return an emptycurrentBranchinstead of throwing.Integration branch falls back to
refs/remotes/origin/<branch>— when the configured branch exists only as a remote-tracking ref (e.g. operator setintegrationBranch: "release/v2"without evergit switch-ing it locally),integrationTipShanow resolves to the origin tip instead of being null. A newintegrationTipSource: "local" | "remote-only" | "missing"field tells the UI which side won; the Git Manager surfaces this with a(remote-only — run git switch <branch> to track locally)sub-text and ano ref founderror state when both refs are missing.Copy commit hash shows two buttons — the Copy button now copies
status.commit(the short SHA actually displayed in the<code>element). A second Copy-full button surfacesstatus.headShafor git operations that need the 40-char SHA. Previously the single button silently copied the full SHA when extended was on, so what the user saw on screen was no longer what they pasted.Detached HEAD no longer shows misleading "(not on main)" —
git branch --show-currentreturns empty on detached HEAD; the route now leavesisOnIntegrationBranchasundefined(notfalse) in that case, and the UI's "(not on )" sub-text only renders when we know we're on a different branch — not when we're on no branch at all. - Auto-sync events are now matched by
-
d5cfa92: fix(dashboard): close 7 review findings on the extended-status hardening pass
Follow-up to the prior fix commit; closes 7 more issues that an independent code review surfaced.
Settings inheritance regression (high) —
SettingsModal.handleSave's non-model project branch lost the "only write if changed" gate when the prior commit added null-as-delete support. Result: every effective/inherited project key was being persisted as an explicit project override on every save, silently breaking inheritance across ~30+ keys. Restored thevalue !== initialProjectValuegate, matched against the model-lane branch's existing pattern.Git Manager
Local <branch> vs origincard showed misleading "Synced" in remote-only mode — whenintegrationTipSource === "remote-only", bothaheadOfOriginIntegration/behindOriginIntegrationare deliberately undefined (there's no local branch to compare), but the card's render fell through to(ahead ?? 0) === 0 && (behind ?? 0) === 0 → "Synced". Now renders an explicit "no local tracking" sub-text in that case, with a separateHEAD vs origin/<branch>card surfacing a meaningful distance.isIndexStaleextended to multi-hop and gated to integration-branch worktrees —- Walks up to 16
refs/heads/<integration>reflog entries so an A→B→C burst whose middle sync also missed is detected (the prior check only consulted@{1}). - Only fires when
isOnIntegrationBranch === true. Previously, a feature-branch worktree whose HEAD happened to descend from<integration>@{1}(e.g.git switch -c hotfix main@{N}) would trip the stale-index warning despite being perfectly healthy.
enumeration-failedauto-sync events no longer dropped — the new(taskId, newSha)join filter required bothworktreePathandnewShaon every auto-sync event, which discarded the merger's early-failure events that emit neither. Now: events with both fields use the per-advance pair-key (with macOS realpath canonicalization on both sides); events with neither use a task-id fallback so the diagnostic outcome still surfaces on the matching advance.aheadOfIntegrationno longer silently shifts semantics — split into three distinct distance fields so consumers don't have to readintegrationTipSourceto know which comparison they got:aheadOfIntegration/behindIntegration— HEAD vs local integration tip; undefined when only the remote tip exists.aheadOfIntegrationRemote/behindIntegrationRemote— HEAD vsorigin/<integrationBranch>; defined whenever the remote tracking ref exists.aheadOfOriginIntegration/behindOriginIntegration— local integration tip vsorigin/<integrationBranch>; defined only when both refs exist.
currentBranchfailure no longer masks wrong-branch state —git branch --show-currentreturns empty on detached HEAD (success) and throws on transient git errors (lock contention, timeout). The prior catch collapsed both intocurrentBranch = ""so the UI couldn't distinguish them. NewcurrentBranchDetectionFailed?: booleanfield onGitStatuslets the UI surface "branch detection unavailable" on a real failure rather than silently hiding the wrong-branch warning. - Walks up to 16
-
916047c: feat(dashboard): Integration branch setting is now a dropdown of local branches with Custom… fallback
Replaces the plain text input with a
<select>that lists the project's local branches (loaded viafetchGitBrancheswhen the Merge section becomes visible) plus an(auto-detect — origin/HEAD → main)default and aCustom…option for branches that don't exist locally yet.Branch list is deduplicated and sorted with common integration names (
main,master,trunk,develop) pinned to the top so the typical case is one click. ChoosingCustom…swaps in a text input with aUse dropdownlink to revert.A previously-saved value that isn't in the loaded list (e.g. branch deleted locally, or initial load before branches fetch resolved) falls through to the custom text input automatically so the operator can still see and edit it.
-
084bdc6: feat(dashboard): expose
integrationBranchsetting in the project settings modalAdds a text input for the canonical integration branch (the branch Fusion merges tasks into, and the reference for all ahead/behind / overlap / pre-rebase computations). Lives directly under the Auto-completion mode select inside the merge-strategy panel — visible regardless of direct vs PR mode, since the setting applies to both.
Blank value (the default) preserves the existing auto-resolution cascade:
integrationBranch→ legacybaseBranch→origin/HEADsymbolic ref → fallbackmain. Setting it tomaster/trunk/develop/ etc. pins the resolution explicitly without changing other settings.Field trims whitespace and stores
undefined(not empty string) when cleared so the auto-resolution remains active. -
d8493f9: feat(dashboard): expose
mergeAdvanceAutoSyncin the project settings modalAdds the missing form control for the auto-sync mode introduced by the merger hook. Lives next to the existing Direct merge commit routing / Integration worktree controls inside the merge-strategy panel and only renders when
mergeStrategy === "direct". Three options with the same labels and descriptions asdocs/settings-reference.md:- Stash + fast-forward (default) — preserve local edits across the auto-snap
- Fast-forward only — skip dirty worktrees, surface the banner instead
- Off — legacy behavior, project root stays stale until manual pull
Value is normalized through
normalizeMergeAdvanceAutoSyncModeon both the merged-settings and scoped-settings load paths so a missing/invalid stored value cleanly falls back to the default without spamming validation errors. -
99359b6: fix(dashboard): unbreak Merge Advance Notice banner dismiss and suppress when auto-sync already handled it
Two bugs were keeping the banner stuck on screen even when there was nothing for the user to do:
- Dismiss was dead. The
noticememo never applieddismissedShas, so clicking the close button (or a successful Pull, which callsdismiss()after the API returns) updated localStorage but the same advance event kept matching the filter and the banner re-rendered immediately. - Auto-sync success was ignored. With the new
mergeAdvanceAutoSyncsetting at itsstash-and-ffdefault, the merger snaps the project-root checkout forward as part of the merge — there is nothing left to pull. The banner kept appearing anyway because the route'sautoSyncpayload wasn't consulted. Clicking Pull then hit/api/git/pull, which fetched origin (no change, since the merger only advanced the local ref) and returnedpull-cleanwith no actual work done.
The
noticememo now (a) filters outdismissedShas, and (b) suppresses any advance event whoseautoSyncentry for the current user'sworktreePathreportsclean-syncorsynced-with-edits-restored. Conflict and skipped outcomes (synced-with-pop-conflict,skipped-dirty,skipped-*,failed) still surface the banner so the user can recover.Banner suppression checks the per-worktree path, so a multi-checkout project where auto-sync handled one root and a sibling root is still stale will keep showing the banner on the stale one.
- Dismiss was dead. The
-
6083de2: fix(dashboard): unbreak Merge Advance Notice banner by preserving store
thisbinding in events endpointGET /api/tasks/merge-advance-eventswas extractinggetRunAuditEventsoff the scoped store as a bare function reference and calling it withoutthis, which madethis.db.prepare(...)throw "Cannot read properties of undefined (reading 'db')" on every request. TheuseMergeAdvanceNoticehook caught the failure silently (catch { setEvents([]) }), so the banner never appeared even after the merger advanced the integration branch ref.Fix: keep the store reference and call
storeWithRunAudit.getRunAuditEvents(...)as a method sothisis preserved, matching the pattern used by other routes that read run-audit events. -
dc94494: fix(engine,dashboard): close 7 code-review findings on the mergeAdvanceAutoSync hook
Tightens the freshly-landed merger auto-sync feature based on a structured code review.
Data-loss fixes in
syncWorktreeToHead:- Untracked-file restore now compares against
git ls-tree -r --name-only HEADto detect when the new tip introduced a tracked file at the same path; collisions are reported inuntrackedSkippedAsTrackedand the user's bytes stay in the stage dir instead of clobbering the merged content. - When
git apply --3wayfails because a patched file was deleted/renamed at the new tip (--diff-filter=Ureturns nothing because nothing got staged),conflictedFilesfalls back to parsingdiff --git a/<p> b/<p>headers out of the captured patch — so the conflict surfaces with the right file names instead of[]. git ls-files/diffcalls now pass-c core.quotePath=falseso paths with non-ASCII or special characters round-trip throughcopyFileSyncinstead of failing on backslash-escaped octal tokens.- The stash-and-ff path re-verifies
rev-parse HEAD === newShaimmediately before each destructivereset --hard HEAD; a concurrent merger advance now bails withskipped-head-not-at-new-sha(with the captured patch preserved on disk) instead of applying the patch against the wrong tree. - The stage dir is now tracked with a
preserveStageDirflag in atry/finally: it is rm'd on all clean paths and onskipped-head-not-at-new-shaexits, but preserved whenever the user's edits live only inpatchPath(pop-conflict, untracked-collides-with-tracked, reset failure, outer exception). - Patch is written to disk before the apply attempt, not only on failure, so a crash between snapshot and apply doesn't lose the user's edits.
Multi-worktree-same-branch fix:
- New
getRegisteredWorktreeBrancheshelper inworktree-pool.tsreturns ALL(branch, worktreePath)entries rather than collapsing duplicates into aMap<branch, path>. Multiple worktrees can legitimately share a branch when the user created secondary checkouts viagit worktree add --force -b; the merger now syncs every one of them instead of silently skipping all but the last.
Contract + surfacing fixes:
- JSDoc on
merge:auto-syncGitMutationType now documents the actually-emitted outcome strings (clean-sync,synced-with-edits-restored,synced-with-pop-conflict,skipped-*,failed,enumeration-failed,exception) and the actualstageenum, replacing the obsoletesmartPull-shaped strings. GET /api/tasks/merge-advance-eventsnow joinsmerge:auto-syncevents within a ±5min window of each advance and returns them in a newautoSync: AutoSyncOutcome[]field;useMergeAdvanceNoticeexposes the same shape so the banner can surface pop-conflicts (includingpatchPathpointing at the user's saved edits) instead of leaving them in a black hole.
Hygiene:
- Merger's setting read now uses
normalizeMergeAdvanceAutoSyncMode(settings.mergeAdvanceAutoSync)(the exported normalizer) instead of an inline equality check +as unknowncast that bypassed type-checking.
New backstop tests in
merger-auto-sync.slow.test.ts:- Untracked file colliding with a newly-tracked path is NOT overwritten and the merged content survives.
git apply --3wayfailure on a file deleted at the new tip populatesconflictedFilesfrom the patch header.
Route test asserts
autoSyncoutcomes are joined onto the matching advance event within the time window. - Untracked-file restore now compares against
-
e138289: fix(dashboard): compensate Android Chrome inflated ICB for fixed-position UI
Android Chrome (multi-window / split-screen / certain WebView configs) can leave the initial containing block (window.innerWidth/Height) stuck larger than the actual rendered canvas — DOM, body, and visualViewport report the true dimensions, but
position: fixeduses the ICB, pinning fixed-bottom elements off the bottom of the visible viewport. JS-side meta override (both setAttribute and full element replacement) does not force Chrome to recompute the ICB on these builds.Instead, publish the ICB→visualViewport delta as CSS variables (
--icb-bottom-offset,--icb-right-offset) on<html>and consume them inMobileNavBarandExecutorStatusBarso they pin to the visible viewport edge regardless of ICB drift. The math also handles pinch-zoom in (offsets compensate) and pinch-zoom out (offsets clamp at 0). Healthy browsers see 0px and behave unchanged. -
76429a8: fix(dashboard): keep mobile nav bar pinned to page bottom when keyboard opens
The mobile nav bar's
bottomdefaults tovar(--icb-bottom-offset), which on iOS equals the soft-keyboard height once it opens — floating the bar above the keyboard. The existing.mobile-nav-bar--keyboard-openoverride (which pinsbottom: 0) was only applied when!modalManager.anyModalOpen && isIOS(), so the bar still tracked the keyboard with a modal open. IntroducesmobileNavKeyboardOpen = isMobile && keyboardOpenas a nav-bar-only flag so the bar stays pinned in all keyboard cases. Content padding and ExecutorStatusBar remain on the gatedmobileKeyboardOpento preserve their existing behavior. -
ed4d021: fix(dashboard): keep mobile nav visible on Android in landscape and when keyboard opens
- Broaden mobile media query to
(max-width: 768px), (max-height: 480px)so phones held in landscape (which exceed 768 CSS px wide) still render the bottom nav and mobile board layout instead of the desktop horizontally-scrollable columns. - Distinguish pinch-zoom from keyboard-open in
useMobileKeyboardby checkingvisualViewport.scale > 1— Android Chrome ignoresuser-scalable=nofor a11y, and a zoomed-in textarea was false-positiving keyboard-open and hidingMobileNavBar. - Use
documentElement.clientHeightinstead of stalewindow.innerHeightwhen computing keyboard overlap (Android multi-window can leaveinnerHeightcached at a wildly different value than the actual layout viewport). - Add
interactive-widget=resizes-contentto the viewport meta so Android Chrome shrinks the layout viewport with the soft keyboard, matching iOS behavior.
- Broaden mobile media query to
-
Updated dependencies [98033bc]
-
Updated dependencies [db9928a]
-
Updated dependencies [02971ef]
-
Updated dependencies [9ce26ee]
-
Updated dependencies [e708870]
-
Updated dependencies [a3ec2e5]
-
Updated dependencies [408e20b]
-
Updated dependencies [acf3502]
-
Updated dependencies [ec6643e]
-
Updated dependencies [a201f56]
-
Updated dependencies [4c31e88]
-
Updated dependencies [dc94494]
-
Updated dependencies [bf4428c]
-
Updated dependencies [0c0839e]
-
Updated dependencies [ec1269f]
-
Updated dependencies [51fc826]
-
Updated dependencies [d02cd38]
- @fusion/engine@0.33.0
- @fusion/core@0.33.0
- @fusion-plugin-examples/cli-printing-press@0.1.10
- @fusion-plugin-examples/dependency-graph@0.1.24
- @fusion-plugin-examples/roadmap@0.1.12
- @fusion-plugin-examples/cursor-runtime@0.1.12
- @fusion-plugin-examples/droid-runtime@0.1.19
- @fusion-plugin-examples/hermes-runtime@0.2.43
- @fusion-plugin-examples/openclaw-runtime@0.2.43
- @fusion-plugin-examples/paperclip-runtime@0.2.43
@fusion/desktop
Patch Changes
- Updated dependencies [60a0012]
- Updated dependencies [a10fc56]
- Updated dependencies [de67c51]
- Updated dependencies [6e7f1e5]
- Updated dependencies [5d35b64]
- Updated dependencies [408e20b]
- Updated dependencies [4f38ed1]
- Updated dependencies [ec6643e]
- Updated dependencies [85786e7]
- Updated dependencies [ef12df4]
- Updated dependencies [d5cfa92]
- Updated dependencies [916047c]
- Updated dependencies [084bdc6]
- Updated dependencies [a201f56]
- Updated dependencies [d8493f9]
- Updated dependencies [99359b6]
- Updated dependencies [6083de2]
- Updated dependencies [4c31e88]
- Updated dependencies [dc94494]
- Updated dependencies [e138289]
- Updated dependencies [76429a8]
- Updated dependencies [ed4d021]
- Updated dependencies [51fc826]
- @fusion/dashboard@0.33.0
- @fusion/core@0.33.0
@fusion/engine
Minor Changes
-
98033bc: feat(engine): guard one engine per project per machine
Adds a per-machine singleton lock that engages before each engine
starts, preventing twofndashboard processes from running engines
for the same project on the same host (a scenario that previously
caused worktree corruption and task-state races for in-process
projects).The guard combines two independent checks:
- A
proper-lockfile-backed file at<project>/.fusion/engine.lock
with stale-lock recovery. - A loopback listener (UDS on POSIX, named pipe on Windows) on a
hashed per-project address.
Failures throw
EngineAlreadyRunningError; both guards are released
onstopAll()/pauseProject(). - A
-
db9928a: feat(engine): export
smartPull()library for stash-aware fast-forward of a worktreeStandalone stash → fast-forward → pop implementation that the merger's upcoming
mergeAdvanceAutoSynchook calls after advancing the integration-branch ref to auto-sync other worktrees still pinned at the previous tip. Returns a discriminated union (clean-pull | stash-pull-pop | stash-pop-conflict | skipped-dirty | skipped-not-on-branch | failed) and accepts an optional audit emitter so callers can recordpull:fast-forward,stash:push,stash:pop, andstash:pop-conflictrun-audit events.The dashboard's user-triggered Pull continues to use the existing
POST /api/git/pullintegration path (which runs the AI-aware autostash throughrestoreUnrelatedRootDirChanges) and is unchanged by this changeset —smartPull()is intentionally simpler so the merger's post-advance auto-sync stays free of mid-merge AI conflict resolution. -
4c31e88: feat(engine): merger auto-syncs project-root checkout after advancing integration-branch ref
Wires
mergeAdvanceAutoSyncinto the merger's post-ref-advance code path. AfteradvanceIntegrationBranchRefff-updatesrefs/heads/<integrationBranch>, the merger now enumerates other worktrees still on that branch (typically the user's project-root checkout) and reconciles each one's index + working tree to the new tip viasyncWorktreeToHead.The reconciliation primitive is not a
git pull— origin may still be at the previous tip (nopushAfterMerge), in which casegit pull --ff-onlyis a no-op and a naivestash → pull → popends with the worktree restored to the old state. InsteadsyncWorktreeToHead:- Diffs the worktree against the previous tip to isolate real user edits from the stale-index "phantom diff" that looks like inverted commits.
- When the worktree is clean against the previous tip, runs
git reset --hard HEADto snap index + files forward. - In
stash-and-ffmode with real edits, captures them as a binary patch against the previous tip, snaps to HEAD, thengit apply --3wayto restore. Untracked files are copied to a temp dir and restored after the snap. Patch conflicts surface assynced-with-pop-conflictwith the patch left on disk for manual recovery.
Each per-worktree attempt emits a
merge:auto-syncaudit event (newGitMutationType) with the outcome; the per-steppull:fast-forward,stash:push,stash:pop, andstash:pop-conflictevents that pass through the auditor are taggedmetadata.autoSync = trueso downstream consumers can attribute them.The user-facing effect: with the default
mergeAdvanceAutoSync: "stash-and-ff", after a Fusion task merges the user'sgit statusin the project-root checkout becomes clean and the working tree shows the new commits' content — no manualgit resetor Pull-button click required. SetmergeAdvanceAutoSync: "off"to restore the legacy behavior (the Merge Advance Notice banner still surfaces and the user pulls by hand).Backstopped by
merger-auto-sync.slow.test.tscovering: clean-sync snaps both index and files forward, ff-only with real edits is a no-op, stash-and-ff preserves untracked local files across the snap, task worktrees onfusion/fn-*branches are correctly skipped, and an empty branch map emits nothing. -
51fc826: fix(engine,core): dedup heartbeat-spawned follow-ups by parent task
Heartbeat agents create follow-up tasks via
fn_task_create. Until
now, the intake similarity guard scoped candidates bysourceAgentId
only, so the same parent task could spawn many sibling tasks across
heartbeats whenever triage rewrote their titles enough to dodge the
title-fingerprint guard.The task-scoped heartbeat now stamps
sourceParentTaskId(and
sourceRunId) on everyfn_task_create, and the intake duplicate
matcher treats a candidate as a sibling when it shares either the
caller's agent ID or the caller's parent task ID. Same-parent
siblings with similar descriptions are auto-archived as before.Tool description and heartbeat prompts also now instruct agents to
scan existing open tasks before creating, as a belt-and-suspenders
layer above the deterministic dedup. -
d02cd38: feat(merger): scope pnpm verification to changed packages and short-circuit out-of-scope fix loop
In a pnpm workspace, inferDefaultTestCommand now derives the set of packages touched by the branch diff and emits
pnpm --filter "<pkg>...^" testinstead ofpnpm test. This prevents flakes in unrelated packages from blocking merges. When git context is unavailable or changes are root-only, the command falls back to the unscopedpnpm test.When the in-merge fix agent makes no changes and all failing test files are outside the branch's diff, the merger now marks the task
status: "failed"immediately with a clear "out-of-scope flake" message rather than retrying into the limbo-recovery cycle.
Patch Changes
-
02971ef: fix(engine): treat foreign-attributed commits already on main as promoted
assertCleanBranchAtBaseflagged any commit inbaseSha..branchName
whoseFusion-Task-Idtrailer pointed at a different task as
contamination. That misclassified the FN-5475 cascade: the engine
fast-forwards localmainwith single-parent task commits, and any
worktree created during the brief window where localmaincarried a
sibling task's tip inherited that commit. The audit later (correctly)
saw the commit as not-yet-on-main from its merge-base perspective and
threwBranchCrossContaminationError.The audit now skips foreign-attributed commits that are reachable from
localmain(git merge-base --is-ancestor <sha> main). Commits on
main were promoted through integration regardless of whose trailer
they carry, and downstream branches that inherited them via main are
not contaminated.Resume verifier (FN-5475 fix #2) and the auto-recovery handler
fallback (FN-5475 fix #3) remain in place as defense-in-depth for
the rarer variants (local main rewound, foreign commit not yet on
main when the audit fires). -
9ce26ee: fix(engine): un-deadcode the bootstrap-misbinding auto-recovery fallback
The auto-recovery handler in
auto-recovery-handlers/branch-worktree.ts
calledclassifyBootstrapMisbindingwithforeignCommits: []because it
had noBranchCrossContaminationErrorin hand (it discovers the conflict
viainspectBranchConflict). The classifier's predicate gated on
foreignCommits.length > 0, so the input always resolved to
isBootstrapMisbinding: falseand the re-anchor block was effectively
dead code.The handler also used
ctx.task.baseCommitShaas the contamination base,
which is deliberately preserved across sessions for diff math (FN-4417)
and can lag localmainby many commits — causing legitimately-merged
landings to be classified as foreign at this layer.Changes:
classifyBootstrapMisbindingnow derives the foreign-commit count from
its owngit log baseSha..branchNamewalk;input.foreignCommitsis
optional and advisory only. The result type gainsforeignCommitCount.- The
branch-worktreerecovery handler stops passing an empty array and
computes a fresh merge-base against localmain(falling back to
origin/main), mirroring the executor's primary contamination path. - Regression tests cover both the no-
foreignCommitscall shape and the
foreignCommitCountfield.
-
e708870: fix(engine): verify resumed worktree branches aren't bootstrap-misbound
acquireTaskWorktreeshort-circuited the resume path when
task.worktreeexisted on disk and classifiedok, handing the
worktree back to the executor without inspecting its branch history.
If the branch had been created from a poisoned local-main tip (a
sibling task's commit), the executor preflight would later flag every
intermediate landing as foreign and the task would loop through
contamination recovery until pausing for human adjudication
(observed in the FN-5475 cascade).The resume path now computes a fresh merge-base against local
main
(falling back toorigin/main) and runsclassifyBootstrapMisbinding
on the branch. When the range is purely foreign with zero own commits,
it re-anchors the branch inline viareanchorBranchToBaseand emits a
branch:reanchoraudit event withtrigger: "resume-misbinding".Mixed contamination (own + foreign, or non-attributed commits) is
deliberately left to the executor's existing primary path so the
richer adjudication flow still applies. -
a3ec2e5: fix(engine): never create task branches from arbitrary HEAD in autocorrect
attemptBranchAutocorrectpreviously fell back togit checkout -B <expected>with no start point when rename was not applicable. If the
worktree's HEAD happened to be at a previous occupant's commit (e.g. an
orphaned tip from a different task), the new branch label silently
captured that commit — the "branch: Created from HEAD" contamination
pattern that the cross-contamination guard then refuses to auto-resolve.This is the only branch-creation site in the engine that did not thread
a resolved base SHA; every other path (prepareForTask,
reanchorBranchToBase) already passes the base explicitly.Autocorrect now verifies the expected ref exists and uses a plain
git checkout, so it can only switch to an already-existing branch.
When the ref is missing it returnsfailed, letting upstream recovery
(which knows the proper base) re-anchor withprepareForTask/
reanchorBranchToBase. -
408e20b: fix(merger): two root-cause fixes for tasks landing in Done with no commit on main
Bug 1: sibling fusion/fn-* branch as merge target —
resolveTaskMergeTarget
previously returnedtask.baseBranchunconditionally before falling back to the
project default. When a task was dispatched as a sibling/dependent off another
in-flight task's worktree,baseBranchended up as the upstream's
fusion/fn-<id>branch. The merger then detached onto that sibling, squashed
on top of it, and advancedrefs/heads/fusion/fn-<id>— never main. FN-5233's
squash (84563e549) stranded onfusion/fn-5339; FN-5530's
(4140a3e0a) stranded onfusion/fn-5543. The resolver now refuses any
fusion/fn-\*candidate as a merge destination and falls through to the
project default. The merger emits a newmerge:merge-target-rejected-fusion-sibling
audit event so the upstreambaseBranch-propagation bug stays observable.Bug 2: deadlock-recovery mis-attributed tasks to unrelated commits —
findLandedTaskCommitstep (4) usedgit log --grep=FN-XXXXwhich matches the
entire commit message (not just the subject) and blindly accepted the first
hit. FN-5441 and FN-5446 were both marked done againste3dbfaae— an
FN-5483 commit whose body merely mentioned them by name in a paragraph about
a refusal. The grep fallback now fetches each candidate's body and re-verifies
ownership via a tightenedcommitOwnedByTask: trailers must be line-anchored
((?:^|\n)Fusion-Task-Id: <id>(?:\n|$)), and the subject fallback must match
a conventional-commit form (<type>(<id>):or<id>:), not a substring.
Prose mentions can no longer claim a task.The historical recovery for FN-5233 has been cherry-picked to main as
2d2e5b809. The other 11 affected tasks (FN-5441, FN-5446, FN-5472, FN-5484,
FN-5487, FN-5490, FN-5515, FN-5517, FN-5526, FN-5539, FN-5540, FN-5542)
remain in Done but need separate triage — 3 look like legitimate
verification-only no-ops, the remaining 9 likely lost real work. -
acf3502: fix(merger): refuse to finalize a task as no-op when modifiedFiles is non-empty
Third root-cause fix for tasks marked Done with no commit on main (the first
two — sibling-branch merge target + grep mis-attribution — landed in the
previous commit). When the executor produced edits but the squash didn't
land them as a commit (uncommitted in the worktree, squashed against the
wrong branch, branch dropped by reuse-handoff churn, etc.), the merger's
classifyOwnedLandedEvidencewould returnproven-no-opor
no-changes-finalizedand bothaiMergeTaskandrecoverNoOpReviewTasks
would happily move the task to Done while clearingmodifiedFilesto[]
— silently destroying the audit trail of what was lost.Both call sites now gate the no-op finalize on
task.modifiedFiles.length:
if the task claims work was done but no commit landed, move the task back
totodowith progress preserved and emit a new
task:finalize-lost-work-blockedaudit event. The next executor run
re-attempts the work; the operator sees the audit event in the run-audit
timeline.The post-hoc
reconcileDoneTaskIntegritypath is intentionally NOT gated —
it cleans up tasks that are already in Done (legacy state), which is
out-of-scope for the lost-work prevention. This matters: 9 lost-work tasks
were already in this state at sweep time (FN-5441, FN-5446, FN-5487,
FN-5490, FN-5517, FN-5526, FN-5539, FN-5540, FN-5542) and need to be
re-spec'd as fresh tasks rather than auto-reconciled. See
docs/incidents/2026-05-23-lost-work-tasks.mdfor the per-task catalog. -
dc94494: fix(engine,dashboard): close 7 code-review findings on the mergeAdvanceAutoSync hook
Tightens the freshly-landed merger auto-sync feature based on a structured code review.
Data-loss fixes in
syncWorktreeToHead:- Untracked-file restore now compares against
git ls-tree -r --name-only HEADto detect when the new tip introduced a tracked file at the same path; collisions are reported inuntrackedSkippedAsTrackedand the user's bytes stay in the stage dir instead of clobbering the merged content. - When
git apply --3wayfails because a patched file was deleted/renamed at the new tip (--diff-filter=Ureturns nothing because nothing got staged),conflictedFilesfalls back to parsingdiff --git a/<p> b/<p>headers out of the captured patch — so the conflict surfaces with the right file names instead of[]. git ls-files/diffcalls now pass-c core.quotePath=falseso paths with non-ASCII or special characters round-trip throughcopyFileSyncinstead of failing on backslash-escaped octal tokens.- The stash-and-ff path re-verifies
rev-parse HEAD === newShaimmediately before each destructivereset --hard HEAD; a concurrent merger advance now bails withskipped-head-not-at-new-sha(with the captured patch preserved on disk) instead of applying the patch against the wrong tree. - The stage dir is now tracked with a
preserveStageDirflag in atry/finally: it is rm'd on all clean paths and onskipped-head-not-at-new-shaexits, but preserved whenever the user's edits live only inpatchPath(pop-conflict, untracked-collides-with-tracked, reset failure, outer exception). - Patch is written to disk before the apply attempt, not only on failure, so a crash between snapshot and apply doesn't lose the user's edits.
Multi-worktree-same-branch fix:
- New
getRegisteredWorktreeBrancheshelper inworktree-pool.tsreturns ALL(branch, worktreePath)entries rather than collapsing duplicates into aMap<branch, path>. Multiple worktrees can legitimately share a branch when the user created secondary checkouts viagit worktree add --force -b; the merger now syncs every one of them instead of silently skipping all but the last.
Contract + surfacing fixes:
- JSDoc on
merge:auto-syncGitMutationType now documents the actually-emitted outcome strings (clean-sync,synced-with-edits-restored,synced-with-pop-conflict,skipped-*,failed,enumeration-failed,exception) and the actualstageenum, replacing the obsoletesmartPull-shaped strings. GET /api/tasks/merge-advance-eventsnow joinsmerge:auto-syncevents within a ±5min window of each advance and returns them in a newautoSync: AutoSyncOutcome[]field;useMergeAdvanceNoticeexposes the same shape so the banner can surface pop-conflicts (includingpatchPathpointing at the user's saved edits) instead of leaving them in a black hole.
Hygiene:
- Merger's setting read now uses
normalizeMergeAdvanceAutoSyncMode(settings.mergeAdvanceAutoSync)(the exported normalizer) instead of an inline equality check +as unknowncast that bypassed type-checking.
New backstop tests in
merger-auto-sync.slow.test.ts:- Untracked file colliding with a newly-tracked path is NOT overwritten and the merged content survives.
git apply --3wayfailure on a file deleted at the new tip populatesconflictedFilesfrom the patch header.
Route test asserts
autoSyncoutcomes are joined onto the matching advance event within the time window. - Untracked-file restore now compares against
-
bf4428c: fix(merger): require fast-forward ref advances and read integration tip from refs/heads/<branch>
Closes a class of "orphaned merge" bug where a subsequent merger could overwrite the integration branch tip with a sibling commit, leaving the previous squash reachable only from a feature branch.
Two coupled fixes:
-
advanceIntegrationBranchRefnow refuses non-fast-forward advances. The CAS check still guards against concurrent ref movement, but the newmerge-base --is-ancestorcheck additionally requires the new sha to descend from the expected current sha. Non-FF attempts returnreason: "non-fast-forward-advance"instead of silently orphaning the prior tip. -
runMergeresolves the integration-branch tip viagit rev-parse --verify refs/heads/<integrationBranch>instead ofgit rev-parse HEADinrootDir. In reuse-task-worktree mode,rootDir's HEAD can lag behind the shared ref after a sibling merger advanced it viaupdate-refwithout re-checking-out — using HEAD there caused the eventual squash commit to parent off an earlier sha and orphan the previously-merged tip.
Together these uphold the invariant: local
<integrationBranch>only advances via fast-forward, and the merger never builds a squash off a stale base sha. -
-
0c0839e: fix(merger): treat non-FF ref-advance as concurrent-advance so it triggers retry
When the merger's squash commit was built off a stale integration tip (integration moved between squash prep and
update-ref), the FF guard inadvanceIntegrationBranchRefcorrectly refused the swap with reasonnon-fast-forward-advance— but the caller inmerger.tsonly mappedconcurrent-advancetoIntegrationBranchConcurrentAdvanceError. The non-FF case fell through as a plainError, failing the task instead of routing to the FN-4500/FN-5083 rebind/retry path. Both reasons share a root cause (integration tip moved during the merge window), so they now share the retry path. Observed on FN-5576. -
ec1269f: feat(merger): auto-rehome FF-recoverable orphan commits during contamination recovery
Follow-up to the FF-only ref advance fix: contamination recovery now classifies a fourth bucket —
orphan-our-advance— and fast-forwards the integration branch onto pre-fix orphan commits when safe.When the executor's contamination handler sees a "unique" foreign commit, it now also asks:
- Does the commit's
Fusion-Task-Idtrailer point at adonetask? - Is the commit unreachable from
refs/heads/<integrationBranch>?
If both, the commit is an orphan from the pre-fix non-FF ref-advance bug. Recovery attempts to fast-forward the integration branch onto the orphan:
- FF possible (integration tip is an ancestor of the orphan): advance via
advanceIntegrationBranchRef, then drop the orphan from the task branch alongsidealready-upstreamcommits. Emitsmerger:orphan-rehome-ff. - Non-FF (orphan diverges from integration tip — would require cherry-pick): refuse to auto-rehome. The commit stays in
genuinelyUniquefor human adjudication, but the recovery log line now includes the exactgit cherry-pick <sha>command an operator can run to unstick it. Emitsmerger:orphan-rehome-refused.
The non-FF refusal is intentional: cherry-pick into the integration branch from inside automated recovery introduces conflict-resolution surface that's too high blast radius for a never-event recovery path.
- Does the commit's
-
Updated dependencies [408e20b]
-
Updated dependencies [ec6643e]
-
Updated dependencies [a201f56]
-
Updated dependencies [4c31e88]
-
Updated dependencies [51fc826]
- @fusion/core@0.33.0
- @fusion/pi-claude-cli@0.33.0
@fusion/plugin-sdk
Patch Changes
- Updated dependencies [408e20b]
- Updated dependencies [ec6643e]
- Updated dependencies [a201f56]
- Updated dependencies [4c31e88]
- Updated dependencies [51fc826]
- @fusion/core@0.33.0
@runfusion/fusion
Minor Changes
-
1b49cbc: Surface SQLite corruption in notifications, the dashboard health API, and a persistent dashboard banner.
-
2baaad7:
fn backupnow also snapshots the central database (~/.fusion/fusion-central.db)
alongside the per-project database. Scheduled and manual backups produce a paired
fusion-central-<timestamp>.dbfile in.fusion/backups/;fn backup --list,
--cleanup, and--restoreoperate on the pair. Restoring afusion-central-*
file restores only the central DB. A missing central DB is skipped silently and
does not fail the project backup. -
b12ff26: Saving an opencode or opencode-go API key from the dashboard now immediately refreshes the opencode-go model catalog (no restart required), reports how many models were registered, and surfaces actionable errors when the local
opencodeCLI is missing or returns no models.opencode-gois now always listed as an API-key target in Settings, even before models are registered. -
8a3afcf: feat(executor+engine-tests): preflight premise-stale exit and serialize reliability-interactions
-
Executor: teach the system prompt a Preflight escape hatch. When Step 0 reproduces the issue described in PROMPT.md and finds the work is already done (HEAD matches the desired state), the agent now marks Step 0 done, marks remaining steps
skipped, and callsfn_task_donewith aPREMISE STALE: …summary. Skipped steps already passevaluateTaskDoneRefusal, and the merger's empty-own-diff fast-path auto-finalizes the zero-diff branch — no new tool or refusal class is needed. This stops the executor from looping through plan/review/test/doc when PROMPT.md is out of sync with HEAD (the failure mode that exhausted FN-5521 across four worktrees). -
Engine vitest: split
packages/engine/vitest.config.tsinto two projects.engine-defaultkeeps the full-parallelism layout for the bulk of the suite;engine-reliabilityscopessrc/__tests__/reliability-interactions/**topoolOptions.threads.singleThread: trueso the contention-sensitive event-ordering assertions (e.g.merge-reuse-task-worktree's newest-first audit ordering check) no longer flake under workspace-concurrent merge-gate runs. Within-file order was already linear; this only removes inter-file parallelism for ~99 files that always shared a single git/SQLite contention surface.
-
-
e291d86: Attribute Fusion as a
Co-authored-bytrailer on managed commits instead of overriding the primary author. The user's configured git identity now remains the author/committer of every commit Fusion produces, and the configured commit author (defaultFusion <noreply@runfusion.ai>) is appended as aCo-authored-by:trailer that GitHub recognizes for shared attribution. ThecommitAuthorEnabledtoggle andcommitAuthorName/commitAuthorEmailsettings keep their existing keys; the dashboard settings UI relabels them from "Author" to "Co-author" to match the new behavior. -
22d59b5: Add mergeIntegrationWorktree setting (default reuse-task-worktree) to decouple auto-merge from project-root mutation. Legacy cwd-main behavior preserved as an opt-in escape hatch.
-
5f9f777: Increase default group chat context retention (
chatRoomRecentVerbatimMessages12 → 25,chatRoomCompactionFetchLimit80 → 200,chatRoomSummaryMaxChars1500 → 3000) and raise the room transcript cap from 8KB to 20KB. -
687237b: Per-project
fusion.dbnow persists the canonicalprojectIdin__meta.projectIdentity. If the central registry loses a project row, the next startup reattaches the same id from the stored identity instead of silently minting a new one (which would hide all project-scoped data keyed to the old id). Interactive flows prompt before destructive overwrites.
Patch Changes
-
f403926: Make
fn agent importextract.tar.gz/.tgzAgent Companies archives in-process instead of relying on a hosttarbinary. -
b7ddfc9: Layer FN-5152's near-duplicate intent guard onto the CLI
fn task create
direct-store path. Aligned thresholds (≥2 shared high-signal tokens AND
title-token Jaccard ≥ 0.30 within a 7-day window),--no-dedupbypass,
source.sourceMetadata.intentSignaturestamping, and fail-open semantics
match the dashboardPOST /api/tasksgate. Non-TTY runs refuse with exit
1; TTY runs prompt before creating. GitHub-import and AI-planning paths
intentionally skip the gate (FN-5060 contract). -
35b2971: Duplicating or restoring a task no longer fails when the source PROMPT.md
contains legacy/invalid File Scope tokens. Invalid tokens are dropped from
the rewritten PROMPT.md with a[file-scope-sanitize]log entry. Authoring
paths (createTask, updateTask) continue to reject invalid tokens strictly. -
8f2d5e7: Block explicit
DUPLICATE: FN-NNNNredirect tasks from consuming planning
cycles. The triage planning loop now short-circuits when the generated
PROMPT.md is a one-line duplicate marker (bypassing thefn_review_spec
APPROVE gate), a self-healing sweep resolves already-stuck duplicate-marker
tasks intriage/todo, and the dashboardPOST /api/tasksroute
surfaces a409 duplicate_candidateswithreason: "explicit-marker"when
the description is exactly a duplicate redirect. Layered on top of
FN-4829 / FN-4918 / FN-5152; fails open. -
f6f3867: Merger Layer 2.5: auto-widen
## File Scopefor files whose branch-side commits
are exclusively attributed to the current task before the FN-4956 scope
partition strips them. Emitsmerge:scope:auto-widenrun-audit events.
Fail-closed against foreign commits, cross-task scope claims, and ignored paths. -
8f5c1f9: New projects now default
directMergeCommitStrategyto"always-squash"for direct merges.
Existing projects keep their persisted setting value.
Per-project Settings UI controls and per-task**Direct Merge Commit Strategy:** ...PROMPT overrides are unchanged. -
b936ab9: Apply
heartbeatMultiplierto heartbeat unresponsive timeout calculations inHeartbeatMonitor.When heartbeat speed is slowed (for example
heartbeatMultiplier=2), unresponsive detection/recovery and orphaned-running reconciliation now use the correspondingly scaled timeout base, preventing false unresponsive recovery for expected slower cadence. Dashboard health classifier behavior is unchanged. -
6fdfb1a: Add a Room Coordination Notices prompt-injected advisory that fires when a
user posts an explicit "file a task" / "create a task" request into a chat
room with multiple agent members. Each agent is instructed to either post a
one-line claim before calling fn_task_create (claim branch) or to defer and
acknowledge a peer's prior claim/announcement (defer-suggested branch),
reducing the upstream duplicate pressure on the existing FN-4918 / FN-4829
/ FN-5152 / FN-5220 dedup backstop. Emits a structured
room:coordination:branch run-audit event per decision. Single-agent rooms
and non-task-filing messages are unaffected. -
025683c: Manual merge ("Merge now") no longer rejects in-review tasks that the scheduler has stamped with status: "queued". Auto-merge still honors all existing blockers.
-
4a99e3f: PR-mode merge cleanup (
cleanupMergedTaskArtifacts) now releases theWorktreePoollease for the merged task worktree before removing it, preventing stranded lease bookkeeping after pull-request merges (FN-5420 / FN-4954 follow-up). -
7a20b95: Add
session:runtime-resolvedrun-audit event (FN-5544) emitted fromcreateResolvedAgentSessionfor per-lane provider/runtime/model attribution. Additive surface; existing events unchanged. Replaces the diagnostic-log workaround introduced by FN-5206. -
8380024: fix(executor): exempt
PREMISE STALE:summaries fromsummary-claims-incompleterefusalsThe preflight escape hatch added in the prior commit instructs the agent to call
fn_task_donewith a summary that beginsPREMISE STALE:when reproduction shows HEAD already matches the desired state. Natural premise-stale wording such as "PREMISE STALE: the task has no remaining work — implementation is already done on HEAD" trippedevaluateTaskDoneRefusal's scoped-incomplete regex (/\b(incomplete|not implemented|not done|not finished)\b/i) when the 40-char window containedthe task/this task/first-person pronouns, refusingfn_task_doneand deadlocking the executor — the exact failure the escape hatch was meant to prevent.Add a sentinel bypass: when
summarystarts (case-insensitive) withPREMISE STALE:, skip the dissent-pattern and scoped-incomplete summary checks. Thepending-code-review-reviseandbulk-step-completion-without-reviewguards still run unchanged, so the bypass cannot dodge real review obligations or unfinished work — only the summary-phrasing checks are relaxed. -
1be0702: Mobile (Android Chrome edge-to-edge): the top brand header no longer sits underneath the system status bar — its mobile
padding-topis now additive (var(--space-md) + env(safe-area-inset-top)) instead ofmax(...), so the row keeps its normal breathing room below the system inset. The executor status footer also no longer bleeds into the bottom-nav padding band: itsbottomoffset now uses the samemax(env(safe-area-inset-bottom), 12px)floor thatMobileNavBaralready applies, so the two surfaces meet flush even when Chrome under-reports the bottom inset. BareTypeError: Failed to fetchtoasts (raised by in-flight requests aborted on tab background/resume) are now swallowed at the toast layer; toasts with additional context still surface. -
ba9d632: Mobile bottom nav no longer occasionally hides behind Android Chrome's gesture pill. Chrome under
viewport-fit=coverintermittently reportsenv(safe-area-inset-bottom)as0while the address bar is visible or during URL-bar collapse; the nav now floors that inset to 12px so it always clears the gesture area. Devices that report a larger inset are unaffected. -
fbf7e2c: Dashboard no longer renders into a small upper-left rectangle on Android Chrome in multi-window/freeform/split-screen mode. The page now re-asserts its viewport meta with the live
innerWidthon every resize and orientation change, defeating Chrome's habit of cachingdevice-widthat the original screen size (which left the layout viewport wider than the actual window, so normal-flow elements clipped while position-fixed elements pinned to the full window). Also dropsmaximum-scale=1.0, user-scalable=nofrom the viewport meta, and broadens the existing board scroll-snap stabilization from phones to all touch-primary devices so Android tablets get the same first-cards-loaded reflow that iOS Safari mobile already had. -
5548dc5: Dashboard git pull now autostashes dirty local changes (including untracked files) before pulling and reapplies them on success. If reapplying conflicts, the stash is preserved and the operation reports
stashConflictwith the stash label so the user can resolve later from the Stashes view. Previously a dirty working tree caused the pull to fail outright with no recovery path. -
f58fb89: fix(engine-tests): eliminate full-suite temp-dir leak and harden subprocess guard against concurrent-workspace contention.
-
Two engine merger tests (
merger-no-op-fix-finalize.test.ts,merger-verification-fix-already-on-main.test.ts) createdmkdtempSyncworkspaces directly undertmpdir()with the trackedfusion-test-prefix. Underpnpm -r --workspace-concurrency=2load, transient cleanup races left orphans flagged bycheck-test-isolation. Route both throughFUSION_TEST_WORKER_ROOTlike sibling merger tests so the dirs nest inside the already-tracked worker root and never appear as top-level leaks. -
Bump the engine vitest subprocess guard from 60 s to 120 s and the per-test timeout to 30 s. Under concurrent workspace runs, plain git commands (
git branch -d,git worktree remove --force) queued behind system contention were timing out and failing reliability-interactions tests. The guard fires only on hangs, so healthy tests pay nothing for the higher ceiling.
-
-
97e6a0c: Skip the dependency-cycle preflight in
TaskStore.assertNoDependencyCyclewhen the new task or update has no dependencies. The FN-5256 cycle-check rollout (e12adeb) added an unconditionallistTasks()call on every write to build the dependency lookup, but an empty dependency list can never form a cycle so the query was wasted work. It also broke fail-open semantics in the same-agent duplicate intake path: tests that stubbedlistTasksto throw saw the cycle check consume the rejection and propagate "boom" out ofcreateTaskbefore the duplicate-intake try/catch could swallow it. -
cf0101b: fix(FN-5256): harden three independent code paths that were losing live task worktrees mid-execution and producing
wrong_toplevelerrors.-
Executor stale-self-owned classifier (
reconcileSelfOwnedActiveSessionForRemoval) now requires two additional signals before dropping a same-task registry entry: a process-active probe (executingTaskLock.has) and a minimum-idle window (default 5s) since the entry was registered. This closes the pause/resume race where the new executor cycle hadn't repopulatedactiveWorktreesyet and the old session's registry entry was reaped under a still-live shell. The post-throw reconcile inremoveOwnWorktreeWithReconcileand theremoveWorktreedefensive reconcile inworktree-backend.tsroute through the same hardened path. -
Pause-before-park now synchronously awaits agent/step/workflow session disposal via a new
awaitAbortInFlightTaskWorkmethod, so by the timeparkTaskAfterWorkflowStepPausecallsmoveTask("todo")the spawned shells are already reaped and any fast re-dispatch sees a clean slate. The user-initiated pause handler ontask:updatedwas collapsed onto the same await path for the same reason. -
Self-healing
reconcileTaskWorktreeMetadatanow normalizes both sides viarealpathSync(with ENOENT fallback) before comparing the task worktree against the registered set, fixing the macOS/private/var/...false-stale flag. It additionally refuses to clearworktree/branchmetadata for in-progress or in-review tasks — those go throughtask:auto-recover-worktree-metadata-skipped-activeaudit events and leave executor-level recovery in charge. -
The
task:moved-away andtask:deletedlisteners now track an awaited disposal promise per task. A re-dispatch (task:moved→ in-progress) awaits any in-flight disposal for the same task before callingexecute(), so a fast bounce (in-progress → todo → in-progress) can no longer race the conflict-cleanup path against a still-live shell.awaitAbortInFlightTaskWorkclaims each session surface synchronously before awaiting its abort, so concurrent disposal calls dedupe naturally and the legacy fire-and-forgetabortInFlightTaskWorkhas been removed.
-
-
c824810: Fix reuse-task-worktree merge mode (FN-5279) never applying the squash commit to the project root's local integration branch. The merger detaches HEAD in the task worktree and lands the squash on the detached HEAD; previously nothing advanced the project root's local
main, so changes never appeared on the user'smain(and any subsequentpushAfterMergewould push the stale ref or fail outright becauseparsePushRemoteTargetcan't resolve a branch from a detached HEAD). A new step 5c now applies the squash to the project root's integration branch viagit merge --ff-only, falling back to a regular merge with AI conflict resolution ifmainhas diverged. Push-after-merge (when enabled) now runs from the project root where the integration branch was just advanced. -
1983dac: Engine reliability: prevent the FN-5345 in-review wedge class.
- Fusion task worktrees now install a
prepare-commit-msgempty-commit guard that refusesgit commit --allow-emptyand other zero-staged-diff commits, while still allowing legitimate amend / merge / squash / cherry-pick / revert / rebase paths. Amend detection scansps -o args=(with/proc/$PPID/cmdlinefallback for Alpine/busybox) tokenized, stopping at the first message-supplying flag (-m,-F,--message,--file) so a commit message containing the substring--amendcannot bypass the guard. - Merger gains an early empty-own-diff fast-path in
reuse-task-worktreeintegration mode: branches with own commits but zero net tree change vs merge-base now auto-finalize as no-op BEFORE any reuse-handoff acquisition runs, preventingregistered-branch-mismatch+merge-deadlock-detected: verified content not on mainwedges. The fast-path best-effort cleans up the stranded worktree andfusion/<id>branch so empty-own-diff residuals do not accumulate. classifyOwnedLandedEvidencealso detects the empty-own-diff case and returnsproven-no-opso downstream self-healing and post-handoff finalize paths benefit too.- Merger's reuse-fallback path now consults
git worktree list --porcelainbefore creating a new worktree, reusing extant usable registrations offusion/<id>and pruning stale ones, eliminating FN-5083-class branch-registration double-registration. The direct-reuse shortcut is guarded by FN-4811 (refuses paths owned by a different task inactiveSessionRegistry) and FN-4954 (skipped whenrecycleWorktrees=truewith a pool attached, soWorktreePool.acquirelease bookkeeping stays consistent). Two new audit subtypes (merge:reuse-fallback-pruned-stale-registration,merge:reuse-fallback-reused-existing-registration) replace the prior overloading ofmerge:reuse-fallback-new-worktreefor these cases.
- Fusion task worktrees now install a
-
57dbff4: Fix
fn backupcorrupting the live database. The paired-central-backup feature opened a secondnode:sqliteconnection against the livefusion.dband ranPRAGMA wal_checkpoint(TRUNCATE)before the file copy. Anode:sqliteSIGSEGV mid-checkpoint (a known recurring crash mode for this codebase) could leave the main DB file extended-but-zeroed. Backups now copy the main DB plus any sibling-wal/-shmfiles via plaincp; SQLite replays the WAL on first open, so uncheckpointed pages are preserved without us ever opening a second connection against the live database. -
1bffa22: fix(FN-5483): allow merger-driven commits past the identity-guard pre-commit hook (detached HEAD false-positive).
The reuse-task-worktree merge path intentionally detaches HEAD at the integration target before running squash and verification-fix ceremonies. The identity-guard hook (
buildIdentityGuardHook) refused every such commit becauseHEAD_BRANCH=detachednever matches the owning task branch, surfacing asmerge-deadlock-detected: requires manual intervention — verified content not on mainon FN-5441 and FN-5446.The hook now honors a
FUSION_MERGER_BYPASS_IDENTITY_GUARD=1env-var bypass (gated to the exact value"1"), set only on merger-drivengit commitcalls. The marker is placed after theTASK_FILEcheck so non-fusion worktrees stay no-op, and beforeEXPECTED_BRANCHso detached HEAD never reaches the refusal printf. Agent commits never set this env, so the guard still catches executor/reviewer misuse.buildCommitMsgTrailerHookandbuildPrepareCommitMsgEmptyGuardHookare unchanged and continue to run on every merger commit, preserving FN-5089 trailer attribution and FN-5345/FN-5377 empty-commit refusal. -
2d425b1: fix: clear scheduler-side
status='queued',blockedBy, andoverlapBlockedBywhen a task transitions intoin-reviewso the merge gate is no longer permanently blocked by stale todo-dispatch markers.Repro: a task that picked up
status='queued'while waiting intodo(e.g. file-scope overlap with a higher-priority queued peer) and then completed and was handed off toin-review— directly viahandoffToReviewor indirectly via stranded-completed-todo recovery — would carry the queued flag into review. Every subsequent merge attempt failed withCannot merge <id>: task is marked 'queued', and the in-review stall surface kept re-firing[no-worktree-no-merge-confirmed]without progress. Ghost-review → todo → scheduler re-queue → stranded → in-review formed a steady-state loop.Fix:
TaskStore.moveTaskInternalnow treatsqueued/blockedBy/overlapBlockedByas todo-only dispatch state and scrubs them on every transition intoin-review. Failed/awaiting-* statuses are unaffected. -
d947197: Engine reliability: auto-recover merge handoff from HEAD drift when the branch ref is authoritative.
acquireReuseHandoffpreviously refused outright withhead-branch-mismatch / unexpected-branchwhenever the worktree's HEAD pointed at anything other thanfusion/<id>(detached, recycled tomain, on a sibling branch). The existing case-mismatch autocorrect did not cover these states, leaving FN-5339-class tasks wedged in review even though their branch ref still held a clean, task-attributed lineage.- New
isBranchAuthoritativeForTaskhelper (inbranch-conflicts.ts) confirms the expected branch ref exists, its tip carries theFusion-Task-Id: <id>trailer, and thebase..branchrange has no foreign FN-attributed commits. - When that probe passes, the handoff now performs a plain
git checkout <branch>(not-B, which would clobber the ref) inside the already-asserted-clean worktree, re-reads HEAD, and emits abranch:auto-reattach-authoritativeaudit. Refusal still fires unchanged when the branch ref itself is missing, missing a trailer, or contaminated — so the FN-5363 strict-lease and foreign-commit protections remain authoritative.
-
5c15031: Make the dashboard's "Refresh health" button actually re-run the SQLite integrity check. The background scheduler in
Database.scheduleBackgroundIntegrityCheckonly ran the check once at engine boot, so oncecorruptionDetectedflipped totrueit was sticky for the life of the process — the refresh action just re-read the same cached flag and the corruption banner could not be cleared even after the user repaired the DB (e.g. viaREINDEX).POST /api/health/refreshnow calls a newTaskStore.refreshDatabaseHealthwhich synchronously re-runs the integrity check and updates the cached state before responding. -
24686ca: Stop losing uncommitted dev edits during task merges. Two fixes to the merger:
-
The pre-merge autostash in
stashUnrelatedRootDirChangesno longer silently proceeds when stash creation fails over a dirty working tree. It now throwsAutostashCreationFailedError, which the merger catches and surfaces to the task feed before any destructivegit reset --hard/git clean -fdruns — your edits stay in the working tree. -
acquireReuseHandoffno longer refuses the handoff on a dirty reused task worktree (the FN-5138 "Merge handoff refused (working-tree-dirty)" failure). It now autostashes the dirty content (git add -A→git stash create→git stash store -m fusion-reuse-handoff-autostash:<taskId>:<ts>), emits amerge:reuse-handoff-autostashaudit event with the stash SHA and a recover command, and lets the merge proceed.
The new failure mode
dirty-worktree-autostash-failedis reserved for the (rare) case where stash creation itself fails — so the operator can distinguish "we tried and failed" from the old "we refused to try." -
-
a7ad30f: Mobile (iOS): the bottom navigation bar no longer slides up when the on-screen keyboard appears. The viewport-compensation offset that pulls fixed elements back into the visible area (intended for Android ICB quirks / pinch-zoom) was also being driven by the keyboard's shrunken
visualViewport.heighton iOS, pushing the nav bar above the keyboard. The nav now ignores that offset whilekeyboardOpenis true and stays pinned at the page bottom — the keyboard simply covers it. -
a2a5db8: Engine reliability: better diagnostics + clean-baseline reset on phantom finalize.
commitOrAmendMergeWithFixespreviously swallowed all unexpected errors asreason: "unknown-phantom"and the two callers re-threw averification fix finalize failed (unknown phantom)error with no surface area beyond the SHAs. FN-5422-class tasks wedged in review with no actionable signal in the failure message.- The catch now captures the original error message and runs an
isBranchAuthoritativeForTaskprobe (existing branch ref carries this task'sFusion-Task-Idtrailer + foreign-contamination check against base). When the branch ref is authoritative — meaning the AI's work is safely stored onfusion/<id>and only the in-merge attempt's integration worktree drifted — the catch resets rootDir topreAttemptHeadShaand returnsreason: "branch-ref-ahead-reset". The next merge attempt then starts from a known-good baseline instead of inheriting half-built squash state. - Verification-fix and build-verification-fix callers now include the original error and the branch-authority probe outcome in the thrown error, so operators see the actual failure cause (e.g. diff-volume regression, file-scope violation, transient git error) rather than
unknown phantom.
-
5848606: Engine reliability: post-session branch-attribution audit catches contamination within minutes instead of days.
- The executor already checks branch contamination at acquisition time (
assertCleanBranchAtBase) and at reclaim time. The gap was the active session window itself: commits added tofusion/<id>between acquisition and merge handoff went undetected until merge-time refusal, which is how FN-5233 ended up with two untraileredfeat(FN-5353):commits sitting onfusion/fn-5233. - New
reportBranchAttribution(repoDir, branch, baseSha, taskId)walksbase..branchand classifies every commit into four buckets:ownTrailed(subject tag +Fusion-Task-Idtrailer — healthy),ownUntrailed(subject tag but missing trailer — signals the commit-msg hook didn't fire),foreign(different FN-id via subject or trailer — contamination), andunattributed(neither — typically a hand-merge or plumbing commit). - Wired into the executor's post-session path (right after
captureModifiedFiles): if any anomaly bucket is non-empty, the executor logs a structuredbranch:attribution-anomalyaudit event and a task log entry. Failures in the audit itself are caught and warn-only — the audit must never destabilize a completing session. Newbranch:attribution-anomalyandbranch:auto-reattach-authoritativegit-mutation types accept the structured metadata. - Five new vitest cases cover the four anomaly buckets and the empty-range no-op.
- The executor already checks branch contamination at acquisition time (
-
fbf7e2c: Dashboard board no longer squeezes all 6 columns into the visible width on tablet-sized viewports (769–1024px). Columns now keep a 260px minimum and the board scrolls horizontally, matching desktop behavior. Previously the tablet rule used
minmax(0, 1fr)withoverflow-x: hidden, collapsing columns to ~130–170px wide on Android tablets and forcing task card titles to stack one word per line. -
d90da81: Coerce
task.descriptionto an empty string when persisting inTaskStore.getTaskPersistValues. Thedescriptioncolumn isNOT NULL, so a task with a missing description previously failed insertion with a constraint error. Defaulting to""mirrors the existing?? null/?? 0treatment of other optional fields. -
2d661df: fix(engine): prevent worktree-pool branch creation from inheriting a previous occupant's tip, and auto-reanchor branches that already inherited foreign commits.
-
WorktreePool.prepareForTasknow rejects empty/HEADbase values and verifies post-detach HEAD matches the resolved base SHA before creating the branch. This closes the FN-5432 / FN-5255 contamination pattern where recycled worktrees branched from a stale HEAD (reflog:branch: Created from HEAD) and pinned the new task's tip to the previous task's commit. -
SelfHealingManagernow attempts a foreign-only contamination reanchor before pausing a task withbranch-conflict-unrecoverable. When the task's branch carries only foreign commits (no own work), the branch is reset back to its base via the existingrecoverForeignOnlyContaminationpath instead of stranding the task for human adjudication.
-
-
93b11c6: Atomic in-review handoff: introduce
TaskStore.handoffToReviewthat performs the column move andmergeQueueenqueue inside a single transaction, and migrate every executor + self-healing site that promotes a completed task intoin-reviewto use it. DirectmoveTask(taskId, "in-review")writes now emit atask:handoff-invariant-violationrun-audit event for forensics. Pairs with FN-5242 (queue schema) and FN-5243 (merger lease consumption). -
9efcf93: Fix Fusion worktree pre-commit identity-guard hook to accept canonical lowercase
fusion/<id>branches when the on-diskfusion-task-idmetadata stores an uppercase id, eliminating spurious "refusing commit" rejections that previously required--no-verify. -
e908cbc: Downgrade merge-time file-scope invariant violations from task-failing errors to warning-only logs so merges can continue while still recording audit telemetry.
-
9011c21: Fix the Worktrunk integration to probe the canonical
wtbinary, point release metadata at the realmax-sixty/worktrunkupstream, and fail closed when install metadata is still unverified. This preserves default-off behavior whenworktrunk.enabled=falseand hardens enabled setups that rely on an explicitworktrunk.binaryPath. -
b06cf64: Add deterministic external-integration safeguards by introducing a shared integration manifest validator, a triage-time spec evidence gate, and registry contract tests that prevent hallucinated third-party repo/binary/checksum metadata from landing.
-
232a9fe: Fix main chat composer (direct chat and rooms) so it visually grows on
multi-paragraph paste up to the 640px cap, matching QuickChat behavior.
The 640px cap from FN-5146 was already in place but an ancestor layout
constraint was clipping the rendered height. -
fd202e9: Remove the
fn task branch-recoverysurface and retire orphan-branch auto-rescue wiring.Fusion now treats orphan
fusion/*branches as operator-managed git state: branch conflicts still fail loudly with diagnostics, and operators resolve/reclaim/discard branches manually with standard git tooling before retrying. -
a8715ed: Self-healing: gate every backward-moving recovery stage on triple proof (dead session + unusable worktree + no recent executor activity). Stages that cannot satisfy the predicate are downgraded to observation-only, emitting task:-no-action audit events instead of lifecycle moves. Authoritative per-stage disposition lives in docs/self-healing-backward-move-audit.md. Companion to FN-5337.
-
1a5aff9: Self-healing: remove speculative auto-requeue from
recoverOrphanedExecutions. The sweep no longer calls lease-manager recovery/reconcile, no longer writesstatus: "stuck-killed"or clearsworktree/branch, no longer writes theAuto-recovered orphaned executor tasklog entry, and no longer moves tasks back totodo.recoverOrphanedExecutionsis now observation-only and emitstask:orphan-detected-no-actionrun-audit events plus[orphan-detected]diagnostics when stale in-progress candidates are detected. Proof-based lifecycle recovery remains owned byrecoverInProgressLimbo,RestartRecoveryCoordinator, and explicit executor/merger failure paths (fixes FN-5279 false-positive class). -
216c32b: Manual task retry now resets the full persisted retry-budget counter set (and
nextRecoveryAt) across CLI, pi extension, and dashboard retry surfaces, so retry badges/details no longer stay inflated after a user-triggered fresh attempt. -
8df21a6: Fix in-review merge stall under the new mergeIntegrationWorktree=reuse-task-worktree default: strict targetTaskId leasing prevents cross-task lease bleed, and missing task.worktree always triggers the reacquire fallback instead of misrouting handoff gates against the project root.
-
dbccdb1: Harden cloudflared auto-install (dashboard remote-access opt-in flow): add a pinned release manifest and SHA-256 verification, mirroring the FN-5320 Worktrunk pattern. Auto-download fails closed in
upstream-pending-verificationmode; package-manager paths (brew,winget) are unchanged. Replaces the previous unverifiedreleases/latest/downloaddirect-curl install path. -
a04ba3c: Coerce null or undefined
task.descriptionvalues to an empty string when persisting TaskStore rows. -
c3890a9: Improve soft-deleted blocker recovery so blocked tasks become schedulable without manual intervention.
SelfHealingManager.clearStaleBlockedBynow emits an explicitsoft-deleted at ...reason when a stale blocker is a soft-deleted row, and the scheduler now reconciles downstreamblockedBy/dependency state immediately ontask:deletedevents to reblock on remaining live deps or unblock tasks in the same tick. -
31c71a3: Surface exhausted soft-deleted in-review blockers through opt-in visibility paths so operators can diagnose stalled dependent chains without direct DB access.
fn_task_shownow falls back to include soft-deleted task reads and prints a[SOFT-DELETED at ...]marker, whilefn_task_listadds anincludeDeletedflag for listing hidden blockers.Also adds dashboard/API visibility with
GET /api/tasks/exhausted-in-review(including?includeDeleted=true),GET /api/tasks/:id?includeDeleted=true, and a ReliabilityView panel for exhausted hidden blockers plus blocked dependents. -
3ccb132: Fix Windows worktree creation and cleanup by replacing POSIX shell
mkdir -p/rm -rfcalls in worktree setup paths with cross-platform Node.js filesystem APIs.
runfusion.ai
Patch Changes
- Updated dependencies [f403926]
- Updated dependencies [b7ddfc9]
- Updated dependencies [35b2971]
- Updated dependencies [8f2d5e7]
- Updated dependencies [f6f3867]
- Updated dependencies [8f5c1f9]
- Updated dependencies [1b49cbc]
- Updated dependencies [2baaad7]
- Updated dependencies [b936ab9]
- Updated dependencies [b12ff26]
- Updated dependencies [6fdfb1a]
- Updated dependencies [025683c]
- Updated dependencies [4a99e3f]
- Updated dependencies [7a20b95]
- Updated dependencies [8a3afcf]
- Updated dependencies [8380024]
- Updated dependencies [e291d86]
- Updated dependencies [1be0702]
- Updated dependencies [ba9d632]
- Updated dependencies [fbf7e2c]
- Updated dependencies [5548dc5]
- Updated dependencies [f58fb89]
- Updated dependencies [97e6a0c]
- Updated dependencies [cf0101b]
- Updated dependencies [c824810]
- Updated dependencies [1983dac]
- Updated dependencies [57dbff4]
- Updated dependencies [1bffa22]
- Updated dependencies [2d425b1]
- Updated dependencies [d947197]
- Updated dependencies [5c15031]
- Updated dependencies [24686ca]
- Updated dependencies [a7ad30f]
- Updated dependencies [a2a5db8]
- Updated dependencies [5848606]
- Updated dependencies [fbf7e2c]
- Updated dependencies [d90da81]
- Updated dependencies [2d661df]
- Updated dependencies [93b11c6]
- Updated dependencies [9efcf93]
- Updated dependencies [22d59b5]
- Updated dependencies [e908cbc]
- Updated dependencies [9011c21]
- Updated dependencies [b06cf64]
- Updated dependencies [232a9fe]
- Updated dependencies [fd202e9]
- Updated dependencies [a8715ed]
- Updated dependencies [1a5aff9]
- Updated dependencies [216c32b]
- Updated dependencies [8df21a6]
- Updated dependencies [5f9f777]
- Updated dependencies [dbccdb1]
- Updated dependencies [687237b]
- Updated dependencies [a04ba3c]
- Updated dependencies [c3890a9]
- Updated dependencies [31c71a3]
- Updated dependencies [3ccb132]
- @runfusion/fusion@0.33.0