@gian-tiaga/eda 1.0.5 → 2.1.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 (63) hide show
  1. package/MIGRATION.md +60 -80
  2. package/README.md +116 -36
  3. package/agents/eda-plan-review-api/agent.json +9 -0
  4. package/agents/eda-plan-review-api/prompt.md +35 -0
  5. package/agents/eda-plan-review-architecture/agent.json +9 -0
  6. package/agents/eda-plan-review-architecture/prompt.md +35 -0
  7. package/agents/eda-plan-review-database/agent.json +9 -0
  8. package/agents/eda-plan-review-database/prompt.md +35 -0
  9. package/agents/eda-plan-review-execution/agent.json +9 -0
  10. package/agents/eda-plan-review-execution/prompt.md +35 -0
  11. package/agents/eda-plan-review-feasibility/agent.json +9 -0
  12. package/agents/eda-plan-review-feasibility/prompt.md +35 -0
  13. package/agents/eda-plan-review-frontend/agent.json +9 -0
  14. package/agents/eda-plan-review-frontend/prompt.md +35 -0
  15. package/agents/eda-plan-review-performance/agent.json +9 -0
  16. package/agents/eda-plan-review-performance/prompt.md +35 -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 +35 -0
  21. package/agents/eda-plan-review-rules/agent.json +9 -0
  22. package/agents/eda-plan-review-rules/prompt.md +35 -0
  23. package/agents/eda-plan-review-security/agent.json +9 -0
  24. package/agents/eda-plan-review-security/prompt.md +35 -0
  25. package/agents/eda-plan-review-verification/agent.json +9 -0
  26. package/agents/eda-plan-review-verification/prompt.md +35 -0
  27. package/agents/eda-review-api/prompt.md +4 -3
  28. package/agents/eda-review-architecture/prompt.md +4 -3
  29. package/agents/eda-review-business/agent.json +15 -0
  30. package/agents/eda-review-business/prompt.md +47 -0
  31. package/agents/eda-review-code-quality/prompt.md +5 -4
  32. package/agents/eda-review-correctness/prompt.md +4 -3
  33. package/agents/eda-review-database/prompt.md +4 -3
  34. package/agents/eda-review-documentation/prompt.md +4 -3
  35. package/agents/eda-review-frontend/prompt.md +4 -3
  36. package/agents/eda-review-performance/prompt.md +4 -3
  37. package/agents/eda-review-plan-alignment/prompt.md +4 -3
  38. package/agents/eda-review-previous-reviews/prompt.md +5 -4
  39. package/agents/eda-review-references/prompt.md +4 -3
  40. package/agents/eda-review-rules/prompt.md +4 -3
  41. package/agents/eda-review-security/prompt.md +4 -3
  42. package/agents/eda-review-tests/prompt.md +4 -3
  43. package/bin/cli.js +2 -2
  44. package/lib/install.js +524 -368
  45. package/package.json +3 -2
  46. package/skills/eda-aim/SKILL.md +1 -1
  47. package/skills/eda-business/SKILL.md +162 -0
  48. package/skills/eda-discover-automations/SKILL.md +4 -2
  49. package/skills/eda-explore/SKILL.md +7 -6
  50. package/skills/eda-fix/SKILL.md +5 -4
  51. package/skills/eda-fix-by-review/SKILL.md +41 -57
  52. package/skills/eda-manual-test/SKILL.md +13 -5
  53. package/skills/eda-orhestra/SKILL.md +14 -11
  54. package/skills/eda-plan/SKILL.md +45 -42
  55. package/skills/eda-plan-execute/SKILL.md +8 -6
  56. package/skills/eda-plan-polish/SKILL.md +53 -169
  57. package/skills/eda-plan-review/SKILL.md +189 -0
  58. package/skills/eda-plan-review-fix/SKILL.md +88 -0
  59. package/skills/eda-polish/SKILL.md +19 -31
  60. package/skills/eda-prepare-ai/SKILL.md +7 -2
  61. package/skills/eda-review/SKILL.md +21 -21
  62. package/skills/eda-roadmap/SKILL.md +6 -3
  63. package/skills/eda-send-review/SKILL.md +8 -8
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: 'Проверяет закрытие находок предыдущей связанной итерации 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
@@ -234,6 +364,7 @@ const SETTINGS_SECTION_NAMES = [
234
364
  'aim',
235
365
  'explore',
236
366
  'plan',
367
+ 'plan-review',
237
368
  'plan-polish',
238
369
  'review',
239
370
  'send-review',
@@ -242,30 +373,35 @@ const SETTINGS_SECTION_NAMES = [
242
373
  const SETTINGS_CHOICES = [
243
374
  {
244
375
  section: 'explore',
376
+ key: 'explore.strict',
245
377
  value: 'exploreStrict',
246
378
  name: 'Strict по умолчанию для eda-explore',
247
379
  checked: DEFAULT_SETTINGS.explore.strict
248
380
  },
249
381
  {
250
382
  section: 'plan',
383
+ key: 'plan.strict',
251
384
  value: 'planStrict',
252
385
  name: 'Strict по умолчанию для eda-plan',
253
386
  checked: DEFAULT_SETTINGS.plan.strict
254
387
  },
255
388
  {
256
- section: 'plan-polish',
257
- value: 'planPolishStrict',
258
- name: 'Strict по умолчанию для eda-plan-polish',
259
- 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
260
394
  },
261
395
  {
262
396
  section: 'send-review',
397
+ key: 'sendReview.closePreviousReviews',
263
398
  value: 'sendReviewClosePreviousReviews',
264
399
  name: 'Закрывать предыдущие ревью eda-send-review',
265
400
  checked: DEFAULT_SETTINGS.sendReview.closePreviousReviews
266
401
  },
267
402
  {
268
403
  section: 'discover-automations',
404
+ key: 'discoverAutomations.includePlans',
269
405
  value: 'discoverAutomationsIncludePlans',
270
406
  name: 'Анализировать планы в eda-discover-automations по умолчанию',
271
407
  checked: DEFAULT_SETTINGS.discoverAutomations.includePlans
@@ -295,6 +431,14 @@ const ORHESTRA_POLISH_LIMIT_CHOICES = [1, 2, 3, 5, 10].map(value => ({
295
431
  value,
296
432
  name: `${value} ${value === 1 ? 'итерация' : value < 5 ? 'итерации' : 'итераций'}`
297
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
+ }));
298
442
  const PLAN_SIZE_CHOICES = [
299
443
  {
300
444
  value: 'normal',
@@ -377,7 +521,7 @@ const UPDATE_ALL_SETTINGS_MODE_CHOICES = [
377
521
  },
378
522
  {
379
523
  value: 'skip',
380
- name: 'Skip — сохранить существующие конфиги, defaults создать только там, где файла нет'
524
+ name: 'Skip — сохранить полный v3, остальные конфиги перенести с defaults'
381
525
  }
382
526
  ];
383
527
 
@@ -446,7 +590,7 @@ export async function updateAll({
446
590
  const sharedSettings = await askSettings({ input, output });
447
591
  sharedSettingsContent = formatSettings(sharedSettings);
448
592
  } else {
449
- output.write(`Режим skip: существующие ${SETTINGS_RELATIVE_PATH} сохраняются, defaults создаются только для проектов без файла.\n`);
593
+ output.write(`Режим skip: полный version: ${SETTINGS_VERSION} сохраняется, старые конфиги переносятся, отсутствующие создаются с defaults.\n`);
450
594
  sharedSettingsContent = formatSettings(structuredClone(DEFAULT_SETTINGS));
451
595
  }
452
596
 
@@ -469,7 +613,14 @@ export async function updateAll({
469
613
  output.write(`Найдены установленные среды: ${targets.join(', ')}\n`);
470
614
  const projectSettingsPath = path.join(projectDir, SETTINGS_RELATIVE_PATH);
471
615
  if (resolvedSettingsMode === 'skip' && await fileExists(projectSettingsPath)) {
472
- output.write(`Существующий файл настроек сохранён: ${SETTINGS_RELATIVE_PATH}\n`);
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
+ }
473
624
  } else {
474
625
  await writeSettingsContent(projectDir, sharedSettingsContent);
475
626
  output.write(resolvedSettingsMode === 'skip'
@@ -537,7 +688,7 @@ export async function askUpdateAllSettingsMode({
537
688
  selectPrompt = select
538
689
  } = {}) {
539
690
  if (!input.isTTY || !output.isTTY) {
540
- output.write(`Нет интерактивного терминала — использую режим skip: сохраняю существующие ${SETTINGS_RELATIVE_PATH}, defaults создаю только при отсутствии файла.\n`);
691
+ output.write(`Нет интерактивного терминала — использую режим skip: сохраняю полный v3, старые конфиги переношу с defaults.\n`);
541
692
  return 'skip';
542
693
  }
543
694
 
@@ -556,212 +707,186 @@ export async function askSettings({
556
707
  output = process.stdout,
557
708
  sections = SETTINGS_SECTION_NAMES,
558
709
  checkboxPrompt = checkbox,
559
- selectPrompt = select
710
+ selectPrompt = select,
711
+ seedSettings = DEFAULT_SETTINGS,
712
+ missingKeys = null
560
713
  } = {}) {
714
+ const settings = structuredClone(seedSettings);
561
715
  if (!input.isTTY || !output.isTTY) {
562
- output.write(`Нет интерактивного терминала — использую настройки ${SETTINGS_RELATIVE_PATH} по умолчанию.\n`);
563
- return structuredClone(DEFAULT_SETTINGS);
716
+ output.write(`Нет интерактивного терминала — использую перенесённые значения и defaults ${SETTINGS_RELATIVE_PATH}.\n`);
717
+ return settings;
564
718
  }
565
719
 
566
720
  const requestedSections = new Set(sections);
567
- const booleanChoices = SETTINGS_CHOICES.filter(choice => requestedSections.has(choice.section));
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
+ ));
568
725
  const selected = booleanChoices.length > 0
569
726
  ? await checkboxPrompt({
570
727
  message: 'Какие настройки включить?',
571
728
  instructions: 'Стрелки — выбрать, Space — отметить, Enter — продолжить',
572
- choices: booleanChoices.map(({ section, ...choice }) => choice)
573
- }, {
574
- input,
575
- output
576
- })
729
+ choices: booleanChoices.map(({ section, key, ...choice }) => choice)
730
+ }, { input, output })
577
731
  : [];
578
732
 
579
- const orhestraMode = requestedSections.has('orhestra')
580
- ? await selectPrompt({
581
- message: 'Как eda-orhestra должен вести полный цикл по умолчанию?',
582
- choices: ORHESTRA_MODE_CHOICES,
583
- default: DEFAULT_SETTINGS.orhestra.mode
584
- }, {
585
- input,
586
- output
587
- })
588
- : DEFAULT_SETTINGS.orhestra.mode;
589
-
590
- const enabledOrhestraSteps = requestedSections.has('orhestra')
591
- ? await checkboxPrompt({
592
- message: 'Какие этапы eda-orhestra включить по умолчанию?',
593
- instructions: 'Порядок можно изменить вручную в docs/settings.yaml',
594
- choices: ORHESTRA_STEP_DEFINITIONS.map(step => ({
595
- name: step.name,
596
- value: step.skill,
597
- checked: step.enabled
598
- }))
599
- }, {
600
- input,
601
- output
602
- })
603
- : DEFAULT_ORHESTRA_STEPS.filter(step => step.enabled).map(step => step.skill);
604
-
605
- const polishLimit = requestedSections.has('orhestra') && enabledOrhestraSteps.includes('eda-polish')
606
- ? await selectPrompt({
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
+ }
756
+
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({
607
769
  message: 'Сколько итераций разрешить eda-polish внутри eda-orhestra?',
608
770
  choices: ORHESTRA_POLISH_LIMIT_CHOICES,
609
771
  default: 5
610
- }, {
611
- input,
612
- output
613
- })
614
- : 5;
615
-
616
- const orhestraSteps = DEFAULT_ORHESTRA_STEPS.map(step => ({
617
- ...step,
618
- enabled: enabledOrhestraSteps.includes(step.skill),
619
- ...(step.skill === 'eda-polish' ? { args: `limit ${polishLimit}` } : {})
620
- }));
621
-
622
- const aimMode = requestedSections.has('aim')
623
- ? await selectPrompt({
624
- message: 'Как eda-aim должен отвечать на рабочие вопросы по умолчанию?',
625
- choices: AIM_MODE_CHOICES,
626
- default: DEFAULT_SETTINGS.aim.mode
627
- }, {
628
- input,
629
- output
630
- })
631
- : DEFAULT_SETTINGS.aim.mode;
632
-
633
- const exploreDecisionMode = requestedSections.has('explore')
634
- ? await selectPrompt({
635
- message: 'Как принимать важные решения в eda-explore?',
636
- choices: DECISION_MODE_CHOICES,
637
- default: DEFAULT_SETTINGS.explore.decisionMode
638
- }, {
639
- input,
640
- output
641
- })
642
- : DEFAULT_SETTINGS.explore.decisionMode;
643
-
644
- const planSize = requestedSections.has('plan')
645
- ? await selectPrompt({
646
- message: 'Какой размер плана eda-plan использовать по умолчанию?',
647
- choices: PLAN_SIZE_CHOICES,
648
- default: DEFAULT_SETTINGS.plan.size
649
- }, {
650
- input,
651
- output
652
- })
653
- : DEFAULT_SETTINGS.plan.size;
654
-
655
- const planDecisionMode = requestedSections.has('plan')
656
- ? await selectPrompt({
657
- message: 'Как принимать важные решения в eda-plan?',
658
- choices: DECISION_MODE_CHOICES,
659
- default: DEFAULT_SETTINGS.plan.decisionMode
660
- }, {
661
- input,
662
- output
663
- })
664
- : DEFAULT_SETTINGS.plan.decisionMode;
665
-
666
- const testStrategy = requestedSections.has('plan')
667
- ? await selectPrompt({
668
- message: 'Какую стратегию тестов eda-plan использовать по умолчанию?',
669
- choices: TEST_STRATEGY_CHOICES,
670
- default: DEFAULT_SETTINGS.plan.testStrategy
671
- }, {
672
- input,
673
- output
674
- })
675
- : DEFAULT_SETTINGS.plan.testStrategy;
676
-
677
- const loggingStrategy = requestedSections.has('plan')
678
- ? await selectPrompt({
679
- message: 'Какую стратегию логирования eda-plan использовать по умолчанию?',
680
- choices: LOGGING_STRATEGY_CHOICES,
681
- default: DEFAULT_SETTINGS.plan.loggingStrategy
682
- }, {
683
- input,
684
- output
685
- })
686
- : DEFAULT_SETTINGS.plan.loggingStrategy;
687
-
688
- const reviewAgents = requestedSections.has('review')
689
- ? await askReviewAgentSettings({ input, output, selectPrompt })
690
- : structuredClone(DEFAULT_REVIEW_AGENTS);
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
+ }
691
780
 
692
- return {
693
- orhestra: {
694
- mode: orhestraMode,
695
- steps: orhestraSteps
696
- },
697
- aim: {
698
- mode: aimMode
699
- },
700
- explore: {
701
- strict: selected.includes('exploreStrict'),
702
- decisionMode: exploreDecisionMode
703
- },
704
- plan: {
705
- strict: selected.includes('planStrict'),
706
- size: planSize,
707
- decisionMode: planDecisionMode,
708
- testStrategy,
709
- loggingStrategy
710
- },
711
- planPolish: {
712
- strict: selected.includes('planPolishStrict')
713
- },
714
- review: {
715
- agents: reviewAgents
716
- },
717
- sendReview: {
718
- closePreviousReviews: selected.includes('sendReviewClosePreviousReviews')
719
- },
720
- discoverAutomations: {
721
- includePlans: selected.includes('discoverAutomationsIncludePlans')
722
- }
723
- };
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
+ }
795
+
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;
724
815
  }
725
816
 
726
817
  export async function askReviewAgentSettings({
727
818
  input = process.stdin,
728
819
  output = process.stdout,
729
- selectPrompt = select
820
+ selectPrompt = select,
821
+ seedAgents = DEFAULT_REVIEW_AGENTS,
822
+ missingKeys = null
730
823
  } = {}) {
731
- const reviewAgents = {};
732
- for (const agent of REVIEW_AGENT_DEFINITIONS) {
733
- const mode = await selectPrompt({
734
- message: `Когда запускать review-проверку ${agent.key}?`,
735
- choices: REVIEW_MODE_CHOICES,
736
- default: agent.mode
737
- }, {
738
- input,
739
- output
740
- });
824
+ return askAgentSettings({
825
+ input,
826
+ output,
827
+ selectPrompt,
828
+ definitions: REVIEW_AGENT_DEFINITIONS,
829
+ seedAgents,
830
+ missingKeys,
831
+ keyPrefix: 'review.agents',
832
+ label: 'review'
833
+ });
834
+ }
741
835
 
742
- let claude = agent.claude;
743
- let codex = agent.codex;
744
- if (mode !== 'off') {
836
+ export async function askPlanReviewAgentSettings({
837
+ input = process.stdin,
838
+ output = process.stdout,
839
+ selectPrompt = select,
840
+ seedAgents = DEFAULT_PLAN_REVIEW_AGENTS,
841
+ missingKeys = null
842
+ } = {}) {
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
+ }
854
+
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)) {
745
874
  claude = await selectPrompt({
746
875
  message: `Какой моделью Claude проверять ${agent.key}?`,
747
876
  choices: CLAUDE_REVIEW_MODEL_CHOICES,
748
- default: agent.claude
749
- }, {
750
- input,
751
- output
752
- });
877
+ default: claude
878
+ }, { input, output });
879
+ }
880
+ if (mode !== 'off' && shouldAsk(codexKey)) {
753
881
  codex = await selectPrompt({
754
882
  message: `Какой моделью Codex проверять ${agent.key}?`,
755
883
  choices: CODEX_REVIEW_MODEL_CHOICES,
756
- default: agent.codex
757
- }, {
758
- input,
759
- output
760
- });
884
+ default: codex
885
+ }, { input, output });
761
886
  }
762
- reviewAgents[agent.key] = { mode, model: { claude, codex } };
887
+ configuredAgents[agent.key] = { mode, model: { claude, codex } };
763
888
  }
764
- return reviewAgents;
889
+ return configuredAgents;
765
890
  }
766
891
 
767
892
  async function detectTargets(cwd) {
@@ -886,39 +1011,58 @@ async function findInstalledProjects(rootDir, maxDepth) {
886
1011
  }
887
1012
 
888
1013
  async function ensureSettings(cwd, { input = process.stdin, output = process.stdout } = {}) {
889
- const existing = await migrateSettingsIfNeeded(cwd, { output });
890
- 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
+ }
891
1022
 
892
- 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
+ });
893
1036
  await writeSettingsContent(cwd, formatSettings(settings));
894
- output.write(`Создан файл настроек: ${SETTINGS_RELATIVE_PATH}\n`);
1037
+ output.write(`Переписан файл настроек: ${SETTINGS_RELATIVE_PATH} → version: ${SETTINGS_VERSION}.\n`);
895
1038
  }
896
1039
 
897
1040
  async function configureSettings(cwd, { input = process.stdin, output = process.stdout } = {}) {
898
1041
  const settingsPath = path.join(cwd, SETTINGS_RELATIVE_PATH);
899
1042
  const existingContent = await readFileIfExists(settingsPath);
900
- if (existingContent !== null && readTopLevelYamlScalar(existingContent, 'version') === '2') {
901
- const presentSections = readTopLevelYamlKeys(existingContent);
902
- const missingSections = SETTINGS_SECTION_NAMES.filter(section => !presentSections.has(section));
903
- if (missingSections.length === 0) {
904
- output.write(`Настройки ${SETTINGS_RELATIVE_PATH} версии 2 полные — вопросы не требуются.\n`);
905
- return;
906
- }
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
+ }
907
1050
 
908
- output.write(`Дополняю отсутствующие разделы ${SETTINGS_RELATIVE_PATH}: ${missingSections.join(', ')}.\n`);
909
- const settings = await askSettings({ input, output, sections: missingSections });
910
- const missingContent = extractSettingsSections(formatSettings(settings), missingSections);
911
- const separator = existingContent.endsWith('\n\n') ? '' : existingContent.endsWith('\n') ? '\n' : '\n\n';
912
- const updatedContent = `${existingContent}${separator}${missingContent}\n`;
913
- await writeSettingsContent(cwd, updatedContent);
914
- output.write(`Добавлены разделы ${SETTINGS_RELATIVE_PATH}: ${missingSections.join(', ')}.\n`);
1051
+ const migrated = migrateSettingsContent(existingContent);
1052
+ if (migrated.version === SETTINGS_VERSION && migrated.missingKeys.size === 0) {
1053
+ output.write(`Настройки ${SETTINGS_RELATIVE_PATH} версии ${SETTINGS_VERSION} полные — вопросы не требуются.\n`);
915
1054
  return;
916
1055
  }
917
1056
 
918
- output.write(`Настраиваю ${SETTINGS_RELATIVE_PATH} заново.\n`);
919
- const settings = await askSettings({ input, output });
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
+ });
920
1064
  await writeSettingsContent(cwd, formatSettings(settings));
921
- output.write(`Записан файл настроек: ${SETTINGS_RELATIVE_PATH}\n`);
1065
+ output.write(`Переписан файл настроек: ${SETTINGS_RELATIVE_PATH} → version: ${SETTINGS_VERSION}.\n`);
922
1066
  }
923
1067
 
924
1068
  async function writeSettingsContent(cwd, content) {
@@ -931,178 +1075,173 @@ async function writeSettingsContent(cwd, content) {
931
1075
  await fs.writeFile(settingsPath, content);
932
1076
  }
933
1077
 
934
- async function migrateSettingsIfNeeded(cwd, {
935
- output = process.stdout,
936
- reportExisting = true
937
- } = {}) {
938
- const settingsPath = path.join(cwd, SETTINGS_RELATIVE_PATH);
939
- const content = await readFileIfExists(settingsPath);
940
- if (content === null) return { exists: false, migrated: false };
941
-
942
- const version = readTopLevelYamlScalar(content, 'version');
943
- if (version !== '1') {
944
- if (reportExisting) {
945
- output.write(`Настройки уже есть: ${SETTINGS_RELATIVE_PATH}\n`);
946
- if (version !== '2') {
947
- output.write('Автомиграция не выполнена: безопасно поддерживается только переход version: 1 → version: 2.\n');
948
- }
949
- }
950
- 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 все отсутствующие значения будут запрошены.
951
1085
  }
952
1086
 
953
- const migratedSettings = migrateSettingsV1(content);
954
- await writeFileAtomically(settingsPath, formatSettings(migratedSettings));
955
- output.write(`Мигрированы настройки ${SETTINGS_RELATIVE_PATH}: version: 1 → version: 2.\n`);
956
- return { exists: true, migrated: true, version: '2' };
957
- }
958
-
959
- function migrateSettingsV1(content) {
960
- const defaultsStrict = readLegacyBoolean(content, 'defaults', 'strict', false);
961
- const decisionMode = readLegacyEnum(
962
- content,
963
- 'defaults',
964
- 'decision_mode',
965
- ['autonomous', 'recommend_and_ask', 'ask_each_time'],
966
- DEFAULT_SETTINGS.explore.decisionMode
967
- );
968
- const reviewAgents = structuredClone(DEFAULT_REVIEW_AGENTS);
969
- reviewAgents.code_quality.mode = readLegacyBoolean(
970
- content,
971
- 'review',
972
- 'include_code_quality',
973
- true
974
- ) ? 'always' : 'off';
975
-
976
- return {
977
- ...structuredClone(DEFAULT_SETTINGS),
978
- explore: {
979
- strict: defaultsStrict,
980
- decisionMode
981
- },
982
- plan: {
983
- strict: defaultsStrict,
984
- size: readLegacyEnum(
985
- content,
986
- 'defaults',
987
- 'plan_size',
988
- ['normal', 'short', 'ask_each_time'],
989
- DEFAULT_SETTINGS.plan.size
990
- ),
991
- decisionMode,
992
- testStrategy: readLegacyEnum(
993
- content,
994
- 'defaults',
995
- 'test_strategy',
996
- ['after_each_phase', 'tdd_each_phase', 'end_of_plan', 'ask_each_time'],
997
- DEFAULT_SETTINGS.plan.testStrategy
998
- ),
999
- loggingStrategy: readLegacyEnum(
1000
- content,
1001
- 'defaults',
1002
- 'logging_strategy',
1003
- ['debug_precise', 'standard', 'ask_each_time'],
1004
- DEFAULT_SETTINGS.plan.loggingStrategy
1005
- )
1006
- },
1007
- planPolish: {
1008
- strict: defaultsStrict
1009
- },
1010
- review: {
1011
- agents: reviewAgents
1012
- },
1013
- discoverAutomations: {
1014
- includePlans: readLegacyBoolean(content, 'automate', 'include_plans', false)
1015
- }
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);
1016
1092
  };
1017
- }
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
+ );
1018
1116
 
1019
- function readLegacyBoolean(content, section, key, fallback) {
1020
- const value = readNestedYamlScalar(content, section, key);
1021
- if (value === 'true') return true;
1022
- if (value === 'false') return false;
1023
- return fallback;
1024
- }
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; }
1143
+ );
1025
1144
 
1026
- function readLegacyEnum(content, section, key, allowedValues, fallback) {
1027
- const value = readNestedYamlScalar(content, section, key);
1028
- return allowedValues.includes(value) ? value : fallback;
1029
- }
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
+ });
1030
1154
 
1031
- function readTopLevelYamlScalar(content, key) {
1032
- for (const line of content.replace(/^\uFEFF/, '').split(/\r?\n/)) {
1033
- const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*?)\s*$/);
1034
- if (match?.[1] === key) return normalizeYamlScalar(match[2]);
1035
- }
1036
- return null;
1037
- }
1155
+ const planPolish = objectValue(raw['plan-polish']);
1156
+ transfer('planPolish.limit', planPolish.limit, positiveInt, value => { settings.planPolish.limit = value; });
1038
1157
 
1039
- function readTopLevelYamlKeys(content) {
1040
- const keys = new Set();
1041
- for (const line of content.replace(/^\uFEFF/, '').split(/\r?\n/)) {
1042
- const match = line.match(/^([A-Za-z0-9_-]+):(?:\s.*)?$/);
1043
- if (match) keys.add(match[1]);
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');
1044
1169
  }
1045
- return keys;
1046
- }
1047
1170
 
1048
- function extractSettingsSections(content, sectionNames) {
1049
- const requested = new Set(sectionNames);
1050
- const lines = content.split(/\r?\n/);
1051
- const sectionStarts = [];
1052
- for (let index = 0; index < lines.length; index += 1) {
1053
- const match = lines[index].match(/^([A-Za-z0-9_-]+):(?:\s.*)?$/);
1054
- if (match) sectionStarts.push({ name: match[1], index });
1055
- }
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
+ );
1056
1186
 
1057
- const blocks = [];
1058
- for (let index = 0; index < sectionStarts.length; index += 1) {
1059
- const section = sectionStarts[index];
1060
- if (!requested.has(section.name)) continue;
1061
- const end = sectionStarts[index + 1]?.index ?? lines.length;
1062
- blocks.push(lines.slice(section.index, end).join('\n').trimEnd());
1063
- }
1187
+ return {
1188
+ settings,
1189
+ missingKeys,
1190
+ version: Number.isInteger(raw.version) ? raw.version : null
1191
+ };
1192
+ }
1064
1193
 
1065
- const found = new Set(blocks.map(block => block.match(/^([A-Za-z0-9_-]+):/)?.[1]));
1066
- const missing = sectionNames.filter(section => !found.has(section));
1067
- if (missing.length > 0) {
1068
- throw new Error(`Не удалось сформировать разделы ${SETTINGS_RELATIVE_PATH}: ${missing.join(', ')}.`);
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`);
1069
1207
  }
1070
- return blocks.join('\n\n');
1071
1208
  }
1072
1209
 
1073
- function readNestedYamlScalar(content, section, key) {
1074
- let currentSection = null;
1075
- for (const line of content.replace(/^\uFEFF/, '').split(/\r?\n/)) {
1076
- if (/^\s*(?:#.*)?$/.test(line)) continue;
1077
-
1078
- const sectionMatch = line.match(/^([A-Za-z0-9_-]+):\s*(?:#.*)?$/);
1079
- if (sectionMatch) {
1080
- currentSection = sectionMatch[1];
1081
- continue;
1082
- }
1083
-
1084
- if (/^\S/.test(line)) {
1085
- currentSection = null;
1086
- 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
+ };
1087
1237
  }
1088
-
1089
- if (currentSection !== section) continue;
1090
- const valueMatch = line.match(/^ {2}([A-Za-z0-9_-]+):\s*(.*?)\s*$/);
1091
- if (valueMatch?.[1] === key) return normalizeYamlScalar(valueMatch[2]);
1238
+ steps.push(normalized);
1092
1239
  }
1093
- return null;
1240
+ return steps;
1094
1241
  }
1095
1242
 
1096
- function normalizeYamlScalar(value) {
1097
- const withoutComment = value.replace(/\s+#.*$/, '').trim();
1098
- if (
1099
- withoutComment.length >= 2
1100
- && ((withoutComment.startsWith('"') && withoutComment.endsWith('"'))
1101
- || (withoutComment.startsWith("'") && withoutComment.endsWith("'")))
1102
- ) {
1103
- return withoutComment.slice(1, -1);
1104
- }
1105
- return withoutComment;
1243
+ function objectValue(value) {
1244
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
1106
1245
  }
1107
1246
 
1108
1247
  async function writeFileAtomically(filePath, content) {
@@ -1143,8 +1282,8 @@ ${onFailureThen}
1143
1282
  # Строка аргументов передаётся скиллу как часть текущего вызова.
1144
1283
  args: ${JSON.stringify(step.args ?? '')}${onFailure}`;
1145
1284
  }).join('\n');
1146
- const reviewAgents = REVIEW_AGENT_DEFINITIONS.map(agent => {
1147
- 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];
1148
1287
  return ` # ${agent.description}
1149
1288
  # ${agent.auto}
1150
1289
  ${agent.key}:
@@ -1156,8 +1295,14 @@ ${onFailureThen}
1156
1295
  # Модель для Codex.
1157
1296
  codex: ${configured.model.codex}`;
1158
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);
1159
1304
 
1160
- return `version: 2
1305
+ return `version: ${SETTINGS_VERSION}
1161
1306
 
1162
1307
  orhestra:
1163
1308
  # Режим полного цикла eda-orhestra.
@@ -1184,6 +1329,9 @@ plan:
1184
1329
  # Включает кросс-CLI ревью в eda-plan.
1185
1330
  # true | false
1186
1331
  strict: ${settings.plan.strict ? 'true' : 'false'}
1332
+ # Запускает изолированный цикл eda-plan-polish.
1333
+ # true | false
1334
+ review: ${settings.plan.review !== false ? 'true' : 'false'}
1187
1335
  # Задаёт размер плана.
1188
1336
  # normal | short | ask_each_time
1189
1337
  size: ${settings.plan.size}
@@ -1197,10 +1345,18 @@ plan:
1197
1345
  # debug_precise | standard | ask_each_time
1198
1346
  logging_strategy: ${settings.plan.loggingStrategy}
1199
1347
 
1348
+ plan-review:
1349
+ # План готов только без открытых находок и при score не ниже этого порога.
1350
+ # 1..100
1351
+ threshold: ${settings.planReview.threshold}
1352
+ # Каждая проверка имеет собственный режим запуска и модели для обеих сред.
1353
+ agents:
1354
+ ${planReviewAgents}
1355
+
1200
1356
  plan-polish:
1201
- # Включает кросс-CLI ревью в eda-plan-polish.
1202
- # true | false
1203
- strict: ${settings.planPolish.strict ? 'true' : 'false'}
1357
+ # Максимальное число подтверждающих review-итераций.
1358
+ # Положительное целое число.
1359
+ limit: ${settings.planPolish.limit}
1204
1360
 
1205
1361
  review:
1206
1362
  # Каждая проверка имеет собственный режим запуска и модели для обеих сред.