feat(FN-4891): merge fusion/fn-4891

This commit is contained in:
gsxdsm
2026-05-17 08:24:49 -07:00
parent da6598a4c0
commit 0d0d818b77
7 changed files with 361 additions and 2 deletions

View File

@@ -0,0 +1,87 @@
import { afterEach, describe, expect, it } from "vitest";
import { hasGit, makeReliabilityFixture } from "./_helpers.js";
const describeIfGit = hasGit ? describe : describe.skip;
describeIfGit("reliability interactions: self-defeating dep reconciliation", () => {
const fixtures: Array<Awaited<ReturnType<typeof makeReliabilityFixture>>> = [];
afterEach(async () => {
while (fixtures.length) await fixtures.pop()!.cleanup();
});
it("reconciles pre-existing offender in todo and records audit + task log", async () => {
const fx = await makeReliabilityFixture({
taskId: "FN-4891-A",
task: {
column: "todo",
title: "safe title",
dependencies: ["FN-100", "FN-200"],
} as any,
});
fixtures.push(fx);
await fx.store.updateTask(fx.task.id, { title: "Finalize FN-100: close loop" });
const recovered = await fx.manager.reconcileSelfDefeatingDependencies();
expect(recovered).toBe(1);
const updated = await fx.store.getTask(fx.task.id);
expect(updated?.dependencies).toEqual(["FN-200"]);
expect(
updated?.log.some((entry) => JSON.stringify(entry).includes("Auto-reconciled self-defeating dependency")),
).toBe(true);
const events = fx.store.getRunAuditEvents({
taskId: fx.task.id,
domain: "database",
mutationType: "task:auto-reconciled-self-defeating-dep",
});
expect(events).toHaveLength(1);
expect(events[0]?.target).toBe(fx.task.id);
expect(events[0]?.metadata).toMatchObject({
matchedVerb: "finalize",
operandTaskId: "FN-100",
originalDependencies: ["FN-100", "FN-200"],
nextDependencies: ["FN-200"],
});
});
it("does not reconcile non-operational test title", async () => {
const fx = await makeReliabilityFixture({
taskId: "FN-4891-B",
task: {
column: "todo",
title: "Test FN-100",
dependencies: ["FN-100"],
} as any,
});
fixtures.push(fx);
const recovered = await fx.manager.reconcileSelfDefeatingDependencies();
expect(recovered).toBe(0);
const updated = await fx.store.getTask(fx.task.id);
expect(updated?.dependencies).toEqual(["FN-100"]);
});
it("does not touch in-progress offenders", async () => {
const fx = await makeReliabilityFixture({
taskId: "FN-4891-C",
task: {
column: "in-progress",
title: "safe title",
dependencies: ["FN-100"],
} as any,
});
fixtures.push(fx);
await fx.store.updateTask(fx.task.id, { title: "Finalize FN-100" });
const recovered = await fx.manager.reconcileSelfDefeatingDependencies();
expect(recovered).toBe(0);
const updated = await fx.store.getTask(fx.task.id);
expect(updated?.dependencies).toEqual(["FN-100"]);
});
});

View File

@@ -667,6 +667,13 @@ export function createTaskCreateTool(
isError: true,
};
}
if (err instanceof Error && (err as { code?: string }).code === "SELF_DEFEATING_DEPENDENCY") {
return {
content: [{ type: "text" as const, text: `ERROR: ${err.message}` }],
details: {},
isError: true,
};
}
throw err;
}
},

View File

@@ -135,6 +135,7 @@ export type DatabaseMutationType =
| "task:auto-recover-finalize-already-on-main"
| "task:auto-recover-branch-misbound"
| "task:auto-recover-node-unreachable"
| "task:auto-reconciled-self-defeating-dep"
/**
* Metadata shape for node:handoff:* and node:lease:* events:
* ```ts

View File

@@ -26,7 +26,7 @@ import { exec, execSync } from "node:child_process";
import { promisify } from "node:util";
import { existsSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
import { isAbsolute, join, relative, resolve } from "node:path";
import { getInReviewStallReason, getStalePausedReviewSignal, getTaskHardMergeBlocker, getTaskMergeBlocker, isEphemeralAgent, type AgentStore, type ChatStore, type MessageStore, type TaskStore, type Settings, type Task, type MergeDetails, type TaskPriority } from "@fusion/core";
import { detectSelfDefeatingDependency, getInReviewStallReason, getStalePausedReviewSignal, getTaskHardMergeBlocker, getTaskMergeBlocker, isEphemeralAgent, type AgentStore, type ChatStore, type MessageStore, type TaskStore, type Settings, type Task, type MergeDetails, type TaskPriority } from "@fusion/core";
import type { MeshLeaseManager } from "./mesh-lease-manager.js";
import { createLogger } from "./logger.js";
import { RemovalReason, getRegisteredWorktreePaths, isUsableTaskWorktree, removeWorktree, resolveWorktreeBackend, scanIdleWorktrees, scanOrphanedBranches } from "./worktree-pool.js";
@@ -472,6 +472,7 @@ export class SelfHealingManager {
{ name: "recover-running-on-inactive-tasks", fn: () => this.recoverAgentsRunningOnInactiveTasks().then(() => undefined) },
{ name: "recover-drifted-agent-task-links", fn: () => this.recoverDriftedAgentTaskLinks().then(() => undefined) },
{ name: "clear-stale-blocked-by", fn: () => this.clearStaleBlockedBy().then(() => undefined) },
{ name: "reconcile-self-defeating-deps", fn: () => this.reconcileSelfDefeatingDependencies().then(() => undefined) },
{ name: "reclaim-self-owned-branch-conflicts", fn: () => this.reclaimSelfOwnedBranchConflicts().then(() => undefined) },
{ name: "reclaim-stale-active-branches", fn: () => this.reclaimStaleActiveBranches().then(() => undefined) },
{ name: "surface-in-review-stalls", fn: () => this.surfaceInReviewStalls().then(() => undefined) },
@@ -1045,6 +1046,7 @@ export class SelfHealingManager {
{ name: "recover-running-on-inactive-tasks", fn: () => this.recoverAgentsRunningOnInactiveTasks() },
{ name: "recover-drifted-agent-task-links", fn: () => this.recoverDriftedAgentTaskLinks() },
{ name: "clear-stale-blocked-by", fn: () => this.clearStaleBlockedBy() },
{ name: "reconcile-self-defeating-deps", fn: () => this.reconcileSelfDefeatingDependencies() },
{ name: "reclaim-self-owned-branch-conflicts", fn: () => this.reclaimSelfOwnedBranchConflicts() },
{ name: "reclaim-stale-active-branches", fn: () => this.reclaimStaleActiveBranches() },
{ name: "surface-in-review-stalls", fn: () => this.surfaceInReviewStalls() },
@@ -2233,6 +2235,64 @@ export class SelfHealingManager {
}
}
async reconcileSelfDefeatingDependencies(): Promise<number> {
const targetColumns: Array<Task["column"]> = ["triage", "planning", "todo"];
let recovered = 0;
for (const column of targetColumns) {
let tasks: Task[] = [];
try {
tasks = await this.store.listTasks({ column, slim: true });
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
log.warn(`reconcileSelfDefeatingDependencies: failed to list ${column} tasks: ${errorMessage}`);
continue;
}
for (const task of tasks) {
if (!task.dependencies.length) continue;
const match = detectSelfDefeatingDependency(task.title, task.dependencies);
if (!match) continue;
const originalDependencies = [...task.dependencies];
const nextDependencies = originalDependencies.filter((dep) => dep.toUpperCase() !== match.operandTaskId.toUpperCase());
if (nextDependencies.length === originalDependencies.length) continue;
try {
await this.store.updateTask(task.id, { dependencies: nextDependencies });
await this.store.logEntry(
task.id,
`Auto-reconciled self-defeating dependency: removed ${match.operandTaskId} (matched verb: "${match.matchedVerb}") from dependencies.`,
);
const auditor = createRunAuditor(this.store, {
runId: generateSyntheticRunId("self-heal-self-defeating-dep"),
agentId: "system:self-healing",
taskId: task.id,
phase: "reconcile-self-defeating-dep",
});
await auditor.database({
type: "task:auto-reconciled-self-defeating-dep",
target: task.id,
metadata: {
matchedVerb: match.matchedVerb,
operandTaskId: match.operandTaskId,
originalDependencies,
nextDependencies,
},
});
recovered++;
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
log.warn(`reconcileSelfDefeatingDependencies: failed for ${task.id}: ${errorMessage}`);
}
}
}
return recovered;
}
private async recordIntegrityAudit(taskId: string, mutationType: "task:finalize-unproven-blocked" | "task:integrity-reconcile-modified-files" | "task:integrity-warning", metadata: Record<string, unknown>): Promise<void> {
const auditor = createRunAuditor(this.store, {
runId: generateSyntheticRunId("self-healing-integrity", taskId),