workflow-ai 1.0.9 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/configs/pipeline.yaml +480 -440
- package/package.json +1 -1
- package/src/cli.mjs +130 -130
- package/src/lib/utils.mjs +57 -0
- package/src/runner.mjs +27 -1
- package/src/scripts/check-conditions.js +69 -4
- package/src/scripts/move-ticket.js +3 -3
- package/src/scripts/pick-next-task.js +168 -2
- package/src/skills/decompose-gaps/SKILL.md +1 -0
- package/src/skills/decompose-plan/SKILL.md +1 -0
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -1,131 +1,131 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { initProject } from './init.mjs';
|
|
4
|
-
import { runPipeline } from './runner.mjs';
|
|
5
|
-
import { readFileSync } from 'node:fs';
|
|
6
|
-
import { join, dirname } from 'node:path';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
|
|
9
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
const pkgPath = join(__dirname, '..', 'package.json');
|
|
11
|
-
|
|
12
|
-
const HELP_TEXT = `workflow-ai v1.0.0
|
|
13
|
-
|
|
14
|
-
Usage:
|
|
15
|
-
workflow init [path] [--force] Initialize .workflow/ in target directory
|
|
16
|
-
workflow run [options] Run the AI pipeline
|
|
17
|
-
workflow help Show this help
|
|
18
|
-
workflow version Show version
|
|
19
|
-
|
|
20
|
-
Run options:
|
|
21
|
-
--plan <plan> Plan ID to execute
|
|
22
|
-
--config <path> Config file path
|
|
23
|
-
--project <path> Project root (default: auto-detect)
|
|
24
|
-
`;
|
|
25
|
-
|
|
26
|
-
function showHelp() {
|
|
27
|
-
console.log(HELP_TEXT);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function showVersion() {
|
|
31
|
-
try {
|
|
32
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
33
|
-
console.log(`workflow-ai v${pkg.version}`);
|
|
34
|
-
} catch (err) {
|
|
35
|
-
console.error('Error reading package.json:', err.message);
|
|
36
|
-
process.exit(1);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function parseArgs(argv) {
|
|
41
|
-
const args = { _: [] };
|
|
42
|
-
let i = 0;
|
|
43
|
-
while (i < argv.length) {
|
|
44
|
-
const arg = argv[i];
|
|
45
|
-
if (arg.startsWith('--')) {
|
|
46
|
-
const key = arg.slice(2);
|
|
47
|
-
if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
|
|
48
|
-
args[key] = argv[i + 1];
|
|
49
|
-
i += 2;
|
|
50
|
-
} else {
|
|
51
|
-
args[key] = true;
|
|
52
|
-
i += 1;
|
|
53
|
-
}
|
|
54
|
-
} else {
|
|
55
|
-
args._.push(arg);
|
|
56
|
-
i += 1;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return args;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async function runInit(args) {
|
|
63
|
-
const targetPath = args._[0] || process.cwd();
|
|
64
|
-
const force = args.force === true;
|
|
65
|
-
const result = initProject(targetPath, { force });
|
|
66
|
-
|
|
67
|
-
if (result.errors && result.errors.length > 0) {
|
|
68
|
-
console.error('Errors:', result.errors.join(', '));
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
console.log('✅ Initialization completed:');
|
|
73
|
-
result.steps.forEach(step => console.log(` • ${step}`));
|
|
74
|
-
if (result.warnings && result.warnings.length > 0) {
|
|
75
|
-
console.warn('Warnings:', result.warnings.join(', '));
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async function runRun(args) {
|
|
80
|
-
// Expose wf's node_modules to child ESM scripts via a custom loader
|
|
81
|
-
const loaderPath = join(__dirname, 'wf-loader.mjs');
|
|
82
|
-
const loaderUrl = `file:///${loaderPath.replace(/\\/g, '/')}`;
|
|
83
|
-
process.env.NODE_OPTIONS = process.env.NODE_OPTIONS
|
|
84
|
-
? `${process.env.NODE_OPTIONS} --import ${loaderUrl}`
|
|
85
|
-
: `--import ${loaderUrl}`;
|
|
86
|
-
|
|
87
|
-
const argv = [];
|
|
88
|
-
if (args.plan) {
|
|
89
|
-
argv.push('--plan', args.plan);
|
|
90
|
-
}
|
|
91
|
-
if (args.config) {
|
|
92
|
-
argv.push('--config', args.config);
|
|
93
|
-
}
|
|
94
|
-
if (args.project) {
|
|
95
|
-
argv.push('--project', args.project);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
await runPipeline(argv);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function run(argv) {
|
|
102
|
-
const args = parseArgs(argv);
|
|
103
|
-
const command = args._.shift();
|
|
104
|
-
|
|
105
|
-
switch (command) {
|
|
106
|
-
case 'init':
|
|
107
|
-
runInit(args);
|
|
108
|
-
break;
|
|
109
|
-
case 'run':
|
|
110
|
-
runRun(args);
|
|
111
|
-
break;
|
|
112
|
-
case 'help':
|
|
113
|
-
showHelp();
|
|
114
|
-
break;
|
|
115
|
-
case 'version':
|
|
116
|
-
showVersion();
|
|
117
|
-
break;
|
|
118
|
-
default:
|
|
119
|
-
if (!command) {
|
|
120
|
-
showHelp();
|
|
121
|
-
} else {
|
|
122
|
-
console.error(`Unknown command: ${command}`);
|
|
123
|
-
console.error('Run "workflow help" for usage.');
|
|
124
|
-
process.exit(1);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
130
|
-
run(process.argv.slice(2));
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { initProject } from './init.mjs';
|
|
4
|
+
import { runPipeline } from './runner.mjs';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { join, dirname } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const pkgPath = join(__dirname, '..', 'package.json');
|
|
11
|
+
|
|
12
|
+
const HELP_TEXT = `workflow-ai v1.0.0
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
workflow init [path] [--force] Initialize .workflow/ in target directory
|
|
16
|
+
workflow run [options] Run the AI pipeline
|
|
17
|
+
workflow help Show this help
|
|
18
|
+
workflow version Show version
|
|
19
|
+
|
|
20
|
+
Run options:
|
|
21
|
+
--plan <plan> Plan ID to execute
|
|
22
|
+
--config <path> Config file path
|
|
23
|
+
--project <path> Project root (default: auto-detect)
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
function showHelp() {
|
|
27
|
+
console.log(HELP_TEXT);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function showVersion() {
|
|
31
|
+
try {
|
|
32
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
33
|
+
console.log(`workflow-ai v${pkg.version}`);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error('Error reading package.json:', err.message);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseArgs(argv) {
|
|
41
|
+
const args = { _: [] };
|
|
42
|
+
let i = 0;
|
|
43
|
+
while (i < argv.length) {
|
|
44
|
+
const arg = argv[i];
|
|
45
|
+
if (arg.startsWith('--')) {
|
|
46
|
+
const key = arg.slice(2);
|
|
47
|
+
if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
|
|
48
|
+
args[key] = argv[i + 1];
|
|
49
|
+
i += 2;
|
|
50
|
+
} else {
|
|
51
|
+
args[key] = true;
|
|
52
|
+
i += 1;
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
args._.push(arg);
|
|
56
|
+
i += 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return args;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function runInit(args) {
|
|
63
|
+
const targetPath = args._[0] || process.cwd();
|
|
64
|
+
const force = args.force === true;
|
|
65
|
+
const result = initProject(targetPath, { force });
|
|
66
|
+
|
|
67
|
+
if (result.errors && result.errors.length > 0) {
|
|
68
|
+
console.error('Errors:', result.errors.join(', '));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log('✅ Initialization completed:');
|
|
73
|
+
result.steps.forEach(step => console.log(` • ${step}`));
|
|
74
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
75
|
+
console.warn('Warnings:', result.warnings.join(', '));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function runRun(args) {
|
|
80
|
+
// Expose wf's node_modules to child ESM scripts via a custom loader
|
|
81
|
+
const loaderPath = join(__dirname, 'wf-loader.mjs');
|
|
82
|
+
const loaderUrl = `file:///${loaderPath.replace(/\\/g, '/')}`;
|
|
83
|
+
process.env.NODE_OPTIONS = process.env.NODE_OPTIONS
|
|
84
|
+
? `${process.env.NODE_OPTIONS} --import ${loaderUrl}`
|
|
85
|
+
: `--import ${loaderUrl}`;
|
|
86
|
+
|
|
87
|
+
const argv = [];
|
|
88
|
+
if (args.plan) {
|
|
89
|
+
argv.push('--plan', args.plan);
|
|
90
|
+
}
|
|
91
|
+
if (args.config) {
|
|
92
|
+
argv.push('--config', args.config);
|
|
93
|
+
}
|
|
94
|
+
if (args.project) {
|
|
95
|
+
argv.push('--project', args.project);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await runPipeline(argv);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function run(argv) {
|
|
102
|
+
const args = parseArgs(argv);
|
|
103
|
+
const command = args._.shift();
|
|
104
|
+
|
|
105
|
+
switch (command) {
|
|
106
|
+
case 'init':
|
|
107
|
+
runInit(args);
|
|
108
|
+
break;
|
|
109
|
+
case 'run':
|
|
110
|
+
runRun(args);
|
|
111
|
+
break;
|
|
112
|
+
case 'help':
|
|
113
|
+
showHelp();
|
|
114
|
+
break;
|
|
115
|
+
case 'version':
|
|
116
|
+
showVersion();
|
|
117
|
+
break;
|
|
118
|
+
default:
|
|
119
|
+
if (!command) {
|
|
120
|
+
showHelp();
|
|
121
|
+
} else {
|
|
122
|
+
console.error(`Unknown command: ${command}`);
|
|
123
|
+
console.error('Run "workflow help" for usage.');
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
130
|
+
run(process.argv.slice(2));
|
|
131
131
|
}
|
package/src/lib/utils.mjs
CHANGED
|
@@ -96,3 +96,60 @@ export function getPackageRoot() {
|
|
|
96
96
|
// result/src/lib → result
|
|
97
97
|
return path.resolve(__dirname, '../../');
|
|
98
98
|
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Парсит секцию "## Ревью" тикета и возвращает статус последней записи.
|
|
102
|
+
* Поддерживает табличный и текстовый форматы.
|
|
103
|
+
*
|
|
104
|
+
* Табличный формат:
|
|
105
|
+
* | Дата | Статус | Комментарий |
|
|
106
|
+
* |------|--------|-------------|
|
|
107
|
+
* | 2026-03-08 | passed | Всё ок |
|
|
108
|
+
*
|
|
109
|
+
* Текстовый формат:
|
|
110
|
+
* - 2026-03-08: passed - Всё ок
|
|
111
|
+
* - 2026-03-08: failed - Есть замечания
|
|
112
|
+
*
|
|
113
|
+
* @param {string} content - Содержимое тикета (markdown)
|
|
114
|
+
* @returns {string|null} "passed", "failed" или null (если нет ревью)
|
|
115
|
+
*/
|
|
116
|
+
export function getLastReviewStatus(content) {
|
|
117
|
+
if (!content) return null;
|
|
118
|
+
|
|
119
|
+
// Находим секцию "## Ревью" — захватываем всё до следующего заголовка ## или конца файла
|
|
120
|
+
const reviewSectionMatch = content.match(/^##\s*Ревью\s*\n([\s\S]*)(?=\n^##\s|$)/m);
|
|
121
|
+
if (!reviewSectionMatch) return null;
|
|
122
|
+
|
|
123
|
+
const reviewSection = reviewSectionMatch[1].trim();
|
|
124
|
+
if (!reviewSection) return null;
|
|
125
|
+
|
|
126
|
+
// Пробуем распарсить табличный формат
|
|
127
|
+
const tableRows = reviewSection.split('\n').filter(line => line.trim().startsWith('|'));
|
|
128
|
+
if (tableRows.length >= 2) {
|
|
129
|
+
// Есть заголовок и разделитель, ищем строки с данными
|
|
130
|
+
const dataRows = tableRows.slice(2).filter(row => {
|
|
131
|
+
const cells = row.split('|').map(c => c.trim()).filter(c => c);
|
|
132
|
+
return cells.length >= 2;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (dataRows.length > 0) {
|
|
136
|
+
const lastRow = dataRows[dataRows.length - 1];
|
|
137
|
+
const cells = lastRow.split('|').map(c => c.trim()).filter(c => c);
|
|
138
|
+
// Статус обычно во второй колонке (после даты), может содержать эмодзи (✅ passed / ❌ failed)
|
|
139
|
+
const statusRaw = cells[1]?.toLowerCase() || '';
|
|
140
|
+
if (statusRaw.includes('passed')) return 'passed';
|
|
141
|
+
if (statusRaw.includes('failed')) return 'failed';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Пробуем распарсить текстовый формат (список)
|
|
146
|
+
const listItems = reviewSection.split('\n').filter(line => line.trim().match(/^[-*]\s/));
|
|
147
|
+
if (listItems.length > 0) {
|
|
148
|
+
const lastItem = listItems[listItems.length - 1].trim();
|
|
149
|
+
// Ищем статус в формате "- дата: passed/failed - комментарий"
|
|
150
|
+
const statusMatch = lastItem.match(/:\s*(passed|failed)\b/i);
|
|
151
|
+
if (statusMatch) return statusMatch[1].toLowerCase();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return null;
|
|
155
|
+
}
|
package/src/runner.mjs
CHANGED
|
@@ -310,6 +310,12 @@ class PromptBuilder {
|
|
|
310
310
|
}
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
+
// Добавляем блок Instructions если поле instructions задано и непустое
|
|
314
|
+
if (stage.instructions && typeof stage.instructions === 'string' && stage.instructions.trim() !== '') {
|
|
315
|
+
parts.push('\n\nInstructions:');
|
|
316
|
+
parts.push(stage.instructions.trim());
|
|
317
|
+
}
|
|
318
|
+
|
|
313
319
|
return parts.join('\n');
|
|
314
320
|
}
|
|
315
321
|
|
|
@@ -718,8 +724,27 @@ class StageExecutor {
|
|
|
718
724
|
throw new Error(`Stage not found: ${stageId}`);
|
|
719
725
|
}
|
|
720
726
|
|
|
721
|
-
// Выбираем
|
|
727
|
+
// Выбираем агента по приоритету:
|
|
728
|
+
// 1. agent_by_attempt[counter] — ротация по попыткам
|
|
729
|
+
// 2. agent_by_type[task_type] — выбор по типу задачи
|
|
730
|
+
// 3. stage.agent — явно указанный агент stage
|
|
731
|
+
// 4. default_agent — глобальный дефолт
|
|
722
732
|
let agentId = stage.agent || this.pipeline.default_agent;
|
|
733
|
+
|
|
734
|
+
// Приоритет 1: agent_by_type (выбор по типу задачи)
|
|
735
|
+
// task_type берётся из контекста (возвращается из pick-next-task)
|
|
736
|
+
if (stage.agent_by_type && this.context.task_type) {
|
|
737
|
+
const taskType = this.context.task_type;
|
|
738
|
+
if (stage.agent_by_type[taskType]) {
|
|
739
|
+
const typeBasedAgent = stage.agent_by_type[taskType];
|
|
740
|
+
if (this.logger) {
|
|
741
|
+
this.logger.info(`Agent by type: task_type="${taskType}" → ${typeBasedAgent}`, stageId);
|
|
742
|
+
}
|
|
743
|
+
agentId = typeBasedAgent;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Приоритет 2: agent_by_attempt (ротация по попыткам) — перекрывает agent_by_type
|
|
723
748
|
if (stage.agent_by_attempt && stage.counter) {
|
|
724
749
|
const attempt = this.counters[stage.counter] || 0;
|
|
725
750
|
if (stage.agent_by_attempt[attempt]) {
|
|
@@ -729,6 +754,7 @@ class StageExecutor {
|
|
|
729
754
|
}
|
|
730
755
|
}
|
|
731
756
|
}
|
|
757
|
+
|
|
732
758
|
const agent = this.pipeline.agents[agentId];
|
|
733
759
|
if (!agent) {
|
|
734
760
|
throw new Error(`Agent not found: ${agentId}`);
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
import fs from 'fs';
|
|
29
29
|
import path from 'path';
|
|
30
30
|
import { findProjectRoot } from '../lib/find-root.mjs';
|
|
31
|
-
import { parseFrontmatter, printResult, normalizePlanId, extractPlanId } from '../lib/utils.mjs';
|
|
31
|
+
import { parseFrontmatter, printResult, normalizePlanId, extractPlanId, serializeFrontmatter } from '../lib/utils.mjs';
|
|
32
32
|
|
|
33
33
|
const PROJECT_DIR = findProjectRoot();
|
|
34
34
|
const WORKFLOW_DIR = path.join(PROJECT_DIR, '.workflow');
|
|
@@ -102,6 +102,34 @@ function readTickets(dir) {
|
|
|
102
102
|
return tickets;
|
|
103
103
|
}
|
|
104
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
|
+
|
|
105
133
|
/**
|
|
106
134
|
* Проверяет все тикеты в backlog/ и возвращает список готовых
|
|
107
135
|
*/
|
|
@@ -137,6 +165,36 @@ function checkBacklog(planId) {
|
|
|
137
165
|
return { ready, waiting, total: tickets.length };
|
|
138
166
|
}
|
|
139
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Проверяет тикеты в ready/ и возвращает тикеты в backlog при невыполненных условиях
|
|
170
|
+
*/
|
|
171
|
+
function checkReady(planId) {
|
|
172
|
+
const allTickets = readTickets(READY_DIR);
|
|
173
|
+
const tickets = planId
|
|
174
|
+
? allTickets.filter(t => normalizePlanId(t.frontmatter.parent_plan) === planId)
|
|
175
|
+
: allTickets;
|
|
176
|
+
|
|
177
|
+
const demoted = [];
|
|
178
|
+
|
|
179
|
+
for (const ticket of tickets) {
|
|
180
|
+
const { frontmatter, id } = ticket;
|
|
181
|
+
const conditions = frontmatter.conditions || [];
|
|
182
|
+
const dependencies = frontmatter.dependencies || [];
|
|
183
|
+
|
|
184
|
+
const depsMet = checkDependencies(dependencies);
|
|
185
|
+
const conditionsMet = conditions.every(checkCondition);
|
|
186
|
+
|
|
187
|
+
if (!depsMet || !conditionsMet) {
|
|
188
|
+
if (demoteToBacklog(id)) {
|
|
189
|
+
console.log(`[INFO] ${id}: ready/ → backlog/ (условия не выполнены)`);
|
|
190
|
+
demoted.push(id);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { demoted, total: tickets.length };
|
|
196
|
+
}
|
|
197
|
+
|
|
140
198
|
async function main() {
|
|
141
199
|
const planId = extractPlanId();
|
|
142
200
|
|
|
@@ -144,6 +202,13 @@ async function main() {
|
|
|
144
202
|
console.log(`[INFO] Filtering by plan_id: ${planId}`);
|
|
145
203
|
}
|
|
146
204
|
|
|
205
|
+
// Сначала демотирование невалидных тикетов из ready/
|
|
206
|
+
console.log(`[INFO] Checking ready/ for invalid tickets: ${READY_DIR}`);
|
|
207
|
+
const { demoted, total: readyTotal } = checkReady(planId);
|
|
208
|
+
console.log(`[INFO] Total in ready/${planId ? ` (plan ${planId})` : ''}: ${readyTotal}`);
|
|
209
|
+
console.log(`[INFO] Demoted to backlog: ${demoted.length}`);
|
|
210
|
+
|
|
211
|
+
// Затем проверка backlog — демотированные тикеты сразу переоцениваются
|
|
147
212
|
console.log(`[INFO] Scanning backlog/: ${BACKLOG_DIR}`);
|
|
148
213
|
|
|
149
214
|
const { ready, waiting, total } = checkBacklog(planId);
|
|
@@ -160,7 +225,7 @@ async function main() {
|
|
|
160
225
|
}
|
|
161
226
|
|
|
162
227
|
if (ready.length > 0) {
|
|
163
|
-
printResult({ status: 'has_ready', ready_tickets: ready.join(', ') });
|
|
228
|
+
printResult({ status: 'has_ready', ready_tickets: ready.join(', '), demoted_tickets: demoted.join(', ') });
|
|
164
229
|
return;
|
|
165
230
|
}
|
|
166
231
|
|
|
@@ -168,10 +233,10 @@ async function main() {
|
|
|
168
233
|
const readyDirTickets = readTickets(READY_DIR);
|
|
169
234
|
if (readyDirTickets.length > 0) {
|
|
170
235
|
console.log(`[INFO] No new ready tickets, but ready/ has ${readyDirTickets.length} ticket(s)`);
|
|
171
|
-
printResult({ status: 'default', ready_tickets: '' });
|
|
236
|
+
printResult({ status: 'default', ready_tickets: '', demoted_tickets: demoted.join(', ') });
|
|
172
237
|
} else {
|
|
173
238
|
console.log('[INFO] No ready tickets and ready/ is empty');
|
|
174
|
-
printResult({ status: 'empty', ready_tickets: '' });
|
|
239
|
+
printResult({ status: 'empty', ready_tickets: '', demoted_tickets: demoted.join(', ') });
|
|
175
240
|
}
|
|
176
241
|
}
|
|
177
242
|
|
|
@@ -27,12 +27,12 @@ const VALID_STATUSES = ['backlog', 'ready', 'in-progress', 'blocked', 'review',
|
|
|
27
27
|
|
|
28
28
|
// Таблица допустимых переходов
|
|
29
29
|
const VALID_TRANSITIONS = {
|
|
30
|
-
'backlog': ['ready'],
|
|
31
|
-
'ready': ['in-progress', 'review'],
|
|
30
|
+
'backlog': ['ready', 'blocked', 'done'],
|
|
31
|
+
'ready': ['in-progress', 'review', 'backlog'],
|
|
32
32
|
'in-progress': ['done', 'blocked', 'review'],
|
|
33
33
|
'blocked': ['ready'],
|
|
34
34
|
'review': ['done', 'ready', 'in-progress', 'blocked'],
|
|
35
|
-
'done': []
|
|
35
|
+
'done': ['ready', 'blocked']
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
/**
|