libretto 0.6.32 → 0.6.34

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 (37) hide show
  1. package/README.md +1 -1
  2. package/README.template.md +1 -1
  3. package/dist/cli/commands/auth.js +119 -268
  4. package/dist/cli/commands/browser.js +1 -0
  5. package/dist/cli/commands/cloud-jobs.js +7 -0
  6. package/dist/cli/commands/execution.js +3 -2
  7. package/dist/cli/core/browser.js +2 -1
  8. package/dist/cli/core/daemon/daemon.js +2 -1
  9. package/dist/cli/core/deploy-artifact.js +41 -16
  10. package/dist/cli/core/providers/kernel.js +3 -2
  11. package/dist/cli/core/providers/libretto-cloud.js +2 -1
  12. package/dist/cli/core/telemetry.js +14 -2
  13. package/dist/index.d.ts +1 -1
  14. package/dist/shared/workflow/workflow.d.ts +18 -0
  15. package/dist/shared/workflow/workflow.js +9 -0
  16. package/docs/releasing.md +3 -1
  17. package/package.json +2 -3
  18. package/scripts/generate-changelog.ts +196 -2
  19. package/skills/libretto/SKILL.md +17 -2
  20. package/skills/libretto/references/website-authentication.md +18 -2
  21. package/skills/libretto-readonly/SKILL.md +1 -1
  22. package/src/cli/commands/auth.ts +169 -382
  23. package/src/cli/commands/browser.ts +1 -0
  24. package/src/cli/commands/cloud-jobs.ts +8 -0
  25. package/src/cli/commands/execution.ts +2 -1
  26. package/src/cli/core/browser.ts +2 -0
  27. package/src/cli/core/daemon/config.ts +1 -0
  28. package/src/cli/core/daemon/daemon.ts +1 -0
  29. package/src/cli/core/deploy-artifact.ts +63 -14
  30. package/src/cli/core/providers/kernel.ts +3 -2
  31. package/src/cli/core/providers/libretto-cloud.ts +1 -0
  32. package/src/cli/core/providers/types.ts +1 -0
  33. package/src/cli/core/resolve-model.ts +5 -5
  34. package/src/cli/core/telemetry.ts +15 -1
  35. package/src/cli/core/workflow-runtime.ts +1 -1
  36. package/src/index.ts +1 -0
  37. package/src/shared/workflow/workflow.ts +22 -0
@@ -49,7 +49,8 @@ function createKernelProvider(options = {}) {
49
49
  const enableRecording = options.enableRecording ?? readBooleanEnv("KERNEL_ENABLE_RECORDING", false);
50
50
  const replays = /* @__PURE__ */ new Map();
51
51
  return {
52
- async createSession() {
52
+ async createSession(sessionOptions) {
53
+ const sessionHeadless = sessionOptions?.headless ?? headless;
53
54
  const json = await kernelFetchJson(
54
55
  endpoint,
55
56
  apiKey,
@@ -57,7 +58,7 @@ function createKernelProvider(options = {}) {
57
58
  {
58
59
  method: "POST",
59
60
  body: JSON.stringify({
60
- headless,
61
+ headless: sessionHeadless,
61
62
  stealth,
62
63
  timeout_seconds: timeoutSeconds
63
64
  })
@@ -25,7 +25,8 @@ function createLibrettoCloudProvider() {
25
25
  json: {
26
26
  timeout_seconds: browserSessionTimeoutSeconds,
27
27
  profile_name: options?.authProfileName,
28
- profile_persist: options?.authProfilePersist
28
+ profile_persist: options?.authProfilePersist,
29
+ headless: options?.headless
29
30
  }
30
31
  })
31
32
  });
@@ -5,6 +5,7 @@ import { homedir } from "node:os";
5
5
  import { basename, dirname, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { resolveHostedApiUrl } from "./auth-fetch.js";
8
+ import { readAuthState } from "./auth-storage.js";
8
9
  const TELEMETRY_FILE_NAME = "telemetry.json";
9
10
  const TELEMETRY_ENDPOINT_PATH = "/v1/telemetry/recordCliEvent";
10
11
  const TELEMETRY_TIMEOUT_MS = 250;
@@ -66,7 +67,7 @@ function writeTelemetryNotice() {
66
67
  if (!process.stderr.isTTY) return;
67
68
  process.stderr.write(
68
69
  [
69
- "Libretto collects anonymous CLI telemetry: install id, timestamp, command event, error status, package version, and build channel only.",
70
+ "Libretto collects CLI telemetry: install id, timestamp, command event, error status, package version, build channel, and signed-in cloud user id only.",
70
71
  "Set LIBRETTO_TELEMETRY_DISABLED=1 or DO_NOT_TRACK=1 to disable it, or set enabled:false in ~/.libretto/telemetry.json."
71
72
  ].join(" ") + "\n"
72
73
  );
@@ -82,15 +83,26 @@ async function recordCliTelemetryEvent(command, error) {
82
83
  if (isTelemetryDisabled()) return;
83
84
  const installId = await readOrCreateInstallId();
84
85
  if (!installId) return;
86
+ const cloudUserId = await readConfiguredCloudUserId();
85
87
  await sendWithTimeout({
86
88
  installId,
87
89
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
88
90
  event: `libretto ${command.path.join(" ")}`,
89
91
  error,
90
92
  packageVersion,
91
- buildChannel
93
+ buildChannel,
94
+ ...cloudUserId ? { cloudUserId } : {}
92
95
  });
93
96
  }
97
+ async function readConfiguredCloudUserId() {
98
+ try {
99
+ const state = await readAuthState();
100
+ const cloudUserId = state?.session?.userId;
101
+ return cloudUserId && cloudUserId.length > 0 ? cloudUserId : null;
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
94
106
  async function sendWithTimeout(payload) {
95
107
  const controller = new AbortController();
96
108
  const timeout = setTimeout(() => controller.abort(), TELEMETRY_TIMEOUT_MS);
package/dist/index.d.ts CHANGED
@@ -14,7 +14,7 @@ export { GhostCursorOptions, ensureGhostCursor, ghostClick, hideGhostCursor, mov
14
14
  export { HighlightOptions, clearHighlights, ensureHighlightLayer, showHighlight } from './shared/visualization/highlight.js';
15
15
  export { BrowserSession, LaunchBrowserArgs, launchBrowser } from './shared/run/browser.js';
16
16
  export { LibrettoAuthenticateOptions, librettoAuthenticate } from './shared/workflow/authenticate.js';
17
- export { ExportedLibrettoWorkflow, LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, LibrettoWorkflowContext, LibrettoWorkflowHandler, LibrettoWorkflowInputError, LibrettoWorkflowOptions, WorkflowInputValidator, getDefaultWorkflowFromModuleExports, getWorkflowFromModuleExports, getWorkflowsFromModuleExports, isLibrettoWorkflow, validateWorkflowInput, workflow } from './shared/workflow/workflow.js';
17
+ export { ExportedLibrettoWorkflow, LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, LibrettoWorkflowContext, LibrettoWorkflowDefinition, LibrettoWorkflowHandler, LibrettoWorkflowInputError, LibrettoWorkflowOptions, WorkflowInputValidator, getDefaultWorkflowFromModuleExports, getWorkflowFromModuleExports, getWorkflowsFromModuleExports, isLibrettoWorkflow, validateWorkflowInput, workflow } from './shared/workflow/workflow.js';
18
18
  export { AuthProfileStorageState, captureAuthProfileStorageState, normalizeAuthProfileSite, parseAuthProfileSites } from './shared/workflow/auth-profile-state.js';
19
19
  import 'zod';
20
20
  import 'playwright';
@@ -3,6 +3,12 @@ import { z } from 'zod';
3
3
  import { RecoveryAction } from '../../runtime/recovery/page-fallbacks.js';
4
4
  import 'ai';
5
5
 
6
+ declare const ViewportConfigSchema: z.ZodObject<{
7
+ width: z.ZodNumber;
8
+ height: z.ZodNumber;
9
+ }, z.core.$strip>;
10
+ type ViewportConfig = z.infer<typeof ViewportConfigSchema>;
11
+
6
12
  declare const LIBRETTO_WORKFLOW_BRAND: unique symbol;
7
13
  type LibrettoWorkflowContext = {
8
14
  session: string;
@@ -18,6 +24,9 @@ type LibrettoWorkflowDefinition<InputSchema extends z.ZodType = z.ZodType<unknow
18
24
  output?: OutputSchema;
19
25
  credentials?: readonly string[];
20
26
  authProfile?: LibrettoWorkflowAuthProfile;
27
+ startUrl?: string;
28
+ gpu?: boolean;
29
+ viewport?: ViewportConfig;
21
30
  recoveryAction?: RecoveryAction;
22
31
  };
23
32
  type LibrettoWorkflowOptions<InputSchema extends z.ZodType = z.ZodType<unknown>, OutputSchema extends z.ZodType = z.ZodType<unknown>> = LibrettoWorkflowDefinition<InputSchema, OutputSchema> & {
@@ -41,6 +50,9 @@ declare class LibrettoWorkflow<InputSchema extends z.ZodType = z.ZodType<unknown
41
50
  readonly credentialNames: readonly string[];
42
51
  readonly authProfileName?: string;
43
52
  readonly authProfileRefresh?: boolean;
53
+ readonly startUrl?: string;
54
+ readonly gpu?: boolean;
55
+ readonly viewport?: ViewportConfig;
44
56
  readonly recoveryAction?: RecoveryAction;
45
57
  private readonly handler;
46
58
  constructor(name: string, options: {
@@ -49,6 +61,9 @@ declare class LibrettoWorkflow<InputSchema extends z.ZodType = z.ZodType<unknown
49
61
  credentialNames?: readonly string[];
50
62
  authProfileName?: string;
51
63
  authProfileRefresh?: boolean;
64
+ startUrl?: string;
65
+ gpu?: boolean;
66
+ viewport?: ViewportConfig;
52
67
  recoveryAction?: RecoveryAction;
53
68
  } | undefined, handler: LibrettoWorkflowHandler<z.infer<InputSchema>, z.infer<OutputSchema>>);
54
69
  run(ctx: LibrettoWorkflowContext, input: unknown): Promise<z.infer<OutputSchema>>;
@@ -61,6 +76,9 @@ type ExportedLibrettoWorkflow = {
61
76
  readonly credentialNames: readonly string[];
62
77
  readonly authProfileName?: string;
63
78
  readonly authProfileRefresh?: boolean;
79
+ readonly startUrl?: string;
80
+ readonly gpu?: boolean;
81
+ readonly viewport?: ViewportConfig;
64
82
  readonly recoveryAction?: RecoveryAction;
65
83
  run: (ctx: LibrettoWorkflowContext, input: unknown) => Promise<unknown>;
66
84
  };
@@ -76,6 +76,9 @@ class LibrettoWorkflow {
76
76
  credentialNames;
77
77
  authProfileName;
78
78
  authProfileRefresh;
79
+ startUrl;
80
+ gpu;
81
+ viewport;
79
82
  recoveryAction;
80
83
  handler;
81
84
  constructor(name, options, handler) {
@@ -85,6 +88,9 @@ class LibrettoWorkflow {
85
88
  this.credentialNames = options?.credentialNames ?? [];
86
89
  this.authProfileName = options?.authProfileName;
87
90
  this.authProfileRefresh = options?.authProfileRefresh;
91
+ this.startUrl = options?.startUrl;
92
+ this.gpu = options?.gpu;
93
+ this.viewport = options?.viewport;
88
94
  this.recoveryAction = options?.recoveryAction;
89
95
  this.handler = handler;
90
96
  }
@@ -159,6 +165,9 @@ function getWorkflowConstructorOptions(options) {
159
165
  credentialNames: normalizeCredentialNames(options.credentials),
160
166
  authProfileName: authProfile?.name,
161
167
  authProfileRefresh: authProfile?.refresh,
168
+ startUrl: options.startUrl,
169
+ gpu: options.gpu,
170
+ viewport: options.viewport,
162
171
  recoveryAction: options.recoveryAction
163
172
  };
164
173
  }
package/docs/releasing.md CHANGED
@@ -49,7 +49,7 @@ The workflow needs `contents: write` to create the GitHub release and tag, and `
49
49
 
50
50
  After trusted publishing is working, remove any old npm publish token from the repo secrets. npm recommends restricting token-based publishing after the migration.
51
51
 
52
- GitHub release notes are generated by `packages/libretto/scripts/generate-changelog.ts`. The script finds the previous release, compares that tag to the release commit, and passes only the merged PRs in that range to the release notes agent.
52
+ GitHub release notes are generated by `packages/libretto/scripts/generate-changelog.ts`. The script finds the previous release, compares that tag to the release commit, and passes only the merged PRs in that range to the release notes agent. The release workflow uploads the generated notes and a changelog audit JSON artifact containing the release range, eligible PRs, inspected PR diffs, tool calls, and final model output.
53
53
 
54
54
  ## Prepare a release PR
55
55
 
@@ -108,6 +108,8 @@ The GitHub Releases page is the changelog for this repo.
108
108
 
109
109
  When the workflow runs `pnpm generate-changelog vX.Y.Z`, the script finds the previous GitHub release, compares that tag to the release commit, and extracts PR numbers from the compare commits. The release notes agent can inspect only those PRs with `gh pr view` and `gh pr diff`.
110
110
 
111
+ The generator logs the release range and changelog-eligible PRs to stderr. When `CHANGELOG_AUDIT_FILE` is set, it also writes a JSON audit file with the eligible PR list, inspected diff PRs, tool-call summaries, and raw/final release notes. Generation fails if the agent does not inspect every eligible PR diff, so omissions are easier to diagnose before a GitHub release is created.
112
+
111
113
  To keep release notes readable, use clear PR titles and descriptions before merging. If a PR should not appear in the changelog, add the `skip-changelog` label.
112
114
 
113
115
  ## Notes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libretto",
3
- "version": "0.6.32",
3
+ "version": "0.6.34",
4
4
  "description": "AI-powered browser automation library and CLI built on Playwright",
5
5
  "license": "MIT",
6
6
  "homepage": "https://libretto.sh",
@@ -70,7 +70,7 @@
70
70
  "playwright": "^1.58.2",
71
71
  "tsx": "^4.21.0",
72
72
  "zod": "^4.3.6",
73
- "affordance": "^0.2.1"
73
+ "affordance": "^0.2.2"
74
74
  },
75
75
  "scripts": {
76
76
  "sync:mirrors": "node ../dev-tools/scripts/sync-mirrors.mjs",
@@ -78,7 +78,6 @@
78
78
  "sync-skills": "pnpm run sync:mirrors",
79
79
  "check:skills": "pnpm run check:mirrors",
80
80
  "build": "tsup --config tsup.config.ts",
81
- "lint": "lintcn lint --tsconfig tsconfig.json",
82
81
  "type-check": "tsc --noEmit",
83
82
  "test": "turbo run test:vitest --filter=libretto --log-order=grouped",
84
83
  "test:vitest": "vitest run",
@@ -1,4 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import { dirname } from "node:path";
3
+ import { mkdirSync, writeFileSync } from "node:fs";
2
4
  import { Agent, type AgentTool, type AgentEvent } from "@mariozechner/pi-agent-core";
3
5
  import { getModel } from "@mariozechner/pi-ai";
4
6
  import { Type, type Static } from "@sinclair/typebox";
@@ -60,6 +62,34 @@ interface ReleaseContext {
60
62
  pullRequests: PullRequestDetails[];
61
63
  }
62
64
 
65
+ interface ToolCallAudit {
66
+ toolCallId: string;
67
+ toolName: string;
68
+ args: unknown;
69
+ isError?: boolean;
70
+ resultTextBytes?: number;
71
+ resultTextPreview?: string;
72
+ }
73
+
74
+ interface ChangelogAudit {
75
+ tag: string;
76
+ generatedAt: string;
77
+ previousTag: string;
78
+ currentRef: string;
79
+ eligiblePullRequests: Array<{
80
+ number: number;
81
+ title: string;
82
+ labels: string[];
83
+ files: string[];
84
+ url: string;
85
+ }>;
86
+ inspectedDiffPullRequests: number[];
87
+ missingDiffPullRequests: number[];
88
+ toolCalls: ToolCallAudit[];
89
+ rawFinalText: string;
90
+ finalText: string;
91
+ }
92
+
63
93
  function runGh(args: string[]): string {
64
94
  return execFileSync("gh", args, {
65
95
  encoding: "utf8",
@@ -190,6 +220,7 @@ function buildReleaseContext(currentTag: string): ReleaseContext {
190
220
 
191
221
  const releaseContext = buildReleaseContext(tag);
192
222
  const allowedPullRequestNumbers = new Set(releaseContext.pullRequests.map((pr) => String(pr.number)));
223
+ const auditFilePath = process.env.CHANGELOG_AUDIT_FILE;
193
224
  const pullRequestSummary = releaseContext.pullRequests
194
225
  .map((pr) => {
195
226
  const labels = pr.labels.map((label) => label.name).join(", ") || "none";
@@ -204,6 +235,13 @@ const pullRequestSummary = releaseContext.pullRequests
204
235
  })
205
236
  .join("\n\n");
206
237
 
238
+ console.error(`Changelog release range: ${releaseContext.previousTag}...${releaseContext.currentRef}`);
239
+ console.error("Changelog-eligible PRs:");
240
+ for (const pr of releaseContext.pullRequests) {
241
+ const labels = pr.labels.map((label) => label.name).join(", ") || "none";
242
+ console.error(`- #${pr.number} ${pr.title} [${labels}]`);
243
+ }
244
+
207
245
  const GhToolParamsSchema = Type.Object({
208
246
  args: Type.String({ description: "Arguments to pass to gh (without the leading 'gh')" }),
209
247
  });
@@ -242,6 +280,12 @@ const ghTool: AgentTool = {
242
280
  `PR #${number ?? "(missing)"} is outside the ${releaseContext.previousTag}...${releaseContext.currentRef} release range.`,
243
281
  );
244
282
  }
283
+
284
+ // Diff inspection enforcement counts successful 'pr diff NUMBER' calls,
285
+ // so reject flags like --name-only that would return less than the full diff.
286
+ if (action === "diff" && parts.length > 3) {
287
+ throw new Error("'pr diff' accepts only a PR number. Run 'pr diff NUMBER' with no extra arguments.");
288
+ }
245
289
  }
246
290
  try {
247
291
  const output = runGh(parts);
@@ -277,6 +321,9 @@ const agent = new Agent({
277
321
  "- Write concise, user-facing release notes in markdown.",
278
322
  "- Group changes into sections like Features, Fixes, and Improvements. Only include sections that have entries.",
279
323
  "- Focus on what changed from the user's perspective, not internal implementation details.",
324
+ "- For feature and improvement entries, include minimal code blocks when they make the change easier to use.",
325
+ "- Each relevant feature or improvement can have its own snippet.",
326
+ "- Keep each code block to at most 10 lines, and use at most one comment line inside the block if helpful.",
280
327
  "- Do NOT include PR numbers or links.",
281
328
  "- Skip PRs labeled 'skip-changelog'.",
282
329
  "- Your response must contain ONLY the raw markdown release notes. No preamble like 'Here are the release notes'. No commentary or explanation. No '---' separators. The very first character of your response must be '#'. Example format:",
@@ -291,8 +338,123 @@ const agent = new Agent({
291
338
  });
292
339
 
293
340
  let finalText = "";
341
+ let rawFinalText = "";
342
+ const toolCalls = new Map<string, ToolCallAudit>();
343
+
344
+ function getObjectProperty(value: unknown, key: string): unknown {
345
+ if (typeof value !== "object" || value === null || !(key in value)) {
346
+ return undefined;
347
+ }
348
+
349
+ return (value as Record<string, unknown>)[key];
350
+ }
351
+
352
+ function getTextContent(value: unknown): string {
353
+ const content = getObjectProperty(value, "content");
354
+ if (!Array.isArray(content)) {
355
+ return "";
356
+ }
357
+
358
+ return content
359
+ .map((block) => {
360
+ if (typeof block !== "object" || block === null) {
361
+ return "";
362
+ }
363
+ const type = getObjectProperty(block, "type");
364
+ const text = getObjectProperty(block, "text");
365
+ return type === "text" && typeof text === "string" ? text : "";
366
+ })
367
+ .filter((text) => text.length > 0)
368
+ .join("\n");
369
+ }
370
+
371
+ function getInspectedDiffPullRequests(): number[] {
372
+ const inspected = new Set<number>();
373
+ for (const toolCall of toolCalls.values()) {
374
+ if (toolCall.isError !== false) {
375
+ // Only count tool calls that completed successfully.
376
+ continue;
377
+ }
378
+
379
+ const args = getObjectProperty(toolCall.args, "args");
380
+ if (typeof args !== "string") {
381
+ continue;
382
+ }
383
+
384
+ const parts = args.trim().split(/\s+/);
385
+ if (parts[0] !== "pr" || parts[1] !== "diff") {
386
+ continue;
387
+ }
388
+
389
+ const number = Number(parts[2]);
390
+ if (Number.isInteger(number)) {
391
+ inspected.add(number);
392
+ }
393
+ }
394
+
395
+ return [...inspected].sort((a, b) => a - b);
396
+ }
397
+
398
+ function buildAudit(): ChangelogAudit {
399
+ const inspectedDiffPullRequests = getInspectedDiffPullRequests();
400
+ const inspected = new Set(inspectedDiffPullRequests);
401
+ const missingDiffPullRequests = releaseContext.pullRequests
402
+ .map((pr) => pr.number)
403
+ .filter((number) => !inspected.has(number));
404
+
405
+ return {
406
+ tag,
407
+ generatedAt: new Date().toISOString(),
408
+ previousTag: releaseContext.previousTag,
409
+ currentRef: releaseContext.currentRef,
410
+ eligiblePullRequests: releaseContext.pullRequests.map((pr) => ({
411
+ number: pr.number,
412
+ title: pr.title,
413
+ labels: pr.labels.map((label) => label.name),
414
+ files: pr.files.map((file) => file.path),
415
+ url: pr.url,
416
+ })),
417
+ inspectedDiffPullRequests,
418
+ missingDiffPullRequests,
419
+ toolCalls: [...toolCalls.values()],
420
+ rawFinalText,
421
+ finalText,
422
+ };
423
+ }
424
+
425
+ function writeAuditFile(): void {
426
+ if (!auditFilePath) {
427
+ return;
428
+ }
429
+
430
+ mkdirSync(dirname(auditFilePath), { recursive: true });
431
+ writeFileSync(auditFilePath, `${JSON.stringify(buildAudit(), null, 2)}\n`);
432
+ }
294
433
 
295
434
  agent.subscribe((event: AgentEvent) => {
435
+ if (event.type === "tool_execution_start") {
436
+ toolCalls.set(event.toolCallId, {
437
+ toolCallId: event.toolCallId,
438
+ toolName: event.toolName,
439
+ args: event.args,
440
+ });
441
+ }
442
+
443
+ if (event.type === "tool_execution_end") {
444
+ const text = getTextContent(event.result);
445
+ const existing = toolCalls.get(event.toolCallId) ?? {
446
+ toolCallId: event.toolCallId,
447
+ toolName: event.toolName,
448
+ args: undefined,
449
+ };
450
+ toolCalls.set(event.toolCallId, {
451
+ ...existing,
452
+ isError: event.isError,
453
+ resultTextBytes: Buffer.byteLength(text, "utf8"),
454
+ resultTextPreview: text.slice(0, 2000),
455
+ });
456
+ }
457
+
296
458
  if (event.type === "agent_end") {
297
459
  const messages = event.messages;
298
460
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -300,7 +462,8 @@ agent.subscribe((event: AgentEvent) => {
300
462
  if (msg.role === "assistant" && Array.isArray(msg.content)) {
301
463
  for (const block of msg.content) {
302
464
  if (typeof block === "object" && "type" in block && block.type === "text" && "text" in block) {
303
- finalText = block.text as string;
465
+ rawFinalText = block.text as string;
466
+ finalText = rawFinalText;
304
467
  return;
305
468
  }
306
469
  }
@@ -309,10 +472,18 @@ agent.subscribe((event: AgentEvent) => {
309
472
  }
310
473
  });
311
474
 
312
- await agent.prompt("Generate the release notes now.");
475
+ try {
476
+ await agent.prompt("Generate the release notes now.");
477
+ } catch (error) {
478
+ console.error("Changelog generation failed: agent prompt threw an error.");
479
+ console.error(error);
480
+ writeAuditFile();
481
+ process.exit(1);
482
+ }
313
483
 
314
484
  if (!finalText) {
315
485
  console.error("Changelog generation failed: no text output from agent.");
486
+ writeAuditFile();
316
487
  process.exit(1);
317
488
  }
318
489
 
@@ -324,7 +495,30 @@ if (headingIndex >= 0) {
324
495
  // Already starts with a heading, keep as-is.
325
496
  } else {
326
497
  console.error("Changelog generation failed: output does not contain markdown headings.");
498
+ writeAuditFile();
327
499
  process.exit(1);
328
500
  }
329
501
 
502
+ const inspectedDiffPullRequests = getInspectedDiffPullRequests();
503
+ const missingDiffPullRequests = releaseContext.pullRequests
504
+ .map((pr) => pr.number)
505
+ .filter((number) => !inspectedDiffPullRequests.includes(number));
506
+
507
+ console.error(`Inspected PR diffs: ${inspectedDiffPullRequests.map((number) => `#${number}`).join(", ") || "none"}`);
508
+
509
+ if (missingDiffPullRequests.length > 0) {
510
+ console.error(
511
+ `Changelog generation failed: the agent did not inspect diffs for PRs ${missingDiffPullRequests
512
+ .map((number) => `#${number}`)
513
+ .join(", ")}.`,
514
+ );
515
+ writeAuditFile();
516
+ process.exit(1);
517
+ }
518
+
519
+ writeAuditFile();
520
+ if (auditFilePath) {
521
+ console.error(`Wrote changelog audit: ${auditFilePath}`);
522
+ }
523
+
330
524
  process.stdout.write(finalText);
@@ -4,7 +4,7 @@ description: "Browser automation CLI for building, maintaining, and running brow
4
4
  license: MIT
5
5
  metadata:
6
6
  author: saffron-health
7
- version: "0.6.32"
7
+ version: "0.6.34"
8
8
  ---
9
9
 
10
10
  ## How Libretto Works
@@ -44,6 +44,18 @@ Prefer to enter sites at a user-facing URL (homepage, login, etc.) on the first
44
44
 
45
45
  - Use `npx libretto experiments` to list internal feature flags and `npx libretto experiments describe <name>` for usage notes when an experiment is enabled.
46
46
 
47
+ ## Run Modes
48
+
49
+ There are three ways to run workflows:
50
+
51
+ - Local browser run: `npx libretto run ./workflow.ts` runs workflow code and local Chromium on the current machine. This is easy to watch and private, but most likely to hit CAPTCHAs or anti-bot checks.
52
+ - Hosted browser run: `npx libretto run ./workflow.ts --provider libretto-cloud` runs workflow code locally while the browser runs on provider infrastructure with stealth mechanisms. Use this for anti-bot/CAPTCHA issues, provider-specific behavior, and pre-deploy validation. If the user has not specified a provider, prefer `libretto-cloud` because it includes one free allocated browser-hour and does not require a third-party provider API key.
53
+ - Deployed workflow: Libretto Cloud packages the workflow as an API-backed workflow with the same remote-browser anti-bot mechanisms as hosted provider runs. Deploy only after the workflow passes with the target provider.
54
+
55
+ When editing a deployed workflow, validate changes with `run --provider <deployment-provider>` before redeploying; do not debug by repeatedly deploying and running the deployed job.
56
+
57
+ If the user prefers a provider for all local CLI runs in a workspace or only deploys workflows to that provider, update `.libretto/config.json` with `provider` instead of repeating `--provider`. Read `references/configuration-file-reference.md` first.
58
+
47
59
  ## Working Rules
48
60
 
49
61
  - Announce which session you are using and what page you are on.
@@ -56,7 +68,7 @@ Prefer to enter sites at a user-facing URL (homepage, login, etc.) on the first
56
68
  - Authenticated workflows must implement `librettoAuthenticate` with declared credentials before validation. Use a reusable `*_totp_secret` credential for authenticator-app MFA, not a one-time `otp_code`; text and email verification codes are not supported for fully automated sign-in.
57
69
  - Read `references/website-authentication.md` when you need `librettoAuthenticate` examples or auth-profile details.
58
70
  - Validation requires a successful clean `run` on a fresh, unauthenticated session with confirmation of the actual returned output, not just process success. Use the same headed or headless mode that the workflow run is already using.
59
- - After validation, always show the user: (1) the output/results from the validation run, and (2) the same command so they can re-run it themselves. Include any `--params`, `--headed`, or `--headless` flags the workflow needs.
71
+ - After validation, always show the user: (1) the output/results from the validation run, and (2) the same command so they can re-run it themselves. Include any `--params`, `--provider`, `--headed`, or `--headless` flags the workflow needs. Do not add `--auth-profile` to `run`; workflow `authProfile` metadata controls profile use.
60
72
  - Treat exploration sessions as disposable unless the user explicitly wants one kept open.
61
73
  - Close disposable sessions before your final response once exploration, debugging, or validation is complete. Open browsers keep consuming local or hosted resources.
62
74
  - Get explicit user confirmation before mutating actions or replaying network requests that may have side effects.
@@ -74,6 +86,7 @@ Prefer to enter sites at a user-facing URL (homepage, login, etc.) on the first
74
86
 
75
87
  ```bash
76
88
  npx libretto open https://example.com
89
+ npx libretto open https://example.com --provider libretto-cloud --session provider-debug
77
90
  npx libretto open https://example.com --read-only --session readonly-example
78
91
  npx libretto open https://example.com --session debug-example
79
92
  ```
@@ -150,6 +163,7 @@ npx libretto exec --session debug-example --page <page-id> "await page.url()"
150
163
  - Use `run` to verify a workflow file after creating it or editing it. Use the same headed or headless mode for validation that the workflow run is already using. Plain `run` defaults to headed mode.
151
164
  - Workflows define their input shape with a Zod schema (see `references/code-generation-rules.md`). `run` validates `--params` against that schema before calling the handler and prints a clear field-by-field error if the input doesn't match.
152
165
  - Successful runs close the browser by default. Pass `--stay-open-on-success` when you need to inspect the completed state with `pages`, `snapshot`, or `exec`.
166
+ - Use `--provider <name>` when validating behavior in that provider's browser runtime.
153
167
  - Pass `--read-only` if the preserved session should come back locked for follow-up terminal inspection after the workflow run.
154
168
  - If the workflow fails, Libretto keeps the browser open. Inspect the failed state with `snapshot` and `exec` before editing code.
155
169
  - Insert `await pause(session)` statements in the workflow file when you need to stop at specific states for interactive debugging, like breakpoints in the browser flow.
@@ -158,6 +172,7 @@ npx libretto exec --session debug-example --page <page-id> "await page.url()"
158
172
 
159
173
  ```bash
160
174
  npx libretto run ./integration.ts --params '{"status":"open"}'
175
+ npx libretto run ./integration.ts --provider libretto-cloud
161
176
  npx libretto run ./integration.ts --read-only
162
177
  npx libretto run ./integration.ts --stay-open-on-success
163
178
  ```
@@ -2,7 +2,7 @@
2
2
 
3
3
  Use this reference when a workflow needs a logged-in website session. The Working Rules in `../SKILL.md` define the required auth workflow; this file explains how to implement sign-in logic with `librettoAuthenticate`, and how auth profiles save signed-in state for later runs.
4
4
 
5
- Build and verify working sign-in logic first. The sign-in code takes priority; an auth profile is added only after the sign-in logic is verified, never as a substitute for it.
5
+ Build and verify working sign-in logic first. Authenticated workflows must use `librettoAuthenticate`; an auth profile is added only after the sign-in logic is verified, never as a substitute for it.
6
6
 
7
7
  ## Sign-In Logic
8
8
 
@@ -44,7 +44,15 @@ export default workflow("accountWorkflow", {
44
44
 
45
45
  Auth profiles save the signed-in browser state (cookies, localStorage, IndexedDB) so later runs can reuse a logged-in session instead of signing in from scratch. The sign-in logic still takes priority: do not add a profile until the `librettoAuthenticate` sign-in step has been verified from a signed-out browser with no profile present. If you add a profile first, validation passes on the saved session while the untested sign-in logic fails the first time that session expires.
46
46
 
47
- A profile only holds whatever a signed-in session wrote into it, so it does nothing until a run has signed in at least once. With `refresh: true`, a successful run writes updated browser state back to the profile, so a fresh sign-in repairs an expired one. Local runs load `.libretto/profiles/<name>.json`; hosted runs use the provider-native profile with the same name.
47
+ A profile only holds whatever a signed-in session wrote into it, so it does nothing until a run has signed in at least once. With `refresh: true`, a successful run writes updated browser state back to the profile, so a fresh sign-in repairs an expired one.
48
+
49
+ Auth profiles are browser-runtime-specific:
50
+
51
+ - Local runs load `.libretto/profiles/<name>.json`.
52
+ - `libretto-cloud` and providers that explicitly support auth profiles use provider-backed profile state; a local profile file is not available to provider browsers.
53
+ - Provider-backed profiles are created or refreshed by workflow-declared `authProfile` metadata. For manual login on a remote provider, run the hosted workflow with a pause/wait, ask the human to sign in through the live URL, use the observed login flow to implement `librettoAuthenticate`, then let the workflow finish so the provider can persist the profile.
54
+ - A plain `open --provider` session does not have workflow auth profile metadata. `libretto save` writes a local profile from an open session; it does not persist provider-backed workflow auth profile state.
55
+ - Revalidate login when switching providers, even if the profile name is the same.
48
56
 
49
57
  Add the profile to the workflow you already verified:
50
58
 
@@ -60,9 +68,15 @@ export default workflow("accountWorkflow", {
60
68
  ## Commands
61
69
 
62
70
  ```bash
71
+ # Open the site in headed mode and ask the user to log in manually.
72
+ npx libretto open https://app.example.com --headed --session login
73
+
63
74
  # Save the current signed-in session as a named, site-scoped profile.
64
75
  npx libretto save example-app --session login --sites app.example.com,auth.example.com
65
76
 
77
+ # You can now reopen the site locally with that profile.
78
+ npx libretto open https://app.example.com --auth-profile example-app
79
+
66
80
  # List or delete hosted auth profile names.
67
81
  npx libretto cloud profiles list
68
82
  npx libretto cloud profiles delete example-app
@@ -71,3 +85,5 @@ npx libretto cloud profiles delete example-app
71
85
  `save` captures cookies, localStorage, and IndexedDB only for the comma-separated `--sites` list.
72
86
 
73
87
  To reuse an existing signed-in Chrome profile instead of signing in, use `npx libretto import-chrome-profiles`. Get the user's consent first, since attaching can close or relaunch their Chrome window.
88
+
89
+ When the user wants to deploy an authenticated workflow, trace the login flow before writing the workflow: open the site with the target provider, inspect behavior while the human signs in, implement `librettoAuthenticate`, then validate the workflow with the auth profile in the target browser runtime.
@@ -4,7 +4,7 @@ description: "Read-only Libretto workflow for diagnosing live browser state with
4
4
  license: MIT
5
5
  metadata:
6
6
  author: saffron-health
7
- version: "0.6.32"
7
+ version: "0.6.34"
8
8
  ---
9
9
 
10
10
  ## How Libretto Read-Only Works