@sweny-ai/core 0.1.123 → 0.1.124

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.
Files changed (56) hide show
  1. package/dist/agent-env.d.ts +171 -0
  2. package/dist/agent-env.js +393 -0
  3. package/dist/browser.d.ts +2 -2
  4. package/dist/browser.js +1 -1
  5. package/dist/claude.d.ts +61 -3
  6. package/dist/claude.js +187 -27
  7. package/dist/cli/check.d.ts +42 -1
  8. package/dist/cli/check.js +130 -8
  9. package/dist/cli/cloud-lifecycle.d.ts +24 -29
  10. package/dist/cli/cloud-lifecycle.js +187 -12
  11. package/dist/cli/cloud-report.js +45 -2
  12. package/dist/cli/config-file.d.ts +8 -1
  13. package/dist/cli/config-file.js +42 -1
  14. package/dist/cli/config.js +11 -14
  15. package/dist/cli/e2e.d.ts +22 -0
  16. package/dist/cli/e2e.js +31 -4
  17. package/dist/cli/main.d.ts +1 -0
  18. package/dist/cli/main.js +138 -46
  19. package/dist/cli/marketplace.d.ts +3 -1
  20. package/dist/cli/marketplace.js +2 -2
  21. package/dist/cli/new.d.ts +15 -0
  22. package/dist/cli/new.js +139 -65
  23. package/dist/cli/publish.js +9 -0
  24. package/dist/cli/run-output.d.ts +59 -0
  25. package/dist/cli/run-output.js +189 -0
  26. package/dist/cli/skill-env.d.ts +34 -0
  27. package/dist/cli/skill-env.js +64 -0
  28. package/dist/cli/templates.js +587 -0
  29. package/dist/cli/workflow-input.d.ts +41 -0
  30. package/dist/cli/workflow-input.js +56 -0
  31. package/dist/executor.d.ts +17 -1
  32. package/dist/executor.js +119 -9
  33. package/dist/index.d.ts +3 -3
  34. package/dist/index.js +2 -2
  35. package/dist/mermaid.d.ts +6 -0
  36. package/dist/mermaid.js +10 -5
  37. package/dist/schema.d.ts +218 -188
  38. package/dist/schema.js +24 -3
  39. package/dist/skills/betterstack.js +4 -0
  40. package/dist/skills/datadog.js +3 -0
  41. package/dist/skills/github.js +8 -0
  42. package/dist/skills/linear.js +8 -0
  43. package/dist/skills/notification.js +3 -0
  44. package/dist/skills/sentry.js +4 -0
  45. package/dist/skills/slack.js +2 -0
  46. package/dist/skills/supabase.js +9 -0
  47. package/dist/testing.d.ts +3 -2
  48. package/dist/testing.js +9 -3
  49. package/dist/types.d.ts +99 -2
  50. package/dist/types.js +17 -0
  51. package/dist/untrusted.d.ts +32 -0
  52. package/dist/untrusted.js +46 -0
  53. package/dist/workflows/browser.js +2 -2
  54. package/dist/workflows/implement.yml +2 -0
  55. package/dist/workflows/triage.yml +3 -1
  56. package/package.json +1 -1
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Small, side-effect-free helpers for building the `workflowInput` bag that
3
+ * `sweny workflow run` hands to `execute()`.
4
+ *
5
+ * Deliberately NOT in main.ts: main.ts ends with a top-level
6
+ * `await program.parseAsync()` (see bottom of file), so importing it at all
7
+ * triggers full CLI argv parsing as a side effect. That makes main.ts unsafe
8
+ * to `import` from a test. Anything worth unit-testing in isolation lives
9
+ * here instead (same pattern as config.ts / credentials.ts / e2e.ts).
10
+ */
11
+ /**
12
+ * Merge the CLI-level `--dry-run` flag into a `--input`-validated workflow
13
+ * input bag.
14
+ *
15
+ * #324: `sweny workflow run <file> --dry-run --input '{...}'` (what the
16
+ * GitHub Action and MCP server both pass) silently dropped `dryRun` before
17
+ * this fix: the `--input` branch built `workflowInput` from the caller's
18
+ * JSON alone, and only the config-derived (no-`--input`) branch merged in
19
+ * `config.dryRun`. The executor's fail-closed dry-run gate (see
20
+ * `advanceFromNode` in executor.ts) reads `input.dryRun` directly, so a
21
+ * dropped flag meant the full workflow, side effects included, ran anyway.
22
+ *
23
+ * Only forces `dryRun` *on*: when `--dry-run` is set it always wins, but a
24
+ * caller-supplied `dryRun: true` already present in the `--input` JSON must
25
+ * never be downgraded just because `--dry-run` was omitted.
26
+ */
27
+ export function mergeDryRunIntoInput(validatedInput, cliDryRun) {
28
+ if (!cliDryRun)
29
+ return validatedInput;
30
+ return { ...validatedInput, dryRun: true };
31
+ }
32
+ /**
33
+ * Parse `--timeout` / `--max-steps` for `sweny workflow run`. Throws a clear
34
+ * error on non-numeric or out-of-range values instead of silently falling
35
+ * back to a default (which hid typos like `--timeout 15m`).
36
+ *
37
+ * - `--timeout <ms>`: whole-run wall-clock budget in ms. `0` means no
38
+ * wall-clock budget (returned as `0`). Absent returns `defaultTimeoutMs`.
39
+ * - `--max-steps <n>`: positive integer. Absent returns `undefined` (the
40
+ * executor's own default applies).
41
+ */
42
+ export function parseRunBudgetFlags(timeout, maxSteps, defaultTimeoutMs) {
43
+ const parse = (raw, flag, min) => {
44
+ const t = raw.trim();
45
+ const n = t === "" ? Number.NaN : Number(t);
46
+ if (!Number.isInteger(n) || n < min) {
47
+ throw new Error(`${flag} must be ${min === 0 ? "a non-negative" : "a positive"} integer, got "${raw}"` +
48
+ (flag === "--timeout" ? " (milliseconds; 0 disables the wall-clock budget)" : ""));
49
+ }
50
+ return n;
51
+ };
52
+ return {
53
+ timeoutMs: timeout === undefined ? defaultTimeoutMs : parse(timeout, "--timeout", 0),
54
+ maxSteps: maxSteps === undefined ? undefined : parse(maxSteps, "--max-steps", 1),
55
+ };
56
+ }
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * This replaces ~8k lines of engine + recipe step code.
14
14
  */
15
- import type { Workflow, Skill, Claude, Observer, Logger, ExecutionResult } from "./types.js";
15
+ import type { Workflow, Skill, Tool, Claude, Observer, Logger, ExecutionResult } from "./types.js";
16
16
  export interface ExecuteOptions {
17
17
  /** Registered skills (id → Skill) */
18
18
  skills: Map<string, Skill>;
@@ -80,3 +80,19 @@ export declare const DEFAULT_MAX_STEPS = 200;
80
80
  * - `trace`: full ordered execution trace including loops and routing decisions
81
81
  */
82
82
  export declare function execute(workflow: Workflow, input: unknown, options: ExecuteOptions): Promise<ExecutionResult>;
83
+ /**
84
+ * Thrown when a conditional routing decision could not be made and there is
85
+ * no explicit default (unconditional) edge to fall back to. Fails closed: the
86
+ * executor refuses to guess an edge (the historical fail-open bug), so an LLM
87
+ * outage terminates the run loudly instead of silently taking the first
88
+ * conditional edge.
89
+ */
90
+ export declare class RouteEvaluationError extends Error {
91
+ readonly node: string;
92
+ constructor(node: string, message: string);
93
+ }
94
+ /**
95
+ * Dry-run tool gate (#380). Only an explicit `access: "read"` passes; a tool
96
+ * with no `access` is treated as a write so new tools fail safe.
97
+ */
98
+ export declare function isReadTool(tool: Pick<Tool, "access">): boolean;
package/dist/executor.js CHANGED
@@ -19,6 +19,8 @@ import { evaluateRequires } from "./requires.js";
19
19
  import { buildRetryPreamble } from "./retry.js";
20
20
  import { resolveExecutionModel } from "./model.js";
21
21
  import { buildToolAliases } from "./skills/index.js";
22
+ import { resolveAgentAccess } from "./agent-env.js";
23
+ import { fenceUntrusted } from "./untrusted.js";
22
24
  /**
23
25
  * Default workflow-level step budget. Counts total node executions across the
24
26
  * whole run (loop iterations included). Generous enough for legitimate deep
@@ -47,6 +49,7 @@ export async function execute(workflow, input, options) {
47
49
  const { claude, observer, signal, timeoutMs } = options;
48
50
  // If the caller already aborted before we started, fail fast.
49
51
  throwIfAborted(signal);
52
+ const dryRun = isDryRunInput(input);
50
53
  // Merge inline workflow skills into the skill map so they resolve at runtime.
51
54
  // Inline skills (instruction/mcp only) become Skill objects with empty tools/config.
52
55
  // The caller's skill map takes precedence — inline skills only fill gaps.
@@ -135,7 +138,15 @@ export async function execute(workflow, input, options) {
135
138
  // skill-tool filter (`tools.allow` / `tools.deny`). Filtered tools are
136
139
  // never registered for the run, so the model cannot see or call them.
137
140
  const resolvedSkillTools = resolveTools(node.skills, skills);
138
- const tools = filterNodeTools(resolvedSkillTools, node.tools, currentId, logger);
141
+ const filteredTools = filterNodeTools(resolvedSkillTools, node.tools, currentId, logger);
142
+ // Dry run (#380): only `access: "read"` tools reach the node. Write and
143
+ // unclassified tools are withheld (never registered, so never callable)
144
+ // and recorded on the node result as `skippedWrites`.
145
+ const tools = dryRun ? filteredTools.filter(isReadTool) : filteredTools;
146
+ const skippedWrites = dryRun ? filteredTools.filter((t) => !isReadTool(t)).map((t) => t.name) : [];
147
+ if (skippedWrites.length > 0) {
148
+ logger.info(` dry run: withheld write tools: ${skippedWrites.join(", ")}`, { node: currentId });
149
+ }
139
150
  const skillInstructions = resolveSkillInstructions(node.skills, skills);
140
151
  // Runtime guard: if this node declares skills but none resolved, the node
141
152
  // cannot do its job (e.g. "create a Linear issue" with no linear skill).
@@ -176,6 +187,13 @@ export async function execute(workflow, input, options) {
176
187
  trace.steps.push({ node: currentId, status: result.status, iteration });
177
188
  safeObserve(observer, { type: "node:exit", node: currentId, result }, logger);
178
189
  logger.warn(` requires ${onFail === "skip" ? "skipped" : "failed"}: ${requiresError}`, { node: currentId });
190
+ // Fail closed here too: a requires failure with on_fail: fail yields a
191
+ // failed node, which halts by default (same policy as a failed run).
192
+ if (result.status === "failed" && (node.on_fail ?? "halt") === "halt") {
193
+ logger.warn(` requires failed; halting workflow (on_fail: halt)`, { node: currentId });
194
+ safeObserve(observer, { type: "route", from: currentId, to: "(end)", reason: "node failed (on_fail: halt)" }, logger);
195
+ break;
196
+ }
179
197
  // Apply normal routing rules (dry run gate + resolveNext).
180
198
  // TODO: dedupe with requires path — see advanceFromNode helper below
181
199
  const next = await advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace, { signal, timeoutMs });
@@ -196,7 +214,8 @@ export async function execute(workflow, input, options) {
196
214
  // Prepend rules and context to instruction per cascade semantics
197
215
  const effectiveRules = assembleCascaded("rules", currentId, workflow.nodes[currentId], inputRules.length, (workflow.rules ?? []).length, resolvedSources);
198
216
  const effectiveContext = assembleCascaded("context", currentId, workflow.nodes[currentId], inputContext.length, (workflow.context ?? []).length, resolvedSources);
199
- const instruction = buildNodeInstruction(resolvedInstruction, effectiveRules, effectiveContext, input, skillInstructions);
217
+ const baseInstruction = buildNodeInstruction(resolvedInstruction, effectiveRules, effectiveContext, input, skillInstructions);
218
+ const instruction = dryRun ? `${dryRunNotice(skippedWrites)}\n\n---\n\n${baseInstruction}` : baseInstruction;
200
219
  // Run Claude on this node, with optional eval-failure retry loop.
201
220
  let attempt = 0;
202
221
  let result;
@@ -221,6 +240,8 @@ export async function execute(workflow, input, options) {
221
240
  model: nodeModel,
222
241
  signal,
223
242
  timeoutMs,
243
+ agentAccess: resolveAgentAccess(node.skills, skills),
244
+ ...(dryRun ? { readOnly: true } : {}),
224
245
  onProgress: (message) => {
225
246
  safeObserve(observer, { type: "node:progress", node: currentId, message }, logger);
226
247
  },
@@ -283,6 +304,23 @@ export async function execute(workflow, input, options) {
283
304
  logger.warn(` eval failed (attempt ${attempt + 1}/${retry.max + 1}): ${outcome.error}`, {
284
305
  node: currentId,
285
306
  });
307
+ // #325: a retry attempt re-invokes the agent on this node (full model
308
+ // spend) exactly like a fresh node visit, so it counts against the same
309
+ // `stepCount`/`maxSteps` budget. Checked BEFORE buildRetryPreamble so no
310
+ // paid reflection call happens once the budget is exhausted. On
311
+ // exhaustion, record a failed result and emit node:exit so observers
312
+ // never see node:enter without a matching node:exit.
313
+ stepCount++;
314
+ if (stepCount > maxSteps) {
315
+ const budgetError = `step budget exceeded: workflow '${workflow.id}' ran ${stepCount} steps (max_steps: ${maxSteps}) ` +
316
+ `while retrying node '${currentId}' (attempt ${attempt + 1}/${retry.max}). Lower 'retry.max' on the ` +
317
+ `offending node or raise 'max_steps' if the workflow legitimately needs more steps.`;
318
+ result = { ...result, status: "failed", data: { ...result.data, error: budgetError } };
319
+ results.set(currentId, result);
320
+ trace.steps.push({ node: currentId, status: "failed", iteration, retryAttempt: attempt });
321
+ safeObserve(observer, { type: "node:exit", node: currentId, result }, logger);
322
+ throw new Error(budgetError);
323
+ }
286
324
  const preamble = await buildRetryPreamble({
287
325
  retry,
288
326
  evalFailures: outcome.failures,
@@ -316,12 +354,26 @@ export async function execute(workflow, input, options) {
316
354
  data: { ...(result.data ?? {}), fail_soft: true },
317
355
  };
318
356
  }
357
+ if (skippedWrites.length > 0)
358
+ result = { ...result, skippedWrites };
319
359
  results.set(currentId, result);
320
360
  trace.steps.push(attempt > 0
321
361
  ? { node: currentId, status: result.status, iteration, retryAttempt: attempt }
322
362
  : { node: currentId, status: result.status, iteration });
323
363
  safeObserve(observer, { type: "node:exit", node: currentId, result }, logger);
324
364
  logger.info(` ✓ ${result.status}`, { node: currentId, toolCalls: result.toolCalls.length });
365
+ // Fail closed on node failure. A node that finishes `failed` (agent-level
366
+ // failure, or an eval failure that exhausted retries and was not softened
367
+ // by `fail_soft`) HALTS the workflow by default. This stops a broken node
368
+ // from advancing down a conditional out-edge (e.g. filing an issue/PR) on
369
+ // the back of a failure. The failed result stays in `results`, so the run
370
+ // surfaces as failed to callers (CLI exit code, cloud status). Authors who
371
+ // want the legacy fall-through opt in per-node with `on_fail: "continue"`.
372
+ if (result.status === "failed" && (node.on_fail ?? "halt") === "halt") {
373
+ logger.warn(` node failed; halting workflow (on_fail: halt)`, { node: currentId });
374
+ safeObserve(observer, { type: "route", from: currentId, to: "(end)", reason: "node failed (on_fail: halt)" }, logger);
375
+ break;
376
+ }
325
377
  // Dry run gate + routing — shared with requires path via advanceFromNode helper.
326
378
  currentId = await advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace, {
327
379
  signal,
@@ -335,6 +387,21 @@ export async function execute(workflow, input, options) {
335
387
  return { results, trace };
336
388
  }
337
389
  // ─── Internals ───────────────────────────────────────────────────
390
+ /**
391
+ * Thrown when a conditional routing decision could not be made and there is
392
+ * no explicit default (unconditional) edge to fall back to. Fails closed: the
393
+ * executor refuses to guess an edge (the historical fail-open bug), so an LLM
394
+ * outage terminates the run loudly instead of silently taking the first
395
+ * conditional edge.
396
+ */
397
+ export class RouteEvaluationError extends Error {
398
+ node;
399
+ constructor(node, message) {
400
+ super(message);
401
+ this.node = node;
402
+ this.name = "RouteEvaluationError";
403
+ }
404
+ }
338
405
  /**
339
406
  * Build the full instruction for a node.
340
407
  *
@@ -353,7 +420,8 @@ function buildNodeInstruction(baseInstruction, effectiveRules, effectiveContext,
353
420
  sections.push(`## Rules — You MUST Follow These\n\n${effectiveRules}`);
354
421
  }
355
422
  if (effectiveContext) {
356
- sections.push(`## Background Context\n\n${effectiveContext}`);
423
+ // Context Sources can be fetched pages or runtime input: fence as untrusted (#360).
424
+ sections.push(`## Background Context\n\n${fenceUntrusted(effectiveContext, "background-context")}`);
357
425
  }
358
426
  // Legacy fallback for `input.additionalContext` when no rules/context cascaded
359
427
  if (sections.length === 0) {
@@ -635,6 +703,24 @@ function mergeInlineSkills(skills, inlineSkills) {
635
703
  }
636
704
  return merged;
637
705
  }
706
+ /** True when the run input requests a dry run (`dryRun === true`, strictly). */
707
+ function isDryRunInput(input) {
708
+ return input != null && typeof input === "object" && input.dryRun === true;
709
+ }
710
+ /**
711
+ * Dry-run tool gate (#380). Only an explicit `access: "read"` passes; a tool
712
+ * with no `access` is treated as a write so new tools fail safe.
713
+ */
714
+ export function isReadTool(tool) {
715
+ return tool.access === "read";
716
+ }
717
+ /** Instruction section prepended to every node under dry-run. */
718
+ function dryRunNotice(skippedWrites) {
719
+ const withheld = skippedWrites.length > 0 ? `These write tools were withheld from this step: ${skippedWrites.join(", ")}. ` : "";
720
+ return (`## Dry run\n\nThis is a dry run. Only read-only tools are available. ${withheld}` +
721
+ `Do not create, modify, post, or send anything. Do the analysis, and where this step would ` +
722
+ `normally write, describe exactly what it would have written instead.`);
723
+ }
638
724
  function resolveTools(skillIds, skills) {
639
725
  return skillIds
640
726
  .map((id) => skills.get(id))
@@ -745,12 +831,13 @@ function resolveConfig(skills, overrides, env = process.env) {
745
831
  * Returns the next node ID, or null when execution should stop.
746
832
  */
747
833
  async function advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace, abort) {
748
- // Dry run hard gate — stop at the first conditional routing decision.
749
- // Unconditional edges are analysis flow (prepare→gather→investigate);
750
- // conditional edges are action decisions (investigate→create_issue/skip).
751
- // Enforced in the executor so it cannot be bypassed by LLM evaluation.
752
- const isDryRun = input && typeof input === "object" && input.dryRun === true;
753
- if (isDryRun) {
834
+ // Dry run path gate: stop at the first conditional routing decision.
835
+ // Safety does not depend on this (#380): under dry-run every node already
836
+ // runs read-only (see execute()). The stop keeps the dry-run path a pure
837
+ // function of the graph (no LLM route evaluation, so the same workflow
838
+ // always visits the same nodes) and skips action branches whose output
839
+ // would only describe writes that cannot happen.
840
+ if (isDryRunInput(input)) {
754
841
  const outEdges = workflow.edges.filter((e) => e.from === currentId);
755
842
  if (outEdges.some((e) => e.when)) {
756
843
  safeObserve(observer, { type: "route", from: currentId, to: "(end)", reason: "dry run" }, logger);
@@ -871,6 +958,29 @@ async function resolveNext(workflow, current, results, input, claude, observer,
871
958
  signal: abort?.signal,
872
959
  timeoutMs: abort?.timeoutMs,
873
960
  });
961
+ // Fail closed. `evaluate` returns null when the routing decision could not
962
+ // be made (SDK error, timeout, non-success subtype, or an unparseable
963
+ // answer). We must NOT fall through to a conditional edge: on a node with a
964
+ // single conditional out-edge, "the first choice" IS that edge, so an outage
965
+ // would always take it (the fail-open bug that silently filed real
966
+ // issues/PRs instead of routing to `skip`). Take the author's explicit
967
+ // default/else edge if one exists; otherwise terminate the run loudly.
968
+ if (chosen === null) {
969
+ if (defaultEdge) {
970
+ logger?.warn(` route eval: evaluation failed for node '${current}'; taking default (unconditional) edge '${defaultEdge.to}'.`, { node: current });
971
+ safeObserve(observer, { type: "route", from: current, to: defaultEdge.to, reason: "route evaluation failed; default edge" }, logger);
972
+ if (edgeCounts) {
973
+ const key = `${current}→${defaultEdge.to}`;
974
+ edgeCounts.set(key, (edgeCounts.get(key) ?? 0) + 1);
975
+ }
976
+ return defaultEdge.to;
977
+ }
978
+ logger?.error(` route eval: evaluation failed for node '${current}' and there is no default (unconditional) edge; ` +
979
+ `refusing to fail open. Terminating the run. Add a default edge or fix the model backend.`, { node: current });
980
+ safeObserve(observer, { type: "route", from: current, to: "(end)", reason: "route evaluation failed; no default edge" }, logger);
981
+ throw new RouteEvaluationError(current, `route evaluation failed for node '${current}' and no default (unconditional) edge exists; ` +
982
+ `refusing to fail open`);
983
+ }
874
984
  // Validate that Claude returned a valid target.
875
985
  const validTargets = new Set(outEdges.map((e) => e.to));
876
986
  if (!validTargets.has(chosen)) {
package/dist/index.d.ts CHANGED
@@ -21,11 +21,11 @@
21
21
  * })
22
22
  * ```
23
23
  */
24
- export type { Skill, SkillCategory, SkillDefinition, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, TraceStep, TraceEdge, ExecutionTrace, ExecutionResult, Source, ResolvedSource, SourceKind, SourceResolutionMap, WorkflowInputs, WorkflowInputField, WorkflowInputType, InputValidationError, InputValidationResult, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, NodeToolFilter, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, McpTransport, WorkflowType, SkillHarnessKey, } from "./types.js";
24
+ export type { Skill, SkillCategory, SkillDefinition, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, TraceStep, TraceEdge, ExecutionTrace, ExecutionResult, Source, ResolvedSource, SourceKind, SourceResolutionMap, WorkflowInputs, WorkflowInputField, WorkflowInputType, InputValidationError, InputValidationResult, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, NodeToolFilter, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, NodeOnFail, McpTransport, WorkflowType, SkillHarnessKey, } from "./types.js";
25
25
  export { WORKFLOW_INPUT_TYPES } from "./types.js";
26
- 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";
26
+ export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, NODE_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, } from "./types.js";
27
27
  export { consoleLogger } from "./types.js";
28
- export { execute } from "./executor.js";
28
+ export { execute, RouteEvaluationError } from "./executor.js";
29
29
  export type { ExecuteOptions } from "./executor.js";
30
30
  export { ClaudeClient, resolveAuthEnv } from "./claude.js";
31
31
  export type { ClaudeClientOptions, SwenyAuthMode, ResolveAuthEnvOpts } from "./claude.js";
package/dist/index.js CHANGED
@@ -23,10 +23,10 @@
23
23
  */
24
24
  export { WORKFLOW_INPUT_TYPES } from "./types.js";
25
25
  // Runtime enum constants + skill-id helpers
26
- 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";
26
+ export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, NODE_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, } from "./types.js";
27
27
  export { consoleLogger } from "./types.js";
28
28
  // Executor
29
- export { execute } from "./executor.js";
29
+ export { execute, RouteEvaluationError } from "./executor.js";
30
30
  // Claude client
31
31
  export { ClaudeClient, resolveAuthEnv } from "./claude.js";
32
32
  // Execution model resolution
package/dist/mermaid.d.ts CHANGED
@@ -20,6 +20,12 @@ export interface MermaidOptions {
20
20
  direction?: "TB" | "LR";
21
21
  /** Title rendered above the diagram */
22
22
  title?: string;
23
+ /**
24
+ * Override the `classDef` style body (the part after the class name) for
25
+ * specific statuses, e.g. `{ success: "fill:#2563eb,stroke:#1d4ed8,color:#fff" }`.
26
+ * Unset statuses keep the defaults.
27
+ */
28
+ classDefs?: Partial<Record<NodeStatus, string>>;
23
29
  }
24
30
  /**
25
31
  * Convert a Workflow to a Mermaid flowchart diagram string.
package/dist/mermaid.js CHANGED
@@ -24,7 +24,7 @@
24
24
  * ```
25
25
  */
26
26
  export function toMermaid(workflow, options = {}) {
27
- const { state = {}, trace, direction = "TB", title } = options;
27
+ const { state = {}, trace, direction = "TB", title, classDefs = {} } = options;
28
28
  const lines = [];
29
29
  if (title) {
30
30
  lines.push("---");
@@ -131,10 +131,15 @@ export function toMermaid(workflow, options = {}) {
131
131
  const statusNodes = groupByStatus(state);
132
132
  if (statusNodes.size > 0) {
133
133
  lines.push("");
134
- lines.push(" classDef current fill:#3b82f6,stroke:#2563eb,color:#fff,stroke-width:2px");
135
- lines.push(" classDef success fill:#22c55e,stroke:#16a34a,color:#fff,stroke-width:2px");
136
- lines.push(" classDef failed fill:#ef4444,stroke:#dc2626,color:#fff,stroke-width:2px");
137
- lines.push(" classDef skipped fill:#6b7280,stroke:#4b5563,color:#fff,stroke-dasharray:5 5");
134
+ const defaults = {
135
+ current: "fill:#3b82f6,stroke:#2563eb,color:#fff,stroke-width:2px",
136
+ success: "fill:#22c55e,stroke:#16a34a,color:#fff,stroke-width:2px",
137
+ failed: "fill:#ef4444,stroke:#dc2626,color:#fff,stroke-width:2px",
138
+ skipped: "fill:#6b7280,stroke:#4b5563,color:#fff,stroke-dasharray:5 5",
139
+ };
140
+ for (const status of ["current", "success", "failed", "skipped"]) {
141
+ lines.push(` classDef ${status} ${classDefs[status] ?? defaults[status]}`);
142
+ }
138
143
  for (const [status, ids] of statusNodes) {
139
144
  const sanitized = ids.map(aliasFor).join(",");
140
145
  lines.push(` class ${sanitized} ${status}`);