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.
@@ -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.50",
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,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.kill('SIGTERM');
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 это ошибка, которая может триггерить fallback
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
- const executor = new StageExecutor(this.config, this.context, this.counters, {}, this.fileGuard, this.logger, this.projectRoot);
1246
- result = await executor.execute(this.currentStage);
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}. Shutting down gracefully...`, 'PipelineRunner');
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
 
@@ -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