@krapht/ork 1.0.0-alpha.1 → 1.0.0-alpha.11
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 +3 -4
- package/dist/{__build_info__-D9lSlMFy.mjs → __build_info__-CIchF6nr.mjs} +2 -2
- package/dist/bin.mjs +1479 -600
- package/dist/chips-BA7SrrH5.mjs +43 -0
- package/dist/{clean-CA_Oi2Wl.mjs → clean-Di-KT7Mf.mjs} +5 -2
- package/dist/{completion-DBGfL-gN.mjs → completion-C5D0sEs5.mjs} +23 -16
- package/dist/context-BaNshWr9.mjs +695 -0
- package/dist/{decide-CoIJh34e.mjs → decide-B6PnKeqo.mjs} +3 -3
- package/dist/{find-rBBk7Jui.mjs → find-BicBbGMg.mjs} +1 -1
- package/dist/format-lWhN-jYR.mjs +286 -0
- package/dist/{in-a-run-DdAakpRR.mjs → in-a-run-CgkVouyG.mjs} +1 -1
- package/dist/{limits-BPM9wUlV.mjs → limits-jZQp9ZJC.mjs} +1 -1
- package/dist/{list-Bv5Lxu7z.mjs → list-CEaJ07jd.mjs} +11 -4
- package/dist/{mcp-DM30MFBw.mjs → mcp-DYgVA9f5.mjs} +6 -6
- package/dist/output-CM95njYt.mjs +421 -0
- package/dist/{pause-C7lJkSma.mjs → pause-D6PuSlNl.mjs} +4 -4
- package/dist/{relay-BipJYszF.mjs → relay-DNx7be9k.mjs} +2 -2
- package/dist/{remove-BDs9isB6.mjs → remove-CpMb4NDl.mjs} +14 -7
- package/dist/{report-DK4LGqwF.mjs → report-CEN8VoUb.mjs} +31 -20
- package/dist/{send-nbkjf3sc.mjs → send-CLs_fyg7.mjs} +3 -3
- package/dist/{src-BxUJPGa5.mjs → src-9iGkjnsy.mjs} +487 -341
- package/dist/{src-B0TT7DBi.mjs → src-B6KvTm7B.mjs} +106 -90
- package/dist/src-BnZgZGrf.mjs +2697 -0
- package/dist/{src-hrj2h0IZ.mjs → src-D6_xjm-x.mjs} +6719 -3389
- package/dist/ui/THIRD_PARTY_LICENSES.txt +594 -0
- package/dist/ui/assets/app-CxURph7a.js +75 -0
- package/dist/ui/assets/app-QoHVi-mb.css +1 -0
- package/dist/ui/assets/{index-CPx74lFf.js → index-By_8KKG6.js} +3 -3
- package/dist/ui/assets/theme-CdXvqTQ6.js +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/{ui-B98FSboa.mjs → ui-CIci4UBW.mjs} +4 -4
- package/dist/{usage-EW7DI6Z7.mjs → usage-GceBGMjU.mjs} +43 -32
- package/dist/{watch-BAkSGaOp.mjs → watch-BmoybMQg.mjs} +28 -16
- package/dist/{workflows-vsEohqa7.mjs → workflows-DZvW3i1e.mjs} +4 -3
- package/dist/{worktrees-Ahj9rdWK.mjs → worktrees-Be51E2rB.mjs} +84 -24
- package/package.json +2 -4
- package/dist/context-6x3kXREO.mjs +0 -274
- package/dist/format-zMmhqyLE.mjs +0 -427
- package/dist/output-TV_j7Ni0.mjs +0 -950
- package/dist/src-B9v0lDc6.mjs +0 -1204
- package/dist/ui/assets/app-BNT0SuIz.css +0 -1
- package/dist/ui/assets/app-CmkynZyW.js +0 -67
- package/dist/ui/assets/theme-3xH5jxKs.js +0 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { $
|
|
1
|
+
import { $t as endOf, Bt as lowerChild, Cr as TOOL_PREFIX, Ft as failedWith, Ht as envReadsOf, It as stoppedEnd, Lt as calledWorkflows, Mt as runScope, Nt as declaredProblem, Pt as endedWith, Rt as slotClock, Sr as INTEGRATIONS, Vt as startAt, Wt as declaredOutputOf, Xt as mcpNotText, Yt as MCP_FILE, Zt as serversIn, _n as parseTemplate, _r as isToolNode, ar as programsRunBy, at as planReason, bt as heldTimeout, dt as sessionDriver, en as outcomeEnd, gn as evaluate, gr as isClaudeNode, ir as beforeOf, lr as markValue, lt as planNodeSession, pt as reportSchemaFor, rn as endFor, tn as END_DETAIL_LIMIT, vn as renderTemplate, wr as integrationFor, xr as timeoutMs } from "./src-D6_xjm-x.mjs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { accessSync, constants,
|
|
3
|
+
import { accessSync, constants, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6
6
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
7
7
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
8
|
+
import { Readable } from "node:stream";
|
|
8
9
|
// ../nodes/src/call.ts
|
|
9
10
|
/**
|
|
10
11
|
* @file The `workflow.call` node type (RFC-004): renders what the node asks for
|
|
@@ -18,24 +19,17 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
|
|
|
18
19
|
*/
|
|
19
20
|
const limitedCall = (result) => {
|
|
20
21
|
if (result.ok) return result;
|
|
21
|
-
|
|
22
|
+
const why = result.end?.detail ?? result.error;
|
|
23
|
+
return result.end?.kind === "plan_limit" || (why ?? "").includes("Not started: ") || result.output["runStatus"] === "quota_exhausted" ? {
|
|
22
24
|
...result,
|
|
23
|
-
limited:
|
|
25
|
+
limited: why ?? "the called run reached a limit"
|
|
24
26
|
} : result;
|
|
25
27
|
};
|
|
26
28
|
const callExecutor = (context) => {
|
|
27
29
|
const { node, calls } = context;
|
|
28
|
-
if (node.type !== "workflow.call") return Promise.resolve({
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
error: `${context.id} is not a call`
|
|
32
|
-
});
|
|
33
|
-
if (calls === void 0) return Promise.resolve({
|
|
34
|
-
ok: false,
|
|
35
|
-
output: { status: "fail" },
|
|
36
|
-
error: "This run cannot call workflows."
|
|
37
|
-
});
|
|
38
|
-
const scope = runScope(context.inputs, context.outputs, context.runId);
|
|
30
|
+
if (node.type !== "workflow.call") return Promise.resolve(failedWith(endFor("unknown", {}, `${context.id} is not a call`), {}));
|
|
31
|
+
if (calls === void 0) return Promise.resolve(failedWith(endFor("invalid_node", { reason: "this run cannot call workflows" }), { status: "fail" }));
|
|
32
|
+
const scope = runScope(context.inputs, context.outputs, context.runId, context.item);
|
|
39
33
|
const render = (text) => {
|
|
40
34
|
const template = parseTemplate(text);
|
|
41
35
|
return template.ok ? renderTemplate(template.parts, scope) : text;
|
|
@@ -61,33 +55,56 @@ const callExecutor = (context) => {
|
|
|
61
55
|
}
|
|
62
56
|
}, context.signal).then(limitedCall);
|
|
63
57
|
};
|
|
58
|
+
// ../nodes/src/claude-plan.ts
|
|
59
|
+
/**
|
|
60
|
+
* @file What a Claude node's session is planned with, read from the node's
|
|
61
|
+
* context.
|
|
62
|
+
*/
|
|
63
|
+
/** The plan options every session of a node starts from. */
|
|
64
|
+
const planOptionsOf = (context, inPlace) => {
|
|
65
|
+
const planOptions = {
|
|
66
|
+
cwd: context.cwd,
|
|
67
|
+
inputs: context.inputs,
|
|
68
|
+
outputs: context.outputs,
|
|
69
|
+
inPlace,
|
|
70
|
+
workflowDir: context.workflowDir,
|
|
71
|
+
projectRoot: context.projectRoot,
|
|
72
|
+
...context.orkDir === void 0 ? {} : { orkDir: context.orkDir },
|
|
73
|
+
pluginDir: path.join(context.runDir, "plugins", context.id)
|
|
74
|
+
};
|
|
75
|
+
if (context.checkout !== void 0) planOptions.checkout = context.checkout;
|
|
76
|
+
if (context.brief !== void 0) planOptions.brief = context.brief;
|
|
77
|
+
if (context.template !== void 0) planOptions.template = context.template;
|
|
78
|
+
if (context.item !== void 0) planOptions.item = context.item;
|
|
79
|
+
if (context.ownership !== void 0) planOptions.ownership = context.ownership;
|
|
80
|
+
if (context.mailbox !== void 0) planOptions.mailbox = context.mailbox;
|
|
81
|
+
if (context.claims !== void 0) planOptions.claims = context.claims;
|
|
82
|
+
return planOptions;
|
|
83
|
+
};
|
|
64
84
|
// ../nodes/src/claude-result.ts
|
|
65
85
|
/**
|
|
66
86
|
* @file A finished session as a node's result: its report is the output, held
|
|
67
|
-
* to what the node declares (RFC-010)
|
|
87
|
+
* to what the node declares (RFC-010), and how it ended is its end kind
|
|
88
|
+
* (specs/runtime/session-ends.md).
|
|
68
89
|
*/
|
|
90
|
+
/** How a session with no report ended; a driver's `done` without one is not. */
|
|
91
|
+
const withoutReport = (outcome) => {
|
|
92
|
+
const end = outcomeEnd(outcome);
|
|
93
|
+
return end.kind === "done" || end.kind === "reported_failure" ? endFor("no_report", {}, end.detail) : end;
|
|
94
|
+
};
|
|
69
95
|
/** A finished session as a node result: its report is the node's output. */
|
|
70
96
|
const resultOf = (outcome, declared) => {
|
|
71
|
-
if (outcome === null) return {
|
|
72
|
-
ok: false,
|
|
73
|
-
output: {
|
|
74
|
-
status: "fail",
|
|
75
|
-
summary: "the session ended without a result"
|
|
76
|
-
},
|
|
77
|
-
error: "the session ended without a result"
|
|
78
|
-
};
|
|
97
|
+
if (outcome === null) return failedWith(endFor("driver_crashed", {}, "the session ended without a result"));
|
|
79
98
|
const { report, usage } = outcome;
|
|
80
99
|
if (report === null) {
|
|
81
|
-
const
|
|
100
|
+
const end = withoutReport(outcome);
|
|
82
101
|
return {
|
|
83
|
-
|
|
84
|
-
output: {
|
|
102
|
+
...failedWith(end, {
|
|
85
103
|
status: "fail",
|
|
86
|
-
summary:
|
|
104
|
+
summary: end.message,
|
|
87
105
|
usage
|
|
88
|
-
},
|
|
106
|
+
}),
|
|
89
107
|
usage,
|
|
90
|
-
error: `ended without a report (${outcome.subtype}): ${reason}`,
|
|
91
108
|
sessionId: outcome.sessionId
|
|
92
109
|
};
|
|
93
110
|
}
|
|
@@ -101,7 +118,7 @@ const resultOf = (outcome, declared) => {
|
|
|
101
118
|
};
|
|
102
119
|
if (report.branch !== void 0) output["branch"] = report.branch;
|
|
103
120
|
const problem = declaredProblem(declared, output);
|
|
104
|
-
if (problem !== null) return {
|
|
121
|
+
if (problem !== null) return endedWith({
|
|
105
122
|
ok: false,
|
|
106
123
|
output: {
|
|
107
124
|
status: "fail",
|
|
@@ -111,18 +128,166 @@ const resultOf = (outcome, declared) => {
|
|
|
111
128
|
...report.branch === void 0 ? {} : { branch: report.branch }
|
|
112
129
|
},
|
|
113
130
|
usage,
|
|
114
|
-
error: problem,
|
|
115
131
|
...ran
|
|
116
|
-
};
|
|
117
|
-
const
|
|
118
|
-
|
|
132
|
+
}, endFor("invalid_report", { reason: problem }));
|
|
133
|
+
const ok = report.status !== "fail";
|
|
134
|
+
return endedWith({
|
|
135
|
+
ok,
|
|
119
136
|
output,
|
|
120
137
|
usage,
|
|
121
138
|
...ran
|
|
139
|
+
}, endFor(ok ? "done" : "reported_failure"));
|
|
140
|
+
};
|
|
141
|
+
// ../nodes/src/environ.ts
|
|
142
|
+
/** A variable of the user's. */
|
|
143
|
+
const readEnv = (environ, name) => Object.hasOwn(environ, name) ? environ[name] : void 0;
|
|
144
|
+
// ../nodes/src/mcp-servers.ts
|
|
145
|
+
/**
|
|
146
|
+
* @file Reading an MCP server's definition from the project's `.mcp.json`, the
|
|
147
|
+
* file Claude Code reads too, so a server is defined in one place (RFC-006).
|
|
148
|
+
* `${VAR}` and `${VAR:-default}` read the user's environment, which is where
|
|
149
|
+
* secrets stay: Ork stores none, and a run masks each value read this way. A
|
|
150
|
+
* server that needs an OAuth sign-in holds Claude Code's tokens, not Ork's,
|
|
151
|
+
* so it is refused.
|
|
152
|
+
*/
|
|
153
|
+
const VARIABLE = /\$\{([A-Za-z_]\w*)(?::-([^}]*))?\}/gu;
|
|
154
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
155
|
+
const stringsOf = (value) => isRecord(value) ? Object.fromEntries(Object.entries(value).flatMap(([key, item]) => typeof item === "string" ? [[key, item]] : [])) : {};
|
|
156
|
+
const listOf = (value) => Array.isArray(value) ? value.flatMap((item) => typeof item === "string" ? [item] : []) : [];
|
|
157
|
+
/** The server as written, or why Ork can't call it. */
|
|
158
|
+
const readMcpServer = (root, name) => {
|
|
159
|
+
const servers = serversIn(root);
|
|
160
|
+
if (!servers.ok) return servers;
|
|
161
|
+
const known = Object.keys(servers.value);
|
|
162
|
+
const server = Object.hasOwn(servers.value, name) ? servers.value[name] : void 0;
|
|
163
|
+
if (!isRecord(server)) return {
|
|
164
|
+
ok: false,
|
|
165
|
+
error: `no MCP server \`${name}\` in ${MCP_FILE}${known.length === 0 ? "" : `, which has ${known.join(", ")}`}`
|
|
166
|
+
};
|
|
167
|
+
if ("oauth" in server) return {
|
|
168
|
+
ok: false,
|
|
169
|
+
error: `MCP server \`${name}\` signs in with OAuth, whose tokens are Claude Code's: give it a token through headers and \`env:\`, or call it from a session`
|
|
170
|
+
};
|
|
171
|
+
const notText = mcpNotText(server);
|
|
172
|
+
if (notText !== void 0) return {
|
|
173
|
+
ok: false,
|
|
174
|
+
error: `MCP server \`${name}\` has a value in ${notText} that isn't a string: write it in quotes`
|
|
175
|
+
};
|
|
176
|
+
const { type, url, command } = server;
|
|
177
|
+
if (type === "http" && typeof url === "string") return {
|
|
178
|
+
ok: true,
|
|
179
|
+
value: {
|
|
180
|
+
kind: "http",
|
|
181
|
+
url,
|
|
182
|
+
headers: stringsOf(server["headers"])
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
if ((type === void 0 || type === "stdio") && typeof command === "string") return {
|
|
186
|
+
ok: true,
|
|
187
|
+
value: {
|
|
188
|
+
kind: "stdio",
|
|
189
|
+
command,
|
|
190
|
+
args: listOf(server["args"]),
|
|
191
|
+
env: stringsOf(server["env"])
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
error: `MCP server \`${name}\` is not one Ork can call: it takes stdio and http servers`
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* The named servers' entries as written, for a session: `${VAR}` stays text for
|
|
201
|
+
* the `claude` that loads them to expand. The compiler has checked the names.
|
|
202
|
+
*/
|
|
203
|
+
const namedMcpEntries = (root, names) => {
|
|
204
|
+
if (names.length === 0) return {
|
|
205
|
+
ok: true,
|
|
206
|
+
value: {}
|
|
207
|
+
};
|
|
208
|
+
const servers = serversIn(root);
|
|
209
|
+
if (!servers.ok) return servers;
|
|
210
|
+
const mcpServers = {};
|
|
211
|
+
for (const name of names) {
|
|
212
|
+
const server = Object.hasOwn(servers.value, name) ? servers.value[name] : void 0;
|
|
213
|
+
if (!isRecord(server)) return {
|
|
214
|
+
ok: false,
|
|
215
|
+
error: `no MCP server \`${name}\` in ${MCP_FILE}`
|
|
216
|
+
};
|
|
217
|
+
mcpServers[name] = server;
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
ok: true,
|
|
221
|
+
value: { mcpServers }
|
|
122
222
|
};
|
|
123
|
-
if (!result.ok) result.error = report.summary;
|
|
124
|
-
return result;
|
|
125
223
|
};
|
|
224
|
+
/** Each variable a server reads, and whether it has a default. */
|
|
225
|
+
const readsOf = (server) => {
|
|
226
|
+
return (server.kind === "stdio" ? [
|
|
227
|
+
server.command,
|
|
228
|
+
...server.args,
|
|
229
|
+
...Object.values(server.env)
|
|
230
|
+
] : [server.url, ...Object.values(server.headers)]).flatMap((text) => [...text.matchAll(VARIABLE)].map((match) => ({
|
|
231
|
+
name: String(match[1]),
|
|
232
|
+
fallback: match[2] !== void 0
|
|
233
|
+
})));
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* The server with its variables filled in, and the values to mask: all it read
|
|
237
|
+
* but those `plain` names, which aren't secret.
|
|
238
|
+
*/
|
|
239
|
+
const expandServer = (server, environ, plain = []) => {
|
|
240
|
+
const unset = readsOf(server).find(({ name, fallback }) => !fallback && readEnv(environ, name) === void 0);
|
|
241
|
+
if (unset !== void 0) return {
|
|
242
|
+
ok: false,
|
|
243
|
+
error: `${MCP_FILE} reads ${unset.name}, which is not set`
|
|
244
|
+
};
|
|
245
|
+
const values = [];
|
|
246
|
+
const fill = (text) => text.replaceAll(VARIABLE, (_all, name, fallback) => {
|
|
247
|
+
const value = readEnv(environ, name);
|
|
248
|
+
if (value !== void 0 && !plain.includes(name)) values.push(value);
|
|
249
|
+
return value ?? fallback ?? "";
|
|
250
|
+
});
|
|
251
|
+
const fillAll = (record) => Object.fromEntries(Object.entries(record).map(([key, text]) => [key, fill(text)]));
|
|
252
|
+
return {
|
|
253
|
+
ok: true,
|
|
254
|
+
value: {
|
|
255
|
+
server: server.kind === "stdio" ? {
|
|
256
|
+
kind: "stdio",
|
|
257
|
+
command: fill(server.command),
|
|
258
|
+
args: server.args.map((arg) => fill(arg)),
|
|
259
|
+
env: fillAll(server.env)
|
|
260
|
+
} : {
|
|
261
|
+
kind: "http",
|
|
262
|
+
url: fill(server.url),
|
|
263
|
+
headers: fillAll(server.headers)
|
|
264
|
+
},
|
|
265
|
+
values
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
/** What a workflow's MCP servers read of the user's, unless called not secret. */
|
|
270
|
+
const mcpVariablesOf = (workflow, root) => Object.values(workflow.nodes).flatMap((node) => {
|
|
271
|
+
if (node.type !== "mcp.call") return [];
|
|
272
|
+
const read = readMcpServer(root, node.server);
|
|
273
|
+
return read.ok ? readsOf(read.value).map(({ name }) => name).filter((name) => node.plainEnv?.includes(name) !== true) : [];
|
|
274
|
+
});
|
|
275
|
+
/** A stdio server's program, when its name can be known before it is filled in. */
|
|
276
|
+
const programOf = (server) => server.kind === "stdio" && !server.command.includes("$") ? server.command : void 0;
|
|
277
|
+
/**
|
|
278
|
+
* What stops a workflow's `mcp.call` nodes, found before a run spends anything:
|
|
279
|
+
* a server that isn't there or can't be called, a program that isn't installed,
|
|
280
|
+
* or a variable that is not set.
|
|
281
|
+
*/
|
|
282
|
+
const mcpProblems = (workflow, root, environ = process.env, finished = /* @__PURE__ */ new Set(), installed = () => true) => Object.entries(workflow.nodes).flatMap(([id, node]) => {
|
|
283
|
+
if (node.type !== "mcp.call" || finished.has(id)) return [];
|
|
284
|
+
const read = readMcpServer(root, node.server);
|
|
285
|
+
if (!read.ok) return [`${id}: ${read.error}`];
|
|
286
|
+
const given = new Set(Object.keys(node.env ?? {}));
|
|
287
|
+
const unset = readsOf(read.value).flatMap(({ name, fallback }) => fallback || given.has(name) || readEnv(environ, name) !== void 0 ? [] : [`${id}: ${MCP_FILE} reads ${name}, which is not set`]);
|
|
288
|
+
const program = programOf(read.value);
|
|
289
|
+
return program === void 0 || installed(program) ? unset : [`\`${id}\`: MCP server \`${node.server}\` runs \`${program}\`, which is not installed`].concat(unset);
|
|
290
|
+
});
|
|
126
291
|
/** The only built-in tools a fork has: it reads, and reports. */
|
|
127
292
|
const FORK_TOOLS = [
|
|
128
293
|
"Read",
|
|
@@ -277,25 +442,39 @@ const askFork = async (context, owner, question, deps) => {
|
|
|
277
442
|
/** What the person sees of a question its asker's owner passed on. */
|
|
278
443
|
const passedOnContext = (context, owner, note) => [...context === void 0 ? [] : [context, ""], note === void 0 ? `${owner} passed this on.` : `${owner} passed this on: ${note}`].join("\n");
|
|
279
444
|
/**
|
|
445
|
+
* The node's channel to the person: what it asked is withdrawn when it stops,
|
|
446
|
+
* so a stopped node leaves no decision open.
|
|
447
|
+
*/
|
|
448
|
+
const withdrawnOnStop = (context) => {
|
|
449
|
+
const { human } = context;
|
|
450
|
+
const signalOf = (asked) => asked === void 0 ? context.signal : AbortSignal.any([asked, context.signal]);
|
|
451
|
+
return {
|
|
452
|
+
permit: (request) => human.permit({
|
|
453
|
+
...request,
|
|
454
|
+
signal: signalOf(request.signal)
|
|
455
|
+
}),
|
|
456
|
+
ask: (question) => human.ask({
|
|
457
|
+
...question,
|
|
458
|
+
signal: signalOf(question.signal)
|
|
459
|
+
}),
|
|
460
|
+
...human.settled === void 0 ? {} : { settled: human.settled }
|
|
461
|
+
};
|
|
462
|
+
};
|
|
463
|
+
/**
|
|
280
464
|
* The child's channel when its owner takes its questions first: a question goes
|
|
281
465
|
* to a fork of the owner's session, then to the person if the fork passes it on
|
|
282
466
|
* or can't answer. Permission prompts go to the person as before.
|
|
283
467
|
*/
|
|
284
468
|
const ownerFirst = (context, owner, deps) => {
|
|
285
|
-
const
|
|
469
|
+
const human = withdrawnOnStop(context);
|
|
286
470
|
return {
|
|
287
471
|
permit: (request) => human.permit(request),
|
|
288
472
|
ask: async (question) => {
|
|
289
473
|
const answered = await askFork(context, owner, question, deps);
|
|
290
474
|
if (context.signal.aborted) throw new Error(`${context.id} was stopped`);
|
|
291
|
-
|
|
292
|
-
if (answered === null) return human.ask({
|
|
293
|
-
...question,
|
|
294
|
-
signal
|
|
295
|
-
});
|
|
475
|
+
if (answered === null) return human.ask(question);
|
|
296
476
|
if (answered.choice === passOnId(question.options)) return human.ask({
|
|
297
477
|
...question,
|
|
298
|
-
signal,
|
|
299
478
|
owner: owner.id,
|
|
300
479
|
context: passedOnContext(question.context, owner.id, answered.note)
|
|
301
480
|
});
|
|
@@ -347,13 +526,16 @@ const withUsageOf = (first, later) => {
|
|
|
347
526
|
*/
|
|
348
527
|
const afterLateTurn = (first, later) => {
|
|
349
528
|
const merged = withUsageOf(first, later);
|
|
350
|
-
|
|
351
|
-
|
|
529
|
+
if (later.report !== null) return merged;
|
|
530
|
+
const { end: _later, ...rest } = merged;
|
|
531
|
+
return {
|
|
532
|
+
...rest,
|
|
352
533
|
report: first.report,
|
|
353
534
|
subtype: first.subtype,
|
|
535
|
+
...first.end === void 0 ? {} : { end: first.end },
|
|
354
536
|
text: first.text,
|
|
355
537
|
errors: first.errors
|
|
356
|
-
}
|
|
538
|
+
};
|
|
357
539
|
};
|
|
358
540
|
/**
|
|
359
541
|
* What another turn of a session may use. `spent` and `turns` are this
|
|
@@ -379,6 +561,20 @@ const budgetNow = (context) => {
|
|
|
379
561
|
return known.length === 0 ? void 0 : Math.min(...known);
|
|
380
562
|
};
|
|
381
563
|
/**
|
|
564
|
+
* What a node's first session may spend, set aside from the run's budget in the
|
|
565
|
+
* same step that reads it (ADR-077), so a node starting beside it is never
|
|
566
|
+
* offered the same part: the least of what is left and the node's own
|
|
567
|
+
* `budgetUsd`. With nothing left, it waits for a worker running beside it to
|
|
568
|
+
* finish, as long as one may.
|
|
569
|
+
*/
|
|
570
|
+
const budgetToStart = async (context) => {
|
|
571
|
+
const left = budgetNow(context);
|
|
572
|
+
if (left !== void 0 && left <= 0) return await context.budgetFreed?.(context.signal) === true ? budgetToStart(context) : left;
|
|
573
|
+
const own = isClaudeNode(context.node) ? context.node.budgetUsd ?? context.workflow.defaults?.budgetUsd : void 0;
|
|
574
|
+
context.reserveBudget?.(own === void 0 || left === void 0 ? left : Math.min(own, left));
|
|
575
|
+
return left;
|
|
576
|
+
};
|
|
577
|
+
/**
|
|
382
578
|
* Whether a limit across runs, rather than the node's work, ended a node: a
|
|
383
579
|
* session whose cap was a quota's and that ran into it, or one its plan refused
|
|
384
580
|
* and that gave no report. A node that still succeeded is left alone.
|
|
@@ -398,24 +594,38 @@ const limitWatch = (context) => {
|
|
|
398
594
|
},
|
|
399
595
|
ended: (outcome) => {
|
|
400
596
|
if (refused !== void 0 && (outcome?.report ?? null) === null) why = refused;
|
|
401
|
-
|
|
597
|
+
const capped = left !== void 0 && asked.budgetUsd !== void 0 && asked.budgetUsd >= left;
|
|
598
|
+
const spent = outcome !== null && outcomeEnd(outcome).kind === "budget_limit";
|
|
599
|
+
if (capped && spent) why = context.quota?.().blocked ?? "a session reached what was left of a quota";
|
|
402
600
|
}
|
|
403
601
|
};
|
|
404
602
|
},
|
|
405
|
-
result: (result) =>
|
|
406
|
-
|
|
407
|
-
|
|
603
|
+
result: (result) => {
|
|
604
|
+
if (result.ok) return result;
|
|
605
|
+
if (why !== void 0) return {
|
|
606
|
+
...endedWith(result, endFor("plan_limit", {}, why)),
|
|
607
|
+
limited: why
|
|
608
|
+
};
|
|
609
|
+
const { end } = result;
|
|
610
|
+
return end?.kind === "plan_limit" ? {
|
|
611
|
+
...result,
|
|
612
|
+
limited: end.message
|
|
613
|
+
} : result;
|
|
408
614
|
}
|
|
409
615
|
};
|
|
410
616
|
};
|
|
411
|
-
/**
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
617
|
+
/**
|
|
618
|
+
* No session started: nothing was left for one, or nothing to start it. Its
|
|
619
|
+
* summary keeps Ork's reason, `end.detail` too; the message is the kind's.
|
|
620
|
+
*/
|
|
621
|
+
const notStarted = (kind, reason, limited) => ({
|
|
622
|
+
...failedWith(endFor(kind, {
|
|
623
|
+
reason,
|
|
624
|
+
program: "an agent driver"
|
|
625
|
+
}, reason), {
|
|
416
626
|
status: "fail",
|
|
417
627
|
summary: `not started: ${reason}`
|
|
418
|
-
},
|
|
628
|
+
}),
|
|
419
629
|
...limited === void 0 ? {} : { limited }
|
|
420
630
|
});
|
|
421
631
|
// ../nodes/src/tool-output.ts
|
|
@@ -660,163 +870,51 @@ const tokenize = (command) => {
|
|
|
660
870
|
args
|
|
661
871
|
};
|
|
662
872
|
};
|
|
663
|
-
// ../nodes/src/
|
|
664
|
-
/** A variable of the user's. */
|
|
665
|
-
const readEnv = (environ, name) => Object.hasOwn(environ, name) ? environ[name] : void 0;
|
|
666
|
-
// ../nodes/src/mcp-servers.ts
|
|
873
|
+
// ../nodes/src/tool-ends.ts
|
|
667
874
|
/**
|
|
668
|
-
* @file
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
* server that needs an OAuth sign-in holds Claude Code's tokens, not Ork's,
|
|
673
|
-
* so it is refused.
|
|
875
|
+
* @file How a tool node ended (specs/runtime/session-ends.md): a node that
|
|
876
|
+
* could not run its command, JSON that broke its declared output, or how a
|
|
877
|
+
* command that ran exited: done, stopped, never started, signed out, or
|
|
878
|
+
* failed. Ork's message names the program; its stderr is the detail.
|
|
674
879
|
*/
|
|
675
|
-
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
ok: true,
|
|
683
|
-
value: {}
|
|
684
|
-
};
|
|
685
|
-
try {
|
|
686
|
-
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
687
|
-
const servers = isRecord(parsed) ? parsed["mcpServers"] : void 0;
|
|
688
|
-
return {
|
|
689
|
-
ok: true,
|
|
690
|
-
value: isRecord(servers) ? servers : {}
|
|
691
|
-
};
|
|
692
|
-
} catch (error) {
|
|
693
|
-
const reason = error instanceof SyntaxError ? "it is not valid JSON" : error instanceof Error ? error.message : String(error);
|
|
694
|
-
return {
|
|
695
|
-
ok: false,
|
|
696
|
-
error: `${MCP_FILE} can't be read: ${reason}`
|
|
697
|
-
};
|
|
880
|
+
/** A node that could not run its command: its own definition is wrong. */
|
|
881
|
+
const failed$1 = (error, kind = "invalid_node") => endedWith({
|
|
882
|
+
ok: false,
|
|
883
|
+
output: {
|
|
884
|
+
exitCode: -1,
|
|
885
|
+
stdout: "",
|
|
886
|
+
stderr: error
|
|
698
887
|
}
|
|
699
|
-
};
|
|
700
|
-
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
"env",
|
|
706
|
-
"headers"
|
|
707
|
-
].find((field) => {
|
|
708
|
-
const value = server[field];
|
|
709
|
-
return (Array.isArray(value) ? value : isRecord(value) ? Object.values(value) : []).some((item) => typeof item !== "string");
|
|
710
|
-
});
|
|
711
|
-
/** The server as written, or why Ork can't call it. */
|
|
712
|
-
const readMcpServer = (root, name) => {
|
|
713
|
-
const servers = serversIn(root);
|
|
714
|
-
if (!servers.ok) return servers;
|
|
715
|
-
const known = Object.keys(servers.value);
|
|
716
|
-
const server = Object.hasOwn(servers.value, name) ? servers.value[name] : void 0;
|
|
717
|
-
if (!isRecord(server)) return {
|
|
718
|
-
ok: false,
|
|
719
|
-
error: `no MCP server \`${name}\` in ${MCP_FILE}${known.length === 0 ? "" : `, which has ${known.join(", ")}`}`
|
|
720
|
-
};
|
|
721
|
-
if ("oauth" in server) return {
|
|
722
|
-
ok: false,
|
|
723
|
-
error: `MCP server \`${name}\` signs in with OAuth, whose tokens are Claude Code's: give it a token through headers and \`env:\`, or call it from a session`
|
|
724
|
-
};
|
|
725
|
-
const notText = notTextIn(server);
|
|
726
|
-
if (notText !== void 0) return {
|
|
727
|
-
ok: false,
|
|
728
|
-
error: `MCP server \`${name}\` has a value in ${notText} that isn't a string: write it in quotes`
|
|
729
|
-
};
|
|
730
|
-
const { type, url, command } = server;
|
|
731
|
-
if (type === "http" && typeof url === "string") return {
|
|
732
|
-
ok: true,
|
|
733
|
-
value: {
|
|
734
|
-
kind: "http",
|
|
735
|
-
url,
|
|
736
|
-
headers: stringsOf(server["headers"])
|
|
737
|
-
}
|
|
738
|
-
};
|
|
739
|
-
if ((type === void 0 || type === "stdio") && typeof command === "string") return {
|
|
740
|
-
ok: true,
|
|
741
|
-
value: {
|
|
742
|
-
kind: "stdio",
|
|
743
|
-
command,
|
|
744
|
-
args: listOf(server["args"]),
|
|
745
|
-
env: stringsOf(server["env"])
|
|
746
|
-
}
|
|
747
|
-
};
|
|
748
|
-
return {
|
|
749
|
-
ok: false,
|
|
750
|
-
error: `MCP server \`${name}\` is not one Ork can call: it takes stdio and http servers`
|
|
751
|
-
};
|
|
752
|
-
};
|
|
753
|
-
/** Each variable a server reads, and whether it has a default. */
|
|
754
|
-
const readsOf = (server) => {
|
|
755
|
-
return (server.kind === "stdio" ? [
|
|
756
|
-
server.command,
|
|
757
|
-
...server.args,
|
|
758
|
-
...Object.values(server.env)
|
|
759
|
-
] : [server.url, ...Object.values(server.headers)]).flatMap((text) => [...text.matchAll(VARIABLE)].map((match) => ({
|
|
760
|
-
name: String(match[1]),
|
|
761
|
-
fallback: match[2] !== void 0
|
|
762
|
-
})));
|
|
763
|
-
};
|
|
888
|
+
}, endFor(kind, { reason: error }, error));
|
|
889
|
+
/** JSON that broke what the node declares, or that it could not read. */
|
|
890
|
+
const invalidOutput = (output, reason) => endedWith({
|
|
891
|
+
ok: false,
|
|
892
|
+
output
|
|
893
|
+
}, endFor("invalid_output", { reason }));
|
|
764
894
|
/**
|
|
765
|
-
*
|
|
766
|
-
*
|
|
895
|
+
* How a command that ran and exited non-zero ended: stopped, never started, a
|
|
896
|
+
* sign-in its program asks for (`signedOut`), or a plain failure.
|
|
767
897
|
*/
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
if (
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
const value = readEnv(environ, name);
|
|
777
|
-
if (value !== void 0 && !plain.includes(name)) values.push(value);
|
|
778
|
-
return value ?? fallback ?? "";
|
|
779
|
-
});
|
|
780
|
-
const fillAll = (record) => Object.fromEntries(Object.entries(record).map(([key, text]) => [key, fill(text)]));
|
|
781
|
-
return {
|
|
898
|
+
const exitKind = (run, context, signedOut) => {
|
|
899
|
+
if (context.signal.aborted) return "interrupted";
|
|
900
|
+
if (run.exitCode === -1 && /\bENOENT\b/u.test(run.stderr)) return "tool_missing";
|
|
901
|
+
return signedOut?.(run) === true ? "auth" : "command_failed";
|
|
902
|
+
};
|
|
903
|
+
/** How a command that ran ended, by its exit code; `program` is scrubbed. */
|
|
904
|
+
const exitEnd = (output, run, context, program, signedOut) => {
|
|
905
|
+
if (run.exitCode === 0) return endedWith({
|
|
782
906
|
ok: true,
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
},
|
|
794
|
-
values
|
|
795
|
-
}
|
|
796
|
-
};
|
|
907
|
+
output
|
|
908
|
+
}, endFor("done"));
|
|
909
|
+
const end = endFor(exitKind(run, context, signedOut), {
|
|
910
|
+
program,
|
|
911
|
+
exitCode: run.exitCode
|
|
912
|
+
}, run.stderr.trim().slice(-END_DETAIL_LIMIT));
|
|
913
|
+
return endedWith({
|
|
914
|
+
ok: false,
|
|
915
|
+
output
|
|
916
|
+
}, end);
|
|
797
917
|
};
|
|
798
|
-
/** What a workflow's MCP servers read of the user's, unless called not secret. */
|
|
799
|
-
const mcpVariablesOf = (workflow, root) => Object.values(workflow.nodes).flatMap((node) => {
|
|
800
|
-
if (node.type !== "mcp.call") return [];
|
|
801
|
-
const read = readMcpServer(root, node.server);
|
|
802
|
-
return read.ok ? readsOf(read.value).map(({ name }) => name).filter((name) => node.plainEnv?.includes(name) !== true) : [];
|
|
803
|
-
});
|
|
804
|
-
/** A stdio server's program, when its name can be known before it is filled in. */
|
|
805
|
-
const programOf = (server) => server.kind === "stdio" && !server.command.includes("$") ? server.command : void 0;
|
|
806
|
-
/**
|
|
807
|
-
* What stops a workflow's `mcp.call` nodes, found before a run spends anything:
|
|
808
|
-
* a server that isn't there or can't be called, a program that isn't installed,
|
|
809
|
-
* or a variable that is not set.
|
|
810
|
-
*/
|
|
811
|
-
const mcpProblems = (workflow, root, environ = process.env, finished = /* @__PURE__ */ new Set(), installed = () => true) => Object.entries(workflow.nodes).flatMap(([id, node]) => {
|
|
812
|
-
if (node.type !== "mcp.call" || finished.has(id)) return [];
|
|
813
|
-
const read = readMcpServer(root, node.server);
|
|
814
|
-
if (!read.ok) return [`${id}: ${read.error}`];
|
|
815
|
-
const given = new Set(Object.keys(node.env ?? {}));
|
|
816
|
-
const unset = readsOf(read.value).flatMap(({ name, fallback }) => fallback || given.has(name) || readEnv(environ, name) !== void 0 ? [] : [`${id}: ${MCP_FILE} reads ${name}, which is not set`]);
|
|
817
|
-
const program = programOf(read.value);
|
|
818
|
-
return program === void 0 || installed(program) ? unset : [`\`${id}\`: MCP server \`${node.server}\` runs \`${program}\`, which is not installed`].concat(unset);
|
|
819
|
-
});
|
|
820
918
|
// ../nodes/src/tool-scrub.ts
|
|
821
919
|
/**
|
|
822
920
|
* @file Masking a secret in what a program prints. A plain search, so a value
|
|
@@ -998,7 +1096,9 @@ const IDENTITY_KEYS = /* @__PURE__ */ new Set([
|
|
|
998
1096
|
"ORK_WORKFLOW",
|
|
999
1097
|
"ORK_BIN",
|
|
1000
1098
|
OUTPUT_ENV,
|
|
1001
|
-
STATE_ENV
|
|
1099
|
+
STATE_ENV,
|
|
1100
|
+
"ORK_ITEM",
|
|
1101
|
+
"ORK_INDEX"
|
|
1002
1102
|
]);
|
|
1003
1103
|
/**
|
|
1004
1104
|
* `env` without Ork's identity variables. A run started by a program in another
|
|
@@ -1026,7 +1126,11 @@ const identityOf = (context, orkBin) => ({
|
|
|
1026
1126
|
ORK_RUN_DIR: context.runDir,
|
|
1027
1127
|
ORK_PROJECT: context.projectRoot,
|
|
1028
1128
|
ORK_WORKFLOW: context.workflow.name,
|
|
1029
|
-
...orkBin === void 0 ? {} : { ORK_BIN: orkBin }
|
|
1129
|
+
...orkBin === void 0 ? {} : { ORK_BIN: orkBin },
|
|
1130
|
+
...context.item === void 0 ? {} : {
|
|
1131
|
+
ORK_ITEM: typeof context.item.value === "string" ? context.item.value : JSON.stringify(context.item.value),
|
|
1132
|
+
ORK_INDEX: String(context.item.index)
|
|
1133
|
+
}
|
|
1030
1134
|
});
|
|
1031
1135
|
/** The variables a workflow's nodes read that no `plainEnv` excuses. */
|
|
1032
1136
|
const secretNamesOf = (workflow) => Object.values(workflow.nodes).flatMap((node) => programsRunBy(node, workflow.defaults)).flatMap(({ program }) => {
|
|
@@ -1068,7 +1172,7 @@ const secretsOf = (workflow, environ, also = []) => valuesOf(namesToMask(workflo
|
|
|
1068
1172
|
const renderEnv = (node, context, environ) => {
|
|
1069
1173
|
const values = {};
|
|
1070
1174
|
const scope = {
|
|
1071
|
-
...runScope(context.inputs, context.outputs, context.runId),
|
|
1175
|
+
...runScope(context.inputs, context.outputs, context.runId, context.item),
|
|
1072
1176
|
output: { env: environ }
|
|
1073
1177
|
};
|
|
1074
1178
|
for (const [key, text] of Object.entries(node.env ?? {})) {
|
|
@@ -1158,7 +1262,7 @@ const render = (text, context) => {
|
|
|
1158
1262
|
};
|
|
1159
1263
|
return {
|
|
1160
1264
|
ok: true,
|
|
1161
|
-
value: renderTemplate(template.parts, runScope(context.inputs, context.outputs, context.runId))
|
|
1265
|
+
value: renderTemplate(template.parts, runScope(context.inputs, context.outputs, context.runId, context.item))
|
|
1162
1266
|
};
|
|
1163
1267
|
};
|
|
1164
1268
|
/** The program and arguments a tool node runs, before the allow check. */
|
|
@@ -1212,19 +1316,10 @@ const capped = (text, file) => {
|
|
|
1212
1316
|
* what a session is told. All of it is kept in the output's fields and files.
|
|
1213
1317
|
*/
|
|
1214
1318
|
const clipError = (text) => text.length <= ERROR_LIMIT ? text : `${text.slice(0, ERROR_LIMIT)}…`;
|
|
1215
|
-
const failed$1 = (error) => ({
|
|
1216
|
-
ok: false,
|
|
1217
|
-
output: {
|
|
1218
|
-
exitCode: -1,
|
|
1219
|
-
stdout: "",
|
|
1220
|
-
stderr: error
|
|
1221
|
-
},
|
|
1222
|
-
error
|
|
1223
|
-
});
|
|
1224
1319
|
/** Runs a user's CLI through its integration's adapter, with its own login. */
|
|
1225
1320
|
const toolExecutor = (given, options = {}) => async (context) => {
|
|
1226
1321
|
const { node } = context;
|
|
1227
|
-
if (!isToolNode(node)) return failed$1(`${context.id} is not a tool
|
|
1322
|
+
if (!isToolNode(node)) return failed$1(`${context.id} is not a tool`, "unknown");
|
|
1228
1323
|
const environ = options.environ ?? process.env;
|
|
1229
1324
|
const scrub = scrubberOf(secretsOf(context.workflow, environ, options.maskedNames));
|
|
1230
1325
|
const command = commandOf(node, context);
|
|
@@ -1251,6 +1346,7 @@ const toolExecutor = (given, options = {}) => async (context) => {
|
|
|
1251
1346
|
...node.watch === void 0 ? {} : { [STATE_ENV]: prepareStateFile(context.runDir, context.id) }
|
|
1252
1347
|
};
|
|
1253
1348
|
const names = Object.keys(nodeEnv.values);
|
|
1349
|
+
const release = await context.machine?.slot([command.program, ...command.args].join(" "), context.signal);
|
|
1254
1350
|
context.emit({
|
|
1255
1351
|
type: "tool.called",
|
|
1256
1352
|
id: context.id,
|
|
@@ -1265,11 +1361,13 @@ const toolExecutor = (given, options = {}) => async (context) => {
|
|
|
1265
1361
|
}
|
|
1266
1362
|
});
|
|
1267
1363
|
const scriptFile = node.script === void 0 ? void 0 : writeScriptFile(context.runDir, context.id, context.round, node.script);
|
|
1364
|
+
const priority = context.machine?.priority;
|
|
1268
1365
|
const raw = await runner(command.program, scriptFile === void 0 ? command.args : [...command.args, scriptFile], {
|
|
1269
1366
|
cwd,
|
|
1270
1367
|
signal: context.signal,
|
|
1271
|
-
env
|
|
1272
|
-
|
|
1368
|
+
env,
|
|
1369
|
+
...priority === void 0 ? {} : { priority }
|
|
1370
|
+
}).finally(() => release?.());
|
|
1273
1371
|
if (scriptFile !== void 0) removeOutputFile(scriptFile);
|
|
1274
1372
|
const run = {
|
|
1275
1373
|
exitCode: raw.exitCode,
|
|
@@ -1306,30 +1404,14 @@ const toolExecutor = (given, options = {}) => async (context) => {
|
|
|
1306
1404
|
context.emit({
|
|
1307
1405
|
type: "tool.result",
|
|
1308
1406
|
id: context.id,
|
|
1309
|
-
isError: run.exitCode !== 0 || json?.ok === false || declared !== null
|
|
1407
|
+
isError: run.exitCode !== 0 || json?.ok === false || declared !== null,
|
|
1408
|
+
...raw.cpu === void 0 ? {} : { cpu: raw.cpu }
|
|
1310
1409
|
});
|
|
1311
|
-
if (json?.ok === false) return
|
|
1312
|
-
|
|
1313
|
-
output,
|
|
1314
|
-
error: `${context.id}: ${json.error}`
|
|
1315
|
-
};
|
|
1316
|
-
if (declared !== null) return {
|
|
1317
|
-
ok: false,
|
|
1318
|
-
output,
|
|
1319
|
-
error: declared
|
|
1320
|
-
};
|
|
1410
|
+
if (json?.ok === false) return invalidOutput(output, json.error);
|
|
1411
|
+
if (declared !== null) return invalidOutput(output, declared);
|
|
1321
1412
|
if (json !== null) output["json"] = json.json;
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
output
|
|
1325
|
-
};
|
|
1326
|
-
const last = run.stderr.trim().split("\n").at(-1) ?? "";
|
|
1327
|
-
return {
|
|
1328
|
-
ok: false,
|
|
1329
|
-
transient: true,
|
|
1330
|
-
output,
|
|
1331
|
-
error: `${scrub(command.program)} exited with ${String(run.exitCode)}${last === "" ? "" : `: ${clipError(last)}`}`
|
|
1332
|
-
};
|
|
1413
|
+
const signedOut = integration?.signedOut;
|
|
1414
|
+
return exitEnd(output, run, context, scrub(command.program), signedOut);
|
|
1333
1415
|
};
|
|
1334
1416
|
// ../nodes/src/verify.ts
|
|
1335
1417
|
/**
|
|
@@ -1341,15 +1423,17 @@ const toolExecutor = (given, options = {}) => async (context) => {
|
|
|
1341
1423
|
/** A setup that failed: no session started, so nothing was spent on it. */
|
|
1342
1424
|
const beforeFailed = (ran) => {
|
|
1343
1425
|
const error = `its before step failed: ${ran.error ?? "no reason given"}`;
|
|
1344
|
-
return {
|
|
1426
|
+
return endedWith({
|
|
1345
1427
|
ok: false,
|
|
1346
|
-
error,
|
|
1347
1428
|
output: {
|
|
1348
1429
|
status: "fail",
|
|
1349
1430
|
summary: error,
|
|
1350
1431
|
before: ran.output
|
|
1351
1432
|
}
|
|
1352
|
-
}
|
|
1433
|
+
}, endOf({
|
|
1434
|
+
...ran,
|
|
1435
|
+
ok: false
|
|
1436
|
+
}));
|
|
1353
1437
|
};
|
|
1354
1438
|
/** With nothing to run a step, a report cannot be accepted. */
|
|
1355
1439
|
const noSteps = () => Promise.resolve({
|
|
@@ -1370,20 +1454,23 @@ const runStep = async (tool, given, { timeout, ...fields }, step) => {
|
|
|
1370
1454
|
type: "tool.shell",
|
|
1371
1455
|
...fields
|
|
1372
1456
|
};
|
|
1373
|
-
const context = {
|
|
1374
|
-
...given,
|
|
1375
|
-
step
|
|
1376
|
-
};
|
|
1377
1457
|
const ms = timeoutMs(timeout);
|
|
1378
1458
|
if (ms === void 0) return tool({
|
|
1379
|
-
...
|
|
1459
|
+
...given,
|
|
1460
|
+
step,
|
|
1380
1461
|
node
|
|
1381
1462
|
});
|
|
1463
|
+
const { clock, machine } = slotClock(given.hold, given.machine);
|
|
1464
|
+
const context = {
|
|
1465
|
+
...given,
|
|
1466
|
+
step,
|
|
1467
|
+
...machine === void 0 ? {} : { machine }
|
|
1468
|
+
};
|
|
1382
1469
|
const limit = new AbortController();
|
|
1383
1470
|
const finished = new AbortController();
|
|
1384
1471
|
let expired = false;
|
|
1385
1472
|
const watch = async () => {
|
|
1386
|
-
if (await heldTimeout(
|
|
1473
|
+
if (await heldTimeout(clock, ms, AbortSignal.any([context.signal, finished.signal]))) {
|
|
1387
1474
|
expired = true;
|
|
1388
1475
|
limit.abort();
|
|
1389
1476
|
}
|
|
@@ -1494,17 +1581,19 @@ const checkedResult = (result, { check, resumes }) => {
|
|
|
1494
1581
|
};
|
|
1495
1582
|
const reason = `its check failed: ${check.error ?? "no reason given"}`;
|
|
1496
1583
|
const error = result.ok ? reason : `${reason} (then: ${result.error ?? "no report"})`;
|
|
1497
|
-
return {
|
|
1584
|
+
return endedWith({
|
|
1498
1585
|
...result,
|
|
1499
1586
|
ok: false,
|
|
1500
|
-
error,
|
|
1501
1587
|
output: {
|
|
1502
1588
|
...result.output,
|
|
1503
1589
|
status: "fail",
|
|
1504
1590
|
summary: error,
|
|
1505
1591
|
verify
|
|
1506
1592
|
}
|
|
1507
|
-
}
|
|
1593
|
+
}, endOf(result.ok ? {
|
|
1594
|
+
...check,
|
|
1595
|
+
ok: false
|
|
1596
|
+
} : result));
|
|
1508
1597
|
};
|
|
1509
1598
|
// ../nodes/src/claude.ts
|
|
1510
1599
|
/**
|
|
@@ -1515,11 +1604,19 @@ const checkedResult = (result, { check, resumes }) => {
|
|
|
1515
1604
|
* (ADR-019).
|
|
1516
1605
|
*/
|
|
1517
1606
|
const MAX_INPUT = 8192;
|
|
1518
|
-
|
|
1607
|
+
const inputJson = (input) => JSON.stringify(input ?? {}, void 0, 2);
|
|
1608
|
+
/** What a sent-back input is whole, for `read_input`, when its note cuts it. */
|
|
1609
|
+
const wholeInput = (input) => {
|
|
1610
|
+
const json = inputJson(input);
|
|
1611
|
+
return json.length > MAX_INPUT ? json : void 0;
|
|
1612
|
+
};
|
|
1613
|
+
/** A re-entry's note of what was sent back; `read_input` has a cut rest. */
|
|
1519
1614
|
const reentryNote = (round, input) => {
|
|
1520
|
-
const json =
|
|
1521
|
-
const
|
|
1522
|
-
|
|
1615
|
+
const json = inputJson(input);
|
|
1616
|
+
const cut = json.length > MAX_INPUT;
|
|
1617
|
+
const body = cut ? `${json.slice(0, MAX_INPUT)}\n…` : json;
|
|
1618
|
+
const rest = cut ? `\n\nThat is the first ${String(MAX_INPUT)} of its ${String(json.length)} characters. Call read_input for all of it before you act on it.` : "";
|
|
1619
|
+
return `\n\n## Round ${String(round)}\n\nThe workflow sent this task back to you with:\n\n\`\`\`json\n${body}\n\`\`\`${rest}`;
|
|
1523
1620
|
};
|
|
1524
1621
|
/**
|
|
1525
1622
|
* What a resumed leader is told: it already has its brief and its earlier work,
|
|
@@ -1545,22 +1642,7 @@ const specFor = (spec, context) => {
|
|
|
1545
1642
|
};
|
|
1546
1643
|
/** Runs a `claude.leader` or `claude.worker` as one session. */
|
|
1547
1644
|
const claudeExecutor = (options) => async (context) => {
|
|
1548
|
-
const planOptions =
|
|
1549
|
-
cwd: context.cwd,
|
|
1550
|
-
inputs: context.inputs,
|
|
1551
|
-
outputs: context.outputs,
|
|
1552
|
-
inPlace: options.inPlace,
|
|
1553
|
-
workflowDir: context.workflowDir,
|
|
1554
|
-
projectRoot: context.projectRoot,
|
|
1555
|
-
...context.orkDir === void 0 ? {} : { orkDir: context.orkDir },
|
|
1556
|
-
pluginDir: path.join(context.runDir, "plugins", context.id)
|
|
1557
|
-
};
|
|
1558
|
-
if (context.checkout !== void 0) planOptions.checkout = context.checkout;
|
|
1559
|
-
if (context.brief !== void 0) planOptions.brief = context.brief;
|
|
1560
|
-
if (context.template !== void 0) planOptions.template = context.template;
|
|
1561
|
-
if (context.ownership !== void 0) planOptions.ownership = context.ownership;
|
|
1562
|
-
if (context.mailbox !== void 0) planOptions.mailbox = context.mailbox;
|
|
1563
|
-
if (context.claims !== void 0) planOptions.claims = context.claims;
|
|
1645
|
+
const planOptions = planOptionsOf(context, options.inPlace);
|
|
1564
1646
|
const before = beforeOf(context.node, context.workflow.defaults);
|
|
1565
1647
|
if (before !== void 0) {
|
|
1566
1648
|
const ran = await runStep(options.steps ?? noSteps, context, before, {
|
|
@@ -1569,49 +1651,56 @@ const claudeExecutor = (options) => async (context) => {
|
|
|
1569
1651
|
});
|
|
1570
1652
|
if (!ran.ok) return beforeFailed(ran);
|
|
1571
1653
|
await context.hold?.wait("start", context.signal);
|
|
1572
|
-
if (context.signal.aborted)
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1654
|
+
if (context.signal.aborted) {
|
|
1655
|
+
const end = stoppedEnd(context.signal);
|
|
1656
|
+
return beforeFailed({
|
|
1657
|
+
...ran,
|
|
1658
|
+
ok: false,
|
|
1659
|
+
error: end.message,
|
|
1660
|
+
end
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1576
1663
|
planOptions.before = ran.output;
|
|
1577
1664
|
}
|
|
1578
1665
|
const blocked = context.quota?.().blocked;
|
|
1579
|
-
if (blocked !== void 0) return notStarted(blocked, blocked);
|
|
1580
|
-
const budget =
|
|
1581
|
-
if (budget !== void 0 && budget <= 0) return notStarted("nothing is left of the run's budget");
|
|
1666
|
+
if (blocked !== void 0) return notStarted("plan_limit", blocked, blocked);
|
|
1667
|
+
const budget = await budgetToStart(context);
|
|
1668
|
+
if (budget !== void 0 && budget <= 0) return notStarted("budget_limit", "nothing is left of the run's budget");
|
|
1582
1669
|
if (budget !== void 0) planOptions.budgetLeftUsd = budget;
|
|
1583
1670
|
const plan = planNodeSession(context.workflow, context.id, planOptions);
|
|
1584
|
-
if (!plan.ok) return
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
error: plan.error
|
|
1671
|
+
if (!plan.ok) return notStarted("invalid_node", plan.error);
|
|
1672
|
+
const servers = namedMcpEntries(context.projectRoot, plan.mcp);
|
|
1673
|
+
if (!servers.ok) return notStarted("invalid_node", servers.error);
|
|
1674
|
+
const spec = {
|
|
1675
|
+
...specFor(plan.spec, context),
|
|
1676
|
+
...servers.value
|
|
1591
1677
|
};
|
|
1592
|
-
const spec = specFor(plan.spec, context);
|
|
1593
1678
|
if (options.machineClaude === true) spec.machineClaude = true;
|
|
1594
1679
|
const driver = sessionDriver({
|
|
1595
1680
|
override: options.driver,
|
|
1596
1681
|
named: plan.driver,
|
|
1597
1682
|
ready: options.readyDriver
|
|
1598
1683
|
});
|
|
1599
|
-
if (driver === void 0) return notStarted("no
|
|
1684
|
+
if (driver === void 0) return notStarted("tool_missing", "no agent driver is ready; see `ork doctor`");
|
|
1600
1685
|
const runtime = await options.runtimeFor(driver);
|
|
1601
|
-
const ports = { human: context.owner === void 0 ? context
|
|
1686
|
+
const ports = { human: context.owner === void 0 ? withdrawnOnStop(context) : ownerFirst(context, context.owner, {
|
|
1602
1687
|
runtimeFor: options.runtimeFor,
|
|
1603
1688
|
driver: options.driver,
|
|
1604
1689
|
readyDriver: options.readyDriver,
|
|
1605
1690
|
inPlace: options.inPlace
|
|
1606
1691
|
}) };
|
|
1607
1692
|
if (context.hold !== void 0) ports.hold = context.hold;
|
|
1693
|
+
if (context.machine !== void 0) ports.machine = context.machine;
|
|
1608
1694
|
if (context.ownership !== void 0) ports.ownership = context.ownership;
|
|
1609
1695
|
if (context.mailbox !== void 0) ports.mailbox = context.mailbox;
|
|
1610
1696
|
if (context.claims !== void 0) ports.claims = context.claims;
|
|
1611
1697
|
if (context.feedback !== void 0) ports.feedback = context.feedback;
|
|
1698
|
+
const whole = context.round > 1 ? wholeInput(context.input) : void 0;
|
|
1699
|
+
if (whole !== void 0) ports.input = whole;
|
|
1612
1700
|
const watch = limitWatch(context);
|
|
1613
1701
|
const run = async (asked) => {
|
|
1614
1702
|
const watching = watch.session(asked);
|
|
1703
|
+
context.reserveBudget?.(asked.budgetUsd);
|
|
1615
1704
|
const session = runtime.start(asked, ports);
|
|
1616
1705
|
const stop = () => {
|
|
1617
1706
|
session.interrupt();
|
|
@@ -1629,6 +1718,7 @@ const claudeExecutor = (options) => async (context) => {
|
|
|
1629
1718
|
context.emit(event);
|
|
1630
1719
|
}
|
|
1631
1720
|
} finally {
|
|
1721
|
+
context.reserveBudget?.(0);
|
|
1632
1722
|
disconnect?.();
|
|
1633
1723
|
context.signal.removeEventListener("abort", stop);
|
|
1634
1724
|
}
|
|
@@ -1691,10 +1781,11 @@ const claudeExecutor = (options) => async (context) => {
|
|
|
1691
1781
|
};
|
|
1692
1782
|
// ../nodes/src/gate.ts
|
|
1693
1783
|
/**
|
|
1694
|
-
* @file The `human.gate` node type: asks the human the rendered question,
|
|
1695
|
-
*
|
|
1696
|
-
*
|
|
1697
|
-
*
|
|
1784
|
+
* @file The `human.gate` node type: asks the human the rendered question, with
|
|
1785
|
+
* its document if it has one, both in readable text (no raw JSON), outputs
|
|
1786
|
+
* their choice with the note they gave, and answers with the `default` when
|
|
1787
|
+
* the `timeout` passes. The timeout counts only while the run and the gate
|
|
1788
|
+
* are not paused (ADR-041).
|
|
1698
1789
|
*/
|
|
1699
1790
|
/** A promise that never settles: the losing side of a race. */
|
|
1700
1791
|
const NEVER = new Promise(() => {});
|
|
@@ -1716,8 +1807,13 @@ const gateExecutor = async (context) => {
|
|
|
1716
1807
|
output: {},
|
|
1717
1808
|
error: `${context.id} is not a gate`
|
|
1718
1809
|
};
|
|
1719
|
-
const
|
|
1720
|
-
const
|
|
1810
|
+
const scope = runScope(context.inputs, context.outputs, context.runId);
|
|
1811
|
+
const render = (text) => {
|
|
1812
|
+
const template = parseTemplate(text);
|
|
1813
|
+
return template.ok ? renderTemplate(template.parts, scope, { readable: true }) : text;
|
|
1814
|
+
};
|
|
1815
|
+
const question = render(node.question);
|
|
1816
|
+
const document = render(node.document ?? "");
|
|
1721
1817
|
const options = node.options.map((o) => choiceOf(o));
|
|
1722
1818
|
const withdraw = new AbortController();
|
|
1723
1819
|
context.signal.addEventListener("abort", () => {
|
|
@@ -1734,6 +1830,7 @@ const gateExecutor = async (context) => {
|
|
|
1734
1830
|
}
|
|
1735
1831
|
};
|
|
1736
1832
|
if (node.recommended !== void 0) asked.recommended = node.recommended;
|
|
1833
|
+
if (document.trim() !== "") asked.document = document;
|
|
1737
1834
|
const answer = context.human.ask(asked).then((choice) => ({
|
|
1738
1835
|
choice,
|
|
1739
1836
|
answeredBy: "user",
|
|
@@ -1752,21 +1849,22 @@ const gateExecutor = async (context) => {
|
|
|
1752
1849
|
})];
|
|
1753
1850
|
try {
|
|
1754
1851
|
const output = await Promise.race([answer, ...expired]);
|
|
1755
|
-
if (!options.some((o) => o.id === output.choice))
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1852
|
+
if (!options.some((o) => o.id === output.choice)) {
|
|
1853
|
+
const facts = node.timeout === void 0 ? {} : { timeout: node.timeout };
|
|
1854
|
+
return endedWith({
|
|
1855
|
+
ok: false,
|
|
1856
|
+
output
|
|
1857
|
+
}, endFor("timed_out", facts, "no one answered the gate"));
|
|
1858
|
+
}
|
|
1760
1859
|
return {
|
|
1761
1860
|
ok: true,
|
|
1762
1861
|
output
|
|
1763
1862
|
};
|
|
1764
1863
|
} catch {
|
|
1765
|
-
return {
|
|
1864
|
+
return endedWith({
|
|
1766
1865
|
ok: false,
|
|
1767
|
-
output: {}
|
|
1768
|
-
|
|
1769
|
-
};
|
|
1866
|
+
output: {}
|
|
1867
|
+
}, endFor("interrupted", {}, "the gate was withdrawn"));
|
|
1770
1868
|
} finally {
|
|
1771
1869
|
answered.abort();
|
|
1772
1870
|
}
|
|
@@ -1781,18 +1879,16 @@ const gateExecutor = async (context) => {
|
|
|
1781
1879
|
*/
|
|
1782
1880
|
/** A node's `timeout:` is what limits a call; this only lifts the SDK's 60 s. */
|
|
1783
1881
|
const NO_LIMIT_MS = 864e5;
|
|
1784
|
-
const failed = (
|
|
1882
|
+
const failed = (kind, facts, detail, output = {}) => endedWith({
|
|
1785
1883
|
ok: false,
|
|
1786
|
-
output
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
const
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
error
|
|
1795
|
-
});
|
|
1884
|
+
output
|
|
1885
|
+
}, endFor(kind, facts, detail));
|
|
1886
|
+
/** How a call that threw ended: a sign-in, a server that won't start, or else. */
|
|
1887
|
+
const thrownKind = (error) => {
|
|
1888
|
+
const code = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
1889
|
+
if (code === 401 || error?.constructor.name === "UnauthorizedError") return "auth";
|
|
1890
|
+
return code === "ENOENT" ? "tool_missing" : "command_failed";
|
|
1891
|
+
};
|
|
1796
1892
|
/**
|
|
1797
1893
|
* A string's templates rendered. A string that is one `{{ … }}` and nothing
|
|
1798
1894
|
* else keeps its value's type, so a tool that asks for a number gets one.
|
|
@@ -1836,19 +1932,22 @@ const reasonOf = (error, server) => {
|
|
|
1836
1932
|
};
|
|
1837
1933
|
const mcpCallExecutor = (options = {}) => async (context) => {
|
|
1838
1934
|
const { node } = context;
|
|
1839
|
-
if (node.type !== "mcp.call")
|
|
1935
|
+
if (node.type !== "mcp.call") {
|
|
1936
|
+
const said = `${context.id} is not an MCP call`;
|
|
1937
|
+
return failed("unknown", {}, said);
|
|
1938
|
+
}
|
|
1840
1939
|
const environ = options.environ ?? process.env;
|
|
1841
1940
|
const declared = readMcpServer(context.projectRoot, node.server);
|
|
1842
|
-
if (!declared.ok) return failed(declared.error);
|
|
1941
|
+
if (!declared.ok) return failed("invalid_node", { reason: declared.error });
|
|
1843
1942
|
const nodeEnv = renderEnv(node, context, environ);
|
|
1844
|
-
if (!nodeEnv.ok) return failed(nodeEnv.error);
|
|
1943
|
+
if (!nodeEnv.ok) return failed("invalid_node", { reason: nodeEnv.error });
|
|
1845
1944
|
const expanded = expandServer(declared.value, {
|
|
1846
1945
|
...environ,
|
|
1847
1946
|
...nodeEnv.values
|
|
1848
1947
|
}, node.plainEnv);
|
|
1849
|
-
if (!expanded.ok) return failed(expanded.error);
|
|
1948
|
+
if (!expanded.ok) return failed("invalid_node", { reason: expanded.error });
|
|
1850
1949
|
const scrub = scrubberOf([...secretsOf(context.workflow, environ, options.maskedNames), ...expanded.value.values]);
|
|
1851
|
-
const scope = runScope(context.inputs, context.outputs, context.runId);
|
|
1950
|
+
const scope = runScope(context.inputs, context.outputs, context.runId, context.item);
|
|
1852
1951
|
const args = Object.fromEntries(Object.entries(node.arguments ?? {}).map(([key, value]) => [key, renderValue(value, scope)]));
|
|
1853
1952
|
const name = `${node.server}.${node.tool}`;
|
|
1854
1953
|
context.emit({
|
|
@@ -1889,19 +1988,22 @@ const mcpCallExecutor = (options = {}) => async (context) => {
|
|
|
1889
1988
|
id: context.id,
|
|
1890
1989
|
isError: reported || tooLong || problem !== null
|
|
1891
1990
|
});
|
|
1892
|
-
if (reported) return
|
|
1893
|
-
if (tooLong)
|
|
1894
|
-
|
|
1991
|
+
if (reported) return failed("command_failed", { program: name }, said, output);
|
|
1992
|
+
if (tooLong) {
|
|
1993
|
+
const reason = `${name}'s result is over 64 KiB`;
|
|
1994
|
+
return failed("invalid_output", { reason }, void 0, output);
|
|
1995
|
+
}
|
|
1996
|
+
return problem === null ? endedWith({
|
|
1895
1997
|
ok: true,
|
|
1896
1998
|
output
|
|
1897
|
-
} : failed(problem, output);
|
|
1999
|
+
}, endFor("done")) : failed("invalid_output", { reason: problem }, void 0, output);
|
|
1898
2000
|
} catch (error) {
|
|
1899
2001
|
context.emit({
|
|
1900
2002
|
type: "tool.result",
|
|
1901
2003
|
id: context.id,
|
|
1902
2004
|
isError: true
|
|
1903
2005
|
});
|
|
1904
|
-
return
|
|
2006
|
+
return failed(thrownKind(error), { program: `MCP server \`${node.server}\`` }, clipError(scrub(reasonOf(error, node.server))));
|
|
1905
2007
|
} finally {
|
|
1906
2008
|
await client.close().catch(() => {});
|
|
1907
2009
|
}
|
|
@@ -1912,13 +2014,39 @@ const mcpCallExecutor = (options = {}) => async (context) => {
|
|
|
1912
2014
|
* text. A program runs in its own process group, so aborting it reaches
|
|
1913
2015
|
* whatever it started: SIGTERM, then SIGKILL for what ignores that. A stop or
|
|
1914
2016
|
* a timeout therefore leaves nothing running, and an escaped process that
|
|
1915
|
-
* keeps the output pipes open cannot hold the node.
|
|
2017
|
+
* keeps the output pipes open cannot hold the node. Off Windows it runs under
|
|
2018
|
+
* `sh`, whose `times` says the CPU the program and its children used, and at
|
|
2019
|
+
* the project's priority: `low` starts it below Ork's own (nice, and on macOS
|
|
2020
|
+
* utility QoS).
|
|
1916
2021
|
*/
|
|
1917
2022
|
/** How long a program may take to end after SIGTERM. */
|
|
1918
2023
|
const KILL_AFTER_MS = 2e3;
|
|
1919
2024
|
/** How long to wait for the pipes to close after SIGKILL. */
|
|
1920
2025
|
const CLOSE_AFTER_MS = 500;
|
|
1921
|
-
|
|
2026
|
+
/**
|
|
2027
|
+
* Runs `$0 "$@"`, then writes the shell's `times` to fd 3: its children's user
|
|
2028
|
+
* and system time, the program's whole tree that it waited for. A program that
|
|
2029
|
+
* isn't there says so on fd 3, as spawn's ENOENT would.
|
|
2030
|
+
*/
|
|
2031
|
+
const TIMED = String.raw`command -v "$0" >/dev/null 2>&1 || { echo ork-missing >&3; exit 127; }; "$0" "$@" 3>&-; s=$?; times >&3; exit $s`;
|
|
2032
|
+
const MISSING = "ork-missing";
|
|
2033
|
+
/** The children's line of `times` (`0m1.250s 0m0.300s`), in milliseconds. */
|
|
2034
|
+
const cpuOfTimes = (text) => {
|
|
2035
|
+
const [, , user, system] = [...text.matchAll(/(\d+)m([\d.]+)s/gu)].map(([, m = "0", s = "0"]) => Number(m) * 60 + Number(s));
|
|
2036
|
+
return user === void 0 || system === void 0 ? void 0 : {
|
|
2037
|
+
userMs: Math.round(user * 1e3),
|
|
2038
|
+
systemMs: Math.round(system * 1e3)
|
|
2039
|
+
};
|
|
2040
|
+
};
|
|
2041
|
+
const timed = process.platform !== "win32";
|
|
2042
|
+
/** What to start: the program, under `sh` to time it, at the priority. */
|
|
2043
|
+
const startOf = (program, args, priority) => timed ? startAt(priority, "sh", [
|
|
2044
|
+
"-c",
|
|
2045
|
+
TIMED,
|
|
2046
|
+
program,
|
|
2047
|
+
...args
|
|
2048
|
+
]) : [program, [...args]];
|
|
2049
|
+
const spawnTool = (program, args, { cwd, signal, env, priority }) => {
|
|
1922
2050
|
if (signal.aborted) return Promise.resolve({
|
|
1923
2051
|
exitCode: -1,
|
|
1924
2052
|
stdout: "",
|
|
@@ -1928,7 +2056,9 @@ const spawnTool = (program, args, { cwd, signal, env }) => {
|
|
|
1928
2056
|
const stdout = [];
|
|
1929
2057
|
const stderr = [];
|
|
1930
2058
|
const timers = [];
|
|
1931
|
-
const
|
|
2059
|
+
const times = [];
|
|
2060
|
+
const [bin, argv] = startOf(program, args, priority);
|
|
2061
|
+
const child = spawn(bin, argv, {
|
|
1932
2062
|
cwd,
|
|
1933
2063
|
detached: true,
|
|
1934
2064
|
env: {
|
|
@@ -1938,9 +2068,15 @@ const spawnTool = (program, args, { cwd, signal, env }) => {
|
|
|
1938
2068
|
stdio: [
|
|
1939
2069
|
"ignore",
|
|
1940
2070
|
"pipe",
|
|
2071
|
+
"pipe",
|
|
1941
2072
|
"pipe"
|
|
1942
2073
|
]
|
|
1943
2074
|
});
|
|
2075
|
+
lowerChild(priority, child.pid);
|
|
2076
|
+
const fd3 = child.stdio[3];
|
|
2077
|
+
if (fd3 instanceof Readable) fd3.setEncoding("utf8").on("data", (chunk) => {
|
|
2078
|
+
times.push(chunk);
|
|
2079
|
+
});
|
|
1944
2080
|
const killGroup = (kill) => {
|
|
1945
2081
|
if (child.pid === void 0) return;
|
|
1946
2082
|
try {
|
|
@@ -1952,11 +2088,21 @@ const spawnTool = (program, args, { cwd, signal, env }) => {
|
|
|
1952
2088
|
signal.removeEventListener("abort", abort);
|
|
1953
2089
|
resolve(run);
|
|
1954
2090
|
};
|
|
1955
|
-
const collected = (exitCode) =>
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
2091
|
+
const collected = (exitCode) => {
|
|
2092
|
+
const said = times.join("");
|
|
2093
|
+
if (said.includes(MISSING)) return {
|
|
2094
|
+
exitCode: -1,
|
|
2095
|
+
stdout: stdout.join(""),
|
|
2096
|
+
stderr: `could not run ${program}: spawn ${program} ENOENT`
|
|
2097
|
+
};
|
|
2098
|
+
const cpu = cpuOfTimes(said);
|
|
2099
|
+
return {
|
|
2100
|
+
exitCode,
|
|
2101
|
+
stdout: stdout.join(""),
|
|
2102
|
+
stderr: stderr.join(""),
|
|
2103
|
+
...cpu === void 0 ? {} : { cpu }
|
|
2104
|
+
};
|
|
2105
|
+
};
|
|
1960
2106
|
const abort = () => {
|
|
1961
2107
|
killGroup("SIGTERM");
|
|
1962
2108
|
timers.push(setTimeout(() => {
|
|
@@ -1967,10 +2113,10 @@ const spawnTool = (program, args, { cwd, signal, env }) => {
|
|
|
1967
2113
|
}, KILL_AFTER_MS));
|
|
1968
2114
|
};
|
|
1969
2115
|
signal.addEventListener("abort", abort, { once: true });
|
|
1970
|
-
child.stdout
|
|
2116
|
+
child.stdout?.setEncoding("utf8").on("data", (chunk) => {
|
|
1971
2117
|
stdout.push(chunk);
|
|
1972
2118
|
});
|
|
1973
|
-
child.stderr
|
|
2119
|
+
child.stderr?.setEncoding("utf8").on("data", (chunk) => {
|
|
1974
2120
|
stderr.push(chunk);
|
|
1975
2121
|
});
|
|
1976
2122
|
child.on("error", (error) => {
|
|
@@ -2130,4 +2276,4 @@ const missingPrograms = (workflow, installed = (program) => onPath(program)) =>
|
|
|
2130
2276
|
}
|
|
2131
2277
|
return [...missing.values()];
|
|
2132
2278
|
};
|
|
2133
|
-
export {
|
|
2279
|
+
export { namedMcpEntries as _, builtinExecutors as a, toolExecutor as c, maskedEnv as d, maskedNamesOfRun as f, clearOutputFiles as g, sessionEnv as h, onPath as i, clearStateFiles as l, runScrubber as m, missingProgramMessage as n, spawnTool as o, runMarkerIn as p, missingPrograms as r, mcpCallExecutor as s, envProblems as t, inheritedScrubber as u, resultOf as v };
|