@ishlabs/cli 0.8.4 → 0.8.5

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.
@@ -6,7 +6,7 @@ import * as fs from "node:fs";
6
6
  import { resolveApiUrl, resolveToken } from "./auth.js";
7
7
  import { getAppUrl } from "../auth.js";
8
8
  import { ApiClient, ApiError } from "./api-client.js";
9
- import { outputError, setVerbose, setFields } from "./output.js";
9
+ import { outputError, setVerbose, setFields, setGetField } from "./output.js";
10
10
  import { setColorsEnabled, colorsEnabled } from "./colors.js";
11
11
  import { loadConfig } from "../config.js";
12
12
  import { resolveId } from "./alias-store.js";
@@ -129,8 +129,58 @@ export async function resolveAudienceProfileIds(client, workspace, flags, opts =
129
129
  if (opts.excludeProfileIds && opts.excludeProfileIds.size > 0 && !filterDesc) {
130
130
  throw new Error("All matching profiles are already in this audience.");
131
131
  }
132
+ // When --country was the binding constraint, query the broader pool (drop
133
+ // country filter, keep the rest) and surface the top populated countries
134
+ // so the agent doesn't have to round-trip through `ish profile list` to
135
+ // find one that matches. Pure best-effort — any failure falls back to the
136
+ // original error.
137
+ let suggestion = "";
138
+ if (flags.country && flags.country.length > 0) {
139
+ try {
140
+ const broader = {
141
+ product_id: workspace,
142
+ type: "ai",
143
+ limit: "500",
144
+ offset: "0",
145
+ };
146
+ if (flags.search)
147
+ broader.search = flags.search;
148
+ if (flags.gender && flags.gender.length > 0)
149
+ broader.gender = flags.gender;
150
+ if (flags.minAge)
151
+ broader.min_age = flags.minAge;
152
+ if (flags.maxAge)
153
+ broader.max_age = flags.maxAge;
154
+ if (flags.visibility)
155
+ broader.visibility = flags.visibility;
156
+ const broaderData = await client.get("/tester-profiles", broader);
157
+ const broaderItems = Array.isArray(broaderData)
158
+ ? broaderData
159
+ : Array.isArray(broaderData?.items)
160
+ ? broaderData.items
161
+ : [];
162
+ const broaderPool = opts.requireSimulatable
163
+ ? broaderItems.filter(isSimulatable)
164
+ : broaderItems;
165
+ const counts = new Map();
166
+ for (const p of broaderPool) {
167
+ const c = typeof p.country === "string" ? p.country : null;
168
+ if (c)
169
+ counts.set(c, (counts.get(c) ?? 0) + 1);
170
+ }
171
+ const top = [...counts.entries()]
172
+ .sort((a, b) => b[1] - a[1])
173
+ .slice(0, 3);
174
+ if (top.length > 0) {
175
+ suggestion = ` Populated countries with these other filters: ${top.map(([c, n]) => `${c} (${n})`).join(", ")}.`;
176
+ }
177
+ }
178
+ catch {
179
+ // Swallow — never replace the user's error with a secondary failure.
180
+ }
181
+ }
132
182
  if (filterDesc) {
133
- throw new Error(`No ${sim}tester profiles in workspace ${workspace} match: ${filterDesc}. Broaden your filters or run \`ish profile list\` to inspect the pool.`);
183
+ throw new Error(`No ${sim}tester profiles in workspace ${workspace} match: ${filterDesc}.${suggestion} Broaden your filters or run \`ish profile list\` to inspect the pool.`);
134
184
  }
135
185
  throw new Error(`No ${sim}tester profiles found in workspace ${workspace}.${opts.requireSimulatable ? " Create profiles with simulation configs first." : ""}`);
136
186
  }
@@ -165,9 +215,52 @@ export function addAudienceFilterFlags(cmd, opts = {}) {
165
215
  .option("--visibility <v>", "Filter by visibility (private|public)");
166
216
  }
167
217
  export function getGlobals(cmd) {
218
+ let globals;
219
+ try {
220
+ globals = computeGlobals(cmd);
221
+ }
222
+ catch (err) {
223
+ // Validation errors (e.g. --get with --human) need to surface as a
224
+ // clean usage error regardless of which entry point invoked us. Many
225
+ // commands resolve globals without a withClient/runInline wrapper
226
+ // (e.g. `ish docs *`) so we cannot rely on those try/catches.
227
+ const useJson = process.argv.includes("--json")
228
+ || process.argv.includes("--get")
229
+ || !process.stdout.isTTY;
230
+ outputError(err, useJson);
231
+ process.exit(exitCodeFromError(err));
232
+ }
233
+ // Apply side effects (verbose, fields, colors, get-field, active workspace)
234
+ // here so commands that resolve globals without going through withClient /
235
+ // runInline still get --get / --human / --fields honored.
236
+ applyGlobals(globals);
237
+ return globals;
238
+ }
239
+ function computeGlobals(cmd) {
168
240
  const opts = cmd.optsWithGlobals();
169
- // Auto-switch to JSON when stdout is piped (non-TTY)
170
- const json = opts.json ?? !process.stdout.isTTY;
241
+ // Pattern Ω: display-vs-capture controls.
242
+ // --get <field> implies --json (need structured data to extract from).
243
+ // --human forces the human renderer regardless of TTY/pipe state.
244
+ // Both passed together is a usage error — capture and display are
245
+ // different modes; pick one.
246
+ const getField = typeof opts.get === "string" && opts.get.length > 0
247
+ ? opts.get
248
+ : undefined;
249
+ const human = opts.human === true;
250
+ if (getField && human) {
251
+ const err = new Error("--get and --human are mutually exclusive: --get captures a value (JSON-derived), --human forces human display.");
252
+ err.name = "ValidationError";
253
+ throw err;
254
+ }
255
+ // Auto-switch to JSON when stdout is piped (non-TTY).
256
+ // --human overrides the auto-flip; --get implies --json.
257
+ let json;
258
+ if (human)
259
+ json = false;
260
+ else if (getField)
261
+ json = true;
262
+ else
263
+ json = opts.json ?? !process.stdout.isTTY;
171
264
  // Parse --fields into an array
172
265
  const fields = opts.fields
173
266
  ? String(opts.fields).split(",").map((f) => f.trim()).filter(Boolean)
@@ -182,9 +275,15 @@ export function getGlobals(cmd) {
182
275
  dev: opts.dev,
183
276
  json,
184
277
  verbose: opts.verbose ?? false,
185
- quiet: opts.quiet ?? (json && !opts.json), // auto-quiet when auto-json via pipe
278
+ // --get is silent capture: suppress stderr progress so the bare value is
279
+ // the only thing the agent has to parse. --human keeps progress on.
280
+ quiet: opts.quiet ?? (getField ? true : (json && !opts.json)),
281
+ quietExplicit: opts.quiet === true || getField !== undefined,
186
282
  color,
187
283
  fields,
284
+ get: getField,
285
+ human,
286
+ workspace: typeof opts.workspace === "string" ? opts.workspace : undefined,
188
287
  };
189
288
  }
190
289
  /**
@@ -224,14 +323,23 @@ export async function createClient(globals) {
224
323
  const token = await resolveToken(globals.token, apiUrl, globals.tokenFile);
225
324
  return new ApiClient({ apiUrl, token });
226
325
  }
326
+ /**
327
+ * Module-level fallback for `resolveWorkspace`. Set by `applyGlobals` from the
328
+ * merged `optsWithGlobals().workspace`, so the program-root --workspace flag
329
+ * propagates to subcommand resolvers without each call site having to thread
330
+ * `globals` through. Subcommand-level --workspace still wins via Commander's
331
+ * own merge (it's already reflected in `globals.workspace`).
332
+ */
333
+ let _activeWorkspace;
227
334
  function applyGlobals(globals) {
228
335
  setVerbose(globals.verbose);
229
336
  setFields(globals.fields);
337
+ setGetField(globals.get);
230
338
  setColorsEnabled(globals.color);
339
+ _activeWorkspace = globals.workspace;
231
340
  }
232
341
  export async function withClient(cmd, fn) {
233
342
  const globals = getGlobals(cmd);
234
- applyGlobals(globals);
235
343
  try {
236
344
  const client = await createClient(globals);
237
345
  await fn(client, globals);
@@ -248,7 +356,6 @@ export async function withClient(cmd, fn) {
248
356
  */
249
357
  export async function runInline(cmd, fn) {
250
358
  const globals = getGlobals(cmd);
251
- applyGlobals(globals);
252
359
  try {
253
360
  await fn(globals);
254
361
  }
@@ -299,9 +406,61 @@ export function readJsonFileOrStdin(filePath) {
299
406
  process.stdin.on("error", reject);
300
407
  });
301
408
  }
409
+ /**
410
+ * Prompt for confirmation of a destructive action, or short-circuit when
411
+ * `--yes` is set. In `--json` mode without `--yes` we refuse with a usage
412
+ * error rather than silently proceeding or hanging on a prompt — agents
413
+ * piping through CLI must be explicit about destructive intent.
414
+ */
415
+ export async function confirmDestructive(prompt, opts) {
416
+ if (opts.yes)
417
+ return;
418
+ if (opts.json) {
419
+ const err = new Error(`--yes is required for destructive actions in --json mode. Refusing to proceed without explicit confirmation.`);
420
+ err.name = "ValidationError";
421
+ throw err;
422
+ }
423
+ if (!process.stdin.isTTY) {
424
+ const err = new Error(`--yes is required for destructive actions when stdin is not a TTY. Refusing to proceed without explicit confirmation.`);
425
+ err.name = "ValidationError";
426
+ throw err;
427
+ }
428
+ process.stderr.write(`${prompt} [y/N] `);
429
+ const answer = await new Promise((resolve, reject) => {
430
+ let data = "";
431
+ const onData = (chunk) => {
432
+ data += chunk.toString();
433
+ if (data.includes("\n")) {
434
+ process.stdin.off("data", onData);
435
+ process.stdin.off("error", onError);
436
+ process.stdin.pause();
437
+ resolve(data.trim().toLowerCase());
438
+ }
439
+ };
440
+ const onError = (err) => {
441
+ process.stdin.off("data", onData);
442
+ process.stdin.off("error", onError);
443
+ reject(err);
444
+ };
445
+ process.stdin.setEncoding("utf-8");
446
+ process.stdin.on("data", onData);
447
+ process.stdin.on("error", onError);
448
+ process.stdin.resume();
449
+ });
450
+ if (answer !== "y" && answer !== "yes") {
451
+ const err = new Error("Aborted.");
452
+ err.name = "ValidationError";
453
+ throw err;
454
+ }
455
+ }
302
456
  export function resolveWorkspace(explicit) {
303
457
  if (explicit)
304
458
  return resolveId(explicit);
459
+ // Fall back to the program-root --workspace cached by applyGlobals — covers
460
+ // `ish --workspace W study list` where the subcommand action doesn't see the
461
+ // flag in its local opts.
462
+ if (_activeWorkspace)
463
+ return resolveId(_activeWorkspace);
305
464
  const env = process.env.ISH_WORKSPACE;
306
465
  if (env)
307
466
  return resolveId(env);
package/dist/lib/docs.js CHANGED
@@ -47,7 +47,8 @@ Two top-level run verbs:
47
47
  and prints active workspace/study/ask. See \`concepts/active-context\`.
48
48
  - Running your first study? \`ish docs get-page guides/first-study\`.
49
49
  - Comparing study vs ask? \`ish docs get-page concepts/run-verbs\`.
50
- - Need machine-readable output? \`ish docs get-page reference/json-mode\`.
50
+ - **Output modes** (display vs capture vs chain — \`--human\`, \`--get\`,
51
+ \`--json\`)? \`ish docs get-page reference/json-mode\`.
51
52
  - Auth gated URL? \`ish docs get-page concepts/site-access\`.
52
53
 
53
54
  ## Install the skill into this project
@@ -74,6 +75,22 @@ A workspace carries:
74
75
  - Site-access credentials (encrypted at rest) — see \`concepts/site-access\`.
75
76
  - Tester profiles + sources visible to every study/ask in the workspace.
76
77
 
78
+ ## Selecting a workspace per command
79
+
80
+ \`--workspace <id>\` works at the **program root** as well as on each
81
+ subcommand — both forms are equivalent, and the subcommand-level flag
82
+ wins on conflict:
83
+
84
+ \`\`\`
85
+ ish --workspace w-6ec study list # program root
86
+ ish study list --workspace w-6ec # subcommand (same effect)
87
+ ish --workspace w-6ec study list --workspace w-other # w-other wins
88
+ \`\`\`
89
+
90
+ Use whichever is most natural for your scripting. Without either, the
91
+ CLI falls back to \`ISH_WORKSPACE\` (env var) and then the
92
+ \`workspace\` saved in \`~/.ish/config.json\`.
93
+
77
94
  ## Common commands
78
95
 
79
96
  \`\`\`
@@ -148,6 +165,21 @@ Every study response carries two status-shaped fields:
148
165
  The CLI also surfaces a \`status_inferred\` field + stderr warning when
149
166
  it detects raw-vs-derived inconsistencies. See \`reference/json-mode\`.
150
167
 
168
+ ## Deleting a study
169
+
170
+ \`ish study delete <id>\` requires explicit confirmation:
171
+
172
+ - **Interactive (TTY)**: prompts on stderr; type \`y\` to proceed.
173
+ - **Non-interactive** (\`--json\`, piped, or non-TTY stdin): pass
174
+ \`-y\` / \`--yes\` to confirm. Without it, the CLI exits with usage
175
+ code 2 rather than deleting silently.
176
+
177
+ \`\`\`
178
+ ish study delete s-b2c # interactive prompt
179
+ ish study delete s-b2c --yes # skip prompt
180
+ ish study delete s-b2c --json --yes # JSON consumers must be explicit
181
+ \`\`\`
182
+
151
183
  ## Generate vs create
152
184
 
153
185
  \`ish study generate --problem "..."\` runs an LLM-backed flow that
@@ -379,7 +411,12 @@ ish ask results a-6ec --json | jq '.rounds[0].aggregates'
379
411
 
380
412
  For \`--wants-pick\` / \`--wants-ratings\` rounds, \`ask results --json\`
381
413
  includes an \`aggregates\` field per round so you don't have to parse
382
- prose:
414
+ prose. Each individual pick also carries a **\`pick_confidence\`** score
415
+ (0..1) — the model's self-reported confidence in its variant choice.
416
+ Use it to break ties: when two variants are nominally close on count,
417
+ the variant with higher mean \`pick_confidence\` is the more decisive
418
+ choice. \`pick_confidence\` is only present on rounds run with
419
+ \`--wants-pick\`.
383
420
 
384
421
  \`\`\`json
385
422
  {
@@ -517,6 +554,24 @@ ish profile create --file profile.json
517
554
  Expected JSON: \`{ "name": "...", "type": "ai", "gender": "female",
518
555
  "country": "US", "occupation": "...", "bio": "..." }\`
519
556
 
557
+ ## Generation behavior to expect
558
+
559
+ - **Latency**: \`profile generate\` is LLM-backed and typically takes
560
+ 10–20s for 1–5 profiles. The CLI emits stderr progress lines
561
+ (\`generating N profiles…\` then \`generated N profiles\`) so you
562
+ know it's not stuck. Suppress with \`--quiet\`.
563
+ - **Brief fidelity**: bios reference domain-specific terms from your
564
+ description verbatim or as close paraphrase. If you mention
565
+ \`F-skatt\`, "manual Excel invoicing", "Stripe payouts", or similar
566
+ tools/jargon, expect those terms (or paraphrases) to appear in
567
+ each generated bio's daily-routine framing — not sanded down to
568
+ generic prose.
569
+ - **DOB diversity**: month-and-day are derived from a deterministic
570
+ per-profile hash so birthdays spread across the year (no more
571
+ every-profile-on-\`06-15\`). Year follows the requested age.
572
+ Re-generating the same name/country/occupation/age yields the
573
+ same DOB.
574
+
520
575
  ## Related
521
576
 
522
577
  - \`concepts/source\` — the inputs to \`profile generate\`.
@@ -577,6 +632,24 @@ flags. Two ways to select:
577
632
  The two modes are **mutually exclusive** — pass either \`--profile\` or
578
633
  the filter set, not both.
579
634
 
635
+ ## Empty-pool suggestions
636
+
637
+ When a filter combination matches zero profiles, the error message
638
+ includes the top three populated countries that satisfy your *other*
639
+ filters — so you can pivot to a country with actual coverage without a
640
+ second \`profile list\` round-trip:
641
+
642
+ \`\`\`
643
+ $ ish study run --country XX --min-age 35 --sample 5
644
+ Error: No simulatable AI tester profiles in workspace w-b32 match:
645
+ --country XX --min-age 35.
646
+ Populated countries with these other filters: SE (12), DE (8), NL (3).
647
+ Broaden your filters or run \`ish profile list\` to inspect the pool.
648
+ \`\`\`
649
+
650
+ The suggestion is best-effort — it never replaces the original error,
651
+ just augments it.
652
+
580
653
  ## Defaults
581
654
 
582
655
  - \`ish study run\` with no audience flags → reuses the iteration's
@@ -709,6 +782,13 @@ ish study cancel <tester_id> # cancel a running simulation
709
782
  \`<tester_id>\` accepts a tester alias (\`t-…\`) or a full UUID. The
710
783
  study-level \`poll\`/\`wait\` forms also exist (\`--study <id>\` /
711
784
  \`--iteration <id>\`) for whole-batch progress.
785
+
786
+ ## Related
787
+
788
+ - \`reference/json-mode\` — output modes (display vs capture vs chain).
789
+ Use \`--get tester_aliases\` to capture the run's testers without
790
+ piping through \`jq\`. \`--human\` forces table output even through
791
+ \`tee\`/redirection.
712
792
  `;
713
793
  const REFERENCE_ALIASES = `# reference: aliases
714
794
 
@@ -740,15 +820,84 @@ ish profile generate --source tps-3a4 --count 4
740
820
  The full UUID is also always accepted. Add \`--verbose\` to JSON output
741
821
  to see UUIDs alongside aliases.
742
822
  `;
743
- const REFERENCE_JSON_MODE = `# reference: JSON output for agents
823
+ const REFERENCE_JSON_MODE = `# reference: output modes for agents
824
+
825
+ \`ish\` distinguishes **three output modes** so agents don't have to
826
+ post-process CLI output with \`jq\` or \`python\` for routine tasks:
827
+
828
+ 1. **Display mode (human)** — readable tables and key/value blocks.
829
+ Default on a TTY. Force it anywhere with \`--human\` (e.g. \`ish
830
+ workspace list --human | tee /tmp/x.txt\` keeps the table layout
831
+ even though stdout is redirected).
832
+ 2. **Capture mode (single value)** — \`--get <field>\` extracts the
833
+ value at a dotted path and prints it bare (no JSON quotes, no
834
+ indentation). Use this to feed one CLI's output into another:
835
+ \`ASK=$(ish ask create … --get alias)\` instead of
836
+ \`ASK=$(ish ask create … --json | jq -r .alias)\`.
837
+ 3. **Chain mode (full JSON)** — \`--json\` (or auto-enabled when stdout
838
+ is piped). Returns structured payloads for downstream parsing.
839
+ Reach for this only when you actually need multiple fields or a
840
+ nested shape; for one value, \`--get\` is shorter.
841
+
842
+ ## Picking the right mode
843
+
844
+ | You want to… | Mode |
845
+ |-------------------------------------------|--------------------------------------------------|
846
+ | Show the user a list of workspaces | bare command (TTY) or \`--human\` if redirecting |
847
+ | Capture an alias for a follow-up command | \`--get alias\` |
848
+ | Inspect a specific nested field | \`--get tester_profile.name\` |
849
+ | Compare 2+ fields, or pipe into jq | \`--json\` (or auto-on when piped) |
850
+ | Force human output through \`tee\` | \`--human\` |
851
+ | Force JSON on a TTY | \`--json\` |
852
+
853
+ \`--get\` and \`--human\` are mutually exclusive — capture and display are
854
+ different intents; pick one. \`--get\` implies \`--json\` internally so the
855
+ renderer always has structured data to extract from; you don't need to
856
+ add \`--json\` yourself.
857
+
858
+ ### Worked example: display vs. capture
859
+
860
+ \`\`\`bash
861
+ # Display: bare command on a TTY → human table.
862
+ ish workspace list
863
+
864
+ # Capture: feed one alias into the next command, no jq required.
865
+ ASK=$(ish ask create --new --name demo \\
866
+ --prompt "Which?" --variant text:A --variant text:B \\
867
+ --sample 30 --get alias)
868
+ ish ask wait "$ASK" --timeout 600
869
+
870
+ # Capture across an entire list: one value per line.
871
+ ish workspace list --get alias
872
+ # w-6ec
873
+ # w-d02
874
+ # …
744
875
 
745
- Every command that produces output supports machine-readable JSON. JSON
746
- mode is **auto-enabled when stdout is piped**, so an agent rarely needs
747
- \`--json\` explicitly.
876
+ # Display preserved through tee:
877
+ ish ask results "$ASK" --human | tee /tmp/transcript.txt
878
+ \`\`\`
748
879
 
749
880
  ## Flags
750
881
 
751
- - \`--json\` — force JSON output even on a TTY.
882
+ - \`--human\` — force human-readable output regardless of TTY
883
+ state (overrides the auto-flip-to-JSON when
884
+ stdout is piped). Mutually exclusive with
885
+ \`--get\`.
886
+ - \`--get <field>\` — extract a single field from the JSON response
887
+ and print only its bare value. Supports dotted
888
+ paths (\`tester_profile.name\`). On a paginated
889
+ \`{items: [...]}\` response, the path
890
+ auto-descends into \`items\` so \`--get alias\`
891
+ on a list yields one value per line. Implies
892
+ \`--json\` internally; mutually exclusive with
893
+ \`--human\`. Strings/numbers/bools are printed
894
+ unquoted; \`null\` prints as an empty line;
895
+ arrays print one element per line; objects
896
+ print as compact one-line JSON. Missing field
897
+ → exit 2 with a usage error.
898
+ - \`--json\` — force JSON output even on a TTY. Auto-enabled
899
+ when stdout is piped (unless \`--human\` is
900
+ set).
752
901
  - \`--fields a,b,c\` — keep only these fields in JSON output (e.g.
753
902
  \`alias,name,status\`). Filters per item only;
754
903
  list wrappers (\`{items, total, returned,
@@ -757,7 +906,9 @@ mode is **auto-enabled when stdout is piped**, so an agent rarely needs
757
906
  write paths) the full server payload instead
758
907
  of the compact response.
759
908
  - \`-q, --quiet\` — suppress progress messages on stderr (errors
760
- still go to stderr).
909
+ still go to stderr). \`--get\` implies
910
+ \`--quiet\` so the bare value is the only
911
+ thing on stdout.
761
912
 
762
913
  ## Stable shape rules
763
914
 
@@ -854,9 +1005,12 @@ The CLI guarantees these contracts so agents can chain safely:
854
1005
  in the \`testers[]\` array, and a "Failed testers" subsection in
855
1006
  human output. Empty when the tester succeeded.
856
1007
  - **\`profile list\` emits a stderr pagination hint** when
857
- \`has_more=true\` and stdout is human (TTY, not piped, not \`--quiet\`).
858
- Format: "showing N–M of TOTAL; pass --offset M --limit N for more."
859
- JSON consumers read \`has_more\` directly off the envelope.
1008
+ \`has_more=true\` and \`--quiet\` is not set. The hint goes to **stderr
1009
+ in every mode** including \`--json\` and piped stdout it never
1010
+ pollutes machine-readable stdout but is visible to any agent that
1011
+ reads stderr (which they should, for warnings and progress). Format:
1012
+ "showing N–M of TOTAL; pass --offset M --limit N for more."
1013
+ JSON consumers can also read \`has_more\` directly off the envelope.
860
1014
  - **\`ask results --json\` adds an \`aggregates\` field per round.** For
861
1015
  rounds with \`wants_pick\`/\`wants_ratings\`, the CLI computes the
862
1016
  verdict locally so agents don't have to parse comment prose:
@@ -938,25 +1092,43 @@ a structured error object on **stdout** and a human message on
938
1092
  ## Examples
939
1093
 
940
1094
  \`\`\`
941
- ish workspace list --json | jq '.[].alias'
942
- ish study get s-b2c --fields alias,name,status,iterations
1095
+ # Display (table on TTY, JSON when piped):
1096
+ ish workspace list
1097
+
1098
+ # Display preserved through tee/pipe (force human):
1099
+ ish ask results a-6ec --human | tee /tmp/results.txt
1100
+
1101
+ # Capture a single alias to feed into the next command:
1102
+ WS=$(ish workspace list --get alias | head -1)
1103
+
1104
+ # Inspect a nested field:
1105
+ ish study tester t-a17 --get tester_profile.name
1106
+
1107
+ # Chain (full JSON for jq when you need multiple fields):
1108
+ ish study get s-b2c --fields alias,name,status,iterations --json
943
1109
  ish ask results a-6ec --round 1 --json
944
- ish profile generate --description "..." --count 3 --json | jq '.[].alias'
945
1110
  \`\`\`
946
1111
 
947
1112
  ## Composing commands
948
1113
 
949
- JSON mode + alias resolution makes pipelines safe:
1114
+ \`--get\` removes most of the \`jq\` shims agents reach for. Capture in
1115
+ a script, then display the final result back to the user:
950
1116
 
951
1117
  \`\`\`
952
- ITER=$(ish iteration create --url https://example.com --json | jq -r .alias)
953
- TESTERS=$(ish study run --iteration "$ITER" --sample 5 --country SE \\
954
- --json | jq -r '.tester_aliases[]')
1118
+ # Capture bare values, no jq needed:
1119
+ ITER=$(ish iteration create --url https://example.com --get alias)
1120
+ TESTERS=$(ish study run --iteration "$ITER" --sample 5 --country SE --get tester_aliases)
955
1121
  for t in $TESTERS; do
956
1122
  ish study wait "$t" --timeout 600
957
1123
  done
958
- ish study results --json | jq .
1124
+
1125
+ # Display the final results to the user, even though we're in a script:
1126
+ ish study results --human
959
1127
  \`\`\`
1128
+
1129
+ When you genuinely need multiple fields in one parse pass, \`--json\` is
1130
+ still the right tool — \`--get\` is for single-value capture, not for
1131
+ reshaping output.
960
1132
  `;
961
1133
  const GUIDE_FIRST_STUDY = `# guide: your first study, end to end
962
1134
 
@@ -1098,7 +1270,9 @@ of scope: \`workspace\`, \`config\`, \`docs\`, \`init\`, \`login\`,
1098
1270
  ## Related
1099
1271
 
1100
1272
  - \`reference/aliases\` — the prefix scheme used by every entity.
1101
- - \`reference/json-mode\` — output contracts for piping \`ish status\`.
1273
+ - \`reference/json-mode\` — output modes (display vs capture vs chain),
1274
+ including \`--get workspace.alias\` to capture the active workspace
1275
+ without piping \`ish status --json\` through \`jq\`.
1102
1276
  `;
1103
1277
  const REFERENCE_BILLING_LIMITS = `# reference: billing tier limits
1104
1278
 
@@ -1272,8 +1446,8 @@ const PAGES = [
1272
1446
  },
1273
1447
  {
1274
1448
  slug: "reference/json-mode",
1275
- title: "reference: JSON output for agents",
1276
- description: "JSON, --fields, --verbose, exit codes, pipe behaviour.",
1449
+ title: "reference: output modes for agents (display, capture, chain)",
1450
+ description: "Display vs capture vs chain: --human, --get, --json, --fields, exit codes, pipe behavior.",
1277
1451
  body: REFERENCE_JSON_MODE,
1278
1452
  },
1279
1453
  {
@@ -9,6 +9,12 @@
9
9
  /** Set by withClient() based on global flags. */
10
10
  export declare function setVerbose(v: boolean): void;
11
11
  export declare function setFields(fields?: string[]): void;
12
+ /**
13
+ * Pattern Ω capture mode: when set, jsonOutput() returns the bare value at
14
+ * the dotted path instead of the full JSON. Cleared between command runs by
15
+ * each invocation of `applyGlobals()`.
16
+ */
17
+ export declare function setGetField(field?: string): void;
12
18
  /** Per-call output options for stable JSON contracts. */
13
19
  export interface OutputOptions {
14
20
  /**