botmux-workflow-core 3.10.0
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/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/cjs/control.cjs +80 -0
- package/dist/cjs/control.cjs.map +7 -0
- package/dist/cjs/engine.cjs +380 -0
- package/dist/cjs/engine.cjs.map +7 -0
- package/dist/cjs/events.cjs +19 -0
- package/dist/cjs/events.cjs.map +7 -0
- package/dist/cjs/gate-policy.cjs +55 -0
- package/dist/cjs/gate-policy.cjs.map +7 -0
- package/dist/cjs/host-bindings.cjs +285 -0
- package/dist/cjs/host-bindings.cjs.map +7 -0
- package/dist/cjs/host-contract.cjs +19 -0
- package/dist/cjs/host-contract.cjs.map +7 -0
- package/dist/cjs/index.cjs +1523 -0
- package/dist/cjs/index.cjs.map +7 -0
- package/dist/cjs/runtime.cjs +6393 -0
- package/dist/cjs/runtime.cjs.map +7 -0
- package/dist/cjs/schema.cjs +1190 -0
- package/dist/cjs/schema.cjs.map +7 -0
- package/dist/esm/control.js +52 -0
- package/dist/esm/control.js.map +7 -0
- package/dist/esm/engine.js +352 -0
- package/dist/esm/engine.js.map +7 -0
- package/dist/esm/events.js +1 -0
- package/dist/esm/events.js.map +7 -0
- package/dist/esm/gate-policy.js +26 -0
- package/dist/esm/gate-policy.js.map +7 -0
- package/dist/esm/host-bindings.js +252 -0
- package/dist/esm/host-bindings.js.map +7 -0
- package/dist/esm/host-contract.js +1 -0
- package/dist/esm/host-contract.js.map +7 -0
- package/dist/esm/index.js +1481 -0
- package/dist/esm/index.js.map +7 -0
- package/dist/esm/runtime.js +6426 -0
- package/dist/esm/runtime.js.map +7 -0
- package/dist/esm/schema.js +1140 -0
- package/dist/esm/schema.js.map +7 -0
- package/dist/types/packages/workflow-core/src/control.d.ts +1 -0
- package/dist/types/packages/workflow-core/src/engine.d.ts +1 -0
- package/dist/types/packages/workflow-core/src/events.d.ts +1 -0
- package/dist/types/packages/workflow-core/src/gate-policy.d.ts +1 -0
- package/dist/types/packages/workflow-core/src/host-bindings.d.ts +1 -0
- package/dist/types/packages/workflow-core/src/host-contract.d.ts +1 -0
- package/dist/types/packages/workflow-core/src/index.d.ts +1 -0
- package/dist/types/packages/workflow-core/src/runtime.d.ts +7 -0
- package/dist/types/packages/workflow-core/src/schema.d.ts +1 -0
- package/dist/types/src/workflows/v3/artifact-contract.d.ts +40 -0
- package/dist/types/src/workflows/v3/core-control.d.ts +15 -0
- package/dist/types/src/workflows/v3/dag.d.ts +341 -0
- package/dist/types/src/workflows/v3/event-contract.d.ts +272 -0
- package/dist/types/src/workflows/v3/gate-policy.d.ts +11 -0
- package/dist/types/src/workflows/v3/host-bindings.d.ts +39 -0
- package/dist/types/src/workflows/v3/in-process-attempt-lease.d.ts +9 -0
- package/dist/types/src/workflows/v3/orchestrator.d.ts +174 -0
- package/dist/types/src/workflows/v3/portable-final-outputs.d.ts +36 -0
- package/dist/types/src/workflows/v3/portable-runtime.d.ts +82 -0
- package/dist/types/src/workflows/v3/runtime-host-contract.d.ts +181 -0
- package/dist/types/src/workflows/v3/shared-runtime.d.ts +16 -0
- package/package.json +83 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
// src/workflows/v3/dag.ts
|
|
2
|
+
function isLoopNode(node) {
|
|
3
|
+
return node.type === "loop";
|
|
4
|
+
}
|
|
5
|
+
function loopInstanceId(loopId, iteration, bodyNodeId) {
|
|
6
|
+
return `${loopId}.i${String(iteration).padStart(3, "0")}.${bodyNodeId}`;
|
|
7
|
+
}
|
|
8
|
+
var DagValidationError = class extends Error {
|
|
9
|
+
constructor(problems) {
|
|
10
|
+
super(`Invalid v3 dag.json:
|
|
11
|
+
- ${problems.join("\n - ")}`);
|
|
12
|
+
this.problems = problems;
|
|
13
|
+
this.name = "DagValidationError";
|
|
14
|
+
}
|
|
15
|
+
problems;
|
|
16
|
+
};
|
|
17
|
+
function topologicalOrder(dag) {
|
|
18
|
+
const indeg = /* @__PURE__ */ new Map();
|
|
19
|
+
const adj = /* @__PURE__ */ new Map();
|
|
20
|
+
for (const node of dag.nodes) {
|
|
21
|
+
indeg.set(node.id, indeg.get(node.id) ?? 0);
|
|
22
|
+
if (!adj.has(node.id)) adj.set(node.id, []);
|
|
23
|
+
}
|
|
24
|
+
for (const node of dag.nodes) {
|
|
25
|
+
for (const dep of node.depends) {
|
|
26
|
+
indeg.set(node.id, (indeg.get(node.id) ?? 0) + 1);
|
|
27
|
+
adj.get(dep.from).push(node.id);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const ready = [...indeg.entries()].filter(([, d]) => d === 0).map(([id]) => id).sort();
|
|
31
|
+
const order = [];
|
|
32
|
+
while (ready.length > 0) {
|
|
33
|
+
const id = ready.shift();
|
|
34
|
+
order.push(id);
|
|
35
|
+
for (const next of adj.get(id) ?? []) {
|
|
36
|
+
const d = indeg.get(next) - 1;
|
|
37
|
+
indeg.set(next, d);
|
|
38
|
+
if (d === 0) {
|
|
39
|
+
const pos = lowerBound(ready, next);
|
|
40
|
+
ready.splice(pos, 0, next);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (order.length !== dag.nodes.length) {
|
|
45
|
+
const stuck = dag.nodes.map((n) => n.id).filter((id) => !order.includes(id));
|
|
46
|
+
throw new DagValidationError([`dag has a cycle among nodes: ${stuck.join(", ")}`]);
|
|
47
|
+
}
|
|
48
|
+
return order;
|
|
49
|
+
}
|
|
50
|
+
function lowerBound(arr, x) {
|
|
51
|
+
let lo = 0;
|
|
52
|
+
let hi = arr.length;
|
|
53
|
+
while (lo < hi) {
|
|
54
|
+
const mid = lo + hi >> 1;
|
|
55
|
+
if (arr[mid] < x) lo = mid + 1;
|
|
56
|
+
else hi = mid;
|
|
57
|
+
}
|
|
58
|
+
return lo;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/workflows/v3/orchestrator.ts
|
|
62
|
+
function decideNext(dag, state, loops = /* @__PURE__ */ new Map(), edges = /* @__PURE__ */ new Map(), instances = /* @__PURE__ */ new Map()) {
|
|
63
|
+
const order = topologicalOrder(dag);
|
|
64
|
+
const nodes = new Map(dag.nodes.map((n) => [n.id, n]));
|
|
65
|
+
for (const id of order) {
|
|
66
|
+
const node = nodes.get(id);
|
|
67
|
+
for (const sweepId of [id, ...currentInstanceIds(node, loops)]) {
|
|
68
|
+
if (st(state, sweepId).status === "failed" && (sweepId !== id || isFailureRelevant(id, dag, state, edges))) {
|
|
69
|
+
return [{ kind: "completeRunFailed", failedNodeId: sweepId }];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const id of order) {
|
|
74
|
+
const node = nodes.get(id);
|
|
75
|
+
for (const sweepId of [id, ...currentInstanceIds(node, loops)]) {
|
|
76
|
+
if (st(state, sweepId).status === "blocked" && (sweepId !== id || isFailureRelevant(id, dag, state, edges))) {
|
|
77
|
+
return [{ kind: "completeRunBlocked", blockedNodeId: sweepId }];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const actions = [];
|
|
82
|
+
let pending = 0;
|
|
83
|
+
for (const id of order) {
|
|
84
|
+
const node = nodes.get(id);
|
|
85
|
+
const s = st(state, id);
|
|
86
|
+
if (isAcceptableTerminal(id, s.status, dag, state, edges)) continue;
|
|
87
|
+
if (isLoopNode(node)) {
|
|
88
|
+
pending++;
|
|
89
|
+
const ls = loops.get(id);
|
|
90
|
+
if (!ls) {
|
|
91
|
+
const readiness2 = readinessFor(node, state, edges);
|
|
92
|
+
if (readiness2.kind === "wait") continue;
|
|
93
|
+
if (readiness2.kind === "resolveEdge") {
|
|
94
|
+
actions.push({ kind: "resolveEdge", from: readiness2.from, to: id });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (readiness2.kind === "skip") {
|
|
98
|
+
actions.push({ kind: "skipNode", nodeId: id, detail: readiness2.detail });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
actions.push({ kind: "startLoop", loopId: id });
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (ls.iteration === 0) {
|
|
105
|
+
actions.push({ kind: "startLoopIteration", loopId: id, iteration: 1 });
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (ls.decided) {
|
|
109
|
+
if (ls.lastDecision === "exit") {
|
|
110
|
+
actions.push({ kind: "completeLoop", loopId: id, iteration: ls.iteration });
|
|
111
|
+
} else if (ls.lastDecision === "continue" || // After an exhausted-block, a grant re-opens exactly one round. An
|
|
112
|
+
// exhausted loop WITHOUT a pending grant never reaches here — its
|
|
113
|
+
// node status is 'blocked' and the sweep above already returned.
|
|
114
|
+
ls.lastDecision === "exhausted" && ls.pendingGrant) {
|
|
115
|
+
actions.push({ kind: "startLoopIteration", loopId: id, iteration: ls.iteration + 1 });
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const bodyOrder = topologicalOrder({ runId: id, nodes: node.body.nodes });
|
|
120
|
+
const bodyById = new Map(node.body.nodes.map((b) => [b.id, b]));
|
|
121
|
+
let allDone = true;
|
|
122
|
+
for (const bodyId of bodyOrder) {
|
|
123
|
+
const instId = loopInstanceId(id, ls.iteration, bodyId);
|
|
124
|
+
const bs = st(state, instId);
|
|
125
|
+
if (bs.status === "done") continue;
|
|
126
|
+
allDone = false;
|
|
127
|
+
if (bs.status === "running" || bs.status === "gateWaiting") continue;
|
|
128
|
+
const bodyDef = bodyById.get(bodyId);
|
|
129
|
+
const depsOk = bodyDef.depends.every(
|
|
130
|
+
(dep) => st(state, loopInstanceId(id, ls.iteration, dep.from)).status === "done"
|
|
131
|
+
);
|
|
132
|
+
if (!depsOk) continue;
|
|
133
|
+
actions.push({
|
|
134
|
+
kind: "dispatchWork",
|
|
135
|
+
nodeId: instId,
|
|
136
|
+
loop: { loopId: id, iteration: ls.iteration, bodyNodeId: bodyId }
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (allDone) {
|
|
140
|
+
actions.push({ kind: "evaluateLoopIteration", loopId: id, iteration: ls.iteration });
|
|
141
|
+
}
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (s.status === "running" || s.status === "gateWaiting") {
|
|
145
|
+
pending++;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
pending++;
|
|
149
|
+
const readiness = readinessFor(node, state, edges);
|
|
150
|
+
if (readiness.kind === "wait") continue;
|
|
151
|
+
if (readiness.kind === "resolveEdge") {
|
|
152
|
+
actions.push({ kind: "resolveEdge", from: readiness.from, to: id });
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (readiness.kind === "skip") {
|
|
156
|
+
actions.push({ kind: "skipNode", nodeId: id, detail: readiness.detail });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (node.humanGate && !s.gateCleared) {
|
|
160
|
+
actions.push({ kind: "dispatchGate", nodeId: id, instanceId: s.effectiveInstanceId ?? nextInstanceId(id, instances) });
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const instanceId = s.effectiveInstanceId ?? nextInstanceId(id, instances);
|
|
164
|
+
actions.push({
|
|
165
|
+
kind: "dispatchWork",
|
|
166
|
+
nodeId: id,
|
|
167
|
+
instanceId,
|
|
168
|
+
...readiness.omitted ? { omitted: readiness.omitted } : {}
|
|
169
|
+
});
|
|
170
|
+
for (const loser of readiness.earlyLosers ?? []) {
|
|
171
|
+
if (canCancelLoser(loser, id, dag, state, edges)) {
|
|
172
|
+
actions.push({
|
|
173
|
+
kind: "cancelNode",
|
|
174
|
+
nodeId: loser,
|
|
175
|
+
byNodeId: id,
|
|
176
|
+
detail: `early-release loser for "${id}"`
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (actions.length === 0 && pending === 0) {
|
|
182
|
+
const sinks = findSinks(dag);
|
|
183
|
+
if (sinks.some((id) => st(state, id).status === "done")) {
|
|
184
|
+
return [{ kind: "completeRunSucceeded" }];
|
|
185
|
+
}
|
|
186
|
+
return [{
|
|
187
|
+
kind: "completeRunFailed",
|
|
188
|
+
reason: "allSinksSkipped",
|
|
189
|
+
detail: sinkOmissionDetail(sinks, state)
|
|
190
|
+
}];
|
|
191
|
+
}
|
|
192
|
+
return actions;
|
|
193
|
+
}
|
|
194
|
+
function readinessFor(node, state, edges) {
|
|
195
|
+
if (node.depends.length === 0) return { kind: "ready" };
|
|
196
|
+
const activities = node.depends.map((dep) => {
|
|
197
|
+
const source = st(state, dep.from);
|
|
198
|
+
if (source.status === "done") {
|
|
199
|
+
if (!dep.when) return { kind: "active", from: dep.from };
|
|
200
|
+
const edge = edges.get(currentEdgeKey(dep.from, node.id, state));
|
|
201
|
+
if (!edge) return { kind: "unresolved", from: dep.from };
|
|
202
|
+
return edge.active ? { kind: "active", from: dep.from } : { kind: "inactive", from: dep.from, reason: "edgeInactive" };
|
|
203
|
+
}
|
|
204
|
+
if (source.status === "skipped") return { kind: "inactive", from: dep.from, reason: "sourceSkipped" };
|
|
205
|
+
if (source.status === "cancelled") return { kind: "inactive", from: dep.from, reason: "sourceCancelled" };
|
|
206
|
+
return { kind: "unsettled", from: dep.from };
|
|
207
|
+
});
|
|
208
|
+
const active = activities.filter((a) => a.kind === "active").length;
|
|
209
|
+
const triggerRule = node.triggerRule ?? "all_success";
|
|
210
|
+
const required = triggerRule === "all_success" ? node.depends.length : triggerRule === "one_success" ? 1 : triggerRule.quorum;
|
|
211
|
+
if (triggerRule === "all_success") {
|
|
212
|
+
if (activities.some((a) => a.kind === "unsettled")) return { kind: "wait" };
|
|
213
|
+
const unresolved = firstUnresolved(activities);
|
|
214
|
+
if (unresolved) return { kind: "resolveEdge", from: unresolved.from };
|
|
215
|
+
} else if (active < required) {
|
|
216
|
+
const unresolved = firstUnresolved(activities);
|
|
217
|
+
if (unresolved) return { kind: "resolveEdge", from: unresolved.from };
|
|
218
|
+
}
|
|
219
|
+
const maybe = activities.filter((a) => a.kind === "unsettled" || a.kind === "unresolved").length;
|
|
220
|
+
if (active >= required) {
|
|
221
|
+
const omitted = activities.filter((a) => a.kind !== "active").map((a) => ({
|
|
222
|
+
from: a.from,
|
|
223
|
+
reason: a.kind === "inactive" ? a.reason : "earlyRelease"
|
|
224
|
+
}));
|
|
225
|
+
const earlyLosers = activities.filter((a) => a.kind === "unsettled").map((a) => a.from);
|
|
226
|
+
return {
|
|
227
|
+
kind: "ready",
|
|
228
|
+
...omitted.length > 0 ? { omitted } : {},
|
|
229
|
+
...earlyLosers.length > 0 ? { earlyLosers } : {}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
if (active + maybe >= required) return { kind: "wait" };
|
|
233
|
+
const detail = activities.map((a, idx) => {
|
|
234
|
+
const dep = node.depends[idx];
|
|
235
|
+
if (a.kind === "active") return `${dep.from}:active`;
|
|
236
|
+
if (a.kind === "inactive") return `${dep.from}:inactive(${a.reason})`;
|
|
237
|
+
return `${dep.from}:${a.kind}`;
|
|
238
|
+
}).join(", ");
|
|
239
|
+
return { kind: "skip", detail: `triggerRule=${JSON.stringify(triggerRule)} unsatisfied; ${detail}` };
|
|
240
|
+
}
|
|
241
|
+
function firstUnresolved(activities) {
|
|
242
|
+
return activities.find((a) => a.kind === "unresolved");
|
|
243
|
+
}
|
|
244
|
+
function currentEdgeKey(from, to, state) {
|
|
245
|
+
const fromInst = st(state, from).effectiveInstanceId ?? from;
|
|
246
|
+
return `${fromInst}->${to}`;
|
|
247
|
+
}
|
|
248
|
+
function currentInstanceIds(node, loops) {
|
|
249
|
+
if (!isLoopNode(node)) return [];
|
|
250
|
+
const ls = loops.get(node.id);
|
|
251
|
+
if (!ls || ls.iteration === 0) return [];
|
|
252
|
+
return node.body.nodes.map((b) => loopInstanceId(node.id, ls.iteration, b.id));
|
|
253
|
+
}
|
|
254
|
+
function isAcceptableTerminal(id, status, dag, state, edges) {
|
|
255
|
+
if (status === "done" || status === "skipped" || status === "cancelled") return true;
|
|
256
|
+
if (status === "failed" || status === "blocked") return !isFailureRelevant(id, dag, state, edges);
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
function canCancelLoser(candidateId, byNodeId, dag, state, edges) {
|
|
260
|
+
const candidate = dag.nodes.find((n) => n.id === candidateId);
|
|
261
|
+
if (!candidate || isLoopNode(candidate)) return false;
|
|
262
|
+
const status = st(state, candidateId).status;
|
|
263
|
+
if (status !== "pending" && status !== "gateWaiting" && status !== "running") return false;
|
|
264
|
+
for (const downstream of downstreamsOf(candidateId, dag)) {
|
|
265
|
+
const ds = st(state, downstream.id).status;
|
|
266
|
+
if (terminalStatus(ds)) continue;
|
|
267
|
+
if (downstream.id === byNodeId) continue;
|
|
268
|
+
if (isSatisfiedWithoutSource(downstream, candidateId, state, edges)) continue;
|
|
269
|
+
if (isImpossibleNow(downstream, state, edges)) continue;
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
function isFailureRelevant(failedNodeId, dag, state, edges) {
|
|
275
|
+
const downstreams = downstreamsOf(failedNodeId, dag);
|
|
276
|
+
if (downstreams.length === 0) return true;
|
|
277
|
+
for (const downstream of downstreams) {
|
|
278
|
+
const ds = st(state, downstream.id).status;
|
|
279
|
+
if (terminalStatus(ds)) continue;
|
|
280
|
+
if (!isSatisfiedWithoutSource(downstream, failedNodeId, state, edges)) return true;
|
|
281
|
+
}
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
function isSatisfiedWithoutSource(node, ignoredSourceId, state, edges) {
|
|
285
|
+
const activities = node.depends.filter((dep) => dep.from !== ignoredSourceId).map((dep) => edgeActivityFor(dep.from, node.id, dep.when !== void 0, state, edges));
|
|
286
|
+
const active = activities.filter((a) => a.kind === "active").length;
|
|
287
|
+
return active >= requiredFor(node);
|
|
288
|
+
}
|
|
289
|
+
function isImpossibleNow(node, state, edges) {
|
|
290
|
+
const activities = node.depends.map((dep) => edgeActivityFor(dep.from, node.id, dep.when !== void 0, state, edges));
|
|
291
|
+
const active = activities.filter((a) => a.kind === "active").length;
|
|
292
|
+
const maybe = activities.filter((a) => a.kind === "unsettled" || a.kind === "unresolved").length;
|
|
293
|
+
return active + maybe < requiredFor(node);
|
|
294
|
+
}
|
|
295
|
+
function edgeActivityFor(from, to, conditional, state, edges) {
|
|
296
|
+
const source = st(state, from);
|
|
297
|
+
if (source.status === "done") {
|
|
298
|
+
if (!conditional) return { kind: "active", from };
|
|
299
|
+
const edge = edges.get(currentEdgeKey(from, to, state));
|
|
300
|
+
if (!edge) return { kind: "unresolved", from };
|
|
301
|
+
return edge.active ? { kind: "active", from } : { kind: "inactive", from, reason: "edgeInactive" };
|
|
302
|
+
}
|
|
303
|
+
if (source.status === "skipped") return { kind: "inactive", from, reason: "sourceSkipped" };
|
|
304
|
+
if (source.status === "cancelled") return { kind: "inactive", from, reason: "sourceCancelled" };
|
|
305
|
+
return { kind: "unsettled", from };
|
|
306
|
+
}
|
|
307
|
+
function requiredFor(node) {
|
|
308
|
+
const triggerRule = node.triggerRule ?? "all_success";
|
|
309
|
+
return triggerRule === "all_success" ? node.depends.length : triggerRule === "one_success" ? 1 : triggerRule.quorum;
|
|
310
|
+
}
|
|
311
|
+
function downstreamsOf(nodeId, dag) {
|
|
312
|
+
return dag.nodes.filter((n) => n.depends.some((dep) => dep.from === nodeId));
|
|
313
|
+
}
|
|
314
|
+
function terminalStatus(status) {
|
|
315
|
+
return status === "done" || status === "skipped" || status === "cancelled" || status === "failed" || status === "blocked" || // A superseded INSTANCE is settled (it won't change); the DEFINITION node
|
|
316
|
+
// re-dispatches under a fresh instance, but that is tracked at the node
|
|
317
|
+
// level (effectiveInstanceId), not by this per-instance status.
|
|
318
|
+
status === "superseded";
|
|
319
|
+
}
|
|
320
|
+
function sinkOmissionDetail(sinks, state) {
|
|
321
|
+
let skipped = 0;
|
|
322
|
+
let cancelled = 0;
|
|
323
|
+
for (const id of sinks) {
|
|
324
|
+
const status = st(state, id).status;
|
|
325
|
+
if (status === "skipped") skipped++;
|
|
326
|
+
if (status === "cancelled") cancelled++;
|
|
327
|
+
}
|
|
328
|
+
return `${skipped} skipped, ${cancelled} cancelled`;
|
|
329
|
+
}
|
|
330
|
+
function findSinks(dag) {
|
|
331
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
332
|
+
for (const node of dag.nodes) for (const dep of node.depends) referenced.add(dep.from);
|
|
333
|
+
return dag.nodes.map((n) => n.id).filter((id) => !referenced.has(id));
|
|
334
|
+
}
|
|
335
|
+
function st(state, id) {
|
|
336
|
+
return state.get(id) ?? { status: "pending" };
|
|
337
|
+
}
|
|
338
|
+
function nextInstanceId(nodeId, instances) {
|
|
339
|
+
const prefix = `${nodeId}#`;
|
|
340
|
+
let max = 0;
|
|
341
|
+
for (const instanceId of instances.keys()) {
|
|
342
|
+
if (!instanceId.startsWith(prefix)) continue;
|
|
343
|
+
const n = Number.parseInt(instanceId.slice(prefix.length), 10);
|
|
344
|
+
if (Number.isFinite(n) && n > max) max = n;
|
|
345
|
+
}
|
|
346
|
+
return `${prefix}${String(max + 1).padStart(3, "0")}`;
|
|
347
|
+
}
|
|
348
|
+
export {
|
|
349
|
+
decideNext,
|
|
350
|
+
findSinks
|
|
351
|
+
};
|
|
352
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/workflows/v3/dag.ts", "../../../../src/workflows/v3/orchestrator.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * v3 DAG definition \u2014 schema, loader, validator, topological order.\n *\n * The v3 runtime (LLM-driven workflow) loads a hand-written `dag.json`,\n * validates it, and walks it in topological order with deps gating. This\n * module is the *schema half* of the engine: pure data + validation. Node.js\n * filesystem loading lives in `dag-loader.ts`.\n *\n * Deliberately standalone from v0.2's `definition.ts` \u2014 v3 nodes are a much\n * smaller surface (goal / host, no loop / decision / fanout) and coupling the\n * two schemas would drag v0.2's complexity into the new engine. See\n * `docs/design/2026-06-01-v3-mvp-engine-split.md` \u00A73 for the authored shape.\n */\n\nimport { collectV3HostBindingRefs, V3HostBindingError } from './host-bindings.js';\n\n// \u2500\u2500\u2500 Schema \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * `goal` \u2014 an LLM node driven by the `botmux-goal` skill (single goal, one\n * ephemeral worker). `host` \u2014 a deterministic side-effect node (feishu-send\n * / base write / schedule) that does NOT route through an LLM. MVP runs\n * `goal` nodes end to end; `host` is reserved in the schema so the runtime\n * can grow into it without a breaking change (it is rejected at validate\n * time until the executor lands \u2014 see `validateDag`).\n * `loop` \u2014 a composite node wrapping a bounded sub-pipeline (structured rework:\n * `code -> test` until the test's structured result passes). The outer DAG\n * stays acyclic \u2014 rework NEVER appears as a back-edge; it only exists inside\n * an explicit loop body. See docs/design/2026-06-06-v3-structured-loop-design.md.\n */\nexport type V3NodeType = 'goal' | 'host' | 'loop';\n\nexport const NODE_KINDS: readonly V3NodeType[] = ['goal', 'host', 'loop'];\n\n/** First host slice: every registered executor is side-effecting and must be\n * approved against its frozen runtime input. Keep this list in lockstep with\n * the shared host-executor registry. */\nexport const V3_HOST_EXECUTORS = ['feishu-send', 'feishu-reply', 'botmux-schedule'] as const;\nexport type V3HostExecutorName = typeof V3_HOST_EXECUTORS[number];\n\n/** Default per-node wall-clock budget when a node omits `timeoutSec`.\n * Generous on purpose: completion is detected by the manifest watcher\n * (seconds after the agent finishes), so the timeout only fires for hung\n * nodes \u2014 a long default costs nothing on the happy path. The architect is\n * prompted to set per-node `timeoutSec` explicitly for long tasks. */\nexport const DEFAULT_NODE_TIMEOUT_SEC = 1800;\n\n/** Hard ceiling for per-node `timeoutSec` (4h) \u2014 rejects runaway budgets the\n * architect might hallucinate while still allowing genuinely long tasks. */\nexport const MAX_NODE_TIMEOUT_SEC = 14400;\n\n/** A humanGate frozen at authoring time \u2014 the runtime never lets a node\n * add / skip a gate at runtime (design Q10). */\nexport interface V3HumanGate {\n /** Approval-card body shown to the human reviewer. */\n prompt: string;\n /** Button option keys shown on the approval card. */\n options?: string[];\n /** Selecting any of these options maps to `resolution:'approved'`. */\n approveOptions?: string[];\n /** Empty = any operator allowed by the outer daemon permission gate. */\n approvers?: string[];\n}\n\nexport const DEFAULT_HUMAN_GATE_OPTIONS: readonly string[] = ['approve', 'reject'];\nexport const MAX_HUMAN_GATE_OPTIONS = 8;\nexport const MAX_HUMAN_GATE_OPTION_LENGTH = 32;\n\n/**\n * Declares that this node consumes an upstream node's products. MVP pulls the\n * upstream node's *whole* manifest (all files) into this node's `inputs.json`;\n * a per-file selector is deferred (design \u00A72.3). Invariant: `from` MUST also\n * appear in the node's `depends` \u2014 you can only read outputs of a node you\n * wait for.\n */\nexport interface V3InputRef {\n /** Upstream nodeId whose manifest files become this node's inputs. */\n from: string;\n /** P3 per-file selector: pull ONE named product instead of the whole\n * manifest. Exactly one of `name` (manifest logical name) / `path`\n * (manifest relative path) when present. A selector that matches nothing\n * at dispatch time is surfaced to the agent via `GoalInputs.omitted`\n * (reason 'selectorMiss') \u2014 absence reads as a contract gap, not silence. */\n select?: { name?: string; path?: string };\n}\n\n/**\n * A normalized incoming edge (edge-activation design 2026-06-06 \u00A71.1).\n * Authored as either a plain string (`\"build\"`) or an object\n * (`{ \"from\": \"review\", \"when\": {...} }`); validateDag normalizes both to this\n * shape. No `when` = unconditional (source `done` \u21D2 active). With `when`,\n * the edge's activation is decided ONCE by the runtime reading the source's\n * `result.json` and journaled as `edgeResolved` \u2014 never re-read afterwards.\n *\n * `from` values are deduped per node: P0 supports at most ONE edge per\n * (from, to) pair, so `(from, to)` is a stable idempotency key for\n * `edgeResolved`. Express OR over outcomes inside the source's structured\n * result instead of authoring parallel conditional edges.\n */\nexport interface V3DependRef {\n from: string;\n /** Predicate over the SOURCE node's structured result \u2014 same shape and\n * validation as a loop exit predicate (`result.<key>` + exactly one\n * comparison operator, declared + required + type-compatible). */\n when?: V3EdgeWhen;\n}\n\n/** Edge predicates reuse the loop-exit predicate shape verbatim. */\nexport type V3EdgeWhen = V3LoopExitWhen;\n\n/**\n * Join semantics over a node's incoming edges (design \u00A71.2). Evaluated ONCE,\n * only after every incoming edge has settled (source done/skipped and any\n * predicate journaled) \u2014 no early release, no loser cancellation in P0.\n */\nexport type V3TriggerRule = 'all_success' | 'one_success' | { quorum: number };\n\n/**\n * Per-node capability override (P2, edge-activation follow-up). Merged onto\n * the bot's frozen `BotSnapshot` at dispatch time:\n * - `model` picks a different model for THIS node (cost control: cheap\n * models for research nodes, strong models for code nodes);\n * - `systemPromptAppend` adds node-specific instructions to the goal file.\n * Permission is deliberately not overridable: every workflow worker requires\n * CLI bypass permission, and bots configured to disable it are rejected.\n * `toolsSubset` is deferred \u2014 it needs a per-CLI capability matrix across the\n * daemon init/worker/adapter chain (P2b).\n */\nexport interface V3CapabilityOverride {\n model?: string;\n systemPromptAppend?: string;\n}\n\nexport const MAX_OVERRIDE_MODEL_LENGTH = 64;\nexport const MAX_OVERRIDE_SYSTEM_PROMPT_APPEND = 8000;\n\n/**\n * Opt-in structured-output contract \u2014 a deliberately TINY subset of\n * JSON-Schema (flat object, primitive-typed properties, optional required\n * list). Hand-validated (no deps, repo style); anything outside the subset\n * is rejected at validateDag time so the architect can never author a schema\n * the runtime's validator cannot execute.\n *\n * NOT supported (first slice): nested schemas, array item types, patterns.\n * `type:'array'|'object'` properties validate the TOP-LEVEL type only.\n * `enum` is supported on STRING properties only (edge-activation design \u00A71.3)\n * \u2014 it is the decision-vocabulary anchor for edge predicates: validateDag\n * cross-checks `equals`/`notEquals` operands against the source field's enum,\n * so a typo'd decision value fails at validate time, not at runtime.\n */\nexport interface V3ResultSchema {\n type: 'object';\n properties: Record<string, { type: V3ResultFieldType; enum?: string[] }>;\n required?: string[];\n}\n\nexport type V3ResultFieldType = 'string' | 'number' | 'boolean' | 'array' | 'object';\n\nconst RESULT_FIELD_TYPES: readonly V3ResultFieldType[] = ['string', 'number', 'boolean', 'array', 'object'];\n\n/** Caps on the resultSchema subset (anti-runaway: a giant schema bloats the\n * goal prompt and the validator). Checked at validateDag time. */\nexport const RESULT_SCHEMA_MAX_PROPERTIES = 32;\nexport const RESULT_SCHEMA_MAX_BYTES = 4096;\n\n/** Caps on a string property's `enum` (anti prompt-bloat; counted inside the\n * 4KB schema budget like everything else). */\nexport const RESULT_ENUM_MAX_VALUES = 16;\nexport const RESULT_ENUM_MAX_VALUE_LENGTH = 64;\n\n/** Backstop ceiling for `maxIterations` \u2014 like the timeout cap, it rejects a\n * runaway budget the architect might hallucinate; a human can still grant\n * extra iterations one at a time once the loop blocks. */\nexport const MAX_LOOP_ITERATIONS = 20;\n\n/** Cross-node revisit budgets (anti-infinite-loop). Two tiers:\n * - PER-PAIR (source\u2192target): how many times one node may revisit one ancestor\n * before the run blocks \u2014 default 1 (a node sends each ancestor back once;\n * expected multi-round rework belongs in a structured loop, not ad-hoc\n * revisit). Pinpoints which edge is ping-ponging.\n * - PER-RUN: total revisits across the whole run \u2014 a generous backstop so many\n * distinct pairs (or many nodes revisiting) can't run away.\n * Exhaustion blocks the run; a human grants +1 (revisitBudgetGranted). */\nexport const DEFAULT_REVISIT_BUDGET_PER_PAIR = 1;\nexport const DEFAULT_REVISIT_BUDGET_PER_RUN = 8;\n\n// \u2500\u2500\u2500 Loop schema \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Exit predicate over the exit node's structured result. Deliberately tiny:\n * `path` is fixed to `result.<key>` (the resultSchema subset is flat, so there\n * is nothing deeper to address) and exactly ONE comparison operator must be\n * set. validateDag cross-checks the key against the exit node's resultSchema\n * (declared AND required, operator type-compatible), so \"field missing at\n * runtime\" is a validate-time impossibility, not a runtime branch.\n *\n * No `continue.when` counterpart \u2014 when the predicate does not match, the loop\n * implicitly continues (until maxIterations). Two independent predicates\n * would create undefined both-match / neither-match states.\n */\nexport interface V3LoopExitWhen {\n /** `result.<key>` \u2014 a key of the exit node's resultSchema. */\n path: string;\n equals?: string | number | boolean;\n notEquals?: string | number | boolean;\n gt?: number;\n gte?: number;\n lt?: number;\n lte?: number;\n}\n\nconst LOOP_WHEN_OPERATORS = ['equals', 'notEquals', 'gt', 'gte', 'lt', 'lte'] as const;\n\nexport interface V3LoopExit {\n /** Body nodeId whose structured result decides the loop's exit. */\n node: string;\n when: V3LoopExitWhen;\n}\n\n/** Which body node's final-iteration manifest is the loop's outward product\n * (what downstream `inputs: [{from: <loopId>}]` reads). Defaults to the\n * exit node, but a repair loop usually exports the WORKER's product (`code`),\n * not the gate's (`test`). */\nexport interface V3LoopOutput {\n from: string;\n}\n\nexport interface V3Node {\n /** Unique within the DAG; also used as a runDir path segment, so it is\n * constrained to `[A-Za-z0-9._-]`. */\n id: string;\n type: V3NodeType;\n /** Required + non-empty for `goal` nodes; the single-sentence objective. */\n goal?: string;\n /** Which bot/CLI runs this node. MVP dogfoods a single CLI, but the field\n * is per-node so a mixed-backend DAG is a non-breaking extension. */\n bot?: string;\n /** Normalized incoming edges. Authored as `string | {from, when?}`;\n * validateDag normalizes to `V3DependRef[]` (edge-activation design \u00A71.1).\n * Unconditional edges gate on source `done`; `when` edges additionally\n * gate on the journaled `edgeResolved` verdict. */\n depends: V3DependRef[];\n /** Join semantics over incoming edges; defaults to 'all_success' (exactly\n * today's behavior). Only meaningful on nodes with \u22651 incoming edge. */\n triggerRule?: V3TriggerRule;\n /** Per-node capability override (restrict/redirect only \u2014 see\n * V3CapabilityOverride). Goal nodes (incl. loop body nodes) only; a loop\n * composite never spawns a worker, so it rejects this field. */\n override?: V3CapabilityOverride;\n /** Upstream products to thread in as inputs (every `from` \u2286 `depends`). */\n inputs: V3InputRef[];\n /** Wall-clock budget in seconds; falls back to DEFAULT_NODE_TIMEOUT_SEC. */\n timeoutSec?: number;\n /** Optional human approval gate, evaluated *before* the node's work runs. */\n humanGate?: V3HumanGate | null;\n /** Opt-in structured-output contract: when set, the node must write a\n * `result.json` (listed in its manifest files) matching this schema; a\n * violation blocks (not fails) the node. Absent \u2192 zero behavior change. */\n resultSchema?: V3ResultSchema;\n /** Definition-level revisit exits (cross-node\u56DE\u6EAF). When this node's\n * `result.json` returns `{ \"status\": \"revisit\", \"revisitTo\": \"<A>\" }`, the\n * runtime may revisit ancestor node `<A>` \u2014 but ONLY if `<A>` is listed\n * here. Default (absent / empty) = the node cannot revisit anything.\n * validateDag enforces every entry is an ANCESTOR (transitive `depends`),\n * so a revisit can never create a forward jump or a cycle in the run. */\n revisitTo?: string[];\n\n // \u2500\u2500 host-only fields (type === 'host') \u2500\u2500\n /** Deterministic executor invoked by the host runtime (never an LLM). */\n executor?: V3HostExecutorName;\n /** Frozen before the runtime gate; supports typed host bindings. */\n input?: unknown;\n\n // \u2500\u2500 loop-only fields (type === 'loop'; see V3LoopNode) \u2500\u2500\n /** Hard iteration bound; the loop blocks (recoverable, human can grant +1)\n * when it is exhausted without the exit predicate matching. */\n maxIterations?: number;\n /** The per-iteration sub-pipeline. Goal nodes only \u2014 no nesting, no\n * humanGate inside a body (both first-cut restrictions). */\n body?: { nodes: V3Node[] };\n /** Structured exit condition; not matching \u21D2 implicit continue. */\n exit?: V3LoopExit;\n /** Previous-iteration products threaded into the NEXT iteration's inputs.\n * Entries are `<bodyId>.result` | `<bodyId>.files` | `<bodyId>.manifest`. */\n feedback?: string[];\n /** Outward product projection (defaults to exit.node). */\n output?: V3LoopOutput;\n /** Only supported value (and the default): 'blocked'. */\n onExhausted?: 'blocked';\n /** Only supported value (and the default): 'fresh' \u2014 every iteration's every\n * body node runs a fresh ephemeral worker. `resumeWithinLoop` is deferred. */\n sessionPolicy?: 'fresh';\n}\n\n/** A `V3Node` narrowed to a goal node \u2014 `goal` is guaranteed present. This is\n * what crosses into `runNode` (the pool only ever runs goal nodes in MVP). */\nexport interface V3GoalNode extends V3Node {\n type: 'goal';\n goal: string;\n}\n\n/** Narrowing guard: a validated goal node always has a non-empty `goal`. */\nexport function isGoalNode(node: V3Node): node is V3GoalNode {\n return node.type === 'goal' && typeof node.goal === 'string' && node.goal.length > 0;\n}\n\nexport interface V3HostNode extends V3Node {\n type: 'host';\n executor: V3HostExecutorName;\n input: unknown;\n humanGate: V3HumanGate;\n}\n\nexport function isHostNode(node: V3Node): node is V3HostNode {\n return node.type === 'host' &&\n typeof node.executor === 'string' &&\n (V3_HOST_EXECUTORS as readonly string[]).includes(node.executor) &&\n node.humanGate !== null;\n}\n\n/** A `V3Node` narrowed to a loop node \u2014 validateDag guarantees every loop\n * field is present and normalized (output defaulted to exit.node, feedback\n * defaulted to `[]`). */\nexport interface V3LoopNode extends V3Node {\n type: 'loop';\n maxIterations: number;\n body: { nodes: V3Node[] };\n exit: V3LoopExit;\n feedback: string[];\n output: V3LoopOutput;\n}\n\n/** Narrowing guard for validated loop nodes. */\nexport function isLoopNode(node: V3Node): node is V3LoopNode {\n return node.type === 'loop';\n}\n\n/**\n * The expanded id a body node instance runs under in iteration N:\n * `repairLoop.i001.code`. Path-safe by construction (loopId/bodyId are\n * SEGMENT_RE, `.` is in the charset) and free of the `:` the blocked-card\n * nonce uses as a separator. OPAQUE \u2014 never parse this string back; journal\n * events carry a structured `loop: {loopId, iteration, bodyNodeId}` instead.\n */\nexport function loopInstanceId(loopId: string, iteration: number, bodyNodeId: string): string {\n return `${loopId}.i${String(iteration).padStart(3, '0')}.${bodyNodeId}`;\n}\n\nexport interface V3Dag {\n /** Stable id for this run; used as the runDir name, so path-segment safe. */\n runId: string;\n nodes: V3Node[];\n}\n\n// \u2500\u2500\u2500 Validation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Thrown by `validateDag` / `loadDag` with every problem found, not just the\n * first \u2014 authoring a DAG by hand is iterative, so surface the full list. */\nexport class DagValidationError extends Error {\n constructor(public readonly problems: string[]) {\n super(`Invalid v3 dag.json:\\n - ${problems.join('\\n - ')}`);\n this.name = 'DagValidationError';\n }\n}\n\n/** Node ids and runId double as filesystem path segments under the runDir. */\nexport const V3_DAG_SEGMENT_RE = /^[A-Za-z0-9._-]+$/;\nconst SEGMENT_RE = V3_DAG_SEGMENT_RE;\n\nfunction isObject(v: unknown): v is Record<string, unknown> {\n return typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n\n/**\n * Validate an untrusted parsed value into a `V3Dag`. Pure \u2014 throws\n * `DagValidationError` with the full problem list on any violation, otherwise\n * returns a normalized dag (defaults filled, `humanGate: undefined` \u2192 `null`).\n *\n * Checks: runId shape; non-empty unique path-safe node ids; known `type`;\n * `goal` non-empty for goal nodes; host executor/input/gate policy;\n * `depends` reference existing nodes, no self-dep, no dup `from` (P0: one\n * edge per (from,to)); edge predicates validated against the SOURCE's\n * resultSchema (goal-with-schema sources only); `triggerRule` shape/bounds;\n * `inputs.from` reference existing nodes AND appear in `depends`; acyclic\n * (delegated to `topologicalOrder`, conditional edges included).\n */\nexport function validateDag(raw: unknown): V3Dag {\n const problems: string[] = [];\n\n if (!isObject(raw)) {\n throw new DagValidationError(['root must be a JSON object']);\n }\n if (typeof raw.runId !== 'string' || !SEGMENT_RE.test(raw.runId)) {\n problems.push(`runId must be a path-safe string matching ${SEGMENT_RE} (got ${JSON.stringify(raw.runId)})`);\n }\n if (!Array.isArray(raw.nodes) || raw.nodes.length === 0) {\n throw new DagValidationError([...problems, 'nodes must be a non-empty array']);\n }\n\n const ids = new Set<string>();\n const nodes: V3Node[] = [];\n // Edge predicates parked until every node (and thus every source's\n // resultSchema) is collected \u2014 validated in the cross-node pass below.\n const pendingWhens: PendingWhen[] = [];\n\n for (let i = 0; i < raw.nodes.length; i++) {\n const n = raw.nodes[i];\n const where = `nodes[${i}]`;\n if (!isObject(n)) {\n problems.push(`${where} must be an object`);\n continue;\n }\n const id = n.id;\n if (typeof id !== 'string' || !SEGMENT_RE.test(id)) {\n problems.push(`${where}.id must be a path-safe string matching ${SEGMENT_RE} (got ${JSON.stringify(id)})`);\n continue;\n }\n if (ids.has(id)) {\n problems.push(`duplicate node id \"${id}\"`);\n continue;\n }\n ids.add(id);\n\n const type = n.type;\n if (type !== 'goal' && type !== 'host' && type !== 'loop') {\n problems.push(`node \"${id}\".type must be one of ${NODE_KINDS.join(' | ')} (got ${JSON.stringify(type)})`);\n continue;\n }\n const depends = normDepends(n.depends, `node \"${id}\"`, problems, { ownerId: id, list: pendingWhens });\n const fromList = depends.map((d) => d.from);\n if (fromList.includes(id)) problems.push(`node \"${id}\" depends on itself`);\n if (new Set(fromList).size !== fromList.length) problems.push(`node \"${id}\".depends has duplicates`);\n\n const triggerRule = normTriggerRule(n.triggerRule, depends.length, `node \"${id}\"`, problems);\n\n const inputs = normInputs(n.inputs, id, problems);\n\n if (type === 'loop') {\n const loopFields = normLoopFields(n, id, problems);\n if (loopFields) {\n nodes.push({\n id,\n type,\n goal: typeof n.goal === 'string' ? n.goal : undefined,\n bot: typeof n.bot === 'string' ? n.bot : undefined,\n depends,\n triggerRule,\n inputs,\n humanGate: null,\n ...loopFields,\n });\n }\n continue;\n }\n\n if (type === 'host') {\n if (n.goal !== undefined) problems.push(`host node \"${id}\".goal is not supported`);\n if (n.bot !== undefined) problems.push(`host node \"${id}\".bot is not supported \u2014 host nodes do not spawn a CLI`);\n if (n.override !== undefined) problems.push(`host node \"${id}\".override is not supported`);\n if (n.revisitTo !== undefined) problems.push(`host node \"${id}\".revisitTo is not supported`);\n if (n.resultSchema !== undefined) {\n problems.push(`host node \"${id}\".resultSchema is not supported \u2014 host output uses the trusted executor result contract`);\n }\n if (inputs.length > 0) {\n problems.push(`host node \"${id}\".inputs must be empty \u2014 use typed bindings in host input`);\n }\n // `undefined` is the normalized/default all-success rule (and is the\n // only legal representation for a root node with no incoming edges).\n // Reject only an explicitly different trigger policy.\n if (triggerRule !== undefined && triggerRule !== 'all_success') {\n problems.push(\n `host node \"${id}\".triggerRule must be \"all_success\"; ` +\n 'P0 host bindings do not accept skipped/omitted dependencies',\n );\n }\n const executor = typeof n.executor === 'string' ? n.executor : '';\n if (!(V3_HOST_EXECUTORS as readonly string[]).includes(executor)) {\n problems.push(\n `host node \"${id}\".executor must be one of ${V3_HOST_EXECUTORS.join(' | ')} ` +\n `(got ${JSON.stringify(n.executor)})`,\n );\n }\n if (!Object.prototype.hasOwnProperty.call(n, 'input')) {\n problems.push(`host node \"${id}\".input is required`);\n } else {\n validateHostInputShape(executor as V3HostExecutorName, n.input, id, problems);\n try {\n collectV3HostBindingRefs(n.input);\n } catch (err) {\n problems.push(\n `host node \"${id}\".input is invalid: ${err instanceof V3HostBindingError ? err.message : String(err)}`,\n );\n }\n }\n if (n.timeoutSec !== undefined) {\n problems.push(\n `host node \"${id}\".timeoutSec is not supported \u2014 abandoning an in-flight provider call would make its effect outcome unknown`,\n );\n }\n const humanGate = normHumanGate(n.humanGate, `host node \"${id}\"`, problems);\n if (!humanGate) {\n problems.push(\n `host node \"${id}\" must declare a humanGate; v3 P0 does not allow ungated external side effects`,\n );\n } else {\n // Host gates authorize an external side effect. Do not inherit the\n // generic gate's legacy \"first option means approve\" fallback: it can\n // turn a button labelled `reject` into an approve action. P0 requires\n // the reserved choices to have explicit, invariant semantics.\n if (!humanGate.options?.includes('approve')) {\n problems.push(\n `host node \"${id}\".humanGate.options must include \"approve\" explicitly; ` +\n 'host side effects cannot use an implicit first-option approval',\n );\n }\n if (\n humanGate.approveOptions?.length !== 1 ||\n humanGate.approveOptions[0] !== 'approve'\n ) {\n problems.push(\n `host node \"${id}\".humanGate.approveOptions must be exactly [\"approve\"]; ` +\n 'custom-labelled choices cannot authorize a host side effect',\n );\n }\n }\n nodes.push({\n id,\n type,\n executor: executor as V3HostExecutorName,\n input: n.input,\n depends,\n triggerRule,\n inputs: [],\n humanGate,\n });\n continue;\n }\n\n if (typeof n.goal !== 'string' || n.goal.trim() === '') {\n problems.push(`goal node \"${id}\".goal must be a non-empty string`);\n }\n\n const timeoutSec = normTimeoutSec(n.timeoutSec, `node \"${id}\"`, problems);\n\n const resultSchema = normResultSchema(n.resultSchema, id, problems);\n\n const humanGate = normHumanGate(n.humanGate, `node \"${id}\"`, problems);\n\n const override = normOverride(n.override, `node \"${id}\"`, problems);\n\n const revisitTo = normRevisitTo(n.revisitTo, id, problems);\n\n nodes.push({\n id,\n type,\n goal: typeof n.goal === 'string' ? n.goal : undefined,\n bot: typeof n.bot === 'string' ? n.bot : undefined,\n depends,\n triggerRule,\n override,\n inputs,\n timeoutSec,\n humanGate,\n resultSchema,\n ...(revisitTo ? { revisitTo } : {}),\n });\n }\n\n // Cross-node reference checks \u2014 only meaningful once ids are collected.\n for (const node of nodes) {\n for (const dep of node.depends) {\n if (!ids.has(dep.from)) problems.push(`node \"${node.id}\" depends on unknown node \"${dep.from}\"`);\n }\n for (const inp of node.inputs) {\n if (!ids.has(inp.from)) {\n problems.push(`node \"${node.id}\".inputs references unknown node \"${inp.from}\"`);\n } else if (!node.depends.some((d) => d.from === inp.from)) {\n problems.push(`node \"${node.id}\".inputs.from \"${inp.from}\" must also be in depends`);\n }\n }\n if (node.type === 'host') {\n try {\n for (const ref of collectV3HostBindingRefs(node.input)) {\n if (ref.kind !== 'result') continue;\n if (!ids.has(ref.nodeId)) {\n problems.push(`host node \"${node.id}\".input references unknown result node \"${ref.nodeId}\"`);\n } else if (!node.depends.some((dep) => dep.from === ref.nodeId)) {\n problems.push(\n `host node \"${node.id}\".input result source \"${ref.nodeId}\" must also be in depends`,\n );\n } else if (!node.depends.some((dep) => dep.from === ref.nodeId && dep.when === undefined)) {\n problems.push(\n `host node \"${node.id}\".input result source \"${ref.nodeId}\" must use an unconditional depends edge; ` +\n 'P0 host bindings do not accept omitted conditional inputs',\n );\n }\n }\n } catch {\n // Per-node validation already reports the malformed binding.\n }\n }\n // revisitTo: each target must exist AND be a (transitive) ANCESTOR of this\n // node \u2014 a revisit only ever jumps BACKWARD, so the definition graph stays\n // acyclic and the supersede cone is well-defined (cross-node\u56DE\u6EAF design).\n if (node.revisitTo && node.revisitTo.length > 0) {\n const ancestors = ancestorsOf(node.id, nodes);\n for (const target of node.revisitTo) {\n if (!ids.has(target)) {\n problems.push(`node \"${node.id}\".revisitTo references unknown node \"${target}\"`);\n } else if (target === node.id) {\n problems.push(`node \"${node.id}\".revisitTo cannot point at itself`);\n } else if (!ancestors.has(target)) {\n problems.push(\n `node \"${node.id}\".revisitTo \"${target}\" must be an ancestor (reachable via depends) \u2014 revisit only jumps backward`,\n );\n }\n }\n }\n }\n\n // Revisit can replay an entire downstream cone. External effects are not\n // replay-safe under a fresh attempt/idempotency key, so P0 forbids a host in\n // any cone that a goal is allowed to revisit.\n for (const requester of nodes) {\n for (const target of requester.revisitTo ?? []) {\n const cone = downstreamCone(target, nodes);\n for (const nodeId of cone) {\n if (nodeByIdUnsafe(nodes, nodeId)?.type === 'host') {\n problems.push(\n `node \"${requester.id}\".revisitTo \"${target}\" would replay host node \"${nodeId}\"; ` +\n 'host nodes are not allowed in a revisit cone',\n );\n }\n }\n }\n }\n\n // Edge-predicate validation (design \u00A72): the source must be a goal node\n // declaring a resultSchema \u2014 loop sources are forbidden in P0 (a loop's\n // outward manifest belongs to its output-projection body node; put an\n // explicit verifier goal after the loop instead). Host sources are also\n // forbidden because their fixed receipt schema has no authored resultSchema.\n // The predicate reuses the\n // loop-exit validator: declared + required key, exactly one operator,\n // type-compatible, enum-reconciled.\n const nodeById = new Map(nodes.map((nn) => [nn.id, nn]));\n for (const pw of pendingWhens) {\n const source = nodeById.get(pw.ref.from);\n if (!source) continue; // unknown `from` already reported above\n if (source.type !== 'goal') {\n problems.push(\n `${pw.where}: conditional edge source \"${pw.ref.from}\" must be a goal node ` +\n `(P0 forbids loop sources \u2014 add a verifier goal after the loop and branch on ITS result)`,\n );\n continue;\n }\n if (!source.resultSchema) {\n problems.push(\n `${pw.where}: conditional edge source \"${pw.ref.from}\" must declare a resultSchema \u2014 the predicate reads its structured result`,\n );\n continue;\n }\n const when = normLoopExitWhen(pw.raw, source.resultSchema, pw.where, problems);\n if (when) pw.ref.when = when;\n }\n\n // Loop expansion namespace guard: iteration instances run under\n // `<loopId>.iNNN.<bodyId>` (see loopInstanceId), so no OTHER top-level id may\n // sit inside a loop's dot-prefix \u2014 an authored `repairLoop.i001.code` node\n // would collide with the expansion. Plain ids may still contain dots.\n for (const node of nodes) {\n if (node.type !== 'loop') continue;\n for (const other of ids) {\n if (other !== node.id && other.startsWith(`${node.id}.`)) {\n problems.push(\n `node id \"${other}\" collides with loop \"${node.id}\" expansion namespace (\"${node.id}.*\")`,\n );\n }\n }\n }\n\n if (problems.length > 0) throw new DagValidationError(problems);\n\n const dag: V3Dag = { runId: raw.runId as string, nodes };\n // Cycle detection: topologicalOrder throws on a cycle. Run it here so\n // loadDag rejects a cyclic DAG up front rather than mid-run.\n topologicalOrder(dag);\n return dag;\n}\n\nfunction validateHostInputShape(\n executor: V3HostExecutorName,\n input: unknown,\n nodeId: string,\n problems: string[],\n): void {\n if (!isObject(input) || Object.prototype.hasOwnProperty.call(input, '$ref')) {\n problems.push(`host node \"${nodeId}\".input must be an object with explicit executor fields`);\n return;\n }\n const shape: Record<V3HostExecutorName, { required: string[]; allowed: string[] }> = {\n 'feishu-send': {\n required: ['larkAppId', 'chatId', 'content'],\n allowed: ['larkAppId', 'chatId', 'content', 'msgType'],\n },\n 'feishu-reply': {\n required: ['larkAppId', 'rootMessageId', 'content'],\n allowed: ['larkAppId', 'rootMessageId', 'content', 'msgType', 'replyInThread'],\n },\n 'botmux-schedule': {\n required: ['name', 'schedule', 'prompt', 'workingDir', 'chatId', 'chatType', 'larkAppId'],\n allowed: [\n 'name', 'schedule', 'prompt', 'workingDir', 'chatId',\n 'chatType', 'rootMessageId', 'scope', 'larkAppId', 'repeat', 'deliver',\n ],\n },\n };\n const expected = shape[executor];\n if (!expected) return;\n for (const field of expected.required) {\n if (!Object.prototype.hasOwnProperty.call(input, field)) {\n problems.push(`host node \"${nodeId}\".input.${field} is required for ${executor}`);\n }\n }\n for (const field of Object.keys(input)) {\n if (!expected.allowed.includes(field)) {\n problems.push(`host node \"${nodeId}\".input.${field} is not supported by ${executor}`);\n }\n }\n const identity: Record<string, string> =\n executor === 'feishu-send' ? { larkAppId: 'larkAppId', chatId: 'chatId' }\n : executor === 'feishu-reply' ? { larkAppId: 'larkAppId', rootMessageId: 'rootMessageId' }\n : { larkAppId: 'larkAppId', chatId: 'chatId', chatType: 'chatType' };\n if (executor === 'botmux-schedule' && Object.prototype.hasOwnProperty.call(input, 'rootMessageId')) {\n identity.rootMessageId = 'rootMessageId';\n }\n if (\n executor === 'botmux-schedule' &&\n Object.prototype.hasOwnProperty.call(input, 'deliver') &&\n input.deliver !== 'origin' &&\n input.deliver !== 'new-topic'\n ) {\n problems.push(\n `host node \"${nodeId}\".input.deliver must be \"origin\" or \"new-topic\"; ` +\n 'v3 P0 does not support local-only schedule delivery',\n );\n }\n for (const [field, contextName] of Object.entries(identity)) {\n const value = input[field];\n if (\n !isObject(value) ||\n Object.keys(value).length !== 1 ||\n value.$ref !== `context.${contextName}`\n ) {\n problems.push(\n `host node \"${nodeId}\".input.${field} must be exact ` +\n `{ \"$ref\": \"context.${contextName}\" }; IM host effects cannot target another bot/chat`,\n );\n }\n }\n}\n\nfunction normHumanGate(raw: unknown, where: string, problems: string[]): V3HumanGate | null {\n if (raw == null) return null;\n if (!isObject(raw) || typeof raw.prompt !== 'string' || raw.prompt.trim() === '') {\n problems.push(`${where}.humanGate must be { prompt: <non-empty string>, options?, approveOptions?, approvers? } or null`);\n return null;\n }\n\n let options = [...DEFAULT_HUMAN_GATE_OPTIONS];\n if (raw.options !== undefined) {\n if (!Array.isArray(raw.options)) {\n problems.push(`${where}.humanGate.options must be a non-empty string array`);\n } else {\n const parsed = parseUniqueStringList(\n raw.options,\n `${where}.humanGate.options`,\n problems,\n { allowEmptyList: false, maxItems: MAX_HUMAN_GATE_OPTIONS, maxLength: MAX_HUMAN_GATE_OPTION_LENGTH },\n );\n if (parsed) options = parsed;\n }\n }\n\n let approveOptions: string[] | undefined;\n if (raw.approveOptions !== undefined) {\n if (!Array.isArray(raw.approveOptions)) {\n problems.push(`${where}.humanGate.approveOptions must be a non-empty string array`);\n } else {\n approveOptions = parseUniqueStringList(\n raw.approveOptions,\n `${where}.humanGate.approveOptions`,\n problems,\n { allowEmptyList: false, maxLength: MAX_HUMAN_GATE_OPTION_LENGTH },\n );\n }\n }\n approveOptions ??= options.includes('approve') ? ['approve'] : [options[0]!];\n for (const opt of approveOptions) {\n if (!options.includes(opt)) {\n problems.push(`${where}.humanGate.approveOptions value ${JSON.stringify(opt)} must also appear in options`);\n }\n }\n\n let approvers: string[] = [];\n if (raw.approvers !== undefined) {\n if (!Array.isArray(raw.approvers)) {\n problems.push(`${where}.humanGate.approvers must be a string array`);\n } else {\n approvers = parseUniqueStringList(\n raw.approvers,\n `${where}.humanGate.approvers`,\n problems,\n { allowEmptyList: true },\n ) ?? [];\n }\n }\n\n return { prompt: raw.prompt, options, approveOptions, approvers };\n}\n\nfunction parseUniqueStringList(\n raw: unknown[],\n where: string,\n problems: string[],\n opts: { allowEmptyList: boolean; maxItems?: number; maxLength?: number },\n): string[] | undefined {\n const values: string[] = [];\n const seen = new Set<string>();\n if (!opts.allowEmptyList && raw.length === 0) {\n problems.push(`${where} must not be empty`);\n }\n if (opts.maxItems !== undefined && raw.length > opts.maxItems) {\n problems.push(`${where} supports at most ${opts.maxItems} entries`);\n }\n for (const item of raw) {\n if (typeof item !== 'string' || item.trim() === '') {\n problems.push(`${where} entries must be non-empty strings`);\n continue;\n }\n if (opts.maxLength !== undefined && item.length > opts.maxLength) {\n problems.push(`${where} entry ${JSON.stringify(item)} exceeds ${opts.maxLength} characters`);\n }\n if (seen.has(item)) {\n problems.push(`${where} has duplicate value ${JSON.stringify(item)}`);\n continue;\n }\n seen.add(item);\n values.push(item);\n }\n return problems.some((p) => p.startsWith(where)) ? undefined : values;\n}\n\nfunction normStringArray(v: unknown, where: string, problems: string[]): string[] {\n if (v === undefined) return [];\n if (!Array.isArray(v) || v.some((x) => typeof x !== 'string')) {\n problems.push(`${where} must be an array of strings`);\n return [];\n }\n return v as string[];\n}\n\n/** An edge predicate parked during the per-node pass: validated against the\n * SOURCE node's resultSchema in the cross-node pass, then written back into\n * `ref.when`. */\ninterface PendingWhen {\n where: string;\n ref: V3DependRef;\n raw: Record<string, unknown>;\n}\n\n/**\n * Normalize a `depends` array of `string | { from, when? }` entries into\n * `V3DependRef[]`. `when` objects are NOT validated here (the source's\n * resultSchema may not be collected yet) \u2014 they are parked in `whenSink` for\n * the cross-node pass. `whenSink === undefined` means conditional edges are\n * not allowed in this position (loop bodies, first cut).\n */\nfunction normDepends(\n v: unknown,\n where: string,\n problems: string[],\n whenSink?: { ownerId: string; list: PendingWhen[] },\n): V3DependRef[] {\n if (v === undefined) return [];\n if (!Array.isArray(v)) {\n problems.push(`${where}.depends must be an array of nodeId strings or { from, when? } objects`);\n return [];\n }\n const out: V3DependRef[] = [];\n for (let j = 0; j < v.length; j++) {\n const entry = v[j];\n if (typeof entry === 'string') {\n out.push({ from: entry });\n continue;\n }\n if (isObject(entry) && typeof entry.from === 'string') {\n const extra = Object.keys(entry).filter((k) => k !== 'from' && k !== 'when');\n if (extra.length > 0) {\n problems.push(`${where}.depends[${j}] has unsupported key(s): ${extra.join(', ')} (allowed: from, when)`);\n continue;\n }\n const ref: V3DependRef = { from: entry.from };\n if (entry.when !== undefined) {\n if (!whenSink) {\n problems.push(`${where}.depends[${j}].when: conditional edges are not supported inside a loop body (first cut)`);\n continue;\n }\n if (!isObject(entry.when)) {\n problems.push(`${where}.depends[${j}].when must be an object`);\n continue;\n }\n whenSink.list.push({\n where: `${where}.depends[${j}].when (edge \"${entry.from}\" -> \"${whenSink.ownerId}\")`,\n ref,\n raw: entry.when,\n });\n }\n out.push(ref);\n continue;\n }\n problems.push(`${where}.depends[${j}] must be a nodeId string or { from, when? }`);\n }\n return out;\n}\n\n/**\n * Validate `triggerRule` (design \u00A71.2). Bounds depend on the node's indegree:\n * a join rule on a node with no incoming edges is an authoring error, and a\n * quorum must be satisfiable (1..indegree).\n */\nfunction normTriggerRule(\n v: unknown,\n indegree: number,\n where: string,\n problems: string[],\n): V3TriggerRule | undefined {\n if (v === undefined) return undefined;\n if (v === 'all_success' || v === 'one_success') {\n if (indegree === 0) {\n problems.push(`${where}.triggerRule requires at least one incoming edge (depends is empty)`);\n return undefined;\n }\n return v;\n }\n if (isObject(v)) {\n const extra = Object.keys(v).filter((k) => k !== 'quorum');\n if (extra.length > 0) {\n problems.push(`${where}.triggerRule object only supports { quorum: N } (got extra: ${extra.join(', ')})`);\n return undefined;\n }\n if (indegree === 0) {\n problems.push(`${where}.triggerRule requires at least one incoming edge (depends is empty)`);\n return undefined;\n }\n const q = v.quorum;\n if (typeof q !== 'number' || !Number.isInteger(q) || q < 1 || q > indegree) {\n problems.push(`${where}.triggerRule.quorum must be an integer in [1, ${indegree}] (got ${JSON.stringify(q)})`);\n return undefined;\n }\n return { quorum: q };\n }\n problems.push(`${where}.triggerRule must be 'all_success' | 'one_success' | { quorum: N }`);\n return undefined;\n}\n\n/**\n * Validate the per-node capability override (P2). Fail-loud on unknown keys\n * (incl. the deferred `toolsSubset` \u2014 better an explicit \"not yet\" than a\n * field the runtime silently ignores). Permissions are not part of this\n * object: workflow workers always require CLI bypass permission.\n */\nfunction normOverride(\n v: unknown,\n where: string,\n problems: string[],\n): V3CapabilityOverride | undefined {\n if (v === undefined || v === null) return undefined;\n if (!isObject(v)) {\n problems.push(`${where}.override must be an object`);\n return undefined;\n }\n const known = new Set(['model', 'systemPromptAppend']);\n const extra = Object.keys(v).filter((k) => !known.has(k));\n if (extra.length > 0) {\n const hints: string[] = [];\n if (extra.includes('toolsSubset')) hints.push('toolsSubset is deferred \u2014 P2b');\n if (extra.includes('permissionMode')) {\n hints.push('permissionMode was removed \u2014 v3 workflow workers always require CLI bypass; delete this key');\n }\n const hint = hints.length > 0 ? ` (${hints.join('; ')})` : '';\n problems.push(`${where}.override has unsupported key(s): ${extra.join(', ')}${hint} (allowed: model, systemPromptAppend)`);\n return undefined;\n }\n const out: V3CapabilityOverride = {};\n if (v.model !== undefined) {\n if (typeof v.model !== 'string' || v.model.trim() === '' || v.model.length > MAX_OVERRIDE_MODEL_LENGTH) {\n problems.push(`${where}.override.model must be a non-empty string \u2264${MAX_OVERRIDE_MODEL_LENGTH} chars`);\n return undefined;\n }\n out.model = v.model.trim();\n }\n if (v.systemPromptAppend !== undefined) {\n if (\n typeof v.systemPromptAppend !== 'string' ||\n v.systemPromptAppend.trim() === '' ||\n Buffer.byteLength(v.systemPromptAppend, 'utf-8') > MAX_OVERRIDE_SYSTEM_PROMPT_APPEND\n ) {\n problems.push(`${where}.override.systemPromptAppend must be a non-empty string \u2264${MAX_OVERRIDE_SYSTEM_PROMPT_APPEND} bytes`);\n return undefined;\n }\n out.systemPromptAppend = v.systemPromptAppend;\n }\n if (Object.keys(out).length === 0) {\n problems.push(`${where}.override must set at least one of model / systemPromptAppend`);\n return undefined;\n }\n return out;\n}\n\nfunction normTimeoutSec(v: unknown, where: string, problems: string[]): number | undefined {\n if (v === undefined) return undefined;\n if (typeof v !== 'number' || !Number.isFinite(v) || v <= 0) {\n problems.push(`${where}.timeoutSec must be a positive number`);\n return undefined;\n }\n if (v > MAX_NODE_TIMEOUT_SEC) {\n problems.push(`${where}.timeoutSec ${v} exceeds the ${MAX_NODE_TIMEOUT_SEC}s (4h) ceiling`);\n return undefined;\n }\n return v;\n}\n\n/** Normalize `revisitTo`: an optional array of non-empty path-safe node ids.\n * Shape only here; the ancestor / existence cross-checks run once all ids are\n * collected (validateDag's cross-node pass). */\nfunction normRevisitTo(v: unknown, id: string, problems: string[]): string[] | undefined {\n if (v === undefined) return undefined;\n if (!Array.isArray(v)) {\n problems.push(`node \"${id}\".revisitTo must be an array of node ids`);\n return undefined;\n }\n const out: string[] = [];\n for (const entry of v) {\n if (typeof entry !== 'string' || entry.trim() === '') {\n problems.push(`node \"${id}\".revisitTo entries must be non-empty node-id strings`);\n continue;\n }\n out.push(entry);\n }\n if (new Set(out).size !== out.length) problems.push(`node \"${id}\".revisitTo has duplicates`);\n return out.length > 0 ? out : undefined;\n}\n\n/** Transitive ancestors of `nodeId` over `depends` edges (the set of nodes\n * from which `nodeId` is reachable downstream). Used to constrain\n * `revisitTo` to backward-only jumps. Pure BFS over the (acyclic-by-design)\n * definition graph. */\nfunction ancestorsOf(nodeId: string, nodes: V3Node[]): Set<string> {\n const byId = new Map(nodes.map((n) => [n.id, n]));\n const seen = new Set<string>();\n const queue = [...(byId.get(nodeId)?.depends.map((d) => d.from) ?? [])];\n while (queue.length > 0) {\n const cur = queue.shift()!;\n if (seen.has(cur)) continue;\n seen.add(cur);\n for (const dep of byId.get(cur)?.depends ?? []) queue.push(dep.from);\n }\n return seen;\n}\n\nfunction downstreamCone(nodeId: string, nodes: V3Node[]): Set<string> {\n const seen = new Set<string>([nodeId]);\n let changed = true;\n while (changed) {\n changed = false;\n for (const node of nodes) {\n if (seen.has(node.id)) continue;\n if (node.depends.some((dep) => seen.has(dep.from))) {\n seen.add(node.id);\n changed = true;\n }\n }\n }\n return seen;\n}\n\nfunction nodeByIdUnsafe(nodes: V3Node[], nodeId: string): V3Node | undefined {\n return nodes.find((node) => node.id === nodeId);\n}\n\n/**\n * Validate + normalize a loop node's composite fields. Self-contained: the\n * body is its own little DAG (goal nodes only, internal refs, acyclic), and\n * exit/feedback/output all reference INTO the body, so every cross-check lives\n * here rather than in the top-level pass. Returns `undefined` (with problems\n * pushed) on any violation.\n */\nfunction normLoopFields(\n n: Record<string, unknown>,\n id: string,\n problems: string[],\n): Pick<V3LoopNode, 'maxIterations' | 'body' | 'exit' | 'feedback' | 'output' | 'onExhausted' | 'sessionPolicy'> | undefined {\n const where = `loop node \"${id}\"`;\n const before = problems.length;\n\n // Fields that make no sense on a composite node \u2014 reject loudly rather than\n // silently ignore (same fail-loud stance as the resultSchema subset).\n if (n.timeoutSec !== undefined) {\n problems.push(`${where}.timeoutSec is not supported \u2014 set timeoutSec on body nodes instead`);\n }\n if (n.resultSchema !== undefined) {\n problems.push(`${where}.resultSchema is not supported \u2014 declare it on the exit body node`);\n }\n if (n.humanGate != null) {\n problems.push(`${where}.humanGate is not supported (first cut) \u2014 gate an upstream node instead`);\n }\n if (n.onExhausted !== undefined && n.onExhausted !== 'blocked') {\n problems.push(`${where}.onExhausted only supports \"blocked\"`);\n }\n if (n.sessionPolicy !== undefined && n.sessionPolicy !== 'fresh') {\n problems.push(`${where}.sessionPolicy only supports \"fresh\" (resumeWithinLoop is deferred)`);\n }\n if (n.override !== undefined) {\n problems.push(`${where}.override is not supported \u2014 a loop composite never spawns a worker; set override on body nodes instead`);\n }\n\n let maxIterations: number | undefined;\n if (typeof n.maxIterations !== 'number' || !Number.isInteger(n.maxIterations) || n.maxIterations < 1) {\n problems.push(`${where}.maxIterations must be a positive integer`);\n } else if (n.maxIterations > MAX_LOOP_ITERATIONS) {\n problems.push(`${where}.maxIterations ${n.maxIterations} exceeds the ${MAX_LOOP_ITERATIONS} ceiling`);\n } else {\n maxIterations = n.maxIterations;\n }\n\n // \u2500\u2500 body: a small inline DAG of goal nodes \u2500\u2500\n const bodyRaw = n.body;\n if (!isObject(bodyRaw) || !Array.isArray(bodyRaw.nodes) || bodyRaw.nodes.length === 0) {\n problems.push(`${where}.body.nodes must be a non-empty array`);\n return undefined; // exit/feedback/output are unverifiable without a body\n }\n const bodyBefore = problems.length;\n const bodyIds = new Set<string>();\n const bodyNodes: V3Node[] = [];\n for (let j = 0; j < bodyRaw.nodes.length; j++) {\n const b = bodyRaw.nodes[j];\n const bwhere = `${where}.body.nodes[${j}]`;\n if (!isObject(b)) {\n problems.push(`${bwhere} must be an object`);\n continue;\n }\n const bid = b.id;\n if (typeof bid !== 'string' || !SEGMENT_RE.test(bid)) {\n problems.push(`${bwhere}.id must be a path-safe string matching ${SEGMENT_RE} (got ${JSON.stringify(bid)})`);\n continue;\n }\n if (bodyIds.has(bid)) {\n problems.push(`${where}.body has duplicate node id \"${bid}\"`);\n continue;\n }\n bodyIds.add(bid);\n if (b.type !== 'goal') {\n problems.push(`${where}.body node \"${bid}\": only \"goal\" nodes are allowed in a loop body (no nested loops, no host)`);\n continue;\n }\n if (typeof b.goal !== 'string' || b.goal.trim() === '') {\n problems.push(`${where}.body node \"${bid}\".goal must be a non-empty string`);\n }\n if (b.humanGate != null) {\n problems.push(`${where}.body node \"${bid}\".humanGate is not supported inside a loop body (first cut)`);\n }\n if (b.triggerRule !== undefined) {\n problems.push(`${where}.body node \"${bid}\".triggerRule is not supported inside a loop body (first cut)`);\n }\n // No whenSink: conditional edges are rejected inside a body (first cut).\n const bdepends = normDepends(b.depends, `${where}.body node \"${bid}\"`, problems);\n const bFromList = bdepends.map((d) => d.from);\n if (bFromList.includes(bid)) problems.push(`${where}.body node \"${bid}\" depends on itself`);\n if (new Set(bFromList).size !== bFromList.length) problems.push(`${where}.body node \"${bid}\".depends has duplicates`);\n const binputs = normInputs(b.inputs, `${id}.body.${bid}`, problems);\n const btimeout = normTimeoutSec(b.timeoutSec, `${where}.body node \"${bid}\"`, problems);\n const bschema = normResultSchema(b.resultSchema, `${id}.body.${bid}`, problems);\n const boverride = normOverride(b.override, `${where}.body node \"${bid}\"`, problems);\n bodyNodes.push({\n id: bid,\n type: 'goal',\n goal: typeof b.goal === 'string' ? b.goal : undefined,\n bot: typeof b.bot === 'string' ? b.bot : undefined,\n depends: bdepends,\n override: boverride,\n inputs: binputs,\n timeoutSec: btimeout,\n humanGate: null,\n resultSchema: bschema,\n });\n }\n // Body-internal references.\n for (const bn of bodyNodes) {\n for (const dep of bn.depends) {\n if (!bodyIds.has(dep.from)) problems.push(`${where}.body node \"${bn.id}\" depends on unknown body node \"${dep.from}\"`);\n }\n for (const inp of bn.inputs) {\n if (!bodyIds.has(inp.from)) {\n problems.push(`${where}.body node \"${bn.id}\".inputs references unknown body node \"${inp.from}\"`);\n } else if (!bn.depends.some((d) => d.from === inp.from)) {\n problems.push(`${where}.body node \"${bn.id}\".inputs.from \"${inp.from}\" must also be in depends`);\n }\n }\n }\n // Body acyclic \u2014 only checkable once its refs are sane (topologicalOrder\n // assumes valid deps).\n if (problems.length === bodyBefore && bodyNodes.length === bodyRaw.nodes.length) {\n try {\n topologicalOrder({ runId: 'body', nodes: bodyNodes });\n } catch (err) {\n if (err instanceof DagValidationError) {\n for (const p of err.problems) problems.push(`${where}.body: ${p}`);\n } else {\n throw err;\n }\n }\n }\n\n // \u2500\u2500 exit \u2500\u2500\n let exit: V3LoopExit | undefined;\n const exitRaw = n.exit;\n if (!isObject(exitRaw) || typeof exitRaw.node !== 'string' || !isObject(exitRaw.when)) {\n problems.push(`${where}.exit must be { node: <bodyId>, when: { path, <operator> } }`);\n } else if (!bodyIds.has(exitRaw.node)) {\n problems.push(`${where}.exit.node \"${exitRaw.node}\" is not a body node`);\n } else {\n const exitNode = bodyNodes.find((b) => b.id === exitRaw.node);\n if (!exitNode?.resultSchema) {\n problems.push(`${where}.exit.node \"${exitRaw.node}\" must declare a resultSchema \u2014 the exit decision reads its structured result`);\n } else {\n const when = normLoopExitWhen(exitRaw.when, exitNode.resultSchema, `${where}.exit.when`, problems);\n if (when) exit = { node: exitRaw.node, when };\n }\n }\n\n // \u2500\u2500 feedback: previous-iteration product references \u2500\u2500\n const feedback: string[] = [];\n if (n.feedback !== undefined) {\n if (!Array.isArray(n.feedback) || n.feedback.some((x) => typeof x !== 'string')) {\n problems.push(`${where}.feedback must be an array of strings`);\n } else {\n for (const ref of n.feedback as string[]) {\n const dot = ref.lastIndexOf('.');\n const bodyId = dot > 0 ? ref.slice(0, dot) : '';\n const kind = dot > 0 ? ref.slice(dot + 1) : '';\n if (!bodyIds.has(bodyId) || !['result', 'files', 'manifest'].includes(kind)) {\n problems.push(`${where}.feedback \"${ref}\" must be <bodyId>.result | <bodyId>.files | <bodyId>.manifest`);\n continue;\n }\n if (kind === 'result' && !bodyNodes.find((b) => b.id === bodyId)?.resultSchema) {\n problems.push(`${where}.feedback \"${ref}\" requires body node \"${bodyId}\" to declare a resultSchema`);\n continue;\n }\n if (feedback.includes(ref)) {\n problems.push(`${where}.feedback has duplicate \"${ref}\"`);\n continue;\n }\n feedback.push(ref);\n }\n }\n }\n\n // \u2500\u2500 output projection (defaults to the exit node) \u2500\u2500\n let output: V3LoopOutput | undefined;\n if (n.output !== undefined) {\n if (!isObject(n.output) || typeof n.output.from !== 'string' || !bodyIds.has(n.output.from)) {\n problems.push(`${where}.output must be { from: <bodyId> }`);\n } else {\n output = { from: n.output.from };\n }\n } else if (exit) {\n output = { from: exit.node };\n }\n\n if (problems.length > before || maxIterations === undefined || !exit || !output) return undefined;\n return {\n maxIterations,\n body: { nodes: bodyNodes },\n exit,\n feedback,\n output,\n onExhausted: 'blocked',\n sessionPolicy: 'fresh',\n };\n}\n\n/**\n * Validate the exit predicate against the exit node's resultSchema:\n * `path` must be `result.<key>` for a DECLARED + REQUIRED key, and the single\n * comparison operator must be type-compatible with the key (boolean/string \u2192\n * equals/notEquals; number \u2192 also gt/gte/lt/lte; array/object \u2192 unusable).\n */\nfunction normLoopExitWhen(\n v: Record<string, unknown>,\n schema: V3ResultSchema,\n where: string,\n problems: string[],\n): V3LoopExitWhen | undefined {\n const unknown = Object.keys(v).filter((k) => k !== 'path' && !(LOOP_WHEN_OPERATORS as readonly string[]).includes(k));\n if (unknown.length > 0) {\n problems.push(`${where} has unsupported keyword(s): ${unknown.join(', ')} (allowed: path + one of ${LOOP_WHEN_OPERATORS.join('/')})`);\n return undefined;\n }\n const m = typeof v.path === 'string' ? /^result\\.([A-Za-z0-9_-]+)$/.exec(v.path) : null;\n if (!m) {\n problems.push(`${where}.path must be \"result.<key>\" (the resultSchema subset is flat \u2014 no deeper paths)`);\n return undefined;\n }\n const key = m[1]!;\n const prop = schema.properties[key];\n if (!prop) {\n problems.push(`${where}.path references \"${key}\", which is not declared in the exit node's resultSchema`);\n return undefined;\n }\n if (!(schema.required ?? []).includes(key)) {\n problems.push(`${where}.path references \"${key}\", which must be in the exit node's resultSchema.required (otherwise the field may be absent at runtime)`);\n return undefined;\n }\n const ops = LOOP_WHEN_OPERATORS.filter((op) => v[op] !== undefined);\n if (ops.length !== 1) {\n problems.push(`${where} must set exactly ONE operator (${LOOP_WHEN_OPERATORS.join('/')})`);\n return undefined;\n }\n const op = ops[0]!;\n const operand = v[op];\n if (prop.type === 'array' || prop.type === 'object') {\n problems.push(`${where}: cannot compare \"${key}\" \u2014 exit predicates only support string/number/boolean fields`);\n return undefined;\n }\n if (op === 'gt' || op === 'gte' || op === 'lt' || op === 'lte') {\n if (prop.type !== 'number') {\n problems.push(`${where}.${op} requires \"${key}\" to be a number field (it is ${prop.type})`);\n return undefined;\n }\n if (typeof operand !== 'number' || !Number.isFinite(operand)) {\n problems.push(`${where}.${op} must be a finite number`);\n return undefined;\n }\n } else {\n // equals / notEquals \u2014 operand must match the field's primitive type.\n if (typeof operand !== prop.type) {\n problems.push(`${where}.${op} must be a ${prop.type} to match \"${key}\"`);\n return undefined;\n }\n // Enum reconciliation (edge-activation design \u00A72.3): when the field\n // declares a vocabulary, an operand outside it is a validate-time typo,\n // not a runtime surprise \u2014 the seedclaw `decision_values` equivalent.\n if (prop.type === 'string' && prop.enum && !prop.enum.includes(operand as string)) {\n problems.push(\n `${where}.${op} value ${JSON.stringify(operand)} is not in \"${key}\"'s enum [${prop.enum.join(', ')}]`,\n );\n return undefined;\n }\n }\n return { path: v.path as string, [op]: operand } as V3LoopExitWhen;\n}\n\n/**\n * Validate the opt-in `resultSchema` against the supported subset. Strict on\n * purpose: unknown keywords are REJECTED (not ignored) so a schema the\n * validator silently wouldn't enforce can never enter a dag (codex v2 of the\n * blocked design). Caps: \u226432 properties, \u22644KB serialized, flat (depth 1).\n */\nfunction normResultSchema(v: unknown, id: string, problems: string[]): V3ResultSchema | undefined {\n if (v === undefined || v === null) return undefined;\n const where = `node \"${id}\".resultSchema`;\n if (!isObject(v)) {\n problems.push(`${where} must be an object`);\n return undefined;\n }\n const knownTop = new Set(['type', 'properties', 'required']);\n for (const key of Object.keys(v)) {\n if (!knownTop.has(key)) {\n problems.push(`${where} has unsupported keyword \"${key}\" (subset allows: type/properties/required)`);\n return undefined;\n }\n }\n if (v.type !== 'object') {\n problems.push(`${where}.type must be \"object\"`);\n return undefined;\n }\n if (!isObject(v.properties) || Object.keys(v.properties).length === 0) {\n problems.push(`${where}.properties must be a non-empty object`);\n return undefined;\n }\n const props = Object.entries(v.properties);\n if (props.length > RESULT_SCHEMA_MAX_PROPERTIES) {\n problems.push(`${where} has ${props.length} properties (max ${RESULT_SCHEMA_MAX_PROPERTIES})`);\n return undefined;\n }\n const properties: Record<string, { type: V3ResultFieldType; enum?: string[] }> = {};\n for (const [name, spec] of props) {\n if (!isObject(spec)) {\n problems.push(`${where}.properties.${name} must be an object`);\n return undefined;\n }\n for (const key of Object.keys(spec)) {\n if (key !== 'type' && key !== 'enum') {\n problems.push(`${where}.properties.${name} has unsupported keyword \"${key}\" (subset allows: type, enum)`);\n return undefined;\n }\n }\n if (!RESULT_FIELD_TYPES.includes(spec.type as V3ResultFieldType)) {\n problems.push(`${where}.properties.${name}.type must be one of ${RESULT_FIELD_TYPES.join(' | ')}`);\n return undefined;\n }\n let enumValues: string[] | undefined;\n if (spec.enum !== undefined) {\n // enum on STRING fields only (edge-activation design \u00A71.3) \u2014 it anchors\n // edge-predicate vocabulary; other types have nothing to enumerate.\n if (spec.type !== 'string') {\n problems.push(`${where}.properties.${name}.enum is only supported on string fields (it is ${String(spec.type)})`);\n return undefined;\n }\n if (!Array.isArray(spec.enum) || spec.enum.length === 0 || spec.enum.some((x) => typeof x !== 'string' || x.length === 0)) {\n problems.push(`${where}.properties.${name}.enum must be a non-empty array of non-empty strings`);\n return undefined;\n }\n if (spec.enum.length > RESULT_ENUM_MAX_VALUES) {\n problems.push(`${where}.properties.${name}.enum has ${spec.enum.length} values (max ${RESULT_ENUM_MAX_VALUES})`);\n return undefined;\n }\n if (new Set(spec.enum).size !== spec.enum.length) {\n problems.push(`${where}.properties.${name}.enum has duplicates`);\n return undefined;\n }\n const tooLong = (spec.enum as string[]).filter((x) => x.length > RESULT_ENUM_MAX_VALUE_LENGTH);\n if (tooLong.length > 0) {\n problems.push(`${where}.properties.${name}.enum value(s) exceed ${RESULT_ENUM_MAX_VALUE_LENGTH} chars: ${tooLong.join(', ')}`);\n return undefined;\n }\n enumValues = spec.enum as string[];\n }\n properties[name] = enumValues ? { type: spec.type as V3ResultFieldType, enum: enumValues } : { type: spec.type as V3ResultFieldType };\n }\n let required: string[] | undefined;\n if (v.required !== undefined) {\n if (!Array.isArray(v.required) || v.required.some((x) => typeof x !== 'string')) {\n problems.push(`${where}.required must be an array of strings`);\n return undefined;\n }\n const unknown = (v.required as string[]).filter((r) => !(r in properties));\n if (unknown.length > 0) {\n problems.push(`${where}.required references undeclared properties: ${unknown.join(', ')}`);\n return undefined;\n }\n if (new Set(v.required).size !== v.required.length) {\n problems.push(`${where}.required has duplicates`);\n return undefined;\n }\n required = v.required as string[];\n }\n const schema: V3ResultSchema = required ? { type: 'object', properties, required } : { type: 'object', properties };\n const bytes = Buffer.byteLength(JSON.stringify(schema), 'utf-8');\n if (bytes > RESULT_SCHEMA_MAX_BYTES) {\n problems.push(`${where} serializes to ${bytes} bytes (max ${RESULT_SCHEMA_MAX_BYTES})`);\n return undefined;\n }\n return schema;\n}\n\nfunction normInputs(v: unknown, id: string, problems: string[]): V3InputRef[] {\n if (v === undefined) return [];\n if (!Array.isArray(v)) {\n problems.push(`node \"${id}\".inputs must be an array`);\n return [];\n }\n const out: V3InputRef[] = [];\n for (let j = 0; j < v.length; j++) {\n const inp = v[j];\n if (!isObject(inp) || typeof inp.from !== 'string') {\n problems.push(`node \"${id}\".inputs[${j}] must be { from: <nodeId>, select? }`);\n continue;\n }\n const extra = Object.keys(inp).filter((k) => k !== 'from' && k !== 'select');\n if (extra.length > 0) {\n problems.push(`node \"${id}\".inputs[${j}] has unsupported key(s): ${extra.join(', ')} (allowed: from, select)`);\n continue;\n }\n if (inp.select === undefined) {\n out.push({ from: inp.from });\n continue;\n }\n // P3 selector: exactly one of name/path, both non-empty strings.\n if (!isObject(inp.select)) {\n problems.push(`node \"${id}\".inputs[${j}].select must be { name: <string> } or { path: <string> }`);\n continue;\n }\n const selKeys = Object.keys(inp.select);\n const badKeys = selKeys.filter((k) => k !== 'name' && k !== 'path');\n if (badKeys.length > 0 || selKeys.length !== 1) {\n problems.push(`node \"${id}\".inputs[${j}].select must set exactly ONE of name / path`);\n continue;\n }\n const selVal = inp.select.name ?? inp.select.path;\n if (typeof selVal !== 'string' || selVal.trim() === '') {\n problems.push(`node \"${id}\".inputs[${j}].select.${selKeys[0]} must be a non-empty string`);\n continue;\n }\n out.push({\n from: inp.from,\n select: inp.select.name !== undefined ? { name: selVal } : { path: selVal },\n });\n }\n return out;\n}\n\n// \u2500\u2500\u2500 Topological order \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Deterministic topological order via Kahn's algorithm. Ties (nodes with the\n * same remaining in-degree available at once) are broken by ascending id so\n * the schedule is stable across runs \u2014 important for reproducible journals.\n * Throws if the graph contains a cycle (lists the offending nodes).\n *\n * Assumes `depends` already reference existing nodes; `validateDag` enforces\n * that before calling here.\n */\nexport function topologicalOrder(dag: V3Dag): string[] {\n const indeg = new Map<string, number>();\n const adj = new Map<string, string[]>(); // dep \u2192 dependents\n for (const node of dag.nodes) {\n indeg.set(node.id, indeg.get(node.id) ?? 0);\n if (!adj.has(node.id)) adj.set(node.id, []);\n }\n for (const node of dag.nodes) {\n // Conditional and unconditional edges alike count for ordering/acyclicity\n // (edge-activation design H2): an edge that may never activate is still a\n // structural edge \u2014 the graph must be acyclic regardless of run outcomes.\n for (const dep of node.depends) {\n indeg.set(node.id, (indeg.get(node.id) ?? 0) + 1);\n adj.get(dep.from)!.push(node.id);\n }\n }\n\n // Ready set kept sorted for deterministic tie-breaking.\n const ready = [...indeg.entries()].filter(([, d]) => d === 0).map(([id]) => id).sort();\n const order: string[] = [];\n while (ready.length > 0) {\n const id = ready.shift()!;\n order.push(id);\n for (const next of adj.get(id) ?? []) {\n const d = indeg.get(next)! - 1;\n indeg.set(next, d);\n if (d === 0) {\n // Insert keeping `ready` sorted.\n const pos = lowerBound(ready, next);\n ready.splice(pos, 0, next);\n }\n }\n }\n\n if (order.length !== dag.nodes.length) {\n const stuck = dag.nodes.map((n) => n.id).filter((id) => !order.includes(id));\n throw new DagValidationError([`dag has a cycle among nodes: ${stuck.join(', ')}`]);\n }\n return order;\n}\n\n/** Index of the first element in sorted `arr` not less than `x`. */\nfunction lowerBound(arr: string[], x: string): number {\n let lo = 0;\n let hi = arr.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (arr[mid]! < x) lo = mid + 1;\n else hi = mid;\n }\n return lo;\n}\n", "/**\n * v3 orchestrator \u2014 pure decision layer.\n *\n * Mirrors v0.2's `orchestrator.ts` pattern: a pure function maps the current\n * run state + DAG to a list of action descriptors. The runtime (`runtime.ts`)\n * owns every side effect \u2014 journal/STATE writes, ephemeral-worker dispatch via\n * `runNode`, humanGate card posting \u2014 and the per-bot/per-CLI/global\n * concurrency caps. Keeping the decision pure makes the critical-path\n * semantics testable without spawning workers or touching the filesystem.\n *\n * MVP scope: static DAG, fail-fast. No loops / decisions / dynamic expand\n * (those are deferred \u2014 design Q3/\u00A77). Gate set is frozen at authoring time;\n * the orchestrator never invents or skips a gate (design Q10).\n */\n\nimport { isLoopNode, loopInstanceId, topologicalOrder, type V3Dag, type V3Node } from './dag.js';\nimport type { V3LoopRef, V3RunFailureReason } from './event-contract.js';\n\n// \u2500\u2500\u2500 Run state (materialized from the journal by state.ts) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type V3NodeStatus =\n | 'pending' // not yet dispatched (deps may or may not be ready)\n | 'gateWaiting' // humanGate dispatched, awaiting human resolution\n | 'running' // worker dispatched, in flight\n | 'done' // succeeded (work + manifest validated)\n | 'skipped' // triggerRule unsatisfied; acceptable terminal if a sink still reaches done\n | 'cancelled' // early-release loser; neutral terminal, not fail-fast\n | 'blocked' // semantic/contract failure \u2014 recoverable via retry (new attempt)\n | 'superseded' // an INSTANCE refreshed by a cross-node revisit; the node gets\n // a fresh instance and re-dispatches. A settled-terminal for\n // that instance, NOT a failure (instance restoration 2026-06-08)\n | 'failed'; // infrastructure failure / gate rejected / timed out \u2014 needs intervention\n\nexport interface V3NodeState {\n status: V3NodeStatus;\n /** True once an approved humanGate cleared this node \u2014 so after approval the\n * next tick dispatches work instead of re-dispatching the gate. A rejected\n * gate transitions the node straight to `failed` (set by the runtime), so\n * this flag only ever records the approved case. */\n gateCleared?: boolean;\n /** Host-only: the frozen input approved by this gate. It must match the\n * prepared sidecar before the runtime may publish hostEffectIntent. */\n approvedHostInput?: { attemptId: string; approvalDigest: string; inputHash: string };\n /** The current live runtime instance of this DEFINITION node (`A#002`).\n * Set on dispatch; cleared when a revisit supersedes it (the node then\n * re-dispatches a fresh instance). Absent on the pre-instance-layer path\n * (plain nodeId-keyed events) and loop body expansions. */\n effectiveInstanceId?: string;\n}\n\n/** nodeId \u2192 state. A node absent from the map is treated as `pending`. */\nexport type V3RunState = Map<string, V3NodeState>;\n\n/**\n * Per-loop composite state, folded from the loop lifecycle events. A loop\n * absent from the map has not started. The loop's coarse status (running /\n * blocked / done) lives in the regular node-state map under the loop's id;\n * this struct carries what that one enum can't: where the iteration cursor is\n * and what the last decision said.\n */\nexport interface V3LoopState {\n /** Current iteration, 1-based; 0 between loopStarted and the first\n * loopIterationStarted. */\n iteration: number;\n /** True once the CURRENT iteration's decision event is recorded (reset by\n * the next loopIterationStarted). */\n decided: boolean;\n /** The latest decision \u2014 drives what the orchestrator does next. */\n lastDecision?: 'exit' | 'continue' | 'exhausted';\n /** Extra iterations granted (each loopIterationGranted adds one); the\n * effective budget is maxIterations + granted. */\n granted: number;\n /** An appended-but-unconsumed grant (cleared by the next\n * loopIterationStarted) \u2014 the idempotency key for \"already granted\". */\n pendingGrant: boolean;\n}\n\n/** loopId \u2192 loop state. */\nexport type V3LoopRunState = Map<string, V3LoopState>;\n\nexport interface V3EdgeState {\n active: boolean;\n sourceAttemptId: string;\n}\n\n/** `${from}->${to}` \u2192 conditional edge state. */\nexport type V3EdgeRunState = Map<string, V3EdgeState>;\n\nexport interface V3OmittedInput {\n from: string;\n reason: 'edgeInactive' | 'sourceSkipped' | 'sourceCancelled' | 'earlyRelease';\n}\n\n// \u2500\u2500\u2500 Actions (runtime translates each into journal writes + side effects) \u2500\u2500\u2500\n\nexport type V3Action =\n /** Read one source result.json once and append edgeResolved. */\n | { kind: 'resolveEdge'; from: string; to: string }\n /** Mark a node skipped because its triggerRule cannot be satisfied. */\n | { kind: 'skipNode'; nodeId: string; detail?: string }\n /** Abort an early-release loser whose remaining products are no longer used. */\n | { kind: 'cancelNode'; nodeId: string; byNodeId: string; detail?: string }\n /** Post the humanGate approval card + persist a `waits/<id>.json` (Q10). */\n | { kind: 'dispatchGate'; nodeId: string; instanceId?: string }\n /** Spawn an ephemeral worker via `runNode` for this node's goal. `loop` is\n * set for body-instance dispatches (the runtime synthesizes the instance\n * node from the loop's body definition). `instanceId` is set when this is a\n * cross-node-revisit RE-DISPATCH (`A#002`): the prior instance was\n * superseded, so decideNext computes the next instance number deterministically\n * from `state.instances` (constraint 4 \u2014 the action carries it, the runtime\n * does not guess). Absent on a first dispatch / loop body (those keep the\n * pre-instance-layer path until the runtime brick threads instances through). */\n | { kind: 'dispatchWork'; nodeId: string; instanceId?: string; loop?: V3LoopRef; omitted?: V3OmittedInput[] }\n // \u2500\u2500 loop control (the runtime translates each into ONE journal append) \u2500\u2500\n /** Outer deps of a loop are done \u2192 append loopStarted. */\n | { kind: 'startLoop'; loopId: string }\n /** Begin iteration N (first, after a continue-decision, or after a grant). */\n | { kind: 'startLoopIteration'; loopId: string; iteration: number }\n /** Current iteration's body is fully done and undecided \u2192 the runtime reads\n * the exit node's result.json, evaluates exit.when, appends the decision. */\n | { kind: 'evaluateLoopIteration'; loopId: string; iteration: number }\n /** Decision was 'exit' \u2192 seal the loop with a nodeSucceeded on the LOOP id\n * carrying the output projection's manifest (downstream inputs/deps then\n * treat the loop like any done node \u2014 zero special-casing). */\n | { kind: 'completeLoop'; loopId: string; iteration: number }\n /** Terminal: every node done; the run's product is the sink set. */\n | { kind: 'completeRunSucceeded' }\n /** Terminal (fail-fast): a node failed, so the run cannot proceed. */\n | { kind: 'completeRunFailed'; failedNodeId?: string; reason?: V3RunFailureReason; detail?: string }\n /** Terminal-for-now: a node is blocked (contract failure, recoverable).\n * Halts dispatch like failed, but the run can resume via a retry event. */\n | { kind: 'completeRunBlocked'; blockedNodeId: string };\n\n// \u2500\u2500\u2500 Decision function \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Pure decision: given the current `state`, return every action that can be\n * taken *now*. The runtime applies concurrency caps by acting on a prefix of\n * the returned dispatch actions and re-invoking on the next tick \u2014 this\n * function intentionally returns ALL ready dispatches (it does not throttle).\n *\n * Ordering follows topological order so callers see deps-ready nodes first.\n * Fail-fast: the moment any node is `failed`, the only action is\n * `completeRunFailed` (the runtime's attempt-quiescence barrier tears down and\n * proves close for every in-flight peer before publishing the run terminal).\n * When no dispatch is possible and nothing is pending, the run is complete.\n */\nexport function decideNext(\n dag: V3Dag,\n state: V3RunState,\n loops: V3LoopRunState = new Map(),\n edges: V3EdgeRunState = new Map(),\n instances: V3RunState = new Map(),\n): V3Action[] {\n const order = topologicalOrder(dag);\n const nodes = new Map(dag.nodes.map((n) => [n.id, n]));\n\n // Fail-fast sweep first: a single failed node ends the run. Pick the\n // earliest in topo order for a deterministic `failedNodeId`. `failed`\n // (infrastructure, needs intervention) takes priority over `blocked`\n // (contract failure, retryable) when both exist. Loop body instances are\n // swept too \u2014 only the CURRENT iteration can be non-done (a decision is\n // only ever recorded once the whole iteration completed).\n for (const id of order) {\n const node = nodes.get(id)!;\n for (const sweepId of [id, ...currentInstanceIds(node, loops)]) {\n if (\n st(state, sweepId).status === 'failed' &&\n (sweepId !== id || isFailureRelevant(id, dag, state, edges))\n ) {\n return [{ kind: 'completeRunFailed', failedNodeId: sweepId }];\n }\n }\n }\n for (const id of order) {\n const node = nodes.get(id)!;\n for (const sweepId of [id, ...currentInstanceIds(node, loops)]) {\n if (\n st(state, sweepId).status === 'blocked' &&\n (sweepId !== id || isFailureRelevant(id, dag, state, edges))\n ) {\n return [{ kind: 'completeRunBlocked', blockedNodeId: sweepId }];\n }\n }\n }\n\n const actions: V3Action[] = [];\n let pending = 0; // nodes not yet terminal \u2014 gates the success sweep\n\n for (const id of order) {\n const node = nodes.get(id)!;\n const s = st(state, id);\n\n if (isAcceptableTerminal(id, s.status, dag, state, edges)) continue;\n\n if (isLoopNode(node)) {\n pending++;\n const ls = loops.get(id);\n if (!ls) {\n const readiness = readinessFor(node, state, edges);\n if (readiness.kind === 'wait') continue;\n if (readiness.kind === 'resolveEdge') {\n actions.push({ kind: 'resolveEdge', from: readiness.from, to: id });\n continue;\n }\n if (readiness.kind === 'skip') {\n actions.push({ kind: 'skipNode', nodeId: id, detail: readiness.detail });\n continue;\n }\n // Not started: deps/trigger satisfied, start the composite node.\n actions.push({ kind: 'startLoop', loopId: id });\n continue;\n }\n if (ls.iteration === 0) {\n actions.push({ kind: 'startLoopIteration', loopId: id, iteration: 1 });\n continue;\n }\n if (ls.decided) {\n if (ls.lastDecision === 'exit') {\n actions.push({ kind: 'completeLoop', loopId: id, iteration: ls.iteration });\n } else if (\n ls.lastDecision === 'continue' ||\n // After an exhausted-block, a grant re-opens exactly one round. An\n // exhausted loop WITHOUT a pending grant never reaches here \u2014 its\n // node status is 'blocked' and the sweep above already returned.\n (ls.lastDecision === 'exhausted' && ls.pendingGrant)\n ) {\n actions.push({ kind: 'startLoopIteration', loopId: id, iteration: ls.iteration + 1 });\n }\n continue;\n }\n // Undecided current iteration: schedule the body like a mini-DAG over\n // instance ids; once every body instance is done, ask for the decision.\n const bodyOrder = topologicalOrder({ runId: id, nodes: node.body.nodes });\n const bodyById = new Map(node.body.nodes.map((b) => [b.id, b]));\n let allDone = true;\n for (const bodyId of bodyOrder) {\n const instId = loopInstanceId(id, ls.iteration, bodyId);\n const bs = st(state, instId);\n if (bs.status === 'done') continue;\n allDone = false;\n if (bs.status === 'running' || bs.status === 'gateWaiting') continue;\n const bodyDef = bodyById.get(bodyId)!;\n const depsOk = bodyDef.depends.every(\n (dep) => st(state, loopInstanceId(id, ls.iteration, dep.from)).status === 'done',\n );\n if (!depsOk) continue;\n actions.push({\n kind: 'dispatchWork',\n nodeId: instId,\n loop: { loopId: id, iteration: ls.iteration, bodyNodeId: bodyId },\n });\n }\n if (allDone) {\n actions.push({ kind: 'evaluateLoopIteration', loopId: id, iteration: ls.iteration });\n }\n continue;\n }\n\n // In-flight: a dispatched worker or an open gate. Nothing to emit; the\n // node is still pending completion.\n if (s.status === 'running' || s.status === 'gateWaiting') {\n pending++;\n continue;\n }\n\n // s.status === 'pending'\n pending++;\n const readiness = readinessFor(node, state, edges);\n if (readiness.kind === 'wait') continue;\n if (readiness.kind === 'resolveEdge') {\n actions.push({ kind: 'resolveEdge', from: readiness.from, to: id });\n continue;\n }\n if (readiness.kind === 'skip') {\n actions.push({ kind: 'skipNode', nodeId: id, detail: readiness.detail });\n continue;\n }\n\n if (node.humanGate && !s.gateCleared) {\n // The gate belongs to the instance that will run on approval \u2014 same\n // id resolution as dispatchWork (constraint 6), so gate + work share it.\n actions.push({ kind: 'dispatchGate', nodeId: id, instanceId: s.effectiveInstanceId ?? nextInstanceId(id, instances) });\n continue;\n }\n // Every plain-node dispatch runs under a runtime instance (constraint 4 +\n // design rule: \u9996\u6D3E\u4E5F\u8981 #001). decideNext \u2014 not the runtime \u2014 owns the id:\n // - blocked / human-ask RETRY stays in the SAME instance (constraint 5):\n // the node still carries `effectiveInstanceId`, so reuse it (the retry\n // is a new attempt INSIDE it, e.g. A#001/attempts/002).\n // - first dispatch (no effective) \u2192 #001; a revisit re-dispatch (supersede\n // cleared the effective) \u2192 next #NNN from state.instances.\n // (loop body dispatches use loopInstanceId, handled above.)\n const instanceId = s.effectiveInstanceId ?? nextInstanceId(id, instances);\n actions.push({\n kind: 'dispatchWork',\n nodeId: id,\n instanceId,\n ...(readiness.omitted ? { omitted: readiness.omitted } : {}),\n });\n for (const loser of readiness.earlyLosers ?? []) {\n if (canCancelLoser(loser, id, dag, state, edges)) {\n actions.push({\n kind: 'cancelNode',\n nodeId: loser,\n byNodeId: id,\n detail: `early-release loser for \"${id}\"`,\n });\n }\n }\n }\n\n if (actions.length === 0 && pending === 0) {\n const sinks = findSinks(dag);\n if (sinks.some((id) => st(state, id).status === 'done')) {\n return [{ kind: 'completeRunSucceeded' }];\n }\n return [{\n kind: 'completeRunFailed',\n reason: 'allSinksSkipped',\n detail: sinkOmissionDetail(sinks, state),\n }];\n }\n return actions;\n}\n\ntype EdgeActivity =\n | { kind: 'active'; from: string }\n | { kind: 'inactive'; from: string; reason: 'edgeInactive' | 'sourceSkipped' | 'sourceCancelled' }\n | { kind: 'unresolved'; from: string }\n | { kind: 'unsettled'; from: string };\n\ntype NodeReadiness =\n | { kind: 'ready'; omitted?: V3OmittedInput[]; earlyLosers?: string[] }\n | { kind: 'skip'; detail: string }\n | { kind: 'resolveEdge'; from: string }\n | { kind: 'wait' };\n\nfunction readinessFor(node: V3Node, state: V3RunState, edges: V3EdgeRunState): NodeReadiness {\n if (node.depends.length === 0) return { kind: 'ready' };\n const activities = node.depends.map((dep): EdgeActivity => {\n const source = st(state, dep.from);\n if (source.status === 'done') {\n if (!dep.when) return { kind: 'active', from: dep.from };\n const edge = edges.get(currentEdgeKey(dep.from, node.id, state));\n if (!edge) return { kind: 'unresolved', from: dep.from };\n return edge.active\n ? { kind: 'active', from: dep.from }\n : { kind: 'inactive', from: dep.from, reason: 'edgeInactive' };\n }\n if (source.status === 'skipped') return { kind: 'inactive', from: dep.from, reason: 'sourceSkipped' };\n if (source.status === 'cancelled') return { kind: 'inactive', from: dep.from, reason: 'sourceCancelled' };\n return { kind: 'unsettled', from: dep.from };\n });\n\n const active = activities.filter((a) => a.kind === 'active').length;\n const triggerRule = node.triggerRule ?? 'all_success';\n const required =\n triggerRule === 'all_success' ? node.depends.length\n : triggerRule === 'one_success' ? 1\n : triggerRule.quorum;\n\n if (triggerRule === 'all_success') {\n if (activities.some((a) => a.kind === 'unsettled')) return { kind: 'wait' };\n const unresolved = firstUnresolved(activities);\n if (unresolved) return { kind: 'resolveEdge', from: unresolved.from };\n } else if (active < required) {\n const unresolved = firstUnresolved(activities);\n if (unresolved) return { kind: 'resolveEdge', from: unresolved.from };\n }\n\n const maybe = activities.filter((a) => a.kind === 'unsettled' || a.kind === 'unresolved').length;\n if (active >= required) {\n const omitted = activities\n .filter((a): a is Exclude<EdgeActivity, { kind: 'active' }> => a.kind !== 'active')\n .map((a) => ({\n from: a.from,\n reason: a.kind === 'inactive' ? a.reason : 'earlyRelease' as const,\n }));\n const earlyLosers = activities\n .filter((a): a is Extract<EdgeActivity, { kind: 'unsettled' }> => a.kind === 'unsettled')\n .map((a) => a.from);\n return {\n kind: 'ready',\n ...(omitted.length > 0 ? { omitted } : {}),\n ...(earlyLosers.length > 0 ? { earlyLosers } : {}),\n };\n }\n if (active + maybe >= required) return { kind: 'wait' };\n\n const detail = activities\n .map((a, idx) => {\n const dep = node.depends[idx]!;\n if (a.kind === 'active') return `${dep.from}:active`;\n if (a.kind === 'inactive') return `${dep.from}:inactive(${a.reason})`;\n return `${dep.from}:${a.kind}`;\n })\n .join(', ');\n return { kind: 'skip', detail: `triggerRule=${JSON.stringify(triggerRule)} unsatisfied; ${detail}` };\n}\n\nfunction firstUnresolved(activities: EdgeActivity[]): Extract<EdgeActivity, { kind: 'unresolved' }> | undefined {\n return activities.find((a): a is Extract<EdgeActivity, { kind: 'unresolved' }> => a.kind === 'unresolved');\n}\n\n/** The edge key for the source's CURRENT effective instance \u2014 mirrors the key\n * materialize stores edgeResolved under (verdict bound to SOURCE instance, per code\n * review). A source revisit (`A#001`\u2192`A#002`) reads a fresh `A#002->B`, never\n * the superseded `A#001->B`. Target is keyed by nodeId (it has no instance at\n * edge-resolve time); a target-only revisit reusing the verdict is a known,\n * documented limitation. */\nfunction currentEdgeKey(from: string, to: string, state: V3RunState): string {\n const fromInst = st(state, from).effectiveInstanceId ?? from;\n return `${fromInst}->${to}`;\n}\n\n/** The CURRENT iteration's expanded instance ids of a loop node (empty for\n * plain nodes / unstarted loops) \u2014 the sweep surface for failed/blocked. */\nfunction currentInstanceIds(node: V3Node, loops: V3LoopRunState): string[] {\n if (!isLoopNode(node)) return [];\n const ls = loops.get(node.id);\n if (!ls || ls.iteration === 0) return [];\n return node.body.nodes.map((b) => loopInstanceId(node.id, ls.iteration, b.id));\n}\n\nfunction isAcceptableTerminal(\n id: string,\n status: V3NodeStatus,\n dag: V3Dag,\n state: V3RunState,\n edges: V3EdgeRunState,\n): boolean {\n if (status === 'done' || status === 'skipped' || status === 'cancelled') return true;\n if (status === 'failed' || status === 'blocked') return !isFailureRelevant(id, dag, state, edges);\n return false;\n}\n\nfunction canCancelLoser(\n candidateId: string,\n byNodeId: string,\n dag: V3Dag,\n state: V3RunState,\n edges: V3EdgeRunState,\n): boolean {\n const candidate = dag.nodes.find((n) => n.id === candidateId);\n if (!candidate || isLoopNode(candidate)) return false;\n const status = st(state, candidateId).status;\n if (status !== 'pending' && status !== 'gateWaiting' && status !== 'running') return false;\n\n for (const downstream of downstreamsOf(candidateId, dag)) {\n const ds = st(state, downstream.id).status;\n if (terminalStatus(ds)) continue;\n if (downstream.id === byNodeId) continue;\n if (isSatisfiedWithoutSource(downstream, candidateId, state, edges)) continue;\n if (isImpossibleNow(downstream, state, edges)) continue;\n return false;\n }\n return true;\n}\n\nfunction isFailureRelevant(\n failedNodeId: string,\n dag: V3Dag,\n state: V3RunState,\n edges: V3EdgeRunState,\n): boolean {\n const downstreams = downstreamsOf(failedNodeId, dag);\n if (downstreams.length === 0) return true;\n for (const downstream of downstreams) {\n const ds = st(state, downstream.id).status;\n if (terminalStatus(ds)) continue;\n if (!isSatisfiedWithoutSource(downstream, failedNodeId, state, edges)) return true;\n }\n return false;\n}\n\nfunction isSatisfiedWithoutSource(\n node: V3Node,\n ignoredSourceId: string,\n state: V3RunState,\n edges: V3EdgeRunState,\n): boolean {\n const activities = node.depends\n .filter((dep) => dep.from !== ignoredSourceId)\n .map((dep) => edgeActivityFor(dep.from, node.id, dep.when !== undefined, state, edges));\n const active = activities.filter((a) => a.kind === 'active').length;\n return active >= requiredFor(node);\n}\n\nfunction isImpossibleNow(node: V3Node, state: V3RunState, edges: V3EdgeRunState): boolean {\n const activities = node.depends.map((dep) =>\n edgeActivityFor(dep.from, node.id, dep.when !== undefined, state, edges));\n const active = activities.filter((a) => a.kind === 'active').length;\n const maybe = activities.filter((a) => a.kind === 'unsettled' || a.kind === 'unresolved').length;\n return active + maybe < requiredFor(node);\n}\n\nfunction edgeActivityFor(\n from: string,\n to: string,\n conditional: boolean,\n state: V3RunState,\n edges: V3EdgeRunState,\n): EdgeActivity {\n const source = st(state, from);\n if (source.status === 'done') {\n if (!conditional) return { kind: 'active', from };\n const edge = edges.get(currentEdgeKey(from, to, state));\n if (!edge) return { kind: 'unresolved', from };\n return edge.active\n ? { kind: 'active', from }\n : { kind: 'inactive', from, reason: 'edgeInactive' };\n }\n if (source.status === 'skipped') return { kind: 'inactive', from, reason: 'sourceSkipped' };\n if (source.status === 'cancelled') return { kind: 'inactive', from, reason: 'sourceCancelled' };\n return { kind: 'unsettled', from };\n}\n\nfunction requiredFor(node: V3Node): number {\n const triggerRule = node.triggerRule ?? 'all_success';\n return triggerRule === 'all_success' ? node.depends.length\n : triggerRule === 'one_success' ? 1\n : triggerRule.quorum;\n}\n\nfunction downstreamsOf(nodeId: string, dag: V3Dag): V3Node[] {\n return dag.nodes.filter((n) => n.depends.some((dep) => dep.from === nodeId));\n}\n\nfunction terminalStatus(status: V3NodeStatus): boolean {\n return status === 'done' ||\n status === 'skipped' ||\n status === 'cancelled' ||\n status === 'failed' ||\n status === 'blocked' ||\n // A superseded INSTANCE is settled (it won't change); the DEFINITION node\n // re-dispatches under a fresh instance, but that is tracked at the node\n // level (effectiveInstanceId), not by this per-instance status.\n status === 'superseded';\n}\n\nfunction sinkOmissionDetail(sinks: string[], state: V3RunState): string {\n let skipped = 0;\n let cancelled = 0;\n for (const id of sinks) {\n const status = st(state, id).status;\n if (status === 'skipped') skipped++;\n if (status === 'cancelled') cancelled++;\n }\n return `${skipped} skipped, ${cancelled} cancelled`;\n}\n\n/** Sink nodes \u2014 no other node depends on them. Their products are the run's\n * output. Pure helper for the runtime's success path. */\nexport function findSinks(dag: V3Dag): string[] {\n const referenced = new Set<string>();\n for (const node of dag.nodes) for (const dep of node.depends) referenced.add(dep.from);\n return dag.nodes.map((n) => n.id).filter((id) => !referenced.has(id));\n}\n\nfunction st(state: V3RunState, id: string): V3NodeState {\n return state.get(id) ?? { status: 'pending' };\n}\n\n/** The next runtime instance id for a definition node, computed from every\n * instance that has EVER appeared for it (`running/done/blocked/superseded`,\n * per code review \u2014 not just the effective/terminal ones, else a re-dispatch\n * could collide with an existing `A#002`). First dispatch (no instances) \u2192\n * `#001`; a revisit re-dispatch \u2192 `#002`, \u2026 \u2014 instance is the real runtime\n * node, so EVERY plain-node dispatch gets one (design rule: \u9996\u6D3E\u4E5F\u8981 #001). Instance\n * ids are `<nodeId>#NNN`, zero-padded to mirror attempt `001`. */\nfunction nextInstanceId(nodeId: string, instances: V3RunState): string {\n const prefix = `${nodeId}#`;\n let max = 0;\n for (const instanceId of instances.keys()) {\n if (!instanceId.startsWith(prefix)) continue;\n const n = Number.parseInt(instanceId.slice(prefix.length), 10);\n if (Number.isFinite(n) && n > max) max = n;\n }\n return `${prefix}${String(max + 1).padStart(3, '0')}`;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["readiness"]
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=events.js.map
|