workflow-ai 1.0.60 → 1.0.62
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/agent-templates/CLAUDE.md.tpl +58 -58
- package/agent-templates/QWEN.md.tpl +58 -58
- package/package.json +1 -1
- package/src/init.mjs +437 -437
- package/src/runner.mjs +1735 -1713
- package/src/scripts/archive-plan-tickets.js +102 -0
- package/src/scripts/check-anomalies.js +161 -0
- package/src/scripts/check-conditions.js +258 -0
- package/src/scripts/check-mcp.js +277 -0
- package/src/scripts/check-plan-decomposed.js +217 -0
- package/src/scripts/check-plan-templates.js +297 -0
- package/src/scripts/check-relevance.js +311 -0
- package/src/scripts/complete-plan.js +106 -0
- package/src/scripts/get-next-id.js +214 -0
- package/src/scripts/move-ticket.js +260 -0
- package/src/scripts/move-to-ready.js +115 -0
- package/src/scripts/move-to-review.js +151 -0
- package/src/scripts/pick-next-task.js +791 -0
- package/templates/ticket-template.md +3 -10
package/src/init.mjs
CHANGED
|
@@ -1,437 +1,437 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, appendFileSync, symlinkSync, statSync, readdirSync, unlinkSync } from 'node:fs';
|
|
2
|
-
import { join, resolve, dirname, basename } from 'node:path';
|
|
3
|
-
import { execSync } from 'node:child_process';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { getGlobalDir, ensureGlobalDir } from './global-dir.mjs';
|
|
6
|
-
import { createSkillJunctions, createScriptJunction, createConfigJunction } from './junction-manager.mjs';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Возвращает абсолютный путь к корню npm-пакета через import.meta.url.
|
|
10
|
-
*
|
|
11
|
-
* @returns {string} Абсолютный путь к корню пакета
|
|
12
|
-
*/
|
|
13
|
-
function getPackageRoot() {
|
|
14
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
-
const __dirname = dirname(__filename);
|
|
16
|
-
// result/src → result
|
|
17
|
-
return resolve(__dirname, '../');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Создаёт директорию если она не существует.
|
|
22
|
-
*
|
|
23
|
-
* @param {string} dirPath - Путь к директории
|
|
24
|
-
*/
|
|
25
|
-
function ensureDir(dirPath) {
|
|
26
|
-
if (!existsSync(dirPath)) {
|
|
27
|
-
mkdirSync(dirPath, { recursive: true });
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Копирует файл из источника в назначение.
|
|
33
|
-
*
|
|
34
|
-
* @param {string} src - Исходный путь
|
|
35
|
-
* @param {string} dest - Путь назначения
|
|
36
|
-
*/
|
|
37
|
-
function copyFile(src, dest) {
|
|
38
|
-
const destDir = dirname(dest);
|
|
39
|
-
ensureDir(destDir);
|
|
40
|
-
copyFileSync(src, dest);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Рекурсивно копирует директорию.
|
|
45
|
-
*
|
|
46
|
-
* @param {string} srcDir - Исходная директория
|
|
47
|
-
* @param {string} destDir - Директория назначения
|
|
48
|
-
*/
|
|
49
|
-
function copyDirRecursive(srcDir, destDir) {
|
|
50
|
-
ensureDir(destDir);
|
|
51
|
-
|
|
52
|
-
const entries = [];
|
|
53
|
-
try {
|
|
54
|
-
const dirEntries = readdirSync(srcDir, { withFileTypes: true });
|
|
55
|
-
for (const entry of dirEntries) {
|
|
56
|
-
entries.push(entry);
|
|
57
|
-
}
|
|
58
|
-
} catch (e) {
|
|
59
|
-
// Directory doesn't exist, skip
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
for (const entry of entries) {
|
|
64
|
-
const srcPath = join(srcDir, entry.name);
|
|
65
|
-
const destPath = join(destDir, entry.name);
|
|
66
|
-
|
|
67
|
-
if (entry.isDirectory()) {
|
|
68
|
-
copyDirRecursive(srcPath, destPath);
|
|
69
|
-
} else {
|
|
70
|
-
copyFile(srcPath, destPath);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Генерирует таблицу skills из директории .workflow/src/skills/.
|
|
77
|
-
*
|
|
78
|
-
* @param {string} workflowRoot - Путь к корню .workflow
|
|
79
|
-
* @returns {string} Markdown-таблица с навыками
|
|
80
|
-
*/
|
|
81
|
-
function generateSkillsTable(workflowRoot) {
|
|
82
|
-
const skillsDir = join(workflowRoot, 'src', 'skills');
|
|
83
|
-
|
|
84
|
-
if (!existsSync(skillsDir)) {
|
|
85
|
-
return '| Задача | Инструкция |\n|--------|------------|\n';
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const skillsMap = {
|
|
89
|
-
'create-plan': 'Создание плана',
|
|
90
|
-
'analyze-report': 'Анализ отчёта',
|
|
91
|
-
'decompose-plan': 'Декомпозиция плана',
|
|
92
|
-
'check-conditions': 'Проверка готовности',
|
|
93
|
-
'create-report': 'Создание отчёта',
|
|
94
|
-
'execute-task': 'Выполнение задачи',
|
|
95
|
-
'move-ticket': 'Перемещение тикета',
|
|
96
|
-
'pick-next-task': 'Выбор следующей задачи',
|
|
97
|
-
'decompose-gaps': 'Декомпозиция пробелов',
|
|
98
|
-
'review-result': 'Ревью результата',
|
|
99
|
-
'coach': 'Коуч скилов',
|
|
100
|
-
'deep-research': 'Глубокий ресерч'
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
let table = '| Задача | Инструкция |\n|--------|------------|\n';
|
|
104
|
-
|
|
105
|
-
const skillDirs = readdirSync(skillsDir, { withFileTypes: true })
|
|
106
|
-
.filter(entry => entry.isDirectory() || entry.isSymbolicLink())
|
|
107
|
-
.map(entry => entry.name);
|
|
108
|
-
|
|
109
|
-
for (const skillDir of skillDirs) {
|
|
110
|
-
const description = skillsMap[skillDir] || skillDir;
|
|
111
|
-
const instruction = `.workflow/src/skills/${skillDir}/SKILL.md`;
|
|
112
|
-
table += `| ${description} | \`${instruction}\` |\n`;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return table;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Генерирует CLAUDE.md из шаблона.
|
|
120
|
-
*
|
|
121
|
-
* @param {string} workflowRoot - Путь к корню .workflow
|
|
122
|
-
* @param {string} projectRoot - Путь к корню проекта
|
|
123
|
-
* @param {string} packageRoot - Путь к корню пакета
|
|
124
|
-
*/
|
|
125
|
-
function generateClaudeMd(workflowRoot, projectRoot, packageRoot) {
|
|
126
|
-
const templatePath = join(packageRoot, 'agent-templates', 'CLAUDE.md.tpl');
|
|
127
|
-
const destPath = join(projectRoot, 'CLAUDE.md');
|
|
128
|
-
|
|
129
|
-
let content;
|
|
130
|
-
if (existsSync(templatePath)) {
|
|
131
|
-
content = readFileSync(templatePath, 'utf-8');
|
|
132
|
-
} else {
|
|
133
|
-
// Default template
|
|
134
|
-
content = `# Инструкции для Claude Code
|
|
135
|
-
|
|
136
|
-
Этот проект использует систему координации AI-агентов через файловую канбан-доску.
|
|
137
|
-
|
|
138
|
-
## Структура проекта
|
|
139
|
-
|
|
140
|
-
- \`.workflow/\` — канбан-доска с тикетами
|
|
141
|
-
- \`.workflow/src/skills/\` — инструкции для выполнения задач
|
|
142
|
-
|
|
143
|
-
## Доступные Skills
|
|
144
|
-
|
|
145
|
-
{{SKILLS_TABLE}}
|
|
146
|
-
|
|
147
|
-
## Workflow
|
|
148
|
-
|
|
149
|
-
1. **Планирование**: Создай план в \`.workflow/plans/current/\`
|
|
150
|
-
2. **Декомпозиция**: Разбей план на тикеты в \`.workflow/tickets/backlog/\`
|
|
151
|
-
3. **Выполнение**: Бери задачи из \`ready/\`, выполняй, перемещай в \`done/\`
|
|
152
|
-
4. **Отчётность**: Создавай отчёты в \`.workflow/reports/\`
|
|
153
|
-
|
|
154
|
-
## Шаблоны
|
|
155
|
-
|
|
156
|
-
- \`.workflow/templates/ticket-template.md\` — шаблон тикета
|
|
157
|
-
- \`.workflow/templates/plan-template.md\` — шаблон плана
|
|
158
|
-
- \`.workflow/templates/report-template.md\` — шаблон отчёта
|
|
159
|
-
|
|
160
|
-
## Конфигурация
|
|
161
|
-
|
|
162
|
-
Настройки в \`.workflow/config/config.yaml\`
|
|
163
|
-
|
|
164
|
-
## Правила написания кода
|
|
165
|
-
При написании кода использовать методологии TDD, SOLID, DRY
|
|
166
|
-
`;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const skillsTable = generateSkillsTable(workflowRoot);
|
|
170
|
-
content = content.replace('{{SKILLS_TABLE}}', skillsTable);
|
|
171
|
-
|
|
172
|
-
writeFileSync(destPath, content, 'utf-8');
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Генерирует QWEN.md из шаблона.
|
|
177
|
-
*
|
|
178
|
-
* @param {string} workflowRoot - Путь к корню .workflow
|
|
179
|
-
* @param {string} projectRoot - Путь к корню проекта
|
|
180
|
-
* @param {string} packageRoot - Путь к корню пакета
|
|
181
|
-
*/
|
|
182
|
-
function generateQwenMd(workflowRoot, projectRoot, packageRoot) {
|
|
183
|
-
const templatePath = join(packageRoot, 'agent-templates', 'QWEN.md.tpl');
|
|
184
|
-
const destPath = join(projectRoot, 'QWEN.md');
|
|
185
|
-
|
|
186
|
-
let content;
|
|
187
|
-
if (existsSync(templatePath)) {
|
|
188
|
-
content = readFileSync(templatePath, 'utf-8');
|
|
189
|
-
} else {
|
|
190
|
-
// Default template
|
|
191
|
-
content = `# Инструкции для qwen Code
|
|
192
|
-
|
|
193
|
-
Этот проект использует систему координации AI-агентов через файловую канбан-доску.
|
|
194
|
-
|
|
195
|
-
## Структура проекта
|
|
196
|
-
|
|
197
|
-
- \`.workflow/\` — канбан-доска с тикетами
|
|
198
|
-
- \`.workflow/src/skills/\` — инструкции для выполнения задач
|
|
199
|
-
|
|
200
|
-
## Доступные Skills
|
|
201
|
-
|
|
202
|
-
{{SKILLS_TABLE}}
|
|
203
|
-
|
|
204
|
-
## Workflow
|
|
205
|
-
|
|
206
|
-
1. **Планирование**: Создай план в \`.workflow/plans/current/\`
|
|
207
|
-
2. **Декомпозиция**: Разбей план на тикеты в \`.workflow/tickets/backlog/\`
|
|
208
|
-
3. **Выполнение**: Бери задачи из \`ready/\`, выполняй, перемещай в \`done/\`
|
|
209
|
-
4. **Отчётность**: Создавай отчёты в \`.workflow/reports/\`
|
|
210
|
-
|
|
211
|
-
## Шаблоны
|
|
212
|
-
|
|
213
|
-
- \`.workflow/templates/ticket-template.md\` — шаблон тикета
|
|
214
|
-
- \`.workflow/templates/plan-template.md\` — шаблон плана
|
|
215
|
-
- \`.workflow/templates/report-template.md\` — шаблон отчёта
|
|
216
|
-
|
|
217
|
-
## Конфигурация
|
|
218
|
-
|
|
219
|
-
Настройки в \`.workflow/config/config.yaml\`
|
|
220
|
-
|
|
221
|
-
## Правила написания кода
|
|
222
|
-
При написании кода использовать методологии TDD, SOLID, DRY
|
|
223
|
-
`;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const skillsTable = generateSkillsTable(workflowRoot);
|
|
227
|
-
content = content.replace('{{SKILLS_TABLE}}', skillsTable);
|
|
228
|
-
|
|
229
|
-
writeFileSync(destPath, content, 'utf-8');
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Генерирует .kilocodemodes из шаблона agent-templates/kilocodemodes.tpl.
|
|
234
|
-
*
|
|
235
|
-
* @param {string} projectRoot - Путь к корню проекта
|
|
236
|
-
* @param {string} packageRoot - Путь к корню пакета
|
|
237
|
-
*/
|
|
238
|
-
function generateKilocodemodes(projectRoot, packageRoot) {
|
|
239
|
-
const templatePath = join(packageRoot, 'agent-templates', 'kilocodemodes.tpl');
|
|
240
|
-
const destPath = join(projectRoot, '.kilocodemodes');
|
|
241
|
-
|
|
242
|
-
if (existsSync(templatePath)) {
|
|
243
|
-
copyFileSync(templatePath, destPath);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Обновляет .gitignore, добавляя указанные строки.
|
|
249
|
-
*
|
|
250
|
-
* @param {string} projectRoot - Путь к корню проекта
|
|
251
|
-
*/
|
|
252
|
-
function updateGitignore(projectRoot) {
|
|
253
|
-
const gitignorePath = join(projectRoot, '.gitignore');
|
|
254
|
-
const linesToAdd = [
|
|
255
|
-
'',
|
|
256
|
-
'# Workflow AI specific',
|
|
257
|
-
'.workflow-state/',
|
|
258
|
-
'.cache/',
|
|
259
|
-
'.workflow/',
|
|
260
|
-
'',
|
|
261
|
-
'# AI',
|
|
262
|
-
'QWEN.md',
|
|
263
|
-
'CLAUDE.md',
|
|
264
|
-
'.kilocode/',
|
|
265
|
-
'.kilocodemodes',
|
|
266
|
-
];
|
|
267
|
-
|
|
268
|
-
let currentContent = '';
|
|
269
|
-
if (existsSync(gitignorePath)) {
|
|
270
|
-
currentContent = readFileSync(gitignorePath, 'utf-8');
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const existingLines = currentContent.split('\n').map(line => line.trim());
|
|
274
|
-
|
|
275
|
-
const newLines = linesToAdd.filter(line => line === '' || !existingLines.includes(line));
|
|
276
|
-
if (newLines.some(line => line !== '')) {
|
|
277
|
-
appendFileSync(gitignorePath, newLines.join('\n') + '\n');
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Создаёт симлинки .kilocode.
|
|
283
|
-
*
|
|
284
|
-
* @param {string} projectRoot - Путь к корню проекта
|
|
285
|
-
* @param {boolean} force - Принудительное создание
|
|
286
|
-
* @returns {{ success: boolean, warning?: string }} Результат операции
|
|
287
|
-
*/
|
|
288
|
-
function createKilocodeSymlinks(projectRoot, force = false) {
|
|
289
|
-
const kilocodeDir = join(projectRoot, '.kilocode');
|
|
290
|
-
const skillsTarget = join(projectRoot, '.workflow', 'src', 'skills');
|
|
291
|
-
const skillsLink = join(kilocodeDir, 'skills');
|
|
292
|
-
|
|
293
|
-
ensureDir(kilocodeDir);
|
|
294
|
-
|
|
295
|
-
const isWindows = process.platform === 'win32';
|
|
296
|
-
|
|
297
|
-
try {
|
|
298
|
-
// Remove existing link if exists
|
|
299
|
-
if (existsSync(skillsLink)) {
|
|
300
|
-
const stats = statSync(skillsLink);
|
|
301
|
-
if (stats.isSymbolicLink() || stats.isDirectory()) {
|
|
302
|
-
try {
|
|
303
|
-
if (isWindows) {
|
|
304
|
-
execSync(`rmdir "${skillsLink}"`);
|
|
305
|
-
} else {
|
|
306
|
-
unlinkSync(skillsLink);
|
|
307
|
-
}
|
|
308
|
-
} catch (e) {
|
|
309
|
-
// Ignore errors
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (isWindows) {
|
|
315
|
-
// Windows: use Junction Point
|
|
316
|
-
try {
|
|
317
|
-
execSync(`mklink /J "${skillsLink}" "${skillsTarget}"`);
|
|
318
|
-
return { success: true };
|
|
319
|
-
} catch (e) {
|
|
320
|
-
// Fallback: copy directory
|
|
321
|
-
copyDirRecursive(skillsTarget, skillsLink);
|
|
322
|
-
return {
|
|
323
|
-
success: true,
|
|
324
|
-
warning: 'Junction Point creation failed, copied files instead'
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
} else {
|
|
328
|
-
// Linux/macOS: use symlink
|
|
329
|
-
symlinkSync(skillsTarget, skillsLink);
|
|
330
|
-
return { success: true };
|
|
331
|
-
}
|
|
332
|
-
} catch (e) {
|
|
333
|
-
return {
|
|
334
|
-
success: false,
|
|
335
|
-
warning: `Failed to create symlink: ${e.message}`
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Инициализирует проект, создавая структуру .workflow/ и копируя файлы.
|
|
342
|
-
*
|
|
343
|
-
* @param {string} targetPath - Путь к целевому проекту (по умолчанию process.cwd())
|
|
344
|
-
* @param {object} options - Опции инициализации
|
|
345
|
-
* @param {boolean} options.force - Принудительная перезапись файлов
|
|
346
|
-
* @returns {object} Результат инициализации
|
|
347
|
-
*/
|
|
348
|
-
export function initProject(targetPath = process.cwd(), options = {}) {
|
|
349
|
-
const { force = false } = options;
|
|
350
|
-
const projectRoot = resolve(targetPath);
|
|
351
|
-
const workflowRoot = join(projectRoot, '.workflow');
|
|
352
|
-
const packageRoot = getPackageRoot();
|
|
353
|
-
|
|
354
|
-
const result = {
|
|
355
|
-
steps: [],
|
|
356
|
-
warnings: [],
|
|
357
|
-
errors: []
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
// Step 1: Create .workflow/ structure (15 directories)
|
|
361
|
-
const directories = [
|
|
362
|
-
'tickets/backlog',
|
|
363
|
-
'tickets/ready',
|
|
364
|
-
'tickets/in-progress',
|
|
365
|
-
'tickets/blocked',
|
|
366
|
-
'tickets/review',
|
|
367
|
-
'tickets/done',
|
|
368
|
-
'plans/current',
|
|
369
|
-
'plans/archive',
|
|
370
|
-
'reports',
|
|
371
|
-
'logs',
|
|
372
|
-
'templates',
|
|
373
|
-
'src/skills'
|
|
374
|
-
];
|
|
375
|
-
|
|
376
|
-
for (const dir of directories) {
|
|
377
|
-
ensureDir(join(workflowRoot, dir));
|
|
378
|
-
}
|
|
379
|
-
result.steps.push('Created .workflow/ directory structure (15 directories)');
|
|
380
|
-
|
|
381
|
-
// Step 2: Ensure global dir and create skill junctions
|
|
382
|
-
const globalDir = getGlobalDir();
|
|
383
|
-
ensureGlobalDir(packageRoot);
|
|
384
|
-
const srcSkillsDest = join(workflowRoot, 'src', 'skills');
|
|
385
|
-
createSkillJunctions(globalDir, srcSkillsDest);
|
|
386
|
-
result.steps.push('Created skill junctions from global dir → .workflow/src/skills/');
|
|
387
|
-
|
|
388
|
-
// Step 3: Create script junction
|
|
389
|
-
const srcScriptsDest = join(workflowRoot, 'src', 'scripts');
|
|
390
|
-
createScriptJunction(globalDir, srcScriptsDest);
|
|
391
|
-
result.steps.push('Created script junction from global dir → .workflow/src/scripts/');
|
|
392
|
-
|
|
393
|
-
// Step 5: Copy templates (3 templates)
|
|
394
|
-
const templatesSrc = join(packageRoot, 'templates');
|
|
395
|
-
const templatesDest = join(workflowRoot, 'templates');
|
|
396
|
-
ensureDir(templatesDest);
|
|
397
|
-
|
|
398
|
-
const templateFiles = ['ticket-template.md', 'plan-template.md', 'report-template.md'];
|
|
399
|
-
for (const template of templateFiles) {
|
|
400
|
-
const srcPath = join(templatesSrc, template);
|
|
401
|
-
const destPath = join(templatesDest, template);
|
|
402
|
-
if (existsSync(srcPath)) {
|
|
403
|
-
copyFile(srcPath, destPath);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
result.steps.push('Copied 3 templates → .workflow/templates/');
|
|
407
|
-
|
|
408
|
-
// Step 6: Create config junction
|
|
409
|
-
const configDest = join(workflowRoot, 'config');
|
|
410
|
-
createConfigJunction(globalDir, configDest);
|
|
411
|
-
result.steps.push('Created config junction from global dir → .workflow/config/');
|
|
412
|
-
|
|
413
|
-
// Step 7: Create .kilocode symlinks
|
|
414
|
-
const symlinkResult = createKilocodeSymlinks(projectRoot, force);
|
|
415
|
-
if (symlinkResult.success) {
|
|
416
|
-
result.steps.push('Created .kilocode symlinks (Junction Point on Windows)');
|
|
417
|
-
if (symlinkResult.warning) {
|
|
418
|
-
result.warnings.push(symlinkResult.warning);
|
|
419
|
-
}
|
|
420
|
-
} else {
|
|
421
|
-
result.errors.push(symlinkResult.warning || 'Failed to create .kilocode symlinks');
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Step 8: Generate CLAUDE.md, QWEN.md and .kilocodemodes
|
|
425
|
-
generateClaudeMd(workflowRoot, projectRoot, packageRoot);
|
|
426
|
-
generateQwenMd(workflowRoot, projectRoot, packageRoot);
|
|
427
|
-
generateKilocodemodes(projectRoot, packageRoot);
|
|
428
|
-
result.steps.push('Generated CLAUDE.md, QWEN.md and .kilocodemodes from agent-templates');
|
|
429
|
-
|
|
430
|
-
// Step 9: Update .gitignore
|
|
431
|
-
updateGitignore(projectRoot);
|
|
432
|
-
result.steps.push('Updated .gitignore with .workflow/logs/');
|
|
433
|
-
|
|
434
|
-
return result;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
export default initProject;
|
|
1
|
+
import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, appendFileSync, symlinkSync, statSync, readdirSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { join, resolve, dirname, basename } from 'node:path';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { getGlobalDir, ensureGlobalDir } from './global-dir.mjs';
|
|
6
|
+
import { createSkillJunctions, createScriptJunction, createConfigJunction } from './junction-manager.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Возвращает абсолютный путь к корню npm-пакета через import.meta.url.
|
|
10
|
+
*
|
|
11
|
+
* @returns {string} Абсолютный путь к корню пакета
|
|
12
|
+
*/
|
|
13
|
+
function getPackageRoot() {
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
// result/src → result
|
|
17
|
+
return resolve(__dirname, '../');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Создаёт директорию если она не существует.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} dirPath - Путь к директории
|
|
24
|
+
*/
|
|
25
|
+
function ensureDir(dirPath) {
|
|
26
|
+
if (!existsSync(dirPath)) {
|
|
27
|
+
mkdirSync(dirPath, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Копирует файл из источника в назначение.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} src - Исходный путь
|
|
35
|
+
* @param {string} dest - Путь назначения
|
|
36
|
+
*/
|
|
37
|
+
function copyFile(src, dest) {
|
|
38
|
+
const destDir = dirname(dest);
|
|
39
|
+
ensureDir(destDir);
|
|
40
|
+
copyFileSync(src, dest);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Рекурсивно копирует директорию.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} srcDir - Исходная директория
|
|
47
|
+
* @param {string} destDir - Директория назначения
|
|
48
|
+
*/
|
|
49
|
+
function copyDirRecursive(srcDir, destDir) {
|
|
50
|
+
ensureDir(destDir);
|
|
51
|
+
|
|
52
|
+
const entries = [];
|
|
53
|
+
try {
|
|
54
|
+
const dirEntries = readdirSync(srcDir, { withFileTypes: true });
|
|
55
|
+
for (const entry of dirEntries) {
|
|
56
|
+
entries.push(entry);
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {
|
|
59
|
+
// Directory doesn't exist, skip
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
const srcPath = join(srcDir, entry.name);
|
|
65
|
+
const destPath = join(destDir, entry.name);
|
|
66
|
+
|
|
67
|
+
if (entry.isDirectory()) {
|
|
68
|
+
copyDirRecursive(srcPath, destPath);
|
|
69
|
+
} else {
|
|
70
|
+
copyFile(srcPath, destPath);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Генерирует таблицу skills из директории .workflow/src/skills/.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} workflowRoot - Путь к корню .workflow
|
|
79
|
+
* @returns {string} Markdown-таблица с навыками
|
|
80
|
+
*/
|
|
81
|
+
function generateSkillsTable(workflowRoot) {
|
|
82
|
+
const skillsDir = join(workflowRoot, 'src', 'skills');
|
|
83
|
+
|
|
84
|
+
if (!existsSync(skillsDir)) {
|
|
85
|
+
return '| Задача | Инструкция |\n|--------|------------|\n';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const skillsMap = {
|
|
89
|
+
'create-plan': 'Создание плана',
|
|
90
|
+
'analyze-report': 'Анализ отчёта',
|
|
91
|
+
'decompose-plan': 'Декомпозиция плана',
|
|
92
|
+
'check-conditions': 'Проверка готовности',
|
|
93
|
+
'create-report': 'Создание отчёта',
|
|
94
|
+
'execute-task': 'Выполнение задачи',
|
|
95
|
+
'move-ticket': 'Перемещение тикета',
|
|
96
|
+
'pick-next-task': 'Выбор следующей задачи',
|
|
97
|
+
'decompose-gaps': 'Декомпозиция пробелов',
|
|
98
|
+
'review-result': 'Ревью результата',
|
|
99
|
+
'coach': 'Коуч скилов',
|
|
100
|
+
'deep-research': 'Глубокий ресерч'
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
let table = '| Задача | Инструкция |\n|--------|------------|\n';
|
|
104
|
+
|
|
105
|
+
const skillDirs = readdirSync(skillsDir, { withFileTypes: true })
|
|
106
|
+
.filter(entry => entry.isDirectory() || entry.isSymbolicLink())
|
|
107
|
+
.map(entry => entry.name);
|
|
108
|
+
|
|
109
|
+
for (const skillDir of skillDirs) {
|
|
110
|
+
const description = skillsMap[skillDir] || skillDir;
|
|
111
|
+
const instruction = `.workflow/src/skills/${skillDir}/SKILL.md`;
|
|
112
|
+
table += `| ${description} | \`${instruction}\` |\n`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return table;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Генерирует CLAUDE.md из шаблона.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} workflowRoot - Путь к корню .workflow
|
|
122
|
+
* @param {string} projectRoot - Путь к корню проекта
|
|
123
|
+
* @param {string} packageRoot - Путь к корню пакета
|
|
124
|
+
*/
|
|
125
|
+
function generateClaudeMd(workflowRoot, projectRoot, packageRoot) {
|
|
126
|
+
const templatePath = join(packageRoot, 'agent-templates', 'CLAUDE.md.tpl');
|
|
127
|
+
const destPath = join(projectRoot, 'CLAUDE.md');
|
|
128
|
+
|
|
129
|
+
let content;
|
|
130
|
+
if (existsSync(templatePath)) {
|
|
131
|
+
content = readFileSync(templatePath, 'utf-8');
|
|
132
|
+
} else {
|
|
133
|
+
// Default template
|
|
134
|
+
content = `# Инструкции для Claude Code
|
|
135
|
+
|
|
136
|
+
Этот проект использует систему координации AI-агентов через файловую канбан-доску.
|
|
137
|
+
|
|
138
|
+
## Структура проекта
|
|
139
|
+
|
|
140
|
+
- \`.workflow/\` — канбан-доска с тикетами
|
|
141
|
+
- \`.workflow/src/skills/\` — инструкции для выполнения задач
|
|
142
|
+
|
|
143
|
+
## Доступные Skills
|
|
144
|
+
|
|
145
|
+
{{SKILLS_TABLE}}
|
|
146
|
+
|
|
147
|
+
## Workflow
|
|
148
|
+
|
|
149
|
+
1. **Планирование**: Создай план в \`.workflow/plans/current/\`
|
|
150
|
+
2. **Декомпозиция**: Разбей план на тикеты в \`.workflow/tickets/backlog/\`
|
|
151
|
+
3. **Выполнение**: Бери задачи из \`ready/\`, выполняй, перемещай в \`done/\`
|
|
152
|
+
4. **Отчётность**: Создавай отчёты в \`.workflow/reports/\`
|
|
153
|
+
|
|
154
|
+
## Шаблоны
|
|
155
|
+
|
|
156
|
+
- \`.workflow/templates/ticket-template.md\` — шаблон тикета
|
|
157
|
+
- \`.workflow/templates/plan-template.md\` — шаблон плана
|
|
158
|
+
- \`.workflow/templates/report-template.md\` — шаблон отчёта
|
|
159
|
+
|
|
160
|
+
## Конфигурация
|
|
161
|
+
|
|
162
|
+
Настройки в \`.workflow/config/config.yaml\`
|
|
163
|
+
|
|
164
|
+
## Правила написания кода
|
|
165
|
+
При написании кода использовать методологии TDD, SOLID, DRY
|
|
166
|
+
`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const skillsTable = generateSkillsTable(workflowRoot);
|
|
170
|
+
content = content.replace('{{SKILLS_TABLE}}', skillsTable);
|
|
171
|
+
|
|
172
|
+
writeFileSync(destPath, content, 'utf-8');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Генерирует QWEN.md из шаблона.
|
|
177
|
+
*
|
|
178
|
+
* @param {string} workflowRoot - Путь к корню .workflow
|
|
179
|
+
* @param {string} projectRoot - Путь к корню проекта
|
|
180
|
+
* @param {string} packageRoot - Путь к корню пакета
|
|
181
|
+
*/
|
|
182
|
+
function generateQwenMd(workflowRoot, projectRoot, packageRoot) {
|
|
183
|
+
const templatePath = join(packageRoot, 'agent-templates', 'QWEN.md.tpl');
|
|
184
|
+
const destPath = join(projectRoot, 'QWEN.md');
|
|
185
|
+
|
|
186
|
+
let content;
|
|
187
|
+
if (existsSync(templatePath)) {
|
|
188
|
+
content = readFileSync(templatePath, 'utf-8');
|
|
189
|
+
} else {
|
|
190
|
+
// Default template
|
|
191
|
+
content = `# Инструкции для qwen Code
|
|
192
|
+
|
|
193
|
+
Этот проект использует систему координации AI-агентов через файловую канбан-доску.
|
|
194
|
+
|
|
195
|
+
## Структура проекта
|
|
196
|
+
|
|
197
|
+
- \`.workflow/\` — канбан-доска с тикетами
|
|
198
|
+
- \`.workflow/src/skills/\` — инструкции для выполнения задач
|
|
199
|
+
|
|
200
|
+
## Доступные Skills
|
|
201
|
+
|
|
202
|
+
{{SKILLS_TABLE}}
|
|
203
|
+
|
|
204
|
+
## Workflow
|
|
205
|
+
|
|
206
|
+
1. **Планирование**: Создай план в \`.workflow/plans/current/\`
|
|
207
|
+
2. **Декомпозиция**: Разбей план на тикеты в \`.workflow/tickets/backlog/\`
|
|
208
|
+
3. **Выполнение**: Бери задачи из \`ready/\`, выполняй, перемещай в \`done/\`
|
|
209
|
+
4. **Отчётность**: Создавай отчёты в \`.workflow/reports/\`
|
|
210
|
+
|
|
211
|
+
## Шаблоны
|
|
212
|
+
|
|
213
|
+
- \`.workflow/templates/ticket-template.md\` — шаблон тикета
|
|
214
|
+
- \`.workflow/templates/plan-template.md\` — шаблон плана
|
|
215
|
+
- \`.workflow/templates/report-template.md\` — шаблон отчёта
|
|
216
|
+
|
|
217
|
+
## Конфигурация
|
|
218
|
+
|
|
219
|
+
Настройки в \`.workflow/config/config.yaml\`
|
|
220
|
+
|
|
221
|
+
## Правила написания кода
|
|
222
|
+
При написании кода использовать методологии TDD, SOLID, DRY
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const skillsTable = generateSkillsTable(workflowRoot);
|
|
227
|
+
content = content.replace('{{SKILLS_TABLE}}', skillsTable);
|
|
228
|
+
|
|
229
|
+
writeFileSync(destPath, content, 'utf-8');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Генерирует .kilocodemodes из шаблона agent-templates/kilocodemodes.tpl.
|
|
234
|
+
*
|
|
235
|
+
* @param {string} projectRoot - Путь к корню проекта
|
|
236
|
+
* @param {string} packageRoot - Путь к корню пакета
|
|
237
|
+
*/
|
|
238
|
+
function generateKilocodemodes(projectRoot, packageRoot) {
|
|
239
|
+
const templatePath = join(packageRoot, 'agent-templates', 'kilocodemodes.tpl');
|
|
240
|
+
const destPath = join(projectRoot, '.kilocodemodes');
|
|
241
|
+
|
|
242
|
+
if (existsSync(templatePath)) {
|
|
243
|
+
copyFileSync(templatePath, destPath);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Обновляет .gitignore, добавляя указанные строки.
|
|
249
|
+
*
|
|
250
|
+
* @param {string} projectRoot - Путь к корню проекта
|
|
251
|
+
*/
|
|
252
|
+
function updateGitignore(projectRoot) {
|
|
253
|
+
const gitignorePath = join(projectRoot, '.gitignore');
|
|
254
|
+
const linesToAdd = [
|
|
255
|
+
'',
|
|
256
|
+
'# Workflow AI specific',
|
|
257
|
+
'.workflow-state/',
|
|
258
|
+
'.cache/',
|
|
259
|
+
'.workflow/',
|
|
260
|
+
'',
|
|
261
|
+
'# AI',
|
|
262
|
+
'QWEN.md',
|
|
263
|
+
'CLAUDE.md',
|
|
264
|
+
'.kilocode/',
|
|
265
|
+
'.kilocodemodes',
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
let currentContent = '';
|
|
269
|
+
if (existsSync(gitignorePath)) {
|
|
270
|
+
currentContent = readFileSync(gitignorePath, 'utf-8');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const existingLines = currentContent.split('\n').map(line => line.trim());
|
|
274
|
+
|
|
275
|
+
const newLines = linesToAdd.filter(line => line === '' || !existingLines.includes(line));
|
|
276
|
+
if (newLines.some(line => line !== '')) {
|
|
277
|
+
appendFileSync(gitignorePath, newLines.join('\n') + '\n');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Создаёт симлинки .kilocode.
|
|
283
|
+
*
|
|
284
|
+
* @param {string} projectRoot - Путь к корню проекта
|
|
285
|
+
* @param {boolean} force - Принудительное создание
|
|
286
|
+
* @returns {{ success: boolean, warning?: string }} Результат операции
|
|
287
|
+
*/
|
|
288
|
+
function createKilocodeSymlinks(projectRoot, force = false) {
|
|
289
|
+
const kilocodeDir = join(projectRoot, '.kilocode');
|
|
290
|
+
const skillsTarget = join(projectRoot, '.workflow', 'src', 'skills');
|
|
291
|
+
const skillsLink = join(kilocodeDir, 'skills');
|
|
292
|
+
|
|
293
|
+
ensureDir(kilocodeDir);
|
|
294
|
+
|
|
295
|
+
const isWindows = process.platform === 'win32';
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
// Remove existing link if exists
|
|
299
|
+
if (existsSync(skillsLink)) {
|
|
300
|
+
const stats = statSync(skillsLink);
|
|
301
|
+
if (stats.isSymbolicLink() || stats.isDirectory()) {
|
|
302
|
+
try {
|
|
303
|
+
if (isWindows) {
|
|
304
|
+
execSync(`rmdir "${skillsLink}"`);
|
|
305
|
+
} else {
|
|
306
|
+
unlinkSync(skillsLink);
|
|
307
|
+
}
|
|
308
|
+
} catch (e) {
|
|
309
|
+
// Ignore errors
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (isWindows) {
|
|
315
|
+
// Windows: use Junction Point
|
|
316
|
+
try {
|
|
317
|
+
execSync(`mklink /J "${skillsLink}" "${skillsTarget}"`);
|
|
318
|
+
return { success: true };
|
|
319
|
+
} catch (e) {
|
|
320
|
+
// Fallback: copy directory
|
|
321
|
+
copyDirRecursive(skillsTarget, skillsLink);
|
|
322
|
+
return {
|
|
323
|
+
success: true,
|
|
324
|
+
warning: 'Junction Point creation failed, copied files instead'
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
// Linux/macOS: use symlink
|
|
329
|
+
symlinkSync(skillsTarget, skillsLink);
|
|
330
|
+
return { success: true };
|
|
331
|
+
}
|
|
332
|
+
} catch (e) {
|
|
333
|
+
return {
|
|
334
|
+
success: false,
|
|
335
|
+
warning: `Failed to create symlink: ${e.message}`
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Инициализирует проект, создавая структуру .workflow/ и копируя файлы.
|
|
342
|
+
*
|
|
343
|
+
* @param {string} targetPath - Путь к целевому проекту (по умолчанию process.cwd())
|
|
344
|
+
* @param {object} options - Опции инициализации
|
|
345
|
+
* @param {boolean} options.force - Принудительная перезапись файлов
|
|
346
|
+
* @returns {object} Результат инициализации
|
|
347
|
+
*/
|
|
348
|
+
export function initProject(targetPath = process.cwd(), options = {}) {
|
|
349
|
+
const { force = false } = options;
|
|
350
|
+
const projectRoot = resolve(targetPath);
|
|
351
|
+
const workflowRoot = join(projectRoot, '.workflow');
|
|
352
|
+
const packageRoot = getPackageRoot();
|
|
353
|
+
|
|
354
|
+
const result = {
|
|
355
|
+
steps: [],
|
|
356
|
+
warnings: [],
|
|
357
|
+
errors: []
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// Step 1: Create .workflow/ structure (15 directories)
|
|
361
|
+
const directories = [
|
|
362
|
+
'tickets/backlog',
|
|
363
|
+
'tickets/ready',
|
|
364
|
+
'tickets/in-progress',
|
|
365
|
+
'tickets/blocked',
|
|
366
|
+
'tickets/review',
|
|
367
|
+
'tickets/done',
|
|
368
|
+
'plans/current',
|
|
369
|
+
'plans/archive',
|
|
370
|
+
'reports',
|
|
371
|
+
'logs',
|
|
372
|
+
'templates',
|
|
373
|
+
'src/skills'
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
for (const dir of directories) {
|
|
377
|
+
ensureDir(join(workflowRoot, dir));
|
|
378
|
+
}
|
|
379
|
+
result.steps.push('Created .workflow/ directory structure (15 directories)');
|
|
380
|
+
|
|
381
|
+
// Step 2: Ensure global dir and create skill junctions
|
|
382
|
+
const globalDir = getGlobalDir();
|
|
383
|
+
ensureGlobalDir(packageRoot);
|
|
384
|
+
const srcSkillsDest = join(workflowRoot, 'src', 'skills');
|
|
385
|
+
createSkillJunctions(globalDir, srcSkillsDest);
|
|
386
|
+
result.steps.push('Created skill junctions from global dir → .workflow/src/skills/');
|
|
387
|
+
|
|
388
|
+
// Step 3: Create script junction
|
|
389
|
+
const srcScriptsDest = join(workflowRoot, 'src', 'scripts');
|
|
390
|
+
createScriptJunction(globalDir, srcScriptsDest);
|
|
391
|
+
result.steps.push('Created script junction from global dir → .workflow/src/scripts/');
|
|
392
|
+
|
|
393
|
+
// Step 5: Copy templates (3 templates)
|
|
394
|
+
const templatesSrc = join(packageRoot, 'templates');
|
|
395
|
+
const templatesDest = join(workflowRoot, 'templates');
|
|
396
|
+
ensureDir(templatesDest);
|
|
397
|
+
|
|
398
|
+
const templateFiles = ['ticket-template.md', 'plan-template.md', 'report-template.md'];
|
|
399
|
+
for (const template of templateFiles) {
|
|
400
|
+
const srcPath = join(templatesSrc, template);
|
|
401
|
+
const destPath = join(templatesDest, template);
|
|
402
|
+
if (existsSync(srcPath)) {
|
|
403
|
+
copyFile(srcPath, destPath);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
result.steps.push('Copied 3 templates → .workflow/templates/');
|
|
407
|
+
|
|
408
|
+
// Step 6: Create config junction
|
|
409
|
+
const configDest = join(workflowRoot, 'config');
|
|
410
|
+
createConfigJunction(globalDir, configDest);
|
|
411
|
+
result.steps.push('Created config junction from global dir → .workflow/config/');
|
|
412
|
+
|
|
413
|
+
// Step 7: Create .kilocode symlinks
|
|
414
|
+
const symlinkResult = createKilocodeSymlinks(projectRoot, force);
|
|
415
|
+
if (symlinkResult.success) {
|
|
416
|
+
result.steps.push('Created .kilocode symlinks (Junction Point on Windows)');
|
|
417
|
+
if (symlinkResult.warning) {
|
|
418
|
+
result.warnings.push(symlinkResult.warning);
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
result.errors.push(symlinkResult.warning || 'Failed to create .kilocode symlinks');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Step 8: Generate CLAUDE.md, QWEN.md and .kilocodemodes
|
|
425
|
+
generateClaudeMd(workflowRoot, projectRoot, packageRoot);
|
|
426
|
+
generateQwenMd(workflowRoot, projectRoot, packageRoot);
|
|
427
|
+
generateKilocodemodes(projectRoot, packageRoot);
|
|
428
|
+
result.steps.push('Generated CLAUDE.md, QWEN.md and .kilocodemodes from agent-templates');
|
|
429
|
+
|
|
430
|
+
// Step 9: Update .gitignore
|
|
431
|
+
updateGitignore(projectRoot);
|
|
432
|
+
result.steps.push('Updated .gitignore with .workflow/logs/');
|
|
433
|
+
|
|
434
|
+
return result;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export default initProject;
|