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