@allocator-one/harness-cli 1.13.0 → 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 +197 -19
- package/dist/{chunk-XFMECLOU.js → chunk-F2YVTB2A.js} +683 -77
- package/dist/cli.js +81 -127
- package/dist/{server-CYBKYG5E.js → server-75QJASTY.js} +130 -7
- package/package.json +2 -1
- package/spec/openapi.json +3924 -11
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,12 +50,18 @@ 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,
|
|
@@ -62,8 +70,12 @@ import {
|
|
|
62
70
|
requireCloseArgs,
|
|
63
71
|
requireCommentId,
|
|
64
72
|
requireIdentifiers,
|
|
73
|
+
requirePlanId,
|
|
74
|
+
requireReadyClaimArgs,
|
|
75
|
+
requiredTeam,
|
|
65
76
|
resolveCacheHome,
|
|
66
77
|
resolveConfig,
|
|
78
|
+
resolveTextSources,
|
|
67
79
|
search,
|
|
68
80
|
show,
|
|
69
81
|
stateAdd,
|
|
@@ -80,10 +92,11 @@ import {
|
|
|
80
92
|
upload,
|
|
81
93
|
userList,
|
|
82
94
|
validateBaseUrl,
|
|
95
|
+
validateIdempotencyKey,
|
|
83
96
|
version,
|
|
84
97
|
workflowStates,
|
|
85
98
|
writeCredentials
|
|
86
|
-
} from "./chunk-
|
|
99
|
+
} from "./chunk-F2YVTB2A.js";
|
|
87
100
|
|
|
88
101
|
// src/cli.ts
|
|
89
102
|
import { createInterface as createInterface2 } from "readline";
|
|
@@ -809,113 +822,6 @@ function defaultOpenBrowser(url) {
|
|
|
809
822
|
}
|
|
810
823
|
}
|
|
811
824
|
|
|
812
|
-
// src/core/textinput.ts
|
|
813
|
-
import { readFileSync, statSync } from "fs";
|
|
814
|
-
import { isatty } from "tty";
|
|
815
|
-
var STDIN_PATH = "-";
|
|
816
|
-
var MAX_TEXT_BYTES = 1e6;
|
|
817
|
-
function flagOf(name) {
|
|
818
|
-
return name.replace(/[A-Z]/g, (upper) => `-${upper.toLowerCase()}`);
|
|
819
|
-
}
|
|
820
|
-
function resolveTextSources(sources) {
|
|
821
|
-
const resolved = {};
|
|
822
|
-
const toRead = [];
|
|
823
|
-
let stdinClaimedBy;
|
|
824
|
-
for (const { name, inline, file } of sources) {
|
|
825
|
-
if (inline !== void 0 && file !== void 0) {
|
|
826
|
-
throw new UsageError(
|
|
827
|
-
`--${flagOf(name)} and --${flagOf(name)}-file both given \u2014 pass one; there is no precedence between them`
|
|
828
|
-
);
|
|
829
|
-
}
|
|
830
|
-
if (file === void 0) {
|
|
831
|
-
if (inline !== void 0) resolved[name] = inline;
|
|
832
|
-
continue;
|
|
833
|
-
}
|
|
834
|
-
if (file === STDIN_PATH) {
|
|
835
|
-
if (isatty(0)) {
|
|
836
|
-
throw new UsageError(
|
|
837
|
-
`--${flagOf(name)}-file - reads stdin, but stdin is a terminal \u2014 pipe something in or pass a path`
|
|
838
|
-
);
|
|
839
|
-
}
|
|
840
|
-
if (stdinClaimedBy !== void 0) {
|
|
841
|
-
throw new UsageError(
|
|
842
|
-
`--${flagOf(name)}-file and --${flagOf(stdinClaimedBy)}-file both read stdin \u2014 there is only one stream, so only one may be "-"`
|
|
843
|
-
);
|
|
844
|
-
}
|
|
845
|
-
stdinClaimedBy = name;
|
|
846
|
-
}
|
|
847
|
-
if (file !== STDIN_PATH) checkReadable(file, name);
|
|
848
|
-
toRead.push({ name, file });
|
|
849
|
-
}
|
|
850
|
-
for (const { name, file } of [...toRead].sort(byStdinLast)) {
|
|
851
|
-
resolved[name] = stripOneTrailingNewline(readTextFile(file, name));
|
|
852
|
-
}
|
|
853
|
-
return resolved;
|
|
854
|
-
}
|
|
855
|
-
function readTextFile(path, name) {
|
|
856
|
-
if (path === STDIN_PATH) {
|
|
857
|
-
let piped;
|
|
858
|
-
try {
|
|
859
|
-
piped = readFileSync(0);
|
|
860
|
-
} catch (error) {
|
|
861
|
-
throw new UsageError(`--${flagOf(name)}-file -: cannot read stdin (${describe(error)})`);
|
|
862
|
-
}
|
|
863
|
-
if (piped.length > MAX_TEXT_BYTES) {
|
|
864
|
-
throw new UsageError(oversize(`--${flagOf(name)}-file -`));
|
|
865
|
-
}
|
|
866
|
-
return decodeUtf8(piped, `--${flagOf(name)}-file -`);
|
|
867
|
-
}
|
|
868
|
-
const flag = `--${flagOf(name)}-file ${path}`;
|
|
869
|
-
let bytes;
|
|
870
|
-
try {
|
|
871
|
-
bytes = readFileSync(path);
|
|
872
|
-
} catch (error) {
|
|
873
|
-
throw new UsageError(`${flag}: ${describe(error)}`);
|
|
874
|
-
}
|
|
875
|
-
if (bytes.length > MAX_TEXT_BYTES) throw new UsageError(oversize(flag));
|
|
876
|
-
return decodeUtf8(bytes, flag);
|
|
877
|
-
}
|
|
878
|
-
function decodeUtf8(bytes, flag) {
|
|
879
|
-
try {
|
|
880
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
881
|
-
} catch {
|
|
882
|
-
throw new UsageError(`${flag}: not valid UTF-8 \u2014 convert it first`);
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
function oversize(what) {
|
|
886
|
-
return `${what}: over ${MAX_TEXT_BYTES / 1e6} MB \u2014 that is not prose; check the path`;
|
|
887
|
-
}
|
|
888
|
-
function checkReadable(path, name) {
|
|
889
|
-
const flag = `--${flagOf(name)}-file ${path}`;
|
|
890
|
-
let stats2;
|
|
891
|
-
try {
|
|
892
|
-
stats2 = statSync(path);
|
|
893
|
-
} catch (error) {
|
|
894
|
-
throw new UsageError(`${flag}: ${describe(error)}`);
|
|
895
|
-
}
|
|
896
|
-
if (!stats2.isFile()) {
|
|
897
|
-
throw new UsageError(
|
|
898
|
-
`${flag}: ${stats2.isDirectory() ? "is a directory" : "not a regular file"}`
|
|
899
|
-
);
|
|
900
|
-
}
|
|
901
|
-
if (stats2.size > MAX_TEXT_BYTES) throw new UsageError(oversize(flag));
|
|
902
|
-
}
|
|
903
|
-
function byStdinLast(a, b) {
|
|
904
|
-
return Number(a.file === STDIN_PATH) - Number(b.file === STDIN_PATH);
|
|
905
|
-
}
|
|
906
|
-
function stripOneTrailingNewline(text) {
|
|
907
|
-
if (text.endsWith("\r\n")) return text.slice(0, -2);
|
|
908
|
-
if (text.endsWith("\n")) return text.slice(0, -1);
|
|
909
|
-
return text;
|
|
910
|
-
}
|
|
911
|
-
function describe(error) {
|
|
912
|
-
const code = error?.code;
|
|
913
|
-
if (code === "ENOENT") return "no such file";
|
|
914
|
-
if (code === "EISDIR") return "is a directory";
|
|
915
|
-
if (code === "EACCES") return "permission denied";
|
|
916
|
-
return error instanceof Error ? error.message : String(error);
|
|
917
|
-
}
|
|
918
|
-
|
|
919
825
|
// src/core/stdio.ts
|
|
920
826
|
var BROKEN_PIPE_CODES = /* @__PURE__ */ new Set(["EPIPE", "ERR_STREAM_DESTROYED"]);
|
|
921
827
|
function guardBrokenPipes(streams = [process.stdout, process.stderr]) {
|
|
@@ -934,11 +840,11 @@ import {
|
|
|
934
840
|
existsSync,
|
|
935
841
|
linkSync,
|
|
936
842
|
mkdirSync,
|
|
937
|
-
readFileSync
|
|
843
|
+
readFileSync,
|
|
938
844
|
realpathSync,
|
|
939
845
|
renameSync,
|
|
940
846
|
rmSync,
|
|
941
|
-
statSync
|
|
847
|
+
statSync,
|
|
942
848
|
writeFileSync
|
|
943
849
|
} from "fs";
|
|
944
850
|
import { basename, dirname, join as join2, parse, sep } from "path";
|
|
@@ -981,7 +887,7 @@ function findPackageRoot(from) {
|
|
|
981
887
|
}
|
|
982
888
|
function packageNameAt(path) {
|
|
983
889
|
try {
|
|
984
|
-
const parsed = JSON.parse(
|
|
890
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
985
891
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
986
892
|
const name = parsed["name"];
|
|
987
893
|
return typeof name === "string" ? name : null;
|
|
@@ -992,7 +898,7 @@ function packageNameAt(path) {
|
|
|
992
898
|
function versionAt(packageRoot) {
|
|
993
899
|
try {
|
|
994
900
|
const parsed = JSON.parse(
|
|
995
|
-
|
|
901
|
+
readFileSync(join2(packageRoot, "package.json"), "utf8")
|
|
996
902
|
);
|
|
997
903
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
998
904
|
const version2 = parsed["version"];
|
|
@@ -1057,7 +963,7 @@ function stampPath(env = process.env) {
|
|
|
1057
963
|
}
|
|
1058
964
|
function readStamp(env = process.env) {
|
|
1059
965
|
try {
|
|
1060
|
-
const parsed = JSON.parse(
|
|
966
|
+
const parsed = JSON.parse(readFileSync(stampPath(env), "utf8"));
|
|
1061
967
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1062
968
|
const record = parsed;
|
|
1063
969
|
const checkedAt = record["checkedAt"];
|
|
@@ -1362,8 +1268,8 @@ function releaseLock(lock) {
|
|
|
1362
1268
|
}
|
|
1363
1269
|
function readHolder(path) {
|
|
1364
1270
|
try {
|
|
1365
|
-
const mtimeMs =
|
|
1366
|
-
const pid = Number.parseInt(
|
|
1271
|
+
const mtimeMs = statSync(path).mtimeMs;
|
|
1272
|
+
const pid = Number.parseInt(readFileSync(path, "utf8").trim(), 10);
|
|
1367
1273
|
return { pid: Number.isInteger(pid) && pid > 0 ? pid : null, mtimeMs };
|
|
1368
1274
|
} catch {
|
|
1369
1275
|
return null;
|
|
@@ -1557,17 +1463,11 @@ async function fail(error, hostNotice) {
|
|
|
1557
1463
|
}
|
|
1558
1464
|
throw error;
|
|
1559
1465
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
const text = Array.isArray(messages) ? messages.map(String).join("; ") : String(messages);
|
|
1566
|
-
return `
|
|
1567
|
-
${clean(field)}: ${clean(text)}`;
|
|
1568
|
-
}).join("");
|
|
1569
|
-
}
|
|
1570
|
-
program.command("ready").description("issues ready to work, highest priority first").option("--claim", "atomically claim the first claimable issue").option("--team <key>", "team filter").option("--limit <n>", "page size", integer).option("--page <n>", "page number", integer).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
|
|
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);
|
|
1571
1471
|
await withConfig(
|
|
1572
1472
|
opts.json === true,
|
|
1573
1473
|
opts.claim === true,
|
|
@@ -1814,6 +1714,60 @@ relation.command("rm").argument("[issue]").argument("[id-or-issue]").option("--j
|
|
|
1814
1714
|
return relationRm(ctx, issue, idOrIssue);
|
|
1815
1715
|
});
|
|
1816
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
|
+
}
|
|
1817
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(
|
|
1818
1772
|
async (id, text, opts) => {
|
|
1819
1773
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
@@ -2094,7 +2048,7 @@ program.command("upload").description("upload an image (25 MB) or H.264 MP4 vide
|
|
|
2094
2048
|
});
|
|
2095
2049
|
});
|
|
2096
2050
|
program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
|
|
2097
|
-
const { startMcpServer } = await import("./server-
|
|
2051
|
+
const { startMcpServer } = await import("./server-75QJASTY.js");
|
|
2098
2052
|
await startMcpServer();
|
|
2099
2053
|
});
|
|
2100
2054
|
var keys = program.command("keys").description("the org's provider API keys, handed to tooling without printing them").enablePositionalOptions();
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
import {
|
|
3
3
|
ACTIVITY_MAX_LIMIT,
|
|
4
4
|
CLI_VERSION,
|
|
5
|
+
CONDITION_KINDS,
|
|
5
6
|
Client,
|
|
6
7
|
HttpError,
|
|
7
8
|
IDEMPOTENCY_KEY_FORMAT,
|
|
8
9
|
MAX_IDEMPOTENCY_KEY_BYTES,
|
|
10
|
+
PLAN_EXECUTORS,
|
|
9
11
|
RELATION_CREATE_TYPES,
|
|
10
12
|
RELATION_UPDATE_TYPES,
|
|
11
13
|
TransportError,
|
|
@@ -24,6 +26,8 @@ import {
|
|
|
24
26
|
docShow,
|
|
25
27
|
docUpdate,
|
|
26
28
|
editComment,
|
|
29
|
+
errorDetails,
|
|
30
|
+
gateSet,
|
|
27
31
|
labelCreate,
|
|
28
32
|
labelDelete,
|
|
29
33
|
labelList,
|
|
@@ -38,6 +42,11 @@ import {
|
|
|
38
42
|
memberSetRole,
|
|
39
43
|
milestoneCreate,
|
|
40
44
|
milestoneList,
|
|
45
|
+
planCreate,
|
|
46
|
+
planEdit,
|
|
47
|
+
planHold,
|
|
48
|
+
planRelease,
|
|
49
|
+
planShow,
|
|
41
50
|
projectCreate,
|
|
42
51
|
projectList,
|
|
43
52
|
projectShow,
|
|
@@ -63,7 +72,7 @@ import {
|
|
|
63
72
|
upload,
|
|
64
73
|
userList,
|
|
65
74
|
workflowStates
|
|
66
|
-
} from "./chunk-
|
|
75
|
+
} from "./chunk-F2YVTB2A.js";
|
|
67
76
|
|
|
68
77
|
// src/mcp/server.ts
|
|
69
78
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -99,7 +108,9 @@ async function run(work) {
|
|
|
99
108
|
return { content };
|
|
100
109
|
} catch (error) {
|
|
101
110
|
if (error instanceof NeedsLoginError) return toolError(error.message);
|
|
102
|
-
if (error instanceof HttpError)
|
|
111
|
+
if (error instanceof HttpError) {
|
|
112
|
+
return toolError(`${error.message} (HTTP ${error.status})${errorDetails(error.body)}`);
|
|
113
|
+
}
|
|
103
114
|
if (error instanceof TransportError) return toolError(`cannot reach the API: ${error.message}`);
|
|
104
115
|
if (error instanceof Error) return toolError(error.message);
|
|
105
116
|
throw error;
|
|
@@ -113,11 +124,55 @@ var page = {
|
|
|
113
124
|
var identifier = z.string().describe("issue identifier, e.g. IO-123");
|
|
114
125
|
var commentId = z.string().uuid().describe("comment UUID from issue_show or comment_list");
|
|
115
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");
|
|
116
171
|
var TOOLS = [
|
|
117
172
|
// ── Issues ──
|
|
118
173
|
{
|
|
119
174
|
name: "issue_list",
|
|
120
|
-
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.",
|
|
121
176
|
readOnly: true,
|
|
122
177
|
shape: {
|
|
123
178
|
...page,
|
|
@@ -160,7 +215,7 @@ var TOOLS = [
|
|
|
160
215
|
},
|
|
161
216
|
{
|
|
162
217
|
name: "issue_show",
|
|
163
|
-
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.",
|
|
164
219
|
readOnly: true,
|
|
165
220
|
shape: { id: identifier, images: z.boolean().optional() },
|
|
166
221
|
handler: (ctx, args) => show(ctx, args["id"], { images: args["images"] === true })
|
|
@@ -193,7 +248,7 @@ var TOOLS = [
|
|
|
193
248
|
parent: z.string().optional(),
|
|
194
249
|
assignee: z.string().optional(),
|
|
195
250
|
labels: z.array(z.string()).optional(),
|
|
196
|
-
idempotency_key:
|
|
251
|
+
idempotency_key: idempotencyKey.optional().describe("reuse the same printable-ASCII key when deliberately retrying this create")
|
|
197
252
|
},
|
|
198
253
|
handler: (ctx, args) => create(ctx, {
|
|
199
254
|
...args,
|
|
@@ -234,14 +289,14 @@ var TOOLS = [
|
|
|
234
289
|
},
|
|
235
290
|
{
|
|
236
291
|
name: "issue_claim",
|
|
237
|
-
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).",
|
|
238
293
|
readOnly: false,
|
|
239
294
|
shape: { id: identifier },
|
|
240
295
|
handler: (ctx, args) => update(ctx, args["id"], { claim: true })
|
|
241
296
|
},
|
|
242
297
|
{
|
|
243
298
|
name: "issue_close",
|
|
244
|
-
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.",
|
|
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.",
|
|
245
300
|
readOnly: false,
|
|
246
301
|
shape: {
|
|
247
302
|
ids: z.array(identifier).min(1),
|
|
@@ -665,6 +720,74 @@ var TOOLS = [
|
|
|
665
720
|
shape: {},
|
|
666
721
|
handler: (ctx) => stats(ctx)
|
|
667
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
|
+
},
|
|
668
791
|
// ── Uploads ──
|
|
669
792
|
{
|
|
670
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": {
|