@sweny-ai/core 0.1.160 → 0.1.161

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,3 +17,6 @@ export declare function workflowExportAction(name: string): void;
17
17
  export declare function workflowValidateAction(file: string, options: {
18
18
  json?: boolean;
19
19
  }): void;
20
+ export declare function workflowUpgradeAction(file: string, options: {
21
+ dryRun?: boolean;
22
+ }): void;
package/dist/cli/main.js CHANGED
@@ -19,6 +19,7 @@ import { configuredSkills, configuredSkillsWithDiagnostics } from "../skills/cus
19
19
  import { buildAutoMcpServers, buildSkillMcpServers, buildProviderContext } from "../mcp.js";
20
20
  import { loadAdditionalContext } from "../templates.js";
21
21
  import { loadAndValidateWorkflow } from "../loader.js";
22
+ import { upgradeWorkflowFile } from "./workflow-upgrade.js";
22
23
  import { validateRuntimeInput } from "../inputs.js";
23
24
  import { mergeDryRunIntoInput, parseInputFlag, parseRunBudgetFlags, parseSpendFlags } from "./workflow-input.js";
24
25
  import { stringify as stringifyYaml } from "yaml";
@@ -725,6 +726,8 @@ export function loadWorkflowFile(filePath, knownSkills) {
725
726
  if (!result.ok) {
726
727
  throw new Error(`Invalid workflow file:\n${result.errors.map((e) => ` ${e.message}`).join("\n")}`);
727
728
  }
729
+ for (const w of result.warnings ?? [])
730
+ console.error(chalk.yellow(` \u26A0 ${w}`));
728
731
  return result.workflow;
729
732
  }
730
733
  export async function workflowRunAction(file, options) {
@@ -1122,7 +1125,10 @@ export function workflowValidateAction(file, options) {
1122
1125
  const result = loadAndValidateWorkflow(file);
1123
1126
  // Missing skill env is a warning here, not a failure: `run` is where it blocks.
1124
1127
  const warnings = result.ok
1125
- ? skillEnvWarnings(result.workflow, process.env, builtinSkills.concat(configuredSkills(process.env, process.cwd())))
1128
+ ? [
1129
+ ...(result.warnings ?? []),
1130
+ ...skillEnvWarnings(result.workflow, process.env, builtinSkills.concat(configuredSkills(process.env, process.cwd()))),
1131
+ ]
1126
1132
  : [];
1127
1133
  if (options.json) {
1128
1134
  const errs = result.ok ? [] : result.errors;
@@ -1149,6 +1155,29 @@ workflowCmd
1149
1155
  .description("Validate a workflow YAML or JSON file")
1150
1156
  .option("--json", "Output result as JSON")
1151
1157
  .action(workflowValidateAction);
1158
+ export function workflowUpgradeAction(file, options) {
1159
+ const result = upgradeWorkflowFile(file, { dryRun: options.dryRun });
1160
+ if (result.status === "error") {
1161
+ console.error(chalk.red(` \u2717 ${result.message}`));
1162
+ process.exit(1);
1163
+ return;
1164
+ }
1165
+ if (result.status === "current") {
1166
+ console.log(chalk.green(` \u2713 ${file} is already at spec_version "${result.version}"`));
1167
+ return;
1168
+ }
1169
+ const verb = result.status === "dry-run" ? "Would upgrade" : "Upgraded";
1170
+ console.log(chalk.green(` \u2713 ${verb} ${file}: spec_version "${result.from}" -> "${result.to}"`));
1171
+ for (const step of result.steps)
1172
+ console.log(chalk.dim(` - ${step}`));
1173
+ if (result.status === "dry-run")
1174
+ console.log(chalk.dim(" (dry run, file not changed)"));
1175
+ }
1176
+ workflowCmd
1177
+ .command("upgrade <file>")
1178
+ .description("Rewrite a workflow file at the current spec version (comments are kept where possible)")
1179
+ .option("--dry-run", "Show what would change without writing the file")
1180
+ .action(workflowUpgradeAction);
1152
1181
  const workflowRunCmd = workflowCmd
1153
1182
  .command("run [file]")
1154
1183
  .description(WORKFLOW_RUN_DESCRIPTION)
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `sweny workflow upgrade <file> [--dry-run]`
3
+ *
4
+ * Rewrites a workflow file at the current spec version by running the same
5
+ * migration chain the loader applies in memory.
6
+ *
7
+ * Comment preservation (YAML): the file is edited as a YAML document, not
8
+ * re-serialized from scratch. Comments, blank lines, key order and quoting on
9
+ * everything a migration did not touch are kept. NOT preserved: a comment
10
+ * attached to a value a migration replaced or removed, the original position of
11
+ * keys a migration renamed (they are added at the end of their map), and
12
+ * formatting of arrays whose length a migration changed (rewritten whole).
13
+ * JSON files carry no comments and are rewritten with 2-space indentation.
14
+ */
15
+ import { type MigrationConfig } from "../migrations.js";
16
+ export type UpgradeResult = {
17
+ status: "error";
18
+ message: string;
19
+ } | {
20
+ status: "current";
21
+ version: number;
22
+ } | {
23
+ status: "upgraded" | "dry-run";
24
+ from: number;
25
+ to: number;
26
+ /** Migration descriptions applied, in order. Empty when only `spec_version` was stamped. */
27
+ steps: string[];
28
+ /** The full text that was (or, for dry-run, would be) written. */
29
+ content: string;
30
+ };
31
+ export interface UpgradeOptions {
32
+ dryRun?: boolean;
33
+ migration?: MigrationConfig;
34
+ }
35
+ export declare function upgradeWorkflowFile(filePath: string, options?: UpgradeOptions): UpgradeResult;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * `sweny workflow upgrade <file> [--dry-run]`
3
+ *
4
+ * Rewrites a workflow file at the current spec version by running the same
5
+ * migration chain the loader applies in memory.
6
+ *
7
+ * Comment preservation (YAML): the file is edited as a YAML document, not
8
+ * re-serialized from scratch. Comments, blank lines, key order and quoting on
9
+ * everything a migration did not touch are kept. NOT preserved: a comment
10
+ * attached to a value a migration replaced or removed, the original position of
11
+ * keys a migration renamed (they are added at the end of their map), and
12
+ * formatting of arrays whose length a migration changed (rewritten whole).
13
+ * JSON files carry no comments and are rewritten with 2-space indentation.
14
+ */
15
+ import * as fs from "node:fs";
16
+ import * as path from "node:path";
17
+ import { isMap, parseDocument } from "yaml";
18
+ import { DEFAULT_MIGRATION_CONFIG, migrateWorkflow } from "../migrations.js";
19
+ import { validateParsed } from "../loader.js";
20
+ function isPlainObject(v) {
21
+ return typeof v === "object" && v !== null && !Array.isArray(v);
22
+ }
23
+ function withoutSpecVersion(obj) {
24
+ const copy = { ...obj };
25
+ delete copy.spec_version;
26
+ return copy;
27
+ }
28
+ function sameJson(a, b) {
29
+ return JSON.stringify(a) === JSON.stringify(b);
30
+ }
31
+ /** Apply the difference old -> next onto the YAML document, touching only what changed. */
32
+ function applyDiff(doc, at, old, next) {
33
+ if (sameJson(old, next))
34
+ return;
35
+ if (isPlainObject(old) && isPlainObject(next)) {
36
+ for (const key of Object.keys(old)) {
37
+ if (!(key in next))
38
+ doc.deleteIn([...at, key]);
39
+ }
40
+ for (const key of Object.keys(next)) {
41
+ if (key in old)
42
+ applyDiff(doc, [...at, key], old[key], next[key]);
43
+ else
44
+ doc.setIn([...at, key], next[key]);
45
+ }
46
+ return;
47
+ }
48
+ if (Array.isArray(old) && Array.isArray(next) && old.length === next.length) {
49
+ next.forEach((item, i) => applyDiff(doc, [...at, i], old[i], item));
50
+ return;
51
+ }
52
+ if (at.length === 0)
53
+ doc.contents = doc.createNode(next);
54
+ else
55
+ doc.setIn(at, next);
56
+ }
57
+ function setSpecVersion(doc, version) {
58
+ const root = doc.contents;
59
+ if (!isMap(root))
60
+ return;
61
+ if (root.has("spec_version")) {
62
+ root.set("spec_version", version);
63
+ return;
64
+ }
65
+ // Insert as the first key. A comment sitting directly above the old first key
66
+ // is the file's header comment; carry it onto the new pair so it stays on top.
67
+ const first = root.items[0];
68
+ const pair = doc.createPair("spec_version", version);
69
+ const firstKey = first?.key;
70
+ if (firstKey && firstKey.commentBefore) {
71
+ pair.key.commentBefore = firstKey.commentBefore;
72
+ firstKey.commentBefore = undefined;
73
+ }
74
+ root.items.unshift(pair);
75
+ }
76
+ export function upgradeWorkflowFile(filePath, options = {}) {
77
+ const config = options.migration ?? DEFAULT_MIGRATION_CONFIG;
78
+ let content;
79
+ try {
80
+ content = fs.readFileSync(filePath, "utf-8");
81
+ }
82
+ catch (err) {
83
+ return { status: "error", message: `Could not read "${filePath}": ${err instanceof Error ? err.message : err}` };
84
+ }
85
+ const isYaml = [".yaml", ".yml"].includes(path.extname(filePath).toLowerCase());
86
+ let doc;
87
+ let raw;
88
+ try {
89
+ if (isYaml) {
90
+ doc = parseDocument(content);
91
+ if (doc.errors.length > 0)
92
+ throw doc.errors[0];
93
+ raw = doc.toJS();
94
+ }
95
+ else {
96
+ raw = JSON.parse(content);
97
+ }
98
+ }
99
+ catch (err) {
100
+ return { status: "error", message: `Could not parse "${filePath}": ${err instanceof Error ? err.message : err}` };
101
+ }
102
+ if (!isPlainObject(raw))
103
+ return { status: "error", message: `Expected a workflow object in "${filePath}"` };
104
+ const migrated = migrateWorkflow(raw, config);
105
+ if (!migrated.ok)
106
+ return { status: "error", message: migrated.message };
107
+ const versionText = String(migrated.to);
108
+ if (migrated.applied.length === 0 && raw.spec_version === versionText) {
109
+ return { status: "current", version: migrated.to };
110
+ }
111
+ // Never write a file that would not load.
112
+ const check = validateParsed(migrated.workflow, { migration: config });
113
+ if (!check.ok) {
114
+ return {
115
+ status: "error",
116
+ message: `The upgraded workflow is not valid, so "${filePath}" was left unchanged:\n${check.errors
117
+ .map((e) => ` ${e.message}`)
118
+ .join("\n")}`,
119
+ };
120
+ }
121
+ let out;
122
+ if (doc) {
123
+ applyDiff(doc, [], withoutSpecVersion(raw), withoutSpecVersion(migrated.workflow));
124
+ setSpecVersion(doc, versionText);
125
+ out = doc.toString();
126
+ }
127
+ else {
128
+ out = JSON.stringify({ spec_version: versionText, ...withoutSpecVersion(migrated.workflow) }, null, 2) + "\n";
129
+ }
130
+ if (!options.dryRun)
131
+ fs.writeFileSync(filePath, out, "utf-8");
132
+ return {
133
+ status: options.dryRun ? "dry-run" : "upgraded",
134
+ from: migrated.from,
135
+ to: migrated.to,
136
+ steps: migrated.applied.map((m) => m.description),
137
+ content: out,
138
+ };
139
+ }
package/dist/index.d.ts CHANGED
@@ -45,6 +45,8 @@ export { workflowZ, nodeZ, edgeZ, skillZ, mcpServerConfigZ, skillDefinitionZ, so
45
45
  export type { WorkflowError } from "./schema.js";
46
46
  export { loadAndValidateWorkflow, validateParsed } from "./loader.js";
47
47
  export type { LoaderResult, LoaderError, LoaderOptions } from "./loader.js";
48
+ export { CURRENT_SPEC_VERSION, MIGRATIONS, migrateWorkflow } from "./migrations.js";
49
+ export type { Migration, MigrationConfig, MigrateResult } from "./migrations.js";
48
50
  export { validateRuntimeInput, summarizeInputShape } from "./inputs.js";
49
51
  export { buildAutoMcpServers, buildSkillMcpServers, buildProviderContext } from "./mcp.js";
50
52
  export type { ProviderContextOptions, SkillMcpOptions } from "./mcp.js";
package/dist/index.js CHANGED
@@ -47,6 +47,7 @@ export { workflowZ, nodeZ, edgeZ, skillZ, mcpServerConfigZ, skillDefinitionZ, so
47
47
  // the CLI uses for `workflow run`, `workflow validate`, and `publish`).
48
48
  // Node-only: loader.ts imports `node:fs`, so this is NOT mirrored in browser.ts.
49
49
  export { loadAndValidateWorkflow, validateParsed } from "./loader.js";
50
+ export { CURRENT_SPEC_VERSION, MIGRATIONS, migrateWorkflow } from "./migrations.js";
50
51
  // Workflow input validation
51
52
  export { validateRuntimeInput, summarizeInputShape } from "./inputs.js";
52
53
  // MCP auto-injection
package/dist/loader.d.ts CHANGED
@@ -11,15 +11,18 @@
11
11
  */
12
12
  import type { Workflow } from "./types.js";
13
13
  import { type WorkflowError } from "./schema.js";
14
+ import { type MigrationConfig } from "./migrations.js";
14
15
  /** Structural/schema error. `code` is present only for structural errors. */
15
16
  export interface LoaderError {
16
17
  message: string;
17
- code?: WorkflowError["code"] | "IO" | "PARSE" | "SCHEMA";
18
+ code?: WorkflowError["code"] | "IO" | "PARSE" | "SCHEMA" | "SPEC_VERSION";
18
19
  nodeId?: string;
19
20
  }
20
21
  export type LoaderResult = {
21
22
  ok: true;
22
23
  workflow: Workflow;
24
+ /** Non-fatal notes, e.g. the one-line "migrated in memory" warning. */
25
+ warnings?: string[];
23
26
  } | {
24
27
  ok: false;
25
28
  errors: LoaderError[];
@@ -27,6 +30,10 @@ export type LoaderResult = {
27
30
  export interface LoaderOptions {
28
31
  /** Optional set of known skill IDs for UNKNOWN_SKILL validation. */
29
32
  knownSkills?: Set<string>;
33
+ /** Spec version + migration registry. Defaults to the built-in one; tests inject their own. */
34
+ migration?: MigrationConfig;
35
+ /** Path shown in the migration warning. Set by loadAndValidateWorkflow. */
36
+ filePath?: string;
30
37
  }
31
38
  /**
32
39
  * Load a workflow YAML or JSON file, parse with the canonical Zod schema,
package/dist/loader.js CHANGED
@@ -14,6 +14,7 @@ import * as path from "node:path";
14
14
  import { parse as parseYaml } from "yaml";
15
15
  import { ZodError } from "zod";
16
16
  import { workflowZ, validateWorkflow, preflightLegacyVerify } from "./schema.js";
17
+ import { DEFAULT_MIGRATION_CONFIG, migrateWorkflow, migrationWarning, } from "./migrations.js";
17
18
  /**
18
19
  * Load a workflow YAML or JSON file, parse with the canonical Zod schema,
19
20
  * and run structural validation. All three phases contribute to `errors`.
@@ -36,7 +37,7 @@ export function loadAndValidateWorkflow(filePath, options = {}) {
36
37
  const msg = err instanceof Error ? err.message : String(err);
37
38
  return { ok: false, errors: [{ message: `Could not parse "${filePath}": ${msg}`, code: "PARSE" }] };
38
39
  }
39
- return validateParsed(raw, options);
40
+ return validateParsed(raw, { ...options, filePath: options.filePath ?? filePath });
40
41
  }
41
42
  /**
42
43
  * Validate an already-parsed workflow object (e.g. from a marketplace fetch
@@ -47,6 +48,15 @@ export function validateParsed(raw, options = {}) {
47
48
  const kind = raw === null ? "null" : Array.isArray(raw) ? "array" : typeof raw;
48
49
  return { ok: false, errors: [{ message: `Expected a workflow object, got ${kind}`, code: "SCHEMA" }] };
49
50
  }
51
+ // Apply spec_version migrations in memory (older versions), or refuse a
52
+ // version newer than this build supports. Runs first so a migration can
53
+ // reshape a file before the checks below see it.
54
+ const migrated = migrateWorkflow(raw, options.migration ?? DEFAULT_MIGRATION_CONFIG);
55
+ if (!migrated.ok) {
56
+ return { ok: false, errors: [{ message: migrated.message, code: "SPEC_VERSION" }] };
57
+ }
58
+ raw = migrated.workflow;
59
+ const warnings = migrated.applied.length > 0 ? [migrationWarning(migrated.from, migrated.to, options.filePath)] : [];
50
60
  // Detect legacy `verify:` blocks (renamed to `eval:` in v0.2.0) before the
51
61
  // Zod parse. workflowZ is intentionally non-strict at the top level, so a
52
62
  // top-level `verify:` would be silently STRIPPED instead of surfacing the
@@ -83,5 +93,5 @@ export function validateParsed(raw, options = {}) {
83
93
  errors: structural.map((e) => ({ message: e.message, code: e.code, nodeId: e.nodeId })),
84
94
  };
85
95
  }
86
- return { ok: true, workflow: parsed };
96
+ return warnings.length > 0 ? { ok: true, workflow: parsed, warnings } : { ok: true, workflow: parsed };
87
97
  }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Workflow spec versioning and migrations.
3
+ *
4
+ * Every workflow file carries an optional top-level `spec_version` (a string
5
+ * holding a positive integer, e.g. "1"). A file without the field is version 1:
6
+ * that is what every file written before the field existed means.
7
+ *
8
+ * Breaking spec changes bump CURRENT_SPEC_VERSION and add one entry to
9
+ * MIGRATIONS: a pure function from the raw (unvalidated) vN object to the raw
10
+ * vN+1 object. The loader applies the chain in memory so old files keep
11
+ * running; `sweny workflow upgrade <file>` writes the result back to disk.
12
+ *
13
+ * Adding a migration: see `__tests__/migrations.test.ts` for the harness
14
+ * (registry validity check plus a chain test to copy).
15
+ */
16
+ /** A raw, not-yet-validated workflow object as parsed from YAML or JSON. */
17
+ export type RawWorkflow = Record<string, unknown>;
18
+ export interface Migration {
19
+ /** Version this migration reads. */
20
+ from: number;
21
+ /** Version it produces. Always `from + 1`. */
22
+ to: number;
23
+ /** One line shown by `sweny workflow upgrade`. */
24
+ description: string;
25
+ /** Pure: must not mutate its input. Returns the next-version object. */
26
+ migrate: (raw: RawWorkflow) => RawWorkflow;
27
+ }
28
+ /** The spec version this build of sweny reads natively. */
29
+ export declare const CURRENT_SPEC_VERSION = 1;
30
+ /**
31
+ * Ordered registry, one entry per version step. Empty while v1 is current.
32
+ * Append only; never edit a shipped migration.
33
+ */
34
+ export declare const MIGRATIONS: readonly Migration[];
35
+ export interface MigrationConfig {
36
+ current: number;
37
+ migrations: readonly Migration[];
38
+ }
39
+ export declare const DEFAULT_MIGRATION_CONFIG: MigrationConfig;
40
+ export type MigrateError = {
41
+ code: "INVALID_VERSION" | "TOO_NEW" | "MIGRATION_FAILED";
42
+ message: string;
43
+ };
44
+ export type MigrateResult = {
45
+ ok: true;
46
+ /** The (possibly migrated) object. `spec_version` is a string when it was declared or migrated. */
47
+ workflow: RawWorkflow;
48
+ /** Version the file declared (1 when absent). */
49
+ from: number;
50
+ /** Version of `workflow` after migration. */
51
+ to: number;
52
+ /** Whether the input declared `spec_version`. */
53
+ declared: boolean;
54
+ /** Migrations applied, in order. Empty when the file is already current. */
55
+ applied: Migration[];
56
+ } | ({
57
+ ok: false;
58
+ } & MigrateError);
59
+ /** Throws if a registry is not an unbroken chain of single steps ending at `current`. */
60
+ export declare function assertRegistryValid(config: MigrationConfig): void;
61
+ export declare function migrateWorkflow(raw: RawWorkflow, config?: MigrationConfig): MigrateResult;
62
+ /** The single warning the loader emits when it migrated in memory. */
63
+ export declare function migrationWarning(from: number, to: number, filePath?: string): string;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Workflow spec versioning and migrations.
3
+ *
4
+ * Every workflow file carries an optional top-level `spec_version` (a string
5
+ * holding a positive integer, e.g. "1"). A file without the field is version 1:
6
+ * that is what every file written before the field existed means.
7
+ *
8
+ * Breaking spec changes bump CURRENT_SPEC_VERSION and add one entry to
9
+ * MIGRATIONS: a pure function from the raw (unvalidated) vN object to the raw
10
+ * vN+1 object. The loader applies the chain in memory so old files keep
11
+ * running; `sweny workflow upgrade <file>` writes the result back to disk.
12
+ *
13
+ * Adding a migration: see `__tests__/migrations.test.ts` for the harness
14
+ * (registry validity check plus a chain test to copy).
15
+ */
16
+ /** The spec version this build of sweny reads natively. */
17
+ export const CURRENT_SPEC_VERSION = 1;
18
+ /**
19
+ * Ordered registry, one entry per version step. Empty while v1 is current.
20
+ * Append only; never edit a shipped migration.
21
+ */
22
+ export const MIGRATIONS = [];
23
+ export const DEFAULT_MIGRATION_CONFIG = {
24
+ current: CURRENT_SPEC_VERSION,
25
+ migrations: MIGRATIONS,
26
+ };
27
+ /** Throws if a registry is not an unbroken chain of single steps ending at `current`. */
28
+ export function assertRegistryValid(config) {
29
+ const { current, migrations } = config;
30
+ if (!Number.isInteger(current) || current < 1)
31
+ throw new Error("current spec version must be a positive integer");
32
+ if (migrations.length !== current - 1) {
33
+ throw new Error(`expected ${current - 1} migration(s) to reach v${current}, found ${migrations.length}`);
34
+ }
35
+ migrations.forEach((m, i) => {
36
+ if (m.from !== i + 1 || m.to !== i + 2) {
37
+ throw new Error(`migration #${i} must be v${i + 1} -> v${i + 2}, got v${m.from} -> v${m.to}`);
38
+ }
39
+ });
40
+ }
41
+ function parseVersion(value) {
42
+ if (typeof value === "number" && Number.isInteger(value) && value >= 1)
43
+ return value;
44
+ if (typeof value === "string" && /^[1-9]\d*$/.test(value))
45
+ return Number(value);
46
+ return null;
47
+ }
48
+ export function migrateWorkflow(raw, config = DEFAULT_MIGRATION_CONFIG) {
49
+ const declared = "spec_version" in raw && raw.spec_version !== undefined;
50
+ const from = declared ? parseVersion(raw.spec_version) : 1;
51
+ if (from === null) {
52
+ return {
53
+ ok: false,
54
+ code: "INVALID_VERSION",
55
+ message: `spec_version must be a positive integer string like "1", got ${JSON.stringify(raw.spec_version)}`,
56
+ };
57
+ }
58
+ if (from > config.current) {
59
+ return {
60
+ ok: false,
61
+ code: "TOO_NEW",
62
+ message: `This workflow declares spec_version "${from}", but this version of sweny supports up to "${config.current}". ` +
63
+ `Upgrade sweny (run "sweny upgrade", or npm install -g @sweny-ai/core@latest) and try again.`,
64
+ };
65
+ }
66
+ let current = declared ? { ...raw, spec_version: String(from) } : raw;
67
+ const applied = [];
68
+ for (let v = from; v < config.current; v++) {
69
+ const step = config.migrations.find((m) => m.from === v);
70
+ if (!step) {
71
+ return { ok: false, code: "MIGRATION_FAILED", message: `No migration registered from spec_version "${v}".` };
72
+ }
73
+ try {
74
+ current = { ...step.migrate(current), spec_version: String(step.to) };
75
+ }
76
+ catch (err) {
77
+ const msg = err instanceof Error ? err.message : String(err);
78
+ return {
79
+ ok: false,
80
+ code: "MIGRATION_FAILED",
81
+ message: `Migration from spec_version "${step.from}" to "${step.to}" failed: ${msg}`,
82
+ };
83
+ }
84
+ applied.push(step);
85
+ }
86
+ return { ok: true, workflow: current, from, to: config.current, declared, applied };
87
+ }
88
+ /** The single warning the loader emits when it migrated in memory. */
89
+ export function migrationWarning(from, to, filePath) {
90
+ const target = filePath ? `"${filePath}"` : "<file>";
91
+ return (`Workflow uses spec_version "${from}" (current is "${to}"); migrated in memory for this run. ` +
92
+ `Run \`sweny workflow upgrade ${target}\` to update the file.`);
93
+ }
package/dist/schema.d.ts CHANGED
@@ -1714,6 +1714,7 @@ export declare const workflowZ: z.ZodObject<{
1714
1714
  id: z.ZodString;
1715
1715
  name: z.ZodString;
1716
1716
  description: z.ZodDefault<z.ZodString>;
1717
+ spec_version: z.ZodOptional<z.ZodString>;
1717
1718
  workflow_type: z.ZodOptional<z.ZodEnum<["pr_review", "e2e_test", "content_generation", "monitor", "data_sync", "generic"]>>;
1718
1719
  nodes: z.ZodRecord<z.ZodString, z.ZodObject<{
1719
1720
  name: z.ZodString;
@@ -2876,6 +2877,7 @@ export declare const workflowZ: z.ZodObject<{
2876
2877
  type?: string | undefined;
2877
2878
  })[] | undefined;
2878
2879
  judge_model?: string | undefined;
2880
+ spec_version?: string | undefined;
2879
2881
  workflow_type?: "pr_review" | "e2e_test" | "content_generation" | "monitor" | "data_sync" | "generic" | undefined;
2880
2882
  judge_budget?: number | undefined;
2881
2883
  context_mode?: "full" | "bounded" | undefined;
@@ -3064,6 +3066,7 @@ export declare const workflowZ: z.ZodObject<{
3064
3066
  type?: string | undefined;
3065
3067
  })[] | undefined;
3066
3068
  judge_model?: string | undefined;
3069
+ spec_version?: string | undefined;
3067
3070
  workflow_type?: "pr_review" | "e2e_test" | "content_generation" | "monitor" | "data_sync" | "generic" | undefined;
3068
3071
  judge_budget?: number | undefined;
3069
3072
  context_mode?: "full" | "bounded" | undefined;
@@ -3266,6 +3269,7 @@ export declare function parseWorkflow(raw: unknown): {
3266
3269
  type?: string | undefined;
3267
3270
  })[] | undefined;
3268
3271
  judge_model?: string | undefined;
3272
+ spec_version?: string | undefined;
3269
3273
  workflow_type?: "pr_review" | "e2e_test" | "content_generation" | "monitor" | "data_sync" | "generic" | undefined;
3270
3274
  judge_budget?: number | undefined;
3271
3275
  context_mode?: "full" | "bounded" | undefined;
@@ -3649,6 +3653,11 @@ export declare const workflowJsonSchema: {
3649
3653
  readonly description: {
3650
3654
  readonly type: "string";
3651
3655
  };
3656
+ readonly spec_version: {
3657
+ readonly type: "string";
3658
+ readonly pattern: "^[1-9][0-9]*$";
3659
+ readonly description: "Workflow spec version as a positive integer string. Absent means 1. Older versions are migrated in memory on load; newer than the CLI supports is refused. Run `sweny workflow upgrade <file>` to rewrite a file at the current version.";
3660
+ };
3652
3661
  readonly entry: {
3653
3662
  readonly type: "string";
3654
3663
  readonly minLength: 1;
package/dist/schema.js CHANGED
@@ -357,6 +357,10 @@ export const workflowZ = z.object({
357
357
  id: z.string().min(1),
358
358
  name: z.string().min(1),
359
359
  description: z.string().default(""),
360
+ spec_version: z
361
+ .string()
362
+ .regex(/^[1-9]\d*$/, "spec_version must be a positive integer string")
363
+ .optional(),
360
364
  workflow_type: workflowTypeZ.optional(),
361
365
  nodes: z.record(nodeZ),
362
366
  edges: z.array(edgeZ),
@@ -954,6 +958,11 @@ export const workflowJsonSchema = {
954
958
  id: { type: "string", minLength: 1 },
955
959
  name: { type: "string", minLength: 1 },
956
960
  description: { type: "string" },
961
+ spec_version: {
962
+ type: "string",
963
+ pattern: "^[1-9][0-9]*$",
964
+ description: "Workflow spec version as a positive integer string. Absent means 1. Older versions are migrated in memory on load; newer than the CLI supports is refused. Run `sweny workflow upgrade <file>` to rewrite a file at the current version.",
965
+ },
957
966
  entry: { type: "string", minLength: 1, description: "ID of the entry node" },
958
967
  rules: {
959
968
  type: "array",
package/dist/types.d.ts CHANGED
@@ -514,6 +514,8 @@ export interface Workflow {
514
514
  id: string;
515
515
  name: string;
516
516
  description: string;
517
+ /** Spec version, a positive integer string (e.g. "1"). Absent means "1". See `migrations.ts`. */
518
+ spec_version?: string;
517
519
  /** Optional. Defaults to "generic" when absent. Required on marketplace templates. */
518
520
  workflow_type?: WorkflowType;
519
521
  nodes: Record<string, Node>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.160",
3
+ "version": "0.1.161",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"