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) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-06-20 22:47:38 -07:00
parent 2c1cff820b
commit e4f4f431c8
3 changed files with 22 additions and 6 deletions

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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;