workflow-ai 1.0.39 → 1.0.41
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 +10 -1
- package/agent-templates/QWEN.md.tpl +18 -1
- package/package.json +3 -1
- package/src/cli.mjs +79 -1
- package/src/global-dir.mjs +90 -0
- package/src/init.mjs +15 -30
- package/src/junction-manager.mjs +184 -0
- package/src/runner.mjs +26 -7
- package/src/scripts/check-conditions.js +7 -0
- package/src/scripts/move-to-ready.js +6 -0
- package/src/scripts/move-to-review.js +5 -1
- package/src/scripts/pick-next-task.js +67 -0
- package/src/skills/coach/SKILL.md +12 -2
- package/src/skills/coach/workflows/analyze.md +12 -0
- package/src/skills/create-plan/SKILL.md +9 -0
- package/src/skills/decompose-gaps/SKILL.md +6 -0
- package/src/skills/decompose-plan/SKILL.md +52 -1
- package/src/skills/deep-research/README.md +50 -0
- package/src/skills/deep-research/SKILL.md +148 -0
- package/src/skills/deep-research/algorithms/source-scoring.md +63 -0
- package/src/skills/deep-research/algorithms/synthesis.md +67 -0
- package/src/skills/deep-research/knowledge/data-validation.md +44 -0
- package/src/skills/deep-research/knowledge/research-methodology.md +54 -0
- package/src/skills/deep-research/knowledge/source-evaluation.md +33 -0
- package/src/skills/deep-research/scripts/perplexity-research.js +315 -0
- package/src/skills/deep-research/templates/brief-summary.md +25 -0
- package/src/skills/deep-research/templates/research-report.md +76 -0
- package/src/skills/deep-research/workflows/benchmark.md +56 -0
- package/src/skills/deep-research/workflows/competitor.md +63 -0
- package/src/skills/deep-research/workflows/custom.md +45 -0
- package/src/skills/deep-research/workflows/market.md +64 -0
- package/src/skills/deep-research/workflows/technology.md +52 -0
- package/src/skills/deep-research/workflows/trend.md +51 -0
- package/src/skills/execute-task/SKILL.md +48 -2
- package/src/skills/review-result/SKILL.md +329 -285
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
3. **Выполнение**: Бери задачи из `ready/`, выполняй, перемещай в `done/`
|
|
29
29
|
4. **Отчётность**: Создавай отчёты в `.workflow/reports/`
|
|
30
30
|
|
|
31
|
+
> **ВАЖНО:** В этой сессии только составляй планы. НЕ выполняй и НЕ декомпозируй их — выполнение и декомпозиция будут происходить позже через workflow автоматически.
|
|
32
|
+
|
|
31
33
|
## Шаблоны
|
|
32
34
|
|
|
33
35
|
- `.workflow/templates/ticket-template.md` — шаблон тикета
|
|
@@ -36,7 +38,14 @@
|
|
|
36
38
|
|
|
37
39
|
## Конфигурация
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
Настройки в `.workflow/config/config.yaml`
|
|
42
|
+
|
|
43
|
+
## Правила работы со скилами
|
|
44
|
+
|
|
45
|
+
> **⛔ ВАЖНО:** Любые изменения в скилах (создание, правка, аудит, улучшение) выполняются ТОЛЬКО через скил коуча (`.workflow/src/skills/coach/SKILL.md`). Не правь скилы напрямую — загрузи коуча и действуй по его воркфлоу.
|
|
40
46
|
|
|
41
47
|
## Правила написания кода
|
|
42
48
|
При написании кода использовать методологии TDD, SOLID, DRY
|
|
49
|
+
|
|
50
|
+
## Общие инструкции
|
|
51
|
+
Отвечай всегда на Русском языке
|
|
@@ -13,6 +13,14 @@
|
|
|
13
13
|
|
|
14
14
|
{{SKILLS_TABLE}}
|
|
15
15
|
|
|
16
|
+
### Скрипты (перемещение и выбор тикетов)
|
|
17
|
+
|
|
18
|
+
| Действие | Скрипт |
|
|
19
|
+
|----------|--------|
|
|
20
|
+
| Перемещение тикета | `node .workflow/src/scripts/move-ticket.js <id> <target>` |
|
|
21
|
+
| Выбор следующей задачи | `node .workflow/src/scripts/pick-next-task.js` |
|
|
22
|
+
| Перемещение готовых в ready | `node .workflow/src/scripts/move-to-ready.js` |
|
|
23
|
+
|
|
16
24
|
## Workflow
|
|
17
25
|
|
|
18
26
|
1. **Планирование**: Создай план в `.workflow/plans/current/`
|
|
@@ -20,6 +28,8 @@
|
|
|
20
28
|
3. **Выполнение**: Бери задачи из `ready/`, выполняй, перемещай в `done/`
|
|
21
29
|
4. **Отчётность**: Создавай отчёты в `.workflow/reports/`
|
|
22
30
|
|
|
31
|
+
> **ВАЖНО:** В этой сессии только составляй планы. НЕ выполняй и НЕ декомпозируй их — выполнение и декомпозиция будут происходить позже через workflow автоматически.
|
|
32
|
+
|
|
23
33
|
## Шаблоны
|
|
24
34
|
|
|
25
35
|
- `.workflow/templates/ticket-template.md` — шаблон тикета
|
|
@@ -28,7 +38,14 @@
|
|
|
28
38
|
|
|
29
39
|
## Конфигурация
|
|
30
40
|
|
|
31
|
-
|
|
41
|
+
Настройки в `.workflow/config/config.yaml`
|
|
42
|
+
|
|
43
|
+
## Правила работы со скилами
|
|
44
|
+
|
|
45
|
+
> **⛔ ВАЖНО:** Любые изменения в скилах (создание, правка, аудит, улучшение) выполняются ТОЛЬКО через скил коуча (`.workflow/src/skills/coach/SKILL.md`). Не правь скилы напрямую — загрузи коуча и действуй по его воркфлоу.
|
|
32
46
|
|
|
33
47
|
## Правила написания кода
|
|
34
48
|
При написании кода использовать методологии TDD, SOLID, DRY
|
|
49
|
+
|
|
50
|
+
## Общие инструкции
|
|
51
|
+
Отвечай всегда на Русском языке
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workflow-ai",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.41",
|
|
4
4
|
"description": "AI Agent Workflow Coordinator — kanban-based pipeline for AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
"src/init.mjs",
|
|
13
13
|
"src/runner.mjs",
|
|
14
14
|
"src/wf-loader.mjs",
|
|
15
|
+
"src/global-dir.mjs",
|
|
16
|
+
"src/junction-manager.mjs",
|
|
15
17
|
"src/lib/",
|
|
16
18
|
"src/scripts/",
|
|
17
19
|
"src/skills/",
|
package/src/cli.mjs
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
import { initProject } from './init.mjs';
|
|
4
4
|
import { runPipeline } from './runner.mjs';
|
|
5
5
|
import { readFileSync } from 'node:fs';
|
|
6
|
-
import { join, dirname } from 'node:path';
|
|
6
|
+
import { join, dirname, resolve } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { getGlobalDir, refreshGlobalDir, ensureGlobalDir } from './global-dir.mjs';
|
|
9
|
+
import { createSkillJunctions, createScriptHardlinks, ejectSkill, listSkillsWithStatus } from './junction-manager.mjs';
|
|
8
10
|
|
|
9
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
12
|
const pkgPath = join(__dirname, '..', 'package.json');
|
|
@@ -14,6 +16,9 @@ const HELP_TEXT = `workflow-ai v1.0.0
|
|
|
14
16
|
Usage:
|
|
15
17
|
workflow init [path] [--force] Initialize .workflow/ in target directory
|
|
16
18
|
workflow run [options] Run the AI pipeline
|
|
19
|
+
workflow update [path] Update global dir and recreate junctions/hardlinks
|
|
20
|
+
workflow eject <skill> [path] Eject a skill (copy from global to project)
|
|
21
|
+
workflow list [path] List skills with status (shared/ejected/project-only)
|
|
17
22
|
workflow help Show this help
|
|
18
23
|
workflow version Show version
|
|
19
24
|
|
|
@@ -76,6 +81,70 @@ async function runInit(args) {
|
|
|
76
81
|
}
|
|
77
82
|
}
|
|
78
83
|
|
|
84
|
+
function getPackageRoot() {
|
|
85
|
+
return resolve(__dirname, '..');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getWorkflowRoot(projectRoot) {
|
|
89
|
+
return join(projectRoot, '.workflow');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function runUpdate(args) {
|
|
93
|
+
const projectRoot = resolve(args._[0] || process.cwd());
|
|
94
|
+
const workflowRoot = getWorkflowRoot(projectRoot);
|
|
95
|
+
const packageRoot = getPackageRoot();
|
|
96
|
+
const globalDir = getGlobalDir();
|
|
97
|
+
|
|
98
|
+
refreshGlobalDir(packageRoot);
|
|
99
|
+
console.log('✅ Global dir updated (~/.workflow/)');
|
|
100
|
+
|
|
101
|
+
const skillsDir = join(workflowRoot, 'src', 'skills');
|
|
102
|
+
createSkillJunctions(globalDir, skillsDir);
|
|
103
|
+
console.log('✅ Skill junctions recreated');
|
|
104
|
+
|
|
105
|
+
const scriptsDir = join(workflowRoot, 'src', 'scripts');
|
|
106
|
+
createScriptHardlinks(globalDir, scriptsDir);
|
|
107
|
+
console.log('✅ Script hardlinks recreated');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function runEject(args) {
|
|
111
|
+
const skillName = args._[0];
|
|
112
|
+
if (!skillName) {
|
|
113
|
+
console.error('Error: skill name is required. Usage: workflow eject <skill>');
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
const projectRoot = resolve(args._[1] || process.cwd());
|
|
117
|
+
const workflowRoot = getWorkflowRoot(projectRoot);
|
|
118
|
+
const globalDir = getGlobalDir();
|
|
119
|
+
const skillsDir = join(workflowRoot, 'src', 'skills');
|
|
120
|
+
|
|
121
|
+
ejectSkill(skillName, globalDir, skillsDir);
|
|
122
|
+
console.log(`✅ Skill "${skillName}" ejected (copied to project)`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function runList(args) {
|
|
126
|
+
const projectRoot = resolve(args._[0] || process.cwd());
|
|
127
|
+
const workflowRoot = getWorkflowRoot(projectRoot);
|
|
128
|
+
const globalDir = getGlobalDir();
|
|
129
|
+
const skillsDir = join(workflowRoot, 'src', 'skills');
|
|
130
|
+
|
|
131
|
+
const skills = listSkillsWithStatus(globalDir, skillsDir);
|
|
132
|
+
|
|
133
|
+
if (skills.length === 0) {
|
|
134
|
+
console.log('No skills found.');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const maxName = Math.max(...skills.map(s => s.name.length), 4);
|
|
139
|
+
const maxStatus = Math.max(...skills.map(s => s.status.length), 6);
|
|
140
|
+
|
|
141
|
+
console.log(`${'Skill'.padEnd(maxName)} ${'Status'.padEnd(maxStatus)}`);
|
|
142
|
+
console.log(`${'─'.repeat(maxName)} ${'─'.repeat(maxStatus)}`);
|
|
143
|
+
for (const skill of skills) {
|
|
144
|
+
console.log(`${skill.name.padEnd(maxName)} ${skill.status.padEnd(maxStatus)}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
79
148
|
async function runRun(args) {
|
|
80
149
|
// Expose wf's node_modules to child ESM scripts via a custom loader
|
|
81
150
|
const loaderPath = join(__dirname, 'wf-loader.mjs');
|
|
@@ -109,6 +178,15 @@ export function run(argv) {
|
|
|
109
178
|
case 'run':
|
|
110
179
|
runRun(args);
|
|
111
180
|
break;
|
|
181
|
+
case 'update':
|
|
182
|
+
runUpdate(args);
|
|
183
|
+
break;
|
|
184
|
+
case 'eject':
|
|
185
|
+
runEject(args);
|
|
186
|
+
break;
|
|
187
|
+
case 'list':
|
|
188
|
+
runList(args);
|
|
189
|
+
break;
|
|
112
190
|
case 'help':
|
|
113
191
|
showHelp();
|
|
114
192
|
break;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync, rmSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
export function getGlobalDir() {
|
|
6
|
+
if (process.env.WORKFLOW_HOME) {
|
|
7
|
+
return process.env.WORKFLOW_HOME;
|
|
8
|
+
}
|
|
9
|
+
return join(homedir(), '.workflow');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getPackageVersion(packageRoot) {
|
|
13
|
+
const packageJsonPath = join(packageRoot, 'package.json');
|
|
14
|
+
if (!existsSync(packageJsonPath)) {
|
|
15
|
+
throw new Error(`package.json not found in ${packageRoot}`);
|
|
16
|
+
}
|
|
17
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
18
|
+
return pkg.version;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getGlobalVersion() {
|
|
22
|
+
const globalDir = getGlobalDir();
|
|
23
|
+
const versionFile = join(globalDir, '.version');
|
|
24
|
+
if (!existsSync(versionFile)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return readFileSync(versionFile, 'utf-8').trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function copyDirectory(src, dest) {
|
|
31
|
+
if (!existsSync(src)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
rmSync(dest, { recursive: true, force: true });
|
|
35
|
+
cpSync(src, dest, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function copySkillsAndScripts(packageRoot) {
|
|
39
|
+
const globalDir = getGlobalDir();
|
|
40
|
+
const srcSkills = join(packageRoot, 'src', 'skills');
|
|
41
|
+
const srcScripts = join(packageRoot, 'src', 'scripts');
|
|
42
|
+
const destSkills = join(globalDir, 'skills');
|
|
43
|
+
const destScripts = join(globalDir, 'scripts');
|
|
44
|
+
|
|
45
|
+
if (existsSync(srcSkills)) {
|
|
46
|
+
copyDirectory(srcSkills, destSkills);
|
|
47
|
+
}
|
|
48
|
+
if (existsSync(srcScripts)) {
|
|
49
|
+
copyDirectory(srcScripts, destScripts);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isGlobalDirStale(packageRoot) {
|
|
54
|
+
const globalDir = getGlobalDir();
|
|
55
|
+
if (!existsSync(globalDir)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
const globalVersion = getGlobalVersion();
|
|
59
|
+
if (globalVersion === null) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
const packageVersion = getPackageVersion(packageRoot);
|
|
63
|
+
return packageVersion !== globalVersion;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function ensureGlobalDir(packageRoot) {
|
|
67
|
+
const globalDir = getGlobalDir();
|
|
68
|
+
if (!existsSync(globalDir)) {
|
|
69
|
+
mkdirSync(globalDir, { recursive: true });
|
|
70
|
+
copySkillsAndScripts(packageRoot);
|
|
71
|
+
const version = getPackageVersion(packageRoot);
|
|
72
|
+
writeFileSync(join(globalDir, '.version'), version);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (isGlobalDirStale(packageRoot)) {
|
|
76
|
+
const version = getPackageVersion(packageRoot);
|
|
77
|
+
writeFileSync(join(globalDir, '.version'), version);
|
|
78
|
+
copySkillsAndScripts(packageRoot);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function refreshGlobalDir(packageRoot) {
|
|
83
|
+
const globalDir = getGlobalDir();
|
|
84
|
+
if (!existsSync(globalDir)) {
|
|
85
|
+
mkdirSync(globalDir, { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
const version = getPackageVersion(packageRoot);
|
|
88
|
+
writeFileSync(join(globalDir, '.version'), version);
|
|
89
|
+
copySkillsAndScripts(packageRoot);
|
|
90
|
+
}
|
package/src/init.mjs
CHANGED
|
@@ -2,6 +2,8 @@ import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, appen
|
|
|
2
2
|
import { join, resolve, dirname, basename } from 'node:path';
|
|
3
3
|
import { execSync } from 'node:child_process';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { getGlobalDir, ensureGlobalDir } from './global-dir.mjs';
|
|
6
|
+
import { createSkillJunctions, createScriptHardlinks } from './junction-manager.mjs';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Возвращает абсолютный путь к корню npm-пакета через import.meta.url.
|
|
@@ -325,7 +327,7 @@ export function initProject(targetPath = process.cwd(), options = {}) {
|
|
|
325
327
|
errors: []
|
|
326
328
|
};
|
|
327
329
|
|
|
328
|
-
// Step 1: Create .workflow/ structure (
|
|
330
|
+
// Step 1: Create .workflow/ structure (15 directories)
|
|
329
331
|
const directories = [
|
|
330
332
|
'tickets/backlog',
|
|
331
333
|
'tickets/ready',
|
|
@@ -339,43 +341,26 @@ export function initProject(targetPath = process.cwd(), options = {}) {
|
|
|
339
341
|
'logs',
|
|
340
342
|
'templates',
|
|
341
343
|
'config',
|
|
342
|
-
'src/skills'
|
|
343
|
-
'src/scripts',
|
|
344
|
-
'src/lib'
|
|
344
|
+
'src/skills'
|
|
345
345
|
];
|
|
346
346
|
|
|
347
347
|
for (const dir of directories) {
|
|
348
348
|
ensureDir(join(workflowRoot, dir));
|
|
349
349
|
}
|
|
350
|
-
result.steps.push('Created .workflow/ directory structure (
|
|
351
|
-
|
|
352
|
-
// Step 2:
|
|
353
|
-
const
|
|
350
|
+
result.steps.push('Created .workflow/ directory structure (15 directories)');
|
|
351
|
+
|
|
352
|
+
// Step 2: Ensure global dir and create skill junctions
|
|
353
|
+
const globalDir = getGlobalDir();
|
|
354
|
+
ensureGlobalDir(packageRoot);
|
|
354
355
|
const srcSkillsDest = join(workflowRoot, 'src', 'skills');
|
|
355
|
-
|
|
356
|
-
result.steps.push('
|
|
357
|
-
|
|
358
|
-
// Step 3:
|
|
359
|
-
const srcScriptsSrc = join(packageRoot, 'src', 'scripts');
|
|
356
|
+
createSkillJunctions(globalDir, srcSkillsDest);
|
|
357
|
+
result.steps.push('Created skill junctions from global dir → .workflow/src/skills/');
|
|
358
|
+
|
|
359
|
+
// Step 3: Create script hardlinks
|
|
360
360
|
const srcScriptsDest = join(workflowRoot, 'src', 'scripts');
|
|
361
|
-
|
|
362
|
-
result.steps.push('
|
|
363
|
-
|
|
364
|
-
// Step 4: Copy lib (utils.mjs + find-root.mjs)
|
|
365
|
-
const libSrc = join(packageRoot, 'src', 'lib');
|
|
366
|
-
const libDest = join(workflowRoot, 'src', 'lib');
|
|
367
|
-
ensureDir(libDest);
|
|
368
|
-
|
|
369
|
-
const libFiles = ['utils.mjs', 'find-root.mjs', 'js-yaml.mjs', 'logger.mjs'];
|
|
361
|
+
createScriptHardlinks(globalDir, srcScriptsDest);
|
|
362
|
+
result.steps.push('Created script hardlinks from global dir → .workflow/src/scripts/');
|
|
370
363
|
|
|
371
|
-
for (const file of libFiles) {
|
|
372
|
-
const fileSrc = join(libSrc, file);
|
|
373
|
-
if (existsSync(fileSrc)) {
|
|
374
|
-
copyFile(fileSrc, join(libDest, file));
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
result.steps.push('Copied lib files (utils.mjs, find-root.mjs, js-yaml.mjs) → .workflow/src/lib/');
|
|
378
|
-
|
|
379
364
|
// Step 5: Copy templates (3 templates)
|
|
380
365
|
const templatesSrc = join(packageRoot, 'templates');
|
|
381
366
|
const templatesDest = join(workflowRoot, 'templates');
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
symlinkSync,
|
|
9
|
+
linkSync,
|
|
10
|
+
cpSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
unlinkSync
|
|
14
|
+
} from 'node:fs';
|
|
15
|
+
import { join, basename } from 'node:path';
|
|
16
|
+
|
|
17
|
+
const isWindows = process.platform === 'win32';
|
|
18
|
+
|
|
19
|
+
export function createJunction(target, linkPath) {
|
|
20
|
+
if (!existsSync(target)) {
|
|
21
|
+
throw new Error(`Target does not exist: ${target}`);
|
|
22
|
+
}
|
|
23
|
+
const linkDir = join(linkPath, '..');
|
|
24
|
+
if (!existsSync(linkDir)) {
|
|
25
|
+
mkdirSync(linkDir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
rmSync(linkPath, { recursive: true, force: true });
|
|
28
|
+
|
|
29
|
+
if (isWindows) {
|
|
30
|
+
execSync(`mklink /J "${linkPath}" "${target}"`, { stdio: 'pipe' });
|
|
31
|
+
} else {
|
|
32
|
+
symlinkSync(target, linkPath, 'dir');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function removeJunction(linkPath) {
|
|
37
|
+
if (existsSync(linkPath)) {
|
|
38
|
+
rmSync(linkPath, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isJunction(path) {
|
|
43
|
+
if (!existsSync(path)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const stats = lstatSync(path);
|
|
48
|
+
return stats.isSymbolicLink();
|
|
49
|
+
} catch {
|
|
50
|
+
if (isWindows) {
|
|
51
|
+
try {
|
|
52
|
+
const output = execSync(`fsutil reparsepoint query "${path}"`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
53
|
+
return output.includes('Symbolic Link') || output.includes('Mount Point');
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createHardlink(target, linkPath) {
|
|
63
|
+
if (!existsSync(target)) {
|
|
64
|
+
throw new Error(`Target does not exist: ${target}`);
|
|
65
|
+
}
|
|
66
|
+
const linkDir = join(linkPath, '..');
|
|
67
|
+
if (!existsSync(linkDir)) {
|
|
68
|
+
mkdirSync(linkDir, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
rmSync(linkPath, { force: true });
|
|
71
|
+
|
|
72
|
+
if (isWindows) {
|
|
73
|
+
execSync(`mklink /H "${linkPath}" "${target}"`, { stdio: 'pipe' });
|
|
74
|
+
} else {
|
|
75
|
+
linkSync(target, linkPath);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function removeHardlink(linkPath) {
|
|
80
|
+
if (existsSync(linkPath)) {
|
|
81
|
+
unlinkSync(linkPath);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function createSkillJunctions(globalDir, projectSkillsDir) {
|
|
86
|
+
const globalSkillsDir = join(globalDir, 'skills');
|
|
87
|
+
if (!existsSync(globalSkillsDir)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!existsSync(projectSkillsDir)) {
|
|
91
|
+
mkdirSync(projectSkillsDir, { recursive: true });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const skills = readdirSync(globalSkillsDir, { withFileTypes: true });
|
|
95
|
+
for (const skill of skills) {
|
|
96
|
+
if (skill.isDirectory()) {
|
|
97
|
+
const skillName = skill.name;
|
|
98
|
+
const targetPath = join(globalSkillsDir, skillName);
|
|
99
|
+
const linkPath = join(projectSkillsDir, skillName);
|
|
100
|
+
createJunction(targetPath, linkPath);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function createScriptHardlinks(globalDir, projectScriptsDir) {
|
|
106
|
+
const globalScriptsDir = join(globalDir, 'scripts');
|
|
107
|
+
if (!existsSync(globalScriptsDir)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (!existsSync(projectScriptsDir)) {
|
|
111
|
+
mkdirSync(projectScriptsDir, { recursive: true });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const files = readdirSync(globalScriptsDir);
|
|
115
|
+
for (const file of files) {
|
|
116
|
+
const targetPath = join(globalScriptsDir, file);
|
|
117
|
+
const stats = lstatSync(targetPath);
|
|
118
|
+
if (stats.isFile()) {
|
|
119
|
+
const linkPath = join(projectScriptsDir, file);
|
|
120
|
+
createHardlink(targetPath, linkPath);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function ejectSkill(skillName, globalDir, projectSkillsDir) {
|
|
126
|
+
const globalSkillPath = join(globalDir, 'skills', skillName);
|
|
127
|
+
const projectSkillPath = join(projectSkillsDir, skillName);
|
|
128
|
+
|
|
129
|
+
if (!existsSync(globalSkillPath)) {
|
|
130
|
+
throw new Error(`Skill does not exist in global dir: ${skillName}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
removeJunction(projectSkillPath);
|
|
134
|
+
cpSync(globalSkillPath, projectSkillPath, { recursive: true });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function listSkillsWithStatus(globalDir, projectSkillsDir) {
|
|
138
|
+
const result = [];
|
|
139
|
+
const globalSkillsDir = join(globalDir, 'skills');
|
|
140
|
+
const projectSkillsDirFull = projectSkillsDir;
|
|
141
|
+
|
|
142
|
+
if (!existsSync(globalSkillsDir)) {
|
|
143
|
+
if (existsSync(projectSkillsDirFull)) {
|
|
144
|
+
const projectSkills = readdirSync(projectSkillsDirFull, { withFileTypes: true });
|
|
145
|
+
for (const skill of projectSkills) {
|
|
146
|
+
if (skill.isDirectory()) {
|
|
147
|
+
result.push({ name: skill.name, status: 'project-only' });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const globalSkills = readdirSync(globalSkillsDir, { withFileTypes: true });
|
|
155
|
+
const projectSkills = existsSync(projectSkillsDirFull)
|
|
156
|
+
? readdirSync(projectSkillsDirFull, { withFileTypes: true })
|
|
157
|
+
: [];
|
|
158
|
+
|
|
159
|
+
const projectSkillNames = new Set(projectSkills.map(s => s.name));
|
|
160
|
+
|
|
161
|
+
for (const skill of globalSkills) {
|
|
162
|
+
if (skill.isDirectory()) {
|
|
163
|
+
const skillName = skill.name;
|
|
164
|
+
const projectSkillPath = join(projectSkillsDirFull, skillName);
|
|
165
|
+
let status = 'shared';
|
|
166
|
+
|
|
167
|
+
if (projectSkillNames.has(skillName)) {
|
|
168
|
+
if (!isJunction(projectSkillPath)) {
|
|
169
|
+
status = 'ejected';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
result.push({ name: skillName, status });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const skill of projectSkills) {
|
|
178
|
+
if (skill.isDirectory() && !globalSkills.some(s => s.name === skill.name)) {
|
|
179
|
+
result.push({ name: skill.name, status: 'project-only' });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return result;
|
|
184
|
+
}
|
package/src/runner.mjs
CHANGED
|
@@ -543,13 +543,30 @@ class ResultParser {
|
|
|
543
543
|
// FileGuard — защита файлов от несанкционированного изменения агентами
|
|
544
544
|
// ============================================================================
|
|
545
545
|
class FileGuard {
|
|
546
|
-
constructor(patterns, projectRoot = process.cwd()) {
|
|
546
|
+
constructor(patterns, projectRoot = process.cwd(), trustedAgents = []) {
|
|
547
547
|
// Паттерны указаны относительно корня проекта
|
|
548
548
|
this.patterns = (patterns || []).map(p => p.replace(/\\/g, '/'));
|
|
549
549
|
this.enabled = this.patterns.length > 0;
|
|
550
550
|
this.snapshots = new Map();
|
|
551
551
|
// projectRoot — корневая директория проекта, относительно которой указаны паттерны
|
|
552
552
|
this.projectRoot = projectRoot;
|
|
553
|
+
// Доверенные агенты — для них FileGuard не откатывает изменения
|
|
554
|
+
this.trustedAgents = trustedAgents;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Проверяет, является ли агент доверенным (пропускает FileGuard)
|
|
559
|
+
* Поддерживает glob-паттерны: "script-*" соответствует "script-move", "script-pick" и т.д.
|
|
560
|
+
* @param {string} agentId - ID агента
|
|
561
|
+
* @returns {boolean}
|
|
562
|
+
*/
|
|
563
|
+
isTrusted(agentId) {
|
|
564
|
+
return this.trustedAgents.some(pattern => {
|
|
565
|
+
if (pattern.endsWith('*')) {
|
|
566
|
+
return agentId.startsWith(pattern.slice(0, -1));
|
|
567
|
+
}
|
|
568
|
+
return agentId === pattern;
|
|
569
|
+
});
|
|
553
570
|
}
|
|
554
571
|
|
|
555
572
|
/**
|
|
@@ -805,8 +822,9 @@ class StageExecutor {
|
|
|
805
822
|
console.log(` Skill: ${stage.skill}`);
|
|
806
823
|
}
|
|
807
824
|
|
|
808
|
-
// Снимаем snapshot защищённых файлов перед выполнением
|
|
809
|
-
|
|
825
|
+
// Снимаем snapshot защищённых файлов перед выполнением (кроме trusted agents)
|
|
826
|
+
const skipGuard = this.fileGuard && this.fileGuard.isTrusted(agentId);
|
|
827
|
+
if (this.fileGuard && !skipGuard) {
|
|
810
828
|
this.fileGuard.takeSnapshot();
|
|
811
829
|
}
|
|
812
830
|
|
|
@@ -818,8 +836,8 @@ class StageExecutor {
|
|
|
818
836
|
this.logger.stageComplete(stageId, result.status, result.exitCode);
|
|
819
837
|
}
|
|
820
838
|
|
|
821
|
-
// Проверяем и откатываем несанкционированные изменения
|
|
822
|
-
if (this.fileGuard) {
|
|
839
|
+
// Проверяем и откатываем несанкционированные изменения (кроме trusted agents)
|
|
840
|
+
if (this.fileGuard && !skipGuard) {
|
|
823
841
|
const violations = this.fileGuard.checkAndRollback();
|
|
824
842
|
if (violations.length > 0) {
|
|
825
843
|
result.violations = violations;
|
|
@@ -1100,7 +1118,8 @@ class PipelineRunner {
|
|
|
1100
1118
|
|
|
1101
1119
|
// Инициализация FileGuard для защиты файлов от изменений агентами
|
|
1102
1120
|
const protectedPatterns = this.pipeline.protected_files || [];
|
|
1103
|
-
|
|
1121
|
+
const trustedAgents = this.pipeline.trusted_agents || [];
|
|
1122
|
+
this.fileGuard = new FileGuard(protectedPatterns, projectRoot, trustedAgents);
|
|
1104
1123
|
this.projectRoot = projectRoot;
|
|
1105
1124
|
|
|
1106
1125
|
// Настройка graceful shutdown
|
|
@@ -1573,4 +1592,4 @@ async function runPipeline(argv = process.argv.slice(2)) {
|
|
|
1573
1592
|
}
|
|
1574
1593
|
|
|
1575
1594
|
// Export for use as ES module
|
|
1576
|
-
export { runPipeline, parseArgs, PipelineRunner };
|
|
1595
|
+
export { runPipeline, parseArgs, PipelineRunner, FileGuard };
|
|
@@ -144,6 +144,13 @@ function checkBacklog(planId) {
|
|
|
144
144
|
|
|
145
145
|
for (const ticket of tickets) {
|
|
146
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
|
+
|
|
147
154
|
const conditions = frontmatter.conditions || [];
|
|
148
155
|
const dependencies = frontmatter.dependencies || [];
|
|
149
156
|
|
|
@@ -51,6 +51,12 @@ function moveToReady(ticketId) {
|
|
|
51
51
|
const content = fs.readFileSync(sourcePath, 'utf8');
|
|
52
52
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
53
53
|
|
|
54
|
+
// Пропускаем тикеты, требующие ручного выполнения
|
|
55
|
+
if (frontmatter.type === 'human') {
|
|
56
|
+
console.log(`[INFO] ${ticketId}: type is 'human', skipping (requires manual execution)`);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
54
60
|
frontmatter.updated_at = new Date().toISOString();
|
|
55
61
|
|
|
56
62
|
const newContent = serializeFrontmatter(frontmatter) + body;
|
|
@@ -39,7 +39,11 @@ function moveToReview(ticketId) {
|
|
|
39
39
|
const targetPath = path.join(REVIEW_DIR, `${ticketId}.md`);
|
|
40
40
|
|
|
41
41
|
if (!fs.existsSync(sourcePath)) {
|
|
42
|
-
// Ticket may have been moved
|
|
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
|
+
}
|
|
43
47
|
const donePath = path.join(TICKETS_DIR, 'done', `${ticketId}.md`);
|
|
44
48
|
if (fs.existsSync(donePath)) {
|
|
45
49
|
return { status: 'skipped', ticket_id: ticketId, reason: `${ticketId} already in done/` };
|