@allocator-one/harness-cli 1.15.0 → 1.16.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 +192 -47
- package/dist/chunk-FQWV5MIC.js +73 -0
- package/dist/{chunk-I2S2JVCQ.js → chunk-GVTZKAXC.js} +432 -107
- package/dist/cli.js +455 -158
- package/dist/{login-page-UVUKFFTT.js → login-page-KGEJFFXR.js} +12 -5
- package/dist/{server-O7CX2ZCC.js → server-F6YRKNC2.js} +78 -17
- package/package.json +1 -1
- package/spec/openapi.json +6593 -4421
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
invokedName,
|
|
4
|
+
named
|
|
5
|
+
} from "./chunk-FQWV5MIC.js";
|
|
2
6
|
|
|
3
7
|
// src/core/login-page.ts
|
|
4
8
|
import { createHash } from "crypto";
|
|
@@ -157,8 +161,10 @@ var TERMINAL_SCRIPT = `(function () {
|
|
|
157
161
|
|
|
158
162
|
void play();
|
|
159
163
|
})();`;
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
function terminalScript(name) {
|
|
165
|
+
const script = named(TERMINAL_SCRIPT, name).replace(/\r\n/g, "\n");
|
|
166
|
+
return { script, hash: createHash("sha256").update(script).digest("base64") };
|
|
167
|
+
}
|
|
162
168
|
var PRESENTATION = {
|
|
163
169
|
success: {
|
|
164
170
|
eyebrow: "Login complete",
|
|
@@ -176,7 +182,8 @@ var PRESENTATION = {
|
|
|
176
182
|
function escapeHtml(text) {
|
|
177
183
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
178
184
|
}
|
|
179
|
-
function loginPage(message, state) {
|
|
185
|
+
function loginPage(message, state, name = invokedName()) {
|
|
186
|
+
const { script, hash } = terminalScript(name);
|
|
180
187
|
const resolvedState = Object.hasOwn(PRESENTATION, state) ? state : "waiting";
|
|
181
188
|
const { eyebrow, heading } = PRESENTATION[resolvedState];
|
|
182
189
|
const safeEyebrow = escapeHtml(eyebrow);
|
|
@@ -187,7 +194,7 @@ function loginPage(message, state) {
|
|
|
187
194
|
<html lang="en"><head>
|
|
188
195
|
<meta charset="utf-8">
|
|
189
196
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
190
|
-
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src data:; img-src data:; script-src 'sha256-${
|
|
197
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src data:; img-src data:; script-src 'sha256-${hash}'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'">
|
|
191
198
|
<meta name="color-scheme" content="light">
|
|
192
199
|
<title>${safeHeading} \xB7 Harness</title>
|
|
193
200
|
<style>
|
|
@@ -281,7 +288,7 @@ function loginPage(message, state) {
|
|
|
281
288
|
<div class="terminal-stream" data-terminal-stream aria-hidden="true"></div>
|
|
282
289
|
</div>
|
|
283
290
|
</div>
|
|
284
|
-
<script>${
|
|
291
|
+
<script>${script}</script>` : ""}
|
|
285
292
|
</aside>
|
|
286
293
|
</main>
|
|
287
294
|
</body></html>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ACTIVITY_MAX_LIMIT,
|
|
4
|
-
|
|
4
|
+
BUDGET_MAX_DOLLARS,
|
|
5
5
|
CONDITION_KINDS,
|
|
6
6
|
Client,
|
|
7
7
|
GATE_OWNER_ROLES,
|
|
@@ -11,7 +11,11 @@ import {
|
|
|
11
11
|
JOB_STATES,
|
|
12
12
|
JOB_STATUS_ALIASES,
|
|
13
13
|
MAX_IDEMPOTENCY_KEY_BYTES,
|
|
14
|
+
MCP_SURFACE,
|
|
15
|
+
OUTCOME_MAX_CHARACTERS,
|
|
14
16
|
PLAN_EXECUTORS,
|
|
17
|
+
PLAN_LIST_MAX_LIMIT,
|
|
18
|
+
PLAN_STATUSES,
|
|
15
19
|
RELATION_CREATE_TYPES,
|
|
16
20
|
RELATION_UPDATE_TYPES,
|
|
17
21
|
STEER_MAX_CHARACTERS,
|
|
@@ -59,8 +63,10 @@ import {
|
|
|
59
63
|
planCreate,
|
|
60
64
|
planEdit,
|
|
61
65
|
planHold,
|
|
66
|
+
planList,
|
|
62
67
|
planRelease,
|
|
63
68
|
planShow,
|
|
69
|
+
prepareRun,
|
|
64
70
|
procedureRun,
|
|
65
71
|
procedureSave,
|
|
66
72
|
procedureShow,
|
|
@@ -77,6 +83,7 @@ import {
|
|
|
77
83
|
search,
|
|
78
84
|
show,
|
|
79
85
|
skillsList,
|
|
86
|
+
startRun,
|
|
80
87
|
stateAdd,
|
|
81
88
|
stateDelete,
|
|
82
89
|
stateOrder,
|
|
@@ -92,7 +99,12 @@ import {
|
|
|
92
99
|
upload,
|
|
93
100
|
userList,
|
|
94
101
|
workflowStates
|
|
95
|
-
} from "./chunk-
|
|
102
|
+
} from "./chunk-GVTZKAXC.js";
|
|
103
|
+
import {
|
|
104
|
+
CLI_VERSION,
|
|
105
|
+
invokedName,
|
|
106
|
+
named
|
|
107
|
+
} from "./chunk-FQWV5MIC.js";
|
|
96
108
|
|
|
97
109
|
// src/mcp/server.ts
|
|
98
110
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -102,11 +114,11 @@ var NeedsLoginError = class extends Error {
|
|
|
102
114
|
};
|
|
103
115
|
function context() {
|
|
104
116
|
const resolved = resolveConfig();
|
|
105
|
-
for (const warning of resolved.warnings) process.stderr.write(warning + "\n");
|
|
117
|
+
for (const warning of resolved.warnings) process.stderr.write(named(warning + "\n"));
|
|
106
118
|
if (resolved.kind === "error") throw new NeedsLoginError(resolved.message);
|
|
107
119
|
if (resolved.kind === "needs-login") {
|
|
108
120
|
throw new NeedsLoginError(
|
|
109
|
-
`Not signed in to ${resolved.baseUrl}. Run
|
|
121
|
+
`Not signed in to ${resolved.baseUrl}${resolved.org === void 0 ? "" : ` for organization ${resolved.org}`}. Run \`${invokedName()} login\` in a terminal (or set HARNESS_API_TOKEN for headless use) \u2014 the MCP server never opens a browser itself.`
|
|
110
122
|
);
|
|
111
123
|
}
|
|
112
124
|
return { client: new Client(resolved.config), config: resolved.config };
|
|
@@ -122,7 +134,7 @@ async function run(work) {
|
|
|
122
134
|
const output = await work(ctx);
|
|
123
135
|
const content = [{ type: "text", text: JSON.stringify(output.json) }];
|
|
124
136
|
if (output.notice !== void 0 && output.notice !== "") {
|
|
125
|
-
content.push({ type: "text", text: output.notice.trimEnd() });
|
|
137
|
+
content.push({ type: "text", text: named(output.notice.trimEnd()) });
|
|
126
138
|
}
|
|
127
139
|
if (output.partial === true) {
|
|
128
140
|
content.push({ type: "text", text: "partial failure \u2014 inspect the JSON entries for per-item errors" });
|
|
@@ -133,10 +145,10 @@ async function run(work) {
|
|
|
133
145
|
const error = explainExpertRefusal(caught, token);
|
|
134
146
|
if (error instanceof NeedsLoginError) return toolError(error.message);
|
|
135
147
|
if (error instanceof HttpError) {
|
|
136
|
-
return toolError(`${error.message} (HTTP ${error.status})${errorDetails(error.body)}`);
|
|
148
|
+
return toolError(named(`${error.message} (HTTP ${error.status})${errorDetails(error.body)}`));
|
|
137
149
|
}
|
|
138
150
|
if (error instanceof TransportError) return toolError(`cannot reach the API: ${error.message}`);
|
|
139
|
-
if (error instanceof Error) return toolError(error.message);
|
|
151
|
+
if (error instanceof Error) return toolError(named(error.message));
|
|
140
152
|
throw error;
|
|
141
153
|
}
|
|
142
154
|
}
|
|
@@ -187,9 +199,11 @@ var planDocument = z.object({
|
|
|
187
199
|
parent: z.string().optional().describe("an existing issue to file the root under, e.g. IO-12"),
|
|
188
200
|
retry_budget: z.number().int().min(0).optional().describe("retries per failed Operator step (default 2)"),
|
|
189
201
|
spend_budget_cents: z.number().int().min(0).nullable().optional().describe("needed to release a plan with Operator steps"),
|
|
190
|
-
steps: z.array(planStep).min(1).describe(
|
|
202
|
+
steps: z.array(planStep).min(1).describe(
|
|
203
|
+
"with the root, at most the organization's max plan steps (default 1000, at most 2000), and at most 100 Operator steps"
|
|
204
|
+
)
|
|
191
205
|
}).passthrough();
|
|
192
|
-
var planId = z.string().
|
|
206
|
+
var planId = z.string().describe("the plan's UUID, or its root issue's identifier (e.g. FR-1, any casing): plan_list or issue_show's plan.root");
|
|
193
207
|
var planStepEdit = z.object({
|
|
194
208
|
step: z.string().optional().describe("the step's key; give this or issue"),
|
|
195
209
|
issue: z.string().optional().describe("the step's issue identifier; give this or step"),
|
|
@@ -258,6 +272,40 @@ var OPERATOR_CONTROL_TOOLS = [
|
|
|
258
272
|
handler: (ctx, args) => jobPause(ctx, args["id"])
|
|
259
273
|
}
|
|
260
274
|
];
|
|
275
|
+
var OPERATOR_RUN_TOOLS = [
|
|
276
|
+
{
|
|
277
|
+
name: "job_start",
|
|
278
|
+
description: `Start an Operator job from an outcome, as Run as job does in Chat Mode in the app: the request is posted, attributed to the caller, in a new organization chat of theirs, and Operator takes it from there under the caller's own current rights. Returns job_id, state, workbench_url and, when a change set already awaits approval, approval_url. Approval happens in the app: give a person the approval_url; this tool never approves anything, and no tool does. Each call starts another job, so never call it again to "retry": not after a timeout, a dropped connection or a 5xx either. An "Outcome unknown" error means a job may have been started; look for it with jobs_list (mine) before calling again. Only the organization's admins may pass a budget. Follow the job with job_show.`,
|
|
279
|
+
readOnly: false,
|
|
280
|
+
destructive: false,
|
|
281
|
+
idempotent: false,
|
|
282
|
+
shape: {
|
|
283
|
+
outcome: z.string().describe(
|
|
284
|
+
`what the job should achieve, in plain language: 1 to ${OUTCOME_MAX_CHARACTERS} characters once trimmed`
|
|
285
|
+
),
|
|
286
|
+
budget: z.number().positive().max(BUDGET_MAX_DOLLARS).optional().describe(
|
|
287
|
+
`the job's budget in US dollars, at most two decimals, up to ${BUDGET_MAX_DOLLARS}; only the organization's admins may pass one`
|
|
288
|
+
),
|
|
289
|
+
skill: z.string().optional().describe("an Operator skill's invocation name, like /summarize-lps (skills_list names them)")
|
|
290
|
+
},
|
|
291
|
+
handler: async (ctx, args) => {
|
|
292
|
+
const request = prepareRun(args["outcome"], {
|
|
293
|
+
budget: args["budget"] === void 0 ? void 0 : String(args["budget"]),
|
|
294
|
+
skill: args["skill"]
|
|
295
|
+
}, MCP_SURFACE);
|
|
296
|
+
const job = await startRun(ctx, request, MCP_SURFACE);
|
|
297
|
+
return {
|
|
298
|
+
json: {
|
|
299
|
+
job_id: job.job_id,
|
|
300
|
+
state: job.state,
|
|
301
|
+
workbench_url: job.workbench_url,
|
|
302
|
+
...job.approval_url === null ? {} : { approval_url: job.approval_url }
|
|
303
|
+
},
|
|
304
|
+
human: `Started Operator job ${job.job_id}.`
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
];
|
|
261
309
|
var NOT_IDEMPOTENT = 'Not idempotent: never retry it automatically, not after a timeout or a 5xx either. An "Outcome unknown" error may have landed and says what to check before calling again; only "cannot reach the API" means the request never reached the server, so nothing changed.';
|
|
262
310
|
var OPERATOR_LIFECYCLE_TOOLS = [
|
|
263
311
|
{
|
|
@@ -363,7 +411,7 @@ var TOOLS = [
|
|
|
363
411
|
},
|
|
364
412
|
{
|
|
365
413
|
name: "issue_show",
|
|
366
|
-
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.",
|
|
414
|
+
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. plan is the plan governing the issue (the nearest rooted at it or an ancestor), or null: its id, root identifier (which the plan tools take), status, held, the revision number it runs, in_revision (whether that revision lists the issue: an Operator step outside it waits for plan_release, a person's step does not) and the issue's executor. images=true downloads attached images to the local cache and includes their paths as local_images.",
|
|
367
415
|
readOnly: true,
|
|
368
416
|
shape: { id: identifier, images: z.boolean().optional() },
|
|
369
417
|
handler: (ctx, args) => show(ctx, args["id"], { images: args["images"] === true })
|
|
@@ -437,7 +485,7 @@ var TOOLS = [
|
|
|
437
485
|
},
|
|
438
486
|
{
|
|
439
487
|
name: "issue_claim",
|
|
440
|
-
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).",
|
|
488
|
+
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). A refusal that comes from a plan (agent_step, plan_not_released, not_in_plan_revision, edited_since_release) names the plan by its root: the release that fixes it when it is the one plan governing the issue (plan_release takes the root), the nearest plan to look at when several do. A person never claims an agent_step, an Operator step.",
|
|
441
489
|
readOnly: false,
|
|
442
490
|
shape: { id: identifier },
|
|
443
491
|
handler: (ctx, args) => update(ctx, args["id"], { claim: true })
|
|
@@ -499,7 +547,7 @@ var TOOLS = [
|
|
|
499
547
|
},
|
|
500
548
|
{
|
|
501
549
|
name: "comment_list",
|
|
502
|
-
description: "An issue's
|
|
550
|
+
description: "An issue's complete comment thread, oldest first. Follows every older page; pagination failures return an error, not a partial thread.",
|
|
503
551
|
readOnly: true,
|
|
504
552
|
shape: { id: identifier },
|
|
505
553
|
handler: (ctx, args) => comments(ctx, args["id"])
|
|
@@ -869,9 +917,20 @@ var TOOLS = [
|
|
|
869
917
|
handler: (ctx) => stats(ctx)
|
|
870
918
|
},
|
|
871
919
|
// ── Plans and gates ──
|
|
920
|
+
{
|
|
921
|
+
name: "plan_list",
|
|
922
|
+
description: `A team's plans, newest first: each plan's id, root (its root issue's identifier, which the other plan tools take in place of the id), title, status, held, revision (null before its first release), released_at and inserted_at. One list of at most ${PLAN_LIST_MAX_LIMIT}, no pages: a second text item says when it came back full. issue_show's plan names the plan an issue belongs to.`,
|
|
923
|
+
readOnly: true,
|
|
924
|
+
shape: {
|
|
925
|
+
team: z.string().optional().describe("team key whose issues root the plans; defaults to HARNESS_TEAM"),
|
|
926
|
+
status: z.enum(PLAN_STATUSES).optional().describe("only plans in this status"),
|
|
927
|
+
limit: z.number().int().min(1).max(PLAN_LIST_MAX_LIMIT).optional().describe("maximum plans (default 50)")
|
|
928
|
+
},
|
|
929
|
+
handler: (ctx, args) => planList(ctx, { team: args["team"], status: args["status"], limit: args["limit"] })
|
|
930
|
+
},
|
|
872
931
|
{
|
|
873
932
|
name: "plan_create",
|
|
874
|
-
description: "Create a plan: a root issue and
|
|
933
|
+
description: "Create a plan: a root issue and its 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); a gate may carry a key (gate: <key> is shorthand) and an owner, who alone ticks it. An invalid plan writes nothing and the error lists every violation by step key; too_many_steps says which cap the plan is over: the organization's max plan steps (default 1000, which a Harness admin raises in Harness settings \u2192 Plans, up to 2000) or the fixed 100 Operator steps of the delegated grant. Reuse idempotency_key when retrying. Once released, the changes an Operator step proposes need a person's approval in the app, as plan_release says.",
|
|
875
934
|
readOnly: false,
|
|
876
935
|
shape: {
|
|
877
936
|
team: z.string().optional().describe("team key for the root and people's steps; defaults to HARNESS_TEAM"),
|
|
@@ -882,14 +941,14 @@ var TOOLS = [
|
|
|
882
941
|
},
|
|
883
942
|
{
|
|
884
943
|
name: "plan_show",
|
|
885
|
-
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 (
|
|
944
|
+
description: "A plan, by its id or its root issue's identifier: status, budgets, current revision and spend estimate, and its whole tree root first: each step's key, executor, parent, state, ready flag, held flag (changed since the release: only an Operator step waits for the next release, a person's stays ready), in_revision, wait conditions (a gate's key and owner) and waiting_for (each open gate's owner and holders, the people who can tick it: [] when nobody can, null when unowned or not known), plus the blocks edges between steps.",
|
|
886
945
|
readOnly: true,
|
|
887
946
|
shape: { id: planId },
|
|
888
947
|
handler: (ctx, args) => planShow(ctx, args["id"])
|
|
889
948
|
},
|
|
890
949
|
{
|
|
891
950
|
name: "plan_edit",
|
|
892
|
-
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.",
|
|
951
|
+
description: "Change step executors and keys (each step named by its key or issue identifier) and the plan's budgets. Editing a released Operator step, or handing one to a person, holds it until the plan is released again; a person's step stays workable, and the next release takes the change in. 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. Once released, the changes an Operator step proposes need a person's approval in the app, as plan_release says.",
|
|
893
952
|
readOnly: false,
|
|
894
953
|
shape: {
|
|
895
954
|
id: planId,
|
|
@@ -904,7 +963,7 @@ var TOOLS = [
|
|
|
904
963
|
},
|
|
905
964
|
{
|
|
906
965
|
name: "plan_release",
|
|
907
|
-
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.",
|
|
966
|
+
description: "Release a held plan (or one that needs attention) as the caller: freezes its next revision, which takes in the steps added or edited since the last one, and lets its ready steps run. A plan over a step cap is refused with too_many_steps, naming the cap as plan_create does. A plan with Operator steps needs a spend budget and the caller's right to run Operator; collaborators join its plan room. Each Operator step runs as a job (jobs_list names its plan_step), and the changes a job proposes need a person's approval in the app: give them the job's approval_url. No tool approves.",
|
|
908
967
|
readOnly: false,
|
|
909
968
|
shape: {
|
|
910
969
|
id: planId,
|
|
@@ -927,7 +986,7 @@ var TOOLS = [
|
|
|
927
986
|
},
|
|
928
987
|
{
|
|
929
988
|
name: "gate_set",
|
|
930
|
-
description: "Satisfy (satisfied true) or un-satisfy (false) a gate wait condition on an issue, found by its key, label or condition id (condition_list and issue_show list conditions). Needs staff standing in the organization or a place in its plan's room; only an owned gate's owner ticks it, and its owner or staff untick it. Anyone else gets a 403 naming the owner. An outside expert's pxc_ credential ticks the gates it owns on a step assigned to it (assignment_list); any other step is 404.",
|
|
989
|
+
description: "Satisfy (satisfied true) or un-satisfy (false) a gate wait condition on an issue, found by its key, label or condition id (condition_list and issue_show list conditions). A condition id writes directly, without a staff-only conditions lookup; a key or label needs that lookup. Needs staff standing in the organization or a place in its plan's room; only an owned gate's owner ticks it, and its owner or staff untick it. Anyone else gets a 403 naming the owner. An outside expert's pxc_ credential ticks the gates it owns on a step assigned to it (assignment_list); any other step is 404. An Operator-only login (operator-cli) is refused here, as on every tool outside Operator's, even for a person in the plan's room.",
|
|
931
990
|
readOnly: false,
|
|
932
991
|
shape: {
|
|
933
992
|
issue: identifier,
|
|
@@ -954,6 +1013,8 @@ var TOOLS = [
|
|
|
954
1013
|
...OPERATOR_JOB_TOOLS,
|
|
955
1014
|
// ── Operator job controls (CLI-46) ──
|
|
956
1015
|
...OPERATOR_CONTROL_TOOLS,
|
|
1016
|
+
// ── Operator job start (CLI-45) ──
|
|
1017
|
+
...OPERATOR_RUN_TOOLS,
|
|
957
1018
|
// ── Operator job lifecycle (CLI-47) ──
|
|
958
1019
|
...OPERATOR_LIFECYCLE_TOOLS,
|
|
959
1020
|
// ── Uploads ──
|