@nerviq/cli 1.8.1 → 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 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');
@@ -328,6 +329,7 @@ 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
333
335
  nerviq fix --auto Apply fixes without confirmation prompt
@@ -745,6 +747,17 @@ async function main() {
745
747
  });
746
748
  return; // keep process alive for http
747
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
+ }
748
761
  if (parsed.feedbackKey) {
749
762
  if (!parsed.feedbackStatus) {
750
763
  console.error('\n Error: feedback logging requires --status when --key is provided.\n');
@@ -1284,9 +1297,10 @@ async function main() {
1284
1297
  console.log(output);
1285
1298
  process.exit(0);
1286
1299
  } else if (normalizedCommand === 'fix') {
1287
- // nerviq fix [key] [--all-critical] [--dry-run] [--auto]
1300
+ // nerviq fix [key] [--all-critical] [--dry-run] [--auto] [--prompt]
1288
1301
  const fixKey = parsed.extraArgs[0] || null;
1289
1302
  const allCritical = flags.includes('--all-critical');
1303
+ const promptOnly = flags.includes('--prompt');
1290
1304
  const autoApply = options.auto || options.dryRun;
1291
1305
 
1292
1306
  // Step 1: Run silent audit to find failed checks (only actual failures, not skipped/null)
@@ -1300,6 +1314,7 @@ async function main() {
1300
1314
 
1301
1315
  // Step 2: Determine which checks to fix
1302
1316
  const { TECHNIQUES } = require('../src/techniques');
1317
+ const { FIX_PROMPTS, formatFixPrompt } = require('../src/fix-prompts');
1303
1318
  const fs = require('fs');
1304
1319
  const pathMod = require('path');
1305
1320
 
@@ -1348,6 +1363,18 @@ async function main() {
1348
1363
  }
1349
1364
  process.exit(1);
1350
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
+ }
1351
1378
  targetKeys = [fixKey];
1352
1379
  } else if (allCritical) {
1353
1380
  targetKeys = failedResults.filter(r => r.impact === 'critical').map(r => r.key);
@@ -1371,15 +1398,30 @@ async function main() {
1371
1398
  console.log('');
1372
1399
  }
1373
1400
  if (nonFixable.length > 0) {
1374
- console.log(` Manual fix needed (${nonFixable.length}):`);
1375
- for (const r of nonFixable.slice(0, 5)) {
1376
- const tier = r.impact === 'critical' ? '🔴' : r.impact === 'high' ? '🟡' : '🔵';
1377
- console.log(` ${tier} ${r.key}: ${r.fix}`);
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('');
1378
1413
  }
1379
- if (nonFixable.length > 5) {
1380
- console.log(` ... and ${nonFixable.length - 5} more (use --full to see all)`);
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('');
1381
1424
  }
1382
- console.log('');
1383
1425
  }
1384
1426
  if (fixable.length > 0) {
1385
1427
  console.log(` Quick actions:`);
@@ -1448,55 +1490,175 @@ async function main() {
1448
1490
  });
1449
1491
  rl.close();
1450
1492
  if (answer && answer.trim().toLowerCase() === 'n') {
1493
+ for (const key of targetKeys) {
1494
+ recordPattern(options.dir, key, 'rejected');
1495
+ }
1451
1496
  console.log('\n Aborted.\n');
1452
1497
  process.exit(0);
1453
1498
  }
1454
1499
  }
1455
1500
 
1456
- // Step 3: For each target, either use template, inline fix, or show manual instructions
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
1457
1530
  let fixed = 0;
1458
1531
  let manual = 0;
1532
+ let runningScore = preScore;
1459
1533
 
1460
- for (const key of targetKeys) {
1534
+ for (let i = 0; i < targetKeys.length; i++) {
1535
+ const key = targetKeys[i];
1461
1536
  const technique = TECHNIQUES[key];
1462
1537
  const failedCheck = failedResults.find(r => r.key === key);
1538
+ const progress = isBatch ? `${i + 1}/${targetKeys.length}: ` : '';
1463
1539
 
1464
1540
  if (technique && technique.template) {
1465
1541
  if (options.dryRun) {
1466
- console.log(`\n [dry-run] Would fix: ${failedCheck.name} (${key})`);
1542
+ console.log(` [dry-run] Would fix: ${progress}${failedCheck.name} (${key})`);
1543
+ fixResults.push({ key, name: failedCheck.name, status: 'dry-run', delta: 0 });
1467
1544
  fixed++;
1468
1545
  } else {
1469
- // Use setup with only this key
1470
- await setup({ ...options, only: [key], silent: true });
1471
- console.log(` ✅ Fixed: ${failedCheck.name}`);
1472
- fixed++;
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
+ }
1473
1569
  }
1474
1570
  } else if (INLINE_FIXERS[key]) {
1475
1571
  if (options.dryRun) {
1476
- 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 });
1477
1574
  fixed++;
1478
1575
  } else {
1479
- const didFix = INLINE_FIXERS[key](options.dir);
1480
- if (didFix) {
1481
- console.log(` ✅ Fixed: ${failedCheck.name}`);
1482
- fixed++;
1483
- } else {
1484
- console.log(` ⏭️ Already fixed: ${failedCheck.name}`);
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
+ }
1485
1598
  }
1486
1599
  }
1487
1600
  } else {
1488
- console.log(` 📋 ${failedCheck.name} (manual fix needed)`);
1489
- console.log(` ${failedCheck.fix}`);
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 });
1490
1611
  manual++;
1491
1612
  }
1492
1613
  }
1493
1614
 
1494
- // Step 4: Show score impact
1495
- if (fixed > 0 && !options.dryRun) {
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) {
1496
1655
  const postResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
1497
1656
  const delta = postResult.score - preScore;
1498
1657
  console.log('');
1499
1658
  console.log(` Score: ${preScore} → ${postResult.score} (${delta >= 0 ? '+' : ''}${delta})`);
1659
+ if (rollbackId) {
1660
+ console.log(` Rollback available: nerviq rollback --id ${rollbackId}`);
1661
+ }
1500
1662
  }
1501
1663
 
1502
1664
  console.log(`\n ${fixed} fixed, ${manual} need manual action.\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nerviq/cli",
3
- "version": "1.8.1",
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, 2);
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, summary } = healthReport;
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
- console.log(` ${index + 1}. ${tier} ${colorize(item.name, 'bold')}`);
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 };
@@ -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 };