@allocator-one/harness-cli 1.13.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,25 +1,37 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ACTIVITY_MAX_LIMIT,
4
+ CLI_PACKAGE,
4
5
  CLI_VERSION,
5
6
  Client,
6
7
  CommandError,
7
8
  DEFAULT_BASE_URL,
9
+ EXIT_INTERRUPTED,
10
+ EXIT_JOB_ENDED,
11
+ EXIT_WAITING_FOR_PERSON,
12
+ EXPERT_ASSIGNMENTS_PATH,
13
+ EXPERT_LAPSED,
8
14
  HttpError,
15
+ JOBS_MAX_LIMIT,
16
+ JOBS_MAX_PAGES,
17
+ JOB_STATES,
9
18
  LocalError,
10
19
  RELATION_CREATE_TYPES,
11
20
  RELATION_UPDATE_TYPES,
21
+ STEER_MAX_CHARACTERS,
12
22
  TransportError,
13
23
  UpgradeRequiredError,
14
24
  UsageError,
15
25
  activity,
16
26
  archive,
27
+ assignments,
17
28
  attachImage,
18
29
  blocked,
19
30
  clean,
20
31
  close,
21
32
  comment,
22
33
  comments,
34
+ conditionList,
23
35
  create,
24
36
  defaultCredentialsPath,
25
37
  deleteComment,
@@ -30,8 +42,21 @@ import {
30
42
  docShow,
31
43
  docUpdate,
32
44
  editComment,
45
+ errorDetails,
46
+ expertSummary,
47
+ explainExpertRefusal,
33
48
  findRepoConfig,
49
+ gateSet,
50
+ instructionFrom,
51
+ isExpertToken,
34
52
  isHostTrusted,
53
+ jobCancel,
54
+ jobContinue,
55
+ jobPause,
56
+ jobShow,
57
+ jobSteer,
58
+ jobWatch,
59
+ jobsList,
35
60
  json,
36
61
  labelCreate,
37
62
  labelDelete,
@@ -48,24 +73,47 @@ import {
48
73
  memberSetRole,
49
74
  milestoneCreate,
50
75
  milestoneList,
76
+ planCreate,
77
+ planEdit,
78
+ planHold,
79
+ planRelease,
80
+ planShow,
81
+ prepareProcedureRun,
51
82
  prime,
83
+ procedureSave,
84
+ procedureShow,
52
85
  projectCreate,
53
86
  projectList,
54
87
  projectShow,
55
88
  projectUpdate,
56
89
  readCredentials,
90
+ readPlanFile,
57
91
  ready,
58
92
  relationAdd,
59
93
  relationList,
60
94
  relationRm,
61
95
  relationUpdate,
96
+ requireCapabilityId,
62
97
  requireCloseArgs,
63
98
  requireCommentId,
64
99
  requireIdentifiers,
100
+ requireJobId,
101
+ requireJobsArgs,
102
+ requirePlanId,
103
+ requireProcedureId,
104
+ requireProcedureName,
105
+ requireReadyClaimArgs,
106
+ requireSchemaView,
107
+ requireSearchQuery,
108
+ requireToolsOpts,
109
+ requiredTeam,
65
110
  resolveCacheHome,
66
111
  resolveConfig,
112
+ resolveTextSources,
67
113
  search,
68
114
  show,
115
+ skillsList,
116
+ startProcedureRun,
69
117
  stateAdd,
70
118
  stateDelete,
71
119
  stateOrder,
@@ -75,15 +123,21 @@ import {
75
123
  teamList,
76
124
  teamRename,
77
125
  teamSetDefault,
126
+ toolShow,
127
+ toolsDomains,
128
+ toolsList,
129
+ toolsSearch,
78
130
  trustHost,
79
131
  update,
80
132
  upload,
81
133
  userList,
82
134
  validateBaseUrl,
135
+ validateIdempotencyKey,
83
136
  version,
137
+ watchStopSignal,
84
138
  workflowStates,
85
139
  writeCredentials
86
- } from "./chunk-XFMECLOU.js";
140
+ } from "./chunk-I2S2JVCQ.js";
87
141
 
88
142
  // src/cli.ts
89
143
  import { createInterface as createInterface2 } from "readline";
@@ -666,7 +720,9 @@ async function exchangeCode(baseUrl, code, verifier, options) {
666
720
  async function manualLogin(baseUrl, options) {
667
721
  const notify = options.notify ?? defaultNotify;
668
722
  const prompt = options.prompt ?? defaultPrompt;
669
- notify(`Open ${baseUrl}/cli/login in a browser, authorize, and paste the token below.`);
723
+ notify(
724
+ `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).`
725
+ );
670
726
  const token = (await prompt()).trim();
671
727
  if (token === "") return { ok: false, message: "no token entered" };
672
728
  return verifyAndStore(baseUrl, token, options);
@@ -674,13 +730,15 @@ async function manualLogin(baseUrl, options) {
674
730
  async function verifyAndStore(baseUrl, token, options) {
675
731
  const path = options.credentialsPath ?? defaultCredentialsPath(options.env ?? process.env);
676
732
  const client = bareClient(baseUrl, token, options);
733
+ const expert = isExpertToken(token);
677
734
  try {
678
- await client.get("/api/v1/teams");
735
+ await client.get(expert ? EXPERT_ASSIGNMENTS_PATH : "/api/v1/teams");
679
736
  } catch (error) {
680
737
  if (error instanceof HttpError) {
738
+ const lapsed = expert && error.status === 401 ? ` \u2014 ${EXPERT_LAPSED}` : "";
681
739
  return {
682
740
  ok: false,
683
- message: `the received token failed verification (${error.message}, HTTP ${error.status}) \u2014 nothing was stored`
741
+ message: `the received token failed verification (${error.message}, HTTP ${error.status}) \u2014 nothing was stored${lapsed}`
684
742
  };
685
743
  }
686
744
  if (error instanceof TransportError) {
@@ -696,33 +754,44 @@ async function verifyAndStore(baseUrl, token, options) {
696
754
  try {
697
755
  writeCredentials(path, baseUrl, token);
698
756
  } catch (error) {
699
- await client.delete("/api/v1/session").catch(() => void 0);
757
+ await revokeSession(client, token);
700
758
  const detail = error instanceof Error ? error.message : String(error);
701
759
  return {
702
760
  ok: false,
703
- message: `could not store credentials at ${path}: ${detail}`
761
+ message: `could not store credentials at ${path}: ${detail}` + (expert ? " (the pxc_ credential is still valid: revoke it in your expert view)" : "")
704
762
  };
705
763
  }
764
+ let supersededNote = "";
706
765
  if (superseded !== null) {
707
- await bareClient(superseded.url, superseded.token, options).delete("/api/v1/session").catch(() => void 0);
766
+ await revokeSession(bareClient(superseded.url, superseded.token, options), superseded.token);
767
+ if (isExpertToken(superseded.token)) {
768
+ supersededNote = "The pxc_ credential stored before stays valid until it expires: revoke it in your expert view.\n";
769
+ }
708
770
  }
709
771
  return {
710
772
  ok: true,
711
773
  json: { url: baseUrl, credentials: path },
712
- human: `Logged in to ${baseUrl} \u2014 login token stored at ${path} (valid ~30 days; harness logout revokes it).
713
- `
774
+ 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 harness logout only forgets it). Commands now act as the expert: only assignments, conditions and gate work until you log in again.
775
+ ` : `Logged in to ${baseUrl} \u2014 login token stored at ${path} (valid ~30 days; harness logout revokes it).
776
+ `) + supersededNote
714
777
  };
715
778
  }
779
+ async function revokeSession(client, token) {
780
+ if (isExpertToken(token)) return false;
781
+ return client.delete("/api/v1/session").then(() => true).catch(() => false);
782
+ }
716
783
  async function logout(options = {}) {
717
784
  const path = options.credentialsPath ?? defaultCredentialsPath(options.env ?? process.env);
718
785
  const stored = readCredentials(path);
719
786
  if (stored === null) {
787
+ const envToken = (options.env ?? process.env)["HARNESS_API_TOKEN"] ?? "";
720
788
  return {
721
789
  ok: false,
722
- 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."
790
+ 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.")
723
791
  };
724
792
  }
725
- const revoked = await bareClient(stored.url, stored.token, options).delete("/api/v1/session").then(() => true).catch(() => false);
793
+ const expert = isExpertToken(stored.token);
794
+ const revoked = await revokeSession(bareClient(stored.url, stored.token, options), stored.token);
726
795
  const note = revoked ? "" : " (server-side revoke failed \u2014 the token still expires on its own)";
727
796
  try {
728
797
  deleteCredentials(path);
@@ -736,23 +805,36 @@ async function logout(options = {}) {
736
805
  return {
737
806
  ok: true,
738
807
  json: { logged_out: true, revoked },
739
- human: `Logged out of ${stored.url}${note}.
808
+ 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.
809
+ ` : `Logged out of ${stored.url}${note}.
740
810
  `
741
811
  };
742
812
  }
743
813
  async function whoami(config, credentialSource) {
744
814
  const client = new Client(config);
815
+ const expert = isExpertToken(config.token);
816
+ let probe;
745
817
  try {
746
- await client.get("/api/v1/stats");
818
+ probe = (await client.get(expert ? EXPERT_ASSIGNMENTS_PATH : "/api/v1/stats")).data;
747
819
  } catch (error) {
748
820
  if (error instanceof HttpError) {
749
821
  return {
750
822
  ok: false,
751
- message: `signed in to ${config.baseUrl} with a ${credentialSource} credential, but it failed verification: ${error.message} (HTTP ${error.status})`
823
+ 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})`
752
824
  };
753
825
  }
754
826
  throw error;
755
827
  }
828
+ let seat = null;
829
+ if (expert) {
830
+ seat = expertSeat(probe);
831
+ if (seat === null) {
832
+ return {
833
+ ok: false,
834
+ message: `signed in to ${config.baseUrl} with an outside expert's pxc_ credential, but the assignments response carried no expert`
835
+ };
836
+ }
837
+ }
756
838
  const serverVersion = await client.serverVersion().catch(() => "unknown");
757
839
  const hostSourceLabels = {
758
840
  env: "HARNESS_API_URL",
@@ -761,6 +843,9 @@ async function whoami(config, credentialSource) {
761
843
  default: "workspace default"
762
844
  };
763
845
  const via = config.hostSource !== void 0 ? ` (via ${hostSourceLabels[config.hostSource]})` : "";
846
+ const expertLines = seat === null ? "" : `Expert: ${expertSummary(seat)}
847
+ Reaches: its own assignments and the gates it owns on them (assignments, conditions, gate)
848
+ `;
764
849
  return {
765
850
  ok: true,
766
851
  json: {
@@ -768,14 +853,24 @@ async function whoami(config, credentialSource) {
768
853
  source: credentialSource,
769
854
  verified: true,
770
855
  server_version: serverVersion,
771
- host_source: config.hostSource
856
+ host_source: config.hostSource,
857
+ expert: seat
772
858
  },
773
859
  human: `Host: ${config.baseUrl}${via}
774
- Credential: ${credentialSource}
775
- Verified: ok (server API ${serverVersion})
860
+ Credential: ${credentialSource}${seat === null ? "" : ", an outside expert's pxc_ credential"}
861
+ ` + expertLines + `Verified: ok (server API ${serverVersion})
776
862
  `
777
863
  };
778
864
  }
865
+ function expertSeat(data) {
866
+ if (!isRecord2(data) || !isRecord2(data["expert"])) return null;
867
+ const plan2 = isRecord2(data["plan"]) ? { id: data["plan"]["id"], status: data["plan"]["status"] } : null;
868
+ const steps = Array.isArray(data["assignments"]) ? data["assignments"].filter(isRecord2).length : 0;
869
+ return { id: data["expert"]["id"], role: data["expert"]["role"] ?? null, plan: plan2, assigned_steps: steps };
870
+ }
871
+ function isRecord2(value) {
872
+ return typeof value === "object" && value !== null && !Array.isArray(value);
873
+ }
779
874
  function bareClient(baseUrl, token, options) {
780
875
  const config = {
781
876
  baseUrl: baseUrl.replace(/\/+$/, ""),
@@ -809,113 +904,6 @@ function defaultOpenBrowser(url) {
809
904
  }
810
905
  }
811
906
 
812
- // src/core/textinput.ts
813
- import { readFileSync, statSync } from "fs";
814
- import { isatty } from "tty";
815
- var STDIN_PATH = "-";
816
- var MAX_TEXT_BYTES = 1e6;
817
- function flagOf(name) {
818
- return name.replace(/[A-Z]/g, (upper) => `-${upper.toLowerCase()}`);
819
- }
820
- function resolveTextSources(sources) {
821
- const resolved = {};
822
- const toRead = [];
823
- let stdinClaimedBy;
824
- for (const { name, inline, file } of sources) {
825
- if (inline !== void 0 && file !== void 0) {
826
- throw new UsageError(
827
- `--${flagOf(name)} and --${flagOf(name)}-file both given \u2014 pass one; there is no precedence between them`
828
- );
829
- }
830
- if (file === void 0) {
831
- if (inline !== void 0) resolved[name] = inline;
832
- continue;
833
- }
834
- if (file === STDIN_PATH) {
835
- if (isatty(0)) {
836
- throw new UsageError(
837
- `--${flagOf(name)}-file - reads stdin, but stdin is a terminal \u2014 pipe something in or pass a path`
838
- );
839
- }
840
- if (stdinClaimedBy !== void 0) {
841
- throw new UsageError(
842
- `--${flagOf(name)}-file and --${flagOf(stdinClaimedBy)}-file both read stdin \u2014 there is only one stream, so only one may be "-"`
843
- );
844
- }
845
- stdinClaimedBy = name;
846
- }
847
- if (file !== STDIN_PATH) checkReadable(file, name);
848
- toRead.push({ name, file });
849
- }
850
- for (const { name, file } of [...toRead].sort(byStdinLast)) {
851
- resolved[name] = stripOneTrailingNewline(readTextFile(file, name));
852
- }
853
- return resolved;
854
- }
855
- function readTextFile(path, name) {
856
- if (path === STDIN_PATH) {
857
- let piped;
858
- try {
859
- piped = readFileSync(0);
860
- } catch (error) {
861
- throw new UsageError(`--${flagOf(name)}-file -: cannot read stdin (${describe(error)})`);
862
- }
863
- if (piped.length > MAX_TEXT_BYTES) {
864
- throw new UsageError(oversize(`--${flagOf(name)}-file -`));
865
- }
866
- return decodeUtf8(piped, `--${flagOf(name)}-file -`);
867
- }
868
- const flag = `--${flagOf(name)}-file ${path}`;
869
- let bytes;
870
- try {
871
- bytes = readFileSync(path);
872
- } catch (error) {
873
- throw new UsageError(`${flag}: ${describe(error)}`);
874
- }
875
- if (bytes.length > MAX_TEXT_BYTES) throw new UsageError(oversize(flag));
876
- return decodeUtf8(bytes, flag);
877
- }
878
- function decodeUtf8(bytes, flag) {
879
- try {
880
- return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
881
- } catch {
882
- throw new UsageError(`${flag}: not valid UTF-8 \u2014 convert it first`);
883
- }
884
- }
885
- function oversize(what) {
886
- return `${what}: over ${MAX_TEXT_BYTES / 1e6} MB \u2014 that is not prose; check the path`;
887
- }
888
- function checkReadable(path, name) {
889
- const flag = `--${flagOf(name)}-file ${path}`;
890
- let stats2;
891
- try {
892
- stats2 = statSync(path);
893
- } catch (error) {
894
- throw new UsageError(`${flag}: ${describe(error)}`);
895
- }
896
- if (!stats2.isFile()) {
897
- throw new UsageError(
898
- `${flag}: ${stats2.isDirectory() ? "is a directory" : "not a regular file"}`
899
- );
900
- }
901
- if (stats2.size > MAX_TEXT_BYTES) throw new UsageError(oversize(flag));
902
- }
903
- function byStdinLast(a, b) {
904
- return Number(a.file === STDIN_PATH) - Number(b.file === STDIN_PATH);
905
- }
906
- function stripOneTrailingNewline(text) {
907
- if (text.endsWith("\r\n")) return text.slice(0, -2);
908
- if (text.endsWith("\n")) return text.slice(0, -1);
909
- return text;
910
- }
911
- function describe(error) {
912
- const code = error?.code;
913
- if (code === "ENOENT") return "no such file";
914
- if (code === "EISDIR") return "is a directory";
915
- if (code === "EACCES") return "permission denied";
916
- return error instanceof Error ? error.message : String(error);
917
- }
918
-
919
907
  // src/core/stdio.ts
920
908
  var BROKEN_PIPE_CODES = /* @__PURE__ */ new Set(["EPIPE", "ERR_STREAM_DESTROYED"]);
921
909
  function guardBrokenPipes(streams = [process.stdout, process.stderr]) {
@@ -926,6 +914,57 @@ function guardBrokenPipes(streams = [process.stdout, process.stderr]) {
926
914
  }
927
915
  }
928
916
 
917
+ // src/invocation.ts
918
+ import { basename } from "path";
919
+ var OPERATOR_PACKAGE = "@allocator-one/operator";
920
+ function invokedName(argv1 = process.argv[1], packageName = CLI_PACKAGE) {
921
+ const typed = basename(argv1 ?? "").replace(/\.[cm]?js$/i, "").toLowerCase();
922
+ if (typed === "operator" || typed === "harness") return typed;
923
+ return packageName === OPERATOR_PACKAGE ? "operator" : "harness";
924
+ }
925
+ var OPERATOR_COMMANDS = [
926
+ "run",
927
+ "jobs",
928
+ "job",
929
+ "watch",
930
+ "steer",
931
+ "cancel",
932
+ "pause",
933
+ "continue",
934
+ "procedure",
935
+ "plan",
936
+ "gate",
937
+ "conditions",
938
+ "assignments",
939
+ "tools",
940
+ "tool",
941
+ "skills",
942
+ "mcp"
943
+ ];
944
+ var GENERAL_COMMANDS = [
945
+ "login",
946
+ "logout",
947
+ "whoami",
948
+ "version",
949
+ "keys"
950
+ ];
951
+ var OPERATOR_HEADING = "Operator:";
952
+ var ISSUES_HEADING = "Issues:";
953
+ var GENERAL_HEADING = "General:";
954
+ function layoutOperatorHelp(program2) {
955
+ const rank = (name) => {
956
+ const operator = OPERATOR_COMMANDS.indexOf(name);
957
+ if (operator !== -1) return operator;
958
+ return GENERAL_COMMANDS.includes(name) ? 2 * OPERATOR_COMMANDS.length : OPERATOR_COMMANDS.length;
959
+ };
960
+ const heading = (name) => OPERATOR_COMMANDS.includes(name) ? OPERATOR_HEADING : GENERAL_COMMANDS.includes(name) ? GENERAL_HEADING : ISSUES_HEADING;
961
+ for (const command of program2.commands) command.helpGroup(heading(command.name()));
962
+ program2.commands.sort(
963
+ (left, right) => rank(left.name()) - rank(right.name())
964
+ );
965
+ program2.commandsGroup(GENERAL_HEADING).helpCommand(true);
966
+ }
967
+
929
968
  // src/core/upgrade.ts
930
969
  import { spawn as spawn2, spawnSync } from "child_process";
931
970
  import {
@@ -934,20 +973,26 @@ import {
934
973
  existsSync,
935
974
  linkSync,
936
975
  mkdirSync,
937
- readFileSync as readFileSync2,
976
+ readFileSync,
938
977
  realpathSync,
939
978
  renameSync,
940
979
  rmSync,
941
- statSync as statSync2,
980
+ statSync,
942
981
  writeFileSync
943
982
  } from "fs";
944
- import { basename, dirname, join as join2, parse, sep } from "path";
983
+ import { basename as basename2, dirname, join as join2, parse, sep } from "path";
945
984
  import { fileURLToPath } from "url";
946
985
  var PACKAGE_NAME = "@allocator-one/harness-cli";
986
+ var PACKAGE_NAMES = [PACKAGE_NAME, OPERATOR_PACKAGE];
947
987
  var UPGRADE_INSTALL_SPEC = `${PACKAGE_NAME}@latest`;
988
+ function upgradeInstallSpec(install) {
989
+ return `${install.packageName ?? PACKAGE_NAME}@latest`;
990
+ }
948
991
  var UPDATE_CHECK_COMMAND = "__update-check";
949
992
  var REEXEC_SENTINEL = "HARNESS_UPGRADED";
950
- var DEFAULT_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
993
+ function latestUrl(packageName = PACKAGE_NAME) {
994
+ return `https://registry.npmjs.org/${packageName}/latest`;
995
+ }
951
996
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
952
997
  var REGISTRY_TIMEOUT_MS = 5e3;
953
998
  var NPM_ROOT_TIMEOUT_MS = 15e3;
@@ -960,28 +1005,31 @@ function detectInstall(from = fileURLToPath(import.meta.url)) {
960
1005
  nodeModules: null
961
1006
  };
962
1007
  if (from.split(sep).includes("_npx")) return { ...unknown, kind: "npx" };
963
- const packageRoot = findPackageRoot(from);
964
- if (packageRoot === null) return unknown;
1008
+ const found = findPackageRoot(from);
1009
+ if (found === null) return unknown;
1010
+ const { packageRoot, packageName } = found;
965
1011
  if (existsSync(join2(packageRoot, ".git"))) {
966
- return { kind: "checkout", packageRoot, nodeModules: null };
1012
+ return { kind: "checkout", packageRoot, packageName, nodeModules: null };
967
1013
  }
968
1014
  const nodeModules = enclosingNodeModules(packageRoot);
969
- if (nodeModules === null) return { ...unknown, packageRoot };
1015
+ if (nodeModules === null) return { ...unknown, packageRoot, packageName };
970
1016
  const kind = existsSync(join2(nodeModules, ".bin")) ? "local" : "global";
971
- return { kind, packageRoot, nodeModules };
1017
+ return { kind, packageRoot, packageName, nodeModules };
972
1018
  }
973
1019
  function findPackageRoot(from) {
974
1020
  let dir = dirname(from);
975
1021
  const { root } = parse(dir);
976
1022
  for (; ; ) {
977
- if (packageNameAt(join2(dir, "package.json")) === PACKAGE_NAME) return dir;
1023
+ const name = packageNameAt(join2(dir, "package.json"));
1024
+ if (name !== null && PACKAGE_NAMES.includes(name))
1025
+ return { packageRoot: dir, packageName: name };
978
1026
  if (dir === root) return null;
979
1027
  dir = dirname(dir);
980
1028
  }
981
1029
  }
982
1030
  function packageNameAt(path) {
983
1031
  try {
984
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
1032
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
985
1033
  if (typeof parsed !== "object" || parsed === null) return null;
986
1034
  const name = parsed["name"];
987
1035
  return typeof name === "string" ? name : null;
@@ -992,7 +1040,7 @@ function packageNameAt(path) {
992
1040
  function versionAt(packageRoot) {
993
1041
  try {
994
1042
  const parsed = JSON.parse(
995
- readFileSync2(join2(packageRoot, "package.json"), "utf8")
1043
+ readFileSync(join2(packageRoot, "package.json"), "utf8")
996
1044
  );
997
1045
  if (typeof parsed !== "object" || parsed === null) return null;
998
1046
  const version2 = parsed["version"];
@@ -1005,7 +1053,7 @@ function enclosingNodeModules(packageRoot) {
1005
1053
  let dir = dirname(packageRoot);
1006
1054
  const { root } = parse(dir);
1007
1055
  for (; ; ) {
1008
- if (basename(dir) === "node_modules") return dir;
1056
+ if (basename2(dir) === "node_modules") return dir;
1009
1057
  if (dir === root) return null;
1010
1058
  dir = dirname(dir);
1011
1059
  }
@@ -1052,12 +1100,15 @@ function comparePrerelease(left, right) {
1052
1100
  }
1053
1101
  return 0;
1054
1102
  }
1055
- function stampPath(env = process.env) {
1056
- return join2(resolveCacheHome(env), "harness", "update-check.json");
1103
+ function stampPath(env = process.env, packageName = PACKAGE_NAME) {
1104
+ const file = packageName === PACKAGE_NAME ? "update-check.json" : `update-check-${packageName.replace(/^@[^/]+\//, "")}.json`;
1105
+ return join2(resolveCacheHome(env), "harness", file);
1057
1106
  }
1058
- function readStamp(env = process.env) {
1107
+ function readStamp(env = process.env, packageName = PACKAGE_NAME) {
1059
1108
  try {
1060
- const parsed = JSON.parse(readFileSync2(stampPath(env), "utf8"));
1109
+ const parsed = JSON.parse(
1110
+ readFileSync(stampPath(env, packageName), "utf8")
1111
+ );
1061
1112
  if (typeof parsed !== "object" || parsed === null) return null;
1062
1113
  const record = parsed;
1063
1114
  const checkedAt = record["checkedAt"];
@@ -1069,10 +1120,10 @@ function readStamp(env = process.env) {
1069
1120
  return null;
1070
1121
  }
1071
1122
  }
1072
- function writeStamp(stamp, env = process.env) {
1073
- const path = stampPath(env);
1123
+ function writeStamp(stamp, env = process.env, packageName = PACKAGE_NAME) {
1124
+ const path = stampPath(env, packageName);
1074
1125
  const temporary = `${path}.${process.pid}.tmp`;
1075
- const latest = stamp.latest ?? readStamp(env)?.latest ?? null;
1126
+ const latest = stamp.latest ?? readStamp(env, packageName)?.latest ?? null;
1076
1127
  try {
1077
1128
  mkdirSync(dirname(path), { recursive: true });
1078
1129
  writeFileSync(temporary, JSON.stringify({ ...stamp, latest }) + "\n");
@@ -1090,9 +1141,10 @@ function updateCheck(options = {}) {
1090
1141
  if (!checksEnabled(env)) return null;
1091
1142
  const install = options.install ?? detectInstall();
1092
1143
  if (install.kind !== "global" && install.kind !== "local") return null;
1093
- const stamp = readStamp(env);
1144
+ const packageName = install.packageName ?? PACKAGE_NAME;
1145
+ const stamp = readStamp(env, packageName);
1094
1146
  if (stamp === null || now - stamp.checkedAt >= CHECK_INTERVAL_MS) {
1095
- writeStamp({ checkedAt: now, latest: stamp?.latest ?? null }, env);
1147
+ writeStamp({ checkedAt: now, latest: stamp?.latest ?? null }, env, packageName);
1096
1148
  (options.refresh ?? spawnRefresh)();
1097
1149
  }
1098
1150
  if (stamp !== null && stamp.latest !== null && isNewer(stamp.latest, CLI_VERSION)) {
@@ -1102,7 +1154,7 @@ function updateCheck(options = {}) {
1102
1154
  return null;
1103
1155
  }
1104
1156
  function upgradeHint(install) {
1105
- return install.kind === "local" ? `npm i ${PACKAGE_NAME}@latest` : `npm i -g ${PACKAGE_NAME}@latest`;
1157
+ return install.kind === "local" ? `npm i ${upgradeInstallSpec(install)}` : `npm i -g ${upgradeInstallSpec(install)}`;
1106
1158
  }
1107
1159
  function spawnRefresh() {
1108
1160
  const self = process.argv[1];
@@ -1118,13 +1170,17 @@ function spawnRefresh() {
1118
1170
  } catch {
1119
1171
  }
1120
1172
  }
1121
- async function performUpdateCheck(env = process.env, now = Date.now()) {
1173
+ async function performUpdateCheck(env = process.env, now = Date.now(), packageName = detectInstall().packageName ?? PACKAGE_NAME) {
1122
1174
  if (!checksEnabled(env)) return;
1123
- writeStamp({ checkedAt: now, latest: await fetchLatestVersion(env) }, env);
1175
+ writeStamp(
1176
+ { checkedAt: now, latest: await fetchLatestVersion(env, packageName) },
1177
+ env,
1178
+ packageName
1179
+ );
1124
1180
  }
1125
- async function fetchLatestVersion(env) {
1181
+ async function fetchLatestVersion(env, packageName) {
1126
1182
  const configured = env["HARNESS_REGISTRY_URL"];
1127
- const url = configured !== void 0 && configured.trim() !== "" ? configured.trim() : DEFAULT_REGISTRY_URL;
1183
+ const url = configured !== void 0 && configured.trim() !== "" ? configured.trim() : latestUrl(packageName);
1128
1184
  try {
1129
1185
  const response = await fetch(url, {
1130
1186
  headers: { accept: "application/json" },
@@ -1242,8 +1298,10 @@ function npmInstallGlobal(install, stillHeld) {
1242
1298
  detail: "lost the upgrade lock while preparing \u2014 another process took over"
1243
1299
  };
1244
1300
  }
1245
- process.stderr.write(`harness: upgrading ${PACKAGE_NAME} \u2026
1246
- `);
1301
+ process.stderr.write(
1302
+ `harness: upgrading ${install.packageName ?? PACKAGE_NAME} \u2026
1303
+ `
1304
+ );
1247
1305
  const args = [
1248
1306
  "install",
1249
1307
  "--global",
@@ -1252,7 +1310,7 @@ function npmInstallGlobal(install, stillHeld) {
1252
1310
  "--no-fund",
1253
1311
  "--no-update-notifier",
1254
1312
  "--loglevel=error",
1255
- UPGRADE_INSTALL_SPEC
1313
+ upgradeInstallSpec(install)
1256
1314
  ];
1257
1315
  const result = spawnSync(npmCommand(), args, {
1258
1316
  // npm's diagnostics belong on stderr with ours; stdout stays the
@@ -1362,8 +1420,8 @@ function releaseLock(lock) {
1362
1420
  }
1363
1421
  function readHolder(path) {
1364
1422
  try {
1365
- const mtimeMs = statSync2(path).mtimeMs;
1366
- const pid = Number.parseInt(readFileSync2(path, "utf8").trim(), 10);
1423
+ const mtimeMs = statSync(path).mtimeMs;
1424
+ const pid = Number.parseInt(readFileSync(path, "utf8").trim(), 10);
1367
1425
  return { pid: Number.isInteger(pid) && pid > 0 ? pid : null, mtimeMs };
1368
1426
  } catch {
1369
1427
  return null;
@@ -1414,7 +1472,10 @@ function exitWith(code) {
1414
1472
  }
1415
1473
  guardBrokenPipes();
1416
1474
  var program = new Command();
1417
- program.name("harness").description("issue tracking for humans and agents (harness.infra.one)").exitOverride().configureOutput({
1475
+ var invokedAs = invokedName();
1476
+ program.name(invokedAs).description(
1477
+ 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)"
1478
+ ).exitOverride().configureOutput({
1418
1479
  writeOut: (text) => process.stdout.write(text),
1419
1480
  writeErr: (text) => process.stderr.write(text)
1420
1481
  });
@@ -1513,7 +1574,7 @@ async function execute(config, jsonFlag, mutating, run) {
1513
1574
  process.stdout.write(jsonFlag ? json(output.json) : output.human);
1514
1575
  return exitWith(output.exitCode ?? (output.partial === true ? 1 : 0));
1515
1576
  } catch (error) {
1516
- return fail(error, hostNotice);
1577
+ return fail(explainExpertRefusal(error, config.token), hostNotice);
1517
1578
  }
1518
1579
  }
1519
1580
  async function fail(error, hostNotice) {
@@ -1557,17 +1618,11 @@ async function fail(error, hostNotice) {
1557
1618
  }
1558
1619
  throw error;
1559
1620
  }
1560
- function errorDetails(body) {
1561
- const details = body["details"];
1562
- if (typeof details !== "object" || details === null || Array.isArray(details))
1563
- return "";
1564
- return Object.entries(details).map(([field, messages]) => {
1565
- const text = Array.isArray(messages) ? messages.map(String).join("; ") : String(messages);
1566
- return `
1567
- ${clean(field)}: ${clean(text)}`;
1568
- }).join("");
1569
- }
1570
- program.command("ready").description("issues ready to work, highest priority first").option("--claim", "atomically claim the first claimable issue").option("--team <key>", "team filter").option("--limit <n>", "page size", integer).option("--page <n>", "page number", integer).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
1621
+ program.command("ready").description("issues ready to work, highest priority first").option(
1622
+ "--claim",
1623
+ "claim the team's next ready, unassigned issue in one server-side request (team from --team, HARNESS_TEAM or the repo config; no paging flags)"
1624
+ ).option("--team <key>", "team filter; with --claim, the team to claim from").option("--limit <n>", "page size", integer).option("--page <n>", "page number", integer).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
1625
+ if (opts.claim === true) requireReadyClaimArgs(opts);
1571
1626
  await withConfig(
1572
1627
  opts.json === true,
1573
1628
  opts.claim === true,
@@ -1618,7 +1673,7 @@ program.command("show").description("full issue detail; downloads attached image
1618
1673
  });
1619
1674
  }
1620
1675
  );
1621
- program.command("activity").description("mixed issue activity: comments, creation, state and assignee changes").argument("[id]").option(
1676
+ program.command("activity").description("mixed issue activity: comments, creation, state, assignee, wait-condition and field changes").argument("[id]").option(
1622
1677
  "--limit <n>",
1623
1678
  `entries per page (default 20, max ${ACTIVITY_MAX_LIMIT})`,
1624
1679
  activityLimit
@@ -1814,6 +1869,232 @@ relation.command("rm").argument("[issue]").argument("[id-or-issue]").option("--j
1814
1869
  return relationRm(ctx, issue, idOrIssue);
1815
1870
  });
1816
1871
  });
1872
+ var plan = program.command("plan").description("plans: a whole graph of issues created in one request, then released and held");
1873
+ plan.command("create").description("create a plan from a JSON or YAML file (the API's request body); it starts held").option("--file <path>", 'the plan: .json is JSON, anything else YAML ("-" for stdin)').option("--team <key>", "team of the root and of people's steps (or HARNESS_TEAM)").option("--idempotency-key <key>", "reuse the same key when deliberately retrying this create").option("--json", "emit JSON").action(async (opts) => {
1874
+ await withConfig(opts.json === true, true, (ctx) => {
1875
+ if (opts.file === void 0) {
1876
+ throw new UsageError("usage: harness plan create --file <plan.json|plan.yaml> [--team <key>]");
1877
+ }
1878
+ const team2 = requiredTeam(ctx, opts.team);
1879
+ validateIdempotencyKey(opts.idempotencyKey);
1880
+ return planCreate(ctx, readPlanFile(opts.file), { ...opts, team: team2 });
1881
+ });
1882
+ });
1883
+ plan.command("show").description("a plan's status, budgets, revision, steps, edges and wait conditions").argument("[id]").option("--json", "emit JSON").action(async (id, opts) => {
1884
+ await withConfig(opts.json === true, false, (ctx) => {
1885
+ if (id === void 0) throw new UsageError("usage: harness plan show <plan-id>");
1886
+ return planShow(ctx, id);
1887
+ });
1888
+ });
1889
+ plan.command("edit").description("change step executors and keys, and budgets, from a JSON or YAML file").argument("[id]").option("--file <path>", 'the edit: .json is JSON, anything else YAML ("-" for stdin)').option("--json", "emit JSON").action(async (id, opts) => {
1890
+ await withConfig(opts.json === true, true, (ctx) => {
1891
+ if (id === void 0 || opts.file === void 0) {
1892
+ throw new UsageError("usage: harness plan edit <plan-id> --file <edits.json|edits.yaml>");
1893
+ }
1894
+ requirePlanId(id);
1895
+ return planEdit(ctx, id, readPlanFile(opts.file));
1896
+ });
1897
+ });
1898
+ plan.command("release").description("release a plan as yourself: freeze its next revision and let its ready steps run").argument("[id]").option("--collaborator <who>", "a person of this organization (id or email) for the plan room (repeatable)", collect, []).option("--retry-budget <n>", "retries per failed Operator step", integer).option("--spend-budget-cents <n>", "the plan's spend budget; needed to release Operator steps", integer).option("--json", "emit JSON").action(
1899
+ async (id, opts) => {
1900
+ await withConfig(opts.json === true, true, (ctx) => {
1901
+ if (id === void 0) throw new UsageError("usage: harness plan release <plan-id>");
1902
+ return planRelease(ctx, id, { ...opts, collaborators: opts.collaborator });
1903
+ });
1904
+ }
1905
+ );
1906
+ plan.command("hold").description("pause every step of a plan until it is released again").argument("[id]").option("--json", "emit JSON").action(async (id, opts) => {
1907
+ await withConfig(opts.json === true, true, (ctx) => {
1908
+ if (id === void 0) throw new UsageError("usage: harness plan hold <plan-id>");
1909
+ return planHold(ctx, id);
1910
+ });
1911
+ });
1912
+ var gate = program.command("gate").description("manual wait conditions: gate satisfy|unsatisfy <issue> <gate>");
1913
+ for (const [verb, satisfied] of [
1914
+ ["satisfy", true],
1915
+ ["unsatisfy", false]
1916
+ ]) {
1917
+ 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) => {
1918
+ await withConfig(opts.json === true, true, (ctx) => {
1919
+ if (issue === void 0 || gateName === void 0) {
1920
+ throw new UsageError(`usage: harness gate ${verb} <issue> <gate-key-label-or-id>`);
1921
+ }
1922
+ return gateSet(ctx, issue, gateName, satisfied);
1923
+ });
1924
+ });
1925
+ }
1926
+ 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) => {
1927
+ await withConfig(opts.json === true, false, (ctx) => {
1928
+ if (issue === void 0) throw new UsageError("usage: harness conditions <issue>");
1929
+ return conditionList(ctx, issue);
1930
+ });
1931
+ });
1932
+ 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) => {
1933
+ await withConfig(opts.json === true, false, (ctx) => assignments(ctx));
1934
+ });
1935
+ 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.";
1936
+ program.command("jobs").description("Operator jobs you see in the workbench, newest first").option(
1937
+ "--status <states>",
1938
+ `comma-separated job states (${JOB_STATES.join(", ")}) or open | waiting | done`
1939
+ ).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", `
1940
+ ${NO_OBJECTIVE_TEXT}
1941
+ `).action(async (opts) => {
1942
+ requireJobsArgs(opts);
1943
+ await withConfig(opts.json === true, false, (ctx) => jobsList(ctx, opts));
1944
+ });
1945
+ 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", `
1946
+ ${NO_OBJECTIVE_TEXT}
1947
+ `).action(async (id, opts) => {
1948
+ if (id === void 0) throw new UsageError("usage: harness job <job-id> [--changes] [--people]");
1949
+ requireJobId(id);
1950
+ await withConfig(
1951
+ opts.json === true,
1952
+ false,
1953
+ (ctx) => jobShow(ctx, id, { changes: opts.changes, people: opts.people })
1954
+ );
1955
+ });
1956
+ 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(
1957
+ "after",
1958
+ `
1959
+ Polls the job's events every 2 s, and every 10 s while the job is paused or
1960
+ a person has to act. A person has to act exactly when the job's waiting_on is
1961
+ set: an approval, a person, reconciliation, verification or delivery. A job
1962
+ paused with nothing for a person to do resumes by itself, so watch follows it.
1963
+ It only reads: approval happens in the app, and the approval link is printed.
1964
+ A failed poll is retried 3 times (1, 2, 4 s, or the server's Retry-After).
1965
+
1966
+ Exit codes:
1967
+ 0 the job completed (or stdout's reader went away, e.g. | head)
1968
+ ${EXIT_WAITING_FOR_PERSON} a person has to act (not with --through-waits); the link is printed
1969
+ ${EXIT_JOB_ENDED} the job failed or was cancelled
1970
+ ${EXIT_INTERRUPTED} interrupted with Ctrl-C; stderr says where to resume (--after)
1971
+ 1, 3 as for every command: an API error, or the API unreachable after the retries;
1972
+ stderr says where to resume, and --json still prints the events read so far
1973
+ 2 a usage error
1974
+ `
1975
+ ).action(async (id, opts) => {
1976
+ if (id === void 0) throw new UsageError("usage: harness watch <job-id>");
1977
+ requireJobId(id);
1978
+ const json2 = opts.json === true;
1979
+ await withConfig(
1980
+ json2,
1981
+ false,
1982
+ (ctx) => jobWatch(ctx, id, {
1983
+ after: opts.after,
1984
+ throughWaits: opts.throughWaits === true,
1985
+ collectEvents: json2,
1986
+ onLine: json2 ? void 0 : (line) => process.stdout.write(line),
1987
+ signal: watchStopSignal()
1988
+ })
1989
+ );
1990
+ });
1991
+ var NOT_RETRIED = `
1992
+ Not idempotent, so it is never retried, not even after a timeout or a server
1993
+ error. When a failure leaves the outcome unknown it says so and exits 1:
1994
+ check \`job <id>\` before trying again. Exit 3 means the request never
1995
+ reached the server, so nothing changed. It answers the job as \`job\` shows
1996
+ it. Approving stays in the app.
1997
+ `;
1998
+ 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(
1999
+ "after",
2000
+ `
2001
+ Posts the instruction in the job's room, attributed to you, wherever the app
2002
+ offers Steer; the job's owner and collaborators may steer. Work it changes
2003
+ needs a fresh approval, and a running job stops its current run to take it.
2004
+ At most ${STEER_MAX_CHARACTERS} characters once trimmed.
2005
+
2006
+ Quote the instruction: unquoted words that look like options (--json, --help)
2007
+ are read as options, not text. Put -- before an instruction that starts with
2008
+ "-", or pass it with --instruction-file, which takes any text as it is.
2009
+ ${NOT_RETRIED}`
2010
+ ).action(async (id, words, opts) => {
2011
+ if (id === void 0 || words.length === 0 && opts.instructionFile === void 0) {
2012
+ throw new UsageError(
2013
+ "usage: harness steer <job-id> <instruction> | harness steer <job-id> --instruction-file <path>"
2014
+ );
2015
+ }
2016
+ requireJobId(id);
2017
+ const instruction = instructionFrom(words, opts.instructionFile);
2018
+ await withConfig(opts.json === true, true, (ctx) => jobSteer(ctx, id, instruction));
2019
+ });
2020
+ program.command("cancel").description("cancel an Operator job; committed effects stay committed, and their counts are printed").argument("[id]").option("--json", "emit JSON").addHelpText(
2021
+ "after",
2022
+ `
2023
+ Ends the job, as Cancel does in the app; only its execution principal may.
2024
+ Staged effects that never ran count as unknown.
2025
+ ${NOT_RETRIED}`
2026
+ ).action(async (id, opts) => {
2027
+ if (id === void 0) throw new UsageError("usage: harness cancel <job-id>");
2028
+ requireJobId(id);
2029
+ await withConfig(opts.json === true, true, (ctx) => jobCancel(ctx, id));
2030
+ });
2031
+ program.command("pause").description("pause a running Operator job for a person, keeping its active-time budget").argument("[id]").option("--json", "emit JSON").addHelpText(
2032
+ "after",
2033
+ `
2034
+ Holds the job, as Pause does in the app; only its execution principal may.
2035
+ Continue it to run again.
2036
+ ${NOT_RETRIED}`
2037
+ ).action(async (id, opts) => {
2038
+ if (id === void 0) throw new UsageError("usage: harness pause <job-id>");
2039
+ requireJobId(id);
2040
+ await withConfig(opts.json === true, true, (ctx) => jobPause(ctx, id));
2041
+ });
2042
+ 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";
2043
+ function once(flag) {
2044
+ return (value, previous) => {
2045
+ if (previous !== void 0) throw new InvalidArgumentError(`${flag} given twice \u2014 pass it once`);
2046
+ return value;
2047
+ };
2048
+ }
2049
+ program.command("continue").description(
2050
+ "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"
2051
+ ).argument("[id]").option("--json", "emit JSON").addHelpText(
2052
+ "after",
2053
+ `
2054
+ Only the job's execution principal, the person it runs as, may continue it. ${NOT_IDEMPOTENT} \`job <id>\` before continuing it again.
2055
+ `
2056
+ ).action(async (id, opts) => {
2057
+ if (id === void 0) throw new UsageError("usage: harness continue <job-id>");
2058
+ requireJobId(id);
2059
+ await withConfig(opts.json === true, true, (ctx) => jobContinue(ctx, id));
2060
+ });
2061
+ var procedure = program.command("procedure").description("saved Operator procedures: procedure save|show|run");
2062
+ 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(
2063
+ "after",
2064
+ `
2065
+ Saving again saves another procedure. ${NOT_IDEMPOTENT} the job's procedures (\`job <id>\`) before saving it again.
2066
+ `
2067
+ ).action(async (jobId, opts) => {
2068
+ if (jobId === void 0) throw new UsageError('usage: harness procedure save <job-id> --name "\u2026"');
2069
+ requireJobId(jobId);
2070
+ const name = requireProcedureName(opts.name);
2071
+ await withConfig(opts.json === true, true, (ctx) => procedureSave(ctx, jobId, name));
2072
+ });
2073
+ 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) => {
2074
+ if (id === void 0) throw new UsageError("usage: harness procedure show <procedure-id>");
2075
+ requireProcedureId(id);
2076
+ await withConfig(opts.json === true, false, (ctx) => procedureShow(ctx, id));
2077
+ });
2078
+ 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(
2079
+ "after",
2080
+ `
2081
+ 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.
2082
+ `
2083
+ ).action(
2084
+ async (id, opts) => {
2085
+ if (id === void 0) {
2086
+ throw new UsageError("usage: harness procedure run <procedure-id> [--version N] [--param name=value]\u2026");
2087
+ }
2088
+ const request = prepareProcedureRun(id, {
2089
+ version: opts.version,
2090
+ param: opts.param,
2091
+ paramsFile: opts.paramsFile,
2092
+ group: opts.group,
2093
+ source: opts.source
2094
+ });
2095
+ await withConfig(opts.json === true, true, (ctx) => startProcedureRun(ctx, request));
2096
+ }
2097
+ );
1817
2098
  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(
1818
2099
  async (id, text, opts) => {
1819
2100
  await withConfig(opts.json === true, true, (ctx) => {
@@ -2093,9 +2374,40 @@ program.command("upload").description("upload an image (25 MB) or H.264 MP4 vide
2093
2374
  return upload(ctx, path);
2094
2375
  });
2095
2376
  });
2377
+ var tools = program.command("tools").description(
2378
+ "the capabilities Operator can use now in this organization (switched-off ones never appear); write and send capabilities need approval in the app"
2379
+ ).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) => {
2380
+ requireToolsOpts(opts);
2381
+ await withConfig(opts.json === true, false, (ctx) => toolsList(ctx, opts));
2382
+ });
2383
+ 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) => {
2384
+ const merged = { ...tools.opts(), ...opts };
2385
+ if (query.length === 0) {
2386
+ throw new UsageError("usage: harness tools search <query> [--domain <domain>] [--all]");
2387
+ }
2388
+ requireSearchQuery(query.join(" "));
2389
+ requireToolsOpts(merged);
2390
+ await withConfig(merged.json === true, false, (ctx) => toolsSearch(ctx, query.join(" "), merged));
2391
+ });
2392
+ tools.command("domains").description("every domain Operator can use capabilities in, with how many").option("--json", "emit JSON").action(async (opts) => {
2393
+ const parent = tools.opts();
2394
+ if (parent.domain !== void 0 || parent.cursor !== void 0 || parent.all === true) {
2395
+ throw new UsageError("usage: harness tools domains [--json] (it takes no --domain, --cursor or --all)");
2396
+ }
2397
+ await withConfig(opts.json === true || parent.json === true, false, (ctx) => toolsDomains(ctx));
2398
+ });
2399
+ 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) => {
2400
+ if (id === void 0) throw new UsageError("usage: harness tool <capability-id> [--schema input|result]");
2401
+ requireCapabilityId(id);
2402
+ requireSchemaView(opts.schema);
2403
+ await withConfig(opts.json === true, false, (ctx) => toolShow(ctx, id, opts));
2404
+ });
2405
+ program.command("skills").description("the organization's skills assigned to Operator: identity and content checksum, never content").option("--json", "emit JSON").action(async (opts) => {
2406
+ await withConfig(opts.json === true, false, (ctx) => skillsList(ctx));
2407
+ });
2096
2408
  program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
2097
- const { startMcpServer } = await import("./server-CYBKYG5E.js");
2098
- await startMcpServer();
2409
+ const { startMcpServer } = await import("./server-O7CX2ZCC.js");
2410
+ await startMcpServer(invokedAs);
2099
2411
  });
2100
2412
  var keys = program.command("keys").description("the org's provider API keys, handed to tooling without printing them").enablePositionalOptions();
2101
2413
  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) => {
@@ -2155,7 +2467,16 @@ program.command("whoami").description("show the resolved host and credential sou
2155
2467
  return exitWith(2);
2156
2468
  }
2157
2469
  const source = blankToUndefined(process.env["HARNESS_API_TOKEN"]) !== void 0 ? "environment" : "stored login";
2158
- await finishAuth(await whoami(resolved.config, source), opts.json === true);
2470
+ let result;
2471
+ try {
2472
+ result = await whoami(resolved.config, source);
2473
+ } catch (error) {
2474
+ if (!(error instanceof TransportError)) throw error;
2475
+ process.stderr.write(`harness: cannot reach the API: ${clean(error.message)}
2476
+ `);
2477
+ return exitWith(3);
2478
+ }
2479
+ await finishAuth(result, opts.json === true);
2159
2480
  });
2160
2481
  function storedLoginUrl() {
2161
2482
  return readCredentials(defaultCredentialsPath())?.url;
@@ -2183,6 +2504,7 @@ try {
2183
2504
  if (notice !== null) process.stderr.write(notice);
2184
2505
  } catch {
2185
2506
  }
2507
+ if (invokedAs === "operator") layoutOperatorHelp(program);
2186
2508
  if (argv.length <= 2) {
2187
2509
  program.outputHelp({ error: true });
2188
2510
  await exitWith(2);