Skip to content

fix: classify harmless merge worktree cleanup failures - #1663

Merged
gsxdsm merged 7 commits into
Runfusion:mainfrom
plarson:fix/fn-343-merge-worktree-cleanup
Jun 13, 2026
Merged

fix: classify harmless merge worktree cleanup failures#1663
gsxdsm merged 7 commits into
Runfusion:mainfrom
plarson:fix/fn-343-merge-worktree-cleanup

Conversation

@plarson

@plarson plarson commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • classify Fusion-created merge/post-merge temp worktree removal failures as harmless only when git worktree prune plus git worktree list --porcelain proves the path is no longer registered
  • keep still-registered leaked worktrees visible as cleanup failures/audit events
  • document the diagnostic distinction and add a changeset for operator-visible cleanup logging
  • include the existing dashboard workflow-node type mapping fix required for CI typecheck on current main (merge-gate, merge-attempt, etc.)

Test Plan

  • corepack pnpm --filter @fusion/engine exec vitest run src/__tests__/worktree-backend.test.ts src/__tests__/reliability-interactions/worktrunk-worktree-removal.test.ts --silent=passed-only --reporter=dot — 2 files / 60 tests passed
  • corepack pnpm --filter @fusion/engine typecheck — passed
  • corepack pnpm --filter @fusion/engine build — passed
  • corepack pnpm lint -- packages/engine/src/worktree-backend.ts packages/engine/src/merger.ts packages/engine/src/run-audit.ts packages/engine/src/__tests__/worktree-backend.test.ts packages/engine/src/__tests__/reliability-interactions/worktrunk-worktree-removal.test.ts — passed with existing test-file ignored warnings
  • corepack pnpm typecheck — passed after adding the workflow-node type mapping fix
  • corepack pnpm build — passed

CI

  • GitHub Actions Build, Gate, Lint, Typecheck: passing on the latest push.

Summary by CodeRabbit

  • Documentation

    • Added diagnostics guidance describing how merge cleanup can classify certain temporary worktree removal failures as harmless after Git verification.
  • Bug Fixes

    • Merge cleanup now downgrades specific transient removal failures to harmless, logs a warning, and only marks worktrees removed when confirmed.
    • New audit events track harmless classifications, probe failures, and leaked registered worktrees.
  • Tests

    • Expanded tests covering worktree removal classification and workflow editor round-trip preservation of original node kinds.

Copilot AI review requested due to automatic review settings June 12, 2026 02:14
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7eed23c2-2f15-4d02-82f8-ae3605888137

📥 Commits

Reviewing files that changed from the base of the PR and between e62889d and 02a8581.

📒 Files selected for processing (9)
  • .changeset/fn-343-merge-worktree-cleanup.md
  • docs/diagnostics.md
  • packages/dashboard/app/components/__tests__/workflow-flow-mapping.test.ts
  • packages/dashboard/app/components/workflow-flow-mapping.ts
  • packages/engine/src/__tests__/reliability-interactions/worktrunk-worktree-removal.test.ts
  • packages/engine/src/__tests__/worktree-backend.test.ts
  • packages/engine/src/merger.ts
  • packages/engine/src/run-audit.ts
  • packages/engine/src/worktree-backend.ts
✅ Files skipped from review due to trivial changes (2)
  • .changeset/fn-343-merge-worktree-cleanup.md
  • docs/diagnostics.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/engine/src/run-audit.ts
  • packages/dashboard/app/components/tests/workflow-flow-mapping.test.ts
  • packages/engine/src/tests/worktree-backend.test.ts
  • packages/engine/src/tests/reliability-interactions/worktrunk-worktree-removal.test.ts

📝 Walkthrough

Walkthrough

Classifies certain temporary post-merge git worktree remove failures as harmless after git worktree prune + git worktree list --porcelain verification; returns structured removal outcomes, emits new audit events, updates merger cleanup to use outcomes, adds tests, and documents the behavior.

Changes

Harmless Merge Worktree Cleanup Classification

Layer / File(s) Summary
Harmless classification contract and helpers
packages/engine/src/worktree-backend.ts
Adds WorktreeRemoveOutcome and classifier: stderr preview, merge-temp detection, path normalization, git worktree prune + git worktree list --porcelain verification, and audit emission for classification.
removeWorktree signature and return paths
packages/engine/src/worktree-backend.ts
removeWorktree now returns Promise<WorktreeRemoveOutcome>; success returns removed outcome; backend/native failures trigger harmless classification probes and may return a classification outcome instead of throwing.
Merger integration with outcome handling
packages/engine/src/merger.ts
removePostMergeWorktree and main merge cleanup now forward audit into removeWorktree, warn on harmless classification (including outcome.message), and set result.worktreeRemoved based on outcome.removed or harmless classification.
Audit event types for classification
packages/engine/src/run-audit.ts
Extends GitMutationType with new worktree removal classification events: worktree:remove-classified-harmless, worktree:remove-classification-probe-failed, and worktree:remove-leaked-registered-worktree.
Unit tests for classification logic
packages/engine/src/__tests__/worktree-backend.test.ts
Adds tests covering harmless classification (not-registered-after-prune), non-temp failure behavior (no downgrade), leaked-registered case, and classification-probe failure; asserts outcomes, thrown errors, and emitted audit events.
Integration/reliability tests for post-merge cleanup
packages/engine/src/__tests__/reliability-interactions/worktrunk-worktree-removal.test.ts
Adds helper mocks and reliability tests simulating git worktree remove failures, asserting remove → prune → list --porcelain sequencing, harmless vs registered warnings, and restores mocks after each test.
Documentation and changeset
docs/diagnostics.md, .changeset/fn-343-merge-worktree-cleanup.md
New diagnostics section describing harmless vs leaked classification criteria, verification steps (prune + porcelain), operator verification command, and changeset patch bump for @fusion/engine.
Dashboard editor kind mapping and tests
packages/dashboard/app/components/workflow-flow-mapping.ts, packages/dashboard/app/components/__tests__/workflow-flow-mapping.test.ts
Preserves original IR kind as irKind when editor type differs; stamps/reads irKind across irToFlow/flowToIr, insertFragment, and adds a round-trip test validating aliased kinds (merge-gate, merge-attempt, manual-merge-hold, retry-backoff) survive editing round-trips.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • gsxdsm

Poem

I nibble at the prune and list,
Through transient paths in morning mist,
If Git forgets a fleeting lair,
I warn, I note — no need to tear.
A rabbit audits what I kissed 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: classifying harmless merge worktree cleanup failures, which is the primary focus of the changeset across multiple files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a “harmless” classification path for temporary merge/post-merge worktree removal failures by pruning + inspecting Git worktree porcelain output, so merger cleanup can proceed when no registered worktree leak remains.

Changes:

  • Introduce WorktreeRemoveOutcome and a classifier that runs git worktree prune + git worktree list --porcelain to distinguish harmless temp cleanup failures from real leaks.
  • Update merger cleanup callers to consume the new outcome and log a targeted warning for harmless residue.
  • Extend audit event types, add unit/reliability tests, and document the new diagnostic behavior.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/engine/src/worktree-backend.ts Adds outcome type + harmless failure classifier; updates removeWorktree to return outcomes and audit new classifications.
packages/engine/src/run-audit.ts Extends GitMutationType with new audit event strings for classified outcomes.
packages/engine/src/merger.ts Consumes removeWorktree outcome to warn on harmless classified failures and set worktreeRemoved accurately.
packages/engine/src/tests/worktree-backend.test.ts Adds unit tests covering harmless classification, non-temp behavior, and still-registered leak behavior.
packages/engine/src/tests/reliability-interactions/worktrunk-worktree-removal.test.ts Adds integration-style tests verifying merger logging + command sequence around prune/porcelain classification.
docs/diagnostics.md Documents the new merge temp worktree cleanup classification and operator verification steps.
.changeset/fn-343-merge-worktree-cleanup.md Declares the change as an @fusion/engine patch release.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/engine/src/worktree-backend.ts Outdated
Comment on lines +98 to +112
await execAsync("git worktree prune", {
cwd: input.rootDir,
encoding: "utf-8",
timeout: NATIVE_TIMEOUT_MS,
maxBuffer: MAX_BUFFER,
});

const listResult = await execAsync("git worktree list --porcelain", {
cwd: input.rootDir,
encoding: "utf-8",
timeout: 10_000,
maxBuffer: MAX_BUFFER,
});
const stdout = typeof listResult === "string" ? listResult : String(listResult.stdout ?? "");
const registeredAfterPrune = porcelainContainsWorktree(stdout, input.worktreePath);
Comment thread packages/engine/src/worktree-backend.ts Outdated
processActiveProbe?: ProcessActiveProbe;
reconcileMinIdleMs?: number;
}): Promise<void> {
}): Promise<void | WorktreeRemoveOutcome> {
Comment thread packages/engine/src/merger.ts Outdated
Comment on lines +7325 to +7327
if (outcome && "harmless" in outcome && outcome.harmless) {
mergerLog.warn(`${taskId}: post-merge worktree cleanup remove failed, but no registered worktree remains after prune for ${postMergeWorktree}: ${outcome.message}`);
}
@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a classifyHarmlessMergeRemoveFailure helper in worktree-backend.ts that runs git worktree prune + git worktree list --porcelain after a git worktree remove validation error, treating the removal as harmless only when git confirms the path is no longer registered. It also fixes a dashboard bug where aliased IR node kinds (merge-gate, merge-attempt, manual-merge-hold, retry-backoff) were collapsed to their editor-display kinds on round-trip through the workflow flow editor.

  • Engine (worktree-backend.ts, merger.ts): Adds WorktreeRemoveOutcome discriminated union, a path-prefix + error-pattern guard (isMergeTempCleanupCandidate), and three new audit event types; wires the optional audit parameter into removePostMergeWorktree; treats both removed: true and harmless-classified outcomes as worktreeRemoved = true so the task-store pointer is cleared correctly.
  • Dashboard (workflow-flow-mapping.ts): Introduces dataIrKind/preservedIrKind helpers that stash the original IR node kind in WorkflowFlowNodeData.irKind (via the interface's index signature) and restore it in flowToIr, covering all aliased node kinds at every code site (irToFlow, irNodeToFlowNode, insertFragment).

Confidence Score: 5/5

Safe to merge — the previously flagged gaps (missing audit parameter in the post-merge path, stale task-store worktree pointer, void return type) are all resolved, and the new classification logic is well-guarded by both reason and path-prefix checks.

The harmless classification is limited to two specific path prefixes and two merger-only removal reasons; any probe failure or still-registered worktree causes the original error to be rethrown, preserving the pre-existing failure behaviour. The dashboard round-trip fix is narrow and fully covered by the new test. No new code paths introduce data loss or incorrect state transitions.

packages/engine/src/worktree-backend.ts carries a pre-existing dead branch in porcelainContainsWorktree (the privateTarget comparison) that was identified in an earlier review cycle but not yet removed.

Important Files Changed

Filename Overview
packages/engine/src/worktree-backend.ts Adds WorktreeRemoveOutcome type, classifyHarmlessMergeRemoveFailure helper, and updates removeWorktree to return outcomes; contains a pre-existing dead branch in porcelainContainsWorktree (privateTarget can never match a normalized candidate) flagged in previous review thread.
packages/engine/src/merger.ts Adds optional audit parameter to removePostMergeWorktree, threads it through the call site, and correctly treats harmless-classified outcomes as worktreeRemoved=true so the task store is cleared.
packages/dashboard/app/components/workflow-flow-mapping.ts Adds dataIrKind/preservedIrKind helpers to stash and restore original IR node kinds; applied consistently at all node-creation sites; toIrNode correctly handles merge, loop/foreach, and default paths for all current GRAPH_ONLY_EDITOR_KIND mappings.
packages/engine/src/run-audit.ts Adds three new GitMutationType literals for the harmless-classification audit trail; purely additive change.
packages/engine/src/tests/worktree-backend.test.ts Adds four new test cases covering harmless classification, non-temp path rejection, leaked-registered detection, and probe failure; test inputs match the production error patterns.
packages/engine/src/tests/reliability-interactions/worktrunk-worktree-removal.test.ts Adds mockWorktreeRemoveFailure helper and two integration tests for post-merge harmless/leak paths; adds afterEach vi.restoreAllMocks to prevent spy bleed-over.
packages/dashboard/app/components/tests/workflow-flow-mapping.test.ts New round-trip test covers merge-gate, merge-attempt, manual-merge-hold, and retry-backoff kinds including config preservation; assertions are complete and correct.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[git worktree remove --force path] -->|success| B[return removed:true]
    A -->|error| C{isMergeTempCleanupCandidate?<br/>reason=MergerCleanup/PostMerge<br/>AND path starts with fusion-ai-merge- or post-merge-<br/>AND error matches patterns}
    C -->|no| D{WorktrunkOperationError<br/>+ fallback-native configured?}
    D -->|no| E[throw original error]
    D -->|yes| F[native.remove]
    F -->|success| G[return removed:true]
    F -->|error| H{classifyHarmless<br/>for native error}
    H -->|harmless| I[return removed:false harmless:true]
    H -->|not harmless| J[throw native error]
    C -->|yes| K[git worktree prune]
    K -->|prune fails| L[emit probe-failed audit<br/>return null → throw original]
    K -->|success| M[git worktree list --porcelain]
    M --> N{path still registered?}
    N -->|yes| O[emit leaked-registered-worktree audit<br/>return null → throw original]
    N -->|no| P[emit classified-harmless audit<br/>return removed:false harmless:true]
Loading

Reviews (5): Last reviewed commit: "fix(FN-343): address follow-up review fi..." | Re-trigger Greptile

Comment thread packages/engine/src/merger.ts
Comment thread packages/engine/src/worktree-backend.ts
Comment thread packages/engine/src/merger.ts Outdated
Comment thread packages/engine/src/worktree-backend.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/engine/src/__tests__/reliability-interactions/worktrunk-worktree-removal.test.ts (1)

62-77: ⚡ Quick win

Consider extracting the shared mock setup to reduce duplication.

The mock implementations in both new tests are nearly identical (16 lines each), differing only in the git worktree list --porcelain output. Extracting a helper function would improve maintainability and make it easier to add similar tests in the future.

♻️ Example refactor to extract shared setup
function mockWorktreeRemoveFailure(postMergePath: string, porcelainOutput: string) {
  execSpy.mockImplementation((cmd: string, _opts: unknown, cb: (err: any, stdout: string, stderr: string) => void) => {
    if (cmd.includes("git worktree remove")) {
      const stderr = `fatal: validation failed, cannot remove working tree: '${postMergePath}/.git' is not a .git file, error code 2`;
      cb(Object.assign(new Error(stderr), { stderr, status: 2 }), "", stderr);
      return;
    }
    if (cmd === "git worktree prune") {
      cb(null, "", "");
      return;
    }
    if (cmd === "git worktree list --porcelain") {
      cb(null, porcelainOutput, "");
      return;
    }
    cb(null, "", "");
  });
}

Then use it in tests:

-  execSpy.mockImplementation((cmd: string, _opts: unknown, cb: (err: any, stdout: string, stderr: string) => void) => {
-    if (cmd.includes("git worktree remove")) {
-      const stderr = `fatal: validation failed, cannot remove working tree: '${postMergePath}/.git' is not a .git file, error code 2`;
-      cb(Object.assign(new Error(stderr), { stderr, status: 2 }), "", stderr);
-      return;
-    }
-    if (cmd === "git worktree prune") {
-      cb(null, "", "");
-      return;
-    }
-    if (cmd === "git worktree list --porcelain") {
-      cb(null, "worktree /repo\nbranch refs/heads/main\n", "");
-      return;
-    }
-    cb(null, "", "");
-  });
+  mockWorktreeRemoveFailure(postMergePath, "worktree /repo\nbranch refs/heads/main\n");

Also applies to: 94-109

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/engine/src/__tests__/reliability-interactions/worktrunk-worktree-removal.test.ts`
around lines 62 - 77, Extract the duplicated execSpy.mockImplementation into a
helper (e.g., mockWorktreeRemoveFailure) that takes postMergePath and
porcelainOutput parameters and installs the shared mock behavior for "git
worktree remove", "git worktree prune", and "git worktree list --porcelain";
then replace the duplicated mockImplementation blocks in the tests with calls to
this helper (pass the file's postMergePath variable and the differing
porcelainOutput), keeping execSpy and existing error/stderr/status behavior
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/engine/src/worktree-backend.ts`:
- Around line 98-110: The current probe commands ("git worktree prune" and "git
worktree list --porcelain") can throw inside classifyHarmlessMergeRemoveFailure
and overwrite the original git worktree remove failure; change the logic around
classifyHarmlessMergeRemoveFailure in worktree-backend.ts so any errors from
prune/list are caught and used only to annotate/log/return a diagnostic but do
not replace the original remove error—i.e., call
classifyHarmlessMergeRemoveFailure with the probe results inside a try/catch,
capture its classification or warning text, attach it to or log it alongside the
original git worktree remove error (or rethrow the original error with the
classification appended) instead of throwing the probe error itself; reference
classifyHarmlessMergeRemoveFailure, the probe calls for "git worktree prune" and
"git worktree list --porcelain", and follow the downstream pattern used in
merger.ts where callers surface the caught error/warning text.

---

Nitpick comments:
In
`@packages/engine/src/__tests__/reliability-interactions/worktrunk-worktree-removal.test.ts`:
- Around line 62-77: Extract the duplicated execSpy.mockImplementation into a
helper (e.g., mockWorktreeRemoveFailure) that takes postMergePath and
porcelainOutput parameters and installs the shared mock behavior for "git
worktree remove", "git worktree prune", and "git worktree list --porcelain";
then replace the duplicated mockImplementation blocks in the tests with calls to
this helper (pass the file's postMergePath variable and the differing
porcelainOutput), keeping execSpy and existing error/stderr/status behavior
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 30f24112-28e4-425e-8838-186e85b765e1

📥 Commits

Reviewing files that changed from the base of the PR and between 535c40d and eccb9a5.

📒 Files selected for processing (7)
  • .changeset/fn-343-merge-worktree-cleanup.md
  • docs/diagnostics.md
  • packages/engine/src/__tests__/reliability-interactions/worktrunk-worktree-removal.test.ts
  • packages/engine/src/__tests__/worktree-backend.test.ts
  • packages/engine/src/merger.ts
  • packages/engine/src/run-audit.ts
  • packages/engine/src/worktree-backend.ts

Comment thread packages/engine/src/worktree-backend.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/dashboard/app/components/workflow-flow-mapping.ts`:
- Around line 139-149: The aliasing in workflow-flow-mapping.ts collapses
distinct IR node kinds (e.g., "merge-gate", "merge-attempt",
"branch-group-member-integration", "branch-group-promotion", "recovery-router",
"manual-merge-hold", "retry-backoff") into editor render kinds
("merge"/"hold"/"loop"), but flowToIr and toIrNode later serialize from
data.kind and will lose the original IR kind; to fix, preserve the original IR
kind on the flow node data when you map to editor render kinds (e.g., add a
stable property like irKind or originalKind to the node data in the mapping
function that returns "merge"/"hold"/"loop"), and update toIrNode (and/or
flowToIr) to prefer that irKind/originalKind when reconstructing IR nodes so the
persisted workflow retains the original semantic kinds instead of the editor
aliases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: deb2400a-b159-47ae-b020-0e4b6c2e7de7

📥 Commits

Reviewing files that changed from the base of the PR and between eccb9a5 and 4fcf418.

📒 Files selected for processing (1)
  • packages/dashboard/app/components/workflow-flow-mapping.ts

Comment thread packages/dashboard/app/components/workflow-flow-mapping.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/dashboard/app/components/workflow-flow-mapping.ts (1)

149-149: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

retry-backoff templates lost on round-trip due to mismatched expand/reassemble logic.

The retry-backoff"loop" editor mapping (line 149) causes template data loss:

  1. loopConfigOf (line 199) only recognizes kind === "loop", so retry-backoff templates aren't expanded to child nodes in irToFlow
  2. In toIrNode, the data.kind === "loop" check (line 408) reassembles templates from (empty) children
  3. Line 424 overwrites the original template with the empty reassembled one

A retry-backoff node with config: { template: { nodes: [A,B], edges: [...] } } round-trips back with template: { nodes: [], edges: [] }.

🛠️ Suggested fix: skip template reassembly for aliased kinds
 if (data.kind === "foreach" || data.kind === "loop") {
+  // Aliased kinds (e.g. retry-backoff) weren't expanded, so don't reassemble their templates
+  const shouldReassembleTemplate = !originalKind || originalKind === "foreach" || originalKind === "loop";
+  if (!shouldReassembleTemplate) {
+    return {
+      id: localId,
+      kind: originalKind,
+      config: config && Object.keys(config).length ? config : undefined,
+    };
+  }
   // Reassemble the template from this group's children.
   const children = childrenByGroup.get(node.id) ?? [];

Also applies to: 408-425

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dashboard/app/components/workflow-flow-mapping.ts` at line 149, The
mapping that aliases node.kind "retry-backoff" to "loop" causes template loss
because loopConfigOf and toIrNode only treat literal "loop"; fix by preserving
the original kind when aliasing and skipping template reassembly for aliased
kinds: when you map "retry-backoff" -> "loop" in the editor mapping (the code
that returns "loop" for node.kind === "retry-backoff"), attach a small marker
(e.g., originalKind or __aliasedKind) to the node or its data so downstream
functions can see it, then update toIrNode (the branch that runs when data.kind
=== "loop" and reassembles template from children) to check that marker and, if
originalKind exists and is not "loop" (i.e., it was an alias like
"retry-backoff"), skip reconstructing/overwriting the template (or alternatively
have loopConfigOf accept the aliased kind); this preserves the original template
payload across irToFlow -> toIrNode round-trips.
🧹 Nitpick comments (1)
packages/dashboard/app/components/__tests__/workflow-flow-mapping.test.ts (1)

201-228: ⚡ Quick win

Consider adding config preservation assertion for comprehensive coverage.

The test correctly verifies kind round-trip, but doesn't assert that node configs (especially the retry-backoff template) are preserved. Given the aliasing complexity, adding config checks would catch data loss regressions.

💡 Suggested enhancement
     expect(byId.retry.kind).toBe("retry-backoff");
+    // Verify configs survive round-trip
+    expect(byId.gate.config?.name).toBe("Gate");
+    expect(byId.hold.config?.release).toBe("manual");
+    expect(byId.retry.config).toMatchObject({ maxIterations: 2, template: { nodes: [], edges: [] } });
   });

For stronger coverage, consider a separate test with a non-empty retry-backoff template to ensure template content isn't lost.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dashboard/app/components/__tests__/workflow-flow-mapping.test.ts`
around lines 201 - 228, The test currently verifies kind round-tripping but
misses asserting that node configs (especially the retry-backoff template) are
preserved; update this test (or add a new one) to include a non-empty template
in the `retry` node and assert after round-trip (via irToFlow and flowToIr) that
the output IR `byId.retry.config` (and specifically
`byId.retry.config.template`) matches the original template content; reference
the existing test flow using irToFlow, flowToIr, and the byId lookup to add
these config/template assertions for nodes with kind "retry-backoff" (and any
other nodes with config like "manual-merge-hold") so we catch config loss during
aliasing conversions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/dashboard/app/components/workflow-flow-mapping.ts`:
- Line 149: The mapping that aliases node.kind "retry-backoff" to "loop" causes
template loss because loopConfigOf and toIrNode only treat literal "loop"; fix
by preserving the original kind when aliasing and skipping template reassembly
for aliased kinds: when you map "retry-backoff" -> "loop" in the editor mapping
(the code that returns "loop" for node.kind === "retry-backoff"), attach a small
marker (e.g., originalKind or __aliasedKind) to the node or its data so
downstream functions can see it, then update toIrNode (the branch that runs when
data.kind === "loop" and reassembles template from children) to check that
marker and, if originalKind exists and is not "loop" (i.e., it was an alias like
"retry-backoff"), skip reconstructing/overwriting the template (or alternatively
have loopConfigOf accept the aliased kind); this preserves the original template
payload across irToFlow -> toIrNode round-trips.

---

Nitpick comments:
In `@packages/dashboard/app/components/__tests__/workflow-flow-mapping.test.ts`:
- Around line 201-228: The test currently verifies kind round-tripping but
misses asserting that node configs (especially the retry-backoff template) are
preserved; update this test (or add a new one) to include a non-empty template
in the `retry` node and assert after round-trip (via irToFlow and flowToIr) that
the output IR `byId.retry.config` (and specifically
`byId.retry.config.template`) matches the original template content; reference
the existing test flow using irToFlow, flowToIr, and the byId lookup to add
these config/template assertions for nodes with kind "retry-backoff" (and any
other nodes with config like "manual-merge-hold") so we catch config loss during
aliasing conversions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7f1f3329-7472-473e-9e7b-2af1b9b6b49a

📥 Commits

Reviewing files that changed from the base of the PR and between 4fcf418 and 8b4c942.

📒 Files selected for processing (6)
  • packages/dashboard/app/components/__tests__/workflow-flow-mapping.test.ts
  • packages/dashboard/app/components/workflow-flow-mapping.ts
  • packages/engine/src/__tests__/reliability-interactions/worktrunk-worktree-removal.test.ts
  • packages/engine/src/__tests__/worktree-backend.test.ts
  • packages/engine/src/run-audit.ts
  • packages/engine/src/worktree-backend.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/engine/src/run-audit.ts
  • packages/engine/src/tests/worktree-backend.test.ts
  • packages/engine/src/worktree-backend.ts

plarson and others added 7 commits June 12, 2026 15:45
Fusion-Task-Id: FN-343

Co-authored-by: Fusion <noreply@runfusion.ai>
Fusion-Task-Id: FN-343

Co-authored-by: Fusion <noreply@runfusion.ai>
Fusion-Task-Id: FN-343

Co-authored-by: Fusion <noreply@runfusion.ai>
…geset

Fusion-Task-Id: FN-343

Co-authored-by: Fusion <noreply@runfusion.ai>
@plarson
plarson force-pushed the fix/fn-343-merge-worktree-cleanup branch from e62889d to 02a8581 Compare June 12, 2026 22:51
@gsxdsm
gsxdsm merged commit 9d0573a into Runfusion:main Jun 13, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants