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.
- package/.claude/skills/skills-manifest.json +1 -1
- package/README.md +65 -2
- package/package.json +1 -1
- package/scripts/generate-opencode-agents.ts +8 -1
- package/scripts/generate-opencode-skills.ts +1 -1
- package/v3/CHANGELOG.md +21 -0
- package/v3/dist/cli/bundle.js +1020 -259
- package/v3/dist/cli/commands/init.d.ts.map +1 -1
- package/v3/dist/cli/commands/init.js +2 -0
- package/v3/dist/cli/commands/init.js.map +1 -1
- package/v3/dist/cli/handlers/init-handler.d.ts +1 -0
- package/v3/dist/cli/handlers/init-handler.d.ts.map +1 -1
- package/v3/dist/cli/handlers/init-handler.js +2 -0
- package/v3/dist/cli/handlers/init-handler.js.map +1 -1
- package/v3/dist/init/kiro-installer.d.ts +70 -0
- package/v3/dist/init/kiro-installer.d.ts.map +1 -0
- package/v3/dist/init/kiro-installer.js +794 -0
- package/v3/dist/init/kiro-installer.js.map +1 -0
- package/v3/dist/init/orchestrator.d.ts.map +1 -1
- package/v3/dist/init/orchestrator.js +1 -0
- package/v3/dist/init/orchestrator.js.map +1 -1
- package/v3/dist/init/phases/09-assets.d.ts +3 -0
- package/v3/dist/init/phases/09-assets.d.ts.map +1 -1
- package/v3/dist/init/phases/09-assets.js +33 -0
- package/v3/dist/init/phases/09-assets.js.map +1 -1
- package/v3/dist/init/phases/phase-interface.d.ts +2 -0
- package/v3/dist/init/phases/phase-interface.d.ts.map +1 -1
- package/v3/dist/init/phases/phase-interface.js.map +1 -1
- package/v3/package.json +1 -1
|
@@ -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
|