@ishlabs/cli 0.18.0 → 0.20.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 +26 -2
- package/dist/commands/config.js +9 -1
- package/dist/commands/docs.js +6 -7
- package/dist/commands/person.js +123 -9
- package/dist/commands/secret.js +25 -2
- package/dist/commands/source.d.ts +1 -1
- package/dist/commands/source.js +10 -6
- package/dist/commands/study-run.js +57 -30
- package/dist/commands/study.js +36 -6
- package/dist/commands/workspace.js +41 -6
- package/dist/index.js +227 -44
- package/dist/lib/alias-store.js +23 -4
- package/dist/lib/auth.js +22 -4
- package/dist/lib/baggage.d.ts +15 -6
- package/dist/lib/baggage.js +14 -8
- package/dist/lib/command-helpers.d.ts +1 -0
- package/dist/lib/command-helpers.js +79 -7
- package/dist/lib/docs.js +221 -22
- package/dist/lib/output.d.ts +3 -3
- package/dist/lib/output.js +125 -76
- package/dist/lib/profile-sources.js +18 -0
- package/dist/lib/skill-content.js +11 -2
- package/dist/lib/study-participants.d.ts +32 -0
- package/dist/lib/study-participants.js +12 -0
- package/dist/lib/types.d.ts +0 -1
- package/dist/upgrade.js +9 -2
- package/package.json +1 -1
package/dist/commands/ask.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ish ask — Create and run asks (multi-round surveys with variants).
|
|
3
3
|
*/
|
|
4
|
-
import { withClient, getWebUrl, terminalLink, resolveWorkspace, resolveAsk, collectRepeatable, parseWaitTimeout, resolvePersonIds, addPersonFilterFlags, } from "../lib/command-helpers.js";
|
|
4
|
+
import { withClient, getWebUrl, terminalLink, resolveWorkspace, resolveAsk, collectRepeatable, parseWaitTimeout, resolvePersonIds, addPersonFilterFlags, confirmDestructive, } from "../lib/command-helpers.js";
|
|
5
5
|
import { resolveId, tagAlias, ALIAS_PREFIX } from "../lib/alias-store.js";
|
|
6
6
|
import { loadConfig, saveConfig } from "../config.js";
|
|
7
7
|
import { formatAskList, formatAskDetail, formatRoundDetail, formatAskResults, output, } from "../lib/output.js";
|
|
@@ -927,9 +927,33 @@ Examples:
|
|
|
927
927
|
.description("Delete an ask and all its rounds, responses, and participants")
|
|
928
928
|
.argument("[id]", "Ask alias or UUID (defaults to active ask)")
|
|
929
929
|
.option("--ask <id>", "Ask ID; alternative to positional argument")
|
|
930
|
-
.
|
|
930
|
+
.option("-y, --yes", "Skip confirmation prompt (required in --json or non-TTY contexts)")
|
|
931
|
+
.addHelpText("after", `
|
|
932
|
+
Examples:
|
|
933
|
+
$ ish ask delete a-6ec # interactive — prompts for confirmation
|
|
934
|
+
$ ish ask delete a-6ec --yes # non-interactive
|
|
935
|
+
$ ish ask delete a-6ec --json --yes`)
|
|
931
936
|
.action(async (id, opts, cmd) => {
|
|
932
937
|
await withClient(cmd, async (client, globals) => {
|
|
938
|
+
// Code-review #5: when neither positional id nor --ask is provided,
|
|
939
|
+
// we fall back to the active ask in config. If config has no active
|
|
940
|
+
// ask either, the prompt would say "Delete the active ask..." and
|
|
941
|
+
// then error AFTER confirmation with "No active ask" — confusing.
|
|
942
|
+
// Detect the empty-ref case BEFORE the prompt so the user sees
|
|
943
|
+
// the actionable error first.
|
|
944
|
+
const ref = id ?? opts.ask;
|
|
945
|
+
if (!ref) {
|
|
946
|
+
const config = loadConfig();
|
|
947
|
+
if (!config.ask) {
|
|
948
|
+
throw new Error("No active ask to delete. Pass an ask id (positional or --ask <id>), or run `ish ask use <id>` to set one first.");
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
// Pattern G follow-up: confirm BEFORE resolving so a typo'd alias
|
|
952
|
+
// (e.g. `a-fff`) doesn't bypass the guard. Show the raw input in
|
|
953
|
+
// the prompt; the alias resolution happens after the user
|
|
954
|
+
// confirms.
|
|
955
|
+
const displayRef = ref ?? "active ask";
|
|
956
|
+
await confirmDestructive(`Delete ${displayRef === "active ask" ? "the active ask" : `ask ${displayRef}`} (and all its rounds, responses, participants)? This cannot be undone.`, { yes: opts.yes, json: globals.json });
|
|
933
957
|
const aid = resolveAsk(pickAskRef(id, opts.ask));
|
|
934
958
|
await client.del(`/asks/${aid}`);
|
|
935
959
|
// If we just deleted the active ask, clear it from config.
|
package/dist/commands/config.js
CHANGED
|
@@ -12,7 +12,15 @@ export function registerConfigCommands(program) {
|
|
|
12
12
|
A simulation config tunes how participants behave during a run (model, timing, retries, etc.).
|
|
13
13
|
Pass \`--config <id>\` to \`ish study run\` to override the default for one dispatch.
|
|
14
14
|
|
|
15
|
-
Configs are global — not scoped to a workspace.
|
|
15
|
+
Configs are global — not scoped to a workspace. Passing \`--workspace\` to a
|
|
16
|
+
\`config\` command is silently ignored (the global flag is parsed; the value
|
|
17
|
+
has no effect on which configs are returned). Identify configs by alias
|
|
18
|
+
(\`c-...\`) or UUID.
|
|
19
|
+
|
|
20
|
+
Note: listing simulation configs requires admin privileges. Non-admins get a
|
|
21
|
+
\`forbidden\` (403) envelope on \`ish config list\`. If you need a specific
|
|
22
|
+
config, pass it directly via \`ish study run --config <id>\`; the run-time
|
|
23
|
+
override doesn't require admin access.
|
|
16
24
|
|
|
17
25
|
Run \`ish docs overview\` for the full mental model.`);
|
|
18
26
|
config
|
package/dist/commands/docs.js
CHANGED
|
@@ -85,13 +85,12 @@ export function registerDocsCommands(program) {
|
|
|
85
85
|
const docs = program
|
|
86
86
|
.command("docs")
|
|
87
87
|
.description("Offline docs for agents — mental model, concept pages, search")
|
|
88
|
-
.addHelpText("after",
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
Examples:
|
|
88
|
+
.addHelpText("after",
|
|
89
|
+
// ISSUE-013: dropped the manual "Subcommands:" block — Commander's
|
|
90
|
+
// auto-generated "Commands:" section already lists the verbs. Keeping
|
|
91
|
+
// just Examples avoids the near-duplicate that was visible on
|
|
92
|
+
// `ish docs --help`.
|
|
93
|
+
`\nExamples:
|
|
95
94
|
$ ish docs # overview
|
|
96
95
|
$ ish docs list
|
|
97
96
|
$ ish docs get-page concepts/study
|
package/dist/commands/person.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* ish person — Manage people, generation, and source uploads.
|
|
3
3
|
*/
|
|
4
4
|
import fs from "node:fs";
|
|
5
|
-
import { withClient, readJsonFileOrStdin, resolveWorkspace } from "../lib/command-helpers.js";
|
|
5
|
+
import { withClient, readJsonFileOrStdin, resolveWorkspace, confirmDestructive, normalizeVisibility } from "../lib/command-helpers.js";
|
|
6
6
|
import { resolveId, tagAlias, ALIAS_PREFIX } from "../lib/alias-store.js";
|
|
7
7
|
import { formatPersonList, output, } from "../lib/output.js";
|
|
8
8
|
import { resolveTextContent } from "../lib/upload.js";
|
|
@@ -37,6 +37,12 @@ Concept pages: ish docs get-page concepts/person
|
|
|
37
37
|
.option("--country <country>", "Filter by country code, e.g. US (repeatable)", collect, [])
|
|
38
38
|
.option("--min-age <n>", "Minimum age")
|
|
39
39
|
.option("--max-age <n>", "Maximum age")
|
|
40
|
+
// ISSUE-032: asymmetric — `ask create` and `study run` accept
|
|
41
|
+
// `--visibility workspace|shared|platform` but `person list` didn't,
|
|
42
|
+
// so a user looking for "people scoped to MY workspace" had to
|
|
43
|
+
// download all 13k+ platform people and grep client-side. Same
|
|
44
|
+
// normalizer + flag wording as the sibling commands.
|
|
45
|
+
.option("--visibility <v>", "Filter by visibility: workspace | shared | platform (legacy 'private'/'public' accepted as aliases)")
|
|
40
46
|
.option("--limit <n>", "Max results (default 50)", "50")
|
|
41
47
|
.option("--offset <n>", "Offset for pagination", "0")
|
|
42
48
|
.addHelpText("after", `
|
|
@@ -47,6 +53,8 @@ Examples:
|
|
|
47
53
|
$ ish person list --occupation founder --occupation designer
|
|
48
54
|
$ ish person list --gender female --gender male --country US --country GB
|
|
49
55
|
$ ish person list --type all --json
|
|
56
|
+
$ ish person list --visibility workspace # only workspace-scoped people
|
|
57
|
+
$ ish person list --visibility platform --country US # platform pool by country
|
|
50
58
|
|
|
51
59
|
# Pagination: default --limit is 50, iterate with --offset.
|
|
52
60
|
$ ish person list --limit 100
|
|
@@ -75,6 +83,9 @@ Examples:
|
|
|
75
83
|
params.min_age = opts.minAge;
|
|
76
84
|
if (opts.maxAge)
|
|
77
85
|
params.max_age = opts.maxAge;
|
|
86
|
+
const normVis = normalizeVisibility(opts.visibility);
|
|
87
|
+
if (normVis)
|
|
88
|
+
params.visibility = normVis;
|
|
78
89
|
const data = await client.get("/people", params);
|
|
79
90
|
formatPersonList(data, globals.json, parseInt(opts.limit, 10));
|
|
80
91
|
// Pattern H1: when there's more data, surface a stderr hint so agents
|
|
@@ -104,13 +115,110 @@ Examples:
|
|
|
104
115
|
});
|
|
105
116
|
person
|
|
106
117
|
.command("create")
|
|
107
|
-
.description("Create a person from an exact JSON spec (no LLM)")
|
|
108
|
-
|
|
118
|
+
.description("Create a person from an exact JSON spec (no LLM). Accepts inline flags or --file.")
|
|
119
|
+
// ISSUE-033: previously --file was the ONLY accepted input — asymmetric
|
|
120
|
+
// with `person update` which has a rich inline-flag surface. First-time
|
|
121
|
+
// users reading `person update --help` reasonably expected the same on
|
|
122
|
+
// create. Now mirrored: --file is optional, inline flags compose into
|
|
123
|
+
// the body, inline flags override --file values.
|
|
124
|
+
.option("--file <path>", "JSON file with person data (or '-' for stdin). Escape hatch for fields not covered by inline flags.")
|
|
125
|
+
.option("--name <text>", "Person name (required if --file omitted)")
|
|
126
|
+
.option("--type <value>", "Person type: ai | human (default: ai when --file omitted)")
|
|
127
|
+
.option("--description <text>", "Person description")
|
|
128
|
+
.option("--bio <text>", "Person bio")
|
|
129
|
+
.option("--occupation <text>", "Occupation")
|
|
130
|
+
.option("--country <code>", "Country code, e.g. US")
|
|
131
|
+
.option("--gender <g>", "Gender, e.g. female")
|
|
132
|
+
.option("--date-of-birth <YYYY-MM-DD>", "Date of birth")
|
|
133
|
+
.option("--education-level <value>", `Education level. One of: ${EDUCATION_LEVELS.join(", ")}`)
|
|
134
|
+
.option("--household <value>", `Household composition (MECE). One of: ${HOUSEHOLDS.join(", ")}. A couple raising children is couple_with_kids, not couple_no_kids; "single" means lives alone with no partner, roommates, parents, or children in the household.`)
|
|
135
|
+
.option("--locale-type <value>", `Self-described neighborhood type. One of: ${LOCALE_TYPES.join(", ")}`)
|
|
136
|
+
.option("--income-level <value>", `Self-identified relative socioeconomic position. One of: ${INCOME_LEVELS.join(", ")}`)
|
|
137
|
+
.option("--employment-status <value>", `Primary daytime activity / labor-force status. One of: ${EMPLOYMENT_STATUSES.join(", ")}`)
|
|
138
|
+
.option("--accessibility-profile <json-or-path>", "AccessibilityProfile v1.0 as an inline JSON string OR a path to a JSON file. Empty object {} is the canonical default. Validated client-side against the spec before submit.")
|
|
109
139
|
.option("--workspace <id>", "Workspace (product) ID; falls back to active workspace")
|
|
110
|
-
.addHelpText("after",
|
|
140
|
+
.addHelpText("after", `
|
|
141
|
+
Examples:
|
|
142
|
+
# Inline (mirrors person update):
|
|
143
|
+
$ ish person create --name "Alice" --type ai --country US
|
|
144
|
+
$ ish person create --name "Bob" --gender male --country GB --occupation founder \\
|
|
145
|
+
--household single --locale-type urban --income-level middle \\
|
|
146
|
+
--employment-status employed_full_time --bio "..."
|
|
147
|
+
|
|
148
|
+
# From file:
|
|
149
|
+
$ ish person create --file profile.json
|
|
150
|
+
|
|
151
|
+
# From stdin:
|
|
152
|
+
$ cat profile.json | ish person create --file -
|
|
153
|
+
|
|
154
|
+
# Compose file + inline (inline overrides):
|
|
155
|
+
$ ish person create --file profile.json --bio "Updated bio for this run"
|
|
156
|
+
|
|
157
|
+
Inline flags compose into the create body. --file is an escape hatch for
|
|
158
|
+
fields not covered by inline flags. When both are provided, inline flags
|
|
159
|
+
override values from --file.
|
|
160
|
+
|
|
161
|
+
Minimum required (when --file omitted): --name (--type defaults to "ai").
|
|
162
|
+
|
|
163
|
+
Household MECE rule: a couple raising children is \`couple_with_kids\`, not
|
|
164
|
+
\`couple_no_kids\`. \`single\` means lives alone with no partner, roommates,
|
|
165
|
+
parents, or children sharing the household.
|
|
166
|
+
|
|
167
|
+
\`--accessibility-profile\` accepts either an inline JSON string OR a path to
|
|
168
|
+
a JSON file. Schema: https://ishlabs.io/spec/accessibility-profile-schema.v1.json`)
|
|
111
169
|
.action(async (opts, cmd) => {
|
|
112
170
|
await withClient(cmd, async (client, globals) => {
|
|
113
|
-
|
|
171
|
+
let body = {};
|
|
172
|
+
if (opts.file) {
|
|
173
|
+
body = (await readJsonFileOrStdin(opts.file));
|
|
174
|
+
}
|
|
175
|
+
// Inline flags compose / override the file body (mirrors person update).
|
|
176
|
+
if (opts.name !== undefined)
|
|
177
|
+
body.name = opts.name;
|
|
178
|
+
if (opts.type !== undefined)
|
|
179
|
+
body.type = opts.type;
|
|
180
|
+
if (opts.description !== undefined)
|
|
181
|
+
body.description = opts.description;
|
|
182
|
+
if (opts.bio !== undefined)
|
|
183
|
+
body.bio = opts.bio;
|
|
184
|
+
if (opts.occupation !== undefined)
|
|
185
|
+
body.occupation = opts.occupation;
|
|
186
|
+
if (opts.country !== undefined)
|
|
187
|
+
body.country = opts.country;
|
|
188
|
+
if (opts.gender !== undefined)
|
|
189
|
+
body.gender = opts.gender;
|
|
190
|
+
if (opts.dateOfBirth !== undefined)
|
|
191
|
+
body.date_of_birth = opts.dateOfBirth;
|
|
192
|
+
if (opts.educationLevel !== undefined) {
|
|
193
|
+
body.education_level = assertEnumValue(opts.educationLevel, EDUCATION_LEVELS, "--education-level");
|
|
194
|
+
}
|
|
195
|
+
if (opts.household !== undefined) {
|
|
196
|
+
body.household = assertEnumValue(opts.household, HOUSEHOLDS, "--household");
|
|
197
|
+
}
|
|
198
|
+
if (opts.localeType !== undefined) {
|
|
199
|
+
body.locale_type = assertEnumValue(opts.localeType, LOCALE_TYPES, "--locale-type");
|
|
200
|
+
}
|
|
201
|
+
if (opts.incomeLevel !== undefined) {
|
|
202
|
+
body.income_level = assertEnumValue(opts.incomeLevel, INCOME_LEVELS, "--income-level");
|
|
203
|
+
}
|
|
204
|
+
if (opts.employmentStatus !== undefined) {
|
|
205
|
+
body.employment_status = assertEnumValue(opts.employmentStatus, EMPLOYMENT_STATUSES, "--employment-status");
|
|
206
|
+
}
|
|
207
|
+
if (opts.accessibilityProfile !== undefined) {
|
|
208
|
+
body.accessibility_profile = parseAccessibilityProfileFlag(opts.accessibilityProfile);
|
|
209
|
+
}
|
|
210
|
+
// Type default — keeps the inline-only path ergonomic without
|
|
211
|
+
// requiring `--type ai` on every minimal create.
|
|
212
|
+
if (body.name && body.type === undefined) {
|
|
213
|
+
body.type = "ai";
|
|
214
|
+
}
|
|
215
|
+
// Without --file AND with no inline flags, the body has only
|
|
216
|
+
// product_id — that would 422 server-side with no useful guidance.
|
|
217
|
+
// Provide a clear local error first.
|
|
218
|
+
if (!opts.file && body.name === undefined) {
|
|
219
|
+
throw new Error("Nothing to create. Provide --file <path> or at least --name (plus optional inline flags). " +
|
|
220
|
+
"Run `ish person create --help` for the full inline-flag set.");
|
|
221
|
+
}
|
|
114
222
|
if (opts.workspace || body.product_id == null) {
|
|
115
223
|
body.product_id = resolveWorkspace(opts.workspace);
|
|
116
224
|
}
|
|
@@ -135,7 +243,7 @@ Examples:
|
|
|
135
243
|
.option("--no-scenarios", "Skip fetching the evidence-grounded scenarios for each generated person")
|
|
136
244
|
.option("--no-wait", "Don't poll source-processing status. Only relevant when --source is a local path (paths get auto-uploaded and processed).")
|
|
137
245
|
.option("--source-timeout <seconds>", "Source-processing poll timeout in seconds. Only relevant when --source is a local path.", "300")
|
|
138
|
-
.option("--timeout <seconds>", "Generation-job poll timeout in seconds (default
|
|
246
|
+
.option("--timeout <seconds>", "Generation-job poll timeout in seconds (default 180; pass a larger value for long-running jobs). The job keeps running server-side past this; re-poll later or re-run with a longer --timeout — don't re-enqueue, that would duplicate the work.", "180")
|
|
139
247
|
.addHelpText("after", `
|
|
140
248
|
Generation is an async job: ish enqueues it, polls until the people plus
|
|
141
249
|
their evidence-grounded scenarios are ready (~30-60s), then prints them. The
|
|
@@ -428,12 +536,18 @@ Schema: https://ishlabs.io/spec/accessibility-profile-schema.v1.json`)
|
|
|
428
536
|
.description("Delete a person")
|
|
429
537
|
.argument("<id>", "Person ID")
|
|
430
538
|
.option("--workspace <id>", "Workspace ID; accepted for consistency (workspace is inferred from the person)")
|
|
431
|
-
.
|
|
432
|
-
.
|
|
539
|
+
.option("-y, --yes", "Skip confirmation prompt (required in --json or non-TTY contexts)")
|
|
540
|
+
.addHelpText("after", `
|
|
541
|
+
Examples:
|
|
542
|
+
$ ish person delete <id> # interactive — prompts for confirmation
|
|
543
|
+
$ ish person delete <id> --yes # non-interactive
|
|
544
|
+
$ ish person delete <id> --json --yes`)
|
|
545
|
+
.action(async (id, opts, cmd) => {
|
|
433
546
|
await withClient(cmd, async (client, globals) => {
|
|
434
547
|
const rid = resolveId(id);
|
|
548
|
+
await confirmDestructive(`Delete person ${tagAlias(ALIAS_PREFIX.person, rid)}? This cannot be undone.`, { yes: opts.yes, json: globals.json });
|
|
435
549
|
await client.del(`/people/${rid}`);
|
|
436
|
-
output({ id: rid, alias: tagAlias(ALIAS_PREFIX.person, rid), message: "
|
|
550
|
+
output({ id: rid, alias: tagAlias(ALIAS_PREFIX.person, rid), message: "Person deleted" }, globals.json, { writePath: true });
|
|
437
551
|
});
|
|
438
552
|
});
|
|
439
553
|
person
|
package/dist/commands/secret.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* of shell history.
|
|
17
17
|
* - No interactive prompts (CLI is for autonomous agents).
|
|
18
18
|
*/
|
|
19
|
-
import { withClient, resolveWorkspace, readFileOrStdin } from "../lib/command-helpers.js";
|
|
19
|
+
import { withClient, resolveWorkspace, readFileOrStdin, confirmDestructive } from "../lib/command-helpers.js";
|
|
20
20
|
import { output } from "../lib/output.js";
|
|
21
21
|
const RESERVED_SITE_ACCESS_KEYS = new Set([
|
|
22
22
|
"BASIC_AUTH_USERNAME",
|
|
@@ -127,6 +127,8 @@ Examples:
|
|
|
127
127
|
.option("--workspace <id>", "Workspace ID; defaults to active workspace")
|
|
128
128
|
.addHelpText("after", `
|
|
129
129
|
Pick exactly one of: positional <value>, --value-file <path>, --value-stdin.
|
|
130
|
+
Bare \`-\` as the positional value is accepted as an alias for --value-stdin
|
|
131
|
+
(matches the \`--value-file -\` convention).
|
|
130
132
|
Stdin and --value-file are preferred for real keys so the value never lands
|
|
131
133
|
in shell history.
|
|
132
134
|
|
|
@@ -138,6 +140,16 @@ Examples:
|
|
|
138
140
|
.action(async (key, value, opts, cmd) => {
|
|
139
141
|
await withClient(cmd, async (client, globals) => {
|
|
140
142
|
validateKey(key);
|
|
143
|
+
// ISSUE-019: bare `-` as the positional value is a common Unix
|
|
144
|
+
// shorthand for "read from stdin" (matches `--value-file -`'s
|
|
145
|
+
// existing convention). Previously bare `-` was silently stored
|
|
146
|
+
// as the literal string "-" — undocumented + surprising. Treat
|
|
147
|
+
// it as an alias for --value-stdin so the convention matches
|
|
148
|
+
// user expectation. Documented in --help below.
|
|
149
|
+
if (value === "-") {
|
|
150
|
+
value = undefined;
|
|
151
|
+
opts.valueStdin = true;
|
|
152
|
+
}
|
|
141
153
|
// Exactly one input source.
|
|
142
154
|
const sources = [
|
|
143
155
|
value !== undefined,
|
|
@@ -212,7 +224,12 @@ Examples:
|
|
|
212
224
|
.description("Delete a workspace secret by key")
|
|
213
225
|
.argument("<key>", "Secret key to delete")
|
|
214
226
|
.option("--workspace <id>", "Workspace ID; defaults to active workspace")
|
|
215
|
-
.
|
|
227
|
+
.option("-y, --yes", "Skip confirmation prompt (required in --json or non-TTY contexts)")
|
|
228
|
+
.addHelpText("after", `
|
|
229
|
+
Examples:
|
|
230
|
+
$ ish secret delete GROQ_API_KEY # interactive — prompts for confirmation
|
|
231
|
+
$ ish secret delete GROQ_API_KEY --yes # non-interactive
|
|
232
|
+
$ ish secret delete GROQ_API_KEY --json --yes`)
|
|
216
233
|
.action(async (key, opts, cmd) => {
|
|
217
234
|
await withClient(cmd, async (client, globals) => {
|
|
218
235
|
if (!KEY_PATTERN.test(key)) {
|
|
@@ -220,6 +237,12 @@ Examples:
|
|
|
220
237
|
err.name = "ValidationError";
|
|
221
238
|
throw err;
|
|
222
239
|
}
|
|
240
|
+
// Pattern G follow-up: the original Pattern G fix covered
|
|
241
|
+
// workspace/person/source delete (commit 891b22d), then a
|
|
242
|
+
// follow-up added ask delete. `secret delete` was the seventh
|
|
243
|
+
// destructive op and slipped through; rolling the guard in
|
|
244
|
+
// for full consistency.
|
|
245
|
+
await confirmDestructive(`Delete secret "${key}"? This cannot be undone.`, { yes: opts.yes, json: globals.json });
|
|
223
246
|
const wid = resolveWorkspace(opts.workspace);
|
|
224
247
|
const all = await client.get(`/products/${wid}/secrets`);
|
|
225
248
|
const match = all.find((s) => s.key === key);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* ish source — Upload and inspect participant attachments used as generation inputs.
|
|
3
3
|
*
|
|
4
4
|
* Attachments (transcripts, audio, images, PDFs) are inputs to `ish person
|
|
5
|
-
* generate`. For one-shot generation, `
|
|
5
|
+
* generate`. For one-shot generation, `ish person generate --source <path>`
|
|
6
6
|
* auto-uploads. Use these commands to upload once and reuse across multiple
|
|
7
7
|
* generation runs, or to inspect processing status.
|
|
8
8
|
*
|
package/dist/commands/source.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* ish source — Upload and inspect participant attachments used as generation inputs.
|
|
3
3
|
*
|
|
4
4
|
* Attachments (transcripts, audio, images, PDFs) are inputs to `ish person
|
|
5
|
-
* generate`. For one-shot generation, `
|
|
5
|
+
* generate`. For one-shot generation, `ish person generate --source <path>`
|
|
6
6
|
* auto-uploads. Use these commands to upload once and reuse across multiple
|
|
7
7
|
* generation runs, or to inspect processing status.
|
|
8
8
|
*
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* existing scripts and agents; internally we call the unified
|
|
11
11
|
* /people/attachments/* endpoint family (see ADR-0034).
|
|
12
12
|
*/
|
|
13
|
-
import { withClient, resolveWorkspace } from "../lib/command-helpers.js";
|
|
13
|
+
import { withClient, resolveWorkspace, confirmDestructive } from "../lib/command-helpers.js";
|
|
14
14
|
import { resolveId, tagAlias, ALIAS_PREFIX } from "../lib/alias-store.js";
|
|
15
15
|
import { normalizeEnumValue } from "../lib/enums.js";
|
|
16
16
|
import { formatAttachment, output } from "../lib/output.js";
|
|
@@ -24,7 +24,7 @@ export function registerSourceCommands(program) {
|
|
|
24
24
|
A source — internally a participant attachment — is an input to \`ish person generate\`:
|
|
25
25
|
transcript, audio, image, or PDF. Use \`source upload\` when you want to reuse the
|
|
26
26
|
same attachment across multiple generation runs; otherwise pass a local path directly
|
|
27
|
-
to \`
|
|
27
|
+
to \`person generate --source\` and it auto-uploads.
|
|
28
28
|
|
|
29
29
|
Concept pages: ish docs get-page concepts/source
|
|
30
30
|
ish docs get-page concepts/profile`);
|
|
@@ -86,21 +86,25 @@ Examples:
|
|
|
86
86
|
.command("delete")
|
|
87
87
|
.description("Delete a participant attachment plus its uploaded file")
|
|
88
88
|
.argument("<id>", "Attachment ID or alias")
|
|
89
|
+
.option("-y, --yes", "Skip confirmation prompt (required in --json or non-TTY contexts)")
|
|
89
90
|
.addHelpText("after", `
|
|
90
91
|
The backend ref-counts attachment deletes: the file row + storage object are
|
|
91
92
|
removed only when no profile mappings remain. Profiles already generated from
|
|
92
93
|
this attachment keep their seed mapping until they themselves are deleted.
|
|
93
94
|
|
|
94
95
|
Examples:
|
|
95
|
-
$ ish source delete ps-3a4
|
|
96
|
-
|
|
96
|
+
$ ish source delete ps-3a4 # interactive — prompts for confirmation
|
|
97
|
+
$ ish source delete ps-3a4 --yes # non-interactive
|
|
98
|
+
$ ish source delete ps-3a4 --json --yes`)
|
|
99
|
+
.action(async (id, opts, cmd) => {
|
|
97
100
|
await withClient(cmd, async (client, globals) => {
|
|
98
101
|
const rid = resolveId(id);
|
|
102
|
+
await confirmDestructive(`Delete source ${tagAlias(ALIAS_PREFIX.personSource, rid)}? This cannot be undone.`, { yes: opts.yes, json: globals.json });
|
|
99
103
|
await client.del(`/people/attachments/${rid}`);
|
|
100
104
|
output({
|
|
101
105
|
id: rid,
|
|
102
106
|
alias: tagAlias(ALIAS_PREFIX.personSource, rid),
|
|
103
|
-
message: "
|
|
107
|
+
message: "Source deleted",
|
|
104
108
|
}, globals.json, { writePath: true });
|
|
105
109
|
});
|
|
106
110
|
});
|
|
@@ -12,6 +12,7 @@ import * as readline from "node:readline/promises";
|
|
|
12
12
|
import { withClient, getWebUrl, terminalLink, resolveWorkspace, resolveStudy, parseWaitTimeout, resolvePersonIds, addPersonFilterFlags, hasPersonFlags, readFileOrStdin, } from "../lib/command-helpers.js";
|
|
13
13
|
import { resolveId, tagAlias, ALIAS_PREFIX } from "../lib/alias-store.js";
|
|
14
14
|
import { output, formatSimulationPoll } from "../lib/output.js";
|
|
15
|
+
import { fetchStudyParticipants } from "../lib/study-participants.js";
|
|
15
16
|
import { streamStudyEvents } from "../lib/study-events.js";
|
|
16
17
|
import { isMediaModality, isChatModality, iterationHasContent, describeRequiredContentFlag, readChatMode, readParticipantPairConfig, summarizeRoleCriteria, } from "../lib/modality.js";
|
|
17
18
|
// NOTE: local-sim modules are loaded via dynamic import at the `--local`
|
|
@@ -107,24 +108,27 @@ const POLL_INTERVAL_MS = 5_000;
|
|
|
107
108
|
// transparently reverts to POLL_INTERVAL_MS.
|
|
108
109
|
const SSE_BACKSTOP_INTERVAL_MS = 30_000;
|
|
109
110
|
const TERMINAL_STATUSES = new Set(["completed", "errored", "failed", "cancelled", "canceled"]);
|
|
110
|
-
function flattenParticipantStatuses(
|
|
111
|
+
function flattenParticipantStatuses(participants, opts = {}) {
|
|
111
112
|
const rows = [];
|
|
112
|
-
for (const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
113
|
+
for (const t of participants ?? []) {
|
|
114
|
+
if (opts.iterationId && t.iteration_id !== opts.iterationId)
|
|
115
|
+
continue;
|
|
116
|
+
if (opts.only && !opts.only.has(t.id))
|
|
117
|
+
continue;
|
|
118
|
+
// Pattern A (cli half): backend now reports per-participant crash detail at
|
|
119
|
+
// `error_message`. Keep `error` / `failure_reason` as legacy fallbacks
|
|
120
|
+
// until every backend deploy is on the new contract.
|
|
121
|
+
const errorMessage = t.error_message ||
|
|
122
|
+
t.error ||
|
|
123
|
+
t.failure_reason ||
|
|
124
|
+
null;
|
|
125
|
+
rows.push({
|
|
126
|
+
id: t.id,
|
|
127
|
+
status: t.status,
|
|
128
|
+
participant_name: t.person?.name || "Unknown",
|
|
129
|
+
interaction_count: Array.isArray(t.interactions) ? t.interactions.length : 0,
|
|
130
|
+
...(errorMessage && { error_message: String(errorMessage) }),
|
|
131
|
+
});
|
|
128
132
|
}
|
|
129
133
|
return rows;
|
|
130
134
|
}
|
|
@@ -146,13 +150,15 @@ async function pollStudyUntilDone(client, opts) {
|
|
|
146
150
|
let pendingEvent = eventIter.next();
|
|
147
151
|
try {
|
|
148
152
|
while (true) {
|
|
149
|
-
const study = await
|
|
153
|
+
const [study, participants] = await Promise.all([
|
|
154
|
+
client.get(`/studies/${opts.studyId}`, undefined, { timeout: 60_000 }),
|
|
155
|
+
fetchStudyParticipants(client, opts.studyId, { timeout: 60_000 }),
|
|
156
|
+
]);
|
|
150
157
|
const isMedia = isMediaModality(study.modality);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
const rows = flattenParticipantStatuses(iterations, opts.participantIds);
|
|
158
|
+
const rows = flattenParticipantStatuses(participants, {
|
|
159
|
+
iterationId: opts.iterationId,
|
|
160
|
+
only: opts.participantIds,
|
|
161
|
+
});
|
|
156
162
|
const total = rows.length;
|
|
157
163
|
const done = rows.filter((r) => TERMINAL_STATUSES.has(r.status)).length;
|
|
158
164
|
const errored = rows.filter((r) => r.status === "errored" || r.status === "failed").length;
|
|
@@ -366,7 +372,8 @@ Examples:
|
|
|
366
372
|
|| parseInt(opts.dispatchTimeout, 10) < 1)) {
|
|
367
373
|
throw new Error(`--dispatch-timeout must be a positive integer (seconds), got "${opts.dispatchTimeout}".`);
|
|
368
374
|
}
|
|
369
|
-
// Step 0: Fetch study (
|
|
375
|
+
// Step 0: Fetch study metadata (lite — participants live on a
|
|
376
|
+
// separate endpoint; we fetch them below only when reuse is possible).
|
|
370
377
|
const study = await client.get(`/studies/${resolvedStudy}`);
|
|
371
378
|
const modality = study.modality || "interactive";
|
|
372
379
|
const isMedia = isMediaModality(modality);
|
|
@@ -455,8 +462,15 @@ Examples:
|
|
|
455
462
|
const resolved = await resolvePersonIds(client, resolvedWorkspace, opts, { requireSimulatable: false, allFlagName: "--all" });
|
|
456
463
|
personIds.push(...resolved);
|
|
457
464
|
}
|
|
458
|
-
else if (
|
|
459
|
-
|
|
465
|
+
else if (!isPair) {
|
|
466
|
+
// Reuse-existing path: no person flags, non-pair iteration. Fetch
|
|
467
|
+
// participants from the dedicated endpoint and filter to the
|
|
468
|
+
// current iteration. (Pair iterations don't reuse participant rows
|
|
469
|
+
// — they reuse Conversation refs above.)
|
|
470
|
+
const studyParticipants = await fetchStudyParticipants(client, resolvedStudy);
|
|
471
|
+
for (const t of studyParticipants) {
|
|
472
|
+
if (t.iteration_id !== iteration.id)
|
|
473
|
+
continue;
|
|
460
474
|
const pid = t.person_id || t.person?.id;
|
|
461
475
|
const name = t.person?.name;
|
|
462
476
|
if (pid && !personNames.has(pid)) {
|
|
@@ -667,12 +681,22 @@ Examples:
|
|
|
667
681
|
// language?: str }
|
|
668
682
|
// reply : { conversations: [{ conversation_id, pair_index,
|
|
669
683
|
// participant_a_id, participant_b_id }] }
|
|
684
|
+
//
|
|
685
|
+
// On the LITE study response, the iteration's conversation refs
|
|
686
|
+
// use the storage-shape field names group_a_participant_id /
|
|
687
|
+
// group_b_participant_id. Map them back to the pair-batch reply
|
|
688
|
+
// shape (participant_a_id / participant_b_id) the rest of this
|
|
689
|
+
// function expects.
|
|
670
690
|
const existingConvs = iteration.conversations ?? [];
|
|
671
691
|
const reusable = [];
|
|
672
692
|
for (const c of existingConvs) {
|
|
673
693
|
const cid = c.conversation_id || c.id;
|
|
674
|
-
if (cid && c.
|
|
675
|
-
reusable.push({
|
|
694
|
+
if (cid && c.group_a_participant_id && c.group_b_participant_id) {
|
|
695
|
+
reusable.push({
|
|
696
|
+
conversation_id: cid,
|
|
697
|
+
participant_a_id: c.group_a_participant_id,
|
|
698
|
+
participant_b_id: c.group_b_participant_id,
|
|
699
|
+
});
|
|
676
700
|
}
|
|
677
701
|
}
|
|
678
702
|
let pairRows;
|
|
@@ -1059,9 +1083,12 @@ Examples:
|
|
|
1059
1083
|
// an agent that ran `study use s-...` then `study poll` would get a
|
|
1060
1084
|
// confusing "Provide a participant_id argument or --study flag" error.
|
|
1061
1085
|
const rid = resolveStudy(opts.study);
|
|
1062
|
-
const study = await
|
|
1086
|
+
const [study, participants] = await Promise.all([
|
|
1087
|
+
client.get(`/studies/${rid}`),
|
|
1088
|
+
fetchStudyParticipants(client, rid),
|
|
1089
|
+
]);
|
|
1063
1090
|
const isMedia = isMediaModality(study.modality);
|
|
1064
|
-
const allParticipants = flattenParticipantStatuses(
|
|
1091
|
+
const allParticipants = flattenParticipantStatuses(participants);
|
|
1065
1092
|
formatSimulationPoll(allParticipants, globals.json, isMedia);
|
|
1066
1093
|
if (!globals.json && study.product_id) {
|
|
1067
1094
|
const url = getWebUrl(globals, `/${study.product_id}/${rid}/timeline`);
|
package/dist/commands/study.js
CHANGED
|
@@ -8,6 +8,7 @@ import { resolveId, tagAlias, ALIAS_PREFIX } from "../lib/alias-store.js";
|
|
|
8
8
|
import { loadConfig, saveConfig } from "../config.js";
|
|
9
9
|
import { formatStudyList, formatStudyDetail, formatStudyResults, buildStudyResultsSummary, buildChatTranscript, output, ValidationError, } from "../lib/output.js";
|
|
10
10
|
import { VALID_CONTENT_TYPES } from "../lib/types.js";
|
|
11
|
+
import { fetchStudyParticipants } from "../lib/study-participants.js";
|
|
11
12
|
import { parseAssignment, loadAssignmentsFile, validateAssignmentsArray, parseQuestion } from "../lib/study-inputs.js";
|
|
12
13
|
import { loadQuestionsManifest } from "../lib/ask-questions.js";
|
|
13
14
|
import { isLocalPath } from "../lib/upload.js";
|
|
@@ -543,8 +544,18 @@ Next: configure a run with \`ish iteration create --study <id>\`,
|
|
|
543
544
|
const data = await client.post(`/products/${resolvedWs}/studies`, body);
|
|
544
545
|
if (data.id) {
|
|
545
546
|
const config = loadConfig();
|
|
547
|
+
const prevStudy = config.study;
|
|
546
548
|
config.study = data.id;
|
|
547
549
|
saveConfig(config);
|
|
550
|
+
// Auto-activating the new study is intentional ergonomics (the
|
|
551
|
+
// common next step is `ish iteration create --study <new>`), but
|
|
552
|
+
// it was previously silent — surprised users who had set a
|
|
553
|
+
// different active study just before (ISSUE-030). Always
|
|
554
|
+
// surface the side-effect on stderr.
|
|
555
|
+
if (!globals.json) {
|
|
556
|
+
const verb = prevStudy && prevStudy !== data.id ? "replaced" : "set";
|
|
557
|
+
console.error(`Active study ${verb} to "${data.name || data.id}".`);
|
|
558
|
+
}
|
|
548
559
|
}
|
|
549
560
|
const result = data;
|
|
550
561
|
if (result.id)
|
|
@@ -616,14 +627,17 @@ list table layout in human mode.`)
|
|
|
616
627
|
throw new Error("Provide at least one study id.");
|
|
617
628
|
if (flat.length === 1) {
|
|
618
629
|
const rid = resolveId(flat[0]);
|
|
619
|
-
const data = await
|
|
630
|
+
const [data, participants] = await Promise.all([
|
|
631
|
+
client.get(`/studies/${rid}`),
|
|
632
|
+
fetchStudyParticipants(client, rid),
|
|
633
|
+
]);
|
|
620
634
|
const result = data;
|
|
621
635
|
if (result.id)
|
|
622
636
|
result.alias = tagAlias(ALIAS_PREFIX.study, String(result.id));
|
|
623
637
|
if (data.product_id) {
|
|
624
638
|
result.url = getWebUrl(globals, `/${data.product_id}/${rid}/overview`);
|
|
625
639
|
}
|
|
626
|
-
formatStudyDetail(result, globals.json);
|
|
640
|
+
formatStudyDetail(result, globals.json, {}, participants);
|
|
627
641
|
if (!globals.json && data.product_id) {
|
|
628
642
|
const url = getWebUrl(globals, `/${data.product_id}/${rid}/overview`);
|
|
629
643
|
console.error(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
|
|
@@ -632,13 +646,17 @@ list table layout in human mode.`)
|
|
|
632
646
|
}
|
|
633
647
|
const results = await Promise.all(flat.map(async (raw) => {
|
|
634
648
|
const rid = resolveId(raw);
|
|
635
|
-
const data = await
|
|
649
|
+
const [data, participants] = await Promise.all([
|
|
650
|
+
client.get(`/studies/${rid}`),
|
|
651
|
+
fetchStudyParticipants(client, rid),
|
|
652
|
+
]);
|
|
636
653
|
const r = data;
|
|
637
654
|
if (r.id)
|
|
638
655
|
r.alias = tagAlias(ALIAS_PREFIX.study, String(r.id));
|
|
639
656
|
if (data.product_id) {
|
|
640
657
|
r.url = getWebUrl(globals, `/${data.product_id}/${rid}/overview`);
|
|
641
658
|
}
|
|
659
|
+
r.participants = participants;
|
|
642
660
|
return r;
|
|
643
661
|
}));
|
|
644
662
|
if (globals.json) {
|
|
@@ -751,12 +769,15 @@ When no runs have completed, the default envelope is returned with zero counts a
|
|
|
751
769
|
output(buildChatTranscript(participant), globals.json, { preProjected: true });
|
|
752
770
|
return;
|
|
753
771
|
}
|
|
754
|
-
const data = await
|
|
772
|
+
const [data, participants] = await Promise.all([
|
|
773
|
+
client.get(`/studies/${rid}`),
|
|
774
|
+
fetchStudyParticipants(client, rid),
|
|
775
|
+
]);
|
|
755
776
|
if (wantsSummary) {
|
|
756
|
-
output(buildStudyResultsSummary(data), globals.json, { preProjected: true });
|
|
777
|
+
output(buildStudyResultsSummary(data, participants), globals.json, { preProjected: true });
|
|
757
778
|
}
|
|
758
779
|
else {
|
|
759
|
-
formatStudyResults(data, globals.json);
|
|
780
|
+
formatStudyResults(data, participants, globals.json);
|
|
760
781
|
}
|
|
761
782
|
if (!globals.json && data.product_id) {
|
|
762
783
|
const url = getWebUrl(globals, `/${data.product_id}/${rid}/overview`);
|
|
@@ -845,6 +866,15 @@ checklists ("steps") ride along when present in the JSON forms
|
|
|
845
866
|
json: globals.json,
|
|
846
867
|
});
|
|
847
868
|
await client.del(`/studies/${rid}`);
|
|
869
|
+
// If the deleted study was active, clear it (Pattern A — parallel to
|
|
870
|
+
// ask delete and chat endpoint delete which already do this).
|
|
871
|
+
const config = loadConfig();
|
|
872
|
+
if (config.study === rid) {
|
|
873
|
+
delete config.study;
|
|
874
|
+
saveConfig(config);
|
|
875
|
+
if (!globals.json)
|
|
876
|
+
console.error("(Cleared active study.)");
|
|
877
|
+
}
|
|
848
878
|
output({ id: rid, alias: tagAlias(ALIAS_PREFIX.study, rid), message: "Study deleted" }, globals.json, { writePath: true });
|
|
849
879
|
});
|
|
850
880
|
});
|