@krapht/ork 1.0.0-alpha.7 → 1.0.0-alpha.9

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 (33) hide show
  1. package/dist/{__build_info__-BGoaUwLd.mjs → __build_info__-y1TPvZvp.mjs} +2 -2
  2. package/dist/bin.mjs +598 -51
  3. package/dist/{clean-BPITM-6p.mjs → clean-BRUXyk87.mjs} +5 -2
  4. package/dist/{completion-CCn8xWmO.mjs → completion-cpZHtv50.mjs} +5 -5
  5. package/dist/{context-CZNM454F.mjs → context-Dbay8l7P.mjs} +8 -7
  6. package/dist/{decide-DsA6kOT7.mjs → decide-BphP3ONM.mjs} +1 -1
  7. package/dist/{find-ntyniv2E.mjs → find-BXZjliwr.mjs} +1 -1
  8. package/dist/format-DeT7vL-8.mjs +191 -0
  9. package/dist/{in-a-run-D_fnj1Mw.mjs → in-a-run-C4E9HfED.mjs} +1 -1
  10. package/dist/{limits-BKzVMwkt.mjs → limits-CBeoYrUH.mjs} +1 -1
  11. package/dist/{list-D9HPWLPH.mjs → list-BmxgNyd3.mjs} +11 -4
  12. package/dist/{mcp-BFl3cIDp.mjs → mcp-DTreb7Nq.mjs} +6 -6
  13. package/dist/{output-CwqlG1Le.mjs → output-BVkI6ZVv.mjs} +1 -1
  14. package/dist/{pause-DhgKQORW.mjs → pause-h66SC4rV.mjs} +4 -4
  15. package/dist/{relay-Cp8JGrdv.mjs → relay-BxRhxL6n.mjs} +2 -2
  16. package/dist/{remove-CSpT_nJH.mjs → remove-CYGd9Csj.mjs} +14 -7
  17. package/dist/{send-CHiYkM6X.mjs → send-BskOwN4p.mjs} +3 -3
  18. package/dist/{src-BsLHKQRc.mjs → src-BwFxTank.mjs} +335 -107
  19. package/dist/{src-CUeMUVwW.mjs → src-W1i5Bl6N.mjs} +23 -9
  20. package/dist/{src-DhGLtYR-.mjs → src-bOImrq43.mjs} +30 -11
  21. package/dist/{src-BwnitlJh.mjs → src-xd9DbQw8.mjs} +8 -8
  22. package/dist/ui/assets/{app-D4CoH-lh.js → app-BMg0OOv4.js} +19 -19
  23. package/dist/ui/assets/app-C8uhHVYp.css +1 -0
  24. package/dist/ui/assets/{index-DxDGdEwz.js → index-DY1q4SDl.js} +2 -2
  25. package/dist/ui/index.html +1 -1
  26. package/dist/{ui-BxE0L7V0.mjs → ui-B46Z3e9E.mjs} +4 -4
  27. package/dist/{usage-BuzDsEhq.mjs → usage-DpHXbQEu.mjs} +5 -5
  28. package/dist/{watch-CCj0vU3B.mjs → watch-CJO1ee6Y.mjs} +6 -6
  29. package/dist/{workflows-BNCDD6J-.mjs → workflows-BJdSHnkr.mjs} +4 -3
  30. package/dist/{worktrees-DBBcaimX.mjs → worktrees-Ccf2lUhE.mjs} +72 -19
  31. package/package.json +1 -1
  32. package/dist/format-DaROJUgG.mjs +0 -662
  33. package/dist/ui/assets/app-Dt_tjFj6.css +0 -1
@@ -608,7 +608,10 @@ const agentSettings = {
608
608
  practices: PracticesSchema.optional(),
609
609
  tools: ToolRulesSchema.optional(),
610
610
  session: z.enum(["fresh", "resume"]).optional().describe("When the node runs again (a loop, another delegation): resume its session, or start a fresh one with the new input. Default: resume for a leader, fresh for a worker."),
611
- output: z.strictObject({ schema: z.record(z.string(), z.unknown()).optional().describe("Extra structured fields the node must report, e.g. { verdict: [approve, escalate] }.") }).optional()
611
+ output: z.strictObject({
612
+ schema: z.record(z.string(), z.unknown()).optional().describe("Extra structured fields the node must report, e.g. { verdict: [approve, escalate] }."),
613
+ gather: z.array(z.string().min(1)).min(1).optional().describe("List fields kept across rounds: each round's entries are added to the earlier rounds' instead of replacing them, e.g. [decisions].")
614
+ }).optional()
612
615
  };
613
616
  /** The settings keys an agent can bring, in canonical order. */
614
617
  const AGENT_SETTING_KEYS = [
@@ -858,7 +861,7 @@ const serializeAgent = (name, agent) => {
858
861
  // ../schema/src/edges.ts
859
862
  const ARROW = "->";
860
863
  const splitList = (side) => side.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
861
- const asList$1 = (value) => Array.isArray(value) ? value : [value];
864
+ const asList$2 = (value) => Array.isArray(value) ? value : [value];
862
865
  /**
863
866
  * Expands `"a, b -> c, d"` (or the object form) into the cartesian product of
864
867
  * edges.
@@ -866,7 +869,7 @@ const asList$1 = (value) => Array.isArray(value) ? value : [value];
866
869
  const expandEdge = (input, source) => {
867
870
  if (typeof input !== "string") return {
868
871
  ok: true,
869
- edges: asList$1(input.from).flatMap((from) => asList$1(input.to).map((to) => {
872
+ edges: asList$2(input.from).flatMap((from) => asList$2(input.to).map((to) => {
870
873
  const edge = {
871
874
  from,
872
875
  to,
@@ -907,7 +910,7 @@ const formatEdge = (input) => {
907
910
  const [left = "", right = ""] = input.split(ARROW);
908
911
  return `${splitList(left).join(", ")} ${ARROW} ${splitList(right).join(", ")}`;
909
912
  }
910
- if (input.when === void 0 && input.maxRounds === void 0) return `${asList$1(input.from).join(", ")} ${ARROW} ${asList$1(input.to).join(", ")}`;
913
+ if (input.when === void 0 && input.maxRounds === void 0) return `${asList$2(input.from).join(", ")} ${ARROW} ${asList$2(input.to).join(", ")}`;
911
914
  return input;
912
915
  };
913
916
  /** Settings every Claude node takes unless it (or its agent) sets its own. */
@@ -1055,6 +1058,31 @@ const markValue = (value) => {
1055
1058
  const pad = line.startsWith("`") || line.endsWith("`") ? " " : "";
1056
1059
  return `${fence}${pad}${line}${pad}${fence}`;
1057
1060
  };
1061
+ // ../schema/src/issue-messages.ts
1062
+ const NODE_TYPES = NodeSchema.options.map((o) => o.shape.type.value);
1063
+ const isMap$1 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
1064
+ /** The text for an issue at or under a `forEach`, else `null`. */
1065
+ const forEachMessage = (issue, path, value) => {
1066
+ const key = path.at(-1);
1067
+ if (issue.code === "too_small" && path.at(-2) === "forEach") {
1068
+ if (key === "items") return "`forEach.items` is empty: list at least one item, or use `from`";
1069
+ if (key === "maxItems") return "`forEach.maxItems` must be at least 1: the most items to run before Ork asks";
1070
+ }
1071
+ if (issue.code !== "invalid_union" || key !== "forEach") return null;
1072
+ if (!isMap$1(value)) return "`forEach` takes a map: `items: [...]` for a list written here, or `from:` with `maxItems:` for an earlier node's";
1073
+ if ("items" in value && "from" in value) return "`forEach` takes `items` or `from`, not both";
1074
+ if ("from" in value) return "`forEach` with `from` needs `maxItems`: the most items to run before Ork asks";
1075
+ if (!("items" in value)) return "`forEach` needs `items` or `from`: a list written here, or an earlier node's";
1076
+ return null;
1077
+ };
1078
+ /** The text for a `list` rule that is not a map of fields, else `null`. */
1079
+ const listRuleMessage = (issue, value) => issue.code === "invalid_union" && isMap$1(value) && "list" in value ? "`list` takes a map of each item's fields, e.g. `list: { title: string }` (`list: {}` for items of any shape)" : null;
1080
+ /** The text for a node whose `type` is missing or unknown, else `null`. */
1081
+ const nodeTypeMessage = (issue, value) => {
1082
+ if (issue.code !== "invalid_union" || issue.discriminator !== "type") return null;
1083
+ const types = NODE_TYPES.map((type) => markValue(type)).join(", ");
1084
+ return value === void 0 ? `node needs a \`type\`: one of ${types}` : `unknown node type ${markValue(typeof value === "string" ? value : JSON.stringify(value) ?? "")}: expected one of ${types}`;
1085
+ };
1058
1086
  // ../schema/src/newer-version.ts
1059
1087
  /**
1060
1088
  * A file with a higher `schemaVersion` than this Ork reads was made by a newer
@@ -1075,7 +1103,6 @@ const newerVersion = (issues, raw) => {
1075
1103
  * diagnostics, and the workflow's byte-stable canonical serializer.
1076
1104
  */
1077
1105
  const SCHEMA_URL = "https://raw.githubusercontent.com/mimshins/ork/main/packages/schema/workflow.schema.json";
1078
- const NODE_TYPES = NodeSchema.options.map((o) => o.shape.type.value);
1079
1106
  const createLocate = (doc, counter) => (path) => {
1080
1107
  for (let depth = path.length; depth >= 0; depth -= 1) {
1081
1108
  const node = doc.getIn(path.slice(0, depth), true);
@@ -1090,6 +1117,12 @@ const createLocate = (doc, counter) => (path) => {
1090
1117
  return null;
1091
1118
  };
1092
1119
  const toPath = (path) => path.map((p) => typeof p === "number" ? p : String(p));
1120
+ /** The value at a path of the raw document, or undefined. */
1121
+ const valueAt = (raw, path) => {
1122
+ let value = raw;
1123
+ for (const segment of path) value = value !== null && typeof value === "object" ? Reflect.get(value, segment) : void 0;
1124
+ return value;
1125
+ };
1093
1126
  const describeIssue = (issue, raw, path) => {
1094
1127
  if (issue.code === "unrecognized_keys") {
1095
1128
  const [first] = issue.keys;
@@ -1098,14 +1131,12 @@ const describeIssue = (issue, raw, path) => {
1098
1131
  path: first === void 0 ? path : [...path, first]
1099
1132
  };
1100
1133
  }
1101
- if (issue.code === "invalid_union" && issue.discriminator === "type") {
1102
- let value = raw;
1103
- for (const segment of path) value = value !== null && typeof value === "object" ? Reflect.get(value, segment) : void 0;
1104
- return {
1105
- message: value === void 0 ? `node needs a \`type\`: one of ${NODE_TYPES.map((type) => markValue(type)).join(", ")}` : `unknown node type ${markValue(typeof value === "string" ? value : JSON.stringify(value) ?? "")}: expected one of ${NODE_TYPES.map((type) => markValue(type)).join(", ")}`,
1106
- path
1107
- };
1108
- }
1134
+ const value = valueAt(raw, path);
1135
+ const said = forEachMessage(issue, path, value) ?? listRuleMessage(issue, value) ?? nodeTypeMessage(issue, value);
1136
+ if (said !== null) return {
1137
+ message: said,
1138
+ path
1139
+ };
1109
1140
  return {
1110
1141
  message: issue.message.replaceAll(/"([^"`\n]+)"/gu, "`$1`"),
1111
1142
  path
@@ -4194,7 +4225,7 @@ const resolvePath = (segments, scope) => {
4194
4225
  };
4195
4226
  /** Truthiness as expressions see it: an empty list is false. */
4196
4227
  const truthy = (value) => Array.isArray(value) ? value.length > 0 : Boolean(value);
4197
- const asList = (value) => Array.isArray(value) ? value : [];
4228
+ const asList$1 = (value) => Array.isArray(value) ? value : [];
4198
4229
  const order = (op, l, r) => {
4199
4230
  if (op === "<") return l < r;
4200
4231
  if (op === "<=") return l <= r;
@@ -4222,7 +4253,7 @@ const evaluate = (expr, scope) => {
4222
4253
  case "call": {
4223
4254
  const args = expr.args.map((arg) => evaluate(arg, scope));
4224
4255
  const [subject, value] = args;
4225
- const list = asList(subject);
4256
+ const list = asList$1(subject);
4226
4257
  const matches = (item) => args.length > 1 ? item === value : truthy(item);
4227
4258
  switch (expr.name) {
4228
4259
  case "any": return list.some((item) => matches(item));
@@ -4328,11 +4359,21 @@ const readableText = (value, before) => {
4328
4359
  * Fills a parsed template. `readable` is for text a person reads (a gate's
4329
4360
  * `question` and `document`); scripts and prompts keep JSON (ADR-047).
4330
4361
  */
4331
- const renderTemplate = (parts, scope, { readable = false } = {}) => parts.reduce((text, part) => {
4332
- if (typeof part === "string") return text + part;
4333
- const value = evaluate(part.expr, scope);
4334
- return text + (readable ? readableText(value, text) : stringify$1(value));
4335
- }, "");
4362
+ const renderTemplate = (parts, scope, { readable = false } = {}) => {
4363
+ let text = "";
4364
+ let afterList = false;
4365
+ for (const part of parts) {
4366
+ const value = typeof part === "string" ? part : evaluate(part.expr, scope);
4367
+ const piece = readable ? readableText(value, text) : stringify$1(value);
4368
+ if (piece.trim() === "") {
4369
+ text += piece;
4370
+ continue;
4371
+ }
4372
+ text = afterList && !isContainer(value) ? `${text.trimEnd()}\n\n${piece.trimStart()}` : text + piece;
4373
+ afterList = readable && isContainer(value) && depthOf(value) <= 2;
4374
+ }
4375
+ return text;
4376
+ };
4336
4377
  // ../core/src/feedback/fingerprint.ts
4337
4378
  /**
4338
4379
  * @file Feedback fingerprints: records about the same thing (same target, kind,
@@ -5055,6 +5096,8 @@ const REPORT_STATUSES = [
5055
5096
  ];
5056
5097
  /** What a node declares of its output (RFC-010), if it does. */
5057
5098
  const declaredOutputOf = (node) => "output" in node ? node.output?.schema : void 0;
5099
+ /** The report fields a node keeps across its rounds (ADR-073), if any. */
5100
+ const gatheredFieldsOf = (node) => node !== void 0 && isClaudeNode(node) ? node.output?.gather ?? [] : [];
5058
5101
  /** What a node with `forEach` reports instead of its type's fields (RFC-013). */
5059
5102
  const GATHERED_FIELDS = [
5060
5103
  "items",
@@ -5686,6 +5729,11 @@ const ownersOf = (workflow, id) => {
5686
5729
  return owners;
5687
5730
  };
5688
5731
  // ../core/src/graph/for-each.ts
5732
+ /**
5733
+ * @file The checks on `forEach` (RFC-013): where its list comes from, and where
5734
+ * `{{item}}` and `{{index}}` may be read and what `{{item.<field>}}` may
5735
+ * name.
5736
+ */
5689
5737
  const isItemRoot = (root) => ITEM_ROOTS.includes(root);
5690
5738
  /**
5691
5739
  * Nodes whose output exists before `id` runs: its flow ancestors and its
@@ -5701,7 +5749,7 @@ const upstreamOf = (workflow, edges, id) => {
5701
5749
  };
5702
5750
  /** The field a path under `nodes.<id>.output` reads, as `says.ts` names it. */
5703
5751
  const fieldOfPath = (segments) => segments.slice(3).join(".");
5704
- const isList = (said) => said.items !== void 0 || said.kind === "statuses";
5752
+ const isList$1 = (said) => said.items !== void 0 || said.kind === "statuses";
5705
5753
  /** What a node's `from` list's items have, or `undefined` if nothing says. */
5706
5754
  const sourceFields = (segments, says) => {
5707
5755
  const [root, id] = segments;
@@ -5745,6 +5793,11 @@ const checkFiles = (id, key, files) => files.flatMap((file, index) => {
5745
5793
  index
5746
5794
  ]))) : [];
5747
5795
  });
5796
+ /**
5797
+ * Whether one activation's parsed JSON is at `output.json` (a program, an MCP
5798
+ * call).
5799
+ */
5800
+ const readsJson = (node) => (isToolNode(node) || node.type === "mcp.call") && forEachOf$1(node) === void 0;
5748
5801
  /** The source of one `from`: it exists, runs first, and is a declared list. */
5749
5802
  const checkSource = (workflow, edges, says, id, segments, path) => {
5750
5803
  const named = describePath(segments);
@@ -5759,8 +5812,15 @@ const checkSource = (workflow, edges, says, id, segments, path) => {
5759
5812
  if (source === id || !ancestorsOf(id, first).has(String(source))) return [error("forEach.order", `\`${named}\`: \`${String(source)}\` does not run before \`${id}\`, so its list can't exist when \`${id}\` starts`, path)];
5760
5813
  const field = fieldOfPath(segments);
5761
5814
  const said = says[String(source)]?.fields.find(({ field: name }) => name === field);
5762
- if (field === "" || said !== void 0 && !isList(said)) return [error("forEach.not-list", `\`${named}\` is not a list: \`${String(source)}\` declares ${field === "" ? "its whole output" : `\`${field}\``} as something else; declare it as \`{ list: { … } }\``, path)];
5763
- return said === void 0 ? [warning("forEach.undeclared", `\`${named}\`: nothing declares this list, so a run can't check it or its items; add \`{ list: { … } }\` to \`${String(source)}\`'s \`output.schema\``, path)] : [];
5815
+ if (field === "" || said !== void 0 && !isList$1(said)) return [error("forEach.not-list", `\`${named}\` is not a list: \`${String(source)}\` declares ${field === "" ? "its whole output" : `\`${field}\``} as something else; declare it as \`{ list: { … } }\``, path)];
5816
+ if (said !== void 0) return [];
5817
+ const add = `add \`{ list: { … } }\` to \`${String(source)}\`'s \`output.schema\``;
5818
+ const node = workflow.nodes[String(source)];
5819
+ if (node !== void 0 && readsJson(node) && !field.startsWith("json.")) {
5820
+ const declared = says[String(source)]?.fields.some(({ field: name }) => name === `json.${field}`);
5821
+ return [warning("forEach.undeclared", `\`${named}\`: a tool's JSON is read as \`nodes.${String(source)}.output.json.${field}\`${declared === true ? "" : `; ${add}`}`, path)];
5822
+ }
5823
+ return [warning("forEach.undeclared", `\`${named}\`: nothing declares this list, so a run can't check it or its items; ${add}`, path)];
5764
5824
  };
5765
5825
  /** Every `forEach`'s source, and `{{item}}` in the lists no template reads. */
5766
5826
  const checkForEach = (workflow, edges) => {
@@ -5839,6 +5899,26 @@ const checkGroup = (workflow, edges, component) => {
5839
5899
  };
5840
5900
  /** What's wrong with the shape of each group of nodes that loop together. */
5841
5901
  const checkLoops = (workflow, edges) => stronglyConnected(Object.keys(workflow.nodes), edges).flatMap((component) => checkGroup(workflow, edges, component));
5902
+ // ../core/src/graph/gather.ts
5903
+ /** Whether a declared field is a list: `{ type: array }` or `{ list: … }`. */
5904
+ const isList = (rule) => listShape(rule) !== void 0 || typeof rule === "object" && rule !== null && "type" in rule && rule.type === "array";
5905
+ const gatherProblems = (id, node) => {
5906
+ const fields = gatheredFieldsOf(node);
5907
+ if (fields.length === 0) return [];
5908
+ const path = [
5909
+ "nodes",
5910
+ id,
5911
+ "output",
5912
+ "gather"
5913
+ ];
5914
+ if (forEachOf$1(node) !== void 0) return [error("output.gather", `\`${id}\` runs once per item and reports its items' results, so it has no fields of its own to keep across rounds`, path)];
5915
+ const declared = declaredOutputOf(node) ?? {};
5916
+ return fields.flatMap((field, index) => {
5917
+ const rule = declared[field];
5918
+ if (rule === void 0) return [error("output.gather", `\`${field}\` isn't in \`${id}\`'s \`output.schema\`; declare it as a list to keep it across rounds`, [...path, index])];
5919
+ return isList(rule) ? [] : [error("output.gather", `\`${field}\` isn't declared as a list (\`{ type: array }\` or \`{ list: … }\`), so its rounds can't be added together`, [...path, index])];
5920
+ });
5921
+ };
5842
5922
  // ../core/src/graph/script.ts
5843
5923
  /**
5844
5924
  * @file Checks on the programs a workflow declares (RFC-006): a tool node's and
@@ -5910,7 +5990,7 @@ const checkNodes = (workflow, edges) => {
5910
5990
  const incoming = edges.filter((e) => e.to === id);
5911
5991
  const outgoing = edges.filter((e) => e.from === id);
5912
5992
  if (node.type === "claude.subagent" && node.timeout !== void 0) diagnostics.push(warning("timeout.subagent", `sub-agent \`${id}\` runs inside its parent's session, so its timeout has no effect; set one on the parent`, [...path, "timeout"]));
5913
- diagnostics.push(...statusProblems(id, node));
5993
+ diagnostics.push(...statusProblems(id, node), ...gatherProblems(id, node));
5914
5994
  if (node.type === "control.join") {
5915
5995
  const sources = new Set(incoming.map((e) => e.from)).size;
5916
5996
  if (sources < 2) diagnostics.push(warning("join.inputs", `join \`${id}\` has ${sources} incoming node(s); a join waits for two or more`, path));
@@ -6823,14 +6903,23 @@ const createLoops = (options) => {
6823
6903
  };
6824
6904
  /**
6825
6905
  * What a list over its cap runs: asked of a live owner, then the human. All
6826
- * of it, the first `maxItems`, or `null` to stop.
6906
+ * of it, the first `maxItems`, or `null` to stop. A stop (`signal`) ends the
6907
+ * wait and withdraws the question; the list comes back whole, and nothing of
6908
+ * it starts because the stop also bars its items.
6827
6909
  */
6828
- const overList = async (node, list, maxItems, round) => {
6910
+ const overList = async (node, list, maxItems, round, signal) => {
6829
6911
  const question = eachQuestion(node, list.length, maxItems, round);
6830
- const choice = await options.bus?.askOwner(node, question) ?? await options.human.ask({
6912
+ const asking = async () => await options.bus?.askOwner(node, question) ?? (signal.aborted ? null : options.human.ask({
6831
6913
  nodeId: node,
6832
- ...question
6833
- });
6914
+ ...question,
6915
+ signal
6916
+ }));
6917
+ const stopped = Promise.withResolvers();
6918
+ signal.addEventListener("abort", () => {
6919
+ stopped.resolve(null);
6920
+ }, { once: true });
6921
+ const choice = await Promise.race([asking(), stopped.promise]);
6922
+ if (signal.aborted) return list;
6834
6923
  if (choice === EACH_CHOICES.first) return list.slice(0, maxItems);
6835
6924
  return choice === EACH_CHOICES.all ? list : null;
6836
6925
  };
@@ -6911,6 +7000,40 @@ const declaredProblem = (declared, value) => {
6911
7000
  * still masks to the record is the file's, with its real values.
6912
7001
  */
6913
7002
  const unmasked = (forEach, recorded) => "items" in forEach && JSON.stringify(forEach.items, maskReplacer) === JSON.stringify(recorded) ? [...forEach.items] : recorded;
7003
+ // ../core/src/run/contacts.ts
7004
+ const SESSION_TYPES$4 = /* @__PURE__ */ new Set(["claude.leader", "claude.worker"]);
7005
+ const isSession = (node) => SESSION_TYPES$4.has(node?.type ?? "");
7006
+ const isTool = (node) => node?.type.startsWith("tool.") ?? false;
7007
+ const channelPeersOf = (id, channels) => channels.flatMap(([a, b]) => {
7008
+ if (a === id) return [b];
7009
+ return b === id ? [a] : [];
7010
+ });
7011
+ /** Every declared node, with its owner, as a run records them. */
7012
+ const declaredNodes = (graph) => [...graph.nodes.values()].map(({ id, node }) => node.parent === void 0 ? {
7013
+ id,
7014
+ type: node.type
7015
+ } : {
7016
+ id,
7017
+ type: node.type,
7018
+ parent: node.parent
7019
+ });
7020
+ /** Whom a session may write to, by how it knows them. */
7021
+ const contactsOf = (id, nodes, channels) => {
7022
+ const byId = new Map(nodes.map((node) => [node.id, node]));
7023
+ return {
7024
+ owner: byId.get(id)?.parent,
7025
+ children: nodes.filter((node) => node.parent === id && isSession(node)).map((node) => node.id),
7026
+ channels: channelPeersOf(id, channels).filter((peer) => !isTool(byId.get(peer)))
7027
+ };
7028
+ };
7029
+ /** Whom a program in the tool node `id` may write to. */
7030
+ const scriptContactsOf = (id, nodes, channels) => {
7031
+ const byId = new Map(nodes.map((node) => [node.id, node]));
7032
+ const owner = byId.get(id)?.parent;
7033
+ return [.../* @__PURE__ */ new Set([...owner !== void 0 && isSession(byId.get(owner)) ? [owner] : [], ...channelPeersOf(id, channels).filter((peer) => isSession(byId.get(peer)))])];
7034
+ };
7035
+ /** Whether a program in a tool node may write to the session `id`. */
7036
+ const hearsFromScripts = (id, nodes, channels) => nodes.some((node) => isTool(node) && scriptContactsOf(node.id, nodes, channels).includes(id));
6914
7037
  // ../core/src/usage/spend.ts
6915
7038
  /** What a report would add to the session's earlier ones, counting nothing. */
6916
7039
  const unreported = (reported, key, total) => {
@@ -6966,46 +7089,7 @@ const earlierSpend = (events) => {
6966
7089
  forks
6967
7090
  };
6968
7091
  };
6969
- // ../core/src/run/contacts.ts
6970
- const SESSION_TYPES$4 = /* @__PURE__ */ new Set(["claude.leader", "claude.worker"]);
6971
- const isSession = (node) => SESSION_TYPES$4.has(node?.type ?? "");
6972
- const isTool = (node) => node?.type.startsWith("tool.") ?? false;
6973
- const channelPeersOf = (id, channels) => channels.flatMap(([a, b]) => {
6974
- if (a === id) return [b];
6975
- return b === id ? [a] : [];
6976
- });
6977
- /** Every declared node, with its owner, as a run records them. */
6978
- const declaredNodes = (graph) => [...graph.nodes.values()].map(({ id, node }) => node.parent === void 0 ? {
6979
- id,
6980
- type: node.type
6981
- } : {
6982
- id,
6983
- type: node.type,
6984
- parent: node.parent
6985
- });
6986
- /** Whom a session may write to, by how it knows them. */
6987
- const contactsOf = (id, nodes, channels) => {
6988
- const byId = new Map(nodes.map((node) => [node.id, node]));
6989
- return {
6990
- owner: byId.get(id)?.parent,
6991
- children: nodes.filter((node) => node.parent === id && isSession(node)).map((node) => node.id),
6992
- channels: channelPeersOf(id, channels).filter((peer) => !isTool(byId.get(peer)))
6993
- };
6994
- };
6995
- /** Whom a program in the tool node `id` may write to. */
6996
- const scriptContactsOf = (id, nodes, channels) => {
6997
- const byId = new Map(nodes.map((node) => [node.id, node]));
6998
- const owner = byId.get(id)?.parent;
6999
- return [.../* @__PURE__ */ new Set([...owner !== void 0 && isSession(byId.get(owner)) ? [owner] : [], ...channelPeersOf(id, channels).filter((peer) => isSession(byId.get(peer)))])];
7000
- };
7001
- /** Whether a program in a tool node may write to the session `id`. */
7002
- const hearsFromScripts = (id, nodes, channels) => nodes.some((node) => isTool(node) && scriptContactsOf(node.id, nodes, channels).includes(id));
7003
- // ../core/src/run/results.ts
7004
- /** Control nodes pass their input through; a join's input is its output. */
7005
- const passThrough = (input) => ({
7006
- output: input ?? {},
7007
- ok: true
7008
- });
7092
+ // ../core/src/run/spending.ts
7009
7093
  /**
7010
7094
  * A result with only what this activation spent (ADR-046): a session's report
7011
7095
  * holds its total so far, while its tokens are this process's own. `reported`
@@ -7031,11 +7115,23 @@ const withOwnSpend = (id, result, reported) => {
7031
7115
  const createSpend = (budgetUsd, earlier, limits) => {
7032
7116
  const reported = new Map(earlier?.reported);
7033
7117
  let spent = earlier?.total ?? 0;
7034
- const budgetLeft = () => budgetUsd === void 0 ? void 0 : budgetUsd - spent;
7118
+ /** What running sessions last reported, not yet in `spent`, by `node/session`. */
7119
+ const inFlight = /* @__PURE__ */ new Map();
7120
+ const committed = () => spent + [...inFlight.values()].reduce((sum, cost) => sum + cost, 0);
7121
+ const budgetLeft = () => budgetUsd === void 0 ? void 0 : budgetUsd - committed();
7035
7122
  return {
7036
7123
  total: () => spent,
7037
7124
  /** What a session has spent in this activation, from its total so far. */
7038
7125
  ownOf: (id, sessionId, total) => unreported(reported, `${id}/${sessionId}`, total),
7126
+ /** A running session said what it has spent so far. */
7127
+ report: (id, sessionId, total) => {
7128
+ const key = `${id}/${sessionId}`;
7129
+ inFlight.set(key, unreported(reported, key, total));
7130
+ },
7131
+ /** The node ended: `count` has what its sessions last reported. */
7132
+ settled: (id) => {
7133
+ for (const key of inFlight.keys()) if (key.startsWith(`${id}/`)) inFlight.delete(key);
7134
+ },
7039
7135
  /** What a session may spend: the least of the budget and any quota. */
7040
7136
  left: () => {
7041
7137
  const known = [budgetLeft(), limits?.().leftUsd].filter((left) => left !== void 0);
@@ -7054,7 +7150,7 @@ const createSpend = (budgetUsd, earlier, limits) => {
7054
7150
  status: "quota_exhausted",
7055
7151
  reason: blocked
7056
7152
  };
7057
- return budgetUsd !== void 0 && spent >= budgetUsd ? {
7153
+ return budgetUsd !== void 0 && committed() >= budgetUsd ? {
7058
7154
  status: "budget_exhausted",
7059
7155
  reason: `the run's budget of $${String(budgetUsd)} is spent`
7060
7156
  } : null;
@@ -7073,6 +7169,56 @@ const createSpend = (budgetUsd, earlier, limits) => {
7073
7169
  };
7074
7170
  /** A run's spending, from what the scheduler is given. */
7075
7171
  const spendOf = (budgetUsd, options) => createSpend(budgetUsd, options.replay?.earlier, options.limits);
7172
+ // ../core/src/run/gather.ts
7173
+ const asList = (value) => Array.isArray(value) ? value : [];
7174
+ /** `before` then `now`, leaving out an entry already there. */
7175
+ const joined = (before, now) => {
7176
+ const seen = /* @__PURE__ */ new Set();
7177
+ const out = [];
7178
+ for (const entry of [...asList(before), ...asList(now)]) {
7179
+ const key = JSON.stringify(entry);
7180
+ if (seen.has(key)) continue;
7181
+ seen.add(key);
7182
+ out.push(entry);
7183
+ }
7184
+ return out;
7185
+ };
7186
+ /**
7187
+ * `result` with each gathered field holding every round's entries so far.
7188
+ * `previous` is the node's output from its last round, already gathered.
7189
+ */
7190
+ const joinRounds = (fields, previous, result) => {
7191
+ if (fields.length === 0 || previous === void 0) return result;
7192
+ const output = { ...result.output };
7193
+ for (const field of fields) output[field] = joined(previous[field], result.output[field]);
7194
+ return {
7195
+ ...result,
7196
+ output
7197
+ };
7198
+ };
7199
+ /** `result` with `node`'s `output.gather` fields joined to its earlier rounds. */
7200
+ const gatherRounds = (node, previous, result) => joinRounds(gatheredFieldsOf(node), previous, result);
7201
+ /**
7202
+ * What `activation`'s rounds reported so far, to gather onto. A spawned
7203
+ * instance gathers nothing earlier: its id (`impl-2`) is reused by a different
7204
+ * worker in the leader's next round. A delegated child that hasn't run since a
7205
+ * resume starts from what it reported before it, since its leader's recorded
7206
+ * rounds are handed back whole and never run it again.
7207
+ */
7208
+ const previousOf = (activation, current, replay) => {
7209
+ const { brief, template, id } = activation;
7210
+ if (brief !== void 0 && template !== void 0) return void 0;
7211
+ if (current !== void 0 || brief === void 0) return current;
7212
+ return replay?.lastOutput?.(id);
7213
+ };
7214
+ /** Gathers each activation's result onto what its node reported before. */
7215
+ const createGatherer = (outputs, replay, nodeOf) => (activation, result) => gatherRounds(nodeOf(activation), previousOf(activation, outputs[activation.id], replay), result);
7216
+ // ../core/src/run/results.ts
7217
+ /** Control nodes pass their input through; a join's input is its output. */
7218
+ const passThrough = (input) => ({
7219
+ output: input ?? {},
7220
+ ok: true
7221
+ });
7076
7222
  const recordRun = (entry, result) => {
7077
7223
  entry.runs += 1;
7078
7224
  entry.ok = result.ok;
@@ -7270,10 +7416,10 @@ const sourceOf = (forEach, scope) => {
7270
7416
  const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
7271
7417
  /**
7272
7418
  * What `results` keeps of an item: its declared fields (a program's are under
7273
- * `json`), its summary, and Ork's own branch and worktree. The full output
7274
- * stays in the item's own `node.finished`.
7419
+ * `json`), its summary, Ork's own branch and worktree, and how a program
7420
+ * failed. The full output stays in the item's own `node.finished`.
7275
7421
  */
7276
- const entryOf = (declared, output) => {
7422
+ const entryOf = (declared, { ok, output, error }) => {
7277
7423
  const json = isRecord$1(output["json"]) ? output["json"] : {};
7278
7424
  const fields = Object.keys(declared ?? {}).map((field) => [field, Object.hasOwn(json, field) ? json[field] : output[field]]);
7279
7425
  const own = [
@@ -7282,7 +7428,12 @@ const entryOf = (declared, output) => {
7282
7428
  "worktree",
7283
7429
  "merge"
7284
7430
  ].map((field) => [field, output[field]]);
7285
- return Object.fromEntries([...fields, ...own].filter(([, value]) => value !== void 0));
7431
+ const failed = !ok && typeof output["exitCode"] === "number" ? [["exitCode", output["exitCode"]], ["error", error]] : [];
7432
+ return Object.fromEntries([
7433
+ ...fields,
7434
+ ...own,
7435
+ ...failed
7436
+ ].filter(([, value]) => value !== void 0));
7286
7437
  };
7287
7438
  /** Whether an item passed but its work could not reach its owner. */
7288
7439
  const mergeFailed = ({ output }) => isRecord$1(output["merge"]) && output["merge"]["ok"] === false;
@@ -7326,13 +7477,18 @@ const createFanOut = (deps) => {
7326
7477
  };
7327
7478
  if (!listed.ok) return failure(listed.error);
7328
7479
  let { list } = listed;
7329
- options.onEvent({
7330
- type: "each.listed",
7331
- node: id,
7332
- round,
7333
- count: list.length,
7334
- list
7335
- });
7480
+ const recordList = () => {
7481
+ options.onEvent({
7482
+ type: "each.listed",
7483
+ node: id,
7484
+ round,
7485
+ count: list.length,
7486
+ list,
7487
+ ...recorded === null && options.replay !== void 0 ? { anew: true } : {}
7488
+ });
7489
+ };
7490
+ recordList();
7491
+ const signal = stops.signalFor(id);
7336
7492
  if ("maxItems" in forEach && list.length > forEach.maxItems) {
7337
7493
  options.onEvent({
7338
7494
  type: "each.exceeded",
@@ -7341,22 +7497,23 @@ const createFanOut = (deps) => {
7341
7497
  count: list.length,
7342
7498
  maxItems: forEach.maxItems
7343
7499
  });
7344
- const chosen = await deps.loops.overList(id, list, forEach.maxItems, round);
7500
+ const chosen = await deps.loops.overList(id, list, forEach.maxItems, round, signal);
7345
7501
  if (chosen === null) {
7346
7502
  const reason = `the list of ${id} has ${String(list.length)} items, over maxItems ${String(forEach.maxItems)}`;
7347
- return {
7503
+ return stops.ended(id, {
7348
7504
  ...failure(reason),
7349
7505
  limited: reason,
7350
7506
  stopsAs: "failed"
7351
- };
7507
+ });
7352
7508
  }
7509
+ const narrowed = chosen.length !== list.length;
7353
7510
  list = chosen;
7511
+ if (narrowed) recordList();
7354
7512
  }
7355
7513
  const node = nodeOf(activation);
7356
7514
  const session = node !== void 0 && needsSlot(node);
7357
7515
  const spends = node !== void 0 && spendsBudget(node);
7358
7516
  const declared = node === void 0 ? void 0 : declaredOutputOf(node);
7359
- const signal = stops.signalFor(id);
7360
7517
  const results = list.map(() => ({}));
7361
7518
  const statuses = list.map(() => "skipped");
7362
7519
  const failures = [];
@@ -7378,7 +7535,7 @@ const createFanOut = (deps) => {
7378
7535
  },
7379
7536
  round
7380
7537
  });
7381
- results[index] = entryOf(declared, result.output);
7538
+ results[index] = entryOf(declared, result);
7382
7539
  statuses[index] = statusOf(result);
7383
7540
  if (!result.ok) failures.push(`${itemId}: ${result.error ?? "failed"}`);
7384
7541
  else if (mergeFailed(result)) failures.push(`${itemId}: its work could not be merged`);
@@ -7614,6 +7771,10 @@ const nodeContext = (deps, activation, cwd, round) => {
7614
7771
  ...options.calls === void 0 ? {} : { calls: options.calls },
7615
7772
  emit: (event) => {
7616
7773
  deps.ports.owners.seen(id, event, cwd);
7774
+ if (event.type === "session.finished" && event.fork !== true) {
7775
+ const { sessionId, usage } = event.outcome;
7776
+ deps.spend.report(id, sessionId, usage.costUsd);
7777
+ }
7617
7778
  options.onEvent({
7618
7779
  type: "node.event",
7619
7780
  id,
@@ -7675,6 +7836,27 @@ const resumeOf = (node, sessionId) => {
7675
7836
  if (node.type === "claude.subagent") return void 0;
7676
7837
  return (node.session ?? (node.type === "claude.leader" ? "resume" : "fresh")) === "resume" ? sessionId : void 0;
7677
7838
  };
7839
+ // ../core/src/store/nest-items.ts
7840
+ /** Whether a tree node is an item of a fanned node (`build.2`). */
7841
+ const isItem = (node) => node.template !== void 0 && /\.\d+$/u.test(node.id);
7842
+ /**
7843
+ * Moves each item of a node with `forEach` (`build.2`) under its node, in list
7844
+ * order, from beside it or under its owner, where the events put it.
7845
+ */
7846
+ const nestItems = (roots) => {
7847
+ const all = [];
7848
+ const collect = (node) => {
7849
+ all.push(node);
7850
+ for (const child of node.children) collect(child);
7851
+ };
7852
+ for (const root of roots) collect(root);
7853
+ const items = all.filter((node) => isItem(node));
7854
+ const home = new Map(all.map((node) => [node.id, node]));
7855
+ const isMoved = (node) => items.includes(node) && home.has(node.template ?? "");
7856
+ for (const node of all) node.children = node.children.filter((c) => !isMoved(c));
7857
+ for (const item of items) home.get(item.template ?? "")?.children.push(item);
7858
+ return roots.filter((root) => !isMoved(root));
7859
+ };
7678
7860
  // ../core/src/store/process-age.ts
7679
7861
  /**
7680
7862
  * @file Telling a process from a later one that got its id. A process id comes
@@ -7847,10 +8029,14 @@ const templateOf = (id, declared) => {
7847
8029
  const base = /^(?<base>.+)[-.]\d+$/u.exec(id)?.groups?.["base"];
7848
8030
  return base !== void 0 && declared.has(base) ? base : void 0;
7849
8031
  };
8032
+ const setItemsWaiting = (nodes, of) => {
8033
+ for (const node of nodes) if (node.template === of && isItem(node)) node.state = "waiting";
8034
+ };
7850
8035
  /** Folds a run's events into its ownership tree; roots in declared order. */
7851
8036
  const runTree = (events) => {
7852
8037
  const nodes = /* @__PURE__ */ new Map();
7853
8038
  const declared = /* @__PURE__ */ new Map();
8039
+ const listedRounds = /* @__PURE__ */ new Map();
7854
8040
  for (const event of events) if (event.type === "run.started") for (const info of event.nodes) {
7855
8041
  if (nodes.has(info.id)) continue;
7856
8042
  const node = blank(info.id, info.type, info.parent);
@@ -7911,6 +8097,13 @@ const runTree = (events) => {
7911
8097
  } else if (event.type === "node.skipped") {
7912
8098
  const node = nodes.get(event.id);
7913
8099
  if (node !== void 0) node.state = "skipped";
8100
+ } else if (event.type === "each.listed") {
8101
+ const node = nodes.get(event.node);
8102
+ if (node !== void 0) node.listed = event.count;
8103
+ if (event.round > (listedRounds.get(event.node) ?? 0)) {
8104
+ listedRounds.set(event.node, event.round);
8105
+ setItemsWaiting(nodes.values(), event.node);
8106
+ }
7914
8107
  } else if (event.type === "call.started") {
7915
8108
  const node = nodes.get(event.node);
7916
8109
  if (node !== void 0) node.calledRun = event.run;
@@ -9338,6 +9531,7 @@ const startRun = (options) => {
9338
9531
  loops
9339
9532
  })
9340
9533
  });
9534
+ const gather = createGatherer(outputs, options.replay, nodeOf);
9341
9535
  /** Runs one activation and records it; routing is the caller's. */
9342
9536
  const perform = async (activation) => {
9343
9537
  const { id } = activation;
@@ -9350,8 +9544,9 @@ const startRun = (options) => {
9350
9544
  const agent = graph.agents.get(activation.template ?? id);
9351
9545
  onEvent(startedEvent(activation, type, round, agent));
9352
9546
  }
9353
- const result = spend.count(id, stops.ended(id, recorded ?? await execute(activation, round)), recorded !== null);
9547
+ const result = gather(activation, spend.count(id, stops.ended(id, recorded ?? await execute(activation, round)), recorded !== null));
9354
9548
  if (result.limited !== void 0) stop(result.stopsAs ?? "quota_exhausted", result.limited);
9549
+ spend.settled(id);
9355
9550
  holds.finished(id);
9356
9551
  ports.finished(id);
9357
9552
  clock.leave(id);
@@ -11171,10 +11366,11 @@ const mailOf = (events) => {
11171
11366
  * (ADR-026). Each node's recorded results, in order, up to its first that
11172
11367
  * failed or never finished, are handed back by the replay, so the scheduler
11173
11368
  * rebuilds outputs, rounds, joins and loop counts without running them; the
11174
- * rest run again, and so does everything a new result feeds. Also: the run's
11175
- * inputs and settings, its worktrees, the answers to its loops, and a fanned
11176
- * node's lists and its items' results by round (RFC-013). `replay.ts` hands
11177
- * them back.
11369
+ * rest run again, and so does everything a new result feeds. A run resumed
11370
+ * more than once reads as one history: a later result replaces the earlier
11371
+ * one at its place. Also: the run's inputs and settings, its worktrees, the
11372
+ * answers to its loops, and a fanned node's lists and its items' results by
11373
+ * round (RFC-013). `replay.ts` hands them back.
11178
11374
  */
11179
11375
  const listKey = (node, round) => `${node}/${String(round)}`;
11180
11376
  /** How a run ended that cut its working nodes off, rather than failing them. */
@@ -11184,6 +11380,8 @@ const CUTTING = /* @__PURE__ */ new Set([
11184
11380
  "quota_exhausted",
11185
11381
  "timed_out"
11186
11382
  ]);
11383
+ /** Whether `id` is an item (`<node>.<index>`) of `node`. */
11384
+ const isItemOf = (id, node) => id.startsWith(`${node}.`) && /^\d+$/u.test(id.slice(node.length + 1));
11187
11385
  /** Reads a run's events back into what `ork resume` needs. */
11188
11386
  const resumeState = (events) => {
11189
11387
  const history = /* @__PURE__ */ new Map();
@@ -11206,18 +11404,36 @@ const resumeState = (events) => {
11206
11404
  let ended;
11207
11405
  /** Nodes whose last activation did not finish well, and its session. */
11208
11406
  const unfinished = /* @__PURE__ */ new Map();
11407
+ /** A new run of `id`'s `round` replaces what is recorded there and after. */
11408
+ const supersede = (id, round) => {
11409
+ fanOuts.items.get(id)?.delete(round);
11410
+ const kept = history.get(id)?.slice(0, round - 1);
11411
+ if (kept === void 0) return;
11412
+ if (kept.length === 0) history.delete(id);
11413
+ else history.set(id, kept);
11414
+ };
11415
+ /** A list resolved anew: what its items did came from another list. */
11416
+ const forgetItems = (node, round) => {
11417
+ for (const [id, rounds] of fanOuts.items) if (isItemOf(id, node)) rounds.delete(round);
11418
+ for (const id of unfinished.keys()) if (isItemOf(id, node)) unfinished.delete(id);
11419
+ };
11209
11420
  for (const event of events) {
11210
11421
  if (event.type === "run.started") {
11211
11422
  inputs = event.inputs ?? inputs;
11212
11423
  settings = event.settings ?? settings;
11424
+ closed.clear();
11213
11425
  continue;
11214
11426
  }
11215
11427
  if (event.type === "node.started") {
11216
- if (event.owner === void 0) open.set(event.id, event.item === void 0 ? {} : { item: true });
11428
+ if (event.owner === void 0) {
11429
+ open.set(event.id, event.item === void 0 ? {} : { item: true });
11430
+ supersede(event.id, event.round);
11431
+ }
11217
11432
  continue;
11218
11433
  }
11219
11434
  if (event.type === "each.listed") {
11220
11435
  fanOuts.lists.set(listKey(event.node, event.round), event.list);
11436
+ if (event.anew === true) forgetItems(event.node, event.round);
11221
11437
  continue;
11222
11438
  }
11223
11439
  if (event.type === "node.event") {
@@ -11237,6 +11453,7 @@ const resumeState = (events) => {
11237
11453
  if (current.sessionId !== void 0) unfinished.set(event.id, current.sessionId);
11238
11454
  continue;
11239
11455
  }
11456
+ unfinished.delete(event.id);
11240
11457
  const result = {
11241
11458
  ok: true,
11242
11459
  output: event.output
@@ -13442,6 +13659,13 @@ const syncFrom = async (worktree, from) => {
13442
13659
  return snapshot(worktree);
13443
13660
  };
13444
13661
  // ../core/src/worktree/upstream.ts
13662
+ /** Where `id` worked: its own place, or for a fanned node each item's. */
13663
+ const placesOf = (id, placeOf) => {
13664
+ const own = placeOf.get(id);
13665
+ if (own !== void 0) return [own];
13666
+ const item = `${id}.`;
13667
+ return [...placeOf].filter(([other]) => other.startsWith(item)).map(([, place]) => place);
13668
+ };
13445
13669
  /**
13446
13670
  * The one place `id`'s upstream nodes worked in other than `root`, or
13447
13671
  * `undefined` when there is none or they disagree. `placeOf` is where each node
@@ -13460,8 +13684,7 @@ const upstreamPlace = (graph, id, placeOf, root) => {
13460
13684
  queue.push(...(node?.incoming ?? []).map((e) => e.from));
13461
13685
  continue;
13462
13686
  }
13463
- const place = placeOf.get(from);
13464
- if (place !== void 0 && place !== root) places.add(place);
13687
+ for (const place of placesOf(from, placeOf)) if (place !== root) places.add(place);
13465
13688
  }
13466
13689
  const [only, other] = places;
13467
13690
  return other === void 0 ? only : void 0;
@@ -13667,6 +13890,18 @@ const closeWorktrees = async (options) => {
13667
13890
  }
13668
13891
  return kept;
13669
13892
  };
13893
+ // ../core/src/worktree/conflict.ts
13894
+ const withConflict = (result, conflicts, into) => ({
13895
+ ...result,
13896
+ output: {
13897
+ ...result.output,
13898
+ merge: {
13899
+ ok: false,
13900
+ conflicts,
13901
+ into
13902
+ }
13903
+ }
13904
+ });
13670
13905
  // ../core/src/worktree/workspace.ts
13671
13906
  /**
13672
13907
  * @file Where each node of a run works (ADR-020), under the project's git rules
@@ -13679,6 +13914,7 @@ const closeWorktrees = async (options) => {
13679
13914
  * its owner: a merge, a patch, or not at all. A spawned worker works like the
13680
13915
  * declared child it is an instance of.
13681
13916
  */
13917
+ const ITEM_FAILED = "item failed; kept for inspection";
13682
13918
  const createWorkspace = (options) => {
13683
13919
  const { graph, runId, root, head, onEvent } = options;
13684
13920
  const rules = gitRulesOf(graph.workflow);
@@ -13826,6 +14062,8 @@ const createWorkspace = (options) => {
13826
14062
  }
13827
14063
  const next = await settle(id, id, worktree, result);
13828
14064
  outputs.set(id, next.output);
14065
+ if (!next.ok && templateOf.has(id) && !unmerged.has(id)) unmerged.set(id, ITEM_FAILED);
14066
+ if (next.ok && unmerged.get(id) === ITEM_FAILED) unmerged.delete(id);
13829
14067
  const owner = ownerOf(id);
13830
14068
  const ownerTree = owner === void 0 ? void 0 : worktrees.get(owner);
13831
14069
  if (!next.ok || owner === void 0 || ownerTree === void 0) return next;
@@ -13843,17 +14081,7 @@ const createWorkspace = (options) => {
13843
14081
  const files = outcome.conflicts.join(", ");
13844
14082
  unmerged.set(id, `merge conflict${files === "" ? "" : ` in ${files}`}`);
13845
14083
  event(id, "conflict", worktree, files === "" ? outcome.error : files);
13846
- return {
13847
- ...next,
13848
- output: {
13849
- ...next.output,
13850
- merge: {
13851
- ok: false,
13852
- conflicts: outcome.conflicts,
13853
- into: nameOf(ownerTree)
13854
- }
13855
- }
13856
- };
14084
+ return withConflict(next, outcome.conflicts, nameOf(ownerTree));
13857
14085
  };
13858
14086
  const close = () => closeWorktrees({
13859
14087
  root,
@@ -13873,4 +14101,4 @@ const createWorkspace = (options) => {
13873
14101
  close
13874
14102
  };
13875
14103
  };
13876
- export { adoptedCalls as $, isClaudeNode as $n, evaluate as $t, gitOk as A, projectRootOf as An, declaredProblem as At, peerOf as B, readDocument as Bn, serversIn as Bt, workflowTextHash as C, placeFiles as Cn, runTree as Ct, inspectProject as D, shownPath as Dn, stillTheSameProcess as Dt, BRANCH_PREFIX as E, runDirOf as En, processAlive as Et, createCalls as F, writePlainFile as Fn, PROJECT_FILE as Ft, requestPause as G, serializeWorkflow as Gn, planPost as Gt, unlinkNodes as H, programsRunBy as Hn, FEEDBACK_SEVERITIES as Ht, addressOf as I, skillsIn as In, gitRulesOf as It, backfillUsage as J, WorkflowSchema as Jn, publishPost as Jt, requestStop as K, markValue as Kn, runGh as Kt, endLink as L, WorkflowEditSchema as Ln, loadProject as Lt, clearGitLocalEnv as M, estimateTokens as Mn, envReadsOf as Mt, parseInputPairs as N, guardedBase as Nn, nodeSays as Nt, removeWorktree as O, workflowDirOf as On, runScope as Ot, resolveInputs as P, plainPathInside as Pn, declaredOutputOf as Pt, text as Q, NodeSchema as Qn, targetOf as Qt, endLinksOf as R, applyEdits as Rn, MCP_FILE as Rt, readProjectTriggers as S, newProjectId as Sn, runStatus as St, watchingPid as T, projectsHome as Tn, ownProcessStart as Tt, flattenTree as U, renderJsonSchema as Un, FEEDBACK_TARGETS as Ut, tellNode as V, beforeOf as Vn, FEEDBACK_KINDS as Vt, requestContinue as W, parseWorkflow as Wn, DISCUSSIONS as Wt, eventOf as X, expandEdge as Xn, fingerprint as Xt, usageQueries as Y, CheckReportSchema as Yn, createFeedback as Yt, inTransaction as Z, settingsOf as Zn, normalizeTitle as Zt, lateTriggers as _, catalogAgent as _n, MESSAGE_LIMIT as _t, limitsNow as a, socketRoom as an, integrationFor as ar, followEvents as at, triggerStanding as b, hasInRepoFiles as bn, liveStatus as bt, usd as c, maskSecrets as cn, CLAUDE_DRIVERS as cr, planNodeSession as ct, runningRunIds as d, planMoveInto as dn, experimentsOn as dr, parseReport as dt, parseTemplate as en, isToolNode as er, detachResume as et, runningScript as f, linkTools as fn, knownExperiments as fr, reportSchemaFor as ft, parseVersion as g, deleteAgent as gn, startRun as gt, isNewer as h, createAgent as hn, walkSummary as ht, NOT_STARTED as i, socketPath as in, TOOL_PREFIX as ir, detachRun as it, lines as j, LESSON_CAP_TOKENS as jn, calledWorkflows as jt, git as k, inside as kn, scriptContactsOf as kt, planUpgrade as l, planAgentMigration as ln, firstLine as lr, namingNoDriver as lt, compareVersions as m, toolsInUse as mn, walkChecks as mt, resolveBase as n, readCondition as nn, timeoutMs as nr, listKey as nt, limitsReport as o, wakeRun as on, AGENT_TOOL_LAYOUTS as or, explainWorkflowText as ot, detectInstaller as p, toolLinks as pn, checkWorkflowText as pt, tellLiveRun as q, EdgeInputSchema as qn, sendPost as qt, capsHere as r, serveRun as rn, INTEGRATIONS as rr, DETACHED_ENV as rt, planReason as s, maskReplacer as sn, ORK_LAYOUT as sr, explainSession as st, createWorkspace as t, renderTemplate as tn, NodeIdSchema as tr, resumable as tt, releaseNotes as u, applySteps as un, EXPERIMENTS as ur, sessionDriver as ut, triggersReport as v, loadAgents as vn, userMessageText as vt, takeWatchLock as w, placeFor as wn, templateOf as wt, declaredTriggers as x, inRepoPlace as xn, pausedOf as xt, createWatcher as y, PROJECT_MODES as yn, heldTimeout as yt, linkNodes as z, newWorkflowText as zn, mcpNotText as zt };
14104
+ export { adoptedCalls as $, settingsOf as $n, normalizeTitle as $t, gitOk as A, workflowDirOf as An, runScope as At, peerOf as B, applyEdits as Bn, MCP_FILE as Bt, workflowTextHash as C, inRepoPlace as Cn, runTree as Ct, inspectProject as D, projectsHome as Dn, stillTheSameProcess as Dt, BRANCH_PREFIX as E, placeFor as En, processAlive as Et, createCalls as F, guardedBase as Fn, nodeSays as Ft, requestPause as G, renderJsonSchema as Gn, FEEDBACK_TARGETS as Gt, unlinkNodes as H, readDocument as Hn, serversIn as Ht, addressOf as I, plainPathInside as In, declaredOutputOf as It, backfillUsage as J, markValue as Jn, runGh as Jt, requestStop as K, parseWorkflow as Kn, DISCUSSIONS as Kt, endLink as L, writePlainFile as Ln, PROJECT_FILE as Lt, clearGitLocalEnv as M, projectRootOf as Mn, declaredProblem as Mt, parseInputPairs as N, LESSON_CAP_TOKENS as Nn, calledWorkflows as Nt, removeWorktree as O, runDirOf as On, isItem as Ot, resolveInputs as P, estimateTokens as Pn, envReadsOf as Pt, text as Q, expandEdge as Qn, fingerprint as Qt, endLinksOf as R, skillsIn as Rn, gitRulesOf as Rt, readProjectTriggers as S, hasInRepoFiles as Sn, runStatus as St, watchingPid as T, placeFiles as Tn, ownProcessStart as Tt, flattenTree as U, beforeOf as Un, FEEDBACK_KINDS as Ut, tellNode as V, newWorkflowText as Vn, mcpNotText as Vt, requestContinue as W, programsRunBy as Wn, FEEDBACK_SEVERITIES as Wt, eventOf as X, WorkflowSchema as Xn, publishPost as Xt, usageQueries as Y, EdgeInputSchema as Yn, sendPost as Yt, inTransaction as Z, CheckReportSchema as Zn, createFeedback as Zt, lateTriggers as _, createAgent as _n, MESSAGE_LIMIT as _t, limitsNow as a, serveRun as an, INTEGRATIONS as ar, followEvents as at, triggerStanding as b, loadAgents as bn, liveStatus as bt, usd as c, wakeRun as cn, AGENT_TOOL_LAYOUTS as cr, planNodeSession as ct, runningRunIds as d, planAgentMigration as dn, firstLine as dr, parseReport as dt, targetOf as en, NodeSchema as er, detachResume as et, runningScript as f, applySteps as fn, EXPERIMENTS as fr, reportSchemaFor as ft, parseVersion as g, toolsInUse as gn, startRun as gt, isNewer as h, toolLinks as hn, walkSummary as ht, NOT_STARTED as i, readCondition as in, timeoutMs as ir, detachRun as it, lines as j, inside as jn, scriptContactsOf as jt, git as k, shownPath as kn, nestItems as kt, planUpgrade as l, maskReplacer as ln, ORK_LAYOUT as lr, namingNoDriver as lt, compareVersions as m, linkTools as mn, knownExperiments as mr, walkChecks as mt, resolveBase as n, parseTemplate as nn, isToolNode as nr, listKey as nt, limitsReport as o, socketPath as on, TOOL_PREFIX as or, explainWorkflowText as ot, detectInstaller as p, planMoveInto as pn, experimentsOn as pr, checkWorkflowText as pt, tellLiveRun as q, serializeWorkflow as qn, planPost as qt, capsHere as r, renderTemplate as rn, NodeIdSchema as rr, DETACHED_ENV as rt, planReason as s, socketRoom as sn, integrationFor as sr, explainSession as st, createWorkspace as t, evaluate as tn, isClaudeNode as tr, resumable as tt, releaseNotes as u, maskSecrets as un, CLAUDE_DRIVERS as ur, sessionDriver as ut, triggersReport as v, deleteAgent as vn, userMessageText as vt, takeWatchLock as w, newProjectId as wn, templateOf as wt, declaredTriggers as x, PROJECT_MODES as xn, pausedOf as xt, createWatcher as y, catalogAgent as yn, heldTimeout as yt, linkNodes as z, WorkflowEditSchema as zn, loadProject as zt };