waygraph 0.16.2 → 0.16.4

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 (123) hide show
  1. package/README.md +12 -0
  2. package/dist/analysis/practices-check.d.ts +1 -1
  3. package/dist/analysis/practices-check.js +36 -0
  4. package/dist/beta.d.ts +6 -0
  5. package/dist/beta.js +5 -0
  6. package/dist/cli/commands/codemod-flows.js +2 -2
  7. package/dist/cli/commands/codemod-stubs.js +43 -1
  8. package/dist/cli/commands/pilot.js +3 -16
  9. package/dist/cli/commands/session-ops.js +26 -1
  10. package/dist/cli/flags.d.ts +1 -1
  11. package/dist/cli/flags.js +1 -1
  12. package/dist/cli/usage.js +6 -0
  13. package/dist/demo/narration/stub-ctx.js +126 -124
  14. package/dist/demo/overlay/cursor.js +1 -1
  15. package/dist/demo/overlay/inpage/banner.js +5 -0
  16. package/dist/demo/overlay/inpage/core.js +1 -0
  17. package/dist/demo/overlay/inpage/network.js +1 -1
  18. package/dist/demo/overlay/inpage/todos.js +0 -15
  19. package/dist/demo/overlay/network-capture.js +2 -2
  20. package/dist/demo/overlay/overlay-css.js +18 -21
  21. package/dist/demo/overlay/overlay-stage.js +1 -1
  22. package/dist/demo/overlay/phase-fixtures.js +5 -5
  23. package/dist/demo/overlay/pilot/shell.js +7 -5
  24. package/dist/demo/overlay/ui/components/ring.js +2 -2
  25. package/dist/demo/overlay/ui/overlay.css +98 -35
  26. package/dist/demo/overlay/ui/shadow.js +69 -2
  27. package/dist/demo/stepper/demo-log.js +8 -8
  28. package/dist/demo/stepper/slides.d.ts +1 -1
  29. package/dist/demo/stepper/slides.js +1 -1
  30. package/dist/demo/stepper/stepper.js +1 -1
  31. package/dist/engine/block/kind.d.ts +3 -0
  32. package/dist/engine/block/kind.js +11 -0
  33. package/dist/engine/errors.d.ts +12 -0
  34. package/dist/engine/errors.js +12 -0
  35. package/dist/engine/read/read-block.d.ts +61 -0
  36. package/dist/engine/read/read-block.js +94 -0
  37. package/dist/engine/read/read-key.d.ts +32 -0
  38. package/dist/engine/read/read-key.js +34 -0
  39. package/dist/engine/read/read-page.d.ts +19 -0
  40. package/dist/engine/read/read-page.js +50 -0
  41. package/dist/engine/read/readers.d.ts +82 -0
  42. package/dist/engine/read/readers.js +43 -0
  43. package/dist/engine/read/run-read.d.ts +17 -0
  44. package/dist/engine/read/run-read.js +156 -0
  45. package/dist/engine/run/connect.js +3 -2
  46. package/dist/engine/run/events.d.ts +28 -10
  47. package/dist/engine/run/events.js +52 -38
  48. package/dist/engine/run/run.js +25 -13
  49. package/dist/engine/stub/stub.d.ts +26 -21
  50. package/dist/index.d.ts +7 -4
  51. package/dist/index.js +9 -4
  52. package/dist/live/auto/auto.js +2 -3
  53. package/dist/live/auto/menu.js +3 -2
  54. package/dist/live/auto-session/auto-session.d.ts +9 -2
  55. package/dist/live/auto-session/auto-session.js +23 -20
  56. package/dist/live/auto-session/feed.d.ts +16 -0
  57. package/dist/live/auto-session/feed.js +71 -0
  58. package/dist/live/auto-session/helpers.d.ts +7 -0
  59. package/dist/live/auto-session/helpers.js +9 -0
  60. package/dist/live/auto-session/ipc.d.ts +15 -47
  61. package/dist/live/auto-session/ipc.js +34 -4
  62. package/dist/live/auto-session/protocol.d.ts +127 -0
  63. package/dist/live/auto-session/protocol.js +6 -0
  64. package/dist/live/auto-session/read.d.ts +25 -0
  65. package/dist/live/auto-session/read.js +50 -0
  66. package/dist/live/traverse/worker.d.ts +1 -0
  67. package/dist/live/traverse/worker.js +6 -1
  68. package/dist/manifest/manifest.d.ts +22 -2
  69. package/dist/manifest/manifest.js +27 -1
  70. package/dist/manifest/project.d.ts +10 -1
  71. package/dist/manifest/project.js +9 -12
  72. package/dist/manifest/read-blocks.d.ts +2 -0
  73. package/dist/manifest/read-blocks.js +23 -0
  74. package/dist/manifest/scan.d.ts +3 -2
  75. package/dist/manifest/scan.js +8 -4
  76. package/dist/runner/runner.d.ts +0 -1
  77. package/dist/runner/runner.js +6 -6
  78. package/dist/runtime/full-launch.d.ts +2 -1
  79. package/dist/runtime/full-launch.js +29 -27
  80. package/dist/runtime/run-log.d.ts +2 -0
  81. package/dist/runtime/run-log.js +13 -6
  82. package/examples/saucedemo/README.md +2 -2
  83. package/examples/saucedemo/src/blocks/SITE-MAP.md +3 -0
  84. package/examples/saucedemo/src/blocks/saucedemo-web/cart/cart.read.block.ts +26 -0
  85. package/examples/saucedemo/src/blocks/saucedemo-web/header.read.block.ts +25 -0
  86. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/inventory.read.block.ts +28 -0
  87. package/examples/saucedemo/src/flows/api-demo.flow.ts +1 -1
  88. package/examples/saucedemo/src/flows/cart-bulk.flow.ts +1 -1
  89. package/examples/saucedemo/src/flows/cart-continue.flow.ts +1 -1
  90. package/examples/saucedemo/src/flows/catalog-logout.flow.ts +1 -1
  91. package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +1 -1
  92. package/examples/saucedemo/src/flows/checkout.flow.ts +11 -11
  93. package/examples/saucedemo/src/flows/login.flow.ts +1 -1
  94. package/examples/saucedemo/src/flows/shop.flow.ts +1 -1
  95. package/examples/saucedemo/src/flows/stub-on-error.flow.ts +1 -1
  96. package/examples/saucedemo/src/flows/viewer-blocked.flow.ts +1 -1
  97. package/examples/saucedemo/src/states/checkout.reads.ts +9 -0
  98. package/examples/saucedemo/src/stubs/device-showcase.ts +37 -37
  99. package/examples/saucedemo/src/stubs/finish-order.ts +2 -2
  100. package/examples/saucedemo/src/stubs/login.ts +5 -5
  101. package/package.json +5 -1
  102. package/templates/quickstart/src/flows/api-demo.flow.ts +1 -1
  103. package/templates/quickstart/src/flows/cart-bulk.flow.ts +1 -1
  104. package/templates/quickstart/src/flows/cart-continue.flow.ts +1 -1
  105. package/templates/quickstart/src/flows/catalog-logout.flow.ts +1 -1
  106. package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +1 -1
  107. package/templates/quickstart/src/flows/checkout.flow.ts +11 -11
  108. package/templates/quickstart/src/flows/login.flow.ts +1 -1
  109. package/templates/quickstart/src/flows/shop.flow.ts +1 -1
  110. package/templates/quickstart/src/flows/stub-on-error.flow.ts +1 -1
  111. package/templates/quickstart/src/flows/viewer-blocked.flow.ts +1 -1
  112. package/templates/quickstart/src/stubs/device-showcase.ts +37 -37
  113. package/templates/quickstart/src/stubs/finish-order.ts +2 -2
  114. package/templates/quickstart/src/stubs/login.ts +5 -5
  115. package/templates/scaffold/src/flows/(external)/hoppscotch/auth.flow.ts +1 -1
  116. package/templates/scaffold/src/flows/(external)/hoppscotch/get.flow.ts +1 -1
  117. package/templates/scaffold/src/flows/(external)/hoppscotch/post.flow.ts +1 -1
  118. package/templates/scaffold/src/flows/(user-stories)/email-verification.flow.ts +4 -4
  119. package/templates/scaffold/src/flows/README.md +1 -1
  120. package/templates/scaffold/src/flows/home/cart.flow.ts +7 -7
  121. package/templates/scaffold/src/flows/home/hello.flow.ts +8 -8
  122. package/templates/skills/waygraph-convention.skill.md +10 -2
  123. package/templates/skills/waygraph-pilot.skill.md +7 -0
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
- // The runtime layer supplies how an Engine launches its own browser (config, Chromium path, extensions).
2
- import "./runtime/full-launch.js";
3
- // Run events are written to the project's run log (.waygraph/runs) when the CLI turns it on.
4
- import "./runtime/run-log.js";
1
+ import { installFullLaunch } from "./runtime/full-launch.js";
2
+ import { installRunLog } from "./runtime/run-log.js";
3
+ // The package entry wires the runtime in, explicitly: how an Engine launches its own browser (config, Chromium path,
4
+ // extensions), and the run log (.waygraph/runs) the CLI turns on. No module switches itself on by being imported.
5
+ installFullLaunch();
6
+ installRunLog();
5
7
  export { connect } from "./engine/run/connect.js";
6
8
  export { checkpoint } from "./engine/checkpoint/checkpoint.js";
7
9
  export { MemKey, MemPage, key, keyGroup } from "./engine/mem/mem.js";
@@ -39,6 +41,9 @@ export { runTraverse } from "./live/traverse/traverse.js";
39
41
  export { buildCoverageReport, collectGraphEdgeKeys, defaultCoverageOutPath, formatCoverageLine, normalizeHitKey, parseMinEdgeCoverage, writeCoverageReport } from "./live/traverse/coverage.js";
40
42
  export { WG_UI_SEL, WG_READY_PANEL_SEL, WG_READY_AUTO_SEL, WG_READY_BANNER_SEL, readWgOverlayBeacon, readWgOverlayBeaconInPage, assertWgOverlayReady } from "./demo/overlay/beacon.js";
41
43
  export { AutoSession } from "./live/auto-session/auto-session.js";
44
+ export { SESSION_OPS, SESSION_PROTOCOL_VERSION } from "./live/auto-session/protocol.js";
45
+ export { subscribeSession } from "./live/auto-session/feed.js";
46
+ export { requestSession } from "./live/auto-session/ipc.js";
42
47
  export { stubFilePath } from "./live/auto-session/helpers.js";
43
48
  export { pilotStart } from "./live/pilot/pilot.js";
44
49
  export { defineConfig, resolveBaseUrl } from "./runtime/waygraph-config.js";
@@ -61,8 +61,6 @@ export async function runOneBlock(engine, entry, context, page, mem, headful) {
61
61
  await page.evaluate("delete window.__wgPendingNavClickLabel").catch(ignorePageGone);
62
62
  return result;
63
63
  }
64
- /** Shown in the headful panel after each Block run. */
65
- let lastRunNote = null;
66
64
  /**
67
65
  * Interactive waygraph auto: locate current Checkpoint, list runnable Blocks, pick one, repeat.
68
66
  */
@@ -70,7 +68,8 @@ export async function runAutoExplore(projectDir, options = {}) {
70
68
  await withWebServer(projectDir, () => runAutoExploreCore(projectDir, options));
71
69
  }
72
70
  async function runAutoExploreCore(projectDir, options = {}) {
73
- lastRunNote = null;
71
+ /** Shown in the headful panel after each Block run. */
72
+ let lastRunNote = null;
74
73
  const cli = options.cli === true;
75
74
  const baseURL = options.baseURL ?? (await resolveBaseUrl(projectDir)) ?? envString("WAYGRAPH_BASE_URL");
76
75
  const startUrl = options.startUrl ?? baseURL;
@@ -27,8 +27,9 @@ export async function setAutoPanelRunning(page, label) {
27
27
  }, label)
28
28
  .catch(ignorePageGone);
29
29
  }
30
- export const PANEL_CSS = "#wg-auto-panel{position:fixed;z-index:2147483647;left:50%;bottom:12px;transform:translateX(-50%);" +
31
- "max-width:min(92vw,720px);max-height:calc(100vh - 24px);overflow-y:auto;box-sizing:border-box;" +
30
+ // In the bottom-center slot of the overlay layout (demo/overlay/ui/css/layout.css), like the demo's step panel.
31
+ export const PANEL_CSS = "#wg-auto-panel{z-index:var(--wg-z-top);" +
32
+ "width:min(92vw,720px);overflow-y:auto;box-sizing:border-box;" +
32
33
  "background:rgba(20,10,40,.94);color:#fff;border-radius:14px;padding:16px 20px;" +
33
34
  "font:14px/1.4 system-ui,sans-serif;box-shadow:0 12px 30px rgba(0,0,0,.35);}" +
34
35
  "#wg-auto-panel h3{margin:0 0 10px;font-size:16px;}" +
@@ -1,5 +1,6 @@
1
1
  import type { PilotHighlightFixtures } from "../../demo/overlay/pilot/fixtures.js";
2
2
  import type { ApplyPathResult, ApplyPickResult, AutoSessionInit, ConsoleLogEntry, InspectDomOptions, InspectDomResult, SessionSnapshot, StorageSnapshot, StubFileKind, TraceStep } from "./helpers.js";
3
+ import { type SessionRead } from "./read.js";
3
4
  /**
4
5
  * A live session driven over IPC (`browser start`, `pilot start`): the same graph, library, mem and browser as
5
6
  * `waygraph auto`, moved one call at a time (`applyPick`, `applyPath`, the raw actions) instead of a menu loop.
@@ -17,12 +18,12 @@ export declare class AutoSession {
17
18
  private page;
18
19
  private readonly startUrl;
19
20
  private readonly sessionId;
21
+ private readonly knownInteractionSelectors;
22
+ private readonly headless;
20
23
  private here;
21
24
  private lastRunNote;
22
25
  private readonly trace;
23
26
  private readonly consoleLog;
24
- private readonly knownInteractionSelectors;
25
- private readonly headless;
26
27
  private lastOverlayInfo;
27
28
  private constructor();
28
29
  static start(init: AutoSessionInit): Promise<AutoSession>;
@@ -43,8 +44,14 @@ export declare class AutoSession {
43
44
  * Deduped per page load so repeated status/send calls stay quiet.
44
45
  */
45
46
  private warnUnmappedInteractions;
47
+ /** The Checkpoint the session is at, if known. */
48
+ get where(): string | null;
46
49
  /** Pure getter - no Block runs, no mem/page mutation. */
47
50
  currentSnapshot(): Promise<SessionSnapshot>;
51
+ /** The menu as a snapshot, with what the page shows (Read Blocks) when any covers it. */
52
+ private snapshotOf;
53
+ /** Pure getter - what the page shows, from the project's Read Blocks (beta). */
54
+ read(path?: string): Promise<SessionRead>;
48
55
  /**
49
56
  * Pure getter - reads the live page's structure at the requested fidelity.
50
57
  * No Block runs, no mem/page mutation. `selector` scopes either mode to one
@@ -3,7 +3,7 @@ import { findBlockPathDetailed } from "../../analysis/graph.js";
3
3
  import { runStubPhase } from "../../demo/narration/narration.js";
4
4
  import { describeOverlayFixtures, phaseToOverlayFixtures } from "../../demo/overlay/phase-fixtures.js";
5
5
  import { showPilotFixtures } from "../../demo/overlay/pilot/fixtures.js";
6
- import { showPilotActivity, showPilotVision } from "../../demo/overlay/pilot/shell.js";
6
+ import { showPilotActivity } from "../../demo/overlay/pilot/shell.js";
7
7
  import { keepOverlayOnNavigation } from "../../demo/overlay/reattach.js";
8
8
  import { verboseLog } from "../../demo/verbose-log.js";
9
9
  import { Engine } from "../../engine/engine.js";
@@ -15,17 +15,10 @@ import { buildExploreContext, buildExploreMenu } from "../auto/explore.js";
15
15
  import { ensureMem, seedDefaultMem } from "../auto/mem.js";
16
16
  import { updatePilotOverlay } from "../pilot/panel.js";
17
17
  import { recordPageConsole } from "./console.js";
18
- import { CONSOLE_LOG_MAX_ENTRIES, TRACE_MAX_STEPS, buildSessionSnapshot, describeHighlight, parsePick, stubFilePath, stubPhaseHasContent } from "./helpers.js";
18
+ import { CONSOLE_LOG_MAX_ENTRIES, TRACE_MAX_STEPS, buildSessionSnapshot, describeHighlight, parsePick, rawTarget, stubFilePath, stubPhaseHasContent } from "./helpers.js";
19
19
  import { inspectDom, inspectStorage } from "./inspect.js";
20
20
  import { openSessionBrowser } from "./open-browser.js";
21
- /** The first element `selector` matches, ringed with the pilot's vision ring; or why there is none. */
22
- async function rawTarget(page, selector, label) {
23
- const locator = page.locator(selector).first();
24
- if ((await locator.count()) === 0)
25
- return `no element matches selector "${selector}"`;
26
- await showPilotVision(page, selector, label);
27
- return locator;
28
- }
21
+ import { readSession, snapshotRead } from "./read.js";
29
22
  /**
30
23
  * A live session driven over IPC (`browser start`, `pilot start`): the same graph, library, mem and browser as
31
24
  * `waygraph auto`, moved one call at a time (`applyPick`, `applyPath`, the raw actions) instead of a menu loop.
@@ -43,12 +36,12 @@ export class AutoSession {
43
36
  page;
44
37
  startUrl;
45
38
  sessionId;
39
+ knownInteractionSelectors;
40
+ headless;
46
41
  here = null;
47
42
  lastRunNote = null;
48
43
  trace = [];
49
44
  consoleLog = [];
50
- knownInteractionSelectors;
51
- headless;
52
45
  lastOverlayInfo = null;
53
46
  constructor(projectDir, inject, blocksSelect, graph, library, mem, engine, closeBrowser, context, page, startUrl, sessionId, knownInteractionSelectors, headless) {
54
47
  this.projectDir = projectDir;
@@ -118,7 +111,7 @@ export class AutoSession {
118
111
  // watching sees the state an API caller just got back, not a frame behind.
119
112
  this.lastOverlayInfo = {
120
113
  sessionId: this.sessionId,
121
- snapshot: buildSessionSnapshot(menu, this.library.byName, this.here, this.lastRunNote),
114
+ snapshot: await this.snapshotOf(menu),
122
115
  graph: this.graph,
123
116
  };
124
117
  await updatePilotOverlay(this.page, this.lastOverlayInfo);
@@ -163,10 +156,20 @@ export class AutoSession {
163
156
  };
164
157
  await this.page.evaluate(unmappedInteractionsPageScript, payload).catch(ignorePageGone);
165
158
  }
159
+ /** The Checkpoint the session is at, if known. */
160
+ get where() { return this.here; }
166
161
  /** Pure getter - no Block runs, no mem/page mutation. */
167
162
  async currentSnapshot() {
168
- const menu = await this.currentMenu();
169
- return buildSessionSnapshot(menu, this.library.byName, this.here, this.lastRunNote);
163
+ return this.snapshotOf(await this.currentMenu());
164
+ }
165
+ /** The menu as a snapshot, with what the page shows (Read Blocks) when any covers it. */
166
+ async snapshotOf(menu) {
167
+ return { ...buildSessionSnapshot(menu, this.library.byName, this.here, this.lastRunNote), ...(await snapshotRead(this.page, this.mem, this.here)) };
168
+ }
169
+ /** Pure getter - what the page shows, from the project's Read Blocks (beta). */
170
+ async read(path) {
171
+ this.page = await ensureLivePage(this.context, this.page, this.startUrl);
172
+ return readSession(this.page, this.mem, this.here, path);
170
173
  }
171
174
  /**
172
175
  * Pure getter - reads the live page's structure at the requested fidelity.
@@ -238,7 +241,7 @@ export class AutoSession {
238
241
  const nextMenu = await this.currentMenu();
239
242
  return {
240
243
  ok: true,
241
- snapshot: buildSessionSnapshot(nextMenu, this.library.byName, this.here, this.lastRunNote),
244
+ snapshot: await this.snapshotOf(nextMenu),
242
245
  quit: false,
243
246
  };
244
247
  }
@@ -251,7 +254,7 @@ export class AutoSession {
251
254
  if (pick.type === "quit") {
252
255
  return {
253
256
  ok: true,
254
- snapshot: buildSessionSnapshot(menu, this.library.byName, this.here, this.lastRunNote),
257
+ snapshot: await this.snapshotOf(menu),
255
258
  quit: true,
256
259
  };
257
260
  }
@@ -292,7 +295,7 @@ export class AutoSession {
292
295
  this.lastRunNote = `resync -> ${this.here ?? "Unknown"}`;
293
296
  return {
294
297
  ok: true,
295
- snapshot: buildSessionSnapshot(menu, this.library.byName, this.here, this.lastRunNote),
298
+ snapshot: await this.snapshotOf(menu),
296
299
  quit: false,
297
300
  };
298
301
  }
@@ -363,7 +366,7 @@ export class AutoSession {
363
366
  this.lastRunNote = `${what} -> ${this.here ?? "Unknown"}`;
364
367
  return {
365
368
  ok: true,
366
- snapshot: buildSessionSnapshot(menu, this.library.byName, this.here, this.lastRunNote),
369
+ snapshot: await this.snapshotOf(menu),
367
370
  quit: false,
368
371
  };
369
372
  }
@@ -390,7 +393,7 @@ export class AutoSession {
390
393
  ok: true,
391
394
  painted,
392
395
  missing,
393
- snapshot: buildSessionSnapshot(menu, this.library.byName, this.here, this.lastRunNote),
396
+ snapshot: await this.snapshotOf(menu),
394
397
  };
395
398
  }
396
399
  catch (err) {
@@ -0,0 +1,16 @@
1
+ import { type Socket } from "node:net";
2
+ import type { SessionStreamMessage } from "./protocol.js";
3
+ /** The server side: subscribed sockets, fed every run event in this process until `close`. */
4
+ export declare function createFeed(): {
5
+ add(sock: Socket): void;
6
+ send(message: SessionStreamMessage): void;
7
+ close(): void;
8
+ };
9
+ /**
10
+ * The client side: subscribes to a running session and calls `onMessage` for each line until `close()` (or the session
11
+ * ends). Rejects when the session is unknown or does not answer the subscribe.
12
+ */
13
+ export declare function subscribeSession(projectDir: string, sessionId: string, onMessage: (message: SessionStreamMessage) => void, timeoutMs?: number): Promise<{
14
+ close(): void;
15
+ closed: Promise<void>;
16
+ }>;
@@ -0,0 +1,71 @@
1
+ // A session's live feed (`subscribe`): every run event in the session (steps, reads) and the snapshot after each change,
2
+ // as JSON lines to every subscribed socket. Waygraph UI watches a session through it; `pilot watch <id>` prints it.
3
+ import { connect } from "node:net";
4
+ import { onRunEvent } from "../../engine/run/events.js";
5
+ import { resolveSessionMeta } from "./files.js";
6
+ import { SESSION_PROTOCOL_VERSION } from "./protocol.js";
7
+ /** The server side: subscribed sockets, fed every run event in this process until `close`. */
8
+ export function createFeed() {
9
+ const subscribers = new Set();
10
+ const send = (message) => {
11
+ const line = JSON.stringify(message) + "\n";
12
+ for (const sock of subscribers)
13
+ if (!sock.destroyed)
14
+ sock.write(line);
15
+ };
16
+ const off = onRunEvent((event) => send({ type: "event", event }));
17
+ return {
18
+ add(sock) {
19
+ subscribers.add(sock);
20
+ sock.on("close", () => subscribers.delete(sock));
21
+ sock.write(JSON.stringify({ ok: true, protocol: SESSION_PROTOCOL_VERSION }) + "\n");
22
+ },
23
+ send,
24
+ close() {
25
+ off();
26
+ for (const sock of subscribers)
27
+ sock.end();
28
+ subscribers.clear();
29
+ },
30
+ };
31
+ }
32
+ /**
33
+ * The client side: subscribes to a running session and calls `onMessage` for each line until `close()` (or the session
34
+ * ends). Rejects when the session is unknown or does not answer the subscribe.
35
+ */
36
+ export function subscribeSession(projectDir, sessionId, onMessage, timeoutMs = 15_000) {
37
+ const meta = resolveSessionMeta(sessionId, projectDir);
38
+ if (!meta)
39
+ return Promise.reject(new Error(`no such session "${sessionId}" - \`waygraph pilot sessions\` lists live ids`));
40
+ return new Promise((resolve, reject) => {
41
+ const sock = connect(meta.socketPath);
42
+ const timer = setTimeout(() => {
43
+ sock.destroy();
44
+ reject(new Error(`session "${sessionId}" did not answer subscribe within ${timeoutMs}ms`));
45
+ }, timeoutMs);
46
+ const closed = new Promise((done) => sock.on("close", () => done()));
47
+ let buf = "";
48
+ let acked = false;
49
+ sock.on("connect", () => sock.write(JSON.stringify({ op: "subscribe" }) + "\n"));
50
+ sock.on("error", (err) => {
51
+ clearTimeout(timer);
52
+ if (!acked)
53
+ reject(err);
54
+ });
55
+ sock.on("data", (chunk) => {
56
+ buf += chunk.toString("utf-8");
57
+ let nl;
58
+ while ((nl = buf.indexOf("\n")) !== -1) {
59
+ const line = buf.slice(0, nl);
60
+ buf = buf.slice(nl + 1);
61
+ if (!acked) {
62
+ acked = true;
63
+ clearTimeout(timer);
64
+ resolve({ close: () => sock.end(), closed });
65
+ continue;
66
+ }
67
+ onMessage(JSON.parse(line));
68
+ }
69
+ });
70
+ });
71
+ }
@@ -1,3 +1,4 @@
1
+ import type { Locator, Page } from "@playwright/test";
1
2
  import type { PilotHighlightFixtures } from "../../demo/overlay/pilot/fixtures.js";
2
3
  import type { StubPhaseResult } from "../../engine/stub/stub.js";
3
4
  import type { BlockEntry } from "../../manifest/project.js";
@@ -27,6 +28,10 @@ export interface SessionSnapshot {
27
28
  /** True when there are no runnable moves from `here` right now. */
28
29
  done: boolean;
29
30
  lastRunNote: string | null;
31
+ /** What the page shows, from the project's Read Blocks (beta); absent when none covers this page. */
32
+ read?: Record<string, unknown>;
33
+ missing?: string[];
34
+ readNotes?: Record<string, string>;
30
35
  }
31
36
  /** Plain, JSON-serializable view of a menu - no Block/MemKey/class references. */
32
37
  export declare function buildSessionSnapshot(menu: ExploreMenu, library: Map<string, BlockEntry>, here: string | null, lastRunNote: string | null): SessionSnapshot;
@@ -214,4 +219,6 @@ export interface StorageSnapshot {
214
219
  }
215
220
  /** What a highlight call paints, in a few words, for the session's activity line. */
216
221
  export declare function describeHighlight(fixtures: PilotHighlightFixtures): string;
222
+ /** The first element `selector` matches, ringed with the pilot's vision ring; or why there is none. */
223
+ export declare function rawTarget(page: Page, selector: string, label: string): Promise<Locator | string>;
217
224
  export {};
@@ -1,6 +1,7 @@
1
1
  // The session's shared shapes and pure helpers: snapshots, picks, trace / console entries, DOM walking, stub files.
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { showPilotVision } from "../../demo/overlay/pilot/shell.js";
4
5
  /** True when a stub-phase result actually carries authored content worth keeping. */
5
6
  export function stubPhaseHasContent(r) {
6
7
  return (Object.keys(r.highlights).length > 0 ||
@@ -163,3 +164,11 @@ export function describeHighlight(fixtures) {
163
164
  .filter(Boolean)
164
165
  .join(", ") || "fixtures");
165
166
  }
167
+ /** The first element `selector` matches, ringed with the pilot's vision ring; or why there is none. */
168
+ export async function rawTarget(page, selector, label) {
169
+ const locator = page.locator(selector).first();
170
+ if ((await locator.count()) === 0)
171
+ return `no element matches selector "${selector}"`;
172
+ await showPilotVision(page, selector, label);
173
+ return locator;
174
+ }
@@ -1,52 +1,8 @@
1
1
  import type { PilotHighlightFixtures } from "../../demo/overlay/pilot/fixtures.js";
2
2
  import { AutoSession } from "./auto-session.js";
3
3
  import { SessionMeta } from "./files.js";
4
- import { type ApplyPathResult, type AutoSessionInit, type ConsoleLogEntry, type DomSnapshot, type InspectDomOptions, type SessionSnapshot, type StorageSnapshot, type StubFileKind, type TraceStep } from "./helpers.js";
5
- export type StatusOrSendResponse = {
6
- ok: true;
7
- snapshot: SessionSnapshot;
8
- quit: boolean;
9
- } | {
10
- ok: false;
11
- error: string;
12
- };
13
- export type DomResponse = {
14
- ok: true;
15
- snapshot: DomSnapshot;
16
- } | {
17
- ok: false;
18
- error: string;
19
- };
20
- export type TraceResponse = {
21
- ok: true;
22
- trace: TraceStep[];
23
- } | {
24
- ok: false;
25
- error: string;
26
- };
27
- export type ConsoleResponse = {
28
- ok: true;
29
- consoleLog: ConsoleLogEntry[];
30
- } | {
31
- ok: false;
32
- error: string;
33
- };
34
- export type StorageResponse = ({
35
- ok: true;
36
- } & StorageSnapshot) | {
37
- ok: false;
38
- error: string;
39
- };
40
- export type ReachResponse = ApplyPathResult;
41
- export type HighlightResponse = {
42
- ok: true;
43
- painted: number;
44
- missing: string[];
45
- snapshot: SessionSnapshot;
46
- } | {
47
- ok: false;
48
- error: string;
49
- };
4
+ import { type AutoSessionInit, type InspectDomOptions, type StubFileKind } from "./helpers.js";
5
+ import type { HelloResponse, ConsoleResponse, DomResponse, HighlightResponse, ReachResponse, ReadResponse, StatusOrSendResponse, StorageResponse, TraceResponse } from "./protocol.js";
50
6
  /**
51
7
  * Client-side: sends one request to a running session and returns its
52
8
  * response. Reports a stale/unreachable session as an error within
@@ -94,6 +50,16 @@ export declare function requestSession(projectDir: string, sessionId: string, re
94
50
  export declare function requestSession(projectDir: string, sessionId: string, request: {
95
51
  op: "storage";
96
52
  }, timeoutMs?: number): Promise<StorageResponse>;
53
+ export declare function requestSession(projectDir: string, sessionId: string, request: {
54
+ op: "hello";
55
+ }, timeoutMs?: number): Promise<HelloResponse | {
56
+ ok: false;
57
+ error: string;
58
+ }>;
59
+ export declare function requestSession(projectDir: string, sessionId: string, request: {
60
+ op: "read";
61
+ path?: string;
62
+ }, timeoutMs?: number): Promise<ReadResponse>;
97
63
  export declare function requestSession(projectDir: string, sessionId: string, request: {
98
64
  op: "reach";
99
65
  checkpoint: string;
@@ -110,7 +76,9 @@ export declare function requestSession(projectDir: string, sessionId: string, re
110
76
  * already prevents true parallelism, but two overlapping `await`-heavy
111
77
  * handlers could still interleave against the same page/mem.
112
78
  */
113
- export declare function serveSession(session: AutoSession, projectDir: string, sessionId: string, headless?: boolean): Promise<void>;
79
+ export declare function serveSession(session: AutoSession, projectDir: string, sessionId: string, headless?: boolean,
80
+ /** After a quit closed the session and removed its files: the session process exits here. */
81
+ onShutdown?: () => void): Promise<void>;
114
82
  /** Entry point for the hidden `__auto-serve` CLI command (runs in the detached child). */
115
83
  export declare function runAutoServeCommand(init: AutoSessionInit, sessionId: string): Promise<void>;
116
84
  /**
@@ -5,7 +5,7 @@
5
5
  * See openspec/changes/waygraph-auto-cli-session-control.
6
6
  */
7
7
  import { spawn } from "node:child_process";
8
- import { mkdirSync, openSync, rmSync, writeFileSync, } from "node:fs";
8
+ import { mkdirSync, openSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
9
9
  import { createRequire } from "node:module";
10
10
  import { connect, createServer } from "node:net";
11
11
  import { dirname, join } from "node:path";
@@ -15,6 +15,8 @@ import { fileURLToPath } from "node:url";
15
15
  import { ignorePageGone } from "../../engine/page-gone.js";
16
16
  import { AutoSession } from "./auto-session.js";
17
17
  import { generateSessionId, isPidAlive, logPathFor, metaPath, readSessionMeta, removeSessionFiles, resolveSessionMeta, sessionDir, socketDir, socketPathFor, writeGlobalSessionMeta } from "./files.js";
18
+ import { createFeed } from "./feed.js";
19
+ import { SESSION_PROTOCOL_VERSION } from "./protocol.js";
18
20
  /** Reads newline-delimited JSON off a socket, one message at a time. */
19
21
  function readOneMessage(sock, timeoutMs) {
20
22
  return new Promise((resolvePromise, reject) => {
@@ -109,7 +111,9 @@ export async function requestSession(projectDir, sessionId, request, timeoutMs =
109
111
  * already prevents true parallelism, but two overlapping `await`-heavy
110
112
  * handlers could still interleave against the same page/mem.
111
113
  */
112
- export async function serveSession(session, projectDir, sessionId, headless = true) {
114
+ export async function serveSession(session, projectDir, sessionId, headless = true,
115
+ /** After a quit closed the session and removed its files: the session process exits here. */
116
+ onShutdown = () => { }) {
113
117
  mkdirSync(sessionDir(projectDir), { recursive: true });
114
118
  mkdirSync(socketDir(), { recursive: true });
115
119
  const socketPath = socketPathFor(sessionId);
@@ -120,6 +124,7 @@ export async function serveSession(session, projectDir, sessionId, headless = tr
120
124
  queue = next.catch(() => { });
121
125
  return next;
122
126
  };
127
+ const feed = createFeed();
123
128
  const server = createServer((sock) => {
124
129
  let buf = "";
125
130
  sock.on("data", (chunk) => {
@@ -141,6 +146,15 @@ export async function serveSession(session, projectDir, sessionId, headless = tr
141
146
  sock.write(JSON.stringify({ ok: false, error: "malformed request (invalid JSON)" }) + "\n");
142
147
  return;
143
148
  }
149
+ // Neither waits behind a running request: hello names the protocol, subscribe turns this socket into a live feed.
150
+ if (request.op === "hello") {
151
+ sock.write(JSON.stringify({ ok: true, protocol: SESSION_PROTOCOL_VERSION, waygraph: waygraphVersion(), sessionId, here: session.where }) + "\n");
152
+ return;
153
+ }
154
+ if (request.op === "subscribe") {
155
+ feed.add(sock);
156
+ return;
157
+ }
144
158
  await enqueue(async () => {
145
159
  let response;
146
160
  let shouldQuit = false;
@@ -165,6 +179,9 @@ export async function serveSession(session, projectDir, sessionId, headless = tr
165
179
  else if (request.op === "console") {
166
180
  response = { ok: true, consoleLog: session.getConsoleLog() };
167
181
  }
182
+ else if (request.op === "read") {
183
+ response = { ok: true, ...(await session.read(request.path)) };
184
+ }
168
185
  else if (request.op === "storage") {
169
186
  response = { ok: true, ...(await session.inspectStorage()) };
170
187
  }
@@ -213,6 +230,9 @@ export async function serveSession(session, projectDir, sessionId, headless = tr
213
230
  response = { ok: false, error: err instanceof Error ? err.message : String(err) };
214
231
  }
215
232
  sock.write(JSON.stringify(response) + "\n");
233
+ // Whatever changed (a step ran, a reload, a raw action): subscribers get the new snapshot.
234
+ if (response.ok && "snapshot" in response && "sections" in response.snapshot)
235
+ feed.send({ type: "snapshot", snapshot: response.snapshot });
216
236
  if (shouldQuit) {
217
237
  setImmediate(() => void shutdown());
218
238
  }
@@ -224,9 +244,10 @@ export async function serveSession(session, projectDir, sessionId, headless = tr
224
244
  return;
225
245
  shuttingDown = true;
226
246
  server.close();
247
+ feed.close();
227
248
  await session.close().catch(ignorePageGone);
228
249
  removeSessionFiles(projectDir, sessionId);
229
- process.exit(0);
250
+ onShutdown();
230
251
  }
231
252
  await new Promise((resolvePromise, reject) => {
232
253
  server.on("error", reject);
@@ -248,7 +269,7 @@ export async function serveSession(session, projectDir, sessionId, headless = tr
248
269
  /** Entry point for the hidden `__auto-serve` CLI command (runs in the detached child). */
249
270
  export async function runAutoServeCommand(init, sessionId) {
250
271
  const session = await AutoSession.start({ ...init, sessionId });
251
- await serveSession(session, init.projectDir, sessionId, init.headless ?? true);
272
+ await serveSession(session, init.projectDir, sessionId, init.headless ?? true, () => process.exit(0));
252
273
  // Tell the parent that spawned us (spawnDetachedSession) we are serving, then let it go.
253
274
  process.send?.({ ready: true });
254
275
  process.disconnect?.();
@@ -372,3 +393,12 @@ export async function runAttachLoop(projectDir, sessionId) {
372
393
  rl.close();
373
394
  }
374
395
  }
396
+ /** This package's version, for `hello` (the file sits three levels below the package root in src/ and dist/). */
397
+ function waygraphVersion() {
398
+ try {
399
+ return JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "package.json"), "utf8")).version;
400
+ }
401
+ catch {
402
+ return "unknown";
403
+ }
404
+ }
@@ -0,0 +1,127 @@
1
+ import type { PilotHighlightFixtures } from "../../demo/overlay/pilot/fixtures.js";
2
+ import type { RunEvent } from "../../engine/run/events.js";
3
+ import type { ApplyPathResult, ConsoleLogEntry, DomSnapshot, InspectDomOptions, SessionSnapshot, StorageSnapshot, StubFileKind, TraceStep } from "./helpers.js";
4
+ import type { SessionRead } from "./read.js";
5
+ /** Bumped when a request or response changes shape (adding an op or an optional field does not). */
6
+ export declare const SESSION_PROTOCOL_VERSION = 1;
7
+ /** Every request a session answers. */
8
+ export type SessionRequest = {
9
+ op: "hello";
10
+ } | {
11
+ op: "subscribe";
12
+ } | {
13
+ op: "status";
14
+ } | {
15
+ op: "send";
16
+ pick: string;
17
+ } | ({
18
+ op: "dom";
19
+ } & InspectDomOptions) | {
20
+ op: "trace";
21
+ } | {
22
+ op: "click";
23
+ selector: string;
24
+ } | {
25
+ op: "type";
26
+ selector: string;
27
+ text: string;
28
+ } | {
29
+ op: "press";
30
+ selector: string;
31
+ key: string;
32
+ } | {
33
+ op: "goto";
34
+ url: string;
35
+ } | {
36
+ op: "reload";
37
+ } | {
38
+ op: "reach";
39
+ checkpoint: string;
40
+ } | {
41
+ op: "resync";
42
+ } | {
43
+ op: "console";
44
+ } | {
45
+ op: "storage";
46
+ } | {
47
+ op: "read";
48
+ path?: string;
49
+ } | {
50
+ op: "upload";
51
+ selector: string;
52
+ stub: StubFileKind | {
53
+ filePath: string;
54
+ };
55
+ } | ({
56
+ op: "highlight";
57
+ } & PilotHighlightFixtures);
58
+ export type StatusOrSendResponse = {
59
+ ok: true;
60
+ snapshot: SessionSnapshot;
61
+ quit: boolean;
62
+ } | {
63
+ ok: false;
64
+ error: string;
65
+ };
66
+ export type DomResponse = {
67
+ ok: true;
68
+ snapshot: DomSnapshot;
69
+ } | {
70
+ ok: false;
71
+ error: string;
72
+ };
73
+ export type TraceResponse = {
74
+ ok: true;
75
+ trace: TraceStep[];
76
+ } | {
77
+ ok: false;
78
+ error: string;
79
+ };
80
+ export type ConsoleResponse = {
81
+ ok: true;
82
+ consoleLog: ConsoleLogEntry[];
83
+ } | {
84
+ ok: false;
85
+ error: string;
86
+ };
87
+ export type StorageResponse = ({
88
+ ok: true;
89
+ } & StorageSnapshot) | {
90
+ ok: false;
91
+ error: string;
92
+ };
93
+ export type ReachResponse = ApplyPathResult;
94
+ export type ReadResponse = ({
95
+ ok: true;
96
+ } & SessionRead) | {
97
+ ok: false;
98
+ error: string;
99
+ };
100
+ export type HighlightResponse = {
101
+ ok: true;
102
+ painted: number;
103
+ missing: string[];
104
+ snapshot: SessionSnapshot;
105
+ } | {
106
+ ok: false;
107
+ error: string;
108
+ };
109
+ /** What `hello` answers: the protocol a client is talking to, and where the session is (`null` until a status or step has located it). */
110
+ export type HelloResponse = {
111
+ ok: true;
112
+ protocol: number;
113
+ waygraph: string;
114
+ sessionId: string;
115
+ here: string | null;
116
+ };
117
+ /** One line of a `subscribe` feed: every run event in the session (steps, reads), and the snapshot after each change. */
118
+ export type SessionStreamMessage = {
119
+ type: "event";
120
+ event: RunEvent;
121
+ } | {
122
+ type: "snapshot";
123
+ snapshot: SessionSnapshot;
124
+ };
125
+ export type SessionResponse = HelloResponse | StatusOrSendResponse | DomResponse | TraceResponse | ConsoleResponse | StorageResponse | ReachResponse | ReadResponse | HighlightResponse;
126
+ /** Every op, once: the server, the CLI and `pilot` all check against this list. */
127
+ export declare const SESSION_OPS: readonly ["hello", "subscribe", "status", "send", "dom", "trace", "console", "storage", "read", "upload", "click", "type", "press", "goto", "reload", "reach", "resync", "highlight"];