workflow-ai 1.0.49 → 1.0.50
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/configs/pipeline.yaml +4 -4
- package/package.json +1 -1
- package/src/runner.mjs +96 -8
- package/templates/plan-template.md +1 -1
package/configs/pipeline.yaml
CHANGED
|
@@ -33,25 +33,25 @@ pipeline:
|
|
|
33
33
|
agents:
|
|
34
34
|
claude-sonnet:
|
|
35
35
|
command: "claude"
|
|
36
|
-
args: ["--model", "claude-sonnet-4-6", "--permission-mode", "bypassPermissions
|
|
36
|
+
args: ["--model", "claude-sonnet-4-6", "--permission-mode", "bypassPermissions", "-p"]
|
|
37
37
|
workdir: "."
|
|
38
38
|
description: "Claude Sonnet — быстрая модель для простых задач"
|
|
39
39
|
|
|
40
40
|
claude-opus:
|
|
41
41
|
command: "claude"
|
|
42
|
-
args: ["--model", "claude-opus-4-6", "--permission-mode", "bypassPermissions
|
|
42
|
+
args: ["--model", "claude-opus-4-6", "--permission-mode", "bypassPermissions", "-p"]
|
|
43
43
|
workdir: "."
|
|
44
44
|
description: "Claude Opus — мощная модель для сложных задач"
|
|
45
45
|
|
|
46
46
|
claude-coach:
|
|
47
47
|
command: "claude"
|
|
48
|
-
args: ["--model", "claude-opus-4-6", "--permission-mode", "bypassPermissions
|
|
48
|
+
args: ["--model", "claude-opus-4-6", "--permission-mode", "bypassPermissions", "-p", "coach"]
|
|
49
49
|
workdir: "."
|
|
50
50
|
description: "Claude Opus — coach"
|
|
51
51
|
|
|
52
52
|
claude-qa:
|
|
53
53
|
command: "claude"
|
|
54
|
-
args: ["--model", "claude-sonnet-4-6", "--permission-mode", "bypassPermissions
|
|
54
|
+
args: ["--model", "claude-sonnet-4-6", "--permission-mode", "bypassPermissions", "-p", "manual-testing"]
|
|
55
55
|
workdir: "."
|
|
56
56
|
description: "Claude Opus — QA"
|
|
57
57
|
|
package/package.json
CHANGED
package/src/runner.mjs
CHANGED
|
@@ -2,11 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import { spawn, execSync } from 'child_process';
|
|
5
|
+
import { spawn, execSync, exec } from 'child_process';
|
|
6
6
|
import crypto from 'crypto';
|
|
7
7
|
import yaml from './lib/js-yaml.mjs';
|
|
8
8
|
import { findProjectRoot } from './lib/find-root.mjs';
|
|
9
9
|
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// killProcessTree — убивает процесс и всех его потомков
|
|
12
|
+
// На Windows child.kill() не убивает дочерние процессы (npx → node → python),
|
|
13
|
+
// поэтому используем taskkill /T /F для уничтожения всего дерева.
|
|
14
|
+
// ============================================================================
|
|
15
|
+
function killProcessTree(child, logger = null) {
|
|
16
|
+
const pid = child.pid;
|
|
17
|
+
if (!pid) return;
|
|
18
|
+
|
|
19
|
+
if (process.platform === 'win32') {
|
|
20
|
+
try {
|
|
21
|
+
execSync(`taskkill /pid ${pid} /T /F`, { stdio: 'pipe' });
|
|
22
|
+
} catch {
|
|
23
|
+
// Процесс уже завершился — это нормально
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
// Unix: отправляем SIGTERM, через 5 сек — SIGKILL если ещё жив
|
|
27
|
+
try {
|
|
28
|
+
child.kill('SIGTERM');
|
|
29
|
+
} catch {
|
|
30
|
+
return; // процесс уже завершился
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setTimeout(() => {
|
|
34
|
+
try {
|
|
35
|
+
// Проверяем что процесс ещё жив (kill(0) не убивает, только проверяет)
|
|
36
|
+
process.kill(pid, 0);
|
|
37
|
+
child.kill('SIGKILL');
|
|
38
|
+
if (logger) {
|
|
39
|
+
logger.warn(`Process ${pid} did not exit after SIGTERM, sent SIGKILL`, 'ProcessCleanup');
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// Процесс уже завершился после SIGTERM — всё ок
|
|
43
|
+
}
|
|
44
|
+
}, 5000);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
10
48
|
// ============================================================================
|
|
11
49
|
// Logger — система логирования с уровнями DEBUG/INFO/WARN/ERROR
|
|
12
50
|
// ============================================================================
|
|
@@ -761,6 +799,19 @@ class StageExecutor {
|
|
|
761
799
|
// Инициализируем билдер и парсер
|
|
762
800
|
this.promptBuilder = new PromptBuilder(context, counters, previousResults);
|
|
763
801
|
this.resultParser = new ResultParser();
|
|
802
|
+
|
|
803
|
+
// Трекинг активных дочерних процессов для cleanup
|
|
804
|
+
this.activeChildren = new Set();
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Убивает все активные дочерние процессы
|
|
809
|
+
*/
|
|
810
|
+
killAllChildren() {
|
|
811
|
+
for (const child of this.activeChildren) {
|
|
812
|
+
killProcessTree(child, this.logger);
|
|
813
|
+
}
|
|
814
|
+
this.activeChildren.clear();
|
|
764
815
|
}
|
|
765
816
|
|
|
766
817
|
/**
|
|
@@ -904,6 +955,9 @@ class StageExecutor {
|
|
|
904
955
|
shell: useShell
|
|
905
956
|
});
|
|
906
957
|
|
|
958
|
+
// Регистрируем дочерний процесс для cleanup
|
|
959
|
+
this.activeChildren.add(child);
|
|
960
|
+
|
|
907
961
|
// Передаём промпт через stdin или закрываем если не нужно
|
|
908
962
|
if (useStdin) {
|
|
909
963
|
child.stdin.write(finalPrompt);
|
|
@@ -916,10 +970,11 @@ class StageExecutor {
|
|
|
916
970
|
let stderr = '';
|
|
917
971
|
let timedOut = false;
|
|
918
972
|
|
|
919
|
-
// Таймаут
|
|
973
|
+
// Таймаут — убиваем всё дерево процессов (на Windows child.kill не убивает внуков)
|
|
920
974
|
const timeoutId = setTimeout(() => {
|
|
921
975
|
timedOut = true;
|
|
922
|
-
child.
|
|
976
|
+
killProcessTree(child, this.logger);
|
|
977
|
+
this.activeChildren.delete(child);
|
|
923
978
|
if (this.logger) {
|
|
924
979
|
this.logger.timeout(stageId, timeout);
|
|
925
980
|
}
|
|
@@ -969,6 +1024,7 @@ class StageExecutor {
|
|
|
969
1024
|
|
|
970
1025
|
child.on('close', (code) => {
|
|
971
1026
|
clearTimeout(timeoutId);
|
|
1027
|
+
this.activeChildren.delete(child);
|
|
972
1028
|
// Обрабатываем остаток буфера стриминга
|
|
973
1029
|
if (stdoutBuffer.trim()) {
|
|
974
1030
|
try {
|
|
@@ -1002,8 +1058,16 @@ class StageExecutor {
|
|
|
1002
1058
|
// Парсим результат из вывода агента через ResultParser
|
|
1003
1059
|
const result = this.resultParser.parse(stdout, stageId);
|
|
1004
1060
|
|
|
1005
|
-
// Если exit code ≠ 0
|
|
1006
|
-
if (code !== 0) {
|
|
1061
|
+
// Если exit code ≠ 0, но результат уже распарсен — используем его
|
|
1062
|
+
if (code !== 0 && result.parsed && result.status && result.status !== 'default') {
|
|
1063
|
+
if (this.logger) {
|
|
1064
|
+
this.logger.warn(
|
|
1065
|
+
`Agent exited with code ${code}, but RESULT was parsed (status: ${result.status}). Using parsed result.`,
|
|
1066
|
+
stageId
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
// Проваливаемся в resolve ниже
|
|
1070
|
+
} else if (code !== 0) {
|
|
1007
1071
|
const err = new Error(`Agent exited with code ${code}`);
|
|
1008
1072
|
err.code = 'NON_ZERO_EXIT';
|
|
1009
1073
|
err.exitCode = code;
|
|
@@ -1032,6 +1096,7 @@ class StageExecutor {
|
|
|
1032
1096
|
|
|
1033
1097
|
child.on('error', (err) => {
|
|
1034
1098
|
clearTimeout(timeoutId);
|
|
1099
|
+
this.activeChildren.delete(child);
|
|
1035
1100
|
if (!timedOut) {
|
|
1036
1101
|
if (this.logger) {
|
|
1037
1102
|
this.logger.error(`CLI error: ${err.message}`, stageId);
|
|
@@ -1148,6 +1213,9 @@ class PipelineRunner {
|
|
|
1148
1213
|
this.fileGuard = new FileGuard(protectedPatterns, projectRoot, trustedAgents);
|
|
1149
1214
|
this.projectRoot = projectRoot;
|
|
1150
1215
|
|
|
1216
|
+
// Текущий executor для доступа к activeChildren при shutdown
|
|
1217
|
+
this.currentExecutor = null;
|
|
1218
|
+
|
|
1151
1219
|
// Настройка graceful shutdown
|
|
1152
1220
|
this.setupGracefulShutdown();
|
|
1153
1221
|
}
|
|
@@ -1242,8 +1310,9 @@ class PipelineRunner {
|
|
|
1242
1310
|
if (stage.type === 'update-counter') {
|
|
1243
1311
|
result = this.executeUpdateCounter(this.currentStage, stage);
|
|
1244
1312
|
} else {
|
|
1245
|
-
|
|
1246
|
-
result = await
|
|
1313
|
+
this.currentExecutor = new StageExecutor(this.config, this.context, this.counters, {}, this.fileGuard, this.logger, this.projectRoot);
|
|
1314
|
+
result = await this.currentExecutor.execute(this.currentStage);
|
|
1315
|
+
this.currentExecutor = null;
|
|
1247
1316
|
}
|
|
1248
1317
|
|
|
1249
1318
|
this.logger.info(`Stage ${this.currentStage} completed with status: ${result.status}`, 'PipelineRunner');
|
|
@@ -1427,13 +1496,32 @@ class PipelineRunner {
|
|
|
1427
1496
|
setupGracefulShutdown() {
|
|
1428
1497
|
const shutdown = (signal) => {
|
|
1429
1498
|
if (this.logger) {
|
|
1430
|
-
this.logger.info(`Received ${signal}.
|
|
1499
|
+
this.logger.info(`Received ${signal}. Killing child processes and shutting down...`, 'PipelineRunner');
|
|
1431
1500
|
}
|
|
1432
1501
|
this.running = false;
|
|
1502
|
+
// Убиваем все активные дочерние процессы (включая внуков на Windows)
|
|
1503
|
+
if (this.currentExecutor) {
|
|
1504
|
+
this.currentExecutor.killAllChildren();
|
|
1505
|
+
}
|
|
1433
1506
|
};
|
|
1434
1507
|
|
|
1435
1508
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
1436
1509
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
1510
|
+
|
|
1511
|
+
// Финальный cleanup при выходе процесса — синхронный, последний шанс убить детей
|
|
1512
|
+
process.on('exit', () => {
|
|
1513
|
+
if (this.currentExecutor && this.currentExecutor.activeChildren.size > 0) {
|
|
1514
|
+
for (const child of this.currentExecutor.activeChildren) {
|
|
1515
|
+
if (child.pid) {
|
|
1516
|
+
if (process.platform === 'win32') {
|
|
1517
|
+
try { execSync(`taskkill /pid ${child.pid} /T /F`, { stdio: 'pipe' }); } catch {}
|
|
1518
|
+
} else {
|
|
1519
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
});
|
|
1437
1525
|
}
|
|
1438
1526
|
}
|
|
1439
1527
|
|