@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
@@ -8,13 +8,14 @@ import { resolveId, tagAlias, ALIAS_PREFIX } from "../lib/alias-store.js";
8
8
  import { loadConfig, saveConfig } from "../config.js";
9
9
  import { formatStudyList, formatStudyDetail, formatStudyResults, buildStudyResultsSummary, buildChatTranscript, output, ValidationError, } from "../lib/output.js";
10
10
  import { VALID_CONTENT_TYPES } from "../lib/types.js";
11
- import { parseAssignment, loadAssignmentsFile, parseQuestion } from "../lib/study-inputs.js";
11
+ import { fetchStudyParticipants } from "../lib/study-participants.js";
12
+ import { parseAssignment, loadAssignmentsFile, validateAssignmentsArray, parseQuestion } from "../lib/study-inputs.js";
12
13
  import { loadQuestionsManifest } from "../lib/ask-questions.js";
13
14
  import { isLocalPath } from "../lib/upload.js";
14
15
  import { normalizeChatMode, validateRoleCriteria } from "../lib/modality.js";
15
16
  import { normalizeEnumValue, SCREEN_FORMATS } from "../lib/enums.js";
16
17
  import { attachStudyRunCommands } from "./study-run.js";
17
- import { attachStudyTesterCommands } from "./study-tester.js";
18
+ import { attachStudyParticipantCommands } from "./study-participant.js";
18
19
  import { attachStudyAnalyzeCommands } from "./study-analyze.js";
19
20
  import { attachStudyScreenshotsCommands } from "./study-screenshots.js";
20
21
  function collectRepeatable(value, prev = []) {
@@ -51,12 +52,14 @@ function resolveAssignments(opts) {
51
52
  return loadAssignmentsFile(opts.assignmentsFile);
52
53
  }
53
54
  if (opts.assignments) {
55
+ let parsed;
54
56
  try {
55
- return JSON.parse(opts.assignments);
57
+ parsed = JSON.parse(opts.assignments);
56
58
  }
57
59
  catch {
58
60
  throw new Error("Invalid --assignments JSON");
59
61
  }
62
+ return validateAssignmentsArray(parsed, "--assignments");
60
63
  }
61
64
  return undefined;
62
65
  }
@@ -120,19 +123,19 @@ Concept pages: ish docs get-page concepts/study
120
123
  .option("--title <title>", "Content title (text + media modalities — image, video, audio, document; optional). Not used for interactive / chat.")
121
124
  .option("--endpoint <id>", "Saved chatbot endpoint id or alias. Creates iteration A inline (chat modality, external_chatbot mode)")
122
125
  .option("--endpoint-config <file>", "ChatbotEndpointConfig JSON file (or `-` for stdin); embedded directly. Mutually exclusive with --endpoint (chat modality, external_chatbot mode)")
123
- .option("--max-turns <n>", "Maximum conversation turns per tester (chat modality only; default 12)", (v) => Number(v))
124
- .option("--chat-mode <mode>", "Chat mode: external_chatbot (default) or tester_pair (two AI audiences talk to each other) — chat modality only")
125
- .option("--audience-a <ids>", "Tester profile IDs/aliases for audience A (comma-separated or repeatable). Pass a single profile and N on --audience-b to broadcast (1×N rehearsal: fix side A, vary side B) — chat tester_pair mode", (value, prev = []) => prev.concat(value.split(",").map((s) => s.trim()).filter(Boolean)), [])
126
- .option("--audience-b <ids>", "Tester profile IDs/aliases for audience B. When both sides are explicit they must be equal length, BUT if either side is a singleton it's auto-broadcast to match the other (1×N rehearsal) — chat tester_pair mode", (value, prev = []) => prev.concat(value.split(",").map((s) => s.trim()).filter(Boolean)), [])
127
- .option("--scenario-a <text-or-@file>", "Side-A scenario + goal — chat tester_pair mode")
128
- .option("--scenario-b <text-or-@file>", "Side-B scenario + goal — chat tester_pair mode")
129
- .option("--initiator-side <a|b>", "Which side speaks first (default: a) — chat tester_pair mode")
130
- .option("--role-criteria-a <json-or-@file>", 'RoleCriteria filter for side A (JSON object or @filepath). Keys: occupation[], min_age, max_age, gender[], country[], education_level_in[], household_in[], locale_type_in[], income_level_in[], employment_status_in[], requires_captions, uses_screen_reader, prefers_reduced_motion, prefers_high_contrast, has_any_accessibility_need. The five *_in arrays accept snake_case spec values; the five accessibility filters are booleans. Use INSTEAD of --audience-a or alongside it. chat tester_pair mode.')
131
- .option("--role-criteria-b <json-or-@file>", "RoleCriteria filter for side B — same shape as --role-criteria-a. chat tester_pair mode.")
126
+ .option("--max-turns <n>", "Maximum conversation turns per participant (chat modality only; default 12)", (v) => Number(v))
127
+ .option("--chat-mode <mode>", "Chat mode: external_chatbot (default) or participant_pair (two AI groups talk to each other) — chat modality only")
128
+ .option("--group-a <ids>", "Person IDs/aliases for group A (comma-separated or repeatable). Pass a single profile and N on --group-b to broadcast (1×N rehearsal: fix side A, vary side B) — chat participant_pair mode", (value, prev = []) => prev.concat(value.split(",").map((s) => s.trim()).filter(Boolean)), [])
129
+ .option("--group-b <ids>", "Person IDs/aliases for group B. When both sides are explicit they must be equal length, BUT if either side is a singleton it's auto-broadcast to match the other (1×N rehearsal) — chat participant_pair mode", (value, prev = []) => prev.concat(value.split(",").map((s) => s.trim()).filter(Boolean)), [])
130
+ .option("--scenario-a <text-or-@file>", "Side-A scenario + goal — chat participant_pair mode")
131
+ .option("--scenario-b <text-or-@file>", "Side-B scenario + goal — chat participant_pair mode")
132
+ .option("--initiator-side <a|b>", "Which side speaks first (default: a) — chat participant_pair mode")
133
+ .option("--role-criteria-a <json-or-@file>", 'RoleCriteria filter for side A (JSON object or @filepath). Keys: occupation[], min_age, max_age, gender[], country[], education_level_in[], household_in[], locale_type_in[], income_level_in[], employment_status_in[], requires_captions, uses_screen_reader, prefers_reduced_motion, prefers_high_contrast, has_any_accessibility_need. The five *_in arrays accept snake_case spec values; the five accessibility filters are booleans. Use INSTEAD of --group-a or alongside it. chat participant_pair mode.')
134
+ .option("--role-criteria-b <json-or-@file>", "RoleCriteria filter for side B — same shape as --role-criteria-a. chat participant_pair mode.")
132
135
  .addHelpText("after", `
133
136
  Note: --workspace is optional if set via \`ish workspace use <alias>\`.
134
137
 
135
- The questionnaire is the set of questions testers answer. Use \`--question\` to
138
+ The questionnaire is the set of questions participants answer. Use \`--question\` to
136
139
  quickly add simple text questions, or \`--questionnaire <file.json>\` for richer
137
140
  types (slider, likert, single-choice, multiple-choice, number) and custom
138
141
  timing. The two forms are mutually exclusive — pick one.
@@ -187,6 +190,23 @@ Examples:
187
190
  $ ish study create --name "Newsletter" --modality text --content-type email \\
188
191
  --assignments-file ./assignments.json
189
192
 
193
+ # Interactive study whose assignment carries a step-by-step checklist
194
+ # (JSON only — the inline --assignment shorthand can't express steps).
195
+ # Steps are honored for interactive + external_chatbot chat modalities only;
196
+ # the backend rejects them for media (text/video/audio/image/document) and
197
+ # for chat participant_pair. After a run, study get reports per-step completion.
198
+ $ ish study create --name "Checkout" --modality interactive \\
199
+ --assignments-file ./assignments.json
200
+ # assignments.json:
201
+ # [
202
+ # { "name": "Buy", "instructions": "Add to cart and check out",
203
+ # "steps": [
204
+ # { "name": "Find a product", "description": "Browse to any item" },
205
+ # { "name": "Add to cart" },
206
+ # { "name": "Complete checkout" }
207
+ # ] }
208
+ # ]
209
+
190
210
  # Chat study targeting a saved chatbot endpoint:
191
211
  $ ish study create --name "Onboarding bot" --modality chat \\
192
212
  --endpoint ep-abc \\
@@ -213,7 +233,7 @@ Tips:
213
233
  \`--fields a,b,c\` to project the JSON output to listed fields.
214
234
 
215
235
  Next: configure a run with \`ish iteration create --study <id>\`,
216
- then dispatch with \`ish study run\` (audience size is set on run, not create).`)
236
+ then dispatch with \`ish study run\` (people are selected on run, not create).`)
217
237
  .action(async (opts, cmd) => {
218
238
  await withClient(cmd, async (client, globals) => {
219
239
  const assignments = resolveAssignments(opts);
@@ -246,23 +266,23 @@ Next: configure a run with \`ish iteration create --study <id>\`,
246
266
  // against the freshly-created study.
247
267
  const normalizedChatMode = normalizeChatMode(opts.chatMode);
248
268
  if (opts.chatMode !== undefined && normalizedChatMode === null) {
249
- throw new ValidationError(`Invalid --chat-mode "${opts.chatMode}". Expected "external_chatbot" or "tester_pair" (hyphenated variants accepted).`, ["external_chatbot", "tester_pair"]);
269
+ throw new ValidationError(`Invalid --chat-mode "${opts.chatMode}". Expected "external_chatbot" or "participant_pair" (hyphenated variants accepted).`, ["external_chatbot", "participant_pair"]);
250
270
  }
251
- const pairFlagsSet = (opts.audienceA && opts.audienceA.length > 0)
252
- || (opts.audienceB && opts.audienceB.length > 0)
271
+ const pairFlagsSet = (opts.groupA && opts.groupA.length > 0)
272
+ || (opts.groupB && opts.groupB.length > 0)
253
273
  || opts.scenarioA !== undefined
254
274
  || opts.scenarioB !== undefined
255
275
  || opts.initiatorSide !== undefined
256
276
  || opts.roleCriteriaA !== undefined
257
277
  || opts.roleCriteriaB !== undefined
258
- || normalizedChatMode === "tester_pair";
278
+ || normalizedChatMode === "participant_pair";
259
279
  const inlineMediaFlagsSet = [
260
280
  opts.contentText !== undefined ? "--content-text" : null,
261
281
  opts.url !== undefined ? "--url" : null,
262
282
  opts.contentUrl !== undefined ? "--content-url" : null,
263
283
  opts.imageUrls !== undefined ? "--image-urls" : null,
264
284
  (opts.endpoint !== undefined || opts.endpointConfig !== undefined) ? "--endpoint/--endpoint-config" : null,
265
- pairFlagsSet ? "--chat-mode tester_pair (with --audience-a/-b or --role-criteria-a/-b plus --scenario-a/-b)" : null,
285
+ pairFlagsSet ? "--chat-mode participant_pair (with --group-a/-b or --role-criteria-a/-b plus --scenario-a/-b)" : null,
266
286
  ].filter((f) => f !== null);
267
287
  if (inlineMediaFlagsSet.length > 1) {
268
288
  throw new ValidationError(`Pass only one inline-iteration flag: ${inlineMediaFlagsSet.join(", ")}.`, inlineMediaFlagsSet);
@@ -367,7 +387,7 @@ Next: configure a run with \`ish iteration create --study <id>\`,
367
387
  throw new ValidationError(`--endpoint / --endpoint-config require --modality chat (got "${opts.modality}").`, ["chat"]);
368
388
  }
369
389
  if (normalizedChatMode && normalizedChatMode !== "external_chatbot") {
370
- throw new ValidationError(`--endpoint / --endpoint-config are only valid with --chat-mode external_chatbot (got "${opts.chatMode}"). For tester_pair use --audience-a/-b and --scenario-a/-b.`, ["external_chatbot"]);
390
+ throw new ValidationError(`--endpoint / --endpoint-config are only valid with --chat-mode external_chatbot (got "${opts.chatMode}"). For participant_pair use --group-a/-b and --scenario-a/-b.`, ["external_chatbot"]);
371
391
  }
372
392
  let endpointConfig;
373
393
  if (opts.endpoint !== undefined) {
@@ -389,7 +409,7 @@ Next: configure a run with \`ish iteration create --study <id>\`,
389
409
  throw new Error("Invalid --endpoint-config JSON.");
390
410
  }
391
411
  }
392
- const maxTurns = opts.maxTurns ?? 12;
412
+ const maxTurns = opts.maxTurns ?? 14;
393
413
  inlineIteration = {
394
414
  name: "A",
395
415
  details: {
@@ -406,13 +426,13 @@ Next: configure a run with \`ish iteration create --study <id>\`,
406
426
  }
407
427
  else if (pairFlagsSet) {
408
428
  if (opts.modality && opts.modality !== "chat") {
409
- throw new ValidationError(`--chat-mode tester_pair (with --audience-a/-b or --role-criteria-a/-b plus --scenario-a/-b) requires --modality chat (got "${opts.modality}").`, ["chat"]);
429
+ throw new ValidationError(`--chat-mode participant_pair (with --group-a/-b or --role-criteria-a/-b plus --scenario-a/-b) requires --modality chat (got "${opts.modality}").`, ["chat"]);
410
430
  }
411
- if (normalizedChatMode && normalizedChatMode !== "tester_pair") {
412
- throw new ValidationError(`--audience-a/-b or --role-criteria-a/-b imply --chat-mode tester_pair (got "${opts.chatMode}").`, ["tester_pair"]);
431
+ if (normalizedChatMode && normalizedChatMode !== "participant_pair") {
432
+ throw new ValidationError(`--group-a/-b or --role-criteria-a/-b imply --chat-mode participant_pair (got "${opts.chatMode}").`, ["participant_pair"]);
413
433
  }
414
- const audA = (opts.audienceA ?? []).map(resolveId);
415
- const audB = (opts.audienceB ?? []).map(resolveId);
434
+ const audA = (opts.groupA ?? []).map(resolveId);
435
+ const audB = (opts.groupB ?? []).map(resolveId);
416
436
  // Parse + validate role criteria if supplied (JSON or @filepath).
417
437
  const parseCriteria = (raw, flag) => {
418
438
  if (raw === undefined)
@@ -445,21 +465,21 @@ Next: configure a run with \`ish iteration create --study <id>\`,
445
465
  const sideAHasInput = audA.length > 0 || !!critA;
446
466
  const sideBHasInput = audB.length > 0 || !!critB;
447
467
  if (!sideAHasInput || !sideBHasInput) {
448
- throw new Error("tester_pair chat iterations require, for each side, either an explicit audience (--audience-a / --audience-b) or a role-criteria filter (--role-criteria-a / --role-criteria-b).");
468
+ throw new Error("participant_pair chat iterations require, for each side, either explicit people (--group-a / --group-b) or a role-criteria filter (--role-criteria-a / --role-criteria-b).");
449
469
  }
450
470
  // 1×N broadcast: canonical "rehearse one side against N
451
471
  // variations" shape. See iteration.ts buildIterationDetails
452
- // tester_pair arm for the rationale.
472
+ // participant_pair arm for the rationale.
453
473
  let audA_final = audA;
454
474
  let audB_final = audB;
455
475
  let broadcastMsg;
456
476
  if (audA.length === 1 && audB.length > 1 && !critA && !critB) {
457
477
  audA_final = Array(audB.length).fill(audA[0]);
458
- broadcastMsg = `Broadcasting --audience-a (1 profile) to length ${audB.length} to match --audience-b — same side-A profile across all ${audB.length} conversations.`;
478
+ broadcastMsg = `Broadcasting --group-a (1 profile) to length ${audB.length} to match --group-b — same side-A profile across all ${audB.length} conversations.`;
459
479
  }
460
480
  else if (audB.length === 1 && audA.length > 1 && !critA && !critB) {
461
481
  audB_final = Array(audA.length).fill(audB[0]);
462
- broadcastMsg = `Broadcasting --audience-b (1 profile) to length ${audA.length} to match --audience-a — same side-B profile across all ${audA.length} conversations.`;
482
+ broadcastMsg = `Broadcasting --group-b (1 profile) to length ${audA.length} to match --group-a — same side-B profile across all ${audA.length} conversations.`;
463
483
  }
464
484
  if (broadcastMsg) {
465
485
  console.error(broadcastMsg);
@@ -469,13 +489,13 @@ Next: configure a run with \`ish iteration create --study <id>\`,
469
489
  // CLI's 1×N broadcast (above) already cloned the singleton side,
470
490
  // so this branch only fires when both sides ship >1 with
471
491
  // mismatched counts. Server rejects the same way.
472
- throw new ValidationError(`--audience-a (${audA_final.length}) and --audience-b (${audB_final.length}) cannot be paired. ` +
492
+ throw new ValidationError(`--group-a (${audA_final.length}) and --group-b (${audB_final.length}) cannot be paired. ` +
473
493
  `Pick the same number on each side (1:1 by index), or pass exactly one profile on one side to broadcast ` +
474
- `(e.g. --audience-a tp-rep --audience-b tp-cto1,tp-cto2,tp-cto3), ` +
475
- `or use --role-criteria-a/-b on either side to let the backend resolve the pool.`, ["--audience-a", "--audience-b"]);
494
+ `(e.g. --group-a p-rep --group-b p-cto1,p-cto2,p-cto3), ` +
495
+ `or use --role-criteria-a/-b on either side to let the backend resolve the pool.`, ["--group-a", "--group-b"]);
476
496
  }
477
497
  if (!opts.scenarioA || !opts.scenarioB) {
478
- throw new Error("tester_pair chat iterations require --scenario-a <text-or-@file> and --scenario-b <text-or-@file>.");
498
+ throw new Error("participant_pair chat iterations require --scenario-a <text-or-@file> and --scenario-b <text-or-@file>.");
479
499
  }
480
500
  const scenarioA = opts.scenarioA.startsWith("@")
481
501
  ? readFileSync(opts.scenarioA.slice(1), "utf8")
@@ -490,15 +510,15 @@ Next: configure a run with \`ish iteration create --study <id>\`,
490
510
  if (initiator !== "a" && initiator !== "b") {
491
511
  throw new ValidationError(`Invalid --initiator-side "${opts.initiatorSide}". Expected "a" or "b".`, ["a", "b"]);
492
512
  }
493
- const maxTurns = opts.maxTurns ?? 12;
513
+ const maxTurns = opts.maxTurns ?? 14;
494
514
  inlineIteration = {
495
515
  name: "A",
496
516
  details: {
497
517
  type: "chat",
498
518
  mode_details: {
499
- mode: "tester_pair",
500
- audience_a: audA_final,
501
- audience_b: audB_final,
519
+ mode: "participant_pair",
520
+ group_a: audA_final,
521
+ group_b: audB_final,
502
522
  scenario_a: scenarioA,
503
523
  scenario_b: scenarioB,
504
524
  initiator_side: initiator,
@@ -597,14 +617,17 @@ list table layout in human mode.`)
597
617
  throw new Error("Provide at least one study id.");
598
618
  if (flat.length === 1) {
599
619
  const rid = resolveId(flat[0]);
600
- const data = await client.get(`/studies/${rid}`);
620
+ const [data, participants] = await Promise.all([
621
+ client.get(`/studies/${rid}`),
622
+ fetchStudyParticipants(client, rid),
623
+ ]);
601
624
  const result = data;
602
625
  if (result.id)
603
626
  result.alias = tagAlias(ALIAS_PREFIX.study, String(result.id));
604
627
  if (data.product_id) {
605
628
  result.url = getWebUrl(globals, `/${data.product_id}/${rid}/overview`);
606
629
  }
607
- formatStudyDetail(result, globals.json);
630
+ formatStudyDetail(result, globals.json, {}, participants);
608
631
  if (!globals.json && data.product_id) {
609
632
  const url = getWebUrl(globals, `/${data.product_id}/${rid}/overview`);
610
633
  console.error(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
@@ -613,13 +636,17 @@ list table layout in human mode.`)
613
636
  }
614
637
  const results = await Promise.all(flat.map(async (raw) => {
615
638
  const rid = resolveId(raw);
616
- const data = await client.get(`/studies/${rid}`);
639
+ const [data, participants] = await Promise.all([
640
+ client.get(`/studies/${rid}`),
641
+ fetchStudyParticipants(client, rid),
642
+ ]);
617
643
  const r = data;
618
644
  if (r.id)
619
645
  r.alias = tagAlias(ALIAS_PREFIX.study, String(r.id));
620
646
  if (data.product_id) {
621
647
  r.url = getWebUrl(globals, `/${data.product_id}/${rid}/overview`);
622
648
  }
649
+ r.participants = participants;
623
650
  return r;
624
651
  }));
625
652
  if (globals.json) {
@@ -632,84 +659,84 @@ list table layout in human mode.`)
632
659
  });
633
660
  study
634
661
  .command("results")
635
- .description("View aggregated results: tester counts, sentiment, interview answers. Returns a stable envelope with empty fields when no runs have completed.")
662
+ .description("View aggregated results: participant counts, sentiment, interview answers. Returns a stable envelope with empty fields when no runs have completed.")
636
663
  .argument("<id>", "Study ID")
637
664
  .option("--workspace <id>", "Workspace ID; accepted for consistency (workspace is inferred from the study)")
638
- .option("--summary", "Lean summary projection: counts + sentiment + per-tester {alias, status, sentiment, comment}. Drops interview_answers + per-interaction breakdowns.")
665
+ .option("--summary", "Lean summary projection: counts + sentiment + per-participant {alias, status, sentiment, comment}. Drops interview_answers + per-interaction breakdowns.")
639
666
  // PC-N4: agents reach for `--summarize` (verb) by analogy with the MCP
640
667
  // `summarize` action; accept it as a hidden alias of --summary so the
641
668
  // canonical flag stays the documented one but the muscle-memory variant
642
669
  // works without a round-trip.
643
670
  .addOption(new Option("--summarize", "Hidden alias for --summary").hideHelp())
644
- .option("--transcript <tester_id>", "Chat transcript projection for one tester: flat role/text/turn-index array (chat-modality only). Mirrors the MCP `get_chat_transcript` shape.")
671
+ .option("--transcript <participant_id>", "Chat transcript projection for one participant: flat role/text/turn-index array (chat-modality only). Mirrors the MCP `get_chat_transcript` shape.")
645
672
  .addHelpText("after", `
646
673
  Examples:
647
674
  $ ish study results <id>
648
675
  $ ish study results <id> --json
649
676
  $ ish study results <id> --summary --json
650
- $ ish study results <id> --transcript t-d4e --json
677
+ $ ish study results <id> --transcript pt-d4e --json
651
678
 
652
679
  Default --json envelope (M10: per-answer sentiment now included):
653
680
  {
654
681
  "study": { "alias": "s-...", "name": "...", "modality": "..." },
655
- "tester_count": 12,
682
+ "participant_count": 12,
656
683
  "completed_count": 8,
657
684
  "failed_count": 0,
658
685
  "sentiment": { "counts": { "Satisfied": 5, "Frustrated": 2 }, "total": 7 },
659
686
  "interview_answers": [
660
687
  { "question": "...", "type": "text",
661
688
  "answers": [
662
- { "tester_alias": "t-...", "tester_name": "...", "iteration": "A",
689
+ { "participant_alias": "pt-...", "participant_name": "...", "iteration": "A",
663
690
  "answer": "...", "sentiment": "Satisfied" }
664
691
  ] }
665
692
  ],
666
- "testers": [
667
- { "alias": "t-...", "name": "...", "iteration": "A", "status": "completed",
693
+ "participants": [
694
+ { "alias": "pt-...", "name": "...", "iteration": "A", "status": "completed",
668
695
  "interaction_count": 12, "sentiment": "Satisfied", "comment": "...",
669
696
  "error_message": "..." }
670
697
  ]
671
698
  }
672
699
 
673
700
  --summary projection (M2-friction-7: drops the interview_answers payload):
674
- { study, tester_count, completed_count, failed_count, sentiment, testers: [...] }
701
+ { study, participant_count, completed_count, failed_count, sentiment, participants: [...] }
675
702
 
676
- --transcript <tester_id> projection (M2-friction-12, chat modality):
703
+ --transcript <participant_id> projection (M2-friction-12, chat modality):
677
704
  {
678
- "tester_id": "...", "tester_alias": "t-...",
705
+ "participant_id": "...", "participant_alias": "pt-...",
679
706
  "instance_name": "...", "modality": "chat",
680
707
  "transcript": [
681
708
  { "role": "bot", "text": "Hi…", "turn_index": 0, "failure": null },
682
- { "role": "tester", "text": "Pricing?", "turn_index": 0,
709
+ { "role": "participant", "text": "Pricing?", "turn_index": 0,
683
710
  "action_type": "send_text", "option_label": null, "sentiment": null }
684
711
  ],
685
712
  "unique_bot_replies": 2,
686
- "tester_summary": { "comment": "...", "sentiment": {...} }
713
+ "participant_summary": { "comment": "...", "sentiment": {...} }
687
714
  }
688
715
 
689
716
  Tips:
690
- Use \`--get <path>\` for a single value (e.g. \`--get tester_count\`),
717
+ Use \`--get <path>\` for a single value (e.g. \`--get participant_count\`),
691
718
  \`--fields a,b,c\` to project the JSON output further.
692
719
 
693
720
  Common --get paths (default envelope):
694
- --get tester_count # how many testers ran
721
+ --get participant_count # how many participants ran
695
722
  --get completed_count # how many finished
696
723
  --get failed_count # how many errored
697
724
  --get sentiment # {counts, total} histogram
698
725
  --get sentiment.counts # bare label→count map
699
726
  --get sentiment.total # total sentiment-tagged answers
700
727
  --get study.modality # interactive | text | image | …
701
- --get testers.alias # one alias per line
702
- --get testers.0.comment # first tester's narrative comment
703
- --get testers.0.sentiment # first tester's aggregate sentiment
728
+ --get participants.alias # one alias per line
729
+ --get participants.0.comment # first participant's narrative comment
730
+ --get participants.0.sentiment # first participant's aggregate sentiment
704
731
  --get interview_answers # full per-question payload
705
732
  --get interview_answers.0.question # text of the first question
706
733
  --get interview_answers.0.answers.0.answer # first answer to the first question
707
734
 
708
- Common --get paths (--transcript <tester_id> envelope):
735
+ Common --get paths (--transcript <participant_id> envelope):
709
736
  --get transcript # full role/text/turn array
710
737
  --get transcript.text # one text per turn
711
- --get tester_summary.comment # narrative comment
712
- --get tester_summary.sentiment # aggregate sentiment map
738
+ --get participant_summary.comment # narrative comment
739
+ --get participant_summary.sentiment # aggregate sentiment map
713
740
  --get unique_bot_replies # bot-side message count
714
741
 
715
742
  When no runs have completed, the default envelope is returned with zero counts and empty arrays.`)
@@ -724,20 +751,23 @@ When no runs have completed, the default envelope is returned with zero counts a
724
751
  }
725
752
  const rid = resolveId(id);
726
753
  if (opts.transcript) {
727
- // --transcript <tester_id>: bypass the study aggregator; fetch
728
- // the named tester directly. Cheaper (one GET, no nested
754
+ // --transcript <participant_id>: bypass the study aggregator; fetch
755
+ // the named participant directly. Cheaper (one GET, no nested
729
756
  // iterations payload) and shapes 1:1 with the MCP transcript.
730
- const testerId = resolveId(opts.transcript);
731
- const tester = await client.get(`/testers/${testerId}`);
732
- output(buildChatTranscript(tester), globals.json, { preProjected: true });
757
+ const participantId = resolveId(opts.transcript);
758
+ const participant = await client.get(`/participants/${participantId}`);
759
+ output(buildChatTranscript(participant), globals.json, { preProjected: true });
733
760
  return;
734
761
  }
735
- const data = await client.get(`/studies/${rid}`);
762
+ const [data, participants] = await Promise.all([
763
+ client.get(`/studies/${rid}`),
764
+ fetchStudyParticipants(client, rid),
765
+ ]);
736
766
  if (wantsSummary) {
737
- output(buildStudyResultsSummary(data), globals.json, { preProjected: true });
767
+ output(buildStudyResultsSummary(data, participants), globals.json, { preProjected: true });
738
768
  }
739
769
  else {
740
- formatStudyResults(data, globals.json);
770
+ formatStudyResults(data, participants, globals.json);
741
771
  }
742
772
  if (!globals.json && data.product_id) {
743
773
  const url = getWebUrl(globals, `/${data.product_id}/${rid}/overview`);
@@ -770,7 +800,11 @@ Examples:
770
800
  $ ish study update <id> --assignment "Sign up:Complete the signup flow" \\
771
801
  --question "How easy was it?"
772
802
  $ ish study update <id> --questionnaire ./questionnaire.json
773
- $ ish study update <id> --assignments-file ./assignments.json`)
803
+ $ ish study update <id> --assignments-file ./assignments.json
804
+
805
+ Replacing assignments replaces the full list (no additive edit). Assignment
806
+ checklists ("steps") ride along when present in the JSON forms
807
+ (--assignments-file / --assignments) — see docs get-page concepts/assignment.`)
774
808
  .action(async (id, opts, cmd) => {
775
809
  await withClient(cmd, async (client, globals) => {
776
810
  const assignments = resolveAssignments(opts);
@@ -852,7 +886,7 @@ Examples:
852
886
  });
853
887
  });
854
888
  attachStudyRunCommands(study);
855
- attachStudyTesterCommands(study);
889
+ attachStudyParticipantCommands(study);
856
890
  attachStudyAnalyzeCommands(study);
857
891
  attachStudyScreenshotsCommands(study);
858
892
  }
@@ -152,7 +152,7 @@ existing workspace was returned. On creation, \`reused: false\`.`)
152
152
  .addHelpText("after", `
153
153
  Usage counters:
154
154
  studies_used / studies_max — current study count vs the user's plan cap
155
- testers_used / testers_max — workspace-private tester profile count vs cap
155
+ people_used / people_max — workspace-private participant profile count vs cap
156
156
 
157
157
  Caps fall back to null when the user's plan grants unlimited (math.inf). The
158
158
  account tier is read from /account/me; limit tables come from /billing/limits.
@@ -174,7 +174,7 @@ Examples:
174
174
  if (usage.tier)
175
175
  console.log(`Plan: ${usage.tier}`);
176
176
  console.log(`Studies: ${usage.studies_used} / ${renderCap(usage.studies_max)}`);
177
- console.log(`Custom testers: ${usage.testers_used} / ${renderCap(usage.testers_max)}`);
177
+ console.log(`Custom people: ${usage.people_used} / ${renderCap(usage.people_max)}`);
178
178
  });
179
179
  });
180
180
  workspace
@@ -213,11 +213,11 @@ async function collectWorkspaceUsage(client, workspaceId) {
213
213
  const studiesPromise = client
214
214
  .get(`/products/${workspaceId}/studies`)
215
215
  .catch(() => []);
216
- // testers_used: paginated list returns { total, items, ... }. Backend
217
- // gates `maxCustomTesterProfiles` on visibility=workspace (rows owned by
216
+ // people_used: paginated list returns { total, items, ... }. Backend
217
+ // gates `maxCustomPersons` on visibility=workspace (rows owned by
218
218
  // this workspace; was `private` before the visibility rename).
219
- const testersPromise = client
220
- .get("/tester-profiles", {
219
+ const peoplePromise = client
220
+ .get("/people", {
221
221
  product_id: workspaceId,
222
222
  visibility: "workspace",
223
223
  type: "ai",
@@ -232,18 +232,18 @@ async function collectWorkspaceUsage(client, workspaceId) {
232
232
  const limitsPromise = client
233
233
  .get("/billing/limits")
234
234
  .catch(() => ({ tiers: {} }));
235
- const [product, studies, testers, account, limits] = await Promise.all([
235
+ const [product, studies, participants, account, limits] = await Promise.all([
236
236
  productPromise,
237
237
  studiesPromise,
238
- testersPromise,
238
+ peoplePromise,
239
239
  acctPromise,
240
240
  limitsPromise,
241
241
  ]);
242
242
  const tier = typeof account.credits?.tier === "string" ? account.credits.tier : null;
243
243
  const tierTable = tier ? limits.tiers?.[tier] ?? null : null;
244
244
  const studiesMax = tierTable && "maxStudiesPerProduct" in tierTable ? tierTable.maxStudiesPerProduct : null;
245
- const testersMax = tierTable && "maxCustomTesterProfiles" in tierTable
246
- ? tierTable.maxCustomTesterProfiles
245
+ const peopleMax = tierTable && "maxCustomPersons" in tierTable
246
+ ? tierTable.maxCustomPersons
247
247
  : null;
248
248
  return {
249
249
  id: workspaceId,
@@ -252,8 +252,8 @@ async function collectWorkspaceUsage(client, workspaceId) {
252
252
  tier,
253
253
  studies_used: Array.isArray(studies) ? studies.length : 0,
254
254
  studies_max: studiesMax,
255
- testers_used: typeof testers.total === "number" ? testers.total : 0,
256
- testers_max: testersMax,
255
+ people_used: typeof participants.total === "number" ? participants.total : 0,
256
+ people_max: peopleMax,
257
257
  };
258
258
  }
259
259
  // ---------------------------------------------------------------------------
@@ -364,7 +364,7 @@ function registerSiteAccessCommands(workspace) {
364
364
  });
365
365
  sa
366
366
  .command("login")
367
- .description("Set login form credentials a tester will type into the site")
367
+ .description("Set login form credentials a participant will type into the site")
368
368
  .requiredOption("--username <u>", 'Username (or "-" to read from stdin)')
369
369
  .requiredOption("--password <p>", 'Password (or "-" to read from stdin)')
370
370
  .option("--workspace <id>", "Workspace ID; defaults to active workspace")
package/dist/connect.js CHANGED
@@ -46,7 +46,7 @@ function row(content, inner) {
46
46
  }
47
47
  function renderCard(sim) {
48
48
  const inner = CARD_WIDTH - 4;
49
- const name = truncate(sim.tester_name ?? sim.instance_name ?? sim.tester_id.slice(0, 8), inner - 2);
49
+ const name = truncate(sim.participant_name ?? sim.instance_name ?? sim.participant_id.slice(0, 8), inner - 2);
50
50
  // Top border with name
51
51
  const nameSegment = `─ ${name} `;
52
52
  const topPad = CARD_WIDTH - 2 - nameSegment.length;
@@ -160,16 +160,16 @@ function renderSimulationCards(simulations) {
160
160
  }
161
161
  }
162
162
  // --- Local storage for completed simulations ---
163
- const storedTesterIds = new Set();
163
+ const storedParticipantIds = new Set();
164
164
  function storeCompletedSimulation(sim) {
165
- if (storedTesterIds.has(sim.tester_id))
165
+ if (storedParticipantIds.has(sim.participant_id))
166
166
  return;
167
- storedTesterIds.add(sim.tester_id);
167
+ storedParticipantIds.add(sim.participant_id);
168
168
  const dir = simulationsDir();
169
169
  mkdirSync(dir, { recursive: true });
170
170
  const logFile = join(dir, "history.jsonl");
171
171
  const entry = {
172
- tester_id: sim.tester_id,
172
+ participant_id: sim.participant_id,
173
173
  instance_name: sim.instance_name,
174
174
  status: sim.status,
175
175
  study_name: sim.study_name,
package/dist/index.js CHANGED
@@ -10,13 +10,14 @@ import { upgrade } from "./upgrade.js";
10
10
  import { registerWorkspaceCommands } from "./commands/workspace.js";
11
11
  import { registerStudyCommands } from "./commands/study.js";
12
12
  import { registerIterationCommands } from "./commands/iteration.js";
13
- import { registerProfileCommands } from "./commands/profile.js";
13
+ import { registerPersonCommands } from "./commands/person.js";
14
14
  import { registerSourceCommands } from "./commands/source.js";
15
15
  import { registerConfigCommands } from "./commands/config.js";
16
16
  import { registerAskCommands } from "./commands/ask.js";
17
17
  import { registerChatCommand } from "./commands/chat.js";
18
18
  import { registerDocsCommands } from "./commands/docs.js";
19
19
  import { registerInitCommands } from "./commands/init.js";
20
+ import { registerMcpCommands } from "./commands/mcp.js";
20
21
  import { registerSecretCommands } from "./commands/secret.js";
21
22
  import { AGENT_HELP_FOOTER } from "./lib/docs.js";
22
23
  import { runInline, EXIT_USAGE, injectGlobalWorkspaceOption } from "./lib/command-helpers.js";
@@ -43,7 +44,7 @@ catch {
43
44
  }
44
45
  program
45
46
  .name("ish")
46
- .description("ish CLI — run studies and asks against AI tester audiences")
47
+ .description("ish CLI — run studies and asks with AI people")
47
48
  .version(version)
48
49
  .addHelpText("after", AGENT_HELP_FOOTER);
49
50
  // Unified error envelope for Commander-level failures (unknown command,
@@ -91,7 +92,7 @@ program
91
92
  .addOption(new Option("--dev", "Use local dev API (http://localhost:8000)").hideHelp())
92
93
  .option("--workspace <id>", "Default workspace ID; per-subcommand --workspace overrides")
93
94
  .option("--json", "Output as JSON (auto-enabled when piped)")
94
- .option("--get <field>", "Extract a single field from the JSON response and print only its value (implies --json internally; supports dotted paths e.g. tester_profile.name)")
95
+ .option("--get <field>", "Extract a single field from the JSON response and print only its value (implies --json internally; supports dotted paths e.g. person.name)")
95
96
  .option("--human", "Force human-readable output even when stdout is piped (overrides JSON-when-piped auto-detection)")
96
97
  .option("--fields <fields>", "Comma-separated fields to include in JSON output (e.g. alias,name,status)")
97
98
  .option("--verbose", "Include full UUIDs and timestamps in JSON output")
@@ -305,13 +306,14 @@ program
305
306
  registerWorkspaceCommands(program);
306
307
  registerStudyCommands(program);
307
308
  registerIterationCommands(program);
308
- registerProfileCommands(program);
309
+ registerPersonCommands(program);
309
310
  registerSourceCommands(program);
310
311
  registerConfigCommands(program);
311
312
  registerAskCommands(program);
312
313
  registerChatCommand(program);
313
314
  registerDocsCommands(program);
314
315
  registerInitCommands(program);
316
+ registerMcpCommands(program);
315
317
  registerSecretCommands(program);
316
318
  program
317
319
  .command("upgrade")
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Client-side validator for the AccessibilityProfile v1.0 JSONB shape on
3
- * TesterProfile.accessibility_profile. Mirrors
3
+ * Person.accessibility_profile. Mirrors
4
4
  * `ish-mcp/spec/accessibility-profile-schema.v1.json` (additionalProperties:
5
5
  * false at every level except `extensions`). An empty object `{}` is the
6
6
  * canonical default. When non-empty, `version` is required and must be
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Client-side validator for the AccessibilityProfile v1.0 JSONB shape on
3
- * TesterProfile.accessibility_profile. Mirrors
3
+ * Person.accessibility_profile. Mirrors
4
4
  * `ish-mcp/spec/accessibility-profile-schema.v1.json` (additionalProperties:
5
5
  * false at every level except `extensions`). An empty object `{}` is the
6
6
  * canonical default. When non-empty, `version` is required and must be
@@ -116,10 +116,10 @@ export async function hydrateForAlias(client, alias, hints = {}) {
116
116
  await hydrateList(client, ALIAS_PREFIX.iteration, `/studies/${study}/iterations`);
117
117
  return;
118
118
  }
119
- case ALIAS_PREFIX.testerProfile: {
119
+ case ALIAS_PREFIX.person: {
120
120
  if (!ws)
121
121
  return;
122
- await hydrateList(client, ALIAS_PREFIX.testerProfile, "/tester-profiles", { workspace_id: ws, type: "all", limit: "200" });
122
+ await hydrateList(client, ALIAS_PREFIX.person, "/people", { workspace_id: ws, type: "all", limit: "200" });
123
123
  return;
124
124
  }
125
125
  case ALIAS_PREFIX.ask: {
@@ -135,9 +135,9 @@ export async function hydrateForAlias(client, alias, hints = {}) {
135
135
  return;
136
136
  }
137
137
  // No cheap single-call hydrate for the rest:
138
- // tester (`t-`) — scoped to iteration
138
+ // participant (`pt-`) — scoped to iteration
139
139
  // ask round (`r-`) — nested on the ask
140
- // audience source (`tps-`) — fetched per-id
140
+ // person source (`ps-`) — fetched per-id
141
141
  // simulation config (`c-`) — no list endpoint yet
142
142
  default:
143
143
  return;