workflow-ai 1.0.52 → 1.0.54

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.
@@ -124,6 +124,7 @@ paths:
124
124
  tickets: tickets
125
125
  reports: reports
126
126
  templates: templates
127
+ plan_templates: plans/templates
127
128
  archive: archive
128
129
 
129
130
  # Настройки отчётности
@@ -121,6 +121,12 @@ pipeline:
121
121
  workdir: "."
122
122
  description: "Скрипт для перемещения завершённых тикетов из in-progress/ в review/"
123
123
 
124
+ script-check-templates:
125
+ command: "node"
126
+ args: [".workflow/src/scripts/check-plan-templates.js"]
127
+ workdir: "."
128
+ description: "Проверка и активация шаблонов планов по триггерам"
129
+
124
130
 
125
131
  default_agent: qwen-code
126
132
 
@@ -185,9 +191,24 @@ pipeline:
185
191
  stage: move-to-review
186
192
  params:
187
193
  ticket_id: "$result.ticket_id"
188
- empty: check-plan-decomposition
194
+ empty: check-plan-templates
189
195
  error: create-report
190
196
 
197
+ # -------------------------------------------------------------------------
198
+ # 0a. check-plan-templates
199
+ # Проверяет шаблоны планов в plans/templates/.
200
+ # Если триггер сработал — создаёт план в plans/current/ со статусом approved.
201
+ # Всегда переходит на check-plan-decomposition.
202
+ # -------------------------------------------------------------------------
203
+ check-plan-templates:
204
+ description: "Проверить шаблоны планов и создать планы по триггерам"
205
+ agent: script-check-templates
206
+ goto:
207
+ plan_created: check-plan-decomposition
208
+ no_triggers: check-plan-decomposition
209
+ default: check-plan-decomposition
210
+ error: check-plan-decomposition
211
+
191
212
  # -------------------------------------------------------------------------
192
213
  # 0b. check-plan-decomposition
193
214
  # Если нет тикетов — проверяет, есть ли недекомпозированный план.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workflow-ai",
3
- "version": "1.0.52",
3
+ "version": "1.0.54",
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
@@ -8,15 +8,23 @@ import yaml from './lib/js-yaml.mjs';
8
8
  import { findProjectRoot } from './lib/find-root.mjs';
9
9
 
10
10
  // ============================================================================
11
- // ProcessTracker — snapshot-based process cleanup для Windows.
11
+ // ProcessTracker — файловый трекинг дочерних процессов.
12
12
  //
13
13
  // Проблема: runner spawn → claude CLI → npx → node (MCP-сервер).
14
- // При завершении claude CLI, MCP-серверы становятся сиротами.
15
- // taskkill /T не находит их, wmic parent-tree разорвано.
14
+ // При жёстком завершении runner'а (kill, закрытие терминала) process.on('exit')
15
+ // не срабатывает, MCP-серверы остаются сиротами навсегда.
16
16
  //
17
- // Решение: снимаем снапшот PID'ов ДО spawn, при cleanup берём текущие PID'ы,
18
- // разница = процессы порождённые нашим агентом (включая внуков и сирот).
17
+ // Решение: PID-файл (.workflow/logs/.runner-pids).
18
+ // - Перед spawn: записываем снапшот существующих PID'ов в файл
19
+ // - При старте runner'а: читаем файл, вычисляем сирот (текущие − снапшот), убиваем
20
+ // - Файл переживает крэш/kill — cleanup гарантирован при следующем запуске
19
21
  // ============================================================================
22
+ const PIDFILE_NAME = '.runner-pids';
23
+
24
+ function getPidFilePath(projectRoot) {
25
+ return path.resolve(projectRoot, '.workflow/logs', PIDFILE_NAME);
26
+ }
27
+
20
28
  function getProcessSnapshot() {
21
29
  if (process.platform !== 'win32') return new Set();
22
30
  try {
@@ -35,6 +43,80 @@ function getProcessSnapshot() {
35
43
  }
36
44
  }
37
45
 
46
+ /**
47
+ * Записывает снапшот + runner PID в файл.
48
+ * Формат: JSON { runnerPid, snapshot: [...], timestamp }
49
+ */
50
+ function writePidFile(pidFilePath, snapshot) {
51
+ try {
52
+ const dir = path.dirname(pidFilePath);
53
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
54
+ fs.writeFileSync(pidFilePath, JSON.stringify({
55
+ runnerPid: process.pid,
56
+ snapshot: [...snapshot],
57
+ timestamp: new Date().toISOString()
58
+ }));
59
+ } catch { /* non-critical */ }
60
+ }
61
+
62
+ /**
63
+ * Читает PID-файл. Возвращает { runnerPid, snapshot } или null.
64
+ */
65
+ function readPidFile(pidFilePath) {
66
+ try {
67
+ if (!fs.existsSync(pidFilePath)) return null;
68
+ const data = JSON.parse(fs.readFileSync(pidFilePath, 'utf-8'));
69
+ return {
70
+ runnerPid: data.runnerPid,
71
+ snapshot: new Set(data.snapshot || [])
72
+ };
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ function removePidFile(pidFilePath) {
79
+ try { fs.unlinkSync(pidFilePath); } catch { /* ok */ }
80
+ }
81
+
82
+ /**
83
+ * Cleanup сирот от предыдущего запуска.
84
+ * Читает PID-файл, берёт текущие процессы, убивает разницу (текущие − снапшот).
85
+ * Пропускает собственный PID runner'а.
86
+ */
87
+ function cleanupOrphansFromFile(pidFilePath, logger = null) {
88
+ const saved = readPidFile(pidFilePath);
89
+ if (!saved) return;
90
+
91
+ // Проверяем что предыдущий runner мёртв (иначе не трогаем — он ещё работает)
92
+ if (saved.runnerPid && isProcessAlive(saved.runnerPid)) {
93
+ return;
94
+ }
95
+
96
+ const currentPids = getProcessSnapshot();
97
+ const orphans = [];
98
+ for (const pid of currentPids) {
99
+ if (!saved.snapshot.has(pid) && pid !== process.pid) {
100
+ orphans.push(pid);
101
+ }
102
+ }
103
+
104
+ if (orphans.length > 0) {
105
+ killPids(orphans, logger);
106
+ }
107
+
108
+ removePidFile(pidFilePath);
109
+ }
110
+
111
+ function isProcessAlive(pid) {
112
+ try {
113
+ process.kill(pid, 0);
114
+ return true;
115
+ } catch {
116
+ return false;
117
+ }
118
+ }
119
+
38
120
  function killPids(pids, logger = null) {
39
121
  if (pids.length === 0) return;
40
122
  if (process.platform === 'win32') {
@@ -48,7 +130,7 @@ function killPids(pids, logger = null) {
48
130
  try { process.kill(pid, 'SIGKILL'); } catch { /* already dead */ }
49
131
  }
50
132
  }
51
- if (logger && pids.length > 0) {
133
+ if (logger) {
52
134
  logger.info(`Cleaned up ${pids.length} orphan process(es): ${pids.join(', ')}`, 'ProcessCleanup');
53
135
  }
54
136
  }
@@ -56,7 +138,6 @@ function killPids(pids, logger = null) {
56
138
  function killChildProcess(child) {
57
139
  if (!child.pid) return;
58
140
  if (process.platform === 'win32') {
59
- // taskkill /T /F — лучший effort для прямого дерева
60
141
  try { execSync(`taskkill /pid ${child.pid} /T /F`, { stdio: 'pipe' }); } catch {}
61
142
  } else {
62
143
  try { child.kill('SIGTERM'); } catch {}
@@ -1255,6 +1336,8 @@ class PipelineRunner {
1255
1336
 
1256
1337
  // Текущий executor для доступа к activeChildren при shutdown
1257
1338
  this.currentExecutor = null;
1339
+ // Путь к PID-файлу для cleanup сирот между запусками
1340
+ this.pidFilePath = getPidFilePath(projectRoot);
1258
1341
  // Снапшот PID'ов перед стартом пайплайна — для snapshot-based cleanup
1259
1342
  this.preRunSnapshot = new Set();
1260
1343
 
@@ -1323,8 +1406,12 @@ class PipelineRunner {
1323
1406
  const maxSteps = this.pipeline.execution?.max_steps || 100;
1324
1407
  const delayBetweenStages = this.pipeline.execution?.delay_between_stages || 5;
1325
1408
 
1326
- // Снапшот процессов до старта для snapshot-based cleanup при выходе
1409
+ // Cleanup сирот от предыдущего запуска (если runner был убит жёстко)
1410
+ cleanupOrphansFromFile(this.pidFilePath, this.logger);
1411
+
1412
+ // Снапшот процессов до старта + запись PID-файла
1327
1413
  this.preRunSnapshot = getProcessSnapshot();
1414
+ writePidFile(this.pidFilePath, this.preRunSnapshot);
1328
1415
 
1329
1416
  this.logger.info('=== Pipeline Runner Started ===', 'PipelineRunner');
1330
1417
  this.logger.info(`Entry stage: ${this.pipeline.entry}`, 'PipelineRunner');
@@ -1558,6 +1645,8 @@ class PipelineRunner {
1558
1645
  if (orphans.length > 0) {
1559
1646
  killPids(orphans, this.logger);
1560
1647
  }
1648
+ // PID-файл больше не нужен — cleanup выполнен
1649
+ removePidFile(this.pidFilePath);
1561
1650
  }
1562
1651
 
1563
1652
  setupGracefulShutdown() {