libretto 0.6.11 → 0.6.12

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 (119) hide show
  1. package/README.md +4 -0
  2. package/README.template.md +4 -0
  3. package/dist/cli/cli.js +4 -3
  4. package/dist/cli/commands/ai.js +3 -2
  5. package/dist/cli/commands/browser.js +17 -17
  6. package/dist/cli/commands/execution.js +254 -234
  7. package/dist/cli/commands/experiments.js +100 -0
  8. package/dist/cli/commands/setup.js +20 -34
  9. package/dist/cli/commands/shared.js +10 -0
  10. package/dist/cli/commands/snapshot.js +81 -9
  11. package/dist/cli/commands/status.js +5 -4
  12. package/dist/cli/core/ai-model.js +6 -3
  13. package/dist/cli/core/browser.js +300 -121
  14. package/dist/cli/core/config.js +4 -2
  15. package/dist/cli/core/context.js +4 -0
  16. package/dist/cli/core/daemon/config.js +0 -6
  17. package/dist/cli/core/daemon/daemon.js +535 -89
  18. package/dist/cli/core/daemon/ipc.js +170 -129
  19. package/dist/cli/core/daemon/snapshot.js +72 -6
  20. package/dist/cli/core/experiments.js +66 -0
  21. package/dist/cli/core/session.js +5 -4
  22. package/dist/cli/core/skill-version.js +2 -1
  23. package/dist/cli/core/snapshot-analyzer.js +4 -3
  24. package/dist/cli/core/workflow-runner/runner.js +147 -0
  25. package/dist/cli/core/workflow-runtime.js +60 -0
  26. package/dist/cli/router.js +4 -1
  27. package/dist/shared/debug/pause-handler.d.ts +9 -0
  28. package/dist/shared/debug/pause-handler.js +15 -0
  29. package/dist/shared/debug/pause.d.ts +1 -2
  30. package/dist/shared/debug/pause.js +13 -36
  31. package/dist/shared/ipc/child-process-transport.d.ts +7 -0
  32. package/dist/shared/ipc/child-process-transport.js +60 -0
  33. package/dist/shared/ipc/child-process-transport.spec.d.ts +2 -0
  34. package/dist/shared/ipc/child-process-transport.spec.js +68 -0
  35. package/dist/shared/ipc/ipc.d.ts +46 -0
  36. package/dist/shared/ipc/ipc.js +165 -0
  37. package/dist/shared/ipc/ipc.spec.d.ts +2 -0
  38. package/dist/shared/ipc/ipc.spec.js +114 -0
  39. package/dist/shared/ipc/socket-transport.d.ts +9 -0
  40. package/dist/shared/ipc/socket-transport.js +143 -0
  41. package/dist/shared/ipc/socket-transport.spec.d.ts +2 -0
  42. package/dist/shared/ipc/socket-transport.spec.js +117 -0
  43. package/dist/shared/package-manager.d.ts +7 -0
  44. package/dist/shared/package-manager.js +60 -0
  45. package/dist/shared/paths/paths.d.ts +1 -8
  46. package/dist/shared/paths/paths.js +1 -49
  47. package/dist/shared/snapshot/capture-snapshot.d.ts +9 -0
  48. package/dist/shared/snapshot/capture-snapshot.js +463 -0
  49. package/dist/shared/snapshot/diff-snapshots.d.ts +72 -0
  50. package/dist/shared/snapshot/diff-snapshots.js +358 -0
  51. package/dist/shared/snapshot/render-snapshot.d.ts +39 -0
  52. package/dist/shared/snapshot/render-snapshot.js +651 -0
  53. package/dist/shared/snapshot/snapshot.spec.d.ts +2 -0
  54. package/dist/shared/snapshot/snapshot.spec.js +333 -0
  55. package/dist/shared/snapshot/types.d.ts +40 -0
  56. package/dist/shared/snapshot/types.js +0 -0
  57. package/dist/shared/snapshot/wait-for-page-stable.d.ts +17 -0
  58. package/dist/shared/snapshot/wait-for-page-stable.js +281 -0
  59. package/dist/shared/state/session-state.d.ts +1 -0
  60. package/dist/shared/state/session-state.js +1 -0
  61. package/docs/experiments.md +67 -0
  62. package/package.json +4 -2
  63. package/skills/libretto/SKILL.md +3 -1
  64. package/skills/libretto-readonly/SKILL.md +1 -1
  65. package/src/cli/AGENTS.md +7 -0
  66. package/src/cli/cli.ts +4 -3
  67. package/src/cli/commands/ai.ts +3 -2
  68. package/src/cli/commands/browser.ts +13 -11
  69. package/src/cli/commands/execution.ts +303 -271
  70. package/src/cli/commands/experiments.ts +120 -0
  71. package/src/cli/commands/setup.ts +18 -36
  72. package/src/cli/commands/shared.ts +20 -0
  73. package/src/cli/commands/snapshot.ts +99 -11
  74. package/src/cli/commands/status.ts +5 -4
  75. package/src/cli/core/ai-model.ts +6 -3
  76. package/src/cli/core/browser.ts +369 -147
  77. package/src/cli/core/config.ts +3 -1
  78. package/src/cli/core/context.ts +4 -0
  79. package/src/cli/core/daemon/config.ts +35 -19
  80. package/src/cli/core/daemon/daemon.ts +686 -106
  81. package/src/cli/core/daemon/ipc.ts +330 -214
  82. package/src/cli/core/daemon/snapshot.ts +106 -8
  83. package/src/cli/core/experiments.ts +85 -0
  84. package/src/cli/core/session.ts +5 -4
  85. package/src/cli/core/skill-version.ts +2 -1
  86. package/src/cli/core/snapshot-analyzer.ts +4 -3
  87. package/src/cli/core/workflow-runner/runner.ts +237 -0
  88. package/src/cli/core/workflow-runtime.ts +85 -0
  89. package/src/cli/router.ts +4 -1
  90. package/src/shared/debug/pause-handler.ts +20 -0
  91. package/src/shared/debug/pause.ts +14 -48
  92. package/src/shared/ipc/AGENTS.md +24 -0
  93. package/src/shared/ipc/child-process-transport.spec.ts +86 -0
  94. package/src/shared/ipc/child-process-transport.ts +96 -0
  95. package/src/shared/ipc/ipc.spec.ts +161 -0
  96. package/src/shared/ipc/ipc.ts +288 -0
  97. package/src/shared/ipc/socket-transport.spec.ts +141 -0
  98. package/src/shared/ipc/socket-transport.ts +189 -0
  99. package/src/shared/package-manager.ts +76 -0
  100. package/src/shared/paths/paths.ts +0 -72
  101. package/src/shared/snapshot/capture-snapshot.ts +615 -0
  102. package/src/shared/snapshot/diff-snapshots.ts +579 -0
  103. package/src/shared/snapshot/render-snapshot.ts +962 -0
  104. package/src/shared/snapshot/snapshot.spec.ts +388 -0
  105. package/src/shared/snapshot/types.ts +43 -0
  106. package/src/shared/snapshot/wait-for-page-stable.ts +425 -0
  107. package/src/shared/state/session-state.ts +1 -0
  108. package/dist/cli/core/daemon/index.js +0 -16
  109. package/dist/cli/core/daemon/spawn.js +0 -90
  110. package/dist/cli/core/pause-signals.js +0 -29
  111. package/dist/cli/workers/run-integration-runtime.js +0 -235
  112. package/dist/cli/workers/run-integration-worker-protocol.js +0 -17
  113. package/dist/cli/workers/run-integration-worker.js +0 -64
  114. package/src/cli/core/daemon/index.ts +0 -24
  115. package/src/cli/core/daemon/spawn.ts +0 -171
  116. package/src/cli/core/pause-signals.ts +0 -35
  117. package/src/cli/workers/run-integration-runtime.ts +0 -326
  118. package/src/cli/workers/run-integration-worker-protocol.ts +0 -19
  119. package/src/cli/workers/run-integration-worker.ts +0 -72
@@ -0,0 +1,388 @@
1
+ import { chromium, type Page } from "playwright";
2
+ import outdent from "outdent";
3
+ import { describe, expect, test as base } from "vitest";
4
+ import { snapshot } from "./capture-snapshot.js";
5
+ import {
6
+ diffSnapshots,
7
+ renderSnapshotDiff,
8
+ } from "./diff-snapshots.js";
9
+ import { renderSnapshot } from "./render-snapshot.js";
10
+ import type {
11
+ Snapshot,
12
+ SnapshotNode,
13
+ SnapshotPrimitive,
14
+ } from "./types.js";
15
+
16
+ type SnapshotRenderFixtures = {
17
+ page: Page;
18
+ expectSnapshot: (html: string, expected: string) => Promise<void>;
19
+ expectScopedSnapshot: (
20
+ html: string,
21
+ refId: string,
22
+ expected: string,
23
+ ) => Promise<void>;
24
+ expectSnapshotDiff: (
25
+ beforeHtml: string,
26
+ afterHtml: string,
27
+ expected: string,
28
+ ) => Promise<void>;
29
+ };
30
+
31
+ const test = base.extend<SnapshotRenderFixtures>({
32
+ page: async ({}, use) => {
33
+ const browser = await chromium.launch({ headless: true });
34
+ const page = await browser.newPage();
35
+ await use(page);
36
+ await page.close();
37
+ await browser.close();
38
+ },
39
+
40
+ expectSnapshot: async ({ page }, use) => {
41
+ await use(async (html: string, expected: string) => {
42
+ await page.setContent(outdent.string(html));
43
+ const raw = await snapshot(page);
44
+ expect(normalizeIncidentalUrls(renderSnapshot(raw))).toBe(
45
+ outdent.string(expected),
46
+ );
47
+ });
48
+ },
49
+
50
+ expectScopedSnapshot: async ({ page }, use) => {
51
+ await use(async (html: string, refId: string, expected: string) => {
52
+ await page.setContent(outdent.string(html));
53
+ const raw = await snapshot(page);
54
+ expect(normalizeIncidentalUrls(renderSnapshot(raw, refId))).toBe(
55
+ outdent.string(expected),
56
+ );
57
+ });
58
+ },
59
+
60
+ expectSnapshotDiff: async ({ page }, use) => {
61
+ await use(async (beforeHtml: string, afterHtml: string, expected: string) => {
62
+ await page.setContent(outdent.string(beforeHtml));
63
+ const before = await snapshot(page);
64
+ await page.setContent(outdent.string(afterHtml));
65
+ const after = await snapshot(page);
66
+ expect(
67
+ normalizeIncidentalUrls(
68
+ renderSnapshotDiff(diffSnapshots(before, after)),
69
+ ),
70
+ ).toBe(outdent.string(expected));
71
+ });
72
+ },
73
+ });
74
+
75
+ function normalizeIncidentalUrls(rendered: string): string {
76
+ return rendered.replaceAll(/url="[^"]*"/g, `url="<page-url>"`);
77
+ }
78
+
79
+ type SnapshotNodeInput = {
80
+ nodeId: string;
81
+ role: string;
82
+ name?: string | null;
83
+ ref?: string | null;
84
+ value?: SnapshotPrimitive;
85
+ description?: string | null;
86
+ properties?: Record<string, SnapshotPrimitive>;
87
+ attributes?: Record<string, string>;
88
+ children?: SnapshotNode[];
89
+ ignored?: boolean;
90
+ };
91
+
92
+ function makeNode(input: SnapshotNodeInput): SnapshotNode {
93
+ const node: SnapshotNode = {
94
+ nodeId: input.nodeId,
95
+ ignored: input.ignored ?? false,
96
+ role: input.role,
97
+ name: input.name ?? null,
98
+ value: input.value ?? null,
99
+ description: input.description ?? null,
100
+ properties: input.properties ?? {},
101
+ attributes: input.attributes ?? {},
102
+ children: input.children ?? [],
103
+ ref: input.ref ?? null,
104
+ subtreeSize: 1,
105
+ };
106
+ node.subtreeSize = countSubtree(node);
107
+ return node;
108
+ }
109
+
110
+ function countSubtree(node: SnapshotNode): number {
111
+ return 1 + node.children.reduce((sum, child) => sum + countSubtree(child), 0);
112
+ }
113
+
114
+ function makeSnapshot(
115
+ roots: SnapshotNode[],
116
+ options: { title?: string; url?: string } = {},
117
+ ): Snapshot {
118
+ const url = options.url ?? "about:blank";
119
+ return {
120
+ title: options.title ?? "Demo Page",
121
+ url,
122
+ frames: [
123
+ {
124
+ status: "ok",
125
+ id: "main",
126
+ index: 0,
127
+ url,
128
+ name: null,
129
+ parentId: null,
130
+ roots,
131
+ },
132
+ ],
133
+ };
134
+ }
135
+
136
+ describe("renderSnapshot", () => {
137
+ test("renders page, frame, semantic roles, heading text, refs, and no command hint", async ({
138
+ expectSnapshot,
139
+ }) => {
140
+ await expectSnapshot(
141
+ `
142
+ <!doctype html>
143
+ <html>
144
+ <head><title>Product Docs</title></head>
145
+ <body>
146
+ <header>
147
+ <nav aria-label="Primary">
148
+ <a href="/docs">Docs</a>
149
+ </nav>
150
+ </header>
151
+ <main>
152
+ <h1>Welcome</h1>
153
+ <button>Save</button>
154
+ <input placeholder="Search docs" value="query" />
155
+ </main>
156
+ </body>
157
+ </html>
158
+ `,
159
+ `
160
+ <page title="Product Docs" url="<page-url>">
161
+ <frame index="0" url="<page-url>">
162
+ <document ref="l1">
163
+ Product Docs
164
+ <banner ref="l2">
165
+ ...
166
+ Primary
167
+ <link ref="l4" href="/docs">Docs</link>
168
+ </banner>
169
+ <main ref="l5">
170
+ # Welcome
171
+ <button ref="l7">Save</button>
172
+ <textbox ref="l8" value="query" placeholder="Search docs">
173
+ Search docs
174
+ query
175
+ </textbox>
176
+ </main>
177
+ </document>
178
+ </frame>
179
+ </page>
180
+ `,
181
+ );
182
+ });
183
+
184
+ test("scopes an already-captured tree by ref with numeric-suffix fallback", async ({
185
+ expectScopedSnapshot,
186
+ }) => {
187
+ await expectScopedSnapshot(
188
+ `
189
+ <!doctype html>
190
+ <title>Scoped Snapshot</title>
191
+ <main>
192
+ <button>Sibling</button>
193
+ <button>Target</button>
194
+ </main>
195
+ `,
196
+ "e4",
197
+ `
198
+ <page title="Scoped Snapshot" url="<page-url>">
199
+ <frame index="0" url="<page-url>">
200
+ <button ref="l4">Target</button>
201
+ </frame>
202
+ </page>
203
+ `,
204
+ );
205
+ });
206
+
207
+ test("compacts low-value wrappers, clickable generics, single-child chains, and long child lists", async ({
208
+ expectSnapshot,
209
+ }) => {
210
+ await expectSnapshot(
211
+ `
212
+ <!doctype html>
213
+ <title>Compact Demo</title>
214
+ <style>.card { cursor: pointer; }</style>
215
+ <main>
216
+ <div><section><p>Flattened wrapper text</p></section></div>
217
+ <div class="card" onclick="void 0">Open card</div>
218
+ <main aria-label="Outer">
219
+ <nav aria-label="Navigation">
220
+ <form aria-label="Lookup"><button>Submit chain</button></form>
221
+ </nav>
222
+ </main>
223
+ <ul>
224
+ <li><button>One</button></li>
225
+ <li><button>Two</button></li>
226
+ <li><button>Three</button></li>
227
+ <li><button>Four</button></li>
228
+ <li><button>Five</button></li>
229
+ <li><button>Six</button></li>
230
+ </ul>
231
+ </main>
232
+ `,
233
+ `
234
+ <page title="Compact Demo" url="<page-url>">
235
+ <frame index="0" url="<page-url>">
236
+ <document ref="l1">
237
+ Compact Demo
238
+ <main ref="l2">
239
+ Flattened wrapper text
240
+ <button ref="l3">Open card</button>
241
+ <main ref="l4">
242
+ Outer
243
+ ...
244
+ Lookup
245
+ <button ref="l7">Submit chain</button>
246
+ </main>
247
+ <list>
248
+ <listitem>
249
+ <button ref="l9">One</button>
250
+ </listitem>
251
+ <listitem>
252
+ <button ref="l11">Two</button>
253
+ </listitem>
254
+ <listitem>
255
+ <button ref="l13">Three</button>
256
+ </listitem>
257
+ <listitem>
258
+ <button ref="l15">Four</button>
259
+ </listitem>
260
+ [Truncated 2 more elements. Interactive elements: <button ref="l17">Five</button>, <button ref="l19">Six</button>]
261
+ </list>
262
+ </main>
263
+ </document>
264
+ </frame>
265
+ </page>
266
+ `,
267
+ );
268
+ });
269
+ });
270
+
271
+ describe("diffSnapshots", () => {
272
+ test("returns no rendered diff for unchanged browser-rendered snapshots", async ({
273
+ expectSnapshotDiff,
274
+ }) => {
275
+ const html = `
276
+ <!-- BEFORE -->
277
+ <!doctype html>
278
+ <title>Stable Demo</title>
279
+ <main><button>Stable</button></main>
280
+ `;
281
+
282
+ // AFTER: unchanged from BEFORE.
283
+ await expectSnapshotDiff(html, html, "");
284
+ });
285
+
286
+ test("tracks added, removed, and modified nodes under context ancestors", async ({
287
+ expectSnapshotDiff,
288
+ }) => {
289
+ await expectSnapshotDiff(
290
+ `
291
+ <!-- BEFORE -->
292
+ <!doctype html>
293
+ <title>Diff Demo</title>
294
+ <main>
295
+ <h1>Tasks</h1>
296
+ <button id="stable">Stable</button>
297
+ <button id="save">Save</button>
298
+ <button id="delete">Delete</button>
299
+ </main>
300
+ `,
301
+ `
302
+ <!-- AFTER -->
303
+ <!doctype html>
304
+ <title>Diff Demo</title>
305
+ <main>
306
+ <h1>Tasks</h1>
307
+ <button id="stable">Stable</button>
308
+ <button id="save">Saved</button>
309
+ <a id="docs" href="https://example.test/docs">Docs</a>
310
+ </main>
311
+ `,
312
+ `
313
+ <page title="Diff Demo" url="<page-url>">
314
+ <frame index="0" url="<page-url>">
315
+ <document ref="l1">
316
+ ...
317
+ <main ref="l2">
318
+ ...
319
+ ~ <button ref="l5">Saved</button>
320
+ + <link ref="l6" href="https://example.test/docs">Docs</link>
321
+ - <button ref="l6">...</button>
322
+ </main>
323
+ </document>
324
+ </frame>
325
+ </page>
326
+ `,
327
+ );
328
+ });
329
+
330
+ test("suppresses href query and hash changes from browser-rendered links", async ({
331
+ expectSnapshotDiff,
332
+ }) => {
333
+ await expectSnapshotDiff(
334
+ `
335
+ <!-- BEFORE -->
336
+ <!doctype html>
337
+ <title>Href Demo</title>
338
+ <main><a id="docs" href="https://example.test/docs?utm=old#intro">Docs</a></main>
339
+ `,
340
+ `
341
+ <!-- AFTER -->
342
+ <!doctype html>
343
+ <title>Href Demo</title>
344
+ <main><a id="docs" href="https://example.test/docs?utm=new#api">Docs</a></main>
345
+ `,
346
+ "",
347
+ );
348
+ });
349
+
350
+ test("suppresses ref-only changes that browser HTML cannot express directly", () => {
351
+ const before = makeSnapshot([
352
+ makeNode({
353
+ nodeId: "root",
354
+ role: "RootWebArea",
355
+ ref: "l1",
356
+ children: [
357
+ makeNode({
358
+ nodeId: "docs",
359
+ role: "link",
360
+ name: "Docs",
361
+ ref: "l2",
362
+ attributes: { href: "https://example.test/docs" },
363
+ }),
364
+ ],
365
+ }),
366
+ ]);
367
+ const after = makeSnapshot([
368
+ makeNode({
369
+ nodeId: "root",
370
+ role: "RootWebArea",
371
+ ref: "l10",
372
+ children: [
373
+ makeNode({
374
+ nodeId: "docs",
375
+ role: "link",
376
+ name: "Docs",
377
+ ref: "l20",
378
+ attributes: { href: "https://example.test/docs" },
379
+ }),
380
+ ],
381
+ }),
382
+ ]);
383
+
384
+ const diff = diffSnapshots(before, after);
385
+
386
+ expect(renderSnapshotDiff(diff)).toBe("");
387
+ });
388
+ });
@@ -0,0 +1,43 @@
1
+ export type SnapshotPrimitive = string | number | boolean | null;
2
+
3
+ export type Snapshot = {
4
+ title: string;
5
+ url: string;
6
+ frames: SnapshotFrame[];
7
+ };
8
+
9
+ export type SnapshotFrame = SnapshotAvailableFrame | SnapshotUnavailableFrame;
10
+
11
+ export type SnapshotAvailableFrame = {
12
+ status: "ok";
13
+ id: string;
14
+ index: number;
15
+ url: string;
16
+ name: string | null;
17
+ parentId: string | null;
18
+ roots: SnapshotNode[];
19
+ };
20
+
21
+ export type SnapshotUnavailableFrame = {
22
+ status: "unavailable";
23
+ id: string;
24
+ index: number;
25
+ url: string;
26
+ name: string | null;
27
+ parentId: string | null;
28
+ error: string;
29
+ };
30
+
31
+ export type SnapshotNode = {
32
+ nodeId: string;
33
+ ignored: boolean;
34
+ role: string;
35
+ name: string | null;
36
+ value: SnapshotPrimitive;
37
+ description: string | null;
38
+ properties: Record<string, SnapshotPrimitive>;
39
+ attributes: Record<string, string>;
40
+ children: SnapshotNode[];
41
+ ref: string | null;
42
+ subtreeSize: number;
43
+ };