@allocator-one/harness-cli 1.12.1 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +584 -199
- package/dist/{chunk-VOH6BBJD.js → chunk-F2YVTB2A.js} +736 -84
- package/dist/cli.js +90 -135
- package/dist/{server-FNNMISOZ.js → server-75QJASTY.js} +142 -10
- package/package.json +2 -1
- package/spec/openapi.json +3965 -14
package/dist/cli.js
CHANGED
|
@@ -30,7 +30,9 @@ import {
|
|
|
30
30
|
docShow,
|
|
31
31
|
docUpdate,
|
|
32
32
|
editComment,
|
|
33
|
+
errorDetails,
|
|
33
34
|
findRepoConfig,
|
|
35
|
+
gateSet,
|
|
34
36
|
isHostTrusted,
|
|
35
37
|
json,
|
|
36
38
|
labelCreate,
|
|
@@ -48,21 +50,32 @@ import {
|
|
|
48
50
|
memberSetRole,
|
|
49
51
|
milestoneCreate,
|
|
50
52
|
milestoneList,
|
|
53
|
+
planCreate,
|
|
54
|
+
planEdit,
|
|
55
|
+
planHold,
|
|
56
|
+
planRelease,
|
|
57
|
+
planShow,
|
|
51
58
|
prime,
|
|
52
59
|
projectCreate,
|
|
53
60
|
projectList,
|
|
54
61
|
projectShow,
|
|
55
62
|
projectUpdate,
|
|
56
63
|
readCredentials,
|
|
64
|
+
readPlanFile,
|
|
57
65
|
ready,
|
|
58
66
|
relationAdd,
|
|
59
67
|
relationList,
|
|
60
68
|
relationRm,
|
|
61
69
|
relationUpdate,
|
|
70
|
+
requireCloseArgs,
|
|
62
71
|
requireCommentId,
|
|
63
72
|
requireIdentifiers,
|
|
73
|
+
requirePlanId,
|
|
74
|
+
requireReadyClaimArgs,
|
|
75
|
+
requiredTeam,
|
|
64
76
|
resolveCacheHome,
|
|
65
77
|
resolveConfig,
|
|
78
|
+
resolveTextSources,
|
|
66
79
|
search,
|
|
67
80
|
show,
|
|
68
81
|
stateAdd,
|
|
@@ -79,10 +92,11 @@ import {
|
|
|
79
92
|
upload,
|
|
80
93
|
userList,
|
|
81
94
|
validateBaseUrl,
|
|
95
|
+
validateIdempotencyKey,
|
|
82
96
|
version,
|
|
83
97
|
workflowStates,
|
|
84
98
|
writeCredentials
|
|
85
|
-
} from "./chunk-
|
|
99
|
+
} from "./chunk-F2YVTB2A.js";
|
|
86
100
|
|
|
87
101
|
// src/cli.ts
|
|
88
102
|
import { createInterface as createInterface2 } from "readline";
|
|
@@ -808,113 +822,6 @@ function defaultOpenBrowser(url) {
|
|
|
808
822
|
}
|
|
809
823
|
}
|
|
810
824
|
|
|
811
|
-
// src/core/textinput.ts
|
|
812
|
-
import { readFileSync, statSync } from "fs";
|
|
813
|
-
import { isatty } from "tty";
|
|
814
|
-
var STDIN_PATH = "-";
|
|
815
|
-
var MAX_TEXT_BYTES = 1e6;
|
|
816
|
-
function flagOf(name) {
|
|
817
|
-
return name.replace(/[A-Z]/g, (upper) => `-${upper.toLowerCase()}`);
|
|
818
|
-
}
|
|
819
|
-
function resolveTextSources(sources) {
|
|
820
|
-
const resolved = {};
|
|
821
|
-
const toRead = [];
|
|
822
|
-
let stdinClaimedBy;
|
|
823
|
-
for (const { name, inline, file } of sources) {
|
|
824
|
-
if (inline !== void 0 && file !== void 0) {
|
|
825
|
-
throw new UsageError(
|
|
826
|
-
`--${flagOf(name)} and --${flagOf(name)}-file both given \u2014 pass one; there is no precedence between them`
|
|
827
|
-
);
|
|
828
|
-
}
|
|
829
|
-
if (file === void 0) {
|
|
830
|
-
if (inline !== void 0) resolved[name] = inline;
|
|
831
|
-
continue;
|
|
832
|
-
}
|
|
833
|
-
if (file === STDIN_PATH) {
|
|
834
|
-
if (isatty(0)) {
|
|
835
|
-
throw new UsageError(
|
|
836
|
-
`--${flagOf(name)}-file - reads stdin, but stdin is a terminal \u2014 pipe something in or pass a path`
|
|
837
|
-
);
|
|
838
|
-
}
|
|
839
|
-
if (stdinClaimedBy !== void 0) {
|
|
840
|
-
throw new UsageError(
|
|
841
|
-
`--${flagOf(name)}-file and --${flagOf(stdinClaimedBy)}-file both read stdin \u2014 there is only one stream, so only one may be "-"`
|
|
842
|
-
);
|
|
843
|
-
}
|
|
844
|
-
stdinClaimedBy = name;
|
|
845
|
-
}
|
|
846
|
-
if (file !== STDIN_PATH) checkReadable(file, name);
|
|
847
|
-
toRead.push({ name, file });
|
|
848
|
-
}
|
|
849
|
-
for (const { name, file } of [...toRead].sort(byStdinLast)) {
|
|
850
|
-
resolved[name] = stripOneTrailingNewline(readTextFile(file, name));
|
|
851
|
-
}
|
|
852
|
-
return resolved;
|
|
853
|
-
}
|
|
854
|
-
function readTextFile(path, name) {
|
|
855
|
-
if (path === STDIN_PATH) {
|
|
856
|
-
let piped;
|
|
857
|
-
try {
|
|
858
|
-
piped = readFileSync(0);
|
|
859
|
-
} catch (error) {
|
|
860
|
-
throw new UsageError(`--${flagOf(name)}-file -: cannot read stdin (${describe(error)})`);
|
|
861
|
-
}
|
|
862
|
-
if (piped.length > MAX_TEXT_BYTES) {
|
|
863
|
-
throw new UsageError(oversize(`--${flagOf(name)}-file -`));
|
|
864
|
-
}
|
|
865
|
-
return decodeUtf8(piped, `--${flagOf(name)}-file -`);
|
|
866
|
-
}
|
|
867
|
-
const flag = `--${flagOf(name)}-file ${path}`;
|
|
868
|
-
let bytes;
|
|
869
|
-
try {
|
|
870
|
-
bytes = readFileSync(path);
|
|
871
|
-
} catch (error) {
|
|
872
|
-
throw new UsageError(`${flag}: ${describe(error)}`);
|
|
873
|
-
}
|
|
874
|
-
if (bytes.length > MAX_TEXT_BYTES) throw new UsageError(oversize(flag));
|
|
875
|
-
return decodeUtf8(bytes, flag);
|
|
876
|
-
}
|
|
877
|
-
function decodeUtf8(bytes, flag) {
|
|
878
|
-
try {
|
|
879
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
880
|
-
} catch {
|
|
881
|
-
throw new UsageError(`${flag}: not valid UTF-8 \u2014 convert it first`);
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
function oversize(what) {
|
|
885
|
-
return `${what}: over ${MAX_TEXT_BYTES / 1e6} MB \u2014 that is not prose; check the path`;
|
|
886
|
-
}
|
|
887
|
-
function checkReadable(path, name) {
|
|
888
|
-
const flag = `--${flagOf(name)}-file ${path}`;
|
|
889
|
-
let stats2;
|
|
890
|
-
try {
|
|
891
|
-
stats2 = statSync(path);
|
|
892
|
-
} catch (error) {
|
|
893
|
-
throw new UsageError(`${flag}: ${describe(error)}`);
|
|
894
|
-
}
|
|
895
|
-
if (!stats2.isFile()) {
|
|
896
|
-
throw new UsageError(
|
|
897
|
-
`${flag}: ${stats2.isDirectory() ? "is a directory" : "not a regular file"}`
|
|
898
|
-
);
|
|
899
|
-
}
|
|
900
|
-
if (stats2.size > MAX_TEXT_BYTES) throw new UsageError(oversize(flag));
|
|
901
|
-
}
|
|
902
|
-
function byStdinLast(a, b) {
|
|
903
|
-
return Number(a.file === STDIN_PATH) - Number(b.file === STDIN_PATH);
|
|
904
|
-
}
|
|
905
|
-
function stripOneTrailingNewline(text) {
|
|
906
|
-
if (text.endsWith("\r\n")) return text.slice(0, -2);
|
|
907
|
-
if (text.endsWith("\n")) return text.slice(0, -1);
|
|
908
|
-
return text;
|
|
909
|
-
}
|
|
910
|
-
function describe(error) {
|
|
911
|
-
const code = error?.code;
|
|
912
|
-
if (code === "ENOENT") return "no such file";
|
|
913
|
-
if (code === "EISDIR") return "is a directory";
|
|
914
|
-
if (code === "EACCES") return "permission denied";
|
|
915
|
-
return error instanceof Error ? error.message : String(error);
|
|
916
|
-
}
|
|
917
|
-
|
|
918
825
|
// src/core/stdio.ts
|
|
919
826
|
var BROKEN_PIPE_CODES = /* @__PURE__ */ new Set(["EPIPE", "ERR_STREAM_DESTROYED"]);
|
|
920
827
|
function guardBrokenPipes(streams = [process.stdout, process.stderr]) {
|
|
@@ -933,11 +840,11 @@ import {
|
|
|
933
840
|
existsSync,
|
|
934
841
|
linkSync,
|
|
935
842
|
mkdirSync,
|
|
936
|
-
readFileSync
|
|
843
|
+
readFileSync,
|
|
937
844
|
realpathSync,
|
|
938
845
|
renameSync,
|
|
939
846
|
rmSync,
|
|
940
|
-
statSync
|
|
847
|
+
statSync,
|
|
941
848
|
writeFileSync
|
|
942
849
|
} from "fs";
|
|
943
850
|
import { basename, dirname, join as join2, parse, sep } from "path";
|
|
@@ -980,7 +887,7 @@ function findPackageRoot(from) {
|
|
|
980
887
|
}
|
|
981
888
|
function packageNameAt(path) {
|
|
982
889
|
try {
|
|
983
|
-
const parsed = JSON.parse(
|
|
890
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
984
891
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
985
892
|
const name = parsed["name"];
|
|
986
893
|
return typeof name === "string" ? name : null;
|
|
@@ -991,7 +898,7 @@ function packageNameAt(path) {
|
|
|
991
898
|
function versionAt(packageRoot) {
|
|
992
899
|
try {
|
|
993
900
|
const parsed = JSON.parse(
|
|
994
|
-
|
|
901
|
+
readFileSync(join2(packageRoot, "package.json"), "utf8")
|
|
995
902
|
);
|
|
996
903
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
997
904
|
const version2 = parsed["version"];
|
|
@@ -1056,7 +963,7 @@ function stampPath(env = process.env) {
|
|
|
1056
963
|
}
|
|
1057
964
|
function readStamp(env = process.env) {
|
|
1058
965
|
try {
|
|
1059
|
-
const parsed = JSON.parse(
|
|
966
|
+
const parsed = JSON.parse(readFileSync(stampPath(env), "utf8"));
|
|
1060
967
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1061
968
|
const record = parsed;
|
|
1062
969
|
const checkedAt = record["checkedAt"];
|
|
@@ -1361,8 +1268,8 @@ function releaseLock(lock) {
|
|
|
1361
1268
|
}
|
|
1362
1269
|
function readHolder(path) {
|
|
1363
1270
|
try {
|
|
1364
|
-
const mtimeMs =
|
|
1365
|
-
const pid = Number.parseInt(
|
|
1271
|
+
const mtimeMs = statSync(path).mtimeMs;
|
|
1272
|
+
const pid = Number.parseInt(readFileSync(path, "utf8").trim(), 10);
|
|
1366
1273
|
return { pid: Number.isInteger(pid) && pid > 0 ? pid : null, mtimeMs };
|
|
1367
1274
|
} catch {
|
|
1368
1275
|
return null;
|
|
@@ -1556,17 +1463,11 @@ async function fail(error, hostNotice) {
|
|
|
1556
1463
|
}
|
|
1557
1464
|
throw error;
|
|
1558
1465
|
}
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
const text = Array.isArray(messages) ? messages.map(String).join("; ") : String(messages);
|
|
1565
|
-
return `
|
|
1566
|
-
${clean(field)}: ${clean(text)}`;
|
|
1567
|
-
}).join("");
|
|
1568
|
-
}
|
|
1569
|
-
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) => {
|
|
1466
|
+
program.command("ready").description("issues ready to work, highest priority first").option(
|
|
1467
|
+
"--claim",
|
|
1468
|
+
"claim the team's next ready, unassigned issue in one server-side request (team from --team, HARNESS_TEAM or the repo config; no paging flags)"
|
|
1469
|
+
).option("--team <key>", "team filter; with --claim, the team to claim from").option("--limit <n>", "page size", integer).option("--page <n>", "page number", integer).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
|
|
1470
|
+
if (opts.claim === true) requireReadyClaimArgs(opts);
|
|
1570
1471
|
await withConfig(
|
|
1571
1472
|
opts.json === true,
|
|
1572
1473
|
opts.claim === true,
|
|
@@ -1676,7 +1577,7 @@ function requireNonBlankTitle(text, titleFile) {
|
|
|
1676
1577
|
program.command("create").description("create an issue atomically after resolving requested labels").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("--title-file <path>", 'read the title from a file ("-" for stdin)').option("--description-file <path>", 'read the description from a file ("-" for stdin)').option("--design-file <path>", 'read the design notes from a file ("-" for stdin)').option("--notes-file <path>", 'read the notes from a file ("-" for stdin)').option("--acceptance-file <path>", 'read the acceptance criteria from a file ("-" for stdin)').option(
|
|
1677
1578
|
"--type <type>",
|
|
1678
1579
|
"task | bug | feature | epic (applied as a type/ label)"
|
|
1679
|
-
).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(
|
|
1580
|
+
).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("--idempotency-key <key>", "reuse the same key when deliberately retrying this create").option(
|
|
1680
1581
|
"--label <name>",
|
|
1681
1582
|
"resolve team-first, then workspace; create missing workspace labels before the issue (repeatable)",
|
|
1682
1583
|
collect,
|
|
@@ -1726,15 +1627,15 @@ program.command("update").description("update an issue; --claim takes it atomica
|
|
|
1726
1627
|
});
|
|
1727
1628
|
}
|
|
1728
1629
|
);
|
|
1729
|
-
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("--reason-file <path>", 'read the reason from a file ("-" for stdin)').option("--cancel", "close as canceled instead of completed").option("--json", "emit JSON").action(
|
|
1630
|
+
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("--reason-file <path>", 'read the reason from a file ("-" for stdin)').option("--cancel", "close as canceled instead of completed").option("--duplicate-of <id>", "close one issue as a duplicate of the canonical issue").option("--json", "emit JSON").action(
|
|
1730
1631
|
async (ids, opts) => {
|
|
1632
|
+
if (ids.length === 0) {
|
|
1633
|
+
throw new UsageError(
|
|
1634
|
+
"usage: harness close <id> [<id>\u2026] [--reason <text> | --reason-file <path>] [--cancel | --duplicate-of <id>]"
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
requireCloseArgs(ids, opts);
|
|
1731
1638
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
1732
|
-
if (ids.length === 0) {
|
|
1733
|
-
throw new UsageError(
|
|
1734
|
-
"usage: harness close <id> [<id>\u2026] [--reason <text> | --reason-file <path>] [--cancel]"
|
|
1735
|
-
);
|
|
1736
|
-
}
|
|
1737
|
-
requireIdentifiers2(ids);
|
|
1738
1639
|
const text = resolveTextSources([
|
|
1739
1640
|
{ name: "reason", inline: opts.reason, file: opts.reasonFile }
|
|
1740
1641
|
]);
|
|
@@ -1813,6 +1714,60 @@ relation.command("rm").argument("[issue]").argument("[id-or-issue]").option("--j
|
|
|
1813
1714
|
return relationRm(ctx, issue, idOrIssue);
|
|
1814
1715
|
});
|
|
1815
1716
|
});
|
|
1717
|
+
var plan = program.command("plan").description("plans: a whole graph of issues created in one request, then released and held");
|
|
1718
|
+
plan.command("create").description("create a plan from a JSON or YAML file (the API's request body); it starts held").option("--file <path>", 'the plan: .json is JSON, anything else YAML ("-" for stdin)').option("--team <key>", "team of the root and of people's steps (or HARNESS_TEAM)").option("--idempotency-key <key>", "reuse the same key when deliberately retrying this create").option("--json", "emit JSON").action(async (opts) => {
|
|
1719
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
1720
|
+
if (opts.file === void 0) {
|
|
1721
|
+
throw new UsageError("usage: harness plan create --file <plan.json|plan.yaml> [--team <key>]");
|
|
1722
|
+
}
|
|
1723
|
+
const team2 = requiredTeam(ctx, opts.team);
|
|
1724
|
+
validateIdempotencyKey(opts.idempotencyKey);
|
|
1725
|
+
return planCreate(ctx, readPlanFile(opts.file), { ...opts, team: team2 });
|
|
1726
|
+
});
|
|
1727
|
+
});
|
|
1728
|
+
plan.command("show").description("a plan's status, budgets, revision, steps, edges and wait conditions").argument("[id]").option("--json", "emit JSON").action(async (id, opts) => {
|
|
1729
|
+
await withConfig(opts.json === true, false, (ctx) => {
|
|
1730
|
+
if (id === void 0) throw new UsageError("usage: harness plan show <plan-id>");
|
|
1731
|
+
return planShow(ctx, id);
|
|
1732
|
+
});
|
|
1733
|
+
});
|
|
1734
|
+
plan.command("edit").description("change step executors and keys, and budgets, from a JSON or YAML file").argument("[id]").option("--file <path>", 'the edit: .json is JSON, anything else YAML ("-" for stdin)').option("--json", "emit JSON").action(async (id, opts) => {
|
|
1735
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
1736
|
+
if (id === void 0 || opts.file === void 0) {
|
|
1737
|
+
throw new UsageError("usage: harness plan edit <plan-id> --file <edits.json|edits.yaml>");
|
|
1738
|
+
}
|
|
1739
|
+
requirePlanId(id);
|
|
1740
|
+
return planEdit(ctx, id, readPlanFile(opts.file));
|
|
1741
|
+
});
|
|
1742
|
+
});
|
|
1743
|
+
plan.command("release").description("release a plan as yourself: freeze its next revision and let its ready steps run").argument("[id]").option("--collaborator <who>", "a person of this organization (id or email) for the plan room (repeatable)", collect, []).option("--retry-budget <n>", "retries per failed Operator step", integer).option("--spend-budget-cents <n>", "the plan's spend budget; needed to release Operator steps", integer).option("--json", "emit JSON").action(
|
|
1744
|
+
async (id, opts) => {
|
|
1745
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
1746
|
+
if (id === void 0) throw new UsageError("usage: harness plan release <plan-id>");
|
|
1747
|
+
return planRelease(ctx, id, { ...opts, collaborators: opts.collaborator });
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
);
|
|
1751
|
+
plan.command("hold").description("pause every step of a plan until it is released again").argument("[id]").option("--json", "emit JSON").action(async (id, opts) => {
|
|
1752
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
1753
|
+
if (id === void 0) throw new UsageError("usage: harness plan hold <plan-id>");
|
|
1754
|
+
return planHold(ctx, id);
|
|
1755
|
+
});
|
|
1756
|
+
});
|
|
1757
|
+
var gate = program.command("gate").description("manual wait conditions: gate satisfy|unsatisfy <issue> <gate>");
|
|
1758
|
+
for (const [verb, satisfied] of [
|
|
1759
|
+
["satisfy", true],
|
|
1760
|
+
["unsatisfy", false]
|
|
1761
|
+
]) {
|
|
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) => {
|
|
1763
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
1764
|
+
if (issue === void 0 || gateName === void 0) {
|
|
1765
|
+
throw new UsageError(`usage: harness gate ${verb} <issue> <gate-label-or-id>`);
|
|
1766
|
+
}
|
|
1767
|
+
return gateSet(ctx, issue, gateName, satisfied);
|
|
1768
|
+
});
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1816
1771
|
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(
|
|
1817
1772
|
async (id, text, opts) => {
|
|
1818
1773
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
@@ -2093,7 +2048,7 @@ program.command("upload").description("upload an image (25 MB) or H.264 MP4 vide
|
|
|
2093
2048
|
});
|
|
2094
2049
|
});
|
|
2095
2050
|
program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
|
|
2096
|
-
const { startMcpServer } = await import("./server-
|
|
2051
|
+
const { startMcpServer } = await import("./server-75QJASTY.js");
|
|
2097
2052
|
await startMcpServer();
|
|
2098
2053
|
});
|
|
2099
2054
|
var keys = program.command("keys").description("the org's provider API keys, handed to tooling without printing them").enablePositionalOptions();
|
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
import {
|
|
3
3
|
ACTIVITY_MAX_LIMIT,
|
|
4
4
|
CLI_VERSION,
|
|
5
|
+
CONDITION_KINDS,
|
|
5
6
|
Client,
|
|
6
7
|
HttpError,
|
|
8
|
+
IDEMPOTENCY_KEY_FORMAT,
|
|
9
|
+
MAX_IDEMPOTENCY_KEY_BYTES,
|
|
10
|
+
PLAN_EXECUTORS,
|
|
7
11
|
RELATION_CREATE_TYPES,
|
|
8
12
|
RELATION_UPDATE_TYPES,
|
|
9
13
|
TransportError,
|
|
@@ -22,6 +26,8 @@ import {
|
|
|
22
26
|
docShow,
|
|
23
27
|
docUpdate,
|
|
24
28
|
editComment,
|
|
29
|
+
errorDetails,
|
|
30
|
+
gateSet,
|
|
25
31
|
labelCreate,
|
|
26
32
|
labelDelete,
|
|
27
33
|
labelList,
|
|
@@ -36,6 +42,11 @@ import {
|
|
|
36
42
|
memberSetRole,
|
|
37
43
|
milestoneCreate,
|
|
38
44
|
milestoneList,
|
|
45
|
+
planCreate,
|
|
46
|
+
planEdit,
|
|
47
|
+
planHold,
|
|
48
|
+
planRelease,
|
|
49
|
+
planShow,
|
|
39
50
|
projectCreate,
|
|
40
51
|
projectList,
|
|
41
52
|
projectShow,
|
|
@@ -61,7 +72,7 @@ import {
|
|
|
61
72
|
upload,
|
|
62
73
|
userList,
|
|
63
74
|
workflowStates
|
|
64
|
-
} from "./chunk-
|
|
75
|
+
} from "./chunk-F2YVTB2A.js";
|
|
65
76
|
|
|
66
77
|
// src/mcp/server.ts
|
|
67
78
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -97,7 +108,9 @@ async function run(work) {
|
|
|
97
108
|
return { content };
|
|
98
109
|
} catch (error) {
|
|
99
110
|
if (error instanceof NeedsLoginError) return toolError(error.message);
|
|
100
|
-
if (error instanceof HttpError)
|
|
111
|
+
if (error instanceof HttpError) {
|
|
112
|
+
return toolError(`${error.message} (HTTP ${error.status})${errorDetails(error.body)}`);
|
|
113
|
+
}
|
|
101
114
|
if (error instanceof TransportError) return toolError(`cannot reach the API: ${error.message}`);
|
|
102
115
|
if (error instanceof Error) return toolError(error.message);
|
|
103
116
|
throw error;
|
|
@@ -111,11 +124,55 @@ var page = {
|
|
|
111
124
|
var identifier = z.string().describe("issue identifier, e.g. IO-123");
|
|
112
125
|
var commentId = z.string().uuid().describe("comment UUID from issue_show or comment_list");
|
|
113
126
|
var dateField = z.string().optional().describe('YYYY-MM-DD; "" clears the date');
|
|
127
|
+
var planCondition = z.object({
|
|
128
|
+
kind: z.enum(CONDITION_KINDS),
|
|
129
|
+
not_before: z.string().optional().describe("not_before: an ISO 8601 UTC instant"),
|
|
130
|
+
label: z.string().optional().describe("gate: what a person ticks, e.g. Finance sign-off"),
|
|
131
|
+
event: z.string().optional().describe("event: deal_stage_changed, opportunity_stage_changed, form_submitted or investment_executed"),
|
|
132
|
+
subject_id: z.string().optional().describe("event: the record the event is about (one you can read)"),
|
|
133
|
+
target_id: z.string().optional().describe("event: the stage or status a stage change must reach")
|
|
134
|
+
}).passthrough();
|
|
135
|
+
var planIssueFields = {
|
|
136
|
+
title: z.string(),
|
|
137
|
+
description: z.string().optional().describe("an Operator step's description is its instruction (required for one)"),
|
|
138
|
+
design: z.string().optional(),
|
|
139
|
+
notes: z.string().optional(),
|
|
140
|
+
acceptance_criteria: z.string().optional(),
|
|
141
|
+
priority: z.number().int().optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low"),
|
|
142
|
+
estimate: z.number().optional(),
|
|
143
|
+
due_date: z.string().optional().describe("YYYY-MM-DD"),
|
|
144
|
+
assignee: z.string().optional().describe("email or user id; never on an Operator step"),
|
|
145
|
+
label_ids: z.array(z.string()).optional()
|
|
146
|
+
};
|
|
147
|
+
var planStep = z.object({
|
|
148
|
+
...planIssueFields,
|
|
149
|
+
key: z.string().describe("unique in the plan; other steps refer to this step by it"),
|
|
150
|
+
parent: z.string().optional().describe("key of the step that contains this one; the root when absent"),
|
|
151
|
+
blocked_by: z.array(z.string()).optional().describe("keys of the steps this one waits for"),
|
|
152
|
+
executor: z.enum(PLAN_EXECUTORS).optional().describe("none (a person, the default) or operator"),
|
|
153
|
+
team: z.string().optional().describe("team key for a person's step; Operator steps go to the Operator team"),
|
|
154
|
+
conditions: z.array(planCondition).optional().describe("wait conditions: all must hold before the step is ready")
|
|
155
|
+
}).passthrough();
|
|
156
|
+
var planDocument = z.object({
|
|
157
|
+
...planIssueFields,
|
|
158
|
+
parent: z.string().optional().describe("an existing issue to file the root under, e.g. IO-12"),
|
|
159
|
+
retry_budget: z.number().int().min(0).optional().describe("retries per failed Operator step (default 2)"),
|
|
160
|
+
spend_budget_cents: z.number().int().min(0).nullable().optional().describe("needed to release a plan with Operator steps"),
|
|
161
|
+
steps: z.array(planStep).min(1).describe("at most 99 steps besides the root")
|
|
162
|
+
}).passthrough();
|
|
163
|
+
var planId = z.string().uuid().describe("plan UUID, from plan_create");
|
|
164
|
+
var planStepEdit = z.object({
|
|
165
|
+
step: z.string().optional().describe("the step's key; give this or issue"),
|
|
166
|
+
issue: z.string().optional().describe("the step's issue identifier; give this or step"),
|
|
167
|
+
executor: z.enum(PLAN_EXECUTORS).optional().describe("none hands the step to a person"),
|
|
168
|
+
key: z.string().nullable().optional().describe("the step's new key")
|
|
169
|
+
}).passthrough();
|
|
170
|
+
var idempotencyKey = z.string().min(1).max(MAX_IDEMPOTENCY_KEY_BYTES).regex(IDEMPOTENCY_KEY_FORMAT, "must use printable ASCII without leading or trailing spaces");
|
|
114
171
|
var TOOLS = [
|
|
115
172
|
// ── Issues ──
|
|
116
173
|
{
|
|
117
174
|
name: "issue_list",
|
|
118
|
-
description: "List issues with filters. ready=true \u2192 unblocked open issues; blocked=true \u2192 issues waiting on a blocker.",
|
|
175
|
+
description: "List issues with filters. ready=true \u2192 unblocked open issues, each with agent_step (true when it is filed in the Operator team, where plan steps Operator runs live: only the plan driver claims those, and a claim refusal is the authority); blocked=true \u2192 issues waiting on a blocker.",
|
|
119
176
|
readOnly: true,
|
|
120
177
|
shape: {
|
|
121
178
|
...page,
|
|
@@ -158,7 +215,7 @@ var TOOLS = [
|
|
|
158
215
|
},
|
|
159
216
|
{
|
|
160
217
|
name: "issue_show",
|
|
161
|
-
description: "Full issue detail: fields, relations, children, comments. images=true downloads attached images to the local cache and includes their paths as local_images.",
|
|
218
|
+
description: "Full issue detail: fields, relations, children, comments. blockers gives each unresolved blocker's state_type and archived flag: only a completed blocker resolves, so a cancelled or archived one still blocks. waiting_on_condition and conditions say which wait conditions (not_before, gate, event) still hold it; tick a gate with gate_set. images=true downloads attached images to the local cache and includes their paths as local_images.",
|
|
162
219
|
readOnly: true,
|
|
163
220
|
shape: { id: identifier, images: z.boolean().optional() },
|
|
164
221
|
handler: (ctx, args) => show(ctx, args["id"], { images: args["images"] === true })
|
|
@@ -190,13 +247,15 @@ var TOOLS = [
|
|
|
190
247
|
due_date: dateField,
|
|
191
248
|
parent: z.string().optional(),
|
|
192
249
|
assignee: z.string().optional(),
|
|
193
|
-
labels: z.array(z.string()).optional()
|
|
250
|
+
labels: z.array(z.string()).optional(),
|
|
251
|
+
idempotency_key: idempotencyKey.optional().describe("reuse the same printable-ASCII key when deliberately retrying this create")
|
|
194
252
|
},
|
|
195
253
|
handler: (ctx, args) => create(ctx, {
|
|
196
254
|
...args,
|
|
197
255
|
acceptance: args["acceptance_criteria"],
|
|
198
256
|
dueDate: args["due_date"],
|
|
199
|
-
label: args["labels"]
|
|
257
|
+
label: args["labels"],
|
|
258
|
+
idempotencyKey: args["idempotency_key"]
|
|
200
259
|
})
|
|
201
260
|
},
|
|
202
261
|
{
|
|
@@ -230,21 +289,26 @@ var TOOLS = [
|
|
|
230
289
|
},
|
|
231
290
|
{
|
|
232
291
|
name: "issue_claim",
|
|
233
|
-
description: "Atomically claim an issue: assigns it and moves it to the team's first started state. A conflict
|
|
292
|
+
description: "Atomically claim an issue: assigns it and moves it to the team's first started state. A conflict names its reason: someone else holds it, or it is not claimable (e.g. agent_step, waiting_on_condition).",
|
|
234
293
|
readOnly: false,
|
|
235
294
|
shape: { id: identifier },
|
|
236
295
|
handler: (ctx, args) => update(ctx, args["id"], { claim: true })
|
|
237
296
|
},
|
|
238
297
|
{
|
|
239
298
|
name: "issue_close",
|
|
240
|
-
description: "Close issues. The result lists newly unblocked issues \u2014 surface them.",
|
|
299
|
+
description: "Close issues. Set duplicate_of for one source issue to close it as a duplicate; it cannot be combined with cancel. The result lists newly unblocked issues \u2014 surface them. still_blocking lists the dependents a cancelled or duplicate-closed issue still blocks: only completing releases them.",
|
|
241
300
|
readOnly: false,
|
|
242
301
|
shape: {
|
|
243
302
|
ids: z.array(identifier).min(1),
|
|
244
303
|
reason: z.string().optional().describe("why it is done, e.g. the commit hash"),
|
|
245
|
-
cancel: z.boolean().optional().describe("close as canceled instead of completed")
|
|
304
|
+
cancel: z.boolean().optional().describe("close as canceled instead of completed; incompatible with duplicate_of"),
|
|
305
|
+
duplicate_of: identifier.optional().describe("canonical issue; requires exactly one source id and is incompatible with cancel")
|
|
246
306
|
},
|
|
247
|
-
handler: (ctx, args) => close(ctx, args["ids"],
|
|
307
|
+
handler: (ctx, args) => close(ctx, args["ids"], {
|
|
308
|
+
reason: args["reason"],
|
|
309
|
+
cancel: args["cancel"],
|
|
310
|
+
duplicateOf: args["duplicate_of"]
|
|
311
|
+
})
|
|
248
312
|
},
|
|
249
313
|
{
|
|
250
314
|
name: "issue_comment",
|
|
@@ -656,6 +720,74 @@ var TOOLS = [
|
|
|
656
720
|
shape: {},
|
|
657
721
|
handler: (ctx) => stats(ctx)
|
|
658
722
|
},
|
|
723
|
+
// ── Plans and gates ──
|
|
724
|
+
{
|
|
725
|
+
name: "plan_create",
|
|
726
|
+
description: "Create a plan: a root issue and up to 99 steps, created in one transaction or not at all, and held until a person releases it (plan_release). Steps refer to each other by key: parent (containment) and blocked_by (dependencies). executor operator files a step in the organization's Operator team, where only the plan driver claims it; a person's step goes to its team (or the plan's). conditions are wait conditions (not_before, gate, event). An invalid plan writes nothing and the error lists every violation by step key. Reuse idempotency_key when retrying.",
|
|
727
|
+
readOnly: false,
|
|
728
|
+
shape: {
|
|
729
|
+
team: z.string().optional().describe("team key for the root and people's steps; defaults to HARNESS_TEAM"),
|
|
730
|
+
plan: planDocument.describe("the root's fields plus steps"),
|
|
731
|
+
idempotency_key: idempotencyKey.optional().describe("reuse the same printable-ASCII key when deliberately retrying this create")
|
|
732
|
+
},
|
|
733
|
+
handler: (ctx, args) => planCreate(ctx, args["plan"], { team: args["team"], idempotencyKey: args["idempotency_key"] })
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
name: "plan_show",
|
|
737
|
+
description: "A plan's status, budgets, current revision and spend estimate, and its whole tree root first: each step's key, executor, parent, state, ready flag, held flag (edited since release), in_revision and wait conditions, plus the blocks edges between steps.",
|
|
738
|
+
readOnly: true,
|
|
739
|
+
shape: { id: planId },
|
|
740
|
+
handler: (ctx, args) => planShow(ctx, args["id"])
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
name: "plan_edit",
|
|
744
|
+
description: "Change step executors and keys (each step named by its key or issue identifier) and the plan's budgets. Editing a released step holds it until the plan is released again; a budget edit needs the plan held. Descriptions, parents and edges are edited with issue_update and relation_add/relation_rm; conditions are set at plan_create, and gate_set ticks a gate.",
|
|
745
|
+
readOnly: false,
|
|
746
|
+
shape: {
|
|
747
|
+
id: planId,
|
|
748
|
+
steps: z.array(planStepEdit).optional(),
|
|
749
|
+
retry_budget: z.number().int().min(0).optional(),
|
|
750
|
+
spend_budget_cents: z.number().int().min(0).nullable().optional().describe("null clears the budget")
|
|
751
|
+
},
|
|
752
|
+
handler: (ctx, args) => {
|
|
753
|
+
const { id, ...edits } = args;
|
|
754
|
+
return planEdit(ctx, id, edits);
|
|
755
|
+
}
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
name: "plan_release",
|
|
759
|
+
description: "Release a held plan (or one that needs attention) as the caller: freezes its next revision and lets its ready steps run. A plan with Operator steps needs a spend budget and the caller's right to run Operator; collaborators join its plan room.",
|
|
760
|
+
readOnly: false,
|
|
761
|
+
shape: {
|
|
762
|
+
id: planId,
|
|
763
|
+
collaborators: z.array(z.string()).max(20).optional().describe("people of this organization, by id or email"),
|
|
764
|
+
retry_budget: z.number().int().min(0).optional(),
|
|
765
|
+
spend_budget_cents: z.number().int().min(0).nullable().optional().describe("null clears the budget")
|
|
766
|
+
},
|
|
767
|
+
handler: (ctx, args) => planRelease(ctx, args["id"], {
|
|
768
|
+
collaborators: args["collaborators"],
|
|
769
|
+
retryBudget: args["retry_budget"],
|
|
770
|
+
spendBudgetCents: args["spend_budget_cents"]
|
|
771
|
+
})
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
name: "plan_hold",
|
|
775
|
+
description: "Pause every step of a plan: none is ready until it is released again.",
|
|
776
|
+
readOnly: false,
|
|
777
|
+
shape: { id: planId },
|
|
778
|
+
handler: (ctx, args) => planHold(ctx, args["id"])
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
name: "gate_set",
|
|
782
|
+
description: "Satisfy (satisfied true) or un-satisfy (false) a gate wait condition on an issue, found by its label or condition id (issue_show lists conditions). Needs staff standing in the organization.",
|
|
783
|
+
readOnly: false,
|
|
784
|
+
shape: {
|
|
785
|
+
issue: identifier,
|
|
786
|
+
gate: z.string().describe("the gate's label, or its condition id"),
|
|
787
|
+
satisfied: z.boolean()
|
|
788
|
+
},
|
|
789
|
+
handler: (ctx, args) => gateSet(ctx, args["issue"], args["gate"], args["satisfied"])
|
|
790
|
+
},
|
|
659
791
|
// ── Uploads ──
|
|
660
792
|
{
|
|
661
793
|
name: "upload",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@allocator-one/harness-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "CLI and MCP server for Harness, allocator.one's issue tracker",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
35
35
|
"commander": "^14.0.0",
|
|
36
|
+
"yaml": "^2.9.1",
|
|
36
37
|
"zod": "^3.25.76"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|