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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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: {},
|
||||
|
||||
Reference in New Issue
Block a user