@moda-ai/cli 1.41.1 → 1.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.
package/dist/cli.js CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  HARNESS_REPORT_PATH,
16
16
  SIGNALS_MANAGEMENT_SUBCOMMANDS,
17
17
  assertFlags,
18
+ assertPositionals,
18
19
  callControlAPI,
19
20
  callDataAPI,
20
21
  getApiRequestCount,
@@ -2196,6 +2197,9 @@ async function pushRegistry(context) {
2196
2197
  const detail = section.status === "error" ? ` — ${section.error}` : describeActions(section.response);
2197
2198
  console.error(` ${section.section}: ${section.status} (${section.count})${detail}`);
2198
2199
  }
2200
+ if (sections.some((section) => section.section === "tools" && section.status === "synced")) {
2201
+ console.error(" Replays now give your agent these tools with their real schemas and descriptions.");
2202
+ }
2199
2203
  }
2200
2204
  context.output.writeData({
2201
2205
  schema: REGISTRY_SCHEMA,
@@ -2497,6 +2501,183 @@ function isDefined(value) {
2497
2501
  return value !== undefined;
2498
2502
  }
2499
2503
 
2504
+ // src/replays.ts
2505
+ var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2506
+ var PLAYOUT_ID_RE = /^[0-9a-f]{64}$/i;
2507
+ var ARMS = ["prod", "proposed"];
2508
+ var REPLAYS_SUBCOMMANDS = ["sets", "runs", "run", "playouts", "playout"];
2509
+ var REPLAYS_SUBCOMMAND_HELP = [
2510
+ {
2511
+ name: "sets",
2512
+ description: "List replay sets (ids, names, case counts)",
2513
+ usage: "moda replays sets",
2514
+ examples: ["moda replays sets --json"]
2515
+ },
2516
+ {
2517
+ name: "runs",
2518
+ description: "Run history for a set, newest first, every status",
2519
+ usage: "moda replays runs <set_id> [--limit=N]",
2520
+ flags: [{ name: "--limit=N", description: "Runs to return (default 20, max 100)." }],
2521
+ examples: ["moda replays runs <set_id> --limit=5 --json"]
2522
+ },
2523
+ {
2524
+ name: "run",
2525
+ description: "One run: summary, per-case aggregate, every case x arm x seed verdict, playout count",
2526
+ usage: "moda replays run <set_id> <run_id|latest>",
2527
+ examples: ["moda replays run <set_id> <run_id> --json", "moda replays run <set_id> latest --json"]
2528
+ },
2529
+ {
2530
+ name: "playouts",
2531
+ description: "Playouts of a run (one per case x arm x seed) with verdicts; --transcript inlines what was said and every tool call",
2532
+ usage: "moda replays playouts <set_id> <run_id> [--case=<case_id>] [--arm=prod|proposed] [--seed=N] [--transcript] [--max-chars=N] [--limit=N] [--cursor=<next_cursor>]",
2533
+ flags: [
2534
+ { name: "--case=<case_id>", description: "Only this case." },
2535
+ { name: "--arm=prod|proposed", description: "Only this arm (prod = baseline, proposed = candidate)." },
2536
+ { name: "--seed=N", description: "Only this seed." },
2537
+ { name: "--transcript", description: "Inline each transcript (messages, tool calls with served output and provenance, criteria). Page cap 10." },
2538
+ { name: "--max-chars=N", description: "Per-field cap for message text and tool JSON (default 4000, 100-32000)." },
2539
+ { name: "--limit=N", description: "Page size (default 50, max 200; 10 with --transcript)." },
2540
+ { name: "--cursor=<next_cursor>", description: "Next page: the previous response's nextCursor." }
2541
+ ],
2542
+ examples: [
2543
+ "moda replays playouts <set_id> <run_id> --json",
2544
+ "moda replays playouts <set_id> <run_id> --case=<case_id> --arm=proposed --seed=1 --transcript --json"
2545
+ ]
2546
+ },
2547
+ {
2548
+ name: "playout",
2549
+ description: "One playout with its full transcript",
2550
+ usage: "moda replays playout <set_id> <run_id> <playout_id> [--max-chars=N]",
2551
+ flags: [{ name: "--max-chars=N", description: "Per-field cap (default 4000, 100-32000)." }],
2552
+ examples: ["moda replays playout <set_id> <run_id> <playout_id> --json"]
2553
+ }
2554
+ ];
2555
+ function profileOptions(context) {
2556
+ return { profile: context.profile, env: context.env };
2557
+ }
2558
+ function requireTenant(context) {
2559
+ const key = resolveApiKey(profileOptions(context));
2560
+ if (!key) {
2561
+ throw new CliInputError("No Moda API key found for moda replays.", "Set MODA_API_KEY, run `moda auth login --api-key`, or select a profile with an API key.");
2562
+ }
2563
+ const keyTenant = embeddedTenantIdFromApiKey(key);
2564
+ const flagTenant = context.flags["tenant-id"];
2565
+ if (flagTenant && keyTenant && flagTenant !== keyTenant) {
2566
+ throw new CliInputError(`--tenant-id=${flagTenant} does not match this API key's tenant (${keyTenant}), so nothing was sent.`, "Drop --tenant-id (the key names its tenant) or use a key for that tenant.");
2567
+ }
2568
+ const tenantId = flagTenant || resolveApiTenantId(profileOptions(context));
2569
+ if (!tenantId) {
2570
+ throw new CliInputError("Could not determine the tenant.", "Use a signed API key (it names its tenant), set MODA_TENANT_ID, or pass --tenant-id=<id>.");
2571
+ }
2572
+ return tenantId;
2573
+ }
2574
+ function intFlag(name, value, min, max) {
2575
+ if (value === undefined)
2576
+ return;
2577
+ const parsed = Number(value);
2578
+ if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
2579
+ throw new CliInputError(`--${name} must be an integer from ${min} to ${max} (got '${value}').`);
2580
+ }
2581
+ return String(parsed);
2582
+ }
2583
+ function query(params) {
2584
+ const search = new URLSearchParams(Object.entries(params).filter((entry) => entry[1] !== undefined)).toString();
2585
+ return search ? `?${search}` : "";
2586
+ }
2587
+ function needArg(value, name, usage) {
2588
+ if (!value)
2589
+ throw new CliInputError(`<${name}> is required.`, `Usage: ${usage}`);
2590
+ return value;
2591
+ }
2592
+ function needRunId(value, usage, allowLatest = false) {
2593
+ const runId = needArg(value, "run_id", usage);
2594
+ if (allowLatest && runId === "latest")
2595
+ return runId;
2596
+ if (!UUID_RE2.test(runId)) {
2597
+ throw new CliInputError(`<run_id> must be a run UUID (got '${runId}').`, "List runs with `moda replays runs <set_id>`.");
2598
+ }
2599
+ return runId;
2600
+ }
2601
+ async function runReplaysCommand(context) {
2602
+ const [subcommand, ...args] = context.positionals;
2603
+ const flags = context.flags;
2604
+ const help = REPLAYS_SUBCOMMAND_HELP.find((entry) => entry.name === subcommand);
2605
+ if (!subcommand || !help) {
2606
+ throw new CliInputError(subcommand ? `Unknown replays command '${subcommand}'.` : "A replays subcommand is required.", `Use one of: ${REPLAYS_SUBCOMMANDS.join(", ")}. See \`moda replays --help\`.`);
2607
+ }
2608
+ const usage = help.usage;
2609
+ const allowed = {
2610
+ sets: [],
2611
+ runs: ["limit"],
2612
+ run: [],
2613
+ playouts: ["case", "arm", "seed", "transcript", "max-chars", "limit", "cursor"],
2614
+ playout: ["max-chars"]
2615
+ };
2616
+ const name = subcommand;
2617
+ assertFlags(`replays ${name}`, flags, [...allowed[name], "tenant-id"]);
2618
+ const maxArgs = { sets: 0, runs: 1, run: 2, playouts: 2, playout: 3 }[name];
2619
+ assertPositionals(`replays ${name}`, args, maxArgs, usage);
2620
+ const tenantId = requireTenant(context);
2621
+ const base = `/tenants/${encodeURIComponent(tenantId)}/replay-sets`;
2622
+ let endpoint;
2623
+ switch (name) {
2624
+ case "sets":
2625
+ endpoint = base;
2626
+ break;
2627
+ case "runs": {
2628
+ const setId = needArg(args[0], "set_id", usage);
2629
+ endpoint = `${base}/${encodeURIComponent(setId)}/runs${query({ limit: intFlag("limit", flags.limit, 1, 100) })}`;
2630
+ break;
2631
+ }
2632
+ case "run": {
2633
+ const setId = needArg(args[0], "set_id", usage);
2634
+ const runId = needRunId(args[1], usage, true);
2635
+ endpoint = runId === "latest" ? `${base}/${encodeURIComponent(setId)}/runs/latest` : `${base}/${encodeURIComponent(setId)}/runs/${encodeURIComponent(runId)}`;
2636
+ break;
2637
+ }
2638
+ case "playouts": {
2639
+ const setId = needArg(args[0], "set_id", usage);
2640
+ const runId = needRunId(args[1], usage);
2641
+ const arm = flags.arm;
2642
+ if (arm !== undefined && !ARMS.includes(arm)) {
2643
+ throw new CliInputError(`--arm must be one of ${ARMS.join(", ")} (got '${arm}').`);
2644
+ }
2645
+ if (flags.case !== undefined && (flags.case.length === 0 || flags.case.length > 128)) {
2646
+ throw new CliInputError("--case must be a case id of 1 to 128 characters.");
2647
+ }
2648
+ if (flags.cursor !== undefined && !/^\d{1,9}$/.test(flags.cursor)) {
2649
+ throw new CliInputError(`--cursor must be the nextCursor of a previous page (got '${flags.cursor}').`);
2650
+ }
2651
+ const transcript = flags.transcript === "true";
2652
+ endpoint = `${base}/${encodeURIComponent(setId)}/runs/${encodeURIComponent(runId)}/playouts${query({
2653
+ caseId: flags.case,
2654
+ arm,
2655
+ seed: intFlag("seed", flags.seed, 0, 1000),
2656
+ include: transcript ? "transcript" : undefined,
2657
+ maxChars: intFlag("max-chars", flags["max-chars"], 100, 32000),
2658
+ limit: intFlag("limit", flags.limit, 1, transcript ? 10 : 200),
2659
+ cursor: flags.cursor
2660
+ })}`;
2661
+ break;
2662
+ }
2663
+ case "playout": {
2664
+ const setId = needArg(args[0], "set_id", usage);
2665
+ const runId = needRunId(args[1], usage);
2666
+ const playoutId = needArg(args[2], "playout_id", usage);
2667
+ if (!PLAYOUT_ID_RE.test(playoutId)) {
2668
+ throw new CliInputError(`<playout_id> must be a 64-character hex id (got '${playoutId}').`, "List playouts with `moda replays playouts <set_id> <run_id>`.");
2669
+ }
2670
+ endpoint = `${base}/${encodeURIComponent(setId)}/runs/${encodeURIComponent(runId)}/playouts/${encodeURIComponent(playoutId)}${query({
2671
+ maxChars: intFlag("max-chars", flags["max-chars"], 100, 32000)
2672
+ })}`;
2673
+ break;
2674
+ }
2675
+ }
2676
+ const data = await callControlAPI(endpoint, { method: "GET" }, profileOptions(context), { timeoutMs: 60000 });
2677
+ context.output.writeData(data ?? {});
2678
+ return { exitCode: 0 };
2679
+ }
2680
+
2500
2681
  // src/telemetry.ts
2501
2682
  import { randomUUID } from "node:crypto";
2502
2683
  var TELEMETRY_TIMEOUT_MS = 500;
@@ -2511,8 +2692,8 @@ var SEARCH_SURFACE_BY_COMMAND = {
2511
2692
  context: "context"
2512
2693
  };
2513
2694
  var SEARCH_MODES2 = ["keyword", "semantic", "hybrid"];
2514
- function termCount(query) {
2515
- return query.trim().split(/\s+/).filter(Boolean).length;
2695
+ function termCount(query2) {
2696
+ return query2.trim().split(/\s+/).filter(Boolean).length;
2516
2697
  }
2517
2698
  function clampInt(value, min, max) {
2518
2699
  if (typeof value !== "number" || !Number.isFinite(value))
@@ -5177,8 +5358,8 @@ function parsePayload(payload) {
5177
5358
  }
5178
5359
  var DEFAULT_STREAM_TIMEOUT_MS = 250000;
5179
5360
  async function streamAsk(options) {
5180
- const profileOptions = options.profile ? { profile: options.profile } : {};
5181
- const url = `${resolveModaBaseUrl(profileOptions)}/api/cli/ask/stream`;
5361
+ const profileOptions2 = options.profile ? { profile: options.profile } : {};
5362
+ const url = `${resolveModaBaseUrl(profileOptions2)}/api/cli/ask/stream`;
5182
5363
  const controller = new AbortController;
5183
5364
  const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? DEFAULT_STREAM_TIMEOUT_MS);
5184
5365
  const startedAt = Date.now();
@@ -5194,7 +5375,7 @@ async function streamAsk(options) {
5194
5375
  response = await fetchImpl(url, {
5195
5376
  method: "POST",
5196
5377
  headers: {
5197
- "x-api-key": resolveApiKey(profileOptions) ?? "",
5378
+ "x-api-key": resolveApiKey(profileOptions2) ?? "",
5198
5379
  "Content-Type": "application/json",
5199
5380
  Accept: "text/event-stream"
5200
5381
  },
@@ -7002,6 +7183,15 @@ Commands:
7002
7183
  prompts ab Run a judged prompt A/B replay experiment
7003
7184
  prompts propose Revise a prompt from a run's failures (--gate to auto-A/B, --promote-on-win)
7004
7185
 
7186
+ Replays (read-only results):
7187
+ replays sets List replay sets
7188
+ replays runs <set_id> Run history for a set (every status)
7189
+ replays run <set_id> <run_id> One run: per-case and per-seed verdicts, playout count
7190
+ replays playouts <set_id> <run_id>
7191
+ Playouts per case/arm/seed (--transcript inlines messages and tool calls)
7192
+ replays playout <set_id> <run_id> <playout_id>
7193
+ One playout transcript
7194
+
7005
7195
  Skills:
7006
7196
  skills gen Kick off tenant-wide skill generation
7007
7197
  skills status [run-id] Show a generation run's status, outcome, and recent events (default: latest run)
@@ -7380,10 +7570,10 @@ async function attachWindows(rows, windowSize) {
7380
7570
  return { ...row, window: conversation };
7381
7571
  }
7382
7572
  try {
7383
- const query = new URLSearchParams;
7384
- query.set("msg_index", msg_index.toString());
7385
- query.set("window", windowSize.toString());
7386
- const data = await callDataAPI(`/conversations/${conv_id}/context?${query.toString()}`);
7573
+ const query2 = new URLSearchParams;
7574
+ query2.set("msg_index", msg_index.toString());
7575
+ query2.set("window", windowSize.toString());
7576
+ const data = await callDataAPI(`/conversations/${conv_id}/context?${query2.toString()}`);
7387
7577
  return { ...row, window: data };
7388
7578
  } catch (err) {
7389
7579
  const message = err instanceof Error ? err.message : String(err);
@@ -7784,15 +7974,15 @@ function shellArg(value) {
7784
7974
  async function searchFallbackToTraces(params) {
7785
7975
  const limit = Math.min(params.limit ?? 20, 100);
7786
7976
  const offset = params.offset ?? 0;
7787
- const query = new URLSearchParams;
7788
- query.set("search", params.q);
7977
+ const query2 = new URLSearchParams;
7978
+ query2.set("search", params.q);
7789
7979
  if (params.user_id)
7790
- query.set("user_id", params.user_id);
7980
+ query2.set("user_id", params.user_id);
7791
7981
  if (params.time_range)
7792
- query.set("time_range", params.time_range);
7793
- query.set("limit", String(limit));
7982
+ query2.set("time_range", params.time_range);
7983
+ query2.set("limit", String(limit));
7794
7984
  if (offset > 0)
7795
- query.set("offset", String(offset));
7985
+ query2.set("offset", String(offset));
7796
7986
  const tracesCommand = [
7797
7987
  `moda traces --search=${shellArg(params.q)}`,
7798
7988
  params.user_id ? `--user-id=${shellArg(params.user_id)}` : "",
@@ -7801,7 +7991,7 @@ async function searchFallbackToTraces(params) {
7801
7991
  ].filter(Boolean).join(" ");
7802
7992
  let traces;
7803
7993
  try {
7804
- traces = asRecord(await callDataAPI(`/conversations?${query.toString()}`)) ?? {};
7994
+ traces = asRecord(await callDataAPI(`/conversations?${query2.toString()}`)) ?? {};
7805
7995
  } catch (error) {
7806
7996
  const reason = error instanceof Error ? error.message : String(error);
7807
7997
  return {
@@ -7950,16 +8140,16 @@ function assertHandParsedFlags(command, flags, positionals) {
7950
8140
  var PROFILE_MANAGING_COMMANDS = new Set(["profiles", "auth", "init", "provision", "status", "config", "keys", "manifest", "agent", "doctor"]);
7951
8141
  async function runProblemsRemainder(flags, context) {
7952
8142
  const params = parseCommandFlags(ProblemsRemainderSchema, flagsToArgs(flags), "problems remainder");
7953
- const query = new URLSearchParams;
8143
+ const query2 = new URLSearchParams;
7954
8144
  if (params.days_back)
7955
- query.set("days_back", params.days_back.toString());
8145
+ query2.set("days_back", params.days_back.toString());
7956
8146
  if (params.limit)
7957
- query.set("limit", params.limit.toString());
8147
+ query2.set("limit", params.limit.toString());
7958
8148
  if (params.family)
7959
- query.set("family", params.family);
8149
+ query2.set("family", params.family);
7960
8150
  if (params.cursor)
7961
- query.set("cursor", params.cursor);
7962
- const queryString = query.toString() ? `?${query.toString()}` : "";
8151
+ query2.set("cursor", params.cursor);
8152
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
7963
8153
  context.output.writeData(await callDataAPI(`/problems/remainder${queryString}`));
7964
8154
  }
7965
8155
  function processingDisabledWarning(data, family) {
@@ -8155,28 +8345,28 @@ async function runCommand(command, positional, flags, positionals = positional ?
8155
8345
  const args = flagsToArgs(flags);
8156
8346
  const params = parseCommandFlags(ClustersSchema, args, command);
8157
8347
  if (params.search !== undefined || params.node_id !== undefined) {
8158
- const query2 = new URLSearchParams;
8348
+ const query3 = new URLSearchParams;
8159
8349
  if (params.search !== undefined)
8160
- query2.set("q", params.search);
8350
+ query3.set("q", params.search);
8161
8351
  if (params.node_id !== undefined)
8162
- query2.set("node_id", params.node_id);
8352
+ query3.set("node_id", params.node_id);
8163
8353
  if (params.mode)
8164
- query2.set("mode", params.mode);
8354
+ query3.set("mode", params.mode);
8165
8355
  if (params.limit)
8166
- query2.set("limit", params.limit.toString());
8167
- const data2 = await callDataAPI(`/task-clusters/search?${query2.toString()}`);
8356
+ query3.set("limit", params.limit.toString());
8357
+ const data2 = await callDataAPI(`/task-clusters/search?${query3.toString()}`);
8168
8358
  const record = asRecord(data2) ?? {};
8169
8359
  const matches = Array.isArray(record.matches) ? record.matches : [];
8170
8360
  const warnings = params.node_id !== undefined && matches.length === 0 ? notFoundWarning("cluster", params.node_id, true) : [];
8171
8361
  context.output.writeData(data2, warnings.length > 0 ? { warnings } : undefined);
8172
8362
  break;
8173
8363
  }
8174
- const query = new URLSearchParams;
8364
+ const query2 = new URLSearchParams;
8175
8365
  if (params.parent_id)
8176
- query.set("parent_id", params.parent_id);
8366
+ query2.set("parent_id", params.parent_id);
8177
8367
  if (params.time_range)
8178
- query.set("time_range", params.time_range);
8179
- const queryString = query.toString() ? `?${query.toString()}` : "";
8368
+ query2.set("time_range", params.time_range);
8369
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8180
8370
  const data = await callDataAPI(`/clusters${queryString}`);
8181
8371
  context.output.writeData(data);
8182
8372
  break;
@@ -8188,12 +8378,12 @@ async function runCommand(command, positional, flags, positionals = positional ?
8188
8378
  }
8189
8379
  const args = flagsToArgs(flags, "node_id", positional);
8190
8380
  const params = parseCommandFlags(ClusterConversationsSchema, args, command, { positionalKey: "node_id" });
8191
- const query = new URLSearchParams;
8381
+ const query2 = new URLSearchParams;
8192
8382
  if (params.limit)
8193
- query.set("limit", params.limit.toString());
8383
+ query2.set("limit", params.limit.toString());
8194
8384
  if (params.offset !== undefined)
8195
- query.set("offset", params.offset.toString());
8196
- const queryString = query.toString() ? `?${query.toString()}` : "";
8385
+ query2.set("offset", params.offset.toString());
8386
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8197
8387
  const data = await callDataAPI(`/clusters/${params.node_id}/conversations${queryString}`);
8198
8388
  const clusterRecord = asRecord(data) ?? {};
8199
8389
  const warnings = notFoundWarning("cluster", params.node_id, clusterRecord.cluster === null || clusterRecord.cluster === undefined);
@@ -8204,28 +8394,28 @@ async function runCommand(command, positional, flags, positionals = positional ?
8204
8394
  case "conversations": {
8205
8395
  const args = withStringIdFlags(flagsToArgs(flags), flags);
8206
8396
  const params = parseCommandFlags(ConversationsSchema, args, command);
8207
- const query = new URLSearchParams;
8397
+ const query2 = new URLSearchParams;
8208
8398
  if (params.search)
8209
- query.set("search", params.search);
8399
+ query2.set("search", params.search);
8210
8400
  if (params.cluster_id)
8211
- query.set("cluster_id", params.cluster_id);
8401
+ query2.set("cluster_id", params.cluster_id);
8212
8402
  if (params.user_id)
8213
- query.set("user_id", params.user_id);
8403
+ query2.set("user_id", params.user_id);
8214
8404
  if (params.time_range)
8215
- query.set("time_range", params.time_range);
8405
+ query2.set("time_range", params.time_range);
8216
8406
  if (params.environment)
8217
- query.set("environment", params.environment);
8407
+ query2.set("environment", params.environment);
8218
8408
  if (params.world_state)
8219
- query.set("world_state", params.world_state);
8409
+ query2.set("world_state", params.world_state);
8220
8410
  if (params.outcome && params.outcome !== "any")
8221
- query.set("outcome", params.outcome);
8411
+ query2.set("outcome", params.outcome);
8222
8412
  if (params.sort)
8223
- query.set("sort_by", params.sort);
8413
+ query2.set("sort_by", params.sort);
8224
8414
  if (params.limit)
8225
- query.set("limit", params.limit.toString());
8415
+ query2.set("limit", params.limit.toString());
8226
8416
  if (params.offset !== undefined)
8227
- query.set("offset", params.offset.toString());
8228
- const queryString = query.toString() ? `?${query.toString()}` : "";
8417
+ query2.set("offset", params.offset.toString());
8418
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8229
8419
  const raw = await callDataAPI(`/conversations${queryString}`);
8230
8420
  recordConversationsTelemetry(context, raw);
8231
8421
  const searchWarnings = [];
@@ -8262,19 +8452,19 @@ async function runCommand(command, positional, flags, positionals = positional ?
8262
8452
  }
8263
8453
  const args = withStringIdFlags(flagsToArgs(flags, "q", positional), flags);
8264
8454
  const params = parseCommandFlags(SearchSchema, args, command, { positionalKey: "q" });
8265
- const query = new URLSearchParams;
8266
- query.set("q", params.q);
8455
+ const query2 = new URLSearchParams;
8456
+ query2.set("q", params.q);
8267
8457
  if (params.mode)
8268
- query.set("mode", params.mode);
8458
+ query2.set("mode", params.mode);
8269
8459
  if (params.user_id)
8270
- query.set("user_id", params.user_id);
8460
+ query2.set("user_id", params.user_id);
8271
8461
  if (params.time_range)
8272
- query.set("time_range", params.time_range);
8462
+ query2.set("time_range", params.time_range);
8273
8463
  if (params.limit)
8274
- query.set("limit", params.limit.toString());
8464
+ query2.set("limit", params.limit.toString());
8275
8465
  if (params.include_tool_io)
8276
- query.set("include_tool_io", "true");
8277
- const data = await callDataAPI(`/search?${query.toString()}`);
8466
+ query2.set("include_tool_io", "true");
8467
+ const data = await callDataAPI(`/search?${query2.toString()}`);
8278
8468
  recordSearchTelemetry(context, data);
8279
8469
  const searchRecord = asRecord(data) ?? {};
8280
8470
  const degradeReason = asString(searchRecord.degrade_reason);
@@ -8322,13 +8512,13 @@ async function runCommand(command, positional, flags, positionals = positional ?
8322
8512
  throw new CliInputError("Choose either --snapshot or --replay, not both.", "Usage: moda world-state <conversation_id> [--snapshot --msg-index=N [--block-index=M] [--segment-index=K]] | [--replay --message-count=N [--format=frames|fold-inputs]]");
8323
8513
  }
8324
8514
  if (params.snapshot === true) {
8325
- const query2 = new URLSearchParams;
8326
- query2.set("msg_index", (params.msg_index ?? 0).toString());
8515
+ const query3 = new URLSearchParams;
8516
+ query3.set("msg_index", (params.msg_index ?? 0).toString());
8327
8517
  if (params.block_index !== undefined)
8328
- query2.set("block_index", params.block_index.toString());
8518
+ query3.set("block_index", params.block_index.toString());
8329
8519
  if (params.segment_index !== undefined)
8330
- query2.set("segment_index", params.segment_index.toString());
8331
- const data2 = await callDataAPI(`/conversations/${params.conversation_id}/world-state/snapshot?${query2.toString()}`);
8520
+ query3.set("segment_index", params.segment_index.toString());
8521
+ const data2 = await callDataAPI(`/conversations/${params.conversation_id}/world-state/snapshot?${query3.toString()}`);
8332
8522
  const snapshotRecord = asRecord(data2) ?? {};
8333
8523
  const warnings2 = notFoundWarning("world state", params.conversation_id, snapshotRecord.found === false);
8334
8524
  context.output.writeData(data2, warnings2.length > 0 ? { warnings: warnings2 } : undefined);
@@ -8339,22 +8529,22 @@ async function runCommand(command, positional, flags, positionals = positional ?
8339
8529
  if (format === "frames" && params.message_count === undefined) {
8340
8530
  throw new CliInputError("--message-count=N is required with --replay in frames mode (the API returns no frames without it). Use --format=fold-inputs for the raw keyframes/events without a count.");
8341
8531
  }
8342
- const query2 = new URLSearchParams;
8343
- query2.set("format", format === "fold-inputs" ? "fold_inputs" : "frames");
8532
+ const query3 = new URLSearchParams;
8533
+ query3.set("format", format === "fold-inputs" ? "fold_inputs" : "frames");
8344
8534
  if (params.message_count !== undefined)
8345
- query2.set("message_count", params.message_count.toString());
8346
- const data2 = await callDataAPI(`/conversations/${params.conversation_id}/world-state/replay?${query2.toString()}`);
8535
+ query3.set("message_count", params.message_count.toString());
8536
+ const data2 = await callDataAPI(`/conversations/${params.conversation_id}/world-state/replay?${query3.toString()}`);
8347
8537
  const replayRecord = asRecord(data2) ?? {};
8348
8538
  const warnings2 = notFoundWarning("world state", params.conversation_id, replayRecord.hasData === false);
8349
8539
  context.output.writeData(data2, warnings2.length > 0 ? { warnings: warnings2 } : undefined);
8350
8540
  break;
8351
8541
  }
8352
- const query = new URLSearchParams;
8542
+ const query2 = new URLSearchParams;
8353
8543
  if (params.summary_only === true)
8354
- query.set("summary_only", "1");
8544
+ query2.set("summary_only", "1");
8355
8545
  if (params.event_limit)
8356
- query.set("event_limit", params.event_limit.toString());
8357
- const queryString = query.toString() ? `?${query.toString()}` : "";
8546
+ query2.set("event_limit", params.event_limit.toString());
8547
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8358
8548
  const data = await callDataAPI(`/conversations/${params.conversation_id}/world-state${queryString}`);
8359
8549
  const wsRecord = asRecord(data) ?? {};
8360
8550
  const warnings = notFoundWarning("world state", params.conversation_id, wsRecord.hasData === false);
@@ -8386,12 +8576,12 @@ async function runCommand(command, positional, flags, positionals = positional ?
8386
8576
  if (params.from !== undefined || params.max_messages !== undefined) {
8387
8577
  throw new CliInputError("--from and --max-messages page the whole trace and need --all.", "Usage: moda context <conversation_id> --all [--from=N] [--max-messages=N]");
8388
8578
  }
8389
- const query = new URLSearchParams;
8579
+ const query2 = new URLSearchParams;
8390
8580
  if (params.msg_index !== undefined)
8391
- query.set("msg_index", params.msg_index.toString());
8581
+ query2.set("msg_index", params.msg_index.toString());
8392
8582
  if (params.window)
8393
- query.set("window", params.window.toString());
8394
- const queryString = query.toString() ? `?${query.toString()}` : "";
8583
+ query2.set("window", params.window.toString());
8584
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8395
8585
  const data = await callDataAPI(`/conversations/${params.conversation_id}/context${queryString}`);
8396
8586
  const ctxRecord = asRecord(data) ?? {};
8397
8587
  const warnings = notFoundWarning("trace", params.conversation_id, asNumber(ctxRecord.total_messages) === 0);
@@ -8405,14 +8595,14 @@ async function runCommand(command, positional, flags, positionals = positional ?
8405
8595
  }
8406
8596
  const args = flagsToArgs(flags, "id", positional);
8407
8597
  const params = parseCommandFlags(AuditSchema, args, command, { positionalKey: "id" });
8408
- const query = new URLSearchParams;
8598
+ const query2 = new URLSearchParams;
8409
8599
  if (params.kind && params.kind !== "auto")
8410
- query.set("kind", params.kind);
8600
+ query2.set("kind", params.kind);
8411
8601
  if (params.include_raw === true)
8412
- query.set("include_raw", "1");
8602
+ query2.set("include_raw", "1");
8413
8603
  if (params.limit)
8414
- query.set("limit", params.limit.toString());
8415
- const queryString = query.toString() ? `?${query.toString()}` : "";
8604
+ query2.set("limit", params.limit.toString());
8605
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8416
8606
  const data = await callDataAPI(`/audit/${encodeURIComponent(params.id)}${queryString}`);
8417
8607
  const capped = auditCapStatus(data, params.limit ?? AUDIT_DEFAULT_LIMIT);
8418
8608
  const auditRecord = asRecord(data);
@@ -8445,19 +8635,19 @@ async function runCommand(command, positional, flags, positionals = positional ?
8445
8635
  case "frustrations": {
8446
8636
  const args = withStringIdFlags(flagsToArgs(flags), flags);
8447
8637
  const params = parseCommandFlags(FrustrationsSchema, args, command);
8448
- const query = new URLSearchParams;
8638
+ const query2 = new URLSearchParams;
8449
8639
  if (params.days_back)
8450
- query.set("days_back", params.days_back.toString());
8640
+ query2.set("days_back", params.days_back.toString());
8451
8641
  if (params.limit)
8452
- query.set("limit", params.limit.toString());
8642
+ query2.set("limit", params.limit.toString());
8453
8643
  if (params.offset !== undefined)
8454
- query.set("offset", params.offset.toString());
8644
+ query2.set("offset", params.offset.toString());
8455
8645
  if (params.user_id)
8456
- query.set("user_id", params.user_id);
8646
+ query2.set("user_id", params.user_id);
8457
8647
  if (params.conversation_id)
8458
- query.set("conversation_id", params.conversation_id);
8459
- query.set("family", "frustration");
8460
- const data = frustrationsFromEmotions(await callDataAPI(`/emotions?${query.toString()}`));
8648
+ query2.set("conversation_id", params.conversation_id);
8649
+ query2.set("family", "frustration");
8650
+ const data = frustrationsFromEmotions(await callDataAPI(`/emotions?${query2.toString()}`));
8461
8651
  await printWithOptionalWindows(withFrustrationAnchors(data), "frustrations", params.include_window === true, params.window ?? 1, context.output, processingDisabledWarning(data, "frustration"));
8462
8652
  break;
8463
8653
  }
@@ -8486,15 +8676,15 @@ async function runCommand(command, positional, flags, positionals = positional ?
8486
8676
  if (params.compact === true && params.include_window === true) {
8487
8677
  throw new CliInputError("--compact drops the conversation window; drop --include-window, or drop --compact.");
8488
8678
  }
8489
- const query = new URLSearchParams;
8679
+ const query2 = new URLSearchParams;
8490
8680
  if (params.subtype)
8491
- query.set("subtype", params.subtype);
8681
+ query2.set("subtype", params.subtype);
8492
8682
  if (params.days_back)
8493
- query.set("days_back", params.days_back.toString());
8494
- query.set("limit", (params.limit ?? TOOL_FAILURE_DETAIL_DEFAULT_LIMIT).toString());
8683
+ query2.set("days_back", params.days_back.toString());
8684
+ query2.set("limit", (params.limit ?? TOOL_FAILURE_DETAIL_DEFAULT_LIMIT).toString());
8495
8685
  if (params.offset !== undefined)
8496
- query.set("offset", params.offset.toString());
8497
- const queryString = query.toString() ? `?${query.toString()}` : "";
8686
+ query2.set("offset", params.offset.toString());
8687
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8498
8688
  const data = await callDataAPI(`/tool-failures/${params.tool_name}${queryString}`);
8499
8689
  const tfRecord = asRecord(data) ?? {};
8500
8690
  const subtypeCount = Array.isArray(tfRecord.subtypes) ? tfRecord.subtypes.length : 0;
@@ -8528,7 +8718,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
8528
8718
  }
8529
8719
  case "problem-impact": {
8530
8720
  const params = ProblemImpactSchema.parse(Object.fromEntries(Object.entries(flags).map(([key, value]) => [key.replace(/-/g, "_"), value])));
8531
- const query = new URLSearchParams({ days: String(params.days_back), mode: params.mode });
8721
+ const query2 = new URLSearchParams({ days: String(params.days_back), mode: params.mode });
8532
8722
  for (const [key, value] of Object.entries({
8533
8723
  nodeId: params.node_id,
8534
8724
  problemId: params.problem_id,
@@ -8538,8 +8728,8 @@ async function runCommand(command, positional, flags, positionals = positional ?
8538
8728
  conversationIds: params.conversation_ids ? JSON.stringify(params.conversation_ids) : undefined
8539
8729
  }))
8540
8730
  if (value !== undefined)
8541
- query.set(key, value);
8542
- const data = asRecord(await callDataAPI(`/problem-impact?${query}`)) ?? {};
8731
+ query2.set(key, value);
8732
+ const data = asRecord(await callDataAPI(`/problem-impact?${query2}`)) ?? {};
8543
8733
  const link = new URLSearchParams({ impactDays: String(params.days_back) });
8544
8734
  const window = asRecord(data.window);
8545
8735
  const resolvedRun = asString(data.runId);
@@ -8566,12 +8756,12 @@ async function runCommand(command, positional, flags, positionals = positional ?
8566
8756
  }
8567
8757
  const args = flagsToArgs(flags);
8568
8758
  const params = parseCommandFlags(ProblemsSchema, args, command);
8569
- const query = new URLSearchParams;
8759
+ const query2 = new URLSearchParams;
8570
8760
  if (params.days_back)
8571
- query.set("days_back", params.days_back.toString());
8761
+ query2.set("days_back", params.days_back.toString());
8572
8762
  if (params.limit)
8573
- query.set("limit", params.limit.toString());
8574
- const queryString = query.toString() ? `?${query.toString()}` : "";
8763
+ query2.set("limit", params.limit.toString());
8764
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8575
8765
  const data = await callDataAPI(`/problems${queryString}`);
8576
8766
  const { payload, truncated } = capProblemsBlocks(asRecord(data) ?? {}, params.limit ?? 25);
8577
8767
  const withLink = {
@@ -8613,18 +8803,18 @@ async function runCommand(command, positional, flags, positionals = positional ?
8613
8803
  const viewFlag = view === "conversations" ? "--traces" : `--${view}`;
8614
8804
  throw new CliInputError(`${viewFlag} requires a canonical problem UUID; got "${params.id}". Run \`moda problems\` or \`moda problem <id>\` first to resolve the id.`);
8615
8805
  }
8616
- const query = new URLSearchParams;
8806
+ const query2 = new URLSearchParams;
8617
8807
  if (params.limit)
8618
- query.set("limit", params.limit.toString());
8808
+ query2.set("limit", params.limit.toString());
8619
8809
  if (params.cursor)
8620
- query.set("cursor", params.cursor);
8810
+ query2.set("cursor", params.cursor);
8621
8811
  if (view === "conversations") {
8622
8812
  if (params.family)
8623
- query.set("family", params.family);
8813
+ query2.set("family", params.family);
8624
8814
  if (params.door)
8625
- query.set("door", params.door);
8815
+ query2.set("door", params.door);
8626
8816
  }
8627
- const queryString = query.toString() ? `?${query.toString()}` : "";
8817
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8628
8818
  const data = await callDataAPI(`/problems/${params.id}/${view}${queryString}`);
8629
8819
  context.output.writeData(data);
8630
8820
  break;
@@ -8760,14 +8950,14 @@ async function runCommand(command, positional, flags, positionals = positional ?
8760
8950
  context.output.writeData(data2);
8761
8951
  break;
8762
8952
  }
8763
- const query = new URLSearchParams;
8953
+ const query2 = new URLSearchParams;
8764
8954
  if (params.days_back)
8765
- query.set("days_back", params.days_back.toString());
8955
+ query2.set("days_back", params.days_back.toString());
8766
8956
  if (params.limit)
8767
- query.set("limit", params.limit.toString());
8957
+ query2.set("limit", params.limit.toString());
8768
8958
  if (params.offset !== undefined)
8769
- query.set("offset", params.offset.toString());
8770
- const queryString = query.toString() ? `?${query.toString()}` : "";
8959
+ query2.set("offset", params.offset.toString());
8960
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8771
8961
  const data = await callDataAPI(`/custom-signals/${id}/detections${queryString}`);
8772
8962
  context.output.writeData(data);
8773
8963
  break;
@@ -8919,20 +9109,20 @@ async function runCommand(command, positional, flags, positionals = positional ?
8919
9109
  case "false-positives": {
8920
9110
  if (!positional) {
8921
9111
  const kinds = { quote: "frustration_quote", tool: "tool_call", laziness: "laziness", hallucination: "hallucination", "custom-signal": "custom_signal" };
8922
- const query = new URLSearchParams;
9112
+ const query2 = new URLSearchParams;
8923
9113
  if (flags.kind !== undefined) {
8924
9114
  if (!kinds[flags.kind])
8925
9115
  throw new CliInputError(`--kind must be one of ${Object.keys(kinds).join("|")}`);
8926
- query.set("kind", kinds[flags.kind]);
9116
+ query2.set("kind", kinds[flags.kind]);
8927
9117
  }
8928
9118
  if (flags.limit !== undefined) {
8929
9119
  if (!/^\d+$/.test(flags.limit) || Number(flags.limit) < 1 || Number(flags.limit) > 200)
8930
9120
  throw new CliInputError("--limit must be 1-200");
8931
- query.set("limit", flags.limit);
9121
+ query2.set("limit", flags.limit);
8932
9122
  }
8933
9123
  if (flags.all !== undefined && flags.all !== "false")
8934
- query.set("include_cleared", "true");
8935
- const queryString = query.toString() ? `?${query.toString()}` : "";
9124
+ query2.set("include_cleared", "true");
9125
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8936
9126
  const data2 = await callDataAPI(`/false-positives${queryString}`);
8937
9127
  context.output.writeData(data2);
8938
9128
  break;
@@ -8944,20 +9134,20 @@ async function runCommand(command, positional, flags, positionals = positional ?
8944
9134
  case "emotions": {
8945
9135
  const args = withStringIdFlags(flagsToArgs(flags), flags);
8946
9136
  const params = parseCommandFlags(EmotionsSchema, args, command);
8947
- const query = new URLSearchParams;
9137
+ const query2 = new URLSearchParams;
8948
9138
  if (params.days_back)
8949
- query.set("days_back", params.days_back.toString());
9139
+ query2.set("days_back", params.days_back.toString());
8950
9140
  if (params.limit)
8951
- query.set("limit", params.limit.toString());
9141
+ query2.set("limit", params.limit.toString());
8952
9142
  if (params.offset !== undefined)
8953
- query.set("offset", params.offset.toString());
9143
+ query2.set("offset", params.offset.toString());
8954
9144
  if (params.family)
8955
- query.set("family", params.family);
9145
+ query2.set("family", params.family);
8956
9146
  if (params.user_id)
8957
- query.set("user_id", params.user_id);
9147
+ query2.set("user_id", params.user_id);
8958
9148
  if (params.conversation_id)
8959
- query.set("conversation_id", params.conversation_id);
8960
- const queryString = query.toString() ? `?${query.toString()}` : "";
9149
+ query2.set("conversation_id", params.conversation_id);
9150
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8961
9151
  const data = await callDataAPI(`/emotions${queryString}`);
8962
9152
  const processingWarnings = processingDisabledWarning(data, "emotion");
8963
9153
  context.output.writeData(data, processingWarnings.length > 0 ? { warnings: processingWarnings } : undefined);
@@ -8969,18 +9159,18 @@ async function runCommand(command, positional, flags, positionals = positional ?
8969
9159
  args.conversation_id = flags["conversation-id"];
8970
9160
  }
8971
9161
  const params = parseCommandFlags(LazinessSchema, args, command);
8972
- const query = new URLSearchParams;
9162
+ const query2 = new URLSearchParams;
8973
9163
  if (params.days_back)
8974
- query.set("days_back", params.days_back.toString());
9164
+ query2.set("days_back", params.days_back.toString());
8975
9165
  if (params.limit)
8976
- query.set("limit", params.limit.toString());
9166
+ query2.set("limit", params.limit.toString());
8977
9167
  if (params.offset !== undefined)
8978
- query.set("offset", params.offset.toString());
9168
+ query2.set("offset", params.offset.toString());
8979
9169
  if (params.conversation_id)
8980
- query.set("conversation_id", params.conversation_id);
9170
+ query2.set("conversation_id", params.conversation_id);
8981
9171
  if (params.family)
8982
- query.set("family", params.family);
8983
- const queryString = query.toString() ? `?${query.toString()}` : "";
9172
+ query2.set("family", params.family);
9173
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8984
9174
  const data = await callDataAPI(`/laziness${queryString}`);
8985
9175
  context.output.writeData(data);
8986
9176
  break;
@@ -8991,18 +9181,18 @@ async function runCommand(command, positional, flags, positionals = positional ?
8991
9181
  args.conversation_id = flags["conversation-id"];
8992
9182
  }
8993
9183
  const params = parseCommandFlags(HallucinationsSchema, args, command);
8994
- const query = new URLSearchParams;
9184
+ const query2 = new URLSearchParams;
8995
9185
  if (params.days_back)
8996
- query.set("days_back", params.days_back.toString());
9186
+ query2.set("days_back", params.days_back.toString());
8997
9187
  if (params.limit)
8998
- query.set("limit", params.limit.toString());
9188
+ query2.set("limit", params.limit.toString());
8999
9189
  if (params.offset !== undefined)
9000
- query.set("offset", params.offset.toString());
9190
+ query2.set("offset", params.offset.toString());
9001
9191
  if (params.conversation_id)
9002
- query.set("conversation_id", params.conversation_id);
9192
+ query2.set("conversation_id", params.conversation_id);
9003
9193
  if (params.kind)
9004
- query.set("kind", params.kind);
9005
- const queryString = query.toString() ? `?${query.toString()}` : "";
9194
+ query2.set("kind", params.kind);
9195
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
9006
9196
  const data = await callDataAPI(`/hallucinations${queryString}`);
9007
9197
  context.output.writeData(data);
9008
9198
  break;
@@ -9478,6 +9668,23 @@ var commandRegistry = createCommandRegistry([
9478
9668
  telemetry: "result-and-error",
9479
9669
  handler: runUserCommand
9480
9670
  },
9671
+ {
9672
+ name: "replays",
9673
+ description: "Read replay results: sets, run history, per-seed verdicts, and playout transcripts",
9674
+ examples: [
9675
+ "moda replays runs <set_id>",
9676
+ "moda replays run <set_id> <run_id> --json",
9677
+ "moda replays playouts <set_id> <run_id> --case=<case_id> --arm=proposed --transcript --json"
9678
+ ],
9679
+ subcommands: REPLAYS_SUBCOMMAND_HELP,
9680
+ auth: "api-key",
9681
+ outputModes: ["human", "json", "agent"],
9682
+ mutability: "read",
9683
+ validateConfigBeforeRun: false,
9684
+ resetApiRequestCountBeforeRun: false,
9685
+ telemetry: "result-and-error",
9686
+ handler: runReplaysCommand
9687
+ },
9481
9688
  {
9482
9689
  name: "problem-reopen",
9483
9690
  description: "Reopen a problem that was marked fixed (resolved problems return only via re-detection)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moda-ai/cli",
3
- "version": "1.41.1",
3
+ "version": "1.42.0",
4
4
  "description": "CLI for Moda - AI agent analytics and observability",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": "moda.skill_index.v1",
3
- "bundled_at": "2026-10-09T01:20:13.374Z",
4
- "cli_version": "1.41.1",
3
+ "bundled_at": "2026-10-09T02:07:16.635Z",
4
+ "cli_version": "1.42.0",
5
5
  "skills": [
6
6
  {
7
7
  "id": "integration-cloudflare-think",
@@ -785,6 +785,43 @@ Do not copy prompt text from the dashboard into source code. The repo prompt
785
785
  files are the source of truth; the dashboard is for visibility, usage, labels,
786
786
  and future evals.
787
787
 
788
+ ### 7a. Check a replay verdict (`moda replays`)
789
+
790
+ Use after `prompts ab`, `prompts propose --gate`, or a dashboard replay when
791
+ you need to see *why* an arm passed or failed. Read-only; no replay spend.
792
+
793
+ ```bash
794
+ moda replays sets --json # set ids
795
+ moda replays runs <set_id> --limit=5 --json # newest runs, every status
796
+ moda replays run <set_id> <run_id> --json # per-case + per-seed verdicts
797
+ moda replays playouts <set_id> <run_id> --json # one row per case x arm x seed
798
+ moda replays playouts <set_id> <run_id> --case=<case_id> --arm=proposed --seed=1 --transcript --json
799
+ moda replays playout <set_id> <run_id> <playout_id> --json
800
+ ```
801
+
802
+ `run` returns `results[]` (one per case × arm × seed: `pass`, `abstained`,
803
+ `coverageReason`, `score`, `rationale`, `criteria[]`) next to the per-case
804
+ majority in `cases[]`. `arm=prod` is the baseline and `arm=proposed` the
805
+ candidate. An `abstained: true` row is not a verdict: the pair hit a replay
806
+ coverage hole and is excluded from pass counts.
807
+
808
+ A playout's `transcript.events[]` holds the simulated user and agent messages
809
+ and every tool call with its `input`, the `output` the replay served, and
810
+ `provenance.source`: `replay_runtime` (compiled mock over the case's recorded
811
+ source state), `agent_tool_simulator` (LLM simulator grounded in recorded
812
+ receipts; `simulator.model`), `counterfactual_reconstruction`, or
813
+ `generic_handler`. `provenance.errorSubtype` such as `missing_source_state`
814
+ marks an unservable call. `judge` is the judgement stored with the playout and
815
+ `verdict` the stored eval row (null when `seed` is null, which happens on
816
+ older runs whose graphs cannot be matched to a seed).
817
+
818
+ Limits: `--transcript` pages at most 10 playouts (`--limit` default 50, max
819
+ 200 without it); text and tool JSON are clipped at `--max-chars` (default
820
+ 4000, max 32000) and `transcript.truncated.fields` counts the clipped fields.
821
+ Page with `--cursor=<nextCursor>`. Playouts exist only for runs whose
822
+ playouts were captured; `playoutCount: 0` on `run` means there is no
823
+ transcript to read.
824
+
788
825
  ### 7b. Push prompts, skills, and tools without a repo scan (`moda registry`)
789
826
 
790
827
  Use when the user wants their prompts, skills, and tools visible in Moda but
@@ -1026,6 +1063,7 @@ Writes listed here accept `--dry-run` (prints the exact request, sends nothing)
1026
1063
  | Custom signals: edit, lifecycle | `moda signals update <id> …`, `moda signals activate\|pause\|delete <id>` (delete: drafts only; archiving is dashboard-only) |
1027
1064
  | Custom signals: review & tune | `moda signals examples <id> add\|update\|remove --conversation-id=…`, `refine <id> --request="…"`, `revise <id> --conversation-ids=…`, `proposals <id>`, `accept\|reject <id> <proposal_id>` |
1028
1065
  | Custom signals: find matches | `moda signals search <id>`, `search-status <id> <search_id>`, `search-stop <id> <search_id>`, `preview --file=detector.json` |
1066
+ | Replays: run detail, playout transcripts | `moda replays runs <set_id>`, `run <set_id> <run_id>`, `playouts <set_id> <run_id> [--case --arm --seed --transcript]`, `playout <set_id> <run_id> <playout_id>` |
1029
1067
  | Prompts registry | `moda prompts list [--label --search]`, `show <key>`, `proposals <key>`, `usage <key>`, `replay-runs <key>`, `decide <key> <proposal_id> --action=merge\|reject\|reopen` |
1030
1068
  | Skills library | `moda skills list`, `show <key>`, `policy <key> --live\|--local` |
1031
1069
  | Harness pages | `moda harness list`, `show <id>`, `versions <id>`, `diff <id> --from=… --to=…` |
@@ -1234,6 +1272,7 @@ npx: `npx -p @moda-ai/cli moda <command>`.
1234
1272
  | `moda problem-reopen <pid>` | Reopen a Problem marked fixed |
1235
1273
  | `moda signals <create\|update\|activate\|pause\|delete\|examples\|refine\|revise\|proposals\|accept\|reject\|search\|preview>` | Manage custom signals (writes take `--dry-run`; LLM-backed ones are rate-limited per key) |
1236
1274
  | `moda prompts list\|show\|proposals\|usage\|replay-runs\|decide` | Read the prompt registry; merge/reject proposals |
1275
+ | `moda replays sets\|runs\|run\|playouts\|playout` | Replay results: run history, per-seed verdicts, playout transcripts with tool provenance (read-only) |
1237
1276
  | `moda skills list\|show\|policy` / `moda harness list\|show\|versions\|diff` | Skill library and synced harnesses |
1238
1277
  | `moda fixes` | Ranked queue of Fixes ("a fix with proof"); `--status=S`, `--limit=N`, `--cursor=TOKEN` |
1239
1278
  | `moda fix <fix_id>` | One Fix: status + gate result; `--packet` prints `moda.fix_packet.v1` and writes tool/skill candidates under `.moda/fixes/<ref>/`; `--wait` drives a running pipeline to rest |