feat(FN-3539): remove duplicate todos entry and update navigation docs

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
This commit is contained in:
Fusion
2026-05-06 03:39:37 -07:00
committed by gsxdsm
parent e518ce0b6c
commit 79a6b2c840
14 changed files with 101 additions and 72 deletions

View File

@@ -269,7 +269,7 @@ function createMockStore() {
setPluginWorkflowStepTemplates: vi.fn(),
appendAgentLog: vi.fn().mockResolvedValue(undefined),
getFusionDir: vi.fn().mockReturnValue("/tmp/test/.fusion"),
clearStaleBaseBranchReferences: vi.fn().mockReturnValue([]),
clearStaleExecutionStartBranchReferences: vi.fn().mockReturnValue([]),
};
return store as any;
}
@@ -1057,7 +1057,7 @@ describe("TaskExecutor worktree recovery", () => {
);
});
it("falls back to default base and clears task.baseBranch when the configured base ref is missing (FN-2165)", async () => {
it("falls back to default base and clears task.executionStartBranch when the configured base ref is missing (FN-2165)", async () => {
const store = createMockStore();
mockedExecSync.mockImplementation((cmd: string | string[]) => {
@@ -1074,7 +1074,7 @@ describe("TaskExecutor worktree recovery", () => {
const onError = vi.fn();
const executor = new TaskExecutor(store, "/tmp/test", { onError });
await executor.execute({ ...makeTask(), baseBranch: "fusion/missing-base" });
await executor.execute({ ...makeTask(), executionStartBranch: "fusion/missing-base" });
// Should log the soft fallback, not a terminal failure
expect(store.logEntry).toHaveBeenCalledWith(
@@ -1085,7 +1085,7 @@ describe("TaskExecutor worktree recovery", () => {
// Should clear baseBranch on the task so retries use the default
expect(store.updateTask).toHaveBeenCalledWith(
"FN-050",
expect.objectContaining({ baseBranch: null }),
expect.objectContaining({ executionStartBranch: null }),
);
// Should proceed to create a worktree from HEAD (no startPoint)
const worktreeAddCalls = mockedExecSync.mock.calls.filter(
@@ -1342,7 +1342,7 @@ describe("TaskExecutor worktree recovery", () => {
mockedGenerateWorktreeName.mockReturnValueOnce("jade-finch");
const executor = new TaskExecutor(store, "/tmp/test");
await executor.execute({ ...makeTask(), baseBranch: "fusion/fn-049" });
await executor.execute({ ...makeTask(), executionStartBranch: "fusion/fn-049" });
// Should log that we're trying a new path
expect(store.logEntry).toHaveBeenCalledWith(
@@ -1805,7 +1805,7 @@ describe("TaskExecutor dependency-based worktree creation", () => {
await executor.execute(makeTask({
id: "FN-060",
baseBranch: "fusion/fn-059",
executionStartBranch: "fusion/fn-059",
}));
// The git worktree add command should include the startPoint
@@ -1843,7 +1843,7 @@ describe("TaskExecutor dependency-based worktree creation", () => {
await executor.execute(makeTask({
id: "FN-062",
baseBranch: "fusion/fn-061",
executionStartBranch: "fusion/fn-061",
}));
expect(store.logEntry).toHaveBeenCalledWith(
@@ -1969,7 +1969,7 @@ describe("TaskExecutor dependency-based worktree creation", () => {
await executor.execute(makeTask({
id: "FN-064",
baseBranch: "fusion/fn-063",
executionStartBranch: "fusion/fn-063",
}));
expect(prepareSpy).toHaveBeenCalledWith(

View File

@@ -187,7 +187,7 @@ function createMockStore(taskOverrides: Partial<Task> = {}, allTasks: Task[] = [
getActiveMergingTask: vi.fn().mockReturnValue(null),
emit: vi.fn(),
on: vi.fn(),
clearStaleBaseBranchReferences: vi.fn().mockReturnValue([]),
clearStaleExecutionStartBranchReferences: vi.fn().mockReturnValue([]),
getVerificationCacheHit: vi.fn().mockReturnValue(null),
recordVerificationCachePass: vi.fn(),
} as unknown as TaskStore;
@@ -834,7 +834,7 @@ describe("aiMergeTask — pre-merge rebase abort observability", () => {
).toBe(true);
});
it("Layer 1 recovery: surgically drops dep commits via rebase --onto when baseBranch is set and primary rebase aborted", async () => {
it("Layer 1 recovery: surgically drops dep commits via rebase --onto when executionStartBranch is set and primary rebase aborted", async () => {
// Scenario: FN-2849 declared baseBranch=fusion/fn-2729 (a dep). The
// worktree was forked off FN-2729's tip and inherited its raw commits.
// FN-2729 was later squash-merged to main. Now the primary rebase onto
@@ -845,7 +845,7 @@ describe("aiMergeTask — pre-merge rebase abort observability", () => {
const store = createMockStore(
{
id: "FN-2849",
baseBranch: "fusion/fn-2729",
executionStartBranch: "fusion/fn-2729",
branch: "fusion/fn-2849",
worktree: "/tmp/root/.worktrees/coral-stone",
},

View File

@@ -854,7 +854,7 @@ describe("Scheduler", () => {
expect(updateTask).toHaveBeenCalledWith("FN-010", {
status: null,
blockedBy: null,
baseBranch: undefined,
executionStartBranch: undefined,
worktree: "/test/project/.worktrees/fn-010",
effectiveNodeId: null,
effectiveNodeSource: "local",
@@ -893,7 +893,7 @@ describe("Scheduler", () => {
expect(updateTask).toHaveBeenNthCalledWith(1, "FN-011", {
status: null,
blockedBy: null,
baseBranch: undefined,
executionStartBranch: undefined,
worktree: "/test/project/.worktrees/amber-aspen",
effectiveNodeId: null,
effectiveNodeSource: "local",
@@ -902,7 +902,7 @@ describe("Scheduler", () => {
expect(updateTask).toHaveBeenNthCalledWith(2, "FN-012", {
status: null,
blockedBy: null,
baseBranch: undefined,
executionStartBranch: undefined,
worktree: "/test/project/.worktrees/amber-aspen-2",
effectiveNodeId: null,
effectiveNodeSource: "local",

View File

@@ -119,7 +119,7 @@ function createMockStore(overrides: Record<string, unknown> = {}): TaskStore & E
walCheckpoint: vi.fn().mockReturnValue({ busy: 0, log: 5, checkpointed: 5 }),
listTasks: vi.fn().mockResolvedValue([]),
getRootDir: vi.fn().mockReturnValue("/tmp/test-project"),
clearStaleBaseBranchReferences: vi.fn().mockReturnValue([]),
clearStaleExecutionStartBranchReferences: vi.fn().mockReturnValue([]),
...overrides,
}) as unknown as TaskStore & EventEmitter;
return store;

View File

@@ -2165,7 +2165,7 @@ export class TaskExecutor {
let acquiredFromPool = false;
// Resolve the base branch — set by the scheduler when a dep is in-review
const baseBranch = task.baseBranch || null;
const baseBranch = task.executionStartBranch || null;
if (task.worktree && isResume && !await isUsableTaskWorktree(this.rootDir, worktreePath)) {
const invalidWorktreePath = worktreePath;
@@ -4227,7 +4227,7 @@ export class TaskExecutor {
}
if (branchDeleted) {
// FN-2165 regression guard: null baseBranch on any task that stored this branch
try { this.store.clearStaleBaseBranchReferences([branch], taskId); } catch { /* best-effort */ }
try { this.store.clearStaleExecutionStartBranchReferences([branch], taskId); } catch { /* best-effort */ }
}
// Clear worktree tracking
@@ -5438,7 +5438,7 @@ and show an appropriate message to the user.\`
// Stored baseBranch no longer exists (e.g., upstream dep merged and branch
// deleted while this task sat queued/stuck). Clear it on the task so any
// subsequent retry branches from the default base, and proceed from HEAD.
await this.store.updateTask(taskId, { baseBranch: null });
await this.store.updateTask(taskId, { executionStartBranch: null });
} else {
resolvedStartPoint = resolved;
}
@@ -6163,7 +6163,7 @@ and show an appropriate message to the user.\`
});
await this.store.logEntry(taskId, `Deleted branch`, branch);
// FN-2165 regression guard: null baseBranch on any task that stored this branch
this.store.clearStaleBaseBranchReferences([branch], taskId);
this.store.clearStaleExecutionStartBranchReferences([branch], taskId);
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
executorLog.warn(`${taskId}: failed to delete conflicting branch ${branch}: ${msg}`);
@@ -6210,7 +6210,7 @@ and show an appropriate message to the user.\`
});
await this.store.logEntry(taskId, `Removed stale branch`, branch);
// FN-2165 regression guard: null baseBranch on any task that stored this branch
try { this.store.clearStaleBaseBranchReferences([branch], taskId); } catch { /* best-effort */ }
try { this.store.clearStaleExecutionStartBranchReferences([branch], taskId); } catch { /* best-effort */ }
return true;
} catch (branchDeleteError: unknown) {
const branchDeleteErrorMessage = branchDeleteError instanceof Error ? branchDeleteError.message : String(branchDeleteError);
@@ -6229,7 +6229,7 @@ and show an appropriate message to the user.\`
});
await this.store.logEntry(taskId, `Force-removed stale branch reference via update-ref`, refPath);
// FN-2165 regression guard: null baseBranch on any task that stored this branch
try { this.store.clearStaleBaseBranchReferences([branch], taskId); } catch { /* best-effort */ }
try { this.store.clearStaleExecutionStartBranchReferences([branch], taskId); } catch { /* best-effort */ }
return true;
} catch (updateRefError: unknown) {
const updateRefErrorMessage = updateRefError instanceof Error ? updateRefError.message : String(updateRefError);

View File

@@ -3482,19 +3482,19 @@ export async function aiMergeTask(
}
// Layer 1: surgical drop of declared-dependency commits.
// When `task.baseBranch` is a non-main branch (a sibling task's branch),
// When `task.executionStartBranch` is a non-main branch (a sibling task's branch),
// the dependent worktree was forked off it and inherited its commits.
// If the dep was later squash-merged to main, those raw commits are now
// orphans whose content already exists in main. Re-rebase the task
// branch onto main using `git rebase --onto <target> <dep-tip> <branch>`,
// which peels off the dep's commits cleanly.
if (rebaseTarget && task.baseBranch && task.baseBranch !== "main") {
if (rebaseTarget && task.executionStartBranch && task.executionStartBranch !== "main") {
// Resolve the dep's tip — prefer the live branch ref, fall back to
// the recorded baseCommitSha if the branch was already deleted.
let depTip: string | undefined;
try {
const { stdout } = await execAsync(
`git rev-parse --verify "${task.baseBranch}^{commit}"`,
`git rev-parse --verify "${task.executionStartBranch}^{commit}"`,
{ cwd: rootDir, encoding: "utf-8" },
);
depTip = stdout.trim() || undefined;
@@ -3528,11 +3528,11 @@ export async function aiMergeTask(
preferMainRebaseFailureMessage = undefined;
rebaseHappened = true;
mergerLog.log(
`${taskId}: Layer 1 recovery — rebased ${branch} --onto ${rebaseTarget.slice(0, 8)} dropping commits up to dep tip ${depTip.slice(0, 8)} (baseBranch=${task.baseBranch})`,
`${taskId}: Layer 1 recovery — rebased ${branch} --onto ${rebaseTarget.slice(0, 8)} dropping commits up to dep tip ${depTip.slice(0, 8)} (executionStartBranch=${task.executionStartBranch})`,
);
await store.logEntry(
taskId,
`Pre-merge recovery (Layer 1): dropped dependency commits from ${task.baseBranch} via rebase --onto ${rebaseTarget.slice(0, 8)} ${depTip.slice(0, 8)} ${branch}; the merge will proceed against the cleaned branch`,
`Pre-merge recovery (Layer 1): dropped dependency commits from ${task.executionStartBranch} via rebase --onto ${rebaseTarget.slice(0, 8)} ${depTip.slice(0, 8)} ${branch}; the merge will proceed against the cleaned branch`,
);
} catch (layer1Err) {
rethrowIfMergeAborted(layer1Err);
@@ -4348,7 +4348,7 @@ export async function aiMergeTask(
// conflict-suffixed branch), null it so the dependent task doesn't
// hard-fail at worktree creation once this branch is gone.
try {
const cleared = store.clearStaleBaseBranchReferences([branch], taskId);
const cleared = store.clearStaleExecutionStartBranchReferences([branch], taskId);
if (cleared.length > 0) {
mergerLog.log(`${taskId}: cleared stale baseBranch on ${cleared.length} dependent task(s): ${cleared.join(", ")}`);
}

View File

@@ -835,7 +835,7 @@ export class Scheduler {
await this.store.updateTask(task.id, {
status: null,
blockedBy: null,
baseBranch: baseBranch ?? undefined,
executionStartBranch: baseBranch ?? undefined,
worktree: plannedWorktree,
effectiveNodeId: effectiveNode.nodeId ?? null,
effectiveNodeSource: effectiveNode.source,

View File

@@ -2079,7 +2079,7 @@ export class SelfHealingManager {
// FN-2165 regression guard: if any dependent task stored one of these
// now-gone branches as its baseBranch, null it so the task doesn't
// hard-fail at worktree creation time.
const cleared = this.store.clearStaleBaseBranchReferences(deletedBranches);
const cleared = this.store.clearStaleExecutionStartBranchReferences(deletedBranches);
if (cleared.length > 0) {
log.log(`Cleared stale baseBranch on ${cleared.length} task(s): ${cleared.join(", ")}`);
}