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