feat(FN-3935): align engineer role routing with explicit assignment and del

Centralized routing policy helpers in core and aligned both direct assignment and delegation to route engineer-role agents consistently, ensuring assigned tasks respect explicit engineer routing the same way delegated tasks do. Added comprehensive test coverage across core, CLI, dashboard, and engin

Fusion-Task-Id: FN-3935

Fusion-Task-Lineage: 069f4d54-f7a8-4bd3-a2c3-4de830de042f
This commit is contained in:
Fusion
2026-05-11 20:20:43 -07:00
committed by gsxdsm
parent 6fd19dd0db
commit 1f7958fc0f
18 changed files with 342 additions and 29 deletions

View File

@@ -256,7 +256,34 @@ describe("createDelegateTaskTool", () => {
expect(taskStore.createTask).not.toHaveBeenCalled();
});
it("rejects non-executor target without override", async () => {
it("allows durable engineer target without override", async () => {
const engineer = createAgent({ id: "agent-009", name: "Eli", role: "engineer" });
vi.mocked(agentStore.getAgent).mockResolvedValue(engineer);
vi.mocked(taskStore.createTask).mockResolvedValue({
id: "FN-053",
description: "Do something",
dependencies: [],
column: "todo" as const,
steps: [],
currentStep: 0,
log: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
});
const tool = createDelegateTaskTool(agentStore, taskStore);
await tool.execute("session-1", {
agent_id: "agent-009",
description: "Do something",
}, undefined as any, undefined as any, undefined as any);
expect(taskStore.createTask).toHaveBeenCalledWith(expect.objectContaining({
assignedAgentId: "agent-009",
source: { sourceType: "api" },
}), expect.anything());
});
it("rejects reviewer target without override", async () => {
const reviewer = createAgent({ id: "agent-002", name: "Rita", role: "reviewer" });
vi.mocked(agentStore.getAgent).mockResolvedValue(reviewer);

View File

@@ -18,7 +18,7 @@
*/
import type { AgentStore, AgentHeartbeatRun, HeartbeatInvocationSource, AgentHeartbeatConfig, AgentBudgetStatus, Message, MessageStore, TaskStore, TaskDetail, AgentRole, Agent, InboxTask, RunMutationContext, Settings, AgentConfigRevision, ReflectionStore } from "@fusion/core";
import { ApprovalRequestStore, buildExecutionMemoryInstructions, isEphemeralAgent, hasAgentIdentity, resolveEffectiveAgentPermissionPolicy, canAgentTakeImplementationTask } from "@fusion/core";
import { ApprovalRequestStore, buildExecutionMemoryInstructions, isEphemeralAgent, hasAgentIdentity, resolveEffectiveAgentPermissionPolicy, canAgentTakeImplementationTask, canAgentTakeImplementationTaskForExplicitRouting } from "@fusion/core";
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
import { Type, type Static } from "@mariozechner/pi-ai";
import { createHash } from "node:crypto";
@@ -1502,7 +1502,7 @@ export class HeartbeatMonitor {
if (!taskId) {
inboxSelection = await taskStore.selectNextTaskForAgent(agentId, { id: agent.id, role: agent.role });
if (inboxSelection && !canAgentTakeImplementationTask(agent, inboxSelection.task)) {
if (inboxSelection && !canAgentTakeImplementationTaskForExplicitRouting(agent, inboxSelection.task)) {
const hasRoleOverride = inboxSelection.task.sourceMetadata?.executorRoleOverride === true;
if (!hasRoleOverride) {
heartbeatLog.log(

View File

@@ -12,7 +12,7 @@ import { existsSync } from "node:fs";
import { createHash } from "node:crypto";
import { join, relative, resolve } from "node:path";
import type { AgentStore, AgentState, AgentCapability, AgentUpdateInput, TaskDocument, TaskDocumentCreateInput, TaskStore, RunMutationContext, MessageStore, Message, SourceType, Settings, ResearchRun, ResearchRunStatus, TaskCreateInput, ReflectionStore, ApprovalRequestStore, ProjectSettings } from "@fusion/core";
import { DASHBOARD_USER_ID, canAgentTakeImplementationTask, dailyMemoryPath, ensureOpenClawMemoryFiles, extractAgentProvisioningRequest, formatRoleMismatchReason, getMemoryBackendCapabilities, getProjectMemory, isEphemeralAgent, memoryLongTermPath, normalizeMessageParticipant, resolveAgentProvisioningPolicy, resolveMemoryBackend, resolveResearchSettings, resolveTitleSummarizerSettingsModel, scheduleQmdProjectMemoryRefresh, searchProjectMemory, shouldSkipBackgroundQmdRefresh, summarizeTitle } from "@fusion/core";
import { DASHBOARD_USER_ID, canAgentTakeImplementationTaskForExplicitRouting, dailyMemoryPath, ensureOpenClawMemoryFiles, extractAgentProvisioningRequest, formatRoleMismatchReason, getMemoryBackendCapabilities, getProjectMemory, isEphemeralAgent, memoryLongTermPath, normalizeMessageParticipant, resolveAgentProvisioningPolicy, resolveMemoryBackend, resolveResearchSettings, resolveTitleSummarizerSettingsModel, scheduleQmdProjectMemoryRefresh, searchProjectMemory, shouldSkipBackgroundQmdRefresh, summarizeTitle } from "@fusion/core";
import { ResearchOrchestrator } from "./research-orchestrator.js";
import { ResearchProviderRegistry } from "./research/provider-registry.js";
import { ResearchStepRunner } from "./research-step-runner.js";
@@ -1754,7 +1754,7 @@ export function createDelegateTaskTool(
const override = params.override === true;
const newTaskRef = { id: "<new>", column: "todo" } as const;
if (!override && !canAgentTakeImplementationTask(agent, { column: newTaskRef.column })) {
if (!override && !canAgentTakeImplementationTaskForExplicitRouting(agent, { column: newTaskRef.column })) {
return {
content: [{ type: "text" as const, text: `ERROR: ${formatRoleMismatchReason(agent, newTaskRef)}` }],
details: {},