@ishlabs/cli 0.24.1 → 0.25.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.
- package/dist/commands/ask.js +3 -3
- package/dist/commands/iteration.js +1 -1
- package/dist/commands/study-analyze.js +1 -1
- package/dist/commands/study-run.js +80 -12
- package/dist/commands/study.js +11 -7
- package/dist/lib/alias-store.js +1 -1
- package/dist/lib/api-client.d.ts +2 -0
- package/dist/lib/docs.js +57 -42
- package/dist/lib/local-sim/actions.d.ts +10 -2
- package/dist/lib/local-sim/actions.js +16 -11
- package/dist/lib/local-sim/adb.d.ts +103 -0
- package/dist/lib/local-sim/adb.js +352 -0
- package/dist/lib/local-sim/android.d.ts +111 -0
- package/dist/lib/local-sim/android.js +499 -0
- package/dist/lib/local-sim/apk-manifest.d.ts +22 -0
- package/dist/lib/local-sim/apk-manifest.js +210 -0
- package/dist/lib/local-sim/browser.d.ts +22 -0
- package/dist/lib/local-sim/browser.js +65 -0
- package/dist/lib/local-sim/coordinates.d.ts +69 -0
- package/dist/lib/local-sim/coordinates.js +59 -0
- package/dist/lib/local-sim/device.d.ts +143 -0
- package/dist/lib/local-sim/device.js +152 -0
- package/dist/lib/local-sim/ios.d.ts +168 -0
- package/dist/lib/local-sim/ios.js +546 -0
- package/dist/lib/local-sim/loop.d.ts +14 -2
- package/dist/lib/local-sim/loop.js +166 -73
- package/dist/lib/local-sim/native-a11y.d.ts +97 -0
- package/dist/lib/local-sim/native-a11y.js +384 -0
- package/dist/lib/local-sim/simctl.d.ts +85 -0
- package/dist/lib/local-sim/simctl.js +273 -0
- package/dist/lib/local-sim/types.d.ts +37 -2
- package/dist/lib/local-sim/upload.d.ts +1 -1
- package/dist/lib/local-sim/upload.js +9 -6
- package/dist/lib/output.js +58 -12
- package/dist/lib/skill-content.js +10 -9
- package/package.json +2 -1
package/dist/commands/ask.js
CHANGED
|
@@ -436,15 +436,15 @@ Picks come back with a \`pick_confidence\` (0..1) score per participant when
|
|
|
436
436
|
// error rather than a transient failure.
|
|
437
437
|
ask
|
|
438
438
|
.command("dispatch")
|
|
439
|
-
.description("Dispatch a draft ask —
|
|
439
|
+
.description("Dispatch a draft ask — draws credits and starts the round")
|
|
440
440
|
.argument("[id]", "Ask alias or UUID (defaults to active ask)")
|
|
441
441
|
.option("--ask <id>", "Ask ID; alternative to positional argument")
|
|
442
442
|
.option("--wait", "Wait until the first round completes (or errors)")
|
|
443
443
|
.option("--timeout <s>", "Wait timeout in seconds (default 300)")
|
|
444
444
|
.addHelpText("after", `
|
|
445
445
|
Use after \`ish ask create --no-dispatch\` to start a draft once the user has
|
|
446
|
-
reviewed it.
|
|
447
|
-
|
|
446
|
+
reviewed it. Dispatch draws credits as responses land, the same as a normal
|
|
447
|
+
create. This is the expected way to run an ask — go ahead and dispatch.
|
|
448
448
|
|
|
449
449
|
Examples:
|
|
450
450
|
# Dispatch the active draft and wait for results:
|
|
@@ -383,7 +383,7 @@ Concept pages: ish docs get-page concepts/iteration
|
|
|
383
383
|
// Media image
|
|
384
384
|
.option("--image-urls <urls>", "Comma-separated image URLs or local file paths — image modality")
|
|
385
385
|
// Shared media
|
|
386
|
-
.option("--title <title>", "Content title — media modalities")
|
|
386
|
+
.option("--title <title>", "Content title shown to participants (the leading headline they read) — media modalities")
|
|
387
387
|
.option("--mime-type <type>", "MIME type (e.g. video/mp4) — media modalities")
|
|
388
388
|
// Copy/caption
|
|
389
389
|
.option("--copy-text <text>", "Ad copy or social post caption (or @filepath) — ads & social posts")
|
|
@@ -81,7 +81,7 @@ export function attachStudyAnalyzeCommands(study) {
|
|
|
81
81
|
study
|
|
82
82
|
.command("analyze")
|
|
83
83
|
.description("Trigger an AI summary + key-insights analysis for a study. " +
|
|
84
|
-
"First analysis per study is
|
|
84
|
+
"First analysis per study is included; subsequent runs draw 10 credits.")
|
|
85
85
|
.argument("[id]", "Study ID (defaults to active study)")
|
|
86
86
|
.option("--workspace <id>", "Workspace ID; accepted for consistency (workspace is inferred from the study)")
|
|
87
87
|
.option("--wait", "Poll until the run reaches completed or failed")
|
|
@@ -33,8 +33,8 @@ function parseMaxInteractions(value) {
|
|
|
33
33
|
/**
|
|
34
34
|
* Default cap the CLI sends when neither `--max-interactions` nor the
|
|
35
35
|
* iteration carries its own value. Picked to match the frontend's
|
|
36
|
-
* conservative interactive launchers and to prevent runaway
|
|
37
|
-
* iteration runs against a broken or non-responsive surface — without a
|
|
36
|
+
* conservative interactive launchers and to prevent runaway credit draw when
|
|
37
|
+
* an iteration runs against a broken or non-responsive surface — without a
|
|
38
38
|
* cap, a stuck participant can rack up hundreds of steps before the SDK gives
|
|
39
39
|
* up.
|
|
40
40
|
*/
|
|
@@ -264,6 +264,34 @@ function readIterationDetails(details) {
|
|
|
264
264
|
...(typeof details.title === "string" && { title: details.title }),
|
|
265
265
|
};
|
|
266
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Normalize a platform string for matching. "web", "browser", and unset all
|
|
269
|
+
* mean the default browser path; "android"/"ios" are native. Lets `--platform
|
|
270
|
+
* web` match a "browser" iteration (and vice-versa) without false mismatches.
|
|
271
|
+
*/
|
|
272
|
+
function normalizePlatform(platform) {
|
|
273
|
+
const p = (platform ?? "").toLowerCase();
|
|
274
|
+
if (p === "" || p === "web" || p === "browser")
|
|
275
|
+
return "browser";
|
|
276
|
+
return p;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* The local platform the user explicitly requested via flags, before any
|
|
280
|
+
* iteration is picked: --platform, or inferred from --app's extension
|
|
281
|
+
* (.apk → android, .app → ios). Undefined when neither is set (no preference →
|
|
282
|
+
* don't filter iterations by platform). The iteration's stored platform is
|
|
283
|
+
* deliberately NOT consulted here — it's what we're selecting against.
|
|
284
|
+
*/
|
|
285
|
+
function requestedLocalPlatform(opts) {
|
|
286
|
+
if (opts.platform)
|
|
287
|
+
return opts.platform;
|
|
288
|
+
const app = opts.app?.toLowerCase();
|
|
289
|
+
if (app?.endsWith(".apk"))
|
|
290
|
+
return "android";
|
|
291
|
+
if (app?.endsWith(".app"))
|
|
292
|
+
return "ios";
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
267
295
|
export function attachStudyRunCommands(study) {
|
|
268
296
|
// --- Primary: `study run` ---
|
|
269
297
|
const studyRun = study
|
|
@@ -294,6 +322,8 @@ export function attachStudyRunCommands(study) {
|
|
|
294
322
|
.option("--devtools", "Open Chrome DevTools (local mode only)")
|
|
295
323
|
.option("--debug", "Enable detailed debug logging to stderr and ~/.ish/local-sim.log")
|
|
296
324
|
.option("--parallel <n>", "Run N participants in parallel (local mode only, default: all)")
|
|
325
|
+
.option("--platform <platform>", "Local target platform: 'web' (Playwright), 'android' (adb emulator), or 'ios' (simctl+idb simulator). Defaults to the iteration's platform.")
|
|
326
|
+
.option("--app <path>", "Native local mode: path to an .apk (android) / .app (ios) to install, or an installed package/bundle id to launch. The extension implies --platform.")
|
|
297
327
|
.addHelpText("after", `
|
|
298
328
|
Note: --workspace and --study are optional if you have set active context
|
|
299
329
|
via \`ish workspace use <alias>\` and \`ish study use <alias>\`.
|
|
@@ -326,7 +356,7 @@ Examples:
|
|
|
326
356
|
$ ish study run --config c-c3c
|
|
327
357
|
|
|
328
358
|
# Cap interactions per participant (default 20 — pass higher to allow deeper
|
|
329
|
-
# exploration, lower to cap
|
|
359
|
+
# exploration, lower to cap credit draw on a known-broken surface):
|
|
330
360
|
$ ish study run --max-interactions 30
|
|
331
361
|
|
|
332
362
|
# Block until all simulations finish (or timeout):
|
|
@@ -412,8 +442,13 @@ Examples:
|
|
|
412
442
|
if (!study.assignments || study.assignments.length === 0) {
|
|
413
443
|
throw new Error("Study has no assignments. Add tasks with --assignments when creating the study, or use `ish study generate`.");
|
|
414
444
|
}
|
|
415
|
-
// Step 1: Pick iteration (explicit --iteration, or latest on study)
|
|
445
|
+
// Step 1: Pick iteration (explicit --iteration, or latest on study).
|
|
446
|
+
// When --platform / --app requests a local platform, select the
|
|
447
|
+
// iteration whose details.platform matches — otherwise a multi-platform
|
|
448
|
+
// study (e.g. an android AND an ios iteration) would silently run the
|
|
449
|
+
// latest, which may be the wrong platform.
|
|
416
450
|
const iterations = study.iterations || [];
|
|
451
|
+
const wantPlatform = opts.local ? requestedLocalPlatform(opts) : undefined;
|
|
417
452
|
let iteration;
|
|
418
453
|
if (opts.iteration) {
|
|
419
454
|
const wantedId = resolveId(opts.iteration);
|
|
@@ -421,6 +456,25 @@ Examples:
|
|
|
421
456
|
if (!iteration) {
|
|
422
457
|
throw new Error(`Iteration ${opts.iteration} not found on this study.`);
|
|
423
458
|
}
|
|
459
|
+
// An explicit --iteration whose platform contradicts --platform is a
|
|
460
|
+
// footgun (you'd drive the wrong device); refuse rather than guess.
|
|
461
|
+
if (wantPlatform) {
|
|
462
|
+
const itPlatform = readIterationDetails(iteration.details).platform;
|
|
463
|
+
if (normalizePlatform(itPlatform) !== normalizePlatform(wantPlatform)) {
|
|
464
|
+
throw new Error(`--platform ${wantPlatform} but iteration ${opts.iteration} is platform ` +
|
|
465
|
+
`'${itPlatform ?? "browser"}'. Pass the matching iteration or drop --platform.`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
else if (wantPlatform) {
|
|
470
|
+
// Latest iteration whose platform matches the requested one.
|
|
471
|
+
const matching = iterations.filter((it) => normalizePlatform(readIterationDetails(it.details).platform) === normalizePlatform(wantPlatform));
|
|
472
|
+
if (matching.length === 0) {
|
|
473
|
+
const available = [...new Set(iterations.map((it) => normalizePlatform(readIterationDetails(it.details).platform)))].join(", ") || "none";
|
|
474
|
+
throw new Error(`No ${wantPlatform} iteration on this study (platforms present: ${available}). ` +
|
|
475
|
+
`Create one with \`ish iteration create --study ${resolvedStudy} ...\`, or pass --iteration.`);
|
|
476
|
+
}
|
|
477
|
+
iteration = matching[matching.length - 1];
|
|
424
478
|
}
|
|
425
479
|
else if (iterations.length > 0) {
|
|
426
480
|
iteration = iterations[iterations.length - 1];
|
|
@@ -691,6 +745,21 @@ Examples:
|
|
|
691
745
|
// by reusing the iteration's existing Conversation rows or by
|
|
692
746
|
// calling pair-batch.
|
|
693
747
|
let pairConversationIds = [];
|
|
748
|
+
// Resolve the local target platform ONCE so participant-create and the
|
|
749
|
+
// local-sim dispatch agree. Precedence: --platform flag > --app
|
|
750
|
+
// extension (.apk → android, .app → ios) > iteration's stored platform
|
|
751
|
+
// > browser. Used to set participant.platform (so the backend's native
|
|
752
|
+
// trigger is driven primarily by the participant, not just the
|
|
753
|
+
// empty-tree fallback) and to pick the device in the local loop.
|
|
754
|
+
const platformFromApp = opts.app?.toLowerCase().endsWith(".apk")
|
|
755
|
+
? "android"
|
|
756
|
+
: opts.app?.toLowerCase().endsWith(".app")
|
|
757
|
+
? "ios"
|
|
758
|
+
: undefined;
|
|
759
|
+
const resolvedPlatform = opts.platform
|
|
760
|
+
?? platformFromApp
|
|
761
|
+
?? detailsView.platform
|
|
762
|
+
?? "browser";
|
|
694
763
|
if (isPair && pairConfig) {
|
|
695
764
|
// Pair-mode flow mirrors the MCP (`ish-mcp` `_run_pair_mode`):
|
|
696
765
|
// 1. If the iteration already carries `conversations[]` from a
|
|
@@ -782,7 +851,7 @@ Examples:
|
|
|
782
851
|
participant_type: "ai",
|
|
783
852
|
status: "draft",
|
|
784
853
|
...(opts.language && { language: opts.language }),
|
|
785
|
-
...(!isMedia && !isChat && { platform:
|
|
854
|
+
...(!isMedia && !isChat && { platform: resolvedPlatform }),
|
|
786
855
|
}));
|
|
787
856
|
log(`Creating ${participantInputs.length} participant${participantInputs.length > 1 ? "s" : ""}...`);
|
|
788
857
|
const batchResult = await client.post(`/iterations/${iterationId}/participants/batch`, { participants: participantInputs }, { timeout: dispatchTimeoutMs });
|
|
@@ -814,6 +883,8 @@ Examples:
|
|
|
814
883
|
devtools: opts.devtools,
|
|
815
884
|
debug: opts.debug,
|
|
816
885
|
parallel: opts.parallel ? parseInt(opts.parallel, 10) : undefined,
|
|
886
|
+
platform: resolvedPlatform,
|
|
887
|
+
...(opts.app && { appPath: opts.app }),
|
|
817
888
|
quiet: globals.quiet,
|
|
818
889
|
json: globals.json,
|
|
819
890
|
});
|
|
@@ -1042,14 +1113,11 @@ Examples:
|
|
|
1042
1113
|
}, true);
|
|
1043
1114
|
}
|
|
1044
1115
|
else {
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
log(` ${personName.padEnd(24)} QUEUED`);
|
|
1049
|
-
}
|
|
1116
|
+
const studyAlias = tagAlias(ALIAS_PREFIX.study, resolvedStudy);
|
|
1117
|
+
const n = createdParticipants.length;
|
|
1118
|
+
log(`Dispatched ${n} participant${n > 1 ? "s" : ""} — run \`ish study results ${studyAlias}\` for results (or \`ish study poll --study ${studyAlias}\` / --wait to track progress).`);
|
|
1050
1119
|
const url = getWebUrl(globals, `/${resolvedWorkspace}/${resolvedStudy}/timeline`);
|
|
1051
|
-
log(
|
|
1052
|
-
log(`Run \`ish study poll --study ${resolvedStudy}\` (or --wait next time) to check progress.`);
|
|
1120
|
+
log(` ${terminalLink(url, "Open in browser ↗")}`);
|
|
1053
1121
|
}
|
|
1054
1122
|
return;
|
|
1055
1123
|
}
|
package/dist/commands/study.js
CHANGED
|
@@ -113,7 +113,7 @@ Concept pages: ish docs get-page concepts/study
|
|
|
113
113
|
.requiredOption("--name <name>", "Study name")
|
|
114
114
|
.option("--description <description>", "Study description")
|
|
115
115
|
.option("--modality <modality>", "Study modality (interactive, video, audio, text, image, document, chat)")
|
|
116
|
-
.option("--content-type <type>", "Content type (per-modality enum — see 'Content types by modality' below). Not used for interactive / chat.")
|
|
116
|
+
.option("--content-type <type>", "Content type (per-modality enum — see 'Content types by modality' below). Changes how --title is presented to participants (e.g. content-type email renders --title as the Subject: line). Not used for interactive / chat.")
|
|
117
117
|
.option("--assignment <name:instructions>", "Assignment as 'Name:Instructions' (repeatable)", collectRepeatable, [])
|
|
118
118
|
.option("--assignments-file <path>", "JSON file with assignments array")
|
|
119
119
|
.option("--assignments <json>", "Inline JSON array of assignments (escape hatch)")
|
|
@@ -124,7 +124,7 @@ Concept pages: ish docs get-page concepts/study
|
|
|
124
124
|
.option("--screen-format <format>", "Screen format for interactive iterations: desktop (default) or mobile_portrait (hyphen/underscore variants accepted)")
|
|
125
125
|
.option("--content-url <url>", "Public URL of the media file. Creates iteration A inline (video, audio, document modalities). For local files, use the 2-step `iteration create` flow.")
|
|
126
126
|
.option("--image-urls <urls>", "Comma-separated public image URLs. Creates iteration A inline (image modality). For local files, use the 2-step `iteration create` flow.")
|
|
127
|
-
.option("--title <title>", "
|
|
127
|
+
.option("--title <title>", "Participant-facing content title — the headline participants read before the body (text + media modalities — image, video, audio, document; optional). With --content-type email it becomes the email Subject: line. Not an internal label. Not used for interactive / chat.")
|
|
128
128
|
.option("--segmentation-json <json>", "Segmentation JSON for the inline iteration A — time_based {intervals_seconds, labels?}, section_based {sections[{name,label,...}]}, or page_based {} (text + media). section_based sections are SEMANTIC: group related paragraphs into a few coherent sections (a long article is usually 3-6 sections, not one per paragraph). Lets one `study create` build a complete segmented iteration — no separate `iteration create` needed.")
|
|
129
129
|
.option("--content-config-json <json>", "Content-config JSON for the inline iteration A (early_termination, selected_segment_indices) — text + media.")
|
|
130
130
|
.option("--content-html <html>", "HTML version of the text, or @filepath — text modality (email rendering)")
|
|
@@ -239,6 +239,9 @@ Examples:
|
|
|
239
239
|
|
|
240
240
|
Content types by modality (source: VALID_CONTENT_TYPES in src/lib/types.ts; interactive + chat omitted — they don't take --content-type):
|
|
241
241
|
${describeContentTypes()}
|
|
242
|
+
The content type also shapes how --title is rendered to participants: with
|
|
243
|
+
content-type email, --title becomes the email Subject: line; otherwise it
|
|
244
|
+
reads as the leading headline of the content.
|
|
242
245
|
|
|
243
246
|
Tips:
|
|
244
247
|
Use \`--get <path>\` to capture a single value (e.g. \`--get id\`),
|
|
@@ -310,11 +313,12 @@ Next: configure a run with \`ish iteration create --study <id>\`,
|
|
|
310
313
|
}
|
|
311
314
|
normalizedScreenFormat = normalized;
|
|
312
315
|
}
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
316
|
+
// --title is participant-facing content: the backend renders it as a
|
|
317
|
+
// leading headline participants read before the body (and as the email
|
|
318
|
+
// Subject: line when content-type is email). It only exists on text +
|
|
319
|
+
// media modalities (see `buildIterationDetails` in iteration.ts), so
|
|
320
|
+
// reject it on shapes that have no title field — interactive (URL only)
|
|
321
|
+
// and chat (endpoint config carries its own configuration).
|
|
318
322
|
if (opts.title !== undefined
|
|
319
323
|
&& opts.contentText === undefined
|
|
320
324
|
&& opts.contentUrl === undefined
|
package/dist/lib/alias-store.js
CHANGED
|
@@ -129,7 +129,7 @@ const HYDRATE_HINT = {
|
|
|
129
129
|
// alias map at ~/.ish/aliases.json carries any sources the CLI has
|
|
130
130
|
// touched in this session.
|
|
131
131
|
ps: "ish source upload <file> # or `cat ~/.ish/aliases.json | grep ^ps-` to recover prior aliases",
|
|
132
|
-
pt: "ish participant
|
|
132
|
+
pt: "ish study participant <participant-id>",
|
|
133
133
|
c: "ish config list",
|
|
134
134
|
a: "ish ask list",
|
|
135
135
|
r: "ish ask get <ask-id>",
|
package/dist/lib/api-client.d.ts
CHANGED
package/dist/lib/docs.js
CHANGED
|
@@ -339,7 +339,7 @@ pick was wrong.
|
|
|
339
339
|
- \`guides/slicing-results\` — filter / project \`study results\` by frame,
|
|
340
340
|
segment, turn, sentiment, assignment, step.
|
|
341
341
|
- \`reference/billing-limits\` — \`maxStudiesPerProduct\` cap on study creation.
|
|
342
|
-
- \`reference/credits\` — per-run credit
|
|
342
|
+
- \`reference/credits\` — per-run credit draw & how to preview before dispatch.
|
|
343
343
|
`;
|
|
344
344
|
const CONCEPT_ITERATION = `# concept: iteration
|
|
345
345
|
|
|
@@ -806,7 +806,7 @@ Treat this as actionable, not transient — re-running won't change anything.
|
|
|
806
806
|
- \`concepts/run-verbs\` — how \`ish study run\` selects the iteration.
|
|
807
807
|
- \`concepts/people\` — how participants are picked for a run.
|
|
808
808
|
- \`reference/billing-limits\` — \`maxIterationsPerStudy\` cap on iteration creation.
|
|
809
|
-
- \`reference/credits\` — per-iteration-run credit
|
|
809
|
+
- \`reference/credits\` — per-iteration-run credit draw & preview shape (\`pair_preview.credit_estimate\` for participant-pair, top-level \`credit_estimate\` otherwise).
|
|
810
810
|
`;
|
|
811
811
|
const CONCEPT_ASSIGNMENT = `# concept: assignment
|
|
812
812
|
|
|
@@ -987,7 +987,7 @@ ish ask results a-6ec --json | jq '.rounds[0].aggregates'
|
|
|
987
987
|
|
|
988
988
|
Asks carry a top-level \`status\`:
|
|
989
989
|
|
|
990
|
-
- \`draft\` — created but not dispatched yet. No credits
|
|
990
|
+
- \`draft\` — created but not dispatched yet. No credits drawn. Created
|
|
991
991
|
by \`ish ask create --no-dispatch\`.
|
|
992
992
|
- \`running\` — dispatched; the round is executing or queued.
|
|
993
993
|
- \`completed\` — round 1 (or the most recent round) finished.
|
|
@@ -1000,10 +1000,10 @@ intact — no \`--verbose\` needed to see it.
|
|
|
1000
1000
|
## Stage-then-dispatch (draft asks)
|
|
1001
1001
|
|
|
1002
1002
|
When you want a human to review the people and prompt **before** any
|
|
1003
|
-
credits are
|
|
1003
|
+
credits are drawn, separate creation from dispatch:
|
|
1004
1004
|
|
|
1005
1005
|
\`\`\`
|
|
1006
|
-
# 1. Stage — materializes participants, no worker enqueue, no
|
|
1006
|
+
# 1. Stage — materializes participants, no worker enqueue, no credits drawn yet
|
|
1007
1007
|
ish ask create --workspace w-6ec --name "tagline AB" \\
|
|
1008
1008
|
--prompt "Which sounds better?" \\
|
|
1009
1009
|
--variant text:"Short and punchy." \\
|
|
@@ -1013,18 +1013,20 @@ ish ask create --workspace w-6ec --name "tagline AB" \\
|
|
|
1013
1013
|
|
|
1014
1014
|
# Returns an ask with status="draft". Hand the alias back to the user.
|
|
1015
1015
|
|
|
1016
|
-
# 2. Dispatch — flips DRAFT → RUNNING and enqueues the round (
|
|
1016
|
+
# 2. Dispatch — flips DRAFT → RUNNING and enqueues the round (draws credits)
|
|
1017
1017
|
ish ask dispatch a-6ec --wait
|
|
1018
1018
|
\`\`\`
|
|
1019
1019
|
|
|
1020
1020
|
\`--no-dispatch\` requires people flags (participants are still materialized
|
|
1021
|
-
at create time — only the worker enqueue and
|
|
1022
|
-
is incompatible with \`--wait\` since there is nothing to
|
|
1021
|
+
at create time — only the worker enqueue and the credit draw are
|
|
1022
|
+
deferred). It is incompatible with \`--wait\` since there is nothing to
|
|
1023
|
+
wait for.
|
|
1023
1024
|
|
|
1024
1025
|
\`ish ask dispatch\` is idempotent on the server: a non-DRAFT ask returns
|
|
1025
1026
|
HTTP 409 (\`already dispatched\`) which the CLI maps to a usage error, so
|
|
1026
1027
|
re-running the command is safe. The user who calls \`dispatch\` is the
|
|
1027
|
-
|
|
1028
|
+
principal whose credits are drawn — keep that in mind for shared
|
|
1029
|
+
workspaces.
|
|
1028
1030
|
|
|
1029
1031
|
## Reading the verdict
|
|
1030
1032
|
|
|
@@ -1170,7 +1172,7 @@ deleted ask was the active one.
|
|
|
1170
1172
|
- \`concepts/round\` — what a round is and how it executes.
|
|
1171
1173
|
- \`concepts/people\` — how participants are chosen at ask creation.
|
|
1172
1174
|
- \`concepts/run-verbs\` — \`ish ask run\` vs \`ish study run\`.
|
|
1173
|
-
- \`reference/credits\` — ask rounds
|
|
1175
|
+
- \`reference/credits\` — ask rounds draw **one credit per successful participant per round**, regardless of how many \`questions\` were included. The backend's asks worker draws \`amount=succeeded\` once per round dispatch; questions and round-summary synthesis don't draw separately. A 3-person panel with 2 follow-up questions draws \`3\` credits when all complete, the same as a no-questions run. Failed participant responses (pre-flight errors, refusals) draw nothing.
|
|
1174
1176
|
`;
|
|
1175
1177
|
const CONCEPT_ROUND = `# concept: round
|
|
1176
1178
|
|
|
@@ -1197,7 +1199,7 @@ ish ask results a-6ec --round 1
|
|
|
1197
1199
|
|
|
1198
1200
|
Appending questions to a completed round preserves prior data — variant
|
|
1199
1201
|
comments, picks, ratings, and earlier-question answers all stay. Only
|
|
1200
|
-
the new question(s) get dispatched to the existing participants.
|
|
1202
|
+
the new question(s) get dispatched to the existing participants. Usage is
|
|
1201
1203
|
roughly N phase-2 LLM calls instead of 2N (no phase-1 re-run). Errored
|
|
1202
1204
|
responses are skipped entirely; completed responses flip to PENDING and
|
|
1203
1205
|
re-finalize after the new question is answered.
|
|
@@ -1902,16 +1904,16 @@ load-bearing return value — same exception \`study run\` makes.
|
|
|
1902
1904
|
| Source not terminal (RUNNING / QUEUED) | \`Participant is still running — cancel it first or wait for completion.\` | 2 |
|
|
1903
1905
|
| Source participant not found | \`Participant not found: <id>\` | 4 |
|
|
1904
1906
|
| \`additional_steps\` out of range | Client-side parser rejects before the network call | 2 |
|
|
1905
|
-
| Insufficient credits | Bubbles the server message; retry only after
|
|
1907
|
+
| Insufficient credits | Bubbles the server message; retry only after the balance is replenished | 5 |
|
|
1906
1908
|
| Wait timed out (\`--wait\` only) | \`WaitTimeoutError\` envelope with current status under \`progress.rows[0]\` — the run keeps going server-side; resume with \`study wait <new-participant>\` | 5 |
|
|
1907
1909
|
|
|
1908
|
-
##
|
|
1910
|
+
## Credit model
|
|
1909
1911
|
|
|
1910
|
-
\`extend\`
|
|
1912
|
+
\`extend\` draws credits for **only \`additional_steps\`**, not for
|
|
1911
1913
|
the source's original \`max_interactions\` cap. The formula is the same
|
|
1912
1914
|
as \`study run\` for interactive runs: \`max(1, round(N / 10))\` per
|
|
1913
|
-
participant. So \`--add-steps 10\`
|
|
1914
|
-
|
|
1915
|
+
participant. So \`--add-steps 10\` draws **1 credit**; \`--add-steps 50\`
|
|
1916
|
+
draws **5 credits**. See \`reference/credits\` for the full table.
|
|
1915
1917
|
|
|
1916
1918
|
## Worked example — push past the step cap
|
|
1917
1919
|
|
|
@@ -1945,7 +1947,7 @@ ish study extend pt-072 \\
|
|
|
1945
1947
|
|
|
1946
1948
|
- \`concepts/run-verbs\` — the top-level decision rule (\`study run\` vs
|
|
1947
1949
|
\`ask run\`); extend is a lifecycle verb downstream of either.
|
|
1948
|
-
- \`reference/credits\` — per-modality
|
|
1950
|
+
- \`reference/credits\` — per-modality credit-draw formulas. \`extend\` follows
|
|
1949
1951
|
the interactive formula scaled to \`additional_steps\`.
|
|
1950
1952
|
- \`reference/aliases\` — the \`t-…\` prefix and how aliases resolve.
|
|
1951
1953
|
- \`reference/json-mode\` — capture-mode (\`--get participant_alias\`) for
|
|
@@ -2366,7 +2368,7 @@ The CLI guarantees these contracts so agents can chain safely:
|
|
|
2366
2368
|
\`study update <id> --modality text\` before adding iterations.
|
|
2367
2369
|
- **\`ask add-questions\` is additive by default.** Appending questions
|
|
2368
2370
|
preserves variant comments / picks / ratings / prior-question
|
|
2369
|
-
answers; only the new question(s) get dispatched.
|
|
2371
|
+
answers; only the new question(s) get dispatched. Usage: roughly N
|
|
2370
2372
|
phase-2 LLM calls instead of 2N. Pass \`--redispatch-all\` for the
|
|
2371
2373
|
legacy reset behavior when you want fresh first impressions.
|
|
2372
2374
|
- **\`ask results --json\` includes \`cross_round_summary\` for 2+
|
|
@@ -2990,49 +2992,62 @@ of scope: \`workspace\`, \`config\`, \`docs\`, \`init\`, \`login\`,
|
|
|
2990
2992
|
including \`--get workspace.alias\` to capture the active workspace
|
|
2991
2993
|
without piping \`ish status --json\` through \`jq\`.
|
|
2992
2994
|
`;
|
|
2993
|
-
const REFERENCE_CREDITS = `# reference: credits &
|
|
2995
|
+
const REFERENCE_CREDITS = `# reference: credits & usage preview
|
|
2994
2996
|
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2997
|
+
Credits are ish's **usage allowance** — a resource that runs draw, not
|
|
2998
|
+
money you pay per call. Each run (study, ask, insight) **draws credits**
|
|
2999
|
+
from the account's balance. Drawing credits to run studies and asks is
|
|
3000
|
+
the normal, expected way to use ish — an agent should run them without
|
|
3001
|
+
hesitation, the same as making any other tool call. The job here is just
|
|
3002
|
+
to be *aware* of the balance, not to ration it.
|
|
2999
3003
|
|
|
3000
|
-
|
|
3004
|
+
**Where credits come from:**
|
|
3005
|
+
- **Paid plans (Starter / Pro / Enterprise)** get a **monthly credit
|
|
3006
|
+
allowance** that refills each billing cycle.
|
|
3007
|
+
- **The free tier** gets a **one-time signup grant** — it is *not*
|
|
3008
|
+
refilled monthly. Once it's drawn down, the user adds more or upgrades.
|
|
3009
|
+
|
|
3010
|
+
The CLI surfaces a usage upper bound *before* you dispatch so you can see
|
|
3011
|
+
how much a run will draw. The backend is the authoritative source — its
|
|
3012
|
+
rejection envelope on \`insufficient_credits\` carries the live
|
|
3013
|
+
required/available pair.
|
|
3014
|
+
|
|
3015
|
+
## How usage is shaped
|
|
3001
3016
|
|
|
3002
3017
|
The formula has the same shape across modalities — \`max(1, round(N / 10))\`
|
|
3003
3018
|
per principal — but the inputs differ. **Treat the rates below as the
|
|
3004
3019
|
current calibration**; they will evolve as we differentiate per-modality
|
|
3005
|
-
compute
|
|
3020
|
+
compute. Agents should:
|
|
3006
3021
|
|
|
3007
|
-
- For prospective
|
|
3022
|
+
- For prospective usage preview: read \`credit_estimate\` from \`study run\`'s
|
|
3008
3023
|
JSON envelope (top-level for solo/media runs; under \`pair_preview\` for
|
|
3009
3024
|
participant-pair chat).
|
|
3010
|
-
- For hard
|
|
3025
|
+
- For hard balance checks: catch the backend's \`insufficient_credits\`
|
|
3011
3026
|
rejection (HTTP 402; envelope shape below) and react to
|
|
3012
3027
|
\`required\` / \`available\`.
|
|
3013
3028
|
|
|
3014
|
-
| Surface | Per-principal
|
|
3029
|
+
| Surface | Per-principal draw | Total formula | Example |
|
|
3015
3030
|
|---------------------|---------------------------------|--------------------------------------------------|--------------------------------------|
|
|
3016
3031
|
| Interactive (URL) | \`max(1, round(steps/10))\` | \`participants × per-participant\` | 10 participants × 30 steps → 30 credits |
|
|
3017
3032
|
| Text/image/video/audio/document | same | same | 5 participants × 20 steps → 10 credits |
|
|
3018
3033
|
| Chat (external chatbot, solo) | \`max(1, round(turns/10))\` | \`participants × per-participant\` | 5 participants × 12 turns → 10 credits |
|
|
3019
3034
|
| Chat (participant pair) | \`max(1, round(turns/10))\` × 2 | \`conv × per-side × 2\` | 3 conv × 14 turns → 6 credits |
|
|
3020
3035
|
| Ask round | 1 / successful response | \`successful_participants\` | 50 responses → 50 credits |
|
|
3021
|
-
| Study insights | first
|
|
3036
|
+
| Study insights | first included, then **10 flat** | n/a | 2nd analysis → 10 credits |
|
|
3022
3037
|
|
|
3023
3038
|
All numbers are **upper bounds**. Early termination, refusals, or
|
|
3024
|
-
backend trimming can reduce actual
|
|
3039
|
+
backend trimming can reduce the actual draw.
|
|
3025
3040
|
|
|
3026
|
-
## Capping interactive/media
|
|
3041
|
+
## Capping interactive/media usage (\`--max-interactions\`)
|
|
3027
3042
|
|
|
3028
3043
|
\`ish study run\` always sends \`max_interactions\` to the backend for
|
|
3029
3044
|
interactive and media runs. Precedence: \`--max-interactions <n>\` flag
|
|
3030
3045
|
> the iteration's stored \`details.max_interactions\` > **CLI default
|
|
3031
|
-
of 20**. The default exists to prevent runaway
|
|
3032
|
-
gets stuck on a broken or non-responsive surface — without a
|
|
3033
|
-
stuck participant can rack up 100+ steps before the SDK gives
|
|
3034
|
-
\`--max-interactions\` to override (e.g. \`--max-interactions 50\`
|
|
3035
|
-
deeper exploration, \`--max-interactions 5\` for a
|
|
3046
|
+
of 20**. The default exists to prevent runaway credit draw when a
|
|
3047
|
+
participant gets stuck on a broken or non-responsive surface — without a
|
|
3048
|
+
cap, one stuck participant can rack up 100+ steps before the SDK gives
|
|
3049
|
+
up. Pass \`--max-interactions\` to override (e.g. \`--max-interactions 50\`
|
|
3050
|
+
for deeper exploration, \`--max-interactions 5\` for a quick smoke test).
|
|
3036
3051
|
The confirmation block shows the resolved value and where it came
|
|
3037
3052
|
from (flag / iteration / CLI default). The JSON envelope's
|
|
3038
3053
|
\`credit_estimate.breakdown\` reflects the dispatched value.
|
|
@@ -3121,7 +3136,7 @@ HTTP 402. The CLI surfaces it as a structured error envelope:
|
|
|
3121
3136
|
\`\`\`
|
|
3122
3137
|
|
|
3123
3138
|
Exit code \`1\` (non-retryable). Don't poll — the user has to upgrade or
|
|
3124
|
-
free credits before re-dispatch.
|
|
3139
|
+
free up credits before re-dispatch.
|
|
3125
3140
|
|
|
3126
3141
|
## Agent recipe
|
|
3127
3142
|
|
|
@@ -3135,7 +3150,7 @@ free credits before re-dispatch.
|
|
|
3135
3150
|
|
|
3136
3151
|
## Caveats
|
|
3137
3152
|
|
|
3138
|
-
- The CLI's preview uses the **same formula** the backend
|
|
3153
|
+
- The CLI's preview uses the **same formula** the backend draws against,
|
|
3139
3154
|
but does **not** make a network preflight call — it's pure math
|
|
3140
3155
|
client-side. If the backend formula changes mid-version, the preview
|
|
3141
3156
|
will drift until the CLI is updated. The \`insufficient_credits\`
|
|
@@ -3238,9 +3253,9 @@ upgrade or delete an existing resource to free up headroom.
|
|
|
3238
3253
|
|
|
3239
3254
|
## Related
|
|
3240
3255
|
|
|
3241
|
-
- \`reference/credits\` — per-run credit
|
|
3256
|
+
- \`reference/credits\` — per-run credit draw & preview (separate from
|
|
3242
3257
|
these entity caps; this page is about *how many things you can have*,
|
|
3243
|
-
that page is about *how
|
|
3258
|
+
that page is about *how many credits each run draws*).
|
|
3244
3259
|
- \`concepts/workspace\` — \`maxProducts\` is per-account.
|
|
3245
3260
|
- \`concepts/study\` — \`maxStudiesPerProduct\` gates study creation.
|
|
3246
3261
|
- \`concepts/iteration\` — \`maxIterationsPerStudy\` gates iteration creation.
|
|
@@ -4442,8 +4457,8 @@ const PAGES = [
|
|
|
4442
4457
|
},
|
|
4443
4458
|
{
|
|
4444
4459
|
slug: "reference/credits",
|
|
4445
|
-
title: "reference: credits &
|
|
4446
|
-
description: "
|
|
4460
|
+
title: "reference: credits & usage preview",
|
|
4461
|
+
description: "Credits as a usage allowance (paid plans refill monthly; free tier is a one-time signup grant), per-modality credit draw formulas, where the CLI surfaces usage estimates (Scale line, pair_preview.credit_estimate, top-level credit_estimate), tier allotments, insufficient_credits error shape.",
|
|
4447
4462
|
body: REFERENCE_CREDITS,
|
|
4448
4463
|
},
|
|
4449
4464
|
{
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Action executor — resolves elements and executes Playwright actions.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Element resolution strategy (browser only):
|
|
5
5
|
* 1. CDP node resolution (using node_id from tree data)
|
|
6
6
|
* 2. Playwright locator fallback (using element_name + element_type)
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
|
+
* Native (Android) targets are vision-located by the backend and tapped via
|
|
9
|
+
* normalized coordinates in AndroidDevice.executeAction — that coordinate path
|
|
10
|
+
* lives there, not here.
|
|
8
11
|
*/
|
|
9
12
|
import type { Page } from "playwright-core";
|
|
10
13
|
import type { LocalStepAction, ActionResult, ContextValue, TreeData } from "./types.js";
|
|
@@ -13,6 +16,11 @@ import type { TabManager } from "./tabs.js";
|
|
|
13
16
|
* Execute a single action on the page.
|
|
14
17
|
*/
|
|
15
18
|
export declare function executeAction(page: Page, action: LocalStepAction, treeData: TreeData, contextValues: ContextValue[], tabs?: TabManager): Promise<ActionResult>;
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the actual text to type from an action, handling var/secret value types.
|
|
21
|
+
* Exported so the native (Android) executor can resolve values the same way.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveTextValue(action: LocalStepAction, contextValues: ContextValue[]): string;
|
|
16
24
|
/**
|
|
17
25
|
* Compare two base64 screenshots to detect visible change.
|
|
18
26
|
*/
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Action executor — resolves elements and executes Playwright actions.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Element resolution strategy (browser only):
|
|
5
5
|
* 1. CDP node resolution (using node_id from tree data)
|
|
6
6
|
* 2. Playwright locator fallback (using element_name + element_type)
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
|
+
* Native (Android) targets are vision-located by the backend and tapped via
|
|
9
|
+
* normalized coordinates in AndroidDevice.executeAction — that coordinate path
|
|
10
|
+
* lives there, not here.
|
|
8
11
|
*/
|
|
9
12
|
import { resolveNodeToBoundingBox } from "./browser.js";
|
|
10
13
|
import { isDebugEnabled } from "./debug.js";
|
|
@@ -78,7 +81,7 @@ export async function executeAction(page, action, treeData, contextValues, tabs)
|
|
|
78
81
|
coordinates = await executeTextInput(page, action, treeData, contextValues);
|
|
79
82
|
break;
|
|
80
83
|
case "scroll":
|
|
81
|
-
await executeScroll(page, action
|
|
84
|
+
await executeScroll(page, action);
|
|
82
85
|
break;
|
|
83
86
|
case "swipe":
|
|
84
87
|
case "pull_to_refresh":
|
|
@@ -87,9 +90,6 @@ export async function executeAction(page, action, treeData, contextValues, tabs)
|
|
|
87
90
|
case "wait":
|
|
88
91
|
await page.waitForTimeout(action.duration_ms ?? 1000);
|
|
89
92
|
break;
|
|
90
|
-
case "navigate_back":
|
|
91
|
-
await page.goBack({ timeout: 10_000 }).catch(() => { });
|
|
92
|
-
break;
|
|
93
93
|
case "long_press":
|
|
94
94
|
coordinates = await executeLongPress(page, action, treeData);
|
|
95
95
|
break;
|
|
@@ -162,7 +162,7 @@ async function resolveElement(page, action, treeData) {
|
|
|
162
162
|
/**
|
|
163
163
|
* Resolve to a Playwright Locator (for fill/type operations that need a Locator).
|
|
164
164
|
*/
|
|
165
|
-
async function resolveLocator(page, action
|
|
165
|
+
async function resolveLocator(page, action) {
|
|
166
166
|
return findElement(page, action);
|
|
167
167
|
}
|
|
168
168
|
/**
|
|
@@ -237,7 +237,7 @@ async function executeTextInput(page, action, treeData, contextValues) {
|
|
|
237
237
|
// Resolve the actual text to type
|
|
238
238
|
const text = resolveTextValue(action, contextValues);
|
|
239
239
|
// Try to get a Playwright locator for fill operations
|
|
240
|
-
const locator = await resolveLocator(page, action
|
|
240
|
+
const locator = await resolveLocator(page, action);
|
|
241
241
|
if (locator) {
|
|
242
242
|
if (action.mode === "click_type") {
|
|
243
243
|
await locator.click({ timeout: 5000 });
|
|
@@ -273,7 +273,7 @@ async function executeTextInput(page, action, treeData, contextValues) {
|
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
|
-
async function executeScroll(page, action
|
|
276
|
+
async function executeScroll(page, action) {
|
|
277
277
|
const viewport = page.viewportSize() ?? { width: 1440, height: 900 };
|
|
278
278
|
const amountMap = {
|
|
279
279
|
small: 0.5, medium: 0.8, large: 1.5, extra_large: 3.0,
|
|
@@ -378,8 +378,9 @@ async function executeKeyboardShortcut(page, action) {
|
|
|
378
378
|
// --- Helpers ---
|
|
379
379
|
/**
|
|
380
380
|
* Resolve the actual text to type from an action, handling var/secret value types.
|
|
381
|
+
* Exported so the native (Android) executor can resolve values the same way.
|
|
381
382
|
*/
|
|
382
|
-
function resolveTextValue(action, contextValues) {
|
|
383
|
+
export function resolveTextValue(action, contextValues) {
|
|
383
384
|
if (action.value_type === "var" || action.value_type === "secret") {
|
|
384
385
|
const cv = contextValues.find(v => v.name === action.value);
|
|
385
386
|
if (cv?.value)
|
|
@@ -433,7 +434,11 @@ export function describeAction(action) {
|
|
|
433
434
|
case "double_tap":
|
|
434
435
|
return `double_tap on '${element}'${modSuffix}`;
|
|
435
436
|
case "drag":
|
|
436
|
-
return
|
|
437
|
+
return action.drag
|
|
438
|
+
? `drag '${element}' (${action.drag.startX},${action.drag.startY}→${action.drag.endX},${action.drag.endY})`
|
|
439
|
+
: `drag '${element}'`;
|
|
440
|
+
case "rotate_device":
|
|
441
|
+
return `rotate_device ${action.orientation ?? "?"}`;
|
|
437
442
|
case "think":
|
|
438
443
|
return `think: "${(action.thoughts ?? "").slice(0, 50)}"`;
|
|
439
444
|
case "pull_to_refresh":
|