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

This commit is contained in:
Fusion
2026-04-17 14:26:12 -07:00
committed by gsxdsm
parent ac9b0fcd9d
commit 96aeb67a4f
37 changed files with 960 additions and 533 deletions

View File

@@ -6,7 +6,7 @@ import { isAbsolute, join } from "node:path";
import { existsSync } from "node:fs";
import { readFile, writeFile } from "node:fs/promises";
import type { TaskStore, Task, TaskDetail, StepStatus, Settings, WorkflowStep, MissionStore, Slice, AgentState, AgentCapability, RunMutationContext } from "@fusion/core";
import { buildExecutionMemoryInstructions, getTaskMergeBlocker, resolveAgentPrompt, runCommandAsync, type RunCommandResult } from "@fusion/core";
import { buildExecutionMemoryInstructions, getTaskMergeBlocker, resolveAgentPrompt, type RunCommandResult } from "@fusion/core";
import { findWorktreeUser } from "./merger.js";
import { generateWorktreeName, slugify } from "./worktree-names.js";
import { Type, type Static } from "@mariozechner/pi-ai";
@@ -96,11 +96,43 @@ async function runConfiguredCommand(
cwd: string,
timeoutMs: number,
): Promise<RunCommandResult> {
return runCommandAsync(command, {
cwd,
timeoutMs,
maxBuffer: 10 * 1024 * 1024,
});
try {
const { stdout, stderr } = await execAsync(command, {
cwd,
timeout: timeoutMs,
maxBuffer: 10 * 1024 * 1024,
encoding: "utf-8",
});
return {
stdout: stdout?.toString?.() ?? "",
stderr: stderr?.toString?.() ?? "",
exitCode: 0,
signal: null,
bufferExceeded: false,
timedOut: false,
};
} catch (error: any) {
const code = error?.code;
const status = typeof error?.status === "number" ? error.status : null;
const exitCode = typeof code === "number" ? code : status;
const message = String(error?.message ?? "");
return {
stdout: error?.stdout?.toString?.() ?? "",
stderr: error?.stderr?.toString?.() ?? "",
exitCode,
signal: (error?.signal as NodeJS.Signals | null | undefined) ?? null,
bufferExceeded:
code === "ENOBUFS"
|| code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"
|| message.includes("maxBuffer"),
timedOut:
code === "ETIMEDOUT"
|| (error?.killed === true && (error?.signal === "SIGTERM" || message.includes("timed out"))),
spawnError: code === "ENOENT" || code === "EACCES" ? error : undefined,
};
}
}
// ── Tool parameter schemas (module-level for reuse in ToolDefinition generics) ──

View File

@@ -5,7 +5,7 @@ import { promisify } from "node:util";
const execAsync = promisify(exec);
import { existsSync } from "node:fs";
import { join } from "node:path";
import { getTaskMergeBlocker, runCommandAsync, type TaskStore, type MergeResult, type MergeDetails, type WorkflowStep, type WorkflowStepResult, type Settings, type AgentPromptsConfig } from "@fusion/core";
import { getTaskMergeBlocker, type TaskStore, type MergeResult, type MergeDetails, type WorkflowStep, type WorkflowStepResult, type Settings, type AgentPromptsConfig } from "@fusion/core";
import { resolveAgentPrompt } from "@fusion/core";
import { createKbAgent, describeModel, promptWithFallback, compactSessionContext } from "./pi.js";
import { buildSessionSkillContext } from "./session-skill-context.js";
@@ -570,43 +570,31 @@ async function runVerificationCommand(
};
try {
const commandResult = await runCommandAsync(command, {
const { stdout, stderr } = await execAsync(command, {
cwd: rootDir,
timeoutMs: 300_000,
encoding: "utf-8",
timeout: 300_000,
maxBuffer: VERIFICATION_COMMAND_MAX_BUFFER,
});
result.stdout = commandResult.stdout;
result.stderr = commandResult.stderr;
result.exitCode = commandResult.exitCode;
result.success = !commandResult.spawnError
&& !commandResult.timedOut
&& commandResult.exitCode === 0;
if (result.success) {
const bufferNote = commandResult.bufferExceeded ? ", output exceeded buffer" : "";
mergerLog.log(`${taskId}: ${type} command succeeded`);
await store.logEntry(taskId, `[verification] ${type} command succeeded (exit 0${bufferNote})`);
return result;
}
result.stdout = stdout?.toString?.() || "";
result.stderr = stderr?.toString?.() || "";
result.exitCode = 0;
result.success = true;
const failureText = commandResult.spawnError?.message
|| (commandResult.timedOut ? "Command timed out" : "")
|| commandResult.stderr
|| commandResult.stdout
|| `Command exited with ${commandResult.exitCode ?? commandResult.signal ?? "unknown status"}`;
throw Object.assign(new Error(failureText), {
stdout: commandResult.stdout,
stderr: commandResult.stderr,
status: commandResult.exitCode,
code: commandResult.timedOut ? "ETIMEDOUT" : undefined,
});
mergerLog.log(`${taskId}: ${type} command succeeded`);
await store.logEntry(taskId, `[verification] ${type} command succeeded (exit 0)`);
return result;
} catch (error: any) {
result.stdout = error.stdout?.toString() || "";
result.stderr = error.stderr?.toString() || "";
result.exitCode = error.status ?? null;
const maxBufferExceeded = error.code === "ENOBUFS"
|| error.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"
|| error.message?.includes("maxBuffer");
result.stdout = error?.stdout?.toString?.() || "";
result.stderr = error?.stderr?.toString?.() || "";
result.exitCode = typeof error?.status === "number"
? error.status
: (typeof error?.code === "number" ? error.code : null);
const maxBufferExceeded = error?.code === "ENOBUFS"
|| error?.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"
|| String(error?.message ?? "").includes("maxBuffer");
result.success = maxBufferExceeded && result.exitCode === 0;
if (result.success) {
@@ -620,7 +608,7 @@ async function runVerificationCommand(
// Keep command output out of process logs. The bounded excerpt is stored on
// the task for diagnostics without dumping test output to the engine stdout.
const output = result.stderr || result.stdout || error.message || "Unknown error";
const output = result.stderr || result.stdout || error?.message || "Unknown error";
const summary = summarizeVerificationOutput(output, type);
mergerLog.error(`${taskId}: ${type} command failed (exit ${result.exitCode}); output captured in task log`);
await store.logEntry(

View File

@@ -332,9 +332,15 @@ export async function reviewStep(
}
// Spawn a reviewer agent with read-only tools
const memoryAgent = options.rootDir && options.agentStore && options.task?.assignedAgentId
? await options.agentStore.getAgent(options.task.assignedAgentId).catch(() => null)
: null;
const assignedAgentId = options.task?.assignedAgentId ?? null;
const agentStore = options.agentStore;
const memoryAgent =
options.rootDir
&& agentStore
&& assignedAgentId
&& typeof (agentStore as { getAgent?: unknown }).getAgent === "function"
? await agentStore.getAgent(assignedAgentId).catch(() => null)
: null;
const memoryTools = options.rootDir && options.settings?.memoryEnabled !== false
? [
createMemorySearchTool(options.rootDir, options.settings, memoryAgent ? {