@gian-tiaga/eda 0.8.5 → 1.0.0

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 (86) hide show
  1. package/MIGRATION.md +117 -0
  2. package/README.md +324 -71
  3. package/agents/eda-aim-executor/agent.json +15 -0
  4. package/agents/eda-aim-executor/prompt.md +61 -0
  5. package/agents/eda-aim-planner/agent.json +15 -0
  6. package/agents/eda-aim-planner/prompt.md +43 -0
  7. package/agents/eda-aim-verifier/agent.json +15 -0
  8. package/agents/eda-aim-verifier/prompt.md +57 -0
  9. package/agents/eda-commit-context/agent.json +15 -0
  10. package/agents/eda-commit-context/prompt.md +58 -0
  11. package/agents/eda-commit-executor/agent.json +15 -0
  12. package/agents/eda-commit-executor/prompt.md +87 -0
  13. package/agents/eda-merge-worktree-executor/agent.json +15 -0
  14. package/agents/eda-merge-worktree-executor/prompt.md +57 -0
  15. package/agents/eda-review-api/agent.json +15 -0
  16. package/agents/eda-review-api/prompt.md +34 -0
  17. package/agents/eda-review-architecture/agent.json +15 -0
  18. package/agents/eda-review-architecture/prompt.md +34 -0
  19. package/agents/eda-review-code-quality/agent.json +15 -0
  20. package/agents/eda-review-code-quality/prompt.md +34 -0
  21. package/agents/eda-review-correctness/agent.json +15 -0
  22. package/agents/eda-review-correctness/prompt.md +34 -0
  23. package/agents/eda-review-database/agent.json +15 -0
  24. package/agents/eda-review-database/prompt.md +34 -0
  25. package/agents/eda-review-documentation/agent.json +15 -0
  26. package/agents/eda-review-documentation/prompt.md +34 -0
  27. package/agents/eda-review-frontend/agent.json +15 -0
  28. package/agents/eda-review-frontend/prompt.md +34 -0
  29. package/agents/eda-review-performance/agent.json +15 -0
  30. package/agents/eda-review-performance/prompt.md +34 -0
  31. package/agents/eda-review-plan-alignment/agent.json +15 -0
  32. package/agents/eda-review-plan-alignment/prompt.md +34 -0
  33. package/agents/eda-review-previous-reviews/agent.json +15 -0
  34. package/agents/eda-review-previous-reviews/prompt.md +44 -0
  35. package/agents/eda-review-references/agent.json +15 -0
  36. package/agents/eda-review-references/prompt.md +34 -0
  37. package/agents/eda-review-rules/agent.json +15 -0
  38. package/agents/eda-review-rules/prompt.md +34 -0
  39. package/agents/eda-review-security/agent.json +15 -0
  40. package/agents/eda-review-security/prompt.md +34 -0
  41. package/agents/eda-review-tests/agent.json +15 -0
  42. package/agents/eda-review-tests/prompt.md +34 -0
  43. package/agents/eda-worktree-executor/agent.json +15 -0
  44. package/agents/eda-worktree-executor/prompt.md +49 -0
  45. package/bin/cli.js +4 -4
  46. package/lib/catalog.js +118 -0
  47. package/lib/install.js +782 -110
  48. package/lib/renderers/claude-agent.js +24 -0
  49. package/lib/renderers/claude-skill.js +11 -0
  50. package/lib/renderers/codex-agent.js +24 -0
  51. package/lib/renderers/codex-skill.js +3 -0
  52. package/package.json +5 -3
  53. package/skills/eda-aim/SKILL.md +212 -0
  54. package/skills/eda-commit/SKILL.md +124 -0
  55. package/skills/eda-commit/skill.json +6 -0
  56. package/skills/eda-discover-automations/SKILL.md +184 -0
  57. package/skills/{eda-explore.md → eda-explore/SKILL.md} +11 -8
  58. package/skills/{eda-fix.md → eda-fix/SKILL.md} +7 -6
  59. package/skills/{eda-fix-by-review.md → eda-fix-by-review/SKILL.md} +5 -4
  60. package/skills/{eda-manual-test.md → eda-manual-test/SKILL.md} +1 -1
  61. package/skills/eda-merge-worktree/SKILL.md +72 -0
  62. package/skills/eda-merge-worktree/skill.json +7 -0
  63. package/skills/eda-new-project/SKILL.md +163 -0
  64. package/skills/eda-orhestra/SKILL.md +146 -0
  65. package/skills/{eda-plan.md → eda-plan/SKILL.md} +39 -29
  66. package/skills/eda-plan-execute/SKILL.md +186 -0
  67. package/skills/{eda-plan-polish.md → eda-plan-polish/SKILL.md} +14 -13
  68. package/skills/eda-polish/SKILL.md +80 -0
  69. package/skills/eda-prepare-ai/SKILL.md +227 -0
  70. package/skills/eda-review/SKILL.md +212 -0
  71. package/skills/eda-send-review/SKILL.md +178 -0
  72. package/skills/eda-send-review/skill.json +7 -0
  73. package/skills/eda-worktree/SKILL.md +73 -0
  74. package/skills/eda-worktree/skill.json +7 -0
  75. package/skills/eda-automate.md +0 -185
  76. package/skills/eda-commit.md +0 -91
  77. package/skills/eda-docs.md +0 -123
  78. package/skills/eda-execute.md +0 -140
  79. package/skills/eda-merge-worktree.md +0 -116
  80. package/skills/eda-polish.md +0 -113
  81. package/skills/eda-review-check.md +0 -149
  82. package/skills/eda-review.md +0 -173
  83. package/skills/eda-send-review.md +0 -105
  84. package/skills/eda-start.md +0 -233
  85. package/skills/eda-worktree.md +0 -113
  86. /package/skills/{eda-roadmap.md → eda-roadmap/SKILL.md} +0 -0
package/lib/install.js CHANGED
@@ -4,10 +4,18 @@ import fs from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
 
7
+ import { listAgents, listSkills } from './catalog.js';
8
+ import { renderClaudeAgent } from './renderers/claude-agent.js';
9
+ import { renderClaudeSkill } from './renderers/claude-skill.js';
10
+ import { renderCodexAgent } from './renderers/codex-agent.js';
11
+ import { renderCodexSkill } from './renderers/codex-skill.js';
12
+
7
13
  const __filename = fileURLToPath(import.meta.url);
8
14
  const __dirname = path.dirname(__filename);
9
15
  const PACKAGE_ROOT = path.resolve(__dirname, '..');
10
16
  const SKILLS_SRC = path.join(PACKAGE_ROOT, 'skills');
17
+ const AGENTS_SRC = path.join(PACKAGE_ROOT, 'agents');
18
+ const PACKAGE_JSON_PATH = path.join(PACKAGE_ROOT, 'package.json');
11
19
  const SETTINGS_RELATIVE_PATH = 'docs/settings.yaml';
12
20
  const TARGET_CHOICES = [
13
21
  { value: 'claude', label: 'Claude Code', dir: '.claude/skills/' },
@@ -15,33 +23,260 @@ const TARGET_CHOICES = [
15
23
  ];
16
24
  const UPDATE_ALL_MAX_DEPTH = 2;
17
25
  const UPDATE_ALL_SKIP_DIRS = new Set(['.git', '.claude', '.codex', 'node_modules']);
18
- const RETIRED_SKILLS = ['eda-research'];
26
+ const RETIRED_SKILLS = [
27
+ 'eda-research',
28
+ 'eda-review-check',
29
+ 'eda-execute',
30
+ 'eda-automate',
31
+ 'eda-docs',
32
+ 'eda-start'
33
+ ];
34
+ const RETIRED_AGENTS = [];
35
+ const MANIFEST_FILE = 'eda-manifest.json';
36
+ const REVIEW_AGENT_DEFINITIONS = [
37
+ {
38
+ key: 'correctness',
39
+ description: 'Проверяет ошибки в логике, крайние случаи и обработку ошибок.',
40
+ auto: 'Эта базовая проверка по умолчанию запускается всегда.',
41
+ mode: 'always',
42
+ claude: 'sonnet',
43
+ codex: 'gpt-5.6-terra'
44
+ },
45
+ {
46
+ key: 'architecture',
47
+ description: 'Проверяет архитектурные границы, зависимости и разделение ответственности.',
48
+ auto: 'В auto запускается при изменении модулей, зависимостей, публичных контрактов или границ системы.',
49
+ mode: 'auto',
50
+ claude: 'opus',
51
+ codex: 'gpt-5.6-sol'
52
+ },
53
+ {
54
+ key: 'rules',
55
+ description: 'Проверяет соблюдение AGENTS.md, CLAUDE.md и docs/rules.md.',
56
+ auto: 'Эта базовая проверка по умолчанию запускается всегда.',
57
+ mode: 'always',
58
+ claude: 'haiku',
59
+ codex: 'gpt-5.6-luna'
60
+ },
61
+ {
62
+ key: 'references',
63
+ description: 'Проверяет соответствие применимым карточкам из docs/references/.',
64
+ auto: 'В auto запускается только когда для изменений выбраны применимые карточки.',
65
+ mode: 'auto',
66
+ claude: 'haiku',
67
+ codex: 'gpt-5.6-luna'
68
+ },
69
+ {
70
+ key: 'plan_alignment',
71
+ description: 'Проверяет, что реализация соответствует указанному или однозначно найденному плану.',
72
+ auto: 'В auto запускается только когда у ревью есть конкретный план.',
73
+ mode: 'auto',
74
+ claude: 'sonnet',
75
+ codex: 'gpt-5.6-terra'
76
+ },
77
+ {
78
+ key: 'code_quality',
79
+ description: 'Проверяет читаемость, сложность, дублирование и поддерживаемость кода.',
80
+ auto: 'Эта базовая проверка по умолчанию запускается всегда.',
81
+ mode: 'always',
82
+ claude: 'sonnet',
83
+ codex: 'gpt-5.6-terra'
84
+ },
85
+ {
86
+ key: 'tests',
87
+ description: 'Проверяет достаточность, корректность и устойчивость тестов.',
88
+ auto: 'Эта базовая проверка по умолчанию запускается всегда.',
89
+ mode: 'always',
90
+ claude: 'sonnet',
91
+ codex: 'gpt-5.6-terra'
92
+ },
93
+ {
94
+ key: 'security',
95
+ description: 'Проверяет уязвимости, права доступа, валидацию и работу с чувствительными данными.',
96
+ auto: 'В auto запускается для auth, permissions, внешнего ввода, файлов, сети, секретов и криптографии.',
97
+ mode: 'auto',
98
+ claude: 'opus',
99
+ codex: 'gpt-5.6-sol'
100
+ },
101
+ {
102
+ key: 'performance',
103
+ description: 'Проверяет запросы, циклы, память, конкурентность и другие риски производительности.',
104
+ auto: 'В auto запускается при изменении запросов, пакетной обработки, кешей, конкурентности или горячих путей.',
105
+ mode: 'auto',
106
+ claude: 'sonnet',
107
+ codex: 'gpt-5.6-terra'
108
+ },
109
+ {
110
+ key: 'frontend',
111
+ description: 'Проверяет фронтенд-код, UI, UX, адаптивность и состояния интерфейса.',
112
+ auto: 'В auto обязательно запускается при изменении компонентов, стилей, клиентских маршрутов или состояния UI.',
113
+ mode: 'auto',
114
+ claude: 'sonnet',
115
+ codex: 'gpt-5.6-terra'
116
+ },
117
+ {
118
+ key: 'api',
119
+ description: 'Проверяет API-контракты, совместимость, валидацию и обработку ошибок.',
120
+ auto: 'В auto запускается при изменении маршрутов, handlers, DTO, схем API, GraphQL или RPC.',
121
+ mode: 'auto',
122
+ claude: 'sonnet',
123
+ codex: 'gpt-5.6-terra'
124
+ },
125
+ {
126
+ key: 'database',
127
+ description: 'Проверяет схемы, миграции, запросы, индексы и целостность данных.',
128
+ auto: 'В auto запускается при изменении миграций, моделей хранения, SQL, схем или транзакций.',
129
+ mode: 'auto',
130
+ claude: 'opus',
131
+ codex: 'gpt-5.6-sol'
132
+ },
133
+ {
134
+ key: 'documentation',
135
+ description: 'Проверяет, обновлена ли документация для изменившегося пользовательского поведения.',
136
+ auto: 'В auto запускается при изменении публичного API, CLI, настроек или документируемого поведения.',
137
+ mode: 'auto',
138
+ claude: 'haiku',
139
+ codex: 'gpt-5.6-luna'
140
+ },
141
+ {
142
+ key: 'previous_reviews',
143
+ description: 'Перепроверяет незакрытые замечания предыдущих ревью для удалённого PR или MR.',
144
+ auto: 'В auto запускается только для ссылки на PR/MR или другой цели с доступными удалёнными обсуждениями.',
145
+ mode: 'auto',
146
+ claude: 'haiku',
147
+ codex: 'gpt-5.6-luna'
148
+ }
149
+ ];
150
+ const DEFAULT_REVIEW_AGENTS = Object.fromEntries(REVIEW_AGENT_DEFINITIONS.map(agent => [
151
+ agent.key,
152
+ {
153
+ mode: agent.mode,
154
+ model: {
155
+ claude: agent.claude,
156
+ codex: agent.codex
157
+ }
158
+ }
159
+ ]));
160
+ const ORHESTRA_STEP_DEFINITIONS = [
161
+ {
162
+ id: 'plan',
163
+ skill: 'eda-plan',
164
+ name: 'Планирование через eda-plan',
165
+ enabled: true,
166
+ args: ''
167
+ },
168
+ {
169
+ id: 'execute',
170
+ skill: 'eda-plan-execute',
171
+ name: 'Управляемое выполнение плана через eda-plan-execute',
172
+ enabled: true,
173
+ args: ''
174
+ },
175
+ {
176
+ id: 'polish',
177
+ skill: 'eda-polish',
178
+ name: 'Ревью и исправления через eda-polish',
179
+ enabled: true,
180
+ args: 'limit 5'
181
+ },
182
+ {
183
+ id: 'manual-test',
184
+ skill: 'eda-manual-test',
185
+ name: 'Ручная проверка через eda-manual-test',
186
+ enabled: true,
187
+ args: '',
188
+ onFailure: {
189
+ skill: 'eda-fix',
190
+ args: '',
191
+ then: ['polish', 'manual-test'],
192
+ maxCycles: 5
193
+ }
194
+ }
195
+ ];
196
+ const DEFAULT_ORHESTRA_STEPS = ORHESTRA_STEP_DEFINITIONS.map(({ name, ...step }) => step);
19
197
  const DEFAULT_SETTINGS = {
20
- strict: false,
21
- planSize: 'normal',
22
- decisionMode: 'recommend_and_ask',
23
- testStrategy: 'ask_each_time',
24
- loggingStrategy: 'ask_each_time',
25
- includePlans: false,
26
- includeCodeQuality: true
198
+ orhestra: {
199
+ mode: 'automatic',
200
+ steps: DEFAULT_ORHESTRA_STEPS
201
+ },
202
+ aim: {
203
+ mode: 'automatic'
204
+ },
205
+ explore: {
206
+ strict: false,
207
+ decisionMode: 'recommend_and_ask'
208
+ },
209
+ plan: {
210
+ strict: false,
211
+ size: 'normal',
212
+ decisionMode: 'recommend_and_ask',
213
+ testStrategy: 'ask_each_time',
214
+ loggingStrategy: 'ask_each_time'
215
+ },
216
+ planPolish: {
217
+ strict: false
218
+ },
219
+ review: {
220
+ agents: DEFAULT_REVIEW_AGENTS
221
+ },
222
+ sendReview: {
223
+ closePreviousReviews: false
224
+ },
225
+ discoverAutomations: {
226
+ includePlans: false
227
+ }
27
228
  };
28
229
  const SETTINGS_CHOICES = [
29
230
  {
30
- value: 'strict',
31
- name: 'Strict по умолчанию для explore / plan / review / review-check',
32
- checked: DEFAULT_SETTINGS.strict
231
+ value: 'exploreStrict',
232
+ name: 'Strict по умолчанию для eda-explore',
233
+ checked: DEFAULT_SETTINGS.explore.strict
234
+ },
235
+ {
236
+ value: 'planStrict',
237
+ name: 'Strict по умолчанию для eda-plan',
238
+ checked: DEFAULT_SETTINGS.plan.strict
33
239
  },
34
240
  {
35
- value: 'includePlans',
36
- name: 'Анализировать планы в eda-automate по умолчанию',
37
- checked: DEFAULT_SETTINGS.includePlans
241
+ value: 'planPolishStrict',
242
+ name: 'Strict по умолчанию для eda-plan-polish',
243
+ checked: DEFAULT_SETTINGS.planPolish.strict
38
244
  },
39
245
  {
40
- value: 'includeCodeQuality',
41
- name: 'Проверять качество кода в eda-review / eda-review-check',
42
- checked: DEFAULT_SETTINGS.includeCodeQuality
246
+ value: 'sendReviewClosePreviousReviews',
247
+ name: 'Закрывать предыдущие ревью eda-send-review',
248
+ checked: DEFAULT_SETTINGS.sendReview.closePreviousReviews
249
+ },
250
+ {
251
+ value: 'discoverAutomationsIncludePlans',
252
+ name: 'Анализировать планы в eda-discover-automations по умолчанию',
253
+ checked: DEFAULT_SETTINGS.discoverAutomations.includePlans
43
254
  }
44
255
  ];
256
+ const ORHESTRA_MODE_CHOICES = [
257
+ {
258
+ value: 'automatic',
259
+ name: 'Автоматически пройти план, выполнение, ревью и ручные тесты'
260
+ },
261
+ {
262
+ value: 'manual',
263
+ name: 'Писать план вместе с человеком и передавать ему вопросы'
264
+ }
265
+ ];
266
+ const AIM_MODE_CHOICES = [
267
+ {
268
+ value: 'automatic',
269
+ name: 'Автоматически отвечать на безопасные рабочие вопросы'
270
+ },
271
+ {
272
+ value: 'manual',
273
+ name: 'Передавать рабочие вопросы человеку'
274
+ }
275
+ ];
276
+ const ORHESTRA_POLISH_LIMIT_CHOICES = [1, 2, 3, 5, 10].map(value => ({
277
+ value,
278
+ name: `${value} ${value === 1 ? 'итерация' : value < 5 ? 'итерации' : 'итераций'}`
279
+ }));
45
280
  const PLAN_SIZE_CHOICES = [
46
281
  {
47
282
  value: 'normal',
@@ -102,6 +337,21 @@ const LOGGING_STRATEGY_CHOICES = [
102
337
  name: 'Спрашивать стратегию логирования каждый раз'
103
338
  }
104
339
  ];
340
+ const REVIEW_MODE_CHOICES = [
341
+ { value: 'always', name: 'always — запускать при каждом ревью' },
342
+ { value: 'auto', name: 'auto — запускать только когда проверка применима' },
343
+ { value: 'off', name: 'off — отключить проверку' }
344
+ ];
345
+ const CLAUDE_REVIEW_MODEL_CHOICES = [
346
+ { value: 'haiku', name: 'haiku — быстрая проверка' },
347
+ { value: 'sonnet', name: 'sonnet — сильная проверка кода' },
348
+ { value: 'opus', name: 'opus — максимальная глубина' }
349
+ ];
350
+ const CODEX_REVIEW_MODEL_CHOICES = [
351
+ { value: 'gpt-5.6-luna', name: 'gpt-5.6-luna — быстрая проверка' },
352
+ { value: 'gpt-5.6-terra', name: 'gpt-5.6-terra — сильная проверка кода' },
353
+ { value: 'gpt-5.6-sol', name: 'gpt-5.6-sol — максимальная глубина' }
354
+ ];
105
355
 
106
356
  export async function init({ cwd, input = process.stdin, output = process.stdout }) {
107
357
  const targets = await askTargets({ input, output });
@@ -110,18 +360,18 @@ export async function init({ cwd, input = process.stdin, output = process.stdout
110
360
  return;
111
361
  }
112
362
  await ensureSettings(cwd, { input, output });
113
- await syncSkills(cwd, targets, output, { action: 'install' });
363
+ await syncPackage(cwd, targets, output, { action: 'install' });
114
364
  }
115
365
 
116
366
  export async function update({ cwd, input = process.stdin, output = process.stdout }) {
117
367
  const targets = await detectTargets(cwd);
118
368
  if (targets.length === 0) {
119
- output.write('Не нашёл установленных скилов в этом проекте. Запусти `eda init`.\n');
369
+ output.write('Не нашёл установленного пакета eda в этом проекте. Запусти `eda init`.\n');
120
370
  return;
121
371
  }
122
372
  output.write(`Найдены установленные среды: ${targets.join(', ')}\n`);
123
373
  await ensureSettings(cwd, { input, output });
124
- await syncSkills(cwd, targets, output, { action: 'update' });
374
+ await syncPackage(cwd, targets, output, { action: 'update' });
125
375
  }
126
376
 
127
377
  export async function updateAll({
@@ -139,11 +389,11 @@ export async function updateAll({
139
389
  throw new Error(`Директория не найдена: ${rootDir}`);
140
390
  }
141
391
 
142
- output.write(`Ищу проекты с установленными скилами в ${rootDir} (глубина ${maxDepth}).\n`);
392
+ output.write(`Ищу проекты с установленным пакетом eda в ${rootDir} (глубина ${maxDepth}).\n`);
143
393
 
144
394
  const projects = await findInstalledProjects(rootDir, maxDepth);
145
395
  if (projects.length === 0) {
146
- output.write('Не нашёл проектов с установленными скилами.\n');
396
+ output.write('Не нашёл проектов с установленным пакетом eda.\n');
147
397
  return {
148
398
  root: rootDir,
149
399
  maxDepth,
@@ -165,7 +415,7 @@ export async function updateAll({
165
415
  output.write(`\n=== ${projectLabel} ===\n`);
166
416
 
167
417
  try {
168
- const targets = await detectTargets(projectDir);
418
+ const targets = await detectInstalledTargets(projectDir);
169
419
  if (targets.length === 0) {
170
420
  output.write('Пропускаю: установленные среды исчезли во время обхода.\n');
171
421
  skippedProjects.push(projectDir);
@@ -173,14 +423,15 @@ export async function updateAll({
173
423
  }
174
424
 
175
425
  output.write(`Найдены установленные среды: ${targets.join(', ')}\n`);
176
- const result = await syncSkills(projectDir, targets, output, {
426
+ const result = await syncPackage(projectDir, targets, output, {
177
427
  action: 'update',
178
428
  writeDone: false
179
429
  });
180
430
  updatedProjects.push({
181
431
  path: projectDir,
182
432
  targets,
183
- changedSkills: result.changedSkills
433
+ changedSkills: result.changedSkills,
434
+ changedAgents: result.changedAgents
184
435
  });
185
436
  } catch (err) {
186
437
  failedProjects.push({ path: projectDir, error: err });
@@ -213,7 +464,7 @@ export async function askTargets({ input = process.stdin, output = process.stdou
213
464
  }
214
465
 
215
466
  return checkbox({
216
- message: 'Куда устанавливать скилы?',
467
+ message: 'Куда устанавливать пакет eda?',
217
468
  instructions: 'Стрелки — выбрать, Space — отметить, Enter — продолжить',
218
469
  choices: TARGET_CHOICES.map(choice => ({
219
470
  name: `${choice.label} (${choice.dir})`,
@@ -229,7 +480,7 @@ export async function askTargets({ input = process.stdin, output = process.stdou
229
480
  export async function askSettings({ input = process.stdin, output = process.stdout } = {}) {
230
481
  if (!input.isTTY || !output.isTTY) {
231
482
  output.write(`Нет интерактивного терминала — создаю ${SETTINGS_RELATIVE_PATH} с настройками по умолчанию.\n`);
232
- return { ...DEFAULT_SETTINGS };
483
+ return structuredClone(DEFAULT_SETTINGS);
233
484
  }
234
485
 
235
486
  const selected = await checkbox({
@@ -241,19 +492,76 @@ export async function askSettings({ input = process.stdin, output = process.stdo
241
492
  output
242
493
  });
243
494
 
495
+ const orhestraMode = await select({
496
+ message: 'Как eda-orhestra должен вести полный цикл по умолчанию?',
497
+ choices: ORHESTRA_MODE_CHOICES,
498
+ default: DEFAULT_SETTINGS.orhestra.mode
499
+ }, {
500
+ input,
501
+ output
502
+ });
503
+
504
+ const enabledOrhestraSteps = await checkbox({
505
+ message: 'Какие этапы eda-orhestra включить по умолчанию?',
506
+ instructions: 'Порядок можно изменить вручную в docs/settings.yaml',
507
+ choices: ORHESTRA_STEP_DEFINITIONS.map(step => ({
508
+ name: step.name,
509
+ value: step.skill,
510
+ checked: step.enabled
511
+ }))
512
+ }, {
513
+ input,
514
+ output
515
+ });
516
+
517
+ const polishLimit = enabledOrhestraSteps.includes('eda-polish')
518
+ ? await select({
519
+ message: 'Сколько итераций разрешить eda-polish внутри eda-orhestra?',
520
+ choices: ORHESTRA_POLISH_LIMIT_CHOICES,
521
+ default: 5
522
+ }, {
523
+ input,
524
+ output
525
+ })
526
+ : 5;
527
+
528
+ const orhestraSteps = DEFAULT_ORHESTRA_STEPS.map(step => ({
529
+ ...step,
530
+ enabled: enabledOrhestraSteps.includes(step.skill),
531
+ ...(step.skill === 'eda-polish' ? { args: `limit ${polishLimit}` } : {})
532
+ }));
533
+
534
+ const aimMode = await select({
535
+ message: 'Как eda-aim должен отвечать на рабочие вопросы по умолчанию?',
536
+ choices: AIM_MODE_CHOICES,
537
+ default: DEFAULT_SETTINGS.aim.mode
538
+ }, {
539
+ input,
540
+ output
541
+ });
542
+
543
+ const exploreDecisionMode = await select({
544
+ message: 'Как принимать важные решения в eda-explore?',
545
+ choices: DECISION_MODE_CHOICES,
546
+ default: DEFAULT_SETTINGS.explore.decisionMode
547
+ }, {
548
+ input,
549
+ output
550
+ });
551
+
244
552
  const planSize = await select({
245
553
  message: 'Какой размер плана eda-plan использовать по умолчанию?',
246
554
  choices: PLAN_SIZE_CHOICES,
247
- default: DEFAULT_SETTINGS.planSize
555
+ default: DEFAULT_SETTINGS.plan.size
248
556
  }, {
249
557
  input,
250
558
  output
251
559
  });
252
560
 
253
- const decisionMode = await select({
254
- message: 'Как принимать важные решения в eda-explore и eda-plan?',
561
+ const planDecisionMode = await select({
562
+ message: 'Как принимать важные решения в eda-plan?',
255
563
  choices: DECISION_MODE_CHOICES,
256
- default: DEFAULT_SETTINGS.decisionMode
564
+ default: DEFAULT_SETTINGS.plan.decisionMode
257
565
  }, {
258
566
  input,
259
567
  output
@@ -262,7 +570,7 @@ export async function askSettings({ input = process.stdin, output = process.stdo
262
570
  const testStrategy = await select({
263
571
  message: 'Какую стратегию тестов eda-plan использовать по умолчанию?',
264
572
  choices: TEST_STRATEGY_CHOICES,
265
- default: DEFAULT_SETTINGS.testStrategy
573
+ default: DEFAULT_SETTINGS.plan.testStrategy
266
574
  }, {
267
575
  input,
268
576
  output
@@ -271,62 +579,186 @@ export async function askSettings({ input = process.stdin, output = process.stdo
271
579
  const loggingStrategy = await select({
272
580
  message: 'Какую стратегию логирования eda-plan использовать по умолчанию?',
273
581
  choices: LOGGING_STRATEGY_CHOICES,
274
- default: DEFAULT_SETTINGS.loggingStrategy
582
+ default: DEFAULT_SETTINGS.plan.loggingStrategy
275
583
  }, {
276
584
  input,
277
585
  output
278
586
  });
279
587
 
588
+ const reviewAgents = await askReviewAgentSettings({ input, output });
589
+
280
590
  return {
281
- strict: selected.includes('strict'),
282
- planSize,
283
- decisionMode,
284
- testStrategy,
285
- loggingStrategy,
286
- includePlans: selected.includes('includePlans'),
287
- includeCodeQuality: selected.includes('includeCodeQuality')
591
+ orhestra: {
592
+ mode: orhestraMode,
593
+ steps: orhestraSteps
594
+ },
595
+ aim: {
596
+ mode: aimMode
597
+ },
598
+ explore: {
599
+ strict: selected.includes('exploreStrict'),
600
+ decisionMode: exploreDecisionMode
601
+ },
602
+ plan: {
603
+ strict: selected.includes('planStrict'),
604
+ size: planSize,
605
+ decisionMode: planDecisionMode,
606
+ testStrategy,
607
+ loggingStrategy
608
+ },
609
+ planPolish: {
610
+ strict: selected.includes('planPolishStrict')
611
+ },
612
+ review: {
613
+ agents: reviewAgents
614
+ },
615
+ sendReview: {
616
+ closePreviousReviews: selected.includes('sendReviewClosePreviousReviews')
617
+ },
618
+ discoverAutomations: {
619
+ includePlans: selected.includes('discoverAutomationsIncludePlans')
620
+ }
288
621
  };
289
622
  }
290
623
 
624
+ export async function askReviewAgentSettings({
625
+ input = process.stdin,
626
+ output = process.stdout,
627
+ selectPrompt = select
628
+ } = {}) {
629
+ const reviewAgents = {};
630
+ for (const agent of REVIEW_AGENT_DEFINITIONS) {
631
+ const mode = await selectPrompt({
632
+ message: `Когда запускать review-проверку ${agent.key}?`,
633
+ choices: REVIEW_MODE_CHOICES,
634
+ default: agent.mode
635
+ }, {
636
+ input,
637
+ output
638
+ });
639
+
640
+ let claude = agent.claude;
641
+ let codex = agent.codex;
642
+ if (mode !== 'off') {
643
+ claude = await selectPrompt({
644
+ message: `Какой моделью Claude проверять ${agent.key}?`,
645
+ choices: CLAUDE_REVIEW_MODEL_CHOICES,
646
+ default: agent.claude
647
+ }, {
648
+ input,
649
+ output
650
+ });
651
+ codex = await selectPrompt({
652
+ message: `Какой моделью Codex проверять ${agent.key}?`,
653
+ choices: CODEX_REVIEW_MODEL_CHOICES,
654
+ default: agent.codex
655
+ }, {
656
+ input,
657
+ output
658
+ });
659
+ }
660
+ reviewAgents[agent.key] = { mode, model: { claude, codex } };
661
+ }
662
+ return reviewAgents;
663
+ }
664
+
291
665
  async function detectTargets(cwd) {
292
666
  const targets = [];
293
- if (await dirExists(path.join(cwd, '.claude/skills'))) targets.push('claude');
294
- if (await dirExists(path.join(cwd, '.codex/skills'))) targets.push('codex');
667
+ if (await targetStructureExists(cwd, 'claude')) targets.push('claude');
668
+ if (await targetStructureExists(cwd, 'codex')) targets.push('codex');
669
+ return targets;
670
+ }
671
+
672
+ // При массовом обходе среда считается установленной по eda-скилу, eda-агенту
673
+ // или manifest владения. Одних пустых/чужих каталогов skills и agents недостаточно.
674
+ async function detectInstalledTargets(cwd) {
675
+ const targets = [];
676
+ if (await hasInstalledPackage(cwd, 'claude')) targets.push('claude');
677
+ if (await hasInstalledPackage(cwd, 'codex')) targets.push('codex');
295
678
  return targets;
296
679
  }
297
680
 
298
- async function syncSkills(cwd, targets, output = process.stdout, { action = 'update', writeDone = true } = {}) {
299
- const skills = await listSkills();
681
+ async function targetStructureExists(cwd, target) {
682
+ const root = path.join(cwd, `.${target}`);
683
+ return await dirExists(path.join(root, 'skills'))
684
+ || await dirExists(path.join(root, 'agents'))
685
+ || await fileExists(path.join(root, MANIFEST_FILE));
686
+ }
687
+
688
+ async function hasInstalledPackage(cwd, target) {
689
+ const root = path.join(cwd, `.${target}`);
690
+ const agentExtension = target === 'claude' ? '.md' : '.toml';
691
+ return await fileExists(path.join(root, MANIFEST_FILE))
692
+ || await hasInstalledSkill(path.join(root, 'skills'))
693
+ || await hasInstalledAgent(path.join(root, 'agents'), agentExtension);
694
+ }
695
+
696
+ async function hasInstalledSkill(skillsDir) {
697
+ let entries;
698
+ try {
699
+ entries = await fs.readdir(skillsDir, { withFileTypes: true });
700
+ } catch (err) {
701
+ if (['ENOENT', 'ENOTDIR', 'EACCES', 'EPERM'].includes(err?.code)) return false;
702
+ throw err;
703
+ }
704
+ // eda-скилы лежат как папка eda-* (новый layout) или файл eda-*.md (старый Codex layout).
705
+ return entries.some(entry => entry.name.startsWith('eda-'));
706
+ }
707
+
708
+ async function hasInstalledAgent(agentsDir, extension) {
709
+ let entries;
710
+ try {
711
+ entries = await fs.readdir(agentsDir, { withFileTypes: true });
712
+ } catch (err) {
713
+ if (['ENOENT', 'ENOTDIR', 'EACCES', 'EPERM'].includes(err?.code)) return false;
714
+ throw err;
715
+ }
716
+ return entries.some(entry => entry.isFile() && entry.name.startsWith('eda-') && entry.name.endsWith(extension));
717
+ }
718
+
719
+ async function syncPackage(cwd, targets, output = process.stdout, { action = 'update', writeDone = true } = {}) {
720
+ const skills = await listSkills(SKILLS_SRC);
300
721
  if (skills.length === 0) {
301
722
  throw new Error(`В пакете нет скилов (искал в ${SKILLS_SRC}).`);
302
723
  }
724
+ const agents = await listAgents(AGENTS_SRC);
725
+ const packageVersion = JSON.parse(await fs.readFile(PACKAGE_JSON_PATH, 'utf8')).version;
303
726
  output.write(`Скилы для установки: ${skills.map(s => s.name).join(', ')}\n`);
727
+ if (agents.length > 0) {
728
+ output.write(`Агенты для установки: ${agents.map(agent => agent.name).join(', ')}\n`);
729
+ }
304
730
 
305
731
  const changedSkills = new Set();
732
+ const changedAgents = new Set();
306
733
  if (targets.includes('claude')) {
307
- const result = await installToClaude(cwd, skills, output);
734
+ const result = await installTarget(cwd, 'claude', skills, agents, packageVersion, output);
308
735
  for (const skillName of result.changedSkills) changedSkills.add(skillName);
736
+ for (const agentName of result.changedAgents) changedAgents.add(agentName);
309
737
  }
310
738
  if (targets.includes('codex')) {
311
- const result = await installToCodex(cwd, skills, output);
739
+ const result = await installTarget(cwd, 'codex', skills, agents, packageVersion, output);
312
740
  for (const skillName of result.changedSkills) changedSkills.add(skillName);
741
+ for (const agentName of result.changedAgents) changedAgents.add(agentName);
313
742
  }
314
- await removeRetiredSkills(cwd, targets);
315
743
 
316
744
  const actionLabel = action === 'install' ? 'Установлено' : 'Обновлено';
317
745
  const changedSkillNames = skills
318
746
  .map(skill => skill.name)
319
747
  .filter(skillName => changedSkills.has(skillName));
748
+ const changedAgentNames = agents
749
+ .map(agent => agent.name)
750
+ .filter(agentName => changedAgents.has(agentName));
320
751
  output.write(formatChangedSkills(actionLabel, changedSkillNames));
752
+ output.write(formatChangedAgents(actionLabel, changedAgentNames));
321
753
  if (writeDone) output.write('\nГотово.\n');
322
- return { changedSkills: changedSkillNames };
754
+ return { changedSkills: changedSkillNames, changedAgents: changedAgentNames };
323
755
  }
324
756
 
325
757
  async function findInstalledProjects(rootDir, maxDepth) {
326
758
  const projects = [];
327
759
 
328
760
  async function walk(dir, depth) {
329
- const targets = await detectTargets(dir);
761
+ const targets = await detectInstalledTargets(dir);
330
762
  if (targets.length > 0) projects.push(dir);
331
763
  if (depth >= maxDepth) return;
332
764
 
@@ -351,14 +783,6 @@ async function findInstalledProjects(rootDir, maxDepth) {
351
783
  return projects;
352
784
  }
353
785
 
354
- async function listSkills() {
355
- const entries = await fs.readdir(SKILLS_SRC);
356
- return entries
357
- .filter(f => f.endsWith('.md'))
358
- .map(f => ({ name: f.replace(/\.md$/, ''), path: path.join(SKILLS_SRC, f) }))
359
- .sort((a, b) => a.name.localeCompare(b.name));
360
- }
361
-
362
786
  async function ensureSettings(cwd, { input = process.stdin, output = process.stdout } = {}) {
363
787
  const settingsPath = path.join(cwd, SETTINGS_RELATIVE_PATH);
364
788
  if (await fileExists(settingsPath)) {
@@ -375,72 +799,253 @@ async function ensureSettings(cwd, { input = process.stdin, output = process.std
375
799
  }
376
800
 
377
801
  function formatSettings(settings) {
378
- return `version: 1
802
+ const orhestra = settings.orhestra ?? DEFAULT_SETTINGS.orhestra;
803
+ const aim = settings.aim ?? DEFAULT_SETTINGS.aim;
804
+ const orhestraSteps = (orhestra.steps ?? DEFAULT_ORHESTRA_STEPS).map(step => {
805
+ const onFailureThen = step.onFailure?.then
806
+ ?.map(stepId => ` - ${stepId}`)
807
+ .join('\n');
808
+ const onFailure = step.onFailure
809
+ ? `
810
+ # Обработка failed-результата этого шага.
811
+ on_failure:
812
+ skill: ${step.onFailure.skill}
813
+ args: ${JSON.stringify(step.onFailure.args ?? '')}
814
+ # После исправления повторно запускаются включённые шаги с этими id.
815
+ then:
816
+ ${onFailureThen}
817
+ max_cycles: ${step.onFailure.maxCycles ?? 5}`
818
+ : '';
819
+ return ` - id: ${step.id}
820
+ skill: ${step.skill}
821
+ enabled: ${step.enabled !== false ? 'true' : 'false'}
822
+ # Строка аргументов передаётся скиллу как часть текущего вызова.
823
+ args: ${JSON.stringify(step.args ?? '')}${onFailure}`;
824
+ }).join('\n');
825
+ const reviewAgents = REVIEW_AGENT_DEFINITIONS.map(agent => {
826
+ const configured = settings.review.agents[agent.key] ?? DEFAULT_REVIEW_AGENTS[agent.key];
827
+ return ` # ${agent.description}
828
+ # ${agent.auto}
829
+ ${agent.key}:
830
+ # always — всегда, auto — когда применимо, off — отключено.
831
+ mode: ${configured.mode}
832
+ model:
833
+ # Модель для Claude Code.
834
+ claude: ${configured.model.claude}
835
+ # Модель для Codex.
836
+ codex: ${configured.model.codex}`;
837
+ }).join('\n\n');
838
+
839
+ return `version: 2
840
+
841
+ orhestra:
842
+ # Режим полного цикла eda-orhestra.
843
+ # automatic | manual
844
+ mode: ${orhestra.mode ?? 'automatic'}
845
+ # Упорядоченная цепочка. Шаги можно отключать, удалять и переставлять.
846
+ steps:
847
+ ${orhestraSteps}
379
848
 
380
- defaults:
381
- # Включает strict-режим по умолчанию для eda-explore, eda-plan, eda-plan-polish, eda-review и eda-review-check.
849
+ aim:
850
+ # Режим ответов на рабочие вопросы eda-aim.
851
+ # automatic | manual
852
+ mode: ${aim.mode ?? 'automatic'}
853
+
854
+ explore:
855
+ # Включает кросс-CLI ревью в eda-explore.
856
+ # true | false
857
+ strict: ${settings.explore.strict ? 'true' : 'false'}
858
+ # Определяет, как eda-explore ведёт исследовательские развилки.
859
+ # autonomous | recommend_and_ask | ask_each_time
860
+ decision_mode: ${settings.explore.decisionMode}
861
+
862
+ plan:
863
+ # Включает кросс-CLI ревью в eda-plan.
382
864
  # true | false
383
- strict: ${settings.strict ? 'true' : 'false'}
384
- # Задаёт размер плана по умолчанию для eda-plan.
865
+ strict: ${settings.plan.strict ? 'true' : 'false'}
866
+ # Задаёт размер плана.
385
867
  # normal | short | ask_each_time
386
- plan_size: ${settings.planSize}
387
- # Определяет, как eda-explore ведёт исследовательские развилки, а eda-plan принимает существенные решения.
868
+ size: ${settings.plan.size}
869
+ # Определяет, как eda-plan принимает существенные решения.
388
870
  # autonomous | recommend_and_ask | ask_each_time
389
- decision_mode: ${settings.decisionMode}
390
- # Задаёт стратегию тестов по умолчанию для eda-plan.
871
+ decision_mode: ${settings.plan.decisionMode}
872
+ # Задаёт стратегию тестов.
391
873
  # after_each_phase | tdd_each_phase | end_of_plan | ask_each_time
392
- test_strategy: ${settings.testStrategy}
393
- # Задаёт стратегию логирования по умолчанию для eda-plan.
874
+ test_strategy: ${settings.plan.testStrategy}
875
+ # Задаёт стратегию логирования.
394
876
  # debug_precise | standard | ask_each_time
395
- logging_strategy: ${settings.loggingStrategy}
877
+ logging_strategy: ${settings.plan.loggingStrategy}
396
878
 
397
- automate:
398
- # Добавляет docs/plans/ в обычный запуск eda-automate.
879
+ plan-polish:
880
+ # Включает кросс-CLI ревью в eda-plan-polish.
399
881
  # true | false
400
- include_plans: ${settings.includePlans ? 'true' : 'false'}
882
+ strict: ${settings.planPolish.strict ? 'true' : 'false'}
401
883
 
402
884
  review:
403
- # Добавляет в eda-review / eda-review-check проверку качества кода и meta-reviewer quality-check.
885
+ # Каждая проверка имеет собственный режим запуска и модели для обеих сред.
886
+ agents:
887
+ ${reviewAgents}
888
+
889
+ send-review:
890
+ # После успешной отправки скрывает предыдущие сводки eda-send-review и резолвит их inline-треды.
891
+ # true | false
892
+ close_previous_reviews: ${settings.sendReview.closePreviousReviews ? 'true' : 'false'}
893
+
894
+ discover-automations:
895
+ # Добавляет docs/plans/ в обычный запуск eda-discover-automations.
404
896
  # true | false
405
- include_code_quality: ${settings.includeCodeQuality ? 'true' : 'false'}
897
+ include_plans: ${settings.discoverAutomations.includePlans ? 'true' : 'false'}
406
898
  `;
407
899
  }
408
900
 
409
- async function installToClaude(cwd, skills, output = process.stdout) {
410
- const dst = path.join(cwd, '.claude/skills');
411
- await fs.mkdir(dst, { recursive: true });
901
+ async function installTarget(cwd, target, skills, agents, packageVersion, output = process.stdout) {
902
+ const targetRoot = path.join(cwd, `.${target}`);
903
+ const skillsDir = path.join(targetRoot, 'skills');
904
+ const agentsDir = path.join(targetRoot, 'agents');
905
+ const manifestPath = path.join(targetRoot, MANIFEST_FILE);
906
+ await assertManagedDirectory(targetRoot);
907
+ await assertManagedDirectory(skillsDir);
908
+ await assertManagedDirectory(agentsDir);
909
+ const previousManifest = await readManifest(manifestPath);
412
910
  const changedSkills = [];
911
+ const changedAgents = [];
912
+
913
+ await fs.mkdir(skillsDir, { recursive: true });
413
914
  for (const skill of skills) {
414
- const skillDir = path.join(dst, skill.name);
415
- await fs.mkdir(skillDir, { recursive: true });
416
- const content = await fs.readFile(skill.path, 'utf-8');
417
- const changed = await writeSkillFile(path.join(skillDir, 'SKILL.md'), content);
915
+ const desiredFiles = await buildSkillFiles(skill, target);
916
+ const changed = await syncManagedDirectory(path.join(skillsDir, skill.name), desiredFiles);
418
917
  if (changed) changedSkills.push(skill.name);
918
+ if (target === 'codex') await removeObsoleteCodexFile(skillsDir, skill.name);
919
+ }
920
+
921
+ if (agents.length > 0) await fs.mkdir(agentsDir, { recursive: true });
922
+ for (const agent of agents) {
923
+ const extension = target === 'claude' ? '.md' : '.toml';
924
+ const rendered = target === 'claude'
925
+ ? renderClaudeAgent(agent, agent.prompt)
926
+ : renderCodexAgent(agent, agent.prompt);
927
+ const changed = await writeManagedFile(path.join(agentsDir, `${agent.name}${extension}`), rendered);
928
+ if (changed) changedAgents.push(agent.name);
419
929
  }
420
- output.write(` ✓ Claude Code: ${dst} (изменилось ${formatSkillCount(changedSkills.length)})\n`);
421
- return { changedSkills };
930
+
931
+ await removeRetiredComponents(targetRoot, target, previousManifest, skills, agents);
932
+ await writeManifest(manifestPath, {
933
+ schemaVersion: 1,
934
+ packageVersion,
935
+ skills: skills.map(skill => skill.name),
936
+ agents: agents.map(agent => agent.name)
937
+ });
938
+
939
+ const label = target === 'claude' ? 'Claude Code' : 'Codex CLI';
940
+ output.write(` ✓ ${label}: ${targetRoot} (скилы: ${formatSkillCount(changedSkills.length)}, агенты: ${formatAgentCount(changedAgents.length)})\n`);
941
+ return { changedSkills, changedAgents };
422
942
  }
423
943
 
424
- async function installToCodex(cwd, skills, output = process.stdout) {
425
- const dst = path.join(cwd, '.codex/skills');
426
- await fs.mkdir(dst, { recursive: true });
427
- const changedSkills = [];
428
- for (const skill of skills) {
429
- const skillDir = path.join(dst, skill.name);
430
- await fs.mkdir(skillDir, { recursive: true });
431
- const content = await fs.readFile(skill.path, 'utf-8');
432
- const changed = await writeSkillFile(path.join(skillDir, 'SKILL.md'), content);
433
- if (changed) changedSkills.push(skill.name);
434
- await removeObsoleteCodexFile(dst, skill.name);
944
+ async function buildSkillFiles(skill, target) {
945
+ const files = await readSourceDirectory(skill.sourceDir, new Set(['skill.json']));
946
+ const entrypoint = files.get('SKILL.md');
947
+ if (!entrypoint) throw new Error(`У скила ${skill.name} нет SKILL.md.`);
948
+
949
+ const content = entrypoint.toString('utf8');
950
+ const rendered = target === 'claude'
951
+ ? renderClaudeSkill(content, skill.config)
952
+ : renderCodexSkill(content, skill.config);
953
+ files.set('SKILL.md', Buffer.from(rendered));
954
+ return files;
955
+ }
956
+
957
+ async function syncManagedDirectory(directoryPath, desiredFiles) {
958
+ const currentFiles = await readInstalledDirectory(directoryPath);
959
+ if (fileMapsEqual(currentFiles, desiredFiles)) return false;
960
+
961
+ await fs.rm(directoryPath, { recursive: true, force: true });
962
+ for (const [relativePath, content] of desiredFiles) {
963
+ const destination = path.join(directoryPath, relativePath);
964
+ await fs.mkdir(path.dirname(destination), { recursive: true });
965
+ await fs.writeFile(destination, content);
435
966
  }
436
- output.write(` ✓ Codex CLI: ${dst} (изменилось ${formatSkillCount(changedSkills.length)})\n`);
437
- return { changedSkills };
967
+ return true;
438
968
  }
439
969
 
440
- async function writeSkillFile(filePath, content) {
441
- const previousContent = await readFileIfExists(filePath);
970
+ async function readSourceDirectory(root, excludedFiles = new Set()) {
971
+ const files = new Map();
972
+
973
+ async function walk(directory, relativeDirectory = '') {
974
+ const entries = await fs.readdir(directory, { withFileTypes: true });
975
+ entries.sort((a, b) => a.name.localeCompare(b.name));
976
+ for (const entry of entries) {
977
+ const relativePath = path.join(relativeDirectory, entry.name);
978
+ if (relativeDirectory === '' && excludedFiles.has(entry.name)) continue;
979
+ if (entry.isSymbolicLink()) {
980
+ throw new Error(`Symlink не поддерживается в исходниках пакета: ${path.join(root, relativePath)}`);
981
+ }
982
+ if (entry.isDirectory()) {
983
+ await walk(path.join(directory, entry.name), relativePath);
984
+ } else if (entry.isFile()) {
985
+ files.set(relativePath, await fs.readFile(path.join(directory, entry.name)));
986
+ }
987
+ }
988
+ }
989
+
990
+ await walk(root);
991
+ return files;
992
+ }
993
+
994
+ async function readInstalledDirectory(root) {
995
+ const files = new Map();
996
+ let rootStat;
997
+ try {
998
+ rootStat = await fs.lstat(root);
999
+ } catch (err) {
1000
+ if (err?.code === 'ENOENT') return files;
1001
+ throw err;
1002
+ }
1003
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
1004
+ files.set('', Buffer.from('__not_a_directory__'));
1005
+ return files;
1006
+ }
1007
+
1008
+ async function walk(directory, relativeDirectory = '') {
1009
+ const entries = await fs.readdir(directory, { withFileTypes: true });
1010
+ entries.sort((a, b) => a.name.localeCompare(b.name));
1011
+ for (const entry of entries) {
1012
+ const relativePath = path.join(relativeDirectory, entry.name);
1013
+ if (entry.isSymbolicLink()) {
1014
+ files.set(relativePath, Buffer.from('__symlink__'));
1015
+ } else if (entry.isDirectory()) {
1016
+ await walk(path.join(directory, entry.name), relativePath);
1017
+ } else if (entry.isFile()) {
1018
+ files.set(relativePath, await fs.readFile(path.join(directory, entry.name)));
1019
+ }
1020
+ }
1021
+ }
1022
+
1023
+ await walk(root);
1024
+ return files;
1025
+ }
1026
+
1027
+ function fileMapsEqual(left, right) {
1028
+ if (left.size !== right.size) return false;
1029
+ for (const [filePath, content] of right) {
1030
+ if (!left.get(filePath)?.equals(content)) return false;
1031
+ }
1032
+ return true;
1033
+ }
1034
+
1035
+ async function writeManagedFile(filePath, content) {
1036
+ const currentStat = await lstatIfExists(filePath);
1037
+ let previousContent = null;
1038
+
1039
+ if (currentStat?.isFile() && !currentStat.isSymbolicLink()) {
1040
+ previousContent = await fs.readFile(filePath, 'utf8');
1041
+ } else if (currentStat) {
1042
+ await fs.rm(filePath, { recursive: true, force: true });
1043
+ }
1044
+
1045
+ if (previousContent === content) return false;
1046
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
442
1047
  await fs.writeFile(filePath, content);
443
- return previousContent !== content;
1048
+ return true;
444
1049
  }
445
1050
 
446
1051
  async function readFileIfExists(filePath) {
@@ -457,10 +1062,19 @@ function formatChangedSkills(actionLabel, skillNames) {
457
1062
  return `${actionLabel} ${formatSkillCount(skillNames.length)}: ${skillNames.join(', ')}.\n`;
458
1063
  }
459
1064
 
1065
+ function formatChangedAgents(actionLabel, agentNames) {
1066
+ if (agentNames.length === 0) return `${actionLabel} 0 агентов.\n`;
1067
+ return `${actionLabel} ${formatAgentCount(agentNames.length)}: ${agentNames.join(', ')}.\n`;
1068
+ }
1069
+
460
1070
  function formatSkillCount(count) {
461
1071
  return `${count} ${pluralizeSkill(count)}`;
462
1072
  }
463
1073
 
1074
+ function formatAgentCount(count) {
1075
+ return `${count} ${pluralizeAgent(count)}`;
1076
+ }
1077
+
464
1078
  function formatProjectPath(rootDir, projectDir) {
465
1079
  const relative = path.relative(rootDir, projectDir);
466
1080
  return relative === '' ? '.' : relative;
@@ -498,6 +1112,14 @@ function pluralizeSkill(count) {
498
1112
  return 'скилов';
499
1113
  }
500
1114
 
1115
+ function pluralizeAgent(count) {
1116
+ const mod10 = count % 10;
1117
+ const mod100 = count % 100;
1118
+ if (mod10 === 1 && mod100 !== 11) return 'агент';
1119
+ if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return 'агента';
1120
+ return 'агентов';
1121
+ }
1122
+
501
1123
  async function removeObsoleteCodexFile(dst, skillName) {
502
1124
  try {
503
1125
  await fs.rm(path.join(dst, `${skillName}.md`));
@@ -506,23 +1128,53 @@ async function removeObsoleteCodexFile(dst, skillName) {
506
1128
  }
507
1129
  }
508
1130
 
509
- async function removeRetiredSkills(cwd, targets) {
510
- const targetDirs = {
511
- claude: path.join(cwd, '.claude/skills'),
512
- codex: path.join(cwd, '.codex/skills')
513
- };
1131
+ async function removeRetiredComponents(targetRoot, target, previousManifest, skills, agents) {
1132
+ const currentSkills = new Set(skills.map(skill => skill.name));
1133
+ const currentAgents = new Set(agents.map(agent => agent.name));
1134
+ const staleSkills = new Set([
1135
+ ...RETIRED_SKILLS,
1136
+ ...(previousManifest?.skills ?? []).filter(name => !currentSkills.has(name))
1137
+ ].filter(isManagedComponentName));
1138
+ const staleAgents = new Set([
1139
+ ...RETIRED_AGENTS,
1140
+ ...(previousManifest?.agents ?? []).filter(name => !currentAgents.has(name))
1141
+ ].filter(isManagedComponentName));
514
1142
 
515
- for (const target of targets) {
516
- const dst = targetDirs[target];
517
- if (!dst) continue;
1143
+ for (const skillName of staleSkills) {
1144
+ await fs.rm(path.join(targetRoot, 'skills', skillName), { recursive: true, force: true });
1145
+ await fs.rm(path.join(targetRoot, 'skills', `${skillName}.md`), { force: true });
1146
+ }
518
1147
 
519
- for (const skillName of RETIRED_SKILLS) {
520
- await fs.rm(path.join(dst, skillName), { recursive: true, force: true });
521
- await fs.rm(path.join(dst, `${skillName}.md`), { force: true });
1148
+ const extension = target === 'claude' ? '.md' : '.toml';
1149
+ for (const agentName of staleAgents) {
1150
+ await fs.rm(path.join(targetRoot, 'agents', `${agentName}${extension}`), { force: true });
1151
+ }
1152
+ }
1153
+
1154
+ async function readManifest(manifestPath) {
1155
+ try {
1156
+ const manifestStat = await fs.lstat(manifestPath);
1157
+ if (!manifestStat.isFile() || manifestStat.isSymbolicLink()) return null;
1158
+ const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
1159
+ if (manifest.schemaVersion !== 1 || !Array.isArray(manifest.skills) || !Array.isArray(manifest.agents)) {
1160
+ return null;
522
1161
  }
1162
+ return manifest;
1163
+ } catch (err) {
1164
+ if (err?.code === 'ENOENT' || err instanceof SyntaxError) return null;
1165
+ throw err;
523
1166
  }
524
1167
  }
525
1168
 
1169
+ async function writeManifest(manifestPath, manifest) {
1170
+ const content = `${JSON.stringify(manifest, null, 2)}\n`;
1171
+ await writeManagedFile(manifestPath, content);
1172
+ }
1173
+
1174
+ function isManagedComponentName(name) {
1175
+ return typeof name === 'string' && /^eda-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
1176
+ }
1177
+
526
1178
  async function dirExists(p) {
527
1179
  try {
528
1180
  const st = await fs.stat(p);
@@ -549,3 +1201,23 @@ async function statIfExists(p) {
549
1201
  return null;
550
1202
  }
551
1203
  }
1204
+
1205
+ async function lstatIfExists(p) {
1206
+ try {
1207
+ return await fs.lstat(p);
1208
+ } catch (err) {
1209
+ if (err?.code !== 'ENOENT') throw err;
1210
+ return null;
1211
+ }
1212
+ }
1213
+
1214
+ async function assertManagedDirectory(directoryPath) {
1215
+ const directoryStat = await lstatIfExists(directoryPath);
1216
+ if (!directoryStat) return;
1217
+ if (directoryStat.isSymbolicLink()) {
1218
+ throw new Error(`Управляемый каталог не должен быть symlink: ${directoryPath}`);
1219
+ }
1220
+ if (!directoryStat.isDirectory()) {
1221
+ throw new Error(`Ожидался каталог: ${directoryPath}`);
1222
+ }
1223
+ }