@gian-tiaga/eda 1.0.4 → 2.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 (48) hide show
  1. package/MIGRATION.md +58 -80
  2. package/README.md +116 -33
  3. package/agents/eda-plan-review-api/agent.json +9 -0
  4. package/agents/eda-plan-review-api/prompt.md +34 -0
  5. package/agents/eda-plan-review-architecture/agent.json +9 -0
  6. package/agents/eda-plan-review-architecture/prompt.md +34 -0
  7. package/agents/eda-plan-review-database/agent.json +9 -0
  8. package/agents/eda-plan-review-database/prompt.md +34 -0
  9. package/agents/eda-plan-review-execution/agent.json +9 -0
  10. package/agents/eda-plan-review-execution/prompt.md +34 -0
  11. package/agents/eda-plan-review-feasibility/agent.json +9 -0
  12. package/agents/eda-plan-review-feasibility/prompt.md +34 -0
  13. package/agents/eda-plan-review-frontend/agent.json +9 -0
  14. package/agents/eda-plan-review-frontend/prompt.md +34 -0
  15. package/agents/eda-plan-review-performance/agent.json +9 -0
  16. package/agents/eda-plan-review-performance/prompt.md +34 -0
  17. package/agents/eda-plan-review-previous/agent.json +9 -0
  18. package/agents/eda-plan-review-previous/prompt.md +28 -0
  19. package/agents/eda-plan-review-requirements/agent.json +9 -0
  20. package/agents/eda-plan-review-requirements/prompt.md +34 -0
  21. package/agents/eda-plan-review-rules/agent.json +9 -0
  22. package/agents/eda-plan-review-rules/prompt.md +34 -0
  23. package/agents/eda-plan-review-security/agent.json +9 -0
  24. package/agents/eda-plan-review-security/prompt.md +34 -0
  25. package/agents/eda-plan-review-verification/agent.json +9 -0
  26. package/agents/eda-plan-review-verification/prompt.md +34 -0
  27. package/agents/eda-review-business/agent.json +15 -0
  28. package/agents/eda-review-business/prompt.md +46 -0
  29. package/bin/cli.js +2 -2
  30. package/lib/install.js +601 -298
  31. package/package.json +3 -2
  32. package/skills/eda-aim/SKILL.md +1 -1
  33. package/skills/eda-business/SKILL.md +147 -0
  34. package/skills/eda-discover-automations/SKILL.md +4 -2
  35. package/skills/eda-explore/SKILL.md +7 -6
  36. package/skills/eda-fix/SKILL.md +5 -4
  37. package/skills/eda-fix-by-review/SKILL.md +5 -4
  38. package/skills/eda-manual-test/SKILL.md +13 -5
  39. package/skills/eda-orhestra/SKILL.md +13 -10
  40. package/skills/eda-plan/SKILL.md +46 -38
  41. package/skills/eda-plan-execute/SKILL.md +8 -6
  42. package/skills/eda-plan-polish/SKILL.md +53 -169
  43. package/skills/eda-plan-review/SKILL.md +188 -0
  44. package/skills/eda-plan-review-fix/SKILL.md +105 -0
  45. package/skills/eda-prepare-ai/SKILL.md +7 -2
  46. package/skills/eda-review/SKILL.md +13 -5
  47. package/skills/eda-roadmap/SKILL.md +6 -3
  48. package/skills/eda-send-review/SKILL.md +1 -1
package/lib/install.js CHANGED
@@ -4,6 +4,7 @@ import { randomUUID } from 'node:crypto';
4
4
  import fs from 'node:fs/promises';
5
5
  import path from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
+ import { parse as parseYaml } from 'yaml';
7
8
 
8
9
  import { listAgents, listSkills } from './catalog.js';
9
10
  import { renderClaudeAgent } from './renderers/claude-agent.js';
@@ -36,6 +37,7 @@ const RETIRED_AGENTS = [
36
37
  'eda-commit-context'
37
38
  ];
38
39
  const MANIFEST_FILE = 'eda-manifest.json';
40
+ const SETTINGS_VERSION = 3;
39
41
  const REVIEW_AGENT_DEFINITIONS = [
40
42
  {
41
43
  key: 'correctness',
@@ -69,6 +71,14 @@ const REVIEW_AGENT_DEFINITIONS = [
69
71
  claude: 'haiku',
70
72
  codex: 'gpt-5.6-luna'
71
73
  },
74
+ {
75
+ key: 'business',
76
+ description: 'Проверяет изменённое поведение по применимым карточкам из docs/business/.',
77
+ auto: 'В auto запускается только когда изменения затрагивают поведение с применимой business-карточкой.',
78
+ mode: 'auto',
79
+ claude: 'sonnet',
80
+ codex: 'gpt-5.6-terra'
81
+ },
72
82
  {
73
83
  key: 'plan_alignment',
74
84
  description: 'Проверяет, что реализация соответствует указанному или однозначно найденному плану.',
@@ -160,11 +170,126 @@ const DEFAULT_REVIEW_AGENTS = Object.fromEntries(REVIEW_AGENT_DEFINITIONS.map(ag
160
170
  }
161
171
  }
162
172
  ]));
173
+ const PLAN_REVIEW_AGENT_DEFINITIONS = [
174
+ {
175
+ key: 'requirements',
176
+ description: 'Проверяет соответствие плана задаче, scope, критериям готовности и применимым business-требованиям.',
177
+ auto: 'Эта базовая проверка по умолчанию запускается всегда.',
178
+ mode: 'always',
179
+ claude: 'sonnet',
180
+ codex: 'gpt-5.6-terra'
181
+ },
182
+ {
183
+ key: 'rules',
184
+ description: 'Проверяет соблюдение AGENTS.md, CLAUDE.md и docs/rules.md.',
185
+ auto: 'Эта базовая проверка по умолчанию запускается всегда.',
186
+ mode: 'always',
187
+ claude: 'haiku',
188
+ codex: 'gpt-5.6-luna'
189
+ },
190
+ {
191
+ key: 'architecture',
192
+ description: 'Проверяет архитектурные границы, зависимости и соответствие docs/arch.md.',
193
+ auto: 'Эта базовая проверка по умолчанию запускается всегда.',
194
+ mode: 'always',
195
+ claude: 'opus',
196
+ codex: 'gpt-5.6-sol'
197
+ },
198
+ {
199
+ key: 'feasibility',
200
+ description: 'Проверяет реализуемость плана относительно фактического кода и отсутствие скрытого исследования.',
201
+ auto: 'Эта базовая проверка по умолчанию запускается всегда.',
202
+ mode: 'always',
203
+ claude: 'opus',
204
+ codex: 'gpt-5.6-sol'
205
+ },
206
+ {
207
+ key: 'execution',
208
+ description: 'Проверяет связность фаз, ID задач, зависимости и размер работы для одного исполнителя.',
209
+ auto: 'Эта базовая проверка по умолчанию запускается всегда.',
210
+ mode: 'always',
211
+ claude: 'sonnet',
212
+ codex: 'gpt-5.6-terra'
213
+ },
214
+ {
215
+ key: 'verification',
216
+ description: 'Проверяет тесты, команды проверок, ошибки, логирование, документацию и эксплуатационную готовность.',
217
+ auto: 'Эта базовая проверка по умолчанию запускается всегда.',
218
+ mode: 'always',
219
+ claude: 'sonnet',
220
+ codex: 'gpt-5.6-terra'
221
+ },
222
+ {
223
+ key: 'api',
224
+ description: 'Проверяет API, DTO, события, очереди, внешние интеграции и совместимость контрактов.',
225
+ auto: 'В auto запускается при изменении API, событий, очередей или внешних интеграций.',
226
+ mode: 'auto',
227
+ claude: 'sonnet',
228
+ codex: 'gpt-5.6-terra'
229
+ },
230
+ {
231
+ key: 'database',
232
+ description: 'Проверяет схему данных, миграции, backfill, индексы, транзакции и rollback.',
233
+ auto: 'В auto запускается при изменении хранения, моделей, SQL, схем или миграций.',
234
+ mode: 'auto',
235
+ claude: 'opus',
236
+ codex: 'gpt-5.6-sol'
237
+ },
238
+ {
239
+ key: 'security',
240
+ description: 'Проверяет auth, permissions, внешний ввод, секреты, персональные данные и опасные операции.',
241
+ auto: 'В auto запускается для auth, permissions, внешнего ввода, секретов и чувствительных данных.',
242
+ mode: 'auto',
243
+ claude: 'opus',
244
+ codex: 'gpt-5.6-sol'
245
+ },
246
+ {
247
+ key: 'frontend',
248
+ description: 'Проверяет пользовательский сценарий, состояния UI, ошибки, accessibility и адаптивность.',
249
+ auto: 'В auto запускается для компонентов, стилей, клиентских маршрутов и состояния UI.',
250
+ mode: 'auto',
251
+ claude: 'sonnet',
252
+ codex: 'gpt-5.6-terra'
253
+ },
254
+ {
255
+ key: 'performance',
256
+ description: 'Проверяет нагрузку, запросы, конкурентность, кеши и горячие пути.',
257
+ auto: 'В auto запускается для запросов, пакетной обработки, кешей, конкурентности и горячих путей.',
258
+ mode: 'auto',
259
+ claude: 'opus',
260
+ codex: 'gpt-5.6-sol'
261
+ },
262
+ {
263
+ key: 'previous',
264
+ description: 'Проверяет закрытие findings предыдущей связанной итерации plan-review.',
265
+ auto: 'В auto запускается только при наличии связанной цепочки review/fix для текущего SHA-256 плана.',
266
+ mode: 'auto',
267
+ claude: 'haiku',
268
+ codex: 'gpt-5.6-luna'
269
+ }
270
+ ];
271
+ const DEFAULT_PLAN_REVIEW_AGENTS = Object.fromEntries(PLAN_REVIEW_AGENT_DEFINITIONS.map(agent => [
272
+ agent.key,
273
+ {
274
+ mode: agent.mode,
275
+ model: {
276
+ claude: agent.claude,
277
+ codex: agent.codex
278
+ }
279
+ }
280
+ ]));
163
281
  const ORHESTRA_STEP_DEFINITIONS = [
164
282
  {
165
283
  id: 'plan',
166
284
  skill: 'eda-plan',
167
- name: 'Планирование через eda-plan',
285
+ name: 'Планирование без проверок через eda-plan',
286
+ enabled: true,
287
+ args: 'без проверок'
288
+ },
289
+ {
290
+ id: 'plan-polish',
291
+ skill: 'eda-plan-polish',
292
+ name: 'Полировка плана через eda-plan-polish',
168
293
  enabled: true,
169
294
  args: ''
170
295
  },
@@ -191,7 +316,7 @@ const ORHESTRA_STEP_DEFINITIONS = [
191
316
  onFailure: {
192
317
  skill: 'eda-fix',
193
318
  args: '',
194
- then: ['polish', 'manual-test'],
319
+ then: ['manual-test'],
195
320
  maxCycles: 5
196
321
  }
197
322
  }
@@ -211,13 +336,18 @@ const DEFAULT_SETTINGS = {
211
336
  },
212
337
  plan: {
213
338
  strict: false,
339
+ review: true,
214
340
  size: 'normal',
215
341
  decisionMode: 'recommend_and_ask',
216
342
  testStrategy: 'ask_each_time',
217
343
  loggingStrategy: 'ask_each_time'
218
344
  },
345
+ planReview: {
346
+ threshold: 95,
347
+ agents: DEFAULT_PLAN_REVIEW_AGENTS
348
+ },
219
349
  planPolish: {
220
- strict: false
350
+ limit: 3
221
351
  },
222
352
  review: {
223
353
  agents: DEFAULT_REVIEW_AGENTS
@@ -229,28 +359,49 @@ const DEFAULT_SETTINGS = {
229
359
  includePlans: false
230
360
  }
231
361
  };
362
+ const SETTINGS_SECTION_NAMES = [
363
+ 'orhestra',
364
+ 'aim',
365
+ 'explore',
366
+ 'plan',
367
+ 'plan-review',
368
+ 'plan-polish',
369
+ 'review',
370
+ 'send-review',
371
+ 'discover-automations'
372
+ ];
232
373
  const SETTINGS_CHOICES = [
233
374
  {
375
+ section: 'explore',
376
+ key: 'explore.strict',
234
377
  value: 'exploreStrict',
235
378
  name: 'Strict по умолчанию для eda-explore',
236
379
  checked: DEFAULT_SETTINGS.explore.strict
237
380
  },
238
381
  {
382
+ section: 'plan',
383
+ key: 'plan.strict',
239
384
  value: 'planStrict',
240
385
  name: 'Strict по умолчанию для eda-plan',
241
386
  checked: DEFAULT_SETTINGS.plan.strict
242
387
  },
243
388
  {
244
- value: 'planPolishStrict',
245
- name: 'Strict по умолчанию для eda-plan-polish',
246
- checked: DEFAULT_SETTINGS.planPolish.strict
389
+ section: 'plan',
390
+ key: 'plan.review',
391
+ value: 'planReview',
392
+ name: 'Запускать eda-plan-review внутри eda-plan',
393
+ checked: DEFAULT_SETTINGS.plan.review
247
394
  },
248
395
  {
396
+ section: 'send-review',
397
+ key: 'sendReview.closePreviousReviews',
249
398
  value: 'sendReviewClosePreviousReviews',
250
399
  name: 'Закрывать предыдущие ревью eda-send-review',
251
400
  checked: DEFAULT_SETTINGS.sendReview.closePreviousReviews
252
401
  },
253
402
  {
403
+ section: 'discover-automations',
404
+ key: 'discoverAutomations.includePlans',
254
405
  value: 'discoverAutomationsIncludePlans',
255
406
  name: 'Анализировать планы в eda-discover-automations по умолчанию',
256
407
  checked: DEFAULT_SETTINGS.discoverAutomations.includePlans
@@ -280,6 +431,14 @@ const ORHESTRA_POLISH_LIMIT_CHOICES = [1, 2, 3, 5, 10].map(value => ({
280
431
  value,
281
432
  name: `${value} ${value === 1 ? 'итерация' : value < 5 ? 'итерации' : 'итераций'}`
282
433
  }));
434
+ const PLAN_REVIEW_THRESHOLD_CHOICES = [90, 95, 100].map(value => ({
435
+ value,
436
+ name: `${value}/100`
437
+ }));
438
+ const PLAN_POLISH_LIMIT_CHOICES = [1, 2, 3, 5, 10].map(value => ({
439
+ value,
440
+ name: `${value} ${value === 1 ? 'итерация' : value < 5 ? 'итерации' : 'итераций'}`
441
+ }));
283
442
  const PLAN_SIZE_CHOICES = [
284
443
  {
285
444
  value: 'normal',
@@ -355,6 +514,16 @@ const CODEX_REVIEW_MODEL_CHOICES = [
355
514
  { value: 'gpt-5.6-terra', name: 'gpt-5.6-terra — сильная проверка кода' },
356
515
  { value: 'gpt-5.6-sol', name: 'gpt-5.6-sol — максимальная глубина' }
357
516
  ];
517
+ const UPDATE_ALL_SETTINGS_MODE_CHOICES = [
518
+ {
519
+ value: 'configure',
520
+ name: 'Настроить один общий профиль и записать его во все проекты'
521
+ },
522
+ {
523
+ value: 'skip',
524
+ name: 'Skip — сохранить полный v3, остальные конфиги перенести с defaults'
525
+ }
526
+ ];
358
527
 
359
528
  export async function init({ cwd, input = process.stdin, output = process.stdout }) {
360
529
  const targets = await askTargets({ input, output });
@@ -381,7 +550,8 @@ export async function updateAll({
381
550
  root = process.cwd(),
382
551
  input = process.stdin,
383
552
  output = process.stdout,
384
- maxDepth = UPDATE_ALL_MAX_DEPTH
553
+ maxDepth = UPDATE_ALL_MAX_DEPTH,
554
+ settingsMode
385
555
  } = {}) {
386
556
  if (!Number.isInteger(maxDepth) || maxDepth < 0) {
387
557
  throw new Error('Глубина поиска должна быть неотрицательным целым числом.');
@@ -409,9 +579,20 @@ export async function updateAll({
409
579
  }
410
580
 
411
581
  output.write(`Найдено ${formatProjectCount(projects.length)}: ${projects.map(project => formatProjectPath(rootDir, project)).join(', ')}\n`);
412
- output.write('Настройки будут запрошены один раз и записаны одинаково во все найденные проекты.\n');
413
- const sharedSettings = await askSettings({ input, output });
414
- const sharedSettingsContent = formatSettings(sharedSettings);
582
+ const resolvedSettingsMode = settingsMode ?? await askUpdateAllSettingsMode({ input, output });
583
+ if (!['configure', 'skip'].includes(resolvedSettingsMode)) {
584
+ throw new Error(`Неизвестный режим настроек update-all: ${resolvedSettingsMode}.`);
585
+ }
586
+
587
+ let sharedSettingsContent;
588
+ if (resolvedSettingsMode === 'configure') {
589
+ output.write('Настройки будут запрошены один раз и записаны одинаково во все найденные проекты.\n');
590
+ const sharedSettings = await askSettings({ input, output });
591
+ sharedSettingsContent = formatSettings(sharedSettings);
592
+ } else {
593
+ output.write(`Режим skip: полный version: ${SETTINGS_VERSION} сохраняется, старые конфиги переносятся, отсутствующие создаются с defaults.\n`);
594
+ sharedSettingsContent = formatSettings(structuredClone(DEFAULT_SETTINGS));
595
+ }
415
596
 
416
597
  const updatedProjects = [];
417
598
  const skippedProjects = [];
@@ -430,8 +611,22 @@ export async function updateAll({
430
611
  }
431
612
 
432
613
  output.write(`Найдены установленные среды: ${targets.join(', ')}\n`);
433
- await writeSettingsContent(projectDir, sharedSettingsContent);
434
- output.write(`Записан общий файл настроек: ${SETTINGS_RELATIVE_PATH}\n`);
614
+ const projectSettingsPath = path.join(projectDir, SETTINGS_RELATIVE_PATH);
615
+ if (resolvedSettingsMode === 'skip' && await fileExists(projectSettingsPath)) {
616
+ const existingContent = await fs.readFile(projectSettingsPath, 'utf8');
617
+ const migrated = migrateSettingsContent(existingContent);
618
+ if (migrated.version === SETTINGS_VERSION && migrated.missingKeys.size === 0) {
619
+ output.write(`Полный файл настроек сохранён: ${SETTINGS_RELATIVE_PATH}\n`);
620
+ } else {
621
+ await writeSettingsContent(projectDir, formatSettings(migrated.settings));
622
+ output.write(`Старый или неполный файл настроек перенесён на version: ${SETTINGS_VERSION}: ${SETTINGS_RELATIVE_PATH}\n`);
623
+ }
624
+ } else {
625
+ await writeSettingsContent(projectDir, sharedSettingsContent);
626
+ output.write(resolvedSettingsMode === 'skip'
627
+ ? `Создан файл настроек с defaults: ${SETTINGS_RELATIVE_PATH}\n`
628
+ : `Записан общий файл настроек: ${SETTINGS_RELATIVE_PATH}\n`);
629
+ }
435
630
  const result = await syncPackage(projectDir, targets, output, {
436
631
  action: 'update',
437
632
  writeDone: false
@@ -459,6 +654,7 @@ export async function updateAll({
459
654
  return {
460
655
  root: rootDir,
461
656
  maxDepth,
657
+ settingsMode: resolvedSettingsMode,
462
658
  projects,
463
659
  updatedProjects,
464
660
  skippedProjects,
@@ -486,189 +682,211 @@ export async function askTargets({ input = process.stdin, output = process.stdou
486
682
  });
487
683
  }
488
684
 
489
- export async function askSettings({ input = process.stdin, output = process.stdout } = {}) {
685
+ export async function askUpdateAllSettingsMode({
686
+ input = process.stdin,
687
+ output = process.stdout,
688
+ selectPrompt = select
689
+ } = {}) {
490
690
  if (!input.isTTY || !output.isTTY) {
491
- output.write(`Нет интерактивного терминала — использую настройки ${SETTINGS_RELATIVE_PATH} по умолчанию.\n`);
492
- return structuredClone(DEFAULT_SETTINGS);
691
+ output.write(`Нет интерактивного терминала — использую режим skip: сохраняю полный v3, старые конфиги переношу с defaults.\n`);
692
+ return 'skip';
493
693
  }
494
694
 
495
- const selected = await checkbox({
496
- message: 'Какие настройки включить?',
497
- instructions: 'Стрелки — выбрать, Space — отметить, Enter — продолжить',
498
- choices: SETTINGS_CHOICES
695
+ return selectPrompt({
696
+ message: `Как обновить ${SETTINGS_RELATIVE_PATH} в найденных проектах?`,
697
+ choices: UPDATE_ALL_SETTINGS_MODE_CHOICES,
698
+ default: 'configure'
499
699
  }, {
500
700
  input,
501
701
  output
502
702
  });
703
+ }
503
704
 
504
- const orhestraMode = await select({
505
- message: 'Как eda-orhestra должен вести полный цикл по умолчанию?',
506
- choices: ORHESTRA_MODE_CHOICES,
507
- default: DEFAULT_SETTINGS.orhestra.mode
508
- }, {
509
- input,
510
- output
511
- });
705
+ export async function askSettings({
706
+ input = process.stdin,
707
+ output = process.stdout,
708
+ sections = SETTINGS_SECTION_NAMES,
709
+ checkboxPrompt = checkbox,
710
+ selectPrompt = select,
711
+ seedSettings = DEFAULT_SETTINGS,
712
+ missingKeys = null
713
+ } = {}) {
714
+ const settings = structuredClone(seedSettings);
715
+ if (!input.isTTY || !output.isTTY) {
716
+ output.write(`Нет интерактивного терминала — использую перенесённые значения и defaults ${SETTINGS_RELATIVE_PATH}.\n`);
717
+ return settings;
718
+ }
512
719
 
513
- const enabledOrhestraSteps = await checkbox({
514
- message: 'Какие этапы eda-orhestra включить по умолчанию?',
515
- instructions: 'Порядок можно изменить вручную в docs/settings.yaml',
516
- choices: ORHESTRA_STEP_DEFINITIONS.map(step => ({
517
- name: step.name,
518
- value: step.skill,
519
- checked: step.enabled
520
- }))
521
- }, {
522
- input,
523
- output
524
- });
720
+ const requestedSections = new Set(sections);
721
+ const shouldAsk = key => missingKeys === null || missingKeys.has(key);
722
+ const booleanChoices = SETTINGS_CHOICES.filter(choice => (
723
+ requestedSections.has(choice.section) && shouldAsk(choice.key)
724
+ ));
725
+ const selected = booleanChoices.length > 0
726
+ ? await checkboxPrompt({
727
+ message: 'Какие настройки включить?',
728
+ instructions: 'Стрелки — выбрать, Space — отметить, Enter — продолжить',
729
+ choices: booleanChoices.map(({ section, key, ...choice }) => choice)
730
+ }, { input, output })
731
+ : [];
732
+
733
+ if (requestedSections.has('explore') && shouldAsk('explore.strict')) {
734
+ settings.explore.strict = selected.includes('exploreStrict');
735
+ }
736
+ if (requestedSections.has('plan') && shouldAsk('plan.strict')) {
737
+ settings.plan.strict = selected.includes('planStrict');
738
+ }
739
+ if (requestedSections.has('plan') && shouldAsk('plan.review')) {
740
+ settings.plan.review = selected.includes('planReview');
741
+ }
742
+ if (requestedSections.has('send-review') && shouldAsk('sendReview.closePreviousReviews')) {
743
+ settings.sendReview.closePreviousReviews = selected.includes('sendReviewClosePreviousReviews');
744
+ }
745
+ if (requestedSections.has('discover-automations') && shouldAsk('discoverAutomations.includePlans')) {
746
+ settings.discoverAutomations.includePlans = selected.includes('discoverAutomationsIncludePlans');
747
+ }
748
+
749
+ if (requestedSections.has('orhestra') && shouldAsk('orhestra.mode')) {
750
+ settings.orhestra.mode = await selectPrompt({
751
+ message: 'Как eda-orhestra должен вести полный цикл по умолчанию?',
752
+ choices: ORHESTRA_MODE_CHOICES,
753
+ default: settings.orhestra.mode
754
+ }, { input, output });
755
+ }
525
756
 
526
- const polishLimit = enabledOrhestraSteps.includes('eda-polish')
527
- ? await select({
757
+ if (requestedSections.has('orhestra') && shouldAsk('orhestra.steps')) {
758
+ const enabledSteps = await checkboxPrompt({
759
+ message: 'Какие этапы eda-orhestra включить по умолчанию?',
760
+ instructions: 'Порядок можно изменить вручную в docs/settings.yaml',
761
+ choices: ORHESTRA_STEP_DEFINITIONS.map(step => ({
762
+ name: step.name,
763
+ value: step.skill,
764
+ checked: settings.orhestra.steps.find(item => item.skill === step.skill)?.enabled !== false
765
+ }))
766
+ }, { input, output });
767
+ const codePolishLimit = enabledSteps.includes('eda-polish')
768
+ ? await selectPrompt({
528
769
  message: 'Сколько итераций разрешить eda-polish внутри eda-orhestra?',
529
770
  choices: ORHESTRA_POLISH_LIMIT_CHOICES,
530
771
  default: 5
531
- }, {
532
- input,
533
- output
534
- })
535
- : 5;
536
-
537
- const orhestraSteps = DEFAULT_ORHESTRA_STEPS.map(step => ({
538
- ...step,
539
- enabled: enabledOrhestraSteps.includes(step.skill),
540
- ...(step.skill === 'eda-polish' ? { args: `limit ${polishLimit}` } : {})
541
- }));
542
-
543
- const aimMode = await select({
544
- message: 'Как eda-aim должен отвечать на рабочие вопросы по умолчанию?',
545
- choices: AIM_MODE_CHOICES,
546
- default: DEFAULT_SETTINGS.aim.mode
547
- }, {
548
- input,
549
- output
550
- });
551
-
552
- const exploreDecisionMode = await select({
553
- message: 'Как принимать важные решения в eda-explore?',
554
- choices: DECISION_MODE_CHOICES,
555
- default: DEFAULT_SETTINGS.explore.decisionMode
556
- }, {
557
- input,
558
- output
559
- });
560
-
561
- const planSize = await select({
562
- message: 'Какой размер плана eda-plan использовать по умолчанию?',
563
- choices: PLAN_SIZE_CHOICES,
564
- default: DEFAULT_SETTINGS.plan.size
565
- }, {
566
- input,
567
- output
568
- });
772
+ }, { input, output })
773
+ : 5;
774
+ settings.orhestra.steps = DEFAULT_ORHESTRA_STEPS.map(step => ({
775
+ ...step,
776
+ enabled: enabledSteps.includes(step.skill),
777
+ ...(step.skill === 'eda-polish' ? { args: `limit ${codePolishLimit}` } : {})
778
+ }));
779
+ }
569
780
 
570
- const planDecisionMode = await select({
571
- message: 'Как принимать важные решения в eda-plan?',
572
- choices: DECISION_MODE_CHOICES,
573
- default: DEFAULT_SETTINGS.plan.decisionMode
574
- }, {
575
- input,
576
- output
577
- });
781
+ const scalarPrompts = [
782
+ ['aim', 'aim.mode', settings.aim, 'mode', 'Как eda-aim должен отвечать на рабочие вопросы по умолчанию?', AIM_MODE_CHOICES],
783
+ ['explore', 'explore.decisionMode', settings.explore, 'decisionMode', 'Как принимать важные решения в eda-explore?', DECISION_MODE_CHOICES],
784
+ ['plan', 'plan.size', settings.plan, 'size', 'Какой размер плана eda-plan использовать по умолчанию?', PLAN_SIZE_CHOICES],
785
+ ['plan', 'plan.decisionMode', settings.plan, 'decisionMode', 'Как принимать важные решения в eda-plan?', DECISION_MODE_CHOICES],
786
+ ['plan', 'plan.testStrategy', settings.plan, 'testStrategy', 'Какую стратегию тестов eda-plan использовать по умолчанию?', TEST_STRATEGY_CHOICES],
787
+ ['plan', 'plan.loggingStrategy', settings.plan, 'loggingStrategy', 'Какую стратегию логирования eda-plan использовать по умолчанию?', LOGGING_STRATEGY_CHOICES],
788
+ ['plan-review', 'planReview.threshold', settings.planReview, 'threshold', 'Какой порог готовности использовать в eda-plan-review?', PLAN_REVIEW_THRESHOLD_CHOICES],
789
+ ['plan-polish', 'planPolish.limit', settings.planPolish, 'limit', 'Сколько review-итераций разрешить eda-plan-polish?', PLAN_POLISH_LIMIT_CHOICES]
790
+ ];
791
+ for (const [section, key, target, property, message, choices] of scalarPrompts) {
792
+ if (!requestedSections.has(section) || !shouldAsk(key)) continue;
793
+ target[property] = await selectPrompt({ message, choices, default: target[property] }, { input, output });
794
+ }
578
795
 
579
- const testStrategy = await select({
580
- message: 'Какую стратегию тестов eda-plan использовать по умолчанию?',
581
- choices: TEST_STRATEGY_CHOICES,
582
- default: DEFAULT_SETTINGS.plan.testStrategy
583
- }, {
584
- input,
585
- output
586
- });
796
+ if (requestedSections.has('plan-review')) {
797
+ settings.planReview.agents = await askPlanReviewAgentSettings({
798
+ input,
799
+ output,
800
+ selectPrompt,
801
+ seedAgents: settings.planReview.agents,
802
+ missingKeys
803
+ });
804
+ }
805
+ if (requestedSections.has('review')) {
806
+ settings.review.agents = await askReviewAgentSettings({
807
+ input,
808
+ output,
809
+ selectPrompt,
810
+ seedAgents: settings.review.agents,
811
+ missingKeys
812
+ });
813
+ }
814
+ return settings;
815
+ }
587
816
 
588
- const loggingStrategy = await select({
589
- message: 'Какую стратегию логирования eda-plan использовать по умолчанию?',
590
- choices: LOGGING_STRATEGY_CHOICES,
591
- default: DEFAULT_SETTINGS.plan.loggingStrategy
592
- }, {
817
+ export async function askReviewAgentSettings({
818
+ input = process.stdin,
819
+ output = process.stdout,
820
+ selectPrompt = select,
821
+ seedAgents = DEFAULT_REVIEW_AGENTS,
822
+ missingKeys = null
823
+ } = {}) {
824
+ return askAgentSettings({
593
825
  input,
594
- output
826
+ output,
827
+ selectPrompt,
828
+ definitions: REVIEW_AGENT_DEFINITIONS,
829
+ seedAgents,
830
+ missingKeys,
831
+ keyPrefix: 'review.agents',
832
+ label: 'review'
595
833
  });
596
-
597
- const reviewAgents = await askReviewAgentSettings({ input, output });
598
-
599
- return {
600
- orhestra: {
601
- mode: orhestraMode,
602
- steps: orhestraSteps
603
- },
604
- aim: {
605
- mode: aimMode
606
- },
607
- explore: {
608
- strict: selected.includes('exploreStrict'),
609
- decisionMode: exploreDecisionMode
610
- },
611
- plan: {
612
- strict: selected.includes('planStrict'),
613
- size: planSize,
614
- decisionMode: planDecisionMode,
615
- testStrategy,
616
- loggingStrategy
617
- },
618
- planPolish: {
619
- strict: selected.includes('planPolishStrict')
620
- },
621
- review: {
622
- agents: reviewAgents
623
- },
624
- sendReview: {
625
- closePreviousReviews: selected.includes('sendReviewClosePreviousReviews')
626
- },
627
- discoverAutomations: {
628
- includePlans: selected.includes('discoverAutomationsIncludePlans')
629
- }
630
- };
631
834
  }
632
835
 
633
- export async function askReviewAgentSettings({
836
+ export async function askPlanReviewAgentSettings({
634
837
  input = process.stdin,
635
838
  output = process.stdout,
636
- selectPrompt = select
839
+ selectPrompt = select,
840
+ seedAgents = DEFAULT_PLAN_REVIEW_AGENTS,
841
+ missingKeys = null
637
842
  } = {}) {
638
- const reviewAgents = {};
639
- for (const agent of REVIEW_AGENT_DEFINITIONS) {
640
- const mode = await selectPrompt({
641
- message: `Когда запускать review-проверку ${agent.key}?`,
642
- choices: REVIEW_MODE_CHOICES,
643
- default: agent.mode
644
- }, {
645
- input,
646
- output
647
- });
843
+ return askAgentSettings({
844
+ input,
845
+ output,
846
+ selectPrompt,
847
+ definitions: PLAN_REVIEW_AGENT_DEFINITIONS,
848
+ seedAgents,
849
+ missingKeys,
850
+ keyPrefix: 'planReview.agents',
851
+ label: 'plan-review'
852
+ });
853
+ }
648
854
 
649
- let claude = agent.claude;
650
- let codex = agent.codex;
651
- if (mode !== 'off') {
855
+ async function askAgentSettings({ input, output, selectPrompt, definitions, seedAgents, missingKeys, keyPrefix, label }) {
856
+ const configuredAgents = structuredClone(seedAgents);
857
+ const shouldAsk = key => missingKeys === null || missingKeys.has(key);
858
+ for (const agent of definitions) {
859
+ const modeKey = `${keyPrefix}.${agent.key}.mode`;
860
+ const claudeKey = `${keyPrefix}.${agent.key}.model.claude`;
861
+ const codexKey = `${keyPrefix}.${agent.key}.model.codex`;
862
+ let mode = configuredAgents[agent.key].mode;
863
+ if (shouldAsk(modeKey)) {
864
+ mode = await selectPrompt({
865
+ message: `Когда запускать ${label}-проверку ${agent.key}?`,
866
+ choices: REVIEW_MODE_CHOICES,
867
+ default: mode
868
+ }, { input, output });
869
+ }
870
+
871
+ let claude = configuredAgents[agent.key].model.claude;
872
+ let codex = configuredAgents[agent.key].model.codex;
873
+ if (mode !== 'off' && shouldAsk(claudeKey)) {
652
874
  claude = await selectPrompt({
653
875
  message: `Какой моделью Claude проверять ${agent.key}?`,
654
876
  choices: CLAUDE_REVIEW_MODEL_CHOICES,
655
- default: agent.claude
656
- }, {
657
- input,
658
- output
659
- });
877
+ default: claude
878
+ }, { input, output });
879
+ }
880
+ if (mode !== 'off' && shouldAsk(codexKey)) {
660
881
  codex = await selectPrompt({
661
882
  message: `Какой моделью Codex проверять ${agent.key}?`,
662
883
  choices: CODEX_REVIEW_MODEL_CHOICES,
663
- default: agent.codex
664
- }, {
665
- input,
666
- output
667
- });
884
+ default: codex
885
+ }, { input, output });
668
886
  }
669
- reviewAgents[agent.key] = { mode, model: { claude, codex } };
887
+ configuredAgents[agent.key] = { mode, model: { claude, codex } };
670
888
  }
671
- return reviewAgents;
889
+ return configuredAgents;
672
890
  }
673
891
 
674
892
  async function detectTargets(cwd) {
@@ -793,19 +1011,58 @@ async function findInstalledProjects(rootDir, maxDepth) {
793
1011
  }
794
1012
 
795
1013
  async function ensureSettings(cwd, { input = process.stdin, output = process.stdout } = {}) {
796
- const existing = await migrateSettingsIfNeeded(cwd, { output });
797
- if (existing.exists) return;
1014
+ const settingsPath = path.join(cwd, SETTINGS_RELATIVE_PATH);
1015
+ const existingContent = await readFileIfExists(settingsPath);
1016
+ if (existingContent === null) {
1017
+ const settings = await askSettings({ input, output });
1018
+ await writeSettingsContent(cwd, formatSettings(settings));
1019
+ output.write(`Создан файл настроек: ${SETTINGS_RELATIVE_PATH}\n`);
1020
+ return;
1021
+ }
798
1022
 
799
- const settings = await askSettings({ input, output });
1023
+ const migrated = migrateSettingsContent(existingContent);
1024
+ if (migrated.version === SETTINGS_VERSION && migrated.missingKeys.size === 0) {
1025
+ output.write(`Настройки уже есть: ${SETTINGS_RELATIVE_PATH}\n`);
1026
+ return;
1027
+ }
1028
+
1029
+ output.write(`Переношу ${SETTINGS_RELATIVE_PATH} на version: ${SETTINGS_VERSION}; спрашиваются только отсутствующие значения.\n`);
1030
+ const settings = await askSettings({
1031
+ input,
1032
+ output,
1033
+ seedSettings: migrated.settings,
1034
+ missingKeys: migrated.missingKeys
1035
+ });
800
1036
  await writeSettingsContent(cwd, formatSettings(settings));
801
- output.write(`Создан файл настроек: ${SETTINGS_RELATIVE_PATH}\n`);
1037
+ output.write(`Переписан файл настроек: ${SETTINGS_RELATIVE_PATH} → version: ${SETTINGS_VERSION}.\n`);
802
1038
  }
803
1039
 
804
1040
  async function configureSettings(cwd, { input = process.stdin, output = process.stdout } = {}) {
805
- output.write(`Настраиваю ${SETTINGS_RELATIVE_PATH} заново.\n`);
806
- const settings = await askSettings({ input, output });
1041
+ const settingsPath = path.join(cwd, SETTINGS_RELATIVE_PATH);
1042
+ const existingContent = await readFileIfExists(settingsPath);
1043
+ if (existingContent === null) {
1044
+ output.write(`Настраиваю ${SETTINGS_RELATIVE_PATH} заново.\n`);
1045
+ const settings = await askSettings({ input, output });
1046
+ await writeSettingsContent(cwd, formatSettings(settings));
1047
+ output.write(`Записан файл настроек: ${SETTINGS_RELATIVE_PATH}\n`);
1048
+ return;
1049
+ }
1050
+
1051
+ const migrated = migrateSettingsContent(existingContent);
1052
+ if (migrated.version === SETTINGS_VERSION && migrated.missingKeys.size === 0) {
1053
+ output.write(`Настройки ${SETTINGS_RELATIVE_PATH} версии ${SETTINGS_VERSION} полные — вопросы не требуются.\n`);
1054
+ return;
1055
+ }
1056
+
1057
+ output.write(`Переношу ${SETTINGS_RELATIVE_PATH} на version: ${SETTINGS_VERSION}; спрашиваются только отсутствующие значения.\n`);
1058
+ const settings = await askSettings({
1059
+ input,
1060
+ output,
1061
+ seedSettings: migrated.settings,
1062
+ missingKeys: migrated.missingKeys
1063
+ });
807
1064
  await writeSettingsContent(cwd, formatSettings(settings));
808
- output.write(`Записан файл настроек: ${SETTINGS_RELATIVE_PATH}\n`);
1065
+ output.write(`Переписан файл настроек: ${SETTINGS_RELATIVE_PATH} → version: ${SETTINGS_VERSION}.\n`);
809
1066
  }
810
1067
 
811
1068
  async function writeSettingsContent(cwd, content) {
@@ -818,144 +1075,173 @@ async function writeSettingsContent(cwd, content) {
818
1075
  await fs.writeFile(settingsPath, content);
819
1076
  }
820
1077
 
821
- async function migrateSettingsIfNeeded(cwd, {
822
- output = process.stdout,
823
- reportExisting = true
824
- } = {}) {
825
- const settingsPath = path.join(cwd, SETTINGS_RELATIVE_PATH);
826
- const content = await readFileIfExists(settingsPath);
827
- if (content === null) return { exists: false, migrated: false };
828
-
829
- const version = readTopLevelYamlScalar(content, 'version');
830
- if (version !== '1') {
831
- if (reportExisting) {
832
- output.write(`Настройки уже есть: ${SETTINGS_RELATIVE_PATH}\n`);
833
- if (version !== '2') {
834
- output.write('Автомиграция не выполнена: безопасно поддерживается только переход version: 1 → version: 2.\n');
835
- }
836
- }
837
- return { exists: true, migrated: false, version };
1078
+ function migrateSettingsContent(content) {
1079
+ let raw = {};
1080
+ try {
1081
+ const parsed = parseYaml(content);
1082
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) raw = parsed;
1083
+ } catch {
1084
+ // Некорректный YAML пересобирается из defaults; в TTY все отсутствующие значения будут запрошены.
838
1085
  }
839
1086
 
840
- const migratedSettings = migrateSettingsV1(content);
841
- await writeFileAtomically(settingsPath, formatSettings(migratedSettings));
842
- output.write(`Мигрированы настройки ${SETTINGS_RELATIVE_PATH}: version: 1 → version: 2.\n`);
843
- return { exists: true, migrated: true, version: '2' };
844
- }
1087
+ const settings = structuredClone(DEFAULT_SETTINGS);
1088
+ const missingKeys = new Set();
1089
+ const transfer = (key, value, validator, setter) => {
1090
+ if (validator(value)) setter(value);
1091
+ else missingKeys.add(key);
1092
+ };
1093
+ const oneOf = values => value => values.includes(value);
1094
+ const bool = value => typeof value === 'boolean';
1095
+ const positiveInt = value => Number.isInteger(value) && value > 0;
1096
+ const score = value => Number.isInteger(value) && value >= 1 && value <= 100;
1097
+
1098
+ const legacyDefaults = objectValue(raw.defaults);
1099
+ const orhestra = objectValue(raw.orhestra);
1100
+ transfer('orhestra.mode', orhestra.mode, oneOf(['automatic', 'manual']), value => { settings.orhestra.mode = value; });
1101
+ const steps = normalizeStoredOrhestraSteps(orhestra.steps);
1102
+ transfer('orhestra.steps', steps, value => Array.isArray(value), value => { settings.orhestra.steps = value; });
1103
+
1104
+ const aim = objectValue(raw.aim);
1105
+ transfer('aim.mode', aim.mode, oneOf(['automatic', 'manual']), value => { settings.aim.mode = value; });
1106
+
1107
+ const explore = objectValue(raw.explore);
1108
+ const legacyStrict = legacyDefaults.strict;
1109
+ transfer('explore.strict', explore.strict ?? legacyStrict, bool, value => { settings.explore.strict = value; });
1110
+ transfer(
1111
+ 'explore.decisionMode',
1112
+ explore.decision_mode ?? legacyDefaults.decision_mode,
1113
+ oneOf(['autonomous', 'recommend_and_ask', 'ask_each_time']),
1114
+ value => { settings.explore.decisionMode = value; }
1115
+ );
845
1116
 
846
- function migrateSettingsV1(content) {
847
- const defaultsStrict = readLegacyBoolean(content, 'defaults', 'strict', false);
848
- const decisionMode = readLegacyEnum(
849
- content,
850
- 'defaults',
851
- 'decision_mode',
852
- ['autonomous', 'recommend_and_ask', 'ask_each_time'],
853
- DEFAULT_SETTINGS.explore.decisionMode
1117
+ const plan = objectValue(raw.plan);
1118
+ transfer('plan.strict', plan.strict ?? legacyStrict, bool, value => { settings.plan.strict = value; });
1119
+ transfer('plan.review', plan.review ?? plan.meta_review, bool, value => { settings.plan.review = value; });
1120
+ transfer(
1121
+ 'plan.size',
1122
+ plan.size ?? legacyDefaults.plan_size,
1123
+ oneOf(['normal', 'short', 'ask_each_time']),
1124
+ value => { settings.plan.size = value; }
1125
+ );
1126
+ transfer(
1127
+ 'plan.decisionMode',
1128
+ plan.decision_mode ?? legacyDefaults.decision_mode,
1129
+ oneOf(['autonomous', 'recommend_and_ask', 'ask_each_time']),
1130
+ value => { settings.plan.decisionMode = value; }
1131
+ );
1132
+ transfer(
1133
+ 'plan.testStrategy',
1134
+ plan.test_strategy ?? legacyDefaults.test_strategy,
1135
+ oneOf(['after_each_phase', 'tdd_each_phase', 'end_of_plan', 'ask_each_time']),
1136
+ value => { settings.plan.testStrategy = value; }
1137
+ );
1138
+ transfer(
1139
+ 'plan.loggingStrategy',
1140
+ plan.logging_strategy ?? legacyDefaults.logging_strategy,
1141
+ oneOf(['debug_precise', 'standard', 'ask_each_time']),
1142
+ value => { settings.plan.loggingStrategy = value; }
854
1143
  );
855
- const reviewAgents = structuredClone(DEFAULT_REVIEW_AGENTS);
856
- reviewAgents.code_quality.mode = readLegacyBoolean(
857
- content,
858
- 'review',
859
- 'include_code_quality',
860
- true
861
- ) ? 'always' : 'off';
862
1144
 
863
- return {
864
- ...structuredClone(DEFAULT_SETTINGS),
865
- explore: {
866
- strict: defaultsStrict,
867
- decisionMode
868
- },
869
- plan: {
870
- strict: defaultsStrict,
871
- size: readLegacyEnum(
872
- content,
873
- 'defaults',
874
- 'plan_size',
875
- ['normal', 'short', 'ask_each_time'],
876
- DEFAULT_SETTINGS.plan.size
877
- ),
878
- decisionMode,
879
- testStrategy: readLegacyEnum(
880
- content,
881
- 'defaults',
882
- 'test_strategy',
883
- ['after_each_phase', 'tdd_each_phase', 'end_of_plan', 'ask_each_time'],
884
- DEFAULT_SETTINGS.plan.testStrategy
885
- ),
886
- loggingStrategy: readLegacyEnum(
887
- content,
888
- 'defaults',
889
- 'logging_strategy',
890
- ['debug_precise', 'standard', 'ask_each_time'],
891
- DEFAULT_SETTINGS.plan.loggingStrategy
892
- )
893
- },
894
- planPolish: {
895
- strict: defaultsStrict
896
- },
897
- review: {
898
- agents: reviewAgents
899
- },
900
- discoverAutomations: {
901
- includePlans: readLegacyBoolean(content, 'automate', 'include_plans', false)
902
- }
903
- };
904
- }
1145
+ const planReview = objectValue(raw['plan-review']);
1146
+ transfer('planReview.threshold', planReview.threshold, score, value => { settings.planReview.threshold = value; });
1147
+ transferAgentSettings({
1148
+ rawAgents: objectValue(planReview.agents),
1149
+ settingsAgents: settings.planReview.agents,
1150
+ definitions: PLAN_REVIEW_AGENT_DEFINITIONS,
1151
+ prefix: 'planReview.agents',
1152
+ missingKeys
1153
+ });
905
1154
 
906
- function readLegacyBoolean(content, section, key, fallback) {
907
- const value = readNestedYamlScalar(content, section, key);
908
- if (value === 'true') return true;
909
- if (value === 'false') return false;
910
- return fallback;
911
- }
1155
+ const planPolish = objectValue(raw['plan-polish']);
1156
+ transfer('planPolish.limit', planPolish.limit, positiveInt, value => { settings.planPolish.limit = value; });
1157
+
1158
+ const review = objectValue(raw.review);
1159
+ transferAgentSettings({
1160
+ rawAgents: objectValue(review.agents),
1161
+ settingsAgents: settings.review.agents,
1162
+ definitions: REVIEW_AGENT_DEFINITIONS,
1163
+ prefix: 'review.agents',
1164
+ missingKeys
1165
+ });
1166
+ if (typeof review.include_code_quality === 'boolean' && !objectValue(review.agents).code_quality) {
1167
+ settings.review.agents.code_quality.mode = review.include_code_quality ? 'always' : 'off';
1168
+ missingKeys.delete('review.agents.code_quality.mode');
1169
+ }
1170
+
1171
+ const sendReview = objectValue(raw['send-review']);
1172
+ transfer(
1173
+ 'sendReview.closePreviousReviews',
1174
+ sendReview.close_previous_reviews,
1175
+ bool,
1176
+ value => { settings.sendReview.closePreviousReviews = value; }
1177
+ );
1178
+ const discoverAutomations = objectValue(raw['discover-automations']);
1179
+ const legacyAutomate = objectValue(raw.automate);
1180
+ transfer(
1181
+ 'discoverAutomations.includePlans',
1182
+ discoverAutomations.include_plans ?? legacyAutomate.include_plans,
1183
+ bool,
1184
+ value => { settings.discoverAutomations.includePlans = value; }
1185
+ );
912
1186
 
913
- function readLegacyEnum(content, section, key, allowedValues, fallback) {
914
- const value = readNestedYamlScalar(content, section, key);
915
- return allowedValues.includes(value) ? value : fallback;
1187
+ return {
1188
+ settings,
1189
+ missingKeys,
1190
+ version: Number.isInteger(raw.version) ? raw.version : null
1191
+ };
916
1192
  }
917
1193
 
918
- function readTopLevelYamlScalar(content, key) {
919
- for (const line of content.replace(/^\uFEFF/, '').split(/\r?\n/)) {
920
- const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*?)\s*$/);
921
- if (match?.[1] === key) return normalizeYamlScalar(match[2]);
1194
+ function transferAgentSettings({ rawAgents, settingsAgents, definitions, prefix, missingKeys }) {
1195
+ const allowedModes = ['always', 'auto', 'off'];
1196
+ const allowedClaudeModels = CLAUDE_REVIEW_MODEL_CHOICES.map(choice => choice.value);
1197
+ const allowedCodexModels = CODEX_REVIEW_MODEL_CHOICES.map(choice => choice.value);
1198
+ for (const definition of definitions) {
1199
+ const configured = objectValue(rawAgents[definition.key]);
1200
+ const model = objectValue(configured.model);
1201
+ if (allowedModes.includes(configured.mode)) settingsAgents[definition.key].mode = configured.mode;
1202
+ else missingKeys.add(`${prefix}.${definition.key}.mode`);
1203
+ if (allowedClaudeModels.includes(model.claude)) settingsAgents[definition.key].model.claude = model.claude;
1204
+ else missingKeys.add(`${prefix}.${definition.key}.model.claude`);
1205
+ if (allowedCodexModels.includes(model.codex)) settingsAgents[definition.key].model.codex = model.codex;
1206
+ else missingKeys.add(`${prefix}.${definition.key}.model.codex`);
922
1207
  }
923
- return null;
924
1208
  }
925
1209
 
926
- function readNestedYamlScalar(content, section, key) {
927
- let currentSection = null;
928
- for (const line of content.replace(/^\uFEFF/, '').split(/\r?\n/)) {
929
- if (/^\s*(?:#.*)?$/.test(line)) continue;
930
-
931
- const sectionMatch = line.match(/^([A-Za-z0-9_-]+):\s*(?:#.*)?$/);
932
- if (sectionMatch) {
933
- currentSection = sectionMatch[1];
934
- continue;
935
- }
936
-
937
- if (/^\S/.test(line)) {
938
- currentSection = null;
939
- continue;
1210
+ function normalizeStoredOrhestraSteps(value) {
1211
+ if (!Array.isArray(value) || value.length === 0) return null;
1212
+ const steps = [];
1213
+ for (const step of value) {
1214
+ if (!step || typeof step !== 'object' || Array.isArray(step)) return null;
1215
+ if (typeof step.id !== 'string' || step.id.length === 0) return null;
1216
+ if (typeof step.skill !== 'string' || step.skill.length === 0) return null;
1217
+ if (step.enabled !== undefined && typeof step.enabled !== 'boolean') return null;
1218
+ if (step.args !== undefined && typeof step.args !== 'string') return null;
1219
+ const normalized = {
1220
+ id: step.id,
1221
+ skill: step.skill,
1222
+ enabled: step.enabled !== false,
1223
+ args: step.args ?? ''
1224
+ };
1225
+ const onFailure = objectValue(step.on_failure);
1226
+ if (Object.keys(onFailure).length > 0) {
1227
+ if (typeof onFailure.skill !== 'string' || onFailure.skill.length === 0) return null;
1228
+ if (onFailure.args !== undefined && typeof onFailure.args !== 'string') return null;
1229
+ if (!Array.isArray(onFailure.then) || !onFailure.then.every(item => typeof item === 'string')) return null;
1230
+ if (onFailure.max_cycles !== undefined && (!Number.isInteger(onFailure.max_cycles) || onFailure.max_cycles <= 0)) return null;
1231
+ normalized.onFailure = {
1232
+ skill: onFailure.skill,
1233
+ args: onFailure.args ?? '',
1234
+ then: onFailure.then,
1235
+ maxCycles: onFailure.max_cycles ?? 5
1236
+ };
940
1237
  }
941
-
942
- if (currentSection !== section) continue;
943
- const valueMatch = line.match(/^ {2}([A-Za-z0-9_-]+):\s*(.*?)\s*$/);
944
- if (valueMatch?.[1] === key) return normalizeYamlScalar(valueMatch[2]);
1238
+ steps.push(normalized);
945
1239
  }
946
- return null;
1240
+ return steps;
947
1241
  }
948
1242
 
949
- function normalizeYamlScalar(value) {
950
- const withoutComment = value.replace(/\s+#.*$/, '').trim();
951
- if (
952
- withoutComment.length >= 2
953
- && ((withoutComment.startsWith('"') && withoutComment.endsWith('"'))
954
- || (withoutComment.startsWith("'") && withoutComment.endsWith("'")))
955
- ) {
956
- return withoutComment.slice(1, -1);
957
- }
958
- return withoutComment;
1243
+ function objectValue(value) {
1244
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
959
1245
  }
960
1246
 
961
1247
  async function writeFileAtomically(filePath, content) {
@@ -996,8 +1282,8 @@ ${onFailureThen}
996
1282
  # Строка аргументов передаётся скиллу как часть текущего вызова.
997
1283
  args: ${JSON.stringify(step.args ?? '')}${onFailure}`;
998
1284
  }).join('\n');
999
- const reviewAgents = REVIEW_AGENT_DEFINITIONS.map(agent => {
1000
- const configured = settings.review.agents[agent.key] ?? DEFAULT_REVIEW_AGENTS[agent.key];
1285
+ const formatAgents = (definitions, configuredAgents, defaults) => definitions.map(agent => {
1286
+ const configured = configuredAgents[agent.key] ?? defaults[agent.key];
1001
1287
  return ` # ${agent.description}
1002
1288
  # ${agent.auto}
1003
1289
  ${agent.key}:
@@ -1009,8 +1295,14 @@ ${onFailureThen}
1009
1295
  # Модель для Codex.
1010
1296
  codex: ${configured.model.codex}`;
1011
1297
  }).join('\n\n');
1298
+ const planReviewAgents = formatAgents(
1299
+ PLAN_REVIEW_AGENT_DEFINITIONS,
1300
+ settings.planReview.agents,
1301
+ DEFAULT_PLAN_REVIEW_AGENTS
1302
+ );
1303
+ const reviewAgents = formatAgents(REVIEW_AGENT_DEFINITIONS, settings.review.agents, DEFAULT_REVIEW_AGENTS);
1012
1304
 
1013
- return `version: 2
1305
+ return `version: ${SETTINGS_VERSION}
1014
1306
 
1015
1307
  orhestra:
1016
1308
  # Режим полного цикла eda-orhestra.
@@ -1037,6 +1329,9 @@ plan:
1037
1329
  # Включает кросс-CLI ревью в eda-plan.
1038
1330
  # true | false
1039
1331
  strict: ${settings.plan.strict ? 'true' : 'false'}
1332
+ # Запускает один eda-plan-review и при необходимости один eda-plan-review-fix.
1333
+ # true | false
1334
+ review: ${settings.plan.review !== false ? 'true' : 'false'}
1040
1335
  # Задаёт размер плана.
1041
1336
  # normal | short | ask_each_time
1042
1337
  size: ${settings.plan.size}
@@ -1050,10 +1345,18 @@ plan:
1050
1345
  # debug_precise | standard | ask_each_time
1051
1346
  logging_strategy: ${settings.plan.loggingStrategy}
1052
1347
 
1348
+ plan-review:
1349
+ # План готов только без обязательных findings и при score не ниже этого порога.
1350
+ # 1..100
1351
+ threshold: ${settings.planReview.threshold}
1352
+ # Каждая проверка имеет собственный режим запуска и модели для обеих сред.
1353
+ agents:
1354
+ ${planReviewAgents}
1355
+
1053
1356
  plan-polish:
1054
- # Включает кросс-CLI ревью в eda-plan-polish.
1055
- # true | false
1056
- strict: ${settings.planPolish.strict ? 'true' : 'false'}
1357
+ # Максимальное число подтверждающих review-итераций.
1358
+ # Положительное целое число.
1359
+ limit: ${settings.planPolish.limit}
1057
1360
 
1058
1361
  review:
1059
1362
  # Каждая проверка имеет собственный режим запуска и модели для обеих сред.