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.
@@ -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: 3
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: 3
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.7",
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
- console.log(`[ResultParser] Parsed structured result for ${stageId}: status=${data.status}`);
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: data.status || 'default',
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
- console.log(`[ResultParser] Fallback parsing for ${stageId}: status=${status}`);
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
- const agentId = stage.agent;
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 logFilePath = this.pipeline.execution?.log_file
958
- ? path.resolve(projectRoot, this.pipeline.execution.log_file)
959
- : path.resolve(projectRoot, '.workflow/logs/pipeline.log');
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
- if (stage.agent && !agentIds.includes(stage.agent)) {
1402
- errors.push(`Stage "${stageId}" references non-existent agent: ${stage.agent}`);
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
- ID тикета передаётся в промпте как `ticket_id` в секции Context.
20
+ **ОБЯЗАТЕЛЬНО:** Используй ТОЛЬКО `ticket_id` из секции Context промпта. Запрещено выбирать другой тикет.
21
21
 
22
22
  ```
23
- Путь: .workflow/tickets/ready/{TICKET-ID}.md
23
+ Путь: .workflow/tickets/in-progress/{TICKET-ID}.md
24
24
  ```
25
25
 
26
- **Важно:** НЕ перемещай тикет он остаётся в `ready/` на время выполнения. Перемещение выполняется отдельным stage пайплайна.
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
- - Тикет остаётся в `ready/` (перемещение выполняется отдельным stage)
114
+ - Тикет остаётся на месте (перемещение выполняется отдельным stage)
113
115
 
114
116
  ## Связанные skills
115
117