@krapht/ork 1.0.0-alpha.12 → 1.0.0-alpha.14

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 (44) hide show
  1. package/dist/{__build_info__-BJbE3Zvp.mjs → __build_info__-B82-dGJY.mjs} +3 -3
  2. package/dist/{attach-BmV5ckb9.mjs → attach-CdtGX9se.mjs} +5 -5
  3. package/dist/bin.mjs +274 -236
  4. package/dist/{builtin-files-w0YULJ4c.mjs → builtin-files-Bdq23IIi.mjs} +2 -2
  5. package/dist/{chips-CyeB_rhR.mjs → chips-BqcT58qr.mjs} +1 -1
  6. package/dist/{completion-CzElB8nS.mjs → completion--ywde-56.mjs} +52 -11
  7. package/dist/{context-BAwgVSX5.mjs → context-Y_I4Mofq.mjs} +18 -8
  8. package/dist/{find-p05wlbmu.mjs → find-BVX-bL4u.mjs} +1 -1
  9. package/dist/{format-Dni98cbh.mjs → format-CvTE5CZQ.mjs} +12 -12
  10. package/dist/{help-groups-DYP_zMRP.mjs → help-groups-CpR2UzSn.mjs} +20 -6
  11. package/dist/{in-a-run-BI-hLIl_.mjs → in-a-run-cL5N9gpH.mjs} +1 -1
  12. package/dist/{limits-CElmhowV.mjs → limits-Dhe--wmX.mjs} +2 -2
  13. package/dist/{mcp-CM3b65AZ.mjs → mcp-CsmboyXv.mjs} +6 -6
  14. package/dist/once-CrwI-ZdN.mjs +76 -0
  15. package/dist/{output-CnRzrbUM.mjs → output-B1kwo0YU.mjs} +17 -1
  16. package/dist/{pause-BuS83WAI.mjs → pause-DssrYcda.mjs} +4 -4
  17. package/dist/{relay-DAsJA8wF.mjs → relay-DASRgsTQ.mjs} +2 -2
  18. package/dist/run-D8divGCs.mjs +192 -0
  19. package/dist/send-CXv8bx0i.mjs +105 -0
  20. package/dist/{src-S9yK2OHU.mjs → src-8Mul0695.mjs} +8 -10
  21. package/dist/{src-BfxkJOYm.mjs → src-C7z_6fhn.mjs} +200 -72
  22. package/dist/{src-D1QedD5y.mjs → src-njdtejgZ.mjs} +140 -38
  23. package/dist/{src-CdDTQLdQ.mjs → src-tsuZRu0H.mjs} +2787 -1134
  24. package/dist/{store-B2OaE4ID.mjs → store-CleF7KeS.mjs} +33 -76
  25. package/dist/ui/THIRD_PARTY_LICENSES.txt +18 -0
  26. package/dist/ui/assets/app-D_nAPnHM.css +1 -0
  27. package/dist/ui/assets/app-DqBAtXSZ.js +111 -0
  28. package/dist/ui/assets/{index-BL7Ad0OA.js → index-D-_uPI2v.js} +2 -2
  29. package/dist/ui/index.html +1 -1
  30. package/dist/{ui-C4Ys26oO.mjs → ui-CNO1M0qT.mjs} +4 -4
  31. package/dist/{usage-C2zx-X6q.mjs → usage-BVXi0iFY.mjs} +13 -141
  32. package/dist/{verdict-CIy8tAyX.mjs → verdict-CMvPCpWC.mjs} +2 -2
  33. package/dist/{watch-DzCeqM05.mjs → watch-DVNIciHp.mjs} +10 -8
  34. package/dist/{workflows-Rr5w90A0.mjs → workflows-B30QkwTk.mjs} +3 -3
  35. package/dist/{worktrees-f6SAbum5.mjs → worktrees-DD4_TpkH.mjs} +3 -5
  36. package/package.json +2 -2
  37. package/dist/clean-D8GI_2K-.mjs +0 -91
  38. package/dist/decide-DlbyctOE.mjs +0 -50
  39. package/dist/list-zW2xKuGr.mjs +0 -117
  40. package/dist/remove-Dh7D_hVn.mjs +0 -201
  41. package/dist/report-CEN8VoUb.mjs +0 -140
  42. package/dist/send-MFEQZH31.mjs +0 -163
  43. package/dist/ui/assets/app-CAoZtku5.js +0 -75
  44. package/dist/ui/assets/app-QoHVi-mb.css +0 -1
@@ -1,4 +1,5 @@
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-CdDTQLdQ.mjs";
1
+ import { $t as countedTokens, Cn as MCP_FILE, Dn as endOf, Ft as reportSchemaFor, Gn as parseTemplate, Gr as isToolNode, Ht as heldTimeout, Kn as renderTemplate, Lr as markValue, Mr as beforeOf, Nr as programsRunBy, Nt as planNodeSession, On as outcomeEnd, Qr as integrationFor, Tn as serversIn, Wn as evaluate, Wr as isClaudeNode, Xr as INTEGRATIONS, Yr as timeoutMs, Zr as TOOL_PREFIX, an as stoppedEnd, en as runScope, gn as sessionDriver, ii as AGENT_DRIVERS, in as failedWith, jn as endFor, kn as END_DETAIL_LIMIT, kt as planReason, ln as lowerChild, mn as envReadsOf, nt as sessionRefusals, on as declaredProblem, rn as endedWith, sn as slotClock, tn as calledWorkflows, un as startAt, vn as declaredOutputOf, wn as mcpNotText } from "./src-tsuZRu0H.mjs";
2
+ import { t as runOnce } from "./run-D8divGCs.mjs";
2
3
  import path from "node:path";
3
4
  import { accessSync, constants, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
5
  import { spawn } from "node:child_process";
@@ -323,7 +324,7 @@ const forkPrompt = (question, passOn) => [
323
324
  /** The fork's report: one of the options or the pass-on, and a note. */
324
325
  const forkReportSchema = (ids) => {
325
326
  const node = {
326
- type: "claude.worker",
327
+ type: "agent.worker",
327
328
  prompt: "",
328
329
  output: { schema: {
329
330
  choice: [...ids],
@@ -537,43 +538,36 @@ const afterLateTurn = (first, later) => {
537
538
  errors: first.errors
538
539
  };
539
540
  };
541
+ /** The least of the limits that are set; `undefined` with none. */
542
+ const least = (...limits) => {
543
+ const known = limits.filter((limit) => limit !== void 0);
544
+ return known.length === 0 ? void 0 : Math.min(...known);
545
+ };
546
+ /** A cap less what this activation used, beside what a quota says is left. */
547
+ const leftOf = (caps, used, quotaLeft) => least(...caps.map((cap) => cap === void 0 ? void 0 : cap - used), quotaLeft);
540
548
  /**
541
- * What another turn of a session may use. `spent` and `turns` are this
542
- * activation's, which neither the node's cap nor the run's budget has counted.
543
- * The quota has: the ledger took it as the session finished, so what it says is
544
- * left is left. `null` when nothing is, and the turn does not start.
549
+ * What another turn of a session may use. `spent`, `tokens` (counted, RFC-015)
550
+ * and `turns` are this activation's, which neither the node's cap nor the run's
551
+ * budget has counted. The quota has: the ledger took it as the session
552
+ * finished, so what it says is left is left. The run's tokens left have too:
553
+ * only the node's own `budgetTokens` takes `tokens` off. `null` when nothing
554
+ * is, and the turn does not start.
545
555
  */
546
- const lateLimits = (spec, { spent, turns }, runLeft, quotaLeft) => {
547
- const budgets = [...[spec.budgetUsd, runLeft].flatMap((limit) => limit === void 0 ? [] : [limit - spent]), ...quotaLeft === void 0 ? [] : [quotaLeft]];
548
- const budgetUsd = budgets.length === 0 ? void 0 : Math.min(...budgets);
549
- if (budgetUsd !== void 0 && budgetUsd <= 0) return null;
550
- const maxTurns = spec.maxTurns === void 0 ? void 0 : spec.maxTurns - turns;
551
- if (maxTurns !== void 0 && maxTurns <= 0) return null;
556
+ const lateLimits = (spec, used, runLeft, quotaLeft, tokensLeft = {}) => {
557
+ const budgetUsd = leftOf([spec.budgetUsd, runLeft], used.spent, quotaLeft);
558
+ const budgetTokens = leftOf([spec.budgetTokens], used.tokens ?? 0, least(tokensLeft.run, tokensLeft.quota));
559
+ const maxTurns = spec.maxTurns === void 0 ? void 0 : spec.maxTurns - used.turns;
560
+ if ([
561
+ budgetUsd,
562
+ budgetTokens,
563
+ maxTurns
564
+ ].some((left) => (left ?? 1) <= 0)) return null;
552
565
  return {
553
566
  ...budgetUsd === void 0 ? {} : { budgetUsd },
567
+ ...budgetTokens === void 0 ? {} : { budgetTokens },
554
568
  ...maxTurns === void 0 ? {} : { maxTurns }
555
569
  };
556
570
  };
557
- /** What a session may spend as it starts: the least of the budget and any quota. */
558
- const budgetNow = (context) => {
559
- if (context.budgetLeftNow === void 0) return context.budgetLeftUsd;
560
- const known = [context.budgetLeftNow(), context.quota?.().leftUsd].flatMap((left) => left === void 0 ? [] : [left]);
561
- return known.length === 0 ? void 0 : Math.min(...known);
562
- };
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
571
  /**
578
572
  * Whether a limit across runs, rather than the node's work, ended a node: a
579
573
  * session whose cap was a quota's and that ran into it, or one its plan refused
@@ -586,7 +580,7 @@ const limitWatch = (context) => {
586
580
  why = reason;
587
581
  },
588
582
  session: (asked) => {
589
- const left = context.quota?.().leftUsd;
583
+ const { leftUsd: left, leftTokens } = context.quota?.() ?? {};
590
584
  let refused;
591
585
  return {
592
586
  saw: (event) => {
@@ -594,7 +588,7 @@ const limitWatch = (context) => {
594
588
  },
595
589
  ended: (outcome) => {
596
590
  if (refused !== void 0 && (outcome?.report ?? null) === null) why = refused;
597
- const capped = left !== void 0 && asked.budgetUsd !== void 0 && asked.budgetUsd >= left;
591
+ const capped = left !== void 0 && asked.budgetUsd !== void 0 && asked.budgetUsd >= left || leftTokens !== void 0 && asked.budgetTokens !== void 0 && asked.budgetTokens >= leftTokens;
598
592
  const spent = outcome !== null && outcomeEnd(outcome).kind === "budget_limit";
599
593
  if (capped && spent) why = context.quota?.().blocked ?? "a session reached what was left of a quota";
600
594
  }
@@ -628,6 +622,89 @@ const notStarted = (kind, reason, limited) => ({
628
622
  }),
629
623
  ...limited === void 0 ? {} : { limited }
630
624
  });
625
+ // ../nodes/src/session-driver.ts
626
+ /**
627
+ * @file The driver a Claude node's session starts on (core's `sessionDriver`),
628
+ * and, when its model names that driver (`command-code.deepseek/…`, RFC-015),
629
+ * the fit check on the value an item rendered: the run's own check before it
630
+ * started saw only the written model.
631
+ */
632
+ /** What stops the node from running on `driver`, in one line, or nothing. */
633
+ const refusalOn = (context, driver, drivers) => {
634
+ const id = context.template ?? context.id;
635
+ const node = context.workflow.nodes[id];
636
+ const descriptor = drivers.find((d) => d.id === driver);
637
+ if (node === void 0 || descriptor === void 0) return void 0;
638
+ const refusals = sessionRefusals(id, node, context.workflow, descriptor);
639
+ return refusals.length === 0 ? void 0 : refusals.map((r) => r.message).join("; ");
640
+ };
641
+ /** The session's driver, or the node's result when none fits. */
642
+ const sessionDriverOf = (context, plan, options) => {
643
+ const driver = sessionDriver({
644
+ pinned: plan.pinned,
645
+ override: options.driver,
646
+ named: plan.driver,
647
+ ready: options.readyDriver
648
+ });
649
+ if (driver === void 0) return notStarted("tool_missing", "no agent driver is ready; see `ork doctor`");
650
+ const unfit = plan.pinned === void 0 ? void 0 : refusalOn(context, driver, options.drivers ?? AGENT_DRIVERS);
651
+ return unfit === void 0 ? driver : notStarted("invalid_node", unfit);
652
+ };
653
+ // ../nodes/src/start-budgets.ts
654
+ /** What a session may spend as it starts: the least of the budget and any quota. */
655
+ const budgetNow = (context) => context.budgetLeftNow === void 0 ? context.budgetLeftUsd : least(context.budgetLeftNow(), context.quota?.().leftUsd);
656
+ /** The counted tokens a session may use as it starts: the run's and a quota's. */
657
+ const tokensNow = (context) => least(context.tokensLeftNow?.(), context.quota?.().leftTokens);
658
+ /**
659
+ * What is `left` now, with `own` (the node's cap) set aside through `reserve`
660
+ * in the same step; with nothing left, it waits for a worker's reserve to
661
+ * free.
662
+ */
663
+ const startWith = async (context, left, own, reserve) => {
664
+ const now = left();
665
+ if (now !== void 0 && now <= 0) {
666
+ context.reserveBudget?.(0);
667
+ context.reserveTokens?.(0);
668
+ return await context.budgetFreed?.(context.signal) === true ? startWith(context, left, own, reserve) : now;
669
+ }
670
+ reserve?.(own === void 0 || now === void 0 ? now : Math.min(own, now));
671
+ return now;
672
+ };
673
+ /**
674
+ * What a node's first session may spend, set aside from the run's budget in the
675
+ * same step that reads it (ADR-077), so a node starting beside it is never
676
+ * offered the same part: the least of what is left and the node's own
677
+ * `budgetUsd`. With nothing left, it waits for a worker running beside it to
678
+ * finish, as long as one may.
679
+ */
680
+ const budgetToStart = (context) => startWith(context, () => budgetNow(context), isClaudeNode(context.node) ? context.node.budgetUsd ?? context.workflow.defaults?.budgetUsd : void 0, context.reserveBudget);
681
+ /** `budgetToStart` in counted tokens: the run's `budgetTokens` and a quota's. */
682
+ const tokensToStart = (context) => startWith(context, () => tokensNow(context), isClaudeNode(context.node) ? context.node.budgetTokens ?? context.workflow.defaults?.budgetTokens : void 0, context.reserveTokens);
683
+ /**
684
+ * What a node's first session may spend, in dollars and in counted tokens, set
685
+ * into `planOptions`; the node's result instead when nothing is left of
686
+ * either.
687
+ */
688
+ const startBudgets = async (context, planOptions) => {
689
+ const usd = await budgetToStart(context);
690
+ if (usd !== void 0 && usd <= 0) return notStarted("budget_limit", "nothing is left of the run's budget");
691
+ if (usd !== void 0) planOptions.budgetLeftUsd = usd;
692
+ const waited = { done: false };
693
+ const tokens = await tokensToStart({
694
+ ...context,
695
+ budgetFreed: async (signal) => {
696
+ waited.done = true;
697
+ return await context.budgetFreed?.(signal) ?? false;
698
+ }
699
+ });
700
+ if (tokens !== void 0 && tokens <= 0) return notStarted("budget_limit", "nothing is left of the run's tokens");
701
+ if (waited.done) {
702
+ context.reserveTokens?.(0);
703
+ return startBudgets(context, planOptions);
704
+ }
705
+ if (tokens !== void 0) planOptions.budgetLeftTokens = tokens;
706
+ return null;
707
+ };
631
708
  // ../nodes/src/tool-output.ts
632
709
  /**
633
710
  * @file A tool node's structured result (`output: { format: json }`): the file
@@ -870,6 +947,28 @@ const tokenize = (command) => {
870
947
  args
871
948
  };
872
949
  };
950
+ /**
951
+ * Runs `execute`, or, with `once: true`, skips it after a pass on these files:
952
+ * `null` then, and `verify.skipped` is recorded. `shown` is the scrubbed
953
+ * command.
954
+ */
955
+ const runUnlessPassed = async ({ node, context, cwd, shown, environ, execute }) => {
956
+ if (node.once !== true) return execute();
957
+ const done = {};
958
+ const once = await runOnce({
959
+ cwd,
960
+ name: node.script === void 0 ? shown : `${shown}\n${node.script}`,
961
+ who: `${context.runId}/${context.id}`,
962
+ env: environ,
963
+ run: async () => {
964
+ done.run = await execute();
965
+ return done.run.exitCode;
966
+ }
967
+ });
968
+ if (!once.skipped) return done.run ?? execute();
969
+ context.skipped?.(shown, once.pass);
970
+ return null;
971
+ };
873
972
  // ../nodes/src/tool-ends.ts
874
973
  /**
875
974
  * @file How a tool node ended (specs/runtime/session-ends.md): a node that
@@ -915,6 +1014,18 @@ const exitEnd = (output, run, context, program, signedOut) => {
915
1014
  output
916
1015
  }, end);
917
1016
  };
1017
+ /** The result of a skipped check: it passed, and printed nothing this time. */
1018
+ const skippedCheck = (outputFile) => {
1019
+ if (outputFile !== void 0) removeOutputFile(outputFile);
1020
+ return endedWith({
1021
+ ok: true,
1022
+ output: {
1023
+ exitCode: 0,
1024
+ stdout: "",
1025
+ stderr: ""
1026
+ }
1027
+ }, endFor("done"));
1028
+ };
918
1029
  // ../nodes/src/tool-scrub.ts
919
1030
  /**
920
1031
  * @file Masking a secret in what a program prints. A plain search, so a value
@@ -1245,7 +1356,8 @@ const clearStateFiles = (runDir, warn) => {
1245
1356
  * without a shell, checked against `allow` (or the integration's default),
1246
1357
  * its arguments and observed output fields from the integration's adapter,
1247
1358
  * long output kept in run files. It sees who it is and the node's `env:`
1248
- * (`tool-env.ts`), and may return JSON (`tool-output.ts`).
1359
+ * (`tool-env.ts`), may return JSON (`tool-output.ts`), and may run once per
1360
+ * set of files (`runUnlessPassed` in `tool-command.ts`).
1249
1361
  */
1250
1362
  /** How much of stdout and stderr a node's output keeps. */
1251
1363
  const OUTPUT_LIMIT = 65536;
@@ -1346,29 +1458,43 @@ const toolExecutor = (given, options = {}) => async (context) => {
1346
1458
  ...node.watch === void 0 ? {} : { [STATE_ENV]: prepareStateFile(context.runDir, context.id) }
1347
1459
  };
1348
1460
  const names = Object.keys(nodeEnv.values);
1349
- const release = await context.machine?.slot([command.program, ...command.args].join(" "), context.signal);
1350
- context.emit({
1351
- type: "tool.called",
1352
- id: context.id,
1353
- name: scrub(command.program),
1354
- input: {
1355
- command: scrub([
1356
- command.program,
1357
- ...command.args,
1358
- ...node.script === void 0 ? [] : ["<script>"]
1359
- ].join(" ")),
1360
- ...names.length === 0 ? {} : { env: names }
1361
- }
1362
- });
1363
- const scriptFile = node.script === void 0 ? void 0 : writeScriptFile(context.runDir, context.id, context.round, node.script);
1364
- const priority = context.machine?.priority;
1365
- const raw = await runner(command.program, scriptFile === void 0 ? command.args : [...command.args, scriptFile], {
1461
+ const shown = scrub([
1462
+ command.program,
1463
+ ...command.args,
1464
+ ...node.script === void 0 ? [] : ["<script>"]
1465
+ ].join(" "));
1466
+ const execute = async () => {
1467
+ const release = await context.machine?.slot([command.program, ...command.args].join(" "), context.signal);
1468
+ context.emit({
1469
+ type: "tool.called",
1470
+ id: context.id,
1471
+ name: scrub(command.program),
1472
+ input: {
1473
+ command: shown,
1474
+ ...names.length === 0 ? {} : { env: names }
1475
+ }
1476
+ });
1477
+ const scriptFile = node.script === void 0 ? void 0 : writeScriptFile(context.runDir, context.id, context.round, node.script);
1478
+ const priority = context.machine?.priority;
1479
+ return runner(command.program, scriptFile === void 0 ? command.args : [...command.args, scriptFile], {
1480
+ cwd,
1481
+ signal: context.signal,
1482
+ env,
1483
+ ...priority === void 0 ? {} : { priority }
1484
+ }).finally(() => {
1485
+ release?.();
1486
+ if (scriptFile !== void 0) removeOutputFile(scriptFile);
1487
+ });
1488
+ };
1489
+ const raw = await runUnlessPassed({
1490
+ node,
1491
+ context,
1366
1492
  cwd,
1367
- signal: context.signal,
1368
- env,
1369
- ...priority === void 0 ? {} : { priority }
1370
- }).finally(() => release?.());
1371
- if (scriptFile !== void 0) removeOutputFile(scriptFile);
1493
+ shown,
1494
+ environ,
1495
+ execute
1496
+ });
1497
+ if (raw === null) return skippedCheck(outputFile);
1372
1498
  const run = {
1373
1499
  exitCode: raw.exitCode,
1374
1500
  stdout: scrub(raw.stdout),
@@ -1597,7 +1723,7 @@ const checkedResult = (result, { check, resumes }) => {
1597
1723
  };
1598
1724
  // ../nodes/src/claude.ts
1599
1725
  /**
1600
- * @file The Claude node type: runs a `claude.leader` or `claude.worker` as one
1726
+ * @file The Claude node type: runs an `agent.leader` or `agent.worker` as one
1601
1727
  * session, planned with the run's outputs (and the brief its owner gave it),
1602
1728
  * and turns its report into the node's output. A leader gets its children
1603
1729
  * through the session's ports, and resumes its session on re-entry
@@ -1640,7 +1766,7 @@ const specFor = (spec, context) => {
1640
1766
  };
1641
1767
  return spec;
1642
1768
  };
1643
- /** Runs a `claude.leader` or `claude.worker` as one session. */
1769
+ /** Runs an `agent.leader` or `agent.worker` as one session. */
1644
1770
  const claudeExecutor = (options) => async (context) => {
1645
1771
  const planOptions = planOptionsOf(context, options.inPlace);
1646
1772
  const before = beforeOf(context.node, context.workflow.defaults);
@@ -1664,9 +1790,8 @@ const claudeExecutor = (options) => async (context) => {
1664
1790
  }
1665
1791
  const blocked = context.quota?.().blocked;
1666
1792
  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");
1669
- if (budget !== void 0) planOptions.budgetLeftUsd = budget;
1793
+ const refused = await startBudgets(context, planOptions);
1794
+ if (refused !== null) return refused;
1670
1795
  const plan = planNodeSession(context.workflow, context.id, planOptions);
1671
1796
  if (!plan.ok) return notStarted("invalid_node", plan.error);
1672
1797
  const servers = namedMcpEntries(context.projectRoot, plan.mcp);
@@ -1676,12 +1801,8 @@ const claudeExecutor = (options) => async (context) => {
1676
1801
  ...servers.value
1677
1802
  };
1678
1803
  if (options.machineClaude === true) spec.machineClaude = true;
1679
- const driver = sessionDriver({
1680
- override: options.driver,
1681
- named: plan.driver,
1682
- ready: options.readyDriver
1683
- });
1684
- if (driver === void 0) return notStarted("tool_missing", "no agent driver is ready; see `ork doctor`");
1804
+ const driver = sessionDriverOf(context, plan, options);
1805
+ if (typeof driver !== "string") return driver;
1685
1806
  const runtime = await options.runtimeFor(driver);
1686
1807
  const ports = { human: context.owner === void 0 ? withdrawnOnStop(context) : ownerFirst(context, context.owner, {
1687
1808
  runtimeFor: options.runtimeFor,
@@ -1701,6 +1822,7 @@ const claudeExecutor = (options) => async (context) => {
1701
1822
  const run = async (asked) => {
1702
1823
  const watching = watch.session(asked);
1703
1824
  context.reserveBudget?.(asked.budgetUsd);
1825
+ context.reserveTokens?.(asked.budgetTokens);
1704
1826
  const session = runtime.start(asked, ports);
1705
1827
  const stop = () => {
1706
1828
  session.interrupt();
@@ -1719,6 +1841,7 @@ const claudeExecutor = (options) => async (context) => {
1719
1841
  }
1720
1842
  } finally {
1721
1843
  context.reserveBudget?.(0);
1844
+ context.reserveTokens?.(0);
1722
1845
  disconnect?.();
1723
1846
  context.signal.removeEventListener("abort", stop);
1724
1847
  }
@@ -1734,8 +1857,12 @@ const claudeExecutor = (options) => async (context) => {
1734
1857
  }
1735
1858
  return lateLimits(spec, {
1736
1859
  spent: context.ownSpend?.(sessionId, usage.costUsd) ?? usage.costUsd,
1737
- turns: usage.turns
1738
- }, context.budgetLeftNow?.(), quota?.leftUsd);
1860
+ turns: usage.turns,
1861
+ tokens: countedTokens(usage)
1862
+ }, context.budgetLeftNow?.(), quota?.leftUsd, {
1863
+ run: context.tokensLeftNow?.(),
1864
+ quota: quota?.leftTokens
1865
+ });
1739
1866
  };
1740
1867
  const resumeWith = (outcome, prompt, limits) => run({
1741
1868
  ...spec,
@@ -1824,6 +1951,7 @@ const gateExecutor = async (context) => {
1824
1951
  nodeId: context.id,
1825
1952
  question,
1826
1953
  options,
1954
+ waits: true,
1827
1955
  signal: withdraw.signal,
1828
1956
  onNote: (given) => {
1829
1957
  note = given;
@@ -2143,8 +2271,8 @@ const builtinExecutors = (options) => {
2143
2271
  steps: tool
2144
2272
  });
2145
2273
  const executors = {
2146
- "claude.leader": claude,
2147
- "claude.worker": claude,
2274
+ "agent.leader": claude,
2275
+ "agent.worker": claude,
2148
2276
  "human.gate": gateExecutor,
2149
2277
  "workflow.call": callExecutor,
2150
2278
  "mcp.call": mcpCallExecutor(options.maskedNames === void 0 ? {} : { maskedNames: options.maskedNames })