qaa-agent 1.9.2 → 1.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +206 -179
  2. package/CLAUDE.md +718 -557
  3. package/README.md +384 -357
  4. package/VERSION +1 -1
  5. package/agents/qa-pipeline-orchestrator.md +1739 -1425
  6. package/agents/qaa-analyzer.md +0 -1
  7. package/agents/qaa-bug-detective.md +790 -655
  8. package/agents/qaa-codebase-mapper.md +50 -1
  9. package/agents/qaa-discovery.md +421 -384
  10. package/agents/qaa-e2e-runner.md +715 -577
  11. package/agents/qaa-executor.md +947 -830
  12. package/agents/qaa-planner.md +14 -1
  13. package/agents/qaa-project-researcher.md +533 -400
  14. package/agents/qaa-scanner.md +77 -1
  15. package/agents/qaa-testid-injector.md +0 -1
  16. package/agents/qaa-validator.md +86 -1
  17. package/bin/install.cjs +375 -253
  18. package/bin/lib/context7-cache.cjs +299 -0
  19. package/bin/lib/intent-detector.cjs +488 -0
  20. package/commands/qa-audit.md +255 -126
  21. package/commands/qa-create-test.md +666 -365
  22. package/commands/qa-fix.md +684 -513
  23. package/commands/qa-map.md +283 -139
  24. package/commands/qa-pr.md +63 -0
  25. package/commands/qa-research.md +181 -157
  26. package/commands/qa-start.md +62 -6
  27. package/commands/qa-test-report.md +219 -219
  28. package/frameworks/cypress.json +54 -0
  29. package/frameworks/jest.json +59 -0
  30. package/frameworks/playwright.json +58 -0
  31. package/frameworks/pytest.json +59 -0
  32. package/frameworks/robot-framework.json +54 -0
  33. package/frameworks/selenium.json +65 -0
  34. package/frameworks/vitest.json +57 -0
  35. package/package.json +5 -2
  36. package/workflows/qa-analyze.md +100 -4
  37. package/workflows/qa-from-ticket.md +50 -2
  38. package/workflows/qa-gap.md +50 -2
  39. package/workflows/qa-start.md +2048 -1405
  40. package/workflows/qa-testid.md +50 -2
  41. package/workflows/qa-validate.md +50 -2
package/bin/install.cjs CHANGED
@@ -1,253 +1,375 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * QAA Agent Installer
5
- *
6
- * Installs QAA (QA Automation Agent) into the user's Claude Code environment.
7
- *
8
- * What it does:
9
- * 1. Copies agents, commands, skills, templates, workflows, docs, bin, and config files
10
- * to the chosen install directory (global ~/.claude/qaa or local ./.claude/qaa)
11
- * 2. Registers Playwright MCP and Context7 MCP as global MCP servers
12
- * 3. Merges required permissions into Claude Code settings.json
13
- *
14
- * Usage:
15
- * npx qaa-agent
16
- */
17
-
18
- const fs = require('fs');
19
- const path = require('path');
20
- const readline = require('readline');
21
- const { execSync } = require('child_process');
22
-
23
- // ── Helpers ──────────────────────────────────────────────────────────────────
24
-
25
- function log(msg) { console.log(` ${msg}`); }
26
- function success(msg) { console.log(` ✓ ${msg}`); }
27
- function warn(msg) { console.log(` ⚠ ${msg}`); }
28
- function fail(msg) { console.error(` ✗ ${msg}`); }
29
-
30
- function ask(question) {
31
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
32
- return new Promise(resolve => {
33
- rl.question(` ${question} `, answer => {
34
- rl.close();
35
- resolve(answer.trim());
36
- });
37
- });
38
- }
39
-
40
- function copyDirRecursive(src, dest) {
41
- if (!fs.existsSync(src)) return 0;
42
- fs.mkdirSync(dest, { recursive: true });
43
- let count = 0;
44
- const entries = fs.readdirSync(src, { withFileTypes: true });
45
- for (const entry of entries) {
46
- const srcPath = path.join(src, entry.name);
47
- const destPath = path.join(dest, entry.name);
48
- if (entry.isDirectory()) {
49
- count += copyDirRecursive(srcPath, destPath);
50
- } else {
51
- fs.copyFileSync(srcPath, destPath);
52
- count++;
53
- }
54
- }
55
- return count;
56
- }
57
-
58
- function deepMerge(target, source) {
59
- for (const key of Object.keys(source)) {
60
- if (
61
- source[key] && typeof source[key] === 'object' && !Array.isArray(source[key]) &&
62
- target[key] && typeof target[key] === 'object' && !Array.isArray(target[key])
63
- ) {
64
- deepMerge(target[key], source[key]);
65
- } else if (Array.isArray(source[key]) && Array.isArray(target[key])) {
66
- // Merge arrays without duplicates
67
- const merged = [...new Set([...target[key], ...source[key]])];
68
- target[key] = merged;
69
- } else {
70
- target[key] = source[key];
71
- }
72
- }
73
- return target;
74
- }
75
-
76
- // ── MCP Registration ─────────────────────────────────────────────────────────
77
-
78
- function registerMcpServers(claudeJsonPath) {
79
- const mcpServers = {
80
- playwright: {
81
- command: 'npx',
82
- args: ['@playwright/mcp@latest']
83
- },
84
- context7: {
85
- command: 'npx',
86
- args: ['-y', '@upstash/context7-mcp@latest']
87
- }
88
- };
89
-
90
- let config = {};
91
- if (fs.existsSync(claudeJsonPath)) {
92
- try {
93
- config = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf-8'));
94
- } catch {
95
- config = {};
96
- }
97
- }
98
-
99
- if (!config.mcpServers) config.mcpServers = {};
100
-
101
- let added = [];
102
- for (const [name, serverConfig] of Object.entries(mcpServers)) {
103
- if (!config.mcpServers[name]) {
104
- config.mcpServers[name] = serverConfig;
105
- added.push(name);
106
- }
107
- }
108
-
109
- fs.writeFileSync(claudeJsonPath, JSON.stringify(config, null, 2) + '\n');
110
- return added;
111
- }
112
-
113
- // ── Settings Merge ───────────────────────────────────────────────────────────
114
-
115
- function mergeSettings(installDir, packageDir) {
116
- const srcSettings = path.join(packageDir, 'settings.json');
117
- if (!fs.existsSync(srcSettings)) return false;
118
-
119
- const claudeDir = path.dirname(installDir);
120
- const destSettings = path.join(claudeDir, 'settings.json');
121
-
122
- const source = JSON.parse(fs.readFileSync(srcSettings, 'utf-8'));
123
-
124
- let target = {};
125
- if (fs.existsSync(destSettings)) {
126
- try {
127
- target = JSON.parse(fs.readFileSync(destSettings, 'utf-8'));
128
- } catch {
129
- target = {};
130
- }
131
- }
132
-
133
- deepMerge(target, source);
134
- fs.writeFileSync(destSettings, JSON.stringify(target, null, 2) + '\n');
135
- return true;
136
- }
137
-
138
- // ── Main ─────────────────────────────────────────────────────────────────────
139
-
140
- async function main() {
141
- console.log('');
142
- console.log(' ╔═══════════════════════════════════════╗');
143
- console.log(' ║ QAA QA Automation Agent Installer ║');
144
- console.log(' ╚═══════════════════════════════════════╝');
145
- console.log('');
146
-
147
- // Determine package root (where the npm package files are)
148
- const packageDir = path.resolve(__dirname, '..');
149
-
150
- // Check that package files exist
151
- const requiredDirs = ['agents', 'commands', 'skills'];
152
- const missing = requiredDirs.filter(d => !fs.existsSync(path.join(packageDir, d)));
153
- if (missing.length > 0) {
154
- fail(`Package incomplete — missing: ${missing.join(', ')}`);
155
- process.exit(1);
156
- }
157
-
158
- // Ask install scope
159
- console.log(' Install scope:');
160
- console.log(' 1) Global — ~/.claude/qaa (available in all projects)');
161
- console.log(' 2) Local — ./.claude/qaa (this project only)');
162
- console.log('');
163
- const scopeChoice = await ask('Choose [1/2] (default: 1):');
164
- const isGlobal = scopeChoice !== '2';
165
-
166
- const homeDir = process.env.HOME || process.env.USERPROFILE;
167
- const claudeDir = isGlobal
168
- ? path.join(homeDir, '.claude')
169
- : path.join(process.cwd(), '.claude');
170
- const installDir = path.join(claudeDir, 'qaa');
171
-
172
- // Check for existing installation
173
- if (fs.existsSync(installDir)) {
174
- const overwrite = await ask('QAA already installed at this location. Overwrite? [y/N]:');
175
- if (overwrite.toLowerCase() !== 'y') {
176
- log('Installation cancelled.');
177
- process.exit(0);
178
- }
179
- }
180
-
181
- console.log('');
182
- log(`Installing to: ${installDir}`);
183
- console.log('');
184
-
185
- // ── Step 1: Copy files ──────────────────────────────────────────────────
186
-
187
- const dirsToCopy = ['agents', 'commands', 'skills', 'templates', 'workflows', 'docs', 'bin'];
188
- const filesToCopy = ['CLAUDE.md', 'CHANGELOG.md', '.mcp.json', 'package.json'];
189
-
190
- let totalFiles = 0;
191
-
192
- for (const dir of dirsToCopy) {
193
- const src = path.join(packageDir, dir);
194
- const dest = path.join(installDir, dir);
195
- if (fs.existsSync(src)) {
196
- const count = copyDirRecursive(src, dest);
197
- success(`${dir}/ ${count} files`);
198
- totalFiles += count;
199
- }
200
- }
201
-
202
- for (const file of filesToCopy) {
203
- const src = path.join(packageDir, file);
204
- const dest = path.join(installDir, file);
205
- if (fs.existsSync(src)) {
206
- fs.mkdirSync(path.dirname(dest), { recursive: true });
207
- fs.copyFileSync(src, dest);
208
- success(file);
209
- totalFiles++;
210
- }
211
- }
212
-
213
- console.log('');
214
-
215
- // ── Step 2: Register MCP servers ────────────────────────────────────────
216
-
217
- const claudeJsonPath = path.join(homeDir, '.claude.json');
218
- const addedMcps = registerMcpServers(claudeJsonPath);
219
-
220
- if (addedMcps.length > 0) {
221
- success(`MCP servers registered: ${addedMcps.join(', ')} → ${claudeJsonPath}`);
222
- } else {
223
- success('MCP servers already configured (playwright, context7)');
224
- }
225
-
226
- // ── Step 3: Merge settings ──────────────────────────────────────────────
227
-
228
- const settingsMerged = mergeSettings(installDir, packageDir);
229
- if (settingsMerged) {
230
- success('Permissions merged into settings.json');
231
- }
232
-
233
- // ── Done ────────────────────────────────────────────────────────────────
234
-
235
- console.log('');
236
- console.log(' ╔═══════════════════════════════════════╗');
237
- console.log(' ║ Installation complete! ║');
238
- console.log(' ╚═══════════════════════════════════════╝');
239
- console.log('');
240
- log(`${totalFiles} files installed to ${installDir}`);
241
- log('MCP servers: playwright, context7');
242
- log('');
243
- log('Restart Claude Code, then run any QAA command:');
244
- log(' /qa-start --dev-repo ./your-project');
245
- log(' /qa-create-test login');
246
- log(' /qa-map');
247
- console.log('');
248
- }
249
-
250
- main().catch(err => {
251
- fail(err.message);
252
- process.exit(1);
253
- });
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * QAA Agent Installer
5
+ *
6
+ * Installs QAA (QA Automation Agent) into the user's Claude Code environment.
7
+ *
8
+ * What it does:
9
+ * 1. Copies agents, commands, skills, templates, workflows, docs, bin, and config files
10
+ * to the chosen install directory (global ~/.claude/qaa or local ./.claude/qaa)
11
+ * 2. Registers Playwright MCP and Context7 MCP as global MCP servers
12
+ * 3. Merges required permissions into Claude Code settings.json
13
+ *
14
+ * Usage:
15
+ * npx qaa-agent
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const readline = require('readline');
21
+ const { execSync } = require('child_process');
22
+
23
+ // ── Helpers ──────────────────────────────────────────────────────────────────
24
+
25
+ function log(msg) { console.log(` ${msg}`); }
26
+ function success(msg) { console.log(` ✓ ${msg}`); }
27
+ function warn(msg) { console.log(` ⚠ ${msg}`); }
28
+ function fail(msg) { console.error(` ✗ ${msg}`); }
29
+
30
+ function ask(question) {
31
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
32
+ return new Promise(resolve => {
33
+ rl.question(` ${question} `, answer => {
34
+ rl.close();
35
+ resolve(answer.trim());
36
+ });
37
+ });
38
+ }
39
+
40
+ function copyDirRecursive(src, dest, dryRun) {
41
+ if (!fs.existsSync(src)) return 0;
42
+ if (!dryRun) fs.mkdirSync(dest, { recursive: true });
43
+ let count = 0;
44
+ const entries = fs.readdirSync(src, { withFileTypes: true });
45
+ for (const entry of entries) {
46
+ const srcPath = path.join(src, entry.name);
47
+ const destPath = path.join(dest, entry.name);
48
+ if (entry.isDirectory()) {
49
+ count += copyDirRecursive(srcPath, destPath, dryRun);
50
+ } else {
51
+ if (!dryRun) fs.copyFileSync(srcPath, destPath);
52
+ count++;
53
+ }
54
+ }
55
+ return count;
56
+ }
57
+
58
+ function deepMerge(target, source) {
59
+ for (const key of Object.keys(source)) {
60
+ if (
61
+ source[key] && typeof source[key] === 'object' && !Array.isArray(source[key]) &&
62
+ target[key] && typeof target[key] === 'object' && !Array.isArray(target[key])
63
+ ) {
64
+ deepMerge(target[key], source[key]);
65
+ } else if (Array.isArray(source[key]) && Array.isArray(target[key])) {
66
+ // Merge arrays without duplicates
67
+ const merged = [...new Set([...target[key], ...source[key]])];
68
+ target[key] = merged;
69
+ } else {
70
+ target[key] = source[key];
71
+ }
72
+ }
73
+ return target;
74
+ }
75
+
76
+ // ── MCP Registration ─────────────────────────────────────────────────────────
77
+
78
+ function registerMcpServers(claudeJsonPath, dryRun) {
79
+ const mcpServers = {
80
+ playwright: {
81
+ command: 'npx',
82
+ args: ['@playwright/mcp@latest']
83
+ },
84
+ context7: {
85
+ command: 'npx',
86
+ args: ['-y', '@upstash/context7-mcp@latest']
87
+ }
88
+ };
89
+
90
+ let config = {};
91
+ if (fs.existsSync(claudeJsonPath)) {
92
+ try {
93
+ config = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf-8'));
94
+ } catch {
95
+ config = {};
96
+ }
97
+ }
98
+
99
+ if (!config.mcpServers) config.mcpServers = {};
100
+
101
+ let added = [];
102
+ for (const [name, serverConfig] of Object.entries(mcpServers)) {
103
+ if (!config.mcpServers[name]) {
104
+ config.mcpServers[name] = serverConfig;
105
+ added.push(name);
106
+ }
107
+ }
108
+
109
+ if (!dryRun) fs.writeFileSync(claudeJsonPath, JSON.stringify(config, null, 2) + '\n');
110
+ return added;
111
+ }
112
+
113
+ // ── Settings Merge ───────────────────────────────────────────────────────────
114
+
115
+ function mergeSettings(installDir, packageDir, dryRun) {
116
+ const srcSettings = path.join(packageDir, 'settings.json');
117
+ if (!fs.existsSync(srcSettings)) return false;
118
+
119
+ const claudeDir = path.dirname(installDir);
120
+ const destSettings = path.join(claudeDir, 'settings.json');
121
+
122
+ const source = JSON.parse(fs.readFileSync(srcSettings, 'utf-8'));
123
+
124
+ let target = {};
125
+ if (fs.existsSync(destSettings)) {
126
+ try {
127
+ target = JSON.parse(fs.readFileSync(destSettings, 'utf-8'));
128
+ } catch {
129
+ target = {};
130
+ }
131
+ }
132
+
133
+ deepMerge(target, source);
134
+ if (!dryRun) fs.writeFileSync(destSettings, JSON.stringify(target, null, 2) + '\n');
135
+ return true;
136
+ }
137
+
138
+ // ── Live Location Sync (finding #52) ─────────────────────────────────────────
139
+
140
+ // Mirror QAA-owned files into a Claude Code live dir. Only files matching the
141
+ // QAA pattern are copied; QAA orphans (removed in this version) are pruned.
142
+ // Foreign files (gsd-*, sc:*, etc.) are never copied or deleted.
143
+ function syncLive(srcDir, destDir, matches, dryRun) {
144
+ const copied = [];
145
+ const pruned = [];
146
+ if (!fs.existsSync(srcDir)) return { copied, pruned, skipped: true };
147
+
148
+ const srcFiles = fs.readdirSync(srcDir).filter(matches);
149
+ const srcSet = new Set(srcFiles);
150
+
151
+ // Prune QAA orphans that no longer ship in this version
152
+ if (fs.existsSync(destDir)) {
153
+ for (const f of fs.readdirSync(destDir).filter(matches)) {
154
+ if (!srcSet.has(f)) {
155
+ pruned.push(f);
156
+ if (!dryRun) fs.rmSync(path.join(destDir, f), { force: true });
157
+ }
158
+ }
159
+ }
160
+
161
+ // Copy / overwrite QAA files
162
+ if (srcFiles.length > 0 && !dryRun) fs.mkdirSync(destDir, { recursive: true });
163
+ for (const f of srcFiles) {
164
+ copied.push(f);
165
+ if (!dryRun) fs.copyFileSync(path.join(srcDir, f), path.join(destDir, f));
166
+ }
167
+ return { copied, pruned, skipped: false };
168
+ }
169
+
170
+ // Sync CLAUDE.md into the live location using QAA markers so user content is
171
+ // preserved. Returns { action, backupPath? }.
172
+ function syncClaudeMd(installDir, claudeDir, dryRun, version) {
173
+ const srcPath = path.join(installDir, 'CLAUDE.md');
174
+ const destPath = path.join(claudeDir, 'CLAUDE.md');
175
+ if (!fs.existsSync(srcPath)) return { action: 'skip' };
176
+
177
+ const BEGIN = `<!-- QAA-BEGIN v${version} -->`;
178
+ const END = '<!-- QAA-END -->';
179
+ const markerRe = /<!-- QAA-BEGIN[^>]*-->[\s\S]*?<!-- QAA-END -->/;
180
+ const srcContent = fs.readFileSync(srcPath, 'utf-8').trim();
181
+ const wrapped = `${BEGIN}\n${srcContent}\n${END}`;
182
+
183
+ // Case 1: no live CLAUDE.md → create it wrapped
184
+ if (!fs.existsSync(destPath)) {
185
+ if (!dryRun) {
186
+ fs.mkdirSync(claudeDir, { recursive: true });
187
+ fs.writeFileSync(destPath, wrapped + '\n');
188
+ }
189
+ return { action: 'created' };
190
+ }
191
+
192
+ const existing = fs.readFileSync(destPath, 'utf-8');
193
+
194
+ // Case 2: markers present → replace only the QAA section
195
+ if (markerRe.test(existing)) {
196
+ if (!dryRun) fs.writeFileSync(destPath, existing.replace(markerRe, wrapped));
197
+ return { action: 'updated-section' };
198
+ }
199
+
200
+ // Case 3: exists without markers → backup, then replace wrapped
201
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
202
+ const backupPath = path.join(claudeDir, `CLAUDE.md.bak-${ts}`);
203
+ if (!dryRun) {
204
+ fs.copyFileSync(destPath, backupPath);
205
+ fs.writeFileSync(destPath, wrapped + '\n');
206
+ }
207
+ return { action: 'backup-replaced', backupPath };
208
+ }
209
+
210
+ // ── Main ─────────────────────────────────────────────────────────────────────
211
+
212
+ async function main() {
213
+ console.log('');
214
+ console.log(' ╔═══════════════════════════════════════╗');
215
+ console.log(' ║ QAA QA Automation Agent Installer ║');
216
+ console.log(' ╚═══════════════════════════════════════╝');
217
+ console.log('');
218
+
219
+ // Determine package root (where the npm package files are)
220
+ const packageDir = path.resolve(__dirname, '..');
221
+
222
+ const dryRun = process.argv.includes('--dry-run');
223
+ if (dryRun) warn('--dry-run mode no files will be written.');
224
+
225
+ let pkgVersion = 'unknown';
226
+ try {
227
+ pkgVersion = JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf-8')).version || 'unknown';
228
+ } catch { /* keep 'unknown' */ }
229
+
230
+ // Check that package files exist
231
+ const requiredDirs = ['agents', 'commands', 'skills'];
232
+ const missing = requiredDirs.filter(d => !fs.existsSync(path.join(packageDir, d)));
233
+ if (missing.length > 0) {
234
+ fail(`Package incomplete — missing: ${missing.join(', ')}`);
235
+ process.exit(1);
236
+ }
237
+
238
+ // Ask install scope
239
+ console.log(' Install scope:');
240
+ console.log(' 1) Global — ~/.claude/qaa (available in all projects)');
241
+ console.log(' 2) Local — ./.claude/qaa (this project only)');
242
+ console.log('');
243
+ const scopeChoice = await ask('Choose [1/2] (default: 1):');
244
+ const isGlobal = scopeChoice !== '2';
245
+
246
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
247
+ if (!homeDir) {
248
+ fail('Cannot determine home directory (HOME / USERPROFILE not set).');
249
+ process.exit(1);
250
+ }
251
+ const claudeDir = isGlobal
252
+ ? path.join(homeDir, '.claude')
253
+ : path.join(process.cwd(), '.claude');
254
+ const installDir = path.join(claudeDir, 'qaa');
255
+
256
+ // Check for existing installation
257
+ if (fs.existsSync(installDir)) {
258
+ const overwrite = await ask('QAA already installed at this location. Overwrite? [y/N]:');
259
+ if (overwrite.toLowerCase() !== 'y') {
260
+ log('Installation cancelled.');
261
+ process.exit(0);
262
+ }
263
+ }
264
+
265
+ console.log('');
266
+ log(`Installing to: ${installDir}`);
267
+ console.log('');
268
+
269
+ // ── Step 1: Copy files ──────────────────────────────────────────────────
270
+
271
+ const dirsToCopy = ['agents', 'commands', 'skills', 'templates', 'workflows', 'docs', 'bin'];
272
+ const filesToCopy = ['CLAUDE.md', 'CHANGELOG.md', '.mcp.json', 'package.json'];
273
+
274
+ let totalFiles = 0;
275
+
276
+ for (const dir of dirsToCopy) {
277
+ const src = path.join(packageDir, dir);
278
+ const dest = path.join(installDir, dir);
279
+ if (fs.existsSync(src)) {
280
+ const count = copyDirRecursive(src, dest, dryRun);
281
+ success(`${dir}/ — ${count} files`);
282
+ totalFiles += count;
283
+ }
284
+ }
285
+
286
+ for (const file of filesToCopy) {
287
+ const src = path.join(packageDir, file);
288
+ const dest = path.join(installDir, file);
289
+ if (fs.existsSync(src)) {
290
+ if (!dryRun) {
291
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
292
+ fs.copyFileSync(src, dest);
293
+ }
294
+ success(file);
295
+ totalFiles++;
296
+ }
297
+ }
298
+
299
+ console.log('');
300
+
301
+ // ── Step 2: Register MCP servers ────────────────────────────────────────
302
+
303
+ const claudeJsonPath = path.join(homeDir, '.claude.json');
304
+ const addedMcps = registerMcpServers(claudeJsonPath, dryRun);
305
+
306
+ if (addedMcps.length > 0) {
307
+ success(`MCP servers registered: ${addedMcps.join(', ')} → ${claudeJsonPath}`);
308
+ } else {
309
+ success('MCP servers already configured (playwright, context7)');
310
+ }
311
+
312
+ // ── Step 3: Merge settings ──────────────────────────────────────────────
313
+
314
+ const settingsMerged = mergeSettings(installDir, packageDir, dryRun);
315
+ if (settingsMerged) {
316
+ success('Permissions merged into settings.json');
317
+ }
318
+
319
+ // ── Step 4: Sync to live locations (finding #52) ────────────────────────
320
+ //
321
+ // Claude Code loads slash commands from <claudeDir>/commands, sub-agents from
322
+ // <claudeDir>/agents, and global standards from <claudeDir>/CLAUDE.md — NOT
323
+ // from the qaa/ install dir. Mirror QAA files there so new versions activate.
324
+ // Only QAA-owned files (qa*.md / qaa-*.md) are touched.
325
+
326
+ console.log('');
327
+
328
+ const qaMatch = (f) => /^qa.*\.md$/i.test(f);
329
+ const tag = dryRun ? '[dry-run] ' : '';
330
+
331
+ const cmdSync = syncLive(
332
+ path.join(installDir, 'commands'),
333
+ path.join(claudeDir, 'commands'),
334
+ qaMatch, dryRun
335
+ );
336
+ success(`${tag}Live commands → ${path.join(claudeDir, 'commands')} (${cmdSync.copied.length} synced${cmdSync.pruned.length ? `, ${cmdSync.pruned.length} pruned` : ''})`);
337
+ if (cmdSync.pruned.length) log(`${tag}Pruned: ${cmdSync.pruned.join(', ')}`);
338
+
339
+ const agentSync = syncLive(
340
+ path.join(installDir, 'agents'),
341
+ path.join(claudeDir, 'agents'),
342
+ qaMatch, dryRun
343
+ );
344
+ success(`${tag}Live agents → ${path.join(claudeDir, 'agents')} (${agentSync.copied.length} synced${agentSync.pruned.length ? `, ${agentSync.pruned.length} pruned` : ''})`);
345
+ if (agentSync.pruned.length) log(`${tag}Pruned: ${agentSync.pruned.join(', ')}`);
346
+
347
+ const cmd = syncClaudeMd(installDir, claudeDir, dryRun, pkgVersion);
348
+ if (cmd.action === 'created') success(`${tag}CLAUDE.md created (QAA markers) → ${path.join(claudeDir, 'CLAUDE.md')}`);
349
+ else if (cmd.action === 'updated-section') success(`${tag}CLAUDE.md QAA section updated → ${path.join(claudeDir, 'CLAUDE.md')}`);
350
+ else if (cmd.action === 'backup-replaced') warn(`${tag}CLAUDE.md had no QAA markers — backed up to ${cmd.backupPath}, replaced with marked version`);
351
+ else if (cmd.action === 'skip') warn(`${tag}No CLAUDE.md in package to sync`);
352
+
353
+ // ── Done ────────────────────────────────────────────────────────────────
354
+
355
+ console.log('');
356
+ console.log(' ╔═══════════════════════════════════════╗');
357
+ console.log(' ║ Installation complete! ║');
358
+ console.log(' ╚═══════════════════════════════════════╝');
359
+ console.log('');
360
+ if (dryRun) log('Dry-run complete — nothing was written.');
361
+ log(`${totalFiles} files installed to ${installDir}`);
362
+ log(`Live commands & agents synced to ${claudeDir}`);
363
+ log('MCP servers: playwright, context7');
364
+ log('');
365
+ log('Restart Claude Code, then run any QAA command:');
366
+ log(' /qa-start --dev-repo ./your-project');
367
+ log(' /qa-create-test login');
368
+ log(' /qa-map');
369
+ console.log('');
370
+ }
371
+
372
+ main().catch(err => {
373
+ fail(err.message);
374
+ process.exit(1);
375
+ });