From e4f4f431c819da0c59b3e42ca561e4ccbd3347cb Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 20 Jun 2026 22:47:38 -0700 Subject: [PATCH] feat(workflow): carry skillName through the WorkflowStep round-trip (U1) Add skillName to WorkflowStep and WorkflowStepInput, and round-trip it through nodeToStepInput / stepInputToNode so a skill-executor node's skill is available to the step session. Honors the compiler INVERSION CONTRACT (parity test). Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/core/src/types.ts | 8 ++++++++ packages/core/src/workflow-compiler.ts | 9 +++++++-- packages/core/src/workflow-steps-to-ir.ts | 11 +++++++---- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 5bca54843c..a064210265 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -626,6 +626,11 @@ export interface WorkflowStep { prompt: string; /** Tool set available to prompt-mode workflow agents. Defaults to readonly. */ toolMode?: WorkflowStepToolMode; + /** Name of a skill to load into this step's session (e.g. + * "compound-engineering:ce-work"). When set, the step session loads the named + * skill (discovery + selection) and the engine injects the Fusion workflow-step + * conventions preamble. Only meaningful for skill-executor graph nodes. */ + skillName?: string; /** Name of a script from project settings `scripts` map to execute (required when mode is "script") */ scriptName?: string; /** Whether this step is available for selection on new tasks */ @@ -746,6 +751,9 @@ export interface WorkflowStepInput { prompt?: string; /** Tool set available to prompt-mode workflow agents. Defaults to readonly. */ toolMode?: WorkflowStepToolMode; + /** Name of a skill to load into this step's session (e.g. + * "compound-engineering:ce-work"). See `WorkflowStep.skillName`. */ + skillName?: string; /** Script name from project settings (required when mode is "script"). * Must reference a named script in `settings.scripts` — no raw commands. */ scriptName?: string; diff --git a/packages/core/src/workflow-compiler.ts b/packages/core/src/workflow-compiler.ts index 5d617d0a7b..9d13370793 100644 --- a/packages/core/src/workflow-compiler.ts +++ b/packages/core/src/workflow-compiler.ts @@ -231,8 +231,9 @@ function defaultGateMode(node: WorkflowIrNode, mode: "prompt" | "script"): Workf * its exact inverse is `stepInputToNode` in `workflow-steps-to-ir.ts`. Parity is * pinned by `__tests__/workflow-steps-to-ir.test.ts` over exactly the * compiler-visible fields: name / mode / phase / gateMode / prompt / scriptName / - * toolMode / modelProvider / modelId. `enabled` / `defaultOn` / `templateId` are - * NOT compiler-visible and are handled by migration policy, not the converter. + * toolMode / skillName / modelProvider / modelId. `enabled` / `defaultOn` / + * `templateId` are NOT compiler-visible and are handled by migration policy, not + * the converter. * * INVERSION CONTRACT: when you add a field here, extend `stepInputToNode` (and * the parity test) in `workflow-steps-to-ir.ts` to keep the round-trip exact. @@ -255,6 +256,10 @@ function nodeToStepInput(node: WorkflowIrNode, phase: "pre-merge" | "post-merge" } else { input.prompt = configString(node, "prompt") ?? ""; input.toolMode = node.config?.toolMode === "coding" ? "coding" : "readonly"; + // Carry the node's skill name so the step session can load it (U1). Only + // present on skill-executor nodes; omitted otherwise to keep round-trip exact. + const skillName = configString(node, "skillName"); + if (skillName) input.skillName = skillName; const provider = configString(node, "modelProvider"); const modelId = configString(node, "modelId"); if (provider && modelId) { diff --git a/packages/core/src/workflow-steps-to-ir.ts b/packages/core/src/workflow-steps-to-ir.ts index 5d4f3cbe96..196a942aa4 100644 --- a/packages/core/src/workflow-steps-to-ir.ts +++ b/packages/core/src/workflow-steps-to-ir.ts @@ -12,10 +12,10 @@ import { parseWorkflowIr } from "./workflow-ir.js"; * compileWorkflowToSteps(stepsToWorkflowIr(steps, name)) ≡ steps * * over exactly the compiler-visible fields: name / mode / phase / gateMode / - * prompt / scriptName / toolMode / modelProvider / modelId. `enabled` / - * `defaultOn` / `templateId` / `migratedFragmentId` are NOT compiler-visible and - * are handled by migration policy (KTD-3), not by this converter. Parity is - * pinned by `__tests__/workflow-steps-to-ir.test.ts`. + * prompt / scriptName / toolMode / skillName / modelProvider / modelId. + * `enabled` / `defaultOn` / `templateId` / `migratedFragmentId` are NOT + * compiler-visible and are handled by migration policy (KTD-3), not by this + * converter. Parity is pinned by `__tests__/workflow-steps-to-ir.test.ts`. * * INVERSION CONTRACT: when a compiler-visible field is added to `nodeToStepInput` * (see the contract comment there), extend `stepInputToNode` below and the parity @@ -68,6 +68,9 @@ function stepInputToNode(step: WorkflowStep, id: string): WorkflowIrNode { // prompt mode config.prompt = step.prompt ?? ""; config.toolMode = step.toolMode === "coding" ? "coding" : "readonly"; + // Skill name round-trips when set (U1). The compiler reads it back via + // configString(node, "skillName"), so this keeps the inverse exact. + if (step.skillName) config.skillName = step.skillName; // Model overrides only round-trip when BOTH are present (compiler requirement). if (step.modelProvider && step.modelId) { config.modelProvider = step.modelProvider;