@allocator-one/harness-cli 1.14.0 → 1.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,25 +1,36 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ACTIVITY_MAX_LIMIT,
4
- CLI_VERSION,
5
4
  Client,
6
5
  CommandError,
7
6
  DEFAULT_BASE_URL,
7
+ EXIT_INTERRUPTED,
8
+ EXIT_JOB_ENDED,
9
+ EXIT_WAITING_FOR_PERSON,
10
+ EXPERT_ASSIGNMENTS_PATH,
11
+ EXPERT_LAPSED,
8
12
  HttpError,
13
+ JOBS_MAX_LIMIT,
14
+ JOBS_MAX_PAGES,
15
+ JOB_STATES,
9
16
  LocalError,
10
17
  RELATION_CREATE_TYPES,
11
18
  RELATION_UPDATE_TYPES,
19
+ SESSION_PATH,
20
+ STEER_MAX_CHARACTERS,
12
21
  TransportError,
13
22
  UpgradeRequiredError,
14
23
  UsageError,
15
24
  activity,
16
25
  archive,
26
+ assignments,
17
27
  attachImage,
18
28
  blocked,
19
29
  clean,
20
30
  close,
21
31
  comment,
22
32
  comments,
33
+ conditionList,
23
34
  create,
24
35
  defaultCredentialsPath,
25
36
  deleteComment,
@@ -31,9 +42,21 @@ import {
31
42
  docUpdate,
32
43
  editComment,
33
44
  errorDetails,
45
+ expertSummary,
46
+ explainExpertRefusal,
34
47
  findRepoConfig,
35
48
  gateSet,
49
+ instructionFrom,
50
+ isExpertToken,
36
51
  isHostTrusted,
52
+ isLoginCredential,
53
+ jobCancel,
54
+ jobContinue,
55
+ jobPause,
56
+ jobShow,
57
+ jobSteer,
58
+ jobWatch,
59
+ jobsList,
37
60
  json,
38
61
  labelCreate,
39
62
  labelDelete,
@@ -55,7 +78,11 @@ import {
55
78
  planHold,
56
79
  planRelease,
57
80
  planShow,
81
+ prepareProcedureRun,
82
+ prepareRun,
58
83
  prime,
84
+ procedureSave,
85
+ procedureShow,
59
86
  projectCreate,
60
87
  projectList,
61
88
  projectShow,
@@ -67,17 +94,28 @@ import {
67
94
  relationList,
68
95
  relationRm,
69
96
  relationUpdate,
97
+ requireCapabilityId,
70
98
  requireCloseArgs,
71
99
  requireCommentId,
72
100
  requireIdentifiers,
101
+ requireJobId,
102
+ requireJobsArgs,
73
103
  requirePlanId,
104
+ requireProcedureId,
105
+ requireProcedureName,
74
106
  requireReadyClaimArgs,
107
+ requireSchemaView,
108
+ requireSearchQuery,
109
+ requireToolsOpts,
75
110
  requiredTeam,
76
111
  resolveCacheHome,
77
112
  resolveConfig,
78
113
  resolveTextSources,
114
+ run,
79
115
  search,
80
116
  show,
117
+ skillsList,
118
+ startProcedureRun,
81
119
  stateAdd,
82
120
  stateDelete,
83
121
  stateOrder,
@@ -87,6 +125,10 @@ import {
87
125
  teamList,
88
126
  teamRename,
89
127
  teamSetDefault,
128
+ toolShow,
129
+ toolsDomains,
130
+ toolsList,
131
+ toolsSearch,
90
132
  trustHost,
91
133
  update,
92
134
  upload,
@@ -94,9 +136,17 @@ import {
94
136
  validateBaseUrl,
95
137
  validateIdempotencyKey,
96
138
  version,
139
+ watchStopSignal,
97
140
  workflowStates,
98
141
  writeCredentials
99
- } from "./chunk-F2YVTB2A.js";
142
+ } from "./chunk-L2OWOZ4N.js";
143
+ import {
144
+ CLI_VERSION,
145
+ OPERATOR_PACKAGE,
146
+ invokedName,
147
+ layoutOperatorHelp,
148
+ named
149
+ } from "./chunk-FQWV5MIC.js";
100
150
 
101
151
  // src/cli.ts
102
152
  import { createInterface as createInterface2 } from "readline";
@@ -226,8 +276,10 @@ async function exec(ctx, opts) {
226
276
  const missing = unstoredSlugs(requested, stored);
227
277
  if (missing.length > 0) {
228
278
  process.stderr.write(
229
- `harness: no key stored for ${missing.join(", ")} \u2014 the command gets your shell's own variable for those, if any
279
+ named(
280
+ `harness: no key stored for ${missing.join(", ")} \u2014 the command gets your shell's own variable for those, if any
230
281
  `
282
+ )
231
283
  );
232
284
  }
233
285
  }
@@ -297,9 +349,10 @@ var RESPONSE_FLUSH_TIMEOUT_MS = 2e3;
297
349
  var MAX_PAGE_RESPONSES = 4;
298
350
  var loginPageImport;
299
351
  function loadLoginPage() {
300
- loginPageImport ??= import("./login-page-UVUKFFTT.js");
352
+ loginPageImport ??= import("./login-page-KGEJFFXR.js");
301
353
  return loginPageImport;
302
354
  }
355
+ var NO_SESSION = { kind: null, expires_at: null, scopes: null, user: null, org: null };
303
356
  function generateState() {
304
357
  return randomBytes(24).toString("base64url");
305
358
  }
@@ -398,7 +451,7 @@ async function loopbackLogin(baseUrl, options) {
398
451
  );
399
452
  return token;
400
453
  }
401
- const result = await verifyAndStore(baseUrl, token.token, options);
454
+ const result = await verifyAndStore(baseUrl, token.token, options, { minted: true, kind: token.kind });
402
455
  if (result.ok) {
403
456
  await callback.respond(200, "You can close this browser and return to the terminal.", "success");
404
457
  } else {
@@ -451,7 +504,7 @@ function awaitCallback(server, state2, timeoutMs) {
451
504
  if (phase !== "waiting") return;
452
505
  const timeoutResponse = {
453
506
  status: 410,
454
- message: "This login attempt has ended \u2014 run harness login again if needed.",
507
+ message: `This login attempt has ended \u2014 run ${invokedName()} login again if needed.`,
455
508
  pageState: "error"
456
509
  };
457
510
  phase = "terminal";
@@ -523,7 +576,7 @@ function awaitCallback(server, state2, timeoutMs) {
523
576
  }
524
577
  const result = finalResponse ?? {
525
578
  status: 410,
526
- message: "This login attempt has ended \u2014 run harness login again if needed.",
579
+ message: `This login attempt has ended \u2014 run ${invokedName()} login again if needed.`,
527
580
  pageState: "error"
528
581
  };
529
582
  void trackResponse(respondWithPage(result.status, result.message, result.pageState));
@@ -655,7 +708,8 @@ async function exchangeCode(baseUrl, code, verifier, options) {
655
708
  try {
656
709
  const body = await bareClient(baseUrl, "", options).post("/api/v1/cli/token-exchange", { code, code_verifier: verifier });
657
710
  const token = body.data?.token;
658
- if (typeof token === "string" && token !== "") return { ok: true, token };
711
+ const kind = typeof body.data?.kind === "string" ? body.data.kind : null;
712
+ if (typeof token === "string" && token !== "") return { ok: true, token, kind };
659
713
  return {
660
714
  ok: false,
661
715
  message: "the login-code exchange returned no token \u2014 aborting"
@@ -679,21 +733,33 @@ async function exchangeCode(baseUrl, code, verifier, options) {
679
733
  async function manualLogin(baseUrl, options) {
680
734
  const notify = options.notify ?? defaultNotify;
681
735
  const prompt = options.prompt ?? defaultPrompt;
682
- notify(`Open ${baseUrl}/cli/login in a browser, authorize, and paste the token below.`);
736
+ notify(
737
+ `Open ${baseUrl}/cli/login in a browser, authorize, and paste the token below (an outside expert pastes the pxc_ credential minted in their expert view instead).`
738
+ );
683
739
  const token = (await prompt()).trim();
684
740
  if (token === "") return { ok: false, message: "no token entered" };
685
- return verifyAndStore(baseUrl, token, options);
741
+ return verifyAndStore(baseUrl, token, options, { minted: false, kind: null });
686
742
  }
687
- async function verifyAndStore(baseUrl, token, options) {
743
+ async function verifyAndStore(baseUrl, token, options, received) {
688
744
  const path = options.credentialsPath ?? defaultCredentialsPath(options.env ?? process.env);
689
745
  const client = bareClient(baseUrl, token, options);
746
+ const expert = isExpertToken(token);
747
+ let kind = received.kind;
690
748
  try {
691
- await client.get("/api/v1/teams");
749
+ if (expert) {
750
+ await client.get(EXPERT_ASSIGNMENTS_PATH);
751
+ } else {
752
+ const described = isLoginCredential(token) ? await describeSession(client) : null;
753
+ if (described === null) await client.get("/api/v1/teams");
754
+ else kind = described.session.kind ?? kind;
755
+ }
692
756
  } catch (error) {
693
757
  if (error instanceof HttpError) {
758
+ const lapsed = expert && error.status === 401 ? ` \u2014 ${EXPERT_LAPSED}` : "";
759
+ const revoked = received.minted && await revokeSession(client, token);
694
760
  return {
695
761
  ok: false,
696
- message: `the received token failed verification (${error.message}, HTTP ${error.status}) \u2014 nothing was stored`
762
+ message: `the received token failed verification (${error.message}, HTTP ${error.status}) \u2014 nothing was stored${revoked ? ", and the token was revoked" : ""}${lapsed}`
697
763
  };
698
764
  }
699
765
  if (error instanceof TransportError) {
@@ -709,33 +775,72 @@ async function verifyAndStore(baseUrl, token, options) {
709
775
  try {
710
776
  writeCredentials(path, baseUrl, token);
711
777
  } catch (error) {
712
- await client.delete("/api/v1/session").catch(() => void 0);
778
+ if (received.minted) await revokeSession(client, token);
713
779
  const detail = error instanceof Error ? error.message : String(error);
714
780
  return {
715
781
  ok: false,
716
- message: `could not store credentials at ${path}: ${detail}`
782
+ message: `could not store credentials at ${path}: ${detail}` + (expert ? " (the pxc_ credential is still valid: revoke it in your expert view)" : received.minted ? "" : " (the pasted token was not revoked and stays valid)")
717
783
  };
718
784
  }
785
+ let supersededNote = "";
719
786
  if (superseded !== null) {
720
- await bareClient(superseded.url, superseded.token, options).delete("/api/v1/session").catch(() => void 0);
787
+ await revokeSession(bareClient(superseded.url, superseded.token, options), superseded.token);
788
+ if (isExpertToken(superseded.token)) {
789
+ supersededNote = "The pxc_ credential stored before stays valid until it expires: revoke it in your expert view.\n";
790
+ }
721
791
  }
792
+ const name = invokedName();
722
793
  return {
723
794
  ok: true,
724
- json: { url: baseUrl, credentials: path },
725
- human: `Logged in to ${baseUrl} \u2014 login token stored at ${path} (valid ~30 days; harness logout revokes it).
726
- `
795
+ json: { url: baseUrl, credentials: path, kind },
796
+ human: (expert ? `Stored an outside expert's pxc_ credential for ${baseUrl} at ${path} (it lasts at most 30 days; revoke it in your expert view \u2014 ${name} logout only forgets it). Commands now act as the expert: only assignments, conditions and gate work until you log in again.
797
+ ` : `Logged in to ${baseUrl} \u2014 login token stored at ${path} (valid ~30 days; ${name} logout revokes it).
798
+ `) + kindNote(kind) + supersededNote
799
+ };
800
+ }
801
+ function kindNote(kind) {
802
+ if (kind === "operator-cli") {
803
+ return "This credential is Operator-only (operator-cli): Operator commands work; issue, plan and gate commands are refused.\n";
804
+ }
805
+ if (kind === "harness-cli") return "This credential has full access (harness-cli).\n";
806
+ return "";
807
+ }
808
+ async function describeSession(client) {
809
+ try {
810
+ const { envelope, apiVersion } = await client.getVersioned(SESSION_PATH);
811
+ return { session: sessionOf(envelope.data), apiVersion };
812
+ } catch (error) {
813
+ if (error instanceof HttpError && (error.status === 404 || error.status === 405)) return null;
814
+ throw error;
815
+ }
816
+ }
817
+ function sessionOf(data) {
818
+ if (!isRecord2(data)) return NO_SESSION;
819
+ const text = (value) => typeof value === "string" ? value : null;
820
+ return {
821
+ kind: text(data["kind"]),
822
+ expires_at: text(data["expires_at"]),
823
+ scopes: Array.isArray(data["scopes"]) ? data["scopes"].filter((scope) => typeof scope === "string") : null,
824
+ user: isRecord2(data["user"]) ? data["user"] : null,
825
+ org: isRecord2(data["org"]) ? data["org"] : null
727
826
  };
728
827
  }
828
+ async function revokeSession(client, token) {
829
+ if (isExpertToken(token)) return false;
830
+ return client.delete("/api/v1/session").then(() => true).catch(() => false);
831
+ }
729
832
  async function logout(options = {}) {
730
833
  const path = options.credentialsPath ?? defaultCredentialsPath(options.env ?? process.env);
731
834
  const stored = readCredentials(path);
732
835
  if (stored === null) {
836
+ const envToken = (options.env ?? process.env)["HARNESS_API_TOKEN"] ?? "";
733
837
  return {
734
838
  ok: false,
735
- message: "No stored login to remove. A HARNESS_API_TOKEN from the environment is yours to manage \u2014 unset it, or disable the token in the admin UI."
839
+ message: "No stored login to remove. A HARNESS_API_TOKEN from the environment is yours to manage \u2014 " + (isExpertToken(envToken.trim()) ? "unset it, or revoke the pxc_ credential in your expert view." : "unset it, or disable the token in the admin UI.")
736
840
  };
737
841
  }
738
- const revoked = await bareClient(stored.url, stored.token, options).delete("/api/v1/session").then(() => true).catch(() => false);
842
+ const expert = isExpertToken(stored.token);
843
+ const revoked = await revokeSession(bareClient(stored.url, stored.token, options), stored.token);
739
844
  const note = revoked ? "" : " (server-side revoke failed \u2014 the token still expires on its own)";
740
845
  try {
741
846
  deleteCredentials(path);
@@ -749,24 +854,47 @@ async function logout(options = {}) {
749
854
  return {
750
855
  ok: true,
751
856
  json: { logged_out: true, revoked },
752
- human: `Logged out of ${stored.url}${note}.
857
+ human: expert ? `Forgot the outside expert's pxc_ credential for ${stored.url}. It stays valid until it expires: revoke it in your expert view to end it now.
858
+ ` : `Logged out of ${stored.url}${note}.
753
859
  `
754
860
  };
755
861
  }
756
862
  async function whoami(config, credentialSource) {
757
863
  const client = new Client(config);
864
+ const expert = isExpertToken(config.token);
865
+ let probe;
866
+ let session = null;
867
+ let serverVersion;
758
868
  try {
759
- await client.get("/api/v1/stats");
869
+ const described2 = isLoginCredential(config.token) ? await describeSession(client) : null;
870
+ if (described2 !== null) {
871
+ session = described2.session;
872
+ serverVersion = described2.apiVersion;
873
+ } else {
874
+ const answered = await client.getVersioned(expert ? EXPERT_ASSIGNMENTS_PATH : "/api/v1/stats");
875
+ probe = answered.envelope.data;
876
+ serverVersion = answered.apiVersion;
877
+ }
760
878
  } catch (error) {
761
879
  if (error instanceof HttpError) {
762
880
  return {
763
881
  ok: false,
764
- message: `signed in to ${config.baseUrl} with a ${credentialSource} credential, but it failed verification: ${error.message} (HTTP ${error.status})`
882
+ message: expert ? `signed in to ${config.baseUrl} with an outside expert's pxc_ credential (${credentialSource}), but it was refused: ${error.message} (HTTP ${error.status})${error.status === 401 ? ` \u2014 ${EXPERT_LAPSED}` : ""}` : `signed in to ${config.baseUrl} with a ${credentialSource} credential, but it failed verification: ${error.message} (HTTP ${error.status})`
765
883
  };
766
884
  }
767
885
  throw error;
768
886
  }
769
- const serverVersion = await client.serverVersion().catch(() => "unknown");
887
+ let seat = null;
888
+ if (expert) {
889
+ seat = expertSeat(probe);
890
+ if (seat === null) {
891
+ return {
892
+ ok: false,
893
+ message: `signed in to ${config.baseUrl} with an outside expert's pxc_ credential, but the assignments response carried no expert`
894
+ };
895
+ }
896
+ }
897
+ serverVersion ??= "unknown";
770
898
  const hostSourceLabels = {
771
899
  env: "HARNESS_API_URL",
772
900
  "repo-config": ".harness-cli/config.json",
@@ -774,6 +902,10 @@ async function whoami(config, credentialSource) {
774
902
  default: "workspace default"
775
903
  };
776
904
  const via = config.hostSource !== void 0 ? ` (via ${hostSourceLabels[config.hostSource]})` : "";
905
+ const expertLines = seat === null ? "" : `Expert: ${expertSummary(seat)}
906
+ Reaches: its own assignments and the gates it owns on them (assignments, conditions, gate)
907
+ `;
908
+ const described = session ?? NO_SESSION;
777
909
  return {
778
910
  ok: true,
779
911
  json: {
@@ -781,14 +913,58 @@ async function whoami(config, credentialSource) {
781
913
  source: credentialSource,
782
914
  verified: true,
783
915
  server_version: serverVersion,
784
- host_source: config.hostSource
916
+ host_source: config.hostSource,
917
+ kind: described.kind,
918
+ expires_at: described.expires_at,
919
+ scopes: described.scopes,
920
+ user: described.user,
921
+ org: described.org,
922
+ expert: seat
785
923
  },
786
924
  human: `Host: ${config.baseUrl}${via}
787
- Credential: ${credentialSource}
788
- Verified: ok (server API ${serverVersion})
925
+ Credential: ${credentialSource}${seat === null ? "" : ", an outside expert's pxc_ credential"}${kindLabel(described.kind)}
926
+ ` + sessionLines(described) + expertLines + `Verified: ok (server API ${serverVersion})
789
927
  `
790
928
  };
791
929
  }
930
+ function kindLabel(kind) {
931
+ if (kind === null) return "";
932
+ if (kind === "harness-cli") return ", harness-cli (full access)";
933
+ if (kind === "operator-cli") return ", operator-cli (Operator only)";
934
+ return `, ${clean(kind)}`;
935
+ }
936
+ function sessionLines(session) {
937
+ let lines = "";
938
+ const user2 = session.user;
939
+ if (user2 !== null) {
940
+ const name = clean(user2["name"]);
941
+ const email = clean(user2["email"]);
942
+ lines += `User: ${name === "" ? email : email === "" ? name : `${name} <${email}>`}
943
+ `;
944
+ }
945
+ const org = session.org;
946
+ if (org !== null) {
947
+ const name = clean(org["name"]);
948
+ const slug = clean(org["slug"]);
949
+ lines += `Org: ${slug === "" || slug === name ? name : `${name} (${slug})`}
950
+ `;
951
+ }
952
+ if (session.expires_at !== null) lines += `Expires: ${clean(session.expires_at)}
953
+ `;
954
+ if (session.kind === "operator-cli") {
955
+ lines += "Reaches: Operator commands only; issue, plan and gate commands are refused\n";
956
+ }
957
+ return lines;
958
+ }
959
+ function expertSeat(data) {
960
+ if (!isRecord2(data) || !isRecord2(data["expert"])) return null;
961
+ const plan2 = isRecord2(data["plan"]) ? { id: data["plan"]["id"], status: data["plan"]["status"] } : null;
962
+ const steps = Array.isArray(data["assignments"]) ? data["assignments"].filter(isRecord2).length : 0;
963
+ return { id: data["expert"]["id"], role: data["expert"]["role"] ?? null, plan: plan2, assigned_steps: steps };
964
+ }
965
+ function isRecord2(value) {
966
+ return typeof value === "object" && value !== null && !Array.isArray(value);
967
+ }
792
968
  function bareClient(baseUrl, token, options) {
793
969
  const config = {
794
970
  baseUrl: baseUrl.replace(/\/+$/, ""),
@@ -801,7 +977,7 @@ function bareClient(baseUrl, token, options) {
801
977
  return new Client(config);
802
978
  }
803
979
  function defaultNotify(line) {
804
- process.stderr.write(line + "\n");
980
+ process.stderr.write(named(line + "\n"));
805
981
  }
806
982
  function defaultPrompt() {
807
983
  const readline = createInterface({
@@ -850,10 +1026,16 @@ import {
850
1026
  import { basename, dirname, join as join2, parse, sep } from "path";
851
1027
  import { fileURLToPath } from "url";
852
1028
  var PACKAGE_NAME = "@allocator-one/harness-cli";
1029
+ var PACKAGE_NAMES = [PACKAGE_NAME, OPERATOR_PACKAGE];
853
1030
  var UPGRADE_INSTALL_SPEC = `${PACKAGE_NAME}@latest`;
1031
+ function upgradeInstallSpec(install) {
1032
+ return `${install.packageName ?? PACKAGE_NAME}@latest`;
1033
+ }
854
1034
  var UPDATE_CHECK_COMMAND = "__update-check";
855
1035
  var REEXEC_SENTINEL = "HARNESS_UPGRADED";
856
- var DEFAULT_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
1036
+ function latestUrl(packageName = PACKAGE_NAME) {
1037
+ return `https://registry.npmjs.org/${packageName}/latest`;
1038
+ }
857
1039
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
858
1040
  var REGISTRY_TIMEOUT_MS = 5e3;
859
1041
  var NPM_ROOT_TIMEOUT_MS = 15e3;
@@ -866,21 +1048,24 @@ function detectInstall(from = fileURLToPath(import.meta.url)) {
866
1048
  nodeModules: null
867
1049
  };
868
1050
  if (from.split(sep).includes("_npx")) return { ...unknown, kind: "npx" };
869
- const packageRoot = findPackageRoot(from);
870
- if (packageRoot === null) return unknown;
1051
+ const found = findPackageRoot(from);
1052
+ if (found === null) return unknown;
1053
+ const { packageRoot, packageName } = found;
871
1054
  if (existsSync(join2(packageRoot, ".git"))) {
872
- return { kind: "checkout", packageRoot, nodeModules: null };
1055
+ return { kind: "checkout", packageRoot, packageName, nodeModules: null };
873
1056
  }
874
1057
  const nodeModules = enclosingNodeModules(packageRoot);
875
- if (nodeModules === null) return { ...unknown, packageRoot };
1058
+ if (nodeModules === null) return { ...unknown, packageRoot, packageName };
876
1059
  const kind = existsSync(join2(nodeModules, ".bin")) ? "local" : "global";
877
- return { kind, packageRoot, nodeModules };
1060
+ return { kind, packageRoot, packageName, nodeModules };
878
1061
  }
879
1062
  function findPackageRoot(from) {
880
1063
  let dir = dirname(from);
881
1064
  const { root } = parse(dir);
882
1065
  for (; ; ) {
883
- if (packageNameAt(join2(dir, "package.json")) === PACKAGE_NAME) return dir;
1066
+ const name = packageNameAt(join2(dir, "package.json"));
1067
+ if (name !== null && PACKAGE_NAMES.includes(name))
1068
+ return { packageRoot: dir, packageName: name };
884
1069
  if (dir === root) return null;
885
1070
  dir = dirname(dir);
886
1071
  }
@@ -958,12 +1143,15 @@ function comparePrerelease(left, right) {
958
1143
  }
959
1144
  return 0;
960
1145
  }
961
- function stampPath(env = process.env) {
962
- return join2(resolveCacheHome(env), "harness", "update-check.json");
1146
+ function stampPath(env = process.env, packageName = PACKAGE_NAME) {
1147
+ const file = packageName === PACKAGE_NAME ? "update-check.json" : `update-check-${packageName.replace(/^@[^/]+\//, "")}.json`;
1148
+ return join2(resolveCacheHome(env), "harness", file);
963
1149
  }
964
- function readStamp(env = process.env) {
1150
+ function readStamp(env = process.env, packageName = PACKAGE_NAME) {
965
1151
  try {
966
- const parsed = JSON.parse(readFileSync(stampPath(env), "utf8"));
1152
+ const parsed = JSON.parse(
1153
+ readFileSync(stampPath(env, packageName), "utf8")
1154
+ );
967
1155
  if (typeof parsed !== "object" || parsed === null) return null;
968
1156
  const record = parsed;
969
1157
  const checkedAt = record["checkedAt"];
@@ -975,10 +1163,10 @@ function readStamp(env = process.env) {
975
1163
  return null;
976
1164
  }
977
1165
  }
978
- function writeStamp(stamp, env = process.env) {
979
- const path = stampPath(env);
1166
+ function writeStamp(stamp, env = process.env, packageName = PACKAGE_NAME) {
1167
+ const path = stampPath(env, packageName);
980
1168
  const temporary = `${path}.${process.pid}.tmp`;
981
- const latest = stamp.latest ?? readStamp(env)?.latest ?? null;
1169
+ const latest = stamp.latest ?? readStamp(env, packageName)?.latest ?? null;
982
1170
  try {
983
1171
  mkdirSync(dirname(path), { recursive: true });
984
1172
  writeFileSync(temporary, JSON.stringify({ ...stamp, latest }) + "\n");
@@ -996,9 +1184,10 @@ function updateCheck(options = {}) {
996
1184
  if (!checksEnabled(env)) return null;
997
1185
  const install = options.install ?? detectInstall();
998
1186
  if (install.kind !== "global" && install.kind !== "local") return null;
999
- const stamp = readStamp(env);
1187
+ const packageName = install.packageName ?? PACKAGE_NAME;
1188
+ const stamp = readStamp(env, packageName);
1000
1189
  if (stamp === null || now - stamp.checkedAt >= CHECK_INTERVAL_MS) {
1001
- writeStamp({ checkedAt: now, latest: stamp?.latest ?? null }, env);
1190
+ writeStamp({ checkedAt: now, latest: stamp?.latest ?? null }, env, packageName);
1002
1191
  (options.refresh ?? spawnRefresh)();
1003
1192
  }
1004
1193
  if (stamp !== null && stamp.latest !== null && isNewer(stamp.latest, CLI_VERSION)) {
@@ -1008,7 +1197,7 @@ function updateCheck(options = {}) {
1008
1197
  return null;
1009
1198
  }
1010
1199
  function upgradeHint(install) {
1011
- return install.kind === "local" ? `npm i ${PACKAGE_NAME}@latest` : `npm i -g ${PACKAGE_NAME}@latest`;
1200
+ return install.kind === "local" ? `npm i ${upgradeInstallSpec(install)}` : `npm i -g ${upgradeInstallSpec(install)}`;
1012
1201
  }
1013
1202
  function spawnRefresh() {
1014
1203
  const self = process.argv[1];
@@ -1024,13 +1213,17 @@ function spawnRefresh() {
1024
1213
  } catch {
1025
1214
  }
1026
1215
  }
1027
- async function performUpdateCheck(env = process.env, now = Date.now()) {
1216
+ async function performUpdateCheck(env = process.env, now = Date.now(), packageName = detectInstall().packageName ?? PACKAGE_NAME) {
1028
1217
  if (!checksEnabled(env)) return;
1029
- writeStamp({ checkedAt: now, latest: await fetchLatestVersion(env) }, env);
1218
+ writeStamp(
1219
+ { checkedAt: now, latest: await fetchLatestVersion(env, packageName) },
1220
+ env,
1221
+ packageName
1222
+ );
1030
1223
  }
1031
- async function fetchLatestVersion(env) {
1224
+ async function fetchLatestVersion(env, packageName) {
1032
1225
  const configured = env["HARNESS_REGISTRY_URL"];
1033
- const url = configured !== void 0 && configured.trim() !== "" ? configured.trim() : DEFAULT_REGISTRY_URL;
1226
+ const url = configured !== void 0 && configured.trim() !== "" ? configured.trim() : latestUrl(packageName);
1034
1227
  try {
1035
1228
  const response = await fetch(url, {
1036
1229
  headers: { accept: "application/json" },
@@ -1148,8 +1341,10 @@ function npmInstallGlobal(install, stillHeld) {
1148
1341
  detail: "lost the upgrade lock while preparing \u2014 another process took over"
1149
1342
  };
1150
1343
  }
1151
- process.stderr.write(`harness: upgrading ${PACKAGE_NAME} \u2026
1152
- `);
1344
+ process.stderr.write(
1345
+ named(`harness: upgrading ${install.packageName ?? PACKAGE_NAME} \u2026
1346
+ `)
1347
+ );
1153
1348
  const args = [
1154
1349
  "install",
1155
1350
  "--global",
@@ -1158,7 +1353,7 @@ function npmInstallGlobal(install, stillHeld) {
1158
1353
  "--no-fund",
1159
1354
  "--no-update-notifier",
1160
1355
  "--loglevel=error",
1161
- UPGRADE_INSTALL_SPEC
1356
+ upgradeInstallSpec(install)
1162
1357
  ];
1163
1358
  const result = spawnSync(npmCommand(), args, {
1164
1359
  // npm's diagnostics belong on stderr with ours; stdout stays the
@@ -1216,8 +1411,8 @@ function reexecSelf(env, argv2) {
1216
1411
  });
1217
1412
  if (result.error !== void 0) {
1218
1413
  process.stderr.write(
1219
- `harness: upgraded, but re-running failed: ${result.error.message}
1220
- `
1414
+ named(`harness: upgraded, but re-running failed: ${result.error.message}
1415
+ `)
1221
1416
  );
1222
1417
  return 1;
1223
1418
  }
@@ -1320,9 +1515,15 @@ function exitWith(code) {
1320
1515
  }
1321
1516
  guardBrokenPipes();
1322
1517
  var program = new Command();
1323
- program.name("harness").description("issue tracking for humans and agents (harness.infra.one)").exitOverride().configureOutput({
1324
- writeOut: (text) => process.stdout.write(text),
1325
- writeErr: (text) => process.stderr.write(text)
1518
+ var invokedAs = invokedName();
1519
+ function writeStderr(text) {
1520
+ return process.stderr.write(named(text, invokedAs));
1521
+ }
1522
+ program.name(invokedAs).description(
1523
+ invokedAs === "operator" ? "drive Operator from the terminal and any agent, with Harness issue tracking alongside (harness.infra.one)" : "issue tracking for humans and agents (harness.infra.one)"
1524
+ ).exitOverride().configureOutput({
1525
+ writeOut: (text) => process.stdout.write(named(text, invokedAs)),
1526
+ writeErr: (text) => writeStderr(text)
1326
1527
  });
1327
1528
  function integer(value) {
1328
1529
  if (!/^\d+$/.test(value))
@@ -1363,17 +1564,17 @@ function interactive() {
1363
1564
  const ci = process.env["CI"];
1364
1565
  return process.stdout.isTTY === true && (ci === void 0 || ci === "");
1365
1566
  }
1366
- async function withConfig(jsonFlag, mutating, run) {
1567
+ async function withConfig(jsonFlag, mutating, run2) {
1367
1568
  const resolved = resolveConfig();
1368
- for (const warning of resolved.warnings) process.stderr.write(warning + "\n");
1569
+ for (const warning of resolved.warnings) writeStderr(warning + "\n");
1369
1570
  if (resolved.kind === "error") {
1370
- process.stderr.write(`harness: ${resolved.message}
1571
+ writeStderr(`harness: ${resolved.message}
1371
1572
  `);
1372
1573
  return exitWith(2);
1373
1574
  }
1374
1575
  if (resolved.kind === "needs-login") {
1375
1576
  if (!interactive()) {
1376
- process.stderr.write(loginHelp(resolved.baseUrl));
1577
+ writeStderr(loginHelp(resolved.baseUrl));
1377
1578
  return exitWith(2);
1378
1579
  }
1379
1580
  if (resolved.hostSource === "repo-config" && !isHostTrusted(resolved.baseUrl)) {
@@ -1381,7 +1582,7 @@ async function withConfig(jsonFlag, mutating, run) {
1381
1582
  `This repo's config (${resolved.repoConfigPath ?? ".harness-cli/config.json"}) targets ${resolved.baseUrl} \u2014 log in there? [y/N] `
1382
1583
  );
1383
1584
  if (answer.trim().toLowerCase() !== "y") {
1384
- process.stderr.write(
1585
+ writeStderr(
1385
1586
  `harness: not logging in to ${resolved.baseUrl} \u2014 set HARNESS_API_URL to override the repo config
1386
1587
  `
1387
1588
  );
@@ -1391,49 +1592,49 @@ async function withConfig(jsonFlag, mutating, run) {
1391
1592
  }
1392
1593
  const loginResult = await login(resolved.baseUrl, {});
1393
1594
  if (!loginResult.ok) {
1394
- process.stderr.write(`harness: ${clean(loginResult.message)}
1595
+ writeStderr(`harness: ${clean(loginResult.message)}
1395
1596
  `);
1396
1597
  return exitWith(1);
1397
1598
  }
1398
- process.stderr.write(loginResult.human);
1599
+ writeStderr(loginResult.human);
1399
1600
  const again = resolveConfig();
1400
1601
  if (again.kind !== "ok") {
1401
- process.stderr.write(
1602
+ writeStderr(
1402
1603
  `harness: still not signed in to ${resolved.baseUrl} after logging in
1403
1604
  `
1404
1605
  );
1405
1606
  return exitWith(1);
1406
1607
  }
1407
- return execute(again.config, jsonFlag, mutating, run);
1608
+ return execute(again.config, jsonFlag, mutating, run2);
1408
1609
  }
1409
- return execute(resolved.config, jsonFlag, mutating, run);
1610
+ return execute(resolved.config, jsonFlag, mutating, run2);
1410
1611
  }
1411
- async function execute(config, jsonFlag, mutating, run) {
1612
+ async function execute(config, jsonFlag, mutating, run2) {
1412
1613
  const client = new Client(config);
1413
1614
  const hostNotice = mutating ? `\u2192 ${config.baseUrl}
1414
1615
  ` : "";
1415
1616
  try {
1416
- const output = await run({ client, config });
1417
- process.stderr.write(hostNotice);
1418
- if (output.notice !== void 0) process.stderr.write(output.notice);
1617
+ const output = await run2({ client, config });
1618
+ writeStderr(hostNotice);
1619
+ if (output.notice !== void 0) writeStderr(output.notice);
1419
1620
  process.stdout.write(jsonFlag ? json(output.json) : output.human);
1420
1621
  return exitWith(output.exitCode ?? (output.partial === true ? 1 : 0));
1421
1622
  } catch (error) {
1422
- return fail(error, hostNotice);
1623
+ return fail(explainExpertRefusal(error, config.token), hostNotice);
1423
1624
  }
1424
1625
  }
1425
1626
  async function fail(error, hostNotice) {
1426
1627
  if (error instanceof UsageError) {
1427
- process.stderr.write(error.message + "\n");
1628
+ writeStderr(error.message + "\n");
1428
1629
  return exitWith(2);
1429
1630
  }
1430
1631
  if (error instanceof LocalError) {
1431
- process.stderr.write(`harness: ${clean(error.message)}
1632
+ writeStderr(`harness: ${clean(error.message)}
1432
1633
  `);
1433
1634
  return exitWith(1);
1434
1635
  }
1435
1636
  if (error instanceof TransportError) {
1436
- process.stderr.write(
1637
+ writeStderr(
1437
1638
  hostNotice + `harness: cannot reach the API: ${clean(error.message)}
1438
1639
  `
1439
1640
  );
@@ -1443,21 +1644,21 @@ async function fail(error, hostNotice) {
1443
1644
  const healed = selfHeal();
1444
1645
  if (healed.kind === "upgraded") return exitWith(healed.code);
1445
1646
  const why = healed.kind === "failed" ? ` (tried to upgrade: ${clean(healed.reason)})` : "";
1446
- process.stderr.write(
1647
+ writeStderr(
1447
1648
  hostNotice + `harness: ${clean(error.message)} (HTTP 426)${why}
1448
1649
  `
1449
1650
  );
1450
1651
  return exitWith(1);
1451
1652
  }
1452
1653
  if (error instanceof HttpError) {
1453
- process.stderr.write(
1654
+ writeStderr(
1454
1655
  hostNotice + `harness: ${clean(error.message)} (HTTP ${error.status})${errorDetails(error.body)}
1455
1656
  `
1456
1657
  );
1457
1658
  return exitWith(1);
1458
1659
  }
1459
1660
  if (error instanceof CommandError) {
1460
- process.stderr.write(hostNotice + `harness: ${clean(error.message)}
1661
+ writeStderr(hostNotice + `harness: ${clean(error.message)}
1461
1662
  `);
1462
1663
  return exitWith(1);
1463
1664
  }
@@ -1518,7 +1719,7 @@ program.command("show").description("full issue detail; downloads attached image
1518
1719
  });
1519
1720
  }
1520
1721
  );
1521
- program.command("activity").description("mixed issue activity: comments, creation, state and assignee changes").argument("[id]").option(
1722
+ program.command("activity").description("mixed issue activity: comments, creation, state, assignee, wait-condition and field changes").argument("[id]").option(
1522
1723
  "--limit <n>",
1523
1724
  `entries per page (default 20, max ${ACTIVITY_MAX_LIMIT})`,
1524
1725
  activityLimit
@@ -1759,15 +1960,213 @@ for (const [verb, satisfied] of [
1759
1960
  ["satisfy", true],
1760
1961
  ["unsatisfy", false]
1761
1962
  ]) {
1762
- gate.command(verb).description(`${satisfied ? "tick" : "untick"} a gate on an issue, by its label or condition id`).argument("[issue]").argument("[gate]").option("--json", "emit JSON").action(async (issue, gateName, opts) => {
1963
+ gate.command(verb).description(`${satisfied ? "tick" : "untick"} a gate on an issue, by its key, label or condition id`).argument("[issue]").argument("[gate]").option("--json", "emit JSON").action(async (issue, gateName, opts) => {
1763
1964
  await withConfig(opts.json === true, true, (ctx) => {
1764
1965
  if (issue === void 0 || gateName === void 0) {
1765
- throw new UsageError(`usage: harness gate ${verb} <issue> <gate-label-or-id>`);
1966
+ throw new UsageError(`usage: harness gate ${verb} <issue> <gate-key-label-or-id>`);
1766
1967
  }
1767
1968
  return gateSet(ctx, issue, gateName, satisfied);
1768
1969
  });
1769
1970
  });
1770
1971
  }
1972
+ program.command("conditions").description("an issue's wait conditions; with an outside expert's pxc_ credential, the gates they own on it").argument("[issue]").option("--json", "emit JSON").action(async (issue, opts) => {
1973
+ await withConfig(opts.json === true, false, (ctx) => {
1974
+ if (issue === void 0) throw new UsageError("usage: harness conditions <issue>");
1975
+ return conditionList(ctx, issue);
1976
+ });
1977
+ });
1978
+ program.command("assignments").description("an outside expert's steps and the gates they own on them (pxc_ credentials only)").option("--json", "emit JSON").action(async (opts) => {
1979
+ await withConfig(opts.json === true, false, (ctx) => assignments(ctx));
1980
+ });
1981
+ var NO_OBJECTIVE_TEXT = "By reference only: never the objective's text, which the API does not return because it can quote what Operator read; the workbench link opens it. Approval happens in the app.";
1982
+ program.command("jobs").description("Operator jobs you see in the workbench, newest first").option(
1983
+ "--status <states>",
1984
+ `comma-separated job states (${JOB_STATES.join(", ")}) or open | waiting | done`
1985
+ ).option("--mine", "only jobs you own").option(`--limit <n>`, `jobs per page (default 20, max ${JOBS_MAX_LIMIT})`, integer).option("--all", `follow every page (up to ${JOBS_MAX_PAGES})`).option("--json", "emit JSON").addHelpText("after", `
1986
+ ${NO_OBJECTIVE_TEXT}
1987
+ `).action(async (opts) => {
1988
+ requireJobsArgs(opts);
1989
+ await withConfig(opts.json === true, false, (ctx) => jobsList(ctx, opts));
1990
+ });
1991
+ program.command("job").description("one Operator job: status, plan, progress, window, spend, approval link, effects, artifacts, people").argument("[id]").option("--changes", "instead, its change set by reference (never values or targets), with the approval link").option("--people", "instead, who is on it by name: requester, execution principal, owner, approvers, collaborators").option("--json", "emit JSON").addHelpText("after", `
1992
+ ${NO_OBJECTIVE_TEXT}
1993
+ `).action(async (id, opts) => {
1994
+ if (id === void 0) throw new UsageError("usage: harness job <job-id> [--changes] [--people]");
1995
+ requireJobId(id);
1996
+ await withConfig(
1997
+ opts.json === true,
1998
+ false,
1999
+ (ctx) => jobShow(ctx, id, { changes: opts.changes, people: opts.people })
2000
+ );
2001
+ });
2002
+ program.command("watch").description("follow an Operator job until it completes, fails, or waits for a person").argument("[id]").option("--after <seq>", "start after this event sequence (default 0: the whole history)", integer).option("--through-waits", "keep following while a person has to act, instead of stopping").option("--json", "emit one JSON document when the watch ends").addHelpText(
2003
+ "after",
2004
+ `
2005
+ Polls the job's events every 2 s, and every 10 s while the job is paused or
2006
+ a person has to act. A person has to act exactly when the job's waiting_on is
2007
+ set: an approval, a person, reconciliation, verification or delivery. A job
2008
+ paused with nothing for a person to do resumes by itself, so watch follows it.
2009
+ It only reads: approval happens in the app, and the approval link is printed.
2010
+ A failed poll is retried 3 times (1, 2, 4 s, or the server's Retry-After).
2011
+
2012
+ Exit codes:
2013
+ 0 the job completed (or stdout's reader went away, e.g. | head)
2014
+ ${EXIT_WAITING_FOR_PERSON} a person has to act (not with --through-waits); the link is printed
2015
+ ${EXIT_JOB_ENDED} the job failed or was cancelled
2016
+ ${EXIT_INTERRUPTED} interrupted with Ctrl-C; stderr says where to resume (--after)
2017
+ 1, 3 as for every command: an API error, or the API unreachable after the retries;
2018
+ stderr says where to resume, and --json still prints the events read so far
2019
+ 2 a usage error
2020
+ `
2021
+ ).action(async (id, opts) => {
2022
+ if (id === void 0) throw new UsageError("usage: harness watch <job-id>");
2023
+ requireJobId(id);
2024
+ const json2 = opts.json === true;
2025
+ await withConfig(
2026
+ json2,
2027
+ false,
2028
+ (ctx) => jobWatch(ctx, id, {
2029
+ after: opts.after,
2030
+ throughWaits: opts.throughWaits === true,
2031
+ collectEvents: json2,
2032
+ onLine: json2 ? void 0 : (line) => process.stdout.write(line),
2033
+ signal: watchStopSignal()
2034
+ })
2035
+ );
2036
+ });
2037
+ var NOT_RETRIED = `
2038
+ Not idempotent, so it is never retried, not even after a timeout or a server
2039
+ error. When a failure leaves the outcome unknown it says so and exits 1:
2040
+ check \`job <id>\` before trying again. Exit 3 means the request never
2041
+ reached the server, so nothing changed. It answers the job as \`job\` shows
2042
+ it. Approving stays in the app.
2043
+ `;
2044
+ program.command("steer").description("post an instruction to an Operator job, as Steer does in the app; earlier approvals stop applying").argument("[id]").argument("[instruction...]").option("--instruction-file <path>", 'read the instruction from a file ("-" for stdin)').option("--json", "emit JSON").addHelpText(
2045
+ "after",
2046
+ `
2047
+ Posts the instruction in the job's room, attributed to you, wherever the app
2048
+ offers Steer; the job's owner and collaborators may steer. Work it changes
2049
+ needs a fresh approval, and a running job stops its current run to take it.
2050
+ At most ${STEER_MAX_CHARACTERS} characters once trimmed.
2051
+
2052
+ Quote the instruction: unquoted words that look like options (--json, --help)
2053
+ are read as options, not text. Put -- before an instruction that starts with
2054
+ "-", or pass it with --instruction-file, which takes any text as it is.
2055
+ ${NOT_RETRIED}`
2056
+ ).action(async (id, words, opts) => {
2057
+ if (id === void 0 || words.length === 0 && opts.instructionFile === void 0) {
2058
+ throw new UsageError(
2059
+ "usage: harness steer <job-id> <instruction> | harness steer <job-id> --instruction-file <path>"
2060
+ );
2061
+ }
2062
+ requireJobId(id);
2063
+ const instruction = instructionFrom(words, opts.instructionFile);
2064
+ await withConfig(opts.json === true, true, (ctx) => jobSteer(ctx, id, instruction));
2065
+ });
2066
+ program.command("cancel").description("cancel an Operator job; committed effects stay committed, and their counts are printed").argument("[id]").option("--json", "emit JSON").addHelpText(
2067
+ "after",
2068
+ `
2069
+ Ends the job, as Cancel does in the app; only its execution principal may.
2070
+ Staged effects that never ran count as unknown.
2071
+ ${NOT_RETRIED}`
2072
+ ).action(async (id, opts) => {
2073
+ if (id === void 0) throw new UsageError("usage: harness cancel <job-id>");
2074
+ requireJobId(id);
2075
+ await withConfig(opts.json === true, true, (ctx) => jobCancel(ctx, id));
2076
+ });
2077
+ program.command("pause").description("pause a running Operator job for a person, keeping its active-time budget").argument("[id]").option("--json", "emit JSON").addHelpText(
2078
+ "after",
2079
+ `
2080
+ Holds the job, as Pause does in the app; only its execution principal may.
2081
+ Continue it to run again.
2082
+ ${NOT_RETRIED}`
2083
+ ).action(async (id, opts) => {
2084
+ if (id === void 0) throw new UsageError("usage: harness pause <job-id>");
2085
+ requireJobId(id);
2086
+ await withConfig(opts.json === true, true, (ctx) => jobPause(ctx, id));
2087
+ });
2088
+ var NOT_IDEMPOTENT = "Not idempotent: it is sent once and never retried, not after a timeout or a 5xx either. Exit 3 means the request never reached the server, so nothing changed. When it may have, the outcome is unknown: it exits 1 and says so. Then check";
2089
+ program.command("run").description("start an Operator job from an outcome, as Run as job does in the app; --watch follows it").argument("[outcome...]").option("--budget <usd>", "US dollars the organization's monthly Operator budget must still have (admins only)", once("--budget")).option("--skill <name>", "run one of the organization's Operator skills, by invocation name (/name)", once("--skill")).option("--watch", "follow the job like `watch --through-waits`; it never approves").option("--json", "emit JSON").addHelpText(
2090
+ "after",
2091
+ `
2092
+ Starts a job for you in a new org chat of yours, under your own current rights.
2093
+ Prints the job id, its state and its workbench link, and the approval link
2094
+ while a change set waits for approval. Approving happens in the app; the CLI
2095
+ never approves. You can start 30 jobs an hour. ${NOT_IDEMPOTENT} \`jobs --mine\` for the new job before running it again.
2096
+
2097
+ With --watch the job is followed until it completes (exit 0) or fails or is
2098
+ cancelled (exit ${EXIT_JOB_ENDED}). When it waits for a person, the approval link is printed and the
2099
+ watch keeps waiting; Ctrl-C stops it (exit ${EXIT_INTERRUPTED}) and stderr says how to resume.
2100
+ `
2101
+ ).action(async (words, opts) => {
2102
+ const request = prepareRun(words.join(" "), { budget: opts.budget, skill: opts.skill });
2103
+ const json2 = opts.json === true;
2104
+ await withConfig(
2105
+ json2,
2106
+ true,
2107
+ (ctx) => run(ctx, request, {
2108
+ watch: opts.watch === true,
2109
+ json: json2,
2110
+ onLine: (line) => process.stdout.write(line),
2111
+ signal: opts.watch === true ? watchStopSignal() : void 0
2112
+ })
2113
+ );
2114
+ });
2115
+ function once(flag) {
2116
+ return (value, previous) => {
2117
+ if (previous !== void 0) throw new InvalidArgumentError(`${flag} given twice \u2014 pass it once`);
2118
+ return value;
2119
+ };
2120
+ }
2121
+ program.command("continue").description(
2122
+ "open a new audited window for an Operator job that ran out of time or waits on a person, as Continue does in the app"
2123
+ ).argument("[id]").option("--json", "emit JSON").addHelpText(
2124
+ "after",
2125
+ `
2126
+ Only the job's execution principal, the person it runs as, may continue it. ${NOT_IDEMPOTENT} \`job <id>\` before continuing it again.
2127
+ `
2128
+ ).action(async (id, opts) => {
2129
+ if (id === void 0) throw new UsageError("usage: harness continue <job-id>");
2130
+ requireJobId(id);
2131
+ await withConfig(opts.json === true, true, (ctx) => jobContinue(ctx, id));
2132
+ });
2133
+ var procedure = program.command("procedure").description("saved Operator procedures: procedure save|show|run");
2134
+ procedure.command("save").description("save a completed, verified job as a procedure, as Save procedure does in the app (its owner only)").argument("[job-id]").option("--name <name>", "the procedure's name (trimmed, at most 200 characters)", once("--name")).option("--json", "emit JSON").addHelpText(
2135
+ "after",
2136
+ `
2137
+ Saving again saves another procedure. ${NOT_IDEMPOTENT} the job's procedures (\`job <id>\`) before saving it again.
2138
+ `
2139
+ ).action(async (jobId, opts) => {
2140
+ if (jobId === void 0) throw new UsageError('usage: harness procedure save <job-id> --name "\u2026"');
2141
+ requireJobId(jobId);
2142
+ const name = requireProcedureName(opts.name);
2143
+ await withConfig(opts.json === true, true, (ctx) => procedureSave(ctx, jobId, name));
2144
+ });
2145
+ procedure.command("show").description("one procedure: its source job, versions (digest, capabilities, actions) and the parameters a run takes").argument("[id]").option("--json", "emit JSON").action(async (id, opts) => {
2146
+ if (id === void 0) throw new UsageError("usage: harness procedure show <procedure-id>");
2147
+ requireProcedureId(id);
2148
+ await withConfig(opts.json === true, false, (ctx) => procedureShow(ctx, id));
2149
+ });
2150
+ procedure.command("run").description("run a procedure as a new job, with fresh inputs, your current rights and an approval of its own in the app").argument("[id]").option("--version <n>", "the version expected to be current (default: whichever is current)", once("--version")).option("--param <name=value>", "a parameter, as a string; name:=<json> for a JSON value (repeatable)", collect, []).option("--params-file <path>", 'the parameters as one JSON object ("-" for stdin)', once("--params-file")).option("--group <id>", "the contact group to reconcile, when the procedure needs one", once("--group")).option("--source <id>", "the uploaded file to reconcile against, when the procedure needs one", once("--source")).option("--json", "emit JSON").addHelpText(
2151
+ "after",
2152
+ `
2153
+ Nothing of the saved job carries over: no grant, approval, delegation, collaborator or read material. Approving happens in the app. ${NOT_IDEMPOTENT} \`jobs\` for the new job before running it again.
2154
+ `
2155
+ ).action(
2156
+ async (id, opts) => {
2157
+ if (id === void 0) {
2158
+ throw new UsageError("usage: harness procedure run <procedure-id> [--version N] [--param name=value]\u2026");
2159
+ }
2160
+ const request = prepareProcedureRun(id, {
2161
+ version: opts.version,
2162
+ param: opts.param,
2163
+ paramsFile: opts.paramsFile,
2164
+ group: opts.group,
2165
+ source: opts.source
2166
+ });
2167
+ await withConfig(opts.json === true, true, (ctx) => startProcedureRun(ctx, request));
2168
+ }
2169
+ );
1771
2170
  var commentCommand = program.command("comment").description("comment on an issue").argument("[id]").argument("[text...]").option("--body-file <path>", 'read the comment body from a file ("-" for stdin)').option("--mention <name>", "notify a person (repeatable)", collect, []).option("--json", "emit JSON").action(
1772
2171
  async (id, text, opts) => {
1773
2172
  await withConfig(opts.json === true, true, (ctx) => {
@@ -2047,9 +2446,40 @@ program.command("upload").description("upload an image (25 MB) or H.264 MP4 vide
2047
2446
  return upload(ctx, path);
2048
2447
  });
2049
2448
  });
2449
+ var tools = program.command("tools").description(
2450
+ "the capabilities Operator can use now in this organization (switched-off ones never appear); write and send capabilities need approval in the app"
2451
+ ).option("--domain <domain>", "only this domain (`tools domains` lists them)").option("--cursor <cursor>", "continue from a page's cursor (from the paging notice)").option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
2452
+ requireToolsOpts(opts);
2453
+ await withConfig(opts.json === true, false, (ctx) => toolsList(ctx, opts));
2454
+ });
2455
+ tools.command("search").description("search what Operator can use now by keyword, best match first").argument("[query...]").option("--domain <domain>", "only this domain").option("--cursor <cursor>", "continue from a page's cursor (from the paging notice)").option("--all", "follow every page").option("--json", "emit JSON").action(async (query, opts) => {
2456
+ const merged = { ...tools.opts(), ...opts };
2457
+ if (query.length === 0) {
2458
+ throw new UsageError("usage: harness tools search <query> [--domain <domain>] [--all]");
2459
+ }
2460
+ requireSearchQuery(query.join(" "));
2461
+ requireToolsOpts(merged);
2462
+ await withConfig(merged.json === true, false, (ctx) => toolsSearch(ctx, query.join(" "), merged));
2463
+ });
2464
+ tools.command("domains").description("every domain Operator can use capabilities in, with how many").option("--json", "emit JSON").action(async (opts) => {
2465
+ const parent = tools.opts();
2466
+ if (parent.domain !== void 0 || parent.cursor !== void 0 || parent.all === true) {
2467
+ throw new UsageError("usage: harness tools domains [--json] (it takes no --domain, --cursor or --all)");
2468
+ }
2469
+ await withConfig(opts.json === true || parent.json === true, false, (ctx) => toolsDomains(ctx));
2470
+ });
2471
+ program.command("tool").description("one capability Operator can use, with its whole input schema (or, for a read, its result schema)").argument("[id]").option("--schema <which>", "input (the default) or result, for a read").option("--json", "emit JSON").action(async (id, opts) => {
2472
+ if (id === void 0) throw new UsageError("usage: harness tool <capability-id> [--schema input|result]");
2473
+ requireCapabilityId(id);
2474
+ requireSchemaView(opts.schema);
2475
+ await withConfig(opts.json === true, false, (ctx) => toolShow(ctx, id, opts));
2476
+ });
2477
+ program.command("skills").description("the organization's skills assigned to Operator: identity and content checksum, never content").option("--json", "emit JSON").action(async (opts) => {
2478
+ await withConfig(opts.json === true, false, (ctx) => skillsList(ctx));
2479
+ });
2050
2480
  program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
2051
- const { startMcpServer } = await import("./server-75QJASTY.js");
2052
- await startMcpServer();
2481
+ const { startMcpServer } = await import("./server-KVRQBVXI.js");
2482
+ await startMcpServer(invokedAs);
2053
2483
  });
2054
2484
  var keys = program.command("keys").description("the org's provider API keys, handed to tooling without printing them").enablePositionalOptions();
2055
2485
  keys.command("list").description("provider slugs the org has keys for and the env var each maps to \u2014 never the values").option("--json", "emit JSON").action(async (opts) => {
@@ -2078,11 +2508,11 @@ program.command("login").description("sign in via the browser (infra.one session
2078
2508
  async (opts) => {
2079
2509
  const repoLookup = findRepoConfig();
2080
2510
  for (const warning of repoLookup.warnings)
2081
- process.stderr.write(warning + "\n");
2511
+ writeStderr(warning + "\n");
2082
2512
  const repoUrl = repoLookup.config?.url;
2083
2513
  const url = opts.url ?? blankToUndefined(process.env["HARNESS_API_URL"]) ?? repoUrl ?? storedLoginUrl() ?? DEFAULT_BASE_URL;
2084
2514
  if (url === repoUrl && opts.url === void 0 && blankToUndefined(process.env["HARNESS_API_URL"]) === void 0) {
2085
- process.stderr.write(
2515
+ writeStderr(
2086
2516
  `harness: host ${url} comes from ${repoLookup.config?.path}
2087
2517
  `
2088
2518
  );
@@ -2098,18 +2528,27 @@ program.command("logout").description("revoke and forget the stored login").opti
2098
2528
  program.command("whoami").description("show the resolved host and credential source").option("--json", "emit JSON").action(async (opts) => {
2099
2529
  const resolved = resolveConfig();
2100
2530
  for (const warning of resolved.warnings)
2101
- process.stderr.write(warning + "\n");
2531
+ writeStderr(warning + "\n");
2102
2532
  if (resolved.kind === "error") {
2103
- process.stderr.write(`harness: ${resolved.message}
2533
+ writeStderr(`harness: ${resolved.message}
2104
2534
  `);
2105
2535
  return exitWith(2);
2106
2536
  }
2107
2537
  if (resolved.kind === "needs-login") {
2108
- process.stderr.write(loginHelp(resolved.baseUrl));
2538
+ writeStderr(loginHelp(resolved.baseUrl));
2109
2539
  return exitWith(2);
2110
2540
  }
2111
2541
  const source = blankToUndefined(process.env["HARNESS_API_TOKEN"]) !== void 0 ? "environment" : "stored login";
2112
- await finishAuth(await whoami(resolved.config, source), opts.json === true);
2542
+ let result;
2543
+ try {
2544
+ result = await whoami(resolved.config, source);
2545
+ } catch (error) {
2546
+ if (!(error instanceof TransportError)) throw error;
2547
+ writeStderr(`harness: cannot reach the API: ${clean(error.message)}
2548
+ `);
2549
+ return exitWith(3);
2550
+ }
2551
+ await finishAuth(result, opts.json === true);
2113
2552
  });
2114
2553
  function storedLoginUrl() {
2115
2554
  return readCredentials(defaultCredentialsPath())?.url;
@@ -2122,7 +2561,7 @@ async function finishAuth(result, json2) {
2122
2561
  process.stdout.write(json2 ? json(result.json) : result.human);
2123
2562
  return exitWith(0);
2124
2563
  }
2125
- process.stderr.write(`harness: ${clean(result.message)}
2564
+ writeStderr(`harness: ${clean(result.message)}
2126
2565
  `);
2127
2566
  return exitWith(1);
2128
2567
  }
@@ -2134,9 +2573,10 @@ if (argv[2] === UPDATE_CHECK_COMMAND) {
2134
2573
  }
2135
2574
  try {
2136
2575
  const notice = updateCheck();
2137
- if (notice !== null) process.stderr.write(notice);
2576
+ if (notice !== null) writeStderr(notice);
2138
2577
  } catch {
2139
2578
  }
2579
+ if (invokedAs === "operator") layoutOperatorHelp(program);
2140
2580
  if (argv.length <= 2) {
2141
2581
  program.outputHelp({ error: true });
2142
2582
  await exitWith(2);
@@ -2145,7 +2585,7 @@ try {
2145
2585
  await program.parseAsync(argv);
2146
2586
  } catch (error) {
2147
2587
  if (error instanceof UsageError) {
2148
- process.stderr.write(error.message + "\n");
2588
+ writeStderr(error.message + "\n");
2149
2589
  await exitWith(2);
2150
2590
  }
2151
2591
  if (error instanceof CommanderError) {