workflow-ai 1.0.60 → 1.0.62
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/agent-templates/CLAUDE.md.tpl +58 -58
- package/agent-templates/QWEN.md.tpl +58 -58
- package/package.json +1 -1
- package/src/init.mjs +437 -437
- package/src/runner.mjs +1735 -1713
- package/src/scripts/archive-plan-tickets.js +102 -0
- package/src/scripts/check-anomalies.js +161 -0
- package/src/scripts/check-conditions.js +258 -0
- package/src/scripts/check-mcp.js +277 -0
- package/src/scripts/check-plan-decomposed.js +217 -0
- package/src/scripts/check-plan-templates.js +297 -0
- package/src/scripts/check-relevance.js +311 -0
- package/src/scripts/complete-plan.js +106 -0
- package/src/scripts/get-next-id.js +214 -0
- package/src/scripts/move-ticket.js +260 -0
- package/src/scripts/move-to-ready.js +115 -0
- package/src/scripts/move-to-review.js +151 -0
- package/src/scripts/pick-next-task.js +791 -0
- package/templates/ticket-template.md +3 -10
package/src/runner.mjs
CHANGED
|
@@ -1,1713 +1,1735 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { spawn, execSync } from 'child_process';
|
|
6
|
-
import crypto from 'crypto';
|
|
7
|
-
import yaml from './lib/js-yaml.mjs';
|
|
8
|
-
import { findProjectRoot } from './lib/find-root.mjs';
|
|
9
|
-
|
|
10
|
-
// ============================================================================
|
|
11
|
-
// Logger — система логирования с уровнями DEBUG/INFO/WARN/ERROR
|
|
12
|
-
// ============================================================================
|
|
13
|
-
class Logger {
|
|
14
|
-
static LEVELS = {
|
|
15
|
-
DEBUG: -1,
|
|
16
|
-
INFO: 0,
|
|
17
|
-
WARN: 1,
|
|
18
|
-
ERROR: 2
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
static COLORS = {
|
|
22
|
-
DEBUG: '\x1b[90m', // gray
|
|
23
|
-
INFO: '\x1b[36m', // cyan
|
|
24
|
-
WARN: '\x1b[33m', // yellow
|
|
25
|
-
ERROR: '\x1b[31m', // red
|
|
26
|
-
RESET: '\x1b[0m'
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
constructor(logFilePath, consoleLevel = Logger.LEVELS.INFO) {
|
|
30
|
-
this.logFilePath = logFilePath;
|
|
31
|
-
this.consoleLevel = consoleLevel;
|
|
32
|
-
this.stats = {
|
|
33
|
-
debug: 0,
|
|
34
|
-
info: 0,
|
|
35
|
-
warn: 0,
|
|
36
|
-
error: 0,
|
|
37
|
-
stagesStarted: 0,
|
|
38
|
-
stagesCompleted: 0,
|
|
39
|
-
stagesFailed: 0,
|
|
40
|
-
cliCalls: 0,
|
|
41
|
-
gotoTransitions: 0,
|
|
42
|
-
retries: 0,
|
|
43
|
-
startTime: null,
|
|
44
|
-
endTime: null
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Создаёт директорию для логов если она не существует
|
|
50
|
-
*/
|
|
51
|
-
_ensureLogDirectory() {
|
|
52
|
-
const logDir = path.dirname(this.logFilePath);
|
|
53
|
-
if (!fs.existsSync(logDir)) {
|
|
54
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
55
|
-
console.log(`[Logger] Created log directory: ${logDir}`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Открывает файл для записи (append mode)
|
|
61
|
-
*/
|
|
62
|
-
_openFile() {
|
|
63
|
-
// Создаём директорию и файл если не существуют
|
|
64
|
-
this._ensureLogDirectory();
|
|
65
|
-
if (!fs.existsSync(this.logFilePath)) {
|
|
66
|
-
fs.writeFileSync(this.logFilePath, '');
|
|
67
|
-
}
|
|
68
|
-
this.stats.startTime = new Date();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Инициализирует logger
|
|
73
|
-
*/
|
|
74
|
-
async init() {
|
|
75
|
-
this._openFile();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Форматирует timestamp для логов
|
|
80
|
-
*/
|
|
81
|
-
_formatTimestamp() {
|
|
82
|
-
const now = new Date();
|
|
83
|
-
return now.toISOString().replace('T', ' ').substring(0, 19);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Форматирует сообщение для вывода
|
|
88
|
-
*/
|
|
89
|
-
_formatMessage(level, stage, message) {
|
|
90
|
-
const timestamp = this._formatTimestamp();
|
|
91
|
-
const stageTag = stage ? `[${stage}]` : '[Runner]';
|
|
92
|
-
const prefix = `[${timestamp}] [${level}] ${stageTag} `;
|
|
93
|
-
const lines = message.split('\n');
|
|
94
|
-
if (lines.length === 1) {
|
|
95
|
-
return `${prefix}${message}`;
|
|
96
|
-
}
|
|
97
|
-
const indent = ' '.repeat(prefix.length);
|
|
98
|
-
return lines.map((line, i) => i === 0 ? `${prefix}${line}` : `${indent}${line}`).join('\n');
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Записывает лог в файл (синхронно)
|
|
103
|
-
*/
|
|
104
|
-
_writeToFile(formattedMessage) {
|
|
105
|
-
fs.appendFileSync(this.logFilePath, formattedMessage + '\n', 'utf8');
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Выводит в консоль с цветом
|
|
110
|
-
*/
|
|
111
|
-
_writeToConsole(formattedMessage, level) {
|
|
112
|
-
if (Logger.LEVELS[level] < this.consoleLevel) {
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const color = Logger.COLORS[level];
|
|
117
|
-
const reset = Logger.COLORS.RESET;
|
|
118
|
-
|
|
119
|
-
if (level === 'ERROR') {
|
|
120
|
-
console.error(`${color}${formattedMessage}${reset}`);
|
|
121
|
-
} else if (level === 'WARN') {
|
|
122
|
-
console.warn(`${color}${formattedMessage}${reset}`);
|
|
123
|
-
} else {
|
|
124
|
-
console.log(`${color}${formattedMessage}${reset}`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Базовый метод логирования
|
|
130
|
-
*/
|
|
131
|
-
_log(level, stage, message) {
|
|
132
|
-
const formattedMessage = this._formatMessage(level, stage, message);
|
|
133
|
-
this._writeToFile(formattedMessage);
|
|
134
|
-
this._writeToConsole(formattedMessage, level);
|
|
135
|
-
|
|
136
|
-
// Обновляем статистику
|
|
137
|
-
if (level === 'DEBUG') this.stats.debug++;
|
|
138
|
-
else if (level === 'INFO') this.stats.info++;
|
|
139
|
-
else if (level === 'WARN') this.stats.warn++;
|
|
140
|
-
else if (level === 'ERROR') this.stats.error++;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Логгирует INFO сообщение
|
|
145
|
-
*/
|
|
146
|
-
info(message, stage) {
|
|
147
|
-
this._log('INFO', stage, message);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Логгирует WARN сообщение
|
|
152
|
-
*/
|
|
153
|
-
warn(message, stage) {
|
|
154
|
-
this._log('WARN', stage, message);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Логгирует ERROR сообщение
|
|
159
|
-
*/
|
|
160
|
-
error(message, stage) {
|
|
161
|
-
this._log('ERROR', stage, message);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Логгирует DEBUG сообщение
|
|
166
|
-
*/
|
|
167
|
-
debug(message, stage) {
|
|
168
|
-
this._log('DEBUG', stage, message);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Логгирует старт stage
|
|
173
|
-
*/
|
|
174
|
-
stageStart(stageId, agentId, skillId) {
|
|
175
|
-
this.stats.stagesStarted++;
|
|
176
|
-
this.info(`START stage="${stageId}" agent="${agentId}" skill="${skillId}"`, stageId);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Логгирует завершение stage
|
|
181
|
-
*/
|
|
182
|
-
stageComplete(stageId, status, exitCode) {
|
|
183
|
-
this.stats.stagesCompleted++;
|
|
184
|
-
this.info(`COMPLETE stage="${stageId}" status="${status}" exitCode=${exitCode}`, stageId);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Логгирует ошибку stage
|
|
189
|
-
*/
|
|
190
|
-
stageError(stageId, errorMessage) {
|
|
191
|
-
this.stats.stagesFailed++;
|
|
192
|
-
this.error(`ERROR stage="${stageId}" message="${errorMessage}"`, stageId);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Логгирует goto переход
|
|
197
|
-
*/
|
|
198
|
-
gotoTransition(fromStage, toStage, status, params = {}) {
|
|
199
|
-
this.stats.gotoTransitions++;
|
|
200
|
-
const paramsStr = Object.keys(params).length > 0 ? ` params=${JSON.stringify(params)}` : '';
|
|
201
|
-
this.info(`GOTO ${fromStage} → ${toStage} status="${status}"${paramsStr}`, fromStage);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Логгирует вызов CLI
|
|
206
|
-
*/
|
|
207
|
-
cliCall(command, args, exitCode) {
|
|
208
|
-
this.stats.cliCalls++;
|
|
209
|
-
this.info(`CLI command="${command}" args="${args.join(' ')}" exitCode=${exitCode}`, 'CLI');
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Логгирует retry попытку
|
|
214
|
-
*/
|
|
215
|
-
retry(stageId, attempt, maxAttempts) {
|
|
216
|
-
this.stats.retries++;
|
|
217
|
-
this.warn(`RETRY stage="${stageId}" attempt=${attempt}/${maxAttempts}`, stageId);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Логгирует таймаут
|
|
222
|
-
*/
|
|
223
|
-
timeout(stageId, timeoutSeconds) {
|
|
224
|
-
this.error(`TIMEOUT stage="${stageId}" after ${timeoutSeconds}s`, stageId);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Записывает итоговый summary
|
|
229
|
-
*/
|
|
230
|
-
writeSummary() {
|
|
231
|
-
this.stats.endTime = new Date();
|
|
232
|
-
const duration = this.stats.endTime - this.stats.startTime;
|
|
233
|
-
|
|
234
|
-
const summary = [
|
|
235
|
-
'',
|
|
236
|
-
'═══════════════════════════════════════════════════════════',
|
|
237
|
-
' PIPELINE SUMMARY',
|
|
238
|
-
'═══════════════════════════════════════════════════════════',
|
|
239
|
-
'',
|
|
240
|
-
`Duration: ${(duration / 1000).toFixed(2)}s`,
|
|
241
|
-
'',
|
|
242
|
-
'┌─────────────────────────────────────────────────────────┐',
|
|
243
|
-
'│ LOG STATISTICS │',
|
|
244
|
-
'├─────────────────────────────────────────────────────────┤',
|
|
245
|
-
`│ DEBUG messages: ${String(this.stats.debug).padEnd(34)}│`,
|
|
246
|
-
`│ INFO messages: ${String(this.stats.info).padEnd(34)}│`,
|
|
247
|
-
`│ WARN messages: ${String(this.stats.warn).padEnd(34)}│`,
|
|
248
|
-
`│ ERROR messages: ${String(this.stats.error).padEnd(34)}│`,
|
|
249
|
-
'├─────────────────────────────────────────────────────────┤',
|
|
250
|
-
'│ STAGE STATISTICS │',
|
|
251
|
-
'├─────────────────────────────────────────────────────────┤',
|
|
252
|
-
`│ Stages started: ${String(this.stats.stagesStarted).padEnd(34)}│`,
|
|
253
|
-
`│ Stages completed: ${String(this.stats.stagesCompleted).padEnd(34)}│`,
|
|
254
|
-
`│ Stages failed: ${String(this.stats.stagesFailed).padEnd(34)}│`,
|
|
255
|
-
'├─────────────────────────────────────────────────────────┤',
|
|
256
|
-
'│ ACTIVITY STATISTICS │',
|
|
257
|
-
'├─────────────────────────────────────────────────────────┤',
|
|
258
|
-
`│ CLI calls: ${String(this.stats.cliCalls).padEnd(34)}│`,
|
|
259
|
-
`│ GOTO transitions: ${String(this.stats.gotoTransitions).padEnd(34)}│`,
|
|
260
|
-
`│ Retries: ${String(this.stats.retries).padEnd(34)}│`,
|
|
261
|
-
'└─────────────────────────────────────────────────────────┘',
|
|
262
|
-
'',
|
|
263
|
-
'═══════════════════════════════════════════════════════════'
|
|
264
|
-
].join('\n');
|
|
265
|
-
|
|
266
|
-
// Вывод summary в консоль (всегда, независимо от уровня)
|
|
267
|
-
console.log(summary);
|
|
268
|
-
|
|
269
|
-
// Запись summary в файл
|
|
270
|
-
this._writeToFile(summary);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// ============================================================================
|
|
276
|
-
// PromptBuilder — формирует промпты для CLI-агентов с подстановкой контекста
|
|
277
|
-
// ============================================================================
|
|
278
|
-
class PromptBuilder {
|
|
279
|
-
constructor(context, counters, previousResults = {}) {
|
|
280
|
-
this.context = context;
|
|
281
|
-
this.counters = counters;
|
|
282
|
-
this.previousResults = previousResults;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Формирует промпт для агента на основе skill инструкции
|
|
287
|
-
* @param {object} stage - Stage из конфигурации
|
|
288
|
-
* @param {string} stageId - ID stage
|
|
289
|
-
* @returns {string} Промпт для агента
|
|
290
|
-
*/
|
|
291
|
-
build(stage, stageId) {
|
|
292
|
-
const parts = [stage.skill || stageId];
|
|
293
|
-
|
|
294
|
-
// Добавляем контекст если есть непустые значения
|
|
295
|
-
const contextEntries = Object.entries(this.context)
|
|
296
|
-
.filter(([_, v]) => v !== undefined && v !== null && v !== '');
|
|
297
|
-
if (contextEntries.length > 0) {
|
|
298
|
-
parts.push('\n\nContext:');
|
|
299
|
-
for (const [key, value] of contextEntries) {
|
|
300
|
-
parts.push(` ${key}: ${value}`);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Добавляем счётчики если есть
|
|
305
|
-
const counterEntries = Object.entries(this.counters)
|
|
306
|
-
.filter(([_, v]) => v > 0);
|
|
307
|
-
if (counterEntries.length > 0) {
|
|
308
|
-
parts.push('\nCounters:');
|
|
309
|
-
for (const [key, value] of counterEntries) {
|
|
310
|
-
parts.push(` ${key}: ${value}`);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Добавляем блок Instructions если поле instructions задано и непустое
|
|
315
|
-
if (stage.instructions && typeof stage.instructions === 'string' && stage.instructions.trim() !== '') {
|
|
316
|
-
parts.push('\n\nInstructions:');
|
|
317
|
-
parts.push(this.interpolate(stage.instructions.trim()));
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
return parts.join('\n');
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Форматирует контекст для вывода
|
|
325
|
-
*/
|
|
326
|
-
formatContext() {
|
|
327
|
-
const entries = Object.entries(this.context)
|
|
328
|
-
.filter(([_, v]) => v !== undefined && v !== null && v !== '')
|
|
329
|
-
.map(([k, v]) => ` ${k}: ${v}`);
|
|
330
|
-
return entries.length > 0 ? entries.join('\n') : ' (пусто)';
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Форматирует счётчики для вывода
|
|
335
|
-
*/
|
|
336
|
-
formatCounters() {
|
|
337
|
-
const entries = Object.entries(this.counters)
|
|
338
|
-
.map(([k, v]) => ` ${k}: ${v}`);
|
|
339
|
-
return entries.length > 0 ? entries.join('\n') : ' (пусто)';
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Форматирует результаты предыдущих stages
|
|
344
|
-
*/
|
|
345
|
-
formatPreviousResults() {
|
|
346
|
-
const entries = Object.entries(this.previousResults)
|
|
347
|
-
.map(([k, v]) => ` ${k}: ${JSON.stringify(v)}`);
|
|
348
|
-
return entries.length > 0 ? entries.join('\n') : ' (нет результатов)';
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Интерполирует переменные в строке
|
|
353
|
-
* Поддерживает: $result.field, $context.field, $counter.field
|
|
354
|
-
* @param {string} template - Строка с переменными
|
|
355
|
-
* @param {object} resultData - Данные результата для $result.*
|
|
356
|
-
* @returns {string} Строка с подставленными значениями
|
|
357
|
-
*/
|
|
358
|
-
interpolate(template, resultData = {}) {
|
|
359
|
-
if (typeof template !== 'string') {
|
|
360
|
-
return template;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
let resolved = template;
|
|
364
|
-
|
|
365
|
-
// $result.* - подстановка из результата
|
|
366
|
-
resolved = resolved.replace(/\$result\.(\w+)/g, (_, key) => {
|
|
367
|
-
return resultData[key] !== undefined ? resultData[key] : '';
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
// $context.* - подстановка из контекста
|
|
371
|
-
resolved = resolved.replace(/\$context\.(\w+)/g, (_, key) => {
|
|
372
|
-
return this.context[key] !== undefined ? this.context[key] : '';
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
// $counter.* - подстановка из счётчиков
|
|
376
|
-
resolved = resolved.replace(/\$counter\.(\w+)/g, (_, key) => {
|
|
377
|
-
return this.counters[key] !== undefined ? this.counters[key] : 0;
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
return resolved;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// ============================================================================
|
|
385
|
-
// ResultParser — парсит вывод агентов и извлекает структурированные данные
|
|
386
|
-
// ============================================================================
|
|
387
|
-
class ResultParser {
|
|
388
|
-
// Карта нормализации статусов: синонимы → каноническое значение
|
|
389
|
-
static STATUS_ALIASES = {
|
|
390
|
-
pass: 'passed',
|
|
391
|
-
approved: 'passed',
|
|
392
|
-
success: 'passed',
|
|
393
|
-
succeeded: 'passed',
|
|
394
|
-
ok: 'passed',
|
|
395
|
-
accepted: 'passed',
|
|
396
|
-
lgtm: 'passed',
|
|
397
|
-
fixed: 'passed',
|
|
398
|
-
resolved: 'passed',
|
|
399
|
-
fail: 'failed',
|
|
400
|
-
rejected: 'failed',
|
|
401
|
-
denied: 'failed',
|
|
402
|
-
not_passed: 'failed',
|
|
403
|
-
err: 'error',
|
|
404
|
-
crash: 'error',
|
|
405
|
-
timeout: 'error',
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Нормализует статус: приводит синонимы к каноническому значению
|
|
410
|
-
* @param {string} status
|
|
411
|
-
* @returns {string}
|
|
412
|
-
*/
|
|
413
|
-
normalizeStatus(status) {
|
|
414
|
-
const lower = status.toLowerCase();
|
|
415
|
-
const canonical = ResultParser.STATUS_ALIASES[lower];
|
|
416
|
-
if (canonical) {
|
|
417
|
-
console.log(`[ResultParser] Normalized status: "${status}" → "${canonical}"`);
|
|
418
|
-
return canonical;
|
|
419
|
-
}
|
|
420
|
-
return status;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* Парсит вывод агента и извлекает результат между маркерами
|
|
425
|
-
* @param {string} output - stdout агента
|
|
426
|
-
* @param {string} stageId - ID stage для логирования
|
|
427
|
-
* @returns {{status: string, data: object, raw: string}}
|
|
428
|
-
*/
|
|
429
|
-
parse(output, stageId) {
|
|
430
|
-
const marker = '---RESULT---';
|
|
431
|
-
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
const endIdx =
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
*
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
*
|
|
654
|
-
* @
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
for (const
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
if (
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
//
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
//
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
//
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
*
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
if (
|
|
1277
|
-
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
this.logger
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
if (
|
|
1344
|
-
this.
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
(
|
|
1462
|
-
params.
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
//
|
|
1713
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { spawn, execSync } from 'child_process';
|
|
6
|
+
import crypto from 'crypto';
|
|
7
|
+
import yaml from './lib/js-yaml.mjs';
|
|
8
|
+
import { findProjectRoot } from './lib/find-root.mjs';
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Logger — система логирования с уровнями DEBUG/INFO/WARN/ERROR
|
|
12
|
+
// ============================================================================
|
|
13
|
+
class Logger {
|
|
14
|
+
static LEVELS = {
|
|
15
|
+
DEBUG: -1,
|
|
16
|
+
INFO: 0,
|
|
17
|
+
WARN: 1,
|
|
18
|
+
ERROR: 2
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
static COLORS = {
|
|
22
|
+
DEBUG: '\x1b[90m', // gray
|
|
23
|
+
INFO: '\x1b[36m', // cyan
|
|
24
|
+
WARN: '\x1b[33m', // yellow
|
|
25
|
+
ERROR: '\x1b[31m', // red
|
|
26
|
+
RESET: '\x1b[0m'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
constructor(logFilePath, consoleLevel = Logger.LEVELS.INFO) {
|
|
30
|
+
this.logFilePath = logFilePath;
|
|
31
|
+
this.consoleLevel = consoleLevel;
|
|
32
|
+
this.stats = {
|
|
33
|
+
debug: 0,
|
|
34
|
+
info: 0,
|
|
35
|
+
warn: 0,
|
|
36
|
+
error: 0,
|
|
37
|
+
stagesStarted: 0,
|
|
38
|
+
stagesCompleted: 0,
|
|
39
|
+
stagesFailed: 0,
|
|
40
|
+
cliCalls: 0,
|
|
41
|
+
gotoTransitions: 0,
|
|
42
|
+
retries: 0,
|
|
43
|
+
startTime: null,
|
|
44
|
+
endTime: null
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Создаёт директорию для логов если она не существует
|
|
50
|
+
*/
|
|
51
|
+
_ensureLogDirectory() {
|
|
52
|
+
const logDir = path.dirname(this.logFilePath);
|
|
53
|
+
if (!fs.existsSync(logDir)) {
|
|
54
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
55
|
+
console.log(`[Logger] Created log directory: ${logDir}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Открывает файл для записи (append mode)
|
|
61
|
+
*/
|
|
62
|
+
_openFile() {
|
|
63
|
+
// Создаём директорию и файл если не существуют
|
|
64
|
+
this._ensureLogDirectory();
|
|
65
|
+
if (!fs.existsSync(this.logFilePath)) {
|
|
66
|
+
fs.writeFileSync(this.logFilePath, '');
|
|
67
|
+
}
|
|
68
|
+
this.stats.startTime = new Date();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Инициализирует logger
|
|
73
|
+
*/
|
|
74
|
+
async init() {
|
|
75
|
+
this._openFile();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Форматирует timestamp для логов
|
|
80
|
+
*/
|
|
81
|
+
_formatTimestamp() {
|
|
82
|
+
const now = new Date();
|
|
83
|
+
return now.toISOString().replace('T', ' ').substring(0, 19);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Форматирует сообщение для вывода
|
|
88
|
+
*/
|
|
89
|
+
_formatMessage(level, stage, message) {
|
|
90
|
+
const timestamp = this._formatTimestamp();
|
|
91
|
+
const stageTag = stage ? `[${stage}]` : '[Runner]';
|
|
92
|
+
const prefix = `[${timestamp}] [${level}] ${stageTag} `;
|
|
93
|
+
const lines = message.split('\n');
|
|
94
|
+
if (lines.length === 1) {
|
|
95
|
+
return `${prefix}${message}`;
|
|
96
|
+
}
|
|
97
|
+
const indent = ' '.repeat(prefix.length);
|
|
98
|
+
return lines.map((line, i) => i === 0 ? `${prefix}${line}` : `${indent}${line}`).join('\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Записывает лог в файл (синхронно)
|
|
103
|
+
*/
|
|
104
|
+
_writeToFile(formattedMessage) {
|
|
105
|
+
fs.appendFileSync(this.logFilePath, formattedMessage + '\n', 'utf8');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Выводит в консоль с цветом
|
|
110
|
+
*/
|
|
111
|
+
_writeToConsole(formattedMessage, level) {
|
|
112
|
+
if (Logger.LEVELS[level] < this.consoleLevel) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const color = Logger.COLORS[level];
|
|
117
|
+
const reset = Logger.COLORS.RESET;
|
|
118
|
+
|
|
119
|
+
if (level === 'ERROR') {
|
|
120
|
+
console.error(`${color}${formattedMessage}${reset}`);
|
|
121
|
+
} else if (level === 'WARN') {
|
|
122
|
+
console.warn(`${color}${formattedMessage}${reset}`);
|
|
123
|
+
} else {
|
|
124
|
+
console.log(`${color}${formattedMessage}${reset}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Базовый метод логирования
|
|
130
|
+
*/
|
|
131
|
+
_log(level, stage, message) {
|
|
132
|
+
const formattedMessage = this._formatMessage(level, stage, message);
|
|
133
|
+
this._writeToFile(formattedMessage);
|
|
134
|
+
this._writeToConsole(formattedMessage, level);
|
|
135
|
+
|
|
136
|
+
// Обновляем статистику
|
|
137
|
+
if (level === 'DEBUG') this.stats.debug++;
|
|
138
|
+
else if (level === 'INFO') this.stats.info++;
|
|
139
|
+
else if (level === 'WARN') this.stats.warn++;
|
|
140
|
+
else if (level === 'ERROR') this.stats.error++;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Логгирует INFO сообщение
|
|
145
|
+
*/
|
|
146
|
+
info(message, stage) {
|
|
147
|
+
this._log('INFO', stage, message);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Логгирует WARN сообщение
|
|
152
|
+
*/
|
|
153
|
+
warn(message, stage) {
|
|
154
|
+
this._log('WARN', stage, message);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Логгирует ERROR сообщение
|
|
159
|
+
*/
|
|
160
|
+
error(message, stage) {
|
|
161
|
+
this._log('ERROR', stage, message);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Логгирует DEBUG сообщение
|
|
166
|
+
*/
|
|
167
|
+
debug(message, stage) {
|
|
168
|
+
this._log('DEBUG', stage, message);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Логгирует старт stage
|
|
173
|
+
*/
|
|
174
|
+
stageStart(stageId, agentId, skillId) {
|
|
175
|
+
this.stats.stagesStarted++;
|
|
176
|
+
this.info(`START stage="${stageId}" agent="${agentId}" skill="${skillId}"`, stageId);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Логгирует завершение stage
|
|
181
|
+
*/
|
|
182
|
+
stageComplete(stageId, status, exitCode) {
|
|
183
|
+
this.stats.stagesCompleted++;
|
|
184
|
+
this.info(`COMPLETE stage="${stageId}" status="${status}" exitCode=${exitCode}`, stageId);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Логгирует ошибку stage
|
|
189
|
+
*/
|
|
190
|
+
stageError(stageId, errorMessage) {
|
|
191
|
+
this.stats.stagesFailed++;
|
|
192
|
+
this.error(`ERROR stage="${stageId}" message="${errorMessage}"`, stageId);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Логгирует goto переход
|
|
197
|
+
*/
|
|
198
|
+
gotoTransition(fromStage, toStage, status, params = {}) {
|
|
199
|
+
this.stats.gotoTransitions++;
|
|
200
|
+
const paramsStr = Object.keys(params).length > 0 ? ` params=${JSON.stringify(params)}` : '';
|
|
201
|
+
this.info(`GOTO ${fromStage} → ${toStage} status="${status}"${paramsStr}`, fromStage);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Логгирует вызов CLI
|
|
206
|
+
*/
|
|
207
|
+
cliCall(command, args, exitCode) {
|
|
208
|
+
this.stats.cliCalls++;
|
|
209
|
+
this.info(`CLI command="${command}" args="${args.join(' ')}" exitCode=${exitCode}`, 'CLI');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Логгирует retry попытку
|
|
214
|
+
*/
|
|
215
|
+
retry(stageId, attempt, maxAttempts) {
|
|
216
|
+
this.stats.retries++;
|
|
217
|
+
this.warn(`RETRY stage="${stageId}" attempt=${attempt}/${maxAttempts}`, stageId);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Логгирует таймаут
|
|
222
|
+
*/
|
|
223
|
+
timeout(stageId, timeoutSeconds) {
|
|
224
|
+
this.error(`TIMEOUT stage="${stageId}" after ${timeoutSeconds}s`, stageId);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Записывает итоговый summary
|
|
229
|
+
*/
|
|
230
|
+
writeSummary() {
|
|
231
|
+
this.stats.endTime = new Date();
|
|
232
|
+
const duration = this.stats.endTime - this.stats.startTime;
|
|
233
|
+
|
|
234
|
+
const summary = [
|
|
235
|
+
'',
|
|
236
|
+
'═══════════════════════════════════════════════════════════',
|
|
237
|
+
' PIPELINE SUMMARY',
|
|
238
|
+
'═══════════════════════════════════════════════════════════',
|
|
239
|
+
'',
|
|
240
|
+
`Duration: ${(duration / 1000).toFixed(2)}s`,
|
|
241
|
+
'',
|
|
242
|
+
'┌─────────────────────────────────────────────────────────┐',
|
|
243
|
+
'│ LOG STATISTICS │',
|
|
244
|
+
'├─────────────────────────────────────────────────────────┤',
|
|
245
|
+
`│ DEBUG messages: ${String(this.stats.debug).padEnd(34)}│`,
|
|
246
|
+
`│ INFO messages: ${String(this.stats.info).padEnd(34)}│`,
|
|
247
|
+
`│ WARN messages: ${String(this.stats.warn).padEnd(34)}│`,
|
|
248
|
+
`│ ERROR messages: ${String(this.stats.error).padEnd(34)}│`,
|
|
249
|
+
'├─────────────────────────────────────────────────────────┤',
|
|
250
|
+
'│ STAGE STATISTICS │',
|
|
251
|
+
'├─────────────────────────────────────────────────────────┤',
|
|
252
|
+
`│ Stages started: ${String(this.stats.stagesStarted).padEnd(34)}│`,
|
|
253
|
+
`│ Stages completed: ${String(this.stats.stagesCompleted).padEnd(34)}│`,
|
|
254
|
+
`│ Stages failed: ${String(this.stats.stagesFailed).padEnd(34)}│`,
|
|
255
|
+
'├─────────────────────────────────────────────────────────┤',
|
|
256
|
+
'│ ACTIVITY STATISTICS │',
|
|
257
|
+
'├─────────────────────────────────────────────────────────┤',
|
|
258
|
+
`│ CLI calls: ${String(this.stats.cliCalls).padEnd(34)}│`,
|
|
259
|
+
`│ GOTO transitions: ${String(this.stats.gotoTransitions).padEnd(34)}│`,
|
|
260
|
+
`│ Retries: ${String(this.stats.retries).padEnd(34)}│`,
|
|
261
|
+
'└─────────────────────────────────────────────────────────┘',
|
|
262
|
+
'',
|
|
263
|
+
'═══════════════════════════════════════════════════════════'
|
|
264
|
+
].join('\n');
|
|
265
|
+
|
|
266
|
+
// Вывод summary в консоль (всегда, независимо от уровня)
|
|
267
|
+
console.log(summary);
|
|
268
|
+
|
|
269
|
+
// Запись summary в файл
|
|
270
|
+
this._writeToFile(summary);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ============================================================================
|
|
276
|
+
// PromptBuilder — формирует промпты для CLI-агентов с подстановкой контекста
|
|
277
|
+
// ============================================================================
|
|
278
|
+
class PromptBuilder {
|
|
279
|
+
constructor(context, counters, previousResults = {}) {
|
|
280
|
+
this.context = context;
|
|
281
|
+
this.counters = counters;
|
|
282
|
+
this.previousResults = previousResults;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Формирует промпт для агента на основе skill инструкции
|
|
287
|
+
* @param {object} stage - Stage из конфигурации
|
|
288
|
+
* @param {string} stageId - ID stage
|
|
289
|
+
* @returns {string} Промпт для агента
|
|
290
|
+
*/
|
|
291
|
+
build(stage, stageId) {
|
|
292
|
+
const parts = [stage.skill || stageId];
|
|
293
|
+
|
|
294
|
+
// Добавляем контекст если есть непустые значения
|
|
295
|
+
const contextEntries = Object.entries(this.context)
|
|
296
|
+
.filter(([_, v]) => v !== undefined && v !== null && v !== '');
|
|
297
|
+
if (contextEntries.length > 0) {
|
|
298
|
+
parts.push('\n\nContext:');
|
|
299
|
+
for (const [key, value] of contextEntries) {
|
|
300
|
+
parts.push(` ${key}: ${value}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Добавляем счётчики если есть
|
|
305
|
+
const counterEntries = Object.entries(this.counters)
|
|
306
|
+
.filter(([_, v]) => v > 0);
|
|
307
|
+
if (counterEntries.length > 0) {
|
|
308
|
+
parts.push('\nCounters:');
|
|
309
|
+
for (const [key, value] of counterEntries) {
|
|
310
|
+
parts.push(` ${key}: ${value}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Добавляем блок Instructions если поле instructions задано и непустое
|
|
315
|
+
if (stage.instructions && typeof stage.instructions === 'string' && stage.instructions.trim() !== '') {
|
|
316
|
+
parts.push('\n\nInstructions:');
|
|
317
|
+
parts.push(this.interpolate(stage.instructions.trim()));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return parts.join('\n');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Форматирует контекст для вывода
|
|
325
|
+
*/
|
|
326
|
+
formatContext() {
|
|
327
|
+
const entries = Object.entries(this.context)
|
|
328
|
+
.filter(([_, v]) => v !== undefined && v !== null && v !== '')
|
|
329
|
+
.map(([k, v]) => ` ${k}: ${v}`);
|
|
330
|
+
return entries.length > 0 ? entries.join('\n') : ' (пусто)';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Форматирует счётчики для вывода
|
|
335
|
+
*/
|
|
336
|
+
formatCounters() {
|
|
337
|
+
const entries = Object.entries(this.counters)
|
|
338
|
+
.map(([k, v]) => ` ${k}: ${v}`);
|
|
339
|
+
return entries.length > 0 ? entries.join('\n') : ' (пусто)';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Форматирует результаты предыдущих stages
|
|
344
|
+
*/
|
|
345
|
+
formatPreviousResults() {
|
|
346
|
+
const entries = Object.entries(this.previousResults)
|
|
347
|
+
.map(([k, v]) => ` ${k}: ${JSON.stringify(v)}`);
|
|
348
|
+
return entries.length > 0 ? entries.join('\n') : ' (нет результатов)';
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Интерполирует переменные в строке
|
|
353
|
+
* Поддерживает: $result.field, $context.field, $counter.field
|
|
354
|
+
* @param {string} template - Строка с переменными
|
|
355
|
+
* @param {object} resultData - Данные результата для $result.*
|
|
356
|
+
* @returns {string} Строка с подставленными значениями
|
|
357
|
+
*/
|
|
358
|
+
interpolate(template, resultData = {}) {
|
|
359
|
+
if (typeof template !== 'string') {
|
|
360
|
+
return template;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let resolved = template;
|
|
364
|
+
|
|
365
|
+
// $result.* - подстановка из результата
|
|
366
|
+
resolved = resolved.replace(/\$result\.(\w+)/g, (_, key) => {
|
|
367
|
+
return resultData[key] !== undefined ? resultData[key] : '';
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// $context.* - подстановка из контекста
|
|
371
|
+
resolved = resolved.replace(/\$context\.(\w+)/g, (_, key) => {
|
|
372
|
+
return this.context[key] !== undefined ? this.context[key] : '';
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// $counter.* - подстановка из счётчиков
|
|
376
|
+
resolved = resolved.replace(/\$counter\.(\w+)/g, (_, key) => {
|
|
377
|
+
return this.counters[key] !== undefined ? this.counters[key] : 0;
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
return resolved;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ============================================================================
|
|
385
|
+
// ResultParser — парсит вывод агентов и извлекает структурированные данные
|
|
386
|
+
// ============================================================================
|
|
387
|
+
class ResultParser {
|
|
388
|
+
// Карта нормализации статусов: синонимы → каноническое значение
|
|
389
|
+
static STATUS_ALIASES = {
|
|
390
|
+
pass: 'passed',
|
|
391
|
+
approved: 'passed',
|
|
392
|
+
success: 'passed',
|
|
393
|
+
succeeded: 'passed',
|
|
394
|
+
ok: 'passed',
|
|
395
|
+
accepted: 'passed',
|
|
396
|
+
lgtm: 'passed',
|
|
397
|
+
fixed: 'passed',
|
|
398
|
+
resolved: 'passed',
|
|
399
|
+
fail: 'failed',
|
|
400
|
+
rejected: 'failed',
|
|
401
|
+
denied: 'failed',
|
|
402
|
+
not_passed: 'failed',
|
|
403
|
+
err: 'error',
|
|
404
|
+
crash: 'error',
|
|
405
|
+
timeout: 'error',
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Нормализует статус: приводит синонимы к каноническому значению
|
|
410
|
+
* @param {string} status
|
|
411
|
+
* @returns {string}
|
|
412
|
+
*/
|
|
413
|
+
normalizeStatus(status) {
|
|
414
|
+
const lower = status.toLowerCase();
|
|
415
|
+
const canonical = ResultParser.STATUS_ALIASES[lower];
|
|
416
|
+
if (canonical) {
|
|
417
|
+
console.log(`[ResultParser] Normalized status: "${status}" → "${canonical}"`);
|
|
418
|
+
return canonical;
|
|
419
|
+
}
|
|
420
|
+
return status;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Парсит вывод агента и извлекает результат между маркерами
|
|
425
|
+
* @param {string} output - stdout агента
|
|
426
|
+
* @param {string} stageId - ID stage для логирования
|
|
427
|
+
* @returns {{status: string, data: object, raw: string}}
|
|
428
|
+
*/
|
|
429
|
+
parse(output, stageId) {
|
|
430
|
+
const marker = '---RESULT---';
|
|
431
|
+
|
|
432
|
+
// Ищем ПОСЛЕДНЮЮ пару маркеров: printResult всегда печатается в конце скрипта,
|
|
433
|
+
// а маркер может случайно встретиться в логах до него (напр. в заголовке тикета).
|
|
434
|
+
const endIdx = output.lastIndexOf(marker);
|
|
435
|
+
const startIdx = endIdx !== -1 ? output.lastIndexOf(marker, endIdx - 1) : -1;
|
|
436
|
+
|
|
437
|
+
if (startIdx !== -1 && endIdx !== -1 && startIdx !== endIdx) {
|
|
438
|
+
// Найдены маркеры — парсим структурированный блок
|
|
439
|
+
const resultBlock = output.substring(startIdx + marker.length, endIdx).trim();
|
|
440
|
+
const data = this.parseResultBlock(resultBlock);
|
|
441
|
+
|
|
442
|
+
const normalizedStatus = this.normalizeStatus(data.status || 'default');
|
|
443
|
+
console.log(`[ResultParser] Parsed structured result for ${stageId}: status=${normalizedStatus}`);
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
status: normalizedStatus,
|
|
447
|
+
data: data.data || {},
|
|
448
|
+
raw: output,
|
|
449
|
+
parsed: true
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Fallback: пытаемся парсить текстовый вывод
|
|
454
|
+
console.log(`[ResultParser] No result markers found for ${stageId}, attempting fallback parsing`);
|
|
455
|
+
return this.fallbackParse(output, stageId);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Парсит блок результата в формате key: value с поддержкой многострочных YAML-значений.
|
|
460
|
+
* При обнаружении ключа без значения (key:) читает последующие индентированные строки
|
|
461
|
+
* как тело значения до следующего ключа верхнего уровня (строки без indent).
|
|
462
|
+
* @param {string} block - Текстовый блок результата
|
|
463
|
+
* @returns {{status: string, data: object}}
|
|
464
|
+
*/
|
|
465
|
+
parseResultBlock(block) {
|
|
466
|
+
const lines = block.split('\n');
|
|
467
|
+
const data = {};
|
|
468
|
+
let status = 'default';
|
|
469
|
+
let currentKey = null;
|
|
470
|
+
let multilineValue = null;
|
|
471
|
+
|
|
472
|
+
const flushMultiline = () => {
|
|
473
|
+
if (currentKey !== null && multilineValue !== null) {
|
|
474
|
+
// Убираем trailing newline, сохраняем сырой YAML-блок
|
|
475
|
+
data[currentKey] = multilineValue.replace(/\n$/, '');
|
|
476
|
+
currentKey = null;
|
|
477
|
+
multilineValue = null;
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
for (let i = 0; i < lines.length; i++) {
|
|
482
|
+
const line = lines[i];
|
|
483
|
+
|
|
484
|
+
// Проверяем: строка верхнего уровня (без indent) с ключом
|
|
485
|
+
const topLevelMatch = line.match(/^([^:\s][^:]*):\s*(.*)$/);
|
|
486
|
+
|
|
487
|
+
if (topLevelMatch) {
|
|
488
|
+
// Если копим многострочное значение — сбрасываем
|
|
489
|
+
flushMultiline();
|
|
490
|
+
|
|
491
|
+
const key = topLevelMatch[1].trim();
|
|
492
|
+
const value = topLevelMatch[2].trim();
|
|
493
|
+
|
|
494
|
+
if (value !== '') {
|
|
495
|
+
// Однострочное key: value — как прежде
|
|
496
|
+
if (key === 'status') {
|
|
497
|
+
status = value;
|
|
498
|
+
} else {
|
|
499
|
+
data[key] = value;
|
|
500
|
+
}
|
|
501
|
+
} else {
|
|
502
|
+
// Ключ без значения — потенциальное многострочное YAML-значение
|
|
503
|
+
currentKey = key;
|
|
504
|
+
multilineValue = '';
|
|
505
|
+
}
|
|
506
|
+
} else if (currentKey !== null && (line.startsWith(' ') || line.startsWith('\t') || line === '')) {
|
|
507
|
+
// Индентированная строка (или пустая) — накапливаем как тело multiline-значения
|
|
508
|
+
multilineValue += line + '\n';
|
|
509
|
+
} else if (currentKey !== null) {
|
|
510
|
+
// Строка без indent и не key: value — конец multiline-блока
|
|
511
|
+
flushMultiline();
|
|
512
|
+
}
|
|
513
|
+
// Игнорируем строки без ключа верхнего уровня если не в multiline-режиме
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Сбрасываем последнее multiline-значение
|
|
517
|
+
flushMultiline();
|
|
518
|
+
|
|
519
|
+
return { status, data };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Fallback-парсинг для вывода без маркеров
|
|
524
|
+
* Пытается извлечь статус из текстового вывода
|
|
525
|
+
* @param {string} output - stdout агента
|
|
526
|
+
* @param {string} stageId - ID stage для логирования
|
|
527
|
+
* @returns {{status: string, data: object, raw: string}}
|
|
528
|
+
*/
|
|
529
|
+
fallbackParse(output, stageId) {
|
|
530
|
+
const lines = output.split('\n');
|
|
531
|
+
let status = 'default';
|
|
532
|
+
const extractedData = {};
|
|
533
|
+
let inResultSection = false;
|
|
534
|
+
|
|
535
|
+
// Ищем паттерны вида "status: xxx" или "Status: xxx" в любом месте вывода
|
|
536
|
+
for (const line of lines) {
|
|
537
|
+
const trimmedLine = line.trim();
|
|
538
|
+
|
|
539
|
+
// Паттерн для извлечения статуса
|
|
540
|
+
const statusMatch = trimmedLine.match(/^(?:status|Status):\s*(\w+)/i);
|
|
541
|
+
if (statusMatch) {
|
|
542
|
+
status = statusMatch[1];
|
|
543
|
+
inResultSection = true;
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Если нашли статус, пытаемся извлечь дополнительные данные
|
|
548
|
+
if (inResultSection) {
|
|
549
|
+
const dataMatch = trimmedLine.match(/^(\w+):\s*(.+)$/i);
|
|
550
|
+
if (dataMatch && dataMatch[1].toLowerCase() !== 'status') {
|
|
551
|
+
extractedData[dataMatch[1]] = dataMatch[2];
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Если статус не найден, пытаемся определить по ключевым словам
|
|
557
|
+
if (status === 'default') {
|
|
558
|
+
const lowerOutput = output.toLowerCase();
|
|
559
|
+
if (lowerOutput.includes('completed') || lowerOutput.includes('success') || lowerOutput.includes('done')) {
|
|
560
|
+
status = 'default';
|
|
561
|
+
extractedData._inferred = 'success_keywords';
|
|
562
|
+
} else if (lowerOutput.includes('error') || lowerOutput.includes('failed')) {
|
|
563
|
+
status = 'error';
|
|
564
|
+
extractedData._inferred = 'error_keywords';
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const normalizedStatus = this.normalizeStatus(status);
|
|
569
|
+
console.log(`[ResultParser] Fallback parsing for ${stageId}: status=${normalizedStatus}`);
|
|
570
|
+
|
|
571
|
+
return {
|
|
572
|
+
status: normalizedStatus,
|
|
573
|
+
data: extractedData,
|
|
574
|
+
raw: output,
|
|
575
|
+
parsed: false
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// ============================================================================
|
|
581
|
+
// FileGuard — защита файлов от несанкционированного изменения агентами
|
|
582
|
+
// ============================================================================
|
|
583
|
+
class FileGuard {
|
|
584
|
+
constructor(patterns, projectRoot = process.cwd(), trustedAgents = [], trustedStages = []) {
|
|
585
|
+
this.enabled = patterns && patterns.length > 0;
|
|
586
|
+
this.snapshots = new Map();
|
|
587
|
+
this.patterns = (patterns || []).map(p => {
|
|
588
|
+
if (typeof p === 'string') {
|
|
589
|
+
return { pattern: p.replace(/\\/g, '/'), mode: 'full' };
|
|
590
|
+
}
|
|
591
|
+
return { pattern: p.pattern.replace(/\\/g, '/'), mode: p.mode || 'full' };
|
|
592
|
+
});
|
|
593
|
+
// projectRoot — корневая директория проекта, относительно которой указаны паттерны
|
|
594
|
+
this.projectRoot = projectRoot;
|
|
595
|
+
// Доверенные агенты — для них FileGuard не откатывает изменения
|
|
596
|
+
this.trustedAgents = trustedAgents;
|
|
597
|
+
// Доверенные стейджи — для них FileGuard не откатывает изменения
|
|
598
|
+
this.trustedStages = trustedStages;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Проверяет, является ли агент или стейдж доверенным (пропускает FileGuard)
|
|
603
|
+
* Поддерживает glob-паттерны: "script-*" соответствует "script-move", "script-pick" и т.д.
|
|
604
|
+
* @param {string} agentId - ID агента
|
|
605
|
+
* @param {string} [stageId] - ID стейджа (опционально)
|
|
606
|
+
* @returns {boolean}
|
|
607
|
+
*/
|
|
608
|
+
isTrusted(agentId, stageId) {
|
|
609
|
+
// Проверка по trustedAgents (glob-паттерны)
|
|
610
|
+
const agentMatch = this.trustedAgents.some(pattern => {
|
|
611
|
+
if (pattern.endsWith('*')) {
|
|
612
|
+
return agentId.startsWith(pattern.slice(0, -1));
|
|
613
|
+
}
|
|
614
|
+
return agentId === pattern;
|
|
615
|
+
});
|
|
616
|
+
if (agentMatch) return true;
|
|
617
|
+
|
|
618
|
+
// Проверка по trustedStages (точное совпадение)
|
|
619
|
+
if (stageId && this.trustedStages.includes(stageId)) {
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Проверяет, соответствует ли путь файла защищённым паттернам
|
|
628
|
+
* @param {string} filePath - Путь к файлу (нормализованный через /)
|
|
629
|
+
* @returns {boolean}
|
|
630
|
+
*/
|
|
631
|
+
matchesProtected(filePath) {
|
|
632
|
+
const relativePath = path.relative(this.projectRoot, filePath).replace(/\\/g, '/');
|
|
633
|
+
return this.patterns.some(p => this._matchGlob(relativePath, p.pattern));
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Glob-сопоставление: поддерживает * (в пределах директории) и ** (через директории)
|
|
638
|
+
* @param {string} filePath - Нормализованный путь
|
|
639
|
+
* @param {string} pattern - Glob-паттерн
|
|
640
|
+
* @returns {boolean}
|
|
641
|
+
*/
|
|
642
|
+
_matchGlob(filePath, pattern) {
|
|
643
|
+
const normalizedPattern = pattern.replace(/\\/g, '/');
|
|
644
|
+
const regexStr = normalizedPattern
|
|
645
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // экранируем regex-символы (кроме *)
|
|
646
|
+
.replace(/\*\*/g, '\x00') // ** → временный placeholder
|
|
647
|
+
.replace(/\*/g, '[^/]*') // * → совпадение внутри директории
|
|
648
|
+
.replace(/\x00/g, '.*'); // placeholder → .* (через директории)
|
|
649
|
+
return new RegExp('^' + regexStr + '$').test(filePath);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Извлекает базовую директорию из glob-паттерна (до первого wildcard)
|
|
654
|
+
* @param {string} pattern - Glob-паттерн
|
|
655
|
+
* @returns {string} Базовая директория
|
|
656
|
+
*/
|
|
657
|
+
_getBaseDir(pattern) {
|
|
658
|
+
const parts = pattern.replace(/\\/g, '/').split('/');
|
|
659
|
+
const nonWildcardParts = [];
|
|
660
|
+
for (const part of parts) {
|
|
661
|
+
if (part.includes('*')) break;
|
|
662
|
+
nonWildcardParts.push(part);
|
|
663
|
+
}
|
|
664
|
+
return nonWildcardParts.join('/') || '.';
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Рекурсивно получает все файлы в директории
|
|
669
|
+
* @param {string} dir - Директория для сканирования
|
|
670
|
+
* @returns {string[]} Список путей к файлам (нормализованных через /)
|
|
671
|
+
*/
|
|
672
|
+
_getAllFiles(dir) {
|
|
673
|
+
const files = [];
|
|
674
|
+
if (!fs.existsSync(dir)) return files;
|
|
675
|
+
const stats = fs.statSync(dir);
|
|
676
|
+
if (!stats.isDirectory()) return files;
|
|
677
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
678
|
+
for (const entry of entries) {
|
|
679
|
+
const entryPath = path.join(dir, entry.name).replace(/\\/g, '/');
|
|
680
|
+
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
681
|
+
files.push(...this._getAllFiles(entryPath));
|
|
682
|
+
} else {
|
|
683
|
+
files.push(entryPath);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return files;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Вычисляет SHA256-хэш содержимого файла
|
|
691
|
+
* @param {string} filePath - Путь к файлу
|
|
692
|
+
* @returns {string|null} Хэш или null если файл не существует
|
|
693
|
+
*/
|
|
694
|
+
_hashFile(filePath) {
|
|
695
|
+
if (!fs.existsSync(filePath)) return null;
|
|
696
|
+
const content = fs.readFileSync(filePath);
|
|
697
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Снимает snapshot защищённых файлов перед выполнением stage
|
|
702
|
+
*/
|
|
703
|
+
takeSnapshot() {
|
|
704
|
+
if (!this.enabled) return;
|
|
705
|
+
this.snapshots.clear();
|
|
706
|
+
|
|
707
|
+
for (const { pattern, mode } of this.patterns) {
|
|
708
|
+
if (!pattern.includes('*')) {
|
|
709
|
+
const absolutePath = path.resolve(this.projectRoot, pattern);
|
|
710
|
+
if (fs.existsSync(absolutePath)) {
|
|
711
|
+
if (mode === 'structure') {
|
|
712
|
+
this.snapshots.set(absolutePath, { hash: this._hashFile(absolutePath), content: fs.readFileSync(absolutePath, null), mode: 'structure' });
|
|
713
|
+
} else {
|
|
714
|
+
this.snapshots.set(absolutePath, this._hashFile(absolutePath));
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
} else {
|
|
718
|
+
const baseDir = path.resolve(this.projectRoot, this._getBaseDir(pattern));
|
|
719
|
+
const files = this._getAllFiles(baseDir);
|
|
720
|
+
for (const filePath of files) {
|
|
721
|
+
if (this.matchesProtected(filePath)) {
|
|
722
|
+
if (mode === 'structure') {
|
|
723
|
+
this.snapshots.set(filePath, { hash: this._hashFile(filePath), content: fs.readFileSync(filePath, null), mode: 'structure' });
|
|
724
|
+
} else {
|
|
725
|
+
this.snapshots.set(filePath, this._hashFile(filePath));
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
console.log(`[FileGuard] Snapshot taken: ${this.snapshots.size} protected files`);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Проверяет целостность защищённых файлов и откатывает несанкционированные изменения.
|
|
737
|
+
* Обнаруживает как изменения/удаления существующих файлов, так и создание новых.
|
|
738
|
+
* @returns {string[]} Список изменённых (и откаченных) файлов
|
|
739
|
+
*/
|
|
740
|
+
checkAndRollback() {
|
|
741
|
+
if (!this.enabled) return [];
|
|
742
|
+
|
|
743
|
+
const violations = [];
|
|
744
|
+
|
|
745
|
+
for (const [filePath, snapshot] of this.snapshots) {
|
|
746
|
+
const mode = snapshot.mode || 'full';
|
|
747
|
+
if (mode === 'structure') {
|
|
748
|
+
if (!fs.existsSync(filePath)) {
|
|
749
|
+
violations.push(filePath);
|
|
750
|
+
console.warn(`[FileGuard] WARNING: Protected file deleted: ${filePath}`);
|
|
751
|
+
try {
|
|
752
|
+
fs.writeFileSync(filePath, snapshot.content);
|
|
753
|
+
console.warn(`[FileGuard] WARNING: Restored deleted file: ${filePath}`);
|
|
754
|
+
} catch (err) {
|
|
755
|
+
console.error(`[FileGuard] ERROR: Failed to restore ${filePath}: ${err.message}`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
} else {
|
|
759
|
+
const currentHash = this._hashFile(filePath);
|
|
760
|
+
if (currentHash !== snapshot) {
|
|
761
|
+
violations.push(filePath);
|
|
762
|
+
console.warn(`[FileGuard] WARNING: Protected file modified: ${filePath}`);
|
|
763
|
+
this._rollbackFile(filePath);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
for (const { pattern, mode } of this.patterns) {
|
|
769
|
+
const baseDir = pattern.includes('*')
|
|
770
|
+
? path.resolve(this.projectRoot, this._getBaseDir(pattern))
|
|
771
|
+
: path.resolve(this.projectRoot, pattern);
|
|
772
|
+
|
|
773
|
+
const currentFiles = this._getAllFiles(baseDir);
|
|
774
|
+
for (const filePath of currentFiles) {
|
|
775
|
+
if (this.matchesProtected(filePath) && !this.snapshots.has(filePath)) {
|
|
776
|
+
violations.push(filePath);
|
|
777
|
+
console.warn(`[FileGuard] WARNING: New file in protected area: ${filePath}`);
|
|
778
|
+
this._removeNewFile(filePath);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (violations.length > 0) {
|
|
784
|
+
console.warn(`[FileGuard] WARNING: Rolled back ${violations.length} protected file(s): ${violations.join(', ')}`);
|
|
785
|
+
} else {
|
|
786
|
+
console.log('[FileGuard] No protected files were modified');
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return violations;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Удаляет файл, созданный агентом в защищённой директории
|
|
794
|
+
* @param {string} filePath - Путь к файлу
|
|
795
|
+
*/
|
|
796
|
+
_removeNewFile(filePath) {
|
|
797
|
+
try {
|
|
798
|
+
fs.unlinkSync(filePath);
|
|
799
|
+
console.warn(`[FileGuard] WARNING: Removed unauthorized new file: ${filePath}`);
|
|
800
|
+
} catch (err) {
|
|
801
|
+
console.error(`[FileGuard] ERROR: Failed to remove ${filePath}: ${err.message}`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Откатывает файл к последнему зафиксированному состоянию через git
|
|
807
|
+
* @param {string} filePath - Путь к файлу
|
|
808
|
+
*/
|
|
809
|
+
_rollbackFile(filePath) {
|
|
810
|
+
try {
|
|
811
|
+
execSync(`git checkout -- "${filePath}"`, { stdio: 'pipe' });
|
|
812
|
+
console.warn(`[FileGuard] WARNING: Rolled back: ${filePath}`);
|
|
813
|
+
} catch (err) {
|
|
814
|
+
const errMsg = err.stderr ? err.stderr.toString().trim() : err.message;
|
|
815
|
+
console.error(`[FileGuard] ERROR: Failed to rollback ${filePath}: ${errMsg}`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// ============================================================================
|
|
821
|
+
// StageExecutor — выполняет stages через вызов CLI-агентов
|
|
822
|
+
// ============================================================================
|
|
823
|
+
class StageExecutor {
|
|
824
|
+
constructor(config, context, counters, previousResults = {}, fileGuard = null, logger = null, projectRoot = process.cwd()) {
|
|
825
|
+
this.config = config;
|
|
826
|
+
this.context = context;
|
|
827
|
+
this.counters = counters;
|
|
828
|
+
this.previousResults = previousResults;
|
|
829
|
+
this.pipeline = config.pipeline;
|
|
830
|
+
this.projectRoot = projectRoot;
|
|
831
|
+
this.fileGuard = fileGuard;
|
|
832
|
+
this.logger = logger;
|
|
833
|
+
|
|
834
|
+
// Инициализируем билдер и парсер
|
|
835
|
+
this.promptBuilder = new PromptBuilder(context, counters, previousResults);
|
|
836
|
+
this.resultParser = new ResultParser();
|
|
837
|
+
|
|
838
|
+
// Текущий дочерний процесс агента (для kill при shutdown)
|
|
839
|
+
this.currentChild = null;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Убивает текущий дочерний процесс агента
|
|
844
|
+
*/
|
|
845
|
+
killCurrentChild() {
|
|
846
|
+
const child = this.currentChild;
|
|
847
|
+
if (!child || !child.pid) return;
|
|
848
|
+
if (process.platform === 'win32') {
|
|
849
|
+
try { execSync(`taskkill /pid ${child.pid} /T /F`, { stdio: 'pipe' }); } catch {}
|
|
850
|
+
} else {
|
|
851
|
+
try { child.kill('SIGTERM'); } catch {}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Строит список кандидатов-агентов для стейджа с учётом типа задачи,
|
|
857
|
+
* required_capabilities и номера попытки. Возвращает:
|
|
858
|
+
* { agentId, effectiveStage } — если кандидат найден,
|
|
859
|
+
* { blocked: 'no_capable_agent' | 'attempts_exhausted', reason } — иначе.
|
|
860
|
+
*
|
|
861
|
+
* Алгоритм:
|
|
862
|
+
* 1. Список берётся из stage.agents_by_type[task_type].agents,
|
|
863
|
+
* иначе stage.agents, иначе pipeline.default_agents.
|
|
864
|
+
* 2. Список фильтруется: агент должен покрывать все required_capabilities.
|
|
865
|
+
* 3. Берётся элемент [attempt-1] (1-based attempt).
|
|
866
|
+
* 4. Скрипт-агенты (stage.agent: script-*) обрабатываются в отдельной ветке
|
|
867
|
+
* execute() — сюда не попадают.
|
|
868
|
+
*/
|
|
869
|
+
resolveAgent(stage, stageId) {
|
|
870
|
+
const attempt = (stage.counter && this.counters[stage.counter]) || 1;
|
|
871
|
+
|
|
872
|
+
// Task type: явно из context либо из префикса ticket_id
|
|
873
|
+
const taskType = this.context.task_type
|
|
874
|
+
|| (this.context.ticket_id && this.context.ticket_id.split('-')[0].toLowerCase())
|
|
875
|
+
|| null;
|
|
876
|
+
|
|
877
|
+
// Выбор источника списка агентов и instructions
|
|
878
|
+
let agentIds;
|
|
879
|
+
let instructions = stage.instructions;
|
|
880
|
+
const byType = stage.agents_by_type && taskType && stage.agents_by_type[taskType];
|
|
881
|
+
if (byType && Array.isArray(byType.agents)) {
|
|
882
|
+
agentIds = byType.agents;
|
|
883
|
+
if (byType.instructions !== undefined) instructions = byType.instructions;
|
|
884
|
+
} else if (Array.isArray(stage.agents)) {
|
|
885
|
+
agentIds = stage.agents;
|
|
886
|
+
} else if (Array.isArray(this.pipeline.default_agents)) {
|
|
887
|
+
agentIds = this.pipeline.default_agents;
|
|
888
|
+
} else {
|
|
889
|
+
throw new Error(`Stage "${stageId}": no agents list and no default_agents`);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Требуемые capabilities тикета из context.required_capabilities.
|
|
893
|
+
// Приходит из pick-next-task.js как JSON-строка (из-за toString() в $context.*),
|
|
894
|
+
// либо уже как массив (при прямом задании в pipeline.context).
|
|
895
|
+
let required = [];
|
|
896
|
+
const raw = this.context.required_capabilities;
|
|
897
|
+
if (Array.isArray(raw)) {
|
|
898
|
+
required = raw;
|
|
899
|
+
} else if (typeof raw === 'string' && raw.trim() !== '') {
|
|
900
|
+
try {
|
|
901
|
+
const parsed = JSON.parse(raw);
|
|
902
|
+
if (Array.isArray(parsed)) required = parsed;
|
|
903
|
+
} catch {
|
|
904
|
+
// Не JSON — игнорируем
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Фильтр по capability-совместимости
|
|
909
|
+
const covers = (agentId) => {
|
|
910
|
+
const agent = this.pipeline.agents[agentId];
|
|
911
|
+
if (!agent) return false;
|
|
912
|
+
const caps = Array.isArray(agent.capabilities) ? agent.capabilities : [];
|
|
913
|
+
return required.every(r => caps.includes(r));
|
|
914
|
+
};
|
|
915
|
+
const compatible = agentIds.filter(covers);
|
|
916
|
+
|
|
917
|
+
if (compatible.length === 0) {
|
|
918
|
+
return {
|
|
919
|
+
blocked: 'no_capable_agent',
|
|
920
|
+
reason: `No agent in [${agentIds.join(', ')}] covers required_capabilities [${required.join(', ')}]`,
|
|
921
|
+
attempt
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Курсор = (attempt - 1), clamped
|
|
926
|
+
const cursor = attempt - 1;
|
|
927
|
+
if (cursor >= compatible.length) {
|
|
928
|
+
return {
|
|
929
|
+
blocked: 'attempts_exhausted',
|
|
930
|
+
reason: `Attempt ${attempt} exceeds compatible agents list length (${compatible.length})`,
|
|
931
|
+
attempt,
|
|
932
|
+
triedAgents: compatible
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const agentId = compatible[cursor];
|
|
937
|
+
// Клонируем stage с подменой instructions (для agents_by_type override)
|
|
938
|
+
const effectiveStage = { ...stage, instructions };
|
|
939
|
+
return { agentId, effectiveStage, attempt, compatible };
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Выполняет stage через выбранного CLI-агента (новая модель выбора).
|
|
944
|
+
* @param {string} stageId - ID stage из конфигурации
|
|
945
|
+
* @returns {Promise<{status: string, output: string, result?: object}>}
|
|
946
|
+
*/
|
|
947
|
+
async execute(stageId) {
|
|
948
|
+
const stage = this.pipeline.stages[stageId];
|
|
949
|
+
if (!stage) {
|
|
950
|
+
throw new Error(`Stage not found: ${stageId}`);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Legacy-ветка: скрипт-стейдж (детерминированный). Capability-фильтр не применяется.
|
|
954
|
+
if (stage.agent && !stage.agents) {
|
|
955
|
+
const agent = this.pipeline.agents[stage.agent];
|
|
956
|
+
if (!agent) throw new Error(`Agent not found: ${stage.agent}`);
|
|
957
|
+
const prompt = this.promptBuilder.build(stage, stageId);
|
|
958
|
+
if (this.logger) this.logger.stageStart(stageId, stage.agent, stage.skill);
|
|
959
|
+
|
|
960
|
+
const skipGuard = this.fileGuard && this.fileGuard.isTrusted(stage.agent, stageId);
|
|
961
|
+
if (this.fileGuard && !skipGuard) this.fileGuard.takeSnapshot();
|
|
962
|
+
|
|
963
|
+
const result = await this.callAgent(agent, prompt, stageId, stage.skill);
|
|
964
|
+
|
|
965
|
+
if (this.logger) this.logger.stageComplete(stageId, result.status, result.exitCode);
|
|
966
|
+
if (this.fileGuard && !skipGuard) {
|
|
967
|
+
const violations = this.fileGuard.checkAndRollback();
|
|
968
|
+
if (violations.length > 0) result.violations = violations;
|
|
969
|
+
}
|
|
970
|
+
return result;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Новая ветка: список кандидатов с фильтром по capabilities
|
|
974
|
+
const resolved = this.resolveAgent(stage, stageId);
|
|
975
|
+
if (resolved.blocked) {
|
|
976
|
+
if (this.logger) {
|
|
977
|
+
this.logger.error(
|
|
978
|
+
`Stage "${stageId}" blocked: ${resolved.blocked} — ${resolved.reason}`,
|
|
979
|
+
stageId
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
return {
|
|
983
|
+
status: 'blocked',
|
|
984
|
+
blocked_reason: resolved.blocked,
|
|
985
|
+
output: resolved.reason,
|
|
986
|
+
result: { blocked: resolved.blocked, reason: resolved.reason },
|
|
987
|
+
exitCode: 0,
|
|
988
|
+
parsed: false
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const { agentId, effectiveStage } = resolved;
|
|
993
|
+
const agent = this.pipeline.agents[agentId];
|
|
994
|
+
const prompt = this.promptBuilder.build(effectiveStage, stageId);
|
|
995
|
+
|
|
996
|
+
if (this.logger) {
|
|
997
|
+
this.logger.info(
|
|
998
|
+
`Agent selected: ${agentId} (attempt ${resolved.attempt}, compatible=[${resolved.compatible.join(', ')}])`,
|
|
999
|
+
stageId
|
|
1000
|
+
);
|
|
1001
|
+
this.logger.stageStart(stageId, agentId, effectiveStage.skill);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const skipGuard = this.fileGuard && this.fileGuard.isTrusted(agentId, stageId);
|
|
1005
|
+
if (this.fileGuard && !skipGuard) this.fileGuard.takeSnapshot();
|
|
1006
|
+
|
|
1007
|
+
const result = await this.callAgent(agent, prompt, stageId, effectiveStage.skill);
|
|
1008
|
+
|
|
1009
|
+
if (this.logger) this.logger.stageComplete(stageId, result.status, result.exitCode);
|
|
1010
|
+
if (this.fileGuard && !skipGuard) {
|
|
1011
|
+
const violations = this.fileGuard.checkAndRollback();
|
|
1012
|
+
if (violations.length > 0) result.violations = violations;
|
|
1013
|
+
}
|
|
1014
|
+
return result;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* Вызывает CLI-агента через child_process
|
|
1019
|
+
*/
|
|
1020
|
+
callAgent(agent, prompt, stageId, skillId) {
|
|
1021
|
+
return new Promise((resolve, reject) => {
|
|
1022
|
+
const timeout = this.pipeline.execution?.timeout_per_stage || 300;
|
|
1023
|
+
const args = [...agent.args];
|
|
1024
|
+
const finalPrompt = prompt;
|
|
1025
|
+
|
|
1026
|
+
// На Windows shell: true обрезает многострочные аргументы на \n (cmd.exe).
|
|
1027
|
+
// Поэтому передаём промпт через stdin, а -p (если есть) оставляем как флаг print mode.
|
|
1028
|
+
const useShell = process.platform === 'win32' && agent.command !== 'node';
|
|
1029
|
+
const useStdin = useShell && finalPrompt.includes('\n');
|
|
1030
|
+
|
|
1031
|
+
if (!useStdin) {
|
|
1032
|
+
// Однострочный промпт или не Windows — передаём через аргумент
|
|
1033
|
+
args.push(finalPrompt);
|
|
1034
|
+
}
|
|
1035
|
+
// Иначе промпт пойдёт через stdin, args остаются как есть.
|
|
1036
|
+
|
|
1037
|
+
// Логгируем команду перед запуском (вместо промпта — имя skill)
|
|
1038
|
+
if (this.logger) {
|
|
1039
|
+
this.logger.info(`RUN ${agent.command} ${[...args.slice(0, -1), skillId].join(' ')}`, stageId);
|
|
1040
|
+
// Логгируем входные параметры агента (context + counters)
|
|
1041
|
+
const promptLines = prompt.split('\n').filter(l => l.trim());
|
|
1042
|
+
if (promptLines.length > 1) {
|
|
1043
|
+
for (const line of promptLines.slice(1)) {
|
|
1044
|
+
this.logger.info(` ${line}`, stageId);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const child = spawn(agent.command, args, {
|
|
1050
|
+
cwd: path.resolve(this.projectRoot, agent.workdir || '.'),
|
|
1051
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1052
|
+
shell: useShell
|
|
1053
|
+
});
|
|
1054
|
+
this.currentChild = child;
|
|
1055
|
+
|
|
1056
|
+
// Передаём промпт через stdin или закрываем если не нужно
|
|
1057
|
+
if (useStdin) {
|
|
1058
|
+
child.stdin.write(finalPrompt);
|
|
1059
|
+
child.stdin.end();
|
|
1060
|
+
} else {
|
|
1061
|
+
child.stdin.end();
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
let stdout = '';
|
|
1065
|
+
let stderr = '';
|
|
1066
|
+
let timedOut = false;
|
|
1067
|
+
|
|
1068
|
+
// Таймаут
|
|
1069
|
+
const timeoutId = setTimeout(() => {
|
|
1070
|
+
timedOut = true;
|
|
1071
|
+
// На Windows SIGTERM игнорируется — используем taskkill /T /F для убийства дерева
|
|
1072
|
+
if (process.platform === 'win32' && child.pid) {
|
|
1073
|
+
try { execSync(`taskkill /pid ${child.pid} /T /F`, { stdio: 'pipe' }); } catch {}
|
|
1074
|
+
} else {
|
|
1075
|
+
child.kill('SIGTERM');
|
|
1076
|
+
}
|
|
1077
|
+
if (this.logger) {
|
|
1078
|
+
this.logger.timeout(stageId, timeout);
|
|
1079
|
+
}
|
|
1080
|
+
reject(new Error(`Stage "${stageId}" timed out after ${timeout}s`));
|
|
1081
|
+
}, timeout * 1000);
|
|
1082
|
+
|
|
1083
|
+
let stdoutBuffer = '';
|
|
1084
|
+
let agentText = ''; // собираем текстовый вывод агента для лога
|
|
1085
|
+
child.stdout.on('data', (data) => {
|
|
1086
|
+
const chunk = data.toString();
|
|
1087
|
+
stdout += chunk;
|
|
1088
|
+
// Парсим stream-json и выводим только текст дельт
|
|
1089
|
+
stdoutBuffer += chunk;
|
|
1090
|
+
const lines = stdoutBuffer.split('\n');
|
|
1091
|
+
stdoutBuffer = lines.pop(); // незавершённая строка остаётся в буфере
|
|
1092
|
+
for (const line of lines) {
|
|
1093
|
+
if (!line.trim()) continue;
|
|
1094
|
+
try {
|
|
1095
|
+
const obj = JSON.parse(line);
|
|
1096
|
+
// Claude: content_block_delta с delta.text
|
|
1097
|
+
if (obj.type === 'content_block_delta' && obj.delta?.text) {
|
|
1098
|
+
process.stdout.write(obj.delta.text);
|
|
1099
|
+
agentText += obj.delta.text;
|
|
1100
|
+
}
|
|
1101
|
+
// Qwen/Claude: assistant message с content text
|
|
1102
|
+
else if (obj.type === 'assistant' && obj.message?.content) {
|
|
1103
|
+
for (const block of obj.message.content) {
|
|
1104
|
+
if (block.type === 'text' && block.text) {
|
|
1105
|
+
process.stdout.write(block.text);
|
|
1106
|
+
agentText += block.text;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
// result содержит финальный текст (дублирует assistant) — пропускаем
|
|
1111
|
+
} catch {
|
|
1112
|
+
// не JSON — выводим как есть
|
|
1113
|
+
process.stdout.write(line + '\n');
|
|
1114
|
+
agentText += line + '\n';
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
child.stderr.on('data', (data) => {
|
|
1120
|
+
stderr += data.toString();
|
|
1121
|
+
process.stderr.write(data);
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
child.on('close', (code) => {
|
|
1125
|
+
this.currentChild = null;
|
|
1126
|
+
clearTimeout(timeoutId);
|
|
1127
|
+
// Обрабатываем остаток буфера стриминга
|
|
1128
|
+
if (stdoutBuffer.trim()) {
|
|
1129
|
+
try {
|
|
1130
|
+
const obj = JSON.parse(stdoutBuffer);
|
|
1131
|
+
if (obj.type === 'content_block_delta' && obj.delta?.text) {
|
|
1132
|
+
process.stdout.write(obj.delta.text);
|
|
1133
|
+
}
|
|
1134
|
+
} catch {
|
|
1135
|
+
process.stdout.write(stdoutBuffer + '\n');
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
process.stdout.write('\n');
|
|
1139
|
+
|
|
1140
|
+
if (timedOut) return;
|
|
1141
|
+
|
|
1142
|
+
// Логгируем CLI вызов
|
|
1143
|
+
if (this.logger) {
|
|
1144
|
+
this.logger.cliCall(agent.command, args, code);
|
|
1145
|
+
|
|
1146
|
+
// Логгируем текстовый вывод агента
|
|
1147
|
+
const trimmedOutput = agentText.trim();
|
|
1148
|
+
if (trimmedOutput) {
|
|
1149
|
+
this.logger.info(`OUTPUT ↓`, stageId);
|
|
1150
|
+
for (const line of trimmedOutput.split('\n')) {
|
|
1151
|
+
this.logger.info(` ${line}`, stageId);
|
|
1152
|
+
}
|
|
1153
|
+
this.logger.info(`OUTPUT ↑`, stageId);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Логгируем stderr независимо от exit code
|
|
1157
|
+
if (stderr.trim()) {
|
|
1158
|
+
this.logger.warn(`STDERR ↓`, stageId);
|
|
1159
|
+
for (const line of stderr.trim().split('\n')) {
|
|
1160
|
+
this.logger.warn(` ${line}`, stageId);
|
|
1161
|
+
}
|
|
1162
|
+
this.logger.warn(`STDERR ↑`, stageId);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// Парсим результат из вывода агента через ResultParser
|
|
1167
|
+
const result = this.resultParser.parse(stdout, stageId);
|
|
1168
|
+
|
|
1169
|
+
// Если exit code ≠ 0, но результат уже распарсен — используем его
|
|
1170
|
+
if (code !== 0 && result.parsed && result.status && result.status !== 'default') {
|
|
1171
|
+
if (this.logger) {
|
|
1172
|
+
this.logger.warn(
|
|
1173
|
+
`Agent exited with code ${code}, but RESULT was parsed (status: ${result.status}). Using parsed result.`,
|
|
1174
|
+
stageId
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
// Проваливаемся в resolve ниже
|
|
1178
|
+
} else if (code !== 0) {
|
|
1179
|
+
const err = new Error(`Agent exited with code ${code}`);
|
|
1180
|
+
err.code = 'NON_ZERO_EXIT';
|
|
1181
|
+
err.exitCode = code;
|
|
1182
|
+
err.stderr = stderr;
|
|
1183
|
+
if (this.logger) {
|
|
1184
|
+
this.logger.error(`Agent exited with code ${code}`, stageId);
|
|
1185
|
+
if (stderr.trim()) {
|
|
1186
|
+
for (const line of stderr.trim().split('\n')) {
|
|
1187
|
+
this.logger.error(` stderr: ${line}`, stageId);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
reject(err);
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
resolve({
|
|
1196
|
+
status: result.status || 'default',
|
|
1197
|
+
output: stdout,
|
|
1198
|
+
stderr: stderr,
|
|
1199
|
+
result: result.data || {},
|
|
1200
|
+
exitCode: code,
|
|
1201
|
+
parsed: result.parsed
|
|
1202
|
+
});
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
child.on('error', (err) => {
|
|
1206
|
+
clearTimeout(timeoutId);
|
|
1207
|
+
if (!timedOut) {
|
|
1208
|
+
if (this.logger) {
|
|
1209
|
+
this.logger.error(`CLI error: ${err.message}`, stageId);
|
|
1210
|
+
}
|
|
1211
|
+
reject(err);
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// ============================================================================
|
|
1220
|
+
// PipelineRunner — основной цикл выполнения пайплайна
|
|
1221
|
+
// ============================================================================
|
|
1222
|
+
class PipelineRunner {
|
|
1223
|
+
constructor(config, args) {
|
|
1224
|
+
this.config = config;
|
|
1225
|
+
this.args = args;
|
|
1226
|
+
this.pipeline = config.pipeline;
|
|
1227
|
+
this.context = { ...this.pipeline.context };
|
|
1228
|
+
this.counters = {};
|
|
1229
|
+
this.stepCount = 0;
|
|
1230
|
+
this.tasksExecuted = 0;
|
|
1231
|
+
this.running = true;
|
|
1232
|
+
this.currentStage = this.pipeline.entry;
|
|
1233
|
+
|
|
1234
|
+
// Базовая директория проекта вычисляется динамически
|
|
1235
|
+
const projectRoot = args.project ? path.resolve(args.project) : findProjectRoot();
|
|
1236
|
+
|
|
1237
|
+
// Инициализация Logger — каждый запуск пишется в отдельный файл
|
|
1238
|
+
const logDir = this.pipeline.execution?.log_file
|
|
1239
|
+
? path.dirname(path.resolve(projectRoot, this.pipeline.execution.log_file))
|
|
1240
|
+
: path.resolve(projectRoot, '.workflow/logs');
|
|
1241
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').substring(0, 19);
|
|
1242
|
+
const logFilePath = path.resolve(logDir, `pipeline_${timestamp}.log`);
|
|
1243
|
+
this.logger = new Logger(logFilePath);
|
|
1244
|
+
this.loggerInitialized = false;
|
|
1245
|
+
|
|
1246
|
+
// Инициализация контекста из CLI аргументов
|
|
1247
|
+
if (args.plan) {
|
|
1248
|
+
this.context.plan_id = args.plan;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// Инициализация FileGuard для защиты файлов от изменений агентами
|
|
1252
|
+
const protectedPatterns = this.pipeline.protected_files || [];
|
|
1253
|
+
const trustedAgents = this.pipeline.trusted_agents || [];
|
|
1254
|
+
const trustedStages = this.pipeline.trusted_stages || [];
|
|
1255
|
+
this.fileGuard = new FileGuard(protectedPatterns, projectRoot, trustedAgents, trustedStages);
|
|
1256
|
+
this.projectRoot = projectRoot;
|
|
1257
|
+
this.currentExecutor = null;
|
|
1258
|
+
|
|
1259
|
+
// Настройка graceful shutdown
|
|
1260
|
+
this.setupGracefulShutdown();
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Асинхронно инициализирует runner (logger)
|
|
1265
|
+
*/
|
|
1266
|
+
async init() {
|
|
1267
|
+
await this.logger.init();
|
|
1268
|
+
this.loggerInitialized = true;
|
|
1269
|
+
|
|
1270
|
+
// Логгируем после инициализации
|
|
1271
|
+
const protectedPatterns = this.pipeline.protected_files || [];
|
|
1272
|
+
if (protectedPatterns.length > 0) {
|
|
1273
|
+
this.logger.info(`FileGuard enabled: ${protectedPatterns.length} pattern(s)`, 'PipelineRunner');
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
if (this.context.plan_id) {
|
|
1277
|
+
this.logger.info(`Plan ID: ${this.context.plan_id}`, 'PipelineRunner');
|
|
1278
|
+
} else {
|
|
1279
|
+
this.logger.info('No plan_id set — processing all tickets', 'PipelineRunner');
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* Выполняет встроенный стейдж типа update-counter:
|
|
1285
|
+
* инкрементирует счётчик и возвращает статус для goto-перехода.
|
|
1286
|
+
*
|
|
1287
|
+
* Конфигурация стейджа:
|
|
1288
|
+
* type: update-counter
|
|
1289
|
+
* counter: <name> — имя счётчика
|
|
1290
|
+
* max: <number> — максимальное значение (опционально)
|
|
1291
|
+
* goto:
|
|
1292
|
+
* default: <stage> — следующий стейдж
|
|
1293
|
+
* max_reached: <stage> — стейдж при достижении max
|
|
1294
|
+
*/
|
|
1295
|
+
executeUpdateCounter(stageId, stage) {
|
|
1296
|
+
const counterName = stage.counter;
|
|
1297
|
+
if (!counterName) {
|
|
1298
|
+
throw new Error(`Stage "${stageId}" has type update-counter but no counter specified`);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
this.counters[counterName] = (this.counters[counterName] || 0) + 1;
|
|
1302
|
+
const value = this.counters[counterName];
|
|
1303
|
+
|
|
1304
|
+
if (this.logger) {
|
|
1305
|
+
this.logger.info(`Counter "${counterName}" incremented to ${value}`, stageId);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
const max = stage.max;
|
|
1309
|
+
const status = (max && value >= max) ? 'max_reached' : 'default';
|
|
1310
|
+
|
|
1311
|
+
return { status, result: { counter: counterName, value } };
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
/**
|
|
1315
|
+
* Запускает основной цикл выполнения
|
|
1316
|
+
*/
|
|
1317
|
+
async run() {
|
|
1318
|
+
// Инициализируем logger
|
|
1319
|
+
await this.init();
|
|
1320
|
+
|
|
1321
|
+
const maxSteps = this.pipeline.execution?.max_steps || 100;
|
|
1322
|
+
const delayBetweenStages = this.pipeline.execution?.delay_between_stages || 5;
|
|
1323
|
+
|
|
1324
|
+
this.logger.info('=== Pipeline Runner Started ===', 'PipelineRunner');
|
|
1325
|
+
this.logger.info(`Entry stage: ${this.pipeline.entry}`, 'PipelineRunner');
|
|
1326
|
+
this.logger.info(`Max steps: ${maxSteps}`, 'PipelineRunner');
|
|
1327
|
+
this.logger.info(`Context: ${JSON.stringify(this.context)}`, 'PipelineRunner');
|
|
1328
|
+
|
|
1329
|
+
while (this.running && this.stepCount < maxSteps) {
|
|
1330
|
+
this.stepCount++;
|
|
1331
|
+
|
|
1332
|
+
this.logger.info(`Step ${this.stepCount}`, 'PipelineRunner');
|
|
1333
|
+
this.logger.info(`Current stage: ${this.currentStage}`, 'PipelineRunner');
|
|
1334
|
+
|
|
1335
|
+
if (this.currentStage === 'end') {
|
|
1336
|
+
this.logger.info('Pipeline completed successfully!', 'PipelineRunner');
|
|
1337
|
+
break;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
try {
|
|
1341
|
+
// Выполняем stage
|
|
1342
|
+
const stage = this.pipeline.stages[this.currentStage];
|
|
1343
|
+
if (!stage) {
|
|
1344
|
+
throw new Error(`Stage not found: ${this.currentStage}`);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
let result;
|
|
1348
|
+
|
|
1349
|
+
// Встроенный тип стейджа: update-counter — инкрементирует счётчик без вызова агента
|
|
1350
|
+
if (stage.type === 'update-counter') {
|
|
1351
|
+
result = this.executeUpdateCounter(this.currentStage, stage);
|
|
1352
|
+
} else {
|
|
1353
|
+
this.currentExecutor = new StageExecutor(this.config, this.context, this.counters, {}, this.fileGuard, this.logger, this.projectRoot);
|
|
1354
|
+
result = await this.currentExecutor.execute(this.currentStage);
|
|
1355
|
+
this.currentExecutor = null;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
this.logger.info(`Stage ${this.currentStage} completed with status: ${result.status}`, 'PipelineRunner');
|
|
1359
|
+
|
|
1360
|
+
// Определяем следующий stage по goto-логике
|
|
1361
|
+
const nextStage = this.resolveNextStage(this.currentStage, result);
|
|
1362
|
+
|
|
1363
|
+
// Считаем выполненные задачи (execute-task)
|
|
1364
|
+
if (this.currentStage === 'execute-task' && result.status !== 'error') {
|
|
1365
|
+
this.tasksExecuted++;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// Переход к следующему stage
|
|
1369
|
+
this.currentStage = nextStage;
|
|
1370
|
+
|
|
1371
|
+
// Задержка между stages
|
|
1372
|
+
if (nextStage !== 'end' && this.running) {
|
|
1373
|
+
this.logger.info(`Waiting ${delayBetweenStages}s before next stage...`, 'PipelineRunner');
|
|
1374
|
+
await this.sleep(delayBetweenStages * 1000);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
} catch (err) {
|
|
1378
|
+
this.logger.error(`Error at stage "${this.currentStage}": ${err.message}`, 'PipelineRunner');
|
|
1379
|
+
|
|
1380
|
+
// Пытаемся получить fallback transition
|
|
1381
|
+
const stage = this.pipeline.stages[this.currentStage];
|
|
1382
|
+
if (stage?.goto?.error) {
|
|
1383
|
+
const errorTarget = typeof stage.goto.error === 'string' ? stage.goto.error : stage.goto.error.stage;
|
|
1384
|
+
this.logger.info(`Transitioning to error handler: ${errorTarget}`, 'PipelineRunner');
|
|
1385
|
+
this.currentStage = errorTarget;
|
|
1386
|
+
|
|
1387
|
+
// Обновляем контекст параметрами из error transition
|
|
1388
|
+
if (typeof stage.goto.error === 'object' && stage.goto.error.params) {
|
|
1389
|
+
this.updateContext(stage.goto.error.params, { error: err.message });
|
|
1390
|
+
}
|
|
1391
|
+
} else {
|
|
1392
|
+
this.logger.error('No error handler defined. Stopping.', 'PipelineRunner');
|
|
1393
|
+
this.running = false;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
if (this.stepCount >= maxSteps) {
|
|
1399
|
+
this.logger.error(`Stopped: reached max steps limit (${maxSteps})`, 'PipelineRunner');
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
this.logger.info('=== Pipeline Runner Finished ===', 'PipelineRunner');
|
|
1403
|
+
this.logger.info(`Total steps: ${this.stepCount}`, 'PipelineRunner');
|
|
1404
|
+
this.logger.info(`Tasks executed: ${this.tasksExecuted}`, 'PipelineRunner');
|
|
1405
|
+
this.logger.info(`Final context: ${JSON.stringify(this.context)}`, 'PipelineRunner');
|
|
1406
|
+
|
|
1407
|
+
// Записываем итоговый summary
|
|
1408
|
+
this.logger.writeSummary();
|
|
1409
|
+
|
|
1410
|
+
return {
|
|
1411
|
+
steps: this.stepCount,
|
|
1412
|
+
tasksExecuted: this.tasksExecuted,
|
|
1413
|
+
context: this.context,
|
|
1414
|
+
failed: !this.running && this.stepCount < maxSteps
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
/**
|
|
1419
|
+
* Определяет следующий stage на основе результата и goto-конфигурации
|
|
1420
|
+
* Также управляет retry-логикой с agent_by_attempt
|
|
1421
|
+
*/
|
|
1422
|
+
resolveNextStage(stageId, result) {
|
|
1423
|
+
const stage = this.pipeline.stages[stageId];
|
|
1424
|
+
if (!stage || !stage.goto) {
|
|
1425
|
+
this.logger.gotoTransition(stageId, 'end', result.status);
|
|
1426
|
+
return 'end';
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
const goto = stage.goto;
|
|
1430
|
+
const status = result.status;
|
|
1431
|
+
|
|
1432
|
+
// Проверяем точное совпадение статуса
|
|
1433
|
+
if (goto[status]) {
|
|
1434
|
+
const transition = goto[status];
|
|
1435
|
+
|
|
1436
|
+
// Если переход задан строкой (shorthand: "stage-name")
|
|
1437
|
+
if (typeof transition === 'string') {
|
|
1438
|
+
this.logger.gotoTransition(stageId, transition, status);
|
|
1439
|
+
return transition;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// Обновляем контекст параметрами перехода
|
|
1443
|
+
if (transition.params) {
|
|
1444
|
+
this.updateContext(transition.params, result.result);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
const nextStage = transition.stage || 'end';
|
|
1448
|
+
this.logger.gotoTransition(stageId, nextStage, status, transition.params);
|
|
1449
|
+
return nextStage;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// Fallback на default
|
|
1453
|
+
if (goto.default) {
|
|
1454
|
+
const transition = goto.default;
|
|
1455
|
+
|
|
1456
|
+
if (typeof transition === 'string') {
|
|
1457
|
+
this.logger.gotoTransition(stageId, transition, 'default');
|
|
1458
|
+
return transition;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
if (transition.params) {
|
|
1462
|
+
this.updateContext(transition.params, result.result);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
const nextStage = transition.stage || 'end';
|
|
1466
|
+
this.logger.gotoTransition(stageId, nextStage, 'default', transition.params);
|
|
1467
|
+
return nextStage;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
this.logger.gotoTransition(stageId, 'end', 'default');
|
|
1471
|
+
return 'end';
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
/**
|
|
1475
|
+
* Обновляет контекст переменными из params с подстановкой значений
|
|
1476
|
+
*/
|
|
1477
|
+
updateContext(params, resultData) {
|
|
1478
|
+
if (!params) return;
|
|
1479
|
+
|
|
1480
|
+
// Проверяем смену ticket_id для сброса счётчика попыток
|
|
1481
|
+
const newTicketId = params.ticket_id ?
|
|
1482
|
+
(typeof params.ticket_id === 'string' ?
|
|
1483
|
+
params.ticket_id
|
|
1484
|
+
.replace(/\$result\.(\w+)/g, (_, k) => resultData[k] || '')
|
|
1485
|
+
.replace(/\$context\.(\w+)/g, (_, k) => this.context[k] || '')
|
|
1486
|
+
: params.ticket_id)
|
|
1487
|
+
: null;
|
|
1488
|
+
|
|
1489
|
+
if (newTicketId && this.context.ticket_id && newTicketId !== this.context.ticket_id) {
|
|
1490
|
+
// Тикет сменился — сбрасываем все счётчики попыток
|
|
1491
|
+
for (const counterKey of Object.keys(this.counters)) {
|
|
1492
|
+
if (counterKey.includes('attempt')) {
|
|
1493
|
+
this.counters[counterKey] = 0;
|
|
1494
|
+
if (this.logger) {
|
|
1495
|
+
this.logger.info(`Reset counter "${counterKey}" due to ticket change (${this.context.ticket_id} → ${newTicketId})`, 'PipelineRunner');
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1502
|
+
if (typeof value === 'string') {
|
|
1503
|
+
// Подстановка переменных: $context.*, $result.*, $counter.*
|
|
1504
|
+
let resolvedValue = value;
|
|
1505
|
+
|
|
1506
|
+
// $result.*
|
|
1507
|
+
resolvedValue = resolvedValue.replace(/\$result\.(\w+)/g, (_, k) => resultData[k] || '');
|
|
1508
|
+
|
|
1509
|
+
// $context.*
|
|
1510
|
+
resolvedValue = resolvedValue.replace(/\$context\.(\w+)/g, (_, k) => this.context[k] || '');
|
|
1511
|
+
|
|
1512
|
+
// $counter.*
|
|
1513
|
+
resolvedValue = resolvedValue.replace(/\$counter\.(\w+)/g, (_, k) => this.counters[k] || 0);
|
|
1514
|
+
|
|
1515
|
+
this.context[key] = resolvedValue;
|
|
1516
|
+
} else {
|
|
1517
|
+
this.context[key] = value;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
if (this.logger) {
|
|
1522
|
+
this.logger.info(`Context updated: ${JSON.stringify(this.context)}`, 'PipelineRunner');
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
/**
|
|
1527
|
+
* Утилита для задержки
|
|
1528
|
+
*/
|
|
1529
|
+
sleep(ms) {
|
|
1530
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
* Настройка graceful shutdown
|
|
1535
|
+
*/
|
|
1536
|
+
setupGracefulShutdown() {
|
|
1537
|
+
const shutdown = (signal) => {
|
|
1538
|
+
if (this.logger) {
|
|
1539
|
+
this.logger.info(`Received ${signal}. Shutting down gracefully...`, 'PipelineRunner');
|
|
1540
|
+
}
|
|
1541
|
+
this.running = false;
|
|
1542
|
+
// Убиваем текущего агента
|
|
1543
|
+
if (this.currentExecutor) {
|
|
1544
|
+
this.currentExecutor.killCurrentChild();
|
|
1545
|
+
}
|
|
1546
|
+
};
|
|
1547
|
+
|
|
1548
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
1549
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
function parseArgs(argv) {
|
|
1554
|
+
const args = {
|
|
1555
|
+
plan: null,
|
|
1556
|
+
config: null,
|
|
1557
|
+
project: null,
|
|
1558
|
+
help: false
|
|
1559
|
+
};
|
|
1560
|
+
|
|
1561
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1562
|
+
const arg = argv[i];
|
|
1563
|
+
|
|
1564
|
+
switch (arg) {
|
|
1565
|
+
case '--help':
|
|
1566
|
+
case '-h':
|
|
1567
|
+
args.help = true;
|
|
1568
|
+
break;
|
|
1569
|
+
case '--plan':
|
|
1570
|
+
args.plan = argv[++i] || null;
|
|
1571
|
+
break;
|
|
1572
|
+
case '--config':
|
|
1573
|
+
args.config = argv[++i] || null;
|
|
1574
|
+
break;
|
|
1575
|
+
case '--project':
|
|
1576
|
+
args.project = argv[++i] || null;
|
|
1577
|
+
break;
|
|
1578
|
+
default:
|
|
1579
|
+
if (arg.startsWith('--')) {
|
|
1580
|
+
console.error(`Unknown option: ${arg}`);
|
|
1581
|
+
process.exit(1);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
return args;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
function printHelp() {
|
|
1590
|
+
console.log(`
|
|
1591
|
+
Workflow Runner - Pipeline Orchestrator
|
|
1592
|
+
|
|
1593
|
+
Usage: node runner.mjs [options]
|
|
1594
|
+
|
|
1595
|
+
Options:
|
|
1596
|
+
--plan PLAN-ID Plan ID to execute (e.g., PLAN-003)
|
|
1597
|
+
--config PATH Path to pipeline.yaml config (default: .workflow/config/pipeline.yaml)
|
|
1598
|
+
--project PATH Project root path (overrides auto-detection)
|
|
1599
|
+
--help, -h Show this help message
|
|
1600
|
+
|
|
1601
|
+
Examples:
|
|
1602
|
+
node .workflow/src/runner.mjs --help
|
|
1603
|
+
node .workflow/src/runner.mjs --plan PLAN-003
|
|
1604
|
+
node runner.mjs --project /path/to/project --plan PLAN-003
|
|
1605
|
+
`);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
function loadConfig(configPath) {
|
|
1609
|
+
const fullPath = path.resolve(configPath);
|
|
1610
|
+
|
|
1611
|
+
if (!fs.existsSync(fullPath)) {
|
|
1612
|
+
throw new Error(`Config file not found: ${fullPath}`);
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
1616
|
+
const config = yaml.load(content);
|
|
1617
|
+
|
|
1618
|
+
return config;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
function validateConfig(config) {
|
|
1622
|
+
const errors = [];
|
|
1623
|
+
|
|
1624
|
+
if (!config) {
|
|
1625
|
+
errors.push('Config is empty');
|
|
1626
|
+
return errors;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
if (!config.pipeline) {
|
|
1630
|
+
errors.push('Missing required field: pipeline');
|
|
1631
|
+
return errors;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
const pipeline = config.pipeline;
|
|
1635
|
+
|
|
1636
|
+
if (!pipeline.name || typeof pipeline.name !== 'string') {
|
|
1637
|
+
errors.push('Missing or invalid required field: pipeline.name (string)');
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
if (!pipeline.version || typeof pipeline.version !== 'string') {
|
|
1641
|
+
errors.push('Missing or invalid required field: pipeline.version (string)');
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
if (!pipeline.agents || typeof pipeline.agents !== 'object') {
|
|
1645
|
+
errors.push('Missing or invalid required field: pipeline.agents (object)');
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
if (!pipeline.stages || typeof pipeline.stages !== 'object') {
|
|
1649
|
+
errors.push('Missing or invalid required field: pipeline.stages (object)');
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
if (pipeline.agents && pipeline.stages) {
|
|
1653
|
+
const agentIds = Object.keys(pipeline.agents);
|
|
1654
|
+
const stageIds = Object.keys(pipeline.stages);
|
|
1655
|
+
|
|
1656
|
+
for (const [stageId, stage] of Object.entries(pipeline.stages)) {
|
|
1657
|
+
const resolvedAgent = stage.agent || pipeline.default_agent;
|
|
1658
|
+
if (resolvedAgent && !agentIds.includes(resolvedAgent)) {
|
|
1659
|
+
errors.push(`Stage "${stageId}" references non-existent agent: ${resolvedAgent}`);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
if (stage.goto) {
|
|
1663
|
+
for (const [status, transition] of Object.entries(stage.goto)) {
|
|
1664
|
+
if (status === 'default') continue;
|
|
1665
|
+
if (transition.stage && transition.stage !== 'end' && !stageIds.includes(transition.stage)) {
|
|
1666
|
+
errors.push(`Stage "${stageId}" goto.${status} references non-existent stage: ${transition.stage}`);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
return errors;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
async function runPipeline(argv = process.argv.slice(2)) {
|
|
1677
|
+
const args = parseArgs(argv);
|
|
1678
|
+
|
|
1679
|
+
if (args.help) {
|
|
1680
|
+
printHelp();
|
|
1681
|
+
return { exitCode: 0, help: true };
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
// Resolve config path
|
|
1685
|
+
if (!args.config) {
|
|
1686
|
+
const projectRoot = args.project ? path.resolve(args.project) : findProjectRoot();
|
|
1687
|
+
args.config = path.resolve(projectRoot, '.workflow/config/pipeline.yaml');
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
console.log('=== Workflow Runner ===');
|
|
1691
|
+
console.log(`Config: ${args.config}`);
|
|
1692
|
+
if (args.plan) console.log(`Plan: ${args.plan}`);
|
|
1693
|
+
if (args.project) console.log(`Project: ${args.project}`);
|
|
1694
|
+
console.log('');
|
|
1695
|
+
|
|
1696
|
+
try {
|
|
1697
|
+
const config = loadConfig(args.config);
|
|
1698
|
+
const errors = validateConfig(config);
|
|
1699
|
+
|
|
1700
|
+
if (errors.length > 0) {
|
|
1701
|
+
console.error('Configuration validation failed:');
|
|
1702
|
+
errors.forEach(err => console.error(` - ${err}`));
|
|
1703
|
+
return { exitCode: 1, error: 'Configuration validation failed', details: errors };
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
console.log(`Pipeline: ${config.pipeline.name} v${config.pipeline.version}`);
|
|
1707
|
+
console.log(`Agents: ${Object.keys(config.pipeline.agents).join(', ')}`);
|
|
1708
|
+
console.log(`Stages: ${Object.keys(config.pipeline.stages).join(', ')}`);
|
|
1709
|
+
console.log('');
|
|
1710
|
+
console.log('Configuration validated successfully!');
|
|
1711
|
+
|
|
1712
|
+
// Запускаем пайплайн
|
|
1713
|
+
const runner = new PipelineRunner(config, args);
|
|
1714
|
+
const result = await runner.run();
|
|
1715
|
+
|
|
1716
|
+
console.log('\n=== Summary ===');
|
|
1717
|
+
console.log(`Steps executed: ${result.steps}`);
|
|
1718
|
+
console.log(`Tasks completed: ${result.tasksExecuted}`);
|
|
1719
|
+
|
|
1720
|
+
return { exitCode: result.failed ? 1 : 0, result };
|
|
1721
|
+
|
|
1722
|
+
} catch (err) {
|
|
1723
|
+
console.error(`\nError: ${err.message}`);
|
|
1724
|
+
console.error(err.stack);
|
|
1725
|
+
|
|
1726
|
+
// Даём файлу логов время записаться перед выходом
|
|
1727
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1728
|
+
|
|
1729
|
+
return { exitCode: 1, error: err.message, stack: err.stack };
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// Export for use as ES module
|
|
1734
|
+
export { runPipeline, parseArgs, PipelineRunner, FileGuard, StageExecutor };
|
|
1735
|
+
export default { runPipeline, parseArgs, PipelineRunner, FileGuard, StageExecutor };
|