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.
@@ -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
- // Main entry point
222
- const rawArgs = process.argv.slice(2);
223
- let ticketId, target;
224
-
225
- if (rawArgs.length >= 2) {
226
- // Прямой вызов: node move-ticket.js IMPL-001 in-progress
227
- ticketId = rawArgs[0];
228
- target = rawArgs[1];
229
- } else if (rawArgs.length === 1) {
230
- // Вызов через pipeline runner: один аргумент — промпт с контекстом
231
- // Формат: "skill-name\n\nContext:\n ticket_id: X\n target: Y\n..."
232
- const prompt = rawArgs[0];
233
- const ticketMatch = prompt.match(/ticket_id:\s*(\S+)/);
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
- moveTicket(ticketId, target).then((result) => {
256
- printResult(result);
257
- if (result.status === "error") {
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 eligibleTickets = tickets.filter(ticket => {
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
- return false;
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
- return false;
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
- return false;
607
+ continue;
611
608
  }
612
609
 
613
- return true;
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
- if (eligibleTickets.length === 0) {
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: 'empty',
619
- reason: 'No eligible tickets (conditions/dependencies not met)'
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
- // Сортировка по приоритету (меньше = важнее), затем по created_at
624
- eligibleTickets.sort((a, b) => {
625
- const priorityA = a.frontmatter.priority || 999;
626
- const priorityB = b.frontmatter.priority || 999;
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
- if (priorityA !== priorityB) {
629
- return priorityA - priorityB;
630
- }
650
+ if (priorityA !== priorityB) {
651
+ return priorityA - priorityB;
652
+ }
631
653
 
632
- // При равном приоритете - по дате создания (старые первые)
633
- const dateA = new Date(a.frontmatter.created_at || '9999-12-31');
634
- const dateB = new Date(b.frontmatter.created_at || '9999-12-31');
635
- return dateA - dateB;
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
- const selected = eligibleTickets[0];
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: 'found',
642
- ticket_id: selected.id,
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 });
@@ -0,0 +1,2 @@
1
+ # Calibration Test Skill
2
+ version: 1.0
@@ -0,0 +1,5 @@
1
+ # Test Skill
2
+
3
+ SIGNATURE_PRESENT
4
+ SomeOtherContent
5
+ version: 1.0
@@ -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
- - Обновить/добавить **Summary** — краткое резюме всей работы
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 не содержит добавленных строк `status:` или `completed_at:`** (эти поля устанавливает только пайплайн).
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