@gian-tiaga/eda 3.13.1 → 4.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 (82) hide show
  1. package/MIGRATION.md +23 -2
  2. package/README.md +153 -232
  3. package/agents/eda-aim-executor/prompt.md +2 -0
  4. package/agents/eda-aim-verifier/prompt.md +2 -0
  5. package/agents/eda-review-api/prompt.md +4 -4
  6. package/agents/eda-review-architecture/prompt.md +4 -4
  7. package/agents/eda-review-business/prompt.md +5 -12
  8. package/agents/eda-review-code-quality/prompt.md +4 -4
  9. package/agents/eda-review-correctness/prompt.md +3 -3
  10. package/agents/eda-review-database/prompt.md +4 -4
  11. package/agents/eda-review-documentation/prompt.md +4 -4
  12. package/agents/eda-review-frontend/prompt.md +4 -4
  13. package/agents/eda-review-infra/agent.json +14 -0
  14. package/agents/eda-review-infra/prompt.md +36 -0
  15. package/agents/eda-review-performance/prompt.md +7 -7
  16. package/agents/eda-review-plan-alignment/prompt.md +4 -4
  17. package/agents/eda-review-previous-reviews/prompt.md +5 -4
  18. package/agents/eda-review-references/prompt.md +4 -4
  19. package/agents/eda-review-rules/prompt.md +4 -4
  20. package/agents/eda-review-security/prompt.md +4 -4
  21. package/agents/eda-review-seo/agent.json +14 -0
  22. package/agents/eda-review-seo/prompt.md +36 -0
  23. package/agents/eda-review-tests/prompt.md +4 -4
  24. package/agents/eda-review-triage/agent.json +14 -0
  25. package/agents/eda-review-triage/prompt.md +38 -0
  26. package/agents/eda-skill-runner/agent.json +14 -0
  27. package/agents/eda-skill-runner/prompt.md +11 -0
  28. package/bin/cli.js +3 -3
  29. package/lib/catalog.js +32 -1
  30. package/lib/install.js +266 -611
  31. package/lib/prompts.js +53 -0
  32. package/lib/renderers/codex-agent.js +3 -1
  33. package/lib/renderers/codex-skill.js +21 -1
  34. package/lib/renderers/opencode-agent.js +23 -0
  35. package/lib/renderers/opencode-skill.js +24 -0
  36. package/package.json +4 -4
  37. package/skills/_shared/done.md +15 -0
  38. package/skills/_shared/questions.md +9 -0
  39. package/skills/eda-aim/SKILL.md +2 -3
  40. package/skills/eda-business/SKILL.md +5 -6
  41. package/skills/eda-commit/SKILL.md +2 -3
  42. package/skills/eda-discover-automations/SKILL.md +6 -7
  43. package/skills/eda-explore/SKILL.md +28 -15
  44. package/skills/eda-fix/SKILL.md +7 -7
  45. package/skills/eda-fix-by-review/SKILL.md +4 -3
  46. package/skills/eda-just-do-it/SKILL.md +4 -4
  47. package/skills/eda-manual-test/SKILL.md +7 -5
  48. package/skills/eda-merge-worktree/SKILL.md +2 -3
  49. package/skills/eda-new-project/SKILL.md +3 -5
  50. package/skills/eda-orhestra/SKILL.md +3 -4
  51. package/skills/eda-plan/SKILL.md +126 -168
  52. package/skills/eda-plan-execute/SKILL.md +72 -92
  53. package/skills/eda-plan-polish/SKILL.md +14 -14
  54. package/skills/eda-plan-review/SKILL.md +25 -10
  55. package/skills/eda-plan-review-fix/SKILL.md +10 -7
  56. package/skills/eda-polish/SKILL.md +9 -6
  57. package/skills/eda-prepare-ai/SKILL.md +35 -15
  58. package/skills/eda-prod-check/SKILL.md +129 -0
  59. package/skills/eda-review/SKILL.md +92 -64
  60. package/skills/eda-review/references/checks/api.md +18 -0
  61. package/skills/eda-review/references/checks/architecture.md +16 -0
  62. package/skills/eda-review/references/checks/business.md +13 -0
  63. package/skills/eda-review/references/checks/code-quality.md +18 -0
  64. package/skills/eda-review/references/checks/correctness.md +19 -0
  65. package/skills/eda-review/references/checks/database.md +17 -0
  66. package/skills/eda-review/references/checks/documentation.md +13 -0
  67. package/skills/eda-review/references/checks/frontend.md +19 -0
  68. package/skills/eda-review/references/checks/infra.md +16 -0
  69. package/skills/eda-review/references/checks/performance.md +21 -0
  70. package/skills/eda-review/references/checks/plan-alignment.md +16 -0
  71. package/skills/eda-review/references/checks/previous-reviews.md +9 -0
  72. package/skills/eda-review/references/checks/references.md +11 -0
  73. package/skills/eda-review/references/checks/rules.md +16 -0
  74. package/skills/eda-review/references/checks/security.md +26 -0
  75. package/skills/eda-review/references/checks/seo.md +16 -0
  76. package/skills/eda-review/references/checks/tests.md +18 -0
  77. package/skills/eda-review/references/defects.md +24 -0
  78. package/skills/eda-review/references/triage.md +23 -0
  79. package/skills/eda-roadmap/SKILL.md +2 -4
  80. package/skills/eda-send-review/SKILL.md +3 -5
  81. package/skills/eda-worktree/SKILL.md +2 -3
  82. package/skills/eda-review/references/review-matrix.md +0 -72
package/lib/install.js CHANGED
@@ -1,16 +1,17 @@
1
- import checkbox from '@inquirer/checkbox';
2
- import select from '@inquirer/select';
3
1
  import { randomUUID } from 'node:crypto';
4
2
  import fs from 'node:fs/promises';
5
3
  import path from 'node:path';
6
4
  import { fileURLToPath } from 'node:url';
7
5
  import { parse as parseYaml } from 'yaml';
8
6
 
9
- import { listAgents, listSkills } from './catalog.js';
7
+ import { expandIncludes, listAgents, listSkills } from './catalog.js';
8
+ import { checkboxPrompt as checkbox, finishPrompts, selectPrompt as select, startPrompts } from './prompts.js';
10
9
  import { renderClaudeAgent } from './renderers/claude-agent.js';
11
10
  import { renderClaudeSkill } from './renderers/claude-skill.js';
12
11
  import { renderCodexAgent } from './renderers/codex-agent.js';
13
12
  import { renderCodexSkill } from './renderers/codex-skill.js';
13
+ import { renderOpenCodeAgent } from './renderers/opencode-agent.js';
14
+ import { renderOpenCodeSkill } from './renderers/opencode-skill.js';
14
15
 
15
16
  const __filename = fileURLToPath(import.meta.url);
16
17
  const __dirname = path.dirname(__filename);
@@ -37,10 +38,13 @@ const ARTIFACT_DIRECTORY_NAMES = [
37
38
  ];
38
39
  const TARGET_CHOICES = [
39
40
  { value: 'claude', label: 'Claude Code', dir: '.claude/skills/' },
40
- { value: 'codex', label: 'Codex CLI', dir: '.agents/skills/' }
41
+ { value: 'codex', label: 'Codex CLI', dir: '.agents/skills/' },
42
+ { value: 'opencode', label: 'OpenCode', dir: '.opencode/skills/' }
41
43
  ];
44
+ // В вопросах платформы показываются в этом порядке; установка идёт в порядке TARGET_CHOICES.
45
+ const TARGET_PROMPT_ORDER = ['claude', 'opencode', 'codex'];
42
46
  const UPDATE_ALL_MAX_DEPTH = 2;
43
- const UPDATE_ALL_SKIP_DIRS = new Set(['.git', '.agents', '.claude', '.codex', 'node_modules']);
47
+ const UPDATE_ALL_SKIP_DIRS = new Set(['.git', '.agents', '.claude', '.codex', '.opencode', 'node_modules']);
44
48
  const RETIRED_SKILLS = [
45
49
  'eda-research',
46
50
  'eda-review-check',
@@ -65,6 +69,8 @@ const RETIRED_AGENTS = [
65
69
  'eda-plan-review-previous'
66
70
  ];
67
71
  const RETIRED_SETTINGS_PATHS = [
72
+ 'plan.review',
73
+ 'plan.meta_review',
68
74
  'plan-review.agents',
69
75
  'review-check',
70
76
  'review.strict',
@@ -72,138 +78,30 @@ const RETIRED_SETTINGS_PATHS = [
72
78
  ];
73
79
  const MANIFEST_FILE = 'eda-manifest.json';
74
80
  const SETTINGS_VERSION = 3;
75
- const REVIEW_AGENT_DEFINITIONS = [
76
- {
77
- key: 'correctness',
78
- description: 'Проверяет ошибки в логике, крайние случаи и обработку ошибок.',
79
- auto: 'Эта базовая проверка по умолчанию запускается всегда.',
80
- mode: 'always',
81
- claude: 'sonnet',
82
- codex: 'gpt-5.6-terra'
83
- },
84
- {
85
- key: 'architecture',
86
- description: 'Проверяет архитектурные границы, зависимости и разделение ответственности.',
87
- auto: 'В auto запускается при изменении модулей, зависимостей, публичных контрактов или границ системы.',
88
- mode: 'auto',
89
- claude: 'opus',
90
- codex: 'gpt-5.6-sol'
91
- },
92
- {
93
- key: 'rules',
94
- description: 'Проверяет соблюдение AGENTS.md, CLAUDE.md и docs/rules.md.',
95
- auto: 'Эта базовая проверка по умолчанию запускается всегда.',
96
- mode: 'always',
97
- claude: 'haiku',
98
- codex: 'gpt-5.6-luna'
99
- },
100
- {
101
- key: 'references',
102
- description: 'Проверяет соответствие применимым карточкам из docs/references/.',
103
- auto: 'В auto запускается только когда для изменений выбраны применимые карточки.',
104
- mode: 'auto',
105
- claude: 'haiku',
106
- codex: 'gpt-5.6-luna'
107
- },
108
- {
109
- key: 'business',
110
- description: 'Проверяет изменённое поведение по применимым карточкам из docs/business/.',
111
- auto: 'В auto запускается только когда изменения затрагивают поведение с применимой business-карточкой.',
112
- mode: 'auto',
113
- claude: 'sonnet',
114
- codex: 'gpt-5.6-terra'
115
- },
116
- {
117
- key: 'plan_alignment',
118
- description: 'Проверяет, что реализация соответствует указанному или однозначно найденному плану.',
119
- auto: 'В auto запускается только когда у ревью есть конкретный план.',
120
- mode: 'auto',
121
- claude: 'sonnet',
122
- codex: 'gpt-5.6-terra'
123
- },
124
- {
125
- key: 'code_quality',
126
- description: 'Проверяет читаемость, сложность, дублирование и поддерживаемость кода.',
127
- auto: 'Эта базовая проверка по умолчанию запускается всегда.',
128
- mode: 'always',
129
- claude: 'sonnet',
130
- codex: 'gpt-5.6-terra'
131
- },
132
- {
133
- key: 'tests',
134
- description: 'Проверяет достаточность, корректность и устойчивость тестов.',
135
- auto: 'Эта базовая проверка по умолчанию запускается всегда.',
136
- mode: 'always',
137
- claude: 'sonnet',
138
- codex: 'gpt-5.6-terra'
139
- },
140
- {
141
- key: 'security',
142
- description: 'Проверяет уязвимости, права доступа, валидацию и работу с чувствительными данными.',
143
- auto: 'В auto запускается для auth, permissions, внешнего ввода, файлов, сети, секретов и криптографии.',
144
- mode: 'auto',
145
- claude: 'opus',
146
- codex: 'gpt-5.6-sol'
147
- },
148
- {
149
- key: 'performance',
150
- description: 'Проверяет запросы, циклы, память, конкурентность и другие риски производительности.',
151
- auto: 'В auto запускается при изменении запросов, пакетной обработки, кешей, конкурентности или горячих путей.',
152
- mode: 'auto',
153
- claude: 'sonnet',
154
- codex: 'gpt-5.6-terra'
155
- },
156
- {
157
- key: 'frontend',
158
- description: 'Проверяет фронтенд-код, UI, UX, адаптивность и состояния интерфейса.',
159
- auto: 'В auto обязательно запускается при изменении компонентов, стилей, клиентских маршрутов или состояния UI.',
160
- mode: 'auto',
161
- claude: 'sonnet',
162
- codex: 'gpt-5.6-terra'
163
- },
164
- {
165
- key: 'api',
166
- description: 'Проверяет API-контракты, совместимость, валидацию и обработку ошибок.',
167
- auto: 'В auto запускается при изменении маршрутов, handlers, DTO, схем API, GraphQL или RPC.',
168
- mode: 'auto',
169
- claude: 'sonnet',
170
- codex: 'gpt-5.6-terra'
171
- },
172
- {
173
- key: 'database',
174
- description: 'Проверяет схемы, миграции, запросы, индексы и целостность данных.',
175
- auto: 'В auto запускается при изменении миграций, моделей хранения, SQL, схем или транзакций.',
176
- mode: 'auto',
177
- claude: 'opus',
178
- codex: 'gpt-5.6-sol'
179
- },
180
- {
181
- key: 'documentation',
182
- description: 'Проверяет, обновлена ли документация для изменившегося пользовательского поведения.',
183
- auto: 'В auto запускается при изменении публичного API, CLI, настроек или документируемого поведения.',
184
- mode: 'auto',
185
- claude: 'haiku',
186
- codex: 'gpt-5.6-luna'
187
- },
188
- {
189
- key: 'previous_reviews',
190
- description: 'Перепроверяет незакрытые замечания предыдущих ревью для удалённого PR или MR.',
191
- auto: 'В auto запускается только для ссылки на PR/MR или другой цели с доступными удалёнными обсуждениями.',
192
- mode: 'auto',
193
- claude: 'haiku',
194
- codex: 'gpt-5.6-luna'
195
- }
81
+ // Состав проверок выбирает eda-review-triage по содержимому ревью. В настройках у роли одна строка:
82
+ // default — модель установленного агента — или provider/model#variant, где provider и variant необязательны.
83
+ const REVIEW_AGENT_KEYS = [
84
+ 'triage',
85
+ 'correctness',
86
+ 'architecture',
87
+ 'rules',
88
+ 'references',
89
+ 'business',
90
+ 'plan_alignment',
91
+ 'code_quality',
92
+ 'tests',
93
+ 'security',
94
+ 'performance',
95
+ 'frontend',
96
+ 'api',
97
+ 'database',
98
+ 'infra',
99
+ 'seo',
100
+ 'documentation',
101
+ 'previous_reviews'
196
102
  ];
197
- const DEFAULT_REVIEW_AGENTS = Object.fromEntries(REVIEW_AGENT_DEFINITIONS.map(agent => [
198
- agent.key,
199
- {
200
- mode: agent.mode,
201
- model: {
202
- claude: agent.claude,
203
- codex: agent.codex
204
- }
205
- }
206
- ]));
103
+ const DEFAULT_REVIEW_AGENTS = Object.fromEntries(REVIEW_AGENT_KEYS.map(key => [key, 'default']));
104
+ const REVIEW_AGENT_MODEL_PATTERN = /^[\w.:-]+(?:\/[\w.:-]+)*(?:#[\w.-]+)?$/;
207
105
  const ORHESTRA_STEP_DEFINITIONS = [
208
106
  {
209
107
  id: 'plan',
@@ -263,20 +161,19 @@ const DEFAULT_SETTINGS = {
263
161
  },
264
162
  plan: {
265
163
  strict: false,
266
- review: true,
267
164
  size: 'normal',
268
165
  decisionMode: 'recommend_and_ask',
269
- testStrategy: 'ask_each_time',
270
- loggingStrategy: 'ask_each_time'
166
+ testStrategy: 'phase_tests_final_checks',
167
+ loggingStrategy: 'standard'
271
168
  },
272
169
  planReview: {
273
170
  threshold: 100
274
171
  },
275
172
  planPolish: {
276
- limit: 1
173
+ limit: 5
277
174
  },
278
175
  planExecute: {
279
- mode: 'auto'
176
+ mode: 'main'
280
177
  },
281
178
  polish: {
282
179
  execution: 'subagents'
@@ -285,249 +182,83 @@ const DEFAULT_SETTINGS = {
285
182
  depth: 'full'
286
183
  },
287
184
  review: {
288
- execution: 'subagents',
185
+ execution: 'main',
289
186
  agents: DEFAULT_REVIEW_AGENTS
290
187
  },
291
188
  sendReview: {
292
- closePreviousReviews: false
189
+ closePreviousReviews: true
293
190
  },
294
191
  discoverAutomations: {
295
192
  includePlans: false
296
193
  }
297
194
  };
195
+ // Разделы идут по ходу работы: исследование, план, его проверка, выполнение, ревью,
196
+ // ручные тесты; оркестраторы, которые собирают остальные скилы, — в конце.
298
197
  const SETTINGS_SECTION_NAMES = [
299
- 'orhestra',
300
- 'aim',
301
198
  'explore',
302
199
  'plan',
303
200
  'plan-review',
304
201
  'plan-polish',
305
202
  'plan-execute',
203
+ 'review',
306
204
  'polish',
307
205
  'manual-test',
308
- 'review',
309
206
  'send-review',
310
- 'discover-automations'
311
- ];
312
- const SETTINGS_CHOICES = [
313
- {
314
- section: 'explore',
315
- key: 'explore.strict',
316
- value: 'exploreStrict',
317
- name: 'Strict по умолчанию для eda-explore',
318
- checked: DEFAULT_SETTINGS.explore.strict
319
- },
320
- {
321
- section: 'plan',
322
- key: 'plan.strict',
323
- value: 'planStrict',
324
- name: 'Strict по умолчанию для eda-plan',
325
- checked: DEFAULT_SETTINGS.plan.strict
326
- },
327
- {
328
- section: 'plan',
329
- key: 'plan.review',
330
- value: 'planReview',
331
- name: 'Запускать eda-plan-review внутри eda-plan',
332
- checked: DEFAULT_SETTINGS.plan.review
333
- },
334
- {
335
- section: 'send-review',
336
- key: 'sendReview.closePreviousReviews',
337
- value: 'sendReviewClosePreviousReviews',
338
- name: 'Закрывать предыдущие ревью eda-send-review',
339
- checked: DEFAULT_SETTINGS.sendReview.closePreviousReviews
340
- },
341
- {
342
- section: 'discover-automations',
343
- key: 'discoverAutomations.includePlans',
344
- value: 'discoverAutomationsIncludePlans',
345
- name: 'Анализировать планы в eda-discover-automations по умолчанию',
346
- checked: DEFAULT_SETTINGS.discoverAutomations.includePlans
347
- }
348
- ];
349
- const ORHESTRA_MODE_CHOICES = [
350
- {
351
- value: 'automatic',
352
- name: 'Автоматически пройти план, выполнение, ревью и ручные тесты'
353
- },
354
- {
355
- value: 'manual',
356
- name: 'Писать план вместе с человеком и передавать ему вопросы'
357
- }
358
- ];
359
- const ORHESTRA_EXECUTION_CHOICES = [
360
- {
361
- value: 'subagents',
362
- name: 'Каждый этап выполнять отдельным изолированным субагентом'
363
- },
364
- {
365
- value: 'main',
366
- name: 'Все этапы выполнять основным агентом без передачи режима внутрь'
367
- },
368
- {
369
- value: 'main_propagated',
370
- name: 'Все этапы выполнять основным агентом и передавать main совместимым скилам'
371
- }
372
- ];
373
- const AIM_MODE_CHOICES = [
374
- {
375
- value: 'automatic',
376
- name: 'Автоматически отвечать на безопасные рабочие вопросы'
377
- },
378
- {
379
- value: 'manual',
380
- name: 'Передавать рабочие вопросы человеку'
381
- }
382
- ];
383
- const ORHESTRA_POLISH_LIMIT_CHOICES = [1, 2, 3, 5, 10].map(value => ({
384
- value,
385
- name: `${value} ${value === 1 ? 'итерация' : value < 5 ? 'итерации' : 'итераций'}`
386
- }));
387
- const PLAN_REVIEW_THRESHOLD_CHOICES = [80, 90, 100].map(value => ({
388
- value,
389
- name: `${value}/100`
390
- }));
391
- const PLAN_POLISH_LIMIT_CHOICES = [1, 2, 3].map(value => ({
392
- value,
393
- name: `${value} ${value === 1 ? 'итерация' : value < 5 ? 'итерации' : 'итераций'}`
394
- }));
395
- const PLAN_EXECUTE_MODE_CHOICES = [
396
- {
397
- value: 'auto',
398
- name: 'Решать по плану: один контекст для простого плана, субагенты для остальных'
399
- },
400
- {
401
- value: 'subagents',
402
- name: 'Каждую фазу выполнять отдельным изолированным субагентом'
403
- },
404
- {
405
- value: 'main',
406
- name: 'Выполнять весь план основным агентом без субагентов'
407
- }
408
- ];
409
- const POLISH_EXECUTION_CHOICES = [
410
- {
411
- value: 'subagents',
412
- name: 'Каждое review и исправление выполнять отдельным изолированным субагентом'
413
- },
414
- {
415
- value: 'main',
416
- name: 'Review и исправления выполнять основным агентом без субагентов'
417
- }
207
+ 'discover-automations',
208
+ 'orhestra',
209
+ 'aim'
418
210
  ];
211
+ // Варианты ответа: первым — значение по умолчанию, дальше — от ближайшего к дальнему,
212
+ // «Спрашивать каждый раз» всегда последним.
419
213
  const PLAN_SIZE_CHOICES = [
420
- {
421
- value: 'normal',
422
- name: 'Обычный план по умолчанию'
423
- },
424
- {
425
- value: 'short',
426
- name: 'Короткий plan short по умолчанию'
427
- },
428
- {
429
- value: 'ask_each_time',
430
- name: 'Спрашивать размер плана каждый раз'
431
- }
432
- ];
433
- const MANUAL_TEST_DEPTH_CHOICES = [
434
- {
435
- value: 'full',
436
- name: 'Полная проверка всех областей: API, база, консоль, интерфейс, алгоритмы'
437
- },
438
- {
439
- value: 'smoke',
440
- name: 'Только smoke: запуск и основной сценарий изменённой области'
441
- },
442
- {
443
- value: 'ask_each_time',
444
- name: 'Спрашивать глубину проверки каждый раз'
445
- }
214
+ { value: 'normal', name: 'normal', description: 'обычный план до 40 КБ' },
215
+ { value: 'short', name: 'short', description: 'до 12 КБ и 2 фаз' },
216
+ { value: 'ask_each_time', name: 'Спрашивать каждый раз' }
446
217
  ];
447
218
  const DECISION_MODE_CHOICES = [
448
- {
449
- value: 'recommend_and_ask',
450
- name: 'Рекомендовать вариант и спрашивать по важным развилкам'
451
- },
452
- {
453
- value: 'autonomous',
454
- name: 'Самостоятельно выбирать по коду, правилам и рискам'
455
- },
456
- {
457
- value: 'ask_each_time',
458
- name: 'Спрашивать по каждой значимой развилке'
459
- }
219
+ { value: 'recommend_and_ask', name: 'Рекомендовать и спрашивать', description: 'вопрос только по существенным развилкам' },
220
+ { value: 'autonomous', name: 'Самостоятельно', description: 'агент выбирает сам и записывает причину' },
221
+ { value: 'ask_each_time', name: 'Спрашивать по каждой развилке' }
460
222
  ];
461
223
  const TEST_STRATEGY_CHOICES = [
462
- {
463
- value: 'after_each_phase',
464
- name: 'Писать тесты и запускать все проверки после каждой фазы'
465
- },
466
- {
467
- value: 'phase_tests_final_checks',
468
- name: 'Писать тесты в фазах, запускать только их, остальные проверки — в конце'
469
- },
470
- {
471
- value: 'tdd_each_phase',
472
- name: 'В каждой фазе сначала тесты, затем код и все проверки фазы'
473
- },
474
- {
475
- value: 'end_of_plan',
476
- name: 'Писать тесты и запускать все проверки в конце плана'
477
- },
478
- {
479
- value: 'ask_each_time',
480
- name: 'Спрашивать стратегию тестов и проверок каждый раз'
481
- }
482
- ];
483
- const LOGGING_STRATEGY_CHOICES = [
484
- {
485
- value: 'standard',
486
- name: 'Стандартные info / warning / error по необходимости'
487
- },
488
- {
489
- value: 'debug_precise',
490
- name: 'Подробные debug-логи на важных шагах'
491
- },
492
- {
493
- value: 'ask_each_time',
494
- name: 'Спрашивать стратегию логирования каждый раз'
495
- }
224
+ { value: 'phase_tests_final_checks', name: 'Тесты в фазах, проверки в конце', description: 'в фазе — только её тесты, остальные проверки в конце плана' },
225
+ { value: 'tdd_each_phase', name: 'TDD', description: 'в каждой фазе сначала тесты, затем код и все проверки фазы' },
226
+ { value: 'after_each_phase', name: 'После каждой фазы', description: 'тесты и все проверки после каждой фазы' },
227
+ { value: 'end_of_plan', name: 'В конце плана', description: 'тесты и все проверки один раз в конце' },
228
+ { value: 'ask_each_time', name: 'Спрашивать каждый раз' }
496
229
  ];
497
- const REVIEW_MODE_CHOICES = [
498
- { value: 'always', name: 'always — запускать при каждом ревью' },
499
- { value: 'auto', name: 'auto — запускать только когда проверка применима' },
500
- { value: 'off', name: 'off — отключить проверку' }
230
+ const PLAN_EXECUTE_MODE_CHOICES = [
231
+ { value: 'main', name: 'Основной агент', description: 'весь план одним контекстом' },
232
+ { value: 'auto', name: 'auto', description: 'по плану: небольшой — одним контекстом, остальные — субагентами' },
233
+ { value: 'subagents', name: 'Субагенты', description: 'каждая фаза — отдельный субагент с независимой проверкой' }
501
234
  ];
502
235
  const REVIEW_EXECUTION_CHOICES = [
503
- {
504
- value: 'subagents',
505
- name: 'Каждую review-проверку выполнять отдельным специализированным субагентом'
506
- },
507
- {
508
- value: 'main',
509
- name: 'Все review-проверки выполнять основным агентом без субагентов'
510
- }
236
+ { value: 'main', name: 'Основной агент', description: 'все проверки в одном контексте' },
237
+ { value: 'subagents', name: 'Субагенты', description: 'разведчик и отдельный агент на каждую проверку' }
511
238
  ];
512
- const CLAUDE_REVIEW_MODEL_CHOICES = [
513
- { value: 'haiku', name: 'haiku — быстрая проверка' },
514
- { value: 'sonnet', name: 'sonnet — сильная проверка кода' },
515
- { value: 'opus', name: 'opus — максимальная глубина' }
239
+ const POLISH_EXECUTION_CHOICES = [
240
+ { value: 'subagents', name: 'Субагенты', description: 'каждое ревью и исправление — отдельный субагент' },
241
+ { value: 'main', name: 'Основной агент', description: 'ревью и исправления в одном контексте' }
516
242
  ];
517
- const CODEX_REVIEW_MODEL_CHOICES = [
518
- { value: 'gpt-5.6-luna', name: 'gpt-5.6-luna — быстрая проверка' },
519
- { value: 'gpt-5.6-terra', name: 'gpt-5.6-terra — сильная проверка кода' },
520
- { value: 'gpt-5.6-sol', name: 'gpt-5.6-sol — максимальная глубина' }
243
+ const MANUAL_TEST_DEPTH_CHOICES = [
244
+ { value: 'full', name: 'Полная', description: 'API, БД, консоль, интерфейс, алгоритмы, фон, права, логи, регрессии' },
245
+ { value: 'smoke', name: 'Smoke', description: 'запуск и основной сценарий изменённой области' },
246
+ { value: 'ask_each_time', name: 'Спрашивать каждый раз' }
247
+ ];
248
+ const ORHESTRA_MODE_CHOICES = [
249
+ { value: 'automatic', name: 'automatic', description: 'сам отвечает на безопасные рабочие вопросы' },
250
+ { value: 'manual', name: 'manual', description: 'план пишется с человеком, вопросы передаются ему' }
521
251
  ];
252
+ const ORHESTRA_EXECUTION_CHOICES = [
253
+ { value: 'subagents', name: 'Субагенты', description: 'каждый этап — отдельный субагент' },
254
+ { value: 'main', name: 'Основной агент', description: 'цепочку ведёт текущий агент, вложенные скилы — по своим настройкам' },
255
+ { value: 'main_propagated', name: 'Основной агент везде', description: 'plan-execute, polish и review тоже без субагентов' }
256
+ ];
257
+ // План и выполнение в оркестраторе включены всегда; спрашиваются только необязательные этапы.
258
+ const ORHESTRA_OPTIONAL_STEPS = ['eda-plan-polish', 'eda-polish', 'eda-manual-test'];
522
259
  const UPDATE_ALL_SETTINGS_MODE_CHOICES = [
523
- {
524
- value: 'configure',
525
- name: 'Настроить один общий профиль и записать его во все проекты'
526
- },
527
- {
528
- value: 'skip',
529
- name: 'Skip — сохранить полный v3, остальные конфиги перенести с defaults'
530
- }
260
+ { value: 'configure', name: 'Настроить общий профиль', description: 'ответить один раз и записать во все проекты' },
261
+ { value: 'skip', name: 'Skip', description: 'сохранить актуальные конфиги, старые перенести с defaults' }
531
262
  ];
532
263
 
533
264
  export async function init({ cwd, input = process.stdin, output = process.stdout }) {
@@ -540,13 +271,14 @@ export async function init({ cwd, input = process.stdin, output = process.stdout
540
271
  await syncPackage(cwd, targets, output, { action: 'install' });
541
272
  }
542
273
 
543
- export async function update({ cwd, input = process.stdin, output = process.stdout }) {
544
- const targets = await detectTargets(cwd);
545
- if (targets.length === 0) {
274
+ export async function update({ cwd, input = process.stdin, output = process.stdout, checkboxPrompt = checkbox }) {
275
+ const installedTargets = await detectTargets(cwd);
276
+ if (installedTargets.length === 0) {
546
277
  output.write('Не нашёл установленного пакета eda в этом проекте. Запусти `eda init`.\n');
547
278
  return;
548
279
  }
549
- output.write(`Найдены установленные среды: ${targets.join(', ')}\n`);
280
+ output.write(`Найдены установленные среды: ${installedTargets.join(', ')}\n`);
281
+ const targets = await askUpdateTargets({ installedTargets, input, output, checkboxPrompt });
550
282
  await migrateArtifactDirectories(cwd, output);
551
283
  await configureSettings(cwd, { input, output });
552
284
  await syncPackage(cwd, targets, output, { action: 'update' });
@@ -626,6 +358,12 @@ export async function updateAll({
626
358
  output.write(`Полный файл настроек сохранён: ${SETTINGS_RELATIVE_PATH}\n`);
627
359
  } else {
628
360
  await writeSettingsContent(projectDir, rendered);
361
+ if (migrated.legacyReviewAgents) {
362
+ output.write(`review.agents переведён на формат 4.0, все роли — default: ${SETTINGS_RELATIVE_PATH}\n`);
363
+ }
364
+ if (migrated.resetReviewAgents.length > 0) {
365
+ output.write(`Недопустимые модели review.agents заменены на default (${migrated.resetReviewAgents.join(', ')}): ${SETTINGS_RELATIVE_PATH}\n`);
366
+ }
629
367
  output.write(migrated.retiredPaths.length > 0
630
368
  ? `Из файла настроек убраны устаревшие ключи (${migrated.retiredPaths.join(', ')}): ${SETTINGS_RELATIVE_PATH}\n`
631
369
  : migrated.normalizedPaths.length > 0
@@ -677,22 +415,51 @@ export async function updateAll({
677
415
 
678
416
  export async function askTargets({ input = process.stdin, output = process.stdout } = {}) {
679
417
  if (!input.isTTY || !output.isTTY) {
680
- output.write('Нет интерактивного терминала — устанавливаю Claude Code и Codex CLI.\n');
418
+ output.write('Нет интерактивного терминала — устанавливаю Claude Code, Codex CLI и OpenCode.\n');
681
419
  return TARGET_CHOICES.map(choice => choice.value);
682
420
  }
683
421
 
684
- return checkbox({
685
- message: 'Куда устанавливать пакет eda?',
686
- instructions: 'Стрелки — выбрать, Space — отметить, Enter — продолжить',
687
- choices: TARGET_CHOICES.map(choice => ({
688
- name: `${choice.label} (${choice.dir})`,
422
+ const selected = await checkbox({
423
+ message: 'Куда установить eda?',
424
+ choices: targetPromptChoices().map(choice => ({
425
+ name: choice.label,
689
426
  value: choice.value,
690
- checked: true
691
- }))
427
+ description: choice.dir,
428
+ checked: false
429
+ })),
430
+ required: true
692
431
  }, {
693
432
  input,
694
433
  output
695
434
  });
435
+ return TARGET_CHOICES.map(choice => choice.value).filter(target => selected.includes(target));
436
+ }
437
+
438
+ export async function askUpdateTargets({
439
+ installedTargets,
440
+ input = process.stdin,
441
+ output = process.stdout,
442
+ checkboxPrompt = checkbox
443
+ }) {
444
+ if (!input.isTTY || !output.isTTY) return [...installedTargets];
445
+
446
+ // Установленные платформы обновляются всегда: снятая с них отметка ничего не удаляет.
447
+ const selected = await checkboxPrompt({
448
+ message: 'Какие платформы обновить? Установленные уже отмечены.',
449
+ choices: targetPromptChoices().map(choice => ({
450
+ name: choice.label,
451
+ value: choice.value,
452
+ description: installedTargets.includes(choice.value) ? 'установлена — обновится в любом случае' : choice.dir,
453
+ checked: installedTargets.includes(choice.value)
454
+ }))
455
+ }, { input, output });
456
+
457
+ return TARGET_CHOICES.map(choice => choice.value)
458
+ .filter(target => installedTargets.includes(target) || selected.includes(target));
459
+ }
460
+
461
+ function targetPromptChoices() {
462
+ return TARGET_PROMPT_ORDER.map(value => TARGET_CHOICES.find(choice => choice.value === value));
696
463
  }
697
464
 
698
465
  export async function askUpdateAllSettingsMode({
@@ -715,6 +482,8 @@ export async function askUpdateAllSettingsMode({
715
482
  });
716
483
  }
717
484
 
485
+ // Спрашиваются только основные настройки в порядке работы. Редкие получают defaults и
486
+ // меняются вручную в docs/settings.yaml, как и модели review.agents.
718
487
  export async function askSettings({
719
488
  input = process.stdin,
720
489
  output = process.stdout,
@@ -731,161 +500,57 @@ export async function askSettings({
731
500
  }
732
501
 
733
502
  const requestedSections = new Set(sections);
734
- const shouldAsk = key => missingKeys === null || missingKeys.has(key);
735
- const booleanChoices = SETTINGS_CHOICES.filter(choice => (
736
- requestedSections.has(choice.section) && shouldAsk(choice.key)
737
- ));
738
- const selected = booleanChoices.length > 0
739
- ? await checkboxPrompt({
740
- message: 'Какие настройки включить?',
741
- instructions: 'Стрелки — выбрать, Space — отметить, Enter — продолжить',
742
- choices: booleanChoices.map(({ section, key, ...choice }) => choice)
743
- }, { input, output })
744
- : [];
745
-
746
- if (requestedSections.has('explore') && shouldAsk('explore.strict')) {
747
- settings.explore.strict = selected.includes('exploreStrict');
748
- }
749
- if (requestedSections.has('plan') && shouldAsk('plan.strict')) {
750
- settings.plan.strict = selected.includes('planStrict');
751
- }
752
- if (requestedSections.has('plan') && shouldAsk('plan.review')) {
753
- settings.plan.review = selected.includes('planReview');
754
- }
755
- if (requestedSections.has('send-review') && shouldAsk('sendReview.closePreviousReviews')) {
756
- settings.sendReview.closePreviousReviews = selected.includes('sendReviewClosePreviousReviews');
757
- }
758
- if (requestedSections.has('discover-automations') && shouldAsk('discoverAutomations.includePlans')) {
759
- settings.discoverAutomations.includePlans = selected.includes('discoverAutomationsIncludePlans');
760
- }
761
-
762
- if (requestedSections.has('orhestra') && shouldAsk('orhestra.mode')) {
763
- settings.orhestra.mode = await selectPrompt({
764
- message: 'Как eda-orhestra должен вести полный цикл по умолчанию?',
765
- choices: ORHESTRA_MODE_CHOICES,
766
- default: settings.orhestra.mode
767
- }, { input, output });
503
+ const shouldAsk = (section, key) => requestedSections.has(section) && (missingKeys === null || missingKeys.has(key));
504
+ const scalarPrompts = [
505
+ ['plan', 'plan.size', settings.plan, 'size', 'eda-plan · размер плана', PLAN_SIZE_CHOICES],
506
+ ['plan', 'plan.decisionMode', settings.plan, 'decisionMode', 'eda-plan · как принимать существенные решения', DECISION_MODE_CHOICES],
507
+ ['plan', 'plan.testStrategy', settings.plan, 'testStrategy', 'eda-plan · когда писать тесты и запускать проверки', TEST_STRATEGY_CHOICES],
508
+ ['plan-execute', 'planExecute.mode', settings.planExecute, 'mode', 'eda-plan-execute · где выполнять фазы плана', PLAN_EXECUTE_MODE_CHOICES],
509
+ ['review', 'review.execution', settings.review, 'execution', 'eda-review · где выполнять проверки', REVIEW_EXECUTION_CHOICES],
510
+ ['polish', 'polish.execution', settings.polish, 'execution', 'eda-polish · где выполнять ревью и исправления', POLISH_EXECUTION_CHOICES],
511
+ ['manual-test', 'manualTest.depth', settings.manualTest, 'depth', 'eda-manual-test · глубина ручной проверки', MANUAL_TEST_DEPTH_CHOICES],
512
+ ['orhestra', 'orhestra.mode', settings.orhestra, 'mode', 'eda-orhestra · как вести полный цикл', ORHESTRA_MODE_CHOICES]
513
+ ].filter(([section, key]) => shouldAsk(section, key));
514
+ const askSteps = shouldAsk('orhestra', 'orhestra.steps');
515
+ const askOrhestraExecution = shouldAsk('orhestra', 'orhestra.execution');
516
+ if (scalarPrompts.length === 0 && !askSteps && !askOrhestraExecution) return settings;
517
+
518
+ startPrompts(`Настройки ${SETTINGS_RELATIVE_PATH}`, { output });
519
+ for (const [, , target, property, message, choices] of scalarPrompts) {
520
+ target[property] = await selectPrompt({ message, choices, default: target[property] }, { input, output });
768
521
  }
769
-
770
- if (requestedSections.has('orhestra') && shouldAsk('orhestra.steps')) {
522
+ if (askSteps) {
771
523
  const enabledSteps = await checkboxPrompt({
772
- message: 'Какие этапы eda-orhestra включить по умолчанию?',
773
- instructions: 'Порядок можно изменить вручную в docs/settings.yaml',
774
- choices: ORHESTRA_STEP_DEFINITIONS.map(step => ({
775
- name: step.name,
776
- value: step.skill,
777
- checked: settings.orhestra.steps.find(item => item.skill === step.skill)?.enabled !== false
778
- }))
524
+ message: 'eda-orhestra · какие этапы включить, кроме плана и выполнения',
525
+ choices: ORHESTRA_STEP_DEFINITIONS
526
+ .filter(step => ORHESTRA_OPTIONAL_STEPS.includes(step.skill))
527
+ .map(step => ({
528
+ name: step.name,
529
+ value: step.skill,
530
+ checked: settings.orhestra.steps.find(item => item.skill === step.skill)?.enabled !== false
531
+ }))
779
532
  }, { input, output });
780
- const codePolishLimit = enabledSteps.includes('eda-polish')
781
- ? await selectPrompt({
782
- message: 'Сколько итераций разрешить eda-polish внутри eda-orhestra?',
783
- choices: ORHESTRA_POLISH_LIMIT_CHOICES,
784
- default: 5
785
- }, { input, output })
786
- : 5;
787
533
  settings.orhestra.steps = DEFAULT_ORHESTRA_STEPS.map(step => ({
788
534
  ...step,
789
- enabled: enabledSteps.includes(step.skill),
790
- ...(step.skill === 'eda-polish' ? { args: `limit ${codePolishLimit}` } : {})
535
+ enabled: !ORHESTRA_OPTIONAL_STEPS.includes(step.skill) || enabledSteps.includes(step.skill)
791
536
  }));
792
537
  }
793
-
794
- const scalarPrompts = [
795
- ['orhestra', 'orhestra.execution', settings.orhestra, 'execution', 'Как eda-orhestra должен выполнять этапы по умолчанию?', ORHESTRA_EXECUTION_CHOICES],
796
- ['aim', 'aim.mode', settings.aim, 'mode', 'Как eda-aim должен отвечать на рабочие вопросы по умолчанию?', AIM_MODE_CHOICES],
797
- ['explore', 'explore.decisionMode', settings.explore, 'decisionMode', 'Как принимать важные решения в eda-explore?', DECISION_MODE_CHOICES],
798
- ['plan', 'plan.size', settings.plan, 'size', 'Какой размер плана eda-plan использовать по умолчанию?', PLAN_SIZE_CHOICES],
799
- ['plan', 'plan.decisionMode', settings.plan, 'decisionMode', 'Как принимать важные решения в eda-plan?', DECISION_MODE_CHOICES],
800
- ['plan', 'plan.testStrategy', settings.plan, 'testStrategy', 'Какую стратегию тестов и проверок eda-plan использовать по умолчанию?', TEST_STRATEGY_CHOICES],
801
- ['plan', 'plan.loggingStrategy', settings.plan, 'loggingStrategy', 'Какую стратегию логирования eda-plan использовать по умолчанию?', LOGGING_STRATEGY_CHOICES],
802
- ['plan-review', 'planReview.threshold', settings.planReview, 'threshold', 'Какой порог готовности использовать в eda-plan-review?', PLAN_REVIEW_THRESHOLD_CHOICES],
803
- ['plan-polish', 'planPolish.limit', settings.planPolish, 'limit', 'Сколько итераций «проверка + исправление» разрешить eda-plan-polish?', PLAN_POLISH_LIMIT_CHOICES],
804
- ['plan-execute', 'planExecute.mode', settings.planExecute, 'mode', 'Как eda-plan-execute должен выполнять фазы по умолчанию?', PLAN_EXECUTE_MODE_CHOICES],
805
- ['polish', 'polish.execution', settings.polish, 'execution', 'Как eda-polish должен выполнять review и исправления по умолчанию?', POLISH_EXECUTION_CHOICES],
806
- ['manual-test', 'manualTest.depth', settings.manualTest, 'depth', 'Какую глубину ручной проверки использовать в eda-manual-test по умолчанию?', MANUAL_TEST_DEPTH_CHOICES],
807
- ['review', 'review.execution', settings.review, 'execution', 'Как eda-review должен выполнять проверки по умолчанию?', REVIEW_EXECUTION_CHOICES]
808
- ];
809
- for (const [section, key, target, property, message, choices] of scalarPrompts) {
810
- if (!requestedSections.has(section) || !shouldAsk(key)) continue;
811
- target[property] = await selectPrompt({ message, choices, default: target[property] }, { input, output });
812
- }
813
-
814
- if (requestedSections.has('review')) {
815
- settings.review.agents = await askReviewAgentSettings({
816
- input,
817
- output,
818
- selectPrompt,
819
- seedAgents: settings.review.agents,
820
- missingKeys,
821
- skipModels: settings.review.execution === 'main'
822
- });
538
+ if (askOrhestraExecution) {
539
+ settings.orhestra.execution = await selectPrompt({
540
+ message: 'eda-orhestra · где выполнять этапы',
541
+ choices: ORHESTRA_EXECUTION_CHOICES,
542
+ default: settings.orhestra.execution
543
+ }, { input, output });
823
544
  }
545
+ finishPrompts(`Остальные настройки — в ${SETTINGS_RELATIVE_PATH}`, { output });
824
546
  return settings;
825
547
  }
826
548
 
827
- export async function askReviewAgentSettings({
828
- input = process.stdin,
829
- output = process.stdout,
830
- selectPrompt = select,
831
- seedAgents = DEFAULT_REVIEW_AGENTS,
832
- missingKeys = null,
833
- skipModels = false
834
- } = {}) {
835
- return askAgentSettings({
836
- input,
837
- output,
838
- selectPrompt,
839
- definitions: REVIEW_AGENT_DEFINITIONS,
840
- seedAgents,
841
- missingKeys,
842
- keyPrefix: 'review.agents',
843
- label: 'review',
844
- skipModels
845
- });
846
- }
847
-
848
- async function askAgentSettings({ input, output, selectPrompt, definitions, seedAgents, missingKeys, keyPrefix, label, skipModels = false }) {
849
- const configuredAgents = structuredClone(seedAgents);
850
- const shouldAsk = key => missingKeys === null || missingKeys.has(key);
851
- for (const agent of definitions) {
852
- const modeKey = `${keyPrefix}.${agent.key}.mode`;
853
- const claudeKey = `${keyPrefix}.${agent.key}.model.claude`;
854
- const codexKey = `${keyPrefix}.${agent.key}.model.codex`;
855
- let mode = configuredAgents[agent.key].mode;
856
- if (shouldAsk(modeKey)) {
857
- mode = await selectPrompt({
858
- message: `Когда запускать ${label}-проверку ${agent.key}?`,
859
- choices: REVIEW_MODE_CHOICES,
860
- default: mode
861
- }, { input, output });
862
- }
863
-
864
- let claude = configuredAgents[agent.key].model.claude;
865
- let codex = configuredAgents[agent.key].model.codex;
866
- if (!skipModels && mode !== 'off' && shouldAsk(claudeKey)) {
867
- claude = await selectPrompt({
868
- message: `Какой моделью Claude проверять ${agent.key}?`,
869
- choices: CLAUDE_REVIEW_MODEL_CHOICES,
870
- default: claude
871
- }, { input, output });
872
- }
873
- if (!skipModels && mode !== 'off' && shouldAsk(codexKey)) {
874
- codex = await selectPrompt({
875
- message: `Какой моделью Codex проверять ${agent.key}?`,
876
- choices: CODEX_REVIEW_MODEL_CHOICES,
877
- default: codex
878
- }, { input, output });
879
- }
880
- configuredAgents[agent.key] = { mode, model: { claude, codex } };
881
- }
882
- return configuredAgents;
883
- }
884
-
885
549
  async function detectTargets(cwd) {
886
550
  const targets = [];
887
- if (await targetStructureExists(cwd, 'claude')) targets.push('claude');
888
- if (await targetStructureExists(cwd, 'codex')) targets.push('codex');
551
+ for (const { value } of TARGET_CHOICES) {
552
+ if (await targetStructureExists(cwd, value)) targets.push(value);
553
+ }
889
554
  return targets;
890
555
  }
891
556
 
@@ -893,8 +558,9 @@ async function detectTargets(cwd) {
893
558
  // или manifest владения. Одних пустых/чужих каталогов skills и agents недостаточно.
894
559
  async function detectInstalledTargets(cwd) {
895
560
  const targets = [];
896
- if (await hasInstalledPackage(cwd, 'claude')) targets.push('claude');
897
- if (await hasInstalledPackage(cwd, 'codex')) targets.push('codex');
561
+ for (const { value } of TARGET_CHOICES) {
562
+ if (await hasInstalledPackage(cwd, value)) targets.push(value);
563
+ }
898
564
  return targets;
899
565
  }
900
566
 
@@ -908,7 +574,7 @@ async function targetStructureExists(cwd, target) {
908
574
 
909
575
  async function hasInstalledPackage(cwd, target) {
910
576
  const { skillsDir, agentsDir, manifestPath, legacySkillsDir } = getTargetPaths(cwd, target);
911
- const agentExtension = target === 'claude' ? '.md' : '.toml';
577
+ const agentExtension = target === 'codex' ? '.toml' : '.md';
912
578
  return await fileExists(manifestPath)
913
579
  || await hasInstalledSkill(skillsDir)
914
580
  || (legacySkillsDir !== null && await hasInstalledSkill(legacySkillsDir))
@@ -952,13 +618,9 @@ async function syncPackage(cwd, targets, output = process.stdout, { action = 'up
952
618
 
953
619
  const changedSkills = new Set();
954
620
  const changedAgents = new Set();
955
- if (targets.includes('claude')) {
956
- const result = await installTarget(cwd, 'claude', skills, agents, packageVersion, output);
957
- for (const skillName of result.changedSkills) changedSkills.add(skillName);
958
- for (const agentName of result.changedAgents) changedAgents.add(agentName);
959
- }
960
- if (targets.includes('codex')) {
961
- const result = await installTarget(cwd, 'codex', skills, agents, packageVersion, output);
621
+ for (const { value } of TARGET_CHOICES) {
622
+ if (!targets.includes(value)) continue;
623
+ const result = await installTarget(cwd, value, skills, agents, packageVersion, output);
962
624
  for (const skillName of result.changedSkills) changedSkills.add(skillName);
963
625
  for (const agentName of result.changedAgents) changedAgents.add(agentName);
964
626
  }
@@ -1115,6 +777,12 @@ async function ensureSettings(cwd, { input = process.stdin, output = process.std
1115
777
  if (migrated.retiredPaths.length > 0) {
1116
778
  output.write(`Убираю устаревшие настройки: ${migrated.retiredPaths.join(', ')}.\n`);
1117
779
  }
780
+ if (migrated.legacyReviewAgents) {
781
+ output.write('Перевожу review.agents на формат 4.0: все роли — default, нужные модели впишите вручную.\n');
782
+ }
783
+ if (migrated.resetReviewAgents.length > 0) {
784
+ output.write(`Заменяю недопустимые модели review.agents на default: ${migrated.resetReviewAgents.join(', ')}.\n`);
785
+ }
1118
786
  if (needsMigration) {
1119
787
  output.write(`Переношу ${SETTINGS_RELATIVE_PATH} на version: ${SETTINGS_VERSION}; спрашиваются только отсутствующие значения.\n`);
1120
788
  }
@@ -1153,6 +821,12 @@ async function configureSettings(cwd, { input = process.stdin, output = process.
1153
821
  if (migrated.retiredPaths.length > 0) {
1154
822
  output.write(`Убираю устаревшие настройки: ${migrated.retiredPaths.join(', ')}.\n`);
1155
823
  }
824
+ if (migrated.legacyReviewAgents) {
825
+ output.write('Перевожу review.agents на формат 4.0: все роли — default, нужные модели впишите вручную.\n');
826
+ }
827
+ if (migrated.resetReviewAgents.length > 0) {
828
+ output.write(`Заменяю недопустимые модели review.agents на default: ${migrated.resetReviewAgents.join(', ')}.\n`);
829
+ }
1156
830
  if (needsMigration) {
1157
831
  output.write(`Переношу ${SETTINGS_RELATIVE_PATH} на version: ${SETTINGS_VERSION}; спрашиваются только отсутствующие значения.\n`);
1158
832
  }
@@ -1172,7 +846,9 @@ function resolveSettingsUpdate(existingContent) {
1172
846
  const migrated = migrateSettingsContent(existingContent);
1173
847
  const needsMigration = migrated.version !== SETTINGS_VERSION
1174
848
  || migrated.missingKeys.size > 0
1175
- || migrated.normalizedPaths.length > 0;
849
+ || migrated.normalizedPaths.length > 0
850
+ || migrated.legacyReviewAgents
851
+ || migrated.resetReviewAgents.length > 0;
1176
852
  const rendered = formatSettings(migrated.settings);
1177
853
  return { migrated, needsMigration, rendered, formatChanged: rendered !== existingContent };
1178
854
  }
@@ -1243,7 +919,6 @@ function migrateSettingsContent(content) {
1243
919
 
1244
920
  const plan = objectValue(raw.plan);
1245
921
  transfer('plan.strict', plan.strict ?? legacyStrict, bool, value => { settings.plan.strict = value; });
1246
- transfer('plan.review', plan.review ?? plan.meta_review, bool, value => { settings.plan.review = value; });
1247
922
  transfer(
1248
923
  'plan.size',
1249
924
  plan.size ?? legacyDefaults.plan_size,
@@ -1306,17 +981,7 @@ function migrateSettingsContent(content) {
1306
981
  oneOf(['subagents', 'main']),
1307
982
  value => { settings.review.execution = value; }
1308
983
  );
1309
- transferAgentSettings({
1310
- rawAgents: objectValue(review.agents),
1311
- settingsAgents: settings.review.agents,
1312
- definitions: REVIEW_AGENT_DEFINITIONS,
1313
- prefix: 'review.agents',
1314
- missingKeys
1315
- });
1316
- if (typeof review.include_code_quality === 'boolean' && !objectValue(review.agents).code_quality) {
1317
- settings.review.agents.code_quality.mode = review.include_code_quality ? 'always' : 'off';
1318
- missingKeys.delete('review.agents.code_quality.mode');
1319
- }
984
+ const { legacyReviewAgents, resetReviewAgents } = transferReviewAgents(objectValue(review.agents), settings.review.agents);
1320
985
 
1321
986
  const sendReview = objectValue(raw['send-review']);
1322
987
  transfer(
@@ -1338,25 +1003,27 @@ function migrateSettingsContent(content) {
1338
1003
  settings,
1339
1004
  missingKeys,
1340
1005
  normalizedPaths,
1006
+ legacyReviewAgents,
1007
+ resetReviewAgents,
1341
1008
  retiredPaths: findRetiredSettingsPaths(raw),
1342
1009
  version: Number.isInteger(raw.version) ? raw.version : null
1343
1010
  };
1344
1011
  }
1345
1012
 
1346
- function transferAgentSettings({ rawAgents, settingsAgents, definitions, prefix, missingKeys }) {
1347
- const allowedModes = ['always', 'auto', 'off'];
1348
- const allowedClaudeModels = CLAUDE_REVIEW_MODEL_CHOICES.map(choice => choice.value);
1349
- const allowedCodexModels = CODEX_REVIEW_MODEL_CHOICES.map(choice => choice.value);
1350
- for (const definition of definitions) {
1351
- const configured = objectValue(rawAgents[definition.key]);
1352
- const model = objectValue(configured.model);
1353
- if (allowedModes.includes(configured.mode)) settingsAgents[definition.key].mode = configured.mode;
1354
- else missingKeys.add(`${prefix}.${definition.key}.mode`);
1355
- if (allowedClaudeModels.includes(model.claude)) settingsAgents[definition.key].model.claude = model.claude;
1356
- else missingKeys.add(`${prefix}.${definition.key}.model.claude`);
1357
- if (allowedCodexModels.includes(model.codex)) settingsAgents[definition.key].model.codex = model.codex;
1358
- else missingKeys.add(`${prefix}.${definition.key}.model.codex`);
1359
- }
1013
+ // Формат до 4.0 ({ mode, model } по платформам) не переносится: при мажорном обновлении
1014
+ // все роли получают default. Иное значение не в формате default | provider/model#variant тоже
1015
+ // заменяется на default.
1016
+ function transferReviewAgents(rawAgents, settingsAgents) {
1017
+ const legacyReviewAgents = Object.values(rawAgents).some(value => value && typeof value === 'object');
1018
+ const resetReviewAgents = [];
1019
+ if (legacyReviewAgents) return { legacyReviewAgents, resetReviewAgents };
1020
+ for (const key of REVIEW_AGENT_KEYS) {
1021
+ if (!Object.hasOwn(rawAgents, key)) continue;
1022
+ const value = typeof rawAgents[key] === 'string' ? rawAgents[key].trim() : '';
1023
+ if (REVIEW_AGENT_MODEL_PATTERN.test(value)) settingsAgents[key] = value;
1024
+ else resetReviewAgents.push(key);
1025
+ }
1026
+ return { legacyReviewAgents, resetReviewAgents };
1360
1027
  }
1361
1028
 
1362
1029
  // У eda-polish больше нет порога: цикл завершается по нулю находок.
@@ -1457,66 +1124,36 @@ ${onFailureThen}
1457
1124
  # Строка аргументов передаётся скиллу как часть текущего вызова.
1458
1125
  args: ${JSON.stringify(step.args ?? '')}${onFailure}`;
1459
1126
  }).join('\n');
1460
- const formatAgents = (definitions, configuredAgents, defaults) => definitions.map(agent => {
1461
- const configured = configuredAgents[agent.key] ?? defaults[agent.key];
1462
- return ` # ${agent.description}
1463
- # ${agent.auto}
1464
- ${agent.key}:
1465
- # always — всегда, auto — когда применимо, off — отключено.
1466
- mode: ${configured.mode}
1467
- model:
1468
- # Модель для Claude Code.
1469
- claude: ${configured.model.claude}
1470
- # Модель для Codex.
1471
- codex: ${configured.model.codex}`;
1472
- }).join('\n\n');
1473
- const reviewAgents = formatAgents(REVIEW_AGENT_DEFINITIONS, settings.review.agents, DEFAULT_REVIEW_AGENTS);
1127
+ const reviewAgents = REVIEW_AGENT_KEYS
1128
+ .map(key => ` ${key}: ${settings.review.agents[key] ?? DEFAULT_REVIEW_AGENTS[key]}`)
1129
+ .join('\n');
1474
1130
 
1475
1131
  return `version: ${SETTINGS_VERSION}
1476
1132
 
1477
- orhestra:
1478
- # Режим полного цикла eda-orhestra.
1479
- # automatic | manual
1480
- mode: ${orhestra.mode ?? 'automatic'}
1481
- # Где выполняются этапы и передаётся ли main совместимым вложенным скилам.
1482
- # subagents | main | main_propagated
1483
- execution: ${orhestra.execution ?? 'subagents'}
1484
- # Упорядоченная цепочка. Шаги можно отключать, удалять и переставлять.
1485
- steps:
1486
- ${orhestraSteps}
1487
-
1488
- aim:
1489
- # Режим ответов на рабочие вопросы eda-aim.
1490
- # automatic | manual
1491
- mode: ${aim.mode ?? 'automatic'}
1492
-
1493
1133
  explore:
1494
1134
  # Включает кросс-CLI ревью в eda-explore.
1495
- # true | false
1135
+ # false | true
1496
1136
  strict: ${settings.explore.strict ? 'true' : 'false'}
1497
- # Определяет, как eda-explore ведёт исследовательские развилки.
1498
- # autonomous | recommend_and_ask | ask_each_time
1137
+ # Как eda-explore ведёт исследовательские развилки.
1138
+ # recommend_and_ask | autonomous | ask_each_time
1499
1139
  decision_mode: ${settings.explore.decisionMode}
1500
1140
 
1501
1141
  plan:
1502
- # Включает кросс-CLI ревью в eda-plan.
1503
- # true | false
1504
- strict: ${settings.plan.strict ? 'true' : 'false'}
1505
- # Запускает изолированный цикл eda-plan-polish.
1506
- # true | false
1507
- review: ${settings.plan.review !== false ? 'true' : 'false'}
1508
- # Задаёт размер плана.
1142
+ # Размер плана.
1509
1143
  # normal | short | ask_each_time
1510
1144
  size: ${settings.plan.size}
1511
- # Определяет, как eda-plan принимает существенные решения.
1512
- # autonomous | recommend_and_ask | ask_each_time
1145
+ # Как eda-plan принимает существенные решения.
1146
+ # recommend_and_ask | autonomous | ask_each_time
1513
1147
  decision_mode: ${settings.plan.decisionMode}
1514
- # Задаёт стратегию тестов и автоматических проверок.
1515
- # after_each_phase | phase_tests_final_checks | tdd_each_phase | end_of_plan | ask_each_time
1148
+ # Когда писать тесты и запускать автоматические проверки.
1149
+ # phase_tests_final_checks | tdd_each_phase | after_each_phase | end_of_plan | ask_each_time
1516
1150
  test_strategy: ${settings.plan.testStrategy}
1517
- # Задаёт стратегию логирования.
1518
- # debug_precise | standard | ask_each_time
1151
+ # Стратегия логирования.
1152
+ # standard | debug_precise | ask_each_time
1519
1153
  logging_strategy: ${settings.plan.loggingStrategy}
1154
+ # Включает кросс-CLI ревью в eda-plan.
1155
+ # false | true
1156
+ strict: ${settings.plan.strict ? 'true' : 'false'}
1520
1157
 
1521
1158
  plan-review:
1522
1159
  # Доля закрытых пунктов чек-листа, при которой план считается готовым.
@@ -1530,9 +1167,21 @@ plan-polish:
1530
1167
 
1531
1168
  plan-execute:
1532
1169
  # Где eda-plan-execute выполняет фазы плана.
1533
- # subagents — каждая фаза в отдельном субагенте, main — весь план одним контекстом, auto — решает по плану.
1170
+ # main — весь план одним контекстом, auto — решает по плану, subagents — каждая фаза в отдельном субагенте.
1534
1171
  mode: ${settings.planExecute.mode}
1535
1172
 
1173
+ review:
1174
+ # Где eda-review выполняет выбранные проверки.
1175
+ # main — все проверки основным агентом без субагентов, subagents — разведчик и отдельный агент на проверку.
1176
+ execution: ${settings.review.execution ?? 'main'}
1177
+ # Какие проверки нужны, решает eda-review-triage по содержимому ревью.
1178
+ # Модель роли в subagents: default — модель установленного агента, иначе provider/model#variant,
1179
+ # например anthropic/claude-opus-5-5#high или deepseek/deepseek-flash-4.1; variant задаёт effort.
1180
+ # Если среда не может запустить модель, роль идёт на модели агента с предупреждением в ревью.
1181
+ # В main все проверки выполняет текущая модель.
1182
+ agents:
1183
+ ${reviewAgents}
1184
+
1536
1185
  polish:
1537
1186
  # Где eda-polish выполняет review и исправления цикла полировки.
1538
1187
  # subagents — каждое review и каждое исправление в отдельном изолированном субагенте, main — оба контракта основным агентом.
@@ -1545,14 +1194,6 @@ manual-test:
1545
1194
  # full | smoke | ask_each_time
1546
1195
  depth: ${settings.manualTest.depth}
1547
1196
 
1548
- review:
1549
- # Где eda-review выполняет выбранные проверки.
1550
- # subagents — отдельный специализированный агент на проверку, main — все проверки основным агентом без субагентов.
1551
- execution: ${settings.review.execution ?? 'subagents'}
1552
- # Каждая проверка имеет собственный режим запуска; модели применяются только в subagents.
1553
- agents:
1554
- ${reviewAgents}
1555
-
1556
1197
  send-review:
1557
1198
  # После успешной отправки скрывает предыдущие сводки eda-send-review и резолвит их inline-треды.
1558
1199
  # true | false
@@ -1560,8 +1201,24 @@ send-review:
1560
1201
 
1561
1202
  discover-automations:
1562
1203
  # Добавляет docs/artifacts/plans/ в обычный запуск eda-discover-automations.
1563
- # true | false
1204
+ # false | true
1564
1205
  include_plans: ${settings.discoverAutomations.includePlans ? 'true' : 'false'}
1206
+
1207
+ orhestra:
1208
+ # Режим полного цикла eda-orhestra.
1209
+ # automatic | manual
1210
+ mode: ${orhestra.mode ?? 'automatic'}
1211
+ # Где выполняются этапы и передаётся ли main совместимым вложенным скилам.
1212
+ # subagents | main | main_propagated
1213
+ execution: ${orhestra.execution ?? 'subagents'}
1214
+ # Упорядоченная цепочка. Шаги можно отключать, удалять и переставлять.
1215
+ steps:
1216
+ ${orhestraSteps}
1217
+
1218
+ aim:
1219
+ # Режим ответов на рабочие вопросы eda-aim.
1220
+ # automatic | manual
1221
+ mode: ${aim.mode ?? 'automatic'}
1565
1222
  `;
1566
1223
  }
1567
1224
 
@@ -1592,10 +1249,9 @@ async function installTarget(cwd, target, skills, agents, packageVersion, output
1592
1249
 
1593
1250
  if (agents.length > 0) await fs.mkdir(agentsDir, { recursive: true });
1594
1251
  for (const agent of agents) {
1595
- const extension = target === 'claude' ? '.md' : '.toml';
1596
- const rendered = target === 'claude'
1597
- ? renderClaudeAgent(agent, agent.prompt)
1598
- : renderCodexAgent(agent, agent.prompt);
1252
+ const extension = target === 'codex' ? '.toml' : '.md';
1253
+ const renderAgent = { claude: renderClaudeAgent, codex: renderCodexAgent, opencode: renderOpenCodeAgent }[target];
1254
+ const rendered = renderAgent(agent, agent.prompt);
1599
1255
  const changed = await writeManagedFile(path.join(agentsDir, `${agent.name}${extension}`), rendered);
1600
1256
  if (changed) changedAgents.push(agent.name);
1601
1257
  }
@@ -1611,7 +1267,7 @@ async function installTarget(cwd, target, skills, agents, packageVersion, output
1611
1267
  agents: agents.map(agent => agent.name)
1612
1268
  });
1613
1269
 
1614
- const label = target === 'claude' ? 'Claude Code' : 'Codex CLI';
1270
+ const label = TARGET_CHOICES.find(choice => choice.value === target).label;
1615
1271
  const location = skillsDir === path.join(targetRoot, 'skills')
1616
1272
  ? targetRoot
1617
1273
  : `${skillsDir}; ${agentsDir}`;
@@ -1637,10 +1293,9 @@ async function buildSkillFiles(skill, target) {
1637
1293
  const entrypoint = files.get('SKILL.md');
1638
1294
  if (!entrypoint) throw new Error(`У скила ${skill.name} нет SKILL.md.`);
1639
1295
 
1640
- const content = entrypoint.toString('utf8');
1641
- const rendered = target === 'claude'
1642
- ? renderClaudeSkill(content, skill.config)
1643
- : renderCodexSkill(content, skill.config);
1296
+ const content = await expandIncludes(`Скил ${skill.name}`, entrypoint.toString('utf8'), PACKAGE_ROOT);
1297
+ const renderSkill = { claude: renderClaudeSkill, codex: renderCodexSkill, opencode: renderOpenCodeSkill }[target];
1298
+ const rendered = renderSkill(content, skill.config);
1644
1299
  files.set('SKILL.md', Buffer.from(rendered));
1645
1300
  return files;
1646
1301
  }
@@ -1828,7 +1483,7 @@ async function removeRetiredComponents(skillsDir, agentsDir, target, previousMan
1828
1483
  await fs.rm(path.join(skillsDir, `${skillName}.md`), { force: true });
1829
1484
  }
1830
1485
 
1831
- const extension = target === 'claude' ? '.md' : '.toml';
1486
+ const extension = target === 'codex' ? '.toml' : '.md';
1832
1487
  for (const agentName of staleAgents) {
1833
1488
  await fs.rm(path.join(agentsDir, `${agentName}${extension}`), { force: true });
1834
1489
  }