workflow-ai 1.0.25 → 1.0.27
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/ticket-movement-rules.yaml +83 -0
- package/package.json +1 -1
- package/src/init.mjs +16 -10
- package/src/lib/js-yaml.mjs +3856 -0
- package/src/lib/logger.mjs +73 -0
- package/src/lib/utils.mjs +108 -11
- package/src/runner.mjs +18 -17
- package/src/scripts/check-anomalies.js +1 -1
- package/src/scripts/move-ticket.js +1 -1
- package/src/scripts/move-to-ready.js +1 -1
- package/src/scripts/pick-next-task.js +219 -126
- package/src/skills/decompose-gaps/SKILL.md +1 -6
- package/src/skills/decompose-plan/SKILL.md +1 -8
|
@@ -21,7 +21,10 @@
|
|
|
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, getLastReviewStatus, serializeFrontmatter } from '../lib/utils.mjs';
|
|
24
|
+
import { parseFrontmatter, printResult, normalizePlanId, extractPlanId, getLastReviewStatus, serializeFrontmatter, loadTicketMovementRules, checkAndClosePlan } from '../lib/utils.mjs';
|
|
25
|
+
import { createLogger } from '../lib/logger.mjs';
|
|
26
|
+
|
|
27
|
+
const logger = createLogger();
|
|
25
28
|
|
|
26
29
|
// Корень проекта
|
|
27
30
|
const PROJECT_DIR = findProjectRoot();
|
|
@@ -72,12 +75,133 @@ function checkCondition(condition) {
|
|
|
72
75
|
return false;
|
|
73
76
|
|
|
74
77
|
default:
|
|
75
|
-
|
|
76
|
-
console.error(`[WARN] Unknown condition type: ${type}`);
|
|
78
|
+
logger.warn(`Unknown condition type: ${type}`);
|
|
77
79
|
return true;
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Парсит секцию "## Ревью" тикета и возвращает все записи ревью.
|
|
85
|
+
* @param {string} content - Содержимое тикета
|
|
86
|
+
* @returns {Array<{date: string, status: string, comment: string}>}
|
|
87
|
+
*/
|
|
88
|
+
function parseReviewSection(content) {
|
|
89
|
+
if (!content) return [];
|
|
90
|
+
|
|
91
|
+
const headerIdx = content.search(/^##\s*Ревью\s*$/m);
|
|
92
|
+
if (headerIdx === -1) return [];
|
|
93
|
+
|
|
94
|
+
const bodyStart = content.indexOf('\n', headerIdx);
|
|
95
|
+
if (bodyStart === -1) return [];
|
|
96
|
+
|
|
97
|
+
const nextH2 = content.indexOf('\n## ', bodyStart);
|
|
98
|
+
const reviewSection = (nextH2 === -1
|
|
99
|
+
? content.slice(bodyStart + 1)
|
|
100
|
+
: content.slice(bodyStart + 1, nextH2)).trim();
|
|
101
|
+
|
|
102
|
+
const reviews = [];
|
|
103
|
+
|
|
104
|
+
const tableRows = reviewSection.split('\n').filter(line => line.trim().startsWith('|'));
|
|
105
|
+
if (tableRows.length >= 2) {
|
|
106
|
+
const dataRows = tableRows.slice(2).filter(row => {
|
|
107
|
+
const cells = row.split('|').map(c => c.trim()).filter(c => c);
|
|
108
|
+
return cells.length >= 2;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
for (const row of dataRows) {
|
|
112
|
+
const cells = row.split('|').map(c => c.trim()).filter(c => c);
|
|
113
|
+
const date = cells[0] || '';
|
|
114
|
+
const statusRaw = cells[1]?.toLowerCase() || '';
|
|
115
|
+
const comment = cells[2] || '';
|
|
116
|
+
let status = null;
|
|
117
|
+
if (statusRaw.includes('passed')) status = 'passed';
|
|
118
|
+
else if (statusRaw.includes('failed')) status = 'failed';
|
|
119
|
+
else if (statusRaw.includes('skipped')) status = 'skipped';
|
|
120
|
+
|
|
121
|
+
if (status) {
|
|
122
|
+
reviews.push({ date, status, comment });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const listItems = reviewSection.split('\n').filter(line => line.trim().match(/^[-*]\s/));
|
|
128
|
+
for (const item of listItems) {
|
|
129
|
+
const trimmed = item.trim();
|
|
130
|
+
const dateMatch = trimmed.match(/^[-*]\s*(\d{4}-\d{2}-\d{2})/);
|
|
131
|
+
const statusMatch = trimmed.match(/:\s*(passed|failed|skipped)\b/i);
|
|
132
|
+
if (dateMatch && statusMatch) {
|
|
133
|
+
reviews.push({
|
|
134
|
+
date: dateMatch[1],
|
|
135
|
+
status: statusMatch[1].toLowerCase(),
|
|
136
|
+
comment: trimmed.replace(/^[-*]\s*\d{4}-\d{2}-\d{2}:\s*(passed|failed|skipped)\b/i, '').trim()
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return reviews;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Вычисляет метрики ревью-итераций для всех тикетов
|
|
146
|
+
* @returns {object} Метрики: iterations, avgTimeToFirstPassed, failedVsPassed
|
|
147
|
+
*/
|
|
148
|
+
function calculateReviewMetrics() {
|
|
149
|
+
const allDirs = [BACKLOG_DIR, READY_DIR, IN_PROGRESS_DIR, BLOCKED_DIR, REVIEW_DIR, DONE_DIR];
|
|
150
|
+
const ticketMetrics = {};
|
|
151
|
+
let totalFailed = 0;
|
|
152
|
+
let totalPassed = 0;
|
|
153
|
+
let firstPassedTimes = [];
|
|
154
|
+
|
|
155
|
+
for (const dir of allDirs) {
|
|
156
|
+
if (!fs.existsSync(dir)) continue;
|
|
157
|
+
|
|
158
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md') && f !== '.gitkeep.md');
|
|
159
|
+
|
|
160
|
+
for (const file of files) {
|
|
161
|
+
const filePath = path.join(dir, file);
|
|
162
|
+
try {
|
|
163
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
164
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
165
|
+
const ticketId = frontmatter.id || file.replace('.md', '');
|
|
166
|
+
|
|
167
|
+
const reviews = parseReviewSection(content);
|
|
168
|
+
if (reviews.length === 0) continue;
|
|
169
|
+
|
|
170
|
+
ticketMetrics[ticketId] = reviews.length;
|
|
171
|
+
|
|
172
|
+
for (const review of reviews) {
|
|
173
|
+
if (review.status === 'failed') totalFailed++;
|
|
174
|
+
else if (review.status === 'passed') totalPassed++;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const firstPassed = reviews.find(r => r.status === 'passed');
|
|
178
|
+
if (firstPassed && firstPassed.date) {
|
|
179
|
+
const ticketCreated = new Date(frontmatter.created_at || '1970-01-01');
|
|
180
|
+
const passedDate = new Date(firstPassed.date);
|
|
181
|
+
const daysToPass = Math.floor((passedDate - ticketCreated) / (1000 * 60 * 60 * 24));
|
|
182
|
+
if (daysToPass >= 0) {
|
|
183
|
+
firstPassedTimes.push(daysToPass);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} catch (e) {
|
|
187
|
+
// Skip errors
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const avgTimeToFirstPassed = firstPassedTimes.length > 0
|
|
193
|
+
? Math.round(firstPassedTimes.reduce((a, b) => a + b, 0) / firstPassedTimes.length)
|
|
194
|
+
: null;
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
iterations_per_ticket: ticketMetrics,
|
|
198
|
+
total_failed: totalFailed,
|
|
199
|
+
total_passed: totalPassed,
|
|
200
|
+
avg_time_to_first_passed_days: avgTimeToFirstPassed,
|
|
201
|
+
tickets_with_reviews: Object.keys(ticketMetrics).length
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
81
205
|
/**
|
|
82
206
|
* Проверяет зависимости тикета
|
|
83
207
|
*/
|
|
@@ -94,19 +218,23 @@ function checkDependencies(dependencies) {
|
|
|
94
218
|
|
|
95
219
|
/**
|
|
96
220
|
* Авто-коррекция тикетов на основе статуса ревью.
|
|
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)
|
|
221
|
+
* Сканирует все директории и перемещает тикеты по правилам из конфига.
|
|
104
222
|
*
|
|
223
|
+
* @param {object} config - Конфигурация правил перемещения
|
|
105
224
|
* @returns {object} Результат: { moved: Array<{id, from, to, reason}> }
|
|
106
225
|
*/
|
|
107
|
-
function autoCorrectTickets() {
|
|
226
|
+
function autoCorrectTickets(config) {
|
|
108
227
|
const moved = [];
|
|
109
228
|
|
|
229
|
+
const dirMap = {
|
|
230
|
+
backlog: BACKLOG_DIR,
|
|
231
|
+
ready: READY_DIR,
|
|
232
|
+
in_progress: IN_PROGRESS_DIR,
|
|
233
|
+
blocked: BLOCKED_DIR,
|
|
234
|
+
review: REVIEW_DIR,
|
|
235
|
+
done: DONE_DIR
|
|
236
|
+
};
|
|
237
|
+
|
|
110
238
|
/**
|
|
111
239
|
* Перемещает тикет из одной директории в другую
|
|
112
240
|
*/
|
|
@@ -119,23 +247,18 @@ function autoCorrectTickets() {
|
|
|
119
247
|
}
|
|
120
248
|
|
|
121
249
|
try {
|
|
122
|
-
// Читаем содержимое
|
|
123
250
|
const content = fs.readFileSync(fromPath, 'utf8');
|
|
124
251
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
125
252
|
|
|
126
|
-
// Обновляем updated_at
|
|
127
253
|
frontmatter.updated_at = new Date().toISOString();
|
|
128
254
|
|
|
129
|
-
// Если перемещаем в done — ставим completed_at
|
|
130
255
|
if (toDir === DONE_DIR && !frontmatter.completed_at) {
|
|
131
256
|
frontmatter.completed_at = new Date().toISOString();
|
|
132
257
|
}
|
|
133
258
|
|
|
134
|
-
// Сериализуем и записываем в новую директорию
|
|
135
259
|
const newContent = serializeFrontmatter(frontmatter) + body;
|
|
136
260
|
fs.writeFileSync(toPath, newContent, 'utf8');
|
|
137
261
|
|
|
138
|
-
// Удаляем старый файл
|
|
139
262
|
fs.unlinkSync(fromPath);
|
|
140
263
|
|
|
141
264
|
console.log(`[AUTO-CORRECT] ${ticketId}: ${path.basename(fromDir)} → ${path.basename(toDir)} (${reason})`);
|
|
@@ -149,7 +272,7 @@ function autoCorrectTickets() {
|
|
|
149
272
|
|
|
150
273
|
return true;
|
|
151
274
|
} catch (e) {
|
|
152
|
-
|
|
275
|
+
logger.error(`Failed to move ticket ${ticketId}: ${e.message}`);
|
|
153
276
|
return false;
|
|
154
277
|
}
|
|
155
278
|
}
|
|
@@ -157,7 +280,7 @@ function autoCorrectTickets() {
|
|
|
157
280
|
/**
|
|
158
281
|
* Обрабатывает тикеты в указанной директории
|
|
159
282
|
*/
|
|
160
|
-
function processDirectory(dir, rules) {
|
|
283
|
+
function processDirectory(dir, rules, dirName) {
|
|
161
284
|
if (!fs.existsSync(dir)) return;
|
|
162
285
|
|
|
163
286
|
const files = fs.readdirSync(dir)
|
|
@@ -170,112 +293,46 @@ function autoCorrectTickets() {
|
|
|
170
293
|
const { frontmatter } = parseFrontmatter(content);
|
|
171
294
|
const ticketId = frontmatter.id || file.replace('.md', '');
|
|
172
295
|
|
|
173
|
-
// Получаем статус ревью
|
|
174
296
|
const reviewStatus = getLastReviewStatus(content);
|
|
175
297
|
|
|
176
|
-
// Применяем правила
|
|
177
298
|
for (const rule of rules) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
299
|
+
const ruleCondition = rule.condition;
|
|
300
|
+
let shouldMove = false;
|
|
301
|
+
|
|
302
|
+
if (ruleCondition === null) {
|
|
303
|
+
shouldMove = reviewStatus === null;
|
|
304
|
+
} else {
|
|
305
|
+
shouldMove = reviewStatus === ruleCondition;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (shouldMove) {
|
|
309
|
+
const targetDirName = rule.to_dir;
|
|
310
|
+
const targetDir = dirMap[targetDirName];
|
|
311
|
+
if (targetDir) {
|
|
312
|
+
moveTicket(ticketId, dir, targetDir, rule.reason);
|
|
313
|
+
}
|
|
314
|
+
break;
|
|
181
315
|
}
|
|
182
316
|
}
|
|
183
317
|
} catch (e) {
|
|
184
|
-
|
|
318
|
+
logger.warn(`Failed to process ticket ${file}: ${e.message}`);
|
|
185
319
|
}
|
|
186
320
|
}
|
|
187
321
|
}
|
|
188
322
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
{
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
]);
|
|
202
|
-
|
|
203
|
-
// Правила для blocked/
|
|
204
|
-
processDirectory(BLOCKED_DIR, [
|
|
205
|
-
{
|
|
206
|
-
condition: (status) => status === 'passed',
|
|
207
|
-
toDir: DONE_DIR,
|
|
208
|
-
reason: 'review passed'
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
condition: (status) => status === 'skipped',
|
|
212
|
-
toDir: DONE_DIR,
|
|
213
|
-
reason: 'review skipped'
|
|
214
|
-
},
|
|
215
|
-
{
|
|
216
|
-
condition: (status) => status === 'failed',
|
|
217
|
-
toDir: BACKLOG_DIR,
|
|
218
|
-
reason: 'review failed'
|
|
219
|
-
}
|
|
220
|
-
// null (нет ревью) — не перемещаем
|
|
221
|
-
]);
|
|
222
|
-
|
|
223
|
-
// Правила для done/
|
|
224
|
-
processDirectory(DONE_DIR, [
|
|
225
|
-
{
|
|
226
|
-
condition: (status) => status === 'failed',
|
|
227
|
-
toDir: BACKLOG_DIR,
|
|
228
|
-
reason: 'review failed'
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
condition: (status) => status === null,
|
|
232
|
-
toDir: BACKLOG_DIR,
|
|
233
|
-
reason: 'no review'
|
|
234
|
-
}
|
|
235
|
-
// passed или skipped — не перемещаем
|
|
236
|
-
]);
|
|
237
|
-
|
|
238
|
-
// Правила для review/
|
|
239
|
-
processDirectory(REVIEW_DIR, [
|
|
240
|
-
{
|
|
241
|
-
condition: (status) => status === 'passed',
|
|
242
|
-
toDir: DONE_DIR,
|
|
243
|
-
reason: 'review passed'
|
|
244
|
-
},
|
|
245
|
-
{
|
|
246
|
-
condition: (status) => status === 'skipped',
|
|
247
|
-
toDir: DONE_DIR,
|
|
248
|
-
reason: 'review skipped'
|
|
249
|
-
}
|
|
250
|
-
]);
|
|
251
|
-
|
|
252
|
-
// Правила для in-progress/
|
|
253
|
-
processDirectory(IN_PROGRESS_DIR, [
|
|
254
|
-
{
|
|
255
|
-
condition: (status) => status === 'passed',
|
|
256
|
-
toDir: DONE_DIR,
|
|
257
|
-
reason: 'review passed'
|
|
258
|
-
},
|
|
259
|
-
{
|
|
260
|
-
condition: (status) => status === 'skipped',
|
|
261
|
-
toDir: DONE_DIR,
|
|
262
|
-
reason: 'review skipped'
|
|
263
|
-
}
|
|
264
|
-
]);
|
|
265
|
-
|
|
266
|
-
// Правила для ready/
|
|
267
|
-
processDirectory(READY_DIR, [
|
|
268
|
-
{
|
|
269
|
-
condition: (status) => status === 'passed',
|
|
270
|
-
toDir: DONE_DIR,
|
|
271
|
-
reason: 'review passed'
|
|
272
|
-
},
|
|
273
|
-
{
|
|
274
|
-
condition: (status) => status === 'skipped',
|
|
275
|
-
toDir: DONE_DIR,
|
|
276
|
-
reason: 'review skipped'
|
|
323
|
+
if (!config || !config.rules) {
|
|
324
|
+
logger.error('Ticket movement rules config not loaded');
|
|
325
|
+
return { moved };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const rulesConfig = config.rules;
|
|
329
|
+
|
|
330
|
+
for (const [dirName, rules] of Object.entries(rulesConfig)) {
|
|
331
|
+
const dir = dirMap[dirName];
|
|
332
|
+
if (dir) {
|
|
333
|
+
processDirectory(dir, rules, dirName);
|
|
277
334
|
}
|
|
278
|
-
|
|
335
|
+
}
|
|
279
336
|
|
|
280
337
|
return { moved };
|
|
281
338
|
}
|
|
@@ -438,7 +495,7 @@ function pickNextTicket(planId) {
|
|
|
438
495
|
const completedInProgress = filterByPlan(findCompletedInProgress(), planId);
|
|
439
496
|
if (completedInProgress.length > 0) {
|
|
440
497
|
const first = completedInProgress[0];
|
|
441
|
-
|
|
498
|
+
logger.info(`Found completed ticket in in-progress/: ${first.id}`);
|
|
442
499
|
return {
|
|
443
500
|
status: 'completed_in_progress',
|
|
444
501
|
ticket_id: first.id
|
|
@@ -517,32 +574,68 @@ async function main() {
|
|
|
517
574
|
const planId = extractPlanId();
|
|
518
575
|
|
|
519
576
|
if (planId) {
|
|
520
|
-
|
|
577
|
+
logger.info(`Filtering by plan_id: ${planId}`);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const configPath = path.join(WORKFLOW_DIR, 'config', 'ticket-movement-rules.yaml');
|
|
581
|
+
let movementConfig = null;
|
|
582
|
+
try {
|
|
583
|
+
movementConfig = loadTicketMovementRules(configPath);
|
|
584
|
+
logger.info('Loaded ticket movement rules from config');
|
|
585
|
+
} catch (e) {
|
|
586
|
+
logger.warn(`Failed to load ticket movement config: ${e.message}`);
|
|
521
587
|
}
|
|
522
588
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
const correctionResult = autoCorrectTickets();
|
|
589
|
+
logger.info('Running auto-correction...');
|
|
590
|
+
const correctionResult = autoCorrectTickets(movementConfig);
|
|
526
591
|
if (correctionResult.moved.length > 0) {
|
|
527
|
-
|
|
592
|
+
logger.info(`Auto-corrected ${correctionResult.moved.length} ticket(s)`);
|
|
528
593
|
}
|
|
529
594
|
|
|
530
|
-
|
|
595
|
+
if (planId) {
|
|
596
|
+
const closeResult = checkAndClosePlan(WORKFLOW_DIR, planId);
|
|
597
|
+
if (closeResult.closed) {
|
|
598
|
+
logger.info(`Plan ${planId} closed: all ${closeResult.total} tickets done`);
|
|
599
|
+
} else if (closeResult.total > 0) {
|
|
600
|
+
logger.info(`Plan ${planId} progress: ${closeResult.done}/${closeResult.total} tickets done`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
logger.info(`Scanning ready/ directory: ${READY_DIR}`);
|
|
531
605
|
|
|
532
606
|
const result = pickNextTicket(planId);
|
|
533
607
|
|
|
534
608
|
if (result.status === 'found') {
|
|
535
|
-
|
|
536
|
-
|
|
609
|
+
logger.info(`Selected ticket: ${result.ticket_id} (${result.title})`);
|
|
610
|
+
logger.info(`Priority: ${result.priority}, Type: ${result.type}`);
|
|
537
611
|
} else {
|
|
538
|
-
|
|
612
|
+
logger.info(result.reason);
|
|
539
613
|
}
|
|
540
614
|
|
|
541
|
-
|
|
615
|
+
logger.info('Calculating review metrics...');
|
|
616
|
+
const reviewMetrics = calculateReviewMetrics();
|
|
617
|
+
logger.info(`Found ${reviewMetrics.tickets_with_reviews} tickets with reviews`);
|
|
618
|
+
logger.info(`Total failed: ${reviewMetrics.total_failed}, passed: ${reviewMetrics.total_passed}`);
|
|
619
|
+
|
|
620
|
+
const metricsDir = path.join(WORKFLOW_DIR, 'metrics');
|
|
621
|
+
if (!fs.existsSync(metricsDir)) {
|
|
622
|
+
fs.mkdirSync(metricsDir, { recursive: true });
|
|
623
|
+
}
|
|
624
|
+
const metricsFile = path.join(metricsDir, 'review-metrics.json');
|
|
625
|
+
fs.writeFileSync(metricsFile, JSON.stringify(reviewMetrics, null, 2), 'utf8');
|
|
626
|
+
logger.info(`Metrics saved to ${metricsFile}`);
|
|
627
|
+
|
|
542
628
|
const finalResult = {
|
|
543
629
|
...result,
|
|
544
630
|
auto_corrected: correctionResult.moved.length,
|
|
545
|
-
moved_tickets: correctionResult.moved.map(m => m.id).join(',')
|
|
631
|
+
moved_tickets: correctionResult.moved.map(m => m.id).join(','),
|
|
632
|
+
review_metrics: {
|
|
633
|
+
tickets_with_reviews: reviewMetrics.tickets_with_reviews,
|
|
634
|
+
total_failed: reviewMetrics.total_failed,
|
|
635
|
+
total_passed: reviewMetrics.total_passed,
|
|
636
|
+
avg_time_to_first_passed_days: reviewMetrics.avg_time_to_first_passed_days,
|
|
637
|
+
iterations_per_ticket: reviewMetrics.iterations_per_ticket
|
|
638
|
+
}
|
|
546
639
|
};
|
|
547
640
|
|
|
548
641
|
printResult(finalResult);
|
|
@@ -553,7 +646,7 @@ async function main() {
|
|
|
553
646
|
}
|
|
554
647
|
|
|
555
648
|
main().catch(e => {
|
|
556
|
-
|
|
649
|
+
logger.error(e.message);
|
|
557
650
|
printResult({ status: 'error', error: e.message });
|
|
558
651
|
process.exit(1);
|
|
559
652
|
});
|
|
@@ -63,12 +63,7 @@ description: Декомпозировать недочёты (gaps) из ана
|
|
|
63
63
|
|
|
64
64
|
### 5. Определить тип каждого тикета
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|-----|---------|-------------------|
|
|
68
|
-
| Исправление | FIX | Результат не соответствует ожиданиям |
|
|
69
|
-
| Реализация | IMPL | Задача была пропущена |
|
|
70
|
-
| Ревью | REVIEW | Нужна дополнительная проверка |
|
|
71
|
-
| Документация | DOCS | Не хватает документации |
|
|
66
|
+
Прочитай актуальные типы задач из `.workflow/config/config.yaml` (секция `task_types`). Используй **только** префиксы, определённые в конфиге. Не используй типы, которых нет в конфиге.
|
|
72
67
|
|
|
73
68
|
### 6. Назначить приоритеты
|
|
74
69
|
|
|
@@ -39,14 +39,7 @@ description: Декомпозировать план на исполняемые
|
|
|
39
39
|
|
|
40
40
|
### 3. Определить тип каждого тикета
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|-----|---------|----------|
|
|
44
|
-
| Архитектура | ARCH | Создание планов, архитектуры |
|
|
45
|
-
| Реализация | IMPL | Написание кода |
|
|
46
|
-
| Исправление | FIX | Багфиксы |
|
|
47
|
-
| Ревью | REVIEW | Проверка кода/документации |
|
|
48
|
-
| Документация | DOCS | Написание документации |
|
|
49
|
-
| Администрирование | ADMIN | Настройка, конфигурация |
|
|
42
|
+
Прочитай актуальные типы задач из `.workflow/config/config.yaml` (секция `task_types`). Используй **только** префиксы, определённые в конфиге. Не используй типы, которых нет в конфиге.
|
|
50
43
|
|
|
51
44
|
### 4. Определить зависимости
|
|
52
45
|
|