@sweny-ai/core 0.1.45 → 0.1.46

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/cli/main.js CHANGED
@@ -336,7 +336,7 @@ triageCmd.action(async (options) => {
336
336
  contextUrls,
337
337
  };
338
338
  try {
339
- const { results } = await execute(triageWorkflow, workflowInput, {
339
+ const { results, trace } = await execute(triageWorkflow, workflowInput, {
340
340
  skills,
341
341
  claude,
342
342
  observer,
@@ -353,7 +353,10 @@ triageCmd.action(async (options) => {
353
353
  // GitHub Actions step summary
354
354
  if (config.notificationProvider === "github-summary" && process.env.GITHUB_STEP_SUMMARY) {
355
355
  try {
356
- const md = formatDagResultMarkdown(results, durationMs, config);
356
+ const md = formatDagResultMarkdown(results, durationMs, config, {
357
+ workflow: triageWorkflow,
358
+ trace,
359
+ });
357
360
  fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, md);
358
361
  }
359
362
  catch (err) {
@@ -1,6 +1,6 @@
1
1
  import type { CliConfig } from "./config.js";
2
2
  import type { CheckResult } from "./check.js";
3
- import type { NodeResult } from "../types.js";
3
+ import type { NodeResult, ExecutionTrace, Workflow } from "../types.js";
4
4
  export declare const c: {
5
5
  brand: import("chalk").ChalkInstance;
6
6
  brandDim: import("chalk").ChalkInstance;
@@ -27,10 +27,23 @@ export declare function formatResultJson(results: Map<string, NodeResult>): stri
27
27
  /**
28
28
  * Format triage results as GitHub-flavored markdown for `$GITHUB_STEP_SUMMARY`.
29
29
  *
30
- * Designed to render nicely in the GitHub Actions job summary panel:
31
- * - Status line with an emoji for quick visual scanning
32
- * - Links to any created issues or PRs
33
- * - Findings table (for dry runs or no-action results)
34
- * - Error detail when a node failed
30
+ * Designed to render richly in the GitHub Actions job summary panel:
31
+ * - Status header with an emoji
32
+ * - Mermaid workflow diagram colored by node execution state (when a
33
+ * workflow + trace are supplied; this is the headline feature of the
34
+ * job summary view)
35
+ * - Config table (repository, providers, time range, mode)
36
+ * - Workflow path showing actual execution sequence including loop iterations
37
+ * - Routing decisions (collapsible) when the DAG had conditional edges
38
+ * - Findings table
39
+ * - Actions taken (issue/PR links)
40
+ * - Recommendation / next step
41
+ * - Node execution details (collapsible) with tool-call summaries
35
42
  */
36
- export declare function formatDagResultMarkdown(results: Map<string, NodeResult>, durationMs: number, config?: CliConfig): string;
43
+ export interface FormatMarkdownOptions {
44
+ /** Workflow definition — required for the Mermaid diagram. */
45
+ workflow?: Workflow;
46
+ /** Execution trace from `execute()` — colors the diagram and drives the path/routing sections. */
47
+ trace?: ExecutionTrace;
48
+ }
49
+ export declare function formatDagResultMarkdown(results: Map<string, NodeResult>, durationMs: number, config?: CliConfig, options?: FormatMarkdownOptions): string;
@@ -1,4 +1,5 @@
1
1
  import chalk from "chalk";
2
+ import { toMermaidBlock } from "../mermaid.js";
2
3
  // ── Color palette ───────────────────────────────────────────────
3
4
  export const c = {
4
5
  brand: chalk.hex("#FF6B2B"),
@@ -404,19 +405,10 @@ export function formatCheckResults(results) {
404
405
  export function formatResultJson(results) {
405
406
  return JSON.stringify(Object.fromEntries(results), null, 2);
406
407
  }
407
- // ── Markdown output (for GitHub Actions step summary) ───────────
408
- /**
409
- * Format triage results as GitHub-flavored markdown for `$GITHUB_STEP_SUMMARY`.
410
- *
411
- * Designed to render nicely in the GitHub Actions job summary panel:
412
- * - Status line with an emoji for quick visual scanning
413
- * - Links to any created issues or PRs
414
- * - Findings table (for dry runs or no-action results)
415
- * - Error detail when a node failed
416
- */
417
- export function formatDagResultMarkdown(results, durationMs, config) {
408
+ export function formatDagResultMarkdown(results, durationMs, config, options = {}) {
418
409
  const duration = formatDuration(durationMs);
419
410
  const lines = [];
411
+ const { workflow, trace } = options;
420
412
  // Detect failure anywhere in the DAG
421
413
  let failedNodeId;
422
414
  let failedResult;
@@ -427,33 +419,24 @@ export function formatDagResultMarkdown(results, durationMs, config) {
427
419
  break;
428
420
  }
429
421
  }
430
- if (failedResult) {
431
- lines.push(`## ❌ SWEny Triage Failed`);
432
- lines.push("");
433
- lines.push(`**Failed at:** \`${failedNodeId}\` `);
434
- lines.push(`**Duration:** ${duration}`);
435
- if (failedResult.data?.error) {
436
- lines.push("");
437
- lines.push("```");
438
- lines.push(String(failedResult.data.error));
439
- lines.push("```");
440
- }
441
- return lines.join("\n") + "\n";
442
- }
443
422
  const investigateData = results.get("investigate")?.data;
444
423
  const issueData = results.get("create_issue")?.data;
445
424
  const prData = results.get("create_pr")?.data;
446
425
  const novelCount = investigateData?.novel_count ?? 0;
447
426
  const highestSeverity = investigateData?.highest_severity;
448
427
  const findings = investigateData?.findings;
449
- // Header — pick the most informative title
450
- if (prData?.prUrl) {
428
+ const isDryRun = Boolean(config?.dryRun);
429
+ // ── Header ────────────────────────────────────────────────────
430
+ if (failedResult) {
431
+ lines.push(`## ❌ SWEny Triage Failed`);
432
+ }
433
+ else if (prData?.prUrl) {
451
434
  lines.push(`## ✅ SWEny Triage — PR opened`);
452
435
  }
453
436
  else if (issueData?.issueIdentifier || issueData?.issueUrl) {
454
437
  lines.push(`## ✅ SWEny Triage — Issue created`);
455
438
  }
456
- else if (config?.dryRun) {
439
+ else if (isDryRun) {
457
440
  lines.push(`## 🔍 SWEny Triage — Dry run complete`);
458
441
  }
459
442
  else if (novelCount === 0) {
@@ -463,67 +446,167 @@ export function formatDagResultMarkdown(results, durationMs, config) {
463
446
  lines.push(`## ℹ️ SWEny Triage — No action taken`);
464
447
  }
465
448
  lines.push("");
466
- lines.push(`**Duration:** ${duration} `);
467
- if (config?.serviceFilter && config.serviceFilter !== "*") {
468
- lines.push(`**Service filter:** \`${config.serviceFilter}\` `);
469
- }
470
- if (config?.timeRange) {
471
- lines.push(`**Time range:** ${config.timeRange} `);
472
- }
473
- if (highestSeverity) {
474
- lines.push(`**Highest severity:** ${highestSeverity} `);
475
- }
476
- lines.push(`**Novel findings:** ${novelCount}`);
477
- // Issue and PR links
478
- if (issueData?.issueIdentifier || issueData?.issueUrl) {
479
- lines.push("");
480
- lines.push("### 📋 Issue");
481
- const id = issueData.issueIdentifier ? `**${String(issueData.issueIdentifier)}** — ` : "";
482
- const title = issueData.issueTitle ? String(issueData.issueTitle) : "";
483
- const url = issueData.issueUrl ? String(issueData.issueUrl) : "";
484
- if (url) {
485
- lines.push(`${id}[${title || url}](${url})`);
486
- }
487
- else {
488
- lines.push(`${id}${title}`);
449
+ // ── Workflow diagram (Mermaid) ────────────────────────────────
450
+ // Requires a workflow definition; trace is optional but makes it much more useful.
451
+ if (workflow) {
452
+ const state = {};
453
+ for (const [nodeId, result] of results) {
454
+ state[nodeId] = result.status === "success" ? "success" : result.status === "failed" ? "failed" : "skipped";
489
455
  }
456
+ lines.push(toMermaidBlock(workflow, { state, trace, title: workflow.name }));
457
+ lines.push("");
490
458
  }
491
- if (prData?.prUrl) {
459
+ // ── Config / run metadata table ───────────────────────────────
460
+ const rows = [];
461
+ if (config?.repository)
462
+ rows.push(["Repository", `\`${config.repository}\``]);
463
+ if (config?.observabilityProvider)
464
+ rows.push(["Observability", config.observabilityProvider]);
465
+ if (config?.issueTrackerProvider)
466
+ rows.push(["Issue tracker", config.issueTrackerProvider]);
467
+ if (config?.timeRange)
468
+ rows.push(["Time range", config.timeRange]);
469
+ if (config?.serviceFilter && config.serviceFilter !== "*") {
470
+ rows.push(["Service filter", `\`${config.serviceFilter}\``]);
471
+ }
472
+ if (highestSeverity)
473
+ rows.push(["Highest severity", highestSeverity]);
474
+ rows.push(["Novel findings", String(novelCount)]);
475
+ rows.push(["Duration", duration]);
476
+ rows.push(["Mode", isDryRun ? "dry run" : "live"]);
477
+ lines.push("| Setting | Value |");
478
+ lines.push("| --- | --- |");
479
+ for (const [k, v] of rows)
480
+ lines.push(`| ${k} | ${v} |`);
481
+ lines.push("");
482
+ // ── Workflow path (actual execution sequence including loops) ─
483
+ if (trace && trace.steps.length > 0) {
484
+ const pathSteps = trace.steps.map((s) => {
485
+ const tag = s.iteration > 1 ? ` ×${s.iteration}` : "";
486
+ return `\`${s.node}${tag}\``;
487
+ });
488
+ lines.push(`**Workflow path:** ${pathSteps.join(" → ")}`);
489
+ const routingLines = trace.edges
490
+ .filter((e) => e.reason !== "only path")
491
+ .map((e) => `- \`${e.from}\` → \`${e.to}\` *(${e.reason})*`);
492
+ if (routingLines.length > 0) {
493
+ lines.push("");
494
+ lines.push("<details><summary>Routing decisions</summary>");
495
+ lines.push("");
496
+ lines.push(...routingLines);
497
+ lines.push("");
498
+ lines.push("</details>");
499
+ }
492
500
  lines.push("");
493
- lines.push("### 🔧 Pull request");
494
- const num = prData.prNumber ? `#${String(prData.prNumber)}` : "";
495
- lines.push(`[${num || String(prData.prUrl)}](${String(prData.prUrl)})`);
496
501
  }
497
- // Findings table — mostly useful for dry runs and no-action results
498
- if (findings && findings.length > 0 && !prData?.prUrl) {
502
+ // ── Failure detail (short-circuit) ────────────────────────────
503
+ if (failedResult) {
504
+ lines.push(`**Failed at:** \`${failedNodeId}\``);
505
+ if (failedResult.data?.error) {
506
+ lines.push("");
507
+ lines.push("```");
508
+ lines.push(String(failedResult.data.error));
509
+ lines.push("```");
510
+ }
511
+ // Still render node details below so debugging is easy
499
512
  lines.push("");
513
+ appendNodeDetails(lines, results);
514
+ return lines.join("\n") + "\n";
515
+ }
516
+ // ── Findings table ────────────────────────────────────────────
517
+ if (findings && findings.length > 0) {
500
518
  lines.push("### 🔎 Findings");
501
519
  lines.push("");
502
- lines.push("| Severity | Title | Status |");
503
- lines.push("| --- | --- | --- |");
504
- for (const f of findings.slice(0, 10)) {
520
+ lines.push("| # | Severity | Title | Complexity | Status |");
521
+ lines.push("| --- | --- | --- | --- | --- |");
522
+ for (let i = 0; i < findings.length; i++) {
523
+ const f = findings[i];
505
524
  const sev = String(f.severity ?? "—");
506
- const title = String(f.title ?? "").replace(/\|/g, "\\|");
507
- const status = f.is_duplicate ? "duplicate" : "novel";
508
- lines.push(`| ${sev} | ${title} | ${status} |`);
525
+ const title = String(f.title ?? "—").replace(/\|/g, "\\|");
526
+ const complexity = String(f.fix_complexity ?? "—");
527
+ const status = f.is_duplicate ? `dup of ${f.duplicate_of ?? "existing"}` : "novel";
528
+ lines.push(`| ${i + 1} | ${sev} | ${title} | ${complexity} | ${status} |`);
509
529
  }
510
- if (findings.length > 10) {
511
- lines.push("");
512
- lines.push(`_…and ${findings.length - 10} more_`);
530
+ lines.push("");
531
+ }
532
+ // ── Actions taken ─────────────────────────────────────────────
533
+ if (issueData?.issueIdentifier || issueData?.issueUrl || prData?.prUrl) {
534
+ lines.push("### Actions taken");
535
+ lines.push("");
536
+ if (issueData?.issueIdentifier || issueData?.issueUrl) {
537
+ const id = issueData.issueIdentifier ? String(issueData.issueIdentifier) : "";
538
+ const title = issueData.issueTitle ? String(issueData.issueTitle) : "";
539
+ const url = issueData.issueUrl ? String(issueData.issueUrl) : "";
540
+ const link = url ? `[${id || url}](${url})` : id;
541
+ lines.push(`- **Issue created:** ${link}${title ? ` — ${title}` : ""}`);
513
542
  }
543
+ if (prData?.prUrl) {
544
+ const num = prData.prNumber ? `#${String(prData.prNumber)}` : "";
545
+ const link = `[${num || String(prData.prUrl)}](${String(prData.prUrl)})`;
546
+ lines.push(`- **PR opened:** ${link}`);
547
+ }
548
+ lines.push("");
514
549
  }
515
- // Recommendation / next step
550
+ // ── Recommendation / next step ────────────────────────────────
516
551
  const rec = investigateData?.recommendation;
517
552
  if (rec) {
518
- lines.push("");
519
553
  lines.push(`**Next:** ${String(rec)}`);
520
- }
521
- if (config?.dryRun) {
522
554
  lines.push("");
555
+ }
556
+ if (isDryRun) {
523
557
  lines.push("_Dry run mode — no side effects were taken._");
558
+ lines.push("");
524
559
  }
560
+ // ── Node execution details (collapsible) ──────────────────────
561
+ appendNodeDetails(lines, results);
525
562
  return lines.join("\n") + "\n";
526
563
  }
564
+ function appendNodeDetails(lines, results) {
565
+ if (results.size === 0)
566
+ return;
567
+ lines.push("<details><summary>Node execution details</summary>");
568
+ lines.push("");
569
+ for (const [nodeId, result] of results) {
570
+ const icon = result.status === "success" ? "✓" : result.status === "failed" ? "✗" : "—";
571
+ const toolCount = result.toolCalls?.length ?? 0;
572
+ lines.push(`#### ${icon} \`${nodeId}\` — ${toolCount} tool call${toolCount === 1 ? "" : "s"}`);
573
+ lines.push("");
574
+ if (toolCount > 0 && result.toolCalls) {
575
+ lines.push("| Tool | Input (truncated) |");
576
+ lines.push("| --- | --- |");
577
+ for (const tc of result.toolCalls.slice(0, 20)) {
578
+ const name = tc.tool ?? "—";
579
+ const input = summarizeToolInput(tc.input).replace(/\|/g, "\\|");
580
+ lines.push(`| \`${name}\` | ${input || "—"} |`);
581
+ }
582
+ if (toolCount > 20) {
583
+ lines.push(`| … | _${toolCount - 20} more tool calls_ |`);
584
+ }
585
+ lines.push("");
586
+ }
587
+ }
588
+ lines.push("</details>");
589
+ lines.push("");
590
+ }
591
+ function summarizeToolInput(input) {
592
+ if (input === undefined || input === null)
593
+ return "";
594
+ if (typeof input === "string")
595
+ return truncate(input, 100);
596
+ if (typeof input !== "object")
597
+ return truncate(String(input), 100);
598
+ const parts = [];
599
+ for (const [k, v] of Object.entries(input)) {
600
+ if (typeof v === "string")
601
+ parts.push(`${k}=${truncate(v, 40)}`);
602
+ else if (typeof v === "number" || typeof v === "boolean")
603
+ parts.push(`${k}=${v}`);
604
+ }
605
+ return truncate(parts.join(", "), 100);
606
+ }
607
+ function truncate(s, n) {
608
+ return s.length > n ? s.slice(0, n - 1) + "…" : s;
609
+ }
527
610
  // ── Helpers ─────────────────────────────────────────────────────
528
611
  function formatDuration(ms) {
529
612
  const s = Math.round(ms / 1000);
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { formatDagResultMarkdown } from "./output.js";
3
+ import { triageWorkflow } from "../workflows/index.js";
3
4
  function success(data = {}) {
4
5
  return { status: "success", data, toolCalls: [] };
5
6
  }
@@ -10,10 +11,41 @@ function minimalConfig(overrides = {}) {
10
11
  return {
11
12
  timeRange: "6h",
12
13
  serviceFilter: "permit-service",
14
+ repository: "acme/app",
15
+ observabilityProvider: "betterstack",
16
+ issueTrackerProvider: "linear",
13
17
  dryRun: false,
14
18
  ...overrides,
15
19
  };
16
20
  }
21
+ /** Build a minimal fake workflow for tests that don't need the full triage graph. */
22
+ function fakeWorkflow() {
23
+ return {
24
+ id: "test",
25
+ name: "Test Workflow",
26
+ description: "Minimal fixture for output.test.ts — not a real DAG.",
27
+ entry: "investigate",
28
+ nodes: {
29
+ investigate: { name: "Investigate", instruction: "noop", skills: [] },
30
+ create_issue: { name: "Create Issue", instruction: "noop", skills: [] },
31
+ create_pr: { name: "Open PR", instruction: "noop", skills: [] },
32
+ },
33
+ edges: [
34
+ { from: "investigate", to: "create_issue" },
35
+ { from: "create_issue", to: "create_pr" },
36
+ ],
37
+ };
38
+ }
39
+ function fakeTrace(steps) {
40
+ return {
41
+ steps: steps.map((s) => ({
42
+ node: s.node,
43
+ status: "success",
44
+ iteration: s.iteration ?? 1,
45
+ })),
46
+ edges: [],
47
+ };
48
+ }
17
49
  describe("formatDagResultMarkdown", () => {
18
50
  it("renders a success header with issue + PR when both exist", () => {
19
51
  const results = new Map([
@@ -33,13 +65,15 @@ describe("formatDagResultMarkdown", () => {
33
65
  ]);
34
66
  const md = formatDagResultMarkdown(results, 15_000, minimalConfig());
35
67
  expect(md).toContain("## ✅ SWEny Triage — PR opened");
36
- expect(md).toContain("**Duration:** 15s");
37
- expect(md).toContain("**Service filter:** `permit-service`");
38
- expect(md).toContain("**Highest severity:** high");
68
+ expect(md).toContain("| Duration | 15s |");
69
+ expect(md).toContain("| Service filter | `permit-service` |");
70
+ expect(md).toContain("| Highest severity | high |");
39
71
  expect(md).toContain("OFF-1234");
40
72
  expect(md).toContain("https://linear.app/acme/issue/OFF-1234");
41
73
  expect(md).toContain("#42");
42
74
  expect(md).toContain("https://github.com/acme/app/pull/42");
75
+ expect(md).toContain("**Issue created:**");
76
+ expect(md).toContain("**PR opened:**");
43
77
  });
44
78
  it("renders an issue-only result when no PR was opened", () => {
45
79
  const results = new Map([
@@ -52,13 +86,13 @@ describe("formatDagResultMarkdown", () => {
52
86
  const md = formatDagResultMarkdown(results, 5_000, minimalConfig());
53
87
  expect(md).toContain("## ✅ SWEny Triage — Issue created");
54
88
  expect(md).toContain("OFF-5");
55
- expect(md).not.toContain("Pull request");
89
+ expect(md).not.toContain("**PR opened:**");
56
90
  });
57
91
  it("renders a clean no-incident result when novel_count is 0", () => {
58
92
  const results = new Map([["investigate", success({ novel_count: 0 })]]);
59
93
  const md = formatDagResultMarkdown(results, 3_000, minimalConfig());
60
94
  expect(md).toContain("## ✅ SWEny Triage — No new incidents");
61
- expect(md).toContain("**Novel findings:** 0");
95
+ expect(md).toContain("| Novel findings | 0 |");
62
96
  });
63
97
  it("renders a findings table in dry-run mode", () => {
64
98
  const results = new Map([
@@ -68,18 +102,19 @@ describe("formatDagResultMarkdown", () => {
68
102
  novel_count: 2,
69
103
  highest_severity: "high",
70
104
  findings: [
71
- { severity: "high", title: "OOM in worker", is_duplicate: false },
72
- { severity: "low", title: "Stale metric", is_duplicate: true },
105
+ { severity: "high", title: "OOM in worker", is_duplicate: false, fix_complexity: "medium" },
106
+ { severity: "low", title: "Stale metric", is_duplicate: true, duplicate_of: "OFF-99" },
73
107
  ],
74
108
  }),
75
109
  ],
76
110
  ]);
77
111
  const md = formatDagResultMarkdown(results, 9_000, minimalConfig({ dryRun: true }));
78
112
  expect(md).toContain("## 🔍 SWEny Triage — Dry run complete");
79
- expect(md).toContain("| Severity | Title | Status |");
80
- expect(md).toContain("| high | OOM in worker | novel |");
81
- expect(md).toContain("| low | Stale metric | duplicate |");
113
+ expect(md).toContain("| # | Severity | Title | Complexity | Status |");
114
+ expect(md).toContain("| 1 | high | OOM in worker | medium | novel |");
115
+ expect(md).toContain("| 2 | low | Stale metric | — | dup of OFF-99 |");
82
116
  expect(md).toContain("_Dry run mode — no side effects were taken._");
117
+ expect(md).toContain("| Mode | dry run |");
83
118
  });
84
119
  it("renders a failure summary when a node failed", () => {
85
120
  const results = new Map([
@@ -104,4 +139,41 @@ describe("formatDagResultMarkdown", () => {
104
139
  const md = formatDagResultMarkdown(results, 1_000, minimalConfig({ dryRun: true }));
105
140
  expect(md).toContain("broken \\| pipe \\| title");
106
141
  });
142
+ it("embeds a mermaid workflow diagram when workflow is supplied", () => {
143
+ const results = new Map([
144
+ ["investigate", success({ novel_count: 0 })],
145
+ ["create_issue", success({ issueIdentifier: "OFF-7", issueUrl: "https://linear.app/x/7" })],
146
+ ]);
147
+ const workflow = fakeWorkflow();
148
+ const md = formatDagResultMarkdown(results, 4_000, minimalConfig(), { workflow });
149
+ expect(md).toContain("```mermaid");
150
+ expect(md).toContain("graph TB");
151
+ expect(md).toContain("title: Test Workflow");
152
+ // Status classes should be applied for nodes that ran successfully
153
+ expect(md).toContain("class investigate,create_issue success");
154
+ });
155
+ it("renders the workflow path and iteration counts from the execution trace", () => {
156
+ const results = new Map([
157
+ ["investigate", success({ novel_count: 1 })],
158
+ ["create_issue", success({ issueIdentifier: "OFF-8" })],
159
+ ]);
160
+ const workflow = fakeWorkflow();
161
+ const trace = fakeTrace([
162
+ { node: "investigate" },
163
+ { node: "create_issue" },
164
+ { node: "create_issue", iteration: 2 },
165
+ ]);
166
+ const md = formatDagResultMarkdown(results, 8_000, minimalConfig(), { workflow, trace });
167
+ expect(md).toContain("**Workflow path:** `investigate` → `create_issue` → `create_issue ×2`");
168
+ });
169
+ it("accepts the real triageWorkflow without throwing", () => {
170
+ // Regression guard: the real workflow must serialise cleanly through toMermaidBlock
171
+ // so we do not ship a broken diagram to every downstream consumer.
172
+ const results = new Map([["investigate", success({ novel_count: 0 })]]);
173
+ const md = formatDagResultMarkdown(results, 1_000, minimalConfig(), {
174
+ workflow: triageWorkflow,
175
+ });
176
+ expect(md).toContain("```mermaid");
177
+ expect(md).toContain(`title: ${triageWorkflow.name}`);
178
+ });
107
179
  });
package/dist/mermaid.js CHANGED
@@ -43,7 +43,7 @@ export function toMermaid(workflow, options = {}) {
43
43
  for (const [id, node] of Object.entries(workflow.nodes)) {
44
44
  let label = escapeLabel(node.name);
45
45
  const isEntry = id === workflow.entry;
46
- const entryTag = isEntry ? " \\u25B6" : "";
46
+ const entryTag = isEntry ? " \u25B6" : "";
47
47
  // Show iteration count for nodes that ran multiple times
48
48
  const iterations = nodeIterations.get(id);
49
49
  const iterTag = iterations && iterations > 1 ? ` ×${iterations}` : "";
@@ -55,7 +55,7 @@ describe("toMermaid", () => {
55
55
  const result = toMermaid(simple);
56
56
  expect(result).toContain("graph TB");
57
57
  // Entry node gets stadium shape
58
- expect(result).toContain("a([Triage \\u25B6])");
58
+ expect(result).toContain("a([Triage \u25B6])");
59
59
  // Non-entry nodes get default shape
60
60
  expect(result).toContain("b[Investigate]");
61
61
  expect(result).toContain("c[Report]");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.45",
3
+ "version": "0.1.46",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"