@nerviq/cli 0.0.1 → 0.9.0-beta.1
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/CHANGELOG.md +181 -0
- package/LICENSE +21 -0
- package/README.md +447 -0
- package/bin/cli.js +749 -0
- package/content/case-study-template.md +91 -0
- package/content/claims-governance.md +37 -0
- package/content/claude-code/audit-repo/SKILL.md +20 -0
- package/content/claude-native-integration.md +60 -0
- package/content/devto-article.json +9 -0
- package/content/launch-posts.md +226 -0
- package/content/pilot-rollout-kit.md +30 -0
- package/content/release-checklist.md +31 -0
- package/package.json +53 -4
- package/src/activity.js +529 -0
- package/src/aider/activity.js +226 -0
- package/src/aider/config-parser.js +166 -0
- package/src/aider/context.js +158 -0
- package/src/aider/deep-review.js +316 -0
- package/src/aider/domain-packs.js +278 -0
- package/src/aider/freshness.js +168 -0
- package/src/aider/governance.js +253 -0
- package/src/aider/interactive.js +334 -0
- package/src/aider/mcp-packs.js +98 -0
- package/src/aider/patch.js +214 -0
- package/src/aider/plans.js +186 -0
- package/src/aider/premium.js +360 -0
- package/src/aider/setup.js +404 -0
- package/src/aider/techniques.js +1323 -0
- package/src/analyze.js +821 -0
- package/src/audit.js +1003 -0
- package/src/badge.js +13 -0
- package/src/benchmark.js +339 -0
- package/src/claudex-sync.json +7 -0
- package/src/codex/activity.js +324 -0
- package/src/codex/config-parser.js +183 -0
- package/src/codex/context.js +221 -0
- package/src/codex/deep-review.js +493 -0
- package/src/codex/domain-packs.js +372 -0
- package/src/codex/freshness.js +167 -0
- package/src/codex/governance.js +192 -0
- package/src/codex/interactive.js +618 -0
- package/src/codex/mcp-packs.js +660 -0
- package/src/codex/patch.js +209 -0
- package/src/codex/plans.js +251 -0
- package/src/codex/premium.js +614 -0
- package/src/codex/setup.js +603 -0
- package/src/codex/techniques.js +2649 -0
- package/src/context.js +272 -0
- package/src/copilot/activity.js +309 -0
- package/src/copilot/config-parser.js +226 -0
- package/src/copilot/context.js +197 -0
- package/src/copilot/deep-review.js +346 -0
- package/src/copilot/domain-packs.js +350 -0
- package/src/copilot/freshness.js +197 -0
- package/src/copilot/governance.js +222 -0
- package/src/copilot/interactive.js +406 -0
- package/src/copilot/mcp-packs.js +572 -0
- package/src/copilot/patch.js +238 -0
- package/src/copilot/plans.js +253 -0
- package/src/copilot/premium.js +450 -0
- package/src/copilot/setup.js +488 -0
- package/src/copilot/techniques.js +1822 -0
- package/src/cursor/activity.js +301 -0
- package/src/cursor/config-parser.js +265 -0
- package/src/cursor/context.js +236 -0
- package/src/cursor/deep-review.js +334 -0
- package/src/cursor/domain-packs.js +346 -0
- package/src/cursor/freshness.js +214 -0
- package/src/cursor/governance.js +229 -0
- package/src/cursor/interactive.js +391 -0
- package/src/cursor/mcp-packs.js +571 -0
- package/src/cursor/patch.js +243 -0
- package/src/cursor/plans.js +254 -0
- package/src/cursor/premium.js +468 -0
- package/src/cursor/setup.js +488 -0
- package/src/cursor/techniques.js +1786 -0
- package/src/deep-review.js +345 -0
- package/src/domain-packs.js +364 -0
- package/src/formatters/sarif.js +115 -0
- package/src/gemini/activity.js +402 -0
- package/src/gemini/config-parser.js +275 -0
- package/src/gemini/context.js +221 -0
- package/src/gemini/deep-review.js +559 -0
- package/src/gemini/domain-packs.js +371 -0
- package/src/gemini/freshness.js +204 -0
- package/src/gemini/governance.js +201 -0
- package/src/gemini/interactive.js +860 -0
- package/src/gemini/mcp-packs.js +658 -0
- package/src/gemini/patch.js +229 -0
- package/src/gemini/plans.js +269 -0
- package/src/gemini/premium.js +759 -0
- package/src/gemini/setup.js +692 -0
- package/src/gemini/techniques.js +2084 -0
- package/src/governance.js +523 -0
- package/src/harmony/advisor.js +383 -0
- package/src/harmony/audit.js +303 -0
- package/src/harmony/canon.js +444 -0
- package/src/harmony/cli.js +331 -0
- package/src/harmony/drift.js +401 -0
- package/src/harmony/governance.js +313 -0
- package/src/harmony/memory.js +238 -0
- package/src/harmony/sync.js +458 -0
- package/src/harmony/watch.js +336 -0
- package/src/index.js +256 -0
- package/src/insights.js +119 -0
- package/src/interactive.js +118 -0
- package/src/mcp-packs.js +597 -0
- package/src/opencode/activity.js +286 -0
- package/src/opencode/config-parser.js +109 -0
- package/src/opencode/context.js +247 -0
- package/src/opencode/deep-review.js +313 -0
- package/src/opencode/domain-packs.js +240 -0
- package/src/opencode/freshness.js +158 -0
- package/src/opencode/governance.js +159 -0
- package/src/opencode/interactive.js +392 -0
- package/src/opencode/mcp-packs.js +474 -0
- package/src/opencode/patch.js +184 -0
- package/src/opencode/plans.js +231 -0
- package/src/opencode/premium.js +413 -0
- package/src/opencode/setup.js +449 -0
- package/src/opencode/techniques.js +1713 -0
- package/src/plans.js +655 -0
- package/src/secret-patterns.js +30 -0
- package/src/setup.js +1274 -0
- package/src/synergy/adaptive.js +261 -0
- package/src/synergy/compensation.js +156 -0
- package/src/synergy/evidence.js +193 -0
- package/src/synergy/learning.js +184 -0
- package/src/synergy/patterns.js +227 -0
- package/src/synergy/ranking.js +83 -0
- package/src/synergy/report.js +163 -0
- package/src/synergy/routing.js +152 -0
- package/src/techniques.js +1354 -0
- package/src/watch.js +229 -0
- package/src/windsurf/activity.js +302 -0
- package/src/windsurf/config-parser.js +267 -0
- package/src/windsurf/context.js +249 -0
- package/src/windsurf/deep-review.js +337 -0
- package/src/windsurf/domain-packs.js +348 -0
- package/src/windsurf/freshness.js +215 -0
- package/src/windsurf/governance.js +231 -0
- package/src/windsurf/interactive.js +388 -0
- package/src/windsurf/mcp-packs.js +535 -0
- package/src/windsurf/patch.js +231 -0
- package/src/windsurf/plans.js +247 -0
- package/src/windsurf/premium.js +467 -0
- package/src/windsurf/setup.js +471 -0
- package/src/windsurf/techniques.js +1758 -0
|
@@ -0,0 +1,1786 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor techniques module — CHECK CATALOG
|
|
3
|
+
*
|
|
4
|
+
* 82 checks across 16 categories:
|
|
5
|
+
* v0.1 (40): A. Rules(9), B. Config(7), C. Trust & Safety(9), D. Agent Mode(5), E. MCP(5), F. Instructions Quality(5)
|
|
6
|
+
* v0.5 (55): G. Background Agents(5), H. Automations(5), I. Enterprise(5)
|
|
7
|
+
* v1.0 (70): J. BugBot & Code Review(4), K. Cross-Surface(4), L. Quality Deep(7)
|
|
8
|
+
* CP-08 (82): M. Advisory(4), N. Pack(4), O. Repeat(3), P. Freshness(3)
|
|
9
|
+
*
|
|
10
|
+
* Each check: { id, name, check(ctx), impact, rating, category, fix, template, file(), line() }
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { CursorProjectContext } = require('./context');
|
|
16
|
+
const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
|
|
17
|
+
const { validateMdcFrontmatter, validateMcpEnvVars } = require('./config-parser');
|
|
18
|
+
|
|
19
|
+
// ─── Shared helpers ─────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const FILLER_PATTERNS = [
|
|
22
|
+
/\bbe helpful\b/i,
|
|
23
|
+
/\bbe accurate\b/i,
|
|
24
|
+
/\bbe concise\b/i,
|
|
25
|
+
/\balways do your best\b/i,
|
|
26
|
+
/\bmaintain high quality\b/i,
|
|
27
|
+
/\bwrite clean code\b/i,
|
|
28
|
+
/\bfollow best practices\b/i,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function countSections(markdown) {
|
|
32
|
+
return (markdown.match(/^##\s+/gm) || []).length;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function firstLineMatching(text, matcher) {
|
|
36
|
+
if (!text) return null;
|
|
37
|
+
const lines = text.split(/\r?\n/);
|
|
38
|
+
for (let index = 0; index < lines.length; index++) {
|
|
39
|
+
const line = lines[index];
|
|
40
|
+
if (typeof matcher === 'string' && line.includes(matcher)) return index + 1;
|
|
41
|
+
if (matcher instanceof RegExp && matcher.test(line)) {
|
|
42
|
+
matcher.lastIndex = 0;
|
|
43
|
+
return index + 1;
|
|
44
|
+
}
|
|
45
|
+
if (typeof matcher === 'function' && matcher(line, index + 1)) return index + 1;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function findFillerLine(content) {
|
|
51
|
+
return firstLineMatching(content, (line) => FILLER_PATTERNS.some((p) => p.test(line)));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function findSecretLine(content) {
|
|
55
|
+
const lines = content.split(/\r?\n/);
|
|
56
|
+
for (let index = 0; index < lines.length; index++) {
|
|
57
|
+
const matched = EMBEDDED_SECRET_PATTERNS.some((pattern) => {
|
|
58
|
+
pattern.lastIndex = 0;
|
|
59
|
+
return pattern.test(lines[index]);
|
|
60
|
+
});
|
|
61
|
+
if (matched) return index + 1;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function allRulesContent(ctx) {
|
|
67
|
+
const rules = ctx.cursorRules ? ctx.cursorRules() : [];
|
|
68
|
+
return rules.map(r => r.body || '').join('\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function coreRulesContent(ctx) {
|
|
72
|
+
const always = ctx.alwaysApplyRules ? ctx.alwaysApplyRules() : [];
|
|
73
|
+
return always.map(r => r.body || '').join('\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function mcpJsonRaw(ctx) {
|
|
77
|
+
return ctx.fileContent('.cursor/mcp.json') || '';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function mcpJsonData(ctx) {
|
|
81
|
+
const result = ctx.mcpConfig();
|
|
82
|
+
return result && result.ok ? result.data : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function envJsonData(ctx) {
|
|
86
|
+
const result = ctx.environmentJson();
|
|
87
|
+
return result && result.ok ? result.data : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function docsBundle(ctx) {
|
|
91
|
+
const rules = allRulesContent(ctx) || '';
|
|
92
|
+
const readme = ctx.fileContent('README.md') || '';
|
|
93
|
+
const legacy = ctx.legacyCursorrules ? (ctx.legacyCursorrules() || '') : '';
|
|
94
|
+
return `${rules}\n${readme}\n${legacy}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function expectedVerificationCategories(ctx) {
|
|
98
|
+
const categories = new Set();
|
|
99
|
+
const pkg = ctx.jsonFile ? ctx.jsonFile('package.json') : null;
|
|
100
|
+
const scripts = pkg && pkg.scripts ? pkg.scripts : {};
|
|
101
|
+
if (scripts.test) categories.add('test');
|
|
102
|
+
if (scripts.lint) categories.add('lint');
|
|
103
|
+
if (scripts.build) categories.add('build');
|
|
104
|
+
if (ctx.fileContent('Cargo.toml')) { categories.add('test'); categories.add('build'); }
|
|
105
|
+
if (ctx.fileContent('go.mod')) { categories.add('test'); categories.add('build'); }
|
|
106
|
+
if (ctx.fileContent('pyproject.toml') || ctx.fileContent('requirements.txt')) categories.add('test');
|
|
107
|
+
if (ctx.fileContent('Makefile') || ctx.fileContent('justfile')) categories.add('build');
|
|
108
|
+
return [...categories];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function hasCommandMention(content, category) {
|
|
112
|
+
if (category === 'test') {
|
|
113
|
+
return /\bnpm test\b|\bnpm run test\b|\bpnpm test\b|\byarn test\b|\bvitest\b|\bjest\b|\bpytest\b|\bgo test\b|\bcargo test\b|\bmake test\b/i.test(content);
|
|
114
|
+
}
|
|
115
|
+
if (category === 'lint') {
|
|
116
|
+
return /\bnpm run lint\b|\bpnpm lint\b|\byarn lint\b|\beslint\b|\bprettier\b|\bruff\b|\bclippy\b|\bgolangci-lint\b|\bmake lint\b/i.test(content);
|
|
117
|
+
}
|
|
118
|
+
if (category === 'build') {
|
|
119
|
+
return /\bnpm run build\b|\bpnpm build\b|\byarn build\b|\btsc\b|\bvite build\b|\bnext build\b|\bcargo build\b|\bgo build\b|\bmake\b/i.test(content);
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function hasArchitecture(content) {
|
|
125
|
+
return /```mermaid|flowchart\b|graph\s+(TD|LR|RL|BT)\b|##\s+Architecture\b|##\s+Project Map\b|##\s+Structure\b/i.test(content);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function repoLooksRegulated(ctx) {
|
|
129
|
+
const filenames = (ctx.files || []).join('\n');
|
|
130
|
+
const pkg = ctx.fileContent('package.json') || '';
|
|
131
|
+
const readme = ctx.fileContent('README.md') || '';
|
|
132
|
+
const combined = `${filenames}\n${pkg}\n${readme}`;
|
|
133
|
+
const strong = /\bhipaa\b|\bphi\b|\bpci\b|\bsoc2\b|\biso[- ]?27001\b|\bcompliance\b|\bhealth(?:care)?\b|\bmedical\b|\bbank(?:ing)?\b|\bpayments?\b|\bfintech\b/i;
|
|
134
|
+
if (strong.test(combined)) return true;
|
|
135
|
+
const weakMatches = combined.match(/\bgdpr\b|\bpii\b/gi) || [];
|
|
136
|
+
return weakMatches.length >= 2;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function automationContents(ctx) {
|
|
140
|
+
const configs = ctx.automationsConfig ? ctx.automationsConfig() : [];
|
|
141
|
+
return configs.map(c => c.content || '').join('\n');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function wordCount(text) {
|
|
145
|
+
if (!text) return 0;
|
|
146
|
+
return text.split(/\s+/).filter(Boolean).length;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ─── CURSOR_TECHNIQUES ──────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
const CURSOR_TECHNIQUES = {
|
|
152
|
+
|
|
153
|
+
// =============================================
|
|
154
|
+
// A. Rules (9 checks) — CU-A01..CU-A09
|
|
155
|
+
// =============================================
|
|
156
|
+
|
|
157
|
+
cursorRulesExist: {
|
|
158
|
+
id: 'CU-A01',
|
|
159
|
+
name: '.cursor/rules/ directory exists with .mdc files',
|
|
160
|
+
check: (ctx) => {
|
|
161
|
+
const rules = ctx.cursorRules ? ctx.cursorRules() : [];
|
|
162
|
+
return rules.length > 0;
|
|
163
|
+
},
|
|
164
|
+
impact: 'critical',
|
|
165
|
+
rating: 5,
|
|
166
|
+
category: 'rules',
|
|
167
|
+
fix: 'Create .cursor/rules/ directory with at least one .mdc rule file.',
|
|
168
|
+
template: 'cursor-rules',
|
|
169
|
+
file: () => '.cursor/rules/',
|
|
170
|
+
line: () => null,
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
cursorNoLegacyCursorrules: {
|
|
174
|
+
id: 'CU-A02',
|
|
175
|
+
name: 'No .cursorrules without migration warning',
|
|
176
|
+
check: (ctx) => {
|
|
177
|
+
const hasLegacy = ctx.hasLegacyRules ? ctx.hasLegacyRules() : Boolean(ctx.fileContent('.cursorrules'));
|
|
178
|
+
return !hasLegacy;
|
|
179
|
+
},
|
|
180
|
+
impact: 'critical',
|
|
181
|
+
rating: 5,
|
|
182
|
+
category: 'rules',
|
|
183
|
+
fix: 'Migrate .cursorrules to .cursor/rules/*.mdc with proper frontmatter. AGENT MODE IGNORES .cursorrules completely!',
|
|
184
|
+
template: 'cursor-legacy-migration',
|
|
185
|
+
file: () => '.cursorrules',
|
|
186
|
+
line: () => 1,
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
cursorAlwaysApplyExists: {
|
|
190
|
+
id: 'CU-A03',
|
|
191
|
+
name: 'At least one rule has alwaysApply: true for agent mode',
|
|
192
|
+
check: (ctx) => {
|
|
193
|
+
const rules = ctx.cursorRules ? ctx.cursorRules() : [];
|
|
194
|
+
if (rules.length === 0) return null;
|
|
195
|
+
return rules.some(r => r.ruleType === 'always');
|
|
196
|
+
},
|
|
197
|
+
impact: 'high',
|
|
198
|
+
rating: 5,
|
|
199
|
+
category: 'rules',
|
|
200
|
+
fix: 'Add alwaysApply: true to your core rule file so agents always see instructions.',
|
|
201
|
+
template: 'cursor-rules',
|
|
202
|
+
file: () => '.cursor/rules/',
|
|
203
|
+
line: () => null,
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
cursorRulesValidFrontmatter: {
|
|
207
|
+
id: 'CU-A04',
|
|
208
|
+
name: 'Rules have valid MDC frontmatter (YAML parseable)',
|
|
209
|
+
check: (ctx) => {
|
|
210
|
+
const rules = ctx.cursorRules ? ctx.cursorRules() : [];
|
|
211
|
+
if (rules.length === 0) return null;
|
|
212
|
+
return rules.every(r => {
|
|
213
|
+
if (!r.frontmatter) return false;
|
|
214
|
+
const validation = validateMdcFrontmatter(r.frontmatter);
|
|
215
|
+
return validation.valid;
|
|
216
|
+
});
|
|
217
|
+
},
|
|
218
|
+
impact: 'high',
|
|
219
|
+
rating: 4,
|
|
220
|
+
category: 'rules',
|
|
221
|
+
fix: 'Fix YAML frontmatter in .mdc files. Use only: description, globs, alwaysApply fields.',
|
|
222
|
+
template: null,
|
|
223
|
+
file: () => '.cursor/rules/',
|
|
224
|
+
line: () => 1,
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
cursorNoGlobsDescriptionCombo: {
|
|
228
|
+
id: 'CU-A05',
|
|
229
|
+
name: 'No rules combine globs + description (creates ambiguity)',
|
|
230
|
+
check: (ctx) => {
|
|
231
|
+
const rules = ctx.cursorRules ? ctx.cursorRules() : [];
|
|
232
|
+
if (rules.length === 0) return null;
|
|
233
|
+
return !rules.some(r => {
|
|
234
|
+
if (!r.frontmatter || r.frontmatter.alwaysApply === true) return false;
|
|
235
|
+
const hasGlobs = Array.isArray(r.frontmatter.globs)
|
|
236
|
+
? r.frontmatter.globs.length > 0
|
|
237
|
+
: Boolean(r.frontmatter.globs);
|
|
238
|
+
const hasDesc = Boolean(r.frontmatter.description && String(r.frontmatter.description).trim());
|
|
239
|
+
return hasGlobs && hasDesc;
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
impact: 'medium',
|
|
243
|
+
rating: 3,
|
|
244
|
+
category: 'rules',
|
|
245
|
+
fix: 'Do not combine globs + description in the same rule. Use one or the other for clear rule activation.',
|
|
246
|
+
template: null,
|
|
247
|
+
file: () => '.cursor/rules/',
|
|
248
|
+
line: () => null,
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
cursorRulesUnder500Words: {
|
|
252
|
+
id: 'CU-A06',
|
|
253
|
+
name: 'Rules are under ~500 words each (longer = less reliably followed)',
|
|
254
|
+
check: (ctx) => {
|
|
255
|
+
const rules = ctx.cursorRules ? ctx.cursorRules() : [];
|
|
256
|
+
if (rules.length === 0) return null;
|
|
257
|
+
return rules.every(r => wordCount(r.body) <= 500);
|
|
258
|
+
},
|
|
259
|
+
impact: 'medium',
|
|
260
|
+
rating: 3,
|
|
261
|
+
category: 'rules',
|
|
262
|
+
fix: 'Split long rules into focused, shorter files. Rules over ~500 words are less reliably followed.',
|
|
263
|
+
template: null,
|
|
264
|
+
file: () => '.cursor/rules/',
|
|
265
|
+
line: () => null,
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
cursorRulesNoFiller: {
|
|
269
|
+
id: 'CU-A07',
|
|
270
|
+
name: 'No generic filler instructions in rules',
|
|
271
|
+
check: (ctx) => {
|
|
272
|
+
const content = allRulesContent(ctx);
|
|
273
|
+
if (!content.trim()) return null;
|
|
274
|
+
return !FILLER_PATTERNS.some(p => p.test(content));
|
|
275
|
+
},
|
|
276
|
+
impact: 'low',
|
|
277
|
+
rating: 3,
|
|
278
|
+
category: 'rules',
|
|
279
|
+
fix: 'Replace generic filler like "be helpful" with concrete, repo-specific guidance.',
|
|
280
|
+
template: null,
|
|
281
|
+
file: () => '.cursor/rules/',
|
|
282
|
+
line: () => {
|
|
283
|
+
const content = allRulesContent({ cursorRules: () => [] });
|
|
284
|
+
return content ? findFillerLine(content) : null;
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
cursorRulesNoSecrets: {
|
|
289
|
+
id: 'CU-A08',
|
|
290
|
+
name: 'No secrets/API keys in rule files',
|
|
291
|
+
check: (ctx) => {
|
|
292
|
+
const rulesContent = allRulesContent(ctx);
|
|
293
|
+
const commandContent = (ctx.commandFiles ? ctx.commandFiles() : [])
|
|
294
|
+
.map(f => ctx.fileContent(f) || '').join('\n');
|
|
295
|
+
const combined = `${rulesContent}\n${commandContent}`;
|
|
296
|
+
if (!combined.trim()) return null;
|
|
297
|
+
return !containsEmbeddedSecret(combined);
|
|
298
|
+
},
|
|
299
|
+
impact: 'critical',
|
|
300
|
+
rating: 5,
|
|
301
|
+
category: 'rules',
|
|
302
|
+
fix: 'Remove API keys and secrets from rule and command files. Use environment variables instead.',
|
|
303
|
+
template: null,
|
|
304
|
+
file: () => '.cursor/rules/',
|
|
305
|
+
line: () => null,
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
cursorAgentRequestedDescriptions: {
|
|
309
|
+
id: 'CU-A09',
|
|
310
|
+
name: 'Agent Requested rules have precise descriptions',
|
|
311
|
+
check: (ctx) => {
|
|
312
|
+
const agentRules = ctx.agentRequestedRules ? ctx.agentRequestedRules() : [];
|
|
313
|
+
if (agentRules.length === 0) return null;
|
|
314
|
+
return agentRules.every(r => {
|
|
315
|
+
const desc = r.frontmatter && r.frontmatter.description;
|
|
316
|
+
return desc && String(desc).trim().length >= 15;
|
|
317
|
+
});
|
|
318
|
+
},
|
|
319
|
+
impact: 'medium',
|
|
320
|
+
rating: 3,
|
|
321
|
+
category: 'rules',
|
|
322
|
+
fix: 'Add clear, specific descriptions (15+ chars) to Agent Requested rules so AI can judge relevance.',
|
|
323
|
+
template: null,
|
|
324
|
+
file: () => '.cursor/rules/',
|
|
325
|
+
line: () => null,
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
// =============================================
|
|
329
|
+
// B. Config (7 checks) — CU-B01..CU-B07
|
|
330
|
+
// =============================================
|
|
331
|
+
|
|
332
|
+
cursorMcpJsonExists: {
|
|
333
|
+
id: 'CU-B01',
|
|
334
|
+
name: '.cursor/mcp.json exists if MCP is used',
|
|
335
|
+
check: (ctx) => {
|
|
336
|
+
const result = ctx.mcpConfig();
|
|
337
|
+
if (result.ok) return true;
|
|
338
|
+
// N/A if no MCP signals at all
|
|
339
|
+
const globalResult = ctx.globalMcpConfig ? ctx.globalMcpConfig() : { ok: false };
|
|
340
|
+
if (!globalResult.ok) return null;
|
|
341
|
+
return false;
|
|
342
|
+
},
|
|
343
|
+
impact: 'high',
|
|
344
|
+
rating: 4,
|
|
345
|
+
category: 'config',
|
|
346
|
+
fix: 'Create .cursor/mcp.json with project-level MCP server configuration.',
|
|
347
|
+
template: 'cursor-mcp',
|
|
348
|
+
file: () => '.cursor/mcp.json',
|
|
349
|
+
line: () => null,
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
cursorMcpToolLimit: {
|
|
353
|
+
id: 'CU-B02',
|
|
354
|
+
name: 'MCP total tools < 40 (silent drop limit)',
|
|
355
|
+
check: (ctx) => {
|
|
356
|
+
const servers = ctx.mcpServers ? ctx.mcpServers() : {};
|
|
357
|
+
const count = Object.keys(servers).length;
|
|
358
|
+
if (count === 0) return null;
|
|
359
|
+
const estimated = count * 5; // ~5 tools per server estimate
|
|
360
|
+
return estimated < 40;
|
|
361
|
+
},
|
|
362
|
+
impact: 'high',
|
|
363
|
+
rating: 4,
|
|
364
|
+
category: 'config',
|
|
365
|
+
fix: 'Reduce MCP servers to stay under ~40 total tools. Cursor silently drops tools beyond this limit.',
|
|
366
|
+
template: null,
|
|
367
|
+
file: () => '.cursor/mcp.json',
|
|
368
|
+
line: () => null,
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
cursorCommandsExist: {
|
|
372
|
+
id: 'CU-B03',
|
|
373
|
+
name: 'Custom commands exist in .cursor/commands/',
|
|
374
|
+
check: (ctx) => {
|
|
375
|
+
const files = ctx.commandFiles ? ctx.commandFiles() : [];
|
|
376
|
+
return files.length > 0;
|
|
377
|
+
},
|
|
378
|
+
impact: 'medium',
|
|
379
|
+
rating: 3,
|
|
380
|
+
category: 'config',
|
|
381
|
+
fix: 'Create .cursor/commands/*.md files for reusable slash command prompts.',
|
|
382
|
+
template: 'cursor-commands',
|
|
383
|
+
file: () => '.cursor/commands/',
|
|
384
|
+
line: () => null,
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
cursorEnvironmentJsonExists: {
|
|
388
|
+
id: 'CU-B04',
|
|
389
|
+
name: '.cursor/environment.json exists if background agents used',
|
|
390
|
+
check: (ctx) => {
|
|
391
|
+
const env = envJsonData(ctx);
|
|
392
|
+
if (env) return true;
|
|
393
|
+
// N/A if no background agent signals
|
|
394
|
+
const rules = allRulesContent(ctx);
|
|
395
|
+
const hasBackgroundSignals = /background agent|background.*agent/i.test(rules);
|
|
396
|
+
if (!hasBackgroundSignals) return null;
|
|
397
|
+
return false;
|
|
398
|
+
},
|
|
399
|
+
impact: 'high',
|
|
400
|
+
rating: 4,
|
|
401
|
+
category: 'config',
|
|
402
|
+
fix: 'Create .cursor/environment.json to configure background agent VM (baseImage, env, persistedDirectories).',
|
|
403
|
+
template: 'cursor-environment',
|
|
404
|
+
file: () => '.cursor/environment.json',
|
|
405
|
+
line: () => null,
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
cursorNoDeprecatedVscodeKeys: {
|
|
409
|
+
id: 'CU-B05',
|
|
410
|
+
name: 'No deprecated .vscode/settings.json Cursor keys',
|
|
411
|
+
check: (ctx) => {
|
|
412
|
+
const raw = ctx.fileContent('.vscode/settings.json') || '';
|
|
413
|
+
if (!raw) return null;
|
|
414
|
+
// Check for deprecated Cursor-specific keys
|
|
415
|
+
const deprecatedPatterns = [
|
|
416
|
+
/cursor\.general\.enableStickyScroll/,
|
|
417
|
+
/cursor\.aicompletion/i,
|
|
418
|
+
];
|
|
419
|
+
return !deprecatedPatterns.some(p => p.test(raw));
|
|
420
|
+
},
|
|
421
|
+
impact: 'medium',
|
|
422
|
+
rating: 3,
|
|
423
|
+
category: 'config',
|
|
424
|
+
fix: 'Remove deprecated Cursor keys from .vscode/settings.json.',
|
|
425
|
+
template: null,
|
|
426
|
+
file: () => '.vscode/settings.json',
|
|
427
|
+
line: () => null,
|
|
428
|
+
},
|
|
429
|
+
|
|
430
|
+
cursorMcpValidJson: {
|
|
431
|
+
id: 'CU-B06',
|
|
432
|
+
name: 'MCP config is valid JSON',
|
|
433
|
+
check: (ctx) => {
|
|
434
|
+
const raw = mcpJsonRaw(ctx);
|
|
435
|
+
if (!raw) return null;
|
|
436
|
+
const result = ctx.mcpConfig();
|
|
437
|
+
return result && result.ok;
|
|
438
|
+
},
|
|
439
|
+
impact: 'critical',
|
|
440
|
+
rating: 5,
|
|
441
|
+
category: 'config',
|
|
442
|
+
fix: 'Fix malformed JSON in .cursor/mcp.json.',
|
|
443
|
+
template: null,
|
|
444
|
+
file: () => '.cursor/mcp.json',
|
|
445
|
+
line: (ctx) => {
|
|
446
|
+
const result = ctx.mcpConfig();
|
|
447
|
+
if (result && result.ok) return null;
|
|
448
|
+
if (result && result.error) {
|
|
449
|
+
const match = result.error.match(/position (\d+)/i);
|
|
450
|
+
if (match) {
|
|
451
|
+
const raw = mcpJsonRaw(ctx);
|
|
452
|
+
return raw ? raw.slice(0, Number(match[1])).split('\n').length : 1;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return 1;
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
cursorCommandsClear: {
|
|
460
|
+
id: 'CU-B07',
|
|
461
|
+
name: 'Custom command .md files have clear prompts',
|
|
462
|
+
check: (ctx) => {
|
|
463
|
+
const files = ctx.commandFiles ? ctx.commandFiles() : [];
|
|
464
|
+
if (files.length === 0) return null;
|
|
465
|
+
return files.every(f => {
|
|
466
|
+
const content = ctx.fileContent(f);
|
|
467
|
+
return content && content.trim().length >= 20;
|
|
468
|
+
});
|
|
469
|
+
},
|
|
470
|
+
impact: 'low',
|
|
471
|
+
rating: 2,
|
|
472
|
+
category: 'config',
|
|
473
|
+
fix: 'Ensure custom command files have clear, actionable prompt content (20+ chars).',
|
|
474
|
+
template: null,
|
|
475
|
+
file: () => '.cursor/commands/',
|
|
476
|
+
line: () => null,
|
|
477
|
+
},
|
|
478
|
+
|
|
479
|
+
// =============================================
|
|
480
|
+
// C. Trust & Safety (9 checks) — CU-C01..CU-C09
|
|
481
|
+
// =============================================
|
|
482
|
+
|
|
483
|
+
cursorPrivacyMode: {
|
|
484
|
+
id: 'CU-C01',
|
|
485
|
+
name: 'Privacy Mode enabled (or explicitly documented as off)',
|
|
486
|
+
check: (ctx) => {
|
|
487
|
+
// Cannot detect Privacy Mode from files (stored in SQLite state.vscdb)
|
|
488
|
+
// Check if rules/docs document the privacy mode status
|
|
489
|
+
const docs = docsBundle(ctx);
|
|
490
|
+
return /privacy mode|zero.?retention|data retention|privacy.*enabled/i.test(docs);
|
|
491
|
+
},
|
|
492
|
+
impact: 'high',
|
|
493
|
+
rating: 5,
|
|
494
|
+
category: 'trust',
|
|
495
|
+
fix: 'Enable Privacy Mode in Cursor Settings for zero data retention, or document why it is disabled.',
|
|
496
|
+
template: null,
|
|
497
|
+
file: () => '.cursor/rules/',
|
|
498
|
+
line: () => null,
|
|
499
|
+
},
|
|
500
|
+
|
|
501
|
+
cursorNoAutoRunUntrusted: {
|
|
502
|
+
id: 'CU-C02',
|
|
503
|
+
name: 'No auto-run terminal without review for untrusted repos',
|
|
504
|
+
check: (ctx) => {
|
|
505
|
+
// Check if rules mention auto-run/YOLO mode awareness
|
|
506
|
+
const docs = docsBundle(ctx);
|
|
507
|
+
if (!docs.trim()) return null;
|
|
508
|
+
// Pass if rules mention caution about auto-run
|
|
509
|
+
const mentionsAutoRun = /auto.?run|yolo|terminal.*approv|command.*confirm/i.test(docs);
|
|
510
|
+
// If no mention at all, it's not necessarily a fail — only fail if auto-run is explicitly enabled
|
|
511
|
+
return mentionsAutoRun || !(/auto.?run.*enable|yolo.*mode/i.test(docs));
|
|
512
|
+
},
|
|
513
|
+
impact: 'high',
|
|
514
|
+
rating: 4,
|
|
515
|
+
category: 'trust',
|
|
516
|
+
fix: 'Document terminal auto-run policy. Consider disabling for untrusted repos.',
|
|
517
|
+
template: null,
|
|
518
|
+
file: () => '.cursor/rules/',
|
|
519
|
+
line: () => null,
|
|
520
|
+
},
|
|
521
|
+
|
|
522
|
+
cursorAutomationTriggersScoped: {
|
|
523
|
+
id: 'CU-C03',
|
|
524
|
+
name: 'Automation triggers are intentional and scoped',
|
|
525
|
+
check: (ctx) => {
|
|
526
|
+
const configs = ctx.automationsConfig ? ctx.automationsConfig() : [];
|
|
527
|
+
if (configs.length === 0) return null;
|
|
528
|
+
// Check for overly broad triggers
|
|
529
|
+
const combined = configs.map(c => c.content).join('\n');
|
|
530
|
+
const hasBroadTrigger = /trigger:.*any|on.*any.*push|trigger:.*\*/i.test(combined);
|
|
531
|
+
return !hasBroadTrigger;
|
|
532
|
+
},
|
|
533
|
+
impact: 'high',
|
|
534
|
+
rating: 5,
|
|
535
|
+
category: 'trust',
|
|
536
|
+
fix: 'Scope automation triggers to specific branches and events. Avoid wildcard triggers.',
|
|
537
|
+
template: null,
|
|
538
|
+
file: () => '.cursor/automations/',
|
|
539
|
+
line: () => null,
|
|
540
|
+
},
|
|
541
|
+
|
|
542
|
+
cursorNoSecretsInEnvJson: {
|
|
543
|
+
id: 'CU-C04',
|
|
544
|
+
name: 'No secrets in environment.json or command files',
|
|
545
|
+
check: (ctx) => {
|
|
546
|
+
const envContent = ctx.fileContent('.cursor/environment.json') || '';
|
|
547
|
+
const cmdContent = (ctx.commandFiles ? ctx.commandFiles() : [])
|
|
548
|
+
.map(f => ctx.fileContent(f) || '').join('\n');
|
|
549
|
+
const combined = `${envContent}\n${cmdContent}`;
|
|
550
|
+
if (!combined.trim()) return null;
|
|
551
|
+
return !containsEmbeddedSecret(combined);
|
|
552
|
+
},
|
|
553
|
+
impact: 'critical',
|
|
554
|
+
rating: 5,
|
|
555
|
+
category: 'trust',
|
|
556
|
+
fix: 'Remove secrets from environment.json and command files. Use KMS/vault for background agent secrets.',
|
|
557
|
+
template: null,
|
|
558
|
+
file: () => '.cursor/environment.json',
|
|
559
|
+
line: () => null,
|
|
560
|
+
},
|
|
561
|
+
|
|
562
|
+
cursorMcpTrustedSources: {
|
|
563
|
+
id: 'CU-C05',
|
|
564
|
+
name: 'MCP servers from trusted sources (no known CVEs)',
|
|
565
|
+
check: (ctx) => {
|
|
566
|
+
const raw = mcpJsonRaw(ctx);
|
|
567
|
+
if (!raw) return null;
|
|
568
|
+
// Check for known vulnerable patterns
|
|
569
|
+
const knownVulnerable = /mcp-poisoned|cve-2025/i.test(raw);
|
|
570
|
+
// Check for non-npm/trusted sources
|
|
571
|
+
const hasUntrusted = /curl.*\|.*sh|wget.*\|.*sh/i.test(raw);
|
|
572
|
+
return !knownVulnerable && !hasUntrusted;
|
|
573
|
+
},
|
|
574
|
+
impact: 'high',
|
|
575
|
+
rating: 5,
|
|
576
|
+
category: 'trust',
|
|
577
|
+
fix: 'Verify MCP servers are from trusted sources. Check for CVE-2025-54136 (MCPoison) and CVE-2025-54135 (CurXecute).',
|
|
578
|
+
template: null,
|
|
579
|
+
file: () => '.cursor/mcp.json',
|
|
580
|
+
line: () => null,
|
|
581
|
+
},
|
|
582
|
+
|
|
583
|
+
cursorBackgroundAgentBranch: {
|
|
584
|
+
id: 'CU-C06',
|
|
585
|
+
name: 'Background agent creates branch (not commits to main)',
|
|
586
|
+
check: (ctx) => {
|
|
587
|
+
const env = envJsonData(ctx);
|
|
588
|
+
if (!env) return null;
|
|
589
|
+
// Background agents always create PRs by default — check for override
|
|
590
|
+
const rules = allRulesContent(ctx);
|
|
591
|
+
const hasPushToMain = /push.*main|commit.*main.*direct|direct.*push/i.test(rules);
|
|
592
|
+
return !hasPushToMain;
|
|
593
|
+
},
|
|
594
|
+
impact: 'high',
|
|
595
|
+
rating: 5,
|
|
596
|
+
category: 'trust',
|
|
597
|
+
fix: 'Ensure background agents create branches and PRs, never push directly to main.',
|
|
598
|
+
template: null,
|
|
599
|
+
file: () => '.cursor/rules/',
|
|
600
|
+
line: () => null,
|
|
601
|
+
},
|
|
602
|
+
|
|
603
|
+
cursorCodeReversionRisk: {
|
|
604
|
+
id: 'CU-C07',
|
|
605
|
+
name: 'Code reversion risk mitigated (format-on-save + agent review conflict)',
|
|
606
|
+
check: (ctx) => {
|
|
607
|
+
const vscodeRaw = ctx.fileContent('.vscode/settings.json') || '';
|
|
608
|
+
const hasFormatOnSave = /formatOnSave.*true/i.test(vscodeRaw);
|
|
609
|
+
if (!hasFormatOnSave) return null; // No risk if format-on-save is off
|
|
610
|
+
// Check if rules document the risk
|
|
611
|
+
const rules = allRulesContent(ctx);
|
|
612
|
+
return /code reversion|format.*save.*conflict|revert|format.*save.*warning/i.test(rules);
|
|
613
|
+
},
|
|
614
|
+
impact: 'critical',
|
|
615
|
+
rating: 5,
|
|
616
|
+
category: 'trust',
|
|
617
|
+
fix: 'Document code reversion risk: format-on-save + agent review tab + cloud sync can cause silent code loss.',
|
|
618
|
+
template: null,
|
|
619
|
+
file: () => '.cursor/rules/',
|
|
620
|
+
line: () => null,
|
|
621
|
+
},
|
|
622
|
+
|
|
623
|
+
cursorEnterprisePrivacyMode: {
|
|
624
|
+
id: 'CU-C08',
|
|
625
|
+
name: 'Enterprise: org-wide Privacy Mode enforced',
|
|
626
|
+
check: (ctx) => {
|
|
627
|
+
const docs = docsBundle(ctx);
|
|
628
|
+
if (!/enterprise|org.*policy/i.test(docs)) return null;
|
|
629
|
+
return /privacy mode.*enforc|org.*privacy|enterprise.*privacy/i.test(docs);
|
|
630
|
+
},
|
|
631
|
+
impact: 'medium',
|
|
632
|
+
rating: 4,
|
|
633
|
+
category: 'trust',
|
|
634
|
+
fix: 'Document whether org-wide Privacy Mode enforcement is active for Enterprise tier.',
|
|
635
|
+
template: null,
|
|
636
|
+
file: () => '.cursor/rules/',
|
|
637
|
+
line: () => null,
|
|
638
|
+
},
|
|
639
|
+
|
|
640
|
+
cursorNoWildcardAutomation: {
|
|
641
|
+
id: 'CU-C09',
|
|
642
|
+
name: 'No wildcard automation triggers (e.g., "on any push")',
|
|
643
|
+
check: (ctx) => {
|
|
644
|
+
const configs = ctx.automationsConfig ? ctx.automationsConfig() : [];
|
|
645
|
+
if (configs.length === 0) return null;
|
|
646
|
+
const combined = configs.map(c => c.content).join('\n');
|
|
647
|
+
const hasWildcard = /branches:.*\*|on:.*\*|trigger:.*all/i.test(combined);
|
|
648
|
+
return !hasWildcard;
|
|
649
|
+
},
|
|
650
|
+
impact: 'high',
|
|
651
|
+
rating: 5,
|
|
652
|
+
category: 'trust',
|
|
653
|
+
fix: 'Replace wildcard automation triggers with specific branch/event patterns.',
|
|
654
|
+
template: null,
|
|
655
|
+
file: () => '.cursor/automations/',
|
|
656
|
+
line: () => null,
|
|
657
|
+
},
|
|
658
|
+
|
|
659
|
+
// =============================================
|
|
660
|
+
// D. Agent Mode (5 checks) — CU-D01..CU-D05
|
|
661
|
+
// =============================================
|
|
662
|
+
|
|
663
|
+
cursorRulesReachAgent: {
|
|
664
|
+
id: 'CU-D01',
|
|
665
|
+
name: 'Rules properly reach agent (not just .cursorrules)',
|
|
666
|
+
check: (ctx) => {
|
|
667
|
+
const rules = ctx.cursorRules ? ctx.cursorRules() : [];
|
|
668
|
+
const hasLegacy = ctx.hasLegacyRules ? ctx.hasLegacyRules() : false;
|
|
669
|
+
if (rules.length === 0 && !hasLegacy) return null;
|
|
670
|
+
// Must have .mdc rules, not just legacy
|
|
671
|
+
return rules.length > 0;
|
|
672
|
+
},
|
|
673
|
+
impact: 'critical',
|
|
674
|
+
rating: 5,
|
|
675
|
+
category: 'agent-mode',
|
|
676
|
+
fix: 'Create .cursor/rules/*.mdc files with proper frontmatter. .cursorrules is ignored by agent mode!',
|
|
677
|
+
template: 'cursor-rules',
|
|
678
|
+
file: () => '.cursor/rules/',
|
|
679
|
+
line: () => null,
|
|
680
|
+
},
|
|
681
|
+
|
|
682
|
+
cursorCodebaseIndexed: {
|
|
683
|
+
id: 'CU-D02',
|
|
684
|
+
name: 'Agent has appropriate context scope (@Codebase indexed)',
|
|
685
|
+
check: (ctx) => {
|
|
686
|
+
const rules = allRulesContent(ctx);
|
|
687
|
+
if (!rules.trim()) return null;
|
|
688
|
+
return /@Codebase|@codebase|semantic search|codebase.*index/i.test(rules);
|
|
689
|
+
},
|
|
690
|
+
impact: 'medium',
|
|
691
|
+
rating: 3,
|
|
692
|
+
category: 'agent-mode',
|
|
693
|
+
fix: 'Mention @Codebase in rules to guide agents to use project-wide semantic search.',
|
|
694
|
+
template: null,
|
|
695
|
+
file: () => '.cursor/rules/',
|
|
696
|
+
line: () => null,
|
|
697
|
+
},
|
|
698
|
+
|
|
699
|
+
cursorMultiAgentWorktree: {
|
|
700
|
+
id: 'CU-D03',
|
|
701
|
+
name: 'Multi-agent workflows use Git worktree isolation',
|
|
702
|
+
check: (ctx) => {
|
|
703
|
+
const rules = allRulesContent(ctx);
|
|
704
|
+
if (!rules.trim()) return null;
|
|
705
|
+
// Only relevant if multi-agent mentioned
|
|
706
|
+
if (!/multi.?agent|parallel agent|agent.*window/i.test(rules)) return null;
|
|
707
|
+
return /worktree|git worktree|isolat/i.test(rules);
|
|
708
|
+
},
|
|
709
|
+
impact: 'medium',
|
|
710
|
+
rating: 3,
|
|
711
|
+
category: 'agent-mode',
|
|
712
|
+
fix: 'For multi-agent workflows (Agents Window), use Git worktree isolation to prevent conflicts.',
|
|
713
|
+
template: null,
|
|
714
|
+
file: () => '.cursor/rules/',
|
|
715
|
+
line: () => null,
|
|
716
|
+
},
|
|
717
|
+
|
|
718
|
+
cursorSessionLengthAwareness: {
|
|
719
|
+
id: 'CU-D04',
|
|
720
|
+
name: 'Agent session length awareness (<2h recommended)',
|
|
721
|
+
check: (ctx) => {
|
|
722
|
+
const rules = allRulesContent(ctx);
|
|
723
|
+
if (!rules.trim()) return null;
|
|
724
|
+
return /session.*length|session.*limit|2.*hour|context.*drift|long.*session/i.test(rules);
|
|
725
|
+
},
|
|
726
|
+
impact: 'low',
|
|
727
|
+
rating: 2,
|
|
728
|
+
category: 'agent-mode',
|
|
729
|
+
fix: 'Document session length recommendations. Sessions >2h may lose agent context.',
|
|
730
|
+
template: null,
|
|
731
|
+
file: () => '.cursor/rules/',
|
|
732
|
+
line: () => null,
|
|
733
|
+
},
|
|
734
|
+
|
|
735
|
+
cursorDocsConfigured: {
|
|
736
|
+
id: 'CU-D05',
|
|
737
|
+
name: '@Docs configured for project key libraries',
|
|
738
|
+
check: (ctx) => {
|
|
739
|
+
const rules = allRulesContent(ctx);
|
|
740
|
+
if (!rules.trim()) return null;
|
|
741
|
+
return /@Docs|@docs|documentation.*index|library.*doc/i.test(rules);
|
|
742
|
+
},
|
|
743
|
+
impact: 'medium',
|
|
744
|
+
rating: 3,
|
|
745
|
+
category: 'agent-mode',
|
|
746
|
+
fix: 'Configure @Docs for key project libraries to give agents access to current documentation.',
|
|
747
|
+
template: null,
|
|
748
|
+
file: () => '.cursor/rules/',
|
|
749
|
+
line: () => null,
|
|
750
|
+
},
|
|
751
|
+
|
|
752
|
+
// =============================================
|
|
753
|
+
// E. MCP (5 checks) — CU-E01..CU-E05
|
|
754
|
+
// =============================================
|
|
755
|
+
|
|
756
|
+
cursorMcpPerSurface: {
|
|
757
|
+
id: 'CU-E01',
|
|
758
|
+
name: 'MCP servers configured per surface (project + global)',
|
|
759
|
+
check: (ctx) => {
|
|
760
|
+
const project = ctx.mcpConfig();
|
|
761
|
+
if (!project.ok) return null;
|
|
762
|
+
return true;
|
|
763
|
+
},
|
|
764
|
+
impact: 'medium',
|
|
765
|
+
rating: 3,
|
|
766
|
+
category: 'mcp',
|
|
767
|
+
fix: 'Configure project-level MCP in .cursor/mcp.json. Global config at ~/.cursor/mcp.json.',
|
|
768
|
+
template: 'cursor-mcp',
|
|
769
|
+
file: () => '.cursor/mcp.json',
|
|
770
|
+
line: () => null,
|
|
771
|
+
},
|
|
772
|
+
|
|
773
|
+
cursorMcpProjectOverride: {
|
|
774
|
+
id: 'CU-E02',
|
|
775
|
+
name: 'Project mcp.json overrides global correctly',
|
|
776
|
+
check: (ctx) => {
|
|
777
|
+
const project = ctx.mcpConfig();
|
|
778
|
+
const global = ctx.globalMcpConfig ? ctx.globalMcpConfig() : { ok: false };
|
|
779
|
+
if (!project.ok || !global.ok) return null;
|
|
780
|
+
// Just verify both parse correctly — Cursor handles override automatically
|
|
781
|
+
return true;
|
|
782
|
+
},
|
|
783
|
+
impact: 'medium',
|
|
784
|
+
rating: 3,
|
|
785
|
+
category: 'mcp',
|
|
786
|
+
fix: 'Ensure project .cursor/mcp.json and global ~/.cursor/mcp.json are both valid JSON.',
|
|
787
|
+
template: null,
|
|
788
|
+
file: () => '.cursor/mcp.json',
|
|
789
|
+
line: () => null,
|
|
790
|
+
},
|
|
791
|
+
|
|
792
|
+
cursorMcpEnvVarSyntax: {
|
|
793
|
+
id: 'CU-E03',
|
|
794
|
+
name: 'MCP env vars use ${env:VAR} syntax (not hardcoded)',
|
|
795
|
+
check: (ctx) => {
|
|
796
|
+
const raw = mcpJsonRaw(ctx);
|
|
797
|
+
if (!raw) return null;
|
|
798
|
+
const data = mcpJsonData(ctx);
|
|
799
|
+
if (!data) return null;
|
|
800
|
+
const validation = validateMcpEnvVars(data);
|
|
801
|
+
return validation.valid;
|
|
802
|
+
},
|
|
803
|
+
impact: 'high',
|
|
804
|
+
rating: 5,
|
|
805
|
+
category: 'mcp',
|
|
806
|
+
fix: 'Use ${env:VAR_NAME} syntax for MCP environment variables instead of hardcoded values.',
|
|
807
|
+
template: null,
|
|
808
|
+
file: () => '.cursor/mcp.json',
|
|
809
|
+
line: () => null,
|
|
810
|
+
},
|
|
811
|
+
|
|
812
|
+
cursorMcpCurrentVersion: {
|
|
813
|
+
id: 'CU-E04',
|
|
814
|
+
name: 'MCP marketplace servers are current version',
|
|
815
|
+
check: (ctx) => {
|
|
816
|
+
const raw = mcpJsonRaw(ctx);
|
|
817
|
+
if (!raw) return null;
|
|
818
|
+
// Check for @latest or explicit version
|
|
819
|
+
const hasStale = /\b\d+\.\d+\.\d+\b/.test(raw) && !/@latest\b/.test(raw);
|
|
820
|
+
return !hasStale;
|
|
821
|
+
},
|
|
822
|
+
impact: 'low',
|
|
823
|
+
rating: 2,
|
|
824
|
+
category: 'mcp',
|
|
825
|
+
fix: 'Use @latest for MCP packages or regularly update pinned versions.',
|
|
826
|
+
template: null,
|
|
827
|
+
file: () => '.cursor/mcp.json',
|
|
828
|
+
line: () => null,
|
|
829
|
+
},
|
|
830
|
+
|
|
831
|
+
cursorMcpBackgroundAccess: {
|
|
832
|
+
id: 'CU-E05',
|
|
833
|
+
name: 'Background agent -p mode has MCP access (known bug)',
|
|
834
|
+
check: (ctx) => {
|
|
835
|
+
const env = envJsonData(ctx);
|
|
836
|
+
const mcp = mcpJsonData(ctx);
|
|
837
|
+
if (!env || !mcp) return null;
|
|
838
|
+
// Document awareness of the known bug
|
|
839
|
+
const rules = allRulesContent(ctx);
|
|
840
|
+
return /background.*mcp|mcp.*background|-p.*mode|programmatic.*mode/i.test(rules);
|
|
841
|
+
},
|
|
842
|
+
impact: 'medium',
|
|
843
|
+
rating: 3,
|
|
844
|
+
category: 'mcp',
|
|
845
|
+
fix: 'Document MCP access limitations in background agent -p mode (known bug).',
|
|
846
|
+
template: null,
|
|
847
|
+
file: () => '.cursor/rules/',
|
|
848
|
+
line: () => null,
|
|
849
|
+
},
|
|
850
|
+
|
|
851
|
+
// =============================================
|
|
852
|
+
// F. Instructions Quality (5 checks) — CU-F01..CU-F05
|
|
853
|
+
// =============================================
|
|
854
|
+
|
|
855
|
+
cursorRulesIncludeCommands: {
|
|
856
|
+
id: 'CU-F01',
|
|
857
|
+
name: 'Rules include build/test/lint commands',
|
|
858
|
+
check: (ctx) => {
|
|
859
|
+
const content = coreRulesContent(ctx) || allRulesContent(ctx);
|
|
860
|
+
if (!content.trim()) return null;
|
|
861
|
+
const expected = expectedVerificationCategories(ctx);
|
|
862
|
+
if (expected.length === 0) return /\bverify\b|\btest\b|\blint\b|\bbuild\b/i.test(content);
|
|
863
|
+
return expected.every(cat => hasCommandMention(content, cat));
|
|
864
|
+
},
|
|
865
|
+
impact: 'high',
|
|
866
|
+
rating: 5,
|
|
867
|
+
category: 'instructions-quality',
|
|
868
|
+
fix: 'Add actual build/test/lint commands to your core rules so agents can verify their changes.',
|
|
869
|
+
template: 'cursor-rules',
|
|
870
|
+
file: () => '.cursor/rules/',
|
|
871
|
+
line: () => null,
|
|
872
|
+
},
|
|
873
|
+
|
|
874
|
+
cursorRulesArchitecture: {
|
|
875
|
+
id: 'CU-F02',
|
|
876
|
+
name: 'Rules include architecture section or Mermaid diagram',
|
|
877
|
+
check: (ctx) => {
|
|
878
|
+
const content = allRulesContent(ctx);
|
|
879
|
+
if (!content.trim()) return null;
|
|
880
|
+
return hasArchitecture(content);
|
|
881
|
+
},
|
|
882
|
+
impact: 'medium',
|
|
883
|
+
rating: 4,
|
|
884
|
+
category: 'instructions-quality',
|
|
885
|
+
fix: 'Add an architecture section or Mermaid diagram to your core rule to orient agents.',
|
|
886
|
+
template: 'cursor-rules',
|
|
887
|
+
file: () => '.cursor/rules/',
|
|
888
|
+
line: () => null,
|
|
889
|
+
},
|
|
890
|
+
|
|
891
|
+
cursorRulesVerification: {
|
|
892
|
+
id: 'CU-F03',
|
|
893
|
+
name: 'Rules mention verification/testing expectations',
|
|
894
|
+
check: (ctx) => {
|
|
895
|
+
const content = allRulesContent(ctx);
|
|
896
|
+
if (!content.trim()) return null;
|
|
897
|
+
return /\bverif|\btest.*before|\bbefore.*commit|\brun test|\bensure test/i.test(content);
|
|
898
|
+
},
|
|
899
|
+
impact: 'high',
|
|
900
|
+
rating: 5,
|
|
901
|
+
category: 'instructions-quality',
|
|
902
|
+
fix: 'Add verification expectations: agents should run tests before declaring a task complete.',
|
|
903
|
+
template: 'cursor-rules',
|
|
904
|
+
file: () => '.cursor/rules/',
|
|
905
|
+
line: () => null,
|
|
906
|
+
},
|
|
907
|
+
|
|
908
|
+
cursorRulesNoContradictions: {
|
|
909
|
+
id: 'CU-F04',
|
|
910
|
+
name: 'No contradictions between rules',
|
|
911
|
+
check: (ctx) => {
|
|
912
|
+
const rules = ctx.cursorRules ? ctx.cursorRules() : [];
|
|
913
|
+
if (rules.length < 2) return null;
|
|
914
|
+
// Simple heuristic: check for opposing instructions
|
|
915
|
+
const combined = rules.map(r => r.body || '').join('\n');
|
|
916
|
+
const hasContradiction = /\bnever use.*\balways use|\balways.*\bnever/i.test(combined);
|
|
917
|
+
return !hasContradiction;
|
|
918
|
+
},
|
|
919
|
+
impact: 'medium',
|
|
920
|
+
rating: 3,
|
|
921
|
+
category: 'instructions-quality',
|
|
922
|
+
fix: 'Review rules for contradictions. Cursor concatenates all matching rules without conflict resolution.',
|
|
923
|
+
template: null,
|
|
924
|
+
file: () => '.cursor/rules/',
|
|
925
|
+
line: () => null,
|
|
926
|
+
},
|
|
927
|
+
|
|
928
|
+
cursorRulesProjectSpecific: {
|
|
929
|
+
id: 'CU-F05',
|
|
930
|
+
name: 'Rules reference project-specific patterns (not generic)',
|
|
931
|
+
check: (ctx) => {
|
|
932
|
+
const content = allRulesContent(ctx);
|
|
933
|
+
if (!content.trim()) return null;
|
|
934
|
+
const pkg = ctx.jsonFile ? ctx.jsonFile('package.json') : null;
|
|
935
|
+
const projectName = (pkg && pkg.name) || path.basename(ctx.dir);
|
|
936
|
+
// Check for project-specific references
|
|
937
|
+
const hasSpecific = content.includes(projectName) ||
|
|
938
|
+
/src\/|app\/|api\/|routes\/|services\/|components\/|lib\/|cmd\//i.test(content);
|
|
939
|
+
return hasSpecific;
|
|
940
|
+
},
|
|
941
|
+
impact: 'medium',
|
|
942
|
+
rating: 3,
|
|
943
|
+
category: 'instructions-quality',
|
|
944
|
+
fix: 'Reference actual project directories and patterns in rules instead of generic instructions.',
|
|
945
|
+
template: null,
|
|
946
|
+
file: () => '.cursor/rules/',
|
|
947
|
+
line: () => null,
|
|
948
|
+
},
|
|
949
|
+
|
|
950
|
+
// =============================================
|
|
951
|
+
// G. Background Agents (5 checks) — CU-G01..CU-G05
|
|
952
|
+
// =============================================
|
|
953
|
+
|
|
954
|
+
cursorEnvBaseImage: {
|
|
955
|
+
id: 'CU-G01',
|
|
956
|
+
name: 'environment.json has appropriate baseImage',
|
|
957
|
+
check: (ctx) => {
|
|
958
|
+
const env = envJsonData(ctx);
|
|
959
|
+
if (!env) return null;
|
|
960
|
+
return Boolean(env.baseImage);
|
|
961
|
+
},
|
|
962
|
+
impact: 'high',
|
|
963
|
+
rating: 4,
|
|
964
|
+
category: 'background-agents',
|
|
965
|
+
fix: 'Set baseImage in .cursor/environment.json (e.g., "node:20", "python:3.12").',
|
|
966
|
+
template: 'cursor-environment',
|
|
967
|
+
file: () => '.cursor/environment.json',
|
|
968
|
+
line: () => 1,
|
|
969
|
+
},
|
|
970
|
+
|
|
971
|
+
cursorEnvPersistedDirs: {
|
|
972
|
+
id: 'CU-G02',
|
|
973
|
+
name: 'persistedDirectories includes node_modules/venv',
|
|
974
|
+
check: (ctx) => {
|
|
975
|
+
const env = envJsonData(ctx);
|
|
976
|
+
if (!env) return null;
|
|
977
|
+
const persisted = env.persistedDirectories || [];
|
|
978
|
+
if (persisted.length === 0) return false;
|
|
979
|
+
return true;
|
|
980
|
+
},
|
|
981
|
+
impact: 'medium',
|
|
982
|
+
rating: 3,
|
|
983
|
+
category: 'background-agents',
|
|
984
|
+
fix: 'Add persistedDirectories to environment.json to cache dependencies between runs.',
|
|
985
|
+
template: 'cursor-environment',
|
|
986
|
+
file: () => '.cursor/environment.json',
|
|
987
|
+
line: () => null,
|
|
988
|
+
},
|
|
989
|
+
|
|
990
|
+
cursorEnvProcesses: {
|
|
991
|
+
id: 'CU-G03',
|
|
992
|
+
name: 'Processes defined for dev servers if needed',
|
|
993
|
+
check: (ctx) => {
|
|
994
|
+
const env = envJsonData(ctx);
|
|
995
|
+
if (!env) return null;
|
|
996
|
+
// Only relevant if project has dev server
|
|
997
|
+
const pkg = ctx.jsonFile ? ctx.jsonFile('package.json') : null;
|
|
998
|
+
const hasDevServer = pkg && pkg.scripts && (pkg.scripts.dev || pkg.scripts.start);
|
|
999
|
+
if (!hasDevServer) return null;
|
|
1000
|
+
return Boolean(env.processes && Object.keys(env.processes).length > 0);
|
|
1001
|
+
},
|
|
1002
|
+
impact: 'medium',
|
|
1003
|
+
rating: 3,
|
|
1004
|
+
category: 'background-agents',
|
|
1005
|
+
fix: 'Define processes in environment.json for dev servers that background agents need.',
|
|
1006
|
+
template: 'cursor-environment',
|
|
1007
|
+
file: () => '.cursor/environment.json',
|
|
1008
|
+
line: () => null,
|
|
1009
|
+
},
|
|
1010
|
+
|
|
1011
|
+
cursorEnvSecretsKms: {
|
|
1012
|
+
id: 'CU-G04',
|
|
1013
|
+
name: 'Secrets use KMS (not plaintext env vars)',
|
|
1014
|
+
check: (ctx) => {
|
|
1015
|
+
const envContent = ctx.fileContent('.cursor/environment.json') || '';
|
|
1016
|
+
if (!envContent.trim()) return null;
|
|
1017
|
+
// Check for hardcoded secret-looking values
|
|
1018
|
+
return !containsEmbeddedSecret(envContent);
|
|
1019
|
+
},
|
|
1020
|
+
impact: 'critical',
|
|
1021
|
+
rating: 5,
|
|
1022
|
+
category: 'background-agents',
|
|
1023
|
+
fix: 'Use KMS-encrypted secrets vault for background agents. Never put secrets in environment.json.',
|
|
1024
|
+
template: null,
|
|
1025
|
+
file: () => '.cursor/environment.json',
|
|
1026
|
+
line: () => null,
|
|
1027
|
+
},
|
|
1028
|
+
|
|
1029
|
+
cursorEnvCreatesPr: {
|
|
1030
|
+
id: 'CU-G05',
|
|
1031
|
+
name: 'Agent output creates PR (not direct push)',
|
|
1032
|
+
check: (ctx) => {
|
|
1033
|
+
const env = envJsonData(ctx);
|
|
1034
|
+
if (!env) return null;
|
|
1035
|
+
// Background agents create PRs by default, check for override
|
|
1036
|
+
const rules = allRulesContent(ctx);
|
|
1037
|
+
const hasBadPattern = /push.*directly|commit.*main|--force push/i.test(rules);
|
|
1038
|
+
return !hasBadPattern;
|
|
1039
|
+
},
|
|
1040
|
+
impact: 'high',
|
|
1041
|
+
rating: 5,
|
|
1042
|
+
category: 'background-agents',
|
|
1043
|
+
fix: 'Ensure background agents create PRs for review, never push directly to main.',
|
|
1044
|
+
template: null,
|
|
1045
|
+
file: () => '.cursor/rules/',
|
|
1046
|
+
line: () => null,
|
|
1047
|
+
},
|
|
1048
|
+
|
|
1049
|
+
// =============================================
|
|
1050
|
+
// H. Automations (5 checks) — CU-H01..CU-H05
|
|
1051
|
+
// =============================================
|
|
1052
|
+
|
|
1053
|
+
cursorAutomationDocumented: {
|
|
1054
|
+
id: 'CU-H01',
|
|
1055
|
+
name: 'Automation triggers are documented',
|
|
1056
|
+
check: (ctx) => {
|
|
1057
|
+
const configs = ctx.automationsConfig ? ctx.automationsConfig() : [];
|
|
1058
|
+
if (configs.length === 0) return null;
|
|
1059
|
+
// Check that each automation has clear name/description
|
|
1060
|
+
return configs.every(c => {
|
|
1061
|
+
const content = c.content || '';
|
|
1062
|
+
return /name:|description:|instructions:/i.test(content);
|
|
1063
|
+
});
|
|
1064
|
+
},
|
|
1065
|
+
impact: 'high',
|
|
1066
|
+
rating: 4,
|
|
1067
|
+
category: 'automations',
|
|
1068
|
+
fix: 'Document each automation with name, description, and clear instructions.',
|
|
1069
|
+
template: null,
|
|
1070
|
+
file: () => '.cursor/automations/',
|
|
1071
|
+
line: () => null,
|
|
1072
|
+
},
|
|
1073
|
+
|
|
1074
|
+
cursorAutomationNoBroadTrigger: {
|
|
1075
|
+
id: 'CU-H02',
|
|
1076
|
+
name: 'No overly broad triggers (e.g., "any push to any branch")',
|
|
1077
|
+
check: (ctx) => {
|
|
1078
|
+
const combined = automationContents(ctx);
|
|
1079
|
+
if (!combined.trim()) return null;
|
|
1080
|
+
const hasBroad = /branches:.*\*|on:.*\*.*push|any.*branch|all.*events/i.test(combined);
|
|
1081
|
+
return !hasBroad;
|
|
1082
|
+
},
|
|
1083
|
+
impact: 'high',
|
|
1084
|
+
rating: 5,
|
|
1085
|
+
category: 'automations',
|
|
1086
|
+
fix: 'Scope automation triggers to specific branches. Avoid wildcards that trigger on every push.',
|
|
1087
|
+
template: null,
|
|
1088
|
+
file: () => '.cursor/automations/',
|
|
1089
|
+
line: () => null,
|
|
1090
|
+
},
|
|
1091
|
+
|
|
1092
|
+
cursorAutomationErrorHandling: {
|
|
1093
|
+
id: 'CU-H03',
|
|
1094
|
+
name: 'Automation has error handling / failure notification',
|
|
1095
|
+
check: (ctx) => {
|
|
1096
|
+
const combined = automationContents(ctx);
|
|
1097
|
+
if (!combined.trim()) return null;
|
|
1098
|
+
return /error|fail|notification|alert|on_failure|on_error/i.test(combined);
|
|
1099
|
+
},
|
|
1100
|
+
impact: 'medium',
|
|
1101
|
+
rating: 3,
|
|
1102
|
+
category: 'automations',
|
|
1103
|
+
fix: 'Add error handling and failure notification to automations.',
|
|
1104
|
+
template: null,
|
|
1105
|
+
file: () => '.cursor/automations/',
|
|
1106
|
+
line: () => null,
|
|
1107
|
+
},
|
|
1108
|
+
|
|
1109
|
+
cursorAutomationRateLimits: {
|
|
1110
|
+
id: 'CU-H04',
|
|
1111
|
+
name: 'Rate limits considered (hundreds/hour possible)',
|
|
1112
|
+
check: (ctx) => {
|
|
1113
|
+
const combined = automationContents(ctx);
|
|
1114
|
+
if (!combined.trim()) return null;
|
|
1115
|
+
return /debounce|rate.?limit|throttle|cool.?down|max.*per/i.test(combined);
|
|
1116
|
+
},
|
|
1117
|
+
impact: 'medium',
|
|
1118
|
+
rating: 3,
|
|
1119
|
+
category: 'automations',
|
|
1120
|
+
fix: 'Add debounce_ms or rate limiting to automations that could fire frequently.',
|
|
1121
|
+
template: null,
|
|
1122
|
+
file: () => '.cursor/automations/',
|
|
1123
|
+
line: () => null,
|
|
1124
|
+
},
|
|
1125
|
+
|
|
1126
|
+
cursorAutomationScopedPerms: {
|
|
1127
|
+
id: 'CU-H05',
|
|
1128
|
+
name: 'Automation agents have scoped permissions',
|
|
1129
|
+
check: (ctx) => {
|
|
1130
|
+
const combined = automationContents(ctx);
|
|
1131
|
+
if (!combined.trim()) return null;
|
|
1132
|
+
return /sandbox|permission|scope|restrict|limited/i.test(combined);
|
|
1133
|
+
},
|
|
1134
|
+
impact: 'high',
|
|
1135
|
+
rating: 4,
|
|
1136
|
+
category: 'automations',
|
|
1137
|
+
fix: 'Define scoped permissions and sandbox config for each automation agent.',
|
|
1138
|
+
template: null,
|
|
1139
|
+
file: () => '.cursor/automations/',
|
|
1140
|
+
line: () => null,
|
|
1141
|
+
},
|
|
1142
|
+
|
|
1143
|
+
// =============================================
|
|
1144
|
+
// I. Enterprise (5 checks) — CU-I01..CU-I05
|
|
1145
|
+
// =============================================
|
|
1146
|
+
|
|
1147
|
+
cursorEnterpriseSso: {
|
|
1148
|
+
id: 'CU-I01',
|
|
1149
|
+
name: 'SSO configured (SAML/OIDC) if Enterprise',
|
|
1150
|
+
check: (ctx) => {
|
|
1151
|
+
const docs = docsBundle(ctx);
|
|
1152
|
+
if (!/enterprise/i.test(docs)) return null;
|
|
1153
|
+
return /sso|saml|oidc|single sign/i.test(docs);
|
|
1154
|
+
},
|
|
1155
|
+
impact: 'medium',
|
|
1156
|
+
rating: 3,
|
|
1157
|
+
category: 'enterprise',
|
|
1158
|
+
fix: 'Configure SSO (SAML/OIDC) for Enterprise tier deployments.',
|
|
1159
|
+
template: null,
|
|
1160
|
+
file: () => '.cursor/rules/',
|
|
1161
|
+
line: () => null,
|
|
1162
|
+
},
|
|
1163
|
+
|
|
1164
|
+
cursorEnterpriseScim: {
|
|
1165
|
+
id: 'CU-I02',
|
|
1166
|
+
name: 'SCIM 2.0 provisioning active',
|
|
1167
|
+
check: (ctx) => {
|
|
1168
|
+
const docs = docsBundle(ctx);
|
|
1169
|
+
if (!/enterprise/i.test(docs)) return null;
|
|
1170
|
+
return /scim|provisioning|directory sync/i.test(docs);
|
|
1171
|
+
},
|
|
1172
|
+
impact: 'low',
|
|
1173
|
+
rating: 2,
|
|
1174
|
+
category: 'enterprise',
|
|
1175
|
+
fix: 'Enable SCIM 2.0 provisioning for automated user management.',
|
|
1176
|
+
template: null,
|
|
1177
|
+
file: () => '.cursor/rules/',
|
|
1178
|
+
line: () => null,
|
|
1179
|
+
},
|
|
1180
|
+
|
|
1181
|
+
cursorEnterpriseAuditLogs: {
|
|
1182
|
+
id: 'CU-I03',
|
|
1183
|
+
name: 'Audit logs enabled',
|
|
1184
|
+
check: (ctx) => {
|
|
1185
|
+
const docs = docsBundle(ctx);
|
|
1186
|
+
if (!/enterprise/i.test(docs)) return null;
|
|
1187
|
+
return /audit log|audit trail|tracking/i.test(docs);
|
|
1188
|
+
},
|
|
1189
|
+
impact: 'medium',
|
|
1190
|
+
rating: 3,
|
|
1191
|
+
category: 'enterprise',
|
|
1192
|
+
fix: 'Enable audit logs for Enterprise tier to track AI code generation and tool usage.',
|
|
1193
|
+
template: null,
|
|
1194
|
+
file: () => '.cursor/rules/',
|
|
1195
|
+
line: () => null,
|
|
1196
|
+
},
|
|
1197
|
+
|
|
1198
|
+
cursorEnterpriseMcpAllowlist: {
|
|
1199
|
+
id: 'CU-I04',
|
|
1200
|
+
name: 'MCP server allowlist maintained',
|
|
1201
|
+
check: (ctx) => {
|
|
1202
|
+
const docs = docsBundle(ctx);
|
|
1203
|
+
if (!/enterprise/i.test(docs)) return null;
|
|
1204
|
+
return /mcp.*allowlist|allowlist.*mcp|approved.*server|server.*approval/i.test(docs);
|
|
1205
|
+
},
|
|
1206
|
+
impact: 'high',
|
|
1207
|
+
rating: 4,
|
|
1208
|
+
category: 'enterprise',
|
|
1209
|
+
fix: 'Maintain an MCP server allowlist for Enterprise deployments to control tool access.',
|
|
1210
|
+
template: null,
|
|
1211
|
+
file: () => '.cursor/rules/',
|
|
1212
|
+
line: () => null,
|
|
1213
|
+
},
|
|
1214
|
+
|
|
1215
|
+
cursorEnterpriseModelPolicy: {
|
|
1216
|
+
id: 'CU-I05',
|
|
1217
|
+
name: 'Model access policy matches team needs',
|
|
1218
|
+
check: (ctx) => {
|
|
1219
|
+
const docs = docsBundle(ctx);
|
|
1220
|
+
if (!/enterprise/i.test(docs)) return null;
|
|
1221
|
+
return /model.*policy|model.*access|allowed.*model|model.*restriction/i.test(docs);
|
|
1222
|
+
},
|
|
1223
|
+
impact: 'medium',
|
|
1224
|
+
rating: 3,
|
|
1225
|
+
category: 'enterprise',
|
|
1226
|
+
fix: 'Define model access policy for Enterprise — which models are available to team members.',
|
|
1227
|
+
template: null,
|
|
1228
|
+
file: () => '.cursor/rules/',
|
|
1229
|
+
line: () => null,
|
|
1230
|
+
},
|
|
1231
|
+
|
|
1232
|
+
// =============================================
|
|
1233
|
+
// J. BugBot & Code Review (4 checks) — CU-J01..CU-J04
|
|
1234
|
+
// =============================================
|
|
1235
|
+
|
|
1236
|
+
cursorBugbotEnabled: {
|
|
1237
|
+
id: 'CU-J01',
|
|
1238
|
+
name: 'BugBot enabled for critical repos',
|
|
1239
|
+
check: (ctx) => {
|
|
1240
|
+
const docs = docsBundle(ctx);
|
|
1241
|
+
if (!docs.trim()) return null;
|
|
1242
|
+
return /bugbot|bug.?bot|automated.*pr.*review/i.test(docs);
|
|
1243
|
+
},
|
|
1244
|
+
impact: 'medium',
|
|
1245
|
+
rating: 3,
|
|
1246
|
+
category: 'bugbot',
|
|
1247
|
+
fix: 'Enable BugBot for automated PR code review on critical repos.',
|
|
1248
|
+
template: null,
|
|
1249
|
+
file: () => '.cursor/rules/',
|
|
1250
|
+
line: () => null,
|
|
1251
|
+
},
|
|
1252
|
+
|
|
1253
|
+
cursorBugbotAutofix: {
|
|
1254
|
+
id: 'CU-J02',
|
|
1255
|
+
name: 'BugBot autofix configured appropriately',
|
|
1256
|
+
check: (ctx) => {
|
|
1257
|
+
const docs = docsBundle(ctx);
|
|
1258
|
+
if (!/bugbot|bug.?bot/i.test(docs)) return null;
|
|
1259
|
+
return /autofix|auto.?fix|fix.*mode|resolution/i.test(docs);
|
|
1260
|
+
},
|
|
1261
|
+
impact: 'medium',
|
|
1262
|
+
rating: 3,
|
|
1263
|
+
category: 'bugbot',
|
|
1264
|
+
fix: 'Configure BugBot autofix settings — decide which issue types should be auto-fixed vs flagged.',
|
|
1265
|
+
template: null,
|
|
1266
|
+
file: () => '.cursor/rules/',
|
|
1267
|
+
line: () => null,
|
|
1268
|
+
},
|
|
1269
|
+
|
|
1270
|
+
cursorReviewInstructionsLength: {
|
|
1271
|
+
id: 'CU-J03',
|
|
1272
|
+
name: 'Code review instructions within effective length',
|
|
1273
|
+
check: (ctx) => {
|
|
1274
|
+
const rules = ctx.cursorRules ? ctx.cursorRules() : [];
|
|
1275
|
+
const reviewRules = rules.filter(r =>
|
|
1276
|
+
/review|code.*review/i.test(r.name || '') ||
|
|
1277
|
+
(r.frontmatter && r.frontmatter.description && /review/i.test(r.frontmatter.description))
|
|
1278
|
+
);
|
|
1279
|
+
if (reviewRules.length === 0) return null;
|
|
1280
|
+
return reviewRules.every(r => wordCount(r.body) <= 400);
|
|
1281
|
+
},
|
|
1282
|
+
impact: 'medium',
|
|
1283
|
+
rating: 3,
|
|
1284
|
+
category: 'bugbot',
|
|
1285
|
+
fix: 'Keep code review instruction rules under ~400 words for reliable agent adherence.',
|
|
1286
|
+
template: null,
|
|
1287
|
+
file: () => '.cursor/rules/',
|
|
1288
|
+
line: () => null,
|
|
1289
|
+
},
|
|
1290
|
+
|
|
1291
|
+
cursorReviewNoConflict: {
|
|
1292
|
+
id: 'CU-J04',
|
|
1293
|
+
name: 'Review automation does not conflict with human review',
|
|
1294
|
+
check: (ctx) => {
|
|
1295
|
+
const configs = ctx.automationsConfig ? ctx.automationsConfig() : [];
|
|
1296
|
+
if (configs.length === 0) return null;
|
|
1297
|
+
const combined = configs.map(c => c.content).join('\n');
|
|
1298
|
+
const hasReviewAuto = /auto.*review|review.*auto|merge.*auto/i.test(combined);
|
|
1299
|
+
if (!hasReviewAuto) return null;
|
|
1300
|
+
// If auto-review exists, check it doesn't auto-merge
|
|
1301
|
+
return !/auto.*merge|merge.*without.*review/i.test(combined);
|
|
1302
|
+
},
|
|
1303
|
+
impact: 'low',
|
|
1304
|
+
rating: 2,
|
|
1305
|
+
category: 'bugbot',
|
|
1306
|
+
fix: 'Ensure automated review does not bypass human review requirements.',
|
|
1307
|
+
template: null,
|
|
1308
|
+
file: () => '.cursor/automations/',
|
|
1309
|
+
line: () => null,
|
|
1310
|
+
},
|
|
1311
|
+
|
|
1312
|
+
// =============================================
|
|
1313
|
+
// K. Cross-Surface Consistency (4 checks) — CU-K01..CU-K04
|
|
1314
|
+
// =============================================
|
|
1315
|
+
|
|
1316
|
+
cursorRulesConsistentSurfaces: {
|
|
1317
|
+
id: 'CU-K01',
|
|
1318
|
+
name: 'Rules consistent between foreground and background agents',
|
|
1319
|
+
check: (ctx) => {
|
|
1320
|
+
const rules = ctx.cursorRules ? ctx.cursorRules() : [];
|
|
1321
|
+
const env = envJsonData(ctx);
|
|
1322
|
+
if (rules.length === 0 || !env) return null;
|
|
1323
|
+
// If we have both surfaces, rules should exist for both
|
|
1324
|
+
return rules.length > 0;
|
|
1325
|
+
},
|
|
1326
|
+
impact: 'high',
|
|
1327
|
+
rating: 4,
|
|
1328
|
+
category: 'cross-surface',
|
|
1329
|
+
fix: 'Ensure .cursor/rules/ are accessible to both foreground and background agents.',
|
|
1330
|
+
template: null,
|
|
1331
|
+
file: () => '.cursor/rules/',
|
|
1332
|
+
line: () => null,
|
|
1333
|
+
},
|
|
1334
|
+
|
|
1335
|
+
cursorMcpConsistentSurfaces: {
|
|
1336
|
+
id: 'CU-K02',
|
|
1337
|
+
name: 'MCP config consistent across surfaces',
|
|
1338
|
+
check: (ctx) => {
|
|
1339
|
+
const project = ctx.mcpConfig();
|
|
1340
|
+
if (!project.ok) return null;
|
|
1341
|
+
// MCP config applies to foreground; background has separate access
|
|
1342
|
+
return true;
|
|
1343
|
+
},
|
|
1344
|
+
impact: 'medium',
|
|
1345
|
+
rating: 3,
|
|
1346
|
+
category: 'cross-surface',
|
|
1347
|
+
fix: 'Document which MCP servers are available to foreground vs background agents.',
|
|
1348
|
+
template: null,
|
|
1349
|
+
file: () => '.cursor/mcp.json',
|
|
1350
|
+
line: () => null,
|
|
1351
|
+
},
|
|
1352
|
+
|
|
1353
|
+
cursorAutomationRulesConsistent: {
|
|
1354
|
+
id: 'CU-K03',
|
|
1355
|
+
name: 'Automation agents have same rules as interactive agents',
|
|
1356
|
+
check: (ctx) => {
|
|
1357
|
+
const configs = ctx.automationsConfig ? ctx.automationsConfig() : [];
|
|
1358
|
+
if (configs.length === 0) return null;
|
|
1359
|
+
const rules = ctx.cursorRules ? ctx.cursorRules() : [];
|
|
1360
|
+
return rules.length > 0;
|
|
1361
|
+
},
|
|
1362
|
+
impact: 'high',
|
|
1363
|
+
rating: 4,
|
|
1364
|
+
category: 'cross-surface',
|
|
1365
|
+
fix: 'Ensure automation agents can access the same .cursor/rules/ as interactive agents.',
|
|
1366
|
+
template: null,
|
|
1367
|
+
file: () => '.cursor/rules/',
|
|
1368
|
+
line: () => null,
|
|
1369
|
+
},
|
|
1370
|
+
|
|
1371
|
+
cursorEnvMatchesLocal: {
|
|
1372
|
+
id: 'CU-K04',
|
|
1373
|
+
name: 'environment.json matches local dev environment',
|
|
1374
|
+
check: (ctx) => {
|
|
1375
|
+
const env = envJsonData(ctx);
|
|
1376
|
+
if (!env) return null;
|
|
1377
|
+
// Check that baseImage matches project stack
|
|
1378
|
+
const pkg = ctx.jsonFile ? ctx.jsonFile('package.json') : null;
|
|
1379
|
+
if (pkg && env.baseImage) {
|
|
1380
|
+
const isNode = /node/i.test(env.baseImage);
|
|
1381
|
+
const projectIsNode = Boolean(pkg.dependencies || pkg.devDependencies);
|
|
1382
|
+
return isNode === projectIsNode || true; // relaxed check
|
|
1383
|
+
}
|
|
1384
|
+
return true;
|
|
1385
|
+
},
|
|
1386
|
+
impact: 'medium',
|
|
1387
|
+
rating: 3,
|
|
1388
|
+
category: 'cross-surface',
|
|
1389
|
+
fix: 'Ensure environment.json baseImage matches the local development environment stack.',
|
|
1390
|
+
template: null,
|
|
1391
|
+
file: () => '.cursor/environment.json',
|
|
1392
|
+
line: () => null,
|
|
1393
|
+
},
|
|
1394
|
+
|
|
1395
|
+
// =============================================
|
|
1396
|
+
// L. Quality Deep (7 checks) — CU-L01..CU-L07
|
|
1397
|
+
// =============================================
|
|
1398
|
+
|
|
1399
|
+
cursorModernFeatures: {
|
|
1400
|
+
id: 'CU-L01',
|
|
1401
|
+
name: 'Rules mention modern Cursor features (automations, background agents, BugBot)',
|
|
1402
|
+
check: (ctx) => {
|
|
1403
|
+
const content = allRulesContent(ctx);
|
|
1404
|
+
if (!content.trim()) return null;
|
|
1405
|
+
return /automation|background agent|bugbot|bug.?bot|design mode|agent.*window/i.test(content);
|
|
1406
|
+
},
|
|
1407
|
+
impact: 'medium',
|
|
1408
|
+
rating: 3,
|
|
1409
|
+
category: 'quality-deep',
|
|
1410
|
+
fix: 'Document awareness of modern Cursor features: automations, background agents, BugBot, Design Mode.',
|
|
1411
|
+
template: null,
|
|
1412
|
+
file: () => '.cursor/rules/',
|
|
1413
|
+
line: () => null,
|
|
1414
|
+
},
|
|
1415
|
+
|
|
1416
|
+
cursorNoDeprecatedPatterns: {
|
|
1417
|
+
id: 'CU-L02',
|
|
1418
|
+
name: 'No deprecated patterns (Notepads, .cursorrules for agent)',
|
|
1419
|
+
check: (ctx) => {
|
|
1420
|
+
const content = allRulesContent(ctx);
|
|
1421
|
+
const legacy = ctx.legacyCursorrules ? ctx.legacyCursorrules() : null;
|
|
1422
|
+
const combined = `${content}\n${legacy || ''}`;
|
|
1423
|
+
if (!combined.trim()) return null;
|
|
1424
|
+
const hasDeprecated = /@Notepads|notepad/i.test(combined);
|
|
1425
|
+
return !hasDeprecated && !legacy;
|
|
1426
|
+
},
|
|
1427
|
+
impact: 'high',
|
|
1428
|
+
rating: 4,
|
|
1429
|
+
category: 'quality-deep',
|
|
1430
|
+
fix: 'Remove @Notepads references (deprecated Oct 2025). Migrate .cursorrules to .cursor/rules/*.mdc.',
|
|
1431
|
+
template: 'cursor-legacy-migration',
|
|
1432
|
+
file: () => '.cursor/rules/',
|
|
1433
|
+
line: () => null,
|
|
1434
|
+
},
|
|
1435
|
+
|
|
1436
|
+
cursorRuleCountManageable: {
|
|
1437
|
+
id: 'CU-L03',
|
|
1438
|
+
name: 'Rule file count is manageable (<20 files, avoid context bloat)',
|
|
1439
|
+
check: (ctx) => {
|
|
1440
|
+
const rules = ctx.cursorRules ? ctx.cursorRules() : [];
|
|
1441
|
+
if (rules.length === 0) return null;
|
|
1442
|
+
return rules.length < 20;
|
|
1443
|
+
},
|
|
1444
|
+
impact: 'medium',
|
|
1445
|
+
rating: 3,
|
|
1446
|
+
category: 'quality-deep',
|
|
1447
|
+
fix: 'Keep rule files under 20. Consolidate related rules to avoid context bloat.',
|
|
1448
|
+
template: null,
|
|
1449
|
+
file: () => '.cursor/rules/',
|
|
1450
|
+
line: () => null,
|
|
1451
|
+
},
|
|
1452
|
+
|
|
1453
|
+
cursorAlwaysApplyMinimized: {
|
|
1454
|
+
id: 'CU-L04',
|
|
1455
|
+
name: 'Always Apply rules minimized (token cost per message)',
|
|
1456
|
+
check: (ctx) => {
|
|
1457
|
+
const always = ctx.alwaysApplyRules ? ctx.alwaysApplyRules() : [];
|
|
1458
|
+
if (always.length === 0) return null;
|
|
1459
|
+
// More than 3 always-apply rules is excessive
|
|
1460
|
+
return always.length <= 3;
|
|
1461
|
+
},
|
|
1462
|
+
impact: 'medium',
|
|
1463
|
+
rating: 3,
|
|
1464
|
+
category: 'quality-deep',
|
|
1465
|
+
fix: 'Minimize Always Apply rules (keep to 1-3). Each one adds token cost to every message.',
|
|
1466
|
+
template: null,
|
|
1467
|
+
file: () => '.cursor/rules/',
|
|
1468
|
+
line: () => null,
|
|
1469
|
+
},
|
|
1470
|
+
|
|
1471
|
+
cursorNoNestedRulesDirs: {
|
|
1472
|
+
id: 'CU-L05',
|
|
1473
|
+
name: 'No nested rules directories (silently ignored by Cursor)',
|
|
1474
|
+
check: (ctx) => {
|
|
1475
|
+
const rulesDir = path.join(ctx.dir, '.cursor', 'rules');
|
|
1476
|
+
try {
|
|
1477
|
+
const entries = require('fs').readdirSync(rulesDir, { withFileTypes: true });
|
|
1478
|
+
const hasDirs = entries.some(e => e.isDirectory());
|
|
1479
|
+
return !hasDirs;
|
|
1480
|
+
} catch {
|
|
1481
|
+
return null;
|
|
1482
|
+
}
|
|
1483
|
+
},
|
|
1484
|
+
impact: 'low',
|
|
1485
|
+
rating: 2,
|
|
1486
|
+
category: 'quality-deep',
|
|
1487
|
+
fix: 'Remove subdirectories from .cursor/rules/ — Cursor silently ignores nested rule directories.',
|
|
1488
|
+
template: null,
|
|
1489
|
+
file: () => '.cursor/rules/',
|
|
1490
|
+
line: () => null,
|
|
1491
|
+
},
|
|
1492
|
+
|
|
1493
|
+
cursorDocsIndexed: {
|
|
1494
|
+
id: 'CU-L06',
|
|
1495
|
+
name: '@Docs indexed for project framework documentation',
|
|
1496
|
+
check: (ctx) => {
|
|
1497
|
+
const content = allRulesContent(ctx);
|
|
1498
|
+
if (!content.trim()) return null;
|
|
1499
|
+
return /@Docs|documentation.*crawl|docs.*index|library.*reference/i.test(content);
|
|
1500
|
+
},
|
|
1501
|
+
impact: 'medium',
|
|
1502
|
+
rating: 3,
|
|
1503
|
+
category: 'quality-deep',
|
|
1504
|
+
fix: 'Configure @Docs to index key framework documentation for better agent suggestions.',
|
|
1505
|
+
template: null,
|
|
1506
|
+
file: () => '.cursor/rules/',
|
|
1507
|
+
line: () => null,
|
|
1508
|
+
},
|
|
1509
|
+
|
|
1510
|
+
cursorSessionDriftAwareness: {
|
|
1511
|
+
id: 'CU-L07',
|
|
1512
|
+
name: 'Agent session drift awareness documented',
|
|
1513
|
+
check: (ctx) => {
|
|
1514
|
+
const content = allRulesContent(ctx);
|
|
1515
|
+
if (!content.trim()) return null;
|
|
1516
|
+
return /session.*drift|context.*window|long.*session|session.*length|refresh.*context/i.test(content);
|
|
1517
|
+
},
|
|
1518
|
+
impact: 'low',
|
|
1519
|
+
rating: 2,
|
|
1520
|
+
category: 'quality-deep',
|
|
1521
|
+
fix: 'Document session drift awareness. Long sessions (>2h) may lose agent context.',
|
|
1522
|
+
template: null,
|
|
1523
|
+
file: () => '.cursor/rules/',
|
|
1524
|
+
line: () => null,
|
|
1525
|
+
},
|
|
1526
|
+
|
|
1527
|
+
// =============================================
|
|
1528
|
+
// M. Advisory (4 checks) — CU-M01..CU-M04
|
|
1529
|
+
// =============================================
|
|
1530
|
+
|
|
1531
|
+
cursorAdvisoryInstructionQuality: {
|
|
1532
|
+
id: 'CU-M01',
|
|
1533
|
+
name: 'Instruction quality score meets advisory threshold',
|
|
1534
|
+
check: (ctx) => {
|
|
1535
|
+
const content = coreRulesContent(ctx) || allRulesContent(ctx);
|
|
1536
|
+
if (!content.trim()) return null;
|
|
1537
|
+
const lines = content.split(/\r?\n/).filter(l => l.trim()).length;
|
|
1538
|
+
const sections = countSections(content);
|
|
1539
|
+
const hasArch = hasArchitecture(content);
|
|
1540
|
+
const hasVerify = /\bverif|\btest|\blint|\bbuild/i.test(content);
|
|
1541
|
+
const score = (lines >= 30 ? 2 : lines >= 15 ? 1 : 0) +
|
|
1542
|
+
(sections >= 4 ? 2 : sections >= 2 ? 1 : 0) +
|
|
1543
|
+
(hasArch ? 1 : 0) +
|
|
1544
|
+
(hasVerify ? 1 : 0);
|
|
1545
|
+
return score >= 4;
|
|
1546
|
+
},
|
|
1547
|
+
impact: 'medium',
|
|
1548
|
+
rating: 4,
|
|
1549
|
+
category: 'advisory',
|
|
1550
|
+
fix: 'Improve rule quality: add more sections, architecture diagram, and verification commands.',
|
|
1551
|
+
template: 'cursor-rules',
|
|
1552
|
+
file: () => '.cursor/rules/',
|
|
1553
|
+
line: () => null,
|
|
1554
|
+
},
|
|
1555
|
+
|
|
1556
|
+
cursorAdvisorySecurityPosture: {
|
|
1557
|
+
id: 'CU-M02',
|
|
1558
|
+
name: 'Security posture meets advisory threshold',
|
|
1559
|
+
check: (ctx) => {
|
|
1560
|
+
let score = 0;
|
|
1561
|
+
const docs = docsBundle(ctx);
|
|
1562
|
+
if (/privacy mode/i.test(docs)) score++;
|
|
1563
|
+
if (!ctx.hasLegacyRules || !ctx.hasLegacyRules()) score++;
|
|
1564
|
+
const mcpResult = ctx.mcpConfig();
|
|
1565
|
+
if (mcpResult.ok) {
|
|
1566
|
+
const validation = validateMcpEnvVars(mcpResult.data);
|
|
1567
|
+
if (validation.valid) score++;
|
|
1568
|
+
} else {
|
|
1569
|
+
score++; // No MCP = no MCP risk
|
|
1570
|
+
}
|
|
1571
|
+
if (/security|secret|credential/i.test(docs)) score++;
|
|
1572
|
+
return score >= 2;
|
|
1573
|
+
},
|
|
1574
|
+
impact: 'high',
|
|
1575
|
+
rating: 5,
|
|
1576
|
+
category: 'advisory',
|
|
1577
|
+
fix: 'Improve security posture: enable Privacy Mode, migrate .cursorrules, secure MCP config.',
|
|
1578
|
+
template: null,
|
|
1579
|
+
file: () => '.cursor/rules/',
|
|
1580
|
+
line: () => null,
|
|
1581
|
+
},
|
|
1582
|
+
|
|
1583
|
+
cursorAdvisorySurfaceCoverage: {
|
|
1584
|
+
id: 'CU-M03',
|
|
1585
|
+
name: 'Surface coverage meets advisory threshold',
|
|
1586
|
+
check: (ctx) => {
|
|
1587
|
+
const surfaces = ctx.detectSurfaces ? ctx.detectSurfaces() : {};
|
|
1588
|
+
return surfaces.foreground === true;
|
|
1589
|
+
},
|
|
1590
|
+
impact: 'medium',
|
|
1591
|
+
rating: 4,
|
|
1592
|
+
category: 'advisory',
|
|
1593
|
+
fix: 'Configure at least the foreground agent surface with .cursor/rules/*.mdc files.',
|
|
1594
|
+
template: 'cursor-rules',
|
|
1595
|
+
file: () => '.cursor/rules/',
|
|
1596
|
+
line: () => null,
|
|
1597
|
+
},
|
|
1598
|
+
|
|
1599
|
+
cursorAdvisoryMcpHealth: {
|
|
1600
|
+
id: 'CU-M04',
|
|
1601
|
+
name: 'MCP configuration health meets advisory threshold',
|
|
1602
|
+
check: (ctx) => {
|
|
1603
|
+
const servers = ctx.mcpServers ? ctx.mcpServers() : {};
|
|
1604
|
+
const count = Object.keys(servers).length;
|
|
1605
|
+
if (count === 0) return null;
|
|
1606
|
+
const mcpResult = ctx.mcpConfig();
|
|
1607
|
+
return mcpResult && mcpResult.ok;
|
|
1608
|
+
},
|
|
1609
|
+
impact: 'medium',
|
|
1610
|
+
rating: 3,
|
|
1611
|
+
category: 'advisory',
|
|
1612
|
+
fix: 'Ensure MCP configuration is valid and servers are properly configured.',
|
|
1613
|
+
template: null,
|
|
1614
|
+
file: () => '.cursor/mcp.json',
|
|
1615
|
+
line: () => null,
|
|
1616
|
+
},
|
|
1617
|
+
|
|
1618
|
+
// =============================================
|
|
1619
|
+
// N. Pack (4 checks) — CU-N01..CU-N04
|
|
1620
|
+
// =============================================
|
|
1621
|
+
|
|
1622
|
+
cursorPackDomainDetected: {
|
|
1623
|
+
id: 'CU-N01',
|
|
1624
|
+
name: 'Domain pack detection returns relevant results',
|
|
1625
|
+
check: (ctx) => {
|
|
1626
|
+
const pkg = ctx.jsonFile ? ctx.jsonFile('package.json') : null;
|
|
1627
|
+
return Boolean(pkg || ctx.fileContent('go.mod') || ctx.fileContent('Cargo.toml') || ctx.fileContent('pyproject.toml'));
|
|
1628
|
+
},
|
|
1629
|
+
impact: 'low',
|
|
1630
|
+
rating: 2,
|
|
1631
|
+
category: 'advisory',
|
|
1632
|
+
fix: 'Ensure project has identifiable stack markers for domain pack detection.',
|
|
1633
|
+
template: null,
|
|
1634
|
+
file: () => 'package.json',
|
|
1635
|
+
line: () => null,
|
|
1636
|
+
},
|
|
1637
|
+
|
|
1638
|
+
cursorPackMcpRecommended: {
|
|
1639
|
+
id: 'CU-N02',
|
|
1640
|
+
name: 'MCP packs recommended based on project signals',
|
|
1641
|
+
check: (ctx) => {
|
|
1642
|
+
const servers = ctx.mcpServers ? ctx.mcpServers() : {};
|
|
1643
|
+
return Object.keys(servers).length > 0;
|
|
1644
|
+
},
|
|
1645
|
+
impact: 'low',
|
|
1646
|
+
rating: 2,
|
|
1647
|
+
category: 'advisory',
|
|
1648
|
+
fix: 'Add recommended MCP packs to .cursor/mcp.json based on project domain.',
|
|
1649
|
+
template: 'cursor-mcp',
|
|
1650
|
+
file: () => '.cursor/mcp.json',
|
|
1651
|
+
line: () => null,
|
|
1652
|
+
},
|
|
1653
|
+
|
|
1654
|
+
cursorPackGovernanceApplied: {
|
|
1655
|
+
id: 'CU-N03',
|
|
1656
|
+
name: 'Governance pack applied if enterprise signals detected',
|
|
1657
|
+
check: (ctx) => {
|
|
1658
|
+
const docs = docsBundle(ctx);
|
|
1659
|
+
if (!/enterprise|business/i.test(docs)) return null;
|
|
1660
|
+
return /governance|policy|audit/i.test(docs);
|
|
1661
|
+
},
|
|
1662
|
+
impact: 'medium',
|
|
1663
|
+
rating: 3,
|
|
1664
|
+
category: 'advisory',
|
|
1665
|
+
fix: 'Apply governance pack for enterprise repos.',
|
|
1666
|
+
template: null,
|
|
1667
|
+
file: () => '.cursor/rules/',
|
|
1668
|
+
line: () => null,
|
|
1669
|
+
},
|
|
1670
|
+
|
|
1671
|
+
cursorPackConsistency: {
|
|
1672
|
+
id: 'CU-N04',
|
|
1673
|
+
name: 'All applied packs are consistent with each other',
|
|
1674
|
+
check: (ctx) => {
|
|
1675
|
+
const rules = allRulesContent(ctx);
|
|
1676
|
+
const mcp = mcpJsonRaw(ctx);
|
|
1677
|
+
if (!rules && !mcp) return null;
|
|
1678
|
+
// No contradiction: if rules say "strict" and config says "yolo"
|
|
1679
|
+
const rulesStrict = /\bstrict\b|\blocked.?down\b|\bno auto/i.test(rules);
|
|
1680
|
+
const configPermissive = /yolo|auto.*run.*all/i.test(rules);
|
|
1681
|
+
return !(rulesStrict && configPermissive);
|
|
1682
|
+
},
|
|
1683
|
+
impact: 'medium',
|
|
1684
|
+
rating: 3,
|
|
1685
|
+
category: 'advisory',
|
|
1686
|
+
fix: 'Resolve contradictions between rule guidance and configuration.',
|
|
1687
|
+
template: null,
|
|
1688
|
+
file: () => '.cursor/rules/',
|
|
1689
|
+
line: () => null,
|
|
1690
|
+
},
|
|
1691
|
+
|
|
1692
|
+
// =============================================
|
|
1693
|
+
// O. Repeat (3 checks) — CU-O01..CU-O03
|
|
1694
|
+
// =============================================
|
|
1695
|
+
|
|
1696
|
+
cursorRepeatScoreImproved: {
|
|
1697
|
+
id: 'CU-O01',
|
|
1698
|
+
name: 'Audit score improved since last run',
|
|
1699
|
+
check: () => null, // Requires snapshot history — always N/A in static check
|
|
1700
|
+
impact: 'low',
|
|
1701
|
+
rating: 2,
|
|
1702
|
+
category: 'freshness',
|
|
1703
|
+
fix: 'Run audits regularly and track score improvement over time.',
|
|
1704
|
+
template: null,
|
|
1705
|
+
file: () => null,
|
|
1706
|
+
line: () => null,
|
|
1707
|
+
},
|
|
1708
|
+
|
|
1709
|
+
cursorRepeatNoRegressions: {
|
|
1710
|
+
id: 'CU-O02',
|
|
1711
|
+
name: 'No regressions since last audit',
|
|
1712
|
+
check: () => null, // Requires snapshot history
|
|
1713
|
+
impact: 'medium',
|
|
1714
|
+
rating: 3,
|
|
1715
|
+
category: 'freshness',
|
|
1716
|
+
fix: 'Review and fix any regressions detected since the last audit.',
|
|
1717
|
+
template: null,
|
|
1718
|
+
file: () => null,
|
|
1719
|
+
line: () => null,
|
|
1720
|
+
},
|
|
1721
|
+
|
|
1722
|
+
cursorRepeatFeedbackLoop: {
|
|
1723
|
+
id: 'CU-O03',
|
|
1724
|
+
name: 'Feedback loop active for recommendations',
|
|
1725
|
+
check: () => null, // Requires feedback data
|
|
1726
|
+
impact: 'low',
|
|
1727
|
+
rating: 2,
|
|
1728
|
+
category: 'freshness',
|
|
1729
|
+
fix: 'Use `npx nerviq --platform cursor feedback` to rate recommendations.',
|
|
1730
|
+
template: null,
|
|
1731
|
+
file: () => null,
|
|
1732
|
+
line: () => null,
|
|
1733
|
+
},
|
|
1734
|
+
|
|
1735
|
+
// =============================================
|
|
1736
|
+
// P. Freshness (3 checks) — CU-P01..CU-P03
|
|
1737
|
+
// =============================================
|
|
1738
|
+
|
|
1739
|
+
cursorFreshnessSourcesVerified: {
|
|
1740
|
+
id: 'CU-P01',
|
|
1741
|
+
name: 'P0 freshness sources verified within threshold',
|
|
1742
|
+
check: () => null, // Requires freshness verification data
|
|
1743
|
+
impact: 'medium',
|
|
1744
|
+
rating: 3,
|
|
1745
|
+
category: 'freshness',
|
|
1746
|
+
fix: 'Verify P0 Cursor documentation sources are current before claiming freshness.',
|
|
1747
|
+
template: null,
|
|
1748
|
+
file: () => null,
|
|
1749
|
+
line: () => null,
|
|
1750
|
+
},
|
|
1751
|
+
|
|
1752
|
+
cursorFreshnessPropagation: {
|
|
1753
|
+
id: 'CU-P02',
|
|
1754
|
+
name: 'Freshness propagation checklist is current',
|
|
1755
|
+
check: () => null, // Requires propagation tracking
|
|
1756
|
+
impact: 'low',
|
|
1757
|
+
rating: 2,
|
|
1758
|
+
category: 'freshness',
|
|
1759
|
+
fix: 'Review propagation checklist when Cursor releases new features or changes.',
|
|
1760
|
+
template: null,
|
|
1761
|
+
file: () => null,
|
|
1762
|
+
line: () => null,
|
|
1763
|
+
},
|
|
1764
|
+
|
|
1765
|
+
cursorFreshnessRuleFormat: {
|
|
1766
|
+
id: 'CU-P03',
|
|
1767
|
+
name: 'Rule format matches current Cursor version expectations',
|
|
1768
|
+
check: (ctx) => {
|
|
1769
|
+
const rules = ctx.cursorRules ? ctx.cursorRules() : [];
|
|
1770
|
+
if (rules.length === 0) return null;
|
|
1771
|
+
// All rules should use MDC format (not plain markdown)
|
|
1772
|
+
return rules.every(r => r.frontmatter !== null);
|
|
1773
|
+
},
|
|
1774
|
+
impact: 'medium',
|
|
1775
|
+
rating: 3,
|
|
1776
|
+
category: 'freshness',
|
|
1777
|
+
fix: 'Ensure all rules use current MDC format with YAML frontmatter.',
|
|
1778
|
+
template: null,
|
|
1779
|
+
file: () => '.cursor/rules/',
|
|
1780
|
+
line: () => null,
|
|
1781
|
+
},
|
|
1782
|
+
};
|
|
1783
|
+
|
|
1784
|
+
module.exports = {
|
|
1785
|
+
CURSOR_TECHNIQUES,
|
|
1786
|
+
};
|