@allocator-one/harness-cli 1.14.0 → 1.15.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 +585 -26
- package/dist/chunk-FQWV5MIC.js +73 -0
- package/dist/{chunk-F2YVTB2A.js → chunk-L2OWOZ4N.js} +2362 -288
- package/dist/cli.js +533 -93
- package/dist/{login-page-UVUKFFTT.js → login-page-KGEJFFXR.js} +12 -5
- package/dist/{server-75QJASTY.js → server-KVRQBVXI.js} +264 -24
- package/package.json +2 -1
- package/spec/openapi.json +13100 -3920
|
@@ -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,23 +1,32 @@
|
|
|
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
|
+
GATE_OWNER_ROLES,
|
|
7
8
|
HttpError,
|
|
8
9
|
IDEMPOTENCY_KEY_FORMAT,
|
|
10
|
+
JOBS_MAX_LIMIT,
|
|
11
|
+
JOB_STATES,
|
|
12
|
+
JOB_STATUS_ALIASES,
|
|
9
13
|
MAX_IDEMPOTENCY_KEY_BYTES,
|
|
14
|
+
MCP_SURFACE,
|
|
15
|
+
OUTCOME_MAX_CHARACTERS,
|
|
10
16
|
PLAN_EXECUTORS,
|
|
11
17
|
RELATION_CREATE_TYPES,
|
|
12
18
|
RELATION_UPDATE_TYPES,
|
|
19
|
+
STEER_MAX_CHARACTERS,
|
|
13
20
|
TransportError,
|
|
14
21
|
UsageError,
|
|
15
22
|
activity,
|
|
16
23
|
archive,
|
|
24
|
+
assignments,
|
|
17
25
|
blocked,
|
|
18
26
|
close,
|
|
19
27
|
comment,
|
|
20
28
|
comments,
|
|
29
|
+
conditionList,
|
|
21
30
|
create,
|
|
22
31
|
deleteComment,
|
|
23
32
|
dep,
|
|
@@ -27,7 +36,14 @@ import {
|
|
|
27
36
|
docUpdate,
|
|
28
37
|
editComment,
|
|
29
38
|
errorDetails,
|
|
39
|
+
explainExpertRefusal,
|
|
30
40
|
gateSet,
|
|
41
|
+
jobCancel,
|
|
42
|
+
jobContinue,
|
|
43
|
+
jobPause,
|
|
44
|
+
jobShow,
|
|
45
|
+
jobSteer,
|
|
46
|
+
jobsList,
|
|
31
47
|
labelCreate,
|
|
32
48
|
labelDelete,
|
|
33
49
|
labelList,
|
|
@@ -47,6 +63,10 @@ import {
|
|
|
47
63
|
planHold,
|
|
48
64
|
planRelease,
|
|
49
65
|
planShow,
|
|
66
|
+
prepareRun,
|
|
67
|
+
procedureRun,
|
|
68
|
+
procedureSave,
|
|
69
|
+
procedureShow,
|
|
50
70
|
projectCreate,
|
|
51
71
|
projectList,
|
|
52
72
|
projectShow,
|
|
@@ -59,6 +79,8 @@ import {
|
|
|
59
79
|
resolveConfig,
|
|
60
80
|
search,
|
|
61
81
|
show,
|
|
82
|
+
skillsList,
|
|
83
|
+
startRun,
|
|
62
84
|
stateAdd,
|
|
63
85
|
stateDelete,
|
|
64
86
|
stateOrder,
|
|
@@ -68,11 +90,18 @@ import {
|
|
|
68
90
|
teamList,
|
|
69
91
|
teamRename,
|
|
70
92
|
teamSetDefault,
|
|
93
|
+
toolsList,
|
|
94
|
+
toolsSearch,
|
|
71
95
|
update,
|
|
72
96
|
upload,
|
|
73
97
|
userList,
|
|
74
98
|
workflowStates
|
|
75
|
-
} from "./chunk-
|
|
99
|
+
} from "./chunk-L2OWOZ4N.js";
|
|
100
|
+
import {
|
|
101
|
+
CLI_VERSION,
|
|
102
|
+
invokedName,
|
|
103
|
+
named
|
|
104
|
+
} from "./chunk-FQWV5MIC.js";
|
|
76
105
|
|
|
77
106
|
// src/mcp/server.ts
|
|
78
107
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -82,11 +111,11 @@ var NeedsLoginError = class extends Error {
|
|
|
82
111
|
};
|
|
83
112
|
function context() {
|
|
84
113
|
const resolved = resolveConfig();
|
|
85
|
-
for (const warning of resolved.warnings) process.stderr.write(warning + "\n");
|
|
114
|
+
for (const warning of resolved.warnings) process.stderr.write(named(warning + "\n"));
|
|
86
115
|
if (resolved.kind === "error") throw new NeedsLoginError(resolved.message);
|
|
87
116
|
if (resolved.kind === "needs-login") {
|
|
88
117
|
throw new NeedsLoginError(
|
|
89
|
-
`Not signed in to ${resolved.baseUrl}. Run
|
|
118
|
+
`Not signed in to ${resolved.baseUrl}. Run \`${invokedName()} login\` in a terminal (or set HARNESS_API_TOKEN for headless use) \u2014 the MCP server never opens a browser itself.`
|
|
90
119
|
);
|
|
91
120
|
}
|
|
92
121
|
return { client: new Client(resolved.config), config: resolved.config };
|
|
@@ -95,24 +124,28 @@ function toolError(message) {
|
|
|
95
124
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
96
125
|
}
|
|
97
126
|
async function run(work) {
|
|
127
|
+
let token = "";
|
|
98
128
|
try {
|
|
99
|
-
const
|
|
129
|
+
const ctx = context();
|
|
130
|
+
token = ctx.config.token;
|
|
131
|
+
const output = await work(ctx);
|
|
100
132
|
const content = [{ type: "text", text: JSON.stringify(output.json) }];
|
|
101
133
|
if (output.notice !== void 0 && output.notice !== "") {
|
|
102
|
-
content.push({ type: "text", text: output.notice.trimEnd() });
|
|
134
|
+
content.push({ type: "text", text: named(output.notice.trimEnd()) });
|
|
103
135
|
}
|
|
104
136
|
if (output.partial === true) {
|
|
105
137
|
content.push({ type: "text", text: "partial failure \u2014 inspect the JSON entries for per-item errors" });
|
|
106
138
|
return { content, isError: true };
|
|
107
139
|
}
|
|
108
140
|
return { content };
|
|
109
|
-
} catch (
|
|
141
|
+
} catch (caught) {
|
|
142
|
+
const error = explainExpertRefusal(caught, token);
|
|
110
143
|
if (error instanceof NeedsLoginError) return toolError(error.message);
|
|
111
144
|
if (error instanceof HttpError) {
|
|
112
|
-
return toolError(`${error.message} (HTTP ${error.status})${errorDetails(error.body)}`);
|
|
145
|
+
return toolError(named(`${error.message} (HTTP ${error.status})${errorDetails(error.body)}`));
|
|
113
146
|
}
|
|
114
147
|
if (error instanceof TransportError) return toolError(`cannot reach the API: ${error.message}`);
|
|
115
|
-
if (error instanceof Error) return toolError(error.message);
|
|
148
|
+
if (error instanceof Error) return toolError(named(error.message));
|
|
116
149
|
throw error;
|
|
117
150
|
}
|
|
118
151
|
}
|
|
@@ -125,9 +158,14 @@ var identifier = z.string().describe("issue identifier, e.g. IO-123");
|
|
|
125
158
|
var commentId = z.string().uuid().describe("comment UUID from issue_show or comment_list");
|
|
126
159
|
var dateField = z.string().optional().describe('YYYY-MM-DD; "" clears the date');
|
|
127
160
|
var planCondition = z.object({
|
|
128
|
-
kind: z.enum(CONDITION_KINDS),
|
|
161
|
+
kind: z.enum(CONDITION_KINDS).optional().describe("not_before, gate or event; optional with gate"),
|
|
129
162
|
not_before: z.string().optional().describe("not_before: an ISO 8601 UTC instant"),
|
|
130
|
-
|
|
163
|
+
gate: z.string().optional().describe("shorthand for kind gate with this key; the label defaults to it, e.g. finance-sign-off"),
|
|
164
|
+
key: z.string().optional().describe("gate: a stable lowercase slug (a-z, 0-9 and -), unique among the step's gates; gate_set takes it"),
|
|
165
|
+
label: z.string().optional().describe("gate: what a person ticks, e.g. Finance sign-off; defaults to the key"),
|
|
166
|
+
owner: z.string().nullable().optional().describe(
|
|
167
|
+
`gate: who ticks it. role:<role> (${GATE_OWNER_ROLES.join(", ")}), user:<id or email> (a member who can use Operator) or expert:<email> (an outside expert, never a member's email). Only the owner ticks an owned gate, and nobody names an owner they would hold themselves (owner_is_actor). null or absent: no owner`
|
|
168
|
+
),
|
|
131
169
|
event: z.string().optional().describe("event: deal_stage_changed, opportunity_stage_changed, form_submitted or investment_executed"),
|
|
132
170
|
subject_id: z.string().optional().describe("event: the record the event is about (one you can read)"),
|
|
133
171
|
target_id: z.string().optional().describe("event: the stage or status a stage change must reach")
|
|
@@ -168,6 +206,159 @@ var planStepEdit = z.object({
|
|
|
168
206
|
key: z.string().nullable().optional().describe("the step's new key")
|
|
169
207
|
}).passthrough();
|
|
170
208
|
var idempotencyKey = z.string().min(1).max(MAX_IDEMPOTENCY_KEY_BYTES).regex(IDEMPOTENCY_KEY_FORMAT, "must use printable ASCII without leading or trailing spaces");
|
|
209
|
+
var OPERATOR_APPROVAL_NOTE = "Approval happens in the app: give a person the approval_url (pending_approval.url on job_show); no tool approves a change set. By reference only, never the objective's text.";
|
|
210
|
+
var OPERATOR_JOB_TOOLS = [
|
|
211
|
+
{
|
|
212
|
+
name: "jobs_list",
|
|
213
|
+
description: "List the Operator jobs the caller sees in the workbench, newest first: state, waiting_on (approval, person, reconciliation, verification or delivery), objective_kind, owner, plan step, window, and the workbench url and approval_url. " + OPERATOR_APPROVAL_NOTE + " Pages by cursor; all follows every page, and a second text item says when older jobs exist.",
|
|
214
|
+
readOnly: true,
|
|
215
|
+
shape: {
|
|
216
|
+
status: z.array(z.enum([...JOB_STATES, ...JOB_STATUS_ALIASES])).min(1).optional().describe("job states, or the aliases open (not done), waiting and done"),
|
|
217
|
+
mine: z.boolean().optional().describe("only jobs the caller owns"),
|
|
218
|
+
limit: z.number().int().min(1).max(JOBS_MAX_LIMIT).optional().describe("jobs per page (default 20)"),
|
|
219
|
+
all: z.boolean().optional().describe("follow every page")
|
|
220
|
+
},
|
|
221
|
+
handler: (ctx, args) => jobsList(ctx, {
|
|
222
|
+
status: args["status"] === void 0 ? void 0 : args["status"].join(","),
|
|
223
|
+
mine: args["mine"],
|
|
224
|
+
limit: args["limit"],
|
|
225
|
+
all: args["all"]
|
|
226
|
+
})
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: "job_show",
|
|
230
|
+
description: "One Operator job by reference: state and waiting_on, plan version, progress (stage, situation, needs_viewer), window (remaining and budget, in ms), spend in cents, blockers, pending_approval with its url, grant, verification status, effect counts, artifact links, pinned skill identity, people, saved procedures and viewer_can. " + OPERATOR_APPROVAL_NOTE + " Poll it to follow a job: it has ended when state is completed, failed or cancelled, and a person has to act exactly when waiting_on is set; a job paused with no waiting_on resumes by itself.",
|
|
231
|
+
readOnly: true,
|
|
232
|
+
shape: { id: z.string().uuid().describe("Operator job UUID, from jobs_list") },
|
|
233
|
+
handler: (ctx, args) => jobShow(ctx, args["id"])
|
|
234
|
+
}
|
|
235
|
+
];
|
|
236
|
+
var OPERATOR_CONTROL_NOTE = "Not idempotent: never retry it on your own; when an error says the outcome is unknown (a timeout, a dropped connection or a 5xx), read the job with job_show before trying again. Approving stays in the app: no tool approves a change set. Returns the job as job_show does, by reference only.";
|
|
237
|
+
var jobId = z.string().uuid().describe("Operator job UUID, from jobs_list");
|
|
238
|
+
var OPERATOR_CONTROL_TOOLS = [
|
|
239
|
+
{
|
|
240
|
+
name: "job_steer",
|
|
241
|
+
description: "Steer an Operator job as Steer does in the app: posts the instruction in the job's room, attributed to the caller. The job's owner and collaborators may steer, wherever the app offers it (not a finished, failed or awaiting-approval job). Work the instruction changes needs a fresh approval: earlier approvals stop applying to it. Steering a running job stops its current run so it can take the instruction. " + OPERATOR_CONTROL_NOTE,
|
|
242
|
+
readOnly: false,
|
|
243
|
+
idempotent: false,
|
|
244
|
+
shape: {
|
|
245
|
+
id: jobId,
|
|
246
|
+
instruction: z.string().min(1).describe(
|
|
247
|
+
`what to change, in plain language: 1 to ${STEER_MAX_CHARACTERS} characters once trimmed`
|
|
248
|
+
)
|
|
249
|
+
},
|
|
250
|
+
handler: (ctx, args) => jobSteer(ctx, args["id"], args["instruction"])
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: "job_cancel",
|
|
254
|
+
description: "Cancel an Operator job as Cancel does in the app; only the job's execution principal may. It cannot be undone, but committed effects stay committed: the job's effects still count them, and staged ones that never ran count as unknown. " + OPERATOR_CONTROL_NOTE,
|
|
255
|
+
readOnly: false,
|
|
256
|
+
destructive: true,
|
|
257
|
+
idempotent: false,
|
|
258
|
+
shape: { id: jobId },
|
|
259
|
+
handler: (ctx, args) => jobCancel(ctx, args["id"])
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: "job_pause",
|
|
263
|
+
description: "Pause a running Operator job as Pause does in the app; only the job's execution principal may. Holds it for a person without resetting its active-time budget; continuing it runs it again. " + OPERATOR_CONTROL_NOTE,
|
|
264
|
+
readOnly: false,
|
|
265
|
+
idempotent: false,
|
|
266
|
+
shape: { id: jobId },
|
|
267
|
+
handler: (ctx, args) => jobPause(ctx, args["id"])
|
|
268
|
+
}
|
|
269
|
+
];
|
|
270
|
+
var OPERATOR_RUN_TOOLS = [
|
|
271
|
+
{
|
|
272
|
+
name: "job_start",
|
|
273
|
+
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.`,
|
|
274
|
+
readOnly: false,
|
|
275
|
+
destructive: false,
|
|
276
|
+
idempotent: false,
|
|
277
|
+
shape: {
|
|
278
|
+
outcome: z.string().describe(
|
|
279
|
+
`what the job should achieve, in plain language: 1 to ${OUTCOME_MAX_CHARACTERS} characters once trimmed`
|
|
280
|
+
),
|
|
281
|
+
budget: z.number().positive().max(BUDGET_MAX_DOLLARS).optional().describe(
|
|
282
|
+
`the job's budget in US dollars, at most two decimals, up to ${BUDGET_MAX_DOLLARS}; only the organization's admins may pass one`
|
|
283
|
+
),
|
|
284
|
+
skill: z.string().optional().describe("an Operator skill's invocation name, like /summarize-lps (skills_list names them)")
|
|
285
|
+
},
|
|
286
|
+
handler: async (ctx, args) => {
|
|
287
|
+
const request = prepareRun(args["outcome"], {
|
|
288
|
+
budget: args["budget"] === void 0 ? void 0 : String(args["budget"]),
|
|
289
|
+
skill: args["skill"]
|
|
290
|
+
}, MCP_SURFACE);
|
|
291
|
+
const job = await startRun(ctx, request, MCP_SURFACE);
|
|
292
|
+
return {
|
|
293
|
+
json: {
|
|
294
|
+
job_id: job.job_id,
|
|
295
|
+
state: job.state,
|
|
296
|
+
workbench_url: job.workbench_url,
|
|
297
|
+
...job.approval_url === null ? {} : { approval_url: job.approval_url }
|
|
298
|
+
},
|
|
299
|
+
human: `Started Operator job ${job.job_id}.`
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
];
|
|
304
|
+
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.';
|
|
305
|
+
var OPERATOR_LIFECYCLE_TOOLS = [
|
|
306
|
+
{
|
|
307
|
+
name: "job_continue",
|
|
308
|
+
description: "Continue an Operator job, as Continue does in the app: opens a new audited window for a job that ran out of time or waits on a person, or retries a failed run or verification. Only the job's execution principal may. Returns the job as job_show does. Approval still happens in the app, at its approval_url. " + NOT_IDEMPOTENT,
|
|
309
|
+
readOnly: false,
|
|
310
|
+
destructive: false,
|
|
311
|
+
idempotent: false,
|
|
312
|
+
shape: { id: z.string().uuid().describe("Operator job UUID, from jobs_list") },
|
|
313
|
+
handler: (ctx, args) => jobContinue(ctx, args["id"])
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: "job_changes",
|
|
317
|
+
description: "An Operator job's latest change set by reference: its manifest id, version, status, digest, plan version, maximum effect count, and each action's type, version, order, effect count, required capability ids and digest; never values or targets. change_set is null while the job has proposed nothing. Beside it, the job's state, waiting_on, workbench url and approval_url: review and approve in the app, at that link.",
|
|
318
|
+
readOnly: true,
|
|
319
|
+
shape: { id: z.string().uuid().describe("Operator job UUID, from jobs_list") },
|
|
320
|
+
handler: (ctx, args) => jobShow(ctx, args["id"], { changes: true })
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: "procedure_save",
|
|
324
|
+
description: "Save a completed Operator job whose work was verified as a versioned procedure, as Save procedure does in the app. Only the job's owner can. Returns the procedure as procedure_show does, or only its id and url when it can't be read back. Saving again saves another procedure. " + NOT_IDEMPOTENT,
|
|
325
|
+
readOnly: false,
|
|
326
|
+
destructive: false,
|
|
327
|
+
idempotent: false,
|
|
328
|
+
shape: {
|
|
329
|
+
job_id: z.string().uuid().describe("Operator job UUID, from jobs_list"),
|
|
330
|
+
name: z.string().trim().min(1).describe("the procedure's name; trimmed and cut to 200 characters")
|
|
331
|
+
},
|
|
332
|
+
handler: (ctx, args) => procedureSave(ctx, args["job_id"], args["name"])
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: "procedure_show",
|
|
336
|
+
description: "One saved Operator procedure: its name, source job_id and job_url (the app), current_version, each version's recipe_digest, capability_ids and action_roster, and the parameter_schema a run of the current version takes (scoped_inputs.required naming contact_reconciliation means a run needs group_id and source_resource_id). Never a version's recipe or anything the saved job read.",
|
|
337
|
+
readOnly: true,
|
|
338
|
+
shape: { id: z.string().uuid().describe("Operator procedure UUID, from job_show's procedures") },
|
|
339
|
+
handler: (ctx, args) => procedureShow(ctx, args["id"])
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
name: "procedure_run",
|
|
343
|
+
description: "Run a saved Operator procedure as a new job, as Rerun does in the app: fresh inputs, the caller's own current rights, and a change set that needs its own approval in the app. Nothing of the saved job carries over. Returns the new job as job_show does, or only its id and url when it can't be read back; give a person its approval_url. " + NOT_IDEMPOTENT,
|
|
344
|
+
readOnly: false,
|
|
345
|
+
destructive: false,
|
|
346
|
+
idempotent: false,
|
|
347
|
+
shape: {
|
|
348
|
+
id: z.string().uuid().describe("Operator procedure UUID"),
|
|
349
|
+
version: z.number().int().min(1).optional().describe("the version expected to be current; the run is refused if it no longer is (default: the current one)"),
|
|
350
|
+
parameters: z.record(z.unknown()).optional().describe("values for the procedure's parameter_schema (procedure_show), default {}"),
|
|
351
|
+
group_id: z.string().min(1).optional().describe("the contact group to reconcile, when scoped_inputs require it"),
|
|
352
|
+
source_resource_id: z.string().min(1).optional().describe("the uploaded file to reconcile against, when scoped_inputs require it")
|
|
353
|
+
},
|
|
354
|
+
handler: (ctx, args) => procedureRun(ctx, args["id"], {
|
|
355
|
+
version: args["version"],
|
|
356
|
+
parameters: args["parameters"],
|
|
357
|
+
group: args["group_id"],
|
|
358
|
+
source: args["source_resource_id"]
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
];
|
|
171
362
|
var TOOLS = [
|
|
172
363
|
// ── Issues ──
|
|
173
364
|
{
|
|
@@ -222,7 +413,7 @@ var TOOLS = [
|
|
|
222
413
|
},
|
|
223
414
|
{
|
|
224
415
|
name: "activity_list",
|
|
225
|
-
description: "Mixed issue activity: comments, creation, state and assignee changes. Returns the newest page unless all=true.",
|
|
416
|
+
description: "Mixed issue activity: comments, creation, state and assignee changes, wait-condition events, and field changes (priority, labels, parent, title, description, archived) with from/to values. actor_label, when set, is the agent or CLI label a write was made under. Returns the newest page unless all=true.",
|
|
226
417
|
readOnly: true,
|
|
227
418
|
shape: {
|
|
228
419
|
id: identifier,
|
|
@@ -351,7 +542,7 @@ var TOOLS = [
|
|
|
351
542
|
},
|
|
352
543
|
{
|
|
353
544
|
name: "comment_list",
|
|
354
|
-
description: "An issue's
|
|
545
|
+
description: "An issue's complete comment thread, oldest first. Follows every older page; pagination failures return an error, not a partial thread.",
|
|
355
546
|
readOnly: true,
|
|
356
547
|
shape: { id: identifier },
|
|
357
548
|
handler: (ctx, args) => comments(ctx, args["id"])
|
|
@@ -723,7 +914,7 @@ var TOOLS = [
|
|
|
723
914
|
// ── Plans and gates ──
|
|
724
915
|
{
|
|
725
916
|
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.",
|
|
917
|
+
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); 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. 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.",
|
|
727
918
|
readOnly: false,
|
|
728
919
|
shape: {
|
|
729
920
|
team: z.string().optional().describe("team key for the root and people's steps; defaults to HARNESS_TEAM"),
|
|
@@ -734,14 +925,14 @@ var TOOLS = [
|
|
|
734
925
|
},
|
|
735
926
|
{
|
|
736
927
|
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
|
|
928
|
+
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, 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.",
|
|
738
929
|
readOnly: true,
|
|
739
930
|
shape: { id: planId },
|
|
740
931
|
handler: (ctx, args) => planShow(ctx, args["id"])
|
|
741
932
|
},
|
|
742
933
|
{
|
|
743
934
|
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.",
|
|
935
|
+
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. Once released, the changes an Operator step proposes need a person's approval in the app, as plan_release says.",
|
|
745
936
|
readOnly: false,
|
|
746
937
|
shape: {
|
|
747
938
|
id: planId,
|
|
@@ -756,7 +947,7 @@ var TOOLS = [
|
|
|
756
947
|
},
|
|
757
948
|
{
|
|
758
949
|
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.",
|
|
950
|
+
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. 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.",
|
|
760
951
|
readOnly: false,
|
|
761
952
|
shape: {
|
|
762
953
|
id: planId,
|
|
@@ -779,15 +970,37 @@ var TOOLS = [
|
|
|
779
970
|
},
|
|
780
971
|
{
|
|
781
972
|
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
|
|
973
|
+
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. 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.",
|
|
783
974
|
readOnly: false,
|
|
784
975
|
shape: {
|
|
785
976
|
issue: identifier,
|
|
786
|
-
gate: z.string().describe("the gate's label, or its condition id"),
|
|
977
|
+
gate: z.string().describe("the gate's key, its label, or its condition id"),
|
|
787
978
|
satisfied: z.boolean()
|
|
788
979
|
},
|
|
789
980
|
handler: (ctx, args) => gateSet(ctx, args["issue"], args["gate"], args["satisfied"])
|
|
790
981
|
},
|
|
982
|
+
{
|
|
983
|
+
name: "condition_list",
|
|
984
|
+
description: "An issue's wait conditions (not_before, gate, event), oldest first: each one's id, kind, a gate's key, label and owner, and whether, when and by whom it was satisfied. gate_set ticks a gate. With an outside expert's pxc_ credential: only the gates it owns on a step assigned to it, in the expert view (owner as {kind, role}, satisfied_by as {kind: you|role|expert|member}); any other step is 404.",
|
|
985
|
+
readOnly: true,
|
|
986
|
+
shape: { issue: identifier },
|
|
987
|
+
handler: (ctx, args) => conditionList(ctx, args["issue"])
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
name: "assignment_list",
|
|
991
|
+
description: "An outside expert's own assignments, for their pxc_ credential: expert ({id, role}), plan ({id, status} or null), and assignments, each step assigned to them with its identifier, title, state, due_date, description (null when Operator runs the step), open_blockers (a count) and the gates on it they own. gate_set ticks one. A pxc_ credential reaches only this, condition_list and gate_set: every other tool is refused as expert_bound_credential. Any other credential is refused as expert_credential_required.",
|
|
992
|
+
readOnly: true,
|
|
993
|
+
shape: {},
|
|
994
|
+
handler: (ctx) => assignments(ctx)
|
|
995
|
+
},
|
|
996
|
+
// ── Operator jobs (CLI-43) ──
|
|
997
|
+
...OPERATOR_JOB_TOOLS,
|
|
998
|
+
// ── Operator job controls (CLI-46) ──
|
|
999
|
+
...OPERATOR_CONTROL_TOOLS,
|
|
1000
|
+
// ── Operator job start (CLI-45) ──
|
|
1001
|
+
...OPERATOR_RUN_TOOLS,
|
|
1002
|
+
// ── Operator job lifecycle (CLI-47) ──
|
|
1003
|
+
...OPERATOR_LIFECYCLE_TOOLS,
|
|
791
1004
|
// ── Uploads ──
|
|
792
1005
|
{
|
|
793
1006
|
name: "upload",
|
|
@@ -795,25 +1008,52 @@ var TOOLS = [
|
|
|
795
1008
|
readOnly: false,
|
|
796
1009
|
shape: { path: z.string().describe("local file path") },
|
|
797
1010
|
handler: (ctx, args) => upload(ctx, args["path"])
|
|
1011
|
+
},
|
|
1012
|
+
// ── Operator's catalog (CLI-44) ──
|
|
1013
|
+
{
|
|
1014
|
+
name: "tools_search",
|
|
1015
|
+
description: "Browse or search what Operator can use now in this organization: only capabilities that are switched on appear. Without query, the index in id order (25 a page); with query, a keyword search ranked by score (15 a page). domain narrows either to one domain. Each capability carries its domain, summary, description, mode, effect and kind: read, write, or send (a write delivered beyond the platform). Write and send capabilities, and any proposal, always need a person's approval in the app before they take effect; nothing here runs or approves anything. A page that isn't the last comes with a notice naming the next cursor; all=true follows every page.",
|
|
1016
|
+
readOnly: true,
|
|
1017
|
+
shape: {
|
|
1018
|
+
query: z.string().optional().describe("search terms, at most 200 bytes; omit to browse the index"),
|
|
1019
|
+
domain: z.string().optional().describe("only this capability domain, e.g. crm"),
|
|
1020
|
+
cursor: z.string().optional().describe("the next page's cursor, from the previous call's notice"),
|
|
1021
|
+
all: z.boolean().optional().describe("follow every page")
|
|
1022
|
+
},
|
|
1023
|
+
handler: (ctx, args) => {
|
|
1024
|
+
const opts = { domain: args["domain"], cursor: args["cursor"], all: args["all"] };
|
|
1025
|
+
return args["query"] === void 0 ? toolsList(ctx, opts) : toolsSearch(ctx, args["query"], opts);
|
|
1026
|
+
}
|
|
1027
|
+
},
|
|
1028
|
+
{
|
|
1029
|
+
name: "skills_list",
|
|
1030
|
+
description: "The organization's skills assigned to Operator, in title order: each skill's id, title, invocation_name and content_sha256. A skill's content and description are never exposed.",
|
|
1031
|
+
readOnly: true,
|
|
1032
|
+
shape: {},
|
|
1033
|
+
handler: (ctx) => skillsList(ctx)
|
|
798
1034
|
}
|
|
799
1035
|
];
|
|
800
|
-
function buildServer() {
|
|
801
|
-
const server = new McpServer({ name
|
|
1036
|
+
function buildServer(name = "harness") {
|
|
1037
|
+
const server = new McpServer({ name, version: CLI_VERSION });
|
|
802
1038
|
for (const tool of TOOLS) {
|
|
803
1039
|
server.registerTool(
|
|
804
1040
|
tool.name,
|
|
805
1041
|
{
|
|
806
1042
|
description: tool.description,
|
|
807
1043
|
inputSchema: tool.shape,
|
|
808
|
-
annotations: {
|
|
1044
|
+
annotations: {
|
|
1045
|
+
readOnlyHint: tool.readOnly,
|
|
1046
|
+
...tool.destructive === void 0 ? {} : { destructiveHint: tool.destructive },
|
|
1047
|
+
...tool.idempotent === void 0 ? {} : { idempotentHint: tool.idempotent }
|
|
1048
|
+
}
|
|
809
1049
|
},
|
|
810
1050
|
((args) => run((ctx) => tool.handler(ctx, args)))
|
|
811
1051
|
);
|
|
812
1052
|
}
|
|
813
1053
|
return server;
|
|
814
1054
|
}
|
|
815
|
-
async function startMcpServer() {
|
|
816
|
-
const server = buildServer();
|
|
1055
|
+
async function startMcpServer(name = "harness") {
|
|
1056
|
+
const server = buildServer(name);
|
|
817
1057
|
await server.connect(new StdioServerTransport());
|
|
818
1058
|
}
|
|
819
1059
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@allocator-one/harness-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.1",
|
|
4
4
|
"description": "CLI and MCP server for Harness, allocator.one's issue tracker",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"typecheck": "tsc --noEmit",
|
|
27
27
|
"pretest": "npm run build",
|
|
28
28
|
"test": "vitest run",
|
|
29
|
+
"package:operator": "node scripts/operator-package.mjs ./build/operator",
|
|
29
30
|
"codegen": "openapi-typescript spec/openapi.json -o src/generated/api.ts",
|
|
30
31
|
"spec:refresh": "node scripts/spec-refresh.mjs",
|
|
31
32
|
"prepublishOnly": "npm run build"
|