workflow-ai 1.0.4 → 1.0.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workflow-ai",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "AI Agent Workflow Coordinator — kanban-based pipeline for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/src/lib/utils.mjs CHANGED
@@ -53,6 +53,38 @@ export function printResult(result) {
53
53
  console.log('---RESULT---');
54
54
  }
55
55
 
56
+ /**
57
+ * Нормализует входное значение в формат PLAN-NNN.
58
+ * Принимает: "PLAN-007", "7", "007", "plan-7", "plans/PLAN-007.md", "/abs/path/PLAN-007.md"
59
+ *
60
+ * @param {string} raw - Входное значение
61
+ * @returns {string|null} Нормализованный ID плана или null
62
+ */
63
+ export function normalizePlanId(raw) {
64
+ if (!raw) return null;
65
+
66
+ const basename = path.basename(raw, '.md');
67
+
68
+ const full = basename.match(/^plan-(\d+)$/i);
69
+ if (full) return `PLAN-${String(parseInt(full[1], 10)).padStart(3, '0')}`;
70
+
71
+ const num = raw.trim().match(/^(\d+)$/);
72
+ if (num) return `PLAN-${String(parseInt(num[1], 10)).padStart(3, '0')}`;
73
+
74
+ return null;
75
+ }
76
+
77
+ /**
78
+ * Извлекает plan_id из аргументов командной строки (контекст пайплайна).
79
+ *
80
+ * @returns {string|null} Нормализованный plan_id или null
81
+ */
82
+ export function extractPlanId() {
83
+ const prompt = process.argv.slice(2)[0] || '';
84
+ const match = prompt.match(/plan_id:\s*(\S+)/i);
85
+ return match ? normalizePlanId(match[1]) : null;
86
+ }
87
+
56
88
  /**
57
89
  * Возвращает абсолютный путь к корню npm-пакета через import.meta.url.
58
90
  *
package/src/runner.mjs CHANGED
@@ -771,9 +771,26 @@ class StageExecutor {
771
771
  reject(new Error(`Stage "${stageId}" timed out after ${timeout}s`));
772
772
  }, timeout * 1000);
773
773
 
774
+ let stdoutBuffer = '';
774
775
  child.stdout.on('data', (data) => {
775
- stdout += data.toString();
776
- process.stdout.write(data);
776
+ const chunk = data.toString();
777
+ stdout += chunk;
778
+ // Парсим stream-json и выводим только текст дельт
779
+ stdoutBuffer += chunk;
780
+ const lines = stdoutBuffer.split('\n');
781
+ stdoutBuffer = lines.pop(); // незавершённая строка остаётся в буфере
782
+ for (const line of lines) {
783
+ if (!line.trim()) continue;
784
+ try {
785
+ const obj = JSON.parse(line);
786
+ if (obj.type === 'content_block_delta' && obj.delta?.text) {
787
+ process.stdout.write(obj.delta.text);
788
+ }
789
+ } catch {
790
+ // не JSON — выводим как есть
791
+ process.stdout.write(line + '\n');
792
+ }
793
+ }
777
794
  });
778
795
 
779
796
  child.stderr.on('data', (data) => {
@@ -924,8 +941,6 @@ class PipelineRunner {
924
941
  // Инициализация контекста из CLI аргументов
925
942
  if (args.plan) {
926
943
  this.context.plan_id = args.plan;
927
- } else if (!this.context.plan_id) {
928
- this._detectCurrentPlanId(projectRoot);
929
944
  }
930
945
 
931
946
  // Инициализация FileGuard для защиты файлов от изменений агентами
@@ -937,32 +952,6 @@ class PipelineRunner {
937
952
  this.setupGracefulShutdown();
938
953
  }
939
954
 
940
- /**
941
- * Автоматически определяет plan_id из .workflow/plans/current/
942
- * Ищет первый .md файл (не .gitkeep) и читает поле id: из frontmatter
943
- * @param {string} projectRoot - корневая директория проекта
944
- * @returns {string} plan_id или пустая строка если не найден
945
- */
946
- _detectCurrentPlanId(projectRoot) {
947
- const plansDir = path.resolve(projectRoot, '.workflow/plans/current');
948
- if (!fs.existsSync(plansDir)) return '';
949
-
950
- const files = fs.readdirSync(plansDir)
951
- .filter(f => f.endsWith('.md') && f !== '.gitkeep.md')
952
- .sort();
953
-
954
- for (const file of files) {
955
- const content = fs.readFileSync(path.join(plansDir, file), 'utf8');
956
- const match = content.match(/^id:\s*["']?([^"'\n]+)["']?/m);
957
- if (match) {
958
- this.context.plan_id = match[1].trim();
959
- return this.context.plan_id;
960
- }
961
- }
962
-
963
- return '';
964
- }
965
-
966
955
  /**
967
956
  * Асинхронно инициализирует runner (logger)
968
957
  */
@@ -977,10 +966,9 @@ class PipelineRunner {
977
966
  }
978
967
 
979
968
  if (this.context.plan_id) {
980
- const source = this.args.plan ? 'CLI' : 'auto-detected';
981
- this.logger.info(`Plan ID: ${this.context.plan_id} (${source})`, 'PipelineRunner');
969
+ this.logger.info(`Plan ID: ${this.context.plan_id}`, 'PipelineRunner');
982
970
  } else {
983
- this.logger.warn('No plan_id set and no active plan found in .workflow/plans/current/', 'PipelineRunner');
971
+ this.logger.info('No plan_id set processing all tickets', 'PipelineRunner');
984
972
  }
985
973
  }
986
974
 
@@ -28,7 +28,7 @@
28
28
  import fs from 'fs';
29
29
  import path from 'path';
30
30
  import { findProjectRoot } from '../lib/find-root.mjs';
31
- import { parseFrontmatter, printResult } from '../lib/utils.mjs';
31
+ import { parseFrontmatter, printResult, normalizePlanId, extractPlanId } from '../lib/utils.mjs';
32
32
 
33
33
  const PROJECT_DIR = findProjectRoot();
34
34
  const WORKFLOW_DIR = path.join(PROJECT_DIR, '.workflow');
@@ -102,43 +102,13 @@ function readTickets(dir) {
102
102
  return tickets;
103
103
  }
104
104
 
105
- /**
106
- * Нормализует входное значение в формат PLAN-NNN.
107
- * Принимает: "PLAN-007", "7", "007", "plan-7", "plans/PLAN-007.md", "/abs/path/PLAN-007.md"
108
- */
109
- function normalizePlanId(raw) {
110
- if (!raw) return null;
111
-
112
- // Извлекаем имя файла если передан путь
113
- const basename = path.basename(raw, '.md');
114
-
115
- // Уже в формате PLAN-NNN (регистронезависимо)
116
- const full = basename.match(/^plan-(\d+)$/i);
117
- if (full) return `PLAN-${String(parseInt(full[1], 10)).padStart(3, '0')}`;
118
-
119
- // Просто цифра или число: "7", "007"
120
- const num = raw.trim().match(/^(\d+)$/);
121
- if (num) return `PLAN-${String(parseInt(num[1], 10)).padStart(3, '0')}`;
122
-
123
- return null;
124
- }
125
-
126
- /**
127
- * Извлекает plan_id из аргументов командной строки (контекст пайплайна)
128
- */
129
- function extractPlanId() {
130
- const prompt = process.argv.slice(2)[0] || '';
131
- const match = prompt.match(/plan_id:\s*(\S+)/i);
132
- return match ? normalizePlanId(match[1]) : null;
133
- }
134
-
135
105
  /**
136
106
  * Проверяет все тикеты в backlog/ и возвращает список готовых
137
107
  */
138
108
  function checkBacklog(planId) {
139
109
  const allTickets = readTickets(BACKLOG_DIR);
140
110
  const tickets = planId
141
- ? allTickets.filter(t => t.frontmatter.parent_plan === planId)
111
+ ? allTickets.filter(t => normalizePlanId(t.frontmatter.parent_plan) === planId)
142
112
  : allTickets;
143
113
 
144
114
  const ready = [];
@@ -21,7 +21,7 @@
21
21
  import fs from 'fs';
22
22
  import path from 'path';
23
23
  import { findProjectRoot } from '../lib/find-root.mjs';
24
- import { parseFrontmatter, printResult } from '../lib/utils.mjs';
24
+ import { parseFrontmatter, printResult, normalizePlanId, extractPlanId } from '../lib/utils.mjs';
25
25
 
26
26
  // Корень проекта
27
27
  const PROJECT_DIR = findProjectRoot();
@@ -229,17 +229,22 @@ function findCompletedInProgress() {
229
229
  /**
230
230
  * Выбирает следующий тикет для выполнения
231
231
  */
232
- function pickNextTicket() {
233
- const tickets = readReadyTickets();
232
+ function filterByPlan(tickets, planId) {
233
+ if (!planId) return tickets;
234
+ return tickets.filter(t => normalizePlanId(t.frontmatter.parent_plan) === planId);
235
+ }
236
+
237
+ function pickNextTicket(planId) {
238
+ const tickets = filterByPlan(readReadyTickets(), planId);
234
239
 
235
240
  if (tickets.length === 0) {
236
241
  // Если ready/ пуст, проверяем review/ — нужно завершить ревью
237
- let reviewTickets = readReviewTickets();
242
+ let reviewTickets = filterByPlan(readReviewTickets(), planId);
238
243
 
239
244
  if (reviewTickets.length === 0) {
240
245
  // Нет тикетов ни в ready/, ни в review/ — проверяем in-progress/
241
246
  // на завершённые тикеты (с заполненным Summary)
242
- const completedInProgress = findCompletedInProgress();
247
+ const completedInProgress = filterByPlan(findCompletedInProgress(), planId);
243
248
  if (completedInProgress.length > 0) {
244
249
  const first = completedInProgress[0];
245
250
  console.log(`[INFO] Found completed ticket in in-progress/: ${first.id}`);
@@ -318,9 +323,15 @@ function pickNextTicket() {
318
323
 
319
324
  // Main entry point
320
325
  async function main() {
326
+ const planId = extractPlanId();
327
+
328
+ if (planId) {
329
+ console.log(`[INFO] Filtering by plan_id: ${planId}`);
330
+ }
331
+
321
332
  console.log(`[INFO] Scanning ready/ directory: ${READY_DIR}`);
322
333
 
323
- const result = pickNextTicket();
334
+ const result = pickNextTicket(planId);
324
335
 
325
336
  if (result.status === 'found') {
326
337
  console.log(`[INFO] Selected ticket: ${result.ticket_id} (${result.title})`);