Self-review of the recovery cascade surfaced three issues; this commit
addresses all of them.
1. AI didn't actually receive the safety constraint under Layer 3.
The previous commit logged the safety preamble to the task log via
`store.logEntry`, but the merge agent doesn't read task log entries as
prompt context — so the AI was running blind. The "no silent
re-introduction of main's deletions" guarantee was therefore relying
*entirely* on the deterministic verification gate (test + build),
which is correct as a backstop but doesn't help the AI produce a
correct first attempt.
Fixed by threading `preMergeRebaseFallthrough` through
`MergeAttemptParams` → `executeMergeAttempt` → `runAiAgentForCommit` →
`MergePromptParams` → `buildMergePrompt`, where it now injects an
explicit "⚠️ Pre-merge rebase recovery exhausted" preamble at the top
of the user prompt with three concrete rules:
- Prefer main's deletion when branch re-adds removed lines
- Prefer main's version on ambiguous hunks
- Call `fn_report_build_failure` rather than commit a regression
Also includes the original rebase failure message (truncated) so the
AI has diagnostic context.
The truncated-context retry path also forwards the preamble — it's the
safety constraint, not bulk context, so we keep it even when stripping
diff stat / commit log to fit the window.
2. Layer 2's branch-restore could fail with "uncommitted changes".
When a cherry-pick midway through Layer 2's replay fails, the worktree
is in a half-applied state with conflicts in the index. The previous
restore did `git checkout <branch>` (no -f) followed by
`git reset --hard <originalSha>`. The plain checkout would refuse with
"would overwrite local changes" if there were unmerged paths,
preventing the reset from running and leaving the branch at the
half-replayed tip.
Fixed by reordering: hard-reset to the captured original SHA first
(this clears index/working tree of any cherry-pick state), then
`git checkout -f <branch>` to ensure HEAD points at the named branch,
then a final hard-reset to the original SHA as belt-and-suspenders.
Worst case the worktree is at the original branch tip — never worse
than where Layer 2 started.
3. Pre-existing unrelated lint error blocking workspace lint.
`packages/dashboard/src/server.ts` had an unused `resolve` import from
`node:path` left behind by a recent refactor that extracted
`PACKAGE_VERSION` into its own file. The user explicitly asked to
clean it up so workspace lint passes. One-line drop.
Tests + checks:
- Engine: 2886 / 2886 pass (added safety preamble didn't break any
existing prompt-content assertions)
- Core: 3120 / 3120 pass
- Workspace lint: clean
- Engine typecheck: clean
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>
- Default Cloudflare remote access to Quick Tunnel mode in settings documentation
- Replace the Quick Tunnel checkbox with an Advanced (Named Tunnel) details disclosure in SettingsModal
- Auto-sync quick tunnel state from the details open/close state while preserving named tunnel field editing
- Update SettingsModal tests to cover the new advanced-toggle behavior and persisted quick tunnel payload
Fusion-Task-Id: FN-2916
Tasks were getting stuck in `in-review` forever when auto-merge could not
resolve conflicts within MAX_AUTO_MERGE_RETRIES. The conflict-exhaustion
branch silently cleared `status` (no error, no log entry, no comment),
and the 30-min cooldown sweep would reset retries and re-attempt the
same impossible merge — looping silently with no user-facing surface.
Why:
- FN-2918 and FN-2903 both spent hours in this loop with no error/comment
visible on the task. The only log evidence was repeated
"Auto-merge retry cooldown elapsed (30m idle)" entries with no
follow-up outcome.
How to apply:
- Every merge failure now writes a `<Manual|Auto>-merge failed: <msg>`
entry to the task log so the dashboard surfaces the reason.
- Conflict-retry exhaustion now bounces the task back to `in-progress`
with a comment + log entry so the executor re-rebases against main
and retries — mirroring the verification-failure-bounce pattern.
- New `mergeConflictBounceCount` task field caps outer bounces
(`MAX_MERGE_CONFLICT_BOUNCES = 2`); past the cap, the task is parked
in `in-review` with `status="failed"` and a follow-up triage task is
created so a human can resolve the conflict manually.
- Non-conflict and non-direct-strategy errors now also set
`status="failed"` so the cooldown sweep can't re-pick them up.
- `canMergeTask` skips tasks with `status="failed"` so terminal
failures (verification cap, bounce cap, non-conflict error) are no
longer eligible for cooldown re-attempts.
Schema migration v52 adds the `mergeConflictBounceCount` column.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add attachment-first chat compose flow with paperclip picker, drag-and-drop, and paste support in ChatView input
- Render pending attachment preview chips with per-file remove actions and drag-over visual affordance
- Display sent message attachments with image/file presentation and accessible focus/interaction styling
- Update ChatView tests and icon mocks to cover attachment-only sends, file selection, and preview behavior
Fusion-Task-Id: FN-2911
- fix(FN-2914): remove divider line between close and shortcuts buttons in terminal
- feat(FN-2902): merge fusion/fn-2902
- chore(release): v0.8.3
- Update changeset
- fix(tui): swap 3/4 panel hotkeys so they match the help text
- fix(tui): always show auth token and report accurate macOS memory usage
- chore(release): v0.8.2
- feat(FN-2895): merge fusion/fn-2895
Fusion-Task-Id: FN-2914
3 now jumps to Stats and 4 to Utilities, matching the documented
"System/Logs/Stats/Utilities/Settings" order. PANEL_ORDER updated so
prev/next cycle navigation follows the same sequence.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pin the Token chip after URL in the System panel so it stays visible
(wrapping to a new row at narrow widths) instead of being pushed off-panel.
Switch system memory readings to os.availableMemory() so macOS no longer
reports ~96% used by ignoring reclaimable inactive/cached pages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fn_task_create and fn_task_update accepted any string as `agentId` and
wrote it verbatim onto `task.assignedAgentId`, letting hallucinated IDs
(e.g. `agent-executor-001`) appear as agent badges in the dashboard.
Mirror the validation already used by fn_delegate: look the agent up via
AgentStore and reject unknown or ephemeral/runtime-managed agents. Null
still clears the field on update.
Also clean up two stale failures in bundle-output.test that predated this
change:
- pi-claude-cli no longer imports cross-spawn, so drop the dependency and
its orphan type-decl file.
- Loosen the spawn-import regex to match `spawn` anywhere in the
destructured import (the source has additional named imports).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lead with Project Name, then Project Directory. Move setup-mode toggle
(existing vs clone) and the repository URL field into Advanced Settings
so the default path is one question — pick a folder, name it, register —
without the welcome blurb and mode picker getting in the way.
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>
- Remove btn-icon styling from ChatView render toggle controls and apply dedicated borderless toggle styling
- Update ChatView and QuickChatFAB behavior so quick chat streaming state is preserved when resuming sessions
- Add regression tests for QuickChatFAB and useQuickChat to cover resume and streaming-state handling
Fusion-Task-Id: FN-2906
- Keep active streaming responses alive when resuming an existing quick chat session instead of resetting stream state
- Update useQuickChat session-resume logic to only restart waiting indicators when appropriate
- Adjust QuickChatFAB waiting indicator behavior so it reflects real in-flight assistant activity
- Add focused hook and component tests covering same-session resume and waiting-indicator regressions
Fusion-Task-Id: FN-2904
- Add missing Claude model entries and extend provider metadata handling for model extras
- Improve subprocess diagnostics in pi-claude-cli process management for clearer failure visibility
- Add targeted tests for provider model extras and process-manager diagnostic behavior
- Update Settings modal copy for project default model guidance and record changes in pi-claude-cli changelog
Fusion-Task-Id: FN-2898
- Strip private @fusion/* workspace devDependencies from the published CLI manifest via prepare-publish-manifest and package metadata updates
- Replace cross-spawn usage and add staged bundle layout assertions to verify resolver output in dist packaging
- Add per-task/project model override resolution across core, dashboard settings/task modals, and route coverage with new regression tests
- Strengthen engine merge/recovery handling for paused/interrupted/squash paths and surface merger timeline activity with additional self-healing and merger tests
- Add changesets for npm bundle dependency fixes, project model override stabilization, and FTS5 corruption recovery
Fusion-Task-Id: FN-2897
Follow-up to 502fddf20: when the merge was empty (no commit made),
filesChanged/insertions/deletions were still being captured from
git show --shortstat HEAD — which describes pre-merge HEAD's commit,
unrelated to this task. Consumers (dashboard, audit log) would render
those numbers next to "no commit landed", which is misleading.
Clear stats to 0 alongside the omitted commitSha. Also drop the stats
line from the agent-log summary when mergeWasEmpty.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Users couldn't see the merger's per-task activity from the dashboard
agent-log view — only the executor's session output was visible. When a
merge took an unexpected path (rebase ran twice, attempt 2 auto-resolved
3 lockfiles, attempt 3 fell back, etc.) the only record was in process
logs, which most users don't have access to.
Add appendAgentLog calls at the high-signal merge events:
- Pre-merge rebase: when each stage (remote → remoteRef, local-base
→ local HEAD) completes successfully
- Each merge attempt start, with attempt number + strategy summary
- Final merge outcome: strategy, attempt count, commit sha, file stats,
and edge cases (empty merge, deferred sha)
Source attribution uses "merger" so the dashboard can style/filter
these distinct from the executor's per-step messages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
recoverInterruptedMergingTasks searched for landed commits by grepping
commit subjects for the task ID. Users with includeTaskIdInCommit=false
have commit subjects like `feat: ...` (no task ID), so if the merger
crashed after committing but before storing mergeDetails, recovery would
silently fail to find the commit and incorrectly retry the merge.
Three layered defenses:
1. Emit a Fusion-Task-Id: <id> trailer in every Fusion-managed merge
commit body. The 4 fallback commit invocations now include
`-m "Fusion-Task-Id: ..."`. After the AI agent commits, an
idempotent ensureTaskIdTrailerOnHead() amends the trailer in via
`git interpret-trailers` (no-op if already present).
2. findLandedTaskCommit now tries three sources in order:
a. task.mergeDetails.commitSha (if reachable from HEAD)
b. Fusion-Task-Id trailer grep (anchored regex)
c. Subject grep (legacy commits)
3. Trailer grep uses an anchored regex `^Fusion-Task-Id: <id>$` so
it doesn't false-match task IDs appearing as substrings.
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>
Audit of task.status assignments across packages/core and packages/engine
surfaced five additional transient/in-flight statuses that should block
auto-merge but weren't in BLOCKING_TASK_STATUSES:
- awaiting-approval — triage spec awaiting user approval
- needs-replan — scheduler/executor/triage signaled re-plan needed
- mission-validation — mission-level validation in flight
- queued — scheduler-side transient state
- stuck-killed — defensive guard; task killed by stuck detector
Each represents a state where finalizing the merge violates user intent
or invariants. Group the set with comments explaining the intent of each
group so future additions can be slotted appropriately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>