workflow-ai 1.0.49 → 1.0.51

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.
@@ -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 ", "-p"]
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 ", "-p"]
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 ", "-p", "coach"]
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 ", "-p", "manual-testing"]
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workflow-ai",
3
- "version": "1.0.49",
3
+ "version": "1.0.51",
4
4
  "description": "AI Agent Workflow Coordinator — kanban-based pipeline for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/src/runner.mjs CHANGED
@@ -2,11 +2,95 @@
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 не находит сирот (чей parent уже завершился).
14
+ // Поэтому: сначала собираем всё дерево PID через wmic, потом убиваем снизу вверх.
15
+ // ============================================================================
16
+ function getDescendantPids(pid) {
17
+ if (process.platform !== 'win32') return [];
18
+ try {
19
+ const output = execSync(
20
+ `wmic process where (ParentProcessId=${pid}) get ProcessId /FORMAT:LIST`,
21
+ { stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8' }
22
+ );
23
+ const pids = [];
24
+ for (const line of output.split('\n')) {
25
+ const match = line.match(/ProcessId=(\d+)/);
26
+ if (match) {
27
+ const childPid = parseInt(match[1], 10);
28
+ pids.push(childPid);
29
+ // Рекурсивно собираем внуков
30
+ pids.push(...getDescendantPids(childPid));
31
+ }
32
+ }
33
+ return pids;
34
+ } catch {
35
+ return [];
36
+ }
37
+ }
38
+
39
+ function killProcessTree(child, logger = null) {
40
+ const pid = child.pid;
41
+ if (!pid) return;
42
+
43
+ if (process.platform === 'win32') {
44
+ // Собираем всех потомков ДО убийства (иначе дерево разорвётся)
45
+ const allPids = [...getDescendantPids(pid), pid];
46
+
47
+ // Убиваем снизу вверх (сначала внуки, потом дети, потом root)
48
+ for (const p of allPids) {
49
+ try {
50
+ execSync(`taskkill /pid ${p} /F`, { stdio: 'pipe' });
51
+ } catch {
52
+ // Процесс уже завершился
53
+ }
54
+ }
55
+
56
+ if (logger && allPids.length > 1) {
57
+ logger.info(`Killed process tree: ${allPids.join(' → ')}`, 'ProcessCleanup');
58
+ }
59
+ } else {
60
+ // Unix: отправляем SIGTERM, через 5 сек — SIGKILL если ещё жив
61
+ try {
62
+ child.kill('SIGTERM');
63
+ } catch {
64
+ return;
65
+ }
66
+
67
+ setTimeout(() => {
68
+ try {
69
+ process.kill(pid, 0);
70
+ child.kill('SIGKILL');
71
+ if (logger) {
72
+ logger.warn(`Process ${pid} did not exit after SIGTERM, sent SIGKILL`, 'ProcessCleanup');
73
+ }
74
+ } catch {
75
+ // Процесс уже завершился после SIGTERM
76
+ }
77
+ }, 5000);
78
+ }
79
+ }
80
+
81
+ // killPid — убивает один PID (для cleanup сирот)
82
+ function killPid(pid) {
83
+ try {
84
+ if (process.platform === 'win32') {
85
+ execSync(`taskkill /pid ${pid} /F`, { stdio: 'pipe' });
86
+ } else {
87
+ process.kill(pid, 'SIGKILL');
88
+ }
89
+ } catch {
90
+ // уже завершился
91
+ }
92
+ }
93
+
10
94
  // ============================================================================
11
95
  // Logger — система логирования с уровнями DEBUG/INFO/WARN/ERROR
12
96
  // ============================================================================
@@ -761,6 +845,62 @@ class StageExecutor {
761
845
  // Инициализируем билдер и парсер
762
846
  this.promptBuilder = new PromptBuilder(context, counters, previousResults);
763
847
  this.resultParser = new ResultParser();
848
+
849
+ // Трекинг активных дочерних процессов для cleanup
850
+ this.activeChildren = new Set();
851
+ // Все PID'ы потомков (включая внуков), собранные при жизни процесса
852
+ this.trackedPids = new Set();
853
+ // Интервал для периодического сбора PID-дерева
854
+ this._pidScanInterval = null;
855
+ }
856
+
857
+ /**
858
+ * Начинает периодический сбор PID'ов потомков для всех активных child processes.
859
+ * Нужно потому что при завершении промежуточного процесса (npx, cmd.exe)
860
+ * дерево разрывается и потомки становятся сиротами — их уже не найти через parent PID.
861
+ */
862
+ _startPidTracking() {
863
+ if (this._pidScanInterval || process.platform !== 'win32') return;
864
+ this._pidScanInterval = setInterval(() => {
865
+ for (const child of this.activeChildren) {
866
+ if (child.pid) {
867
+ this.trackedPids.add(child.pid);
868
+ for (const pid of getDescendantPids(child.pid)) {
869
+ this.trackedPids.add(pid);
870
+ }
871
+ }
872
+ }
873
+ }, 3000);
874
+ }
875
+
876
+ /**
877
+ * Останавливает периодический сбор PID'ов
878
+ */
879
+ _stopPidTracking() {
880
+ if (this._pidScanInterval) {
881
+ clearInterval(this._pidScanInterval);
882
+ this._pidScanInterval = null;
883
+ }
884
+ }
885
+
886
+ /**
887
+ * Убивает все активные дочерние процессы и все ранее найденные PID'ы потомков
888
+ */
889
+ killAllChildren() {
890
+ this._stopPidTracking();
891
+
892
+ // Сначала убиваем известные child objects через killProcessTree
893
+ for (const child of this.activeChildren) {
894
+ killProcessTree(child, this.logger);
895
+ if (child.pid) this.trackedPids.delete(child.pid);
896
+ }
897
+ this.activeChildren.clear();
898
+
899
+ // Затем убиваем все ранее собранные PID'ы-сироты которые могли остаться
900
+ for (const pid of this.trackedPids) {
901
+ killPid(pid);
902
+ }
903
+ this.trackedPids.clear();
764
904
  }
765
905
 
766
906
  /**
@@ -904,6 +1044,11 @@ class StageExecutor {
904
1044
  shell: useShell
905
1045
  });
906
1046
 
1047
+ // Регистрируем дочерний процесс для cleanup
1048
+ this.activeChildren.add(child);
1049
+ if (child.pid) this.trackedPids.add(child.pid);
1050
+ this._startPidTracking();
1051
+
907
1052
  // Передаём промпт через stdin или закрываем если не нужно
908
1053
  if (useStdin) {
909
1054
  child.stdin.write(finalPrompt);
@@ -916,10 +1061,11 @@ class StageExecutor {
916
1061
  let stderr = '';
917
1062
  let timedOut = false;
918
1063
 
919
- // Таймаут
1064
+ // Таймаут — убиваем всё дерево процессов (на Windows child.kill не убивает внуков)
920
1065
  const timeoutId = setTimeout(() => {
921
1066
  timedOut = true;
922
- child.kill('SIGTERM');
1067
+ killProcessTree(child, this.logger);
1068
+ this.activeChildren.delete(child);
923
1069
  if (this.logger) {
924
1070
  this.logger.timeout(stageId, timeout);
925
1071
  }
@@ -969,6 +1115,8 @@ class StageExecutor {
969
1115
 
970
1116
  child.on('close', (code) => {
971
1117
  clearTimeout(timeoutId);
1118
+ this.activeChildren.delete(child);
1119
+ if (this.activeChildren.size === 0) this._stopPidTracking();
972
1120
  // Обрабатываем остаток буфера стриминга
973
1121
  if (stdoutBuffer.trim()) {
974
1122
  try {
@@ -1002,8 +1150,16 @@ class StageExecutor {
1002
1150
  // Парсим результат из вывода агента через ResultParser
1003
1151
  const result = this.resultParser.parse(stdout, stageId);
1004
1152
 
1005
- // Если exit code ≠ 0 это ошибка, которая может триггерить fallback
1006
- if (code !== 0) {
1153
+ // Если exit code ≠ 0, но результат уже распарсен используем его
1154
+ if (code !== 0 && result.parsed && result.status && result.status !== 'default') {
1155
+ if (this.logger) {
1156
+ this.logger.warn(
1157
+ `Agent exited with code ${code}, but RESULT was parsed (status: ${result.status}). Using parsed result.`,
1158
+ stageId
1159
+ );
1160
+ }
1161
+ // Проваливаемся в resolve ниже
1162
+ } else if (code !== 0) {
1007
1163
  const err = new Error(`Agent exited with code ${code}`);
1008
1164
  err.code = 'NON_ZERO_EXIT';
1009
1165
  err.exitCode = code;
@@ -1032,6 +1188,7 @@ class StageExecutor {
1032
1188
 
1033
1189
  child.on('error', (err) => {
1034
1190
  clearTimeout(timeoutId);
1191
+ this.activeChildren.delete(child);
1035
1192
  if (!timedOut) {
1036
1193
  if (this.logger) {
1037
1194
  this.logger.error(`CLI error: ${err.message}`, stageId);
@@ -1148,6 +1305,11 @@ class PipelineRunner {
1148
1305
  this.fileGuard = new FileGuard(protectedPatterns, projectRoot, trustedAgents);
1149
1306
  this.projectRoot = projectRoot;
1150
1307
 
1308
+ // Текущий executor для доступа к activeChildren при shutdown
1309
+ this.currentExecutor = null;
1310
+ // Все PID'ы потомков всех executor'ов — для cleanup сирот при выходе
1311
+ this.allTrackedPids = new Set();
1312
+
1151
1313
  // Настройка graceful shutdown
1152
1314
  this.setupGracefulShutdown();
1153
1315
  }
@@ -1242,8 +1404,13 @@ class PipelineRunner {
1242
1404
  if (stage.type === 'update-counter') {
1243
1405
  result = this.executeUpdateCounter(this.currentStage, stage);
1244
1406
  } else {
1245
- const executor = new StageExecutor(this.config, this.context, this.counters, {}, this.fileGuard, this.logger, this.projectRoot);
1246
- result = await executor.execute(this.currentStage);
1407
+ this.currentExecutor = new StageExecutor(this.config, this.context, this.counters, {}, this.fileGuard, this.logger, this.projectRoot);
1408
+ result = await this.currentExecutor.execute(this.currentStage);
1409
+ // Сохраняем tracked PIDs для cleanup сирот при выходе
1410
+ for (const pid of this.currentExecutor.trackedPids) {
1411
+ this.allTrackedPids.add(pid);
1412
+ }
1413
+ this.currentExecutor = null;
1247
1414
  }
1248
1415
 
1249
1416
  this.logger.info(`Stage ${this.currentStage} completed with status: ${result.status}`, 'PipelineRunner');
@@ -1427,13 +1594,53 @@ class PipelineRunner {
1427
1594
  setupGracefulShutdown() {
1428
1595
  const shutdown = (signal) => {
1429
1596
  if (this.logger) {
1430
- this.logger.info(`Received ${signal}. Shutting down gracefully...`, 'PipelineRunner');
1597
+ this.logger.info(`Received ${signal}. Killing child processes and shutting down...`, 'PipelineRunner');
1431
1598
  }
1432
1599
  this.running = false;
1600
+ // Убиваем все активные дочерние процессы (включая внуков на Windows)
1601
+ if (this.currentExecutor) {
1602
+ this.currentExecutor.killAllChildren();
1603
+ }
1604
+ // Убиваем сирот от предыдущих stage'ей
1605
+ for (const pid of this.allTrackedPids) {
1606
+ killPid(pid);
1607
+ }
1608
+ this.allTrackedPids.clear();
1433
1609
  };
1434
1610
 
1435
1611
  process.on('SIGINT', () => shutdown('SIGINT'));
1436
1612
  process.on('SIGTERM', () => shutdown('SIGTERM'));
1613
+
1614
+ // Финальный cleanup при выходе процесса — синхронный, последний шанс убить детей и сирот
1615
+ process.on('exit', () => {
1616
+ // Убиваем активные child objects (если shutdown во время работы stage)
1617
+ if (this.currentExecutor) {
1618
+ for (const child of this.currentExecutor.activeChildren) {
1619
+ if (child.pid) {
1620
+ if (process.platform === 'win32') {
1621
+ try { execSync(`taskkill /pid ${child.pid} /T /F`, { stdio: 'pipe' }); } catch {}
1622
+ } else {
1623
+ try { child.kill('SIGKILL'); } catch {}
1624
+ }
1625
+ }
1626
+ }
1627
+ // Добавляем PID'ы текущего executor'а
1628
+ for (const pid of this.currentExecutor.trackedPids) {
1629
+ this.allTrackedPids.add(pid);
1630
+ }
1631
+ }
1632
+
1633
+ // Убиваем все ранее собранные PID'ы потомков (сироты от всех stage'ей)
1634
+ for (const pid of this.allTrackedPids) {
1635
+ try {
1636
+ if (process.platform === 'win32') {
1637
+ execSync(`taskkill /pid ${pid} /F`, { stdio: 'pipe' });
1638
+ } else {
1639
+ process.kill(pid, 'SIGKILL');
1640
+ }
1641
+ } catch {}
1642
+ }
1643
+ });
1437
1644
  }
1438
1645
  }
1439
1646
 
@@ -3,7 +3,7 @@
3
3
 
4
4
  id: "PLAN-{NNN}"
5
5
  title: "Название плана"
6
- status: draft # draft | active | completed | archived
6
+ status: draft # draft | approved | active | completed | archived
7
7
  author: architect
8
8
 
9
9
  created_at: "" # ISO 8601