Deduplicate reviewer policy text by making workflow IR review seams the engine's built-in prompt source. - Move the canonical built-in reviewer prompt into core agent prompts and export seam prompt resolution helpers. - Resolve reviewer prompts from explicit role overrides first, then workflow IR review seams, with the built-in prompt as a fallback. - Update prompt cache and reviewer tests to cover single-source reviewer prompt behavior. - Add a patch changeset for the published Fusion package. Files changed: .../FN-6235-reviewer-prompt-single-source.md | 5 + packages/core/src/__tests__/agent-prompts.test.ts | 8 +- packages/core/src/agent-prompts.ts | 109 +++++++++-- packages/core/src/index.ts | 2 + packages/core/src/workflow-ir-resolver.ts | 31 ++- .../src/__tests__/prompt-cache-integration.test.ts | 10 +- .../reviewer-prompt-single-source.test.ts | 160 +++++++++++++++ packages/engine/src/__tests__/reviewer.test.ts | 97 ++++----- packages/engine/src/prompt-layers.ts | 2 +- packages/engine/src/reviewer.ts | 217 ++------------------- 10 files changed, 370 insertions(+), 271 deletions(-) Fusion-Task-Id: FN-6235 Fusion-Task-Lineage: 725932d1-2469-4507-a0f9-0946c83a1572
127 lines
4.6 KiB
TypeScript
127 lines
4.6 KiB
TypeScript
/**
|
|
* Single source of truth for the workflow-IR resolution rule.
|
|
*
|
|
* The selection → builtin/custom → default-fallback rule was independently
|
|
* reimplemented in engine/hold-release.ts, engine/merge-trait.ts,
|
|
* engine/plugin-runner.ts (which bypassed the public API via getDatabase()),
|
|
* and dashboard/board-workflows.ts, with behavioral divergence already creeping
|
|
* in (GitHub #1402). This module consolidates the read-only resolution into one
|
|
* pair of helpers built on the *public* store surface so every call site shares
|
|
* one implementation.
|
|
*
|
|
* A missing/corrupt definition degrades to the built-in default workflow so
|
|
* resolution never throws. The store-private, txn-hot `resolveTaskWorkflowIrSync`
|
|
* stays separate by design.
|
|
*/
|
|
|
|
import { getBuiltinWorkflow, isBuiltinWorkflowId } from "./builtin-workflows.js";
|
|
import { BUILTIN_CODING_WORKFLOW_IR } from "./builtin-coding-workflow-ir.js";
|
|
import { parseWorkflowIr } from "./workflow-ir.js";
|
|
import type { WorkflowIr } from "./workflow-ir-types.js";
|
|
|
|
/** Minimal store surface the resolver needs (public APIs only). */
|
|
export interface WorkflowIrResolverStore {
|
|
getTaskWorkflowSelection(taskId: string): { workflowId: string; stepIds: string[] } | undefined;
|
|
getWorkflowDefinition(id: string): Promise<{ ir: string | WorkflowIr } | undefined>;
|
|
}
|
|
|
|
/**
|
|
* Extract a prompt seam's prompt text from a resolved workflow IR.
|
|
*
|
|
* Seam prompt nodes are prompt nodes with `config.seam === seam`;
|
|
* `config.prompt` carries the text installed by builtinPromptConfig or a custom
|
|
* workflow author. Empty/missing prompts return undefined so callers can apply
|
|
* their own fail-soft fallback.
|
|
*/
|
|
export function resolveSeamPromptFromIr(ir: WorkflowIr, seam: string): string | undefined {
|
|
for (const node of ir.nodes) {
|
|
if (node.kind !== "prompt") continue;
|
|
if (node.config?.seam !== seam) continue;
|
|
const prompt = node.config.prompt;
|
|
if (typeof prompt === "string" && prompt.trim().length > 0) return prompt;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/** Extract the planning seam prompt from a resolved workflow IR. */
|
|
export function resolvePlanningPromptFromIr(ir: WorkflowIr): string | undefined {
|
|
return resolveSeamPromptFromIr(ir, "planning");
|
|
}
|
|
|
|
/** Resolve a task's seam prompt via its selected workflow IR. */
|
|
export async function resolveTaskSeamPrompt(
|
|
store: WorkflowIrResolverStore,
|
|
taskId: string,
|
|
seam: string,
|
|
irCache?: Map<string, WorkflowIr>,
|
|
): Promise<string | undefined> {
|
|
try {
|
|
const ir = await resolveWorkflowIrForTask(store, taskId, irCache);
|
|
return resolveSeamPromptFromIr(ir, seam);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/** Resolve a task's planning seam prompt via its selected workflow IR. */
|
|
export async function resolveTaskPlanningPrompt(
|
|
store: WorkflowIrResolverStore,
|
|
taskId: string,
|
|
irCache?: Map<string, WorkflowIr>,
|
|
): Promise<string | undefined> {
|
|
return resolveTaskSeamPrompt(store, taskId, "planning", irCache);
|
|
}
|
|
|
|
/**
|
|
* Resolve a workflow IR by its id (built-in or custom).
|
|
*
|
|
* @param irCache optional cache keyed by workflowId so each distinct workflow's
|
|
* IR (and its definition fetch) is resolved at most once per caller-scoped
|
|
* sweep. Hits short-circuit before any builtin/db lookup.
|
|
*/
|
|
export async function resolveWorkflowIrById(
|
|
store: Pick<WorkflowIrResolverStore, "getWorkflowDefinition">,
|
|
workflowId: string,
|
|
irCache?: Map<string, WorkflowIr>,
|
|
): Promise<WorkflowIr> {
|
|
const cached = irCache?.get(workflowId);
|
|
if (cached) return cached;
|
|
|
|
if (isBuiltinWorkflowId(workflowId)) {
|
|
const builtin = getBuiltinWorkflow(workflowId);
|
|
const ir = builtin?.ir ?? BUILTIN_CODING_WORKFLOW_IR;
|
|
const resolved = typeof ir === "string" ? parseWorkflowIr(ir) : ir;
|
|
irCache?.set(workflowId, resolved);
|
|
return resolved;
|
|
}
|
|
|
|
try {
|
|
const def = await store.getWorkflowDefinition(workflowId);
|
|
if (!def) return BUILTIN_CODING_WORKFLOW_IR;
|
|
const ir = typeof def.ir === "string" ? parseWorkflowIr(def.ir) : def.ir;
|
|
irCache?.set(workflowId, ir);
|
|
return ir;
|
|
} catch {
|
|
return BUILTIN_CODING_WORKFLOW_IR;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve a task's workflow IR via its selection. A null/absent selection or any
|
|
* lookup failure degrades to the built-in default workflow.
|
|
*/
|
|
export async function resolveWorkflowIrForTask(
|
|
store: WorkflowIrResolverStore,
|
|
taskId: string,
|
|
irCache?: Map<string, WorkflowIr>,
|
|
): Promise<WorkflowIr> {
|
|
let workflowId: string | undefined;
|
|
try {
|
|
workflowId = store.getTaskWorkflowSelection(taskId)?.workflowId;
|
|
} catch {
|
|
return BUILTIN_CODING_WORKFLOW_IR;
|
|
}
|
|
if (!workflowId) return BUILTIN_CODING_WORKFLOW_IR;
|
|
return resolveWorkflowIrById(store, workflowId, irCache);
|
|
}
|