@krapht/ork 1.0.0-alpha.8 → 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.
- package/dist/{__build_info__-B83zdwIv.mjs → __build_info__-y1TPvZvp.mjs} +2 -2
- package/dist/bin.mjs +597 -50
- package/dist/{clean-NtapkN2x.mjs → clean-BRUXyk87.mjs} +5 -2
- package/dist/{completion-DAQMrf1u.mjs → completion-cpZHtv50.mjs} +5 -5
- package/dist/{context-C5wQNjp9.mjs → context-Dbay8l7P.mjs} +8 -7
- package/dist/{decide-BaEOo7sO.mjs → decide-BphP3ONM.mjs} +1 -1
- package/dist/{find-DDAkEoda.mjs → find-BXZjliwr.mjs} +1 -1
- package/dist/format-DeT7vL-8.mjs +191 -0
- package/dist/{in-a-run-DENitTUe.mjs → in-a-run-C4E9HfED.mjs} +1 -1
- package/dist/{limits-DezRXN0Z.mjs → limits-CBeoYrUH.mjs} +1 -1
- package/dist/{list-CZTh_wdE.mjs → list-BmxgNyd3.mjs} +11 -4
- package/dist/{mcp-B2DW26HO.mjs → mcp-DTreb7Nq.mjs} +6 -6
- package/dist/{output-D1wMNIly.mjs → output-BVkI6ZVv.mjs} +1 -1
- package/dist/{pause-HuQhHyET.mjs → pause-h66SC4rV.mjs} +4 -4
- package/dist/{relay-DCGGYSdz.mjs → relay-BxRhxL6n.mjs} +2 -2
- package/dist/{remove-0L02cre2.mjs → remove-CYGd9Csj.mjs} +14 -7
- package/dist/{send-C8pQfhED.mjs → send-BskOwN4p.mjs} +3 -3
- package/dist/{src-DosUvy4B.mjs → src-BwFxTank.mjs} +291 -100
- package/dist/{src-COHxAj7X.mjs → src-W1i5Bl6N.mjs} +23 -9
- package/dist/{src-BNhCiDQw.mjs → src-bOImrq43.mjs} +30 -11
- package/dist/{src-DCCbIABO.mjs → src-xd9DbQw8.mjs} +7 -7
- package/dist/ui/assets/{app-DBYZFrdE.js → app-BMg0OOv4.js} +19 -19
- package/dist/ui/assets/app-C8uhHVYp.css +1 -0
- package/dist/ui/assets/{index-DXoKpYjA.js → index-DY1q4SDl.js} +2 -2
- package/dist/ui/index.html +1 -1
- package/dist/{ui-Cdb05xJw.mjs → ui-B46Z3e9E.mjs} +4 -4
- package/dist/{usage-jYI2QHt7.mjs → usage-DpHXbQEu.mjs} +5 -5
- package/dist/{watch-D4lFXnHT.mjs → watch-CJO1ee6Y.mjs} +6 -6
- package/dist/{workflows-BF9fO-ER.mjs → workflows-BJdSHnkr.mjs} +4 -3
- package/dist/{worktrees-0iUiA6eD.mjs → worktrees-Ccf2lUhE.mjs} +72 -19
- package/package.json +1 -1
- package/dist/format-sGEQmQid.mjs +0 -643
- package/dist/ui/assets/app-D14lfYns.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({
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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 } = {}) =>
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
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
|
-
|
|
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
|
|
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/
|
|
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
|
-
|
|
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 &&
|
|
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,
|
|
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
|
-
|
|
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;
|
|
@@ -7332,10 +7483,12 @@ const createFanOut = (deps) => {
|
|
|
7332
7483
|
node: id,
|
|
7333
7484
|
round,
|
|
7334
7485
|
count: list.length,
|
|
7335
|
-
list
|
|
7486
|
+
list,
|
|
7487
|
+
...recorded === null && options.replay !== void 0 ? { anew: true } : {}
|
|
7336
7488
|
});
|
|
7337
7489
|
};
|
|
7338
7490
|
recordList();
|
|
7491
|
+
const signal = stops.signalFor(id);
|
|
7339
7492
|
if ("maxItems" in forEach && list.length > forEach.maxItems) {
|
|
7340
7493
|
options.onEvent({
|
|
7341
7494
|
type: "each.exceeded",
|
|
@@ -7344,14 +7497,14 @@ const createFanOut = (deps) => {
|
|
|
7344
7497
|
count: list.length,
|
|
7345
7498
|
maxItems: forEach.maxItems
|
|
7346
7499
|
});
|
|
7347
|
-
const chosen = await deps.loops.overList(id, list, forEach.maxItems, round);
|
|
7500
|
+
const chosen = await deps.loops.overList(id, list, forEach.maxItems, round, signal);
|
|
7348
7501
|
if (chosen === null) {
|
|
7349
7502
|
const reason = `the list of ${id} has ${String(list.length)} items, over maxItems ${String(forEach.maxItems)}`;
|
|
7350
|
-
return {
|
|
7503
|
+
return stops.ended(id, {
|
|
7351
7504
|
...failure(reason),
|
|
7352
7505
|
limited: reason,
|
|
7353
7506
|
stopsAs: "failed"
|
|
7354
|
-
};
|
|
7507
|
+
});
|
|
7355
7508
|
}
|
|
7356
7509
|
const narrowed = chosen.length !== list.length;
|
|
7357
7510
|
list = chosen;
|
|
@@ -7361,7 +7514,6 @@ const createFanOut = (deps) => {
|
|
|
7361
7514
|
const session = node !== void 0 && needsSlot(node);
|
|
7362
7515
|
const spends = node !== void 0 && spendsBudget(node);
|
|
7363
7516
|
const declared = node === void 0 ? void 0 : declaredOutputOf(node);
|
|
7364
|
-
const signal = stops.signalFor(id);
|
|
7365
7517
|
const results = list.map(() => ({}));
|
|
7366
7518
|
const statuses = list.map(() => "skipped");
|
|
7367
7519
|
const failures = [];
|
|
@@ -7383,7 +7535,7 @@ const createFanOut = (deps) => {
|
|
|
7383
7535
|
},
|
|
7384
7536
|
round
|
|
7385
7537
|
});
|
|
7386
|
-
results[index] = entryOf(declared, result
|
|
7538
|
+
results[index] = entryOf(declared, result);
|
|
7387
7539
|
statuses[index] = statusOf(result);
|
|
7388
7540
|
if (!result.ok) failures.push(`${itemId}: ${result.error ?? "failed"}`);
|
|
7389
7541
|
else if (mergeFailed(result)) failures.push(`${itemId}: its work could not be merged`);
|
|
@@ -7619,6 +7771,10 @@ const nodeContext = (deps, activation, cwd, round) => {
|
|
|
7619
7771
|
...options.calls === void 0 ? {} : { calls: options.calls },
|
|
7620
7772
|
emit: (event) => {
|
|
7621
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
|
+
}
|
|
7622
7778
|
options.onEvent({
|
|
7623
7779
|
type: "node.event",
|
|
7624
7780
|
id,
|
|
@@ -9375,6 +9531,7 @@ const startRun = (options) => {
|
|
|
9375
9531
|
loops
|
|
9376
9532
|
})
|
|
9377
9533
|
});
|
|
9534
|
+
const gather = createGatherer(outputs, options.replay, nodeOf);
|
|
9378
9535
|
/** Runs one activation and records it; routing is the caller's. */
|
|
9379
9536
|
const perform = async (activation) => {
|
|
9380
9537
|
const { id } = activation;
|
|
@@ -9387,8 +9544,9 @@ const startRun = (options) => {
|
|
|
9387
9544
|
const agent = graph.agents.get(activation.template ?? id);
|
|
9388
9545
|
onEvent(startedEvent(activation, type, round, agent));
|
|
9389
9546
|
}
|
|
9390
|
-
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));
|
|
9391
9548
|
if (result.limited !== void 0) stop(result.stopsAs ?? "quota_exhausted", result.limited);
|
|
9549
|
+
spend.settled(id);
|
|
9392
9550
|
holds.finished(id);
|
|
9393
9551
|
ports.finished(id);
|
|
9394
9552
|
clock.leave(id);
|
|
@@ -11208,10 +11366,11 @@ const mailOf = (events) => {
|
|
|
11208
11366
|
* (ADR-026). Each node's recorded results, in order, up to its first that
|
|
11209
11367
|
* failed or never finished, are handed back by the replay, so the scheduler
|
|
11210
11368
|
* rebuilds outputs, rounds, joins and loop counts without running them; the
|
|
11211
|
-
* rest run again, and so does everything a new result feeds.
|
|
11212
|
-
*
|
|
11213
|
-
*
|
|
11214
|
-
*
|
|
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.
|
|
11215
11374
|
*/
|
|
11216
11375
|
const listKey = (node, round) => `${node}/${String(round)}`;
|
|
11217
11376
|
/** How a run ended that cut its working nodes off, rather than failing them. */
|
|
@@ -11221,6 +11380,8 @@ const CUTTING = /* @__PURE__ */ new Set([
|
|
|
11221
11380
|
"quota_exhausted",
|
|
11222
11381
|
"timed_out"
|
|
11223
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));
|
|
11224
11385
|
/** Reads a run's events back into what `ork resume` needs. */
|
|
11225
11386
|
const resumeState = (events) => {
|
|
11226
11387
|
const history = /* @__PURE__ */ new Map();
|
|
@@ -11243,18 +11404,36 @@ const resumeState = (events) => {
|
|
|
11243
11404
|
let ended;
|
|
11244
11405
|
/** Nodes whose last activation did not finish well, and its session. */
|
|
11245
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
|
+
};
|
|
11246
11420
|
for (const event of events) {
|
|
11247
11421
|
if (event.type === "run.started") {
|
|
11248
11422
|
inputs = event.inputs ?? inputs;
|
|
11249
11423
|
settings = event.settings ?? settings;
|
|
11424
|
+
closed.clear();
|
|
11250
11425
|
continue;
|
|
11251
11426
|
}
|
|
11252
11427
|
if (event.type === "node.started") {
|
|
11253
|
-
if (event.owner === void 0)
|
|
11428
|
+
if (event.owner === void 0) {
|
|
11429
|
+
open.set(event.id, event.item === void 0 ? {} : { item: true });
|
|
11430
|
+
supersede(event.id, event.round);
|
|
11431
|
+
}
|
|
11254
11432
|
continue;
|
|
11255
11433
|
}
|
|
11256
11434
|
if (event.type === "each.listed") {
|
|
11257
11435
|
fanOuts.lists.set(listKey(event.node, event.round), event.list);
|
|
11436
|
+
if (event.anew === true) forgetItems(event.node, event.round);
|
|
11258
11437
|
continue;
|
|
11259
11438
|
}
|
|
11260
11439
|
if (event.type === "node.event") {
|
|
@@ -11274,6 +11453,7 @@ const resumeState = (events) => {
|
|
|
11274
11453
|
if (current.sessionId !== void 0) unfinished.set(event.id, current.sessionId);
|
|
11275
11454
|
continue;
|
|
11276
11455
|
}
|
|
11456
|
+
unfinished.delete(event.id);
|
|
11277
11457
|
const result = {
|
|
11278
11458
|
ok: true,
|
|
11279
11459
|
output: event.output
|
|
@@ -13479,6 +13659,13 @@ const syncFrom = async (worktree, from) => {
|
|
|
13479
13659
|
return snapshot(worktree);
|
|
13480
13660
|
};
|
|
13481
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
|
+
};
|
|
13482
13669
|
/**
|
|
13483
13670
|
* The one place `id`'s upstream nodes worked in other than `root`, or
|
|
13484
13671
|
* `undefined` when there is none or they disagree. `placeOf` is where each node
|
|
@@ -13497,8 +13684,7 @@ const upstreamPlace = (graph, id, placeOf, root) => {
|
|
|
13497
13684
|
queue.push(...(node?.incoming ?? []).map((e) => e.from));
|
|
13498
13685
|
continue;
|
|
13499
13686
|
}
|
|
13500
|
-
const place
|
|
13501
|
-
if (place !== void 0 && place !== root) places.add(place);
|
|
13687
|
+
for (const place of placesOf(from, placeOf)) if (place !== root) places.add(place);
|
|
13502
13688
|
}
|
|
13503
13689
|
const [only, other] = places;
|
|
13504
13690
|
return other === void 0 ? only : void 0;
|
|
@@ -13704,6 +13890,18 @@ const closeWorktrees = async (options) => {
|
|
|
13704
13890
|
}
|
|
13705
13891
|
return kept;
|
|
13706
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
|
+
});
|
|
13707
13905
|
// ../core/src/worktree/workspace.ts
|
|
13708
13906
|
/**
|
|
13709
13907
|
* @file Where each node of a run works (ADR-020), under the project's git rules
|
|
@@ -13716,6 +13914,7 @@ const closeWorktrees = async (options) => {
|
|
|
13716
13914
|
* its owner: a merge, a patch, or not at all. A spawned worker works like the
|
|
13717
13915
|
* declared child it is an instance of.
|
|
13718
13916
|
*/
|
|
13917
|
+
const ITEM_FAILED = "item failed; kept for inspection";
|
|
13719
13918
|
const createWorkspace = (options) => {
|
|
13720
13919
|
const { graph, runId, root, head, onEvent } = options;
|
|
13721
13920
|
const rules = gitRulesOf(graph.workflow);
|
|
@@ -13863,6 +14062,8 @@ const createWorkspace = (options) => {
|
|
|
13863
14062
|
}
|
|
13864
14063
|
const next = await settle(id, id, worktree, result);
|
|
13865
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);
|
|
13866
14067
|
const owner = ownerOf(id);
|
|
13867
14068
|
const ownerTree = owner === void 0 ? void 0 : worktrees.get(owner);
|
|
13868
14069
|
if (!next.ok || owner === void 0 || ownerTree === void 0) return next;
|
|
@@ -13880,17 +14081,7 @@ const createWorkspace = (options) => {
|
|
|
13880
14081
|
const files = outcome.conflicts.join(", ");
|
|
13881
14082
|
unmerged.set(id, `merge conflict${files === "" ? "" : ` in ${files}`}`);
|
|
13882
14083
|
event(id, "conflict", worktree, files === "" ? outcome.error : files);
|
|
13883
|
-
return
|
|
13884
|
-
...next,
|
|
13885
|
-
output: {
|
|
13886
|
-
...next.output,
|
|
13887
|
-
merge: {
|
|
13888
|
-
ok: false,
|
|
13889
|
-
conflicts: outcome.conflicts,
|
|
13890
|
-
into: nameOf(ownerTree)
|
|
13891
|
-
}
|
|
13892
|
-
}
|
|
13893
|
-
};
|
|
14084
|
+
return withConflict(next, outcome.conflicts, nameOf(ownerTree));
|
|
13894
14085
|
};
|
|
13895
14086
|
const close = () => closeWorktrees({
|
|
13896
14087
|
root,
|