metergraph-cli 0.2.3 → 0.2.4

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 (59) hide show
  1. package/README.md +78 -14
  2. package/assets/skill/SKILL.md +124 -31
  3. package/assets/skill/manifest.json +3 -3
  4. package/assets/skills/manifest.json +31 -31
  5. package/assets/skills/metergraph-analyze/SKILL.md +161 -58
  6. package/assets/skills/metergraph-candidates/SKILL.md +50 -8
  7. package/assets/skills/metergraph-evals/SKILL.md +83 -16
  8. package/assets/skills/metergraph-investigate/SKILL.md +90 -34
  9. package/assets/skills/metergraph-iterate/SKILL.md +82 -12
  10. package/assets/skills/metergraph-local-bundle/SKILL.md +146 -128
  11. package/assets/skills/metergraph-model-swap/SKILL.md +142 -20
  12. package/assets/skills/metergraph-onboarding/SKILL.md +93 -49
  13. package/assets/skills/metergraph-report/SKILL.md +60 -15
  14. package/assets/skills/metergraph-workloads/SKILL.md +97 -13
  15. package/native/README.md +85 -0
  16. package/native/anchored-fs.c +243 -0
  17. package/native/bin/anchored-fs-darwin-arm64 +0 -0
  18. package/native/bin/anchored-fs-darwin-x64 +0 -0
  19. package/native/bin/anchored-fs-linux-arm64 +0 -0
  20. package/native/bin/anchored-fs-linux-x64 +0 -0
  21. package/native/build.mjs +55 -0
  22. package/native/manifest.json +9 -0
  23. package/native/targets.mjs +24 -0
  24. package/package.json +10 -3
  25. package/src/action-help.js +130 -0
  26. package/src/agent-key.js +21 -14
  27. package/src/anchored-fs.js +163 -0
  28. package/src/args.js +125 -17
  29. package/src/auth-binding.js +12 -7
  30. package/src/auth-callback.js +10 -2
  31. package/src/auth-login.js +111 -28
  32. package/src/auth-oauth.js +89 -45
  33. package/src/auth-session.js +19 -12
  34. package/src/auth-store.js +28 -7
  35. package/src/cli.js +52 -8
  36. package/src/constants.js +37 -9
  37. package/src/experiments-output.js +7 -6
  38. package/src/experiments.js +26 -17
  39. package/src/output.js +274 -79
  40. package/src/read-contract.js +42 -8
  41. package/src/read-output.js +66 -13
  42. package/src/read.js +90 -15
  43. package/src/repository-identity.js +2 -2
  44. package/src/setup-env-git.js +1 -1
  45. package/src/setup-env.js +16 -9
  46. package/src/setup-interactive.js +19 -4
  47. package/src/setup-refusal.js +2 -2
  48. package/src/setup-state.js +9 -4
  49. package/src/setup.js +15 -3
  50. package/src/skill-bundle.js +2 -2
  51. package/src/skill-trusted.js +56 -0
  52. package/src/skill.js +28 -9
  53. package/src/style.js +33 -7
  54. package/src/tools-output.js +73 -0
  55. package/src/tools.js +249 -0
  56. package/src/trace-contract.js +1 -1
  57. package/src/transport.js +21 -0
  58. package/src/verify-output.js +2 -2
  59. package/src/verify.js +5 -2
package/src/cli.js CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  import { runRead } from "./read.js";
20
20
  import { runExperimentStart } from "./experiments.js";
21
21
  import { experimentsMessage, experimentsText } from "./experiments-output.js";
22
+ import { toolsMessage, toolsText } from "./tools-output.js";
22
23
  import { readMessage, readText } from "./read-output.js";
23
24
  import { listSkillPack, runSkill, runSkillPack } from "./skill.js";
24
25
  import { runSetup } from "./setup.js";
@@ -29,6 +30,7 @@ import {
29
30
  canAsk,
30
31
  commandLine,
31
32
  fillSetupArgv,
33
+ fillLoginArgv,
32
34
  offerAgentAccess,
33
35
  terminalPrompter,
34
36
  } from "./setup-interactive.js";
@@ -63,7 +65,12 @@ async function run(argv, { stdout, stderr, stdin }) {
63
65
  let parsed = parseArgs(argv);
64
66
  const style = createStyle(stdout);
65
67
  const say = (...lines) => stdout.write(lines.map((line) => `${line}\n`).join(""));
66
- const asking = canAsk(argv, { stdin, stdout });
68
+ const asking = canAsk(argv, { stdin, stdout, stderr });
69
+
70
+ if (asking && parsed.command === "login") {
71
+ argv = fillLoginArgv(argv);
72
+ parsed = parseArgs(argv);
73
+ }
67
74
 
68
75
  // A person at a terminal is asked for what setup is missing; the answers
69
76
  // become flags and go through the same parsing as a script's.
@@ -95,13 +102,20 @@ async function run(argv, { stdout, stderr, stdin }) {
95
102
  message: parsed.message,
96
103
  });
97
104
  if (parsed.json) stdout.write(toJsonLine(result));
98
- else stderr.write(`${createStyle(stderr).red("Error:")} ${parsed.message}\n`);
105
+ else {
106
+ const errorStyle = createStyle(stderr);
107
+ const [first, ...rest] = paragraph(errorStyle, `Error: ${parsed.message}`, "");
108
+ const help = parsed.command && parsed.command !== "help"
109
+ ? `metergraph ${parsed.command} --help` : "metergraph --help";
110
+ stderr.write(render([first.replace(/^Error:/, errorStyle.red("Error:")), ...rest,
111
+ "", ...steps(errorStyle, [["Help", help]])]));
112
+ }
99
113
  return result;
100
114
  }
101
115
 
102
116
  if (parsed.command === "help") {
103
- const result = envelope({ command: "help", outcome: "ok", data: helpData(parsed.topic) });
104
- stdout.write(parsed.json ? toJsonLine(result) : helpText(parsed.topic, style));
117
+ const result = envelope({ command: "help", outcome: "ok", data: helpData(parsed.topic, parsed.action) });
118
+ stdout.write(parsed.json ? toJsonLine(result) : helpText(parsed.topic, style, parsed.action, parsed.advanced));
105
119
  return result;
106
120
  }
107
121
 
@@ -181,7 +195,7 @@ async function run(argv, { stdout, stderr, stdin }) {
181
195
  }
182
196
 
183
197
  if (READ_COMMANDS.includes(parsed.command)) {
184
- const { outcome, reason, data } = await runRead(parsed);
198
+ const { outcome, reason, data, validateOutput } = await runRead(parsed);
185
199
  const message = readMessage(outcome, reason);
186
200
  const result = envelope({
187
201
  command: parsed.command,
@@ -190,14 +204,16 @@ async function run(argv, { stdout, stderr, stdin }) {
190
204
  data,
191
205
  message: outcome === "ok" ? null : message,
192
206
  });
193
- stdout.write(parsed.json ? toJsonLine(result) : readText(result, message, style));
207
+ const text = parsed.json ? toJsonLine(result) : readText(result, message, style);
208
+ if (validateOutput && !validateOutput(text)) return refuseCredentialOutput(parsed, stdout, style, validateOutput);
209
+ stdout.write(text);
194
210
  return result;
195
211
  }
196
212
 
197
213
  if (parsed.command === "experiments") {
198
214
  // start is the one write: its own key session (experiments.js). The other
199
215
  // actions are Metadata reads through runRead's shared session.
200
- const { outcome, reason, data } = parsed.action === "start" ? await runExperimentStart(parsed) : await runRead(parsed);
216
+ const { outcome, reason, data, validateOutput } = parsed.action === "start" ? await runExperimentStart(parsed) : await runRead(parsed);
201
217
  const message = experimentsMessage(outcome, reason);
202
218
  const result = envelope({
203
219
  command: `experiments ${parsed.action}`,
@@ -206,7 +222,26 @@ async function run(argv, { stdout, stderr, stdin }) {
206
222
  data,
207
223
  message: outcome === "ok" ? null : message,
208
224
  });
209
- stdout.write(parsed.json ? toJsonLine(result) : experimentsText(result, message, style, parsed.action));
225
+ const text = parsed.json ? toJsonLine(result) : experimentsText(result, message, style, parsed.action);
226
+ if (validateOutput && !validateOutput(text)) return refuseCredentialOutput(parsed, stdout, style, validateOutput);
227
+ stdout.write(text);
228
+ return result;
229
+ }
230
+
231
+ if (parsed.command === "tools") {
232
+ // Any Agent Access tool, through runRead's verified session (tools.js).
233
+ const { outcome, reason, data, validateOutput } = await runRead(parsed);
234
+ const message = toolsMessage(outcome, reason);
235
+ const result = envelope({
236
+ command: `tools ${parsed.action}`,
237
+ outcome,
238
+ reason,
239
+ data,
240
+ message: outcome === "ok" ? null : message,
241
+ });
242
+ const text = parsed.json ? toJsonLine(result) : toolsText(result, message, style, parsed.action);
243
+ if (validateOutput && !validateOutput(text)) return refuseCredentialOutput(parsed, stdout, style, validateOutput);
244
+ stdout.write(text);
210
245
  return result;
211
246
  }
212
247
 
@@ -390,3 +425,12 @@ function repositoryValue(repository) {
390
425
  default: return "not recorded (no owner/name git remote; pass --repository OWNER/NAME)";
391
426
  }
392
427
  }
428
+
429
+ function refuseCredentialOutput(parsed, stdout, style, validateOutput) {
430
+ const reason = "credential_in_metadata_response";
431
+ const message = readMessage("verification_failed", reason);
432
+ const result = envelope({ command: parsed.command, outcome: "verification_failed", reason, message });
433
+ const text = parsed.json ? toJsonLine(result) : `${message}\n`;
434
+ if (validateOutput(text)) stdout.write(text);
435
+ return result;
436
+ }
package/src/constants.js CHANGED
@@ -64,9 +64,38 @@ export const LOGIN_MIN_TIMEOUT_MS = 1000;
64
64
  export const LOGIN_MAX_TIMEOUT_MS = 900000;
65
65
  export const AUTH_HTTP_TIMEOUT_MS = 15000;
66
66
 
67
- // The only scope this CLI requests. It never asks for agent:read (Debug) or
67
+ // The scope sign in requests. It never asks for agent:read (Debug) or
68
68
  // agent:replay, and never falls back to them.
69
69
  export const METADATA_SCOPE = "agent:metadata";
70
+ // The Debug role and the workflow permissions an owner adds to it, by the
71
+ // names the Keys page uses. Writing and testing evaluations and starting
72
+ // analyses need Debug with all three. Replay is never used by this CLI.
73
+ export const DEBUG_SCOPE = "agent:read";
74
+ export const REPLAY_SCOPE = "agent:replay";
75
+ export const WORKFLOW_PERMISSIONS = Object.freeze([
76
+ Object.freeze({ field: "allow_evaluation_writes", scope: "agent:eval-write", label: "Allow evaluation writes" }),
77
+ Object.freeze({ field: "allow_evaluation_previews", scope: "agent:eval-preview", label: "Allow evaluation previews" }),
78
+ Object.freeze({ field: "allow_analysis_starts", scope: "agent:analysis-run", label: "Allow analysis starts" }),
79
+ ]);
80
+ export const WORKFLOW_SCOPES = Object.freeze(WORKFLOW_PERMISSIONS.map(({ scope }) => scope).sort());
81
+ // The permissions are offered where the deployment reports managed
82
+ // evaluations; customer-owned AWS (byoc-core) and the open-source server do not.
83
+ export const WORKFLOW_DEPLOYMENT_CAPABILITY = "evaluations";
84
+
85
+ // The scope login asks for and a saved sign in records: Metadata, or Debug
86
+ // followed by the chosen workflow permissions in WORKFLOW_SCOPES order.
87
+ export function loginScope(role, permissions = []) {
88
+ if (role !== "debug") return METADATA_SCOPE;
89
+ return [DEBUG_SCOPE, ...WORKFLOW_SCOPES.filter((scope) => permissions.includes(scope))].join(" ");
90
+ }
91
+
92
+ // Whether a saved sign in's scope is one login asks for, in that exact form.
93
+ export function isLoginScope(scope) {
94
+ if (scope === METADATA_SCOPE) return true;
95
+ if (typeof scope !== "string") return false;
96
+ const [first, ...rest] = scope.split(" ");
97
+ return first === DEBUG_SCOPE && scope === loginScope("debug", rest);
98
+ }
70
99
 
71
100
  // The service's agent access contract version, as sent in schema_version by
72
101
  // GET /v1/agent/workspace and GET /v1/agent/capabilities. It is the
@@ -134,20 +163,19 @@ export const USAGE_COMPARISONS = Object.freeze(["previous", "previous-same-time"
134
163
  // Experiments: replay a workload's recent production calls on 1 to 3
135
164
  // treatments and compare them with production. The list, estimate and
136
165
  // summary are Metadata reads at these fixed paths (one experiment is
137
- // EXPERIMENT_PATHS.list plus "/" and its UUID). Starting one is the only
138
- // write this CLI makes to a workspace: it needs a separate coding-agent key
139
- // with Debug access and the EXPERIMENT_START_SCOPE grant, used for that one
140
- // request and nothing else. The limits match the service's own.
166
+ // EXPERIMENT_PATHS.list plus "/" and its UUID). Starting one needs a
167
+ // coding-agent key with Debug access and the EXPERIMENT_START_SCOPE grant.
168
+ // The limits match the service's own.
141
169
  export const EXPERIMENT_PATHS = Object.freeze({
142
170
  list: "/v1/agent/experiments",
143
171
  estimate: "/v1/agent/experiments/estimate",
144
172
  start: "/v1/agent/experiments",
145
173
  });
146
174
  export const EXPERIMENT_START_SCOPE = "agent:analysis-run";
147
- // Exactly the scopes a start key carries: Debug and the start grant. A key
148
- // with any other scope (Replay, other workflow grants, Metadata, ingest) is
149
- // refused, so the key in an agent's environment can do no more than this.
150
- export const EXPERIMENT_START_KEY_SCOPES = Object.freeze(["agent:read", EXPERIMENT_START_SCOPE]);
175
+ // The scopes a start key must carry: Debug and the start grant. It may also
176
+ // carry the other workflow permissions; a key with
177
+ // Replay, Metadata only or any other scope is refused.
178
+ export const EXPERIMENT_START_KEY_SCOPES = Object.freeze([DEBUG_SCOPE, EXPERIMENT_START_SCOPE]);
151
179
  export const EXPERIMENT_LAST_N = Object.freeze({ min: 10, max: 40, default: 20 });
152
180
  export const EXPERIMENT_MAX_TREATMENTS = 3;
153
181
  export const EXPERIMENT_MAX_TRACKED_FIELDS = 10;
@@ -1,6 +1,6 @@
1
1
  import { STATISTICS } from "./experiments-contract.js";
2
2
  import { readMessage } from "./read-output.js";
3
- import { PLAIN, fields, headline, paragraph, render, steps, toneFor } from "./style.js";
3
+ import { PLAIN, fields, headline, paragraph, render, steps, toneFor, workspaceShown } from "./style.js";
4
4
 
5
5
  // Fixed text for experiments results. Nothing here contains a value from a
6
6
  // service response other than validated metadata; the service's own refusal
@@ -11,11 +11,12 @@ const MESSAGES = {
11
11
  "This deployment does not offer experiments to this credential, for example because the feature is off. " +
12
12
  "Nothing was read or started.",
13
13
  experiment_start_key_required:
14
- "Starting an experiment needs a coding-agent key with Debug access and Allow analysis starts, in " +
15
- "--agent-token-file or METERGRAPH_AGENT_TOKEN. A saved sign in is Metadata only. Nothing was sent.",
14
+ "Starting an experiment needs a coding-agent key with the Debug role and Allow analysis starts, in " +
15
+ "--agent-token-file or METERGRAPH_AGENT_TOKEN. A saved sign in is Metadata only. " +
16
+ "Nothing was sent.",
16
17
  experiment_start_not_granted:
17
- "This key cannot start experiments: it needs exactly Debug access and the Allow analysis starts grant from a " +
18
- "workspace owner, and nothing more. Nothing was started.",
18
+ "This key cannot start experiments: it needs Debug access and the Allow analysis starts grant from a " +
19
+ "workspace owner, and no Replay access. Nothing was started.",
19
20
  experiment_start_unavailable:
20
21
  "The service does not offer experiment starts to this key, for example because the feature is off or the " +
21
22
  "granting owner is no longer an owner. Nothing was started.",
@@ -165,7 +166,7 @@ export function experimentsText(result, message, style = PLAIN, action) {
165
166
  style.dim(action === "start" ? "Replays captured prompts within the server's budget" : "Metadata only")}`,
166
167
  "",
167
168
  ];
168
- const rows = [["Origin", data.origin], ["Workspace", data.workspace?.id ?? null]];
169
+ const rows = [["Origin", data.origin], ["Workspace", workspaceShown(data.workspace)]];
169
170
  if (result.ok) {
170
171
  const [more, body] = resultLines(action, data.result);
171
172
  lines.push(...fields(style, [...rows, ...more]));
@@ -1,6 +1,6 @@
1
1
  import { agentKeySource, keySession, keyTarget, readAgentKey } from "./agent-key.js";
2
2
  import { readBinding, resolveProject } from "./auth-binding.js";
3
- import { Stop, openStore, readCredential, resolveConfigDir } from "./auth-store.js";
3
+ import { Stop, openStore, readProjectCredential, resolveConfigDir } from "./auth-store.js";
4
4
  import { DEFAULT_ORIGIN, EXPERIMENT_PATHS, EXPERIMENT_START_SCOPE, METADATA_SCOPE, READ_MAX_BYTES } from "./constants.js";
5
5
  import { AGENT_URL_ENV, classifyEnvironment, handoffPages } from "./environment.js";
6
6
  import {
@@ -12,7 +12,8 @@ import {
12
12
  } from "./experiments-contract.js";
13
13
  import { parseJsonObject } from "./http.js";
14
14
  import { parseOrigin } from "./origin.js";
15
- import { holdsKnown } from "./read.js";
15
+ import { workspaceLabel, workspacePinned } from "./read-contract.js";
16
+ import { holdsKnown, outputHoldsKnown } from "./read.js";
16
17
  import { deadline, failureOf, send, trapSignals } from "./transport.js";
17
18
 
18
19
  // Experiments: replay a workload's recent production calls on 1 to 3
@@ -20,15 +21,15 @@ import { deadline, failureOf, send, trapSignals } from "./transport.js";
20
21
  //
21
22
  // list, estimate and get are Metadata reads. runRead (read.js) establishes
22
23
  // their session exactly as for every other read command, a saved grant or a
23
- // Metadata key, and calls experimentRead with it. They send only these
24
+ // coding-agent key, and calls experimentRead with it. They send only these
24
25
  // requests and print only validated fields; no input, output or tracked
25
26
  // field value is ever requested.
26
27
  //
27
- // start is the only command that changes a workspace. It needs a separate
28
- // coding-agent key with Debug access and the agent:analysis-run grant, from
29
- // --agent-token-file or METERGRAPH_AGENT_TOKEN, verified for exactly that
30
- // (auth-oauth.js checkStartWorkspace and checkStartCapabilities) before it is
31
- // used for this one POST. A saved sign in is Metadata only and never starts
28
+ // start changes a workspace. It needs a coding-agent key with Debug access
29
+ // and the agent:analysis-run grant (Allow analysis starts), from
30
+ // --agent-token-file or METERGRAPH_AGENT_TOKEN, verified before it is used
31
+ // for this one POST (auth-oauth.js checkStartWorkspace and
32
+ // checkStartCapabilities). A saved sign in is Metadata only and never starts
32
33
  // one. Each start creates a new experiment, so a start that succeeded is not
33
34
  // retried.
34
35
 
@@ -167,8 +168,8 @@ function pollOrOpen(experiment, { started = false } = {}) {
167
168
  return {
168
169
  kind: "poll_experiment",
169
170
  message: started
170
- ? `The experiment is ${experiment.status}. Poll it in about ${POLL_AFTER_SECONDS} seconds with a Metadata key ` +
171
- "or this project's sign in; the start key is not used for reads."
171
+ ? `The experiment is ${experiment.status}. Poll it in about ${POLL_AFTER_SECONDS} seconds with ` +
172
+ "this project's sign in or a coding-agent key."
172
173
  : `The experiment is ${experiment.status}. Run the same command again in about ${POLL_AFTER_SECONDS} seconds.`,
173
174
  command: `metergraph experiments get --id ${experiment.id} --json`,
174
175
  poll_after_seconds: POLL_AFTER_SECONDS,
@@ -217,6 +218,7 @@ export async function runExperimentStart(options) {
217
218
  origin: null,
218
219
  workspaceId: null,
219
220
  intendedWorkspace: null,
221
+ workspaceLabel: { slug: null, name: null },
220
222
  profile: null,
221
223
  authenticated: false,
222
224
  scopes: [],
@@ -234,9 +236,11 @@ export async function runExperimentStart(options) {
234
236
  const data = startData(ctx, notices, outcome, report);
235
237
  if (holdsKnown(data, known)) {
236
238
  return { outcome: "verification_failed", reason: "credential_in_metadata_response",
237
- data: startData({ ...ctx, reason: "credential_in_metadata_response" }, new Set(), "verification_failed", null) };
239
+ data: startData({ ...ctx, reason: "credential_in_metadata_response", workspaceLabel: { slug: null, name: null } },
240
+ new Set(), "verification_failed", null),
241
+ validateOutput: (text) => !outputHoldsKnown(text, known) };
238
242
  }
239
- return { outcome, reason, data };
243
+ return { outcome, reason, data, validateOutput: (text) => !outputHoldsKnown(text, known) };
240
244
  };
241
245
  const settle = (result) => {
242
246
  const interrupted =
@@ -257,7 +261,7 @@ export async function runExperimentStart(options) {
257
261
  if (binding !== null) {
258
262
  try {
259
263
  const store = openStore(resolveConfigDir(options.configDir), { create: false });
260
- const record = store === null ? null : readCredential(store, binding.credential_slot);
264
+ const record = store === null ? null : readProjectCredential(store, binding.credential_slot, root);
261
265
  originConfirmed = record !== null && record.origin === binding.origin;
262
266
  } catch (error) {
263
267
  if (!(error instanceof Stop)) throw error;
@@ -279,6 +283,10 @@ export async function runExperimentStart(options) {
279
283
  return end(settled.outcome, reason);
280
284
  }
281
285
  ctx.profile = session.session.profile;
286
+ if (target.workspaceText !== null && !workspacePinned(session.documents.workspace?.workspace, target.workspaceText)) {
287
+ return end("verification_failed", "workspace_context_mismatch");
288
+ }
289
+ ctx.workspaceLabel = workspaceLabel(session.documents.workspace?.workspace);
282
290
  if (ctx.workspaceId === null) {
283
291
  ctx.workspaceId = session.session.workspaceId;
284
292
  notices.add("workspace_not_pinned");
@@ -335,7 +343,7 @@ function unconfirmed(response, failure) {
335
343
  function startData(ctx, notices, outcome, report) {
336
344
  return {
337
345
  origin: ctx.origin,
338
- workspace: ctx.workspaceId === null ? null : { id: ctx.workspaceId },
346
+ workspace: ctx.workspaceId === null ? null : { id: ctx.workspaceId, ...ctx.workspaceLabel },
339
347
  deployment_profile: ctx.profile,
340
348
  environment: ctx.environment.kind,
341
349
  credential: "agent_key",
@@ -356,7 +364,7 @@ function startNextAction(outcome, ctx, report) {
356
364
  kind: "check_before_retry",
357
365
  message:
358
366
  "The start request reached the service, but its answer was not confirmed: an experiment may exist and " +
359
- "may be running. List the newest experiments with a Metadata key or this project's sign in before " +
367
+ "may be running. List the newest experiments with this project's sign in or a coding-agent key before " +
360
368
  "starting again; each start creates a new experiment.",
361
369
  command: "metergraph experiments list --json",
362
370
  };
@@ -376,8 +384,9 @@ function startNextAction(outcome, ctx, report) {
376
384
  kind: "use_experiment_start_key",
377
385
  url: keys,
378
386
  message:
379
- "Starting an experiment needs a coding-agent key with Debug access and Allow analysis starts, which only " +
380
- "a workspace owner can grant on the Keys page. Pass it with --agent-token-file or METERGRAPH_AGENT_TOKEN; " +
387
+ "Starting an experiment needs a coding-agent key with the Debug role and Allow analysis starts, which " +
388
+ "only a workspace owner can grant on the Keys page (or, on the customer-local bundle, with " +
389
+ "bin/agent-key). Pass it with --agent-token-file or METERGRAPH_AGENT_TOKEN; " +
381
390
  "do not ask for it in chat. A start replays the sampled calls' captured prompts on each treatment's " +
382
391
  "provider within the server's budget. Or start the experiment in the dashboard.",
383
392
  required_scope: EXPERIMENT_START_SCOPE,