When the in-merge fix agent makes no changes AND all failing test files are
outside the branch's diff, the merger now throws OutOfScopeVerificationError
and marks the task status: "failed" with a clear error message:
"Merge verification failed in files outside branch scope — likely
pre-existing flake on main. Fix the base-branch test breakage
separately and retry."
This prevents the task from entering the completion-handoff-limbo recovery
cycle (which would retry the merge endlessly) when the verification failure
is caused by pre-existing flakiness in an unrelated package (e.g. engine
reliability-interaction tests failing while only dashboard was changed).
Failing file paths are parsed from vitest/jest output (FAIL lines and ❯
summary lines). If parsing yields no file list, the existing retry behavior
is preserved. The OutOfScopeVerificationError propagates through the catch
block so it does not count toward completionHandoffLimboRecoveryCount.
New exports: OutOfScopeVerificationError, parseFailingFilesFromOutput,
getBranchChangedFiles.
Tests added: parseFailingFilesFromOutput (4), getBranchChangedFiles (3),
OutOfScopeVerificationError constructor (1). All 58 merger-verification
tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a pnpm workspace is detected and git context is available,
inferDefaultTestCommand now derives the set of packages touched by the
branch diff (git diff --name-only <base>...HEAD) and emits:
pnpm --filter "<pkg>...^" test
instead of the broad `pnpm test`. The `...^` suffix includes dependents
so packages that import the changed one are also exercised. Falls back
to unscoped `pnpm test` when git context is missing, the workspace has
no package roots, or all changed files are at the root (e.g. config).
New exports: parsePnpmWorkspaceGlobs, resolveWorkspacePackageRoots,
mapChangedFilesToPackageNames, deriveScopedPnpmTestCommand.
testSource is now "inferred-scoped" for the scoped path.
Tests added: parsePnpmWorkspaceGlobs (7), resolveWorkspacePackageRoots
(4), mapChangedFilesToPackageNames (4), inferDefaultTestCommand scoping
(6). All 466 merger test files pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Android Chrome under viewport-fit=cover intermittently reports
env(safe-area-inset-bottom) as 0 while the address bar is visible or
during URL-bar collapse, causing the bottom nav to render beneath the
system gesture pill. Floor the inset to 12px in MobileNavBar's own
padding and the matching content-padding rules so the bar always clears
the gesture area; devices that report a larger inset are unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switch commits Fusion produces (both executor step commits and merger squash
commits) from setting `--author="Fusion <noreply@runfusion.ai>"` to appending
`-m "Co-authored-by: Fusion <noreply@runfusion.ai>"`. The user's configured
git identity now stays as the primary author/committer, and Fusion is recorded
as a co-author (recognized by GitHub for shared attribution). The
`commitAuthorEnabled` toggle and `commitAuthorName`/`commitAuthorEmail`
settings keep their existing keys; the dashboard settings UI relabels them
from "Author" to "Co-author" to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two engine merger tests created mkdtempSync workspaces directly in tmpdir()
under the tracked `fusion-test-` prefix; under full-suite concurrent load
the post-run check-test-isolation flagged them as leaks. Route both
(`merger-no-op-fix-finalize.test.ts`, `merger-verification-fix-already-on-main.test.ts`)
through FUSION_TEST_WORKER_ROOT like sibling merger tests so they nest
inside the already-tracked worker root.
Bump engine vitest subprocess guard from 60s to 120s and testTimeout to
30s — plain git commands (branch -d, worktree remove) queued behind
system contention during `pnpm -r --workspace-concurrency=2` runs were
timing out. The guard only fires on hangs, so healthy tests pay nothing.
Also bundles in-progress dashboard mobile-breakpoint regex/CSS test
updates and docs index additions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The vitest subprocess guard's 60 s tracking timer could outlive the test
that spawned it and fire during a later test's afterEach, producing
spurious "Timed out after 60000ms" failures attributed to a different
test name under concurrent recursive test load.
Scope "Left running" reporting + SIGKILL to the current test's procs but
always clear each tracked subprocess's timer so it cannot fire later.
Bump the post-test grace from 200 ms to 1 s to absorb event-loop
contention from slow git shells.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 769–1024px breakpoint forced 6 columns into the visible width with
`grid-template-columns: repeat(6, minmax(0, 1fr))` and `overflow-x:
hidden`, collapsing columns to ~130–170px on Android tablets and
stacking task card titles one word per line. Switch to
`minmax(260px, 1fr)` with `overflow-x: auto` so columns keep a readable
minimum width and the board scrolls horizontally, matching desktop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ChatView and QuickChatFAB both had an iOS-specific onTouchStart on the
textarea that called event.preventDefault() and then programmatically
re-focused the input — meant to suppress iOS's visualViewport auto-scroll
on re-focus. On Android, preventDefault on a textarea touchstart blocks
the soft keyboard from opening (programmatic focus() alone does not raise
the Android keyboard — only the default touch action does), so tapping
the main chat or quick chat composer focused the input but the keyboard
never appeared, looking like an instant dismiss.
Gate the touchstart workaround to iOS via isIOS().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-review follow-ups on 24686cade:
- Only `git reset --hard HEAD` / `git clean -fd` after the stash is confirmed (non-empty SHA + store succeeded). Previously an empty `git stash create` result (no throw) would still trigger the reset and wipe the user's dirty edits before the refusal was raised.
- Quote the stash label with a local `quoteAutostashArg` helper to match `merger.ts`'s `quoteArg` pattern. Task IDs are alphanumeric today, but defense-in-depth keeps `git stash store -m` safe against future label content.
- Add `merge:reuse-handoff-autostash` to the typed audit unions in `run-audit.ts` and `emitReuseHandoffAuditEvent`, so subscribers see it as part of the closed set instead of relying on the `as any` escape hatch.
- Best-effort `git reset` (unstage) in the autostash-failure path so the worktree isn't left half-staged when we refuse.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stop losing uncommitted dev edits during task merges.
- `acquireReuseHandoff` no longer throws MergeHandoffRefusedError("working-tree-dirty") on a dirty reused worktree (FN-5138). It autostashes via `git add -A` + `git stash create` + `git stash store`, emits a `merge:reuse-handoff-autostash` audit event with the stash SHA and a recover command, and lets the merge proceed.
- `stashUnrelatedRootDirChanges` no longer silently proceeds when stash creation fails on a dirty tree. It throws a new `AutostashCreationFailedError`; the merger catches it and surfaces a clear "your edits are intact" message before any destructive op runs.
- New failure reason `dirty-worktree-autostash-failed` distinguishes stash failure from the old refusal.
- Tests in `merger-integration-worktree`, `merger-cwd-fallback-removed`, and `reliability-interactions/{integration-worktree-state,merge-reuse-task-worktree,cwd-integration-fallback-removed}` updated to the new contract; the FN-5348 "no cwd fallback" invariant is preserved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- touch-action is per-element, not inherited. Setting `pan-x pan-y` on
html/body alone didn't stop the kanban board: pinching on `.board`
reads `.board`'s own touch-action (auto by default), which permits
pinch-zoom. Switch to `* { touch-action: pan-x pan-y }` inside the
mobile media query so every element opts out of pinch by default;
element-specific overrides (touch-action: none on resize handles,
drag-source rules on TaskCard, etc.) still win on specificity.
- Add `overscroll-behavior: none` to html/body to prevent Chrome's
rubber-band scroll, which was letting users pull the page up to
expose empty space above the dashboard despite overflow:hidden.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two Android-specific fixes:
1. Keyboard dismissing in main chat. App.tsx derives `mobileKeyboardOpen`
from useMobileKeyboard and uses it to gate the
`project-content--with-mobile-nav` / `--with-footer` className
assignment plus MobileNavBar rendering. When the soft keyboard opened
on Android, 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. With interactive-widget=resizes-content
set on Android, the layout viewport itself shrinks with the keyboard, so
the hide-nav-on-keyboard pattern was redundant on Android (and harmful).
The whole pattern is now gated to iOS via isIOS(). iOS path is unchanged.
2. Pinch-zoom on kanban. Android Chrome ignores user-scalable=no for a11y,
and kanban's overflow-x:auto columns combined with the inflated ICB
produce a broken visual when the user zooms out. Adds
touch-action: pan-x pan-y to html,body inside the mobile media query
(keeps scroll panning, blocks pinch-zoom). Chat and MissionManager were
unaffected before because they don't expose a wide horizontal
scrollable region.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The body scroll-lock applied while the keyboard is up in main chat was an
iOS-specific workaround for visualViewport drift. On Android Chrome the same
mutation does the opposite of what we want — applying position:fixed to body
while the soft keyboard is opening causes Chrome to treat it as a focus-
target relayout and dismiss the keyboard instantly, making the main chat
composer unusable on Android.
useMobileScrollLock now early-returns on non-iOS user agents. Android Chrome
doesn't need it: with interactive-widget=resizes-content the layout viewport
shrinks with the keyboard, so there's no drift to compensate for.
Adds an Android-UA test case that asserts the lock is a no-op there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Some Android Chrome builds (multi-window / split-screen / certain WebViews)
leave window.innerWidth/Height stuck larger than the actual rendered canvas.
DOM, body, and visualViewport report the true dimensions, but position:fixed
uses the ICB, pinning fixed-bottom elements offscreen below the visible area.
JS-side meta override (setAttribute and full replacement) does not force
Chrome to recompute the ICB on those builds.
index.html now publishes the ICB→visualViewport delta as CSS variables
(--icb-bottom-offset, --icb-right-offset) on <html>. MobileNavBar.css and
ExecutorStatusBar.css consume them so the bars pin to the visible viewport
edge regardless of ICB drift. Math is visualViewport-relative so it also
handles pinch-zoom in (offsets compensate) and pinch-zoom out (clamp at 0).
Healthy browsers see 0px and behave unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Broaden mobile media query to include (max-height: 480px) so landscape
phones (which exceed 768 CSS px wide) still render the bottom nav and
mobile board layout instead of desktop horizontally-scrollable columns.
- Guard useMobileKeyboard against pinch-zoom (vv.scale > 1) — Android
Chrome ignores user-scalable=no, and a focused textarea + zoom was
false-positiving keyboard-open and hiding MobileNavBar.
- Read documentElement.clientHeight instead of stale window.innerHeight
when computing keyboard overlap (Android multi-window can leave
innerHeight cached at a wildly different value than the actual layout
viewport — observed 2848 while html was 797).
- Add interactive-widget=resizes-content to the viewport meta so Android
Chrome shrinks the layout viewport with the soft keyboard, matching iOS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a 255-line fast-path regression harness (`self-healing-fn-5488-fast-path-regressions.test.ts`) covering unbacked merge fast-paths, retry-exhausted fanout, grace-window boundaries, and overlap preservation invariants. Also includes a small cleanup to `MobileNavBar.test.tsx`.
Fusion-Task-Id: FN-5498
Fusion-Task-Lineage: f39ca8a1-223b-4b43-a844-6cb17d19e099
Fusion-Task-Id: FN-5498
After a long session of guess-and-check without remote debugging, none
of the viewport/portal/keyboardOpen interventions cleanly fixed the
Android Chrome multi-window layout bug. Reverting index.html,
MobileNavBar.tsx, Board.tsx, and the FN-5476 overlap test back to
their pre-session state. The board-squeeze fix in styles.css is kept
(confirmed working).
The remaining workaround: turn off "Request Desktop Site" on the
Android tablet for an acceptable layout. Revisit with chrome://inspect
remote debugging when available.
Reverts (squashed):
- 49839245e portal MobileNavBar
- 4bd9711e8 drop keyboardOpen guard
- d2e31dc10 don't fight pinch zoom
- f881ed22e visualViewport.width override
- 301e0a050 self-healing overlay
- 16b18bf33 publish mNavDebug
- d3dff9442 extend vpdebug
- a9094d79d restore overlap test
- 94bc43e04 restore FN-5476 observer
- 5a9095e54 re-drop viewport flags + add vpdebug
- and the index.html/Board.tsx portions of fbf7e2cb4
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vpdebug on Android tablet captured the nav bar's getBoundingClientRect
at y=2800 with html height 798 — meaning some ancestor was creating a
containing block for the bar's `position: fixed`, putting it 2000+ px
below the visible window. Any ancestor with `transform`, `filter`,
`will-change`, or `contain: paint` would do that, and it's hard to
audit which of our many ancestors (providers, view wrappers, plugin
hosts) introduces one in a given Android Chrome state.
Sidestep the whole question: render the bar via createPortal into
document.body. Its only ancestor is body, so `position: fixed; bottom: 0`
is now guaranteed to pin to the visual viewport bottom.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vpdebug overlay on Android tablet captured the smoking gun: with the
corner-cutoff fixed (viewport now full window), the bar still disappears
because useMobileKeyboard's keyboardOpen state can get stuck at true.
Repro: the Planning column textarea autofocuses on mount; Android Chrome
fires a transient visualViewport.resize during focus that registers as
"keyboard open", but the matching dismiss event never fires once the
keyboard never actually appeared. visualViewport stays at full height
yet useMobileKeyboard's state remains true, and MobileNavBar's render
guard hides the bar forever.
The original guard existed to keep the bar off the iOS soft keyboard.
On Android the keyboard pushes content up so the bar isn't covered
anyway; on iOS the worst case (bar overlapping keyboard) is far less
broken than the bar permanently vanishing on every Android tablet
session. Drop the keyboardOpen check from the render guard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
visualViewport.resize fires on pinch-zoom, not just window-size change.
The previous viewport-override would re-assert initial-scale=1.0 on
every pinch event, snapping the user's zoom back to 1.0 instantly.
Restrict the listener to orientationchange + initial settle timers
so pinch-zoom is left alone after page load.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Added soft-delete blocker recovery logic to the scheduler and self-healing systems, enabling reconciliation of stale blocker reasons when tasks are archived or restored. The changes include corresponding tests for the completion guard behavior, scheduler recovery paths, and self-healing integration,
Fusion-Task-Id: FN-5496
If something (React portal, modal mount, third-party widget) detaches
the overlay or covers it with a later body child, the overlay now
re-appends itself to the end of document.body on every update tick.
Lets us capture the post-task-load state even when something is
fighting it for top-of-body paint order.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surface the live mode / modalOpen / keyboardOpen / footerVisible / view
values that MobileNavBar uses for its early-return so the ?vpdebug overlay
can show which one is hiding the bar on Android. Also dumps the
.project-content className so we can correlate with `--with-mobile-nav`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add mNav (bounding box + display/visibility/z-index/opacity),
mq768 (mobile media query state), dialogs (open modal count), and
focused element to the ?vpdebug overlay so we can see at a glance
which condition is hiding the bottom nav on Android.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion to 94bc43e04 — the contract test for FN-5476's Android
overlap fix was also dropped by the earlier accidental sweep.
Restored verbatim from the FN-5476 commit (496d27559).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an oscillation backstop and guard mechanism for completed tasks to the self-healing system, including a dedicated 145-line test suite for completed-task oscillation behavior and a small update to the existing self-healing module.
Fusion-Task-Id: FN-5448
The merge delivers the FN-5483 identity-guard bypass for merger-driven commits on detached HEAD, plus Steps 2 and 6 of FN-5479 which gate the limbo counter by enqueue acceptance and document the associated invariant. It also restores the dashboard's PWA and theme-boot contract in index.html. New reg
Fusion-Task-Id: FN-5479
Adds retry-reset logic that clears the user-paused flag on tasks, ensuring they can resume automatically after a retry is triggered, with regression tests covering the behavior across the CLI extension and core manual-reset module.
Fusion-Task-Id: FN-5485
In multi-project mode, all /chat/sessions* handlers used options.chatStore
(the home-dir project's store) regardless of the projectId query param.
Sessions in secondary projects were invisible via the API.
Root cause: registerChatRoutes accessed options.chatStore directly instead
of routing through resolveProjectChatContext (already used correctly in
registerChatRoomRoutes for the rooms API).
Fix: introduce resolveScopedChatStore(projectId) helper that delegates to
resolveProjectChatContext, replacing all ten options.chatStore usages.
Falls back to default store when engineManager is absent (backward compat).
Tests: add two cases to verify engine-scoped chatStore is used when
engineManager is configured for the requested projectId.
WorktreePool.prepareForTask now rejects empty/"HEAD" base values and verifies
that the worktree's HEAD actually landed at the resolved base SHA after
`git checkout --detach`. This closes the FN-5432 / FN-5255 contamination
pattern where a recycled worktree branched from a stale HEAD (reflog:
"branch: Created from HEAD") and pinned the new task's tip to the previous
occupant's commit.
SelfHealingManager.tryReanchorForeignOnlyContamination is invoked from both
PR-conflict and self-owned-branch-conflict catch sites before the
unrecoverable-pause path. When the conflicted branch carries only foreign
commits (no own work), the branch is reset to base via the existing
recoverForeignOnlyContamination flow instead of being escalated for human
adjudication.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FN-5204 reintroduced `maximum-scale=1.0, user-scalable=no` into the
viewport meta as part of unrelated test-mode work, undoing the
Android-tablet fix from fbf7e2cb4. Drop them again.
Also add a `?vpdebug` query-flag-gated overlay that prints live
window / visualViewport / html / body / #root / #board bounding
boxes to a fixed top-right panel. Diagnostic only — remove once
the remaining Android tablet cut-off case is understood.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes FN-5481 by unskipping soft-delete matrix backstop tests, adding substantial new coverage to the soft-delete end-to-end test suite (290 lines added/modified) and updating the verification matrix documentation to reflect the newly enabled test scenarios.
Fusion-Task-Id: FN-5481
Implement the hard-archived delete contract: add a typed `ArchivedTaskDeleteError`, map hard-archived task deletes to HTTP 410 Gone at both the store and routes layers, and document the invariant in the soft-delete verification matrix.
Fusion-Task-Id: FN-5196
- Add core settings schema/types support for testMode with model-resolution override handling
- Enforce engine session lane overrides in test mode with targeted helper coverage
- Add dashboard settings toggle plus persistent test-mode banner and related component tests
- Update settings documentation and parity/roundtrip tests for the new test mode behavior
- Drop the tablet-tier `.board` grid rule that crammed 6 columns into
≤1024px viewports with no min-width, scrunching column content to
unreadable widths. Tablets now use the default `minmax(300px, 1fr)`
and scroll horizontally like desktop.
- Drop `maximum-scale=1.0, user-scalable=no` from the viewport meta.
Combined with `initial-scale=1.0` those flags trigger Android Chrome
layout bugs in multi-window mode; the Capacitor-feel justification
isn't worth the breakage in a browser-rendered dashboard.
- Broaden the existing iOS scroll-snap stabilization in Board.tsx from
`(max-width: 768px)` to any touch-primary device, and re-run it the
first time tasks populate so Android tablets get the same first-cards-
loaded reflow that mobile already had.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Added a runtime navigation height observer to `MobileNavBar` that captures the browser's dynamic nav-height CSS value at runtime, updated both the nav height test coverage and the Android overlap CSS contract test, and documented the new token in `AGENTS.md`.
Fusion-Task-Id: FN-5476
Added resume instrumentation to the `useAgentLogs` hook (`packages/dashboard/app/hooks/useAgentLogs.ts`) with test coverage and diagnostics documentation. The new test file is included in the dashboard test gate via `vitest.config.ts`.
Fusion-Task-Id: FN-5469
Restructured AGENTS.md from a 588-line catch-all into a lean reference of essential rules by offloading deep guidance into five new/expanded doc files: agents.md, architecture.md, dashboard-guide.md, settings-reference.md, and testing.md. Also restored a missing FN-5345 lifecycle invariant in self-h
Fusion-Task-Id: FN-5339
Fusion-Task-Lineage: e1a2dc4f-ae50-46f4-acd3-8f2c3673feb4
Contamination on fusion/<id> branches (FN-5233 was the recent example:
two untrailered feat(FN-5353): commits sitting on fusion/fn-5233) used
to be invisible until merge time, days after it happened. The executor
already runs assertCleanBranchAtBase at worktree acquisition and reclaim
— the gap was the active session window itself.
Add reportBranchAttribution(repoDir, branch, baseSha, taskId) which
walks base..branch and bins every commit into ownTrailed (healthy),
ownUntrailed (subject tag but commit-msg hook didn't fire), foreign
(different FN-id), or unattributed (no subject pattern, no trailer —
typically a hand-merge or plumbing commit). Wire it into the executor
right after captureModifiedFiles in the post-session path: when any
anomaly bucket is non-empty, emit a structured branch:attribution-
anomaly audit event and a task log entry. The audit itself is wrapped
in a try/catch so a probe failure never destabilizes a completing
session. New branch:attribution-anomaly and branch:auto-reattach-
authoritative GitMutationType variants accept the structured metadata
(the latter for the handoff re-attach added earlier this session).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
commitOrAmendMergeWithFixes used to swallow every unexpected throw as
`reason: "unknown-phantom"` and the two callers re-threw a bare
"verification fix finalize failed (unknown phantom)" with no operator-
actionable signal. FN-5422-class wedges (preAttemptHeadSha == currentHead
but branchTip ahead with task-trailered commits) couldn't be diagnosed
without re-running.
The catch now records the original error and probes whether the branch
ref itself is authoritative for the task (tip carries Fusion-Task-Id
trailer, base..branch is foreign-contamination-free). When it is — i.e.
the work isn't lost, the integration worktree just didn't advance — the
catch resets rootDir to preAttemptHeadSha so the next merge attempt
starts from a known baseline instead of inheriting partial squash state,
and returns `reason: "branch-ref-ahead-reset"`. The two callers fold
`originalError=` and `branchAuthority=` into the thrown message so the
real cause (diff-volume gate, file-scope, transient git, etc.) is
visible in the run log.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The merge handoff refused with head-branch-mismatch whenever the reused
worktree's HEAD wasn't on fusion/<id> (detached, recycled to main, or on
a sibling branch), even when the branch ref itself still held a clean,
task-attributed lineage. That wedged FN-5339-class tasks in review for
no good reason.
Add isBranchAuthoritativeForTask in branch-conflicts.ts (branch ref
exists, tip carries Fusion-Task-Id trailer, base..branch is foreign-
contamination-free) and use it in acquireReuseHandoff: when HEAD drifts
but the branch ref is authoritative, run a plain `git checkout <branch>`
inside the already-asserted-clean worktree, re-read HEAD, and emit a
branch:auto-reattach-authoritative audit. Refusal still fires unchanged
when the branch ref is missing, missing the trailer, or contaminated,
so FN-5363 strict-lease and foreign-commit guards remain authoritative.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>