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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/{__build_info__-Da73_nBs.mjs → __build_info__-B82-dGJY.mjs} +2 -2
  2. package/dist/{attach-E6NNyA3T.mjs → attach-CdtGX9se.mjs} +5 -5
  3. package/dist/bin.mjs +194 -215
  4. package/dist/{builtin-files-n6o6oWpB.mjs → builtin-files-Bdq23IIi.mjs} +1 -1
  5. package/dist/{chips-CyeB_rhR.mjs → chips-BqcT58qr.mjs} +1 -1
  6. package/dist/{completion-SNNNxnv9.mjs → completion--ywde-56.mjs} +52 -11
  7. package/dist/{context-DHudLA5e.mjs → context-Y_I4Mofq.mjs} +17 -7
  8. package/dist/{find-CjsNYAAf.mjs → find-BVX-bL4u.mjs} +1 -1
  9. package/dist/{format-C60TFpRe.mjs → format-CvTE5CZQ.mjs} +12 -12
  10. package/dist/{help-groups-CyfPWlST.mjs → help-groups-CpR2UzSn.mjs} +16 -6
  11. package/dist/{in-a-run-BYjTXaST.mjs → in-a-run-cL5N9gpH.mjs} +1 -1
  12. package/dist/{limits-C5QKZNLT.mjs → limits-Dhe--wmX.mjs} +1 -1
  13. package/dist/{mcp-bcctR-wc.mjs → mcp-CsmboyXv.mjs} +6 -6
  14. package/dist/{once-DPQu5Q3H.mjs → once-CrwI-ZdN.mjs} +4 -4
  15. package/dist/{output-CnRzrbUM.mjs → output-B1kwo0YU.mjs} +17 -1
  16. package/dist/{pause-BPvlDIvz.mjs → pause-DssrYcda.mjs} +4 -4
  17. package/dist/{relay-C8W0t_zc.mjs → relay-DASRgsTQ.mjs} +2 -2
  18. package/dist/{run-CQIfq918.mjs → run-D8divGCs.mjs} +1 -1
  19. package/dist/send-CXv8bx0i.mjs +105 -0
  20. package/dist/{src-CGJGL1sZ.mjs → src-8Mul0695.mjs} +8 -10
  21. package/dist/{src-D93tHZ5B.mjs → src-C7z_6fhn.mjs} +92 -39
  22. package/dist/{src-CqRqsb2t.mjs → src-njdtejgZ.mjs} +98 -33
  23. package/dist/{src-BeR6f70A.mjs → src-tsuZRu0H.mjs} +2353 -918
  24. package/dist/{store-CM3cERGl.mjs → store-CleF7KeS.mjs} +21 -73
  25. package/dist/ui/assets/app-D_nAPnHM.css +1 -0
  26. package/dist/ui/assets/{app-DYRC0V4p.js → app-DqBAtXSZ.js} +19 -19
  27. package/dist/ui/assets/{index-CWwm8HTG.js → index-D-_uPI2v.js} +2 -2
  28. package/dist/ui/index.html +1 -1
  29. package/dist/{ui-D2Ydofq0.mjs → ui-CNO1M0qT.mjs} +4 -4
  30. package/dist/{usage-6WIowblz.mjs → usage-BVXi0iFY.mjs} +13 -141
  31. package/dist/{verdict-CIy8tAyX.mjs → verdict-CMvPCpWC.mjs} +2 -2
  32. package/dist/{watch-uOU_WeBI.mjs → watch-DVNIciHp.mjs} +10 -8
  33. package/dist/{workflows-7tQdMnrA.mjs → workflows-B30QkwTk.mjs} +3 -3
  34. package/dist/{worktrees-ZCII298F.mjs → worktrees-DD4_TpkH.mjs} +3 -5
  35. package/package.json +2 -2
  36. package/dist/clean-Bh4KZgzc.mjs +0 -91
  37. package/dist/decide-DuIHACvH.mjs +0 -50
  38. package/dist/list-BUb5Uc3H.mjs +0 -117
  39. package/dist/remove-BkFWGVYo.mjs +0 -201
  40. package/dist/report-CEN8VoUb.mjs +0 -140
  41. package/dist/send-CaZlGG5a.mjs +0 -163
  42. package/dist/ui/assets/app-QoHVi-mb.css +0 -1
@@ -495,6 +495,8 @@ const forEachFields = { forEach: z.union([z.strictObject({
495
495
  maxItems: z.number().int().positive().describe("Most items allowed. A longer list asks whether to stop, run this many, or run all."),
496
496
  ...pacingFields
497
497
  })]).optional().describe("Run this node once per item of a list, with no model turn to fan out or gather. Items are `<id>.0`, `<id>.1`, ….") };
498
+ /** The nodes that run one thing may name a lock (RFC-016). */
499
+ const lockFields = { lock: z.string().regex(/\S/u, "a lock needs a name").describe("The lock this node holds, e.g. `branch:{{inputs.branch}}`. Held from the first node that names it to the last, or the run's end; one run at a time holds it, the others wait. One name per node.").optional() };
498
500
  // ../schema/src/settings.ts
499
501
  /**
500
502
  * @file The settings the workflow format shares between nodes, agents, defaults
@@ -562,9 +564,12 @@ const IsolationSchema = z.enum([
562
564
  "readonly"
563
565
  ]);
564
566
  const BudgetSchema = z.number().positive();
567
+ /** A cap in tokens: input, output and cache-write; cache reads don't count. */
568
+ const TokenBudgetSchema = z.number().int().positive();
565
569
  /** Caps across runs (RFC-007), in Claude Code's estimated dollars. */
566
570
  const QuotasSchema = z.strictObject({
567
571
  dayUsd: BudgetSchema.optional().describe("Most to spend in a calendar day."),
572
+ dayTokens: TokenBudgetSchema.optional().describe("Most tokens (input, output, cache-write) to use in a calendar day."),
568
573
  weekUsd: BudgetSchema.optional().describe("Most to spend in a calendar week, from Monday."),
569
574
  monthUsd: BudgetSchema.optional().describe("Most to spend in a calendar month.")
570
575
  });
@@ -651,6 +656,7 @@ const McpCallNodeSchema = z.strictObject({
651
656
  timeout: TimeoutSchema.optional().describe("How long the call may take, e.g. 5m; then the node fails. With watch:, how long one look may take."),
652
657
  watch: WatchSchema.optional().describe("Call the tool again and again until a condition holds, with no model turn."),
653
658
  ...notifyFields,
659
+ ...lockFields,
654
660
  ...forEachFields
655
661
  });
656
662
  // ../schema/src/steps.ts
@@ -698,6 +704,7 @@ const toolFields = {
698
704
  once: onceField,
699
705
  ...toolProgramFields,
700
706
  ...notifyFields,
707
+ ...lockFields,
701
708
  ...forEachFields
702
709
  };
703
710
  /** What every tool node has: `tool.<id>`, its command, allow list and cwd. */
@@ -727,7 +734,6 @@ const TOOL_NODE_SCHEMAS = INTEGRATIONS.map((i) => toolNodeSchema(i));
727
734
  * @file The node types of the workflow format (Claude, tool, control and gate
728
735
  * nodes) as Zod schemas.
729
736
  */
730
- const base = nodeBaseFields;
731
737
  /** A Claude node's output field that is not a list: a rule or JSON Schema. */
732
738
  const NotListSchema = z.unknown().refine((value) => typeof value !== "object" || value === null || !("list" in value), { message: LIST_RULE_HINT });
733
739
  const NODE_EFFORT_HINT = "expected low, medium, high, xhigh or max, or {{item.<field>}}";
@@ -749,6 +755,7 @@ const agentSettings = {
749
755
  context: z.array(z.string()).optional().describe("Node-layer context files."),
750
756
  isolation: IsolationSchema.optional(),
751
757
  budgetUsd: BudgetSchema.optional(),
758
+ budgetTokens: TokenBudgetSchema.optional(),
752
759
  maxTurns: TurnsSchema.optional(),
753
760
  timeout: TimeoutSchema.optional().describe("How long the node may work, e.g. 30m; time waiting on the user does not count."),
754
761
  practices: PracticesSchema.optional(),
@@ -768,6 +775,7 @@ const AGENT_SETTING_KEYS = [
768
775
  "context",
769
776
  "isolation",
770
777
  "budgetUsd",
778
+ "budgetTokens",
771
779
  "maxTurns",
772
780
  "timeout",
773
781
  "practices",
@@ -776,7 +784,7 @@ const AGENT_SETTING_KEYS = [
776
784
  "output"
777
785
  ];
778
786
  const claudeFields = {
779
- ...base,
787
+ ...nodeBaseFields,
780
788
  agent: AgentNameSchema.optional().describe("The agent that fills this node; its settings apply where the node sets none, and the prompt is its task."),
781
789
  prompt: z.string().optional().describe("Task brief. Supports {{inputs.*}} and {{nodes.<id>.output.*}} templates."),
782
790
  instructions: z.string().optional().describe("Node-layer instructions appended to the system prompt."),
@@ -796,6 +804,7 @@ const ClaudeWorkerNodeSchema = z.strictObject({
796
804
  ...claudeFields,
797
805
  mcp: McpNamesSchema,
798
806
  ...notifyFields,
807
+ ...lockFields,
799
808
  ...forEachFields,
800
809
  ...stepFields
801
810
  });
@@ -807,11 +816,11 @@ const ClaudeSubagentNodeSchema = z.strictObject({
807
816
  });
808
817
  const SplitNodeSchema = z.strictObject({
809
818
  type: z.literal("control.split"),
810
- ...base
819
+ ...nodeBaseFields
811
820
  });
812
821
  const JoinNodeSchema = z.strictObject({
813
822
  type: z.literal("control.join"),
814
- ...base,
823
+ ...nodeBaseFields,
815
824
  mode: z.enum([
816
825
  "all",
817
826
  "any",
@@ -821,7 +830,7 @@ const JoinNodeSchema = z.strictObject({
821
830
  });
822
831
  const RouterNodeSchema = z.strictObject({
823
832
  type: z.literal("control.router"),
824
- ...base,
833
+ ...nodeBaseFields,
825
834
  mode: z.enum(["first", "all"]).optional().describe("Follow the first matching edge (default), or every match.")
826
835
  });
827
836
  const DecisionOptionSchema = z.union([z.string(), z.strictObject({
@@ -831,7 +840,7 @@ const DecisionOptionSchema = z.union([z.string(), z.strictObject({
831
840
  })]);
832
841
  const HumanGateNodeSchema = z.strictObject({
833
842
  type: z.literal("human.gate"),
834
- ...base,
843
+ ...nodeBaseFields,
835
844
  ...notifyFields,
836
845
  question: z.string(),
837
846
  document: z.string().optional(),
@@ -842,7 +851,7 @@ const HumanGateNodeSchema = z.strictObject({
842
851
  });
843
852
  const WorkflowCallNodeSchema = z.strictObject({
844
853
  type: z.literal("workflow.call"),
845
- ...base,
854
+ ...nodeBaseFields,
846
855
  ...notifyFields,
847
856
  ...forEachFields,
848
857
  workflow: z.string().describe("The workflow's name, as in its project's workflows folder."),
@@ -871,12 +880,7 @@ const NodeSchema = z.discriminatedUnion("type", [
871
880
  McpCallNodeSchema
872
881
  ]);
873
882
  const isToolNode = (node) => node.type.startsWith(TOOL_PREFIX);
874
- const CLAUDE_NODE_TYPES = [
875
- "agent.leader",
876
- "agent.worker",
877
- "agent.subagent"
878
- ];
879
- const isClaudeNode = (node) => CLAUDE_NODE_TYPES.includes(node.type);
883
+ const isClaudeNode = (node) => node.type.startsWith("agent.");
880
884
  // ../schema/src/agents.ts
881
885
  /**
882
886
  * @file Agents: who fills a Claude node. An agent is a portable `<name>.md`
@@ -1066,6 +1070,7 @@ const DefaultsSchema = z.strictObject({
1066
1070
  effort: EffortSchema.optional(),
1067
1071
  isolation: IsolationSchema.optional(),
1068
1072
  budgetUsd: BudgetSchema.optional(),
1073
+ budgetTokens: TokenBudgetSchema.optional(),
1069
1074
  maxTurns: TurnsSchema.optional(),
1070
1075
  timeout: TimeoutSchema.optional().describe("For tool and Claude nodes that set none; a gate's own timeout is separate."),
1071
1076
  practices: PracticesSchema.optional(),
@@ -1180,6 +1185,7 @@ const WorkflowSchema = z.strictObject({
1180
1185
  agents: z.record(AgentNameSchema, AgentSchema).optional().describe("Agents defined in this workflow; they win over the project's and the user's of the same name."),
1181
1186
  limits: z.strictObject({
1182
1187
  budgetUsd: BudgetSchema.optional().describe("Hard cap for the whole run."),
1188
+ budgetTokens: TokenBudgetSchema.optional().describe("Hard cap for the whole run in tokens (input, output, cache-write)."),
1183
1189
  concurrency: z.number().int().positive().optional().describe("Max sessions running at once."),
1184
1190
  timeout: TimeoutSchema.optional().describe("How long the whole run may work, e.g. 2h; time waiting on the user does not count.")
1185
1191
  }).optional(),
@@ -1648,7 +1654,7 @@ const sharedAnchors = (doc) => {
1648
1654
  const refuseShared = (doc, node, path) => {
1649
1655
  if (isAlias(node) || isNode(node) && "anchor" in node && typeof node.anchor === "string" && sharedAnchors(doc).has(node.anchor)) throw new EditRefused(`${shownPath$1(path)} is shared through a YAML anchor, so changing it here would change another node too. Replace the anchor in the file first.`);
1650
1656
  };
1651
- const keyOf = (pair) => isScalar(pair.key) ? pair.key.value : pair.key;
1657
+ const keyOf$1 = (pair) => isScalar(pair.key) ? pair.key.value : pair.key;
1652
1658
  /** The value of a map's or list's child, as a node. */
1653
1659
  const childNode = (parent, key) => isMap(parent) || isSeq(parent) ? parent.get(key, true) : void 0;
1654
1660
  /** A key added to a map, before the first existing key that follows it. */
@@ -1657,7 +1663,7 @@ const insertPair = (doc, map, key, value, schema) => {
1657
1663
  const order = keysOf(schema, map.toJS(doc));
1658
1664
  const rank = order.indexOf(key);
1659
1665
  const next = rank === -1 ? -1 : map.items.findIndex((pair) => {
1660
- return order.indexOf(String(keyOf(pair))) > rank;
1666
+ return order.indexOf(String(keyOf$1(pair))) > rank;
1661
1667
  });
1662
1668
  const pair = doc.createPair(key, value);
1663
1669
  if (next === -1) map.items.push(pair);
@@ -1693,9 +1699,9 @@ const updated = (doc, old, value, schema, path) => {
1693
1699
  const updateMap = (doc, map, entries, schema, path) => {
1694
1700
  const keys = new Set(entries.map(([key]) => key));
1695
1701
  const whole = map.toJS(doc);
1696
- map.items = map.items.filter((pair) => keys.has(String(keyOf(pair))));
1702
+ map.items = map.items.filter((pair) => keys.has(String(keyOf$1(pair))));
1697
1703
  for (const [key, value] of entries) {
1698
- const pair = map.items.find((p) => String(keyOf(p)) === key);
1704
+ const pair = map.items.find((p) => String(keyOf$1(p)) === key);
1699
1705
  const child = childOf(schema, key, whole);
1700
1706
  if (pair === void 0) insertPair(doc, map, key, value, schema);
1701
1707
  else pair.value = updated(doc, pair.value, value, child, [...path, key]);
@@ -1729,7 +1735,7 @@ const emptyFor = (next) => typeof next === "number" ? new YAMLSeq() : new YAMLMa
1729
1735
  /** Sets a child that may not exist yet in its map or list. */
1730
1736
  const setChild = (doc, parent, key, value, schema, path) => {
1731
1737
  if (isMap(parent) && typeof key === "string") {
1732
- const pair = parent.items.find((p) => String(keyOf(p)) === key);
1738
+ const pair = parent.items.find((p) => String(keyOf$1(p)) === key);
1733
1739
  if (pair === void 0) insertPair(doc, parent, key, value, schema);
1734
1740
  else pair.value = value;
1735
1741
  return;
@@ -3412,7 +3418,7 @@ const lstatOrUndefined = (target) => {
3412
3418
  return;
3413
3419
  }
3414
3420
  };
3415
- const stateOf$1 = (root, relative, origin) => {
3421
+ const stateOf$2 = (root, relative, origin) => {
3416
3422
  const at = path.join(root, relative);
3417
3423
  const stat = lstatOrUndefined(at);
3418
3424
  if (stat === void 0) return { state: "missing" };
@@ -3442,7 +3448,7 @@ const toolLinks = (projectRoot, tools = linkableTools()) => tools.flatMap((id) =
3442
3448
  kind,
3443
3449
  path: folder,
3444
3450
  origin,
3445
- ...stateOf$1(projectRoot, folder, origin)
3451
+ ...stateOf$2(projectRoot, folder, origin)
3446
3452
  }];
3447
3453
  });
3448
3454
  });
@@ -3942,8 +3948,8 @@ const socketRoom = (home = homedir(), platform = process.platform) => ({
3942
3948
  max: SOCKET_PATH_MAX[platform]
3943
3949
  });
3944
3950
  /** For errors another handler already settles. */
3945
- const ignore$1 = () => {};
3946
- const messageOf$2 = (line) => {
3951
+ const ignore$2 = () => {};
3952
+ const messageOf$3 = (line) => {
3947
3953
  try {
3948
3954
  const value = JSON.parse(line);
3949
3955
  if (value === null || typeof value !== "object" || !("type" in value)) return;
@@ -3970,9 +3976,9 @@ const serveRun = async (options) => {
3970
3976
  followers.delete(socket);
3971
3977
  });
3972
3978
  const lines = createInterface({ input: socket });
3973
- lines.on("error", ignore$1);
3979
+ lines.on("error", ignore$2);
3974
3980
  lines.on("line", (line) => {
3975
- const message = messageOf$2(line);
3981
+ const message = messageOf$3(line);
3976
3982
  if (message?.type === "wake") options.onWake();
3977
3983
  if (message?.type === "follow") {
3978
3984
  for (const stored of options.backlog(message.after)) socket.write(`${JSON.stringify(stored)}\n`);
@@ -4031,7 +4037,7 @@ const followSocket = (file, after, onEvent, signal) => {
4031
4037
  })}\n`);
4032
4038
  });
4033
4039
  const lines = createInterface({ input: socket });
4034
- lines.on("error", ignore$1);
4040
+ lines.on("error", ignore$2);
4035
4041
  lines.on("line", (line) => {
4036
4042
  try {
4037
4043
  onEvent(JSON.parse(line));
@@ -4954,7 +4960,7 @@ const END_KINDS = {
4954
4960
  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."),
4955
4961
  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."),
4956
4962
  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."),
4957
- 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`."),
4963
+ budget_limit: info("Budget limit", "workflow", (f) => `reached its budget${paren([...f.budgetUsd === void 0 ? [] : [usd$1(f.budgetUsd)], ...f.budgetTokens === void 0 ? [] : [`${String(f.budgetTokens)} tokens`]].join(", ") || void 0)}`, "Raise `budgetUsd` or `budgetTokens` on the node, or the same in the workflow's `limits`, then `ork resume`."),
4958
4964
  timed_out: info("Timed out", "workflow", (f) => `reached its timeout${paren(f.timeout)}`, "Raise its `timeout`, or give it a smaller task."),
4959
4965
  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."),
4960
4966
  plan_limit: info("Plan limit", "account", (f) => `ran into a usage limit${paren(f.window?.replaceAll("_", " "))}`, `Wait until the limit resets. ${RETRY}`, {
@@ -7160,7 +7166,8 @@ const TEMPLATE_FIELDS = [
7160
7166
  "document",
7161
7167
  "workflow",
7162
7168
  "project",
7163
- "ref"
7169
+ "ref",
7170
+ "lock"
7164
7171
  ];
7165
7172
  /**
7166
7173
  * The templates of a node that may read `{{item}}`, beside `inputs`,
@@ -7207,6 +7214,14 @@ const checkReferences = (workflow, edges, rules = /* @__PURE__ */ new Map()) =>
7207
7214
  }))));
7208
7215
  const fromMcp = Object.entries(workflow.nodes).flatMap(([id, node]) => node.type === "mcp.call" ? stringsIn(node.arguments ?? {}, ["arguments"]).flatMap(({ at, text }) => checkTemplate(id, at, text, context, itemOf(id))) : []);
7209
7216
  const fromSteps = Object.entries(workflow.nodes).flatMap(([id, node]) => isToolNode(node) ? [] : programsOf(node).flatMap(({ at, program }) => program.command === void 0 ? [] : checkTemplate(id, [...at, "command"], program.command, context)));
7217
+ const fromLocks = Object.entries(workflow.nodes).flatMap(([id, node]) => {
7218
+ const parsed = "lock" in node ? parseTemplate(node.lock ?? "") : void 0;
7219
+ return parsed?.ok === true ? parsed.parts.flatMap((part) => typeof part === "string" ? [] : collectPaths(part.expr).filter(([root]) => root === "nodes").map((segments) => error("lock.scope", `\`${describePath(segments)}\`: a lock's name reads only \`inputs\`, since it is taken before the node runs`, [
7220
+ "nodes",
7221
+ id,
7222
+ "lock"
7223
+ ]))) : [];
7224
+ });
7210
7225
  const fromOutputs = Object.entries(workflow.outputs ?? {}).flatMap(([name, value]) => {
7211
7226
  const parsed = parseTemplate(value);
7212
7227
  const path = ["outputs", name];
@@ -7220,6 +7235,7 @@ const checkReferences = (workflow, edges, rules = /* @__PURE__ */ new Map()) =>
7220
7235
  ...fromEnv,
7221
7236
  ...fromSteps,
7222
7237
  ...fromMcp,
7238
+ ...fromLocks,
7223
7239
  ...checkToolEnv(workflow),
7224
7240
  ...fromOutputs,
7225
7241
  ...checkConditionRefs(workflow, edges, rules, context)
@@ -7566,780 +7582,940 @@ const slotClock = (base, machine) => {
7566
7582
  }
7567
7583
  };
7568
7584
  };
7569
- // ../core/src/project/calls.ts
7585
+ // ../core/src/run/declared.ts
7570
7586
  /**
7571
- * @file `ork validate`'s check of `workflow.call` nodes against what they call
7572
- * (RFC-004): the project, the workflow, the node and the required inputs. A
7573
- * project not registered here is only a warning, since it may be on another
7574
- * machine; a run refuses it. A name with a template is left to the run.
7587
+ * @file Holding a node to what it declares of its output (RFC-010): a missing
7588
+ * field, the wrong type or a value outside the list is a problem to fail the
7589
+ * node on, so a condition never quietly matches nothing. A declared list
7590
+ * (RFC-013) is held to being a list, and each item to its shape. It reads the
7591
+ * format's own shape; a field written out as JSON Schema stays the driver's
7592
+ * to enforce. A list a resume hands back keeps the file's real values when it
7593
+ * only differs by masking.
7575
7594
  */
7576
- const hasTemplate = (text) => text.includes("{{");
7577
- /** A workflow's file: a folder of that name is no workflow. */
7578
- const fileState = (file) => {
7579
- try {
7580
- return statSync(file, { throwIfNoEntry: false })?.isFile() === true ? "file" : "none";
7581
- } catch {
7582
- return "unreadable";
7583
- }
7595
+ const TYPE_NAMES = {
7596
+ string: "text",
7597
+ number: "a number",
7598
+ integer: "a whole number",
7599
+ boolean: "true or false"
7584
7600
  };
7585
- /** A workflow from its file, or `null` when it cannot be read or is not valid. */
7586
- const readWorkflow = (file) => {
7587
- try {
7588
- const parsed = parseWorkflow(readFileSync(file, "utf8"));
7589
- return parsed.ok ? parsed.workflow : null;
7590
- } catch {
7591
- return null;
7592
- }
7601
+ const FITS = {
7602
+ string: (value) => typeof value === "string",
7603
+ number: (value) => typeof value === "number",
7604
+ integer: (value) => Number.isInteger(value),
7605
+ boolean: (value) => typeof value === "boolean"
7606
+ };
7607
+ const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
7608
+ const listed = (values) => {
7609
+ const names = values.map((value) => markValue(String(value)));
7610
+ const last = names.pop();
7611
+ return names.length === 0 ? String(last) : `${names.join(", ")} or ${String(last)}`;
7612
+ };
7613
+ const shown = (value) => {
7614
+ const text = typeof value === "string" ? value : JSON.stringify(value) ?? String(value);
7615
+ return markValue(text.length > 80 ? `${text.slice(0, 80)}…` : text);
7616
+ };
7617
+ /** What's wrong with one field, if it's a shape this can read. */
7618
+ const fieldProblem = (name, rule, value) => {
7619
+ const shape = listShape(rule);
7620
+ if (shape !== void 0) return listProblem(name, shape, value);
7621
+ const expected = Array.isArray(rule) ? listed(rule) : typeof rule === "string" ? TYPE_NAMES[rule] : void 0;
7622
+ if (expected === void 0) return null;
7623
+ if (value === void 0) return `${name} is missing, expected ${expected}`;
7624
+ return (Array.isArray(rule) ? rule.some((item) => item === value) : FITS[String(rule)]?.(value)) === true ? null : `${name} is ${shown(value)}, expected ${expected}`;
7625
+ };
7626
+ const fieldProblems = (declared, value, prefix) => Object.entries(declared).flatMap(([name, rule]) => {
7627
+ const problem = fieldProblem(`${prefix}${name}`, rule, value[name]);
7628
+ return problem === null ? [] : [problem];
7629
+ });
7630
+ /** What's wrong with one item; `name` says which, as `tasks[2]`. */
7631
+ const itemProblems = (name, shape, item) => {
7632
+ if (Object.keys(shape).length === 0) return [];
7633
+ return isRecord$2(item) ? fieldProblems(shape, item, `${name}.`) : [`${name} is ${shown(item)}, expected an object`];
7634
+ };
7635
+ /** Only the first bad item is named, so a long list can't flood the reason. */
7636
+ const listProblem = (name, shape, value) => {
7637
+ if (value === void 0) return `${name} is missing, expected a list`;
7638
+ if (!Array.isArray(value)) return `${name} is ${shown(value)}, expected a list`;
7639
+ const bad = value.map((item, index) => itemProblems(`${name}[${index}]`, shape, item)).find((problems) => problems.length > 0);
7640
+ return bad === void 0 ? null : bad.join("; ");
7593
7641
  };
7594
7642
  /**
7595
- * The workflows a workflow calls by a literal name, and those they call. One
7596
- * that cannot be read is left out: `ork validate` and the call report it. A
7597
- * call of one `node:` runs only that session, so its tool nodes do not count
7598
- * unless `everyCall`, and nor does a call among `finished`. `unreadable` hears
7599
- * of one that is there but could not be read.
7643
+ * What's wrong with `value` by this declaration, or `null`. With nothing
7644
+ * declared, nothing can be.
7600
7645
  */
7601
- const calledWorkflows = (workflow, own, registered, options = {}) => {
7602
- const seen = /* @__PURE__ */ new Set();
7603
- const found = [];
7604
- const walk = (from, place) => {
7605
- for (const [id, node] of Object.entries(from.nodes)) {
7606
- if (node.type !== "workflow.call") continue;
7607
- if (hasTemplate(node.workflow) || hasTemplate(node.project ?? "")) continue;
7608
- if (options.everyCall !== true && node.node !== void 0) continue;
7609
- if (from === workflow && options.finished?.has(id) === true) continue;
7610
- const target = node.project === void 0 ? place : registered.find((other) => other.id === node.project);
7611
- if (target === void 0) continue;
7612
- const file = path.join(placeFiles(target).workflows, `${path.basename(node.workflow)}.ork.yaml`);
7613
- if (seen.has(file)) continue;
7614
- seen.add(file);
7615
- const state = fileState(file);
7616
- if (state === "none") continue;
7617
- const called = state === "file" ? readWorkflow(file) : null;
7618
- if (called === null) {
7619
- options.unreadable?.(node.workflow);
7620
- continue;
7621
- }
7622
- found.push(called);
7623
- walk(called, target);
7624
- }
7625
- };
7626
- walk(workflow, own);
7627
- return found;
7646
+ const declaredProblem = (declared, value) => {
7647
+ if (declared === void 0 || Object.keys(declared).length === 0) return null;
7648
+ if (!isRecord$2(value)) return `the result isn't a JSON object`;
7649
+ const problems = fieldProblems(declared, value, "");
7650
+ return problems.length === 0 ? null : problems.join("; ");
7628
7651
  };
7629
- const checkCallTargets = (workflow, own, registered) => Object.entries(workflow.nodes).flatMap(([id, node]) => {
7630
- if (node.type !== "workflow.call") return [];
7631
- const at = ["nodes", id];
7632
- if (hasTemplate(node.workflow) || hasTemplate(node.project ?? "")) return [];
7633
- if (/[/\\]/u.test(node.workflow) || node.workflow === "..") return [error("call.workflow", `call \`${id}\`: \`${node.workflow}\` is not a workflow's name`, [...at, "workflow"])];
7634
- const target = node.project === void 0 ? own : registered.find((place) => place.id === node.project);
7635
- if (target === void 0) return [warning("call.project", `call \`${id}\`: project \`${String(node.project)}\` is not registered here, so a run will refuse it; register it with \`ork register\``, [...at, "project"])];
7636
- const file = path.join(placeFiles(target).workflows, `${node.workflow}.ork.yaml`);
7637
- const state = fileState(file);
7638
- if (state === "none") return [error("call.workflow", `call \`${id}\`: no workflow \`${node.workflow}\` in ${target.root}`, [...at, "workflow"])];
7639
- const called = state === "file" ? readWorkflow(file) : null;
7640
- if (called === null) return [error("call.workflow", `call \`${id}\`: the workflow \`${node.workflow}\` cannot be read or is not valid; run \`ork validate\` on it`, [...at, "workflow"])];
7641
- const found = [];
7642
- if (node.node !== void 0) {
7643
- const type = called.nodes[node.node]?.type;
7644
- if (type !== "agent.leader" && type !== "agent.worker") found.push(error("call.node", `call \`${id}\`: \`${node.node}\` is not a leader or worker of \`${node.workflow}\``, [...at, "node"]));
7645
- }
7646
- for (const [name, input] of Object.entries(called.inputs ?? {})) if (input.required === true && input.default === void 0 && node.inputs?.[name] === void 0) found.push(error("call.input", `call \`${id}\`: \`${node.workflow}\` requires the input \`${name}\``, [...at, "inputs"]));
7647
- return found;
7652
+ /**
7653
+ * The list a resume hands back. Events are stored masked, so a static list that
7654
+ * still masks to the record is the file's, with its real values.
7655
+ */
7656
+ const unmasked = (forEach, recorded) => "items" in forEach && JSON.stringify(forEach.items, maskReplacer) === JSON.stringify(recorded) ? [...forEach.items] : recorded;
7657
+ // ../core/src/run/ended.ts
7658
+ /**
7659
+ * @file A node's result as it ended (specs/runtime/session-ends.md): one place
7660
+ * that sets `end`, makes a failure's `error` its message and marks a
7661
+ * transient kind, and that the engine's modules build ends through.
7662
+ */
7663
+ /**
7664
+ * The result as it ended: a failure's `error` is the end's message (what the
7665
+ * run summary lists), and a transient kind marks it `transient`.
7666
+ */
7667
+ const endedWith = (result, end) => ({
7668
+ ...result,
7669
+ end,
7670
+ ...result.ok ? {} : { error: end.message },
7671
+ ...END_KINDS[end.kind].transient ? { transient: true } : {}
7648
7672
  });
7649
- // ../core/src/run/routing.ts
7673
+ /** The reason a run's signal is aborted with when `timeout` passed. */
7674
+ const runTimedOut = (timeout) => timeout === void 0 ? { kind: "timed_out" } : {
7675
+ kind: "timed_out",
7676
+ timeout
7677
+ };
7678
+ const isTimedOut = (reason) => typeof reason === "object" && reason !== null && "kind" in reason && reason.kind === "timed_out";
7650
7679
  /**
7651
- * @file Where a finished node's output goes: which outgoing edges are taken,
7652
- * and what each arrival does at its target (activate it, skip it, or wait,
7653
- * for a join). Pure: the scheduler owns time, sessions and loop counters.
7680
+ * The end of a node its run's signal stopped: `timed_out` when the run's
7681
+ * timeout aborted it, `interrupted` otherwise.
7654
7682
  */
7655
- /** A `maxRounds` edge. Removing them leaves a DAG (the compiler checks it). */
7656
- const isLoopEdge = (edge) => edge.maxRounds !== void 0;
7657
- const keys = /* @__PURE__ */ new WeakMap();
7658
- const edgeKey = (edge) => {
7659
- let key = keys.get(edge);
7660
- if (key === void 0) {
7661
- key = `${String(edge.source)}:${edge.from}->${edge.to}`;
7662
- keys.set(edge, key);
7663
- }
7664
- return key;
7683
+ const stoppedEnd = (signal, detail) => {
7684
+ const reason = signal.reason;
7685
+ if (!isTimedOut(reason)) return endFor("interrupted", {}, detail);
7686
+ const facts = reason.timeout === void 0 ? {} : { timeout: reason.timeout };
7687
+ return endFor("timed_out", facts, detail);
7665
7688
  };
7689
+ /** A failure that ended as `end`, its message the output's summary. */
7690
+ const failedWith = (end, output) => endedWith({
7691
+ ok: false,
7692
+ output: output ?? {
7693
+ status: "fail",
7694
+ summary: end.message
7695
+ }
7696
+ }, end);
7666
7697
  /**
7667
- * Decides every outgoing edge of a finished node. An edge with `when:` is taken
7668
- * when its condition holds for the node's output; one without is taken when the
7669
- * node succeeded. A `control.router` in `first` mode takes only its first
7670
- * matching edge.
7698
+ * The result with its end's detail scrubbed with the feedback redactor (the
7699
+ * run's `roots` and the home folder), as a session's is
7700
+ * (specs/runtime/session-ends.md), whichever node type set it.
7671
7701
  */
7672
- const decideEdges = (graph, id, result, scope) => {
7673
- const compiled = graph.nodes.get(id);
7674
- if (compiled === void 0) return [];
7675
- const { node } = compiled;
7676
- const firstOnly = node.type === "control.router" && (node.mode ?? "first") === "first";
7677
- let matched = false;
7678
- return compiled.outgoing.map((edge) => {
7679
- const taken = (edge.condition === void 0 ? result.ok : truthy(evaluate(edge.condition, {
7680
- ...scope,
7681
- output: {
7682
- ok: result.ok,
7683
- ...result.output
7684
- }
7685
- }))) && !(firstOnly && matched);
7686
- if (taken) matched = true;
7687
- return {
7688
- edge,
7689
- taken
7690
- };
7702
+ const scrubbedEnd = (result, roots) => {
7703
+ const { end } = result;
7704
+ if (end?.detail === void 0) return result;
7705
+ const detail = redact(end.detail, {
7706
+ roots,
7707
+ home: homedir()
7691
7708
  });
7692
- };
7693
- const joinOutput = (edges, resolved) => {
7694
- const ran = edges.filter((e) => resolved.get(edgeKey(e))?.taken === true);
7695
- const results = {};
7696
- for (const edge of ran) results[edge.from] = resolved.get(edgeKey(edge))?.output ?? {};
7697
7709
  return {
7698
- statuses: ran.map((e) => results[e.from]?.["status"]),
7699
- results,
7700
- count: ran.length
7710
+ ...result,
7711
+ end: {
7712
+ ...end,
7713
+ detail
7714
+ }
7701
7715
  };
7702
7716
  };
7703
- /** How a reason for a run that was left waiting starts: a bug in Ork. */
7704
- const STUCK_REASON = "ork engine error:";
7717
+ // ../core/src/run/slots.ts
7705
7718
  /**
7706
- * Why a run that was left waiting fails: a node neither ran nor was skipped
7707
- * while an edge could still reach it. The scheduler resolves every edge whose
7708
- * source can't run any more, so only a bug in Ork leaves one.
7719
+ * @file Session slots: at most `limits.concurrency` Claude sessions run at
7720
+ * once. Queued activations take a free slot when the scheduler pumps;
7721
+ * delegated children and leaders returning from `delegate` wait for one and
7722
+ * are served first, so a leader waiting on its children never holds a slot
7723
+ * they need.
7709
7724
  */
7710
- const stuckReason = (waiting) => `${STUCK_REASON} ${waiting.map(({ id, waitingOn }) => `${id} was still waiting on ${waitingOn.join(", ")}`).join("; ")} with nothing left to run; this is a bug in Ork, not in the workflow`;
7711
- /** Each node's place along the non-loop edges, sources first. */
7712
- const orderOf = (graph) => {
7713
- const order = /* @__PURE__ */ new Map();
7714
- const left = /* @__PURE__ */ new Map();
7715
- for (const [id, compiled] of graph.nodes) left.set(id, compiled.incoming.filter((e) => !isLoopEdge(e)).length);
7716
- const ready = [...left].filter(([, count]) => count === 0).map(([id]) => id);
7717
- for (let id = ready.shift(); id !== void 0; id = ready.shift()) {
7718
- order.set(id, order.size);
7719
- for (const edge of graph.nodes.get(id)?.outgoing ?? []) {
7720
- if (isLoopEdge(edge)) continue;
7721
- const count = (left.get(edge.to) ?? 0) - 1;
7722
- left.set(edge.to, count);
7723
- if (count === 0) ready.push(edge.to);
7724
- }
7725
- }
7726
- return order;
7727
- };
7728
- /**
7729
- * Tracks, per target, which incoming non-loop edges have resolved in its
7730
- * current round: since every one last resolved.
7731
- *
7732
- * - A node runs each time an incoming edge is taken, and is skipped once every
7733
- * incoming non-loop edge has resolved as not taken (dead-path elimination),
7734
- * so nothing downstream waits on a branch that will never run.
7735
- * - A `control.join` waits: `all` until every input resolved, `any` for the first
7736
- * that ran, `n` for `n` that ran. A join none of whose required inputs ran is
7737
- * skipped.
7738
- * - An edge whose source can't run any more counts as not taken (`stranded`): in
7739
- * a later round, a branch the loop doesn't go back to never runs again.
7740
- */
7741
- const createInbox = (graph) => {
7742
- const resolved = /* @__PURE__ */ new Map();
7743
- /** Targets that ran, or joins that fired, in their current round. */
7744
- const fired = /* @__PURE__ */ new Set();
7745
- let order;
7746
- const forwards = /* @__PURE__ */ new Map();
7747
- const forwardIn = (id) => {
7748
- let edges = forwards.get(id);
7749
- if (edges === void 0) {
7750
- edges = (graph.nodes.get(id)?.incoming ?? []).filter((e) => !isLoopEdge(e));
7751
- forwards.set(id, edges);
7752
- }
7753
- return edges;
7754
- };
7755
- const reset = (id) => {
7756
- resolved.delete(id);
7757
- fired.delete(id);
7758
- };
7759
- const arriveAtJoin = (id, seen) => {
7760
- const node = graph.nodes.get(id)?.node;
7761
- if (node?.type !== "control.join") return { kind: "wait" };
7762
- const inputs = forwardIn(id);
7763
- const ran = inputs.filter((e) => seen.get(edgeKey(e))?.taken === true);
7764
- const complete = inputs.every((e) => seen.has(edgeKey(e)));
7765
- const mode = node.mode ?? "all";
7766
- const needed = mode === "any" ? 1 : node.n ?? inputs.length;
7767
- if (mode === "all") {
7768
- if (!complete) return { kind: "wait" };
7769
- reset(id);
7770
- return ran.length > 0 ? {
7771
- kind: "activate",
7772
- input: joinOutput(inputs, seen)
7773
- } : { kind: "skip" };
7774
- }
7775
- if (!fired.has(id) && ran.length >= needed) {
7776
- const input = joinOutput(inputs, seen);
7777
- if (complete) reset(id);
7778
- else fired.add(id);
7779
- return {
7780
- kind: "activate",
7781
- input
7782
- };
7725
+ /** Only a Claude session takes a slot; tools and gates are not limited. */
7726
+ const needsSlot = (node) => isClaudeNode(node);
7727
+ /** A call spends through its run, so it needs budget as a session does. */
7728
+ const spendsBudget = (node) => needsSlot(node) || node.type === "workflow.call";
7729
+ const createSlots = (limit, onFree) => {
7730
+ let used = 0;
7731
+ const waiting = [];
7732
+ return {
7733
+ limit,
7734
+ tryTake: () => {
7735
+ if (used >= limit || waiting.length > 0) return false;
7736
+ used += 1;
7737
+ return true;
7738
+ },
7739
+ take: () => {
7740
+ if (used < limit && waiting.length === 0) {
7741
+ used += 1;
7742
+ return Promise.resolve();
7743
+ }
7744
+ const { promise, resolve } = Promise.withResolvers();
7745
+ waiting.push(resolve);
7746
+ return promise;
7747
+ },
7748
+ release: () => {
7749
+ const next = waiting.shift();
7750
+ if (next === void 0) {
7751
+ used -= 1;
7752
+ onFree();
7753
+ } else next();
7783
7754
  }
7784
- if (!complete) return { kind: "wait" };
7785
- const wasFired = fired.has(id);
7786
- reset(id);
7787
- return wasFired ? { kind: "wait" } : { kind: "skip" };
7788
7755
  };
7789
- /** Records that `edge` resolved, and says what happens at its target. */
7790
- const arrive = (edge, taken, output) => {
7791
- const id = edge.to;
7792
- const seen = resolved.get(id) ?? /* @__PURE__ */ new Map();
7793
- resolved.set(id, seen);
7794
- seen.set(edgeKey(edge), {
7795
- taken,
7796
- output
7797
- });
7798
- if (graph.nodes.get(id)?.node.type === "control.join") return arriveAtJoin(id, seen);
7799
- const complete = forwardIn(id).every((e) => seen.has(edgeKey(e)));
7800
- const ran = taken || fired.has(id);
7801
- if (complete) reset(id);
7802
- else if (taken) fired.add(id);
7803
- if (taken) return {
7804
- kind: "activate",
7805
- input: output
7806
- };
7807
- return complete && !ran ? { kind: "skip" } : { kind: "wait" };
7756
+ };
7757
+ // ../core/src/run/fan-out.ts
7758
+ /** The node's `forEach`, whatever its type; `undefined` when it has none. */
7759
+ const forEachOf = (node) => node !== void 0 && "forEach" in node ? node.forEach : void 0;
7760
+ /** The list a node runs over, or why there isn't one. */
7761
+ const listOf = (forEach, scope) => {
7762
+ if ("items" in forEach) return {
7763
+ ok: true,
7764
+ list: forEach.items
7808
7765
  };
7809
- /** The edges a target still waits on. */
7810
- const missingOf = (id, seen) => forwardIn(id).filter((e) => !seen.has(edgeKey(e)));
7811
- /**
7812
- * The first target, in graph order, whose every edge still to resolve comes
7813
- * from a node that can't run any more: neither `active` (queued or running)
7814
- * nor after one along the non-loop edges. A loop's edge starts a new round,
7815
- * so it doesn't count. Its edges count as not taken; the caller delivers
7816
- * them, and asks again, as that can start something.
7817
- */
7818
- const stranded = (active) => {
7819
- const candidates = [...resolved].map(([id, seen]) => ({
7820
- id,
7821
- missing: missingOf(id, seen)
7822
- })).filter(({ missing }) => !missing.some((e) => active(e.from)));
7823
- if (candidates.length === 0) return [];
7824
- /** Nodes known to be unable to run, in this call. */
7825
- const dead = /* @__PURE__ */ new Set();
7826
- const canRun = (from) => {
7827
- const visited = /* @__PURE__ */ new Set([from]);
7828
- const stack = [from];
7829
- for (let id = stack.pop(); id !== void 0; id = stack.pop()) {
7830
- if (active(id)) return true;
7831
- for (const edge of forwardIn(id)) {
7832
- if (visited.has(edge.from) || dead.has(edge.from)) continue;
7833
- visited.add(edge.from);
7834
- stack.push(edge.from);
7835
- }
7836
- }
7837
- for (const id of visited) dead.add(id);
7838
- return false;
7839
- };
7840
- order ??= orderOf(graph);
7841
- const known = order;
7842
- const place = (id) => known.get(id) ?? Infinity;
7843
- return candidates.filter(({ missing }) => !missing.some((e) => canRun(e.from))).toSorted((a, b) => place(a.id) - place(b.id))[0]?.missing ?? [];
7766
+ const read = templateValue(forEach.from, scope);
7767
+ if (!read.ok) return read;
7768
+ const { value } = read;
7769
+ if (Array.isArray(value)) return {
7770
+ ok: true,
7771
+ list: value
7844
7772
  };
7845
- /** Targets an edge reached that still wait on others. */
7846
- const waiting = () => [...resolved].map(([id, seen]) => ({
7847
- id,
7848
- waitingOn: [...new Set(missingOf(id, seen).map((e) => e.from))]
7849
- }));
7773
+ const name = String(templatePath(forEach.from)?.at(-1) ?? forEach.from);
7850
7774
  return {
7851
- arrive,
7852
- stranded,
7853
- waiting
7775
+ ok: false,
7776
+ error: declaredProblem({ [name]: { list: {} } }, { [name]: value }) ?? `${name} is not a list`
7854
7777
  };
7855
7778
  };
7856
- // ../core/src/run/escalation.ts
7857
- const LOOP_CHOICES = {
7858
- stop: "stop",
7859
- again: "again",
7860
- onward: "onward"
7861
- };
7862
- /** What a list over its `maxItems` can be answered with (RFC-013). */
7863
- const EACH_CHOICES = {
7864
- stop: "stop",
7865
- first: "first",
7866
- all: "all"
7779
+ /** The output a `from` list comes from, whose freshness resume asks about. */
7780
+ const sourceOf = (forEach, scope) => {
7781
+ if (!("from" in forEach)) return void 0;
7782
+ const [root, id] = templatePath(forEach.from) ?? [];
7783
+ return root === "nodes" ? scope.nodes[String(id)]?.output : void 0;
7867
7784
  };
7868
- /** The question about a list over its cap; the recommended choice stops. */
7869
- const eachQuestion = (node, count, maxItems, round = 1) => ({
7870
- question: `The list of ${node}${round > 1 ? ` (round ${String(round)})` : ""} has ${String(count)} items, more than its maxItems of ${String(maxItems)}. What now?`,
7871
- options: [
7872
- {
7873
- id: EACH_CHOICES.stop,
7874
- description: "Stop the run"
7875
- },
7876
- {
7877
- id: EACH_CHOICES.first,
7878
- description: `Run the first ${String(maxItems)}`
7879
- },
7880
- {
7881
- id: EACH_CHOICES.all,
7882
- description: `Run all ${String(count)}`
7883
- }
7884
- ],
7885
- recommended: EACH_CHOICES.stop,
7886
- context: `${node} runs at most ${String(maxItems)} items unless someone says otherwise.`
7887
- });
7888
- /** The question about a loop out of rounds; the recommended choice stops. */
7889
- const loopQuestion = (edge) => ({
7890
- question: `The loop ${edge.from} -> ${edge.to} used all ${String(edge.maxRounds ?? 0)} of its rounds. What now?`,
7891
- options: [
7892
- {
7893
- id: LOOP_CHOICES.stop,
7894
- description: "Stop the run"
7895
- },
7896
- {
7897
- id: LOOP_CHOICES.again,
7898
- description: "Allow one more round"
7899
- },
7900
- {
7901
- id: LOOP_CHOICES.onward,
7902
- description: "Go on along the other edges"
7903
- }
7904
- ],
7905
- recommended: LOOP_CHOICES.stop,
7906
- context: `${edge.from} still wants another round${edge.when === void 0 ? "" : ` (${edge.when.trim()})`}.`
7907
- });
7785
+ const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
7908
7786
  /**
7909
- * Asks the owner of `edge.from` while its session is live (the node runs in the
7910
- * flow, so its owner is not waiting on it), and the human otherwise or when the
7911
- * owner's session ends first.
7787
+ * What `results` keeps of an item: its declared fields (a program's are under
7788
+ * `json`), its summary, Ork's own branch and worktree, and how a program
7789
+ * failed. The full output stays in the item's own `node.finished`.
7912
7790
  */
7913
- const askAboutLoop = async (human, edge, bus) => {
7914
- const question = loopQuestion(edge);
7915
- return await bus?.askOwner(edge.from, question) ?? await bus?.askOwner(edge.to, question) ?? human.ask({
7916
- nodeId: edge.from,
7917
- ...question
7918
- });
7919
- };
7920
- /** Counts each loop edge's rounds; `take` is false once one is out of them. */
7921
- const createLoops = (options) => {
7922
- const taken = /* @__PURE__ */ new Map();
7923
- const extra = /* @__PURE__ */ new Map();
7924
- const take = async (edge, output) => {
7925
- const key = edgeKey(edge);
7926
- const count = taken.get(key) ?? 0;
7927
- if (count < (edge.maxRounds ?? 0) + (extra.get(key) ?? 0)) {
7928
- taken.set(key, count + 1);
7929
- options.activate(edge.to, output);
7791
+ const entryOf = (declared, { ok, output, error }) => {
7792
+ const json = isRecord$1(output["json"]) ? output["json"] : {};
7793
+ const fields = Object.keys(declared ?? {}).map((field) => [field, Object.hasOwn(json, field) ? json[field] : output[field]]);
7794
+ const own = [
7795
+ "summary",
7796
+ "branch",
7797
+ "worktree",
7798
+ "merge"
7799
+ ].map((field) => [field, output[field]]);
7800
+ const failed = !ok && typeof output["exitCode"] === "number" ? [["exitCode", output["exitCode"]], ["error", error]] : [];
7801
+ return Object.fromEntries([
7802
+ ...fields,
7803
+ ...own,
7804
+ ...failed
7805
+ ].filter(([, value]) => value !== void 0));
7806
+ };
7807
+ /** Whether an item passed but its work could not reach its owner. */
7808
+ const mergeFailed = ({ output }) => isRecord$1(output["merge"]) && output["merge"]["ok"] === false;
7809
+ const statusOf$2 = (result) => {
7810
+ if (mergeFailed(result)) return "fail";
7811
+ const { ok, output } = result;
7812
+ const status = output["status"];
7813
+ if (typeof status === "string") return status;
7814
+ return ok ? "pass" : "fail";
7815
+ };
7816
+ /** Why a fan-out did not succeed, naming the first item that failed. */
7817
+ const whyNot = (count, failures, skipped) => {
7818
+ const parts = [`${String(failures.length)} of ${String(count)} items failed`];
7819
+ const [first] = failures;
7820
+ if (first !== void 0) parts[0] = `${parts[0] ?? ""}; first, ${first}`;
7821
+ if (skipped > 0) parts.push(`${String(skipped)} not started`);
7822
+ return parts.join("; ");
7823
+ };
7824
+ /**
7825
+ * Waits until the item may start: not while it is held, and without keeping a
7826
+ * session slot through a pause, which would stop other nodes.
7827
+ */
7828
+ const awaitTurn = async (hold, slots, signal) => {
7829
+ for (;;) {
7830
+ await hold.wait("start", signal);
7831
+ if (slots === null) return;
7832
+ await slots.take();
7833
+ if (!hold.held() || signal.aborted) return;
7834
+ slots.release();
7835
+ }
7836
+ };
7837
+ const createFanOut = (deps) => {
7838
+ const { holds, slots, stops, nodeOf, perform, options } = deps;
7839
+ return async (activation, forEach, round) => {
7840
+ const { id, input } = activation;
7841
+ const scope = deps.scope();
7842
+ const recorded = options.replay?.listed?.(id, round, sourceOf(forEach, scope)) ?? null;
7843
+ const listed = recorded === null ? listOf(forEach, scope) : {
7844
+ ok: true,
7845
+ list: unmasked(forEach, recorded)
7846
+ };
7847
+ if (!listed.ok) return failedWith(endFor("invalid_node", { reason: listed.error }));
7848
+ let { list } = listed;
7849
+ const recordList = () => {
7930
7850
  options.onEvent({
7931
- type: "edge.taken",
7932
- from: edge.from,
7933
- to: edge.to
7851
+ type: "each.listed",
7852
+ node: id,
7853
+ round,
7854
+ count: list.length,
7855
+ list,
7856
+ ...recorded === null && options.replay !== void 0 ? { anew: true } : {}
7934
7857
  });
7935
- return true;
7858
+ };
7859
+ recordList();
7860
+ const signal = stops.signalFor(id);
7861
+ if ("maxItems" in forEach && list.length > forEach.maxItems) {
7862
+ options.onEvent({
7863
+ type: "each.exceeded",
7864
+ node: id,
7865
+ round,
7866
+ count: list.length,
7867
+ maxItems: forEach.maxItems
7868
+ });
7869
+ const chosen = await deps.loops.overList(id, list, forEach.maxItems, round, signal);
7870
+ if (chosen === null) {
7871
+ const reason = `the list of ${id} has ${String(list.length)} items, over maxItems ${String(forEach.maxItems)}`;
7872
+ return stops.ended(id, {
7873
+ ...failedWith(endFor("invalid_node", { reason }), {
7874
+ status: "fail",
7875
+ summary: reason
7876
+ }),
7877
+ limited: reason,
7878
+ stopsAs: "failed"
7879
+ });
7880
+ }
7881
+ const narrowed = chosen.length !== list.length;
7882
+ list = chosen;
7883
+ if (narrowed) recordList();
7936
7884
  }
7937
- options.onEvent({
7938
- type: "loop.exhausted",
7939
- from: edge.from,
7940
- to: edge.to,
7941
- maxRounds: edge.maxRounds ?? 0
7942
- });
7943
- const choice = await askAboutLoop(options.human, edge, options.bus);
7944
- if (choice === LOOP_CHOICES.again) {
7945
- extra.set(key, (extra.get(key) ?? 0) + 1);
7946
- return take(edge, output);
7885
+ const node = nodeOf(activation);
7886
+ const session = node !== void 0 && needsSlot(node);
7887
+ const spends = node !== void 0 && spendsBudget(node);
7888
+ const declared = node === void 0 ? void 0 : declaredOutputOf(node);
7889
+ const results = list.map(() => ({}));
7890
+ const statuses = list.map(() => "skipped");
7891
+ const failures = [];
7892
+ /** How the first item that failed ended: the fanned node's own end. */
7893
+ let firstEnd;
7894
+ const mayStart = () => !signal.aborted && !deps.isStopped() && (forEach.onFailure !== "stop" || failures.length === 0);
7895
+ const runItem = async (index) => {
7896
+ const itemId = `${id}.${String(index)}`;
7897
+ const hold = holds.forNode(itemId, id);
7898
+ await awaitTurn(hold, session ? slots : null, signal);
7899
+ try {
7900
+ if (!mayStart() || spends && deps.budgetSpent()) return;
7901
+ const result = await perform({
7902
+ id: itemId,
7903
+ input,
7904
+ template: id,
7905
+ item: {
7906
+ of: id,
7907
+ index,
7908
+ value: list[index]
7909
+ },
7910
+ round
7911
+ });
7912
+ results[index] = entryOf(declared, result);
7913
+ statuses[index] = statusOf$2(result);
7914
+ if (!result.ok) {
7915
+ firstEnd ??= endOf(result);
7916
+ failures.push(`${itemId}: ${result.error ?? "failed"}`);
7917
+ } else if (mergeFailed(result)) failures.push(`${itemId}: its work could not be merged`);
7918
+ } finally {
7919
+ if (session) slots.release();
7920
+ }
7921
+ };
7922
+ let next = 0;
7923
+ const worker = async () => {
7924
+ while (next < list.length && mayStart()) {
7925
+ const index = next;
7926
+ next += 1;
7927
+ await runItem(index);
7928
+ }
7929
+ };
7930
+ const lanes = Math.min(forEach.maxParallel ?? slots.limit, list.length);
7931
+ await Promise.all(Array.from({ length: lanes }, () => worker()));
7932
+ const skipped = statuses.filter((status) => status === "skipped").length;
7933
+ const ok = failures.length === 0 && skipped === 0;
7934
+ const gathered = {
7935
+ ok,
7936
+ output: {
7937
+ items: list,
7938
+ results,
7939
+ statuses,
7940
+ count: list.length
7941
+ },
7942
+ ...ok ? {} : { error: whyNot(list.length, failures, skipped) },
7943
+ ...ok || firstEnd === void 0 ? {} : { end: firstEnd },
7944
+ ...!ok && deps.isStopped() ? { limited: deps.reasons[0] ?? "the run stopped" } : {}
7945
+ };
7946
+ return {
7947
+ ...stops.ended(id, gathered),
7948
+ output: gathered.output
7949
+ };
7950
+ };
7951
+ };
7952
+ // ../core/src/run/locks.ts
7953
+ /**
7954
+ * @file A run's locks (RFC-016): a node that names a lock takes it from the
7955
+ * machine's ledger before it runs, or joins the run's hold when the run has
7956
+ * it already. The hold spans nodes: it goes when a node naming it ends and no
7957
+ * node the graph can still reach from there names it, and on every end of the
7958
+ * run. A fanned node's items each take and release it (unless the run already
7959
+ * holds it). A wait is never silent: `lock.waiting` names the holders, and
7960
+ * the node's, its owners' and the run's clocks stand still meanwhile.
7961
+ */
7962
+ /** A wait for a lock, as `ork ps`, `ork logs`, `ork tree` and the UI say it. */
7963
+ const lockWaitLine = (lock, holders) => holders.length === 0 ? `waiting for ${lock}` : `waiting for ${lock}, held by ${holders.join(", ")}`;
7964
+ /** The node an activation is of: an item's or a worker's declared node. */
7965
+ const declaredOf = (activation) => activation.item?.of ?? activation.template ?? activation.id;
7966
+ const createRunLocks = (deps) => {
7967
+ const { graph, ledgers, onEvent, runId } = deps;
7968
+ /** The run's holds across nodes, by lock name. */
7969
+ const spans = /* @__PURE__ */ new Map();
7970
+ /** Each activation running under a lock; an item's own release with it. */
7971
+ const entered = /* @__PURE__ */ new Map();
7972
+ let ended = false;
7973
+ const nameOf = (id, item) => {
7974
+ const node = graph.nodes.get(id)?.node;
7975
+ const lock = node !== void 0 && "lock" in node ? node.lock : void 0;
7976
+ if (lock === void 0) return void 0;
7977
+ const parsed = parseTemplate(lock);
7978
+ return parsed.ok ? renderTemplate(parsed.parts, deps.scope(item)) : lock;
7979
+ };
7980
+ /** Whether a node the graph can reach from `from` names `lock` itself. */
7981
+ const laterNamed = (from, lock) => {
7982
+ const seen = /* @__PURE__ */ new Set();
7983
+ const next = [from];
7984
+ for (let id = next.pop(); id !== void 0; id = next.pop()) for (const { to } of graph.nodes.get(id)?.outgoing ?? []) {
7985
+ if (seen.has(to)) continue;
7986
+ seen.add(to);
7987
+ if (!(forEachOf(graph.nodes.get(to)?.node) !== void 0) && nameOf(to) === lock) return true;
7988
+ next.push(to);
7947
7989
  }
7948
- if (choice !== LOOP_CHOICES.onward) options.fail(`the loop ${edge.from} -> ${edge.to} ran out of rounds`);
7949
7990
  return false;
7950
7991
  };
7951
- /**
7952
- * What a list over its cap runs: asked of a live owner, then the human. All
7953
- * of it, the first `maxItems`, or `null` to stop. A stop (`signal`) ends the
7954
- * wait and withdraws the question; the list comes back whole, and nothing of
7955
- * it starts because the stop also bars its items.
7956
- */
7957
- const overList = async (node, list, maxItems, round, signal) => {
7958
- const question = eachQuestion(node, list.length, maxItems, round);
7959
- const asking = async () => await options.bus?.askOwner(node, question) ?? (signal.aborted ? null : options.human.ask({
7960
- nodeId: node,
7961
- ...question,
7962
- signal
7992
+ const take = async (id, lock, ledger, signal) => {
7993
+ const slots = await ledger.of(lock);
7994
+ const release = await deps.waitFor(id, () => slots.take({
7995
+ run: runId,
7996
+ node: id,
7997
+ pid: process.pid
7998
+ }, {
7999
+ signal,
8000
+ onWait: (holders) => {
8001
+ onEvent({
8002
+ type: "lock.waiting",
8003
+ id,
8004
+ lock,
8005
+ holders: holders.map((held) => `${held.run}/${held.node}`)
8006
+ });
8007
+ }
7963
8008
  }));
7964
- const stopped = Promise.withResolvers();
7965
- signal.addEventListener("abort", () => {
7966
- stopped.resolve(null);
7967
- }, { once: true });
7968
- const choice = await Promise.race([asking(), stopped.promise]);
7969
- if (signal.aborted) return list;
7970
- if (choice === EACH_CHOICES.first) return list.slice(0, maxItems);
7971
- return choice === EACH_CHOICES.all ? list : null;
8009
+ if (release !== null && ended) {
8010
+ release();
8011
+ return null;
8012
+ }
8013
+ return release;
8014
+ };
8015
+ /** Joins the run's hold of `lock`, taking it first when nobody has. */
8016
+ const join = async (id, lock, ledger, signal) => {
8017
+ for (;;) {
8018
+ let span = spans.get(lock);
8019
+ if (span === void 0) {
8020
+ span = {
8021
+ taking: take(id, lock, ledger, signal),
8022
+ users: /* @__PURE__ */ new Set(),
8023
+ last: id
8024
+ };
8025
+ spans.set(lock, span);
8026
+ }
8027
+ span.users.add(id);
8028
+ span.last = id;
8029
+ let release;
8030
+ try {
8031
+ release = await span.taking;
8032
+ } catch (error) {
8033
+ if (spans.get(lock) === span) spans.delete(lock);
8034
+ throw error;
8035
+ }
8036
+ if (release !== null) {
8037
+ span.release = release;
8038
+ return true;
8039
+ }
8040
+ span.users.delete(id);
8041
+ if (spans.get(lock) === span) spans.delete(lock);
8042
+ if (signal.aborted || ended) return false;
8043
+ }
8044
+ };
8045
+ const released = (id, lock, end) => {
8046
+ onEvent({
8047
+ type: "lock.released",
8048
+ id,
8049
+ lock,
8050
+ end
8051
+ });
7972
8052
  };
7973
8053
  return {
7974
- take,
7975
- overList
8054
+ enter: async (activation, signal) => {
8055
+ const { id, item } = activation;
8056
+ const lock = nameOf(declaredOf(activation), item);
8057
+ if (lock === void 0 || ledgers === void 0) return true;
8058
+ if (item === void 0 || spans.has(lock)) {
8059
+ if (!await join(id, lock, ledgers, signal)) return false;
8060
+ entered.set(id, { lock });
8061
+ } else {
8062
+ const release = await take(id, lock, ledgers, signal);
8063
+ if (release === null) return false;
8064
+ entered.set(id, {
8065
+ lock,
8066
+ release
8067
+ });
8068
+ }
8069
+ onEvent({
8070
+ type: "lock.taken",
8071
+ id,
8072
+ lock
8073
+ });
8074
+ return true;
8075
+ },
8076
+ leave: (activation) => {
8077
+ const { id } = activation;
8078
+ const held = entered.get(id);
8079
+ if (held === void 0) return;
8080
+ entered.delete(id);
8081
+ const { lock, release } = held;
8082
+ if (release !== void 0) {
8083
+ release();
8084
+ released(id, lock, "span");
8085
+ return;
8086
+ }
8087
+ const span = spans.get(lock);
8088
+ if (span === void 0) return;
8089
+ span.users.delete(id);
8090
+ if (span.users.size > 0 || laterNamed(declaredOf(activation), lock)) return;
8091
+ spans.delete(lock);
8092
+ span.release?.();
8093
+ released(id, lock, "span");
8094
+ },
8095
+ end: () => {
8096
+ ended = true;
8097
+ for (const [id, { lock, release }] of entered) {
8098
+ if (release === void 0) continue;
8099
+ release();
8100
+ released(id, lock, "run");
8101
+ }
8102
+ entered.clear();
8103
+ for (const [lock, span] of spans) {
8104
+ if (span.release === void 0) continue;
8105
+ span.release();
8106
+ released(span.last, lock, "run");
8107
+ }
8108
+ spans.clear();
8109
+ }
7976
8110
  };
7977
8111
  };
7978
- // ../core/src/run/ended.ts
7979
- /**
7980
- * @file A node's result as it ended (specs/runtime/session-ends.md): one place
7981
- * that sets `end`, makes a failure's `error` its message and marks a
7982
- * transient kind, and that the engine's modules build ends through.
7983
- */
8112
+ // ../core/src/project/calls.ts
7984
8113
  /**
7985
- * The result as it ended: a failure's `error` is the end's message (what the
7986
- * run summary lists), and a transient kind marks it `transient`.
8114
+ * @file `ork validate`'s check of `workflow.call` nodes against what they call
8115
+ * (RFC-004): the project, the workflow, the node and the required inputs. A
8116
+ * project not registered here is only a warning, since it may be on another
8117
+ * machine; a run refuses it. A name with a template is left to the run.
7987
8118
  */
7988
- const endedWith = (result, end) => ({
7989
- ...result,
7990
- end,
7991
- ...result.ok ? {} : { error: end.message },
7992
- ...END_KINDS[end.kind].transient ? { transient: true } : {}
7993
- });
7994
- /** The reason a run's signal is aborted with when `timeout` passed. */
7995
- const runTimedOut = (timeout) => timeout === void 0 ? { kind: "timed_out" } : {
7996
- kind: "timed_out",
7997
- timeout
7998
- };
7999
- const isTimedOut = (reason) => typeof reason === "object" && reason !== null && "kind" in reason && reason.kind === "timed_out";
8000
- /**
8001
- * The end of a node its run's signal stopped: `timed_out` when the run's
8002
- * timeout aborted it, `interrupted` otherwise.
8003
- */
8004
- const stoppedEnd = (signal, detail) => {
8005
- const reason = signal.reason;
8006
- if (!isTimedOut(reason)) return endFor("interrupted", {}, detail);
8007
- const facts = reason.timeout === void 0 ? {} : { timeout: reason.timeout };
8008
- return endFor("timed_out", facts, detail);
8119
+ const hasTemplate = (text) => text.includes("{{");
8120
+ /** A workflow's file: a folder of that name is no workflow. */
8121
+ const fileState = (file) => {
8122
+ try {
8123
+ return statSync(file, { throwIfNoEntry: false })?.isFile() === true ? "file" : "none";
8124
+ } catch {
8125
+ return "unreadable";
8126
+ }
8009
8127
  };
8010
- /** A failure that ended as `end`, its message the output's summary. */
8011
- const failedWith = (end, output) => endedWith({
8012
- ok: false,
8013
- output: output ?? {
8014
- status: "fail",
8015
- summary: end.message
8128
+ /** A workflow from its file, or `null` when it cannot be read or is not valid. */
8129
+ const readWorkflow = (file) => {
8130
+ try {
8131
+ const parsed = parseWorkflow(readFileSync(file, "utf8"));
8132
+ return parsed.ok ? parsed.workflow : null;
8133
+ } catch {
8134
+ return null;
8016
8135
  }
8017
- }, end);
8136
+ };
8018
8137
  /**
8019
- * The result with its end's detail scrubbed with the feedback redactor (the
8020
- * run's `roots` and the home folder), as a session's is
8021
- * (specs/runtime/session-ends.md), whichever node type set it.
8138
+ * The workflows a workflow calls by a literal name, and those they call. One
8139
+ * that cannot be read is left out: `ork validate` and the call report it. A
8140
+ * call of one `node:` runs only that session, so its tool nodes do not count
8141
+ * unless `everyCall`, and nor does a call among `finished`. `unreadable` hears
8142
+ * of one that is there but could not be read.
8022
8143
  */
8023
- const scrubbedEnd = (result, roots) => {
8024
- const { end } = result;
8025
- if (end?.detail === void 0) return result;
8026
- const detail = redact(end.detail, {
8027
- roots,
8028
- home: homedir()
8029
- });
8030
- return {
8031
- ...result,
8032
- end: {
8033
- ...end,
8034
- detail
8144
+ const calledWorkflows = (workflow, own, registered, options = {}) => {
8145
+ const seen = /* @__PURE__ */ new Set();
8146
+ const found = [];
8147
+ const walk = (from, place) => {
8148
+ for (const [id, node] of Object.entries(from.nodes)) {
8149
+ if (node.type !== "workflow.call") continue;
8150
+ if (hasTemplate(node.workflow) || hasTemplate(node.project ?? "")) continue;
8151
+ if (options.everyCall !== true && node.node !== void 0) continue;
8152
+ if (from === workflow && options.finished?.has(id) === true) continue;
8153
+ const target = node.project === void 0 ? place : registered.find((other) => other.id === node.project);
8154
+ if (target === void 0) continue;
8155
+ const file = path.join(placeFiles(target).workflows, `${path.basename(node.workflow)}.ork.yaml`);
8156
+ if (seen.has(file)) continue;
8157
+ seen.add(file);
8158
+ const state = fileState(file);
8159
+ if (state === "none") continue;
8160
+ const called = state === "file" ? readWorkflow(file) : null;
8161
+ if (called === null) {
8162
+ options.unreadable?.(node.workflow);
8163
+ continue;
8164
+ }
8165
+ found.push(called);
8166
+ walk(called, target);
8035
8167
  }
8036
8168
  };
8169
+ walk(workflow, own);
8170
+ return found;
8037
8171
  };
8038
- // ../core/src/run/declared.ts
8172
+ const checkCallTargets = (workflow, own, registered) => Object.entries(workflow.nodes).flatMap(([id, node]) => {
8173
+ if (node.type !== "workflow.call") return [];
8174
+ const at = ["nodes", id];
8175
+ if (hasTemplate(node.workflow) || hasTemplate(node.project ?? "")) return [];
8176
+ if (/[/\\]/u.test(node.workflow) || node.workflow === "..") return [error("call.workflow", `call \`${id}\`: \`${node.workflow}\` is not a workflow's name`, [...at, "workflow"])];
8177
+ const target = node.project === void 0 ? own : registered.find((place) => place.id === node.project);
8178
+ if (target === void 0) return [warning("call.project", `call \`${id}\`: project \`${String(node.project)}\` is not registered here, so a run will refuse it; register it with \`ork register\``, [...at, "project"])];
8179
+ const file = path.join(placeFiles(target).workflows, `${node.workflow}.ork.yaml`);
8180
+ const state = fileState(file);
8181
+ if (state === "none") return [error("call.workflow", `call \`${id}\`: no workflow \`${node.workflow}\` in ${target.root}`, [...at, "workflow"])];
8182
+ const called = state === "file" ? readWorkflow(file) : null;
8183
+ if (called === null) return [error("call.workflow", `call \`${id}\`: the workflow \`${node.workflow}\` cannot be read or is not valid; run \`ork validate\` on it`, [...at, "workflow"])];
8184
+ const found = [];
8185
+ if (node.node !== void 0) {
8186
+ const type = called.nodes[node.node]?.type;
8187
+ if (type !== "agent.leader" && type !== "agent.worker") found.push(error("call.node", `call \`${id}\`: \`${node.node}\` is not a leader or worker of \`${node.workflow}\``, [...at, "node"]));
8188
+ }
8189
+ for (const [name, input] of Object.entries(called.inputs ?? {})) if (input.required === true && input.default === void 0 && node.inputs?.[name] === void 0) found.push(error("call.input", `call \`${id}\`: \`${node.workflow}\` requires the input \`${name}\``, [...at, "inputs"]));
8190
+ return found;
8191
+ });
8192
+ // ../core/src/run/routing.ts
8039
8193
  /**
8040
- * @file Holding a node to what it declares of its output (RFC-010): a missing
8041
- * field, the wrong type or a value outside the list is a problem to fail the
8042
- * node on, so a condition never quietly matches nothing. A declared list
8043
- * (RFC-013) is held to being a list, and each item to its shape. It reads the
8044
- * format's own shape; a field written out as JSON Schema stays the driver's
8045
- * to enforce. A list a resume hands back keeps the file's real values when it
8046
- * only differs by masking.
8194
+ * @file Where a finished node's output goes: which outgoing edges are taken,
8195
+ * and what each arrival does at its target (activate it, skip it, or wait,
8196
+ * for a join). Pure: the scheduler owns time, sessions and loop counters.
8047
8197
  */
8048
- const TYPE_NAMES = {
8049
- string: "text",
8050
- number: "a number",
8051
- integer: "a whole number",
8052
- boolean: "true or false"
8053
- };
8054
- const FITS = {
8055
- string: (value) => typeof value === "string",
8056
- number: (value) => typeof value === "number",
8057
- integer: (value) => Number.isInteger(value),
8058
- boolean: (value) => typeof value === "boolean"
8059
- };
8060
- const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
8061
- const listed = (values) => {
8062
- const names = values.map((value) => markValue(String(value)));
8063
- const last = names.pop();
8064
- return names.length === 0 ? String(last) : `${names.join(", ")} or ${String(last)}`;
8065
- };
8066
- const shown = (value) => {
8067
- const text = typeof value === "string" ? value : JSON.stringify(value) ?? String(value);
8068
- return markValue(text.length > 80 ? `${text.slice(0, 80)}…` : text);
8069
- };
8070
- /** What's wrong with one field, if it's a shape this can read. */
8071
- const fieldProblem = (name, rule, value) => {
8072
- const shape = listShape(rule);
8073
- if (shape !== void 0) return listProblem(name, shape, value);
8074
- const expected = Array.isArray(rule) ? listed(rule) : typeof rule === "string" ? TYPE_NAMES[rule] : void 0;
8075
- if (expected === void 0) return null;
8076
- if (value === void 0) return `${name} is missing, expected ${expected}`;
8077
- return (Array.isArray(rule) ? rule.some((item) => item === value) : FITS[String(rule)]?.(value)) === true ? null : `${name} is ${shown(value)}, expected ${expected}`;
8198
+ /** A `maxRounds` edge. Removing them leaves a DAG (the compiler checks it). */
8199
+ const isLoopEdge = (edge) => edge.maxRounds !== void 0;
8200
+ const keys = /* @__PURE__ */ new WeakMap();
8201
+ const edgeKey = (edge) => {
8202
+ let key = keys.get(edge);
8203
+ if (key === void 0) {
8204
+ key = `${String(edge.source)}:${edge.from}->${edge.to}`;
8205
+ keys.set(edge, key);
8206
+ }
8207
+ return key;
8078
8208
  };
8079
- const fieldProblems = (declared, value, prefix) => Object.entries(declared).flatMap(([name, rule]) => {
8080
- const problem = fieldProblem(`${prefix}${name}`, rule, value[name]);
8081
- return problem === null ? [] : [problem];
8082
- });
8083
- /** What's wrong with one item; `name` says which, as `tasks[2]`. */
8084
- const itemProblems = (name, shape, item) => {
8085
- if (Object.keys(shape).length === 0) return [];
8086
- return isRecord$2(item) ? fieldProblems(shape, item, `${name}.`) : [`${name} is ${shown(item)}, expected an object`];
8209
+ /**
8210
+ * Decides every outgoing edge of a finished node. An edge with `when:` is taken
8211
+ * when its condition holds for the node's output; one without is taken when the
8212
+ * node succeeded. A `control.router` in `first` mode takes only its first
8213
+ * matching edge.
8214
+ */
8215
+ const decideEdges = (graph, id, result, scope) => {
8216
+ const compiled = graph.nodes.get(id);
8217
+ if (compiled === void 0) return [];
8218
+ const { node } = compiled;
8219
+ const firstOnly = node.type === "control.router" && (node.mode ?? "first") === "first";
8220
+ let matched = false;
8221
+ return compiled.outgoing.map((edge) => {
8222
+ const taken = (edge.condition === void 0 ? result.ok : truthy(evaluate(edge.condition, {
8223
+ ...scope,
8224
+ output: {
8225
+ ok: result.ok,
8226
+ ...result.output
8227
+ }
8228
+ }))) && !(firstOnly && matched);
8229
+ if (taken) matched = true;
8230
+ return {
8231
+ edge,
8232
+ taken
8233
+ };
8234
+ });
8087
8235
  };
8088
- /** Only the first bad item is named, so a long list can't flood the reason. */
8089
- const listProblem = (name, shape, value) => {
8090
- if (value === void 0) return `${name} is missing, expected a list`;
8091
- if (!Array.isArray(value)) return `${name} is ${shown(value)}, expected a list`;
8092
- const bad = value.map((item, index) => itemProblems(`${name}[${index}]`, shape, item)).find((problems) => problems.length > 0);
8093
- return bad === void 0 ? null : bad.join("; ");
8236
+ const joinOutput = (edges, resolved) => {
8237
+ const ran = edges.filter((e) => resolved.get(edgeKey(e))?.taken === true);
8238
+ const results = {};
8239
+ for (const edge of ran) results[edge.from] = resolved.get(edgeKey(edge))?.output ?? {};
8240
+ return {
8241
+ statuses: ran.map((e) => results[e.from]?.["status"]),
8242
+ results,
8243
+ count: ran.length
8244
+ };
8094
8245
  };
8246
+ /** How a reason for a run that was left waiting starts: a bug in Ork. */
8247
+ const STUCK_REASON = "ork engine error:";
8095
8248
  /**
8096
- * What's wrong with `value` by this declaration, or `null`. With nothing
8097
- * declared, nothing can be.
8249
+ * Why a run that was left waiting fails: a node neither ran nor was skipped
8250
+ * while an edge could still reach it. The scheduler resolves every edge whose
8251
+ * source can't run any more, so only a bug in Ork leaves one.
8098
8252
  */
8099
- const declaredProblem = (declared, value) => {
8100
- if (declared === void 0 || Object.keys(declared).length === 0) return null;
8101
- if (!isRecord$2(value)) return `the result isn't a JSON object`;
8102
- const problems = fieldProblems(declared, value, "");
8103
- return problems.length === 0 ? null : problems.join("; ");
8253
+ const stuckReason = (waiting) => `${STUCK_REASON} ${waiting.map(({ id, waitingOn }) => `${id} was still waiting on ${waitingOn.join(", ")}`).join("; ")} with nothing left to run; this is a bug in Ork, not in the workflow`;
8254
+ /** Each node's place along the non-loop edges, sources first. */
8255
+ const orderOf = (graph) => {
8256
+ const order = /* @__PURE__ */ new Map();
8257
+ const left = /* @__PURE__ */ new Map();
8258
+ for (const [id, compiled] of graph.nodes) left.set(id, compiled.incoming.filter((e) => !isLoopEdge(e)).length);
8259
+ const ready = [...left].filter(([, count]) => count === 0).map(([id]) => id);
8260
+ for (let id = ready.shift(); id !== void 0; id = ready.shift()) {
8261
+ order.set(id, order.size);
8262
+ for (const edge of graph.nodes.get(id)?.outgoing ?? []) {
8263
+ if (isLoopEdge(edge)) continue;
8264
+ const count = (left.get(edge.to) ?? 0) - 1;
8265
+ left.set(edge.to, count);
8266
+ if (count === 0) ready.push(edge.to);
8267
+ }
8268
+ }
8269
+ return order;
8104
8270
  };
8105
8271
  /**
8106
- * The list a resume hands back. Events are stored masked, so a static list that
8107
- * still masks to the record is the file's, with its real values.
8272
+ * Tracks, per target, which incoming non-loop edges have resolved in its
8273
+ * current round: since every one last resolved.
8274
+ *
8275
+ * - A node runs each time an incoming edge is taken, and is skipped once every
8276
+ * incoming non-loop edge has resolved as not taken (dead-path elimination),
8277
+ * so nothing downstream waits on a branch that will never run.
8278
+ * - A `control.join` waits: `all` until every input resolved, `any` for the first
8279
+ * that ran, `n` for `n` that ran. A join none of whose required inputs ran is
8280
+ * skipped.
8281
+ * - An edge whose source can't run any more counts as not taken (`stranded`): in
8282
+ * a later round, a branch the loop doesn't go back to never runs again.
8108
8283
  */
8109
- const unmasked = (forEach, recorded) => "items" in forEach && JSON.stringify(forEach.items, maskReplacer) === JSON.stringify(recorded) ? [...forEach.items] : recorded;
8110
- // ../core/src/run/slots.ts
8111
- /**
8112
- * @file Session slots: at most `limits.concurrency` Claude sessions run at
8113
- * once. Queued activations take a free slot when the scheduler pumps;
8114
- * delegated children and leaders returning from `delegate` wait for one and
8115
- * are served first, so a leader waiting on its children never holds a slot
8116
- * they need.
8117
- */
8118
- /** Only a Claude session takes a slot; tools and gates are not limited. */
8119
- const needsSlot = (node) => isClaudeNode(node);
8120
- /** A call spends through its run, so it needs budget as a session does. */
8121
- const spendsBudget = (node) => needsSlot(node) || node.type === "workflow.call";
8122
- const createSlots = (limit, onFree) => {
8123
- let used = 0;
8124
- const waiting = [];
8125
- return {
8126
- limit,
8127
- tryTake: () => {
8128
- if (used >= limit || waiting.length > 0) return false;
8129
- used += 1;
8130
- return true;
8131
- },
8132
- take: () => {
8133
- if (used < limit && waiting.length === 0) {
8134
- used += 1;
8135
- return Promise.resolve();
8136
- }
8137
- const { promise, resolve } = Promise.withResolvers();
8138
- waiting.push(resolve);
8139
- return promise;
8140
- },
8141
- release: () => {
8142
- const next = waiting.shift();
8143
- if (next === void 0) {
8144
- used -= 1;
8145
- onFree();
8146
- } else next();
8284
+ const createInbox = (graph) => {
8285
+ const resolved = /* @__PURE__ */ new Map();
8286
+ /** Targets that ran, or joins that fired, in their current round. */
8287
+ const fired = /* @__PURE__ */ new Set();
8288
+ let order;
8289
+ const forwards = /* @__PURE__ */ new Map();
8290
+ const forwardIn = (id) => {
8291
+ let edges = forwards.get(id);
8292
+ if (edges === void 0) {
8293
+ edges = (graph.nodes.get(id)?.incoming ?? []).filter((e) => !isLoopEdge(e));
8294
+ forwards.set(id, edges);
8147
8295
  }
8296
+ return edges;
8148
8297
  };
8149
- };
8150
- // ../core/src/run/fan-out.ts
8151
- /** The node's `forEach`, whatever its type; `undefined` when it has none. */
8152
- const forEachOf = (node) => node !== void 0 && "forEach" in node ? node.forEach : void 0;
8153
- /** The list a node runs over, or why there isn't one. */
8154
- const listOf = (forEach, scope) => {
8155
- if ("items" in forEach) return {
8156
- ok: true,
8157
- list: forEach.items
8298
+ const reset = (id) => {
8299
+ resolved.delete(id);
8300
+ fired.delete(id);
8158
8301
  };
8159
- const read = templateValue(forEach.from, scope);
8160
- if (!read.ok) return read;
8161
- const { value } = read;
8162
- if (Array.isArray(value)) return {
8163
- ok: true,
8164
- list: value
8302
+ const arriveAtJoin = (id, seen) => {
8303
+ const node = graph.nodes.get(id)?.node;
8304
+ if (node?.type !== "control.join") return { kind: "wait" };
8305
+ const inputs = forwardIn(id);
8306
+ const ran = inputs.filter((e) => seen.get(edgeKey(e))?.taken === true);
8307
+ const complete = inputs.every((e) => seen.has(edgeKey(e)));
8308
+ const mode = node.mode ?? "all";
8309
+ const needed = mode === "any" ? 1 : node.n ?? inputs.length;
8310
+ if (mode === "all") {
8311
+ if (!complete) return { kind: "wait" };
8312
+ reset(id);
8313
+ return ran.length > 0 ? {
8314
+ kind: "activate",
8315
+ input: joinOutput(inputs, seen)
8316
+ } : { kind: "skip" };
8317
+ }
8318
+ if (!fired.has(id) && ran.length >= needed) {
8319
+ const input = joinOutput(inputs, seen);
8320
+ if (complete) reset(id);
8321
+ else fired.add(id);
8322
+ return {
8323
+ kind: "activate",
8324
+ input
8325
+ };
8326
+ }
8327
+ if (!complete) return { kind: "wait" };
8328
+ const wasFired = fired.has(id);
8329
+ reset(id);
8330
+ return wasFired ? { kind: "wait" } : { kind: "skip" };
8165
8331
  };
8166
- const name = String(templatePath(forEach.from)?.at(-1) ?? forEach.from);
8332
+ /** Records that `edge` resolved, and says what happens at its target. */
8333
+ const arrive = (edge, taken, output) => {
8334
+ const id = edge.to;
8335
+ const seen = resolved.get(id) ?? /* @__PURE__ */ new Map();
8336
+ resolved.set(id, seen);
8337
+ seen.set(edgeKey(edge), {
8338
+ taken,
8339
+ output
8340
+ });
8341
+ if (graph.nodes.get(id)?.node.type === "control.join") return arriveAtJoin(id, seen);
8342
+ const complete = forwardIn(id).every((e) => seen.has(edgeKey(e)));
8343
+ const ran = taken || fired.has(id);
8344
+ if (complete) reset(id);
8345
+ else if (taken) fired.add(id);
8346
+ if (taken) return {
8347
+ kind: "activate",
8348
+ input: output
8349
+ };
8350
+ return complete && !ran ? { kind: "skip" } : { kind: "wait" };
8351
+ };
8352
+ /** The edges a target still waits on. */
8353
+ const missingOf = (id, seen) => forwardIn(id).filter((e) => !seen.has(edgeKey(e)));
8354
+ /**
8355
+ * The first target, in graph order, whose every edge still to resolve comes
8356
+ * from a node that can't run any more: neither `active` (queued or running)
8357
+ * nor after one along the non-loop edges. A loop's edge starts a new round,
8358
+ * so it doesn't count. Its edges count as not taken; the caller delivers
8359
+ * them, and asks again, as that can start something.
8360
+ */
8361
+ const stranded = (active) => {
8362
+ const candidates = [...resolved].map(([id, seen]) => ({
8363
+ id,
8364
+ missing: missingOf(id, seen)
8365
+ })).filter(({ missing }) => !missing.some((e) => active(e.from)));
8366
+ if (candidates.length === 0) return [];
8367
+ /** Nodes known to be unable to run, in this call. */
8368
+ const dead = /* @__PURE__ */ new Set();
8369
+ const canRun = (from) => {
8370
+ const visited = /* @__PURE__ */ new Set([from]);
8371
+ const stack = [from];
8372
+ for (let id = stack.pop(); id !== void 0; id = stack.pop()) {
8373
+ if (active(id)) return true;
8374
+ for (const edge of forwardIn(id)) {
8375
+ if (visited.has(edge.from) || dead.has(edge.from)) continue;
8376
+ visited.add(edge.from);
8377
+ stack.push(edge.from);
8378
+ }
8379
+ }
8380
+ for (const id of visited) dead.add(id);
8381
+ return false;
8382
+ };
8383
+ order ??= orderOf(graph);
8384
+ const known = order;
8385
+ const place = (id) => known.get(id) ?? Infinity;
8386
+ return candidates.filter(({ missing }) => !missing.some((e) => canRun(e.from))).toSorted((a, b) => place(a.id) - place(b.id))[0]?.missing ?? [];
8387
+ };
8388
+ /** Targets an edge reached that still wait on others. */
8389
+ const waiting = () => [...resolved].map(([id, seen]) => ({
8390
+ id,
8391
+ waitingOn: [...new Set(missingOf(id, seen).map((e) => e.from))]
8392
+ }));
8167
8393
  return {
8168
- ok: false,
8169
- error: declaredProblem({ [name]: { list: {} } }, { [name]: value }) ?? `${name} is not a list`
8394
+ arrive,
8395
+ stranded,
8396
+ waiting
8170
8397
  };
8171
8398
  };
8172
- /** The output a `from` list comes from, whose freshness resume asks about. */
8173
- const sourceOf = (forEach, scope) => {
8174
- if (!("from" in forEach)) return void 0;
8175
- const [root, id] = templatePath(forEach.from) ?? [];
8176
- return root === "nodes" ? scope.nodes[String(id)]?.output : void 0;
8177
- };
8178
- const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
8179
- /**
8180
- * What `results` keeps of an item: its declared fields (a program's are under
8181
- * `json`), its summary, Ork's own branch and worktree, and how a program
8182
- * failed. The full output stays in the item's own `node.finished`.
8183
- */
8184
- const entryOf = (declared, { ok, output, error }) => {
8185
- const json = isRecord$1(output["json"]) ? output["json"] : {};
8186
- const fields = Object.keys(declared ?? {}).map((field) => [field, Object.hasOwn(json, field) ? json[field] : output[field]]);
8187
- const own = [
8188
- "summary",
8189
- "branch",
8190
- "worktree",
8191
- "merge"
8192
- ].map((field) => [field, output[field]]);
8193
- const failed = !ok && typeof output["exitCode"] === "number" ? [["exitCode", output["exitCode"]], ["error", error]] : [];
8194
- return Object.fromEntries([
8195
- ...fields,
8196
- ...own,
8197
- ...failed
8198
- ].filter(([, value]) => value !== void 0));
8199
- };
8200
- /** Whether an item passed but its work could not reach its owner. */
8201
- const mergeFailed = ({ output }) => isRecord$1(output["merge"]) && output["merge"]["ok"] === false;
8202
- const statusOf$2 = (result) => {
8203
- if (mergeFailed(result)) return "fail";
8204
- const { ok, output } = result;
8205
- const status = output["status"];
8206
- if (typeof status === "string") return status;
8207
- return ok ? "pass" : "fail";
8399
+ // ../core/src/run/escalation.ts
8400
+ const LOOP_CHOICES = {
8401
+ stop: "stop",
8402
+ again: "again",
8403
+ onward: "onward"
8208
8404
  };
8209
- /** Why a fan-out did not succeed, naming the first item that failed. */
8210
- const whyNot = (count, failures, skipped) => {
8211
- const parts = [`${String(failures.length)} of ${String(count)} items failed`];
8212
- const [first] = failures;
8213
- if (first !== void 0) parts[0] = `${parts[0] ?? ""}; first, ${first}`;
8214
- if (skipped > 0) parts.push(`${String(skipped)} not started`);
8215
- return parts.join("; ");
8405
+ /** What a list over its `maxItems` can be answered with (RFC-013). */
8406
+ const EACH_CHOICES = {
8407
+ stop: "stop",
8408
+ first: "first",
8409
+ all: "all"
8216
8410
  };
8411
+ /** The question about a list over its cap; the recommended choice stops. */
8412
+ const eachQuestion = (node, count, maxItems, round = 1) => ({
8413
+ question: `The list of ${node}${round > 1 ? ` (round ${String(round)})` : ""} has ${String(count)} items, more than its maxItems of ${String(maxItems)}. What now?`,
8414
+ options: [
8415
+ {
8416
+ id: EACH_CHOICES.stop,
8417
+ description: "Stop the run"
8418
+ },
8419
+ {
8420
+ id: EACH_CHOICES.first,
8421
+ description: `Run the first ${String(maxItems)}`
8422
+ },
8423
+ {
8424
+ id: EACH_CHOICES.all,
8425
+ description: `Run all ${String(count)}`
8426
+ }
8427
+ ],
8428
+ recommended: EACH_CHOICES.stop,
8429
+ context: `${node} runs at most ${String(maxItems)} items unless someone says otherwise.`
8430
+ });
8431
+ /** The question about a loop out of rounds; the recommended choice stops. */
8432
+ const loopQuestion = (edge) => ({
8433
+ question: `The loop ${edge.from} -> ${edge.to} used all ${String(edge.maxRounds ?? 0)} of its rounds. What now?`,
8434
+ options: [
8435
+ {
8436
+ id: LOOP_CHOICES.stop,
8437
+ description: "Stop the run"
8438
+ },
8439
+ {
8440
+ id: LOOP_CHOICES.again,
8441
+ description: "Allow one more round"
8442
+ },
8443
+ {
8444
+ id: LOOP_CHOICES.onward,
8445
+ description: "Go on along the other edges"
8446
+ }
8447
+ ],
8448
+ recommended: LOOP_CHOICES.stop,
8449
+ context: `${edge.from} still wants another round${edge.when === void 0 ? "" : ` (${edge.when.trim()})`}.`
8450
+ });
8217
8451
  /**
8218
- * Waits until the item may start: not while it is held, and without keeping a
8219
- * session slot through a pause, which would stop other nodes.
8452
+ * Asks the owner of `edge.from` while its session is live (the node runs in the
8453
+ * flow, so its owner is not waiting on it), and the human otherwise or when the
8454
+ * owner's session ends first.
8220
8455
  */
8221
- const awaitTurn = async (hold, slots, signal) => {
8222
- for (;;) {
8223
- await hold.wait("start", signal);
8224
- if (slots === null) return;
8225
- await slots.take();
8226
- if (!hold.held() || signal.aborted) return;
8227
- slots.release();
8228
- }
8456
+ const askAboutLoop = async (human, edge, bus) => {
8457
+ const question = loopQuestion(edge);
8458
+ return await bus?.askOwner(edge.from, question) ?? await bus?.askOwner(edge.to, question) ?? human.ask({
8459
+ nodeId: edge.from,
8460
+ ...question
8461
+ });
8229
8462
  };
8230
- const createFanOut = (deps) => {
8231
- const { holds, slots, stops, nodeOf, perform, options } = deps;
8232
- return async (activation, forEach, round) => {
8233
- const { id, input } = activation;
8234
- const scope = deps.scope();
8235
- const recorded = options.replay?.listed?.(id, round, sourceOf(forEach, scope)) ?? null;
8236
- const listed = recorded === null ? listOf(forEach, scope) : {
8237
- ok: true,
8238
- list: unmasked(forEach, recorded)
8239
- };
8240
- if (!listed.ok) return failedWith(endFor("invalid_node", { reason: listed.error }));
8241
- let { list } = listed;
8242
- const recordList = () => {
8243
- options.onEvent({
8244
- type: "each.listed",
8245
- node: id,
8246
- round,
8247
- count: list.length,
8248
- list,
8249
- ...recorded === null && options.replay !== void 0 ? { anew: true } : {}
8250
- });
8251
- };
8252
- recordList();
8253
- const signal = stops.signalFor(id);
8254
- if ("maxItems" in forEach && list.length > forEach.maxItems) {
8255
- options.onEvent({
8256
- type: "each.exceeded",
8257
- node: id,
8258
- round,
8259
- count: list.length,
8260
- maxItems: forEach.maxItems
8261
- });
8262
- const chosen = await deps.loops.overList(id, list, forEach.maxItems, round, signal);
8263
- if (chosen === null) {
8264
- const reason = `the list of ${id} has ${String(list.length)} items, over maxItems ${String(forEach.maxItems)}`;
8265
- return stops.ended(id, {
8266
- ...failedWith(endFor("invalid_node", { reason }), {
8267
- status: "fail",
8268
- summary: reason
8269
- }),
8270
- limited: reason,
8271
- stopsAs: "failed"
8272
- });
8273
- }
8274
- const narrowed = chosen.length !== list.length;
8275
- list = chosen;
8276
- if (narrowed) recordList();
8277
- }
8278
- const node = nodeOf(activation);
8279
- const session = node !== void 0 && needsSlot(node);
8280
- const spends = node !== void 0 && spendsBudget(node);
8281
- const declared = node === void 0 ? void 0 : declaredOutputOf(node);
8282
- const results = list.map(() => ({}));
8283
- const statuses = list.map(() => "skipped");
8284
- const failures = [];
8285
- /** How the first item that failed ended: the fanned node's own end. */
8286
- let firstEnd;
8287
- const mayStart = () => !signal.aborted && !deps.isStopped() && (forEach.onFailure !== "stop" || failures.length === 0);
8288
- const runItem = async (index) => {
8289
- const itemId = `${id}.${String(index)}`;
8290
- const hold = holds.forNode(itemId, id);
8291
- await awaitTurn(hold, session ? slots : null, signal);
8292
- try {
8293
- if (!mayStart() || spends && deps.budgetSpent()) return;
8294
- const result = await perform({
8295
- id: itemId,
8296
- input,
8297
- template: id,
8298
- item: {
8299
- of: id,
8300
- index,
8301
- value: list[index]
8302
- },
8303
- round
8304
- });
8305
- results[index] = entryOf(declared, result);
8306
- statuses[index] = statusOf$2(result);
8307
- if (!result.ok) {
8308
- firstEnd ??= endOf(result);
8309
- failures.push(`${itemId}: ${result.error ?? "failed"}`);
8310
- } else if (mergeFailed(result)) failures.push(`${itemId}: its work could not be merged`);
8311
- } finally {
8312
- if (session) slots.release();
8313
- }
8314
- };
8315
- let next = 0;
8316
- const worker = async () => {
8317
- while (next < list.length && mayStart()) {
8318
- const index = next;
8319
- next += 1;
8320
- await runItem(index);
8321
- }
8322
- };
8323
- const lanes = Math.min(forEach.maxParallel ?? slots.limit, list.length);
8324
- await Promise.all(Array.from({ length: lanes }, () => worker()));
8325
- const skipped = statuses.filter((status) => status === "skipped").length;
8326
- const ok = failures.length === 0 && skipped === 0;
8327
- const gathered = {
8328
- ok,
8329
- output: {
8330
- items: list,
8331
- results,
8332
- statuses,
8333
- count: list.length
8334
- },
8335
- ...ok ? {} : { error: whyNot(list.length, failures, skipped) },
8336
- ...ok || firstEnd === void 0 ? {} : { end: firstEnd },
8337
- ...!ok && deps.isStopped() ? { limited: deps.reasons[0] ?? "the run stopped" } : {}
8338
- };
8339
- return {
8340
- ...stops.ended(id, gathered),
8341
- output: gathered.output
8342
- };
8463
+ /** Counts each loop edge's rounds; `take` is false once one is out of them. */
8464
+ const createLoops = (options) => {
8465
+ const taken = /* @__PURE__ */ new Map();
8466
+ const extra = /* @__PURE__ */ new Map();
8467
+ const take = async (edge, output) => {
8468
+ const key = edgeKey(edge);
8469
+ const count = taken.get(key) ?? 0;
8470
+ if (count < (edge.maxRounds ?? 0) + (extra.get(key) ?? 0)) {
8471
+ taken.set(key, count + 1);
8472
+ options.activate(edge.to, output);
8473
+ options.onEvent({
8474
+ type: "edge.taken",
8475
+ from: edge.from,
8476
+ to: edge.to
8477
+ });
8478
+ return true;
8479
+ }
8480
+ options.onEvent({
8481
+ type: "loop.exhausted",
8482
+ from: edge.from,
8483
+ to: edge.to,
8484
+ maxRounds: edge.maxRounds ?? 0
8485
+ });
8486
+ const choice = await askAboutLoop(options.human, edge, options.bus);
8487
+ if (choice === LOOP_CHOICES.again) {
8488
+ extra.set(key, (extra.get(key) ?? 0) + 1);
8489
+ return take(edge, output);
8490
+ }
8491
+ if (choice !== LOOP_CHOICES.onward) options.fail(`the loop ${edge.from} -> ${edge.to} ran out of rounds`);
8492
+ return false;
8493
+ };
8494
+ /**
8495
+ * What a list over its cap runs: asked of a live owner, then the human. All
8496
+ * of it, the first `maxItems`, or `null` to stop. A stop (`signal`) ends the
8497
+ * wait and withdraws the question; the list comes back whole, and nothing of
8498
+ * it starts because the stop also bars its items.
8499
+ */
8500
+ const overList = async (node, list, maxItems, round, signal) => {
8501
+ const question = eachQuestion(node, list.length, maxItems, round);
8502
+ const asking = async () => await options.bus?.askOwner(node, question) ?? (signal.aborted ? null : options.human.ask({
8503
+ nodeId: node,
8504
+ ...question,
8505
+ signal
8506
+ }));
8507
+ const stopped = Promise.withResolvers();
8508
+ signal.addEventListener("abort", () => {
8509
+ stopped.resolve(null);
8510
+ }, { once: true });
8511
+ const choice = await Promise.race([asking(), stopped.promise]);
8512
+ if (signal.aborted) return list;
8513
+ if (choice === EACH_CHOICES.first) return list.slice(0, maxItems);
8514
+ return choice === EACH_CHOICES.all ? list : null;
8515
+ };
8516
+ return {
8517
+ take,
8518
+ overList
8343
8519
  };
8344
8520
  };
8345
8521
  // ../core/src/run/delegation.ts
@@ -8543,6 +8719,10 @@ const nodeContext = (deps, activation, cwd, round, clock) => {
8543
8719
  reserveBudget: (usd) => {
8544
8720
  deps.spend.reserve(id, usd, deps.nodeOf(activation)?.type !== "agent.leader");
8545
8721
  },
8722
+ tokensLeftNow: deps.spend.tokensLeft,
8723
+ reserveTokens: (tokens) => {
8724
+ deps.spend.reserve(id, tokens, deps.nodeOf(activation)?.type !== "agent.leader", "tokens");
8725
+ },
8546
8726
  budgetFreed: (signal) => deps.spend.freed(id, signal),
8547
8727
  quota: deps.spend.quota,
8548
8728
  ownSpend: (sessionId, total) => deps.spend.ownOf(id, sessionId, total),
@@ -8552,9 +8732,10 @@ const nodeContext = (deps, activation, cwd, round, clock) => {
8552
8732
  ...options.calls === void 0 ? {} : { calls: options.calls },
8553
8733
  emit: (event) => {
8554
8734
  deps.ports.owners.seen(id, event, cwd);
8735
+ if (event.type === "session.started") deps.spend.started(event.driver);
8555
8736
  if (event.type === "session.finished" && event.fork !== true) {
8556
8737
  const { sessionId, usage } = event.outcome;
8557
- deps.spend.report(id, sessionId, usage.costUsd);
8738
+ deps.spend.reported(id, sessionId, usage);
8558
8739
  }
8559
8740
  options.onEvent({
8560
8741
  type: "node.event",
@@ -8663,6 +8844,22 @@ const scriptContactsOf = (id, nodes, channels) => {
8663
8844
  /** Whether a program in a tool node may write to the session `id`. */
8664
8845
  const hearsFromScripts = (id, nodes, channels) => nodes.some((node) => isTool(node) && scriptContactsOf(node.id, nodes, channels).includes(id));
8665
8846
  // ../core/src/usage/spend.ts
8847
+ /**
8848
+ * @file What a session spent, from what it reported (ADR-046). Claude Code
8849
+ * reports a resumed session's cost as its total so far, carried over from its
8850
+ * transcript when that has one, and a crash's result may carry zero. The
8851
+ * ledger and the scheduler both read a session's reports through `ownSpend`;
8852
+ * `earlierSpend` is what a resumed run starts from.
8853
+ */
8854
+ /**
8855
+ * The tokens a token budget or quota counts (RFC-015): input, output and cache
8856
+ * writes. Cache reads never count. The ledger's `tokensSince` sums the same.
8857
+ */
8858
+ const countedTokens = (usage) => usage.inputTokens + usage.outputTokens + usage.cacheWriteTokens;
8859
+ /** The drivers that report no dollars: a dollar total can't see their sessions. */
8860
+ const UNPRICED_DRIVERS = AGENT_DRIVERS.filter((driver) => !driver.capabilities.price).map((driver) => driver.id);
8861
+ /** Whether a session on `driver` spends dollars no total can see. */
8862
+ const unpriced = (driver) => driver !== void 0 && UNPRICED_DRIVERS.includes(driver);
8666
8863
  /** What a report would add to the session's earlier ones, counting nothing. */
8667
8864
  const unreported = (reported, key, total) => {
8668
8865
  if (total <= 0) return 0;
@@ -8692,18 +8889,23 @@ const earlierSpend = (events) => {
8692
8889
  const open = /* @__PURE__ */ new Map();
8693
8890
  const forks = /* @__PURE__ */ new Map();
8694
8891
  let total = 0;
8892
+ let tokens = 0;
8893
+ let partial = false;
8695
8894
  for (const event of events) if (event.type === "node.started") open.set(event.id, null);
8696
8895
  else if (event.type === "node.finished") {
8697
8896
  total += event.usage?.costUsd ?? 0;
8897
+ tokens += event.usage === void 0 ? 0 : countedTokens(event.usage);
8698
8898
  const last = open.get(event.id);
8699
8899
  if (last !== null && last !== void 0) ownSpend(reported, last.key, last.cost);
8700
8900
  open.delete(event.id);
8701
- } else if (event.type === "node.event" && event.event.type === "session.finished") {
8901
+ } else if (event.type === "node.event" && event.event.type === "session.started") partial ||= unpriced(event.event.driver);
8902
+ else if (event.type === "node.event" && event.event.type === "session.finished") {
8702
8903
  const { sessionId, usage } = event.event.outcome;
8703
8904
  const key = `${event.id}/${sessionId}`;
8704
8905
  if (event.event.fork === true) {
8705
8906
  const own = ownSpend(reported, key, usage.costUsd);
8706
8907
  total += own;
8908
+ tokens += countedTokens(usage);
8707
8909
  forks.set(event.id, (forks.get(event.id) ?? 0) + own);
8708
8910
  } else if (open.has(event.id)) open.set(event.id, {
8709
8911
  key,
@@ -8713,6 +8915,8 @@ const earlierSpend = (events) => {
8713
8915
  }
8714
8916
  return {
8715
8917
  total,
8918
+ tokens,
8919
+ partial,
8716
8920
  reported,
8717
8921
  forks
8718
8922
  };
@@ -8727,20 +8931,27 @@ const createReserves = () => {
8727
8931
  for (const resolve of woken) resolve();
8728
8932
  };
8729
8933
  return {
8730
- /** All that is set aside now. */
8731
- total: () => [...held.values()].reduce((sum, reserve) => sum + reserve.usd, 0),
8934
+ /** All that is set aside now, in `unit`. */
8935
+ total: (unit = "usd") => [...held.values()].reduce((sum, reserve) => sum + reserve[unit], 0),
8732
8936
  /**
8733
- * `id` sets aside `usd` (nothing, or 0, lets it go). `waitable` is false
8734
- * for a leader, whose children must never wait on it.
8937
+ * `id` sets aside `amount` of `unit` (nothing, or 0, lets it go).
8938
+ * `waitable` is false for a leader, whose children must never wait on it.
8735
8939
  */
8736
- reserve: (id, usd, waitable) => {
8737
- const before = held.get(id)?.usd ?? 0;
8738
- if (usd === void 0 || usd <= 0) held.delete(id);
8739
- else held.set(id, {
8740
- usd,
8940
+ reserve: (id, amount, waitable, unit = "usd") => {
8941
+ const now = held.get(id) ?? {
8942
+ usd: 0,
8943
+ tokens: 0,
8741
8944
  waitable
8742
- });
8743
- if ((usd ?? 0) < before) wake();
8945
+ };
8946
+ const before = now[unit];
8947
+ const next = {
8948
+ ...now,
8949
+ waitable,
8950
+ [unit]: Math.max(amount ?? 0, 0)
8951
+ };
8952
+ if (next.usd === 0 && next.tokens === 0) held.delete(id);
8953
+ else held.set(id, next);
8954
+ if ((amount ?? 0) < before) wake();
8744
8955
  },
8745
8956
  /** The node ended: whatever it still held is let go. */
8746
8957
  drop: (id) => {
@@ -8788,40 +8999,77 @@ const withOwnSpend = (id, result, reported) => {
8788
8999
  } : result.output
8789
9000
  };
8790
9001
  };
8791
- /** A run's spending so far; a resumed run starts from what it had spent. */
8792
- const createSpend = (budgetUsd, earlier, limits) => {
9002
+ const sum = (values) => [...values].reduce((total, value) => total + value, 0);
9003
+ /** The least of the limits that are set; `undefined` with none. */
9004
+ const least = (...limits) => {
9005
+ const known = limits.filter((limit) => limit !== void 0);
9006
+ return known.length === 0 ? void 0 : Math.min(...known);
9007
+ };
9008
+ /**
9009
+ * A run's spending so far; a resumed run starts from what it had spent.
9010
+ * `budgetTokens` caps its counted tokens as `budgetUsd` caps its dollars.
9011
+ */
9012
+ const createSpend = (budgetUsd, earlier, limits, budgetTokens) => {
8793
9013
  const reported = new Map(earlier?.reported);
8794
9014
  let spent = earlier?.total ?? 0;
9015
+ let tokens = earlier?.tokens ?? 0;
9016
+ let partial = earlier?.partial ?? false;
8795
9017
  /** What running sessions last reported, not yet in `spent`, by `node/session`. */
8796
9018
  const inFlight = /* @__PURE__ */ new Map();
8797
- const committed = () => spent + [...inFlight.values()].reduce((sum, cost) => sum + cost, 0);
9019
+ /** Their counted tokens: each process's own, so a session's add up. */
9020
+ const tokensInFlight = /* @__PURE__ */ new Map();
9021
+ const committed = () => spent + sum(inFlight.values());
9022
+ const committedTokens = () => tokens + sum(tokensInFlight.values());
8798
9023
  /** Caps running sessions were given (ADR-077): out of what is offered. */
8799
9024
  const reserves = createReserves();
8800
9025
  const budgetLeft = () => budgetUsd === void 0 ? void 0 : budgetUsd - committed() - reserves.total();
9026
+ const tokensLeft = () => budgetTokens === void 0 ? void 0 : budgetTokens - committedTokens() - reserves.total("tokens");
9027
+ /**
9028
+ * A running session said what it has spent so far, and the counted tokens of
9029
+ * the process that just ended.
9030
+ */
9031
+ const report = (id, sessionId, total, counted = 0) => {
9032
+ const key = `${id}/${sessionId}`;
9033
+ inFlight.set(key, unreported(reported, key, total));
9034
+ tokensInFlight.set(key, (tokensInFlight.get(key) ?? 0) + counted);
9035
+ };
8801
9036
  return {
8802
9037
  total: () => spent,
9038
+ /** The counted tokens of what `total` counts. */
9039
+ tokens: () => tokens,
9040
+ /** A session ran on a driver that reports no dollars: `total` is partial. */
9041
+ partial: () => partial,
9042
+ /** What the run's result says it spent. */
9043
+ sums: () => ({
9044
+ costUsd: spent,
9045
+ tokens,
9046
+ partial
9047
+ }),
9048
+ /** A session ran on `driver`. */
9049
+ started: (driver) => {
9050
+ partial ||= unpriced(driver);
9051
+ },
8803
9052
  /** A node sets aside its session's cap; nothing or 0 lets it go. */
8804
9053
  reserve: reserves.reserve,
9054
+ /** A session finished with `usage`: `report` with its counted tokens. */
9055
+ reported: (id, sessionId, usage) => {
9056
+ report(id, sessionId, usage.costUsd, countedTokens(usage));
9057
+ },
8805
9058
  /** Waits until another node's reserve frees; `false` when none can. */
8806
9059
  freed: reserves.freed,
8807
9060
  /** What a session has spent in this activation, from its total so far. */
8808
9061
  ownOf: (id, sessionId, total) => unreported(reported, `${id}/${sessionId}`, total),
8809
- /** A running session said what it has spent so far. */
8810
- report: (id, sessionId, total) => {
8811
- const key = `${id}/${sessionId}`;
8812
- inFlight.set(key, unreported(reported, key, total));
8813
- },
9062
+ report,
8814
9063
  /** The node ended: `count` has what its sessions last reported. */
8815
9064
  settled: (id) => {
8816
- for (const key of inFlight.keys()) if (key.startsWith(`${id}/`)) inFlight.delete(key);
9065
+ for (const map of [inFlight, tokensInFlight]) for (const key of map.keys()) if (key.startsWith(`${id}/`)) map.delete(key);
8817
9066
  reserves.drop(id);
8818
9067
  },
8819
9068
  /** What a session may spend: the least of the budget and any quota. */
8820
- left: () => {
8821
- const known = [budgetLeft(), limits?.().leftUsd].filter((left) => left !== void 0);
8822
- return known.length === 0 ? void 0 : Math.min(...known);
8823
- },
9069
+ left: () => least(budgetLeft(), limits?.().leftUsd),
8824
9070
  budgetLeft,
9071
+ /** What is left of the run's `budgetTokens`, its reserves out. */
9072
+ tokensLeft,
8825
9073
  /** What the limits across runs allow now. */
8826
9074
  quota: () => limits?.() ?? {},
8827
9075
  /**
@@ -8834,9 +9082,13 @@ const createSpend = (budgetUsd, earlier, limits) => {
8834
9082
  status: "quota_exhausted",
8835
9083
  reason: blocked
8836
9084
  };
8837
- return budgetUsd !== void 0 && committed() >= budgetUsd ? {
9085
+ if (budgetUsd !== void 0 && committed() >= budgetUsd) return {
8838
9086
  status: "budget_exhausted",
8839
9087
  reason: `the run's budget of $${String(budgetUsd)} is spent`
9088
+ };
9089
+ return budgetTokens !== void 0 && committedTokens() >= budgetTokens ? {
9090
+ status: "budget_exhausted",
9091
+ reason: `the run's budget of ${String(budgetTokens)} tokens is spent`
8840
9092
  } : null;
8841
9093
  },
8842
9094
  /**
@@ -8847,12 +9099,13 @@ const createSpend = (budgetUsd, earlier, limits) => {
8847
9099
  if (replayed) return result;
8848
9100
  const own = withOwnSpend(id, result, reported);
8849
9101
  spent += own.usage?.costUsd ?? 0;
9102
+ tokens += own.usage === void 0 ? 0 : countedTokens(own.usage);
8850
9103
  return own;
8851
9104
  }
8852
9105
  };
8853
9106
  };
8854
9107
  /** A run's spending, from what the scheduler is given. */
8855
- const spendOf = (budgetUsd, options) => createSpend(budgetUsd, options.replay?.earlier, options.limits);
9108
+ const spendOf = (budgetUsd, options, budgetTokens) => createSpend(budgetUsd, options.replay?.earlier, options.limits, budgetTokens);
8856
9109
  // ../core/src/run/gather.ts
8857
9110
  const asList = (value) => Array.isArray(value) ? value : [];
8858
9111
  /** `before` then `now`, leaving out an entry already there. */
@@ -9015,6 +9268,8 @@ const runResult = (run) => ({
9015
9268
  runId: run.runId,
9016
9269
  status: run.stopped ?? (run.reasons.length > 0 ? "failed" : "succeeded"),
9017
9270
  costUsd: run.costUsd,
9271
+ ...run.tokens === void 0 || run.tokens === 0 ? {} : { tokens: run.tokens },
9272
+ ...run.partial === true ? { partial: true } : {},
9018
9273
  nodes: run.nodes,
9019
9274
  reasons: run.reasons,
9020
9275
  worktrees: run.worktrees,
@@ -9291,6 +9546,8 @@ const runTree = (events) => {
9291
9546
  node.costUsd += event.usage?.costUsd ?? 0;
9292
9547
  node.inputTokens += event.usage?.inputTokens ?? 0;
9293
9548
  node.outputTokens += event.usage?.outputTokens ?? 0;
9549
+ const written = event.usage?.cacheWriteTokens ?? 0;
9550
+ if (written > 0) node.cacheWriteTokens = (node.cacheWriteTokens ?? 0) + written;
9294
9551
  } else if (event.type === "node.skipped") {
9295
9552
  const node = nodes.get(event.id);
9296
9553
  if (node !== void 0) node.state = "skipped";
@@ -9306,7 +9563,10 @@ const runTree = (events) => {
9306
9563
  if (node !== void 0) node.calledRun = event.run;
9307
9564
  } else if (event.type === "node.event" && event.event.type === "session.started") {
9308
9565
  const node = nodes.get(event.id);
9309
- if (node !== void 0) node.model = event.event.model;
9566
+ if (node !== void 0) {
9567
+ node.model = event.event.model;
9568
+ if (unpriced(event.event.driver)) node.unpriced = true;
9569
+ }
9310
9570
  }
9311
9571
  const roots = [];
9312
9572
  for (const node of nodes.values()) {
@@ -9323,6 +9583,19 @@ const slotWaitsOf = (events) => {
9323
9583
  else if (event.type === "slot.taken" || event.type === "node.finished") waiting.delete(event.id);
9324
9584
  return [...waiting];
9325
9585
  };
9586
+ /** The nodes of a run waiting for a lock now (RFC-016). */
9587
+ const lockWaitsOf = (events) => {
9588
+ const waiting = /* @__PURE__ */ new Map();
9589
+ for (const event of events) if (event.type === "lock.waiting") {
9590
+ const { id, lock, holders } = event;
9591
+ waiting.set(id, {
9592
+ id,
9593
+ lock,
9594
+ holders
9595
+ });
9596
+ } else if (event.type === "lock.taken" || event.type === "node.finished") waiting.delete(event.id);
9597
+ return [...waiting.values()];
9598
+ };
9326
9599
  /** Folds a run's `ork pause` and `ork continue` commands (ADR-041). */
9327
9600
  const pausedOf = (events) => {
9328
9601
  let run = false;
@@ -9526,7 +9799,7 @@ const createStops = (run) => {
9526
9799
  /** A promise that never settles: the losing side of a race. */
9527
9800
  const NEVER = new Promise(() => {});
9528
9801
  /** For a rejection no one is left to hear. */
9529
- const ignore = () => {};
9802
+ const ignore$1 = () => {};
9530
9803
  /** The timeout that applies to a node; a gate's own means something else. */
9531
9804
  const timeoutOf = (node, workflow) => (isToolNode(node) || isClaudeNode(node) || node.type === "workflow.call" || node.type === "mcp.call") && node.type !== "agent.subagent" ? node.timeout ?? workflow.defaults?.timeout : void 0;
9532
9805
  const createClock = (options) => {
@@ -9641,7 +9914,7 @@ const withTimeout = async (bound) => {
9641
9914
  }, graceMs);
9642
9915
  return promise;
9643
9916
  });
9644
- running.catch(ignore);
9917
+ running.catch(ignore$1);
9645
9918
  try {
9646
9919
  const result = await Promise.race([running, leftBehind]);
9647
9920
  return expired || result === null ? timedOut(timeout, result) : result;
@@ -9837,8 +10110,7 @@ const runWatch = async (options) => {
9837
10110
  };
9838
10111
  /** Whether an event only repeats a look's call, once it's been recorded. */
9839
10112
  const repeatsCall = (event) => event.type === "tool.called" || event.type === "tool.result";
9840
- // ../core/src/run/execute.ts
9841
- /** Runs an activation; control nodes pass their input through. */
10113
+ // ../core/src/run/execute-placed.ts
9842
10114
  const saidBy = (error) => error instanceof Error ? error.message : String(error);
9843
10115
  /**
9844
10116
  * A thrown error's end: a failed git or missing program by its text, else
@@ -9853,10 +10125,70 @@ const thrown = (error, fallback) => {
9853
10125
  });
9854
10126
  return failedWith(read.kind === "unknown" ? endFor(fallback, { reason: text }, text) : read);
9855
10127
  };
9856
- /** Runs an activation, its end's detail scrubbed of the run's paths. */
9857
- const createExecute = (deps) => async (activation, round) => scrubbedEnd(await executeOne(deps, activation, round), [deps.options.cwd, deps.options.projectRoot ?? ""]);
9858
- const executeOne = async (deps, activation, round) => {
9859
- const { options, contextDeps, clock, graceMs, nodeOf } = deps;
10128
+ /** Runs an activation in its checkout, under its timeout. */
10129
+ const executePlaced = async (deps, activation, round, node, executor) => {
10130
+ const { options, contextDeps, clock, graceMs } = deps;
10131
+ const { workspace } = options;
10132
+ const place = await (workspace?.prepare(activation.id, activation.template) ?? Promise.resolve({ cwd: options.cwd })).catch((error) => thrown(error, "invalid_node"));
10133
+ if ("ok" in place) return place;
10134
+ const context = nodeContext(contextDeps, activation, place.cwd, round, clock);
10135
+ const once = (signal, emit) => withTimeout({
10136
+ timeout: timeoutOf(node, options.graph.workflow),
10137
+ clock: clock.forNode(activation.id),
10138
+ signal,
10139
+ graceMs,
10140
+ start: (inner) => executor({
10141
+ ...context,
10142
+ emit,
10143
+ signal: inner,
10144
+ node,
10145
+ ...place.checkout === void 0 ? {} : { checkout: place.checkout }
10146
+ })
10147
+ });
10148
+ const watch = "watch" in node ? node.watch : void 0;
10149
+ const result = watch === void 0 ? await once(context.signal, context.emit) : await runWatch({
10150
+ id: activation.id,
10151
+ watch,
10152
+ declared: "output" in node ? node.output?.schema : void 0,
10153
+ look: (signal, quiet) => once(signal, (event) => {
10154
+ if (!quiet || !repeatsCall(event)) context.emit(event);
10155
+ }),
10156
+ scope: () => runScope(options.inputs, contextDeps.outputs, contextDeps.runId),
10157
+ hold: context.hold,
10158
+ signal: context.signal,
10159
+ graceMs,
10160
+ onEvent: options.onEvent,
10161
+ resting: (on) => {
10162
+ clock.resting(activation.id, on);
10163
+ },
10164
+ everyMs: options.watchEveryMs
10165
+ });
10166
+ const ended = result.ok || !context.signal.aborted ? result : endedWith(result, stoppedEnd(context.signal));
10167
+ return await workspace?.finish(activation.id, ended) ?? ended;
10168
+ };
10169
+ // ../core/src/run/execute.ts
10170
+ /**
10171
+ * Runs activations, each end's detail scrubbed of the run's paths; and the
10172
+ * run's locks, which the run lets go when it ends.
10173
+ */
10174
+ const createExecute = (deps) => {
10175
+ const { options, contextDeps, clock } = deps;
10176
+ const { runId, outputs } = contextDeps;
10177
+ const locks = createRunLocks({
10178
+ graph: options.graph,
10179
+ ledgers: options.locks,
10180
+ runId,
10181
+ scope: (item) => runScope(options.inputs, outputs, runId, item),
10182
+ onEvent: options.onEvent,
10183
+ waitFor: clock.waitFor
10184
+ });
10185
+ return {
10186
+ execute: async (activation, round) => scrubbedEnd(await executeOne(deps, locks, activation, round), [options.cwd, options.projectRoot ?? ""]),
10187
+ locks
10188
+ };
10189
+ };
10190
+ const executeOne = async (deps, locks, activation, round) => {
10191
+ const { options, contextDeps, nodeOf } = deps;
9860
10192
  const node = nodeOf(activation);
9861
10193
  if (node === void 0) return failedWith(endFor("unknown", {}, `unknown node ${activation.id}`));
9862
10194
  if (node.type.startsWith("control.")) return passThrough(activation.input);
@@ -9865,43 +10197,14 @@ const executeOne = async (deps, activation, round) => {
9865
10197
  try {
9866
10198
  const forEach = activation.item === void 0 ? forEachOf(node) : void 0;
9867
10199
  if (forEach !== void 0) return await deps.fanOut(activation, forEach, round);
9868
- const { workspace } = options;
9869
- const place = await (workspace?.prepare(activation.id, activation.template) ?? Promise.resolve({ cwd: options.cwd })).catch((error) => thrown(error, "invalid_node"));
9870
- if ("ok" in place) return place;
9871
- const context = nodeContext(contextDeps, activation, place.cwd, round, clock);
9872
- const once = (signal, emit) => withTimeout({
9873
- timeout: timeoutOf(node, options.graph.workflow),
9874
- clock: clock.forNode(activation.id),
9875
- signal,
9876
- graceMs,
9877
- start: (inner) => executor({
9878
- ...context,
9879
- emit,
9880
- signal: inner,
9881
- node,
9882
- ...place.checkout === void 0 ? {} : { checkout: place.checkout }
9883
- })
9884
- });
9885
- const watch = "watch" in node ? node.watch : void 0;
9886
- const result = watch === void 0 ? await once(context.signal, context.emit) : await runWatch({
9887
- id: activation.id,
9888
- watch,
9889
- declared: "output" in node ? node.output?.schema : void 0,
9890
- look: (signal, quiet) => once(signal, (event) => {
9891
- if (!quiet || !repeatsCall(event)) context.emit(event);
9892
- }),
9893
- scope: () => runScope(options.inputs, contextDeps.outputs, contextDeps.runId),
9894
- hold: context.hold,
9895
- signal: context.signal,
9896
- graceMs,
9897
- onEvent: options.onEvent,
9898
- resting: (on) => {
9899
- clock.resting(activation.id, on);
9900
- },
9901
- everyMs: options.watchEveryMs
9902
- });
9903
- const ended = result.ok || !context.signal.aborted ? result : endedWith(result, stoppedEnd(context.signal));
9904
- return await workspace?.finish(activation.id, ended) ?? ended;
10200
+ if (!("lock" in node) || node.lock === void 0) return await executePlaced(deps, activation, round, node, executor);
10201
+ const signal = contextDeps.stops.signalFor(activation.id, activation.template);
10202
+ if (!await locks.enter(activation, signal)) return failedWith(stoppedEnd(signal));
10203
+ try {
10204
+ return await executePlaced(deps, activation, round, node, executor);
10205
+ } finally {
10206
+ locks.leave(activation);
10207
+ }
9905
10208
  } catch (error) {
9906
10209
  return thrown(error, "unknown");
9907
10210
  }
@@ -10620,7 +10923,7 @@ const startRun = (options) => {
10620
10923
  const outputs = {};
10621
10924
  const rounds = /* @__PURE__ */ new Map();
10622
10925
  const sessionIds = /* @__PURE__ */ new Map();
10623
- const spend = spendOf(limits.budgetUsd, options);
10926
+ const spend = spendOf(limits.budgetUsd, options, limits.budgetTokens);
10624
10927
  const running = /* @__PURE__ */ new Set();
10625
10928
  const { summaries, summary } = createSummaries(options.replay?.earlier);
10626
10929
  const queue = [];
@@ -10705,12 +11008,13 @@ const startRun = (options) => {
10705
11008
  });
10706
11009
  const close = async () => {
10707
11010
  cancelTimeout();
11011
+ locks.end();
10708
11012
  const worktrees = await closeWorkspace(options.workspace, reasons);
10709
11013
  await options.beforeFinish?.();
10710
11014
  const result = runResult({
10711
11015
  runId,
10712
11016
  stopped,
10713
- costUsd: spend.total(),
11017
+ ...spend.sums(),
10714
11018
  nodes: [...summaries.values()],
10715
11019
  reasons,
10716
11020
  worktrees,
@@ -10763,7 +11067,7 @@ const startRun = (options) => {
10763
11067
  spend,
10764
11068
  summary
10765
11069
  };
10766
- const execute = createExecute({
11070
+ const { execute, locks } = createExecute({
10767
11071
  options,
10768
11072
  contextDeps,
10769
11073
  clock,
@@ -12153,6 +12457,9 @@ const planNodeSession = (workflow, id, options) => {
12153
12457
  if (maxTurns !== void 0) spec.maxTurns = maxTurns;
12154
12458
  const budgets = [node.budgetUsd ?? defaults.budgetUsd, options.budgetLeftUsd].filter((b) => b !== void 0);
12155
12459
  if (budgets.length > 0) spec.budgetUsd = Math.min(...budgets);
12460
+ const own = node.budgetTokens ?? defaults.budgetTokens;
12461
+ const left = options.budgetLeftTokens ?? own;
12462
+ if (left !== void 0) spec.budgetTokens = Math.min(left, own ?? left);
12156
12463
  const plugin = options.pluginDir === void 0 ? null : writePlugin(context, options.pluginDir, `Skills Ork compiled for ${id}`);
12157
12464
  if (plugin !== null) spec.plugins = [plugin];
12158
12465
  const settings = sessionSettings(context, options.cwd);
@@ -12363,6 +12670,9 @@ const FIELDS = {
12363
12670
  week: "weekUsd",
12364
12671
  month: "monthUsd"
12365
12672
  };
12673
+ /** The windows a quota in counted tokens (RFC-015) has, and their fields. */
12674
+ const TOKEN_FIELDS = { day: "dayTokens" };
12675
+ const inTokens = (cap) => "capTokens" in cap;
12366
12676
  /** Where a window began and when the next begins, in local time. */
12367
12677
  const windowOf = (window, now) => {
12368
12678
  const [year, month, day] = [
@@ -12384,33 +12694,50 @@ const windowOf = (window, now) => {
12384
12694
  until: new Date(year, month, monday + 7)
12385
12695
  };
12386
12696
  };
12387
- /** The caps that apply: the user's on everything, and the project's on itself. */
12388
- const capsOf = (user, project, root) => WINDOWS.flatMap((window) => {
12389
- const field = FIELDS[window];
12390
- const everywhere = user?.[field];
12391
- const here = project?.[field];
12392
- const caps = [];
12393
- if (everywhere !== void 0) caps.push({
12394
- window,
12395
- capUsd: everywhere
12396
- });
12397
- if (here !== void 0 && root !== void 0) caps.push({
12697
+ /**
12698
+ * The caps that apply: the user's on everything, and the project's on itself;
12699
+ * those in dollars, then those in tokens.
12700
+ */
12701
+ const capsOf = (user, project, root) => {
12702
+ const whose = (field, cap) => {
12703
+ const everywhere = user?.[field];
12704
+ const here = project?.[field];
12705
+ return [...everywhere === void 0 ? [] : [cap(everywhere)], ...here === void 0 || root === void 0 ? [] : [Object.assign(cap(here), { project: root })]];
12706
+ };
12707
+ return [...WINDOWS.flatMap((window) => whose(FIELDS[window], (capUsd) => ({
12398
12708
  window,
12399
- capUsd: here,
12400
- project: root
12401
- });
12402
- return caps;
12403
- });
12404
- /** Each cap with what has been spent against it. */
12405
- const standingOf = (store, caps, now) => caps.map((cap) => {
12709
+ capUsd
12710
+ }))), ...WINDOWS.flatMap((window) => {
12711
+ const field = TOKEN_FIELDS[window];
12712
+ return field === void 0 ? [] : whose(field, (capTokens) => ({
12713
+ window,
12714
+ capTokens
12715
+ }));
12716
+ })];
12717
+ };
12718
+ /** Each cap in dollars with what has been spent against it. */
12719
+ const standingOf = (store, caps, now) => caps.flatMap((cap) => {
12720
+ if (inTokens(cap)) return [];
12406
12721
  const { since, until } = windowOf(cap.window, now);
12407
12722
  const spentUsd = store.spentSince(since.toISOString(), cap.project);
12408
- return {
12723
+ return [{
12409
12724
  ...cap,
12410
12725
  spentUsd,
12411
12726
  leftUsd: cap.capUsd - spentUsd,
12412
12727
  resetsAt: until.toISOString()
12413
- };
12728
+ }];
12729
+ });
12730
+ /** Each cap in tokens with the counted tokens used against it. */
12731
+ const tokenStandingOf = (store, caps, now) => caps.flatMap((cap) => {
12732
+ if (!inTokens(cap)) return [];
12733
+ const { since, until } = windowOf(cap.window, now);
12734
+ const spentTokens = store.spentSince(since.toISOString(), cap.project, "tokens");
12735
+ return [{
12736
+ ...cap,
12737
+ spentTokens,
12738
+ leftTokens: cap.capTokens - spentTokens,
12739
+ resetsAt: until.toISOString()
12740
+ }];
12414
12741
  });
12415
12742
  /** Dollars: cents, or more digits for an amount under a dollar. */
12416
12743
  const usd = (amount) => `$${amount.toFixed(amount > 0 && amount < 1 ? 4 : 2)}`;
@@ -12418,7 +12745,10 @@ const at = (iso) => new Date(iso).toLocaleString(void 0, {
12418
12745
  dateStyle: "medium",
12419
12746
  timeStyle: "short"
12420
12747
  });
12421
- 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"}`;
12748
+ const capReason = (spent) => {
12749
+ const [cap, used] = "capTokens" in spent ? [`${String(spent.capTokens)} tokens`, String(spent.spentTokens)] : [usd(spent.capUsd), usd(spent.spentUsd)];
12750
+ return `${spent.project === void 0 ? "your" : "this project's"} ${spent.window} quota of ${cap} is spent (${used}); it lifts on ${at(spent.resetsAt)}, or raise it in ${spent.project === void 0 ? "~/.ork/settings.yaml" : ".ork/project.yaml"}`;
12751
+ };
12422
12752
  /** What a run or a call refused at its start says first, for its caller to read. */
12423
12753
  const NOT_STARTED = "Not started: ";
12424
12754
  /** Why a plan limit stops a run. */
@@ -12441,11 +12771,13 @@ const SHOWN_MS = 864e5;
12441
12771
  /** What the caps and the plan allow a run now. */
12442
12772
  const limitsNow = (store, caps, now) => {
12443
12773
  const standing = standingOf(store, caps, now);
12774
+ const tokens = tokenStandingOf(store, caps, now);
12444
12775
  const [plan] = refusing(store.planLimits(), now);
12445
- const spent = standing.find((cap) => cap.leftUsd <= 0);
12776
+ const spent = standing.find((cap) => cap.leftUsd <= 0) ?? tokens.find((cap) => cap.leftTokens <= 0);
12446
12777
  const blocked = plan === void 0 ? spent === void 0 ? void 0 : capReason(spent) : planReason(plan);
12447
12778
  return {
12448
12779
  ...standing.length === 0 ? {} : { leftUsd: Math.min(...standing.map((cap) => cap.leftUsd)) },
12780
+ ...tokens.length === 0 ? {} : { leftTokens: Math.min(...tokens.map((cap) => cap.leftTokens)) },
12449
12781
  ...blocked === void 0 ? {} : { blocked }
12450
12782
  };
12451
12783
  };
@@ -12453,6 +12785,7 @@ const limitsNow = (store, caps, now) => {
12453
12785
  const limitsReport = (store, { caps, problems }, now) => ({
12454
12786
  problems,
12455
12787
  caps: standingOf(store, caps, now),
12788
+ tokenCaps: tokenStandingOf(store, caps, now),
12456
12789
  plan: store.planLimits().filter((limit) => {
12457
12790
  if (limit.status === "allowed") return false;
12458
12791
  return (limit.resetsAt === void 0 ? Date.parse(limit.at) + SHOWN_MS : Date.parse(limit.resetsAt)) > now.getTime();
@@ -12783,7 +13116,9 @@ const mailOf = (events) => {
12783
13116
  * more than once reads as one history: a later result replaces the earlier
12784
13117
  * one at its place. Also: the run's inputs and settings, its worktrees, the
12785
13118
  * answers to its loops, and a fanned node's lists and its items' results by
12786
- * round (RFC-013). `replay.ts` hands them back.
13119
+ * round (RFC-013). A lock the run ended holding (RFC-016) hands back nothing
13120
+ * its nodes did under it, so they run again under the lock. `replay.ts` hands
13121
+ * them back.
12787
13122
  */
12788
13123
  const listKey = (node, round) => `${node}/${String(round)}`;
12789
13124
  /** How a run ended that cut its working nodes off, rather than failing them. */
@@ -12818,6 +13153,11 @@ const resumeState = (events) => {
12818
13153
  /** Nodes whose last activation did not finish well, and its session. */
12819
13154
  const unfinished = /* @__PURE__ */ new Map();
12820
13155
  const places = /* @__PURE__ */ new Map();
13156
+ /**
13157
+ * Locks held at the end so far, each node that ran under it and how many of
13158
+ * its results came before (RFC-016).
13159
+ */
13160
+ const spans = /* @__PURE__ */ new Map();
12821
13161
  /** A new run of `id`'s `round` replaces what is recorded there and after. */
12822
13162
  const supersede = (id, round) => {
12823
13163
  fanOuts.items.get(id)?.delete(round);
@@ -12883,6 +13223,18 @@ const resumeState = (events) => {
12883
13223
  } else history.set(event.id, [...history.get(event.id) ?? [], result]);
12884
13224
  continue;
12885
13225
  }
13226
+ if (event.type === "lock.taken") {
13227
+ if (open.get(event.id)?.item === true) continue;
13228
+ const span = spans.get(event.lock) ?? /* @__PURE__ */ new Map();
13229
+ if (!span.has(event.id)) span.set(event.id, history.get(event.id)?.length ?? 0);
13230
+ spans.set(event.lock, span);
13231
+ continue;
13232
+ }
13233
+ if (event.type === "lock.released") {
13234
+ if (open.get(event.id)?.item === true) continue;
13235
+ if (event.end === "span") spans.delete(event.lock);
13236
+ continue;
13237
+ }
12886
13238
  if (event.type === "worktree") {
12887
13239
  const known = worktrees.get(event.id);
12888
13240
  if (event.action === "created" || known === void 0) worktrees.set(event.id, {
@@ -12934,6 +13286,11 @@ const resumeState = (events) => {
12934
13286
  continue;
12935
13287
  }
12936
13288
  }
13289
+ for (const span of spans.values()) for (const [id, kept] of span) {
13290
+ const results = history.get(id)?.slice(0, kept) ?? [];
13291
+ if (results.length === 0) history.delete(id);
13292
+ else history.set(id, results);
13293
+ }
12937
13294
  for (const [id, current] of open) {
12938
13295
  closed.add(id);
12939
13296
  if (current.sessionId !== void 0) unfinished.set(id, current.sessionId);
@@ -13161,6 +13518,63 @@ const inTransaction = (db, work) => {
13161
13518
  throw error;
13162
13519
  }
13163
13520
  };
13521
+ /**
13522
+ * A run's row, with its nodes' counted tokens and whether a session ran on a
13523
+ * driver that reports no dollars (RFC-015), from the usage ledger.
13524
+ */
13525
+ const RUN_COLUMNS = `runs.*,
13526
+ (SELECT COALESCE(SUM(input_tokens + output_tokens + cache_write_tokens), 0)
13527
+ FROM usage WHERE usage.run_id = runs.id AND kind = 'node') AS tokens,
13528
+ EXISTS (SELECT 1 FROM usage WHERE usage.run_id = runs.id
13529
+ AND driver IN (${UNPRICED_DRIVERS.map((id) => [
13530
+ "'",
13531
+ id,
13532
+ "'"
13533
+ ].join("")).join(", ") || "NULL"})) AS partial`;
13534
+ /** A run row's `tokens` and `partial`, as `RUN_COLUMNS` reads them. */
13535
+ const spendOfRow = (row) => {
13536
+ const tokens = Number(row?.["tokens"] ?? 0);
13537
+ return {
13538
+ ...tokens > 0 ? { tokens } : {},
13539
+ ...Number(row?.["partial"] ?? 0) === 1 ? { partial: true } : {}
13540
+ };
13541
+ };
13542
+ const STATUSES$1 = [
13543
+ "running",
13544
+ "succeeded",
13545
+ "failed",
13546
+ "budget_exhausted",
13547
+ "quota_exhausted",
13548
+ "timed_out",
13549
+ "interrupted"
13550
+ ];
13551
+ const statusOf$1 = (value) => STATUSES$1.find((s) => s === value) ?? "failed";
13552
+ /** A run's record from its row, read with `RUN_COLUMNS`. */
13553
+ const recordOf$3 = (row) => {
13554
+ const record = {
13555
+ id: text(row, "id"),
13556
+ workflow: text(row, "workflow"),
13557
+ file: text(row, "file"),
13558
+ project: text(row, "project"),
13559
+ status: statusOf$1(text(row, "status")),
13560
+ pid: Number(row?.["pid"]),
13561
+ startedAt: text(row, "started_at"),
13562
+ costUsd: Number(row?.["cost_usd"]),
13563
+ depth: Number(row?.["depth"] ?? 0),
13564
+ ...spendOfRow(row)
13565
+ };
13566
+ const processStart = text(row, "process_start");
13567
+ if (processStart !== "") record.processStart = processStart;
13568
+ const parent = text(row, "parent");
13569
+ if (parent !== "") record.parent = parent;
13570
+ const finished = text(row, "finished_at");
13571
+ if (finished !== "") record.finishedAt = finished;
13572
+ const skipped = row?.["skipped"];
13573
+ if (typeof skipped === "number") record.skipped = skipped;
13574
+ const handled = row?.["failures_handled"];
13575
+ if (typeof handled === "number") record.failuresHandled = handled;
13576
+ return record;
13577
+ };
13164
13578
  // ../core/src/store/usage.ts
13165
13579
  /** Folds a run's events into its sessions' rows, oldest first. */
13166
13580
  const usageRowsOf = (run, events) => {
@@ -13289,8 +13703,14 @@ const alive = (row) => {
13289
13703
  /** The ledger's queries; `recordUsage` is the store's own, called on append. */
13290
13704
  const usageQueries = (db) => {
13291
13705
  const insert = db.prepare(INSERT$1);
13292
- const spentAll = db.prepare("SELECT COALESCE(SUM(cost_usd), 0) AS spent FROM usage WHERE at >= ?");
13293
- const spentHere = db.prepare("SELECT COALESCE(SUM(cost_usd), 0) AS spent FROM usage WHERE at >= ? AND project = ?");
13706
+ const spentOf = (column) => ({
13707
+ all: db.prepare(`SELECT COALESCE(SUM(${column}), 0) AS spent FROM usage WHERE at >= ?`),
13708
+ here: db.prepare(`SELECT COALESCE(SUM(${column}), 0) AS spent FROM usage WHERE at >= ? AND project = ?`)
13709
+ });
13710
+ const spent = {
13711
+ usd: spentOf("cost_usd"),
13712
+ tokens: spentOf("input_tokens + output_tokens + cache_write_tokens")
13713
+ };
13294
13714
  const runById = db.prepare("SELECT id, project, workflow FROM runs WHERE id = ?");
13295
13715
  const lastEvent = db.prepare("SELECT at FROM events WHERE run_id = ? ORDER BY seq DESC LIMIT 1");
13296
13716
  const liveOne = db.prepare("SELECT pid, process_start FROM runs WHERE id = ? AND status = 'running'");
@@ -13313,7 +13733,7 @@ const usageQueries = (db) => {
13313
13733
  });
13314
13734
  for (const row of rows) writeRow(insert, row);
13315
13735
  },
13316
- spentSince: (since, project) => Number((project === void 0 ? spentAll.get(since) : spentHere.get(since, project))?.["spent"] ?? 0),
13736
+ spentSince: (since, project, unit = "usd") => Number((project === void 0 ? spent[unit].all.get(since) : spent[unit].here.get(since, project))?.["spent"] ?? 0),
13317
13737
  usage: (filter = {}) => {
13318
13738
  const { sql, values } = whereOf(filter);
13319
13739
  return db.prepare(`SELECT * FROM usage ${sql} ORDER BY at, run_id, seq`).all(...values).map((r) => rowOf$1(r));
@@ -13369,7 +13789,7 @@ const oneNode = (run, cannot) => run.id.startsWith("node-") ? `Run ${run.id} run
13369
13789
  /** Every node of a run's tree, spawned instances included. */
13370
13790
  const flattenTree = (nodes) => nodes.flatMap((node) => [node, ...flattenTree(node.children)]);
13371
13791
  /** What a run's stored events say about it now. */
13372
- const stateOf = (store, run) => {
13792
+ const stateOf$1 = (store, run) => {
13373
13793
  const events = store.events(run.id).map((stored) => stored.event);
13374
13794
  return {
13375
13795
  paused: pausedOf(events),
@@ -13393,14 +13813,14 @@ const stopRefusal = (store, run, node) => {
13393
13813
  if (refused !== null || node === void 0) return refused;
13394
13814
  const single = oneNode(run, "stop by name");
13395
13815
  if (single !== null) return single;
13396
- const named = stateOf(store, run).nodes.filter((n) => n.id === node || n.template === node);
13816
+ const named = stateOf$1(store, run).nodes.filter((n) => n.id === node || n.template === node);
13397
13817
  if (named.length === 0) return `${node} is not a node of run ${run.id}.`;
13398
13818
  return named.some((n) => n.state === "running") ? null : `${node} is not running in run ${run.id}.`;
13399
13819
  };
13400
13820
  const pauseRefusal = (store, run, node) => {
13401
13821
  const refused = notRunning(run) ?? oneNode(run, "pause or continue");
13402
13822
  if (refused !== null) return refused;
13403
- const { paused, nodes } = stateOf(store, run);
13823
+ const { paused, nodes } = stateOf$1(store, run);
13404
13824
  if (paused.run) return `Run ${run.id} is already paused.`;
13405
13825
  if (node === void 0) return null;
13406
13826
  if (!nodes.some((n) => n.id === node)) return `${node} is not a node of run ${run.id}.`;
@@ -13409,7 +13829,7 @@ const pauseRefusal = (store, run, node) => {
13409
13829
  const continueRefusal = (store, run, node) => {
13410
13830
  const refused = notRunning(run) ?? oneNode(run, "pause or continue");
13411
13831
  if (refused !== null) return refused;
13412
- const { paused } = stateOf(store, run);
13832
+ const { paused } = stateOf$1(store, run);
13413
13833
  if (node === void 0) return paused.run || paused.nodes.length > 0 ? null : `Run ${run.id} is not paused.`;
13414
13834
  if (paused.nodes.includes(node)) return null;
13415
13835
  return paused.run ? `Run ${run.id} is paused as a whole; continue the run, not ${node}.` : `${node} is not paused in run ${run.id}.`;
@@ -13800,17 +14220,282 @@ const createCalls = (options) => {
13800
14220
  done();
13801
14221
  }
13802
14222
  };
13803
- return {
13804
- invoke: (request, signal) => {
13805
- if (request.node === void 0) return callOnce(request, signal);
13806
- const turn = (queues.get(request.callId) ?? Promise.resolve()).catch(() => null).then(() => converse(request, signal));
13807
- queues.set(request.callId, turn);
13808
- return turn;
13809
- },
13810
- stopAll: () => {
13811
- for (const id of started) stop(id);
13812
- }
14223
+ return {
14224
+ invoke: (request, signal) => {
14225
+ if (request.node === void 0) return callOnce(request, signal);
14226
+ const turn = (queues.get(request.callId) ?? Promise.resolve()).catch(() => null).then(() => converse(request, signal));
14227
+ queues.set(request.callId, turn);
14228
+ return turn;
14229
+ },
14230
+ stopAll: () => {
14231
+ for (const id of started) stop(id);
14232
+ }
14233
+ };
14234
+ };
14235
+ // ../core/src/remote/decide.ts
14236
+ /**
14237
+ * @file The open decisions of live runs, and answering one from another process
14238
+ * (`ork decide`, the local server), with the person's note if they gave one.
14239
+ * An answer goes to the store (the first one wins), then the run is woken on
14240
+ * its socket so it picks the answer up at once (specs/messaging.md).
14241
+ */
14242
+ /** Open decisions whose run is still live, oldest first, ANDing the filter. */
14243
+ const pendingDecisions = (store, filter) => {
14244
+ const live = /* @__PURE__ */ new Map();
14245
+ return store.decisions({
14246
+ ...filter,
14247
+ status: "open"
14248
+ }).filter((decision) => {
14249
+ if (!live.has(decision.runId)) {
14250
+ const [run] = store.lookup(decision.runId);
14251
+ live.set(decision.runId, run !== void 0 && liveStatus(run) === "running" && (filter.workflow === void 0 || run.workflow === filter.workflow));
14252
+ }
14253
+ return live.get(decision.runId) === true;
14254
+ });
14255
+ };
14256
+ /**
14257
+ * Answers an open decision with one of its options, and wakes its run. `home`
14258
+ * is `~`, where the sockets are (tests pass a folder); `by` says who answered;
14259
+ * `note` is what they said with it, if anything (blank is nothing).
14260
+ */
14261
+ const answerDecision = async (store, id, choice, home, by = "decide", note) => {
14262
+ const decision = store.decision(id);
14263
+ if (decision === void 0) return {
14264
+ ok: false,
14265
+ error: `No decision \`${id}\`.`
14266
+ };
14267
+ if (!decision.options.some((o) => o.id === choice)) return {
14268
+ ok: false,
14269
+ error: `\`${choice}\` is not an option of \`${id}\`: \`${decision.options.map((o) => o.id).join("`, `")}\``
14270
+ };
14271
+ if (!store.answerDecision(id, choice, by, note)) {
14272
+ const now = store.decision(id);
14273
+ return {
14274
+ ok: false,
14275
+ error: now?.status === "answered" ? `${id} was already answered: ${now.answer ?? ""} (${now.answeredBy ?? ""})` : `${id} was withdrawn`
14276
+ };
14277
+ }
14278
+ return {
14279
+ ok: true,
14280
+ woke: await wakeRun(socketPath(decision.runId, home))
14281
+ };
14282
+ };
14283
+ // ../core/src/remote/follow-tree.ts
14284
+ /**
14285
+ * Calls `onEvent` for each event of `root` and of the runs it called, once
14286
+ * each. A called run's stored events come right after the `call.started` that
14287
+ * started it; live events as they arrive. Ends when the root ends or `signal`
14288
+ * aborts.
14289
+ */
14290
+ const followRunTree = async (store, root, onEvent, options = {}) => {
14291
+ /** Every run followed, with its call path, in the order found. */
14292
+ const paths = /* @__PURE__ */ new Map([[root, []]]);
14293
+ /** Each run's last event seen, for the catch-up once the root ends. */
14294
+ const seqs = /* @__PURE__ */ new Map();
14295
+ const followers = [];
14296
+ /** Ends the called runs' following once the root's has ended. */
14297
+ const halt = new AbortController();
14298
+ const signal = options.signal === void 0 ? halt.signal : AbortSignal.any([options.signal, halt.signal]);
14299
+ const handle = (run, path) => (stored) => {
14300
+ seqs.set(run, stored.seq);
14301
+ onEvent({
14302
+ run,
14303
+ path,
14304
+ stored
14305
+ });
14306
+ const { event } = stored;
14307
+ if (event.type === "call.started" && !paths.has(event.run)) follow(event.run, [...path, event.node]);
14308
+ };
14309
+ const follow = (run, path) => {
14310
+ paths.set(run, path);
14311
+ followers.push(followEvents(store, run, handle(run, path), {
14312
+ ...options,
14313
+ follow: true,
14314
+ after: seqs.get(run) ?? 0,
14315
+ signal
14316
+ }));
14317
+ };
14318
+ /** Waits for every follower, including those started while waiting. */
14319
+ const settle = async () => {
14320
+ while (followers.length > 0) await Promise.all(followers.splice(0));
14321
+ };
14322
+ const end = await followEvents(store, root, handle(root, []), {
14323
+ ...options,
14324
+ follow: true
14325
+ });
14326
+ halt.abort();
14327
+ await settle();
14328
+ if (end !== "aborted") {
14329
+ for (const [run, path] of [...paths].slice(1)) followers.push(followEvents(store, run, handle(run, path), {
14330
+ follow: false,
14331
+ after: seqs.get(run) ?? 0
14332
+ }));
14333
+ await settle();
14334
+ }
14335
+ return {
14336
+ end,
14337
+ stillRunning: [...paths.keys()].slice(1).filter((id) => {
14338
+ const [run] = store.lookup(id);
14339
+ return run !== void 0 && liveStatus(run) === "running";
14340
+ })
14341
+ };
14342
+ };
14343
+ // ../core/src/remote/relay.ts
14344
+ /**
14345
+ * @file A run's side of the relay (RFC-003): its nodes' links, read from the
14346
+ * store, and sending to a node of another run. A message is a row in the
14347
+ * store and a wake on that run's socket, so no process has to stay up for it,
14348
+ * and the store has it if the socket is gone. Reaching a link's limit asks
14349
+ * the user whether to go on, as a loop out of rounds does.
14350
+ */
14351
+ /** For a refused or abandoned question: a stopped run has nothing to do. */
14352
+ const ignore = () => {};
14353
+ const createRelay = (options) => {
14354
+ const { store, runId, home, human } = options;
14355
+ /** Links with the question about their limit open. */
14356
+ const asking = /* @__PURE__ */ new Set();
14357
+ const askToGoOn = async (link, node) => {
14358
+ if (asking.has(link.id)) return;
14359
+ asking.add(link.id);
14360
+ try {
14361
+ if (await human.ask({
14362
+ nodeId: node,
14363
+ question: `${addressOf(link.a)} and ${addressOf(link.b)} have used all ${String(link.maxMessages)} messages of their link. Let them go on?`,
14364
+ options: [{
14365
+ id: "extend",
14366
+ description: `${String(link.maxMessages)} more`
14367
+ }, {
14368
+ id: "end",
14369
+ description: "End the link"
14370
+ }],
14371
+ recommended: "end"
14372
+ }) === "extend") {
14373
+ store.extendLink(link.id, link.maxMessages);
14374
+ await Promise.all([link.a, link.b].map((end) => tellNode(store, end, "[ork] The user let this link go on; you can send again.", home)));
14375
+ } else await endLink(store, link, "the user ended it", home);
14376
+ } finally {
14377
+ asking.delete(link.id);
14378
+ }
14379
+ };
14380
+ /** Why `peer`'s run cannot take a message now, or null. */
14381
+ const closed = (peer) => {
14382
+ const [run] = store.lookup(peer.run);
14383
+ if (run === void 0) return `run ${peer.run} is gone`;
14384
+ const status = liveStatus(run);
14385
+ return status === "running" ? null : `its run has finished (${status}). A finished run's session is not resumed by another agent; the user can resume it with \`ork chat\``;
14386
+ };
14387
+ const isRunning = (peer) => {
14388
+ const events = store.events(peer.run).map((stored) => stored.event);
14389
+ return flattenTree(runTree(events)).some((n) => n.id === peer.node && n.state === "running");
14390
+ };
14391
+ return {
14392
+ peers: (node) => store.links({
14393
+ run: runId,
14394
+ live: true
14395
+ }).flatMap((link) => {
14396
+ const peer = peerOf(link, runId, node);
14397
+ return peer === void 0 ? [] : [addressOf(peer)];
14398
+ }),
14399
+ send: (node, to, body) => {
14400
+ const link = store.links({
14401
+ run: runId,
14402
+ live: true
14403
+ }).find((candidate) => {
14404
+ const peer = peerOf(candidate, runId, node);
14405
+ return peer !== void 0 && addressOf(peer) === to;
14406
+ });
14407
+ const peer = link && peerOf(link, runId, node);
14408
+ if (link === void 0 || peer === void 0) return {
14409
+ ok: false,
14410
+ error: `you have no live link to ${to}`
14411
+ };
14412
+ const why = closed(peer);
14413
+ if (why !== null) {
14414
+ endLink(store, link, why, home);
14415
+ return {
14416
+ ok: false,
14417
+ error: `${to} cannot take it: ${why}`
14418
+ };
14419
+ }
14420
+ if (link.messages >= link.maxMessages) {
14421
+ askToGoOn(link, node).catch(ignore);
14422
+ return {
14423
+ ok: false,
14424
+ error: `this link has used all ${String(link.maxMessages)} of its messages, and the user has been asked whether to go on`
14425
+ };
14426
+ }
14427
+ store.countMessage(link.id);
14428
+ store.command(peer.run, {
14429
+ kind: "relay",
14430
+ node: peer.node,
14431
+ from: `${runId}/${node}`,
14432
+ body
14433
+ });
14434
+ wakeRun(socketPath(peer.run, home));
14435
+ return {
14436
+ ok: true,
14437
+ delivered: isRunning(peer) ? "now" : "queued"
14438
+ };
14439
+ }
14440
+ };
14441
+ };
14442
+ // ../core/src/remote/send.ts
14443
+ /**
14444
+ * @file `ork send` (RFC-005): a program in a tool node writes to a session on
14445
+ * its run's bus. The check is on the sender's side, as a refusal must be an
14446
+ * exit code a script can act on; then a row in the store and a wake, as for a
14447
+ * message between linked runs. `ORK_RUN` and `ORK_NODE` say who it is, and
14448
+ * they are attribution, not proof: the check stops mistakes, not a program
14449
+ * running as the same user.
14450
+ */
14451
+ /** A first value, as for links; real use will show whether it is the right one. */
14452
+ const DEFAULT_SCRIPT_MESSAGES = 100;
14453
+ /**
14454
+ * Why this sender may not write to this node; `null` when it may. The body is
14455
+ * not needed, so a program is told before it pipes a whole message in.
14456
+ */
14457
+ const scriptRefusal = (store, send) => {
14458
+ const { run: runId, node, to } = send;
14459
+ if (runId === void 0 || node === void 0) return "`ork send` runs inside a tool node of a run: `ORK_RUN` and `ORK_NODE` are not set.";
14460
+ if (runId.startsWith("node-")) return `Run ${runId} runs one node, which has no message bus; run the whole workflow.`;
14461
+ const run = store.lookup(runId).find((found) => found.id === runId);
14462
+ if (run === void 0) return `There is no run ${runId}.`;
14463
+ const status = liveStatus(run);
14464
+ if (status !== "running") return `Run ${runId} is not running (${status}).`;
14465
+ const started = store.events(runId).map((stored) => stored.event).findLast((event) => event.type === "run.started");
14466
+ if (started === void 0) return `Run ${runId} has not started yet.`;
14467
+ const own = started.nodes.find((found) => found.id === node);
14468
+ if (own === void 0 || !own.type.startsWith("tool.")) return `${node} is not a tool node of run ${runId}.`;
14469
+ const contacts = scriptContactsOf(node, started.nodes, started.channels ?? []);
14470
+ if (contacts.includes(to)) return null;
14471
+ return contacts.length === 0 ? `\`${node}\` has no one to write to: give it an owner, or a channel under \`messages:\`.` : `\`${to}\` is not a contact of \`${node}\`, which can write to \`${contacts.join("`, `")}\` (its owner and its channels).`;
14472
+ };
14473
+ /** Reading all of it would cost as much as sending it. */
14474
+ const TOO_LONG = "The message is over 64 KiB. Write it to a file and send the file's path.";
14475
+ /** Checks the message, writes it for the run, and wakes it. */
14476
+ const sendFromScript = async (store, send, home) => {
14477
+ const refused = scriptRefusal(store, send);
14478
+ if (refused !== null) return {
14479
+ ok: false,
14480
+ error: refused
13813
14481
  };
14482
+ if (Buffer.byteLength(send.body) > 65536) return {
14483
+ ok: false,
14484
+ error: TOO_LONG
14485
+ };
14486
+ const run = String(send.run);
14487
+ const node = String(send.node);
14488
+ if (!store.scriptCommand(run, {
14489
+ kind: "script",
14490
+ node: send.to,
14491
+ from: node,
14492
+ body: send.body
14493
+ }, DEFAULT_SCRIPT_MESSAGES)) return {
14494
+ ok: false,
14495
+ error: `\`${node}\` has sent ${String(DEFAULT_SCRIPT_MESSAGES)} messages in this run, which is the most it may.`
14496
+ };
14497
+ await wakeRun(socketPath(run, home));
14498
+ return { ok: true };
13814
14499
  };
13815
14500
  // ../core/src/run/driver-queues.ts
13816
14501
  const isSession = (node) => node.type === "agent.leader" || node.type === "agent.worker";
@@ -13878,7 +14563,7 @@ const checkoutContention = (workflow, include) => {
13878
14563
  }
13879
14564
  return found;
13880
14565
  };
13881
- const messageOf$1 = (driver, found) => {
14566
+ const messageOf$2 = (driver, found) => {
13882
14567
  const names = found.nodes.map((n) => `\`${n}\``).join(", ");
13883
14568
  const where = found.checkout === void 0 ? "the project checkout" : `the worktree of \`${found.checkout}\``;
13884
14569
  return found.owner === void 0 ? `driver \`${driver}\` runs one session per checkout, but ${names} work in ${where} and could run at once: one session waits for the other. Give them separate worktrees (\`isolation: worktree\`), or run them on another driver.` : `node \`${found.owner}\` runs on driver \`${driver}\`, which runs one session per checkout, but ${names} work in ${where} and one owns another: the owner's session holds the checkout while the nodes it owns wait for it, so the run would never finish. Give each owned node its own worktree (\`isolation: worktree\`), or run them on another driver.`;
@@ -13893,16 +14578,17 @@ const driverQueues = (workflow, drivers, choice) => drivers.filter((driver) => d
13893
14578
  return node !== void 0 && nodeDriver(node, workflow, choice) === driver.id;
13894
14579
  }).map((found) => Object.assign({}, found, {
13895
14580
  driver: driver.id,
13896
- message: messageOf$1(driver.id, found)
14581
+ message: messageOf$2(driver.id, found)
13897
14582
  })));
13898
14583
  // ../core/src/run/driver-needs.ts
13899
14584
  const SESSION_TYPES = /* @__PURE__ */ new Set(["agent.leader", "agent.worker"]);
13900
- /** The node's turn limit and dollar budget, its own or the workflow's default. */
14585
+ /** The node's turn limit and budgets, its own or the workflow's default. */
13901
14586
  const limitsOf = (node, workflow) => {
13902
14587
  const own = isClaudeNode(node) ? node : void 0;
13903
14588
  return {
13904
14589
  maxTurns: own?.maxTurns ?? workflow.defaults?.maxTurns,
13905
- budgetUsd: own?.budgetUsd ?? workflow.defaults?.budgetUsd
14590
+ budgetUsd: own?.budgetUsd ?? workflow.defaults?.budgetUsd,
14591
+ budgetTokens: own?.budgetTokens ?? workflow.defaults?.budgetTokens
13906
14592
  };
13907
14593
  };
13908
14594
  /**
@@ -13932,7 +14618,7 @@ const subagentRefusals = (id, workflow, driver) => Object.entries(workflow.nodes
13932
14618
  * rendered names the driver.
13933
14619
  */
13934
14620
  const sessionRefusals = (id, node, workflow, driver) => {
13935
- const { maxTurns, budgetUsd } = limitsOf(node, workflow);
14621
+ const { maxTurns } = limitsOf(node, workflow);
13936
14622
  const refuse = (message) => ({
13937
14623
  node: id,
13938
14624
  message: `node \`${id}\` ${message}`
@@ -13940,14 +14626,37 @@ const sessionRefusals = (id, node, workflow, driver) => {
13940
14626
  const refusals = [];
13941
14627
  if (!driver.proven) refusals.push(refuse(`runs on driver \`${driver.id}\`, which is not yet proven on a real session`));
13942
14628
  if (maxTurns !== void 0 && !driver.capabilities.turnLimit) refusals.push(refuse(`sets maxTurns, but driver \`${driver.id}\` has no turn limit`));
13943
- if (budgetUsd !== void 0 && !driver.capabilities.price) refusals.push(refuse(`sets budgetUsd, but driver \`${driver.id}\` does not report cost in dollars (a token budget comes with RFC-015)`));
13944
- else if (workflow.limits?.budgetUsd !== void 0 && !driver.capabilities.price) refusals.push(refuse(`runs under the workflow's limits.budgetUsd, but driver \`${driver.id}\` does not report cost in dollars, so the budget can't be kept (a token budget comes with RFC-015)`));
13945
14629
  if ((("mcp" in node ? node.mcp : void 0) ?? workflow.defaults?.mcp ?? []).length > 0 && !driver.capabilities.mcpServers) refusals.push(refuse(`names MCP servers (\`mcp\`), but driver \`${driver.id}\` does not load a node's own MCP servers`));
13946
14630
  const ask = askOfTools(id, node, workflow);
13947
14631
  if (ask !== void 0 && !driver.capabilities.questions) refusals.push(refuse(`${ask}, but driver \`${driver.id}\` does not reach Ork's tools (ask_user, delegate, messages)`));
13948
14632
  return [...refusals, ...subagentRefusals(id, workflow, driver)];
13949
14633
  };
13950
14634
  /**
14635
+ * What a dollar budget can't see on `driver`, which reports no dollars
14636
+ * (RFC-015): the node's own `budgetUsd` with no `budgetTokens` beside it, or
14637
+ * the run's `limits.budgetUsd`, which this node's sessions spend unseen.
14638
+ */
14639
+ const sessionBudgetWarnings = (id, node, workflow, driver) => {
14640
+ if (driver.capabilities.price) return [];
14641
+ const { budgetUsd, budgetTokens } = limitsOf(node, workflow);
14642
+ const warn = (message) => ({
14643
+ node: id,
14644
+ message: `node \`${id}\` ${message}`
14645
+ });
14646
+ return [...budgetUsd !== void 0 && budgetTokens === void 0 ? [warn(`sets only budgetUsd, but driver \`${driver.id}\` reports no dollars: only budgetTokens applies on ${driver.id}`)] : [], ...workflow.limits?.budgetUsd === void 0 ? [] : [warn(`runs under the workflow's limits.budgetUsd, but driver \`${driver.id}\` reports no dollars, so that limit can't see its sessions`)]];
14647
+ };
14648
+ /**
14649
+ * The warnings `sessionBudgetWarnings` gives each session node (or only
14650
+ * `only`), on the driver it takes as for `driverRefusals`. Never a refusal.
14651
+ */
14652
+ const budgetWarnings = (workflow, drivers, choice) => Object.entries(workflow.nodes).flatMap(([id, node]) => {
14653
+ if (!SESSION_TYPES.has(node.type)) return [];
14654
+ if (choice.only !== void 0 && choice.only !== id) return [];
14655
+ const driverId = nodeDriver(node, workflow, choice);
14656
+ const driver = drivers.find((d) => d.id === driverId);
14657
+ return driver === void 0 ? [] : sessionBudgetWarnings(id, node, workflow, driver);
14658
+ });
14659
+ /**
13951
14660
  * What stops each session node (or only `only`) from running on its driver: its
13952
14661
  * model's prefix, else `override` (`--driver`), else what the workflow names,
13953
14662
  * else `ready`. A node with no driver to check is left out; the run refuses
@@ -14023,6 +14732,89 @@ const parseInputPairs = (pairs) => {
14023
14732
  raw
14024
14733
  };
14025
14734
  };
14735
+ // ../core/src/run/replay.ts
14736
+ const NO_FAN_OUTS = {
14737
+ lists: /* @__PURE__ */ new Map(),
14738
+ items: /* @__PURE__ */ new Map()
14739
+ };
14740
+ /** Whether an activation's input carries a result this run made anew. */
14741
+ const carries = (fresh, input) => {
14742
+ if (input === void 0) return false;
14743
+ if (fresh.has(input)) return true;
14744
+ const results = input["results"];
14745
+ if (results === null || typeof results !== "object") return false;
14746
+ return Object.values(results).some((value) => value !== null && typeof value === "object" && fresh.has(value));
14747
+ };
14748
+ /**
14749
+ * Hands each node's recorded results back in order, until the node runs anew
14750
+ * once (it failed or never finished last time, or its input is new); from then
14751
+ * on it always runs. A node cut off mid-session continues that session, once.
14752
+ */
14753
+ const createReplay = (history, cutOff = /* @__PURE__ */ new Map(), mail, earlier, fanOuts = NO_FAN_OUTS) => {
14754
+ const sessions = new Map(cutOff);
14755
+ const used = /* @__PURE__ */ new Map();
14756
+ const anew = /* @__PURE__ */ new Set();
14757
+ const fresh = /* @__PURE__ */ new WeakSet();
14758
+ /** Fanned nodes whose list was resolved anew: their items can't be replayed. */
14759
+ const renewed = /* @__PURE__ */ new Set();
14760
+ const takeItem = (activation, of) => {
14761
+ const recorded = fanOuts.items.get(activation.id)?.get(activation.round ?? 0);
14762
+ if (recorded !== void 0 && !renewed.has(of) && !carries(fresh, activation.input)) return recorded;
14763
+ anew.add(activation.id);
14764
+ return null;
14765
+ };
14766
+ return {
14767
+ take: (activation) => {
14768
+ const { id } = activation;
14769
+ if (activation.brief !== void 0 || anew.has(id)) return null;
14770
+ if (activation.item !== void 0) return takeItem(activation, activation.item.of);
14771
+ const index = used.get(id) ?? 0;
14772
+ const recorded = history.get(id)?.[index];
14773
+ if (recorded === void 0 || carries(fresh, activation.input)) {
14774
+ anew.add(id);
14775
+ return null;
14776
+ }
14777
+ used.set(id, index + 1);
14778
+ return recorded;
14779
+ },
14780
+ ran: (result) => {
14781
+ fresh.add(result.output);
14782
+ },
14783
+ cutOff: (activation) => {
14784
+ const session = activation.brief === void 0 && (activation.item === void 0 || !renewed.has(activation.item.of)) ? sessions.get(activation.id) : void 0;
14785
+ sessions.delete(activation.id);
14786
+ return session;
14787
+ },
14788
+ listed: (id, round, source) => {
14789
+ const list = fanOuts.lists.get(listKey(id, round));
14790
+ const ranAnew = typeof source === "object" && source !== null && fresh.has(source);
14791
+ if (list !== void 0 && !ranAnew) return list;
14792
+ renewed.add(id);
14793
+ return null;
14794
+ },
14795
+ previousSession: ({ id, item, round = 0 }) => item === void 0 || renewed.has(item.of) ? void 0 : [...fanOuts.items.get(id) ?? []].findLast(([before]) => before < round)?.[1].sessionId,
14796
+ lastOutput: (id) => history.get(id)?.at(-1)?.output,
14797
+ kept: () => [...history.keys(), ...fanOuts.items.keys()],
14798
+ ...mail === void 0 ? {} : { mail },
14799
+ ...earlier === void 0 ? {} : { earlier }
14800
+ };
14801
+ };
14802
+ /**
14803
+ * The run's `HumanChannel`, answering questions about loops out of rounds as
14804
+ * they were answered last time, in order; anything else goes to `human`.
14805
+ */
14806
+ const replayLoopAnswers = (human, answers) => {
14807
+ const left = [...answers];
14808
+ return {
14809
+ permit: (request) => human.permit(request),
14810
+ ...human.settled === void 0 ? {} : { settled: human.settled },
14811
+ ask: (question) => {
14812
+ const index = left.findIndex((a) => a.question === question.question);
14813
+ const [given] = index === -1 ? [] : left.splice(index, 1);
14814
+ return given === void 0 ? human.ask(question) : Promise.resolve(given.answer);
14815
+ }
14816
+ };
14817
+ };
14026
14818
  // ../core/src/store/drafts.ts
14027
14819
  const rowOf = (row) => {
14028
14820
  const baseHash = text(row, "base_hash");
@@ -14053,15 +14845,6 @@ const draftQueries = (db) => {
14053
14845
  }
14054
14846
  };
14055
14847
  };
14056
- // ../core/src/worktree/manager.ts
14057
- /**
14058
- * @file Ork's git worktrees: one per `worktree` node, beside the clone, by
14059
- * default in `../<clone>-worktrees/ork-<run>-<node>` on branch
14060
- * `ork/<run>/<node>` (ADR-020; a project's git rules rename them, ADR-032).
14061
- * Ork only creates, commits, merges and removes what its runs created; it
14062
- * never pushes.
14063
- */
14064
- const BRANCH_PREFIX = "ork/";
14065
14848
  /** Gitignored files a session needs as much in a worktree as in the clone. */
14066
14849
  const LOCAL_AGENT_FILES = ["CLAUDE.local.md", ".claude/settings.local.json"];
14067
14850
  /** Where the project's instructions, skills and agents live. */
@@ -14205,11 +14988,27 @@ const createWorktree = async (options) => {
14205
14988
  };
14206
14989
  /** Copies files that exist in `from` and are missing in the worktree. */
14207
14990
  const carryFiles = (from, worktree, files) => copyInto(from, worktree, files.filter((f) => !path.isAbsolute(f) && !f.split(/[\\/]/u).includes("..")));
14991
+ /** What git keeps in a worktree's git dir while a rebase, merge or pick runs. */
14992
+ const UNDERWAY = [
14993
+ "rebase-merge",
14994
+ "rebase-apply",
14995
+ "MERGE_HEAD",
14996
+ "CHERRY_PICK_HEAD",
14997
+ "REVERT_HEAD"
14998
+ ];
14999
+ /** Whether a rebase, merge, cherry-pick or revert is underway in `worktree`. */
15000
+ const gitOperationUnderway = async (worktree) => {
15001
+ const paths = await gitOk(worktree, ["rev-parse", ...UNDERWAY.flatMap((name) => ["--git-path", name])]);
15002
+ return lines(paths).some((file) => existsSync(path.resolve(worktree, file)));
15003
+ };
14208
15004
  /**
14209
15005
  * Commits what a session left uncommitted. The repo's hooks run; when they
14210
- * refuse, the error says why and the worktree keeps the changes.
15006
+ * refuse, the error says why and the worktree keeps the changes. A worktree
15007
+ * mid-rebase (or merge, cherry-pick, revert) is left exactly as it is: its
15008
+ * changes are the operation's to finish, as a conflict resolver's are.
14211
15009
  */
14212
15010
  const commitLeftovers = async (worktree, message) => {
15011
+ if (await gitOperationUnderway(worktree)) return false;
14213
15012
  const status = await gitOk(worktree, ["status", "--porcelain"]);
14214
15013
  if (lines(status).length === 0) return false;
14215
15014
  await gitOk(worktree, ["add", "-A"]);
@@ -14264,6 +15063,245 @@ const removeWorktree = async (root, worktree, force = false) => {
14264
15063
  worktree
14265
15064
  ]);
14266
15065
  };
15066
+ // ../core/src/worktree/list.ts
15067
+ /**
15068
+ * @file What `ork worktrees` shows: every worktree and branch Ork's runs
15069
+ * recorded creating, whatever the project's git rules named them, and any
15070
+ * `ork/*` branch besides, with their state read from git itself. What clean
15071
+ * and remove do with them is clean.ts.
15072
+ */
15073
+ /**
15074
+ * The worktrees the project's runs recorded creating, oldest run first; one
15075
+ * entry per branch or folder, from the latest run that created it.
15076
+ */
15077
+ const createdWorktrees = (store, project) => {
15078
+ const byName = /* @__PURE__ */ new Map();
15079
+ for (const run of store.runs({ project }).toReversed()) for (const { event } of store.events(run.id)) {
15080
+ if (event.type !== "worktree" || event.action !== "created") continue;
15081
+ byName.set(event.branch ?? event.path, {
15082
+ runId: run.id,
15083
+ nodeId: event.id,
15084
+ path: event.path,
15085
+ ...event.branch === void 0 ? {} : { branch: event.branch }
15086
+ });
15087
+ }
15088
+ return [...byName.values()];
15089
+ };
15090
+ const parsePorcelain = (text) => text.split("\n\n").map((block) => lines(block)).map((fields) => {
15091
+ const value = (key) => fields.find((f) => f.startsWith(`${key} `))?.slice(key.length + 1);
15092
+ return {
15093
+ path: value("worktree") ?? "",
15094
+ branch: (value("branch") ?? "").replace(/^refs\/heads\//u, ""),
15095
+ prunable: fields.some((f) => f.startsWith("prunable"))
15096
+ };
15097
+ }).filter((entry) => entry.path !== "");
15098
+ /**
15099
+ * The `ork/<run>/<node>` branches of runs that recorded nothing; with no path
15100
+ * recorded, the worktree is whichever has the branch checked out.
15101
+ */
15102
+ const prefixed = (branch) => {
15103
+ const [, runId = "", ...rest] = branch.split("/");
15104
+ return {
15105
+ runId,
15106
+ nodeId: rest.join("/"),
15107
+ path: "",
15108
+ branch
15109
+ };
15110
+ };
15111
+ /** How many commits a detached worktree holds that no branch, tag or remote has. */
15112
+ const onNoBranch = async (entry) => entry === void 0 ? "0" : (await git(entry.path, [
15113
+ "rev-list",
15114
+ "--count",
15115
+ "HEAD",
15116
+ "--not",
15117
+ "--branches",
15118
+ "--tags",
15119
+ "--remotes"
15120
+ ])).stdout;
15121
+ /** One created worktree or branch, with its state read from git. */
15122
+ const stateOf = async (root, entries, item, recorded) => {
15123
+ const { branch } = item;
15124
+ const entry = branch === void 0 || item.path !== "" ? entries.find((e) => e.path === item.path) : entries.find((e) => e.branch === branch);
15125
+ const holder = branch === void 0 ? void 0 : entries.find((e) => e.branch === branch && e.path !== entry?.path);
15126
+ const missing = entry !== void 0 && (entry.prunable || !existsSync(entry.path));
15127
+ const status = entry === void 0 || missing ? "" : (await git(entry.path, ["status", "--porcelain"])).stdout;
15128
+ const checkedOut = branch === void 0 && entry !== void 0 && entry.branch !== "" ? entry.branch : void 0;
15129
+ const on = branch ?? checkedOut;
15130
+ const counted = on === void 0 ? await onNoBranch(entry === void 0 || missing ? void 0 : entry) : (await git(root, [
15131
+ "rev-list",
15132
+ "--count",
15133
+ `HEAD..${on}`
15134
+ ])).stdout;
15135
+ const ahead = Number(counted);
15136
+ const changes = lines(status).length;
15137
+ return {
15138
+ ...branch === void 0 ? {} : { branch },
15139
+ ...checkedOut === void 0 ? {} : { checkedOut },
15140
+ runId: item.runId,
15141
+ nodeId: item.nodeId,
15142
+ recorded,
15143
+ path: entry?.path ?? null,
15144
+ missing,
15145
+ dirty: changes > 0,
15146
+ changes,
15147
+ ahead: Number.isNaN(ahead) ? 0 : ahead,
15148
+ merged: on !== void 0 && ahead === 0,
15149
+ inMainCheckout: entry?.path === root || holder?.path === root,
15150
+ ...holder === void 0 || holder.path === root ? {} : { heldBy: holder.path }
15151
+ };
15152
+ };
15153
+ /**
15154
+ * Every worktree and branch Ork's runs created in the repository at `root`,
15155
+ * with its state: those the runs recorded (`created`, whatever the project's
15156
+ * git rules named them), and any `ork/` branch besides. A recorded one that git
15157
+ * no longer has is left out.
15158
+ */
15159
+ const listOrkWorktrees = async (root, created = []) => {
15160
+ const entries = parsePorcelain(await gitOk(root, [
15161
+ "worktree",
15162
+ "list",
15163
+ "--porcelain"
15164
+ ]));
15165
+ const branches = lines(await gitOk(root, [
15166
+ "for-each-ref",
15167
+ "--format=%(refname:short)",
15168
+ "refs/heads/"
15169
+ ]));
15170
+ const recordedBranches = new Set(created.flatMap((c) => c.branch ?? []));
15171
+ const items = [...created.filter((c) => c.branch === void 0 ? entries.some((e) => e.path === c.path) : branches.includes(c.branch)).map((item) => ({
15172
+ item,
15173
+ recorded: true
15174
+ })), ...branches.filter((b) => b.startsWith("ork/") && !recordedBranches.has(b)).map((b) => ({
15175
+ item: prefixed(b),
15176
+ recorded: false
15177
+ }))];
15178
+ return Promise.all(items.map(({ item, recorded }) => stateOf(root, entries, item, recorded)));
15179
+ };
15180
+ // ../core/src/store/prune.ts
15181
+ /**
15182
+ * @file `ork prune` (ADR-046): which runs may lose their transcript, and doing
15183
+ * it. A run goes with the runs it called or was called by, or not at all, and
15184
+ * not while it is live, is linked, or has a worktree or branch left: those
15185
+ * read the run's events. The usage ledger and feedback records stay.
15186
+ */
15187
+ const messageOf$1 = (error) => error instanceof Error ? error.message : String(error);
15188
+ /**
15189
+ * The runs in a project that still have a worktree or branch. With its folder
15190
+ * gone, or no longer a repository, there is nothing left to clean up; any other
15191
+ * failure says nothing about them, so it rejects.
15192
+ */
15193
+ const runsWithWorktrees = (store) => async (root) => {
15194
+ if (!existsSync(root)) return /* @__PURE__ */ new Set();
15195
+ try {
15196
+ const found = await listOrkWorktrees(root, createdWorktrees(store, root));
15197
+ return new Set(found.map((w) => w.runId));
15198
+ } catch (error) {
15199
+ if (/not a git repository/iu.test(messageOf$1(error))) return /* @__PURE__ */ new Set();
15200
+ throw error;
15201
+ }
15202
+ };
15203
+ /** The run at the top of a run's chain of calls. */
15204
+ const callRootOf = (run, byId) => {
15205
+ let top = run;
15206
+ const seen = /* @__PURE__ */ new Set([run.id]);
15207
+ while (top.parent !== void 0) {
15208
+ const parent = byId.get(top.parent);
15209
+ if (parent === void 0 || seen.has(parent.id)) break;
15210
+ seen.add(parent.id);
15211
+ top = parent;
15212
+ }
15213
+ return top.id;
15214
+ };
15215
+ /** Sorts the runs before the cutoff into those to remove and those to keep. */
15216
+ const planPrune = async (store, filter, worktreesOf = runsWithWorktrees(store)) => {
15217
+ const all = store.runs();
15218
+ const byId = new Map(all.map((run) => [run.id, run]));
15219
+ const inScope = (run) => filter.project === void 0 || run.project === filter.project;
15220
+ const old = (run) => (store.lastEventAt(run.id) ?? run.finishedAt ?? run.startedAt) < filter.before;
15221
+ const trees = /* @__PURE__ */ new Map();
15222
+ for (const run of all) {
15223
+ const root = callRootOf(run, byId);
15224
+ trees.set(root, [...trees.get(root) ?? [], run]);
15225
+ }
15226
+ const projects = [...new Set(all.filter((r) => old(r) && inScope(r)).map((r) => r.project))];
15227
+ const withWorktrees = new Map(await Promise.all(projects.map(async (project) => {
15228
+ try {
15229
+ return [project, await worktreesOf(project)];
15230
+ } catch (error) {
15231
+ return [project, messageOf$1(error)];
15232
+ }
15233
+ })));
15234
+ /** Why this run itself cannot go, or `null` when it can. */
15235
+ const blocker = (run) => {
15236
+ if (liveStatus(run) === "running") return "it is still running";
15237
+ const worktrees = withWorktrees.get(run.project);
15238
+ if (typeof worktrees === "string") return `cannot tell whether it has a worktree left: ${worktrees}`;
15239
+ if (worktrees?.has(run.id) === true) return "it has a worktree or branch left (ork worktrees clean)";
15240
+ if (store.links({
15241
+ run: run.id,
15242
+ live: true
15243
+ }).length > 0) return "it has a live link";
15244
+ return null;
15245
+ };
15246
+ const plan = {
15247
+ remove: [],
15248
+ keep: []
15249
+ };
15250
+ for (const tree of trees.values()) {
15251
+ const held = tree.map((run) => {
15252
+ if (!inScope(run)) return `run ${run.id} of its call is in another project (--all)`;
15253
+ if (!old(run)) return `run ${run.id} of its call is newer`;
15254
+ const why = blocker(run);
15255
+ return why === null ? null : `run ${run.id} of its call stays: ${why}`;
15256
+ }).find((why) => why !== null);
15257
+ for (const run of tree) {
15258
+ if (!inScope(run) || !old(run)) continue;
15259
+ if (held === void 0) plan.remove.push(run);
15260
+ else plan.keep.push({
15261
+ run,
15262
+ reason: blocker(run) ?? held
15263
+ });
15264
+ }
15265
+ }
15266
+ return plan;
15267
+ };
15268
+ const SAFE_ID = /^[\w-]+$/u;
15269
+ /** Forgets the plan's runs and removes their folders. */
15270
+ const applyPrune = (store, plan) => {
15271
+ const outcome = {
15272
+ removed: 0,
15273
+ folders: 0,
15274
+ failed: []
15275
+ };
15276
+ const places = store.projects();
15277
+ for (const run of plan.remove) {
15278
+ if (!store.deleteRun(run.id)) {
15279
+ outcome.failed.push({
15280
+ run: run.id,
15281
+ reason: "it is running again"
15282
+ });
15283
+ continue;
15284
+ }
15285
+ outcome.removed += 1;
15286
+ if (!SAFE_ID.test(run.id)) continue;
15287
+ const place = places.find((p) => p.root === run.project) ?? placeFor(run.project, places);
15288
+ const folder = runDirOf(place, run.id);
15289
+ if (!existsSync(folder)) continue;
15290
+ try {
15291
+ rmSync(folder, {
15292
+ recursive: true,
15293
+ force: true
15294
+ });
15295
+ outcome.folders += 1;
15296
+ } catch (error) {
15297
+ outcome.failed.push({
15298
+ run: run.id,
15299
+ reason: `its folder stays, at ${folder}: ${messageOf$1(error)}`
15300
+ });
15301
+ }
15302
+ }
15303
+ return outcome;
15304
+ };
14267
15305
  // ../core/src/store/decisions.ts
14268
15306
  const choicesOf = (json) => {
14269
15307
  const parsed = JSON.parse(json);
@@ -14284,7 +15322,7 @@ const noteOf = (note) => {
14284
15322
  return trimmed === "" ? null : maskSecrets(trimmed);
14285
15323
  };
14286
15324
  const kindOf = (value) => value === "permission" ? "permission" : "question";
14287
- const statusOf$1 = (value) => value === "answered" || value === "withdrawn" ? value : "open";
15325
+ const statusOf = (value) => value === "answered" || value === "withdrawn" ? value : "open";
14288
15326
  const decisionOf = (row) => {
14289
15327
  const record = {
14290
15328
  id: text(row, "id"),
@@ -14294,7 +15332,7 @@ const decisionOf = (row) => {
14294
15332
  question: text(row, "question"),
14295
15333
  options: choicesOf(text(row, "options") || "[]"),
14296
15334
  openedAt: text(row, "opened_at"),
14297
- status: statusOf$1(text(row, "status"))
15335
+ status: statusOf(text(row, "status"))
14298
15336
  };
14299
15337
  const optional = {
14300
15338
  recommended: text(row, "recommended"),
@@ -14391,7 +15429,7 @@ const decisionQueries = (db) => {
14391
15429
  };
14392
15430
  };
14393
15431
  // ../core/src/store/feedback.ts
14394
- const STATUSES$1 = [
15432
+ const STATUSES = [
14395
15433
  "new",
14396
15434
  "triaged",
14397
15435
  "accepted",
@@ -14419,7 +15457,7 @@ const ORIGIN_COLUMNS = [
14419
15457
  ["driver", "driver"],
14420
15458
  ["version", "version"]
14421
15459
  ];
14422
- const recordOf$3 = (row) => {
15460
+ const recordOf$2 = (row) => {
14423
15461
  const origin = {};
14424
15462
  for (const [key, column] of ORIGIN_COLUMNS) {
14425
15463
  const value = key === "driver" ? currentDriver(text(row, column)) : text(row, column);
@@ -14455,7 +15493,7 @@ const itemOf = (row) => {
14455
15493
  return {
14456
15494
  id,
14457
15495
  merged: strings(row, "fingerprints").filter((f) => f !== id),
14458
- status: oneOf(STATUSES$1, text(row, "status"), "new"),
15496
+ status: oneOf(STATUSES, text(row, "status"), "new"),
14459
15497
  title: text(row, "title"),
14460
15498
  target: oneOf(FEEDBACK_TARGETS, text(row, "target"), "workflow"),
14461
15499
  kind: oneOf(FEEDBACK_KINDS, text(row, "kind"), "idea"),
@@ -14520,7 +15558,7 @@ const feedbackQueries = (db) => {
14520
15558
  insertFeedback: insertRecord,
14521
15559
  feedbackItems: (filter = {}) => (filter.all === true ? every : shown).all().map((r) => itemOf(r)),
14522
15560
  findFeedback: (prefix) => byPrefix.all(prefix.length, prefix).map((r) => itemOf(r)),
14523
- feedbackRecords: (fingerprint) => records.all(fingerprint, fingerprint).map((r) => recordOf$3(r)),
15561
+ feedbackRecords: (fingerprint) => records.all(fingerprint, fingerprint).map((r) => recordOf$2(r)),
14524
15562
  setFeedbackStatus: (fingerprint, status) => Number(setStatus.run(status, (/* @__PURE__ */ new Date()).toISOString(), fingerprint).changes) > 0,
14525
15563
  mergeFeedback: (into, from) => {
14526
15564
  const root = text(rootOf.get(into), "root");
@@ -14529,7 +15567,7 @@ const feedbackQueries = (db) => {
14529
15567
  return inTransaction(db, () => from.filter((fingerprint) => fingerprint !== root).reduce((moved, fingerprint) => moved + Number(mergeInto.run(root, at, fingerprint, fingerprint, root).changes), 0));
14530
15568
  },
14531
15569
  splitFeedback: (fingerprint) => Number(split.run((/* @__PURE__ */ new Date()).toISOString(), fingerprint).changes),
14532
- exportFeedback: (filter = {}) => exported.all(filter.all === true ? 1 : 0).map((r) => recordOf$3(r)),
15570
+ exportFeedback: (filter = {}) => exported.all(filter.all === true ? 1 : 0).map((r) => recordOf$2(r)),
14533
15571
  importFeedback: (record) => {
14534
15572
  if (known.get(record.runId, record.seq) !== void 0) return false;
14535
15573
  insertRecord(record.runId, record.seq, record.at, record.node, record.feedback);
@@ -14618,7 +15656,7 @@ const planLimitQueries = (db) => {
14618
15656
  * `~/.ork/projects/<id>/` when attached), and moving it from one to the other
14619
15657
  * (`ork migrate`) with its runs' workflow files.
14620
15658
  */
14621
- const recordOf$2 = (row) => ({
15659
+ const recordOf$1 = (row) => ({
14622
15660
  id: text(row, "id"),
14623
15661
  root: text(row, "root"),
14624
15662
  mode: PROJECT_MODES.find((m) => m === text(row, "mode")) ?? "in-repo",
@@ -14639,7 +15677,7 @@ const projectQueries = (db) => {
14639
15677
  const refile = db.prepare(`UPDATE runs SET file = ? || substr(file, ?)
14640
15678
  WHERE project = ? AND substr(file, 1, ?) = ?`);
14641
15679
  return {
14642
- projects: () => all.all().map((r) => recordOf$2(r)),
15680
+ projects: () => all.all().map((r) => recordOf$1(r)),
14643
15681
  addProject: (project) => {
14644
15682
  const record = {
14645
15683
  ...project,
@@ -14655,7 +15693,7 @@ const projectQueries = (db) => {
14655
15693
  return Number(remove.run(id).changes) > 0;
14656
15694
  },
14657
15695
  moveProject: (id, place) => {
14658
- const before = all.all().map((r) => recordOf$2(r)).find((p) => p.id === id);
15696
+ const before = all.all().map((r) => recordOf$1(r)).find((p) => p.id === id);
14659
15697
  if (before === void 0) return false;
14660
15698
  const from = `${before.dir}${path.sep}`;
14661
15699
  const root = place.root ?? before.root;
@@ -14751,7 +15789,7 @@ const toolCpuQueries = (db) => ({
14751
15789
  }
14752
15790
  });
14753
15791
  // ../core/src/store/triggers.ts
14754
- const recordOf$1 = (row) => {
15792
+ const recordOf = (row) => {
14755
15793
  const acceptedHash = text(row, "accepted_hash");
14756
15794
  const lookedAt = text(row, "looked_at");
14757
15795
  const error = text(row, "error");
@@ -14793,7 +15831,7 @@ const triggerQueries = (db) => {
14793
15831
  ON CONFLICT (id) DO UPDATE SET pid = excluded.pid, at = excluded.at`);
14794
15832
  const letGo = db.prepare("DELETE FROM watch_lock WHERE pid = ?");
14795
15833
  return {
14796
- triggerRecords: (project) => (project === void 0 ? all.all() : ofProject.all(project)).map((row) => recordOf$1(row)),
15834
+ triggerRecords: (project) => (project === void 0 ? all.all() : ofProject.all(project)).map((row) => recordOf(row)),
14797
15835
  acceptTrigger: (project, trigger, hash) => {
14798
15836
  accept.run(project, trigger, hash);
14799
15837
  },
@@ -15091,40 +16129,6 @@ const migrate = (db, migrations = MIGRATIONS) => {
15091
16129
  */
15092
16130
  /** `~/.ork/state.db`; `home` is `~` (tests pass a folder). */
15093
16131
  const storePath = (home = homedir()) => path.join(home, ".ork", "state.db");
15094
- const STATUSES = [
15095
- "running",
15096
- "succeeded",
15097
- "failed",
15098
- "budget_exhausted",
15099
- "quota_exhausted",
15100
- "timed_out",
15101
- "interrupted"
15102
- ];
15103
- const statusOf = (value) => STATUSES.find((s) => s === value) ?? "failed";
15104
- const recordOf = (row) => {
15105
- const record = {
15106
- id: text(row, "id"),
15107
- workflow: text(row, "workflow"),
15108
- file: text(row, "file"),
15109
- project: text(row, "project"),
15110
- status: statusOf(text(row, "status")),
15111
- pid: Number(row?.["pid"]),
15112
- startedAt: text(row, "started_at"),
15113
- costUsd: Number(row?.["cost_usd"]),
15114
- depth: Number(row?.["depth"] ?? 0)
15115
- };
15116
- const processStart = text(row, "process_start");
15117
- if (processStart !== "") record.processStart = processStart;
15118
- const parent = text(row, "parent");
15119
- if (parent !== "") record.parent = parent;
15120
- const finished = text(row, "finished_at");
15121
- if (finished !== "") record.finishedAt = finished;
15122
- const skipped = row?.["skipped"];
15123
- if (typeof skipped === "number") record.skipped = skipped;
15124
- const handled = row?.["failures_handled"];
15125
- if (typeof handled === "number") record.failuresHandled = handled;
15126
- return record;
15127
- };
15128
16132
  /** The node an event is about, for filtering. */
15129
16133
  const nodeOf = (event) => {
15130
16134
  if ("id" in event && typeof event.id === "string") return event.id;
@@ -15177,10 +16181,10 @@ const openStore = (file = storePath(), options = {}) => {
15177
16181
  const finishRun = db.prepare("UPDATE runs SET status = ?, finished_at = ?, cost_usd = ?, skipped = ?, failures_handled = ? WHERE id = ?");
15178
16182
  const reopenRun = db.prepare("UPDATE runs SET status = 'running', pid = ?, process_start = ?, finished_at = NULL, skipped = NULL, failures_handled = NULL WHERE id = ?");
15179
16183
  const addCost = db.prepare("UPDATE runs SET cost_usd = cost_usd + ? WHERE id = ?");
15180
- const byId = db.prepare("SELECT * FROM runs WHERE id = ?");
15181
- const byPrefix = db.prepare("SELECT * FROM runs WHERE substr(id, 1, ?) = ? ORDER BY started_at DESC, rowid DESC");
15182
- const allRuns = db.prepare("SELECT * FROM runs ORDER BY started_at DESC, rowid DESC LIMIT ?");
15183
- const projectRuns = db.prepare("SELECT * FROM runs WHERE project = ? ORDER BY started_at DESC, rowid DESC LIMIT ?");
16184
+ const byId = db.prepare(`SELECT ${RUN_COLUMNS} FROM runs WHERE id = ?`);
16185
+ const byPrefix = db.prepare(`SELECT ${RUN_COLUMNS} FROM runs WHERE substr(id, 1, ?) = ? ORDER BY started_at DESC, rowid DESC`);
16186
+ const allRuns = db.prepare(`SELECT ${RUN_COLUMNS} FROM runs ORDER BY started_at DESC, rowid DESC LIMIT ?`);
16187
+ const projectRuns = db.prepare(`SELECT ${RUN_COLUMNS} FROM runs WHERE project = ? ORDER BY started_at DESC, rowid DESC LIMIT ?`);
15184
16188
  const eventsAfter = db.prepare("SELECT seq, at, data FROM events WHERE run_id = ? AND seq > ? ORDER BY seq");
15185
16189
  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");
15186
16190
  const runNodes = db.prepare("SELECT node FROM events WHERE run_id = ? AND node IS NOT NULL GROUP BY node ORDER BY MIN(seq)");
@@ -15227,12 +16231,12 @@ const openStore = (file = storePath(), options = {}) => {
15227
16231
  }),
15228
16232
  runs: (filter = {}) => {
15229
16233
  const limit = filter.limit ?? -1;
15230
- return (filter.project === void 0 ? allRuns.all(limit) : projectRuns.all(filter.project, limit)).map((r) => recordOf(r));
16234
+ return (filter.project === void 0 ? allRuns.all(limit) : projectRuns.all(filter.project, limit)).map((r) => recordOf$3(r));
15231
16235
  },
15232
16236
  lookup: (prefix) => {
15233
16237
  const exact = byId.get(prefix);
15234
- if (exact !== void 0) return [recordOf(exact)];
15235
- return byPrefix.all(prefix.length, prefix).map((r) => recordOf(r));
16238
+ if (exact !== void 0) return [recordOf$3(exact)];
16239
+ return byPrefix.all(prefix.length, prefix).map((r) => recordOf$3(r));
15236
16240
  },
15237
16241
  events: (runId, after = 0) => eventsAfter.all(runId, after).map((r) => ({
15238
16242
  runId,
@@ -15306,6 +16310,8 @@ const runActivity = (stored) => {
15306
16310
  if (event.event.type === "tool.called") of(event.id).now = toolLine(event.event);
15307
16311
  } else if (event.type === "slot.waiting") of(event.id).now = oneLine(slotWaitLine(event.holders));
15308
16312
  else if (event.type === "slot.taken") delete of(event.id).now;
16313
+ else if (event.type === "lock.waiting") of(event.id).now = oneLine(lockWaitLine(event.lock, event.holders));
16314
+ else if (event.type === "lock.taken") delete of(event.id).now;
15309
16315
  else if (event.type === "decision.opened") of(event.node).now = oneLine(`Asked: ${event.question}`);
15310
16316
  else if (event.type === "decision.answered") of(event.node).now = oneLine(`Answered: ${event.answer}`);
15311
16317
  else if (event.type === "decision.withdrawn") delete of(event.node).now;
@@ -16017,6 +17023,238 @@ const capsHere = (home, place) => {
16017
17023
  };
16018
17024
  }
16019
17025
  };
17026
+ // ../core/src/usage/report.ts
17027
+ const USAGE_DIMENSIONS = [
17028
+ "day",
17029
+ "week",
17030
+ "month",
17031
+ "workflow",
17032
+ "node",
17033
+ "model",
17034
+ "driver",
17035
+ "project"
17036
+ ];
17037
+ const pad = (value) => String(value).padStart(2, "0");
17038
+ /** `2026-11-03`, in the machine's own time zone. */
17039
+ const dayOf = (date) => `${String(date.getFullYear())}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
17040
+ /** The Monday of a time's week. */
17041
+ const weekOf = (date) => {
17042
+ const monday = new Date(date);
17043
+ monday.setDate(date.getDate() - (date.getDay() + 6) % 7);
17044
+ return dayOf(monday);
17045
+ };
17046
+ const KEYS = {
17047
+ day: (row) => dayOf(new Date(row.at)),
17048
+ week: (row) => weekOf(new Date(row.at)),
17049
+ month: (row) => dayOf(new Date(row.at)).slice(0, 7),
17050
+ workflow: (row) => row.workflow,
17051
+ node: (row) => `${row.workflow}/${row.template ?? row.node}`,
17052
+ model: (row) => row.model ?? "unknown",
17053
+ driver: (row) => row.driver ?? "unknown",
17054
+ project: (row) => row.project
17055
+ };
17056
+ const TIMES = /* @__PURE__ */ new Set([
17057
+ "day",
17058
+ "week",
17059
+ "month"
17060
+ ]);
17061
+ /** Dimensions a tool command has no value in. */
17062
+ const SESSION_ONLY = /* @__PURE__ */ new Set(["model", "driver"]);
17063
+ const totalsOf = (rows, cpu) => {
17064
+ const totals = {
17065
+ costUsd: 0,
17066
+ tokens: 0,
17067
+ partial: false,
17068
+ inputTokens: 0,
17069
+ outputTokens: 0,
17070
+ cacheReadTokens: 0,
17071
+ cacheWriteTokens: 0,
17072
+ turns: 0,
17073
+ durationMs: 0,
17074
+ sessions: rows.length,
17075
+ runs: new Set(rows.map((row) => row.runId)).size,
17076
+ failedUsd: 0,
17077
+ reworkUsd: 0,
17078
+ chatUsd: 0,
17079
+ cacheHitRate: 0,
17080
+ cpuMs: cpu.reduce((sum, row) => sum + row.userMs + row.systemMs, 0)
17081
+ };
17082
+ for (const row of rows) {
17083
+ totals.costUsd += row.costUsd;
17084
+ totals.tokens += countedTokens(row);
17085
+ totals.partial ||= unpriced(row.driver);
17086
+ totals.inputTokens += row.inputTokens;
17087
+ totals.outputTokens += row.outputTokens;
17088
+ totals.cacheReadTokens += row.cacheReadTokens;
17089
+ totals.cacheWriteTokens += row.cacheWriteTokens;
17090
+ totals.turns += row.turns;
17091
+ totals.durationMs += row.durationMs;
17092
+ if (row.failed) totals.failedUsd += row.costUsd;
17093
+ if (row.kind === "chat") totals.chatUsd += row.costUsd;
17094
+ else if (row.round > 1) totals.reworkUsd += row.costUsd;
17095
+ }
17096
+ const read = totals.inputTokens + totals.cacheReadTokens + totals.cacheWriteTokens;
17097
+ totals.cacheHitRate = read === 0 ? 0 : totals.cacheReadTokens / read;
17098
+ return totals;
17099
+ };
17100
+ const groupBy = (rows, cpu, dimension) => {
17101
+ const buckets = /* @__PURE__ */ new Map();
17102
+ const bucketOf = (row) => {
17103
+ const key = KEYS[dimension](row);
17104
+ const found = buckets.get(key);
17105
+ if (found !== void 0) return found;
17106
+ const fresh = {
17107
+ rows: [],
17108
+ cpu: []
17109
+ };
17110
+ buckets.set(key, fresh);
17111
+ return fresh;
17112
+ };
17113
+ for (const row of rows) bucketOf(row).rows.push(row);
17114
+ if (!SESSION_ONLY.has(dimension)) for (const row of cpu) bucketOf(row).cpu.push(row);
17115
+ const groups = [...buckets].map(([key, bucket]) => Object.assign(totalsOf(bucket.rows, bucket.cpu), { key }));
17116
+ return TIMES.has(dimension) ? groups.toSorted((a, b) => a.key.localeCompare(b.key)) : groups.toSorted((a, b) => b.costUsd - a.costUsd);
17117
+ };
17118
+ /** Below this, a percentage says nothing worth acting on. */
17119
+ const MIN_SPEND_USD = .5;
17120
+ const REWORK_SHARE = .2;
17121
+ const FAILED_SHARE = .1;
17122
+ const CONCENTRATED_SHARE = .5;
17123
+ const LOW_CACHE_RATE = .5;
17124
+ const MIN_CACHE_TOKENS = 1e5;
17125
+ const percent = (share) => `${String(Math.round(share * 100))}%`;
17126
+ const hintsOf = (totals, nodes) => {
17127
+ const spend = totals.costUsd;
17128
+ if (spend < MIN_SPEND_USD) return [];
17129
+ const hints = [];
17130
+ if (totals.reworkUsd / spend >= REWORK_SHARE) hints.push({
17131
+ code: "rework",
17132
+ message: `${percent(totals.reworkUsd / spend)} of the spend went to loop rounds after the first. Sharper briefs or an earlier check can stop work coming back.`
17133
+ });
17134
+ if (totals.failedUsd / spend >= FAILED_SHARE) hints.push({
17135
+ code: "failed",
17136
+ message: `${percent(totals.failedUsd / spend)} of the spend went to sessions that failed. A cheap check before them, or a lower budget per session, can stop them sooner.`
17137
+ });
17138
+ const top = nodes[0];
17139
+ if (top !== void 0 && nodes.length > 1 && top.costUsd / spend >= CONCENTRATED_SHARE) hints.push({
17140
+ code: "concentrated",
17141
+ message: `${top.key} took ${percent(top.costUsd / spend)} of the spend. It is the first place to try a smaller model, a lower effort or a tighter brief.`
17142
+ });
17143
+ if (totals.inputTokens + totals.cacheReadTokens + totals.cacheWriteTokens >= MIN_CACHE_TOKENS && totals.cacheHitRate < LOW_CACHE_RATE) hints.push({
17144
+ code: "cache",
17145
+ message: `Only ${percent(totals.cacheHitRate)} of input tokens came from the prompt cache. Long runs of fresh sessions cost more than resumed ones.`
17146
+ });
17147
+ return hints;
17148
+ };
17149
+ /** Everything a report shows, from the ledger's rows and tool commands' CPU. */
17150
+ const usageReport = (rows, cpu = []) => {
17151
+ const totals = totalsOf(rows, cpu);
17152
+ const by = (dimension) => groupBy(rows, cpu, dimension);
17153
+ const groups = {
17154
+ day: by("day"),
17155
+ week: by("week"),
17156
+ month: by("month"),
17157
+ workflow: by("workflow"),
17158
+ node: by("node"),
17159
+ model: by("model"),
17160
+ driver: by("driver"),
17161
+ project: by("project")
17162
+ };
17163
+ return {
17164
+ totals,
17165
+ groups,
17166
+ hints: hintsOf(totals, groups.node)
17167
+ };
17168
+ };
17169
+ // ../core/src/worktree/clean.ts
17170
+ /**
17171
+ * @file What `ork worktrees clean` and `ork worktrees remove` do with what
17172
+ * list.ts reads: the plan (what goes, what's kept and why), by one rule for
17173
+ * every caller, and carrying it out. The CLI and the Worktrees panel both
17174
+ * plan here, so neither removes what the other would keep.
17175
+ */
17176
+ /** What names an item for `remove`: its branch, or its worktree's path. */
17177
+ const keyOf = (item) => item.branch ?? item.path ?? `${item.runId}/${item.nodeId}`;
17178
+ /** The reasons no `force` overrides: removing would break something. */
17179
+ const MAIN_CHECKOUT = "checked out in the main checkout";
17180
+ const RUN_GOING = "its run is still going";
17181
+ const NOT_ORKS = "not created by Ork";
17182
+ const kept = (item, skip) => ({
17183
+ item,
17184
+ removeWorktree: false,
17185
+ deleteBranch: false,
17186
+ skip
17187
+ });
17188
+ /** What clean does with one item. */
17189
+ const cleanStep = (item, options) => {
17190
+ if (!item.recorded) return kept(item, NOT_ORKS);
17191
+ if (item.inMainCheckout) return kept(item, MAIN_CHECKOUT);
17192
+ if (item.heldBy !== void 0) return kept(item, `checked out in ${item.heldBy}`);
17193
+ if (options.live?.has(item.runId) === true) return kept(item, RUN_GOING);
17194
+ if (item.dirty && !options.force) return kept(item, "uncommitted changes");
17195
+ if (item.branch === void 0 && item.checkedOut === void 0 && item.ahead > 0 && !options.force) return kept(item, "commits on no branch");
17196
+ const removable = item.path !== null && !item.missing;
17197
+ if (options.branches && item.branch !== void 0 && !item.merged && !options.force) return {
17198
+ item,
17199
+ removeWorktree: removable,
17200
+ deleteBranch: false,
17201
+ skip: "branch not merged"
17202
+ };
17203
+ return {
17204
+ item,
17205
+ removeWorktree: removable,
17206
+ deleteBranch: options.branches && item.branch !== void 0
17207
+ };
17208
+ };
17209
+ /** Whether a step drops a worktree whose folder is gone, by its own path. */
17210
+ const stepPrunes = (step) => step.item.missing && step.item.path !== null && step.skip === void 0;
17211
+ /** Whether a step does anything. */
17212
+ const stepActs = (step) => step.removeWorktree || step.deleteBranch || stepPrunes(step);
17213
+ /**
17214
+ * What `ork worktrees clean` would do. A worktree with uncommitted changes, an
17215
+ * unmerged branch, or a detached worktree holding commits no branch has, is
17216
+ * kept unless `force`; one checked out in the main checkout or in a worktree
17217
+ * Ork didn't create, whose run is still going, or an `ork/` branch no run
17218
+ * recorded creating, is kept even then.
17219
+ */
17220
+ const planClean = (items, filter, options) => items.filter((item) => filter.runId === void 0 || item.runId === filter.runId).filter((item) => filter.key === void 0 || keyOf(item) === filter.key).filter((item) => !filter.merged || item.merged).map((item) => cleanStep(item, options));
17221
+ /**
17222
+ * Carries out one step; what it did goes into `done` as it goes. It touches
17223
+ * only its own item: a missing worktree is dropped by its own path (`git
17224
+ * worktree remove`), never by `git worktree prune`, which would drop every
17225
+ * missing worktree in the repository, the user's own included.
17226
+ */
17227
+ const applyStep = async (root, step, force, done) => {
17228
+ const { item } = step;
17229
+ if (stepPrunes(step) && !step.removeWorktree && item.path !== null) {
17230
+ await gitOk(root, [
17231
+ "worktree",
17232
+ "remove",
17233
+ item.path
17234
+ ]);
17235
+ done.push(`pruned ${item.path}`);
17236
+ }
17237
+ if (step.removeWorktree && item.path !== null) {
17238
+ await removeWorktree(root, item.path, force);
17239
+ done.push(`removed ${item.path}`);
17240
+ }
17241
+ if (step.deleteBranch && item.branch !== void 0) {
17242
+ await gitOk(root, [
17243
+ "branch",
17244
+ force ? "-D" : "-d",
17245
+ item.branch
17246
+ ]);
17247
+ done.push(`deleted ${item.branch}`);
17248
+ }
17249
+ };
17250
+ /**
17251
+ * Carries out a clean plan, step by step. What it did goes into `done` as it
17252
+ * goes, so a caller still has it when git fails halfway.
17253
+ */
17254
+ const applyClean = async (root, steps, force, done = []) => {
17255
+ for (const step of steps) await applyStep(root, step, force, done);
17256
+ return done;
17257
+ };
16020
17258
  // ../core/src/worktree/names.ts
16021
17259
  /**
16022
17260
  * @file The names Ork gives a node's worktree and branch, from the project's
@@ -16147,6 +17385,203 @@ const syncFrom = async (worktree, from) => {
16147
17385
  if (!applied.ok) throw new Error(`could not take ${from}'s changes: ${applied.error}`);
16148
17386
  return snapshot(worktree);
16149
17387
  };
17388
+ // ../core/src/worktree/words.ts
17389
+ /**
17390
+ * @file The plain words for what `ork worktrees` shows, in one place, so the
17391
+ * CLI and the Worktrees panel say the same thing: an item's name, its merge
17392
+ * state, its state as parts (each marked when it's work that removing would
17393
+ * lose), a plan's line, and why an item can't be removed. The CLI colors
17394
+ * them; the panel lays them out.
17395
+ */
17396
+ /** An item's name: its branch, or its run and node when created detached. */
17397
+ const labelOf = (item) => item.branch ?? `${item.runId}/${item.nodeId}`;
17398
+ /** Merged into the current branch, not, or detached on no branch at all. */
17399
+ const mergeStateOf = (item) => {
17400
+ if (item.branch === void 0 && item.checkedOut === void 0) return "detached";
17401
+ return item.merged ? "merged" : "unmerged";
17402
+ };
17403
+ /** `3 commits`, `1 commit`. */
17404
+ const counted = (count, noun) => `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
17405
+ /**
17406
+ * An item's state, in order: where its worktree is when it isn't plain, the
17407
+ * workflow's own branch, uncommitted changes, unmerged or unreachable commits,
17408
+ * and the main checkout or another worktree holding the branch. `into` names
17409
+ * the branch "not merged" is measured against, when the words should say it.
17410
+ */
17411
+ const stateParts = (item, into) => {
17412
+ const parts = [];
17413
+ const part = (text, risk = false) => {
17414
+ parts.push({
17415
+ text,
17416
+ risk
17417
+ });
17418
+ };
17419
+ if (item.path === null) part("no worktree");
17420
+ else if (item.missing) part("missing (prunable)");
17421
+ if (item.checkedOut !== void 0) part(`on the workflow's branch ${item.checkedOut}`);
17422
+ if (item.dirty) part("uncommitted changes", true);
17423
+ const state = mergeStateOf(item);
17424
+ if (state === "unmerged") {
17425
+ const where = into === void 0 ? "" : ` into ${into}`;
17426
+ part(`${counted(item.ahead, "commit")} not merged${where}`);
17427
+ }
17428
+ if (state === "detached") {
17429
+ if (item.ahead > 0) part(`${counted(item.ahead, "commit")} on no branch`, true);
17430
+ else if (item.path !== null && !item.missing) part("nothing committed");
17431
+ }
17432
+ if (!item.recorded) part("not created by Ork");
17433
+ if (item.inMainCheckout) part("checked out in the main checkout");
17434
+ if (item.heldBy !== void 0) part(`checked out in ${item.heldBy}`);
17435
+ return parts;
17436
+ };
17437
+ /** What a step does, as a plan says it. */
17438
+ const planWhat = (step) => {
17439
+ const actions = [
17440
+ ...step.removeWorktree ? ["remove worktree"] : [],
17441
+ ...stepPrunes(step) && !step.removeWorktree ? ["prune worktree"] : [],
17442
+ ...step.deleteBranch ? ["delete branch"] : []
17443
+ ];
17444
+ return actions.length > 0 ? actions.join(" + ") : "keep";
17445
+ };
17446
+ /** `ork/r1/lead: remove worktree`, `r1/check (detached): keep (…)`. */
17447
+ const planLine = (step) => {
17448
+ const { item } = step;
17449
+ return {
17450
+ text: `${item.branch === void 0 && item.checkedOut === void 0 ? `${labelOf(item)} (detached)` : labelOf(item)}: ${planWhat(step)}`,
17451
+ ...step.skip === void 0 ? {} : { reason: step.skip }
17452
+ };
17453
+ };
17454
+ /**
17455
+ * Why an item can't be removed at all, even forced, if it can't: no run
17456
+ * recorded creating it, the main checkout or a worktree Ork didn't create holds
17457
+ * it, or its run is still going.
17458
+ */
17459
+ const removeRefusal = (item, live) => {
17460
+ if (!item.recorded) return "not created by Ork (no run recorded it); delete it with git if it's yours to delete";
17461
+ if (item.inMainCheckout) return "checked out in the main checkout; switch it to another branch first";
17462
+ if (item.heldBy !== void 0) return `checked out in ${item.heldBy}, a worktree Ork didn't create; switch it to another branch first`;
17463
+ return live ? `run ${item.runId} is still going; stop it first` : void 0;
17464
+ };
17465
+ // ../core/src/worktree/remove.ts
17466
+ /** The ids of the project's runs whose process is still going. */
17467
+ const liveRunIds = (store, root) => new Set(store.runs({ project: root }).filter((run) => liveStatus(run) === "running").map((run) => run.id));
17468
+ /**
17469
+ * Whether `root` is a repository with a commit, and the branch checked out
17470
+ * there, which "merged" means merged into (null when its HEAD is detached).
17471
+ */
17472
+ const projectHead = async (root) => {
17473
+ if ((await git(root, [
17474
+ "rev-parse",
17475
+ "--verify",
17476
+ "--quiet",
17477
+ "HEAD"
17478
+ ])).code !== 0) return { ok: false };
17479
+ const answer = await git(root, [
17480
+ "symbolic-ref",
17481
+ "--short",
17482
+ "-q",
17483
+ "HEAD"
17484
+ ]);
17485
+ return {
17486
+ ok: true,
17487
+ branch: answer.code === 0 ? answer.stdout.trim() : null
17488
+ };
17489
+ };
17490
+ /** The top of the repository `cwd` is in, even one with no commit yet. */
17491
+ const repoRoot = async (cwd) => {
17492
+ const top = await git(cwd, ["rev-parse", "--show-toplevel"]);
17493
+ return top.code === 0 ? top.stdout.trim() : null;
17494
+ };
17495
+ /** What Ork created in the repo at `root`, from its runs' records and git. */
17496
+ const readWorktrees = async (store, root) => ({
17497
+ items: await listOrkWorktrees(root, createdWorktrees(store, root)),
17498
+ live: liveRunIds(store, root)
17499
+ });
17500
+ /**
17501
+ * The plan to remove one item, named by `keyOf`. A branch with no worktree is
17502
+ * removed by deleting it, so `deleteBranch` is implied there.
17503
+ */
17504
+ const planRemove = ({ items, live }, key, deleteBranch) => {
17505
+ const item = items.find((i) => keyOf(i) === key);
17506
+ if (item === void 0) return {
17507
+ ok: false,
17508
+ error: `No Ork worktree or branch ${key}.`,
17509
+ gone: true
17510
+ };
17511
+ const refusal = removeRefusal(item, live.has(item.runId));
17512
+ if (refusal !== void 0) return {
17513
+ ok: false,
17514
+ error: `Can't remove ${labelOf(item)}: ${refusal}.`,
17515
+ gone: false
17516
+ };
17517
+ const branches = item.branch !== void 0 && (deleteBranch || item.path === null);
17518
+ const careful = cleanStep(item, {
17519
+ branches,
17520
+ force: false,
17521
+ live
17522
+ });
17523
+ if (careful.skip === void 0) return {
17524
+ ok: true,
17525
+ plan: {
17526
+ step: careful,
17527
+ force: false
17528
+ }
17529
+ };
17530
+ return {
17531
+ ok: true,
17532
+ plan: {
17533
+ step: cleanStep(item, {
17534
+ branches,
17535
+ force: true,
17536
+ live
17537
+ }),
17538
+ force: true
17539
+ }
17540
+ };
17541
+ };
17542
+ const out = async (cwd, args) => lines((await git(cwd, args)).stdout);
17543
+ /** What carrying out `plan` would lose, read from git. */
17544
+ const lostWork = async (root, { step }) => {
17545
+ const { item } = step;
17546
+ const inside = item.path !== null && !item.missing ? item.path : null;
17547
+ const changes = inside !== null && item.dirty && step.removeWorktree ? await out(inside, ["status", "--porcelain"]) : [];
17548
+ if (step.deleteBranch && item.branch !== void 0 && !item.merged) return {
17549
+ changes,
17550
+ commits: await out(root, [
17551
+ "log",
17552
+ "--oneline",
17553
+ `HEAD..${item.branch}`
17554
+ ]),
17555
+ commitsOn: "unmerged"
17556
+ };
17557
+ if (item.branch === void 0 && item.checkedOut === void 0 && inside !== null && item.ahead > 0 && step.removeWorktree) return {
17558
+ changes,
17559
+ commits: await out(inside, [
17560
+ "log",
17561
+ "--oneline",
17562
+ "HEAD",
17563
+ "--not",
17564
+ "--branches",
17565
+ "--tags",
17566
+ "--remotes"
17567
+ ]),
17568
+ commitsOn: "no branch"
17569
+ };
17570
+ return {
17571
+ changes,
17572
+ commits: [],
17573
+ commitsOn: null
17574
+ };
17575
+ };
17576
+ /**
17577
+ * Carries out a remove plan; what it did goes into `done` as it goes. The
17578
+ * caller has checked that a plan needing `force` was asked to force. It touches
17579
+ * only its own item, as each of clean's steps does.
17580
+ */
17581
+ const applyRemove = async (root, { step, force }, done = []) => {
17582
+ await applyStep(root, step, force, done);
17583
+ return done;
17584
+ };
16150
17585
  // ../core/src/worktree/resumed.ts
16151
17586
  /**
16152
17587
  * @file What a resumed run's workspace starts from: the worktrees the run left
@@ -16619,4 +18054,4 @@ const createWorkspace = (options) => {
16619
18054
  close
16620
18055
  };
16621
18056
  };
16622
- export { followEvents as $, plainPathInside as $n, serversIn as $t, resolveInputs as A, applySteps as An, readyDriver as Ar, endedWith as At, tellNode as B, inRepoPlace as Bn, lines as Bt, isNodeEvent as C, socketPath as Cn, retiredDriverHint as Cr, processAlive as Ct, inspectProject as D, maskReplacer as Dn, integrationFor as Dr, scriptContactsOf as Dt, BRANCH_PREFIX as E, MASK as En, TOOL_PREFIX as Er, nestItems as Et, addressOf as F, createAgent as Fn, firstLine as Fr, slotWaitLine as Ft, requestStop as G, runDirOf as Gn, nodeSays as Gt, flattenTree as H, placeFiles as Hn, envReadsOf as Ht, endLink as I, deleteAgent as In, EXPERIMENTS as Ir, lowerChild as It, detachResume as J, inside as Jn, gitRulesOf as Jt, tellLiveRun as K, shownPath as Kn, declaredOutputOf as Kt, endLinksOf as L, catalogAgent as Ln, experimentsOn as Lr, startAt as Lt, sessionRefusals as M, linkTools as Mn, ORK_LAYOUT as Mr, stoppedEnd as Mt, driverQueues as N, toolLinks as Nn, AGENT_DRIVERS as Nr, calledWorkflows as Nt, removeWorktree as O, maskSecrets as On, PREFERRED_DRIVER as Or, runScope as Ot, createCalls as P, toolsInUse as Pn, DRIVER_IDS as Pr, slotClock as Pt, detachRun as Q, guardedBase as Qn, mcpNotText as Qt, linkNodes as R, loadAgents as Rn, knownExperiments as Rr, git as Rt, activeTree as S, serveRun as Sn, NodeIdSchema as Sr, templateOf as St, storePath as T, wakeRun as Tn, INTEGRATIONS as Tr, isItem as Tt, requestContinue as U, placeFor as Un, namingNoDriver as Ut, unlinkNodes as V, newProjectId as Vn, clearGitLocalEnv as Vt, requestPause as W, projectsHome as Wn, sessionDriver as Wt, listKey as X, LESSON_CAP_TOKENS as Xn, machineRulesOf as Xt, resumable as Y, projectRootOf as Yn, loadProject as Yt, DETACHED_ENV as Z, estimateTokens as Zn, MCP_FILE as Zt, declaredTriggers as _, targetOf as _n, settingsOf as _r, liveStatus as _t, planUpgrade as a, endFor as an, readDocument as ar, explainWorkflowText as at, takeWatchLock as b, renderTemplate as bn, isToolNode as br, runTree as bt, runningScript as c, FEEDBACK_TARGETS as cn, programsRunBy as cr, parseReport as ct, isNewer as d, runGh as dn, serializeWorkflow as dr, walkChecks as dt, createSignals as en, writePlainFile as er, NOT_STARTED as et, parseVersion as f, sendPost as fn, markValue as fr, walkSummary as ft, triggerStanding as g, normalizeTitle as gn, expandEdge as gr, heldTimeout as gt, createWatcher as h, fingerprint as hn, CheckReportSchema as hr, userMessageText as ht, capsHere as i, END_KINDS as in, newWorkflowText as ir, usd as it, driverRefusals as j, planMoveInto as jn, AGENT_TOOL_LAYOUTS as jr, failedWith as jt, parseInputPairs as k, planAgentMigration as kn, driverChecks as kr, declaredProblem as kt, detectInstaller as l, DISCUSSIONS as ln, renderJsonSchema as lr, reportSchemaFor as lt, triggersReport as m, createFeedback as mn, WorkflowSchema as mr, MESSAGE_LIMIT as mt, resumedOptions as n, outcomeEnd as nn, WorkflowEditSchema as nr, limitsReport as nt, releaseNotes as o, FEEDBACK_KINDS as on, renameNodeTypes as or, explainSession as ot, lateTriggers as p, publishPost as pn, EdgeInputSchema as pr, startRun as pt, adoptedCalls as q, workflowDirOf as qn, PROJECT_FILE as qt, resolveBase as r, END_DETAIL_LIMIT as rn, applyEdits as rr, planReason as rt, runningRunIds as s, FEEDBACK_SEVERITIES as sn, beforeOf as sr, planNodeSession as st, createWorkspace as t, endOf as tn, skillsIn as tr, limitsNow as tt, compareVersions as u, planPost as un, parseWorkflow as ur, checkWorkflowText as ut, readProjectTriggers as v, evaluate as vn, NodeSchema as vr, pausedOf as vt, openStore as w, socketRoom as wn, timeoutMs as wr, stillTheSameProcess as wt, watchingPid as x, readCondition as xn, DriverSchema as xr, slotWaitsOf as xt, workflowTextHash as y, parseTemplate as yn, isClaudeNode as yr, runStatus as yt, peerOf as z, hasInRepoFiles as zn, gitOk as zt };
18057
+ export { resolveInputs as $, maskSecrets as $n, PREFERRED_DRIVER as $r, countedTokens as $t, isNewer as A, END_KINDS as An, readDocument as Ar, usd as At, watchingPid as B, createFeedback as Bn, CheckReportSchema as Br, MESSAGE_LIMIT as Bt, capsHere as C, MCP_FILE as Cn, guardedBase as Cr, DETACHED_ENV as Ct, runningScript as D, endOf as Dn, WorkflowEditSchema as Dr, limitsNow as Dt, runningRunIds as E, createSignals as En, skillsIn as Er, NOT_STARTED as Et, triggerStanding as F, DISCUSSIONS as Fn, parseWorkflow as Fr, reportSchemaFor as Ft, applyPrune as G, parseTemplate as Gn, isToolNode as Gr, pausedOf as Gt, isNodeEvent as H, normalizeTitle as Hn, settingsOf as Hr, heldTimeout as Ht, declaredTriggers as I, planPost as In, serializeWorkflow as Ir, checkWorkflowText as It, listOrkWorktrees as J, serveRun as Jn, retiredDriverHint as Jr, templateOf as Jt, planPrune as K, renderTemplate as Kn, DriverSchema as Kr, runStatus as Kt, readProjectTriggers as L, runGh as Ln, markValue as Lr, walkChecks as Lt, lateTriggers as M, FEEDBACK_KINDS as Mn, beforeOf as Mr, explainSession as Mt, triggersReport as N, FEEDBACK_SEVERITIES as Nn, programsRunBy as Nr, planNodeSession as Nt, detectInstaller as O, outcomeEnd as On, applyEdits as Or, limitsReport as Ot, createWatcher as P, FEEDBACK_TARGETS as Pn, renderJsonSchema as Pr, parseReport as Pt, parseInputPairs as Q, maskReplacer as Qn, integrationFor as Qr, nestItems as Qt, workflowTextHash as R, sendPost as Rn, EdgeInputSchema as Rr, walkSummary as Rt, usageReport as S, machineRulesOf as Sn, estimateTokens as Sr, resumable as St, releaseNotes as T, serversIn as Tn, writePlainFile as Tr, followEvents as Tt, openStore as U, targetOf as Un, NodeSchema as Ur, liveStatus as Ut, activeTree as V, fingerprint as Vn, expandEdge as Vr, userMessageText as Vt, storePath as W, evaluate as Wn, isClaudeNode as Wr, lockWaitsOf as Wt, createReplay as X, socketRoom as Xn, INTEGRATIONS as Xr, stillTheSameProcess as Xt, inspectProject as Y, socketPath as Yn, timeoutMs as Yr, processAlive as Yt, replayLoopAnswers as Z, MASK as Zn, TOOL_PREFIX as Zr, isItem as Zt, keyOf as _, nodeSays as _n, shownPath as _r, requestPause as _t, planRemove as a, DRIVER_IDS as ai, stoppedEnd as an, toolsInUse as ar, scriptRefusal as at, stepPrunes as b, gitRulesOf as bn, projectRootOf as br, adoptedCalls as bt, repoRoot as c, experimentsOn as ci, slotWaitLine as cn, catalogAgent as cr, followRunTree as ct, planLine as d, git as dn, inRepoPlace as dr, createCalls as dt, driverChecks as ei, runScope as en, planAgentMigration as er, budgetWarnings as et, removeRefusal as f, gitOk as fn, newProjectId as fr, addressOf as ft, cleanStep as g, sessionDriver as gn, runDirOf as gr, requestContinue as gt, applyClean as h, namingNoDriver as hn, projectsHome as hr, unlinkNodes as ht, lostWork as i, AGENT_DRIVERS as ii, failedWith as in, toolLinks as ir, TOO_LONG as it, parseVersion as j, endFor as jn, renameNodeTypes as jr, explainWorkflowText as jt, compareVersions as k, END_DETAIL_LIMIT as kn, newWorkflowText as kr, planReason as kt, labelOf as l, knownExperiments as li, lowerChild as ln, loadAgents as lr, answerDecision as lt, resolveBase as m, envReadsOf as mn, placeFor as mr, linkNodes as mt, resumedOptions as n, AGENT_TOOL_LAYOUTS as ni, lockWaitLine as nn, planMoveInto as nr, sessionRefusals as nt, projectHead as o, firstLine as oi, declaredProblem as on, createAgent as or, sendFromScript as ot, stateParts as p, clearGitLocalEnv as pn, placeFiles as pr, endLinksOf as pt, createdWorktrees as q, readCondition as qn, NodeIdSchema as qr, slotWaitsOf as qt, applyRemove as r, ORK_LAYOUT as ri, endedWith as rn, linkTools as rr, driverQueues as rt, readWorktrees as s, EXPERIMENTS as si, slotClock as sn, deleteAgent as sr, createRelay as st, createWorkspace as t, readyDriver as ti, calledWorkflows as tn, applySteps as tr, driverRefusals as tt, mergeStateOf as u, startAt as un, hasInRepoFiles as ur, pendingDecisions as ut, planClean as v, declaredOutputOf as vn, workflowDirOf as vr, requestStop as vt, planUpgrade as w, mcpNotText as wn, plainPathInside as wr, detachRun as wt, USAGE_DIMENSIONS as x, loadProject as xn, LESSON_CAP_TOKENS as xr, detachResume as xt, stepActs as y, PROJECT_FILE as yn, inside as yr, tellLiveRun as yt, takeWatchLock as z, publishPost as zn, WorkflowSchema as zr, startRun as zt };