@crewx/workflow 0.3.21 → 0.3.22-rc.10
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 +152 -11
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +72 -3
- package/dist/cli.js.map +1 -1
- package/dist/src/engine.d.ts +2 -0
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +22 -1
- package/dist/src/engine.js.map +1 -1
- package/dist/src/mermaid.d.ts.map +1 -1
- package/dist/src/mermaid.js +16 -3
- package/dist/src/mermaid.js.map +1 -1
- package/dist/src/run-manager.d.ts +7 -2
- package/dist/src/run-manager.d.ts.map +1 -1
- package/dist/src/run-manager.js +394 -21
- package/dist/src/run-manager.js.map +1 -1
- package/dist/src/types.d.ts +28 -2
- package/dist/src/types.d.ts.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 +124 -0
- package/dist/src/utils/shell-security.js.map +1 -0
- package/dist/workflow-schema.json +34 -1
- package/package.json +16 -4
- package/workflow-schema.json +34 -1
package/dist/src/run-manager.js
CHANGED
|
@@ -33,7 +33,7 @@ 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 = void 0;
|
|
37
37
|
exports.parseSetArgs = parseSetArgs;
|
|
38
38
|
exports.readStdin = readStdin;
|
|
39
39
|
exports.interpolateTemplate = interpolateTemplate;
|
|
@@ -45,6 +45,112 @@ const cp = __importStar(require("child_process"));
|
|
|
45
45
|
const sdk_1 = require("@crewx/sdk");
|
|
46
46
|
const file_lock_1 = require("./file-lock");
|
|
47
47
|
const output_format_1 = require("./utils/output-format");
|
|
48
|
+
const shell_security_1 = require("./utils/shell-security");
|
|
49
|
+
exports.SKILL_TASK_DEFAULT_TIMEOUT = 600000;
|
|
50
|
+
const KNOWN_AUTO_NODE_TYPES = new Set([
|
|
51
|
+
'agent_task', 'skill_task', 'shell_task', 'branch', 'expression', 'join', 'end',
|
|
52
|
+
]);
|
|
53
|
+
const SELF_ADVANCING_NODE_TYPES = new Set(['branch', 'expression', 'end']);
|
|
54
|
+
function killProcessGroup(child) {
|
|
55
|
+
try {
|
|
56
|
+
if (process.platform === 'win32') {
|
|
57
|
+
if (child.pid) {
|
|
58
|
+
cp.spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else if (child.pid) {
|
|
62
|
+
try {
|
|
63
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
try {
|
|
67
|
+
child.kill('SIGKILL');
|
|
68
|
+
}
|
|
69
|
+
catch { }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch { }
|
|
74
|
+
}
|
|
75
|
+
async function runProcess(command, argv, opts) {
|
|
76
|
+
return new Promise((resolve) => {
|
|
77
|
+
let child;
|
|
78
|
+
try {
|
|
79
|
+
child = cp.spawn(command, argv, {
|
|
80
|
+
cwd: opts.cwd,
|
|
81
|
+
env: opts.env,
|
|
82
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
83
|
+
shell: opts.shell ?? false,
|
|
84
|
+
detached: process.platform !== 'win32',
|
|
85
|
+
windowsHide: opts.windowsHide ?? true,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
resolve({ status: null, stdout: '', stderr: '', timedOut: false, error: err });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
let stdout = '';
|
|
93
|
+
let stderr = '';
|
|
94
|
+
let settled = false;
|
|
95
|
+
const finish = (result) => {
|
|
96
|
+
if (settled)
|
|
97
|
+
return;
|
|
98
|
+
settled = true;
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
resolve(result);
|
|
101
|
+
};
|
|
102
|
+
const timer = setTimeout(() => {
|
|
103
|
+
killProcessGroup(child);
|
|
104
|
+
finish({ status: null, stdout, stderr, timedOut: true });
|
|
105
|
+
}, opts.timeout);
|
|
106
|
+
if (typeof timer.unref === 'function')
|
|
107
|
+
timer.unref();
|
|
108
|
+
child.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
109
|
+
child.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
110
|
+
child.on('error', (err) => finish({ status: null, stdout, stderr, timedOut: false, error: err }));
|
|
111
|
+
child.on('close', (code) => finish({ status: code, stdout, stderr, timedOut: false }));
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function maskArg(arg) {
|
|
115
|
+
return arg.replace(/^(--?[\w-]*(?:token|secret|password|passwd|key|auth)[\w-]*)=(.+)$/i, '$1=***');
|
|
116
|
+
}
|
|
117
|
+
function summarizeNode(node) {
|
|
118
|
+
switch (node.type) {
|
|
119
|
+
case 'agent_task':
|
|
120
|
+
return `@${node.agent ?? '?'} (${node.mode === 'query' ? 'q' : 'x'})`;
|
|
121
|
+
case 'skill_task':
|
|
122
|
+
return `skill ${node.skill ?? '?'} ${(node.args ?? []).map(maskArg).join(' ')}`.trim();
|
|
123
|
+
case 'shell_task':
|
|
124
|
+
return (node.command ?? []).map(maskArg).join(' ');
|
|
125
|
+
case 'branch':
|
|
126
|
+
return `branch: ${node.condition ?? ''}`;
|
|
127
|
+
case 'expression':
|
|
128
|
+
return `set ${Object.keys(node.set ?? {}).join(', ')}`;
|
|
129
|
+
default:
|
|
130
|
+
return node.type;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function storeNodeOutput(nodeSpec, nodeId, stdout, stateUpdates) {
|
|
134
|
+
if (!nodeSpec.output)
|
|
135
|
+
return;
|
|
136
|
+
const trimmed = stdout.trim();
|
|
137
|
+
if (nodeSpec.output_format === 'json') {
|
|
138
|
+
let parsed;
|
|
139
|
+
try {
|
|
140
|
+
parsed = JSON.parse((0, output_format_1.extractJson)(trimmed));
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
throw new Error(`Node "${nodeId}" output_format=json parse failed: ${e.message}`);
|
|
144
|
+
}
|
|
145
|
+
if (nodeSpec.output_schema) {
|
|
146
|
+
await (0, output_format_1.validateJsonSchema)(parsed, nodeSpec.output_schema);
|
|
147
|
+
}
|
|
148
|
+
stateUpdates[nodeSpec.output] = parsed;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
stateUpdates[nodeSpec.output] = trimmed;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
48
154
|
function parseValue(raw) {
|
|
49
155
|
try {
|
|
50
156
|
return JSON.parse(raw);
|
|
@@ -360,10 +466,11 @@ function safeEvaluate(expr, state) {
|
|
|
360
466
|
return parser.parse();
|
|
361
467
|
}
|
|
362
468
|
class RunManager {
|
|
363
|
-
constructor(runsDir = '.crewx/workflow-runs') {
|
|
469
|
+
constructor(runsDir = '.crewx/workflow-runs', projectRoot) {
|
|
364
470
|
this.runsDir = runsDir;
|
|
365
|
-
const base = process.env.CREWX_WORKSPACE || process.cwd();
|
|
471
|
+
const base = projectRoot || process.env.CREWX_WORKSPACE || process.cwd();
|
|
366
472
|
this.resolvedDir = path.resolve(base, this.runsDir);
|
|
473
|
+
this.projectRoot = path.resolve(base);
|
|
367
474
|
}
|
|
368
475
|
ensureRunsDir() {
|
|
369
476
|
if (!fs.existsSync(this.resolvedDir)) {
|
|
@@ -522,6 +629,26 @@ class RunManager {
|
|
|
522
629
|
let currentNode = nodeId;
|
|
523
630
|
let status;
|
|
524
631
|
let nodeTaskId;
|
|
632
|
+
const trigger = opts?.trigger ?? 'manual';
|
|
633
|
+
const startedAt = new Date();
|
|
634
|
+
let nodeStdout = '';
|
|
635
|
+
let nodeStderr = '';
|
|
636
|
+
let nodeExitCode = null;
|
|
637
|
+
const buildAudit = (error) => {
|
|
638
|
+
const endedAt = new Date();
|
|
639
|
+
return {
|
|
640
|
+
exec_id: snapshot.id,
|
|
641
|
+
node_id: nodeId,
|
|
642
|
+
type: nodeSpec.type,
|
|
643
|
+
summary: summarizeNode(nodeSpec),
|
|
644
|
+
started_at: startedAt.toISOString(),
|
|
645
|
+
ended_at: endedAt.toISOString(),
|
|
646
|
+
duration_ms: endedAt.getTime() - startedAt.getTime(),
|
|
647
|
+
exit_code: nodeExitCode,
|
|
648
|
+
...(error ? { error } : {}),
|
|
649
|
+
trigger,
|
|
650
|
+
};
|
|
651
|
+
};
|
|
525
652
|
try {
|
|
526
653
|
switch (nodeSpec.type) {
|
|
527
654
|
case 'agent_task': {
|
|
@@ -545,7 +672,7 @@ class RunManager {
|
|
|
545
672
|
const cliParts = crewxCli.split(/\s+/);
|
|
546
673
|
const bin = cliParts[0];
|
|
547
674
|
const baseArgs = cliParts.slice(1);
|
|
548
|
-
const nodeTimeout = Number(process.env[mode === 'q' ? 'CREWX_TIMEOUT_QUERY' : 'CREWX_TIMEOUT_EXECUTE']) || 3600000;
|
|
675
|
+
const nodeTimeout = Number(process.env[mode === 'q' ? 'CREWX_TIMEOUT_QUERY' : 'CREWX_TIMEOUT_EXECUTE']) || 8 * 3600000;
|
|
549
676
|
const wfArgs = [...baseArgs, mode, prompt];
|
|
550
677
|
if (process.env.CREWX_WORKFLOW_THREAD === 'on') {
|
|
551
678
|
wfArgs.push(`--thread=workflow:${snapshot.id}`);
|
|
@@ -564,30 +691,34 @@ class RunManager {
|
|
|
564
691
|
packageName: 'crewx',
|
|
565
692
|
});
|
|
566
693
|
const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, wfArgs);
|
|
567
|
-
const result =
|
|
568
|
-
encoding: 'utf-8',
|
|
569
|
-
timeout: nodeTimeout,
|
|
694
|
+
const result = await runProcess(invocation.command, invocation.argv, {
|
|
570
695
|
cwd: process.cwd(),
|
|
571
|
-
stdio: ['inherit', 'pipe', 'pipe'],
|
|
572
|
-
shell: invocation.shell ?? false,
|
|
573
|
-
windowsHide: invocation.windowsHide,
|
|
574
696
|
env: {
|
|
575
697
|
...process.env,
|
|
576
698
|
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
577
699
|
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
578
700
|
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
579
701
|
},
|
|
702
|
+
timeout: nodeTimeout,
|
|
703
|
+
shell: invocation.shell ?? false,
|
|
704
|
+
windowsHide: invocation.windowsHide,
|
|
580
705
|
});
|
|
706
|
+
nodeStdout = result.stdout;
|
|
707
|
+
nodeStderr = result.stderr;
|
|
708
|
+
nodeExitCode = result.status;
|
|
581
709
|
if (result.error) {
|
|
582
710
|
throw result.error;
|
|
583
711
|
}
|
|
584
|
-
|
|
712
|
+
if (result.timedOut) {
|
|
713
|
+
throw new Error(`Agent execution timed out after ${nodeTimeout}ms`);
|
|
714
|
+
}
|
|
715
|
+
const taskIdMatch = result.stderr.match(/crewx kill (tsk_\w+)/);
|
|
585
716
|
nodeTaskId = taskIdMatch?.[1];
|
|
586
717
|
if (result.status !== 0) {
|
|
587
|
-
const stderr = result.stderr
|
|
718
|
+
const stderr = result.stderr.trim();
|
|
588
719
|
throw new Error(`Agent execution failed (exit ${result.status}): ${stderr}`);
|
|
589
720
|
}
|
|
590
|
-
const output = result.stdout
|
|
721
|
+
const output = result.stdout.trim();
|
|
591
722
|
if (nodeSpec.output) {
|
|
592
723
|
if (nodeSpec.output_format === 'json') {
|
|
593
724
|
const retryMax = nodeSpec.output_retry ?? 1;
|
|
@@ -607,26 +738,29 @@ class RunManager {
|
|
|
607
738
|
}
|
|
608
739
|
retryArgs.push('--metadata', JSON.stringify(wfMetadata));
|
|
609
740
|
const retryInvocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, retryArgs);
|
|
610
|
-
const retryResult =
|
|
611
|
-
encoding: 'utf-8',
|
|
612
|
-
timeout: nodeTimeout,
|
|
741
|
+
const retryResult = await runProcess(retryInvocation.command, retryInvocation.argv, {
|
|
613
742
|
cwd: process.cwd(),
|
|
614
|
-
stdio: ['inherit', 'pipe', 'pipe'],
|
|
615
|
-
shell: retryInvocation.shell ?? false,
|
|
616
|
-
windowsHide: retryInvocation.windowsHide,
|
|
617
743
|
env: {
|
|
618
744
|
...process.env,
|
|
619
745
|
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
620
746
|
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
621
747
|
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
622
748
|
},
|
|
749
|
+
timeout: nodeTimeout,
|
|
750
|
+
shell: retryInvocation.shell ?? false,
|
|
751
|
+
windowsHide: retryInvocation.windowsHide,
|
|
623
752
|
});
|
|
753
|
+
nodeStdout = retryResult.stdout;
|
|
754
|
+
nodeStderr = retryResult.stderr;
|
|
755
|
+
nodeExitCode = retryResult.status;
|
|
624
756
|
if (retryResult.error)
|
|
625
757
|
throw retryResult.error;
|
|
758
|
+
if (retryResult.timedOut)
|
|
759
|
+
throw new Error(`Agent retry timed out after ${nodeTimeout}ms`);
|
|
626
760
|
if (retryResult.status !== 0) {
|
|
627
|
-
throw new Error(`Agent retry failed (exit ${retryResult.status}): ${retryResult.stderr
|
|
761
|
+
throw new Error(`Agent retry failed (exit ${retryResult.status}): ${retryResult.stderr.trim()}`);
|
|
628
762
|
}
|
|
629
|
-
attemptOutput = retryResult.stdout
|
|
763
|
+
attemptOutput = retryResult.stdout.trim();
|
|
630
764
|
}
|
|
631
765
|
try {
|
|
632
766
|
parsed = JSON.parse((0, output_format_1.extractJson)(attemptOutput));
|
|
@@ -652,6 +786,118 @@ class RunManager {
|
|
|
652
786
|
}
|
|
653
787
|
break;
|
|
654
788
|
}
|
|
789
|
+
case 'skill_task': {
|
|
790
|
+
if (!nodeSpec.skill) {
|
|
791
|
+
throw new Error(`Node "${nodeId}" is skill_task but has no skill defined`);
|
|
792
|
+
}
|
|
793
|
+
if (!shell_security_1.SKILL_NAME_RE.test(nodeSpec.skill)) {
|
|
794
|
+
throw new Error(`Node "${nodeId}" has invalid skill name "${nodeSpec.skill}" (must match ^[a-z0-9][a-z0-9-]*$)`);
|
|
795
|
+
}
|
|
796
|
+
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
797
|
+
const skillArgs = (nodeSpec.args ?? []).map((a) => interpolateTemplate(String(a), ctx));
|
|
798
|
+
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, ctx) : undefined, this.projectRoot);
|
|
799
|
+
if (!cwdCheck.ok)
|
|
800
|
+
throw new Error(`Node "${nodeId}": ${cwdCheck.error}`);
|
|
801
|
+
const timeout = nodeSpec.timeout ?? exports.SKILL_TASK_DEFAULT_TIMEOUT;
|
|
802
|
+
if (opts?.dryRun) {
|
|
803
|
+
console.log(`[dry-run] Would execute: crewx skill ${nodeSpec.skill} ${skillArgs.join(' ')}`.trim());
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
console.log(`Executing skill: ${nodeSpec.skill} ${skillArgs.join(' ')}`.trim());
|
|
807
|
+
const crewxCli = process.env.CREWX_CLI || 'npx crewx';
|
|
808
|
+
const cliParts = crewxCli.split(/\s+/);
|
|
809
|
+
const bin = cliParts[0];
|
|
810
|
+
const baseArgs = cliParts.slice(1);
|
|
811
|
+
const skillArgv = [...baseArgs, 'skill', nodeSpec.skill, ...skillArgs];
|
|
812
|
+
const program = (0, sdk_1.resolveWindowsSpawnProgram)({
|
|
813
|
+
command: bin,
|
|
814
|
+
allowShellFallback: true,
|
|
815
|
+
packageName: 'crewx',
|
|
816
|
+
});
|
|
817
|
+
const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, skillArgv);
|
|
818
|
+
const result = await runProcess(invocation.command, invocation.argv, {
|
|
819
|
+
cwd: cwdCheck.resolved,
|
|
820
|
+
env: {
|
|
821
|
+
...process.env,
|
|
822
|
+
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
823
|
+
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
824
|
+
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
825
|
+
},
|
|
826
|
+
timeout,
|
|
827
|
+
shell: invocation.shell ?? false,
|
|
828
|
+
windowsHide: invocation.windowsHide,
|
|
829
|
+
});
|
|
830
|
+
nodeStdout = result.stdout;
|
|
831
|
+
nodeStderr = result.stderr;
|
|
832
|
+
nodeExitCode = result.status;
|
|
833
|
+
if (result.error)
|
|
834
|
+
throw result.error;
|
|
835
|
+
if (result.timedOut)
|
|
836
|
+
throw new Error(`skill_task "${nodeId}" timed out after ${timeout}ms`);
|
|
837
|
+
if (result.status !== 0) {
|
|
838
|
+
throw new Error(`skill_task "${nodeId}" failed (exit ${result.status}): ${result.stderr.trim()}`);
|
|
839
|
+
}
|
|
840
|
+
await storeNodeOutput(nodeSpec, nodeId, result.stdout, stateUpdates);
|
|
841
|
+
if (nodeSpec.output) {
|
|
842
|
+
console.log(`Result stored in state.${nodeSpec.output}`);
|
|
843
|
+
}
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
case 'shell_task': {
|
|
847
|
+
const shellEnvEnabled = process.env.CREWX_WORKFLOW_SHELL === '1';
|
|
848
|
+
const shellMetaAllowed = spec.metadata?.shell_task_allowed === true;
|
|
849
|
+
if (!shellEnvEnabled || !shellMetaAllowed) {
|
|
850
|
+
throw new Error('shell_task is disabled. Set CREWX_WORKFLOW_SHELL=1 and metadata.shell_task_allowed=true to enable.');
|
|
851
|
+
}
|
|
852
|
+
const cmdCheck = (0, shell_security_1.validateShellCommand)(nodeSpec.command);
|
|
853
|
+
if (!cmdCheck.ok)
|
|
854
|
+
throw new Error(`Node "${nodeId}": ${cmdCheck.error}`);
|
|
855
|
+
const envCheck = (0, shell_security_1.validateEnvKeys)(nodeSpec.env);
|
|
856
|
+
if (!envCheck.ok)
|
|
857
|
+
throw new Error(`Node "${nodeId}": ${envCheck.error}`);
|
|
858
|
+
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
859
|
+
const command = nodeSpec.command.map((c) => interpolateTemplate(c, ctx));
|
|
860
|
+
const postCheck = (0, shell_security_1.validateShellCommand)(command);
|
|
861
|
+
if (!postCheck.ok)
|
|
862
|
+
throw new Error(`Node "${nodeId}": ${postCheck.error}`);
|
|
863
|
+
const interpolatedEnv = nodeSpec.env
|
|
864
|
+
? Object.fromEntries(Object.entries(nodeSpec.env).map(([k, v]) => [k, interpolateTemplate(String(v), ctx)]))
|
|
865
|
+
: undefined;
|
|
866
|
+
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, ctx) : undefined, this.projectRoot);
|
|
867
|
+
if (!cwdCheck.ok)
|
|
868
|
+
throw new Error(`Node "${nodeId}": ${cwdCheck.error}`);
|
|
869
|
+
const timeout = nodeSpec.timeout ?? exports.SKILL_TASK_DEFAULT_TIMEOUT;
|
|
870
|
+
if (opts?.dryRun) {
|
|
871
|
+
console.log(`[dry-run] Would execute: ${command.join(' ')}`);
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
console.log(`Executing shell_task: ${command[0]} (${command.length - 1} args)`);
|
|
875
|
+
const childEnv = (0, shell_security_1.buildShellEnv)(interpolatedEnv, process.env);
|
|
876
|
+
childEnv.CREWX_WORKFLOW_EXEC_ID = snapshot.id;
|
|
877
|
+
childEnv.CREWX_WORKFLOW_NODE_ID = nodeId;
|
|
878
|
+
childEnv.CREWX_WORKFLOW_ID = snapshot.workflow_id;
|
|
879
|
+
const result = await runProcess(command[0], command.slice(1), {
|
|
880
|
+
cwd: cwdCheck.resolved,
|
|
881
|
+
env: childEnv,
|
|
882
|
+
timeout,
|
|
883
|
+
shell: false,
|
|
884
|
+
});
|
|
885
|
+
nodeStdout = result.stdout;
|
|
886
|
+
nodeStderr = result.stderr;
|
|
887
|
+
nodeExitCode = result.status;
|
|
888
|
+
if (result.error)
|
|
889
|
+
throw result.error;
|
|
890
|
+
if (result.timedOut)
|
|
891
|
+
throw new Error(`shell_task "${nodeId}" timed out after ${timeout}ms`);
|
|
892
|
+
if (result.status !== 0) {
|
|
893
|
+
throw new Error(`shell_task "${nodeId}" failed (exit ${result.status}): ${result.stderr.trim()}`);
|
|
894
|
+
}
|
|
895
|
+
await storeNodeOutput(nodeSpec, nodeId, result.stdout, stateUpdates);
|
|
896
|
+
if (nodeSpec.output) {
|
|
897
|
+
console.log(`Result stored in state.${nodeSpec.output}`);
|
|
898
|
+
}
|
|
899
|
+
break;
|
|
900
|
+
}
|
|
655
901
|
case 'branch': {
|
|
656
902
|
if (!nodeSpec.condition) {
|
|
657
903
|
throw new Error(`Branch node "${nodeId}" has no condition`);
|
|
@@ -710,13 +956,27 @@ class RunManager {
|
|
|
710
956
|
}
|
|
711
957
|
}
|
|
712
958
|
catch (e) {
|
|
959
|
+
const errMessage = e.message;
|
|
960
|
+
const partial = [
|
|
961
|
+
nodeStdout.trim() ? `stdout: ${nodeStdout.trim().slice(-500)}` : '',
|
|
962
|
+
nodeStderr.trim() ? `stderr: ${nodeStderr.trim().slice(-500)}` : '',
|
|
963
|
+
].filter(Boolean).join(' | ');
|
|
964
|
+
const auditEntry = buildAudit(partial ? `${errMessage} (${partial})` : errMessage);
|
|
713
965
|
await this.atomicUpdateAsync(execId, (run) => {
|
|
966
|
+
if (run.status === 'cancelled')
|
|
967
|
+
return;
|
|
714
968
|
run.current_node = nodeId;
|
|
715
969
|
run.status = 'failed';
|
|
970
|
+
run.error = errMessage;
|
|
971
|
+
run.audit = run.audit ?? [];
|
|
972
|
+
run.audit.push(auditEntry);
|
|
716
973
|
});
|
|
717
974
|
throw e;
|
|
718
975
|
}
|
|
976
|
+
const successAudit = buildAudit();
|
|
719
977
|
return this.atomicUpdateAsync(execId, (run) => {
|
|
978
|
+
if (run.status === 'cancelled')
|
|
979
|
+
return;
|
|
720
980
|
Object.assign(run.state, stateUpdates);
|
|
721
981
|
for (const node of nodesToComplete) {
|
|
722
982
|
if (!run.completed_nodes.includes(node)) {
|
|
@@ -733,6 +993,119 @@ class RunManager {
|
|
|
733
993
|
run.tasks = run.tasks ?? {};
|
|
734
994
|
run.tasks[nodeId] = nodeTaskId;
|
|
735
995
|
}
|
|
996
|
+
run.audit = run.audit ?? [];
|
|
997
|
+
run.audit.push(successAudit);
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
async runAuto(execId) {
|
|
1001
|
+
const initial = this.loadRun(execId);
|
|
1002
|
+
if (!initial)
|
|
1003
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1004
|
+
const doc = this.loadWorkflowYaml(initial.workflow_file);
|
|
1005
|
+
if (!doc)
|
|
1006
|
+
throw new Error(`Failed to load workflow file: ${initial.workflow_file}`);
|
|
1007
|
+
const spec = doc.workflows[initial.workflow_id];
|
|
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
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
const firstNode = Object.keys(spec.nodes ?? {})[0];
|
|
1021
|
+
const maxIterations = (typeof spec.max_iterations === 'number' && spec.max_iterations > 0)
|
|
1022
|
+
? spec.max_iterations
|
|
1023
|
+
: 30;
|
|
1024
|
+
const visitCounts = {};
|
|
1025
|
+
let iterations = 0;
|
|
1026
|
+
let run = initial;
|
|
1027
|
+
while (true) {
|
|
1028
|
+
run = this.loadRun(execId);
|
|
1029
|
+
if (run.status === 'completed')
|
|
1030
|
+
return { run, outcome: 'completed', executed };
|
|
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 };
|
|
1052
|
+
}
|
|
1053
|
+
if (!KNOWN_AUTO_NODE_TYPES.has(node.type)) {
|
|
1054
|
+
return { run, outcome: 'paused', reason: `unknown node type "${node.type}" (node "${nodeId}")`, executed };
|
|
1055
|
+
}
|
|
1056
|
+
visitCounts[nodeId] = (visitCounts[nodeId] ?? 0) + 1;
|
|
1057
|
+
iterations++;
|
|
1058
|
+
if (visitCounts[nodeId] > maxIterations || iterations > maxIterations) {
|
|
1059
|
+
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1060
|
+
r.status = 'failed';
|
|
1061
|
+
r.error = `auto runner exceeded max_iterations (${maxIterations}); possible loop at "${nodeId}"`;
|
|
1062
|
+
});
|
|
1063
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1064
|
+
}
|
|
1065
|
+
try {
|
|
1066
|
+
run = await this.executeNode(execId, nodeId, { trigger: 'auto' });
|
|
1067
|
+
executed.push(nodeId);
|
|
1068
|
+
}
|
|
1069
|
+
catch (e) {
|
|
1070
|
+
run = this.loadRun(execId);
|
|
1071
|
+
return { run, outcome: 'failed', reason: e.message, executed };
|
|
1072
|
+
}
|
|
1073
|
+
if (run.status === 'completed')
|
|
1074
|
+
return { run, outcome: 'completed', executed };
|
|
1075
|
+
if (run.status === 'failed')
|
|
1076
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1077
|
+
if (run.status === 'cancelled')
|
|
1078
|
+
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
1079
|
+
if (!SELF_ADVANCING_NODE_TYPES.has(node.type)) {
|
|
1080
|
+
const next = node.next;
|
|
1081
|
+
if (!next) {
|
|
1082
|
+
return { run, outcome: 'paused', reason: `node "${nodeId}" has no "next"; stopping`, executed };
|
|
1083
|
+
}
|
|
1084
|
+
run = this.moveNode(execId, next);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
cancelRun(execId, reason = 'Cancelled by user') {
|
|
1089
|
+
return this.atomicUpdate(execId, (run) => {
|
|
1090
|
+
if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
const now = new Date();
|
|
1094
|
+
run.status = 'cancelled';
|
|
1095
|
+
run.error = reason;
|
|
1096
|
+
run.audit = run.audit ?? [];
|
|
1097
|
+
run.audit.push({
|
|
1098
|
+
exec_id: run.id,
|
|
1099
|
+
node_id: run.current_node || '',
|
|
1100
|
+
type: 'cancel',
|
|
1101
|
+
summary: reason,
|
|
1102
|
+
started_at: now.toISOString(),
|
|
1103
|
+
ended_at: now.toISOString(),
|
|
1104
|
+
duration_ms: 0,
|
|
1105
|
+
exit_code: null,
|
|
1106
|
+
error: reason,
|
|
1107
|
+
trigger: 'manual',
|
|
1108
|
+
});
|
|
736
1109
|
});
|
|
737
1110
|
}
|
|
738
1111
|
reset(execId) {
|