@nerviq/cli 1.8.0 → 1.8.3
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/bin/cli.js +258 -28
- package/package.json +1 -1
- package/src/activity.js +97 -3
- package/src/audit.js +5 -1
- package/src/fix-prompts.js +122 -0
- package/src/init.js +184 -0
- package/src/usage-patterns.js +99 -0
package/bin/cli.js
CHANGED
|
@@ -6,8 +6,9 @@ const { analyzeProject, printAnalysis, exportMarkdown } = require('../src/analyz
|
|
|
6
6
|
const { buildProposalBundle, printProposalBundle, writePlanFile, applyProposalBundle, printApplyResult } = require('../src/plans');
|
|
7
7
|
const { getGovernanceSummary, printGovernanceSummary, ensureWritableProfile, renderGovernanceMarkdown } = require('../src/governance');
|
|
8
8
|
const { runBenchmark, printBenchmark, writeBenchmarkReport } = require('../src/benchmark');
|
|
9
|
-
const { writeSnapshotArtifact, recordRecommendationOutcome, formatRecommendationOutcomeSummary, getRecommendationOutcomeSummary } = require('../src/activity');
|
|
9
|
+
const { writeSnapshotArtifact, writeRollbackArtifact, recordRecommendationOutcome, formatRecommendationOutcomeSummary, getRecommendationOutcomeSummary } = require('../src/activity');
|
|
10
10
|
const { collectFeedback } = require('../src/feedback');
|
|
11
|
+
const { recordPattern, getPriorityAdjustment, formatUsageSummary } = require('../src/usage-patterns');
|
|
11
12
|
const { startServer } = require('../src/server');
|
|
12
13
|
const { auditWorkspaces } = require('../src/workspace');
|
|
13
14
|
const { scanOrg } = require('../src/org');
|
|
@@ -26,7 +27,7 @@ const COMMAND_ALIASES = {
|
|
|
26
27
|
gov: 'governance',
|
|
27
28
|
outcome: 'feedback',
|
|
28
29
|
};
|
|
29
|
-
const KNOWN_COMMANDS = ['audit', 'org', 'setup', 'augment', 'suggest-only', 'plan', 'apply', 'fix', 'rollback', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'doctor', 'convert', 'migrate', 'catalog', 'certify', 'serve', 'check-health', 'dashboard', 'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise', 'harmony-watch', 'harmony-governance', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export', 'freshness', 'help', 'version'];
|
|
30
|
+
const KNOWN_COMMANDS = ['audit', 'org', 'setup', 'init', 'augment', 'suggest-only', 'plan', 'apply', 'fix', 'rollback', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'doctor', 'convert', 'migrate', 'catalog', 'certify', 'serve', 'check-health', 'dashboard', 'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise', 'harmony-watch', 'harmony-governance', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export', 'freshness', 'help', 'version'];
|
|
30
31
|
|
|
31
32
|
function levenshtein(a, b) {
|
|
32
33
|
const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
@@ -328,8 +329,10 @@ const HELP = `
|
|
|
328
329
|
FIX
|
|
329
330
|
nerviq fix Show fixable checks and manual-fix guidance
|
|
330
331
|
nerviq fix <key> Auto-fix a specific check (with score impact)
|
|
332
|
+
nerviq fix <key> --prompt Show AI agent prompt for a check (no auto-fix)
|
|
331
333
|
nerviq fix --all-critical Fix all critical issues at once
|
|
332
334
|
nerviq fix --dry-run Preview fixes without writing
|
|
335
|
+
nerviq fix --auto Apply fixes without confirmation prompt
|
|
333
336
|
nerviq rollback Undo the most recent apply (delete created files)
|
|
334
337
|
nerviq rollback --list Show available rollback points
|
|
335
338
|
nerviq rollback --dry-run Preview what would be deleted
|
|
@@ -744,6 +747,17 @@ async function main() {
|
|
|
744
747
|
});
|
|
745
748
|
return; // keep process alive for http
|
|
746
749
|
} else if (normalizedCommand === 'feedback') {
|
|
750
|
+
if (flags.includes('--patterns')) {
|
|
751
|
+
if (options.json) {
|
|
752
|
+
const { getUsageSummary } = require('../src/usage-patterns');
|
|
753
|
+
console.log(JSON.stringify(getUsageSummary(options.dir), null, 2));
|
|
754
|
+
} else {
|
|
755
|
+
console.log('');
|
|
756
|
+
console.log(formatUsageSummary(options.dir));
|
|
757
|
+
console.log('');
|
|
758
|
+
}
|
|
759
|
+
process.exit(0);
|
|
760
|
+
}
|
|
747
761
|
if (parsed.feedbackKey) {
|
|
748
762
|
if (!parsed.feedbackStatus) {
|
|
749
763
|
console.error('\n Error: feedback logging requires --status when --key is provided.\n');
|
|
@@ -1283,9 +1297,11 @@ async function main() {
|
|
|
1283
1297
|
console.log(output);
|
|
1284
1298
|
process.exit(0);
|
|
1285
1299
|
} else if (normalizedCommand === 'fix') {
|
|
1286
|
-
// nerviq fix [key] [--all-critical] [--dry-run]
|
|
1300
|
+
// nerviq fix [key] [--all-critical] [--dry-run] [--auto] [--prompt]
|
|
1287
1301
|
const fixKey = parsed.extraArgs[0] || null;
|
|
1288
1302
|
const allCritical = flags.includes('--all-critical');
|
|
1303
|
+
const promptOnly = flags.includes('--prompt');
|
|
1304
|
+
const autoApply = options.auto || options.dryRun;
|
|
1289
1305
|
|
|
1290
1306
|
// Step 1: Run silent audit to find failed checks (only actual failures, not skipped/null)
|
|
1291
1307
|
const auditResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
|
|
@@ -1298,6 +1314,7 @@ async function main() {
|
|
|
1298
1314
|
|
|
1299
1315
|
// Step 2: Determine which checks to fix
|
|
1300
1316
|
const { TECHNIQUES } = require('../src/techniques');
|
|
1317
|
+
const { FIX_PROMPTS, formatFixPrompt } = require('../src/fix-prompts');
|
|
1301
1318
|
const fs = require('fs');
|
|
1302
1319
|
const pathMod = require('path');
|
|
1303
1320
|
|
|
@@ -1346,6 +1363,18 @@ async function main() {
|
|
|
1346
1363
|
}
|
|
1347
1364
|
process.exit(1);
|
|
1348
1365
|
}
|
|
1366
|
+
// --prompt flag: show AI prompt and exit without attempting fix
|
|
1367
|
+
if (promptOnly) {
|
|
1368
|
+
const prompt = FIX_PROMPTS[fixKey];
|
|
1369
|
+
if (prompt) {
|
|
1370
|
+
console.log(formatFixPrompt(fixKey, prompt));
|
|
1371
|
+
} else {
|
|
1372
|
+
const failedCheck = failedResults.find(r => r.key === fixKey);
|
|
1373
|
+
console.log(`\n No AI prompt available for '${fixKey}'.`);
|
|
1374
|
+
console.log(` Manual fix: ${failedCheck ? failedCheck.fix : 'See nerviq audit --full.'}\n`);
|
|
1375
|
+
}
|
|
1376
|
+
process.exit(0);
|
|
1377
|
+
}
|
|
1349
1378
|
targetKeys = [fixKey];
|
|
1350
1379
|
} else if (allCritical) {
|
|
1351
1380
|
targetKeys = failedResults.filter(r => r.impact === 'critical').map(r => r.key);
|
|
@@ -1369,15 +1398,30 @@ async function main() {
|
|
|
1369
1398
|
console.log('');
|
|
1370
1399
|
}
|
|
1371
1400
|
if (nonFixable.length > 0) {
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
console.log(`
|
|
1401
|
+
const withPrompt = nonFixable.filter(r => FIX_PROMPTS[r.key]);
|
|
1402
|
+
const withoutPrompt = nonFixable.filter(r => !FIX_PROMPTS[r.key]);
|
|
1403
|
+
if (withPrompt.length > 0) {
|
|
1404
|
+
console.log(` AI prompt available (${withPrompt.length}):`);
|
|
1405
|
+
for (const r of withPrompt.slice(0, 5)) {
|
|
1406
|
+
const tier = r.impact === 'critical' ? '🔴' : r.impact === 'high' ? '🟡' : '🔵';
|
|
1407
|
+
console.log(` ${tier} nerviq fix ${r.key} --prompt`);
|
|
1408
|
+
}
|
|
1409
|
+
if (withPrompt.length > 5) {
|
|
1410
|
+
console.log(` ... and ${withPrompt.length - 5} more`);
|
|
1411
|
+
}
|
|
1412
|
+
console.log('');
|
|
1376
1413
|
}
|
|
1377
|
-
if (
|
|
1378
|
-
console.log(`
|
|
1414
|
+
if (withoutPrompt.length > 0) {
|
|
1415
|
+
console.log(` Manual fix needed (${withoutPrompt.length}):`);
|
|
1416
|
+
for (const r of withoutPrompt.slice(0, 5)) {
|
|
1417
|
+
const tier = r.impact === 'critical' ? '🔴' : r.impact === 'high' ? '🟡' : '🔵';
|
|
1418
|
+
console.log(` ${tier} ${r.key}: ${r.fix}`);
|
|
1419
|
+
}
|
|
1420
|
+
if (withoutPrompt.length > 5) {
|
|
1421
|
+
console.log(` ... and ${withoutPrompt.length - 5} more (use --full to see all)`);
|
|
1422
|
+
}
|
|
1423
|
+
console.log('');
|
|
1379
1424
|
}
|
|
1380
|
-
console.log('');
|
|
1381
1425
|
}
|
|
1382
1426
|
if (fixable.length > 0) {
|
|
1383
1427
|
console.log(` Quick actions:`);
|
|
@@ -1388,55 +1432,241 @@ async function main() {
|
|
|
1388
1432
|
process.exit(0);
|
|
1389
1433
|
}
|
|
1390
1434
|
|
|
1391
|
-
// Step
|
|
1435
|
+
// Step 2.5: Predict impact and show preview before applying
|
|
1436
|
+
const IMPACT_WEIGHTS = { critical: 15, high: 10, medium: 5, low: 2 };
|
|
1392
1437
|
const preScore = auditResult.score;
|
|
1438
|
+
const applicableResults = (auditResult.results || []).filter(r => r.passed !== null);
|
|
1439
|
+
const maxScore = applicableResults.reduce((sum, r) => sum + (IMPACT_WEIGHTS[r.impact] || 5), 0);
|
|
1440
|
+
|
|
1441
|
+
// Compute predicted score by simulating target fixes as passing
|
|
1442
|
+
const targetKeySet = new Set(targetKeys);
|
|
1443
|
+
const INLINE_FIX_KEYS = new Set(Object.keys(INLINE_FIXERS));
|
|
1444
|
+
const fixableTargets = targetKeys.filter(k => {
|
|
1445
|
+
const tech = TECHNIQUES[k];
|
|
1446
|
+
return (tech && tech.template) || INLINE_FIX_KEYS.has(k);
|
|
1447
|
+
});
|
|
1448
|
+
const fixableTargetSet = new Set(fixableTargets);
|
|
1449
|
+
const simulatedEarned = applicableResults.reduce((sum, r) => {
|
|
1450
|
+
const w = IMPACT_WEIGHTS[r.impact] || 5;
|
|
1451
|
+
if (r.passed) return sum + w;
|
|
1452
|
+
if (fixableTargetSet.has(r.key)) return sum + w;
|
|
1453
|
+
return sum;
|
|
1454
|
+
}, 0);
|
|
1455
|
+
const predictedScore = maxScore > 0 ? Math.round((simulatedEarned / maxScore) * 100) : 0;
|
|
1456
|
+
const predictedDelta = predictedScore - preScore;
|
|
1457
|
+
|
|
1458
|
+
if (!autoApply) {
|
|
1459
|
+
console.log('');
|
|
1460
|
+
if (allCritical && fixableTargets.length > 1) {
|
|
1461
|
+
// Multi-fix summary
|
|
1462
|
+
console.log(` ${fixableTargets.length} critical fixes available:`);
|
|
1463
|
+
let runningEarned = applicableResults.reduce((s, r) => s + (r.passed ? (IMPACT_WEIGHTS[r.impact] || 5) : 0), 0);
|
|
1464
|
+
let runningScore = maxScore > 0 ? Math.round((runningEarned / maxScore) * 100) : 0;
|
|
1465
|
+
fixableTargets.forEach((k, idx) => {
|
|
1466
|
+
const r = failedResults.find(fr => fr.key === k);
|
|
1467
|
+
const w = IMPACT_WEIGHTS[r.impact] || 5;
|
|
1468
|
+
const nextEarned = runningEarned + w;
|
|
1469
|
+
const nextScore = maxScore > 0 ? Math.round((nextEarned / maxScore) * 100) : 0;
|
|
1470
|
+
const d = nextScore - runningScore;
|
|
1471
|
+
console.log(` ${idx + 1}. ${(r.key).padEnd(18)} ${runningScore} → ${nextScore} (+${d})`);
|
|
1472
|
+
runningEarned = nextEarned;
|
|
1473
|
+
runningScore = nextScore;
|
|
1474
|
+
});
|
|
1475
|
+
console.log('');
|
|
1476
|
+
console.log(` Total: ${preScore} → ${predictedScore} (+${predictedDelta})`);
|
|
1477
|
+
} else {
|
|
1478
|
+
// Single fix preview
|
|
1479
|
+
const targetCheck = failedResults.find(r => r.key === fixableTargets[0]) || failedResults.find(r => r.key === targetKeys[0]);
|
|
1480
|
+
if (targetCheck) {
|
|
1481
|
+
console.log(` Predicted impact: ${preScore} → ${predictedScore} (+${predictedDelta})`);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// Prompt for confirmation
|
|
1486
|
+
const readline = require('readline');
|
|
1487
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1488
|
+
const answer = await new Promise(resolve => {
|
|
1489
|
+
rl.question(' Apply? (Y/n) ', resolve);
|
|
1490
|
+
});
|
|
1491
|
+
rl.close();
|
|
1492
|
+
if (answer && answer.trim().toLowerCase() === 'n') {
|
|
1493
|
+
for (const key of targetKeys) {
|
|
1494
|
+
recordPattern(options.dir, key, 'rejected');
|
|
1495
|
+
}
|
|
1496
|
+
console.log('\n Aborted.\n');
|
|
1497
|
+
process.exit(0);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// Step 3: Create rollback snapshot before applying fixes
|
|
1502
|
+
const isBatch = allCritical && targetKeys.length > 1;
|
|
1503
|
+
let rollbackId = null;
|
|
1504
|
+
const allCreatedFiles = [];
|
|
1505
|
+
const fixResults = []; // { key, name, status, delta }
|
|
1506
|
+
|
|
1507
|
+
if (!options.dryRun && targetKeys.length > 0) {
|
|
1508
|
+
// Snapshot existing files for rollback
|
|
1509
|
+
const snapshotFiles = {};
|
|
1510
|
+
for (const key of targetKeys) {
|
|
1511
|
+
const technique = TECHNIQUES[key];
|
|
1512
|
+
if (technique && technique.template && technique.template.path) {
|
|
1513
|
+
const tplPath = pathMod.join(options.dir, technique.template.path);
|
|
1514
|
+
if (fs.existsSync(tplPath)) {
|
|
1515
|
+
snapshotFiles[technique.template.path] = fs.readFileSync(tplPath, 'utf8');
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
const rollbackArtifact = writeRollbackArtifact(options.dir, {
|
|
1520
|
+
sourcePlan: 'fix-batch',
|
|
1521
|
+
preSnapshot: snapshotFiles,
|
|
1522
|
+
createdFiles: [],
|
|
1523
|
+
patchedFiles: Object.keys(snapshotFiles),
|
|
1524
|
+
rollbackInstructions: ['Use nerviq rollback to undo these fixes'],
|
|
1525
|
+
});
|
|
1526
|
+
rollbackId = rollbackArtifact.id;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// Step 3b: Apply fixes sequentially with progress
|
|
1393
1530
|
let fixed = 0;
|
|
1394
1531
|
let manual = 0;
|
|
1532
|
+
let runningScore = preScore;
|
|
1395
1533
|
|
|
1396
|
-
for (
|
|
1534
|
+
for (let i = 0; i < targetKeys.length; i++) {
|
|
1535
|
+
const key = targetKeys[i];
|
|
1397
1536
|
const technique = TECHNIQUES[key];
|
|
1398
1537
|
const failedCheck = failedResults.find(r => r.key === key);
|
|
1538
|
+
const progress = isBatch ? `${i + 1}/${targetKeys.length}: ` : '';
|
|
1399
1539
|
|
|
1400
1540
|
if (technique && technique.template) {
|
|
1401
1541
|
if (options.dryRun) {
|
|
1402
|
-
console.log(
|
|
1542
|
+
console.log(` [dry-run] Would fix: ${progress}${failedCheck.name} (${key})`);
|
|
1543
|
+
fixResults.push({ key, name: failedCheck.name, status: 'dry-run', delta: 0 });
|
|
1403
1544
|
fixed++;
|
|
1404
1545
|
} else {
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1546
|
+
try {
|
|
1547
|
+
if (isBatch) console.log(` Fixing ${progress}${key}...`);
|
|
1548
|
+
const setupResult = await setup({ ...options, only: [key], silent: true });
|
|
1549
|
+
if (setupResult && setupResult.writtenFiles) {
|
|
1550
|
+
allCreatedFiles.push(...setupResult.writtenFiles);
|
|
1551
|
+
}
|
|
1552
|
+
const midResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
|
|
1553
|
+
const delta = midResult.score - runningScore;
|
|
1554
|
+
fixResults.push({ key, name: failedCheck.name, status: 'fixed', delta });
|
|
1555
|
+
runningScore = midResult.score;
|
|
1556
|
+
if (!isBatch) console.log(` ✅ Fixed: ${failedCheck.name}`);
|
|
1557
|
+
fixed++;
|
|
1558
|
+
} catch (err) {
|
|
1559
|
+
fixResults.push({ key, name: failedCheck.name, status: 'failed', delta: 0 });
|
|
1560
|
+
if (isBatch) {
|
|
1561
|
+
console.log(` ❌ Failed: ${key} — ${err.message}`);
|
|
1562
|
+
console.log(` Stopping batch. ${fixed} fixes applied so far.`);
|
|
1563
|
+
console.log(` Rollback: nerviq rollback --id ${rollbackId}`);
|
|
1564
|
+
break;
|
|
1565
|
+
} else {
|
|
1566
|
+
console.log(` ❌ Failed: ${failedCheck.name} — ${err.message}`);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1409
1569
|
}
|
|
1410
1570
|
} else if (INLINE_FIXERS[key]) {
|
|
1411
1571
|
if (options.dryRun) {
|
|
1412
|
-
console.log(` [dry-run] Would fix: ${failedCheck.name} (${key})`);
|
|
1572
|
+
console.log(` [dry-run] Would fix: ${progress}${failedCheck.name} (${key})`);
|
|
1573
|
+
fixResults.push({ key, name: failedCheck.name, status: 'dry-run', delta: 0 });
|
|
1413
1574
|
fixed++;
|
|
1414
1575
|
} else {
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1576
|
+
try {
|
|
1577
|
+
if (isBatch) console.log(` Fixing ${progress}${key}...`);
|
|
1578
|
+
const didFix = INLINE_FIXERS[key](options.dir);
|
|
1579
|
+
if (didFix) {
|
|
1580
|
+
const midResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
|
|
1581
|
+
const delta = midResult.score - runningScore;
|
|
1582
|
+
fixResults.push({ key, name: failedCheck.name, status: 'fixed', delta });
|
|
1583
|
+
runningScore = midResult.score;
|
|
1584
|
+
if (!isBatch) console.log(` ✅ Fixed: ${failedCheck.name}`);
|
|
1585
|
+
fixed++;
|
|
1586
|
+
} else {
|
|
1587
|
+
fixResults.push({ key, name: failedCheck.name, status: 'skipped', delta: 0 });
|
|
1588
|
+
if (!isBatch) console.log(` ⏭️ Already fixed: ${failedCheck.name}`);
|
|
1589
|
+
}
|
|
1590
|
+
} catch (err) {
|
|
1591
|
+
fixResults.push({ key, name: failedCheck.name, status: 'failed', delta: 0 });
|
|
1592
|
+
if (isBatch) {
|
|
1593
|
+
console.log(` ❌ Failed: ${key} — ${err.message}`);
|
|
1594
|
+
console.log(` Stopping batch. ${fixed} fixes applied so far.`);
|
|
1595
|
+
console.log(` Rollback: nerviq rollback --id ${rollbackId}`);
|
|
1596
|
+
break;
|
|
1597
|
+
}
|
|
1421
1598
|
}
|
|
1422
1599
|
}
|
|
1423
1600
|
} else {
|
|
1424
|
-
|
|
1425
|
-
|
|
1601
|
+
if (!isBatch) {
|
|
1602
|
+
const aiPrompt = FIX_PROMPTS[key];
|
|
1603
|
+
if (aiPrompt) {
|
|
1604
|
+
console.log(formatFixPrompt(key, aiPrompt));
|
|
1605
|
+
} else {
|
|
1606
|
+
console.log(` 📋 ${failedCheck.name} (manual fix needed)`);
|
|
1607
|
+
console.log(` ${failedCheck.fix}`);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
fixResults.push({ key, name: failedCheck.name, status: 'skipped', delta: 0 });
|
|
1426
1611
|
manual++;
|
|
1427
1612
|
}
|
|
1428
1613
|
}
|
|
1429
1614
|
|
|
1430
|
-
//
|
|
1431
|
-
if (
|
|
1615
|
+
// Record accepted patterns for successfully fixed checks
|
|
1616
|
+
if (!options.dryRun) {
|
|
1617
|
+
for (const key of targetKeys) {
|
|
1618
|
+
const fr = fixResults.find(r => r.key === key);
|
|
1619
|
+
recordPattern(options.dir, key, fr && fr.status === 'fixed' ? 'accepted' : 'rejected');
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// Update rollback artifact with actual created files
|
|
1624
|
+
if (!options.dryRun && rollbackId && allCreatedFiles.length > 0) {
|
|
1625
|
+
const { ensureArtifactDirs } = require('../src/activity');
|
|
1626
|
+
const { rollbackDir } = ensureArtifactDirs(options.dir);
|
|
1627
|
+
const rbFiles = fs.readdirSync(rollbackDir).filter(f => f.includes(rollbackId));
|
|
1628
|
+
if (rbFiles.length > 0) {
|
|
1629
|
+
const rbPath = pathMod.join(rollbackDir, rbFiles[0]);
|
|
1630
|
+
try {
|
|
1631
|
+
const rbData = JSON.parse(fs.readFileSync(rbPath, 'utf8'));
|
|
1632
|
+
rbData.createdFiles = allCreatedFiles;
|
|
1633
|
+
fs.writeFileSync(rbPath, JSON.stringify(rbData, null, 2), 'utf8');
|
|
1634
|
+
} catch { /* best effort */ }
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// Step 4: Show batch summary or simple score impact
|
|
1639
|
+
if (isBatch && fixResults.length > 0) {
|
|
1640
|
+
console.log('');
|
|
1641
|
+
console.log(' Batch fix complete:');
|
|
1642
|
+
for (let i = 0; i < fixResults.length; i++) {
|
|
1643
|
+
const r = fixResults[i];
|
|
1644
|
+
const icon = r.status === 'fixed' ? '✅' : r.status === 'failed' ? '❌' : '⚠ ';
|
|
1645
|
+
const deltaStr = r.status === 'fixed' ? ` (+${r.delta})` : r.status === 'skipped' ? ' (skipped — no auto-fix)' : r.status === 'failed' ? ' (failed)' : ' (dry-run)';
|
|
1646
|
+
console.log(` ${icon} ${i + 1}. ${r.key.padEnd(20)}${deltaStr}`);
|
|
1647
|
+
}
|
|
1648
|
+
const totalDelta = runningScore - preScore;
|
|
1649
|
+
console.log('');
|
|
1650
|
+
console.log(` Score: ${preScore} → ${runningScore} (${totalDelta >= 0 ? '+' : ''}${totalDelta})`);
|
|
1651
|
+
if (rollbackId && !options.dryRun) {
|
|
1652
|
+
console.log(` Rollback available: nerviq rollback --id ${rollbackId}`);
|
|
1653
|
+
}
|
|
1654
|
+
} else if (fixed > 0 && !options.dryRun) {
|
|
1432
1655
|
const postResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
|
|
1433
1656
|
const delta = postResult.score - preScore;
|
|
1434
1657
|
console.log('');
|
|
1435
1658
|
console.log(` Score: ${preScore} → ${postResult.score} (${delta >= 0 ? '+' : ''}${delta})`);
|
|
1659
|
+
if (rollbackId) {
|
|
1660
|
+
console.log(` Rollback available: nerviq rollback --id ${rollbackId}`);
|
|
1661
|
+
}
|
|
1436
1662
|
}
|
|
1437
1663
|
|
|
1438
1664
|
console.log(`\n ${fixed} fixed, ${manual} need manual action.\n`);
|
|
1439
1665
|
|
|
1666
|
+
} else if (normalizedCommand === 'init') {
|
|
1667
|
+
const { runInit } = require('../src/init');
|
|
1668
|
+
await runInit(options.dir, flags);
|
|
1669
|
+
process.exit(0);
|
|
1440
1670
|
} else if (normalizedCommand === 'setup') {
|
|
1441
1671
|
await setup(options);
|
|
1442
1672
|
if (options.snapshot) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nerviq/cli",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.3",
|
|
4
4
|
"description": "The intelligent nervous system for AI coding agents — 2,431 checks across 8 platforms, 10 languages, and 62 domain packs. Audit, align, and amplify.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/activity.js
CHANGED
|
@@ -557,12 +557,13 @@ function loadSnapshotPayload(dir, indexEntry) {
|
|
|
557
557
|
* Analyze check health by comparing the two most recent audit snapshots.
|
|
558
558
|
* Detects checks that regressed (passed → failed), improved (failed → passed),
|
|
559
559
|
* and flags sudden drops that may indicate platform format changes.
|
|
560
|
+
* When more than 2 snapshots exist, also computes per-check pass rates.
|
|
560
561
|
*
|
|
561
562
|
* @param {string} dir - Project root directory.
|
|
562
563
|
* @returns {Object|null} Health report, or null if fewer than 2 audit snapshots exist.
|
|
563
564
|
*/
|
|
564
565
|
function checkHealth(dir) {
|
|
565
|
-
const history = getHistory(dir,
|
|
566
|
+
const history = getHistory(dir, 20);
|
|
566
567
|
if (history.length < 2) return null;
|
|
567
568
|
|
|
568
569
|
const currentPayload = loadSnapshotPayload(dir, history[0]);
|
|
@@ -627,15 +628,20 @@ function checkHealth(dir) {
|
|
|
627
628
|
}
|
|
628
629
|
}
|
|
629
630
|
|
|
631
|
+
// Per-check pass rates across all snapshots
|
|
632
|
+
const passRates = computePassRates(dir, history);
|
|
633
|
+
|
|
630
634
|
return {
|
|
631
635
|
currentDate: history[0].createdAt,
|
|
632
636
|
previousDate: history[1].createdAt,
|
|
637
|
+
snapshotsAnalyzed: history.length,
|
|
633
638
|
scoreDelta: (currentPayload.score || 0) - (previousPayload.score || 0),
|
|
634
639
|
regressions,
|
|
635
640
|
improvements,
|
|
636
641
|
newChecks,
|
|
637
642
|
removedChecks,
|
|
638
643
|
platformAlerts,
|
|
644
|
+
passRates,
|
|
639
645
|
summary: {
|
|
640
646
|
regressionsCount: regressions.length,
|
|
641
647
|
improvementsCount: improvements.length,
|
|
@@ -646,6 +652,66 @@ function checkHealth(dir) {
|
|
|
646
652
|
};
|
|
647
653
|
}
|
|
648
654
|
|
|
655
|
+
/**
|
|
656
|
+
* Compute per-check pass rates across all snapshots.
|
|
657
|
+
* Returns { declining, consistentlyFailing, consistentlyPassing, overallHealth }.
|
|
658
|
+
*/
|
|
659
|
+
function computePassRates(dir, history) {
|
|
660
|
+
// key → { passes, total, recentResults: [bool...] (newest first) }
|
|
661
|
+
const stats = {};
|
|
662
|
+
for (const entry of history) {
|
|
663
|
+
const payload = loadSnapshotPayload(dir, entry);
|
|
664
|
+
if (!payload || !payload.results) continue;
|
|
665
|
+
for (const r of payload.results) {
|
|
666
|
+
if (!r.key || r.passed === null || r.passed === undefined) continue;
|
|
667
|
+
if (!stats[r.key]) stats[r.key] = { name: r.name, passes: 0, total: 0, recentResults: [] };
|
|
668
|
+
stats[r.key].total++;
|
|
669
|
+
if (r.passed) stats[r.key].passes++;
|
|
670
|
+
stats[r.key].recentResults.push(!!r.passed);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const declining = [];
|
|
675
|
+
const consistentlyFailing = [];
|
|
676
|
+
let consistentlyPassingCount = 0;
|
|
677
|
+
let totalChecks = 0;
|
|
678
|
+
let totalPasses = 0;
|
|
679
|
+
let totalAppearances = 0;
|
|
680
|
+
|
|
681
|
+
for (const [key, s] of Object.entries(stats)) {
|
|
682
|
+
const rate = s.total > 0 ? s.passes / s.total : 0;
|
|
683
|
+
totalChecks++;
|
|
684
|
+
totalPasses += s.passes;
|
|
685
|
+
totalAppearances += s.total;
|
|
686
|
+
|
|
687
|
+
if (s.total >= 2 && rate === 0) {
|
|
688
|
+
consistentlyFailing.push({ key, name: s.name, runs: s.total });
|
|
689
|
+
} else if (rate === 1) {
|
|
690
|
+
consistentlyPassingCount++;
|
|
691
|
+
} else if (s.total >= 2) {
|
|
692
|
+
// Check if declining: earlier results passed, recent ones failed
|
|
693
|
+
const half = Math.ceil(s.recentResults.length / 2);
|
|
694
|
+
const recentHalf = s.recentResults.slice(0, half);
|
|
695
|
+
const olderHalf = s.recentResults.slice(half);
|
|
696
|
+
const recentRate = recentHalf.filter(Boolean).length / recentHalf.length;
|
|
697
|
+
const olderRate = olderHalf.length > 0 ? olderHalf.filter(Boolean).length / olderHalf.length : recentRate;
|
|
698
|
+
if (olderRate > recentRate) {
|
|
699
|
+
const failStreak = s.recentResults.findIndex(v => v === true);
|
|
700
|
+
declining.push({
|
|
701
|
+
key, name: s.name,
|
|
702
|
+
oldRate: Math.round(olderRate * 100),
|
|
703
|
+
newRate: Math.round(recentRate * 100),
|
|
704
|
+
failingRuns: failStreak === -1 ? s.recentResults.length : failStreak,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const overallHealth = totalAppearances > 0 ? Math.round((totalPasses / totalAppearances) * 100) : 100;
|
|
711
|
+
|
|
712
|
+
return { declining, consistentlyFailing, consistentlyPassingCount, overallHealth };
|
|
713
|
+
}
|
|
714
|
+
|
|
649
715
|
/**
|
|
650
716
|
* Format check-health report for CLI display.
|
|
651
717
|
*/
|
|
@@ -653,11 +719,12 @@ function formatCheckHealth(healthReport) {
|
|
|
653
719
|
if (!healthReport) return 'Need at least 2 audit snapshots. Run `nerviq audit --snapshot` twice.';
|
|
654
720
|
|
|
655
721
|
const lines = [];
|
|
656
|
-
const { scoreDelta, regressions, improvements, platformAlerts, newChecks,
|
|
722
|
+
const { scoreDelta, regressions, improvements, platformAlerts, newChecks, passRates } = healthReport;
|
|
657
723
|
const sign = scoreDelta >= 0 ? '+' : '';
|
|
658
724
|
|
|
659
725
|
lines.push(` Check Health Report`);
|
|
660
|
-
lines.push(`
|
|
726
|
+
lines.push(` ═══════════════════════════════════════`);
|
|
727
|
+
lines.push(` Snapshots analyzed: ${healthReport.snapshotsAnalyzed}`);
|
|
661
728
|
lines.push(` Period: ${healthReport.previousDate?.split('T')[0]} → ${healthReport.currentDate?.split('T')[0]}`);
|
|
662
729
|
lines.push(` Score delta: ${sign}${scoreDelta}`);
|
|
663
730
|
lines.push('');
|
|
@@ -671,6 +738,23 @@ function formatCheckHealth(healthReport) {
|
|
|
671
738
|
lines.push('');
|
|
672
739
|
}
|
|
673
740
|
|
|
741
|
+
if (passRates && passRates.declining.length > 0) {
|
|
742
|
+
lines.push(` Checks with declining pass rate:`);
|
|
743
|
+
for (const d of passRates.declining) {
|
|
744
|
+
const detail = d.failingRuns > 0 ? `(failing in last ${d.failingRuns} runs)` : '';
|
|
745
|
+
lines.push(` ⚠ ${d.key.padEnd(22)} ${d.oldRate}% → ${d.newRate}% ${detail}`);
|
|
746
|
+
}
|
|
747
|
+
lines.push('');
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (passRates && passRates.consistentlyFailing.length > 0) {
|
|
751
|
+
lines.push(` Consistently failing (0% pass rate):`);
|
|
752
|
+
for (const f of passRates.consistentlyFailing) {
|
|
753
|
+
lines.push(` ✗ ${f.key.padEnd(22)} 0/${f.runs} runs`);
|
|
754
|
+
}
|
|
755
|
+
lines.push('');
|
|
756
|
+
}
|
|
757
|
+
|
|
674
758
|
if (regressions.length > 0) {
|
|
675
759
|
lines.push(` 🔴 Regressions (${regressions.length} checks now failing)`);
|
|
676
760
|
for (const r of regressions) {
|
|
@@ -692,11 +776,21 @@ function formatCheckHealth(healthReport) {
|
|
|
692
776
|
lines.push('');
|
|
693
777
|
}
|
|
694
778
|
|
|
779
|
+
if (passRates && passRates.consistentlyPassingCount > 0) {
|
|
780
|
+
lines.push(` Consistently passing (100%):`);
|
|
781
|
+
lines.push(` ✓ ${passRates.consistentlyPassingCount} checks at 100% pass rate`);
|
|
782
|
+
lines.push('');
|
|
783
|
+
}
|
|
784
|
+
|
|
695
785
|
if (regressions.length === 0 && platformAlerts.length === 0) {
|
|
696
786
|
lines.push(` ✅ All checks stable. No regressions detected.`);
|
|
697
787
|
lines.push('');
|
|
698
788
|
}
|
|
699
789
|
|
|
790
|
+
if (passRates) {
|
|
791
|
+
lines.push(` Overall health: ${passRates.overallHealth}%`);
|
|
792
|
+
}
|
|
793
|
+
|
|
700
794
|
return lines.join('\n');
|
|
701
795
|
}
|
|
702
796
|
|
package/src/audit.js
CHANGED
|
@@ -953,9 +953,13 @@ function printLiteAudit(result, dir) {
|
|
|
953
953
|
|
|
954
954
|
console.log(colorize(' Top 3 things to fix right now:', 'magenta'));
|
|
955
955
|
console.log('');
|
|
956
|
+
let usagePatterns;
|
|
957
|
+
try { usagePatterns = require('./usage-patterns'); } catch { usagePatterns = null; }
|
|
956
958
|
result.liteSummary.topNextActions.forEach((item, index) => {
|
|
957
959
|
const tier = item.impact === 'critical' ? '🔴' : item.impact === 'high' ? '🟡' : '🔵';
|
|
958
|
-
|
|
960
|
+
const suppressed = usagePatterns && usagePatterns.getPriorityAdjustment(dir, item.key) === 'suppress';
|
|
961
|
+
const suffix = suppressed ? colorize(' (suppressed)', 'dim') : '';
|
|
962
|
+
console.log(` ${index + 1}. ${tier} ${colorize(item.name, 'bold')}${suffix}`);
|
|
959
963
|
console.log(colorize(` ${item.fix}`, 'dim'));
|
|
960
964
|
});
|
|
961
965
|
console.log('');
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI-generated fix prompts for checks without template auto-fixes.
|
|
3
|
+
* Each key maps to a check key from techniques.js.
|
|
4
|
+
* These prompts are designed to be copy-pasted into an AI coding agent.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const FIX_PROMPTS = {
|
|
8
|
+
importSyntax:
|
|
9
|
+
'Refactor CLAUDE.md to use @path imports for modularity. Split large sections into separate files (e.g. @docs/coding-style.md, @docs/architecture.md) and reference them with @path syntax. Also consider using .claude/rules/ for path-specific rules.',
|
|
10
|
+
|
|
11
|
+
underlines200:
|
|
12
|
+
'Refactor CLAUDE.md to be under 200 lines. Move detailed sections into separate files using @import or .claude/rules/ for path-specific rules. Keep only essential project overview, build commands, and key conventions in the main file.',
|
|
13
|
+
|
|
14
|
+
verificationLoop:
|
|
15
|
+
'Add a verification section to CLAUDE.md with commands Claude should run after making changes. Include test, lint, and build commands. Example:\n\n## Verification\nAfter every change, run:\n- `npm test` to verify tests pass\n- `npm run lint` to check code style\n- `npm run build` to verify compilation',
|
|
16
|
+
|
|
17
|
+
testCommand:
|
|
18
|
+
'Add an explicit test command to CLAUDE.md. Example: "Run `npm test` before committing." or "Run `pytest` to verify changes." Place it in a ## Commands or ## Verification section.',
|
|
19
|
+
|
|
20
|
+
lintCommand:
|
|
21
|
+
'Add a lint command to CLAUDE.md so the AI agent auto-checks code style. Example: "Run `npm run lint` or `eslint .` before committing." Place it in a ## Commands section.',
|
|
22
|
+
|
|
23
|
+
buildCommand:
|
|
24
|
+
'Add a build command to CLAUDE.md so the AI agent can verify compilation. Example: "Run `npm run build` or `tsc` to verify the project compiles." Place it in a ## Commands section.',
|
|
25
|
+
|
|
26
|
+
settingsPermissions:
|
|
27
|
+
'Create or update .claude/settings.json with permission configuration. Add "permissions": { "allow": ["Read", "Write src/**"], "deny": ["Write .env", "Write **/secrets/**"] } to control which tools and paths the AI agent can access.',
|
|
28
|
+
|
|
29
|
+
permissionDeny:
|
|
30
|
+
'Add deny rules to .claude/settings.json under permissions.deny to block dangerous operations. Example entries: "rm -rf /", "DROP TABLE", "Write .env", "Write **/*.pem", "Write **/secrets/**".',
|
|
31
|
+
|
|
32
|
+
noBypassPermissions:
|
|
33
|
+
'Remove bypassPermissions from your .claude/settings.json defaultMode. Instead, use explicit allow rules in permissions.allow to grant only the access needed.',
|
|
34
|
+
|
|
35
|
+
secretsProtection:
|
|
36
|
+
'Add permissions.deny rules in .claude/settings.json to block reading sensitive files. Add entries like: ".env", ".env.*", "**/.env", "**/*.pem", "**/secrets/**" to the deny array.',
|
|
37
|
+
|
|
38
|
+
securityReview:
|
|
39
|
+
'Add a /security-review command or mention security review in CLAUDE.md. Create .claude/commands/security-review.md with: "Review the codebase for OWASP Top 10 vulnerabilities. Check for: SQL injection, XSS, CSRF, insecure dependencies, hardcoded secrets, and misconfigured permissions."',
|
|
40
|
+
|
|
41
|
+
preToolUseHook:
|
|
42
|
+
'Add a PreToolUse hook in .claude/settings.json to validate tool calls before execution. Example: add a hook that blocks writes to protected files or validates file paths. See hooks documentation for the event schema.',
|
|
43
|
+
|
|
44
|
+
postToolUseHook:
|
|
45
|
+
'Add a PostToolUse hook in .claude/settings.json for automated actions after tool calls. Example: auto-run linting after file writes, or validate output format after code generation.',
|
|
46
|
+
|
|
47
|
+
sessionStartHook:
|
|
48
|
+
'Add a SessionStart hook in .claude/settings.json for initialization tasks. Example: load project state, rotate logs, or display a welcome message with project status at the start of each session.',
|
|
49
|
+
|
|
50
|
+
deployCommand:
|
|
51
|
+
'Create .claude/commands/deploy.md with deployment instructions. Include: pre-deploy checks (tests, lint, build), deployment steps for your platform (Vercel, AWS, etc.), and post-deploy verification.',
|
|
52
|
+
|
|
53
|
+
reviewCommand:
|
|
54
|
+
'Create .claude/commands/review.md with code review instructions. Include: check for security issues, verify test coverage, review naming conventions, check for code duplication, and validate error handling.',
|
|
55
|
+
|
|
56
|
+
compactionAwareness:
|
|
57
|
+
'Add compaction guidance to CLAUDE.md. Add a line like: "Run /compact when context gets heavy or before large operations." This helps the AI agent manage its context window effectively.',
|
|
58
|
+
|
|
59
|
+
contextManagement:
|
|
60
|
+
'Add context management tips to CLAUDE.md. Include: "Use /compact proactively at 70% capacity. Prefer targeted file reads over broad searches. Keep conversation focused on one task at a time."',
|
|
61
|
+
|
|
62
|
+
mcpServers:
|
|
63
|
+
'Create .mcp.json at the project root to configure MCP servers. Example:\n{\n "mcpServers": {\n "memory": { "command": "npx", "args": ["-y", "@anthropic/mcp-memory"] }\n }\n}\nUse `claude mcp add <name>` to add servers interactively.',
|
|
64
|
+
|
|
65
|
+
context7Mcp:
|
|
66
|
+
'Add the Context7 MCP server for real-time documentation lookup. Add to .mcp.json:\n"context7": { "command": "npx", "args": ["-y", "@anthropic/context7-mcp"] }\nThis provides always-up-to-date library documentation.',
|
|
67
|
+
|
|
68
|
+
xmlTags:
|
|
69
|
+
'Add XML-tagged sections to CLAUDE.md for structured rules. Wrap critical rules in tags like <constraints>, <validation>, or <rules>. Example:\n<constraints>\n- Never modify package-lock.json manually\n- Always run tests before committing\n</constraints>',
|
|
70
|
+
|
|
71
|
+
fewShotExamples:
|
|
72
|
+
'Add code examples to CLAUDE.md showing preferred patterns. Include 2-3 examples of your coding style: naming conventions, error handling patterns, file structure. Use fenced code blocks with the appropriate language tag.',
|
|
73
|
+
|
|
74
|
+
roleDefinition:
|
|
75
|
+
'Add a role definition to the top of CLAUDE.md. Example: "You are a senior backend engineer working on a Node.js microservices platform. Prioritize type safety, comprehensive error handling, and test coverage."',
|
|
76
|
+
|
|
77
|
+
constraintBlocks:
|
|
78
|
+
'Add XML constraint blocks to CLAUDE.md for critical rules. Wrap must-follow rules in <constraints> tags for ~40% better adherence. Example:\n<constraints>\n- Never delete database migrations\n- Always use parameterized queries\n- Run the full test suite before committing\n</constraints>',
|
|
79
|
+
|
|
80
|
+
readme:
|
|
81
|
+
'Create a README.md with: project name and description, installation/setup instructions, usage examples, configuration options, and contribution guidelines.',
|
|
82
|
+
|
|
83
|
+
changelog:
|
|
84
|
+
'Create a CHANGELOG.md following Keep a Changelog format. Include sections: Added, Changed, Deprecated, Removed, Fixed, Security. Start with your current version.',
|
|
85
|
+
|
|
86
|
+
contributing:
|
|
87
|
+
'Create a CONTRIBUTING.md with: how to set up the dev environment, coding standards and style guide, pull request process, issue reporting guidelines, and code of conduct reference.',
|
|
88
|
+
|
|
89
|
+
editorconfig:
|
|
90
|
+
'Create a .editorconfig file at the project root with consistent formatting rules:\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true',
|
|
91
|
+
|
|
92
|
+
ciPipeline:
|
|
93
|
+
'Add a CI pipeline for automated testing. For GitHub Actions, create .github/workflows/ci.yml with steps: checkout, setup Node/Python, install dependencies, run lint, run tests, run build.',
|
|
94
|
+
|
|
95
|
+
dockerfile:
|
|
96
|
+
'Create a Dockerfile for the project. Use a multi-stage build: stage 1 installs dependencies and builds, stage 2 copies only production artifacts. Use a slim base image and set a non-root user.',
|
|
97
|
+
|
|
98
|
+
noSecretsInClaude:
|
|
99
|
+
'Remove any API keys, tokens, or secrets from CLAUDE.md. Replace them with environment variable references (e.g. $API_KEY or process.env.API_KEY). Store actual values in .env files that are gitignored.',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Format a fix prompt for display in the terminal.
|
|
104
|
+
*/
|
|
105
|
+
function formatFixPrompt(key, prompt) {
|
|
106
|
+
const divider = '\u2500'.repeat(38);
|
|
107
|
+
const lines = [
|
|
108
|
+
'',
|
|
109
|
+
` No auto-fix for '${key}'. Here's a prompt for your AI agent:`,
|
|
110
|
+
'',
|
|
111
|
+
` ${divider}`,
|
|
112
|
+
];
|
|
113
|
+
for (const line of prompt.split('\n')) {
|
|
114
|
+
lines.push(` ${line}`);
|
|
115
|
+
}
|
|
116
|
+
lines.push(` ${divider}`);
|
|
117
|
+
lines.push('');
|
|
118
|
+
lines.push(' Copy and paste this into Claude Code, Cursor, or your preferred AI agent.');
|
|
119
|
+
return lines.join('\n');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { FIX_PROMPTS, formatFixPrompt };
|
package/src/init.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
const { detectPlatforms } = require('./public-api');
|
|
5
|
+
const { audit } = require('./audit');
|
|
6
|
+
const { setup } = require('./setup');
|
|
7
|
+
const { ProjectContext } = require('./context');
|
|
8
|
+
const { STACKS } = require('./techniques');
|
|
9
|
+
|
|
10
|
+
const PLATFORM_LABELS = {
|
|
11
|
+
claude: 'Claude Code',
|
|
12
|
+
codex: 'Codex',
|
|
13
|
+
gemini: 'Gemini CLI',
|
|
14
|
+
copilot: 'GitHub Copilot',
|
|
15
|
+
cursor: 'Cursor',
|
|
16
|
+
windsurf: 'Windsurf',
|
|
17
|
+
aider: 'Aider',
|
|
18
|
+
opencode: 'OpenCode',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const ALL_PLATFORMS = Object.keys(PLATFORM_LABELS);
|
|
22
|
+
|
|
23
|
+
const TEAM_SIZES = ['solo', 'small', 'team', 'enterprise'];
|
|
24
|
+
const TEAM_LABELS = {
|
|
25
|
+
solo: 'Solo developer',
|
|
26
|
+
small: 'Small team (2-5)',
|
|
27
|
+
team: 'Team (6-20)',
|
|
28
|
+
enterprise: 'Enterprise (20+)',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function prompt(rl, question) {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
rl.question(question, (answer) => resolve(answer.trim()));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function detectStacks(dir) {
|
|
38
|
+
const ctx = new ProjectContext(dir);
|
|
39
|
+
return ctx.detectStacks(STACKS);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function labelPlatforms(platforms) {
|
|
43
|
+
return platforms.map((p) => PLATFORM_LABELS[p] || p).join(', ');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parsePlatforms(input) {
|
|
47
|
+
return input
|
|
48
|
+
.split(/[,\s]+/)
|
|
49
|
+
.map((s) => s.trim().toLowerCase())
|
|
50
|
+
.filter((s) => ALL_PLATFORMS.includes(s));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function runInit(dir, flags) {
|
|
54
|
+
const rl = readline.createInterface({
|
|
55
|
+
input: process.stdin,
|
|
56
|
+
output: process.stdout,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const dim = '\x1b[2m';
|
|
60
|
+
const bold = '\x1b[1m';
|
|
61
|
+
const cyan = '\x1b[36m';
|
|
62
|
+
const green = '\x1b[32m';
|
|
63
|
+
const reset = '\x1b[0m';
|
|
64
|
+
|
|
65
|
+
console.log('');
|
|
66
|
+
console.log(`${bold} Welcome to Nerviq${reset} — let's set up your AI coding agent governance.`);
|
|
67
|
+
console.log('');
|
|
68
|
+
|
|
69
|
+
// --- Question 1: Platforms ---
|
|
70
|
+
const detected = detectPlatforms(dir);
|
|
71
|
+
const detectedLabel = detected.length > 0
|
|
72
|
+
? `${dim}[auto-detected: ${labelPlatforms(detected)}]${reset}`
|
|
73
|
+
: `${dim}[no platforms detected]${reset}`;
|
|
74
|
+
console.log(` ${bold}1.${reset} Which platform(s) do you use?`);
|
|
75
|
+
console.log(` ${detectedLabel}`);
|
|
76
|
+
const platformInput = await prompt(
|
|
77
|
+
rl,
|
|
78
|
+
` ${dim}> Press Enter to confirm, or type: ${ALL_PLATFORMS.join(',')}${reset}\n > `,
|
|
79
|
+
);
|
|
80
|
+
const platforms = platformInput === ''
|
|
81
|
+
? (detected.length > 0 ? detected : ['claude'])
|
|
82
|
+
: parsePlatforms(platformInput);
|
|
83
|
+
if (platforms.length === 0) platforms.push('claude');
|
|
84
|
+
|
|
85
|
+
console.log('');
|
|
86
|
+
|
|
87
|
+
// --- Question 2: Stack ---
|
|
88
|
+
const stacks = detectStacks(dir);
|
|
89
|
+
const stackLabels = stacks.map((s) => s.label);
|
|
90
|
+
const stackDetectedLabel = stackLabels.length > 0
|
|
91
|
+
? `${dim}[auto-detected: ${stackLabels.join(', ')}]${reset}`
|
|
92
|
+
: `${dim}[no stack detected]${reset}`;
|
|
93
|
+
console.log(` ${bold}2.${reset} What's your primary stack?`);
|
|
94
|
+
console.log(` ${stackDetectedLabel}`);
|
|
95
|
+
const stackInput = await prompt(
|
|
96
|
+
rl,
|
|
97
|
+
` ${dim}> Press Enter to confirm, or type your stack${reset}\n > `,
|
|
98
|
+
);
|
|
99
|
+
const stackDisplay = stackInput === ''
|
|
100
|
+
? (stackLabels.length > 0 ? stackLabels.join(', ') : 'General')
|
|
101
|
+
: stackInput;
|
|
102
|
+
|
|
103
|
+
console.log('');
|
|
104
|
+
|
|
105
|
+
// --- Question 3: Team size ---
|
|
106
|
+
console.log(` ${bold}3.${reset} What's your team size?`);
|
|
107
|
+
const teamInput = await prompt(
|
|
108
|
+
rl,
|
|
109
|
+
` ${dim}> solo / small (2-5) / team (6-20) / enterprise (20+)${reset}\n > `,
|
|
110
|
+
);
|
|
111
|
+
const teamKey = TEAM_SIZES.find((t) => teamInput.toLowerCase().startsWith(t)) || 'solo';
|
|
112
|
+
const teamLabel = TEAM_LABELS[teamKey];
|
|
113
|
+
|
|
114
|
+
rl.close();
|
|
115
|
+
|
|
116
|
+
console.log('');
|
|
117
|
+
console.log(` ${cyan}Setting up for: ${labelPlatforms(platforms)} | ${stackDisplay} | ${teamLabel}${reset}`);
|
|
118
|
+
console.log('');
|
|
119
|
+
|
|
120
|
+
// --- Run audit (before) ---
|
|
121
|
+
const primaryPlatform = platforms[0];
|
|
122
|
+
console.log(` ${dim}Running audit...${reset}`);
|
|
123
|
+
const preResult = await audit({ dir, silent: true, platform: primaryPlatform });
|
|
124
|
+
const preScore = preResult.score;
|
|
125
|
+
console.log(` Score: ${bold}${preScore}/100${reset}`);
|
|
126
|
+
console.log('');
|
|
127
|
+
|
|
128
|
+
// --- Run setup ---
|
|
129
|
+
console.log(` ${dim}Running setup...${reset}`);
|
|
130
|
+
const setupResult = await setup({
|
|
131
|
+
dir,
|
|
132
|
+
platform: primaryPlatform,
|
|
133
|
+
silent: true,
|
|
134
|
+
profile: 'safe-write',
|
|
135
|
+
mcpPacks: [],
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
for (const f of setupResult.writtenFiles) {
|
|
139
|
+
console.log(` ${green}✅${reset} Created ${f}`);
|
|
140
|
+
}
|
|
141
|
+
for (const f of setupResult.preservedFiles) {
|
|
142
|
+
console.log(` ${dim}⏭️ Kept ${f} (already exists)${reset}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// --- Run additional platform setups ---
|
|
146
|
+
for (const plat of platforms.slice(1)) {
|
|
147
|
+
try {
|
|
148
|
+
const extraResult = await setup({
|
|
149
|
+
dir,
|
|
150
|
+
platform: plat,
|
|
151
|
+
silent: true,
|
|
152
|
+
profile: 'safe-write',
|
|
153
|
+
mcpPacks: [],
|
|
154
|
+
});
|
|
155
|
+
for (const f of extraResult.writtenFiles) {
|
|
156
|
+
console.log(` ${green}✅${reset} Created ${f}`);
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
// Platform setup not available, skip
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// --- Run audit (after) ---
|
|
164
|
+
const postResult = await audit({ dir, silent: true, platform: primaryPlatform });
|
|
165
|
+
const postScore = postResult.score;
|
|
166
|
+
const delta = postScore - preScore;
|
|
167
|
+
|
|
168
|
+
if (delta > 0) {
|
|
169
|
+
console.log(` Score: ${bold}${postScore}/100${reset} (${green}+${delta}${reset})`);
|
|
170
|
+
} else {
|
|
171
|
+
console.log(` Score: ${bold}${postScore}/100${reset}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log('');
|
|
175
|
+
console.log(` ${bold}Next steps:${reset}`);
|
|
176
|
+
console.log(` - Review: ${cyan}nerviq audit --full${reset}`);
|
|
177
|
+
if (platforms.length > 1) {
|
|
178
|
+
console.log(` - Cross-platform: ${cyan}nerviq harmony-audit${reset}`);
|
|
179
|
+
}
|
|
180
|
+
console.log(` - Customize: ${cyan}nerviq augment${reset}`);
|
|
181
|
+
console.log('');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = { runInit };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { ensureProjectStateDir, resolveProjectStateReadPath } = require('./state-paths');
|
|
4
|
+
|
|
5
|
+
const PATTERNS_FILE = 'patterns.json';
|
|
6
|
+
const SUPPRESS_THRESHOLD = 3;
|
|
7
|
+
|
|
8
|
+
function patternsPath(dir, writable) {
|
|
9
|
+
if (writable) {
|
|
10
|
+
const feedbackDir = ensureProjectStateDir(dir, 'feedback');
|
|
11
|
+
return path.join(feedbackDir, PATTERNS_FILE);
|
|
12
|
+
}
|
|
13
|
+
const feedbackDir = resolveProjectStateReadPath(dir, 'feedback');
|
|
14
|
+
return path.join(feedbackDir, PATTERNS_FILE);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function loadPatterns(dir) {
|
|
18
|
+
const filePath = patternsPath(dir, false);
|
|
19
|
+
if (!fs.existsSync(filePath)) return {};
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
22
|
+
} catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function savePatterns(dir, patterns) {
|
|
28
|
+
const filePath = patternsPath(dir, true);
|
|
29
|
+
fs.writeFileSync(filePath, JSON.stringify(patterns, null, 2), 'utf8');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function recordPattern(dir, checkKey, action) {
|
|
33
|
+
if (!['accepted', 'rejected', 'skipped'].includes(action)) return;
|
|
34
|
+
const patterns = loadPatterns(dir);
|
|
35
|
+
if (!patterns[checkKey]) {
|
|
36
|
+
patterns[checkKey] = { accepted: 0, rejected: 0, skipped: 0, lastAction: null, lastDate: null };
|
|
37
|
+
}
|
|
38
|
+
patterns[checkKey][action] += 1;
|
|
39
|
+
patterns[checkKey].lastAction = action;
|
|
40
|
+
patterns[checkKey].lastDate = new Date().toISOString();
|
|
41
|
+
savePatterns(dir, patterns);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getPriorityAdjustment(dir, checkKey) {
|
|
45
|
+
const patterns = loadPatterns(dir);
|
|
46
|
+
const entry = patterns[checkKey];
|
|
47
|
+
if (!entry) return null;
|
|
48
|
+
const total = entry.accepted + entry.rejected;
|
|
49
|
+
if (total < 2) return null;
|
|
50
|
+
if (entry.accepted > 0 && entry.rejected === 0) return 'boost';
|
|
51
|
+
if (entry.rejected >= SUPPRESS_THRESHOLD && entry.accepted === 0) return 'suppress';
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getUsageSummary(dir) {
|
|
56
|
+
const patterns = loadPatterns(dir);
|
|
57
|
+
const keys = Object.keys(patterns);
|
|
58
|
+
const totalEvents = keys.reduce((sum, k) => {
|
|
59
|
+
const e = patterns[k];
|
|
60
|
+
return sum + e.accepted + e.rejected + e.skipped;
|
|
61
|
+
}, 0);
|
|
62
|
+
|
|
63
|
+
const withRates = keys
|
|
64
|
+
.filter(k => (patterns[k].accepted + patterns[k].rejected) > 0)
|
|
65
|
+
.map(k => {
|
|
66
|
+
const e = patterns[k];
|
|
67
|
+
const total = e.accepted + e.rejected;
|
|
68
|
+
return { key: k, accepted: e.accepted, rejected: e.rejected, total, rate: total > 0 ? e.accepted / total : 0 };
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
withRates.sort((a, b) => b.rate - a.rate || b.total - a.total);
|
|
72
|
+
const topAccepted = withRates.filter(e => e.rate > 0).slice(0, 5);
|
|
73
|
+
const topRejected = withRates.filter(e => e.rate < 1).sort((a, b) => a.rate - b.rate || b.total - a.total).slice(0, 5);
|
|
74
|
+
|
|
75
|
+
return { totalEvents, topAccepted, topRejected, patterns };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatUsageSummary(dir) {
|
|
79
|
+
const summary = getUsageSummary(dir);
|
|
80
|
+
if (summary.totalEvents === 0) return ' No usage patterns recorded yet.\n Patterns are tracked when you run nerviq fix.';
|
|
81
|
+
|
|
82
|
+
const lines = [` Usage Patterns (${summary.totalEvents} events recorded):`];
|
|
83
|
+
if (summary.topAccepted.length > 0) {
|
|
84
|
+
lines.push('', ' Most accepted:');
|
|
85
|
+
summary.topAccepted.forEach((e, i) => {
|
|
86
|
+
lines.push(` ${i + 1}. ${e.key.padEnd(20)} ${e.accepted}/${e.total} (${Math.round(e.rate * 100)}%)`);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (summary.topRejected.length > 0) {
|
|
90
|
+
lines.push('', ' Most rejected:');
|
|
91
|
+
summary.topRejected.forEach((e, i) => {
|
|
92
|
+
const hint = e.rate === 0 && e.total >= SUPPRESS_THRESHOLD ? ' -- consider suppressing' : '';
|
|
93
|
+
lines.push(` ${i + 1}. ${e.key.padEnd(20)} ${e.accepted}/${e.total} (${Math.round(e.rate * 100)}%)${hint}`);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return lines.join('\n');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = { loadPatterns, recordPattern, getPriorityAdjustment, getUsageSummary, formatUsageSummary };
|