@crewx/workflow 0.3.22-rc.9 → 0.3.22-rc.90
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 +104 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +59 -5
- package/dist/cli.js.map +1 -1
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +29 -2
- package/dist/src/engine.js.map +1 -1
- package/dist/src/mermaid.d.ts.map +1 -1
- package/dist/src/mermaid.js +7 -4
- package/dist/src/mermaid.js.map +1 -1
- package/dist/src/run-manager.d.ts +7 -1
- package/dist/src/run-manager.d.ts.map +1 -1
- package/dist/src/run-manager.js +319 -93
- package/dist/src/run-manager.js.map +1 -1
- package/dist/src/types.d.ts +6 -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.map +1 -1
- package/dist/src/utils/shell-security.js +16 -2
- package/dist/src/utils/shell-security.js.map +1 -1
- package/dist/workflow-schema.json +14 -2
- package/package.json +14 -4
- package/workflow-schema.json +14 -2
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 = exports.SKILL_TASK_DEFAULT_TIMEOUT = 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"));
|
|
@@ -46,11 +49,13 @@ 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");
|
|
48
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; } });
|
|
49
54
|
exports.SKILL_TASK_DEFAULT_TIMEOUT = 600000;
|
|
50
55
|
const KNOWN_AUTO_NODE_TYPES = new Set([
|
|
51
|
-
'agent_task', 'skill_task', 'shell_task', 'branch', 'expression', 'join', 'end',
|
|
56
|
+
'agent_task', 'skill_task', 'shell_task', 'branch', 'expression', 'join', 'end', 'error',
|
|
52
57
|
]);
|
|
53
|
-
const SELF_ADVANCING_NODE_TYPES = new Set(['branch', 'expression', 'end']);
|
|
58
|
+
const SELF_ADVANCING_NODE_TYPES = new Set(['branch', 'expression', 'end', 'error']);
|
|
54
59
|
function killProcessGroup(child) {
|
|
55
60
|
try {
|
|
56
61
|
if (process.platform === 'win32') {
|
|
@@ -79,7 +84,7 @@ async function runProcess(command, argv, opts) {
|
|
|
79
84
|
child = cp.spawn(command, argv, {
|
|
80
85
|
cwd: opts.cwd,
|
|
81
86
|
env: opts.env,
|
|
82
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
87
|
+
stdio: [opts.stdin !== undefined ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
|
83
88
|
shell: opts.shell ?? false,
|
|
84
89
|
detached: process.platform !== 'win32',
|
|
85
90
|
windowsHide: opts.windowsHide ?? true,
|
|
@@ -107,6 +112,9 @@ async function runProcess(command, argv, opts) {
|
|
|
107
112
|
timer.unref();
|
|
108
113
|
child.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
109
114
|
child.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
115
|
+
if (opts.stdin !== undefined) {
|
|
116
|
+
child.stdin?.end(opts.stdin);
|
|
117
|
+
}
|
|
110
118
|
child.on('error', (err) => finish({ status: null, stdout, stderr, timedOut: false, error: err }));
|
|
111
119
|
child.on('close', (code) => finish({ status: code, stdout, stderr, timedOut: false }));
|
|
112
120
|
});
|
|
@@ -210,6 +218,7 @@ const FORBIDDEN_IDENTS = new Set([
|
|
|
210
218
|
'require', 'import', 'process', 'global', 'globalThis',
|
|
211
219
|
'Function', 'eval', '__proto__', 'constructor', 'prototype',
|
|
212
220
|
]);
|
|
221
|
+
const FORBIDDEN_STATE_PATH_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
213
222
|
function tokenize(expr) {
|
|
214
223
|
const tokens = [];
|
|
215
224
|
let i = 0;
|
|
@@ -443,6 +452,8 @@ class ExprParser {
|
|
|
443
452
|
const val = this.state[key];
|
|
444
453
|
if (val === undefined || val === null)
|
|
445
454
|
return 0;
|
|
455
|
+
if (typeof val === 'boolean' || typeof val === 'number')
|
|
456
|
+
return val;
|
|
446
457
|
const strVal = String(val);
|
|
447
458
|
if (strVal === '')
|
|
448
459
|
return '';
|
|
@@ -465,12 +476,97 @@ function safeEvaluate(expr, state) {
|
|
|
465
476
|
const parser = new ExprParser(tokens, state);
|
|
466
477
|
return parser.parse();
|
|
467
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
|
+
}
|
|
468
559
|
class RunManager {
|
|
469
560
|
constructor(runsDir = '.crewx/workflow-runs', projectRoot) {
|
|
470
561
|
this.runsDir = runsDir;
|
|
471
562
|
const base = projectRoot || process.env.CREWX_WORKSPACE || process.cwd();
|
|
472
563
|
this.resolvedDir = path.resolve(base, this.runsDir);
|
|
473
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
|
+
}
|
|
474
570
|
}
|
|
475
571
|
ensureRunsDir() {
|
|
476
572
|
if (!fs.existsSync(this.resolvedDir)) {
|
|
@@ -545,6 +641,12 @@ class RunManager {
|
|
|
545
641
|
return null;
|
|
546
642
|
return doc;
|
|
547
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
|
+
}
|
|
548
650
|
start(yamlPath, workflowId, overrides) {
|
|
549
651
|
const doc = this.loadWorkflowYaml(yamlPath);
|
|
550
652
|
if (!doc) {
|
|
@@ -628,6 +730,7 @@ class RunManager {
|
|
|
628
730
|
const nodesToComplete = [];
|
|
629
731
|
let currentNode = nodeId;
|
|
630
732
|
let status;
|
|
733
|
+
let failureMessage;
|
|
631
734
|
let nodeTaskId;
|
|
632
735
|
const trigger = opts?.trigger ?? 'manual';
|
|
633
736
|
const startedAt = new Date();
|
|
@@ -655,19 +758,39 @@ class RunManager {
|
|
|
655
758
|
if (!nodeSpec.agent) {
|
|
656
759
|
throw new Error(`Node "${nodeId}" is agent_task but has no agent defined`);
|
|
657
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;
|
|
658
774
|
const input = nodeSpec.input
|
|
659
|
-
? interpolateTemplate(nodeSpec.input,
|
|
775
|
+
? interpolateTemplate(nodeSpec.input, agentCtx)
|
|
660
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
|
+
});
|
|
661
784
|
const finalInput = nodeSpec.output_format === 'json'
|
|
662
|
-
? input + (0, output_format_1.buildJsonOutputSuffix)(nodeSpec.output_schema, nodeSpec.output_strict)
|
|
663
|
-
: input;
|
|
785
|
+
? preamble + input + (0, output_format_1.buildJsonOutputSuffix)(nodeSpec.output_schema, nodeSpec.output_strict)
|
|
786
|
+
: preamble + input;
|
|
664
787
|
const mode = nodeSpec.mode === 'query' ? 'q' : 'x';
|
|
665
|
-
const prompt = `@${
|
|
788
|
+
const prompt = `@${resolvedAgent} ${finalInput}`;
|
|
666
789
|
if (opts?.dryRun) {
|
|
667
|
-
console.log(`[dry-run] Would execute: crewx ${mode} "@${
|
|
790
|
+
console.log(`[dry-run] Would execute: crewx ${mode} "@${resolvedAgent} ..."`);
|
|
668
791
|
break;
|
|
669
792
|
}
|
|
670
|
-
console.log(`Executing: @${
|
|
793
|
+
console.log(`Executing: @${resolvedAgent} (${mode} mode)`);
|
|
671
794
|
const crewxCli = process.env.CREWX_CLI || 'npx crewx';
|
|
672
795
|
const cliParts = crewxCli.split(/\s+/);
|
|
673
796
|
const bin = cliParts[0];
|
|
@@ -692,9 +815,10 @@ class RunManager {
|
|
|
692
815
|
});
|
|
693
816
|
const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, wfArgs);
|
|
694
817
|
const result = await runProcess(invocation.command, invocation.argv, {
|
|
695
|
-
cwd:
|
|
818
|
+
cwd: nodeCwd,
|
|
696
819
|
env: {
|
|
697
820
|
...process.env,
|
|
821
|
+
CREWX_WORKSPACE: workspaceRoot,
|
|
698
822
|
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
699
823
|
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
700
824
|
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
@@ -719,6 +843,10 @@ class RunManager {
|
|
|
719
843
|
throw new Error(`Agent execution failed (exit ${result.status}): ${stderr}`);
|
|
720
844
|
}
|
|
721
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
|
+
}
|
|
722
850
|
if (nodeSpec.output) {
|
|
723
851
|
if (nodeSpec.output_format === 'json') {
|
|
724
852
|
const retryMax = nodeSpec.output_retry ?? 1;
|
|
@@ -731,7 +859,7 @@ class RunManager {
|
|
|
731
859
|
retryInput = finalInput +
|
|
732
860
|
`\n\n## ⚠️ PREVIOUS OUTPUT FAILED JSON.parse: ${lastError.message}\n` +
|
|
733
861
|
`Return STRICT JSON ONLY. First char "{", last char "}". No prose, no fences.`;
|
|
734
|
-
const retryPrompt = `@${
|
|
862
|
+
const retryPrompt = `@${resolvedAgent} ${retryInput}`;
|
|
735
863
|
const retryArgs = [...baseArgs, mode, retryPrompt];
|
|
736
864
|
if (process.env.CREWX_WORKFLOW_THREAD === 'on') {
|
|
737
865
|
retryArgs.push(`--thread=workflow:${snapshot.id}`);
|
|
@@ -739,9 +867,10 @@ class RunManager {
|
|
|
739
867
|
retryArgs.push('--metadata', JSON.stringify(wfMetadata));
|
|
740
868
|
const retryInvocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, retryArgs);
|
|
741
869
|
const retryResult = await runProcess(retryInvocation.command, retryInvocation.argv, {
|
|
742
|
-
cwd:
|
|
870
|
+
cwd: nodeCwd,
|
|
743
871
|
env: {
|
|
744
872
|
...process.env,
|
|
873
|
+
CREWX_WORKSPACE: workspaceRoot,
|
|
745
874
|
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
746
875
|
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
747
876
|
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
@@ -761,6 +890,10 @@ class RunManager {
|
|
|
761
890
|
throw new Error(`Agent retry failed (exit ${retryResult.status}): ${retryResult.stderr.trim()}`);
|
|
762
891
|
}
|
|
763
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)'}`);
|
|
896
|
+
}
|
|
764
897
|
}
|
|
765
898
|
try {
|
|
766
899
|
parsed = JSON.parse((0, output_format_1.extractJson)(attemptOutput));
|
|
@@ -795,9 +928,16 @@ class RunManager {
|
|
|
795
928
|
}
|
|
796
929
|
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
797
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;
|
|
798
934
|
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, ctx) : undefined, this.projectRoot);
|
|
799
935
|
if (!cwdCheck.ok)
|
|
800
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;
|
|
801
941
|
const timeout = nodeSpec.timeout ?? exports.SKILL_TASK_DEFAULT_TIMEOUT;
|
|
802
942
|
if (opts?.dryRun) {
|
|
803
943
|
console.log(`[dry-run] Would execute: crewx skill ${nodeSpec.skill} ${skillArgs.join(' ')}`.trim());
|
|
@@ -819,6 +959,7 @@ class RunManager {
|
|
|
819
959
|
cwd: cwdCheck.resolved,
|
|
820
960
|
env: {
|
|
821
961
|
...process.env,
|
|
962
|
+
CREWX_WORKSPACE: skillWorkspaceRoot,
|
|
822
963
|
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
823
964
|
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
824
965
|
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
@@ -826,6 +967,7 @@ class RunManager {
|
|
|
826
967
|
timeout,
|
|
827
968
|
shell: invocation.shell ?? false,
|
|
828
969
|
windowsHide: invocation.windowsHide,
|
|
970
|
+
stdin: skillStdin,
|
|
829
971
|
});
|
|
830
972
|
nodeStdout = result.stdout;
|
|
831
973
|
nodeStderr = result.stderr;
|
|
@@ -876,11 +1018,15 @@ class RunManager {
|
|
|
876
1018
|
childEnv.CREWX_WORKFLOW_EXEC_ID = snapshot.id;
|
|
877
1019
|
childEnv.CREWX_WORKFLOW_NODE_ID = nodeId;
|
|
878
1020
|
childEnv.CREWX_WORKFLOW_ID = snapshot.workflow_id;
|
|
1021
|
+
const shellStdin = nodeSpec.stdin !== undefined
|
|
1022
|
+
? interpolateTemplate(String(nodeSpec.stdin), ctx)
|
|
1023
|
+
: undefined;
|
|
879
1024
|
const result = await runProcess(command[0], command.slice(1), {
|
|
880
1025
|
cwd: cwdCheck.resolved,
|
|
881
1026
|
env: childEnv,
|
|
882
1027
|
timeout,
|
|
883
1028
|
shell: false,
|
|
1029
|
+
stdin: shellStdin,
|
|
884
1030
|
});
|
|
885
1031
|
nodeStdout = result.stdout;
|
|
886
1032
|
nodeStderr = result.stderr;
|
|
@@ -902,8 +1048,7 @@ class RunManager {
|
|
|
902
1048
|
if (!nodeSpec.condition) {
|
|
903
1049
|
throw new Error(`Branch node "${nodeId}" has no condition`);
|
|
904
1050
|
}
|
|
905
|
-
const
|
|
906
|
-
const condResult = String(evalFn(snapshot.state));
|
|
1051
|
+
const condResult = String(safeEvaluateBranchCondition(String(nodeSpec.condition), snapshot.state));
|
|
907
1052
|
const branches = nodeSpec.branches;
|
|
908
1053
|
const targetNode = branches?.[condResult] ?? nodeSpec.default;
|
|
909
1054
|
if (!targetNode) {
|
|
@@ -931,6 +1076,15 @@ class RunManager {
|
|
|
931
1076
|
console.log(`End "${nodeId}": workflow execution completed`);
|
|
932
1077
|
break;
|
|
933
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
|
+
}
|
|
934
1088
|
case 'expression': {
|
|
935
1089
|
if (!nodeSpec.set || typeof nodeSpec.set !== 'object') {
|
|
936
1090
|
throw new Error(`Expression node "${nodeId}" requires "set" field`);
|
|
@@ -954,6 +1108,14 @@ class RunManager {
|
|
|
954
1108
|
default:
|
|
955
1109
|
console.log(`Unknown node type "${nodeSpec.type}" for node "${nodeId}"`);
|
|
956
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
|
+
}
|
|
957
1119
|
}
|
|
958
1120
|
catch (e) {
|
|
959
1121
|
const errMessage = e.message;
|
|
@@ -965,6 +1127,7 @@ class RunManager {
|
|
|
965
1127
|
await this.atomicUpdateAsync(execId, (run) => {
|
|
966
1128
|
if (run.status === 'cancelled')
|
|
967
1129
|
return;
|
|
1130
|
+
Object.assign(run.state, stateUpdates);
|
|
968
1131
|
run.current_node = nodeId;
|
|
969
1132
|
run.status = 'failed';
|
|
970
1133
|
run.error = errMessage;
|
|
@@ -989,6 +1152,8 @@ class RunManager {
|
|
|
989
1152
|
run.current_node = currentNode;
|
|
990
1153
|
if (status)
|
|
991
1154
|
run.status = status;
|
|
1155
|
+
if (failureMessage)
|
|
1156
|
+
run.error = failureMessage;
|
|
992
1157
|
if (nodeTaskId) {
|
|
993
1158
|
run.tasks = run.tasks ?? {};
|
|
994
1159
|
run.tasks[nodeId] = nodeTaskId;
|
|
@@ -998,92 +1163,117 @@ class RunManager {
|
|
|
998
1163
|
});
|
|
999
1164
|
}
|
|
1000
1165
|
async runAuto(execId) {
|
|
1001
|
-
const
|
|
1002
|
-
if (
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
if (!spec) {
|
|
1009
|
-
throw new Error(`Workflow "${initial.workflow_id}" not found in ${initial.workflow_file}`);
|
|
1010
|
-
}
|
|
1011
|
-
const executed = [];
|
|
1012
|
-
if (initial.status === 'completed' || initial.status === 'failed' || initial.status === 'cancelled') {
|
|
1013
|
-
return {
|
|
1014
|
-
run: initial,
|
|
1015
|
-
outcome: initial.status === 'completed' ? 'completed' : initial.status === 'cancelled' ? 'cancelled' : 'failed',
|
|
1016
|
-
reason: `run already ${initial.status}`,
|
|
1017
|
-
executed,
|
|
1018
|
-
};
|
|
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: [] };
|
|
1019
1173
|
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
if (
|
|
1030
|
-
|
|
1031
|
-
if (run.status === 'failed')
|
|
1032
|
-
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1033
|
-
if (run.status === 'cancelled')
|
|
1034
|
-
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
1035
|
-
const nodeId = run.current_node || firstNode;
|
|
1036
|
-
if (!nodeId)
|
|
1037
|
-
return { run, outcome: 'failed', reason: 'workflow has no nodes', executed };
|
|
1038
|
-
const node = spec.nodes[nodeId];
|
|
1039
|
-
if (!node) {
|
|
1040
|
-
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1041
|
-
r.current_node = nodeId;
|
|
1042
|
-
r.status = 'failed';
|
|
1043
|
-
r.error = `node "${nodeId}" not found in workflow`;
|
|
1044
|
-
});
|
|
1045
|
-
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1046
|
-
}
|
|
1047
|
-
if (node.type === 'approval') {
|
|
1048
|
-
return { run, outcome: 'paused', reason: `approval node "${nodeId}" requires manual decision`, executed };
|
|
1049
|
-
}
|
|
1050
|
-
if (node.type === 'parallel') {
|
|
1051
|
-
return { run, outcome: 'paused', reason: `parallel auto-run not implemented (node "${nodeId}")`, executed };
|
|
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}`);
|
|
1052
1185
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
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
|
+
};
|
|
1055
1194
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
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
|
+
}
|
|
1059
1238
|
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1060
|
-
|
|
1061
|
-
|
|
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
|
+
}
|
|
1062
1247
|
});
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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);
|
|
1083
1271
|
}
|
|
1084
|
-
run = this.moveNode(execId, next);
|
|
1085
1272
|
}
|
|
1086
1273
|
}
|
|
1274
|
+
finally {
|
|
1275
|
+
inFlightAutoRuns.delete(leaseKey);
|
|
1276
|
+
}
|
|
1087
1277
|
}
|
|
1088
1278
|
cancelRun(execId, reason = 'Cancelled by user') {
|
|
1089
1279
|
return this.atomicUpdate(execId, (run) => {
|
|
@@ -1115,6 +1305,42 @@ class RunManager {
|
|
|
1115
1305
|
run.completed_nodes = [];
|
|
1116
1306
|
});
|
|
1117
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
|
+
}
|
|
1118
1344
|
listRuns(opts) {
|
|
1119
1345
|
this.ensureRunsDir();
|
|
1120
1346
|
const files = fs.readdirSync(this.resolvedDir).filter(f => f.endsWith('.json'));
|