Even with FN-5627's merger TOCTOU fix + transient-failure self-healing
sweep + safety-fallback auto-prerebase landed, the merger can still hit
transient failure classes (lease handoff races, brief same-SHA non-FF
advances) for tasks whose branches are particularly out-of-sync. The
self-healing sweep auto-recovers them within bounded budget \u2014 but each
individual failure cycle was firing a ntfy alarm before the recovery
cleared the failed state, producing user-facing alarm spam for tasks
that were never actually stuck.
Two layers of fix:
1. NotificationService.handleTaskUpdated now classifies task.error via
the new shared classifyTransientMergeError helper before scheduling
the deferred failure notification. Transient classes
(lease-handoff-target-not-queued, spurious-concurrent-advance-same-sha)
get logged as suppressed and never schedule a ntfy timer.
2. Defense-in-depth: fireDeferredFailureNotification re-classifies the
error at dispatch time, so a failure scheduled before the suppression
landed on a newer cycle still suppresses if the error matches a
transient class.
The classifier itself moved from self-healing.ts to a new logger-free
transient-merge-error-classifier.ts module so consumers in
NotificationService don't pull createLogger through the import chain and
break test mocks of ../logger.js (per project-memory rule about new
modules using createLogger). self-healing.ts re-exports the symbol for
backward compatibility.
Log prefix for the recovery actions also changed from
'[FN-5627] Auto-recovering...' to 'Auto-recovered:' so that
NotificationService.maybeSuppressTransientFailedNotification's existing
/^Auto-recovered:/ log-prefix check cancels any already-scheduled failure
notification when the sweep runs mid-grace-window.
Tests (3 new):
- transient lease-handoff-target-not-queued failure NOT notified
- transient spurious-concurrent-advance-same-sha failure NOT notified
- genuine different-SHAs concurrent-advance still notifies (control)
Engine suite: 6166 tests pass.
Fusion-Task-Id: FN-5627
The FN-5627 default-threshold fix changed the prerebase threshold default
from 0 (never fire on commit-count) to 1 (fire on any divergence). But
that only affected projects WITHOUT an explicit threshold. Projects with
user-set values like 'prerebaseDivergenceThreshold: 50' continued to
skip prerebase for small divergences (e.g., 4 commits behind), so the
squash built against stale base and update-ref refused non-FF \u2014
producing the same-SHA spurious-concurrent-advance signature that
stranded FN-5626/FN-5628/FN-5633.
Root distinction missed earlier:
- prerebaseDivergenceThreshold is for USER-VISIBLE SEVERITY REPORTING
(this branch is N commits behind, warn me).
- Engine correctness requires a SAFETY INVARIANT (any branch behind main
MUST be rebased before squash or update-ref will fail).
These are independent concerns. The safety invariant must not be gated
on the user's threshold.
decideAutoPrerebase() now returns fire=true with reason
'safety-fallback-any-divergence' whenever commitsBehind > 0, after the
hot-file and threshold checks. The threshold path still wins the reason
label when its condition trips, so user-visible severity reporting is
unchanged for non-pathological cases.
Full opt-out remains prerebaseAutoEnabled=false (skips the safety
fallback; user accepts behind-branch merges will fail).
prerebaseDivergenceThreshold=0 is no longer a complete opt-out from the
commit-count gate \u2014 it only suppresses the threshold-based reason label.
Tests (4 updated/new):
- safety-fallback-any-divergence reason added to AutoPrerebaseDecision
- 4 commits behind with threshold=50 fires via safety fallback
- prerebaseAutoEnabled=false respects full opt-out
- threshold trip still wins reason label
- commitsBehind=0 returns no-divergence (unchanged)
Engine suite: 6163 tests pass.
In-flight: FN-5626, FN-5628, FN-5633 manually SQL-reset to mergeRetries=0,
status=null, error=null, transientRecoveryCount=0 so the next merger tick
(after engine restart picks up this code) auto-prerebases via safety
fallback and lands the work. Future occurrences self-heal automatically.
Fusion-Task-Id: FN-5627
Adds collision retry logic to the `fn update` CLI command to handle binary path conflicts, including test coverage and a getting-started guide update.
Fusion-Task-Id: FN-5632
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5632
decideAutoPrerebase() previously defaulted prerebaseDivergenceThreshold
to 0, which meant the threshold path NEVER fired unless the user
explicitly set a positive value. Only hot-file matches could trigger
prerebase.
The result: tasks whose branch was started against an older main tip
(because other tasks landed concurrently) skipped prerebase, built their
squash commit against the stale base, and then failed at git update-ref
because the squash didn't descend from current main. The merger correctly
detected this as non-fast-forward and threw
IntegrationBranchConcurrentAdvanceError, but with both 'expected' and
'observed' SHAs set to current main tip \u2014 because observedCurrentSha was
captured from the pre-update rev-parse, not post-failure. This produced
the misleading 'expected X, observed X' same-SHA error signature that
stranded FN-5632 stuck at mergeRetries=3 after the FN-5627 merger fix
and engine restart.
New default: prerebaseDivergenceThreshold = 1. Any branch behind by at
least 1 commit auto-rebases before squash. Users who want the legacy
never-fire behavior can explicitly set prerebaseDivergenceThreshold = 0.
Threshold comparison also changed from > to >= so an explicit threshold
of N rebases at N+ commits behind instead of N+1+.
The self-healing classifier comment for spurious-concurrent-advance-same-sha
is updated to note the signature can come from either pre-FN-5627
misclassification OR the legitimate post-FN-5627 non-FF path; the
auto-recovery sweep is unchanged because both cases self-heal cleanly
once prerebase fires on the retry.
Tests (3 new):
- Default threshold (undefined) fires at 1 commit behind
- Explicit threshold = 0 stays as opt-out (never fire on commit-count)
- Default threshold doesn't fire when branch is up-to-date
Engine suite: 6160 tests pass.
In-flight: FN-5632 manually SQL-reset to mergeRetries=0 / status=null
once more so the next merger tick (after engine restart picks up this
code) auto-prerebases and lands the work. Future occurrences self-recover.
Fusion-Task-Id: FN-5627
After the FN-5627 merger fix (b2d547eae, 230f6f45b) landed, two in-review
tasks (FN-5628, FN-5632) remained stuck at mergeRetries=3 with
status=failed because the merger correctly identified transient failure
classes but had no auto-recovery path \u2014 the AUTO_MERGE_COOLDOWN_MS reset
takes hours and gives up too easily.
Failure classes covered:
- lease-handoff-failed: target-not-queued (FN-5353/FN-5363 race where the
merge queue lease was cleared between enqueue and handoff acquisition).
- Legacy same-SHA spurious 'Integration branch X advanced concurrently
(expected SHA, observed SHA)' errors from pre-FN-5627 code paths.
Implementation:
- New MergeDetails.transientRecoveryCount field tracks per-task recovery
attempts, bounded by MAX_TRANSIENT_MERGE_RECOVERIES = 2.
- New classifyTransientMergeError() string matcher in self-healing.ts
identifies recoverable classes by error pattern. Returns null for
genuine merge failures (verification, conflicts, real concurrent
advances with different SHAs).
- SelfHealingManager.recoverTransientMergeFailures() sweep finds
matching in-review tasks, resets mergeRetries=0, clears status/error,
increments recovery count, re-enqueues via requeueForAutoMerge.
- Wired into BOTH startup recovery and periodic Batch 2 maintenance loop.
- Emits merger:transient-failure-auto-recovered (recovered) and
merger:transient-failure-budget-exhausted (terminal) audit events.
No-op when autoMerge=false, requeueForAutoMerge not wired, or pause
active. Repeat-suppression on budget-exhausted emit via error marker
[transient-recovery-budget-exhausted] to prevent log spam.
Tests (6 new):
- target-not-queued recovery path
- spurious-concurrent-advance-same-sha recovery path (legacy)
- genuine concurrent-advance (different SHAs) NOT recovered
- non-transient failures NOT recovered (verification, conflicts)
- budget exhaustion emits marker once, no further requeue
- autoMerge=false no-op
Engine suite: 6157 tests pass (6 new).
In-flight: FN-5628 and FN-5632 were manually reset via SQL so the
already-running engine (which has the FN-5627 merger fix) can re-attempt
their merges before this self-healing path lands and reloads. Future
occurrences self-recover.
Fusion-Task-Id: FN-5627
Adds bundled plugin resolution for `@runfusion/fusion`, including a dist candidate check in `bundled-plugin-install.ts` and regression tests covering the install path, with a patch changeset prepared.
Fusion-Task-Id: FN-5630
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5630
The merger persisted `mergeConfirmed: true` + `commitSha` to the task row
as soon as the local squash commit was built, BEFORE running
`git update-ref refs/heads/<integration>` to actually advance the
integration branch. If the ref-advance then failed for any reason (lock
contention, hook rejection, packed-refs race, or a misclassified non-CAS
error via the merger-ref-update-advance.ts string heuristic), the task row
was poisoned: the auto-merge scheduler's mergeConfirmed fast-path would
silently promote the never-landed work to 'done' on the next tick,
including emitting task:merged and closing the linked GitHub tracking
issue. The 'expected SHA == observed SHA' log signature on FN-5625 was a
red herring — the ref-advance had failed for non-race reasons but the
string heuristic in merger-ref-update-advance.ts classified it as
'concurrent-advance', and the downstream IntegrationBranchConcurrentAdvanceError
routed through the unsafe 'merge already confirmed' recovery path.
This silently dropped real work on at least 9 tasks across 2026-05-27/28
(FN-5596, FN-5597, FN-5599, FN-5612, FN-5613, FN-5614, FN-5616, FN-5623,
FN-5625) and likely affected older now-archived tasks for which evidence
has been pruned.
Three-layer fix:
1. merger.ts (~9752): in reuseTaskWorktreeMerge mode, persist
`mergeConfirmed: false` initially. After advanceIntegrationBranchRef
returns advanced=true, do a follow-up updateTask to flip the flag.
Other merge paths (legacy in-place, verified no-op fast-paths,
owned-commit recovery) advance the ref BEFORE the mergeDetails write
and remain unchanged.
2. project-engine.ts (~1378): defense-in-depth reachability gate on the
auto-merge 'merge already confirmed' fast-path. Before moveTask to
'done', verify `git merge-base --is-ancestor <commitSha>
refs/heads/<integration>` succeeds. On failure, clear mergeConfirmed,
set status='failed' with descriptive error, leave task in 'in-review',
and emit `merger:fast-path-blocked-foreign-commit` run-audit event.
Legitimate no-op merges (no commitSha) bypass the gate; ancient tasks
missing mergeTargetBranch also bypass to avoid false-positive parks.
3. merger-ref-update-advance.ts (~189): replace fragile string heuristic
('is at' / 'expected' / 'cannot lock ref' in stderr) with structured
detection. After update-ref fails, re-read the ref: if observed ==
expected, classify as `ref-update-refused` (no race occurred); only
classify as `concurrent-advance` when ref actually moved. Eliminates
the misleading 'expected X observed X' same-SHA pair.
Tests: 3 new regression tests covering all three layers. Full engine
suite: 6150 tests pass.
Fixes:
- FN-5625 (autopilot validator trigger fix lost)
- FN-5623 (`fn goals` CLI lost)
- FN-5616 (source-issue close handlers lost)
- FN-5614 (`fn update` collision retry lost)
- FN-5613 (dashboard reload banner lost)
- FN-5612 (bundled-plugin-install lost)
- FN-5599 (tablet modal width lost)
- FN-5597 (ntfy notifier priority lost)
- FN-5596 (PR tab spacing test lost)
Fusion-Task-Id: FN-5627
Implements graceful suppression of transient `task.json` ENOENT errors in the executor, logging a suppression signal and surfacing a banner in the UI, with test coverage for both the executor behavior and notification service. Documentation in `docs/architecture.md` and a changeset for `@runfusion/f
Fusion-Task-Id: FN-5624
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5624
Introduces a Goals REST API (`GET/POST/PUT /api/goals` and `GET/PUT /api/goals/:id`) backed by a new `@fusion/core` goal store and typed goal types, including comprehensive route and store test coverage. Documentation on architecture and storage is updated to reflect the new domain, and a changeset
Fusion-Task-Id: FN-5622
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5622
Implements source issue delete handling in the task dashboard, including prompt-based user interaction for source-linked issues, new GitHub tracking state logic in `packages/dashboard/src/github-tracking-state.ts`, and corresponding test coverage, with documentation added to `docs/task-management.md
Fusion-Task-Id: FN-5618
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5618
FN-5615 improves TUI token visibility by dynamically sizing the system panel height and clarifying the manual token copy hint, with regression tests added in the dashboard-tui app and a changeset for the patch release.
Fusion-Task-Id: FN-5615
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5615
Complete the standalone build pipeline fix by setting `DEV=false` during bun compile to eliminate React DevTools from production executables, with hardened regression tests and documentation.
Fusion-Task-Id: FN-5611
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5611
Added OpenAI Responses API as a new custom provider type, wiring `apiType: "responses"` through the core registry, engine routes, and dashboard UI with a dropdown selector; includes test coverage across the registry, routes, and component layers.
Fusion-Task-Id: FN-5601
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5601
This merge implements an OAuth relogin banner feature (FN-5595) that displays in the dashboard when OAuth tokens expire. The feature includes a new `OAuthReloginBanner` component with styling and tests, an OAuth validity logger in the engine for tracking token state, and corresponding API route inte
Fusion-Task-Id: FN-5595
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5595
Cold-start dashboard responsiveness went from ~99s to ~6-11s. CPU profiling
identified two synchronous-spawn hotspots blocking the event loop:
- `GitHubTrackingReconciler` scanned up to 200 done tasks per startup,
each call into `getIssue` invoking `isGhAvailable()` + `isGhAuthenticated()`
via `execFileSync`. `gh auth status` makes a network roundtrip, so 400
sync spawns ≈ 71s of pure event-loop blocking (69% of cold-start CPU).
Memoized both checks with a 60s TTL; `resetGhAvailabilityCache()` is
exported for login/logout flows that need immediate invalidation.
- `PRAGMA integrity_check(100)` walks every page of the SQLite file (~7s
per database, multiple DBs × projects). The deferred check was scheduled
3s after init — right in the responsiveness-critical window. Pushed to
60s so the user is already interacting before it runs; check itself is
unchanged.
Also yields the event loop between major InProcessRuntime init phases and
between self-healing recovery steps (34 per project), defers orphan-task
AI agent resumption by 30s (env-overridable, auto-zero under Vitest), and
ships an opt-in `FUSION_TRACE_EL_LAG=/path/to/file` event-loop lag tracer
that diagnosed all of the above.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two compounding bugs surfaced as `feat(FN-XXXX): merge fusion/fn-XXXX`
commits landing on main:
1. The verification-fix finalize path could bind `undefined` to SQLite
parameter 4 (`commitSha`) of `upsertTaskCommitAssociation` under the
parallel-attempt race, failing the merge over a denormalization
write after the commit had already landed. Centralized both
duplicated callsites into a helper that validates each git output
before binding.
2. Four self-healing/aiMergeTask recovery sites copied
`classification.commit.subject` verbatim into
`mergeDetails.mergeCommitMessage`, persisting the tier-3
`merge ${branch}` fallback when it ended up on the landed commit.
New `regenerateBareMergeSubject` helper detects the bare pattern
and rebuilds a descriptive subject via the AI summarizer. Cosmetic
only — the git commit is not amended.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Multiple coordinated fixes for the perceived "dashboard takes forever to
load" complaint. Per-page-load HTTP requests drop from ~177 to ~101 and
duplicate per-project InProcessRuntime creation is eliminated.
- engine: shouldUseHybridExecutor no longer auto-enables for local-only
multi-project setups (set FUSION_HYBRID_EXECUTOR=1 to force). The
duplicate-runtime path was running self-healing twice per project and
contending on the same SQLite file. ProjectEngineManager already
handles N local projects with one InProcessRuntime each.
- dashboard cli: parallelized independent store inits, started
CentralCore.init early in background, ran plugin loading concurrently
with extension resolution. Sequenced SQLite store inits to avoid a
TOCTOU race in addColumnIfMissing migrations across TaskStore /
AutomationStore / PluginStore / AgentStore (all open the same
.fusion/fusion.db). Restored try/catch around HybridExecutor.initialize
and engineManager.ensureEngine so a paused or broken cwd project no
longer aborts dashboard startup.
- dashboard client: added in-flight request dedupe wrapped around the
top API offenders. /api/plugins/ui-slots drops from 17x to 1x per load.
dedupe.forceFresh redirects ALL in-flight waiters to receive the fresh
post-mutation response, not just the forcing caller. Generation
counters in useAgents and AgentListModal protect against slow polls
overwriting fresh state.
- dashboard SSE: agent event handler now debounces 250ms with a
trailing-edge guard so multi-agent activity bursts coalesce to at
most 2 refetches per burst.
- dashboard route: PATCH /api/projects/:id with isolationMode change
returns 503 with actionable guidance when HybridExecutor is
unavailable, instead of silently persisting a config the live runtime
won't honor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds planning branch controls to the PlanningModeModal, extending the planning API and routes with supporting tests and documentation. The changeset bumps `@runfusion/fusion` as a minor release.
Fusion-Task-Id: FN-5585
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5585
The top-level promptWithFallback bypassed the session-attached rich fallback
path that runs isRetryableModelSelectionError + swapPromptSession, so errors
like "No API key for provider: anthropic" propagated without trying the
configured planning fallback. Restore the dispatch with a WeakSet re-entry
guard that preserves the FN-4900 recursion fix for plugin-runtime sessions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace execFileAsync("mkdir", ["-p", ...]) with fs.mkdir({ recursive: true })
in the cloudflared install fallback path. The -p flag is Unix-only and breaks
on Windows cmd.exe ("A subdirectory or file -p already exists"). Test mocks
updated to verify the fs.mkdir call instead of the shelled-out mkdir.
The original report covered both this site and packages/engine/src/worktree-hooks.ts;
the latter was already converted to fs.mkdir independently, so only the
dashboard route change is needed.
Co-Authored-By: kenlin8827 <kenlin8827@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Test shards 3 and 4 were silently failing on every open PR because vitest's
CLI parser was treating `--shard X/Y` as positional file filters whenever the
arg arrived after a `--` separator. Removing the `--` in ci-test-shard.mjs
restores per-shard slicing; verified locally that shard 1/4 and 2/4 now run
distinct subsets.
The two consistently-failing engine tests:
1. self-healing in-review-branch-rebind ambiguous case-variant detection:
dedup keyed on lowercase branch name collapsed two physically distinct
refs (allowed on Linux ext4) into one candidate, so the "applied" path
ran instead of "ambiguous-candidates". Dedup now keys on the resolved
SHA — macOS APFS still collapses (same ref, same SHA), Linux keeps both
(distinct SHAs) and the ambiguity skip path fires as designed.
2. worktree-acquisition resume-misbinding spy: the production
verifyResumeBranchNotMisbound returns early when `git merge-base HEAD main`
fails, which is exactly what happens on shallow checkouts. Bumping the
test-shards checkout to fetch-depth: 0 makes CI mirror the local git
state these engine tests rely on.
Also adds `push: branches: [main]` to PR Checks so regressions like this
(which slipped into v0.33.0 with no post-merge run) go red immediately
on landing instead of being discovered on the next PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds support for project-only notification deep links in the dashboard hook, with comprehensive test coverage across all deep-link URL shapes and a changeset for release.
Fusion-Task-Id: FN-5583
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5583
The Git Manager's recent integration-advances list derived `needsAction`
purely from the original `merge:auto-sync` audit-event outcome, so it kept
showing "(N need action)" after the operator clicked "Sync working tree" or
fixed up the worktree by hand. `collectRecentMergeAdvances` now also checks
whether each advance's `toSha` is reachable from HEAD — if it is, the
worktree already contains that advance and `needsAction` is false
regardless of what the audit trail recorded.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three coupled fixes to make `pnpm test:full` exit cleanly when the local
`fn` dashboard is running:
1. scripts/check-test-isolation.mjs — replace timing-based "is the
engine writing?" heuristic with a deterministic check: if
`.fusion/engine.lock.lock/` exists (proper-lockfile's held-lock
marker), the dir is engine-active and auto-skipped from violation
reporting. The 2-second mutability probe is retained as a backstop
for dirs with another external writer but no live lock. Also adds
`engine.lock` / `engine.lock.lock/` to RUNTIME_IGNORE_PATTERNS so
a mid-test engine start/stop doesn't trip the signature compare.
2. packages/dashboard/.../__tests__/GitManagerModal.test.tsx — prune
the Status-panel Sync button + Recent-advances-events describe
blocks. Their UI was removed in 5d35b64bd ("remove duplicate
integration-advances UI") but the tests stayed and were timing
out at 1s each. The Remotes-panel Sync describe is kept because
the `remotes-sync-integration-tip-btn` still exists.
3. packages/engine/.../merge-reuse-task-worktree.slow.test.ts —
update the happy-path assertion to reflect 4c31e885b
("merger auto-syncs project-root checkout after ref advance").
Before that change, the merger's `update-ref` advance left the
project root's working tree stale, so `git status --porcelain`
would differ after the merge. With auto-sync, the new file is
tracked + clean at HEAD, so status doesn't change. Verify the
file actually landed via `git ls-files` instead.
After this, `pnpm test:full` exits 0 with the local dashboard running.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the squash commit was built off a stale integration tip, the FF guard
in advanceIntegrationBranchRef refused the swap with reason
`non-fast-forward-advance` — but the caller only mapped `concurrent-advance`
to IntegrationBranchConcurrentAdvanceError, so the non-FF case fell through
as a plain Error and failed the task. Both reasons share a root cause
(integration moved during the merge window), so they now share the
FN-4500/FN-5083 rebind/retry path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a static pretest check and a runtime vitest-setup wrapper that block
shell/process calls matching `kill|pkill|killall|fuser|lsof ... <port>` or
`.listen(<port>)` against reserved Fusion ports. Reserved set is dynamic:
default 4040 plus $PORT, $FUSION_SERVER_PORT, $FUSION_RESERVED_PORTS, and any
port responding to /api/health on 4040..4045 at worker startup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two cleanups in Git Manager → Status:
- Removed the duplicate "Sync local tip" button (gm-integration-actions)
and the second "Recent integration advances" list (gm-recent-advances)
that rendered above the highlighted block. Also dropped the dead
mergeAdvanceEvents state, fetcher, and SSE subscription that only
fed the deleted UI.
- Sync working tree is now pure-local. Added skipOriginFetch to
PullGitBranchOptions.integration (and the matching POST /api/git/pull
body field). When set, pullGitBranch skips tryFastForwardFromOrigin
entirely — the sequence is just auto-stash → git reset --hard
refs/heads/<integration> → restore stash. The Sync button passes
skipOriginFetch: true because the "N need action" recovery is for
catching the worktree up to a *local* merger ref-advance; touching
origin could silently pull in unrelated remote commits.
Help disclosure rewritten to reflect the pure-local behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`notice` is `events.find(...)` which returns `undefined` (not `null`)
when no match. `waitFor(() => expect(...).not.toBeNull())` exited
immediately because `undefined !== null` — the test never actually
waited for the api mock to resolve. Sometimes the followup assertions
happened to land after the events fetched (test passed by luck);
sometimes they ran while notice was still undefined and the assertions
failed.
Switched all five waitFor sites to `.toBeDefined()` so they actually
block on the events-fetch resolution.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the merger advances local `refs/heads/<integrationBranch>` via
`update-ref` without pushing, the user's project-root worktree HEAD
(symbolic to that branch) follows immediately to the new sha, but the
working files and index don't. The integration-mode pull only ran
`git merge --ff-only origin/<branch>`, which short-circuits as
"already up to date" when local is ahead of origin — leaving the
worktree visibly stale even though "Pull completed" was reported.
Pull now explicitly `git reset --hard <localIntegrationTip>` after
the origin fast-forward step. The autostash above protects user edits,
so the reset is safe regardless of whether the origin FF ran.
Regression test in routes-git.test.ts simulates the
local-ahead-of-origin scenario and asserts the reset-to-local-tip is
issued.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Third root-cause fix in the FN-5475 sweep. When `aiMergeTask` /
`recoverNoOpReviewTasks` classified a task as `proven-no-op` or
`no-changes-finalized`, both call sites moved the task to Done while
clearing `modifiedFiles: []` — silently destroying the audit trail when the
work product was uncommitted in the worktree, squashed against the wrong
branch, or dropped by reuse-handoff churn. This was the load-bearing site
of the FN-5490 / FN-5517 / FN-5526 / FN-5540 lost-work patterns.
Both call sites now check `task.modifiedFiles.length` before finalizing as
no-op. If the task claims work was done but no commit landed, the task is
moved back to `todo` with progress preserved and a new
`task:finalize-lost-work-blocked` audit event is emitted. The next
executor run re-attempts the work; the operator sees the audit event in
the timeline.
The post-hoc `reconcileDoneTaskIntegrity` path is intentionally NOT gated
— it cleans up already-Done tasks (legacy state) and is out-of-scope for
prevention. 9 lost-work tasks already in this state at sweep time are
cataloged in docs/incidents/2026-05-23-lost-work-tasks.md for fresh
re-spec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Settings inheritance (high):
- Restored the value !== initialProjectValue gate on the non-model
project branch. Previously every effective/inherited project key was
persisted as an explicit override on every save.
Git Manager UI lie in remote-only mode:
- "Local <branch> vs origin" card now renders "no local tracking"
instead of a green "Synced" badge when integrationTipSource ===
"remote-only" (no local branch to compare).
- New dedicated "HEAD vs origin/<branch>" card surfaces a meaningful
distance in remote-only mode.
isIndexStale correctness:
- Walks up to 16 reflog entries so multi-hop misses (A→B→C without
sync) are detected; the prior check only consulted @{1}.
- Gated on isOnIntegrationBranch === true so a feature-branch worktree
whose HEAD happens to descend from <integration>@{1} no longer trips
the FN-INDEX-DESYNC warning.
Enumeration-failed events surfaced:
- collectRecentMergeAdvances pairs events with (taskId, newSha) when
both are present, falls back to taskId-only for early-failure events
(e.g. "enumeration-failed") that have neither path nor newSha. The
diagnostic outcome now surfaces on the matching advance instead of
being silently dropped.
aheadOfIntegration semantics no longer shift:
- Split into three distinct fields: aheadOfIntegration (HEAD vs local),
aheadOfIntegrationRemote (HEAD vs origin/<branch>),
aheadOfOriginIntegration (local vs origin). Consumers no longer have
to read integrationTipSource to know which comparison they got.
currentBranch failure no longer masks wrong-branch state:
- Distinguish "command threw" (transient git error) from "command
succeeded with empty stdout" (legitimate detached HEAD). New
currentBranchDetectionFailed field lets the UI surface "branch
detection unavailable" on a real failure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two root-cause fixes for the "fake done" patterns surfaced while debugging
FN-5475's stuck preflight (it depended on FN-5233, which the board reported
as Done but whose squash had stranded on a sibling fusion/fn-* branch).
1. resolveTaskMergeTarget rejects fusion/fn-* sibling branches as a merge
destination — when a task's baseBranch was inherited from a sibling/dependent
dispatch, the merger detached onto and squashed against that branch instead
of advancing main. New audit event surfaces the steering miss so the
underlying baseBranch-propagation bug stays observable.
2. self-healing findLandedTaskCommit verifies ownership against each grep
candidate's body before attribution. The previous code blindly accepted the
first hit of `git log --grep=FN-XXXX` (which matches the entire commit
message); FN-5441 and FN-5446 were both marked done against an unrelated
FN-5483 commit whose body merely mentioned them in prose. commitOwnedByTask
is also tightened: trailers must be line-anchored and the subject fallback
must match conventional-commit form, not a bare substring.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>