@ishlabs/cli 0.17.6 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +54 -54
  2. package/dist/commands/ask.d.ts +4 -4
  3. package/dist/commands/ask.js +66 -66
  4. package/dist/commands/chat.js +10 -10
  5. package/dist/commands/config.js +1 -1
  6. package/dist/commands/docs.js +1 -1
  7. package/dist/commands/iteration.js +57 -57
  8. package/dist/commands/mcp.d.ts +23 -0
  9. package/dist/commands/mcp.js +676 -0
  10. package/dist/commands/person.d.ts +5 -0
  11. package/dist/commands/{profile.js → person.js} +197 -162
  12. package/dist/commands/source.d.ts +6 -2
  13. package/dist/commands/source.js +35 -30
  14. package/dist/commands/study-analyze.d.ts +1 -1
  15. package/dist/commands/study-analyze.js +3 -3
  16. package/dist/commands/study-participant.d.ts +8 -0
  17. package/dist/commands/{study-tester.js → study-participant.js} +50 -50
  18. package/dist/commands/study-run.d.ts +6 -6
  19. package/dist/commands/study-run.js +295 -271
  20. package/dist/commands/study.js +89 -66
  21. package/dist/commands/workspace.js +13 -13
  22. package/dist/connect.js +5 -5
  23. package/dist/index.js +6 -4
  24. package/dist/lib/accessibility-profile.d.ts +1 -1
  25. package/dist/lib/accessibility-profile.js +1 -1
  26. package/dist/lib/alias-hydrate.js +4 -4
  27. package/dist/lib/alias-store.d.ts +5 -5
  28. package/dist/lib/alias-store.js +8 -8
  29. package/dist/lib/api-client.d.ts +1 -1
  30. package/dist/lib/api-client.js +1 -1
  31. package/dist/lib/billing.d.ts +11 -11
  32. package/dist/lib/billing.js +16 -16
  33. package/dist/lib/chat-endpoint-templates.js +1 -1
  34. package/dist/lib/command-helpers.d.ts +18 -18
  35. package/dist/lib/command-helpers.js +83 -53
  36. package/dist/lib/docs.js +560 -386
  37. package/dist/lib/enums.d.ts +2 -2
  38. package/dist/lib/enums.js +2 -2
  39. package/dist/lib/local-sim/browser.d.ts +1 -1
  40. package/dist/lib/local-sim/browser.js +1 -1
  41. package/dist/lib/local-sim/debug-report.d.ts +2 -2
  42. package/dist/lib/local-sim/debug-report.js +3 -3
  43. package/dist/lib/local-sim/loop.d.ts +5 -5
  44. package/dist/lib/local-sim/loop.js +38 -38
  45. package/dist/lib/local-sim/types.d.ts +12 -12
  46. package/dist/lib/mcp-clients.d.ts +51 -0
  47. package/dist/lib/mcp-clients.js +175 -0
  48. package/dist/lib/modality.d.ts +10 -10
  49. package/dist/lib/modality.js +46 -46
  50. package/dist/lib/observability.d.ts +11 -0
  51. package/dist/lib/observability.js +16 -3
  52. package/dist/lib/output.d.ts +13 -12
  53. package/dist/lib/output.js +244 -184
  54. package/dist/lib/profile-sources.d.ts +64 -16
  55. package/dist/lib/profile-sources.js +91 -30
  56. package/dist/lib/skill-content.js +215 -168
  57. package/dist/lib/study-events.d.ts +3 -3
  58. package/dist/lib/study-events.js +1 -1
  59. package/dist/lib/study-inputs.d.ts +11 -1
  60. package/dist/lib/study-inputs.js +68 -17
  61. package/dist/lib/types.d.ts +105 -34
  62. package/package.json +1 -1
  63. package/dist/commands/profile.d.ts +0 -5
  64. package/dist/commands/study-tester.d.ts +0 -8
@@ -8,13 +8,13 @@ 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 { parseAssignment, loadAssignmentsFile, validateAssignmentsArray, parseQuestion } from "../lib/study-inputs.js";
12
12
  import { loadQuestionsManifest } from "../lib/ask-questions.js";
13
13
  import { isLocalPath } from "../lib/upload.js";
14
14
  import { normalizeChatMode, validateRoleCriteria } from "../lib/modality.js";
15
15
  import { normalizeEnumValue, SCREEN_FORMATS } from "../lib/enums.js";
16
16
  import { attachStudyRunCommands } from "./study-run.js";
17
- import { attachStudyTesterCommands } from "./study-tester.js";
17
+ import { attachStudyParticipantCommands } from "./study-participant.js";
18
18
  import { attachStudyAnalyzeCommands } from "./study-analyze.js";
19
19
  import { attachStudyScreenshotsCommands } from "./study-screenshots.js";
20
20
  function collectRepeatable(value, prev = []) {
@@ -51,12 +51,14 @@ function resolveAssignments(opts) {
51
51
  return loadAssignmentsFile(opts.assignmentsFile);
52
52
  }
53
53
  if (opts.assignments) {
54
+ let parsed;
54
55
  try {
55
- return JSON.parse(opts.assignments);
56
+ parsed = JSON.parse(opts.assignments);
56
57
  }
57
58
  catch {
58
59
  throw new Error("Invalid --assignments JSON");
59
60
  }
61
+ return validateAssignmentsArray(parsed, "--assignments");
60
62
  }
61
63
  return undefined;
62
64
  }
@@ -120,19 +122,19 @@ Concept pages: ish docs get-page concepts/study
120
122
  .option("--title <title>", "Content title (text + media modalities — image, video, audio, document; optional). Not used for interactive / chat.")
121
123
  .option("--endpoint <id>", "Saved chatbot endpoint id or alias. Creates iteration A inline (chat modality, external_chatbot mode)")
122
124
  .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.")
125
+ .option("--max-turns <n>", "Maximum conversation turns per participant (chat modality only; default 12)", (v) => Number(v))
126
+ .option("--chat-mode <mode>", "Chat mode: external_chatbot (default) or participant_pair (two AI groups talk to each other) — chat modality only")
127
+ .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)), [])
128
+ .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)), [])
129
+ .option("--scenario-a <text-or-@file>", "Side-A scenario + goal — chat participant_pair mode")
130
+ .option("--scenario-b <text-or-@file>", "Side-B scenario + goal — chat participant_pair mode")
131
+ .option("--initiator-side <a|b>", "Which side speaks first (default: a) — chat participant_pair mode")
132
+ .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.')
133
+ .option("--role-criteria-b <json-or-@file>", "RoleCriteria filter for side B — same shape as --role-criteria-a. chat participant_pair mode.")
132
134
  .addHelpText("after", `
133
135
  Note: --workspace is optional if set via \`ish workspace use <alias>\`.
134
136
 
135
- The questionnaire is the set of questions testers answer. Use \`--question\` to
137
+ The questionnaire is the set of questions participants answer. Use \`--question\` to
136
138
  quickly add simple text questions, or \`--questionnaire <file.json>\` for richer
137
139
  types (slider, likert, single-choice, multiple-choice, number) and custom
138
140
  timing. The two forms are mutually exclusive — pick one.
@@ -187,6 +189,23 @@ Examples:
187
189
  $ ish study create --name "Newsletter" --modality text --content-type email \\
188
190
  --assignments-file ./assignments.json
189
191
 
192
+ # Interactive study whose assignment carries a step-by-step checklist
193
+ # (JSON only — the inline --assignment shorthand can't express steps).
194
+ # Steps are honored for interactive + external_chatbot chat modalities only;
195
+ # the backend rejects them for media (text/video/audio/image/document) and
196
+ # for chat participant_pair. After a run, study get reports per-step completion.
197
+ $ ish study create --name "Checkout" --modality interactive \\
198
+ --assignments-file ./assignments.json
199
+ # assignments.json:
200
+ # [
201
+ # { "name": "Buy", "instructions": "Add to cart and check out",
202
+ # "steps": [
203
+ # { "name": "Find a product", "description": "Browse to any item" },
204
+ # { "name": "Add to cart" },
205
+ # { "name": "Complete checkout" }
206
+ # ] }
207
+ # ]
208
+
190
209
  # Chat study targeting a saved chatbot endpoint:
191
210
  $ ish study create --name "Onboarding bot" --modality chat \\
192
211
  --endpoint ep-abc \\
@@ -213,7 +232,7 @@ Tips:
213
232
  \`--fields a,b,c\` to project the JSON output to listed fields.
214
233
 
215
234
  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).`)
235
+ then dispatch with \`ish study run\` (people are selected on run, not create).`)
217
236
  .action(async (opts, cmd) => {
218
237
  await withClient(cmd, async (client, globals) => {
219
238
  const assignments = resolveAssignments(opts);
@@ -246,23 +265,23 @@ Next: configure a run with \`ish iteration create --study <id>\`,
246
265
  // against the freshly-created study.
247
266
  const normalizedChatMode = normalizeChatMode(opts.chatMode);
248
267
  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"]);
268
+ throw new ValidationError(`Invalid --chat-mode "${opts.chatMode}". Expected "external_chatbot" or "participant_pair" (hyphenated variants accepted).`, ["external_chatbot", "participant_pair"]);
250
269
  }
251
- const pairFlagsSet = (opts.audienceA && opts.audienceA.length > 0)
252
- || (opts.audienceB && opts.audienceB.length > 0)
270
+ const pairFlagsSet = (opts.groupA && opts.groupA.length > 0)
271
+ || (opts.groupB && opts.groupB.length > 0)
253
272
  || opts.scenarioA !== undefined
254
273
  || opts.scenarioB !== undefined
255
274
  || opts.initiatorSide !== undefined
256
275
  || opts.roleCriteriaA !== undefined
257
276
  || opts.roleCriteriaB !== undefined
258
- || normalizedChatMode === "tester_pair";
277
+ || normalizedChatMode === "participant_pair";
259
278
  const inlineMediaFlagsSet = [
260
279
  opts.contentText !== undefined ? "--content-text" : null,
261
280
  opts.url !== undefined ? "--url" : null,
262
281
  opts.contentUrl !== undefined ? "--content-url" : null,
263
282
  opts.imageUrls !== undefined ? "--image-urls" : null,
264
283
  (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,
284
+ pairFlagsSet ? "--chat-mode participant_pair (with --group-a/-b or --role-criteria-a/-b plus --scenario-a/-b)" : null,
266
285
  ].filter((f) => f !== null);
267
286
  if (inlineMediaFlagsSet.length > 1) {
268
287
  throw new ValidationError(`Pass only one inline-iteration flag: ${inlineMediaFlagsSet.join(", ")}.`, inlineMediaFlagsSet);
@@ -367,7 +386,7 @@ Next: configure a run with \`ish iteration create --study <id>\`,
367
386
  throw new ValidationError(`--endpoint / --endpoint-config require --modality chat (got "${opts.modality}").`, ["chat"]);
368
387
  }
369
388
  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"]);
389
+ 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
390
  }
372
391
  let endpointConfig;
373
392
  if (opts.endpoint !== undefined) {
@@ -389,7 +408,7 @@ Next: configure a run with \`ish iteration create --study <id>\`,
389
408
  throw new Error("Invalid --endpoint-config JSON.");
390
409
  }
391
410
  }
392
- const maxTurns = opts.maxTurns ?? 12;
411
+ const maxTurns = opts.maxTurns ?? 14;
393
412
  inlineIteration = {
394
413
  name: "A",
395
414
  details: {
@@ -406,13 +425,13 @@ Next: configure a run with \`ish iteration create --study <id>\`,
406
425
  }
407
426
  else if (pairFlagsSet) {
408
427
  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"]);
428
+ 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
429
  }
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"]);
430
+ if (normalizedChatMode && normalizedChatMode !== "participant_pair") {
431
+ throw new ValidationError(`--group-a/-b or --role-criteria-a/-b imply --chat-mode participant_pair (got "${opts.chatMode}").`, ["participant_pair"]);
413
432
  }
414
- const audA = (opts.audienceA ?? []).map(resolveId);
415
- const audB = (opts.audienceB ?? []).map(resolveId);
433
+ const audA = (opts.groupA ?? []).map(resolveId);
434
+ const audB = (opts.groupB ?? []).map(resolveId);
416
435
  // Parse + validate role criteria if supplied (JSON or @filepath).
417
436
  const parseCriteria = (raw, flag) => {
418
437
  if (raw === undefined)
@@ -445,21 +464,21 @@ Next: configure a run with \`ish iteration create --study <id>\`,
445
464
  const sideAHasInput = audA.length > 0 || !!critA;
446
465
  const sideBHasInput = audB.length > 0 || !!critB;
447
466
  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).");
467
+ 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
468
  }
450
469
  // 1×N broadcast: canonical "rehearse one side against N
451
470
  // variations" shape. See iteration.ts buildIterationDetails
452
- // tester_pair arm for the rationale.
471
+ // participant_pair arm for the rationale.
453
472
  let audA_final = audA;
454
473
  let audB_final = audB;
455
474
  let broadcastMsg;
456
475
  if (audA.length === 1 && audB.length > 1 && !critA && !critB) {
457
476
  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.`;
477
+ broadcastMsg = `Broadcasting --group-a (1 profile) to length ${audB.length} to match --group-b — same side-A profile across all ${audB.length} conversations.`;
459
478
  }
460
479
  else if (audB.length === 1 && audA.length > 1 && !critA && !critB) {
461
480
  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.`;
481
+ broadcastMsg = `Broadcasting --group-b (1 profile) to length ${audA.length} to match --group-a — same side-B profile across all ${audA.length} conversations.`;
463
482
  }
464
483
  if (broadcastMsg) {
465
484
  console.error(broadcastMsg);
@@ -469,13 +488,13 @@ Next: configure a run with \`ish iteration create --study <id>\`,
469
488
  // CLI's 1×N broadcast (above) already cloned the singleton side,
470
489
  // so this branch only fires when both sides ship >1 with
471
490
  // 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. ` +
491
+ throw new ValidationError(`--group-a (${audA_final.length}) and --group-b (${audB_final.length}) cannot be paired. ` +
473
492
  `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"]);
493
+ `(e.g. --group-a p-rep --group-b p-cto1,p-cto2,p-cto3), ` +
494
+ `or use --role-criteria-a/-b on either side to let the backend resolve the pool.`, ["--group-a", "--group-b"]);
476
495
  }
477
496
  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>.");
497
+ throw new Error("participant_pair chat iterations require --scenario-a <text-or-@file> and --scenario-b <text-or-@file>.");
479
498
  }
480
499
  const scenarioA = opts.scenarioA.startsWith("@")
481
500
  ? readFileSync(opts.scenarioA.slice(1), "utf8")
@@ -490,15 +509,15 @@ Next: configure a run with \`ish iteration create --study <id>\`,
490
509
  if (initiator !== "a" && initiator !== "b") {
491
510
  throw new ValidationError(`Invalid --initiator-side "${opts.initiatorSide}". Expected "a" or "b".`, ["a", "b"]);
492
511
  }
493
- const maxTurns = opts.maxTurns ?? 12;
512
+ const maxTurns = opts.maxTurns ?? 14;
494
513
  inlineIteration = {
495
514
  name: "A",
496
515
  details: {
497
516
  type: "chat",
498
517
  mode_details: {
499
- mode: "tester_pair",
500
- audience_a: audA_final,
501
- audience_b: audB_final,
518
+ mode: "participant_pair",
519
+ group_a: audA_final,
520
+ group_b: audB_final,
502
521
  scenario_a: scenarioA,
503
522
  scenario_b: scenarioB,
504
523
  initiator_side: initiator,
@@ -632,84 +651,84 @@ list table layout in human mode.`)
632
651
  });
633
652
  study
634
653
  .command("results")
635
- .description("View aggregated results: tester counts, sentiment, interview answers. Returns a stable envelope with empty fields when no runs have completed.")
654
+ .description("View aggregated results: participant counts, sentiment, interview answers. Returns a stable envelope with empty fields when no runs have completed.")
636
655
  .argument("<id>", "Study ID")
637
656
  .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.")
657
+ .option("--summary", "Lean summary projection: counts + sentiment + per-participant {alias, status, sentiment, comment}. Drops interview_answers + per-interaction breakdowns.")
639
658
  // PC-N4: agents reach for `--summarize` (verb) by analogy with the MCP
640
659
  // `summarize` action; accept it as a hidden alias of --summary so the
641
660
  // canonical flag stays the documented one but the muscle-memory variant
642
661
  // works without a round-trip.
643
662
  .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.")
663
+ .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
664
  .addHelpText("after", `
646
665
  Examples:
647
666
  $ ish study results <id>
648
667
  $ ish study results <id> --json
649
668
  $ ish study results <id> --summary --json
650
- $ ish study results <id> --transcript t-d4e --json
669
+ $ ish study results <id> --transcript pt-d4e --json
651
670
 
652
671
  Default --json envelope (M10: per-answer sentiment now included):
653
672
  {
654
673
  "study": { "alias": "s-...", "name": "...", "modality": "..." },
655
- "tester_count": 12,
674
+ "participant_count": 12,
656
675
  "completed_count": 8,
657
676
  "failed_count": 0,
658
677
  "sentiment": { "counts": { "Satisfied": 5, "Frustrated": 2 }, "total": 7 },
659
678
  "interview_answers": [
660
679
  { "question": "...", "type": "text",
661
680
  "answers": [
662
- { "tester_alias": "t-...", "tester_name": "...", "iteration": "A",
681
+ { "participant_alias": "pt-...", "participant_name": "...", "iteration": "A",
663
682
  "answer": "...", "sentiment": "Satisfied" }
664
683
  ] }
665
684
  ],
666
- "testers": [
667
- { "alias": "t-...", "name": "...", "iteration": "A", "status": "completed",
685
+ "participants": [
686
+ { "alias": "pt-...", "name": "...", "iteration": "A", "status": "completed",
668
687
  "interaction_count": 12, "sentiment": "Satisfied", "comment": "...",
669
688
  "error_message": "..." }
670
689
  ]
671
690
  }
672
691
 
673
692
  --summary projection (M2-friction-7: drops the interview_answers payload):
674
- { study, tester_count, completed_count, failed_count, sentiment, testers: [...] }
693
+ { study, participant_count, completed_count, failed_count, sentiment, participants: [...] }
675
694
 
676
- --transcript <tester_id> projection (M2-friction-12, chat modality):
695
+ --transcript <participant_id> projection (M2-friction-12, chat modality):
677
696
  {
678
- "tester_id": "...", "tester_alias": "t-...",
697
+ "participant_id": "...", "participant_alias": "pt-...",
679
698
  "instance_name": "...", "modality": "chat",
680
699
  "transcript": [
681
700
  { "role": "bot", "text": "Hi…", "turn_index": 0, "failure": null },
682
- { "role": "tester", "text": "Pricing?", "turn_index": 0,
701
+ { "role": "participant", "text": "Pricing?", "turn_index": 0,
683
702
  "action_type": "send_text", "option_label": null, "sentiment": null }
684
703
  ],
685
704
  "unique_bot_replies": 2,
686
- "tester_summary": { "comment": "...", "sentiment": {...} }
705
+ "participant_summary": { "comment": "...", "sentiment": {...} }
687
706
  }
688
707
 
689
708
  Tips:
690
- Use \`--get <path>\` for a single value (e.g. \`--get tester_count\`),
709
+ Use \`--get <path>\` for a single value (e.g. \`--get participant_count\`),
691
710
  \`--fields a,b,c\` to project the JSON output further.
692
711
 
693
712
  Common --get paths (default envelope):
694
- --get tester_count # how many testers ran
713
+ --get participant_count # how many participants ran
695
714
  --get completed_count # how many finished
696
715
  --get failed_count # how many errored
697
716
  --get sentiment # {counts, total} histogram
698
717
  --get sentiment.counts # bare label→count map
699
718
  --get sentiment.total # total sentiment-tagged answers
700
719
  --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
720
+ --get participants.alias # one alias per line
721
+ --get participants.0.comment # first participant's narrative comment
722
+ --get participants.0.sentiment # first participant's aggregate sentiment
704
723
  --get interview_answers # full per-question payload
705
724
  --get interview_answers.0.question # text of the first question
706
725
  --get interview_answers.0.answers.0.answer # first answer to the first question
707
726
 
708
- Common --get paths (--transcript <tester_id> envelope):
727
+ Common --get paths (--transcript <participant_id> envelope):
709
728
  --get transcript # full role/text/turn array
710
729
  --get transcript.text # one text per turn
711
- --get tester_summary.comment # narrative comment
712
- --get tester_summary.sentiment # aggregate sentiment map
730
+ --get participant_summary.comment # narrative comment
731
+ --get participant_summary.sentiment # aggregate sentiment map
713
732
  --get unique_bot_replies # bot-side message count
714
733
 
715
734
  When no runs have completed, the default envelope is returned with zero counts and empty arrays.`)
@@ -724,12 +743,12 @@ When no runs have completed, the default envelope is returned with zero counts a
724
743
  }
725
744
  const rid = resolveId(id);
726
745
  if (opts.transcript) {
727
- // --transcript <tester_id>: bypass the study aggregator; fetch
728
- // the named tester directly. Cheaper (one GET, no nested
746
+ // --transcript <participant_id>: bypass the study aggregator; fetch
747
+ // the named participant directly. Cheaper (one GET, no nested
729
748
  // 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 });
749
+ const participantId = resolveId(opts.transcript);
750
+ const participant = await client.get(`/participants/${participantId}`);
751
+ output(buildChatTranscript(participant), globals.json, { preProjected: true });
733
752
  return;
734
753
  }
735
754
  const data = await client.get(`/studies/${rid}`);
@@ -770,7 +789,11 @@ Examples:
770
789
  $ ish study update <id> --assignment "Sign up:Complete the signup flow" \\
771
790
  --question "How easy was it?"
772
791
  $ ish study update <id> --questionnaire ./questionnaire.json
773
- $ ish study update <id> --assignments-file ./assignments.json`)
792
+ $ ish study update <id> --assignments-file ./assignments.json
793
+
794
+ Replacing assignments replaces the full list (no additive edit). Assignment
795
+ checklists ("steps") ride along when present in the JSON forms
796
+ (--assignments-file / --assignments) — see docs get-page concepts/assignment.`)
774
797
  .action(async (id, opts, cmd) => {
775
798
  await withClient(cmd, async (client, globals) => {
776
799
  const assignments = resolveAssignments(opts);
@@ -852,7 +875,7 @@ Examples:
852
875
  });
853
876
  });
854
877
  attachStudyRunCommands(study);
855
- attachStudyTesterCommands(study);
878
+ attachStudyParticipantCommands(study);
856
879
  attachStudyAnalyzeCommands(study);
857
880
  attachStudyScreenshotsCommands(study);
858
881
  }
@@ -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;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Short alias system for entity IDs.
3
3
  *
4
- * Generates deterministic aliases from UUID prefixes (e.g. w-6ec, s-b2c, tp-d4e).
4
+ * Generates deterministic aliases from UUID prefixes (e.g. w-6ec, s-b2c, p-d4e, pt-a3f).
5
5
  * Same UUID always produces the same alias — stable across commands and terminals.
6
6
  * Aliases are persisted to ~/.ish/aliases.json for resolution.
7
7
  */
@@ -10,9 +10,9 @@ export declare const ALIAS_PREFIX: {
10
10
  readonly workspace: "w";
11
11
  readonly study: "s";
12
12
  readonly iteration: "i";
13
- readonly testerProfile: "tp";
14
- readonly testerProfileSource: "tps";
15
- readonly tester: "t";
13
+ readonly person: "p";
14
+ readonly personSource: "ps";
15
+ readonly participant: "pt";
16
16
  readonly config: "c";
17
17
  readonly job: "j";
18
18
  readonly ask: "a";
@@ -46,7 +46,7 @@ export declare function deterministicAlias(prefix: string, uuid: string): string
46
46
  * Resolve a short alias to a full UUID, or validate and pass through a full UUID.
47
47
  *
48
48
  * Accepted formats:
49
- * - Short alias: w-6ec, s-b2c, tp-d4e (resolved from ~/.ish/aliases.json)
49
+ * - Short alias: w-6ec, s-b2c, p-d4e (resolved from ~/.ish/aliases.json)
50
50
  * - Full UUID: 6ecf2857-1d7a-4f9c-85da-c2ac6c5c5346
51
51
  *
52
52
  * Everything else throws with guidance toward the correct usage.