workflow-ai 1.0.41 → 1.0.43

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.
Files changed (55) hide show
  1. package/package.json +1 -1
  2. package/src/init.mjs +13 -8
  3. package/src/junction-manager.mjs +8 -4
  4. package/src/runner.mjs +1 -1
  5. package/src/scripts/archive-plan-tickets.js +0 -102
  6. package/src/scripts/check-anomalies.js +0 -161
  7. package/src/scripts/check-conditions.js +0 -254
  8. package/src/scripts/check-plan-decomposed.js +0 -179
  9. package/src/scripts/move-ticket.js +0 -228
  10. package/src/scripts/move-to-ready.js +0 -115
  11. package/src/scripts/move-to-review.js +0 -100
  12. package/src/scripts/pick-next-task.js +0 -723
  13. package/src/skills/analyze-report/SKILL.md +0 -110
  14. package/src/skills/check-relevance/SKILL.md +0 -236
  15. package/src/skills/coach/README.md +0 -100
  16. package/src/skills/coach/SKILL.md +0 -119
  17. package/src/skills/coach/algorithms/gap-analysis.md +0 -69
  18. package/src/skills/coach/algorithms/improvement-prioritization.md +0 -62
  19. package/src/skills/coach/algorithms/skill-scoring.md +0 -79
  20. package/src/skills/coach/knowledge/backlog-management.md +0 -71
  21. package/src/skills/coach/knowledge/common-antipatterns.md +0 -56
  22. package/src/skills/coach/knowledge/prompt-engineering.md +0 -86
  23. package/src/skills/coach/knowledge/skill-anatomy.md +0 -71
  24. package/src/skills/coach/templates/audit-report.md +0 -54
  25. package/src/skills/coach/templates/coach-backlog-init.yaml +0 -10
  26. package/src/skills/coach/templates/improvement-plan.md +0 -54
  27. package/src/skills/coach/templates/new-skill.md +0 -137
  28. package/src/skills/coach/workflows/analyze.md +0 -85
  29. package/src/skills/coach/workflows/audit.md +0 -68
  30. package/src/skills/coach/workflows/create.md +0 -66
  31. package/src/skills/coach/workflows/improve.md +0 -70
  32. package/src/skills/coach/workflows/research.md +0 -55
  33. package/src/skills/coach/workflows/review.md +0 -74
  34. package/src/skills/create-plan/SKILL.md +0 -107
  35. package/src/skills/create-report/SKILL.md +0 -156
  36. package/src/skills/decompose-gaps/SKILL.md +0 -167
  37. package/src/skills/decompose-plan/SKILL.md +0 -219
  38. package/src/skills/deep-research/README.md +0 -50
  39. package/src/skills/deep-research/SKILL.md +0 -148
  40. package/src/skills/deep-research/algorithms/source-scoring.md +0 -63
  41. package/src/skills/deep-research/algorithms/synthesis.md +0 -67
  42. package/src/skills/deep-research/knowledge/data-validation.md +0 -44
  43. package/src/skills/deep-research/knowledge/research-methodology.md +0 -54
  44. package/src/skills/deep-research/knowledge/source-evaluation.md +0 -33
  45. package/src/skills/deep-research/scripts/perplexity-research.js +0 -315
  46. package/src/skills/deep-research/templates/brief-summary.md +0 -25
  47. package/src/skills/deep-research/templates/research-report.md +0 -76
  48. package/src/skills/deep-research/workflows/benchmark.md +0 -56
  49. package/src/skills/deep-research/workflows/competitor.md +0 -63
  50. package/src/skills/deep-research/workflows/custom.md +0 -45
  51. package/src/skills/deep-research/workflows/market.md +0 -64
  52. package/src/skills/deep-research/workflows/technology.md +0 -52
  53. package/src/skills/deep-research/workflows/trend.md +0 -51
  54. package/src/skills/execute-task/SKILL.md +0 -207
  55. package/src/skills/review-result/SKILL.md +0 -329
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workflow-ai",
3
- "version": "1.0.41",
3
+ "version": "1.0.43",
4
4
  "description": "AI Agent Workflow Coordinator — kanban-based pipeline for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/src/init.mjs CHANGED
@@ -95,13 +95,16 @@ function generateSkillsTable(workflowRoot) {
95
95
  'move-ticket': 'Перемещение тикета',
96
96
  'pick-next-task': 'Выбор следующей задачи',
97
97
  'decompose-gaps': 'Декомпозиция пробелов',
98
- 'review-result': 'Ревью результата'
98
+ 'review-result': 'Ревью результата',
99
+ 'check-relevance': 'Проверка актуальности',
100
+ 'coach': 'Коуч скилов',
101
+ 'deep-research': 'Глубокий ресерч'
99
102
  };
100
103
 
101
104
  let table = '| Задача | Инструкция |\n|--------|------------|\n';
102
105
 
103
106
  const skillDirs = readdirSync(skillsDir, { withFileTypes: true })
104
- .filter(entry => entry.isDirectory())
107
+ .filter(entry => entry.isDirectory() || entry.isSymbolicLink())
105
108
  .map(entry => entry.name);
106
109
 
107
110
  for (const skillDir of skillDirs) {
@@ -118,9 +121,10 @@ function generateSkillsTable(workflowRoot) {
118
121
  *
119
122
  * @param {string} workflowRoot - Путь к корню .workflow
120
123
  * @param {string} projectRoot - Путь к корню проекта
124
+ * @param {string} packageRoot - Путь к корню пакета
121
125
  */
122
- function generateClaudeMd(workflowRoot, projectRoot) {
123
- const templatePath = join(workflowRoot, 'templates', 'agent-templates', 'CLAUDE.md.tpl');
126
+ function generateClaudeMd(workflowRoot, projectRoot, packageRoot) {
127
+ const templatePath = join(packageRoot, 'agent-templates', 'CLAUDE.md.tpl');
124
128
  const destPath = join(projectRoot, 'CLAUDE.md');
125
129
 
126
130
  let content;
@@ -174,9 +178,10 @@ function generateClaudeMd(workflowRoot, projectRoot) {
174
178
  *
175
179
  * @param {string} workflowRoot - Путь к корню .workflow
176
180
  * @param {string} projectRoot - Путь к корню проекта
181
+ * @param {string} packageRoot - Путь к корню пакета
177
182
  */
178
- function generateQwenMd(workflowRoot, projectRoot) {
179
- const templatePath = join(workflowRoot, 'templates', 'agent-templates', 'QWEN.md.tpl');
183
+ function generateQwenMd(workflowRoot, projectRoot, packageRoot) {
184
+ const templatePath = join(packageRoot, 'agent-templates', 'QWEN.md.tpl');
180
185
  const destPath = join(projectRoot, 'QWEN.md');
181
186
 
182
187
  let content;
@@ -418,8 +423,8 @@ export function initProject(targetPath = process.cwd(), options = {}) {
418
423
  }
419
424
 
420
425
  // Step 8: Generate CLAUDE.md and QWEN.md
421
- generateClaudeMd(workflowRoot, projectRoot);
422
- generateQwenMd(workflowRoot, projectRoot);
426
+ generateClaudeMd(workflowRoot, projectRoot, packageRoot);
427
+ generateQwenMd(workflowRoot, projectRoot, packageRoot);
423
428
  result.steps.push('Generated CLAUDE.md and QWEN.md from agent-templates');
424
429
 
425
430
  // Step 9: Update .gitignore
@@ -69,10 +69,14 @@ export function createHardlink(target, linkPath) {
69
69
  }
70
70
  rmSync(linkPath, { force: true });
71
71
 
72
- if (isWindows) {
73
- execSync(`mklink /H "${linkPath}" "${target}"`, { stdio: 'pipe' });
74
- } else {
75
- linkSync(target, linkPath);
72
+ try {
73
+ if (isWindows) {
74
+ execSync(`mklink /H "${linkPath}" "${target}"`, { stdio: 'pipe' });
75
+ } else {
76
+ linkSync(target, linkPath);
77
+ }
78
+ } catch {
79
+ cpSync(target, linkPath);
76
80
  }
77
81
  }
78
82
 
package/src/runner.mjs CHANGED
@@ -622,7 +622,7 @@ class FileGuard {
622
622
  const entries = fs.readdirSync(dir, { withFileTypes: true });
623
623
  for (const entry of entries) {
624
624
  const entryPath = path.join(dir, entry.name).replace(/\\/g, '/');
625
- if (entry.isDirectory()) {
625
+ if (entry.isDirectory() || entry.isSymbolicLink()) {
626
626
  files.push(...this._getAllFiles(entryPath));
627
627
  } else {
628
628
  files.push(entryPath);
@@ -1,102 +0,0 @@
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
- }
@@ -1,161 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * check-anomalies.js - Скрипт для проверки аномалий в тикетах
5
- *
6
- * Проверяет in-progress тикеты на наличие заполненных результатов.
7
- * Если тикет в in-progress, но имеет заполненный раздел "Результат выполнения" —
8
- * это аномалия (тикет, вероятно, выполнен, но не перемещён в done/review).
9
- *
10
- * Использование:
11
- * node check-anomalies.js
12
- *
13
- * Выводит результат в формате:
14
- * ---RESULT---
15
- * status: ok|anomalies_found|error
16
- * anomalies_count: N
17
- * anomalies: [{"id": "IMPL-001", "title": "...", "recommendation": "..."}]
18
- * ---RESULT---
19
- */
20
-
21
- import fs from 'fs';
22
- import path from 'path';
23
- import YAML from '../lib/js-yaml.mjs';
24
- import { findProjectRoot } from '../lib/find-root.mjs';
25
- import { parseFrontmatter, printResult } from '../lib/utils.mjs';
26
-
27
- // Корень проекта
28
- const PROJECT_DIR = findProjectRoot();
29
- const TICKETS_DIR = path.join(PROJECT_DIR, '.workflow', 'tickets');
30
- const IN_PROGRESS_DIR = path.join(TICKETS_DIR, 'in-progress');
31
-
32
- /**
33
- * Проверяет, заполнен ли раздел результатов
34
- * Возвращает true, если раздел содержит реальный контент (не только комментарии)
35
- */
36
- function hasFilledResult(body) {
37
- // Ищем раздел "Результат выполнения" или "Result"
38
- // Используем более гибкий паттерн
39
- const resultSectionRegex = /^##\s*(Результат выполнения|Result)\s*$/m;
40
- const sectionStart = body.search(resultSectionRegex);
41
-
42
- if (sectionStart === -1) {
43
- return false;
44
- }
45
-
46
- // Находим начало следующей секции ## или конец файла
47
- const nextSectionRegex = /^##\s+/gm;
48
- nextSectionRegex.lastIndex = sectionStart + 1;
49
- const nextSectionMatch = nextSectionRegex.exec(body);
50
- const sectionEnd = nextSectionMatch ? nextSectionMatch.index : body.length;
51
-
52
- const sectionContent = body.substring(sectionStart, sectionEnd);
53
-
54
- // Ищем подраздел Summary или "Что сделано"
55
- const summaryRegex = /^###\s*(Summary|Что сделано)\s*$/m;
56
- const summaryStart = sectionContent.search(summaryRegex);
57
-
58
- if (summaryStart === -1) {
59
- return false;
60
- }
61
-
62
- // Находим начало следующего подраздела ### или конец секции
63
- const nextSubsectionRegex = /^###\s+/gm;
64
- nextSubsectionRegex.lastIndex = summaryStart + 1;
65
- const nextSubsectionMatch = nextSubsectionRegex.exec(sectionContent);
66
- const summaryEnd = nextSubsectionMatch ? nextSubsectionMatch.index : sectionContent.length;
67
-
68
- const summaryContent = sectionContent.substring(summaryStart, summaryEnd);
69
-
70
- // Проверяем, что контент не пустой и не состоит только из комментариев
71
- // Удаляем HTML комментарии и проверяем остаток
72
- const withoutComments = summaryContent.replace(/<!--[\s\S]*?-->/g, '').trim();
73
-
74
- // Если после удаления комментариев остался текст — раздел заполнен
75
- return withoutComments.length > 0;
76
- }
77
-
78
- /**
79
- * Основная функция проверки аномалий
80
- */
81
- async function checkAnomalies() {
82
- const anomalies = [];
83
-
84
- // Проверяем существование директории in-progress
85
- if (!fs.existsSync(IN_PROGRESS_DIR)) {
86
- return {
87
- status: 'ok',
88
- anomalies_count: 0,
89
- anomalies: [],
90
- message: 'in-progress directory does not exist'
91
- };
92
- }
93
-
94
- // Читаем все файлы в in-progress
95
- let files;
96
- try {
97
- files = fs.readdirSync(IN_PROGRESS_DIR);
98
- } catch (e) {
99
- return {
100
- status: 'error',
101
- error: `Failed to read in-progress directory: ${e.message}`
102
- };
103
- }
104
-
105
- // Фильтруем .md файлы (исключаем .gitkeep)
106
- const ticketFiles = files.filter(f => f.endsWith('.md') && f !== '.gitkeep.md');
107
-
108
- for (const file of ticketFiles) {
109
- const filePath = path.join(IN_PROGRESS_DIR, file);
110
- let content;
111
-
112
- try {
113
- content = fs.readFileSync(filePath, 'utf8');
114
- } catch (e) {
115
- anomalies.push({
116
- id: file.replace('.md', ''),
117
- title: 'Unknown (read error)',
118
- recommendation: `Не удалось прочитать файл: ${e.message}`
119
- });
120
- continue;
121
- }
122
-
123
- // Парсим frontmatter для получения id и title
124
- const { frontmatter, body } = parseFrontmatter(content);
125
- const ticketId = frontmatter.id || file.replace('.md', '');
126
- const ticketTitle = frontmatter.title || 'Unknown';
127
-
128
- // Проверяем наличие заполненного результата
129
- if (hasFilledResult(body)) {
130
- anomalies.push({
131
- id: ticketId,
132
- title: ticketTitle,
133
- recommendation: 'Проверьте тикет и переместите в done/ или review/ если выполнен'
134
- });
135
- }
136
- }
137
-
138
- return {
139
- status: anomalies.length > 0 ? 'anomalies_found' : 'ok',
140
- anomalies_count: anomalies.length,
141
- anomalies: anomalies
142
- };
143
- }
144
-
145
- // Main entry point
146
- checkAnomalies().then(result => {
147
- printResult(result);
148
-
149
- // Если найдены аномалии, выводим их в читаемом виде
150
- if (result.anomalies && result.anomalies.length > 0) {
151
- console.log('\n[ANOMALIES DETECTED]');
152
- for (const anomaly of result.anomalies) {
153
- console.log(` - ${anomaly.id}: ${anomaly.title}`);
154
- console.log(` Recommendation: ${anomaly.recommendation}`);
155
- }
156
- }
157
-
158
- if (result.status === 'error') {
159
- process.exit(1);
160
- }
161
- });
@@ -1,254 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * check-conditions.js — Проверяет условия тикетов в backlog/ и выводит список готовых
5
- *
6
- * Использование:
7
- * node check-conditions.js
8
- *
9
- * Выводит результат в формате:
10
- * ---RESULT---
11
- * status: has_ready
12
- * ready_tickets: IMPL-002, DOCS-001
13
- * ---RESULT---
14
- *
15
- * или если готовых нет, но есть тикеты в ready/:
16
- * ---RESULT---
17
- * status: default
18
- * ready_tickets:
19
- * ---RESULT---
20
- *
21
- * или если backlog пуст и нет тикетов в ready/:
22
- * ---RESULT---
23
- * status: empty
24
- * ready_tickets:
25
- * ---RESULT---
26
- */
27
-
28
- import fs from 'fs';
29
- import path from 'path';
30
- import { findProjectRoot } from '../lib/find-root.mjs';
31
- import { parseFrontmatter, printResult, normalizePlanId, extractPlanId, serializeFrontmatter } from '../lib/utils.mjs';
32
-
33
- const PROJECT_DIR = findProjectRoot();
34
- const WORKFLOW_DIR = path.join(PROJECT_DIR, '.workflow');
35
- const TICKETS_DIR = path.join(WORKFLOW_DIR, 'tickets');
36
- const BACKLOG_DIR = path.join(TICKETS_DIR, 'backlog');
37
- const READY_DIR = path.join(TICKETS_DIR, 'ready');
38
- const DONE_DIR = path.join(TICKETS_DIR, 'done');
39
-
40
- /**
41
- * Проверяет одно условие тикета
42
- */
43
- function checkCondition(condition) {
44
- const { type, value } = condition;
45
-
46
- switch (type) {
47
- case 'file_exists':
48
- return fs.existsSync(path.join(PROJECT_DIR, value));
49
-
50
- case 'file_not_exists':
51
- return !fs.existsSync(path.join(PROJECT_DIR, value));
52
-
53
- case 'tasks_completed': {
54
- if (!value || (Array.isArray(value) && value.length === 0)) return true;
55
- const ids = Array.isArray(value) ? value : [value];
56
- return ids.every(taskId => fs.existsSync(path.join(DONE_DIR, `${taskId}.md`)));
57
- }
58
-
59
- case 'date_after':
60
- return new Date() > new Date(value);
61
-
62
- case 'date_before':
63
- return new Date() < new Date(value);
64
-
65
- case 'manual_approval':
66
- return false;
67
-
68
- default:
69
- console.error(`[WARN] Unknown condition type: ${type}`);
70
- return true;
71
- }
72
- }
73
-
74
- /**
75
- * Проверяет зависимости тикета
76
- */
77
- function checkDependencies(dependencies) {
78
- if (!dependencies || dependencies.length === 0) return true;
79
- return dependencies.every(depId => fs.existsSync(path.join(DONE_DIR, `${depId}.md`)));
80
- }
81
-
82
- /**
83
- * Считывает все тикеты из директории
84
- */
85
- function readTickets(dir) {
86
- if (!fs.existsSync(dir)) return [];
87
-
88
- const tickets = [];
89
- const files = fs.readdirSync(dir).filter(f => f.endsWith('.md') && f !== '.gitkeep.md');
90
-
91
- for (const file of files) {
92
- const filePath = path.join(dir, file);
93
- try {
94
- const content = fs.readFileSync(filePath, 'utf8');
95
- const { frontmatter } = parseFrontmatter(content);
96
- tickets.push({ id: frontmatter.id || file.replace('.md', ''), frontmatter });
97
- } catch (e) {
98
- console.error(`[WARN] Failed to read ticket ${file}: ${e.message}`);
99
- }
100
- }
101
-
102
- return tickets;
103
- }
104
-
105
- /**
106
- * Перемещает тикет из ready/ в backlog/
107
- */
108
- function demoteToBacklog(ticketId) {
109
- const sourcePath = path.join(READY_DIR, `${ticketId}.md`);
110
- const targetPath = path.join(BACKLOG_DIR, `${ticketId}.md`);
111
-
112
- if (!fs.existsSync(sourcePath)) {
113
- console.error(`[WARN] ${ticketId}: not found in ready/, skipping`);
114
- return false;
115
- }
116
-
117
- const content = fs.readFileSync(sourcePath, 'utf8');
118
- const { frontmatter, body } = parseFrontmatter(content);
119
-
120
- frontmatter.updated_at = new Date().toISOString();
121
-
122
- const newContent = serializeFrontmatter(frontmatter) + body;
123
-
124
- if (!fs.existsSync(BACKLOG_DIR)) {
125
- fs.mkdirSync(BACKLOG_DIR, { recursive: true });
126
- }
127
-
128
- fs.renameSync(sourcePath, targetPath);
129
- fs.writeFileSync(targetPath, newContent, 'utf8');
130
- return true;
131
- }
132
-
133
- /**
134
- * Проверяет все тикеты в backlog/ и возвращает список готовых
135
- */
136
- function checkBacklog(planId) {
137
- const allTickets = readTickets(BACKLOG_DIR);
138
- const tickets = planId
139
- ? allTickets.filter(t => normalizePlanId(t.frontmatter.parent_plan) === planId)
140
- : allTickets;
141
-
142
- const ready = [];
143
- const waiting = [];
144
-
145
- for (const ticket of tickets) {
146
- const { frontmatter, id } = ticket;
147
-
148
- // Пропускаем тикеты, требующие ручного выполнения
149
- if (frontmatter.type === 'human') {
150
- console.log(`[INFO] ${id}: type is 'human', skipping (requires manual execution)`);
151
- continue;
152
- }
153
-
154
- const conditions = frontmatter.conditions || [];
155
- const dependencies = frontmatter.dependencies || [];
156
-
157
- const depsMet = checkDependencies(dependencies);
158
- const conditionsMet = conditions.every(checkCondition);
159
-
160
- if (depsMet && conditionsMet) {
161
- ready.push(id);
162
- } else {
163
- const reasons = [];
164
- if (!depsMet) reasons.push(`ждёт зависимости: ${dependencies.join(', ')}`);
165
- conditions.forEach(c => {
166
- if (!checkCondition(c)) reasons.push(`условие не выполнено: ${c.type}`);
167
- });
168
- waiting.push({ id, reasons });
169
- }
170
- }
171
-
172
- return { ready, waiting, total: tickets.length };
173
- }
174
-
175
- /**
176
- * Проверяет тикеты в ready/ и возвращает тикеты в backlog при невыполненных условиях
177
- */
178
- function checkReady(planId) {
179
- const allTickets = readTickets(READY_DIR);
180
- const tickets = planId
181
- ? allTickets.filter(t => normalizePlanId(t.frontmatter.parent_plan) === planId)
182
- : allTickets;
183
-
184
- const demoted = [];
185
-
186
- for (const ticket of tickets) {
187
- const { frontmatter, id } = ticket;
188
- const conditions = frontmatter.conditions || [];
189
- const dependencies = frontmatter.dependencies || [];
190
-
191
- const depsMet = checkDependencies(dependencies);
192
- const conditionsMet = conditions.every(checkCondition);
193
-
194
- if (!depsMet || !conditionsMet) {
195
- if (demoteToBacklog(id)) {
196
- console.log(`[INFO] ${id}: ready/ → backlog/ (условия не выполнены)`);
197
- demoted.push(id);
198
- }
199
- }
200
- }
201
-
202
- return { demoted, total: tickets.length };
203
- }
204
-
205
- async function main() {
206
- const planId = extractPlanId();
207
-
208
- if (planId) {
209
- console.log(`[INFO] Filtering by plan_id: ${planId}`);
210
- }
211
-
212
- // Сначала демотирование невалидных тикетов из ready/
213
- console.log(`[INFO] Checking ready/ for invalid tickets: ${READY_DIR}`);
214
- const { demoted, total: readyTotal } = checkReady(planId);
215
- console.log(`[INFO] Total in ready/${planId ? ` (plan ${planId})` : ''}: ${readyTotal}`);
216
- console.log(`[INFO] Demoted to backlog: ${demoted.length}`);
217
-
218
- // Затем проверка backlog — демотированные тикеты сразу переоцениваются
219
- console.log(`[INFO] Scanning backlog/: ${BACKLOG_DIR}`);
220
-
221
- const { ready, waiting, total } = checkBacklog(planId);
222
-
223
- console.log(`[INFO] Total in backlog${planId ? ` (plan ${planId})` : ''}: ${total}`);
224
- console.log(`[INFO] Ready: ${ready.length}, Waiting: ${waiting.length}`);
225
-
226
- if (ready.length > 0) {
227
- console.log(`[INFO] Ready tickets: ${ready.join(', ')}`);
228
- }
229
-
230
- for (const { id, reasons } of waiting) {
231
- console.log(`[INFO] ${id}: ${reasons.join('; ')}`);
232
- }
233
-
234
- if (ready.length > 0) {
235
- printResult({ status: 'has_ready', ready_tickets: ready.join(', '), demoted_tickets: demoted.join(', ') });
236
- return;
237
- }
238
-
239
- // Нет готовых — проверяем есть ли что-то в ready/
240
- const readyDirTickets = readTickets(READY_DIR);
241
- if (readyDirTickets.length > 0) {
242
- console.log(`[INFO] No new ready tickets, but ready/ has ${readyDirTickets.length} ticket(s)`);
243
- printResult({ status: 'default', ready_tickets: '', demoted_tickets: demoted.join(', ') });
244
- } else {
245
- console.log('[INFO] No ready tickets and ready/ is empty');
246
- printResult({ status: 'empty', ready_tickets: '', demoted_tickets: demoted.join(', ') });
247
- }
248
- }
249
-
250
- main().catch(e => {
251
- console.error(`[ERROR] ${e.message}`);
252
- printResult({ status: 'error', error: e.message });
253
- process.exit(1);
254
- });