@krapht/ork 1.0.0-alpha.10 → 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.
Files changed (35) hide show
  1. package/dist/{__build_info__-DFF18K8y.mjs → __build_info__-CIchF6nr.mjs} +3 -72
  2. package/dist/bin.mjs +1066 -387
  3. package/dist/chips-BA7SrrH5.mjs +43 -0
  4. package/dist/{clean-DVaEH9e7.mjs → clean-Di-KT7Mf.mjs} +1 -1
  5. package/dist/{completion-Dv5kf598.mjs → completion-C5D0sEs5.mjs} +22 -15
  6. package/dist/{context-CEaKOVmG.mjs → context-BaNshWr9.mjs} +43 -31
  7. package/dist/{decide-3l9q8UtX.mjs → decide-B6PnKeqo.mjs} +1 -1
  8. package/dist/{find-DEjjOlTG.mjs → find-BicBbGMg.mjs} +1 -1
  9. package/dist/{format-Cb5_m-Qd.mjs → format-lWhN-jYR.mjs} +130 -35
  10. package/dist/{in-a-run-BS9Nc-JW.mjs → in-a-run-CgkVouyG.mjs} +1 -1
  11. package/dist/{limits-BSwEjQST.mjs → limits-jZQp9ZJC.mjs} +1 -1
  12. package/dist/{list-Qxr6rwsY.mjs → list-CEaJ07jd.mjs} +1 -1
  13. package/dist/{mcp-CslnG5wf.mjs → mcp-DYgVA9f5.mjs} +6 -6
  14. package/dist/output-CM95njYt.mjs +421 -0
  15. package/dist/{pause-D7OVB5Ac.mjs → pause-D6PuSlNl.mjs} +4 -4
  16. package/dist/{relay-EceRIebO.mjs → relay-DNx7be9k.mjs} +2 -2
  17. package/dist/{remove-0IQZVWmI.mjs → remove-CpMb4NDl.mjs} +3 -3
  18. package/dist/{report-DK4LGqwF.mjs → report-CEN8VoUb.mjs} +31 -20
  19. package/dist/{send-C0uwfUYi.mjs → send-CLs_fyg7.mjs} +3 -3
  20. package/dist/{src-cmmXKHn1.mjs → src-9iGkjnsy.mjs} +234 -150
  21. package/dist/{src-i5k2Y-kf.mjs → src-B6KvTm7B.mjs} +78 -11
  22. package/dist/{src-oaJYIddp.mjs → src-BnZgZGrf.mjs} +523 -81
  23. package/dist/{src-Df6KOIDC.mjs → src-D6_xjm-x.mjs} +1722 -825
  24. package/dist/ui/assets/{app-qs8Lofv6.js → app-CxURph7a.js} +16 -16
  25. package/dist/ui/assets/app-QoHVi-mb.css +1 -0
  26. package/dist/ui/assets/{index-DV1ZaOk-.js → index-By_8KKG6.js} +2 -2
  27. package/dist/ui/index.html +1 -1
  28. package/dist/{ui-CBkHJdqk.mjs → ui-CIci4UBW.mjs} +4 -4
  29. package/dist/{usage-Cd8pEYyK.mjs → usage-GceBGMjU.mjs} +43 -32
  30. package/dist/{watch-DW5R2OPO.mjs → watch-BmoybMQg.mjs} +28 -16
  31. package/dist/{workflows-BT1gDRIV.mjs → workflows-DZvW3i1e.mjs} +3 -3
  32. package/dist/{worktrees-g1MQMGie.mjs → worktrees-Be51E2rB.mjs} +11 -10
  33. package/package.json +1 -1
  34. package/dist/output-Bqljtmp_.mjs +0 -40
  35. package/dist/ui/assets/app-C8uhHVYp.css +0 -1
@@ -1,10 +1,11 @@
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";
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
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,23 +19,16 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
18
19
  */
19
20
  const limitedCall = (result) => {
20
21
  if (result.ok) return result;
21
- return (result.error ?? "").includes("Not started: ") || result.output["runStatus"] === "quota_exhausted" ? {
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: result.error ?? "the called run reached a limit"
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
- ok: false,
30
- output: {},
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
- });
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" }));
38
32
  const scope = runScope(context.inputs, context.outputs, context.runId, context.item);
39
33
  const render = (text) => {
40
34
  const template = parseTemplate(text);
@@ -90,30 +84,27 @@ const planOptionsOf = (context, inPlace) => {
90
84
  // ../nodes/src/claude-result.ts
91
85
  /**
92
86
  * @file A finished session as a node's result: its report is the output, held
93
- * 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).
94
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
+ };
95
95
  /** A finished session as a node result: its report is the node's output. */
96
96
  const resultOf = (outcome, declared) => {
97
- if (outcome === null) return {
98
- ok: false,
99
- output: {
100
- status: "fail",
101
- summary: "the session ended without a result"
102
- },
103
- error: "the session ended without a result"
104
- };
97
+ if (outcome === null) return failedWith(endFor("driver_crashed", {}, "the session ended without a result"));
105
98
  const { report, usage } = outcome;
106
99
  if (report === null) {
107
- const reason = outcome.errors[0] ?? "no valid report";
100
+ const end = withoutReport(outcome);
108
101
  return {
109
- ok: false,
110
- output: {
102
+ ...failedWith(end, {
111
103
  status: "fail",
112
- summary: reason,
104
+ summary: end.message,
113
105
  usage
114
- },
106
+ }),
115
107
  usage,
116
- error: `ended without a report (${outcome.subtype}): ${reason}`,
117
108
  sessionId: outcome.sessionId
118
109
  };
119
110
  }
@@ -127,7 +118,7 @@ const resultOf = (outcome, declared) => {
127
118
  };
128
119
  if (report.branch !== void 0) output["branch"] = report.branch;
129
120
  const problem = declaredProblem(declared, output);
130
- if (problem !== null) return {
121
+ if (problem !== null) return endedWith({
131
122
  ok: false,
132
123
  output: {
133
124
  status: "fail",
@@ -137,17 +128,15 @@ const resultOf = (outcome, declared) => {
137
128
  ...report.branch === void 0 ? {} : { branch: report.branch }
138
129
  },
139
130
  usage,
140
- error: problem,
141
131
  ...ran
142
- };
143
- const result = {
144
- ok: report.status !== "fail",
132
+ }, endFor("invalid_report", { reason: problem }));
133
+ const ok = report.status !== "fail";
134
+ return endedWith({
135
+ ok,
145
136
  output,
146
137
  usage,
147
138
  ...ran
148
- };
149
- if (!result.ok) result.error = report.summary;
150
- return result;
139
+ }, endFor(ok ? "done" : "reported_failure"));
151
140
  };
152
141
  // ../nodes/src/environ.ts
153
142
  /** A variable of the user's. */
@@ -537,13 +526,16 @@ const withUsageOf = (first, later) => {
537
526
  */
538
527
  const afterLateTurn = (first, later) => {
539
528
  const merged = withUsageOf(first, later);
540
- return later.report === null ? {
541
- ...merged,
529
+ if (later.report !== null) return merged;
530
+ const { end: _later, ...rest } = merged;
531
+ return {
532
+ ...rest,
542
533
  report: first.report,
543
534
  subtype: first.subtype,
535
+ ...first.end === void 0 ? {} : { end: first.end },
544
536
  text: first.text,
545
537
  errors: first.errors
546
- } : merged;
538
+ };
547
539
  };
548
540
  /**
549
541
  * What another turn of a session may use. `spent` and `turns` are this
@@ -602,24 +594,38 @@ const limitWatch = (context) => {
602
594
  },
603
595
  ended: (outcome) => {
604
596
  if (refused !== void 0 && (outcome?.report ?? null) === null) why = refused;
605
- if (left !== void 0 && asked.budgetUsd !== void 0 && asked.budgetUsd >= left && outcome?.subtype === "error_max_budget_usd") why = context.quota?.().blocked ?? "a session reached what was left of a quota";
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";
606
600
  }
607
601
  };
608
602
  },
609
- result: (result) => result.ok || why === void 0 ? result : {
610
- ...result,
611
- limited: why
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;
612
614
  }
613
615
  };
614
616
  };
615
- /** No session started: nothing was left for one. */
616
- const notStarted = (reason, limited) => ({
617
- ok: false,
618
- error: `not started: ${reason}`,
619
- output: {
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), {
620
626
  status: "fail",
621
627
  summary: `not started: ${reason}`
622
- },
628
+ }),
623
629
  ...limited === void 0 ? {} : { limited }
624
630
  });
625
631
  // ../nodes/src/tool-output.ts
@@ -864,6 +870,51 @@ const tokenize = (command) => {
864
870
  args
865
871
  };
866
872
  };
873
+ // ../nodes/src/tool-ends.ts
874
+ /**
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.
879
+ */
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
887
+ }
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 }));
894
+ /**
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.
897
+ */
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({
906
+ ok: true,
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);
917
+ };
867
918
  // ../nodes/src/tool-scrub.ts
868
919
  /**
869
920
  * @file Masking a secret in what a program prints. A plain search, so a value
@@ -1265,19 +1316,10 @@ const capped = (text, file) => {
1265
1316
  * what a session is told. All of it is kept in the output's fields and files.
1266
1317
  */
1267
1318
  const clipError = (text) => text.length <= ERROR_LIMIT ? text : `${text.slice(0, ERROR_LIMIT)}…`;
1268
- const failed$1 = (error) => ({
1269
- ok: false,
1270
- output: {
1271
- exitCode: -1,
1272
- stdout: "",
1273
- stderr: error
1274
- },
1275
- error
1276
- });
1277
1319
  /** Runs a user's CLI through its integration's adapter, with its own login. */
1278
1320
  const toolExecutor = (given, options = {}) => async (context) => {
1279
1321
  const { node } = context;
1280
- if (!isToolNode(node)) return failed$1(`${context.id} is not a tool node`);
1322
+ if (!isToolNode(node)) return failed$1(`${context.id} is not a tool`, "unknown");
1281
1323
  const environ = options.environ ?? process.env;
1282
1324
  const scrub = scrubberOf(secretsOf(context.workflow, environ, options.maskedNames));
1283
1325
  const command = commandOf(node, context);
@@ -1304,6 +1346,7 @@ const toolExecutor = (given, options = {}) => async (context) => {
1304
1346
  ...node.watch === void 0 ? {} : { [STATE_ENV]: prepareStateFile(context.runDir, context.id) }
1305
1347
  };
1306
1348
  const names = Object.keys(nodeEnv.values);
1349
+ const release = await context.machine?.slot([command.program, ...command.args].join(" "), context.signal);
1307
1350
  context.emit({
1308
1351
  type: "tool.called",
1309
1352
  id: context.id,
@@ -1318,11 +1361,13 @@ const toolExecutor = (given, options = {}) => async (context) => {
1318
1361
  }
1319
1362
  });
1320
1363
  const scriptFile = node.script === void 0 ? void 0 : writeScriptFile(context.runDir, context.id, context.round, node.script);
1364
+ const priority = context.machine?.priority;
1321
1365
  const raw = await runner(command.program, scriptFile === void 0 ? command.args : [...command.args, scriptFile], {
1322
1366
  cwd,
1323
1367
  signal: context.signal,
1324
- env
1325
- });
1368
+ env,
1369
+ ...priority === void 0 ? {} : { priority }
1370
+ }).finally(() => release?.());
1326
1371
  if (scriptFile !== void 0) removeOutputFile(scriptFile);
1327
1372
  const run = {
1328
1373
  exitCode: raw.exitCode,
@@ -1359,30 +1404,14 @@ const toolExecutor = (given, options = {}) => async (context) => {
1359
1404
  context.emit({
1360
1405
  type: "tool.result",
1361
1406
  id: context.id,
1362
- 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 }
1363
1409
  });
1364
- if (json?.ok === false) return {
1365
- ok: false,
1366
- output,
1367
- error: `${context.id}: ${json.error}`
1368
- };
1369
- if (declared !== null) return {
1370
- ok: false,
1371
- output,
1372
- error: declared
1373
- };
1410
+ if (json?.ok === false) return invalidOutput(output, json.error);
1411
+ if (declared !== null) return invalidOutput(output, declared);
1374
1412
  if (json !== null) output["json"] = json.json;
1375
- if (run.exitCode === 0) return {
1376
- ok: true,
1377
- output
1378
- };
1379
- const last = run.stderr.trim().split("\n").at(-1) ?? "";
1380
- return {
1381
- ok: false,
1382
- transient: true,
1383
- output,
1384
- error: `${scrub(command.program)} exited with ${String(run.exitCode)}${last === "" ? "" : `: ${clipError(last)}`}`
1385
- };
1413
+ const signedOut = integration?.signedOut;
1414
+ return exitEnd(output, run, context, scrub(command.program), signedOut);
1386
1415
  };
1387
1416
  // ../nodes/src/verify.ts
1388
1417
  /**
@@ -1394,15 +1423,17 @@ const toolExecutor = (given, options = {}) => async (context) => {
1394
1423
  /** A setup that failed: no session started, so nothing was spent on it. */
1395
1424
  const beforeFailed = (ran) => {
1396
1425
  const error = `its before step failed: ${ran.error ?? "no reason given"}`;
1397
- return {
1426
+ return endedWith({
1398
1427
  ok: false,
1399
- error,
1400
1428
  output: {
1401
1429
  status: "fail",
1402
1430
  summary: error,
1403
1431
  before: ran.output
1404
1432
  }
1405
- };
1433
+ }, endOf({
1434
+ ...ran,
1435
+ ok: false
1436
+ }));
1406
1437
  };
1407
1438
  /** With nothing to run a step, a report cannot be accepted. */
1408
1439
  const noSteps = () => Promise.resolve({
@@ -1423,20 +1454,23 @@ const runStep = async (tool, given, { timeout, ...fields }, step) => {
1423
1454
  type: "tool.shell",
1424
1455
  ...fields
1425
1456
  };
1426
- const context = {
1427
- ...given,
1428
- step
1429
- };
1430
1457
  const ms = timeoutMs(timeout);
1431
1458
  if (ms === void 0) return tool({
1432
- ...context,
1459
+ ...given,
1460
+ step,
1433
1461
  node
1434
1462
  });
1463
+ const { clock, machine } = slotClock(given.hold, given.machine);
1464
+ const context = {
1465
+ ...given,
1466
+ step,
1467
+ ...machine === void 0 ? {} : { machine }
1468
+ };
1435
1469
  const limit = new AbortController();
1436
1470
  const finished = new AbortController();
1437
1471
  let expired = false;
1438
1472
  const watch = async () => {
1439
- if (await heldTimeout(context.hold, ms, AbortSignal.any([context.signal, finished.signal]))) {
1473
+ if (await heldTimeout(clock, ms, AbortSignal.any([context.signal, finished.signal]))) {
1440
1474
  expired = true;
1441
1475
  limit.abort();
1442
1476
  }
@@ -1547,17 +1581,19 @@ const checkedResult = (result, { check, resumes }) => {
1547
1581
  };
1548
1582
  const reason = `its check failed: ${check.error ?? "no reason given"}`;
1549
1583
  const error = result.ok ? reason : `${reason} (then: ${result.error ?? "no report"})`;
1550
- return {
1584
+ return endedWith({
1551
1585
  ...result,
1552
1586
  ok: false,
1553
- error,
1554
1587
  output: {
1555
1588
  ...result.output,
1556
1589
  status: "fail",
1557
1590
  summary: error,
1558
1591
  verify
1559
1592
  }
1560
- };
1593
+ }, endOf(result.ok ? {
1594
+ ...check,
1595
+ ok: false
1596
+ } : result));
1561
1597
  };
1562
1598
  // ../nodes/src/claude.ts
1563
1599
  /**
@@ -1615,28 +1651,26 @@ const claudeExecutor = (options) => async (context) => {
1615
1651
  });
1616
1652
  if (!ran.ok) return beforeFailed(ran);
1617
1653
  await context.hold?.wait("start", context.signal);
1618
- if (context.signal.aborted) return beforeFailed({
1619
- ...ran,
1620
- error: "the node was stopped"
1621
- });
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
+ }
1622
1663
  planOptions.before = ran.output;
1623
1664
  }
1624
1665
  const blocked = context.quota?.().blocked;
1625
- if (blocked !== void 0) return notStarted(blocked, blocked);
1666
+ if (blocked !== void 0) return notStarted("plan_limit", blocked, blocked);
1626
1667
  const budget = await budgetToStart(context);
1627
- if (budget !== void 0 && budget <= 0) return notStarted("nothing is left of the run's budget");
1668
+ if (budget !== void 0 && budget <= 0) return notStarted("budget_limit", "nothing is left of the run's budget");
1628
1669
  if (budget !== void 0) planOptions.budgetLeftUsd = budget;
1629
1670
  const plan = planNodeSession(context.workflow, context.id, planOptions);
1630
- if (!plan.ok) return {
1631
- ok: false,
1632
- output: {
1633
- status: "fail",
1634
- summary: plan.error
1635
- },
1636
- error: plan.error
1637
- };
1671
+ if (!plan.ok) return notStarted("invalid_node", plan.error);
1638
1672
  const servers = namedMcpEntries(context.projectRoot, plan.mcp);
1639
- if (!servers.ok) return notStarted(servers.error);
1673
+ if (!servers.ok) return notStarted("invalid_node", servers.error);
1640
1674
  const spec = {
1641
1675
  ...specFor(plan.spec, context),
1642
1676
  ...servers.value
@@ -1647,7 +1681,7 @@ const claudeExecutor = (options) => async (context) => {
1647
1681
  named: plan.driver,
1648
1682
  ready: options.readyDriver
1649
1683
  });
1650
- if (driver === void 0) return notStarted("no agent driver is ready; see `ork doctor`");
1684
+ if (driver === void 0) return notStarted("tool_missing", "no agent driver is ready; see `ork doctor`");
1651
1685
  const runtime = await options.runtimeFor(driver);
1652
1686
  const ports = { human: context.owner === void 0 ? withdrawnOnStop(context) : ownerFirst(context, context.owner, {
1653
1687
  runtimeFor: options.runtimeFor,
@@ -1656,6 +1690,7 @@ const claudeExecutor = (options) => async (context) => {
1656
1690
  inPlace: options.inPlace
1657
1691
  }) };
1658
1692
  if (context.hold !== void 0) ports.hold = context.hold;
1693
+ if (context.machine !== void 0) ports.machine = context.machine;
1659
1694
  if (context.ownership !== void 0) ports.ownership = context.ownership;
1660
1695
  if (context.mailbox !== void 0) ports.mailbox = context.mailbox;
1661
1696
  if (context.claims !== void 0) ports.claims = context.claims;
@@ -1814,21 +1849,22 @@ const gateExecutor = async (context) => {
1814
1849
  })];
1815
1850
  try {
1816
1851
  const output = await Promise.race([answer, ...expired]);
1817
- if (!options.some((o) => o.id === output.choice)) return {
1818
- ok: false,
1819
- output,
1820
- error: "no one answered the gate"
1821
- };
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
+ }
1822
1859
  return {
1823
1860
  ok: true,
1824
1861
  output
1825
1862
  };
1826
1863
  } catch {
1827
- return {
1864
+ return endedWith({
1828
1865
  ok: false,
1829
- output: {},
1830
- error: "the gate was withdrawn"
1831
- };
1866
+ output: {}
1867
+ }, endFor("interrupted", {}, "the gate was withdrawn"));
1832
1868
  } finally {
1833
1869
  answered.abort();
1834
1870
  }
@@ -1843,18 +1879,16 @@ const gateExecutor = async (context) => {
1843
1879
  */
1844
1880
  /** A node's `timeout:` is what limits a call; this only lifts the SDK's 60 s. */
1845
1881
  const NO_LIMIT_MS = 864e5;
1846
- const failed = (error, output = {}) => ({
1847
- ok: false,
1848
- output,
1849
- error
1850
- });
1851
- /** A failure another try might not have, so a `watch:` looks again. */
1852
- const missed = (error, output = {}) => ({
1882
+ const failed = (kind, facts, detail, output = {}) => endedWith({
1853
1883
  ok: false,
1854
- transient: true,
1855
- output,
1856
- error
1857
- });
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
+ };
1858
1892
  /**
1859
1893
  * A string's templates rendered. A string that is one `{{ … }}` and nothing
1860
1894
  * else keeps its value's type, so a tool that asks for a number gets one.
@@ -1898,17 +1932,20 @@ const reasonOf = (error, server) => {
1898
1932
  };
1899
1933
  const mcpCallExecutor = (options = {}) => async (context) => {
1900
1934
  const { node } = context;
1901
- if (node.type !== "mcp.call") return failed(`${context.id} is not an MCP call`);
1935
+ if (node.type !== "mcp.call") {
1936
+ const said = `${context.id} is not an MCP call`;
1937
+ return failed("unknown", {}, said);
1938
+ }
1902
1939
  const environ = options.environ ?? process.env;
1903
1940
  const declared = readMcpServer(context.projectRoot, node.server);
1904
- if (!declared.ok) return failed(declared.error);
1941
+ if (!declared.ok) return failed("invalid_node", { reason: declared.error });
1905
1942
  const nodeEnv = renderEnv(node, context, environ);
1906
- if (!nodeEnv.ok) return failed(nodeEnv.error);
1943
+ if (!nodeEnv.ok) return failed("invalid_node", { reason: nodeEnv.error });
1907
1944
  const expanded = expandServer(declared.value, {
1908
1945
  ...environ,
1909
1946
  ...nodeEnv.values
1910
1947
  }, node.plainEnv);
1911
- if (!expanded.ok) return failed(expanded.error);
1948
+ if (!expanded.ok) return failed("invalid_node", { reason: expanded.error });
1912
1949
  const scrub = scrubberOf([...secretsOf(context.workflow, environ, options.maskedNames), ...expanded.value.values]);
1913
1950
  const scope = runScope(context.inputs, context.outputs, context.runId, context.item);
1914
1951
  const args = Object.fromEntries(Object.entries(node.arguments ?? {}).map(([key, value]) => [key, renderValue(value, scope)]));
@@ -1951,19 +1988,22 @@ const mcpCallExecutor = (options = {}) => async (context) => {
1951
1988
  id: context.id,
1952
1989
  isError: reported || tooLong || problem !== null
1953
1990
  });
1954
- if (reported) return missed(`${name} failed: ${clipError(said)}`, output);
1955
- if (tooLong) return failed(`${name}'s result is over 64 KiB`, output);
1956
- return problem === null ? {
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({
1957
1997
  ok: true,
1958
1998
  output
1959
- } : failed(problem, output);
1999
+ }, endFor("done")) : failed("invalid_output", { reason: problem }, void 0, output);
1960
2000
  } catch (error) {
1961
2001
  context.emit({
1962
2002
  type: "tool.result",
1963
2003
  id: context.id,
1964
2004
  isError: true
1965
2005
  });
1966
- return missed(clipError(scrub(reasonOf(error, node.server))));
2006
+ return failed(thrownKind(error), { program: `MCP server \`${node.server}\`` }, clipError(scrub(reasonOf(error, node.server))));
1967
2007
  } finally {
1968
2008
  await client.close().catch(() => {});
1969
2009
  }
@@ -1974,13 +2014,39 @@ const mcpCallExecutor = (options = {}) => async (context) => {
1974
2014
  * text. A program runs in its own process group, so aborting it reaches
1975
2015
  * whatever it started: SIGTERM, then SIGKILL for what ignores that. A stop or
1976
2016
  * a timeout therefore leaves nothing running, and an escaped process that
1977
- * 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).
1978
2021
  */
1979
2022
  /** How long a program may take to end after SIGTERM. */
1980
2023
  const KILL_AFTER_MS = 2e3;
1981
2024
  /** How long to wait for the pipes to close after SIGKILL. */
1982
2025
  const CLOSE_AFTER_MS = 500;
1983
- const spawnTool = (program, args, { cwd, signal, env }) => {
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 }) => {
1984
2050
  if (signal.aborted) return Promise.resolve({
1985
2051
  exitCode: -1,
1986
2052
  stdout: "",
@@ -1990,7 +2056,9 @@ const spawnTool = (program, args, { cwd, signal, env }) => {
1990
2056
  const stdout = [];
1991
2057
  const stderr = [];
1992
2058
  const timers = [];
1993
- const child = spawn(program, args, {
2059
+ const times = [];
2060
+ const [bin, argv] = startOf(program, args, priority);
2061
+ const child = spawn(bin, argv, {
1994
2062
  cwd,
1995
2063
  detached: true,
1996
2064
  env: {
@@ -2000,9 +2068,15 @@ const spawnTool = (program, args, { cwd, signal, env }) => {
2000
2068
  stdio: [
2001
2069
  "ignore",
2002
2070
  "pipe",
2071
+ "pipe",
2003
2072
  "pipe"
2004
2073
  ]
2005
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
+ });
2006
2080
  const killGroup = (kill) => {
2007
2081
  if (child.pid === void 0) return;
2008
2082
  try {
@@ -2014,11 +2088,21 @@ const spawnTool = (program, args, { cwd, signal, env }) => {
2014
2088
  signal.removeEventListener("abort", abort);
2015
2089
  resolve(run);
2016
2090
  };
2017
- const collected = (exitCode) => ({
2018
- exitCode,
2019
- stdout: stdout.join(""),
2020
- stderr: stderr.join("")
2021
- });
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
+ };
2022
2106
  const abort = () => {
2023
2107
  killGroup("SIGTERM");
2024
2108
  timers.push(setTimeout(() => {
@@ -2029,10 +2113,10 @@ const spawnTool = (program, args, { cwd, signal, env }) => {
2029
2113
  }, KILL_AFTER_MS));
2030
2114
  };
2031
2115
  signal.addEventListener("abort", abort, { once: true });
2032
- child.stdout.setEncoding("utf8").on("data", (chunk) => {
2116
+ child.stdout?.setEncoding("utf8").on("data", (chunk) => {
2033
2117
  stdout.push(chunk);
2034
2118
  });
2035
- child.stderr.setEncoding("utf8").on("data", (chunk) => {
2119
+ child.stderr?.setEncoding("utf8").on("data", (chunk) => {
2036
2120
  stderr.push(chunk);
2037
2121
  });
2038
2122
  child.on("error", (error) => {