@allocator-one/harness-cli 1.15.1 → 1.17.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.
@@ -1,989 +1,40 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- CLI_PACKAGE,
3
+ Client,
4
+ CommandError,
5
+ EXPERT_ASSIGNMENTS_PATH,
6
+ HttpError,
7
+ LocalError,
8
+ MalformedResponseError,
9
+ TransportError,
10
+ UpgradeRequiredError,
11
+ UsageError,
12
+ activityEntries,
13
+ clean,
14
+ cleanMultiline,
15
+ conditionLine,
16
+ errorDetails,
17
+ expertAssignments,
18
+ expertSummary,
19
+ explainExpertRefusal,
20
+ gateName,
21
+ graphemeLength,
22
+ isAgentStep,
23
+ isExpertToken,
24
+ issueDetail,
25
+ issuesTable,
26
+ ownerText,
27
+ planDetail,
28
+ plansTable,
29
+ stats,
30
+ table,
31
+ truncate
32
+ } from "./chunk-BHBEIT5I.js";
33
+ import {
4
34
  CLI_VERSION,
5
35
  invokedName,
6
36
  named
7
- } from "./chunk-FQWV5MIC.js";
8
-
9
- // src/core/errors.ts
10
- var HttpError = class extends Error {
11
- constructor(status, message, body = {}, retryAfter = null) {
12
- super(message);
13
- this.status = status;
14
- this.body = body;
15
- this.retryAfter = retryAfter;
16
- this.name = "HttpError";
17
- }
18
- status;
19
- body;
20
- retryAfter;
21
- };
22
- var UpgradeRequiredError = class extends HttpError {
23
- constructor(message, body = {}) {
24
- super(426, message, body);
25
- this.name = "UpgradeRequiredError";
26
- }
27
- };
28
- var TransportError = class extends Error {
29
- constructor(message, code = null) {
30
- super(message);
31
- this.code = code;
32
- this.name = "TransportError";
33
- }
34
- code;
35
- };
36
- var MalformedResponseError = class extends TransportError {
37
- constructor(message) {
38
- super(message);
39
- this.name = "MalformedResponseError";
40
- }
41
- };
42
- var UsageError = class extends Error {
43
- constructor(message) {
44
- super(message);
45
- this.name = "UsageError";
46
- }
47
- };
48
- var LocalError = class extends Error {
49
- constructor(message) {
50
- super(message);
51
- this.name = "LocalError";
52
- }
53
- };
54
- var CommandError = class extends Error {
55
- constructor(message) {
56
- super(message);
57
- this.name = "CommandError";
58
- }
59
- };
60
-
61
- // src/core/expert.ts
62
- var EXPERT_TOKEN_PREFIX = "pxc_";
63
- var EXPERT_ASSIGNMENTS_PATH = "/api/v1/expert/assignments";
64
- function isExpertToken(token) {
65
- return token.startsWith(EXPERT_TOKEN_PREFIX);
66
- }
67
- var EXPERT_REACH = "an outside expert's pxc_ credential only lists its own assignments and reads and ticks the gates it owns on them: assignments, conditions <issue> and gate satisfy|unsatisfy (assignment_list, condition_list and gate_set over MCP)";
68
- var EXPERT_LAPSED = "the pxc_ credential is not valid here: it has expired (it lasts at most 30 days) or was revoked, its seat was revoked or has ended, the account is suspended, its TOTP enrolment changed, or it was minted for another host. If your seat is still open, mint a new one in your expert view";
69
- var NOT_AN_EXPERT = "this credential is a member's login or an API token, not an outside expert's pxc_ credential";
70
- function explainExpertRefusal(error, token) {
71
- if (!(error instanceof HttpError) || error instanceof UpgradeRequiredError) return error;
72
- const code = error.body["error"];
73
- let hint;
74
- if (error.status === 403 && code === "expert_bound_credential") hint = EXPERT_REACH;
75
- else if (error.status === 403 && code === "expert_credential_required") hint = NOT_AN_EXPERT;
76
- else if (error.status === 401 && isExpertToken(token)) hint = EXPERT_LAPSED;
77
- if (hint === void 0) return error;
78
- return new HttpError(error.status, `${error.message} \u2014 ${hint}`, error.body, error.retryAfter);
79
- }
80
-
81
- // src/core/client.ts
82
- var RECEIVE_TIMEOUT_MS = 3e4;
83
- var UPLOAD_RECEIVE_TIMEOUT_MS = 6e4;
84
- var UPLOAD_FLOOR_BYTES_PER_MS = 100;
85
- function uploadTimeoutMs(bodyBytes) {
86
- return UPLOAD_RECEIVE_TIMEOUT_MS + Math.ceil(bodyBytes / UPLOAD_FLOOR_BYTES_PER_MS);
87
- }
88
- var DEFAULT_MESSAGES = {
89
- 401: "Missing or invalid token (check HARNESS_API_TOKEN)",
90
- 403: "Token not permitted (scope or admin gate)",
91
- 404: "Not found",
92
- 409: "Conflict",
93
- 422: "Validation error",
94
- 426: `CLI too old for this API \u2014 upgrade: npm i -g ${CLI_PACKAGE}@latest`,
95
- 429: "Rate limited \u2014 retry later"
96
- };
97
- var API_VERSION_HEADER = "x-harness-api-version";
98
- var STATS_PATH = "/api/v1/stats";
99
- var SESSION_PATH = "/api/v1/session";
100
- function isLoginCredential(token) {
101
- return !token.startsWith("aone_") && !isExpertToken(token);
102
- }
103
- var Client = class {
104
- constructor(config) {
105
- this.config = config;
106
- this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
107
- }
108
- config;
109
- fetchImpl;
110
- get cliVersion() {
111
- return CLI_VERSION;
112
- }
113
- get baseUrl() {
114
- return this.config.baseUrl;
115
- }
116
- async get(path2, params) {
117
- return this.json(await this.raw("GET", path2, { params }));
118
- }
119
- /**
120
- * `actor: false` keeps a configured actor out of the body, for endpoints
121
- * whose body is closed: they refuse a field they do not name, and record
122
- * the credential's own person instead.
123
- */
124
- async post(path2, body = {}, options = {}) {
125
- return this.json(
126
- await this.raw("POST", path2, {
127
- json: options.actor === false ? body : this.withActor(body),
128
- idempotencyKey: options.idempotencyKey
129
- })
130
- );
131
- }
132
- async patch(path2, body) {
133
- return this.json(await this.raw("PATCH", path2, { json: this.withActor(body) }));
134
- }
135
- async put(path2, body) {
136
- return this.json(await this.raw("PUT", path2, { json: this.withActor(body) }));
137
- }
138
- async delete(path2) {
139
- return this.json(await this.raw("DELETE", path2, {}));
140
- }
141
- /**
142
- * Posts multipart/form-data — the one endpoint that does not take JSON.
143
- * Shares auth, the version header, and error mapping with the JSON verbs,
144
- * but never the actor: there is no JSON body to carry an `actor` field,
145
- * and the server's file contract is the form itself.
146
- */
147
- async postMultipart(path2, form, bodyBytes = 0) {
148
- return this.json(await this.raw("POST", path2, { form, timeoutMs: uploadTimeoutMs(bodyBytes) }));
149
- }
150
- /** Binary download (attachments); non-2xx still maps through the JSON error envelope. */
151
- async getBytes(path2) {
152
- const response = await this.raw("GET", path2, {});
153
- if (!response.ok) throw await this.toHttpError(response);
154
- try {
155
- return new Uint8Array(await response.arrayBuffer());
156
- } catch (error) {
157
- throw new TransportError(transportMessage(error), transportCode(error));
158
- }
159
- }
160
- /**
161
- * A GET that also hands back the server's API version header, so a probe
162
- * that verifies a credential reports the version without a second
163
- * request. Errors map as for `get`.
164
- */
165
- async getVersioned(path2) {
166
- const response = await this.raw("GET", path2, {});
167
- const apiVersion = response.headers.get(API_VERSION_HEADER);
168
- return { envelope: await this.json(response), apiVersion };
169
- }
170
- /**
171
- * The server's API version, for `version`'s server side. The header rides
172
- * on every response of a route the server has — the versioning plug runs
173
- * before auth — so even a 401 answers. Each credential probes a route it
174
- * reaches, so the probe is never a refusal in the server's log: a pxc_
175
- * credential its assignments, a login credential of either kind (the
176
- * Operator-only one reaches no tracker read) GET /api/v1/session, and an
177
- * aone_ token the stats. A server older than GET /api/v1/session answers
178
- * that route's 404 without the header, so the stats answer then.
179
- */
180
- async serverVersion() {
181
- const token = this.config.token;
182
- const path2 = isExpertToken(token) ? EXPERT_ASSIGNMENTS_PATH : isLoginCredential(token) ? SESSION_PATH : STATS_PATH;
183
- const version2 = (await this.raw("GET", path2, {})).headers.get(API_VERSION_HEADER);
184
- if (version2 !== null || path2 !== SESSION_PATH) return version2 ?? "unknown";
185
- return (await this.raw("GET", STATS_PATH, {})).headers.get(API_VERSION_HEADER) ?? "unknown";
186
- }
187
- // Only an explicitly configured actor is sent, and never over one the
188
- // caller already put in the body.
189
- withActor(body) {
190
- if (this.config.actor === null || "actor" in body) return body;
191
- return { ...body, actor: this.config.actor };
192
- }
193
- async raw(method, path2, options) {
194
- const url = new URL(this.config.baseUrl + path2);
195
- for (const [key, value] of Object.entries(options.params ?? {})) {
196
- if (value !== void 0) url.searchParams.set(key, String(value));
197
- }
198
- const headers = {
199
- authorization: `Bearer ${this.config.token}`,
200
- "x-harness-cli-version": CLI_VERSION
201
- };
202
- if (options.idempotencyKey !== void 0) {
203
- headers["idempotency-key"] = options.idempotencyKey;
204
- }
205
- let body;
206
- if (options.json !== void 0) {
207
- headers["content-type"] = "application/json";
208
- body = JSON.stringify(options.json);
209
- } else if (options.form !== void 0) {
210
- body = options.form;
211
- }
212
- try {
213
- return await this.fetchImpl(url, {
214
- method,
215
- headers,
216
- body,
217
- signal: AbortSignal.timeout(options.timeoutMs ?? RECEIVE_TIMEOUT_MS),
218
- redirect: "manual"
219
- });
220
- } catch (error) {
221
- throw new TransportError(transportMessage(error), transportCode(error));
222
- }
223
- }
224
- async json(response) {
225
- if (!response.ok) throw await this.toHttpError(response);
226
- let text;
227
- try {
228
- text = await response.text();
229
- } catch (error) {
230
- throw new TransportError(transportMessage(error), transportCode(error));
231
- }
232
- if (text === "") return { data: void 0 };
233
- let parsed;
234
- try {
235
- parsed = JSON.parse(text);
236
- } catch {
237
- throw new MalformedResponseError(`the API answered with a non-JSON body (HTTP ${response.status})`);
238
- }
239
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
240
- throw new MalformedResponseError(`the API answered with an invalid JSON envelope (HTTP ${response.status})`);
241
- }
242
- return parsed;
243
- }
244
- async toHttpError(response) {
245
- let parsed = {};
246
- try {
247
- const candidate = JSON.parse(await response.text());
248
- if (typeof candidate === "object" && candidate !== null) {
249
- parsed = candidate;
250
- }
251
- } catch {
252
- }
253
- const fromBody = typeof parsed["message"] === "string" ? parsed["message"] : null;
254
- const message = fromBody ?? DEFAULT_MESSAGES[response.status] ?? `HTTP ${response.status}`;
255
- if (response.status === 426) return new UpgradeRequiredError(message, parsed);
256
- return new HttpError(response.status, message, parsed, response.headers.get("retry-after"));
257
- }
258
- };
259
- function transportCode(error) {
260
- const cause = error instanceof Error ? error.cause : void 0;
261
- if (typeof cause !== "object" || cause === null) return null;
262
- const code = cause.code;
263
- if (typeof code === "string") return code;
264
- const errors = cause.errors;
265
- if (!Array.isArray(errors)) return null;
266
- const codes = new Set(errors.map((inner) => inner?.code));
267
- const [only] = codes;
268
- return codes.size === 1 && typeof only === "string" ? only : null;
269
- }
270
- function transportMessage(error) {
271
- if (error instanceof Error) {
272
- if (error.name === "TimeoutError" || error.name === "AbortError") return "request timed out";
273
- const cause = error.cause;
274
- if (cause instanceof Error && cause.message !== "") return cause.message;
275
- return error.message;
276
- }
277
- return String(error);
278
- }
279
-
280
- // src/core/output.ts
281
- var TITLE_WIDTH = 70;
282
- var segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
283
- function graphemes(text) {
284
- return [...segmenter.segment(text)].map((piece) => piece.segment);
285
- }
286
- function graphemeLength(text) {
287
- return graphemes(text).length;
288
- }
289
- function padGraphemes(text, width) {
290
- const shortfall = width - graphemeLength(text);
291
- return shortfall > 0 ? text + " ".repeat(shortfall) : text;
292
- }
293
- function clean(value) {
294
- if (value === null || value === void 0) return "";
295
- return String(value).replace(/[\x00-\x1F\x7F\u0080-\u009F]/gu, "");
296
- }
297
- function json(term) {
298
- return JSON.stringify(term) + "\n";
299
- }
300
- var OPERATOR_TEAM_KEY = /^OPERATOR\d*$/;
301
- var OPERATOR_TEAM_NAME = "Operator";
302
- var AGENT_MARK = "[agent]";
303
- var AGENT_LEGEND = `
304
- ${AGENT_MARK} Operator runs this step for its plan; only the plan driver claims it.
305
- `;
306
- function isAgentStep(issue) {
307
- return OPERATOR_TEAM_KEY.test(str(path(issue, "team", "key"))) && str(path(issue, "team", "name")) === OPERATOR_TEAM_NAME;
308
- }
309
- function issuesTable(issues, options = {}) {
310
- if (issues.length === 0) return "No issues.\n";
311
- let marked = false;
312
- const rows = issues.map((issue) => {
313
- const agentStep = options.markAgentSteps === true && isAgentStep(issue);
314
- if (agentStep) marked = true;
315
- return [
316
- str(issue["identifier"]),
317
- str(path(issue, "state", "name")) || "-",
318
- str(issue["priority_label"]) || "-",
319
- str(path(issue, "assignee", "name")) || "-",
320
- agentStep ? truncate(`${AGENT_MARK} ${clean(issue["title"])}`) : truncate(issue["title"])
321
- ];
322
- });
323
- const rendered = table(["ID", "STATE", "PRIORITY", "ASSIGNEE", "TITLE"], rows);
324
- return marked ? rendered + AGENT_LEGEND : rendered;
325
- }
326
- function issueDetail(issue, images = []) {
327
- const header = `${str(issue["identifier"])} \xB7 ${clean(issue["title"])}
328
- `;
329
- const fields = [
330
- ["State", path(issue, "state", "name")],
331
- ["Priority", issue["priority_label"]],
332
- ["Assignee", path(issue, "assignee", "name")],
333
- ["Team", path(issue, "team", "key")],
334
- ["Labels", labelsLine(issue["labels"])],
335
- ["Blocked", blockedLine(issue)],
336
- ["Waiting", waitingLine(issue)],
337
- ["Due", issue["due_date"]],
338
- ["Updated", issue["updated_at"]]
339
- ];
340
- const fieldLines = fields.filter(([, value]) => value !== null && value !== void 0 && value !== "").map(([label, value]) => ` ${(label + ":").padEnd(12)}${clean(value)}
341
- `).join("");
342
- const sections = [
343
- ["Description", issue["description"]],
344
- ["Design", issue["design"]],
345
- ["Acceptance criteria", issue["acceptance_criteria"]],
346
- ["Notes", issue["notes"]]
347
- ];
348
- const sectionText = sections.filter(([, text]) => text !== null && text !== void 0 && text !== "").map(([title, text]) => `
349
- ${title}:
350
- ${indent(String(text))}
351
- `).join("");
352
- return header + fieldLines + sectionText + conditionsSection(issue["conditions"]) + relationsSection(issue["relations"]) + childrenSection(issue["children"]) + commentsSection(issue["comments"]) + imagesSection(images);
353
- }
354
- function activityEntries(entries) {
355
- if (entries.length === 0) return "No activity.\n";
356
- return entries.map(activityEntry).join("\n");
357
- }
358
- function activityEntry(entry) {
359
- switch (entry["kind"]) {
360
- case "comment":
361
- return activityComment(entry);
362
- case "state_event":
363
- return activityStateEvent(entry);
364
- case "assignee_event":
365
- return activityAssigneeEvent(entry);
366
- case "condition_event":
367
- return activityConditionEvent(entry);
368
- case "field_event":
369
- return activityFieldEvent(entry);
370
- default:
371
- throw new CommandError(`Unknown activity entry kind: ${clean(entry["kind"])}`);
372
- }
373
- }
374
- function activityComment(entry) {
375
- const comment2 = requiredRecord(entry["comment"], "activity comment");
376
- const authorValue = comment2["author"];
377
- const author = authorValue === null ? "unknown" : optionalLabel(requiredRecord(authorValue, "comment author")["name"], "unknown", "comment author name");
378
- const deleted = comment2["deleted"];
379
- if (typeof deleted !== "boolean") throw new CommandError("Invalid comment deleted flag.");
380
- const body = activityCommentBody(comment2["body"], deleted);
381
- const text = cleanMultiline(body).split("\n").map((line) => " " + line).join("\n");
382
- return `[${occurredAt(entry)}] ${clean(author)} (${clean(requiredString(comment2["id"], "comment id"))}):
383
- ${text}
384
- `;
385
- }
386
- function activityCommentBody(value, deleted) {
387
- if (deleted) return "[deleted comment]";
388
- if (value === null || value === "") return "[comment has no text]";
389
- return requiredString(value, "comment body");
390
- }
391
- function activityStateEvent(entry) {
392
- const actor = activityWho(entry);
393
- const provenance = activityProvenance(requiredRecord(entry["provenance"], "activity provenance"));
394
- let description;
395
- switch (entry["action"]) {
396
- case "created":
397
- description = `${actor} created the issue in ${stateName(entry["to_state"], "created to_state")}`;
398
- break;
399
- case "transition":
400
- description = `${actor} moved the issue from ${nullableStateName(entry["from_state"], "transition from_state")} to ${stateName(entry["to_state"], "transition to_state")}`;
401
- break;
402
- case "milestone":
403
- description = `${actor} recorded the ${stateName(entry["to_state"], "milestone to_state")} milestone`;
404
- break;
405
- default:
406
- throw new CommandError(`Unknown activity action: ${clean(entry["action"])}`);
407
- }
408
- return `[${occurredAt(entry)}] ${description} \xB7 ${provenance}
409
- `;
410
- }
411
- function activityAssigneeEvent(entry) {
412
- const actor = activityWho(entry);
413
- const provenance = activityProvenance(requiredRecord(entry["provenance"], "activity provenance"));
414
- let description;
415
- switch (entry["action"]) {
416
- case "assigned":
417
- description = `${actor} assigned the issue to ${assigneeName(entry["to_assignee"])}`;
418
- break;
419
- case "reassigned":
420
- description = `${actor} reassigned the issue from ${assigneeName(entry["from_assignee"])} to ${assigneeName(entry["to_assignee"])}`;
421
- break;
422
- case "unassigned":
423
- description = `${actor} unassigned the issue from ${assigneeName(entry["from_assignee"])}`;
424
- break;
425
- default:
426
- throw new CommandError(`Unknown activity action: ${clean(entry["action"])}`);
427
- }
428
- return `[${occurredAt(entry)}] ${description} - ${provenance}
429
- `;
430
- }
431
- function activityConditionEvent(entry) {
432
- const actor = activityWho(entry);
433
- const provenance = activityProvenance(requiredRecord(entry["provenance"], "activity provenance"));
434
- const condition = conditionSubject(requiredRecord(entry["condition"], "activity condition"));
435
- let description;
436
- switch (entry["action"]) {
437
- case "added":
438
- description = `${actor} added ${condition}`;
439
- break;
440
- case "changed":
441
- description = `${actor} changed ${condition}`;
442
- break;
443
- case "removed":
444
- description = `${actor} removed ${condition}`;
445
- break;
446
- case "satisfied":
447
- description = `${actor} satisfied ${condition}`;
448
- break;
449
- case "unsatisfied":
450
- description = `${actor} marked ${condition} unsatisfied`;
451
- break;
452
- default:
453
- throw new CommandError(`Unknown activity action: ${clean(entry["action"])}`);
454
- }
455
- return `[${occurredAt(entry)}] ${description} \xB7 ${provenance}
456
- `;
457
- }
458
- function conditionSubject(condition) {
459
- const label = clean(condition["label"]);
460
- switch (condition["kind"]) {
461
- case "gate":
462
- return label === "" ? "the gate" : `the gate "${label}"`;
463
- case "not_before": {
464
- const at = clean(condition["not_before"]);
465
- return at === "" ? "the not-before date" : `the not-before date ${at}`;
466
- }
467
- case "event":
468
- return label === "" ? "the wait condition" : `the system event "${label}"`;
469
- default:
470
- return "the wait condition";
471
- }
472
- }
473
- function activityFieldEvent(entry) {
474
- const actor = activityWho(entry);
475
- const provenance = activityProvenance(requiredRecord(entry["provenance"], "activity provenance"));
476
- const change = fieldChange(requiredString(entry["field"], "activity field"), entry["from"], entry["to"]);
477
- return `[${occurredAt(entry)}] ${actor} ${change} \xB7 ${provenance}
478
- `;
479
- }
480
- function fieldChange(field3, from, to) {
481
- switch (field3) {
482
- case "priority":
483
- return `changed priority from ${priorityName(from)} to ${priorityName(to)}`;
484
- case "title":
485
- return `renamed the issue from \u201C${activityTitle(from)}\u201D to \u201C${activityTitle(to)}\u201D`;
486
- case "description":
487
- return "edited the description";
488
- case "labels":
489
- return labelsChange(to);
490
- case "parent":
491
- return parentChange(from, to);
492
- case "archived":
493
- if (typeof to !== "boolean") throw new CommandError("Invalid activity archived value.");
494
- return to ? "archived the issue" : "unarchived the issue";
495
- default:
496
- return `changed the ${clean(field3)} field`;
497
- }
498
- }
499
- var ACTIVITY_PRIORITIES = {
500
- 0: "No priority",
501
- 1: "Urgent",
502
- 2: "High",
503
- 3: "Medium",
504
- 4: "Low"
505
- };
506
- function priorityName(value) {
507
- if (typeof value !== "number" || !Number.isInteger(value)) throw new CommandError("Invalid activity priority.");
508
- return ACTIVITY_PRIORITIES[value] ?? String(value);
509
- }
510
- function activityTitle(value) {
511
- if (typeof value !== "string") throw new CommandError("Invalid activity title.");
512
- return clean(value);
513
- }
514
- function labelsChange(to) {
515
- const change = requiredRecord(to, "activity labels");
516
- const added = labelNames(change["added"]);
517
- const removed = labelNames(change["removed"]);
518
- const parts = [
519
- ...added.length > 0 ? [`added ${labelList(added)}`] : [],
520
- ...removed.length > 0 ? [`removed ${labelList(removed)}`] : []
521
- ];
522
- return parts.length === 0 ? "changed the labels" : parts.join(" and ");
523
- }
524
- function labelNames(value) {
525
- if (!Array.isArray(value)) throw new CommandError("Invalid activity labels.");
526
- return value.map((label) => clean(requiredString(requiredRecord(label, "activity labels")["name"], "activity labels")));
527
- }
528
- function labelList(names) {
529
- return `${names.length === 1 ? "label" : "labels"} ${names.join(", ")}`;
530
- }
531
- function parentChange(from, to) {
532
- if (from === null && to === null) return "changed the parent";
533
- if (from === null) return `set the parent to ${parentName(to)}`;
534
- if (to === null) return `removed the parent ${parentName(from)}`;
535
- return `changed the parent from ${parentName(from)} to ${parentName(to)}`;
536
- }
537
- function parentName(value) {
538
- const parent = requiredRecord(value, "activity parent");
539
- return optionalLabel(parent["identifier"], "an unknown issue", "activity parent identifier");
540
- }
541
- function activityWho(entry) {
542
- const actor = activityActor(requiredRecord(entry["actor"], "activity actor"));
543
- const label = entry["actor_label"];
544
- if (label === void 0 || label === null || label === "") return actor;
545
- if (typeof label !== "string") throw new CommandError("Invalid activity actor label.");
546
- const cleaned = clean(label);
547
- return cleaned === "" ? actor : `${actor} via \u201C${cleaned}\u201D`;
548
- }
549
- function occurredAt(entry) {
550
- return clean(requiredString(entry["occurred_at"], "activity occurrence time"));
551
- }
552
- function assigneeName(value) {
553
- const assignee = requiredRecord(value, "activity assignee");
554
- if (assignee["kind"] === "unknown") return "Unknown assignee";
555
- return activityActor(assignee);
556
- }
557
- function activityActor(actor) {
558
- switch (actor["kind"]) {
559
- case "user":
560
- return optionalLabel(actor["name"], "Unknown actor", "activity actor name");
561
- case "system":
562
- return "System";
563
- case "deleted_user":
564
- return "Deleted user";
565
- case "unknown":
566
- return "Unknown actor";
567
- default:
568
- throw new CommandError(`Unknown activity actor kind: ${clean(actor["kind"])}`);
569
- }
570
- }
571
- function activityProvenance(provenance) {
572
- switch (provenance["kind"]) {
573
- case "live":
574
- case "import":
575
- case "backfill":
576
- return String(provenance["kind"]);
577
- default:
578
- throw new CommandError(`Unknown activity provenance: ${clean(provenance["kind"])}`);
579
- }
580
- }
581
- function stateName(value, field3) {
582
- return optionalLabel(requiredRecord(value, field3)["name"], "Unknown state", `${field3} name`);
583
- }
584
- function nullableStateName(value, field3) {
585
- return value === null ? "Unknown state" : stateName(value, field3);
586
- }
587
- function optionalLabel(value, fallback, field3) {
588
- if (value === null || value === "") return fallback;
589
- return clean(requiredString(value, field3));
590
- }
591
- function requiredRecord(value, field3) {
592
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
593
- throw new CommandError(`Invalid ${field3}.`);
594
- }
595
- return value;
596
- }
597
- function requiredString(value, field3) {
598
- if (typeof value !== "string" || value === "") throw new CommandError(`Invalid ${field3}.`);
599
- return value;
600
- }
601
- function stats(data) {
602
- const teams = data["teams"] ?? [];
603
- const totals = data["totals"] ?? {};
604
- const rows = teams.map((team) => [
605
- str(path(team, "team", "key")),
606
- str(team["open"]),
607
- str(team["started"]),
608
- str(team["blocked"]),
609
- str(team["ready"]),
610
- str(team["closed"])
611
- ]);
612
- const totalsRow = ["TOTAL", ...["open", "started", "blocked", "ready", "closed"].map((key) => str(totals[key]))];
613
- return table(["TEAM", "OPEN", "STARTED", "BLOCKED", "READY", "CLOSED"], [...rows, totalsRow]);
614
- }
615
- function table(headers, rows) {
616
- const all = [headers, ...rows.map((row) => row.map((cell) => clean(cell)))];
617
- const widths = headers.map((_, column) => Math.max(...all.map((row) => graphemeLength(row[column] ?? ""))));
618
- return all.map(
619
- (row) => row.map((cell, column) => padGraphemes(cell, widths[column] ?? 0)).join(" ").replace(/\s+$/, "") + "\n"
620
- ).join("");
621
- }
622
- function records(value) {
623
- if (!Array.isArray(value)) return [];
624
- return value.filter(
625
- (entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry)
626
- );
627
- }
628
- function waitingLine(issue) {
629
- if (issue["waiting_on_condition"] !== true) return null;
630
- const conditions = records(issue["conditions"]);
631
- const open = conditions.filter((condition) => condition["satisfied"] !== true).length;
632
- return `yes, on ${open} of ${conditions.length} wait condition${conditions.length === 1 ? "" : "s"}`;
633
- }
634
- function conditionsSection(conditions) {
635
- const list3 = records(conditions);
636
- if (list3.length === 0) return "";
637
- return "\nWait conditions:\n" + list3.map((condition) => ` ${conditionLine(condition)}
638
- `).join("");
639
- }
640
- function conditionLine(condition, options = {}) {
641
- const satisfied = condition["satisfied"] === true;
642
- let what;
643
- switch (condition["kind"]) {
644
- case "gate": {
645
- const owner = condition["owner"];
646
- let ownedBy = "";
647
- if (typeof owner === "object" && owner !== null) {
648
- const record = owner;
649
- ownedBy = options.expertView === true && record["kind"] === "expert" ? "you" : ownerText(record);
650
- }
651
- what = `gate ${gateName(condition)}${ownedBy === "" ? "" : ` owned by ${ownedBy}`}`;
652
- break;
653
- }
654
- case "not_before":
655
- what = `not before ${clean(condition["not_before"])}`;
656
- break;
657
- case "event":
658
- what = "system event";
659
- break;
660
- default:
661
- what = clean(condition["kind"]) || "condition";
662
- }
663
- let when = "";
664
- if (satisfied && typeof condition["satisfied_at"] === "string") {
665
- let by = clean(path(condition, "satisfied_by", "name"));
666
- if (by === "" && condition["satisfied_by_kind"] === "expert") by = "an outside expert";
667
- let role = typeof condition["satisfied_as_role"] === "string" ? roleName(condition["satisfied_as_role"]) : "";
668
- switch (path(condition, "satisfied_by", "kind")) {
669
- case "you":
670
- by = "you";
671
- break;
672
- case "role": {
673
- const as = path(condition, "satisfied_by", "role");
674
- if (typeof as === "string" && as !== "") role = roleName(as);
675
- break;
676
- }
677
- case "expert":
678
- by = "an outside expert";
679
- break;
680
- case "member":
681
- by = "a member";
682
- break;
683
- }
684
- when = ` (satisfied ${clean(condition["satisfied_at"])}${by === "" ? "" : ` by ${by}`}${role === "" ? "" : ` as ${role}`})`;
685
- }
686
- return `${satisfied ? "[x]" : "[ ]"} ${what}${when} \xB7 ${clean(condition["id"])}`;
687
- }
688
- function gateName(gate) {
689
- const key = clean(gate["key"]);
690
- return `"${clean(gate["label"])}"${key === "" ? "" : ` [${key}]`}`;
691
- }
692
- function ownerText(owner) {
693
- const label = clean(owner["label"]) || (owner["kind"] === "role" && typeof owner["role"] === "string" ? roleName(owner["role"]) : "");
694
- const ref = clean(owner["ref"]);
695
- if (owner["kind"] === "expert") {
696
- const email = label || ref.replace(/^expert:/, "");
697
- return email === "" ? "" : `${email} (expert)`;
698
- }
699
- if (label === "") return ref;
700
- return ref === "" || ref === label ? label : `${label} (${ref})`;
701
- }
702
- function expertSummary(seat) {
703
- const role = typeof seat.role === "string" && seat.role !== "" ? roleName(seat.role) : "Outside expert";
704
- const status = clean(seat.plan?.status);
705
- const plan = seat.plan === null ? ", no plan" : ` on plan ${clean(seat.plan.id)}${status === "" ? "" : ` (${status})`}`;
706
- const steps = seat.assigned_steps;
707
- const count = steps === 0 ? "no assigned steps" : `${steps} assigned step${steps === 1 ? "" : "s"}`;
708
- return `${role}${plan} \xB7 ${count}`;
709
- }
710
- function expertAssignments(steps) {
711
- return steps.map((step) => "\n" + assignedStep(step)).join("");
712
- }
713
- function assignedStep(step) {
714
- const blockers = typeof step["open_blockers"] === "number" ? step["open_blockers"] : 0;
715
- const fields = [
716
- ["State", path(step, "state", "name")],
717
- ["Due", step["due_date"]],
718
- ["Blocked", blockers > 0 ? `yes, by ${blockers} open issue${blockers === 1 ? "" : "s"}` : null]
719
- ];
720
- const fieldLines = fields.filter(([, value]) => value !== null && value !== void 0 && value !== "").map(([label, value]) => ` ${(label + ":").padEnd(12)}${clean(value)}
721
- `).join("");
722
- const gates = records(step["gates"]);
723
- const gateLines = gates.length === 0 ? " Gates: none of yours\n" : " Gates:\n" + gates.map((gate) => ` ${conditionLine(gate, { expertView: true })}
724
- `).join("");
725
- const description = step["description"];
726
- const descriptionLines = typeof description === "string" && description !== "" ? " Description:\n" + indent(indent(description)) + "\n" : "";
727
- return `${clean(step["identifier"])} \xB7 ${clean(step["title"])}
728
- ${fieldLines}${gateLines}${descriptionLines}`;
729
- }
730
- var ROLE_NAMES = { gp: "GP" };
731
- function roleName(role) {
732
- const slug = clean(role);
733
- if (Object.hasOwn(ROLE_NAMES, slug)) return ROLE_NAMES[slug];
734
- const words = slug.replace(/_/g, " ");
735
- return words.charAt(0).toUpperCase() + words.slice(1);
736
- }
737
- var LIVE_PLAN_STATUSES = ["released", "running", "needs_attention"];
738
- function planDetail(plan) {
739
- const steps = records(plan["steps"]);
740
- const fields = [
741
- ["Root", planRootLine(plan["root"])],
742
- ["Revision", planRevisionLine(plan)],
743
- ["Budgets", planBudgetsLine(plan)],
744
- ["Estimate", planEstimateLine(plan["estimate"])]
745
- ];
746
- const fieldLines = fields.filter((field3) => field3[1] !== null).map(([label, value]) => ` ${(label + ":").padEnd(12)}${value}
747
- `).join("");
748
- return `Plan ${clean(plan["id"])} \xB7 ${clean(plan["status"])}
749
- ` + fieldLines + planStepsSection(steps, str(plan["status"])) + planEdgesSection(plan["edges"]) + planConditionsSection(steps) + planWaitingSection(steps) + planHeldSection(plan["held_steps"]);
750
- }
751
- function planRootLine(root) {
752
- if (typeof root !== "object" || root === null) return null;
753
- const record = root;
754
- return `${clean(record["identifier"])} \xB7 ${clean(record["title"])}`;
755
- }
756
- function planRevisionLine(plan) {
757
- const revision = plan["revision"];
758
- if (typeof revision !== "object" || revision === null) return "none yet (never released)";
759
- const number = clean(revision["number"]);
760
- const by = clean(path(plan, "released_by", "name"));
761
- const at = clean(plan["released_at"]);
762
- if (by === "" && at === "") return number;
763
- return `${number} \xB7 released${by === "" ? "" : ` by ${by}`}${at === "" ? "" : ` at ${at}`}`;
764
- }
765
- function planBudgetsLine(plan) {
766
- const retries = plan["retry_budget"];
767
- const retryText = typeof retries === "number" ? `${retries} ${retries === 1 ? "retry" : "retries"} per failed Operator step` : null;
768
- const spend = plan["spend_budget_cents"];
769
- const spendText = typeof spend === "number" ? `spend budget ${spend} cents` : "no spend budget";
770
- return [retryText, spendText].filter((part) => part !== null).join(" \xB7 ");
771
- }
772
- function planEstimateLine(estimate) {
773
- if (typeof estimate !== "object" || estimate === null) return null;
774
- const record = estimate;
775
- const ceiling = record["ceiling_cents"];
776
- if (typeof ceiling !== "number") return null;
777
- const minimum = record["minimum_budget_cents"];
778
- return `up to ${ceiling} cents${typeof minimum === "number" ? ` (minimum budget ${minimum} cents)` : ""}`;
779
- }
780
- function planStepsSection(steps, planStatus) {
781
- if (steps.length === 0) return "";
782
- const depths = stepDepths(steps);
783
- const rows = steps.map((step) => [
784
- " ".repeat(depths.get(str(step["identifier"])) ?? 0) + str(step["identifier"]),
785
- str(step["key"]) || "-",
786
- str(step["executor"]) || "-",
787
- str(path(step, "state", "name")) || "-",
788
- stepStatus(step, planStatus),
789
- truncate(step["title"])
790
- ]);
791
- const rendered = table(["STEP", "KEY", "EXECUTOR", "STATE", "STATUS", "TITLE"], rows);
792
- return "\nSteps:\n" + indentLines(rendered);
793
- }
794
- function stepDepths(steps) {
795
- const parents = new Map(steps.map((step) => [str(step["identifier"]), str(step["parent"])]));
796
- const depths = /* @__PURE__ */ new Map();
797
- for (const identifier of parents.keys()) {
798
- let depth = 0;
799
- let parent = parents.get(identifier) ?? "";
800
- const seen = /* @__PURE__ */ new Set([identifier]);
801
- while (parent !== "" && parents.has(parent) && !seen.has(parent)) {
802
- seen.add(parent);
803
- depth += 1;
804
- parent = parents.get(parent) ?? "";
805
- }
806
- depths.set(identifier, depth);
807
- }
808
- return depths;
809
- }
810
- function stepStatus(step, planStatus) {
811
- const stateType = path(step, "state", "type");
812
- if (step["archived"] === true) return "archived";
813
- if (stateType === "completed") return "done";
814
- if (stateType === "canceled") return "cancelled";
815
- if (step["container"] === true) return "container";
816
- if (step["ready"] === true) return "ready";
817
- if (step["held"] === true) return "held";
818
- if (step["in_revision"] === false && LIVE_PLAN_STATUSES.includes(planStatus)) return "not in revision";
819
- if (records(step["conditions"]).some((condition) => condition["satisfied"] !== true)) return "waiting";
820
- return "-";
821
- }
822
- function planEdgesSection(edges) {
823
- const list3 = records(edges);
824
- if (list3.length === 0) return "";
825
- return "\nEdges (blocker \u2192 blocked):\n" + list3.map((edge) => ` ${clean(edge["from"])} \u2192 ${clean(edge["to"])}
826
- `).join("");
827
- }
828
- function planConditionsSection(steps) {
829
- const lines = steps.flatMap(
830
- (step) => records(step["conditions"]).map((condition) => [clean(step["identifier"]), conditionLine(condition)])
831
- );
832
- if (lines.length === 0) return "";
833
- const width = Math.max(...lines.map(([identifier]) => graphemeLength(identifier)));
834
- return "\nWait conditions:\n" + lines.map(([identifier, line]) => ` ${padGraphemes(identifier, width)} ${line}
835
- `).join("");
836
- }
837
- function planWaitingSection(steps) {
838
- const lines = steps.flatMap(
839
- (step) => records(step["waiting_for"]).map((wait) => [clean(step["identifier"]), waitLine(wait)])
840
- );
841
- if (lines.length === 0) return "";
842
- const width = Math.max(...lines.map(([identifier]) => graphemeLength(identifier)));
843
- return "\nWaiting for:\n" + lines.map(([identifier, line]) => ` ${padGraphemes(identifier, width)} ${line}
844
- `).join("");
845
- }
846
- function waitLine(wait) {
847
- const name = gateName(wait);
848
- const owner = wait["owner"];
849
- if (typeof owner !== "object" || owner === null) return `${name} \xB7 no owner, anyone with standing can tick`;
850
- const holders = wait["holders"];
851
- let who;
852
- if (!Array.isArray(holders)) who = "who can tick is not known";
853
- else if (holders.length === 0) who = "nobody can tick";
854
- else who = `can tick: ${records(holders).map(holderText).join(", ")}`;
855
- return `${name} \xB7 ${ownerText(owner)} \xB7 ${who}`;
856
- }
857
- function holderText(holder) {
858
- const name = clean(holder["name"]);
859
- if (holder["kind"] !== "expert") return name;
860
- const organization = clean(holder["organization"]);
861
- return `${name} (expert${organization === "" ? "" : `, ${organization}`})`;
862
- }
863
- function planHeldSection(heldSteps) {
864
- if (!Array.isArray(heldSteps) || heldSteps.length === 0) return "";
865
- return `
866
- Held by an edit until the plan is released again: ${heldSteps.map((step) => clean(step)).join(", ")}
867
- `;
868
- }
869
- function indentLines(text) {
870
- return text.split("\n").map((line) => line === "" ? line : " " + line).join("\n");
871
- }
872
- function errorDetails(body) {
873
- return changesetDetails(body["details"]) + violationDetails(body["violations"]);
874
- }
875
- function changesetDetails(details) {
876
- if (typeof details !== "object" || details === null || Array.isArray(details)) return "";
877
- return Object.entries(details).map(([field3, messages]) => {
878
- const text = Array.isArray(messages) ? messages.map(String).join("; ") : String(messages);
879
- return `
880
- ${clean(field3)}: ${clean(text)}`;
881
- }).join("");
882
- }
883
- function violationDetails(violations) {
884
- return records(violations).map((violation) => {
885
- const code = clean(violation["code"]);
886
- const message = clean(violation["message"]) || code || "invalid";
887
- return `
888
- ${violationSubject(violation)}: ${message}${code === "" ? "" : ` [${code}]`}`;
889
- }).join("");
890
- }
891
- function violationSubject(violation) {
892
- const key = typeof violation["key"] === "string" ? clean(violation["key"]) : null;
893
- const index = typeof violation["index"] === "number" ? `steps[${violation["index"]}]` : null;
894
- const issue = typeof violation["issue"] === "string" ? clean(violation["issue"]) : null;
895
- if (key !== null) {
896
- const where = index ?? issue;
897
- return where === null ? `step "${key}"` : `step "${key}" (${where})`;
898
- }
899
- return issue ?? index ?? "plan";
900
- }
901
- function relationsSection(relations) {
902
- if (!Array.isArray(relations) || relations.length === 0) return "";
903
- const lines = relations.map((relation) => ` ${str(relation["direction"]).padEnd(14)}${str(relation["other_issue"])}
904
- `).join("");
905
- return "\nRelations:\n" + lines;
906
- }
907
- function childrenSection(children) {
908
- if (!Array.isArray(children) || children.length === 0) return "";
909
- const lines = children.map((child) => {
910
- const state = str(path(child, "state", "name")) || "-";
911
- return ` ${str(child["identifier"])} [${state}] ${truncate(child["title"])}
912
- `;
913
- }).join("");
914
- return "\nSub-issues:\n" + lines;
915
- }
916
- function commentsSection(comments2) {
917
- if (!Array.isArray(comments2) || comments2.length === 0) return "";
918
- const lines = comments2.map((comment2) => {
919
- const author = clean(path(comment2, "author", "name") ?? "unknown");
920
- return ` [${str(comment2["inserted_at"])}] ${author}: ${truncate(comment2["body"])}
921
- `;
922
- }).join("");
923
- return "\nComments:\n" + lines;
924
- }
925
- function imagesSection(images) {
926
- if (images.length === 0) return "";
927
- const lines = images.map((image) => ` [${image["source"]}] ${clean(image["path"])}
928
- `).join("");
929
- return "\nImages (attachments downloaded locally \u2014 read them, they are part of the issue):\n" + lines;
930
- }
931
- function labelsLine(labels) {
932
- if (!Array.isArray(labels) || labels.length === 0) return null;
933
- return labels.map((label) => clean(label["name"])).join(", ");
934
- }
935
- function blockedLine(issue) {
936
- if (issue["blocked"] === true && Array.isArray(issue["blocked_by"])) {
937
- const notes = blockerNotes(issue["blockers"]);
938
- const blockers = issue["blocked_by"].map((blocker) => {
939
- const identifier = String(blocker);
940
- const note = notes.get(identifier);
941
- return note === void 0 ? identifier : `${identifier} (${note})`;
942
- });
943
- return `yes, by ${blockers.join(", ")}`;
944
- }
945
- return null;
946
- }
947
- function blockerNotes(blockers) {
948
- const notes = /* @__PURE__ */ new Map();
949
- if (!Array.isArray(blockers)) return notes;
950
- for (const blocker of blockers) {
951
- if (typeof blocker !== "object" || blocker === null) continue;
952
- const record = blocker;
953
- if (typeof record["identifier"] !== "string") continue;
954
- const reasons = [];
955
- if (record["state_type"] === "canceled") reasons.push("cancelled");
956
- if (record["archived"] === true) reasons.push("archived");
957
- if (reasons.length > 0) notes.set(record["identifier"], reasons.join(", "));
958
- }
959
- return notes;
960
- }
961
- function indent(text) {
962
- return cleanMultiline(text).split("\n").map((line) => " " + line).join("\n");
963
- }
964
- function cleanMultiline(text) {
965
- return text.split("\n").map((line) => clean(line)).join("\n");
966
- }
967
- function truncate(text) {
968
- if (text === null || text === void 0) return "";
969
- const cleaned = clean(text);
970
- const pieces = graphemes(cleaned);
971
- if (pieces.length > TITLE_WIDTH) {
972
- return pieces.slice(0, TITLE_WIDTH - 1).join("") + "\u2026";
973
- }
974
- return cleaned;
975
- }
976
- function str(value) {
977
- return value === null || value === void 0 ? "" : String(value);
978
- }
979
- function path(record, ...keys) {
980
- let current = record;
981
- for (const key of keys) {
982
- if (typeof current !== "object" || current === null) return null;
983
- current = current[key];
984
- }
985
- return current ?? null;
986
- }
37
+ } from "./chunk-5THYIKLW.js";
987
38
 
988
39
  // src/commands/issues.ts
989
40
  import { openAsBlob, openSync, closeSync, readFileSync, statSync as statSync2 } from "fs";
@@ -1051,14 +102,14 @@ async function fetchImages(client, issue, cacheDir) {
1051
102
  return { images, failures };
1052
103
  }
1053
104
  async function download(client, ref, cacheDir) {
1054
- const path2 = join(cacheDir, hostSegment(client.baseUrl), ref.attachmentId, ref.file);
105
+ const path = join(cacheDir, hostSegment(client.baseUrl), ref.attachmentId, ref.file);
1055
106
  try {
1056
- if (statSync(path2).size > 0) return { ok: true, path: path2 };
107
+ if (statSync(path).size > 0) return { ok: true, path };
1057
108
  } catch {
1058
109
  }
1059
110
  const bytes = await fetchBytes(client, ref);
1060
111
  if (!bytes.ok) return bytes;
1061
- return write(path2, bytes.bytes);
112
+ return write(path, bytes.bytes);
1062
113
  }
1063
114
  function hostSegment(baseUrl) {
1064
115
  let host = "unknown-host";
@@ -1098,25 +149,25 @@ function isImage(bytes) {
1098
149
  return false;
1099
150
  }
1100
151
  var partialCounter = 0;
1101
- function write(path2, bytes) {
1102
- const partial = `${path2}.partial.${process.pid}.${++partialCounter}`;
152
+ function write(path, bytes) {
153
+ const partial = `${path}.partial.${process.pid}.${++partialCounter}`;
1103
154
  try {
1104
- mkdirSync(dirname(path2), { recursive: true });
1105
- chmodSync(dirname(path2), 448);
155
+ mkdirSync(dirname(path), { recursive: true });
156
+ chmodSync(dirname(path), 448);
1106
157
  writeFileSync(partial, bytes);
1107
158
  chmodSync(partial, 384);
1108
- renameSync(partial, path2);
1109
- return { ok: true, path: path2 };
159
+ renameSync(partial, path);
160
+ return { ok: true, path };
1110
161
  } catch (error) {
1111
162
  rmSync(partial, { force: true });
1112
163
  const detail = error instanceof Error ? error.message : String(error);
1113
- return { ok: false, message: `cannot write ${path2}: ${detail}` };
164
+ return { ok: false, message: `cannot write ${path}: ${detail}` };
1114
165
  }
1115
166
  }
1116
167
 
1117
168
  // src/core/pagination.ts
1118
- function notices(meta, requestedPageSize, records5) {
1119
- return capNotice(meta, requestedPageSize) + truncationNotice(meta, records5);
169
+ function notices(meta, requestedPageSize, records4) {
170
+ return capNotice(meta, requestedPageSize) + truncationNotice(meta, records4);
1120
171
  }
1121
172
  function capNotice(meta, requestedPageSize) {
1122
173
  const served = meta?.page_size;
@@ -1126,12 +177,12 @@ function capNotice(meta, requestedPageSize) {
1126
177
  }
1127
178
  return "";
1128
179
  }
1129
- function truncationNotice(meta, records5) {
180
+ function truncationNotice(meta, records4) {
1130
181
  const total = meta?.total;
1131
182
  const page = meta?.page;
1132
183
  const pages = meta?.total_pages;
1133
184
  if (typeof total !== "number" || typeof page !== "number" || typeof pages !== "number") return "";
1134
- const shown = records5.length;
185
+ const shown = records4.length;
1135
186
  if (shown < total) {
1136
187
  return `harness: showing ${shown} of ${total} (page ${page} of ${pages}) \u2014 pass --all for the rest, or --page to step through.
1137
188
  `;
@@ -1142,20 +193,20 @@ async function fetchEveryPage(fetchPage) {
1142
193
  const first = await fetchPage(1);
1143
194
  const totalPages = pageCount(first.meta);
1144
195
  if (totalPages === null) return first.data;
1145
- let records5 = first.data;
196
+ let records4 = first.data;
1146
197
  for (let page = 2; page <= totalPages; page++) {
1147
198
  const body = await fetchPage(page);
1148
- records5 = dedupe([...records5, ...body.data]);
199
+ records4 = dedupe([...records4, ...body.data]);
1149
200
  }
1150
- return records5;
201
+ return records4;
1151
202
  }
1152
203
  function pageCount(meta) {
1153
204
  const total = meta?.total_pages;
1154
205
  return typeof total === "number" && total > 0 ? total : null;
1155
206
  }
1156
- function dedupe(records5) {
207
+ function dedupe(records4) {
1157
208
  const seen = /* @__PURE__ */ new Set();
1158
- return records5.filter((record) => {
209
+ return records4.filter((record) => {
1159
210
  const id = record["id"];
1160
211
  const key = id === void 0 || id === null ? `struct:${stableKey(record)}` : `id:${stableKey(id)}`;
1161
212
  if (seen.has(key)) return false;
@@ -1185,6 +236,9 @@ var UUID_FORMAT = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}
1185
236
  function isUuid(value) {
1186
237
  return UUID_FORMAT.test(value);
1187
238
  }
239
+ function isIdentifier(value) {
240
+ return IDENTIFIER_FORMAT.test(value);
241
+ }
1188
242
  function requireIdentifiers(identifiers) {
1189
243
  const bad = identifiers.filter((identifier) => !IDENTIFIER_FORMAT.test(identifier));
1190
244
  if (bad.length > 0) {
@@ -1194,9 +248,9 @@ function requireIdentifiers(identifiers) {
1194
248
  async function ready(ctx, opts) {
1195
249
  if (opts.claim === true) return claimNext(ctx, opts);
1196
250
  const params = { ...listParams(ctx, opts), ready: true };
1197
- const { records: records5, notice } = await fetchIssues(ctx, params, opts);
251
+ const { records: records4, notice } = await fetchIssues(ctx, params, opts);
1198
252
  const rank = (issue) => PRIORITY_RANK[issue["priority"]] ?? 4;
1199
- const sorted = markAgentSteps([...records5].sort((a, b) => rank(a) - rank(b)));
253
+ const sorted = markAgentSteps([...records4].sort((a, b) => rank(a) - rank(b)));
1200
254
  return { json: sorted, human: issuesTable(sorted, { markAgentSteps: true }), notice };
1201
255
  }
1202
256
  function markAgentSteps(issues) {
@@ -1240,7 +294,7 @@ async function claimNext(ctx, opts) {
1240
294
  if (!isIssueRecord(claimed)) throw unreadable("carried neither an issue nor null");
1241
295
  return {
1242
296
  json: claimed,
1243
- human: `Claimed ${str2(claimed["identifier"])}: ${clean(claimed["title"])}
297
+ human: `Claimed ${str(claimed["identifier"])}: ${clean(claimed["title"])}
1244
298
  `
1245
299
  };
1246
300
  }
@@ -1251,13 +305,13 @@ function isIssueRecord(value) {
1251
305
  }
1252
306
  async function blocked(ctx, opts) {
1253
307
  const params = { ...listParams(ctx, opts), blocked: true };
1254
- const { records: records5, notice } = await fetchIssues(ctx, params, opts);
1255
- return { json: records5, human: issuesTable(records5), notice };
308
+ const { records: records4, notice } = await fetchIssues(ctx, params, opts);
309
+ return { json: records4, human: issuesTable(records4), notice };
1256
310
  }
1257
311
  async function list(ctx, opts) {
1258
312
  const params = listParams(ctx, opts);
1259
- const { records: records5, notice } = await fetchIssues(ctx, params, opts);
1260
- return { json: records5, human: issuesTable(records5), notice };
313
+ const { records: records4, notice } = await fetchIssues(ctx, params, opts);
314
+ return { json: records4, human: issuesTable(records4), notice };
1261
315
  }
1262
316
  async function search(ctx, queryWords, opts) {
1263
317
  if (queryWords.length === 0) throw new UsageError("usage: harness search <query>");
@@ -1281,11 +335,11 @@ async function show(ctx, identifier, opts) {
1281
335
  if (opts.images !== false) {
1282
336
  ({ images, failures } = await fetchImages(ctx.client, issue, ctx.config.cacheDir));
1283
337
  }
1284
- const json2 = images.length === 0 ? issue : { ...issue, local_images: images };
338
+ const json = images.length === 0 ? issue : { ...issue, local_images: images };
1285
339
  const human = issueDetail(issue, images);
1286
340
  const notice = failures.map((failure) => `harness: image not downloaded: ${clean(failure)}
1287
341
  `).join("");
1288
- return notice === "" ? { json: json2, human } : { json: json2, human, notice };
342
+ return notice === "" ? { json, human } : { json, human, notice };
1289
343
  }
1290
344
  var ACTIVITY_MAX_LIMIT = 100;
1291
345
  var ACTIVITY_INCLUDE = "condition_events,field_events";
@@ -1375,7 +429,7 @@ async function stats2(ctx) {
1375
429
  async function prime(ctx) {
1376
430
  const statsBody = await ctx.client.get("/api/v1/stats");
1377
431
  const ready2 = markAgentSteps(await mostImportantReady(ctx));
1378
- const json2 = { stats: statsBody.data, ready: ready2 };
432
+ const json = { stats: statsBody.data, ready: ready2 };
1379
433
  const human = "# Harness workflow context\n\n" + stats(statsBody.data) + "\nReady to work:\n" + issuesTable(ready2, { markAgentSteps: true }) + named(`
1380
434
  Workflow: harness ready \u2192 harness update <id> --claim \u2192 work \u2192
1381
435
  harness close <id> --reason "Implemented in <commit-hash>" (response
@@ -1384,7 +438,7 @@ Create tracked work BEFORE coding: harness create --title "\u2026" --type task.
1384
438
  Dependencies: harness dep add <issue> <blocker> (blocker blocks issue).
1385
439
  Before reporting done: close the issues you finished.
1386
440
  `);
1387
- return { json: json2, human };
441
+ return { json, human };
1388
442
  }
1389
443
  async function mostImportantReady(ctx) {
1390
444
  try {
@@ -1431,7 +485,7 @@ async function create(ctx, opts) {
1431
485
  const issue = created.data;
1432
486
  return {
1433
487
  json: issue,
1434
- human: `Created ${str2(issue["identifier"])}: ${clean(issue["title"])}
488
+ human: `Created ${str(issue["identifier"])}: ${clean(issue["title"])}
1435
489
  `
1436
490
  };
1437
491
  }
@@ -1484,7 +538,7 @@ async function update(ctx, identifier, opts) {
1484
538
  if (opts.appendNotes !== void 0) {
1485
539
  const current = await ctx.client.get(`/api/v1/issues/${identifier}`);
1486
540
  const existing = current.data["notes"];
1487
- const combined = existing === null || existing === void 0 || existing === "" ? opts.appendNotes : `${str2(existing)}
541
+ const combined = existing === null || existing === void 0 || existing === "" ? opts.appendNotes : `${str(existing)}
1488
542
 
1489
543
  ${opts.appendNotes}`;
1490
544
  patch["notes"] = combined;
@@ -1508,24 +562,48 @@ ${opts.appendNotes}`;
1508
562
  if (issue === null) {
1509
563
  throw new UsageError("usage: harness update <id> --claim | --title \u2026 | --state <name> | \u2026");
1510
564
  }
1511
- const human = wrote ? `Updated ${str2(issue["identifier"])}
1512
- ` : `Labels already up to date for ${str2(issue["identifier"])}
565
+ const human = wrote ? `Updated ${str(issue["identifier"])}
566
+ ` : `Labels already up to date for ${str(issue["identifier"])}
1513
567
  `;
1514
568
  return { json: issue, human };
1515
569
  }
1516
570
  var CLAIM_REFUSAL_HINTS = {
1517
- // The edit holds a released step, so the plan needs releasing again before
1518
- // the step is claimable.
1519
- agent_step: () => `Operator runs it for its plan; to do it yourself, set its executor to none (${invokedName()} plan edit <plan-id>) and release the plan again`,
571
+ // A person never claims an Operator step, so releasing the plan is not the
572
+ // fix: handing the step to a person is, and a handed-over Operator step
573
+ // stays frozen until the next release.
574
+ agent_step: (_identifier, plan) => {
575
+ const runs = plan.kind === "sole" ? `plan ${plan.root}` : plan.kind === "nearest" ? `its plans (several govern it; the nearest is ${plan.root})` : "its plan";
576
+ const target = plan.kind === "sole" ? plan.root : "<plan>";
577
+ return `a person can't claim an Operator step: Operator runs it for ${runs}. To do it yourself, set its executor to none (${invokedName()} plan edit ${target}) and release the plan again`;
578
+ },
579
+ plan_not_released: (_identifier, plan) => planReleaseHint(plan),
580
+ not_in_plan_revision: (_identifier, plan) => planReleaseHint(plan),
581
+ edited_since_release: (_identifier, plan) => planReleaseHint(plan),
1520
582
  waiting_on_condition: (identifier) => `${invokedName()} show ${identifier} lists what it waits on; tick a gate with ${invokedName()} gate satisfy ${identifier} <gate>`
1521
583
  };
584
+ function planReleaseHint(plan) {
585
+ if (plan.kind === "sole") return plan.release ? `run ${invokedName()} plan release ${plan.root}` : void 0;
586
+ if (plan.kind === "nearest") {
587
+ return `several plans govern it; the nearest is ${plan.root} (${invokedName()} plan show ${plan.root})`;
588
+ }
589
+ return void 0;
590
+ }
1522
591
  function explainClaimRefusal(error, identifier) {
1523
592
  if (!(error instanceof HttpError) || error.status !== 409) return error;
1524
593
  const reason = error.body["reason"];
1525
- const hint = typeof reason === "string" && Object.hasOwn(CLAIM_REFUSAL_HINTS, reason) ? CLAIM_REFUSAL_HINTS[reason](identifier) : void 0;
594
+ const hint = typeof reason === "string" && Object.hasOwn(CLAIM_REFUSAL_HINTS, reason) ? CLAIM_REFUSAL_HINTS[reason](identifier, refusingPlan(error.body)) : void 0;
1526
595
  if (hint === void 0) return error;
1527
596
  return new HttpError(error.status, `${error.message} \u2014 ${hint}`, error.body, error.retryAfter);
1528
597
  }
598
+ function refusingPlan(body) {
599
+ const plan = body["plan"];
600
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan)) return { kind: "none" };
601
+ const root = plan["root"];
602
+ if (typeof root !== "string" || !isIdentifier(root)) return { kind: "none" };
603
+ const message = typeof body["message"] === "string" ? body["message"] : "";
604
+ if (!new RegExp(`\\bplan ${root}\\b`).test(message)) return { kind: "nearest", root };
605
+ return { kind: "sole", root, release: new RegExp(`\\brelease plan ${root}\\b`).test(message) };
606
+ }
1529
607
  function requireCloseArgs(identifiers, opts) {
1530
608
  if (identifiers.length === 0) {
1531
609
  throw new UsageError(
@@ -1562,7 +640,7 @@ async function close(ctx, identifiers, opts) {
1562
640
  }
1563
641
  }
1564
642
  const human = results.map((result) => closeLine(result, opts.duplicateOf)).join("");
1565
- const json2 = results.map(
643
+ const json = results.map(
1566
644
  (result) => result.ok ? {
1567
645
  identifier: result.identifier,
1568
646
  closed: true,
@@ -1573,7 +651,7 @@ async function close(ctx, identifiers, opts) {
1573
651
  } : { identifier: result.identifier, closed: false, error: errorText(result.error) }
1574
652
  );
1575
653
  const partial = results.some((result) => !result.ok);
1576
- return partial ? { json: json2, human, partial: true } : { json: json2, human };
654
+ return partial ? { json, human, partial: true } : { json, human };
1577
655
  }
1578
656
  function closeLine(result, duplicateOf) {
1579
657
  if (!result.ok) return `FAILED ${result.identifier}: ${errorText(result.error)}
@@ -1616,25 +694,25 @@ async function archive(ctx, identifiers, opts) {
1616
694
  ` : `FAILED ${result.identifier}: ${errorText(result.error)}
1617
695
  `
1618
696
  ).join("");
1619
- const json2 = results.map(
697
+ const json = results.map(
1620
698
  (result) => result.ok ? { identifier: result.identifier, archived } : { identifier: result.identifier, error: errorText(result.error) }
1621
699
  );
1622
700
  const partial = results.some((result) => !result.ok);
1623
- return partial ? { json: json2, human, partial: true } : { json: json2, human };
701
+ return partial ? { json, human, partial: true } : { json, human };
1624
702
  }
1625
703
  async function comments(ctx, identifier) {
1626
704
  requireIdentifiers([identifier]);
1627
- const records5 = await completeCommentThread(ctx, identifier);
1628
- if (records5.length === 0) return { json: records5, human: "No comments.\n" };
1629
- const human = records5.map((comment2) => {
705
+ const records4 = await completeCommentThread(ctx, identifier);
706
+ if (records4.length === 0) return { json: records4, human: "No comments.\n" };
707
+ const human = records4.map((comment2) => {
1630
708
  const author = clean((comment2["author"] ?? {})["name"] ?? "unknown");
1631
709
  const id = clean(comment2["id"] ?? "unknown id");
1632
- const text = cleanMultiline(str2(comment2["body"])).split("\n").map((line) => " " + line).join("\n");
1633
- return `[${str2(comment2["inserted_at"])}] ${author} (${id}):
710
+ const text = cleanMultiline(str(comment2["body"])).split("\n").map((line) => " " + line).join("\n");
711
+ return `[${str(comment2["inserted_at"])}] ${author} (${id}):
1634
712
  ${text}
1635
713
  `;
1636
714
  }).join("\n");
1637
- return { json: records5, human };
715
+ return { json: records4, human };
1638
716
  }
1639
717
  async function completeCommentThread(ctx, identifier) {
1640
718
  const pages = [];
@@ -1648,20 +726,20 @@ async function completeCommentThread(ctx, identifier) {
1648
726
  if (!Array.isArray(page.data) || typeof page.meta?.has_more !== "boolean") {
1649
727
  throw new CommandError("Invalid comment pagination: expected a comment array and boolean meta.has_more; completeness unknown.");
1650
728
  }
1651
- const records5 = [];
729
+ const records4 = [];
1652
730
  for (const record of page.data) {
1653
731
  if (record === null || typeof record !== "object" || Array.isArray(record) || typeof record.id !== "string" || record.id.trim() === "" || seen.has(record.id)) {
1654
732
  throw new CommandError("Invalid comment pagination: missing or repeated comment id; completeness unknown.");
1655
733
  }
1656
734
  seen.add(record.id);
1657
- records5.push(record);
735
+ records4.push(record);
1658
736
  }
1659
- pages.push(records5);
737
+ pages.push(records4);
1660
738
  if (!page.meta.has_more) return pages.reverse().flat();
1661
- if (records5.length === 0) {
739
+ if (records4.length === 0) {
1662
740
  throw new CommandError("Invalid comment pagination: has_more on an empty page; completeness unknown.");
1663
741
  }
1664
- before = records5[0]["id"];
742
+ before = records4[0]["id"];
1665
743
  }
1666
744
  }
1667
745
  async function dep(ctx, verb, issue, blocker) {
@@ -1680,7 +758,7 @@ async function dep(ctx, verb, issue, blocker) {
1680
758
  (candidate) => candidate["direction"] === "blocked_by" && candidate["other_issue"] === blocker
1681
759
  );
1682
760
  if (relation === void 0) throw new CommandError(`${blocker} does not block this issue.`);
1683
- await ctx.client.delete(`/api/v1/issues/${issue}/relations/${str2(relation["id"])}`);
761
+ await ctx.client.delete(`/api/v1/issues/${issue}/relations/${str(relation["id"])}`);
1684
762
  return { json: { deleted: relation["id"] }, human: `${blocker} no longer blocks ${issue}
1685
763
  ` };
1686
764
  }
@@ -1803,17 +881,17 @@ function requireCommentId(commentId) {
1803
881
  }
1804
882
  var MAX_IMAGE_BYTES = 25e6;
1805
883
  var MAX_VIDEO_BYTES = 1e8;
1806
- async function attachImage(ctx, path2) {
1807
- return uploadAttachment(ctx, path2, MAX_IMAGE_BYTES);
884
+ async function attachImage(ctx, path) {
885
+ return uploadAttachment(ctx, path, MAX_IMAGE_BYTES);
1808
886
  }
1809
- async function attachFile(ctx, path2) {
1810
- const maxBytes = extname(path2).toLowerCase() === ".mp4" ? MAX_VIDEO_BYTES : MAX_IMAGE_BYTES;
1811
- return uploadAttachment(ctx, path2, maxBytes);
887
+ async function attachFile(ctx, path) {
888
+ const maxBytes = extname(path).toLowerCase() === ".mp4" ? MAX_VIDEO_BYTES : MAX_IMAGE_BYTES;
889
+ return uploadAttachment(ctx, path, maxBytes);
1812
890
  }
1813
- async function uploadAttachment(ctx, path2, maxBytes) {
1814
- attachableFile(path2, maxBytes);
1815
- const file = maxBytes === MAX_VIDEO_BYTES ? new File([await openAsBlob(path2)], basename(path2)) : new File([readFileSync(path2)], basename(path2));
1816
- requireAttachmentSize(path2, file.size, maxBytes);
891
+ async function uploadAttachment(ctx, path, maxBytes) {
892
+ attachableFile(path, maxBytes);
893
+ const file = maxBytes === MAX_VIDEO_BYTES ? new File([await openAsBlob(path)], basename(path)) : new File([readFileSync(path)], basename(path));
894
+ requireAttachmentSize(path, file.size, maxBytes);
1817
895
  const form = new FormData();
1818
896
  form.set("file", file);
1819
897
  const body = await ctx.client.postMultipart("/api/v1/uploads", form, file.size);
@@ -1822,27 +900,27 @@ async function uploadAttachment(ctx, path2, maxBytes) {
1822
900
  if (typeof markdown !== "string") throw new CommandError("the upload response carried no markdown reference");
1823
901
  return { json: attachment, human: clean(markdown) + "\n" };
1824
902
  }
1825
- function attachableFile(path2, maxBytes) {
903
+ function attachableFile(path, maxBytes) {
1826
904
  let fileStats;
1827
905
  try {
1828
- fileStats = statSync2(path2);
906
+ fileStats = statSync2(path);
1829
907
  } catch (error) {
1830
- throw new LocalError(`cannot read ${path2}: ${posixDetail(error)}`);
908
+ throw new LocalError(`cannot read ${path}: ${posixDetail(error)}`);
1831
909
  }
1832
910
  if (!fileStats.isFile()) {
1833
911
  const kind = fileStats.isDirectory() ? "directory" : "special file";
1834
- throw new LocalError(`${path2} is a ${kind}, not a file.`);
912
+ throw new LocalError(`${path} is a ${kind}, not a file.`);
1835
913
  }
1836
- requireAttachmentSize(path2, fileStats.size, maxBytes);
914
+ requireAttachmentSize(path, fileStats.size, maxBytes);
1837
915
  try {
1838
- closeSync(openSync(path2, "r"));
916
+ closeSync(openSync(path, "r"));
1839
917
  } catch (error) {
1840
- throw new LocalError(`cannot read ${path2}: ${posixDetail(error)}`);
918
+ throw new LocalError(`cannot read ${path}: ${posixDetail(error)}`);
1841
919
  }
1842
920
  }
1843
- function requireAttachmentSize(path2, size, maxBytes) {
921
+ function requireAttachmentSize(path, size, maxBytes) {
1844
922
  if (size > maxBytes) {
1845
- throw new LocalError(`${path2} is too large: ${size} bytes, and attachments are capped at ${maxBytes}.`);
923
+ throw new LocalError(`${path} is too large: ${size} bytes, and attachments are capped at ${maxBytes}.`);
1846
924
  }
1847
925
  }
1848
926
  function posixDetail(error) {
@@ -1851,19 +929,19 @@ function posixDetail(error) {
1851
929
  if (code === "EACCES") return "permission denied";
1852
930
  return error instanceof Error ? error.message : String(error);
1853
931
  }
1854
- async function setState(ctx, identifier, stateName2) {
932
+ async function setState(ctx, identifier, stateName) {
1855
933
  const showBody = await ctx.client.get(`/api/v1/issues/${identifier}`);
1856
- const teamKey = str2((showBody.data["team"] ?? {})["key"]);
934
+ const teamKey = str((showBody.data["team"] ?? {})["key"]);
1857
935
  const statesBody = await ctx.client.get(
1858
936
  `/api/v1/teams/${teamKey}/workflow_states`
1859
937
  );
1860
- const wanted = stateName2.toLowerCase();
938
+ const wanted = stateName.toLowerCase();
1861
939
  const state = statesBody.data.find(
1862
- (candidate) => str2(candidate["name"]).toLowerCase() === wanted || str2(candidate["type"]) === wanted
940
+ (candidate) => str(candidate["name"]).toLowerCase() === wanted || str(candidate["type"]) === wanted
1863
941
  );
1864
942
  if (state === void 0) {
1865
- const names = statesBody.data.map((candidate) => str2(candidate["name"])).join(", ");
1866
- throw new CommandError(`No workflow state matches "${stateName2}". States: ${names}`);
943
+ const names = statesBody.data.map((candidate) => str(candidate["name"])).join(", ");
944
+ throw new CommandError(`No workflow state matches "${stateName}". States: ${names}`);
1867
945
  }
1868
946
  const body = await ctx.client.put(`/api/v1/issues/${identifier}/state`, { state_id: state["id"] });
1869
947
  return body.data;
@@ -1894,16 +972,16 @@ function requestedLabelInputs(values) {
1894
972
  });
1895
973
  }
1896
974
  function normalizeLabelName(value) {
1897
- return str2(value).trim().toLowerCase();
975
+ return str(value).trim().toLowerCase();
1898
976
  }
1899
977
  async function prepareCreateLabelIds(ctx, teamKey, names) {
1900
978
  if (names.length === 0) return [];
1901
979
  const teams = await ctx.client.get("/api/v1/teams");
1902
980
  const target = teams.data.find(
1903
- (team) => str2(team["key"]).toLowerCase() === teamKey.toLowerCase()
981
+ (team) => str(team["key"]).toLowerCase() === teamKey.toLowerCase()
1904
982
  );
1905
983
  if (target === void 0) throw new CommandError(`No team matches key "${teamKey}".`);
1906
- const teamId = str2(target["id"]);
984
+ const teamId = str(target["id"]);
1907
985
  if (teamId === "") throw new CommandError(`team ${teamKey} carried no id`);
1908
986
  return prepareApplicableLabelIds(ctx, teamId, names);
1909
987
  }
@@ -1951,7 +1029,7 @@ async function resolveOrCreateApplicableLabel(ctx, catalog, name, teamId) {
1951
1029
  }
1952
1030
  }
1953
1031
  function labelId(label) {
1954
- const id = str2(label["id"]);
1032
+ const id = str(label["id"]);
1955
1033
  if (id === "") throw new CommandError("the label response carried no id");
1956
1034
  return id;
1957
1035
  }
@@ -1980,7 +1058,7 @@ async function prepareLabelUpdate(ctx, identifier, addInputs, removeInputs) {
1980
1058
  const ids = originalIds.filter((id) => !removed.has(id));
1981
1059
  if (addInputs.length > 0) {
1982
1060
  const team = issue["team"] ?? {};
1983
- const teamId = str2(team["id"]);
1061
+ const teamId = str(team["id"]);
1984
1062
  if (teamId === "") throw new CommandError(`issue ${identifier} carried no team id`);
1985
1063
  let catalog = (await ctx.client.get("/api/v1/labels")).data;
1986
1064
  for (const input of addInputs) {
@@ -2014,15 +1092,15 @@ async function fetchIssues(ctx, params, opts) {
2014
1092
  throw new UsageError("--all reads every page from the first \u2014 pass --all or --page, not both");
2015
1093
  }
2016
1094
  if (opts.all === true) {
2017
- const records6 = await fetchEveryPage(
1095
+ const records5 = await fetchEveryPage(
2018
1096
  (page) => ctx.client.get("/api/v1/issues", { ...params, page })
2019
1097
  );
2020
- return { records: records6, notice: "" };
1098
+ return { records: records5, notice: "" };
2021
1099
  }
2022
1100
  const body = await ctx.client.get("/api/v1/issues", params);
2023
- const records5 = body.data;
1101
+ const records4 = body.data;
2024
1102
  const requested = typeof params["page_size"] === "number" ? params["page_size"] : void 0;
2025
- return { records: records5, notice: notices(body.meta, requested, records5) };
1103
+ return { records: records4, notice: notices(body.meta, requested, records4) };
2026
1104
  }
2027
1105
  function listParams(ctx, opts) {
2028
1106
  if (opts.type !== void 0 && opts.label !== void 0) {
@@ -2115,7 +1193,7 @@ function errorText(error) {
2115
1193
  if (error instanceof HttpError) return `${clean(error.message)} (HTTP ${error.status})`;
2116
1194
  return `cannot reach the API: ${clean(error.message)}`;
2117
1195
  }
2118
- function str2(value) {
1196
+ function str(value) {
2119
1197
  return value === null || value === void 0 ? "" : String(value);
2120
1198
  }
2121
1199
 
@@ -2124,7 +1202,7 @@ var SAFE_ID = /^(?!\.{1,2}$)[A-Za-z0-9._-]+$/;
2124
1202
  function requireSafeId(kind, id) {
2125
1203
  if (!SAFE_ID.test(id)) throw new UsageError(`not a ${kind} id: ${id}`);
2126
1204
  }
2127
- function str3(value) {
1205
+ function str2(value) {
2128
1206
  return value === null || value === void 0 ? "" : String(value);
2129
1207
  }
2130
1208
  function field(row, ...keys) {
@@ -2143,15 +1221,15 @@ function putDate(body, key, raw) {
2143
1221
  if (parsed === null) return body;
2144
1222
  return { ...body, [key]: parsed === "clear" ? null : parsed };
2145
1223
  }
2146
- async function paginatedRead(ctx, path2, params, opts) {
1224
+ async function paginatedRead(ctx, path, params, opts) {
2147
1225
  if (opts.all === true && opts.page !== void 0) {
2148
1226
  throw new UsageError("--all reads every page from the first \u2014 pass --all or --page, not both");
2149
1227
  }
2150
1228
  if (opts.all === true) {
2151
- const records5 = await fetchEveryPage((page) => ctx.client.get(path2, { ...params, page }));
2152
- return { records: records5, notice: "" };
1229
+ const records4 = await fetchEveryPage((page) => ctx.client.get(path, { ...params, page }));
1230
+ return { records: records4, notice: "" };
2153
1231
  }
2154
- const body = await ctx.client.get(path2, params);
1232
+ const body = await ctx.client.get(path, params);
2155
1233
  const requested = typeof params["page_size"] === "number" ? params["page_size"] : void 0;
2156
1234
  return { records: body.data, notice: notices(body.meta, requested, body.data) };
2157
1235
  }
@@ -2159,18 +1237,18 @@ async function projectList(ctx, opts) {
2159
1237
  let params = { page_size: opts.limit ?? 50, page: opts.page ?? 1 };
2160
1238
  if (opts.search !== void 0) params = { ...params, search: opts.search };
2161
1239
  if (opts.state !== void 0) params = { ...params, state: opts.state };
2162
- const { records: records5, notice } = await paginatedRead(ctx, "/api/v1/projects", params, opts);
2163
- const human = records5.length === 0 ? "No projects.\n" : table(
1240
+ const { records: records4, notice } = await paginatedRead(ctx, "/api/v1/projects", params, opts);
1241
+ const human = records4.length === 0 ? "No projects.\n" : table(
2164
1242
  ["ID", "STATE", "LEAD", "TARGET", "NAME"],
2165
- records5.map((project) => [
2166
- str3(project["id"]),
2167
- str3(project["state"]) || "-",
2168
- str3(field(project, "lead", "name")) || "-",
2169
- str3(project["target_date"]) || "-",
2170
- str3(project["name"])
1243
+ records4.map((project) => [
1244
+ str2(project["id"]),
1245
+ str2(project["state"]) || "-",
1246
+ str2(field(project, "lead", "name")) || "-",
1247
+ str2(project["target_date"]) || "-",
1248
+ str2(project["name"])
2171
1249
  ])
2172
1250
  );
2173
- return { json: records5, human, notice };
1251
+ return { json: records4, human, notice };
2174
1252
  }
2175
1253
  async function projectShow(ctx, id) {
2176
1254
  requireSafeId("project", id);
@@ -2191,12 +1269,12 @@ async function projectShow(ctx, id) {
2191
1269
  ];
2192
1270
  const sectionText = sections.filter(([, text]) => text !== null && text !== void 0 && text !== "").map(([title, text]) => `
2193
1271
  ${title}:
2194
- ${indent2(String(text))}
1272
+ ${indent(String(text))}
2195
1273
  `).join("");
2196
1274
  const milestones = Array.isArray(project["milestones"]) ? project["milestones"] : [];
2197
- const milestoneText = milestones.length === 0 ? "" : "\nMilestones:\n" + milestones.map((milestone) => ` ${(str3(milestone["target_date"]) || "-").padEnd(12)}${clean(milestone["name"])}
1275
+ const milestoneText = milestones.length === 0 ? "" : "\nMilestones:\n" + milestones.map((milestone) => ` ${(str2(milestone["target_date"]) || "-").padEnd(12)}${clean(milestone["name"])}
2198
1276
  `).join("");
2199
- const human = `${str3(project["id"])} \xB7 ${clean(project["name"])}
1277
+ const human = `${str2(project["id"])} \xB7 ${clean(project["name"])}
2200
1278
  ` + fieldLines + sectionText + milestoneText;
2201
1279
  return { json: project, human };
2202
1280
  }
@@ -2215,7 +1293,7 @@ async function projectCreate(ctx, opts) {
2215
1293
  if (opts.name === void 0) throw new UsageError("usage: harness project create --name <name> \u2026");
2216
1294
  const body = await ctx.client.post("/api/v1/projects", projectBody(opts));
2217
1295
  const project = body.data;
2218
- return { json: project, human: `Created project ${clean(project["name"])} (${str3(project["id"])})
1296
+ return { json: project, human: `Created project ${clean(project["name"])} (${str2(project["id"])})
2219
1297
  ` };
2220
1298
  }
2221
1299
  async function projectUpdate(ctx, id, opts) {
@@ -2225,7 +1303,7 @@ async function projectUpdate(ctx, id, opts) {
2225
1303
  throw new UsageError("usage: harness project update <id> --name \u2026 | --state \u2026 | \u2026");
2226
1304
  }
2227
1305
  const body = await ctx.client.patch(`/api/v1/projects/${id}`, patch);
2228
- return { json: body.data, human: `Updated project ${str3(body.data["id"])}
1306
+ return { json: body.data, human: `Updated project ${str2(body.data["id"])}
2229
1307
  ` };
2230
1308
  }
2231
1309
  async function milestoneList(ctx, project) {
@@ -2235,9 +1313,9 @@ async function milestoneList(ctx, project) {
2235
1313
  const human = body.data.length === 0 ? "No milestones.\n" : table(
2236
1314
  ["ID", "TARGET", "NAME"],
2237
1315
  body.data.map((milestone) => [
2238
- str3(milestone["id"]),
2239
- str3(milestone["target_date"]) || "-",
2240
- str3(milestone["name"])
1316
+ str2(milestone["id"]),
1317
+ str2(milestone["target_date"]) || "-",
1318
+ str2(milestone["name"])
2241
1319
  ])
2242
1320
  );
2243
1321
  return { json: body.data, human };
@@ -2254,26 +1332,26 @@ async function milestoneCreate(ctx, opts) {
2254
1332
  const milestone = response.data;
2255
1333
  return {
2256
1334
  json: milestone,
2257
- human: `Created milestone ${clean(milestone["name"])} (${str3(milestone["id"])})
1335
+ human: `Created milestone ${clean(milestone["name"])} (${str2(milestone["id"])})
2258
1336
  `
2259
1337
  };
2260
1338
  }
2261
1339
  async function docList(ctx, opts) {
2262
1340
  let params = { page_size: opts.limit ?? 50, page: opts.page ?? 1 };
2263
1341
  if (opts.search !== void 0) params = { ...params, search: opts.search };
2264
- const { records: records5, notice } = await paginatedRead(ctx, "/api/v1/documents", params, opts);
2265
- const human = records5.length === 0 ? "No documents.\n" : table(
1342
+ const { records: records4, notice } = await paginatedRead(ctx, "/api/v1/documents", params, opts);
1343
+ const human = records4.length === 0 ? "No documents.\n" : table(
2266
1344
  ["ID", "UPDATED", "TITLE"],
2267
- records5.map((doc) => [str3(doc["id"]), str3(doc["updated_at"]) || "-", str3(doc["title"])])
1345
+ records4.map((doc) => [str2(doc["id"]), str2(doc["updated_at"]) || "-", str2(doc["title"])])
2268
1346
  );
2269
- return { json: records5, human, notice };
1347
+ return { json: records4, human, notice };
2270
1348
  }
2271
1349
  async function docShow(ctx, id) {
2272
1350
  requireSafeId("document", id);
2273
1351
  const body = await ctx.client.get(`/api/v1/documents/${id}`);
2274
1352
  const doc = body.data;
2275
1353
  const content = doc["content"];
2276
- const human = `${str3(doc["id"])} \xB7 ${clean(doc["title"])}
1354
+ const human = `${str2(doc["id"])} \xB7 ${clean(doc["title"])}
2277
1355
  Updated: ${clean(doc["updated_at"])}
2278
1356
  ` + (content === null || content === void 0 || content === "" ? "" : "\n" + cleanMultiline(String(content)) + "\n");
2279
1357
  return { json: doc, human };
@@ -2285,7 +1363,7 @@ async function docCreate(ctx, opts) {
2285
1363
  body = putOpt2(body, "project_id", opts.project);
2286
1364
  const response = await ctx.client.post("/api/v1/documents", body);
2287
1365
  const doc = response.data;
2288
- return { json: doc, human: `Created document ${clean(doc["title"])} (${str3(doc["id"])})
1366
+ return { json: doc, human: `Created document ${clean(doc["title"])} (${str2(doc["id"])})
2289
1367
  ` };
2290
1368
  }
2291
1369
  async function docUpdate(ctx, id, opts) {
@@ -2298,7 +1376,7 @@ async function docUpdate(ctx, id, opts) {
2298
1376
  throw new UsageError("usage: harness doc update <id> --title \u2026 | --content \u2026 | --project \u2026");
2299
1377
  }
2300
1378
  const response = await ctx.client.patch(`/api/v1/documents/${id}`, patch);
2301
- return { json: response.data, human: `Updated document ${str3(response.data["id"])}
1379
+ return { json: response.data, human: `Updated document ${str2(response.data["id"])}
2302
1380
  ` };
2303
1381
  }
2304
1382
  async function linkList(ctx, issue) {
@@ -2306,7 +1384,7 @@ async function linkList(ctx, issue) {
2306
1384
  const body = await ctx.client.get(`/api/v1/issues/${issue}/links`);
2307
1385
  const human = body.data.length === 0 ? "No links.\n" : table(
2308
1386
  ["ID", "URL", "TITLE"],
2309
- body.data.map((link) => [str3(link["id"]), str3(link["url"]), str3(link["title"]) || "-"])
1387
+ body.data.map((link) => [str2(link["id"]), str2(link["url"]), str2(link["title"]) || "-"])
2310
1388
  );
2311
1389
  return { json: body.data, human };
2312
1390
  }
@@ -2339,7 +1417,7 @@ async function linkUpdate(ctx, issue, idOrUrl, opts) {
2339
1417
  if (Object.keys(patch).length === 0) {
2340
1418
  throw new UsageError("usage: harness link update <issue> <link-id-or-url> --url <url> | --title <title>");
2341
1419
  }
2342
- const id = isUuid(idOrUrl) ? idOrUrl.toLowerCase() : str3((await findLink(ctx, issue, idOrUrl))["id"]);
1420
+ const id = isUuid(idOrUrl) ? idOrUrl.toLowerCase() : str2((await findLink(ctx, issue, idOrUrl))["id"]);
2343
1421
  if (!isUuid(idOrUrl)) patch["expected_url"] = idOrUrl;
2344
1422
  const response = await ctx.client.patch(`/api/v1/issues/${issue}/links/${id}`, patch);
2345
1423
  return { json: response.data, human: `Updated link ${clean(response.data["url"])} on ${issue}
@@ -2348,19 +1426,19 @@ async function linkUpdate(ctx, issue, idOrUrl, opts) {
2348
1426
  async function linkRm(ctx, issue, idOrUrl) {
2349
1427
  requireIdentifiers([issue]);
2350
1428
  const link = await findLink(ctx, issue, idOrUrl, true);
2351
- await ctx.client.delete(`/api/v1/issues/${issue}/links/${str3(link["id"])}`);
1429
+ await ctx.client.delete(`/api/v1/issues/${issue}/links/${str2(link["id"])}`);
2352
1430
  return { json: { deleted: link["id"] }, human: `Removed ${clean(link["url"])} from ${issue}
2353
1431
  ` };
2354
1432
  }
2355
- async function labelList2(ctx) {
1433
+ async function labelList(ctx) {
2356
1434
  const body = await ctx.client.get("/api/v1/labels");
2357
1435
  const human = body.data.length === 0 ? "No labels.\n" : table(
2358
1436
  ["NAME", "COLOR", "ISSUES", "ID"],
2359
1437
  body.data.map((label) => [
2360
- str3(label["name"]),
2361
- str3(label["color"]) || "-",
2362
- str3(label["issue_count"] ?? 0),
2363
- str3(label["id"])
1438
+ str2(label["name"]),
1439
+ str2(label["color"]) || "-",
1440
+ str2(label["issue_count"] ?? 0),
1441
+ str2(label["id"])
2364
1442
  ])
2365
1443
  );
2366
1444
  return { json: body.data, human };
@@ -2383,11 +1461,11 @@ async function teamCreate(ctx, opts) {
2383
1461
  }
2384
1462
  const created = await ctx.client.post("/api/v1/teams", { key: opts.key, name: opts.name });
2385
1463
  const team = created.data;
2386
- const states = await ctx.client.get(`/api/v1/teams/${str3(team["key"])}/workflow_states`);
2387
- const human = `Created team ${str3(team["key"])} \u2014 ${clean(team["name"])}
1464
+ const states = await ctx.client.get(`/api/v1/teams/${str2(team["key"])}/workflow_states`);
1465
+ const human = `Created team ${str2(team["key"])} \u2014 ${clean(team["name"])}
2388
1466
  ` + table(
2389
1467
  ["NAME", "TYPE"],
2390
- states.data.map((state) => [str3(state["name"]), str3(state["type"])])
1468
+ states.data.map((state) => [str2(state["name"]), str2(state["type"])])
2391
1469
  );
2392
1470
  return { json: { ...team, workflow_states: states.data }, human };
2393
1471
  }
@@ -2396,9 +1474,9 @@ async function teamList(ctx) {
2396
1474
  const human = body.data.length === 0 ? "No teams.\n" : table(
2397
1475
  ["KEY", "NAME", "ISSUES", "DEFAULT"],
2398
1476
  body.data.map((team) => [
2399
- str3(team["key"]),
2400
- str3(team["name"]),
2401
- str3(team["issue_count"] ?? 0),
1477
+ str2(team["key"]),
1478
+ str2(team["name"]),
1479
+ str2(team["issue_count"] ?? 0),
2402
1480
  team["is_default"] === true ? "yes" : ""
2403
1481
  ])
2404
1482
  );
@@ -2410,10 +1488,10 @@ async function workflowStates(ctx, team) {
2410
1488
  const human = body.data.length === 0 ? "No workflow states.\n" : table(
2411
1489
  ["NAME", "TYPE", "COLOR", "ID"],
2412
1490
  body.data.map((state) => [
2413
- str3(state["name"]),
2414
- str3(state["type"]),
2415
- str3(state["color"]) || "-",
2416
- str3(state["id"])
1491
+ str2(state["name"]),
1492
+ str2(state["type"]),
1493
+ str2(state["color"]) || "-",
1494
+ str2(state["id"])
2417
1495
  ])
2418
1496
  );
2419
1497
  return { json: body.data, human };
@@ -2422,7 +1500,7 @@ async function userList(ctx) {
2422
1500
  const body = await ctx.client.get("/api/v1/users");
2423
1501
  const human = body.data.length === 0 ? "No users.\n" : table(
2424
1502
  ["NAME", "EMAIL", "ID"],
2425
- body.data.map((user) => [str3(user["name"]), str3(user["email"]), str3(user["id"])])
1503
+ body.data.map((user) => [str2(user["name"]), str2(user["email"]), str2(user["id"])])
2426
1504
  );
2427
1505
  return { json: body.data, human };
2428
1506
  }
@@ -2431,7 +1509,7 @@ async function teamRename(ctx, key, name) {
2431
1509
  throw new UsageError("usage: harness team rename <key> --name <name>");
2432
1510
  }
2433
1511
  const body = await ctx.client.patch(`/api/v1/teams/${encodeURIComponent(key)}`, { name });
2434
- return { json: body.data, human: `Renamed ${str3(body.data["key"])} to ${clean(body.data["name"])}
1512
+ return { json: body.data, human: `Renamed ${str2(body.data["key"])} to ${clean(body.data["name"])}
2435
1513
  ` };
2436
1514
  }
2437
1515
  async function teamSetDefault(ctx, key) {
@@ -2439,7 +1517,7 @@ async function teamSetDefault(ctx, key) {
2439
1517
  const body = await ctx.client.put(`/api/v1/teams/${encodeURIComponent(key)}/default`, {});
2440
1518
  return {
2441
1519
  json: body.data,
2442
- human: `${str3(body.data["key"])} is now the default filing target
1520
+ human: `${str2(body.data["key"])} is now the default filing target
2443
1521
  `
2444
1522
  };
2445
1523
  }
@@ -2478,7 +1556,7 @@ async function stateOrder(ctx, ids, opts) {
2478
1556
  const body = await ctx.client.put(`/api/v1/teams/${key}/workflow_states/order`, { state_ids });
2479
1557
  const human = table(
2480
1558
  ["NAME", "TYPE"],
2481
- body.data.map((state) => [str3(state["name"]), str3(state["type"])])
1559
+ body.data.map((state) => [str2(state["name"]), str2(state["type"])])
2482
1560
  );
2483
1561
  return { json: body.data, human };
2484
1562
  }
@@ -2514,11 +1592,11 @@ async function memberList(ctx) {
2514
1592
  const human = body.data.length === 0 ? "No members.\n" : table(
2515
1593
  ["NAME", "EMAIL", "RANK", "STANDING", "ID"],
2516
1594
  body.data.map((member) => [
2517
- str3(field(member, "user", "name")),
2518
- str3(field(member, "user", "email")),
2519
- str3(member["role"]) || "-",
2520
- str3(member["standing"]),
2521
- str3(field(member, "user", "id"))
1595
+ str2(field(member, "user", "name")),
1596
+ str2(field(member, "user", "email")),
1597
+ str2(member["role"]) || "-",
1598
+ str2(member["standing"]),
1599
+ str2(field(member, "user", "id"))
2522
1600
  ])
2523
1601
  );
2524
1602
  return { json: body.data, human };
@@ -2530,7 +1608,7 @@ async function memberAdd(ctx, opts) {
2530
1608
  const body = await ctx.client.post("/api/v1/members", { email: opts.email, role: opts.role });
2531
1609
  return {
2532
1610
  json: body.data,
2533
- human: `Added ${str3(field(body.data, "user", "email"))} as ${str3(body.data["role"])}
1611
+ human: `Added ${str2(field(body.data, "user", "email"))} as ${str2(body.data["role"])}
2534
1612
  `
2535
1613
  };
2536
1614
  }
@@ -2539,19 +1617,19 @@ async function memberSetRole(ctx, userId, opts) {
2539
1617
  throw new UsageError("usage: harness member set-role <user-id> --role <admin|staff|collaborator|none>");
2540
1618
  }
2541
1619
  const body = await ctx.client.put(`/api/v1/members/${encodeURIComponent(userId)}/role`, { role: opts.role });
2542
- const rank = str3(body.data["role"]);
2543
- const human = rank === "" ? `Revoked the rank for ${str3(field(body.data, "user", "email"))}
2544
- ` : `${str3(field(body.data, "user", "email"))} is now ${rank}
1620
+ const rank = str2(body.data["role"]);
1621
+ const human = rank === "" ? `Revoked the rank for ${str2(field(body.data, "user", "email"))}
1622
+ ` : `${str2(field(body.data, "user", "email"))} is now ${rank}
2545
1623
  `;
2546
1624
  return { json: body.data, human };
2547
1625
  }
2548
1626
  function requireYes(command, id, yes) {
2549
1627
  if (yes !== true) throw new UsageError(`${command} ${id} is destructive \u2014 pass --yes to confirm`);
2550
1628
  }
2551
- async function upload(ctx, path2) {
2552
- return attachFile(ctx, path2);
1629
+ async function upload(ctx, path) {
1630
+ return attachFile(ctx, path);
2553
1631
  }
2554
- function indent2(text) {
1632
+ function indent(text) {
2555
1633
  return cleanMultiline(text).split("\n").map((line) => " " + line).join("\n");
2556
1634
  }
2557
1635
 
@@ -2592,12 +1670,12 @@ async function toolsSearch(ctx, query, opts) {
2592
1670
  }
2593
1671
  async function toolsDomains(ctx) {
2594
1672
  const body = await operatorRequest(() => ctx.client.get("/api/v1/operator/capabilities/domains"));
2595
- const domains = records2(body.data, "domains");
1673
+ const domains = records(body.data, "domains");
2596
1674
  return {
2597
1675
  json: domains,
2598
1676
  human: domains.length === 0 ? "Operator can use no capabilities here.\n" : table(
2599
1677
  ["DOMAIN", "CAPABILITIES"],
2600
- domains.map((domain) => [str4(domain["domain"]), str4(domain["count"])])
1678
+ domains.map((domain) => [str3(domain["domain"]), str3(domain["count"])])
2601
1679
  )
2602
1680
  };
2603
1681
  }
@@ -2662,7 +1740,7 @@ async function toolShow(ctx, id, opts) {
2662
1740
  }
2663
1741
  async function skillsList(ctx) {
2664
1742
  const body = await operatorRequest(() => ctx.client.get("/api/v1/operator/skills"));
2665
- const skills = records2(body.data, "skills").map((skill) => ({
1743
+ const skills = records(body.data, "skills").map((skill) => ({
2666
1744
  id: skill["id"],
2667
1745
  title: skill["title"],
2668
1746
  invocation_name: skill["invocation_name"],
@@ -2673,10 +1751,10 @@ async function skillsList(ctx) {
2673
1751
  human: skills.length === 0 ? "No skills are assigned to Operator in this organization.\n" : table(
2674
1752
  ["INVOCATION", "TITLE", "SHA-256", "ID"],
2675
1753
  skills.map((skill) => [
2676
- str4(skill.invocation_name),
1754
+ str3(skill.invocation_name),
2677
1755
  truncate(skill.title),
2678
- str4(skill.content_sha256),
2679
- str4(skill.id)
1756
+ str3(skill.content_sha256),
1757
+ str3(skill.id)
2680
1758
  ])
2681
1759
  )
2682
1760
  };
@@ -2736,7 +1814,7 @@ async function capabilityPage(ctx, params, staleCursor) {
2736
1814
  void 0,
2737
1815
  staleCursor
2738
1816
  );
2739
- const items = records2(body.data, "capabilities");
1817
+ const items = records(body.data, "capabilities");
2740
1818
  const meta = body.meta;
2741
1819
  if (!isRecord(meta)) throw new CommandError("The capabilities response has no pagination metadata.");
2742
1820
  const nextCursor = meta.next_cursor;
@@ -2771,14 +1849,14 @@ function explain(error, notFound2, staleCursor) {
2771
1849
  function capabilitiesTable(items, search2) {
2772
1850
  const headers = ["ID", "KIND", "DOMAIN", "SUMMARY"];
2773
1851
  const rows = items.map((item) => [
2774
- str4(item["id"]),
1852
+ str3(item["id"]),
2775
1853
  kindName(item),
2776
- str4(item["domain"]),
1854
+ str3(item["domain"]),
2777
1855
  truncate(item["summary"])
2778
1856
  ]);
2779
1857
  const table2 = search2 ? table(
2780
1858
  ["SCORE", ...headers],
2781
- rows.map((row, index) => [str4(items[index]["score"]), ...row])
1859
+ rows.map((row, index) => [str3(items[index]["score"]), ...row])
2782
1860
  ) : table(headers, rows);
2783
1861
  return table2 + (items.some(needsApproval) ? "\n" + APPROVAL_NOTE : "");
2784
1862
  }
@@ -2792,14 +1870,14 @@ function capabilityDetail(capability, view, schema) {
2792
1870
  const kind = kindName(capability);
2793
1871
  const line = (label, value) => ` ${(label + ":").padEnd(10)}${clean(value)}
2794
1872
  `;
2795
- const description = cleanMultiline(str4(capability["description"]));
1873
+ const description = cleanMultiline(str3(capability["description"]));
2796
1874
  return `${clean(capability["id"])}
2797
- ` + line("Domain", str4(capability["domain"])) + line("Kind", kind === "send" ? "send \u2014 delivered beyond the platform" : kind) + line("Mode", `${str4(capability["mode"])} (effect: ${str4(capability["effect"])})`) + line("Summary", str4(capability["summary"])) + line("Approval", approval(capability)) + (description === "" ? "" : `
1875
+ ` + line("Domain", str3(capability["domain"])) + line("Kind", kind === "send" ? "send \u2014 delivered beyond the platform" : kind) + line("Mode", `${str3(capability["mode"])} (effect: ${str3(capability["effect"])})`) + line("Summary", str3(capability["summary"])) + line("Approval", approval(capability)) + (description === "" ? "" : `
2798
1876
  Description:
2799
- ${indent3(description)}
1877
+ ${indent2(description)}
2800
1878
  `) + `
2801
1879
  ${view === "input" ? "Input" : "Result"} schema:
2802
- ${indent3(cleanMultiline(JSON.stringify(schema, null, 2)))}
1880
+ ${indent2(cleanMultiline(JSON.stringify(schema, null, 2)))}
2803
1881
  `;
2804
1882
  }
2805
1883
  function needsApproval(capability) {
@@ -2819,7 +1897,7 @@ var SCHEMA_PAGING_FIELDS = /* @__PURE__ */ new Set(["schema", "schema_encoding",
2819
1897
  function withoutSchemaPaging(capability) {
2820
1898
  return Object.fromEntries(Object.entries(capability).filter(([key]) => !SCHEMA_PAGING_FIELDS.has(key)));
2821
1899
  }
2822
- function records2(value, what) {
1900
+ function records(value, what) {
2823
1901
  if (!Array.isArray(value) || !value.every(isRecord)) {
2824
1902
  throw new CommandError(`The ${what} response carried no list.`);
2825
1903
  }
@@ -2833,13 +1911,13 @@ function dedupeById(items) {
2833
1911
  return true;
2834
1912
  });
2835
1913
  }
2836
- function indent3(text) {
1914
+ function indent2(text) {
2837
1915
  return text.split("\n").map((line) => " " + line).join("\n");
2838
1916
  }
2839
1917
  function isRecord(value) {
2840
1918
  return typeof value === "object" && value !== null && !Array.isArray(value);
2841
1919
  }
2842
- function str4(value) {
1920
+ function str3(value) {
2843
1921
  return value === null || value === void 0 ? "" : String(value);
2844
1922
  }
2845
1923
 
@@ -3036,11 +2114,11 @@ async function jobChangeSet(ctx, id) {
3036
2114
  if (!isRecord2(body.data)) throw new CommandError("The change set response carried no change set.");
3037
2115
  return {
3038
2116
  ...pick(body.data, CHANGE_SET_FIELDS),
3039
- actions: records3(body.data["actions"]).map((action) => pick(action, CHANGE_SET_ACTION_FIELDS))
2117
+ actions: records2(body.data["actions"]).map((action) => pick(action, CHANGE_SET_ACTION_FIELDS))
3040
2118
  };
3041
2119
  }
3042
2120
  function jobPeople(job) {
3043
- const participants = records3(field2(job, "people", "participants"));
2121
+ const participants = records2(field2(job, "people", "participants"));
3044
2122
  const owner = isRecord2(job["owner"]) ? job["owner"] : null;
3045
2123
  const names = /* @__PURE__ */ new Map();
3046
2124
  for (const named2 of owner === null ? participants : [...participants, owner]) {
@@ -3111,7 +2189,7 @@ async function jobWatch(ctx, id, opts) {
3111
2189
  after = page.nextAfter;
3112
2190
  job = page.job;
3113
2191
  if (page.hasMore) continue;
3114
- const state = str5(job["state"]);
2192
+ const state = str4(job["state"]);
3115
2193
  const waitingOn = typeof job["waiting_on"] === "string" ? job["waiting_on"] : null;
3116
2194
  if (shown === null || shown.state !== state || shown.waitingOn !== waitingOn) {
3117
2195
  emit(stateLine(shown?.state ?? null, state, waitingOn));
@@ -3252,13 +2330,13 @@ async function explained(send, why) {
3252
2330
  function jobsTable(jobs) {
3253
2331
  if (jobs.length === 0) return "No Operator jobs.\n";
3254
2332
  const rows = jobs.map((job) => [
3255
- str5(job["id"]),
3256
- str5(job["state"]) || "-",
3257
- str5(job["waiting_on"]) || "-",
3258
- str5(job["objective_kind"]) || "-",
2333
+ str4(job["id"]),
2334
+ str4(job["state"]) || "-",
2335
+ str4(job["waiting_on"]) || "-",
2336
+ str4(job["objective_kind"]) || "-",
3259
2337
  personName(job["owner"]),
3260
- str5(field2(job, "plan_step", "issue")) || "-",
3261
- str5(job["updated_at"]) || "-"
2338
+ str4(field2(job, "plan_step", "issue")) || "-",
2339
+ str4(job["updated_at"]) || "-"
3262
2340
  ]);
3263
2341
  const table2 = table(["ID", "STATE", "WAITING ON", "KIND", "OWNER", "PLAN STEP", "UPDATED"], rows);
3264
2342
  const approvals = jobs.filter((job) => typeof job["approval_url"] === "string");
@@ -3267,30 +2345,30 @@ function jobsTable(jobs) {
3267
2345
  `).join("");
3268
2346
  }
3269
2347
  function jobDetail(job) {
3270
- const waitingOn = str5(job["waiting_on"]);
2348
+ const waitingOn = str4(job["waiting_on"]);
3271
2349
  const header = `Operator job ${clean(job["id"])} \xB7 ${clean(job["state"])}` + (waitingOn === "" ? "" : ` \xB7 waiting on ${clean(waitingOn)}`) + "\n";
3272
- const people = records3(field2(job, "people", "participants"));
2350
+ const people = records2(field2(job, "people", "participants"));
3273
2351
  const fields = [
3274
- ["Kind", str5(job["objective_kind"]) || "-"],
3275
- ["Role", str5(job["viewer_role"]) || null],
2352
+ ["Kind", str4(job["objective_kind"]) || "-"],
2353
+ ["Role", str4(job["viewer_role"]) || null],
3276
2354
  ["Owner", isRecord2(job["owner"]) ? personName(job["owner"]) : null],
3277
2355
  ["Plan step", planStepLine(job["plan_step"])],
3278
2356
  ["Plan", planLine(job["plan"])],
3279
2357
  ["Progress", progressLine(job["progress"])],
3280
2358
  ["Window", windowLine(job["window"])],
3281
2359
  ["Spend", spendLine(job["spend"])],
3282
- ["Verification", str5(field2(job, "verification", "status")) || null],
2360
+ ["Verification", str4(field2(job, "verification", "status")) || null],
3283
2361
  ["Effects", effectsLine(job["effects"])],
3284
2362
  ["Skill", skillLine(job["skill"])],
3285
2363
  ["Grant", grantLine(job["grant"], people)],
3286
- ["Created", str5(job["inserted_at"]) || null],
3287
- ["Updated", str5(job["updated_at"]) || null],
3288
- ["Workbench", str5(job["url"]) || null]
2364
+ ["Created", str4(job["inserted_at"]) || null],
2365
+ ["Updated", str4(job["updated_at"]) || null],
2366
+ ["Workbench", str4(job["url"]) || null]
3289
2367
  ];
3290
- return header + labelled(fields) + approvalSection(job["pending_approval"], people) + listSection("Blockers", records3(job["blockers"]), (item) => `${str5(item["kind"])}: ${str5(item["state"])}`) + listSection("Artifacts", records3(job["artifacts"]), (item) => `${str5(item["id"])} ${str5(item["url"])}`) + peopleSection(people) + listSection(
2368
+ return header + labelled(fields) + approvalSection(job["pending_approval"], people) + listSection("Blockers", records2(job["blockers"]), (item) => `${str4(item["kind"])}: ${str4(item["state"])}`) + listSection("Artifacts", records2(job["artifacts"]), (item) => `${str4(item["id"])} ${str4(item["url"])}`) + peopleSection(people) + listSection(
3291
2369
  "Procedures",
3292
- records3(job["procedures"]),
3293
- (procedure) => `${str5(procedure["name"])} v${str5(procedure["current_version"])} ${str5(procedure["id"])}`
2370
+ records2(job["procedures"]),
2371
+ (procedure) => `${str4(procedure["name"])} v${str4(procedure["current_version"])} ${str4(procedure["id"])}`
3294
2372
  ) + controlsLine(job["viewer_can"]) + "\nThe objective's text is not shown: the API never returns it, because it can quote what Operator read. Open the workbench link to read it.\n";
3295
2373
  }
3296
2374
  function labelled(fields, width = 15) {
@@ -3299,16 +2377,16 @@ function labelled(fields, width = 15) {
3299
2377
  }
3300
2378
  function planStepLine(step) {
3301
2379
  if (!isRecord2(step)) return null;
3302
- return `${str5(step["issue"])} (plan ${str5(step["plan_id"])})`;
2380
+ return `${str4(step["issue"])} (plan ${str4(step["plan_id"])})`;
3303
2381
  }
3304
2382
  function planLine(plan) {
3305
2383
  if (!isRecord2(plan)) return "none yet";
3306
2384
  const count = plan["capability_count"];
3307
- return `version ${str5(plan["version"])} \xB7 instructions v${str5(plan["instruction_version"])} \xB7 ${str5(count)} capabilit${count === 1 ? "y" : "ies"}`;
2385
+ return `version ${str4(plan["version"])} \xB7 instructions v${str4(plan["instruction_version"])} \xB7 ${str4(count)} capabilit${count === 1 ? "y" : "ies"}`;
3308
2386
  }
3309
2387
  function progressLine(progress) {
3310
2388
  if (!isRecord2(progress)) return null;
3311
- const parts = [str5(progress["stage"]), str5(progress["situation"])].filter((part) => part !== "");
2389
+ const parts = [str4(progress["stage"]), str4(progress["situation"])].filter((part) => part !== "");
3312
2390
  if (progress["needs_viewer"] === true) parts.push("needs you");
3313
2391
  return parts.length === 0 ? null : parts.join(" \xB7 ");
3314
2392
  }
@@ -3317,28 +2395,28 @@ function windowLine(window) {
3317
2395
  const left = duration(window["remaining_ms"]);
3318
2396
  const budget = duration(window["budget_ms"]);
3319
2397
  const sequence = window["sequence"];
3320
- return `${left ?? "?"} left${budget === null ? "" : ` of ${budget}`} \xB7 ${str5(window["state"])}` + (typeof sequence === "number" ? ` (window ${sequence})` : "");
2398
+ return `${left ?? "?"} left${budget === null ? "" : ` of ${budget}`} \xB7 ${str4(window["state"])}` + (typeof sequence === "number" ? ` (window ${sequence})` : "");
3321
2399
  }
3322
2400
  function spendLine(spend) {
3323
2401
  if (!isRecord2(spend)) return null;
3324
- const spent = `${str5(spend["spent_cents"]) || "0"} cents spent`;
2402
+ const spent = `${str4(spend["spent_cents"]) || "0"} cents spent`;
3325
2403
  if (typeof spend["plan_budget_cents"] !== "number") return spent;
3326
2404
  const remaining = spend["plan_remaining_cents"];
3327
2405
  return `${spent} \xB7 plan budget ${spend["plan_budget_cents"]} cents` + (typeof remaining === "number" ? `, ${remaining} cents left` : "");
3328
2406
  }
3329
2407
  function effectsLine(effects) {
3330
2408
  if (!isRecord2(effects)) return null;
3331
- return ["staged", "succeeded", "failed", "unknown"].map((key) => `${str5(effects[key]) || "0"} ${key}`).join(" \xB7 ");
2409
+ return ["staged", "succeeded", "failed", "unknown"].map((key) => `${str4(effects[key]) || "0"} ${key}`).join(" \xB7 ");
3332
2410
  }
3333
2411
  function skillLine(skill) {
3334
2412
  if (!isRecord2(skill)) return null;
3335
- return `${str5(skill["title"])} (${str5(skill["invocation_name"])}) \xB7 sha256 ${str5(skill["content_sha256"])} \xB7 pinned ${str5(skill["pinned_at"])}`;
2413
+ return `${str4(skill["title"])} (${str4(skill["invocation_name"])}) \xB7 sha256 ${str4(skill["content_sha256"])} \xB7 pinned ${str4(skill["pinned_at"])}`;
3336
2414
  }
3337
2415
  function grantLine(grant, people) {
3338
2416
  if (!isRecord2(grant)) return null;
3339
- const approvedAt = str5(grant["approved_at"]);
3340
- const expiresAt = str5(grant["expires_at"]);
3341
- return `${str5(grant["status"])} v${str5(grant["version"])} \xB7 approved by ${nameOf(grant["approver_user_id"], people)}` + (approvedAt === "" ? "" : ` at ${approvedAt}`) + ` \xB7 ${expiresAt === "" ? "no expiry" : `expires ${expiresAt}`}`;
2417
+ const approvedAt = str4(grant["approved_at"]);
2418
+ const expiresAt = str4(grant["expires_at"]);
2419
+ return `${str4(grant["status"])} v${str4(grant["version"])} \xB7 approved by ${nameOf(grant["approver_user_id"], people)}` + (approvedAt === "" ? "" : ` at ${approvedAt}`) + ` \xB7 ${expiresAt === "" ? "no expiry" : `expires ${expiresAt}`}`;
3342
2420
  }
3343
2421
  function approvalSection(pending, people) {
3344
2422
  if (!isRecord2(pending)) return "";
@@ -3347,19 +2425,19 @@ function approvalSection(pending, people) {
3347
2425
  return "\nWaiting for approval. Approve it in the app; the CLI cannot:\n" + labelled([
3348
2426
  [
3349
2427
  "Change set",
3350
- `${str5(pending["manifest_id"])} v${str5(pending["version"])} \xB7 ${str5(pending["action_count"])} actions \xB7 up to ${str5(pending["max_effect_count"])} effects \xB7 plan version ${str5(pending["plan_version"])}`
2428
+ `${str4(pending["manifest_id"])} v${str4(pending["version"])} \xB7 ${str4(pending["action_count"])} actions \xB7 up to ${str4(pending["max_effect_count"])} effects \xB7 plan version ${str4(pending["plan_version"])}`
3351
2429
  ],
3352
- ["Digest", str5(pending["digest"]) || null],
2430
+ ["Digest", str4(pending["digest"]) || null],
3353
2431
  ["Approvers", approvers === "" ? canApprove : `${approvers} \xB7 ${canApprove}`],
3354
- ["Approve at", str5(pending["url"]) || null]
2432
+ ["Approve at", str4(pending["url"]) || null]
3355
2433
  ]);
3356
2434
  }
3357
2435
  function peopleSection(people) {
3358
2436
  if (people.length === 0) return "";
3359
2437
  const rows = people.map((person) => [
3360
- str5(person["name"]) || "(no name)",
3361
- str5(person["role"]),
3362
- str5(person["user_id"])
2438
+ str4(person["name"]) || "(no name)",
2439
+ str4(person["role"]),
2440
+ str4(person["user_id"])
3363
2441
  ]);
3364
2442
  const lines = table(["", "", ""], rows).split("\n").slice(1, -1);
3365
2443
  return "\nPeople:\n" + lines.map((line) => ` ${line}
@@ -3368,15 +2446,15 @@ function peopleSection(people) {
3368
2446
  function changesText(id, job, changeSet) {
3369
2447
  if (changeSet === null) return `No pending change set: Operator has proposed nothing for job ${id} yet.
3370
2448
  `;
3371
- const approvalUrl = str5(job["approval_url"]);
3372
- const rows = records3(changeSet["actions"]).map((action) => [
3373
- str5(action["display_order"]) || "-",
3374
- str5(action["action_type"]),
3375
- str5(action["action_version"]),
3376
- str5(action["effect_count"]),
3377
- (Array.isArray(action["required_capability_ids"]) ? action["required_capability_ids"].map(str5).join(", ") : "") || "-",
3378
- str5(action["digest"]),
3379
- str5(action["id"])
2449
+ const approvalUrl = str4(job["approval_url"]);
2450
+ const rows = records2(changeSet["actions"]).map((action) => [
2451
+ str4(action["display_order"]) || "-",
2452
+ str4(action["action_type"]),
2453
+ str4(action["action_version"]),
2454
+ str4(action["effect_count"]),
2455
+ (Array.isArray(action["required_capability_ids"]) ? action["required_capability_ids"].map(str4).join(", ") : "") || "-",
2456
+ str4(action["digest"]),
2457
+ str4(action["id"])
3380
2458
  ]);
3381
2459
  const actions = rows.length === 0 ? "\nActions: none\n" : "\nActions:\n" + table(["#", "ACTION", "VERSION", "EFFECTS", "CAPABILITIES", "DIGEST", "ID"], rows).split("\n").slice(0, -1).map((line) => ` ${line}
3382
2460
  `).join("");
@@ -3384,14 +2462,14 @@ function changesText(id, job, changeSet) {
3384
2462
  ` + labelled([
3385
2463
  [
3386
2464
  "Change set",
3387
- `${str5(changeSet["manifest_id"])} v${str5(changeSet["version"])} \xB7 plan version ${str5(changeSet["plan_version"])} \xB7 up to ${str5(changeSet["max_effect_count"])} effects`
2465
+ `${str4(changeSet["manifest_id"])} v${str4(changeSet["version"])} \xB7 plan version ${str4(changeSet["plan_version"])} \xB7 up to ${str4(changeSet["max_effect_count"])} effects`
3388
2466
  ],
3389
- ["Digest", str5(changeSet["digest"]) || null],
3390
- approvalUrl === "" ? ["Workbench", str5(job["url"]) || null] : ["Approve at", approvalUrl]
2467
+ ["Digest", str4(changeSet["digest"]) || null],
2468
+ approvalUrl === "" ? ["Workbench", str4(job["url"]) || null] : ["Approve at", approvalUrl]
3391
2469
  ]) + actions + "\nValues and targets are not shown: review them in the app at the " + (approvalUrl === "" ? "workbench link.\n" : "approval link. The CLI cannot approve.\n");
3392
2470
  }
3393
2471
  function peopleText(id, people) {
3394
- const one = (entry) => isRecord2(entry) ? str5(entry["name"]) || str5(entry["user_id"]) : "-";
2472
+ const one = (entry) => isRecord2(entry) ? str4(entry["name"]) || str4(entry["user_id"]) : "-";
3395
2473
  const many = (entries) => (Array.isArray(entries) ? entries.map(one).join(", ") : "") || "none";
3396
2474
  return `People on Operator job ${id}:
3397
2475
  ` + labelled(
@@ -3499,7 +2577,7 @@ function plural(count, noun) {
3499
2577
  function uniqueById(jobs) {
3500
2578
  const seen = /* @__PURE__ */ new Set();
3501
2579
  return jobs.filter((job) => {
3502
- const id = str5(job["id"]);
2580
+ const id = str4(job["id"]);
3503
2581
  if (seen.has(id)) return false;
3504
2582
  seen.add(id);
3505
2583
  return true;
@@ -3507,12 +2585,12 @@ function uniqueById(jobs) {
3507
2585
  }
3508
2586
  function personName(person) {
3509
2587
  if (!isRecord2(person)) return "-";
3510
- return str5(person["name"]) || str5(person["user_id"]) || "-";
2588
+ return str4(person["name"]) || str4(person["user_id"]) || "-";
3511
2589
  }
3512
2590
  function nameOf(userId, people) {
3513
- const id = str5(userId);
2591
+ const id = str4(userId);
3514
2592
  const person = people.find((candidate) => candidate["user_id"] === id);
3515
- return (person === void 0 ? "" : str5(person["name"])) || id || "?";
2593
+ return (person === void 0 ? "" : str4(person["name"])) || id || "?";
3516
2594
  }
3517
2595
  function duration(ms) {
3518
2596
  if (typeof ms !== "number" || !Number.isFinite(ms)) return null;
@@ -3538,13 +2616,13 @@ function field2(record, ...keys) {
3538
2616
  }
3539
2617
  return current ?? null;
3540
2618
  }
3541
- function records3(value) {
2619
+ function records2(value) {
3542
2620
  return Array.isArray(value) ? value.filter(isRecord2) : [];
3543
2621
  }
3544
2622
  function isRecord2(value) {
3545
2623
  return typeof value === "object" && value !== null && !Array.isArray(value);
3546
2624
  }
3547
- function str5(value) {
2625
+ function str4(value) {
3548
2626
  return value === null || value === void 0 ? "" : String(value);
3549
2627
  }
3550
2628
 
@@ -3589,9 +2667,9 @@ function outcomeUnknown(why, write2) {
3589
2667
  `Outcome unknown: ${why}, so ${write2.mayHave}. It is not retried; check ${write2.check} before trying again.`
3590
2668
  );
3591
2669
  }
3592
- async function sendOnce(ctx, path2, body, write2) {
2670
+ async function sendOnce(ctx, path, body, write2) {
3593
2671
  try {
3594
- return await ctx.client.post(path2, body, { actor: false });
2672
+ return await ctx.client.post(path, body, { actor: false });
3595
2673
  } catch (error) {
3596
2674
  if (error instanceof TransportError) {
3597
2675
  if (neverSent(error)) {
@@ -3730,14 +2808,14 @@ function startedText(job) {
3730
2808
  async function run(ctx, request, opts = {}) {
3731
2809
  const job = await startRun(ctx, request);
3732
2810
  if (opts.watch !== true) return { json: job, human: startedText(job) };
3733
- const json2 = opts.json === true;
3734
- if (!json2) opts.onLine?.(startedText(job) + "\n");
2811
+ const json = opts.json === true;
2812
+ if (!json) opts.onLine?.(startedText(job) + "\n");
3735
2813
  let watched;
3736
2814
  try {
3737
2815
  watched = await jobWatch(ctx, job.job_id, {
3738
2816
  throughWaits: true,
3739
- collectEvents: json2,
3740
- onLine: json2 ? void 0 : opts.onLine,
2817
+ collectEvents: json,
2818
+ onLine: json ? void 0 : opts.onLine,
3741
2819
  signal: opts.signal,
3742
2820
  sleep: opts.sleep
3743
2821
  });
@@ -3829,18 +2907,18 @@ function resolveTextSources(sources) {
3829
2907
  }
3830
2908
  return resolved;
3831
2909
  }
3832
- function readFileOption(flag, path2) {
3833
- if (path2 === STDIN_PATH) {
2910
+ function readFileOption(flag, path) {
2911
+ if (path === STDIN_PATH) {
3834
2912
  if (isatty(0)) {
3835
2913
  throw new UsageError(`${flag} - reads stdin, but stdin is a terminal \u2014 pipe something in or pass a path`);
3836
2914
  }
3837
2915
  } else {
3838
- checkReadable(path2, flag, DOCUMENT_LIMIT);
2916
+ checkReadable(path, flag, DOCUMENT_LIMIT);
3839
2917
  }
3840
- return readTextFile(path2, flag, DOCUMENT_LIMIT);
2918
+ return readTextFile(path, flag, DOCUMENT_LIMIT);
3841
2919
  }
3842
- function readTextFile(path2, option, limit = TEXT_LIMIT) {
3843
- if (path2 === STDIN_PATH) {
2920
+ function readTextFile(path, option, limit = TEXT_LIMIT) {
2921
+ if (path === STDIN_PATH) {
3844
2922
  let piped;
3845
2923
  try {
3846
2924
  piped = readFileSync2(0);
@@ -3852,10 +2930,10 @@ function readTextFile(path2, option, limit = TEXT_LIMIT) {
3852
2930
  }
3853
2931
  return decodeUtf8(piped, `${option} -`);
3854
2932
  }
3855
- const flag = `${option} ${path2}`;
2933
+ const flag = `${option} ${path}`;
3856
2934
  let bytes;
3857
2935
  try {
3858
- bytes = readFileSync2(path2);
2936
+ bytes = readFileSync2(path);
3859
2937
  } catch (error) {
3860
2938
  throw new UsageError(`${flag}: ${describe(error)}`);
3861
2939
  }
@@ -3872,11 +2950,11 @@ function decodeUtf8(bytes, flag) {
3872
2950
  function oversize(what, limit) {
3873
2951
  return `${what}: over ${limit.bytes / 1e6} MB \u2014 ${limit.why}`;
3874
2952
  }
3875
- function checkReadable(path2, option, limit = TEXT_LIMIT) {
3876
- const flag = `${option} ${path2}`;
2953
+ function checkReadable(path, option, limit = TEXT_LIMIT) {
2954
+ const flag = `${option} ${path}`;
3877
2955
  let stats3;
3878
2956
  try {
3879
- stats3 = statSync3(path2);
2957
+ stats3 = statSync3(path);
3880
2958
  } catch (error) {
3881
2959
  throw new UsageError(`${flag}: ${describe(error)}`);
3882
2960
  }
@@ -4076,7 +3154,7 @@ async function procedureSave(ctx, jobId, name) {
4076
3154
  return {
4077
3155
  json: pick2(data, ["id", "url"]),
4078
3156
  human: `${saved}, but it can't be read back as you.
4079
- ` + labelled([["Workbench", str6(data["url"]) || null]])
3157
+ ` + labelled([["Workbench", str5(data["url"]) || null]])
4080
3158
  };
4081
3159
  }
4082
3160
  const procedure = procedureJson(data);
@@ -4139,18 +3217,18 @@ async function startProcedureRun(ctx, request) {
4139
3217
  return {
4140
3218
  json: pick2(data, ["id", "url"]),
4141
3219
  human: `${started}, but it can't be read back as you.
4142
- ` + labelled([["Workbench", str6(data["url"]) || null]]) + "\n" + RUN_NOTE
3220
+ ` + labelled([["Workbench", str5(data["url"]) || null]]) + "\n" + RUN_NOTE
4143
3221
  };
4144
3222
  }
4145
3223
  const job = withheld(data);
4146
- const waitingOn = str6(job["waiting_on"]);
3224
+ const waitingOn = str5(job["waiting_on"]);
4147
3225
  return {
4148
3226
  json: job,
4149
3227
  human: `${started}.
4150
3228
  ` + labelled([
4151
- ["State", str6(job["state"]) + (waitingOn === "" ? "" : ` \xB7 waiting on ${waitingOn}`)],
4152
- ["Workbench", str6(job["url"]) || null],
4153
- ["Approve at", str6(job["approval_url"]) || null]
3229
+ ["State", str5(job["state"]) + (waitingOn === "" ? "" : ` \xB7 waiting on ${waitingOn}`)],
3230
+ ["Workbench", str5(job["url"]) || null],
3231
+ ["Approve at", str5(job["approval_url"]) || null]
4154
3232
  ]) + "\n" + RUN_NOTE
4155
3233
  };
4156
3234
  }
@@ -4192,16 +3270,16 @@ function jsonValue(text, what) {
4192
3270
  throw new UsageError(`${what}: not valid JSON after := (${firstLine(error)}); quote a string as name=value`);
4193
3271
  }
4194
3272
  }
4195
- function readParamsFile(path2) {
4196
- const text = readFileOption("--params-file", path2);
3273
+ function readParamsFile(path) {
3274
+ const text = readFileOption("--params-file", path);
4197
3275
  let parsed;
4198
3276
  try {
4199
3277
  parsed = JSON.parse(text);
4200
3278
  } catch (error) {
4201
- throw new UsageError(`--params-file ${path2}: not valid JSON (${firstLine(error)})`);
3279
+ throw new UsageError(`--params-file ${path}: not valid JSON (${firstLine(error)})`);
4202
3280
  }
4203
3281
  if (!isRecord4(parsed)) {
4204
- throw new UsageError(`--params-file ${path2}: the parameters are one JSON object of names and values`);
3282
+ throw new UsageError(`--params-file ${path}: the parameters are one JSON object of names and values`);
4205
3283
  }
4206
3284
  return parsed;
4207
3285
  }
@@ -4213,24 +3291,24 @@ function bareId(value, flag) {
4213
3291
  function procedureJson(data) {
4214
3292
  const procedure = pick2(data, PROCEDURE_FIELDS);
4215
3293
  if ("versions" in procedure) {
4216
- procedure["versions"] = records4(procedure["versions"]).map((version2) => pick2(version2, PROCEDURE_VERSION_FIELDS));
3294
+ procedure["versions"] = records3(procedure["versions"]).map((version2) => pick2(version2, PROCEDURE_VERSION_FIELDS));
4217
3295
  }
4218
3296
  return procedure;
4219
3297
  }
4220
3298
  function procedureDetail(procedure) {
4221
3299
  const current = procedure["current_version"];
4222
- const versions = records4(procedure["versions"]).map(
4223
- (version2) => ` ${clean(`v${str6(version2["version"])}${version2["version"] === current ? " (current)" : ""} ${str6(version2["inserted_at"])} recipe ${str6(version2["recipe_digest"])}`)}
3300
+ const versions = records3(procedure["versions"]).map(
3301
+ (version2) => ` ${clean(`v${str5(version2["version"])}${version2["version"] === current ? " (current)" : ""} ${str5(version2["inserted_at"])} recipe ${str5(version2["recipe_digest"])}`)}
4224
3302
  ${"Capabilities:".padEnd(15)}${clean(list2(version2["capability_ids"]))}
4225
3303
  ${"Actions:".padEnd(15)}${clean(list2(version2["action_roster"]))}
4226
3304
  `
4227
3305
  );
4228
3306
  return `Procedure ${clean(procedure["name"])} \xB7 v${clean(current)} \xB7 ${clean(procedure["id"])}
4229
3307
  ` + labelled([
4230
- ["Saved from", `job ${str6(procedure["job_id"])}`],
4231
- ["Workbench", str6(procedure["job_url"]) || null],
4232
- ["Created", str6(procedure["inserted_at"]) || null],
4233
- ["Updated", str6(procedure["updated_at"]) || null]
3308
+ ["Saved from", `job ${str5(procedure["job_id"])}`],
3309
+ ["Workbench", str5(procedure["job_url"]) || null],
3310
+ ["Created", str5(procedure["inserted_at"]) || null],
3311
+ ["Updated", str5(procedure["updated_at"]) || null]
4234
3312
  ]) + (versions.length === 0 ? "\nVersions: none\n" : "\nVersions:\n" + versions.join("")) + parametersSection(procedure["parameter_schema"]) + "\n" + RUN_NOTE;
4235
3313
  }
4236
3314
  function parametersSection(schema) {
@@ -4238,7 +3316,7 @@ function parametersSection(schema) {
4238
3316
  const lines = JSON.stringify(schema, null, 2).split("\n").map((line) => ` ${clean(line)}
4239
3317
  `).join("");
4240
3318
  const scoped = isRecord4(schema["scoped_inputs"]) ? schema["scoped_inputs"]["required"] : void 0;
4241
- const scopes = Array.isArray(scoped) ? scoped.map(str6) : [];
3319
+ const scopes = Array.isArray(scoped) ? scoped.map(str5) : [];
4242
3320
  let needs = "";
4243
3321
  if (scopes.includes("contact_reconciliation")) {
4244
3322
  needs = "Its runs also need --group <contact-group-id> and --source <uploaded-file-id> (contact_reconciliation).\n";
@@ -4249,18 +3327,18 @@ function parametersSection(schema) {
4249
3327
  return "\nParameters a run takes (JSON schema):\n" + lines + needs;
4250
3328
  }
4251
3329
  function list2(value) {
4252
- return (Array.isArray(value) ? value.map(str6).join(", ") : "") || "none";
3330
+ return (Array.isArray(value) ? value.map(str5).join(", ") : "") || "none";
4253
3331
  }
4254
3332
  function pick2(record, keys) {
4255
3333
  return Object.fromEntries(keys.filter((key) => key in record).map((key) => [key, record[key]]));
4256
3334
  }
4257
- function records4(value) {
3335
+ function records3(value) {
4258
3336
  return Array.isArray(value) ? value.filter(isRecord4) : [];
4259
3337
  }
4260
3338
  function isRecord4(value) {
4261
3339
  return typeof value === "object" && value !== null && !Array.isArray(value);
4262
3340
  }
4263
- function str6(value) {
3341
+ function str5(value) {
4264
3342
  return value === null || value === void 0 ? "" : String(value);
4265
3343
  }
4266
3344
  function firstLine(error) {
@@ -4273,6 +3351,9 @@ import { parseDocument } from "yaml";
4273
3351
  var PLAN_EXECUTORS = ["none", "operator"];
4274
3352
  var CONDITION_KINDS = ["not_before", "gate", "event"];
4275
3353
  var GATE_OWNER_ROLES = ["gp", "compliance_officer", "valuer", "accountant", "auditor", "counsel"];
3354
+ var PLAN_STATUSES = ["held", "released", "running", "completed", "needs_attention"];
3355
+ var PLAN_LIST_MAX_LIMIT = 100;
3356
+ var PLAN_LIST_DEFAULT_LIMIT = 50;
4276
3357
  function parsePlanDocument(text, source) {
4277
3358
  const parsed = extname2(source).toLowerCase() === ".json" ? parseJson(text, source) : parseYamlDocument(text, source);
4278
3359
  if (!isRecord5(parsed)) {
@@ -4311,8 +3392,8 @@ function jsonSafe(value) {
4311
3392
  function firstLine2(error) {
4312
3393
  return (error instanceof Error ? error.message : String(error)).split("\n")[0] ?? "";
4313
3394
  }
4314
- function readPlanFile(path2, flag = "--file") {
4315
- return parsePlanDocument(readFileOption(flag, path2), path2);
3395
+ function readPlanFile(path, flag = "--file") {
3396
+ return parsePlanDocument(readFileOption(flag, path), path);
4316
3397
  }
4317
3398
  async function planCreate(ctx, plan, opts) {
4318
3399
  requireDocument(plan, "a plan is one object: the root's fields and its steps");
@@ -4325,7 +3406,7 @@ async function planCreate(ctx, plan, opts) {
4325
3406
  throw explainMissing(error, `no team ${team} here, or this Harness server has no plan API yet`);
4326
3407
  }
4327
3408
  const created = body.data;
4328
- const id = str7(created["id"]);
3409
+ const id = str6(created["id"]);
4329
3410
  return {
4330
3411
  json: created,
4331
3412
  human: `Created plan ${clean(id)} (${clean(created["status"]) || "held"})
@@ -4335,13 +3416,44 @@ Nothing runs until it is released: ${invokedName()} plan release ${clean(id)}
4335
3416
  `
4336
3417
  };
4337
3418
  }
3419
+ function requirePlanListArgs(opts) {
3420
+ if (opts.status !== void 0 && !PLAN_STATUSES.includes(opts.status)) {
3421
+ throw new UsageError(`status must be one of: ${PLAN_STATUSES.join(", ")}`);
3422
+ }
3423
+ if (opts.limit !== void 0 && (!Number.isInteger(opts.limit) || opts.limit < 1 || opts.limit > PLAN_LIST_MAX_LIMIT)) {
3424
+ throw new UsageError(`--limit takes 1 to ${PLAN_LIST_MAX_LIMIT}: the server returns at most ${PLAN_LIST_MAX_LIMIT} plans`);
3425
+ }
3426
+ }
3427
+ async function planList(ctx, opts) {
3428
+ requirePlanListArgs(opts);
3429
+ const team = requiredTeam(ctx, opts.team);
3430
+ let body;
3431
+ try {
3432
+ body = await ctx.client.get(`/api/v1/teams/${team}/plans`, { status: opts.status, limit: opts.limit });
3433
+ } catch (error) {
3434
+ throw explainMissing(error, `no team ${team} here, or this Harness server cannot list plans yet`);
3435
+ }
3436
+ if (!Array.isArray(body.data)) throw new CommandError("The plan list response carried no list.");
3437
+ const list3 = body.data.filter(isRecord5);
3438
+ const limit = opts.limit ?? PLAN_LIST_DEFAULT_LIMIT;
3439
+ let notice;
3440
+ if (list3.length >= limit) {
3441
+ notice = limit < PLAN_LIST_MAX_LIMIT ? `harness: showing the newest ${list3.length} plans \u2014 pass --limit (up to ${PLAN_LIST_MAX_LIMIT}) for older ones.
3442
+ ` : `harness: showing the newest ${list3.length} plans, the most one list returns \u2014 narrow it with --status.
3443
+ `;
3444
+ }
3445
+ const what = opts.status === void 0 ? "plans" : `${opts.status} plans`;
3446
+ const human = list3.length === 0 ? `No ${what} in team ${team}.
3447
+ ` : plansTable(list3);
3448
+ return notice === void 0 ? { json: body.data, human } : { json: body.data, human, notice };
3449
+ }
4338
3450
  async function planShow(ctx, id) {
4339
- requirePlanId(id);
3451
+ requirePlanRef(id);
4340
3452
  const plan = await planRequest(id, () => ctx.client.get(`/api/v1/plans/${id}`));
4341
3453
  return { json: plan, human: planDetail(plan) };
4342
3454
  }
4343
3455
  async function planEdit(ctx, id, edits) {
4344
- requirePlanId(id);
3456
+ requirePlanRef(id);
4345
3457
  requireDocument(edits, "a plan edit is one object: steps to change and budgets");
4346
3458
  const plan = await planRequest(id, () => ctx.client.patch(`/api/v1/plans/${id}`, edits));
4347
3459
  const held = Array.isArray(plan["held_steps"]) && plan["held_steps"].length > 0;
@@ -4350,12 +3462,12 @@ async function planEdit(ctx, id, edits) {
4350
3462
  human: `Updated plan ${clean(id)}
4351
3463
 
4352
3464
  ` + planDetail(plan) + (held ? `
4353
- Release it again to run the held steps: ${invokedName()} plan release ${clean(id)}
3465
+ Only Operator steps are held until the next release; release the plan again to take in the changes: ${invokedName()} plan release ${clean(id)}
4354
3466
  ` : "")
4355
3467
  };
4356
3468
  }
4357
3469
  async function planRelease(ctx, id, opts) {
4358
- requirePlanId(id);
3470
+ requirePlanRef(id);
4359
3471
  const body = {};
4360
3472
  if (opts.collaborators !== void 0 && opts.collaborators.length > 0) body["collaborators"] = opts.collaborators;
4361
3473
  if (opts.retryBudget !== void 0) body["retry_budget"] = opts.retryBudget;
@@ -4370,7 +3482,7 @@ async function planRelease(ctx, id, opts) {
4370
3482
  };
4371
3483
  }
4372
3484
  async function planHold(ctx, id) {
4373
- requirePlanId(id);
3485
+ requirePlanRef(id);
4374
3486
  const plan = await planRequest(id, () => ctx.client.post(`/api/v1/plans/${id}/hold`));
4375
3487
  return {
4376
3488
  json: plan,
@@ -4383,25 +3495,41 @@ async function gateSet(ctx, issue, gate, satisfied) {
4383
3495
  const wanted = gate.trim();
4384
3496
  if (wanted === "") throw new UsageError("name the gate by its key, its label or its condition id");
4385
3497
  const expert = isExpertToken(ctx.config.token);
4386
- const list3 = await readConditions(ctx, issue, expert);
4387
- const target = findGate(list3, issue, wanted, expert);
4388
- const id = str7(target["id"]);
3498
+ let list3 = null;
3499
+ if (!isUuid(wanted)) {
3500
+ try {
3501
+ list3 = await readConditions(ctx, issue, expert);
3502
+ } catch (error) {
3503
+ if (error instanceof HttpError && error.status === 403) {
3504
+ throw new HttpError(
3505
+ error.status,
3506
+ `${error.message} \u2014 find the gate by its condition id; only Harness staff can list an issue's conditions`,
3507
+ error.body,
3508
+ error.retryAfter
3509
+ );
3510
+ }
3511
+ throw error;
3512
+ }
3513
+ }
3514
+ const target = list3 === null ? { id: wanted.toLowerCase(), label: wanted.toLowerCase() } : findGate(list3, issue, wanted, expert);
3515
+ const id = str6(target["id"]);
4389
3516
  let body;
4390
3517
  try {
4391
3518
  body = await ctx.client.put(`/api/v1/issues/${issue}/conditions/${id}/satisfaction`, { satisfied });
4392
3519
  } catch (error) {
4393
- throw await explainNotGateOwner(ctx, error, target, issue, satisfied);
3520
+ throw await explainNotGateOwner(ctx, error, target, issue, satisfied, list3 !== null);
4394
3521
  }
4395
- const others = list3.filter((condition) => condition["id"] !== id && condition["satisfied"] !== true).length;
3522
+ const others = list3?.filter((condition) => condition["id"] !== id && condition["satisfied"] !== true).length ?? 0;
4396
3523
  let tail = "";
4397
- if (satisfied && expert) {
3524
+ if (satisfied && list3 !== null && expert) {
4398
3525
  tail = others > 0 ? ` \u2014 ${others} other gate${others === 1 ? "" : "s"} of yours on it ${others === 1 ? "is" : "are"} open` : "";
4399
- } else if (satisfied) {
3526
+ } else if (satisfied && list3 !== null) {
4400
3527
  tail = others > 0 ? ` \u2014 it still waits on ${others} other condition${others === 1 ? "" : "s"}` : " \u2014 no other wait condition holds it";
4401
3528
  }
3529
+ const label = list3 === null && isRecord5(body.data) ? body.data["label"] ?? id : target["label"];
4402
3530
  return {
4403
3531
  json: body.data,
4404
- human: `${satisfied ? "Satisfied" : "Un-satisfied"} gate "${clean(target["label"])}" on ${issue}${tail}
3532
+ human: `${satisfied ? "Satisfied" : "Un-satisfied"} gate "${clean(label)}" on ${issue}${tail}
4405
3533
  `
4406
3534
  };
4407
3535
  }
@@ -4435,8 +3563,10 @@ async function readConditions(ctx, issue, expert) {
4435
3563
  if (!Array.isArray(conditions)) throw new CommandError("The conditions response carried no list.");
4436
3564
  return conditions.filter(isRecord5);
4437
3565
  }
4438
- function requirePlanId(id) {
4439
- if (!isUuid(id)) throw new UsageError(`not a plan id: ${id} (expected the plan's UUID)`);
3566
+ function requirePlanRef(id) {
3567
+ if (!isUuid(id) && !isIdentifier(id)) {
3568
+ throw new UsageError(`not a plan: ${id} (expected the plan's id or its root issue's identifier, e.g. FR-1)`);
3569
+ }
4440
3570
  }
4441
3571
  function requireDocument(value, what) {
4442
3572
  if (!isRecord5(value)) throw new UsageError(what);
@@ -4445,18 +3575,21 @@ async function planRequest(id, send) {
4445
3575
  try {
4446
3576
  return (await send()).data;
4447
3577
  } catch (error) {
4448
- throw explainMissing(error, `no plan ${id} in this organization, or this Harness server has no plan API yet`);
3578
+ throw explainMissing(
3579
+ error,
3580
+ isUuid(id) ? `no plan ${id} in this organization, or this Harness server has no plan API yet` : `${id} roots no plan in this organization (${invokedName()} show ${id} names the plan an issue belongs to, by its root), or this Harness server opens plans by their id only`
3581
+ );
4449
3582
  }
4450
3583
  }
4451
3584
  function explainMissing(error, why) {
4452
3585
  if (!(error instanceof HttpError) || error.status !== 404) return error;
4453
3586
  return new HttpError(404, `${error.message} \u2014 ${why}`, error.body, error.retryAfter);
4454
3587
  }
4455
- async function explainNotGateOwner(ctx, error, gate, issue, satisfied) {
3588
+ async function explainNotGateOwner(ctx, error, gate, issue, satisfied, allowLookup) {
4456
3589
  if (!(error instanceof HttpError) || error.status !== 403 || error.body["reason"] !== "not_gate_owner") return error;
4457
3590
  let current;
4458
3591
  let currentOwner = error.body["owner"];
4459
- if (currentOwner !== null && !isRecord5(currentOwner)) {
3592
+ if (allowLookup && currentOwner !== null && !isRecord5(currentOwner)) {
4460
3593
  try {
4461
3594
  const fresh = (await ctx.client.get(`/api/v1/issues/${issue}/conditions`)).data;
4462
3595
  if (Array.isArray(fresh)) current = fresh.filter(isRecord5).find((condition) => condition["id"] === gate["id"]);
@@ -4470,26 +3603,20 @@ async function explainNotGateOwner(ctx, error, gate, issue, satisfied) {
4470
3603
  const message = satisfied ? `Cannot satisfy ${named2}: only ${subject} can tick it` : `Cannot un-satisfy ${named2}: only ${subject} or Harness staff can untick it`;
4471
3604
  return new HttpError(error.status, message, error.body, error.retryAfter);
4472
3605
  }
4473
- var gateLabel = (condition) => str7(condition["label"]).trim();
4474
- var gateKey = (condition) => str7(condition["key"]);
3606
+ var gateLabel = (condition) => str6(condition["label"]).trim();
3607
+ var gateKey = (condition) => str6(condition["key"]);
4475
3608
  function exactMatches(conditions, wanted) {
4476
- const id = isUuid(wanted) ? wanted.toLowerCase() : null;
4477
- return conditions.filter((condition) => {
4478
- if (id !== null && str7(condition["id"]).toLowerCase() === id) return true;
4479
- if (condition["kind"] !== "gate") return false;
4480
- const key = gateKey(condition);
4481
- return key !== "" && key === (id ?? wanted) || gateLabel(condition) === wanted;
4482
- });
3609
+ return conditions.filter(
3610
+ (condition) => condition["kind"] === "gate" && (gateKey(condition) !== "" && gateKey(condition) === wanted || gateLabel(condition) === wanted)
3611
+ );
4483
3612
  }
4484
3613
  function ambiguous(conditions, issue, wanted, matches) {
4485
3614
  const listed = matches.map((condition) => {
4486
3615
  const what = condition["kind"] === "gate" ? `gate ${gateName({ label: gateLabel(condition), key: gateKey(condition) })}` : `${clean(condition["kind"])} condition`;
4487
3616
  return `${clean(condition["id"])} (${what})`;
4488
3617
  }).join(", ");
4489
- const unnameable = matches.map((condition) => str7(condition["id"])).filter((id) => exactMatches(conditions, id).length !== 1).map((id) => clean(id));
4490
- const caveat = unnameable.length === 0 ? "" : ` (${unnameable.join(", ")} ${unnameable.length === 1 ? "is" : "are"} also another gate's key or label, so ${unnameable.length === 1 ? "it" : "they"} cannot be named by id)`;
4491
3618
  return new CommandError(
4492
- `several gates on ${issue} match "${wanted}": ${listed} \u2014 pass the condition id of the one you mean${caveat}`
3619
+ `several gates on ${issue} match "${wanted}": ${listed} \u2014 pass the condition id of the one you mean`
4493
3620
  );
4494
3621
  }
4495
3622
  function findGate(conditions, issue, wanted, expert = false) {
@@ -4497,11 +3624,6 @@ function findGate(conditions, issue, wanted, expert = false) {
4497
3624
  if (exact.length > 1) throw ambiguous(conditions, issue, wanted, exact);
4498
3625
  if (exact.length === 1) {
4499
3626
  const only = exact[0];
4500
- if (only["kind"] !== "gate") {
4501
- throw new CommandError(
4502
- `condition ${wanted} on ${issue} is a ${clean(only["kind"])} condition \u2014 only a gate is satisfied by hand`
4503
- );
4504
- }
4505
3627
  return only;
4506
3628
  }
4507
3629
  const gates = conditions.filter((condition) => condition["kind"] === "gate");
@@ -4511,7 +3633,6 @@ function findGate(conditions, issue, wanted, expert = false) {
4511
3633
  );
4512
3634
  if (loose.length > 1) throw ambiguous(conditions, issue, wanted, loose);
4513
3635
  if (loose.length === 1) return loose[0];
4514
- if (isUuid(wanted)) throw new CommandError(`No condition ${wanted} on ${issue}${expert ? " that you own" : ""}.`);
4515
3636
  const none = expert ? `No gate of yours on ${issue}` : `No gate on ${issue}`;
4516
3637
  if (gates.length === 0) {
4517
3638
  throw new CommandError(`${none} matches "${wanted}" \u2014 ${expert ? "you own no gate on it" : "it has no gates"}`);
@@ -4522,7 +3643,7 @@ function findGate(conditions, issue, wanted, expert = false) {
4522
3643
  function isRecord5(value) {
4523
3644
  return typeof value === "object" && value !== null && !Array.isArray(value);
4524
3645
  }
4525
- function str7(value) {
3646
+ function str6(value) {
4526
3647
  return value === null || value === void 0 ? "" : String(value);
4527
3648
  }
4528
3649
 
@@ -4543,265 +3664,7 @@ function isRecord6(value) {
4543
3664
  return typeof value === "object" && value !== null && !Array.isArray(value);
4544
3665
  }
4545
3666
 
4546
- // src/core/credentials.ts
4547
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
4548
- import { homedir } from "os";
4549
- import { dirname as dirname2, isAbsolute, join as join2 } from "path";
4550
- function defaultCredentialsPath(env = process.env) {
4551
- const xdg = env["XDG_CONFIG_HOME"];
4552
- const base = xdg !== void 0 && xdg !== "" && isAbsolute(xdg) ? xdg : join2(homedir(), ".config");
4553
- return join2(base, "harness", "credentials.json");
4554
- }
4555
- function readCredentials(path2) {
4556
- try {
4557
- const parsed = JSON.parse(readFileSync3(path2, "utf8"));
4558
- if (typeof parsed === "object" && parsed !== null) {
4559
- const { url, token } = parsed;
4560
- if (typeof url === "string" && typeof token === "string") return { url, token };
4561
- }
4562
- } catch {
4563
- }
4564
- return null;
4565
- }
4566
- var uniqueCounter = 0;
4567
- function writeCredentials(path2, url, token) {
4568
- mkdirSync2(dirname2(path2), { recursive: true });
4569
- const tempPath = `${path2}.${process.pid}.${++uniqueCounter}.tmp`;
4570
- try {
4571
- writeFileSync2(tempPath, "", { mode: 384 });
4572
- chmodSync2(tempPath, 384);
4573
- writeFileSync2(tempPath, JSON.stringify({ url, token }));
4574
- renameSync2(tempPath, path2);
4575
- } finally {
4576
- rmSync2(tempPath, { force: true });
4577
- }
4578
- }
4579
- function deleteCredentials(path2) {
4580
- rmSync2(path2, { force: true });
4581
- }
4582
-
4583
- // src/core/repo-config.ts
4584
- import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
4585
- import { homedir as homedir2 } from "os";
4586
- import { dirname as dirname3, isAbsolute as isAbsolute2, join as join3, parse } from "path";
4587
- var CONFIG_RELATIVE_PATH = join3(".harness-cli", "config.json");
4588
- function findRepoConfig(startDir = process.cwd()) {
4589
- let dir = startDir;
4590
- const { root } = parse(startDir);
4591
- for (; ; ) {
4592
- const candidate = join3(dir, CONFIG_RELATIVE_PATH);
4593
- if (existsSync(candidate)) {
4594
- return readRepoConfig(candidate);
4595
- }
4596
- if (dir === root) return { config: null, warnings: [] };
4597
- dir = dirname3(dir);
4598
- }
4599
- }
4600
- function readRepoConfig(path2) {
4601
- let parsed;
4602
- try {
4603
- parsed = JSON.parse(readFileSync4(path2, "utf8"));
4604
- } catch (error) {
4605
- const detail = error instanceof Error ? error.message : String(error);
4606
- return { config: null, warnings: [`harness: ignoring malformed ${path2} (${detail})`] };
4607
- }
4608
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
4609
- return { config: null, warnings: [`harness: ignoring ${path2} \u2014 expected a JSON object`] };
4610
- }
4611
- const record = parsed;
4612
- const config = { path: path2 };
4613
- const warnings = [];
4614
- for (const field3 of ["team", "url", "org"]) {
4615
- const value = record[field3];
4616
- if (value === void 0) continue;
4617
- if (typeof value === "string" && value.trim() !== "") {
4618
- config[field3] = value.trim();
4619
- } else {
4620
- warnings.push(`harness: ignoring "${field3}" in ${path2} \u2014 expected a non-empty string`);
4621
- }
4622
- }
4623
- return { config, warnings };
4624
- }
4625
- function trustedHostsPath(env = process.env) {
4626
- const xdg = env["XDG_CONFIG_HOME"];
4627
- const base = xdg !== void 0 && xdg !== "" && isAbsolute2(xdg) ? xdg : join3(homedir2(), ".config");
4628
- return join3(base, "harness", "trusted-hosts.json");
4629
- }
4630
- function readTrustedHosts(env) {
4631
- try {
4632
- const parsed = JSON.parse(readFileSync4(trustedHostsPath(env), "utf8"));
4633
- if (typeof parsed === "object" && parsed !== null && Array.isArray(parsed["hosts"])) {
4634
- return parsed["hosts"].filter(
4635
- (host) => typeof host === "string"
4636
- );
4637
- }
4638
- } catch {
4639
- }
4640
- return [];
4641
- }
4642
- function isHostTrusted(baseUrl, env) {
4643
- return readTrustedHosts(env).includes(baseUrl);
4644
- }
4645
- function trustHost(baseUrl, env) {
4646
- const hosts = readTrustedHosts(env);
4647
- if (hosts.includes(baseUrl)) return;
4648
- const path2 = trustedHostsPath(env);
4649
- mkdirSync3(dirname3(path2), { recursive: true });
4650
- writeFileSync3(path2, JSON.stringify({ hosts: [...hosts, baseUrl] }, null, 2) + "\n");
4651
- }
4652
-
4653
- // src/core/config.ts
4654
- import { homedir as homedir3, tmpdir } from "os";
4655
- import { isAbsolute as isAbsolute3, join as join4 } from "path";
4656
- var DEFAULT_BASE_URL = "https://harness.infra.one";
4657
- function resolveConfig(env = process.env, credentialsPath, repo) {
4658
- let warnings = [];
4659
- if (repo === void 0) {
4660
- const lookup = findRepoConfig();
4661
- repo = lookup.config;
4662
- warnings = lookup.warnings;
4663
- }
4664
- const path2 = credentialsPath ?? defaultCredentialsPath(env);
4665
- const stored = readCredentials(path2);
4666
- const envUrl = blankToNull(env["HARNESS_API_URL"]);
4667
- const envToken = blankToNull(env["HARNESS_API_TOKEN"]);
4668
- const repoUrl = repo?.url?.replace(/\/+$/, "") ?? null;
4669
- let rawUrl;
4670
- let hostSource;
4671
- if (envUrl !== null) {
4672
- rawUrl = envUrl;
4673
- hostSource = "env";
4674
- } else if (envToken !== null) {
4675
- if (repoUrl !== null && normalize(repoUrl) !== normalize(DEFAULT_BASE_URL)) {
4676
- return {
4677
- kind: "error",
4678
- message: `this repo's config (${repo?.path}) targets ${repoUrl}, but HARNESS_API_TOKEN only travels to the host the environment names \u2014 set HARNESS_API_URL explicitly to pair them`,
4679
- warnings
4680
- };
4681
- }
4682
- rawUrl = DEFAULT_BASE_URL;
4683
- hostSource = "default";
4684
- } else if (repoUrl !== null) {
4685
- rawUrl = repoUrl;
4686
- hostSource = "repo-config";
4687
- } else if (stored !== null) {
4688
- rawUrl = stored.url;
4689
- hostSource = "stored-login";
4690
- } else {
4691
- rawUrl = DEFAULT_BASE_URL;
4692
- hostSource = "default";
4693
- }
4694
- const validated = validateBaseUrl(rawUrl);
4695
- if (!validated.ok) return { kind: "error", message: validated.message, warnings };
4696
- const baseUrl = rawUrl.replace(/\/+$/, "");
4697
- let token;
4698
- if (envToken !== null) {
4699
- token = envToken;
4700
- } else if (stored !== null && normalize(stored.url) !== null && normalize(stored.url) === normalize(baseUrl)) {
4701
- token = stored.token;
4702
- } else {
4703
- const result = { kind: "needs-login", baseUrl, hostSource, warnings };
4704
- if (hostSource === "repo-config" && repo?.path !== void 0) result.repoConfigPath = repo.path;
4705
- return result;
4706
- }
4707
- return {
4708
- kind: "ok",
4709
- config: {
4710
- baseUrl,
4711
- token,
4712
- actor: blankToNull(env["HARNESS_ACTOR"]),
4713
- // The committed team is the fallback: the environment (and, above it,
4714
- // the --team flag at the command layer) stays authoritative.
4715
- team: blankToNull(env["HARNESS_TEAM"]) ?? repo?.team ?? null,
4716
- cacheDir: resolveCacheDir(env),
4717
- hostSource
4718
- },
4719
- warnings
4720
- };
4721
- }
4722
- function resolveCacheDir(env = process.env) {
4723
- return absolute(env["HARNESS_CACHE_DIR"]) ?? join4(resolveCacheHome(env), "harness", "attachments");
4724
- }
4725
- function absolute(value) {
4726
- return value !== void 0 && value !== "" && isAbsolute3(value) ? value : null;
4727
- }
4728
- function resolveCacheHome(env = process.env) {
4729
- return absolute(env["XDG_CACHE_HOME"]) ?? join4(homedir3() || tmpdir(), ".cache");
4730
- }
4731
- function normalize(url) {
4732
- let parsed;
4733
- try {
4734
- parsed = new URL(url);
4735
- } catch {
4736
- return null;
4737
- }
4738
- const port = parsed.port !== "" ? parsed.port : parsed.protocol === "https:" ? "443" : parsed.protocol === "http:" ? "80" : "";
4739
- return JSON.stringify([parsed.protocol, parsed.hostname, port, parsed.pathname.replace(/\/+$/, "")]);
4740
- }
4741
- function validateBaseUrl(baseUrl) {
4742
- let parsed;
4743
- try {
4744
- parsed = new URL(baseUrl);
4745
- } catch {
4746
- return { ok: false, message: `HARNESS_API_URL must be an http(s) URL with a host, got: ${baseUrl}` };
4747
- }
4748
- if (parsed.username !== "" || parsed.password !== "") {
4749
- return { ok: false, message: "HARNESS_API_URL must not embed credentials (user:pass@) \u2014 use HARNESS_API_TOKEN" };
4750
- }
4751
- if (parsed.search !== "" || parsed.hash !== "") {
4752
- return { ok: false, message: `HARNESS_API_URL must not carry a query or fragment, got: ${baseUrl}` };
4753
- }
4754
- if (parsed.protocol === "https:" && parsed.hostname !== "") return { ok: true };
4755
- if (parsed.protocol === "http:") {
4756
- if (["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname)) return { ok: true };
4757
- return {
4758
- ok: false,
4759
- message: `refusing plain-http HARNESS_API_URL (${baseUrl}) \u2014 the API token would travel in cleartext. Use https, or http against localhost only.`
4760
- };
4761
- }
4762
- return { ok: false, message: `HARNESS_API_URL must be an http(s) URL with a host, got: ${baseUrl}` };
4763
- }
4764
- function loginHelp(baseUrl, name = invokedName()) {
4765
- return `Not signed in to ${baseUrl}.
4766
-
4767
- Interactively, in a terminal (not from inside an agent \u2014 the browser
4768
- tab needs someone to see it):
4769
-
4770
- ${name} login
4771
-
4772
- For agents and CI, mint an API token (admin \u2192 API tokens) with the
4773
- harness:read scope \u2014 plus harness:write only where the workflow
4774
- creates or closes \u2014 set an expiry, and export it:
4775
-
4776
- export HARNESS_API_TOKEN=aone_...
4777
-
4778
- An outside expert uses the pxc_ credential minted in their expert view:
4779
- export it the same way, or paste it into \`${name} login --no-browser\`.
4780
- `;
4781
- }
4782
- function blankToNull(value) {
4783
- if (value === void 0 || value.trim() === "") return null;
4784
- return value;
4785
- }
4786
-
4787
3667
  export {
4788
- HttpError,
4789
- UpgradeRequiredError,
4790
- TransportError,
4791
- UsageError,
4792
- LocalError,
4793
- CommandError,
4794
- EXPERT_ASSIGNMENTS_PATH,
4795
- isExpertToken,
4796
- EXPERT_LAPSED,
4797
- explainExpertRefusal,
4798
- SESSION_PATH,
4799
- isLoginCredential,
4800
- Client,
4801
- clean,
4802
- json,
4803
- expertSummary,
4804
- errorDetails,
4805
3668
  MAX_IDEMPOTENCY_KEY_BYTES,
4806
3669
  IDEMPOTENCY_KEY_FORMAT,
4807
3670
  requireIdentifiers,
@@ -4850,7 +3713,7 @@ export {
4850
3713
  linkAdd,
4851
3714
  linkUpdate,
4852
3715
  linkRm,
4853
- labelList2 as labelList,
3716
+ labelList,
4854
3717
  labelCreate,
4855
3718
  teamCreate,
4856
3719
  teamList,
@@ -4913,26 +3776,18 @@ export {
4913
3776
  PLAN_EXECUTORS,
4914
3777
  CONDITION_KINDS,
4915
3778
  GATE_OWNER_ROLES,
3779
+ PLAN_STATUSES,
3780
+ PLAN_LIST_MAX_LIMIT,
4916
3781
  readPlanFile,
4917
3782
  planCreate,
3783
+ requirePlanListArgs,
3784
+ planList,
4918
3785
  planShow,
4919
3786
  planEdit,
4920
3787
  planRelease,
4921
3788
  planHold,
4922
3789
  gateSet,
4923
3790
  conditionList,
4924
- requirePlanId,
4925
- assignments,
4926
- defaultCredentialsPath,
4927
- readCredentials,
4928
- writeCredentials,
4929
- deleteCredentials,
4930
- findRepoConfig,
4931
- isHostTrusted,
4932
- trustHost,
4933
- DEFAULT_BASE_URL,
4934
- resolveConfig,
4935
- resolveCacheHome,
4936
- validateBaseUrl,
4937
- loginHelp
3791
+ requirePlanRef,
3792
+ assignments
4938
3793
  };