@allocator-one/harness-cli 1.13.0 → 1.14.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.
@@ -25,6 +25,12 @@ var TransportError = class extends Error {
25
25
  this.name = "TransportError";
26
26
  }
27
27
  };
28
+ var MalformedResponseError = class extends TransportError {
29
+ constructor(message) {
30
+ super(message);
31
+ this.name = "MalformedResponseError";
32
+ }
33
+ };
28
34
  var UsageError = class extends Error {
29
35
  constructor(message) {
30
36
  super(message);
@@ -178,10 +184,10 @@ var Client = class {
178
184
  try {
179
185
  parsed = JSON.parse(text);
180
186
  } catch {
181
- throw new TransportError(`the API answered with a non-JSON body (HTTP ${response.status})`);
187
+ throw new MalformedResponseError(`the API answered with a non-JSON body (HTTP ${response.status})`);
182
188
  }
183
189
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
184
- throw new TransportError(`the API answered with an invalid JSON envelope (HTTP ${response.status})`);
190
+ throw new MalformedResponseError(`the API answered with an invalid JSON envelope (HTTP ${response.status})`);
185
191
  }
186
192
  return parsed;
187
193
  }
@@ -230,16 +236,31 @@ function clean(value) {
230
236
  function json(term) {
231
237
  return JSON.stringify(term) + "\n";
232
238
  }
233
- function issuesTable(issues) {
239
+ var OPERATOR_TEAM_KEY = /^OPERATOR\d*$/;
240
+ var OPERATOR_TEAM_NAME = "Operator";
241
+ var AGENT_MARK = "[agent]";
242
+ var AGENT_LEGEND = `
243
+ ${AGENT_MARK} Operator runs this step for its plan; only the plan driver claims it.
244
+ `;
245
+ function isAgentStep(issue) {
246
+ return OPERATOR_TEAM_KEY.test(str(path(issue, "team", "key"))) && str(path(issue, "team", "name")) === OPERATOR_TEAM_NAME;
247
+ }
248
+ function issuesTable(issues, options = {}) {
234
249
  if (issues.length === 0) return "No issues.\n";
235
- const rows = issues.map((issue) => [
236
- str(issue["identifier"]),
237
- str(path(issue, "state", "name")) || "-",
238
- str(issue["priority_label"]) || "-",
239
- str(path(issue, "assignee", "name")) || "-",
240
- truncate(issue["title"])
241
- ]);
242
- return table(["ID", "STATE", "PRIORITY", "ASSIGNEE", "TITLE"], rows);
250
+ let marked = false;
251
+ const rows = issues.map((issue) => {
252
+ const agentStep = options.markAgentSteps === true && isAgentStep(issue);
253
+ if (agentStep) marked = true;
254
+ return [
255
+ str(issue["identifier"]),
256
+ str(path(issue, "state", "name")) || "-",
257
+ str(issue["priority_label"]) || "-",
258
+ str(path(issue, "assignee", "name")) || "-",
259
+ agentStep ? truncate(`${AGENT_MARK} ${clean(issue["title"])}`) : truncate(issue["title"])
260
+ ];
261
+ });
262
+ const rendered = table(["ID", "STATE", "PRIORITY", "ASSIGNEE", "TITLE"], rows);
263
+ return marked ? rendered + AGENT_LEGEND : rendered;
243
264
  }
244
265
  function issueDetail(issue, images = []) {
245
266
  const header = `${str(issue["identifier"])} \xB7 ${clean(issue["title"])}
@@ -251,6 +272,7 @@ function issueDetail(issue, images = []) {
251
272
  ["Team", path(issue, "team", "key")],
252
273
  ["Labels", labelsLine(issue["labels"])],
253
274
  ["Blocked", blockedLine(issue)],
275
+ ["Waiting", waitingLine(issue)],
254
276
  ["Due", issue["due_date"]],
255
277
  ["Updated", issue["updated_at"]]
256
278
  ];
@@ -266,7 +288,7 @@ function issueDetail(issue, images = []) {
266
288
  ${title}:
267
289
  ${indent(String(text))}
268
290
  `).join("");
269
- return header + fieldLines + sectionText + relationsSection(issue["relations"]) + childrenSection(issue["children"]) + commentsSection(issue["comments"]) + imagesSection(images);
291
+ return header + fieldLines + sectionText + conditionsSection(issue["conditions"]) + relationsSection(issue["relations"]) + childrenSection(issue["children"]) + commentsSection(issue["comments"]) + imagesSection(images);
270
292
  }
271
293
  function activityEntries(entries) {
272
294
  if (entries.length === 0) return "No activity.\n";
@@ -411,6 +433,185 @@ function table(headers, rows) {
411
433
  (row) => row.map((cell, column) => padGraphemes(cell, widths[column] ?? 0)).join(" ").replace(/\s+$/, "") + "\n"
412
434
  ).join("");
413
435
  }
436
+ function records(value) {
437
+ if (!Array.isArray(value)) return [];
438
+ return value.filter(
439
+ (entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry)
440
+ );
441
+ }
442
+ function waitingLine(issue) {
443
+ if (issue["waiting_on_condition"] !== true) return null;
444
+ const conditions = records(issue["conditions"]);
445
+ const open = conditions.filter((condition) => condition["satisfied"] !== true).length;
446
+ return `yes, on ${open} of ${conditions.length} wait condition${conditions.length === 1 ? "" : "s"}`;
447
+ }
448
+ function conditionsSection(conditions) {
449
+ const list2 = records(conditions);
450
+ if (list2.length === 0) return "";
451
+ return "\nWait conditions:\n" + list2.map((condition) => ` ${conditionLine(condition)}
452
+ `).join("");
453
+ }
454
+ function conditionLine(condition) {
455
+ const satisfied = condition["satisfied"] === true;
456
+ let what;
457
+ switch (condition["kind"]) {
458
+ case "gate":
459
+ what = `gate "${clean(condition["label"])}"`;
460
+ break;
461
+ case "not_before":
462
+ what = `not before ${clean(condition["not_before"])}`;
463
+ break;
464
+ case "event":
465
+ what = "system event";
466
+ break;
467
+ default:
468
+ what = clean(condition["kind"]) || "condition";
469
+ }
470
+ let when = "";
471
+ if (satisfied && typeof condition["satisfied_at"] === "string") {
472
+ const by = clean(path(condition, "satisfied_by", "name"));
473
+ when = ` (satisfied ${clean(condition["satisfied_at"])}${by === "" ? "" : ` by ${by}`})`;
474
+ }
475
+ return `${satisfied ? "[x]" : "[ ]"} ${what}${when} \xB7 ${clean(condition["id"])}`;
476
+ }
477
+ var LIVE_PLAN_STATUSES = ["released", "running", "needs_attention"];
478
+ function planDetail(plan) {
479
+ const steps = records(plan["steps"]);
480
+ const fields = [
481
+ ["Root", planRootLine(plan["root"])],
482
+ ["Revision", planRevisionLine(plan)],
483
+ ["Budgets", planBudgetsLine(plan)],
484
+ ["Estimate", planEstimateLine(plan["estimate"])]
485
+ ];
486
+ const fieldLines = fields.filter((field2) => field2[1] !== null).map(([label, value]) => ` ${(label + ":").padEnd(12)}${value}
487
+ `).join("");
488
+ return `Plan ${clean(plan["id"])} \xB7 ${clean(plan["status"])}
489
+ ` + fieldLines + planStepsSection(steps, str(plan["status"])) + planEdgesSection(plan["edges"]) + planConditionsSection(steps) + planHeldSection(plan["held_steps"]);
490
+ }
491
+ function planRootLine(root) {
492
+ if (typeof root !== "object" || root === null) return null;
493
+ const record = root;
494
+ return `${clean(record["identifier"])} \xB7 ${clean(record["title"])}`;
495
+ }
496
+ function planRevisionLine(plan) {
497
+ const revision = plan["revision"];
498
+ if (typeof revision !== "object" || revision === null) return "none yet (never released)";
499
+ const number = clean(revision["number"]);
500
+ const by = clean(path(plan, "released_by", "name"));
501
+ const at = clean(plan["released_at"]);
502
+ if (by === "" && at === "") return number;
503
+ return `${number} \xB7 released${by === "" ? "" : ` by ${by}`}${at === "" ? "" : ` at ${at}`}`;
504
+ }
505
+ function planBudgetsLine(plan) {
506
+ const retries = plan["retry_budget"];
507
+ const retryText = typeof retries === "number" ? `${retries} ${retries === 1 ? "retry" : "retries"} per failed Operator step` : null;
508
+ const spend = plan["spend_budget_cents"];
509
+ const spendText = typeof spend === "number" ? `spend budget ${spend} cents` : "no spend budget";
510
+ return [retryText, spendText].filter((part) => part !== null).join(" \xB7 ");
511
+ }
512
+ function planEstimateLine(estimate) {
513
+ if (typeof estimate !== "object" || estimate === null) return null;
514
+ const record = estimate;
515
+ const ceiling = record["ceiling_cents"];
516
+ if (typeof ceiling !== "number") return null;
517
+ const minimum = record["minimum_budget_cents"];
518
+ return `up to ${ceiling} cents${typeof minimum === "number" ? ` (minimum budget ${minimum} cents)` : ""}`;
519
+ }
520
+ function planStepsSection(steps, planStatus) {
521
+ if (steps.length === 0) return "";
522
+ const depths = stepDepths(steps);
523
+ const rows = steps.map((step) => [
524
+ " ".repeat(depths.get(str(step["identifier"])) ?? 0) + str(step["identifier"]),
525
+ str(step["key"]) || "-",
526
+ str(step["executor"]) || "-",
527
+ str(path(step, "state", "name")) || "-",
528
+ stepStatus(step, planStatus),
529
+ truncate(step["title"])
530
+ ]);
531
+ const rendered = table(["STEP", "KEY", "EXECUTOR", "STATE", "STATUS", "TITLE"], rows);
532
+ return "\nSteps:\n" + indentLines(rendered);
533
+ }
534
+ function stepDepths(steps) {
535
+ const parents = new Map(steps.map((step) => [str(step["identifier"]), str(step["parent"])]));
536
+ const depths = /* @__PURE__ */ new Map();
537
+ for (const identifier of parents.keys()) {
538
+ let depth = 0;
539
+ let parent = parents.get(identifier) ?? "";
540
+ const seen = /* @__PURE__ */ new Set([identifier]);
541
+ while (parent !== "" && parents.has(parent) && !seen.has(parent)) {
542
+ seen.add(parent);
543
+ depth += 1;
544
+ parent = parents.get(parent) ?? "";
545
+ }
546
+ depths.set(identifier, depth);
547
+ }
548
+ return depths;
549
+ }
550
+ function stepStatus(step, planStatus) {
551
+ const stateType = path(step, "state", "type");
552
+ if (step["archived"] === true) return "archived";
553
+ if (stateType === "completed") return "done";
554
+ if (stateType === "canceled") return "cancelled";
555
+ if (step["container"] === true) return "container";
556
+ if (step["ready"] === true) return "ready";
557
+ if (step["held"] === true) return "held";
558
+ if (step["in_revision"] === false && LIVE_PLAN_STATUSES.includes(planStatus)) return "not in revision";
559
+ if (records(step["conditions"]).some((condition) => condition["satisfied"] !== true)) return "waiting";
560
+ return "-";
561
+ }
562
+ function planEdgesSection(edges) {
563
+ const list2 = records(edges);
564
+ if (list2.length === 0) return "";
565
+ return "\nEdges (blocker \u2192 blocked):\n" + list2.map((edge) => ` ${clean(edge["from"])} \u2192 ${clean(edge["to"])}
566
+ `).join("");
567
+ }
568
+ function planConditionsSection(steps) {
569
+ const lines = steps.flatMap(
570
+ (step) => records(step["conditions"]).map((condition) => [clean(step["identifier"]), conditionLine(condition)])
571
+ );
572
+ if (lines.length === 0) return "";
573
+ const width = Math.max(...lines.map(([identifier]) => graphemeLength(identifier)));
574
+ return "\nWait conditions:\n" + lines.map(([identifier, line]) => ` ${padGraphemes(identifier, width)} ${line}
575
+ `).join("");
576
+ }
577
+ function planHeldSection(heldSteps) {
578
+ if (!Array.isArray(heldSteps) || heldSteps.length === 0) return "";
579
+ return `
580
+ Held by an edit until the plan is released again: ${heldSteps.map((step) => clean(step)).join(", ")}
581
+ `;
582
+ }
583
+ function indentLines(text) {
584
+ return text.split("\n").map((line) => line === "" ? line : " " + line).join("\n");
585
+ }
586
+ function errorDetails(body) {
587
+ return changesetDetails(body["details"]) + violationDetails(body["violations"]);
588
+ }
589
+ function changesetDetails(details) {
590
+ if (typeof details !== "object" || details === null || Array.isArray(details)) return "";
591
+ return Object.entries(details).map(([field2, messages]) => {
592
+ const text = Array.isArray(messages) ? messages.map(String).join("; ") : String(messages);
593
+ return `
594
+ ${clean(field2)}: ${clean(text)}`;
595
+ }).join("");
596
+ }
597
+ function violationDetails(violations) {
598
+ return records(violations).map((violation) => {
599
+ const code = clean(violation["code"]);
600
+ const message = clean(violation["message"]) || code || "invalid";
601
+ return `
602
+ ${violationSubject(violation)}: ${message}${code === "" ? "" : ` [${code}]`}`;
603
+ }).join("");
604
+ }
605
+ function violationSubject(violation) {
606
+ const key = typeof violation["key"] === "string" ? clean(violation["key"]) : null;
607
+ const index = typeof violation["index"] === "number" ? `steps[${violation["index"]}]` : null;
608
+ const issue = typeof violation["issue"] === "string" ? clean(violation["issue"]) : null;
609
+ if (key !== null) {
610
+ const where = index ?? issue;
611
+ return where === null ? `step "${key}"` : `step "${key}" (${where})`;
612
+ }
613
+ return issue ?? index ?? "plan";
614
+ }
414
615
  function relationsSection(relations) {
415
616
  if (!Array.isArray(relations) || relations.length === 0) return "";
416
617
  const lines = relations.map((relation) => ` ${str(relation["direction"]).padEnd(14)}${str(relation["other_issue"])}
@@ -447,10 +648,30 @@ function labelsLine(labels) {
447
648
  }
448
649
  function blockedLine(issue) {
449
650
  if (issue["blocked"] === true && Array.isArray(issue["blocked_by"])) {
450
- return `yes, by ${issue["blocked_by"].map(String).join(", ")}`;
651
+ const notes = blockerNotes(issue["blockers"]);
652
+ const blockers = issue["blocked_by"].map((blocker) => {
653
+ const identifier = String(blocker);
654
+ const note = notes.get(identifier);
655
+ return note === void 0 ? identifier : `${identifier} (${note})`;
656
+ });
657
+ return `yes, by ${blockers.join(", ")}`;
451
658
  }
452
659
  return null;
453
660
  }
661
+ function blockerNotes(blockers) {
662
+ const notes = /* @__PURE__ */ new Map();
663
+ if (!Array.isArray(blockers)) return notes;
664
+ for (const blocker of blockers) {
665
+ if (typeof blocker !== "object" || blocker === null) continue;
666
+ const record = blocker;
667
+ if (typeof record["identifier"] !== "string") continue;
668
+ const reasons = [];
669
+ if (record["state_type"] === "canceled") reasons.push("cancelled");
670
+ if (record["archived"] === true) reasons.push("archived");
671
+ if (reasons.length > 0) notes.set(record["identifier"], reasons.join(", "));
672
+ }
673
+ return notes;
674
+ }
454
675
  function indent(text) {
455
676
  return cleanMultiline(text).split("\n").map((line) => " " + line).join("\n");
456
677
  }
@@ -608,8 +829,8 @@ function write(path2, bytes) {
608
829
  }
609
830
 
610
831
  // src/core/pagination.ts
611
- function notices(meta, requestedPageSize, records) {
612
- return capNotice(meta, requestedPageSize) + truncationNotice(meta, records);
832
+ function notices(meta, requestedPageSize, records2) {
833
+ return capNotice(meta, requestedPageSize) + truncationNotice(meta, records2);
613
834
  }
614
835
  function capNotice(meta, requestedPageSize) {
615
836
  const served = meta?.page_size;
@@ -619,12 +840,12 @@ function capNotice(meta, requestedPageSize) {
619
840
  }
620
841
  return "";
621
842
  }
622
- function truncationNotice(meta, records) {
843
+ function truncationNotice(meta, records2) {
623
844
  const total = meta?.total;
624
845
  const page = meta?.page;
625
846
  const pages = meta?.total_pages;
626
847
  if (typeof total !== "number" || typeof page !== "number" || typeof pages !== "number") return "";
627
- const shown = records.length;
848
+ const shown = records2.length;
628
849
  if (shown < total) {
629
850
  return `harness: showing ${shown} of ${total} (page ${page} of ${pages}) \u2014 pass --all for the rest, or --page to step through.
630
851
  `;
@@ -635,20 +856,20 @@ async function fetchEveryPage(fetchPage) {
635
856
  const first = await fetchPage(1);
636
857
  const totalPages = pageCount(first.meta);
637
858
  if (totalPages === null) return first.data;
638
- let records = first.data;
859
+ let records2 = first.data;
639
860
  for (let page = 2; page <= totalPages; page++) {
640
861
  const body = await fetchPage(page);
641
- records = dedupe([...records, ...body.data]);
862
+ records2 = dedupe([...records2, ...body.data]);
642
863
  }
643
- return records;
864
+ return records2;
644
865
  }
645
866
  function pageCount(meta) {
646
867
  const total = meta?.total_pages;
647
868
  return typeof total === "number" && total > 0 ? total : null;
648
869
  }
649
- function dedupe(records) {
870
+ function dedupe(records2) {
650
871
  const seen = /* @__PURE__ */ new Set();
651
- return records.filter((record) => {
872
+ return records2.filter((record) => {
652
873
  const id = record["id"];
653
874
  const key = id === void 0 || id === null ? `struct:${stableKey(record)}` : `id:${stableKey(id)}`;
654
875
  if (seen.has(key)) return false;
@@ -671,7 +892,6 @@ var STATUS_STATE_TYPES = {
671
892
  in_progress: "started",
672
893
  closed: "completed,canceled"
673
894
  };
674
- var READY_CLAIM_ATTEMPTS = 10;
675
895
  var MAX_IDEMPOTENCY_KEY_BYTES = 200;
676
896
  var IDEMPOTENCY_KEY_FORMAT = /^[\x21-\x7e](?:[\x20-\x7e]*[\x21-\x7e])?(?![\s\S])/;
677
897
  var IDENTIFIER_FORMAT = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
@@ -686,22 +906,72 @@ function requireIdentifiers(identifiers) {
686
906
  }
687
907
  }
688
908
  async function ready(ctx, opts) {
909
+ if (opts.claim === true) return claimNext(ctx, opts);
689
910
  const params = { ...listParams(ctx, opts), ready: true };
690
- const { records, notice } = await fetchIssues(ctx, params, opts);
911
+ const { records: records2, notice } = await fetchIssues(ctx, params, opts);
691
912
  const rank = (issue) => PRIORITY_RANK[issue["priority"]] ?? 4;
692
- const sorted = [...records].sort((a, b) => rank(a) - rank(b));
693
- if (opts.claim === true) return claimFirst(ctx, sorted);
694
- return { json: sorted, human: issuesTable(sorted), notice };
913
+ const sorted = markAgentSteps([...records2].sort((a, b) => rank(a) - rank(b)));
914
+ return { json: sorted, human: issuesTable(sorted, { markAgentSteps: true }), notice };
915
+ }
916
+ function markAgentSteps(issues) {
917
+ return issues.map((issue) => ({ ...issue, agent_step: isAgentStep(issue) }));
918
+ }
919
+ function requireReadyClaimArgs(opts) {
920
+ const paging = [
921
+ opts.all === true ? "--all" : null,
922
+ opts.limit !== void 0 ? "--limit" : null,
923
+ opts.page !== void 0 ? "--page" : null
924
+ ].filter((flag) => flag !== null);
925
+ if (paging.length === 0) return;
926
+ const last = paging.pop();
927
+ const flags = paging.length === 0 ? last : `${paging.join(", ")} or ${last}`;
928
+ throw new UsageError(
929
+ `--claim cannot be combined with ${flags}: the server picks from the team's whole ready queue, so there are no pages to choose from`
930
+ );
931
+ }
932
+ async function claimNext(ctx, opts) {
933
+ requireReadyClaimArgs(opts);
934
+ const team = requiredTeam(ctx, opts.team);
935
+ const check = `check \`harness list --team ${team} --assignee me --status in_progress\``;
936
+ const unreadable = (why) => new CommandError(`The claim-next response ${why}, so an issue may have been claimed \u2014 ${check}.`);
937
+ let body;
938
+ try {
939
+ body = await ctx.client.post(`/api/v1/teams/${team}/issues/claim-next`);
940
+ } catch (error) {
941
+ if (error instanceof MalformedResponseError) throw unreadable(`could not be read (${error.message})`);
942
+ if (error instanceof TransportError) {
943
+ throw new TransportError(
944
+ `${error.message} \u2014 if the request reached the server, an issue may have been claimed; ${check} before claiming again`
945
+ );
946
+ }
947
+ throw error;
948
+ }
949
+ if (body.data === null) {
950
+ return { json: null, human: `Nothing to claim: team ${team} has no unassigned ready issues.
951
+ ` };
952
+ }
953
+ const claimed = body.data;
954
+ if (!isIssueRecord(claimed)) throw unreadable("carried neither an issue nor null");
955
+ return {
956
+ json: claimed,
957
+ human: `Claimed ${str2(claimed["identifier"])}: ${clean(claimed["title"])}
958
+ `
959
+ };
960
+ }
961
+ function isIssueRecord(value) {
962
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
963
+ const identifier = value["identifier"];
964
+ return typeof identifier === "string" && identifier !== "";
695
965
  }
696
966
  async function blocked(ctx, opts) {
697
967
  const params = { ...listParams(ctx, opts), blocked: true };
698
- const { records, notice } = await fetchIssues(ctx, params, opts);
699
- return { json: records, human: issuesTable(records), notice };
968
+ const { records: records2, notice } = await fetchIssues(ctx, params, opts);
969
+ return { json: records2, human: issuesTable(records2), notice };
700
970
  }
701
971
  async function list(ctx, opts) {
702
972
  const params = listParams(ctx, opts);
703
- const { records, notice } = await fetchIssues(ctx, params, opts);
704
- return { json: records, human: issuesTable(records), notice };
973
+ const { records: records2, notice } = await fetchIssues(ctx, params, opts);
974
+ return { json: records2, human: issuesTable(records2), notice };
705
975
  }
706
976
  async function search(ctx, queryWords, opts) {
707
977
  if (queryWords.length === 0) throw new UsageError("usage: harness search <query>");
@@ -792,9 +1062,9 @@ async function stats2(ctx) {
792
1062
  }
793
1063
  async function prime(ctx) {
794
1064
  const statsBody = await ctx.client.get("/api/v1/stats");
795
- const readyBody = await ctx.client.get("/api/v1/issues", { ready: true, page_size: 10 });
796
- const json2 = { stats: statsBody.data, ready: readyBody.data };
797
- const human = "# Harness workflow context\n\n" + stats(statsBody.data) + "\nReady to work:\n" + issuesTable(readyBody.data) + `
1065
+ const ready2 = markAgentSteps(await mostImportantReady(ctx));
1066
+ const json2 = { stats: statsBody.data, ready: ready2 };
1067
+ const human = "# Harness workflow context\n\n" + stats(statsBody.data) + "\nReady to work:\n" + issuesTable(ready2, { markAgentSteps: true }) + `
798
1068
  Workflow: harness ready \u2192 harness update <id> --claim \u2192 work \u2192
799
1069
  harness close <id> --reason "Implemented in <commit-hash>" (response
800
1070
  lists newly unblocked issues \u2014 surface them).
@@ -804,6 +1074,14 @@ Before reporting done: close the issues you finished.
804
1074
  `;
805
1075
  return { json: json2, human };
806
1076
  }
1077
+ async function mostImportantReady(ctx) {
1078
+ try {
1079
+ return (await ctx.client.get("/api/v1/issues/next", { limit: 10 })).data;
1080
+ } catch (error) {
1081
+ if (!(error instanceof HttpError) || error.status !== 404) throw error;
1082
+ return (await ctx.client.get("/api/v1/issues", { ready: true, page_size: 10 })).data;
1083
+ }
1084
+ }
807
1085
  async function version(config) {
808
1086
  let server;
809
1087
  if (config === null || config.baseUrl === "") {
@@ -873,8 +1151,12 @@ async function update(ctx, identifier, opts) {
873
1151
  let issue = labelPlan?.issue ?? null;
874
1152
  let wrote = false;
875
1153
  if (opts.claim === true) {
876
- const body = await ctx.client.post(`/api/v1/issues/${identifier}/claim`);
877
- issue = body.data;
1154
+ try {
1155
+ const body = await ctx.client.post(`/api/v1/issues/${identifier}/claim`);
1156
+ issue = body.data;
1157
+ } catch (error) {
1158
+ throw explainClaimRefusal(error, identifier);
1159
+ }
878
1160
  wrote = true;
879
1161
  }
880
1162
  let patch = {};
@@ -919,6 +1201,19 @@ ${opts.appendNotes}`;
919
1201
  `;
920
1202
  return { json: issue, human };
921
1203
  }
1204
+ var CLAIM_REFUSAL_HINTS = {
1205
+ // The edit holds a released step, so the plan needs releasing again before
1206
+ // the step is claimable.
1207
+ agent_step: () => "Operator runs it for its plan; to do it yourself, set its executor to none (harness plan edit <plan-id>) and release the plan again",
1208
+ waiting_on_condition: (identifier) => `harness show ${identifier} lists what it waits on; tick a gate with harness gate satisfy ${identifier} <gate>`
1209
+ };
1210
+ function explainClaimRefusal(error, identifier) {
1211
+ if (!(error instanceof HttpError) || error.status !== 409) return error;
1212
+ const reason = error.body["reason"];
1213
+ const hint = typeof reason === "string" && Object.hasOwn(CLAIM_REFUSAL_HINTS, reason) ? CLAIM_REFUSAL_HINTS[reason](identifier) : void 0;
1214
+ if (hint === void 0) return error;
1215
+ return new HttpError(error.status, `${error.message} \u2014 ${hint}`, error.body, error.retryAfter);
1216
+ }
922
1217
  function requireCloseArgs(identifiers, opts) {
923
1218
  if (identifiers.length === 0) {
924
1219
  throw new UsageError(
@@ -960,7 +1255,9 @@ async function close(ctx, identifiers, opts) {
960
1255
  identifier: result.identifier,
961
1256
  closed: true,
962
1257
  ...opts.duplicateOf === void 0 ? {} : { duplicate_of: opts.duplicateOf },
963
- unblocked: result.response["unblocked"] ?? null
1258
+ unblocked: result.response["unblocked"] ?? null,
1259
+ // Passed through as sent: older servers omit it, and so does this.
1260
+ ..."still_blocking" in result.response ? { still_blocking: result.response["still_blocking"] } : {}
964
1261
  } : { identifier: result.identifier, closed: false, error: errorText(result.error) }
965
1262
  );
966
1263
  const partial = results.some((result) => !result.ok);
@@ -971,11 +1268,15 @@ function closeLine(result, duplicateOf) {
971
1268
  `;
972
1269
  const closure = duplicateOf === void 0 ? `Closed ${result.identifier}` : `Closed ${result.identifier} as duplicate of ${duplicateOf}`;
973
1270
  const unblocked = result.response["unblocked"];
974
- if (Array.isArray(unblocked) && unblocked.length > 0) {
975
- return `${closure} \u2014 unblocked: ${unblocked.map(String).join(", ")}
1271
+ const line = Array.isArray(unblocked) && unblocked.length > 0 ? `${closure} \u2014 unblocked: ${unblocked.map((identifier) => clean(identifier)).join(", ")}
1272
+ ` : `${closure}
976
1273
  `;
977
- }
978
- return `${closure}
1274
+ return line + stillBlockingLine(result.response["still_blocking"]);
1275
+ }
1276
+ function stillBlockingLine(stillBlocking) {
1277
+ if (!Array.isArray(stillBlocking) || stillBlocking.length === 0) return "";
1278
+ const identifiers = stillBlocking.map((identifier) => clean(identifier)).join(", ");
1279
+ return ` Still blocking: ${identifiers} (a cancelled issue still blocks its dependents)
979
1280
  `;
980
1281
  }
981
1282
  async function archive(ctx, identifiers, opts) {
@@ -1210,23 +1511,6 @@ function posixDetail(error) {
1210
1511
  if (code === "EACCES") return "permission denied";
1211
1512
  return error instanceof Error ? error.message : String(error);
1212
1513
  }
1213
- async function claimFirst(ctx, issues) {
1214
- for (const issue of issues.slice(0, READY_CLAIM_ATTEMPTS)) {
1215
- try {
1216
- const body = await ctx.client.post(`/api/v1/issues/${str2(issue["identifier"])}/claim`);
1217
- const claimed = body.data;
1218
- return {
1219
- json: claimed,
1220
- human: `Claimed ${str2(claimed["identifier"])}: ${clean(claimed["title"])}
1221
- `
1222
- };
1223
- } catch (error) {
1224
- if (error instanceof HttpError && error.status === 409) continue;
1225
- throw error;
1226
- }
1227
- }
1228
- throw new CommandError("No claimable ready issues.");
1229
- }
1230
1514
  async function setState(ctx, identifier, stateName2) {
1231
1515
  const showBody = await ctx.client.get(`/api/v1/issues/${identifier}`);
1232
1516
  const teamKey = str2((showBody.data["team"] ?? {})["key"]);
@@ -1390,15 +1674,15 @@ async function fetchIssues(ctx, params, opts) {
1390
1674
  throw new UsageError("--all reads every page from the first \u2014 pass --all or --page, not both");
1391
1675
  }
1392
1676
  if (opts.all === true) {
1393
- const records2 = await fetchEveryPage(
1677
+ const records3 = await fetchEveryPage(
1394
1678
  (page) => ctx.client.get("/api/v1/issues", { ...params, page })
1395
1679
  );
1396
- return { records: records2, notice: "" };
1680
+ return { records: records3, notice: "" };
1397
1681
  }
1398
1682
  const body = await ctx.client.get("/api/v1/issues", params);
1399
- const records = body.data;
1683
+ const records2 = body.data;
1400
1684
  const requested = typeof params["page_size"] === "number" ? params["page_size"] : void 0;
1401
- return { records, notice: notices(body.meta, requested, records) };
1685
+ return { records: records2, notice: notices(body.meta, requested, records2) };
1402
1686
  }
1403
1687
  function listParams(ctx, opts) {
1404
1688
  if (opts.type !== void 0 && opts.label !== void 0) {
@@ -1524,8 +1808,8 @@ async function paginatedRead(ctx, path2, params, opts) {
1524
1808
  throw new UsageError("--all reads every page from the first \u2014 pass --all or --page, not both");
1525
1809
  }
1526
1810
  if (opts.all === true) {
1527
- const records = await fetchEveryPage((page) => ctx.client.get(path2, { ...params, page }));
1528
- return { records, notice: "" };
1811
+ const records2 = await fetchEveryPage((page) => ctx.client.get(path2, { ...params, page }));
1812
+ return { records: records2, notice: "" };
1529
1813
  }
1530
1814
  const body = await ctx.client.get(path2, params);
1531
1815
  const requested = typeof params["page_size"] === "number" ? params["page_size"] : void 0;
@@ -1535,10 +1819,10 @@ async function projectList(ctx, opts) {
1535
1819
  let params = { page_size: opts.limit ?? 50, page: opts.page ?? 1 };
1536
1820
  if (opts.search !== void 0) params = { ...params, search: opts.search };
1537
1821
  if (opts.state !== void 0) params = { ...params, state: opts.state };
1538
- const { records, notice } = await paginatedRead(ctx, "/api/v1/projects", params, opts);
1539
- const human = records.length === 0 ? "No projects.\n" : table(
1822
+ const { records: records2, notice } = await paginatedRead(ctx, "/api/v1/projects", params, opts);
1823
+ const human = records2.length === 0 ? "No projects.\n" : table(
1540
1824
  ["ID", "STATE", "LEAD", "TARGET", "NAME"],
1541
- records.map((project) => [
1825
+ records2.map((project) => [
1542
1826
  str3(project["id"]),
1543
1827
  str3(project["state"]) || "-",
1544
1828
  str3(field(project, "lead", "name")) || "-",
@@ -1546,7 +1830,7 @@ async function projectList(ctx, opts) {
1546
1830
  str3(project["name"])
1547
1831
  ])
1548
1832
  );
1549
- return { json: records, human, notice };
1833
+ return { json: records2, human, notice };
1550
1834
  }
1551
1835
  async function projectShow(ctx, id) {
1552
1836
  requireSafeId("project", id);
@@ -1637,12 +1921,12 @@ async function milestoneCreate(ctx, opts) {
1637
1921
  async function docList(ctx, opts) {
1638
1922
  let params = { page_size: opts.limit ?? 50, page: opts.page ?? 1 };
1639
1923
  if (opts.search !== void 0) params = { ...params, search: opts.search };
1640
- const { records, notice } = await paginatedRead(ctx, "/api/v1/documents", params, opts);
1641
- const human = records.length === 0 ? "No documents.\n" : table(
1924
+ const { records: records2, notice } = await paginatedRead(ctx, "/api/v1/documents", params, opts);
1925
+ const human = records2.length === 0 ? "No documents.\n" : table(
1642
1926
  ["ID", "UPDATED", "TITLE"],
1643
- records.map((doc) => [str3(doc["id"]), str3(doc["updated_at"]) || "-", str3(doc["title"])])
1927
+ records2.map((doc) => [str3(doc["id"]), str3(doc["updated_at"]) || "-", str3(doc["title"])])
1644
1928
  );
1645
- return { json: records, human, notice };
1929
+ return { json: records2, human, notice };
1646
1930
  }
1647
1931
  async function docShow(ctx, id) {
1648
1932
  requireSafeId("document", id);
@@ -1931,8 +2215,315 @@ function indent2(text) {
1931
2215
  return cleanMultiline(text).split("\n").map((line) => " " + line).join("\n");
1932
2216
  }
1933
2217
 
2218
+ // src/core/textinput.ts
2219
+ import { readFileSync as readFileSync3, statSync as statSync3 } from "fs";
2220
+ import { isatty } from "tty";
2221
+ var STDIN_PATH = "-";
2222
+ var MAX_TEXT_BYTES = 1e6;
2223
+ var TEXT_LIMIT = { bytes: MAX_TEXT_BYTES, why: "that is not prose; check the path" };
2224
+ var DOCUMENT_LIMIT = { bytes: 8e6, why: "larger than the API accepts in one request" };
2225
+ function flagOf(name) {
2226
+ return name.replace(/[A-Z]/g, (upper) => `-${upper.toLowerCase()}`);
2227
+ }
2228
+ function resolveTextSources(sources) {
2229
+ const resolved = {};
2230
+ const toRead = [];
2231
+ let stdinClaimedBy;
2232
+ for (const { name, inline, file } of sources) {
2233
+ if (inline !== void 0 && file !== void 0) {
2234
+ throw new UsageError(
2235
+ `--${flagOf(name)} and --${flagOf(name)}-file both given \u2014 pass one; there is no precedence between them`
2236
+ );
2237
+ }
2238
+ if (file === void 0) {
2239
+ if (inline !== void 0) resolved[name] = inline;
2240
+ continue;
2241
+ }
2242
+ if (file === STDIN_PATH) {
2243
+ if (isatty(0)) {
2244
+ throw new UsageError(
2245
+ `--${flagOf(name)}-file - reads stdin, but stdin is a terminal \u2014 pipe something in or pass a path`
2246
+ );
2247
+ }
2248
+ if (stdinClaimedBy !== void 0) {
2249
+ throw new UsageError(
2250
+ `--${flagOf(name)}-file and --${flagOf(stdinClaimedBy)}-file both read stdin \u2014 there is only one stream, so only one may be "-"`
2251
+ );
2252
+ }
2253
+ stdinClaimedBy = name;
2254
+ }
2255
+ if (file !== STDIN_PATH) checkReadable(file, `--${flagOf(name)}-file`);
2256
+ toRead.push({ name, file });
2257
+ }
2258
+ for (const { name, file } of [...toRead].sort(byStdinLast)) {
2259
+ resolved[name] = stripOneTrailingNewline(readTextFile(file, `--${flagOf(name)}-file`));
2260
+ }
2261
+ return resolved;
2262
+ }
2263
+ function readFileOption(flag, path2) {
2264
+ if (path2 === STDIN_PATH) {
2265
+ if (isatty(0)) {
2266
+ throw new UsageError(`${flag} - reads stdin, but stdin is a terminal \u2014 pipe something in or pass a path`);
2267
+ }
2268
+ } else {
2269
+ checkReadable(path2, flag, DOCUMENT_LIMIT);
2270
+ }
2271
+ return readTextFile(path2, flag, DOCUMENT_LIMIT);
2272
+ }
2273
+ function readTextFile(path2, option, limit = TEXT_LIMIT) {
2274
+ if (path2 === STDIN_PATH) {
2275
+ let piped;
2276
+ try {
2277
+ piped = readFileSync3(0);
2278
+ } catch (error) {
2279
+ throw new UsageError(`${option} -: cannot read stdin (${describe(error)})`);
2280
+ }
2281
+ if (piped.length > limit.bytes) {
2282
+ throw new UsageError(oversize(`${option} -`, limit));
2283
+ }
2284
+ return decodeUtf8(piped, `${option} -`);
2285
+ }
2286
+ const flag = `${option} ${path2}`;
2287
+ let bytes;
2288
+ try {
2289
+ bytes = readFileSync3(path2);
2290
+ } catch (error) {
2291
+ throw new UsageError(`${flag}: ${describe(error)}`);
2292
+ }
2293
+ if (bytes.length > limit.bytes) throw new UsageError(oversize(flag, limit));
2294
+ return decodeUtf8(bytes, flag);
2295
+ }
2296
+ function decodeUtf8(bytes, flag) {
2297
+ try {
2298
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
2299
+ } catch {
2300
+ throw new UsageError(`${flag}: not valid UTF-8 \u2014 convert it first`);
2301
+ }
2302
+ }
2303
+ function oversize(what, limit) {
2304
+ return `${what}: over ${limit.bytes / 1e6} MB \u2014 ${limit.why}`;
2305
+ }
2306
+ function checkReadable(path2, option, limit = TEXT_LIMIT) {
2307
+ const flag = `${option} ${path2}`;
2308
+ let stats3;
2309
+ try {
2310
+ stats3 = statSync3(path2);
2311
+ } catch (error) {
2312
+ throw new UsageError(`${flag}: ${describe(error)}`);
2313
+ }
2314
+ if (!stats3.isFile()) {
2315
+ throw new UsageError(
2316
+ `${flag}: ${stats3.isDirectory() ? "is a directory" : "not a regular file"}`
2317
+ );
2318
+ }
2319
+ if (stats3.size > limit.bytes) throw new UsageError(oversize(flag, limit));
2320
+ }
2321
+ function byStdinLast(a, b) {
2322
+ return Number(a.file === STDIN_PATH) - Number(b.file === STDIN_PATH);
2323
+ }
2324
+ function stripOneTrailingNewline(text) {
2325
+ if (text.endsWith("\r\n")) return text.slice(0, -2);
2326
+ if (text.endsWith("\n")) return text.slice(0, -1);
2327
+ return text;
2328
+ }
2329
+ function describe(error) {
2330
+ const code = error?.code;
2331
+ if (code === "ENOENT") return "no such file";
2332
+ if (code === "EISDIR") return "is a directory";
2333
+ if (code === "EACCES") return "permission denied";
2334
+ return error instanceof Error ? error.message : String(error);
2335
+ }
2336
+
2337
+ // src/commands/plans.ts
2338
+ import { extname as extname2 } from "path";
2339
+ import { parseDocument } from "yaml";
2340
+ var PLAN_EXECUTORS = ["none", "operator"];
2341
+ var CONDITION_KINDS = ["not_before", "gate", "event"];
2342
+ function parsePlanDocument(text, source) {
2343
+ const parsed = extname2(source).toLowerCase() === ".json" ? parseJson(text, source) : parseYamlDocument(text, source);
2344
+ if (!isRecord(parsed)) {
2345
+ throw new UsageError(`${source}: a plan file holds one object (a mapping), not a list or a scalar`);
2346
+ }
2347
+ return parsed;
2348
+ }
2349
+ function parseJson(text, source) {
2350
+ try {
2351
+ return JSON.parse(text);
2352
+ } catch (error) {
2353
+ throw new UsageError(`${source}: not valid JSON (${firstLine(error)})`);
2354
+ }
2355
+ }
2356
+ function parseYamlDocument(text, source) {
2357
+ const document = parseDocument(text, { resolveKnownTags: false });
2358
+ const problem = document.errors[0] ?? document.warnings[0];
2359
+ if (problem !== void 0) throw new UsageError(`${source}: not valid YAML (${firstLine(problem)})`);
2360
+ let value;
2361
+ try {
2362
+ value = document.toJS();
2363
+ } catch (error) {
2364
+ throw new UsageError(`${source}: not valid YAML (${firstLine(error)})`);
2365
+ }
2366
+ if (!jsonSafe(value)) {
2367
+ throw new UsageError(`${source}: holds a value JSON cannot carry (.inf or .nan) \u2014 write a number or a string`);
2368
+ }
2369
+ return value;
2370
+ }
2371
+ function jsonSafe(value) {
2372
+ if (typeof value === "number") return Number.isFinite(value);
2373
+ if (Array.isArray(value)) return value.every(jsonSafe);
2374
+ if (typeof value === "object" && value !== null) return Object.values(value).every(jsonSafe);
2375
+ return true;
2376
+ }
2377
+ function firstLine(error) {
2378
+ return (error instanceof Error ? error.message : String(error)).split("\n")[0] ?? "";
2379
+ }
2380
+ function readPlanFile(path2, flag = "--file") {
2381
+ return parsePlanDocument(readFileOption(flag, path2), path2);
2382
+ }
2383
+ async function planCreate(ctx, plan, opts) {
2384
+ requireDocument(plan, "a plan is one object: the root's fields and its steps");
2385
+ const team = requiredTeam(ctx, opts.team);
2386
+ validateIdempotencyKey(opts.idempotencyKey);
2387
+ let body;
2388
+ try {
2389
+ body = await ctx.client.post(`/api/v1/teams/${team}/plans`, plan, { idempotencyKey: opts.idempotencyKey });
2390
+ } catch (error) {
2391
+ throw explainMissing(error, `no team ${team} here, or this Harness server has no plan API yet`);
2392
+ }
2393
+ const created = body.data;
2394
+ const id = str4(created["id"]);
2395
+ return {
2396
+ json: created,
2397
+ human: `Created plan ${clean(id)} (${clean(created["status"]) || "held"})
2398
+
2399
+ ` + planDetail(created) + `
2400
+ Nothing runs until it is released: harness plan release ${clean(id)}
2401
+ `
2402
+ };
2403
+ }
2404
+ async function planShow(ctx, id) {
2405
+ requirePlanId(id);
2406
+ const plan = await planRequest(id, () => ctx.client.get(`/api/v1/plans/${id}`));
2407
+ return { json: plan, human: planDetail(plan) };
2408
+ }
2409
+ async function planEdit(ctx, id, edits) {
2410
+ requirePlanId(id);
2411
+ requireDocument(edits, "a plan edit is one object: steps to change and budgets");
2412
+ const plan = await planRequest(id, () => ctx.client.patch(`/api/v1/plans/${id}`, edits));
2413
+ const held = Array.isArray(plan["held_steps"]) && plan["held_steps"].length > 0;
2414
+ return {
2415
+ json: plan,
2416
+ human: `Updated plan ${clean(id)}
2417
+
2418
+ ` + planDetail(plan) + (held ? `
2419
+ Release it again to run the held steps: harness plan release ${clean(id)}
2420
+ ` : "")
2421
+ };
2422
+ }
2423
+ async function planRelease(ctx, id, opts) {
2424
+ requirePlanId(id);
2425
+ const body = {};
2426
+ if (opts.collaborators !== void 0 && opts.collaborators.length > 0) body["collaborators"] = opts.collaborators;
2427
+ if (opts.retryBudget !== void 0) body["retry_budget"] = opts.retryBudget;
2428
+ if (opts.spendBudgetCents !== void 0) body["spend_budget_cents"] = opts.spendBudgetCents;
2429
+ const plan = await planRequest(id, () => ctx.client.post(`/api/v1/plans/${id}/release`, body));
2430
+ const revision = plan["revision"] ?? {};
2431
+ const ready2 = Array.isArray(plan["steps"]) ? plan["steps"].filter((step) => isRecord(step) && step["ready"] === true).length : 0;
2432
+ return {
2433
+ json: plan,
2434
+ human: `Released plan ${clean(id)} as revision ${clean(revision["number"]) || "?"}: ${ready2} ${ready2 === 1 ? "step" : "steps"} ready now
2435
+ `
2436
+ };
2437
+ }
2438
+ async function planHold(ctx, id) {
2439
+ requirePlanId(id);
2440
+ const plan = await planRequest(id, () => ctx.client.post(`/api/v1/plans/${id}/hold`));
2441
+ return {
2442
+ json: plan,
2443
+ human: `Held plan ${clean(id)}: none of its steps is ready until it is released again
2444
+ `
2445
+ };
2446
+ }
2447
+ async function gateSet(ctx, issue, gate, satisfied) {
2448
+ requireIdentifiers([issue]);
2449
+ const wanted = gate.trim();
2450
+ if (wanted === "") throw new UsageError("name the gate by its label or its condition id");
2451
+ let conditions;
2452
+ try {
2453
+ conditions = (await ctx.client.get(`/api/v1/issues/${issue}/conditions`)).data;
2454
+ } catch (error) {
2455
+ throw explainMissing(error, `no issue ${issue} here, or this Harness server has no wait conditions yet`);
2456
+ }
2457
+ if (!Array.isArray(conditions)) throw new CommandError("The conditions response carried no list.");
2458
+ const list2 = conditions.filter(isRecord);
2459
+ const target = findGate(list2, issue, wanted);
2460
+ const id = str4(target["id"]);
2461
+ const body = await ctx.client.put(`/api/v1/issues/${issue}/conditions/${id}/satisfaction`, {
2462
+ satisfied
2463
+ });
2464
+ const others = list2.filter((condition) => condition["id"] !== id && condition["satisfied"] !== true).length;
2465
+ let tail = "";
2466
+ if (satisfied) {
2467
+ tail = others > 0 ? ` \u2014 it still waits on ${others} other condition${others === 1 ? "" : "s"}` : " \u2014 no other wait condition holds it";
2468
+ }
2469
+ return {
2470
+ json: body.data,
2471
+ human: `${satisfied ? "Satisfied" : "Un-satisfied"} gate "${clean(target["label"])}" on ${issue}${tail}
2472
+ `
2473
+ };
2474
+ }
2475
+ function requirePlanId(id) {
2476
+ if (!isUuid(id)) throw new UsageError(`not a plan id: ${id} (expected the plan's UUID)`);
2477
+ }
2478
+ function requireDocument(value, what) {
2479
+ if (!isRecord(value)) throw new UsageError(what);
2480
+ }
2481
+ async function planRequest(id, send) {
2482
+ try {
2483
+ return (await send()).data;
2484
+ } catch (error) {
2485
+ throw explainMissing(error, `no plan ${id} in this organization, or this Harness server has no plan API yet`);
2486
+ }
2487
+ }
2488
+ function explainMissing(error, why) {
2489
+ if (!(error instanceof HttpError) || error.status !== 404) return error;
2490
+ return new HttpError(404, `${error.message} \u2014 ${why}`, error.body, error.retryAfter);
2491
+ }
2492
+ function findGate(conditions, issue, wanted) {
2493
+ const gates = conditions.filter((condition) => condition["kind"] === "gate");
2494
+ const label = (condition) => str4(condition["label"]).trim();
2495
+ if (isUuid(wanted) && !gates.some((condition) => label(condition) === wanted)) {
2496
+ const byId = conditions.find((condition) => str4(condition["id"]).toLowerCase() === wanted.toLowerCase());
2497
+ if (byId === void 0) throw new CommandError(`No condition ${wanted} on ${issue}.`);
2498
+ if (byId["kind"] !== "gate") {
2499
+ throw new CommandError(
2500
+ `condition ${wanted} on ${issue} is a ${clean(byId["kind"])} condition \u2014 only a gate is satisfied by hand`
2501
+ );
2502
+ }
2503
+ return byId;
2504
+ }
2505
+ let matches = gates.filter((condition) => label(condition) === wanted);
2506
+ if (matches.length === 0) {
2507
+ matches = gates.filter((condition) => label(condition).toLowerCase() === wanted.toLowerCase());
2508
+ }
2509
+ if (matches.length === 1) return matches[0];
2510
+ if (matches.length > 1) {
2511
+ const ids = matches.map((condition) => clean(condition["id"])).join(", ");
2512
+ throw new CommandError(`several gates on ${issue} match "${wanted}" \u2014 pass the condition id: ${ids}`);
2513
+ }
2514
+ if (gates.length === 0) throw new CommandError(`No gate on ${issue} matches "${wanted}" \u2014 it has no gates`);
2515
+ const labels = gates.map((condition) => `"${clean(label(condition))}"`).join(", ");
2516
+ throw new CommandError(`No gate on ${issue} matches "${wanted}". Its gates: ${labels}`);
2517
+ }
2518
+ function isRecord(value) {
2519
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2520
+ }
2521
+ function str4(value) {
2522
+ return value === null || value === void 0 ? "" : String(value);
2523
+ }
2524
+
1934
2525
  // src/core/credentials.ts
1935
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
2526
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
1936
2527
  import { homedir } from "os";
1937
2528
  import { dirname as dirname2, isAbsolute, join as join2 } from "path";
1938
2529
  function defaultCredentialsPath(env = process.env) {
@@ -1942,7 +2533,7 @@ function defaultCredentialsPath(env = process.env) {
1942
2533
  }
1943
2534
  function readCredentials(path2) {
1944
2535
  try {
1945
- const parsed = JSON.parse(readFileSync3(path2, "utf8"));
2536
+ const parsed = JSON.parse(readFileSync4(path2, "utf8"));
1946
2537
  if (typeof parsed === "object" && parsed !== null) {
1947
2538
  const { url, token } = parsed;
1948
2539
  if (typeof url === "string" && typeof token === "string") return { url, token };
@@ -1969,7 +2560,7 @@ function deleteCredentials(path2) {
1969
2560
  }
1970
2561
 
1971
2562
  // src/core/repo-config.ts
1972
- import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
2563
+ import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1973
2564
  import { homedir as homedir2 } from "os";
1974
2565
  import { dirname as dirname3, isAbsolute as isAbsolute2, join as join3, parse } from "path";
1975
2566
  var CONFIG_RELATIVE_PATH = join3(".harness-cli", "config.json");
@@ -1988,7 +2579,7 @@ function findRepoConfig(startDir = process.cwd()) {
1988
2579
  function readRepoConfig(path2) {
1989
2580
  let parsed;
1990
2581
  try {
1991
- parsed = JSON.parse(readFileSync4(path2, "utf8"));
2582
+ parsed = JSON.parse(readFileSync5(path2, "utf8"));
1992
2583
  } catch (error) {
1993
2584
  const detail = error instanceof Error ? error.message : String(error);
1994
2585
  return { config: null, warnings: [`harness: ignoring malformed ${path2} (${detail})`] };
@@ -2017,7 +2608,7 @@ function trustedHostsPath(env = process.env) {
2017
2608
  }
2018
2609
  function readTrustedHosts(env) {
2019
2610
  try {
2020
- const parsed = JSON.parse(readFileSync4(trustedHostsPath(env), "utf8"));
2611
+ const parsed = JSON.parse(readFileSync5(trustedHostsPath(env), "utf8"));
2021
2612
  if (typeof parsed === "object" && parsed !== null && Array.isArray(parsed["hosts"])) {
2022
2613
  return parsed["hosts"].filter(
2023
2614
  (host) => typeof host === "string"
@@ -2180,10 +2771,12 @@ export {
2180
2771
  Client,
2181
2772
  clean,
2182
2773
  json,
2774
+ errorDetails,
2183
2775
  MAX_IDEMPOTENCY_KEY_BYTES,
2184
2776
  IDEMPOTENCY_KEY_FORMAT,
2185
2777
  requireIdentifiers,
2186
2778
  ready,
2779
+ requireReadyClaimArgs,
2187
2780
  blocked,
2188
2781
  list,
2189
2782
  search,
@@ -2194,6 +2787,7 @@ export {
2194
2787
  prime,
2195
2788
  version,
2196
2789
  create,
2790
+ validateIdempotencyKey,
2197
2791
  update,
2198
2792
  requireCloseArgs,
2199
2793
  close,
@@ -2211,6 +2805,7 @@ export {
2211
2805
  deleteComment,
2212
2806
  requireCommentId,
2213
2807
  attachImage,
2808
+ requiredTeam,
2214
2809
  projectList,
2215
2810
  projectShow,
2216
2811
  projectCreate,
@@ -2243,6 +2838,17 @@ export {
2243
2838
  memberAdd,
2244
2839
  memberSetRole,
2245
2840
  upload,
2841
+ resolveTextSources,
2842
+ PLAN_EXECUTORS,
2843
+ CONDITION_KINDS,
2844
+ readPlanFile,
2845
+ planCreate,
2846
+ planShow,
2847
+ planEdit,
2848
+ planRelease,
2849
+ planHold,
2850
+ gateSet,
2851
+ requirePlanId,
2246
2852
  defaultCredentialsPath,
2247
2853
  readCredentials,
2248
2854
  writeCredentials,