@moda-ai/cli 1.41.2 → 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,
@@ -2500,6 +2501,183 @@ function isDefined(value) {
2500
2501
  return value !== undefined;
2501
2502
  }
2502
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
+
2503
2681
  // src/telemetry.ts
2504
2682
  import { randomUUID } from "node:crypto";
2505
2683
  var TELEMETRY_TIMEOUT_MS = 500;
@@ -2514,8 +2692,8 @@ var SEARCH_SURFACE_BY_COMMAND = {
2514
2692
  context: "context"
2515
2693
  };
2516
2694
  var SEARCH_MODES2 = ["keyword", "semantic", "hybrid"];
2517
- function termCount(query) {
2518
- return query.trim().split(/\s+/).filter(Boolean).length;
2695
+ function termCount(query2) {
2696
+ return query2.trim().split(/\s+/).filter(Boolean).length;
2519
2697
  }
2520
2698
  function clampInt(value, min, max) {
2521
2699
  if (typeof value !== "number" || !Number.isFinite(value))
@@ -5180,8 +5358,8 @@ function parsePayload(payload) {
5180
5358
  }
5181
5359
  var DEFAULT_STREAM_TIMEOUT_MS = 250000;
5182
5360
  async function streamAsk(options) {
5183
- const profileOptions = options.profile ? { profile: options.profile } : {};
5184
- 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`;
5185
5363
  const controller = new AbortController;
5186
5364
  const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? DEFAULT_STREAM_TIMEOUT_MS);
5187
5365
  const startedAt = Date.now();
@@ -5197,7 +5375,7 @@ async function streamAsk(options) {
5197
5375
  response = await fetchImpl(url, {
5198
5376
  method: "POST",
5199
5377
  headers: {
5200
- "x-api-key": resolveApiKey(profileOptions) ?? "",
5378
+ "x-api-key": resolveApiKey(profileOptions2) ?? "",
5201
5379
  "Content-Type": "application/json",
5202
5380
  Accept: "text/event-stream"
5203
5381
  },
@@ -7005,6 +7183,15 @@ Commands:
7005
7183
  prompts ab Run a judged prompt A/B replay experiment
7006
7184
  prompts propose Revise a prompt from a run's failures (--gate to auto-A/B, --promote-on-win)
7007
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
+
7008
7195
  Skills:
7009
7196
  skills gen Kick off tenant-wide skill generation
7010
7197
  skills status [run-id] Show a generation run's status, outcome, and recent events (default: latest run)
@@ -7383,10 +7570,10 @@ async function attachWindows(rows, windowSize) {
7383
7570
  return { ...row, window: conversation };
7384
7571
  }
7385
7572
  try {
7386
- const query = new URLSearchParams;
7387
- query.set("msg_index", msg_index.toString());
7388
- query.set("window", windowSize.toString());
7389
- 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()}`);
7390
7577
  return { ...row, window: data };
7391
7578
  } catch (err) {
7392
7579
  const message = err instanceof Error ? err.message : String(err);
@@ -7787,15 +7974,15 @@ function shellArg(value) {
7787
7974
  async function searchFallbackToTraces(params) {
7788
7975
  const limit = Math.min(params.limit ?? 20, 100);
7789
7976
  const offset = params.offset ?? 0;
7790
- const query = new URLSearchParams;
7791
- query.set("search", params.q);
7977
+ const query2 = new URLSearchParams;
7978
+ query2.set("search", params.q);
7792
7979
  if (params.user_id)
7793
- query.set("user_id", params.user_id);
7980
+ query2.set("user_id", params.user_id);
7794
7981
  if (params.time_range)
7795
- query.set("time_range", params.time_range);
7796
- query.set("limit", String(limit));
7982
+ query2.set("time_range", params.time_range);
7983
+ query2.set("limit", String(limit));
7797
7984
  if (offset > 0)
7798
- query.set("offset", String(offset));
7985
+ query2.set("offset", String(offset));
7799
7986
  const tracesCommand = [
7800
7987
  `moda traces --search=${shellArg(params.q)}`,
7801
7988
  params.user_id ? `--user-id=${shellArg(params.user_id)}` : "",
@@ -7804,7 +7991,7 @@ async function searchFallbackToTraces(params) {
7804
7991
  ].filter(Boolean).join(" ");
7805
7992
  let traces;
7806
7993
  try {
7807
- traces = asRecord(await callDataAPI(`/conversations?${query.toString()}`)) ?? {};
7994
+ traces = asRecord(await callDataAPI(`/conversations?${query2.toString()}`)) ?? {};
7808
7995
  } catch (error) {
7809
7996
  const reason = error instanceof Error ? error.message : String(error);
7810
7997
  return {
@@ -7953,16 +8140,16 @@ function assertHandParsedFlags(command, flags, positionals) {
7953
8140
  var PROFILE_MANAGING_COMMANDS = new Set(["profiles", "auth", "init", "provision", "status", "config", "keys", "manifest", "agent", "doctor"]);
7954
8141
  async function runProblemsRemainder(flags, context) {
7955
8142
  const params = parseCommandFlags(ProblemsRemainderSchema, flagsToArgs(flags), "problems remainder");
7956
- const query = new URLSearchParams;
8143
+ const query2 = new URLSearchParams;
7957
8144
  if (params.days_back)
7958
- query.set("days_back", params.days_back.toString());
8145
+ query2.set("days_back", params.days_back.toString());
7959
8146
  if (params.limit)
7960
- query.set("limit", params.limit.toString());
8147
+ query2.set("limit", params.limit.toString());
7961
8148
  if (params.family)
7962
- query.set("family", params.family);
8149
+ query2.set("family", params.family);
7963
8150
  if (params.cursor)
7964
- query.set("cursor", params.cursor);
7965
- const queryString = query.toString() ? `?${query.toString()}` : "";
8151
+ query2.set("cursor", params.cursor);
8152
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
7966
8153
  context.output.writeData(await callDataAPI(`/problems/remainder${queryString}`));
7967
8154
  }
7968
8155
  function processingDisabledWarning(data, family) {
@@ -8158,28 +8345,28 @@ async function runCommand(command, positional, flags, positionals = positional ?
8158
8345
  const args = flagsToArgs(flags);
8159
8346
  const params = parseCommandFlags(ClustersSchema, args, command);
8160
8347
  if (params.search !== undefined || params.node_id !== undefined) {
8161
- const query2 = new URLSearchParams;
8348
+ const query3 = new URLSearchParams;
8162
8349
  if (params.search !== undefined)
8163
- query2.set("q", params.search);
8350
+ query3.set("q", params.search);
8164
8351
  if (params.node_id !== undefined)
8165
- query2.set("node_id", params.node_id);
8352
+ query3.set("node_id", params.node_id);
8166
8353
  if (params.mode)
8167
- query2.set("mode", params.mode);
8354
+ query3.set("mode", params.mode);
8168
8355
  if (params.limit)
8169
- query2.set("limit", params.limit.toString());
8170
- 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()}`);
8171
8358
  const record = asRecord(data2) ?? {};
8172
8359
  const matches = Array.isArray(record.matches) ? record.matches : [];
8173
8360
  const warnings = params.node_id !== undefined && matches.length === 0 ? notFoundWarning("cluster", params.node_id, true) : [];
8174
8361
  context.output.writeData(data2, warnings.length > 0 ? { warnings } : undefined);
8175
8362
  break;
8176
8363
  }
8177
- const query = new URLSearchParams;
8364
+ const query2 = new URLSearchParams;
8178
8365
  if (params.parent_id)
8179
- query.set("parent_id", params.parent_id);
8366
+ query2.set("parent_id", params.parent_id);
8180
8367
  if (params.time_range)
8181
- query.set("time_range", params.time_range);
8182
- const queryString = query.toString() ? `?${query.toString()}` : "";
8368
+ query2.set("time_range", params.time_range);
8369
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8183
8370
  const data = await callDataAPI(`/clusters${queryString}`);
8184
8371
  context.output.writeData(data);
8185
8372
  break;
@@ -8191,12 +8378,12 @@ async function runCommand(command, positional, flags, positionals = positional ?
8191
8378
  }
8192
8379
  const args = flagsToArgs(flags, "node_id", positional);
8193
8380
  const params = parseCommandFlags(ClusterConversationsSchema, args, command, { positionalKey: "node_id" });
8194
- const query = new URLSearchParams;
8381
+ const query2 = new URLSearchParams;
8195
8382
  if (params.limit)
8196
- query.set("limit", params.limit.toString());
8383
+ query2.set("limit", params.limit.toString());
8197
8384
  if (params.offset !== undefined)
8198
- query.set("offset", params.offset.toString());
8199
- const queryString = query.toString() ? `?${query.toString()}` : "";
8385
+ query2.set("offset", params.offset.toString());
8386
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8200
8387
  const data = await callDataAPI(`/clusters/${params.node_id}/conversations${queryString}`);
8201
8388
  const clusterRecord = asRecord(data) ?? {};
8202
8389
  const warnings = notFoundWarning("cluster", params.node_id, clusterRecord.cluster === null || clusterRecord.cluster === undefined);
@@ -8207,28 +8394,28 @@ async function runCommand(command, positional, flags, positionals = positional ?
8207
8394
  case "conversations": {
8208
8395
  const args = withStringIdFlags(flagsToArgs(flags), flags);
8209
8396
  const params = parseCommandFlags(ConversationsSchema, args, command);
8210
- const query = new URLSearchParams;
8397
+ const query2 = new URLSearchParams;
8211
8398
  if (params.search)
8212
- query.set("search", params.search);
8399
+ query2.set("search", params.search);
8213
8400
  if (params.cluster_id)
8214
- query.set("cluster_id", params.cluster_id);
8401
+ query2.set("cluster_id", params.cluster_id);
8215
8402
  if (params.user_id)
8216
- query.set("user_id", params.user_id);
8403
+ query2.set("user_id", params.user_id);
8217
8404
  if (params.time_range)
8218
- query.set("time_range", params.time_range);
8405
+ query2.set("time_range", params.time_range);
8219
8406
  if (params.environment)
8220
- query.set("environment", params.environment);
8407
+ query2.set("environment", params.environment);
8221
8408
  if (params.world_state)
8222
- query.set("world_state", params.world_state);
8409
+ query2.set("world_state", params.world_state);
8223
8410
  if (params.outcome && params.outcome !== "any")
8224
- query.set("outcome", params.outcome);
8411
+ query2.set("outcome", params.outcome);
8225
8412
  if (params.sort)
8226
- query.set("sort_by", params.sort);
8413
+ query2.set("sort_by", params.sort);
8227
8414
  if (params.limit)
8228
- query.set("limit", params.limit.toString());
8415
+ query2.set("limit", params.limit.toString());
8229
8416
  if (params.offset !== undefined)
8230
- query.set("offset", params.offset.toString());
8231
- const queryString = query.toString() ? `?${query.toString()}` : "";
8417
+ query2.set("offset", params.offset.toString());
8418
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8232
8419
  const raw = await callDataAPI(`/conversations${queryString}`);
8233
8420
  recordConversationsTelemetry(context, raw);
8234
8421
  const searchWarnings = [];
@@ -8265,19 +8452,19 @@ async function runCommand(command, positional, flags, positionals = positional ?
8265
8452
  }
8266
8453
  const args = withStringIdFlags(flagsToArgs(flags, "q", positional), flags);
8267
8454
  const params = parseCommandFlags(SearchSchema, args, command, { positionalKey: "q" });
8268
- const query = new URLSearchParams;
8269
- query.set("q", params.q);
8455
+ const query2 = new URLSearchParams;
8456
+ query2.set("q", params.q);
8270
8457
  if (params.mode)
8271
- query.set("mode", params.mode);
8458
+ query2.set("mode", params.mode);
8272
8459
  if (params.user_id)
8273
- query.set("user_id", params.user_id);
8460
+ query2.set("user_id", params.user_id);
8274
8461
  if (params.time_range)
8275
- query.set("time_range", params.time_range);
8462
+ query2.set("time_range", params.time_range);
8276
8463
  if (params.limit)
8277
- query.set("limit", params.limit.toString());
8464
+ query2.set("limit", params.limit.toString());
8278
8465
  if (params.include_tool_io)
8279
- query.set("include_tool_io", "true");
8280
- const data = await callDataAPI(`/search?${query.toString()}`);
8466
+ query2.set("include_tool_io", "true");
8467
+ const data = await callDataAPI(`/search?${query2.toString()}`);
8281
8468
  recordSearchTelemetry(context, data);
8282
8469
  const searchRecord = asRecord(data) ?? {};
8283
8470
  const degradeReason = asString(searchRecord.degrade_reason);
@@ -8325,13 +8512,13 @@ async function runCommand(command, positional, flags, positionals = positional ?
8325
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]]");
8326
8513
  }
8327
8514
  if (params.snapshot === true) {
8328
- const query2 = new URLSearchParams;
8329
- query2.set("msg_index", (params.msg_index ?? 0).toString());
8515
+ const query3 = new URLSearchParams;
8516
+ query3.set("msg_index", (params.msg_index ?? 0).toString());
8330
8517
  if (params.block_index !== undefined)
8331
- query2.set("block_index", params.block_index.toString());
8518
+ query3.set("block_index", params.block_index.toString());
8332
8519
  if (params.segment_index !== undefined)
8333
- query2.set("segment_index", params.segment_index.toString());
8334
- 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()}`);
8335
8522
  const snapshotRecord = asRecord(data2) ?? {};
8336
8523
  const warnings2 = notFoundWarning("world state", params.conversation_id, snapshotRecord.found === false);
8337
8524
  context.output.writeData(data2, warnings2.length > 0 ? { warnings: warnings2 } : undefined);
@@ -8342,22 +8529,22 @@ async function runCommand(command, positional, flags, positionals = positional ?
8342
8529
  if (format === "frames" && params.message_count === undefined) {
8343
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.");
8344
8531
  }
8345
- const query2 = new URLSearchParams;
8346
- query2.set("format", format === "fold-inputs" ? "fold_inputs" : "frames");
8532
+ const query3 = new URLSearchParams;
8533
+ query3.set("format", format === "fold-inputs" ? "fold_inputs" : "frames");
8347
8534
  if (params.message_count !== undefined)
8348
- query2.set("message_count", params.message_count.toString());
8349
- 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()}`);
8350
8537
  const replayRecord = asRecord(data2) ?? {};
8351
8538
  const warnings2 = notFoundWarning("world state", params.conversation_id, replayRecord.hasData === false);
8352
8539
  context.output.writeData(data2, warnings2.length > 0 ? { warnings: warnings2 } : undefined);
8353
8540
  break;
8354
8541
  }
8355
- const query = new URLSearchParams;
8542
+ const query2 = new URLSearchParams;
8356
8543
  if (params.summary_only === true)
8357
- query.set("summary_only", "1");
8544
+ query2.set("summary_only", "1");
8358
8545
  if (params.event_limit)
8359
- query.set("event_limit", params.event_limit.toString());
8360
- const queryString = query.toString() ? `?${query.toString()}` : "";
8546
+ query2.set("event_limit", params.event_limit.toString());
8547
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8361
8548
  const data = await callDataAPI(`/conversations/${params.conversation_id}/world-state${queryString}`);
8362
8549
  const wsRecord = asRecord(data) ?? {};
8363
8550
  const warnings = notFoundWarning("world state", params.conversation_id, wsRecord.hasData === false);
@@ -8389,12 +8576,12 @@ async function runCommand(command, positional, flags, positionals = positional ?
8389
8576
  if (params.from !== undefined || params.max_messages !== undefined) {
8390
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]");
8391
8578
  }
8392
- const query = new URLSearchParams;
8579
+ const query2 = new URLSearchParams;
8393
8580
  if (params.msg_index !== undefined)
8394
- query.set("msg_index", params.msg_index.toString());
8581
+ query2.set("msg_index", params.msg_index.toString());
8395
8582
  if (params.window)
8396
- query.set("window", params.window.toString());
8397
- const queryString = query.toString() ? `?${query.toString()}` : "";
8583
+ query2.set("window", params.window.toString());
8584
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8398
8585
  const data = await callDataAPI(`/conversations/${params.conversation_id}/context${queryString}`);
8399
8586
  const ctxRecord = asRecord(data) ?? {};
8400
8587
  const warnings = notFoundWarning("trace", params.conversation_id, asNumber(ctxRecord.total_messages) === 0);
@@ -8408,14 +8595,14 @@ async function runCommand(command, positional, flags, positionals = positional ?
8408
8595
  }
8409
8596
  const args = flagsToArgs(flags, "id", positional);
8410
8597
  const params = parseCommandFlags(AuditSchema, args, command, { positionalKey: "id" });
8411
- const query = new URLSearchParams;
8598
+ const query2 = new URLSearchParams;
8412
8599
  if (params.kind && params.kind !== "auto")
8413
- query.set("kind", params.kind);
8600
+ query2.set("kind", params.kind);
8414
8601
  if (params.include_raw === true)
8415
- query.set("include_raw", "1");
8602
+ query2.set("include_raw", "1");
8416
8603
  if (params.limit)
8417
- query.set("limit", params.limit.toString());
8418
- const queryString = query.toString() ? `?${query.toString()}` : "";
8604
+ query2.set("limit", params.limit.toString());
8605
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8419
8606
  const data = await callDataAPI(`/audit/${encodeURIComponent(params.id)}${queryString}`);
8420
8607
  const capped = auditCapStatus(data, params.limit ?? AUDIT_DEFAULT_LIMIT);
8421
8608
  const auditRecord = asRecord(data);
@@ -8448,19 +8635,19 @@ async function runCommand(command, positional, flags, positionals = positional ?
8448
8635
  case "frustrations": {
8449
8636
  const args = withStringIdFlags(flagsToArgs(flags), flags);
8450
8637
  const params = parseCommandFlags(FrustrationsSchema, args, command);
8451
- const query = new URLSearchParams;
8638
+ const query2 = new URLSearchParams;
8452
8639
  if (params.days_back)
8453
- query.set("days_back", params.days_back.toString());
8640
+ query2.set("days_back", params.days_back.toString());
8454
8641
  if (params.limit)
8455
- query.set("limit", params.limit.toString());
8642
+ query2.set("limit", params.limit.toString());
8456
8643
  if (params.offset !== undefined)
8457
- query.set("offset", params.offset.toString());
8644
+ query2.set("offset", params.offset.toString());
8458
8645
  if (params.user_id)
8459
- query.set("user_id", params.user_id);
8646
+ query2.set("user_id", params.user_id);
8460
8647
  if (params.conversation_id)
8461
- query.set("conversation_id", params.conversation_id);
8462
- query.set("family", "frustration");
8463
- 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()}`));
8464
8651
  await printWithOptionalWindows(withFrustrationAnchors(data), "frustrations", params.include_window === true, params.window ?? 1, context.output, processingDisabledWarning(data, "frustration"));
8465
8652
  break;
8466
8653
  }
@@ -8489,15 +8676,15 @@ async function runCommand(command, positional, flags, positionals = positional ?
8489
8676
  if (params.compact === true && params.include_window === true) {
8490
8677
  throw new CliInputError("--compact drops the conversation window; drop --include-window, or drop --compact.");
8491
8678
  }
8492
- const query = new URLSearchParams;
8679
+ const query2 = new URLSearchParams;
8493
8680
  if (params.subtype)
8494
- query.set("subtype", params.subtype);
8681
+ query2.set("subtype", params.subtype);
8495
8682
  if (params.days_back)
8496
- query.set("days_back", params.days_back.toString());
8497
- 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());
8498
8685
  if (params.offset !== undefined)
8499
- query.set("offset", params.offset.toString());
8500
- const queryString = query.toString() ? `?${query.toString()}` : "";
8686
+ query2.set("offset", params.offset.toString());
8687
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8501
8688
  const data = await callDataAPI(`/tool-failures/${params.tool_name}${queryString}`);
8502
8689
  const tfRecord = asRecord(data) ?? {};
8503
8690
  const subtypeCount = Array.isArray(tfRecord.subtypes) ? tfRecord.subtypes.length : 0;
@@ -8531,7 +8718,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
8531
8718
  }
8532
8719
  case "problem-impact": {
8533
8720
  const params = ProblemImpactSchema.parse(Object.fromEntries(Object.entries(flags).map(([key, value]) => [key.replace(/-/g, "_"), value])));
8534
- 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 });
8535
8722
  for (const [key, value] of Object.entries({
8536
8723
  nodeId: params.node_id,
8537
8724
  problemId: params.problem_id,
@@ -8541,8 +8728,8 @@ async function runCommand(command, positional, flags, positionals = positional ?
8541
8728
  conversationIds: params.conversation_ids ? JSON.stringify(params.conversation_ids) : undefined
8542
8729
  }))
8543
8730
  if (value !== undefined)
8544
- query.set(key, value);
8545
- const data = asRecord(await callDataAPI(`/problem-impact?${query}`)) ?? {};
8731
+ query2.set(key, value);
8732
+ const data = asRecord(await callDataAPI(`/problem-impact?${query2}`)) ?? {};
8546
8733
  const link = new URLSearchParams({ impactDays: String(params.days_back) });
8547
8734
  const window = asRecord(data.window);
8548
8735
  const resolvedRun = asString(data.runId);
@@ -8569,12 +8756,12 @@ async function runCommand(command, positional, flags, positionals = positional ?
8569
8756
  }
8570
8757
  const args = flagsToArgs(flags);
8571
8758
  const params = parseCommandFlags(ProblemsSchema, args, command);
8572
- const query = new URLSearchParams;
8759
+ const query2 = new URLSearchParams;
8573
8760
  if (params.days_back)
8574
- query.set("days_back", params.days_back.toString());
8761
+ query2.set("days_back", params.days_back.toString());
8575
8762
  if (params.limit)
8576
- query.set("limit", params.limit.toString());
8577
- const queryString = query.toString() ? `?${query.toString()}` : "";
8763
+ query2.set("limit", params.limit.toString());
8764
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8578
8765
  const data = await callDataAPI(`/problems${queryString}`);
8579
8766
  const { payload, truncated } = capProblemsBlocks(asRecord(data) ?? {}, params.limit ?? 25);
8580
8767
  const withLink = {
@@ -8616,18 +8803,18 @@ async function runCommand(command, positional, flags, positionals = positional ?
8616
8803
  const viewFlag = view === "conversations" ? "--traces" : `--${view}`;
8617
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.`);
8618
8805
  }
8619
- const query = new URLSearchParams;
8806
+ const query2 = new URLSearchParams;
8620
8807
  if (params.limit)
8621
- query.set("limit", params.limit.toString());
8808
+ query2.set("limit", params.limit.toString());
8622
8809
  if (params.cursor)
8623
- query.set("cursor", params.cursor);
8810
+ query2.set("cursor", params.cursor);
8624
8811
  if (view === "conversations") {
8625
8812
  if (params.family)
8626
- query.set("family", params.family);
8813
+ query2.set("family", params.family);
8627
8814
  if (params.door)
8628
- query.set("door", params.door);
8815
+ query2.set("door", params.door);
8629
8816
  }
8630
- const queryString = query.toString() ? `?${query.toString()}` : "";
8817
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8631
8818
  const data = await callDataAPI(`/problems/${params.id}/${view}${queryString}`);
8632
8819
  context.output.writeData(data);
8633
8820
  break;
@@ -8763,14 +8950,14 @@ async function runCommand(command, positional, flags, positionals = positional ?
8763
8950
  context.output.writeData(data2);
8764
8951
  break;
8765
8952
  }
8766
- const query = new URLSearchParams;
8953
+ const query2 = new URLSearchParams;
8767
8954
  if (params.days_back)
8768
- query.set("days_back", params.days_back.toString());
8955
+ query2.set("days_back", params.days_back.toString());
8769
8956
  if (params.limit)
8770
- query.set("limit", params.limit.toString());
8957
+ query2.set("limit", params.limit.toString());
8771
8958
  if (params.offset !== undefined)
8772
- query.set("offset", params.offset.toString());
8773
- const queryString = query.toString() ? `?${query.toString()}` : "";
8959
+ query2.set("offset", params.offset.toString());
8960
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8774
8961
  const data = await callDataAPI(`/custom-signals/${id}/detections${queryString}`);
8775
8962
  context.output.writeData(data);
8776
8963
  break;
@@ -8922,20 +9109,20 @@ async function runCommand(command, positional, flags, positionals = positional ?
8922
9109
  case "false-positives": {
8923
9110
  if (!positional) {
8924
9111
  const kinds = { quote: "frustration_quote", tool: "tool_call", laziness: "laziness", hallucination: "hallucination", "custom-signal": "custom_signal" };
8925
- const query = new URLSearchParams;
9112
+ const query2 = new URLSearchParams;
8926
9113
  if (flags.kind !== undefined) {
8927
9114
  if (!kinds[flags.kind])
8928
9115
  throw new CliInputError(`--kind must be one of ${Object.keys(kinds).join("|")}`);
8929
- query.set("kind", kinds[flags.kind]);
9116
+ query2.set("kind", kinds[flags.kind]);
8930
9117
  }
8931
9118
  if (flags.limit !== undefined) {
8932
9119
  if (!/^\d+$/.test(flags.limit) || Number(flags.limit) < 1 || Number(flags.limit) > 200)
8933
9120
  throw new CliInputError("--limit must be 1-200");
8934
- query.set("limit", flags.limit);
9121
+ query2.set("limit", flags.limit);
8935
9122
  }
8936
9123
  if (flags.all !== undefined && flags.all !== "false")
8937
- query.set("include_cleared", "true");
8938
- const queryString = query.toString() ? `?${query.toString()}` : "";
9124
+ query2.set("include_cleared", "true");
9125
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8939
9126
  const data2 = await callDataAPI(`/false-positives${queryString}`);
8940
9127
  context.output.writeData(data2);
8941
9128
  break;
@@ -8947,20 +9134,20 @@ async function runCommand(command, positional, flags, positionals = positional ?
8947
9134
  case "emotions": {
8948
9135
  const args = withStringIdFlags(flagsToArgs(flags), flags);
8949
9136
  const params = parseCommandFlags(EmotionsSchema, args, command);
8950
- const query = new URLSearchParams;
9137
+ const query2 = new URLSearchParams;
8951
9138
  if (params.days_back)
8952
- query.set("days_back", params.days_back.toString());
9139
+ query2.set("days_back", params.days_back.toString());
8953
9140
  if (params.limit)
8954
- query.set("limit", params.limit.toString());
9141
+ query2.set("limit", params.limit.toString());
8955
9142
  if (params.offset !== undefined)
8956
- query.set("offset", params.offset.toString());
9143
+ query2.set("offset", params.offset.toString());
8957
9144
  if (params.family)
8958
- query.set("family", params.family);
9145
+ query2.set("family", params.family);
8959
9146
  if (params.user_id)
8960
- query.set("user_id", params.user_id);
9147
+ query2.set("user_id", params.user_id);
8961
9148
  if (params.conversation_id)
8962
- query.set("conversation_id", params.conversation_id);
8963
- const queryString = query.toString() ? `?${query.toString()}` : "";
9149
+ query2.set("conversation_id", params.conversation_id);
9150
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8964
9151
  const data = await callDataAPI(`/emotions${queryString}`);
8965
9152
  const processingWarnings = processingDisabledWarning(data, "emotion");
8966
9153
  context.output.writeData(data, processingWarnings.length > 0 ? { warnings: processingWarnings } : undefined);
@@ -8972,18 +9159,18 @@ async function runCommand(command, positional, flags, positionals = positional ?
8972
9159
  args.conversation_id = flags["conversation-id"];
8973
9160
  }
8974
9161
  const params = parseCommandFlags(LazinessSchema, args, command);
8975
- const query = new URLSearchParams;
9162
+ const query2 = new URLSearchParams;
8976
9163
  if (params.days_back)
8977
- query.set("days_back", params.days_back.toString());
9164
+ query2.set("days_back", params.days_back.toString());
8978
9165
  if (params.limit)
8979
- query.set("limit", params.limit.toString());
9166
+ query2.set("limit", params.limit.toString());
8980
9167
  if (params.offset !== undefined)
8981
- query.set("offset", params.offset.toString());
9168
+ query2.set("offset", params.offset.toString());
8982
9169
  if (params.conversation_id)
8983
- query.set("conversation_id", params.conversation_id);
9170
+ query2.set("conversation_id", params.conversation_id);
8984
9171
  if (params.family)
8985
- query.set("family", params.family);
8986
- const queryString = query.toString() ? `?${query.toString()}` : "";
9172
+ query2.set("family", params.family);
9173
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
8987
9174
  const data = await callDataAPI(`/laziness${queryString}`);
8988
9175
  context.output.writeData(data);
8989
9176
  break;
@@ -8994,18 +9181,18 @@ async function runCommand(command, positional, flags, positionals = positional ?
8994
9181
  args.conversation_id = flags["conversation-id"];
8995
9182
  }
8996
9183
  const params = parseCommandFlags(HallucinationsSchema, args, command);
8997
- const query = new URLSearchParams;
9184
+ const query2 = new URLSearchParams;
8998
9185
  if (params.days_back)
8999
- query.set("days_back", params.days_back.toString());
9186
+ query2.set("days_back", params.days_back.toString());
9000
9187
  if (params.limit)
9001
- query.set("limit", params.limit.toString());
9188
+ query2.set("limit", params.limit.toString());
9002
9189
  if (params.offset !== undefined)
9003
- query.set("offset", params.offset.toString());
9190
+ query2.set("offset", params.offset.toString());
9004
9191
  if (params.conversation_id)
9005
- query.set("conversation_id", params.conversation_id);
9192
+ query2.set("conversation_id", params.conversation_id);
9006
9193
  if (params.kind)
9007
- query.set("kind", params.kind);
9008
- const queryString = query.toString() ? `?${query.toString()}` : "";
9194
+ query2.set("kind", params.kind);
9195
+ const queryString = query2.toString() ? `?${query2.toString()}` : "";
9009
9196
  const data = await callDataAPI(`/hallucinations${queryString}`);
9010
9197
  context.output.writeData(data);
9011
9198
  break;
@@ -9481,6 +9668,23 @@ var commandRegistry = createCommandRegistry([
9481
9668
  telemetry: "result-and-error",
9482
9669
  handler: runUserCommand
9483
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
+ },
9484
9688
  {
9485
9689
  name: "problem-reopen",
9486
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.2",
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:27:15.161Z",
4
- "cli_version": "1.41.2",
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 |