@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
@@ -24,7 +24,7 @@ function readTextOrAtFile(value) {
24
24
  }
25
25
  /**
26
26
  * Parse a JSON-blob flag that also supports `@filepath` (read from disk).
27
- * Used for tester_pair `--role-criteria-a/-b` and any future blob inputs.
27
+ * Used for participant_pair `--role-criteria-a/-b` and any future blob inputs.
28
28
  * Throws a descriptive Error on bad JSON or wrong top-level type.
29
29
  */
30
30
  function parseJsonOrAtFile(raw, flagName) {
@@ -46,27 +46,27 @@ function parseJsonOrAtFile(raw, flagName) {
46
46
  return parsed;
47
47
  }
48
48
  /**
49
- * Pattern C / M12: project each tester row on an iteration response so its
49
+ * Pattern C / M12: project each participant row on an iteration response so its
50
50
  * `alias` and `name` survive `leanJson` (which strips raw UUID values). The
51
- * existing `id` and `tester_profile` payload still pass through under
51
+ * existing `id` and `person` payload still pass through under
52
52
  * `--verbose`, but the agent-default `--json` shape now exposes the same
53
53
  * `{alias, name, status}` triple `study results` already does, so an agent
54
- * can correlate testers across the two verbs.
54
+ * can correlate participants across the two verbs.
55
55
  */
56
- function enrichIterationTesters(iteration) {
57
- const testers = iteration.testers;
58
- if (!Array.isArray(testers))
56
+ function enrichIterationParticipants(iteration) {
57
+ const participants = iteration.participants;
58
+ if (!Array.isArray(participants))
59
59
  return;
60
- for (const t of testers) {
60
+ for (const t of participants) {
61
61
  if (!t || typeof t !== "object")
62
62
  continue;
63
- const tester = t;
64
- const id = tester.id ? String(tester.id) : "";
63
+ const participant = t;
64
+ const id = participant.id ? String(participant.id) : "";
65
65
  if (id)
66
- tester.alias = tagAlias(ALIAS_PREFIX.tester, id);
67
- const profile = tester.tester_profile;
68
- if (!tester.name) {
69
- tester.name = profile?.name ?? tester.instance_name ?? null;
66
+ participant.alias = tagAlias(ALIAS_PREFIX.participant, id);
67
+ const profile = participant.person;
68
+ if (!participant.name) {
69
+ participant.name = profile?.name ?? participant.instance_name ?? null;
70
70
  }
71
71
  }
72
72
  }
@@ -164,7 +164,7 @@ function buildIterationDetails(modality, opts) {
164
164
  case "chat": {
165
165
  const mode = opts.chatMode === undefined ? "external_chatbot" : normalizeChatMode(opts.chatMode);
166
166
  if (mode === null) {
167
- throw new ValidationError(`Invalid --chat-mode "${opts.chatMode}". Expected "external_chatbot" or "tester_pair" (hyphenated variants accepted: "external-chatbot", "tester-pair").`, ["external_chatbot", "tester_pair"]);
167
+ throw new ValidationError(`Invalid --chat-mode "${opts.chatMode}". Expected "external_chatbot" or "participant_pair" (hyphenated variants accepted: "external-chatbot", "participant-pair").`, ["external_chatbot", "participant_pair"]);
168
168
  }
169
169
  let maxTurns;
170
170
  if (opts.maxTurns !== undefined) {
@@ -179,7 +179,7 @@ function buildIterationDetails(modality, opts) {
179
179
  ...(maxTurns !== undefined && { max_turns: maxTurns }),
180
180
  ...(opts.earlyTermination !== undefined && { early_termination: opts.earlyTermination }),
181
181
  };
182
- if (mode === "tester_pair") {
182
+ if (mode === "participant_pair") {
183
183
  // Reject external-chatbot flags so users don't silently lose them.
184
184
  const conflictingFlags = [];
185
185
  if (opts.chatEndpointId)
@@ -187,10 +187,10 @@ function buildIterationDetails(modality, opts) {
187
187
  if (opts.chatEndpointJson)
188
188
  conflictingFlags.push("--chat-endpoint-json");
189
189
  if (conflictingFlags.length > 0) {
190
- throw new ValidationError(`--chat-mode tester_pair is incompatible with ${conflictingFlags.join(", ")}. Use --audience-a/-b or --role-criteria-a/-b plus --scenario-a/-b instead.`, conflictingFlags);
190
+ throw new ValidationError(`--chat-mode participant_pair is incompatible with ${conflictingFlags.join(", ")}. Use --group-a/-b or --role-criteria-a/-b plus --scenario-a/-b instead.`, conflictingFlags);
191
191
  }
192
- const audA = (opts.audienceA ?? []).map(resolveId);
193
- const audB = (opts.audienceB ?? []).map(resolveId);
192
+ const audA = (opts.groupA ?? []).map(resolveId);
193
+ const audB = (opts.groupB ?? []).map(resolveId);
194
194
  const critARaw = parseJsonOrAtFile(opts.roleCriteriaA, "--role-criteria-a");
195
195
  const critBRaw = parseJsonOrAtFile(opts.roleCriteriaB, "--role-criteria-b");
196
196
  let critA;
@@ -205,14 +205,14 @@ function buildIterationDetails(modality, opts) {
205
205
  const sideAHasInput = audA.length > 0 || !!critA;
206
206
  const sideBHasInput = audB.length > 0 || !!critB;
207
207
  if (!sideAHasInput || !sideBHasInput) {
208
- throw new ValidationError("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).", ["--audience-a", "--audience-b", "--role-criteria-a", "--role-criteria-b"]);
208
+ throw new ValidationError("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).", ["--group-a", "--group-b", "--role-criteria-a", "--role-criteria-b"]);
209
209
  }
210
210
  // 1×N broadcast: the canonical "rehearse one side against N
211
211
  // variations" shape. When one side has exactly one explicit
212
212
  // profile and the other has more, broadcast the singleton so
213
213
  // every conversation has the same fixed side and a distinct
214
- // varying side. Example: --audience-a tp-rep --audience-b
215
- // tp-cto1,tp-cto2,tp-cto3 → N=3 conversations, same rep vs
214
+ // varying side. Example: --group-a p-rep --group-b
215
+ // p-cto1,p-cto2,p-cto3 → N=3 conversations, same rep vs
216
216
  // 3 different CTO personas. Same backend wire shape, just
217
217
  // CLI-side ergonomics.
218
218
  let audA_final = audA;
@@ -220,11 +220,11 @@ function buildIterationDetails(modality, opts) {
220
220
  let broadcastMsg;
221
221
  if (audA.length === 1 && audB.length > 1 && !critA && !critB) {
222
222
  audA_final = Array(audB.length).fill(audA[0]);
223
- broadcastMsg = `Broadcasting --audience-a (1 profile) to length ${audB.length} to match --audience-b — same side-A profile across all ${audB.length} conversations.`;
223
+ broadcastMsg = `Broadcasting --group-a (1 profile) to length ${audB.length} to match --group-b — same side-A profile across all ${audB.length} conversations.`;
224
224
  }
225
225
  else if (audB.length === 1 && audA.length > 1 && !critA && !critB) {
226
226
  audB_final = Array(audA.length).fill(audB[0]);
227
- broadcastMsg = `Broadcasting --audience-b (1 profile) to length ${audA.length} to match --audience-a — same side-B profile across all ${audA.length} conversations.`;
227
+ broadcastMsg = `Broadcasting --group-b (1 profile) to length ${audA.length} to match --group-a — same side-B profile across all ${audA.length} conversations.`;
228
228
  }
229
229
  if (broadcastMsg) {
230
230
  // stderr so it doesn't pollute --json stdout.
@@ -235,13 +235,13 @@ function buildIterationDetails(modality, opts) {
235
235
  // resolution happens server-side and may yield differing counts.
236
236
  const bothExplicit = audA_final.length > 0 && audB_final.length > 0 && !critA && !critB;
237
237
  if (bothExplicit && audA_final.length !== audB_final.length) {
238
- throw new ValidationError(`--audience-a (${audA_final.length}) and --audience-b (${audB_final.length}) cannot be paired. ` +
238
+ throw new ValidationError(`--group-a (${audA_final.length}) and --group-b (${audB_final.length}) cannot be paired. ` +
239
239
  `Pick the same number on each side (1:1 by index), or pass exactly one profile on one side to broadcast ` +
240
- `(e.g. --audience-a tp-rep --audience-b tp-cto1,tp-cto2,tp-cto3), ` +
241
- `or use --role-criteria-a/-b on either side to let the backend resolve the pool.`, ["--audience-a", "--audience-b"]);
240
+ `(e.g. --group-a p-rep --group-b p-cto1,p-cto2,p-cto3), ` +
241
+ `or use --role-criteria-a/-b on either side to let the backend resolve the pool.`, ["--group-a", "--group-b"]);
242
242
  }
243
243
  if (!opts.scenarioA || !opts.scenarioB) {
244
- throw new ValidationError("tester_pair chat iterations require --scenario-a and --scenario-b (text or @filepath).", ["--scenario-a", "--scenario-b"]);
244
+ throw new ValidationError("participant_pair chat iterations require --scenario-a and --scenario-b (text or @filepath).", ["--scenario-a", "--scenario-b"]);
245
245
  }
246
246
  const scenarioA = readTextOrAtFile(opts.scenarioA);
247
247
  const scenarioB = readTextOrAtFile(opts.scenarioB);
@@ -255,9 +255,9 @@ function buildIterationDetails(modality, opts) {
255
255
  return {
256
256
  ...topLevel,
257
257
  mode_details: {
258
- mode: "tester_pair",
259
- audience_a: audA_final,
260
- audience_b: audB_final,
258
+ mode: "participant_pair",
259
+ group_a: audA_final,
260
+ group_b: audB_final,
261
261
  scenario_a: scenarioA,
262
262
  scenario_b: scenarioB,
263
263
  initiator_side: initiatorRaw,
@@ -269,7 +269,7 @@ function buildIterationDetails(modality, opts) {
269
269
  // external_chatbot (default)
270
270
  const endpoint = parseJsonFlag(opts.chatEndpointJson, "--chat-endpoint-json");
271
271
  if (!endpoint && !opts.chatEndpointId) {
272
- throw new Error("Chat iterations require either --chat-endpoint-id (saved endpoint) or --chat-endpoint-json (inline config). For two-AI rehearsal use --chat-mode tester_pair with --audience-a/-b and --scenario-a/-b.");
272
+ throw new Error("Chat iterations require either --chat-endpoint-id (saved endpoint) or --chat-endpoint-json (inline config). For two-AI rehearsal use --chat-mode participant_pair with --group-a/-b and --scenario-a/-b.");
273
273
  }
274
274
  return {
275
275
  ...topLevel,
@@ -393,11 +393,11 @@ Concept pages: ish docs get-page concepts/iteration
393
393
  .option("--segmentation-json <json>", "Segmentation JSON — time_based {intervals_seconds, labels?}, section_based {sections[{name,label,...}]}, or page_based {} — media modalities")
394
394
  .option("--content-config-json <json>", "Content config JSON — {early_termination, selected_segment_indices?} — media modalities")
395
395
  // Chat modality
396
- .option("--chat-mode <mode>", "Chat mode: external_chatbot (default; probe a customer chatbot) or tester_pair (two AI audiences talk to each other)")
396
+ .option("--chat-mode <mode>", "Chat mode: external_chatbot (default; probe a customer chatbot) or participant_pair (two AI people talk to each other)")
397
397
  .option("--chat-endpoint-id <id>", "Saved chatbot endpoint id — chat modality, external_chatbot mode (legacy; prefer --endpoint)")
398
398
  .option("--chat-endpoint-json <json>", "Inline chatbot endpoint config JSON — chat modality, external_chatbot mode (legacy; prefer --endpoint-config)")
399
- .option("--max-turns <n>", "Max tester turns (1-50) — chat modality (default 12)")
400
- .option("--early-termination", "End the chat session early when the tester signals stop — chat modality")
399
+ .option("--max-turns <n>", "Max participant turns (1-50) — chat modality (default 12)")
400
+ .option("--early-termination", "End the chat session early when the participant signals stop — chat modality")
401
401
  // Agent-friendly chat shortcuts: --endpoint <id> resolves a saved
402
402
  // chatbot endpoint via alias / UUID and fetches its config inline;
403
403
  // --endpoint-config <file> takes a raw config file (or `-` for
@@ -406,16 +406,16 @@ Concept pages: ish docs get-page concepts/iteration
406
406
  // external_chatbot mode.
407
407
  .option("--endpoint <id>", "Saved chatbot endpoint id (alias or UUID) — chat modality, external_chatbot mode")
408
408
  .option("--endpoint-config <file>", "Raw ChatbotEndpointConfig JSON file or `-` for stdin — chat modality, external_chatbot mode")
409
- // tester_pair (two-AI rehearsal) flags. audience_a and audience_b must
409
+ // participant_pair (two-AI rehearsal) flags. group_a and group_b must
410
410
  // be the same length — pairs are 1:1 by index. Each side has its own
411
411
  // scenario + goal; the partner does NOT see the other side's prompt.
412
- .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", collectIds, [])
413
- .option("--audience-b <ids>", "Tester profile IDs/aliases for audience B (comma-separated or repeatable). 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", collectIds, [])
414
- .option("--scenario-a <text-or-@file>", "Side-A scenario + goal text, or @filepath — chat tester_pair mode")
415
- .option("--scenario-b <text-or-@file>", "Side-B scenario + goal text, or @filepath — chat tester_pair mode")
416
- .option("--initiator-side <a|b>", "Which side speaks first (default: a) — chat tester_pair mode")
417
- .option("--role-criteria-a <json-or-@file>", 'RoleCriteria filter for side A (inline JSON 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. Persona-first: filters the eligible profile pool without altering personas. Use INSTEAD of --audience-a or alongside it (criteria then validates the explicit list). chat tester_pair mode.')
418
- .option("--role-criteria-b <json-or-@file>", "RoleCriteria filter for side B — same shape as --role-criteria-a. chat tester_pair mode.")
412
+ .option("--group-a <ids>", "Person IDs/aliases for side A (comma-separated or repeatable). Pass a single person and N on --group-b to broadcast (1×N rehearsal: fix side A, vary side B) — chat participant_pair mode", collectIds, [])
413
+ .option("--group-b <ids>", "Person IDs/aliases for side B (comma-separated or repeatable). 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", collectIds, [])
414
+ .option("--scenario-a <text-or-@file>", "Side-A scenario + goal text, or @filepath — chat participant_pair mode")
415
+ .option("--scenario-b <text-or-@file>", "Side-B scenario + goal text, or @filepath — chat participant_pair mode")
416
+ .option("--initiator-side <a|b>", "Which side speaks first (default: a) — chat participant_pair mode")
417
+ .option("--role-criteria-a <json-or-@file>", 'RoleCriteria filter for side A (inline JSON 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. Persona-first: filters the eligible profile pool without altering personas. Use INSTEAD of --group-a or alongside it (criteria then validates the explicit list). chat participant_pair mode.')
418
+ .option("--role-criteria-b <json-or-@file>", "RoleCriteria filter for side B — same shape as --role-criteria-a. chat participant_pair mode.")
419
419
  // Escape hatch
420
420
  .option("--details-json <json>", "Raw iteration details JSON (overrides individual flags)")
421
421
  .addHelpText("after", `
@@ -517,16 +517,16 @@ Next: \`ish study run\` to dispatch simulations against this iteration.`)
517
517
  if (!isChat && (opts.endpoint !== undefined || opts.endpointConfig !== undefined)) {
518
518
  throw new Error(`This study uses "${modality}" modality — --endpoint / --endpoint-config are for chat studies.`);
519
519
  }
520
- const pairFlagsSet = (opts.audienceA && opts.audienceA.length > 0)
521
- || (opts.audienceB && opts.audienceB.length > 0)
520
+ const pairFlagsSet = (opts.groupA && opts.groupA.length > 0)
521
+ || (opts.groupB && opts.groupB.length > 0)
522
522
  || opts.scenarioA !== undefined
523
523
  || opts.scenarioB !== undefined
524
524
  || opts.initiatorSide !== undefined
525
525
  || opts.roleCriteriaA !== undefined
526
526
  || opts.roleCriteriaB !== undefined
527
- || normalizeChatMode(opts.chatMode) === "tester_pair";
527
+ || normalizeChatMode(opts.chatMode) === "participant_pair";
528
528
  if (!isChat && pairFlagsSet) {
529
- throw new Error(`This study uses "${modality}" modality — --chat-mode / --audience-a/-b / --scenario-a/-b / --role-criteria-a/-b are for chat studies.`);
529
+ throw new Error(`This study uses "${modality}" modality — --chat-mode / --group-a/-b / --scenario-a/-b / --role-criteria-a/-b are for chat studies.`);
530
530
  }
531
531
  // Validate per-modality required flags BEFORE any upload so we don't
532
532
  // orphan blobs in storage when the wrong flag is passed (e.g.
@@ -556,9 +556,9 @@ Next: \`ish study run\` to dispatch simulations against this iteration.`)
556
556
  case "chat": {
557
557
  const mode = opts.chatMode === undefined ? "external_chatbot" : normalizeChatMode(opts.chatMode);
558
558
  if (mode === null) {
559
- throw new ValidationError(`Invalid --chat-mode "${opts.chatMode}". Expected "external_chatbot" or "tester_pair" (hyphenated variants accepted).`, ["external_chatbot", "tester_pair"]);
559
+ throw new ValidationError(`Invalid --chat-mode "${opts.chatMode}". Expected "external_chatbot" or "participant_pair" (hyphenated variants accepted).`, ["external_chatbot", "participant_pair"]);
560
560
  }
561
- if (mode === "tester_pair") {
561
+ if (mode === "participant_pair") {
562
562
  const conflicting = [];
563
563
  if (opts.endpoint)
564
564
  conflicting.push("--endpoint");
@@ -569,15 +569,15 @@ Next: \`ish study run\` to dispatch simulations against this iteration.`)
569
569
  if (opts.chatEndpointJson)
570
570
  conflicting.push("--chat-endpoint-json");
571
571
  if (conflicting.length > 0) {
572
- throw new ValidationError(`--chat-mode tester_pair is incompatible with ${conflicting.join(", ")}. Use --audience-a/-b or --role-criteria-a/-b plus --scenario-a/-b instead.`, conflicting);
572
+ throw new ValidationError(`--chat-mode participant_pair is incompatible with ${conflicting.join(", ")}. Use --group-a/-b or --role-criteria-a/-b plus --scenario-a/-b instead.`, conflicting);
573
573
  }
574
- const sideAHasInput = (opts.audienceA && opts.audienceA.length > 0) || !!opts.roleCriteriaA;
575
- const sideBHasInput = (opts.audienceB && opts.audienceB.length > 0) || !!opts.roleCriteriaB;
574
+ const sideAHasInput = (opts.groupA && opts.groupA.length > 0) || !!opts.roleCriteriaA;
575
+ const sideBHasInput = (opts.groupB && opts.groupB.length > 0) || !!opts.roleCriteriaB;
576
576
  if (!sideAHasInput || !sideBHasInput) {
577
- 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).");
577
+ 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).");
578
578
  }
579
579
  if (!opts.scenarioA || !opts.scenarioB) {
580
- throw new Error("tester_pair chat iterations require --scenario-a <text-or-@file> and --scenario-b <text-or-@file>.");
580
+ throw new Error("participant_pair chat iterations require --scenario-a <text-or-@file> and --scenario-b <text-or-@file>.");
581
581
  }
582
582
  break;
583
583
  }
@@ -586,7 +586,7 @@ Next: \`ish study run\` to dispatch simulations against this iteration.`)
586
586
  && !opts.chatEndpointJson
587
587
  && !opts.endpoint
588
588
  && !opts.endpointConfig) {
589
- throw new Error("Chat iterations (external_chatbot mode) require one of: --endpoint <id>, --endpoint-config <file>, --chat-endpoint-id, or --chat-endpoint-json. For two-AI rehearsal use --chat-mode tester_pair.");
589
+ throw new Error("Chat iterations (external_chatbot mode) require one of: --endpoint <id>, --endpoint-config <file>, --chat-endpoint-id, or --chat-endpoint-json. For two-AI rehearsal use --chat-mode participant_pair.");
590
590
  }
591
591
  if ((opts.endpoint || opts.endpointConfig)
592
592
  && (opts.chatEndpointId || opts.chatEndpointJson)) {
@@ -708,7 +708,7 @@ With multiple IDs, returns a {items:[...], total:N} envelope.`)
708
708
  const result = data;
709
709
  if (result.id)
710
710
  result.alias = tagAlias(ALIAS_PREFIX.iteration, String(result.id));
711
- enrichIterationTesters(result);
711
+ enrichIterationParticipants(result);
712
712
  output(result, globals.json);
713
713
  return;
714
714
  }
@@ -717,7 +717,7 @@ With multiple IDs, returns a {items:[...], total:N} envelope.`)
717
717
  const r = data;
718
718
  if (r.id)
719
719
  r.alias = tagAlias(ALIAS_PREFIX.iteration, String(r.id));
720
- enrichIterationTesters(r);
720
+ enrichIterationParticipants(r);
721
721
  return r;
722
722
  }));
723
723
  if (globals.json) {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * ish mcp — wire the ish MCP server into local AI clients.
3
+ *
4
+ * Three verbs:
5
+ * - `ish mcp add` Wire ish MCP into selected clients (idempotent).
6
+ * - `ish mcp list` Show detected clients + ish-MCP wiring status.
7
+ * - `ish mcp remove` Inverse of add — cleanly unwire the ish block.
8
+ *
9
+ * Design choices (per plan workstream C):
10
+ * - No API call required — all writes are local JSON files. We use
11
+ * `runInline` instead of `withClient`.
12
+ * - Atomic writes (tmp file + rename), deep-equal idempotence, and
13
+ * unrelated keys in the target config are preserved verbatim.
14
+ * - Never embed access tokens in the file. The hosted ish MCP server
15
+ * handles OAuth on first connect; we only write the URL.
16
+ * - No interactive prompts by default (CLI is for autonomous agents).
17
+ * `--yes` is required to commit writes under `--json` / non-TTY.
18
+ *
19
+ * Per-client config paths + per-client server-block shapes live in
20
+ * `src/lib/mcp-clients.ts` (MCP_CLIENT_TARGETS).
21
+ */
22
+ import type { Command } from "commander";
23
+ export declare function registerMcpCommands(program: Command): void;