FN-6622: wire dashboard interviews to session skills

Dashboard interview lanes now request the same role-fallback and enabled plugin skills as agent-acting sessions.

- Add skillSelection to agent onboarding and milestone/slice interview agent creation.
- Thread the plugin runner through mission interview and onboarding routes.
- Cover enabled and disabled plugin skill selection behavior in dashboard tests.
- Document the newly covered dashboard interview lanes and add a minor changeset.

Files changed:
 .../fn-6622-session-skill-interview-lanes.md       |  5 ++
 docs/agents.md                                     |  2 +-
 .../src/__tests__/agent-onboarding.test.ts         | 68 ++++++++++++++++++++++
 .../__tests__/milestone-slice-interview.test.ts    | 66 +++++++++++++++++++++
 packages/dashboard/src/agent-onboarding.ts         | 10 +++-
 .../dashboard/src/milestone-slice-interview.ts     | 39 ++++++++++---
 packages/dashboard/src/mission-routes.ts           | 10 ++--
 ...gister-agent-import-export-generation-routes.ts |  3 +-
 8 files changed, 187 insertions(+), 16 deletions(-)

Fusion-Task-Id: FN-6622

Fusion-Task-Lineage: 5e3dded1-1866-4b76-8302-253e5b3867fa
This commit is contained in:
gsxdsm
2026-06-17 22:21:34 -07:00
parent 29b27a7ebb
commit cdadac1662
8 changed files with 187 additions and 16 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": minor
---
Load selected Fusion and enabled plugin skills in milestone/slice interview and agent-onboarding dashboard sessions.

View File

@@ -24,7 +24,7 @@ fn chat <agent-id> [message…] [--once] [--non-interactive] [--poll-ms <n>]
- Each message is stored as a `user-to-agent` MessageStore message from `cli` with `metadata.wakeRecipient=true`. - Each message is stored as a `user-to-agent` MessageStore message from `cli` with `metadata.wakeRecipient=true`.
- Agent replies are polled from your inbox and printed as they arrive. - Agent replies are polled from your inbox and printed as they arrive.
- Dashboard-created agent chat sessions request the target agent's declared `metadata.skills` plus enabled plugin-contributed skills, so skills such as `ce-debug` are available in chat when the contributing plugin is enabled. Model-only QuickChat sessions request enabled plugin skills, and room responder sessions request the responder agent's skills. - Dashboard-created agent chat sessions request the target agent's declared `metadata.skills` plus enabled plugin-contributed skills, so skills such as `ce-debug` are available in chat when the contributing plugin is enabled. Model-only QuickChat sessions request enabled plugin skills, and room responder sessions request the responder agent's skills.
- Agent-acting session lanes share the same skill-injection contract as executor sessions: executor, merger, triage, reviewer, heartbeat, step-session, dashboard chat/room responders, CLI agent execution, planning, mission interview, workflow design, memory dreams/insight extraction, and scheduled cron automation all request agent/fallback skills plus enabled plugin-contributed skills when a plugin runner is available. Utility-only lanes that only summarize/extract/generate JSON (title/PR summaries, memory compaction, subtask breakdown, text refinement, agent generation, PR metadata generation, evaluator/research synthesis, and similar one-shot helpers) intentionally stay exempt to avoid loading skills where no agent-style tool loop can use them. - Agent-acting session lanes share the same skill-injection contract as executor sessions: executor, merger, triage, reviewer, heartbeat, step-session, dashboard chat/room responders, CLI agent execution, planning, mission interview, milestone/slice interview, agent-onboarding interview, workflow design, memory dreams/insight extraction, and scheduled cron automation all request agent/fallback skills plus enabled plugin-contributed skills when a plugin runner is available. Utility-only lanes that only summarize/extract/generate JSON (title/PR summaries, memory compaction, subtask breakdown, text refinement, agent generation, PR metadata generation, evaluator/research synthesis, and similar one-shot helpers) intentionally stay exempt to avoid loading skills where no agent-style tool loop can use them.
- In dashboard model-loop chat (main chat, QuickChat, and room responders), typing `/skill:{name}` requests that skill for the current AI session and strips the slash token from the prompt sent to the model. The requested skill is still subject to the normal enabled/disabled execution-skill filters; CLI-agent-backed PTY chat keeps raw terminal input semantics and does not interpret this command. - In dashboard model-loop chat (main chat, QuickChat, and room responders), typing `/skill:{name}` requests that skill for the current AI session and strips the slash token from the prompt sent to the model. The requested skill is still subject to the normal enabled/disabled execution-skill filters; CLI-agent-backed PTY chat keeps raw terminal input semantics and does not interpret this command.
### Flags ### Flags

View File

@@ -6,6 +6,21 @@ const { mockCreateFnAgent } = vi.hoisted(() => ({
vi.mock("@fusion/engine", () => ({ vi.mock("@fusion/engine", () => ({
listCliAdapterDescriptors: () => [], listCliAdapterDescriptors: () => [],
buildSessionSkillContextSync: (_agent: unknown, sessionPurpose: string, projectRootDir: string, pluginRunner?: { getPluginSkills?: () => Array<{ pluginId: string; skill: { name: string; enabled?: boolean } }> }) => {
const requestedSkillNames = ["fusion"];
for (const contribution of pluginRunner?.getPluginSkills?.() ?? []) {
const name = contribution.skill.name.trim();
if (contribution.skill.enabled === false || name.length === 0 || requestedSkillNames.includes(name)) {
continue;
}
requestedSkillNames.push(name);
}
return {
skillSelectionContext: { projectRootDir, requestedSkillNames, sessionPurpose },
resolvedSkillNames: requestedSkillNames,
skillSource: "role-fallback" as const,
};
},
createFnAgent: mockCreateFnAgent, createFnAgent: mockCreateFnAgent,
})); }));
@@ -47,6 +62,12 @@ async function waitFor(check: () => boolean, timeoutMs = 2000): Promise<void> {
} }
} }
function createSkillPluginRunner(skills: Array<{ name: string; enabled?: boolean }>) {
return {
getPluginSkills: () => skills.map((skill) => ({ pluginId: "fusion-plugin-compound-engineering", skill })),
};
}
describe("agent-onboarding", () => { describe("agent-onboarding", () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
@@ -246,6 +267,53 @@ describe("agent-onboarding", () => {
expect(prompt).toContain("messageResponseMode: immediate"); expect(prompt).toContain("messageResponseMode: immediate");
}); });
it("requests role-fallback and enabled plugin skills for model-only onboarding agents", async () => {
mockCreateFnAgent.mockResolvedValueOnce(
createMockAgent([
JSON.stringify({
type: "question",
data: { id: "goal", type: "text", question: "What is the primary goal?" },
}),
]),
);
await startAgentOnboardingSession(
"127.0.0.1",
{ intent: "skills", existingAgents: [], templates: [] },
process.cwd(),
undefined,
undefined,
undefined,
createSkillPluginRunner([
{ name: "ce-debug" },
{ name: "disabled-skill", enabled: false },
]),
);
const options = mockCreateFnAgent.mock.calls.at(-1)?.[0] as { skillSelection?: { requestedSkillNames?: string[] } };
expect(options.skillSelection?.requestedSkillNames).toEqual(["fusion", "ce-debug"]);
});
it("requests role-fallback skills when onboarding plugin runner is unavailable", async () => {
mockCreateFnAgent.mockResolvedValueOnce(
createMockAgent([
JSON.stringify({
type: "question",
data: { id: "goal", type: "text", question: "What is the primary goal?" },
}),
]),
);
await startAgentOnboardingSession(
"127.0.0.1",
{ intent: "skills", existingAgents: [], templates: [] },
process.cwd(),
);
const options = mockCreateFnAgent.mock.calls.at(-1)?.[0] as { skillSelection?: { requestedSkillNames?: string[] } };
expect(options.skillSelection?.requestedSkillNames).toEqual(["fusion"]);
});
it("progresses through start -> question -> response -> final summary", async () => { it("progresses through start -> question -> response -> final summary", async () => {
mockCreateFnAgent.mockResolvedValueOnce( mockCreateFnAgent.mockResolvedValueOnce(
createMockAgent([ createMockAgent([

View File

@@ -8,6 +8,21 @@ const { mockCreateFnAgent } = vi.hoisted(() => ({
vi.mock("@fusion/engine", () => ({ vi.mock("@fusion/engine", () => ({
listCliAdapterDescriptors: () => [], listCliAdapterDescriptors: () => [],
buildSessionSkillContextSync: (_agent: unknown, sessionPurpose: string, projectRootDir: string, pluginRunner?: { getPluginSkills?: () => Array<{ pluginId: string; skill: { name: string; enabled?: boolean } }> }) => {
const requestedSkillNames = ["fusion"];
for (const contribution of pluginRunner?.getPluginSkills?.() ?? []) {
const name = contribution.skill.name.trim();
if (contribution.skill.enabled === false || name.length === 0 || requestedSkillNames.includes(name)) {
continue;
}
requestedSkillNames.push(name);
}
return {
skillSelectionContext: { projectRootDir, requestedSkillNames, sessionPurpose },
resolvedSkillNames: requestedSkillNames,
skillSource: "role-fallback" as const,
};
},
createFnAgent: mockCreateFnAgent, createFnAgent: mockCreateFnAgent,
})); }));
@@ -140,6 +155,23 @@ async function waitForCurrentQuestion(sessionId: string): Promise<void> {
throw new Error("Timed out waiting for currentQuestion"); throw new Error("Timed out waiting for currentQuestion");
} }
async function waitForCreateFnAgentOptions(): Promise<{ skillSelection?: { requestedSkillNames?: string[] } }> {
for (let i = 0; i < 50; i++) {
const options = mockCreateFnAgent.mock.calls.at(-1)?.[0];
if (options) {
return options;
}
await new Promise((resolve) => setTimeout(resolve, 5));
}
throw new Error("Timed out waiting for createFnAgent options");
}
function createSkillPluginRunner(skills: Array<{ name: string; enabled?: boolean }>) {
return {
getPluginSkills: () => skills.map((skill) => ({ pluginId: "fusion-plugin-compound-engineering", skill })),
};
}
class MockAiSessionStore extends EventEmitter { class MockAiSessionStore extends EventEmitter {
rows = new Map<string, AiSessionRow>(); rows = new Map<string, AiSessionRow>();
@@ -235,6 +267,40 @@ describe("milestone-slice-interview module", () => {
}); });
describe("session lifecycle", () => { describe("session lifecycle", () => {
it("requests role-fallback and enabled plugin skills for model-only milestone/slice interview agents", async () => {
await createTargetInterviewSession(
"127.0.0.1",
"milestone",
"ms-skills",
"Skillful Milestone",
undefined,
"/tmp/project",
MOCK_TASK_STORE,
createSkillPluginRunner([
{ name: "ce-debug" },
{ name: "disabled-skill", enabled: false },
]),
);
const options = await waitForCreateFnAgentOptions();
expect(options.skillSelection?.requestedSkillNames).toEqual(["fusion", "ce-debug"]);
});
it("requests role-fallback skills when milestone/slice plugin runner is unavailable", async () => {
await createTargetInterviewSession(
"127.0.0.1",
"slice",
"sl-skills",
"Fallback Slice",
undefined,
"/tmp/project",
MOCK_TASK_STORE,
);
const options = await waitForCreateFnAgentOptions();
expect(options.skillSelection?.requestedSkillNames).toEqual(["fusion"]);
});
it("creates, retrieves, and cleans up a milestone session", async () => { it("creates, retrieves, and cleans up a milestone session", async () => {
const sessionId = await createTargetInterviewSession( const sessionId = await createTargetInterviewSession(
"127.0.0.1", "127.0.0.1",

View File

@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
import { EventEmitter } from "node:events"; import { EventEmitter } from "node:events";
import type { AgentCapability, PlanningQuestion } from "@fusion/core"; import type { AgentCapability, PlanningQuestion } from "@fusion/core";
import { resolvePrompt, type PromptOverrideMap } from "@fusion/core"; import { resolvePrompt, type PromptOverrideMap } from "@fusion/core";
import { createFnAgent as engineCreateFnAgent } from "@fusion/engine"; import { buildSessionSkillContextSync, createFnAgent as engineCreateFnAgent } from "@fusion/engine";
import { SessionEventBuffer, type SessionBufferedEvent } from "./sse-buffer.js"; import { SessionEventBuffer, type SessionBufferedEvent } from "./sse-buffer.js";
export interface AgentOnboardingSummary { export interface AgentOnboardingSummary {
@@ -61,6 +61,7 @@ export type AgentOnboardingStreamEvent =
export type AgentOnboardingStreamCallback = (event: AgentOnboardingStreamEvent, eventId?: number) => void; export type AgentOnboardingStreamCallback = (event: AgentOnboardingStreamEvent, eventId?: number) => void;
const createFnAgent: typeof engineCreateFnAgent = engineCreateFnAgent; const createFnAgent: typeof engineCreateFnAgent = engineCreateFnAgent;
type SkillSelectionPluginRunner = Parameters<typeof buildSessionSkillContextSync>[3];
const SESSION_TTL_MS = 30 * 60 * 1000; const SESSION_TTL_MS = 30 * 60 * 1000;
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; const CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
const GENERATION_TIMEOUT_MS = 120_000; const GENERATION_TIMEOUT_MS = 120_000;
@@ -281,6 +282,7 @@ export async function startAgentOnboardingSession(
modelProvider?: string, modelProvider?: string,
modelId?: string, modelId?: string,
promptOverrides?: PromptOverrideMap, promptOverrides?: PromptOverrideMap,
pluginRunner?: SkillSelectionPluginRunner,
): Promise<string> { ): Promise<string> {
const id = randomUUID(); const id = randomUUID();
const mode: OnboardingMode = initialContext.mode ?? "create"; const mode: OnboardingMode = initialContext.mode ?? "create";
@@ -303,11 +305,17 @@ export async function startAgentOnboardingSession(
sessions.set(id, session); sessions.set(id, session);
const systemPrompt = resolvePrompt("agent-onboarding-system", promptOverrides) || AGENT_ONBOARDING_SYSTEM_PROMPT; const systemPrompt = resolvePrompt("agent-onboarding-system", promptOverrides) || AGENT_ONBOARDING_SYSTEM_PROMPT;
const skillContext = buildSessionSkillContextSync(null, "executor", rootDir, pluginRunner);
session.agent = await createFnAgent({ session.agent = await createFnAgent({
cwd: rootDir, cwd: rootDir,
systemPrompt, systemPrompt,
tools: "readonly", tools: "readonly",
...(modelProvider && modelId ? { defaultProvider: modelProvider, defaultModelId: modelId } : {}), ...(modelProvider && modelId ? { defaultProvider: modelProvider, defaultModelId: modelId } : {}),
/*
FNXC:InterviewSkills 2026-06-17-21:53:
Agent onboarding is a model-only dashboard interview lane, so it must request executor role-fallback skills plus enabled plugin skills such as ce-debug like other agent-acting sessions.
*/
...(skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}),
onThinking: (delta: string) => { onThinking: (delta: string) => {
session.thinkingOutput += delta; session.thinkingOutput += delta;
agentOnboardingStreamManager.broadcast(session.id, { type: "thinking", data: delta }); agentOnboardingStreamManager.broadcast(session.id, { type: "thinking", data: delta });

View File

@@ -96,11 +96,12 @@ function parseTargetInterviewResponseImpl(text: string): TargetInterviewResponse
// Export the parse function for tests // Export the parse function for tests
export { parseTargetInterviewResponseImpl as parseTargetInterviewResponse }; export { parseTargetInterviewResponseImpl as parseTargetInterviewResponse };
import { createFnAgent as engineCreateFnAgent } from "@fusion/engine"; import { buildSessionSkillContextSync, createFnAgent as engineCreateFnAgent } from "@fusion/engine";
import { createPlanningBoardTools } from "./planning-board-tools.js"; import { createPlanningBoardTools } from "./planning-board-tools.js";
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
type AgentResult = any; type AgentResult = any;
type SkillSelectionPluginRunner = Parameters<typeof buildSessionSkillContextSync>[3];
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const createFnAgent: any = engineCreateFnAgent; const createFnAgent: any = engineCreateFnAgent;
@@ -730,14 +731,21 @@ async function createTargetInterviewAgent(
session: TargetInterviewSession, session: TargetInterviewSession,
rootDir: string, rootDir: string,
store: TaskStore, store: TaskStore,
pluginRunner?: SkillSelectionPluginRunner,
): Promise<AgentResult> { ): Promise<AgentResult> {
await ensureEngineReady(); await ensureEngineReady();
const skillContext = buildSessionSkillContextSync(null, "executor", rootDir, pluginRunner);
return createFnAgent({ return createFnAgent({
cwd: rootDir, cwd: rootDir,
systemPrompt: getSystemPrompt(session.targetType), systemPrompt: getSystemPrompt(session.targetType),
tools: "readonly", tools: "readonly",
customTools: [...createPlanningBoardTools(store)], customTools: [...createPlanningBoardTools(store)],
/*
FNXC:InterviewSkills 2026-06-17-21:42:
Milestone and slice interview agents are model-only tool-loop sessions, so they must request executor role-fallback skills plus enabled plugin skills such as ce-debug instead of creating skill-less dashboard sessions.
*/
...(skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}),
onThinking: (delta: string) => { onThinking: (delta: string) => {
session.thinkingOutput += delta; session.thinkingOutput += delta;
persistThinking(session.id, session.thinkingOutput); persistThinking(session.id, session.thinkingOutput);
@@ -787,6 +795,7 @@ async function ensureInterviewAgent(
rootDir: string | undefined, rootDir: string | undefined,
store: TaskStore | undefined, store: TaskStore | undefined,
historyForReplay: Array<{ question: PlanningQuestion; response: unknown }>, historyForReplay: Array<{ question: PlanningQuestion; response: unknown }>,
pluginRunner?: SkillSelectionPluginRunner,
): Promise<void> { ): Promise<void> {
if (session.agent) { if (session.agent) {
return; return;
@@ -804,7 +813,7 @@ async function ensureInterviewAgent(
); );
} }
session.agent = await createTargetInterviewAgent(session, rootDir, store); session.agent = await createTargetInterviewAgent(session, rootDir, store, pluginRunner);
if (historyForReplay.length === 0) { if (historyForReplay.length === 0) {
return; return;
@@ -841,9 +850,14 @@ async function ensureInterviewAgent(
/** /**
* Initialize the AI agent for a session and start the first turn. * Initialize the AI agent for a session and start the first turn.
*/ */
async function initializeAgent(session: TargetInterviewSession, rootDir: string, store: TaskStore): Promise<void> { async function initializeAgent(
session: TargetInterviewSession,
rootDir: string,
store: TaskStore,
pluginRunner?: SkillSelectionPluginRunner,
): Promise<void> {
try { try {
session.agent = await createTargetInterviewAgent(session, rootDir, store); session.agent = await createTargetInterviewAgent(session, rootDir, store, pluginRunner);
session.updatedAt = new Date(); session.updatedAt = new Date();
// Send initial message to get first question // Send initial message to get first question
@@ -1025,6 +1039,7 @@ export async function createTargetInterviewSession(
missionContext: string | undefined, missionContext: string | undefined,
rootDir: string, rootDir: string,
store: TaskStore, store: TaskStore,
pluginRunner?: SkillSelectionPluginRunner,
): Promise<string> { ): Promise<string> {
if (!checkRateLimit(ip)) { if (!checkRateLimit(ip)) {
const resetTime = getRateLimitResetTime(ip); const resetTime = getRateLimitResetTime(ip);
@@ -1054,7 +1069,7 @@ export async function createTargetInterviewSession(
persistSession(session, "generating"); persistSession(session, "generating");
// Initialize AI agent in background // Initialize AI agent in background
initializeAgent(session, rootDir, store).catch((err) => { initializeAgent(session, rootDir, store, pluginRunner).catch((err) => {
diagnostics.errorFromException("Failed to initialize agent for session", err, { sessionId, operation: "initialize-agent" }); diagnostics.errorFromException("Failed to initialize agent for session", err, { sessionId, operation: "initialize-agent" });
persistSession(session, "error", err.message || "Failed to initialize AI agent"); persistSession(session, "error", err.message || "Failed to initialize AI agent");
milestoneSliceInterviewStreamManager.broadcast(sessionId, { milestoneSliceInterviewStreamManager.broadcast(sessionId, {
@@ -1074,6 +1089,7 @@ export async function submitTargetInterviewResponse(
responses: Record<string, unknown>, responses: Record<string, unknown>,
rootDir?: string, rootDir?: string,
store?: TaskStore, store?: TaskStore,
pluginRunner?: SkillSelectionPluginRunner,
): Promise<TargetInterviewResponse> { ): Promise<TargetInterviewResponse> {
const session = getTargetInterviewSession(sessionId); const session = getTargetInterviewSession(sessionId);
if (!session) { if (!session) {
@@ -1095,7 +1111,7 @@ export async function submitTargetInterviewResponse(
if (!session.agent) { if (!session.agent) {
const replayHistory = session.history.slice(0, -1); const replayHistory = session.history.slice(0, -1);
await ensureInterviewAgent(session, rootDir, store, replayHistory); await ensureInterviewAgent(session, rootDir, store, replayHistory, pluginRunner);
} }
const message = formatResponseForAgent(session.currentQuestion, responses); const message = formatResponseForAgent(session.currentQuestion, responses);
@@ -1122,7 +1138,12 @@ export async function submitTargetInterviewResponse(
/** /**
* Retry a failed interview session. * Retry a failed interview session.
*/ */
export async function retryTargetInterviewSession(sessionId: string, rootDir: string, store?: TaskStore): Promise<void> { export async function retryTargetInterviewSession(
sessionId: string,
rootDir: string,
store?: TaskStore,
pluginRunner?: SkillSelectionPluginRunner,
): Promise<void> {
const session = getTargetInterviewSession(sessionId); const session = getTargetInterviewSession(sessionId);
if (!session) { if (!session) {
throw new TargetSessionNotFoundError(`Interview session ${sessionId} not found or expired`); throw new TargetSessionNotFoundError(`Interview session ${sessionId} not found or expired`);
@@ -1149,7 +1170,7 @@ export async function retryTargetInterviewSession(sessionId: string, rootDir: st
persistSession(session, "generating"); persistSession(session, "generating");
if (session.history.length === 0) { if (session.history.length === 0) {
await ensureInterviewAgent(session, rootDir, store, []); await ensureInterviewAgent(session, rootDir, store, [], pluginRunner);
await continueAgentConversation( await continueAgentConversation(
session, session,
`I want to refine the scope for this ${session.targetType}: "${session.targetTitle}".` + `I want to refine the scope for this ${session.targetType}: "${session.targetTitle}".` +
@@ -1162,7 +1183,7 @@ export async function retryTargetInterviewSession(sessionId: string, rootDir: st
const replayHistory = session.history.slice(0, -1); const replayHistory = session.history.slice(0, -1);
const lastEntry = session.history[session.history.length - 1]; const lastEntry = session.history[session.history.length - 1];
await ensureInterviewAgent(session, rootDir, store, replayHistory); await ensureInterviewAgent(session, rootDir, store, replayHistory, pluginRunner);
const replayMessage = formatResponseForAgent( const replayMessage = formatResponseForAgent(
lastEntry.question, lastEntry.question,
coerceResponseRecord(lastEntry.question, lastEntry.response), coerceResponseRecord(lastEntry.question, lastEntry.response),

View File

@@ -3294,6 +3294,7 @@ export function createMissionRouter(
missionContext, missionContext,
rootDir, rootDir,
scopedStore, scopedStore,
pluginRunner,
); );
res.status(201).json({ sessionId }); res.status(201).json({ sessionId });
} catch (err: unknown) { } catch (err: unknown) {
@@ -3347,7 +3348,7 @@ export function createMissionRouter(
const { store: scopedStore } = await getProjectContext(req); const { store: scopedStore } = await getProjectContext(req);
const rootDir = scopedStore.getRootDir(); const rootDir = scopedStore.getRootDir();
const result = await submitTargetInterviewResponse(sessionId, responses, rootDir, scopedStore); const result = await submitTargetInterviewResponse(sessionId, responses, rootDir, scopedStore, pluginRunner);
res.json(result); res.json(result);
} catch (err: unknown) { } catch (err: unknown) {
const errName = err instanceof Error ? err.name : ""; const errName = err instanceof Error ? err.name : "";
@@ -3511,7 +3512,7 @@ export function createMissionRouter(
const { store: scopedStore } = await getProjectContext(req); const { store: scopedStore } = await getProjectContext(req);
const rootDir = scopedStore.getRootDir(); const rootDir = scopedStore.getRootDir();
await retryTargetInterviewSession(sessionId, rootDir, scopedStore); await retryTargetInterviewSession(sessionId, rootDir, scopedStore, pluginRunner);
res.json({ success: true, sessionId }); res.json({ success: true, sessionId });
} catch (err: unknown) { } catch (err: unknown) {
const errName = err instanceof Error ? err.name : ""; const errName = err instanceof Error ? err.name : "";
@@ -3642,6 +3643,7 @@ export function createMissionRouter(
missionContext, missionContext,
rootDir, rootDir,
scopedStore, scopedStore,
pluginRunner,
); );
res.status(201).json({ sessionId }); res.status(201).json({ sessionId });
} catch (err: unknown) { } catch (err: unknown) {
@@ -3695,7 +3697,7 @@ export function createMissionRouter(
const { store: scopedStore } = await getProjectContext(req); const { store: scopedStore } = await getProjectContext(req);
const rootDir = scopedStore.getRootDir(); const rootDir = scopedStore.getRootDir();
const result = await submitTargetInterviewResponse(sessionId, responses, rootDir, scopedStore); const result = await submitTargetInterviewResponse(sessionId, responses, rootDir, scopedStore, pluginRunner);
res.json(result); res.json(result);
} catch (err: unknown) { } catch (err: unknown) {
const errName = err instanceof Error ? err.name : ""; const errName = err instanceof Error ? err.name : "";
@@ -3859,7 +3861,7 @@ export function createMissionRouter(
const { store: scopedStore } = await getProjectContext(req); const { store: scopedStore } = await getProjectContext(req);
const rootDir = scopedStore.getRootDir(); const rootDir = scopedStore.getRootDir();
await retryTargetInterviewSession(sessionId, rootDir, scopedStore); await retryTargetInterviewSession(sessionId, rootDir, scopedStore, pluginRunner);
res.json({ success: true, sessionId }); res.json({ success: true, sessionId });
} catch (err: unknown) { } catch (err: unknown) {
const errName = err instanceof Error ? err.name : ""; const errName = err instanceof Error ? err.name : "";

View File

@@ -875,7 +875,7 @@ async function persistImportedSkills(
} }
export function registerAgentGenerationRoutes(ctx: ApiRoutesContext): void { export function registerAgentGenerationRoutes(ctx: ApiRoutesContext): void {
const { router, getProjectContext, rethrowAsApiError } = ctx; const { router, getProjectContext, rethrowAsApiError, options } = ctx;
const agentGenerationDiagnostics = createSessionDiagnostics("agent-generation"); const agentGenerationDiagnostics = createSessionDiagnostics("agent-generation");
router.post("/agents/onboarding/start-streaming", async (req, res) => { router.post("/agents/onboarding/start-streaming", async (req, res) => {
@@ -922,6 +922,7 @@ export function registerAgentGenerationRoutes(ctx: ApiRoutesContext): void {
planningModelProvider, planningModelProvider,
planningModelId, planningModelId,
settings.promptOverrides, settings.promptOverrides,
options?.pluginRunner as Parameters<typeof import("@fusion/engine").buildSessionSkillContextSync>[3],
); );
res.status(201).json({ sessionId }); res.status(201).json({ sessionId });