@beryl-so/cli 0.41.0 → 0.42.0

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.
@@ -10,6 +10,7 @@ import { ApiClient, ApiError } from "../http.js";
10
10
  import { parseArgv } from "./cli.js";
11
11
  import { commands } from "../registry/index.js";
12
12
  import { analyticsOptions, createPostHogClient, setClient, setSurface, shutdownTelemetry, withTool, } from "../telemetry.js";
13
+ import { refreshInstalledSkills } from "../commands/init.js";
13
14
  import { cliVersion, warnIfStale } from "../version-check.js";
14
15
  export function toolName(spec) {
15
16
  return spec.name.replace(/ /g, "_").replace(/-/g, "_");
@@ -292,6 +293,17 @@ export async function serveMcp(baseCtx) {
292
293
  // Fire-and-forget staleness warning: a stale MCP server silently exposes fewer
293
294
  // tools, and stderr is the one channel a stdio MCP server can safely log to.
294
295
  void warnIfStale(cliVersion(), (msg) => console.error(msg));
296
+ // Installed skill copies only refresh on `init`; this server is respawned from @latest
297
+ // every session, so bring them up to this version here. Never fatal.
298
+ try {
299
+ for (const { outcome, file } of refreshInstalledSkills(process.cwd())) {
300
+ if (outcome !== "unchanged")
301
+ console.error(`beryl-test authoring skill ${outcome}: ${file}`);
302
+ }
303
+ }
304
+ catch {
305
+ /* best-effort */
306
+ }
295
307
  const server = new Server({ name: "beryl", version: cliVersion() }, {
296
308
  capabilities: { tools: {} },
297
309
  instructions: mcpInstructions(),
@@ -93,10 +93,10 @@ Then, per test:
93
93
  2. **Write the ActionPlan**, with one real outcome assertion (§2).
94
94
  3. \`beryl tests lint --file plan.json\` — schema check, offline, no network.
95
95
  4. \`beryl tests create --title … --file … --description "<the intent>"\` — replays the
96
- plan locally, banks it only on green; a red replay hands back the failure — fix and
97
- re-run (§3 for the intent, §4 for the loop).
98
- 5. \`beryl runs local\` re-run banked tests on your machine while iterating.
99
- 6. \`beryl runs trigger\` hand it to Beryl's cloud, on demand or on a schedule.
96
+ plan locally, banks it only on green, records no run; a red replay hands back the
97
+ failure — fix and re-run (§3 for the intent, §4 for the loop).
98
+ 5. When the batch is banked, run the new ids together ONCE (\`beryl runs local <id…>\`,
99
+ or \`beryl runs trigger\` for the cloud) so they land as one run with real first results.
100
100
 
101
101
  ## 1. First: who do your tests sign in as?
102
102
 
@@ -285,8 +285,8 @@ ${JSON.stringify(BERYL_TEST_SKILL_EXAMPLE_PLAN, null, 2)
285
285
  --description "<the intent — see §3>"
286
286
  \`\`\`
287
287
  \`create\` banks the plan only on a green local replay; a red one hands back the
288
- failure — fix and re-run. Replay mechanics (where it runs over MCP, \`--no-sync\`,
289
- \`--no-verify\`, the cloud-session exception) are in \`tests create\`'s own
288
+ failure — fix and re-run. It records no run. Replay mechanics (where it runs over
289
+ MCP, \`--no-verify\`, the cloud-session exception) are in \`tests create\`'s own
290
290
  description. Inspect the exact spec with \`beryl tests script --file plan.json\`.
291
291
  Full ActionPlan JSON Schema:
292
292
  https://api.beryl.so/api/v1/schemas/action-plan.schema.json.
@@ -378,14 +378,12 @@ beryl runs local <test-id> --no-sync --url-override http://localhost:3000 --dir
378
378
  beryl runs local # the whole suite, results recorded in Beryl
379
379
  \`\`\`
380
380
 
381
- The loop: draft → \`tests lint\` → \`tests create\` \`runs local --no-sync\` while
382
- iterating (a synced run lands in the project's run history) → read \`--dir\`'s
383
- \`report.json\` to see which step or assertion failed and why → fix the plan →
384
- \`tests set-plan\` run again. Flag semantics, session-mode behavior, \`await_email\`
385
- and authenticated-test handling, and exit codes are in \`runs local\`'s own description.
386
-
387
- Once the test passes locally against a real outcome, it's ready to bank and let Beryl
388
- run and heal it.
381
+ The loop: draft → \`tests lint\` → \`tests create\` (proves, records nothing)
382
+ \`runs local --no-sync\` while iterating → read \`--dir\`'s \`report.json\` to see which
383
+ step or assertion failed and why → fix the plan → \`tests set-plan\` → run again. When
384
+ the batch is done, one synced \`runs local <id…>\` (or \`runs trigger\`) records it as a
385
+ single run. Flag semantics, session-mode behavior, \`await_email\` and
386
+ authenticated-test handling, and exit codes are in \`runs local\`'s own description.
389
387
 
390
388
  ## 5. Testing an OTP / signup flow (\`await_email\`)
391
389
 
@@ -118,6 +118,16 @@ function writeSkills(cwd, scope) {
118
118
  writeSkillFile(skillLeaf(path.join(root, ".claude", "skills"))),
119
119
  ];
120
120
  }
121
+ // The MCP server is respawned from `npx @beryl-so/cli@latest` at every session start, so
122
+ // its startup is the one moment new CLI code reliably runs on a user's machine. Refresh
123
+ // whatever copies `init` installed; never create one (scope is `init`'s call).
124
+ export function refreshInstalledSkills(cwd) {
125
+ const files = new Set([os.homedir(), cwd].flatMap((root) => [
126
+ skillLeaf(path.join(root, ".agents", "skills")),
127
+ skillLeaf(path.join(root, ".claude", "skills")),
128
+ ]));
129
+ return [...files].filter((file) => fs.existsSync(file)).map(writeSkillFile);
130
+ }
121
131
  // Local authoring drives a real browser, and the whole authoring workflow (walk the flow
122
132
  // first, then bank the plan) depends on it — so the install is mandatory, not offered:
123
133
  // missing means install now, no prompt, no opt-out. The runner ships with the CLI; the
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import { CliError, UsageError } from "../errors.js";
3
3
  import { ApiError } from "../http.js";
4
4
  import { lintPlan } from "../lint.js";
5
- import { buildImportForm, establishAccountSession, executeLocalSpec, toRunEntry, } from "../local-exec.js";
5
+ import { establishAccountSession, executeLocalSpec, } from "../local-exec.js";
6
6
  import { PlaywrightMissingError } from "../local-run.js";
7
7
  import { dim, green, red, table, yellow } from "../output.js";
8
8
  import { confirmInstall, installCommandsFor, installPlaywright, playwrightGaps, } from "../playwright-install.js";
@@ -216,8 +216,9 @@ export const testCommands = [
216
216
  "Beryl's; if the Chromium browser is missing there the tool returns the install command " +
217
217
  "(`beryl init` installs it; on a terminal the CLI offers to). " +
218
218
  "A red replay banks NOTHING: the failure evidence comes back (over MCP the screenshot is " +
219
- "image content), you fix the plan file and re-run. The proving run is imported as the " +
220
- "test's first run (--no-sync to skip). A plan that signs in with a session Beryl captured " +
219
+ "image content), you fix the plan file and re-run. No run is recorded: once a batch of " +
220
+ "tests is banked, run their ids together once (`runs local`, or `runs trigger` for the " +
221
+ "cloud) so they land as ONE run with real first results. A plan that signs in with a session Beryl captured " +
221
222
  "server-side cannot replay locally (that session never leaves Beryl's cloud). It falls " +
222
223
  "back to server-side verification automatically, and says so. A session-mode plan replays locally " +
223
224
  "fine: the server renders it with its account's stored sign-in steps in front, so " +
@@ -258,13 +259,6 @@ export const testCommands = [
258
259
  "name is an error (create groups with `beryl groups create`). Omit for no group.",
259
260
  },
260
261
  { name: "env", type: "string", description: "Environment id to compile and prove against" },
261
- {
262
- name: "sync",
263
- type: "boolean",
264
- default: true,
265
- description: "Import the green proving replay as the test's first run (--no-sync: bank only, " +
266
- "no run recorded)",
267
- },
268
262
  {
269
263
  name: "dir",
270
264
  type: "string",
@@ -285,7 +279,6 @@ export const testCommands = [
285
279
  const description = flagStr(input, "description");
286
280
  const urlOverride = flagStr(input, "url-override");
287
281
  const env = flagStr(input, "env");
288
- const sync = input.flags.sync !== false;
289
282
  const groups = flagStrings(input, "group");
290
283
  const bank = (extra) => ctx.client.post(`${projectPath(workspaceId, projectId)}/tests`, {
291
284
  title,
@@ -306,9 +299,6 @@ export const testCommands = [
306
299
  title,
307
300
  base_url: urlOverride,
308
301
  environment_id: env,
309
- // The proof replay becomes the test's first run, so capture the filmstrip
310
- // frames its replay view needs.
311
- frames: sync ? true : undefined,
312
302
  });
313
303
  let compiled = await compile();
314
304
  // Session-mode plan: establish the account's session once (server-side, reused
@@ -370,7 +360,6 @@ export const testCommands = [
370
360
  inlinedLoginSteps: compiled.inlined_login_steps ?? 0,
371
361
  email: compiled.email ?? undefined,
372
362
  });
373
- const startedAt = new Date().toISOString();
374
363
  const attempts = 1 + Math.max(0, compiled.policy.error_retries);
375
364
  let outcome;
376
365
  let runError;
@@ -457,31 +446,13 @@ export const testCommands = [
457
446
  return conflict;
458
447
  throw err;
459
448
  }
460
- let runId;
461
- if (sync) {
462
- const entry = toRunEntry({ ...specOf(compiled.content), id: String(created.id) }, outcome, undefined, startedAt, 0, [...(loginPassword ? [loginPassword] : []), ...(session?.secrets ?? [])]);
463
- const imported = (await ctx.client.request("POST", `${projectPath(workspaceId, projectId)}/runs/import`, {
464
- form: buildImportForm([entry], {
465
- environmentId: env,
466
- targetUrlOverride: urlOverride,
467
- // The proof replay is authoring machinery, not a run the customer
468
- // asked for — it must never email/Slack "your report is ready".
469
- notifications: false,
470
- startedAt,
471
- completedAt: new Date().toISOString(),
472
- onNote: (line) => ctx.err(yellow(`! ${line}`)),
473
- }),
474
- }));
475
- runId = imported.id;
476
- }
477
449
  if (urlOverride) {
478
450
  ctx.err(yellow(`! The proof ran against ${urlOverride}, not the environment's root URL — ` +
479
451
  "the banked test is unproven against its real target until a run there passes."));
480
452
  }
481
453
  return {
482
- data: { ...created, ...(runId ? { proof_run_id: runId } : {}) },
483
- human: green(`✓ Replay passed locally — banked as ${created.id}`) +
484
- (runId ? `\nProving run imported: \`beryl runs get ${runId}\`` : ""),
454
+ data: created,
455
+ human: green(`✓ Replay passed locally — banked as ${created.id}`),
485
456
  };
486
457
  },
487
458
  },
@@ -117,7 +117,6 @@ export function buildImportForm(entries, opts) {
117
117
  const manifest = {
118
118
  environment_id: opts.environmentId ?? null,
119
119
  target_url_override: opts.targetUrlOverride ?? null,
120
- ...(opts.notifications !== undefined ? { notifications: opts.notifications } : {}),
121
120
  started_at: opts.startedAt,
122
121
  completed_at: opts.completedAt,
123
122
  results: entries.map(({ files: _files, ...rest }) => rest),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beryl-so/cli",
3
- "version": "0.41.0",
3
+ "version": "0.42.0",
4
4
  "description": "Beryl on the command line — projects, runs, the exploring agent, and an MCP server over the same commands.",
5
5
  "license": "MIT",
6
6
  "type": "module",