workflow-ai 1.2.1 → 1.3.1
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/CHANGELOG.md +61 -49
- package/README.md +64 -475
- package/configs/pipeline.yaml +90 -2
- package/package.json +11 -1
- package/src/lib/marker.mjs +108 -0
- package/src/lib/process-alive.mjs +11 -0
- package/src/lib/stop-command.mjs +82 -0
- package/src/runner.mjs +40 -6
- package/src/scripts/check-relevance.js +3 -1
- package/src/scripts/get-next-id.js +1 -1
- package/src/scripts/mark-blocked.js +160 -0
- package/src/scripts/move-ticket.js +100 -35
- package/src/scripts/pick-next-task.js +64 -35
- package/src/skills/__test-cal-001-1777553217513/SKILL.md +2 -0
- package/src/skills/__test-runner-1777553217483/SKILL.md +5 -0
- package/src/skills/coach/SKILL.md +2 -1
- package/src/skills/execute-task/SKILL.md +29 -2
- package/src/skills/review-result/SKILL.md +23 -1
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import fs from "fs";
|
|
14
14
|
import path from "path";
|
|
15
|
+
import { fileURLToPath } from "url";
|
|
15
16
|
import YAML from "workflow-ai/lib/js-yaml.mjs";
|
|
16
17
|
import { findProjectRoot } from "workflow-ai/lib/find-root.mjs";
|
|
17
18
|
import {
|
|
@@ -21,6 +22,11 @@ import {
|
|
|
21
22
|
getLastReviewStatus,
|
|
22
23
|
} from "workflow-ai/lib/utils.mjs";
|
|
23
24
|
|
|
25
|
+
const logger = {
|
|
26
|
+
info: (msg) => console.error(`[INFO] ${msg}`),
|
|
27
|
+
warn: (msg) => console.error(`[WARN] ${msg}`),
|
|
28
|
+
};
|
|
29
|
+
|
|
24
30
|
// Корень проекта
|
|
25
31
|
const PROJECT_DIR = findProjectRoot();
|
|
26
32
|
// Базовая директория workflow
|
|
@@ -89,6 +95,45 @@ function isValidTransition(from, to) {
|
|
|
89
95
|
return { valid: true };
|
|
90
96
|
}
|
|
91
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Hook для обновления approval-файлов при перемещении тикета
|
|
100
|
+
* @param {string} ticketId - ID тикета
|
|
101
|
+
* @param {string} target - целевой статус
|
|
102
|
+
* @param {object} fsModule - модуль fs (для mock в тестах)
|
|
103
|
+
* @param {string} workflowDir - директория .workflow
|
|
104
|
+
*/
|
|
105
|
+
function updateApprovalFilesHook(ticketId, target, fsModule = fs, workflowDir = WORKFLOW_DIR) {
|
|
106
|
+
try {
|
|
107
|
+
const approvalsDir = path.join(workflowDir, "approvals");
|
|
108
|
+
if (fsModule.existsSync(approvalsDir)) {
|
|
109
|
+
const files = fsModule.readdirSync(approvalsDir);
|
|
110
|
+
const escapedTicketId = ticketId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
111
|
+
const pattern = new RegExp(`^${escapedTicketId}_manual-gate-.*_\\d+\\.json$`);
|
|
112
|
+
for (const file of files) {
|
|
113
|
+
if (!pattern.test(file)) continue;
|
|
114
|
+
const filePath = path.join(approvalsDir, file);
|
|
115
|
+
try {
|
|
116
|
+
const data = JSON.parse(fsModule.readFileSync(filePath, "utf8"));
|
|
117
|
+
if (data.status === "pending") {
|
|
118
|
+
data.status = "approved";
|
|
119
|
+
data.decided_by = "move-ticket";
|
|
120
|
+
data.comment = `auto-approved on move to ${target}`;
|
|
121
|
+
data.updated_at = new Date().toISOString();
|
|
122
|
+
fsModule.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
|
|
123
|
+
logger.info(`Approval file ${file} auto-approved on move to ${target}`);
|
|
124
|
+
}
|
|
125
|
+
} catch (err) {
|
|
126
|
+
logger.warn(`Corrupt approval file ${file}: ${err.message}`);
|
|
127
|
+
// продолжаем, не падаем
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} catch (err) {
|
|
132
|
+
// Ошибка hook'а не должна фейлить само перемещение
|
|
133
|
+
logger.warn(`Approval hook error: ${err.message}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
92
137
|
/**
|
|
93
138
|
* Основная функция перемещения тикета
|
|
94
139
|
*/
|
|
@@ -210,6 +255,9 @@ async function moveTicket(ticketId, target) {
|
|
|
210
255
|
};
|
|
211
256
|
}
|
|
212
257
|
|
|
258
|
+
// Hook: обновление approval-файлов (если есть) — срабатывает на любой move
|
|
259
|
+
updateApprovalFilesHook(ticketId, target);
|
|
260
|
+
|
|
213
261
|
return {
|
|
214
262
|
status: "moved",
|
|
215
263
|
ticket_id: ticketId,
|
|
@@ -218,43 +266,60 @@ async function moveTicket(ticketId, target) {
|
|
|
218
266
|
};
|
|
219
267
|
}
|
|
220
268
|
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const targetMatch = prompt.match(/target:\s*(\S+)/);
|
|
235
|
-
ticketId = ticketMatch?.[1];
|
|
236
|
-
target = targetMatch?.[1];
|
|
237
|
-
if (!ticketId || !target) {
|
|
238
|
-
console.error(
|
|
239
|
-
"[ERROR] Cannot parse ticket_id or target from pipeline context",
|
|
240
|
-
);
|
|
241
|
-
printResult({
|
|
242
|
-
status: "error",
|
|
243
|
-
error: "Missing ticket_id or target in pipeline context",
|
|
244
|
-
});
|
|
245
|
-
process.exit(1);
|
|
269
|
+
// Export for testing
|
|
270
|
+
export { moveTicket, updateApprovalFilesHook };
|
|
271
|
+
|
|
272
|
+
// Main entry point — guard prevents execution when imported as module in tests.
|
|
273
|
+
// Используем fs.realpathSync чтобы корректно сравнивать пути на Windows когда .workflow/src/scripts/ — junction.
|
|
274
|
+
// Без realpathSync argv[1] = путь через junction, а import.meta.url = разрешённый target — строки не совпадают.
|
|
275
|
+
function __isMainModule() {
|
|
276
|
+
try {
|
|
277
|
+
const argvPath = fs.realpathSync(path.resolve(process.argv[1] || ""));
|
|
278
|
+
const metaPath = fileURLToPath(import.meta.url);
|
|
279
|
+
return argvPath === metaPath;
|
|
280
|
+
} catch {
|
|
281
|
+
return false;
|
|
246
282
|
}
|
|
247
|
-
} else {
|
|
248
|
-
console.error("Usage: node move-ticket.js <ticket_id> <target>");
|
|
249
|
-
console.error("Example: node move-ticket.js IMPL-001 in-progress");
|
|
250
|
-
console.error("Available targets:", VALID_STATUSES.join(", "));
|
|
251
|
-
printResult({ status: "error", error: "Missing arguments" });
|
|
252
|
-
process.exit(1);
|
|
253
283
|
}
|
|
254
284
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
285
|
+
if (__isMainModule()) {
|
|
286
|
+
const rawArgs = process.argv.slice(2);
|
|
287
|
+
let ticketId, target;
|
|
288
|
+
|
|
289
|
+
if (rawArgs.length >= 2) {
|
|
290
|
+
// Прямой вызов: node move-ticket.js IMPL-001 in-progress
|
|
291
|
+
ticketId = rawArgs[0];
|
|
292
|
+
target = rawArgs[1];
|
|
293
|
+
} else if (rawArgs.length === 1) {
|
|
294
|
+
// Вызов через pipeline runner: один аргумент — промпт с контекстом
|
|
295
|
+
// Формат: "skill-name\n\nContext:\n ticket_id: X\n target: Y\n..."
|
|
296
|
+
const prompt = rawArgs[0];
|
|
297
|
+
const ticketMatch = prompt.match(/ticket_id:\s*(\S+)/);
|
|
298
|
+
const targetMatch = prompt.match(/target:\s*(\S+)/);
|
|
299
|
+
ticketId = ticketMatch?.[1];
|
|
300
|
+
target = targetMatch?.[1];
|
|
301
|
+
if (!ticketId || !target) {
|
|
302
|
+
console.error(
|
|
303
|
+
"[ERROR] Cannot parse ticket_id or target from pipeline context",
|
|
304
|
+
);
|
|
305
|
+
printResult({
|
|
306
|
+
status: "error",
|
|
307
|
+
error: "Missing ticket_id or target in pipeline context",
|
|
308
|
+
});
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
console.error("Usage: node move-ticket.js <ticket_id> <target>");
|
|
313
|
+
console.error("Example: node move-ticket.js IMPL-001 in-progress");
|
|
314
|
+
console.error("Available targets:", VALID_STATUSES.join(", "));
|
|
315
|
+
printResult({ status: "error", error: "Missing arguments" });
|
|
258
316
|
process.exit(1);
|
|
259
317
|
}
|
|
260
|
-
|
|
318
|
+
|
|
319
|
+
moveTicket(ticketId, target).then((result) => {
|
|
320
|
+
printResult(result);
|
|
321
|
+
if (result.status === "error") {
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
@@ -567,28 +567,25 @@ function pickNextTicket(planId) {
|
|
|
567
567
|
return { status: 'empty', reason: 'No tickets in ready/' };
|
|
568
568
|
}
|
|
569
569
|
|
|
570
|
-
//
|
|
571
|
-
const
|
|
570
|
+
// Фильтрация: разделяем на обычные и human с проверкой условий/зависимостей
|
|
571
|
+
const eligibleNonHuman = [];
|
|
572
|
+
const humanCandidates = [];
|
|
573
|
+
|
|
574
|
+
for (const ticket of tickets) {
|
|
572
575
|
const { frontmatter } = ticket;
|
|
573
576
|
|
|
574
|
-
// Пропускаем тикеты, требующие ручного выполнения
|
|
575
|
-
if (frontmatter.type === 'human') {
|
|
576
|
-
logger.info(`Skipping ticket ${ticket.id}: type is 'human' (requires manual execution)`);
|
|
577
|
-
return false;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
577
|
// Проверка условий
|
|
581
578
|
const conditions = frontmatter.conditions || [];
|
|
582
579
|
const conditionsMet = conditions.every(checkCondition);
|
|
583
580
|
if (!conditionsMet) {
|
|
584
|
-
|
|
581
|
+
continue;
|
|
585
582
|
}
|
|
586
583
|
|
|
587
584
|
// Проверка зависимостей
|
|
588
585
|
const dependencies = frontmatter.dependencies || [];
|
|
589
586
|
const depsMet = checkDependencies(dependencies);
|
|
590
587
|
if (!depsMet) {
|
|
591
|
-
|
|
588
|
+
continue;
|
|
592
589
|
}
|
|
593
590
|
|
|
594
591
|
// Обнаружение и удаление дубликатов: тикет не должен существовать в других колонках
|
|
@@ -607,46 +604,75 @@ function pickNextTicket(planId) {
|
|
|
607
604
|
} catch (err) {
|
|
608
605
|
logger.error(`Failed to archive duplicate ${ticket.id}: ${err.message}`);
|
|
609
606
|
}
|
|
610
|
-
|
|
607
|
+
continue;
|
|
611
608
|
}
|
|
612
609
|
|
|
613
|
-
|
|
614
|
-
|
|
610
|
+
// Разделение по типу
|
|
611
|
+
if (frontmatter.type === 'human') {
|
|
612
|
+
humanCandidates.push(ticket);
|
|
613
|
+
} else {
|
|
614
|
+
eligibleNonHuman.push(ticket);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Имеются ли обычные (non-human) готовые тикеты — старший приоритет
|
|
619
|
+
if (eligibleNonHuman.length > 0) {
|
|
620
|
+
eligibleNonHuman.sort((a, b) => {
|
|
621
|
+
const priorityA = a.frontmatter.priority || 999;
|
|
622
|
+
const priorityB = b.frontmatter.priority || 999;
|
|
615
623
|
|
|
616
|
-
|
|
624
|
+
if (priorityA !== priorityB) {
|
|
625
|
+
return priorityA - priorityB;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const dateA = new Date(a.frontmatter.created_at || '9999-12-31');
|
|
629
|
+
const dateB = new Date(b.frontmatter.created_at || '9999-12-31');
|
|
630
|
+
return dateA - dateB;
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
const selected = eligibleNonHuman[0];
|
|
617
634
|
return {
|
|
618
|
-
status: '
|
|
619
|
-
|
|
635
|
+
status: 'found',
|
|
636
|
+
ticket_id: selected.id,
|
|
637
|
+
priority: selected.frontmatter.priority,
|
|
638
|
+
title: selected.frontmatter.title,
|
|
639
|
+
type: selected.frontmatter.type,
|
|
640
|
+
required_capabilities: JSON.stringify(selected.frontmatter.required_capabilities || [])
|
|
620
641
|
};
|
|
621
642
|
}
|
|
622
643
|
|
|
623
|
-
//
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
644
|
+
// Если есть созревшие human-тикеты — новый статус human_ready (для manual-gate)
|
|
645
|
+
if (humanCandidates.length > 0) {
|
|
646
|
+
humanCandidates.sort((a, b) => {
|
|
647
|
+
const priorityA = a.frontmatter.priority || 999;
|
|
648
|
+
const priorityB = b.frontmatter.priority || 999;
|
|
627
649
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
650
|
+
if (priorityA !== priorityB) {
|
|
651
|
+
return priorityA - priorityB;
|
|
652
|
+
}
|
|
631
653
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
});
|
|
654
|
+
const dateA = new Date(a.frontmatter.created_at || '9999-12-31');
|
|
655
|
+
const dateB = new Date(b.frontmatter.created_at || '9999-12-31');
|
|
656
|
+
return dateA - dateB;
|
|
657
|
+
});
|
|
637
658
|
|
|
638
|
-
|
|
659
|
+
const selected = humanCandidates[0];
|
|
660
|
+
return {
|
|
661
|
+
status: 'human_ready',
|
|
662
|
+
ticket_id: selected.id,
|
|
663
|
+
priority: selected.frontmatter.priority,
|
|
664
|
+
title: selected.frontmatter.title,
|
|
665
|
+
pending_count: humanCandidates.length
|
|
666
|
+
};
|
|
667
|
+
}
|
|
639
668
|
|
|
640
669
|
return {
|
|
641
|
-
status: '
|
|
642
|
-
|
|
643
|
-
priority: selected.frontmatter.priority,
|
|
644
|
-
title: selected.frontmatter.title,
|
|
645
|
-
type: selected.frontmatter.type,
|
|
646
|
-
required_capabilities: JSON.stringify(selected.frontmatter.required_capabilities || [])
|
|
670
|
+
status: 'empty',
|
|
671
|
+
reason: 'No eligible non-human tickets (and no ready human tickets)'
|
|
647
672
|
};
|
|
648
673
|
}
|
|
649
674
|
|
|
675
|
+
|
|
650
676
|
/**
|
|
651
677
|
* Архивирует все done-тикеты, принадлежащие архивным планам (plans/archive/).
|
|
652
678
|
* Сканирует все планы в plans/archive/, находит их тикеты в done/ и перемещает в archive/.
|
|
@@ -778,6 +804,9 @@ async function main() {
|
|
|
778
804
|
}
|
|
779
805
|
}
|
|
780
806
|
|
|
807
|
+
// Экспортируем функции для тестирования
|
|
808
|
+
export { pickNextTicket, readReadyTickets, readReviewTickets, readInProgressTickets, findCompletedInProgress, filterByPlan };
|
|
809
|
+
|
|
781
810
|
main().catch(e => {
|
|
782
811
|
logger.error(e.message);
|
|
783
812
|
printResult({ status: 'error', error: e.message });
|
|
@@ -123,11 +123,12 @@ ticket_prefix: COACH
|
|
|
123
123
|
|
|
124
124
|
## Принципы
|
|
125
125
|
|
|
126
|
-
1. **Root Cause First** — при обнаружении проблемы в артефакте (тикете, плане, отчёте) всегда определи скил-источник, который создал этот артефакт, и предложи исправить **скил** первым. Не предлагай ручную правку артефактов (последствий), пока корневая причина (скил) не исправлена. Порядок действий: (1) найти скил-источник → (2) проследить цепочку вверх: если артефакт-источник (план, шаблон) уже содержал дефект — root cause в скиле, создавшем **его**, а не в скиле-обработчике → (3) исправить скил → (4) если нужно, предложить пересоздать артефакт исправленным скилом. **Антипаттерн «остановка на ближайшем скиле»:** тикет неатомарен → правишь декомпозитор. Но если задача **плана** уже неатомарна — root cause в скиле планирования, декомпозитор — вторая линия обороны. **Антипаттерн:** если данные невалидны — root cause в том, кто/что генерирует данные (шаблон, скил, воркфлоу), а НЕ в обработчике данных (скрипт, парсер). Не правь обработчик под невалидный формат — исправь источник формата. **⚠️ Обязательно перед правкой:** прочитай лог или артефакт до конца — определи точный паттерн нарушения (кто, когда, что именно записал). Гипотеза о root cause без evidence из лога — не основание для правки. **Семантика первична:** перед диагностикой сформулируй назначение скила одним предложением (что он должен решать, что НЕ должен). Если поведение противоречит назначению — это ошибка в скиле, не в смежных компонентах. **⚠️ Физический автор ≠ семантический владелец:** при определении скила-источника ищи не «кто владеет предметной областью артефакта», а **кто физически записывает** (Edit/Write) проблемный фрагмент. Если скил A выполняет предметную работу, но скил B записывает результат в тикет — root cause в инструкциях скила B, а не A. Антипаттерн: «тикет предметной области X → правлю скил предметной области», хотя физическую запись в тикет выполняет скил-исполнитель. **⛔ Повторный инцидент по той же корневой проблеме:** перед формулированием правки **обязательно** прогрепай `coach-backlog.yaml` на ключевые термины текущей проблемы (имя скила-жертвы, имя нарушенного правила, имя задействованной нормы). Если обнаружен CHG за последние 30 дней на тот же скил и ту же корневую проблему — это сигнал, что **текстовое усиление инструкции не работает** (предыдущий текст уже содержал норму, но нарушитель её проигнорировал). В этом случае: (а) ещё одна текстовая правка того же скила — недостаточная мера; (б) обязательно создай тикет эскалации стейкхолдеру с рекомендацией ввести **машинную защиту**, не зависящую от дисциплины агента (валидация пайплайном, пост-гейт-стадия, автоматический откат, инфраструктурная проверка); (в) в тикете явно опиши, что попытки дисциплинарного усиления исчерпаны, и почему только машинная защита закрывает класс ошибки. Текстовую правку всё равно применяй — она страхует дисциплинированного агента, — но **не считай её решением проблемы**, пока машинная защита не введена.
|
|
126
|
+
1. **Root Cause First** — при обнаружении проблемы в артефакте (тикете, плане, отчёте) всегда определи скил-источник, который создал этот артефакт, и предложи исправить **скил** первым. Не предлагай ручную правку артефактов (последствий), пока корневая причина (скил) не исправлена. Порядок действий: (1) найти скил-источник → (2) проследить цепочку вверх: если артефакт-источник (план, шаблон) уже содержал дефект — root cause в скиле, создавшем **его**, а не в скиле-обработчике → (3) исправить скил → (4) если нужно, предложить пересоздать артефакт исправленным скилом. **Антипаттерн «остановка на ближайшем скиле»:** тикет неатомарен → правишь декомпозитор. Но если задача **плана** уже неатомарна — root cause в скиле планирования, декомпозитор — вторая линия обороны. **Антипаттерн:** если данные невалидны — root cause в том, кто/что генерирует данные (шаблон, скил, воркфлоу), а НЕ в обработчике данных (скрипт, парсер). Не правь обработчик под невалидный формат — исправь источник формата. **⚠️ Обязательно перед правкой:** прочитай лог или артефакт до конца — определи точный паттерн нарушения (кто, когда, что именно записал). Гипотеза о root cause без evidence из лога — не основание для правки. **Семантика первична:** перед диагностикой сформулируй назначение скила одним предложением (что он должен решать, что НЕ должен). Если поведение противоречит назначению — это ошибка в скиле, не в смежных компонентах. **⚠️ Физический автор ≠ семантический владелец:** при определении скила-источника ищи не «кто владеет предметной областью артефакта», а **кто физически записывает** (Edit/Write) проблемный фрагмент. Если скил A выполняет предметную работу, но скил B записывает результат в тикет — root cause в инструкциях скила B, а не A. Антипаттерн: «тикет предметной области X → правлю скил предметной области», хотя физическую запись в тикет выполняет скил-исполнитель. **⛔ Повторный инцидент по той же корневой проблеме:** перед формулированием правки **обязательно** прогрепай `coach-backlog.yaml` на ключевые термины текущей проблемы (имя скила-жертвы, имя нарушенного правила, имя задействованной нормы). Если обнаружен CHG за последние 30 дней на тот же скил и ту же корневую проблему — **сначала сравни даты выполнения тикета-нарушителя и применения предыдущего CHG, до классификации «повторный инцидент»**. Источники дат: для CHG — `git log` на файл скила (commit-дата правки) или `analyzed_tickets.analyzed_date` в `coach-backlog.yaml`; для тикета — `updated_at`/`completed_at` в frontmatter или ближайшие временные метки в логе пайплайна, где произошёл инцидент (стадии execute-task / review-result, дата записи `## Ревью`). **Если выполнение тикета предшествует дате CHG** — это **не** повторный инцидент: тикет проходил пайплайн до того, как текстовое усиление было применено, нарушение исторически объяснимо (фикса ещё не существовало). Класс — обычный новый, эскалация на машинную защиту **не нужна**, обработай как стандартный CHG. **Только если выполнение тикета строго ПОСЛЕ даты CHG** — это сигнал, что **текстовое усиление инструкции не работает** (предыдущий текст уже содержал норму, но нарушитель её проигнорировал). В этом случае: (а) ещё одна текстовая правка того же скила — недостаточная мера; (б) обязательно создай тикет эскалации стейкхолдеру с рекомендацией ввести **машинную защиту**, не зависящую от дисциплины агента (валидация пайплайном, пост-гейт-стадия, автоматический откат, инфраструктурная проверка); (в) в тикете явно опиши, что попытки дисциплинарного усиления исчерпаны, и почему только машинная защита закрывает класс ошибки. Текстовую правку всё равно применяй — она страхует дисциплинированного агента, — но **не считай её решением проблемы**, пока машинная защита не введена. Антипаттерн 1: «усилю формулировку ещё жёстче, напишу ⛔ крупнее» — агент, который не прочитал прошлую норму, не прочитает и новую. Антипаттерн 2: классифицировать «повторный инцидент» по факту повторяемости пары (скил, класс ошибки) **без сравнения дат** CHG и выполнения тикета. Эскалация без date-check — преждевременная: возможно, тикет проходил пайплайн ещё до текстового усиления, и фикс не имел шанса сработать. **Date-check — необходимое, но не достаточное условие.** Дополнительно сверь **семантический класс ошибки** предыдущего CHG и текущего инцидента: даже если выполнение тикета строго после CHG, инцидент может быть другого класса (CHG покрывал ортогональную проблему — например, semantic-mismatch vs structural-integrity, test-isolation vs review-rubric). Тогда это новый класс, а не повторный инцидент, и эскалация не требуется. Условие эскалации: **(дата выполнения > дата CHG) И (семантический класс совпадает)**.
|
|
127
127
|
2. **Evidence-Based** — все выводы основаны на данных из завершённых тикетов, планов и логов пайплайна, а не на предположениях. **При анализе лога обязательно строй временную диаграмму ключевых событий по ID артефакта** (тикет, план, отчёт): проследи всю цепочку перемещений/изменений артефакта от первого упоминания до последнего, обращая внимание на события, отстоящие далеко друг от друга по времени, но связанные одним ID. **Антипаттерн:** прочитал начало лога (события archive/cleanup), прочитал середину (события create/decompose), но **не сопоставил** их — упустил коллизию ID или другой паттерн взаимного влияния. Перед формулированием findings задай себе вопрос: «Я проверил всю историю каждого упомянутого ID, или только последнее событие с ним?» **⚠️ Проверка фактической практики перед нормативной правкой:** если правка вводит новое правило про путь, имя, формат, расположение — **обязательно `Grep` по всему проекту** (код, конфиги, скилы, тикеты) на ключевой термин этого правила, чтобы измерить **масштаб уже существующей практики**. Один-два аномальных артефакта — не основание объявлять их новой нормой. Если фактическая практика противоположна гипотезе — гипотеза неверна, или (если стейкхолдер действительно хочет миграцию) нужен явный миграционный план и согласие на масштаб правок. Антипаттерн: получил короткий ответ стейкхолдера на развилку → принял за сильное правило → пошёл править скилы → не проверил, что в проекте 20+ артефактов уже живут по противоположному правилу. Перед каждой нормативной правкой задай себе вопрос: «Сколько уже существующих файлов/строк проекта противоречат тому, что я собираюсь записать?» Если ответ > 5 — остановись и переспроси у стейкхолдера, точно ли это миграция. **⚠️ Обязательный diff формулировок при анализе цепочки артефактов:** когда анализируешь инцидент, прошедший через несколько стадий (план → тикет → исполнение → ревью), **перед назначением виновного** обязан построчно сопоставить формулировки критериев на каждом стыке: (1) дословная строка критерия в плане, (2) дословная строка в тикете, (3) что реально проверяет assertion/тест, (4) что ревьюер проверял. Виновник — стадия, на которой произошла первая потеря семантики. Антипаттерн: прочитал план и увидел расхождение с результатом → обвинил последнюю стадию (ревьюера), не проверив, на какой промежуточной стадии формулировка была ослаблена. Гипотеза «ревьюер должен был поймать» невалидна, если ревьюер работал по формулировке тикета, а тикет уже не содержал потерянного уточнения.
|
|
128
128
|
**⚠️ Антипаттерн «уход в формулировки вместо root cause»:** стейкхолдер задаёт вопрос о наблюдаемом дефекте («почему не поймали?»), а коуч анализирует текст формулировок, семантику переносов, чеклисты — вместо того чтобы ответить на прямой вопрос: какой конкретный шаг в какой конкретной стадии не выполнил конкретное физическое действие (открыть файл, посмотреть на картинку, запустить команду). Формулировки — это причина второго порядка; причина первого порядка — «агент X не сделал действие Y». Всегда начинай с причины первого порядка, потом объясняй, почему инструкции это допустили.
|
|
129
129
|
**⚠️ Антипаттерн «оценка по результату вместо сверки с инструкцией»:** при анализе действия агента — **не оценивай** его «разумность» или «допустимость» по своему суждению. Вместо этого открой скил агента и **дословно сверь** действие с инструкцией. Если инструкция говорит «разбей тикет», а агент объединил шаги — это нарушение, даже если результат выглядит «приемлемо». Коуч не имеет права смягчать finding на основании того, что дефект «небольшой» или «единичный» — скил либо нарушен, либо нет.
|
|
130
130
|
**⚠️ Антипаттерн «пересказ вместо цитаты» при утверждениях о коде:** перед утверждением вида «скрипт/функция X использует/читает/пишет Y» обязан открыть файл и **дословно процитировать** строку, на которой это поведение происходит. Пересказ по памяти (даже свежей) теряет операторы-fallback (`a || b`, `a ?? b`), условные ветви, ранние return'ы — те детали, которые как раз и задают реальное поведение. Источник ошибки: агент видит ключевое слово в строке, строит «достаточное» утверждение о поведении и идёт дальше. Правило: если утверждение про код войдёт в финдинг, CHG, черновик правки или ответ стейкхолдеру — строка должна быть в отчёте целиком (либо скопированной в цитату, либо явной ссылкой `file:line`, открытой и перечитанной непосредственно перед утверждением).
|
|
131
|
+
**⚠️ Антипаттерн «отрицательное утверждение о capability инструмента без проверки» (расширение предыдущего):** правило «дословная цитата» применяется не только к **позитивным** утверждениям («X делает Y»), но и к **отрицательным** («инструмент/фреймворк/тест X **не умеет / не покрывает / не подходит для** Y»). Отрицательное утверждение о capability — такое же утверждение о коде, как и позитивное, и требует тех же доказательств: либо (1) Grep по тестам/коду проекта, использующим инструмент, на ключевой паттерн использования (например, для гипотезы «Playwright не покрывает extension popup» — `Grep "loadExtension|launchPersistentContext|chrome-extension://"` по `tests/`), либо (2) дословная цитата из официальной документации. Источник ошибки: агент экстраполирует «общеизвестное» назначение инструмента (Playwright = веб-сайты, jest = unit-тесты и т.п.) без проверки фактической практики проекта или актуальных возможностей. Триггер срабатывания: если в черновике финдинга/ответа есть конструкция «{инструмент} не {глагол} {объект}» и от неё зависит root cause или CHG — **обязательна** проверка перед показом стейкхолдеру. Без проверки финдинг невалиден, даже если интуиция кажется правильной.
|
|
131
132
|
3. **Итеративность** — улучшай скилы инкрементально. Маленькие точечные улучшения > масштабные переписывания.
|
|
132
133
|
4. **Обратная совместимость** — улучшения не должны ломать существующие воркфлоу и интеграции.
|
|
133
134
|
5. **Актуальность знаний** — активно ищи в интернете лучшие практики, фреймворки и подходы для обогащения скилов.
|
|
@@ -70,6 +70,14 @@ description: >
|
|
|
70
70
|
|----------|----------------|
|
|
71
71
|
| `algorithms/execution-strategy.md` | **ВСЕГДА** — стратегия анализа, выполнения и верификации задачи |
|
|
72
72
|
|
|
73
|
+
## Загрузка шаблонов
|
|
74
|
+
|
|
75
|
+
Подгружай из `templates/` при необходимости:
|
|
76
|
+
|
|
77
|
+
| Шаблон | Когда загружать |
|
|
78
|
+
|--------|----------------|
|
|
79
|
+
| `templates/result-template.md` | При создании секции `## Result` в тикете — структура и правила заполнения |
|
|
80
|
+
|
|
73
81
|
## Шаги выполнения
|
|
74
82
|
|
|
75
83
|
### 1. Прочитать тикет
|
|
@@ -122,6 +130,8 @@ description: >
|
|
|
122
130
|
3. Прочитать `context.notes` — дополнительный контекст от создателя тикета
|
|
123
131
|
4. Если тикет ссылается на план (`parent_plan`) — прочитать план для понимания общей картины
|
|
124
132
|
|
|
133
|
+
**⛔ Валидация путей перед Edit:** если DoD требует изменить конкретный файл, указанный в `context.files` или описании, используй **ровно тот путь**, который указан. Перед каждым Edit сверь целевой путь с путём из контекста тикета. Одноимённый файл в другой директории — не целевой файл. Пример нарушения: тикет указывает `workflow-ai/README.md`, агент редактирует `./README.md`.
|
|
134
|
+
|
|
125
135
|
### 5. Выполнить работу и фиксировать результат инкрементально
|
|
126
136
|
|
|
127
137
|
Действовать по описанию и DoD тикета. Подход определяется **содержимым тикета**, а не типом:
|
|
@@ -131,6 +141,8 @@ description: >
|
|
|
131
141
|
- Если тикет требует тестирования — выполнить чеклист проверок из DoD, зафиксировать pass/fail по каждому пункту
|
|
132
142
|
- Если тикет требует исследования — использовать доступные инструменты для сбора данных, подкреплять источниками
|
|
133
143
|
|
|
144
|
+
**⛔ Перед вставкой кода в существующий файл — прочитай целевой участок и пойми его структуру.** Определи границы функции/класса/блока, в который вставляешь код. Вставляй код в семантически правильное место, а не в произвольную точку файла. Если не уверен в месте вставки — прочитай окружающий контекст (строки до и после) и убедись, что вставка не разрывает существующую структуру.
|
|
145
|
+
|
|
134
146
|
**⚠️ ИНКРЕМЕНТАЛЬНАЯ ЗАПИСЬ (ОБЯЗАТЕЛЬНО):**
|
|
135
147
|
|
|
136
148
|
После выполнения **каждого пункта** — **сразу** запиши результат в тикет:
|
|
@@ -156,7 +168,7 @@ description: >
|
|
|
156
168
|
|
|
157
169
|
К этому моменту секция Result уже содержит результаты по каждому пункту (записаны инкрементально на шаге 5). Осталось:
|
|
158
170
|
|
|
159
|
-
- Обновить/добавить
|
|
171
|
+
- Обновить/добавить **Что сделано** — краткое резюме всей работы
|
|
160
172
|
- Дополнить **Изменённые файлы** и **Заметки** если нужно
|
|
161
173
|
- **НЕ удалять и не переписывать** уже записанные результаты
|
|
162
174
|
|
|
@@ -191,13 +203,21 @@ description: >
|
|
|
191
203
|
|
|
192
204
|
### 9. Вывести структурированный результат
|
|
193
205
|
|
|
206
|
+
**⛔ Перед выводом RESULT пройди все три GATE из `workflows/execute.md` (шаг 6).** GATE-1 (Edit-проверка), GATE-2 (механическая Read-проверка), GATE-3 (self-check). При любом нарушении — вернись к шагам 5–7.
|
|
207
|
+
|
|
208
|
+
⛔ **GATE-1 — EDIT-ПРОВЕРКА (выполни ПЕРЕД Read-проверкой):**
|
|
209
|
+
|
|
210
|
+
За текущую сессию ты должен был вызвать инструмент **`Edit`** на файл тикета как минимум дважды: один раз для обновления DoD-чекбоксов (`[ ]` → `[x]`), один раз для записи секции Result. Если ни одного вызова `Edit` на файл тикета `.workflow/tickets/in-progress/{TICKET-ID}.md` не было — секция Result физически пустая и DoD не отмечен, независимо от написанного в stdout. Это призрачное выполнение (ограничение #9). Немедленно вернись к шагам 5–7.
|
|
211
|
+
|
|
212
|
+
**Ключевой принцип:** stdout (текст ответа) ≠ файл тикета. Результат существует только в том, что записано через инструмент `Edit`.
|
|
213
|
+
|
|
194
214
|
**⛔ ОБЯЗАТЕЛЬНАЯ МЕХАНИЧЕСКАЯ ПРОВЕРКА — перечитай файл тикета перед RESULT:**
|
|
195
215
|
|
|
196
216
|
Перед выводом `---RESULT---` выполни `Read` на файл тикета (`.workflow/tickets/in-progress/{TICKET-ID}.md`) и глазами убедись:
|
|
197
217
|
|
|
198
218
|
1. **Ни одного чекбокса `[ ]`** в секции критериев готовности / DoD. Все переведены в `[x]` или помечены причиной невыполнения (`[x] Пункт — не применимо: <причина>`).
|
|
199
219
|
2. **Секция `## Result` / `## Результат выполнения` физически заполнена** — содержит реальный текст (summary, изменённые файлы, заметки), а не оставлена в виде скелета-шаблона с `### Что сделано\n- ...`.
|
|
200
|
-
3. **Frontmatter не
|
|
220
|
+
3. **Frontmatter не модифицирован вообще** — никаких новых ключей, никаких изменённых значений (включая `notes`, `tags`, `context.*`). Frontmatter — собственность создателя тикета и пайплайна. Эта проверка шире, чем запрет на `status:` и `completed_at:` (см. ограничение #5): любая правка frontmatter — потенциально источник YAML-несовместимостей (двоеточие+пробел в неэкранированном скаляре, дубль ключа, нарушенный отступ массива). Если необходимо зафиксировать прогресс/итерации/наблюдения — пиши в **тело тикета** (секции Result/Заметки), не в frontmatter.
|
|
201
221
|
|
|
202
222
|
Если хоть один пункт нарушен — **вернись к шагу 5 или 7** и выполни правки инструментом `Edit` на файл тикета. Не обходи эту проверку: вывод `---RESULT---` при пустом Result или `[ ]`-чекбоксах считается **призрачным выполнением** (см. ограничение #9) и ведёт к retry → blocked.
|
|
203
223
|
|
|
@@ -219,6 +239,7 @@ description: >
|
|
|
219
239
|
- Нет побочных эффектов — не созданы тикеты/планы, не перемещены файлы
|
|
220
240
|
- Поля `status` и `completed_at` не записаны в файл тикета ни в каком виде
|
|
221
241
|
- Секция `## Ревью` не создавалась и не редактировалась тобой
|
|
242
|
+
- Все временные/промежуточные файлы и пустые директории, созданные в ходе работы и не являющиеся deliverable, удалены; файлы, путь которых зависит от конфига инструмента (mock/test/fixture/snapshot/output), лежат в директории, указанной в конфиге, а не в произвольном месте
|
|
222
243
|
|
|
223
244
|
**⛔ ФОРМАТ STDOUT — СТРОГО:**
|
|
224
245
|
|
|
@@ -273,6 +294,12 @@ status: default
|
|
|
273
294
|
|
|
274
295
|
При визуальных/семантических/поведенческих критериях в Result **явно обоснуй**, почему структурной проверки недостаточно (одна строка). Полная таблица соответствий — `algorithms/execution-strategy.md` раздел «Соразмерность проверки критерию».
|
|
275
296
|
|
|
297
|
+
9. **Configured paths и cleanup** — файлы, создаваемые во время работы тикета, размещай в местах, явно указанных в конфиге соответствующего инструмента. Перед созданием файла, путь которого может зависеть от конфигурации (mock, тест, фикстура, стаб, snapshot, сгенерированный artifact, временный файл сборки и подобное) — **прочитай соответствующий конфиг проекта**, найди в нём поле target-директории (`roots`, `testDir`, `paths`, `output`, `outDir`, `srcDir` или аналог в конфиге твоего инструмента) и помести файл туда. Создавать файл в корне репозитория без проверки конфига запрещено.
|
|
298
|
+
|
|
299
|
+
**После удаления временных/ошибочных файлов — проверь и удали пустые родительские директории**, созданные в ходе тикета. Каждая созданная директория — твоя ответственность вплоть до закрытия тикета. Пустая папка или артефакт вне scope, оставленные в репозитории, — побочный эффект (см. принцип 4 «No Side Effects»).
|
|
300
|
+
|
|
301
|
+
⛔ **Антипаттерн:** создал файл в корне (mock/snapshot/test) → инструмент не подхватил из-за неправильного пути → удалил файл, но оставил пустую папку. Лечение: до создания — проверка конфига; после rm — удаление пустых директорий.
|
|
302
|
+
|
|
276
303
|
## Формат вывода
|
|
277
304
|
|
|
278
305
|
- Русский язык
|
|
@@ -57,7 +57,29 @@ description: >
|
|
|
57
57
|
|
|
58
58
|
## Шаги проверки
|
|
59
59
|
|
|
60
|
-
### 0.
|
|
60
|
+
### 0. Целостность frontmatter (pre-flight)
|
|
61
|
+
|
|
62
|
+
Перед любой содержательной проверкой убедись, что frontmatter тикета **валидно парсится** YAML-парсером. Запусти:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
node -e "const y=require('js-yaml'),f=require('fs');const c=f.readFileSync(process.argv[1],'utf8');const m=c.match(/^---\\n([\\s\\S]*?)\\n---/);if(!m){console.log('NO_FRONTMATTER');process.exit(1)}try{y.load(m[1]);console.log('OK')}catch(e){console.log('YAML_ERROR:'+e.message);process.exit(2)}" .workflow/tickets/in-progress/{TICKET-ID}.md
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
(или эквивалент в проекте — `js-yaml.load` секции между `---`).
|
|
69
|
+
|
|
70
|
+
Если parse возвращает ошибку (`duplicated mapping key`, `bad indentation`, `:` followed by space в неэкранированном скаляре и т.п.) — **немедленный fail**:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
---RESULT---
|
|
74
|
+
status: failed
|
|
75
|
+
issues:
|
|
76
|
+
- "Frontmatter тикета невалиден: <текст YAML-ошибки>. Файл нельзя смержить в done — downstream MCP-ресурсы (workflow://human-queue, alerts) падают на парсинге. Исправить frontmatter и перезапустить."
|
|
77
|
+
---RESULT---
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Не пытайся «прочесть содержимое глазами» и одобрить — структурно повреждённый frontmatter ломает скрипты пайплайна и MCP-ресурсы.
|
|
81
|
+
|
|
82
|
+
### 0.1. Быстрый выход
|
|
61
83
|
|
|
62
84
|
Прочитай тикет. Если секция `## Ревью` существует и последняя запись — `passed` или `⏭ skipped` → немедленно верни `status: passed`.
|
|
63
85
|
|