@allocator-one/harness-cli 1.14.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,
@@ -31,9 +43,20 @@ import {
31
43
  docUpdate,
32
44
  editComment,
33
45
  errorDetails,
46
+ expertSummary,
47
+ explainExpertRefusal,
34
48
  findRepoConfig,
35
49
  gateSet,
50
+ instructionFrom,
51
+ isExpertToken,
36
52
  isHostTrusted,
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,10 @@ import {
55
78
  planHold,
56
79
  planRelease,
57
80
  planShow,
81
+ prepareProcedureRun,
58
82
  prime,
83
+ procedureSave,
84
+ procedureShow,
59
85
  projectCreate,
60
86
  projectList,
61
87
  projectShow,
@@ -67,17 +93,27 @@ import {
67
93
  relationList,
68
94
  relationRm,
69
95
  relationUpdate,
96
+ requireCapabilityId,
70
97
  requireCloseArgs,
71
98
  requireCommentId,
72
99
  requireIdentifiers,
100
+ requireJobId,
101
+ requireJobsArgs,
73
102
  requirePlanId,
103
+ requireProcedureId,
104
+ requireProcedureName,
74
105
  requireReadyClaimArgs,
106
+ requireSchemaView,
107
+ requireSearchQuery,
108
+ requireToolsOpts,
75
109
  requiredTeam,
76
110
  resolveCacheHome,
77
111
  resolveConfig,
78
112
  resolveTextSources,
79
113
  search,
80
114
  show,
115
+ skillsList,
116
+ startProcedureRun,
81
117
  stateAdd,
82
118
  stateDelete,
83
119
  stateOrder,
@@ -87,6 +123,10 @@ import {
87
123
  teamList,
88
124
  teamRename,
89
125
  teamSetDefault,
126
+ toolShow,
127
+ toolsDomains,
128
+ toolsList,
129
+ toolsSearch,
90
130
  trustHost,
91
131
  update,
92
132
  upload,
@@ -94,9 +134,10 @@ import {
94
134
  validateBaseUrl,
95
135
  validateIdempotencyKey,
96
136
  version,
137
+ watchStopSignal,
97
138
  workflowStates,
98
139
  writeCredentials
99
- } from "./chunk-F2YVTB2A.js";
140
+ } from "./chunk-I2S2JVCQ.js";
100
141
 
101
142
  // src/cli.ts
102
143
  import { createInterface as createInterface2 } from "readline";
@@ -679,7 +720,9 @@ async function exchangeCode(baseUrl, code, verifier, options) {
679
720
  async function manualLogin(baseUrl, options) {
680
721
  const notify = options.notify ?? defaultNotify;
681
722
  const prompt = options.prompt ?? defaultPrompt;
682
- 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
+ );
683
726
  const token = (await prompt()).trim();
684
727
  if (token === "") return { ok: false, message: "no token entered" };
685
728
  return verifyAndStore(baseUrl, token, options);
@@ -687,13 +730,15 @@ async function manualLogin(baseUrl, options) {
687
730
  async function verifyAndStore(baseUrl, token, options) {
688
731
  const path = options.credentialsPath ?? defaultCredentialsPath(options.env ?? process.env);
689
732
  const client = bareClient(baseUrl, token, options);
733
+ const expert = isExpertToken(token);
690
734
  try {
691
- await client.get("/api/v1/teams");
735
+ await client.get(expert ? EXPERT_ASSIGNMENTS_PATH : "/api/v1/teams");
692
736
  } catch (error) {
693
737
  if (error instanceof HttpError) {
738
+ const lapsed = expert && error.status === 401 ? ` \u2014 ${EXPERT_LAPSED}` : "";
694
739
  return {
695
740
  ok: false,
696
- 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}`
697
742
  };
698
743
  }
699
744
  if (error instanceof TransportError) {
@@ -709,33 +754,44 @@ async function verifyAndStore(baseUrl, token, options) {
709
754
  try {
710
755
  writeCredentials(path, baseUrl, token);
711
756
  } catch (error) {
712
- await client.delete("/api/v1/session").catch(() => void 0);
757
+ await revokeSession(client, token);
713
758
  const detail = error instanceof Error ? error.message : String(error);
714
759
  return {
715
760
  ok: false,
716
- 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)" : "")
717
762
  };
718
763
  }
764
+ let supersededNote = "";
719
765
  if (superseded !== null) {
720
- 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
+ }
721
770
  }
722
771
  return {
723
772
  ok: true,
724
773
  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
- `
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
727
777
  };
728
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
+ }
729
783
  async function logout(options = {}) {
730
784
  const path = options.credentialsPath ?? defaultCredentialsPath(options.env ?? process.env);
731
785
  const stored = readCredentials(path);
732
786
  if (stored === null) {
787
+ const envToken = (options.env ?? process.env)["HARNESS_API_TOKEN"] ?? "";
733
788
  return {
734
789
  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."
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.")
736
791
  };
737
792
  }
738
- 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);
739
795
  const note = revoked ? "" : " (server-side revoke failed \u2014 the token still expires on its own)";
740
796
  try {
741
797
  deleteCredentials(path);
@@ -749,23 +805,36 @@ async function logout(options = {}) {
749
805
  return {
750
806
  ok: true,
751
807
  json: { logged_out: true, revoked },
752
- 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}.
753
810
  `
754
811
  };
755
812
  }
756
813
  async function whoami(config, credentialSource) {
757
814
  const client = new Client(config);
815
+ const expert = isExpertToken(config.token);
816
+ let probe;
758
817
  try {
759
- await client.get("/api/v1/stats");
818
+ probe = (await client.get(expert ? EXPERT_ASSIGNMENTS_PATH : "/api/v1/stats")).data;
760
819
  } catch (error) {
761
820
  if (error instanceof HttpError) {
762
821
  return {
763
822
  ok: false,
764
- 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})`
765
824
  };
766
825
  }
767
826
  throw error;
768
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
+ }
769
838
  const serverVersion = await client.serverVersion().catch(() => "unknown");
770
839
  const hostSourceLabels = {
771
840
  env: "HARNESS_API_URL",
@@ -774,6 +843,9 @@ async function whoami(config, credentialSource) {
774
843
  default: "workspace default"
775
844
  };
776
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
+ `;
777
849
  return {
778
850
  ok: true,
779
851
  json: {
@@ -781,14 +853,24 @@ async function whoami(config, credentialSource) {
781
853
  source: credentialSource,
782
854
  verified: true,
783
855
  server_version: serverVersion,
784
- host_source: config.hostSource
856
+ host_source: config.hostSource,
857
+ expert: seat
785
858
  },
786
859
  human: `Host: ${config.baseUrl}${via}
787
- Credential: ${credentialSource}
788
- Verified: ok (server API ${serverVersion})
860
+ Credential: ${credentialSource}${seat === null ? "" : ", an outside expert's pxc_ credential"}
861
+ ` + expertLines + `Verified: ok (server API ${serverVersion})
789
862
  `
790
863
  };
791
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
+ }
792
874
  function bareClient(baseUrl, token, options) {
793
875
  const config = {
794
876
  baseUrl: baseUrl.replace(/\/+$/, ""),
@@ -832,6 +914,57 @@ function guardBrokenPipes(streams = [process.stdout, process.stderr]) {
832
914
  }
833
915
  }
834
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
+
835
968
  // src/core/upgrade.ts
836
969
  import { spawn as spawn2, spawnSync } from "child_process";
837
970
  import {
@@ -847,13 +980,19 @@ import {
847
980
  statSync,
848
981
  writeFileSync
849
982
  } from "fs";
850
- import { basename, dirname, join as join2, parse, sep } from "path";
983
+ import { basename as basename2, dirname, join as join2, parse, sep } from "path";
851
984
  import { fileURLToPath } from "url";
852
985
  var PACKAGE_NAME = "@allocator-one/harness-cli";
986
+ var PACKAGE_NAMES = [PACKAGE_NAME, OPERATOR_PACKAGE];
853
987
  var UPGRADE_INSTALL_SPEC = `${PACKAGE_NAME}@latest`;
988
+ function upgradeInstallSpec(install) {
989
+ return `${install.packageName ?? PACKAGE_NAME}@latest`;
990
+ }
854
991
  var UPDATE_CHECK_COMMAND = "__update-check";
855
992
  var REEXEC_SENTINEL = "HARNESS_UPGRADED";
856
- 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
+ }
857
996
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
858
997
  var REGISTRY_TIMEOUT_MS = 5e3;
859
998
  var NPM_ROOT_TIMEOUT_MS = 15e3;
@@ -866,21 +1005,24 @@ function detectInstall(from = fileURLToPath(import.meta.url)) {
866
1005
  nodeModules: null
867
1006
  };
868
1007
  if (from.split(sep).includes("_npx")) return { ...unknown, kind: "npx" };
869
- const packageRoot = findPackageRoot(from);
870
- if (packageRoot === null) return unknown;
1008
+ const found = findPackageRoot(from);
1009
+ if (found === null) return unknown;
1010
+ const { packageRoot, packageName } = found;
871
1011
  if (existsSync(join2(packageRoot, ".git"))) {
872
- return { kind: "checkout", packageRoot, nodeModules: null };
1012
+ return { kind: "checkout", packageRoot, packageName, nodeModules: null };
873
1013
  }
874
1014
  const nodeModules = enclosingNodeModules(packageRoot);
875
- if (nodeModules === null) return { ...unknown, packageRoot };
1015
+ if (nodeModules === null) return { ...unknown, packageRoot, packageName };
876
1016
  const kind = existsSync(join2(nodeModules, ".bin")) ? "local" : "global";
877
- return { kind, packageRoot, nodeModules };
1017
+ return { kind, packageRoot, packageName, nodeModules };
878
1018
  }
879
1019
  function findPackageRoot(from) {
880
1020
  let dir = dirname(from);
881
1021
  const { root } = parse(dir);
882
1022
  for (; ; ) {
883
- 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 };
884
1026
  if (dir === root) return null;
885
1027
  dir = dirname(dir);
886
1028
  }
@@ -911,7 +1053,7 @@ function enclosingNodeModules(packageRoot) {
911
1053
  let dir = dirname(packageRoot);
912
1054
  const { root } = parse(dir);
913
1055
  for (; ; ) {
914
- if (basename(dir) === "node_modules") return dir;
1056
+ if (basename2(dir) === "node_modules") return dir;
915
1057
  if (dir === root) return null;
916
1058
  dir = dirname(dir);
917
1059
  }
@@ -958,12 +1100,15 @@ function comparePrerelease(left, right) {
958
1100
  }
959
1101
  return 0;
960
1102
  }
961
- function stampPath(env = process.env) {
962
- 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);
963
1106
  }
964
- function readStamp(env = process.env) {
1107
+ function readStamp(env = process.env, packageName = PACKAGE_NAME) {
965
1108
  try {
966
- const parsed = JSON.parse(readFileSync(stampPath(env), "utf8"));
1109
+ const parsed = JSON.parse(
1110
+ readFileSync(stampPath(env, packageName), "utf8")
1111
+ );
967
1112
  if (typeof parsed !== "object" || parsed === null) return null;
968
1113
  const record = parsed;
969
1114
  const checkedAt = record["checkedAt"];
@@ -975,10 +1120,10 @@ function readStamp(env = process.env) {
975
1120
  return null;
976
1121
  }
977
1122
  }
978
- function writeStamp(stamp, env = process.env) {
979
- const path = stampPath(env);
1123
+ function writeStamp(stamp, env = process.env, packageName = PACKAGE_NAME) {
1124
+ const path = stampPath(env, packageName);
980
1125
  const temporary = `${path}.${process.pid}.tmp`;
981
- const latest = stamp.latest ?? readStamp(env)?.latest ?? null;
1126
+ const latest = stamp.latest ?? readStamp(env, packageName)?.latest ?? null;
982
1127
  try {
983
1128
  mkdirSync(dirname(path), { recursive: true });
984
1129
  writeFileSync(temporary, JSON.stringify({ ...stamp, latest }) + "\n");
@@ -996,9 +1141,10 @@ function updateCheck(options = {}) {
996
1141
  if (!checksEnabled(env)) return null;
997
1142
  const install = options.install ?? detectInstall();
998
1143
  if (install.kind !== "global" && install.kind !== "local") return null;
999
- const stamp = readStamp(env);
1144
+ const packageName = install.packageName ?? PACKAGE_NAME;
1145
+ const stamp = readStamp(env, packageName);
1000
1146
  if (stamp === null || now - stamp.checkedAt >= CHECK_INTERVAL_MS) {
1001
- writeStamp({ checkedAt: now, latest: stamp?.latest ?? null }, env);
1147
+ writeStamp({ checkedAt: now, latest: stamp?.latest ?? null }, env, packageName);
1002
1148
  (options.refresh ?? spawnRefresh)();
1003
1149
  }
1004
1150
  if (stamp !== null && stamp.latest !== null && isNewer(stamp.latest, CLI_VERSION)) {
@@ -1008,7 +1154,7 @@ function updateCheck(options = {}) {
1008
1154
  return null;
1009
1155
  }
1010
1156
  function upgradeHint(install) {
1011
- 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)}`;
1012
1158
  }
1013
1159
  function spawnRefresh() {
1014
1160
  const self = process.argv[1];
@@ -1024,13 +1170,17 @@ function spawnRefresh() {
1024
1170
  } catch {
1025
1171
  }
1026
1172
  }
1027
- async function performUpdateCheck(env = process.env, now = Date.now()) {
1173
+ async function performUpdateCheck(env = process.env, now = Date.now(), packageName = detectInstall().packageName ?? PACKAGE_NAME) {
1028
1174
  if (!checksEnabled(env)) return;
1029
- writeStamp({ checkedAt: now, latest: await fetchLatestVersion(env) }, env);
1175
+ writeStamp(
1176
+ { checkedAt: now, latest: await fetchLatestVersion(env, packageName) },
1177
+ env,
1178
+ packageName
1179
+ );
1030
1180
  }
1031
- async function fetchLatestVersion(env) {
1181
+ async function fetchLatestVersion(env, packageName) {
1032
1182
  const configured = env["HARNESS_REGISTRY_URL"];
1033
- const url = configured !== void 0 && configured.trim() !== "" ? configured.trim() : DEFAULT_REGISTRY_URL;
1183
+ const url = configured !== void 0 && configured.trim() !== "" ? configured.trim() : latestUrl(packageName);
1034
1184
  try {
1035
1185
  const response = await fetch(url, {
1036
1186
  headers: { accept: "application/json" },
@@ -1148,8 +1298,10 @@ function npmInstallGlobal(install, stillHeld) {
1148
1298
  detail: "lost the upgrade lock while preparing \u2014 another process took over"
1149
1299
  };
1150
1300
  }
1151
- process.stderr.write(`harness: upgrading ${PACKAGE_NAME} \u2026
1152
- `);
1301
+ process.stderr.write(
1302
+ `harness: upgrading ${install.packageName ?? PACKAGE_NAME} \u2026
1303
+ `
1304
+ );
1153
1305
  const args = [
1154
1306
  "install",
1155
1307
  "--global",
@@ -1158,7 +1310,7 @@ function npmInstallGlobal(install, stillHeld) {
1158
1310
  "--no-fund",
1159
1311
  "--no-update-notifier",
1160
1312
  "--loglevel=error",
1161
- UPGRADE_INSTALL_SPEC
1313
+ upgradeInstallSpec(install)
1162
1314
  ];
1163
1315
  const result = spawnSync(npmCommand(), args, {
1164
1316
  // npm's diagnostics belong on stderr with ours; stdout stays the
@@ -1320,7 +1472,10 @@ function exitWith(code) {
1320
1472
  }
1321
1473
  guardBrokenPipes();
1322
1474
  var program = new Command();
1323
- 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({
1324
1479
  writeOut: (text) => process.stdout.write(text),
1325
1480
  writeErr: (text) => process.stderr.write(text)
1326
1481
  });
@@ -1419,7 +1574,7 @@ async function execute(config, jsonFlag, mutating, run) {
1419
1574
  process.stdout.write(jsonFlag ? json(output.json) : output.human);
1420
1575
  return exitWith(output.exitCode ?? (output.partial === true ? 1 : 0));
1421
1576
  } catch (error) {
1422
- return fail(error, hostNotice);
1577
+ return fail(explainExpertRefusal(error, config.token), hostNotice);
1423
1578
  }
1424
1579
  }
1425
1580
  async function fail(error, hostNotice) {
@@ -1518,7 +1673,7 @@ program.command("show").description("full issue detail; downloads attached image
1518
1673
  });
1519
1674
  }
1520
1675
  );
1521
- 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(
1522
1677
  "--limit <n>",
1523
1678
  `entries per page (default 20, max ${ACTIVITY_MAX_LIMIT})`,
1524
1679
  activityLimit
@@ -1759,15 +1914,187 @@ for (const [verb, satisfied] of [
1759
1914
  ["satisfy", true],
1760
1915
  ["unsatisfy", false]
1761
1916
  ]) {
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) => {
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) => {
1763
1918
  await withConfig(opts.json === true, true, (ctx) => {
1764
1919
  if (issue === void 0 || gateName === void 0) {
1765
- throw new UsageError(`usage: harness gate ${verb} <issue> <gate-label-or-id>`);
1920
+ throw new UsageError(`usage: harness gate ${verb} <issue> <gate-key-label-or-id>`);
1766
1921
  }
1767
1922
  return gateSet(ctx, issue, gateName, satisfied);
1768
1923
  });
1769
1924
  });
1770
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
+ );
1771
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(
1772
2099
  async (id, text, opts) => {
1773
2100
  await withConfig(opts.json === true, true, (ctx) => {
@@ -2047,9 +2374,40 @@ program.command("upload").description("upload an image (25 MB) or H.264 MP4 vide
2047
2374
  return upload(ctx, path);
2048
2375
  });
2049
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
+ });
2050
2408
  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();
2409
+ const { startMcpServer } = await import("./server-O7CX2ZCC.js");
2410
+ await startMcpServer(invokedAs);
2053
2411
  });
2054
2412
  var keys = program.command("keys").description("the org's provider API keys, handed to tooling without printing them").enablePositionalOptions();
2055
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) => {
@@ -2109,7 +2467,16 @@ program.command("whoami").description("show the resolved host and credential sou
2109
2467
  return exitWith(2);
2110
2468
  }
2111
2469
  const source = blankToUndefined(process.env["HARNESS_API_TOKEN"]) !== void 0 ? "environment" : "stored login";
2112
- 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);
2113
2480
  });
2114
2481
  function storedLoginUrl() {
2115
2482
  return readCredentials(defaultCredentialsPath())?.url;
@@ -2137,6 +2504,7 @@ try {
2137
2504
  if (notice !== null) process.stderr.write(notice);
2138
2505
  } catch {
2139
2506
  }
2507
+ if (invokedAs === "operator") layoutOperatorHelp(program);
2140
2508
  if (argv.length <= 2) {
2141
2509
  program.outputHelp({ error: true });
2142
2510
  await exitWith(2);