@crewx/workflow 0.3.22-rc.1 → 0.3.22-rc.100
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 +255 -11
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +129 -6
- package/dist/cli.js.map +1 -1
- package/dist/src/engine.d.ts +4 -0
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +54 -1
- package/dist/src/engine.js.map +1 -1
- package/dist/src/mermaid.d.ts.map +1 -1
- package/dist/src/mermaid.js +23 -7
- package/dist/src/mermaid.js.map +1 -1
- package/dist/src/parent-task-sync.d.ts +49 -0
- package/dist/src/parent-task-sync.d.ts.map +1 -0
- package/dist/src/parent-task-sync.js +125 -0
- package/dist/src/parent-task-sync.js.map +1 -0
- package/dist/src/run-manager.d.ts +13 -2
- package/dist/src/run-manager.d.ts.map +1 -1
- package/dist/src/run-manager.js +630 -31
- package/dist/src/run-manager.js.map +1 -1
- package/dist/src/types.d.ts +32 -2
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/output-format.d.ts +12 -0
- package/dist/src/utils/output-format.d.ts.map +1 -1
- package/dist/src/utils/output-format.js +103 -5
- package/dist/src/utils/output-format.js.map +1 -1
- package/dist/src/utils/shell-security.d.ts +18 -0
- package/dist/src/utils/shell-security.d.ts.map +1 -0
- package/dist/src/utils/shell-security.js +138 -0
- package/dist/src/utils/shell-security.js.map +1 -0
- package/dist/workflow-schema.json +46 -1
- package/package.json +26 -4
- package/workflow-schema.json +46 -1
package/dist/src/run-manager.js
CHANGED
|
@@ -33,11 +33,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.RunManager = void 0;
|
|
36
|
+
exports.RunManager = exports.SKILL_TASK_DEFAULT_TIMEOUT = exports.FileLock = void 0;
|
|
37
37
|
exports.parseSetArgs = parseSetArgs;
|
|
38
38
|
exports.readStdin = readStdin;
|
|
39
39
|
exports.interpolateTemplate = interpolateTemplate;
|
|
40
40
|
exports.safeEvaluate = safeEvaluate;
|
|
41
|
+
exports.safeEvaluateBranchCondition = safeEvaluateBranchCondition;
|
|
42
|
+
exports.validateSafeBranchCondition = validateSafeBranchCondition;
|
|
43
|
+
exports.isRetryableFailure = isRetryableFailure;
|
|
41
44
|
const fs = __importStar(require("fs"));
|
|
42
45
|
const path = __importStar(require("path"));
|
|
43
46
|
const yaml = __importStar(require("js-yaml"));
|
|
@@ -45,6 +48,117 @@ const cp = __importStar(require("child_process"));
|
|
|
45
48
|
const sdk_1 = require("@crewx/sdk");
|
|
46
49
|
const file_lock_1 = require("./file-lock");
|
|
47
50
|
const output_format_1 = require("./utils/output-format");
|
|
51
|
+
const shell_security_1 = require("./utils/shell-security");
|
|
52
|
+
var file_lock_2 = require("./file-lock");
|
|
53
|
+
Object.defineProperty(exports, "FileLock", { enumerable: true, get: function () { return file_lock_2.FileLock; } });
|
|
54
|
+
exports.SKILL_TASK_DEFAULT_TIMEOUT = 600000;
|
|
55
|
+
const KNOWN_AUTO_NODE_TYPES = new Set([
|
|
56
|
+
'agent_task', 'skill_task', 'shell_task', 'branch', 'expression', 'join', 'end', 'error',
|
|
57
|
+
]);
|
|
58
|
+
const SELF_ADVANCING_NODE_TYPES = new Set(['branch', 'expression', 'end', 'error']);
|
|
59
|
+
function killProcessGroup(child) {
|
|
60
|
+
try {
|
|
61
|
+
if (process.platform === 'win32') {
|
|
62
|
+
if (child.pid) {
|
|
63
|
+
cp.spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else if (child.pid) {
|
|
67
|
+
try {
|
|
68
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
try {
|
|
72
|
+
child.kill('SIGKILL');
|
|
73
|
+
}
|
|
74
|
+
catch { }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch { }
|
|
79
|
+
}
|
|
80
|
+
async function runProcess(command, argv, opts) {
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
let child;
|
|
83
|
+
try {
|
|
84
|
+
child = cp.spawn(command, argv, {
|
|
85
|
+
cwd: opts.cwd,
|
|
86
|
+
env: opts.env,
|
|
87
|
+
stdio: [opts.stdin !== undefined ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
|
88
|
+
shell: opts.shell ?? false,
|
|
89
|
+
detached: process.platform !== 'win32',
|
|
90
|
+
windowsHide: opts.windowsHide ?? true,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
resolve({ status: null, stdout: '', stderr: '', timedOut: false, error: err });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
let stdout = '';
|
|
98
|
+
let stderr = '';
|
|
99
|
+
let settled = false;
|
|
100
|
+
const finish = (result) => {
|
|
101
|
+
if (settled)
|
|
102
|
+
return;
|
|
103
|
+
settled = true;
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
resolve(result);
|
|
106
|
+
};
|
|
107
|
+
const timer = setTimeout(() => {
|
|
108
|
+
killProcessGroup(child);
|
|
109
|
+
finish({ status: null, stdout, stderr, timedOut: true });
|
|
110
|
+
}, opts.timeout);
|
|
111
|
+
if (typeof timer.unref === 'function')
|
|
112
|
+
timer.unref();
|
|
113
|
+
child.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
114
|
+
child.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
115
|
+
if (opts.stdin !== undefined) {
|
|
116
|
+
child.stdin?.end(opts.stdin);
|
|
117
|
+
}
|
|
118
|
+
child.on('error', (err) => finish({ status: null, stdout, stderr, timedOut: false, error: err }));
|
|
119
|
+
child.on('close', (code) => finish({ status: code, stdout, stderr, timedOut: false }));
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function maskArg(arg) {
|
|
123
|
+
return arg.replace(/^(--?[\w-]*(?:token|secret|password|passwd|key|auth)[\w-]*)=(.+)$/i, '$1=***');
|
|
124
|
+
}
|
|
125
|
+
function summarizeNode(node) {
|
|
126
|
+
switch (node.type) {
|
|
127
|
+
case 'agent_task':
|
|
128
|
+
return `@${node.agent ?? '?'} (${node.mode === 'query' ? 'q' : 'x'})`;
|
|
129
|
+
case 'skill_task':
|
|
130
|
+
return `skill ${node.skill ?? '?'} ${(node.args ?? []).map(maskArg).join(' ')}`.trim();
|
|
131
|
+
case 'shell_task':
|
|
132
|
+
return (node.command ?? []).map(maskArg).join(' ');
|
|
133
|
+
case 'branch':
|
|
134
|
+
return `branch: ${node.condition ?? ''}`;
|
|
135
|
+
case 'expression':
|
|
136
|
+
return `set ${Object.keys(node.set ?? {}).join(', ')}`;
|
|
137
|
+
default:
|
|
138
|
+
return node.type;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function storeNodeOutput(nodeSpec, nodeId, stdout, stateUpdates) {
|
|
142
|
+
if (!nodeSpec.output)
|
|
143
|
+
return;
|
|
144
|
+
const trimmed = stdout.trim();
|
|
145
|
+
if (nodeSpec.output_format === 'json') {
|
|
146
|
+
let parsed;
|
|
147
|
+
try {
|
|
148
|
+
parsed = JSON.parse((0, output_format_1.extractJson)(trimmed));
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
throw new Error(`Node "${nodeId}" output_format=json parse failed: ${e.message}`);
|
|
152
|
+
}
|
|
153
|
+
if (nodeSpec.output_schema) {
|
|
154
|
+
await (0, output_format_1.validateJsonSchema)(parsed, nodeSpec.output_schema);
|
|
155
|
+
}
|
|
156
|
+
stateUpdates[nodeSpec.output] = parsed;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
stateUpdates[nodeSpec.output] = trimmed;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
48
162
|
function parseValue(raw) {
|
|
49
163
|
try {
|
|
50
164
|
return JSON.parse(raw);
|
|
@@ -104,6 +218,7 @@ const FORBIDDEN_IDENTS = new Set([
|
|
|
104
218
|
'require', 'import', 'process', 'global', 'globalThis',
|
|
105
219
|
'Function', 'eval', '__proto__', 'constructor', 'prototype',
|
|
106
220
|
]);
|
|
221
|
+
const FORBIDDEN_STATE_PATH_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
107
222
|
function tokenize(expr) {
|
|
108
223
|
const tokens = [];
|
|
109
224
|
let i = 0;
|
|
@@ -337,6 +452,8 @@ class ExprParser {
|
|
|
337
452
|
const val = this.state[key];
|
|
338
453
|
if (val === undefined || val === null)
|
|
339
454
|
return 0;
|
|
455
|
+
if (typeof val === 'boolean' || typeof val === 'number')
|
|
456
|
+
return val;
|
|
340
457
|
const strVal = String(val);
|
|
341
458
|
if (strVal === '')
|
|
342
459
|
return '';
|
|
@@ -359,11 +476,97 @@ function safeEvaluate(expr, state) {
|
|
|
359
476
|
const parser = new ExprParser(tokens, state);
|
|
360
477
|
return parser.parse();
|
|
361
478
|
}
|
|
479
|
+
function getStatePathValue(state, pathExpr) {
|
|
480
|
+
const parts = pathExpr.split('.');
|
|
481
|
+
let value = state;
|
|
482
|
+
for (const part of parts) {
|
|
483
|
+
if (FORBIDDEN_STATE_PATH_SEGMENTS.has(part)) {
|
|
484
|
+
throw new Error(`Forbidden state path segment in expression: "${part}"`);
|
|
485
|
+
}
|
|
486
|
+
if (!part || value === null || value === undefined || typeof value !== 'object') {
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
value = value[part];
|
|
490
|
+
}
|
|
491
|
+
return value;
|
|
492
|
+
}
|
|
493
|
+
function unescapeQuotedLiteral(value) {
|
|
494
|
+
return value
|
|
495
|
+
.replace(/\\'/g, "'")
|
|
496
|
+
.replace(/\\"/g, '"')
|
|
497
|
+
.replace(/\\\\/g, '\\');
|
|
498
|
+
}
|
|
499
|
+
function normalizeBranchCondition(expr, state) {
|
|
500
|
+
const scope = {};
|
|
501
|
+
let counter = 0;
|
|
502
|
+
const bind = (value) => {
|
|
503
|
+
const key = `__state_${counter++}`;
|
|
504
|
+
scope[key] = value;
|
|
505
|
+
return key;
|
|
506
|
+
};
|
|
507
|
+
let normalized = expr
|
|
508
|
+
.replace(/!==/g, '!=')
|
|
509
|
+
.replace(/===/g, '==');
|
|
510
|
+
const replaceIncludes = (pattern) => {
|
|
511
|
+
normalized = normalized.replace(pattern, (_match, statePath, literal) => {
|
|
512
|
+
const value = getStatePathValue(state, statePath);
|
|
513
|
+
return bind(String(value ?? '').includes(unescapeQuotedLiteral(literal)));
|
|
514
|
+
});
|
|
515
|
+
};
|
|
516
|
+
replaceIncludes(/\bstate\.([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\.includes\(\s*'((?:\\.|[^'\\])*)'\s*\)/g);
|
|
517
|
+
replaceIncludes(/\bstate\.([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\.includes\(\s*"((?:\\.|[^"\\])*)"\s*\)/g);
|
|
518
|
+
normalized = normalized.replace(/\bstate\.([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)/g, (_match, statePath) => bind(getStatePathValue(state, statePath)));
|
|
519
|
+
normalized = normalized.replace(/\bNumber\(\s*([A-Za-z_]\w*)\s*\)/g, '$1');
|
|
520
|
+
normalized = normalized.replace(/\bparseInt\(\s*([A-Za-z_]\w*(?:\s*\|\|\s*(?:'[^']*'|"[^"]*"))?)\s*\)/g, '($1)');
|
|
521
|
+
return { expr: normalized, scope };
|
|
522
|
+
}
|
|
523
|
+
function safeEvaluateBranchCondition(expr, state) {
|
|
524
|
+
const normalized = normalizeBranchCondition(expr, state);
|
|
525
|
+
return safeEvaluate(normalized.expr, { ...state, ...normalized.scope });
|
|
526
|
+
}
|
|
527
|
+
function validateSafeBranchCondition(expr) {
|
|
528
|
+
safeEvaluateBranchCondition(expr, {});
|
|
529
|
+
}
|
|
530
|
+
function readRunEngineState(run) {
|
|
531
|
+
const raw = run.state['__engine'];
|
|
532
|
+
if (!raw || typeof raw !== 'object')
|
|
533
|
+
return { visitCounts: {}, iterations: 0 };
|
|
534
|
+
const record = raw;
|
|
535
|
+
const rawCounts = record['visitCounts'];
|
|
536
|
+
const visitCounts = {};
|
|
537
|
+
if (rawCounts && typeof rawCounts === 'object') {
|
|
538
|
+
for (const [key, value] of Object.entries(rawCounts)) {
|
|
539
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
540
|
+
visitCounts[key] = value;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const iterations = typeof record['iterations'] === 'number' && Number.isFinite(record['iterations'])
|
|
544
|
+
? record['iterations']
|
|
545
|
+
: 0;
|
|
546
|
+
return { visitCounts, iterations };
|
|
547
|
+
}
|
|
548
|
+
const inFlightAutoRuns = new Set();
|
|
549
|
+
function isRetryableFailure(run, spec) {
|
|
550
|
+
if (run.status !== 'failed')
|
|
551
|
+
return false;
|
|
552
|
+
if (!run.current_node)
|
|
553
|
+
return false;
|
|
554
|
+
const node = spec.nodes?.[run.current_node];
|
|
555
|
+
if (!node)
|
|
556
|
+
return false;
|
|
557
|
+
return node.type !== 'error';
|
|
558
|
+
}
|
|
362
559
|
class RunManager {
|
|
363
|
-
constructor(runsDir = '.crewx/workflow-runs') {
|
|
560
|
+
constructor(runsDir = '.crewx/workflow-runs', projectRoot) {
|
|
364
561
|
this.runsDir = runsDir;
|
|
365
|
-
const base = process.env.CREWX_WORKSPACE || process.cwd();
|
|
562
|
+
const base = projectRoot || process.env.CREWX_WORKSPACE || process.cwd();
|
|
366
563
|
this.resolvedDir = path.resolve(base, this.runsDir);
|
|
564
|
+
this.projectRoot = path.resolve(base);
|
|
565
|
+
const hasConfig = fs.existsSync(path.join(this.projectRoot, 'crewx.yaml'))
|
|
566
|
+
|| fs.existsSync(path.join(this.projectRoot, 'crewx.yml'));
|
|
567
|
+
if (!hasConfig) {
|
|
568
|
+
console.warn(`[RunManager] projectRoot "${this.projectRoot}" has no crewx.yaml/crewx.yml — agent_task nodes may fail to resolve agents.`);
|
|
569
|
+
}
|
|
367
570
|
}
|
|
368
571
|
ensureRunsDir() {
|
|
369
572
|
if (!fs.existsSync(this.resolvedDir)) {
|
|
@@ -438,6 +641,12 @@ class RunManager {
|
|
|
438
641
|
return null;
|
|
439
642
|
return doc;
|
|
440
643
|
}
|
|
644
|
+
getNodeType(workflowFile, workflowId, nodeId) {
|
|
645
|
+
const doc = this.loadWorkflowYaml(workflowFile);
|
|
646
|
+
const spec = doc?.workflows[workflowId];
|
|
647
|
+
const node = spec?.nodes?.[nodeId];
|
|
648
|
+
return typeof node?.type === 'string' ? node.type : null;
|
|
649
|
+
}
|
|
441
650
|
start(yamlPath, workflowId, overrides) {
|
|
442
651
|
const doc = this.loadWorkflowYaml(yamlPath);
|
|
443
652
|
if (!doc) {
|
|
@@ -521,26 +730,67 @@ class RunManager {
|
|
|
521
730
|
const nodesToComplete = [];
|
|
522
731
|
let currentNode = nodeId;
|
|
523
732
|
let status;
|
|
733
|
+
let failureMessage;
|
|
524
734
|
let nodeTaskId;
|
|
735
|
+
const trigger = opts?.trigger ?? 'manual';
|
|
736
|
+
const startedAt = new Date();
|
|
737
|
+
let nodeStdout = '';
|
|
738
|
+
let nodeStderr = '';
|
|
739
|
+
let nodeExitCode = null;
|
|
740
|
+
const buildAudit = (error) => {
|
|
741
|
+
const endedAt = new Date();
|
|
742
|
+
return {
|
|
743
|
+
exec_id: snapshot.id,
|
|
744
|
+
node_id: nodeId,
|
|
745
|
+
type: nodeSpec.type,
|
|
746
|
+
summary: summarizeNode(nodeSpec),
|
|
747
|
+
started_at: startedAt.toISOString(),
|
|
748
|
+
ended_at: endedAt.toISOString(),
|
|
749
|
+
duration_ms: endedAt.getTime() - startedAt.getTime(),
|
|
750
|
+
exit_code: nodeExitCode,
|
|
751
|
+
...(error ? { error } : {}),
|
|
752
|
+
trigger,
|
|
753
|
+
};
|
|
754
|
+
};
|
|
525
755
|
try {
|
|
526
756
|
switch (nodeSpec.type) {
|
|
527
757
|
case 'agent_task': {
|
|
528
758
|
if (!nodeSpec.agent) {
|
|
529
759
|
throw new Error(`Node "${nodeId}" is agent_task but has no agent defined`);
|
|
530
760
|
}
|
|
761
|
+
const agentCtx = { ...snapshot.state, state: snapshot.state };
|
|
762
|
+
const resolvedAgent = interpolateTemplate(nodeSpec.agent, agentCtx).trim();
|
|
763
|
+
if (!resolvedAgent) {
|
|
764
|
+
throw new Error(`Node "${nodeId}": assignee not resolved (agent field "${nodeSpec.agent}" interpolated to empty)`);
|
|
765
|
+
}
|
|
766
|
+
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, agentCtx) : undefined, this.projectRoot);
|
|
767
|
+
if (!cwdCheck.ok)
|
|
768
|
+
throw new Error(`Node "${nodeId}": ${cwdCheck.error}`);
|
|
769
|
+
const nodeCwd = cwdCheck.resolved;
|
|
770
|
+
const rootCheck = (0, shell_security_1.normalizeCwd)(undefined, this.projectRoot);
|
|
771
|
+
if (!rootCheck.ok)
|
|
772
|
+
throw new Error(`Node "${nodeId}": ${rootCheck.error}`);
|
|
773
|
+
const workspaceRoot = rootCheck.resolved;
|
|
531
774
|
const input = nodeSpec.input
|
|
532
|
-
? interpolateTemplate(nodeSpec.input,
|
|
775
|
+
? interpolateTemplate(nodeSpec.input, agentCtx)
|
|
533
776
|
: '';
|
|
777
|
+
const preamble = process.env.CREWX_WORKFLOW_PREAMBLE === 'off'
|
|
778
|
+
? ''
|
|
779
|
+
: (0, output_format_1.buildWorkflowPreamble)({
|
|
780
|
+
workflowId: snapshot.workflow_id,
|
|
781
|
+
nodeId,
|
|
782
|
+
execId: snapshot.id,
|
|
783
|
+
});
|
|
534
784
|
const finalInput = nodeSpec.output_format === 'json'
|
|
535
|
-
? input + (0, output_format_1.buildJsonOutputSuffix)(nodeSpec.output_schema, nodeSpec.output_strict)
|
|
536
|
-
: input;
|
|
785
|
+
? preamble + input + (0, output_format_1.buildJsonOutputSuffix)(nodeSpec.output_schema, nodeSpec.output_strict)
|
|
786
|
+
: preamble + input;
|
|
537
787
|
const mode = nodeSpec.mode === 'query' ? 'q' : 'x';
|
|
538
|
-
const prompt = `@${
|
|
788
|
+
const prompt = `@${resolvedAgent} ${finalInput}`;
|
|
539
789
|
if (opts?.dryRun) {
|
|
540
|
-
console.log(`[dry-run] Would execute: crewx ${mode} "@${
|
|
790
|
+
console.log(`[dry-run] Would execute: crewx ${mode} "@${resolvedAgent} ..."`);
|
|
541
791
|
break;
|
|
542
792
|
}
|
|
543
|
-
console.log(`Executing: @${
|
|
793
|
+
console.log(`Executing: @${resolvedAgent} (${mode} mode)`);
|
|
544
794
|
const crewxCli = process.env.CREWX_CLI || 'npx crewx';
|
|
545
795
|
const cliParts = crewxCli.split(/\s+/);
|
|
546
796
|
const bin = cliParts[0];
|
|
@@ -564,30 +814,39 @@ class RunManager {
|
|
|
564
814
|
packageName: 'crewx',
|
|
565
815
|
});
|
|
566
816
|
const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, wfArgs);
|
|
567
|
-
const result =
|
|
568
|
-
|
|
569
|
-
timeout: nodeTimeout,
|
|
570
|
-
cwd: process.cwd(),
|
|
571
|
-
stdio: ['inherit', 'pipe', 'pipe'],
|
|
572
|
-
shell: invocation.shell ?? false,
|
|
573
|
-
windowsHide: invocation.windowsHide,
|
|
817
|
+
const result = await runProcess(invocation.command, invocation.argv, {
|
|
818
|
+
cwd: nodeCwd,
|
|
574
819
|
env: {
|
|
575
820
|
...process.env,
|
|
821
|
+
CREWX_WORKSPACE: workspaceRoot,
|
|
576
822
|
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
577
823
|
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
578
824
|
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
579
825
|
},
|
|
826
|
+
timeout: nodeTimeout,
|
|
827
|
+
shell: invocation.shell ?? false,
|
|
828
|
+
windowsHide: invocation.windowsHide,
|
|
580
829
|
});
|
|
830
|
+
nodeStdout = result.stdout;
|
|
831
|
+
nodeStderr = result.stderr;
|
|
832
|
+
nodeExitCode = result.status;
|
|
581
833
|
if (result.error) {
|
|
582
834
|
throw result.error;
|
|
583
835
|
}
|
|
584
|
-
|
|
836
|
+
if (result.timedOut) {
|
|
837
|
+
throw new Error(`Agent execution timed out after ${nodeTimeout}ms`);
|
|
838
|
+
}
|
|
839
|
+
const taskIdMatch = result.stderr.match(/crewx kill (tsk_\w+)/);
|
|
585
840
|
nodeTaskId = taskIdMatch?.[1];
|
|
586
841
|
if (result.status !== 0) {
|
|
587
|
-
const stderr = result.stderr
|
|
842
|
+
const stderr = result.stderr.trim();
|
|
588
843
|
throw new Error(`Agent execution failed (exit ${result.status}): ${stderr}`);
|
|
589
844
|
}
|
|
590
|
-
const output = result.stdout
|
|
845
|
+
const output = result.stdout.trim();
|
|
846
|
+
const signal = (0, output_format_1.detectWorkflowSignal)(output);
|
|
847
|
+
if (signal) {
|
|
848
|
+
throw new Error(`Node "${nodeId}" agent signaled failure: ${signal.reason ?? '(no reason given)'}`);
|
|
849
|
+
}
|
|
591
850
|
if (nodeSpec.output) {
|
|
592
851
|
if (nodeSpec.output_format === 'json') {
|
|
593
852
|
const retryMax = nodeSpec.output_retry ?? 1;
|
|
@@ -600,33 +859,41 @@ class RunManager {
|
|
|
600
859
|
retryInput = finalInput +
|
|
601
860
|
`\n\n## ⚠️ PREVIOUS OUTPUT FAILED JSON.parse: ${lastError.message}\n` +
|
|
602
861
|
`Return STRICT JSON ONLY. First char "{", last char "}". No prose, no fences.`;
|
|
603
|
-
const retryPrompt = `@${
|
|
862
|
+
const retryPrompt = `@${resolvedAgent} ${retryInput}`;
|
|
604
863
|
const retryArgs = [...baseArgs, mode, retryPrompt];
|
|
605
864
|
if (process.env.CREWX_WORKFLOW_THREAD === 'on') {
|
|
606
865
|
retryArgs.push(`--thread=workflow:${snapshot.id}`);
|
|
607
866
|
}
|
|
608
867
|
retryArgs.push('--metadata', JSON.stringify(wfMetadata));
|
|
609
868
|
const retryInvocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, retryArgs);
|
|
610
|
-
const retryResult =
|
|
611
|
-
|
|
612
|
-
timeout: nodeTimeout,
|
|
613
|
-
cwd: process.cwd(),
|
|
614
|
-
stdio: ['inherit', 'pipe', 'pipe'],
|
|
615
|
-
shell: retryInvocation.shell ?? false,
|
|
616
|
-
windowsHide: retryInvocation.windowsHide,
|
|
869
|
+
const retryResult = await runProcess(retryInvocation.command, retryInvocation.argv, {
|
|
870
|
+
cwd: nodeCwd,
|
|
617
871
|
env: {
|
|
618
872
|
...process.env,
|
|
873
|
+
CREWX_WORKSPACE: workspaceRoot,
|
|
619
874
|
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
620
875
|
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
621
876
|
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
622
877
|
},
|
|
878
|
+
timeout: nodeTimeout,
|
|
879
|
+
shell: retryInvocation.shell ?? false,
|
|
880
|
+
windowsHide: retryInvocation.windowsHide,
|
|
623
881
|
});
|
|
882
|
+
nodeStdout = retryResult.stdout;
|
|
883
|
+
nodeStderr = retryResult.stderr;
|
|
884
|
+
nodeExitCode = retryResult.status;
|
|
624
885
|
if (retryResult.error)
|
|
625
886
|
throw retryResult.error;
|
|
887
|
+
if (retryResult.timedOut)
|
|
888
|
+
throw new Error(`Agent retry timed out after ${nodeTimeout}ms`);
|
|
626
889
|
if (retryResult.status !== 0) {
|
|
627
|
-
throw new Error(`Agent retry failed (exit ${retryResult.status}): ${retryResult.stderr
|
|
890
|
+
throw new Error(`Agent retry failed (exit ${retryResult.status}): ${retryResult.stderr.trim()}`);
|
|
891
|
+
}
|
|
892
|
+
attemptOutput = retryResult.stdout.trim();
|
|
893
|
+
const retrySignal = (0, output_format_1.detectWorkflowSignal)(attemptOutput);
|
|
894
|
+
if (retrySignal) {
|
|
895
|
+
throw new Error(`Node "${nodeId}" agent signaled failure: ${retrySignal.reason ?? '(no reason given)'}`);
|
|
628
896
|
}
|
|
629
|
-
attemptOutput = retryResult.stdout?.trim() ?? '';
|
|
630
897
|
}
|
|
631
898
|
try {
|
|
632
899
|
parsed = JSON.parse((0, output_format_1.extractJson)(attemptOutput));
|
|
@@ -652,12 +919,136 @@ class RunManager {
|
|
|
652
919
|
}
|
|
653
920
|
break;
|
|
654
921
|
}
|
|
922
|
+
case 'skill_task': {
|
|
923
|
+
if (!nodeSpec.skill) {
|
|
924
|
+
throw new Error(`Node "${nodeId}" is skill_task but has no skill defined`);
|
|
925
|
+
}
|
|
926
|
+
if (!shell_security_1.SKILL_NAME_RE.test(nodeSpec.skill)) {
|
|
927
|
+
throw new Error(`Node "${nodeId}" has invalid skill name "${nodeSpec.skill}" (must match ^[a-z0-9][a-z0-9-]*$)`);
|
|
928
|
+
}
|
|
929
|
+
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
930
|
+
const skillArgs = (nodeSpec.args ?? []).map((a) => interpolateTemplate(String(a), ctx));
|
|
931
|
+
const skillStdin = nodeSpec.stdin !== undefined
|
|
932
|
+
? interpolateTemplate(String(nodeSpec.stdin), ctx)
|
|
933
|
+
: undefined;
|
|
934
|
+
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, ctx) : undefined, this.projectRoot);
|
|
935
|
+
if (!cwdCheck.ok)
|
|
936
|
+
throw new Error(`Node "${nodeId}": ${cwdCheck.error}`);
|
|
937
|
+
const skillRootCheck = (0, shell_security_1.normalizeCwd)(undefined, this.projectRoot);
|
|
938
|
+
if (!skillRootCheck.ok)
|
|
939
|
+
throw new Error(`Node "${nodeId}": ${skillRootCheck.error}`);
|
|
940
|
+
const skillWorkspaceRoot = skillRootCheck.resolved;
|
|
941
|
+
const timeout = nodeSpec.timeout ?? exports.SKILL_TASK_DEFAULT_TIMEOUT;
|
|
942
|
+
if (opts?.dryRun) {
|
|
943
|
+
console.log(`[dry-run] Would execute: crewx skill ${nodeSpec.skill} ${skillArgs.join(' ')}`.trim());
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
console.log(`Executing skill: ${nodeSpec.skill} ${skillArgs.join(' ')}`.trim());
|
|
947
|
+
const crewxCli = process.env.CREWX_CLI || 'npx crewx';
|
|
948
|
+
const cliParts = crewxCli.split(/\s+/);
|
|
949
|
+
const bin = cliParts[0];
|
|
950
|
+
const baseArgs = cliParts.slice(1);
|
|
951
|
+
const skillArgv = [...baseArgs, 'skill', nodeSpec.skill, ...skillArgs];
|
|
952
|
+
const program = (0, sdk_1.resolveWindowsSpawnProgram)({
|
|
953
|
+
command: bin,
|
|
954
|
+
allowShellFallback: true,
|
|
955
|
+
packageName: 'crewx',
|
|
956
|
+
});
|
|
957
|
+
const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, skillArgv);
|
|
958
|
+
const result = await runProcess(invocation.command, invocation.argv, {
|
|
959
|
+
cwd: cwdCheck.resolved,
|
|
960
|
+
env: {
|
|
961
|
+
...process.env,
|
|
962
|
+
CREWX_WORKSPACE: skillWorkspaceRoot,
|
|
963
|
+
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
964
|
+
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
965
|
+
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
966
|
+
},
|
|
967
|
+
timeout,
|
|
968
|
+
shell: invocation.shell ?? false,
|
|
969
|
+
windowsHide: invocation.windowsHide,
|
|
970
|
+
stdin: skillStdin,
|
|
971
|
+
});
|
|
972
|
+
nodeStdout = result.stdout;
|
|
973
|
+
nodeStderr = result.stderr;
|
|
974
|
+
nodeExitCode = result.status;
|
|
975
|
+
if (result.error)
|
|
976
|
+
throw result.error;
|
|
977
|
+
if (result.timedOut)
|
|
978
|
+
throw new Error(`skill_task "${nodeId}" timed out after ${timeout}ms`);
|
|
979
|
+
if (result.status !== 0) {
|
|
980
|
+
throw new Error(`skill_task "${nodeId}" failed (exit ${result.status}): ${result.stderr.trim()}`);
|
|
981
|
+
}
|
|
982
|
+
await storeNodeOutput(nodeSpec, nodeId, result.stdout, stateUpdates);
|
|
983
|
+
if (nodeSpec.output) {
|
|
984
|
+
console.log(`Result stored in state.${nodeSpec.output}`);
|
|
985
|
+
}
|
|
986
|
+
break;
|
|
987
|
+
}
|
|
988
|
+
case 'shell_task': {
|
|
989
|
+
const shellEnvEnabled = process.env.CREWX_WORKFLOW_SHELL === '1';
|
|
990
|
+
const shellMetaAllowed = spec.metadata?.shell_task_allowed === true;
|
|
991
|
+
if (!shellEnvEnabled || !shellMetaAllowed) {
|
|
992
|
+
throw new Error('shell_task is disabled. Set CREWX_WORKFLOW_SHELL=1 and metadata.shell_task_allowed=true to enable.');
|
|
993
|
+
}
|
|
994
|
+
const cmdCheck = (0, shell_security_1.validateShellCommand)(nodeSpec.command);
|
|
995
|
+
if (!cmdCheck.ok)
|
|
996
|
+
throw new Error(`Node "${nodeId}": ${cmdCheck.error}`);
|
|
997
|
+
const envCheck = (0, shell_security_1.validateEnvKeys)(nodeSpec.env);
|
|
998
|
+
if (!envCheck.ok)
|
|
999
|
+
throw new Error(`Node "${nodeId}": ${envCheck.error}`);
|
|
1000
|
+
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
1001
|
+
const command = nodeSpec.command.map((c) => interpolateTemplate(c, ctx));
|
|
1002
|
+
const postCheck = (0, shell_security_1.validateShellCommand)(command);
|
|
1003
|
+
if (!postCheck.ok)
|
|
1004
|
+
throw new Error(`Node "${nodeId}": ${postCheck.error}`);
|
|
1005
|
+
const interpolatedEnv = nodeSpec.env
|
|
1006
|
+
? Object.fromEntries(Object.entries(nodeSpec.env).map(([k, v]) => [k, interpolateTemplate(String(v), ctx)]))
|
|
1007
|
+
: undefined;
|
|
1008
|
+
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, ctx) : undefined, this.projectRoot);
|
|
1009
|
+
if (!cwdCheck.ok)
|
|
1010
|
+
throw new Error(`Node "${nodeId}": ${cwdCheck.error}`);
|
|
1011
|
+
const timeout = nodeSpec.timeout ?? exports.SKILL_TASK_DEFAULT_TIMEOUT;
|
|
1012
|
+
if (opts?.dryRun) {
|
|
1013
|
+
console.log(`[dry-run] Would execute: ${command.join(' ')}`);
|
|
1014
|
+
break;
|
|
1015
|
+
}
|
|
1016
|
+
console.log(`Executing shell_task: ${command[0]} (${command.length - 1} args)`);
|
|
1017
|
+
const childEnv = (0, shell_security_1.buildShellEnv)(interpolatedEnv, process.env);
|
|
1018
|
+
childEnv.CREWX_WORKFLOW_EXEC_ID = snapshot.id;
|
|
1019
|
+
childEnv.CREWX_WORKFLOW_NODE_ID = nodeId;
|
|
1020
|
+
childEnv.CREWX_WORKFLOW_ID = snapshot.workflow_id;
|
|
1021
|
+
const shellStdin = nodeSpec.stdin !== undefined
|
|
1022
|
+
? interpolateTemplate(String(nodeSpec.stdin), ctx)
|
|
1023
|
+
: undefined;
|
|
1024
|
+
const result = await runProcess(command[0], command.slice(1), {
|
|
1025
|
+
cwd: cwdCheck.resolved,
|
|
1026
|
+
env: childEnv,
|
|
1027
|
+
timeout,
|
|
1028
|
+
shell: false,
|
|
1029
|
+
stdin: shellStdin,
|
|
1030
|
+
});
|
|
1031
|
+
nodeStdout = result.stdout;
|
|
1032
|
+
nodeStderr = result.stderr;
|
|
1033
|
+
nodeExitCode = result.status;
|
|
1034
|
+
if (result.error)
|
|
1035
|
+
throw result.error;
|
|
1036
|
+
if (result.timedOut)
|
|
1037
|
+
throw new Error(`shell_task "${nodeId}" timed out after ${timeout}ms`);
|
|
1038
|
+
if (result.status !== 0) {
|
|
1039
|
+
throw new Error(`shell_task "${nodeId}" failed (exit ${result.status}): ${result.stderr.trim()}`);
|
|
1040
|
+
}
|
|
1041
|
+
await storeNodeOutput(nodeSpec, nodeId, result.stdout, stateUpdates);
|
|
1042
|
+
if (nodeSpec.output) {
|
|
1043
|
+
console.log(`Result stored in state.${nodeSpec.output}`);
|
|
1044
|
+
}
|
|
1045
|
+
break;
|
|
1046
|
+
}
|
|
655
1047
|
case 'branch': {
|
|
656
1048
|
if (!nodeSpec.condition) {
|
|
657
1049
|
throw new Error(`Branch node "${nodeId}" has no condition`);
|
|
658
1050
|
}
|
|
659
|
-
const
|
|
660
|
-
const condResult = String(evalFn(snapshot.state));
|
|
1051
|
+
const condResult = String(safeEvaluateBranchCondition(String(nodeSpec.condition), snapshot.state));
|
|
661
1052
|
const branches = nodeSpec.branches;
|
|
662
1053
|
const targetNode = branches?.[condResult] ?? nodeSpec.default;
|
|
663
1054
|
if (!targetNode) {
|
|
@@ -685,6 +1076,15 @@ class RunManager {
|
|
|
685
1076
|
console.log(`End "${nodeId}": workflow execution completed`);
|
|
686
1077
|
break;
|
|
687
1078
|
}
|
|
1079
|
+
case 'error': {
|
|
1080
|
+
status = 'failed';
|
|
1081
|
+
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
1082
|
+
failureMessage = nodeSpec.message
|
|
1083
|
+
? interpolateTemplate(nodeSpec.message, ctx)
|
|
1084
|
+
: `Workflow failed at error node "${nodeId}"`;
|
|
1085
|
+
console.log(`Error "${nodeId}": workflow execution failed — ${failureMessage}`);
|
|
1086
|
+
break;
|
|
1087
|
+
}
|
|
688
1088
|
case 'expression': {
|
|
689
1089
|
if (!nodeSpec.set || typeof nodeSpec.set !== 'object') {
|
|
690
1090
|
throw new Error(`Expression node "${nodeId}" requires "set" field`);
|
|
@@ -708,15 +1108,38 @@ class RunManager {
|
|
|
708
1108
|
default:
|
|
709
1109
|
console.log(`Unknown node type "${nodeSpec.type}" for node "${nodeId}"`);
|
|
710
1110
|
}
|
|
1111
|
+
if (nodeSpec.fail_when &&
|
|
1112
|
+
!opts?.dryRun &&
|
|
1113
|
+
(nodeSpec.type === 'agent_task' || nodeSpec.type === 'skill_task' || nodeSpec.type === 'shell_task')) {
|
|
1114
|
+
const judged = safeEvaluateBranchCondition(String(nodeSpec.fail_when), { ...snapshot.state, ...stateUpdates });
|
|
1115
|
+
if (judged === true || judged === 'true') {
|
|
1116
|
+
throw new Error(`Node "${nodeId}" judged failed by fail_when: ${nodeSpec.fail_when}`);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
711
1119
|
}
|
|
712
1120
|
catch (e) {
|
|
1121
|
+
const errMessage = e.message;
|
|
1122
|
+
const partial = [
|
|
1123
|
+
nodeStdout.trim() ? `stdout: ${nodeStdout.trim().slice(-500)}` : '',
|
|
1124
|
+
nodeStderr.trim() ? `stderr: ${nodeStderr.trim().slice(-500)}` : '',
|
|
1125
|
+
].filter(Boolean).join(' | ');
|
|
1126
|
+
const auditEntry = buildAudit(partial ? `${errMessage} (${partial})` : errMessage);
|
|
713
1127
|
await this.atomicUpdateAsync(execId, (run) => {
|
|
1128
|
+
if (run.status === 'cancelled')
|
|
1129
|
+
return;
|
|
1130
|
+
Object.assign(run.state, stateUpdates);
|
|
714
1131
|
run.current_node = nodeId;
|
|
715
1132
|
run.status = 'failed';
|
|
1133
|
+
run.error = errMessage;
|
|
1134
|
+
run.audit = run.audit ?? [];
|
|
1135
|
+
run.audit.push(auditEntry);
|
|
716
1136
|
});
|
|
717
1137
|
throw e;
|
|
718
1138
|
}
|
|
1139
|
+
const successAudit = buildAudit();
|
|
719
1140
|
return this.atomicUpdateAsync(execId, (run) => {
|
|
1141
|
+
if (run.status === 'cancelled')
|
|
1142
|
+
return;
|
|
720
1143
|
Object.assign(run.state, stateUpdates);
|
|
721
1144
|
for (const node of nodesToComplete) {
|
|
722
1145
|
if (!run.completed_nodes.includes(node)) {
|
|
@@ -729,10 +1152,150 @@ class RunManager {
|
|
|
729
1152
|
run.current_node = currentNode;
|
|
730
1153
|
if (status)
|
|
731
1154
|
run.status = status;
|
|
1155
|
+
if (failureMessage)
|
|
1156
|
+
run.error = failureMessage;
|
|
732
1157
|
if (nodeTaskId) {
|
|
733
1158
|
run.tasks = run.tasks ?? {};
|
|
734
1159
|
run.tasks[nodeId] = nodeTaskId;
|
|
735
1160
|
}
|
|
1161
|
+
run.audit = run.audit ?? [];
|
|
1162
|
+
run.audit.push(successAudit);
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
async runAuto(execId) {
|
|
1166
|
+
const leaseKey = `${this.resolvedDir}::${execId}`;
|
|
1167
|
+
if (inFlightAutoRuns.has(leaseKey)) {
|
|
1168
|
+
const run = this.loadRun(execId);
|
|
1169
|
+
if (!run)
|
|
1170
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1171
|
+
console.log(`runAuto "${execId}": already in-flight — skipping duplicate auto-run (single-flight guard)`);
|
|
1172
|
+
return { run, outcome: 'paused', reason: 'execution already in-flight (single-flight guard)', executed: [] };
|
|
1173
|
+
}
|
|
1174
|
+
inFlightAutoRuns.add(leaseKey);
|
|
1175
|
+
try {
|
|
1176
|
+
const initial = this.loadRun(execId);
|
|
1177
|
+
if (!initial)
|
|
1178
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1179
|
+
const doc = this.loadWorkflowYaml(initial.workflow_file);
|
|
1180
|
+
if (!doc)
|
|
1181
|
+
throw new Error(`Failed to load workflow file: ${initial.workflow_file}`);
|
|
1182
|
+
const spec = doc.workflows[initial.workflow_id];
|
|
1183
|
+
if (!spec) {
|
|
1184
|
+
throw new Error(`Workflow "${initial.workflow_id}" not found in ${initial.workflow_file}`);
|
|
1185
|
+
}
|
|
1186
|
+
const executed = [];
|
|
1187
|
+
if (initial.status === 'completed' || initial.status === 'failed' || initial.status === 'cancelled') {
|
|
1188
|
+
return {
|
|
1189
|
+
run: initial,
|
|
1190
|
+
outcome: initial.status === 'completed' ? 'completed' : initial.status === 'cancelled' ? 'cancelled' : 'failed',
|
|
1191
|
+
reason: `run already ${initial.status}`,
|
|
1192
|
+
executed,
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
const firstNode = Object.keys(spec.nodes ?? {})[0];
|
|
1196
|
+
const maxIterations = (typeof spec.max_iterations === 'number' && spec.max_iterations > 0)
|
|
1197
|
+
? spec.max_iterations
|
|
1198
|
+
: 30;
|
|
1199
|
+
let run = initial;
|
|
1200
|
+
while (true) {
|
|
1201
|
+
run = this.loadRun(execId);
|
|
1202
|
+
if (run.status === 'completed')
|
|
1203
|
+
return { run, outcome: 'completed', executed };
|
|
1204
|
+
if (run.status === 'failed')
|
|
1205
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1206
|
+
if (run.status === 'cancelled')
|
|
1207
|
+
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
1208
|
+
const nodeId = run.current_node || firstNode;
|
|
1209
|
+
if (!nodeId)
|
|
1210
|
+
return { run, outcome: 'failed', reason: 'workflow has no nodes', executed };
|
|
1211
|
+
const node = spec.nodes[nodeId];
|
|
1212
|
+
if (!node) {
|
|
1213
|
+
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1214
|
+
r.current_node = nodeId;
|
|
1215
|
+
r.status = 'failed';
|
|
1216
|
+
r.error = `node "${nodeId}" not found in workflow`;
|
|
1217
|
+
});
|
|
1218
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1219
|
+
}
|
|
1220
|
+
if (node.type === 'approval') {
|
|
1221
|
+
if (run.current_node !== nodeId) {
|
|
1222
|
+
run = await this.atomicUpdateAsync(execId, (r) => { r.current_node = nodeId; });
|
|
1223
|
+
}
|
|
1224
|
+
return { run, outcome: 'paused', reason: `approval node "${nodeId}" requires manual decision`, executed };
|
|
1225
|
+
}
|
|
1226
|
+
if (node.type === 'parallel') {
|
|
1227
|
+
if (run.current_node !== nodeId) {
|
|
1228
|
+
run = await this.atomicUpdateAsync(execId, (r) => { r.current_node = nodeId; });
|
|
1229
|
+
}
|
|
1230
|
+
return { run, outcome: 'paused', reason: `parallel auto-run not implemented (node "${nodeId}")`, executed };
|
|
1231
|
+
}
|
|
1232
|
+
if (!KNOWN_AUTO_NODE_TYPES.has(node.type)) {
|
|
1233
|
+
if (run.current_node !== nodeId) {
|
|
1234
|
+
run = await this.atomicUpdateAsync(execId, (r) => { r.current_node = nodeId; });
|
|
1235
|
+
}
|
|
1236
|
+
return { run, outcome: 'paused', reason: `unknown node type "${node.type}" (node "${nodeId}")`, executed };
|
|
1237
|
+
}
|
|
1238
|
+
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1239
|
+
const engine = readRunEngineState(r);
|
|
1240
|
+
engine.visitCounts[nodeId] = (engine.visitCounts[nodeId] ?? 0) + 1;
|
|
1241
|
+
engine.iterations += 1;
|
|
1242
|
+
r.state['__engine'] = engine;
|
|
1243
|
+
if (engine.visitCounts[nodeId] > maxIterations || engine.iterations > maxIterations) {
|
|
1244
|
+
r.status = 'failed';
|
|
1245
|
+
r.error = `auto runner exceeded max_iterations (${maxIterations}); possible loop at "${nodeId}"`;
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
if (run.status === 'failed') {
|
|
1249
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1250
|
+
}
|
|
1251
|
+
try {
|
|
1252
|
+
run = await this.executeNode(execId, nodeId, { trigger: 'auto' });
|
|
1253
|
+
executed.push(nodeId);
|
|
1254
|
+
}
|
|
1255
|
+
catch (e) {
|
|
1256
|
+
run = this.loadRun(execId);
|
|
1257
|
+
return { run, outcome: 'failed', reason: e.message, executed };
|
|
1258
|
+
}
|
|
1259
|
+
if (run.status === 'completed')
|
|
1260
|
+
return { run, outcome: 'completed', executed };
|
|
1261
|
+
if (run.status === 'failed')
|
|
1262
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1263
|
+
if (run.status === 'cancelled')
|
|
1264
|
+
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
1265
|
+
if (!SELF_ADVANCING_NODE_TYPES.has(node.type)) {
|
|
1266
|
+
const next = node.next;
|
|
1267
|
+
if (!next) {
|
|
1268
|
+
return { run, outcome: 'paused', reason: `node "${nodeId}" has no "next"; stopping`, executed };
|
|
1269
|
+
}
|
|
1270
|
+
run = this.moveNode(execId, next);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
finally {
|
|
1275
|
+
inFlightAutoRuns.delete(leaseKey);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
cancelRun(execId, reason = 'Cancelled by user') {
|
|
1279
|
+
return this.atomicUpdate(execId, (run) => {
|
|
1280
|
+
if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
const now = new Date();
|
|
1284
|
+
run.status = 'cancelled';
|
|
1285
|
+
run.error = reason;
|
|
1286
|
+
run.audit = run.audit ?? [];
|
|
1287
|
+
run.audit.push({
|
|
1288
|
+
exec_id: run.id,
|
|
1289
|
+
node_id: run.current_node || '',
|
|
1290
|
+
type: 'cancel',
|
|
1291
|
+
summary: reason,
|
|
1292
|
+
started_at: now.toISOString(),
|
|
1293
|
+
ended_at: now.toISOString(),
|
|
1294
|
+
duration_ms: 0,
|
|
1295
|
+
exit_code: null,
|
|
1296
|
+
error: reason,
|
|
1297
|
+
trigger: 'manual',
|
|
1298
|
+
});
|
|
736
1299
|
});
|
|
737
1300
|
}
|
|
738
1301
|
reset(execId) {
|
|
@@ -742,6 +1305,42 @@ class RunManager {
|
|
|
742
1305
|
run.completed_nodes = [];
|
|
743
1306
|
});
|
|
744
1307
|
}
|
|
1308
|
+
retry(execId) {
|
|
1309
|
+
const snapshot = this.loadRun(execId);
|
|
1310
|
+
if (!snapshot)
|
|
1311
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1312
|
+
const doc = this.loadWorkflowYaml(snapshot.workflow_file);
|
|
1313
|
+
if (!doc)
|
|
1314
|
+
throw new Error(`Failed to load workflow file: ${snapshot.workflow_file}`);
|
|
1315
|
+
const spec = doc.workflows[snapshot.workflow_id];
|
|
1316
|
+
if (!spec)
|
|
1317
|
+
throw new Error(`Workflow "${snapshot.workflow_id}" not found`);
|
|
1318
|
+
if (snapshot.status !== 'failed') {
|
|
1319
|
+
throw new Error(`Run "${execId}" is not failed (status=${snapshot.status ?? 'running'}); only failed runs can be retried`);
|
|
1320
|
+
}
|
|
1321
|
+
if (!isRetryableFailure(snapshot, spec)) {
|
|
1322
|
+
throw new Error(`Run "${execId}" failed at a non-retryable node "${snapshot.current_node}" (error node); use reset to restart`);
|
|
1323
|
+
}
|
|
1324
|
+
return this.atomicUpdate(execId, (run) => {
|
|
1325
|
+
const failedNode = run.current_node;
|
|
1326
|
+
delete run.status;
|
|
1327
|
+
delete run.error;
|
|
1328
|
+
run.retry_count = (run.retry_count ?? 0) + 1;
|
|
1329
|
+
run.audit = run.audit ?? [];
|
|
1330
|
+
const now = new Date().toISOString();
|
|
1331
|
+
run.audit.push({
|
|
1332
|
+
exec_id: run.id,
|
|
1333
|
+
node_id: failedNode,
|
|
1334
|
+
type: 'retry',
|
|
1335
|
+
summary: `manual retry #${run.retry_count} of node "${failedNode}"`,
|
|
1336
|
+
started_at: now,
|
|
1337
|
+
ended_at: now,
|
|
1338
|
+
duration_ms: 0,
|
|
1339
|
+
exit_code: null,
|
|
1340
|
+
trigger: 'manual',
|
|
1341
|
+
});
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
745
1344
|
listRuns(opts) {
|
|
746
1345
|
this.ensureRunsDir();
|
|
747
1346
|
const files = fs.readdirSync(this.resolvedDir).filter(f => f.endsWith('.json'));
|