agentic-qe 3.7.1 → 3.7.2

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.
@@ -0,0 +1,794 @@
1
+ /**
2
+ * Kiro Platform Installer
3
+ * Converts OpenCode YAML agents/skills to Kiro JSON format and generates
4
+ * MCP config, steering files, and hooks for AWS Kiro IDE integration.
5
+ *
6
+ * Follows the OpenCode/N8n installer pattern (ADR-025).
7
+ */
8
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from 'fs';
9
+ import { join, dirname } from 'path';
10
+ import { fileURLToPath } from 'url';
11
+ import { toErrorMessage } from '../shared/error-utils.js';
12
+ // ESM compatibility
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+ // ============================================================================
16
+ // Kiro Installer Class
17
+ // ============================================================================
18
+ export class KiroInstaller {
19
+ projectRoot;
20
+ options;
21
+ openCodeDir;
22
+ constructor(options) {
23
+ this.projectRoot = options.projectRoot;
24
+ this.options = {
25
+ installAgents: true,
26
+ installSkills: true,
27
+ installHooks: true,
28
+ installSteering: true,
29
+ overwrite: false,
30
+ ...options,
31
+ };
32
+ this.openCodeDir = this.findOpenCodeDir();
33
+ }
34
+ // ==========================================================================
35
+ // Source Directory Detection
36
+ // ==========================================================================
37
+ findOpenCodeDir() {
38
+ const possiblePaths = [
39
+ join(__dirname, '../../../.opencode'),
40
+ join(__dirname, '../../.opencode'),
41
+ join(process.cwd(), '.opencode'),
42
+ join(process.cwd(), '../.opencode'),
43
+ ];
44
+ for (const p of possiblePaths) {
45
+ if (existsSync(p)) {
46
+ return p;
47
+ }
48
+ }
49
+ return join(process.cwd(), '.opencode');
50
+ }
51
+ // ==========================================================================
52
+ // Installation
53
+ // ==========================================================================
54
+ async install() {
55
+ const targetDir = join(this.projectRoot, '.kiro');
56
+ const result = {
57
+ success: true,
58
+ agentsInstalled: [],
59
+ skillsInstalled: [],
60
+ hooksInstalled: [],
61
+ steeringInstalled: [],
62
+ mcpConfigured: false,
63
+ errors: [],
64
+ targetDir,
65
+ };
66
+ try {
67
+ // MCP config is always installed (core integration)
68
+ result.mcpConfigured = this.installMcpConfig(targetDir);
69
+ // Convert OpenCode agents → Kiro agents
70
+ if (this.options.installAgents) {
71
+ const agentResult = this.installAgents(targetDir);
72
+ result.agentsInstalled = agentResult.installed;
73
+ result.errors.push(...agentResult.errors);
74
+ }
75
+ // Convert OpenCode skills → Kiro SKILL.md files
76
+ if (this.options.installSkills) {
77
+ const skillResult = this.installSkills(targetDir);
78
+ result.skillsInstalled = skillResult.installed;
79
+ result.errors.push(...skillResult.errors);
80
+ }
81
+ // Generate Kiro hooks
82
+ if (this.options.installHooks) {
83
+ const hookResult = this.installHooks(targetDir);
84
+ result.hooksInstalled = hookResult.installed;
85
+ result.errors.push(...hookResult.errors);
86
+ }
87
+ // Generate steering files
88
+ if (this.options.installSteering) {
89
+ const steeringResult = this.installSteering(targetDir);
90
+ result.steeringInstalled = steeringResult.installed;
91
+ result.errors.push(...steeringResult.errors);
92
+ }
93
+ }
94
+ catch (error) {
95
+ result.success = false;
96
+ result.errors.push(`Kiro installation failed: ${toErrorMessage(error)}`);
97
+ }
98
+ return result;
99
+ }
100
+ // ==========================================================================
101
+ // MCP Configuration
102
+ // ==========================================================================
103
+ installMcpConfig(targetDir) {
104
+ const settingsDir = join(targetDir, 'settings');
105
+ const configPath = join(settingsDir, 'mcp.json');
106
+ if (existsSync(configPath) && !this.options.overwrite) {
107
+ return false;
108
+ }
109
+ mkdirSync(settingsDir, { recursive: true });
110
+ const config = {
111
+ mcpServers: {
112
+ 'agentic-qe': {
113
+ command: 'npx',
114
+ args: ['-y', 'agentic-qe@latest', 'mcp'],
115
+ env: {
116
+ AQE_MEMORY_PATH: '.agentic-qe/memory.db',
117
+ AQE_V3_MODE: 'true',
118
+ },
119
+ disabled: false,
120
+ autoApprove: [
121
+ 'fleet_init',
122
+ 'fleet_status',
123
+ 'test_generate_enhanced',
124
+ 'coverage_analyze_sublinear',
125
+ 'quality_assess',
126
+ 'memory_store',
127
+ 'memory_query',
128
+ ],
129
+ },
130
+ },
131
+ };
132
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
133
+ return true;
134
+ }
135
+ // ==========================================================================
136
+ // Agent Conversion: OpenCode YAML → Kiro JSON
137
+ // ==========================================================================
138
+ installAgents(targetDir) {
139
+ const installed = [];
140
+ const errors = [];
141
+ const sourceDir = join(this.openCodeDir, 'agents');
142
+ const targetAgentsDir = join(targetDir, 'agents');
143
+ if (!existsSync(sourceDir)) {
144
+ // No OpenCode agents to convert — generate a default QE agent
145
+ mkdirSync(targetAgentsDir, { recursive: true });
146
+ this.writeDefaultQEAgent(targetAgentsDir);
147
+ installed.push('qe-specialist');
148
+ return { installed, errors };
149
+ }
150
+ mkdirSync(targetAgentsDir, { recursive: true });
151
+ const files = readdirSync(sourceDir).filter(f => f.endsWith('.yaml'));
152
+ for (const file of files) {
153
+ const name = file.replace('.yaml', '');
154
+ const targetFile = join(targetAgentsDir, `${name}.json`);
155
+ if (existsSync(targetFile) && !this.options.overwrite) {
156
+ continue;
157
+ }
158
+ try {
159
+ const yaml = readFileSync(join(sourceDir, file), 'utf-8');
160
+ const parsed = this.parseYamlAgent(yaml);
161
+ const kiroAgent = this.convertToKiroAgent(parsed);
162
+ writeFileSync(targetFile, JSON.stringify(kiroAgent, null, 2) + '\n');
163
+ installed.push(name);
164
+ }
165
+ catch (error) {
166
+ errors.push(`Failed to convert agent ${file}: ${toErrorMessage(error)}`);
167
+ }
168
+ }
169
+ // Phase 2: Convert QE-relevant subagents from .claude/agents/ (markdown format)
170
+ const claudeAgentsDir = join(this.options.projectRoot, '.claude', 'agents');
171
+ if (existsSync(claudeAgentsDir)) {
172
+ const qeSubagentDirs = ['subagents', 'n8n', 'testing', 'analysis'];
173
+ for (const subdir of qeSubagentDirs) {
174
+ const dirPath = join(claudeAgentsDir, subdir);
175
+ if (!existsSync(dirPath))
176
+ continue;
177
+ const mdFiles = readdirSync(dirPath, { recursive: false })
178
+ .filter((f) => typeof f === 'string' && f.endsWith('.md') && f !== 'README.md');
179
+ for (const file of mdFiles) {
180
+ const name = file.replace('.md', '');
181
+ const targetFile = join(targetAgentsDir, `${name}.json`);
182
+ // Skip if already exists (OpenCode agent takes precedence)
183
+ if (existsSync(targetFile) && !this.options.overwrite)
184
+ continue;
185
+ try {
186
+ const content = readFileSync(join(dirPath, file), 'utf-8');
187
+ const kiroAgent = this.convertMdAgentToKiro(content, name);
188
+ if (kiroAgent) {
189
+ writeFileSync(targetFile, JSON.stringify(kiroAgent, null, 2) + '\n');
190
+ installed.push(name);
191
+ }
192
+ }
193
+ catch (error) {
194
+ errors.push(`Failed to convert subagent ${file}: ${toErrorMessage(error)}`);
195
+ }
196
+ }
197
+ }
198
+ }
199
+ return { installed, errors };
200
+ }
201
+ /**
202
+ * Convert a Claude Code markdown agent (.md) to Kiro JSON format.
203
+ * These have YAML frontmatter + XML-structured prompt body.
204
+ */
205
+ convertMdAgentToKiro(content, name) {
206
+ // Extract YAML frontmatter
207
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
208
+ if (!fmMatch)
209
+ return null;
210
+ const frontmatter = fmMatch[1];
211
+ const body = fmMatch[2].trim();
212
+ const getField = (key) => {
213
+ const re = new RegExp(`^${key}:\\s*"?([^"\n]*)"?`, 'm');
214
+ const m = frontmatter.match(re);
215
+ return m?.[1]?.trim() ?? '';
216
+ };
217
+ const agentName = getField('name') || name;
218
+ const description = getField('description') || `QE subagent: ${name}`;
219
+ // Convert mcp: references in the prompt body
220
+ const prompt = body.replace(/mcp:agentic-qe:/g, '@agentic-qe/');
221
+ // Map model from frontmatter, falling back to category/priority heuristics
222
+ const rawModel = getField('model');
223
+ const priority = getField('priority');
224
+ let model = 'claude-sonnet-4';
225
+ if (rawModel) {
226
+ if (rawModel.includes('opus'))
227
+ model = 'claude-opus-4';
228
+ else if (rawModel.includes('haiku'))
229
+ model = 'claude-haiku-4';
230
+ }
231
+ else if (priority === 'critical') {
232
+ model = 'claude-sonnet-4';
233
+ }
234
+ return {
235
+ name: agentName,
236
+ description,
237
+ model,
238
+ prompt,
239
+ mcpServers: {
240
+ 'agentic-qe': {
241
+ command: 'npx',
242
+ args: ['-y', 'agentic-qe@latest', 'mcp'],
243
+ },
244
+ },
245
+ tools: ['read', 'write', 'shell', '@agentic-qe'],
246
+ includeMcpJson: true,
247
+ };
248
+ }
249
+ parseYamlAgent(yaml) {
250
+ const get = (key) => {
251
+ // Match key at start of line (not indented — top-level only)
252
+ const re = new RegExp(`^${key}:\\s*(?:"([^"]*)"|(.*))`, 'm');
253
+ const m = yaml.match(re);
254
+ return m ? (m[1] ?? m[2] ?? '').trim() : '';
255
+ };
256
+ // Extract systemPrompt (multiline block after "systemPrompt: |")
257
+ let systemPrompt = '';
258
+ const promptMatch = yaml.match(/^systemPrompt:\s*\|\s*\n([\s\S]*?)(?=\n\w|\n$)/m);
259
+ if (promptMatch) {
260
+ systemPrompt = promptMatch[1]
261
+ .split('\n')
262
+ .map(l => l.replace(/^ {2}/, ''))
263
+ .join('\n')
264
+ .trim();
265
+ }
266
+ // Extract tools list
267
+ const tools = [];
268
+ const toolsMatch = yaml.match(/^tools:\s*\n((?:\s+-\s+.*\n?)*)/m);
269
+ if (toolsMatch) {
270
+ const lines = toolsMatch[1].split('\n');
271
+ for (const line of lines) {
272
+ const tm = line.match(/^\s+-\s+"?([^"\n]+)"?/);
273
+ if (tm)
274
+ tools.push(tm[1].trim());
275
+ }
276
+ }
277
+ // Extract permissions
278
+ const permissions = {};
279
+ const permMatch = yaml.match(/^permissions:\s*\n((?:\s+.*\n?)*)/m);
280
+ if (permMatch) {
281
+ const lines = permMatch[1].split('\n');
282
+ for (const line of lines) {
283
+ const pm = line.match(/^\s+"?([^":]+)"?\s*:\s*(\w+)/);
284
+ if (pm)
285
+ permissions[pm[1].trim()] = pm[2].trim();
286
+ }
287
+ }
288
+ return {
289
+ name: get('name'),
290
+ description: get('description'),
291
+ model: get('model') || undefined,
292
+ systemPrompt: systemPrompt || undefined,
293
+ tools,
294
+ permissions,
295
+ };
296
+ }
297
+ convertToKiroAgent(agent) {
298
+ // Map Claude Code tool names to Kiro equivalents, then convert MCP refs
299
+ const toolNameMap = {
300
+ 'bash': 'shell', 'edit': 'write', 'grep': 'shell', 'glob': 'shell',
301
+ };
302
+ const kiroTools = [...new Set(agent.tools?.map(t => {
303
+ if (t.startsWith('mcp:agentic-qe:')) {
304
+ return `@agentic-qe/${t.replace('mcp:agentic-qe:', '')}`;
305
+ }
306
+ return toolNameMap[t] ?? t;
307
+ }) ?? [])];
308
+ // Build allowedTools from permissions, mapping Claude Code names to Kiro equivalents
309
+ const allowedToolsSet = new Set();
310
+ if (agent.permissions) {
311
+ for (const [key, value] of Object.entries(agent.permissions)) {
312
+ if (value === 'allow') {
313
+ if (key.startsWith('mcp:agentic-qe:')) {
314
+ allowedToolsSet.add(`@agentic-qe/${key.replace('mcp:agentic-qe:', '')}`);
315
+ }
316
+ else {
317
+ allowedToolsSet.add(toolNameMap[key] ?? key);
318
+ }
319
+ }
320
+ }
321
+ }
322
+ const allowedTools = [...allowedToolsSet];
323
+ // Map model names
324
+ let model = 'claude-sonnet-4';
325
+ if (agent.model) {
326
+ if (agent.model.includes('opus'))
327
+ model = 'claude-opus-4';
328
+ else if (agent.model.includes('haiku'))
329
+ model = 'claude-haiku-4';
330
+ }
331
+ // Convert mcp:agentic-qe: references in prompt text to Kiro @agentic-qe/ format
332
+ const rawPrompt = agent.systemPrompt ?? `You are ${agent.name}, a specialized QE agent in the Agentic QE v3 platform.`;
333
+ const prompt = rawPrompt.replace(/mcp:agentic-qe:/g, '@agentic-qe/');
334
+ const kiroAgent = {
335
+ name: agent.name,
336
+ description: agent.description,
337
+ model,
338
+ prompt,
339
+ mcpServers: {
340
+ 'agentic-qe': {
341
+ command: 'npx',
342
+ args: ['-y', 'agentic-qe@latest', 'mcp'],
343
+ },
344
+ },
345
+ tools: kiroTools.length > 0 ? kiroTools : ['read', 'write', 'shell', '@agentic-qe'],
346
+ includeMcpJson: true,
347
+ };
348
+ if (allowedTools.length > 0) {
349
+ kiroAgent.allowedTools = allowedTools;
350
+ }
351
+ return kiroAgent;
352
+ }
353
+ writeDefaultQEAgent(targetDir) {
354
+ const agent = {
355
+ name: 'qe-specialist',
356
+ description: 'Quality Engineering specialist powered by Agentic QE',
357
+ model: 'claude-sonnet-4',
358
+ prompt: 'You are a QE specialist. Use AQE tools for test generation, coverage analysis, and quality assessment. Always call fleet_init before other AQE tools.',
359
+ mcpServers: {
360
+ 'agentic-qe': {
361
+ command: 'npx',
362
+ args: ['-y', 'agentic-qe@latest', 'mcp'],
363
+ },
364
+ },
365
+ tools: ['read', 'write', 'shell', '@agentic-qe'],
366
+ allowedTools: ['read', 'write', 'shell', '@agentic-qe/*'],
367
+ includeMcpJson: true,
368
+ welcomeMessage: 'QE Agent ready. I can generate tests, analyze coverage, and assess quality.',
369
+ };
370
+ writeFileSync(join(targetDir, 'qe-specialist.json'), JSON.stringify(agent, null, 2) + '\n');
371
+ }
372
+ // ==========================================================================
373
+ // Skill Conversion: Claude Code SKILL.md → Kiro SKILL.md (full content)
374
+ // Falls back to OpenCode YAML → Kiro SKILL.md for skills without Claude source
375
+ // ==========================================================================
376
+ installSkills(targetDir) {
377
+ const installed = [];
378
+ const errors = [];
379
+ const targetSkillsDir = join(targetDir, 'skills');
380
+ const claudeSkillsDir = join(this.options.projectRoot, '.claude', 'skills');
381
+ const openCodeSkillsDir = join(this.openCodeDir, 'skills');
382
+ mkdirSync(targetSkillsDir, { recursive: true });
383
+ // Build a set of OpenCode skill names to convert
384
+ const openCodeSkills = new Set();
385
+ if (existsSync(openCodeSkillsDir)) {
386
+ for (const f of readdirSync(openCodeSkillsDir).filter(f => f.endsWith('.yaml'))) {
387
+ openCodeSkills.add(f.replace('.yaml', ''));
388
+ }
389
+ }
390
+ // For each OpenCode skill, try Claude Code source first (full content)
391
+ for (const skillName of openCodeSkills) {
392
+ const kiroSkillDir = join(targetSkillsDir, skillName);
393
+ const targetFile = join(kiroSkillDir, 'SKILL.md');
394
+ if (existsSync(targetFile) && !this.options.overwrite) {
395
+ continue;
396
+ }
397
+ try {
398
+ // Try Claude Code source (full rich content)
399
+ const claudeSource = this.findClaudeSkillSource(claudeSkillsDir, skillName);
400
+ if (claudeSource) {
401
+ const kiroMd = this.convertClaudeSkillToKiro(claudeSource, skillName);
402
+ mkdirSync(kiroSkillDir, { recursive: true });
403
+ writeFileSync(targetFile, kiroMd, { mode: 0o644 });
404
+ installed.push(skillName);
405
+ }
406
+ else {
407
+ // Fallback: convert from OpenCode YAML (thin content)
408
+ const yamlPath = join(openCodeSkillsDir, `${skillName}.yaml`);
409
+ if (existsSync(yamlPath)) {
410
+ const yaml = readFileSync(yamlPath, 'utf-8');
411
+ const parsed = this.parseYamlSkill(yaml);
412
+ const markdown = this.convertToSkillMd(parsed);
413
+ mkdirSync(kiroSkillDir, { recursive: true });
414
+ writeFileSync(targetFile, markdown, { mode: 0o644 });
415
+ installed.push(skillName);
416
+ }
417
+ }
418
+ }
419
+ catch (error) {
420
+ errors.push(`Failed to convert skill ${skillName}: ${toErrorMessage(error)}`);
421
+ }
422
+ }
423
+ return { installed, errors };
424
+ }
425
+ /**
426
+ * Find the Claude Code source SKILL.md for a given OpenCode skill name.
427
+ * OpenCode skills use `qe-` prefix; Claude Code may use bare name or `qe-` prefix.
428
+ */
429
+ findClaudeSkillSource(claudeSkillsDir, skillName) {
430
+ if (!existsSync(claudeSkillsDir))
431
+ return null;
432
+ // Try direct match: qe-database-testing -> .claude/skills/qe-database-testing/SKILL.md
433
+ const directPath = join(claudeSkillsDir, skillName, 'SKILL.md');
434
+ if (existsSync(directPath))
435
+ return readFileSync(directPath, 'utf-8');
436
+ // Try without qe- prefix: qe-database-testing -> .claude/skills/database-testing/SKILL.md
437
+ const bareName = skillName.replace(/^qe-/, '');
438
+ const barePath = join(claudeSkillsDir, bareName, 'SKILL.md');
439
+ if (existsSync(barePath))
440
+ return readFileSync(barePath, 'utf-8');
441
+ // Try QCSD skills: qcsd-ideation-swarm -> .claude/skills/qcsd-ideation-swarm/SKILL.md
442
+ if (skillName.startsWith('qcsd-')) {
443
+ const qcsdPath = join(claudeSkillsDir, skillName, 'SKILL.md');
444
+ if (existsSync(qcsdPath))
445
+ return readFileSync(qcsdPath, 'utf-8');
446
+ }
447
+ return null;
448
+ }
449
+ /**
450
+ * Convert a Claude Code SKILL.md to Kiro format:
451
+ * - Replace frontmatter with Kiro-compatible fields (inclusion, name, description)
452
+ * - Keep the full markdown body content intact
453
+ * - Convert mcp:agentic-qe: references to @agentic-qe/ format
454
+ */
455
+ convertClaudeSkillToKiro(claudeContent, skillName) {
456
+ // Extract frontmatter and body
457
+ const fmMatch = claudeContent.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
458
+ if (!fmMatch) {
459
+ // No frontmatter — wrap the entire content
460
+ return `---\ninclusion: auto\nname: ${skillName}\ndescription: AQE skill\n---\n\n${claudeContent}`;
461
+ }
462
+ const frontmatter = fmMatch[1];
463
+ let body = fmMatch[2];
464
+ // Extract name and description from original frontmatter
465
+ const nameMatch = frontmatter.match(/^name:\s*(.+)/m);
466
+ const descMatch = frontmatter.match(/^description:\s*"?([^"\n]*)"?/m);
467
+ const tagsMatch = frontmatter.match(/^tags:\s*\[([^\]]*)\]/m);
468
+ const description = descMatch?.[1]?.trim() ?? '';
469
+ const tags = tagsMatch?.[1]?.trim() ?? '';
470
+ // Build Kiro frontmatter (agentskills.io compatible)
471
+ const kiroFrontmatter = [
472
+ '---',
473
+ 'inclusion: auto',
474
+ `name: ${skillName}`,
475
+ `description: "${description}"`,
476
+ tags ? `tags: [${tags}]` : '',
477
+ '---',
478
+ ].filter(Boolean).join('\n');
479
+ // Convert mcp:agentic-qe: references to @agentic-qe/ in body
480
+ body = body.replace(/mcp:agentic-qe:/g, '@agentic-qe/');
481
+ // Remove Claude Code-specific directives that Kiro won't understand
482
+ // (keep <default_to_action> blocks — they're useful instructions)
483
+ return `${kiroFrontmatter}\n${body}`;
484
+ }
485
+ parseYamlSkill(yaml) {
486
+ const get = (key) => {
487
+ const re = new RegExp(`^${key}:\\s*(?:"([^"]*)"|(.*))`, 'm');
488
+ const m = yaml.match(re);
489
+ return m ? (m[1] ?? m[2] ?? '').trim() : '';
490
+ };
491
+ // Extract tags
492
+ const tags = [];
493
+ const tagsMatch = yaml.match(/^tags:\s*\[([^\]]*)\]/m);
494
+ if (tagsMatch) {
495
+ tags.push(...tagsMatch[1].split(',').map(t => t.trim().replace(/^"|"$/g, '')));
496
+ }
497
+ // Extract steps — split on ` - name:` boundaries, capture description + full prompt
498
+ const steps = [];
499
+ const stepsMatch = yaml.match(/^steps:\s*\n([\s\S]*)$/m);
500
+ if (stepsMatch) {
501
+ // Split into individual step blocks on the `- name:` delimiter
502
+ const rawBlocks = stepsMatch[1].split(/\n\s*-\s+name:\s*/);
503
+ for (const block of rawBlocks) {
504
+ if (!block.trim())
505
+ continue;
506
+ // First line of the block is the step name
507
+ const nameMatch = block.match(/^([^\n]+)/);
508
+ if (!nameMatch)
509
+ continue;
510
+ const rawName = nameMatch[1].trim();
511
+ // Clean step name: remove leading `- name:` residue and YAML artifacts
512
+ const cleanName = rawName.replace(/^-\s*name:\s*/, '').replace(/^["']|["']$/g, '');
513
+ const descMatch = block.match(/description:\s*"([^"]*)"/);
514
+ const description = descMatch?.[1]?.trim() ?? '';
515
+ // Capture multi-line prompt content (everything indented after `prompt: |`)
516
+ let prompt = '';
517
+ const promptMatch = block.match(/prompt:\s*\|\s*\n([\s\S]*?)(?=\n\s+-\s+name:|\n\s*$|$)/);
518
+ if (promptMatch) {
519
+ prompt = promptMatch[1]
520
+ .split('\n')
521
+ .map(l => l.replace(/^ {4,6}/, ''))
522
+ .join('\n')
523
+ .trim();
524
+ }
525
+ steps.push({ name: cleanName, description, prompt });
526
+ }
527
+ }
528
+ return {
529
+ name: get('name'),
530
+ description: get('description'),
531
+ minModelTier: get('minModelTier') || undefined,
532
+ tags,
533
+ steps,
534
+ };
535
+ }
536
+ convertToSkillMd(skill) {
537
+ const lines = [];
538
+ // YAML front matter for Kiro skill discovery
539
+ lines.push('---');
540
+ lines.push('inclusion: auto');
541
+ lines.push(`name: ${skill.name}`);
542
+ lines.push(`description: ${skill.description}`);
543
+ lines.push('---');
544
+ lines.push('');
545
+ lines.push(`# ${skill.name}`);
546
+ lines.push('');
547
+ lines.push(skill.description);
548
+ lines.push('');
549
+ if (skill.tags && skill.tags.length > 0) {
550
+ lines.push(`**Tags:** ${skill.tags.join(', ')}`);
551
+ lines.push('');
552
+ }
553
+ lines.push('## Prerequisites');
554
+ lines.push('');
555
+ lines.push('This skill requires the AQE MCP server. Ensure it is configured in `.kiro/settings/mcp.json`.');
556
+ lines.push('');
557
+ if (skill.steps && skill.steps.length > 0) {
558
+ lines.push('## Steps');
559
+ lines.push('');
560
+ for (let i = 0; i < skill.steps.length; i++) {
561
+ const step = skill.steps[i];
562
+ // Format step name: convert kebab-case to Title Case, strip leading numbering
563
+ const displayName = step.name
564
+ .replace(/^[-\d]+\s*/, '')
565
+ .replace(/-/g, ' ')
566
+ .replace(/\b\w/g, c => c.toUpperCase());
567
+ lines.push(`### ${i + 1}. ${displayName}`);
568
+ lines.push('');
569
+ // Use description as body; fall back to prompt if description is just a title
570
+ const body = step.description && step.description.length > step.name.length
571
+ ? step.description
572
+ : step.prompt && step.prompt.length > step.name.length
573
+ ? step.prompt
574
+ : step.description || step.prompt || '';
575
+ if (body) {
576
+ lines.push(body);
577
+ lines.push('');
578
+ }
579
+ }
580
+ }
581
+ lines.push('## MCP Tools');
582
+ lines.push('');
583
+ lines.push('Use AQE tools via the `@agentic-qe` MCP server:');
584
+ lines.push('');
585
+ lines.push('- `@agentic-qe/fleet_init` — Initialize the QE fleet');
586
+ lines.push('- `@agentic-qe/test_generate_enhanced` — Generate tests');
587
+ lines.push('- `@agentic-qe/coverage_analyze_sublinear` — Analyze coverage');
588
+ lines.push('- `@agentic-qe/quality_assess` — Assess quality gates');
589
+ lines.push('- `@agentic-qe/memory_store` — Store learned patterns');
590
+ lines.push('- `@agentic-qe/memory_query` — Query past patterns');
591
+ lines.push('');
592
+ return lines.join('\n');
593
+ }
594
+ // ==========================================================================
595
+ // Hooks Generation
596
+ // ==========================================================================
597
+ installHooks(targetDir) {
598
+ const installed = [];
599
+ const errors = [];
600
+ const hooksDir = join(targetDir, 'hooks');
601
+ mkdirSync(hooksDir, { recursive: true });
602
+ const hooks = this.getKiroHooks();
603
+ for (const hook of hooks) {
604
+ const filePath = join(hooksDir, hook.filename);
605
+ if (existsSync(filePath) && !this.options.overwrite) {
606
+ continue;
607
+ }
608
+ try {
609
+ writeFileSync(filePath, JSON.stringify(hook.config, null, 2) + '\n');
610
+ installed.push(hook.filename);
611
+ }
612
+ catch (error) {
613
+ errors.push(`Failed to install hook ${hook.filename}: ${toErrorMessage(error)}`);
614
+ }
615
+ }
616
+ return { installed, errors };
617
+ }
618
+ getKiroHooks() {
619
+ return [
620
+ {
621
+ filename: 'aqe-test-updater.kiro.hook',
622
+ config: {
623
+ name: 'AQE Test Updater',
624
+ description: 'Auto-generate or update tests when source files change',
625
+ version: '1',
626
+ when: {
627
+ type: 'fileEdited',
628
+ patterns: ['src/**/*.ts', 'src/**/*.js', '!**/*.test.*', '!**/*.spec.*', '!**/node_modules/**'],
629
+ },
630
+ then: {
631
+ type: 'askAgent',
632
+ prompt: 'A source file was edited. Use @agentic-qe/test_generate_enhanced to check if corresponding tests need updating. Only update tests if the public API changed.',
633
+ },
634
+ },
635
+ },
636
+ {
637
+ filename: 'aqe-coverage-check.kiro.hook',
638
+ config: {
639
+ name: 'AQE Coverage Check',
640
+ description: 'Run coverage analysis after test files are created or edited',
641
+ version: '1',
642
+ when: {
643
+ type: 'fileEdited',
644
+ patterns: ['**/*.test.ts', '**/*.spec.ts', '**/*.test.js', '**/*.spec.js'],
645
+ },
646
+ then: {
647
+ type: 'askAgent',
648
+ prompt: 'A test file was modified. Use @agentic-qe/coverage_analyze_sublinear to check if coverage targets are still met. Report any gaps.',
649
+ },
650
+ },
651
+ },
652
+ {
653
+ filename: 'aqe-spec-quality-gate.kiro.hook',
654
+ config: {
655
+ name: 'AQE Spec Quality Gate',
656
+ description: 'Run quality assessment after each spec task completes',
657
+ version: '1',
658
+ when: {
659
+ type: 'postSpecTask',
660
+ },
661
+ then: {
662
+ type: 'askAgent',
663
+ prompt: 'A spec task just completed. Use @agentic-qe/quality_assess on the files changed by this task. If coverage drops below 80% or quality gates fail, flag it before moving to the next task.',
664
+ },
665
+ },
666
+ },
667
+ {
668
+ filename: 'aqe-security-scan.kiro.hook',
669
+ config: {
670
+ name: 'AQE Security Scan',
671
+ description: 'Run security scan when security-sensitive files change',
672
+ version: '1',
673
+ when: {
674
+ type: 'fileEdited',
675
+ patterns: ['**/auth/**', '**/security/**', '**/middleware/**', '**/*credential*', '**/*secret*'],
676
+ },
677
+ then: {
678
+ type: 'askAgent',
679
+ prompt: 'A security-sensitive file was modified. Use @agentic-qe/security_scan_comprehensive to check for vulnerabilities. Flag any OWASP Top 10 issues.',
680
+ },
681
+ },
682
+ },
683
+ {
684
+ filename: 'aqe-pre-commit-quality.kiro.hook',
685
+ config: {
686
+ name: 'AQE Pre-Commit Quality',
687
+ description: 'Run quality assessment before agent stops to ensure standards are met',
688
+ version: '1',
689
+ when: {
690
+ type: 'agentStop',
691
+ },
692
+ then: {
693
+ type: 'askAgent',
694
+ prompt: 'Before finishing, use @agentic-qe/quality_assess to verify all modified files meet quality standards. Store the result with @agentic-qe/memory_store for learning.',
695
+ },
696
+ },
697
+ },
698
+ ];
699
+ }
700
+ // ==========================================================================
701
+ // Steering Files
702
+ // ==========================================================================
703
+ installSteering(targetDir) {
704
+ const installed = [];
705
+ const errors = [];
706
+ const steeringDir = join(targetDir, 'steering');
707
+ mkdirSync(steeringDir, { recursive: true });
708
+ const files = this.getSteeringFiles();
709
+ for (const file of files) {
710
+ const filePath = join(steeringDir, file.filename);
711
+ if (existsSync(filePath) && !this.options.overwrite) {
712
+ continue;
713
+ }
714
+ try {
715
+ writeFileSync(filePath, file.content);
716
+ installed.push(file.filename);
717
+ }
718
+ catch (error) {
719
+ errors.push(`Failed to install steering file ${file.filename}: ${toErrorMessage(error)}`);
720
+ }
721
+ }
722
+ return { installed, errors };
723
+ }
724
+ getSteeringFiles() {
725
+ return [
726
+ {
727
+ filename: 'qe-standards.md',
728
+ content: `---
729
+ inclusion: auto
730
+ name: qe-standards
731
+ description: Quality engineering standards and practices. Triggered when discussing tests, coverage, quality gates, or code review.
732
+ ---
733
+
734
+ # Quality Engineering Standards (AQE v3)
735
+
736
+ ## Test Generation
737
+ - Use \`@agentic-qe/test_generate_enhanced\` for AI-powered test creation
738
+ - Follow the test pyramid: 70% unit, 20% integration, 10% e2e
739
+ - Use boundary value analysis and equivalence partitioning
740
+ - Always call \`@agentic-qe/fleet_init\` before using other AQE tools
741
+
742
+ ## Coverage Analysis
743
+ - Use \`@agentic-qe/coverage_analyze_sublinear\` for O(log n) gap detection
744
+ - Target: 80% statement coverage minimum
745
+ - Focus on risk-weighted coverage, not just line counts
746
+
747
+ ## Quality Gates
748
+ - Use \`@agentic-qe/quality_assess\` before marking tasks complete
749
+ - Gates: coverage threshold, complexity limits, security scan pass
750
+ - Store results with \`@agentic-qe/memory_store\` for pattern learning
751
+
752
+ ## Learning
753
+ - Query past patterns with \`@agentic-qe/memory_query\` before starting work
754
+ - Store successful patterns after task completion
755
+ - Use namespace \`aqe/learning/patterns/\` for pattern storage
756
+ `,
757
+ },
758
+ {
759
+ filename: 'testing-conventions.md',
760
+ content: `---
761
+ inclusion: fileMatch
762
+ name: testing-conventions
763
+ description: Testing conventions for test files
764
+ fileMatchPattern: "**/*.test.{ts,js,tsx,jsx}"
765
+ ---
766
+
767
+ # Testing Conventions
768
+
769
+ ## Structure
770
+ - Use Arrange-Act-Assert (AAA) pattern
771
+ - One logical assertion per test
772
+ - Descriptive names: \`should_returnValue_when_condition\`
773
+
774
+ ## Frameworks
775
+ - Unit tests: Vitest or Jest
776
+ - Integration tests: Vitest with real dependencies
777
+ - E2E tests: Playwright
778
+
779
+ ## Mocking
780
+ - Mock external dependencies at system boundaries
781
+ - Prefer dependency injection over module mocking
782
+ - Never mock the system under test
783
+ `,
784
+ },
785
+ ];
786
+ }
787
+ }
788
+ // ============================================================================
789
+ // Factory Function
790
+ // ============================================================================
791
+ export function createKiroInstaller(options) {
792
+ return new KiroInstaller(options);
793
+ }
794
+ //# sourceMappingURL=kiro-installer.js.map