@sweny-ai/core 0.1.104 → 0.1.106

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.
package/dist/browser.d.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  export type { Skill, SkillCategory, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, McpTransport, WorkflowType, SkillHarnessKey, } from "./types.js";
17
17
  export { consoleLogger } from "./types.js";
18
18
  export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, } from "./types.js";
19
- export { github, linear, slack, sentry, datadog, notification, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
19
+ export { github, linear, slack, sentry, datadog, betterstack, notification, supabase, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
20
20
  export type { SkillValidationResult } from "./skills/index.js";
21
21
  export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema, skillJsonSchema, } from "./schema.js";
22
22
  export type { WorkflowError } from "./schema.js";
package/dist/browser.js CHANGED
@@ -20,7 +20,7 @@ export { consoleLogger } from "./types.js";
20
20
  // skill id, needs these without hardcoding the members.
21
21
  export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, } from "./types.js";
22
22
  // Skills (browser-safe — no filesystem access, no `process.env` reads)
23
- export { github, linear, slack, sentry, datadog, notification, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
23
+ export { github, linear, slack, sentry, datadog, betterstack, notification, supabase, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
24
24
  // Schema & validation
25
25
  export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema, skillJsonSchema, } from "./schema.js";
26
26
  // Studio adapter
package/dist/cli/e2e.js CHANGED
@@ -760,9 +760,15 @@ export async function runE2eInit(options = {}) {
760
760
  }
761
761
  }
762
762
  // ── Timeout helper ─────────────────────────────────────────────────────
763
- function withTimeout(promise, ms, label) {
763
+ function withTimeout(promise, ms, label, onTimeout) {
764
764
  return new Promise((resolve, reject) => {
765
- const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms / 1000}s`)), ms);
765
+ const timer = setTimeout(() => {
766
+ // Abort the underlying work first so it stops advancing and the in-flight
767
+ // model query is interrupted, then reject the wrapper. Without the abort
768
+ // the execute() promise is abandoned but its model query keeps running.
769
+ onTimeout?.();
770
+ reject(new Error(`${label} timed out after ${ms / 1000}s`));
771
+ }, ms);
766
772
  promise
767
773
  .then(resolve)
768
774
  .catch(reject)
@@ -905,8 +911,9 @@ export async function runE2eRun(options) {
905
911
  }
906
912
  }
907
913
  };
914
+ const controller = new AbortController();
908
915
  try {
909
- const { results } = await withTimeout(execute(workflow, { run_id: vars.run_id, base_url: vars.base_url }, { skills, claude, observer, logger: consoleLogger }), timeoutMs, `Workflow ${workflow.name}`);
916
+ const { results } = await withTimeout(execute(workflow, { run_id: vars.run_id, base_url: vars.base_url }, { skills, claude, observer, logger: consoleLogger, signal: controller.signal }), timeoutMs, `Workflow ${workflow.name}`, () => controller.abort());
910
917
  // Extract report
911
918
  const report = results.get("report");
912
919
  if (report?.data) {
package/dist/cli/skill.js CHANGED
@@ -29,7 +29,12 @@ export function renderSkillTemplate(opts) {
29
29
  const lines = [];
30
30
  lines.push("---");
31
31
  lines.push(`name: ${id}`);
32
- lines.push(`description: ${description}`);
32
+ // JSON-quote the user-supplied description so YAML-special chars (`:`, a
33
+ // leading `[`/`{`/`@`/`>`/`|`, `#`, embedded quotes) round-trip cleanly
34
+ // through the loader's frontmatter parser. A JSON double-quoted string is a
35
+ // valid YAML double-quoted scalar. `id`/`category` are constrained elsewhere
36
+ // (isValidSkillId / SKILL_CATEGORIES) so they stay bare.
37
+ lines.push(`description: ${JSON.stringify(description)}`);
33
38
  lines.push(`category: ${category}`);
34
39
  lines.push("# config:");
35
40
  lines.push("# EXAMPLE_ENV_VAR:");
@@ -41,6 +41,20 @@ export interface ExecuteOptions {
41
41
  * Per-edge `max_iterations` behavior is unchanged.
42
42
  */
43
43
  max_steps?: number;
44
+ /**
45
+ * Caller-supplied abort signal. When it aborts, `execute()` rejects promptly
46
+ * and issues no further node / eval / route model calls. The signal is also
47
+ * threaded into every `claude.run` / `claude.evaluate` / `claude.ask` call so
48
+ * the underlying query is interrupted rather than left running detached.
49
+ * Default: no signal (back-compat).
50
+ */
51
+ signal?: AbortSignal;
52
+ /**
53
+ * Per-model-call wall-clock budget in ms, forwarded to every `claude.run` /
54
+ * `claude.evaluate` / `claude.ask` call. This is a per-call timeout, not a
55
+ * whole-run budget. Default: no timeout (back-compat).
56
+ */
57
+ timeoutMs?: number;
44
58
  }
45
59
  /**
46
60
  * Default workflow-level step budget. Counts total node executions across the
package/dist/executor.js CHANGED
@@ -26,6 +26,16 @@ import { buildToolAliases } from "./skills/index.js";
26
26
  * before it burns meaningful model spend.
27
27
  */
28
28
  export const DEFAULT_MAX_STEPS = 200;
29
+ /**
30
+ * Throw a clear "workflow aborted" error if the caller's signal has aborted.
31
+ * Used at the top of the run loop and before issuing any further model call so
32
+ * an aborted run stops promptly instead of advancing to the next node.
33
+ */
34
+ function throwIfAborted(signal) {
35
+ if (signal?.aborted) {
36
+ throw new Error("workflow aborted");
37
+ }
38
+ }
29
39
  /**
30
40
  * Execute a workflow from entry to completion.
31
41
  *
@@ -34,7 +44,9 @@ export const DEFAULT_MAX_STEPS = 200;
34
44
  * - `trace`: full ordered execution trace including loops and routing decisions
35
45
  */
36
46
  export async function execute(workflow, input, options) {
37
- const { claude, observer } = options;
47
+ const { claude, observer, signal, timeoutMs } = options;
48
+ // If the caller already aborted before we started, fail fast.
49
+ throwIfAborted(signal);
38
50
  // Merge inline workflow skills into the skill map so they resolve at runtime.
39
51
  // Inline skills (instruction/mcp only) become Skill objects with empty tools/config.
40
52
  // The caller's skill map takes precedence — inline skills only fill gaps.
@@ -93,6 +105,9 @@ export async function execute(workflow, input, options) {
93
105
  safeObserve(observer, { type: "sources:resolved", sources: resolvedSources }, logger);
94
106
  let currentId = workflow.entry;
95
107
  while (currentId) {
108
+ // Abort before issuing the next node/eval/route model call. Aborting the
109
+ // signal mid-run makes execute() reject promptly rather than advancing.
110
+ throwIfAborted(signal);
96
111
  const node = workflow.nodes[currentId];
97
112
  if (!node)
98
113
  throw new Error(`Unknown node: "${currentId}"`);
@@ -156,7 +171,7 @@ export async function execute(workflow, input, options) {
156
171
  logger.warn(` requires ${onFail === "skip" ? "skipped" : "failed"}: ${requiresError}`, { node: currentId });
157
172
  // Apply normal routing rules (dry run gate + resolveNext).
158
173
  // TODO: dedupe with requires path — see advanceFromNode helper below
159
- const next = await advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace);
174
+ const next = await advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace, { signal, timeoutMs });
160
175
  currentId = next;
161
176
  continue;
162
177
  }
@@ -192,6 +207,8 @@ export async function execute(workflow, input, options) {
192
207
  maxTurns: node.max_turns,
193
208
  disallowedTools: node.disallowed_tools,
194
209
  model: nodeModel,
210
+ signal,
211
+ timeoutMs,
195
212
  onProgress: (message) => {
196
213
  safeObserve(observer, { type: "node:progress", node: currentId, message }, logger);
197
214
  },
@@ -261,6 +278,8 @@ export async function execute(workflow, input, options) {
261
278
  logger,
262
279
  context,
263
280
  model: nodeModel,
281
+ signal,
282
+ timeoutMs,
264
283
  });
265
284
  currentInstruction = `${preamble}\n\n---\n\n${instruction}`;
266
285
  safeObserve(observer, { type: "node:retry", node: currentId, attempt: attempt + 1, reason: outcome.error ?? "eval failed", preamble }, logger);
@@ -273,7 +292,10 @@ export async function execute(workflow, input, options) {
273
292
  safeObserve(observer, { type: "node:exit", node: currentId, result }, logger);
274
293
  logger.info(` ✓ ${result.status}`, { node: currentId, toolCalls: result.toolCalls.length });
275
294
  // Dry run gate + routing — shared with requires path via advanceFromNode helper.
276
- currentId = await advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace);
295
+ currentId = await advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace, {
296
+ signal,
297
+ timeoutMs,
298
+ });
277
299
  }
278
300
  safeObserve(observer, {
279
301
  type: "workflow:end",
@@ -603,18 +625,50 @@ function resolveSkillInstructions(skillIds, skills) {
603
625
  * `options.env ?? process.env` so an explicit env map is honored consistently
604
626
  * with Source resolution (which reads the same map). Defaults to
605
627
  * `process.env` when omitted.
628
+ *
629
+ * Presence vs. truthiness: an explicitly-provided empty string is treated as
630
+ * present, not absent. Resolution rules:
631
+ * - An override that is present (any string, including "") wins; the env
632
+ * var is not consulted. An explicit "" override does NOT silently fall
633
+ * through to env.
634
+ * - Otherwise the env var (when declared) is used.
635
+ * - A value that is `undefined` (no override, no env var) is absent: a
636
+ * required field reports "not provided".
637
+ * - A value that is "" is present-but-empty: a required field reports a
638
+ * distinct "set to empty" message (you set it, but to an unusable value),
639
+ * while an optional field passes the empty string through.
606
640
  */
607
641
  function resolveConfig(skills, overrides, env = process.env) {
608
642
  const config = {};
609
643
  const missing = [];
610
644
  for (const skill of skills.values()) {
611
645
  for (const [key, field] of Object.entries(skill.config)) {
612
- const value = overrides?.[key] ?? (field.env ? env[field.env] : undefined);
613
- if (value) {
614
- config[key] = value;
646
+ // Explicit override wins, including an empty string — `in` distinguishes
647
+ // a present "" from an absent key, so an override never falls through to
648
+ // env in a surprising way.
649
+ const hasOverride = overrides != null && key in overrides;
650
+ const value = hasOverride ? overrides[key] : field.env ? env[field.env] : undefined;
651
+ const source = field.env ? ` (env: ${field.env})` : "";
652
+ const label = `${skill.id}.${key}${source || " (env: none)"}`;
653
+ if (value === undefined) {
654
+ // Truly absent: no override and no env value.
655
+ if (field.required) {
656
+ missing.push(`${label}: not provided`);
657
+ }
658
+ }
659
+ else if (value === "") {
660
+ // Present but empty. An empty credential is unusable for a required
661
+ // field; surface a distinct message so the user can tell "set to empty"
662
+ // from "forgot it". Optional empty values pass through unchanged.
663
+ if (field.required) {
664
+ missing.push(`${label}: set to empty`);
665
+ }
666
+ else {
667
+ config[key] = value;
668
+ }
615
669
  }
616
- else if (field.required) {
617
- missing.push(`${skill.id}.${key} (env: ${field.env ?? "none"})`);
670
+ else {
671
+ config[key] = value;
618
672
  }
619
673
  }
620
674
  }
@@ -631,7 +685,7 @@ function resolveConfig(skills, overrides, env = process.env) {
631
685
  *
632
686
  * Returns the next node ID, or null when execution should stop.
633
687
  */
634
- async function advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace) {
688
+ async function advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace, abort) {
635
689
  // Dry run hard gate — stop at the first conditional routing decision.
636
690
  // Unconditional edges are analysis flow (prepare→gather→investigate);
637
691
  // conditional edges are action decisions (investigate→create_issue/skip).
@@ -645,7 +699,7 @@ async function advanceFromNode(workflow, currentId, results, input, claude, obse
645
699
  }
646
700
  }
647
701
  const prevId = currentId;
648
- const nextId = await resolveNext(workflow, currentId, results, input, claude, observer, edgeCounts, logger);
702
+ const nextId = await resolveNext(workflow, currentId, results, input, claude, observer, edgeCounts, logger, abort);
649
703
  if (nextId) {
650
704
  const reason = workflow.edges.find((e) => e.from === prevId && e.to === nextId)?.when ?? "only path";
651
705
  trace.edges.push({ from: prevId, to: nextId, reason });
@@ -661,7 +715,7 @@ async function advanceFromNode(workflow, currentId, results, input, claude, obse
661
715
  *
662
716
  * Edges with max_iterations are filtered out once exhausted.
663
717
  */
664
- async function resolveNext(workflow, current, results, input, claude, observer, edgeCounts, logger) {
718
+ async function resolveNext(workflow, current, results, input, claude, observer, edgeCounts, logger, abort) {
665
719
  // Filter out edges that have exceeded their max_iterations
666
720
  const outEdges = workflow.edges.filter((e) => {
667
721
  if (e.from !== current)
@@ -731,6 +785,8 @@ async function resolveNext(workflow, current, results, input, claude, observer,
731
785
  question: "Based on the results so far, which condition is true?",
732
786
  context,
733
787
  choices,
788
+ signal: abort?.signal,
789
+ timeoutMs: abort?.timeoutMs,
734
790
  });
735
791
  // Validate that Claude returned a valid target.
736
792
  const validTargets = new Set(outEdges.map((e) => e.to));
package/dist/mermaid.js CHANGED
@@ -32,6 +32,31 @@ export function toMermaid(workflow, options = {}) {
32
32
  lines.push("---");
33
33
  }
34
34
  lines.push(`graph ${direction}`);
35
+ // Build an injective real-ID → Mermaid-alias map. `sanitizeId` is lossy
36
+ // (every non-alphanumeric char collapses to `_`), so distinct node IDs like
37
+ // `a.b` and `a_b` would otherwise sanitize to the same alias and render as a
38
+ // single merged node, silently corrupting the diagram. Resolve every alias
39
+ // through this map so node declarations, edge endpoints, and class styling
40
+ // all stay consistent and collision-free.
41
+ const idAlias = new Map();
42
+ const usedAliases = new Set();
43
+ const aliasFor = (id) => {
44
+ const cached = idAlias.get(id);
45
+ if (cached)
46
+ return cached;
47
+ const base = sanitizeId(id);
48
+ let alias = base;
49
+ let n = 2;
50
+ while (usedAliases.has(alias))
51
+ alias = `${base}_${n++}`;
52
+ usedAliases.add(alias);
53
+ idAlias.set(id, alias);
54
+ return alias;
55
+ };
56
+ // Seed aliases in node-declaration order so already-alphanumeric IDs keep
57
+ // their byte-identical alias (and stable suffixing for any collisions).
58
+ for (const id of Object.keys(workflow.nodes))
59
+ aliasFor(id);
35
60
  // Build node iteration counts from trace
36
61
  const nodeIterations = new Map();
37
62
  if (trace) {
@@ -48,10 +73,10 @@ export function toMermaid(workflow, options = {}) {
48
73
  const iterations = nodeIterations.get(id);
49
74
  const iterTag = iterations && iterations > 1 ? ` ×${iterations}` : "";
50
75
  if (isEntry) {
51
- lines.push(` ${sanitizeId(id)}([${label}${entryTag}${iterTag}])`);
76
+ lines.push(` ${aliasFor(id)}([${label}${entryTag}${iterTag}])`);
52
77
  }
53
78
  else {
54
- lines.push(` ${sanitizeId(id)}[${label}${iterTag}]`);
79
+ lines.push(` ${aliasFor(id)}[${label}${iterTag}]`);
55
80
  }
56
81
  }
57
82
  lines.push("");
@@ -70,8 +95,8 @@ export function toMermaid(workflow, options = {}) {
70
95
  const notTakenIndices = [];
71
96
  let edgeIndex = 0;
72
97
  for (const edge of workflow.edges) {
73
- const from = sanitizeId(edge.from);
74
- const to = sanitizeId(edge.to);
98
+ const from = aliasFor(edge.from);
99
+ const to = aliasFor(edge.to);
75
100
  const key = `${edge.from}→${edge.to}`;
76
101
  const taken = takenEdges.has(key);
77
102
  const takenCount = edgeTakenCounts.get(key) ?? 0;
@@ -111,7 +136,7 @@ export function toMermaid(workflow, options = {}) {
111
136
  lines.push(" classDef failed fill:#ef4444,stroke:#dc2626,color:#fff,stroke-width:2px");
112
137
  lines.push(" classDef skipped fill:#6b7280,stroke:#4b5563,color:#fff,stroke-dasharray:5 5");
113
138
  for (const [status, ids] of statusNodes) {
114
- const sanitized = ids.map(sanitizeId).join(",");
139
+ const sanitized = ids.map(aliasFor).join(",");
115
140
  lines.push(` class ${sanitized} ${status}`);
116
141
  }
117
142
  }
@@ -141,7 +166,11 @@ function sanitizeId(id) {
141
166
  }
142
167
  /** Escape characters that break Mermaid labels */
143
168
  function escapeLabel(text) {
144
- return text.replace(/"/g, """).replace(/\[/g, "[").replace(/\]/g, "]");
169
+ // `|` must be escaped too: edge labels are emitted as `-->|"label"|`, so a
170
+ // literal `|` inside the label (e.g. an edge `when` of "high | critical")
171
+ // terminates Mermaid's edge-label token and corrupts the edge. `|`
172
+ // renders as a literal `|`, consistent with the existing `[`/`]`.
173
+ return text.replace(/"/g, """).replace(/\[/g, "[").replace(/\]/g, "]").replace(/\|/g, "|");
145
174
  }
146
175
  /** Group node IDs by their status */
147
176
  function groupByStatus(state) {
package/dist/retry.d.ts CHANGED
@@ -17,6 +17,10 @@ export interface BuildRetryPreambleOptions {
17
17
  * runs on the node's model. When undefined, the client default applies.
18
18
  */
19
19
  model?: string;
20
+ /** Caller-supplied abort signal, forwarded to the reflection `claude.ask`. */
21
+ signal?: AbortSignal;
22
+ /** Per-call wall-clock budget in ms, forwarded to the reflection `claude.ask`. */
23
+ timeoutMs?: number;
20
24
  }
21
25
  /**
22
26
  * Build the retry preamble for a single retry attempt.
package/dist/retry.js CHANGED
@@ -21,7 +21,7 @@ const DEFAULT_PREAMBLE_HEADER = "## Previous attempt failed evaluation";
21
21
  * default static preamble and logs a warning.
22
22
  */
23
23
  export async function buildRetryPreamble(opts) {
24
- const { retry, evalFailures, toolCalls, nodeInstruction, claude, logger, context, model } = opts;
24
+ const { retry, evalFailures, toolCalls, nodeInstruction, claude, logger, context, model, signal, timeoutMs } = opts;
25
25
  const failureList = formatEvalFailures(evalFailures);
26
26
  const inst = retry.instruction;
27
27
  if (typeof inst === "string") {
@@ -31,7 +31,7 @@ export async function buildRetryPreamble(opts) {
31
31
  const reflectPrompt = "reflect" in inst && typeof inst.reflect === "string" ? inst.reflect : DEFAULT_REFLECTION_PROMPT;
32
32
  const askInstruction = buildReflectionPrompt(reflectPrompt, nodeInstruction, failureList, toolCalls);
33
33
  try {
34
- const diagnosis = await claude.ask({ instruction: askInstruction, context, model });
34
+ const diagnosis = await claude.ask({ instruction: askInstruction, context, model, signal, timeoutMs });
35
35
  const trimmed = diagnosis.trim();
36
36
  if (trimmed.length > 0) {
37
37
  return `## Reflection on previous attempt\n\n${trimmed}\n\n${DEFAULT_PREAMBLE_HEADER}\n\n${failureList}`;
package/dist/schema.d.ts CHANGED
@@ -2623,7 +2623,7 @@ export declare function parseWorkflow(raw: unknown): {
2623
2623
  }> | undefined;
2624
2624
  };
2625
2625
  export interface WorkflowError {
2626
- code: "MISSING_ENTRY" | "UNKNOWN_EDGE_SOURCE" | "UNKNOWN_EDGE_TARGET" | "UNREACHABLE_NODE" | "UNKNOWN_SKILL" | "SELF_LOOP" | "UNBOUNDED_CYCLE" | "INVALID_INLINE_SKILL";
2626
+ code: "MISSING_ENTRY" | "UNKNOWN_EDGE_SOURCE" | "UNKNOWN_EDGE_TARGET" | "UNREACHABLE_NODE" | "UNKNOWN_SKILL" | "SELF_LOOP" | "UNBOUNDED_CYCLE" | "UNSUPPORTED_EVAL_POLICY" | "INVALID_INLINE_SKILL";
2627
2627
  message: string;
2628
2628
  nodeId?: string;
2629
2629
  }
package/dist/schema.js CHANGED
@@ -102,6 +102,13 @@ export const outputMatchZ = z
102
102
  }, { message: "output_matches entry must declare exactly one of: equals, in, matches" });
103
103
  export const evaluatorKindZ = z.enum(EVALUATOR_KINDS);
104
104
  export const evalPolicyZ = z.enum(EVAL_POLICIES);
105
+ /**
106
+ * Eval policies actually implemented at runtime. The Zod schema and published
107
+ * JSON schema accept the full {@link EVAL_POLICIES} vocabulary (the others are
108
+ * reserved), but `validateWorkflow` rejects any policy not in this set so a
109
+ * workflow fails at load time rather than throwing mid-run from `aggregateEval`.
110
+ */
111
+ const SUPPORTED_EVAL_POLICIES = new Set(["all_pass"]);
105
112
  export const evaluatorRuleZ = z
106
113
  .object({
107
114
  any_tool_called: z.array(z.string().min(1)).min(1).optional(),
@@ -400,6 +407,23 @@ export function validateWorkflow(workflow, knownSkills) {
400
407
  }
401
408
  }
402
409
  }
410
+ // Reject eval policies that are reserved in the vocabulary but not yet
411
+ // implemented. The Zod schema accepts the whole EVAL_POLICIES enum (the
412
+ // published JSON schema advertises them as reserved), but only `all_pass`
413
+ // is implemented at runtime. Without this check a workflow declaring
414
+ // `eval_policy: "any_pass"` or `"weighted"` parses and validates cleanly,
415
+ // then `aggregateEval` throws uncaught mid-run — a validate-then-crash
416
+ // failure mode. Reject it at load time instead. The runtime throw in
417
+ // aggregateEval stays as a defense-in-depth backstop.
418
+ for (const [nodeId, node] of Object.entries(workflow.nodes)) {
419
+ if (node.eval_policy != null && !SUPPORTED_EVAL_POLICIES.has(node.eval_policy)) {
420
+ errors.push({
421
+ code: "UNSUPPORTED_EVAL_POLICY",
422
+ message: `Node "${nodeId}" declares eval_policy "${node.eval_policy}", which is reserved but not yet implemented; use "all_pass" (the only supported policy in v1.0)`,
423
+ nodeId,
424
+ });
425
+ }
426
+ }
403
427
  // Inline skill definitions must have instruction or mcp
404
428
  for (const [skillId, def] of Object.entries(workflow.skills ?? {})) {
405
429
  if (!def.instruction && !def.mcp) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.104",
3
+ "version": "0.1.106",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"