feat(FN-3756): extract roadmap UI into bundled plugin and add autostash orp

Merges major roadmap plugin extraction (FN-3160/3161/3162), hardening the merger with autostash orphan cleanup and TOCTOU defenses (FN-3755/3756), adding shared state snapshots for mesh sync (FN-3451), shipping polling task notifications for the even realities plugin (FN-3743), defaulting non-epheme

Fusion-Task-Id: FN-3756
This commit is contained in:
Fusion
2026-05-08 17:39:50 -07:00
committed by gsxdsm
parent 726eb9a1d7
commit 08d2d130b5
4 changed files with 189 additions and 4 deletions

View File

@@ -1258,6 +1258,11 @@ async function listOrphanedAutostashes(
}
}
function parseAutostashTaskId(label: string): string | null {
const match = /^fusion-merger-autostash:([A-Za-z]+-\d+):/.exec(label.trim());
return match?.[1] ?? null;
}
/**
* Stash any unrelated dirty changes in `rootDir` before a merge runs.
*
@@ -1326,6 +1331,8 @@ async function sweepAutostashOrphans(
const subsumed: Array<{ sha: string; ref: string; label: string }> = [];
const live: Array<{ sha: string; ref: string; label: string }> = [];
const droppedClosedTask: Array<{ sha: string; taskId: string; column: Task["column"] }> = [];
for (const orphan of orphans) {
try {
const stashFiles = await listStashChangedPaths(rootDir, orphan.sha);
@@ -1335,15 +1342,50 @@ async function sweepAutostashOrphans(
continue;
}
const pathsArg = [...stashFiles].map(quoteArg).join(" ");
const { stdout } = await execAsync(
const { stdout: pathDiffOut } = await execAsync(
`git diff --name-only HEAD ${quoteArg(orphan.sha)} -- ${pathsArg}`,
{ cwd: rootDir, encoding: "utf-8" },
);
if (stdout.trim() === "") {
const isPathSubsumed = pathDiffOut.trim() === "";
if (isPathSubsumed) {
subsumed.push(orphan);
} else {
live.push(orphan);
continue;
}
const sourceTaskId = parseAutostashTaskId(orphan.label);
if (!sourceTaskId) {
live.push(orphan);
continue;
}
let sourceTask: Task | null = null;
try {
sourceTask = await store.getTask(sourceTaskId);
} catch {
live.push(orphan);
continue;
}
if (!sourceTask || (sourceTask.column !== "done" && sourceTask.column !== "archived")) {
live.push(orphan);
continue;
}
try {
const { stdout: netDiffOut } = await execAsync(
`git diff HEAD ${quoteArg(orphan.sha)}`,
{ cwd: rootDir, encoding: "utf-8" },
);
if (netDiffOut.trim() === "") {
subsumed.push(orphan);
droppedClosedTask.push({ sha: orphan.sha, taskId: sourceTaskId, column: sourceTask.column });
continue;
}
} catch {
live.push(orphan);
continue;
}
live.push(orphan);
} catch {
// If we can't classify, treat as live — better to leave a real stash
// sitting around than to drop one that still contains lost work.
@@ -1353,6 +1395,13 @@ async function sweepAutostashOrphans(
for (const orphan of subsumed) {
await dropAutostashBySha(rootDir, taskId, orphan.sha);
const closedTaskDrop = droppedClosedTask.find((entry) => entry.sha === orphan.sha);
if (closedTaskDrop) {
mergerLog.log(
`${taskId}: dropped closed-task autostash ${orphan.sha.slice(0, 7)} (task ${closedTaskDrop.taskId} is ${closedTaskDrop.column})`,
);
continue;
}
mergerLog.log(
`${taskId}: dropped subsumed autostash ${orphan.sha.slice(0, 7)} (${orphan.label}) — content already present on HEAD`,
);
@@ -1388,6 +1437,11 @@ async function sweepAutostashOrphans(
}
}
export const __test__ = {
sweepAutostashOrphans,
parseAutostashTaskId,
};
async function stashUnrelatedRootDirChanges(
rootDir: string,
taskId: string,