Added soft-delete reliability sweeps and guardrails to prevent blocker residue from persisting across delete operations, including column drift detection, deleted row sweep guards, and in-progress delete reconciliation, with comprehensive test coverage and documentation updates to the soft-delete ve
Fusion-Task-Id: FN-5566
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5566
Three coupled fixes to make `pnpm test:full` exit cleanly when the local
`fn` dashboard is running:
1. scripts/check-test-isolation.mjs — replace timing-based "is the
engine writing?" heuristic with a deterministic check: if
`.fusion/engine.lock.lock/` exists (proper-lockfile's held-lock
marker), the dir is engine-active and auto-skipped from violation
reporting. The 2-second mutability probe is retained as a backstop
for dirs with another external writer but no live lock. Also adds
`engine.lock` / `engine.lock.lock/` to RUNTIME_IGNORE_PATTERNS so
a mid-test engine start/stop doesn't trip the signature compare.
2. packages/dashboard/.../__tests__/GitManagerModal.test.tsx — prune
the Status-panel Sync button + Recent-advances-events describe
blocks. Their UI was removed in 5d35b64bd ("remove duplicate
integration-advances UI") but the tests stayed and were timing
out at 1s each. The Remotes-panel Sync describe is kept because
the `remotes-sync-integration-tip-btn` still exists.
3. packages/engine/.../merge-reuse-task-worktree.slow.test.ts —
update the happy-path assertion to reflect 4c31e885b
("merger auto-syncs project-root checkout after ref advance").
Before that change, the merger's `update-ref` advance left the
project root's working tree stale, so `git status --porcelain`
would differ after the merge. With auto-sync, the new file is
tracked + clean at HEAD, so status doesn't change. Verify the
file actually landed via `git ls-files` instead.
After this, `pnpm test:full` exits 0 with the local dashboard running.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the squash commit was built off a stale integration tip, the FF guard
in advanceIntegrationBranchRef refused the swap with reason
`non-fast-forward-advance` — but the caller only mapped `concurrent-advance`
to IntegrationBranchConcurrentAdvanceError, so the non-FF case fell through
as a plain Error and failed the task. Both reasons share a root cause
(integration moved during the merge window), so they now share the
FN-4500/FN-5083 rebind/retry path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a static pretest check and a runtime vitest-setup wrapper that block
shell/process calls matching `kill|pkill|killall|fuser|lsof ... <port>` or
`.listen(<port>)` against reserved Fusion ports. Reserved set is dynamic:
default 4040 plus $PORT, $FUSION_SERVER_PORT, $FUSION_RESERVED_PORTS, and any
port responding to /api/health on 4040..4045 at worker startup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop a fixture-existence test that read PROMPT.md from an absolute
/Users/eclipxe path (CI would fail), and generalize remaining hardcoded
home-directory paths in self-healing and worktree-stale-registration
fixture strings to neutral /tmp/test-project and /repo paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`notice` is `events.find(...)` which returns `undefined` (not `null`)
when no match. `waitFor(() => expect(...).not.toBeNull())` exited
immediately because `undefined !== null` — the test never actually
waited for the api mock to resolve. Sometimes the followup assertions
happened to land after the events fetched (test passed by luck);
sometimes they ran while notice was still undefined and the assertions
failed.
Switched all five waitFor sites to `.toBeDefined()` so they actually
block on the events-fetch resolution.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Third root-cause fix in the FN-5475 sweep. When `aiMergeTask` /
`recoverNoOpReviewTasks` classified a task as `proven-no-op` or
`no-changes-finalized`, both call sites moved the task to Done while
clearing `modifiedFiles: []` — silently destroying the audit trail when the
work product was uncommitted in the worktree, squashed against the wrong
branch, or dropped by reuse-handoff churn. This was the load-bearing site
of the FN-5490 / FN-5517 / FN-5526 / FN-5540 lost-work patterns.
Both call sites now check `task.modifiedFiles.length` before finalizing as
no-op. If the task claims work was done but no commit landed, the task is
moved back to `todo` with progress preserved and a new
`task:finalize-lost-work-blocked` audit event is emitted. The next
executor run re-attempts the work; the operator sees the audit event in
the timeline.
The post-hoc `reconcileDoneTaskIntegrity` path is intentionally NOT gated
— it cleans up already-Done tasks (legacy state) and is out-of-scope for
prevention. 9 lost-work tasks already in this state at sweep time are
cataloged in docs/incidents/2026-05-23-lost-work-tasks.md for fresh
re-spec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two root-cause fixes for the "fake done" patterns surfaced while debugging
FN-5475's stuck preflight (it depended on FN-5233, which the board reported
as Done but whose squash had stranded on a sibling fusion/fn-* branch).
1. resolveTaskMergeTarget rejects fusion/fn-* sibling branches as a merge
destination — when a task's baseBranch was inherited from a sibling/dependent
dispatch, the merger detached onto and squashed against that branch instead
of advancing main. New audit event surfaces the steering miss so the
underlying baseBranch-propagation bug stays observable.
2. self-healing findLandedTaskCommit verifies ownership against each grep
candidate's body before attribution. The previous code blindly accepted the
first hit of `git log --grep=FN-XXXX` (which matches the entire commit
message); FN-5441 and FN-5446 were both marked done against an unrelated
FN-5483 commit whose body merely mentioned them in prose. commitOwnedByTask
is also tightened: trailers must be line-anchored and the subject fallback
must match conventional-commit form, not a bare substring.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the FN-5233 tombstone system for soft-delete resurrection: a configurable `tombstoneWindowSeconds` deduplicates recreation of recently deleted tasks, with an `allowResurrection` flag that permits explicit resurrect-on-recreate, tombstone recreate guards in the store layer, and cleanup of
Fusion-Task-Id: FN-5233
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>
Adds comprehensive test coverage for the run-audit system across the engine package, including lane session audit tests (triage, executor, reviewer, merger, heartbeat) and runtime audit invariants, plus a backcompat test for no-auditor scenarios.
Fusion-Task-Id: FN-5556
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5556
Implements a pull-based merge workflow by wiring the merger pull helpers from the engine, extending the git pull and stash routes, and aligning the `MergeAdvanceNotice` and `StashConflictModal` components to gate dismissal on stash drop. The `run-audit` module is updated with pull mutation documenta
Fusion-Task-Id: FN-5419
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5419
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>
The file previously did `mkdtemp` + `git init` + initial commit in each
test's beforeEach, paying ~5 git invocations per test. Move the repo
setup to beforeAll and add a `resetRepoToInitial` helper that uses
`git reset --hard` + branch cleanup + `git clean -fdx` between tests.
Safe because the file runs in the single-threaded engine-slow vitest
project.
Wall time: 17.1s → 10.3s (40% faster), 12 tests, all still passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`pnpm test` was dominated by a handful of merger and reliability-
interaction files that each spawn `mkdtemp` + `git init` + multiple
commits per test. Renaming them to `*.slow.test.ts` and routing them
to a new `engine-slow` vitest project moves them out of the default
local run.
Local `pnpm test` drops from 198s to 84s (~57% faster).
- `pnpm test` — engine-default + engine-reliability lanes only
- `pnpm test:slow` — engine-slow lane (4 files, 63 tests, ~37s)
- `pnpm test:all` — everything (for CI / verify:workspace)
Files moved:
- reliability-interactions/merge-reuse-task-worktree.test.ts (was 20.6s)
- merger-overlap-guard.test.ts (was 17.1s)
- merger-staging-allowlist.test.ts (was 11.8s)
- merger-diff-volume-gate.test.ts (was 8.4s)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a push-to-origin workflow to the merge notice system, introducing a new `useMergeAdvanceNotice` hook, a `merge-advance-push-origin` route handler, and corresponding UI affordance in the `MergeAdvanceNotice` banner component. The engine gains TOCTOU and refusal audit assertions, and coverage exp
Fusion-Task-Id: FN-5359
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5359
Adds a regression test for retry-exhausted in-review policy convergence behavior in the engine, exports `MAX_AUTO_MERGE_RETRIES` for test reuse, and includes a small fix to restore workspace build and test green in `merger.ts` and `self-healing.ts`.
Fusion-Task-Id: FN-5536
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5536
Adds `deletedAt` sweep guards to the engine's self-healing and merger to prevent recovery operations from processing soft-deleted tasks, filters deleted tasks in the `recover-stale-blocked-by` script, includes a new regression test for the deadlock-scan exclusion pattern, and updates the soft-delete
Fusion-Task-Id: FN-5528
Fusion-Task-Lineage: 5c9e45ca-49a8-47a0-a23d-6fe8e15e7e00
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5528
Drops the "workflow-step" MockSessionPurpose enum value and the
workflowStepId / workflowStepTemplateId plumbing through
agent-runtime, agent-session-helpers, mock-provider, executor, and
merger. The seeded-workflow-prompts script loses its FN-5205
rationale comment + test (no longer applicable now that workflow
steps run through the regular session purposes).
Also strips the stale FN-5482 architecture-invariant bullet from
AGENTS.md and the corresponding audit-event line from
docs/architecture.md (the self-healing reclaim invariant they
described no longer holds).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes the BROAD_SCOPE_FLAG_VERSION/decideBroadScopeFlag pipeline,
the triage.ts call site that wrote `broadScopeFlag` source metadata
and emitted task:broad-scope-flagged-at-triage audit events, the
DatabaseMutationType enum entry, the diagnostic doc section, and the
associated unit + reliability-interaction tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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 resume-path verifier in acquireTaskWorktree now also calls
classifyBootstrapMisbinding / reanchorBranchToBase before the
executor's primary contamination check runs. With a once-spy the mock
was depleted by the verifier, leaving the executor path with the real
implementation and the FN-4488 shape regression test no longer
exercised its expected recovery branch.
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>
Test coverage for mobile views: `MobileNavBar.test.tsx` now syncs its mock with the viewport constant, and `SecretsView.mobile.test.tsx` derives secrets media parsing from the hook query.
Fusion-Task-Id: FN-5521
Fusion-Task-Lineage: eb37d760-ab67-49c5-9b57-2c14d648a98a
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5521
The SIGTERM/SIGINT/SIGHUP handlers added to db.test.ts and the engine
tmp-dir tests re-raised signals after cleanup, which killed vitest itself
(exit 143) under the full engine reliability suite. Keep `afterAll` +
`beforeExit`/`exit` + lock-child kill — those cover the macOS file-handle
leak that was the actual driver of the merge-verification cascade.
Also skip project-engine-manager `retries failed project starts on
subsequent reconciliation ticks` — flake under full-suite load (30s
timeout) that passes in ~46ms standalone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
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>
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).
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>
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>
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>
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>
`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>
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>