@beryl-so/cli 0.67.0 → 0.73.2
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.
- package/README.md +21 -5
- package/dist/adapters/mcp.js +6 -4
- package/dist/beryl-test-skill.js +79 -88
- package/dist/commands/accounts.js +61 -8
- package/dist/commands/config-vars.js +1 -1
- package/dist/commands/init.js +18 -18
- package/dist/commands/jobs.js +125 -0
- package/dist/commands/projects.js +59 -10
- package/dist/commands/runs.js +3 -2
- package/dist/commands/tests.js +54 -327
- package/dist/commands/watch.js +36 -0
- package/dist/lint.js +0 -6
- package/dist/local-exec.js +4 -4
- package/dist/registry/index.js +2 -0
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -20,8 +20,8 @@ const MCP_SERVER_ENTRY = {
|
|
|
20
20
|
command: "npx",
|
|
21
21
|
args: ["-y", "@beryl-so/cli@latest", "mcp"],
|
|
22
22
|
};
|
|
23
|
-
// Headless: the coding agent drives this browser to
|
|
24
|
-
// window, and a headed default breaks on CI / headless boxes. Matches how most devs
|
|
23
|
+
// Headless: the coding agent drives this browser to set up sign-ins and repair plans; nobody
|
|
24
|
+
// watches the window, and a headed default breaks on CI / headless boxes. Matches how most devs
|
|
25
25
|
// already wire their own user-scoped playwright.
|
|
26
26
|
const PLAYWRIGHT_SERVER_ENTRY = {
|
|
27
27
|
command: "npx",
|
|
@@ -194,7 +194,7 @@ const outcomeLabel = (o) => o === "installed"
|
|
|
194
194
|
? "updated (refreshed to this CLI's version)"
|
|
195
195
|
: "already current";
|
|
196
196
|
const skillLeaf = (root) => path.join(root, BERYL_TEST_SKILL_DIR, BERYL_TEST_SKILL_FILENAME);
|
|
197
|
-
// The
|
|
197
|
+
// The beryl-test skill follows the `--scope` flag, same as the MCP servers: under `user`
|
|
198
198
|
// (the default) it lands in the HOME `.agents/skills/` + `.claude/skills/` dirs, so the
|
|
199
199
|
// knowledge travels with the user into every session the user-scoped MCP tools do —
|
|
200
200
|
// otherwise an agent outside this repo has all the tools and none of the guide. Under
|
|
@@ -230,8 +230,7 @@ export function refreshInstalledSkills(cwd) {
|
|
|
230
230
|
}
|
|
231
231
|
return refreshed;
|
|
232
232
|
}
|
|
233
|
-
// Local
|
|
234
|
-
// first, then bank the plan) depends on it — so the install is mandatory, not offered:
|
|
233
|
+
// Local runs drive a real browser, so the install is mandatory, not offered:
|
|
235
234
|
// missing means install now, no prompt, no opt-out. The runner ships with the CLI; the
|
|
236
235
|
// browser is the per-machine half, fetched for the runner's own revision. Never throws — a
|
|
237
236
|
// failed install must not fail `init`, which has already done its wiring; it prints the
|
|
@@ -253,7 +252,7 @@ async function ensureLocalPlaywright(ctx, cwd) {
|
|
|
253
252
|
}
|
|
254
253
|
catch (err) {
|
|
255
254
|
ctx.err(`${red("✗")} Playwright install failed: ${err.message}`);
|
|
256
|
-
ctx.err(`${dim("•")} Finish the install by hand
|
|
255
|
+
ctx.err(`${dim("•")} Finish the install by hand; local runs need it:\n` +
|
|
257
256
|
` ${cyan(installCommandsFor(gaps, cwd))}`);
|
|
258
257
|
}
|
|
259
258
|
}
|
|
@@ -292,7 +291,7 @@ export const initCommands = [
|
|
|
292
291
|
name: "init",
|
|
293
292
|
summary: "Set up Beryl in this repo: sign in and wire up your coding agent",
|
|
294
293
|
description: "Signs you in, wires the beryl and playwright MCP servers into the coding agent you " +
|
|
295
|
-
"pick, writes the beryl-test
|
|
294
|
+
"pick, writes the beryl-test skill, and installs Chromium if it is missing. " +
|
|
296
295
|
"Pick an agent with --claude, --cursor, --vscode and/or --opencode. With no flag it " +
|
|
297
296
|
"prompts (Enter picks Claude); with no flag and no TTY it wires nothing and prints the " +
|
|
298
297
|
"flags. --claude also wires the Claude DESKTOP app when installed, which needs a quit " +
|
|
@@ -507,7 +506,7 @@ export const initCommands = [
|
|
|
507
506
|
const where = skill.outcome === "updated"
|
|
508
507
|
? `${skillWhere} (refreshed to this CLI's version)`
|
|
509
508
|
: skillWhere;
|
|
510
|
-
ctx.err(`${green("✓")}
|
|
509
|
+
ctx.err(`${green("✓")} beryl-test skill ${what} ${dim(where)}`);
|
|
511
510
|
}
|
|
512
511
|
await ensureLocalPlaywright(ctx, cwd);
|
|
513
512
|
// Name the agent actually wired, not the raw flag: the picker sets `clients` too.
|
|
@@ -519,7 +518,7 @@ export const initCommands = [
|
|
|
519
518
|
? { open: "open VS Code", agent: "Copilot" }
|
|
520
519
|
: { open: "open opencode", agent: "opencode" };
|
|
521
520
|
const nextSteps = clients.size === 0
|
|
522
|
-
? `\n${bold("Beryl is signed in and the
|
|
521
|
+
? `\n${bold("Beryl is signed in and the beryl-test skill is installed.")}\n` +
|
|
523
522
|
` ${dim("• No MCP server is wired yet, so no agent can reach Beryl.")}\n` +
|
|
524
523
|
` ${dim(`• Using ${CLIENT_LABEL_LIST}? Re-run and pick it:`)}\n` +
|
|
525
524
|
` ${cyan("npx @beryl-so/cli@latest init")}\n` +
|
|
@@ -527,10 +526,10 @@ export const initCommands = [
|
|
|
527
526
|
`${cyan(MANUAL_CONFIG_SNIPPET)}\n` +
|
|
528
527
|
` ${dim("Some clients name that key `servers` or `mcp`.")}\n` +
|
|
529
528
|
` ${yellow(`Details: ${MANUAL_SETUP_DOCS}`)}\n`
|
|
530
|
-
: `\n${bold(`Beryl is set up. Now ${ed.open} and ask ${ed.agent} to
|
|
531
|
-
` ${dim(`• Say: "
|
|
532
|
-
` ${dim("
|
|
533
|
-
` ${dim(`•
|
|
529
|
+
: `\n${bold(`Beryl is set up. Now ${ed.open} and ask ${ed.agent} to create tests.`)}\n` +
|
|
530
|
+
` ${dim(`• Say: "create tests for https://example.com from docs/checkout.md" and ${ed.agent} starts`)}\n` +
|
|
531
|
+
` ${dim(" a cloud test-creation job that authors them from that document.")}\n` +
|
|
532
|
+
` ${dim(`• Then ask ${ed.agent} to run them on this machine (local runs) or in the cloud.`)}\n`;
|
|
534
533
|
// The .mcp.json entry is pinned to @latest, but a global install / old npx cache
|
|
535
534
|
// still wins resolution — so tell the user when the CLI they just ran is stale.
|
|
536
535
|
await warnIfStale(cliVersion(), (msg) => ctx.err(yellow(msg)));
|
|
@@ -549,11 +548,12 @@ export const initCommands = [
|
|
|
549
548
|
},
|
|
550
549
|
{
|
|
551
550
|
name: "guide",
|
|
552
|
-
summary: "Print the Beryl test
|
|
553
|
-
description: "The
|
|
554
|
-
"assertions, natural-language intent, test accounts and the
|
|
555
|
-
"({{login_email}}, {{mailbox_address}}, {{inbox_address}} +
|
|
556
|
-
"local run-fix loop. Same content as the beryl-test skill
|
|
551
|
+
summary: "Print the Beryl test guide",
|
|
552
|
+
description: "The guide to durable, healable tests: where new tests come from (`jobs create`), the " +
|
|
553
|
+
"ActionPlan shape, outcome assertions, natural-language intent, test accounts and the " +
|
|
554
|
+
"project mailbox ({{login_email}}, {{mailbox_address}}, {{inbox_address}} + " +
|
|
555
|
+
"await_email), and the local run-fix loop. Same content as the beryl-test skill " +
|
|
556
|
+
"`beryl init` installs. " +
|
|
557
557
|
`Skip if a loaded beryl-test skill states v${cliVersion()}; else call this first ` +
|
|
558
558
|
"(works without logging in).",
|
|
559
559
|
examples: ["beryl guide"],
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { UsageError } from "../errors.js";
|
|
4
|
+
import { arg, flagBool, flagStr, projectPath } from "./util.js";
|
|
5
|
+
const jobsPath = (ws, p) => `${projectPath(ws, p)}/test-creation-jobs`;
|
|
6
|
+
const LLM_NOTICE = "The document is sent to the LLM that drives the agent, so leave out anything you " +
|
|
7
|
+
"would not share with it.";
|
|
8
|
+
function readDocument(file) {
|
|
9
|
+
try {
|
|
10
|
+
return new Uint8Array(fs.readFileSync(file));
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
const code = err.code;
|
|
14
|
+
if (code === "ENOENT")
|
|
15
|
+
throw new UsageError(`file not found: ${file}`);
|
|
16
|
+
throw new UsageError(`cannot read ${file} (${code ?? err.message})`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function jobHuman(job) {
|
|
20
|
+
const lines = [`Job ${job.id}: ${job.status}`];
|
|
21
|
+
if (job.error)
|
|
22
|
+
lines.push(`Error: ${job.error}`);
|
|
23
|
+
for (const t of job.tests)
|
|
24
|
+
lines.push(` ${t.title ?? t.id} [${t.surface}] ${t.id}`);
|
|
25
|
+
if (!job.tests.length)
|
|
26
|
+
lines.push(" No tests yet.");
|
|
27
|
+
const skipped = job.skipped_flows ?? [];
|
|
28
|
+
if (skipped.length)
|
|
29
|
+
lines.push("Skipped scenarios:");
|
|
30
|
+
for (const s of skipped)
|
|
31
|
+
lines.push(` ${s.handle}: ${s.reason}`);
|
|
32
|
+
return lines.join("\n");
|
|
33
|
+
}
|
|
34
|
+
export const jobCommands = [
|
|
35
|
+
{
|
|
36
|
+
name: "jobs create",
|
|
37
|
+
groupSummary: "Test-creation jobs: the cloud agent reads a document and authors tests from it.",
|
|
38
|
+
summary: "Create tests from a document (PRD, release notes) on Beryl's cloud",
|
|
39
|
+
description: "Uploads a document (Markdown, plain text, PDF or DOCX, or --text) and queues a " +
|
|
40
|
+
"job: the agent explores the environment, drafts one flow per scenario the document " +
|
|
41
|
+
"describes, and authors one test per enabled surface of the project. New tests get " +
|
|
42
|
+
"no groups. A job runs once; to run it again, use `jobs clone`. " +
|
|
43
|
+
LLM_NOTICE,
|
|
44
|
+
scope: "project",
|
|
45
|
+
args: [{ name: "file", description: "Path to the document" }],
|
|
46
|
+
flags: [
|
|
47
|
+
{ name: "text", type: "string", description: "The document as text, instead of a file" },
|
|
48
|
+
{
|
|
49
|
+
name: "env",
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "Environment id to explore (defaults to the project's default)",
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
examples: ["beryl jobs create ./release-notes.md --env <env-id>"],
|
|
55
|
+
async run(ctx, input) {
|
|
56
|
+
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
57
|
+
const file = input.args.file;
|
|
58
|
+
const text = flagStr(input, "text");
|
|
59
|
+
if (Boolean(file) === Boolean(text)) {
|
|
60
|
+
throw new UsageError("pass a document file or --text, not both");
|
|
61
|
+
}
|
|
62
|
+
const form = new FormData();
|
|
63
|
+
if (file)
|
|
64
|
+
form.set("file", new Blob([readDocument(file)]), path.basename(file));
|
|
65
|
+
else
|
|
66
|
+
form.set("text", text);
|
|
67
|
+
const envId = flagStr(input, "env");
|
|
68
|
+
if (envId)
|
|
69
|
+
form.set("environment_id", envId);
|
|
70
|
+
const job = (await ctx.client.request("POST", jobsPath(workspaceId, projectId), {
|
|
71
|
+
form,
|
|
72
|
+
}));
|
|
73
|
+
return {
|
|
74
|
+
data: job,
|
|
75
|
+
human: `Job ${job.id} queued. Check it with \`beryl jobs get ${job.id}\`.`,
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "jobs list",
|
|
81
|
+
summary: "List the project's test-creation jobs",
|
|
82
|
+
scope: "project",
|
|
83
|
+
async run(ctx, input) {
|
|
84
|
+
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
85
|
+
return { data: await ctx.client.get(jobsPath(workspaceId, projectId)) };
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "jobs get",
|
|
90
|
+
summary: "Show a job's status and the tests it created",
|
|
91
|
+
scope: "project",
|
|
92
|
+
args: [{ name: "job-id", description: "Job id", required: true }],
|
|
93
|
+
async run(ctx, input) {
|
|
94
|
+
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
95
|
+
const job = (await ctx.client.get(`${jobsPath(workspaceId, projectId)}/${arg(input, "job-id")}`));
|
|
96
|
+
return { data: job, human: jobHuman(job) };
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "jobs clone",
|
|
101
|
+
summary: "Run a job again: queue a new job with the same document and environment",
|
|
102
|
+
description: LLM_NOTICE,
|
|
103
|
+
scope: "project",
|
|
104
|
+
args: [{ name: "job-id", description: "Job id to clone", required: true }],
|
|
105
|
+
async run(ctx, input) {
|
|
106
|
+
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
107
|
+
const job = (await ctx.client.post(`${jobsPath(workspaceId, projectId)}/${arg(input, "job-id")}/clone`));
|
|
108
|
+
return { data: job, human: `Job ${job.id} queued.` };
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "jobs delete",
|
|
113
|
+
summary: "Delete a job; the flows and tests it created stay",
|
|
114
|
+
scope: "project",
|
|
115
|
+
args: [{ name: "job-id", description: "Job id", required: true }],
|
|
116
|
+
flags: [{ name: "force", type: "boolean", description: "Skip the confirmation prompt" }],
|
|
117
|
+
async run(ctx, input) {
|
|
118
|
+
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
119
|
+
const jobId = arg(input, "job-id");
|
|
120
|
+
await ctx.confirm(`Delete job ${jobId}? Its tests stay.`, flagBool(input, "force"));
|
|
121
|
+
await ctx.client.del(`${jobsPath(workspaceId, projectId)}/${jobId}`);
|
|
122
|
+
return { human: "Deleted." };
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { detectAuthGating } from "../detect.js";
|
|
2
2
|
import { UsageError } from "../errors.js";
|
|
3
3
|
import { dim, green, table, timeAgo } from "../output.js";
|
|
4
|
-
import { arg, flagBool, flagNum, flagStr, projectPath } from "./util.js";
|
|
4
|
+
import { arg, argList, flagBool, flagNum, flagStr, projectPath } from "./util.js";
|
|
5
5
|
import { pollCurrentExploration, watchExploration } from "./watch.js";
|
|
6
6
|
// The API treats the auth choice as the visitor's answer and never infers it — a null
|
|
7
7
|
// choice leaves the project waiting on a login capture that never comes — so resolve it
|
|
@@ -84,7 +84,7 @@ export const projectCommands = [
|
|
|
84
84
|
name: "name",
|
|
85
85
|
type: "string",
|
|
86
86
|
description: "Name for an empty project when no URL is given. Add a URL later with " +
|
|
87
|
-
"`beryl envs update <env-id
|
|
87
|
+
"`beryl envs update <env-id>`",
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
90
|
name: "auth",
|
|
@@ -97,9 +97,9 @@ export const projectCommands = [
|
|
|
97
97
|
{
|
|
98
98
|
name: "explore",
|
|
99
99
|
type: "boolean",
|
|
100
|
-
description: "Also start a cloud exploration that authors tests for you. Off by default:
|
|
101
|
-
"tests with `beryl
|
|
102
|
-
mcpDescription: "Leave unset:
|
|
100
|
+
description: "Also start a cloud exploration that authors tests for you. Off by default: create " +
|
|
101
|
+
"tests from a document with `beryl jobs create`",
|
|
102
|
+
mcpDescription: "Leave unset: create tests from a document with `jobs create`. Set only when the " +
|
|
103
103
|
"user explicitly asks for a cloud exploration",
|
|
104
104
|
},
|
|
105
105
|
{ name: "watch", type: "boolean", description: "Start the exploration (implies --explore) and stream it live" },
|
|
@@ -120,7 +120,7 @@ export const projectCommands = [
|
|
|
120
120
|
const url = typeof rawUrl === "string" ? rawUrl : "";
|
|
121
121
|
const name = flagStr(input, "name");
|
|
122
122
|
// Name-only: create a real empty project (no URL, no exploration). Set a URL
|
|
123
|
-
// later with `beryl envs update <env-id
|
|
123
|
+
// later with `beryl envs update <env-id>`.
|
|
124
124
|
if (!url) {
|
|
125
125
|
if (!name)
|
|
126
126
|
throw new UsageError("Provide a URL, or --name to create an empty project");
|
|
@@ -132,8 +132,8 @@ export const projectCommands = [
|
|
|
132
132
|
return {
|
|
133
133
|
data: created,
|
|
134
134
|
human: `${green("Project created")}: ${created.project_id} ${dim("(empty)")}\n` +
|
|
135
|
-
`Add a site with \`beryl envs update <env-id>\`,
|
|
136
|
-
`\`beryl
|
|
135
|
+
`Add a site with \`beryl envs update <env-id>\`, then create tests with ` +
|
|
136
|
+
`\`beryl jobs create <document>\`.`,
|
|
137
137
|
};
|
|
138
138
|
}
|
|
139
139
|
const auth = flagStr(input, "auth") ?? (await resolveAuthChoice(ctx, ws, url));
|
|
@@ -147,7 +147,7 @@ export const projectCommands = [
|
|
|
147
147
|
return {
|
|
148
148
|
data: created,
|
|
149
149
|
human: `${green("Project created")}: ${created.project_id}\n` +
|
|
150
|
-
`
|
|
150
|
+
`Create tests from a document describing the flows: \`beryl jobs create <file>\`.`,
|
|
151
151
|
};
|
|
152
152
|
// The agent cannot start until a login is captured, so there is nothing to watch.
|
|
153
153
|
if (created.auth_status === "needs_capture")
|
|
@@ -227,6 +227,55 @@ export const projectCommands = [
|
|
|
227
227
|
return { data: await ctx.client.patch(projectPath(workspaceId, projectId), body) };
|
|
228
228
|
},
|
|
229
229
|
},
|
|
230
|
+
{
|
|
231
|
+
name: "projects surfaces",
|
|
232
|
+
summary: "Set which surfaces are enabled for this project",
|
|
233
|
+
description: "Replaces the project's enabled surfaces with the ones listed: web-desktop, " +
|
|
234
|
+
"web-tablet, web-mobile. A new project has web-desktop only. At least one is " +
|
|
235
|
+
"required; `projects get` shows the current set. Turning a surface off removes " +
|
|
236
|
+
"no tests: delete a surface's tests to stop running them.",
|
|
237
|
+
scope: "project",
|
|
238
|
+
args: [
|
|
239
|
+
{
|
|
240
|
+
name: "surfaces",
|
|
241
|
+
description: "web-desktop | web-tablet | web-mobile, one or more",
|
|
242
|
+
required: true,
|
|
243
|
+
variadic: true,
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
examples: ["beryl projects surfaces web-desktop web-mobile", "beryl projects surfaces web-desktop"],
|
|
247
|
+
async run(ctx, input) {
|
|
248
|
+
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
249
|
+
const data = (await ctx.client.patch(projectPath(workspaceId, projectId), {
|
|
250
|
+
enabled_surfaces: argList(input, "surfaces"),
|
|
251
|
+
}));
|
|
252
|
+
return { data, human: `Enabled surfaces: ${data.enabled_surfaces.join(", ")}` };
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: "projects findings-gate",
|
|
257
|
+
summary: "Turn the findings gate on each run on or off",
|
|
258
|
+
description: "With the gate on (new-since-green), every run also reports a findings gate: failed " +
|
|
259
|
+
"when the environment's Site Health has a finding first seen after the last green run " +
|
|
260
|
+
"or came back after it (every current finding if there is no green run yet), unknown " +
|
|
261
|
+
"when Site Health has not run there or is not recording findings (its issue tracker " +
|
|
262
|
+
"must be connected). The gate sits next to the test verdicts and never changes one, nor the " +
|
|
263
|
+
"exit code of `runs watch`; read it from `findings_gate` on `runs get`. Off by default.",
|
|
264
|
+
scope: "project",
|
|
265
|
+
args: [{ name: "mode", description: "off | new-since-green", required: true }],
|
|
266
|
+
examples: ["beryl projects findings-gate new-since-green", "beryl projects findings-gate off"],
|
|
267
|
+
async run(ctx, input) {
|
|
268
|
+
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
269
|
+
const mode = arg(input, "mode").toLowerCase().replace(/-/g, "_");
|
|
270
|
+
if (mode !== "off" && mode !== "new_since_green") {
|
|
271
|
+
throw new UsageError(`Expected "off" or "new-since-green", got "${arg(input, "mode")}"`);
|
|
272
|
+
}
|
|
273
|
+
const data = (await ctx.client.patch(projectPath(workspaceId, projectId), {
|
|
274
|
+
findings_gate: mode,
|
|
275
|
+
}));
|
|
276
|
+
return { data, human: `Findings gate: ${data.findings_gate.replace(/_/g, "-")}` };
|
|
277
|
+
},
|
|
278
|
+
},
|
|
230
279
|
{
|
|
231
280
|
name: "projects delete",
|
|
232
281
|
summary: "Delete a project and all its tests and runs",
|
|
@@ -258,7 +307,7 @@ export const projectCommands = [
|
|
|
258
307
|
},
|
|
259
308
|
{
|
|
260
309
|
name: "projects report",
|
|
261
|
-
summary: "Aggregate quality report across recent runs (pass rates,
|
|
310
|
+
summary: "Aggregate quality report across recent runs (pass rates, per-test history, trend)",
|
|
262
311
|
scope: "project",
|
|
263
312
|
flags: [
|
|
264
313
|
{ name: "runs", type: "number", description: "How many recent runs to aggregate" },
|
package/dist/commands/runs.js
CHANGED
|
@@ -581,7 +581,8 @@ export const runCommands = [
|
|
|
581
581
|
"carries `heal` when Beryl healed it: its steps were rewritten with the intent " +
|
|
582
582
|
"unchanged, proved in a browser and it re-ran green; `version_no` is the revision it " +
|
|
583
583
|
"wrote (see `tests diff`) and `rationale` the agent's reason. A heal that did not turn " +
|
|
584
|
-
"the test green is not reported: a failure is a failure. " +
|
|
584
|
+
"the test green is not reported: a failure is a failure. A red that Beryl did not try " +
|
|
585
|
+
"to heal carries `heal_skip_reason` saying why. " +
|
|
585
586
|
"`runs report` returns the generated report document, `runs explain` an AI explanation " +
|
|
586
587
|
"of one failed result.",
|
|
587
588
|
scope: "project",
|
|
@@ -607,7 +608,7 @@ export const runCommands = [
|
|
|
607
608
|
name: "status",
|
|
608
609
|
type: "string",
|
|
609
610
|
description: "With --page, show only this bucket: passed, failed, errored, cancelled, " +
|
|
610
|
-
"
|
|
611
|
+
"or quarantined",
|
|
611
612
|
},
|
|
612
613
|
{
|
|
613
614
|
name: "group",
|