@smithers-orchestrator/graph 0.16.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/package.json +35 -0
- package/src/AgentLike.ts +1 -0
- package/src/ApprovalOption.ts +1 -0
- package/src/CachePolicy.ts +1 -0
- package/src/ExtractGraph.ts +1 -0
- package/src/ExtractOptions.ts +1 -0
- package/src/ExtractResult.ts +8 -0
- package/src/GraphSnapshot.ts +9 -0
- package/src/HostElement.ts +1 -0
- package/src/HostNode.ts +1 -0
- package/src/HostText.ts +1 -0
- package/src/MemoryNamespace.ts +1 -0
- package/src/MemoryNamespaceKind.ts +1 -0
- package/src/RetryPolicy.ts +1 -0
- package/src/SamplingConfig.ts +1 -0
- package/src/ScoreResult.ts +1 -0
- package/src/Scorer.ts +1 -0
- package/src/ScorerBinding.ts +1 -0
- package/src/ScorerFn.ts +1 -0
- package/src/ScorerInput.ts +1 -0
- package/src/ScorersMap.ts +1 -0
- package/src/TaskDescriptor.ts +1 -0
- package/src/TaskMemoryConfig.ts +1 -0
- package/src/WorkflowGraph.ts +1 -0
- package/src/XmlElement.ts +1 -0
- package/src/XmlNode.ts +1 -0
- package/src/XmlText.ts +1 -0
- package/src/constants.js +5 -0
- package/src/dom/extract.js +796 -0
- package/src/extract.js +616 -0
- package/src/index.d.ts +212 -0
- package/src/index.js +32 -0
- package/src/types.ts +192 -0
- package/src/utils/tree-ids.js +22 -0
- package/src/utils/xml.js +42 -0
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
// @smithers-type-exports-begin
|
|
2
|
+
/** @typedef {import("../HostElement.ts").HostElement} HostElement */
|
|
3
|
+
/** @typedef {import("../HostText.ts").HostText} HostText */
|
|
4
|
+
// @smithers-type-exports-end
|
|
5
|
+
|
|
6
|
+
import { resolveStableId } from "@smithers-orchestrator/graph/utils/tree-ids";
|
|
7
|
+
import { isAbsolute, resolve as resolvePath } from "node:path";
|
|
8
|
+
import { getTableName } from "drizzle-orm";
|
|
9
|
+
import { DEFAULT_MERGE_QUEUE_CONCURRENCY, WORKTREE_EMPTY_PATH_ERROR, } from "@smithers-orchestrator/graph/constants";
|
|
10
|
+
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
11
|
+
|
|
12
|
+
/** @typedef {import("../ExtractOptions.ts").ExtractOptions} ExtractOptions */
|
|
13
|
+
/** @typedef {import("../ExtractResult.ts").ExtractResult} ExtractResult */
|
|
14
|
+
/** @typedef {import("../HostNode.ts").HostNode} HostNode */
|
|
15
|
+
/** @typedef {import("../XmlNode.ts").XmlNode} XmlNode */
|
|
16
|
+
|
|
17
|
+
// TODO(migration): Delegate extractFromHost to
|
|
18
|
+
// @smithers-orchestrator/graph.extractGraph once core extraction reaches full
|
|
19
|
+
// legacy parity. Current blockers:
|
|
20
|
+
// - <Subflow> and <Sandbox> descriptors here attach runtime computeFn handlers
|
|
21
|
+
// that call executeChildWorkflow/executeSandbox; core extractGraph currently
|
|
22
|
+
// emits extraction metadata only.
|
|
23
|
+
// - Inline <Subflow> validation and some legacy descriptor-shape details still
|
|
24
|
+
// differ, so replacing this implementation would not produce identical output
|
|
25
|
+
// for all inputs.
|
|
26
|
+
const loadRuntimeModule = new Function("specifier", "return import(specifier)");
|
|
27
|
+
// CLI agents (Claude Code, Codex, etc.) can spend minutes reading files and
|
|
28
|
+
// thinking without producing stdout. 60s was too aggressive and caused
|
|
29
|
+
// spurious aborts on complex spec/research/plan generation tasks.
|
|
30
|
+
const DEFAULT_LOCAL_TASK_HEARTBEAT_TIMEOUT_MS = 300_000;
|
|
31
|
+
const DEFAULT_SANDBOX_TASK_HEARTBEAT_TIMEOUT_MS = 300_000;
|
|
32
|
+
/**
|
|
33
|
+
* @param {unknown} value
|
|
34
|
+
* @returns {boolean}
|
|
35
|
+
*/
|
|
36
|
+
function isDrizzleTable(value) {
|
|
37
|
+
if (!value || typeof value !== "object")
|
|
38
|
+
return false;
|
|
39
|
+
try {
|
|
40
|
+
const name = getTableName(value);
|
|
41
|
+
return typeof name === "string" && name.length > 0;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* @param {unknown} value
|
|
49
|
+
* @returns {boolean}
|
|
50
|
+
*/
|
|
51
|
+
function isZodObject(value) {
|
|
52
|
+
return Boolean(value && typeof value === "object" && "shape" in value);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* @param {Record<string, unknown>} raw
|
|
56
|
+
* @returns {number | null}
|
|
57
|
+
*/
|
|
58
|
+
function parseHeartbeatTimeoutMs(raw) {
|
|
59
|
+
const candidate = typeof raw.heartbeatTimeoutMs === "number"
|
|
60
|
+
? raw.heartbeatTimeoutMs
|
|
61
|
+
: typeof raw.heartbeatTimeout === "number"
|
|
62
|
+
? raw.heartbeatTimeout
|
|
63
|
+
: null;
|
|
64
|
+
if (candidate == null || !Number.isFinite(candidate) || candidate <= 0) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return Math.floor(candidate);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* @param {HostNode} node
|
|
71
|
+
* @returns {XmlNode}
|
|
72
|
+
*/
|
|
73
|
+
function toXmlNode(node) {
|
|
74
|
+
if (node.kind === "text") {
|
|
75
|
+
return { kind: "text", text: node.text };
|
|
76
|
+
}
|
|
77
|
+
const element = {
|
|
78
|
+
kind: "element",
|
|
79
|
+
tag: node.tag,
|
|
80
|
+
props: node.props ?? {},
|
|
81
|
+
children: node.children.map(toXmlNode),
|
|
82
|
+
};
|
|
83
|
+
return element;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* @param {ExtractOptions | undefined} opts
|
|
87
|
+
* @param {string} id
|
|
88
|
+
* @returns {number}
|
|
89
|
+
*/
|
|
90
|
+
function getRalphIteration(opts, id) {
|
|
91
|
+
const map = opts?.ralphIterations;
|
|
92
|
+
const fallback = typeof opts?.defaultIteration === "number" ? opts.defaultIteration : 0;
|
|
93
|
+
if (!map)
|
|
94
|
+
return fallback;
|
|
95
|
+
if (map instanceof Map) {
|
|
96
|
+
return map.get(id) ?? fallback;
|
|
97
|
+
}
|
|
98
|
+
const value = map[id];
|
|
99
|
+
return typeof value === "number" ? value : fallback;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* @param {Record<string, unknown>} raw
|
|
103
|
+
*/
|
|
104
|
+
function resolveRetryConfig(raw) {
|
|
105
|
+
const noRetry = Boolean(raw.noRetry);
|
|
106
|
+
const continueOnFail = Boolean(raw.continueOnFail);
|
|
107
|
+
const hasExplicitRetries = typeof raw.retries === "number" && !Number.isNaN(raw.retries);
|
|
108
|
+
const hasExplicitRetryPolicy = Boolean(raw.retryPolicy && typeof raw.retryPolicy === "object");
|
|
109
|
+
const defaultNoRetryForContinueOnFail = continueOnFail && !hasExplicitRetries && !hasExplicitRetryPolicy;
|
|
110
|
+
const retries = noRetry || defaultNoRetryForContinueOnFail
|
|
111
|
+
? 0
|
|
112
|
+
: hasExplicitRetries
|
|
113
|
+
? /** @type {number} */ (raw.retries)
|
|
114
|
+
: Infinity;
|
|
115
|
+
const retryPolicy = hasExplicitRetryPolicy
|
|
116
|
+
? /** @type {import("../RetryPolicy.ts").RetryPolicy} */ (raw.retryPolicy)
|
|
117
|
+
: retries > 0
|
|
118
|
+
? { backoff: "exponential", initialDelayMs: 1000 }
|
|
119
|
+
: undefined;
|
|
120
|
+
return { retries, retryPolicy };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* @param {HostNode | null} root
|
|
124
|
+
* @param {ExtractOptions} [opts]
|
|
125
|
+
* @returns {ExtractResult}
|
|
126
|
+
*/
|
|
127
|
+
export function extractFromHost(root, opts) {
|
|
128
|
+
if (!root) {
|
|
129
|
+
return { xml: null, tasks: [], mountedTaskIds: [] };
|
|
130
|
+
}
|
|
131
|
+
const tasks = [];
|
|
132
|
+
const mountedTaskIds = [];
|
|
133
|
+
const seen = new Set();
|
|
134
|
+
const seenRalph = new Set();
|
|
135
|
+
const seenWorktree = new Set();
|
|
136
|
+
const seenSaga = new Set();
|
|
137
|
+
const seenTcf = new Set();
|
|
138
|
+
let ordinal = 0;
|
|
139
|
+
/**
|
|
140
|
+
* @param {"parallel" | "merge-queue"} tag
|
|
141
|
+
* @param {any} raw
|
|
142
|
+
* @param {number[]} path
|
|
143
|
+
* @param {{ id: string; max?: number }[]} stack
|
|
144
|
+
*/
|
|
145
|
+
function pushGroup(tag, raw, path, stack) {
|
|
146
|
+
const id = resolveStableId(raw?.id, tag, path);
|
|
147
|
+
// Coerce numeric strings (e.g. from MDX) in line with scheduler.parseNum
|
|
148
|
+
const n = Number(raw?.maxConcurrency);
|
|
149
|
+
const rawMax = Number.isFinite(n) ? Math.floor(n) : undefined;
|
|
150
|
+
// Concurrency semantics:
|
|
151
|
+
// - merge-queue: default to 1 and always clamp to >= 1
|
|
152
|
+
// - parallel: undefined => unlimited; <= 0 => unlimited; fractional floored
|
|
153
|
+
let max;
|
|
154
|
+
if (tag === "merge-queue") {
|
|
155
|
+
const base = rawMax ?? DEFAULT_MERGE_QUEUE_CONCURRENCY;
|
|
156
|
+
max = Math.max(1, base);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
if (rawMax == null) {
|
|
160
|
+
max = undefined;
|
|
161
|
+
}
|
|
162
|
+
else if (rawMax <= 0) {
|
|
163
|
+
max = undefined; // unbounded for non-positive values
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
max = rawMax; // positive integer; fractional already floored
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return [...stack, { id, max }];
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* @param {{ ralphId: string; iteration: number }[]} loopStack
|
|
173
|
+
* @returns {string}
|
|
174
|
+
*/
|
|
175
|
+
function buildLoopScope(loopStack) {
|
|
176
|
+
if (loopStack.length === 0)
|
|
177
|
+
return "";
|
|
178
|
+
return ("@@" +
|
|
179
|
+
loopStack.map((l) => `${l.ralphId}=${l.iteration}`).join(","));
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* @param {HostNode} node
|
|
183
|
+
* @param {{ path: number[]; iteration: number; ralphId?: string; parentIsRalph: boolean; parallelStack: { id: string; max?: number }[]; worktreeStack: { id: string; path: string; branch?: string; baseBranch?: string }[]; loopStack: { ralphId: string; iteration: number }[]; }} ctx
|
|
184
|
+
*/
|
|
185
|
+
function walk(node, ctx) {
|
|
186
|
+
if (node.kind === "text")
|
|
187
|
+
return;
|
|
188
|
+
let iteration = ctx.iteration;
|
|
189
|
+
const parallelStack = ctx.parallelStack;
|
|
190
|
+
let ralphId = ctx.ralphId;
|
|
191
|
+
const worktreeStack = ctx.worktreeStack;
|
|
192
|
+
let loopStack = ctx.loopStack;
|
|
193
|
+
if (node.tag === "smithers:ralph") {
|
|
194
|
+
if (ctx.parentIsRalph) {
|
|
195
|
+
throw new SmithersError("NESTED_LOOP", "Nested <Ralph> is not supported.");
|
|
196
|
+
}
|
|
197
|
+
const logicalId = resolveStableId(node.rawProps?.id, "ralph", ctx.path);
|
|
198
|
+
// Scope ralph ID by ancestor loop iterations for nested loops
|
|
199
|
+
const scope = buildLoopScope(loopStack);
|
|
200
|
+
const id = logicalId + scope;
|
|
201
|
+
if (seenRalph.has(id)) {
|
|
202
|
+
throw new SmithersError("DUPLICATE_ID", `Duplicate Ralph id detected: ${id}`, { kind: "ralph", id });
|
|
203
|
+
}
|
|
204
|
+
seenRalph.add(id);
|
|
205
|
+
ralphId = id;
|
|
206
|
+
iteration = getRalphIteration(opts, id);
|
|
207
|
+
// Push this loop onto the stack for children
|
|
208
|
+
loopStack = [...loopStack, { ralphId: logicalId, iteration }];
|
|
209
|
+
}
|
|
210
|
+
let nextParallelStack = parallelStack;
|
|
211
|
+
if (node.tag === "smithers:parallel") {
|
|
212
|
+
nextParallelStack = pushGroup("parallel", node.rawProps, ctx.path, parallelStack);
|
|
213
|
+
}
|
|
214
|
+
// Treat <MergeQueue> as a parallel-concurrency group with default 1
|
|
215
|
+
if (node.tag === "smithers:merge-queue") {
|
|
216
|
+
nextParallelStack = pushGroup("merge-queue", node.rawProps, ctx.path, nextParallelStack);
|
|
217
|
+
}
|
|
218
|
+
// Entering a Worktree node: push onto the worktree stack
|
|
219
|
+
let nextWorktreeStack = worktreeStack;
|
|
220
|
+
if (node.tag === "smithers:worktree") {
|
|
221
|
+
const id = resolveStableId(node.rawProps?.id, "worktree", ctx.path);
|
|
222
|
+
if (seenWorktree.has(id)) {
|
|
223
|
+
throw new SmithersError("DUPLICATE_ID", `Duplicate Worktree id detected: ${id}`, { kind: "worktree", id });
|
|
224
|
+
}
|
|
225
|
+
seenWorktree.add(id);
|
|
226
|
+
let pathVal = String(node.rawProps?.path ?? "").trim();
|
|
227
|
+
if (!pathVal) {
|
|
228
|
+
throw new SmithersError("WORKTREE_EMPTY_PATH", WORKTREE_EMPTY_PATH_ERROR);
|
|
229
|
+
}
|
|
230
|
+
const baseRoot = opts?.baseRootDir;
|
|
231
|
+
const base = typeof baseRoot === "string" && baseRoot.length > 0
|
|
232
|
+
? baseRoot
|
|
233
|
+
: process.cwd();
|
|
234
|
+
const normPath = isAbsolute(pathVal)
|
|
235
|
+
? resolvePath(pathVal)
|
|
236
|
+
: resolvePath(base, pathVal);
|
|
237
|
+
const branch = node.rawProps?.branch ? String(node.rawProps.branch) : undefined;
|
|
238
|
+
const baseBranch = node.rawProps?.baseBranch ? String(node.rawProps.baseBranch) : undefined;
|
|
239
|
+
nextWorktreeStack = [...worktreeStack, { id, path: normPath, branch, baseBranch }];
|
|
240
|
+
}
|
|
241
|
+
if (node.tag === "smithers:subflow") {
|
|
242
|
+
const raw = node.rawProps || {};
|
|
243
|
+
const logicalNodeId = raw.id;
|
|
244
|
+
if (!logicalNodeId || typeof logicalNodeId !== "string") {
|
|
245
|
+
throw new SmithersError("TASK_ID_REQUIRED", "Subflow id is required and must be a string.");
|
|
246
|
+
}
|
|
247
|
+
const ancestorScope = loopStack.length > 1
|
|
248
|
+
? buildLoopScope(loopStack.slice(0, -1))
|
|
249
|
+
: "";
|
|
250
|
+
const nodeId = logicalNodeId + ancestorScope;
|
|
251
|
+
if (seen.has(nodeId)) {
|
|
252
|
+
throw new SmithersError("DUPLICATE_ID", `Duplicate Subflow id detected: ${nodeId}`, { kind: "subflow", id: nodeId });
|
|
253
|
+
}
|
|
254
|
+
seen.add(nodeId);
|
|
255
|
+
const outputRaw = raw.output;
|
|
256
|
+
if (!outputRaw) {
|
|
257
|
+
throw new SmithersError("TASK_MISSING_OUTPUT", `Subflow ${nodeId} is missing output.`, { nodeId });
|
|
258
|
+
}
|
|
259
|
+
const outputTable = isDrizzleTable(outputRaw) ? outputRaw : null;
|
|
260
|
+
const outputTableName = outputTable
|
|
261
|
+
? getTableName(outputTable)
|
|
262
|
+
: typeof outputRaw === "string"
|
|
263
|
+
? outputRaw
|
|
264
|
+
: "";
|
|
265
|
+
const outputRef = !outputTable && isZodObject(outputRaw) ? outputRaw : undefined;
|
|
266
|
+
const { retries, retryPolicy } = resolveRetryConfig(raw);
|
|
267
|
+
const timeoutMs = typeof raw.timeoutMs === "number" ? raw.timeoutMs : null;
|
|
268
|
+
const heartbeatTimeoutMs = parseHeartbeatTimeoutMs(raw);
|
|
269
|
+
const continueOnFail = Boolean(raw.continueOnFail);
|
|
270
|
+
const cachePolicy = raw.cache && typeof raw.cache === "object" ? raw.cache : undefined;
|
|
271
|
+
const dependsOn = Array.isArray(raw.dependsOn)
|
|
272
|
+
? raw.dependsOn.filter((v) => typeof v === "string")
|
|
273
|
+
: undefined;
|
|
274
|
+
const needs = raw.needs && typeof raw.needs === "object" && !Array.isArray(raw.needs)
|
|
275
|
+
? Object.fromEntries(Object.entries(raw.needs).filter(([, v]) => typeof v === "string"))
|
|
276
|
+
: undefined;
|
|
277
|
+
const mode = raw.__smithersSubflowMode ?? raw.mode ?? "childRun";
|
|
278
|
+
if (mode === "inline") {
|
|
279
|
+
// Inline mode is represented structurally by the subtree itself.
|
|
280
|
+
// No standalone task descriptor is created for the subflow node.
|
|
281
|
+
// Children are visited in the generic child traversal below.
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
const parallelGroup = nextParallelStack[nextParallelStack.length - 1];
|
|
285
|
+
const topWorktree = nextWorktreeStack[nextWorktreeStack.length - 1];
|
|
286
|
+
const descriptor = {
|
|
287
|
+
nodeId,
|
|
288
|
+
ordinal: ordinal++,
|
|
289
|
+
iteration,
|
|
290
|
+
ralphId,
|
|
291
|
+
worktreeId: topWorktree?.id,
|
|
292
|
+
worktreePath: topWorktree?.path,
|
|
293
|
+
worktreeBranch: topWorktree?.branch,
|
|
294
|
+
worktreeBaseBranch: topWorktree?.baseBranch,
|
|
295
|
+
outputTable,
|
|
296
|
+
outputTableName,
|
|
297
|
+
outputRef,
|
|
298
|
+
outputSchema: undefined,
|
|
299
|
+
dependsOn,
|
|
300
|
+
needs,
|
|
301
|
+
needsApproval: false,
|
|
302
|
+
skipIf: Boolean(raw.skipIf),
|
|
303
|
+
retries,
|
|
304
|
+
retryPolicy,
|
|
305
|
+
timeoutMs,
|
|
306
|
+
heartbeatTimeoutMs,
|
|
307
|
+
continueOnFail,
|
|
308
|
+
cachePolicy,
|
|
309
|
+
agent: undefined,
|
|
310
|
+
prompt: undefined,
|
|
311
|
+
staticPayload: undefined,
|
|
312
|
+
computeFn: async () => {
|
|
313
|
+
const { executeChildWorkflow } = await loadRuntimeModule("@smithers-orchestrator/engine/child-workflow");
|
|
314
|
+
const result = await executeChildWorkflow(undefined, {
|
|
315
|
+
workflow: raw.__smithersSubflowWorkflow,
|
|
316
|
+
input: raw.__smithersSubflowInput,
|
|
317
|
+
rootDir: opts?.baseRootDir,
|
|
318
|
+
workflowPath: opts?.workflowPath ?? undefined,
|
|
319
|
+
});
|
|
320
|
+
if (result.status !== "finished") {
|
|
321
|
+
throw new SmithersError("WORKFLOW_EXECUTION_FAILED", `Subflow ${nodeId} failed with status ${result.status}.`, { nodeId, status: result.status });
|
|
322
|
+
}
|
|
323
|
+
return result.output;
|
|
324
|
+
},
|
|
325
|
+
label: raw.label,
|
|
326
|
+
meta: {
|
|
327
|
+
...raw.meta,
|
|
328
|
+
__subflow: true,
|
|
329
|
+
__subflowMode: mode,
|
|
330
|
+
__subflowInput: raw.__smithersSubflowInput,
|
|
331
|
+
},
|
|
332
|
+
parallelGroupId: parallelGroup?.id,
|
|
333
|
+
parallelMaxConcurrency: parallelGroup?.max,
|
|
334
|
+
};
|
|
335
|
+
tasks.push(descriptor);
|
|
336
|
+
mountedTaskIds.push(`${nodeId}::${iteration}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (node.tag === "smithers:sandbox") {
|
|
340
|
+
const raw = node.rawProps || {};
|
|
341
|
+
const logicalNodeId = raw.id;
|
|
342
|
+
if (!logicalNodeId || typeof logicalNodeId !== "string") {
|
|
343
|
+
throw new SmithersError("TASK_ID_REQUIRED", "Sandbox id is required and must be a string.");
|
|
344
|
+
}
|
|
345
|
+
const ancestorScope = loopStack.length > 1
|
|
346
|
+
? buildLoopScope(loopStack.slice(0, -1))
|
|
347
|
+
: "";
|
|
348
|
+
const nodeId = logicalNodeId + ancestorScope;
|
|
349
|
+
if (seen.has(nodeId)) {
|
|
350
|
+
throw new SmithersError("DUPLICATE_ID", `Duplicate Sandbox id detected: ${nodeId}`, { kind: "sandbox", id: nodeId });
|
|
351
|
+
}
|
|
352
|
+
seen.add(nodeId);
|
|
353
|
+
const outputRaw = raw.output;
|
|
354
|
+
if (!outputRaw) {
|
|
355
|
+
throw new SmithersError("TASK_MISSING_OUTPUT", `Sandbox ${nodeId} is missing output.`, { nodeId });
|
|
356
|
+
}
|
|
357
|
+
const outputTable = isDrizzleTable(outputRaw) ? outputRaw : null;
|
|
358
|
+
const outputTableName = outputTable
|
|
359
|
+
? getTableName(outputTable)
|
|
360
|
+
: typeof outputRaw === "string"
|
|
361
|
+
? outputRaw
|
|
362
|
+
: "";
|
|
363
|
+
const outputRef = !outputTable && isZodObject(outputRaw) ? outputRaw : undefined;
|
|
364
|
+
const { retries, retryPolicy } = resolveRetryConfig(raw);
|
|
365
|
+
const timeoutMs = typeof raw.timeoutMs === "number" ? raw.timeoutMs : null;
|
|
366
|
+
const heartbeatTimeoutMs = parseHeartbeatTimeoutMs(raw) ?? DEFAULT_SANDBOX_TASK_HEARTBEAT_TIMEOUT_MS;
|
|
367
|
+
const continueOnFail = Boolean(raw.continueOnFail);
|
|
368
|
+
const cachePolicy = raw.cache && typeof raw.cache === "object" ? raw.cache : undefined;
|
|
369
|
+
const dependsOn = Array.isArray(raw.dependsOn)
|
|
370
|
+
? raw.dependsOn.filter((v) => typeof v === "string")
|
|
371
|
+
: undefined;
|
|
372
|
+
const needs = raw.needs && typeof raw.needs === "object" && !Array.isArray(raw.needs)
|
|
373
|
+
? Object.fromEntries(Object.entries(raw.needs).filter(([, v]) => typeof v === "string"))
|
|
374
|
+
: undefined;
|
|
375
|
+
const parallelGroup = nextParallelStack[nextParallelStack.length - 1];
|
|
376
|
+
const topWorktree = nextWorktreeStack[nextWorktreeStack.length - 1];
|
|
377
|
+
const runtime = raw.__smithersSandboxRuntime ?? raw.runtime ?? "bubblewrap";
|
|
378
|
+
const workflowDef = raw.__smithersSandboxWorkflow ??
|
|
379
|
+
raw.workflow;
|
|
380
|
+
const descriptor = {
|
|
381
|
+
nodeId,
|
|
382
|
+
ordinal: ordinal++,
|
|
383
|
+
iteration,
|
|
384
|
+
ralphId,
|
|
385
|
+
worktreeId: topWorktree?.id,
|
|
386
|
+
worktreePath: topWorktree?.path,
|
|
387
|
+
worktreeBranch: topWorktree?.branch,
|
|
388
|
+
worktreeBaseBranch: topWorktree?.baseBranch,
|
|
389
|
+
outputTable,
|
|
390
|
+
outputTableName,
|
|
391
|
+
outputRef,
|
|
392
|
+
outputSchema: undefined,
|
|
393
|
+
dependsOn,
|
|
394
|
+
needs,
|
|
395
|
+
needsApproval: false,
|
|
396
|
+
skipIf: Boolean(raw.skipIf),
|
|
397
|
+
retries,
|
|
398
|
+
retryPolicy,
|
|
399
|
+
timeoutMs,
|
|
400
|
+
heartbeatTimeoutMs,
|
|
401
|
+
continueOnFail,
|
|
402
|
+
cachePolicy,
|
|
403
|
+
agent: undefined,
|
|
404
|
+
prompt: undefined,
|
|
405
|
+
staticPayload: undefined,
|
|
406
|
+
computeFn: async () => {
|
|
407
|
+
const { executeSandbox } = await loadRuntimeModule("@smithers-orchestrator/sandbox/execute");
|
|
408
|
+
if (!workflowDef) {
|
|
409
|
+
throw new SmithersError("INVALID_INPUT", `Sandbox ${nodeId} is missing workflow definition.`, { nodeId });
|
|
410
|
+
}
|
|
411
|
+
return executeSandbox({
|
|
412
|
+
parentWorkflow: workflowDef && typeof workflowDef === "object" && "build" in workflowDef
|
|
413
|
+
? workflowDef
|
|
414
|
+
: undefined,
|
|
415
|
+
sandboxId: nodeId,
|
|
416
|
+
runtime: runtime === "docker" || runtime === "codeplane" || runtime === "bubblewrap"
|
|
417
|
+
? runtime
|
|
418
|
+
: "bubblewrap",
|
|
419
|
+
workflow: workflowDef,
|
|
420
|
+
input: raw.__smithersSandboxInput ?? raw.input,
|
|
421
|
+
rootDir: topWorktree?.path ?? process.cwd(),
|
|
422
|
+
allowNetwork: Boolean(raw.allowNetwork),
|
|
423
|
+
maxOutputBytes: 200_000,
|
|
424
|
+
toolTimeoutMs: 60_000,
|
|
425
|
+
reviewDiffs: raw.reviewDiffs,
|
|
426
|
+
autoAcceptDiffs: raw.autoAcceptDiffs,
|
|
427
|
+
config: {
|
|
428
|
+
image: raw.image,
|
|
429
|
+
env: raw.env,
|
|
430
|
+
ports: raw.ports,
|
|
431
|
+
volumes: raw.volumes,
|
|
432
|
+
memoryLimit: raw.memoryLimit,
|
|
433
|
+
cpuLimit: raw.cpuLimit,
|
|
434
|
+
command: raw.command,
|
|
435
|
+
workspace: raw.workspace,
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
},
|
|
439
|
+
label: raw.label,
|
|
440
|
+
meta: {
|
|
441
|
+
...raw.meta,
|
|
442
|
+
__sandbox: true,
|
|
443
|
+
__sandboxRuntime: runtime,
|
|
444
|
+
__sandboxInput: raw.__smithersSandboxInput ?? raw.input,
|
|
445
|
+
},
|
|
446
|
+
parallelGroupId: parallelGroup?.id,
|
|
447
|
+
parallelMaxConcurrency: parallelGroup?.max,
|
|
448
|
+
};
|
|
449
|
+
tasks.push(descriptor);
|
|
450
|
+
mountedTaskIds.push(`${nodeId}::${iteration}`);
|
|
451
|
+
// Isolated subtree: the children execute inside the sandbox child run.
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (node.tag === "smithers:wait-for-event") {
|
|
455
|
+
const raw = node.rawProps || {};
|
|
456
|
+
const logicalNodeId = raw.id;
|
|
457
|
+
if (!logicalNodeId || typeof logicalNodeId !== "string") {
|
|
458
|
+
throw new SmithersError("TASK_ID_REQUIRED", "WaitForEvent id is required and must be a string.");
|
|
459
|
+
}
|
|
460
|
+
const ancestorScope = loopStack.length > 1
|
|
461
|
+
? buildLoopScope(loopStack.slice(0, -1))
|
|
462
|
+
: "";
|
|
463
|
+
const nodeId = logicalNodeId + ancestorScope;
|
|
464
|
+
if (seen.has(nodeId)) {
|
|
465
|
+
throw new SmithersError("DUPLICATE_ID", `Duplicate WaitForEvent id detected: ${nodeId}`, { kind: "wait-for-event", id: nodeId });
|
|
466
|
+
}
|
|
467
|
+
seen.add(nodeId);
|
|
468
|
+
const outputRaw = raw.output;
|
|
469
|
+
if (!outputRaw) {
|
|
470
|
+
throw new SmithersError("TASK_MISSING_OUTPUT", `WaitForEvent ${nodeId} is missing output.`, { nodeId });
|
|
471
|
+
}
|
|
472
|
+
const outputTable = isDrizzleTable(outputRaw) ? outputRaw : null;
|
|
473
|
+
const outputTableName = outputTable
|
|
474
|
+
? getTableName(outputTable)
|
|
475
|
+
: typeof outputRaw === "string"
|
|
476
|
+
? outputRaw
|
|
477
|
+
: "";
|
|
478
|
+
const outputRef = !outputTable && isZodObject(outputRaw) ? outputRaw : undefined;
|
|
479
|
+
const outputSchema = raw.outputSchema ?? outputRef;
|
|
480
|
+
const waitAsync = Boolean(raw.waitAsync);
|
|
481
|
+
const timeoutMs = typeof raw.timeoutMs === "number" ? raw.timeoutMs : null;
|
|
482
|
+
const heartbeatTimeoutMs = parseHeartbeatTimeoutMs(raw);
|
|
483
|
+
const dependsOn = Array.isArray(raw.dependsOn)
|
|
484
|
+
? raw.dependsOn.filter((v) => typeof v === "string")
|
|
485
|
+
: undefined;
|
|
486
|
+
const needs = raw.needs && typeof raw.needs === "object" && !Array.isArray(raw.needs)
|
|
487
|
+
? Object.fromEntries(Object.entries(raw.needs).filter(([, v]) => typeof v === "string"))
|
|
488
|
+
: undefined;
|
|
489
|
+
const onTimeout = raw.__smithersOnTimeout ?? raw.onTimeout ?? "fail";
|
|
490
|
+
const parallelGroup = nextParallelStack[nextParallelStack.length - 1];
|
|
491
|
+
const topWorktree = nextWorktreeStack[nextWorktreeStack.length - 1];
|
|
492
|
+
const descriptor = {
|
|
493
|
+
nodeId,
|
|
494
|
+
ordinal: ordinal++,
|
|
495
|
+
iteration,
|
|
496
|
+
ralphId,
|
|
497
|
+
worktreeId: topWorktree?.id,
|
|
498
|
+
worktreePath: topWorktree?.path,
|
|
499
|
+
worktreeBranch: topWorktree?.branch,
|
|
500
|
+
worktreeBaseBranch: topWorktree?.baseBranch,
|
|
501
|
+
outputTable,
|
|
502
|
+
outputTableName,
|
|
503
|
+
outputRef,
|
|
504
|
+
outputSchema,
|
|
505
|
+
dependsOn,
|
|
506
|
+
needs,
|
|
507
|
+
needsApproval: false,
|
|
508
|
+
waitAsync,
|
|
509
|
+
skipIf: Boolean(raw.skipIf),
|
|
510
|
+
retries: 0,
|
|
511
|
+
timeoutMs,
|
|
512
|
+
heartbeatTimeoutMs,
|
|
513
|
+
continueOnFail: onTimeout === "continue" || onTimeout === "skip",
|
|
514
|
+
agent: undefined,
|
|
515
|
+
prompt: undefined,
|
|
516
|
+
staticPayload: undefined,
|
|
517
|
+
computeFn: undefined,
|
|
518
|
+
label: raw.label,
|
|
519
|
+
meta: {
|
|
520
|
+
...raw.meta,
|
|
521
|
+
__waitForEvent: true,
|
|
522
|
+
__eventName: raw.__smithersEventName ?? raw.event,
|
|
523
|
+
__correlationId: raw.__smithersCorrelationId ?? raw.correlationId,
|
|
524
|
+
__onTimeout: onTimeout,
|
|
525
|
+
},
|
|
526
|
+
parallelGroupId: parallelGroup?.id,
|
|
527
|
+
parallelMaxConcurrency: parallelGroup?.max,
|
|
528
|
+
};
|
|
529
|
+
tasks.push(descriptor);
|
|
530
|
+
mountedTaskIds.push(`${nodeId}::${iteration}`);
|
|
531
|
+
}
|
|
532
|
+
if (node.tag === "smithers:timer") {
|
|
533
|
+
const raw = node.rawProps || {};
|
|
534
|
+
const logicalNodeId = raw.id;
|
|
535
|
+
if (!logicalNodeId || typeof logicalNodeId !== "string") {
|
|
536
|
+
throw new SmithersError("TASK_ID_REQUIRED", "Timer id is required and must be a string.");
|
|
537
|
+
}
|
|
538
|
+
if (logicalNodeId.length > 256) {
|
|
539
|
+
throw new SmithersError("INVALID_INPUT", `Timer id must be 256 characters or fewer (received ${logicalNodeId.length}).`, { nodeId: logicalNodeId, maxLength: 256 });
|
|
540
|
+
}
|
|
541
|
+
const ancestorScope = loopStack.length > 1
|
|
542
|
+
? buildLoopScope(loopStack.slice(0, -1))
|
|
543
|
+
: "";
|
|
544
|
+
const nodeId = logicalNodeId + ancestorScope;
|
|
545
|
+
if (seen.has(nodeId)) {
|
|
546
|
+
throw new SmithersError("DUPLICATE_ID", `Duplicate Timer id detected: ${nodeId}`, { kind: "timer", id: nodeId });
|
|
547
|
+
}
|
|
548
|
+
seen.add(nodeId);
|
|
549
|
+
const duration = typeof (raw.__smithersTimerDuration ?? raw.duration) === "string"
|
|
550
|
+
? String(raw.__smithersTimerDuration ?? raw.duration).trim()
|
|
551
|
+
: "";
|
|
552
|
+
const untilRaw = raw.__smithersTimerUntil ?? raw.until;
|
|
553
|
+
const until = typeof untilRaw === "string"
|
|
554
|
+
? untilRaw.trim()
|
|
555
|
+
: untilRaw instanceof Date
|
|
556
|
+
? untilRaw.toISOString()
|
|
557
|
+
: "";
|
|
558
|
+
const hasDuration = duration.length > 0;
|
|
559
|
+
const hasUntil = until.length > 0;
|
|
560
|
+
if ((hasDuration ? 1 : 0) + (hasUntil ? 1 : 0) !== 1) {
|
|
561
|
+
throw new SmithersError("INVALID_INPUT", `Timer ${nodeId} must define exactly one of duration or until.`, { nodeId, duration: raw.duration, until: raw.until });
|
|
562
|
+
}
|
|
563
|
+
if (raw.every !== undefined) {
|
|
564
|
+
throw new SmithersError("INVALID_INPUT", `Timer ${nodeId} uses every=, but recurring timers are not supported yet.`, { nodeId, every: raw.every });
|
|
565
|
+
}
|
|
566
|
+
const dependsOn = Array.isArray(raw.dependsOn)
|
|
567
|
+
? raw.dependsOn.filter((v) => typeof v === "string")
|
|
568
|
+
: undefined;
|
|
569
|
+
const needs = raw.needs && typeof raw.needs === "object" && !Array.isArray(raw.needs)
|
|
570
|
+
? Object.fromEntries(Object.entries(raw.needs).filter(([, v]) => typeof v === "string"))
|
|
571
|
+
: undefined;
|
|
572
|
+
const parallelGroup = nextParallelStack[nextParallelStack.length - 1];
|
|
573
|
+
const topWorktree = nextWorktreeStack[nextWorktreeStack.length - 1];
|
|
574
|
+
const descriptor = {
|
|
575
|
+
nodeId,
|
|
576
|
+
ordinal: ordinal++,
|
|
577
|
+
iteration,
|
|
578
|
+
ralphId,
|
|
579
|
+
worktreeId: topWorktree?.id,
|
|
580
|
+
worktreePath: topWorktree?.path,
|
|
581
|
+
worktreeBranch: topWorktree?.branch,
|
|
582
|
+
worktreeBaseBranch: topWorktree?.baseBranch,
|
|
583
|
+
outputTable: null,
|
|
584
|
+
outputTableName: "",
|
|
585
|
+
outputRef: undefined,
|
|
586
|
+
outputSchema: undefined,
|
|
587
|
+
dependsOn,
|
|
588
|
+
needs,
|
|
589
|
+
needsApproval: false,
|
|
590
|
+
skipIf: Boolean(raw.skipIf),
|
|
591
|
+
retries: 0,
|
|
592
|
+
timeoutMs: null,
|
|
593
|
+
heartbeatTimeoutMs: null,
|
|
594
|
+
continueOnFail: false,
|
|
595
|
+
cachePolicy: undefined,
|
|
596
|
+
agent: undefined,
|
|
597
|
+
prompt: undefined,
|
|
598
|
+
staticPayload: undefined,
|
|
599
|
+
computeFn: undefined,
|
|
600
|
+
label: raw.label ?? `timer:${nodeId}`,
|
|
601
|
+
meta: {
|
|
602
|
+
...raw.meta,
|
|
603
|
+
__timer: true,
|
|
604
|
+
__timerType: hasDuration ? "duration" : "absolute",
|
|
605
|
+
...(hasDuration ? { __timerDuration: duration } : {}),
|
|
606
|
+
...(hasUntil ? { __timerUntil: until } : {}),
|
|
607
|
+
},
|
|
608
|
+
parallelGroupId: parallelGroup?.id,
|
|
609
|
+
parallelMaxConcurrency: parallelGroup?.max,
|
|
610
|
+
};
|
|
611
|
+
tasks.push(descriptor);
|
|
612
|
+
mountedTaskIds.push(`${nodeId}::${iteration}`);
|
|
613
|
+
}
|
|
614
|
+
// Track Saga nodes for duplicate detection
|
|
615
|
+
if (node.tag === "smithers:saga") {
|
|
616
|
+
const id = resolveStableId(node.rawProps?.id, "saga", ctx.path);
|
|
617
|
+
if (seenSaga.has(id)) {
|
|
618
|
+
throw new SmithersError("DUPLICATE_ID", `Duplicate Saga id detected: ${id}`, { kind: "saga", id });
|
|
619
|
+
}
|
|
620
|
+
seenSaga.add(id);
|
|
621
|
+
}
|
|
622
|
+
// Track TryCatchFinally nodes for duplicate detection
|
|
623
|
+
if (node.tag === "smithers:try-catch-finally") {
|
|
624
|
+
const id = resolveStableId(node.rawProps?.id, "tcf", ctx.path);
|
|
625
|
+
if (seenTcf.has(id)) {
|
|
626
|
+
throw new SmithersError("DUPLICATE_ID", `Duplicate TryCatchFinally id detected: ${id}`, { kind: "try-catch-finally", id });
|
|
627
|
+
}
|
|
628
|
+
seenTcf.add(id);
|
|
629
|
+
}
|
|
630
|
+
if (node.tag === "smithers:task") {
|
|
631
|
+
const raw = node.rawProps || {};
|
|
632
|
+
const logicalNodeId = raw.id;
|
|
633
|
+
if (!logicalNodeId || typeof logicalNodeId !== "string") {
|
|
634
|
+
throw new SmithersError("TASK_ID_REQUIRED", "Task id is required and must be a string.");
|
|
635
|
+
}
|
|
636
|
+
// Scope task nodeId by ancestor loops (all except the innermost, which
|
|
637
|
+
// is already captured by desc.iteration).
|
|
638
|
+
const ancestorScope = loopStack.length > 1
|
|
639
|
+
? buildLoopScope(loopStack.slice(0, -1))
|
|
640
|
+
: "";
|
|
641
|
+
const nodeId = logicalNodeId + ancestorScope;
|
|
642
|
+
if (seen.has(nodeId)) {
|
|
643
|
+
throw new SmithersError("DUPLICATE_ID", `Duplicate Task id detected: ${nodeId}`, { kind: "task", id: nodeId });
|
|
644
|
+
}
|
|
645
|
+
seen.add(nodeId);
|
|
646
|
+
const outputRaw = raw.output;
|
|
647
|
+
if (!outputRaw) {
|
|
648
|
+
throw new SmithersError("TASK_MISSING_OUTPUT", `Task ${nodeId} is missing output.`, { nodeId });
|
|
649
|
+
}
|
|
650
|
+
const outputTable = isDrizzleTable(outputRaw) ? outputRaw : null;
|
|
651
|
+
const outputTableName = outputTable
|
|
652
|
+
? getTableName(outputTable)
|
|
653
|
+
: typeof outputRaw === "string"
|
|
654
|
+
? outputRaw
|
|
655
|
+
: "";
|
|
656
|
+
const outputRef = !outputTable && isZodObject(outputRaw) ? outputRaw : undefined;
|
|
657
|
+
const outputSchema = raw.outputSchema ?? outputRef;
|
|
658
|
+
const needsApproval = Boolean(raw.needsApproval);
|
|
659
|
+
const waitAsync = Boolean(raw.waitAsync);
|
|
660
|
+
const approvalMode = raw.approvalMode === "decision" ||
|
|
661
|
+
raw.approvalMode === "select" ||
|
|
662
|
+
raw.approvalMode === "rank"
|
|
663
|
+
? raw.approvalMode
|
|
664
|
+
: "gate";
|
|
665
|
+
const approvalOnDeny = raw.approvalOnDeny === "continue" ||
|
|
666
|
+
raw.approvalOnDeny === "skip" ||
|
|
667
|
+
raw.approvalOnDeny === "fail"
|
|
668
|
+
? raw.approvalOnDeny
|
|
669
|
+
: undefined;
|
|
670
|
+
const approvalOptions = Array.isArray(raw.approvalOptions)
|
|
671
|
+
? raw.approvalOptions
|
|
672
|
+
.filter((value) => Boolean(value && typeof value === "object" && !Array.isArray(value)))
|
|
673
|
+
.map((value) => ({
|
|
674
|
+
key: typeof value.key === "string" ? value.key : "",
|
|
675
|
+
label: typeof value.label === "string" ? value.label : "",
|
|
676
|
+
...(typeof value.summary === "string" ? { summary: value.summary } : {}),
|
|
677
|
+
...(value.metadata && typeof value.metadata === "object" && !Array.isArray(value.metadata)
|
|
678
|
+
? { metadata: value.metadata }
|
|
679
|
+
: {}),
|
|
680
|
+
}))
|
|
681
|
+
.filter((value) => value.key && value.label)
|
|
682
|
+
: undefined;
|
|
683
|
+
const approvalAllowedScopes = Array.isArray(raw.approvalAllowedScopes)
|
|
684
|
+
? raw.approvalAllowedScopes.filter((value) => typeof value === "string")
|
|
685
|
+
: undefined;
|
|
686
|
+
const approvalAllowedUsers = Array.isArray(raw.approvalAllowedUsers)
|
|
687
|
+
? raw.approvalAllowedUsers.filter((value) => typeof value === "string")
|
|
688
|
+
: undefined;
|
|
689
|
+
const approvalAutoApprove = raw.approvalAutoApprove && typeof raw.approvalAutoApprove === "object" && !Array.isArray(raw.approvalAutoApprove)
|
|
690
|
+
? {
|
|
691
|
+
...(typeof raw.approvalAutoApprove.after === "number"
|
|
692
|
+
? { after: raw.approvalAutoApprove.after }
|
|
693
|
+
: {}),
|
|
694
|
+
...(typeof raw.approvalAutoApprove.audit === "boolean"
|
|
695
|
+
? { audit: raw.approvalAutoApprove.audit }
|
|
696
|
+
: {}),
|
|
697
|
+
...(typeof raw.approvalAutoApprove.conditionMet === "boolean"
|
|
698
|
+
? { conditionMet: raw.approvalAutoApprove.conditionMet }
|
|
699
|
+
: {}),
|
|
700
|
+
...(typeof raw.approvalAutoApprove.revertOnMet === "boolean"
|
|
701
|
+
? { revertOnMet: raw.approvalAutoApprove.revertOnMet }
|
|
702
|
+
: {}),
|
|
703
|
+
}
|
|
704
|
+
: undefined;
|
|
705
|
+
const skipIf = Boolean(raw.skipIf);
|
|
706
|
+
const { retries, retryPolicy } = resolveRetryConfig(raw);
|
|
707
|
+
const timeoutMs = typeof raw.timeoutMs === "number" ? raw.timeoutMs : null;
|
|
708
|
+
const parsedHeartbeatTimeoutMs = parseHeartbeatTimeoutMs(raw);
|
|
709
|
+
const continueOnFail = Boolean(raw.continueOnFail);
|
|
710
|
+
const cachePolicy = raw.cache && typeof raw.cache === "object" ? raw.cache : undefined;
|
|
711
|
+
const agent = raw.agent;
|
|
712
|
+
const kind = raw.__smithersKind;
|
|
713
|
+
const isAgent = kind === "agent" || Boolean(agent);
|
|
714
|
+
const heartbeatTimeoutMs = parsedHeartbeatTimeoutMs ??
|
|
715
|
+
(isAgent ? DEFAULT_LOCAL_TASK_HEARTBEAT_TIMEOUT_MS : null);
|
|
716
|
+
const prompt = isAgent ? String(raw.children ?? "") : undefined;
|
|
717
|
+
if (prompt === "[object Object]") {
|
|
718
|
+
throw new SmithersError("MDX_PRELOAD_INACTIVE", `Task "${raw.id ?? nodeId}" prompt resolved to [object Object] — MDX preload is likely not active.\n` +
|
|
719
|
+
`Check that bunfig.toml has a top-level preload (not under [run]) and mdxPlugin() is registered.`);
|
|
720
|
+
}
|
|
721
|
+
const isCompute = kind === "compute" && typeof raw.__smithersComputeFn === "function";
|
|
722
|
+
const computeFn = isCompute ? raw.__smithersComputeFn : undefined;
|
|
723
|
+
const staticPayload = isAgent || isCompute
|
|
724
|
+
? undefined
|
|
725
|
+
: (raw.__smithersPayload ?? raw.__payload ?? raw.children);
|
|
726
|
+
const dependsOn = Array.isArray(raw.dependsOn)
|
|
727
|
+
? raw.dependsOn.filter((value) => typeof value === "string")
|
|
728
|
+
: undefined;
|
|
729
|
+
const needs = raw.needs && typeof raw.needs === "object" && !Array.isArray(raw.needs)
|
|
730
|
+
? Object.fromEntries(Object.entries(raw.needs).filter(([, value]) => typeof value === "string"))
|
|
731
|
+
: undefined;
|
|
732
|
+
const parallelGroup = nextParallelStack[nextParallelStack.length - 1];
|
|
733
|
+
const topWorktree = nextWorktreeStack[nextWorktreeStack.length - 1];
|
|
734
|
+
const descriptor = {
|
|
735
|
+
nodeId,
|
|
736
|
+
ordinal: ordinal++,
|
|
737
|
+
iteration,
|
|
738
|
+
ralphId,
|
|
739
|
+
worktreeId: topWorktree?.id,
|
|
740
|
+
worktreePath: topWorktree?.path,
|
|
741
|
+
worktreeBranch: topWorktree?.branch,
|
|
742
|
+
worktreeBaseBranch: topWorktree?.baseBranch,
|
|
743
|
+
outputTable,
|
|
744
|
+
outputTableName,
|
|
745
|
+
outputRef,
|
|
746
|
+
outputSchema,
|
|
747
|
+
dependsOn,
|
|
748
|
+
needs,
|
|
749
|
+
needsApproval,
|
|
750
|
+
waitAsync,
|
|
751
|
+
approvalMode,
|
|
752
|
+
approvalOnDeny,
|
|
753
|
+
approvalOptions,
|
|
754
|
+
approvalAllowedScopes,
|
|
755
|
+
approvalAllowedUsers,
|
|
756
|
+
approvalAutoApprove,
|
|
757
|
+
skipIf,
|
|
758
|
+
retries,
|
|
759
|
+
retryPolicy,
|
|
760
|
+
timeoutMs,
|
|
761
|
+
heartbeatTimeoutMs,
|
|
762
|
+
continueOnFail,
|
|
763
|
+
cachePolicy,
|
|
764
|
+
agent,
|
|
765
|
+
prompt,
|
|
766
|
+
staticPayload,
|
|
767
|
+
computeFn,
|
|
768
|
+
label: raw.label,
|
|
769
|
+
meta: raw.meta,
|
|
770
|
+
scorers: raw.scorers,
|
|
771
|
+
parallelGroupId: parallelGroup?.id,
|
|
772
|
+
parallelMaxConcurrency: parallelGroup?.max,
|
|
773
|
+
memoryConfig: raw.memory ?? undefined,
|
|
774
|
+
};
|
|
775
|
+
// Worktree path is captured in typed fields (worktreeId/worktreePath) and
|
|
776
|
+
// consumed by the engine; avoid attaching untyped ad-hoc properties.
|
|
777
|
+
tasks.push(descriptor);
|
|
778
|
+
mountedTaskIds.push(`${nodeId}::${iteration}`);
|
|
779
|
+
}
|
|
780
|
+
let elementIndex = 0;
|
|
781
|
+
for (const child of node.children) {
|
|
782
|
+
const nextPath = child.kind === "element" ? [...ctx.path, elementIndex++] : ctx.path;
|
|
783
|
+
walk(child, {
|
|
784
|
+
path: nextPath,
|
|
785
|
+
iteration,
|
|
786
|
+
ralphId,
|
|
787
|
+
parentIsRalph: node.tag === "smithers:ralph",
|
|
788
|
+
parallelStack: nextParallelStack,
|
|
789
|
+
worktreeStack: nextWorktreeStack,
|
|
790
|
+
loopStack,
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
walk(root, { path: [], iteration: 0, parentIsRalph: false, parallelStack: [], worktreeStack: [], loopStack: [] });
|
|
795
|
+
return { xml: toXmlNode(root), tasks, mountedTaskIds };
|
|
796
|
+
}
|