@krapht/ork 1.0.0-alpha.0 → 1.0.0-alpha.10

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.
Files changed (41) hide show
  1. package/README.md +5 -6
  2. package/dist/{__build_info__-DdTVd2p5.mjs → __build_info__-DFF18K8y.mjs} +72 -3
  3. package/dist/bin.mjs +603 -384
  4. package/dist/{clean-NnboOuz6.mjs → clean-DVaEH9e7.mjs} +5 -2
  5. package/dist/{completion-snnYGjgK.mjs → completion-Dv5kf598.mjs} +6 -6
  6. package/dist/context-CEaKOVmG.mjs +683 -0
  7. package/dist/{decide-B81wCZQg.mjs → decide-3l9q8UtX.mjs} +3 -3
  8. package/dist/{find-BKW-uGey.mjs → find-DEjjOlTG.mjs} +1 -1
  9. package/dist/format-Cb5_m-Qd.mjs +191 -0
  10. package/dist/{in-a-run-BPXX09Bs.mjs → in-a-run-BS9Nc-JW.mjs} +1 -1
  11. package/dist/{limits-JCve2QpI.mjs → limits-BSwEjQST.mjs} +1 -1
  12. package/dist/{list-C7UEgmtd.mjs → list-Qxr6rwsY.mjs} +11 -4
  13. package/dist/{mcp-CKPyNaKD.mjs → mcp-CslnG5wf.mjs} +6 -6
  14. package/dist/output-Bqljtmp_.mjs +40 -0
  15. package/dist/{pause-B_SL_yDz.mjs → pause-D7OVB5Ac.mjs} +4 -4
  16. package/dist/{relay-o-Luee1c.mjs → relay-EceRIebO.mjs} +2 -2
  17. package/dist/{remove-DAZ7ZCzt.mjs → remove-0IQZVWmI.mjs} +14 -7
  18. package/dist/{send-3B64fBNB.mjs → send-C0uwfUYi.mjs} +3 -3
  19. package/dist/{src-DHvWU4tX.mjs → src-Df6KOIDC.mjs} +5682 -3248
  20. package/dist/{src-C38BPzit.mjs → src-cmmXKHn1.mjs} +266 -203
  21. package/dist/{src-BJTegEzY.mjs → src-i5k2Y-kf.mjs} +40 -88
  22. package/dist/src-oaJYIddp.mjs +2255 -0
  23. package/dist/ui/THIRD_PARTY_LICENSES.txt +594 -0
  24. package/dist/ui/assets/app-C8uhHVYp.css +1 -0
  25. package/dist/ui/assets/app-qs8Lofv6.js +75 -0
  26. package/dist/ui/assets/{index-D83F2OKZ.js → index-DV1ZaOk-.js} +3 -3
  27. package/dist/ui/assets/theme-CdXvqTQ6.js +1 -0
  28. package/dist/ui/index.html +2 -2
  29. package/dist/{ui-CY4xxJUu.mjs → ui-CBkHJdqk.mjs} +4 -4
  30. package/dist/{usage-CJnytoe4.mjs → usage-Cd8pEYyK.mjs} +5 -5
  31. package/dist/{watch-BJB82ZJ7.mjs → watch-DW5R2OPO.mjs} +6 -6
  32. package/dist/{workflows-CiM7S3l0.mjs → workflows-BT1gDRIV.mjs} +4 -3
  33. package/dist/{worktrees-86ySXxR6.mjs → worktrees-g1MQMGie.mjs} +78 -19
  34. package/package.json +2 -4
  35. package/dist/context-DO3sBy9v.mjs +0 -274
  36. package/dist/format-CmTlTOjT.mjs +0 -427
  37. package/dist/output-B1V6ZJSx.mjs +0 -950
  38. package/dist/src-Dyz_Gy1q.mjs +0 -1183
  39. package/dist/ui/assets/app-C8nJ6NpT.js +0 -67
  40. package/dist/ui/assets/app-CH7YC3in.css +0 -1
  41. package/dist/ui/assets/theme-3xH5jxKs.js +0 -1
@@ -1,6 +1,6 @@
1
- import { $n as timeoutMs, $t as parseTemplate, At as runScope, F as declaredProblem, Jn as beforeOf, Mt as envReadsOf, Ot as heldTimeout, Pt as declaredOutputOf, Qt as evaluate, Vn as markValue, Xn as isToolNode, Zn as programsRunBy, en as renderTemplate, er as INTEGRATIONS, ft as sessionDriver, jt as calledWorkflows, mt as reportSchemaFor, nr as integrationFor, s as planReason, tr as TOOL_PREFIX, ut as planNodeSession } from "./src-DHvWU4tX.mjs";
1
+ import { Bt as mcpNotText, Ft as declaredOutputOf, Hn as beforeOf, Mt as calledWorkflows, Nt as envReadsOf, Un as programsRunBy, Vt as serversIn, ar as timeoutMs, cr as integrationFor, ct as planNodeSession, en as evaluate, er as isClaudeNode, ft as reportSchemaFor, jt as declaredProblem, kt as runScope, nn as renderTemplate, or as INTEGRATIONS, qn as markValue, s as planReason, sr as TOOL_PREFIX, tn as parseTemplate, tr as isToolNode, ut as sessionDriver, yt as heldTimeout, zt as MCP_FILE } from "./src-Df6KOIDC.mjs";
2
2
  import path from "node:path";
3
- import { accessSync, constants, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
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";
@@ -35,7 +35,7 @@ const callExecutor = (context) => {
35
35
  output: { status: "fail" },
36
36
  error: "This run cannot call workflows."
37
37
  });
38
- const scope = runScope(context.inputs, context.outputs, context.runId);
38
+ const scope = runScope(context.inputs, context.outputs, context.runId, context.item);
39
39
  const render = (text) => {
40
40
  const template = parseTemplate(text);
41
41
  return template.ok ? renderTemplate(template.parts, scope) : text;
@@ -61,6 +61,32 @@ const callExecutor = (context) => {
61
61
  }
62
62
  }, context.signal).then(limitedCall);
63
63
  };
64
+ // ../nodes/src/claude-plan.ts
65
+ /**
66
+ * @file What a Claude node's session is planned with, read from the node's
67
+ * context.
68
+ */
69
+ /** The plan options every session of a node starts from. */
70
+ const planOptionsOf = (context, inPlace) => {
71
+ const planOptions = {
72
+ cwd: context.cwd,
73
+ inputs: context.inputs,
74
+ outputs: context.outputs,
75
+ inPlace,
76
+ workflowDir: context.workflowDir,
77
+ projectRoot: context.projectRoot,
78
+ ...context.orkDir === void 0 ? {} : { orkDir: context.orkDir },
79
+ pluginDir: path.join(context.runDir, "plugins", context.id)
80
+ };
81
+ if (context.checkout !== void 0) planOptions.checkout = context.checkout;
82
+ if (context.brief !== void 0) planOptions.brief = context.brief;
83
+ if (context.template !== void 0) planOptions.template = context.template;
84
+ if (context.item !== void 0) planOptions.item = context.item;
85
+ if (context.ownership !== void 0) planOptions.ownership = context.ownership;
86
+ if (context.mailbox !== void 0) planOptions.mailbox = context.mailbox;
87
+ if (context.claims !== void 0) planOptions.claims = context.claims;
88
+ return planOptions;
89
+ };
64
90
  // ../nodes/src/claude-result.ts
65
91
  /**
66
92
  * @file A finished session as a node's result: its report is the output, held
@@ -123,6 +149,156 @@ const resultOf = (outcome, declared) => {
123
149
  if (!result.ok) result.error = report.summary;
124
150
  return result;
125
151
  };
152
+ // ../nodes/src/environ.ts
153
+ /** A variable of the user's. */
154
+ const readEnv = (environ, name) => Object.hasOwn(environ, name) ? environ[name] : void 0;
155
+ // ../nodes/src/mcp-servers.ts
156
+ /**
157
+ * @file Reading an MCP server's definition from the project's `.mcp.json`, the
158
+ * file Claude Code reads too, so a server is defined in one place (RFC-006).
159
+ * `${VAR}` and `${VAR:-default}` read the user's environment, which is where
160
+ * secrets stay: Ork stores none, and a run masks each value read this way. A
161
+ * server that needs an OAuth sign-in holds Claude Code's tokens, not Ork's,
162
+ * so it is refused.
163
+ */
164
+ const VARIABLE = /\$\{([A-Za-z_]\w*)(?::-([^}]*))?\}/gu;
165
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
166
+ const stringsOf = (value) => isRecord(value) ? Object.fromEntries(Object.entries(value).flatMap(([key, item]) => typeof item === "string" ? [[key, item]] : [])) : {};
167
+ const listOf = (value) => Array.isArray(value) ? value.flatMap((item) => typeof item === "string" ? [item] : []) : [];
168
+ /** The server as written, or why Ork can't call it. */
169
+ const readMcpServer = (root, name) => {
170
+ const servers = serversIn(root);
171
+ if (!servers.ok) return servers;
172
+ const known = Object.keys(servers.value);
173
+ const server = Object.hasOwn(servers.value, name) ? servers.value[name] : void 0;
174
+ if (!isRecord(server)) return {
175
+ ok: false,
176
+ error: `no MCP server \`${name}\` in ${MCP_FILE}${known.length === 0 ? "" : `, which has ${known.join(", ")}`}`
177
+ };
178
+ if ("oauth" in server) return {
179
+ ok: false,
180
+ 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`
181
+ };
182
+ const notText = mcpNotText(server);
183
+ if (notText !== void 0) return {
184
+ ok: false,
185
+ error: `MCP server \`${name}\` has a value in ${notText} that isn't a string: write it in quotes`
186
+ };
187
+ const { type, url, command } = server;
188
+ if (type === "http" && typeof url === "string") return {
189
+ ok: true,
190
+ value: {
191
+ kind: "http",
192
+ url,
193
+ headers: stringsOf(server["headers"])
194
+ }
195
+ };
196
+ if ((type === void 0 || type === "stdio") && typeof command === "string") return {
197
+ ok: true,
198
+ value: {
199
+ kind: "stdio",
200
+ command,
201
+ args: listOf(server["args"]),
202
+ env: stringsOf(server["env"])
203
+ }
204
+ };
205
+ return {
206
+ ok: false,
207
+ error: `MCP server \`${name}\` is not one Ork can call: it takes stdio and http servers`
208
+ };
209
+ };
210
+ /**
211
+ * The named servers' entries as written, for a session: `${VAR}` stays text for
212
+ * the `claude` that loads them to expand. The compiler has checked the names.
213
+ */
214
+ const namedMcpEntries = (root, names) => {
215
+ if (names.length === 0) return {
216
+ ok: true,
217
+ value: {}
218
+ };
219
+ const servers = serversIn(root);
220
+ if (!servers.ok) return servers;
221
+ const mcpServers = {};
222
+ for (const name of names) {
223
+ const server = Object.hasOwn(servers.value, name) ? servers.value[name] : void 0;
224
+ if (!isRecord(server)) return {
225
+ ok: false,
226
+ error: `no MCP server \`${name}\` in ${MCP_FILE}`
227
+ };
228
+ mcpServers[name] = server;
229
+ }
230
+ return {
231
+ ok: true,
232
+ value: { mcpServers }
233
+ };
234
+ };
235
+ /** Each variable a server reads, and whether it has a default. */
236
+ const readsOf = (server) => {
237
+ return (server.kind === "stdio" ? [
238
+ server.command,
239
+ ...server.args,
240
+ ...Object.values(server.env)
241
+ ] : [server.url, ...Object.values(server.headers)]).flatMap((text) => [...text.matchAll(VARIABLE)].map((match) => ({
242
+ name: String(match[1]),
243
+ fallback: match[2] !== void 0
244
+ })));
245
+ };
246
+ /**
247
+ * The server with its variables filled in, and the values to mask: all it read
248
+ * but those `plain` names, which aren't secret.
249
+ */
250
+ const expandServer = (server, environ, plain = []) => {
251
+ const unset = readsOf(server).find(({ name, fallback }) => !fallback && readEnv(environ, name) === void 0);
252
+ if (unset !== void 0) return {
253
+ ok: false,
254
+ error: `${MCP_FILE} reads ${unset.name}, which is not set`
255
+ };
256
+ const values = [];
257
+ const fill = (text) => text.replaceAll(VARIABLE, (_all, name, fallback) => {
258
+ const value = readEnv(environ, name);
259
+ if (value !== void 0 && !plain.includes(name)) values.push(value);
260
+ return value ?? fallback ?? "";
261
+ });
262
+ const fillAll = (record) => Object.fromEntries(Object.entries(record).map(([key, text]) => [key, fill(text)]));
263
+ return {
264
+ ok: true,
265
+ value: {
266
+ server: server.kind === "stdio" ? {
267
+ kind: "stdio",
268
+ command: fill(server.command),
269
+ args: server.args.map((arg) => fill(arg)),
270
+ env: fillAll(server.env)
271
+ } : {
272
+ kind: "http",
273
+ url: fill(server.url),
274
+ headers: fillAll(server.headers)
275
+ },
276
+ values
277
+ }
278
+ };
279
+ };
280
+ /** What a workflow's MCP servers read of the user's, unless called not secret. */
281
+ const mcpVariablesOf = (workflow, root) => Object.values(workflow.nodes).flatMap((node) => {
282
+ if (node.type !== "mcp.call") return [];
283
+ const read = readMcpServer(root, node.server);
284
+ return read.ok ? readsOf(read.value).map(({ name }) => name).filter((name) => node.plainEnv?.includes(name) !== true) : [];
285
+ });
286
+ /** A stdio server's program, when its name can be known before it is filled in. */
287
+ const programOf = (server) => server.kind === "stdio" && !server.command.includes("$") ? server.command : void 0;
288
+ /**
289
+ * What stops a workflow's `mcp.call` nodes, found before a run spends anything:
290
+ * a server that isn't there or can't be called, a program that isn't installed,
291
+ * or a variable that is not set.
292
+ */
293
+ const mcpProblems = (workflow, root, environ = process.env, finished = /* @__PURE__ */ new Set(), installed = () => true) => Object.entries(workflow.nodes).flatMap(([id, node]) => {
294
+ if (node.type !== "mcp.call" || finished.has(id)) return [];
295
+ const read = readMcpServer(root, node.server);
296
+ if (!read.ok) return [`${id}: ${read.error}`];
297
+ const given = new Set(Object.keys(node.env ?? {}));
298
+ 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`]);
299
+ const program = programOf(read.value);
300
+ return program === void 0 || installed(program) ? unset : [`\`${id}\`: MCP server \`${node.server}\` runs \`${program}\`, which is not installed`].concat(unset);
301
+ });
126
302
  /** The only built-in tools a fork has: it reads, and reports. */
127
303
  const FORK_TOOLS = [
128
304
  "Read",
@@ -277,25 +453,39 @@ const askFork = async (context, owner, question, deps) => {
277
453
  /** What the person sees of a question its asker's owner passed on. */
278
454
  const passedOnContext = (context, owner, note) => [...context === void 0 ? [] : [context, ""], note === void 0 ? `${owner} passed this on.` : `${owner} passed this on: ${note}`].join("\n");
279
455
  /**
456
+ * The node's channel to the person: what it asked is withdrawn when it stops,
457
+ * so a stopped node leaves no decision open.
458
+ */
459
+ const withdrawnOnStop = (context) => {
460
+ const { human } = context;
461
+ const signalOf = (asked) => asked === void 0 ? context.signal : AbortSignal.any([asked, context.signal]);
462
+ return {
463
+ permit: (request) => human.permit({
464
+ ...request,
465
+ signal: signalOf(request.signal)
466
+ }),
467
+ ask: (question) => human.ask({
468
+ ...question,
469
+ signal: signalOf(question.signal)
470
+ }),
471
+ ...human.settled === void 0 ? {} : { settled: human.settled }
472
+ };
473
+ };
474
+ /**
280
475
  * The child's channel when its owner takes its questions first: a question goes
281
476
  * to a fork of the owner's session, then to the person if the fork passes it on
282
477
  * or can't answer. Permission prompts go to the person as before.
283
478
  */
284
479
  const ownerFirst = (context, owner, deps) => {
285
- const { human } = context;
480
+ const human = withdrawnOnStop(context);
286
481
  return {
287
482
  permit: (request) => human.permit(request),
288
483
  ask: async (question) => {
289
484
  const answered = await askFork(context, owner, question, deps);
290
485
  if (context.signal.aborted) throw new Error(`${context.id} was stopped`);
291
- const signal = question.signal === void 0 ? context.signal : AbortSignal.any([question.signal, context.signal]);
292
- if (answered === null) return human.ask({
293
- ...question,
294
- signal
295
- });
486
+ if (answered === null) return human.ask(question);
296
487
  if (answered.choice === passOnId(question.options)) return human.ask({
297
488
  ...question,
298
- signal,
299
489
  owner: owner.id,
300
490
  context: passedOnContext(question.context, owner.id, answered.note)
301
491
  });
@@ -379,6 +569,20 @@ const budgetNow = (context) => {
379
569
  return known.length === 0 ? void 0 : Math.min(...known);
380
570
  };
381
571
  /**
572
+ * What a node's first session may spend, set aside from the run's budget in the
573
+ * same step that reads it (ADR-077), so a node starting beside it is never
574
+ * offered the same part: the least of what is left and the node's own
575
+ * `budgetUsd`. With nothing left, it waits for a worker running beside it to
576
+ * finish, as long as one may.
577
+ */
578
+ const budgetToStart = async (context) => {
579
+ const left = budgetNow(context);
580
+ if (left !== void 0 && left <= 0) return await context.budgetFreed?.(context.signal) === true ? budgetToStart(context) : left;
581
+ const own = isClaudeNode(context.node) ? context.node.budgetUsd ?? context.workflow.defaults?.budgetUsd : void 0;
582
+ context.reserveBudget?.(own === void 0 || left === void 0 ? left : Math.min(own, left));
583
+ return left;
584
+ };
585
+ /**
382
586
  * Whether a limit across runs, rather than the node's work, ended a node: a
383
587
  * session whose cap was a quota's and that ran into it, or one its plan refused
384
588
  * and that gave no report. A node that still succeeded is left alone.
@@ -660,163 +864,6 @@ const tokenize = (command) => {
660
864
  args
661
865
  };
662
866
  };
663
- // ../nodes/src/environ.ts
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
667
- /**
668
- * @file Reading an MCP server's definition from the project's `.mcp.json`, the
669
- * file Claude Code reads too, so a server is defined in one place (RFC-006).
670
- * `${VAR}` and `${VAR:-default}` read the user's environment, which is where
671
- * secrets stay: Ork stores none, and a run masks each value read this way. A
672
- * server that needs an OAuth sign-in holds Claude Code's tokens, not Ork's,
673
- * so it is refused.
674
- */
675
- const MCP_FILE = ".mcp.json";
676
- const VARIABLE = /\$\{([A-Za-z_]\w*)(?::-([^}]*))?\}/gu;
677
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
678
- /** The file's `mcpServers`; `{}` when there is no file. */
679
- const serversIn = (root) => {
680
- const file = path.join(root, MCP_FILE);
681
- if (!existsSync(file)) return {
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
- };
698
- }
699
- };
700
- const stringsOf = (value) => isRecord(value) ? Object.fromEntries(Object.entries(value).flatMap(([key, item]) => typeof item === "string" ? [[key, item]] : [])) : {};
701
- const listOf = (value) => Array.isArray(value) ? value.flatMap((item) => typeof item === "string" ? [item] : []) : [];
702
- /** A server's field that holds something other than text: it would be dropped. */
703
- const notTextIn = (server) => [
704
- "args",
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
- };
764
- /**
765
- * The server with its variables filled in, and the values to mask: all it read
766
- * but those `plain` names, which aren't secret.
767
- */
768
- const expandServer = (server, environ, plain = []) => {
769
- const unset = readsOf(server).find(({ name, fallback }) => !fallback && readEnv(environ, name) === void 0);
770
- if (unset !== void 0) return {
771
- ok: false,
772
- error: `${MCP_FILE} reads ${unset.name}, which is not set`
773
- };
774
- const values = [];
775
- const fill = (text) => text.replaceAll(VARIABLE, (_all, name, fallback) => {
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 {
782
- ok: true,
783
- value: {
784
- server: server.kind === "stdio" ? {
785
- kind: "stdio",
786
- command: fill(server.command),
787
- args: server.args.map((arg) => fill(arg)),
788
- env: fillAll(server.env)
789
- } : {
790
- kind: "http",
791
- url: fill(server.url),
792
- headers: fillAll(server.headers)
793
- },
794
- values
795
- }
796
- };
797
- };
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
867
  // ../nodes/src/tool-scrub.ts
821
868
  /**
822
869
  * @file Masking a secret in what a program prints. A plain search, so a value
@@ -998,7 +1045,9 @@ const IDENTITY_KEYS = /* @__PURE__ */ new Set([
998
1045
  "ORK_WORKFLOW",
999
1046
  "ORK_BIN",
1000
1047
  OUTPUT_ENV,
1001
- STATE_ENV
1048
+ STATE_ENV,
1049
+ "ORK_ITEM",
1050
+ "ORK_INDEX"
1002
1051
  ]);
1003
1052
  /**
1004
1053
  * `env` without Ork's identity variables. A run started by a program in another
@@ -1026,7 +1075,11 @@ const identityOf = (context, orkBin) => ({
1026
1075
  ORK_RUN_DIR: context.runDir,
1027
1076
  ORK_PROJECT: context.projectRoot,
1028
1077
  ORK_WORKFLOW: context.workflow.name,
1029
- ...orkBin === void 0 ? {} : { ORK_BIN: orkBin }
1078
+ ...orkBin === void 0 ? {} : { ORK_BIN: orkBin },
1079
+ ...context.item === void 0 ? {} : {
1080
+ ORK_ITEM: typeof context.item.value === "string" ? context.item.value : JSON.stringify(context.item.value),
1081
+ ORK_INDEX: String(context.item.index)
1082
+ }
1030
1083
  });
1031
1084
  /** The variables a workflow's nodes read that no `plainEnv` excuses. */
1032
1085
  const secretNamesOf = (workflow) => Object.values(workflow.nodes).flatMap((node) => programsRunBy(node, workflow.defaults)).flatMap(({ program }) => {
@@ -1068,7 +1121,7 @@ const secretsOf = (workflow, environ, also = []) => valuesOf(namesToMask(workflo
1068
1121
  const renderEnv = (node, context, environ) => {
1069
1122
  const values = {};
1070
1123
  const scope = {
1071
- ...runScope(context.inputs, context.outputs, context.runId),
1124
+ ...runScope(context.inputs, context.outputs, context.runId, context.item),
1072
1125
  output: { env: environ }
1073
1126
  };
1074
1127
  for (const [key, text] of Object.entries(node.env ?? {})) {
@@ -1158,7 +1211,7 @@ const render = (text, context) => {
1158
1211
  };
1159
1212
  return {
1160
1213
  ok: true,
1161
- value: renderTemplate(template.parts, runScope(context.inputs, context.outputs, context.runId))
1214
+ value: renderTemplate(template.parts, runScope(context.inputs, context.outputs, context.runId, context.item))
1162
1215
  };
1163
1216
  };
1164
1217
  /** The program and arguments a tool node runs, before the allow check. */
@@ -1515,11 +1568,19 @@ const checkedResult = (result, { check, resumes }) => {
1515
1568
  * (ADR-019).
1516
1569
  */
1517
1570
  const MAX_INPUT = 8192;
1518
- /** On a loop re-entry, the brief carries what the workflow sent back. */
1571
+ const inputJson = (input) => JSON.stringify(input ?? {}, void 0, 2);
1572
+ /** What a sent-back input is whole, for `read_input`, when its note cuts it. */
1573
+ const wholeInput = (input) => {
1574
+ const json = inputJson(input);
1575
+ return json.length > MAX_INPUT ? json : void 0;
1576
+ };
1577
+ /** A re-entry's note of what was sent back; `read_input` has a cut rest. */
1519
1578
  const reentryNote = (round, input) => {
1520
- const json = JSON.stringify(input ?? {}, void 0, 2);
1521
- const body = json.length > MAX_INPUT ? `${json.slice(0, MAX_INPUT)}\n…` : json;
1522
- return `\n\n## Round ${String(round)}\n\nThe workflow sent this task back to you with:\n\n\`\`\`json\n${body}\n\`\`\``;
1579
+ const json = inputJson(input);
1580
+ const cut = json.length > MAX_INPUT;
1581
+ const body = cut ? `${json.slice(0, MAX_INPUT)}\n…` : json;
1582
+ 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.` : "";
1583
+ return `\n\n## Round ${String(round)}\n\nThe workflow sent this task back to you with:\n\n\`\`\`json\n${body}\n\`\`\`${rest}`;
1523
1584
  };
1524
1585
  /**
1525
1586
  * What a resumed leader is told: it already has its brief and its earlier work,
@@ -1545,22 +1606,7 @@ const specFor = (spec, context) => {
1545
1606
  };
1546
1607
  /** Runs a `claude.leader` or `claude.worker` as one session. */
1547
1608
  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;
1609
+ const planOptions = planOptionsOf(context, options.inPlace);
1564
1610
  const before = beforeOf(context.node, context.workflow.defaults);
1565
1611
  if (before !== void 0) {
1566
1612
  const ran = await runStep(options.steps ?? noSteps, context, before, {
@@ -1577,7 +1623,7 @@ const claudeExecutor = (options) => async (context) => {
1577
1623
  }
1578
1624
  const blocked = context.quota?.().blocked;
1579
1625
  if (blocked !== void 0) return notStarted(blocked, blocked);
1580
- const budget = budgetNow(context);
1626
+ const budget = await budgetToStart(context);
1581
1627
  if (budget !== void 0 && budget <= 0) return notStarted("nothing is left of the run's budget");
1582
1628
  if (budget !== void 0) planOptions.budgetLeftUsd = budget;
1583
1629
  const plan = planNodeSession(context.workflow, context.id, planOptions);
@@ -1589,15 +1635,21 @@ const claudeExecutor = (options) => async (context) => {
1589
1635
  },
1590
1636
  error: plan.error
1591
1637
  };
1592
- const spec = specFor(plan.spec, context);
1638
+ const servers = namedMcpEntries(context.projectRoot, plan.mcp);
1639
+ if (!servers.ok) return notStarted(servers.error);
1640
+ const spec = {
1641
+ ...specFor(plan.spec, context),
1642
+ ...servers.value
1643
+ };
1644
+ if (options.machineClaude === true) spec.machineClaude = true;
1593
1645
  const driver = sessionDriver({
1594
1646
  override: options.driver,
1595
1647
  named: plan.driver,
1596
1648
  ready: options.readyDriver
1597
1649
  });
1598
- if (driver === void 0) return notStarted("no Claude driver is ready; see `ork doctor`");
1650
+ if (driver === void 0) return notStarted("no agent driver is ready; see `ork doctor`");
1599
1651
  const runtime = await options.runtimeFor(driver);
1600
- const ports = { human: context.owner === void 0 ? context.human : ownerFirst(context, context.owner, {
1652
+ const ports = { human: context.owner === void 0 ? withdrawnOnStop(context) : ownerFirst(context, context.owner, {
1601
1653
  runtimeFor: options.runtimeFor,
1602
1654
  driver: options.driver,
1603
1655
  readyDriver: options.readyDriver,
@@ -1608,9 +1660,12 @@ const claudeExecutor = (options) => async (context) => {
1608
1660
  if (context.mailbox !== void 0) ports.mailbox = context.mailbox;
1609
1661
  if (context.claims !== void 0) ports.claims = context.claims;
1610
1662
  if (context.feedback !== void 0) ports.feedback = context.feedback;
1663
+ const whole = context.round > 1 ? wholeInput(context.input) : void 0;
1664
+ if (whole !== void 0) ports.input = whole;
1611
1665
  const watch = limitWatch(context);
1612
1666
  const run = async (asked) => {
1613
1667
  const watching = watch.session(asked);
1668
+ context.reserveBudget?.(asked.budgetUsd);
1614
1669
  const session = runtime.start(asked, ports);
1615
1670
  const stop = () => {
1616
1671
  session.interrupt();
@@ -1628,6 +1683,7 @@ const claudeExecutor = (options) => async (context) => {
1628
1683
  context.emit(event);
1629
1684
  }
1630
1685
  } finally {
1686
+ context.reserveBudget?.(0);
1631
1687
  disconnect?.();
1632
1688
  context.signal.removeEventListener("abort", stop);
1633
1689
  }
@@ -1690,10 +1746,11 @@ const claudeExecutor = (options) => async (context) => {
1690
1746
  };
1691
1747
  // ../nodes/src/gate.ts
1692
1748
  /**
1693
- * @file The `human.gate` node type: asks the human the rendered question,
1694
- * outputs their choice with the note they gave, and answers with the
1695
- * `default` when the `timeout` passes. The timeout counts only while the run
1696
- * and the gate are not paused (ADR-041).
1749
+ * @file The `human.gate` node type: asks the human the rendered question, with
1750
+ * its document if it has one, both in readable text (no raw JSON), outputs
1751
+ * their choice with the note they gave, and answers with the `default` when
1752
+ * the `timeout` passes. The timeout counts only while the run and the gate
1753
+ * are not paused (ADR-041).
1697
1754
  */
1698
1755
  /** A promise that never settles: the losing side of a race. */
1699
1756
  const NEVER = new Promise(() => {});
@@ -1715,8 +1772,13 @@ const gateExecutor = async (context) => {
1715
1772
  output: {},
1716
1773
  error: `${context.id} is not a gate`
1717
1774
  };
1718
- const template = parseTemplate(node.question);
1719
- const question = template.ok ? renderTemplate(template.parts, runScope(context.inputs, context.outputs, context.runId)) : node.question;
1775
+ const scope = runScope(context.inputs, context.outputs, context.runId);
1776
+ const render = (text) => {
1777
+ const template = parseTemplate(text);
1778
+ return template.ok ? renderTemplate(template.parts, scope, { readable: true }) : text;
1779
+ };
1780
+ const question = render(node.question);
1781
+ const document = render(node.document ?? "");
1720
1782
  const options = node.options.map((o) => choiceOf(o));
1721
1783
  const withdraw = new AbortController();
1722
1784
  context.signal.addEventListener("abort", () => {
@@ -1733,6 +1795,7 @@ const gateExecutor = async (context) => {
1733
1795
  }
1734
1796
  };
1735
1797
  if (node.recommended !== void 0) asked.recommended = node.recommended;
1798
+ if (document.trim() !== "") asked.document = document;
1736
1799
  const answer = context.human.ask(asked).then((choice) => ({
1737
1800
  choice,
1738
1801
  answeredBy: "user",
@@ -1847,7 +1910,7 @@ const mcpCallExecutor = (options = {}) => async (context) => {
1847
1910
  }, node.plainEnv);
1848
1911
  if (!expanded.ok) return failed(expanded.error);
1849
1912
  const scrub = scrubberOf([...secretsOf(context.workflow, environ, options.maskedNames), ...expanded.value.values]);
1850
- const scope = runScope(context.inputs, context.outputs, context.runId);
1913
+ const scope = runScope(context.inputs, context.outputs, context.runId, context.item);
1851
1914
  const args = Object.fromEntries(Object.entries(node.arguments ?? {}).map(([key, value]) => [key, renderValue(value, scope)]));
1852
1915
  const name = `${node.server}.${node.tool}`;
1853
1916
  context.emit({
@@ -2129,4 +2192,4 @@ const missingPrograms = (workflow, installed = (program) => onPath(program)) =>
2129
2192
  }
2130
2193
  return [...missing.values()];
2131
2194
  };
2132
- export { resultOf 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 };
2195
+ 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 };