workflow-ai 1.0.37 → 1.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workflow-ai",
3
- "version": "1.0.37",
3
+ "version": "1.0.39",
4
4
  "description": "AI Agent Workflow Coordinator — kanban-based pipeline for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/src/lib/utils.mjs CHANGED
@@ -198,7 +198,7 @@ export function checkAndClosePlan(workflowDir, planId) {
198
198
  }
199
199
 
200
200
  const ticketsDir = path.join(workflowDir, 'tickets');
201
- const allDirNames = ['backlog', 'ready', 'in-progress', 'blocked', 'review', 'done'];
201
+ const allDirNames = ['backlog', 'ready', 'in-progress', 'blocked', 'review', 'done', 'archive'];
202
202
  const allTickets = [];
203
203
 
204
204
  for (const dirName of allDirNames) {
@@ -218,7 +218,7 @@ export function checkAndClosePlan(workflowDir, planId) {
218
218
  }
219
219
 
220
220
  const total = allTickets.length;
221
- const done = allTickets.filter(t => t.dir === 'done').length;
221
+ const done = allTickets.filter(t => t.dir === 'done' || t.dir === 'archive').length;
222
222
 
223
223
  if (total === 0) {
224
224
  return { closed: false, reason: 'No tickets found for plan', total, done };
@@ -255,5 +255,32 @@ export function checkAndClosePlan(workflowDir, planId) {
255
255
 
256
256
  fs.writeFileSync(planPath, serializeFrontmatter(frontmatter) + body, 'utf8');
257
257
 
258
- return { closed: true, reason: 'All tickets done', total, done };
258
+ // Архивируем все done-тикеты этого плана
259
+ const archiveDir = path.join(ticketsDir, 'archive');
260
+ if (!fs.existsSync(archiveDir)) {
261
+ fs.mkdirSync(archiveDir, { recursive: true });
262
+ }
263
+
264
+ const archived = [];
265
+ const doneDir = path.join(ticketsDir, 'done');
266
+ if (fs.existsSync(doneDir)) {
267
+ const doneTickets = allTickets.filter(t => t.dir === 'done');
268
+ for (const ticket of doneTickets) {
269
+ const srcPath = path.join(doneDir, `${ticket.id}.md`);
270
+ const destPath = path.join(archiveDir, `${ticket.id}.md`);
271
+ try {
272
+ if (fs.existsSync(srcPath)) {
273
+ const content = fs.readFileSync(srcPath, 'utf8');
274
+ const { frontmatter: fm, body: bd } = parseFrontmatter(content);
275
+ fm.updated_at = new Date().toISOString();
276
+ fm.archived_at = new Date().toISOString();
277
+ fs.writeFileSync(destPath, serializeFrontmatter(fm) + bd, 'utf8');
278
+ fs.unlinkSync(srcPath);
279
+ archived.push(ticket.id);
280
+ }
281
+ } catch (_) { /* skip errors */ }
282
+ }
283
+ }
284
+
285
+ return { closed: true, reason: 'All tickets done', total, done, archived };
259
286
  }
package/src/runner.mjs CHANGED
@@ -840,14 +840,11 @@ class StageExecutor {
840
840
  // Если args содержит -p с ролью — объединяем роль и промпт в один аргумент
841
841
  // Иначе Claude CLI интерпретирует роль как промпт, а реальный промпт игнорирует
842
842
  const lastPIdx = args.lastIndexOf('-p');
843
- let stdinPrompt = null;
844
843
  if (lastPIdx !== -1 && lastPIdx < args.length - 1) {
845
844
  const role = args[lastPIdx + 1];
846
845
  args[lastPIdx + 1] = `${prompt}\n\nТвоя роль: ${role}`;
847
846
  } else {
848
- // Передаём промпт через stdin вместо аргумента,
849
- // т.к. shell: true на Windows обрезает многострочные аргументы
850
- stdinPrompt = prompt;
847
+ args.push(prompt);
851
848
  }
852
849
 
853
850
  // Логгируем команду перед запуском (вместо промпта — имя skill)
@@ -865,15 +862,10 @@ class StageExecutor {
865
862
  const child = spawn(agent.command, args, {
866
863
  cwd: path.resolve(this.projectRoot, agent.workdir || '.'),
867
864
  stdio: ['pipe', 'pipe', 'pipe'],
868
- // npm-бинари (.cmd) на Windows требуют shell: true,
869
- // но нативные executables (node) — нет: shell: true обрезает многострочные аргументы
870
865
  shell: process.platform === 'win32' && agent.command !== 'node'
871
866
  });
872
867
 
873
- // Передаём промпт через stdin или закрываем если промпт в аргументах
874
- if (stdinPrompt) {
875
- child.stdin.write(stdinPrompt);
876
- }
868
+ // Закрываем stdin чтобы агент не ждал дополнительного ввода
877
869
  child.stdin.end();
878
870
 
879
871
  let stdout = '';
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * archive-plan-tickets.js - Архивирует все done-тикеты указанного плана
5
+ *
6
+ * Использование:
7
+ * node archive-plan-tickets.js <plan_id>
8
+ *
9
+ * Пример:
10
+ * node archive-plan-tickets.js PLAN-002
11
+ * node archive-plan-tickets.js 2
12
+ */
13
+
14
+ import fs from 'fs';
15
+ import path from 'path';
16
+ import { findProjectRoot } from '../lib/find-root.mjs';
17
+ import { parseFrontmatter, serializeFrontmatter, normalizePlanId, extractPlanId, printResult } from '../lib/utils.mjs';
18
+
19
+ const PROJECT_DIR = findProjectRoot();
20
+ const WORKFLOW_DIR = path.join(PROJECT_DIR, '.workflow');
21
+ const TICKETS_DIR = path.join(WORKFLOW_DIR, 'tickets');
22
+ const DONE_DIR = path.join(TICKETS_DIR, 'done');
23
+ const ARCHIVE_DIR = path.join(TICKETS_DIR, 'archive');
24
+
25
+ /**
26
+ * Архивирует все done-тикеты указанного плана
27
+ */
28
+ function archivePlanTickets(planId) {
29
+ if (!planId) {
30
+ return { status: 'error', error: 'Missing plan_id' };
31
+ }
32
+
33
+ if (!fs.existsSync(DONE_DIR)) {
34
+ return { status: 'ok', plan_id: planId, archived: 0, ticket_ids: '' };
35
+ }
36
+
37
+ if (!fs.existsSync(ARCHIVE_DIR)) {
38
+ fs.mkdirSync(ARCHIVE_DIR, { recursive: true });
39
+ }
40
+
41
+ const files = fs.readdirSync(DONE_DIR).filter(f => f.endsWith('.md') && f !== '.gitkeep.md');
42
+ const archived = [];
43
+
44
+ for (const file of files) {
45
+ const filePath = path.join(DONE_DIR, file);
46
+ try {
47
+ const content = fs.readFileSync(filePath, 'utf8');
48
+ const { frontmatter, body } = parseFrontmatter(content);
49
+
50
+ const ticketPlanId = normalizePlanId(frontmatter.parent_plan);
51
+ if (ticketPlanId !== planId) continue;
52
+
53
+ const ticketId = frontmatter.id || file.replace('.md', '');
54
+
55
+ frontmatter.updated_at = new Date().toISOString();
56
+ frontmatter.archived_at = new Date().toISOString();
57
+
58
+ const destPath = path.join(ARCHIVE_DIR, file);
59
+ fs.writeFileSync(destPath, serializeFrontmatter(frontmatter) + body, 'utf8');
60
+ fs.unlinkSync(filePath);
61
+
62
+ archived.push(ticketId);
63
+ console.log(`[ARCHIVE] ${ticketId}: done → archive`);
64
+ } catch (e) {
65
+ console.error(`[ERROR] Failed to archive ${file}: ${e.message}`);
66
+ }
67
+ }
68
+
69
+ return {
70
+ status: 'ok',
71
+ plan_id: planId,
72
+ archived: archived.length,
73
+ ticket_ids: archived.join(',')
74
+ };
75
+ }
76
+
77
+ // Main entry point
78
+ const rawArgs = process.argv.slice(2);
79
+ let planId;
80
+
81
+ if (rawArgs.length >= 1) {
82
+ // Прямой вызов или pipeline context
83
+ const arg = rawArgs[0];
84
+ const planMatch = arg.match(/plan_id:\s*(\S+)/i);
85
+ planId = planMatch ? normalizePlanId(planMatch[1]) : normalizePlanId(arg);
86
+ } else {
87
+ planId = extractPlanId();
88
+ }
89
+
90
+ if (!planId) {
91
+ console.error('Usage: node archive-plan-tickets.js <plan_id>');
92
+ console.error('Example: node archive-plan-tickets.js PLAN-002');
93
+ printResult({ status: 'error', error: 'Missing plan_id argument' });
94
+ process.exit(1);
95
+ }
96
+
97
+ const result = archivePlanTickets(planId);
98
+ printResult(result);
99
+
100
+ if (result.status === 'error') {
101
+ process.exit(1);
102
+ }
@@ -23,7 +23,7 @@ const WORKFLOW_DIR = path.join(PROJECT_DIR, '.workflow');
23
23
  const TICKETS_DIR = path.join(WORKFLOW_DIR, 'tickets');
24
24
 
25
25
  // Доступные статусы
26
- const VALID_STATUSES = ['backlog', 'ready', 'in-progress', 'blocked', 'review', 'done'];
26
+ const VALID_STATUSES = ['backlog', 'ready', 'in-progress', 'blocked', 'review', 'done', 'archive'];
27
27
 
28
28
  // Таблица допустимых переходов
29
29
  const VALID_TRANSITIONS = {
@@ -32,7 +32,8 @@ const VALID_TRANSITIONS = {
32
32
  'in-progress': ['done', 'blocked', 'review'],
33
33
  'blocked': ['ready'],
34
34
  'review': ['done', 'ready', 'in-progress', 'blocked'],
35
- 'done': ['ready', 'blocked']
35
+ 'done': ['ready', 'blocked', 'archive'],
36
+ 'archive': ['backlog']
36
37
  };
37
38
 
38
39
  /**
@@ -36,6 +36,7 @@ const DONE_DIR = path.join(TICKETS_DIR, 'done');
36
36
  const IN_PROGRESS_DIR = path.join(TICKETS_DIR, 'in-progress');
37
37
  const BLOCKED_DIR = path.join(TICKETS_DIR, 'blocked');
38
38
  const REVIEW_DIR = path.join(TICKETS_DIR, 'review');
39
+ const ARCHIVE_DIR = path.join(TICKETS_DIR, 'archive');
39
40
  const BACKLOG_DIR = path.join(TICKETS_DIR, 'backlog');
40
41
 
41
42
 
@@ -60,7 +61,8 @@ function checkCondition(condition) {
60
61
  const ids = Array.isArray(value) ? value : [value];
61
62
  return ids.every(taskId => {
62
63
  const donePath = path.join(DONE_DIR, `${taskId}.md`);
63
- return fs.existsSync(donePath);
64
+ const archivePath = path.join(ARCHIVE_DIR, `${taskId}.md`);
65
+ return fs.existsSync(donePath) || fs.existsSync(archivePath);
64
66
  });
65
67
 
66
68
  case 'date_after':
@@ -146,7 +148,7 @@ function parseReviewSection(content) {
146
148
  * @returns {object} Метрики: iterations, avgTimeToFirstPassed, failedVsPassed
147
149
  */
148
150
  function calculateReviewMetrics() {
149
- const allDirs = [BACKLOG_DIR, READY_DIR, IN_PROGRESS_DIR, BLOCKED_DIR, REVIEW_DIR, DONE_DIR];
151
+ const allDirs = [BACKLOG_DIR, READY_DIR, IN_PROGRESS_DIR, BLOCKED_DIR, REVIEW_DIR, DONE_DIR, ARCHIVE_DIR];
150
152
  const ticketMetrics = {};
151
153
  let totalFailed = 0;
152
154
  let totalPassed = 0;
@@ -212,7 +214,8 @@ function checkDependencies(dependencies) {
212
214
 
213
215
  return dependencies.every(depId => {
214
216
  const donePath = path.join(DONE_DIR, `${depId}.md`);
215
- return fs.existsSync(donePath);
217
+ const archivePath = path.join(ARCHIVE_DIR, `${depId}.md`);
218
+ return fs.existsSync(donePath) || fs.existsSync(archivePath);
216
219
  });
217
220
  }
218
221
 
@@ -232,7 +235,8 @@ function autoCorrectTickets(config) {
232
235
  in_progress: IN_PROGRESS_DIR,
233
236
  blocked: BLOCKED_DIR,
234
237
  review: REVIEW_DIR,
235
- done: DONE_DIR
238
+ done: DONE_DIR,
239
+ archive: ARCHIVE_DIR
236
240
  };
237
241
 
238
242
  /**
@@ -27,6 +27,17 @@ ticket_prefix: COACH
27
27
  | **План** | Файл в `.workflow/plans/` — источник контекста для анализа |
28
28
  | **Тикет** | Файл в `.workflow/tickets/` — единица работы для анализа результатов |
29
29
  | **Отчёт** | Файл в `.workflow/reports/` — источник метрик и выводов |
30
+ | **Бэклог коуча** | Файл `.workflow/coach-backlog.yaml` — реестр проанализированных тикетов и внесённых правок |
31
+
32
+ ## Обязательный шаг: Бэклог коуча
33
+
34
+ **ПЕРЕД ЛЮБОЙ работой** выполни:
35
+
36
+ 1. Прочитай `.workflow/coach-backlog.yaml` (если не существует — создай с пустыми секциями)
37
+ 2. Загрузи `knowledge/backlog-management.md` — правила ведения бэклога
38
+ 3. При анализе тикетов — **пропускай** те, что уже есть в `analyzed_tickets`
39
+ 4. При внесении правок — **проверяй** `applied_changes`, не предлагай уже сделанное
40
+ 5. **После завершения** — обнови бэклог: добавь проанализированные тикеты и внесённые правки
30
41
 
31
42
  ## Маршрутизация тикетов COACH-*
32
43
 
@@ -52,6 +63,7 @@ ticket_prefix: COACH
52
63
  | `knowledge/skill-anatomy.md` | При создании или аудите скила — эталонная структура |
53
64
  | `knowledge/common-antipatterns.md` | При аудите и ревью — типичные ошибки в скилах |
54
65
  | `knowledge/prompt-engineering.md` | При улучшении формулировок в SKILL.md и воркфлоу |
66
+ | `knowledge/backlog-management.md` | **ВСЕГДА** — правила ведения бэклога проанализированных тикетов и правок |
55
67
 
56
68
  ## Загрузка алгоритмов
57
69
 
@@ -0,0 +1,71 @@
1
+ # Управление бэклогом коуча
2
+
3
+ Коуч ведёт бэклог своей работы в файле `.workflow/coach-backlog.yaml`. Этот файл — единственный источник правды о том, что уже было проанализировано и какие правки внесены.
4
+
5
+ ## Структура бэклога
6
+
7
+ ```yaml
8
+ # .workflow/coach-backlog.yaml
9
+ version: 1
10
+ last_updated: "2026-03-20"
11
+
12
+ # Проанализированные тикеты
13
+ analyzed_tickets:
14
+ - ticket_id: "GML-001"
15
+ ticket_file: "tickets/done/GML-001.md"
16
+ analyzed_date: "2026-03-20"
17
+ coach_ticket: "COACH-005" # тикет коуча, в рамках которого проведён анализ
18
+ findings_count: 3
19
+ summary: "Недостаток knowledge по TAM/SAM/SOM, нечёткий воркфлоу"
20
+
21
+ # Внесённые правки в скилы
22
+ applied_changes:
23
+ - change_id: "CHG-001"
24
+ date: "2026-03-20"
25
+ coach_ticket: "COACH-005"
26
+ target_skill: "growth-marketing-lead"
27
+ changed_files:
28
+ - "src/skills/growth-marketing-lead/knowledge/market-sizing.md"
29
+ - "src/skills/growth-marketing-lead/workflows/analyze.md"
30
+ change_type: "improve" # improve | fix | add | refactor
31
+ description: "Добавлен knowledge-модуль market-sizing, уточнён воркфлоу анализа"
32
+ based_on_tickets:
33
+ - "GML-001"
34
+ - "GML-003"
35
+
36
+ # Скилы, прошедшие аудит
37
+ audited_skills:
38
+ - skill_name: "growth-marketing-lead"
39
+ last_audit_date: "2026-03-20"
40
+ coach_ticket: "COACH-004"
41
+ score: 72
42
+ status: "NEEDS_WORK"
43
+ ```
44
+
45
+ ## Правила работы с бэклогом
46
+
47
+ ### Перед началом анализа (ОБЯЗАТЕЛЬНО)
48
+
49
+ 1. Прочитай `.workflow/coach-backlog.yaml`
50
+ 2. Проверь, какие тикеты уже проанализированы (`analyzed_tickets`)
51
+ 3. **Пропусти** тикеты, которые уже есть в бэклоге
52
+ 4. Проверь, какие правки уже внесены (`applied_changes`) в целевой скил
53
+ 5. **Учитывай** уже внесённые правки — не предлагай то, что уже исправлено
54
+
55
+ ### После завершения анализа (ОБЯЗАТЕЛЬНО)
56
+
57
+ 1. Добавь каждый проанализированный тикет в `analyzed_tickets`
58
+ 2. Добавь каждую внесённую правку в `applied_changes`
59
+ 3. Если был аудит — обнови `audited_skills`
60
+ 4. Обнови `last_updated`
61
+
62
+ ### Генерация ID
63
+
64
+ - `change_id`: `CHG-{порядковый номер, 3 цифры}` — следующий после максимального в бэклоге
65
+
66
+ ## Антипаттерны
67
+
68
+ - **Повторный анализ** — анализировать тикет, который уже в бэклоге
69
+ - **Дублирование правок** — предлагать изменение, которое уже в `applied_changes`
70
+ - **Забыть записать** — внести правку, но не добавить в бэклог
71
+ - **Пустой бэклог** — если файла нет, создай его с пустыми секциями
@@ -0,0 +1,10 @@
1
+ # Шаблон инициализации бэклога коуча
2
+ # Скопировать в .workflow/coach-backlog.yaml при первом запуске
3
+ version: 1
4
+ last_updated: ""
5
+
6
+ analyzed_tickets: []
7
+
8
+ applied_changes: []
9
+
10
+ audited_skills: []
@@ -4,9 +4,18 @@
4
4
 
5
5
  ## Алгоритм выполнения
6
6
 
7
+ ### 0. Загрузка бэклога (ОБЯЗАТЕЛЬНО)
8
+
9
+ 1. Прочитай `.workflow/coach-backlog.yaml`
10
+ 2. Выпиши список уже проанализированных `ticket_id` из `analyzed_tickets`
11
+ 3. Выпиши список уже внесённых правок из `applied_changes` для целевого скила
12
+ 4. **В шаге 1 пропусти тикеты, которые уже в бэклоге**
13
+ 5. **В шаге 5 не предлагай правки, которые уже в `applied_changes`**
14
+
7
15
  ### 1. Сбор данных
8
16
 
9
- Собери все завершённые артефакты, связанные со скилом:
17
+ Собери все завершённые артефакты, связанные со скилом.
18
+ **Исключи тикеты, уже присутствующие в бэклоге (`analyzed_tickets`).**
10
19
 
11
20
  | Источник | Где искать | Что извлечь |
12
21
  |----------|-----------|-------------|
@@ -55,3 +64,10 @@
55
64
  - Конкретные рекомендации с указанием файлов для изменения
56
65
 
57
66
  Результат оформи как часть отчёта → `templates/audit-report.md`
67
+
68
+ ### 6. Обновление бэклога (ОБЯЗАТЕЛЬНО)
69
+
70
+ 1. Добавь каждый проанализированный тикет в `analyzed_tickets` бэклога
71
+ 2. Если внесены правки — добавь в `applied_changes`
72
+ 3. Обнови `last_updated`
73
+ 4. Правила формата → `knowledge/backlog-management.md`
@@ -4,6 +4,13 @@
4
4
 
5
5
  ## Алгоритм выполнения
6
6
 
7
+ ### 0. Загрузка бэклога (ОБЯЗАТЕЛЬНО)
8
+
9
+ 1. Прочитай `.workflow/coach-backlog.yaml`
10
+ 2. Проверь `audited_skills` — когда последний раз проводился аудит этого скила
11
+ 3. Проверь `applied_changes` — какие правки уже внесены после предыдущего аудита
12
+ 4. **Учитывай уже внесённые правки** — не отмечай как проблему то, что уже исправлено
13
+
7
14
  ### 1. Инвентаризация
8
15
 
9
16
  Прочитай все файлы скила и составь карту:
@@ -52,3 +59,10 @@
52
59
  - Список найденных проблем с приоритетами
53
60
  - Конкретные рекомендации по каждой проблеме
54
61
  - План улучшений с приоритизацией
62
+
63
+ ### 7. Обновление бэклога (ОБЯЗАТЕЛЬНО)
64
+
65
+ 1. Обнови/добавь запись в `audited_skills` бэклога
66
+ 2. Если анализировались тикеты — добавь в `analyzed_tickets`
67
+ 3. Обнови `last_updated`
68
+ 4. Правила формата → `knowledge/backlog-management.md`
@@ -4,6 +4,13 @@
4
4
 
5
5
  ## Алгоритм выполнения
6
6
 
7
+ ### 0. Загрузка бэклога (ОБЯЗАТЕЛЬНО)
8
+
9
+ 1. Прочитай `.workflow/coach-backlog.yaml`
10
+ 2. Проверь `applied_changes` для целевого скила — какие правки уже внесены
11
+ 3. **Не вноси повторно правки, которые уже есть в бэклоге**
12
+ 4. Учитывай контекст предыдущих улучшений при планировании новых
13
+
7
14
  ### 1. Определи scope улучшения
8
15
 
9
16
  Из тикета извлеки:
@@ -54,3 +61,10 @@
54
61
  - Какие файлы затронуты
55
62
  - Ожидаемый эффект
56
63
  - Как проверить результат
64
+
65
+ ### 8. Обновление бэклога (ОБЯЗАТЕЛЬНО)
66
+
67
+ 1. Добавь каждую внесённую правку в `applied_changes` бэклога
68
+ 2. Если анализировались тикеты — добавь в `analyzed_tickets`
69
+ 3. Обнови `last_updated`
70
+ 4. Правила формата → `knowledge/backlog-management.md`
@@ -4,6 +4,12 @@
4
4
 
5
5
  ## Алгоритм выполнения
6
6
 
7
+ ### 0. Загрузка бэклога (ОБЯЗАТЕЛЬНО)
8
+
9
+ 1. Прочитай `.workflow/coach-backlog.yaml`
10
+ 2. Проверь `applied_changes` — какие правки уже внесены в целевой скил
11
+ 3. **Учитывай контекст предыдущих правок** при формировании замечаний
12
+
7
13
  ### 1. Определи фокус ревью
8
14
 
9
15
  Из тикета извлеки: какой аспект проверить?
@@ -13,18 +13,9 @@ description: Декомпозировать план на исполняемые
13
13
  - Нужно разбить крупную задачу на подзадачи
14
14
  - Требуется детальная декомпозиция
15
15
 
16
- ## ⛔ ЗАПРЕЩЕНО сканировать все планы если указан конкретный план
16
+ ## ⛔ Какой план декомпозировать
17
17
 
18
- Ты получаешь промпт с секциями Context и Instructions.
19
-
20
- **Если в Context или Instructions есть `plan_id` или `plan_file` или путь к конкретному плану (например `PLAN-005.md`):**
21
- - Декомпозируй ТОЛЬКО этот план
22
- - НЕ сканируй `plans/current/` — это ЗАПРЕЩЕНО
23
- - НЕ проверяй статус плана (`draft`, `active` — неважно) — это ЗАПРЕЩЕНО
24
- - НЕ читай и не упоминай другие планы — это ЗАПРЕЩЕНО
25
- - Открой файл плана и сразу переходи к шагу 1
26
-
27
- **Только если нигде в промпте нет указания на конкретный план** — тогда сканируй `plans/current/` и декомпозируй только планы со статусом `approved`.
18
+ Декомпозируй план, указанный в секции Instructions твоего промпта. Открой этот файл и сразу переходи к шагу 1. Не сканируй папку plans/current/. Не проверяй статус плана. Не читай другие планы.
28
19
 
29
20
  ## Шаги выполнения
30
21
 
@@ -65,6 +56,9 @@ description: Декомпозировать план на исполняемые
65
56
  - DoD-критерий: «Результаты внесены непосредственно в файл тикета»
66
57
  - Указание в каком формате ожидается результат
67
58
 
59
+ **⛔ НЕ создавать conditional/fallback HUMAN-тикеты заранее.**
60
+ Если HUMAN-тикет нужен только при провале агентского тикета (например, «выполнить вручную, если агент не смог пройти auth») — **не создавай его на этапе декомпозиции**. Такие тикеты создаёт `decompose-gaps` когда проблема реально возникла. Иначе они висят в backlog вечно и засоряют доску.
61
+
68
62
  **Пример разбиения гибридной задачи:**
69
63
 
70
64
  Задача из плана: «Провести конкурентный анализ и настроить трекинг конкурентов в аналитике»