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

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 (48) hide show
  1. package/README.md +10 -4
  2. package/dist/__build_info__-DcRdK0Du.mjs +359 -0
  3. package/dist/apply-Mb0fNpCX.mjs +17 -0
  4. package/dist/{attach-CdtGX9se.mjs → attach-7lB5_jpr.mjs} +5 -4
  5. package/dist/{store-CleF7KeS.mjs → attach-B-FeE1dJ.mjs} +60 -25
  6. package/dist/bin.mjs +328 -1734
  7. package/dist/brief-BvMtIFio.mjs +58 -0
  8. package/dist/{builtin-files-Bdq23IIi.mjs → builtin-files-BRSOObAv.mjs} +1 -1
  9. package/dist/command-tree-BkSYWmYJ.mjs +63 -0
  10. package/dist/{completion--ywde-56.mjs → completion-Cgb2qPPh.mjs} +45 -6
  11. package/dist/context-BN9A2Nnu.mjs +106 -0
  12. package/dist/{find-BVX-bL4u.mjs → find-mLYsR0uC.mjs} +2 -2
  13. package/dist/flags-CdgIH1L6.mjs +230 -0
  14. package/dist/follow-ups-DxCsOby1.mjs +496 -0
  15. package/dist/{format-CvTE5CZQ.mjs → format-hiDvtNGM.mjs} +35 -8
  16. package/dist/{help-groups-CpR2UzSn.mjs → help-groups-hB6kY34d.mjs} +5 -63
  17. package/dist/{in-a-run-cL5N9gpH.mjs → in-a-run-a447hxq8.mjs} +1 -1
  18. package/dist/{__build_info__-B82-dGJY.mjs → install-23g_1ngs.mjs} +3 -227
  19. package/dist/{limits-Dhe--wmX.mjs → limits-C_Xz_t3C.mjs} +1 -1
  20. package/dist/{mcp-CsmboyXv.mjs → mcp-CWng9I9n.mjs} +6 -6
  21. package/dist/{once-CrwI-ZdN.mjs → once-lvwcWfVf.mjs} +3 -3
  22. package/dist/orphan-guard-BFKg8VSd.mjs +58 -0
  23. package/dist/{pause-DssrYcda.mjs → pause-Cy11KrXK.mjs} +3 -3
  24. package/dist/post-C9soCx7i.mjs +46 -0
  25. package/dist/print-BaWiWf4v.mjs +19 -0
  26. package/dist/project-records-BRiyQzUo.mjs +230 -0
  27. package/dist/{relay-DASRgsTQ.mjs → relay-C0M1TWMB.mjs} +1 -1
  28. package/dist/run-B-j3Qxqc.mjs +1464 -0
  29. package/dist/{run-D8divGCs.mjs → run-D_u9V_Ve.mjs} +1 -1
  30. package/dist/{send-CXv8bx0i.mjs → send-qBxj0JbJ.mjs} +2 -2
  31. package/dist/{src-njdtejgZ.mjs → src-CXBmaGwS.mjs} +213 -75
  32. package/dist/{src-C7z_6fhn.mjs → src-DHDQ0itC.mjs} +106 -50
  33. package/dist/{src-tsuZRu0H.mjs → src-DN-RPsil.mjs} +5423 -4467
  34. package/dist/{src-8Mul0695.mjs → src-nXLIwDrt.mjs} +325 -81
  35. package/dist/store-Dyr1mP8K.mjs +16 -0
  36. package/dist/ui/assets/app-BPSCxb_l.css +1 -0
  37. package/dist/ui/assets/app-CnNRowFn.js +111 -0
  38. package/dist/ui/assets/{index-D-_uPI2v.js → index-CVzwYlz3.js} +2 -2
  39. package/dist/ui/index.html +1 -1
  40. package/dist/{ui-CNO1M0qT.mjs → ui-DLrSt5ny.mjs} +3 -3
  41. package/dist/{usage-BVXi0iFY.mjs → usage-CxmpYYO4.mjs} +61 -107
  42. package/dist/{context-Y_I4Mofq.mjs → validate-BVrT43GO.mjs} +35 -108
  43. package/dist/{watch-DVNIciHp.mjs → watch-mkLm4t9D.mjs} +19 -5
  44. package/dist/{workflows-B30QkwTk.mjs → workflows-Bvl2Fquu.mjs} +2 -2
  45. package/dist/{worktrees-DD4_TpkH.mjs → worktrees-DOOCoS2w.mjs} +49 -20
  46. package/package.json +8 -4
  47. package/dist/ui/assets/app-D_nAPnHM.css +0 -1
  48. package/dist/ui/assets/app-DqBAtXSZ.js +0 -111
@@ -1,12 +1,13 @@
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";
1
+ import { $r as programsRunBy, At as endedWith, Dt as runScope, Et as heldTimeout, Gn as endOf, It as lowerChild, Kn as outcomeEnd, Mt as stoppedEnd, Nt as declaredProblem, Ot as calledWorkflows, Pt as slotClock, Qr as beforeOf, Rt as startAt, Y as sessionRefusals, Yn as endFor, _i as TOOL_PREFIX, _t as planNodeSession, cn as mcpNotText, di as isToolNode, dr as renderTemplate, gi as INTEGRATIONS, hi as timeoutMs, in as sessionDriver, jt as failedWith, ln as serversIn, lr as evaluate, mt as planReason, on as declaredOutputOf, qn as END_DETAIL_LIMIT, ri as markValue, sn as MCP_FILE, tn as envReadsOf, ui as isClaudeNode, ur as parseTemplate, vi as integrationFor, wi as AGENT_DRIVERS, yt as reportSchemaFor, zn as countedTokens } from "./src-DN-RPsil.mjs";
2
+ import { t as runOnce } from "./run-D_u9V_Ve.mjs";
3
+ import { n as holdGuard, r as orphanGuard, t as KILL_AFTER_MS } from "./orphan-guard-BFKg8VSd.mjs";
3
4
  import path from "node:path";
4
5
  import { accessSync, constants, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
6
  import { spawn } from "node:child_process";
7
+ import { Readable } from "node:stream";
6
8
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7
9
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
8
10
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
9
- import { Readable } from "node:stream";
10
11
  // ../nodes/src/call.ts
11
12
  /**
12
13
  * @file The `workflow.call` node type (RFC-004): renders what the node asks for
@@ -59,7 +60,9 @@ const callExecutor = (context) => {
59
60
  // ../nodes/src/claude-plan.ts
60
61
  /**
61
62
  * @file What a Claude node's session is planned with, read from the node's
62
- * context.
63
+ * context, and what it is told beyond its plan: the note of a sent-back input
64
+ * on re-entry, a resumed leader's message, and the turn for messages that
65
+ * came in as it finished.
63
66
  */
64
67
  /** The plan options every session of a node starts from. */
65
68
  const planOptionsOf = (context, inPlace) => {
@@ -82,6 +85,30 @@ const planOptionsOf = (context, inPlace) => {
82
85
  if (context.claims !== void 0) planOptions.claims = context.claims;
83
86
  return planOptions;
84
87
  };
88
+ const MAX_INPUT = 8192;
89
+ const inputJson = (input) => JSON.stringify(input ?? {}, void 0, 2);
90
+ /** What a sent-back input is whole, for `read_input`, when its note cuts it. */
91
+ const wholeInput = (input) => {
92
+ const json = inputJson(input);
93
+ return json.length > MAX_INPUT ? json : void 0;
94
+ };
95
+ /** A re-entry's note of what was sent back; `read_input` has a cut rest. */
96
+ const reentryNote = (round, input) => {
97
+ const json = inputJson(input);
98
+ const cut = json.length > MAX_INPUT;
99
+ const body = cut ? `${json.slice(0, MAX_INPUT)}\n…` : json;
100
+ 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.` : "";
101
+ return `\n\n## Round ${String(round)}\n\nThe workflow sent this task back to you with:\n\n\`\`\`json\n${body}\n\`\`\`${rest}`;
102
+ };
103
+ /**
104
+ * What a resumed leader is told: it already has its brief and its earlier work,
105
+ * so only the new round, and the new task when its owner sent one.
106
+ */
107
+ const resumeMessage = (round, input, brief) => [brief === void 0 ? sentBack(round, input) : `## New task from \`${brief.from}\`\n\n${brief.text}`, "End with a new structured report."].join("\n\n");
108
+ /** A first-round resume is a session the run was cut off in (`ork resume`). */
109
+ const sentBack = (round, input) => round === 1 ? "Ork was stopped while you worked on this task, and the run is resumed. Your earlier turns are above: check what you finished, and carry on from there." : `The workflow sent your task back to you.${reentryNote(round, input)}`;
110
+ /** What a session is told when messages came in as it finished. */
111
+ const lateMessage = (count) => `[ork] ${String(count)} message${count === 1 ? "" : "s"} came in while you finished. Call read_inbox, act on ${count === 1 ? "it" : "them"} if needed, and end with a new structured report.`;
85
112
  // ../nodes/src/claude-result.ts
86
113
  /**
87
114
  * @file A finished session as a node's result: its report is the output, held
@@ -551,12 +578,13 @@ const leftOf = (caps, used, quotaLeft) => least(...caps.map((cap) => cap === voi
551
578
  * budget has counted. The quota has: the ledger took it as the session
552
579
  * finished, so what it says is left is left. The run's tokens left have too:
553
580
  * only the node's own `budgetTokens` takes `tokens` off. `null` when nothing
554
- * is, and the turn does not start.
581
+ * is, and the turn does not start. `turnsAtLeast` lifts the turns left to that
582
+ * many (a failed check's resume: `CHECK_RETRY_TURNS`).
555
583
  */
556
- const lateLimits = (spec, used, runLeft, quotaLeft, tokensLeft = {}) => {
584
+ const lateLimits = (spec, used, runLeft, quotaLeft, tokensLeft = {}, turnsAtLeast = 0) => {
557
585
  const budgetUsd = leftOf([spec.budgetUsd, runLeft], used.spent, quotaLeft);
558
586
  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;
587
+ const maxTurns = spec.maxTurns === void 0 ? void 0 : Math.max(spec.maxTurns - used.turns, turnsAtLeast);
560
588
  if ([
561
589
  budgetUsd,
562
590
  budgetTokens,
@@ -568,18 +596,36 @@ const lateLimits = (spec, used, runLeft, quotaLeft, tokensLeft = {}) => {
568
596
  ...maxTurns === void 0 ? {} : { maxTurns }
569
597
  };
570
598
  };
599
+ const reached = (cap, used) => cap !== void 0 && used >= cap;
600
+ /**
601
+ * Why the run's budget stopped a session that reached its cap, if it did. A
602
+ * unit blames the run when the session reached it, or when it is the only cap
603
+ * the session had; otherwise the node's own cap in the other unit may have
604
+ * stopped it. A resumed turn's dollars are its own: its cost is the session's
605
+ * total (ADR-046) while its cap is what was left, so `priorCostUsd` comes off,
606
+ * read as `ownSpend` reads it (a lower total counted again from zero).
607
+ */
608
+ const runSpent = (run, asked, outcome, priorCostUsd) => {
609
+ const total = outcome.usage.costUsd;
610
+ const turnCostUsd = total < priorCostUsd ? total : total - priorCostUsd;
611
+ if (run.tokens !== void 0 && (reached(asked.budgetTokens, countedTokens(outcome.usage)) || asked.budgetUsd === void 0)) return `the run's budget of ${String(run.tokens)} tokens is spent`;
612
+ return run.usd !== void 0 && (reached(asked.budgetUsd, turnCostUsd) || asked.budgetTokens === void 0) ? `the run's budget of $${String(run.usd)} is spent` : void 0;
613
+ };
571
614
  /**
572
615
  * Whether a limit across runs, rather than the node's work, ended a node: a
573
616
  * session whose cap was a quota's and that ran into it, or one its plan refused
574
- * and that gave no report. A node that still succeeded is left alone.
617
+ * and that gave no report. A session whose cap was the run's budget and that
618
+ * ran into it stops the run as `budget_exhausted` (`run`, from `runCapsOf`). A
619
+ * node that still succeeded is left alone.
575
620
  */
576
- const limitWatch = (context) => {
621
+ const limitWatch = (context, run = {}) => {
577
622
  let why;
623
+ let runWhy;
578
624
  return {
579
625
  stoppedBy: (reason) => {
580
626
  why = reason;
581
627
  },
582
- session: (asked) => {
628
+ session: (asked, priorCostUsd = 0) => {
583
629
  const { leftUsd: left, leftTokens } = context.quota?.() ?? {};
584
630
  let refused;
585
631
  return {
@@ -591,6 +637,7 @@ const limitWatch = (context) => {
591
637
  const capped = left !== void 0 && asked.budgetUsd !== void 0 && asked.budgetUsd >= left || leftTokens !== void 0 && asked.budgetTokens !== void 0 && asked.budgetTokens >= leftTokens;
592
638
  const spent = outcome !== null && outcomeEnd(outcome).kind === "budget_limit";
593
639
  if (capped && spent) why = context.quota?.().blocked ?? "a session reached what was left of a quota";
640
+ else if (spent) runWhy = runSpent(run, asked, outcome, priorCostUsd);
594
641
  }
595
642
  };
596
643
  },
@@ -600,6 +647,11 @@ const limitWatch = (context) => {
600
647
  ...endedWith(result, endFor("plan_limit", {}, why)),
601
648
  limited: why
602
649
  };
650
+ if (runWhy !== void 0) return {
651
+ ...result,
652
+ limited: runWhy,
653
+ stopsAs: "budget_exhausted"
654
+ };
603
655
  const { end } = result;
604
656
  return end?.kind === "plan_limit" ? {
605
657
  ...result,
@@ -677,9 +729,31 @@ const startWith = async (context, left, own, reserve) => {
677
729
  * `budgetUsd`. With nothing left, it waits for a worker running beside it to
678
730
  * finish, as long as one may.
679
731
  */
680
- const budgetToStart = (context) => startWith(context, () => budgetNow(context), isClaudeNode(context.node) ? context.node.budgetUsd ?? context.workflow.defaults?.budgetUsd : void 0, context.reserveBudget);
732
+ const budgetToStart = (context) => startWith(context, () => budgetNow(context), ownCaps(context).usd, context.reserveBudget);
733
+ /** The node's own caps: its `budgetUsd` and `budgetTokens`, else the defaults'. */
734
+ const ownCaps = (context) => isClaudeNode(context.node) ? {
735
+ usd: context.node.budgetUsd ?? context.workflow.defaults?.budgetUsd,
736
+ tokens: context.node.budgetTokens ?? context.workflow.defaults?.budgetTokens
737
+ } : {};
738
+ /**
739
+ * Where the run's budget, not the node's own cap, caps the node's first
740
+ * session: the run has a budget in that unit, and what `startBudgets` left the
741
+ * session is below the node's own cap, or it has none. A later turn of the
742
+ * session keeps them: both its caps go down by what it used, and the run's by
743
+ * what others used too.
744
+ */
745
+ const runCapsOf = (context, planOptions) => {
746
+ const own = ownCaps(context);
747
+ const limits = context.workflow.limits;
748
+ const caps = {};
749
+ const usd = planOptions.budgetLeftUsd;
750
+ if (context.budgetLeftNow?.() !== void 0 && limits?.budgetUsd !== void 0 && usd !== void 0 && usd < (own.usd ?? Infinity)) caps.usd = limits.budgetUsd;
751
+ const tokens = planOptions.budgetLeftTokens;
752
+ if (context.tokensLeftNow?.() !== void 0 && limits?.budgetTokens !== void 0 && tokens !== void 0 && tokens < (own.tokens ?? Infinity)) caps.tokens = limits.budgetTokens;
753
+ return caps;
754
+ };
681
755
  /** `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);
756
+ const tokensToStart = (context) => startWith(context, () => tokensNow(context), ownCaps(context).tokens, context.reserveTokens);
683
757
  /**
684
758
  * What a node's first session may spend, in dollars and in counted tokens, set
685
759
  * into `planOptions`; the node's result instead when nothing is left of
@@ -1729,30 +1803,6 @@ const checkedResult = (result, { check, resumes }) => {
1729
1803
  * through the session's ports, and resumes its session on re-entry
1730
1804
  * (ADR-019).
1731
1805
  */
1732
- const MAX_INPUT = 8192;
1733
- const inputJson = (input) => JSON.stringify(input ?? {}, void 0, 2);
1734
- /** What a sent-back input is whole, for `read_input`, when its note cuts it. */
1735
- const wholeInput = (input) => {
1736
- const json = inputJson(input);
1737
- return json.length > MAX_INPUT ? json : void 0;
1738
- };
1739
- /** A re-entry's note of what was sent back; `read_input` has a cut rest. */
1740
- const reentryNote = (round, input) => {
1741
- const json = inputJson(input);
1742
- const cut = json.length > MAX_INPUT;
1743
- const body = cut ? `${json.slice(0, MAX_INPUT)}\n…` : json;
1744
- 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.` : "";
1745
- return `\n\n## Round ${String(round)}\n\nThe workflow sent this task back to you with:\n\n\`\`\`json\n${body}\n\`\`\`${rest}`;
1746
- };
1747
- /**
1748
- * What a resumed leader is told: it already has its brief and its earlier work,
1749
- * so only the new round, and the new task when its owner sent one.
1750
- */
1751
- const resumeMessage = (round, input, brief) => [brief === void 0 ? sentBack(round, input) : `## New task from \`${brief.from}\`\n\n${brief.text}`, "End with a new structured report."].join("\n\n");
1752
- /** A first-round resume is a session the run was cut off in (`ork resume`). */
1753
- const sentBack = (round, input) => round === 1 ? "Ork was stopped while you worked on this task, and the run is resumed. Your earlier turns are above: check what you finished, and carry on from there." : `The workflow sent your task back to you.${reentryNote(round, input)}`;
1754
- /** What a session is told when messages came in as it finished. */
1755
- const lateMessage = (count) => `[ork] ${String(count)} message${count === 1 ? "" : "s"} came in while you finished. Call read_inbox, act on ${count === 1 ? "it" : "them"} if needed, and end with a new structured report.`;
1756
1806
  /** A re-entered leader resumes; anyone else starts with the full brief. */
1757
1807
  const specFor = (spec, context) => {
1758
1808
  if (context.resume !== void 0) return {
@@ -1816,11 +1866,12 @@ const claudeExecutor = (options) => async (context) => {
1816
1866
  if (context.mailbox !== void 0) ports.mailbox = context.mailbox;
1817
1867
  if (context.claims !== void 0) ports.claims = context.claims;
1818
1868
  if (context.feedback !== void 0) ports.feedback = context.feedback;
1869
+ if (context.followUps !== void 0) ports.followUps = context.followUps;
1819
1870
  const whole = context.round > 1 ? wholeInput(context.input) : void 0;
1820
1871
  if (whole !== void 0) ports.input = whole;
1821
- const watch = limitWatch(context);
1822
- const run = async (asked) => {
1823
- const watching = watch.session(asked);
1872
+ const watch = limitWatch(context, runCapsOf(context, planOptions));
1873
+ const run = async (asked, prior = 0) => {
1874
+ const watching = watch.session(asked, prior);
1824
1875
  context.reserveBudget?.(asked.budgetUsd);
1825
1876
  context.reserveTokens?.(asked.budgetTokens);
1826
1877
  const session = runtime.start(asked, ports);
@@ -1849,7 +1900,7 @@ const claudeExecutor = (options) => async (context) => {
1849
1900
  return outcome;
1850
1901
  };
1851
1902
  const first = await run(spec);
1852
- const limitsAfter = ({ sessionId, usage }) => {
1903
+ const limitsAfter = ({ sessionId, usage }, turnsAtLeast = 0) => {
1853
1904
  const quota = context.quota?.();
1854
1905
  if (quota?.blocked !== void 0) {
1855
1906
  watch.stoppedBy(quota.blocked);
@@ -1862,14 +1913,14 @@ const claudeExecutor = (options) => async (context) => {
1862
1913
  }, context.budgetLeftNow?.(), quota?.leftUsd, {
1863
1914
  run: context.tokensLeftNow?.(),
1864
1915
  quota: quota?.leftTokens
1865
- });
1916
+ }, turnsAtLeast);
1866
1917
  };
1867
1918
  const resumeWith = (outcome, prompt, limits) => run({
1868
1919
  ...spec,
1869
1920
  ...limits,
1870
1921
  resume: outcome.sessionId,
1871
1922
  prompt
1872
- });
1923
+ }, outcome.usage.costUsd);
1873
1924
  const lateTurn = (current) => {
1874
1925
  const late = context.mailbox?.pending() ?? 0;
1875
1926
  const limits = limitsAfter(current);
@@ -1895,7 +1946,7 @@ const claudeExecutor = (options) => async (context) => {
1895
1946
  },
1896
1947
  late: lateTurn,
1897
1948
  resume: async (current, check) => {
1898
- const limits = limitsAfter(current);
1949
+ const limits = limitsAfter(current, 20);
1899
1950
  if (limits === null) return null;
1900
1951
  await context.hold?.wait("start", context.signal);
1901
1952
  return context.signal.aborted ? null : resumeWith(current, checkFailedMessage(check, {
@@ -2143,20 +2194,23 @@ const mcpCallExecutor = (options = {}) => async (context) => {
2143
2194
  * whatever it started: SIGTERM, then SIGKILL for what ignores that. A stop or
2144
2195
  * a timeout therefore leaves nothing running, and an escaped process that
2145
2196
  * keeps the output pipes open cannot hold the node. Off Windows it runs under
2146
- * `sh`, whose `times` says the CPU the program and its children used, and at
2147
- * the project's priority: `low` starts it below Ork's own (nice, and on macOS
2197
+ * `sh`, whose `times` says the CPU the program and its children used, beside
2198
+ * the orphan guard, which ends the group when `ork` dies, and at the
2199
+ * project's priority: `low` starts it below Ork's own (nice, and on macOS
2148
2200
  * utility QoS).
2149
2201
  */
2150
- /** How long a program may take to end after SIGTERM. */
2151
- const KILL_AFTER_MS = 2e3;
2152
2202
  /** How long to wait for the pipes to close after SIGKILL. */
2153
2203
  const CLOSE_AFTER_MS = 500;
2204
+ /** The fd of the orphan guard's pipe from `ork`. */
2205
+ const WATCH_FD = 4;
2154
2206
  /**
2155
- * Runs `$0 "$@"`, then writes the shell's `times` to fd 3: its children's user
2156
- * and system time, the program's whole tree that it waited for. A program that
2207
+ * Runs `$0 "$@"` beside the orphan guard, which ends the group if `ork` dies,
2208
+ * and stops the guard once the program ends, so its leftovers live on as
2209
+ * before. Then writes the shell's `times` to fd 3: its children's user and
2210
+ * system time, the program's whole tree that it waited for. A program that
2157
2211
  * isn't there says so on fd 3, as spawn's ENOENT would.
2158
2212
  */
2159
- const TIMED = String.raw`command -v "$0" >/dev/null 2>&1 || { echo ork-missing >&3; exit 127; }; "$0" "$@" 3>&-; s=$?; times >&3; exit $s`;
2213
+ const TIMED = String.raw`command -v "$0" >/dev/null 2>&1 || { echo ork-missing >&3; exit 127; }; ${orphanGuard(WATCH_FD)}w=$!; "$0" "$@" 3>&- ${String(WATCH_FD)}<&-; s=$?; kill -s KILL $w 2>/dev/null; times >&3; exit $s`;
2160
2214
  const MISSING = "ork-missing";
2161
2215
  /** The children's line of `times` (`0m1.250s 0m0.300s`), in milliseconds. */
2162
2216
  const cpuOfTimes = (text) => {
@@ -2197,10 +2251,12 @@ const spawnTool = (program, args, { cwd, signal, env, priority }) => {
2197
2251
  "ignore",
2198
2252
  "pipe",
2199
2253
  "pipe",
2254
+ "pipe",
2200
2255
  "pipe"
2201
2256
  ]
2202
2257
  });
2203
2258
  lowerChild(priority, child.pid);
2259
+ holdGuard(child.stdio[WATCH_FD]);
2204
2260
  const fd3 = child.stdio[3];
2205
2261
  if (fd3 instanceof Readable) fd3.setEncoding("utf8").on("data", (chunk) => {
2206
2262
  times.push(chunk);