@allocator-one/harness-cli 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  CLI and MCP server for [Harness](https://harness.infra.one), allocator.one's
4
4
  issue tracker. A standalone Node client — no backend checkout, no Erlang
5
- toolchain — that shares credentials with the Elixir escript (`bin/harness`
6
- in the `allocator-one` repo).
5
+ toolchain, and no local issue state: issues live server-side, so there is
6
+ nothing to commit or sync.
7
7
 
8
8
  ## Install
9
9
 
@@ -27,8 +27,8 @@ Requires Node >= 20.
27
27
  No setup needed: commands target `https://harness.infra.one` and start the
28
28
  browser login the first time. One login covers every shell, worktree, and
29
29
  agent slot on a machine — credentials are stored at
30
- `~/.config/harness/credentials.json` (0600), byte-compatible with the
31
- escript's, so either client can log in or out without breaking the other.
30
+ `~/.config/harness/credentials.json` (0600) as a single `{url, token}` pair,
31
+ and a token is only ever sent to the host that minted it.
32
32
 
33
33
  - `harness login` — browser flow (PKCE loopback); `--no-browser` prints the
34
34
  URL and prompts for a pasted token instead.
@@ -88,8 +88,7 @@ from the working directory, gives every clone of a repo its defaults:
88
88
  | `url` | Target host. `HARNESS_API_URL` wins. Tokens stay host-bound: a stored login only serves the host that minted it, an env `HARNESS_API_TOKEN` refuses to pair with a repo-named host (set `HARNESS_API_URL` explicitly), and before the interactive auto-login opens a browser at a host named only by the repo, the CLI asks once per host (remembered in `~/.config/harness/trusted-hosts.json`). |
89
89
  | `org` | Reserved for Harness org-scoping; parsed today, `url` wins meanwhile. |
90
90
 
91
- This is config, not state — issues stay server-side. The escript does not
92
- read this file (TS-CLI-only).
91
+ This is config, not state — issues stay server-side.
93
92
 
94
93
  ## Development
95
94
 
@@ -557,13 +557,14 @@ async function list(ctx, opts) {
557
557
  }
558
558
  async function search(ctx, queryWords, opts) {
559
559
  if (queryWords.length === 0) throw new UsageError("usage: harness search <query>");
560
- const params = {
561
- search: queryWords.join(" "),
562
- page_size: opts.limit ?? 50,
563
- page: opts.page ?? 1
564
- };
565
- const { records, notice } = await fetchIssues(ctx, params, opts);
566
- return { json: records, human: issuesTable(records), notice };
560
+ const deprecation = opts.page !== void 0 || opts.all === true ? "--page/--all are ignored: search returns one ranked list, capped at 20" : void 0;
561
+ let params = { q: queryWords.join(" ") };
562
+ if (opts.limit !== void 0) params = { ...params, limit: opts.limit };
563
+ const body = await ctx.client.get("/api/v1/issues/search", params);
564
+ const meta = body.meta ?? {};
565
+ const degraded = meta["mode"] === "lexical" ? "semantic search unavailable \u2014 showing lexical matches only" : void 0;
566
+ const notice = [deprecation, degraded].filter(Boolean).join("; ") || void 0;
567
+ return { json: body.data, human: issuesTable(body.data), notice };
567
568
  }
568
569
  async function show(ctx, identifier, opts) {
569
570
  requireIdentifiers([identifier]);
package/dist/cli.js CHANGED
@@ -59,7 +59,7 @@ import {
59
59
  version,
60
60
  workflowStates,
61
61
  writeCredentials
62
- } from "./chunk-3WR6YFU6.js";
62
+ } from "./chunk-MF7MQ2L6.js";
63
63
 
64
64
  // src/cli.ts
65
65
  import { createInterface as createInterface2 } from "readline";
@@ -372,6 +372,16 @@ function defaultOpenBrowser(url) {
372
372
  }
373
373
  }
374
374
 
375
+ // src/core/stdio.ts
376
+ var BROKEN_PIPE_CODES = /* @__PURE__ */ new Set(["EPIPE", "ERR_STREAM_DESTROYED"]);
377
+ function guardBrokenPipes(streams = [process.stdout, process.stderr]) {
378
+ for (const stream of streams) {
379
+ stream.on("error", (error) => {
380
+ if (!BROKEN_PIPE_CODES.has(error.code ?? "")) throw error;
381
+ });
382
+ }
383
+ }
384
+
375
385
  // src/core/upgrade.ts
376
386
  import { spawn, spawnSync } from "child_process";
377
387
  import {
@@ -390,6 +400,7 @@ import {
390
400
  import { basename, dirname, join as join2, parse, sep } from "path";
391
401
  import { fileURLToPath } from "url";
392
402
  var PACKAGE_NAME = "@allocator-one/harness-cli";
403
+ var UPGRADE_INSTALL_SPEC = `${PACKAGE_NAME}@latest`;
393
404
  var UPDATE_CHECK_COMMAND = "__update-check";
394
405
  var REEXEC_SENTINEL = "HARNESS_UPGRADED";
395
406
  var DEFAULT_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
@@ -399,7 +410,11 @@ var NPM_ROOT_TIMEOUT_MS = 15e3;
399
410
  var INSTALL_TIMEOUT_MS = 12e4;
400
411
  var LOCK_STALE_MS = NPM_ROOT_TIMEOUT_MS + INSTALL_TIMEOUT_MS + 6e4;
401
412
  function detectInstall(from = fileURLToPath(import.meta.url)) {
402
- const unknown = { kind: "unknown", packageRoot: null, nodeModules: null };
413
+ const unknown = {
414
+ kind: "unknown",
415
+ packageRoot: null,
416
+ nodeModules: null
417
+ };
403
418
  if (from.split(sep).includes("_npx")) return { ...unknown, kind: "npx" };
404
419
  const packageRoot = findPackageRoot(from);
405
420
  if (packageRoot === null) return unknown;
@@ -432,7 +447,9 @@ function packageNameAt(path) {
432
447
  }
433
448
  function versionAt(packageRoot) {
434
449
  try {
435
- const parsed = JSON.parse(readFileSync(join2(packageRoot, "package.json"), "utf8"));
450
+ const parsed = JSON.parse(
451
+ readFileSync(join2(packageRoot, "package.json"), "utf8")
452
+ );
436
453
  if (typeof parsed !== "object" || parsed === null) return null;
437
454
  const version2 = parsed["version"];
438
455
  return typeof version2 === "string" ? version2 : null;
@@ -463,7 +480,8 @@ function isNewer(candidate, current) {
463
480
  const b = parseVersion(current);
464
481
  if (a === null || b === null) return false;
465
482
  for (let index = 0; index < 3; index++) {
466
- if (a.parts[index] !== b.parts[index]) return a.parts[index] > b.parts[index];
483
+ if (a.parts[index] !== b.parts[index])
484
+ return a.parts[index] > b.parts[index];
467
485
  }
468
486
  if (a.pre === b.pre) return false;
469
487
  if (a.pre === null) return true;
@@ -499,7 +517,8 @@ function readStamp(env = process.env) {
499
517
  if (typeof parsed !== "object" || parsed === null) return null;
500
518
  const record = parsed;
501
519
  const checkedAt = record["checkedAt"];
502
- if (typeof checkedAt !== "number" || !Number.isFinite(checkedAt)) return null;
520
+ if (typeof checkedAt !== "number" || !Number.isFinite(checkedAt))
521
+ return null;
503
522
  const latest = record["latest"];
504
523
  return { checkedAt, latest: typeof latest === "string" ? latest : null };
505
524
  } catch {
@@ -571,7 +590,8 @@ async function fetchLatestVersion(env) {
571
590
  const parsed = await response.json();
572
591
  if (typeof parsed !== "object" || parsed === null) return null;
573
592
  const version2 = parsed["version"];
574
- if (typeof version2 !== "string" || parseVersion(version2) === null) return null;
593
+ if (typeof version2 !== "string" || parseVersion(version2) === null)
594
+ return null;
575
595
  return version2;
576
596
  } catch {
577
597
  return null;
@@ -581,10 +601,18 @@ function selfHeal(options = {}) {
581
601
  const env = options.env ?? process.env;
582
602
  const argv2 = options.argv ?? process.argv;
583
603
  if (truthy(env[REEXEC_SENTINEL])) {
584
- return { kind: "skipped", reason: "already upgraded once for this command" };
604
+ return {
605
+ kind: "skipped",
606
+ reason: "already upgraded once for this command"
607
+ };
585
608
  }
586
- if (!autoUpgradeEnabled(env)) return { kind: "skipped", reason: "automatic upgrades are off" };
587
- if (argv2[2] === "mcp") return { kind: "skipped", reason: "an MCP session must not be replaced mid-stream" };
609
+ if (!autoUpgradeEnabled(env))
610
+ return { kind: "skipped", reason: "automatic upgrades are off" };
611
+ if (argv2[2] === "mcp")
612
+ return {
613
+ kind: "skipped",
614
+ reason: "an MCP session must not be replaced mid-stream"
615
+ };
588
616
  const install = options.install ?? detectInstall();
589
617
  if (install.kind !== "global" || install.packageRoot === null) {
590
618
  return { kind: "skipped", reason: describeInstall(install.kind) };
@@ -595,17 +623,27 @@ function selfHeal(options = {}) {
595
623
  return { kind: "upgraded", code: reexec(options, env, argv2) };
596
624
  }
597
625
  const lock = takeLock(env);
598
- if (lock === null) return { kind: "skipped", reason: "another harness process is already upgrading" };
626
+ if (lock === null)
627
+ return {
628
+ kind: "skipped",
629
+ reason: "another harness process is already upgrading"
630
+ };
599
631
  try {
600
632
  const stillHeld = () => readHolder(lock.path)?.pid === lock.pid;
601
633
  const result = (options.runInstall ?? npmInstallGlobal)(install, stillHeld);
602
634
  if (!result.ok) return { kind: "failed", reason: result.detail };
603
635
  const after = versionAt(packageRoot);
604
636
  if (after !== null && after === installed) {
605
- return { kind: "failed", reason: `still ${after} after upgrading \u2014 no newer release is published` };
637
+ return {
638
+ kind: "failed",
639
+ reason: `still ${after} after upgrading \u2014 no newer release is published`
640
+ };
606
641
  }
607
642
  if (after !== null && installed !== null && !isNewer(after, installed)) {
608
- return { kind: "failed", reason: `npm left ${after} installed, which is not newer than ${installed}` };
643
+ return {
644
+ kind: "failed",
645
+ reason: `npm left ${after} installed, which is not newer than ${installed}`
646
+ };
609
647
  }
610
648
  } finally {
611
649
  releaseLock(lock);
@@ -613,7 +651,10 @@ function selfHeal(options = {}) {
613
651
  return { kind: "upgraded", code: reexec(options, env, argv2) };
614
652
  }
615
653
  function reexec(options, env, argv2) {
616
- return (options.reexec ?? reexecSelf)({ ...env, [REEXEC_SENTINEL]: "1" }, argv2);
654
+ return (options.reexec ?? reexecSelf)(
655
+ { ...env, [REEXEC_SENTINEL]: "1" },
656
+ argv2
657
+ );
617
658
  }
618
659
  function describeInstall(kind) {
619
660
  switch (kind) {
@@ -629,9 +670,13 @@ function describeInstall(kind) {
629
670
  }
630
671
  function npmInstallGlobal(install, stillHeld) {
631
672
  const globalRoot = npmGlobalRoot();
632
- if (globalRoot === null) return { ok: false, detail: "npm did not say where its global root is" };
673
+ if (globalRoot === null)
674
+ return { ok: false, detail: "npm did not say where its global root is" };
633
675
  if (globalRoot.includes("***")) {
634
- return { ok: false, detail: "npm redacted its own global root, so this copy's location cannot be confirmed" };
676
+ return {
677
+ ok: false,
678
+ detail: "npm redacted its own global root, so this copy's location cannot be confirmed"
679
+ };
635
680
  }
636
681
  if (install.nodeModules === null || !samePath(globalRoot, install.nodeModules)) {
637
682
  return {
@@ -642,10 +687,16 @@ function npmInstallGlobal(install, stillHeld) {
642
687
  try {
643
688
  accessSync2(globalRoot, constants.W_OK);
644
689
  } catch {
645
- return { ok: false, detail: `${globalRoot} is not writable \u2014 upgrade it yourself` };
690
+ return {
691
+ ok: false,
692
+ detail: `${globalRoot} is not writable \u2014 upgrade it yourself`
693
+ };
646
694
  }
647
695
  if (!stillHeld()) {
648
- return { ok: false, detail: "lost the upgrade lock while preparing \u2014 another process took over" };
696
+ return {
697
+ ok: false,
698
+ detail: "lost the upgrade lock while preparing \u2014 another process took over"
699
+ };
649
700
  }
650
701
  process.stderr.write(`harness: upgrading ${PACKAGE_NAME} \u2026
651
702
  `);
@@ -657,7 +708,7 @@ function npmInstallGlobal(install, stillHeld) {
657
708
  "--no-fund",
658
709
  "--no-update-notifier",
659
710
  "--loglevel=error",
660
- `${PACKAGE_NAME}@latest`
711
+ UPGRADE_INSTALL_SPEC
661
712
  ];
662
713
  const result = spawnSync(npmCommand(), args, {
663
714
  // npm's diagnostics belong on stderr with ours; stdout stays the
@@ -666,9 +717,13 @@ function npmInstallGlobal(install, stillHeld) {
666
717
  timeout: INSTALL_TIMEOUT_MS,
667
718
  ...npmSpawnOptions()
668
719
  });
669
- if (result.error !== void 0) return { ok: false, detail: result.error.message };
720
+ if (result.error !== void 0)
721
+ return { ok: false, detail: result.error.message };
670
722
  if (result.status !== 0) {
671
- return { ok: false, detail: `npm install exited ${result.status ?? `on ${String(result.signal)}`}` };
723
+ return {
724
+ ok: false,
725
+ detail: `npm install exited ${result.status ?? `on ${String(result.signal)}`}`
726
+ };
672
727
  }
673
728
  return { ok: true, detail: "" };
674
729
  }
@@ -710,8 +765,10 @@ function reexecSelf(env, argv2) {
710
765
  env
711
766
  });
712
767
  if (result.error !== void 0) {
713
- process.stderr.write(`harness: upgraded, but re-running failed: ${result.error.message}
714
- `);
768
+ process.stderr.write(
769
+ `harness: upgraded, but re-running failed: ${result.error.message}
770
+ `
771
+ );
715
772
  return 1;
716
773
  }
717
774
  return result.status ?? 1;
@@ -794,7 +851,10 @@ function autoUpgradeEnabled(env) {
794
851
 
795
852
  // src/cli.ts
796
853
  function promptLine(question) {
797
- const readline = createInterface2({ input: process.stdin, output: process.stderr });
854
+ const readline = createInterface2({
855
+ input: process.stdin,
856
+ output: process.stderr
857
+ });
798
858
  return new Promise((resolve) => {
799
859
  readline.question(question, (answer) => {
800
860
  readline.close();
@@ -804,15 +864,19 @@ function promptLine(question) {
804
864
  }
805
865
  function exitWith(code) {
806
866
  const drain = (stream) => new Promise((resolve) => stream.write("", () => resolve()));
807
- return Promise.all([drain(process.stdout), drain(process.stderr)]).then(() => process.exit(code));
867
+ return Promise.all([drain(process.stdout), drain(process.stderr)]).then(
868
+ () => process.exit(code)
869
+ );
808
870
  }
871
+ guardBrokenPipes();
809
872
  var program = new Command();
810
873
  program.name("harness").description("issue tracking for humans and agents (harness.infra.one)").exitOverride().configureOutput({
811
874
  writeOut: (text) => process.stdout.write(text),
812
875
  writeErr: (text) => process.stderr.write(text)
813
876
  });
814
877
  function integer(value) {
815
- if (!/^\d+$/.test(value)) throw new InvalidArgumentError("expected a non-negative integer");
878
+ if (!/^\d+$/.test(value))
879
+ throw new InvalidArgumentError("expected a non-negative integer");
816
880
  return Number.parseInt(value, 10);
817
881
  }
818
882
  function collect(value, previous) {
@@ -858,8 +922,10 @@ async function withConfig(jsonFlag, mutating, run) {
858
922
  process.stderr.write(loginResult.human);
859
923
  const again = resolveConfig();
860
924
  if (again.kind !== "ok") {
861
- process.stderr.write(`harness: still not signed in to ${resolved.baseUrl} after logging in
862
- `);
925
+ process.stderr.write(
926
+ `harness: still not signed in to ${resolved.baseUrl} after logging in
927
+ `
928
+ );
863
929
  return exitWith(1);
864
930
  }
865
931
  return execute(again.config, jsonFlag, mutating, run);
@@ -891,16 +957,20 @@ async function fail(error, hostNotice) {
891
957
  return exitWith(1);
892
958
  }
893
959
  if (error instanceof TransportError) {
894
- process.stderr.write(hostNotice + `harness: cannot reach the API: ${clean(error.message)}
895
- `);
960
+ process.stderr.write(
961
+ hostNotice + `harness: cannot reach the API: ${clean(error.message)}
962
+ `
963
+ );
896
964
  return exitWith(3);
897
965
  }
898
966
  if (error instanceof UpgradeRequiredError) {
899
967
  const healed = selfHeal();
900
968
  if (healed.kind === "upgraded") return exitWith(healed.code);
901
969
  const why = healed.kind === "failed" ? ` (tried to upgrade: ${clean(healed.reason)})` : "";
902
- process.stderr.write(hostNotice + `harness: ${clean(error.message)} (HTTP 426)${why}
903
- `);
970
+ process.stderr.write(
971
+ hostNotice + `harness: ${clean(error.message)} (HTTP 426)${why}
972
+ `
973
+ );
904
974
  return exitWith(1);
905
975
  }
906
976
  if (error instanceof HttpError) {
@@ -919,7 +989,8 @@ async function fail(error, hostNotice) {
919
989
  }
920
990
  function errorDetails(body) {
921
991
  const details = body["details"];
922
- if (typeof details !== "object" || details === null || Array.isArray(details)) return "";
992
+ if (typeof details !== "object" || details === null || Array.isArray(details))
993
+ return "";
923
994
  return Object.entries(details).map(([field, messages]) => {
924
995
  const text = Array.isArray(messages) ? messages.map(String).join("; ") : String(messages);
925
996
  return `
@@ -927,24 +998,53 @@ function errorDetails(body) {
927
998
  }).join("");
928
999
  }
929
1000
  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) => {
930
- await withConfig(opts.json === true, opts.claim === true, (ctx) => ready(ctx, opts));
1001
+ await withConfig(
1002
+ opts.json === true,
1003
+ opts.claim === true,
1004
+ (ctx) => ready(ctx, opts)
1005
+ );
931
1006
  });
932
1007
  program.command("blocked").description("issues waiting on a blocker").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) => {
933
- await withConfig(opts.json === true, false, (ctx) => blocked(ctx, opts));
934
- });
935
- program.command("list").description("list issues with filters").option("--status <status>", "open | in_progress | closed").option("--assignee <who>", 'me, a user id, an email, or a full name; "" for unassigned').option("--priority <name>", "urgent | high | medium | low | none").option("--label <name>", "label filter").option("--type <type>", "task | bug | feature | epic").option("--search <query>", "full-text search").option("--team <key>", "team filter").option("--parent <id>", "children of an epic").option("--scope <scope>", "all | archived").option("--limit <n>", "page size", integer).option("--page <n>", "page number", integer).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
936
- await withConfig(opts.json === true, false, (ctx) => list(ctx, opts));
1008
+ await withConfig(
1009
+ opts.json === true,
1010
+ false,
1011
+ (ctx) => blocked(ctx, opts)
1012
+ );
937
1013
  });
938
- program.command("search").description("full-text search").argument("[query...]").option("--limit <n>", "page size", integer).option("--page <n>", "page number", integer).option("--all", "follow every page").option("--json", "emit JSON").action(async (query, opts) => {
939
- await withConfig(opts.json === true, false, (ctx) => search(ctx, query, opts));
940
- });
941
- program.command("show").description("full issue detail; downloads attached images locally").argument("[id]").option("--no-images", "skip attachment downloads").option("--json", "emit JSON").action(async (id, opts) => {
942
- await withConfig(opts.json === true, false, (ctx) => {
943
- if (id === void 0) throw new UsageError("usage: harness show <id>");
944
- requireIdentifiers2([id]);
945
- return show(ctx, id, opts);
946
- });
1014
+ program.command("list").description("list issues with filters").option("--status <status>", "open | in_progress | closed").option(
1015
+ "--assignee <who>",
1016
+ 'me, a user id, an email, or a full name; "" for unassigned'
1017
+ ).option("--priority <name>", "urgent | high | medium | low | none").option("--label <name>", "label filter").option("--type <type>", "task | bug | feature | epic").option("--search <query>", "full-text search").option("--team <key>", "team filter").option("--parent <id>", "children of an epic").option("--scope <scope>", "all | archived").option("--limit <n>", "page size", integer).option("--page <n>", "page number", integer).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
1018
+ await withConfig(
1019
+ opts.json === true,
1020
+ false,
1021
+ (ctx) => list(ctx, opts)
1022
+ );
947
1023
  });
1024
+ program.command("search").description(
1025
+ "search by meaning \u2014 ranked, exact identifier first; use list --search for paginated filtering"
1026
+ ).argument("[query...]").option("--limit <n>", "maximum results (default 10, capped at 20)", integer).option(
1027
+ "--page <n>",
1028
+ "(deprecated, ignored \u2014 results are ranked and capped)",
1029
+ integer
1030
+ ).option("--all", "(deprecated, ignored \u2014 results are ranked and capped)").option("--json", "emit JSON").action(
1031
+ async (query, opts) => {
1032
+ await withConfig(
1033
+ opts.json === true,
1034
+ false,
1035
+ (ctx) => search(ctx, query, opts)
1036
+ );
1037
+ }
1038
+ );
1039
+ program.command("show").description("full issue detail; downloads attached images locally").argument("[id]").option("--no-images", "skip attachment downloads").option("--json", "emit JSON").action(
1040
+ async (id, opts) => {
1041
+ await withConfig(opts.json === true, false, (ctx) => {
1042
+ if (id === void 0) throw new UsageError("usage: harness show <id>");
1043
+ requireIdentifiers2([id]);
1044
+ return show(ctx, id, opts);
1045
+ });
1046
+ }
1047
+ );
948
1048
  program.command("stats").description("per-team and total counts").option("--json", "emit JSON").action(async (opts) => {
949
1049
  await withConfig(opts.json === true, false, (ctx) => stats(ctx));
950
1050
  });
@@ -956,195 +1056,322 @@ program.command("version").description("print CLI and server API versions").opti
956
1056
  let config = null;
957
1057
  if (resolved.kind === "ok") config = resolved.config;
958
1058
  else if (resolved.kind === "needs-login") {
959
- config = { baseUrl: resolved.baseUrl, token: "", actor: null, team: null, cacheDir: "" };
1059
+ config = {
1060
+ baseUrl: resolved.baseUrl,
1061
+ token: "",
1062
+ actor: null,
1063
+ team: null,
1064
+ cacheDir: ""
1065
+ };
960
1066
  }
961
1067
  const output = await version(config);
962
- process.stdout.write(opts.json === true ? json(output.json) : output.human);
1068
+ process.stdout.write(
1069
+ opts.json === true ? json(output.json) : output.human
1070
+ );
963
1071
  await exitWith(0);
964
1072
  });
965
- program.command("create").description("create an issue").option("--title <text>", "issue title (required)").option("--description <text>", "problem statement").option("--design <text>", "design notes").option("--notes <text>", "working notes").option("--acceptance <text>", "acceptance criteria").option("--type <type>", "task | bug | feature | epic (applied as a type/ label)").option("--priority <name>", "urgent | high | medium | low | none").option("--due-date <date>", "YYYY-MM-DD").option("--parent <id>", "parent epic").option("--assignee <who>", "me, a user id, an email, or a full name").option("--team <key>", "team key (or HARNESS_TEAM)").option("--label <name>", "label to apply (repeatable)", collect, []).option("--json", "emit JSON").action(async (opts) => {
966
- await withConfig(opts.json === true, true, (ctx) => create(ctx, opts));
967
- });
968
- program.command("update").description("update an issue; --claim takes it atomically").argument("[id]").option("--claim", "claim atomically and move to the first started state").option("--state <name>", "workflow state by name or type").option("--title <text>").option("--description <text>").option("--design <text>").option("--notes <text>", "replace the notes").option("--append-notes <text>", "append to the notes").option("--acceptance <text>").option("--priority <name>", "urgent | high | medium | low | none").option("--due-date <date>", 'YYYY-MM-DD; "" clears').option("--parent <id>").option("--assignee <who>", 'me, a user id, an email, or a full name; "" unassigns').option("--json", "emit JSON").action(async (id, opts) => {
969
- await withConfig(opts.json === true, true, (ctx) => {
970
- if (id === void 0) {
971
- throw new UsageError("usage: harness update <id> --claim | --title \u2026 | --state <name> | \u2026");
972
- }
973
- requireIdentifiers2([id]);
974
- return update(ctx, id, opts);
975
- });
976
- });
977
- program.command("close").description("close issues; the response lists newly unblocked issues").argument("[ids...]").option("--reason <text>", "why it is done (e.g. the commit hash)").option("--cancel", "close as canceled instead of completed").option("--json", "emit JSON").action(async (ids, opts) => {
978
- await withConfig(opts.json === true, true, (ctx) => {
979
- if (ids.length === 0) {
980
- throw new UsageError("usage: harness close <id> [<id>\u2026] [--reason <text>] [--cancel]");
981
- }
982
- requireIdentifiers2(ids);
983
- return close(ctx, ids, opts);
984
- });
985
- });
986
- program.command("archive").description("archive issues; --restore brings them back").argument("[ids...]").option("--restore", "restore instead of archive").option("--json", "emit JSON").action(async (ids, opts) => {
987
- await withConfig(opts.json === true, true, (ctx) => {
988
- if (ids.length === 0) throw new UsageError("usage: harness archive <id> [<id>\u2026] [--restore]");
989
- requireIdentifiers2(ids);
990
- return archive(ctx, ids, opts);
991
- });
1073
+ program.command("create").description("create an issue").option("--title <text>", "issue title (required)").option("--description <text>", "problem statement").option("--design <text>", "design notes").option("--notes <text>", "working notes").option("--acceptance <text>", "acceptance criteria").option(
1074
+ "--type <type>",
1075
+ "task | bug | feature | epic (applied as a type/ label)"
1076
+ ).option("--priority <name>", "urgent | high | medium | low | none").option("--due-date <date>", "YYYY-MM-DD").option("--parent <id>", "parent epic").option("--assignee <who>", "me, a user id, an email, or a full name").option("--team <key>", "team key (or HARNESS_TEAM)").option("--label <name>", "label to apply (repeatable)", collect, []).option("--json", "emit JSON").action(async (opts) => {
1077
+ await withConfig(
1078
+ opts.json === true,
1079
+ true,
1080
+ (ctx) => create(ctx, opts)
1081
+ );
992
1082
  });
1083
+ program.command("update").description("update an issue; --claim takes it atomically").argument("[id]").option("--claim", "claim atomically and move to the first started state").option("--state <name>", "workflow state by name or type").option("--title <text>").option("--description <text>").option("--design <text>").option("--notes <text>", "replace the notes").option("--append-notes <text>", "append to the notes").option("--acceptance <text>").option("--priority <name>", "urgent | high | medium | low | none").option("--due-date <date>", 'YYYY-MM-DD; "" clears').option("--parent <id>").option(
1084
+ "--assignee <who>",
1085
+ 'me, a user id, an email, or a full name; "" unassigns'
1086
+ ).option("--json", "emit JSON").action(
1087
+ async (id, opts) => {
1088
+ await withConfig(opts.json === true, true, (ctx) => {
1089
+ if (id === void 0) {
1090
+ throw new UsageError(
1091
+ "usage: harness update <id> --claim | --title \u2026 | --state <name> | \u2026"
1092
+ );
1093
+ }
1094
+ requireIdentifiers2([id]);
1095
+ return update(ctx, id, opts);
1096
+ });
1097
+ }
1098
+ );
1099
+ program.command("close").description("close issues; the response lists newly unblocked issues").argument("[ids...]").option("--reason <text>", "why it is done (e.g. the commit hash)").option("--cancel", "close as canceled instead of completed").option("--json", "emit JSON").action(
1100
+ async (ids, opts) => {
1101
+ await withConfig(opts.json === true, true, (ctx) => {
1102
+ if (ids.length === 0) {
1103
+ throw new UsageError(
1104
+ "usage: harness close <id> [<id>\u2026] [--reason <text>] [--cancel]"
1105
+ );
1106
+ }
1107
+ requireIdentifiers2(ids);
1108
+ return close(ctx, ids, opts);
1109
+ });
1110
+ }
1111
+ );
1112
+ program.command("archive").description("archive issues; --restore brings them back").argument("[ids...]").option("--restore", "restore instead of archive").option("--json", "emit JSON").action(
1113
+ async (ids, opts) => {
1114
+ await withConfig(opts.json === true, true, (ctx) => {
1115
+ if (ids.length === 0)
1116
+ throw new UsageError(
1117
+ "usage: harness archive <id> [<id>\u2026] [--restore]"
1118
+ );
1119
+ requireIdentifiers2(ids);
1120
+ return archive(ctx, ids, opts);
1121
+ });
1122
+ }
1123
+ );
993
1124
  program.command("comments").description("an issue's full comment thread, untruncated").argument("[id]").option("--json", "emit JSON").action(async (id, opts) => {
994
1125
  await withConfig(opts.json === true, false, (ctx) => {
995
- if (id === void 0) throw new UsageError("usage: harness comments <id>");
1126
+ if (id === void 0)
1127
+ throw new UsageError("usage: harness comments <id>");
996
1128
  requireIdentifiers2([id]);
997
1129
  return comments(ctx, id);
998
1130
  });
999
1131
  });
1000
- program.command("dep").description("blocking dependencies: dep add|rm <issue> <blocker>").argument("[verb]").argument("[issue]").argument("[blocker]").option("--json", "emit JSON").action(async (verb, issue, blocker, opts) => {
1001
- await withConfig(opts.json === true, true, (ctx) => {
1002
- if (verb === void 0 || issue === void 0 || blocker === void 0) {
1003
- throw new UsageError("usage: harness dep add|rm <issue> <blocker>");
1004
- }
1005
- requireIdentifiers2([issue, blocker]);
1006
- return dep(ctx, verb, issue, blocker);
1007
- });
1008
- });
1009
- program.command("comment").description("comment on an issue").argument("[id]").argument("[text...]").option("--mention <name>", "notify a person (repeatable)", collect, []).option("--json", "emit JSON").action(async (id, text, opts) => {
1010
- await withConfig(opts.json === true, true, (ctx) => {
1011
- if (id === void 0 || text.length === 0) {
1012
- throw new UsageError("usage: harness comment <id> <text> [--mention NAME]\u2026");
1013
- }
1014
- requireIdentifiers2([id]);
1015
- const mentions = opts.mention.filter((name) => name.trim() !== "");
1016
- return comment(ctx, id, text, mentions);
1017
- });
1018
- });
1132
+ program.command("dep").description("blocking dependencies: dep add|rm <issue> <blocker>").argument("[verb]").argument("[issue]").argument("[blocker]").option("--json", "emit JSON").action(
1133
+ async (verb, issue, blocker, opts) => {
1134
+ await withConfig(opts.json === true, true, (ctx) => {
1135
+ if (verb === void 0 || issue === void 0 || blocker === void 0) {
1136
+ throw new UsageError("usage: harness dep add|rm <issue> <blocker>");
1137
+ }
1138
+ requireIdentifiers2([issue, blocker]);
1139
+ return dep(ctx, verb, issue, blocker);
1140
+ });
1141
+ }
1142
+ );
1143
+ program.command("comment").description("comment on an issue").argument("[id]").argument("[text...]").option("--mention <name>", "notify a person (repeatable)", collect, []).option("--json", "emit JSON").action(
1144
+ async (id, text, opts) => {
1145
+ await withConfig(opts.json === true, true, (ctx) => {
1146
+ if (id === void 0 || text.length === 0) {
1147
+ throw new UsageError(
1148
+ "usage: harness comment <id> <text> [--mention NAME]\u2026"
1149
+ );
1150
+ }
1151
+ requireIdentifiers2([id]);
1152
+ const mentions = opts.mention.filter((name) => name.trim() !== "");
1153
+ return comment(ctx, id, text, mentions);
1154
+ });
1155
+ }
1156
+ );
1019
1157
  program.command("attach-image").description("upload an image; prints markdown to reference it").argument("[path]").option("--json", "emit JSON").action(async (path, opts) => {
1020
1158
  await withConfig(opts.json === true, true, (ctx) => {
1021
1159
  if (path === void 0) {
1022
- throw new UsageError("usage: harness attach-image <path> (one png, jpg, webp or gif, up to 25 MB)");
1160
+ throw new UsageError(
1161
+ "usage: harness attach-image <path> (one png, jpg, webp or gif, up to 25 MB)"
1162
+ );
1023
1163
  }
1024
1164
  return attachImage(ctx, path);
1025
1165
  });
1026
1166
  });
1027
1167
  var project = program.command("project").description("projects");
1028
1168
  project.command("list").option("--search <query>", "full-text search").option("--state <state>", "project state 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) => {
1029
- await withConfig(opts.json === true, false, (ctx) => projectList(ctx, opts));
1169
+ await withConfig(
1170
+ opts.json === true,
1171
+ false,
1172
+ (ctx) => projectList(ctx, opts)
1173
+ );
1030
1174
  });
1031
1175
  project.command("show").argument("[id]").option("--json", "emit JSON").action(async (id, opts) => {
1032
1176
  await withConfig(opts.json === true, false, (ctx) => {
1033
- if (id === void 0) throw new UsageError("usage: harness project show <id>");
1177
+ if (id === void 0)
1178
+ throw new UsageError("usage: harness project show <id>");
1034
1179
  return projectShow(ctx, id);
1035
1180
  });
1036
1181
  });
1037
1182
  project.command("create").option("--name <name>", "project name (required)").option("--description <text>").option("--summary <text>").option("--state <state>").option("--lead <user-id>", "lead by user id").option("--start-date <date>", "YYYY-MM-DD").option("--target-date <date>", "YYYY-MM-DD").option("--json", "emit JSON").action(async (opts) => {
1038
- await withConfig(opts.json === true, true, (ctx) => projectCreate(ctx, opts));
1039
- });
1040
- project.command("update").argument("[id]").option("--name <name>").option("--description <text>").option("--summary <text>").option("--state <state>").option("--lead <user-id>", "lead by user id").option("--start-date <date>", 'YYYY-MM-DD; "" clears').option("--target-date <date>", 'YYYY-MM-DD; "" clears').option("--json", "emit JSON").action(async (id, opts) => {
1041
- await withConfig(opts.json === true, true, (ctx) => {
1042
- if (id === void 0) throw new UsageError("usage: harness project update <id> --name \u2026 | --state \u2026 | \u2026");
1043
- return projectUpdate(ctx, id, opts);
1044
- });
1183
+ await withConfig(
1184
+ opts.json === true,
1185
+ true,
1186
+ (ctx) => projectCreate(ctx, opts)
1187
+ );
1045
1188
  });
1189
+ project.command("update").argument("[id]").option("--name <name>").option("--description <text>").option("--summary <text>").option("--state <state>").option("--lead <user-id>", "lead by user id").option("--start-date <date>", 'YYYY-MM-DD; "" clears').option("--target-date <date>", 'YYYY-MM-DD; "" clears').option("--json", "emit JSON").action(
1190
+ async (id, opts) => {
1191
+ await withConfig(opts.json === true, true, (ctx) => {
1192
+ if (id === void 0)
1193
+ throw new UsageError(
1194
+ "usage: harness project update <id> --name \u2026 | --state \u2026 | \u2026"
1195
+ );
1196
+ return projectUpdate(ctx, id, opts);
1197
+ });
1198
+ }
1199
+ );
1046
1200
  var milestone = program.command("milestone").description("project milestones");
1047
1201
  milestone.command("list").option("--project <id>", "project id (required)").option("--json", "emit JSON").action(async (opts) => {
1048
- await withConfig(opts.json === true, false, (ctx) => milestoneList(ctx, opts.project));
1202
+ await withConfig(
1203
+ opts.json === true,
1204
+ false,
1205
+ (ctx) => milestoneList(ctx, opts.project)
1206
+ );
1049
1207
  });
1050
1208
  milestone.command("create").option("--project <id>", "project id (required)").option("--name <name>", "milestone name (required)").option("--description <text>").option("--target-date <date>", "YYYY-MM-DD").option("--json", "emit JSON").action(async (opts) => {
1051
- await withConfig(opts.json === true, true, (ctx) => milestoneCreate(ctx, opts));
1209
+ await withConfig(
1210
+ opts.json === true,
1211
+ true,
1212
+ (ctx) => milestoneCreate(ctx, opts)
1213
+ );
1052
1214
  });
1053
1215
  var doc = program.command("doc").description("documents");
1054
1216
  doc.command("list").option("--search <query>", "full-text search").option("--limit <n>", "page size", integer).option("--page <n>", "page number", integer).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
1055
- await withConfig(opts.json === true, false, (ctx) => docList(ctx, opts));
1217
+ await withConfig(
1218
+ opts.json === true,
1219
+ false,
1220
+ (ctx) => docList(ctx, opts)
1221
+ );
1056
1222
  });
1057
1223
  doc.command("show").argument("[id]").option("--json", "emit JSON").action(async (id, opts) => {
1058
1224
  await withConfig(opts.json === true, false, (ctx) => {
1059
- if (id === void 0) throw new UsageError("usage: harness doc show <id>");
1225
+ if (id === void 0)
1226
+ throw new UsageError("usage: harness doc show <id>");
1060
1227
  return docShow(ctx, id);
1061
1228
  });
1062
1229
  });
1063
1230
  doc.command("create").option("--title <title>", "document title (required)").option("--content <text>").option("--project <id>", "attach to a project").option("--json", "emit JSON").action(async (opts) => {
1064
- await withConfig(opts.json === true, true, (ctx) => docCreate(ctx, opts));
1065
- });
1066
- doc.command("update").argument("[id]").option("--title <title>").option("--content <text>").option("--project <id>").option("--json", "emit JSON").action(async (id, opts) => {
1067
- await withConfig(opts.json === true, true, (ctx) => {
1068
- if (id === void 0) throw new UsageError("usage: harness doc update <id> --title \u2026 | --content \u2026 | --project \u2026");
1069
- return docUpdate(ctx, id, opts);
1070
- });
1231
+ await withConfig(
1232
+ opts.json === true,
1233
+ true,
1234
+ (ctx) => docCreate(ctx, opts)
1235
+ );
1071
1236
  });
1237
+ doc.command("update").argument("[id]").option("--title <title>").option("--content <text>").option("--project <id>").option("--json", "emit JSON").action(
1238
+ async (id, opts) => {
1239
+ await withConfig(opts.json === true, true, (ctx) => {
1240
+ if (id === void 0)
1241
+ throw new UsageError(
1242
+ "usage: harness doc update <id> --title \u2026 | --content \u2026 | --project \u2026"
1243
+ );
1244
+ return docUpdate(ctx, id, opts);
1245
+ });
1246
+ }
1247
+ );
1072
1248
  var link = program.command("link").description("URLs attached to an issue (distinct from dep relations)");
1073
1249
  link.command("list").argument("[issue]").option("--json", "emit JSON").action(async (issue, opts) => {
1074
1250
  await withConfig(opts.json === true, false, (ctx) => {
1075
- if (issue === void 0) throw new UsageError("usage: harness link list <issue>");
1251
+ if (issue === void 0)
1252
+ throw new UsageError("usage: harness link list <issue>");
1076
1253
  requireIdentifiers2([issue]);
1077
1254
  return linkList(ctx, issue);
1078
1255
  });
1079
1256
  });
1080
- link.command("add").argument("[issue]").argument("[url]").option("--title <title>", "link title").option("--json", "emit JSON").action(async (issue, url, opts) => {
1081
- await withConfig(opts.json === true, true, (ctx) => {
1082
- if (issue === void 0 || url === void 0) {
1083
- throw new UsageError("usage: harness link add <issue> <url> [--title <title>]");
1084
- }
1085
- requireIdentifiers2([issue]);
1086
- return linkAdd(ctx, issue, url, opts.title);
1087
- });
1088
- });
1089
- link.command("rm").argument("[issue]").argument("[id-or-url]").option("--json", "emit JSON").action(async (issue, idOrUrl, opts) => {
1090
- await withConfig(opts.json === true, true, (ctx) => {
1091
- if (issue === void 0 || idOrUrl === void 0) {
1092
- throw new UsageError("usage: harness link rm <issue> <link-id-or-url>");
1093
- }
1094
- requireIdentifiers2([issue]);
1095
- return linkRm(ctx, issue, idOrUrl);
1096
- });
1097
- });
1257
+ link.command("add").argument("[issue]").argument("[url]").option("--title <title>", "link title").option("--json", "emit JSON").action(
1258
+ async (issue, url, opts) => {
1259
+ await withConfig(opts.json === true, true, (ctx) => {
1260
+ if (issue === void 0 || url === void 0) {
1261
+ throw new UsageError(
1262
+ "usage: harness link add <issue> <url> [--title <title>]"
1263
+ );
1264
+ }
1265
+ requireIdentifiers2([issue]);
1266
+ return linkAdd(ctx, issue, url, opts.title);
1267
+ });
1268
+ }
1269
+ );
1270
+ link.command("rm").argument("[issue]").argument("[id-or-url]").option("--json", "emit JSON").action(
1271
+ async (issue, idOrUrl, opts) => {
1272
+ await withConfig(opts.json === true, true, (ctx) => {
1273
+ if (issue === void 0 || idOrUrl === void 0) {
1274
+ throw new UsageError(
1275
+ "usage: harness link rm <issue> <link-id-or-url>"
1276
+ );
1277
+ }
1278
+ requireIdentifiers2([issue]);
1279
+ return linkRm(ctx, issue, idOrUrl);
1280
+ });
1281
+ }
1282
+ );
1098
1283
  var label = program.command("label").description("labels");
1099
1284
  label.command("list").option("--json", "emit JSON").action(async (opts) => {
1100
- await withConfig(opts.json === true, false, (ctx) => labelList(ctx));
1101
- });
1102
- label.command("create").argument("[name]").option("--color <color>", "label color").option("--json", "emit JSON").action(async (name, opts) => {
1103
- await withConfig(opts.json === true, true, (ctx) => labelCreate(ctx, name, opts.color));
1285
+ await withConfig(
1286
+ opts.json === true,
1287
+ false,
1288
+ (ctx) => labelList(ctx)
1289
+ );
1104
1290
  });
1291
+ label.command("create").argument("[name]").option("--color <color>", "label color").option("--json", "emit JSON").action(
1292
+ async (name, opts) => {
1293
+ await withConfig(
1294
+ opts.json === true,
1295
+ true,
1296
+ (ctx) => labelCreate(ctx, name, opts.color)
1297
+ );
1298
+ }
1299
+ );
1105
1300
  var team = program.command("team").description("teams");
1106
1301
  team.command("list").option("--json", "emit JSON").action(async (opts) => {
1107
- await withConfig(opts.json === true, false, (ctx) => teamList(ctx));
1302
+ await withConfig(
1303
+ opts.json === true,
1304
+ false,
1305
+ (ctx) => teamList(ctx)
1306
+ );
1108
1307
  });
1109
- team.command("create").description("create a team, seeded with the default workflow states").option("--key <key>", "identifier prefix for the team's issues, e.g. IOS (required)").option("--name <name>", "display name (required)").option("--json", "emit JSON").action(async (opts) => {
1110
- await withConfig(opts.json === true, true, (ctx) => teamCreate(ctx, opts));
1308
+ team.command("create").description("create a team, seeded with the default workflow states").option(
1309
+ "--key <key>",
1310
+ "identifier prefix for the team's issues, e.g. IOS (required)"
1311
+ ).option("--name <name>", "display name (required)").option("--json", "emit JSON").action(async (opts) => {
1312
+ await withConfig(
1313
+ opts.json === true,
1314
+ true,
1315
+ (ctx) => teamCreate(ctx, opts)
1316
+ );
1111
1317
  });
1112
1318
  program.command("states").description("a team's workflow states").option("--team <key>", "team key (or HARNESS_TEAM)").option("--json", "emit JSON").action(async (opts) => {
1113
- await withConfig(opts.json === true, false, (ctx) => workflowStates(ctx, opts.team));
1319
+ await withConfig(
1320
+ opts.json === true,
1321
+ false,
1322
+ (ctx) => workflowStates(ctx, opts.team)
1323
+ );
1114
1324
  });
1115
1325
  var user = program.command("user").description("users");
1116
1326
  user.command("list").option("--json", "emit JSON").action(async (opts) => {
1117
- await withConfig(opts.json === true, false, (ctx) => userList(ctx));
1327
+ await withConfig(
1328
+ opts.json === true,
1329
+ false,
1330
+ (ctx) => userList(ctx)
1331
+ );
1118
1332
  });
1119
1333
  program.command("upload").description("upload a file; prints markdown to reference it").argument("[path]").option("--json", "emit JSON").action(async (path, opts) => {
1120
1334
  await withConfig(opts.json === true, true, (ctx) => {
1121
- if (path === void 0) throw new UsageError("usage: harness upload <path> (png/jpg/webp/gif, up to 25 MB)");
1335
+ if (path === void 0)
1336
+ throw new UsageError(
1337
+ "usage: harness upload <path> (png/jpg/webp/gif, up to 25 MB)"
1338
+ );
1122
1339
  return upload(ctx, path);
1123
1340
  });
1124
1341
  });
1125
1342
  program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
1126
- const { startMcpServer } = await import("./server-TK2KFAOQ.js");
1343
+ const { startMcpServer } = await import("./server-LJXJLWNK.js");
1127
1344
  await startMcpServer();
1128
1345
  });
1129
- program.command("login").description("sign in via the browser (infra.one session)").option("--url <url>", "host to sign in to").option("--no-browser", "print the URL and prompt for a pasted token instead").option("--timeout <ms>", "how long to wait for the browser callback", integer).option("--json", "emit JSON").action(async (opts) => {
1130
- const repoLookup = findRepoConfig();
1131
- for (const warning of repoLookup.warnings) process.stderr.write(warning + "\n");
1132
- const repoUrl = repoLookup.config?.url;
1133
- const url = opts.url ?? blankToUndefined(process.env["HARNESS_API_URL"]) ?? repoUrl ?? storedLoginUrl() ?? DEFAULT_BASE_URL;
1134
- if (url === repoUrl && opts.url === void 0 && blankToUndefined(process.env["HARNESS_API_URL"]) === void 0) {
1135
- process.stderr.write(`harness: host ${url} comes from ${repoLookup.config?.path}
1136
- `);
1346
+ program.command("login").description("sign in via the browser (infra.one session)").option("--url <url>", "host to sign in to").option("--no-browser", "print the URL and prompt for a pasted token instead").option(
1347
+ "--timeout <ms>",
1348
+ "how long to wait for the browser callback",
1349
+ integer
1350
+ ).option("--json", "emit JSON").action(
1351
+ async (opts) => {
1352
+ const repoLookup = findRepoConfig();
1353
+ for (const warning of repoLookup.warnings)
1354
+ process.stderr.write(warning + "\n");
1355
+ const repoUrl = repoLookup.config?.url;
1356
+ const url = opts.url ?? blankToUndefined(process.env["HARNESS_API_URL"]) ?? repoUrl ?? storedLoginUrl() ?? DEFAULT_BASE_URL;
1357
+ if (url === repoUrl && opts.url === void 0 && blankToUndefined(process.env["HARNESS_API_URL"]) === void 0) {
1358
+ process.stderr.write(
1359
+ `harness: host ${url} comes from ${repoLookup.config?.path}
1360
+ `
1361
+ );
1362
+ }
1363
+ const options = { noBrowser: !opts.browser };
1364
+ if (opts.timeout !== void 0) options.timeoutMs = opts.timeout;
1365
+ await finishAuth(await login(url, options), opts.json === true);
1137
1366
  }
1138
- const options = { noBrowser: !opts.browser };
1139
- if (opts.timeout !== void 0) options.timeoutMs = opts.timeout;
1140
- await finishAuth(await login(url, options), opts.json === true);
1141
- });
1367
+ );
1142
1368
  program.command("logout").description("revoke and forget the stored login").option("--json", "emit JSON").action(async (opts) => {
1143
1369
  await finishAuth(await logout(), opts.json === true);
1144
1370
  });
1145
1371
  program.command("whoami").description("show the resolved host and credential source").option("--json", "emit JSON").action(async (opts) => {
1146
1372
  const resolved = resolveConfig();
1147
- for (const warning of resolved.warnings) process.stderr.write(warning + "\n");
1373
+ for (const warning of resolved.warnings)
1374
+ process.stderr.write(warning + "\n");
1148
1375
  if (resolved.kind === "error") {
1149
1376
  process.stderr.write(`harness: ${resolved.message}
1150
1377
  `);
@@ -30,6 +30,7 @@ import {
30
30
  projectUpdate,
31
31
  ready,
32
32
  resolveConfig,
33
+ search,
33
34
  show,
34
35
  stats,
35
36
  teamCreate,
@@ -38,7 +39,7 @@ import {
38
39
  upload,
39
40
  userList,
40
41
  workflowStates
41
- } from "./chunk-3WR6YFU6.js";
42
+ } from "./chunk-MF7MQ2L6.js";
42
43
 
43
44
  // src/mcp/server.ts
44
45
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -116,6 +117,19 @@ var TOOLS = [
116
117
  return list(ctx, args);
117
118
  }
118
119
  },
120
+ {
121
+ name: "issue_search",
122
+ description: "Search issues by meaning: ranked results across a lexical and a semantic signal, so describing a problem in your own words finds issues that share none of them. Pass the question as asked \u2014 plain language works better than keywords. An exact identifier always ranks first. Returns at most 20 results, no pagination; use issue_list with search for paginated lexical filtering.",
123
+ readOnly: true,
124
+ shape: {
125
+ query: z.string().describe("the question or phrase to search for"),
126
+ limit: z.number().int().optional().describe("maximum results (default 10, capped at 20)")
127
+ },
128
+ handler: (ctx, args) => {
129
+ const limit = args["limit"] === void 0 ? {} : { limit: args["limit"] };
130
+ return search(ctx, [String(args["query"])], limit);
131
+ }
132
+ },
119
133
  {
120
134
  name: "issue_show",
121
135
  description: "Full issue detail: fields, relations, children, comments. images=true downloads attached images to the local cache and includes their paths as local_images.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allocator-one/harness-cli",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "CLI and MCP server for Harness, allocator.one's issue tracker",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/spec/openapi.json CHANGED
@@ -517,6 +517,55 @@
517
517
  "tags": []
518
518
  }
519
519
  },
520
+ "/api/v1/issues/search": {
521
+ "get": {
522
+ "callbacks": {},
523
+ "deprecated": false,
524
+ "description": "Requires the `harness:read` scope. Ranked hybrid search: an exact identifier always comes first, then results ordered by relevance across a lexical and a semantic (embedding) signal — a query can match issues that share none of its words. Returns at most 20 results; there is no pagination. `meta.mode` is `hybrid` when the semantic side ran and `lexical` when it degraded (embeddings disabled or the provider unavailable). Unarchived issues only. For paginated, filterable listings use `GET /api/v1/issues` instead.",
525
+ "parameters": [
526
+ {
527
+ "description": "The question or phrase to search for — plain language works better than keywords",
528
+ "in": "query",
529
+ "name": "q",
530
+ "required": false,
531
+ "schema": {
532
+ "type": "string"
533
+ }
534
+ },
535
+ {
536
+ "description": "Maximum results (default 10, capped at 20)",
537
+ "in": "query",
538
+ "name": "limit",
539
+ "required": false,
540
+ "schema": {
541
+ "type": "integer"
542
+ }
543
+ }
544
+ ],
545
+ "responses": {
546
+ "200": {
547
+ "description": "Success"
548
+ },
549
+ "400": {
550
+ "description": "Bad request"
551
+ },
552
+ "401": {
553
+ "description": "Missing or invalid token"
554
+ },
555
+ "403": {
556
+ "description": "Token not permitted (org-scoped, non-admin, or missing scope)"
557
+ },
558
+ "404": {
559
+ "description": "Not found"
560
+ },
561
+ "422": {
562
+ "description": "Validation error"
563
+ }
564
+ },
565
+ "summary": "Search issues by meaning",
566
+ "tags": []
567
+ }
568
+ },
520
569
  "/api/v1/issues/{identifier}": {
521
570
  "get": {
522
571
  "callbacks": {},
@@ -524,7 +573,7 @@
524
573
  "description": "Requires the `harness:read` scope. The payload always carries `blocked` and `blocked_by` (identifiers of open blockers); `include` embeds relations (direction-aware), children, and recent comments in one response.",
525
574
  "parameters": [
526
575
  {
527
- "description": "Issue identifier, e.g. IO-10001 (minted) or ALL-4223 (imported)",
576
+ "description": "Issue identifier, e.g. ALL-4901 or CLI-12 (issues minted before team-prefixed numbering carry IO-)",
528
577
  "in": "path",
529
578
  "name": "identifier",
530
579
  "required": true,
@@ -571,7 +620,7 @@
571
620
  "description": "Requires the `harness:write` scope.",
572
621
  "parameters": [
573
622
  {
574
- "description": "Issue identifier, e.g. IO-10001 (minted) or ALL-4223 (imported)",
623
+ "description": "Issue identifier, e.g. ALL-4901 or CLI-12 (issues minted before team-prefixed numbering carry IO-)",
575
624
  "in": "path",
576
625
  "name": "identifier",
577
626
  "required": true,
@@ -656,7 +705,7 @@
656
705
  "description": "Requires the `harness:write` scope.",
657
706
  "parameters": [
658
707
  {
659
- "description": "Issue identifier, e.g. IO-10001 (minted) or ALL-4223 (imported)",
708
+ "description": "Issue identifier, e.g. ALL-4901 or CLI-12 (issues minted before team-prefixed numbering carry IO-)",
660
709
  "in": "path",
661
710
  "name": "identifier",
662
711
  "required": true,
@@ -711,7 +760,7 @@
711
760
  "description": "Requires the `harness:write` scope. Atomic compare-and-swap: assigns the caller and moves the issue to the team's first started-type state. Idempotent for the current claimant.",
712
761
  "parameters": [
713
762
  {
714
- "description": "Issue identifier, e.g. IO-10001 (minted) or ALL-4223 (imported)",
763
+ "description": "Issue identifier, e.g. ALL-4901 or CLI-12 (issues minted before team-prefixed numbering carry IO-)",
715
764
  "in": "path",
716
765
  "name": "identifier",
717
766
  "required": true,
@@ -754,7 +803,7 @@
754
803
  "description": "Requires the `harness:write` scope. Moves the issue to the team's first completed-type state (canceled-type with `cancel`), posting `reason` as a comment first. The response's `unblocked` lists identifiers of issues that became ready because this one closed.",
755
804
  "parameters": [
756
805
  {
757
- "description": "Issue identifier, e.g. IO-10001 (minted) or ALL-4223 (imported)",
806
+ "description": "Issue identifier, e.g. ALL-4901 or CLI-12 (issues minted before team-prefixed numbering carry IO-)",
758
807
  "in": "path",
759
808
  "name": "identifier",
760
809
  "required": true,
@@ -818,7 +867,7 @@
818
867
  "description": "Requires the `harness:read` scope.",
819
868
  "parameters": [
820
869
  {
821
- "description": "Issue identifier, e.g. IO-10001 (minted) or ALL-4223 (imported)",
870
+ "description": "Issue identifier, e.g. ALL-4901 or CLI-12 (issues minted before team-prefixed numbering carry IO-)",
822
871
  "in": "path",
823
872
  "name": "identifier",
824
873
  "required": true,
@@ -856,7 +905,7 @@
856
905
  "description": "Requires the `harness:write` scope.",
857
906
  "parameters": [
858
907
  {
859
- "description": "Issue identifier, e.g. IO-10001 (minted) or ALL-4223 (imported)",
908
+ "description": "Issue identifier, e.g. ALL-4901 or CLI-12 (issues minted before team-prefixed numbering carry IO-)",
860
909
  "in": "path",
861
910
  "name": "identifier",
862
911
  "required": true,
@@ -915,7 +964,7 @@
915
964
  "description": "Requires the `harness:write` scope.",
916
965
  "parameters": [
917
966
  {
918
- "description": "Issue identifier, e.g. IO-10001 (minted) or ALL-4223 (imported)",
967
+ "description": "Issue identifier, e.g. ALL-4901 or CLI-12 (issues minted before team-prefixed numbering carry IO-)",
919
968
  "in": "path",
920
969
  "name": "identifier",
921
970
  "required": true,
@@ -973,7 +1022,7 @@
973
1022
  "description": "Requires the `harness:read` scope.",
974
1023
  "parameters": [
975
1024
  {
976
- "description": "Issue identifier, e.g. IO-10001 (minted) or ALL-4223 (imported)",
1025
+ "description": "Issue identifier, e.g. ALL-4901 or CLI-12 (issues minted before team-prefixed numbering carry IO-)",
977
1026
  "in": "path",
978
1027
  "name": "identifier",
979
1028
  "required": true,
@@ -1011,7 +1060,7 @@
1011
1060
  "description": "Requires the `harness:write` scope.",
1012
1061
  "parameters": [
1013
1062
  {
1014
- "description": "Issue identifier, e.g. IO-10001 (minted) or ALL-4223 (imported)",
1063
+ "description": "Issue identifier, e.g. ALL-4901 or CLI-12 (issues minted before team-prefixed numbering carry IO-)",
1015
1064
  "in": "path",
1016
1065
  "name": "identifier",
1017
1066
  "required": true,
@@ -1070,7 +1119,7 @@
1070
1119
  "description": "Requires the `harness:write` scope.",
1071
1120
  "parameters": [
1072
1121
  {
1073
- "description": "Issue identifier, e.g. IO-10001 (minted) or ALL-4223 (imported)",
1122
+ "description": "Issue identifier, e.g. ALL-4901 or CLI-12 (issues minted before team-prefixed numbering carry IO-)",
1074
1123
  "in": "path",
1075
1124
  "name": "identifier",
1076
1125
  "required": true,
@@ -1119,7 +1168,7 @@
1119
1168
  "description": "Requires the `harness:read` scope.",
1120
1169
  "parameters": [
1121
1170
  {
1122
- "description": "Issue identifier, e.g. IO-10001 (minted) or ALL-4223 (imported)",
1171
+ "description": "Issue identifier, e.g. ALL-4901 or CLI-12 (issues minted before team-prefixed numbering carry IO-)",
1123
1172
  "in": "path",
1124
1173
  "name": "identifier",
1125
1174
  "required": true,
@@ -1157,7 +1206,7 @@
1157
1206
  "description": "Requires the `harness:write` scope.",
1158
1207
  "parameters": [
1159
1208
  {
1160
- "description": "Issue identifier, e.g. IO-10001 (minted) or ALL-4223 (imported)",
1209
+ "description": "Issue identifier, e.g. ALL-4901 or CLI-12 (issues minted before team-prefixed numbering carry IO-)",
1161
1210
  "in": "path",
1162
1211
  "name": "identifier",
1163
1212
  "required": true,
@@ -1225,7 +1274,7 @@
1225
1274
  "description": "Requires the `harness:write` scope.",
1226
1275
  "parameters": [
1227
1276
  {
1228
- "description": "Issue identifier, e.g. IO-10001 (minted) or ALL-4223 (imported)",
1277
+ "description": "Issue identifier, e.g. ALL-4901 or CLI-12 (issues minted before team-prefixed numbering carry IO-)",
1229
1278
  "in": "path",
1230
1279
  "name": "identifier",
1231
1280
  "required": true,
@@ -1274,7 +1323,7 @@
1274
1323
  "description": "Requires the `harness:write` scope.",
1275
1324
  "parameters": [
1276
1325
  {
1277
- "description": "Issue identifier, e.g. IO-10001 (minted) or ALL-4223 (imported)",
1326
+ "description": "Issue identifier, e.g. ALL-4901 or CLI-12 (issues minted before team-prefixed numbering carry IO-)",
1278
1327
  "in": "path",
1279
1328
  "name": "identifier",
1280
1329
  "required": true,
@@ -1870,7 +1919,7 @@
1870
1919
  "post": {
1871
1920
  "callbacks": {},
1872
1921
  "deprecated": false,
1873
- "description": "Requires the `harness:write` scope. Creates a team seeded with the default workflow states (Backlog, Todo, In Progress, Done, Canceled), so it is immediately usable for the full issue lifecycle. The key is upcased, must match `^[A-Z][A-Z0-9]*$`, and `IO` is reserved for the issues Harness mints.",
1922
+ "description": "Requires the `harness:write` scope. Creates a team seeded with the default workflow states (Backlog, Todo, In Progress, Done, Canceled), so it is immediately usable for the full issue lifecycle. The key is upcased and must match `^[A-Z][A-Z0-9]*$`; numbering starts above any issues that already carry the key as their prefix.",
1874
1923
  "parameters": [],
1875
1924
  "requestBody": {
1876
1925
  "content": {
@@ -1878,7 +1927,7 @@
1878
1927
  "schema": {
1879
1928
  "properties": {
1880
1929
  "key": {
1881
- "description": "Identifier prefix for issues imported under the team (e.g. ALL). Upcased; create-only.",
1930
+ "description": "Identifier prefix for the team's issues (e.g. ALL in ALL-4901). Upcased; create-only.",
1882
1931
  "type": "string"
1883
1932
  },
1884
1933
  "name": {
@@ -2143,4 +2192,4 @@
2143
2192
  ],
2144
2193
  "servers": [],
2145
2194
  "tags": []
2146
- }
2195
+ }