workflow-ai 1.0.9 → 1.0.11

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.
@@ -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, normalizePlanId, extractPlanId } from '../lib/utils.mjs';
24
+ import { parseFrontmatter, printResult, normalizePlanId, extractPlanId, getLastReviewStatus, serializeFrontmatter } from '../lib/utils.mjs';
25
25
 
26
26
  // Корень проекта
27
27
  const PROJECT_DIR = findProjectRoot();
@@ -31,6 +31,9 @@ const TICKETS_DIR = path.join(WORKFLOW_DIR, 'tickets');
31
31
  const READY_DIR = path.join(TICKETS_DIR, 'ready');
32
32
  const DONE_DIR = path.join(TICKETS_DIR, 'done');
33
33
  const IN_PROGRESS_DIR = path.join(TICKETS_DIR, 'in-progress');
34
+ const BLOCKED_DIR = path.join(TICKETS_DIR, 'blocked');
35
+ const REVIEW_DIR = path.join(TICKETS_DIR, 'review');
36
+ const BACKLOG_DIR = path.join(TICKETS_DIR, 'backlog');
34
37
 
35
38
 
36
39
  /**
@@ -89,6 +92,155 @@ function checkDependencies(dependencies) {
89
92
  });
90
93
  }
91
94
 
95
+ /**
96
+ * Авто-коррекция тикетов на основе статуса ревью.
97
+ * Сканирует все директории и перемещает тикеты по правилам:
98
+ * - blocked → done (если review = passed)
99
+ * - blocked → backlog (если review = failed, НЕ при null)
100
+ * - done → backlog (если review = failed)
101
+ * - review → done (если review = passed)
102
+ * - in-progress → done (если review = passed)
103
+ * - ready → done (если review = passed)
104
+ *
105
+ * @returns {object} Результат: { moved: Array<{id, from, to, reason}> }
106
+ */
107
+ function autoCorrectTickets() {
108
+ const moved = [];
109
+
110
+ /**
111
+ * Перемещает тикет из одной директории в другую
112
+ */
113
+ function moveTicket(ticketId, fromDir, toDir, reason) {
114
+ const fromPath = path.join(fromDir, `${ticketId}.md`);
115
+ const toPath = path.join(toDir, `${ticketId}.md`);
116
+
117
+ if (!fs.existsSync(fromPath)) {
118
+ return false;
119
+ }
120
+
121
+ try {
122
+ // Читаем содержимое
123
+ const content = fs.readFileSync(fromPath, 'utf8');
124
+ const { frontmatter, body } = parseFrontmatter(content);
125
+
126
+ // Обновляем updated_at
127
+ frontmatter.updated_at = new Date().toISOString();
128
+
129
+ // Если перемещаем в done — ставим completed_at
130
+ if (toDir === DONE_DIR && !frontmatter.completed_at) {
131
+ frontmatter.completed_at = new Date().toISOString();
132
+ }
133
+
134
+ // Сериализуем и записываем в новую директорию
135
+ const newContent = serializeFrontmatter(frontmatter) + body;
136
+ fs.writeFileSync(toPath, newContent, 'utf8');
137
+
138
+ // Удаляем старый файл
139
+ fs.unlinkSync(fromPath);
140
+
141
+ console.log(`[AUTO-CORRECT] ${ticketId}: ${path.basename(fromDir)} → ${path.basename(toDir)} (${reason})`);
142
+
143
+ moved.push({
144
+ id: ticketId,
145
+ from: path.basename(fromDir),
146
+ to: path.basename(toDir),
147
+ reason
148
+ });
149
+
150
+ return true;
151
+ } catch (e) {
152
+ console.error(`[ERROR] Failed to move ticket ${ticketId}: ${e.message}`);
153
+ return false;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Обрабатывает тикеты в указанной директории
159
+ */
160
+ function processDirectory(dir, rules) {
161
+ if (!fs.existsSync(dir)) return;
162
+
163
+ const files = fs.readdirSync(dir)
164
+ .filter(f => f.endsWith('.md') && f !== '.gitkeep.md');
165
+
166
+ for (const file of files) {
167
+ const filePath = path.join(dir, file);
168
+ try {
169
+ const content = fs.readFileSync(filePath, 'utf8');
170
+ const { frontmatter } = parseFrontmatter(content);
171
+ const ticketId = frontmatter.id || file.replace('.md', '');
172
+
173
+ // Получаем статус ревью
174
+ const reviewStatus = getLastReviewStatus(content);
175
+
176
+ // Применяем правила
177
+ for (const rule of rules) {
178
+ if (rule.condition(reviewStatus)) {
179
+ moveTicket(ticketId, dir, rule.toDir, rule.reason);
180
+ break; // Только одно перемещение на тикет
181
+ }
182
+ }
183
+ } catch (e) {
184
+ console.error(`[WARN] Failed to process ticket ${file}: ${e.message}`);
185
+ }
186
+ }
187
+ }
188
+
189
+ // Правила для blocked/
190
+ processDirectory(BLOCKED_DIR, [
191
+ {
192
+ condition: (status) => status === 'passed',
193
+ toDir: DONE_DIR,
194
+ reason: 'review passed'
195
+ },
196
+ {
197
+ condition: (status) => status === 'failed',
198
+ toDir: BACKLOG_DIR,
199
+ reason: 'review failed'
200
+ }
201
+ // null (нет ревью) — не перемещаем
202
+ ]);
203
+
204
+ // Правила для done/
205
+ processDirectory(DONE_DIR, [
206
+ {
207
+ condition: (status) => status === 'failed',
208
+ toDir: BACKLOG_DIR,
209
+ reason: 'review failed'
210
+ }
211
+ // passed или null — не перемещаем (важно для legacy-тикетов без ревью)
212
+ ]);
213
+
214
+ // Правила для review/
215
+ processDirectory(REVIEW_DIR, [
216
+ {
217
+ condition: (status) => status === 'passed',
218
+ toDir: DONE_DIR,
219
+ reason: 'review passed'
220
+ }
221
+ ]);
222
+
223
+ // Правила для in-progress/
224
+ processDirectory(IN_PROGRESS_DIR, [
225
+ {
226
+ condition: (status) => status === 'passed',
227
+ toDir: DONE_DIR,
228
+ reason: 'review passed'
229
+ }
230
+ ]);
231
+
232
+ // Правила для ready/
233
+ processDirectory(READY_DIR, [
234
+ {
235
+ condition: (status) => status === 'passed',
236
+ toDir: DONE_DIR,
237
+ reason: 'review passed'
238
+ }
239
+ ]);
240
+
241
+ return { moved };
242
+ }
243
+
92
244
  /**
93
245
  * Считывает все тикеты из директории ready/
94
246
  */
@@ -329,6 +481,13 @@ async function main() {
329
481
  console.log(`[INFO] Filtering by plan_id: ${planId}`);
330
482
  }
331
483
 
484
+ // Авто-коррекция тикетов перед выбором задачи
485
+ console.log('[INFO] Running auto-correction...');
486
+ const correctionResult = autoCorrectTickets();
487
+ if (correctionResult.moved.length > 0) {
488
+ console.log(`[INFO] Auto-corrected ${correctionResult.moved.length} ticket(s)`);
489
+ }
490
+
332
491
  console.log(`[INFO] Scanning ready/ directory: ${READY_DIR}`);
333
492
 
334
493
  const result = pickNextTicket(planId);
@@ -340,7 +499,14 @@ async function main() {
340
499
  console.log(`[INFO] ${result.reason}`);
341
500
  }
342
501
 
343
- printResult(result);
502
+ // Добавляем информацию о авто-коррекции в результат
503
+ const finalResult = {
504
+ ...result,
505
+ auto_corrected: correctionResult.moved.length,
506
+ moved_tickets: correctionResult.moved.map(m => m.id).join(',')
507
+ };
508
+
509
+ printResult(finalResult);
344
510
 
345
511
  if (result.status === 'empty') {
346
512
  process.exit(0);
@@ -86,6 +86,7 @@ description: Декомпозировать недочёты (gaps) из ана
86
86
  2. Для каждого тикета:
87
87
  - Определи следующий ID: найди все файлы `{TYPE}-*.md` во всех папках `.workflow/tickets/`, возьми максимальный номер для этого префикса и прибавь 1. Если файлов нет — начни с `{TYPE}-001`.
88
88
  - Заполни шаблон
89
+ - **Если доработка относится к плану** — заполни `parent_plan: "plans/current/PLAN-{plan_id}.md"`. Это обязательно, если `plan_id` передан в context.
89
90
  - В описании укажи: `Доработка по результатам REPORT-{report_id}`
90
91
  - Сохрани в `.workflow/tickets/backlog/{TYPE}-{NNN}.md`
91
92
 
@@ -76,6 +76,7 @@ description: Декомпозировать план на исполняемые
76
76
  2. Для каждого тикета:
77
77
  - Определи следующий ID: найди все файлы `{TYPE}-*.md` во всех папках `.workflow/tickets/`, возьми максимальный номер для этого префикса и прибавь 1. Если файлов нет — начни с `{TYPE}-001`.
78
78
  - Заполни шаблон
79
+ - **Обязательно** заполни `parent_plan: "plans/current/PLAN-{NNN}.md"` — путь к плану, из которого создан тикет
79
80
  - Сохрани в `.workflow/tickets/backlog/{TYPE}-{NNN}.md`
80
81
 
81
82
  ### 7. Обновить план