Commit Graph

7452 Commits

Author SHA1 Message Date
gsxdsm
4e2e201215 feat(FN-5537): merge fusion/fn-5537 2026-05-23 00:13:27 -07:00
gsxdsm
1be0702155 fix(dashboard-mobile): clear Android status bar, footer-nav overlap, fetch toast spam
- 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>
2026-05-23 00:03:22 -07:00
gsxdsm
d9fe33fec6 fix(FN-5456): tidy autocorrect shell quoting and disambiguate checkout
- Hoist `refs/heads/<name>` into a single quoted token before the verify
  call so the shell-quote boundary is unambiguous in the rendered
  command.
- Append `--` to the recovery `git checkout` so a same-named tracked
  path cannot win the DWIM resolution. The ref existence was already
  verified, so this can only resolve as the branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 00:03:12 -07:00
gsxdsm
d3ad0641c4 feat(FN-5486): merge fusion/fn-5486 2026-05-22 23:42:44 -07:00
gsxdsm
7345ab85d0 fix(engine-tests): plug fusion-test-ref-* leaks on SIGTERM fork recycle
Same class of failure as the kb-db-test-* leak: vitest's forks pool
SIGTERMs a fork on test timeout and skips the in-test `finally { rmSync }`,
leaking `fusion-test-ref-project-*`, `fusion-test-ref-concurrent-project-*`,
and `fusion-test-ref-advance-*` dirs that scripts/check-test-isolation.mjs
flags during deterministic merge verification.

Track every minted dir in a per-file set and sweep it from
SIGTERM/SIGINT/SIGHUP/beforeExit/exit handlers (signals re-raised after
cleanup) plus an `afterAll` for the happy path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 23:30:19 -07:00
gsxdsm
380f8b8e5e fix(core-tests): plug kb-db-test-* leaks on SIGTERM fork recycle
Vitest's forks pool SIGTERMs a fork when a test times out, which skips
`beforeExit`/`exit` handlers and leaves `kb-db-test-*` dirs behind.
`holdWriteLock` child processes also kept WAL/SHM handles open, blocking
recursive removal on macOS. Both paths now run cleanup: SIGTERM/SIGINT/
SIGHUP handlers sweep tracked dirs and re-raise the signal, and active
lock-helper children are tracked and SIGKILLed during cleanup so the
parent dir can be removed.

These leaks tripped scripts/check-test-isolation.mjs during deterministic
merge verification, failing auto-merge with "Completion handoff limbo
recovery exhausted" (e.g. FN-5521, FN-5486).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 23:27:44 -07:00
gsxdsm
2a3a07a612 feat(FN-5403): merge fusion/fn-5403 2026-05-22 23:12:49 -07:00
gsxdsm
3ccb132dcc feat(FN-5519): merge fusion/fn-5519 2026-05-22 22:43:26 -07:00
gsxdsm
394b9adc91 feat(FN-5518): merge fusion/fn-5518 2026-05-22 22:42:57 -07:00
gsxdsm
838002491d fix(executor): bypass summary-incomplete refusal for PREMISE STALE: summaries
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.
2026-05-22 22:34:32 -07:00
gsxdsm
8a3afcf9d4 fix(executor+engine-tests): preflight premise-stale exit and serialize reliability suite
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).
2026-05-22 22:26:18 -07:00
gsxdsm
23a0c27bd5 fix(engine-tests): subprocess timeout attribution + ENOTEMPTY retry on cleanup
Two test-infrastructure fixes from agent #1's flake-stabilization pass.
Skipped its DESC-order assertion rewrite (semantically equivalent no-op
that conflicted with the prior boilerplate consolidation in 848a226ca)
and its vitest.config additions (already in flight on main).

1. vitest-setup.ts: completedSubprocessFailures was a plain string[]. When
   a 30s subprocess-guard timer fired during a *later* test's execution
   window (because the owning test ran for e.g. 40s under its 60s
   timeout budget), the failure surfaced in the innocent successor test's
   afterEach. Typed the array as { ownerTestName, message }[] and filter
   on the current test name; orphaned entries are dropped silently.

2. worktree-contamination-attribution.real-git.test.ts: afterEach rm
   occasionally hits ENOTEMPTY on macOS when a git rebase internal dir
   isn't fully flushed. Added maxRetries: 3, retryDelay: 100.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 22:10:45 -07:00
gsxdsm
ed4575cc6f fix(merger): quote pnpm filter args + git refs, widen out-of-scope detection
Code-review fixes for 036387610 / d02cd38d7:

1. `deriveScopedPnpmTestCommand` now passes each `--filter` argument through
   `quoteArg`. Package names come from workspace `package.json` files which
   are not strictly trusted input — a metacharacter in a name would have
   leaked into the shell command.

2. `getBranchChangedFiles` now quotes both git refs in the `<base>...<head>`
   range. Branch names can legally contain `/` and other characters; this
   is defense-in-depth consistent with the rest of merger.ts.

3. Out-of-scope detection is now package-aware via a new
   `packageNamesForFiles` helper. A failure in `__tests__/foo.test.ts` is
   correctly treated as in-scope when the branch touched `src/foo.ts` in
   the same package, whereas the previous filename-prefix heuristic missed
   that case entirely. The dead `bf.startsWith(ff/)` clause is removed.
   Falls back to the directory-prefix heuristic when pnpm-workspace.yaml
   is unavailable.

195 tests pass across merger-verification.test.ts and the reused
merge-reuse-task-worktree.test.ts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 22:06:18 -07:00
gsxdsm
848a226cab refactor(engine-tests): consolidate merge-reuse-task-worktree boilerplate
Each of the 18 tests in this suite was ~50 lines of identical setup: rename
main→master, mark steps done, create the branch with one content commit,
create the task worktree, set task.worktree, enqueue the merge queue. The
test bodies were dominated by ceremony and the unique assertions were buried.

Consolidated into a single `setupReuseHandoff` helper that takes per-test
overrides (skipWorktreeAdd, worktreeOverride, skipEnqueue, emptyOwnDiff,
extraSettings). Added a top-level beforeEach to clear shared mock/registry
state that several tests forgot to reset.

No behavioral change. All 21 tests pass on two consecutive runs in ~20s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:44:23 -07:00
gsxdsm
bdfe620fab fix(FN-5456): tighten branch-autocorrect verify and add real-git regression
Follow-up to a3ec2e55c addressing code-review feedback:

- Restrict the existence check to local heads via
  `git show-ref --verify --quiet refs/heads/<name>` so a stray tag or
  remote ref with the same name cannot satisfy the check and lead the
  subsequent `git checkout` to a detached HEAD on the wrong object.
- Use `git branch -M` (force) instead of `-m` for the rename path so
  case-only renames succeed on case-insensitive filesystems (macOS,
  default Windows).
- Document the FN-5456 invariant on the function's doc comment.
- Add a real-git regression suite covering the three paths the fix
  governs: no creation when expected ref is missing, switch-only when
  it exists, and rejection of same-named tags.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:40:40 -07:00
gsxdsm
a3ec2e55c8 fix(FN-5456): never create task branches from arbitrary HEAD in autocorrect
`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>
2026-05-22 21:36:02 -07:00
gsxdsm
d02cd38d7b fix(merger): short-circuit out-of-scope fix loop to prevent limbo recovery cycle
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>
2026-05-22 21:32:10 -07:00
gsxdsm
0363876109 feat(merger): scope pnpm verification to changed packages in monorepo
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>
2026-05-22 21:31:59 -07:00
gsxdsm
4a99e3fba2 feat(FN-5455): merge fusion/fn-5455 2026-05-22 21:08:00 -07:00
gsxdsm
ba9d6326e4 fix(dashboard): floor mobile nav inset so Android Chrome gesture bar can't overlap
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>
2026-05-22 20:48:09 -07:00
gsxdsm
e291d86444 feat: attribute Fusion as Co-authored-by trailer instead of primary author
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>
2026-05-22 20:38:11 -07:00
gsxdsm
f58fb8955a fix(engine-tests): eliminate temp-dir leak and raise subprocess guard for concurrent workspace runs
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>
2026-05-22 20:20:42 -07:00
gsxdsm
a96e2ab02b feat(FN-5449): merge fusion/fn-5449 2026-05-22 19:22:10 -07:00
gsxdsm
ec6643e4d9 fix(test-utils): cancel subprocess tracking timer for every proc in afterEach
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>
2026-05-22 18:38:51 -07:00
gsxdsm
d37bebf8bf feat(FN-5516): merge fusion/fn-5516 2026-05-22 17:24:38 -07:00
gsxdsm
45a3c6e426 fix(dashboard): unsqueeze kanban columns on tablet viewports
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>
2026-05-22 17:22:36 -07:00
gsxdsm
31c71a326a feat(FN-5513): merge fusion/fn-5513 2026-05-22 16:10:13 -07:00
gsxdsm
3a6a4ece6a feat(FN-5432): merge fusion/fn-5432 2026-05-22 16:09:43 -07:00
gsxdsm
3fbebd44e9 fix(dashboard): don't preventDefault on chat composer touchstart on Android
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>
2026-05-22 16:00:02 -07:00
gsxdsm
a7e55e3d7a fix(merger): harden reuse-handoff autostash against shell injection and edit loss
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>
2026-05-22 14:44:21 -07:00
gsxdsm
24686cadec fix(merger): autostash dirty reuse worktrees and fail loudly on autostash errors
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>
2026-05-22 14:38:29 -07:00
gsxdsm
e6c135bca1 fix(dashboard): block pinch-zoom and overscroll on mobile globally
- 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>
2026-05-22 14:21:27 -07:00
gsxdsm
a10fc56ee2 fix(dashboard): keep Android keyboard open in main chat; disable kanban pinch-zoom
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>
2026-05-22 14:07:15 -07:00
gsxdsm
60a0012697 fix(dashboard): gate mobile scroll-lock to iOS so Android keyboard stays open
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>
2026-05-22 14:01:09 -07:00
gsxdsm
e138289a80 fix(dashboard): compensate Android Chrome inflated ICB for fixed-position UI
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>
2026-05-22 13:55:20 -07:00
gsxdsm
ed4d021d3d fix(dashboard): keep mobile nav visible on Android landscape and during keyboard
- 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>
2026-05-22 13:12:36 -07:00
Fusion (runfusion.ai)
0373776785 feat(FN-5498): add regression tests for self-healing fast-path behavior
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
2026-05-22 11:32:17 -07:00
gsxdsm
0443718346 revert(dashboard): roll back speculative Android tablet fixes
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>
2026-05-22 11:23:03 -07:00
gsxdsm
49839245e1 fix(dashboard): portal MobileNavBar to document.body for robust fixed positioning
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>
2026-05-22 11:12:32 -07:00
gsxdsm
4bd9711e87 fix(dashboard): keep MobileNavBar visible when keyboardOpen stuck true
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>
2026-05-22 11:02:14 -07:00
gsxdsm
d2e31dc107 fix(dashboard): don't fight user pinch-zoom in viewport override
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>
2026-05-22 10:59:37 -07:00
Fusion (runfusion.ai)
c3890a9b43 feat(FN-5496): reconcile soft-deleted tasks with active blockers
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
2026-05-22 10:58:44 -07:00
gsxdsm
ba066c88a3 feat(FN-5488): merge fusion/fn-5488 2026-05-22 10:37:21 -07:00
gsxdsm
301e0a050f chore(dashboard): make vpdebug overlay self-healing
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>
2026-05-22 10:33:06 -07:00
gsxdsm
16b18bf338 chore(dashboard): publish MobileNavBar render decision to vpdebug
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>
2026-05-22 09:37:23 -07:00
gsxdsm
d3dff94424 chore(dashboard): extend vpdebug overlay with MobileNavBar diagnostics
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>
2026-05-22 09:28:17 -07:00
gsxdsm
a9094d79dd test(dashboard): restore FN-5476 mobile nav android overlap test
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>
2026-05-22 09:25:52 -07:00
Fusion (runfusion.ai)
324cdeb12b feat(FN-5448): add completed-task oscillation backstop and guard
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
2026-05-22 08:57:40 -07:00
Fusion (runfusion.ai)
1bffa22ca9 feat(FN-5479): fix identity-guard merger bypass on detached HEAD and gate l
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
2026-05-22 08:31:12 -07:00
Fusion (runfusion.ai)
4b484fd818 feat(FN-5485): clear userPaused state on manual retry reset with regression
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
2026-05-22 08:23:45 -07:00