Settings (data-loss):
- Non-model project keys now use null-as-delete instead of dropping
undefined via JSON.stringify, so clearing Integration branch (Use
dropdown / auto-detect) actually clears the persisted value.
isIndexStale (false-positive AND false-negative):
- Replaced the empty-worktree heuristic with a reflog-anchored check:
stale iff refs/heads/<integration>@{1} exists, HEAD is descendant of
it, and `git diff-index --cached <prevTip>` is empty.
Auto-sync attribution in collectRecentMergeAdvances:
- Match auto-sync events by (taskId, newSha) instead of taskId-only;
re-merged tasks no longer have older advances mislabeled with the
newest outcome.
- Compare worktreePath after realpathSync on both sides; macOS symlink
paths no longer cause permanent "needs action" false positives.
Extended path no longer 500s:
- Route wraps computeExtendedGitStatus in try/catch and falls back to
basic status on failure. Inner `branch --show-current` wrapped too
so detached HEAD / non-git rootDir doesn't throw.
Integration branch falls back to remote-only ref:
- When refs/heads/<branch> is missing, use refs/remotes/origin/<branch>
as the integration tip. New `integrationTipSource` field
("local"|"remote-only"|"missing") drives a UI badge.
Copy commit hash:
- Short-SHA copy is the default and matches what's displayed; a
separate "full" button copies the 40-char headSha. Previously the
single button silently copied the full SHA when extended was on.
Detached HEAD:
- isOnIntegrationBranch left undefined when currentBranch is empty so
the UI doesn't render "(not on <integration>)" against a
no-branch state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the plain text input with a <select> that lists the project's
local branches (loaded via fetchGitBranches when the Merge section
becomes visible) plus an (auto-detect) default and a Custom… option for
branches that don't exist locally yet.
Branch list is deduped + sorted with common integration names (main,
master, trunk, develop) pinned to the top so the typical case is one
click. Choosing Custom… swaps in a text input with a "Use dropdown"
link to revert.
A previously-saved value not in the loaded list (branch deleted
locally, or initial render before fetch resolves) falls through to the
custom input automatically so the operator can still see + edit it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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 is dismissed.
GET /api/git/status accepts ?extended=1 and returns additional optional
fields:
- integrationBranch + integrationBranchSource (settings|origin-head|fallback)
- integrationTipSha / originIntegrationTipSha
- aheadOfIntegration / behindIntegration (HEAD vs local integration tip)
- aheadOfOriginIntegration / behindOriginIntegration (local tip vs origin)
- dirtyDetails {staged, modified, untracked, conflicted, sample}
- indexStaleVsHead (surfaces the FN-INDEX-DESYNC scenario)
- stashCount
- recentMergeAdvances: up to 5 merge:integration-ref-advance events
joined with merge:auto-sync outcomes; needsAction flag flips when
auto-sync didn't successfully bring this worktree forward
GitManagerModal renders all of it:
- Existing cards get sub-text: branch shows "not on <integration>",
Working Tree shows staged/modified/untracked/conflicted breakdown
- Second row: Integration branch + source, HEAD-vs-integration,
local-vs-origin, stash count
- Yellow warning panel when indexStaleVsHead surfaces the merger's
stale-index situation with a recovery hint
- Recent integration-branch advances list, color-coded by needsAction,
shows the per-advance auto-sync outcome so operators can audit
even after dismissing the banner
All fetchGitStatus calls in GitManagerModal switched to extended:true.
Other callers unaffected — extra fields are optional.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a text input for the canonical integration branch directly under
the Auto-completion mode select. Visible regardless of direct vs PR
mode since the setting applies to both.
Blank preserves the existing auto-resolution cascade
(integrationBranch → baseBranch → origin/HEAD → main). Setting it to
master/trunk/develop/etc. pins the resolution explicitly.
Field trims whitespace and stores undefined when cleared so the
auto-resolution stays active.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs were keeping the Merge Advance Notice banner stuck on screen
even when there was nothing for the user to do:
- Dismiss was dead: the `notice` memo never applied dismissedShas, so
clicking close (or a successful Pull, which calls dismiss()) updated
localStorage but the filter immediately re-matched the same event.
- Auto-sync success was ignored: with mergeAdvanceAutoSync defaulting
to "stash-and-ff", the merger snaps the project-root checkout
forward as part of the merge — nothing left to pull — but the banner
kept appearing. Clicking Pull then hit /api/git/pull which fetched
origin (no change, the merger only advanced the local ref) and
returned pull-clean with no real work done.
The notice memo now (a) filters dismissedShas, and (b) suppresses any
advance event whose autoSync entry for the current user's worktreePath
reports clean-sync or synced-with-edits-restored. Conflict + skipped
outcomes still surface so the user can recover.
Tests: dismiss removes the banner; clean-sync suppresses; pop-conflict
still surfaces; sibling-worktree success doesn't suppress this user.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the missing form control for the auto-sync mode introduced by the
merger hook. Lives next to Direct merge commit routing / Integration
worktree inside the merge-strategy panel and only renders when
mergeStrategy === "direct". Three options with descriptions cribbed from
docs/settings-reference.md:
- Stash + fast-forward (default): preserve local edits across the 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 normalizeMergeAdvanceAutoSyncMode on both the
merged-settings and scoped-settings load paths so a missing/invalid
stored value cleanly falls back to the default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Data-loss fixes in syncWorktreeToHead:
- Untracked-restore checks `git ls-tree -r --name-only HEAD` to skip
paths the new tip added as tracked files; user bytes stay in the
stage dir instead of clobbering merged content.
- Apply-failure on a deleted/renamed file: conflictedFiles falls back
to parsing `diff --git a/<p> b/<p>` headers when --diff-filter=U
returns nothing.
- All git invocations pass `-c core.quotePath=false` so non-ASCII
paths round-trip through copyFileSync.
- Stash-and-ff re-verifies rev-parse HEAD === newSha right before
each `reset --hard HEAD` (TOCTOU). On mismatch we bail with patch
preserved on disk.
- Stage dir lifecycle moved into try/finally with preserveStageDir
flag — kept whenever the user's edits live only in patchPath; rm'd
on all clean exits.
- Patch written to disk before the apply attempt, not only on
failure, so a crash between snapshot and apply doesn't lose edits.
Multi-worktree-same-branch fix:
- New getRegisteredWorktreeBranches returns Array<{branch,path}>
instead of collapsing into a Map. Multiple worktrees can share a
branch via `git worktree add --force -b`; merger now syncs all of
them rather than silently skipping all but the last.
Contract + surfacing fixes:
- JSDoc on merge:auto-sync GitMutationType now lists the actually-
emitted outcome strings + stage enum.
- GET /api/tasks/merge-advance-events joins merge:auto-sync events
within ±5min of the advance and returns them in a new
`autoSync: AutoSyncOutcome[]` field; useMergeAdvanceNotice exposes
the same shape so the banner can surface pop-conflicts (including
patchPath) instead of dropping them.
Hygiene:
- Merger now reads the setting via normalizeMergeAdvanceAutoSyncMode
instead of an inline check + `as unknown` cast.
New tests:
- Untracked-collides-with-tracked preserves merged content.
- Apply failure on deleted file populates conflictedFiles from
patch header.
- Route surfaces autoSync outcomes (clean-sync + pop-conflict)
joined within the time window.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After advanceIntegrationBranchRef ff-updates refs/heads/<integrationBranch>,
the merger now enumerates other worktrees on that branch and reconciles
each one's index + working tree to the new tip via syncWorktreeToHead.
Not a git pull — origin may still be at the previous tip without
pushAfterMerge, so pull --ff-only is a no-op and a naive stash/pull/pop
ends with the worktree restored to the old state. Instead the new
worktree-ref-sync helper:
1. Diffs the worktree against the previous tip to isolate real edits
from the stale-index "phantom diff" against the new HEAD.
2. Snaps clean worktrees forward via reset --hard HEAD.
3. In stash-and-ff mode with real edits, captures them as a binary patch
against the previous tip, snaps to HEAD, then git apply --3way to
restore. Untracked files are saved + restored separately. Patch
conflicts surface as synced-with-pop-conflict with the patch left on
disk for manual recovery.
Per-worktree outcome emitted as merge:auto-sync (new GitMutationType).
Per-step pull:fast-forward / stash:push / stash:pop / stash:pop-conflict
that pass through the auditor are tagged metadata.autoSync=true.
Isolated in its own try-catch so an auto-sync failure can't fail the
already-landed merge. Default behavior is mergeAdvanceAutoSync="stash-and-ff";
"off" preserves the legacy surprise behavior.
Backstopped by merger-auto-sync.slow.test.ts: clean-sync snaps both index
and files forward, ff-only with real edits is a no-op, stash-and-ff
preserves untracked locals across the snap, task worktrees on fusion/fn-*
are skipped, empty branch map emits nothing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Standalone implementation of the stash → ff → pop pipeline used by the
upcoming mergeAdvanceAutoSync merger hook. Returns a discriminated union
(clean-pull | stash-pull-pop | stash-pop-conflict | skipped-dirty |
skipped-not-on-branch | failed) and emits structured audit events via an
optional callback. The dashboard's user-triggered Pull keeps using the
existing /api/git/pull integration path; smartPull stays free of AI
conflict resolution so the merger's post-advance auto-sync is safe to run
inline without escalating to a model call.
Backstopped by smart-pull.slow.test.ts (engine-slow lane): clean-pull,
stash-pull-pop, ff-only skip, off-branch skip, audit-emitter exception
tolerance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Schema for what the merger should do in other worktrees still checked out
on the integration branch when it advances the branch ref. Modes:
off — legacy (user pulls manually)
ff-only — fast-forward only when other worktree is clean
stash-and-ff — Smart Pull pipeline (default)
Threads through DEFAULT_PROJECT_SETTINGS, PROJECT_SETTINGS_KEYS (auto via
Object.keys), the docs settings table, and parity + persistence tests.
Merger consumption lands in the follow-up engine change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The route was extracting getRunAuditEvents off scopedStore and calling it as
a bare function, which made this.db.prepare(...) throw. useMergeAdvanceNotice
silently swallowed the error, so the banner never rendered after merges.
Call the method on the store reference instead so this is preserved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a "session runtime resolved" audit event that flows through the engine's main execution lanes — triage, executor, reviewer, merger, heartbeat, step-session-executor, and mission-execution-loop — with runtime mutation support and test coverage, plus a compile-fix for the merger auditor wiring.
Fusion-Task-Id: FN-5544
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5544
Changes the default merge strategy from squash to direct by flipping `directMergeCommitStrategy` in the settings schema and types, with the core implementation in `merger-ref-update-advance.ts`. Also aligns a heartbeat executor test assertion with the FN-5060 deduplication shape.
Fusion-Task-Id: FN-5255
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5255
Follow-up to bf4428c00 (FF-only ref advance). After the prevention fix
new orphans can't form, but pre-fix orphans like f6358ce4 on
fusion/fn-5419 still need a path back onto the integration branch.
Adds an `orphan-our-advance` classification to contamination recovery:
a "unique" foreign commit whose Fusion-Task-Id trailer points at a
`done` task AND that is unreachable from refs/heads/<integrationBranch>
is treated as a stranded merger output.
For these, the executor attempts a fast-forward rehome onto the
integration branch via advanceIntegrationBranchRef (which still enforces
the FF-only invariant). When successful, the orphan sha is added to the
existing `shasToDrop` set so the same recovery pass that drops
already-upstream/misrouted commits also drops the now-upstream orphan.
Non-FF orphans (diverged from current integration tip) are refused.
Doing a cherry-pick onto the integration branch from inside automated
recovery would introduce conflict-resolution surface that's too high
blast radius for a never-event recovery path. The refusal log line
includes the exact `git cherry-pick <sha>` command an operator can run
manually.
Two new GitMutationType audit events:
- merger:orphan-rehome-ff (successful FF rehome)
- merger:orphan-rehome-refused (non-FF, manual cherry-pick required)
Tests in merger-orphan-rehome.test.ts cover classification (orphan,
not-done, already-reachable, no-trailer) and the rehome operation
(FF success advances the ref + emits the audit event; non-FF refusal
emits the hint and leaves the ref untouched).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes a "non-fast-forward ref overwrite" path where a subsequent merger
could orphan a previously-merged squash by advancing the integration
branch to a sibling commit.
Symptom (observed on fusion/fn-5419): main reflog shows
385b6e93 -> f6358ce4 (FN-5551 squash) -> 63ec7098 (FN-5552 squash)
with f6358ce4 and 63ec7098 both parented at 385b6e93. The FN-5551 squash
was correctly committed to main, then the FN-5552 merger built its own
squash off the stale 385b6e93 base and the CAS update-ref blindly moved
main sideways, orphaning f6358ce4 onto whichever feature branch had
already branched from it.
Two coupled fixes uphold the missing invariant — local <integrationBranch>
only advances via fast-forward, and the merger never builds a squash off
a stale base sha:
1. advanceIntegrationBranchRef: add a `merge-base --is-ancestor` check
before update-ref. Non-FF attempts now return
reason: "non-fast-forward-advance" instead of overwriting the ref.
The existing concurrent-advance CAS guard is retained.
2. runMerge: resolve the integration-branch tip via
`git rev-parse --verify refs/heads/<integrationBranch>` instead of
`git rev-parse HEAD` in rootDir. In reuse-task-worktree mode rootDir's
HEAD can lag behind the shared ref after a sibling merger advanced it
via update-ref without re-checking-out.
Adds regression coverage in merger-ref-update-advance.test.ts: a
sibling-commit advance with a matching expectedCurrentSha is now refused
with the new reason, and multi-commit fast-forwards still succeed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Decouples the nav-bar keyboard-open class from the gated mobileKeyboardOpen
flag so the bar stays pinned to bottom: 0 regardless of modal state or
platform, instead of floating up with the iOS visualViewport.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements project identity tracking and recovery across the Fusion system (FN-5411), enabling persistent identity for projects across storage migrations, daemon reattaches, and CLI session management. Adds a project identity metadata API and central reattach ensure mechanism, wires identity stampin
Fusion-Task-Id: FN-5411
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5411
Same-agent intake guard now also matches siblings sharing a
sourceParentTaskId, so repeated heartbeats from one parent task
can't bypass dedup just because triage rewrites the title.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
assertCleanBranchAtBase now checks each foreign-attributed commit
against `git merge-base --is-ancestor <sha> main`. If the commit is
already on local main, it was promoted through integration regardless
of whose Fusion-Task-Id trailer it carries — treating it as foreign
contamination is wrong and was the root cause of the FN-5475 cascade
(downstream worktrees inherited a sibling task's tip during the brief
fast-forward window before main moved further).
Audit cost: O(N) extra git calls per audit run, where N is the number
of foreign-trailer commits in baseSha..branchName. Each call is ~5-10ms
and N is typically 1-5. Negligible relative to the surrounding I/O.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The visualViewport ICB compensation (intended for Android ICB-stuck-large
and pinch-zoom) was also reacting to iOS soft-keyboard shrinkage, pushing
the mobile bottom nav up above the keyboard. Pass the existing
`keyboardOpen` prop through to a `mobile-nav-bar--keyboard-open` modifier
class that pins `bottom: 0` so the keyboard simply covers the bar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The resume path in acquireTaskWorktree returned a reused worktree
without checking whether its branch contained foreign commits. If a
sibling task's tip had been baked into the branch at creation time,
the executor preflight would later fail contamination checks forever
(observed in the FN-5475 cascade).
The resume path now computes a fresh merge-base and runs
classifyBootstrapMisbinding. For the foreign-only / zero-own-commits
shape it re-anchors inline and emits a branch:reanchor audit event.
Mixed contamination continues to flow through the executor's
primary recovery path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The auto-recovery handler in branch-worktree.ts passed
foreignCommits: [] to classifyBootstrapMisbinding, and the classifier
gated isBootstrapMisbinding on foreignCommits.length > 0. The entire
reanchor block was dead code on this path — the FN-5475 cascade hit
"human adjudication" instead of recovering.
The classifier now derives the foreign-commit count from its own
git log walk; the input field is advisory/optional. Result type gains
foreignCommitCount. The fallback handler also stops using
ctx.task.baseCommitSha (deliberately stale per FN-4417) and computes
a fresh merge-base against local main / origin/main, matching the
executor's primary contamination path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Header.css: mobile padding-top is additive (var(--space-md) + env(safe-area-inset-top)) so the brand row keeps its 12px breathing room below the Android status bar instead of having it replaced by the inset.
- ExecutorStatusBar.css: bottom offset now uses max(env(safe-area-inset-bottom), 12px) to match the floor MobileNavBar already applies, so the footer lands flush on top of the nav instead of inside its padding band.
- useToast.ts: silently drop bare "Failed to fetch" error toasts (from fetch() aborts on tab background/resume); toasts with additional context still pass through.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address the code-review finding on the prior commit: a natural premise-stale
summary like "PREMISE STALE: the task has no remaining work — implementation
is already done on HEAD" matches /\b(incomplete|not implemented|not done|
not finished)\b/i with 'the task' inside the 40-char first-person window,
refusing fn_task_done with summary-claims-incomplete and deadlocking the
escape hatch.
When summary starts (case-insensitive) with PREMISE STALE:, skip the
dissent-pattern and scoped-incomplete summary checks. Pending-code-review
and bulk-step-completion guards still apply unchanged.
Add executor-task-done-premise-stale.test.ts covering: the deadlock case
now passes; dissent phrasing in a sentinel summary is allowed; case-
insensitive sentinel; sentinel must be at the start (mid-summary doesn't
bypass); REVISE verdict still blocks even with the sentinel.
Root-cause prevention for the FN-5521 failure class where PROMPT.md describes
work that is already done on HEAD and the executor still marches through
plan/review/test/doc, then merger burns hours retrying against unrelated
flakes in @fusion/engine reliability-interactions.
- Executor system prompt: add a Preflight escape hatch. When Step 0
reproduces and finds HEAD already matches the desired state, the agent
marks Step 0 done, marks remaining steps skipped, and calls fn_task_done
with a `PREMISE STALE:` summary. Reuses the existing
evaluateTaskDoneRefusal/skipped semantics and the merger's
empty-own-diff fast-path — no new tools or refusal classes needed.
- packages/engine/vitest.config.ts: split into two projects. engine-default
retains full parallelism; engine-reliability scopes
src/__tests__/reliability-interactions/** to
poolOptions.threads.singleThread so event-ordering assertions stop
flaking under workspace-concurrent merge-gate load (the
`expected 24 to be less than 19` rowid interleaving in
merge-reuse-task-worktree).
`attemptBranchAutocorrect` was the only branch-creation site in the
engine that ran `git checkout -B <expected>` without a start point.
When the worktree's HEAD was at a previous occupant's tip, the new
label silently captured that commit — the "branch: Created from HEAD"
contamination pattern that the cross-contamination guard then refuses
to auto-resolve (see FN-5456: orphan FN-5477 commit 268574b9a stranded
on fusion/fn-5456).
Replace the unsafe fallback with verify-then-`git checkout`: only switch
to an existing expected ref; return `failed` when it does not exist so
upstream recovery — which knows the proper base SHA — can re-anchor via
`prepareForTask` / `reanchorBranchToBase`.
Tests updated for the new command sequence; new case covers the
"expected ref missing → fail without creating from HEAD" guarantee.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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
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
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>
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
- 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>
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>