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.
- package/package.json +1 -1
- package/src/init.mjs +13 -8
- package/src/junction-manager.mjs +8 -4
- package/src/runner.mjs +1 -1
- package/src/scripts/archive-plan-tickets.js +0 -102
- package/src/scripts/check-anomalies.js +0 -161
- package/src/scripts/check-conditions.js +0 -254
- package/src/scripts/check-plan-decomposed.js +0 -179
- package/src/scripts/move-ticket.js +0 -228
- package/src/scripts/move-to-ready.js +0 -115
- package/src/scripts/move-to-review.js +0 -100
- package/src/scripts/pick-next-task.js +0 -723
- package/src/skills/analyze-report/SKILL.md +0 -110
- package/src/skills/check-relevance/SKILL.md +0 -236
- package/src/skills/coach/README.md +0 -100
- package/src/skills/coach/SKILL.md +0 -119
- package/src/skills/coach/algorithms/gap-analysis.md +0 -69
- package/src/skills/coach/algorithms/improvement-prioritization.md +0 -62
- package/src/skills/coach/algorithms/skill-scoring.md +0 -79
- package/src/skills/coach/knowledge/backlog-management.md +0 -71
- package/src/skills/coach/knowledge/common-antipatterns.md +0 -56
- package/src/skills/coach/knowledge/prompt-engineering.md +0 -86
- package/src/skills/coach/knowledge/skill-anatomy.md +0 -71
- package/src/skills/coach/templates/audit-report.md +0 -54
- package/src/skills/coach/templates/coach-backlog-init.yaml +0 -10
- package/src/skills/coach/templates/improvement-plan.md +0 -54
- package/src/skills/coach/templates/new-skill.md +0 -137
- package/src/skills/coach/workflows/analyze.md +0 -85
- package/src/skills/coach/workflows/audit.md +0 -68
- package/src/skills/coach/workflows/create.md +0 -66
- package/src/skills/coach/workflows/improve.md +0 -70
- package/src/skills/coach/workflows/research.md +0 -55
- package/src/skills/coach/workflows/review.md +0 -74
- package/src/skills/create-plan/SKILL.md +0 -107
- package/src/skills/create-report/SKILL.md +0 -156
- package/src/skills/decompose-gaps/SKILL.md +0 -167
- package/src/skills/decompose-plan/SKILL.md +0 -219
- package/src/skills/deep-research/README.md +0 -50
- package/src/skills/deep-research/SKILL.md +0 -148
- package/src/skills/deep-research/algorithms/source-scoring.md +0 -63
- package/src/skills/deep-research/algorithms/synthesis.md +0 -67
- package/src/skills/deep-research/knowledge/data-validation.md +0 -44
- package/src/skills/deep-research/knowledge/research-methodology.md +0 -54
- package/src/skills/deep-research/knowledge/source-evaluation.md +0 -33
- package/src/skills/deep-research/scripts/perplexity-research.js +0 -315
- package/src/skills/deep-research/templates/brief-summary.md +0 -25
- package/src/skills/deep-research/templates/research-report.md +0 -76
- package/src/skills/deep-research/workflows/benchmark.md +0 -56
- package/src/skills/deep-research/workflows/competitor.md +0 -63
- package/src/skills/deep-research/workflows/custom.md +0 -45
- package/src/skills/deep-research/workflows/market.md +0 -64
- package/src/skills/deep-research/workflows/technology.md +0 -52
- package/src/skills/deep-research/workflows/trend.md +0 -51
- package/src/skills/execute-task/SKILL.md +0 -207
- package/src/skills/review-result/SKILL.md +0 -329
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* check-plan-decomposed.js — Проверяет, есть ли недекомпозированные планы.
|
|
5
|
-
*
|
|
6
|
-
* Логика:
|
|
7
|
-
* A) Если plan_id задан — проверяет только этот план
|
|
8
|
-
* B) Если plan_id НЕ задан — сканирует все планы в plans/current/
|
|
9
|
-
*
|
|
10
|
-
* Для каждого плана:
|
|
11
|
-
* 1. Если есть тикеты (backlog/, ready/, in-progress/, review/, done/, blocked/) с parent_plan == planId — decomposed
|
|
12
|
-
* 2. Иначе — needs_decomposition
|
|
13
|
-
*
|
|
14
|
-
* Результат:
|
|
15
|
-
* - needs_decomposition + plan_file — найден первый недекомпозированный план
|
|
16
|
-
* - decomposed — все планы декомпозированы
|
|
17
|
-
* - no_plan — нет планов в plans/current/
|
|
18
|
-
*
|
|
19
|
-
* Использование:
|
|
20
|
-
* node check-plan-decomposed.js "plan_id: PLAN-007"
|
|
21
|
-
* node check-plan-decomposed.js
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import fs from 'fs';
|
|
25
|
-
import path from 'path';
|
|
26
|
-
import { findProjectRoot } from '../lib/find-root.mjs';
|
|
27
|
-
import { parseFrontmatter, printResult, normalizePlanId, extractPlanId } from '../lib/utils.mjs';
|
|
28
|
-
|
|
29
|
-
const PROJECT_DIR = findProjectRoot();
|
|
30
|
-
const WORKFLOW_DIR = path.join(PROJECT_DIR, '.workflow');
|
|
31
|
-
const TICKETS_DIR = path.join(WORKFLOW_DIR, 'tickets');
|
|
32
|
-
const PLANS_DIR = path.join(WORKFLOW_DIR, 'plans', 'current');
|
|
33
|
-
|
|
34
|
-
const TICKET_DIRS = ['backlog', 'ready', 'in-progress', 'review', 'done', 'blocked'];
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Читает статус плана из frontmatter.
|
|
38
|
-
* Только планы со статусом "approved" могут быть декомпозированы автоматически.
|
|
39
|
-
* Планы со статусом "draft", "active" или без статуса — пропускаются.
|
|
40
|
-
*/
|
|
41
|
-
function getPlanStatus(planFile) {
|
|
42
|
-
const fullPath = path.join(WORKFLOW_DIR, planFile);
|
|
43
|
-
if (!fs.existsSync(fullPath)) return null;
|
|
44
|
-
try {
|
|
45
|
-
const content = fs.readFileSync(fullPath, 'utf8');
|
|
46
|
-
const { frontmatter } = parseFrontmatter(content);
|
|
47
|
-
return frontmatter.status || null;
|
|
48
|
-
} catch (e) {
|
|
49
|
-
console.error(`[WARN] Failed to read plan status from ${planFile}: ${e.message}`);
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Проверяет, есть ли тикеты, привязанные к данному плану
|
|
56
|
-
*/
|
|
57
|
-
function hasTicketsForPlan(planId) {
|
|
58
|
-
for (const dir of TICKET_DIRS) {
|
|
59
|
-
const dirPath = path.join(TICKETS_DIR, dir);
|
|
60
|
-
if (!fs.existsSync(dirPath)) continue;
|
|
61
|
-
|
|
62
|
-
const files = fs.readdirSync(dirPath)
|
|
63
|
-
.filter(f => f.endsWith('.md') && f !== '.gitkeep.md');
|
|
64
|
-
|
|
65
|
-
for (const file of files) {
|
|
66
|
-
try {
|
|
67
|
-
const content = fs.readFileSync(path.join(dirPath, file), 'utf8');
|
|
68
|
-
const { frontmatter } = parseFrontmatter(content);
|
|
69
|
-
if (normalizePlanId(frontmatter.parent_plan) === planId) {
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
} catch (e) {
|
|
73
|
-
console.error(`[WARN] Failed to read ${dir}/${file}: ${e.message}`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Находит файл плана в plans/current/
|
|
82
|
-
*/
|
|
83
|
-
function findPlanFile(planId) {
|
|
84
|
-
if (!fs.existsSync(PLANS_DIR)) return null;
|
|
85
|
-
|
|
86
|
-
const expectedName = `${planId}.md`;
|
|
87
|
-
const filePath = path.join(PLANS_DIR, expectedName);
|
|
88
|
-
if (fs.existsSync(filePath)) {
|
|
89
|
-
return `plans/current/${expectedName}`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Поиск по всем файлам на случай другого именования
|
|
93
|
-
const files = fs.readdirSync(PLANS_DIR).filter(f => f.endsWith('.md'));
|
|
94
|
-
for (const file of files) {
|
|
95
|
-
if (normalizePlanId(file) === planId) {
|
|
96
|
-
return `plans/current/${file}`;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Возвращает все файлы планов из plans/current/
|
|
105
|
-
*/
|
|
106
|
-
function getAllPlanFiles() {
|
|
107
|
-
if (!fs.existsSync(PLANS_DIR)) return [];
|
|
108
|
-
|
|
109
|
-
return fs.readdirSync(PLANS_DIR)
|
|
110
|
-
.filter(f => f.endsWith('.md'))
|
|
111
|
-
.map(f => ({
|
|
112
|
-
planId: normalizePlanId(f),
|
|
113
|
-
planFile: `plans/current/${f}`
|
|
114
|
-
}))
|
|
115
|
-
.filter(p => p.planId !== null);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async function main() {
|
|
119
|
-
const planId = extractPlanId();
|
|
120
|
-
|
|
121
|
-
if (planId) {
|
|
122
|
-
// Режим A: конкретный план
|
|
123
|
-
console.log(`[INFO] Checking decomposition for plan: ${planId}`);
|
|
124
|
-
|
|
125
|
-
const planFile = findPlanFile(planId);
|
|
126
|
-
if (!planFile) {
|
|
127
|
-
console.log(`[INFO] Plan ${planId} not found in plans/current/`);
|
|
128
|
-
printResult({ status: 'no_plan' });
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
console.log(`[INFO] Found plan file: ${planFile}`);
|
|
133
|
-
|
|
134
|
-
if (hasTicketsForPlan(planId)) {
|
|
135
|
-
console.log(`[INFO] Plan ${planId} already has tickets — decomposed`);
|
|
136
|
-
printResult({ status: 'decomposed' });
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
console.log(`[INFO] Plan ${planId} has no tickets — needs decomposition`);
|
|
141
|
-
printResult({ status: 'needs_decomposition', plan_file: planFile });
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Режим B: сканируем все планы в plans/current/
|
|
146
|
-
console.log('[INFO] No plan_id specified, scanning all plans in plans/current/');
|
|
147
|
-
|
|
148
|
-
const allPlans = getAllPlanFiles();
|
|
149
|
-
if (allPlans.length === 0) {
|
|
150
|
-
console.log('[INFO] No plans found in plans/current/');
|
|
151
|
-
printResult({ status: 'no_plan' });
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
console.log(`[INFO] Found ${allPlans.length} plan(s) in plans/current/`);
|
|
156
|
-
|
|
157
|
-
for (const { planId: pid, planFile } of allPlans) {
|
|
158
|
-
const planStatus = getPlanStatus(planFile);
|
|
159
|
-
if (planStatus !== 'approved') {
|
|
160
|
-
console.log(`[INFO] Plan ${pid} has status "${planStatus}" (not "approved") — skipping`);
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
if (!hasTicketsForPlan(pid)) {
|
|
164
|
-
console.log(`[INFO] Plan ${pid} has no tickets — needs decomposition`);
|
|
165
|
-
printResult({ status: 'needs_decomposition', plan_file: planFile });
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
console.log(`[INFO] Plan ${pid} already decomposed`);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
console.log('[INFO] All plans are decomposed');
|
|
172
|
-
printResult({ status: 'decomposed' });
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
main().catch(e => {
|
|
176
|
-
console.error(`[ERROR] ${e.message}`);
|
|
177
|
-
printResult({ status: 'error', error: e.message });
|
|
178
|
-
process.exit(1);
|
|
179
|
-
});
|
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* move-ticket.js - Скрипт для перемещения тикетов между директориями канбан-доски
|
|
5
|
-
*
|
|
6
|
-
* Использование:
|
|
7
|
-
* node move-ticket.js <ticket_id> <target>
|
|
8
|
-
*
|
|
9
|
-
* Пример:
|
|
10
|
-
* node move-ticket.js IMPL-001 in-progress
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import fs from 'fs';
|
|
14
|
-
import path from 'path';
|
|
15
|
-
import YAML from '../lib/js-yaml.mjs';
|
|
16
|
-
import { findProjectRoot } from '../lib/find-root.mjs';
|
|
17
|
-
import { parseFrontmatter, printResult, serializeFrontmatter } from '../lib/utils.mjs';
|
|
18
|
-
|
|
19
|
-
// Корень проекта
|
|
20
|
-
const PROJECT_DIR = findProjectRoot();
|
|
21
|
-
// Базовая директория workflow
|
|
22
|
-
const WORKFLOW_DIR = path.join(PROJECT_DIR, '.workflow');
|
|
23
|
-
const TICKETS_DIR = path.join(WORKFLOW_DIR, 'tickets');
|
|
24
|
-
|
|
25
|
-
// Доступные статусы
|
|
26
|
-
const VALID_STATUSES = ['backlog', 'ready', 'in-progress', 'blocked', 'review', 'done', 'archive'];
|
|
27
|
-
|
|
28
|
-
// Таблица допустимых переходов
|
|
29
|
-
const VALID_TRANSITIONS = {
|
|
30
|
-
'backlog': ['ready', 'blocked', 'done'],
|
|
31
|
-
'ready': ['in-progress', 'review', 'backlog'],
|
|
32
|
-
'in-progress': ['done', 'blocked', 'review'],
|
|
33
|
-
'blocked': ['ready'],
|
|
34
|
-
'review': ['done', 'ready', 'in-progress', 'blocked'],
|
|
35
|
-
'done': ['ready', 'blocked', 'archive'],
|
|
36
|
-
'archive': ['backlog']
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Определяет текущий статус тикета по расположению файла
|
|
41
|
-
*/
|
|
42
|
-
function getStatusFromPath(filePath) {
|
|
43
|
-
const fileName = path.basename(filePath);
|
|
44
|
-
for (const status of VALID_STATUSES) {
|
|
45
|
-
const statusDir = path.join(TICKETS_DIR, status);
|
|
46
|
-
const expectedPath = path.join(statusDir, fileName);
|
|
47
|
-
if (filePath === expectedPath) {
|
|
48
|
-
return status;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Проверяет допустимость перехода
|
|
56
|
-
*/
|
|
57
|
-
function isValidTransition(from, to) {
|
|
58
|
-
if (!VALID_STATUSES.includes(from)) {
|
|
59
|
-
return { valid: false, error: `Неверный исходный статус: ${from}` };
|
|
60
|
-
}
|
|
61
|
-
if (!VALID_STATUSES.includes(to)) {
|
|
62
|
-
return { valid: false, error: `Неверный целевой статус: ${to}. Доступные: ${VALID_STATUSES.join(', ')}` };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const allowedTransitions = VALID_TRANSITIONS[from] || [];
|
|
66
|
-
if (!allowedTransitions.includes(to)) {
|
|
67
|
-
return {
|
|
68
|
-
valid: false,
|
|
69
|
-
error: `Переход из ${from} в ${to} недопустим. Доступные переходы: ${allowedTransitions.join(', ') || 'нет'}`
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return { valid: true };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Основная функция перемещения тикета
|
|
78
|
-
*/
|
|
79
|
-
async function moveTicket(ticketId, target) {
|
|
80
|
-
// Поиск файла тикета во всех директориях
|
|
81
|
-
let sourceDir = null;
|
|
82
|
-
let currentStatus = null;
|
|
83
|
-
|
|
84
|
-
for (const status of VALID_STATUSES) {
|
|
85
|
-
const statusDir = path.join(TICKETS_DIR, status);
|
|
86
|
-
const ticketPath = path.join(statusDir, `${ticketId}.md`);
|
|
87
|
-
if (fs.existsSync(ticketPath)) {
|
|
88
|
-
sourceDir = statusDir;
|
|
89
|
-
currentStatus = status;
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (!sourceDir) {
|
|
95
|
-
return {
|
|
96
|
-
status: 'error',
|
|
97
|
-
ticket_id: ticketId,
|
|
98
|
-
error: `Тикет ${ticketId} не найден ни в одной из директорий`
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Проверка допустимости перехода
|
|
103
|
-
const transitionCheck = isValidTransition(currentStatus, target);
|
|
104
|
-
if (!transitionCheck.valid) {
|
|
105
|
-
return {
|
|
106
|
-
status: 'error',
|
|
107
|
-
ticket_id: ticketId,
|
|
108
|
-
from: currentStatus,
|
|
109
|
-
to: target,
|
|
110
|
-
error: transitionCheck.error
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const sourcePath = path.join(sourceDir, `${ticketId}.md`);
|
|
115
|
-
const targetDir = path.join(TICKETS_DIR, target);
|
|
116
|
-
const targetPath = path.join(targetDir, `${ticketId}.md`);
|
|
117
|
-
|
|
118
|
-
// Чтение файла тикета
|
|
119
|
-
let content;
|
|
120
|
-
try {
|
|
121
|
-
content = fs.readFileSync(sourcePath, 'utf8');
|
|
122
|
-
} catch (e) {
|
|
123
|
-
return {
|
|
124
|
-
status: 'error',
|
|
125
|
-
ticket_id: ticketId,
|
|
126
|
-
error: `Не удалось прочитать файл: ${e.message}`
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Парсинг frontmatter
|
|
131
|
-
let frontmatter, body;
|
|
132
|
-
try {
|
|
133
|
-
({ frontmatter, body } = parseFrontmatter(content));
|
|
134
|
-
} catch (e) {
|
|
135
|
-
return {
|
|
136
|
-
status: 'error',
|
|
137
|
-
ticket_id: ticketId,
|
|
138
|
-
error: e.message
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Обновление frontmatter
|
|
143
|
-
const now = new Date().toISOString();
|
|
144
|
-
frontmatter.updated_at = now;
|
|
145
|
-
|
|
146
|
-
// Если переход в done, добавляем completed_at
|
|
147
|
-
if (target === 'done' && currentStatus !== 'done') {
|
|
148
|
-
frontmatter.completed_at = now;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Если переход из blocked, удаляем blocked_reason
|
|
152
|
-
if (currentStatus === 'blocked' && frontmatter.blocked_reason) {
|
|
153
|
-
delete frontmatter.blocked_reason;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Сериализация нового контента
|
|
157
|
-
const newContent = serializeFrontmatter(frontmatter) + body;
|
|
158
|
-
|
|
159
|
-
// Создание целевой директории если не существует
|
|
160
|
-
if (!fs.existsSync(targetDir)) {
|
|
161
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Перемещение файла
|
|
165
|
-
try {
|
|
166
|
-
fs.renameSync(sourcePath, targetPath);
|
|
167
|
-
} catch (e) {
|
|
168
|
-
return {
|
|
169
|
-
status: 'error',
|
|
170
|
-
ticket_id: ticketId,
|
|
171
|
-
error: `Не удалось переместить файл: ${e.message}`
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Запись обновлённого контента
|
|
176
|
-
try {
|
|
177
|
-
fs.writeFileSync(targetPath, newContent, 'utf8');
|
|
178
|
-
} catch (e) {
|
|
179
|
-
return {
|
|
180
|
-
status: 'error',
|
|
181
|
-
ticket_id: ticketId,
|
|
182
|
-
error: `Не удалось записать файл: ${e.message}`
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return {
|
|
187
|
-
status: 'moved',
|
|
188
|
-
ticket_id: ticketId,
|
|
189
|
-
from: currentStatus,
|
|
190
|
-
to: target
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Main entry point
|
|
195
|
-
const rawArgs = process.argv.slice(2);
|
|
196
|
-
let ticketId, target;
|
|
197
|
-
|
|
198
|
-
if (rawArgs.length >= 2) {
|
|
199
|
-
// Прямой вызов: node move-ticket.js IMPL-001 in-progress
|
|
200
|
-
ticketId = rawArgs[0];
|
|
201
|
-
target = rawArgs[1];
|
|
202
|
-
} else if (rawArgs.length === 1) {
|
|
203
|
-
// Вызов через pipeline runner: один аргумент — промпт с контекстом
|
|
204
|
-
// Формат: "skill-name\n\nContext:\n ticket_id: X\n target: Y\n..."
|
|
205
|
-
const prompt = rawArgs[0];
|
|
206
|
-
const ticketMatch = prompt.match(/ticket_id:\s*(\S+)/);
|
|
207
|
-
const targetMatch = prompt.match(/target:\s*(\S+)/);
|
|
208
|
-
ticketId = ticketMatch?.[1];
|
|
209
|
-
target = targetMatch?.[1];
|
|
210
|
-
if (!ticketId || !target) {
|
|
211
|
-
console.error('[ERROR] Cannot parse ticket_id or target from pipeline context');
|
|
212
|
-
printResult({ status: 'error', error: 'Missing ticket_id or target in pipeline context' });
|
|
213
|
-
process.exit(1);
|
|
214
|
-
}
|
|
215
|
-
} else {
|
|
216
|
-
console.error('Usage: node move-ticket.js <ticket_id> <target>');
|
|
217
|
-
console.error('Example: node move-ticket.js IMPL-001 in-progress');
|
|
218
|
-
console.error('Available targets:', VALID_STATUSES.join(', '));
|
|
219
|
-
printResult({ status: 'error', error: 'Missing arguments' });
|
|
220
|
-
process.exit(1);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
moveTicket(ticketId, target).then(result => {
|
|
224
|
-
printResult(result);
|
|
225
|
-
if (result.status === 'error') {
|
|
226
|
-
process.exit(1);
|
|
227
|
-
}
|
|
228
|
-
});
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* move-to-ready.js — Перемещает тикеты из backlog/ в ready/
|
|
5
|
-
*
|
|
6
|
-
* Читает список ticket IDs из контекста (поле ready_tickets),
|
|
7
|
-
* переданного pipeline runner'ом, и перемещает каждый тикет.
|
|
8
|
-
*
|
|
9
|
-
* Формат ready_tickets: "IMPL-002, DOCS-001" (через запятую)
|
|
10
|
-
*
|
|
11
|
-
* Выводит результат:
|
|
12
|
-
* ---RESULT---
|
|
13
|
-
* status: moved | default
|
|
14
|
-
* moved: 2
|
|
15
|
-
* ---RESULT---
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import fs from 'fs';
|
|
19
|
-
import path from 'path';
|
|
20
|
-
import YAML from '../lib/js-yaml.mjs';
|
|
21
|
-
import { findProjectRoot } from '../lib/find-root.mjs';
|
|
22
|
-
import { parseFrontmatter, serializeFrontmatter } from '../lib/utils.mjs';
|
|
23
|
-
|
|
24
|
-
// Корень проекта
|
|
25
|
-
const PROJECT_DIR = findProjectRoot();
|
|
26
|
-
const TICKETS_DIR = path.join(PROJECT_DIR, '.workflow', 'tickets');
|
|
27
|
-
const BACKLOG_DIR = path.join(TICKETS_DIR, 'backlog');
|
|
28
|
-
const READY_DIR = path.join(TICKETS_DIR, 'ready');
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Парсит список ticket IDs из промпта (контекста pipeline runner)
|
|
32
|
-
*/
|
|
33
|
-
function parseReadyTickets(prompt) {
|
|
34
|
-
const match = prompt.match(/ready_tickets:\s*(.+)/);
|
|
35
|
-
if (!match || !match[1].trim()) return [];
|
|
36
|
-
return match[1].split(',').map(id => id.trim()).filter(Boolean);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Перемещает один тикет из backlog/ в ready/
|
|
41
|
-
*/
|
|
42
|
-
function moveToReady(ticketId) {
|
|
43
|
-
const sourcePath = path.join(BACKLOG_DIR, `${ticketId}.md`);
|
|
44
|
-
const targetPath = path.join(READY_DIR, `${ticketId}.md`);
|
|
45
|
-
|
|
46
|
-
if (!fs.existsSync(sourcePath)) {
|
|
47
|
-
console.error(`[WARN] ${ticketId}: not found in backlog/, skipping`);
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const content = fs.readFileSync(sourcePath, 'utf8');
|
|
52
|
-
const { frontmatter, body } = parseFrontmatter(content);
|
|
53
|
-
|
|
54
|
-
// Пропускаем тикеты, требующие ручного выполнения
|
|
55
|
-
if (frontmatter.type === 'human') {
|
|
56
|
-
console.log(`[INFO] ${ticketId}: type is 'human', skipping (requires manual execution)`);
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
frontmatter.updated_at = new Date().toISOString();
|
|
61
|
-
|
|
62
|
-
const newContent = serializeFrontmatter(frontmatter) + body;
|
|
63
|
-
|
|
64
|
-
if (!fs.existsSync(READY_DIR)) {
|
|
65
|
-
fs.mkdirSync(READY_DIR, { recursive: true });
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
fs.renameSync(sourcePath, targetPath);
|
|
69
|
-
fs.writeFileSync(targetPath, newContent, 'utf8');
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function printResult(result) {
|
|
74
|
-
console.log('---RESULT---');
|
|
75
|
-
for (const [key, value] of Object.entries(result)) {
|
|
76
|
-
console.log(`${key}: ${value}`);
|
|
77
|
-
}
|
|
78
|
-
console.log('---RESULT---');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function main() {
|
|
82
|
-
const rawArgs = process.argv.slice(2);
|
|
83
|
-
const prompt = rawArgs[0] || '';
|
|
84
|
-
|
|
85
|
-
const ticketIds = parseReadyTickets(prompt);
|
|
86
|
-
|
|
87
|
-
if (ticketIds.length === 0) {
|
|
88
|
-
console.log('[INFO] No tickets to move');
|
|
89
|
-
printResult({ status: 'default', moved: 0 });
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
console.log(`[INFO] Moving ${ticketIds.length} ticket(s) to ready/`);
|
|
94
|
-
|
|
95
|
-
let moved = 0;
|
|
96
|
-
for (const id of ticketIds) {
|
|
97
|
-
try {
|
|
98
|
-
if (moveToReady(id)) {
|
|
99
|
-
console.log(`[INFO] ${id}: backlog/ → ready/`);
|
|
100
|
-
moved++;
|
|
101
|
-
}
|
|
102
|
-
} catch (e) {
|
|
103
|
-
console.error(`[ERROR] ${id}: ${e.message}`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
console.log(`[INFO] Moved: ${moved}/${ticketIds.length}`);
|
|
108
|
-
printResult({ status: moved > 0 ? 'moved' : 'default', moved });
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
main().catch(e => {
|
|
112
|
-
console.error(`[ERROR] ${e.message}`);
|
|
113
|
-
printResult({ status: 'error', error: e.message });
|
|
114
|
-
process.exit(1);
|
|
115
|
-
});
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* move-to-review.js — Перемещает тикет из in-progress/ в review/
|
|
5
|
-
*
|
|
6
|
-
* Читает ticket_id из контекста pipeline runner'а.
|
|
7
|
-
*
|
|
8
|
-
* Выводит результат:
|
|
9
|
-
* ---RESULT---
|
|
10
|
-
* status: moved | error
|
|
11
|
-
* ticket_id: IMPL-001
|
|
12
|
-
* ---RESULT---
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import fs from 'fs';
|
|
16
|
-
import path from 'path';
|
|
17
|
-
import { findProjectRoot } from '../lib/find-root.mjs';
|
|
18
|
-
import { parseFrontmatter, serializeFrontmatter, printResult } from '../lib/utils.mjs';
|
|
19
|
-
|
|
20
|
-
// Корень проекта
|
|
21
|
-
const PROJECT_DIR = findProjectRoot();
|
|
22
|
-
const TICKETS_DIR = path.join(PROJECT_DIR, '.workflow', 'tickets');
|
|
23
|
-
const IN_PROGRESS_DIR = path.join(TICKETS_DIR, 'in-progress');
|
|
24
|
-
const REVIEW_DIR = path.join(TICKETS_DIR, 'review');
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Парсит ticket_id из промпта (контекста pipeline runner)
|
|
28
|
-
*/
|
|
29
|
-
function parseTicketId(prompt) {
|
|
30
|
-
const match = prompt.match(/ticket_id:\s*(\S+)/);
|
|
31
|
-
return match ? match[1].trim() : null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Перемещает тикет из in-progress/ в review/
|
|
36
|
-
*/
|
|
37
|
-
function moveToReview(ticketId) {
|
|
38
|
-
const sourcePath = path.join(IN_PROGRESS_DIR, `${ticketId}.md`);
|
|
39
|
-
const targetPath = path.join(REVIEW_DIR, `${ticketId}.md`);
|
|
40
|
-
|
|
41
|
-
if (!fs.existsSync(sourcePath)) {
|
|
42
|
-
// Ticket may have been moved by the agent — check other locations
|
|
43
|
-
const reviewPath = path.join(REVIEW_DIR, `${ticketId}.md`);
|
|
44
|
-
if (fs.existsSync(reviewPath)) {
|
|
45
|
-
return { status: 'skipped', ticket_id: ticketId, reason: `${ticketId} already in review/` };
|
|
46
|
-
}
|
|
47
|
-
const donePath = path.join(TICKETS_DIR, 'done', `${ticketId}.md`);
|
|
48
|
-
if (fs.existsSync(donePath)) {
|
|
49
|
-
return { status: 'skipped', ticket_id: ticketId, reason: `${ticketId} already in done/` };
|
|
50
|
-
}
|
|
51
|
-
return { status: 'error', ticket_id: ticketId, error: `${ticketId} not found in in-progress/` };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const content = fs.readFileSync(sourcePath, 'utf8');
|
|
55
|
-
const { frontmatter, body } = parseFrontmatter(content);
|
|
56
|
-
|
|
57
|
-
frontmatter.updated_at = new Date().toISOString();
|
|
58
|
-
|
|
59
|
-
const newContent = serializeFrontmatter(frontmatter) + body;
|
|
60
|
-
|
|
61
|
-
if (!fs.existsSync(REVIEW_DIR)) {
|
|
62
|
-
fs.mkdirSync(REVIEW_DIR, { recursive: true });
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
fs.renameSync(sourcePath, targetPath);
|
|
66
|
-
fs.writeFileSync(targetPath, newContent, 'utf8');
|
|
67
|
-
|
|
68
|
-
return { status: 'moved', ticket_id: ticketId, from: 'in-progress', to: 'review' };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function main() {
|
|
72
|
-
const rawArgs = process.argv.slice(2);
|
|
73
|
-
const prompt = rawArgs[0] || '';
|
|
74
|
-
|
|
75
|
-
const ticketId = parseTicketId(prompt);
|
|
76
|
-
|
|
77
|
-
if (!ticketId) {
|
|
78
|
-
console.error('[ERROR] No ticket_id in context');
|
|
79
|
-
printResult({ status: 'error', error: 'Missing ticket_id' });
|
|
80
|
-
process.exit(1);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
console.log(`[INFO] Moving ${ticketId}: in-progress/ → review/`);
|
|
84
|
-
const result = moveToReview(ticketId);
|
|
85
|
-
printResult(result);
|
|
86
|
-
|
|
87
|
-
if (result.status === 'error') {
|
|
88
|
-
process.exit(1);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (result.status === 'skipped') {
|
|
92
|
-
console.log(`[INFO] Skipped: ${result.reason}`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
main().catch(e => {
|
|
97
|
-
console.error(`[ERROR] ${e.message}`);
|
|
98
|
-
printResult({ status: 'error', error: e.message });
|
|
99
|
-
process.exit(1);
|
|
100
|
-
});
|