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

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/dist/{__build_info__-DFF18K8y.mjs → __build_info__-BJbE3Zvp.mjs} +4 -72
  2. package/dist/attach-BmV5ckb9.mjs +89 -0
  3. package/dist/bin.mjs +467 -864
  4. package/dist/{decide-3l9q8UtX.mjs → builtin-files-w0YULJ4c.mjs} +2 -50
  5. package/dist/chips-CyeB_rhR.mjs +43 -0
  6. package/dist/{clean-DVaEH9e7.mjs → clean-D8GI_2K-.mjs} +1 -1
  7. package/dist/{completion-Dv5kf598.mjs → completion-CzElB8nS.mjs} +24 -16
  8. package/dist/{context-CEaKOVmG.mjs → context-BAwgVSX5.mjs} +43 -31
  9. package/dist/decide-DlbyctOE.mjs +50 -0
  10. package/dist/{find-DEjjOlTG.mjs → find-p05wlbmu.mjs} +1 -1
  11. package/dist/format-Dni98cbh.mjs +206 -0
  12. package/dist/{help-groups-BwYoAWsh.mjs → help-groups-DYP_zMRP.mjs} +4 -0
  13. package/dist/{in-a-run-BS9Nc-JW.mjs → in-a-run-BI-hLIl_.mjs} +4 -2
  14. package/dist/{limits-BSwEjQST.mjs → limits-CElmhowV.mjs} +1 -1
  15. package/dist/{list-Qxr6rwsY.mjs → list-zW2xKuGr.mjs} +1 -1
  16. package/dist/{mcp-CslnG5wf.mjs → mcp-CM3b65AZ.mjs} +6 -6
  17. package/dist/output-CnRzrbUM.mjs +423 -0
  18. package/dist/{pause-D7OVB5Ac.mjs → pause-BuS83WAI.mjs} +4 -4
  19. package/dist/{relay-EceRIebO.mjs → relay-DAsJA8wF.mjs} +2 -2
  20. package/dist/{remove-0IQZVWmI.mjs → remove-Dh7D_hVn.mjs} +3 -3
  21. package/dist/{report-DK4LGqwF.mjs → report-CEN8VoUb.mjs} +31 -20
  22. package/dist/{send-C0uwfUYi.mjs → send-MFEQZH31.mjs} +3 -3
  23. package/dist/{src-cmmXKHn1.mjs → src-BfxkJOYm.mjs} +234 -150
  24. package/dist/{src-Df6KOIDC.mjs → src-CdDTQLdQ.mjs} +2081 -1035
  25. package/dist/{src-oaJYIddp.mjs → src-D1QedD5y.mjs} +523 -81
  26. package/dist/{src-i5k2Y-kf.mjs → src-S9yK2OHU.mjs} +79 -11
  27. package/dist/store-B2OaE4ID.mjs +1356 -0
  28. package/dist/ui/assets/{app-qs8Lofv6.js → app-CAoZtku5.js} +16 -16
  29. package/dist/ui/assets/app-QoHVi-mb.css +1 -0
  30. package/dist/ui/assets/{index-DV1ZaOk-.js → index-BL7Ad0OA.js} +2 -2
  31. package/dist/ui/index.html +1 -1
  32. package/dist/{ui-CBkHJdqk.mjs → ui-C4Ys26oO.mjs} +4 -4
  33. package/dist/{usage-Cd8pEYyK.mjs → usage-C2zx-X6q.mjs} +43 -32
  34. package/dist/verdict-CIy8tAyX.mjs +84 -0
  35. package/dist/{watch-DW5R2OPO.mjs → watch-DzCeqM05.mjs} +28 -16
  36. package/dist/{workflows-BT1gDRIV.mjs → workflows-Rr5w90A0.mjs} +3 -3
  37. package/dist/{worktrees-g1MQMGie.mjs → worktrees-f6SAbum5.mjs} +11 -10
  38. package/package.json +1 -1
  39. package/dist/format-Cb5_m-Qd.mjs +0 -191
  40. package/dist/output-Bqljtmp_.mjs +0 -40
  41. package/dist/ui/assets/app-C8uhHVYp.css +0 -1
@@ -3,7 +3,7 @@ import { LineCounter, YAMLMap, YAMLSeq, isAlias, isCollection, isMap, isNode, is
3
3
  import { z } from "zod";
4
4
  import { closeSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
5
5
  import { createHash, randomUUID } from "node:crypto";
6
- import { homedir, tmpdir } from "node:os";
6
+ import { constants as constants$1, homedir, setPriority, tmpdir } from "node:os";
7
7
  import { createConnection, createServer } from "node:net";
8
8
  import { createInterface } from "node:readline";
9
9
  import { execFile, execFileSync, spawn } from "node:child_process";
@@ -86,6 +86,7 @@ const aws = defineIntegration({
86
86
  region: z.string().optional()
87
87
  },
88
88
  args: ({ profile, region }) => [...profile === void 0 ? [] : ["--profile", profile], ...region === void 0 ? [] : ["--region", region]],
89
+ signedOut: ({ stderr }) => /Unable to locate credentials|ExpiredToken|InvalidClientTokenId|Token has expired|aws sso login|SSO session .* expired/iu.test(stderr),
89
90
  doctor: async (exec, env) => {
90
91
  if (!(await exec("aws", ["--version"])).ok) return {
91
92
  name: "aws",
@@ -227,7 +228,7 @@ const installedCli = (id, program, login, rest) => ({
227
228
  const unproven = rest.proven ? "" : ", unproven: not yet proven on a real session";
228
229
  if (!version.ok) return {
229
230
  name,
230
- status: rest.proven ? "warn" : "skip",
231
+ status: "skip",
231
232
  detail: `${program} CLI not found${unproven}`,
232
233
  ...rest.proven ? { hint: `Install ${program} to use the ${id} driver.` } : {}
233
234
  };
@@ -247,7 +248,7 @@ const AGENT_DRIVERS = [
247
248
  claudeCode,
248
249
  installedCli("open-code", "opencode", "opencode auth login", {
249
250
  label: "the `opencode` CLI",
250
- proven: false,
251
+ proven: true,
251
252
  onePerCheckout: false,
252
253
  capabilities: {
253
254
  questions: true,
@@ -255,13 +256,13 @@ const AGENT_DRIVERS = [
255
256
  price: true,
256
257
  midTurnMessages: false,
257
258
  reportSchema: false,
258
- turnLimit: false,
259
+ turnLimit: true,
259
260
  mcpServers: true
260
261
  }
261
262
  }),
262
263
  installedCli("command-code", "cmd", "cmd login", {
263
264
  label: "the `cmd` CLI of Command Code",
264
- proven: false,
265
+ proven: true,
265
266
  onePerCheckout: true,
266
267
  capabilities: {
267
268
  questions: true,
@@ -299,6 +300,7 @@ const gh = defineIntegration({
299
300
  program: "gh",
300
301
  options: {},
301
302
  args: () => [],
303
+ signedOut: ({ exitCode, stderr }) => exitCode === 4 || /\bgh auth login\b/u.test(stderr),
302
304
  doctor: async (exec) => {
303
305
  if (!(await exec("gh", ["--version"])).ok) return {
304
306
  name: "gh",
@@ -542,13 +544,14 @@ const DriverSchema = z.enum(DRIVER_IDS, { error: (issue) => retiredDriverHint(is
542
544
  /** `opus`, `sonnet`, `haiku`, or a full model id such as `claude-opus-5-5`. */
543
545
  const ModelSchema = z.string().min(1);
544
546
  /** How hard the model thinks; Claude Code's own default when unset. */
545
- const EffortSchema = z.enum([
547
+ const EFFORTS = [
546
548
  "low",
547
549
  "medium",
548
550
  "high",
549
551
  "xhigh",
550
552
  "max"
551
- ]).describe("Reasoning effort. Default: Claude Code's for the model.");
553
+ ];
554
+ const EffortSchema = z.enum(EFFORTS).describe("Reasoning effort. Default: Claude Code's for the model.");
552
555
  const IsolationSchema = z.enum([
553
556
  "worktree",
554
557
  "continue",
@@ -718,13 +721,20 @@ const TOOL_NODE_SCHEMAS = INTEGRATIONS.map((i) => toolNodeSchema(i));
718
721
  const base = nodeBaseFields;
719
722
  /** A Claude node's output field that is not a list: a rule or JSON Schema. */
720
723
  const NotListSchema = z.unknown().refine((value) => typeof value !== "object" || value === null || !("list" in value), { message: LIST_RULE_HINT });
724
+ const NODE_EFFORT_HINT = "expected low, medium, high, xhigh or max, or {{item.<field>}}";
725
+ /**
726
+ * A Claude node's effort: a level, or on a `forEach` node a template reading
727
+ * its item (`{{item.effort}}`), which the compiler checks and each item
728
+ * renders.
729
+ */
730
+ const NodeEffortSchema = z.union([EffortSchema, z.string().regex(/\{\{.*\}\}/su, { error: NODE_EFFORT_HINT })], { error: NODE_EFFORT_HINT }).describe("Reasoning effort, or {{item.<field>}} on a forEach node. Default: Claude Code's for the model.");
721
731
  /**
722
732
  * What an agent brings to the node it fills, and what a plain Claude node sets
723
733
  * itself. A node's own value wins over its agent's.
724
734
  */
725
735
  const agentSettings = {
726
736
  model: ModelSchema.optional(),
727
- effort: EffortSchema.optional(),
737
+ effort: NodeEffortSchema.optional(),
728
738
  driver: DriverSchema.optional(),
729
739
  skills: z.array(z.string()).optional().describe("Node-layer skills (highest precedence)."),
730
740
  context: z.array(z.string()).optional().describe("Node-layer context files."),
@@ -1076,6 +1086,8 @@ const RulesSchema = z.strictObject({
1076
1086
  permissions: ToolRulesSchema.optional().describe("Tool rules every Claude node gets on top of its own, e.g. deny: ['Bash(git push *)']."),
1077
1087
  links: z.enum(["on", "off"]).optional().describe("Default: on, the user can link a running node to a node of another run. off: its nodes get no message tools unless they have someone to write to, and cannot be linked.")
1078
1088
  });
1089
+ /** The OS priority Ork starts a run's sessions and tool commands at. */
1090
+ const PrioritySchema = z.enum(["low", "normal"]);
1079
1091
  const ProjectSchema = z.strictObject({
1080
1092
  schemaVersion: z.literal(1),
1081
1093
  ...RulesSchema.shape,
@@ -1083,7 +1095,9 @@ const ProjectSchema = z.strictObject({
1083
1095
  quotas: QuotasSchema.optional().describe("Most this project may spend on this machine. The user's own caps apply too."),
1084
1096
  calls: z.strictObject({ accept: z.array(z.string()).describe("The registered projects whose workflows may call this one's.") }).optional().describe("Default: any registered project may call this one's workflows."),
1085
1097
  context: z.array(z.string()).optional().describe("Context files every node reads, relative to the project root; they come before the workflow's."),
1086
- experimental: z.array(z.string()).optional().describe("Experimental features this project turns on.")
1098
+ experimental: z.array(z.string()).optional().describe("Experimental features this project turns on."),
1099
+ heavy: z.array(z.string().min(1)).optional().describe("The project's full-suite commands, as Bash permission patterns without Bash(…), e.g. pnpm check. A node with verify: may not run them in its session; vitest run does not cover vitest run <files>."),
1100
+ priority: PrioritySchema.optional().describe("Default: low, sessions and tool commands start at a lower OS priority. normal: as the ork process.")
1087
1101
  });
1088
1102
  /** `~/.ork/settings.yaml`: the user's own settings, and never a credential. */
1089
1103
  const UserSettingsSchema = z.strictObject({
@@ -3213,7 +3227,7 @@ const filesOf = (dir, name) => ({
3213
3227
  markdown: path.join(dir, `${name}.md`),
3214
3228
  sidecar: path.join(dir, `${name}${AGENT_SIDECAR_SUFFIX}`)
3215
3229
  });
3216
- const write = (name, agent, options, mode) => {
3230
+ const write$1 = (name, agent, options, mode) => {
3217
3231
  if (!AgentNameSchema.safeParse(name).success) return {
3218
3232
  ok: false,
3219
3233
  error: `\`${name}\` is not an agent name (lowercase letters, digits, '-' and '_')`
@@ -3253,7 +3267,7 @@ const write = (name, agent, options, mode) => {
3253
3267
  };
3254
3268
  };
3255
3269
  /** Creates an agent in Ork's folder for the scope; never overwrites. */
3256
- const createAgent = (name, agent, options) => write(name, agent, options, "create");
3270
+ const createAgent = (name, agent, options) => write$1(name, agent, options, "create");
3257
3271
  /** Deletes an agent from Ork's folder for the scope. */
3258
3272
  const deleteAgent = (name, options) => {
3259
3273
  const dir = agentFolder(options);
@@ -4824,6 +4838,359 @@ const FEEDBACK_SEVERITIES = [
4824
4838
  "medium",
4825
4839
  "high"
4826
4840
  ];
4841
+ // ../core/src/runtime/ends.ts
4842
+ /**
4843
+ * @file How a node ended, as one of a closed set of kinds (specs/runtime/
4844
+ * session-ends.md): the registry that gives each kind its label, Ork's own
4845
+ * message, who should act and what to do, and `endFor`, which builds an end
4846
+ * with its raw detail scrubbed and capped. ends-legacy.ts reads one from runs
4847
+ * stored before ends existed.
4848
+ */
4849
+ /** The most of a detail kept, in characters. */
4850
+ const END_DETAIL_LIMIT = 4096;
4851
+ const RETRY = "This may pass: `ork resume` tries again.";
4852
+ const usd$1 = (n) => `$${Number.isInteger(n) ? String(n) : n.toFixed(2)}`;
4853
+ const paren = (value) => value === void 0 ? "" : ` (${value})`;
4854
+ const colon = (reason) => reason === void 0 ? "" : `: ${reason}`;
4855
+ const info = (label, actor, message, hint, flags = {}) => ({
4856
+ label,
4857
+ message,
4858
+ actor,
4859
+ transient: flags.transient ?? false,
4860
+ stopsRun: flags.stopsRun ?? false,
4861
+ hint,
4862
+ docsAnchor: label.toLowerCase().replaceAll(" ", "-")
4863
+ });
4864
+ /** Each kind's label, message, actor, flags, hint and docs anchor. */
4865
+ const END_KINDS = {
4866
+ done: info("Done", null, () => "finished", ""),
4867
+ reported_failure: info("Reported failure", "agent", () => "reported that it failed", "Read its summary, fix what it names, and run it again."),
4868
+ no_report: info("No report", "agent", () => "ended without handing in a report", "Check that its brief says what to report; its session is in the Logs panel."),
4869
+ invalid_report: info("Invalid report", "agent", (f) => `handed in a report that breaks its declared output${colon(f.reason)}`, "Check that its brief asks for every field its `output.schema` declares."),
4870
+ turn_limit: info("Turn limit", "workflow", (f) => `reached its turn limit${paren(f.maxTurns === void 0 ? void 0 : String(f.maxTurns))}`, "Raise its `maxTurns`, or give it a smaller task."),
4871
+ budget_limit: info("Budget limit", "workflow", (f) => `reached its budget${paren(f.budgetUsd === void 0 ? void 0 : usd$1(f.budgetUsd))}`, "Raise `budgetUsd` on the node or `limits.budgetUsd` on the workflow, then `ork resume`."),
4872
+ timed_out: info("Timed out", "workflow", (f) => `reached its timeout${paren(f.timeout)}`, "Raise its `timeout`, or give it a smaller task."),
4873
+ context_too_long: info("Context too long", "workflow", () => "outgrew its model's context", "Give it a smaller task or less context, or a model with a larger context."),
4874
+ plan_limit: info("Plan limit", "account", (f) => `ran into a usage limit${paren(f.window?.replaceAll("_", " "))}`, `Wait until the limit resets. ${RETRY}`, {
4875
+ transient: true,
4876
+ stopsRun: true
4877
+ }),
4878
+ rate_limited: info("Rate limited", "provider", () => "was asked by its provider to slow down", RETRY, { transient: true }),
4879
+ provider_unavailable: info("Provider unavailable", "provider", () => "could not reach its provider, or the provider was overloaded", RETRY, { transient: true }),
4880
+ auth: info("Not signed in", "account", () => "could not sign in to its provider", "Sign in to the driver's tool again (`ork doctor` says how), then `ork resume`."),
4881
+ out_of_credits: info("Out of credits", "account", () => "found no credit left on its account", "Add credit to the account, or use another, then `ork resume`."),
4882
+ model_unavailable: info("Model unavailable", "account", () => "asked for something its model or provider can't serve", "Use another model or driver for the node."),
4883
+ interrupted: info("Interrupted", "user", () => "was stopped", "`ork resume` goes on from where it stopped."),
4884
+ tool_missing: info("Tool missing", "account", (f) => `could not start ${f.program ?? "its program"}: it isn't installed`, "Install it (`ork doctor` checks the drivers), then `ork resume`."),
4885
+ command_failed: info("Command failed", "workflow", (f) => `${f.program ?? "its command"} exited with ${f.exitCode === void 0 ? "an error" : String(f.exitCode)}`, "Its output is under Details."),
4886
+ invalid_output: info("Invalid output", "workflow", (f) => `gave JSON that breaks its declared output${colon(f.reason)}`, "Make the command print what its `output.schema` declares."),
4887
+ invalid_node: info("Invalid node", "workflow", (f) => `could not start${colon(f.reason)}`, "Fix the node in the workflow file (`ork validate` checks it)."),
4888
+ driver_crashed: info("Driver crashed", "ork", () => "lost its driver in a way Ork can't read", "Its last output is under Details; Ork files it as a bug."),
4889
+ unknown: info("Unknown", "ork", () => "ended for a reason Ork doesn't recognize", "Its detail is under Details; Ork files it as a bug, so a rule can be added.")
4890
+ };
4891
+ /**
4892
+ * An end of `kind`, its message from `facts`. The detail is scrubbed (with the
4893
+ * feedback redactor when `scrub` is given, secrets masked always) and capped.
4894
+ */
4895
+ const endFor = (kind, facts = {}, detail, scrub) => {
4896
+ const end = {
4897
+ kind,
4898
+ message: END_KINDS[kind].message(facts)
4899
+ };
4900
+ if (detail === void 0 || detail.trim() === "") return end;
4901
+ const clean = scrub === void 0 ? maskSecrets(detail) : redact(detail, scrub);
4902
+ end.detail = clean.length > 4096 ? `${clean.slice(0, 4095)}…` : clean;
4903
+ return end;
4904
+ };
4905
+ // ../core/src/runtime/ends-legacy.ts
4906
+ /**
4907
+ * @file The legacy readers of how a node ended: a run stored before ends
4908
+ * existed gets a kind from the text it kept (specs/runtime/session-ends.md),
4909
+ * and the stored event is never rewritten.
4910
+ */
4911
+ /**
4912
+ * Text Ork stored before ends existed, to a kind, in the plan's precedence:
4913
+ * interrupt, plan limit, budget, timeout, turn limit, provider error, missing
4914
+ * tool, failed command, crash. Frozen: old runs' text never changes, so this is
4915
+ * not the live provider classifier.
4916
+ */
4917
+ const LEGACY_TEXT = [
4918
+ ["interrupted", /\bstopped by the user\b|\bthe node was stopped\b|\bstopped while watching\b|\bwas interrupted\b|\bended interrupted\b|\bthe gate was withdrawn\b/u],
4919
+ ["plan_limit", /\bClaude plan's .* limit was reached\b|\bquota of \$[\d.]+ is spent\b|\bended quota_exhausted\b/u],
4920
+ ["budget_limit", /\(error_max_budget_usd\)|\bmaximum budget\b|\bnothing is left of the run's budget\b|\bbudget of \$[\d.]+ is spent\b|\bended budget_exhausted\b/u],
4921
+ ["timed_out", /\btimed out after\b|\bno one answered the gate\b|\btimeout of \S+ passed\b|\bended timed_out\b/u],
4922
+ ["turn_limit", /\(error_max_turns\)|\bmaximum number of turns\b/iu],
4923
+ ["context_too_long", /\bprompt is too long\b|\bcontext[ _](?:length|window)\b/iu],
4924
+ ["rate_limited", /\brate_limit_error\b|\brate.?limit(?:ed)?\b|\b(?:status|HTTP)\W{0,3}429\b/iu],
4925
+ ["out_of_credits", /\bcredit balance\b|\binsufficient[ _](?:credit|funds|quota)\b/iu],
4926
+ ["auth", /\bauthentication_error\b|\binvalid (?:x-)?api.?key\b|\bnot logged in\b|\/login\b|\b(?:status|HTTP)\W{0,3}401\b/iu],
4927
+ ["provider_unavailable", /\boverloaded(?:_error)?\b|\b(?:status|HTTP)\W{0,3}5\d\d\b|\bapi_error\b|\bECONNRESET\b|\bETIMEDOUT\b|\bENOTFOUND\b|\bfetch failed\b/u],
4928
+ ["model_unavailable", /\bassistant message prefill\b|\bInvalid `signature` in `thinking` block\b|\bnot_found_error\b|\binvalid_request_error\b/u],
4929
+ ["tool_missing", /\bENOENT\b|\bcommand not found\b|\bno agent driver is ready\b/u],
4930
+ ["invalid_output", /\$ORK_OUTPUT\b|\bthe JSON result is\b/u],
4931
+ ["command_failed", /\bexited with \d+|^[\w.-]+ [\w-]+ failed: /u],
4932
+ ["driver_crashed", /\bthe session ended without a result\b/u]
4933
+ ];
4934
+ const textKind = (text) => LEGACY_TEXT.find(([, pattern]) => pattern.test(text))?.[0] ?? null;
4935
+ /** What `fieldProblems` writes (run/declared.ts). */
4936
+ const DECLARED_PROBLEM = /\bis missing, expected\b|\bis .+, expected\b|^the result isn't a JSON object$/u;
4937
+ const WITHOUT_REPORT = /^ended without a report \(([\w-]+)\): ([\s\S]*)$/u;
4938
+ /** A driver's subtype, read with the session's error text. */
4939
+ const subtypeKind = (subtype, text) => {
4940
+ if (subtype === "error_max_turns") return "turn_limit";
4941
+ if (subtype === "error_max_budget_usd") return "budget_limit";
4942
+ if (subtype === "error_max_structured_output_retries") return "invalid_report";
4943
+ const said = textKind(text);
4944
+ if (said !== null) return said;
4945
+ if (subtype === "error_driver") return "driver_crashed";
4946
+ return text === "no valid report" || subtype === "success" ? "no_report" : "unknown";
4947
+ };
4948
+ /**
4949
+ * How a finished node ended: its own `end`, or, for an event stored before ends
4950
+ * existed, a kind read from its `error` text (`unknown` when nothing matches),
4951
+ * with that text as the detail.
4952
+ */
4953
+ const endOf = (finished) => {
4954
+ if (finished.end !== void 0) return finished.end;
4955
+ if (finished.ok) return endFor("done");
4956
+ const error = finished.error ?? "";
4957
+ return endFor(legacyKind(error, finished.output), {}, error);
4958
+ };
4959
+ const legacyKind = (error, output) => {
4960
+ if ("artifacts" in output) return DECLARED_PROBLEM.test(error) ? "invalid_report" : "reported_failure";
4961
+ const without = WITHOUT_REPORT.exec(error);
4962
+ if (without !== null) return subtypeKind(without[1] ?? "", without[2] ?? "");
4963
+ return textKind(error) ?? "unknown";
4964
+ };
4965
+ /**
4966
+ * How a session ended: its `end`, or, for an outcome stored before ends existed
4967
+ * (or a driver that does not set one yet), one read from its subtype, report
4968
+ * and errors.
4969
+ */
4970
+ const outcomeEnd = (outcome) => {
4971
+ if (outcome.end !== void 0) return outcome.end;
4972
+ const { report } = outcome;
4973
+ if (report !== null && outcome.subtype === "success") return endFor(report.status === "fail" ? "reported_failure" : "done");
4974
+ const text = outcome.errors[0] ?? (report === null ? "no valid report" : "");
4975
+ return endFor(subtypeKind(outcome.subtype, text), {}, outcome.errors.join("\n"));
4976
+ };
4977
+ // ../core/src/feedback/end-signals.ts
4978
+ /**
4979
+ * @file The feedback a node's end files (specs/feedback.md,
4980
+ * specs/runtime/session-ends.md), by who should act on it: Ork's own bug, the
4981
+ * workflow's, or the agent's context. An account's, a provider's or the
4982
+ * user's end is a setup hint on the screen, not feedback.
4983
+ */
4984
+ /**
4985
+ * Ends that are a node's work saying no, not a problem to act on: an agent that
4986
+ * reports `fail`, a command that exits non-zero. Neither is filed.
4987
+ */
4988
+ const ANSWERS = /* @__PURE__ */ new Set(["reported_failure", "command_failed"]);
4989
+ /**
4990
+ * The feedback a node's end files, by who should act on it: Ork's own bug (with
4991
+ * the scrubbed detail, so a rule can be added), the workflow's, or the agent's
4992
+ * context. An account's, a provider's or the user's is not feedback.
4993
+ */
4994
+ const endSignal = (node, end) => {
4995
+ const info = END_KINDS[end.kind];
4996
+ if (ANSWERS.has(end.kind)) return null;
4997
+ const base = {
4998
+ code: `end.${end.kind}`,
4999
+ title: `${node} ${end.message}`,
5000
+ body: info.hint,
5001
+ ...end.detail === void 0 ? {} : { evidence: [end.detail] }
5002
+ };
5003
+ if (info.actor === "ork") return {
5004
+ ...base,
5005
+ kind: "bug",
5006
+ target: "ork",
5007
+ severity: "medium"
5008
+ };
5009
+ if (info.actor === "workflow") return {
5010
+ ...base,
5011
+ kind: "workflow",
5012
+ target: "workflow",
5013
+ severity: "low"
5014
+ };
5015
+ return info.actor === "agent" ? {
5016
+ ...base,
5017
+ kind: "context-gap",
5018
+ target: "workflow",
5019
+ severity: "low"
5020
+ } : null;
5021
+ };
5022
+ // ../core/src/feedback/signals.ts
5023
+ const ORK_TOOL = "mcp__ork__";
5024
+ /**
5025
+ * An observer for one run's events: give it each event, and record what it
5026
+ * returns, in order (the event, its origin filled in when it is feedback, then
5027
+ * any feedback it signals).
5028
+ */
5029
+ const createSignals = (options = {}) => {
5030
+ let workflow;
5031
+ const nodeTypes = /* @__PURE__ */ new Map();
5032
+ const sessions = /* @__PURE__ */ new Map();
5033
+ const tools = /* @__PURE__ */ new Map();
5034
+ const asked = /* @__PURE__ */ new Map();
5035
+ const seen = /* @__PURE__ */ new Set();
5036
+ const originOf = (node) => {
5037
+ const origin = {};
5038
+ if (workflow !== void 0) origin.workflow = workflow;
5039
+ if (options.workflowHash !== void 0) origin.workflowHash = options.workflowHash;
5040
+ const type = node === void 0 ? void 0 : nodeTypes.get(node);
5041
+ if (type !== void 0) origin.nodeType = type;
5042
+ const session = node === void 0 ? void 0 : sessions.get(node);
5043
+ if (session !== void 0) origin.model = session.model;
5044
+ if (session?.driver !== void 0) origin.driver = session.driver;
5045
+ if (options.version !== void 0) origin.version = options.version;
5046
+ return origin;
5047
+ };
5048
+ const record = (node, signal) => {
5049
+ const print = fingerprint(signal);
5050
+ if (seen.has(print)) return [];
5051
+ seen.add(print);
5052
+ return [{
5053
+ type: "feedback.recorded",
5054
+ ...node === void 0 ? {} : { node },
5055
+ feedback: {
5056
+ source: "system",
5057
+ kind: signal.kind,
5058
+ target: signal.target,
5059
+ severity: signal.severity,
5060
+ title: signal.title,
5061
+ body: signal.body,
5062
+ evidence: signal.evidence ?? [],
5063
+ code: signal.code,
5064
+ fingerprint: print,
5065
+ origin: originOf(node)
5066
+ }
5067
+ }];
5068
+ };
5069
+ const answered = (question, answer) => {
5070
+ if (question.kind === "permission") {
5071
+ if (answer !== "deny") return null;
5072
+ const tool = question.question.replace(/^Allow /u, "").replace(/\?$/u, "");
5073
+ return [question.node, {
5074
+ code: "permission.denied",
5075
+ kind: "friction",
5076
+ target: "workflow",
5077
+ severity: "low",
5078
+ title: `${question.node} was denied ${tool}`,
5079
+ body: "Allow it in the node's `tools.allow` if it needs it, or tell it in its prompt not to use it.",
5080
+ evidence: [tool]
5081
+ }];
5082
+ }
5083
+ if (question.recommended === void 0 || answer === question.recommended) return null;
5084
+ return [question.node, {
5085
+ code: "recommendation.overridden",
5086
+ kind: "context-gap",
5087
+ target: "workflow",
5088
+ severity: "low",
5089
+ title: `The answer to ${question.node}'s question was not the one it recommended`,
5090
+ body: `Asked: ${question.question} Recommended: ${question.recommended}. Answered: ${answer}.`
5091
+ }];
5092
+ };
5093
+ /** The feedback an event signals, if any, and the node it is about. */
5094
+ const signalOf = (event) => {
5095
+ if (event.type === "loop.exhausted") return [event.from, {
5096
+ code: "loop.exhausted",
5097
+ kind: "workflow",
5098
+ target: "workflow",
5099
+ severity: "medium",
5100
+ title: `The loop ${event.from} -> ${event.to} ran out of its ${String(event.maxRounds)} rounds`,
5101
+ body: "Raise `maxRounds`, or make what the loop's nodes are asked to fix clearer so it settles sooner."
5102
+ }];
5103
+ if (event.type === "run.finished" && event.result.status === "budget_exhausted") return [void 0, {
5104
+ code: "budget.exhausted",
5105
+ kind: "workflow",
5106
+ target: "workflow",
5107
+ severity: "medium",
5108
+ title: "The run's budget ran out",
5109
+ body: `The run stopped after spending $${event.result.costUsd.toFixed(4)}. Raise \`limits.budgetUsd\`, or give nodes cheaper models or smaller budgets.`
5110
+ }];
5111
+ if (event.type === "decision.answered") {
5112
+ const question = asked.get(event.decision);
5113
+ return question === void 0 ? null : answered(question, event.answer);
5114
+ }
5115
+ if (event.type === "node.event" && event.event.type === "tool.result") {
5116
+ const name = tools.get(`${event.id}\0${event.event.id}`);
5117
+ if (!event.event.isError || name?.startsWith(ORK_TOOL) !== true) return null;
5118
+ const tool = name.slice(10);
5119
+ if (tool === "report") return [event.id, {
5120
+ code: "report.rejected",
5121
+ kind: "context-gap",
5122
+ target: "workflow",
5123
+ severity: "low",
5124
+ title: `${event.id}'s report was not accepted`,
5125
+ body: END_KINDS.invalid_report.hint,
5126
+ evidence: [name]
5127
+ }];
5128
+ return [event.id, {
5129
+ code: "tool.error",
5130
+ kind: "bug",
5131
+ target: "ork",
5132
+ severity: "medium",
5133
+ title: `Ork's ${tool} tool returned an error`,
5134
+ body: `A call to \`${tool}\` failed; the node's events show the call.`,
5135
+ evidence: [name]
5136
+ }];
5137
+ }
5138
+ if (event.type === "node.finished" && nodeTypes.get(event.id) === "human.gate" && event.output["answeredBy"] === "timeout") return [event.id, {
5139
+ code: "gate.timeout",
5140
+ kind: "workflow",
5141
+ target: "workflow",
5142
+ severity: "low",
5143
+ title: `The gate ${event.id} timed out and took its default`,
5144
+ body: "No one answered in time. Lengthen its `timeout`, or keep it if the default was right."
5145
+ }];
5146
+ if (event.type === "node.finished" && !event.ok) {
5147
+ const signal = endSignal(event.id, endOf(event));
5148
+ return signal === null ? null : [event.id, signal];
5149
+ }
5150
+ return null;
5151
+ };
5152
+ /** Keeps what later events and origins need. */
5153
+ const observe = (event) => {
5154
+ if (event.type === "run.started") workflow = event.workflow;
5155
+ if (event.type === "node.started") nodeTypes.set(event.id, event.nodeType);
5156
+ if (event.type === "decision.opened") {
5157
+ const question = {
5158
+ node: event.node,
5159
+ kind: event.kind,
5160
+ question: event.question
5161
+ };
5162
+ if (event.recommended !== void 0) question.recommended = event.recommended;
5163
+ asked.set(event.decision, question);
5164
+ }
5165
+ if (event.type !== "node.event") return;
5166
+ const inner = event.event;
5167
+ if (inner.type === "session.started" && inner.fork !== true) sessions.set(event.id, {
5168
+ model: inner.model,
5169
+ ...inner.driver === void 0 ? {} : { driver: inner.driver }
5170
+ });
5171
+ if (inner.type === "tool.called") tools.set(`${event.id}\0${inner.id}`, inner.name);
5172
+ };
5173
+ return (event) => {
5174
+ observe(event);
5175
+ if (event.type === "feedback.recorded") {
5176
+ if (seen.has(event.feedback.fingerprint)) return [];
5177
+ seen.add(event.feedback.fingerprint);
5178
+ const origin = {
5179
+ ...originOf(event.node),
5180
+ ...event.feedback.origin
5181
+ };
5182
+ return [{
5183
+ ...event,
5184
+ feedback: {
5185
+ ...event.feedback,
5186
+ origin
5187
+ }
5188
+ }];
5189
+ }
5190
+ const signal = signalOf(event);
5191
+ return signal === null ? [event] : [event, ...record(...signal)];
5192
+ };
5193
+ };
4827
5194
  // ../core/src/project/mcp-file.ts
4828
5195
  /**
4829
5196
  * @file The project's `.mcp.json`, the file Claude Code reads too, so a server
@@ -4937,6 +5304,48 @@ const checkMcpServers = (workflow, options) => {
4937
5304
  });
4938
5305
  });
4939
5306
  };
5307
+ // ../core/src/project/heavy.ts
5308
+ /**
5309
+ * @file A project's rules for the machine its runs share: the heavy commands
5310
+ * (its full-suite checks), denied to the session of a node with `verify:`
5311
+ * since Ork runs that check, and the OS priority, each with Ork's default.
5312
+ */
5313
+ /** A `heavy` pattern as the Bash rule that denies it to a session. */
5314
+ const heavyRule = (pattern) => `Bash(${pattern})`;
5315
+ /**
5316
+ * The nodes, with the heavy commands denied to each Claude node that has
5317
+ * `verify:`; other nodes are left as they are. A pattern without `*` matches
5318
+ * only that command, so `vitest run` leaves `vitest run <files>` allowed.
5319
+ */
5320
+ const withoutHeavy = (nodes, heavy) => {
5321
+ if (heavy.length === 0) return nodes;
5322
+ const rules = heavy.map((pattern) => heavyRule(pattern));
5323
+ return Object.fromEntries(Object.entries(nodes).map(([id, node]) => isClaudeNode(node) && "verify" in node && node.verify !== void 0 ? [id, {
5324
+ ...node,
5325
+ tools: {
5326
+ ...node.tools,
5327
+ deny: [.../* @__PURE__ */ new Set([...node.tools?.deny ?? [], ...rules])]
5328
+ }
5329
+ }] : [id, node]));
5330
+ };
5331
+ const words = (text) => text.trim().replaceAll(/\s+/gu, " ");
5332
+ /**
5333
+ * Whether `command` matches a `heavy` pattern, in the permission patterns'
5334
+ * syntax: `*` is any text, a trailing ` *` (or `:*`) also matches nothing, and
5335
+ * a pattern without `*` matches only that command.
5336
+ */
5337
+ const matchesPattern = (pattern, command) => {
5338
+ const tail = words(pattern).replace(/:\*$/u, " *");
5339
+ const open = tail.endsWith(" *");
5340
+ const body = (open ? tail.slice(0, -2) : tail).split("*").map((part) => part.replaceAll(/[.+?^${}()|[\]\\]/gu, "\\$&")).join(".*");
5341
+ return new RegExp(`^${body}${open ? "(?: .*)?" : ""}$`, "su").test(words(command));
5342
+ };
5343
+ /** Whether `command` is one of the project's heavy commands. */
5344
+ const isHeavy = (heavy, command) => heavy.some((pattern) => matchesPattern(pattern, command));
5345
+ const machineRulesOf = (project) => ({
5346
+ heavy: project?.heavy ?? [],
5347
+ priority: project?.priority ?? "low"
5348
+ });
4940
5349
  // ../core/src/project/templates.ts
4941
5350
  /**
4942
5351
  * @file Git name templates (`ork/{run}/{node}`, `{inputs.type}/{inputs.slug}`):
@@ -5028,7 +5437,8 @@ const dropUndefined = (value) => Object.fromEntries(Object.entries(value).filter
5028
5437
  /**
5029
5438
  * The workflow with its project's rules under it: git rules key by key and
5030
5439
  * defaults key by key, practices one by one (the workflow's win), permissions
5031
- * added together, and the project's context files before the workflow's.
5440
+ * added together, the project's context files before the workflow's, and its
5441
+ * heavy commands denied to nodes with `verify:`.
5032
5442
  */
5033
5443
  const applyProject = (workflow, options) => {
5034
5444
  const { project } = options;
@@ -5062,6 +5472,7 @@ const applyProject = (workflow, options) => {
5062
5472
  });
5063
5473
  return dropUndefined({
5064
5474
  ...workflow,
5475
+ nodes: withoutHeavy(workflow.nodes, project.heavy ?? []),
5065
5476
  defaults: Object.keys(defaults).length === 0 ? void 0 : defaults,
5066
5477
  rules: Object.keys(rules).length === 0 ? void 0 : rules,
5067
5478
  layers: Object.keys(layers).length === 0 ? void 0 : layers
@@ -6046,6 +6457,122 @@ const gatherProblems = (id, node) => {
6046
6457
  return isList(rule) ? [] : [error("output.gather", `\`${field}\` isn't declared as a list (\`{ type: array }\` or \`{ list: … }\`), so its rounds can't be added together`, [...path, index])];
6047
6458
  });
6048
6459
  };
6460
+ // ../core/src/graph/item-settings.ts
6461
+ /**
6462
+ * @file A fanned node's `model` and `effort` from its item (`{{item.model}}`):
6463
+ * the compiler's checks (only on a `forEach` node, reading only `item`, and
6464
+ * every written item's value), and the rendering each item's session plan
6465
+ * uses, which checks an item from `from:` when it runs.
6466
+ */
6467
+ const FIELDS$1 = ["model", "effort"];
6468
+ /**
6469
+ * The parts of a template that reads something, or `null` for a plain value (or
6470
+ * one that does not parse, which the compiler reports).
6471
+ */
6472
+ const templateOf$1 = (text) => {
6473
+ const parsed = parseTemplate(text);
6474
+ if (!parsed.ok) return null;
6475
+ return parsed.parts.some((part) => typeof part !== "string") ? parsed.parts : null;
6476
+ };
6477
+ const isEffort = (text) => EFFORTS.includes(text);
6478
+ /** Checks what one item's template rendered to. */
6479
+ const checkRendered = (field, template, rendered) => {
6480
+ const text = rendered.trim();
6481
+ if (text === "") return {
6482
+ ok: true,
6483
+ value: void 0
6484
+ };
6485
+ return (field === "effort" ? isEffort(text) : /^[^\s{}"]+$/u.test(text)) ? {
6486
+ ok: true,
6487
+ value: text
6488
+ } : {
6489
+ ok: false,
6490
+ error: `\`${field}: ${template}\` gave \`${text}\`, which is not ${field === "effort" ? `an effort level (${EFFORTS.join(", ")})` : "a model name"}`
6491
+ };
6492
+ };
6493
+ /**
6494
+ * A node's `model` or `effort` for one item: a plain value as written, a
6495
+ * template rendered with the item. Without an item, a template renders empty.
6496
+ */
6497
+ const itemSetting = (field, value, scope) => {
6498
+ if (value === void 0) return {
6499
+ ok: true,
6500
+ value: void 0
6501
+ };
6502
+ const parts = templateOf$1(value);
6503
+ if (parts === null) return {
6504
+ ok: true,
6505
+ value
6506
+ };
6507
+ return checkRendered(field, value, renderTemplate(parts, scope));
6508
+ };
6509
+ /** `itemSetting` for `effort`, as an effort level. */
6510
+ const itemEffort = (value, scope) => {
6511
+ const given = itemSetting("effort", value, scope);
6512
+ if (!given.ok) return given;
6513
+ if (given.value === void 0) return {
6514
+ ok: true,
6515
+ value: void 0
6516
+ };
6517
+ return isEffort(given.value) ? {
6518
+ ok: true,
6519
+ value: given.value
6520
+ } : {
6521
+ ok: false,
6522
+ error: `\`effort\` is \`${given.value}\``
6523
+ };
6524
+ };
6525
+ /** A `model` or `effort` template's problems on one node. */
6526
+ const checkNodeField = (workflow, says, id, field) => {
6527
+ const node = workflow.nodes[id];
6528
+ const value = node === void 0 ? void 0 : Reflect.get(node, field);
6529
+ if (node === void 0 || typeof value !== "string") return [];
6530
+ const path = [
6531
+ "nodes",
6532
+ id,
6533
+ field
6534
+ ];
6535
+ const parsed = parseTemplate(value);
6536
+ if (!parsed.ok) return [error("template.syntax", `${parsed.message} at column ${parsed.offset + 1}`, path)];
6537
+ if (parsed.parts.every((part) => typeof part === "string")) return [];
6538
+ const each = forEachOf$1(node);
6539
+ if (each === void 0) return [error(`${field}.template`, `\`${field}\` may be a template only on a node with \`forEach\`, reading its item, e.g. \`{{item.${field}}}\``, path)];
6540
+ const reads = parsed.parts.flatMap((part) => typeof part === "string" ? [] : collectPaths(part.expr));
6541
+ const outside = reads.filter(([root]) => root !== "item");
6542
+ if (outside.length > 0) return outside.map((segments) => error(`${field}.template`, `\`${describePath(segments)}\`: a \`${field}\` template reads only the item, e.g. \`{{item.${field}}}\``, path));
6543
+ const scope = { fields: itemFieldsOf(node, says) };
6544
+ const fields = reads.flatMap((segments) => checkItemRead(segments, scope, path));
6545
+ if (fields.length > 0 || !("items" in each)) return fields;
6546
+ return each.items.flatMap((item, index) => {
6547
+ const given = checkRendered(field, value, renderTemplate(parsed.parts, {
6548
+ inputs: {},
6549
+ nodes: {},
6550
+ item,
6551
+ index
6552
+ }));
6553
+ return given.ok ? [] : [error(`${field}.item`, `item ${String(index)}: ${given.error}`, [
6554
+ "nodes",
6555
+ id,
6556
+ "forEach",
6557
+ "items",
6558
+ index
6559
+ ])];
6560
+ });
6561
+ };
6562
+ /**
6563
+ * `model` and `effort` templates: only on a `forEach` node, reading only
6564
+ * `item`, and every written item's value a model name or an effort level.
6565
+ * `defaults:` belong to no node, so they take no template.
6566
+ */
6567
+ const checkItemSettings = (workflow, edges) => {
6568
+ const says = nodeSays(workflow.nodes, edges);
6569
+ const defaults = FIELDS$1.flatMap((field) => {
6570
+ const value = workflow.defaults?.[field];
6571
+ return value !== void 0 && templateOf$1(value) !== null ? [error(`${field}.template`, `\`defaults.${field}\` applies to every node, so it can't read an item; set the template on the \`forEach\` node`, ["defaults", field])] : [];
6572
+ });
6573
+ const nodes = Object.entries(workflow.nodes).flatMap(([id, node]) => isClaudeNode(node) ? FIELDS$1.flatMap((field) => checkNodeField(workflow, says, id, field)) : []);
6574
+ return [...defaults, ...nodes];
6575
+ };
6049
6576
  // ../core/src/graph/script.ts
6050
6577
  /**
6051
6578
  * @file Checks on the programs a workflow declares (RFC-006): a tool node's and
@@ -6111,7 +6638,7 @@ const statusProblems = (id, node) => {
6111
6638
  };
6112
6639
  /** Per-type checks that need the rest of the graph. */
6113
6640
  const checkNodes = (workflow, edges) => {
6114
- const diagnostics = checkScripts(workflow);
6641
+ const diagnostics = [...checkScripts(workflow), ...checkItemSettings(workflow, edges)];
6115
6642
  for (const [id, node] of Object.entries(workflow.nodes)) {
6116
6643
  const path = ["nodes", id];
6117
6644
  const incoming = edges.filter((e) => e.to === id);
@@ -6646,6 +7173,140 @@ const compileWorkflow = (authored, options = {}) => {
6646
7173
  diagnostics
6647
7174
  };
6648
7175
  };
7176
+ // ../core/src/machine/priority.ts
7177
+ /**
7178
+ * @file Starting a child at the project's priority (`priority:`): `low`, the
7179
+ * default, starts it below Ork's own (under `nice`; on macOS also utility QoS
7180
+ * through `taskpolicy`), so the user's own apps come first; `normal` leaves
7181
+ * it. Only children: the `ork` process itself stays at normal priority.
7182
+ */
7183
+ /** MacOS's tool for a program's QoS. */
7184
+ const TASKPOLICY = "/usr/sbin/taskpolicy";
7185
+ /** The POSIX tool that starts a program at a lower priority. */
7186
+ const NICE = "/usr/bin/nice";
7187
+ /** How far `nice` lowers a `low` child: the same as `PRIORITY_BELOW_NORMAL`. */
7188
+ const NICE_STEP = String(constants$1.priority.PRIORITY_BELOW_NORMAL);
7189
+ /**
7190
+ * Whether `bin` is a program `spawn` would find, so a wrapper can't hide its
7191
+ * absence.
7192
+ */
7193
+ const found = (bin, env) => bin.includes("/") ? existsSync(bin) : (env["PATH"] ?? "").split(path.delimiter).some((dir) => dir !== "" && existsSync(path.join(dir, bin)));
7194
+ /**
7195
+ * What to spawn for `bin args` at `priority`. For `low`, when the program is
7196
+ * there: under `nice`, so it and what it starts run lower from the first
7197
+ * instruction, and on macOS under utility QoS as well; else the program itself,
7198
+ * which {@link lowerChild} lowers once it has started.
7199
+ */
7200
+ const startAt = (priority, bin, args, env = process.env) => {
7201
+ if (priority !== "low" || !found(bin, env)) return [bin, [...args]];
7202
+ const niced = existsSync(NICE) ? [
7203
+ NICE,
7204
+ "-n",
7205
+ NICE_STEP,
7206
+ bin
7207
+ ] : [bin];
7208
+ const [first = bin, ...rest] = process.platform === "darwin" && existsSync(TASKPOLICY) ? [
7209
+ TASKPOLICY,
7210
+ "-c",
7211
+ "utility",
7212
+ ...niced
7213
+ ] : niced;
7214
+ return [first, [...rest, ...args]];
7215
+ };
7216
+ /**
7217
+ * Lowers a started child's priority for `low` where {@link startAt} had no
7218
+ * `nice` to start it under (Windows), best effort; what it starts later
7219
+ * inherits it. Under `nice` it does nothing: `nice` lowers relative to the
7220
+ * priority it finds, so lowering here too could land first and the two add up.
7221
+ */
7222
+ const lowerChild = (priority, pid) => {
7223
+ if (priority !== "low" || pid === void 0 || existsSync(NICE)) return;
7224
+ try {
7225
+ setPriority(pid, constants$1.priority.PRIORITY_BELOW_NORMAL);
7226
+ } catch {}
7227
+ };
7228
+ // ../core/src/run/machine.ts
7229
+ /**
7230
+ * @file A node's share of the machine (the `machine` of its context): the
7231
+ * project's priority for its commands, and a machine-wide slot around each
7232
+ * heavy one. A wait is never silent: `slot.waiting` names who holds the
7233
+ * slots, `slot.taken` ends it, and the node's clock (its owners' and the
7234
+ * run's too) stands still meanwhile, so a timeout counts from the slot.
7235
+ */
7236
+ /** A wait for a slot, as `ork logs`, `ork tree` and the UI say it. */
7237
+ const slotWaitLine = (holders) => holders.length === 0 ? "waiting for a check slot" : `waiting for a check slot, held by ${holders.join(", ")}`;
7238
+ const none = () => {};
7239
+ /** The port for one activation; none for a run that shares nothing. */
7240
+ const machinePort = (options, runId, id, clock) => {
7241
+ const { machine } = options;
7242
+ if (machine === void 0) return void 0;
7243
+ const { rules, ledger } = machine;
7244
+ return {
7245
+ priority: rules.priority,
7246
+ slot: async (command, signal) => {
7247
+ if (ledger === void 0 || !isHeavy(rules.heavy, command)) return none;
7248
+ let waited = false;
7249
+ const release = await clock.waitFor(id, () => ledger.take({
7250
+ run: runId,
7251
+ node: id,
7252
+ pid: process.pid
7253
+ }, {
7254
+ signal,
7255
+ onWait: (holders) => {
7256
+ waited = true;
7257
+ options.onEvent({
7258
+ type: "slot.waiting",
7259
+ id,
7260
+ holders: holders.map((held) => `${held.run}/${held.node}`)
7261
+ });
7262
+ }
7263
+ }));
7264
+ if (release === null) return none;
7265
+ if (waited) options.onEvent({
7266
+ type: "slot.taken",
7267
+ id
7268
+ });
7269
+ return release;
7270
+ }
7271
+ };
7272
+ };
7273
+ /**
7274
+ * A step's clock that also stands still while its command waits for a slot, and
7275
+ * the machine port that tells it so; for a step's own timeout.
7276
+ */
7277
+ const slotClock = (base, machine) => {
7278
+ let waiting = 0;
7279
+ const listeners = /* @__PURE__ */ new Set();
7280
+ const tell = () => {
7281
+ for (const listener of listeners) listener();
7282
+ };
7283
+ return {
7284
+ clock: {
7285
+ held: () => waiting > 0 || base?.held() === true,
7286
+ watch: (listener) => {
7287
+ listeners.add(listener);
7288
+ const unwatch = base?.watch(listener);
7289
+ return () => {
7290
+ listeners.delete(listener);
7291
+ unwatch?.();
7292
+ };
7293
+ }
7294
+ },
7295
+ machine: machine === void 0 ? void 0 : {
7296
+ ...machine,
7297
+ slot: async (command, signal) => {
7298
+ waiting += 1;
7299
+ tell();
7300
+ try {
7301
+ return await machine.slot(command, signal);
7302
+ } finally {
7303
+ waiting -= 1;
7304
+ tell();
7305
+ }
7306
+ }
7307
+ }
7308
+ };
7309
+ };
6649
7310
  // ../core/src/project/calls.ts
6650
7311
  /**
6651
7312
  * @file `ork validate`'s check of `workflow.call` nodes against what they call
@@ -7055,6 +7716,66 @@ const createLoops = (options) => {
7055
7716
  overList
7056
7717
  };
7057
7718
  };
7719
+ // ../core/src/run/ended.ts
7720
+ /**
7721
+ * @file A node's result as it ended (specs/runtime/session-ends.md): one place
7722
+ * that sets `end`, makes a failure's `error` its message and marks a
7723
+ * transient kind, and that the engine's modules build ends through.
7724
+ */
7725
+ /**
7726
+ * The result as it ended: a failure's `error` is the end's message (what the
7727
+ * run summary lists), and a transient kind marks it `transient`.
7728
+ */
7729
+ const endedWith = (result, end) => ({
7730
+ ...result,
7731
+ end,
7732
+ ...result.ok ? {} : { error: end.message },
7733
+ ...END_KINDS[end.kind].transient ? { transient: true } : {}
7734
+ });
7735
+ /** The reason a run's signal is aborted with when `timeout` passed. */
7736
+ const runTimedOut = (timeout) => timeout === void 0 ? { kind: "timed_out" } : {
7737
+ kind: "timed_out",
7738
+ timeout
7739
+ };
7740
+ const isTimedOut = (reason) => typeof reason === "object" && reason !== null && "kind" in reason && reason.kind === "timed_out";
7741
+ /**
7742
+ * The end of a node its run's signal stopped: `timed_out` when the run's
7743
+ * timeout aborted it, `interrupted` otherwise.
7744
+ */
7745
+ const stoppedEnd = (signal, detail) => {
7746
+ const reason = signal.reason;
7747
+ if (!isTimedOut(reason)) return endFor("interrupted", {}, detail);
7748
+ const facts = reason.timeout === void 0 ? {} : { timeout: reason.timeout };
7749
+ return endFor("timed_out", facts, detail);
7750
+ };
7751
+ /** A failure that ended as `end`, its message the output's summary. */
7752
+ const failedWith = (end, output) => endedWith({
7753
+ ok: false,
7754
+ output: output ?? {
7755
+ status: "fail",
7756
+ summary: end.message
7757
+ }
7758
+ }, end);
7759
+ /**
7760
+ * The result with its end's detail scrubbed with the feedback redactor (the
7761
+ * run's `roots` and the home folder), as a session's is
7762
+ * (specs/runtime/session-ends.md), whichever node type set it.
7763
+ */
7764
+ const scrubbedEnd = (result, roots) => {
7765
+ const { end } = result;
7766
+ if (end?.detail === void 0) return result;
7767
+ const detail = redact(end.detail, {
7768
+ roots,
7769
+ home: homedir()
7770
+ });
7771
+ return {
7772
+ ...result,
7773
+ end: {
7774
+ ...end,
7775
+ detail
7776
+ }
7777
+ };
7778
+ };
7058
7779
  // ../core/src/run/declared.ts
7059
7780
  /**
7060
7781
  * @file Holding a node to what it declares of its output (RFC-010): a missing
@@ -7127,903 +7848,923 @@ const declaredProblem = (declared, value) => {
7127
7848
  * still masks to the record is the file's, with its real values.
7128
7849
  */
7129
7850
  const unmasked = (forEach, recorded) => "items" in forEach && JSON.stringify(forEach.items, maskReplacer) === JSON.stringify(recorded) ? [...forEach.items] : recorded;
7130
- // ../core/src/run/contacts.ts
7131
- const SESSION_TYPES$5 = /* @__PURE__ */ new Set(["claude.leader", "claude.worker"]);
7132
- const isSession$1 = (node) => SESSION_TYPES$5.has(node?.type ?? "");
7133
- const isTool = (node) => node?.type.startsWith("tool.") ?? false;
7134
- const channelPeersOf = (id, channels) => channels.flatMap(([a, b]) => {
7135
- if (a === id) return [b];
7136
- return b === id ? [a] : [];
7137
- });
7138
- /** Every declared node, with its owner, as a run records them. */
7139
- const declaredNodes = (graph) => [...graph.nodes.values()].map(({ id, node }) => node.parent === void 0 ? {
7140
- id,
7141
- type: node.type
7142
- } : {
7143
- id,
7144
- type: node.type,
7145
- parent: node.parent
7146
- });
7147
- /** Whom a session may write to, by how it knows them. */
7148
- const contactsOf = (id, nodes, channels) => {
7149
- const byId = new Map(nodes.map((node) => [node.id, node]));
7851
+ // ../core/src/run/slots.ts
7852
+ /**
7853
+ * @file Session slots: at most `limits.concurrency` Claude sessions run at
7854
+ * once. Queued activations take a free slot when the scheduler pumps;
7855
+ * delegated children and leaders returning from `delegate` wait for one and
7856
+ * are served first, so a leader waiting on its children never holds a slot
7857
+ * they need.
7858
+ */
7859
+ /** Only a Claude session takes a slot; tools and gates are not limited. */
7860
+ const needsSlot = (node) => isClaudeNode(node);
7861
+ /** A call spends through its run, so it needs budget as a session does. */
7862
+ const spendsBudget = (node) => needsSlot(node) || node.type === "workflow.call";
7863
+ const createSlots = (limit, onFree) => {
7864
+ let used = 0;
7865
+ const waiting = [];
7150
7866
  return {
7151
- owner: byId.get(id)?.parent,
7152
- children: nodes.filter((node) => node.parent === id && isSession$1(node)).map((node) => node.id),
7153
- channels: channelPeersOf(id, channels).filter((peer) => !isTool(byId.get(peer)))
7867
+ limit,
7868
+ tryTake: () => {
7869
+ if (used >= limit || waiting.length > 0) return false;
7870
+ used += 1;
7871
+ return true;
7872
+ },
7873
+ take: () => {
7874
+ if (used < limit && waiting.length === 0) {
7875
+ used += 1;
7876
+ return Promise.resolve();
7877
+ }
7878
+ const { promise, resolve } = Promise.withResolvers();
7879
+ waiting.push(resolve);
7880
+ return promise;
7881
+ },
7882
+ release: () => {
7883
+ const next = waiting.shift();
7884
+ if (next === void 0) {
7885
+ used -= 1;
7886
+ onFree();
7887
+ } else next();
7888
+ }
7154
7889
  };
7155
7890
  };
7156
- /** Whom a program in the tool node `id` may write to. */
7157
- const scriptContactsOf = (id, nodes, channels) => {
7158
- const byId = new Map(nodes.map((node) => [node.id, node]));
7159
- const owner = byId.get(id)?.parent;
7160
- return [.../* @__PURE__ */ new Set([...owner !== void 0 && isSession$1(byId.get(owner)) ? [owner] : [], ...channelPeersOf(id, channels).filter((peer) => isSession$1(byId.get(peer)))])];
7891
+ // ../core/src/run/fan-out.ts
7892
+ /** The node's `forEach`, whatever its type; `undefined` when it has none. */
7893
+ const forEachOf = (node) => node !== void 0 && "forEach" in node ? node.forEach : void 0;
7894
+ /** The list a node runs over, or why there isn't one. */
7895
+ const listOf = (forEach, scope) => {
7896
+ if ("items" in forEach) return {
7897
+ ok: true,
7898
+ list: forEach.items
7899
+ };
7900
+ const read = templateValue(forEach.from, scope);
7901
+ if (!read.ok) return read;
7902
+ const { value } = read;
7903
+ if (Array.isArray(value)) return {
7904
+ ok: true,
7905
+ list: value
7906
+ };
7907
+ const name = String(templatePath(forEach.from)?.at(-1) ?? forEach.from);
7908
+ return {
7909
+ ok: false,
7910
+ error: declaredProblem({ [name]: { list: {} } }, { [name]: value }) ?? `${name} is not a list`
7911
+ };
7161
7912
  };
7162
- /** Whether a program in a tool node may write to the session `id`. */
7163
- const hearsFromScripts = (id, nodes, channels) => nodes.some((node) => isTool(node) && scriptContactsOf(node.id, nodes, channels).includes(id));
7164
- // ../core/src/usage/spend.ts
7165
- /** What a report would add to the session's earlier ones, counting nothing. */
7166
- const unreported = (reported, key, total) => {
7167
- if (total <= 0) return 0;
7168
- const before = reported.get(key) ?? 0;
7169
- return total < before ? total : total - before;
7913
+ /** The output a `from` list comes from, whose freshness resume asks about. */
7914
+ const sourceOf = (forEach, scope) => {
7915
+ if (!("from" in forEach)) return void 0;
7916
+ const [root, id] = templatePath(forEach.from) ?? [];
7917
+ return root === "nodes" ? scope.nodes[String(id)]?.output : void 0;
7170
7918
  };
7919
+ const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
7171
7920
  /**
7172
- * What a report adds to the session's earlier ones, keeping the total in
7173
- * `reported`. A zero says nothing, so it is ignored; a lower total is a session
7174
- * that started counting again, so all of it is new.
7921
+ * What `results` keeps of an item: its declared fields (a program's are under
7922
+ * `json`), its summary, Ork's own branch and worktree, and how a program
7923
+ * failed. The full output stays in the item's own `node.finished`.
7175
7924
  */
7176
- const ownSpend = (reported, key, total) => {
7177
- const own = unreported(reported, key, total);
7178
- if (total > 0) reported.set(key, total);
7179
- return own;
7925
+ const entryOf = (declared, { ok, output, error }) => {
7926
+ const json = isRecord$1(output["json"]) ? output["json"] : {};
7927
+ const fields = Object.keys(declared ?? {}).map((field) => [field, Object.hasOwn(json, field) ? json[field] : output[field]]);
7928
+ const own = [
7929
+ "summary",
7930
+ "branch",
7931
+ "worktree",
7932
+ "merge"
7933
+ ].map((field) => [field, output[field]]);
7934
+ const failed = !ok && typeof output["exitCode"] === "number" ? [["exitCode", output["exitCode"]], ["error", error]] : [];
7935
+ return Object.fromEntries([
7936
+ ...fields,
7937
+ ...own,
7938
+ ...failed
7939
+ ].filter(([, value]) => value !== void 0));
7940
+ };
7941
+ /** Whether an item passed but its work could not reach its owner. */
7942
+ const mergeFailed = ({ output }) => isRecord$1(output["merge"]) && output["merge"]["ok"] === false;
7943
+ const statusOf$2 = (result) => {
7944
+ if (mergeFailed(result)) return "fail";
7945
+ const { ok, output } = result;
7946
+ const status = output["status"];
7947
+ if (typeof status === "string") return status;
7948
+ return ok ? "pass" : "fail";
7949
+ };
7950
+ /** Why a fan-out did not succeed, naming the first item that failed. */
7951
+ const whyNot = (count, failures, skipped) => {
7952
+ const parts = [`${String(failures.length)} of ${String(count)} items failed`];
7953
+ const [first] = failures;
7954
+ if (first !== void 0) parts[0] = `${parts[0] ?? ""}; first, ${first}`;
7955
+ if (skipped > 0) parts.push(`${String(skipped)} not started`);
7956
+ return parts.join("; ");
7180
7957
  };
7181
7958
  /**
7182
- * What a run's events say it spent, for `ork resume`. The total is what its
7183
- * nodes recorded. A session's report counts once its node finished, or when
7184
- * `ork chat` made it outside any node: a session cut off before its node
7185
- * finished has recorded nothing, so its next total counts the part that was cut
7186
- * off. A leader's fork is never resumed, so its report counts when it comes,
7187
- * whether or not its leader finished.
7959
+ * Waits until the item may start: not while it is held, and without keeping a
7960
+ * session slot through a pause, which would stop other nodes.
7188
7961
  */
7189
- const earlierSpend = (events) => {
7190
- const reported = /* @__PURE__ */ new Map();
7191
- const open = /* @__PURE__ */ new Map();
7192
- const forks = /* @__PURE__ */ new Map();
7193
- let total = 0;
7194
- for (const event of events) if (event.type === "node.started") open.set(event.id, null);
7195
- else if (event.type === "node.finished") {
7196
- total += event.usage?.costUsd ?? 0;
7197
- const last = open.get(event.id);
7198
- if (last !== null && last !== void 0) ownSpend(reported, last.key, last.cost);
7199
- open.delete(event.id);
7200
- } else if (event.type === "node.event" && event.event.type === "session.finished") {
7201
- const { sessionId, usage } = event.event.outcome;
7202
- const key = `${event.id}/${sessionId}`;
7203
- if (event.event.fork === true) {
7204
- const own = ownSpend(reported, key, usage.costUsd);
7205
- total += own;
7206
- forks.set(event.id, (forks.get(event.id) ?? 0) + own);
7207
- } else if (open.has(event.id)) open.set(event.id, {
7208
- key,
7209
- cost: usage.costUsd
7210
- });
7211
- else ownSpend(reported, key, usage.costUsd);
7962
+ const awaitTurn = async (hold, slots, signal) => {
7963
+ for (;;) {
7964
+ await hold.wait("start", signal);
7965
+ if (slots === null) return;
7966
+ await slots.take();
7967
+ if (!hold.held() || signal.aborted) return;
7968
+ slots.release();
7212
7969
  }
7213
- return {
7214
- total,
7215
- reported,
7216
- forks
7217
- };
7218
7970
  };
7219
- // ../core/src/run/reserves.ts
7220
- const createReserves = () => {
7221
- const held = /* @__PURE__ */ new Map();
7222
- const waiters = /* @__PURE__ */ new Set();
7223
- const wake = () => {
7224
- const woken = [...waiters];
7225
- waiters.clear();
7226
- for (const resolve of woken) resolve();
7227
- };
7228
- return {
7229
- /** All that is set aside now. */
7230
- total: () => [...held.values()].reduce((sum, reserve) => sum + reserve.usd, 0),
7231
- /**
7232
- * `id` sets aside `usd` (nothing, or 0, lets it go). `waitable` is false
7233
- * for a leader, whose children must never wait on it.
7234
- */
7235
- reserve: (id, usd, waitable) => {
7236
- const before = held.get(id)?.usd ?? 0;
7237
- if (usd === void 0 || usd <= 0) held.delete(id);
7238
- else held.set(id, {
7239
- usd,
7240
- waitable
7971
+ const createFanOut = (deps) => {
7972
+ const { holds, slots, stops, nodeOf, perform, options } = deps;
7973
+ return async (activation, forEach, round) => {
7974
+ const { id, input } = activation;
7975
+ const scope = deps.scope();
7976
+ const recorded = options.replay?.listed?.(id, round, sourceOf(forEach, scope)) ?? null;
7977
+ const listed = recorded === null ? listOf(forEach, scope) : {
7978
+ ok: true,
7979
+ list: unmasked(forEach, recorded)
7980
+ };
7981
+ if (!listed.ok) return failedWith(endFor("invalid_node", { reason: listed.error }));
7982
+ let { list } = listed;
7983
+ const recordList = () => {
7984
+ options.onEvent({
7985
+ type: "each.listed",
7986
+ node: id,
7987
+ round,
7988
+ count: list.length,
7989
+ list,
7990
+ ...recorded === null && options.replay !== void 0 ? { anew: true } : {}
7241
7991
  });
7242
- if ((usd ?? 0) < before) wake();
7243
- },
7244
- /** The node ended: whatever it still held is let go. */
7245
- drop: (id) => {
7246
- if (held.delete(id)) wake();
7247
- },
7248
- /**
7249
- * Waits until a reserve shrinks. `false` at once when no other node holds
7250
- * one worth waiting on, or when `signal` aborts.
7251
- */
7252
- freed: (id, signal) => {
7253
- if (![...held].some(([other, reserve]) => other !== id && reserve.waitable) || signal.aborted) return Promise.resolve(false);
7254
- return new Promise((resolve) => {
7255
- const done = (freed) => () => {
7256
- waiters.delete(onFree);
7257
- signal.removeEventListener("abort", onAbort);
7258
- resolve(freed);
7259
- };
7260
- const onFree = done(true);
7261
- const onAbort = done(false);
7262
- waiters.add(onFree);
7263
- signal.addEventListener("abort", onAbort, { once: true });
7992
+ };
7993
+ recordList();
7994
+ const signal = stops.signalFor(id);
7995
+ if ("maxItems" in forEach && list.length > forEach.maxItems) {
7996
+ options.onEvent({
7997
+ type: "each.exceeded",
7998
+ node: id,
7999
+ round,
8000
+ count: list.length,
8001
+ maxItems: forEach.maxItems
7264
8002
  });
8003
+ const chosen = await deps.loops.overList(id, list, forEach.maxItems, round, signal);
8004
+ if (chosen === null) {
8005
+ const reason = `the list of ${id} has ${String(list.length)} items, over maxItems ${String(forEach.maxItems)}`;
8006
+ return stops.ended(id, {
8007
+ ...failedWith(endFor("invalid_node", { reason }), {
8008
+ status: "fail",
8009
+ summary: reason
8010
+ }),
8011
+ limited: reason,
8012
+ stopsAs: "failed"
8013
+ });
8014
+ }
8015
+ const narrowed = chosen.length !== list.length;
8016
+ list = chosen;
8017
+ if (narrowed) recordList();
7265
8018
  }
8019
+ const node = nodeOf(activation);
8020
+ const session = node !== void 0 && needsSlot(node);
8021
+ const spends = node !== void 0 && spendsBudget(node);
8022
+ const declared = node === void 0 ? void 0 : declaredOutputOf(node);
8023
+ const results = list.map(() => ({}));
8024
+ const statuses = list.map(() => "skipped");
8025
+ const failures = [];
8026
+ /** How the first item that failed ended: the fanned node's own end. */
8027
+ let firstEnd;
8028
+ const mayStart = () => !signal.aborted && !deps.isStopped() && (forEach.onFailure !== "stop" || failures.length === 0);
8029
+ const runItem = async (index) => {
8030
+ const itemId = `${id}.${String(index)}`;
8031
+ const hold = holds.forNode(itemId, id);
8032
+ await awaitTurn(hold, session ? slots : null, signal);
8033
+ try {
8034
+ if (!mayStart() || spends && deps.budgetSpent()) return;
8035
+ const result = await perform({
8036
+ id: itemId,
8037
+ input,
8038
+ template: id,
8039
+ item: {
8040
+ of: id,
8041
+ index,
8042
+ value: list[index]
8043
+ },
8044
+ round
8045
+ });
8046
+ results[index] = entryOf(declared, result);
8047
+ statuses[index] = statusOf$2(result);
8048
+ if (!result.ok) {
8049
+ firstEnd ??= endOf(result);
8050
+ failures.push(`${itemId}: ${result.error ?? "failed"}`);
8051
+ } else if (mergeFailed(result)) failures.push(`${itemId}: its work could not be merged`);
8052
+ } finally {
8053
+ if (session) slots.release();
8054
+ }
8055
+ };
8056
+ let next = 0;
8057
+ const worker = async () => {
8058
+ while (next < list.length && mayStart()) {
8059
+ const index = next;
8060
+ next += 1;
8061
+ await runItem(index);
8062
+ }
8063
+ };
8064
+ const lanes = Math.min(forEach.maxParallel ?? slots.limit, list.length);
8065
+ await Promise.all(Array.from({ length: lanes }, () => worker()));
8066
+ const skipped = statuses.filter((status) => status === "skipped").length;
8067
+ const ok = failures.length === 0 && skipped === 0;
8068
+ const gathered = {
8069
+ ok,
8070
+ output: {
8071
+ items: list,
8072
+ results,
8073
+ statuses,
8074
+ count: list.length
8075
+ },
8076
+ ...ok ? {} : { error: whyNot(list.length, failures, skipped) },
8077
+ ...ok || firstEnd === void 0 ? {} : { end: firstEnd },
8078
+ ...!ok && deps.isStopped() ? { limited: deps.reasons[0] ?? "the run stopped" } : {}
8079
+ };
8080
+ return {
8081
+ ...stops.ended(id, gathered),
8082
+ output: gathered.output
8083
+ };
7266
8084
  };
7267
8085
  };
7268
- // ../core/src/run/spending.ts
7269
- /**
7270
- * A result with only what this activation spent (ADR-046): a session's report
7271
- * holds its total so far, while its tokens are this process's own. `reported`
7272
- * holds each session's total.
7273
- */
7274
- const withOwnSpend = (id, result, reported) => {
7275
- const { sessionId, usage } = result;
7276
- if (sessionId === void 0 || usage === void 0) return result;
7277
- const own = {
7278
- ...usage,
7279
- costUsd: ownSpend(reported, `${id}/${sessionId}`, usage.costUsd)
7280
- };
7281
- return {
7282
- ...result,
7283
- usage: own,
7284
- output: "usage" in result.output ? {
7285
- ...result.output,
7286
- usage: own
7287
- } : result.output
7288
- };
7289
- };
7290
- /** A run's spending so far; a resumed run starts from what it had spent. */
7291
- const createSpend = (budgetUsd, earlier, limits) => {
7292
- const reported = new Map(earlier?.reported);
7293
- let spent = earlier?.total ?? 0;
7294
- /** What running sessions last reported, not yet in `spent`, by `node/session`. */
7295
- const inFlight = /* @__PURE__ */ new Map();
7296
- const committed = () => spent + [...inFlight.values()].reduce((sum, cost) => sum + cost, 0);
7297
- /** Caps running sessions were given (ADR-077): out of what is offered. */
7298
- const reserves = createReserves();
7299
- const budgetLeft = () => budgetUsd === void 0 ? void 0 : budgetUsd - committed() - reserves.total();
7300
- return {
7301
- total: () => spent,
7302
- /** A node sets aside its session's cap; nothing or 0 lets it go. */
7303
- reserve: reserves.reserve,
7304
- /** Waits until another node's reserve frees; `false` when none can. */
7305
- freed: reserves.freed,
7306
- /** What a session has spent in this activation, from its total so far. */
7307
- ownOf: (id, sessionId, total) => unreported(reported, `${id}/${sessionId}`, total),
7308
- /** A running session said what it has spent so far. */
7309
- report: (id, sessionId, total) => {
7310
- const key = `${id}/${sessionId}`;
7311
- inFlight.set(key, unreported(reported, key, total));
7312
- },
7313
- /** The node ended: `count` has what its sessions last reported. */
7314
- settled: (id) => {
7315
- for (const key of inFlight.keys()) if (key.startsWith(`${id}/`)) inFlight.delete(key);
7316
- reserves.drop(id);
7317
- },
7318
- /** What a session may spend: the least of the budget and any quota. */
7319
- left: () => {
7320
- const known = [budgetLeft(), limits?.().leftUsd].filter((left) => left !== void 0);
7321
- return known.length === 0 ? void 0 : Math.min(...known);
7322
- },
7323
- budgetLeft,
7324
- /** What the limits across runs allow now. */
7325
- quota: () => limits?.() ?? {},
7326
- /**
7327
- * What stops anything new from starting: a quota first, as a budget can be
7328
- * raised.
7329
- */
7330
- spentOut: () => {
7331
- const blocked = limits?.().blocked;
7332
- if (blocked !== void 0) return {
7333
- status: "quota_exhausted",
7334
- reason: blocked
7335
- };
7336
- return budgetUsd !== void 0 && committed() >= budgetUsd ? {
7337
- status: "budget_exhausted",
7338
- reason: `the run's budget of $${String(budgetUsd)} is spent`
7339
- } : null;
7340
- },
7341
- /**
7342
- * Counts a result, and gives it back with only its own spend. A replayed
7343
- * result is as stored, and already in what the run started from.
7344
- */
7345
- count: (id, result, replayed) => {
7346
- if (replayed) return result;
7347
- const own = withOwnSpend(id, result, reported);
7348
- spent += own.usage?.costUsd ?? 0;
7349
- return own;
8086
+ // ../core/src/run/delegation.ts
8087
+ /** Node types a leader can hand work to. */
8088
+ const DELEGABLE = /* @__PURE__ */ new Set(["claude.leader", "claude.worker"]);
8089
+ /** A child a leader delegated to: waits for a slot, runs, reports back. */
8090
+ const createRunChild = (hooks) => async (child, brief) => {
8091
+ await hooks.slots.take();
8092
+ try {
8093
+ await hooks.holds.forNode(child.id, child.template).wait("start", hooks.signal);
8094
+ if (hooks.isStopped() || hooks.budgetSpent()) {
8095
+ const why = `not started: ${hooks.reasons[0] ?? "the run stopped"}`;
8096
+ const end = hooks.budgetSpent() ? endFor("budget_limit", {}, why) : stoppedEnd(hooks.signal, why);
8097
+ return failedWith(end, {
8098
+ status: "fail",
8099
+ summary: why
8100
+ });
7350
8101
  }
7351
- };
7352
- };
7353
- /** A run's spending, from what the scheduler is given. */
7354
- const spendOf = (budgetUsd, options) => createSpend(budgetUsd, options.replay?.earlier, options.limits);
7355
- // ../core/src/run/gather.ts
7356
- const asList = (value) => Array.isArray(value) ? value : [];
7357
- /** `before` then `now`, leaving out an entry already there. */
7358
- const joined = (before, now) => {
7359
- const seen = /* @__PURE__ */ new Set();
7360
- const out = [];
7361
- for (const entry of [...asList(before), ...asList(now)]) {
7362
- const key = JSON.stringify(entry);
7363
- if (seen.has(key)) continue;
7364
- seen.add(key);
7365
- out.push(entry);
8102
+ return await hooks.perform({
8103
+ ...child,
8104
+ input: void 0,
8105
+ brief
8106
+ });
8107
+ } finally {
8108
+ hooks.slots.release();
7366
8109
  }
7367
- return out;
7368
- };
7369
- /**
7370
- * `result` with each gathered field holding every round's entries so far.
7371
- * `previous` is the node's output from its last round, already gathered.
7372
- */
7373
- const joinRounds = (fields, previous, result) => {
7374
- if (fields.length === 0 || previous === void 0) return result;
7375
- const output = { ...result.output };
7376
- for (const field of fields) output[field] = joined(previous[field], result.output[field]);
7377
- return {
7378
- ...result,
7379
- output
7380
- };
7381
- };
7382
- /** `result` with `node`'s `output.gather` fields joined to its earlier rounds. */
7383
- const gatherRounds = (node, previous, result) => joinRounds(gatheredFieldsOf(node), previous, result);
7384
- /**
7385
- * What `activation`'s rounds reported so far, to gather onto. A spawned
7386
- * instance gathers nothing earlier: its id (`impl-2`) is reused by a different
7387
- * worker in the leader's next round. A delegated child that hasn't run since a
7388
- * resume starts from what it reported before it, since its leader's recorded
7389
- * rounds are handed back whole and never run it again.
7390
- */
7391
- const previousOf = (activation, current, replay) => {
7392
- const { brief, template, id } = activation;
7393
- if (brief !== void 0 && template !== void 0) return void 0;
7394
- if (current !== void 0 || brief === void 0) return current;
7395
- return replay?.lastOutput?.(id);
7396
- };
7397
- /** Gathers each activation's result onto what its node reported before. */
7398
- const createGatherer = (outputs, replay, nodeOf) => (activation, result) => gatherRounds(nodeOf(activation), previousOf(activation, outputs[activation.id], replay), result);
7399
- // ../core/src/run/results.ts
7400
- /** Control nodes pass their input through; a join's input is its output. */
7401
- const passThrough = (input) => ({
7402
- output: input ?? {},
7403
- ok: true
7404
- });
7405
- const recordRun = (entry, result) => {
7406
- entry.runs += 1;
7407
- entry.ok = result.ok;
7408
- entry.output = result.output;
7409
- entry.costUsd += result.usage?.costUsd ?? 0;
7410
- if (result.error === void 0) delete entry.error;
7411
- else entry.error = result.error;
7412
- };
7413
- const startedEvent = (activation, nodeType, round, agent) => {
7414
- const event = {
7415
- type: "node.started",
7416
- id: activation.id,
7417
- nodeType,
7418
- round
7419
- };
7420
- if (activation.brief !== void 0) event.owner = activation.brief.from;
7421
- if (agent !== void 0) event.agent = agent;
7422
- const { item } = activation;
7423
- if (item !== void 0) event.item = {
7424
- of: item.of,
7425
- index: item.index
7426
- };
7427
- return event;
7428
- };
7429
- const finishedEvent = (id, round, result) => {
7430
- const event = {
7431
- type: "node.finished",
7432
- id,
7433
- round,
7434
- ok: result.ok,
7435
- output: result.output
7436
- };
7437
- if (result.usage !== void 0) event.usage = result.usage;
7438
- if (result.error !== void 0) event.error = result.error;
7439
- return event;
7440
8110
  };
7441
- const failure = (error) => ({
8111
+ const rejected = (child, error) => ({
8112
+ child,
8113
+ ok: false,
7442
8114
  output: {
7443
8115
  status: "fail",
7444
8116
  summary: error
7445
8117
  },
7446
- ok: false,
7447
8118
  error
7448
8119
  });
7449
- /** The node's entry in the run summary, created on first use. */
7450
- const summaryOf = (summaries, id) => {
7451
- const existing = summaries.get(id);
7452
- if (existing !== void 0) return existing;
7453
- const created = {
7454
- id,
7455
- runs: 0,
7456
- ok: null,
7457
- output: null,
7458
- costUsd: 0
8120
+ const reportOf = (child, result) => {
8121
+ const report = {
8122
+ child,
8123
+ ok: result.ok,
8124
+ output: result.output
7459
8125
  };
7460
- summaries.set(id, created);
7461
- return created;
8126
+ if (result.error !== void 0) report.error = result.error;
8127
+ return report;
7462
8128
  };
7463
- /** A run's summary; a resumed run's leaders keep their forks' spend (ADR-063). */
7464
- const createSummaries = (earlier) => {
7465
- const summaries = /* @__PURE__ */ new Map();
7466
- const summary = (id) => summaryOf(summaries, id);
7467
- for (const [id, costUsd] of earlier?.forks ?? []) summary(id).costUsd += costUsd;
7468
- return {
7469
- summaries,
7470
- summary
8129
+ const createOwnership = (hooks) => {
8130
+ const { graph, leader } = hooks;
8131
+ const owner = graph.nodes.get(leader);
8132
+ const isFanned = (id) => forEachOf(graph.nodes.get(id)?.node) !== void 0;
8133
+ const children = (owner?.children ?? []).flatMap((id) => {
8134
+ const node = graph.nodes.get(id)?.node;
8135
+ if (node === void 0 || !DELEGABLE.has(node.type) || isFanned(id)) return [];
8136
+ const child = {
8137
+ id,
8138
+ type: node.type
8139
+ };
8140
+ if (node.description !== void 0) child.description = node.description;
8141
+ return [child];
8142
+ });
8143
+ const templates = children.filter((c) => c.type === "claude.worker").map((c) => c.id);
8144
+ const leaderNode = owner?.node;
8145
+ const maxWorkers = leaderNode?.type === "claude.leader" ? leaderNode.maxWorkers ?? 0 : 0;
8146
+ const spawned = [];
8147
+ /** Runs the accepted jobs in parallel; the slot goes to them meanwhile. */
8148
+ const runJobs = async (jobs) => {
8149
+ const accepted = jobs.filter((j) => "brief" in j);
8150
+ const notes = [];
8151
+ if (accepted.length > 0) {
8152
+ const problem = await hooks.checkpoint();
8153
+ if (problem !== null) notes.push(`Your uncommitted work could not be committed (${problem}); the children started from your branch's last commit.`);
8154
+ const released = hooks.releaseClaims().map((r) => `\`${r}\``);
8155
+ if (released.length > 0) notes.push(`Your claims on ${released.join(", ")} were released while your children worked; claim again before you edit.`);
8156
+ }
8157
+ hooks.lend();
8158
+ try {
8159
+ return {
8160
+ reports: await Promise.all(jobs.map(async (job) => {
8161
+ if (!("brief" in job)) return job;
8162
+ const child = { id: job.id };
8163
+ if (job.template !== void 0) child.template = job.template;
8164
+ const result = await hooks.runChild(child, {
8165
+ from: leader,
8166
+ text: job.brief
8167
+ });
8168
+ return reportOf(job.id, result);
8169
+ })),
8170
+ notes
8171
+ };
8172
+ } finally {
8173
+ await hooks.reclaim();
8174
+ }
7471
8175
  };
7472
- };
7473
- /** The tighter of the workflow's budget and what a caller left it, if any. */
7474
- const runBudget = (graph, settings) => {
7475
- const limits = [graph.workflow.limits?.budgetUsd, settings?.budgetCap].filter((limit) => limit !== void 0);
7476
- return limits.length === 0 ? {} : { budgetUsd: Math.min(...limits) };
7477
- };
7478
- /** The `run.started` event: the workflow, its entries, nodes and inputs. */
7479
- const runStartedEvent = (graph, runId, options) => ({
7480
- type: "run.started",
7481
- runId,
7482
- workflow: graph.workflow.name,
7483
- entries: graph.entries,
7484
- nodes: declaredNodes(graph),
7485
- edges: graph.edges.map(({ from, to, when, maxRounds }) => ({
7486
- from,
7487
- to,
7488
- ...when === void 0 ? {} : { when },
7489
- ...maxRounds === void 0 ? {} : { maxRounds }
7490
- })),
7491
- channels: graph.channels,
7492
- inputs: options.inputs,
7493
- ...options.settings === void 0 ? {} : { settings: options.settings },
7494
- ...runBudget(graph, options.settings),
7495
- ...options.orkVersion === void 0 ? {} : { ork: options.orkVersion }
7496
- });
7497
- /** The unread mail of nodes that never ran; a node that ran saw its notice. */
7498
- const unreadOf = (nodes, unread) => {
7499
- const ran = new Set(nodes.filter((node) => node.runs > 0).map((node) => node.id));
7500
- const left = [...unread ?? []].filter(([node]) => !ran.has(node)).map(([node, messages]) => ({
7501
- node,
7502
- messages
7503
- }));
7504
- return left.length === 0 ? {} : { unread: left };
7505
- };
7506
- /**
7507
- * The run's result: failed when any reason was given, unless it stopped. A
7508
- * skipped node or a handled failure leaves it `succeeded`, and is counted.
7509
- */
7510
- const runResult = (run) => ({
7511
- runId: run.runId,
7512
- status: run.stopped ?? (run.reasons.length > 0 ? "failed" : "succeeded"),
7513
- costUsd: run.costUsd,
7514
- nodes: run.nodes,
7515
- reasons: run.reasons,
7516
- worktrees: run.worktrees,
7517
- ...run.outputs === void 0 ? {} : { outputs: run.outputs },
7518
- skipped: run.nodes.filter((node) => node.runs === 0).length,
7519
- failuresHandled: run.failuresHandled,
7520
- ...unreadOf(run.nodes, run.unread)
7521
- });
7522
- /** A workflow's `outputs`, rendered as the run ends. */
7523
- const renderOutputs = (outputs, scope) => outputs === void 0 ? void 0 : Object.fromEntries(Object.entries(outputs).map(([name, text]) => {
7524
- const template = parseTemplate(text);
7525
- return [name, template.ok ? renderTemplate(template.parts, scope) : text];
7526
- }));
7527
- /** Closes the run's worktrees; a failure is one more reason, not a crash. */
7528
- const closeWorkspace = (workspace, reasons) => (workspace?.close() ?? Promise.resolve([])).catch((error) => {
7529
- reasons.push(`cleaning up worktrees: ${String(error)}`);
7530
- return [];
7531
- });
7532
- // ../core/src/run/slots.ts
7533
- /**
7534
- * @file Session slots: at most `limits.concurrency` Claude sessions run at
7535
- * once. Queued activations take a free slot when the scheduler pumps;
7536
- * delegated children and leaders returning from `delegate` wait for one and
7537
- * are served first, so a leader waiting on its children never holds a slot
7538
- * they need.
7539
- */
7540
- /** Only a Claude session takes a slot; tools and gates are not limited. */
7541
- const needsSlot = (node) => isClaudeNode(node);
7542
- /** A call spends through its run, so it needs budget as a session does. */
7543
- const spendsBudget = (node) => needsSlot(node) || node.type === "workflow.call";
7544
- const createSlots = (limit, onFree) => {
7545
- let used = 0;
7546
- const waiting = [];
7547
- return {
7548
- limit,
7549
- tryTake: () => {
7550
- if (used >= limit || waiting.length > 0) return false;
7551
- used += 1;
7552
- return true;
7553
- },
7554
- take: () => {
7555
- if (used < limit && waiting.length === 0) {
7556
- used += 1;
7557
- return Promise.resolve();
8176
+ const delegate = (tasks) => {
8177
+ const seen = /* @__PURE__ */ new Set();
8178
+ const jobs = tasks.map((task) => {
8179
+ if (!children.some((c) => c.id === task.child)) {
8180
+ const names = children.map((c) => c.id).join(", ");
8181
+ return rejected(task.child, `not one of your children (${names === "" ? "you have none" : names})`);
7558
8182
  }
7559
- const { promise, resolve } = Promise.withResolvers();
7560
- waiting.push(resolve);
7561
- return promise;
7562
- },
7563
- release: () => {
7564
- const next = waiting.shift();
7565
- if (next === void 0) {
7566
- used -= 1;
7567
- onFree();
7568
- } else next();
7569
- }
8183
+ if (seen.has(task.child) || hooks.isRunning(task.child)) return rejected(task.child, templates.includes(task.child) && maxWorkers > 0 ? "already running; use spawn_worker for another instance" : "already running");
8184
+ seen.add(task.child);
8185
+ return {
8186
+ id: task.child,
8187
+ brief: task.brief
8188
+ };
8189
+ });
8190
+ return runJobs(jobs);
7570
8191
  };
7571
- };
7572
- // ../core/src/run/fan-out.ts
7573
- /** The node's `forEach`, whatever its type; `undefined` when it has none. */
7574
- const forEachOf = (node) => node !== void 0 && "forEach" in node ? node.forEach : void 0;
7575
- /** The list a node runs over, or why there isn't one. */
7576
- const listOf = (forEach, scope) => {
7577
- if ("items" in forEach) return {
7578
- ok: true,
7579
- list: forEach.items
8192
+ const nextInstance = (template) => {
8193
+ for (let n = 2;; n += 1) {
8194
+ const id = `${template}-${String(n)}`;
8195
+ if (!graph.nodes.has(id) && !spawned.includes(id)) return id;
8196
+ }
7580
8197
  };
7581
- const read = templateValue(forEach.from, scope);
7582
- if (!read.ok) return read;
7583
- const { value } = read;
7584
- if (Array.isArray(value)) return {
7585
- ok: true,
7586
- list: value
8198
+ const spawn = (tasks) => {
8199
+ const jobs = tasks.map((task) => {
8200
+ if (!templates.includes(task.template)) {
8201
+ const names = templates.join(", ");
8202
+ return rejected(task.template, `not one of your child workers (${names === "" ? "you have none" : names})`);
8203
+ }
8204
+ if (spawned.length >= maxWorkers) return rejected(task.template, `no workers left to spawn (maxWorkers: ${String(maxWorkers)})`);
8205
+ const id = nextInstance(task.template);
8206
+ spawned.push(id);
8207
+ return {
8208
+ id,
8209
+ template: task.template,
8210
+ brief: task.brief
8211
+ };
8212
+ });
8213
+ return runJobs(jobs);
7587
8214
  };
7588
- const name = String(templatePath(forEach.from)?.at(-1) ?? forEach.from);
7589
8215
  return {
7590
- ok: false,
7591
- error: declaredProblem({ [name]: { list: {} } }, { [name]: value }) ?? `${name} is not a list`
8216
+ children,
8217
+ fanned: (owner?.children ?? []).filter((id) => isFanned(id)),
8218
+ templates,
8219
+ workersLeft: () => Math.max(0, maxWorkers - spawned.length),
8220
+ delegate,
8221
+ spawn
7592
8222
  };
7593
8223
  };
7594
- /** The output a `from` list comes from, whose freshness resume asks about. */
7595
- const sourceOf = (forEach, scope) => {
7596
- if (!("from" in forEach)) return void 0;
7597
- const [root, id] = templatePath(forEach.from) ?? [];
7598
- return root === "nodes" ? scope.nodes[String(id)]?.output : void 0;
7599
- };
7600
- const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
8224
+ // ../core/src/run/scope.ts
7601
8225
  /**
7602
- * What `results` keeps of an item: its declared fields (a program's are under
7603
- * `json`), its summary, Ork's own branch and worktree, and how a program
7604
- * failed. The full output stays in the item's own `node.finished`.
8226
+ * @file A run's id and folder, and the scope templates and `when:` conditions
8227
+ * read during it.
7605
8228
  */
7606
- const entryOf = (declared, { ok, output, error }) => {
7607
- const json = isRecord$1(output["json"]) ? output["json"] : {};
7608
- const fields = Object.keys(declared ?? {}).map((field) => [field, Object.hasOwn(json, field) ? json[field] : output[field]]);
7609
- const own = [
7610
- "summary",
7611
- "branch",
7612
- "worktree",
7613
- "merge"
7614
- ].map((field) => [field, output[field]]);
7615
- const failed = !ok && typeof output["exitCode"] === "number" ? [["exitCode", output["exitCode"]], ["error", error]] : [];
7616
- return Object.fromEntries([
7617
- ...fields,
7618
- ...own,
7619
- ...failed
7620
- ].filter(([, value]) => value !== void 0));
7621
- };
7622
- /** Whether an item passed but its work could not reach its owner. */
7623
- const mergeFailed = ({ output }) => isRecord$1(output["merge"]) && output["merge"]["ok"] === false;
7624
- const statusOf$2 = (result) => {
7625
- if (mergeFailed(result)) return "fail";
7626
- const { ok, output } = result;
7627
- const status = output["status"];
7628
- if (typeof status === "string") return status;
7629
- return ok ? "pass" : "fail";
7630
- };
7631
- /** Why a fan-out did not succeed, naming the first item that failed. */
7632
- const whyNot = (count, failures, skipped) => {
7633
- const parts = [`${String(failures.length)} of ${String(count)} items failed`];
7634
- const [first] = failures;
7635
- if (first !== void 0) parts[0] = `${parts[0] ?? ""}; first, ${first}`;
7636
- if (skipped > 0) parts.push(`${String(skipped)} not started`);
7637
- return parts.join("; ");
8229
+ /** The run's id (given, or a new one) and its `.ork/runs/<run>` folder. */
8230
+ const runPlace = (options) => {
8231
+ const runId = options.runId ?? randomUUID().slice(0, 8);
8232
+ return {
8233
+ runId,
8234
+ runDir: options.runDir ?? path.join(options.cwd, ".ork", "runs", runId)
8235
+ };
7638
8236
  };
7639
8237
  /**
7640
- * Waits until the item may start: not while it is held, and without keeping a
7641
- * session slot through a pause, which would stop other nodes.
8238
+ * The folders every node of a run reads from: the workflow's, the project's,
8239
+ * Ork's.
7642
8240
  */
7643
- const awaitTurn = async (hold, slots, signal) => {
7644
- for (;;) {
7645
- await hold.wait("start", signal);
7646
- if (slots === null) return;
7647
- await slots.take();
7648
- if (!hold.held() || signal.aborted) return;
7649
- slots.release();
8241
+ const foldersOf = (options) => ({
8242
+ workflowDir: options.workflowDir ?? options.cwd,
8243
+ projectRoot: options.projectRoot ?? options.cwd,
8244
+ ...options.orkDir === void 0 ? {} : { orkDir: options.orkDir }
8245
+ });
8246
+ /** What templates and `when:` read during a run. */
8247
+ const runScope = (inputs, outputs, runId, item) => {
8248
+ const scope = {
8249
+ inputs,
8250
+ nodes: Object.fromEntries(Object.entries(outputs).map(([id, output]) => [id, { output }]))
8251
+ };
8252
+ if (runId !== void 0) scope.run = { id: runId };
8253
+ if (item !== void 0) {
8254
+ scope.item = item.value;
8255
+ scope.index = item.index;
7650
8256
  }
8257
+ return scope;
7651
8258
  };
7652
- const createFanOut = (deps) => {
7653
- const { holds, slots, stops, nodeOf, perform, options } = deps;
7654
- return async (activation, forEach, round) => {
7655
- const { id, input } = activation;
7656
- const scope = deps.scope();
7657
- const recorded = options.replay?.listed?.(id, round, sourceOf(forEach, scope)) ?? null;
7658
- const listed = recorded === null ? listOf(forEach, scope) : {
7659
- ok: true,
7660
- list: unmasked(forEach, recorded)
7661
- };
7662
- if (!listed.ok) return failure(listed.error);
7663
- let { list } = listed;
7664
- const recordList = () => {
7665
- options.onEvent({
7666
- type: "each.listed",
7667
- node: id,
7668
- round,
7669
- count: list.length,
7670
- list,
7671
- ...recorded === null && options.replay !== void 0 ? { anew: true } : {}
7672
- });
7673
- };
7674
- recordList();
7675
- const signal = stops.signalFor(id);
7676
- if ("maxItems" in forEach && list.length > forEach.maxItems) {
7677
- options.onEvent({
7678
- type: "each.exceeded",
7679
- node: id,
7680
- round,
7681
- count: list.length,
7682
- maxItems: forEach.maxItems
7683
- });
7684
- const chosen = await deps.loops.overList(id, list, forEach.maxItems, round, signal);
7685
- if (chosen === null) {
7686
- const reason = `the list of ${id} has ${String(list.length)} items, over maxItems ${String(forEach.maxItems)}`;
7687
- return stops.ended(id, {
7688
- ...failure(reason),
7689
- limited: reason,
7690
- stopsAs: "failed"
7691
- });
7692
- }
7693
- const narrowed = chosen.length !== list.length;
7694
- list = chosen;
7695
- if (narrowed) recordList();
7696
- }
7697
- const node = nodeOf(activation);
7698
- const session = node !== void 0 && needsSlot(node);
7699
- const spends = node !== void 0 && spendsBudget(node);
7700
- const declared = node === void 0 ? void 0 : declaredOutputOf(node);
7701
- const results = list.map(() => ({}));
7702
- const statuses = list.map(() => "skipped");
7703
- const failures = [];
7704
- const mayStart = () => !signal.aborted && !deps.isStopped() && (forEach.onFailure !== "stop" || failures.length === 0);
7705
- const runItem = async (index) => {
7706
- const itemId = `${id}.${String(index)}`;
7707
- const hold = holds.forNode(itemId, id);
7708
- await awaitTurn(hold, session ? slots : null, signal);
7709
- try {
7710
- if (!mayStart() || spends && deps.budgetSpent()) return;
7711
- const result = await perform({
7712
- id: itemId,
7713
- input,
7714
- template: id,
7715
- item: {
7716
- of: id,
7717
- index,
7718
- value: list[index]
7719
- },
7720
- round
7721
- });
7722
- results[index] = entryOf(declared, result);
7723
- statuses[index] = statusOf$2(result);
7724
- if (!result.ok) failures.push(`${itemId}: ${result.error ?? "failed"}`);
7725
- else if (mergeFailed(result)) failures.push(`${itemId}: its work could not be merged`);
7726
- } finally {
7727
- if (session) slots.release();
7728
- }
7729
- };
7730
- let next = 0;
7731
- const worker = async () => {
7732
- while (next < list.length && mayStart()) {
7733
- const index = next;
7734
- next += 1;
7735
- await runItem(index);
8259
+ // ../core/src/run/context.ts
8260
+ /**
8261
+ * @file One activation's execute context: what the scheduler hands a node's
8262
+ * executor. Its inputs and the outputs so far, where it works, the budget
8263
+ * left, the human, its hold and its own stop signal (ADR-041), its ports on
8264
+ * the bus, the session it resumes, its share of the machine, and, for a
8265
+ * leader, its ownership of its children.
8266
+ */
8267
+ const nodeContext = (deps, activation, cwd, round, clock) => {
8268
+ const { options, runId } = deps;
8269
+ const { graph } = options;
8270
+ const { id, template } = activation;
8271
+ const context = {
8272
+ runId,
8273
+ id,
8274
+ workflow: graph.workflow,
8275
+ inputs: options.inputs,
8276
+ outputs: deps.outputs,
8277
+ input: activation.input,
8278
+ round,
8279
+ cwd,
8280
+ runDir: deps.runDir,
8281
+ ...foldersOf(options),
8282
+ budgetLeftUsd: deps.spend.left(),
8283
+ budgetLeftNow: deps.spend.budgetLeft,
8284
+ reserveBudget: (usd) => {
8285
+ deps.spend.reserve(id, usd, deps.nodeOf(activation)?.type !== "claude.leader");
8286
+ },
8287
+ budgetFreed: (signal) => deps.spend.freed(id, signal),
8288
+ quota: deps.spend.quota,
8289
+ ownSpend: (sessionId, total) => deps.spend.ownOf(id, sessionId, total),
8290
+ human: deps.human,
8291
+ hold: deps.holds.forNode(id, template),
8292
+ ...deps.ports.forNode(id, template, cwd),
8293
+ ...options.calls === void 0 ? {} : { calls: options.calls },
8294
+ emit: (event) => {
8295
+ deps.ports.owners.seen(id, event, cwd);
8296
+ if (event.type === "session.finished" && event.fork !== true) {
8297
+ const { sessionId, usage } = event.outcome;
8298
+ deps.spend.report(id, sessionId, usage.costUsd);
7736
8299
  }
7737
- };
7738
- const lanes = Math.min(forEach.maxParallel ?? slots.limit, list.length);
7739
- await Promise.all(Array.from({ length: lanes }, () => worker()));
7740
- const skipped = statuses.filter((status) => status === "skipped").length;
7741
- const ok = failures.length === 0 && skipped === 0;
7742
- const gathered = {
7743
- ok,
7744
- output: {
7745
- items: list,
7746
- results,
7747
- statuses,
7748
- count: list.length
7749
- },
7750
- ...ok ? {} : { error: whyNot(list.length, failures, skipped) },
7751
- ...!ok && deps.isStopped() ? { limited: deps.reasons[0] ?? "the run stopped" } : {}
7752
- };
7753
- return {
7754
- ...stops.ended(id, gathered),
7755
- output: gathered.output
7756
- };
8300
+ options.onEvent({
8301
+ type: "node.event",
8302
+ id,
8303
+ event
8304
+ });
8305
+ },
8306
+ signal: deps.stops.signalFor(id, template)
7757
8307
  };
8308
+ const owner = ownerPort(deps, id, template);
8309
+ if (owner !== void 0) context.owner = owner;
8310
+ if (template !== void 0) context.template = template;
8311
+ const machine = clock === void 0 ? void 0 : machinePort(options, runId, id, clock);
8312
+ if (machine !== void 0) context.machine = machine;
8313
+ if (activation.brief !== void 0) context.brief = activation.brief;
8314
+ if (activation.item !== void 0) context.item = activation.item;
8315
+ const resume = options.replay?.cutOff?.(activation) ?? resumeOf(deps.nodeOf(activation), deps.sessionIds.get(id) ?? options.replay?.previousSession?.(activation));
8316
+ if (resume !== void 0) context.resume = resume;
8317
+ if (deps.nodeOf(activation)?.type === "claude.leader") context.ownership = createOwnership({
8318
+ graph,
8319
+ leader: id,
8320
+ runChild: deps.runChild,
8321
+ isRunning: (other) => deps.running.has(other),
8322
+ checkpoint: () => options.workspace?.checkpoint(id) ?? Promise.resolve(null),
8323
+ releaseClaims: () => {
8324
+ const held = context.claims?.held() ?? [];
8325
+ for (const resource of held) context.claims?.release(resource);
8326
+ return held;
8327
+ },
8328
+ lend: deps.slots.release,
8329
+ reclaim: deps.slots.take
8330
+ });
8331
+ return context;
7758
8332
  };
7759
- // ../core/src/run/delegation.ts
7760
- /** Node types a leader can hand work to. */
7761
- const DELEGABLE = /* @__PURE__ */ new Set(["claude.leader", "claude.worker"]);
7762
- /** A child a leader delegated to: waits for a slot, runs, reports back. */
7763
- const createRunChild = (hooks) => async (child, brief) => {
7764
- await hooks.slots.take();
7765
- try {
7766
- await hooks.holds.forNode(child.id, child.template).wait("start", hooks.signal);
7767
- if (hooks.isStopped() || hooks.budgetSpent()) return failure(`not started: ${hooks.reasons[0] ?? "the run stopped"}`);
7768
- return await hooks.perform({
7769
- ...child,
7770
- input: void 0,
7771
- brief
8333
+ /** Where `id`'s questions go first, if its leader takes them (ADR-063). */
8334
+ const ownerPort = (deps, id, template) => deps.ports.owners.portFor(id, template, {
8335
+ budgetLeftUsd: deps.spend.left,
8336
+ charge: (owner, sessionId, usage) => {
8337
+ const own = deps.spend.count(owner, {
8338
+ ok: true,
8339
+ output: {},
8340
+ sessionId,
8341
+ usage
8342
+ }, false);
8343
+ deps.summary(owner).costUsd += own.usage?.costUsd ?? 0;
8344
+ },
8345
+ emit: (owner, event) => {
8346
+ deps.options.onEvent({
8347
+ type: "node.event",
8348
+ id: owner,
8349
+ event
7772
8350
  });
7773
- } finally {
7774
- hooks.slots.release();
7775
8351
  }
8352
+ });
8353
+ /**
8354
+ * The session a node that runs again continues, when it resumes (ADR-033): by
8355
+ * `session: resume`, the default for a leader; a worker starts fresh unless it
8356
+ * says `resume`.
8357
+ */
8358
+ const resumeOf = (node, sessionId) => {
8359
+ if (node === void 0 || !isClaudeNode(node)) return void 0;
8360
+ if (node.type === "claude.subagent") return void 0;
8361
+ return (node.session ?? (node.type === "claude.leader" ? "resume" : "fresh")) === "resume" ? sessionId : void 0;
7776
8362
  };
7777
- const rejected = (child, error) => ({
7778
- child,
7779
- ok: false,
7780
- output: {
7781
- status: "fail",
7782
- summary: error
7783
- },
7784
- error
8363
+ // ../core/src/run/contacts.ts
8364
+ const SESSION_TYPES$5 = /* @__PURE__ */ new Set(["claude.leader", "claude.worker"]);
8365
+ const isSession$1 = (node) => SESSION_TYPES$5.has(node?.type ?? "");
8366
+ const isTool = (node) => node?.type.startsWith("tool.") ?? false;
8367
+ const channelPeersOf = (id, channels) => channels.flatMap(([a, b]) => {
8368
+ if (a === id) return [b];
8369
+ return b === id ? [a] : [];
7785
8370
  });
7786
- const reportOf = (child, result) => {
7787
- const report = {
7788
- child,
7789
- ok: result.ok,
7790
- output: result.output
8371
+ /** Every declared node, with its owner, as a run records them. */
8372
+ const declaredNodes = (graph) => [...graph.nodes.values()].map(({ id, node }) => node.parent === void 0 ? {
8373
+ id,
8374
+ type: node.type
8375
+ } : {
8376
+ id,
8377
+ type: node.type,
8378
+ parent: node.parent
8379
+ });
8380
+ /** Whom a session may write to, by how it knows them. */
8381
+ const contactsOf = (id, nodes, channels) => {
8382
+ const byId = new Map(nodes.map((node) => [node.id, node]));
8383
+ return {
8384
+ owner: byId.get(id)?.parent,
8385
+ children: nodes.filter((node) => node.parent === id && isSession$1(node)).map((node) => node.id),
8386
+ channels: channelPeersOf(id, channels).filter((peer) => !isTool(byId.get(peer)))
7791
8387
  };
7792
- if (result.error !== void 0) report.error = result.error;
7793
- return report;
7794
8388
  };
7795
- const createOwnership = (hooks) => {
7796
- const { graph, leader } = hooks;
7797
- const owner = graph.nodes.get(leader);
7798
- const isFanned = (id) => forEachOf(graph.nodes.get(id)?.node) !== void 0;
7799
- const children = (owner?.children ?? []).flatMap((id) => {
7800
- const node = graph.nodes.get(id)?.node;
7801
- if (node === void 0 || !DELEGABLE.has(node.type) || isFanned(id)) return [];
7802
- const child = {
7803
- id,
7804
- type: node.type
7805
- };
7806
- if (node.description !== void 0) child.description = node.description;
7807
- return [child];
7808
- });
7809
- const templates = children.filter((c) => c.type === "claude.worker").map((c) => c.id);
7810
- const leaderNode = owner?.node;
7811
- const maxWorkers = leaderNode?.type === "claude.leader" ? leaderNode.maxWorkers ?? 0 : 0;
7812
- const spawned = [];
7813
- /** Runs the accepted jobs in parallel; the slot goes to them meanwhile. */
7814
- const runJobs = async (jobs) => {
7815
- const accepted = jobs.filter((j) => "brief" in j);
7816
- const notes = [];
7817
- if (accepted.length > 0) {
7818
- const problem = await hooks.checkpoint();
7819
- if (problem !== null) notes.push(`Your uncommitted work could not be committed (${problem}); the children started from your branch's last commit.`);
7820
- const released = hooks.releaseClaims().map((r) => `\`${r}\``);
7821
- if (released.length > 0) notes.push(`Your claims on ${released.join(", ")} were released while your children worked; claim again before you edit.`);
7822
- }
7823
- hooks.lend();
7824
- try {
7825
- return {
7826
- reports: await Promise.all(jobs.map(async (job) => {
7827
- if (!("brief" in job)) return job;
7828
- const child = { id: job.id };
7829
- if (job.template !== void 0) child.template = job.template;
7830
- const result = await hooks.runChild(child, {
7831
- from: leader,
7832
- text: job.brief
7833
- });
7834
- return reportOf(job.id, result);
7835
- })),
7836
- notes
7837
- };
7838
- } finally {
7839
- await hooks.reclaim();
7840
- }
7841
- };
7842
- const delegate = (tasks) => {
7843
- const seen = /* @__PURE__ */ new Set();
7844
- const jobs = tasks.map((task) => {
7845
- if (!children.some((c) => c.id === task.child)) {
7846
- const names = children.map((c) => c.id).join(", ");
7847
- return rejected(task.child, `not one of your children (${names === "" ? "you have none" : names})`);
7848
- }
7849
- if (seen.has(task.child) || hooks.isRunning(task.child)) return rejected(task.child, templates.includes(task.child) && maxWorkers > 0 ? "already running; use spawn_worker for another instance" : "already running");
7850
- seen.add(task.child);
7851
- return {
7852
- id: task.child,
7853
- brief: task.brief
7854
- };
8389
+ /** Whom a program in the tool node `id` may write to. */
8390
+ const scriptContactsOf = (id, nodes, channels) => {
8391
+ const byId = new Map(nodes.map((node) => [node.id, node]));
8392
+ const owner = byId.get(id)?.parent;
8393
+ return [.../* @__PURE__ */ new Set([...owner !== void 0 && isSession$1(byId.get(owner)) ? [owner] : [], ...channelPeersOf(id, channels).filter((peer) => isSession$1(byId.get(peer)))])];
8394
+ };
8395
+ /** Whether a program in a tool node may write to the session `id`. */
8396
+ const hearsFromScripts = (id, nodes, channels) => nodes.some((node) => isTool(node) && scriptContactsOf(node.id, nodes, channels).includes(id));
8397
+ // ../core/src/usage/spend.ts
8398
+ /** What a report would add to the session's earlier ones, counting nothing. */
8399
+ const unreported = (reported, key, total) => {
8400
+ if (total <= 0) return 0;
8401
+ const before = reported.get(key) ?? 0;
8402
+ return total < before ? total : total - before;
8403
+ };
8404
+ /**
8405
+ * What a report adds to the session's earlier ones, keeping the total in
8406
+ * `reported`. A zero says nothing, so it is ignored; a lower total is a session
8407
+ * that started counting again, so all of it is new.
8408
+ */
8409
+ const ownSpend = (reported, key, total) => {
8410
+ const own = unreported(reported, key, total);
8411
+ if (total > 0) reported.set(key, total);
8412
+ return own;
8413
+ };
8414
+ /**
8415
+ * What a run's events say it spent, for `ork resume`. The total is what its
8416
+ * nodes recorded. A session's report counts once its node finished, or when
8417
+ * `ork chat` made it outside any node: a session cut off before its node
8418
+ * finished has recorded nothing, so its next total counts the part that was cut
8419
+ * off. A leader's fork is never resumed, so its report counts when it comes,
8420
+ * whether or not its leader finished.
8421
+ */
8422
+ const earlierSpend = (events) => {
8423
+ const reported = /* @__PURE__ */ new Map();
8424
+ const open = /* @__PURE__ */ new Map();
8425
+ const forks = /* @__PURE__ */ new Map();
8426
+ let total = 0;
8427
+ for (const event of events) if (event.type === "node.started") open.set(event.id, null);
8428
+ else if (event.type === "node.finished") {
8429
+ total += event.usage?.costUsd ?? 0;
8430
+ const last = open.get(event.id);
8431
+ if (last !== null && last !== void 0) ownSpend(reported, last.key, last.cost);
8432
+ open.delete(event.id);
8433
+ } else if (event.type === "node.event" && event.event.type === "session.finished") {
8434
+ const { sessionId, usage } = event.event.outcome;
8435
+ const key = `${event.id}/${sessionId}`;
8436
+ if (event.event.fork === true) {
8437
+ const own = ownSpend(reported, key, usage.costUsd);
8438
+ total += own;
8439
+ forks.set(event.id, (forks.get(event.id) ?? 0) + own);
8440
+ } else if (open.has(event.id)) open.set(event.id, {
8441
+ key,
8442
+ cost: usage.costUsd
7855
8443
  });
7856
- return runJobs(jobs);
8444
+ else ownSpend(reported, key, usage.costUsd);
8445
+ }
8446
+ return {
8447
+ total,
8448
+ reported,
8449
+ forks
8450
+ };
8451
+ };
8452
+ // ../core/src/run/reserves.ts
8453
+ const createReserves = () => {
8454
+ const held = /* @__PURE__ */ new Map();
8455
+ const waiters = /* @__PURE__ */ new Set();
8456
+ const wake = () => {
8457
+ const woken = [...waiters];
8458
+ waiters.clear();
8459
+ for (const resolve of woken) resolve();
7857
8460
  };
7858
- const nextInstance = (template) => {
7859
- for (let n = 2;; n += 1) {
7860
- const id = `${template}-${String(n)}`;
7861
- if (!graph.nodes.has(id) && !spawned.includes(id)) return id;
8461
+ return {
8462
+ /** All that is set aside now. */
8463
+ total: () => [...held.values()].reduce((sum, reserve) => sum + reserve.usd, 0),
8464
+ /**
8465
+ * `id` sets aside `usd` (nothing, or 0, lets it go). `waitable` is false
8466
+ * for a leader, whose children must never wait on it.
8467
+ */
8468
+ reserve: (id, usd, waitable) => {
8469
+ const before = held.get(id)?.usd ?? 0;
8470
+ if (usd === void 0 || usd <= 0) held.delete(id);
8471
+ else held.set(id, {
8472
+ usd,
8473
+ waitable
8474
+ });
8475
+ if ((usd ?? 0) < before) wake();
8476
+ },
8477
+ /** The node ended: whatever it still held is let go. */
8478
+ drop: (id) => {
8479
+ if (held.delete(id)) wake();
8480
+ },
8481
+ /**
8482
+ * Waits until a reserve shrinks. `false` at once when no other node holds
8483
+ * one worth waiting on, or when `signal` aborts.
8484
+ */
8485
+ freed: (id, signal) => {
8486
+ if (![...held].some(([other, reserve]) => other !== id && reserve.waitable) || signal.aborted) return Promise.resolve(false);
8487
+ return new Promise((resolve) => {
8488
+ const done = (freed) => () => {
8489
+ waiters.delete(onFree);
8490
+ signal.removeEventListener("abort", onAbort);
8491
+ resolve(freed);
8492
+ };
8493
+ const onFree = done(true);
8494
+ const onAbort = done(false);
8495
+ waiters.add(onFree);
8496
+ signal.addEventListener("abort", onAbort, { once: true });
8497
+ });
7862
8498
  }
7863
8499
  };
7864
- const spawn = (tasks) => {
7865
- const jobs = tasks.map((task) => {
7866
- if (!templates.includes(task.template)) {
7867
- const names = templates.join(", ");
7868
- return rejected(task.template, `not one of your child workers (${names === "" ? "you have none" : names})`);
7869
- }
7870
- if (spawned.length >= maxWorkers) return rejected(task.template, `no workers left to spawn (maxWorkers: ${String(maxWorkers)})`);
7871
- const id = nextInstance(task.template);
7872
- spawned.push(id);
7873
- return {
7874
- id,
7875
- template: task.template,
7876
- brief: task.brief
7877
- };
7878
- });
7879
- return runJobs(jobs);
8500
+ };
8501
+ // ../core/src/run/spending.ts
8502
+ /**
8503
+ * A result with only what this activation spent (ADR-046): a session's report
8504
+ * holds its total so far, while its tokens are this process's own. `reported`
8505
+ * holds each session's total.
8506
+ */
8507
+ const withOwnSpend = (id, result, reported) => {
8508
+ const { sessionId, usage } = result;
8509
+ if (sessionId === void 0 || usage === void 0) return result;
8510
+ const own = {
8511
+ ...usage,
8512
+ costUsd: ownSpend(reported, `${id}/${sessionId}`, usage.costUsd)
7880
8513
  };
7881
8514
  return {
7882
- children,
7883
- fanned: (owner?.children ?? []).filter((id) => isFanned(id)),
7884
- templates,
7885
- workersLeft: () => Math.max(0, maxWorkers - spawned.length),
7886
- delegate,
7887
- spawn
8515
+ ...result,
8516
+ usage: own,
8517
+ output: "usage" in result.output ? {
8518
+ ...result.output,
8519
+ usage: own
8520
+ } : result.output
7888
8521
  };
7889
8522
  };
7890
- // ../core/src/run/scope.ts
8523
+ /** A run's spending so far; a resumed run starts from what it had spent. */
8524
+ const createSpend = (budgetUsd, earlier, limits) => {
8525
+ const reported = new Map(earlier?.reported);
8526
+ let spent = earlier?.total ?? 0;
8527
+ /** What running sessions last reported, not yet in `spent`, by `node/session`. */
8528
+ const inFlight = /* @__PURE__ */ new Map();
8529
+ const committed = () => spent + [...inFlight.values()].reduce((sum, cost) => sum + cost, 0);
8530
+ /** Caps running sessions were given (ADR-077): out of what is offered. */
8531
+ const reserves = createReserves();
8532
+ const budgetLeft = () => budgetUsd === void 0 ? void 0 : budgetUsd - committed() - reserves.total();
8533
+ return {
8534
+ total: () => spent,
8535
+ /** A node sets aside its session's cap; nothing or 0 lets it go. */
8536
+ reserve: reserves.reserve,
8537
+ /** Waits until another node's reserve frees; `false` when none can. */
8538
+ freed: reserves.freed,
8539
+ /** What a session has spent in this activation, from its total so far. */
8540
+ ownOf: (id, sessionId, total) => unreported(reported, `${id}/${sessionId}`, total),
8541
+ /** A running session said what it has spent so far. */
8542
+ report: (id, sessionId, total) => {
8543
+ const key = `${id}/${sessionId}`;
8544
+ inFlight.set(key, unreported(reported, key, total));
8545
+ },
8546
+ /** The node ended: `count` has what its sessions last reported. */
8547
+ settled: (id) => {
8548
+ for (const key of inFlight.keys()) if (key.startsWith(`${id}/`)) inFlight.delete(key);
8549
+ reserves.drop(id);
8550
+ },
8551
+ /** What a session may spend: the least of the budget and any quota. */
8552
+ left: () => {
8553
+ const known = [budgetLeft(), limits?.().leftUsd].filter((left) => left !== void 0);
8554
+ return known.length === 0 ? void 0 : Math.min(...known);
8555
+ },
8556
+ budgetLeft,
8557
+ /** What the limits across runs allow now. */
8558
+ quota: () => limits?.() ?? {},
8559
+ /**
8560
+ * What stops anything new from starting: a quota first, as a budget can be
8561
+ * raised.
8562
+ */
8563
+ spentOut: () => {
8564
+ const blocked = limits?.().blocked;
8565
+ if (blocked !== void 0) return {
8566
+ status: "quota_exhausted",
8567
+ reason: blocked
8568
+ };
8569
+ return budgetUsd !== void 0 && committed() >= budgetUsd ? {
8570
+ status: "budget_exhausted",
8571
+ reason: `the run's budget of $${String(budgetUsd)} is spent`
8572
+ } : null;
8573
+ },
8574
+ /**
8575
+ * Counts a result, and gives it back with only its own spend. A replayed
8576
+ * result is as stored, and already in what the run started from.
8577
+ */
8578
+ count: (id, result, replayed) => {
8579
+ if (replayed) return result;
8580
+ const own = withOwnSpend(id, result, reported);
8581
+ spent += own.usage?.costUsd ?? 0;
8582
+ return own;
8583
+ }
8584
+ };
8585
+ };
8586
+ /** A run's spending, from what the scheduler is given. */
8587
+ const spendOf = (budgetUsd, options) => createSpend(budgetUsd, options.replay?.earlier, options.limits);
8588
+ // ../core/src/run/gather.ts
8589
+ const asList = (value) => Array.isArray(value) ? value : [];
8590
+ /** `before` then `now`, leaving out an entry already there. */
8591
+ const joined = (before, now) => {
8592
+ const seen = /* @__PURE__ */ new Set();
8593
+ const out = [];
8594
+ for (const entry of [...asList(before), ...asList(now)]) {
8595
+ const key = JSON.stringify(entry);
8596
+ if (seen.has(key)) continue;
8597
+ seen.add(key);
8598
+ out.push(entry);
8599
+ }
8600
+ return out;
8601
+ };
7891
8602
  /**
7892
- * @file A run's id and folder, and the scope templates and `when:` conditions
7893
- * read during it.
8603
+ * `result` with each gathered field holding every round's entries so far.
8604
+ * `previous` is the node's output from its last round, already gathered.
7894
8605
  */
7895
- /** The run's id (given, or a new one) and its `.ork/runs/<run>` folder. */
7896
- const runPlace = (options) => {
7897
- const runId = options.runId ?? randomUUID().slice(0, 8);
8606
+ const joinRounds = (fields, previous, result) => {
8607
+ if (fields.length === 0 || previous === void 0) return result;
8608
+ const output = { ...result.output };
8609
+ for (const field of fields) output[field] = joined(previous[field], result.output[field]);
7898
8610
  return {
7899
- runId,
7900
- runDir: options.runDir ?? path.join(options.cwd, ".ork", "runs", runId)
8611
+ ...result,
8612
+ output
7901
8613
  };
7902
8614
  };
8615
+ /** `result` with `node`'s `output.gather` fields joined to its earlier rounds. */
8616
+ const gatherRounds = (node, previous, result) => joinRounds(gatheredFieldsOf(node), previous, result);
7903
8617
  /**
7904
- * The folders every node of a run reads from: the workflow's, the project's,
7905
- * Ork's.
8618
+ * What `activation`'s rounds reported so far, to gather onto. A spawned
8619
+ * instance gathers nothing earlier: its id (`impl-2`) is reused by a different
8620
+ * worker in the leader's next round. A delegated child that hasn't run since a
8621
+ * resume starts from what it reported before it, since its leader's recorded
8622
+ * rounds are handed back whole and never run it again.
7906
8623
  */
7907
- const foldersOf = (options) => ({
7908
- workflowDir: options.workflowDir ?? options.cwd,
7909
- projectRoot: options.projectRoot ?? options.cwd,
7910
- ...options.orkDir === void 0 ? {} : { orkDir: options.orkDir }
8624
+ const previousOf = (activation, current, replay) => {
8625
+ const { brief, template, id } = activation;
8626
+ if (brief !== void 0 && template !== void 0) return void 0;
8627
+ if (current !== void 0 || brief === void 0) return current;
8628
+ return replay?.lastOutput?.(id);
8629
+ };
8630
+ /** Gathers each activation's result onto what its node reported before. */
8631
+ const createGatherer = (outputs, replay, nodeOf) => (activation, result) => gatherRounds(nodeOf(activation), previousOf(activation, outputs[activation.id], replay), result);
8632
+ // ../core/src/run/results.ts
8633
+ /** Control nodes pass their input through; a join's input is its output. */
8634
+ const passThrough = (input) => ({
8635
+ output: input ?? {},
8636
+ ok: true
7911
8637
  });
7912
- /** What templates and `when:` read during a run. */
7913
- const runScope = (inputs, outputs, runId, item) => {
7914
- const scope = {
7915
- inputs,
7916
- nodes: Object.fromEntries(Object.entries(outputs).map(([id, output]) => [id, { output }]))
8638
+ const recordRun = (entry, result) => {
8639
+ entry.runs += 1;
8640
+ entry.ok = result.ok;
8641
+ entry.output = result.output;
8642
+ entry.costUsd += result.usage?.costUsd ?? 0;
8643
+ if (result.error === void 0) delete entry.error;
8644
+ else entry.error = result.error;
8645
+ if (result.ok) delete entry.end;
8646
+ else entry.end = endOf(result);
8647
+ };
8648
+ const startedEvent = (activation, nodeType, round, agent) => {
8649
+ const event = {
8650
+ type: "node.started",
8651
+ id: activation.id,
8652
+ nodeType,
8653
+ round
7917
8654
  };
7918
- if (runId !== void 0) scope.run = { id: runId };
7919
- if (item !== void 0) {
7920
- scope.item = item.value;
7921
- scope.index = item.index;
7922
- }
7923
- return scope;
8655
+ if (activation.brief !== void 0) event.owner = activation.brief.from;
8656
+ if (agent !== void 0) event.agent = agent;
8657
+ const { item } = activation;
8658
+ if (item !== void 0) event.item = {
8659
+ of: item.of,
8660
+ index: item.index
8661
+ };
8662
+ return event;
7924
8663
  };
7925
- // ../core/src/run/context.ts
7926
- /**
7927
- * @file One activation's execute context: what the scheduler hands a node's
7928
- * executor. Its inputs and the outputs so far, where it works, the budget
7929
- * left, the human, its hold and its own stop signal (ADR-041), its ports on
7930
- * the bus, the session it resumes, and, for a leader, its ownership of its
7931
- * children.
7932
- */
7933
- const nodeContext = (deps, activation, cwd, round) => {
7934
- const { options, runId } = deps;
7935
- const { graph } = options;
7936
- const { id, template } = activation;
7937
- const context = {
7938
- runId,
7939
- id,
7940
- workflow: graph.workflow,
7941
- inputs: options.inputs,
7942
- outputs: deps.outputs,
7943
- input: activation.input,
8664
+ const finishedEvent = (id, round, result) => {
8665
+ const event = {
8666
+ type: "node.finished",
8667
+ id,
7944
8668
  round,
7945
- cwd,
7946
- runDir: deps.runDir,
7947
- ...foldersOf(options),
7948
- budgetLeftUsd: deps.spend.left(),
7949
- budgetLeftNow: deps.spend.budgetLeft,
7950
- reserveBudget: (usd) => {
7951
- deps.spend.reserve(id, usd, deps.nodeOf(activation)?.type !== "claude.leader");
7952
- },
7953
- budgetFreed: (signal) => deps.spend.freed(id, signal),
7954
- quota: deps.spend.quota,
7955
- ownSpend: (sessionId, total) => deps.spend.ownOf(id, sessionId, total),
7956
- human: deps.human,
7957
- hold: deps.holds.forNode(id, template),
7958
- ...deps.ports.forNode(id, template, cwd),
7959
- ...options.calls === void 0 ? {} : { calls: options.calls },
7960
- emit: (event) => {
7961
- deps.ports.owners.seen(id, event, cwd);
7962
- if (event.type === "session.finished" && event.fork !== true) {
7963
- const { sessionId, usage } = event.outcome;
7964
- deps.spend.report(id, sessionId, usage.costUsd);
7965
- }
7966
- options.onEvent({
7967
- type: "node.event",
7968
- id,
7969
- event
7970
- });
7971
- },
7972
- signal: deps.stops.signalFor(id, template)
8669
+ ok: result.ok,
8670
+ output: result.output
7973
8671
  };
7974
- const owner = ownerPort(deps, id, template);
7975
- if (owner !== void 0) context.owner = owner;
7976
- if (template !== void 0) context.template = template;
7977
- if (activation.brief !== void 0) context.brief = activation.brief;
7978
- if (activation.item !== void 0) context.item = activation.item;
7979
- const resume = options.replay?.cutOff?.(activation) ?? resumeOf(deps.nodeOf(activation), deps.sessionIds.get(id) ?? options.replay?.previousSession?.(activation));
7980
- if (resume !== void 0) context.resume = resume;
7981
- if (deps.nodeOf(activation)?.type === "claude.leader") context.ownership = createOwnership({
7982
- graph,
7983
- leader: id,
7984
- runChild: deps.runChild,
7985
- isRunning: (other) => deps.running.has(other),
7986
- checkpoint: () => options.workspace?.checkpoint(id) ?? Promise.resolve(null),
7987
- releaseClaims: () => {
7988
- const held = context.claims?.held() ?? [];
7989
- for (const resource of held) context.claims?.release(resource);
7990
- return held;
7991
- },
7992
- lend: deps.slots.release,
7993
- reclaim: deps.slots.take
7994
- });
7995
- return context;
8672
+ if (result.usage !== void 0) event.usage = result.usage;
8673
+ if (result.error !== void 0) event.error = result.error;
8674
+ event.end = endOf(result);
8675
+ return event;
7996
8676
  };
7997
- /** Where `id`'s questions go first, if its leader takes them (ADR-063). */
7998
- const ownerPort = (deps, id, template) => deps.ports.owners.portFor(id, template, {
7999
- budgetLeftUsd: deps.spend.left,
8000
- charge: (owner, sessionId, usage) => {
8001
- const own = deps.spend.count(owner, {
8002
- ok: true,
8003
- output: {},
8004
- sessionId,
8005
- usage
8006
- }, false);
8007
- deps.summary(owner).costUsd += own.usage?.costUsd ?? 0;
8677
+ const failure = (error) => ({
8678
+ output: {
8679
+ status: "fail",
8680
+ summary: error
8008
8681
  },
8009
- emit: (owner, event) => {
8010
- deps.options.onEvent({
8011
- type: "node.event",
8012
- id: owner,
8013
- event
8014
- });
8015
- }
8682
+ ok: false,
8683
+ error
8684
+ });
8685
+ /** The node's entry in the run summary, created on first use. */
8686
+ const summaryOf = (summaries, id) => {
8687
+ const existing = summaries.get(id);
8688
+ if (existing !== void 0) return existing;
8689
+ const created = {
8690
+ id,
8691
+ runs: 0,
8692
+ ok: null,
8693
+ output: null,
8694
+ costUsd: 0
8695
+ };
8696
+ summaries.set(id, created);
8697
+ return created;
8698
+ };
8699
+ /** A run's summary; a resumed run's leaders keep their forks' spend (ADR-063). */
8700
+ const createSummaries = (earlier) => {
8701
+ const summaries = /* @__PURE__ */ new Map();
8702
+ const summary = (id) => summaryOf(summaries, id);
8703
+ for (const [id, costUsd] of earlier?.forks ?? []) summary(id).costUsd += costUsd;
8704
+ return {
8705
+ summaries,
8706
+ summary
8707
+ };
8708
+ };
8709
+ /** The tighter of the workflow's budget and what a caller left it, if any. */
8710
+ const runBudget = (graph, settings) => {
8711
+ const limits = [graph.workflow.limits?.budgetUsd, settings?.budgetCap].filter((limit) => limit !== void 0);
8712
+ return limits.length === 0 ? {} : { budgetUsd: Math.min(...limits) };
8713
+ };
8714
+ /** The `run.started` event: the workflow, its entries, nodes and inputs. */
8715
+ const runStartedEvent = (graph, runId, options) => ({
8716
+ type: "run.started",
8717
+ runId,
8718
+ workflow: graph.workflow.name,
8719
+ entries: graph.entries,
8720
+ nodes: declaredNodes(graph),
8721
+ edges: graph.edges.map(({ from, to, when, maxRounds }) => ({
8722
+ from,
8723
+ to,
8724
+ ...when === void 0 ? {} : { when },
8725
+ ...maxRounds === void 0 ? {} : { maxRounds }
8726
+ })),
8727
+ channels: graph.channels,
8728
+ inputs: options.inputs,
8729
+ ...options.settings === void 0 ? {} : { settings: options.settings },
8730
+ ...runBudget(graph, options.settings),
8731
+ ...options.orkVersion === void 0 ? {} : { ork: options.orkVersion }
8016
8732
  });
8733
+ /** The unread mail of nodes that never ran; a node that ran saw its notice. */
8734
+ const unreadOf = (nodes, unread) => {
8735
+ const ran = new Set(nodes.filter((node) => node.runs > 0).map((node) => node.id));
8736
+ const left = [...unread ?? []].filter(([node]) => !ran.has(node)).map(([node, messages]) => ({
8737
+ node,
8738
+ messages
8739
+ }));
8740
+ return left.length === 0 ? {} : { unread: left };
8741
+ };
8017
8742
  /**
8018
- * The session a node that runs again continues, when it resumes (ADR-033): by
8019
- * `session: resume`, the default for a leader; a worker starts fresh unless it
8020
- * says `resume`.
8743
+ * The run's result: failed when any reason was given, unless it stopped. A
8744
+ * skipped node or a handled failure leaves it `succeeded`, and is counted.
8021
8745
  */
8022
- const resumeOf = (node, sessionId) => {
8023
- if (node === void 0 || !isClaudeNode(node)) return void 0;
8024
- if (node.type === "claude.subagent") return void 0;
8025
- return (node.session ?? (node.type === "claude.leader" ? "resume" : "fresh")) === "resume" ? sessionId : void 0;
8026
- };
8746
+ const runResult = (run) => ({
8747
+ runId: run.runId,
8748
+ status: run.stopped ?? (run.reasons.length > 0 ? "failed" : "succeeded"),
8749
+ costUsd: run.costUsd,
8750
+ nodes: run.nodes,
8751
+ reasons: run.reasons,
8752
+ worktrees: run.worktrees,
8753
+ ...run.outputs === void 0 ? {} : { outputs: run.outputs },
8754
+ skipped: run.nodes.filter((node) => node.runs === 0).length,
8755
+ failuresHandled: run.failuresHandled,
8756
+ ...unreadOf(run.nodes, run.unread)
8757
+ });
8758
+ /** A workflow's `outputs`, rendered as the run ends. */
8759
+ const renderOutputs = (outputs, scope) => outputs === void 0 ? void 0 : Object.fromEntries(Object.entries(outputs).map(([name, text]) => {
8760
+ const template = parseTemplate(text);
8761
+ return [name, template.ok ? renderTemplate(template.parts, scope) : text];
8762
+ }));
8763
+ /** Closes the run's worktrees; a failure is one more reason, not a crash. */
8764
+ const closeWorkspace = (workspace, reasons) => (workspace?.close() ?? Promise.resolve([])).catch((error) => {
8765
+ reasons.push(`cleaning up worktrees: ${String(error)}`);
8766
+ return [];
8767
+ });
8027
8768
  // ../core/src/store/nest-items.ts
8028
8769
  /** Whether a tree node is an item of a fanned node (`build.2`). */
8029
8770
  const isItem = (node) => node.template !== void 0 && /\.\d+$/u.test(node.id);
@@ -8307,6 +9048,13 @@ const runTree = (events) => {
8307
9048
  }
8308
9049
  return roots;
8309
9050
  };
9051
+ /** The nodes of a run waiting for a machine-wide slot now. */
9052
+ const slotWaitsOf = (events) => {
9053
+ const waiting = /* @__PURE__ */ new Set();
9054
+ for (const event of events) if (event.type === "slot.waiting") waiting.add(event.id);
9055
+ else if (event.type === "slot.taken" || event.type === "node.finished") waiting.delete(event.id);
9056
+ return [...waiting];
9057
+ };
8310
9058
  /** Folds a run's `ork pause` and `ork continue` commands (ADR-041). */
8311
9059
  const pausedOf = (events) => {
8312
9060
  let run = false;
@@ -8499,7 +9247,10 @@ const createStops = (run) => {
8499
9247
  own.delete(id);
8500
9248
  return stopped.delete(id) ? {
8501
9249
  ...result,
8502
- ...failure("stopped by the user")
9250
+ ...failedWith(endFor("interrupted"), {
9251
+ status: "fail",
9252
+ summary: "stopped by the user"
9253
+ })
8503
9254
  } : result;
8504
9255
  }
8505
9256
  };
@@ -8569,6 +9320,8 @@ const createClock = (options) => {
8569
9320
  else resting.delete(id);
8570
9321
  changed();
8571
9322
  },
9323
+ /** Stands the node's clock (and its owners' and the run's) still for `wait`. */
9324
+ waitFor: asking,
8572
9325
  /** The same channel, noting who waits on the user for how long. */
8573
9326
  human: (channel) => ({
8574
9327
  permit: (request) => asking(request.nodeId, () => channel.permit(request)),
@@ -8588,18 +9341,17 @@ const createClock = (options) => {
8588
9341
  };
8589
9342
  };
8590
9343
  const timedOut = (timeout, partial) => {
8591
- const error = `timed out after ${timeout}`;
8592
- return {
9344
+ const end = endFor("timed_out", { timeout });
9345
+ return endedWith({
8593
9346
  ...partial,
8594
9347
  ok: false,
8595
- error,
8596
9348
  output: {
8597
9349
  ...partial?.output,
8598
9350
  status: "fail",
8599
- summary: error,
9351
+ summary: end.message,
8600
9352
  timedOut: true
8601
9353
  }
8602
- };
9354
+ }, end);
8603
9355
  };
8604
9356
  /** Runs `start` under `timeout`, if it has one. */
8605
9357
  const withTimeout = async (bound) => {
@@ -8650,9 +9402,28 @@ const watchRunTimeout = (watched) => {
8650
9402
  cancelled.abort();
8651
9403
  };
8652
9404
  };
9405
+ // ../core/src/run/watch.ts
9406
+ /**
9407
+ * @file A `watch:` on a tool or MCP node (RFC-011): the node's look, repeated
9408
+ * until its condition holds, with no model turn. The last look is its output.
9409
+ * A failed look, such as a network error, is tried again up to `misses` in a
9410
+ * row; an answer the node didn't declare fails it at once, as waiting can't
9411
+ * mend that. The time between looks isn't work, so a pause keeps it and the
9412
+ * run's clock stands still. It records what changed, not every look.
9413
+ */
9414
+ /**
9415
+ * The ends of a failed look that the next look may mend besides a transient
9416
+ * kind: what a tool's non-zero exit or an MCP call's error ends as.
9417
+ */
9418
+ const LOOKS_AGAIN = /* @__PURE__ */ new Set([
9419
+ "command_failed",
9420
+ "auth",
9421
+ "tool_missing",
9422
+ "interrupted"
9423
+ ]);
8653
9424
  const SAID_LIMIT = 1024;
8654
9425
  /** What a look says, for telling whether it changed and showing it. */
8655
- const saidBy = (result, declared) => {
9426
+ const saidBy$1 = (result, declared) => {
8656
9427
  const { json, exitCode, stdout, text } = result.output;
8657
9428
  const value = declared !== void 0 && typeof json === "object" && json !== null ? Object.fromEntries(Object.keys(declared).map((name) => [name, Reflect.get(json, name)])) : json ?? {
8658
9429
  exitCode,
@@ -8685,7 +9456,7 @@ const leftBehind = (signal, graceMs) => {
8685
9456
  const runWatch = async (options) => {
8686
9457
  const { id, watch, declared, look, hold, signal, graceMs, onEvent } = options;
8687
9458
  const until = parseExpr(watch.until);
8688
- if (!until.ok) return failure(`watch.until: ${until.message}`);
9459
+ if (!until.ok) return failedWith(endFor("invalid_node", { reason: `watch.until: ${until.message}` }));
8689
9460
  const every = options.everyMs ?? timeoutMs(watch.every) ?? 0;
8690
9461
  const misses = watch.misses ?? 3;
8691
9462
  const limit = new AbortController();
@@ -8706,17 +9477,16 @@ const runWatch = async (options) => {
8706
9477
  };
8707
9478
  const expiredResult = () => {
8708
9479
  const error = `watched for ${watch.timeout}, and ${markValue(watch.until)} never held`;
8709
- return {
9480
+ return endedWith({
8710
9481
  ...last,
8711
9482
  ok: false,
8712
- error,
8713
9483
  output: {
8714
9484
  ...last?.output,
8715
9485
  status: "fail",
8716
9486
  summary: error,
8717
9487
  timedOut: true
8718
9488
  }
8719
- };
9489
+ }, endFor("timed_out", { timeout: watch.timeout }, error));
8720
9490
  };
8721
9491
  /** Stands still while paused, and records the hold at once. */
8722
9492
  const rest = async () => {
@@ -8761,11 +9531,14 @@ const runWatch = async (options) => {
8761
9531
  for (;;) {
8762
9532
  await hold?.wait("start", active);
8763
9533
  const result = active.aborted ? null : await lookOnce();
8764
- if (signal.aborted) return ended("stopped", failure("stopped while watching"));
9534
+ if (signal.aborted) return ended("stopped", failedWith(endFor("interrupted"), {
9535
+ status: "fail",
9536
+ summary: "stopped while watching"
9537
+ }));
8765
9538
  if (limit.signal.aborted || result === null) return ended("timed_out", expiredResult());
8766
9539
  looks += 1;
8767
9540
  last = result;
8768
- const said = saidBy(result, declared);
9541
+ const said = saidBy$1(result, declared);
8769
9542
  if (said !== lastSaid) {
8770
9543
  lastSaid = said;
8771
9544
  onEvent({
@@ -8781,7 +9554,7 @@ const runWatch = async (options) => {
8781
9554
  missed = 0;
8782
9555
  if (holds(result.output)) return ended("met", result);
8783
9556
  } else {
8784
- if (!(result.transient === true || result.output["timedOut"] === true)) return ended("failed", result);
9557
+ if (!(result.transient === true || LOOKS_AGAIN.has(result.end?.kind) || result.output["timedOut"] === true)) return ended("failed", result);
8785
9558
  missed += 1;
8786
9559
  if (missed >= misses) return ended("failed", {
8787
9560
  ...result,
@@ -8798,19 +9571,36 @@ const runWatch = async (options) => {
8798
9571
  const repeatsCall = (event) => event.type === "tool.called" || event.type === "tool.result";
8799
9572
  // ../core/src/run/execute.ts
8800
9573
  /** Runs an activation; control nodes pass their input through. */
8801
- const createExecute = (deps) => async (activation, round) => {
9574
+ const saidBy = (error) => error instanceof Error ? error.message : String(error);
9575
+ /**
9576
+ * A thrown error's end: a failed git or missing program by its text, else
9577
+ * `fallback`.
9578
+ */
9579
+ const thrown = (error, fallback) => {
9580
+ const text = saidBy(error);
9581
+ const read = endOf({
9582
+ ok: false,
9583
+ output: {},
9584
+ error: text
9585
+ });
9586
+ return failedWith(read.kind === "unknown" ? endFor(fallback, { reason: text }, text) : read);
9587
+ };
9588
+ /** Runs an activation, its end's detail scrubbed of the run's paths. */
9589
+ const createExecute = (deps) => async (activation, round) => scrubbedEnd(await executeOne(deps, activation, round), [deps.options.cwd, deps.options.projectRoot ?? ""]);
9590
+ const executeOne = async (deps, activation, round) => {
8802
9591
  const { options, contextDeps, clock, graceMs, nodeOf } = deps;
8803
9592
  const node = nodeOf(activation);
8804
- if (node === void 0) return failure(`unknown node ${activation.id}`);
9593
+ if (node === void 0) return failedWith(endFor("unknown", {}, `unknown node ${activation.id}`));
8805
9594
  if (node.type.startsWith("control.")) return passThrough(activation.input);
8806
9595
  const executor = options.executors[node.type];
8807
- if (executor === void 0) return failure(`no executor for ${node.type}`);
9596
+ if (executor === void 0) return failedWith(endFor("unknown", {}, `no executor for ${node.type}`));
8808
9597
  try {
8809
9598
  const forEach = activation.item === void 0 ? forEachOf(node) : void 0;
8810
9599
  if (forEach !== void 0) return await deps.fanOut(activation, forEach, round);
8811
9600
  const { workspace } = options;
8812
- const place = await workspace?.prepare(activation.id, activation.template) ?? { cwd: options.cwd };
8813
- const context = nodeContext(contextDeps, activation, place.cwd, round);
9601
+ const place = await (workspace?.prepare(activation.id, activation.template) ?? Promise.resolve({ cwd: options.cwd })).catch((error) => thrown(error, "invalid_node"));
9602
+ if ("ok" in place) return place;
9603
+ const context = nodeContext(contextDeps, activation, place.cwd, round, clock);
8814
9604
  const once = (signal, emit) => withTimeout({
8815
9605
  timeout: timeoutOf(node, options.graph.workflow),
8816
9606
  clock: clock.forNode(activation.id),
@@ -8842,9 +9632,10 @@ const createExecute = (deps) => async (activation, round) => {
8842
9632
  },
8843
9633
  everyMs: options.watchEveryMs
8844
9634
  });
8845
- return await workspace?.finish(activation.id, result) ?? result;
9635
+ const ended = result.ok || !context.signal.aborted ? result : endedWith(result, stoppedEnd(context.signal));
9636
+ return await workspace?.finish(activation.id, ended) ?? ended;
8846
9637
  } catch (error) {
8847
- return failure(error instanceof Error ? error.message : String(error));
9638
+ return thrown(error, "unknown");
8848
9639
  }
8849
9640
  };
8850
9641
  // ../core/src/run/bus-calls.ts
@@ -9358,7 +10149,7 @@ const failed = (id, why) => {
9358
10149
  return reason === "" ? `${id} failed` : `${id} failed: ${reason}`;
9359
10150
  };
9360
10151
  /** A program that ran says how it exited; anything else has its own reason. */
9361
- const toolLine = (id, result) => {
10152
+ const toolLine$1 = (id, result) => {
9362
10153
  const { output, ok, error } = result;
9363
10154
  const code = output["exitCode"];
9364
10155
  if (!(typeof code === "number" && code >= 0 && output["timedOut"] !== true && (ok || code !== 0))) return ok ? `${id} passed` : failed(id, error ?? "");
@@ -9368,7 +10159,7 @@ const toolLine = (id, result) => {
9368
10159
  /** Ork's own line about a result, when a rule gives no body. */
9369
10160
  const defaultRuleBody = (id, node, result) => {
9370
10161
  const { output, ok } = result;
9371
- if (node !== void 0 && isToolNode(node)) return toolLine(id, result);
10162
+ if (node !== void 0 && isToolNode(node)) return toolLine$1(id, result);
9372
10163
  const summary = output["summary"];
9373
10164
  const said = typeof summary === "string" ? summary : "";
9374
10165
  if (!ok) return failed(id, result.error ?? said);
@@ -9636,7 +10427,7 @@ const startRun = (options) => {
9636
10427
  expire: (reason) => {
9637
10428
  stop("timed_out", reason);
9638
10429
  options.calls?.stopAll();
9639
- abort.abort();
10430
+ abort.abort(runTimedOut(limits.timeout));
9640
10431
  settle();
9641
10432
  },
9642
10433
  leave: () => {
@@ -10816,12 +11607,13 @@ const parseReport = (value) => {
10816
11607
  // ../core/src/run/brief.ts
10817
11608
  /**
10818
11609
  * @file What a Claude node is asked: its brief, rendered against the run's
10819
- * inputs and outputs, and its `claude.subagent` children as Claude Code
10820
- * subagents of its session (the SDK's `agents`, the CLI's `--agents`). A
10821
- * subagent's role (its agent's instructions), `instructions` and `prompt` are
10822
- * its system prompt, and its `description` says when the session hands it
10823
- * work with the Agent tool; it runs inside that session, with no process,
10824
- * worktree or edges of its own.
11610
+ * inputs and outputs, the model and effort it runs on (a fanned node's from
11611
+ * its item), and its `claude.subagent` children as Claude Code subagents of
11612
+ * its session (the SDK's `agents`, the CLI's `--agents`). A subagent's role
11613
+ * (its agent's instructions), `instructions` and `prompt` are its system
11614
+ * prompt, and its `description` says when the session hands it work with the
11615
+ * Agent tool; it runs inside that session, with no process, worktree or edges
11616
+ * of its own.
10825
11617
  */
10826
11618
  /** The tool a session hands a subagent work with. */
10827
11619
  const AGENT_TOOL = "Agent";
@@ -10850,6 +11642,23 @@ const renderBrief = (text, inputs, outputs, before, item) => {
10850
11642
  warnings
10851
11643
  };
10852
11644
  };
11645
+ /**
11646
+ * The model and effort a session asks for: the node's own, rendered with its
11647
+ * item on a fanned node (`{{item.model}}`), else the workflow's defaults. An
11648
+ * item's value that is no model name or effort level fails it.
11649
+ */
11650
+ const modelOf = (node, defaults, run) => {
11651
+ const scope = runScope(run.inputs, run.outputs ?? {}, void 0, run.item);
11652
+ const model = itemSetting("model", node.model, scope);
11653
+ if (!model.ok) return model;
11654
+ const effort = itemEffort(node.effort, scope);
11655
+ if (!effort.ok) return effort;
11656
+ return {
11657
+ ok: true,
11658
+ model: model.value ?? defaults.model,
11659
+ effort: effort.value ?? defaults.effort
11660
+ };
11661
+ };
10853
11662
  const rendered = (text, inputs, outputs) => {
10854
11663
  const brief = renderBrief(text, inputs, outputs);
10855
11664
  return brief.ok ? brief.text : (text ?? "").trim();
@@ -10929,7 +11738,7 @@ const mailboxInstructions = (mailbox, owner) => {
10929
11738
  const verifyInstructions = (node) => {
10930
11739
  if (!("verify" in node) || node.verify === void 0) return [];
10931
11740
  const { command } = node.verify;
10932
- return [`- A report that is not \`fail\` or \`changes_requested\` is accepted only if ${command === void 0 || command.includes("{{") ? "a check the workflow defines" : `\`${command}\``} passes when you report. If it fails, you are told why and asked to fix it, so run it yourself first if you can.`];
11741
+ return [`- A report that is not \`fail\` or \`changes_requested\` is accepted only if ${command === void 0 || command.includes("{{") ? "a check the workflow defines" : `\`${command}\``} passes when you report. If it fails, you are told why and asked to fix it, ${(command !== void 0 && node.tools?.deny?.includes(heavyRule(command))) === true ? "so run targeted checks (such as the tests you changed) first: Ork runs the full check, and this session may not." : "so run it yourself first if you can."}`];
10933
11742
  };
10934
11743
  /** Ork's own instructions for a node, kept short: they cost tokens every turn. */
10935
11744
  const harnessInstructions = (workflow, id, node, options = {}) => {
@@ -11026,6 +11835,11 @@ const planNodeSession = (workflow, id, options) => {
11026
11835
  ok: false,
11027
11836
  error: `\`${id}\` uses worktree isolation, which a run on its own does not set up; run the whole workflow, or pass \`--in-place\` to run it in this checkout`
11028
11837
  };
11838
+ const models = modelOf(node, defaults, options);
11839
+ if (!models.ok) return {
11840
+ ok: false,
11841
+ error: models.error
11842
+ };
11029
11843
  const warnings = [];
11030
11844
  const brief = renderBrief(node.prompt, options.inputs, options.outputs, options.before, options.item);
11031
11845
  if (!brief.ok) return {
@@ -11073,10 +11887,8 @@ const planNodeSession = (workflow, id, options) => {
11073
11887
  spec.agents = agents;
11074
11888
  spec.tools.only?.push(AGENT_TOOL);
11075
11889
  }
11076
- const model = node.model ?? defaults.model;
11077
- if (model !== void 0) spec.model = model;
11078
- const effort = node.effort ?? defaults.effort;
11079
- if (effort !== void 0) spec.effort = effort;
11890
+ if (models.model !== void 0) spec.model = models.model;
11891
+ if (models.effort !== void 0) spec.effort = models.effort;
11080
11892
  const maxTurns = node.maxTurns ?? defaults.maxTurns;
11081
11893
  if (maxTurns !== void 0) spec.maxTurns = maxTurns;
11082
11894
  const budgets = [node.budgetUsd ?? defaults.budgetUsd, options.budgetLeftUsd].filter((b) => b !== void 0);
@@ -11279,6 +12091,112 @@ const explainWorkflowText = (text, file, nodeId, where) => {
11279
12091
  sources: sourcesOf(result.explanation, place, where.home ?? homedir())
11280
12092
  } : result;
11281
12093
  };
12094
+ // ../core/src/usage/quota.ts
12095
+ const WINDOWS = [
12096
+ "day",
12097
+ "week",
12098
+ "month"
12099
+ ];
12100
+ const FIELDS = {
12101
+ day: "dayUsd",
12102
+ week: "weekUsd",
12103
+ month: "monthUsd"
12104
+ };
12105
+ /** Where a window began and when the next begins, in local time. */
12106
+ const windowOf = (window, now) => {
12107
+ const [year, month, day] = [
12108
+ now.getFullYear(),
12109
+ now.getMonth(),
12110
+ now.getDate()
12111
+ ];
12112
+ if (window === "month") return {
12113
+ since: new Date(year, month, 1),
12114
+ until: new Date(year, month + 1)
12115
+ };
12116
+ if (window === "day") return {
12117
+ since: new Date(year, month, day),
12118
+ until: new Date(year, month, day + 1)
12119
+ };
12120
+ const monday = day - (now.getDay() + 6) % 7;
12121
+ return {
12122
+ since: new Date(year, month, monday),
12123
+ until: new Date(year, month, monday + 7)
12124
+ };
12125
+ };
12126
+ /** The caps that apply: the user's on everything, and the project's on itself. */
12127
+ const capsOf = (user, project, root) => WINDOWS.flatMap((window) => {
12128
+ const field = FIELDS[window];
12129
+ const everywhere = user?.[field];
12130
+ const here = project?.[field];
12131
+ const caps = [];
12132
+ if (everywhere !== void 0) caps.push({
12133
+ window,
12134
+ capUsd: everywhere
12135
+ });
12136
+ if (here !== void 0 && root !== void 0) caps.push({
12137
+ window,
12138
+ capUsd: here,
12139
+ project: root
12140
+ });
12141
+ return caps;
12142
+ });
12143
+ /** Each cap with what has been spent against it. */
12144
+ const standingOf = (store, caps, now) => caps.map((cap) => {
12145
+ const { since, until } = windowOf(cap.window, now);
12146
+ const spentUsd = store.spentSince(since.toISOString(), cap.project);
12147
+ return {
12148
+ ...cap,
12149
+ spentUsd,
12150
+ leftUsd: cap.capUsd - spentUsd,
12151
+ resetsAt: until.toISOString()
12152
+ };
12153
+ });
12154
+ /** Dollars: cents, or more digits for an amount under a dollar. */
12155
+ const usd = (amount) => `$${amount.toFixed(amount > 0 && amount < 1 ? 4 : 2)}`;
12156
+ const at = (iso) => new Date(iso).toLocaleString(void 0, {
12157
+ dateStyle: "medium",
12158
+ timeStyle: "short"
12159
+ });
12160
+ const capReason = (spent) => `${spent.project === void 0 ? "your" : "this project's"} ${spent.window} quota of ${usd(spent.capUsd)} is spent (${usd(spent.spentUsd)}); it lifts on ${at(spent.resetsAt)}, or raise it in ${spent.project === void 0 ? "~/.ork/settings.yaml" : ".ork/project.yaml"}`;
12161
+ /** What a run or a call refused at its start says first, for its caller to read. */
12162
+ const NOT_STARTED = "Not started: ";
12163
+ /** Why a plan limit stops a run. */
12164
+ const planReason = (limit) => `your Claude plan's ${limit.window.replaceAll("_", " ")} limit was reached${limit.resetsAt === void 0 ? "" : `; it resets on ${at(limit.resetsAt)}, or use ork usage --clear-plan-limits if it has lifted`}`;
12165
+ /**
12166
+ * The windows that hold the whole account. One for a single model, such as
12167
+ * `seven_day_opus`, stops only the session it refused: other models may be
12168
+ * free.
12169
+ */
12170
+ const ACCOUNT_WINDOWS = /* @__PURE__ */ new Set([
12171
+ "five_hour",
12172
+ "seven_day",
12173
+ "overage",
12174
+ "plan"
12175
+ ]);
12176
+ /** The plan limits in force, the one that lifts last first. */
12177
+ const refusing = (limits, now) => limits.filter((limit) => limit.status === "rejected" && ACCOUNT_WINDOWS.has(limit.window) && limit.resetsAt !== void 0 && Date.parse(limit.resetsAt) > now.getTime()).toSorted((a, b) => String(b.resetsAt).localeCompare(String(a.resetsAt)));
12178
+ /** How long a report with no reset time stays worth showing. */
12179
+ const SHOWN_MS = 864e5;
12180
+ /** What the caps and the plan allow a run now. */
12181
+ const limitsNow = (store, caps, now) => {
12182
+ const standing = standingOf(store, caps, now);
12183
+ const [plan] = refusing(store.planLimits(), now);
12184
+ const spent = standing.find((cap) => cap.leftUsd <= 0);
12185
+ const blocked = plan === void 0 ? spent === void 0 ? void 0 : capReason(spent) : planReason(plan);
12186
+ return {
12187
+ ...standing.length === 0 ? {} : { leftUsd: Math.min(...standing.map((cap) => cap.leftUsd)) },
12188
+ ...blocked === void 0 ? {} : { blocked }
12189
+ };
12190
+ };
12191
+ /** What `ork usage` and the panel show. */
12192
+ const limitsReport = (store, { caps, problems }, now) => ({
12193
+ problems,
12194
+ caps: standingOf(store, caps, now),
12195
+ plan: store.planLimits().filter((limit) => {
12196
+ if (limit.status === "allowed") return false;
12197
+ return (limit.resetsAt === void 0 ? Date.parse(limit.at) + SHOWN_MS : Date.parse(limit.resetsAt)) > now.getTime();
12198
+ })
12199
+ });
11282
12200
  // ../core/src/remote/follow.ts
11283
12201
  /**
11284
12202
  * @file Following a run's events from another process (`ork logs -f`, the local
@@ -11292,18 +12210,16 @@ const followEvents = async (store, runId, onEvent, options) => {
11292
12210
  seq: options.after ?? 0,
11293
12211
  finished: false
11294
12212
  };
11295
- const done = new AbortController();
11296
12213
  const emit = (stored) => {
11297
12214
  if (stored.seq <= seen.seq) return;
11298
12215
  seen.seq = stored.seq;
11299
12216
  onEvent(stored);
11300
- if (stored.event.type === "run.finished") {
11301
- seen.finished = true;
11302
- done.abort();
11303
- }
12217
+ const { type } = stored.event;
12218
+ if (type === "run.finished") seen.finished = true;
12219
+ else if (type === "run.started" || type === "run.resumed") seen.finished = false;
11304
12220
  };
11305
12221
  const aborted = () => options.signal?.aborted === true;
11306
- /** Takes what the store has; true once `run.finished` is among it. */
12222
+ /** Takes what the store has; true once the run's latest go has finished. */
11307
12223
  const drain = () => {
11308
12224
  for (const stored of store.events(runId, seen.seq)) emit(stored);
11309
12225
  return seen.finished;
@@ -11312,7 +12228,11 @@ const followEvents = async (store, runId, onEvent, options) => {
11312
12228
  if (drain()) return "finished";
11313
12229
  if (!options.follow) return "stored";
11314
12230
  if (aborted()) return "aborted";
11315
- const via = await followSocket(socketPath(runId, options.home), seen.seq, emit, options.signal === void 0 ? done.signal : AbortSignal.any([options.signal, done.signal]));
12231
+ const done = new AbortController();
12232
+ const via = await followSocket(socketPath(runId, options.home), seen.seq, (stored) => {
12233
+ emit(stored);
12234
+ if (seen.finished) done.abort();
12235
+ }, options.signal === void 0 ? done.signal : AbortSignal.any([options.signal, done.signal]));
11316
12236
  if (seen.finished) return "finished";
11317
12237
  if (aborted()) return "aborted";
11318
12238
  if (via === "ended") continue;
@@ -11362,31 +12282,58 @@ const noWait = (id) => ({
11362
12282
  run: id
11363
12283
  }
11364
12284
  });
11365
- /** A node's report as a call's result; a failing one says why. */
11366
- const reportResult = (id, node, output, ok, usage) => {
11367
- const summary = output["summary"];
11368
- return {
11369
- ok,
11370
- output: {
11371
- ...output,
11372
- run: id
11373
- },
11374
- usage,
11375
- ...ok ? {} : { error: typeof summary === "string" ? summary : `${node} reported failure.` }
11376
- };
12285
+ /**
12286
+ * A call that could not start its run: refused by a limit across runs
12287
+ * (RFC-007), or something the calling node asks for that isn't there.
12288
+ */
12289
+ const startFailed = (error) => failedWith(error.includes("Not started: ") ? endFor("plan_limit", {}, error) : endFor("invalid_node", { reason: error }), {
12290
+ status: "fail",
12291
+ summary: error
12292
+ });
12293
+ /** How a run that ended `status` ended, when its nodes don't say. */
12294
+ const STATUS_KINDS = {
12295
+ succeeded: "done",
12296
+ failed: "unknown",
12297
+ budget_exhausted: "budget_limit",
12298
+ quota_exhausted: "plan_limit",
12299
+ timed_out: "timed_out",
12300
+ interrupted: "interrupted"
12301
+ };
12302
+ /**
12303
+ * How a called run ended, carried up to the node that called it: a limit the
12304
+ * run stopped at, or else how its first failed node ended.
12305
+ */
12306
+ const runEnd = (result, detail) => {
12307
+ const kind = STATUS_KINDS[result.status];
12308
+ const node = result.nodes.find((entry) => entry.ok === false);
12309
+ return kind === "unknown" && node !== void 0 ? endOf({
12310
+ ok: false,
12311
+ output: node.output ?? {},
12312
+ ...node.error === void 0 ? {} : { error: node.error },
12313
+ ...node.end === void 0 ? {} : { end: node.end }
12314
+ }) : endFor(kind, {}, detail);
11377
12315
  };
12316
+ /** A node's report as a call's result; a failing one says why. */
12317
+ const reportResult = (id, output, ok, usage, end = endFor(ok ? "done" : "reported_failure")) => endedWith({
12318
+ ok,
12319
+ output: {
12320
+ ...output,
12321
+ run: id
12322
+ },
12323
+ usage
12324
+ }, end);
11378
12325
  /**
11379
12326
  * What the node last reported, for a caller coming back to the run: a resumed
11380
12327
  * run hands its kept results back with no new events, so none would come.
11381
12328
  */
11382
12329
  const storedReport = (watch, id, node) => {
11383
12330
  const found = watch.store.events(id).flatMap(({ event }) => event.type === "node.finished" && event.id === node && event.ok ? [event.output] : []).at(-1);
11384
- return found === void 0 ? void 0 : reportResult(id, node, found, true, usageOf(watch.newCost(id)));
12331
+ return found === void 0 ? void 0 : reportResult(id, found, true, usageOf(watch.newCost(id)));
11385
12332
  };
11386
12333
  const resultOf = (id, result, cost) => {
11387
12334
  const ok = result.status === "succeeded";
11388
12335
  const summary = ok ? `Run ${id} succeeded.` : `Run ${id} ended ${result.status}: ${result.reasons.join("; ")}`;
11389
- return {
12336
+ return endedWith({
11390
12337
  ok,
11391
12338
  output: {
11392
12339
  status: ok ? "pass" : "fail",
@@ -11395,9 +12342,8 @@ const resultOf = (id, result, cost) => {
11395
12342
  runStatus: result.status,
11396
12343
  outputs: result.outputs ?? {}
11397
12344
  },
11398
- usage: usageOf(cost),
11399
- ...ok ? {} : { error: summary }
11400
- };
12345
+ usage: usageOf(cost)
12346
+ }, ok ? endFor("done") : runEnd(result, summary));
11401
12347
  };
11402
12348
  /** The first thing `pick` finds in the run's events after `after`, if any. */
11403
12349
  const followRun = async ({ store, home }, id, after, signal, pick) => {
@@ -11420,9 +12366,12 @@ const waitForRun = async (watch, id, after, signal) => {
11420
12366
  const cost = watch.newCost(id);
11421
12367
  if (result !== void 0) return resultOf(id, result, cost);
11422
12368
  const [run] = watch.store.lookup(id);
11423
- const status = run === void 0 ? "gone" : liveStatus(run);
12369
+ const said = `Run ${id} ended without a result (${run === void 0 ? "gone" : liveStatus(run)}).`;
11424
12370
  return {
11425
- ...failure(`Run ${id} ended without a result (${status}).`),
12371
+ ...failedWith(endFor("unknown", {}, said), {
12372
+ status: "fail",
12373
+ summary: said
12374
+ }),
11426
12375
  usage: usageOf(cost)
11427
12376
  };
11428
12377
  };
@@ -11431,20 +12380,34 @@ const waitForNode = async (watch, id, node, after, signal) => {
11431
12380
  const seen = await followRun(watch, id, after, signal, (event) => {
11432
12381
  if (event.type === "node.finished" && event.id === node) return {
11433
12382
  ok: event.ok,
11434
- output: event.output
12383
+ output: event.output,
12384
+ end: endOf(event)
11435
12385
  };
11436
12386
  return event.type === "run.finished" ? { ended: event.result } : void 0;
11437
12387
  });
11438
12388
  const usage = usageOf(watch.newCost(id));
11439
- if (seen === void 0) return {
11440
- ...failure(`Run ${id} ended before ${node} reported.`),
11441
- usage
11442
- };
11443
- if ("ended" in seen) return {
11444
- ...failure(`${node} of run ${id} did not report: the run ended ${seen.ended.status}.`),
11445
- usage
11446
- };
11447
- return reportResult(id, node, seen.output, seen.ok, usage);
12389
+ if (seen === void 0) {
12390
+ const said = `Run ${id} ended before ${node} reported.`;
12391
+ return {
12392
+ ...failedWith(endFor("unknown", {}, said), {
12393
+ status: "fail",
12394
+ summary: said
12395
+ }),
12396
+ usage
12397
+ };
12398
+ }
12399
+ if ("ended" in seen) {
12400
+ const said = `${node} of run ${id} did not report: the run ended ${seen.ended.status}.`;
12401
+ const end = seen.ended.status === "succeeded" ? endFor("no_report", {}, said) : runEnd(seen.ended, said);
12402
+ return {
12403
+ ...failedWith(end, {
12404
+ status: "fail",
12405
+ summary: said
12406
+ }),
12407
+ usage
12408
+ };
12409
+ }
12410
+ return reportResult(id, seen.output, seen.ok, usage, seen.end);
11448
12411
  };
11449
12412
  // ../core/src/remote/detach.ts
11450
12413
  /**
@@ -11976,18 +12939,18 @@ const usageRowsOf = (run, events) => {
11976
12939
  }
11977
12940
  return rows;
11978
12941
  };
11979
- const SOURCE$1 = `SELECT seq, at, data FROM events
12942
+ const SOURCE$2 = `SELECT seq, at, data FROM events
11980
12943
  WHERE run_id = ? AND (
11981
12944
  type IN ('run.started', 'node.started', 'node.finished')
11982
12945
  OR (type = 'node.event'
11983
12946
  AND json_extract(data, '$.event.type') IN ('session.started', 'session.finished'))
11984
12947
  ) ORDER BY seq`;
11985
- const INSERT = `INSERT OR IGNORE INTO usage (
12948
+ const INSERT$1 = `INSERT OR IGNORE INTO usage (
11986
12949
  run_id, seq, at, project, workflow, node, template, model, driver, kind,
11987
12950
  round, failed, cost_usd, input_tokens, output_tokens, cache_read_tokens,
11988
12951
  cache_write_tokens, turns, duration_ms
11989
12952
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
11990
- const rowsFromEvents = (db, run) => usageRowsOf(run, db.prepare(SOURCE$1).all(run.id).map((r) => ({
12953
+ const rowsFromEvents = (db, run) => usageRowsOf(run, db.prepare(SOURCE$2).all(run.id).map((r) => ({
11991
12954
  seq: Number(r["seq"]),
11992
12955
  at: text(r, "at"),
11993
12956
  event: eventOf(text(r, "data"))
@@ -11997,7 +12960,7 @@ const writeRow = (insert, row) => {
11997
12960
  };
11998
12961
  /** Fills the ledger from the runs recorded before it existed. */
11999
12962
  const backfillUsage = (db) => {
12000
- const insert = db.prepare(INSERT);
12963
+ const insert = db.prepare(INSERT$1);
12001
12964
  for (const run of db.prepare("SELECT id, project, workflow FROM runs").all()) {
12002
12965
  const rows = rowsFromEvents(db, {
12003
12966
  id: text(run, "id"),
@@ -12059,7 +13022,7 @@ const alive = (row) => {
12059
13022
  };
12060
13023
  /** The ledger's queries; `recordUsage` is the store's own, called on append. */
12061
13024
  const usageQueries = (db) => {
12062
- const insert = db.prepare(INSERT);
13025
+ const insert = db.prepare(INSERT$1);
12063
13026
  const spentAll = db.prepare("SELECT COALESCE(SUM(cost_usd), 0) AS spent FROM usage WHERE at >= ?");
12064
13027
  const spentHere = db.prepare("SELECT COALESCE(SUM(cost_usd), 0) AS spent FROM usage WHERE at >= ? AND project = ?");
12065
13028
  const runById = db.prepare("SELECT id, project, workflow FROM runs WHERE id = ?");
@@ -12384,21 +13347,26 @@ const resumeNode = async (talk, run, request, node, signal) => {
12384
13347
  const refused = await talk.resumeSession(run, node, body, signal);
12385
13348
  if (refused !== null) return failure(refused);
12386
13349
  const finished = store.events(run.id, from).flatMap(({ event }) => event.type === "node.event" && event.event.type === "session.finished" && event.id === node ? [event.event.outcome] : []).at(-1);
12387
- if (finished?.report === null || finished === void 0) return failure(`${node} ended without a report when resumed.`);
13350
+ if (finished?.report === null || finished === void 0) {
13351
+ const said = `${node} ended without a report when resumed.`;
13352
+ return failedWith(endFor("no_report", {}, said), {
13353
+ status: "fail",
13354
+ summary: said
13355
+ });
13356
+ }
12388
13357
  const ok = finished.report.status !== "fail";
12389
13358
  const own = usageRowsOf(run, store.events(run.id)).filter((row) => row.seq > from && row.node === node).reduce((sum, row) => sum + row.costUsd, 0);
12390
- return {
13359
+ return endedWith({
12391
13360
  ok,
12392
13361
  output: {
12393
13362
  ...finished.report,
12394
13363
  run: run.id
12395
13364
  },
12396
- ...ok ? {} : { error: finished.report.summary },
12397
13365
  usage: {
12398
13366
  ...finished.usage,
12399
13367
  costUsd: own + talk.newCost(run.id)
12400
13368
  }
12401
- };
13369
+ }, endFor(ok ? "done" : "reported_failure"));
12402
13370
  };
12403
13371
  /** A later activation: as a message, or by resuming a session that has ended. */
12404
13372
  const continueConversation = async (talk, id, request, signal) => {
@@ -12417,7 +13385,13 @@ const continueConversation = async (talk, id, request, signal) => {
12417
13385
  if (isLive()) await followRun(talk, id, lastSeq(store, id), signal, finishedRun);
12418
13386
  const [run] = store.lookup(id);
12419
13387
  if (run === void 0) return failure(`Run ${id} is gone.`);
12420
- if (signal.aborted) return failure(`Stopped while run ${id} was finishing.`);
13388
+ if (signal.aborted) {
13389
+ const said = `Stopped while run ${id} was finishing.`;
13390
+ return failedWith(endFor("interrupted", {}, said), {
13391
+ status: "fail",
13392
+ summary: said
13393
+ });
13394
+ }
12421
13395
  return resumeNode(talk, run, request, node, signal);
12422
13396
  };
12423
13397
  // ../core/src/remote/calls.ts
@@ -12513,7 +13487,7 @@ const createCalls = (options) => {
12513
13487
  };
12514
13488
  const converse = async (request, signal) => {
12515
13489
  const unadopted = await adoptConversation(request, signal);
12516
- if (unadopted !== null) return failure(unadopted);
13490
+ if (unadopted !== null) return startFailed(unadopted);
12517
13491
  const known = conversations.get(request.callId);
12518
13492
  if (known !== void 0) {
12519
13493
  const done = stopOnAbort(known, signal);
@@ -12525,7 +13499,7 @@ const createCalls = (options) => {
12525
13499
  }
12526
13500
  const begun = await begin(request);
12527
13501
  adopt.delete(request.callId);
12528
- if (!begun.ok) return failure(begun.error);
13502
+ if (!begun.ok) return startFailed(begun.error);
12529
13503
  const { id, after, fresh } = begun;
12530
13504
  conversations.set(request.callId, id);
12531
13505
  const done = stopOnAbort(id, signal);
@@ -12534,7 +13508,7 @@ const createCalls = (options) => {
12534
13508
  const refused = await linkChannels(talk, id, request, signal);
12535
13509
  if (refused !== null) {
12536
13510
  stop(id);
12537
- return failure(refused);
13511
+ return startFailed(refused);
12538
13512
  }
12539
13513
  }
12540
13514
  if (fresh) tellInput(talk, id, request);
@@ -12550,7 +13524,7 @@ const createCalls = (options) => {
12550
13524
  };
12551
13525
  const callOnce = async (request, signal) => {
12552
13526
  const begun = await begin(request);
12553
- if (!begun.ok) return failure(begun.error);
13527
+ if (!begun.ok) return startFailed(begun.error);
12554
13528
  const { id, after } = begun;
12555
13529
  const done = stopOnAbort(id, signal);
12556
13530
  try {
@@ -13510,6 +14484,82 @@ const projectQueries = (db) => {
13510
14484
  }
13511
14485
  };
13512
14486
  };
14487
+ // ../core/src/store/tool-cpu.ts
14488
+ const SOURCE$1 = `SELECT seq, at, data FROM events
14489
+ WHERE run_id = ? AND (
14490
+ type = 'run.started'
14491
+ OR (type = 'node.event' AND json_extract(data, '$.event.type') = 'tool.result'
14492
+ AND json_extract(data, '$.event.cpu') IS NOT NULL)
14493
+ ) ORDER BY seq`;
14494
+ const INSERT = `INSERT OR IGNORE INTO tool_cpu (
14495
+ run_id, seq, at, project, workflow, node, template, user_ms, system_ms
14496
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`;
14497
+ /** A run's tool commands' rows, from its events. */
14498
+ const rowsOf = (db, run) => {
14499
+ const declared = /* @__PURE__ */ new Set();
14500
+ const rows = [];
14501
+ for (const r of db.prepare(SOURCE$1).all(run.id)) {
14502
+ const event = eventOf(text(r, "data"));
14503
+ if (event.type === "run.started") {
14504
+ for (const node of event.nodes) declared.add(node.id);
14505
+ continue;
14506
+ }
14507
+ if (event.type !== "node.event" || event.event.type !== "tool.result") continue;
14508
+ const { cpu } = event.event;
14509
+ if (cpu === void 0) continue;
14510
+ const template = declared.has(event.id) ? void 0 : templateOf(event.id, declared);
14511
+ rows.push({
14512
+ runId: run.id,
14513
+ seq: Number(r["seq"]),
14514
+ at: text(r, "at"),
14515
+ project: run.project,
14516
+ workflow: run.workflow,
14517
+ node: event.id,
14518
+ ...template === void 0 ? {} : { template },
14519
+ userMs: cpu.userMs,
14520
+ systemMs: cpu.systemMs
14521
+ });
14522
+ }
14523
+ return rows;
14524
+ };
14525
+ const write = (db, rows) => {
14526
+ const insert = db.prepare(INSERT);
14527
+ for (const row of rows) insert.run(row.runId, row.seq, row.at, row.project, row.workflow, row.node, row.template ?? null, row.userMs, row.systemMs);
14528
+ };
14529
+ const runsOf = (db, sql, ...values) => db.prepare(sql).all(...values).map((run) => ({
14530
+ id: text(run, "id"),
14531
+ project: text(run, "project"),
14532
+ workflow: text(run, "workflow")
14533
+ }));
14534
+ /** Fills the table from the runs recorded before it existed. */
14535
+ const backfillToolCpu = (db) => {
14536
+ for (const run of runsOf(db, "SELECT id, project, workflow FROM runs")) write(db, rowsOf(db, run));
14537
+ };
14538
+ const toolCpuQueries = (db) => ({
14539
+ /** Writes a run's rows; the store's own, on each timed `tool.result`. */
14540
+ recordToolCpu: (runId) => {
14541
+ const [run] = runsOf(db, "SELECT id, project, workflow FROM runs WHERE id = ?", runId);
14542
+ if (run !== void 0) write(db, rowsOf(db, run));
14543
+ },
14544
+ toolCpu: (filter = {}) => {
14545
+ const { sql, values } = whereOf(filter);
14546
+ return db.prepare(`SELECT * FROM tool_cpu ${sql} ORDER BY at, run_id, seq`).all(...values).map((r) => {
14547
+ const row = {
14548
+ runId: text(r, "run_id"),
14549
+ seq: Number(r["seq"]),
14550
+ at: text(r, "at"),
14551
+ project: text(r, "project"),
14552
+ workflow: text(r, "workflow"),
14553
+ node: text(r, "node"),
14554
+ userMs: Number(r["user_ms"]),
14555
+ systemMs: Number(r["system_ms"])
14556
+ };
14557
+ const template = text(r, "template");
14558
+ if (template !== "") row.template = template;
14559
+ return row;
14560
+ });
14561
+ }
14562
+ });
13513
14563
  // ../core/src/store/triggers.ts
13514
14564
  const recordOf$1 = (row) => {
13515
14565
  const acceptedHash = text(row, "accepted_hash");
@@ -13587,8 +14637,8 @@ const triggerQueries = (db) => {
13587
14637
  };
13588
14638
  // ../core/src/store/queries.ts
13589
14639
  /**
13590
- * Every query set on `db`; `insertFeedback` and `recordUsage` are the store's
13591
- * own.
14640
+ * Every query set on `db`; `insertFeedback`, `recordUsage` and `recordToolCpu`
14641
+ * are the store's own.
13592
14642
  */
13593
14643
  const storeQueries = (db) => ({
13594
14644
  ...decisionQueries(db),
@@ -13596,6 +14646,7 @@ const storeQueries = (db) => ({
13596
14646
  ...feedbackQueries(db),
13597
14647
  ...projectQueries(db),
13598
14648
  ...usageQueries(db),
14649
+ ...toolCpuQueries(db),
13599
14650
  ...planLimitQueries(db),
13600
14651
  ...triggerQueries(db),
13601
14652
  ...draftQueries(db)
@@ -13799,7 +14850,23 @@ const MIGRATIONS = [
13799
14850
  ALTER TABLE runs ADD COLUMN failures_handled INTEGER;`,
13800
14851
  `ALTER TABLE decisions ADD COLUMN note TEXT;`,
13801
14852
  `ALTER TABLE decisions ADD COLUMN owner TEXT;`,
13802
- `ALTER TABLE decisions ADD COLUMN document TEXT;`
14853
+ `ALTER TABLE decisions ADD COLUMN document TEXT;`,
14854
+ (db) => {
14855
+ db.exec(`CREATE TABLE tool_cpu (
14856
+ run_id TEXT NOT NULL,
14857
+ seq INTEGER NOT NULL,
14858
+ at TEXT NOT NULL,
14859
+ project TEXT NOT NULL,
14860
+ workflow TEXT NOT NULL,
14861
+ node TEXT NOT NULL,
14862
+ template TEXT,
14863
+ user_ms INTEGER NOT NULL,
14864
+ system_ms INTEGER NOT NULL,
14865
+ PRIMARY KEY (run_id, seq)
14866
+ ) WITHOUT ROWID;
14867
+ CREATE INDEX tool_cpu_by_time ON tool_cpu (at);`);
14868
+ backfillToolCpu(db);
14869
+ }
13803
14870
  ];
13804
14871
  const versionOf = (db) => Number(db.prepare("PRAGMA user_version").get()?.["user_version"] ?? 0);
13805
14872
  /**
@@ -13925,8 +14992,9 @@ const openStore = (file = storePath(), options = {}) => {
13925
14992
  const allRuns = db.prepare("SELECT * FROM runs ORDER BY started_at DESC, rowid DESC LIMIT ?");
13926
14993
  const projectRuns = db.prepare("SELECT * FROM runs WHERE project = ? ORDER BY started_at DESC, rowid DESC LIMIT ?");
13927
14994
  const eventsAfter = db.prepare("SELECT seq, at, data FROM events WHERE run_id = ? AND seq > ? ORDER BY seq");
14995
+ const controlEvents = db.prepare("SELECT seq, at, data FROM events WHERE run_id = ? AND type IN ('run.command', 'run.started', 'run.finished') ORDER BY seq");
13928
14996
  const runNodes = db.prepare("SELECT node FROM events WHERE run_id = ? AND node IS NOT NULL GROUP BY node ORDER BY MIN(seq)");
13929
- const { insertFeedback, recordUsage, recordPlanLimit, ...queries } = storeQueries(db);
14997
+ const { insertFeedback, recordUsage, recordToolCpu, recordPlanLimit, ...queries } = storeQueries(db);
13930
14998
  return {
13931
14999
  ...queries,
13932
15000
  begin: (run) => {
@@ -13953,6 +15021,7 @@ const openStore = (file = storePath(), options = {}) => {
13953
15021
  insertEvent.run(runId, seq, at, event.type, nodeOf(event), JSON.stringify(event, maskReplacer));
13954
15022
  if (event.type === "node.finished" && event.usage !== void 0) addCost.run(event.usage.costUsd, runId);
13955
15023
  if (event.type === "node.event" && event.event.type === "session.finished") recordUsage(runId);
15024
+ if (event.type === "node.event" && event.event.type === "tool.result" && event.event.cpu !== void 0) recordToolCpu(runId);
13956
15025
  if (event.type === "node.event" && event.event.type === "plan.limit") recordPlanLimit(event.event, at);
13957
15026
  if (event.type === "feedback.recorded") insertFeedback(runId, seq, at, event.node, event.feedback);
13958
15027
  if (event.type === "run.finished") {
@@ -13981,12 +15050,95 @@ const openStore = (file = storePath(), options = {}) => {
13981
15050
  at: text(r, "at"),
13982
15051
  event: eventOf(text(r, "data"))
13983
15052
  })),
15053
+ controlEvents: (runId) => controlEvents.all(runId).map((r) => ({
15054
+ runId,
15055
+ seq: Number(r["seq"]),
15056
+ at: text(r, "at"),
15057
+ event: eventOf(text(r, "data"))
15058
+ })),
13984
15059
  nodes: (runId) => runNodes.all(runId).map((r) => text(r, "node")),
13985
15060
  close: () => {
13986
15061
  db.close();
13987
15062
  }
13988
15063
  };
13989
15064
  };
15065
+ // ../core/src/store/activity.ts
15066
+ /**
15067
+ * @file What each node of a run is doing, and when it started and finished,
15068
+ * folded from the stored events so nothing new has to be recorded. The tree
15069
+ * (view.ts) folds the events alone; this needs when they were written, and
15070
+ * keeps a screen's one-line answer to "what is it doing" in one place.
15071
+ */
15072
+ const LONGEST_LINE = 120;
15073
+ /**
15074
+ * The fields of a tool's input that say what it's working on, best first. Only
15075
+ * these are shown, so what else a tool was given stays in the logs.
15076
+ */
15077
+ const TARGETS = [
15078
+ "file_path",
15079
+ "notebook_path",
15080
+ "path",
15081
+ "url",
15082
+ "pattern",
15083
+ "query",
15084
+ "command"
15085
+ ];
15086
+ const oneLine = (text) => {
15087
+ const line = text.replaceAll(/\s+/gu, " ").trim();
15088
+ return line.length > LONGEST_LINE ? `${line.slice(0, 119)}…` : line;
15089
+ };
15090
+ const toolLine = ({ name, input }) => {
15091
+ const target = TARGETS.map((field) => input[field]).find((value) => typeof value === "string" && value.trim() !== "");
15092
+ if (typeof target !== "string") return name;
15093
+ return oneLine(target.startsWith(`${name} `) ? target : `${name} ${target}`);
15094
+ };
15095
+ /** What a finished node said, or why it failed. */
15096
+ const finishedNow = (ok, error, summary) => {
15097
+ if (!ok) return error === void 0 ? void 0 : oneLine(error);
15098
+ return typeof summary === "string" && summary.trim() !== "" ? oneLine(summary) : void 0;
15099
+ };
15100
+ /** Each node's activity, by id; a node that never started has none. */
15101
+ const runActivity = (stored) => {
15102
+ const nodes = /* @__PURE__ */ new Map();
15103
+ const of = (id) => {
15104
+ const found = nodes.get(id);
15105
+ if (found !== void 0) return found;
15106
+ const fresh = {};
15107
+ nodes.set(id, fresh);
15108
+ return fresh;
15109
+ };
15110
+ for (const { at, event } of stored) if (event.type === "node.started") {
15111
+ const node = of(event.id);
15112
+ node.startedAt ??= at;
15113
+ delete node.finishedAt;
15114
+ delete node.now;
15115
+ } else if (event.type === "node.event") {
15116
+ if (event.event.type === "tool.called") of(event.id).now = toolLine(event.event);
15117
+ } else if (event.type === "slot.waiting") of(event.id).now = oneLine(slotWaitLine(event.holders));
15118
+ else if (event.type === "slot.taken") delete of(event.id).now;
15119
+ else if (event.type === "decision.opened") of(event.node).now = oneLine(`Asked: ${event.question}`);
15120
+ else if (event.type === "decision.answered") of(event.node).now = oneLine(`Answered: ${event.answer}`);
15121
+ else if (event.type === "decision.withdrawn") delete of(event.node).now;
15122
+ else if (event.type === "node.finished") {
15123
+ const node = of(event.id);
15124
+ node.finishedAt = at;
15125
+ const now = finishedNow(event.ok, event.error, event.output["summary"]);
15126
+ if (now === void 0) delete node.now;
15127
+ else node.now = now;
15128
+ }
15129
+ return nodes;
15130
+ };
15131
+ /** The ownership tree, with what each node is doing and when it ran. */
15132
+ const activeTree = (stored) => {
15133
+ const activity = runActivity(stored);
15134
+ const dress = (node) => {
15135
+ Object.assign(node, activity.get(node.id));
15136
+ for (const child of node.children) dress(child);
15137
+ };
15138
+ const roots = runTree(stored.map(({ event }) => event));
15139
+ for (const root of roots) dress(root);
15140
+ return roots;
15141
+ };
13990
15142
  // ../core/src/triggers/check.ts
13991
15143
  /**
13992
15144
  * @file Checking `triggers.yaml` against the project's workflows (RFC-011):
@@ -14653,112 +15805,6 @@ const planUpgrade = (options) => {
14653
15805
  const RUNS_LOOKED_AT = 500;
14654
15806
  /** The ids of runs still running anywhere on this machine, newest first. */
14655
15807
  const runningRunIds = (store) => store.runs({ limit: RUNS_LOOKED_AT }).filter((run) => liveStatus(run) === "running").map((run) => run.id);
14656
- // ../core/src/usage/quota.ts
14657
- const WINDOWS = [
14658
- "day",
14659
- "week",
14660
- "month"
14661
- ];
14662
- const FIELDS = {
14663
- day: "dayUsd",
14664
- week: "weekUsd",
14665
- month: "monthUsd"
14666
- };
14667
- /** Where a window began and when the next begins, in local time. */
14668
- const windowOf = (window, now) => {
14669
- const [year, month, day] = [
14670
- now.getFullYear(),
14671
- now.getMonth(),
14672
- now.getDate()
14673
- ];
14674
- if (window === "month") return {
14675
- since: new Date(year, month, 1),
14676
- until: new Date(year, month + 1)
14677
- };
14678
- if (window === "day") return {
14679
- since: new Date(year, month, day),
14680
- until: new Date(year, month, day + 1)
14681
- };
14682
- const monday = day - (now.getDay() + 6) % 7;
14683
- return {
14684
- since: new Date(year, month, monday),
14685
- until: new Date(year, month, monday + 7)
14686
- };
14687
- };
14688
- /** The caps that apply: the user's on everything, and the project's on itself. */
14689
- const capsOf = (user, project, root) => WINDOWS.flatMap((window) => {
14690
- const field = FIELDS[window];
14691
- const everywhere = user?.[field];
14692
- const here = project?.[field];
14693
- const caps = [];
14694
- if (everywhere !== void 0) caps.push({
14695
- window,
14696
- capUsd: everywhere
14697
- });
14698
- if (here !== void 0 && root !== void 0) caps.push({
14699
- window,
14700
- capUsd: here,
14701
- project: root
14702
- });
14703
- return caps;
14704
- });
14705
- /** Each cap with what has been spent against it. */
14706
- const standingOf = (store, caps, now) => caps.map((cap) => {
14707
- const { since, until } = windowOf(cap.window, now);
14708
- const spentUsd = store.spentSince(since.toISOString(), cap.project);
14709
- return {
14710
- ...cap,
14711
- spentUsd,
14712
- leftUsd: cap.capUsd - spentUsd,
14713
- resetsAt: until.toISOString()
14714
- };
14715
- });
14716
- /** Dollars: cents, or more digits for an amount under a dollar. */
14717
- const usd = (amount) => `$${amount.toFixed(amount > 0 && amount < 1 ? 4 : 2)}`;
14718
- const at = (iso) => new Date(iso).toLocaleString(void 0, {
14719
- dateStyle: "medium",
14720
- timeStyle: "short"
14721
- });
14722
- const capReason = (spent) => `${spent.project === void 0 ? "your" : "this project's"} ${spent.window} quota of ${usd(spent.capUsd)} is spent (${usd(spent.spentUsd)}); it lifts on ${at(spent.resetsAt)}, or raise it in ${spent.project === void 0 ? "~/.ork/settings.yaml" : ".ork/project.yaml"}`;
14723
- /** What a run or a call refused at its start says first, for its caller to read. */
14724
- const NOT_STARTED = "Not started: ";
14725
- /** Why a plan limit stops a run. */
14726
- const planReason = (limit) => `your Claude plan's ${limit.window.replaceAll("_", " ")} limit was reached${limit.resetsAt === void 0 ? "" : `; it resets on ${at(limit.resetsAt)}, or use ork usage --clear-plan-limits if it has lifted`}`;
14727
- /**
14728
- * The windows that hold the whole account. One for a single model, such as
14729
- * `seven_day_opus`, stops only the session it refused: other models may be
14730
- * free.
14731
- */
14732
- const ACCOUNT_WINDOWS = /* @__PURE__ */ new Set([
14733
- "five_hour",
14734
- "seven_day",
14735
- "overage",
14736
- "plan"
14737
- ]);
14738
- /** The plan limits in force, the one that lifts last first. */
14739
- const refusing = (limits, now) => limits.filter((limit) => limit.status === "rejected" && ACCOUNT_WINDOWS.has(limit.window) && limit.resetsAt !== void 0 && Date.parse(limit.resetsAt) > now.getTime()).toSorted((a, b) => String(b.resetsAt).localeCompare(String(a.resetsAt)));
14740
- /** How long a report with no reset time stays worth showing. */
14741
- const SHOWN_MS = 864e5;
14742
- /** What the caps and the plan allow a run now. */
14743
- const limitsNow = (store, caps, now) => {
14744
- const standing = standingOf(store, caps, now);
14745
- const [plan] = refusing(store.planLimits(), now);
14746
- const spent = standing.find((cap) => cap.leftUsd <= 0);
14747
- const blocked = plan === void 0 ? spent === void 0 ? void 0 : capReason(spent) : planReason(plan);
14748
- return {
14749
- ...standing.length === 0 ? {} : { leftUsd: Math.min(...standing.map((cap) => cap.leftUsd)) },
14750
- ...blocked === void 0 ? {} : { blocked }
14751
- };
14752
- };
14753
- /** What `ork usage` and the panel show. */
14754
- const limitsReport = (store, { caps, problems }, now) => ({
14755
- problems,
14756
- caps: standingOf(store, caps, now),
14757
- plan: store.planLimits().filter((limit) => {
14758
- if (limit.status === "allowed") return false;
14759
- return (limit.resetsAt === void 0 ? Date.parse(limit.at) + SHOWN_MS : Date.parse(limit.resetsAt)) > now.getTime();
14760
- })
14761
- });
14762
15808
  // ../core/src/usage/caps.ts
14763
15809
  /**
14764
15810
  * @file Which limits across runs apply where (RFC-007): the user's settings and
@@ -15113,6 +16159,11 @@ const commitWork = async (worktree, never, message) => {
15113
16159
  };
15114
16160
  }
15115
16161
  };
16162
+ /** A node's result once its work could not be committed: a failed `git commit`. */
16163
+ const uncommitted = (result, error) => endedWith({
16164
+ ...result,
16165
+ ok: false
16166
+ }, endFor("command_failed", { program: "git commit" }, error));
15116
16167
  // ../core/src/worktree/close.ts
15117
16168
  const reasonOf = (options, id) => {
15118
16169
  const why = options.unmerged.get(id);
@@ -15294,12 +16345,7 @@ const createWorkspace = (options) => {
15294
16345
  if (work.committed) event(id, "committed", worktree);
15295
16346
  if (work.error === void 0) return next;
15296
16347
  unmerged.set(holder, `uncommitted: ${work.error}`);
15297
- const error = `could not commit its work: ${work.error}`;
15298
- return {
15299
- ...next,
15300
- ok: false,
15301
- error
15302
- };
16348
+ return uncommitted(next, work.error);
15303
16349
  };
15304
16350
  const finish = async (id, result) => {
15305
16351
  const from = continued.get(id);
@@ -15355,4 +16401,4 @@ const createWorkspace = (options) => {
15355
16401
  close
15356
16402
  };
15357
16403
  };
15358
- export { adoptedCalls as $, NodeSchema as $n, targetOf as $t, inspectProject as A, inside as An, scriptContactsOf as At, createCalls as B, newWorkflowText as Bn, mcpNotText as Bt, workflowTextHash as C, newProjectId as Cn, runTree as Ct, openStore as D, runDirOf as Dn, isItem as Dt, isNodeEvent as E, projectsHome as En, stillTheSameProcess as Et, clearGitLocalEnv as F, plainPathInside as Fn, declaredOutputOf as Ft, peerOf as G, parseWorkflow as Gn, DISCUSSIONS as Gt, endLink as H, beforeOf as Hn, FEEDBACK_KINDS as Ht, parseInputPairs as I, writePlainFile as In, PROJECT_FILE as It, flattenTree as J, EdgeInputSchema as Jn, sendPost as Jt, tellNode as K, serializeWorkflow as Kn, planPost as Kt, resolveInputs as L, skillsIn as Ln, gitRulesOf as Lt, git as M, LESSON_CAP_TOKENS as Mn, calledWorkflows as Mt, gitOk as N, estimateTokens as Nn, envReadsOf as Nt, storePath as O, shownPath as On, nestItems as Ot, lines as P, guardedBase as Pn, nodeSays as Pt, tellLiveRun as Q, settingsOf as Qn, normalizeTitle as Qt, driverRefusals as R, WorkflowEditSchema as Rn, loadProject as Rt, readProjectTriggers as S, inRepoPlace as Sn, runStatus as St, watchingPid as T, placeFor as Tn, processAlive as Tt, endLinksOf as U, programsRunBy as Un, FEEDBACK_SEVERITIES as Ut, addressOf as V, readDocument as Vn, serversIn as Vt, linkNodes as W, renderJsonSchema as Wn, FEEDBACK_TARGETS as Wt, requestPause as X, CheckReportSchema as Xn, createFeedback as Xt, requestContinue as Y, WorkflowSchema as Yn, publishPost as Yt, requestStop as Z, expandEdge as Zn, fingerprint as Zt, lateTriggers as _, createAgent as _n, EXPERIMENTS as _r, MESSAGE_LIMIT as _t, limitsNow as a, socketPath as an, timeoutMs as ar, followEvents as at, triggerStanding as b, loadAgents as bn, liveStatus as bt, usd as c, MASK as cn, integrationFor as cr, planNodeSession as ct, runningRunIds as d, planAgentMigration as dn, readyDriver as dr, parseReport as dt, evaluate as en, isClaudeNode as er, detachResume as et, runningScript as f, applySteps as fn, AGENT_TOOL_LAYOUTS as fr, reportSchemaFor as ft, parseVersion as g, toolsInUse as gn, firstLine as gr, startRun as gt, isNewer as h, toolLinks as hn, DRIVER_IDS as hr, walkSummary as ht, NOT_STARTED as i, serveRun as in, retiredDriverHint as ir, detachRun as it, removeWorktree as j, projectRootOf as jn, declaredProblem as jt, BRANCH_PREFIX as k, workflowDirOf as kn, runScope as kt, planUpgrade as l, maskReplacer as ln, PREFERRED_DRIVER as lr, namingNoDriver as lt, compareVersions as m, linkTools as mn, AGENT_DRIVERS as mr, walkChecks as mt, resolveBase as n, renderTemplate as nn, DriverSchema as nr, listKey as nt, limitsReport as o, socketRoom as on, INTEGRATIONS as or, explainWorkflowText as ot, detectInstaller as p, planMoveInto as pn, ORK_LAYOUT as pr, checkWorkflowText as pt, unlinkNodes as q, markValue as qn, runGh as qt, capsHere as r, readCondition as rn, NodeIdSchema as rr, DETACHED_ENV as rt, planReason as s, wakeRun as sn, TOOL_PREFIX as sr, explainSession as st, createWorkspace as t, parseTemplate as tn, isToolNode as tr, resumable as tt, releaseNotes as u, maskSecrets as un, driverChecks as ur, sessionDriver as ut, triggersReport as v, deleteAgent as vn, experimentsOn as vr, userMessageText as vt, takeWatchLock as w, placeFiles as wn, templateOf as wt, declaredTriggers as x, hasInRepoFiles as xn, pausedOf as xt, createWatcher as y, catalogAgent as yn, knownExperiments as yr, heldTimeout as yt, driverQueues as z, applyEdits as zn, MCP_FILE as zt };
16404
+ export { DETACHED_ENV as $, skillsIn as $n, endOf as $t, lines as A, linkTools as An, AGENT_DRIVERS as Ar, nestItems as At, linkNodes as B, placeFiles as Bn, lowerChild as Bt, openStore as C, wakeRun as Cn, TOOL_PREFIX as Cr, runStatus as Ct, removeWorktree as D, planAgentMigration as Dn, readyDriver as Dr, processAlive as Dt, inspectProject as E, maskSecrets as En, driverChecks as Er, templateOf as Et, driverQueues as F, catalogAgent as Fn, knownExperiments as Fr, failedWith as Ft, requestContinue as G, workflowDirOf as Gn, PROJECT_FILE as Gt, tellNode as H, projectsHome as Hn, envReadsOf as Ht, createCalls as I, loadAgents as In, stoppedEnd as It, tellLiveRun as J, LESSON_CAP_TOKENS as Jn, machineRulesOf as Jt, requestPause as K, inside as Kn, gitRulesOf as Kt, addressOf as L, hasInRepoFiles as Ln, calledWorkflows as Lt, parseInputPairs as M, toolsInUse as Mn, firstLine as Mr, runScope as Mt, resolveInputs as N, createAgent as Nn, EXPERIMENTS as Nr, declaredProblem as Nt, git as O, applySteps as On, AGENT_TOOL_LAYOUTS as Or, stillTheSameProcess as Ot, driverRefusals as P, deleteAgent as Pn, experimentsOn as Pr, endedWith as Pt, listKey as Q, writePlainFile as Qn, createSignals as Qt, endLink as R, inRepoPlace as Rn, slotClock as Rt, isNodeEvent as S, socketRoom as Sn, INTEGRATIONS as Sr, pausedOf as St, BRANCH_PREFIX as T, maskReplacer as Tn, PREFERRED_DRIVER as Tr, slotWaitsOf as Tt, unlinkNodes as U, runDirOf as Un, nodeSays as Ut, peerOf as V, placeFor as Vn, startAt as Vt, flattenTree as W, shownPath as Wn, declaredOutputOf as Wt, detachResume as X, guardedBase as Xn, mcpNotText as Xt, adoptedCalls as Y, estimateTokens as Yn, MCP_FILE as Yt, resumable as Z, plainPathInside as Zn, serversIn as Zt, readProjectTriggers as _, parseTemplate as _n, isToolNode as _r, startRun as _t, releaseNotes as a, FEEDBACK_SEVERITIES as an, programsRunBy as ar, planReason as at, watchingPid as b, serveRun as bn, retiredDriverHint as br, heldTimeout as bt, detectInstaller as c, planPost as cn, serializeWorkflow as cr, explainSession as ct, parseVersion as d, publishPost as dn, WorkflowSchema as dr, sessionDriver as dt, outcomeEnd as en, WorkflowEditSchema as er, detachRun as et, lateTriggers as f, createFeedback as fn, CheckReportSchema as fr, parseReport as ft, declaredTriggers as g, evaluate as gn, isClaudeNode as gr, walkSummary as gt, triggerStanding as h, targetOf as hn, NodeSchema as hr, walkChecks as ht, planUpgrade as i, FEEDBACK_KINDS as in, beforeOf as ir, limitsReport as it, clearGitLocalEnv as j, toolLinks as jn, DRIVER_IDS as jr, scriptContactsOf as jt, gitOk as k, planMoveInto as kn, ORK_LAYOUT as kr, isItem as kt, compareVersions as l, runGh as ln, markValue as lr, planNodeSession as lt, createWatcher as m, normalizeTitle as mn, settingsOf as mr, checkWorkflowText as mt, resolveBase as n, END_KINDS as nn, newWorkflowText as nr, NOT_STARTED as nt, runningRunIds as o, FEEDBACK_TARGETS as on, renderJsonSchema as or, usd as ot, triggersReport as p, fingerprint as pn, expandEdge as pr, reportSchemaFor as pt, requestStop as q, projectRootOf as qn, loadProject as qt, capsHere as r, endFor as rn, readDocument as rr, limitsNow as rt, runningScript as s, DISCUSSIONS as sn, parseWorkflow as sr, explainWorkflowText as st, createWorkspace as t, END_DETAIL_LIMIT as tn, applyEdits as tr, followEvents as tt, isNewer as u, sendPost as un, EdgeInputSchema as ur, namingNoDriver as ut, workflowTextHash as v, renderTemplate as vn, DriverSchema as vr, MESSAGE_LIMIT as vt, storePath as w, MASK as wn, integrationFor as wr, runTree as wt, activeTree as x, socketPath as xn, timeoutMs as xr, liveStatus as xt, takeWatchLock as y, readCondition as yn, NodeIdSchema as yr, userMessageText as yt, endLinksOf as z, newProjectId as zn, slotWaitLine as zt };