substrate-ai 0.18.0 → 0.19.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.
|
@@ -12259,7 +12259,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
12259
12259
|
* @returns Sorted, deduplicated array of story keys in "N-M" format
|
|
12260
12260
|
*/
|
|
12261
12261
|
async function resolveStoryKeys(db, projectRoot, opts) {
|
|
12262
|
-
if (opts?.explicit !== void 0 && opts.explicit.length > 0) return opts.explicit;
|
|
12262
|
+
if (opts?.explicit !== void 0 && opts.explicit.length > 0) return topologicalSortByDependencies(opts.explicit, projectRoot);
|
|
12263
12263
|
let keys = [];
|
|
12264
12264
|
const readyKeys = await db.queryReadyStories();
|
|
12265
12265
|
if (readyKeys.length > 0) {
|
|
@@ -12406,6 +12406,12 @@ function findEpicsFile(projectRoot) {
|
|
|
12406
12406
|
const fullPath = join$1(projectRoot, candidate);
|
|
12407
12407
|
if (existsSync(fullPath)) return fullPath;
|
|
12408
12408
|
}
|
|
12409
|
+
const planningDir = join$1(projectRoot, "_bmad-output", "planning-artifacts");
|
|
12410
|
+
if (existsSync(planningDir)) try {
|
|
12411
|
+
const entries = readdirSync(planningDir, { encoding: "utf-8" });
|
|
12412
|
+
const match$1 = entries.filter((e) => /^epics[-.].*\.md$/i.test(e) && !/^epic-\d+/.test(e)).sort();
|
|
12413
|
+
if (match$1.length > 0) return join$1(planningDir, match$1[0]);
|
|
12414
|
+
} catch {}
|
|
12409
12415
|
return void 0;
|
|
12410
12416
|
}
|
|
12411
12417
|
/**
|
|
@@ -12512,6 +12518,112 @@ function sortStoryKeys(keys) {
|
|
|
12512
12518
|
return a.localeCompare(b);
|
|
12513
12519
|
});
|
|
12514
12520
|
}
|
|
12521
|
+
/**
|
|
12522
|
+
* Parse inter-story dependencies from the consolidated epics document.
|
|
12523
|
+
*
|
|
12524
|
+
* Scans for patterns like:
|
|
12525
|
+
* ### Story 50-2: Title
|
|
12526
|
+
* **Dependencies:** 50-1
|
|
12527
|
+
*
|
|
12528
|
+
* Returns a Map where key=storyKey, value=Set of dependency keys.
|
|
12529
|
+
* Only returns dependencies that are within the provided storyKeys set
|
|
12530
|
+
* (external dependencies to other epics are ignored for ordering purposes).
|
|
12531
|
+
*/
|
|
12532
|
+
function parseEpicsDependencies(projectRoot, storyKeys) {
|
|
12533
|
+
const deps = new Map();
|
|
12534
|
+
const epicsPath = findEpicsFile(projectRoot);
|
|
12535
|
+
if (epicsPath === void 0) return deps;
|
|
12536
|
+
let content;
|
|
12537
|
+
try {
|
|
12538
|
+
content = readFileSync(epicsPath, "utf-8");
|
|
12539
|
+
} catch {
|
|
12540
|
+
return deps;
|
|
12541
|
+
}
|
|
12542
|
+
const storyPattern = /^###\s+Story\s+(\d+)-(\d+)[:\s]/gm;
|
|
12543
|
+
const depPattern = /^\*\*Dependencies:\*\*\s*(.+)$/gm;
|
|
12544
|
+
const storyPositions = [];
|
|
12545
|
+
let match$1;
|
|
12546
|
+
while ((match$1 = storyPattern.exec(content)) !== null) storyPositions.push({
|
|
12547
|
+
key: `${match$1[1]}-${match$1[2]}`,
|
|
12548
|
+
pos: match$1.index
|
|
12549
|
+
});
|
|
12550
|
+
for (let i = 0; i < storyPositions.length; i++) {
|
|
12551
|
+
const story = storyPositions[i];
|
|
12552
|
+
const nextStoryPos = i + 1 < storyPositions.length ? storyPositions[i + 1].pos : content.length;
|
|
12553
|
+
const section = content.slice(story.pos, nextStoryPos);
|
|
12554
|
+
depPattern.lastIndex = 0;
|
|
12555
|
+
const depMatch = depPattern.exec(section);
|
|
12556
|
+
if (depMatch === null || /^none$/i.test(depMatch[1].trim())) continue;
|
|
12557
|
+
const depText = depMatch[1];
|
|
12558
|
+
const storyDeps = new Set();
|
|
12559
|
+
const rangeMatch = /(\d+)-(\d+)\s+through\s+\1-(\d+)/i.exec(depText);
|
|
12560
|
+
if (rangeMatch !== null) {
|
|
12561
|
+
const epic = rangeMatch[1];
|
|
12562
|
+
const start = Number(rangeMatch[2]);
|
|
12563
|
+
const end = Number(rangeMatch[3]);
|
|
12564
|
+
for (let n$1 = start; n$1 <= end; n$1++) {
|
|
12565
|
+
const depKey = `${epic}-${n$1}`;
|
|
12566
|
+
if (storyKeys.has(depKey)) storyDeps.add(depKey);
|
|
12567
|
+
}
|
|
12568
|
+
} else {
|
|
12569
|
+
const keyPattern = /(\d+-\d+[a-z]?)/g;
|
|
12570
|
+
let km;
|
|
12571
|
+
while ((km = keyPattern.exec(depText)) !== null) {
|
|
12572
|
+
const depKey = km[1];
|
|
12573
|
+
if (storyKeys.has(depKey)) storyDeps.add(depKey);
|
|
12574
|
+
}
|
|
12575
|
+
}
|
|
12576
|
+
if (storyDeps.size > 0) deps.set(story.key, storyDeps);
|
|
12577
|
+
}
|
|
12578
|
+
return deps;
|
|
12579
|
+
}
|
|
12580
|
+
/**
|
|
12581
|
+
* Topologically sort explicit story keys by inter-story dependencies.
|
|
12582
|
+
*
|
|
12583
|
+
* Parses the consolidated epics document for dependency metadata, builds
|
|
12584
|
+
* a DAG, and returns keys in dependency-first order using Kahn's algorithm.
|
|
12585
|
+
* Stories with no dependencies come first; stories that depend on others
|
|
12586
|
+
* are placed after their prerequisites.
|
|
12587
|
+
*
|
|
12588
|
+
* Falls back to numeric sort if no epics document exists or no
|
|
12589
|
+
* dependencies are found among the provided keys.
|
|
12590
|
+
*/
|
|
12591
|
+
function topologicalSortByDependencies(keys, projectRoot) {
|
|
12592
|
+
if (keys.length <= 1) return keys;
|
|
12593
|
+
const keySet = new Set(keys);
|
|
12594
|
+
const deps = parseEpicsDependencies(projectRoot, keySet);
|
|
12595
|
+
if (deps.size === 0) return sortStoryKeys(keys);
|
|
12596
|
+
const inDegree = new Map();
|
|
12597
|
+
const successors = new Map();
|
|
12598
|
+
for (const key of keys) {
|
|
12599
|
+
inDegree.set(key, 0);
|
|
12600
|
+
successors.set(key, new Set());
|
|
12601
|
+
}
|
|
12602
|
+
for (const [dependent, depSet] of deps) {
|
|
12603
|
+
if (!keySet.has(dependent)) continue;
|
|
12604
|
+
for (const dep of depSet) {
|
|
12605
|
+
if (!keySet.has(dep)) continue;
|
|
12606
|
+
successors.get(dep).add(dependent);
|
|
12607
|
+
inDegree.set(dependent, (inDegree.get(dependent) ?? 0) + 1);
|
|
12608
|
+
}
|
|
12609
|
+
}
|
|
12610
|
+
const result = [];
|
|
12611
|
+
const processed = new Set();
|
|
12612
|
+
while (processed.size < keys.length) {
|
|
12613
|
+
const wave = [];
|
|
12614
|
+
for (const key of keys) if (!processed.has(key) && (inDegree.get(key) ?? 0) === 0) wave.push(key);
|
|
12615
|
+
if (wave.length === 0) {
|
|
12616
|
+
for (const key of sortStoryKeys(keys)) if (!processed.has(key)) result.push(key);
|
|
12617
|
+
break;
|
|
12618
|
+
}
|
|
12619
|
+
for (const key of sortStoryKeys(wave)) {
|
|
12620
|
+
result.push(key);
|
|
12621
|
+
processed.add(key);
|
|
12622
|
+
for (const succ of successors.get(key) ?? []) inDegree.set(succ, (inDegree.get(succ) ?? 0) - 1);
|
|
12623
|
+
}
|
|
12624
|
+
}
|
|
12625
|
+
return result;
|
|
12626
|
+
}
|
|
12515
12627
|
|
|
12516
12628
|
//#endregion
|
|
12517
12629
|
//#region src/modules/phase-orchestrator/phase-detection.ts
|
|
@@ -21100,7 +21212,9 @@ function buildGraphNode(id, attrs, subgraphClass, defaultMaxRetries) {
|
|
|
21100
21212
|
autoStatus: attrBool(attrs, "auto_status", true),
|
|
21101
21213
|
allowPartial: attrBool(attrs, "allow_partial", false),
|
|
21102
21214
|
toolCommand: attrStr(attrs, "tool_command", ""),
|
|
21103
|
-
backend: attrStr(attrs, "backend", "")
|
|
21215
|
+
backend: attrStr(attrs, "backend", ""),
|
|
21216
|
+
maxParallel: attrInt(attrs, "maxParallel", 0),
|
|
21217
|
+
joinPolicy: attrStr(attrs, "joinPolicy", "")
|
|
21104
21218
|
};
|
|
21105
21219
|
}
|
|
21106
21220
|
function buildGraphEdge(fromNode, toNode, attrs) {
|
|
@@ -21776,7 +21890,7 @@ const conditionSyntaxRule = {
|
|
|
21776
21890
|
const diagnostics = [];
|
|
21777
21891
|
for (let i = 0; i < graph.edges.length; i++) {
|
|
21778
21892
|
const edge = graph.edges[i];
|
|
21779
|
-
if (edge.condition) try {
|
|
21893
|
+
if (edge.condition && !edge.condition.trim().startsWith("llm:")) try {
|
|
21780
21894
|
parseCondition(edge.condition);
|
|
21781
21895
|
} catch (err) {
|
|
21782
21896
|
if (err instanceof ConditionParseError) diagnostics.push({
|
|
@@ -21969,6 +22083,102 @@ function createValidator() {
|
|
|
21969
22083
|
};
|
|
21970
22084
|
}
|
|
21971
22085
|
|
|
22086
|
+
//#endregion
|
|
22087
|
+
//#region packages/factory/dist/graph/llm-evaluator.js
|
|
22088
|
+
/**
|
|
22089
|
+
* LLM-based edge condition evaluator for the factory graph engine.
|
|
22090
|
+
*
|
|
22091
|
+
* Provides pure, side-effect-free functions for detecting `llm:` prefixed
|
|
22092
|
+
* conditions, building evaluation prompts, parsing boolean responses, and
|
|
22093
|
+
* executing an LLM-backed condition evaluation with safe error handling.
|
|
22094
|
+
*
|
|
22095
|
+
* Zero external package imports — all LLM wiring is done by the caller
|
|
22096
|
+
* (edge-selector.ts) via the injectable `llmCall` parameter.
|
|
22097
|
+
*
|
|
22098
|
+
* Story 50-4.
|
|
22099
|
+
*/
|
|
22100
|
+
/**
|
|
22101
|
+
* Returns `true` iff the condition string starts with the `"llm:"` prefix
|
|
22102
|
+
* (after trimming leading/trailing whitespace).
|
|
22103
|
+
*/
|
|
22104
|
+
function isLlmCondition(condition) {
|
|
22105
|
+
return condition.trim().startsWith("llm:");
|
|
22106
|
+
}
|
|
22107
|
+
/**
|
|
22108
|
+
* Strips the `"llm:"` prefix and trims surrounding whitespace from the
|
|
22109
|
+
* remainder of the condition string.
|
|
22110
|
+
*
|
|
22111
|
+
* @param condition - A condition string beginning with `"llm:"`.
|
|
22112
|
+
* @returns The extracted question text, trimmed.
|
|
22113
|
+
*/
|
|
22114
|
+
function extractLlmQuestion(condition) {
|
|
22115
|
+
return condition.trim().slice(4).trim();
|
|
22116
|
+
}
|
|
22117
|
+
/**
|
|
22118
|
+
* Builds an LLM evaluation prompt that includes the question and a JSON
|
|
22119
|
+
* block of the current context snapshot, with an explicit instruction to
|
|
22120
|
+
* answer with only "yes" or "no".
|
|
22121
|
+
*
|
|
22122
|
+
* @param question - The routing question extracted from the condition.
|
|
22123
|
+
* @param contextSnapshot - Shallow copy of the current execution context.
|
|
22124
|
+
* @returns The fully constructed prompt string.
|
|
22125
|
+
*/
|
|
22126
|
+
function buildEvaluationPrompt(question, contextSnapshot) {
|
|
22127
|
+
return [
|
|
22128
|
+
`You are evaluating a routing condition in a software pipeline.`,
|
|
22129
|
+
``,
|
|
22130
|
+
`Context:`,
|
|
22131
|
+
JSON.stringify(contextSnapshot, null, 2),
|
|
22132
|
+
``,
|
|
22133
|
+
`Question: ${question}`,
|
|
22134
|
+
``,
|
|
22135
|
+
`Answer with exactly "yes" or "no".`
|
|
22136
|
+
].join("\n");
|
|
22137
|
+
}
|
|
22138
|
+
/**
|
|
22139
|
+
* Parses an LLM yes/no response into a boolean value.
|
|
22140
|
+
*
|
|
22141
|
+
* Returns `true` if the cleaned (trimmed, lowercased) response starts with or
|
|
22142
|
+
* contains one of: `"yes"`, `"true"`, `"affirmative"`, `"correct"`, `"1"`.
|
|
22143
|
+
* Returns `false` otherwise, including for an empty string.
|
|
22144
|
+
*
|
|
22145
|
+
* @param response - Raw text response from the LLM.
|
|
22146
|
+
* @returns `true` for affirmative responses, `false` for all others.
|
|
22147
|
+
*/
|
|
22148
|
+
function parseLlmBoolResponse(response) {
|
|
22149
|
+
const cleaned = response.trim().toLowerCase();
|
|
22150
|
+
const affirmatives = [
|
|
22151
|
+
"yes",
|
|
22152
|
+
"true",
|
|
22153
|
+
"affirmative",
|
|
22154
|
+
"correct",
|
|
22155
|
+
"1"
|
|
22156
|
+
];
|
|
22157
|
+
return affirmatives.some((token) => cleaned === token || cleaned.startsWith(token + " ") || cleaned.startsWith(token + "\n"));
|
|
22158
|
+
}
|
|
22159
|
+
/**
|
|
22160
|
+
* Evaluates an LLM edge condition asynchronously.
|
|
22161
|
+
*
|
|
22162
|
+
* Builds the evaluation prompt, calls the injectable `llmCall` function,
|
|
22163
|
+
* and parses the response via `parseLlmBoolResponse`. If any step throws,
|
|
22164
|
+
* returns `false` silently — never re-throws.
|
|
22165
|
+
*
|
|
22166
|
+
* @param question - The routing question to evaluate.
|
|
22167
|
+
* @param contextSnapshot - Shallow copy of the current execution context.
|
|
22168
|
+
* @param llmCall - Injectable async function that calls an LLM and
|
|
22169
|
+
* returns the raw text response.
|
|
22170
|
+
* @returns `true` if the LLM responds affirmatively, `false` otherwise or on error.
|
|
22171
|
+
*/
|
|
22172
|
+
async function evaluateLlmCondition(question, contextSnapshot, llmCall) {
|
|
22173
|
+
try {
|
|
22174
|
+
const prompt = buildEvaluationPrompt(question, contextSnapshot);
|
|
22175
|
+
const response = await llmCall(prompt);
|
|
22176
|
+
return parseLlmBoolResponse(response);
|
|
22177
|
+
} catch {
|
|
22178
|
+
return false;
|
|
22179
|
+
}
|
|
22180
|
+
}
|
|
22181
|
+
|
|
21972
22182
|
//#endregion
|
|
21973
22183
|
//#region packages/factory/dist/graph/edge-selector.js
|
|
21974
22184
|
/**
|
|
@@ -22010,6 +22220,7 @@ function bestByWeightThenLexical(edges) {
|
|
|
22010
22220
|
* Select the best outgoing edge from `node` according to the 5-step Attractor spec.
|
|
22011
22221
|
*
|
|
22012
22222
|
* Step 1: Condition-matched edges — highest weight, lexical tiebreak.
|
|
22223
|
+
* Edges with `llm:` prefix conditions are evaluated via LLM call.
|
|
22013
22224
|
* Step 2: Preferred label match on unconditional edges — first match wins.
|
|
22014
22225
|
* Step 3: Suggested next IDs on unconditional edges — first suggestedNextId wins.
|
|
22015
22226
|
* Step 4: Highest weight among all unconditional edges.
|
|
@@ -22019,14 +22230,48 @@ function bestByWeightThenLexical(edges) {
|
|
|
22019
22230
|
* @param outcome - The outcome returned by the node's handler.
|
|
22020
22231
|
* @param context - The current execution context (used for condition evaluation).
|
|
22021
22232
|
* @param graph - The full graph (source of edges).
|
|
22233
|
+
* @param options - Optional injectable overrides (e.g. llmCall for testing).
|
|
22022
22234
|
* @returns The selected `GraphEdge`, or `null` if no outgoing edges exist.
|
|
22023
22235
|
*/
|
|
22024
|
-
function selectEdge(node, outcome, context, graph) {
|
|
22236
|
+
async function selectEdge(node, outcome, context, graph, options) {
|
|
22025
22237
|
const outgoing = graph.edges.filter((e) => e.fromNode === node.id);
|
|
22026
22238
|
if (outgoing.length === 0) return null;
|
|
22239
|
+
const defaultLlmCall$1 = (prompt) => callLLM({
|
|
22240
|
+
model: node.llmModel || "claude-haiku-4-5",
|
|
22241
|
+
provider: node.llmProvider || "anthropic",
|
|
22242
|
+
reasoningEffort: "low",
|
|
22243
|
+
prompt
|
|
22244
|
+
}).then((r) => r.text);
|
|
22245
|
+
const llmCall = options?.llmCall ?? defaultLlmCall$1;
|
|
22027
22246
|
const conditionMatches = [];
|
|
22028
22247
|
const snapshot = context.snapshot();
|
|
22029
|
-
for (const edge of outgoing) if (edge.condition && edge.condition.trim() !== "")
|
|
22248
|
+
for (const edge of outgoing) if (edge.condition && edge.condition.trim() !== "") if (isLlmCondition(edge.condition)) {
|
|
22249
|
+
const question = extractLlmQuestion(edge.condition);
|
|
22250
|
+
let evalError = null;
|
|
22251
|
+
const trackingLlmCall = async (prompt) => {
|
|
22252
|
+
try {
|
|
22253
|
+
return await llmCall(prompt);
|
|
22254
|
+
} catch (err) {
|
|
22255
|
+
evalError = err instanceof Error ? err.message : String(err);
|
|
22256
|
+
throw err;
|
|
22257
|
+
}
|
|
22258
|
+
};
|
|
22259
|
+
const matched = await evaluateLlmCondition(question, snapshot, trackingLlmCall);
|
|
22260
|
+
options?.eventBus?.emit("graph:llm-edge-evaluated", {
|
|
22261
|
+
runId: options.runId ?? "unknown",
|
|
22262
|
+
nodeId: node.id,
|
|
22263
|
+
question,
|
|
22264
|
+
result: matched
|
|
22265
|
+
});
|
|
22266
|
+
context.set("llm.edge_eval_count", context.getNumber("llm.edge_eval_count", 0) + 1);
|
|
22267
|
+
if (evalError !== null) {
|
|
22268
|
+
const existing = context.get("llm.edge_eval_errors");
|
|
22269
|
+
const errors = Array.isArray(existing) ? existing : [];
|
|
22270
|
+
errors.push(evalError);
|
|
22271
|
+
context.set("llm.edge_eval_errors", errors);
|
|
22272
|
+
}
|
|
22273
|
+
if (matched) conditionMatches.push(edge);
|
|
22274
|
+
} else try {
|
|
22030
22275
|
if (evaluateCondition(edge.condition, snapshot)) conditionMatches.push(edge);
|
|
22031
22276
|
} catch {}
|
|
22032
22277
|
if (conditionMatches.length > 0) return bestByWeightThenLexical(conditionMatches);
|
|
@@ -22987,6 +23232,111 @@ function resolveFidelity(node, incomingEdge, graph) {
|
|
|
22987
23232
|
return "";
|
|
22988
23233
|
}
|
|
22989
23234
|
|
|
23235
|
+
//#endregion
|
|
23236
|
+
//#region packages/factory/dist/stylesheet/resolver.js
|
|
23237
|
+
/**
|
|
23238
|
+
* Specificity-based resolver for the model stylesheet.
|
|
23239
|
+
*
|
|
23240
|
+
* Given a parsed stylesheet and a graph node, `resolveNodeStyles` determines
|
|
23241
|
+
* which LLM routing properties apply to that node by:
|
|
23242
|
+
* 1. Filtering rules to only those whose selector matches the node.
|
|
23243
|
+
* 2. Sorting matching rules by specificity ascending (stable sort so that
|
|
23244
|
+
* equal-specificity rules preserve source order).
|
|
23245
|
+
* 3. Iterating the sorted list and letting each rule overwrite properties
|
|
23246
|
+
* — the last rule at the highest specificity wins; for ties the rule
|
|
23247
|
+
* appearing later in the original stylesheet wins.
|
|
23248
|
+
*
|
|
23249
|
+
* **Caller contract**: `resolveNodeStyles` does NOT enforce the "explicit node
|
|
23250
|
+
* attribute wins" rule. The caller (executor / node preparation layer) is
|
|
23251
|
+
* responsible for the final merge:
|
|
23252
|
+
* ```typescript
|
|
23253
|
+
* const resolved = resolveNodeStyles(node, stylesheet)
|
|
23254
|
+
* const finalModel = node.llmModel || resolved.llmModel || graph.defaultLlmModel || ''
|
|
23255
|
+
* ```
|
|
23256
|
+
*
|
|
23257
|
+
* Story 42-7.
|
|
23258
|
+
*/
|
|
23259
|
+
/**
|
|
23260
|
+
* Return `true` if the given `selector` matches the provided `node`.
|
|
23261
|
+
*
|
|
23262
|
+
* Matching rules:
|
|
23263
|
+
* - `universal`: always matches every node.
|
|
23264
|
+
* - `shape`: matches when `node.shape === selector.value`.
|
|
23265
|
+
* - `class`: matches when the node's `class` field, split on commas and
|
|
23266
|
+
* trimmed, contains `selector.value` (case-sensitive).
|
|
23267
|
+
* - `id`: matches when `node.id === selector.value`.
|
|
23268
|
+
*/
|
|
23269
|
+
function matchesNode(node, selector) {
|
|
23270
|
+
switch (selector.type) {
|
|
23271
|
+
case "universal": return true;
|
|
23272
|
+
case "shape": return node.shape === selector.value;
|
|
23273
|
+
case "class": {
|
|
23274
|
+
const tokens = node.class.split(",").map((t) => t.trim());
|
|
23275
|
+
return tokens.includes(selector.value);
|
|
23276
|
+
}
|
|
23277
|
+
case "id": return node.id === selector.value;
|
|
23278
|
+
default: return false;
|
|
23279
|
+
}
|
|
23280
|
+
}
|
|
23281
|
+
/**
|
|
23282
|
+
* Resolve LLM routing properties for a single graph node using the parsed stylesheet.
|
|
23283
|
+
*
|
|
23284
|
+
* @param node - The graph node to resolve properties for.
|
|
23285
|
+
* @param stylesheet - A parsed stylesheet (array of `StylesheetRule` in source order).
|
|
23286
|
+
* @returns A `ResolvedNodeStyles` object containing only the properties that
|
|
23287
|
+
* were resolved from matching rules. Properties absent from matching rules
|
|
23288
|
+
* are omitted (not set to `undefined` explicitly).
|
|
23289
|
+
*
|
|
23290
|
+
* **Important**: this function does NOT check the node's own attributes.
|
|
23291
|
+
* Callers must give explicit node attributes (`node.llmModel`, etc.) priority
|
|
23292
|
+
* over the values returned here.
|
|
23293
|
+
*/
|
|
23294
|
+
function resolveNodeStyles(node, stylesheet) {
|
|
23295
|
+
const matchingRules = stylesheet.filter((rule) => matchesNode(node, rule.selector));
|
|
23296
|
+
matchingRules.sort((a, b) => a.selector.specificity - b.selector.specificity);
|
|
23297
|
+
const resolved = {};
|
|
23298
|
+
for (const rule of matchingRules) for (const decl of rule.declarations) if (decl.property === "llm_model") resolved.llmModel = decl.value;
|
|
23299
|
+
else if (decl.property === "llm_provider") resolved.llmProvider = decl.value;
|
|
23300
|
+
else if (decl.property === "reasoning_effort") resolved.reasoningEffort = decl.value;
|
|
23301
|
+
return resolved;
|
|
23302
|
+
}
|
|
23303
|
+
|
|
23304
|
+
//#endregion
|
|
23305
|
+
//#region packages/factory/dist/graph/transformer.js
|
|
23306
|
+
/**
|
|
23307
|
+
* Apply stylesheet rules to all nodes in the given graph.
|
|
23308
|
+
*
|
|
23309
|
+
* Merges `inheritedStylesheet` (parent rules) with the graph's own
|
|
23310
|
+
* `modelStylesheet` rules, parent rules first so that local child rules win at
|
|
23311
|
+
* equal specificity via source-order tie-breaking (later rule wins).
|
|
23312
|
+
*
|
|
23313
|
+
* For each node, the resolved properties are applied only if the node does NOT
|
|
23314
|
+
* already have a non-empty explicit value (explicit values are preserved).
|
|
23315
|
+
*
|
|
23316
|
+
* This function **mutates** graph nodes in-place — consistent with how the
|
|
23317
|
+
* DOT parser sets node fields after parsing.
|
|
23318
|
+
*
|
|
23319
|
+
* **Idempotency**: calling `applyStylesheet` twice on the same graph is safe
|
|
23320
|
+
* because nodes with an existing non-empty `llmModel` (or `llmProvider` /
|
|
23321
|
+
* `reasoningEffort`) are never overwritten.
|
|
23322
|
+
*
|
|
23323
|
+
* @param graph - The graph whose nodes should be styled.
|
|
23324
|
+
* @param inheritedStylesheet - Optional stylesheet rules inherited from a
|
|
23325
|
+
* parent graph. Prepended before the graph's own rules so that child local
|
|
23326
|
+
* rules win at equal specificity. Pass `undefined` when there is no parent.
|
|
23327
|
+
*/
|
|
23328
|
+
function applyStylesheet(graph, inheritedStylesheet) {
|
|
23329
|
+
const localRules = graph.modelStylesheet ? parseStylesheet(graph.modelStylesheet) : [];
|
|
23330
|
+
const effectiveStylesheet = [...inheritedStylesheet ?? [], ...localRules];
|
|
23331
|
+
if (effectiveStylesheet.length === 0) return;
|
|
23332
|
+
for (const node of graph.nodes.values()) {
|
|
23333
|
+
const resolved = resolveNodeStyles(node, effectiveStylesheet);
|
|
23334
|
+
if (!node.llmModel && resolved.llmModel) node.llmModel = resolved.llmModel;
|
|
23335
|
+
if (!node.llmProvider && resolved.llmProvider) node.llmProvider = resolved.llmProvider;
|
|
23336
|
+
if (!node.reasoningEffort && resolved.reasoningEffort) node.reasoningEffort = resolved.reasoningEffort;
|
|
23337
|
+
}
|
|
23338
|
+
}
|
|
23339
|
+
|
|
22990
23340
|
//#endregion
|
|
22991
23341
|
//#region packages/factory/dist/graph/executor.js
|
|
22992
23342
|
/**
|
|
@@ -23081,6 +23431,7 @@ async function dispatchWithRetry(node, context, graph, config, nodeRetries) {
|
|
|
23081
23431
|
*/
|
|
23082
23432
|
function createGraphExecutor() {
|
|
23083
23433
|
return { async run(graph, config) {
|
|
23434
|
+
applyStylesheet(graph, config.inheritedStylesheet);
|
|
23084
23435
|
const checkpointManager = new CheckpointManager();
|
|
23085
23436
|
const checkpointFilePath = path.join(config.logsRoot, "checkpoint.json");
|
|
23086
23437
|
const controller = createConvergenceController();
|
|
@@ -23176,7 +23527,10 @@ function createGraphExecutor() {
|
|
|
23176
23527
|
if (resumeState.completedNodes.has(checkpoint.currentNode)) {
|
|
23177
23528
|
const lastNode = graph.nodes.get(checkpoint.currentNode);
|
|
23178
23529
|
if (lastNode) {
|
|
23179
|
-
const nextEdge = selectEdge(lastNode, { status: "SUCCESS" }, context, graph
|
|
23530
|
+
const nextEdge = await selectEdge(lastNode, { status: "SUCCESS" }, context, graph, {
|
|
23531
|
+
...config.eventBus !== void 0 ? { eventBus: config.eventBus } : {},
|
|
23532
|
+
runId: config.runId
|
|
23533
|
+
});
|
|
23180
23534
|
if (nextEdge) {
|
|
23181
23535
|
const nextNode = graph.nodes.get(nextEdge.toNode);
|
|
23182
23536
|
if (!nextNode) throw new Error(`Edge target node "${nextEdge.toNode}" not found in graph`);
|
|
@@ -23189,6 +23543,7 @@ function createGraphExecutor() {
|
|
|
23189
23543
|
else currentNode = graph.startNode();
|
|
23190
23544
|
}
|
|
23191
23545
|
} else currentNode = graph.startNode();
|
|
23546
|
+
context.set("__runId", config.runId ?? "unknown");
|
|
23192
23547
|
while (true) {
|
|
23193
23548
|
const sessionResult = sessionManager.checkBudget((config.wallClockCapMs ?? 0) / 1e3);
|
|
23194
23549
|
if (!sessionResult.allowed) {
|
|
@@ -23279,7 +23634,10 @@ function createGraphExecutor() {
|
|
|
23279
23634
|
return { status: "SUCCESS" };
|
|
23280
23635
|
}
|
|
23281
23636
|
if (resumeCompletedSet?.has(currentNode.id)) {
|
|
23282
|
-
const skipEdge = selectEdge(currentNode, { status: "SUCCESS" }, context, graph
|
|
23637
|
+
const skipEdge = await selectEdge(currentNode, { status: "SUCCESS" }, context, graph, {
|
|
23638
|
+
...config.eventBus !== void 0 ? { eventBus: config.eventBus } : {},
|
|
23639
|
+
runId: config.runId
|
|
23640
|
+
});
|
|
23283
23641
|
if (!skipEdge) {
|
|
23284
23642
|
await persistExit("failed", `No outgoing edge from node ${currentNode.id}`);
|
|
23285
23643
|
return {
|
|
@@ -23464,7 +23822,10 @@ function createGraphExecutor() {
|
|
|
23464
23822
|
};
|
|
23465
23823
|
}
|
|
23466
23824
|
}
|
|
23467
|
-
const edge = selectEdge(currentNode, outcome, context, graph
|
|
23825
|
+
const edge = await selectEdge(currentNode, outcome, context, graph, {
|
|
23826
|
+
...config.eventBus !== void 0 ? { eventBus: config.eventBus } : {},
|
|
23827
|
+
runId: config.runId
|
|
23828
|
+
});
|
|
23468
23829
|
if (!edge) {
|
|
23469
23830
|
await persistExit("failed", `No outgoing edge from node ${currentNode.id}`);
|
|
23470
23831
|
return {
|
|
@@ -31087,75 +31448,6 @@ const conditionalHandler = async (_node, _context, _graph) => {
|
|
|
31087
31448
|
return { status: "SUCCESS" };
|
|
31088
31449
|
};
|
|
31089
31450
|
|
|
31090
|
-
//#endregion
|
|
31091
|
-
//#region packages/factory/dist/stylesheet/resolver.js
|
|
31092
|
-
/**
|
|
31093
|
-
* Specificity-based resolver for the model stylesheet.
|
|
31094
|
-
*
|
|
31095
|
-
* Given a parsed stylesheet and a graph node, `resolveNodeStyles` determines
|
|
31096
|
-
* which LLM routing properties apply to that node by:
|
|
31097
|
-
* 1. Filtering rules to only those whose selector matches the node.
|
|
31098
|
-
* 2. Sorting matching rules by specificity ascending (stable sort so that
|
|
31099
|
-
* equal-specificity rules preserve source order).
|
|
31100
|
-
* 3. Iterating the sorted list and letting each rule overwrite properties
|
|
31101
|
-
* — the last rule at the highest specificity wins; for ties the rule
|
|
31102
|
-
* appearing later in the original stylesheet wins.
|
|
31103
|
-
*
|
|
31104
|
-
* **Caller contract**: `resolveNodeStyles` does NOT enforce the "explicit node
|
|
31105
|
-
* attribute wins" rule. The caller (executor / node preparation layer) is
|
|
31106
|
-
* responsible for the final merge:
|
|
31107
|
-
* ```typescript
|
|
31108
|
-
* const resolved = resolveNodeStyles(node, stylesheet)
|
|
31109
|
-
* const finalModel = node.llmModel || resolved.llmModel || graph.defaultLlmModel || ''
|
|
31110
|
-
* ```
|
|
31111
|
-
*
|
|
31112
|
-
* Story 42-7.
|
|
31113
|
-
*/
|
|
31114
|
-
/**
|
|
31115
|
-
* Return `true` if the given `selector` matches the provided `node`.
|
|
31116
|
-
*
|
|
31117
|
-
* Matching rules:
|
|
31118
|
-
* - `universal`: always matches every node.
|
|
31119
|
-
* - `shape`: matches when `node.shape === selector.value`.
|
|
31120
|
-
* - `class`: matches when the node's `class` field, split on commas and
|
|
31121
|
-
* trimmed, contains `selector.value` (case-sensitive).
|
|
31122
|
-
* - `id`: matches when `node.id === selector.value`.
|
|
31123
|
-
*/
|
|
31124
|
-
function matchesNode(node, selector) {
|
|
31125
|
-
switch (selector.type) {
|
|
31126
|
-
case "universal": return true;
|
|
31127
|
-
case "shape": return node.shape === selector.value;
|
|
31128
|
-
case "class": {
|
|
31129
|
-
const tokens = node.class.split(",").map((t) => t.trim());
|
|
31130
|
-
return tokens.includes(selector.value);
|
|
31131
|
-
}
|
|
31132
|
-
case "id": return node.id === selector.value;
|
|
31133
|
-
default: return false;
|
|
31134
|
-
}
|
|
31135
|
-
}
|
|
31136
|
-
/**
|
|
31137
|
-
* Resolve LLM routing properties for a single graph node using the parsed stylesheet.
|
|
31138
|
-
*
|
|
31139
|
-
* @param node - The graph node to resolve properties for.
|
|
31140
|
-
* @param stylesheet - A parsed stylesheet (array of `StylesheetRule` in source order).
|
|
31141
|
-
* @returns A `ResolvedNodeStyles` object containing only the properties that
|
|
31142
|
-
* were resolved from matching rules. Properties absent from matching rules
|
|
31143
|
-
* are omitted (not set to `undefined` explicitly).
|
|
31144
|
-
*
|
|
31145
|
-
* **Important**: this function does NOT check the node's own attributes.
|
|
31146
|
-
* Callers must give explicit node attributes (`node.llmModel`, etc.) priority
|
|
31147
|
-
* over the values returned here.
|
|
31148
|
-
*/
|
|
31149
|
-
function resolveNodeStyles(node, stylesheet) {
|
|
31150
|
-
const matchingRules = stylesheet.filter((rule) => matchesNode(node, rule.selector));
|
|
31151
|
-
matchingRules.sort((a, b) => a.selector.specificity - b.selector.specificity);
|
|
31152
|
-
const resolved = {};
|
|
31153
|
-
for (const rule of matchingRules) for (const decl of rule.declarations) if (decl.property === "llm_model") resolved.llmModel = decl.value;
|
|
31154
|
-
else if (decl.property === "llm_provider") resolved.llmProvider = decl.value;
|
|
31155
|
-
else if (decl.property === "reasoning_effort") resolved.reasoningEffort = decl.value;
|
|
31156
|
-
return resolved;
|
|
31157
|
-
}
|
|
31158
|
-
|
|
31159
31451
|
//#endregion
|
|
31160
31452
|
//#region packages/factory/dist/handlers/codergen-handler.js
|
|
31161
31453
|
const DEFAULT_MODEL = "claude-sonnet-4-5";
|
|
@@ -31423,32 +31715,788 @@ function createWaitHumanHandler(options) {
|
|
|
31423
31715
|
}
|
|
31424
31716
|
|
|
31425
31717
|
//#endregion
|
|
31426
|
-
//#region packages/factory/dist/handlers/
|
|
31427
|
-
|
|
31428
|
-
|
|
31429
|
-
|
|
31430
|
-
|
|
31431
|
-
|
|
31432
|
-
|
|
31433
|
-
|
|
31434
|
-
|
|
31435
|
-
|
|
31436
|
-
|
|
31437
|
-
|
|
31438
|
-
|
|
31439
|
-
|
|
31440
|
-
|
|
31441
|
-
|
|
31442
|
-
|
|
31443
|
-
|
|
31444
|
-
|
|
31445
|
-
|
|
31446
|
-
|
|
31447
|
-
|
|
31448
|
-
|
|
31449
|
-
|
|
31450
|
-
|
|
31451
|
-
|
|
31718
|
+
//#region packages/factory/dist/handlers/join-policy.js
|
|
31719
|
+
/**
|
|
31720
|
+
* Join policy types, pure evaluator, and branch cancellation manager
|
|
31721
|
+
* for parallel node fan-out/fan-in coordination.
|
|
31722
|
+
*
|
|
31723
|
+
* Design constraints:
|
|
31724
|
+
* - Zero external package imports (pure TypeScript/built-ins only)
|
|
31725
|
+
* - `evaluateJoinPolicy` is a pure synchronous function (no I/O, no side effects)
|
|
31726
|
+
* - `BranchCancellationManager` uses the globally-available AbortController (Node 18+)
|
|
31727
|
+
*
|
|
31728
|
+
* Story 50-3 (AC6).
|
|
31729
|
+
*/
|
|
31730
|
+
/**
|
|
31731
|
+
* Pure synchronous function that decides what the parallel handler should do
|
|
31732
|
+
* after a set of branches has completed.
|
|
31733
|
+
*
|
|
31734
|
+
* Called incrementally after each branch finishes: pass the full `completed`
|
|
31735
|
+
* array (including all branches resolved so far) and the `total` branch count.
|
|
31736
|
+
*
|
|
31737
|
+
* @param config - Join policy configuration parsed from the parallel node.
|
|
31738
|
+
* @param completed - All branch results collected so far (any order).
|
|
31739
|
+
* @param total - Total number of branches in the fan-out set.
|
|
31740
|
+
* @returns A `JoinDecision` indicating whether to continue, wait, or fail.
|
|
31741
|
+
*/
|
|
31742
|
+
function evaluateJoinPolicy(config, completed, total) {
|
|
31743
|
+
const successes = completed.filter((r) => r.outcome === "SUCCESS");
|
|
31744
|
+
const failures = completed.filter((r) => r.outcome === "FAIL");
|
|
31745
|
+
switch (config.policy) {
|
|
31746
|
+
case "wait_all": {
|
|
31747
|
+
if (completed.length < total) return { action: "wait" };
|
|
31748
|
+
return {
|
|
31749
|
+
action: "continue",
|
|
31750
|
+
results: completed
|
|
31751
|
+
};
|
|
31752
|
+
}
|
|
31753
|
+
case "first_success": {
|
|
31754
|
+
if (successes.length >= 1) return {
|
|
31755
|
+
action: "continue",
|
|
31756
|
+
results: completed
|
|
31757
|
+
};
|
|
31758
|
+
if (completed.length >= total) return {
|
|
31759
|
+
action: "fail",
|
|
31760
|
+
reason: `first_success: all ${total} branches failed`
|
|
31761
|
+
};
|
|
31762
|
+
return { action: "wait" };
|
|
31763
|
+
}
|
|
31764
|
+
case "quorum": {
|
|
31765
|
+
const needed = config.quorum_size ?? 1;
|
|
31766
|
+
if (needed <= 0) return {
|
|
31767
|
+
action: "fail",
|
|
31768
|
+
reason: "quorum_size must be >= 1"
|
|
31769
|
+
};
|
|
31770
|
+
if (successes.length >= needed) return {
|
|
31771
|
+
action: "continue",
|
|
31772
|
+
results: completed
|
|
31773
|
+
};
|
|
31774
|
+
const remaining = total - completed.length;
|
|
31775
|
+
if (successes.length + remaining < needed) return {
|
|
31776
|
+
action: "fail",
|
|
31777
|
+
reason: `quorum unreachable: ${failures.length} failed, needed ${needed} of ${total}`
|
|
31778
|
+
};
|
|
31779
|
+
return { action: "wait" };
|
|
31780
|
+
}
|
|
31781
|
+
default: {
|
|
31782
|
+
const _exhaustive = config.policy;
|
|
31783
|
+
return {
|
|
31784
|
+
action: "fail",
|
|
31785
|
+
reason: `unknown join policy: ${String(_exhaustive)}`
|
|
31786
|
+
};
|
|
31787
|
+
}
|
|
31788
|
+
}
|
|
31789
|
+
}
|
|
31790
|
+
/**
|
|
31791
|
+
* Manages one `AbortController` per branch in a parallel fan-out execution.
|
|
31792
|
+
*
|
|
31793
|
+
* Typical usage:
|
|
31794
|
+
* 1. Construct with the total branch count.
|
|
31795
|
+
* 2. Pass `getSignal(i)` to the i-th branch executor.
|
|
31796
|
+
* 3. After the join condition is met, call `cancelRemaining(completedSet)`.
|
|
31797
|
+
* 4. Await `drainAsync(timeoutMs)` to give cancelled branches time to clean up.
|
|
31798
|
+
*/
|
|
31799
|
+
var BranchCancellationManager = class {
|
|
31800
|
+
controllers;
|
|
31801
|
+
/**
|
|
31802
|
+
* @param branchCount - Total number of branches; allocates one AbortController per branch.
|
|
31803
|
+
*/
|
|
31804
|
+
constructor(branchCount) {
|
|
31805
|
+
this.controllers = Array.from({ length: branchCount }, () => new AbortController());
|
|
31806
|
+
}
|
|
31807
|
+
/**
|
|
31808
|
+
* Return the AbortSignal for the branch at the given zero-based index.
|
|
31809
|
+
* The signal is not yet aborted; it becomes aborted when `cancelRemaining` includes the index.
|
|
31810
|
+
*/
|
|
31811
|
+
getSignal(index) {
|
|
31812
|
+
const ctrl = this.controllers[index];
|
|
31813
|
+
if (ctrl === void 0) throw new RangeError(`Branch index ${index} is out of range (branchCount=${this.controllers.length})`);
|
|
31814
|
+
return ctrl.signal;
|
|
31815
|
+
}
|
|
31816
|
+
/**
|
|
31817
|
+
* Abort all branches whose index is NOT in `completedIndices`.
|
|
31818
|
+
* Branches that have already completed are not re-aborted.
|
|
31819
|
+
*
|
|
31820
|
+
* @param completedIndices - Set of branch indices that finished successfully/naturally.
|
|
31821
|
+
*/
|
|
31822
|
+
cancelRemaining(completedIndices) {
|
|
31823
|
+
this.controllers.forEach((ctrl, i) => {
|
|
31824
|
+
if (!completedIndices.has(i)) ctrl.abort();
|
|
31825
|
+
});
|
|
31826
|
+
}
|
|
31827
|
+
/**
|
|
31828
|
+
* Wait `timeoutMs` milliseconds for in-flight branches to honour their AbortSignals
|
|
31829
|
+
* and finish any cleanup work before the parallel handler resolves.
|
|
31830
|
+
*
|
|
31831
|
+
* @param timeoutMs - Drain window in milliseconds (default 5000).
|
|
31832
|
+
*/
|
|
31833
|
+
async drainAsync(timeoutMs) {
|
|
31834
|
+
await new Promise((resolve$6) => setTimeout(resolve$6, timeoutMs));
|
|
31835
|
+
}
|
|
31836
|
+
};
|
|
31837
|
+
|
|
31838
|
+
//#endregion
|
|
31839
|
+
//#region packages/factory/dist/handlers/parallel.js
|
|
31840
|
+
/**
|
|
31841
|
+
* Maps a BranchResult outcome string to a StageStatus for event payloads.
|
|
31842
|
+
*
|
|
31843
|
+
* BranchResult.outcome: 'SUCCESS' | 'FAIL' | 'CANCELLED'
|
|
31844
|
+
* StageStatus: 'SUCCESS' | 'FAIL' | 'PARTIAL_SUCCESS' | 'RETRY' | 'SKIPPED'
|
|
31845
|
+
*
|
|
31846
|
+
* Mapping:
|
|
31847
|
+
* 'SUCCESS' → 'SUCCESS'
|
|
31848
|
+
* 'CANCELLED' → 'SKIPPED'
|
|
31849
|
+
* 'FAIL' → 'FAIL'
|
|
31850
|
+
*/
|
|
31851
|
+
function branchOutcomeToStatus(outcome) {
|
|
31852
|
+
if (outcome === "SUCCESS") return "SUCCESS";
|
|
31853
|
+
if (outcome === "CANCELLED") return "SKIPPED";
|
|
31854
|
+
return "FAIL";
|
|
31855
|
+
}
|
|
31856
|
+
/**
|
|
31857
|
+
* Enrich a join-policy `BranchResult` with fan-in-compatible fields so that
|
|
31858
|
+
* `fan-in.ts` can read `parallel.results` without type-shape mismatch.
|
|
31859
|
+
*
|
|
31860
|
+
* The returned object is a superset of `BranchResult` — it preserves every
|
|
31861
|
+
* original field (index, outcome, contextSnapshot, error) AND adds the fields
|
|
31862
|
+
* that `fan-in.ts` requires (branch_id, status, context_updates, failure_reason).
|
|
31863
|
+
*
|
|
31864
|
+
* Mapping:
|
|
31865
|
+
* index → branch_id (same numeric value)
|
|
31866
|
+
* outcome → status ('SUCCESS' | 'FAIL'/'CANCELLED' → 'SUCCESS' | 'FAILURE')
|
|
31867
|
+
* contextSnapshot → context_updates (shallow context snapshot)
|
|
31868
|
+
* error → failure_reason (human-readable failure description)
|
|
31869
|
+
*/
|
|
31870
|
+
function toBridgeBranchResult(result) {
|
|
31871
|
+
return {
|
|
31872
|
+
...result,
|
|
31873
|
+
branch_id: result.index,
|
|
31874
|
+
status: result.outcome === "SUCCESS" ? "SUCCESS" : "FAILURE",
|
|
31875
|
+
context_updates: result.contextSnapshot,
|
|
31876
|
+
failure_reason: result.error
|
|
31877
|
+
};
|
|
31878
|
+
}
|
|
31879
|
+
/**
|
|
31880
|
+
* Run an array of async task factories with at most `limit` executing
|
|
31881
|
+
* concurrently at any point in time, preserving result order.
|
|
31882
|
+
*/
|
|
31883
|
+
async function runWithConcurrencyLimit(tasks, limit) {
|
|
31884
|
+
const results = new Array(tasks.length);
|
|
31885
|
+
const executing = new Set();
|
|
31886
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
31887
|
+
const idx = i;
|
|
31888
|
+
const p = tasks[idx]().then((r) => {
|
|
31889
|
+
results[idx] = r;
|
|
31890
|
+
}).finally(() => {
|
|
31891
|
+
executing.delete(p);
|
|
31892
|
+
});
|
|
31893
|
+
executing.add(p);
|
|
31894
|
+
if (executing.size >= limit) await Promise.race(executing);
|
|
31895
|
+
}
|
|
31896
|
+
await Promise.all(executing);
|
|
31897
|
+
return results;
|
|
31898
|
+
}
|
|
31899
|
+
function parseJoinPolicyConfig(node) {
|
|
31900
|
+
const policyRaw = node.attrs?.["join_policy"] ?? node.joinPolicy ?? "wait_all";
|
|
31901
|
+
const policy = policyRaw === "first_success" || policyRaw === "quorum" ? policyRaw : "wait_all";
|
|
31902
|
+
const quorumSizeRaw = node.attrs?.["quorum_size"];
|
|
31903
|
+
const quorum_size = quorumSizeRaw !== void 0 ? parseInt(quorumSizeRaw, 10) : void 0;
|
|
31904
|
+
const drainRaw = node.attrs?.["cancel_drain_timeout_ms"];
|
|
31905
|
+
const cancel_drain_timeout_ms = drainRaw !== void 0 ? parseInt(drainRaw, 10) : void 0;
|
|
31906
|
+
return {
|
|
31907
|
+
policy,
|
|
31908
|
+
...quorum_size !== void 0 && { quorum_size },
|
|
31909
|
+
...cancel_drain_timeout_ms !== void 0 && { cancel_drain_timeout_ms }
|
|
31910
|
+
};
|
|
31911
|
+
}
|
|
31912
|
+
/**
|
|
31913
|
+
* Build an async task that executes a single branch node and returns a
|
|
31914
|
+
* `BranchResult`. Isolation is guaranteed via `context.clone()`.
|
|
31915
|
+
*/
|
|
31916
|
+
function makeBranchTask(edge, index, signal, context, graph, options) {
|
|
31917
|
+
return async () => {
|
|
31918
|
+
if (signal.aborted) return {
|
|
31919
|
+
index,
|
|
31920
|
+
outcome: "CANCELLED"
|
|
31921
|
+
};
|
|
31922
|
+
const branchNode = graph.nodes.get(edge.toNode);
|
|
31923
|
+
if (!branchNode) return {
|
|
31924
|
+
index,
|
|
31925
|
+
outcome: "FAIL",
|
|
31926
|
+
error: `Branch node "${edge.toNode}" not found in graph`
|
|
31927
|
+
};
|
|
31928
|
+
const branchCtx = context.clone();
|
|
31929
|
+
branchCtx.set("_branch.abort_signal", signal);
|
|
31930
|
+
branchCtx.set("_branch.index", index);
|
|
31931
|
+
const handler = options.handlerRegistry.resolve(branchNode);
|
|
31932
|
+
let branchOutcome;
|
|
31933
|
+
try {
|
|
31934
|
+
branchOutcome = await handler(branchNode, branchCtx, graph);
|
|
31935
|
+
} catch (err) {
|
|
31936
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
31937
|
+
return {
|
|
31938
|
+
index,
|
|
31939
|
+
outcome: "FAIL",
|
|
31940
|
+
error: msg,
|
|
31941
|
+
contextSnapshot: branchCtx.snapshot()
|
|
31942
|
+
};
|
|
31943
|
+
}
|
|
31944
|
+
if (signal.aborted) return {
|
|
31945
|
+
index,
|
|
31946
|
+
outcome: "CANCELLED",
|
|
31947
|
+
contextSnapshot: branchCtx.snapshot()
|
|
31948
|
+
};
|
|
31949
|
+
const isSuccess = branchOutcome.status === "SUCCESS" || branchOutcome.status === "PARTIAL_SUCCESS";
|
|
31950
|
+
const result = {
|
|
31951
|
+
index,
|
|
31952
|
+
outcome: isSuccess ? "SUCCESS" : "FAIL",
|
|
31953
|
+
contextSnapshot: branchCtx.snapshot()
|
|
31954
|
+
};
|
|
31955
|
+
if (!isSuccess) {
|
|
31956
|
+
const reason = branchOutcome.failureReason;
|
|
31957
|
+
if (reason !== void 0) result.error = reason;
|
|
31958
|
+
}
|
|
31959
|
+
return result;
|
|
31960
|
+
};
|
|
31961
|
+
}
|
|
31962
|
+
/**
|
|
31963
|
+
* Create a `parallel` node handler that fans out to all outgoing branch nodes
|
|
31964
|
+
* concurrently, applies a configurable join policy, and writes results to context.
|
|
31965
|
+
*
|
|
31966
|
+
* @param options.handlerRegistry Registry used to resolve each branch node's
|
|
31967
|
+
* handler at invocation time.
|
|
31968
|
+
*/
|
|
31969
|
+
function createParallelHandler(options) {
|
|
31970
|
+
return async (node, context, graph) => {
|
|
31971
|
+
const branchEdges = graph.outgoingEdges(node.id);
|
|
31972
|
+
const branchCount = branchEdges.length;
|
|
31973
|
+
if (branchCount === 0) {
|
|
31974
|
+
context.set("parallel.results", []);
|
|
31975
|
+
return { status: "SUCCESS" };
|
|
31976
|
+
}
|
|
31977
|
+
const config = parseJoinPolicyConfig(node);
|
|
31978
|
+
const maxParallel = node.maxParallel ?? 0;
|
|
31979
|
+
const drainMs = config.cancel_drain_timeout_ms ?? 5e3;
|
|
31980
|
+
const cancellationManager = new BranchCancellationManager(branchCount);
|
|
31981
|
+
const runId = context.getString("__runId", "unknown");
|
|
31982
|
+
options.eventBus?.emit("graph:parallel-started", {
|
|
31983
|
+
runId,
|
|
31984
|
+
nodeId: node.id,
|
|
31985
|
+
branchCount,
|
|
31986
|
+
maxParallel: maxParallel > 0 ? maxParallel : branchCount,
|
|
31987
|
+
policy: config.policy
|
|
31988
|
+
});
|
|
31989
|
+
if (config.policy === "wait_all") {
|
|
31990
|
+
const tasks = branchEdges.map((edge, index) => {
|
|
31991
|
+
const baseTask = makeBranchTask(edge, index, cancellationManager.getSignal(index), context, graph, options);
|
|
31992
|
+
return async () => {
|
|
31993
|
+
options.eventBus?.emit("graph:parallel-branch-started", {
|
|
31994
|
+
runId,
|
|
31995
|
+
nodeId: node.id,
|
|
31996
|
+
branchIndex: index
|
|
31997
|
+
});
|
|
31998
|
+
const branchStart = Date.now();
|
|
31999
|
+
const result = await baseTask();
|
|
32000
|
+
const durationMs = Date.now() - branchStart;
|
|
32001
|
+
options.eventBus?.emit("graph:parallel-branch-completed", {
|
|
32002
|
+
runId,
|
|
32003
|
+
nodeId: node.id,
|
|
32004
|
+
branchIndex: index,
|
|
32005
|
+
status: branchOutcomeToStatus(result.outcome),
|
|
32006
|
+
durationMs
|
|
32007
|
+
});
|
|
32008
|
+
return result;
|
|
32009
|
+
};
|
|
32010
|
+
});
|
|
32011
|
+
const results = maxParallel > 0 ? await runWithConcurrencyLimit(tasks, maxParallel) : await Promise.all(tasks.map((t) => t()));
|
|
32012
|
+
context.set("parallel.results", results.map(toBridgeBranchResult));
|
|
32013
|
+
const completedCount = results.filter((r) => r.outcome === "SUCCESS").length;
|
|
32014
|
+
const cancelledCount = results.filter((r) => r.outcome === "CANCELLED").length;
|
|
32015
|
+
options.eventBus?.emit("graph:parallel-completed", {
|
|
32016
|
+
runId,
|
|
32017
|
+
nodeId: node.id,
|
|
32018
|
+
completedCount,
|
|
32019
|
+
cancelledCount,
|
|
32020
|
+
policy: config.policy
|
|
32021
|
+
});
|
|
32022
|
+
return { status: "SUCCESS" };
|
|
32023
|
+
}
|
|
32024
|
+
const completed = [];
|
|
32025
|
+
const completedIndices = new Set();
|
|
32026
|
+
const pendingEvents = [];
|
|
32027
|
+
const waiters = [];
|
|
32028
|
+
function push(event) {
|
|
32029
|
+
const w = waiters.shift();
|
|
32030
|
+
if (w !== void 0) w(event);
|
|
32031
|
+
else pendingEvents.push(event);
|
|
32032
|
+
}
|
|
32033
|
+
function waitForNext() {
|
|
32034
|
+
return new Promise((resolve$6) => {
|
|
32035
|
+
const e = pendingEvents.shift();
|
|
32036
|
+
if (e !== void 0) resolve$6(e);
|
|
32037
|
+
else waiters.push(resolve$6);
|
|
32038
|
+
});
|
|
32039
|
+
}
|
|
32040
|
+
const branchStarts = new Map();
|
|
32041
|
+
const activeTasks = branchEdges.map((edge, index) => {
|
|
32042
|
+
const task = makeBranchTask(edge, index, cancellationManager.getSignal(index), context, graph, options);
|
|
32043
|
+
options.eventBus?.emit("graph:parallel-branch-started", {
|
|
32044
|
+
runId,
|
|
32045
|
+
nodeId: node.id,
|
|
32046
|
+
branchIndex: index
|
|
32047
|
+
});
|
|
32048
|
+
branchStarts.set(index, Date.now());
|
|
32049
|
+
return task().then((result) => {
|
|
32050
|
+
const branchStart = branchStarts.get(index) ?? Date.now();
|
|
32051
|
+
const durationMs = Date.now() - branchStart;
|
|
32052
|
+
options.eventBus?.emit("graph:parallel-branch-completed", {
|
|
32053
|
+
runId,
|
|
32054
|
+
nodeId: node.id,
|
|
32055
|
+
branchIndex: index,
|
|
32056
|
+
status: branchOutcomeToStatus(result.outcome),
|
|
32057
|
+
durationMs
|
|
32058
|
+
});
|
|
32059
|
+
push({ result });
|
|
32060
|
+
});
|
|
32061
|
+
});
|
|
32062
|
+
let joinOutcome = "SUCCESS";
|
|
32063
|
+
let settled = false;
|
|
32064
|
+
let joinErrorMsg;
|
|
32065
|
+
for (let i = 0; i < branchCount; i++) {
|
|
32066
|
+
const { result } = await waitForNext();
|
|
32067
|
+
if (completedIndices.has(result.index)) continue;
|
|
32068
|
+
completed.push(result);
|
|
32069
|
+
completedIndices.add(result.index);
|
|
32070
|
+
const decision = evaluateJoinPolicy(config, completed, branchCount);
|
|
32071
|
+
if (decision.action === "wait") continue;
|
|
32072
|
+
settled = true;
|
|
32073
|
+
joinOutcome = decision.action === "continue" ? "SUCCESS" : "FAIL";
|
|
32074
|
+
if (decision.action === "fail") joinErrorMsg = decision.reason;
|
|
32075
|
+
const uncancelledCount = branchCount - completedIndices.size;
|
|
32076
|
+
cancellationManager.cancelRemaining(completedIndices);
|
|
32077
|
+
if (uncancelledCount > 0) {
|
|
32078
|
+
await cancellationManager.drainAsync(drainMs);
|
|
32079
|
+
while (pendingEvents.length > 0) {
|
|
32080
|
+
const e = pendingEvents.shift();
|
|
32081
|
+
if (e !== void 0 && !completedIndices.has(e.result.index)) {
|
|
32082
|
+
completed.push(e.result);
|
|
32083
|
+
completedIndices.add(e.result.index);
|
|
32084
|
+
}
|
|
32085
|
+
}
|
|
32086
|
+
for (let j$1 = 0; j$1 < branchCount; j$1++) if (!completedIndices.has(j$1)) {
|
|
32087
|
+
completed.push({
|
|
32088
|
+
index: j$1,
|
|
32089
|
+
outcome: "CANCELLED"
|
|
32090
|
+
});
|
|
32091
|
+
completedIndices.add(j$1);
|
|
32092
|
+
}
|
|
32093
|
+
}
|
|
32094
|
+
context.set("parallel.results", completed.map(toBridgeBranchResult));
|
|
32095
|
+
if (decision.action === "continue") {
|
|
32096
|
+
if (config.policy === "first_success") {
|
|
32097
|
+
const winner = completed.find((r) => r.outcome === "SUCCESS");
|
|
32098
|
+
if (winner !== void 0) context.set("parallel.winner_index", winner.index);
|
|
32099
|
+
}
|
|
32100
|
+
if (config.policy === "quorum") {
|
|
32101
|
+
const successCount = completed.filter((r) => r.outcome === "SUCCESS").length;
|
|
32102
|
+
context.set("parallel.quorum_reached", successCount);
|
|
32103
|
+
}
|
|
32104
|
+
} else context.set("parallel.join_error", joinErrorMsg ?? decision.reason);
|
|
32105
|
+
break;
|
|
32106
|
+
}
|
|
32107
|
+
Promise.allSettled(activeTasks);
|
|
32108
|
+
if (!settled) context.set("parallel.results", completed.map(toBridgeBranchResult));
|
|
32109
|
+
const finalCompletedCount = completed.filter((r) => r.outcome === "SUCCESS").length;
|
|
32110
|
+
const finalCancelledCount = completed.filter((r) => r.outcome === "CANCELLED").length;
|
|
32111
|
+
options.eventBus?.emit("graph:parallel-completed", {
|
|
32112
|
+
runId,
|
|
32113
|
+
nodeId: node.id,
|
|
32114
|
+
completedCount: finalCompletedCount,
|
|
32115
|
+
cancelledCount: finalCancelledCount,
|
|
32116
|
+
policy: config.policy
|
|
32117
|
+
});
|
|
32118
|
+
return { status: joinOutcome === "SUCCESS" ? "SUCCESS" : "FAILURE" };
|
|
32119
|
+
};
|
|
32120
|
+
}
|
|
32121
|
+
|
|
32122
|
+
//#endregion
|
|
32123
|
+
//#region packages/factory/dist/handlers/fan-in.js
|
|
32124
|
+
/** Lower number = better rank. FAILURE is excluded before ranking. */
|
|
32125
|
+
const OUTCOME_RANK = {
|
|
32126
|
+
SUCCESS: 0,
|
|
32127
|
+
PARTIAL_SUCCESS: 1,
|
|
32128
|
+
NEEDS_RETRY: 2,
|
|
32129
|
+
FAILURE: 3,
|
|
32130
|
+
ESCALATE: 4
|
|
32131
|
+
};
|
|
32132
|
+
/**
|
|
32133
|
+
* Select the best non-FAILURE branch from `results`.
|
|
32134
|
+
*
|
|
32135
|
+
* Sort order (ascending priority):
|
|
32136
|
+
* 1. `OUTCOME_RANK[status]` ascending (SUCCESS wins)
|
|
32137
|
+
* 2. `score` descending (higher is better; `undefined` treated as `0`)
|
|
32138
|
+
* 3. `branch_id` ascending (stable tiebreak)
|
|
32139
|
+
*
|
|
32140
|
+
* Returns `null` when every branch has status `FAILURE`.
|
|
32141
|
+
*/
|
|
32142
|
+
function rankBranches(results) {
|
|
32143
|
+
const eligible = results.filter((r) => r.status !== "FAILURE");
|
|
32144
|
+
if (eligible.length === 0) return null;
|
|
32145
|
+
const sorted = [...eligible].sort((a, b) => {
|
|
32146
|
+
const rankDiff = (OUTCOME_RANK[a.status] ?? 99) - (OUTCOME_RANK[b.status] ?? 99);
|
|
32147
|
+
if (rankDiff !== 0) return rankDiff;
|
|
32148
|
+
const scoreA = a.score ?? 0;
|
|
32149
|
+
const scoreB = b.score ?? 0;
|
|
32150
|
+
if (scoreB !== scoreA) return scoreB - scoreA;
|
|
32151
|
+
return a.branch_id - b.branch_id;
|
|
32152
|
+
});
|
|
32153
|
+
return sorted[0] ?? null;
|
|
32154
|
+
}
|
|
32155
|
+
/**
|
|
32156
|
+
* Build the prompt sent to the LLM for selection-based fan-in.
|
|
32157
|
+
*
|
|
32158
|
+
* The prompt prepends `nodePrompt`, then lists each branch with its
|
|
32159
|
+
* `branch_id`, `status`, `score`, and the keys present in `context_updates`
|
|
32160
|
+
* (values omitted for token efficiency). The LLM is instructed to reply with
|
|
32161
|
+
* just the integer `branch_id` of the best candidate.
|
|
32162
|
+
*/
|
|
32163
|
+
function buildSelectionPrompt(nodePrompt, results) {
|
|
32164
|
+
const branchSummaries = results.map((r) => {
|
|
32165
|
+
const contextKeys = r.context_updates && Object.keys(r.context_updates).length > 0 ? Object.keys(r.context_updates).join(", ") : "(none)";
|
|
32166
|
+
return `Branch ${r.branch_id}: status=${r.status}, score=${r.score ?? 0}, context_update_keys=[${contextKeys}]`;
|
|
32167
|
+
}).join("\n");
|
|
32168
|
+
return `${nodePrompt}\n\nParallel branch results:\n${branchSummaries}\n\nReply with only the integer branch_id of the best candidate.`;
|
|
32169
|
+
}
|
|
32170
|
+
/**
|
|
32171
|
+
* Parse an LLM response to extract the winning `branch_id`.
|
|
32172
|
+
*
|
|
32173
|
+
* Scans the response text for the first integer that matches a valid
|
|
32174
|
+
* `branch_id` in `results`. Returns the matching `BranchResult`, or `null`
|
|
32175
|
+
* (triggering heuristic fallback) if no valid branch_id is found.
|
|
32176
|
+
* Logs a warning on fallback.
|
|
32177
|
+
*/
|
|
32178
|
+
function parseLlmWinnerResponse(response, results) {
|
|
32179
|
+
const validIds = new Set(results.map((r) => r.branch_id));
|
|
32180
|
+
const matches = response.match(/\d+/g);
|
|
32181
|
+
if (matches) for (const m of matches) {
|
|
32182
|
+
const id = parseInt(m, 10);
|
|
32183
|
+
if (validIds.has(id)) return results.find((r) => r.branch_id === id) ?? null;
|
|
32184
|
+
}
|
|
32185
|
+
console.warn(`[fan-in] LLM response did not contain a valid branch_id; falling back to heuristic selection. Response: "${response}"`);
|
|
32186
|
+
return null;
|
|
32187
|
+
}
|
|
32188
|
+
/**
|
|
32189
|
+
* Default `llmCall` implementation that wraps `callLLM` from `@substrate-ai/core`
|
|
32190
|
+
* with fixed default routing parameters.
|
|
32191
|
+
*/
|
|
32192
|
+
async function defaultLlmCall(prompt) {
|
|
32193
|
+
const result = await callLLM({
|
|
32194
|
+
model: "claude-sonnet-4-5",
|
|
32195
|
+
provider: "anthropic",
|
|
32196
|
+
reasoningEffort: "low",
|
|
32197
|
+
prompt
|
|
32198
|
+
});
|
|
32199
|
+
return result.text;
|
|
32200
|
+
}
|
|
32201
|
+
/**
|
|
32202
|
+
* Create a `parallel.fan_in` node handler.
|
|
32203
|
+
*
|
|
32204
|
+
* Execution steps:
|
|
32205
|
+
* 1. Read `parallel.results` from context; return FAILURE if absent or empty.
|
|
32206
|
+
* 2. If `node.prompt` is non-empty, call LLM to select winner; fall back to
|
|
32207
|
+
* heuristic on parse failure.
|
|
32208
|
+
* 3. Use heuristic `rankBranches` when no prompt or LLM fallback.
|
|
32209
|
+
* 4. If all branches failed, return FAILURE with aggregated failure reasons.
|
|
32210
|
+
* 5. Merge winner's `context_updates`, set `parallel.fan_in.best_id` and
|
|
32211
|
+
* `parallel.fan_in.best_outcome`, return SUCCESS.
|
|
32212
|
+
*
|
|
32213
|
+
* @param options - Optional configuration (inject `llmCall` for testing).
|
|
32214
|
+
*/
|
|
32215
|
+
function createFanInHandler(options) {
|
|
32216
|
+
const llmCallFn = options?.llmCall ?? defaultLlmCall;
|
|
32217
|
+
return async (node, context, _graph) => {
|
|
32218
|
+
const rawResults = context.get("parallel.results");
|
|
32219
|
+
if (!rawResults || !Array.isArray(rawResults) || rawResults.length === 0) return {
|
|
32220
|
+
status: "FAILURE",
|
|
32221
|
+
failureReason: "fan-in: no parallel results found in context (parallel.results is absent or empty)"
|
|
32222
|
+
};
|
|
32223
|
+
const results = rawResults;
|
|
32224
|
+
let winner = null;
|
|
32225
|
+
if (node.prompt && node.prompt.trim().length > 0) {
|
|
32226
|
+
const prompt = buildSelectionPrompt(node.prompt, results);
|
|
32227
|
+
try {
|
|
32228
|
+
const response = await llmCallFn(prompt);
|
|
32229
|
+
winner = parseLlmWinnerResponse(response, results);
|
|
32230
|
+
} catch (err) {
|
|
32231
|
+
console.warn(`[fan-in] LLM call failed; falling back to heuristic selection. Error: ${String(err)}`);
|
|
32232
|
+
winner = null;
|
|
32233
|
+
}
|
|
32234
|
+
if (winner === null) winner = rankBranches(results);
|
|
32235
|
+
} else winner = rankBranches(results);
|
|
32236
|
+
if (winner === null) {
|
|
32237
|
+
const reasons = results.map((r) => r.failure_reason ?? `branch ${r.branch_id}: no reason provided`).join("; ");
|
|
32238
|
+
return {
|
|
32239
|
+
status: "FAILURE",
|
|
32240
|
+
failureReason: `fan-in: all branches failed — ${reasons}`
|
|
32241
|
+
};
|
|
32242
|
+
}
|
|
32243
|
+
if (winner.context_updates && Object.keys(winner.context_updates).length > 0) context.applyUpdates(winner.context_updates);
|
|
32244
|
+
context.set("parallel.fan_in.best_id", winner.branch_id);
|
|
32245
|
+
context.set("parallel.fan_in.best_outcome", winner.status);
|
|
32246
|
+
return { status: "SUCCESS" };
|
|
32247
|
+
};
|
|
32248
|
+
}
|
|
32249
|
+
|
|
32250
|
+
//#endregion
|
|
32251
|
+
//#region packages/factory/dist/handlers/subgraph.js
|
|
32252
|
+
/**
|
|
32253
|
+
* Converts the sub-executor's `events.ts:StageStatus` back to the handler
|
|
32254
|
+
* return type `types.ts:OutcomeStatus`.
|
|
32255
|
+
*
|
|
32256
|
+
* Mapping:
|
|
32257
|
+
* 'SUCCESS' → 'SUCCESS'
|
|
32258
|
+
* 'PARTIAL_SUCCESS'→ 'PARTIAL_SUCCESS'
|
|
32259
|
+
* all others → 'FAILURE' (covers 'FAIL', 'RETRY', 'SKIPPED')
|
|
32260
|
+
*/
|
|
32261
|
+
function denormalizeStatus(status) {
|
|
32262
|
+
if (status === "SUCCESS") return "SUCCESS";
|
|
32263
|
+
if (status === "PARTIAL_SUCCESS") return "PARTIAL_SUCCESS";
|
|
32264
|
+
return "FAILURE";
|
|
32265
|
+
}
|
|
32266
|
+
/**
|
|
32267
|
+
* Factory function that creates a handler for `type="subgraph"` nodes.
|
|
32268
|
+
*
|
|
32269
|
+
* Each subgraph node references an external `.dot` file, loads and validates
|
|
32270
|
+
* it, then executes it as a nested sub-pipeline. Parent context is seeded into
|
|
32271
|
+
* the sub-executor and context updates from the subgraph are propagated back.
|
|
32272
|
+
*/
|
|
32273
|
+
function createSubgraphHandler(options) {
|
|
32274
|
+
return async (node, context, graph) => {
|
|
32275
|
+
const graphFile = node.attrs?.["graph_file"];
|
|
32276
|
+
if (!graphFile) return {
|
|
32277
|
+
status: "FAILURE",
|
|
32278
|
+
failureReason: `Subgraph node "${node.id}" is missing required attribute graph_file`
|
|
32279
|
+
};
|
|
32280
|
+
const currentDepth = context.getNumber("subgraph._depth", 0);
|
|
32281
|
+
const maxDepth = options.maxDepth ?? 5;
|
|
32282
|
+
if (currentDepth >= maxDepth) return {
|
|
32283
|
+
status: "FAILURE",
|
|
32284
|
+
failureReason: `Subgraph depth limit exceeded (max ${maxDepth}): node "${node.id}"`
|
|
32285
|
+
};
|
|
32286
|
+
const filePath = path.isAbsolute(graphFile) ? graphFile : path.join(options.baseDir, graphFile);
|
|
32287
|
+
const loader = options.graphFileLoader ?? ((fp) => readFile$1(fp, "utf-8"));
|
|
32288
|
+
let dotSource;
|
|
32289
|
+
try {
|
|
32290
|
+
dotSource = await loader(filePath);
|
|
32291
|
+
} catch (err) {
|
|
32292
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32293
|
+
return {
|
|
32294
|
+
status: "FAILURE",
|
|
32295
|
+
failureReason: `Subgraph node "${node.id}": failed to load "${filePath}": ${msg}`
|
|
32296
|
+
};
|
|
32297
|
+
}
|
|
32298
|
+
let subgraph;
|
|
32299
|
+
try {
|
|
32300
|
+
subgraph = parseGraph(dotSource);
|
|
32301
|
+
} catch (err) {
|
|
32302
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32303
|
+
return {
|
|
32304
|
+
status: "FAILURE",
|
|
32305
|
+
failureReason: `Subgraph node "${node.id}": failed to parse "${filePath}": ${msg}`
|
|
32306
|
+
};
|
|
32307
|
+
}
|
|
32308
|
+
try {
|
|
32309
|
+
createValidator().validateOrRaise(subgraph);
|
|
32310
|
+
} catch (err) {
|
|
32311
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32312
|
+
return {
|
|
32313
|
+
status: "FAILURE",
|
|
32314
|
+
failureReason: `Subgraph node "${node.id}": validation failed for "${filePath}": ${msg}`
|
|
32315
|
+
};
|
|
32316
|
+
}
|
|
32317
|
+
const runId = context.getString("__runId", options.runId ?? "unknown");
|
|
32318
|
+
options.eventBus?.emit("graph:subgraph-started", {
|
|
32319
|
+
runId,
|
|
32320
|
+
nodeId: node.id,
|
|
32321
|
+
graphFile: filePath,
|
|
32322
|
+
depth: currentDepth
|
|
32323
|
+
});
|
|
32324
|
+
const subgraphStart = Date.now();
|
|
32325
|
+
const parentStylesheet = graph.modelStylesheet ? parseStylesheet(graph.modelStylesheet) : void 0;
|
|
32326
|
+
const subConfig = {
|
|
32327
|
+
runId: randomUUID(),
|
|
32328
|
+
logsRoot: options.logsRoot ?? tmpdir(),
|
|
32329
|
+
handlerRegistry: options.handlerRegistry,
|
|
32330
|
+
initialContext: {
|
|
32331
|
+
...context.snapshot(),
|
|
32332
|
+
"subgraph._depth": currentDepth + 1
|
|
32333
|
+
},
|
|
32334
|
+
...options.eventBus !== void 0 ? { eventBus: options.eventBus } : {},
|
|
32335
|
+
...parentStylesheet !== void 0 ? { inheritedStylesheet: parentStylesheet } : {}
|
|
32336
|
+
};
|
|
32337
|
+
let subOutcome;
|
|
32338
|
+
try {
|
|
32339
|
+
subOutcome = await createGraphExecutor().run(subgraph, subConfig);
|
|
32340
|
+
} catch (err) {
|
|
32341
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32342
|
+
const durationMs$1 = Date.now() - subgraphStart;
|
|
32343
|
+
options.eventBus?.emit("graph:subgraph-completed", {
|
|
32344
|
+
runId,
|
|
32345
|
+
nodeId: node.id,
|
|
32346
|
+
graphFile: filePath,
|
|
32347
|
+
depth: currentDepth,
|
|
32348
|
+
status: "FAIL",
|
|
32349
|
+
durationMs: durationMs$1
|
|
32350
|
+
});
|
|
32351
|
+
return {
|
|
32352
|
+
status: "FAILURE",
|
|
32353
|
+
failureReason: `Subgraph node "${node.id}": executor threw: ${msg}`
|
|
32354
|
+
};
|
|
32355
|
+
}
|
|
32356
|
+
const durationMs = Date.now() - subgraphStart;
|
|
32357
|
+
options.eventBus?.emit("graph:subgraph-completed", {
|
|
32358
|
+
runId,
|
|
32359
|
+
nodeId: node.id,
|
|
32360
|
+
graphFile: filePath,
|
|
32361
|
+
depth: currentDepth,
|
|
32362
|
+
status: subOutcome.status === "SUCCESS" ? "SUCCESS" : subOutcome.status === "PARTIAL_SUCCESS" ? "PARTIAL_SUCCESS" : "FAIL",
|
|
32363
|
+
durationMs
|
|
32364
|
+
});
|
|
32365
|
+
if (subOutcome.contextUpdates) context.applyUpdates(subOutcome.contextUpdates);
|
|
32366
|
+
return {
|
|
32367
|
+
status: denormalizeStatus(subOutcome.status),
|
|
32368
|
+
...subOutcome.contextUpdates !== void 0 && { contextUpdates: subOutcome.contextUpdates },
|
|
32369
|
+
...subOutcome.notes !== void 0 && { notes: subOutcome.notes },
|
|
32370
|
+
...subOutcome.failureReason !== void 0 && { failureReason: subOutcome.failureReason }
|
|
32371
|
+
};
|
|
32372
|
+
};
|
|
32373
|
+
}
|
|
32374
|
+
|
|
32375
|
+
//#endregion
|
|
32376
|
+
//#region packages/factory/dist/handlers/manager-loop.js
|
|
32377
|
+
/**
|
|
32378
|
+
* Factory function that creates a handler for `type="stack.manager_loop"` nodes.
|
|
32379
|
+
*
|
|
32380
|
+
* Each invocation loads the body graph once, then executes it in a loop for up
|
|
32381
|
+
* to `max_cycles` cycles (default 10). Stop conditions (context key or LLM-based)
|
|
32382
|
+
* allow early exit. Stall detection injects recovery steering after consecutive
|
|
32383
|
+
* non-SUCCESS body executions.
|
|
32384
|
+
*/
|
|
32385
|
+
function createManagerLoopHandler(options) {
|
|
32386
|
+
return async (node, context, _graph) => {
|
|
32387
|
+
const graphFile = node.attrs?.["graph_file"];
|
|
32388
|
+
if (!graphFile) return {
|
|
32389
|
+
status: "FAILURE",
|
|
32390
|
+
failureReason: `Manager loop node "${node.id}" is missing required attribute graph_file`
|
|
32391
|
+
};
|
|
32392
|
+
const rawMaxCycles = node.attrs?.["max_cycles"];
|
|
32393
|
+
let maxCycles = 10;
|
|
32394
|
+
if (rawMaxCycles !== void 0 && rawMaxCycles !== "") {
|
|
32395
|
+
const parsed = parseInt(rawMaxCycles, 10);
|
|
32396
|
+
maxCycles = isNaN(parsed) ? 10 : Math.max(1, parsed);
|
|
32397
|
+
}
|
|
32398
|
+
const filePath = path.isAbsolute(graphFile) ? graphFile : path.join(options.baseDir ?? process.cwd(), graphFile);
|
|
32399
|
+
const loader = options.graphFileLoader ?? ((fp) => readFile$1(fp, "utf-8"));
|
|
32400
|
+
let dotSource;
|
|
32401
|
+
try {
|
|
32402
|
+
dotSource = await loader(filePath);
|
|
32403
|
+
} catch (err) {
|
|
32404
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32405
|
+
return {
|
|
32406
|
+
status: "FAILURE",
|
|
32407
|
+
failureReason: `Manager loop node "${node.id}": failed to load "${filePath}": ${msg}`
|
|
32408
|
+
};
|
|
32409
|
+
}
|
|
32410
|
+
let bodyGraph;
|
|
32411
|
+
try {
|
|
32412
|
+
bodyGraph = parseGraph(dotSource);
|
|
32413
|
+
} catch (err) {
|
|
32414
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32415
|
+
return {
|
|
32416
|
+
status: "FAILURE",
|
|
32417
|
+
failureReason: `Manager loop node "${node.id}": failed to parse "${filePath}": ${msg}`
|
|
32418
|
+
};
|
|
32419
|
+
}
|
|
32420
|
+
try {
|
|
32421
|
+
createValidator().validateOrRaise(bodyGraph);
|
|
32422
|
+
} catch (err) {
|
|
32423
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32424
|
+
return {
|
|
32425
|
+
status: "FAILURE",
|
|
32426
|
+
failureReason: `Manager loop node "${node.id}": validation failed for "${filePath}": ${msg}`
|
|
32427
|
+
};
|
|
32428
|
+
}
|
|
32429
|
+
const stopCondition = node.attrs?.["stop_condition"];
|
|
32430
|
+
let consecutiveFailures = 0;
|
|
32431
|
+
for (let cycle = 1; cycle <= maxCycles; cycle++) {
|
|
32432
|
+
context.set("manager_loop.cycle", cycle);
|
|
32433
|
+
const bodyConfig = {
|
|
32434
|
+
runId: randomUUID(),
|
|
32435
|
+
logsRoot: options.logsRoot ?? tmpdir(),
|
|
32436
|
+
handlerRegistry: options.handlerRegistry,
|
|
32437
|
+
initialContext: context.snapshot()
|
|
32438
|
+
};
|
|
32439
|
+
const bodyOutcome = await createGraphExecutor().run(bodyGraph, bodyConfig);
|
|
32440
|
+
if (bodyOutcome.contextUpdates) context.applyUpdates(bodyOutcome.contextUpdates);
|
|
32441
|
+
context.set("manager_loop.cycles_completed", cycle);
|
|
32442
|
+
context.set("manager_loop.last_outcome", bodyOutcome.status);
|
|
32443
|
+
if (bodyOutcome.status === "SUCCESS") {
|
|
32444
|
+
consecutiveFailures = 0;
|
|
32445
|
+
context.set("manager_loop.steering.mode", "normal");
|
|
32446
|
+
context.set("manager_loop.steering.hints", []);
|
|
32447
|
+
} else {
|
|
32448
|
+
consecutiveFailures++;
|
|
32449
|
+
if (consecutiveFailures >= (options.maxStallCycles ?? 2)) {
|
|
32450
|
+
context.set("manager_loop.steering.mode", "recovery");
|
|
32451
|
+
context.set("manager_loop.steering.hints", [`Previous ${consecutiveFailures} attempts returned ${bodyOutcome.status}. Consider a different strategy.`, "Review context state and adjust approach before retrying."]);
|
|
32452
|
+
}
|
|
32453
|
+
}
|
|
32454
|
+
if (stopCondition) {
|
|
32455
|
+
let shouldStop = false;
|
|
32456
|
+
if (isLlmCondition(stopCondition)) {
|
|
32457
|
+
if (options.llmCall !== void 0) {
|
|
32458
|
+
const question = extractLlmQuestion(stopCondition);
|
|
32459
|
+
shouldStop = await evaluateLlmCondition(question, context.snapshot(), options.llmCall);
|
|
32460
|
+
}
|
|
32461
|
+
} else shouldStop = Boolean(context.get(stopCondition));
|
|
32462
|
+
if (shouldStop) {
|
|
32463
|
+
context.set("manager_loop.stop_reason", "stop_condition");
|
|
32464
|
+
return { status: "SUCCESS" };
|
|
32465
|
+
}
|
|
32466
|
+
}
|
|
32467
|
+
}
|
|
32468
|
+
context.set("manager_loop.stop_reason", "max_cycles");
|
|
32469
|
+
return { status: "SUCCESS" };
|
|
32470
|
+
};
|
|
32471
|
+
}
|
|
32472
|
+
|
|
32473
|
+
//#endregion
|
|
32474
|
+
//#region packages/factory/dist/handlers/registry.js
|
|
32475
|
+
var HandlerRegistry = class {
|
|
32476
|
+
/** Maps node type → handler function */
|
|
32477
|
+
_handlers = new Map();
|
|
32478
|
+
/** Maps DOT shape name → canonical node type */
|
|
32479
|
+
_shapeMap = new Map();
|
|
32480
|
+
/** Fallback handler when no type or shape match is found */
|
|
32481
|
+
_default;
|
|
32482
|
+
/**
|
|
32483
|
+
* Register a handler for the given node type.
|
|
32484
|
+
* Overwrites if already registered.
|
|
32485
|
+
*/
|
|
32486
|
+
register(type, handler) {
|
|
32487
|
+
this._handlers.set(type, handler);
|
|
32488
|
+
}
|
|
32489
|
+
/**
|
|
32490
|
+
* Register a shape → type mapping.
|
|
32491
|
+
* Overwrites if already registered.
|
|
32492
|
+
*/
|
|
32493
|
+
registerShape(shape, type) {
|
|
32494
|
+
this._shapeMap.set(shape, type);
|
|
32495
|
+
}
|
|
32496
|
+
/**
|
|
32497
|
+
* Set the default handler used when no type or shape match is found.
|
|
32498
|
+
*/
|
|
32499
|
+
setDefault(handler) {
|
|
31452
32500
|
this._default = handler;
|
|
31453
32501
|
}
|
|
31454
32502
|
/**
|
|
@@ -31480,19 +32528,40 @@ var HandlerRegistry = class {
|
|
|
31480
32528
|
* Story 42-10 adds codergen as the explicit "codergen" type, as the handler for
|
|
31481
32529
|
* `shape=box` nodes, and as the registry-level default for any node that has no
|
|
31482
32530
|
* recognised type or shape mapping.
|
|
32531
|
+
*
|
|
32532
|
+
* Story 50-5 extends options to `DefaultRegistryOptions` and registers the `subgraph` type.
|
|
31483
32533
|
*/
|
|
31484
32534
|
function createDefaultRegistry(options) {
|
|
31485
32535
|
const registry = new HandlerRegistry();
|
|
32536
|
+
registry.register("parallel", createParallelHandler({
|
|
32537
|
+
handlerRegistry: registry,
|
|
32538
|
+
...options?.eventBus !== void 0 ? { eventBus: options.eventBus } : {},
|
|
32539
|
+
...options?.runId !== void 0 ? { runId: options.runId } : {}
|
|
32540
|
+
}));
|
|
32541
|
+
registry.registerShape("component", "parallel");
|
|
31486
32542
|
registry.register("start", startHandler);
|
|
31487
32543
|
registry.register("exit", exitHandler);
|
|
31488
32544
|
registry.register("conditional", conditionalHandler);
|
|
31489
32545
|
registry.register("codergen", createCodergenHandler(options));
|
|
31490
32546
|
registry.register("tool", createToolHandler());
|
|
31491
32547
|
registry.register("wait.human", createWaitHumanHandler());
|
|
32548
|
+
registry.register("parallel.fan_in", createFanInHandler());
|
|
32549
|
+
registry.register("subgraph", createSubgraphHandler({
|
|
32550
|
+
handlerRegistry: registry,
|
|
32551
|
+
baseDir: options?.baseDir ?? process.cwd(),
|
|
32552
|
+
...options?.eventBus !== void 0 ? { eventBus: options.eventBus } : {},
|
|
32553
|
+
...options?.runId !== void 0 ? { runId: options.runId } : {}
|
|
32554
|
+
}));
|
|
32555
|
+
registry.register("stack.manager_loop", createManagerLoopHandler({
|
|
32556
|
+
handlerRegistry: registry,
|
|
32557
|
+
baseDir: options?.baseDir ?? process.cwd(),
|
|
32558
|
+
...options?.llmCall !== void 0 ? { llmCall: options.llmCall } : {}
|
|
32559
|
+
}));
|
|
31492
32560
|
registry.registerShape("Mdiamond", "start");
|
|
31493
32561
|
registry.registerShape("Msquare", "exit");
|
|
31494
32562
|
registry.registerShape("diamond", "conditional");
|
|
31495
32563
|
registry.registerShape("box", "codergen");
|
|
32564
|
+
registry.registerShape("tripleoctagon", "parallel.fan_in");
|
|
31496
32565
|
registry.setDefault(createCodergenHandler(options));
|
|
31497
32566
|
return registry;
|
|
31498
32567
|
}
|
|
@@ -36738,6 +37807,149 @@ function registerContextCommand(factoryCmd, version, storageDir, engineFactory)
|
|
|
36738
37807
|
});
|
|
36739
37808
|
}
|
|
36740
37809
|
|
|
37810
|
+
//#endregion
|
|
37811
|
+
//#region packages/factory/dist/templates/index.js
|
|
37812
|
+
/**
|
|
37813
|
+
* Pipeline Template Catalog — pre-built DOT graph pipeline templates for common patterns.
|
|
37814
|
+
*
|
|
37815
|
+
* Story 50-10.
|
|
37816
|
+
*
|
|
37817
|
+
* Node type strings used:
|
|
37818
|
+
* - `start` — start node (registered by default registry)
|
|
37819
|
+
* - `exit` — exit node (registered by default registry)
|
|
37820
|
+
* - `codergen` — coding agent node (registered by default registry)
|
|
37821
|
+
* - `parallel` — parallel fan-out node (story 50-1, shape=component)
|
|
37822
|
+
* - `parallel.fan_in`— fan-in/merge node (story 50-2, shape=tripleoctagon)
|
|
37823
|
+
*/
|
|
37824
|
+
const trycycleDotContent = `digraph trycycle {
|
|
37825
|
+
// Trycycle Pattern: iterative refinement with eval gates
|
|
37826
|
+
// Flow: define → plan → eval_plan ⇄ implement → eval_impl → exit
|
|
37827
|
+
// eval nodes can loop back to their upstream node on revision_needed.
|
|
37828
|
+
|
|
37829
|
+
start [type="start"];
|
|
37830
|
+
define [type="codergen", label="Define Requirements"];
|
|
37831
|
+
plan [type="codergen", label="Plan Implementation"];
|
|
37832
|
+
eval_plan [type="codergen", label="Evaluate Plan"];
|
|
37833
|
+
implement [type="codergen", label="Implement"];
|
|
37834
|
+
eval_impl [type="codergen", label="Evaluate Implementation"];
|
|
37835
|
+
exit [type="exit"];
|
|
37836
|
+
|
|
37837
|
+
start -> define;
|
|
37838
|
+
define -> plan;
|
|
37839
|
+
plan -> eval_plan;
|
|
37840
|
+
eval_plan -> implement [label="approved"];
|
|
37841
|
+
eval_plan -> plan [label="revision_needed"];
|
|
37842
|
+
implement -> eval_impl;
|
|
37843
|
+
eval_impl -> exit [label="approved"];
|
|
37844
|
+
eval_impl -> implement [label="revision_needed"];
|
|
37845
|
+
}
|
|
37846
|
+
`;
|
|
37847
|
+
const dualReviewDotContent = `digraph dual_review {
|
|
37848
|
+
// Dual-Review Pattern: fan-out to two independent reviewers, then fan-in
|
|
37849
|
+
// Flow: implement → (reviewer_a + reviewer_b) in parallel → merge → exit
|
|
37850
|
+
|
|
37851
|
+
start [type="start"];
|
|
37852
|
+
implement [type="codergen", label="Implement"];
|
|
37853
|
+
review_parallel [type="parallel", label="Fan-Out to Reviewers"];
|
|
37854
|
+
reviewer_a [type="codergen", label="Reviewer A"];
|
|
37855
|
+
reviewer_b [type="codergen", label="Reviewer B"];
|
|
37856
|
+
review_merge [type="parallel.fan_in",label="Merge Reviews"];
|
|
37857
|
+
exit [type="exit"];
|
|
37858
|
+
|
|
37859
|
+
start -> implement;
|
|
37860
|
+
implement -> review_parallel;
|
|
37861
|
+
review_parallel -> reviewer_a;
|
|
37862
|
+
review_parallel -> reviewer_b;
|
|
37863
|
+
reviewer_a -> review_merge;
|
|
37864
|
+
reviewer_b -> review_merge;
|
|
37865
|
+
review_merge -> exit;
|
|
37866
|
+
}
|
|
37867
|
+
`;
|
|
37868
|
+
const parallelExplorationDotContent = `digraph parallel_exploration {
|
|
37869
|
+
// Parallel-Exploration Pattern: dispatch multiple approaches concurrently,
|
|
37870
|
+
// select the best-scoring result, then refine the winner.
|
|
37871
|
+
// Flow: (approach_a + approach_b) in parallel → select best → refine → exit
|
|
37872
|
+
|
|
37873
|
+
start [type="start"];
|
|
37874
|
+
explore_parallel[type="parallel", label="Fan-Out Exploration"];
|
|
37875
|
+
approach_a [type="codergen", label="Approach A"];
|
|
37876
|
+
approach_b [type="codergen", label="Approach B"];
|
|
37877
|
+
select_best [type="parallel.fan_in", label="Select Best Candidate", selection="best"];
|
|
37878
|
+
refine [type="codergen", label="Refine Winner"];
|
|
37879
|
+
exit [type="exit"];
|
|
37880
|
+
|
|
37881
|
+
start -> explore_parallel;
|
|
37882
|
+
explore_parallel -> approach_a;
|
|
37883
|
+
explore_parallel -> approach_b;
|
|
37884
|
+
approach_a -> select_best;
|
|
37885
|
+
approach_b -> select_best;
|
|
37886
|
+
select_best -> refine;
|
|
37887
|
+
refine -> exit;
|
|
37888
|
+
}
|
|
37889
|
+
`;
|
|
37890
|
+
const stagedValidationDotContent = `digraph staged_validation {
|
|
37891
|
+
// Staged-Validation Pattern: sequential quality gates
|
|
37892
|
+
// Flow: implement → lint → test → validate → exit
|
|
37893
|
+
// Each stage is a separate codergen node; no parallel branches.
|
|
37894
|
+
|
|
37895
|
+
start [type="start"];
|
|
37896
|
+
implement[type="codergen", label="Implement"];
|
|
37897
|
+
lint [type="codergen", label="Lint"];
|
|
37898
|
+
test [type="codergen", label="Test"];
|
|
37899
|
+
validate [type="codergen", label="Validate"];
|
|
37900
|
+
exit [type="exit"];
|
|
37901
|
+
|
|
37902
|
+
start -> implement;
|
|
37903
|
+
implement -> lint;
|
|
37904
|
+
lint -> test;
|
|
37905
|
+
test -> validate;
|
|
37906
|
+
validate -> exit;
|
|
37907
|
+
}
|
|
37908
|
+
`;
|
|
37909
|
+
const trycycleTemplate = {
|
|
37910
|
+
name: "trycycle",
|
|
37911
|
+
description: "Iterative refinement loop with plan and implementation eval gates",
|
|
37912
|
+
dotContent: trycycleDotContent
|
|
37913
|
+
};
|
|
37914
|
+
const dualReviewTemplate = {
|
|
37915
|
+
name: "dual-review",
|
|
37916
|
+
description: "Fan-out to two independent reviewers, then fan-in to merge results",
|
|
37917
|
+
dotContent: dualReviewDotContent
|
|
37918
|
+
};
|
|
37919
|
+
const parallelExplorationTemplate = {
|
|
37920
|
+
name: "parallel-exploration",
|
|
37921
|
+
description: "Dispatch parallel implementation approaches and select the best candidate",
|
|
37922
|
+
dotContent: parallelExplorationDotContent
|
|
37923
|
+
};
|
|
37924
|
+
const stagedValidationTemplate = {
|
|
37925
|
+
name: "staged-validation",
|
|
37926
|
+
description: "Sequential quality gate stages: implement → lint → test → validate",
|
|
37927
|
+
dotContent: stagedValidationDotContent
|
|
37928
|
+
};
|
|
37929
|
+
/**
|
|
37930
|
+
* Map of all built-in pipeline templates, keyed by template name.
|
|
37931
|
+
* Insertion order determines display order in `factory templates list`.
|
|
37932
|
+
*/
|
|
37933
|
+
const PIPELINE_TEMPLATES = new Map([
|
|
37934
|
+
[trycycleTemplate.name, trycycleTemplate],
|
|
37935
|
+
[dualReviewTemplate.name, dualReviewTemplate],
|
|
37936
|
+
[parallelExplorationTemplate.name, parallelExplorationTemplate],
|
|
37937
|
+
[stagedValidationTemplate.name, stagedValidationTemplate]
|
|
37938
|
+
]);
|
|
37939
|
+
/**
|
|
37940
|
+
* Returns all available pipeline template entries in insertion order.
|
|
37941
|
+
*/
|
|
37942
|
+
function listPipelineTemplates() {
|
|
37943
|
+
return Array.from(PIPELINE_TEMPLATES.values());
|
|
37944
|
+
}
|
|
37945
|
+
/**
|
|
37946
|
+
* Returns the pipeline template entry for the given name (case-sensitive),
|
|
37947
|
+
* or `undefined` if not found.
|
|
37948
|
+
*/
|
|
37949
|
+
function getPipelineTemplate(name) {
|
|
37950
|
+
return PIPELINE_TEMPLATES.get(name);
|
|
37951
|
+
}
|
|
37952
|
+
|
|
36741
37953
|
//#endregion
|
|
36742
37954
|
//#region packages/factory/dist/twins/schema.js
|
|
36743
37955
|
/**
|
|
@@ -37776,6 +38988,28 @@ function registerFactoryCommand(program, options) {
|
|
|
37776
38988
|
process.exit(1);
|
|
37777
38989
|
}
|
|
37778
38990
|
});
|
|
38991
|
+
const templatesCmd = factoryCmd.command("templates").description("Manage reusable DOT graph pipeline templates");
|
|
38992
|
+
templatesCmd.command("list").description("List available pipeline templates").action(() => {
|
|
38993
|
+
const templates = listPipelineTemplates();
|
|
38994
|
+
for (const t of templates) process.stdout.write(` ${t.name.padEnd(24)} ${t.description}\n`);
|
|
38995
|
+
});
|
|
38996
|
+
templatesCmd.command("init").description("Create a pipeline.dot from a template").requiredOption("--template <name>", "Template name (see: factory templates list)").option("--output <path>", "Output file path (default: pipeline.dot)", "pipeline.dot").action(async (opts) => {
|
|
38997
|
+
const entry = getPipelineTemplate(opts.template);
|
|
38998
|
+
if (!entry) {
|
|
38999
|
+
const available = listPipelineTemplates().map((t) => t.name).join(", ");
|
|
39000
|
+
process.stderr.write(`Error: Unknown template '${opts.template}'. Available: ${available}\n`);
|
|
39001
|
+
process.exit(1);
|
|
39002
|
+
return;
|
|
39003
|
+
}
|
|
39004
|
+
try {
|
|
39005
|
+
await writeFile$1(opts.output, entry.dotContent, "utf-8");
|
|
39006
|
+
process.stdout.write(`Created ${opts.output} from template '${entry.name}'\n`);
|
|
39007
|
+
} catch (err) {
|
|
39008
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
39009
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
39010
|
+
process.exit(1);
|
|
39011
|
+
}
|
|
39012
|
+
});
|
|
37779
39013
|
}
|
|
37780
39014
|
|
|
37781
39015
|
//#endregion
|
|
@@ -39264,4 +40498,4 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
|
|
|
39264
40498
|
|
|
39265
40499
|
//#endregion
|
|
39266
40500
|
export { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, GitClient, GrammarLoader, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, normalizeGraphSummaryToStatus, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runRunAction, runSolutioningPhase, validateStopAfterFromConflict };
|
|
39267
|
-
//# sourceMappingURL=run-
|
|
40501
|
+
//# sourceMappingURL=run-C3MnOuBe.js.map
|