workflow-ai 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/configs/pipeline.yaml +12 -2
- package/package.json +3 -2
- package/src/runner.mjs +61 -46
- package/src/skills/execute-task/SKILL.md +6 -4
package/configs/pipeline.yaml
CHANGED
|
@@ -97,6 +97,9 @@ pipeline:
|
|
|
97
97
|
workdir: "."
|
|
98
98
|
description: "Скрипт для перемещения завершённых тикетов из in-progress/ в review/"
|
|
99
99
|
|
|
100
|
+
|
|
101
|
+
default_agent: qwen-code
|
|
102
|
+
|
|
100
103
|
# ===========================================================================
|
|
101
104
|
# Этапы (stages)
|
|
102
105
|
# ===========================================================================
|
|
@@ -233,6 +236,12 @@ pipeline:
|
|
|
233
236
|
agent: qwen-code
|
|
234
237
|
fallback_agent: kilo-deepseek
|
|
235
238
|
skill: execute-task
|
|
239
|
+
counter: task_attempts
|
|
240
|
+
agent_by_attempt:
|
|
241
|
+
1: qwen-code
|
|
242
|
+
2: claude-sonnet
|
|
243
|
+
3: claude-opus
|
|
244
|
+
4: kilo-deepseek
|
|
236
245
|
goto:
|
|
237
246
|
default:
|
|
238
247
|
stage: move-to-review
|
|
@@ -275,6 +284,7 @@ pipeline:
|
|
|
275
284
|
agent: claude-sonnet
|
|
276
285
|
fallback_agent: qwen-code
|
|
277
286
|
skill: review-result
|
|
287
|
+
counter: task_attempts
|
|
278
288
|
goto:
|
|
279
289
|
passed:
|
|
280
290
|
stage: move-ticket
|
|
@@ -300,7 +310,7 @@ pipeline:
|
|
|
300
310
|
description: "Обновить счётчик попыток выполнения тикета"
|
|
301
311
|
type: update-counter
|
|
302
312
|
counter: task_attempts
|
|
303
|
-
max:
|
|
313
|
+
max: 6
|
|
304
314
|
goto:
|
|
305
315
|
default:
|
|
306
316
|
stage: move-ticket
|
|
@@ -366,7 +376,7 @@ pipeline:
|
|
|
366
376
|
description: "Обновить счётчик итераций анализа плана"
|
|
367
377
|
type: update-counter
|
|
368
378
|
counter: plan_iterations
|
|
369
|
-
max:
|
|
379
|
+
max: 2
|
|
370
380
|
goto:
|
|
371
381
|
default:
|
|
372
382
|
stage: decompose-gaps
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workflow-ai",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "AI Agent Workflow Coordinator — kanban-based pipeline for AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"node": ">=18.0.0"
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
|
-
"test": "node --test src/tests/*.test.mjs"
|
|
26
|
+
"test": "node --test src/tests/*.test.mjs",
|
|
27
|
+
"release": "npm version patch && npm publish"
|
|
27
28
|
},
|
|
28
29
|
"dependencies": {
|
|
29
30
|
"js-yaml": "^4.1.0"
|
package/src/runner.mjs
CHANGED
|
@@ -378,6 +378,41 @@ class PromptBuilder {
|
|
|
378
378
|
// ResultParser — парсит вывод агентов и извлекает структурированные данные
|
|
379
379
|
// ============================================================================
|
|
380
380
|
class ResultParser {
|
|
381
|
+
// Карта нормализации статусов: синонимы → каноническое значение
|
|
382
|
+
static STATUS_ALIASES = {
|
|
383
|
+
pass: 'passed',
|
|
384
|
+
approved: 'passed',
|
|
385
|
+
success: 'passed',
|
|
386
|
+
succeeded: 'passed',
|
|
387
|
+
ok: 'passed',
|
|
388
|
+
accepted: 'passed',
|
|
389
|
+
lgtm: 'passed',
|
|
390
|
+
fixed: 'passed',
|
|
391
|
+
resolved: 'passed',
|
|
392
|
+
fail: 'failed',
|
|
393
|
+
rejected: 'failed',
|
|
394
|
+
denied: 'failed',
|
|
395
|
+
not_passed: 'failed',
|
|
396
|
+
err: 'error',
|
|
397
|
+
crash: 'error',
|
|
398
|
+
timeout: 'error',
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Нормализует статус: приводит синонимы к каноническому значению
|
|
403
|
+
* @param {string} status
|
|
404
|
+
* @returns {string}
|
|
405
|
+
*/
|
|
406
|
+
normalizeStatus(status) {
|
|
407
|
+
const lower = status.toLowerCase();
|
|
408
|
+
const canonical = ResultParser.STATUS_ALIASES[lower];
|
|
409
|
+
if (canonical) {
|
|
410
|
+
console.log(`[ResultParser] Normalized status: "${status}" → "${canonical}"`);
|
|
411
|
+
return canonical;
|
|
412
|
+
}
|
|
413
|
+
return status;
|
|
414
|
+
}
|
|
415
|
+
|
|
381
416
|
/**
|
|
382
417
|
* Парсит вывод агента и извлекает результат между маркерами
|
|
383
418
|
* @param {string} output - stdout агента
|
|
@@ -396,10 +431,11 @@ class ResultParser {
|
|
|
396
431
|
const resultBlock = output.substring(startIdx + marker.length, endIdx).trim();
|
|
397
432
|
const data = this.parseResultBlock(resultBlock);
|
|
398
433
|
|
|
399
|
-
|
|
434
|
+
const normalizedStatus = this.normalizeStatus(data.status || 'default');
|
|
435
|
+
console.log(`[ResultParser] Parsed structured result for ${stageId}: status=${normalizedStatus}`);
|
|
400
436
|
|
|
401
437
|
return {
|
|
402
|
-
status:
|
|
438
|
+
status: normalizedStatus,
|
|
403
439
|
data: data.data || {},
|
|
404
440
|
raw: output,
|
|
405
441
|
parsed: true
|
|
@@ -484,10 +520,11 @@ class ResultParser {
|
|
|
484
520
|
}
|
|
485
521
|
}
|
|
486
522
|
|
|
487
|
-
|
|
523
|
+
const normalizedStatus = this.normalizeStatus(status);
|
|
524
|
+
console.log(`[ResultParser] Fallback parsing for ${stageId}: status=${normalizedStatus}`);
|
|
488
525
|
|
|
489
526
|
return {
|
|
490
|
-
status,
|
|
527
|
+
status: normalizedStatus,
|
|
491
528
|
data: extractedData,
|
|
492
529
|
raw: output,
|
|
493
530
|
parsed: false
|
|
@@ -681,7 +718,17 @@ class StageExecutor {
|
|
|
681
718
|
throw new Error(`Stage not found: ${stageId}`);
|
|
682
719
|
}
|
|
683
720
|
|
|
684
|
-
|
|
721
|
+
// Выбираем агента: если есть agent_by_attempt и счётчик — ротация по попыткам
|
|
722
|
+
let agentId = stage.agent || this.pipeline.default_agent;
|
|
723
|
+
if (stage.agent_by_attempt && stage.counter) {
|
|
724
|
+
const attempt = this.counters[stage.counter] || 0;
|
|
725
|
+
if (stage.agent_by_attempt[attempt]) {
|
|
726
|
+
agentId = stage.agent_by_attempt[attempt];
|
|
727
|
+
if (this.logger) {
|
|
728
|
+
this.logger.info(`Agent rotation: attempt ${attempt} → ${agentId}`, stageId);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
685
732
|
const agent = this.pipeline.agents[agentId];
|
|
686
733
|
if (!agent) {
|
|
687
734
|
throw new Error(`Agent not found: ${agentId}`);
|
|
@@ -953,10 +1000,12 @@ class PipelineRunner {
|
|
|
953
1000
|
// Базовая директория проекта вычисляется динамически
|
|
954
1001
|
const projectRoot = args.project ? path.resolve(args.project) : findProjectRoot();
|
|
955
1002
|
|
|
956
|
-
// Инициализация Logger
|
|
957
|
-
const
|
|
958
|
-
? path.resolve(projectRoot, this.pipeline.execution.log_file)
|
|
959
|
-
: path.resolve(projectRoot, '.workflow/logs
|
|
1003
|
+
// Инициализация Logger — каждый запуск пишется в отдельный файл
|
|
1004
|
+
const logDir = this.pipeline.execution?.log_file
|
|
1005
|
+
? path.dirname(path.resolve(projectRoot, this.pipeline.execution.log_file))
|
|
1006
|
+
: path.resolve(projectRoot, '.workflow/logs');
|
|
1007
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').substring(0, 19);
|
|
1008
|
+
const logFilePath = path.resolve(logDir, `pipeline_${timestamp}.log`);
|
|
960
1009
|
this.logger = new Logger(logFilePath);
|
|
961
1010
|
this.loggerInitialized = false;
|
|
962
1011
|
|
|
@@ -1157,41 +1206,6 @@ class PipelineRunner {
|
|
|
1157
1206
|
this.updateContext(transition.params, result.result);
|
|
1158
1207
|
}
|
|
1159
1208
|
|
|
1160
|
-
// Проверяем retry-логику с agent_by_attempt
|
|
1161
|
-
if (transition.agent_by_attempt && stage.counter) {
|
|
1162
|
-
const attempt = this.counters[stage.counter] || 1;
|
|
1163
|
-
const maxAttempts = transition.max || 3;
|
|
1164
|
-
|
|
1165
|
-
if (attempt < maxAttempts) {
|
|
1166
|
-
// Ещё есть попытки — переходим на указанный stage с переопределением агента
|
|
1167
|
-
const nextStage = transition.stage || stageId;
|
|
1168
|
-
const overrideAgent = transition.agent_by_attempt[attempt];
|
|
1169
|
-
|
|
1170
|
-
if (overrideAgent) {
|
|
1171
|
-
// Переопределяем агента для следующей попытки
|
|
1172
|
-
stage.agent = overrideAgent;
|
|
1173
|
-
this.logger.info(`Retry attempt ${attempt}: overriding agent to ${overrideAgent}`, stageId);
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
this.logger.retry(stageId, attempt, maxAttempts);
|
|
1177
|
-
this.logger.gotoTransition(stageId, nextStage, status, transition.params);
|
|
1178
|
-
return nextStage;
|
|
1179
|
-
} else {
|
|
1180
|
-
// Попытки исчерпаны — переходим на on_max
|
|
1181
|
-
this.logger.info(`Max attempts (${maxAttempts}) reached for ${stageId}`, stageId);
|
|
1182
|
-
this.logger.retry(stageId, attempt, maxAttempts);
|
|
1183
|
-
|
|
1184
|
-
if (transition.on_max) {
|
|
1185
|
-
if (transition.on_max.params) {
|
|
1186
|
-
this.updateContext(transition.on_max.params, result.result);
|
|
1187
|
-
}
|
|
1188
|
-
const nextStage = transition.on_max.stage || 'end';
|
|
1189
|
-
this.logger.gotoTransition(stageId, nextStage, status, transition.on_max.params);
|
|
1190
|
-
return nextStage;
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
1209
|
const nextStage = transition.stage || 'end';
|
|
1196
1210
|
this.logger.gotoTransition(stageId, nextStage, status, transition.params);
|
|
1197
1211
|
return nextStage;
|
|
@@ -1398,8 +1412,9 @@ function validateConfig(config) {
|
|
|
1398
1412
|
const stageIds = Object.keys(pipeline.stages);
|
|
1399
1413
|
|
|
1400
1414
|
for (const [stageId, stage] of Object.entries(pipeline.stages)) {
|
|
1401
|
-
|
|
1402
|
-
|
|
1415
|
+
const resolvedAgent = stage.agent || pipeline.default_agent;
|
|
1416
|
+
if (resolvedAgent && !agentIds.includes(resolvedAgent)) {
|
|
1417
|
+
errors.push(`Stage "${stageId}" references non-existent agent: ${resolvedAgent}`);
|
|
1403
1418
|
}
|
|
1404
1419
|
|
|
1405
1420
|
if (stage.goto) {
|
|
@@ -17,13 +17,15 @@ description: Выполнить задачу из тикета. Использу
|
|
|
17
17
|
|
|
18
18
|
### 1. Прочитать тикет
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
**ОБЯЗАТЕЛЬНО:** Используй ТОЛЬКО `ticket_id` из секции Context промпта. Запрещено выбирать другой тикет.
|
|
21
21
|
|
|
22
22
|
```
|
|
23
|
-
Путь: .workflow/tickets/
|
|
23
|
+
Путь: .workflow/tickets/in-progress/{TICKET-ID}.md
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Если тикет не найден в `in-progress/`, проверь `review/` (при повторном выполнении тикет может быть там).
|
|
27
|
+
|
|
28
|
+
**Важно:** НЕ перемещай тикет. Перемещение выполняется отдельным stage пайплайна.
|
|
27
29
|
|
|
28
30
|
Извлечь:
|
|
29
31
|
- Описание задачи
|
|
@@ -109,7 +111,7 @@ status: default
|
|
|
109
111
|
|
|
110
112
|
- Выполненная работа (код, документация и т.д.)
|
|
111
113
|
- Обновлённый тикет с секцией Result
|
|
112
|
-
- Тикет остаётся
|
|
114
|
+
- Тикет остаётся на месте (перемещение выполняется отдельным stage)
|
|
113
115
|
|
|
114
116
|
## Связанные skills
|
|
115
117
|
|