@ishlabs/cli 0.17.6 → 0.18.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.
Files changed (64) hide show
  1. package/README.md +54 -54
  2. package/dist/commands/ask.d.ts +4 -4
  3. package/dist/commands/ask.js +66 -66
  4. package/dist/commands/chat.js +10 -10
  5. package/dist/commands/config.js +1 -1
  6. package/dist/commands/docs.js +1 -1
  7. package/dist/commands/iteration.js +57 -57
  8. package/dist/commands/mcp.d.ts +23 -0
  9. package/dist/commands/mcp.js +676 -0
  10. package/dist/commands/person.d.ts +5 -0
  11. package/dist/commands/{profile.js → person.js} +197 -162
  12. package/dist/commands/source.d.ts +6 -2
  13. package/dist/commands/source.js +35 -30
  14. package/dist/commands/study-analyze.d.ts +1 -1
  15. package/dist/commands/study-analyze.js +3 -3
  16. package/dist/commands/study-participant.d.ts +8 -0
  17. package/dist/commands/{study-tester.js → study-participant.js} +50 -50
  18. package/dist/commands/study-run.d.ts +6 -6
  19. package/dist/commands/study-run.js +295 -271
  20. package/dist/commands/study.js +89 -66
  21. package/dist/commands/workspace.js +13 -13
  22. package/dist/connect.js +5 -5
  23. package/dist/index.js +6 -4
  24. package/dist/lib/accessibility-profile.d.ts +1 -1
  25. package/dist/lib/accessibility-profile.js +1 -1
  26. package/dist/lib/alias-hydrate.js +4 -4
  27. package/dist/lib/alias-store.d.ts +5 -5
  28. package/dist/lib/alias-store.js +8 -8
  29. package/dist/lib/api-client.d.ts +1 -1
  30. package/dist/lib/api-client.js +1 -1
  31. package/dist/lib/billing.d.ts +11 -11
  32. package/dist/lib/billing.js +16 -16
  33. package/dist/lib/chat-endpoint-templates.js +1 -1
  34. package/dist/lib/command-helpers.d.ts +18 -18
  35. package/dist/lib/command-helpers.js +83 -53
  36. package/dist/lib/docs.js +560 -386
  37. package/dist/lib/enums.d.ts +2 -2
  38. package/dist/lib/enums.js +2 -2
  39. package/dist/lib/local-sim/browser.d.ts +1 -1
  40. package/dist/lib/local-sim/browser.js +1 -1
  41. package/dist/lib/local-sim/debug-report.d.ts +2 -2
  42. package/dist/lib/local-sim/debug-report.js +3 -3
  43. package/dist/lib/local-sim/loop.d.ts +5 -5
  44. package/dist/lib/local-sim/loop.js +38 -38
  45. package/dist/lib/local-sim/types.d.ts +12 -12
  46. package/dist/lib/mcp-clients.d.ts +51 -0
  47. package/dist/lib/mcp-clients.js +175 -0
  48. package/dist/lib/modality.d.ts +10 -10
  49. package/dist/lib/modality.js +46 -46
  50. package/dist/lib/observability.d.ts +11 -0
  51. package/dist/lib/observability.js +16 -3
  52. package/dist/lib/output.d.ts +13 -12
  53. package/dist/lib/output.js +244 -184
  54. package/dist/lib/profile-sources.d.ts +64 -16
  55. package/dist/lib/profile-sources.js +91 -30
  56. package/dist/lib/skill-content.js +215 -168
  57. package/dist/lib/study-events.d.ts +3 -3
  58. package/dist/lib/study-events.js +1 -1
  59. package/dist/lib/study-inputs.d.ts +11 -1
  60. package/dist/lib/study-inputs.js +68 -17
  61. package/dist/lib/types.d.ts +105 -34
  62. package/package.json +1 -1
  63. package/dist/commands/profile.d.ts +0 -5
  64. package/dist/commands/study-tester.d.ts +0 -8
@@ -1,18 +1,19 @@
1
1
  /**
2
2
  * ish study run — Run, monitor, and cancel simulations of a study.
3
3
  *
4
- * `ish study run` creates testers for the latest (or specified) iteration
4
+ * `ish study run` creates participants for the latest (or specified) iteration
5
5
  * and dispatches simulations. Iterations are created separately via
6
6
  * `ish iteration create`, which carries the URL/content details.
7
7
  *
8
8
  * Lower-level: `study poll`, `study cancel`.
9
9
  */
10
+ import { Option } from "commander";
10
11
  import * as readline from "node:readline/promises";
11
- import { withClient, getWebUrl, terminalLink, resolveWorkspace, resolveStudy, parseWaitTimeout, resolveAudienceProfileIds, addAudienceFilterFlags, hasAudienceFlags, readFileOrStdin, } from "../lib/command-helpers.js";
12
+ import { withClient, getWebUrl, terminalLink, resolveWorkspace, resolveStudy, parseWaitTimeout, resolvePersonIds, addPersonFilterFlags, hasPersonFlags, readFileOrStdin, } from "../lib/command-helpers.js";
12
13
  import { resolveId, tagAlias, ALIAS_PREFIX } from "../lib/alias-store.js";
13
14
  import { output, formatSimulationPoll } from "../lib/output.js";
14
15
  import { streamStudyEvents } from "../lib/study-events.js";
15
- import { isMediaModality, isChatModality, iterationHasContent, describeRequiredContentFlag, readChatMode, readTesterPairConfig, summarizeRoleCriteria, } from "../lib/modality.js";
16
+ import { isMediaModality, isChatModality, iterationHasContent, describeRequiredContentFlag, readChatMode, readParticipantPairConfig, summarizeRoleCriteria, } from "../lib/modality.js";
16
17
  // NOTE: local-sim modules are loaded via dynamic import at the `--local`
17
18
  // branch below, NOT statically here. `local-sim/install.ts` deep-imports
18
19
  // `playwright-core/lib/server/registry/index`, which is not exposed by
@@ -33,7 +34,7 @@ function parseMaxInteractions(value) {
33
34
  * iteration carries its own value. Picked to match the frontend's
34
35
  * conservative interactive launchers and to prevent runaway spend when an
35
36
  * iteration runs against a broken or non-responsive surface — without a
36
- * cap, a stuck tester can rack up hundreds of steps before the SDK gives
37
+ * cap, a stuck participant can rack up hundreds of steps before the SDK gives
37
38
  * up.
38
39
  */
39
40
  const DEFAULT_MAX_INTERACTIONS = 20;
@@ -56,7 +57,7 @@ function parseSlowMo(value) {
56
57
  * produce a structured envelope (`error_code: "wait_timeout"`, exit 5
57
58
  * transient) distinct from the generic timeout/network/server errors
58
59
  * that the api-client wrapper produces. Carries the in-flight progress
59
- * (testers done / total) so `study wait` always emits final state JSON
60
+ * (participants done / total) so `study wait` always emits final state JSON
60
61
  * even when it bails on the timer.
61
62
  */
62
63
  export class WaitTimeoutError extends Error {
@@ -71,9 +72,9 @@ export class WaitTimeoutError extends Error {
71
72
  }
72
73
  /**
73
74
  * M13 / Pattern J: collapse the N near-duplicate `simulations[]` rows the
74
- * batch start endpoint returns (one per tester, all sharing study_id) into a
75
- * single batch entry with `tester_ids[]` + `tester_aliases[]` + `job_ids[]`.
76
- * Agents always need the per-tester ids, but the surrounding scaffolding
75
+ * batch start endpoint returns (one per participant, all sharing study_id) into a
76
+ * single batch entry with `participant_ids[]` + `participant_aliases[]` + `job_ids[]`.
77
+ * Agents always need the per-participant ids, but the surrounding scaffolding
77
78
  * (study_id, message) is shared so repeating it N times costs context for no
78
79
  * extra signal. Falls back to the raw rows if the response shape is unfamiliar.
79
80
  */
@@ -91,8 +92,8 @@ function dedupeSimulations(simResults) {
91
92
  return [
92
93
  {
93
94
  study_id: studyId,
94
- tester_ids: simResults.map((r) => r.tester_id),
95
- tester_aliases: simResults.map((r) => tagAlias(ALIAS_PREFIX.tester, String(r.tester_id))),
95
+ participant_ids: simResults.map((r) => r.participant_id),
96
+ participant_aliases: simResults.map((r) => tagAlias(ALIAS_PREFIX.participant, String(r.participant_id))),
96
97
  job_ids: simResults.map((r) => r.job_id ?? null),
97
98
  count: simResults.length,
98
99
  message: simResults[0].message,
@@ -106,20 +107,20 @@ const POLL_INTERVAL_MS = 5_000;
106
107
  // transparently reverts to POLL_INTERVAL_MS.
107
108
  const SSE_BACKSTOP_INTERVAL_MS = 30_000;
108
109
  const TERMINAL_STATUSES = new Set(["completed", "errored", "failed", "cancelled", "canceled"]);
109
- function flattenTesterStatuses(iterations, only) {
110
+ function flattenParticipantStatuses(iterations, only) {
110
111
  const rows = [];
111
112
  for (const iteration of iterations ?? []) {
112
- for (const t of iteration.testers ?? []) {
113
+ for (const t of iteration.participants ?? []) {
113
114
  if (only && !only.has(t.id))
114
115
  continue;
115
- // Pattern A (cli half): backend now reports per-tester crash detail at
116
+ // Pattern A (cli half): backend now reports per-participant crash detail at
116
117
  // `error_message`. Keep `error` / `failure_reason` as legacy fallbacks
117
118
  // until every backend deploy is on the new contract.
118
119
  const errorMessage = t.error_message || t.error || t.failure_reason || null;
119
120
  rows.push({
120
121
  id: t.id,
121
122
  status: t.status,
122
- tester_name: t.tester_profile?.name || "Unknown",
123
+ participant_name: t.person?.name || "Unknown",
123
124
  interaction_count: Array.isArray(t.interactions) ? t.interactions.length : 0,
124
125
  ...(errorMessage && { error_message: errorMessage }),
125
126
  });
@@ -131,7 +132,7 @@ async function pollStudyUntilDone(client, opts) {
131
132
  const start = Date.now();
132
133
  let lastReported = "";
133
134
  // Open the SSE event stream as a wake source. When the backend supports
134
- // it (enable_realtime=true + broker live), tester status changes wake
135
+ // it (enable_realtime=true + broker live), participant status changes wake
135
136
  // the loop the moment they happen — no need to wait for the next poll
136
137
  // tick. When it doesn't, the iterator exits silently on the first read
137
138
  // and we fall through to pure polling at POLL_INTERVAL_MS.
@@ -151,7 +152,7 @@ async function pollStudyUntilDone(client, opts) {
151
152
  if (opts.iterationId) {
152
153
  iterations = (iterations ?? []).filter((it) => it.id === opts.iterationId);
153
154
  }
154
- const rows = flattenTesterStatuses(iterations, opts.testerIds);
155
+ const rows = flattenParticipantStatuses(iterations, opts.participantIds);
155
156
  const total = rows.length;
156
157
  const done = rows.filter((r) => TERMINAL_STATUSES.has(r.status)).length;
157
158
  const errored = rows.filter((r) => r.status === "errored" || r.status === "failed").length;
@@ -234,22 +235,24 @@ export function attachStudyRunCommands(study) {
234
235
  // --- Primary: `study run` ---
235
236
  const studyRun = study
236
237
  .command("run")
237
- .description("Run a study (creates testers for the latest iteration and dispatches simulations)")
238
+ .description("Run a study (creates participants for the latest iteration and dispatches simulations)")
238
239
  .option("--workspace <id>", "Workspace ID")
239
240
  .option("--study <id>", "Study ID")
240
241
  .option("--iteration <id>", "Iteration to run (defaults to latest on the study)");
241
- addAudienceFilterFlags(studyRun, {
242
+ addPersonFilterFlags(studyRun, {
242
243
  allFlagName: "--all",
243
- allFlagDescription: "Use every AI profile matching the filters (workspace-wide if no filters set)",
244
+ allFlagDescription: "Use every AI person matching the filters (workspace-wide if no filters set)",
244
245
  })
245
- .option("--config <id>", "Simulation config ID (required for media unless every profile has one)")
246
- .option("--max-interactions <n>", `Max interactions per tester (interactive / media only). Precedence: flag > iteration's stored value > CLI default (${DEFAULT_MAX_INTERACTIONS}).`)
247
- .option("--max-turns <n>", "Max conversation turns per tester (chat studies only)")
246
+ .option("--config <id>", "Simulation config ID (required for media unless every person has one)")
247
+ .addOption(new Option("--sim-config <json|@file>", "Dev: per-run config overrides (ConfigOverrides JSON, or @path to a file). Layers on top of --config. "
248
+ + "e.g. --sim-config '{\"model_settings\":{\"agent\":{\"model\":\"gemini-3.5-flash\"}}}'").hideHelp())
249
+ .option("--max-interactions <n>", `Max interactions per participant (interactive / media only). Precedence: flag > iteration's stored value > CLI default (${DEFAULT_MAX_INTERACTIONS}).`)
250
+ .option("--max-turns <n>", "Max conversation turns per participant (chat studies only)")
248
251
  .option("--early-termination", "Allow chat agent to end the conversation early when goals are met (chat studies only)")
249
252
  .option("--language <lang>", "Language code (e.g. en, sv)")
250
253
  .option("--wait", "Wait for all simulations to reach a terminal state before returning")
251
254
  .option("--timeout <s>", "Wait timeout in seconds (default 300; only with --wait)")
252
- .option("--dispatch-timeout <s>", "Per-POST timeout in seconds for the create-testers + dispatch calls (default 120). Bump if `study run` times out client-side after seeding testers but before dispatch — those testers exist server-side and are surfaced under `seeded_but_not_dispatched_*` in the error envelope so the agent can resume.")
255
+ .option("--dispatch-timeout <s>", "Per-POST timeout in seconds for the create-participants + dispatch calls (default 120). Bump if `study run` times out client-side after seeding participants but before dispatch — those participants exist server-side and are surfaced under `seeded_but_not_dispatched_*` in the error envelope so the agent can resume.")
253
256
  .option("-y, --yes", "Skip confirmation prompt")
254
257
  // Local simulation options
255
258
  .option("--local", "Run simulation with local browser (Playwright) instead of remote")
@@ -257,7 +260,7 @@ export function attachStudyRunCommands(study) {
257
260
  .option("--slow-mo <ms>", "Slow down actions by ms (local mode only)")
258
261
  .option("--devtools", "Open Chrome DevTools (local mode only)")
259
262
  .option("--debug", "Enable detailed debug logging to stderr and ~/.ish/local-sim.log")
260
- .option("--parallel <n>", "Run N testers in parallel (local mode only, default: all)")
263
+ .option("--parallel <n>", "Run N participants in parallel (local mode only, default: all)")
261
264
  .addHelpText("after", `
262
265
  Note: --workspace and --study are optional if you have set active context
263
266
  via \`ish workspace use <alias>\` and \`ish study use <alias>\`.
@@ -265,23 +268,22 @@ Note: --workspace and --study are optional if you have set active context
265
268
  Iterations carry the URL/content. If the study has none, create one
266
269
  first with \`ish iteration create\`.
267
270
 
268
- Audience: pass nothing to reuse the iteration's existing testers. Pass
269
- --profile to use specific profiles, or filter flags (--bio, --country,
271
+ People: pass nothing to reuse the iteration's existing participants. Pass
272
+ --person to use specific people, or filter flags (--bio, --country,
270
273
  --gender, --min-age, --max-age, --occupation, --search, --visibility)
271
- with --sample <N> or --all to seed a fresh audience from the workspace
272
- pool.
274
+ with --sample <N> or --all to seed from the workspace pool.
273
275
 
274
276
  Examples:
275
- # Run the latest iteration, reusing its testers:
277
+ # Run the latest iteration, reusing its participants:
276
278
  $ ish study run -y
277
279
 
278
- # Run with an explicit audience:
279
- $ ish study run --profile tp-795,tp-af2
280
+ # Run with explicit people:
281
+ $ ish study run --person p-795,p-af2
280
282
 
281
- # Run with a demographic-filtered sample (3 Swedish profiles aged 35–50):
283
+ # Run with a demographic-filtered sample (3 Swedish people aged 35–50):
282
284
  $ ish study run --country SE --min-age 35 --max-age 50 --sample 3
283
285
 
284
- # Run with every female profile in the workspace:
286
+ # Run with every female person in the workspace:
285
287
  $ ish study run --gender female --all
286
288
 
287
289
  # Run a specific iteration:
@@ -290,7 +292,7 @@ Examples:
290
292
  # Override the simulation config (e.g. for a media study):
291
293
  $ ish study run --config c-c3c
292
294
 
293
- # Cap interactions per tester (default 20 — pass higher to allow deeper
295
+ # Cap interactions per participant (default 20 — pass higher to allow deeper
294
296
  # exploration, lower to cap spend on a known-broken surface):
295
297
  $ ish study run --max-interactions 30
296
298
 
@@ -305,6 +307,26 @@ Examples:
305
307
  await withClient(cmd, async (client, globals) => {
306
308
  const log = (msg) => { if (!globals.quiet)
307
309
  console.error(msg); };
310
+ // Internal-only: per-run ConfigOverrides (model/prompt/sim-settings) layered
311
+ // on top of the base sim config by the backend's merge_config_overrides.
312
+ // Gated behind ISH_INTERNAL (defense-in-depth only — the real authorization
313
+ // belongs on the backend; the API currently accepts config_overrides from any
314
+ // authed caller). Parsed up front so a bad payload fails before seeding participants.
315
+ let configOverrides;
316
+ if (opts.simConfig) {
317
+ if (!process.env.ISH_INTERNAL) {
318
+ throw new Error("--sim-config is an internal-only flag. Set ISH_INTERNAL=1 to use it.");
319
+ }
320
+ const raw = opts.simConfig.startsWith("@")
321
+ ? await readFileOrStdin(opts.simConfig.slice(1))
322
+ : opts.simConfig;
323
+ try {
324
+ configOverrides = JSON.parse(raw);
325
+ }
326
+ catch (e) {
327
+ throw new Error(`--sim-config is not valid JSON: ${e.message}`);
328
+ }
329
+ }
308
330
  const resolvedWorkspace = resolveWorkspace(opts.workspace);
309
331
  // Pattern A (Sprint 2): when `--iteration` is passed without
310
332
  // `--study`, derive the parent study from the iteration itself
@@ -335,7 +357,7 @@ Examples:
335
357
  }
336
358
  // B7: per-POST dispatch timeout (default 120s — long enough to survive
337
359
  // a slow-cold backend, short enough that an agent doesn't sit blind
338
- // for many minutes). Applied to both the testers/batch POST AND the
360
+ // for many minutes). Applied to both the participants/batch POST AND the
339
361
  // simulation start POST so the seed + dispatch budget is the same.
340
362
  const dispatchTimeoutMs = opts.dispatchTimeout
341
363
  ? Math.max(1, parseInt(opts.dispatchTimeout, 10)) * 1000
@@ -344,12 +366,12 @@ Examples:
344
366
  || parseInt(opts.dispatchTimeout, 10) < 1)) {
345
367
  throw new Error(`--dispatch-timeout must be a positive integer (seconds), got "${opts.dispatchTimeout}".`);
346
368
  }
347
- // Step 0: Fetch study (with its iterations + their existing testers)
369
+ // Step 0: Fetch study (with its iterations + their existing participants)
348
370
  const study = await client.get(`/studies/${resolvedStudy}`);
349
371
  const modality = study.modality || "interactive";
350
372
  const isMedia = isMediaModality(modality);
351
373
  const isChat = isChatModality(modality);
352
- // Pair-mode (tester_pair) is read off the iteration once we've
374
+ // Pair-mode (participant_pair) is read off the iteration once we've
353
375
  // resolved it below; set defaults here so the value is in scope.
354
376
  let chatMode = "external_chatbot";
355
377
  let isPair = false;
@@ -381,108 +403,108 @@ Examples:
381
403
  // a clear suggestion rather than masking the problem.
382
404
  if (isChat) {
383
405
  chatMode = readChatMode(iteration.details);
384
- isPair = chatMode === "tester_pair";
406
+ isPair = chatMode === "participant_pair";
385
407
  }
386
408
  if (!iterationHasContent(iteration.details, modality)) {
387
- const flagHint = describeRequiredContentFlag(modality, isPair ? "tester_pair" : undefined);
409
+ const flagHint = describeRequiredContentFlag(modality, isPair ? "participant_pair" : undefined);
388
410
  const iterAlias = tagAlias(ALIAS_PREFIX.iteration, iterationId);
389
- throw new Error(`Iteration "${iterationLabel}" (${iterAlias}) has no ${isMedia ? "content" : isPair ? "audiences/scenarios" : isChat ? "endpoint" : "URL"} configured yet. ` +
411
+ throw new Error(`Iteration "${iterationLabel}" (${iterAlias}) has no ${isMedia ? "content" : isPair ? "people/scenarios" : isChat ? "endpoint" : "URL"} configured yet. ` +
390
412
  `Add ${isMedia ? "content" : isPair ? "the pair-mode payload" : isChat ? "an endpoint" : "a URL"} with ` +
391
413
  `\`ish iteration create --study ${resolvedStudy} ${flagHint}\` ` +
392
414
  `(or update the existing iteration via \`ish iteration update ${iterAlias} --details-json '{...}'\`), then retry.`);
393
415
  }
394
416
  const detailsView = readIterationDetails(iteration.details);
395
- const pairConfig = isPair ? readTesterPairConfig(iteration.details) : undefined;
396
- // Step 2: Resolve audience.
397
- // - If any audience flag is set (--profile / --sample / --all / filter flags),
417
+ const pairConfig = isPair ? readParticipantPairConfig(iteration.details) : undefined;
418
+ // Step 2: Resolve people.
419
+ // - If any person flag is set (--person / --sample / --all / filter flags),
398
420
  // resolve a fresh ID list from the workspace pool via the shared helper.
399
- // - Otherwise reuse the iteration's existing testers.
400
- // - For chat tester_pair iterations, audiences live inside the
421
+ // - Otherwise reuse the iteration's existing participants.
422
+ // - For chat participant_pair iterations, people live inside the
401
423
  // iteration's mode_details and are authoritative; run-time
402
424
  // overrides are refused.
403
- const profileNames = new Map();
404
- const profileIds = [];
405
- const existingTesters = [];
406
- const audienceSet = hasAudienceFlags(opts);
425
+ const personNames = new Map();
426
+ const personIds = [];
427
+ const existingParticipants = [];
428
+ const personSet = hasPersonFlags(opts);
407
429
  if (isPair) {
408
- if (audienceSet) {
409
- throw new Error("tester_pair chat iterations carry their own audiences inside mode_details; run-time audience overrides (--profile / --sample / --all / --country / --gender / --min-age / --max-age / --search / --visibility) are not supported. " +
410
- "To change the audiences, update the iteration via `ish iteration update <id> --details-json '{...}'`.");
430
+ if (personSet) {
431
+ throw new Error("participant_pair chat iterations carry their own people inside mode_details; run-time overrides (--person / --sample / --all / --country / --gender / --min-age / --max-age / --search / --visibility) are not supported. " +
432
+ "To change the people, update the iteration via `ish iteration update <id> --details-json '{...}'`.");
411
433
  }
412
434
  if (!pairConfig) {
413
435
  throw new Error("Pair-mode iteration is missing mode_details; cannot dispatch.");
414
436
  }
415
- // Surface a flat profileIds[] (a then b) so downstream
437
+ // Surface a flat personIds[] (a then b) so downstream
416
438
  // bookkeeping (config resolution, output) still has something to
417
- // chew on. The pair-batch tester-provisioning POST below uses
439
+ // chew on. The pair-batch participant-provisioning POST below uses
418
440
  // the split lists, not this flat one.
419
- for (const pid of pairConfig.audience_a) {
420
- if (!profileNames.has(pid)) {
421
- profileNames.set(pid, "");
422
- profileIds.push(pid);
441
+ for (const pid of pairConfig.group_a) {
442
+ if (!personNames.has(pid)) {
443
+ personNames.set(pid, "");
444
+ personIds.push(pid);
423
445
  }
424
446
  }
425
- for (const pid of pairConfig.audience_b) {
426
- if (!profileNames.has(pid)) {
427
- profileNames.set(pid, "");
428
- profileIds.push(pid);
447
+ for (const pid of pairConfig.group_b) {
448
+ if (!personNames.has(pid)) {
449
+ personNames.set(pid, "");
450
+ personIds.push(pid);
429
451
  }
430
452
  }
431
453
  }
432
- else if (audienceSet) {
433
- const resolved = await resolveAudienceProfileIds(client, resolvedWorkspace, opts, { requireSimulatable: false, allFlagName: "--all" });
434
- profileIds.push(...resolved);
454
+ else if (personSet) {
455
+ const resolved = await resolvePersonIds(client, resolvedWorkspace, opts, { requireSimulatable: false, allFlagName: "--all" });
456
+ personIds.push(...resolved);
435
457
  }
436
- else if (iteration.testers && iteration.testers.length > 0) {
437
- for (const t of iteration.testers) {
438
- const pid = t.tester_profile_id || t.tester_profile?.id;
439
- const name = t.tester_profile?.name;
440
- if (pid && !profileNames.has(pid)) {
441
- profileNames.set(pid, name || "");
442
- profileIds.push(pid);
458
+ else if (iteration.participants && iteration.participants.length > 0) {
459
+ for (const t of iteration.participants) {
460
+ const pid = t.person_id || t.person?.id;
461
+ const name = t.person?.name;
462
+ if (pid && !personNames.has(pid)) {
463
+ personNames.set(pid, name || "");
464
+ personIds.push(pid);
443
465
  }
444
466
  if (t.id) {
445
- existingTesters.push({ id: t.id, tester_profile: { name: name || "Unknown" } });
467
+ existingParticipants.push({ id: t.id, person: { name: name || "Unknown" } });
446
468
  }
447
469
  }
448
470
  }
449
- // Pair iterations always seed fresh testers via the pair-batch
450
- // endpoint; never reuse a stale tester roster from a prior run.
451
- const reuseExistingTesters = !isPair && !audienceSet && existingTesters.length > 0;
452
- // Pair iterations with criteria-only audiences will have empty
453
- // profileIds at this stage if the backend deferred resolution past
471
+ // Pair iterations always seed fresh participants via the pair-batch
472
+ // endpoint; never reuse a stale participant roster from a prior run.
473
+ const reuseExistingParticipants = !isPair && !personSet && existingParticipants.length > 0;
474
+ // Pair iterations with criteria-only groups will have empty
475
+ // personIds at this stage if the backend deferred resolution past
454
476
  // iteration create. That's a valid state — skip the
455
- // "no audience flags" guard for them and let dispatch surface any
477
+ // "no people flags" guard for them and let dispatch surface any
456
478
  // backend-side resolution errors (e.g. pool too small).
457
- const pairCriteriaOnly = isPair && !!pairConfig && profileIds.length === 0
479
+ const pairCriteriaOnly = isPair && !!pairConfig && personIds.length === 0
458
480
  && (!!pairConfig.role_criteria_a || !!pairConfig.role_criteria_b);
459
- if (profileIds.length === 0 && !pairCriteriaOnly) {
460
- throw new Error(`Iteration "${iterationLabel}" has no testers and no audience flags were given. ` +
461
- "Pass --profile <ids>, or filter flags (--country, --gender, --min-age, --max-age, --search, --visibility) with --sample <N> or --all.");
481
+ if (personIds.length === 0 && !pairCriteriaOnly) {
482
+ throw new Error(`Iteration "${iterationLabel}" has no participants and no person flags were given. ` +
483
+ "Pass --person <ids>, or filter flags (--country, --gender, --min-age, --max-age, --search, --visibility) with --sample <N> or --all.");
462
484
  }
463
- // Step 3: Resolve simulation config. Always pre-flight every profile
485
+ // Step 3: Resolve simulation config. Always pre-flight every person
464
486
  // when no --config override is given: missing simulation_config_id
465
487
  // is fatal across all modalities (media + chat batch dispatch use it
466
488
  // per-item; interactive + pair dispatch fail server-side on the
467
- // first sim start) and creating tester rows before discovering it
489
+ // first sim start) and creating participant rows before discovering it
468
490
  // leaves phantom DRAFT rows in the iteration. Pair mode reads
469
- // pairConfig.audience_a; non-pair uses profileIds. profileConfigMap
491
+ // pairConfig.group_a; non-pair uses personIds. personConfigMap
470
492
  // is consumed by the media branch; other branches just need the
471
493
  // validation side effect.
472
494
  const resolvedConfigOverride = opts.config ? resolveId(opts.config) : undefined;
473
- const profileConfigMap = new Map();
495
+ const personConfigMap = new Map();
474
496
  if (!resolvedConfigOverride) {
475
497
  const idsToCheck = isPair && pairConfig
476
- ? [...new Set([...pairConfig.audience_a, ...pairConfig.audience_b])]
477
- : profileIds;
498
+ ? [...new Set([...pairConfig.group_a, ...pairConfig.group_b])]
499
+ : personIds;
478
500
  for (const pid of idsToCheck) {
479
- const profile = await client.get(`/tester-profiles/${pid}`);
501
+ const profile = await client.get(`/people/${pid}`);
480
502
  if (profile.simulation_config_id) {
481
- profileConfigMap.set(pid, profile.simulation_config_id);
503
+ personConfigMap.set(pid, profile.simulation_config_id);
482
504
  }
483
505
  else {
484
- throw new Error(`Profile ${profileNames.get(pid) || pid} has no simulation config assigned.\n` +
485
- "Use --config <id> to specify one, or assign a config to the profile.\n" +
506
+ throw new Error(`Profile ${personNames.get(pid) || pid} has no simulation config assigned.\n` +
507
+ "Use --config <id> to specify one, or assign a config to the person.\n" +
486
508
  "List configs with: ish config list");
487
509
  }
488
510
  }
@@ -496,19 +518,19 @@ Examples:
496
518
  if (study.content_type)
497
519
  log(` Content type: ${study.content_type}`);
498
520
  if (isPair && pairConfig) {
499
- log(` Chat mode: tester_pair`);
500
- // Audience description per side: prefer explicit count when
521
+ log(` Chat mode: participant_pair`);
522
+ // Group description per side: prefer explicit count when
501
523
  // present; otherwise show the criteria filter that the backend
502
524
  // will resolve into a pool.
503
- const describeSide = (audLen, crit) => {
504
- if (audLen > 0)
505
- return `${audLen} profile(s)${crit ? ` (criteria validates list)` : ""}`;
525
+ const describeSide = (groupLen, crit) => {
526
+ if (groupLen > 0)
527
+ return `${groupLen} person(s)${crit ? ` (criteria validates list)` : ""}`;
506
528
  const summary = summarizeRoleCriteria(crit);
507
529
  return summary ? `criteria (${summary}) — pool resolved server-side` : "—";
508
530
  };
509
- log(` Audience A: ${describeSide(pairConfig.audience_a.length, pairConfig.role_criteria_a)}`);
510
- log(` Audience B: ${describeSide(pairConfig.audience_b.length, pairConfig.role_criteria_b)}`);
511
- const explicitConvs = Math.min(pairConfig.audience_a.length, pairConfig.audience_b.length);
531
+ log(` Group A: ${describeSide(pairConfig.group_a.length, pairConfig.role_criteria_a)}`);
532
+ log(` Group B: ${describeSide(pairConfig.group_b.length, pairConfig.role_criteria_b)}`);
533
+ const explicitConvs = Math.min(pairConfig.group_a.length, pairConfig.group_b.length);
512
534
  const criteriaResolved = !!pairConfig.role_criteria_a || !!pairConfig.role_criteria_b;
513
535
  if (explicitConvs > 0 && !criteriaResolved) {
514
536
  log(` Conversations: ${explicitConvs} (1:1 by index)`);
@@ -573,13 +595,13 @@ Examples:
573
595
  if (opts.language)
574
596
  log(` Language: ${opts.language}`);
575
597
  if (!isPair) {
576
- log(` Profiles (${profileIds.length}):`);
577
- for (const pid of profileIds) {
578
- const name = profileNames.get(pid);
598
+ log(` Profiles (${personIds.length}):`);
599
+ for (const pid of personIds) {
600
+ const name = personNames.get(pid);
579
601
  log(` - ${name ? `${name} (${pid})` : pid}`);
580
602
  }
581
- const testerCount = profileIds.length;
582
- if (testerCount > 0) {
603
+ const participantCount = personIds.length;
604
+ if (participantCount > 0) {
583
605
  if (isChat) {
584
606
  const turnsForChat = opts.maxTurns
585
607
  ? parseInt(opts.maxTurns, 10)
@@ -587,7 +609,7 @@ Examples:
587
609
  ? iteration.details.max_turns
588
610
  : 14);
589
611
  if (Number.isFinite(turnsForChat)) {
590
- const est = estimateChatSolo({ testerCount, maxTurns: turnsForChat });
612
+ const est = estimateChatSolo({ participantCount, maxTurns: turnsForChat });
591
613
  log(` Credits (est): ≈ ${est.upper_bound} credit(s) upper bound — ${est.breakdown}`);
592
614
  }
593
615
  }
@@ -600,7 +622,7 @@ Examples:
600
622
  : `CLI default — pass --max-interactions to override`;
601
623
  log(` Max steps: ${stepsForMedia} (${source})`);
602
624
  if (Number.isFinite(stepsForMedia)) {
603
- const est = estimateMediaRun({ testerCount, maxInteractions: stepsForMedia });
625
+ const est = estimateMediaRun({ participantCount, maxInteractions: stepsForMedia });
604
626
  log(` Credits (est): ≈ ${est.upper_bound} credit(s) upper bound — ${est.breakdown}`);
605
627
  }
606
628
  }
@@ -621,10 +643,10 @@ Examples:
621
643
  const { ensureBrowser } = await import("../lib/local-sim/install.js");
622
644
  await ensureBrowser({ quiet: globals.quiet, skipPrompt: globals.json });
623
645
  }
624
- // Step 5: Either reuse the iteration's testers or batch-create new ones
625
- let createdTesters;
646
+ // Step 5: Either reuse the iteration's participants or batch-create new ones
647
+ let createdParticipants;
626
648
  // Pair-mode bookkeeping: the dispatch endpoint takes
627
- // `conversation_ids`, not tester ids. We populate this list either
649
+ // `conversation_ids`, not participant ids. We populate this list either
628
650
  // by reusing the iteration's existing Conversation rows or by
629
651
  // calling pair-batch.
630
652
  let pairConversationIds = [];
@@ -633,24 +655,24 @@ Examples:
633
655
  // 1. If the iteration already carries `conversations[]` from a
634
656
  // prior dispatch, reuse them — skip pair-batch entirely.
635
657
  // 2. Otherwise call pair-batch with the resolved
636
- // audience UUID lists. Criteria-only iterations should
637
- // already have audiences materialised at iteration-create
658
+ // group UUID lists. Criteria-only iterations should
659
+ // already have groups materialised at iteration-create
638
660
  // time; if they're still empty here, the backend's
639
- // `PairAudienceResolutionError` is the authoritative
661
+ // `PairGroupResolutionError` is the authoritative
640
662
  // failure mode — refuse before hitting pair-batch.
641
663
  //
642
664
  // Wire shapes per backend `app/api/iterations/routers`:
643
- // POST /iterations/{id}/testers/pair-batch
665
+ // POST /iterations/{id}/participants/pair-batch
644
666
  // body : { side_a: UUID[1..20], side_b: UUID[1..20] (equal len),
645
667
  // language?: str }
646
668
  // reply : { conversations: [{ conversation_id, pair_index,
647
- // tester_a_id, tester_b_id }] }
669
+ // participant_a_id, participant_b_id }] }
648
670
  const existingConvs = iteration.conversations ?? [];
649
671
  const reusable = [];
650
672
  for (const c of existingConvs) {
651
673
  const cid = c.conversation_id || c.id;
652
- if (cid && c.tester_a_id && c.tester_b_id) {
653
- reusable.push({ conversation_id: cid, tester_a_id: c.tester_a_id, tester_b_id: c.tester_b_id });
674
+ if (cid && c.participant_a_id && c.participant_b_id) {
675
+ reusable.push({ conversation_id: cid, participant_a_id: c.participant_a_id, participant_b_id: c.participant_b_id });
654
676
  }
655
677
  }
656
678
  let pairRows;
@@ -659,79 +681,79 @@ Examples:
659
681
  log(`Reusing ${reusable.length} existing conversation${reusable.length > 1 ? "s" : ""} on iteration "${iterationLabel}"`);
660
682
  }
661
683
  else {
662
- if (pairConfig.audience_a.length === 0 || pairConfig.audience_b.length === 0) {
663
- throw new Error("Pair-mode iteration has empty audience_a / audience_b and no conversations yet. " +
684
+ if (pairConfig.group_a.length === 0 || pairConfig.group_b.length === 0) {
685
+ throw new Error("Pair-mode iteration has empty group_a / group_b and no conversations yet. " +
664
686
  "If this iteration was created with --role-criteria-a/-b, the backend should have " +
665
- "resolved a profile pool at create time — try `ish iteration get <id>` to fetch a " +
666
- "fresh shape, or recreate with explicit --profile-a/-b.");
687
+ "resolved a person pool at create time — try `ish iteration get <id>` to fetch a " +
688
+ "fresh shape, or recreate with explicit --person-a/-b.");
667
689
  }
668
- log(`Provisioning ${pairConfig.audience_a.length} pair conversation${pairConfig.audience_a.length > 1 ? "s" : ""}...`);
669
- const pairBatchResult = await client.post(`/iterations/${iterationId}/testers/pair-batch`, {
670
- side_a: pairConfig.audience_a,
671
- side_b: pairConfig.audience_b,
690
+ log(`Provisioning ${pairConfig.group_a.length} pair conversation${pairConfig.group_a.length > 1 ? "s" : ""}...`);
691
+ const pairBatchResult = await client.post(`/iterations/${iterationId}/participants/pair-batch`, {
692
+ side_a: pairConfig.group_a,
693
+ side_b: pairConfig.group_b,
672
694
  ...(opts.language && { language: opts.language }),
673
695
  }, { timeout: dispatchTimeoutMs });
674
696
  pairRows = (pairBatchResult.conversations ?? []).map((c) => ({
675
697
  conversation_id: c.conversation_id,
676
- tester_a_id: c.tester_a_id,
677
- tester_b_id: c.tester_b_id,
698
+ participant_a_id: c.participant_a_id,
699
+ participant_b_id: c.participant_b_id,
678
700
  }));
679
701
  if (pairRows.length === 0) {
680
702
  throw new Error("Pair-batch returned no conversations. The backend response did not include any conversation IDs.");
681
703
  }
682
- log(`Created ${pairRows.length * 2} testers (${pairRows.length} conversation${pairRows.length > 1 ? "s" : ""})`);
704
+ log(`Created ${pairRows.length * 2} participants (${pairRows.length} conversation${pairRows.length > 1 ? "s" : ""})`);
683
705
  }
684
706
  pairConversationIds = pairRows.map((r) => r.conversation_id);
685
- // Flatten both sides' tester IDs for downstream bookkeeping:
707
+ // Flatten both sides' participant IDs for downstream bookkeeping:
686
708
  // error-tagging (`seeded_but_not_dispatched_ids`), poll filtering,
687
709
  // and JSON output. Names aren't returned by pair-batch; agents
688
710
  // who care can correlate via `ish iteration get <id>`.
689
- createdTesters = [];
711
+ createdParticipants = [];
690
712
  for (let i = 0; i < pairRows.length; i++) {
691
713
  const row = pairRows[i];
692
- createdTesters.push({
693
- id: row.tester_a_id,
694
- tester_profile: { name: `pair ${i} side A` },
714
+ createdParticipants.push({
715
+ id: row.participant_a_id,
716
+ person: { name: `pair ${i} side A` },
695
717
  });
696
- createdTesters.push({
697
- id: row.tester_b_id,
698
- tester_profile: { name: `pair ${i} side B` },
718
+ createdParticipants.push({
719
+ id: row.participant_b_id,
720
+ person: { name: `pair ${i} side B` },
699
721
  });
700
722
  }
701
723
  }
702
- else if (reuseExistingTesters && existingTesters.length > 0) {
703
- createdTesters = existingTesters;
704
- log(`Reusing ${createdTesters.length} existing tester${createdTesters.length > 1 ? "s" : ""} from iteration "${iterationLabel}"`);
724
+ else if (reuseExistingParticipants && existingParticipants.length > 0) {
725
+ createdParticipants = existingParticipants;
726
+ log(`Reusing ${createdParticipants.length} existing participant${createdParticipants.length > 1 ? "s" : ""} from iteration "${iterationLabel}"`);
705
727
  }
706
728
  else {
707
- const testerInputs = profileIds.map((profileId) => ({
708
- tester_profile_id: profileId,
709
- tester_type: "ai",
729
+ const participantInputs = personIds.map((profileId) => ({
730
+ person_id: profileId,
731
+ participant_type: "ai",
710
732
  status: "draft",
711
733
  ...(opts.language && { language: opts.language }),
712
734
  ...(!isMedia && !isChat && { platform: detailsView.platform || "browser" }),
713
735
  }));
714
- log(`Creating ${testerInputs.length} tester${testerInputs.length > 1 ? "s" : ""}...`);
715
- const batchResult = await client.post(`/iterations/${iterationId}/testers/batch`, { testers: testerInputs }, { timeout: dispatchTimeoutMs });
716
- createdTesters = batchResult.testers;
717
- log(`Created ${createdTesters.length} tester${createdTesters.length > 1 ? "s" : ""}`);
736
+ log(`Creating ${participantInputs.length} participant${participantInputs.length > 1 ? "s" : ""}...`);
737
+ const batchResult = await client.post(`/iterations/${iterationId}/participants/batch`, { participants: participantInputs }, { timeout: dispatchTimeoutMs });
738
+ createdParticipants = batchResult.participants;
739
+ log(`Created ${createdParticipants.length} participant${createdParticipants.length > 1 ? "s" : ""}`);
718
740
  }
719
741
  // Step 6: Dispatch
720
742
  if (opts.local) {
721
743
  if (isMedia || isChat) {
722
744
  throw new Error("Local mode is only supported for interactive simulations.");
723
745
  }
724
- const testerNameMap = new Map();
725
- for (const t of createdTesters) {
726
- testerNameMap.set(t.id, t.tester_profile?.name ?? "Unknown");
746
+ const participantNameMap = new Map();
747
+ for (const t of createdParticipants) {
748
+ participantNameMap.set(t.id, t.person?.name ?? "Unknown");
727
749
  }
728
750
  const { runLocalSimulations } = await import("../lib/local-sim/loop.js");
729
751
  await runLocalSimulations(client, {
730
752
  workspaceId: resolvedWorkspace,
731
753
  studyId: resolvedStudy,
732
754
  iterationId,
733
- testerIds: createdTesters.map((t) => t.id),
734
- testerNames: testerNameMap,
755
+ participantIds: createdParticipants.map((t) => t.id),
756
+ participantNames: participantNameMap,
735
757
  url: detailsView.url,
736
758
  screenFormat: detailsView.screenFormat,
737
759
  locale: detailsView.locale,
@@ -745,31 +767,31 @@ Examples:
745
767
  json: globals.json,
746
768
  });
747
769
  if (globals.json) {
748
- const testersOut = createdTesters.map((t) => ({
770
+ const participantsOut = createdParticipants.map((t) => ({
749
771
  id: t.id,
750
- alias: tagAlias(ALIAS_PREFIX.tester, String(t.id)),
751
- profile_name: t.tester_profile?.name,
772
+ alias: tagAlias(ALIAS_PREFIX.participant, String(t.id)),
773
+ person_name: t.person?.name,
752
774
  }));
753
775
  output({
754
776
  iteration_id: iterationId,
755
- testers: testersOut,
756
- tester_ids: testersOut.map((t) => t.id),
757
- tester_aliases: testersOut.map((t) => t.alias),
777
+ participants: participantsOut,
778
+ participant_ids: participantsOut.map((t) => t.id),
779
+ participant_aliases: participantsOut.map((t) => t.alias),
758
780
  mode: "local",
759
781
  }, true);
760
782
  }
761
783
  return;
762
784
  }
763
- log(`Starting ${createdTesters.length} simulation${createdTesters.length > 1 ? "s" : ""}...`);
785
+ log(`Starting ${createdParticipants.length} simulation${createdParticipants.length > 1 ? "s" : ""}...`);
764
786
  let simResults;
765
787
  // B7 / Pattern G: tag any failure during the dispatch POST with the
766
- // testers that already exist server-side. The client doesn't know
788
+ // participants that already exist server-side. The client doesn't know
767
789
  // whether the backend processed our POST or never received it, so
768
790
  // re-running `study run` would double-seed (we'd create another
769
- // batch of testers on top of these). Instead, surface the seeded
791
+ // batch of participants on top of these). Instead, surface the seeded
770
792
  // ids in the error envelope under `seeded_but_not_dispatched_ids`
771
793
  // / `_aliases` so the agent can resume by polling them or calling
772
- // `study tester delete <id>` then retrying.
794
+ // `study participant delete <id>` then retrying.
773
795
  const dispatchAttempt = async (fn) => {
774
796
  try {
775
797
  return await fn();
@@ -777,8 +799,8 @@ Examples:
777
799
  catch (err) {
778
800
  if (err instanceof Error) {
779
801
  const tagged = err;
780
- tagged.seeded_but_not_dispatched_ids = createdTesters.map((t) => t.id);
781
- tagged.seeded_but_not_dispatched_aliases = createdTesters.map((t) => tagAlias(ALIAS_PREFIX.tester, String(t.id)));
802
+ tagged.seeded_but_not_dispatched_ids = createdParticipants.map((t) => t.id);
803
+ tagged.seeded_but_not_dispatched_aliases = createdParticipants.map((t) => tagAlias(ALIAS_PREFIX.participant, String(t.id)));
782
804
  }
783
805
  throw err;
784
806
  }
@@ -803,22 +825,22 @@ Examples:
803
825
  // chat_credit_cost(max_turns) * 2 * len(conversation_ids).
804
826
  let pairConfigId = resolvedConfigOverride;
805
827
  if (!pairConfigId) {
806
- // Fall back to the first audience_a profile's
828
+ // Fall back to the first group_a person's
807
829
  // simulation_config_id. Pair dispatch takes a single config
808
- // for the whole batch, so we don't need the per-profile map
830
+ // for the whole batch, so we don't need the per-person map
809
831
  // the external_chatbot path builds. Step 3 already populated
810
- // profileConfigMap with every audience profile's config when
832
+ // personConfigMap with every group person's config when
811
833
  // --config was not passed, so reuse that.
812
- const fallbackProfileId = pairConfig.audience_a[0];
834
+ const fallbackProfileId = pairConfig.group_a[0];
813
835
  if (!fallbackProfileId) {
814
- throw new Error("Pair-mode dispatch requires --config <id>: the iteration has no audience profile to draw a default config_id from.");
836
+ throw new Error("Pair-mode dispatch requires --config <id>: the iteration has no person to draw a default config_id from.");
815
837
  }
816
- pairConfigId = profileConfigMap.get(fallbackProfileId);
838
+ pairConfigId = personConfigMap.get(fallbackProfileId);
817
839
  if (!pairConfigId) {
818
840
  // Defensive: Step 3 should have either populated the map or
819
841
  // thrown. If we land here something upstream changed.
820
842
  throw new Error(`Pair-mode dispatch requires a config_id. Profile ${fallbackProfileId} has no simulation config assigned and --config was not passed.\n` +
821
- "Use --config <id> to specify one, or assign a config to the profile.\n" +
843
+ "Use --config <id> to specify one, or assign a config to the person.\n" +
822
844
  "List configs with: ish config list");
823
845
  }
824
846
  }
@@ -833,10 +855,10 @@ Examples:
833
855
  simResults = simResult.results;
834
856
  }
835
857
  else {
836
- const chatBatchItems = createdTesters.map((t, i) => ({
858
+ const chatBatchItems = createdParticipants.map((t, i) => ({
837
859
  study_id: resolvedStudy,
838
- tester_id: t.id,
839
- config_id: resolvedConfigOverride || profileConfigMap.get(profileIds[i]),
860
+ participant_id: t.id,
861
+ config_id: resolvedConfigOverride || personConfigMap.get(personIds[i]),
840
862
  ...(opts.language && { language: opts.language }),
841
863
  }));
842
864
  const simResult = await dispatchAttempt(() => client.post("/simulation/chat/start/batch", {
@@ -849,11 +871,12 @@ Examples:
849
871
  }
850
872
  }
851
873
  else if (isMedia) {
852
- const mediaBatchItems = createdTesters.map((t, i) => ({
874
+ const mediaBatchItems = createdParticipants.map((t, i) => ({
853
875
  study_id: resolvedStudy,
854
- tester_id: t.id,
855
- config_id: resolvedConfigOverride || profileConfigMap.get(profileIds[i]),
876
+ participant_id: t.id,
877
+ config_id: resolvedConfigOverride || personConfigMap.get(personIds[i]),
856
878
  ...(opts.language && { language: opts.language }),
879
+ ...(configOverrides !== undefined && { config_overrides: configOverrides }),
857
880
  }));
858
881
  const simResult = await dispatchAttempt(() => client.post("/simulation/media/start/batch", {
859
882
  product_id: resolvedWorkspace,
@@ -863,12 +886,13 @@ Examples:
863
886
  simResults = simResult.results;
864
887
  }
865
888
  else {
866
- const simItems = createdTesters.map((t) => ({
889
+ const simItems = createdParticipants.map((t) => ({
867
890
  study_id: resolvedStudy,
868
- tester_id: t.id,
891
+ participant_id: t.id,
869
892
  ...(resolvedConfigOverride && { config_id: resolvedConfigOverride }),
870
893
  ...(opts.language && { language: opts.language }),
871
894
  ...(detailsView.locale && { locale: detailsView.locale }),
895
+ ...(configOverrides !== undefined && { config_overrides: configOverrides }),
872
896
  }));
873
897
  const simResult = await dispatchAttempt(() => client.post("/simulation/interactive/start/batch", {
874
898
  product_id: resolvedWorkspace,
@@ -880,7 +904,7 @@ Examples:
880
904
  }, { timeout: dispatchTimeoutMs }));
881
905
  simResults = simResult.results;
882
906
  }
883
- // Pair-mode preview block: surface the audience sizes + scenario
907
+ // Pair-mode preview block: surface the group sizes + scenario
884
908
  // previews + initiator in the JSON envelope so agents can verify
885
909
  // what they just dispatched without needing a follow-up
886
910
  // `iteration get`. Mirrors the human confirmation block (which is
@@ -891,12 +915,12 @@ Examples:
891
915
  ? iteration.details.max_turns
892
916
  : 14);
893
917
  const pairPreview = isPair && pairConfig ? {
894
- mode: "tester_pair",
895
- audience_a_size: pairConfig.audience_a.length,
896
- audience_b_size: pairConfig.audience_b.length,
918
+ mode: "participant_pair",
919
+ group_a_size: pairConfig.group_a.length,
920
+ group_b_size: pairConfig.group_b.length,
897
921
  // Post-dispatch we know the actual conversation count from the
898
922
  // pair-batch (or reuse) result. This is the authoritative number
899
- // — better than guessing from audience length, which may diverge
923
+ // — better than guessing from group length, which may diverge
900
924
  // when the backend trims to the smaller side.
901
925
  conversation_count: pairConversationIds.length,
902
926
  conversation_ids: pairConversationIds,
@@ -926,12 +950,12 @@ Examples:
926
950
  // Non-pair credit estimate — surfaced as a top-level field in the
927
951
  // JSON envelope alongside `pair_preview.credit_estimate`. Mirrors
928
952
  // backend formulas (`media_credit_cost` / `chat_credit_cost`).
929
- // null when we can't estimate (criteria-only audience, etc.).
953
+ // null when we can't estimate (criteria-only group, etc.).
930
954
  const nonPairCreditEstimate = (() => {
931
955
  if (isPair)
932
956
  return null;
933
- const testerCount = createdTesters.length || profileIds.length;
934
- if (testerCount <= 0)
957
+ const participantCount = createdParticipants.length || personIds.length;
958
+ if (participantCount <= 0)
935
959
  return null;
936
960
  if (isChat) {
937
961
  const turns = opts.maxTurns
@@ -941,25 +965,25 @@ Examples:
941
965
  : 14);
942
966
  if (!Number.isFinite(turns))
943
967
  return null;
944
- return estimateChatSolo({ testerCount, maxTurns: turns });
968
+ return estimateChatSolo({ participantCount, maxTurns: turns });
945
969
  }
946
970
  const steps = resolveMaxInteractions(opts.maxInteractions, iteration.details);
947
971
  if (!Number.isFinite(steps))
948
972
  return null;
949
- return estimateMediaRun({ testerCount, maxInteractions: steps });
973
+ return estimateMediaRun({ participantCount, maxInteractions: steps });
950
974
  })();
951
975
  if (!opts.wait) {
952
976
  if (globals.json) {
953
- const testersOut = createdTesters.map((t) => ({
977
+ const participantsOut = createdParticipants.map((t) => ({
954
978
  id: t.id,
955
- alias: tagAlias(ALIAS_PREFIX.tester, String(t.id)),
956
- profile_name: t.tester_profile?.name,
979
+ alias: tagAlias(ALIAS_PREFIX.participant, String(t.id)),
980
+ person_name: t.person?.name,
957
981
  }));
958
982
  output({
959
983
  iteration_id: iterationId,
960
- testers: testersOut,
961
- tester_ids: testersOut.map((t) => t.id),
962
- tester_aliases: testersOut.map((t) => t.alias),
984
+ participants: participantsOut,
985
+ participant_ids: participantsOut.map((t) => t.id),
986
+ participant_aliases: participantsOut.map((t) => t.alias),
963
987
  url: getWebUrl(globals, `/${resolvedWorkspace}/${resolvedStudy}/timeline`),
964
988
  ...(pairPreview && { pair_preview: pairPreview }),
965
989
  ...(nonPairCreditEstimate && { credit_estimate: nonPairCreditEstimate }),
@@ -968,9 +992,9 @@ Examples:
968
992
  }
969
993
  else {
970
994
  for (let i = 0; i < simResults.length; i++) {
971
- const tester = createdTesters[i];
972
- const profileName = tester?.tester_profile?.name || "Unknown";
973
- log(` ${profileName.padEnd(24)} QUEUED`);
995
+ const participant = createdParticipants[i];
996
+ const personName = participant?.person?.name || "Unknown";
997
+ log(` ${personName.padEnd(24)} QUEUED`);
974
998
  }
975
999
  const url = getWebUrl(globals, `/${resolvedWorkspace}/${resolvedStudy}/timeline`);
976
1000
  log(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
@@ -978,27 +1002,27 @@ Examples:
978
1002
  }
979
1003
  return;
980
1004
  }
981
- // --wait: block until all dispatched testers reach a terminal state
1005
+ // --wait: block until all dispatched participants reach a terminal state
982
1006
  const timeoutMs = parseWaitTimeout(opts.timeout);
983
- const dispatchedIds = new Set(createdTesters.map((t) => t.id));
1007
+ const dispatchedIds = new Set(createdParticipants.map((t) => t.id));
984
1008
  log(`Waiting for ${dispatchedIds.size} simulation${dispatchedIds.size > 1 ? "s" : ""} to finish...`);
985
1009
  const { rows, isMedia: pollIsMedia } = await pollStudyUntilDone(client, {
986
1010
  studyId: resolvedStudy,
987
- testerIds: dispatchedIds,
1011
+ participantIds: dispatchedIds,
988
1012
  timeoutMs,
989
1013
  quiet: globals.quiet,
990
1014
  });
991
1015
  if (globals.json) {
992
- const testersOut = createdTesters.map((t) => ({
1016
+ const participantsOut = createdParticipants.map((t) => ({
993
1017
  id: t.id,
994
- alias: tagAlias(ALIAS_PREFIX.tester, String(t.id)),
995
- profile_name: t.tester_profile?.name,
1018
+ alias: tagAlias(ALIAS_PREFIX.participant, String(t.id)),
1019
+ person_name: t.person?.name,
996
1020
  }));
997
1021
  output({
998
1022
  iteration_id: iterationId,
999
- testers: testersOut,
1000
- tester_ids: testersOut.map((t) => t.id),
1001
- tester_aliases: testersOut.map((t) => t.alias),
1023
+ participants: participantsOut,
1024
+ participant_ids: participantsOut.map((t) => t.id),
1025
+ participant_aliases: participantsOut.map((t) => t.alias),
1002
1026
  url: getWebUrl(globals, `/${resolvedWorkspace}/${resolvedStudy}/timeline`),
1003
1027
  ...(pairPreview && { pair_preview: pairPreview }),
1004
1028
  ...(nonPairCreditEstimate && { credit_estimate: nonPairCreditEstimate }),
@@ -1017,28 +1041,28 @@ Examples:
1017
1041
  study
1018
1042
  .command("poll")
1019
1043
  .description("Check simulation progress for a study")
1020
- .argument("[tester_id]", "Tester ID (alias or UUID; from `ish study run --json`.tester_aliases[])")
1044
+ .argument("[participant_id]", "Participant ID (alias or UUID; from `ish study run --json`.participant_aliases[])")
1021
1045
  .option("--study <id>", "Study ID (poll all simulations for study)")
1022
- .option("--workspace <id>", "Workspace ID; accepted for consistency (workspace is inferred from --study/--iteration/<tester_id>)")
1023
- .addHelpText("after", "\nExamples:\n $ ish study poll --study <study_id>\n $ ish study poll <tester_id> --json\n\nGet tester IDs from `ish study run --json` (.tester_aliases[] / .tester_ids[]).")
1024
- .action(async (testerId, opts, cmd) => {
1046
+ .option("--workspace <id>", "Workspace ID; accepted for consistency (workspace is inferred from --study/--iteration/<participant_id>)")
1047
+ .addHelpText("after", "\nExamples:\n $ ish study poll --study <study_id>\n $ ish study poll <participant_id> --json\n\nGet participant IDs from `ish study run --json` (.participant_aliases[] / .participant_ids[]).")
1048
+ .action(async (participantId, opts, cmd) => {
1025
1049
  await withClient(cmd, async (client, globals) => {
1026
- if (testerId) {
1027
- const data = await client.get(`/simulation/status/${resolveId(testerId)}`);
1050
+ if (participantId) {
1051
+ const data = await client.get(`/simulation/status/${resolveId(participantId)}`);
1028
1052
  output(data, globals.json);
1029
1053
  return;
1030
1054
  }
1031
1055
  // M7: fall back to the active study (set by `ish study use`) when
1032
- // neither a tester_id nor an explicit --study is passed. This brings
1056
+ // neither a participant_id nor an explicit --study is passed. This brings
1033
1057
  // poll into parity with `study results` / `study wait` / `study run`,
1034
1058
  // all of which already honor the active study. Without the fallback
1035
1059
  // an agent that ran `study use s-...` then `study poll` would get a
1036
- // confusing "Provide a tester_id argument or --study flag" error.
1060
+ // confusing "Provide a participant_id argument or --study flag" error.
1037
1061
  const rid = resolveStudy(opts.study);
1038
1062
  const study = await client.get(`/studies/${rid}`);
1039
1063
  const isMedia = isMediaModality(study.modality);
1040
- const allTesters = flattenTesterStatuses(study.iterations);
1041
- formatSimulationPoll(allTesters, globals.json, isMedia);
1064
+ const allParticipants = flattenParticipantStatuses(study.iterations);
1065
+ formatSimulationPoll(allParticipants, globals.json, isMedia);
1042
1066
  if (!globals.json && study.product_id) {
1043
1067
  const url = getWebUrl(globals, `/${study.product_id}/${rid}/timeline`);
1044
1068
  console.error(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
@@ -1049,21 +1073,21 @@ Examples:
1049
1073
  study
1050
1074
  .command("wait")
1051
1075
  .description("Poll until simulations reach a terminal state (completed/errored/failed/cancelled)")
1052
- .argument("[tester_id]", "Tester ID (alias or UUID; from `ish study run --json`.tester_aliases[])")
1053
- .option("--study <id>", "Study ID (wait for all testers in the study)")
1054
- .option("--iteration <id>", "Iteration ID (wait for testers in this iteration only)")
1076
+ .argument("[participant_id]", "Participant ID (alias or UUID; from `ish study run --json`.participant_aliases[])")
1077
+ .option("--study <id>", "Study ID (wait for all participants in the study)")
1078
+ .option("--iteration <id>", "Iteration ID (wait for participants in this iteration only)")
1055
1079
  .option("--timeout <s>", "Max seconds to wait (default 300)")
1056
- .option("--workspace <id>", "Workspace ID; accepted for consistency (workspace is inferred from --study/--iteration/<tester_id>)")
1057
- .addHelpText("after", "\nExamples:\n $ ish study wait # wait on the active study\n $ ish study wait --iteration i-d4e\n $ ish study wait <tester_id> --timeout 600\n\nGet tester IDs from `ish study run --json` (.tester_aliases[] / .tester_ids[]).")
1058
- .action(async (testerId, opts, cmd) => {
1080
+ .option("--workspace <id>", "Workspace ID; accepted for consistency (workspace is inferred from --study/--iteration/<participant_id>)")
1081
+ .addHelpText("after", "\nExamples:\n $ ish study wait # wait on the active study\n $ ish study wait --iteration i-d4e\n $ ish study wait <participant_id> --timeout 600\n\nGet participant IDs from `ish study run --json` (.participant_aliases[] / .participant_ids[]).")
1082
+ .action(async (participantId, opts, cmd) => {
1059
1083
  await withClient(cmd, async (client, globals) => {
1060
1084
  const timeoutMs = parseWaitTimeout(opts.timeout);
1061
- if (testerId) {
1085
+ if (participantId) {
1062
1086
  const start = Date.now();
1063
1087
  let lastStatus = "";
1064
- const resolvedTester = resolveId(testerId);
1088
+ const resolvedParticipant = resolveId(participantId);
1065
1089
  while (true) {
1066
- const data = await client.get(`/simulation/status/${resolvedTester}`, undefined, { timeout: 60_000 });
1090
+ const data = await client.get(`/simulation/status/${resolvedParticipant}`, undefined, { timeout: 60_000 });
1067
1091
  const status = String(data.status ?? "unknown");
1068
1092
  if (!globals.quiet && status !== lastStatus) {
1069
1093
  process.stderr.write(` ${status}\n`);
@@ -1074,20 +1098,20 @@ Examples:
1074
1098
  return;
1075
1099
  }
1076
1100
  if (Date.now() - start > timeoutMs) {
1077
- // M8 + M9 (per-tester wait): structured wait_timeout with the
1101
+ // M8 + M9 (per-participant wait): structured wait_timeout with the
1078
1102
  // current status as `progress.rows[0]` so `study wait <id>`
1079
1103
  // always emits machine-readable final state.
1080
- throw new WaitTimeoutError(`Timed out after ${Math.round(timeoutMs / 1000)}s waiting for tester ${testerId}. Last status: ${status}.`, {
1081
- study_id: resolvedTester,
1104
+ throw new WaitTimeoutError(`Timed out after ${Math.round(timeoutMs / 1000)}s waiting for participant ${participantId}. Last status: ${status}.`, {
1105
+ study_id: resolvedParticipant,
1082
1106
  timeout_seconds: Math.round(timeoutMs / 1000),
1083
1107
  done: 0,
1084
1108
  total: 1,
1085
1109
  pending: 1,
1086
1110
  rows: [
1087
1111
  {
1088
- id: resolvedTester,
1112
+ id: resolvedParticipant,
1089
1113
  status,
1090
- tester_name: String(data.tester_name ?? "Unknown"),
1114
+ participant_name: String(data.participant_name ?? "Unknown"),
1091
1115
  interaction_count: 0,
1092
1116
  },
1093
1117
  ],
@@ -1133,57 +1157,57 @@ Examples:
1133
1157
  study
1134
1158
  .command("cancel")
1135
1159
  .description("Cancel a running simulation")
1136
- .argument("<tester_id>", "Tester ID (alias or UUID; from `ish study run --json`.tester_aliases[])")
1137
- .option("--workspace <id>", "Workspace ID; accepted for consistency (workspace is inferred from <tester_id>)")
1138
- .addHelpText("after", "\nExamples:\n $ ish study cancel t-072\n $ ish study cancel <uuid>\n\nGet tester IDs from `ish study run --json` (.tester_aliases[] / .tester_ids[]).")
1139
- .action(async (testerId, _opts, cmd) => {
1160
+ .argument("<participant_id>", "Participant ID (alias or UUID; from `ish study run --json`.participant_aliases[])")
1161
+ .option("--workspace <id>", "Workspace ID; accepted for consistency (workspace is inferred from <participant_id>)")
1162
+ .addHelpText("after", "\nExamples:\n $ ish study cancel pt-072\n $ ish study cancel <uuid>\n\nGet participant IDs from `ish study run --json` (.participant_aliases[] / .participant_ids[]).")
1163
+ .action(async (participantId, _opts, cmd) => {
1140
1164
  await withClient(cmd, async (client, globals) => {
1141
- const data = await client.post(`/simulation/cancel/${resolveId(testerId)}`);
1165
+ const data = await client.post(`/simulation/cancel/${resolveId(participantId)}`);
1142
1166
  output(data, globals.json);
1143
1167
  });
1144
1168
  });
1145
1169
  // --- Extend ---
1146
1170
  //
1147
- // Resume a terminal tester with `additional_steps` more turns — the
1171
+ // Resume a terminal participant with `additional_steps` more turns — the
1148
1172
  // "start" half of the cancel + extend pair. The backend spawns a NEW
1149
- // tester under the same iteration, branched from the source's last
1173
+ // participant under the same iteration, branched from the source's last
1150
1174
  // interaction; the source row is left untouched. When --instruction is
1151
- // set, the new tester treats it as overriding direction (the backend
1175
+ // set, the new participant treats it as overriding direction (the backend
1152
1176
  // surfaces it in a dedicated <user_added_instructions> block on every
1153
1177
  // prompt — see app-simulation Fix 1).
1154
1178
  study
1155
1179
  .command("extend")
1156
- .description("Extend a terminal tester with more steps (and optionally a mid-run instruction)")
1157
- .argument("<tester_id>", "Tester to extend (alias or UUID). Must be in a terminal state (completed/failed/cancelled).")
1180
+ .description("Extend a terminal participant with more steps (and optionally a mid-run instruction)")
1181
+ .argument("<participant_id>", "Participant to extend (alias or UUID). Must be in a terminal state (completed/failed/cancelled).")
1158
1182
  .option("--add-steps <n>", "Extra interactions past the source's original cap (1-50; backend caps server-side)", "10")
1159
- .option("--instruction <text>", "User message to inject as the new tester resumes. Accepts inline text, `@/path/to/file`, or `-` for stdin.")
1160
- .option("--wait", "Block until the new tester reaches a terminal state")
1183
+ .option("--instruction <text>", "User message to inject as the new participant resumes. Accepts inline text, `@/path/to/file`, or `-` for stdin.")
1184
+ .option("--wait", "Block until the new participant reaches a terminal state")
1161
1185
  .option("--timeout <s>", "Wait timeout in seconds (default 300; only with --wait)")
1162
1186
  .option("--dispatch-timeout <s>", "Per-POST timeout in seconds for the dispatch call (default 120)")
1163
- .option("--workspace <id>", "Workspace ID; accepted for consistency (workspace is inferred from <tester_id>)")
1187
+ .option("--workspace <id>", "Workspace ID; accepted for consistency (workspace is inferred from <participant_id>)")
1164
1188
  .addHelpText("after", `
1165
- The source tester is left untouched; a new tester is spawned under the
1189
+ The source participant is left untouched; a new participant is spawned under the
1166
1190
  same iteration and branched from the source's last interaction. Get the
1167
- new tester ID from \`.tester_id\` / \`.tester_alias\` on the JSON output.
1191
+ new participant ID from \`.participant_id\` / \`.participant_alias\` on the JSON output.
1168
1192
 
1169
1193
  Examples:
1170
1194
  # Add 5 more steps to a completed run (no new instruction):
1171
- $ ish study extend t-072 --add-steps 5
1195
+ $ ish study extend pt-072 --add-steps 5
1172
1196
 
1173
1197
  # Inject a mid-run instruction and wait for completion:
1174
- $ ish study extend t-072 \\
1198
+ $ ish study extend pt-072 \\
1175
1199
  --instruction "Open the language selector and switch to German." \\
1176
1200
  --wait
1177
1201
 
1178
1202
  # Long instruction from a file:
1179
- $ ish study extend t-072 --instruction @/tmp/prompt.txt --wait --timeout 600
1203
+ $ ish study extend pt-072 --instruction @/tmp/prompt.txt --wait --timeout 600
1180
1204
 
1181
1205
  # Instruction from stdin (pipe-friendly):
1182
- $ echo "Try the search bar instead." | ish study extend t-072 --instruction -
1206
+ $ echo "Try the search bar instead." | ish study extend pt-072 --instruction -
1183
1207
 
1184
- Get tester IDs from \`ish study run --json\` (.tester_aliases[] / .tester_ids[]).
1208
+ Get participant IDs from \`ish study run --json\` (.participant_aliases[] / .participant_ids[]).
1185
1209
  See \`ish docs get-page concepts/extending-a-simulation\` for the full mental model.`)
1186
- .action(async (testerId, opts, cmd) => {
1210
+ .action(async (participantId, opts, cmd) => {
1187
1211
  await withClient(cmd, async (client, globals) => {
1188
1212
  // --add-steps: client-side parser fails fast before the network
1189
1213
  // call. Bound mirrors the backend's `le=50` cap; if the backend
@@ -1221,30 +1245,30 @@ See \`ish docs get-page concepts/extending-a-simulation\` for the full mental mo
1221
1245
  parseInt(opts.dispatchTimeout, 10) < 1)) {
1222
1246
  throw new Error(`--dispatch-timeout must be a positive integer (seconds), got "${opts.dispatchTimeout}".`);
1223
1247
  }
1224
- const sourceId = resolveId(testerId);
1225
- const sourceAlias = tagAlias(ALIAS_PREFIX.tester, sourceId);
1248
+ const sourceId = resolveId(participantId);
1249
+ const sourceAlias = tagAlias(ALIAS_PREFIX.participant, sourceId);
1226
1250
  if (!globals.quiet) {
1227
1251
  const stepNote = `${addSteps} step${addSteps === 1 ? "" : "s"}`;
1228
1252
  const instrNote = instruction ? " and a new instruction" : "";
1229
1253
  console.error(`Extending ${sourceAlias} with ${stepNote}${instrNote}...`);
1230
1254
  }
1231
1255
  const body = {
1232
- source_tester_id: sourceId,
1256
+ source_participant_id: sourceId,
1233
1257
  additional_steps: addSteps,
1234
1258
  };
1235
1259
  if (instruction)
1236
1260
  body.user_message = instruction;
1237
1261
  const data = await client.post("/simulation/interactive/extend", body, { timeout: dispatchTimeoutMs });
1238
- const newTesterId = String(data.tester_id);
1239
- const newAlias = tagAlias(ALIAS_PREFIX.tester, newTesterId);
1262
+ const newParticipantId = String(data.participant_id);
1263
+ const newAlias = tagAlias(ALIAS_PREFIX.participant, newParticipantId);
1240
1264
  // UUIDs preserved on the output — `study extend` is a write-path
1241
- // dispatch command and the new `tester_id` is the load-bearing
1242
- // return value (mirrors how `study run` keeps tester_ids in lean
1265
+ // dispatch command and the new `participant_id` is the load-bearing
1266
+ // return value (mirrors how `study run` keeps participant_ids in lean
1243
1267
  // output via the writePath option).
1244
1268
  const baseEnvelope = {
1245
- tester_id: newTesterId,
1246
- tester_alias: newAlias,
1247
- source_tester_id: sourceId,
1269
+ participant_id: newParticipantId,
1270
+ participant_alias: newAlias,
1271
+ source_participant_id: sourceId,
1248
1272
  source_alias: sourceAlias,
1249
1273
  study_id: data.study_id,
1250
1274
  job_id: data.job_id,
@@ -1257,15 +1281,15 @@ See \`ish docs get-page concepts/extending-a-simulation\` for the full mental mo
1257
1281
  output(baseEnvelope, true, { writePath: true });
1258
1282
  }
1259
1283
  else {
1260
- console.error(` New tester: ${newAlias}`);
1284
+ console.error(` New participant: ${newAlias}`);
1261
1285
  if (data.message)
1262
1286
  console.error(` ${data.message}`);
1263
1287
  console.error(` Run \`ish study wait ${newAlias} --timeout 600\` to block until it finishes.`);
1264
1288
  }
1265
1289
  return;
1266
1290
  }
1267
- // --wait: poll the new tester until it reaches a terminal state.
1268
- // Mirrors the per-tester wait block in `study wait <tester_id>`
1291
+ // --wait: poll the new participant until it reaches a terminal state.
1292
+ // Mirrors the per-participant wait block in `study wait <participant_id>`
1269
1293
  // above — same WaitTimeoutError shape (exit 5, retryable) so the
1270
1294
  // failure envelope is consistent across commands.
1271
1295
  const timeoutMs = parseWaitTimeout(opts.timeout);
@@ -1275,7 +1299,7 @@ See \`ish docs get-page concepts/extending-a-simulation\` for the full mental mo
1275
1299
  const start = Date.now();
1276
1300
  let lastStatus = "";
1277
1301
  while (true) {
1278
- const status = await client.get(`/simulation/status/${newTesterId}`, undefined, { timeout: 60_000 });
1302
+ const status = await client.get(`/simulation/status/${newParticipantId}`, undefined, { timeout: 60_000 });
1279
1303
  const s = String(status.status ?? "unknown");
1280
1304
  if (!globals.quiet && s !== lastStatus) {
1281
1305
  process.stderr.write(` ${s}\n`);
@@ -1287,7 +1311,7 @@ See \`ish docs get-page concepts/extending-a-simulation\` for the full mental mo
1287
1311
  ...(typeof status.interaction_count === "number" && {
1288
1312
  interaction_count: status.interaction_count,
1289
1313
  }),
1290
- ...(status.tester_name && { tester_name: status.tester_name }),
1314
+ ...(status.participant_name && { participant_name: status.participant_name }),
1291
1315
  ...(status.error && { error: status.error }),
1292
1316
  };
1293
1317
  if (globals.json) {
@@ -1301,17 +1325,17 @@ See \`ish docs get-page concepts/extending-a-simulation\` for the full mental mo
1301
1325
  return;
1302
1326
  }
1303
1327
  if (Date.now() - start > timeoutMs) {
1304
- throw new WaitTimeoutError(`Timed out after ${Math.round(timeoutMs / 1000)}s waiting for tester ${newAlias}. Last status: ${s}.`, {
1305
- study_id: newTesterId,
1328
+ throw new WaitTimeoutError(`Timed out after ${Math.round(timeoutMs / 1000)}s waiting for participant ${newAlias}. Last status: ${s}.`, {
1329
+ study_id: newParticipantId,
1306
1330
  timeout_seconds: Math.round(timeoutMs / 1000),
1307
1331
  done: 0,
1308
1332
  total: 1,
1309
1333
  pending: 1,
1310
1334
  rows: [
1311
1335
  {
1312
- id: newTesterId,
1336
+ id: newParticipantId,
1313
1337
  status: s,
1314
- tester_name: String(status.tester_name ?? "Unknown"),
1338
+ participant_name: String(status.participant_name ?? "Unknown"),
1315
1339
  interaction_count: typeof status.interaction_count === "number" ? status.interaction_count : 0,
1316
1340
  },
1317
1341
  ],