workflow-ai 1.0.40 → 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.
Files changed (32) hide show
  1. package/agent-templates/CLAUDE.md.tpl +10 -1
  2. package/agent-templates/QWEN.md.tpl +18 -1
  3. package/package.json +3 -1
  4. package/src/cli.mjs +79 -1
  5. package/src/global-dir.mjs +90 -0
  6. package/src/init.mjs +15 -30
  7. package/src/junction-manager.mjs +184 -0
  8. package/src/runner.mjs +26 -7
  9. package/src/scripts/move-to-review.js +5 -1
  10. package/src/skills/coach/SKILL.md +12 -2
  11. package/src/skills/coach/workflows/analyze.md +12 -0
  12. package/src/skills/create-plan/SKILL.md +9 -0
  13. package/src/skills/decompose-gaps/SKILL.md +6 -0
  14. package/src/skills/decompose-plan/SKILL.md +52 -1
  15. package/src/skills/deep-research/README.md +50 -0
  16. package/src/skills/deep-research/SKILL.md +148 -0
  17. package/src/skills/deep-research/algorithms/source-scoring.md +63 -0
  18. package/src/skills/deep-research/algorithms/synthesis.md +67 -0
  19. package/src/skills/deep-research/knowledge/data-validation.md +44 -0
  20. package/src/skills/deep-research/knowledge/research-methodology.md +54 -0
  21. package/src/skills/deep-research/knowledge/source-evaluation.md +33 -0
  22. package/src/skills/deep-research/scripts/perplexity-research.js +315 -0
  23. package/src/skills/deep-research/templates/brief-summary.md +25 -0
  24. package/src/skills/deep-research/templates/research-report.md +76 -0
  25. package/src/skills/deep-research/workflows/benchmark.md +56 -0
  26. package/src/skills/deep-research/workflows/competitor.md +63 -0
  27. package/src/skills/deep-research/workflows/custom.md +45 -0
  28. package/src/skills/deep-research/workflows/market.md +64 -0
  29. package/src/skills/deep-research/workflows/technology.md +52 -0
  30. package/src/skills/deep-research/workflows/trend.md +51 -0
  31. package/src/skills/execute-task/SKILL.md +48 -2
  32. 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
- Счётчики ID и настройки в `.workflow/config/config.yaml`
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
- Счётчики ID и настройки в `.workflow/config/config.yaml`
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.40",
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 (17 directories)
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 (17 directories)');
351
-
352
- // Step 2: Copy skills recursively
353
- const srcSkillsSrc = join(packageRoot, 'src', 'skills');
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
- copyDirRecursive(srcSkillsSrc, srcSkillsDest);
356
- result.steps.push('Copied skills from getPackageRoot()/src/skills/ → .workflow/src/skills/');
357
-
358
- // Step 3: Copy scripts
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
- copyDirRecursive(srcScriptsSrc, srcScriptsDest);
362
- result.steps.push('Copied scripts from getPackageRoot()/src/scripts/ → .workflow/src/scripts/');
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
- if (this.fileGuard) {
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
- this.fileGuard = new FileGuard(protectedPatterns, projectRoot);
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 };
@@ -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 directly to done/ by the agent — treat as already done
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/` };
@@ -17,7 +17,7 @@ ticket_prefix: COACH
17
17
 
18
18
  **Ты делаешь:** создание новых скилов, аудит существующих, анализ завершённых планов и тикетов, поиск паттернов ошибок и недочётов, поиск лучших практик в интернете, обогащение knowledge/algorithms, рефакторинг воркфлоу, формирование рекомендаций.
19
19
 
20
- **Ты НЕ делаешь:** выполнение бизнес-тикетов других скилов, принятие решений за скил (только рекомендации), удаление скилов без подтверждения.
20
+ **Ты НЕ делаешь:** выполнение бизнес-тикетов других скилов, принятие решений за скил (только рекомендации), удаление скилов без подтверждения. Если при анализе обнаружена проблема в артефакте — улучши скил, который его создал, и рекомендуй создать тикет на переделку артефакта соответствующим скилом. Коуч правит **только** содержимое `.workflow/src/skills/`.
21
21
 
22
22
  ## Объекты работы
23
23
 
@@ -31,13 +31,23 @@ ticket_prefix: COACH
31
31
 
32
32
  ## Обязательный шаг: Бэклог коуча
33
33
 
34
+ **⚠️ КРИТИЧЕСКИ ВАЖНО: Любая работа коуча БЕЗ обновления бэклога считается незавершённой.**
35
+
34
36
  **ПЕРЕД ЛЮБОЙ работой** выполни:
35
37
 
36
38
  1. Прочитай `.workflow/coach-backlog.yaml` (если не существует — создай с пустыми секциями)
37
39
  2. Загрузи `knowledge/backlog-management.md` — правила ведения бэклога
38
40
  3. При анализе тикетов — **пропускай** те, что уже есть в `analyzed_tickets`
39
41
  4. При внесении правок — **проверяй** `applied_changes`, не предлагай уже сделанное
40
- 5. **После завершения** — обнови бэклог: добавь проанализированные тикеты и внесённые правки
42
+
43
+ **ПОСЛЕ ЛЮБОЙ работы (включая ad-hoc запросы без тикета)** выполни:
44
+
45
+ 5. Добавь каждый проанализированный тикет в `analyzed_tickets`
46
+ 6. Добавь каждую внесённую правку в `applied_changes` с описанием всех изменённых файлов
47
+ 7. Если был аудит — обнови `audited_skills`
48
+ 8. Обнови `last_updated`
49
+
50
+ **Это относится ко ВСЕМ формам работы:** формальные COACH-тикеты, ad-hoc запросы стейкхолдера, улучшения по собственной инициативе. Если правка внесена — она должна быть в бэклоге.
41
51
 
42
52
  ## Маршрутизация тикетов COACH-*
43
53
 
@@ -38,6 +38,18 @@
38
38
  - Какие знания отсутствуют и требуют дополнения?
39
39
  - Где агент «додумывает» вместо использования knowledge?
40
40
 
41
+ **⚠️ Проверка соответствия процесса (ОБЯЗАТЕЛЬНО для каждого тикета):**
42
+
43
+ Для каждого анализируемого тикета выполни сверку:
44
+
45
+ 1. Открой SKILL.md анализируемого скила
46
+ 2. Найди предписанные **инструменты** (скрипты, API, MCP-серверы) и **шаги workflow**
47
+ 3. В тикете найди секции «Agent used», «Что сделано», «Время выполнения»
48
+ 4. Сравни: предписанный инструмент == фактически использованный?
49
+ 5. Расхождение = **finding**, даже если результат тикета формально ✅ passed
50
+
51
+ Пример: скил предписывает `perplexity-research.js`, а агент использовал встроенный `web_search` — это нарушение workflow, даже если DoD выполнен. Хороший результат **не маскирует** нарушение процесса.
52
+
41
53
  ### 3. Gap-анализ
42
54
 
43
55
  Применить → `algorithms/gap-analysis.md`