Files
fusion/packages/core/src/workflow-ir-resolver.ts
gsxdsm fb2c6e5098 FN-6235: source reviewer prompts from workflow IR
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
2026-06-11 21:00:23 -07:00

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);
}