Adds a non-mutating `fn workflow validate` dry-run path across CLI, agent tools, and dashboard API so custom workflow IR can be checked before create/update. - Add `packages/cli/src/commands/workflow.ts` implementing `fn workflow validate <id> | --file <path>` with JSON/text output, wired into `bin.ts`. - Add `fn_workflow_validate` agent tool (`agent-tools.ts`, `index.ts`) reusing the existing parseWorkflowIr/trait/code-node/column-agent validation used by create/update, performing no persistence. - Add `POST /api/workflows/validate` route in `register-workflow-routes.ts` plus dashboard route test coverage. - Extend heartbeat tool-gating/exposure tests and gating classifications to include `fn_workflow_validate` alongside the other workflow tools. - Update CLI/agent extension docs (`docs/cli-reference.md`, `docs/agents.md`, `docs/workflow-steps.md`, fusion skill references) to document the new command/tool. - Add changeset `.changeset/fn-7911-workflow-validate.md` (minor) describing the new capability. Files changed: .changeset/fn-7911-workflow-validate.md | 7 ++ docs/agents.md | 5 +- docs/cli-reference.md | 13 ++ docs/workflow-steps.md | 3 +- packages/cli/skill/fusion/SKILL.md | 2 +- .../cli/skill/fusion/references/extension-tools.md | 10 ++ .../skill/fusion/references/fusion-capabilities.md | 1 + .../src/__tests__/extension-workflow-tools.test.ts | 1 + packages/cli/src/__tests__/extension.test.ts | 1 + .../src/__tests__/workflow-docs-current.test.ts | 1 + packages/cli/src/bin.ts | 22 ++++ packages/cli/src/commands/workflow.ts | 80 ++++++++++++ packages/cli/src/extension.ts | 10 ++ .../dashboard/src/__tests__/chat-manager.test.ts | 1 + .../dashboard/src/__tests__/chat.rooms.test.ts | 1 + .../planning-document-tools-exposure.test.ts | 1 + .../__tests__/workflow-validate-route.test.ts | 101 +++++++++++++++ .../src/routes/register-workflow-routes.ts | 27 +++- .../engine/src/__tests__/agent-action-gate.test.ts | 2 +- .../agent-workflow-tools-exposure.test.ts | 70 ++++++++++- .../src/__tests__/gating-classifications.test.ts | 3 +- .../src/__tests__/heartbeat-executor.test.ts | 37 +++--- .../src/__tests__/heartbeat-session-prompt.test.ts | 5 +- .../src/__tests__/permanent-agent-gating.test.ts | 2 +- packages/engine/src/agent-heartbeat.ts | 5 +- packages/engine/src/agent-tools.ts | 140 ++++++++++++++++++++- packages/engine/src/executor.ts | 6 + packages/engine/src/gating-classifications.ts | 2 + packages/engine/src/index.ts | 4 + 29 files changed, 532 insertions(+), 31 deletions(-) Fusion-Task-Id: FN-7911 Fusion-Task-Lineage: 903d15fe-a7ec-458f-aa34-8f2e895a9603 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
81 lines
2.8 KiB
TypeScript
81 lines
2.8 KiB
TypeScript
import { readFile } from "node:fs/promises";
|
|
import { resolve } from "node:path";
|
|
import { TaskStore } from "@fusion/core";
|
|
import { validateWorkflowIrDryRun } from "@fusion/engine";
|
|
import { getStore } from "../project-resolver.js";
|
|
|
|
export interface RunWorkflowValidateOptions {
|
|
workflowId?: string;
|
|
file?: string;
|
|
projectName?: string;
|
|
json?: boolean;
|
|
}
|
|
|
|
async function resolveStore(projectName?: string): Promise<TaskStore> {
|
|
try {
|
|
return await getStore({ project: projectName });
|
|
} catch (error) {
|
|
if (projectName) throw error;
|
|
const store = new TaskStore(process.cwd());
|
|
await store.init();
|
|
return store;
|
|
}
|
|
}
|
|
|
|
function printJsonAndExit(payload: unknown, code: number): never {
|
|
console.log(JSON.stringify(payload, null, 2));
|
|
process.exit(code);
|
|
}
|
|
|
|
/**
|
|
* FNXC:WorkflowCli 2026-07-12-00:00:
|
|
* Workflow authors need a script-friendly dry-run command that can validate a saved workflow id or an IR JSON file without creating or mutating workflow rows.
|
|
*/
|
|
export async function runWorkflowValidate(opts: RunWorkflowValidateOptions): Promise<void> {
|
|
const workflowId = opts.workflowId?.trim();
|
|
if (!workflowId && !opts.file) {
|
|
const message = "Usage: fn workflow validate <id> | --file <path> [--json]";
|
|
if (opts.json) printJsonAndExit({ valid: false, error: message }, 2);
|
|
console.error(message);
|
|
process.exit(2);
|
|
}
|
|
|
|
let store: TaskStore | undefined;
|
|
try {
|
|
store = await resolveStore(opts.projectName);
|
|
let ir: unknown;
|
|
if (opts.file) {
|
|
const filePath = resolve(opts.file);
|
|
try {
|
|
ir = JSON.parse(await readFile(filePath, "utf8"));
|
|
} catch (error) {
|
|
const message = `Failed to read or parse workflow IR file '${opts.file}': ${error instanceof Error ? error.message : String(error)}`;
|
|
if (opts.json) printJsonAndExit({ valid: false, error: message }, 2);
|
|
console.error(message);
|
|
process.exit(2);
|
|
}
|
|
} else {
|
|
const def = await store.getWorkflowDefinition(workflowId!);
|
|
if (!def) {
|
|
const message = `Workflow '${workflowId}' not found`;
|
|
if (opts.json) printJsonAndExit({ valid: false, error: message }, 2);
|
|
console.error(message);
|
|
process.exit(2);
|
|
}
|
|
ir = def.ir;
|
|
}
|
|
|
|
const result = await validateWorkflowIrDryRun(store, ir, false);
|
|
if (opts.json) printJsonAndExit(result.valid ? { valid: true } : { valid: false, errors: result.errors }, result.valid ? 0 : 1);
|
|
if (result.valid) {
|
|
console.log("✓ Workflow IR is valid. No workflow was created or mutated.");
|
|
process.exit(0);
|
|
}
|
|
console.error("✗ Workflow IR is invalid:");
|
|
for (const error of result.errors) console.error(` - ${error.message}`);
|
|
process.exit(1);
|
|
} finally {
|
|
await store?.close?.().catch(() => {});
|
|
}
|
|
}
|