libretto 0.6.12 → 0.6.14

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 (82) hide show
  1. package/README.md +3 -8
  2. package/README.template.md +3 -8
  3. package/dist/cli/cli.js +0 -23
  4. package/dist/cli/commands/auth.js +24 -33
  5. package/dist/cli/commands/billing.js +3 -5
  6. package/dist/cli/commands/browser.js +4 -13
  7. package/dist/cli/commands/deploy.js +54 -45
  8. package/dist/cli/commands/execution.js +6 -3
  9. package/dist/cli/commands/experiments.js +1 -1
  10. package/dist/cli/commands/setup.js +2 -295
  11. package/dist/cli/commands/shared.js +1 -1
  12. package/dist/cli/commands/snapshot.js +10 -100
  13. package/dist/cli/commands/status.js +2 -42
  14. package/dist/cli/core/auth-fetch.js +11 -6
  15. package/dist/cli/core/browser.js +13 -8
  16. package/dist/cli/core/config.js +3 -6
  17. package/dist/cli/core/daemon/daemon.js +88 -74
  18. package/dist/cli/core/daemon/exec-repl.js +133 -0
  19. package/dist/cli/core/daemon/exec.js +6 -21
  20. package/dist/cli/core/daemon/ipc.js +47 -4
  21. package/dist/cli/core/daemon/ipc.spec.js +21 -0
  22. package/dist/cli/core/daemon/snapshot.js +2 -29
  23. package/dist/cli/core/exec-compiler.js +8 -3
  24. package/dist/cli/core/experiments.js +1 -28
  25. package/dist/cli/core/providers/index.js +13 -4
  26. package/dist/cli/core/providers/libretto-cloud.js +178 -26
  27. package/dist/cli/index.js +0 -2
  28. package/dist/cli/router.js +9 -6
  29. package/dist/shared/instrumentation/instrument.js +4 -4
  30. package/dist/shared/ipc/socket-transport.d.ts +2 -1
  31. package/dist/shared/ipc/socket-transport.js +16 -5
  32. package/dist/shared/ipc/socket-transport.spec.js +5 -0
  33. package/docs/releasing.md +8 -6
  34. package/package.json +3 -2
  35. package/skills/libretto/SKILL.md +49 -47
  36. package/skills/libretto/references/code-generation-rules.md +6 -0
  37. package/skills/libretto/references/configuration-file-reference.md +14 -12
  38. package/skills/libretto/references/pages-and-page-targeting.md +1 -1
  39. package/skills/libretto/references/site-security-review.md +6 -6
  40. package/skills/libretto-readonly/SKILL.md +2 -9
  41. package/src/cli/cli.ts +0 -24
  42. package/src/cli/commands/auth.ts +24 -33
  43. package/src/cli/commands/billing.ts +3 -5
  44. package/src/cli/commands/browser.ts +6 -16
  45. package/src/cli/commands/deploy.ts +55 -49
  46. package/src/cli/commands/execution.ts +6 -3
  47. package/src/cli/commands/experiments.ts +1 -1
  48. package/src/cli/commands/setup.ts +2 -381
  49. package/src/cli/commands/shared.ts +1 -1
  50. package/src/cli/commands/snapshot.ts +9 -137
  51. package/src/cli/commands/status.ts +2 -50
  52. package/src/cli/core/auth-fetch.ts +9 -4
  53. package/src/cli/core/browser.ts +15 -8
  54. package/src/cli/core/config.ts +3 -6
  55. package/src/cli/core/daemon/daemon.ts +106 -76
  56. package/src/cli/core/daemon/exec-repl.ts +189 -0
  57. package/src/cli/core/daemon/exec.ts +8 -43
  58. package/src/cli/core/daemon/ipc.spec.ts +27 -0
  59. package/src/cli/core/daemon/ipc.ts +81 -23
  60. package/src/cli/core/daemon/snapshot.ts +1 -43
  61. package/src/cli/core/exec-compiler.ts +8 -3
  62. package/src/cli/core/experiments.ts +9 -38
  63. package/src/cli/core/providers/index.ts +17 -4
  64. package/src/cli/core/providers/libretto-cloud.ts +224 -36
  65. package/src/cli/core/resolve-model.ts +5 -0
  66. package/src/cli/core/workflow-runtime.ts +1 -0
  67. package/src/cli/index.ts +0 -1
  68. package/src/cli/router.ts +9 -6
  69. package/src/shared/instrumentation/instrument.ts +4 -4
  70. package/src/shared/ipc/socket-transport.spec.ts +6 -0
  71. package/src/shared/ipc/socket-transport.ts +20 -5
  72. package/dist/cli/commands/ai.js +0 -110
  73. package/dist/cli/core/ai-model.js +0 -195
  74. package/dist/cli/core/api-snapshot-analyzer.js +0 -86
  75. package/dist/cli/core/snapshot-analyzer.js +0 -667
  76. package/dist/cli/framework/simple-cli.js +0 -880
  77. package/scripts/summarize-evals.mjs +0 -135
  78. package/src/cli/commands/ai.ts +0 -144
  79. package/src/cli/core/ai-model.ts +0 -301
  80. package/src/cli/core/api-snapshot-analyzer.ts +0 -110
  81. package/src/cli/core/snapshot-analyzer.ts +0 -856
  82. package/src/cli/framework/simple-cli.ts +0 -1459
@@ -2,34 +2,7 @@ import {
2
2
  readLibrettoConfig,
3
3
  writeLibrettoConfig
4
4
  } from "./config.js";
5
- const EXPERIMENTS = {
6
- "compact-snapshot-format": {
7
- title: "Compact snapshot format",
8
- oneSentenceDescription: "Use compact accessibility snapshots and exec page-change diffs without an AI sub-agent.",
9
- docs: [
10
- "Compact snapshot format changes how agents should use snapshot and exec after the experiment is enabled.",
11
- "",
12
- "Compared with the skill's documented behavior:",
13
- " - Run libretto snapshot --session <name> without --objective or --context.",
14
- " - Snapshot output is a screenshot path plus a compact accessibility tree; it does not use the PNG + HTML + AI analysis path.",
15
- " - Run libretto snapshot <ref> --session <name> to inspect a subtree from the latest full compact snapshot.",
16
- " - Run libretto exec normally; after successful mutations, Libretto prints page-change diffs from compact snapshots without AI analysis.",
17
- " - If a session was already open before enabling the experiment, close and reopen it before relying on this behavior.",
18
- "",
19
- "Full compact snapshot:",
20
- " libretto snapshot --session <name>",
21
- "",
22
- "Cached subtree snapshot:",
23
- " libretto snapshot <ref> --session <name>",
24
- "",
25
- "Run an unscoped snapshot before using refs. Subtree snapshots capture a fresh screenshot but reuse the latest cached tree.",
26
- "",
27
- "Notes:",
28
- " - Use ref forms printed in the tree, such as l16. Numeric-suffix aliases such as e16 also match l16."
29
- ].join("\n"),
30
- defaultValue: false
31
- }
32
- };
5
+ const EXPERIMENTS = {};
33
6
  function isExperimentName(name) {
34
7
  return Object.hasOwn(EXPERIMENTS, name);
35
8
  }
@@ -2,7 +2,14 @@ import { readLibrettoConfig } from "../config.js";
2
2
  import { createBrowserbaseProvider } from "./browserbase.js";
3
3
  import { createKernelProvider } from "./kernel.js";
4
4
  import { createLibrettoCloudProvider } from "./libretto-cloud.js";
5
- const VALID_PROVIDERS = /* @__PURE__ */ new Set(["local", "kernel", "browserbase", "libretto-cloud"]);
5
+ const VALID_PROVIDERS = /* @__PURE__ */ new Set([
6
+ "local",
7
+ "kernel",
8
+ "browserbase",
9
+ "libretto-cloud"
10
+ ]);
11
+ const DEFAULT_PROVIDER_STARTUP_TIMEOUT_MS = 6e4;
12
+ const LIBRETTO_CLOUD_STARTUP_TIMEOUT_MS = 10 * 6e4;
6
13
  function assertValidProviderName(value, source) {
7
14
  if (!VALID_PROVIDERS.has(value)) {
8
15
  throw new Error(
@@ -32,9 +39,7 @@ function getCloudProviderApi(name) {
32
39
  case "browserbase":
33
40
  return createBrowserbaseProvider();
34
41
  case "libretto-cloud":
35
- console.warn(
36
- "Note: The libretto-cloud provider is in alpha."
37
- );
42
+ console.warn("Note: The libretto-cloud provider is in alpha.");
38
43
  return createLibrettoCloudProvider();
39
44
  default:
40
45
  throw new Error(
@@ -42,7 +47,11 @@ function getCloudProviderApi(name) {
42
47
  );
43
48
  }
44
49
  }
50
+ function getProviderStartupTimeoutMs(providerName) {
51
+ return providerName === "libretto-cloud" ? LIBRETTO_CLOUD_STARTUP_TIMEOUT_MS : DEFAULT_PROVIDER_STARTUP_TIMEOUT_MS;
52
+ }
45
53
  export {
46
54
  getCloudProviderApi,
55
+ getProviderStartupTimeoutMs,
47
56
  resolveProviderName
48
57
  };
@@ -1,15 +1,19 @@
1
- import { HOSTED_API_URL } from "../auth-fetch.js";
1
+ import { resolveHostedApiUrl } from "../auth-fetch.js";
2
+ const DEFAULT_POLL_INTERVAL_MS = 2e3;
3
+ const DEFAULT_BROWSER_SESSION_TIMEOUT_SECONDS = 3600;
4
+ const QUEUE_WAIT_TIMEOUT_MS = 10 * 6e4;
2
5
  function createLibrettoCloudProvider() {
3
6
  const apiKey = process.env.LIBRETTO_API_KEY;
4
7
  if (!apiKey)
5
8
  throw new Error(
6
9
  "LIBRETTO_API_KEY is required for the Libretto Cloud provider."
7
10
  );
8
- const endpoint = HOSTED_API_URL;
11
+ const endpoint = resolveHostedApiUrl();
9
12
  return {
10
13
  async createSession() {
11
- const timeoutSeconds = Number(
12
- process.env.LIBRETTO_TIMEOUT_SECONDS ?? 7200
14
+ const browserSessionTimeoutSeconds = readPositiveNumberEnv(
15
+ "LIBRETTO_TIMEOUT_SECONDS",
16
+ DEFAULT_BROWSER_SESSION_TIMEOUT_SECONDS
13
17
  );
14
18
  const resp = await fetch(`${endpoint}/v1/sessions/create`, {
15
19
  method: "POST",
@@ -18,42 +22,190 @@ function createLibrettoCloudProvider() {
18
22
  "Content-Type": "application/json"
19
23
  },
20
24
  body: JSON.stringify({
21
- json: { timeout_seconds: timeoutSeconds }
25
+ json: { timeout_seconds: browserSessionTimeoutSeconds }
22
26
  })
23
27
  });
24
28
  if (!resp.ok) {
25
29
  const body = await resp.text();
26
- throw new Error(
27
- `Libretto Cloud API error (${resp.status}): ${body}`
28
- );
30
+ throw new Error(`Libretto Cloud API error (${resp.status}): ${body}`);
29
31
  }
30
32
  const { json } = await resp.json();
33
+ const startupCleanup = createStartupSessionCleanup(
34
+ endpoint,
35
+ apiKey,
36
+ json.session_id
37
+ );
38
+ let readySession;
39
+ try {
40
+ readySession = await waitForCloudSessionReady({
41
+ endpoint,
42
+ apiKey,
43
+ session: json,
44
+ timeoutMs: QUEUE_WAIT_TIMEOUT_MS,
45
+ isCancelled: startupCleanup.isCancelled
46
+ });
47
+ } catch (error) {
48
+ if (startupCleanup.isCancelled()) {
49
+ await startupCleanup.waitForClose();
50
+ } else {
51
+ await closeCloudSession(endpoint, apiKey, json.session_id).catch(
52
+ () => {
53
+ }
54
+ );
55
+ }
56
+ throw error;
57
+ } finally {
58
+ startupCleanup.dispose();
59
+ }
31
60
  return {
32
- sessionId: json.session_id,
33
- cdpEndpoint: json.cdp_url,
34
- liveViewUrl: json.live_view_url ?? void 0
61
+ sessionId: readySession.session_id,
62
+ cdpEndpoint: readySession.cdp_url,
63
+ liveViewUrl: readySession.live_view_url ?? void 0
35
64
  };
36
65
  },
37
66
  async closeSession(sessionId) {
38
- const resp = await fetch(`${endpoint}/v1/sessions/close`, {
39
- method: "POST",
40
- headers: {
41
- "x-api-key": apiKey,
42
- "Content-Type": "application/json"
43
- },
44
- body: JSON.stringify({ json: { session_id: sessionId } })
45
- });
46
- if (!resp.ok) {
47
- const body = await resp.text();
48
- throw new Error(
49
- `Libretto Cloud API error closing session ${sessionId} (${resp.status}): ${body}`
50
- );
51
- }
52
- const { json } = await resp.json();
67
+ const json = await closeCloudSession(endpoint, apiKey, sessionId);
53
68
  return { replayUrl: json.replay_url ?? void 0 };
54
69
  }
55
70
  };
56
71
  }
72
+ async function waitForCloudSessionReady(args) {
73
+ let session = args.session;
74
+ if (args.isCancelled?.()) {
75
+ throw new Error(
76
+ `Libretto Cloud session ${session.session_id} was cancelled before browser capacity was available.`
77
+ );
78
+ }
79
+ if (session.cdp_url) {
80
+ return { ...session, cdp_url: session.cdp_url };
81
+ }
82
+ sendStartupStatus(
83
+ `Libretto Cloud browser session queued (session: ${session.session_id}). Waiting for browser capacity...`
84
+ );
85
+ const pollIntervalMs = readPositiveNumberEnv(
86
+ "LIBRETTO_CLOUD_SESSION_POLL_INTERVAL_MS",
87
+ DEFAULT_POLL_INTERVAL_MS
88
+ );
89
+ const deadline = Date.now() + args.timeoutMs;
90
+ while (Date.now() < deadline) {
91
+ if (args.isCancelled?.()) {
92
+ throw new Error(
93
+ `Libretto Cloud session ${session.session_id} was cancelled before browser capacity was available.`
94
+ );
95
+ }
96
+ await sleep(pollIntervalMs);
97
+ if (args.isCancelled?.()) {
98
+ throw new Error(
99
+ `Libretto Cloud session ${session.session_id} was cancelled before browser capacity was available.`
100
+ );
101
+ }
102
+ session = await getCloudSession(
103
+ args.endpoint,
104
+ args.apiKey,
105
+ session.session_id
106
+ );
107
+ if (session.cdp_url) {
108
+ sendStartupStatus(
109
+ `Libretto Cloud browser capacity available (session: ${session.session_id}). Connecting...`
110
+ );
111
+ return { ...session, cdp_url: session.cdp_url };
112
+ }
113
+ if (!["queued", "starting"].includes(session.status)) {
114
+ throw new Error(
115
+ `Libretto Cloud session ${session.session_id} entered status "${session.status}" before a CDP URL was available.`
116
+ );
117
+ }
118
+ }
119
+ throw new Error(
120
+ `Timed out waiting for Libretto Cloud browser capacity after ${Math.ceil(args.timeoutMs / 1e3)}s (session: ${session.session_id}).`
121
+ );
122
+ }
123
+ async function getCloudSession(endpoint, apiKey, sessionId) {
124
+ const resp = await fetch(`${endpoint}/v1/sessions/get`, {
125
+ method: "POST",
126
+ headers: {
127
+ "x-api-key": apiKey,
128
+ "Content-Type": "application/json"
129
+ },
130
+ body: JSON.stringify({ json: { session_id: sessionId } })
131
+ });
132
+ if (!resp.ok) {
133
+ const body = await resp.text();
134
+ throw new Error(
135
+ `Libretto Cloud API error reading session ${sessionId} (${resp.status}): ${body}`
136
+ );
137
+ }
138
+ const { json } = await resp.json();
139
+ return json;
140
+ }
141
+ async function closeCloudSession(endpoint, apiKey, sessionId) {
142
+ const resp = await fetch(`${endpoint}/v1/sessions/close`, {
143
+ method: "POST",
144
+ headers: {
145
+ "x-api-key": apiKey,
146
+ "Content-Type": "application/json"
147
+ },
148
+ body: JSON.stringify({ json: { session_id: sessionId } })
149
+ });
150
+ if (!resp.ok) {
151
+ const body = await resp.text();
152
+ throw new Error(
153
+ `Libretto Cloud API error closing session ${sessionId} (${resp.status}): ${body}`
154
+ );
155
+ }
156
+ const { json } = await resp.json();
157
+ return json;
158
+ }
159
+ function createStartupSessionCleanup(endpoint, apiKey, sessionId) {
160
+ let cancelled = false;
161
+ let closePromise = null;
162
+ const requestClose = (reason) => {
163
+ if (cancelled) return;
164
+ cancelled = true;
165
+ sendStartupStatus(
166
+ `Libretto Cloud browser session cancelled (${reason}). Cleaning up queued session...`
167
+ );
168
+ closePromise = closeCloudSession(endpoint, apiKey, sessionId).then(
169
+ () => {
170
+ },
171
+ () => {
172
+ }
173
+ );
174
+ };
175
+ const onDisconnect = () => requestClose("parent command disconnected");
176
+ const onSigint = () => requestClose("received SIGINT");
177
+ const onSigterm = () => requestClose("received SIGTERM");
178
+ if (typeof process.send === "function") {
179
+ process.once("disconnect", onDisconnect);
180
+ }
181
+ process.once("SIGINT", onSigint);
182
+ process.once("SIGTERM", onSigterm);
183
+ return {
184
+ isCancelled: () => cancelled,
185
+ waitForClose: async () => {
186
+ await closePromise;
187
+ },
188
+ dispose: () => {
189
+ process.off("disconnect", onDisconnect);
190
+ process.off("SIGINT", onSigint);
191
+ process.off("SIGTERM", onSigterm);
192
+ }
193
+ };
194
+ }
195
+ function readPositiveNumberEnv(name, fallback) {
196
+ const raw = process.env[name];
197
+ if (!raw) return fallback;
198
+ const parsed = Number(raw);
199
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
200
+ }
201
+ function sendStartupStatus(message) {
202
+ if (typeof process.send === "function") {
203
+ process.send({ type: "startup-status", message });
204
+ }
205
+ }
206
+ function sleep(ms) {
207
+ return new Promise((resolve) => setTimeout(resolve, ms));
208
+ }
57
209
  export {
58
210
  createLibrettoCloudProvider
59
211
  };
package/dist/cli/index.js CHANGED
@@ -1,8 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { runLibrettoCLI } from "./cli.js";
3
- import { runClose } from "./commands/browser.js";
4
3
  void runLibrettoCLI();
5
4
  export {
6
- runClose,
7
5
  runLibrettoCLI
8
6
  };
@@ -1,4 +1,3 @@
1
- import { aiCommands } from "./commands/ai.js";
2
1
  import { authCommands } from "./commands/auth.js";
3
2
  import { billingCommands } from "./commands/billing.js";
4
3
  import { browserCommands } from "./commands/browser.js";
@@ -9,15 +8,19 @@ import { setupCommand } from "./commands/setup.js";
9
8
  import { statusCommand } from "./commands/status.js";
10
9
  import { snapshotCommand } from "./commands/snapshot.js";
11
10
  import { librettoCommand } from "../shared/package-manager.js";
12
- import { SimpleCLI } from "./framework/simple-cli.js";
11
+ import { SimpleCLI } from "affordance";
13
12
  const cliRoutes = {
14
13
  ...browserCommands,
15
- deploy: deployCommand,
14
+ cloud: SimpleCLI.group({
15
+ description: "Libretto Cloud commands",
16
+ routes: {
17
+ deploy: deployCommand,
18
+ auth: authCommands,
19
+ billing: billingCommands
20
+ }
21
+ }),
16
22
  experiments: experimentsCommand,
17
23
  ...executionCommands,
18
- ai: aiCommands,
19
- auth: authCommands,
20
- billing: billingCommands,
21
24
  setup: setupCommand,
22
25
  status: statusCommand,
23
26
  snapshot: snapshotCommand
@@ -82,12 +82,12 @@ function wrapLocatorActions(locator, page, opts) {
82
82
  try {
83
83
  const result = await orig(...args);
84
84
  if (opts.visualize) {
85
- enqueue(page, () => visualizeAfterAction(page));
85
+ void enqueue(page, () => visualizeAfterAction(page));
86
86
  }
87
87
  return result;
88
88
  } catch (err) {
89
89
  if (opts.visualize) {
90
- enqueue(page, () => visualizeAfterAction(page));
90
+ void enqueue(page, () => visualizeAfterAction(page));
91
91
  }
92
92
  if (POINTER_ACTIONS.has(method) && isTimeoutError(err)) {
93
93
  await enrichTimeoutError(err, locator, page);
@@ -226,12 +226,12 @@ async function installInstrumentation(page, options) {
226
226
  try {
227
227
  const result = await orig(...args);
228
228
  if (visualize) {
229
- enqueue(page, () => visualizeAfterAction(page));
229
+ void enqueue(page, () => visualizeAfterAction(page));
230
230
  }
231
231
  return result;
232
232
  } catch (err) {
233
233
  if (visualize) {
234
- enqueue(page, () => visualizeAfterAction(page));
234
+ void enqueue(page, () => visualizeAfterAction(page));
235
235
  }
236
236
  if (POINTER_ACTIONS.has(method) && isTimeoutError(err) && typeof args[0] === "string") {
237
237
  await enrichTimeoutError(err, page.locator(args[0]), page);
@@ -5,5 +5,6 @@ declare function connectToIpcSocket(socketPath: string): Promise<IpcTransport<Ip
5
5
  declare function createIpcSocketServer(onConnection: (transport: IpcTransport<IpcProtocolMessage>) => void): Server;
6
6
  declare function listenForIpcConnections(socketPath: string, onConnection: (transport: IpcTransport<IpcProtocolMessage>) => void): Promise<Server>;
7
7
  declare function listenOnIpcSocket(server: Server, socketPath: string): Promise<void>;
8
+ declare function isWindowsNamedPipePath(socketPath: string): boolean;
8
9
 
9
- export { connectToIpcSocket, createIpcSocketServer, listenForIpcConnections, listenOnIpcSocket };
10
+ export { connectToIpcSocket, createIpcSocketServer, isWindowsNamedPipePath, listenForIpcConnections, listenOnIpcSocket };
@@ -1,10 +1,9 @@
1
- import { rm } from "node:fs/promises";
1
+ import { mkdir, rm } from "node:fs/promises";
2
2
  import {
3
3
  createServer,
4
4
  createConnection
5
5
  } from "node:net";
6
6
  import { dirname } from "node:path";
7
- import { mkdir } from "node:fs/promises";
8
7
  function createJsonSocketTransport(socket) {
9
8
  socket.setEncoding("utf8");
10
9
  return {
@@ -82,12 +81,11 @@ async function listenForIpcConnections(socketPath, onConnection) {
82
81
  return server;
83
82
  }
84
83
  async function listenOnIpcSocket(server, socketPath) {
85
- await mkdir(dirname(socketPath), { recursive: true });
86
- await rm(socketPath, { force: true });
84
+ await prepareIpcSocketPath(socketPath);
87
85
  const originalClose = server.close.bind(server);
88
86
  server.close = ((callback) => {
89
87
  return originalClose((error) => {
90
- void rm(socketPath, { force: true }).finally(() => callback?.(error));
88
+ void removeStaleSocketFile(socketPath).finally(() => callback?.(error));
91
89
  });
92
90
  });
93
91
  await new Promise((resolve, reject) => {
@@ -104,6 +102,18 @@ async function listenOnIpcSocket(server, socketPath) {
104
102
  server.listen(socketPath);
105
103
  });
106
104
  }
105
+ function isWindowsNamedPipePath(socketPath) {
106
+ return socketPath.startsWith("\\\\.\\pipe\\");
107
+ }
108
+ async function prepareIpcSocketPath(socketPath) {
109
+ if (isWindowsNamedPipePath(socketPath)) return;
110
+ await mkdir(dirname(socketPath), { recursive: true });
111
+ await removeStaleSocketFile(socketPath);
112
+ }
113
+ async function removeStaleSocketFile(socketPath) {
114
+ if (isWindowsNamedPipePath(socketPath)) return;
115
+ await rm(socketPath, { force: true });
116
+ }
107
117
  async function connectSocket(socketPath) {
108
118
  const socket = createConnection(socketPath);
109
119
  return new Promise((resolve, reject) => {
@@ -138,6 +148,7 @@ function isRecord(value) {
138
148
  export {
139
149
  connectToIpcSocket,
140
150
  createIpcSocketServer,
151
+ isWindowsNamedPipePath,
141
152
  listenForIpcConnections,
142
153
  listenOnIpcSocket
143
154
  };
@@ -6,6 +6,7 @@ import { expect, test as base } from "vitest";
6
6
  import { createIpcPeer } from "./ipc.js";
7
7
  import {
8
8
  connectToIpcSocket,
9
+ isWindowsNamedPipePath,
9
10
  listenForIpcConnections
10
11
  } from "./socket-transport.js";
11
12
  const test = base.extend({
@@ -47,6 +48,10 @@ test("sends concurrent calls over one socket", async ({ socketPath }) => {
47
48
  });
48
49
  await expect(stat(socketPath)).rejects.toThrow();
49
50
  });
51
+ test("recognizes Windows named pipe paths", () => {
52
+ expect(isWindowsNamedPipePath("\\\\.\\pipe\\libretto-abc123")).toBe(true);
53
+ expect(isWindowsNamedPipePath("/tmp/libretto-501-abc123.sock")).toBe(false);
54
+ });
50
55
  test("rejects pending calls when the socket closes", async ({ socketPath }) => {
51
56
  const serverPeers = [];
52
57
  const server = await listenForIpcConnections(socketPath, (transport) => {
package/docs/releasing.md CHANGED
@@ -24,7 +24,7 @@ This repo does not publish from local machines and does not push directly to `ma
24
24
 
25
25
  GitHub Actions needs these repository secrets:
26
26
 
27
- - `OPENAI_API_KEY`: used by the existing test suite during the release workflow.
27
+ - `OPENAI_API_KEY`: used by the existing test suite during the release workflow and by the eval workflow's default `openai/gpt-5.5` model.
28
28
 
29
29
  The release workflow uses a GitHub Actions environment named `release`. Create that environment in the repository settings (no required reviewers — access is controlled by branch protection on `main` instead).
30
30
 
@@ -69,7 +69,7 @@ The root `scripts/prepare-release.sh` script does the following:
69
69
  6. Commits the version bump.
70
70
  7. Pushes the branch and opens a PR to `main` with the `release` label.
71
71
 
72
- Release PRs also run the eval workflow. That workflow compares the current eval score against the latest successful `main` baseline and fails if the score drifts by more than 5 percentage points in either direction.
72
+ Release PRs also run the eval workflow. That workflow records score, duration, token, cost, and tool-call metrics for review. Scores are informational: low scores do not fail the workflow, but setup/runtime failures and zero completed records do.
73
73
 
74
74
  ## Merge behavior
75
75
 
@@ -89,11 +89,13 @@ This makes the workflow safe to re-run after partial failures. For example, if n
89
89
 
90
90
  `.github/workflows/evals.yml` now runs automatically for release PRs and for qualifying pushes to `main`.
91
91
 
92
- - On `main`, it records the current eval summary as the baseline artifact for future release PRs.
93
- - On release PRs, it runs evals again and compares the overall score against the latest successful `main` baseline.
94
- - If the score moves outside a `+/-5%` window, the eval job fails and flags the release PR.
92
+ - It runs `pnpm evals --no-auth --output <runner-temp>/eval-run` so CI only runs cases that do not require local auth profiles.
93
+ - It validates and renders the CI report with `pnpm evals summary <runner-temp>/eval-run`.
94
+ - It writes a GitHub step summary and a sticky PR comment with aggregate score, duration, token, cost, and tool-call metrics.
95
+ - It uploads summary files and per-case result records from the run output directory. Raw transcripts and local profile files are not uploaded.
96
+ - It fails when the eval runner crashes, required setup is missing, result records are malformed, or zero completed records are produced.
95
97
 
96
- If no successful baseline artifact exists yet, the release PR eval job reports that and skips the comparison for that run.
98
+ There is no baseline comparison gate yet. Add one only after the eval records and metrics are stable enough to compare reliably.
97
99
 
98
100
  ## Changelog behavior
99
101
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libretto",
3
- "version": "0.6.12",
3
+ "version": "0.6.14",
4
4
  "description": "AI-powered browser automation library and CLI built on Playwright",
5
5
  "license": "MIT",
6
6
  "homepage": "https://libretto.sh",
@@ -9,7 +9,6 @@
9
9
  "url": "https://github.com/saffron-health/libretto"
10
10
  },
11
11
  "type": "module",
12
- "packageManager": "pnpm@10.33.0",
13
12
  "publishConfig": {
14
13
  "access": "public"
15
14
  },
@@ -37,6 +36,7 @@
37
36
  "sync-skills": "pnpm run sync:mirrors",
38
37
  "check:skills": "pnpm run check:mirrors",
39
38
  "build": "tsup --config tsup.config.ts",
39
+ "lint": "lintcn lint --tsconfig tsconfig.json",
40
40
  "type-check": "tsc --noEmit",
41
41
  "test": "turbo run test:vitest --filter=libretto --log-order=grouped",
42
42
  "test:vitest": "vitest run",
@@ -85,6 +85,7 @@
85
85
  "vitest": "^4.1.5"
86
86
  },
87
87
  "dependencies": {
88
+ "affordance": "^0.1.0",
88
89
  "ai": "^6.0.116",
89
90
  "esbuild": "^0.27.0",
90
91
  "playwright": "^1.58.2",