elsabro 7.3.0 → 7.3.2
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/README.md +42 -5
- package/agents/elsabro-orchestrator.md +2 -0
- package/commands/elsabro/execute.md +322 -5
- package/commands/elsabro/quick.md +11 -11
- package/commands/elsabro/start.md +156 -14
- package/flow-engine/src/callbacks.js +79 -0
- package/flow-engine/src/checkpoint.js +41 -0
- package/flow-engine/src/cli.js +214 -6
- package/flow-engine/src/executors.js +134 -3
- package/flow-engine/src/graph.js +30 -2
- package/flow-engine/src/template.js +152 -72
- package/flow-engine/tests/checkpoint.test.js +476 -0
- package/flow-engine/tests/execute-dispatcher.test.js +738 -0
- package/flow-engine/tests/executors-complex.test.js +259 -0
- package/flow-engine/tests/graph.test.js +193 -0
- package/flow-engine/tests/skill-install.test.js +254 -0
- package/flow-engine/tests/validation.test.js +137 -0
- package/flows/development-flow.json +3 -3
- package/hooks/skill-discovery.sh +0 -0
- package/package.json +1 -1
package/flow-engine/src/graph.js
CHANGED
|
@@ -64,15 +64,16 @@ function buildGraph(flowDefinition) {
|
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* Validate that all node references (next, routes, true, false, onMaxIterations,
|
|
67
|
-
* onError) point to existing nodes.
|
|
67
|
+
* onError) point to existing nodes, and detect orphaned nodes.
|
|
68
68
|
*
|
|
69
|
-
* @param {{ nodes: Map<string, object
|
|
69
|
+
* @param {{ nodes: Map<string, object>, entryNode: string }} graph
|
|
70
70
|
* @returns {{ valid: boolean, errors: string[] }}
|
|
71
71
|
*/
|
|
72
72
|
function validateGraph(graph) {
|
|
73
73
|
const errors = [];
|
|
74
74
|
const nodeIds = new Set(graph.nodes.keys());
|
|
75
75
|
|
|
76
|
+
// 1. Check for dangling references (invalid routes)
|
|
76
77
|
for (const [id, node] of graph.nodes) {
|
|
77
78
|
const refs = getNextNodes(node);
|
|
78
79
|
for (const ref of refs) {
|
|
@@ -82,6 +83,33 @@ function validateGraph(graph) {
|
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
// 2. Detect orphaned nodes (nodes that are never reachable from entry)
|
|
87
|
+
const reachable = new Set();
|
|
88
|
+
const queue = [graph.entryNode];
|
|
89
|
+
|
|
90
|
+
while (queue.length > 0) {
|
|
91
|
+
const currentId = queue.shift();
|
|
92
|
+
if (reachable.has(currentId)) continue;
|
|
93
|
+
|
|
94
|
+
reachable.add(currentId);
|
|
95
|
+
const node = graph.nodes.get(currentId);
|
|
96
|
+
if (!node) continue; // Skip if node doesn't exist (handled by dangling ref check)
|
|
97
|
+
|
|
98
|
+
const nextNodes = getNextNodes(node);
|
|
99
|
+
for (const nextId of nextNodes) {
|
|
100
|
+
if (!reachable.has(nextId) && nodeIds.has(nextId)) {
|
|
101
|
+
queue.push(nextId);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Find orphaned nodes
|
|
107
|
+
for (const nodeId of nodeIds) {
|
|
108
|
+
if (!reachable.has(nodeId)) {
|
|
109
|
+
errors.push(`Orphaned node "${nodeId}" is unreachable from entry point`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
85
113
|
return { valid: errors.length === 0, errors };
|
|
86
114
|
}
|
|
87
115
|
|
|
@@ -73,7 +73,17 @@ registerFunction('collectErrors', (context) => {
|
|
|
73
73
|
registerFunction('hasCriticalIssues', (context, obj) => {
|
|
74
74
|
if (!obj) return false;
|
|
75
75
|
const json = typeof obj === 'string' ? obj : JSON.stringify(obj);
|
|
76
|
-
|
|
76
|
+
|
|
77
|
+
// Enhanced pattern detection for critical issues
|
|
78
|
+
const criticalPatterns = [
|
|
79
|
+
'"critical"',
|
|
80
|
+
'"blocking"',
|
|
81
|
+
'"P0"',
|
|
82
|
+
'"MUST_FIX"',
|
|
83
|
+
'"URGENT"'
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
return criticalPatterns.some(pattern => json.includes(pattern));
|
|
77
87
|
});
|
|
78
88
|
|
|
79
89
|
registerFunction('generateSummary', (context) => {
|
|
@@ -144,92 +154,162 @@ function parseLiteral(str) {
|
|
|
144
154
|
}
|
|
145
155
|
|
|
146
156
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* @param {
|
|
150
|
-
* @
|
|
151
|
-
* @returns {*}
|
|
157
|
+
* Handle filter syntax: "value | filterName"
|
|
158
|
+
* @param {string} expr
|
|
159
|
+
* @param {object} context
|
|
160
|
+
* @returns {*|null} - Filtered value or null if no match
|
|
152
161
|
*/
|
|
153
|
-
function
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (!filter) {
|
|
164
|
-
throw new TemplateError(`Unknown filter: "${filterName}"`);
|
|
165
|
-
}
|
|
166
|
-
return filter(innerValue);
|
|
162
|
+
function tryResolveFilter(expr, context) {
|
|
163
|
+
// Only match pipes that are NOT inside || (fallback operator)
|
|
164
|
+
const filterMatch = expr.match(/^(.+?)\s*\|\s*(?!\|)(\w+)\s*$/);
|
|
165
|
+
if (!filterMatch) return null;
|
|
166
|
+
|
|
167
|
+
const innerValue = resolveExpression(filterMatch[1], context);
|
|
168
|
+
const filterName = filterMatch[2];
|
|
169
|
+
const filter = _filters.get(filterName);
|
|
170
|
+
if (!filter) {
|
|
171
|
+
throw new TemplateError(`Unknown filter: "${filterName}"`);
|
|
167
172
|
}
|
|
173
|
+
return filter(innerValue);
|
|
174
|
+
}
|
|
168
175
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
/**
|
|
177
|
+
* Handle fallback operator: "expr || default"
|
|
178
|
+
* @param {string} expr
|
|
179
|
+
* @param {object} context
|
|
180
|
+
* @returns {*|null} - Primary or fallback value, or null if no match
|
|
181
|
+
*/
|
|
182
|
+
function tryResolveFallback(expr, context) {
|
|
183
|
+
const fallbackMatch = expr.match(/^(.+?)\s*\|\|\s*(.+)$/);
|
|
184
|
+
if (!fallbackMatch) return null;
|
|
185
|
+
|
|
186
|
+
const primary = resolveExpression(fallbackMatch[1], context);
|
|
187
|
+
if (primary != null && primary !== '') return primary;
|
|
188
|
+
|
|
189
|
+
// Try to resolve the default as a literal or expression
|
|
190
|
+
const fallbackLiteral = parseLiteral(fallbackMatch[2]);
|
|
191
|
+
if (fallbackLiteral.matched) return fallbackLiteral.value;
|
|
192
|
+
return resolveExpression(fallbackMatch[2], context);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Handle negation: "!expr"
|
|
197
|
+
* @param {string} expr
|
|
198
|
+
* @param {object} context
|
|
199
|
+
* @returns {*|null} - Negated value or null if no match
|
|
200
|
+
*/
|
|
201
|
+
function tryResolveNegation(expr, context) {
|
|
202
|
+
if (!expr.startsWith('!')) return null;
|
|
203
|
+
const inner = resolveExpression(expr.slice(1), context);
|
|
204
|
+
return !inner;
|
|
205
|
+
}
|
|
179
206
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
207
|
+
/**
|
|
208
|
+
* Handle boolean && expression
|
|
209
|
+
* @param {string} expr
|
|
210
|
+
* @param {object} context
|
|
211
|
+
* @returns {*|null} - Boolean result or null if no match
|
|
212
|
+
*/
|
|
213
|
+
function tryResolveAnd(expr, context) {
|
|
214
|
+
if (!expr.includes('&&')) return null;
|
|
215
|
+
|
|
216
|
+
const parts = expr.split('&&').map(p => p.trim());
|
|
217
|
+
return parts.every(part => {
|
|
218
|
+
const val = resolveExpression(part, context);
|
|
219
|
+
return !!val;
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Handle comparison operators: ===, !==
|
|
225
|
+
* @param {string} expr
|
|
226
|
+
* @param {object} context
|
|
227
|
+
* @returns {*|null} - Comparison result or null if no match
|
|
228
|
+
*/
|
|
229
|
+
function tryResolveComparison(expr, context) {
|
|
230
|
+
const compMatch = expr.match(/^(.+?)\s*(===|!==)\s*(.+)$/);
|
|
231
|
+
if (!compMatch) return null;
|
|
232
|
+
|
|
233
|
+
const left = resolveExpression(compMatch[1], context);
|
|
234
|
+
const right = resolveExpression(compMatch[3], context);
|
|
235
|
+
if (compMatch[2] === '===') return left === right;
|
|
236
|
+
if (compMatch[2] === '!==') return left !== right;
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Handle function call: "functionName(args)"
|
|
242
|
+
* @param {string} expr
|
|
243
|
+
* @param {object} context
|
|
244
|
+
* @returns {*|null} - Function result or null if no match
|
|
245
|
+
*/
|
|
246
|
+
function tryResolveFunctionCall(expr, context) {
|
|
247
|
+
const funcMatch = expr.match(/^(\w+)\(([^)]*)\)$/);
|
|
248
|
+
if (!funcMatch) return null;
|
|
249
|
+
|
|
250
|
+
const funcName = funcMatch[1];
|
|
251
|
+
const fn = _functions.get(funcName);
|
|
252
|
+
if (!fn) {
|
|
253
|
+
throw new TemplateError(`Unknown function: "${funcName}"`);
|
|
184
254
|
}
|
|
185
255
|
|
|
186
|
-
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
return parts.every(part => {
|
|
190
|
-
const val = resolveExpression(part, context);
|
|
191
|
-
return !!val;
|
|
192
|
-
});
|
|
256
|
+
const argsStr = funcMatch[2].trim();
|
|
257
|
+
if (argsStr === '') {
|
|
258
|
+
return fn(context);
|
|
193
259
|
}
|
|
194
260
|
|
|
195
|
-
//
|
|
196
|
-
const
|
|
197
|
-
if (
|
|
198
|
-
|
|
199
|
-
const right = resolveExpression(compMatch[3], context);
|
|
200
|
-
if (compMatch[2] === '===') return left === right;
|
|
201
|
-
if (compMatch[2] === '!==') return left !== right;
|
|
261
|
+
// Parse argument: could be a path reference or a quoted string
|
|
262
|
+
const argLiteral = parseLiteral(argsStr);
|
|
263
|
+
if (argLiteral.matched) {
|
|
264
|
+
return fn(context, argLiteral.value);
|
|
202
265
|
}
|
|
203
266
|
|
|
204
|
-
//
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
267
|
+
// Resolve as expression (path)
|
|
268
|
+
const argValue = resolveExpression(argsStr, context);
|
|
269
|
+
return fn(context, argValue);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Resolve a single expression string (the content inside {{ }}).
|
|
274
|
+
*
|
|
275
|
+
* Delegates to specialized handlers in order:
|
|
276
|
+
* 1. Filter syntax: "value | filterName"
|
|
277
|
+
* 2. Fallback operator: "expr || default"
|
|
278
|
+
* 3. Negation: "!expr"
|
|
279
|
+
* 4. Boolean AND: "a && b"
|
|
280
|
+
* 5. Comparison: "a === b", "a !== b"
|
|
281
|
+
* 6. Function call: "functionName(args)"
|
|
282
|
+
* 7. Literal value: true, false, number, string, [], {}
|
|
283
|
+
* 8. Path resolution: "inputs.task", "nodes.X.outputs.Y"
|
|
284
|
+
*
|
|
285
|
+
* @param {string} expr – raw expression without {{ }}
|
|
286
|
+
* @param {object} context – { inputs, nodes, steps, state }
|
|
287
|
+
* @returns {*}
|
|
288
|
+
*/
|
|
289
|
+
function resolveExpression(expr, context) {
|
|
290
|
+
const trimmed = expr.trim();
|
|
291
|
+
|
|
292
|
+
// Try each expression type in order (most specific to least specific)
|
|
293
|
+
const handlers = [
|
|
294
|
+
tryResolveFilter,
|
|
295
|
+
tryResolveFallback,
|
|
296
|
+
tryResolveNegation,
|
|
297
|
+
tryResolveAnd,
|
|
298
|
+
tryResolveComparison,
|
|
299
|
+
tryResolveFunctionCall
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
for (const handler of handlers) {
|
|
303
|
+
const result = handler(trimmed, context);
|
|
304
|
+
if (result !== null) return result;
|
|
224
305
|
}
|
|
225
306
|
|
|
226
|
-
//
|
|
307
|
+
// Try literal value
|
|
227
308
|
const literal = parseLiteral(trimmed);
|
|
228
309
|
if (literal.matched) return literal.value;
|
|
229
310
|
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
return value;
|
|
311
|
+
// Default: path resolution (most common case)
|
|
312
|
+
return resolvePath(context, trimmed);
|
|
233
313
|
}
|
|
234
314
|
|
|
235
315
|
/**
|