Merged branch removes the duplicate desktop "Todos" nav entry from the Header, syncs the `allowParallelExecution` runtime toggle into `AgentDetailView`, and updates the corresponding docs (agents.md, settings-reference.md, todo-view.md, dashboard-guide.md) to reflect the navigation change. Tests wer
Fusion-Task-Id: FN-3539
The merge completes FN-3498 across three steps: adds ownership-aware done-task reconciliation to the merger, prevents branch-missing head SHA pollution during merge operations, and restores workspace typecheck compatibility. Core changes touch the merger (103 lines) and self-healing module (67 lines
Fusion-Task-Id: FN-3498
This merge adds three major features: an eval domain (`eval-store.ts`, `eval-types.ts`) with persistence schema for evaluation data; a plugin dashboard view registry with navigation integration for third-party dashboard extensions; and GitHub source metadata traceability that locks and enforces issu
Fusion-Task-Id: FN-3513
Replaces blanket `git add -A` in `commitOrAmendMergeWithFixes` with an
explicit allowlist of (squash-staged ∪ fix-agent-modified) paths, so
unrelated dirty files in the project root no longer get swept into a
task's squash commit. The in-merge fix agent now snapshots the working
tree before/after its session to capture exactly which files it touched.
Hardens the git invocations the allowlist relies on:
- All `git add` and `git checkout --ours/--theirs` calls switched from
shell-interpolated `execAsync` to `execFile` array form, eliminating
path-injection surface and batching per-file spawns into one call.
- `snapshotDirtyFiles` adopts `git -z` NUL-delimited parsing so paths
with embedded spaces or specials are handled correctly.
- Long allowlist debug logs are truncated to 20 entries with an overflow
marker.
Refused-to-stage paths emit a warn naming each file so the user can
audit what was filtered.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three merger fallback commit paths (auto-resolve-all-conflicts,
-X theirs/ours side strategy, AI-agent-didn't-commit) hard-coded
`feat(FN-XXXX): merge fusion/fn-xxxx` as the subject and never used
the AI subject summarizer. Route them through buildDeterministicMergeMessage
so they pick up aiSubject when available.
When the AI subject summarizer returns null, derive the subject from
the branch's first step commit (with conventional-commit prefix
stripped, plus `(+N more)` for multi-commit branches) instead of the
bare `merge <branch>` template.
Bump DEFAULT_COMMIT_SUBJECT_TIMEOUT_MS 15s → 30s so slow-first-token
providers complete instead of silently falling back.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a layered recovery cascade to the merger's pre-rebase stage so tasks
no longer get stuck in in-review when their declared dependency was
squash-merged to main and left orphan raw commits in the dependent's
history. Also prevents the orphan situation at the source for new tasks.
Why:
- 13 tasks were stuck in in-review for hours, all hitting the same
pre-merge rebase abort because they shared 6 raw commits inherited
from FN-2729's branch (declared baseBranch). FN-2729 was then
squash-merged to main, turning those raw commits into orphans whose
content is in main but in a different commit shape, conflicting with
later-merged tasks. The merger's `smart-prefer-main` strategy
correctly refused -X ours (which would silently re-introduce main's
deletions), but the only escape hatch was a 30-min cooldown loop
that retried the same impossible rebase forever.
Recovery cascade (merger.ts pre-rebase stage):
- Layer 1: surgical `git rebase --onto <main> <dep-tip> <branch>` when
task.baseBranch is set. Resolves the dep tip from the live branch ref
or recorded baseCommitSha; peels off the dep's inherited commits
cleanly. Captures the squash-merge-of-dep case end-to-end.
- Layer 2: generic patch-id duplicate-content stripping. Walks the last
500 main commits, computes patch-ids, then drops branch commits whose
patch-id matches and cherry-picks the remainder onto main. Captures
manual cherry-picks, double-merges, and any other duplicate-content
variant Layer 1 doesn't see. Restores the branch's pre-mutation SHA
on partial-failure so worst case leaves the worktree no worse than
before the recovery attempt.
- Layer 3: AI arbitration fall-through. If Layers 1+2 fail, log the
situation and proceed to the existing 3-attempt AI merge cascade
instead of throwing. The deterministic post-merge verification
(test + build) gates whatever the AI produces — that gate is what
enforces prefer-main's safety contract under fall-through (no silent
re-introduction of main's deletions).
- Critical: the unsafe `-X ours` Attempt 3 is suppressed under
fall-through. AI Attempts 1+2 are the only paths that can complete
the merge; if both fail and verification rejects them, the task
bounces back to in-progress via the existing engine path rather than
silently merging.
Prevention (executor.ts worktree creation):
- When a task declares a non-main `baseBranch`, branch the worktree
off main (origin/<defaultBranch> when worktreeRebaseBeforeMerge is
enabled and a remote is resolvable; otherwise local rootDir HEAD)
and `git merge --squash` the dep's content as a single import commit.
The dependent branch then carries main's history + 1 commit instead
of inheriting the dep's raw commits, so a future squash-merge of the
dep produces patch-id-matching content that rebases cleanly.
- Honors settings: respects `worktreeRebaseBeforeMerge`,
`worktreeRebaseRemote`, and falls back to local HEAD when no remote
is resolvable. Fully fail-soft: any squash-import error falls back to
the legacy fork-from-dep behavior so worktree creation still works
for setups where the squash flow can't run.
Engine-side last-retry fix (project-engine.ts):
- Changed conflict-retry condition from `currentRetries < MAX` to
`currentRetries + 1 < MAX` so the bounce-to-in-progress code fires
in the same engine tick as the failing attempt, rather than relying
on a setTimeout-scheduled Nth attempt that dies on engine restart.
Without this, a dev-time engine restart between the 3rd and 4th
retry left the task with mergeRetries=MAX and only the 30-min
cooldown sweep could try again.
Tests:
- New "Layer 1 recovery" test asserts the surgical --onto rebase fires
when baseBranch is set and primary rebase aborts, and that Layer 3
fall-through is NOT triggered when Layer 1 succeeds.
- Updated the "no silent fall-through to -X ours" test to cover the
new fall-through path: even after Layers 1+2 fail and the merge
cascade proceeds, -X ours must not run, and the task log must record
both the Layer 3 fall-through entry and the Attempt 3 suppression.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the AI merge agent reported a build failure via fn_report_build_failure,
executeMergeAttempt immediately ran `git reset --merge` and threw. The catch
handler then spawned an in-merge fix agent on a clean main and called
amendMergeCommitWithFixes, which blindly amended HEAD — the *previous* task's
merge commit — silently dropping the current task's branch and inheriting
the prior task's stats. The dashboard then reported the new task as merged
with completely unrelated files.
- Drop the immediate reset at the build-failure throw site so the squash
state survives for the in-merge fix path.
- Capture preAttemptHeadSha at each mergeAttempt and refuse to amend when
HEAD never moved past it; instead, create a fresh commit from the squash
+ fix changes. If neither HEAD moved nor anything is staged, abort the
merge instead of fabricating success.
- Move the cleanup reset into the mergeAttempt catch handler (with a
labeled resetMergeWithWarn helper) so it still fires when the fix path
is exhausted or disabled.
- Replace the AI-authored commit body with a deterministic body built from
the branch's actual step-commit subjects after every successful AI merge.
Stops the recurring problem of merge messages describing files that are
not in the diff.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two cases where mergeDetails.commitSha was wrong:
1. Empty-squash success paths (mergeAttempt + attemptWithSideStrategy
return true when nothing was staged) recorded pre-merge HEAD as the
task's commitSha. That commit had nothing to do with this task —
misleading the dashboard, audit log, and recovery scans.
2. pushAfterMerge can trigger an internal pull --rebase that rewrites
HEAD; mergeDetails was captured before push, so the stored sha
referenced a now-orphaned commit.
Fix:
- Track an empty-merge flag on AiInvocationTracker, set at the three
squashIsEmpty/staged===0 sites. Metadata block omits commitSha when
the flag is set.
- After successful pushAfterMerge, recapture HEAD and update
mergeDetails.commitSha if it changed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pre-merge rebase had the local-base rebase nested inside the
remote-rebase success path, so when no remote resolved (or worktreePath
was missing), the entire block exited without running local-base. That
left smart-prefer-main exposed: with no rebase, the -X ours fallback
would silently re-introduce code main had recently deleted, which is
exactly the case the strategy is meant to prevent.
Restructure so remote rebase (Stage 1) and local-base rebase (Stage 2)
run as independent gates. Local-base rebase still picks up sibling-task
merges that landed locally even when the remote stage was skipped or
disabled, so prefer-main always gets at least one defense.
Also relax the semantic-incompatibility guard: prefer-main now requires
EITHER stage to remain enabled (was: required worktreeRebaseBeforeMerge).
Extracted runLocalBaseRebase() helper to remove duplication between the
two entry points (after Stage 1 vs. standalone) and centralize the
ancestor-check + abort handling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Attempt 2 of the merge cascade caught any git merge --squash failure into
mergeExitedWithConflicts=true. If the failure was non-conflict (pre-commit
hook rejection, IO error, locked repo) and produced no U files, the code
fell into the "all conflicts auto-resolved" branch with empty classified
arrays, ran deterministic verification on pre-merge HEAD, and returned
true — recording merge metadata for a merge that never happened.
Distinguish "exit code 1 with U files" (recoverable) from "any other
failure" (real). When a real failure surfaces with no conflicts, raise a
sentinel MergeNonConflictError that the outer mergeAttempt catch propagates
without retrying — retrying just re-runs the same broken command.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The smart-prefer-main strategy depends on a successful pre-merge rebase
to honor main's deletions. Previously, three failure modes silently fell
through to the -X ours merge, which would re-introduce code main had
just removed (because branch additions vs main deletions don't textually
conflict and -X ours only resolves content conflicts, not modify/delete).
- Hard-fail when prefer-main is paired with worktreeRebaseBeforeMerge=false
(semantically incoherent combination)
- Hard-fail when the pre-merge rebase starts and aborts (any of the three
rebase paths: remote, nested local-base, or fallback local-only)
- Warn (not throw) on environmental silent skips — no remote resolvable
or no worktreePath — so the gap is observable in logs without breaking
common test/setup environments
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The single "smart" strategy is now two flavors with the new default flipped
to prefer-main. Both share a pre-cascade `git fetch origin <currentBranch>`
+ best-effort fast-forward so a freshly-pushed sibling commit doesn't get
clobbered when the fallback resolves a conflict against a stale base.
- "smart-prefer-main" (new default): -X ours fallback. Protects just-merged
sibling work from being regressed by a concurrent task branch.
- "smart-prefer-branch": -X theirs fallback. Equivalent to legacy "smart".
Legacy "smart" / "prefer-main" enum values are accepted and normalized via
`normalizeMergeConflictStrategy()` so existing settings.json files migrate
seamlessly. The fast-forward step gracefully degrades on fetch failure or
divergent local main (logs and continues).
Updates settings UI dropdown, test helpers, and adds 5 fetch+ff regression
tests + 7 normalize-helper tests. Lint cleanup of two empty catch blocks
in scripts/release.mjs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the worktree-recycle pool reassigned a path to a new task, the old
task's diff endpoints kept reading the new task's branch state — surfacing
unrelated commits as the original task's "files changed" list.
- Clear task.worktree/branch in the merger after the worktree is released
to the pool or removed, so the path no longer points anywhere.
- Validate the worktree's current branch matches task.branch in the three
worktree-backed diff endpoints; on mismatch return empty rather than
diffing against a foreign branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Fix triage planning model resolution to fall back through project/global planning settings and default overrides
- Fix reviewer model selection to honor validator-specific settings before default provider/model overrides
- Update merger model resolution to apply default override fallback and align shared task setting types/executor flow
- Add regression coverage for triage, reviewer, and merger fallback behavior and update settings hierarchy documentation
The 5-minute exec timeout only killed the immediate shell, leaving
vitest/pnpm worker trees alive. Across retries these accumulated and
thrashed the host, starving the engine and TUI. Switch verification to
spawn-based runner with detached process group so timeouts SIGTERM the
whole tree (SIGKILL after 5s grace), and bump the wallclock to 10m for
larger workspaces. Stream-truncate output instead of relying on ENOBUFS.
Also fix two flaky/race-prone dashboard tests that were red on main and
blocking every in-review task at merge verification.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add merger abort primitives and track active merge runs for coordinated cancellation
- Abort in-flight merges during engine shutdown and propagate AbortError through fallback catch paths
- Honor abort signals before commit, push, and dependency sync to prevent post-cancel side effects
- Expand merger and project-engine tests to cover abort propagation and merge-abort-on-stop behavior
- Move all co-located *.test.* files into sibling __tests__/ directories so the
layout is consistent across packages (159 renames + content-rewrite moves).
Updates relative imports, vi.mock specifiers, and __dirname/import.meta.url
path resolutions where tests read fixtures from disk.
- Drop tracked tsc-emit alongside engine .ts sources (auth-storage/logger/
skill-resolver/context-limit-detector/pi.{js,d.ts,*.map}). These were
accidentally committed in a merge and the stale pi.js was masking a real
test-mock vs source mismatch (tests imported "../pi.js" and vite preferred
the stale build over pi.ts).
- Add packages/engine/.gitignore to block future src/*.{js,d.ts,map}.
- Refactor plugin pi-module seams (openclaw/paperclip/hermes) to ESM-import
createFnAgent / promptWithFallback / describeModel from @fusion/engine
instead of require()-ing packages/engine/src/pi.js. Adds @fusion/engine to
the two plugin package.jsons that were missing it; exports describeModel
from the engine public API.
- Fix engine test mocks now that they run against current pi.ts: add
ModelRegistry.create static to mocks in pi.test.ts and pi-create-fn-agent
.test.ts; switch three boundary-result toEqual assertions to toMatchObject
so the new content/isError fields don't trip exact-match comparison.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>