Files
fusion/packages/engine/src/executor/attempt-executor-verification-fix.ts
Fusion Agent 0b4dbd219b FN-125: prevent workflow agents from creating tasks
Stop workflow executor agents from creating ordinary tasks while preserving explicit self-spawned dependency handling.

- Gate task creation by session identity and execution context.
- Add self-spawned dependency support, validation, and persistence coverage.
- Update agent prompts, tools, documentation, and CLI integration.
- Add a published-package changeset.

Files changed:
 .changeset/fn-125-removal.md                       |  7 +++
 docs/agents.md                                     |  2 +-
 docs/dashboard-guide.md                            |  2 +-
 docs/settings-reference.md                         |  6 +-
 .../cli/skill/fusion/references/engine-tools.md    | 10 ++--
 .../extension-task-execution-task-creation.test.ts | 70 ++++++++++++++++++++++
 packages/cli/src/extension.ts                      | 49 +++++++++++++--
 .../postgres/store-self-spawned-dep.pg.test.ts     | 56 +++++++++++++++++
 .../src/__tests__/self-spawned-dependency.test.ts  | 18 ++++++
 .../__tests__/session-identity-registry.test.ts    | 14 ++++-
 packages/core/src/agents/agent-prompts.ts          | 27 ++++-----
 .../src/agents/task-execution-task-creation.ts     | 19 ++++++
 packages/core/src/index.ts                         |  7 +++
 packages/core/src/session-identity-registry.ts     |  7 +++
 packages/core/src/store.ts                         |  2 +
 packages/core/src/task-store/errors.ts             | 21 +++++++
 packages/core/src/task-store/task-update.ts        | 10 +++-
 packages/core/src/task-store/update-task-deps.ts   | 12 +++-
 .../__tests__/ephemeral-task-create-gate.test.ts   | 18 +++---
 .../src/__tests__/executor-review-verdicts.test.ts |  3 +-
 packages/engine/src/agent-tools.ts                 |  4 ++
 packages/engine/src/agents/agent-runtime.ts        |  2 +
 .../engine/src/execution/step-session-executor.ts  | 15 ++---
 .../executor/attempt-executor-verification-fix.ts  |  1 +
 .../engine/src/executor/create-spawn-agent-tool.ts |  1 +
 .../engine/src/executor/create-task-done-tool.ts   | 18 +++++-
 .../engine/src/executor/create-task-update-tool.ts | 12 +++-
 .../engine/src/executor/execute-workflow-step.ts   |  1 +
 packages/engine/src/executor/run-implementation.ts | 18 +++---
 packages/engine/src/executor/system-prompt.ts      | 21 +++----
 packages/engine/src/executor/task-add-dep-tool.ts  | 14 ++++-
 packages/engine/src/pi.ts                          | 15 ++++-
 32 files changed, 401 insertions(+), 81 deletions(-)

Fusion-Task-Id: FN-125

Fusion-Task-Lineage: da3d69f6-c9cb-45fd-8691-3dfaf7007514
Co-authored-by: Fusion <noreply@runfusion.ai>
2026-08-23 01:36:12 +00:00

256 lines
12 KiB
TypeScript

/**
* FNXC:CodeOrganization 2026-08-03-12:55:
* attemptExecutorVerificationFix peeled from TaskExecutor (U4).
*
* Spawns a dedicated coding session to repair failing deterministic test/build
* verification mid-execution, then re-runs full verification. Mirrors the merger
* in-merge verification-fix pattern.
*
* FNXC:SessionRouting 2026-06-24-11:20:
* Propagate task id so verification-fix requests share session affinity.
*
* FNXC:PluginSkills 2026-07-12-00:00:
* Verification-fix sessions inherit plugin skill body dirs from task skill context.
*/
import type { Settings, Task, TaskStore } from "@fusion/core";
import { resolveExecutorFallbackModel, resolvePersistAgentThinkingLog } from "@fusion/core";
import { AgentLogger } from "../agents/agent-logger.js";
// FNXC:CommandCenterActivity 2026-08-15-22:15: FN-8868 usage telemetry + session boundaries (restored post-wave-18).
import { attachAgentUsageTelemetry, emitAgentSessionStart } from "../agents/agent-usage-telemetry.js";
import {
createResolvedAgentSession,
resolveExecutorSessionModel,
resolveExecutorFallbackThinkingLevel,
resolveExecutorThinkingLevel,
} from "../agents/agent-session-helpers.js";
import { buildSessionSkillContext } from "../cli-runtime/session-skill-context.js";
import { accumulateSessionTokenUsage } from "../execution/session-token-usage.js";
import { VERIFICATION_LOG_MAX_CHARS } from "../execution/verification-utils.js";
import { withRateLimitRetry } from "../errors/rate-limit-retry.js";
import { describeModel, promptWithFallback } from "../pi.js";
import { executorLog } from "../logger.js";
import { createRunAuditor, type EngineRunContext } from "../util/run-audit.js";
import type { PluginRunner } from "../plugins/plugin-runner.js";
import type { AgentStore } from "@fusion/core";
export type AttemptExecutorVerificationFixDeps = {
store: TaskStore;
agentStore?: AgentStore | null;
pluginRunner?: PluginRunner;
onAgentText?: ConstructorParameters<typeof AgentLogger>[0]["onAgentText"];
onAgentTool?: ConstructorParameters<typeof AgentLogger>[0]["onAgentTool"];
getRunContextFor: (taskId: string) => EngineRunContext | undefined;
getAssignedAgentRuntimeConfig: (agentId: string | null | undefined) => Promise<Record<string, unknown> | undefined>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- MCP map shape owned by session helpers
resolveMcpServers: (agentId?: string | null) => Promise<any>;
runExecutorDeterministicVerification: (
task: Task,
worktreePath: string,
settings: Settings,
extraEnv?: NodeJS.ProcessEnv,
) => Promise<{ allPassed: boolean }>;
};
/**
* Attempt to fix verification failures by spawning a dedicated AI fix agent.
* Follows the pattern established by the merger's attemptInMergeVerificationFix.
* Returns true if verification passes after the fix attempt, false otherwise.
*/
export async function attemptExecutorVerificationFix(
deps: AttemptExecutorVerificationFixDeps,
task: Task,
worktreePath: string,
failureContext: {
command: string;
exitCode: number | null;
output: string;
type: "test" | "build";
},
settings: Settings,
retryNumber: number,
maxRetries: number,
extraEnv?: NodeJS.ProcessEnv,
): Promise<boolean> {
try {
executorLog.log(`${task.id}: spawning executor verification fix agent (attempt ${retryNumber}/${maxRetries})`);
const logger = new AgentLogger({
store: deps.store,
taskId: task.id,
agent: "executor",
persistAgentToolOutput: settings.persistAgentToolOutput,
// Executor sessions are task-scoped ephemeral workers.
persistAgentThinkingLog: resolvePersistAgentThinkingLog(settings, { ephemeral: true }),
onAgentText: deps.onAgentText,
onAgentTool: deps.onAgentTool,
});
// FNXC:CommandCenterActivity 2026-08-15-22:15: FN-8868 usage telemetry (restored post-wave-18).
attachAgentUsageTelemetry(logger, { store: deps.store, agentId: task.assignedAgentId ?? null, taskId: task.id, nodeId: task.effectiveNodeId ?? task.nodeId ?? null, lane: "executor" });
// Build skill selection context
let skillContext: Awaited<ReturnType<typeof buildSessionSkillContext>> | undefined;
if (deps.agentStore) {
try {
skillContext = await buildSessionSkillContext({
agentStore: deps.agentStore,
task,
sessionPurpose: "executor",
projectRootDir: worktreePath,
pluginRunner: deps.pluginRunner,
});
} catch {
// Graceful fallback - no skill selection
}
}
// Resolve model using the executor's model hierarchy
const assignedRuntimeConfig = await deps.getAssignedAgentRuntimeConfig(task.assignedAgentId);
const executorSessionModel = resolveExecutorSessionModel(
task.modelProvider,
task.modelId,
settings,
assignedRuntimeConfig,
task.credentialInstanceId,
);
const { provider: executorProvider, modelId: executorModelId } = executorSessionModel;
attachAgentUsageTelemetry(logger, { store: deps.store, agentId: task.assignedAgentId ?? null, taskId: task.id, nodeId: task.effectiveNodeId ?? task.nodeId ?? null, model: executorModelId ?? null, provider: executorProvider ?? null, lane: "executor" });
const executorFallback = resolveExecutorFallbackModel(settings);
// Create the fix agent session
const { session } = await createResolvedAgentSession({
sessionPurpose: "executor",
taskExecutionSession: true,
pluginRunner: deps.pluginRunner,
cwd: worktreePath, // Run in the task's worktree
systemPrompt: `You are a verification fix agent running during task execution in a worktree.
All step-session steps completed successfully but the deterministic verification command failed. Your job is to fix the failing code directly in the working directory.
## Scope
Only fix what is required to make the failing verification pass.
Do not refactor, rename broadly, or make opportunistic improvements.
## Rules
1. Read the error output carefully to understand what is failing before editing anything
2. Before assuming a code fix is needed, check whether the failure is caused by stale/missing build artifacts in a sibling workspace package — typical signatures: \`Failed to resolve import "./X.js"\` pointing into another package's \`dist/\`, \`Cannot find module\`, or \`ERR_MODULE_NOT_FOUND\` referencing a workspace-internal path. In that case, rebuild the affected package(s) (e.g. \`pnpm --filter <pkg> build\`, or \`pnpm --filter "<scope>/*" build\` for a group) and re-run verification before editing source files.
3. Make targeted fixes to the failing code path
4. After fixing, run the verification command to confirm the fix works
5. Do NOT make any git commits — just fix the code
6. You MAY modify any files needed to make the verification pass, including files unrelated to this task's original change. Pre-existing build/test breakage is in scope: fix it. Prefer the smallest change that makes verification green.
7. If you cannot fix the issue within scope, explain why and what evidence indicates a deeper/root problem`,
tools: "coding",
onText: logger.onText,
onThinking: logger.onThinking,
onToolStart: logger.onToolStart,
onToolEnd: logger.onToolEnd,
defaultProvider: executorProvider,
defaultModelId: executorModelId,
...(executorSessionModel.credentialInstanceId ? { credentialInstanceId: executorSessionModel.credentialInstanceId } : {}),
fallbackProvider: executorFallback.provider,
fallbackModelId: executorFallback.modelId,
fallbackThinkingLevel: resolveExecutorFallbackThinkingLevel(task.thinkingLevel, settings),
defaultThinkingLevel: resolveExecutorThinkingLevel(task.thinkingLevel, settings),
runAuditor: createRunAuditor(deps.store, deps.getRunContextFor(task.id)),
settings,
taskEnv: extraEnv,
mcpServers: await deps.resolveMcpServers(undefined),
// FNXC:SessionRouting 2026-06-24-11:20:
// #1675: propagate task id so verification-fix requests carry the same
// X-Session-Id/X-Session-Affinity as the primary session.
taskId: task.id,
// FNXC:PluginSkills 2026-07-12-00:00: Verification-fix sessions share task skill selection; include plugin skill body dirs so fixes can use plugin-authored guidance.
...(skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}),
...(skillContext && skillContext.additionalSkillPaths.length > 0 ? { additionalSkillPaths: skillContext.additionalSkillPaths } : {}),
});
// FNXC:CommandCenterActivity 2026-08-15-22:15: session boundary for the verification-fix runtime session (restored post-wave-18).
emitAgentSessionStart({ store: deps.store, agentId: task.assignedAgentId ?? null, taskId: task.id, nodeId: task.effectiveNodeId ?? task.nodeId ?? null, model: executorModelId ?? null, provider: executorProvider ?? null, lane: "executor" });
await deps.store.logEntry(
task.id,
`Executor verification fix agent started (model: ${describeModel(session)}, attempt ${retryNumber}/${maxRetries})`,
undefined,
deps.getRunContextFor(task.id),
);
await deps.store.appendAgentLog(
task.id,
`Fix agent started (model: ${describeModel(session)}, attempt ${retryNumber}/${maxRetries})`,
"status",
undefined,
"executor",
);
try {
// Build the fix prompt
const fixPrompt = `Fix the failing ${failureContext.type} verification for task ${task.id}.
## Failed command
Command: \`${failureContext.command}\`
Exit code: ${failureContext.exitCode}
## Error output
${failureContext.output.slice(0, VERIFICATION_LOG_MAX_CHARS)}
## Instructions
1. Read the error output and identify the root cause
2. Make targeted fixes to resolve the failure
3. Run the verification command \`${failureContext.command}\` to confirm your fix works
4. If the fix doesn't work, try a different approach
5. Do NOT make any git commits`;
// Run the agent with rate limit retry
await withRateLimitRetry(async () => {
await promptWithFallback(session, fixPrompt);
}, {
onRetry: (attempt, delayMs, error) => {
const delaySec = Math.round(delayMs / 1000);
executorLog.warn(`⏳ ${task.id} executor fix agent rate limited — retry ${attempt} in ${delaySec}s: ${error.message}`);
},
});
await accumulateSessionTokenUsage(deps.store, task.id, session, {
agentId: task.assignedAgentId ?? undefined,
role: "executor",
});
// Re-run full deterministic verification (test AND build) after the fix attempt
executorLog.log(`${task.id}: re-running deterministic verification after fix attempt ${retryNumber}/${maxRetries}`);
await deps.store.logEntry(
task.id,
`Re-running deterministic verification (attempt ${retryNumber}/${maxRetries})`,
undefined,
deps.getRunContextFor(task.id),
);
await deps.store.appendAgentLog(
task.id,
`Re-running verification (attempt ${retryNumber}/${maxRetries})`,
"status",
undefined,
"executor",
);
const reRunResult = await deps.runExecutorDeterministicVerification(task, worktreePath, settings, extraEnv);
return reRunResult.allPassed;
} finally {
await logger.flush();
session.dispose();
}
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
executorLog.warn(`${task.id}: executor verification fix agent error: ${errorMessage}`);
await deps.store.logEntry(
task.id,
`Executor verification fix agent encountered an error`,
errorMessage,
deps.getRunContextFor(task.id),
);
await deps.store.appendAgentLog(
task.id,
"Fix agent encountered an error",
"tool_error",
errorMessage,
"executor",
);
return false;
}
}