waygraph 0.12.7 → 0.12.9

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.
@@ -428,6 +428,7 @@ async function headfulPick(page, menu, library, mem, waitPick) {
428
428
  window.__wgPositionRing(
429
429
  { x: r.x, y: r.y, width: r.width, height: r.height },
430
430
  btn.textContent?.trim()?.slice(0, 80) || "target",
431
+ "auto",
431
432
  );
432
433
  });
433
434
  btn.addEventListener("mouseleave", () => {
package/dist/cli.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * demo Watch (step overlay); `--blocks` `--data` `--auto-next` `--fast` `--full` `--ff-expand`
8
8
  * run Execute; `--blocks` `--data` `--non-headless` `--video`
9
9
  *
10
- * Also: list / nav / validate / check / graph / init / agent-dive / try
10
+ * Also: list / nav / validate / check / graph / init / agent-dive / traverse / try
11
11
  * Aliases (one release): `chain` -> run/demo --blocks; `--autoplay` -> `--auto-next`
12
12
  *
13
13
  * "project" defaults to cwd. Flags beat WAYGRAPH_* env.
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * demo Watch (step overlay); `--blocks` `--data` `--auto-next` `--fast` `--full` `--ff-expand`
8
8
  * run Execute; `--blocks` `--data` `--non-headless` `--video`
9
9
  *
10
- * Also: list / nav / validate / check / graph / init / agent-dive / try
10
+ * Also: list / nav / validate / check / graph / init / agent-dive / traverse / try
11
11
  * Aliases (one release): `chain` -> run/demo --blocks; `--autoplay` -> `--auto-next`
12
12
  *
13
13
  * "project" defaults to cwd. Flags beat WAYGRAPH_* env.
@@ -20,6 +20,7 @@ import { spawn } from "node:child_process";
20
20
  import { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
21
21
  import { runAutoExplore } from "./auto-explore-run.js";
22
22
  import { runAgentDive } from "./agent-dive.js";
23
+ import { runTraverse } from "./traverse-run.js";
23
24
  // ---------------------------------------------------------------------------
24
25
  // Filesystem
25
26
  // ---------------------------------------------------------------------------
@@ -395,7 +396,11 @@ function seedMemForFlow(mem, flowBlocks, json, label) {
395
396
  const RING_CSS =
396
397
  "#wg-ring{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
397
398
  "border:2.5px solid #7C3AED;border-radius:10px;" +
398
- "box-shadow:0 0 0 4px rgba(124,58,237,.16);transition:opacity .3s ease;}" +
399
+ "box-shadow:0 0 0 4px rgba(124,58,237,.16);transition:opacity .3s ease,border-color .15s,box-shadow .15s;}" +
400
+ // Planned (stubs / fixtures / YAP / instruction.highlights) = purple.
401
+ // Automation (verify fallback, unmatched fill/click) = yellow - so operators
402
+ // can ignore engine checks and only watch purple narration.
403
+ "#wg-ring[data-tone=auto]{border-color:#EAB308;box-shadow:0 0 0 4px rgba(234,179,8,.22);}" +
399
404
  // A real element, not a ::after pseudo-element - a pseudo-element's
400
405
  // position is CSS-relative to the ring's own box (left:0 always meant
401
406
  // "the ring's own left edge"), so it had no way to be clamped back onto
@@ -406,7 +411,8 @@ const RING_CSS =
406
411
  // window.__wgPositionRing in installOverlay.
407
412
  "#wg-ring-label{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
408
413
  "max-width:min(360px,70vw);white-space:normal;padding:6px 10px;border-radius:7px;background:#7C3AED;color:#fff;" +
409
- "font:600 12px/1.35 system-ui,sans-serif;transition:opacity .3s ease;}" +
414
+ "font:600 12px/1.35 system-ui,sans-serif;transition:opacity .3s ease,background .15s,color .15s;}" +
415
+ "#wg-ring-label[data-tone=auto]{background:#EAB308;color:#1c1917;}" +
410
416
  // Mouse cursor icon that travels to a target before it's acted on, plus a
411
417
  // quick expanding ripple at the moment of a click - same idea as
412
418
  // help-center-clip-engine's #clip-cursor/#clip-ring (video-pipeline). The
@@ -424,6 +430,7 @@ const RING_CSS =
424
430
  "#wg-click-pulse{position:fixed;z-index:2147483647;width:14px;height:14px;" +
425
431
  "margin-left:-7px;margin-top:-7px;border-radius:50%;pointer-events:none;opacity:0;" +
426
432
  "border:2px solid #7C3AED;background:rgba(124,58,237,.25);}" +
433
+ "#wg-click-pulse[data-tone=auto]{border-color:#EAB308;background:rgba(234,179,8,.28);}" +
427
434
  "#wg-click-pulse.wg-pulse{animation:wg-pulse .5s ease-out;}" +
428
435
  "@keyframes wg-pulse{0%{opacity:.9;transform:scale(.4);}100%{opacity:0;transform:scale(2.4);}}" +
429
436
  "#wg-panel{position:fixed;z-index:2147483647;left:50%;bottom:12px;transform:translateX(-50%);" +
@@ -639,26 +646,13 @@ async function installOverlay(page, title) {
639
646
  const cursor = document.getElementById("wg-cursor");
640
647
  if (cursor) cursor.style.opacity = "0";
641
648
  };
642
- window.__wgClickPulse = (x, y) => {
643
- const pulse = document.getElementById("wg-click-pulse");
644
- if (!pulse) return;
645
- pulse.style.left = x + "px";
646
- pulse.style.top = y + "px";
647
- pulse.classList.remove("wg-pulse");
648
- void pulse.offsetWidth;
649
- pulse.classList.add("wg-pulse");
650
- };
651
- // Clamps the ring AND its label to stay fully on-screen, same
652
- // width/height always - only the position moves. A target near a
653
- // viewport edge (real case: "login success" highlight landing top
654
- // right) used to just run the label text off-screen with no guard
655
- // at all - same mistake as the banner had before its own
656
- // left/center/right positions existed, not repeating it here by
657
- // shrinking anything, only repositioning.
658
- window.__wgPositionRing = (box, label) => {
649
+ window.__wgPositionRing = (box, label, tone) => {
659
650
  const ring = document.getElementById("wg-ring");
660
651
  const ringLabel = document.getElementById("wg-ring-label");
661
652
  if (!ring || !ringLabel) return;
653
+ const t = tone === "auto" ? "auto" : "planned";
654
+ ring.dataset.tone = t;
655
+ ringLabel.dataset.tone = t;
662
656
  const margin = 6;
663
657
  const vw = window.innerWidth;
664
658
  const vh = window.innerHeight;
@@ -690,6 +684,16 @@ async function installOverlay(page, title) {
690
684
  ringLabel.style.left = labelLeft + "px";
691
685
  ringLabel.style.top = labelTop + "px";
692
686
  };
687
+ window.__wgClickPulse = (x, y, tone) => {
688
+ const pulse = document.getElementById("wg-click-pulse");
689
+ if (!pulse) return;
690
+ pulse.dataset.tone = tone === "auto" ? "auto" : "planned";
691
+ pulse.style.left = x + "px";
692
+ pulse.style.top = y + "px";
693
+ pulse.classList.remove("wg-pulse");
694
+ void pulse.offsetWidth;
695
+ pulse.classList.add("wg-pulse");
696
+ };
693
697
  window.__wgHideRing = () => {
694
698
  // While a narrate() call owns the ring (mid multi-step action),
695
699
  // the auto-highlight click/fill patches' own end-of-step hide is
@@ -1030,7 +1034,7 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1030
1034
  try {
1031
1035
  const box = await page.locator(h.selector).first().boundingBox();
1032
1036
  if (box) {
1033
- await showRing(page, box, h.label);
1037
+ await showRing(page, box, h.label, h.tone || "planned");
1034
1038
  const authored = resolveFixtureDwellMs(h, { gatesFast });
1035
1039
  const legacyMs = i === list.length - 1 ? 200 : 900;
1036
1040
  const holdMs =
@@ -1052,7 +1056,7 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1052
1056
  if (window.__wgHideRing) window.__wgHideRing();
1053
1057
  return;
1054
1058
  }
1055
- window.__wgPositionRing(el.getBoundingClientRect(), h.label);
1059
+ window.__wgPositionRing(el.getBoundingClientRect(), h.label, h.tone || "planned");
1056
1060
  };
1057
1061
  reposition();
1058
1062
  window.__wgRingTrack = reposition;
@@ -1296,7 +1300,10 @@ function resolveDeclaredHighlights(block, resultTag) {
1296
1300
  }
1297
1301
  }
1298
1302
  if (!Array.isArray(highlights)) return [];
1299
- return highlights.filter((h) => h && typeof h.selector === "string" && typeof h.label === "string");
1303
+ return highlights.filter((h) => h && typeof h.selector === "string" && typeof h.label === "string").map((h) => ({
1304
+ ...h,
1305
+ tone: h.tone === "auto" ? "auto" : "planned",
1306
+ }));
1300
1307
  }
1301
1308
 
1302
1309
  /**
@@ -1326,12 +1333,12 @@ function extractVerifyHighlights(block, resultTag) {
1326
1333
  if (typeof name !== "string") continue;
1327
1334
  let m = /^visible\\((.+)\\)$/.exec(name);
1328
1335
  if (m) {
1329
- highlights.push({ selector: m[1], label: name });
1336
+ highlights.push({ selector: m[1], label: name, tone: "auto" });
1330
1337
  continue;
1331
1338
  }
1332
1339
  m = /^text-equals\\((.+?),\\s*"/.exec(name);
1333
1340
  if (m) {
1334
- highlights.push({ selector: m[1], label: name });
1341
+ highlights.push({ selector: m[1], label: name, tone: "auto" });
1335
1342
  continue;
1336
1343
  }
1337
1344
  }
@@ -1394,13 +1401,13 @@ async function markStepRunning(page, _opts) {
1394
1401
  .catch(() => {});
1395
1402
  }
1396
1403
 
1397
- async function showRing(page, box, label) {
1404
+ async function showRing(page, box, label, tone) {
1398
1405
  await page
1399
1406
  .evaluate(
1400
- ({ box, label }) => {
1401
- if (window.__wgPositionRing) window.__wgPositionRing(box, label);
1407
+ ({ box, label, tone }) => {
1408
+ if (window.__wgPositionRing) window.__wgPositionRing(box, label, tone || "planned");
1402
1409
  },
1403
- { box, label },
1410
+ { box, label, tone: tone || "planned" },
1404
1411
  )
1405
1412
  .catch(() => {});
1406
1413
  }
@@ -1430,8 +1437,8 @@ async function matchStubForLocator(page, locator, stubs) {
1430
1437
 
1431
1438
  async function captionForLocator(page, locator, stubs, fallback) {
1432
1439
  const matched = await matchStubForLocator(page, locator, stubs);
1433
- if (matched) return formatHighlightCaption(matched);
1434
- return fallback;
1440
+ if (matched) return { label: formatHighlightCaption(matched), tone: "planned" };
1441
+ return { label: fallback, tone: "auto" };
1435
1442
  }
1436
1443
 
1437
1444
  async function dwellMatchedStub(page, locator, stubs, pacing) {
@@ -1473,7 +1480,7 @@ async function presentSlides(page, slides, _gate, opts) {
1473
1480
  if (s.selector) {
1474
1481
  try {
1475
1482
  const box = await page.locator(s.selector).first().boundingBox();
1476
- if (box) await showRing(page, box, caption);
1483
+ if (box) await showRing(page, box, caption, "planned");
1477
1484
  } catch {
1478
1485
  await hideRing(page);
1479
1486
  }
@@ -1704,11 +1711,11 @@ async function moveCursorTo(page, box, ms) {
1704
1711
  return { x, y };
1705
1712
  }
1706
1713
 
1707
- async function clickPulseAt(page, x, y) {
1714
+ async function clickPulseAt(page, x, y, tone) {
1708
1715
  await page
1709
- .evaluate(({ x, y }) => {
1710
- if (window.__wgClickPulse) window.__wgClickPulse(x, y);
1711
- }, { x, y })
1716
+ .evaluate(({ x, y, tone }) => {
1717
+ if (window.__wgClickPulse) window.__wgClickPulse(x, y, tone || "planned");
1718
+ }, { x, y, tone: tone || "planned" })
1712
1719
  .catch(() => {});
1713
1720
  }
1714
1721
 
@@ -1782,9 +1789,9 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
1782
1789
  ? "from mem: " + memTrack.lastKeyName
1783
1790
  : "writing from mem";
1784
1791
  const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
1785
- const label = await captionForLocator(page, this, stubs, fallback);
1792
+ const cap = await captionForLocator(page, this, stubs, fallback);
1786
1793
  await moveCursorTo(page, box, cursorMs(500));
1787
- await showRing(page, box, label);
1794
+ await showRing(page, box, cap.label, cap.tone);
1788
1795
  await dwellMatchedStub(page, this, stubs, pacing);
1789
1796
  await new Promise((res) => setTimeout(res, skipTheater() ? 0 : 200));
1790
1797
  } else if (box) {
@@ -1823,6 +1830,7 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
1823
1830
  const originalClick = proto.click;
1824
1831
  proto.click = async function (options) {
1825
1832
  let clickPoint = null;
1833
+ let clickTone = "auto";
1826
1834
  try {
1827
1835
  await installOverlay(page);
1828
1836
  // Wait until the target is visible BEFORE measuring - otherwise
@@ -1852,9 +1860,10 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
1852
1860
  /* ignore */
1853
1861
  }
1854
1862
  const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
1855
- const label = await captionForLocator(page, this, stubs, fallback);
1863
+ const cap = await captionForLocator(page, this, stubs, fallback);
1864
+ clickTone = cap.tone;
1856
1865
  clickPoint = await moveCursorTo(page, box, cursorMs(600));
1857
- await showRing(page, box, label);
1866
+ await showRing(page, box, cap.label, cap.tone);
1858
1867
  await dwellMatchedStub(page, this, stubs, pacing);
1859
1868
  // "pop for a few seconds" - Dan's own phrase, matching the
1860
1869
  // zsign demo-engine's ring-before-click pattern in
@@ -1864,12 +1873,13 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
1864
1873
  // Ring/caption already handled by narrate() - still move the
1865
1874
  // cursor + pulse the click point, just skip re-showing the ring.
1866
1875
  clickPoint = await moveCursorTo(page, box, cursorMs(200));
1876
+ clickTone = "planned";
1867
1877
  }
1868
1878
  } catch {
1869
1879
  // best-effort - the real click below still runs either way
1870
1880
  }
1871
1881
  if (clickPoint) {
1872
- await clickPulseAt(page, clickPoint.x, clickPoint.y);
1882
+ await clickPulseAt(page, clickPoint.x, clickPoint.y, clickTone);
1873
1883
  // clickPulseAt only triggers the CSS animation class - it doesn't
1874
1884
  // wait for it. Without a pause here, the real click (and any
1875
1885
  // resulting navigation/DOM change) fires while the ripple is still
@@ -2256,6 +2266,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2256
2266
  label: formatHighlightCaption(h),
2257
2267
  duration: h.duration,
2258
2268
  fastMode: h.fastMode,
2269
+ tone: "planned",
2259
2270
  }));
2260
2271
  await cycleHighlightRings(page, errHighlights, !!pacing.gatesFast, {
2261
2272
  defaultHoldMs: 2000,
@@ -2328,6 +2339,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2328
2339
  label: formatHighlightCaption(h),
2329
2340
  duration: h.duration,
2330
2341
  fastMode: h.fastMode,
2342
+ tone: "planned",
2331
2343
  }),
2332
2344
  );
2333
2345
  } else {
@@ -3661,6 +3673,12 @@ Primary (less is more):
3661
3673
 
3662
3674
  Also:
3663
3675
  waygraph list | nav | validate | check | graph | init <name>
3676
+ waygraph traverse [project] Serial graph crawl (Phase B)
3677
+ --from <Checkpoint> Seed / start checkpoint
3678
+ --data '{...}' Mem seed
3679
+ --max-steps N Cap Block runs (default 50)
3680
+ --max-visits N Cap visits per node (default 2)
3681
+ --non-headless Show browser
3664
3682
  waygraph agent-dive [--loop claude|opencode|cursor|vscode] [--prompts]
3665
3683
  Initialize coding-agent defs (Playwright init-agents analogue)
3666
3684
  waygraph try [demo|auto|auto:cli] One-shot saucedemo in a temp dir
@@ -3759,6 +3777,97 @@ async function main() {
3759
3777
  initCommand(args[1] ?? "");
3760
3778
  break;
3761
3779
  }
3780
+ case "traverse": {
3781
+ if (args.includes("--step") || args.includes("--auto-next") || args.includes("--autoplay")) {
3782
+ console.error("waygraph traverse: --step / --auto-next not supported (not a demo)");
3783
+ process.exit(1);
3784
+ }
3785
+ const rest = args.slice(1);
3786
+ let from;
3787
+ let data;
3788
+ let maxSteps;
3789
+ let maxVisits;
3790
+ let maxEdge;
3791
+ let headed = false;
3792
+ let baseUrl;
3793
+ let projectDir = process.cwd();
3794
+ for (let i = 0; i < rest.length; i++) {
3795
+ const a = rest[i];
3796
+ if (a === "--from" || a.startsWith("--from=")) {
3797
+ from = a.startsWith("--from=") ? a.slice(7) : rest[++i];
3798
+ continue;
3799
+ }
3800
+ if (a === "--data" || a.startsWith("--data=")) {
3801
+ data = a.startsWith("--data=") ? a.slice(7) : rest[++i];
3802
+ continue;
3803
+ }
3804
+ if (a === "--max-steps" || a.startsWith("--max-steps=")) {
3805
+ const v = a.startsWith("--max-steps=") ? a.slice(12) : rest[++i];
3806
+ maxSteps = Number(v);
3807
+ continue;
3808
+ }
3809
+ if (a === "--max-visits" || a.startsWith("--max-visits=")) {
3810
+ const v = a.startsWith("--max-visits=") ? a.slice(13) : rest[++i];
3811
+ maxVisits = Number(v);
3812
+ continue;
3813
+ }
3814
+ if (a === "--max-visits-per-edge" || a.startsWith("--max-visits-per-edge=")) {
3815
+ const v = a.startsWith("--max-visits-per-edge=")
3816
+ ? a.slice("--max-visits-per-edge=".length)
3817
+ : rest[++i];
3818
+ maxEdge = Number(v);
3819
+ continue;
3820
+ }
3821
+ if (a === "--non-headless" || a === "--headed") {
3822
+ headed = true;
3823
+ continue;
3824
+ }
3825
+ if (a === "--base-url" || a.startsWith("--base-url=")) {
3826
+ baseUrl = a.startsWith("--base-url=") ? a.slice(11) : rest[++i];
3827
+ continue;
3828
+ }
3829
+ if (a === "--help" || a === "-h") {
3830
+ console.log(`waygraph traverse [project] [flags]
3831
+
3832
+ Serial graph crawl (RFC Phase B). Walks unused legal edges until a leaf,
3833
+ budget kill, or first broken edge.
3834
+
3835
+ --from <Checkpoint> start checkpoint (optional)
3836
+ --data '{...}' Mem seed JSON
3837
+ --max-steps N default 50
3838
+ --max-visits N per-node visit cap (default 2)
3839
+ --max-visits-per-edge N default 1
3840
+ --non-headless show browser
3841
+ --base-url URL
3842
+
3843
+ PASS: [Reached Leaf Node[traverse-1] at=... steps=N]
3844
+ FAIL: [Broke at edge[traverse-1] block=... from=... to=...]
3845
+ `);
3846
+ process.exit(0);
3847
+ }
3848
+ if (!a.startsWith("-")) {
3849
+ projectDir = resolve(a);
3850
+ }
3851
+ }
3852
+ if (!existsSync(projectDir) || !statSync(projectDir).isDirectory()) {
3853
+ console.error(`waygraph traverse: no such directory: ${projectDir}`);
3854
+ process.exit(1);
3855
+ }
3856
+ const code = await runTraverse(projectDir, {
3857
+ ...(from ? { from } : {}),
3858
+ ...(data ? { data } : {}),
3859
+ ...(maxSteps !== undefined && Number.isFinite(maxSteps) ? { maxSteps } : {}),
3860
+ ...(maxVisits !== undefined && Number.isFinite(maxVisits)
3861
+ ? { maxVisitsPerNode: maxVisits }
3862
+ : {}),
3863
+ ...(maxEdge !== undefined && Number.isFinite(maxEdge)
3864
+ ? { maxVisitsPerEdge: maxEdge }
3865
+ : {}),
3866
+ headed,
3867
+ ...(baseUrl ? { baseURL: baseUrl } : {}),
3868
+ });
3869
+ process.exit(code);
3870
+ }
3762
3871
  case "agent-dive":
3763
3872
  case "init-agents": {
3764
3873
  // Playwright analogue: npx playwright init-agents --loop <provider>
package/dist/engine.d.ts CHANGED
@@ -467,6 +467,48 @@ export declare class Engine {
467
467
  Block<H, I>,
468
468
  EndMarker
469
469
  ]): Flow<I>;
470
+ defineFlow<B extends Checkpoint<string>, C extends Checkpoint<string>, D extends Checkpoint<string>, E extends Checkpoint<string>, F extends Checkpoint<string>, G extends Checkpoint<string>, H extends Checkpoint<string>, I extends Checkpoint<string>, J extends Checkpoint<string>>(blocks: readonly [
471
+ StartMarker,
472
+ Block<S, B>,
473
+ Block<B, C>,
474
+ Block<C, D>,
475
+ Block<D, E>,
476
+ Block<E, F>,
477
+ Block<F, G>,
478
+ Block<G, H>,
479
+ Block<H, I>,
480
+ Block<I, J>,
481
+ EndMarker
482
+ ]): Flow<J>;
483
+ defineFlow<B extends Checkpoint<string>, C extends Checkpoint<string>, D extends Checkpoint<string>, E extends Checkpoint<string>, F extends Checkpoint<string>, G extends Checkpoint<string>, H extends Checkpoint<string>, I extends Checkpoint<string>, J extends Checkpoint<string>, K extends Checkpoint<string>>(blocks: readonly [
484
+ StartMarker,
485
+ Block<S, B>,
486
+ Block<B, C>,
487
+ Block<C, D>,
488
+ Block<D, E>,
489
+ Block<E, F>,
490
+ Block<F, G>,
491
+ Block<G, H>,
492
+ Block<H, I>,
493
+ Block<I, J>,
494
+ Block<J, K>,
495
+ EndMarker
496
+ ]): Flow<K>;
497
+ defineFlow<B extends Checkpoint<string>, C extends Checkpoint<string>, D extends Checkpoint<string>, E extends Checkpoint<string>, F extends Checkpoint<string>, G extends Checkpoint<string>, H extends Checkpoint<string>, I extends Checkpoint<string>, J extends Checkpoint<string>, K extends Checkpoint<string>, L extends Checkpoint<string>>(blocks: readonly [
498
+ StartMarker,
499
+ Block<S, B>,
500
+ Block<B, C>,
501
+ Block<C, D>,
502
+ Block<D, E>,
503
+ Block<E, F>,
504
+ Block<F, G>,
505
+ Block<G, H>,
506
+ Block<H, I>,
507
+ Block<I, J>,
508
+ Block<J, K>,
509
+ Block<K, L>,
510
+ EndMarker
511
+ ]): Flow<L>;
470
512
  }
471
513
  /**
472
514
  * Attaches routing to `block` without touching its act/observe/resolve/verify -
package/dist/engine.js CHANGED
@@ -139,7 +139,7 @@ export async function narrate(locator, caption, action) {
139
139
  .evaluate(({ box, caption }) => {
140
140
  const w = globalThis;
141
141
  if (w.__wgPositionRing)
142
- w.__wgPositionRing(box, caption);
142
+ w.__wgPositionRing(box, caption, "planned");
143
143
  // The CLI's own automatic per-fill/per-click narration checks
144
144
  // this before showing (and overwriting) its own guess - an
145
145
  // explicitly authored caption should win, not get immediately
@@ -3,18 +3,18 @@ import type { MemPage } from "./mem-page.js";
3
3
  export declare function installDemoChrome(page: Page, title?: string, options?: {
4
4
  banner?: boolean;
5
5
  }): Promise<void>;
6
- /** Demo cursor + ring on fill/click during auto explore headful runs. */
6
+ /** Demo cursor + ring on fill/click during auto explore headful runs (automation = yellow). */
7
7
  export declare function instrumentInteractionHighlighting(page: Page, mem: MemPage, slowMo: number): void;
8
8
  declare global {
9
9
  interface Window {
10
10
  __wgMoveCursorTo?: (x: number, y: number, ms?: number) => void;
11
- __wgClickPulse?: (x: number, y: number) => void;
11
+ __wgClickPulse?: (x: number, y: number, tone?: string) => void;
12
12
  __wgPositionRing?: (box: {
13
13
  x: number;
14
14
  y: number;
15
15
  width: number;
16
16
  height: number;
17
- }, label: string) => void;
17
+ }, label: string, tone?: string) => void;
18
18
  __wgHideRing?: () => void;
19
19
  __wgPendingNavClickLabel?: string;
20
20
  __wgWirePanelChrome?: (panel: HTMLElement, storageKey: string, chromeTitle?: string) => void;
@@ -1,8 +1,11 @@
1
1
  const RING_CSS = "#wg-ring{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
2
- "border:2.5px solid #7C3AED;border-radius:10px;box-shadow:0 0 0 4px rgba(124,58,237,.16);transition:opacity .3s ease;}" +
2
+ "border:2.5px solid #7C3AED;border-radius:10px;box-shadow:0 0 0 4px rgba(124,58,237,.16);" +
3
+ "transition:opacity .3s ease,border-color .15s,box-shadow .15s;}" +
4
+ "#wg-ring[data-tone=auto]{border-color:#EAB308;box-shadow:0 0 0 4px rgba(234,179,8,.22);}" +
3
5
  "#wg-ring-label{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
4
6
  "white-space:nowrap;padding:4px 9px;border-radius:7px;background:#7C3AED;color:#fff;" +
5
- "font:600 12px/1.2 system-ui,sans-serif;transition:opacity .3s ease;}" +
7
+ "font:600 12px/1.2 system-ui,sans-serif;transition:opacity .3s ease,background .15s,color .15s;}" +
8
+ "#wg-ring-label[data-tone=auto]{background:#EAB308;color:#1c1917;}" +
6
9
  "#wg-cursor{position:fixed;z-index:2147483647;width:24px;height:24px;pointer-events:none;" +
7
10
  "left:0;top:0;opacity:0;margin:0;" +
8
11
  "transition:transform var(--wg-cursor-ms,600ms) cubic-bezier(.22,1,.36,1),opacity .2s ease;" +
@@ -10,6 +13,7 @@ const RING_CSS = "#wg-ring{position:fixed;z-index:2147483646;pointer-events:none
10
13
  "#wg-click-pulse{position:fixed;z-index:2147483647;width:14px;height:14px;" +
11
14
  "margin-left:-7px;margin-top:-7px;border-radius:50%;pointer-events:none;opacity:0;" +
12
15
  "border:2px solid #7C3AED;background:rgba(124,58,237,.25);}" +
16
+ "#wg-click-pulse[data-tone=auto]{border-color:#EAB308;background:rgba(234,179,8,.28);}" +
13
17
  "#wg-click-pulse.wg-pulse{animation:wg-pulse .5s ease-out;}" +
14
18
  "@keyframes wg-pulse{0%{opacity:.9;transform:scale(.4);}100%{opacity:0;transform:scale(2.4);}}" +
15
19
  "#wg-banner{position:fixed;z-index:2147483647;top:14px;left:14px;" +
@@ -24,7 +28,7 @@ export async function installDemoChrome(page, title = "waygraph auto", options =
24
28
  const showBanner = options.banner !== false;
25
29
  await page.addStyleTag({ content: RING_CSS }).catch(() => { });
26
30
  await page
27
- .evaluate(({ title, favicon }) => {
31
+ .evaluate(({ title, favicon, showBanner }) => {
28
32
  if (!document.getElementById("wg-ring")) {
29
33
  const ring = document.createElement("div");
30
34
  ring.id = "wg-ring";
@@ -57,21 +61,25 @@ export async function installDemoChrome(page, title = "waygraph auto", options =
57
61
  cursor.style.transform = "translate(" + x + "px," + y + "px)";
58
62
  cursor.style.opacity = "1";
59
63
  };
60
- window.__wgClickPulse = (x, y) => {
64
+ window.__wgClickPulse = (x, y, tone) => {
61
65
  const pulse = document.getElementById("wg-click-pulse");
62
66
  if (!pulse)
63
67
  return;
68
+ pulse.dataset.tone = tone === "auto" ? "auto" : "planned";
64
69
  pulse.style.left = x + "px";
65
70
  pulse.style.top = y + "px";
66
71
  pulse.classList.remove("wg-pulse");
67
72
  void pulse.offsetWidth;
68
73
  pulse.classList.add("wg-pulse");
69
74
  };
70
- window.__wgPositionRing = (box, label) => {
75
+ window.__wgPositionRing = (box, label, tone) => {
71
76
  const ring = document.getElementById("wg-ring");
72
77
  const ringLabel = document.getElementById("wg-ring-label");
73
78
  if (!ring || !ringLabel)
74
79
  return;
80
+ const t = tone === "auto" ? "auto" : "planned";
81
+ ring.dataset.tone = t;
82
+ ringLabel.dataset.tone = t;
75
83
  ring.style.left = box.x - 6 + "px";
76
84
  ring.style.top = box.y - 6 + "px";
77
85
  ring.style.width = box.width + 12 + "px";
@@ -111,16 +119,24 @@ async function moveCursorTo(page, box, ms) {
111
119
  await new Promise((r) => setTimeout(r, ms));
112
120
  return { x, y };
113
121
  }
114
- async function showRing(page, box, label) {
115
- await page.evaluate(({ box, label }) => window.__wgPositionRing?.(box, label), { box, label }).catch(() => { });
122
+ async function showRing(page, box, label, tone = "auto") {
123
+ await page
124
+ .evaluate(({ box, label, tone }) => window.__wgPositionRing?.(box, label, tone), {
125
+ box,
126
+ label,
127
+ tone,
128
+ })
129
+ .catch(() => { });
116
130
  }
117
131
  async function hideRing(page) {
118
132
  await page.evaluate(() => window.__wgHideRing?.()).catch(() => { });
119
133
  }
120
- async function clickPulseAt(page, x, y) {
121
- await page.evaluate(({ x, y }) => window.__wgClickPulse?.(x, y), { x, y }).catch(() => { });
134
+ async function clickPulseAt(page, x, y, tone = "auto") {
135
+ await page
136
+ .evaluate(({ x, y, tone }) => window.__wgClickPulse?.(x, y, tone), { x, y, tone })
137
+ .catch(() => { });
122
138
  }
123
- /** Demo cursor + ring on fill/click during auto explore headful runs. */
139
+ /** Demo cursor + ring on fill/click during auto explore headful runs (automation = yellow). */
124
140
  export function instrumentInteractionHighlighting(page, mem, slowMo) {
125
141
  const typeDelay = () => (slowMo ? 0 : 30);
126
142
  const clickPrePop = () => (slowMo ? 300 : 700);
@@ -133,89 +149,81 @@ export function instrumentInteractionHighlighting(page, mem, slowMo) {
133
149
  memTrack.at = Date.now();
134
150
  return originalGet(key);
135
151
  };
136
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
137
152
  const proto = Object.getPrototypeOf(page.locator("html"));
138
153
  if (!proto.__wgFillPatched) {
139
154
  proto.__wgFillPatched = true;
140
- // Keep unbound — originalFill.call(this, …) must receive the Locator as `this`, not the prototype.
141
155
  const originalFill = proto.fill;
142
156
  proto.fill = async function (value, options) {
143
- const livePage = (typeof this.page === "function" ? this.page() : page);
144
157
  try {
145
- await installDemoChrome(livePage);
158
+ await installDemoChrome(page, "", { banner: false });
146
159
  const box = await this.boundingBox();
147
160
  if (box) {
148
161
  const label = memTrack.lastKeyName && Date.now() - memTrack.at < 3000
149
162
  ? "from mem: " + memTrack.lastKeyName
150
- : "writing";
151
- await moveCursorTo(livePage, box, cursorMs(500));
152
- await showRing(livePage, box, label);
153
- await new Promise((r) => setTimeout(r, 200));
163
+ : "writing from mem";
164
+ await moveCursorTo(page, box, cursorMs(500));
165
+ await showRing(page, box, label, "auto");
166
+ await new Promise((res) => setTimeout(res, 200));
154
167
  }
155
168
  }
156
169
  catch {
157
170
  /* best-effort */
158
171
  }
159
- // Prefer a single fill - clear+pressSequentially can hang when the auto
160
- // panel steals focus mid-type (looks stuck on "Running: submit-login").
161
- if (slowMo || typeDelay() === 0) {
162
- await originalFill.call(this, value, options);
172
+ let result;
173
+ try {
174
+ await originalFill.call(this, "", {
175
+ timeout: options?.timeout,
176
+ });
177
+ const seqOpts = { delay: typeDelay() };
178
+ const to = options?.timeout;
179
+ if (to !== undefined)
180
+ seqOpts.timeout = to;
181
+ result = await this.pressSequentially(String(value), seqOpts);
163
182
  }
164
- else {
165
- try {
166
- await originalFill.call(this, "", options);
167
- const seqOpts = { delay: typeDelay() };
168
- if (options?.timeout !== undefined)
169
- seqOpts.timeout = options.timeout;
170
- await this.pressSequentially(String(value), seqOpts);
171
- }
172
- catch {
173
- await originalFill.call(this, value, options);
174
- }
183
+ catch {
184
+ result = await originalFill.call(this, value, options);
175
185
  }
176
- await hideRing(livePage);
186
+ await hideRing(page);
187
+ return result;
177
188
  };
178
189
  }
179
190
  if (!proto.__wgClickPatched) {
180
191
  proto.__wgClickPatched = true;
181
192
  const originalClick = proto.click;
182
193
  proto.click = async function (options) {
183
- const livePage = (typeof this.page === "function" ? this.page() : page);
184
194
  let clickPoint = null;
185
195
  try {
186
- await installDemoChrome(livePage);
187
- await this.waitFor({ state: "visible", timeout: options?.timeout ?? 15000 }).catch(() => { });
196
+ await installDemoChrome(page, "", { banner: false });
197
+ await this.waitFor({
198
+ state: "visible",
199
+ timeout: options?.timeout || 30000,
200
+ }).catch(() => { });
188
201
  const box = await this.boundingBox();
189
202
  if (box) {
190
203
  let label = "click";
191
204
  try {
192
- const navLabel = await livePage.evaluate(() => window.__wgPendingNavClickLabel ?? null);
193
- if (navLabel)
194
- label = String(navLabel);
195
- else {
196
- const text = (await this.textContent())?.trim();
197
- if (text && text.length <= 30)
198
- label = text;
199
- }
205
+ const text = (await this.textContent())?.trim();
206
+ if (text && text.length > 0 && text.length <= 30)
207
+ label = text;
200
208
  }
201
209
  catch {
202
210
  /* ignore */
203
211
  }
204
- clickPoint = await moveCursorTo(livePage, box, cursorMs(600));
205
- await showRing(livePage, box, label);
206
- await new Promise((r) => setTimeout(r, clickPrePop()));
212
+ clickPoint = await moveCursorTo(page, box, cursorMs(600));
213
+ await showRing(page, box, label, "auto");
214
+ await new Promise((res) => setTimeout(res, clickPrePop()));
207
215
  }
208
216
  }
209
217
  catch {
210
218
  /* best-effort */
211
219
  }
212
220
  if (clickPoint) {
213
- await clickPulseAt(livePage, clickPoint.x, clickPoint.y);
214
- await new Promise((r) => setTimeout(r, 200));
221
+ await clickPulseAt(page, clickPoint.x, clickPoint.y, "auto");
222
+ await new Promise((res) => setTimeout(res, 80));
215
223
  }
216
224
  const result = await originalClick.call(this, options);
217
- await new Promise((r) => setTimeout(r, clickPostPop()));
218
- await hideRing(livePage).catch(() => { });
225
+ await new Promise((res) => setTimeout(res, clickPostPop()));
226
+ await hideRing(page);
219
227
  return result;
220
228
  };
221
229
  }
@@ -0,0 +1,24 @@
1
+ export interface TraverseOptions {
2
+ baseURL?: string;
3
+ startUrl?: string;
4
+ /** Seed checkpoint label (optional - otherwise detect from page / nav). */
5
+ from?: string;
6
+ /** Global Mem seed JSON (or use WAYGRAPH_DATA / WAYGRAPH_AUTO_MEM). */
7
+ data?: string;
8
+ /** Max Block runs this traverse (default 50). */
9
+ maxSteps?: number;
10
+ /** Max times a checkpoint may be visited (default 2). */
11
+ maxVisitsPerNode?: number;
12
+ /** Max times an edge key (block::instanceId) may be taken (default 1). */
13
+ maxVisitsPerEdge?: number;
14
+ /** Wall-clock budget ms (default 15m). */
15
+ timeoutMs?: number;
16
+ /** Show browser (default headless). */
17
+ headed?: boolean;
18
+ /** Traverse id for greppable lines (default traverse-1). */
19
+ traverseId?: string;
20
+ }
21
+ /**
22
+ * Serial graph crawl. Prints greppable PASS/FAIL lines; non-zero exit on break.
23
+ */
24
+ export declare function runTraverse(projectDir: string, options?: TraverseOptions): Promise<number>;
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Phase B: serial graph traverse (RFC traverse-ffcompose).
3
+ * Walk unused legal edges until leaf / budget / break. No parallel yet.
4
+ */
5
+ import { readFileSync, existsSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { Engine, start, end, locate } from "./engine.js";
8
+ import { MemPage } from "./mem-page.js";
9
+ import { buildExploreContext, buildExploreMenu, } from "./auto-explore.js";
10
+ import { defaultMemValueForKey } from "./auto-explore-run.js";
11
+ function resolveBaseUrl(projectDir) {
12
+ if (process.env.WAYGRAPH_BASE_URL)
13
+ return process.env.WAYGRAPH_BASE_URL;
14
+ const pkgPath = join(projectDir, "package.json");
15
+ if (!existsSync(pkgPath))
16
+ return undefined;
17
+ try {
18
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
19
+ return pkg.waygraph?.baseUrl;
20
+ }
21
+ catch {
22
+ return undefined;
23
+ }
24
+ }
25
+ function seedMem(library, mem, dataJson) {
26
+ const raw = dataJson ?? process.env.WAYGRAPH_DATA ?? process.env.WAYGRAPH_AUTO_MEM;
27
+ let envParsed = {};
28
+ if (raw) {
29
+ try {
30
+ envParsed = JSON.parse(raw);
31
+ }
32
+ catch {
33
+ throw new Error(`waygraph traverse: --data / WAYGRAPH_DATA is not valid JSON`);
34
+ }
35
+ }
36
+ for (const entry of library.values()) {
37
+ for (const k of entry.block.requires ?? []) {
38
+ if (mem.has(k))
39
+ continue;
40
+ if (k.name in envParsed) {
41
+ mem.set(k, envParsed[k.name]);
42
+ continue;
43
+ }
44
+ const def = defaultMemValueForKey(k.name);
45
+ if (def !== undefined)
46
+ mem.set(k, def);
47
+ }
48
+ }
49
+ }
50
+ function missingMemKeys(entry, mem) {
51
+ return (entry.block.requires ?? []).filter((k) => !mem.has(k));
52
+ }
53
+ function ensureMem(entry, mem) {
54
+ const missing = missingMemKeys(entry, mem);
55
+ if (missing.length === 0)
56
+ return;
57
+ throw new Error(`Block "${entry.block.name}" needs Mem keys: ${missing.map((k) => k.name).join(", ")} ` +
58
+ `(pass --data '{...}' or WAYGRAPH_DATA)`);
59
+ }
60
+ function edgeKey(edge) {
61
+ const id = edge.instanceOption?.id;
62
+ return id ? `${edge.block}::${id}` : edge.block;
63
+ }
64
+ async function detectHere(page, navBlocks) {
65
+ try {
66
+ const url = page.url();
67
+ if (url.includes("/login"))
68
+ return "PiaLogin";
69
+ if (url.includes("mailpit") || url.includes(":54324") || url.includes("/mailhog")) {
70
+ return "PiaMailpitGui";
71
+ }
72
+ }
73
+ catch {
74
+ /* ignore */
75
+ }
76
+ const tags = navBlocks.map((n) => n.block);
77
+ return locate(page, tags, { timeoutMs: 1500 });
78
+ }
79
+ async function runOneBlock(engine, entry, context, page, mem) {
80
+ const flow = engine.defineFlow([start, entry.block, end]);
81
+ const BLOCK_TIMEOUT_MS = 45_000;
82
+ let timer;
83
+ try {
84
+ const ran = await Promise.race([
85
+ flow.run(context, mem, { page, closeOnFinish: false }),
86
+ new Promise((_, reject) => {
87
+ timer = setTimeout(() => reject(new Error(`Block "${entry.block.name}" timed out after ${BLOCK_TIMEOUT_MS / 1000}s`)), BLOCK_TIMEOUT_MS);
88
+ }),
89
+ ]);
90
+ return ran.result;
91
+ }
92
+ finally {
93
+ if (timer)
94
+ clearTimeout(timer);
95
+ }
96
+ }
97
+ /**
98
+ * Serial graph crawl. Prints greppable PASS/FAIL lines; non-zero exit on break.
99
+ */
100
+ export async function runTraverse(projectDir, options = {}) {
101
+ const traverseId = options.traverseId ?? "traverse-1";
102
+ const maxSteps = options.maxSteps ?? 50;
103
+ const maxVisitsPerNode = options.maxVisitsPerNode ?? 2;
104
+ const maxVisitsPerEdge = options.maxVisitsPerEdge ?? 1;
105
+ const timeoutMs = options.timeoutMs ?? 15 * 60_000;
106
+ const headed = options.headed === true || process.env.WAYGRAPH_HEADED === "1";
107
+ const baseURL = options.baseURL ?? resolveBaseUrl(projectDir) ?? process.env.WAYGRAPH_BASE_URL;
108
+ const startUrl = options.startUrl ?? baseURL;
109
+ const { graph, library } = await buildExploreContext(projectDir);
110
+ const mem = new MemPage();
111
+ seedMem(library.byName, mem, options.data);
112
+ const engine = new Engine({ headless: !headed, slowMo: headed ? 100 : 0 });
113
+ const { chromium } = await import("@playwright/test");
114
+ const executablePath = process.env.CHROME_PATH || process.env.CHROMIUM_PATH;
115
+ const launchOpts = {
116
+ headless: !headed,
117
+ };
118
+ if (executablePath)
119
+ launchOpts.executablePath = executablePath;
120
+ const browser = await chromium.launch(launchOpts);
121
+ const contextOpts = {
122
+ viewport: { width: 1280, height: 720 },
123
+ };
124
+ if (baseURL)
125
+ contextOpts.baseURL = baseURL;
126
+ const context = await browser.newContext(contextOpts);
127
+ let page = await context.newPage();
128
+ const nodeVisits = new Map();
129
+ const edgeVisits = new Map();
130
+ const edgesHit = new Set();
131
+ let steps = 0;
132
+ const startedAt = Date.now();
133
+ let lastHere = options.from ?? null;
134
+ let exitCode = 0;
135
+ console.error(`waygraph traverse[${traverseId}]: ${graph.nodes.length} node(s), ${graph.edges.length} edge(s)` +
136
+ (baseURL ? ` base=${baseURL}` : "") +
137
+ ` maxSteps=${maxSteps} maxVisits/node=${maxVisitsPerNode} maxVisits/edge=${maxVisitsPerEdge}`);
138
+ try {
139
+ if (startUrl) {
140
+ await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(() => { });
141
+ await page.waitForLoadState("load").catch(() => { });
142
+ }
143
+ for (;;) {
144
+ if (Date.now() - startedAt > timeoutMs) {
145
+ console.log(`[Killed timeout[${traverseId}] node=${lastHere ?? "?"} wallMs=${timeoutMs}]`);
146
+ exitCode = 1;
147
+ break;
148
+ }
149
+ if (steps >= maxSteps) {
150
+ console.log(`[Killed max-steps[${traverseId}] node=${lastHere ?? "?"} steps=${steps} max=${maxSteps}]`);
151
+ exitCode = 1;
152
+ break;
153
+ }
154
+ if (page.isClosed()) {
155
+ page = await context.newPage();
156
+ if (startUrl) {
157
+ await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(() => { });
158
+ }
159
+ }
160
+ const detected = await detectHere(page, library.navBlocks);
161
+ const here = lastHere ?? detected ?? options.from ?? null;
162
+ if (here && (nodeVisits.get(here) ?? 0) >= maxVisitsPerNode) {
163
+ console.log(`[Killed loop[${traverseId}] node=${here} visits=${nodeVisits.get(here)} max=${maxVisitsPerNode}]`);
164
+ exitCode = 1;
165
+ break;
166
+ }
167
+ const menu = await buildExploreMenu(page, graph, library, here);
168
+ const candidates = menu.flat.filter((edge) => {
169
+ const key = edgeKey(edge);
170
+ return (edgeVisits.get(key) ?? 0) < maxVisitsPerEdge;
171
+ });
172
+ if (candidates.length === 0) {
173
+ const label = here ?? detected ?? "unknown";
174
+ console.log(`[Reached Leaf Node[${traverseId}] at=${label} steps=${steps}]`);
175
+ console.error(`waygraph traverse[${traverseId}]: PASS leaf edgesHit=${edgesHit.size} steps=${steps}`);
176
+ break;
177
+ }
178
+ const pick = [...candidates].sort((a, b) => {
179
+ const aWild = a.from === "*" ? 1 : 0;
180
+ const bWild = b.from === "*" ? 1 : 0;
181
+ if (aWild !== bWild)
182
+ return aWild - bWild;
183
+ return edgeKey(a).localeCompare(edgeKey(b));
184
+ })[0];
185
+ const entry = library.byName.get(pick.block);
186
+ if (!entry) {
187
+ console.log(`[Broke at edge[${traverseId}] block=${pick.block} from=${here ?? "?"} to=${pick.to} reason=not-loaded]`);
188
+ exitCode = 1;
189
+ break;
190
+ }
191
+ const fromLabel = here ?? pick.from;
192
+ try {
193
+ if (pick.instanceOption) {
194
+ mem.set(pick.instanceOption.key, pick.instanceOption.value);
195
+ }
196
+ ensureMem(entry, mem);
197
+ console.error(`waygraph traverse[${traverseId}]: step ${steps + 1} ${fromLabel} -[${pick.block}]-> ${pick.to}` +
198
+ (pick.label ? ` (${pick.label})` : ""));
199
+ const out = await runOneBlock(engine, entry, context, page, mem);
200
+ const observed = typeof out === "object" && out && "__state" in out
201
+ ? String(out.__state)
202
+ : pick.to;
203
+ const key = edgeKey(pick);
204
+ edgeVisits.set(key, (edgeVisits.get(key) ?? 0) + 1);
205
+ edgesHit.add(key);
206
+ if (here)
207
+ nodeVisits.set(here, (nodeVisits.get(here) ?? 0) + 1);
208
+ lastHere = observed;
209
+ steps += 1;
210
+ if (observed !== pick.to && pick.to !== "*") {
211
+ console.error(`waygraph traverse[${traverseId}]: WARN static to=${pick.to} observed=${observed}`);
212
+ }
213
+ }
214
+ catch (err) {
215
+ const msg = err instanceof Error ? err.message : String(err);
216
+ console.log(`[Broke at edge[${traverseId}] block=${pick.block} from=${fromLabel} to=${pick.to}] ${msg}`);
217
+ exitCode = 1;
218
+ break;
219
+ }
220
+ }
221
+ }
222
+ finally {
223
+ await browser.close().catch(() => { });
224
+ }
225
+ console.error(`waygraph traverse[${traverseId}]: done exit=${exitCode} steps=${steps} edgesHit=${edgesHit.size}`);
226
+ return exitCode;
227
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.12.7",
3
+ "version": "0.12.9",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",