@crewx/workflow 0.3.22-rc.21 → 0.3.22-rc.211
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/SKILL.md +4 -4
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +21 -1012
- package/dist/src/card-state.d.ts +20 -0
- package/dist/src/card-state.js +1 -0
- package/dist/src/engine.d.ts +10 -3
- package/dist/src/engine.js +1 -348
- package/dist/src/failure.d.ts +41 -0
- package/dist/src/failure.js +1 -0
- package/dist/src/file-lock.d.ts +1 -1
- package/dist/src/file-lock.js +1 -97
- package/dist/src/list-view.d.ts +22 -0
- package/dist/src/list-view.js +3 -0
- package/dist/src/mermaid.d.ts +1 -1
- package/dist/src/mermaid.js +2 -133
- package/dist/src/parent-task-sync.d.ts +139 -0
- package/dist/src/parent-task-sync.js +1 -0
- package/dist/src/retry-policy.d.ts +38 -0
- package/dist/src/retry-policy.js +1 -0
- package/dist/src/run-manager.d.ts +37 -3
- package/dist/src/run-manager.js +4 -1228
- package/dist/src/types.d.ts +37 -4
- package/dist/src/types.js +1 -3
- package/dist/src/utils/output-format.d.ts +1 -1
- package/dist/src/utils/output-format.js +12 -157
- package/dist/src/utils/shell-security.d.ts +1 -1
- package/dist/src/utils/shell-security.js +1 -138
- package/dist/src/workflow-persistence.d.ts +48 -0
- package/dist/src/workflow-persistence.js +1 -0
- package/dist/workflow-schema.json +25 -4
- package/package.json +23 -4
- package/workflow-schema.json +25 -4
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/src/engine.d.ts.map +0 -1
- package/dist/src/engine.js.map +0 -1
- package/dist/src/file-lock.d.ts.map +0 -1
- package/dist/src/file-lock.js.map +0 -1
- package/dist/src/mermaid.d.ts.map +0 -1
- package/dist/src/mermaid.js.map +0 -1
- package/dist/src/run-manager.d.ts.map +0 -1
- package/dist/src/run-manager.js.map +0 -1
- package/dist/src/types.d.ts.map +0 -1
- package/dist/src/types.js.map +0 -1
- package/dist/src/utils/output-format.d.ts.map +0 -1
- package/dist/src/utils/output-format.js.map +0 -1
- package/dist/src/utils/shell-security.d.ts.map +0 -1
- package/dist/src/utils/shell-security.js.map +0 -1
package/dist/src/run-manager.js
CHANGED
|
@@ -1,1228 +1,4 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.RunManager = exports.SKILL_TASK_DEFAULT_TIMEOUT = void 0;
|
|
37
|
-
exports.parseSetArgs = parseSetArgs;
|
|
38
|
-
exports.readStdin = readStdin;
|
|
39
|
-
exports.interpolateTemplate = interpolateTemplate;
|
|
40
|
-
exports.safeEvaluate = safeEvaluate;
|
|
41
|
-
exports.safeEvaluateBranchCondition = safeEvaluateBranchCondition;
|
|
42
|
-
exports.validateSafeBranchCondition = validateSafeBranchCondition;
|
|
43
|
-
const fs = __importStar(require("fs"));
|
|
44
|
-
const path = __importStar(require("path"));
|
|
45
|
-
const yaml = __importStar(require("js-yaml"));
|
|
46
|
-
const cp = __importStar(require("child_process"));
|
|
47
|
-
const sdk_1 = require("@crewx/sdk");
|
|
48
|
-
const file_lock_1 = require("./file-lock");
|
|
49
|
-
const output_format_1 = require("./utils/output-format");
|
|
50
|
-
const shell_security_1 = require("./utils/shell-security");
|
|
51
|
-
exports.SKILL_TASK_DEFAULT_TIMEOUT = 600000;
|
|
52
|
-
const KNOWN_AUTO_NODE_TYPES = new Set([
|
|
53
|
-
'agent_task', 'skill_task', 'shell_task', 'branch', 'expression', 'join', 'end', 'error',
|
|
54
|
-
]);
|
|
55
|
-
const SELF_ADVANCING_NODE_TYPES = new Set(['branch', 'expression', 'end', 'error']);
|
|
56
|
-
function killProcessGroup(child) {
|
|
57
|
-
try {
|
|
58
|
-
if (process.platform === 'win32') {
|
|
59
|
-
if (child.pid) {
|
|
60
|
-
cp.spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' });
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
else if (child.pid) {
|
|
64
|
-
try {
|
|
65
|
-
process.kill(-child.pid, 'SIGKILL');
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
try {
|
|
69
|
-
child.kill('SIGKILL');
|
|
70
|
-
}
|
|
71
|
-
catch { }
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
catch { }
|
|
76
|
-
}
|
|
77
|
-
async function runProcess(command, argv, opts) {
|
|
78
|
-
return new Promise((resolve) => {
|
|
79
|
-
let child;
|
|
80
|
-
try {
|
|
81
|
-
child = cp.spawn(command, argv, {
|
|
82
|
-
cwd: opts.cwd,
|
|
83
|
-
env: opts.env,
|
|
84
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
85
|
-
shell: opts.shell ?? false,
|
|
86
|
-
detached: process.platform !== 'win32',
|
|
87
|
-
windowsHide: opts.windowsHide ?? true,
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
catch (err) {
|
|
91
|
-
resolve({ status: null, stdout: '', stderr: '', timedOut: false, error: err });
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
let stdout = '';
|
|
95
|
-
let stderr = '';
|
|
96
|
-
let settled = false;
|
|
97
|
-
const finish = (result) => {
|
|
98
|
-
if (settled)
|
|
99
|
-
return;
|
|
100
|
-
settled = true;
|
|
101
|
-
clearTimeout(timer);
|
|
102
|
-
resolve(result);
|
|
103
|
-
};
|
|
104
|
-
const timer = setTimeout(() => {
|
|
105
|
-
killProcessGroup(child);
|
|
106
|
-
finish({ status: null, stdout, stderr, timedOut: true });
|
|
107
|
-
}, opts.timeout);
|
|
108
|
-
if (typeof timer.unref === 'function')
|
|
109
|
-
timer.unref();
|
|
110
|
-
child.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
111
|
-
child.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
112
|
-
child.on('error', (err) => finish({ status: null, stdout, stderr, timedOut: false, error: err }));
|
|
113
|
-
child.on('close', (code) => finish({ status: code, stdout, stderr, timedOut: false }));
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
function maskArg(arg) {
|
|
117
|
-
return arg.replace(/^(--?[\w-]*(?:token|secret|password|passwd|key|auth)[\w-]*)=(.+)$/i, '$1=***');
|
|
118
|
-
}
|
|
119
|
-
function summarizeNode(node) {
|
|
120
|
-
switch (node.type) {
|
|
121
|
-
case 'agent_task':
|
|
122
|
-
return `@${node.agent ?? '?'} (${node.mode === 'query' ? 'q' : 'x'})`;
|
|
123
|
-
case 'skill_task':
|
|
124
|
-
return `skill ${node.skill ?? '?'} ${(node.args ?? []).map(maskArg).join(' ')}`.trim();
|
|
125
|
-
case 'shell_task':
|
|
126
|
-
return (node.command ?? []).map(maskArg).join(' ');
|
|
127
|
-
case 'branch':
|
|
128
|
-
return `branch: ${node.condition ?? ''}`;
|
|
129
|
-
case 'expression':
|
|
130
|
-
return `set ${Object.keys(node.set ?? {}).join(', ')}`;
|
|
131
|
-
default:
|
|
132
|
-
return node.type;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
async function storeNodeOutput(nodeSpec, nodeId, stdout, stateUpdates) {
|
|
136
|
-
if (!nodeSpec.output)
|
|
137
|
-
return;
|
|
138
|
-
const trimmed = stdout.trim();
|
|
139
|
-
if (nodeSpec.output_format === 'json') {
|
|
140
|
-
let parsed;
|
|
141
|
-
try {
|
|
142
|
-
parsed = JSON.parse((0, output_format_1.extractJson)(trimmed));
|
|
143
|
-
}
|
|
144
|
-
catch (e) {
|
|
145
|
-
throw new Error(`Node "${nodeId}" output_format=json parse failed: ${e.message}`);
|
|
146
|
-
}
|
|
147
|
-
if (nodeSpec.output_schema) {
|
|
148
|
-
await (0, output_format_1.validateJsonSchema)(parsed, nodeSpec.output_schema);
|
|
149
|
-
}
|
|
150
|
-
stateUpdates[nodeSpec.output] = parsed;
|
|
151
|
-
}
|
|
152
|
-
else {
|
|
153
|
-
stateUpdates[nodeSpec.output] = trimmed;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
function parseValue(raw) {
|
|
157
|
-
try {
|
|
158
|
-
return JSON.parse(raw);
|
|
159
|
-
}
|
|
160
|
-
catch {
|
|
161
|
-
return raw;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
function parseSetArgs(args) {
|
|
165
|
-
const sets = {};
|
|
166
|
-
for (let i = 0; i < args.length; i++) {
|
|
167
|
-
const arg = args[i];
|
|
168
|
-
if (arg === '--set' && i + 1 < args.length) {
|
|
169
|
-
const pair = args[i + 1];
|
|
170
|
-
const eqIdx = pair.indexOf('=');
|
|
171
|
-
if (eqIdx !== -1) {
|
|
172
|
-
const key = pair.substring(0, eqIdx);
|
|
173
|
-
const value = pair.substring(eqIdx + 1);
|
|
174
|
-
sets[key] = parseValue(value);
|
|
175
|
-
}
|
|
176
|
-
i++;
|
|
177
|
-
}
|
|
178
|
-
else if (arg.startsWith('--set=')) {
|
|
179
|
-
const rest = arg.substring('--set='.length);
|
|
180
|
-
const eqIdx = rest.indexOf('=');
|
|
181
|
-
if (eqIdx !== -1) {
|
|
182
|
-
const key = rest.substring(0, eqIdx);
|
|
183
|
-
const value = rest.substring(eqIdx + 1);
|
|
184
|
-
sets[key] = parseValue(value);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return sets;
|
|
189
|
-
}
|
|
190
|
-
async function readStdin() {
|
|
191
|
-
const chunks = [];
|
|
192
|
-
for await (const chunk of process.stdin) {
|
|
193
|
-
chunks.push(chunk);
|
|
194
|
-
}
|
|
195
|
-
return Buffer.concat(chunks).toString('utf-8').trimEnd();
|
|
196
|
-
}
|
|
197
|
-
function interpolateTemplate(template, state) {
|
|
198
|
-
return template.replace(/\{\{([\w.]+)\}\}/g, (_, expr) => {
|
|
199
|
-
const parts = expr.split('.');
|
|
200
|
-
let value = state[parts[0]];
|
|
201
|
-
for (let i = 1; i < parts.length && value != null; i++) {
|
|
202
|
-
value = value[parts[i]];
|
|
203
|
-
}
|
|
204
|
-
if (value === null || value === undefined)
|
|
205
|
-
return '';
|
|
206
|
-
if (typeof value === 'object')
|
|
207
|
-
return JSON.stringify(value, null, 2);
|
|
208
|
-
return String(value);
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
const FORBIDDEN_IDENTS = new Set([
|
|
212
|
-
'require', 'import', 'process', 'global', 'globalThis',
|
|
213
|
-
'Function', 'eval', '__proto__', 'constructor', 'prototype',
|
|
214
|
-
]);
|
|
215
|
-
const FORBIDDEN_STATE_PATH_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
216
|
-
function tokenize(expr) {
|
|
217
|
-
const tokens = [];
|
|
218
|
-
let i = 0;
|
|
219
|
-
while (i < expr.length) {
|
|
220
|
-
const ch = expr[i];
|
|
221
|
-
if (/\s/.test(ch)) {
|
|
222
|
-
i++;
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
|
-
if (/\d/.test(ch)) {
|
|
226
|
-
let num = '';
|
|
227
|
-
while (i < expr.length && /[\d.]/.test(expr[i])) {
|
|
228
|
-
num += expr[i];
|
|
229
|
-
i++;
|
|
230
|
-
}
|
|
231
|
-
tokens.push({ type: 'NUMBER', value: Number(num) });
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
if (ch === "'" || ch === '"') {
|
|
235
|
-
const quote = ch;
|
|
236
|
-
i++;
|
|
237
|
-
let str = '';
|
|
238
|
-
while (i < expr.length && expr[i] !== quote) {
|
|
239
|
-
if (expr[i] === '\\') {
|
|
240
|
-
i++;
|
|
241
|
-
str += expr[i] ?? '';
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
str += expr[i];
|
|
245
|
-
}
|
|
246
|
-
i++;
|
|
247
|
-
}
|
|
248
|
-
if (i >= expr.length)
|
|
249
|
-
throw new Error('Unterminated string literal');
|
|
250
|
-
i++;
|
|
251
|
-
tokens.push({ type: 'STRING', value: str });
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
if (/[a-zA-Z_]/.test(ch)) {
|
|
255
|
-
let ident = '';
|
|
256
|
-
while (i < expr.length && /\w/.test(expr[i])) {
|
|
257
|
-
ident += expr[i];
|
|
258
|
-
i++;
|
|
259
|
-
}
|
|
260
|
-
if (ident === 'true' || ident === 'false') {
|
|
261
|
-
tokens.push({ type: 'BOOLEAN', value: ident === 'true' });
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
264
|
-
if (FORBIDDEN_IDENTS.has(ident)) {
|
|
265
|
-
throw new Error(`Forbidden identifier in expression: "${ident}"`);
|
|
266
|
-
}
|
|
267
|
-
tokens.push({ type: 'IDENT', value: ident });
|
|
268
|
-
}
|
|
269
|
-
continue;
|
|
270
|
-
}
|
|
271
|
-
if (i + 1 < expr.length) {
|
|
272
|
-
const two = expr.substring(i, i + 2);
|
|
273
|
-
const twoCharMap = {
|
|
274
|
-
'>=': 'GTE', '<=': 'LTE', '==': 'EQ', '!=': 'NEQ', '&&': 'AND', '||': 'OR',
|
|
275
|
-
};
|
|
276
|
-
if (twoCharMap[two]) {
|
|
277
|
-
tokens.push({ type: twoCharMap[two], value: two });
|
|
278
|
-
i += 2;
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
const singleCharMap = {
|
|
283
|
-
'+': 'PLUS', '-': 'MINUS', '*': 'STAR', '/': 'SLASH', '%': 'PERCENT',
|
|
284
|
-
'>': 'GT', '<': 'LT', '!': 'NOT',
|
|
285
|
-
'?': 'QUESTION', ':': 'COLON',
|
|
286
|
-
'(': 'LPAREN', ')': 'RPAREN',
|
|
287
|
-
};
|
|
288
|
-
if (singleCharMap[ch]) {
|
|
289
|
-
tokens.push({ type: singleCharMap[ch], value: ch });
|
|
290
|
-
i++;
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
293
|
-
if (ch === '.' || ch === '[' || ch === ']') {
|
|
294
|
-
throw new Error(`Forbidden character in expression: "${ch}" (property access not allowed)`);
|
|
295
|
-
}
|
|
296
|
-
throw new Error(`Unexpected character in expression: "${ch}"`);
|
|
297
|
-
}
|
|
298
|
-
tokens.push({ type: 'EOF', value: '' });
|
|
299
|
-
return tokens;
|
|
300
|
-
}
|
|
301
|
-
class ExprParser {
|
|
302
|
-
constructor(tokens, state) {
|
|
303
|
-
this.tokens = tokens;
|
|
304
|
-
this.state = state;
|
|
305
|
-
this.pos = 0;
|
|
306
|
-
}
|
|
307
|
-
peek() { return this.tokens[this.pos]; }
|
|
308
|
-
advance() { return this.tokens[this.pos++]; }
|
|
309
|
-
expect(type) {
|
|
310
|
-
const t = this.advance();
|
|
311
|
-
if (t.type !== type)
|
|
312
|
-
throw new Error(`Expected ${type}, got ${t.type}`);
|
|
313
|
-
return t;
|
|
314
|
-
}
|
|
315
|
-
parse() {
|
|
316
|
-
const result = this.parseTernary();
|
|
317
|
-
if (this.peek().type !== 'EOF') {
|
|
318
|
-
throw new Error(`Unexpected token after expression: ${this.peek().type}`);
|
|
319
|
-
}
|
|
320
|
-
return result;
|
|
321
|
-
}
|
|
322
|
-
parseTernary() {
|
|
323
|
-
const cond = this.parseOr();
|
|
324
|
-
if (this.peek().type === 'QUESTION') {
|
|
325
|
-
this.advance();
|
|
326
|
-
const thenVal = this.parseTernary();
|
|
327
|
-
this.expect('COLON');
|
|
328
|
-
const elseVal = this.parseTernary();
|
|
329
|
-
return cond ? thenVal : elseVal;
|
|
330
|
-
}
|
|
331
|
-
return cond;
|
|
332
|
-
}
|
|
333
|
-
parseOr() {
|
|
334
|
-
let left = this.parseAnd();
|
|
335
|
-
while (this.peek().type === 'OR') {
|
|
336
|
-
this.advance();
|
|
337
|
-
const right = this.parseAnd();
|
|
338
|
-
left = left || right;
|
|
339
|
-
}
|
|
340
|
-
return left;
|
|
341
|
-
}
|
|
342
|
-
parseAnd() {
|
|
343
|
-
let left = this.parseEquality();
|
|
344
|
-
while (this.peek().type === 'AND') {
|
|
345
|
-
this.advance();
|
|
346
|
-
const right = this.parseEquality();
|
|
347
|
-
left = left && right;
|
|
348
|
-
}
|
|
349
|
-
return left;
|
|
350
|
-
}
|
|
351
|
-
parseEquality() {
|
|
352
|
-
let left = this.parseComparison();
|
|
353
|
-
while (this.peek().type === 'EQ' || this.peek().type === 'NEQ') {
|
|
354
|
-
const op = this.advance().type;
|
|
355
|
-
const right = this.parseComparison();
|
|
356
|
-
left = op === 'EQ' ? left == right : left != right;
|
|
357
|
-
}
|
|
358
|
-
return left;
|
|
359
|
-
}
|
|
360
|
-
parseComparison() {
|
|
361
|
-
let left = this.parseAdditive();
|
|
362
|
-
while (['GT', 'LT', 'GTE', 'LTE'].includes(this.peek().type)) {
|
|
363
|
-
const op = this.advance().type;
|
|
364
|
-
const right = this.parseAdditive();
|
|
365
|
-
switch (op) {
|
|
366
|
-
case 'GT':
|
|
367
|
-
left = left > right;
|
|
368
|
-
break;
|
|
369
|
-
case 'LT':
|
|
370
|
-
left = left < right;
|
|
371
|
-
break;
|
|
372
|
-
case 'GTE':
|
|
373
|
-
left = left >= right;
|
|
374
|
-
break;
|
|
375
|
-
case 'LTE':
|
|
376
|
-
left = left <= right;
|
|
377
|
-
break;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
return left;
|
|
381
|
-
}
|
|
382
|
-
parseAdditive() {
|
|
383
|
-
let left = this.parseMultiplicative();
|
|
384
|
-
while (this.peek().type === 'PLUS' || this.peek().type === 'MINUS') {
|
|
385
|
-
const op = this.advance().type;
|
|
386
|
-
const right = this.parseMultiplicative();
|
|
387
|
-
if (op === 'PLUS') {
|
|
388
|
-
if (typeof left === 'string' || typeof right === 'string') {
|
|
389
|
-
left = String(left) + String(right);
|
|
390
|
-
}
|
|
391
|
-
else {
|
|
392
|
-
left = left + right;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
else {
|
|
396
|
-
left = left - right;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
return left;
|
|
400
|
-
}
|
|
401
|
-
parseMultiplicative() {
|
|
402
|
-
let left = this.parseUnary();
|
|
403
|
-
while (['STAR', 'SLASH', 'PERCENT'].includes(this.peek().type)) {
|
|
404
|
-
const op = this.advance().type;
|
|
405
|
-
const right = this.parseUnary();
|
|
406
|
-
switch (op) {
|
|
407
|
-
case 'STAR':
|
|
408
|
-
left = left * right;
|
|
409
|
-
break;
|
|
410
|
-
case 'SLASH':
|
|
411
|
-
left = left / right;
|
|
412
|
-
break;
|
|
413
|
-
case 'PERCENT':
|
|
414
|
-
left = left % right;
|
|
415
|
-
break;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
return left;
|
|
419
|
-
}
|
|
420
|
-
parseUnary() {
|
|
421
|
-
if (this.peek().type === 'NOT') {
|
|
422
|
-
this.advance();
|
|
423
|
-
return !this.parseUnary();
|
|
424
|
-
}
|
|
425
|
-
if (this.peek().type === 'MINUS') {
|
|
426
|
-
this.advance();
|
|
427
|
-
return -this.parseUnary();
|
|
428
|
-
}
|
|
429
|
-
return this.parsePrimary();
|
|
430
|
-
}
|
|
431
|
-
parsePrimary() {
|
|
432
|
-
const token = this.peek();
|
|
433
|
-
switch (token.type) {
|
|
434
|
-
case 'NUMBER':
|
|
435
|
-
this.advance();
|
|
436
|
-
return token.value;
|
|
437
|
-
case 'STRING':
|
|
438
|
-
this.advance();
|
|
439
|
-
return token.value;
|
|
440
|
-
case 'BOOLEAN':
|
|
441
|
-
this.advance();
|
|
442
|
-
return token.value;
|
|
443
|
-
case 'IDENT': {
|
|
444
|
-
this.advance();
|
|
445
|
-
const key = token.value;
|
|
446
|
-
const val = this.state[key];
|
|
447
|
-
if (val === undefined || val === null)
|
|
448
|
-
return 0;
|
|
449
|
-
if (typeof val === 'boolean' || typeof val === 'number')
|
|
450
|
-
return val;
|
|
451
|
-
const strVal = String(val);
|
|
452
|
-
if (strVal === '')
|
|
453
|
-
return '';
|
|
454
|
-
const num = Number(strVal);
|
|
455
|
-
return isNaN(num) ? strVal : num;
|
|
456
|
-
}
|
|
457
|
-
case 'LPAREN': {
|
|
458
|
-
this.advance();
|
|
459
|
-
const result = this.parseTernary();
|
|
460
|
-
this.expect('RPAREN');
|
|
461
|
-
return result;
|
|
462
|
-
}
|
|
463
|
-
default:
|
|
464
|
-
throw new Error(`Unexpected token: ${token.type}`);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
function safeEvaluate(expr, state) {
|
|
469
|
-
const tokens = tokenize(expr);
|
|
470
|
-
const parser = new ExprParser(tokens, state);
|
|
471
|
-
return parser.parse();
|
|
472
|
-
}
|
|
473
|
-
function getStatePathValue(state, pathExpr) {
|
|
474
|
-
const parts = pathExpr.split('.');
|
|
475
|
-
let value = state;
|
|
476
|
-
for (const part of parts) {
|
|
477
|
-
if (FORBIDDEN_STATE_PATH_SEGMENTS.has(part)) {
|
|
478
|
-
throw new Error(`Forbidden state path segment in expression: "${part}"`);
|
|
479
|
-
}
|
|
480
|
-
if (!part || value === null || value === undefined || typeof value !== 'object') {
|
|
481
|
-
return undefined;
|
|
482
|
-
}
|
|
483
|
-
value = value[part];
|
|
484
|
-
}
|
|
485
|
-
return value;
|
|
486
|
-
}
|
|
487
|
-
function unescapeQuotedLiteral(value) {
|
|
488
|
-
return value
|
|
489
|
-
.replace(/\\'/g, "'")
|
|
490
|
-
.replace(/\\"/g, '"')
|
|
491
|
-
.replace(/\\\\/g, '\\');
|
|
492
|
-
}
|
|
493
|
-
function normalizeBranchCondition(expr, state) {
|
|
494
|
-
const scope = {};
|
|
495
|
-
let counter = 0;
|
|
496
|
-
const bind = (value) => {
|
|
497
|
-
const key = `__state_${counter++}`;
|
|
498
|
-
scope[key] = value;
|
|
499
|
-
return key;
|
|
500
|
-
};
|
|
501
|
-
let normalized = expr
|
|
502
|
-
.replace(/!==/g, '!=')
|
|
503
|
-
.replace(/===/g, '==');
|
|
504
|
-
const replaceIncludes = (pattern) => {
|
|
505
|
-
normalized = normalized.replace(pattern, (_match, statePath, literal) => {
|
|
506
|
-
const value = getStatePathValue(state, statePath);
|
|
507
|
-
return bind(String(value ?? '').includes(unescapeQuotedLiteral(literal)));
|
|
508
|
-
});
|
|
509
|
-
};
|
|
510
|
-
replaceIncludes(/\bstate\.([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\.includes\(\s*'((?:\\.|[^'\\])*)'\s*\)/g);
|
|
511
|
-
replaceIncludes(/\bstate\.([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\.includes\(\s*"((?:\\.|[^"\\])*)"\s*\)/g);
|
|
512
|
-
normalized = normalized.replace(/\bstate\.([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)/g, (_match, statePath) => bind(getStatePathValue(state, statePath)));
|
|
513
|
-
normalized = normalized.replace(/\bNumber\(\s*([A-Za-z_]\w*)\s*\)/g, '$1');
|
|
514
|
-
normalized = normalized.replace(/\bparseInt\(\s*([A-Za-z_]\w*(?:\s*\|\|\s*(?:'[^']*'|"[^"]*"))?)\s*\)/g, '($1)');
|
|
515
|
-
return { expr: normalized, scope };
|
|
516
|
-
}
|
|
517
|
-
function safeEvaluateBranchCondition(expr, state) {
|
|
518
|
-
const normalized = normalizeBranchCondition(expr, state);
|
|
519
|
-
return safeEvaluate(normalized.expr, { ...state, ...normalized.scope });
|
|
520
|
-
}
|
|
521
|
-
function validateSafeBranchCondition(expr) {
|
|
522
|
-
safeEvaluateBranchCondition(expr, {});
|
|
523
|
-
}
|
|
524
|
-
class RunManager {
|
|
525
|
-
constructor(runsDir = '.crewx/workflow-runs', projectRoot) {
|
|
526
|
-
this.runsDir = runsDir;
|
|
527
|
-
const base = projectRoot || process.env.CREWX_WORKSPACE || process.cwd();
|
|
528
|
-
this.resolvedDir = path.resolve(base, this.runsDir);
|
|
529
|
-
this.projectRoot = path.resolve(base);
|
|
530
|
-
}
|
|
531
|
-
ensureRunsDir() {
|
|
532
|
-
if (!fs.existsSync(this.resolvedDir)) {
|
|
533
|
-
fs.mkdirSync(this.resolvedDir, { recursive: true });
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
validateExecId(execId) {
|
|
537
|
-
if (/[/\\]|\.\./.test(execId)) {
|
|
538
|
-
throw new Error(`Invalid execution ID: "${execId}"`);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
loadRun(execId) {
|
|
542
|
-
this.validateExecId(execId);
|
|
543
|
-
const filePath = path.join(this.resolvedDir, `${execId}.json`);
|
|
544
|
-
if (!fs.existsSync(filePath))
|
|
545
|
-
return null;
|
|
546
|
-
try {
|
|
547
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
548
|
-
}
|
|
549
|
-
catch (e) {
|
|
550
|
-
throw new Error(`Failed to parse execution file ${filePath}: ${e.message}`);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
saveRun(run) {
|
|
554
|
-
this.ensureRunsDir();
|
|
555
|
-
run.updated_at = new Date().toISOString();
|
|
556
|
-
const filePath = path.join(this.resolvedDir, `${run.id}.json`);
|
|
557
|
-
fs.writeFileSync(filePath, JSON.stringify(run, null, 2) + '\n', 'utf-8');
|
|
558
|
-
}
|
|
559
|
-
atomicUpdate(execId, updater) {
|
|
560
|
-
this.validateExecId(execId);
|
|
561
|
-
this.ensureRunsDir();
|
|
562
|
-
const filePath = path.join(this.resolvedDir, `${execId}.json`);
|
|
563
|
-
const lock = new file_lock_1.FileLock(filePath);
|
|
564
|
-
lock.acquire();
|
|
565
|
-
try {
|
|
566
|
-
const run = this.loadRun(execId);
|
|
567
|
-
if (!run)
|
|
568
|
-
throw new Error(`Execution not found: ${execId}`);
|
|
569
|
-
updater(run);
|
|
570
|
-
this.saveRun(run);
|
|
571
|
-
return run;
|
|
572
|
-
}
|
|
573
|
-
finally {
|
|
574
|
-
lock.release();
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
async atomicUpdateAsync(execId, updater) {
|
|
578
|
-
this.validateExecId(execId);
|
|
579
|
-
this.ensureRunsDir();
|
|
580
|
-
const filePath = path.join(this.resolvedDir, `${execId}.json`);
|
|
581
|
-
const lock = new file_lock_1.FileLock(filePath);
|
|
582
|
-
await lock.acquireAsync();
|
|
583
|
-
try {
|
|
584
|
-
const run = this.loadRun(execId);
|
|
585
|
-
if (!run)
|
|
586
|
-
throw new Error(`Execution not found: ${execId}`);
|
|
587
|
-
updater(run);
|
|
588
|
-
this.saveRun(run);
|
|
589
|
-
return run;
|
|
590
|
-
}
|
|
591
|
-
finally {
|
|
592
|
-
lock.release();
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
loadWorkflowYaml(yamlPath) {
|
|
596
|
-
if (!fs.existsSync(yamlPath))
|
|
597
|
-
return null;
|
|
598
|
-
const content = fs.readFileSync(yamlPath, 'utf-8');
|
|
599
|
-
const doc = yaml.load(content);
|
|
600
|
-
if (!doc || !doc.workflows || typeof doc.workflows !== 'object')
|
|
601
|
-
return null;
|
|
602
|
-
return doc;
|
|
603
|
-
}
|
|
604
|
-
start(yamlPath, workflowId, overrides) {
|
|
605
|
-
const doc = this.loadWorkflowYaml(yamlPath);
|
|
606
|
-
if (!doc) {
|
|
607
|
-
throw new Error(`Failed to load workflow: ${yamlPath}`);
|
|
608
|
-
}
|
|
609
|
-
const workflowEntries = Object.entries(doc.workflows);
|
|
610
|
-
if (workflowEntries.length === 0) {
|
|
611
|
-
throw new Error(`No workflows found in ${yamlPath}`);
|
|
612
|
-
}
|
|
613
|
-
let resolvedId;
|
|
614
|
-
let spec;
|
|
615
|
-
if (workflowId) {
|
|
616
|
-
if (!doc.workflows[workflowId]) {
|
|
617
|
-
throw new Error(`Workflow "${workflowId}" not found in ${yamlPath}`);
|
|
618
|
-
}
|
|
619
|
-
resolvedId = workflowId;
|
|
620
|
-
spec = doc.workflows[workflowId];
|
|
621
|
-
}
|
|
622
|
-
else {
|
|
623
|
-
[resolvedId, spec] = workflowEntries[0];
|
|
624
|
-
}
|
|
625
|
-
const execId = (0, sdk_1.generateId)('wfr');
|
|
626
|
-
const initialState = spec.state
|
|
627
|
-
? JSON.parse(JSON.stringify(spec.state))
|
|
628
|
-
: {};
|
|
629
|
-
const sets = overrides ?? {};
|
|
630
|
-
const state = { ...initialState, ...sets };
|
|
631
|
-
const run = {
|
|
632
|
-
id: execId,
|
|
633
|
-
workflow_id: resolvedId,
|
|
634
|
-
workflow_file: yamlPath,
|
|
635
|
-
created_at: new Date().toISOString(),
|
|
636
|
-
updated_at: new Date().toISOString(),
|
|
637
|
-
current_node: '',
|
|
638
|
-
completed_nodes: [],
|
|
639
|
-
state,
|
|
640
|
-
initial_state: { ...initialState, ...sets },
|
|
641
|
-
};
|
|
642
|
-
this.saveRun(run);
|
|
643
|
-
return run;
|
|
644
|
-
}
|
|
645
|
-
getState(execId) {
|
|
646
|
-
const run = this.loadRun(execId);
|
|
647
|
-
if (!run) {
|
|
648
|
-
throw new Error(`Execution not found: ${execId}`);
|
|
649
|
-
}
|
|
650
|
-
return run.state;
|
|
651
|
-
}
|
|
652
|
-
setState(execId, updates) {
|
|
653
|
-
this.atomicUpdate(execId, (run) => {
|
|
654
|
-
Object.assign(run.state, updates);
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
moveNode(execId, nodeId) {
|
|
658
|
-
return this.atomicUpdate(execId, (run) => {
|
|
659
|
-
if (run.current_node && !run.completed_nodes.includes(run.current_node)) {
|
|
660
|
-
run.completed_nodes.push(run.current_node);
|
|
661
|
-
}
|
|
662
|
-
run.current_node = nodeId;
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
async executeNode(execId, nodeId, opts) {
|
|
666
|
-
const snapshot = this.loadRun(execId);
|
|
667
|
-
if (!snapshot) {
|
|
668
|
-
throw new Error(`Execution not found: ${execId}`);
|
|
669
|
-
}
|
|
670
|
-
const doc = this.loadWorkflowYaml(snapshot.workflow_file);
|
|
671
|
-
if (!doc) {
|
|
672
|
-
throw new Error(`Failed to load workflow file: ${snapshot.workflow_file}`);
|
|
673
|
-
}
|
|
674
|
-
const spec = doc.workflows[snapshot.workflow_id];
|
|
675
|
-
if (!spec) {
|
|
676
|
-
throw new Error(`Workflow "${snapshot.workflow_id}" not found in ${snapshot.workflow_file}`);
|
|
677
|
-
}
|
|
678
|
-
const nodeSpec = spec.nodes?.[nodeId];
|
|
679
|
-
if (!nodeSpec) {
|
|
680
|
-
throw new Error(`Node "${nodeId}" not found in workflow "${snapshot.workflow_id}"`);
|
|
681
|
-
}
|
|
682
|
-
const prevNode = snapshot.current_node;
|
|
683
|
-
const stateUpdates = {};
|
|
684
|
-
const nodesToComplete = [];
|
|
685
|
-
let currentNode = nodeId;
|
|
686
|
-
let status;
|
|
687
|
-
let failureMessage;
|
|
688
|
-
let nodeTaskId;
|
|
689
|
-
const trigger = opts?.trigger ?? 'manual';
|
|
690
|
-
const startedAt = new Date();
|
|
691
|
-
let nodeStdout = '';
|
|
692
|
-
let nodeStderr = '';
|
|
693
|
-
let nodeExitCode = null;
|
|
694
|
-
const buildAudit = (error) => {
|
|
695
|
-
const endedAt = new Date();
|
|
696
|
-
return {
|
|
697
|
-
exec_id: snapshot.id,
|
|
698
|
-
node_id: nodeId,
|
|
699
|
-
type: nodeSpec.type,
|
|
700
|
-
summary: summarizeNode(nodeSpec),
|
|
701
|
-
started_at: startedAt.toISOString(),
|
|
702
|
-
ended_at: endedAt.toISOString(),
|
|
703
|
-
duration_ms: endedAt.getTime() - startedAt.getTime(),
|
|
704
|
-
exit_code: nodeExitCode,
|
|
705
|
-
...(error ? { error } : {}),
|
|
706
|
-
trigger,
|
|
707
|
-
};
|
|
708
|
-
};
|
|
709
|
-
try {
|
|
710
|
-
switch (nodeSpec.type) {
|
|
711
|
-
case 'agent_task': {
|
|
712
|
-
if (!nodeSpec.agent) {
|
|
713
|
-
throw new Error(`Node "${nodeId}" is agent_task but has no agent defined`);
|
|
714
|
-
}
|
|
715
|
-
const input = nodeSpec.input
|
|
716
|
-
? interpolateTemplate(nodeSpec.input, { ...snapshot.state, state: snapshot.state })
|
|
717
|
-
: '';
|
|
718
|
-
const preamble = process.env.CREWX_WORKFLOW_PREAMBLE === 'off'
|
|
719
|
-
? ''
|
|
720
|
-
: (0, output_format_1.buildWorkflowPreamble)({
|
|
721
|
-
workflowId: snapshot.workflow_id,
|
|
722
|
-
nodeId,
|
|
723
|
-
execId: snapshot.id,
|
|
724
|
-
});
|
|
725
|
-
const finalInput = nodeSpec.output_format === 'json'
|
|
726
|
-
? preamble + input + (0, output_format_1.buildJsonOutputSuffix)(nodeSpec.output_schema, nodeSpec.output_strict)
|
|
727
|
-
: preamble + input;
|
|
728
|
-
const mode = nodeSpec.mode === 'query' ? 'q' : 'x';
|
|
729
|
-
const prompt = `@${nodeSpec.agent} ${finalInput}`;
|
|
730
|
-
if (opts?.dryRun) {
|
|
731
|
-
console.log(`[dry-run] Would execute: crewx ${mode} "@${nodeSpec.agent} ..."`);
|
|
732
|
-
break;
|
|
733
|
-
}
|
|
734
|
-
console.log(`Executing: @${nodeSpec.agent} (${mode} mode)`);
|
|
735
|
-
const crewxCli = process.env.CREWX_CLI || 'npx crewx';
|
|
736
|
-
const cliParts = crewxCli.split(/\s+/);
|
|
737
|
-
const bin = cliParts[0];
|
|
738
|
-
const baseArgs = cliParts.slice(1);
|
|
739
|
-
const nodeTimeout = Number(process.env[mode === 'q' ? 'CREWX_TIMEOUT_QUERY' : 'CREWX_TIMEOUT_EXECUTE']) || 8 * 3600000;
|
|
740
|
-
const wfArgs = [...baseArgs, mode, prompt];
|
|
741
|
-
if (process.env.CREWX_WORKFLOW_THREAD === 'on') {
|
|
742
|
-
wfArgs.push(`--thread=workflow:${snapshot.id}`);
|
|
743
|
-
}
|
|
744
|
-
const wfMetadata = {
|
|
745
|
-
workflow: {
|
|
746
|
-
exec_id: snapshot.id,
|
|
747
|
-
node_id: nodeId,
|
|
748
|
-
workflow_id: snapshot.workflow_id,
|
|
749
|
-
},
|
|
750
|
-
};
|
|
751
|
-
wfArgs.push('--metadata', JSON.stringify(wfMetadata));
|
|
752
|
-
const program = (0, sdk_1.resolveWindowsSpawnProgram)({
|
|
753
|
-
command: bin,
|
|
754
|
-
allowShellFallback: true,
|
|
755
|
-
packageName: 'crewx',
|
|
756
|
-
});
|
|
757
|
-
const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, wfArgs);
|
|
758
|
-
const result = await runProcess(invocation.command, invocation.argv, {
|
|
759
|
-
cwd: process.cwd(),
|
|
760
|
-
env: {
|
|
761
|
-
...process.env,
|
|
762
|
-
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
763
|
-
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
764
|
-
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
765
|
-
},
|
|
766
|
-
timeout: nodeTimeout,
|
|
767
|
-
shell: invocation.shell ?? false,
|
|
768
|
-
windowsHide: invocation.windowsHide,
|
|
769
|
-
});
|
|
770
|
-
nodeStdout = result.stdout;
|
|
771
|
-
nodeStderr = result.stderr;
|
|
772
|
-
nodeExitCode = result.status;
|
|
773
|
-
if (result.error) {
|
|
774
|
-
throw result.error;
|
|
775
|
-
}
|
|
776
|
-
if (result.timedOut) {
|
|
777
|
-
throw new Error(`Agent execution timed out after ${nodeTimeout}ms`);
|
|
778
|
-
}
|
|
779
|
-
const taskIdMatch = result.stderr.match(/crewx kill (tsk_\w+)/);
|
|
780
|
-
nodeTaskId = taskIdMatch?.[1];
|
|
781
|
-
if (result.status !== 0) {
|
|
782
|
-
const stderr = result.stderr.trim();
|
|
783
|
-
throw new Error(`Agent execution failed (exit ${result.status}): ${stderr}`);
|
|
784
|
-
}
|
|
785
|
-
const output = result.stdout.trim();
|
|
786
|
-
const signal = (0, output_format_1.detectWorkflowSignal)(output);
|
|
787
|
-
if (signal) {
|
|
788
|
-
throw new Error(`Node "${nodeId}" agent signaled failure: ${signal.reason ?? '(no reason given)'}`);
|
|
789
|
-
}
|
|
790
|
-
if (nodeSpec.output) {
|
|
791
|
-
if (nodeSpec.output_format === 'json') {
|
|
792
|
-
const retryMax = nodeSpec.output_retry ?? 1;
|
|
793
|
-
let parsed = null;
|
|
794
|
-
let lastError = null;
|
|
795
|
-
let retryInput = finalInput;
|
|
796
|
-
for (let attempt = 0; attempt <= retryMax; attempt++) {
|
|
797
|
-
let attemptOutput = output;
|
|
798
|
-
if (attempt > 0) {
|
|
799
|
-
retryInput = finalInput +
|
|
800
|
-
`\n\n## ⚠️ PREVIOUS OUTPUT FAILED JSON.parse: ${lastError.message}\n` +
|
|
801
|
-
`Return STRICT JSON ONLY. First char "{", last char "}". No prose, no fences.`;
|
|
802
|
-
const retryPrompt = `@${nodeSpec.agent} ${retryInput}`;
|
|
803
|
-
const retryArgs = [...baseArgs, mode, retryPrompt];
|
|
804
|
-
if (process.env.CREWX_WORKFLOW_THREAD === 'on') {
|
|
805
|
-
retryArgs.push(`--thread=workflow:${snapshot.id}`);
|
|
806
|
-
}
|
|
807
|
-
retryArgs.push('--metadata', JSON.stringify(wfMetadata));
|
|
808
|
-
const retryInvocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, retryArgs);
|
|
809
|
-
const retryResult = await runProcess(retryInvocation.command, retryInvocation.argv, {
|
|
810
|
-
cwd: process.cwd(),
|
|
811
|
-
env: {
|
|
812
|
-
...process.env,
|
|
813
|
-
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
814
|
-
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
815
|
-
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
816
|
-
},
|
|
817
|
-
timeout: nodeTimeout,
|
|
818
|
-
shell: retryInvocation.shell ?? false,
|
|
819
|
-
windowsHide: retryInvocation.windowsHide,
|
|
820
|
-
});
|
|
821
|
-
nodeStdout = retryResult.stdout;
|
|
822
|
-
nodeStderr = retryResult.stderr;
|
|
823
|
-
nodeExitCode = retryResult.status;
|
|
824
|
-
if (retryResult.error)
|
|
825
|
-
throw retryResult.error;
|
|
826
|
-
if (retryResult.timedOut)
|
|
827
|
-
throw new Error(`Agent retry timed out after ${nodeTimeout}ms`);
|
|
828
|
-
if (retryResult.status !== 0) {
|
|
829
|
-
throw new Error(`Agent retry failed (exit ${retryResult.status}): ${retryResult.stderr.trim()}`);
|
|
830
|
-
}
|
|
831
|
-
attemptOutput = retryResult.stdout.trim();
|
|
832
|
-
const retrySignal = (0, output_format_1.detectWorkflowSignal)(attemptOutput);
|
|
833
|
-
if (retrySignal) {
|
|
834
|
-
throw new Error(`Node "${nodeId}" agent signaled failure: ${retrySignal.reason ?? '(no reason given)'}`);
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
try {
|
|
838
|
-
parsed = JSON.parse((0, output_format_1.extractJson)(attemptOutput));
|
|
839
|
-
if (nodeSpec.output_schema) {
|
|
840
|
-
await (0, output_format_1.validateJsonSchema)(parsed, nodeSpec.output_schema);
|
|
841
|
-
}
|
|
842
|
-
break;
|
|
843
|
-
}
|
|
844
|
-
catch (e) {
|
|
845
|
-
lastError = e;
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
if (parsed === null) {
|
|
849
|
-
throw new Error(`Node "${nodeId}" output_format=json parse failed after ${retryMax + 1} attempt(s): ${lastError.message}`);
|
|
850
|
-
}
|
|
851
|
-
stateUpdates[nodeSpec.output] = parsed;
|
|
852
|
-
console.log(`Result stored in state.${nodeSpec.output} (JSON object)`);
|
|
853
|
-
}
|
|
854
|
-
else {
|
|
855
|
-
stateUpdates[nodeSpec.output] = output;
|
|
856
|
-
console.log(`Result stored in state.${nodeSpec.output} (${output.length} chars)`);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
break;
|
|
860
|
-
}
|
|
861
|
-
case 'skill_task': {
|
|
862
|
-
if (!nodeSpec.skill) {
|
|
863
|
-
throw new Error(`Node "${nodeId}" is skill_task but has no skill defined`);
|
|
864
|
-
}
|
|
865
|
-
if (!shell_security_1.SKILL_NAME_RE.test(nodeSpec.skill)) {
|
|
866
|
-
throw new Error(`Node "${nodeId}" has invalid skill name "${nodeSpec.skill}" (must match ^[a-z0-9][a-z0-9-]*$)`);
|
|
867
|
-
}
|
|
868
|
-
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
869
|
-
const skillArgs = (nodeSpec.args ?? []).map((a) => interpolateTemplate(String(a), ctx));
|
|
870
|
-
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, ctx) : undefined, this.projectRoot);
|
|
871
|
-
if (!cwdCheck.ok)
|
|
872
|
-
throw new Error(`Node "${nodeId}": ${cwdCheck.error}`);
|
|
873
|
-
const timeout = nodeSpec.timeout ?? exports.SKILL_TASK_DEFAULT_TIMEOUT;
|
|
874
|
-
if (opts?.dryRun) {
|
|
875
|
-
console.log(`[dry-run] Would execute: crewx skill ${nodeSpec.skill} ${skillArgs.join(' ')}`.trim());
|
|
876
|
-
break;
|
|
877
|
-
}
|
|
878
|
-
console.log(`Executing skill: ${nodeSpec.skill} ${skillArgs.join(' ')}`.trim());
|
|
879
|
-
const crewxCli = process.env.CREWX_CLI || 'npx crewx';
|
|
880
|
-
const cliParts = crewxCli.split(/\s+/);
|
|
881
|
-
const bin = cliParts[0];
|
|
882
|
-
const baseArgs = cliParts.slice(1);
|
|
883
|
-
const skillArgv = [...baseArgs, 'skill', nodeSpec.skill, ...skillArgs];
|
|
884
|
-
const program = (0, sdk_1.resolveWindowsSpawnProgram)({
|
|
885
|
-
command: bin,
|
|
886
|
-
allowShellFallback: true,
|
|
887
|
-
packageName: 'crewx',
|
|
888
|
-
});
|
|
889
|
-
const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, skillArgv);
|
|
890
|
-
const result = await runProcess(invocation.command, invocation.argv, {
|
|
891
|
-
cwd: cwdCheck.resolved,
|
|
892
|
-
env: {
|
|
893
|
-
...process.env,
|
|
894
|
-
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
895
|
-
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
896
|
-
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
897
|
-
},
|
|
898
|
-
timeout,
|
|
899
|
-
shell: invocation.shell ?? false,
|
|
900
|
-
windowsHide: invocation.windowsHide,
|
|
901
|
-
});
|
|
902
|
-
nodeStdout = result.stdout;
|
|
903
|
-
nodeStderr = result.stderr;
|
|
904
|
-
nodeExitCode = result.status;
|
|
905
|
-
if (result.error)
|
|
906
|
-
throw result.error;
|
|
907
|
-
if (result.timedOut)
|
|
908
|
-
throw new Error(`skill_task "${nodeId}" timed out after ${timeout}ms`);
|
|
909
|
-
if (result.status !== 0) {
|
|
910
|
-
throw new Error(`skill_task "${nodeId}" failed (exit ${result.status}): ${result.stderr.trim()}`);
|
|
911
|
-
}
|
|
912
|
-
await storeNodeOutput(nodeSpec, nodeId, result.stdout, stateUpdates);
|
|
913
|
-
if (nodeSpec.output) {
|
|
914
|
-
console.log(`Result stored in state.${nodeSpec.output}`);
|
|
915
|
-
}
|
|
916
|
-
break;
|
|
917
|
-
}
|
|
918
|
-
case 'shell_task': {
|
|
919
|
-
const shellEnvEnabled = process.env.CREWX_WORKFLOW_SHELL === '1';
|
|
920
|
-
const shellMetaAllowed = spec.metadata?.shell_task_allowed === true;
|
|
921
|
-
if (!shellEnvEnabled || !shellMetaAllowed) {
|
|
922
|
-
throw new Error('shell_task is disabled. Set CREWX_WORKFLOW_SHELL=1 and metadata.shell_task_allowed=true to enable.');
|
|
923
|
-
}
|
|
924
|
-
const cmdCheck = (0, shell_security_1.validateShellCommand)(nodeSpec.command);
|
|
925
|
-
if (!cmdCheck.ok)
|
|
926
|
-
throw new Error(`Node "${nodeId}": ${cmdCheck.error}`);
|
|
927
|
-
const envCheck = (0, shell_security_1.validateEnvKeys)(nodeSpec.env);
|
|
928
|
-
if (!envCheck.ok)
|
|
929
|
-
throw new Error(`Node "${nodeId}": ${envCheck.error}`);
|
|
930
|
-
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
931
|
-
const command = nodeSpec.command.map((c) => interpolateTemplate(c, ctx));
|
|
932
|
-
const postCheck = (0, shell_security_1.validateShellCommand)(command);
|
|
933
|
-
if (!postCheck.ok)
|
|
934
|
-
throw new Error(`Node "${nodeId}": ${postCheck.error}`);
|
|
935
|
-
const interpolatedEnv = nodeSpec.env
|
|
936
|
-
? Object.fromEntries(Object.entries(nodeSpec.env).map(([k, v]) => [k, interpolateTemplate(String(v), ctx)]))
|
|
937
|
-
: undefined;
|
|
938
|
-
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, ctx) : undefined, this.projectRoot);
|
|
939
|
-
if (!cwdCheck.ok)
|
|
940
|
-
throw new Error(`Node "${nodeId}": ${cwdCheck.error}`);
|
|
941
|
-
const timeout = nodeSpec.timeout ?? exports.SKILL_TASK_DEFAULT_TIMEOUT;
|
|
942
|
-
if (opts?.dryRun) {
|
|
943
|
-
console.log(`[dry-run] Would execute: ${command.join(' ')}`);
|
|
944
|
-
break;
|
|
945
|
-
}
|
|
946
|
-
console.log(`Executing shell_task: ${command[0]} (${command.length - 1} args)`);
|
|
947
|
-
const childEnv = (0, shell_security_1.buildShellEnv)(interpolatedEnv, process.env);
|
|
948
|
-
childEnv.CREWX_WORKFLOW_EXEC_ID = snapshot.id;
|
|
949
|
-
childEnv.CREWX_WORKFLOW_NODE_ID = nodeId;
|
|
950
|
-
childEnv.CREWX_WORKFLOW_ID = snapshot.workflow_id;
|
|
951
|
-
const result = await runProcess(command[0], command.slice(1), {
|
|
952
|
-
cwd: cwdCheck.resolved,
|
|
953
|
-
env: childEnv,
|
|
954
|
-
timeout,
|
|
955
|
-
shell: false,
|
|
956
|
-
});
|
|
957
|
-
nodeStdout = result.stdout;
|
|
958
|
-
nodeStderr = result.stderr;
|
|
959
|
-
nodeExitCode = result.status;
|
|
960
|
-
if (result.error)
|
|
961
|
-
throw result.error;
|
|
962
|
-
if (result.timedOut)
|
|
963
|
-
throw new Error(`shell_task "${nodeId}" timed out after ${timeout}ms`);
|
|
964
|
-
if (result.status !== 0) {
|
|
965
|
-
throw new Error(`shell_task "${nodeId}" failed (exit ${result.status}): ${result.stderr.trim()}`);
|
|
966
|
-
}
|
|
967
|
-
await storeNodeOutput(nodeSpec, nodeId, result.stdout, stateUpdates);
|
|
968
|
-
if (nodeSpec.output) {
|
|
969
|
-
console.log(`Result stored in state.${nodeSpec.output}`);
|
|
970
|
-
}
|
|
971
|
-
break;
|
|
972
|
-
}
|
|
973
|
-
case 'branch': {
|
|
974
|
-
if (!nodeSpec.condition) {
|
|
975
|
-
throw new Error(`Branch node "${nodeId}" has no condition`);
|
|
976
|
-
}
|
|
977
|
-
const condResult = String(safeEvaluateBranchCondition(String(nodeSpec.condition), snapshot.state));
|
|
978
|
-
const branches = nodeSpec.branches;
|
|
979
|
-
const targetNode = branches?.[condResult] ?? nodeSpec.default;
|
|
980
|
-
if (!targetNode) {
|
|
981
|
-
throw new Error(`Branch "${nodeId}": no matching branch for "${condResult}" and no default`);
|
|
982
|
-
}
|
|
983
|
-
console.log(`Branch "${nodeId}": condition="${nodeSpec.condition}" → ${condResult} → ${targetNode}`);
|
|
984
|
-
nodesToComplete.push(nodeId);
|
|
985
|
-
currentNode = targetNode;
|
|
986
|
-
break;
|
|
987
|
-
}
|
|
988
|
-
case 'parallel': {
|
|
989
|
-
const branches = nodeSpec.branches;
|
|
990
|
-
if (branches && Array.isArray(branches)) {
|
|
991
|
-
console.log(`Parallel "${nodeId}": branches = [${branches.join(', ')}]`);
|
|
992
|
-
console.log(` Enter each branch sequentially with: workflow run node <exec-id> <branch-node>`);
|
|
993
|
-
}
|
|
994
|
-
break;
|
|
995
|
-
}
|
|
996
|
-
case 'join': {
|
|
997
|
-
console.log(`Join "${nodeId}": completed_nodes = [${snapshot.completed_nodes.join(', ')}]`);
|
|
998
|
-
break;
|
|
999
|
-
}
|
|
1000
|
-
case 'end': {
|
|
1001
|
-
status = 'completed';
|
|
1002
|
-
console.log(`End "${nodeId}": workflow execution completed`);
|
|
1003
|
-
break;
|
|
1004
|
-
}
|
|
1005
|
-
case 'error': {
|
|
1006
|
-
status = 'failed';
|
|
1007
|
-
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
1008
|
-
failureMessage = nodeSpec.message
|
|
1009
|
-
? interpolateTemplate(nodeSpec.message, ctx)
|
|
1010
|
-
: `Workflow failed at error node "${nodeId}"`;
|
|
1011
|
-
console.log(`Error "${nodeId}": workflow execution failed — ${failureMessage}`);
|
|
1012
|
-
break;
|
|
1013
|
-
}
|
|
1014
|
-
case 'expression': {
|
|
1015
|
-
if (!nodeSpec.set || typeof nodeSpec.set !== 'object') {
|
|
1016
|
-
throw new Error(`Expression node "${nodeId}" requires "set" field`);
|
|
1017
|
-
}
|
|
1018
|
-
for (const [key, expr] of Object.entries(nodeSpec.set)) {
|
|
1019
|
-
const result = safeEvaluate(String(expr), snapshot.state);
|
|
1020
|
-
stateUpdates[key] = String(result);
|
|
1021
|
-
console.log(` ${key} = ${result}`);
|
|
1022
|
-
}
|
|
1023
|
-
console.log(`Expression "${nodeId}": set ${Object.keys(nodeSpec.set).join(', ')}`);
|
|
1024
|
-
if (nodeSpec.next) {
|
|
1025
|
-
nodesToComplete.push(nodeId);
|
|
1026
|
-
currentNode = nodeSpec.next;
|
|
1027
|
-
}
|
|
1028
|
-
break;
|
|
1029
|
-
}
|
|
1030
|
-
case 'approval': {
|
|
1031
|
-
console.log(`Approval "${nodeId}": manual intervention required (not auto-executed)`);
|
|
1032
|
-
break;
|
|
1033
|
-
}
|
|
1034
|
-
default:
|
|
1035
|
-
console.log(`Unknown node type "${nodeSpec.type}" for node "${nodeId}"`);
|
|
1036
|
-
}
|
|
1037
|
-
if (nodeSpec.fail_when &&
|
|
1038
|
-
!opts?.dryRun &&
|
|
1039
|
-
(nodeSpec.type === 'agent_task' || nodeSpec.type === 'skill_task' || nodeSpec.type === 'shell_task')) {
|
|
1040
|
-
const judged = safeEvaluateBranchCondition(String(nodeSpec.fail_when), { ...snapshot.state, ...stateUpdates });
|
|
1041
|
-
if (judged === true || judged === 'true') {
|
|
1042
|
-
throw new Error(`Node "${nodeId}" judged failed by fail_when: ${nodeSpec.fail_when}`);
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
catch (e) {
|
|
1047
|
-
const errMessage = e.message;
|
|
1048
|
-
const partial = [
|
|
1049
|
-
nodeStdout.trim() ? `stdout: ${nodeStdout.trim().slice(-500)}` : '',
|
|
1050
|
-
nodeStderr.trim() ? `stderr: ${nodeStderr.trim().slice(-500)}` : '',
|
|
1051
|
-
].filter(Boolean).join(' | ');
|
|
1052
|
-
const auditEntry = buildAudit(partial ? `${errMessage} (${partial})` : errMessage);
|
|
1053
|
-
await this.atomicUpdateAsync(execId, (run) => {
|
|
1054
|
-
if (run.status === 'cancelled')
|
|
1055
|
-
return;
|
|
1056
|
-
Object.assign(run.state, stateUpdates);
|
|
1057
|
-
run.current_node = nodeId;
|
|
1058
|
-
run.status = 'failed';
|
|
1059
|
-
run.error = errMessage;
|
|
1060
|
-
run.audit = run.audit ?? [];
|
|
1061
|
-
run.audit.push(auditEntry);
|
|
1062
|
-
});
|
|
1063
|
-
throw e;
|
|
1064
|
-
}
|
|
1065
|
-
const successAudit = buildAudit();
|
|
1066
|
-
return this.atomicUpdateAsync(execId, (run) => {
|
|
1067
|
-
if (run.status === 'cancelled')
|
|
1068
|
-
return;
|
|
1069
|
-
Object.assign(run.state, stateUpdates);
|
|
1070
|
-
for (const node of nodesToComplete) {
|
|
1071
|
-
if (!run.completed_nodes.includes(node)) {
|
|
1072
|
-
run.completed_nodes.push(node);
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
if (prevNode && prevNode !== nodeId && !run.completed_nodes.includes(prevNode)) {
|
|
1076
|
-
run.completed_nodes.push(prevNode);
|
|
1077
|
-
}
|
|
1078
|
-
run.current_node = currentNode;
|
|
1079
|
-
if (status)
|
|
1080
|
-
run.status = status;
|
|
1081
|
-
if (failureMessage)
|
|
1082
|
-
run.error = failureMessage;
|
|
1083
|
-
if (nodeTaskId) {
|
|
1084
|
-
run.tasks = run.tasks ?? {};
|
|
1085
|
-
run.tasks[nodeId] = nodeTaskId;
|
|
1086
|
-
}
|
|
1087
|
-
run.audit = run.audit ?? [];
|
|
1088
|
-
run.audit.push(successAudit);
|
|
1089
|
-
});
|
|
1090
|
-
}
|
|
1091
|
-
async runAuto(execId) {
|
|
1092
|
-
const initial = this.loadRun(execId);
|
|
1093
|
-
if (!initial)
|
|
1094
|
-
throw new Error(`Execution not found: ${execId}`);
|
|
1095
|
-
const doc = this.loadWorkflowYaml(initial.workflow_file);
|
|
1096
|
-
if (!doc)
|
|
1097
|
-
throw new Error(`Failed to load workflow file: ${initial.workflow_file}`);
|
|
1098
|
-
const spec = doc.workflows[initial.workflow_id];
|
|
1099
|
-
if (!spec) {
|
|
1100
|
-
throw new Error(`Workflow "${initial.workflow_id}" not found in ${initial.workflow_file}`);
|
|
1101
|
-
}
|
|
1102
|
-
const executed = [];
|
|
1103
|
-
if (initial.status === 'completed' || initial.status === 'failed' || initial.status === 'cancelled') {
|
|
1104
|
-
return {
|
|
1105
|
-
run: initial,
|
|
1106
|
-
outcome: initial.status === 'completed' ? 'completed' : initial.status === 'cancelled' ? 'cancelled' : 'failed',
|
|
1107
|
-
reason: `run already ${initial.status}`,
|
|
1108
|
-
executed,
|
|
1109
|
-
};
|
|
1110
|
-
}
|
|
1111
|
-
const firstNode = Object.keys(spec.nodes ?? {})[0];
|
|
1112
|
-
const maxIterations = (typeof spec.max_iterations === 'number' && spec.max_iterations > 0)
|
|
1113
|
-
? spec.max_iterations
|
|
1114
|
-
: 30;
|
|
1115
|
-
const visitCounts = {};
|
|
1116
|
-
let iterations = 0;
|
|
1117
|
-
let run = initial;
|
|
1118
|
-
while (true) {
|
|
1119
|
-
run = this.loadRun(execId);
|
|
1120
|
-
if (run.status === 'completed')
|
|
1121
|
-
return { run, outcome: 'completed', executed };
|
|
1122
|
-
if (run.status === 'failed')
|
|
1123
|
-
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1124
|
-
if (run.status === 'cancelled')
|
|
1125
|
-
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
1126
|
-
const nodeId = run.current_node || firstNode;
|
|
1127
|
-
if (!nodeId)
|
|
1128
|
-
return { run, outcome: 'failed', reason: 'workflow has no nodes', executed };
|
|
1129
|
-
const node = spec.nodes[nodeId];
|
|
1130
|
-
if (!node) {
|
|
1131
|
-
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1132
|
-
r.current_node = nodeId;
|
|
1133
|
-
r.status = 'failed';
|
|
1134
|
-
r.error = `node "${nodeId}" not found in workflow`;
|
|
1135
|
-
});
|
|
1136
|
-
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1137
|
-
}
|
|
1138
|
-
if (node.type === 'approval') {
|
|
1139
|
-
return { run, outcome: 'paused', reason: `approval node "${nodeId}" requires manual decision`, executed };
|
|
1140
|
-
}
|
|
1141
|
-
if (node.type === 'parallel') {
|
|
1142
|
-
return { run, outcome: 'paused', reason: `parallel auto-run not implemented (node "${nodeId}")`, executed };
|
|
1143
|
-
}
|
|
1144
|
-
if (!KNOWN_AUTO_NODE_TYPES.has(node.type)) {
|
|
1145
|
-
return { run, outcome: 'paused', reason: `unknown node type "${node.type}" (node "${nodeId}")`, executed };
|
|
1146
|
-
}
|
|
1147
|
-
visitCounts[nodeId] = (visitCounts[nodeId] ?? 0) + 1;
|
|
1148
|
-
iterations++;
|
|
1149
|
-
if (visitCounts[nodeId] > maxIterations || iterations > maxIterations) {
|
|
1150
|
-
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1151
|
-
r.status = 'failed';
|
|
1152
|
-
r.error = `auto runner exceeded max_iterations (${maxIterations}); possible loop at "${nodeId}"`;
|
|
1153
|
-
});
|
|
1154
|
-
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1155
|
-
}
|
|
1156
|
-
try {
|
|
1157
|
-
run = await this.executeNode(execId, nodeId, { trigger: 'auto' });
|
|
1158
|
-
executed.push(nodeId);
|
|
1159
|
-
}
|
|
1160
|
-
catch (e) {
|
|
1161
|
-
run = this.loadRun(execId);
|
|
1162
|
-
return { run, outcome: 'failed', reason: e.message, executed };
|
|
1163
|
-
}
|
|
1164
|
-
if (run.status === 'completed')
|
|
1165
|
-
return { run, outcome: 'completed', executed };
|
|
1166
|
-
if (run.status === 'failed')
|
|
1167
|
-
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1168
|
-
if (run.status === 'cancelled')
|
|
1169
|
-
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
1170
|
-
if (!SELF_ADVANCING_NODE_TYPES.has(node.type)) {
|
|
1171
|
-
const next = node.next;
|
|
1172
|
-
if (!next) {
|
|
1173
|
-
return { run, outcome: 'paused', reason: `node "${nodeId}" has no "next"; stopping`, executed };
|
|
1174
|
-
}
|
|
1175
|
-
run = this.moveNode(execId, next);
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
cancelRun(execId, reason = 'Cancelled by user') {
|
|
1180
|
-
return this.atomicUpdate(execId, (run) => {
|
|
1181
|
-
if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {
|
|
1182
|
-
return;
|
|
1183
|
-
}
|
|
1184
|
-
const now = new Date();
|
|
1185
|
-
run.status = 'cancelled';
|
|
1186
|
-
run.error = reason;
|
|
1187
|
-
run.audit = run.audit ?? [];
|
|
1188
|
-
run.audit.push({
|
|
1189
|
-
exec_id: run.id,
|
|
1190
|
-
node_id: run.current_node || '',
|
|
1191
|
-
type: 'cancel',
|
|
1192
|
-
summary: reason,
|
|
1193
|
-
started_at: now.toISOString(),
|
|
1194
|
-
ended_at: now.toISOString(),
|
|
1195
|
-
duration_ms: 0,
|
|
1196
|
-
exit_code: null,
|
|
1197
|
-
error: reason,
|
|
1198
|
-
trigger: 'manual',
|
|
1199
|
-
});
|
|
1200
|
-
});
|
|
1201
|
-
}
|
|
1202
|
-
reset(execId) {
|
|
1203
|
-
return this.atomicUpdate(execId, (run) => {
|
|
1204
|
-
run.state = JSON.parse(JSON.stringify(run.initial_state));
|
|
1205
|
-
run.current_node = '';
|
|
1206
|
-
run.completed_nodes = [];
|
|
1207
|
-
});
|
|
1208
|
-
}
|
|
1209
|
-
listRuns(opts) {
|
|
1210
|
-
this.ensureRunsDir();
|
|
1211
|
-
const files = fs.readdirSync(this.resolvedDir).filter(f => f.endsWith('.json'));
|
|
1212
|
-
const runs = [];
|
|
1213
|
-
for (const file of files) {
|
|
1214
|
-
try {
|
|
1215
|
-
const run = JSON.parse(fs.readFileSync(path.join(this.resolvedDir, file), 'utf-8'));
|
|
1216
|
-
if (opts?.workflowId && run.workflow_id !== opts.workflowId)
|
|
1217
|
-
continue;
|
|
1218
|
-
runs.push(run);
|
|
1219
|
-
}
|
|
1220
|
-
catch {
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
runs.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
1224
|
-
return runs;
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
exports.RunManager = RunManager;
|
|
1228
|
-
//# sourceMappingURL=run-manager.js.map
|
|
1
|
+
"use strict";var xe=exports&&exports.__createBinding||(Object.create?(function(a,e,t,o){if(o===void 0)o=t;var r=Object.getOwnPropertyDescriptor(e,t);if(!r||("get"in r?!e.__esModule:r.writable||r.configurable)){r={enumerable:true,get:function(){return e[t]}}}Object.defineProperty(a,o,r)}):(function(a,e,t,o){if(o===void 0)o=t;a[o]=e[t]}));var Ie=exports&&exports.__setModuleDefault||(Object.create?(function(a,e){Object.defineProperty(a,"default",{enumerable:true,value:e})}):function(a,e){a["default"]=e});var ie=exports&&exports.__importStar||(function(){var a=function(e){a=Object.getOwnPropertyNames||function(t){var o=[];for(var r in t)if(Object.prototype.hasOwnProperty.call(t,r))o[o.length]=r;return o};return a(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null){for(var o=a(e),r=0;r<o.length;r++)if(o[r]!=="default")xe(t,e,o[r])}Ie(t,e);return t}})();Object.defineProperty(exports,"__esModule",{value:true});exports.RunManager=exports.WORKFLOW_RUN_LEGACY_RECOVERY_GRACE_MS=exports.WORKFLOW_RUN_INTERRUPTED_ERROR=exports.WORKFLOW_CANCEL_GRACE_MS=exports.SKILL_TASK_DEFAULT_TIMEOUT=exports.FileLock=void 0;exports.parseSetArgs=He;exports.readStdin=Ze;exports.interpolateTemplate=b;exports.safeEvaluate=Ee;exports.safeEvaluateBranchCondition=de;exports.validateSafeBranchCondition=st;exports.normalizeRunStatus=ee;exports.isRetryableFailure=Ue;const R=ie(require("fs"));const W=ie(require("path"));const Me=ie(require("js-yaml"));const Ge=ie(require("child_process"));const N=require("@crewx/sdk");const d=require("./failure");const le=require("./file-lock");const K=require("./utils/output-format");const U=require("./utils/shell-security");const ce=require("./retry-policy");var Ke=require("./file-lock");Object.defineProperty(exports,"FileLock",{enumerable:true,get:function(){return Ke.FileLock}});exports.SKILL_TASK_DEFAULT_TIMEOUT=6e5;exports.WORKFLOW_CANCEL_GRACE_MS=2e3;const Ve=100;exports.WORKFLOW_RUN_INTERRUPTED_ERROR="Workflow run interrupted: runner process exited before completion";exports.WORKFLOW_RUN_LEGACY_RECOVERY_GRACE_MS=10*6e4;const qe=1e3;function ze(a){return new Promise(e=>{setTimeout(e,a)})}const Ae=new Set(["agent_task","skill_task","shell_task","branch","expression","join","end","error"]);const Xe=new Set(["branch","expression","end","error"]);function Se(a,e){const t=(0,N.resolveCrewxExecutable)({env:a,cwd:e});if(!t.ok){throw(0,d.createWorkflowFailureError)("CLI_UNRESOLVABLE",(0,N.formatCrewxExecutableFailure)(t))}return(0,N.createCrewxChildInvocation)(t,{baseEnv:a})}async function ye(a,e="SIGKILL"){if(!a.pid)return;try{const t=await(0,N.terminateProcessTree)(a.pid,{signal:e});if(t.status==="failed"||t.status==="not_found"){try{a.kill(e)}catch{}}}catch{try{a.kill(e)}catch{}}}async function ue(a,e,t){return new Promise(o=>{const r=()=>{try{return t.cancellation?.isCancelled()===true}catch{return false}};if(r()){o({status:null,stdout:"",stderr:"",timedOut:false,cancelled:true,signal:null,errorOrigin:null});return}let s;try{s=Ge.spawn(a,e,{cwd:t.cwd,env:t.env,stdio:[t.stdin!==void 0?"pipe":"ignore","pipe","pipe"],shell:t.shell??false,detached:process.platform!=="win32",windowsHide:t.windowsHide??true})}catch(g){o({status:null,stdout:"",stderr:"",timedOut:false,signal:null,errorOrigin:"spawn",error:g});return}let i="";let n="";let c=false;let p=false;let l=false;let f=false;let m;let _;let S;let C;let w;let k;const F=new N.ConsoleDecoder;const H=new N.ConsoleDecoder;let X=Promise.resolve();let q=Promise.resolve();const Z=()=>{if(S)clearTimeout(S);if(C)clearTimeout(C);if(w)clearInterval(w);if(t.cancellation&&k&&fe.get(t.cancellation.key)===k){fe.delete(t.cancellation.key)}};const I=g=>{if(c)return;c=true;Z();o(f?{...g,cancelled:true,timedOut:false}:g)};const B=()=>{if(c||f)return;f=true;void ye(s,"SIGTERM");C=setTimeout(()=>{if(!c)void ye(s,"SIGKILL")},exports.WORKFLOW_CANCEL_GRACE_MS);if(typeof C.unref==="function"){C.unref()}};if(t.cancellation){k={cancel:B};fe.set(t.cancellation.key,k);w=setInterval(()=>{if(r())B()},Ve);if(typeof w.unref==="function")w.unref()}S=setTimeout(()=>{void ye(s);Q(()=>({status:null,stdout:i,stderr:n,timedOut:true,signal:null,errorOrigin:_?"stdin":m?"post_spawn":null,..._||m?{error:_??m}:{}}))},t.timeout);if(typeof S.unref==="function")S.unref();s.stdout?.on("data",g=>{X=X.then(async()=>{i+=await F.write(g)})});s.stderr?.on("data",g=>{q=q.then(async()=>{n+=await H.write(g)})});s.once("spawn",()=>{l=true});s.on("error",g=>{if(!l){I({status:null,stdout:i,stderr:n,timedOut:false,signal:null,errorOrigin:"spawn",error:g});return}m=m??g});if(t.stdin!==void 0){s.stdin?.on("error",g=>{_=_??g});try{s.stdin?.end(t.stdin)}catch(g){_=_??g}}const Q=g=>{if(c||p)return;p=true;void(async()=>{await Promise.all([X,q]);const[M,pe]=await Promise.all([F.end(),H.end()]);i+=M;n+=pe;I(g())})().catch(()=>I(g()))};s.on("close",(g,M)=>Q(()=>({status:g,stdout:i,stderr:n,timedOut:false,signal:M,errorOrigin:_?"stdin":m?"post_spawn":null,..._||m?{error:_??m}:{}})));if(r())B()})}const Be=/^[A-Za-z0-9_.-]{1,128}$/;function be(a){if(a===void 0)return void 0;const e=Be.exec(a);return e?.[0]===a?a:void 0}function Je(a){switch(a.type){case"agent_task":{const e=be(a.agent);return e?`@${e} (${a.mode==="query"?"q":"x"})`:`agent_task (${a.mode==="query"?"q":"x"})`}case"skill_task":{const e=be(a.skill);return e?`skill ${e}`:"skill_task"}case"shell_task":return"shell_task";case"branch":return"branch";case"expression":return"expression";default:return"workflow node"}}function J(a){if(a instanceof Error)return a.message;return typeof a==="string"?a:"Node execution failed"}function Ye(a){return{status:a.status,timedOut:a.timedOut,signal:a.signal,errorOrigin:a.errorOrigin}}function Y(a,e,t,o,r=new Date().toISOString()){a.audit=a.audit??[];a.audit.push({exec_id:a.id,node_id:e.failedNode,type:t,summary:e.causeMessage,started_at:r,ended_at:r,duration_ms:0,exit_code:e.exitCode,error:e.causeMessage,trigger:o})}function V(a,e,t){a.status="failed";a.error=(0,d.sanitizeLocalRunError)(t,e.causeMessage);a.last_failure=e}async function Le(a,e,t,o){if(!a.output)return;const r=t.trim();if(a.output_format==="json"){let s;try{s=JSON.parse((0,K.extractJson)(r));if(a.output_schema){await(0,K.validateJsonSchema)(s,a.output_schema)}}catch(i){throw(0,d.createWorkflowFailureError)("OUTPUT_INVALID",`Node "${e}" output_format=json parse or validation failed: ${J(i)}`)}o[a.output]=s}else{o[a.output]=r}}function We(a){try{return JSON.parse(a)}catch{return a}}function He(a){const e={};for(let t=0;t<a.length;t++){const o=a[t];if(o==="--set"&&t+1<a.length){const r=a[t+1];const s=r.indexOf("=");if(s!==-1){const i=r.substring(0,s);const n=r.substring(s+1);e[i]=We(n)}t++}else if(o.startsWith("--set=")){const r=o.substring("--set=".length);const s=r.indexOf("=");if(s!==-1){const i=r.substring(0,s);const n=r.substring(s+1);e[i]=We(n)}}}return e}async function Ze(){const a=[];for await(const e of process.stdin){a.push(e)}return Buffer.concat(a).toString("utf-8").trimEnd()}function b(a,e){return a.replace(/\{\{([\w.]+)\}\}/g,(t,o)=>{const r=o.split(".");let s=e[r[0]];for(let i=1;i<r.length&&s!=null;i++){s=s[r[i]]}if(s===null||s===void 0)return"";if(typeof s==="object")return JSON.stringify(s,null,2);return String(s)})}const Qe=new Set(["require","import","process","global","globalThis","Function","eval","__proto__","constructor","prototype"]);const et=new Set(["__proto__","constructor","prototype"]);function tt(a){const e=[];let t=0;while(t<a.length){const o=a[t];if(/\s/.test(o)){t++;continue}if(/\d/.test(o)){let s="";while(t<a.length&&/[\d.]/.test(a[t])){s+=a[t];t++}e.push({type:"NUMBER",value:Number(s)});continue}if(o==="'"||o==='"'){const s=o;t++;let i="";while(t<a.length&&a[t]!==s){if(a[t]==="\\"){t++;i+=a[t]??""}else{i+=a[t]}t++}if(t>=a.length)throw new Error("Unterminated string literal");t++;e.push({type:"STRING",value:i});continue}if(/[a-zA-Z_]/.test(o)){let s="";while(t<a.length&&/\w/.test(a[t])){s+=a[t];t++}if(s==="true"||s==="false"){e.push({type:"BOOLEAN",value:s==="true"})}else{if(Qe.has(s)){throw new Error(`Forbidden identifier in expression: "${s}"`)}e.push({type:"IDENT",value:s})}continue}if(t+1<a.length){const s=a.substring(t,t+2);const i={">=":"GTE","<=":"LTE","==":"EQ","!=":"NEQ","&&":"AND","||":"OR"};if(i[s]){e.push({type:i[s],value:s});t+=2;continue}}const r={"+":"PLUS","-":"MINUS","*":"STAR","/":"SLASH","%":"PERCENT",">":"GT","<":"LT","!":"NOT","?":"QUESTION",":":"COLON","(":"LPAREN",")":"RPAREN"};if(r[o]){e.push({type:r[o],value:o});t++;continue}if(o==="."||o==="["||o==="]"){throw new Error(`Forbidden character in expression: "${o}" (property access not allowed)`)}throw new Error(`Unexpected character in expression: "${o}"`)}e.push({type:"EOF",value:""});return e}class rt{constructor(e,t){this.tokens=e;this.state=t;this.pos=0}peek(){return this.tokens[this.pos]}advance(){return this.tokens[this.pos++]}expect(e){const t=this.advance();if(t.type!==e)throw new Error(`Expected ${e}, got ${t.type}`);return t}parse(){const e=this.parseTernary();if(this.peek().type!=="EOF"){throw new Error(`Unexpected token after expression: ${this.peek().type}`)}return e}parseTernary(){const e=this.parseOr();if(this.peek().type==="QUESTION"){this.advance();const t=this.parseTernary();this.expect("COLON");const o=this.parseTernary();return e?t:o}return e}parseOr(){let e=this.parseAnd();while(this.peek().type==="OR"){this.advance();const t=this.parseAnd();e=e||t}return e}parseAnd(){let e=this.parseEquality();while(this.peek().type==="AND"){this.advance();const t=this.parseEquality();e=e&&t}return e}parseEquality(){let e=this.parseComparison();while(this.peek().type==="EQ"||this.peek().type==="NEQ"){const t=this.advance().type;const o=this.parseComparison();e=t==="EQ"?e==o:e!=o}return e}parseComparison(){let e=this.parseAdditive();while(["GT","LT","GTE","LTE"].includes(this.peek().type)){const t=this.advance().type;const o=this.parseAdditive();switch(t){case"GT":e=e>o;break;case"LT":e=e<o;break;case"GTE":e=e>=o;break;case"LTE":e=e<=o;break}}return e}parseAdditive(){let e=this.parseMultiplicative();while(this.peek().type==="PLUS"||this.peek().type==="MINUS"){const t=this.advance().type;const o=this.parseMultiplicative();if(t==="PLUS"){if(typeof e==="string"||typeof o==="string"){e=String(e)+String(o)}else{e=e+o}}else{e=e-o}}return e}parseMultiplicative(){let e=this.parseUnary();while(["STAR","SLASH","PERCENT"].includes(this.peek().type)){const t=this.advance().type;const o=this.parseUnary();switch(t){case"STAR":e=e*o;break;case"SLASH":e=e/o;break;case"PERCENT":e=e%o;break}}return e}parseUnary(){if(this.peek().type==="NOT"){this.advance();return!this.parseUnary()}if(this.peek().type==="MINUS"){this.advance();return-this.parseUnary()}return this.parsePrimary()}parsePrimary(){const e=this.peek();switch(e.type){case"NUMBER":this.advance();return e.value;case"STRING":this.advance();return e.value;case"BOOLEAN":this.advance();return e.value;case"IDENT":{this.advance();const t=e.value;const o=this.state[t];if(o===void 0||o===null)return 0;if(typeof o==="boolean"||typeof o==="number")return o;const r=String(o);if(r==="")return"";const s=Number(r);return isNaN(s)?r:s}case"LPAREN":{this.advance();const t=this.parseTernary();this.expect("RPAREN");return t}default:throw new Error(`Unexpected token: ${e.type}`)}}}function Ee(a,e){const t=tt(a);const o=new rt(t,e);return o.parse()}function Fe(a,e){const t=e.split(".");let o=a;for(const r of t){if(et.has(r)){throw new Error(`Forbidden state path segment in expression: "${r}"`)}if(!r||o===null||o===void 0||typeof o!=="object"){return void 0}o=o[r]}return o}function ot(a){return a.replace(/\\'/g,"'").replace(/\\"/g,'"').replace(/\\\\/g,"\\")}function nt(a,e){const t={};let o=0;const r=n=>{const c=`__state_${o++}`;t[c]=n;return c};let s=a.replace(/!==/g,"!=").replace(/===/g,"==");const i=n=>{s=s.replace(n,(c,p,l)=>{const f=Fe(e,p);return r(String(f??"").includes(ot(l)))})};i(/\bstate\.([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\.includes\(\s*'((?:\\.|[^'\\])*)'\s*\)/g);i(/\bstate\.([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\.includes\(\s*"((?:\\.|[^"\\])*)"\s*\)/g);s=s.replace(/\bstate\.([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)/g,(n,c)=>r(Fe(e,c)));s=s.replace(/\bNumber\(\s*([A-Za-z_]\w*)\s*\)/g,"$1");s=s.replace(/\bparseInt\(\s*([A-Za-z_]\w*(?:\s*\|\|\s*(?:'[^']*'|"[^"]*"))?)\s*\)/g,"($1)");return{expr:s,scope:t}}function de(a,e){const t=nt(a,e);return Ee(t.expr,{...e,...t.scope})}function st(a){de(a,{})}function at(a){const e=a.state["__engine"];if(!e||typeof e!=="object")return{visitCounts:{},iterations:0};const t=e;const o=t["visitCounts"];const r={};if(o&&typeof o==="object"){for(const[i,n]of Object.entries(o)){if(typeof n==="number"&&Number.isFinite(n))r[i]=n}}const s=typeof t["iterations"]==="number"&&Number.isFinite(t["iterations"])?t["iterations"]:0;return{visitCounts:r,iterations:s}}const ke=new Set;const fe=new Map;function Te(a,e){return`${a}::${e}`}function it(a,e){fe.get(Te(a,e))?.cancel()}function De(a){if(!Number.isInteger(a)||a<=0)return false;if(a===process.pid)return true;try{process.kill(a,0);return true}catch(e){return e.code==="EPERM"}}function Re(a){return a==="completed"||a==="failed"||a==="cancelled"}function lt(a,e){if(a.status!==void 0||a.auto_run)return false;const t=a.audit?.at(-1);if(t?.type!=="retry")return false;const o=Date.parse(a.updated_at);return Number.isFinite(o)&&e-o<exports.WORKFLOW_RUN_LEGACY_RECOVERY_GRACE_MS}function ee(a,e=Date.now()){if(a.status!==void 0)return a;let t;if(a.current_node===""){t="pending"}else if(a.auto_run){t="running"}else if(lt(a,e)){t="retrying"}else{t="pending"}return{...a,status:t}}function Ue(a,e){if(a.status!=="failed")return false;if(!a.current_node)return false;const t=e.nodes?.[a.current_node];if(!t)return false;return t.type!=="error"}class ct{constructor(e=".crewx/workflow-runs",t,o){this.runsDir=e;this.sleep=o?.sleep??ze;this.onRunPersisted=o?.onRunPersisted;const r=t||process.env.CREWX_WORKSPACE||process.cwd();this.resolvedDir=W.resolve(r,this.runsDir);this.projectRoot=W.resolve(r);const s=R.existsSync(W.join(this.projectRoot,"crewx.yaml"))||R.existsSync(W.join(this.projectRoot,"crewx.yml"));if(!s){console.warn(`[RunManager] projectRoot "${this.projectRoot}" has no crewx.yaml/crewx.yml \u2014 agent_task nodes may fail to resolve agents.`)}}ensureRunsDir(){if(!R.existsSync(this.resolvedDir)){R.mkdirSync(this.resolvedDir,{recursive:true})}}validateExecId(e){if(/[/\\]|\.\./.test(e)){throw new Error(`Invalid execution ID: "${e}"`)}}isRunCancelled(e){try{return this.readRunRaw(e)?.status==="cancelled"}catch{return false}}processCancellation(e){return{key:Te(this.resolvedDir,e),isCancelled:()=>this.isRunCancelled(e)}}readRunRaw(e){const t=W.join(this.resolvedDir,`${e}.json`);if(!R.existsSync(t))return null;try{return JSON.parse(R.readFileSync(t,"utf-8"))}catch(o){throw new Error(`Failed to parse execution file ${t}: ${o.message}`)}}readRun(e){this.validateExecId(e);const t=this.readRunRaw(e);return t?ee(t):null}isLegacyStatuslessStale(e){if(e.status!==void 0||e.auto_run||e.manual_resume||!e.current_node||!e.audit?.length){return false}const t=Date.parse(e.updated_at);if(!Number.isFinite(t)||Date.now()-t<exports.WORKFLOW_RUN_LEGACY_RECOVERY_GRACE_MS){return false}const o=this.loadWorkflowYaml(e.workflow_file);const r=o?.workflows[e.workflow_id];const s=r?.nodes?.[e.current_node];return Boolean(s&&Ae.has(s.type))}withRunLock(e,t){this.validateExecId(e);const o=W.join(this.resolvedDir,`${e}.json`);if(!R.existsSync(o)){throw new Error(`Execution not found: ${e}`)}const r=new le.FileLock(o);r.acquire();try{const s=this.readRunRaw(e);if(!s)throw new Error(`Execution not found: ${e}`);return t(ee(s))}finally{r.release()}}hasStaleRunner(e){if(e.auto_run)return!De(e.auto_run.pid);return this.isLegacyStatuslessStale(e)}recoverStaleRunner(e){const t=W.join(this.resolvedDir,`${e}.json`);const o=new le.FileLock(t);o.acquire();try{const r=this.readRunRaw(e);if(!r||!this.hasStaleRunner(r))return r?ee(r):null;if(Re(r.status)){delete r.auto_run;delete r.manual_resume;this.saveRun(r);return r}const s=r.auto_run;const i=s?.node_id||r.current_node;const n=new Date().toISOString();const c=(0,d.buildNodeFailure)({failedNode:i,attempt:(0,d.readNodeAttempt)(r,i),causeCode:"INTERRUPTED",exitCode:null,signal:null,errorOrigin:null});V(r,c,exports.WORKFLOW_RUN_INTERRUPTED_ERROR);delete r.auto_run;delete r.manual_resume;Y(r,c,"interrupted","auto",n);this.saveRun(r);return r}finally{o.release()}}loadRun(e){this.validateExecId(e);const t=this.readRunRaw(e);if(!t||!this.hasStaleRunner(t))return t?ee(t):null;return this.recoverStaleRunner(e)}saveRun(e){this.ensureRunsDir();e.updated_at=new Date().toISOString();const t=W.join(this.resolvedDir,`${e.id}.json`);let o;try{o=R.statSync(t).mode&4095}catch(c){if(c.code!=="ENOENT")throw c}const r=W.join(this.resolvedDir,`.${e.id}.tmp-${process.pid}-${(0,N.generateId)("wfr")}`);const s=JSON.stringify(e,null,2)+"\n";let i;let n=false;try{i=R.openSync(r,"wx");n=true;R.writeFileSync(i,s,"utf-8");if(o!==void 0)R.fchmodSync(i,o);R.closeSync(i);i=void 0;R.renameSync(r,t);n=false}finally{if(i!==void 0){try{R.closeSync(i)}catch{}}if(n){try{R.unlinkSync(r)}catch{}}}if(!this.onRunPersisted)return;try{this.onRunPersisted(e)}catch(c){console.warn(`[RunManager] onRunPersisted failed for ${e.id}: ${J(c)}`)}}atomicUpdate(e,t){this.validateExecId(e);this.ensureRunsDir();const o=W.join(this.resolvedDir,`${e}.json`);const r=new le.FileLock(o);r.acquire();try{const s=this.readRun(e);if(!s)throw new Error(`Execution not found: ${e}`);t(s);this.saveRun(s);return s}finally{r.release()}}async atomicUpdateAsync(e,t){this.validateExecId(e);this.ensureRunsDir();const o=W.join(this.resolvedDir,`${e}.json`);const r=new le.FileLock(o);await r.acquireAsync();try{const s=this.readRun(e);if(!s)throw new Error(`Execution not found: ${e}`);t(s);this.saveRun(s);return s}finally{r.release()}}async reserveNodeAttempt(e,t){let o=1;await this.atomicUpdateAsync(e,r=>{if(r.status==="cancelled"){o=(0,d.readNodeAttempt)(r,t);return}const s=r.node_attempts?.[t];const i=typeof s==="number"&&Number.isInteger(s)&&s>0?s:0;o=i+1;r.node_attempts=r.node_attempts??{};r.node_attempts[t]=o});return o}clearNodeAttempt(e,t){if(!e.node_attempts)return;delete e.node_attempts[t];if(Object.keys(e.node_attempts).length===0)delete e.node_attempts}loadWorkflowYaml(e){if(!R.existsSync(e))return null;const t=R.readFileSync(e,"utf-8");const o=Me.load(t);if(!o||!o.workflows||typeof o.workflows!=="object")return null;return o}getNodeType(e,t,o){const r=this.loadWorkflowYaml(e);const s=r?.workflows[t];const i=s?.nodes?.[o];return typeof i?.type==="string"?i.type:null}start(e,t,o){const r=this.loadWorkflowYaml(e);if(!r){throw new Error(`Failed to load workflow: ${e}`)}const s=Object.entries(r.workflows);if(s.length===0){throw new Error(`No workflows found in ${e}`)}let i;let n;if(t){if(!r.workflows[t]){throw new Error(`Workflow "${t}" not found in ${e}`)}i=t;n=r.workflows[t]}else{[i,n]=s[0]}const c=(0,N.generateId)("wfr");const p=n.state?JSON.parse(JSON.stringify(n.state)):{};const l=o??{};const f={...p,...l};const m={id:c,workflow_id:i,workflow_file:e,created_at:new Date().toISOString(),updated_at:new Date().toISOString(),current_node:"",completed_nodes:[],state:f,initial_state:{...p,...l},status:"pending"};this.saveRun(m);return m}getState(e){const t=this.loadRun(e);if(!t){throw new Error(`Execution not found: ${e}`)}return t.state}setState(e,t){this.atomicUpdate(e,o=>{Object.assign(o.state,t)})}moveNode(e,t){return this.atomicUpdate(e,o=>{if(o.status==="cancelled")return;if(o.current_node&&!o.completed_nodes.includes(o.current_node)){o.completed_nodes.push(o.current_node)}o.current_node=t})}async executeNode(e,t,o){const r=this.loadRun(e);if(!r){throw new Error(`Execution not found: ${e}`)}if(r.status==="cancelled"){throw(0,d.createWorkflowFailureError)("CANCELLED",`Workflow run "${e}" is already cancelled`)}const s=this.loadWorkflowYaml(r.workflow_file);if(!s){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Failed to load workflow file: ${r.workflow_file}`)}const i=s.workflows[r.workflow_id];if(!i){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Workflow "${r.workflow_id}" not found in ${r.workflow_file}`)}const n=i.nodes?.[t];if(!n){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}" not found in workflow "${r.workflow_id}"`)}const c=r.current_node;const p={};const l=[];let f=t;let m;let _;let S;let C;const w=o?.trigger??"manual";const k=new Date;let F=null;let H=null;let X=null;let q;let Z;const I=u=>{F=u.status;H=u.signal;X=u.errorOrigin;q=Ye(u)};const B=async()=>{const u=(0,N.generateId)("tsk");await this.atomicUpdateAsync(e,h=>{h.tasks=h.tasks??{};h.tasks[t]=u});C=u;return u};const Q=u=>{const h=new Date;return{exec_id:r.id,node_id:t,type:n.type,summary:Je(n),started_at:k.toISOString(),ended_at:h.toISOString(),duration_ms:h.getTime()-k.getTime(),exit_code:F,...u?{error:u}:{},trigger:w}};const g=await this.reserveNodeAttempt(e,t);try{switch(n.type){case"agent_task":{if(!n.agent){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}" is agent_task but has no agent defined`)}const u={...r.state,state:r.state};const h=b(n.agent,u).trim();if(!h){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}": assignee not resolved (agent field "${n.agent}" interpolated to empty)`)}const E=(0,U.normalizeCwd)(n.cwd?b(n.cwd,u):void 0,this.projectRoot);if(!E.ok){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}": ${E.error}`)}const $=E.resolved;const v=(0,U.normalizeCwd)(void 0,this.projectRoot);if(!v.ok){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}": ${v.error}`)}const y=v.resolved;const G=n.input?b(n.input,u):"";const j=process.env.CREWX_WORKFLOW_PREAMBLE==="off"?"":(0,K.buildWorkflowPreamble)({workflowId:r.workflow_id,nodeId:t,execId:r.id});const P=n.output_format==="json"?j+G+(0,K.buildJsonOutputSuffix)(n.output_schema,n.output_strict):j+G;const T=n.mode==="query"?"q":"x";const L=`@${h} ${P}`;if(o?.dryRun){console.log(`[dry-run] Would execute: crewx ${T} "@${h} ..."`);break}console.log(`Executing: @${h} (${T} mode)`);const A=Number(process.env[T==="q"?"CREWX_TIMEOUT_QUERY":"CREWX_TIMEOUT_EXECUTE"])||8*36e5;const O=Se({...process.env,CREWX_WORKSPACE:y,CREWX_WORKFLOW_EXEC_ID:r.id,CREWX_WORKFLOW_NODE_ID:t,CREWX_WORKFLOW_ID:r.workflow_id},y);const z=[...O.args,T,L];if(process.env.CREWX_WORKFLOW_THREAD==="on"){z.push(`--thread=workflow:${r.id}`)}const te={workflow:{exec_id:r.id,node_id:t,workflow_id:r.workflow_id}};z.push("--metadata",JSON.stringify(te));const Ne=(0,N.resolveWindowsSpawnProgram)({command:O.command,env:O.env,cwd:$,allowShellFallback:true,packageName:"crewx"});const re=(0,N.materializeWindowsSpawnProgram)(Ne,z);const je=await B();const D=await ue(re.command,re.argv,{cwd:$,env:(0,N.withoutCrewxRootContext)({...O.env,CREWX_TRACE_ID:je}),timeout:A,shell:re.shell??false,windowsHide:re.windowsHide,cancellation:this.processCancellation(e)});I(D);if(D.cancelled){throw(0,d.createWorkflowFailureError)("CANCELLED",`Agent execution cancelled for node "${t}"`)}if(D.timedOut){throw new Error(`Agent execution timed out after ${A}ms`)}if(D.error){throw D.error}const Oe=D.stderr.match(/crewx kill (tsk_\w+)/);if(Oe?.[1])C=Oe[1];if(D.status!==0){const ne=D.stderr.trim();throw new Error(`Agent execution failed (exit ${D.status}): ${ne}`)}const oe=D.stdout.trim();const ve=(0,K.detectWorkflowSignal)(oe);if(ve){throw new Error(`Node "${t}" agent signaled failure: ${ve.reason??"(no reason given)"}`)}if(n.output){if(n.output_format==="json"){const ne=n.output_retry??1;let se=null;let we=null;let Ce=P;for(let he=0;he<=ne;he++){let _e=oe;if(he>0){Ce=P+`
|
|
2
|
+
|
|
3
|
+
## \u26A0\uFE0F PREVIOUS OUTPUT FAILED JSON.parse: ${we.message}
|
|
4
|
+
Return STRICT JSON ONLY. First char "{", last char "}". No prose, no fences.`;const me=`@${h} ${Ce}`;const ge=[...O.args,T,me];if(process.env.CREWX_WORKFLOW_THREAD==="on"){ge.push(`--thread=workflow:${r.id}`)}ge.push("--metadata",JSON.stringify(te));const ae=(0,N.materializeWindowsSpawnProgram)(Ne,ge);const Pe=await B();const x=await ue(ae.command,ae.argv,{cwd:$,env:(0,N.withoutCrewxRootContext)({...O.env,CREWX_TRACE_ID:Pe}),timeout:A,shell:ae.shell??false,windowsHide:ae.windowsHide,cancellation:this.processCancellation(e)});I(x);if(x.cancelled){throw(0,d.createWorkflowFailureError)("CANCELLED",`Agent retry cancelled for node "${t}"`)}if(x.timedOut)throw new Error(`Agent retry timed out after ${A}ms`);if(x.error)throw x.error;if(x.status!==0){throw new Error(`Agent retry failed (exit ${x.status}): ${x.stderr.trim()}`)}_e=x.stdout.trim();const $e=(0,K.detectWorkflowSignal)(_e);if($e){throw new Error(`Node "${t}" agent signaled failure: ${$e.reason??"(no reason given)"}`)}}try{se=JSON.parse((0,K.extractJson)(_e));if(n.output_schema){await(0,K.validateJsonSchema)(se,n.output_schema)}break}catch(me){we=me}}if(se===null){throw(0,d.createWorkflowFailureError)("OUTPUT_INVALID",`Node "${t}" output_format=json parse failed after ${ne+1} attempt(s): ${we.message}`)}p[n.output]=se;console.log(`Result stored in state.${n.output} (JSON object)`)}else{p[n.output]=oe;console.log(`Result stored in state.${n.output} (${oe.length} chars)`)}}break}case"skill_task":{if(!n.skill){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}" is skill_task but has no skill defined`)}if(!U.SKILL_NAME_RE.test(n.skill)){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}" has invalid skill name "${n.skill}" (must match ^[a-z0-9][a-z0-9-]*$)`)}const u={...r.state,state:r.state};const h=(n.args??[]).map(O=>b(String(O),u));const E=n.stdin!==void 0?b(String(n.stdin),u):void 0;const $=(0,U.normalizeCwd)(n.cwd?b(n.cwd,u):void 0,this.projectRoot);if(!$.ok){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}": ${$.error}`)}const v=(0,U.normalizeCwd)(void 0,this.projectRoot);if(!v.ok){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}": ${v.error}`)}const y=v.resolved;const G=n.timeout??exports.SKILL_TASK_DEFAULT_TIMEOUT;if(o?.dryRun){console.log(`[dry-run] Would execute: crewx skill ${n.skill} ${h.join(" ")}`.trim());break}console.log(`Executing skill: ${n.skill} ${h.join(" ")}`.trim());const j=Se({...process.env,CREWX_WORKSPACE:y,CREWX_WORKFLOW_EXEC_ID:r.id,CREWX_WORKFLOW_NODE_ID:t,CREWX_WORKFLOW_ID:r.workflow_id},y);const P=[...j.args,"skill",n.skill,...h];const T=(0,N.resolveWindowsSpawnProgram)({command:j.command,env:j.env,cwd:$.resolved,allowShellFallback:true,packageName:"crewx"});const L=(0,N.materializeWindowsSpawnProgram)(T,P);const A=await ue(L.command,L.argv,{cwd:$.resolved,env:j.env,timeout:G,shell:L.shell??false,windowsHide:L.windowsHide,stdin:E,cancellation:this.processCancellation(e)});I(A);if(A.cancelled){throw(0,d.createWorkflowFailureError)("CANCELLED",`skill_task "${t}" was cancelled`)}if(A.timedOut)throw new Error(`skill_task "${t}" timed out after ${G}ms`);if(A.error)throw A.error;if(A.status!==0){throw new Error(`skill_task "${t}" failed (exit ${A.status}): ${A.stderr.trim()}`)}await Le(n,t,A.stdout,p);if(n.output){console.log(`Result stored in state.${n.output}`)}break}case"shell_task":{const u=process.env.CREWX_WORKFLOW_SHELL==="1";const h=i.metadata?.shell_task_allowed===true;if(!u||!h){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID","shell_task is disabled. Set CREWX_WORKFLOW_SHELL=1 and metadata.shell_task_allowed=true to enable.")}const E=(0,U.validateShellCommand)(n.command);if(!E.ok){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}": ${E.error}`)}const $=(0,U.validateEnvKeys)(n.env);if(!$.ok){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}": ${$.error}`)}const v={...r.state,state:r.state};const y=n.command.map(z=>b(z,v));const G=(0,U.validateShellCommand)(y);if(!G.ok){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}": ${G.error}`)}const j=n.env?Object.fromEntries(Object.entries(n.env).map(([z,te])=>[z,b(String(te),v)])):void 0;const P=(0,U.normalizeCwd)(n.cwd?b(n.cwd,v):void 0,this.projectRoot);if(!P.ok){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}": ${P.error}`)}const T=n.timeout??exports.SKILL_TASK_DEFAULT_TIMEOUT;if(o?.dryRun){console.log(`[dry-run] Would execute: ${y.join(" ")}`);break}console.log(`Executing shell_task: ${y[0]} (${y.length-1} args)`);const L=(0,U.buildShellEnv)(j,process.env);L.CREWX_WORKFLOW_EXEC_ID=r.id;L.CREWX_WORKFLOW_NODE_ID=t;L.CREWX_WORKFLOW_ID=r.workflow_id;const A=n.stdin!==void 0?b(String(n.stdin),v):void 0;const O=await ue(y[0],y.slice(1),{cwd:P.resolved,env:L,timeout:T,shell:false,stdin:A,cancellation:this.processCancellation(e)});I(O);if(O.cancelled){throw(0,d.createWorkflowFailureError)("CANCELLED",`shell_task "${t}" was cancelled`)}if(O.timedOut)throw new Error(`shell_task "${t}" timed out after ${T}ms`);if(O.error)throw O.error;if(O.status!==0){throw new Error(`shell_task "${t}" failed (exit ${O.status}): ${O.stderr.trim()}`)}await Le(n,t,O.stdout,p);if(n.output){console.log(`Result stored in state.${n.output}`)}break}case"branch":{if(!n.condition){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Branch node "${t}" has no condition`)}const u=String(de(String(n.condition),r.state));const h=n.branches;const E=h?.[u]??n.default;if(!E){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Branch "${t}": no matching branch for "${u}" and no default`)}console.log(`Branch "${t}": condition="${n.condition}" \u2192 ${u} \u2192 ${E}`);l.push(t);f=E;break}case"parallel":{const u=n.branches;if(u&&Array.isArray(u)){console.log(`Parallel "${t}": branches = [${u.join(", ")}]`);console.log(` Enter each branch sequentially with: workflow run node <exec-id> <branch-node>`)}break}case"join":{console.log(`Join "${t}": completed_nodes = [${r.completed_nodes.join(", ")}]`);break}case"end":{m="completed";console.log(`End "${t}": workflow execution completed`);break}case"error":{m="failed";S="UNKNOWN";const u={...r.state,state:r.state};_=n.message?b(n.message,u):`Workflow failed at error node "${t}"`;console.log(`Error "${t}": workflow execution failed \u2014 ${_}`);break}case"expression":{if(!n.set||typeof n.set!=="object"){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Expression node "${t}" requires "set" field`)}for(const[u,h]of Object.entries(n.set)){const E=Ee(String(h),r.state);p[u]=String(E);console.log(` ${u} = ${E}`)}console.log(`Expression "${t}": set ${Object.keys(n.set).join(", ")}`);if(n.next){l.push(t);f=n.next}break}case"approval":{console.log(`Approval "${t}": manual intervention required (not auto-executed)`);break}default:console.log(`Unknown node type "${n.type}" for node "${t}"`)}if(n.fail_when&&!o?.dryRun&&(n.type==="agent_task"||n.type==="skill_task"||n.type==="shell_task")){const u=de(String(n.fail_when),{...r.state,...p});if(u===true||u==="true"){const h=n.output?p[n.output]:void 0;if(h&&typeof h==="object"&&!Array.isArray(h)){const E=h.reason;if(typeof E==="string"){const $=(0,N.redactTechnicalMessage)(E);const v=$?.split(/\r\n|\r|\n/u).find(y=>y.trim());if(v)Z=v.trim().slice(0,180)}}throw new Error(`Node "${t}" judged failed by fail_when: ${n.fail_when}`)}}}catch(u){const h=J(u);const E=(0,d.classifyWorkflowFailure)({errorCode:(0,d.getWorkflowFailureCode)(u),process:q});const $=(0,d.buildNodeFailure)({failedNode:t,attempt:g,causeCode:E,exitCode:E==="NODE_EXIT_NONZERO"&&typeof F==="number"?F:null,signal:E==="INTERRUPTED"?H:null,errorOrigin:X,timedOut:q?.timedOut??false});const v=Q(Z??$.causeMessage);await this.atomicUpdateAsync(e,y=>{if(y.status==="cancelled")return;Object.assign(y.state,p);delete y.manual_resume;y.current_node=t;V(y,$,Z??h);y.audit=y.audit??[];y.audit.push(v)});throw u}const M=S?(0,d.buildNodeFailure)({failedNode:t,attempt:g,causeCode:S,exitCode:null,signal:null,errorOrigin:null}):null;const pe=Q(M?.causeMessage);return this.atomicUpdateAsync(e,u=>{if(u.status==="cancelled")return;Object.assign(u.state,p);delete u.manual_resume;for(const h of l){if(!u.completed_nodes.includes(h)){u.completed_nodes.push(h)}}if(c&&c!==t&&!u.completed_nodes.includes(c)){u.completed_nodes.push(c)}u.current_node=f;if(M){V(u,M,_??M.causeMessage)}else{delete u.error;delete u.last_failure;this.clearNodeAttempt(u,t);if(m){u.status=m}else if(u.status!=="completed"){u.status="running"}}if(C){u.tasks=u.tasks??{};u.tasks[t]=C}u.audit=u.audit??[];u.audit.push(pe)})}async claimAutoRunLease(e,t,o){let r=false;const s=await this.atomicUpdateAsync(e,i=>{if(Re(i.status))return;const n=i.auto_run;const c=n?.pid===process.pid&&typeof n.token==="string";if(n&&!c&&De(n.pid)){throw new Error(`Workflow run "${e}" is already owned by another runner process`)}const p=o??(c?n.token:(0,N.generateId)("wfr"));if(!Re(i.status)){i.status="running"}if(!i.current_node)i.current_node=t;i.auto_run={pid:process.pid,token:p,node_id:t,started_at:c&&n.token===p?n.started_at:new Date().toISOString()};delete i.manual_resume;r=true});return r?s.auto_run?.token??null:null}async releaseAutoRunLease(e,t){await this.atomicUpdateAsync(e,o=>{if(o.auto_run?.token===t)delete o.auto_run})}async failAutoConfiguration(e,t,o){return this.atomicUpdateAsync(e,r=>{if(r.status==="cancelled")return;const s=(0,d.buildNodeFailure)({failedNode:t,attempt:(0,d.readNodeAttempt)(r,t),causeCode:"CONFIG_INVALID",exitCode:null,signal:null,errorOrigin:null});V(r,s,o);Y(r,s,"config","auto")})}async reserveAutoRetry(e,t,o,r,s){let i=false;const n=await this.atomicUpdateAsync(e,c=>{if(c.status==="cancelled")return;if(c.status!=="failed")return;if(c.auto_run?.token!==o)return;const p=(0,ce.decideRetry)({policy:r,run:c,nodeId:t});if(!p.retry||p.attempt===void 0)return;const l=new Date().toISOString();c.status="retrying";delete c.error;delete c.manual_resume;c.audit=c.audit??[];c.audit.push({exec_id:c.id,node_id:t,type:"retry",summary:`auto retry ${p.attempt+1}/${r.max+1} in ${s}ms`,started_at:l,ended_at:l,duration_ms:0,exit_code:null,trigger:"auto"});i=true});return{run:n,reserved:i}}async waitForRetry(e,t,o){const r=()=>{const c=this.loadRun(e);if(!c)throw new Error(`Execution not found: ${e}`);if(c.status==="cancelled"){return{run:c,outcome:"cancelled",reason:"run cancelled during retry delay"}}if(c.auto_run?.token!==t){return{run:c,outcome:"paused",reason:"auto-run lease lost during retry delay"}}if(c.status!=="retrying"){return{run:c,outcome:"paused",reason:"run state changed during retry delay"}}return null};let s=Math.max(0,o);while(s>0){const c=Math.min(s,qe);await this.sleep(c);s-=c;const p=r();if(p)return p}const i=r();if(i)return i;const n=this.loadRun(e);if(!n)throw new Error(`Execution not found: ${e}`);return{run:n,outcome:"ready"}}async runNodeWithRetry(e,t,o,r){let s=o;while(true){try{const i=await this.executeNode(e,t,{trigger:"auto"});if(i.status==="cancelled"){return{run:i,runnerToken:s,outcome:"cancelled",reason:i.error}}return{run:i,runnerToken:s,outcome:"success"}}catch(i){const n=this.loadRun(e);if(!n)throw new Error(`Execution not found: ${e}`);if(n.status==="cancelled"){return{run:n,runnerToken:s,outcome:"cancelled",reason:n.error}}if(n.status!=="failed"||!r){return{run:n,runnerToken:s,outcome:n.status==="failed"?"failed":"paused",reason:J(i)}}const c=(0,ce.decideRetry)({policy:r,run:n,nodeId:t});if(!c.retry||c.attempt===void 0){return{run:n,runnerToken:s,outcome:"failed",reason:J(i)}}const p=(0,ce.computeRetryDelayMs)(r,c.attempt);const l=await this.reserveAutoRetry(e,t,s,r,p);if(!l.reserved){const _=l.run;if(_.status==="cancelled"){return{run:_,runnerToken:s,outcome:"cancelled",reason:_.error}}return{run:_,runnerToken:s,outcome:_.status==="failed"?"failed":"paused",reason:J(i)}}const f=await this.waitForRetry(e,s,p);if(f.outcome!=="ready"){return{run:f.run,runnerToken:s,outcome:f.outcome,reason:f.reason}}const m=await this.claimAutoRunLease(e,t,s);if(!m){const _=this.loadRun(e);if(!_)throw new Error(`Execution not found: ${e}`);if(_.status==="cancelled"){return{run:_,runnerToken:s,outcome:"cancelled",reason:_.error}}return{run:_,runnerToken:s,outcome:"paused",reason:"run stopped before retry lease could be reclaimed"}}s=m}}}async runAuto(e){const t=`${this.resolvedDir}::${e}`;if(ke.has(t)){const r=this.loadRun(e);if(!r)throw new Error(`Execution not found: ${e}`);console.log(`runAuto "${e}": already in-flight \u2014 skipping duplicate auto-run (single-flight guard)`);return{run:r,outcome:"paused",reason:"execution already in-flight (single-flight guard)",executed:[]}}ke.add(t);let o;try{const r=this.loadRun(e);if(!r)throw new Error(`Execution not found: ${e}`);if(r.auto_run?.pid===process.pid){o=r.auto_run.token}const s=this.loadWorkflowYaml(r.workflow_file);if(!s){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Failed to load workflow file: ${r.workflow_file}`)}const i=s.workflows[r.workflow_id];if(!i){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Workflow "${r.workflow_id}" not found in ${r.workflow_file}`)}const n=[];if(r.status==="completed"||r.status==="failed"||r.status==="cancelled"){return{run:r,outcome:r.status==="completed"?"completed":r.status==="cancelled"?"cancelled":"failed",reason:`run already ${r.status}`,executed:n}}const c=Object.keys(i.nodes??{})[0];const p=typeof i.max_iterations==="number"&&i.max_iterations>0?i.max_iterations:30;let l=r;while(true){l=this.loadRun(e);if(l.status==="completed")return{run:l,outcome:"completed",executed:n};if(l.status==="failed")return{run:l,outcome:"failed",reason:l.error,executed:n};if(l.status==="cancelled")return{run:l,outcome:"cancelled",reason:l.error,executed:n};const f=l.current_node||c;if(!f){l=await this.atomicUpdateAsync(e,w=>{const k=(0,d.buildNodeFailure)({failedNode:w.current_node||"",attempt:(0,d.readNodeAttempt)(w,w.current_node||""),causeCode:"CONFIG_INVALID",exitCode:null,signal:null,errorOrigin:null});V(w,k,"workflow has no nodes");Y(w,k,"config","auto")});return{run:l,outcome:"failed",reason:l.error,executed:n}}const m=i.nodes[f];if(!m){l=await this.atomicUpdateAsync(e,w=>{w.current_node=f;const k=(0,d.buildNodeFailure)({failedNode:f,attempt:(0,d.readNodeAttempt)(w,f),causeCode:"CONFIG_INVALID",exitCode:null,signal:null,errorOrigin:null});V(w,k,`node "${f}" not found in workflow`);Y(w,k,"config","auto")});return{run:l,outcome:"failed",reason:l.error,executed:n}}const _=(0,ce.parseRetryPolicy)(m);if(!_.ok){l=await this.failAutoConfiguration(e,f,_.reason);if(l.status==="cancelled"){return{run:l,outcome:"cancelled",reason:l.error,executed:n}}return{run:l,outcome:"failed",reason:l.error,executed:n}}if(m.type==="approval"){if(l.current_node!==f){l=await this.atomicUpdateAsync(e,w=>{w.current_node=f})}return{run:l,outcome:"paused",reason:`approval node "${f}" requires manual decision`,executed:n}}if(m.type==="parallel"){if(l.current_node!==f){l=await this.atomicUpdateAsync(e,w=>{w.current_node=f})}return{run:l,outcome:"paused",reason:`parallel auto-run not implemented (node "${f}")`,executed:n}}if(!Ae.has(m.type)){if(l.current_node!==f){l=await this.atomicUpdateAsync(e,w=>{w.current_node=f})}return{run:l,outcome:"paused",reason:`unknown node type "${m.type}" (node "${f}")`,executed:n}}const S=await this.claimAutoRunLease(e,f,o);if(!S){l=this.loadRun(e);return{run:l,outcome:l.status==="cancelled"?"cancelled":"paused",reason:l.status==="cancelled"?l.error:"run stopped before auto-run lease could be claimed",executed:n}}o=S;l=await this.atomicUpdateAsync(e,w=>{if(w.status==="cancelled")return;const k=at(w);k.visitCounts[f]=(k.visitCounts[f]??0)+1;k.iterations+=1;w.state["__engine"]=k;if(k.visitCounts[f]>p||k.iterations>p){const F=(0,d.buildNodeFailure)({failedNode:f,attempt:(0,d.readNodeAttempt)(w,f),causeCode:"CONFIG_INVALID",exitCode:null,signal:null,errorOrigin:null});V(w,F,`auto runner exceeded max_iterations (${p}); possible loop at "${f}"`);Y(w,F,"config","auto")}});if(l.status==="cancelled"){return{run:l,outcome:"cancelled",reason:l.error,executed:n}}if(l.status==="failed"){return{run:l,outcome:"failed",reason:l.error,executed:n}}const C=await this.runNodeWithRetry(e,f,o,_.policy);l=C.run;o=C.runnerToken;if(C.outcome!=="success"){return{run:l,outcome:C.outcome,reason:C.reason,executed:n}}n.push(f);if(l.status==="completed")return{run:l,outcome:"completed",executed:n};if(l.status==="failed")return{run:l,outcome:"failed",reason:l.error,executed:n};if(l.status==="cancelled")return{run:l,outcome:"cancelled",reason:l.error,executed:n};if(!Xe.has(m.type)){const w=m.next;if(!w){return{run:l,outcome:"paused",reason:`node "${f}" has no "next"; stopping`,executed:n}}l=this.moveNode(e,w)}}}finally{if(o){try{await this.releaseAutoRunLease(e,o)}catch{}}ke.delete(t)}}cancelRun(e,t="Cancelled by user"){const o=this.atomicUpdate(e,r=>{if(r.status==="completed"||r.status==="failed"||r.status==="cancelled"){return}const s=new Date;const i=r.auto_run?.node_id||r.current_node||"";const n=(0,d.buildNodeFailure)({failedNode:i,attempt:(0,d.readNodeAttempt)(r,i),causeCode:"CANCELLED",exitCode:null,signal:null,errorOrigin:null});V(r,n,t);r.status="cancelled";delete r.auto_run;delete r.manual_resume;Y(r,n,"cancel","manual",s.toISOString())});if(o.status==="cancelled"){it(this.resolvedDir,e)}return o}reset(e){return this.atomicUpdate(e,t=>{t.state=JSON.parse(JSON.stringify(t.initial_state));t.current_node="";t.completed_nodes=[];t.status="pending";delete t.error;delete t.last_failure;delete t.node_attempts;delete t.auto_run;delete t.manual_resume})}retry(e,t={}){const o=this.loadRun(e);if(!o)throw new Error(`Execution not found: ${e}`);const r=this.loadWorkflowYaml(o.workflow_file);if(!r)throw new Error(`Failed to load workflow file: ${o.workflow_file}`);const s=r.workflows[o.workflow_id];if(!s)throw new Error(`Workflow "${o.workflow_id}" not found`);if(o.status!=="failed"){throw new Error(`Run "${e}" is not failed (status=${o.status??"running"}); only failed runs can be retried`)}if(!Ue(o,s)){throw new Error(`Run "${e}" failed at a non-retryable node "${o.current_node}" (error node); use reset to restart`)}return this.atomicUpdate(e,i=>{if(i.status!=="failed"){throw new Error(`Run "${e}" is not failed (status=${i.status??"pending"}); only failed runs can be retried`)}const n=i.current_node;if(n&&i.last_failure){const p=i.last_failure.attempt;const l=i.node_attempts?.[n];if(typeof p==="number"&&Number.isInteger(p)&&p>0&&(typeof l!=="number"||l<p)){i.node_attempts=i.node_attempts??{};i.node_attempts[n]=p}}i.status="retrying";delete i.error;delete i.manual_resume;i.retry_count=(i.retry_count??0)+1;i.audit=i.audit??[];const c=new Date().toISOString();if(t.autoRun===false){delete i.auto_run;i.manual_resume={node_id:n,requested_at:c}}else{i.auto_run={pid:process.pid,token:(0,N.generateId)("wfr"),node_id:n,started_at:c}}i.audit.push({exec_id:i.id,node_id:n,type:"retry",summary:`manual retry #${i.retry_count}`,started_at:c,ended_at:c,duration_ms:0,exit_code:null,trigger:"manual"})})}listRunIds(){try{return R.readdirSync(this.resolvedDir,{withFileTypes:true}).filter(e=>e.isFile()&&!e.name.startsWith(".")&&e.name.endsWith(".json")).map(e=>e.name.slice(0,-".json".length))}catch(e){if(e.code==="ENOENT")return[];throw e}}listRuns(e){this.ensureRunsDir();const t=R.readdirSync(this.resolvedDir).filter(r=>r.endsWith(".json"));const o=[];for(const r of t){try{const s=r.slice(0,-".json".length);const i=this.loadRun(s);if(!i)continue;if(e?.workflowId&&i.workflow_id!==e.workflowId)continue;o.push(i)}catch{}}o.sort((r,s)=>new Date(s.created_at).getTime()-new Date(r.created_at).getTime());return o}}exports.RunManager=ct;
|