@nerviq/cli 1.8.1 → 1.8.4
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 +1879 -1614
- package/package.json +1 -1
- package/src/activity.js +97 -3
- package/src/audit.js +5 -1
- package/src/auto-suggest.js +101 -0
- package/src/fix-prompts.js +122 -0
- package/src/profiles.js +121 -0
- package/src/usage-patterns.js +99 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nerviq/cli",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.4",
|
|
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,101 @@
|
|
|
1
|
+
const { loadPatterns } = require('./usage-patterns');
|
|
2
|
+
const { readSnapshotIndex } = require('./activity');
|
|
3
|
+
|
|
4
|
+
const MIN_EVENTS = 2;
|
|
5
|
+
const SUPPRESS_THRESHOLD = 3;
|
|
6
|
+
const RECENT_AUDITS = 10;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Analyze usage patterns and audit history to suggest rules.
|
|
10
|
+
*/
|
|
11
|
+
function analyzeSuggestions(dir) {
|
|
12
|
+
const patterns = loadPatterns(dir);
|
|
13
|
+
const snapshots = readSnapshotIndex(dir);
|
|
14
|
+
|
|
15
|
+
const keys = Object.keys(patterns);
|
|
16
|
+
const totalEvents = keys.reduce((sum, k) => {
|
|
17
|
+
const e = patterns[k];
|
|
18
|
+
return sum + e.accepted + e.rejected + e.skipped;
|
|
19
|
+
}, 0);
|
|
20
|
+
|
|
21
|
+
// Checks always accepted -> suggest as required
|
|
22
|
+
const suggestedRules = keys
|
|
23
|
+
.filter(k => {
|
|
24
|
+
const e = patterns[k];
|
|
25
|
+
return e.accepted >= MIN_EVENTS && e.rejected === 0;
|
|
26
|
+
})
|
|
27
|
+
.map(k => ({ key: k, accepted: patterns[k].accepted, total: patterns[k].accepted }));
|
|
28
|
+
|
|
29
|
+
// Checks always rejected -> suggest suppressing
|
|
30
|
+
const suggestedSuppressions = keys
|
|
31
|
+
.filter(k => {
|
|
32
|
+
const e = patterns[k];
|
|
33
|
+
return e.rejected >= SUPPRESS_THRESHOLD && e.accepted === 0;
|
|
34
|
+
})
|
|
35
|
+
.map(k => ({ key: k, rejected: patterns[k].rejected, total: patterns[k].rejected }));
|
|
36
|
+
|
|
37
|
+
// From audit snapshots: checks that repeatedly appear in topActionKeys (failing)
|
|
38
|
+
const auditSnapshots = snapshots
|
|
39
|
+
.filter(s => s.snapshotKind === 'audit' && s.summary && Array.isArray(s.summary.topActionKeys))
|
|
40
|
+
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
|
41
|
+
.slice(0, RECENT_AUDITS);
|
|
42
|
+
|
|
43
|
+
const failCounts = {};
|
|
44
|
+
for (const snap of auditSnapshots) {
|
|
45
|
+
for (const key of snap.summary.topActionKeys) {
|
|
46
|
+
failCounts[key] = (failCounts[key] || 0) + 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const suggestedPriorities = Object.entries(failCounts)
|
|
51
|
+
.filter(([, count]) => count >= 2)
|
|
52
|
+
.sort((a, b) => b[1] - a[1])
|
|
53
|
+
.map(([key, count]) => ({ key, failCount: count, auditCount: auditSnapshots.length }));
|
|
54
|
+
|
|
55
|
+
return { totalEvents, auditCount: auditSnapshots.length, suggestedRules, suggestedSuppressions, suggestedPriorities };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Format suggestions for CLI output.
|
|
60
|
+
*/
|
|
61
|
+
function formatSuggestions(suggestions) {
|
|
62
|
+
const { totalEvents, auditCount, suggestedRules, suggestedSuppressions, suggestedPriorities } = suggestions;
|
|
63
|
+
|
|
64
|
+
if (totalEvents === 0 && auditCount === 0) {
|
|
65
|
+
return ' No usage data yet. Run nerviq fix or nerviq audit to build pattern history.';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const sources = [];
|
|
69
|
+
if (totalEvents > 0) sources.push(`${totalEvents} pattern events`);
|
|
70
|
+
if (auditCount > 0) sources.push(`${auditCount} audit snapshots`);
|
|
71
|
+
const lines = [` Auto-Suggested Rules (based on ${sources.join(', ')}):`];
|
|
72
|
+
|
|
73
|
+
if (suggestedRules.length > 0) {
|
|
74
|
+
lines.push('', ' Suggested as required (always accepted):');
|
|
75
|
+
for (const r of suggestedRules) {
|
|
76
|
+
lines.push(` + ${r.key.padEnd(20)} — accepted ${r.accepted}/${r.total} times, consider making mandatory`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (suggestedSuppressions.length > 0) {
|
|
81
|
+
lines.push('', ' Suggested to suppress (always rejected):');
|
|
82
|
+
for (const s of suggestedSuppressions) {
|
|
83
|
+
lines.push(` - ${s.key.padEnd(20)} — rejected ${s.rejected}/${s.total} times, may not fit your workflow`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (suggestedPriorities.length > 0) {
|
|
88
|
+
lines.push('', ' Priority focus (failing repeatedly):');
|
|
89
|
+
for (const p of suggestedPriorities) {
|
|
90
|
+
lines.push(` ! ${p.key.padEnd(20)} — failed in ${p.failCount}/${p.auditCount} recent audits`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (suggestedRules.length === 0 && suggestedSuppressions.length === 0 && suggestedPriorities.length === 0) {
|
|
95
|
+
lines.push('', ' No strong patterns detected yet. Keep using nerviq fix and audit to build history.');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return lines.join('\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { analyzeSuggestions, formatSuggestions };
|
|
@@ -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/profiles.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Team profiles — save and share org-specific check weights and preferences.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const PROFILES_DIR = 'profiles';
|
|
9
|
+
|
|
10
|
+
function profilesDir(dir) {
|
|
11
|
+
return path.join(dir, '.nerviq', PROFILES_DIR);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function profilePath(dir, name) {
|
|
15
|
+
return path.join(profilesDir(dir), `${name}.json`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function validateProfileName(name) {
|
|
19
|
+
if (!name || typeof name !== 'string') throw new Error('Profile name is required.');
|
|
20
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
21
|
+
throw new Error(`Invalid profile name '${name}'. Use only letters, numbers, hyphens, and underscores.`);
|
|
22
|
+
}
|
|
23
|
+
if (name.length > 64) throw new Error('Profile name must be 64 characters or fewer.');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function saveProfile(dir, profileName, options = {}) {
|
|
27
|
+
validateProfileName(profileName);
|
|
28
|
+
const profileDir = profilesDir(dir);
|
|
29
|
+
fs.mkdirSync(profileDir, { recursive: true });
|
|
30
|
+
|
|
31
|
+
const profile = {
|
|
32
|
+
name: profileName,
|
|
33
|
+
created: new Date().toISOString().split('T')[0],
|
|
34
|
+
platforms: options.platforms || ['claude'],
|
|
35
|
+
threshold: options.threshold != null ? Number(options.threshold) : 70,
|
|
36
|
+
suppressedChecks: options.suppressedChecks || [],
|
|
37
|
+
priorityBoosts: options.priorityBoosts || [],
|
|
38
|
+
customWeights: options.customWeights || {},
|
|
39
|
+
description: options.description || '',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const filePath = profilePath(dir, profileName);
|
|
43
|
+
fs.writeFileSync(filePath, JSON.stringify(profile, null, 2), 'utf8');
|
|
44
|
+
return { saved: true, path: filePath, profile };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function loadProfile(dir, profileName) {
|
|
48
|
+
validateProfileName(profileName);
|
|
49
|
+
const filePath = profilePath(dir, profileName);
|
|
50
|
+
if (!fs.existsSync(filePath)) {
|
|
51
|
+
throw new Error(`Profile '${profileName}' not found. Run 'nerviq profile list' to see available profiles.`);
|
|
52
|
+
}
|
|
53
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
54
|
+
let profile;
|
|
55
|
+
try {
|
|
56
|
+
profile = JSON.parse(raw);
|
|
57
|
+
} catch {
|
|
58
|
+
throw new Error(`Profile '${profileName}' contains invalid JSON.`);
|
|
59
|
+
}
|
|
60
|
+
return profile;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function listProfiles(dir) {
|
|
64
|
+
const profDir = profilesDir(dir);
|
|
65
|
+
if (!fs.existsSync(profDir)) return [];
|
|
66
|
+
return fs.readdirSync(profDir)
|
|
67
|
+
.filter(f => f.endsWith('.json'))
|
|
68
|
+
.map(f => {
|
|
69
|
+
const name = f.replace(/\.json$/, '');
|
|
70
|
+
try {
|
|
71
|
+
const data = JSON.parse(fs.readFileSync(path.join(profDir, f), 'utf8'));
|
|
72
|
+
return { name, description: data.description || '', platforms: data.platforms || [], threshold: data.threshold };
|
|
73
|
+
} catch {
|
|
74
|
+
return { name, description: '(invalid)', platforms: [], threshold: null };
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function exportProfile(dir, profileName) {
|
|
80
|
+
const profile = loadProfile(dir, profileName);
|
|
81
|
+
return JSON.stringify(profile, null, 2);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function applyProfileToOptions(profile, options) {
|
|
85
|
+
const merged = { ...options };
|
|
86
|
+
if (profile.threshold != null && merged.threshold == null) {
|
|
87
|
+
merged.threshold = profile.threshold;
|
|
88
|
+
}
|
|
89
|
+
if (profile.platforms && profile.platforms.length > 0 && !options.platform) {
|
|
90
|
+
merged.platform = profile.platforms[0];
|
|
91
|
+
}
|
|
92
|
+
merged.suppressedChecks = profile.suppressedChecks || [];
|
|
93
|
+
merged.priorityBoosts = profile.priorityBoosts || [];
|
|
94
|
+
merged.customWeights = profile.customWeights || {};
|
|
95
|
+
return merged;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function formatProfileList(profiles) {
|
|
99
|
+
if (profiles.length === 0) return ' No profiles found. Create one with: nerviq profile save <name>';
|
|
100
|
+
const lines = profiles.map(p => {
|
|
101
|
+
const desc = p.description ? ` — ${p.description}` : '';
|
|
102
|
+
const plats = p.platforms.length > 0 ? ` [${p.platforms.join(', ')}]` : '';
|
|
103
|
+
return ` ${p.name}${plats}${desc}`;
|
|
104
|
+
});
|
|
105
|
+
return lines.join('\n');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function formatProfile(profile) {
|
|
109
|
+
const lines = [
|
|
110
|
+
` Name: ${profile.name}`,
|
|
111
|
+
` Created: ${profile.created || 'unknown'}`,
|
|
112
|
+
` Platforms: ${(profile.platforms || []).join(', ') || 'any'}`,
|
|
113
|
+
` Threshold: ${profile.threshold != null ? profile.threshold : 'default'}`,
|
|
114
|
+
` Suppressed: ${(profile.suppressedChecks || []).join(', ') || 'none'}`,
|
|
115
|
+
` Boosted: ${(profile.priorityBoosts || []).join(', ') || 'none'}`,
|
|
116
|
+
` Description: ${profile.description || '(none)'}`,
|
|
117
|
+
];
|
|
118
|
+
return lines.join('\n');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { saveProfile, loadProfile, listProfiles, exportProfile, applyProfileToOptions, formatProfileList, formatProfile, validateProfileName };
|
|
@@ -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 };
|