@nerviq/cli 0.9.1 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -30
- package/package.json +1 -1
- package/src/aider/techniques.js +82 -11
- package/src/copilot/techniques.js +122 -11
- package/src/cursor/techniques.js +90 -10
- package/src/gemini/techniques.js +174 -23
- package/src/opencode/techniques.js +70 -99
- package/src/windsurf/techniques.js +211 -138
|
@@ -1,33 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Windsurf techniques module — CHECK CATALOG
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 84 checks across 16 categories:
|
|
5
5
|
* v0.1 (40): A. Rules(9), B. Config(7), C. Trust & Safety(9), D. Cascade Agent(5), E. MCP(5), F. Instructions Quality(5)
|
|
6
6
|
* v0.5 (55): G. Workflows & Steps(5), H. Memories(5), I. Enterprise(5)
|
|
7
7
|
* v1.0 (70): J. Cascadeignore & Review(4), K. Cross-Surface(4), L. Quality Deep(7)
|
|
8
|
-
* CP-08 (
|
|
8
|
+
* CP-08 (84): M. Advisory(4), N. Pack(4), O. Repeat(3), P. Freshness(3)
|
|
9
9
|
*
|
|
10
10
|
* Each check: { id, name, check(ctx), impact, rating, category, fix, template, file(), line() }
|
|
11
11
|
*
|
|
12
12
|
* Windsurf key differences from Cursor:
|
|
13
13
|
* - Instructions: .windsurf/rules/*.md (Markdown + YAML frontmatter, NOT MDC)
|
|
14
14
|
* - Legacy: .windsurfrules (like .cursorrules)
|
|
15
|
-
* - 4 activation modes:
|
|
15
|
+
* - 4 activation modes: always_on, glob, model_decision, manual
|
|
16
16
|
* - Agent: Cascade (autonomous agent)
|
|
17
|
-
* - Memories system (
|
|
17
|
+
* - Memories system (workspace-scoped, local to the current workspace)
|
|
18
18
|
* - Workflows -> Slash commands
|
|
19
|
-
* -
|
|
19
|
+
* - 12K char limit for modern rules/workflows; 6K for legacy/global rules
|
|
20
20
|
* - MCP with team whitelist
|
|
21
21
|
* - cascadeignore (gitignore for Cascade)
|
|
22
|
-
* - No background agents
|
|
22
|
+
* - No background agents or supported CLI/headless mode
|
|
23
23
|
* - Check ID prefix: WS-
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
+
const fs = require('fs');
|
|
26
27
|
const os = require('os');
|
|
27
28
|
const path = require('path');
|
|
28
29
|
const { WindsurfProjectContext } = require('./context');
|
|
29
30
|
const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
|
|
30
|
-
const {
|
|
31
|
+
const { tryParseJson, validateMcpEnvVars } = require('./config-parser');
|
|
31
32
|
|
|
32
33
|
// ─── Shared helpers ─────────────────────────────────────────────────────────
|
|
33
34
|
|
|
@@ -87,12 +88,71 @@ function coreRulesContent(ctx) {
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
function mcpJsonRaw(ctx) {
|
|
90
|
-
|
|
91
|
+
const configPath = windsurfMcpConfigPath();
|
|
92
|
+
try {
|
|
93
|
+
return fs.readFileSync(configPath, 'utf8');
|
|
94
|
+
} catch {
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
function mcpJsonData(ctx) {
|
|
94
|
-
const
|
|
95
|
-
|
|
100
|
+
const raw = mcpJsonRaw(ctx);
|
|
101
|
+
if (!raw) return null;
|
|
102
|
+
const result = tryParseJson(raw);
|
|
103
|
+
return result.ok ? result.data : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function windsurfMcpConfigPath() {
|
|
107
|
+
return path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function normalizeWindsurfTrigger(frontmatter) {
|
|
111
|
+
if (!frontmatter) return 'always_on';
|
|
112
|
+
|
|
113
|
+
const trigger = String(frontmatter.trigger || '').trim().toLowerCase();
|
|
114
|
+
if (trigger === 'always_on' || trigger === 'always') return 'always_on';
|
|
115
|
+
if (trigger === 'glob' || trigger === 'auto' || trigger === 'auto_attached') return 'glob';
|
|
116
|
+
if (trigger === 'model_decision' || trigger === 'agent_requested' || trigger === 'agent-requested') return 'model_decision';
|
|
117
|
+
if (trigger === 'manual') return 'manual';
|
|
118
|
+
|
|
119
|
+
const hasGlob = Boolean(frontmatter.glob) ||
|
|
120
|
+
(Array.isArray(frontmatter.globs) ? frontmatter.globs.length > 0 : Boolean(frontmatter.globs));
|
|
121
|
+
const hasDescription = Boolean(frontmatter.description && String(frontmatter.description).trim());
|
|
122
|
+
|
|
123
|
+
if (hasGlob) return 'glob';
|
|
124
|
+
if (hasDescription) return 'model_decision';
|
|
125
|
+
return 'always_on';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isValidWindsurfFrontmatter(frontmatter) {
|
|
129
|
+
if (!frontmatter || typeof frontmatter !== 'object') return false;
|
|
130
|
+
|
|
131
|
+
const validFields = new Set(['trigger', 'description', 'glob', 'globs', 'name']);
|
|
132
|
+
const validTriggers = new Set([
|
|
133
|
+
'always_on', 'always',
|
|
134
|
+
'glob', 'auto', 'auto_attached',
|
|
135
|
+
'model_decision', 'agent_requested', 'agent-requested',
|
|
136
|
+
'manual',
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
for (const key of Object.keys(frontmatter)) {
|
|
140
|
+
if (!validFields.has(key)) return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (frontmatter.trigger && !validTriggers.has(String(frontmatter.trigger).trim().toLowerCase())) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (frontmatter.globs !== undefined && !Array.isArray(frontmatter.globs) && typeof frontmatter.globs !== 'string') {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (frontmatter.glob !== undefined && typeof frontmatter.glob !== 'string') {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return true;
|
|
96
156
|
}
|
|
97
157
|
|
|
98
158
|
function docsBundle(ctx) {
|
|
@@ -154,11 +214,33 @@ function workflowContents(ctx) {
|
|
|
154
214
|
return files.map(f => ctx.fileContent(f) || '').join('\n');
|
|
155
215
|
}
|
|
156
216
|
|
|
217
|
+
function ciWorkflowContents(ctx) {
|
|
218
|
+
const files = ctx.ciWorkflowFiles ? ctx.ciWorkflowFiles() : [];
|
|
219
|
+
return files.map(f => ctx.fileContent(f) || '').join('\n');
|
|
220
|
+
}
|
|
221
|
+
|
|
157
222
|
function wordCount(text) {
|
|
158
223
|
if (!text) return 0;
|
|
159
224
|
return text.split(/\s+/).filter(Boolean).length;
|
|
160
225
|
}
|
|
161
226
|
|
|
227
|
+
function repoFilesOverLineThreshold(ctx, threshold = 300) {
|
|
228
|
+
const oversized = [];
|
|
229
|
+
|
|
230
|
+
for (const filePath of ctx.files || []) {
|
|
231
|
+
if (/^(node_modules|dist|build|coverage|\.git|\.next|vendor|out)\//i.test(filePath)) continue;
|
|
232
|
+
if (/\.(png|jpg|jpeg|gif|webp|ico|pdf|zip|gz|lock|woff2?)$/i.test(filePath)) continue;
|
|
233
|
+
|
|
234
|
+
const content = ctx.fileContent(filePath);
|
|
235
|
+
if (!content) continue;
|
|
236
|
+
|
|
237
|
+
const lineCount = content.split(/\r?\n/).length;
|
|
238
|
+
if (lineCount > threshold) oversized.push({ filePath, lineCount });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return oversized.sort((a, b) => b.lineCount - a.lineCount);
|
|
242
|
+
}
|
|
243
|
+
|
|
162
244
|
// ─── WINDSURF_TECHNIQUES ──────────────────────────────────────────────────────
|
|
163
245
|
|
|
164
246
|
const WINDSURF_TECHNIQUES = {
|
|
@@ -201,16 +283,16 @@ const WINDSURF_TECHNIQUES = {
|
|
|
201
283
|
|
|
202
284
|
windsurfAlwaysRuleExists: {
|
|
203
285
|
id: 'WS-A03',
|
|
204
|
-
name: 'At least one rule
|
|
286
|
+
name: 'At least one rule is always_on for Cascade',
|
|
205
287
|
check: (ctx) => {
|
|
206
288
|
const rules = ctx.windsurfRules ? ctx.windsurfRules() : [];
|
|
207
289
|
if (rules.length === 0) return null;
|
|
208
|
-
return rules.some(
|
|
290
|
+
return rules.some((rule) => normalizeWindsurfTrigger(rule.frontmatter) === 'always_on');
|
|
209
291
|
},
|
|
210
292
|
impact: 'high',
|
|
211
293
|
rating: 5,
|
|
212
294
|
category: 'rules',
|
|
213
|
-
fix: 'Add trigger:
|
|
295
|
+
fix: 'Add a focused `trigger: always_on` rule for core guidance. Files without frontmatter also default to always_on, so make that choice explicit.',
|
|
214
296
|
template: 'windsurf-rules',
|
|
215
297
|
file: () => '.windsurf/rules/',
|
|
216
298
|
line: () => null,
|
|
@@ -222,16 +304,12 @@ const WINDSURF_TECHNIQUES = {
|
|
|
222
304
|
check: (ctx) => {
|
|
223
305
|
const rules = ctx.windsurfRules ? ctx.windsurfRules() : [];
|
|
224
306
|
if (rules.length === 0) return null;
|
|
225
|
-
return rules.every(
|
|
226
|
-
if (!r.frontmatter) return false;
|
|
227
|
-
const validation = validateWindsurfFrontmatter(r.frontmatter);
|
|
228
|
-
return validation.valid;
|
|
229
|
-
});
|
|
307
|
+
return rules.every((rule) => isValidWindsurfFrontmatter(rule.frontmatter));
|
|
230
308
|
},
|
|
231
309
|
impact: 'high',
|
|
232
310
|
rating: 4,
|
|
233
311
|
category: 'rules',
|
|
234
|
-
fix: 'Fix YAML frontmatter in rule
|
|
312
|
+
fix: 'Fix YAML frontmatter in rule files. Use current Windsurf triggers (`always_on`, `glob`, `model_decision`, `manual`) plus `glob`/`globs`, `description`, and `name` as needed.',
|
|
235
313
|
template: null,
|
|
236
314
|
file: () => '.windsurf/rules/',
|
|
237
315
|
line: () => 1,
|
|
@@ -239,16 +317,16 @@ const WINDSURF_TECHNIQUES = {
|
|
|
239
317
|
|
|
240
318
|
windsurfRulesUnder10kChars: {
|
|
241
319
|
id: 'WS-A05',
|
|
242
|
-
name: '
|
|
320
|
+
name: 'Modern rule files stay under the 12K character limit',
|
|
243
321
|
check: (ctx) => {
|
|
244
322
|
const rules = ctx.windsurfRules ? ctx.windsurfRules() : [];
|
|
245
323
|
if (rules.length === 0) return null;
|
|
246
|
-
return rules.every(
|
|
324
|
+
return rules.every((rule) => (rule.charCount || 0) <= 12000);
|
|
247
325
|
},
|
|
248
326
|
impact: 'high',
|
|
249
327
|
rating: 4,
|
|
250
328
|
category: 'rules',
|
|
251
|
-
fix: 'Split
|
|
329
|
+
fix: 'Split rule files before they approach 12,000 characters. Windsurf silently truncates modern rules after 12K with no warning.',
|
|
252
330
|
template: null,
|
|
253
331
|
file: () => '.windsurf/rules/',
|
|
254
332
|
line: () => null,
|
|
@@ -313,11 +391,12 @@ const WINDSURF_TECHNIQUES = {
|
|
|
313
391
|
|
|
314
392
|
windsurfAgentRequestedDescriptions: {
|
|
315
393
|
id: 'WS-A09',
|
|
316
|
-
name: '
|
|
394
|
+
name: 'model_decision rules have precise descriptions',
|
|
317
395
|
check: (ctx) => {
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
396
|
+
const rules = ctx.windsurfRules ? ctx.windsurfRules() : [];
|
|
397
|
+
const modelDecisionRules = rules.filter((rule) => normalizeWindsurfTrigger(rule.frontmatter) === 'model_decision');
|
|
398
|
+
if (modelDecisionRules.length === 0) return null;
|
|
399
|
+
return modelDecisionRules.every((r) => {
|
|
321
400
|
const desc = r.frontmatter && r.frontmatter.description;
|
|
322
401
|
return desc && String(desc).trim().length >= 15;
|
|
323
402
|
});
|
|
@@ -325,7 +404,7 @@ const WINDSURF_TECHNIQUES = {
|
|
|
325
404
|
impact: 'medium',
|
|
326
405
|
rating: 3,
|
|
327
406
|
category: 'rules',
|
|
328
|
-
fix: 'Add clear, specific descriptions (15+ chars) to
|
|
407
|
+
fix: 'Add clear, specific descriptions (15+ chars) to `trigger: model_decision` rules so Windsurf can judge when to load the full rule body.',
|
|
329
408
|
template: null,
|
|
330
409
|
file: () => '.windsurf/rules/',
|
|
331
410
|
line: () => null,
|
|
@@ -337,20 +416,20 @@ const WINDSURF_TECHNIQUES = {
|
|
|
337
416
|
|
|
338
417
|
windsurfMcpJsonExists: {
|
|
339
418
|
id: 'WS-B01',
|
|
340
|
-
name: '
|
|
419
|
+
name: 'Global Windsurf MCP config exists when MCP is used',
|
|
341
420
|
check: (ctx) => {
|
|
342
|
-
const
|
|
343
|
-
if (
|
|
344
|
-
const
|
|
345
|
-
if (
|
|
421
|
+
const raw = mcpJsonRaw(ctx);
|
|
422
|
+
if (raw) return true;
|
|
423
|
+
const docs = docsBundle(ctx);
|
|
424
|
+
if (!/\bmcp\b/i.test(docs)) return null;
|
|
346
425
|
return false;
|
|
347
426
|
},
|
|
348
427
|
impact: 'high',
|
|
349
428
|
rating: 4,
|
|
350
429
|
category: 'config',
|
|
351
|
-
fix: 'Create .windsurf/mcp.json
|
|
430
|
+
fix: 'Create `%USERPROFILE%/.codeium/windsurf/mcp_config.json` for MCP. Windsurf MCP is global-only in current runtime; project `.windsurf/mcp.json` is not the validated surface.',
|
|
352
431
|
template: 'windsurf-mcp',
|
|
353
|
-
file: () =>
|
|
432
|
+
file: () => windsurfMcpConfigPath(),
|
|
354
433
|
line: () => null,
|
|
355
434
|
},
|
|
356
435
|
|
|
@@ -371,7 +450,7 @@ const WINDSURF_TECHNIQUES = {
|
|
|
371
450
|
category: 'config',
|
|
372
451
|
fix: 'Document MCP server team whitelist. Windsurf supports team-level MCP whitelisting.',
|
|
373
452
|
template: null,
|
|
374
|
-
file: () =>
|
|
453
|
+
file: () => windsurfMcpConfigPath(),
|
|
375
454
|
line: () => null,
|
|
376
455
|
},
|
|
377
456
|
|
|
@@ -421,7 +500,7 @@ const WINDSURF_TECHNIQUES = {
|
|
|
421
500
|
impact: 'medium',
|
|
422
501
|
rating: 3,
|
|
423
502
|
category: 'config',
|
|
424
|
-
fix: 'Create
|
|
503
|
+
fix: 'Create `.windsurf/memories/` only for workspace-local persistent context. Do not rely on memories for cross-project or team-shared behavior.',
|
|
425
504
|
template: 'windsurf-memories',
|
|
426
505
|
file: () => '.windsurf/memories/',
|
|
427
506
|
line: () => null,
|
|
@@ -433,17 +512,17 @@ const WINDSURF_TECHNIQUES = {
|
|
|
433
512
|
check: (ctx) => {
|
|
434
513
|
const raw = mcpJsonRaw(ctx);
|
|
435
514
|
if (!raw) return null;
|
|
436
|
-
const result =
|
|
515
|
+
const result = tryParseJson(raw);
|
|
437
516
|
return result && result.ok;
|
|
438
517
|
},
|
|
439
518
|
impact: 'critical',
|
|
440
519
|
rating: 5,
|
|
441
520
|
category: 'config',
|
|
442
|
-
fix: 'Fix malformed JSON in
|
|
521
|
+
fix: 'Fix malformed JSON in `%USERPROFILE%/.codeium/windsurf/mcp_config.json`.',
|
|
443
522
|
template: null,
|
|
444
|
-
file: () =>
|
|
523
|
+
file: () => windsurfMcpConfigPath(),
|
|
445
524
|
line: (ctx) => {
|
|
446
|
-
const result = ctx
|
|
525
|
+
const result = tryParseJson(mcpJsonRaw(ctx));
|
|
447
526
|
if (result && result.ok) return null;
|
|
448
527
|
if (result && result.error) {
|
|
449
528
|
const match = result.error.match(/position (\d+)/i);
|
|
@@ -482,19 +561,20 @@ const WINDSURF_TECHNIQUES = {
|
|
|
482
561
|
|
|
483
562
|
windsurfCascadeignoreSensitive: {
|
|
484
563
|
id: 'WS-C01',
|
|
485
|
-
name: '
|
|
564
|
+
name: 'ZDR guidance does not overclaim local-only processing',
|
|
486
565
|
check: (ctx) => {
|
|
487
|
-
const
|
|
488
|
-
if (
|
|
489
|
-
|
|
490
|
-
|
|
566
|
+
const docs = docsBundle(ctx);
|
|
567
|
+
if (!/\bzdr\b|zero.?data.?retention|privacy|retention/i.test(docs)) return null;
|
|
568
|
+
const claimsNoSend = /\b(no|never|without)\b.{0,40}\b(send|transmit|leave)\b.{0,40}\b(code|data)\b/i.test(docs);
|
|
569
|
+
const explainsTransmission = /\btransmi|sent to windsurf|server.?side processing|retention\b/i.test(docs);
|
|
570
|
+
return !claimsNoSend && explainsTransmission;
|
|
491
571
|
},
|
|
492
572
|
impact: 'high',
|
|
493
573
|
rating: 5,
|
|
494
574
|
category: 'trust',
|
|
495
|
-
fix: '
|
|
575
|
+
fix: 'Document ZDR accurately: it affects retention/training, not whether code is sent to Windsurf for processing. Keep `.cascadeignore` for local exclusion, but do not describe ZDR as a no-transmission mode.',
|
|
496
576
|
template: null,
|
|
497
|
-
file: () => '.
|
|
577
|
+
file: () => '.windsurf/rules/',
|
|
498
578
|
line: () => null,
|
|
499
579
|
},
|
|
500
580
|
|
|
@@ -533,7 +613,7 @@ const WINDSURF_TECHNIQUES = {
|
|
|
533
613
|
category: 'trust',
|
|
534
614
|
fix: 'Verify MCP servers are from trusted sources. Check for known MCP CVEs.',
|
|
535
615
|
template: null,
|
|
536
|
-
file: () =>
|
|
616
|
+
file: () => windsurfMcpConfigPath(),
|
|
537
617
|
line: () => null,
|
|
538
618
|
},
|
|
539
619
|
|
|
@@ -553,7 +633,7 @@ const WINDSURF_TECHNIQUES = {
|
|
|
553
633
|
category: 'trust',
|
|
554
634
|
fix: 'Use ${env:VAR_NAME} syntax for MCP environment variables instead of hardcoded values.',
|
|
555
635
|
template: null,
|
|
556
|
-
file: () =>
|
|
636
|
+
file: () => windsurfMcpConfigPath(),
|
|
557
637
|
line: () => null,
|
|
558
638
|
},
|
|
559
639
|
|
|
@@ -577,7 +657,7 @@ const WINDSURF_TECHNIQUES = {
|
|
|
577
657
|
|
|
578
658
|
windsurfMemoriesNoSecrets: {
|
|
579
659
|
id: 'WS-C06',
|
|
580
|
-
name: 'No secrets in memory files
|
|
660
|
+
name: 'No secrets in workspace-local memory files',
|
|
581
661
|
check: (ctx) => {
|
|
582
662
|
const content = memoryContents(ctx);
|
|
583
663
|
if (!content.trim()) return null;
|
|
@@ -586,7 +666,7 @@ const WINDSURF_TECHNIQUES = {
|
|
|
586
666
|
impact: 'critical',
|
|
587
667
|
rating: 5,
|
|
588
668
|
category: 'trust',
|
|
589
|
-
fix: 'Remove secrets from
|
|
669
|
+
fix: 'Remove secrets from `.windsurf/memories/`. Memories persist inside the current workspace and can resurface in later sessions even though they are not cross-project or team-shared.',
|
|
590
670
|
template: null,
|
|
591
671
|
file: () => '.windsurf/memories/',
|
|
592
672
|
line: () => null,
|
|
@@ -613,16 +693,18 @@ const WINDSURF_TECHNIQUES = {
|
|
|
613
693
|
|
|
614
694
|
windsurfTeamSyncAware: {
|
|
615
695
|
id: 'WS-C08',
|
|
616
|
-
name: '
|
|
696
|
+
name: 'Memory scope limitations are documented accurately',
|
|
617
697
|
check: (ctx) => {
|
|
618
698
|
const docs = docsBundle(ctx);
|
|
619
|
-
if (
|
|
620
|
-
|
|
699
|
+
if (!/\bmemories?\b/i.test(docs)) return null;
|
|
700
|
+
const mentionsWorkspaceScope = /\bworkspace\b|\blocal only\b|\bnot cross-project\b|\bcurrent repo\b/i.test(docs);
|
|
701
|
+
const overclaimsSharing = /\bteam.?sync\b|\bshared across projects\b|\bcross-project memory\b/i.test(docs);
|
|
702
|
+
return mentionsWorkspaceScope && !overclaimsSharing;
|
|
621
703
|
},
|
|
622
704
|
impact: 'medium',
|
|
623
705
|
rating: 4,
|
|
624
706
|
category: 'trust',
|
|
625
|
-
fix: 'Document
|
|
707
|
+
fix: 'Document memories as workspace-scoped and local to the current workspace. Do not rely on cross-project recall or team sync when writing guidance.',
|
|
626
708
|
template: null,
|
|
627
709
|
file: () => '.windsurf/rules/',
|
|
628
710
|
line: () => null,
|
|
@@ -742,36 +824,35 @@ const WINDSURF_TECHNIQUES = {
|
|
|
742
824
|
|
|
743
825
|
windsurfMcpPerSurface: {
|
|
744
826
|
id: 'WS-E01',
|
|
745
|
-
name: 'MCP
|
|
827
|
+
name: 'MCP uses the validated global Windsurf config surface',
|
|
746
828
|
check: (ctx) => {
|
|
747
|
-
const
|
|
748
|
-
if (!
|
|
749
|
-
return
|
|
829
|
+
const raw = mcpJsonRaw(ctx);
|
|
830
|
+
if (!raw) return null;
|
|
831
|
+
return !Boolean(ctx.fileContent('.windsurf/mcp.json'));
|
|
750
832
|
},
|
|
751
833
|
impact: 'medium',
|
|
752
834
|
rating: 3,
|
|
753
835
|
category: 'mcp',
|
|
754
|
-
fix: '
|
|
836
|
+
fix: 'Use `%USERPROFILE%/.codeium/windsurf/mcp_config.json` as the current MCP surface. Do not rely on project `.windsurf/mcp.json` or the old `~/.windsurf/mcp.json` path.',
|
|
755
837
|
template: 'windsurf-mcp',
|
|
756
|
-
file: () =>
|
|
838
|
+
file: () => windsurfMcpConfigPath(),
|
|
757
839
|
line: () => null,
|
|
758
840
|
},
|
|
759
841
|
|
|
760
842
|
windsurfMcpProjectOverride: {
|
|
761
843
|
id: 'WS-E02',
|
|
762
|
-
name: '
|
|
844
|
+
name: 'Windsurf MCP guidance does not assume project-level overrides',
|
|
763
845
|
check: (ctx) => {
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
return true;
|
|
846
|
+
const docs = docsBundle(ctx);
|
|
847
|
+
if (!/\bmcp\b/i.test(docs)) return null;
|
|
848
|
+
return !/\.windsurf\/mcp\.json|~\/\.windsurf\/mcp\.json|project-level mcp/i.test(docs);
|
|
768
849
|
},
|
|
769
850
|
impact: 'medium',
|
|
770
851
|
rating: 3,
|
|
771
852
|
category: 'mcp',
|
|
772
|
-
fix: '
|
|
853
|
+
fix: 'Remove docs that describe project-level MCP override behavior. Current Windsurf MCP runtime is validated only through the global `%USERPROFILE%/.codeium/windsurf/mcp_config.json` file.',
|
|
773
854
|
template: null,
|
|
774
|
-
file: () => '.windsurf/
|
|
855
|
+
file: () => '.windsurf/rules/',
|
|
775
856
|
line: () => null,
|
|
776
857
|
},
|
|
777
858
|
|
|
@@ -791,7 +872,7 @@ const WINDSURF_TECHNIQUES = {
|
|
|
791
872
|
category: 'mcp',
|
|
792
873
|
fix: 'Use ${env:VAR_NAME} syntax for MCP environment variables instead of hardcoded values.',
|
|
793
874
|
template: null,
|
|
794
|
-
file: () =>
|
|
875
|
+
file: () => windsurfMcpConfigPath(),
|
|
795
876
|
line: () => null,
|
|
796
877
|
},
|
|
797
878
|
|
|
@@ -809,7 +890,7 @@ const WINDSURF_TECHNIQUES = {
|
|
|
809
890
|
category: 'mcp',
|
|
810
891
|
fix: 'Use @latest for MCP packages or regularly update pinned versions.',
|
|
811
892
|
template: null,
|
|
812
|
-
file: () =>
|
|
893
|
+
file: () => windsurfMcpConfigPath(),
|
|
813
894
|
line: () => null,
|
|
814
895
|
},
|
|
815
896
|
|
|
@@ -828,7 +909,7 @@ const WINDSURF_TECHNIQUES = {
|
|
|
828
909
|
category: 'mcp',
|
|
829
910
|
fix: 'Enable MCP team whitelist for controlled environments.',
|
|
830
911
|
template: null,
|
|
831
|
-
file: () =>
|
|
912
|
+
file: () => windsurfMcpConfigPath(),
|
|
832
913
|
line: () => null,
|
|
833
914
|
},
|
|
834
915
|
|
|
@@ -1051,7 +1132,7 @@ const WINDSURF_TECHNIQUES = {
|
|
|
1051
1132
|
|
|
1052
1133
|
windsurfMemoriesTeamSafe: {
|
|
1053
1134
|
id: 'WS-H02',
|
|
1054
|
-
name: 'Memories are safe for
|
|
1135
|
+
name: 'Memories are safe for workspace-local persistence (no personal data)',
|
|
1055
1136
|
check: (ctx) => {
|
|
1056
1137
|
const content = memoryContents(ctx);
|
|
1057
1138
|
if (!content.trim()) return null;
|
|
@@ -1061,7 +1142,7 @@ const WINDSURF_TECHNIQUES = {
|
|
|
1061
1142
|
impact: 'high',
|
|
1062
1143
|
rating: 5,
|
|
1063
1144
|
category: 'memories',
|
|
1064
|
-
fix: 'Remove personal data and secrets from memories.
|
|
1145
|
+
fix: 'Remove personal data and secrets from memories. They are workspace-local, but they still persist and can influence future sessions in this repo.',
|
|
1065
1146
|
template: null,
|
|
1066
1147
|
file: () => '.windsurf/memories/',
|
|
1067
1148
|
line: () => null,
|
|
@@ -1148,16 +1229,16 @@ const WINDSURF_TECHNIQUES = {
|
|
|
1148
1229
|
|
|
1149
1230
|
windsurfEnterpriseTeamSync: {
|
|
1150
1231
|
id: 'WS-I02',
|
|
1151
|
-
name: '
|
|
1232
|
+
name: 'Enterprise deployment model is documented',
|
|
1152
1233
|
check: (ctx) => {
|
|
1153
1234
|
const docs = docsBundle(ctx);
|
|
1154
1235
|
if (!/enterprise/i.test(docs)) return null;
|
|
1155
|
-
return /
|
|
1236
|
+
return /self-host|self host|hybrid|cloud deployment|on-prem|on prem/i.test(docs);
|
|
1156
1237
|
},
|
|
1157
1238
|
impact: 'medium',
|
|
1158
1239
|
rating: 3,
|
|
1159
1240
|
category: 'enterprise',
|
|
1160
|
-
fix: '
|
|
1241
|
+
fix: 'Document the actual Enterprise deployment posture (cloud, hybrid, or self-hosted). Do not describe memories as team-synced when runtime evidence shows they are workspace-scoped.',
|
|
1161
1242
|
template: null,
|
|
1162
1243
|
file: () => '.windsurf/rules/',
|
|
1163
1244
|
line: () => null,
|
|
@@ -1182,16 +1263,20 @@ const WINDSURF_TECHNIQUES = {
|
|
|
1182
1263
|
|
|
1183
1264
|
windsurfEnterpriseSecurityPolicy: {
|
|
1184
1265
|
id: 'WS-I04',
|
|
1185
|
-
name: 'Security policy
|
|
1266
|
+
name: 'Security policy documents retention and transmission accurately',
|
|
1186
1267
|
check: (ctx) => {
|
|
1187
1268
|
const docs = docsBundle(ctx);
|
|
1188
1269
|
if (!/enterprise/i.test(docs)) return null;
|
|
1189
|
-
|
|
1270
|
+
const hasPolicy = /security.*policy|data.*retention|compliance|privacy/i.test(docs);
|
|
1271
|
+
const mentionsZdr = /\bzdr\b|zero.?data.?retention/i.test(docs);
|
|
1272
|
+
if (!hasPolicy) return false;
|
|
1273
|
+
if (!mentionsZdr) return true;
|
|
1274
|
+
return /\btransmi|sent to windsurf|processing\b/i.test(docs);
|
|
1190
1275
|
},
|
|
1191
1276
|
impact: 'high',
|
|
1192
1277
|
rating: 4,
|
|
1193
1278
|
category: 'enterprise',
|
|
1194
|
-
fix: 'Document security and data retention
|
|
1279
|
+
fix: 'Document security and data handling accurately. If you mention ZDR, also state that it controls retention/training posture, not whether code is transmitted for processing.',
|
|
1195
1280
|
template: null,
|
|
1196
1281
|
file: () => '.windsurf/rules/',
|
|
1197
1282
|
line: () => null,
|
|
@@ -1318,18 +1403,18 @@ const WINDSURF_TECHNIQUES = {
|
|
|
1318
1403
|
|
|
1319
1404
|
windsurfMcpConsistentSurfaces: {
|
|
1320
1405
|
id: 'WS-K02',
|
|
1321
|
-
name: 'MCP
|
|
1406
|
+
name: 'MCP guidance matches the current global-only config surface',
|
|
1322
1407
|
check: (ctx) => {
|
|
1323
|
-
const
|
|
1324
|
-
if (!
|
|
1325
|
-
return
|
|
1408
|
+
const raw = mcpJsonRaw(ctx);
|
|
1409
|
+
if (!raw) return null;
|
|
1410
|
+
return !Boolean(ctx.fileContent('.windsurf/mcp.json'));
|
|
1326
1411
|
},
|
|
1327
1412
|
impact: 'medium',
|
|
1328
1413
|
rating: 3,
|
|
1329
1414
|
category: 'cross-surface',
|
|
1330
|
-
fix: 'Document
|
|
1415
|
+
fix: 'Document Windsurf MCP as global-only in current runtime. Remove stale references to project `.windsurf/mcp.json` overrides.',
|
|
1331
1416
|
template: null,
|
|
1332
|
-
file: () =>
|
|
1417
|
+
file: () => windsurfMcpConfigPath(),
|
|
1333
1418
|
line: () => null,
|
|
1334
1419
|
},
|
|
1335
1420
|
|
|
@@ -1448,16 +1533,16 @@ const WINDSURF_TECHNIQUES = {
|
|
|
1448
1533
|
|
|
1449
1534
|
windsurfRuleCharLimitAware: {
|
|
1450
1535
|
id: 'WS-L05',
|
|
1451
|
-
name: 'All rules within
|
|
1536
|
+
name: 'All modern rules stay within the 12K char limit',
|
|
1452
1537
|
check: (ctx) => {
|
|
1453
1538
|
const rules = ctx.windsurfRules ? ctx.windsurfRules() : [];
|
|
1454
1539
|
if (rules.length === 0) return null;
|
|
1455
|
-
return rules.every(
|
|
1540
|
+
return rules.every((rule) => (rule.charCount || 0) <= 12000);
|
|
1456
1541
|
},
|
|
1457
1542
|
impact: 'high',
|
|
1458
1543
|
rating: 4,
|
|
1459
1544
|
category: 'quality-deep',
|
|
1460
|
-
fix: '
|
|
1545
|
+
fix: 'Keep modern rule files under 12,000 characters. Windsurf silently truncates content beyond 12K, and legacy/global rule surfaces still have a stricter 6K ceiling.',
|
|
1461
1546
|
template: null,
|
|
1462
1547
|
file: () => '.windsurf/rules/',
|
|
1463
1548
|
line: () => null,
|
|
@@ -1503,51 +1588,36 @@ const WINDSURF_TECHNIQUES = {
|
|
|
1503
1588
|
|
|
1504
1589
|
windsurfAdvisoryInstructionQuality: {
|
|
1505
1590
|
id: 'WS-M01',
|
|
1506
|
-
name: '
|
|
1591
|
+
name: 'No high-risk large files beyond Windsurf’s ~300-line accuracy threshold',
|
|
1507
1592
|
check: (ctx) => {
|
|
1508
|
-
|
|
1509
|
-
if (!content.trim()) return null;
|
|
1510
|
-
const lines = content.split(/\r?\n/).filter(l => l.trim()).length;
|
|
1511
|
-
const sections = countSections(content);
|
|
1512
|
-
const hasArch = hasArchitecture(content);
|
|
1513
|
-
const hasVerify = /\bverif|\btest|\blint|\bbuild/i.test(content);
|
|
1514
|
-
const score = (lines >= 30 ? 2 : lines >= 15 ? 1 : 0) +
|
|
1515
|
-
(sections >= 4 ? 2 : sections >= 2 ? 1 : 0) +
|
|
1516
|
-
(hasArch ? 1 : 0) +
|
|
1517
|
-
(hasVerify ? 1 : 0);
|
|
1518
|
-
return score >= 4;
|
|
1593
|
+
return repoFilesOverLineThreshold(ctx, 300).length === 0;
|
|
1519
1594
|
},
|
|
1520
|
-
impact: '
|
|
1595
|
+
impact: 'high',
|
|
1521
1596
|
rating: 4,
|
|
1522
1597
|
category: 'advisory',
|
|
1523
|
-
fix: '
|
|
1524
|
-
template:
|
|
1525
|
-
file: () =>
|
|
1598
|
+
fix: 'Break files down before they exceed roughly 300 lines. Windsurf accuracy degrades in the 300-500 line range and becomes noticeably unreliable on 500+ line edits.',
|
|
1599
|
+
template: null,
|
|
1600
|
+
file: (ctx) => {
|
|
1601
|
+
const oversized = repoFilesOverLineThreshold(ctx, 300);
|
|
1602
|
+
return oversized[0] ? oversized[0].filePath : null;
|
|
1603
|
+
},
|
|
1526
1604
|
line: () => null,
|
|
1527
1605
|
},
|
|
1528
1606
|
|
|
1529
1607
|
windsurfAdvisorySecurityPosture: {
|
|
1530
1608
|
id: 'WS-M02',
|
|
1531
|
-
name: '
|
|
1609
|
+
name: 'Long-running Cascade tasks are scoped to mitigate stall risk',
|
|
1532
1610
|
check: (ctx) => {
|
|
1533
|
-
let score = 0;
|
|
1534
1611
|
const docs = docsBundle(ctx);
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
const validation = validateMcpEnvVars(mcpResult.data);
|
|
1540
|
-
if (validation.valid) score++;
|
|
1541
|
-
} else {
|
|
1542
|
-
score++;
|
|
1543
|
-
}
|
|
1544
|
-
if (/security|secret|credential/i.test(docs)) score++;
|
|
1545
|
-
return score >= 2;
|
|
1612
|
+
const hasLargeFiles = repoFilesOverLineThreshold(ctx, 300).length > 0;
|
|
1613
|
+
const hasAutomation = (ctx.workflowFiles ? ctx.workflowFiles() : []).length > 0;
|
|
1614
|
+
if (!hasLargeFiles && !hasAutomation) return null;
|
|
1615
|
+
return /small.*task|chunk|restart.*session|new session|retry|focused task/i.test(docs);
|
|
1546
1616
|
},
|
|
1547
|
-
impact: '
|
|
1548
|
-
rating:
|
|
1617
|
+
impact: 'medium',
|
|
1618
|
+
rating: 4,
|
|
1549
1619
|
category: 'advisory',
|
|
1550
|
-
fix: '
|
|
1620
|
+
fix: 'Document that long autonomous tasks can stall without auto-recovery. Prefer smaller, restartable chunks and tell users when to start a new session.',
|
|
1551
1621
|
template: null,
|
|
1552
1622
|
file: () => '.windsurf/rules/',
|
|
1553
1623
|
line: () => null,
|
|
@@ -1555,36 +1625,38 @@ const WINDSURF_TECHNIQUES = {
|
|
|
1555
1625
|
|
|
1556
1626
|
windsurfAdvisorySurfaceCoverage: {
|
|
1557
1627
|
id: 'WS-M03',
|
|
1558
|
-
name: '
|
|
1628
|
+
name: 'Repo does not assume an unsupported Windsurf CLI/headless surface',
|
|
1559
1629
|
check: (ctx) => {
|
|
1560
|
-
const
|
|
1561
|
-
|
|
1630
|
+
const combined = `${docsBundle(ctx)}\n${ciWorkflowContents(ctx)}`;
|
|
1631
|
+
if (!combined.trim()) return null;
|
|
1632
|
+
const unsupportedAutomation = /\bwindsurf\b[\s\S]{0,80}\b(cli|headless|ci|pipeline|github action|automation|run)\b/i.test(combined) ||
|
|
1633
|
+
/\bcascade\b[\s\S]{0,80}\b(headless|ci|pipeline|automation)\b/i.test(combined);
|
|
1634
|
+
return !unsupportedAutomation;
|
|
1562
1635
|
},
|
|
1563
|
-
impact: '
|
|
1636
|
+
impact: 'high',
|
|
1564
1637
|
rating: 4,
|
|
1565
1638
|
category: 'advisory',
|
|
1566
|
-
fix: '
|
|
1567
|
-
template:
|
|
1568
|
-
file: () => '.
|
|
1639
|
+
fix: 'Do not rely on Windsurf for CI/CD or headless automation. Windsurf currently has no supported CLI/headless mode, so automation lanes should use another platform.',
|
|
1640
|
+
template: null,
|
|
1641
|
+
file: () => '.github/workflows/',
|
|
1569
1642
|
line: () => null,
|
|
1570
1643
|
},
|
|
1571
1644
|
|
|
1572
1645
|
windsurfAdvisoryMcpHealth: {
|
|
1573
1646
|
id: 'WS-M04',
|
|
1574
|
-
name: '
|
|
1647
|
+
name: 'Windows/WSL usage includes a Windsurf stability caveat',
|
|
1575
1648
|
check: (ctx) => {
|
|
1576
|
-
const
|
|
1577
|
-
const
|
|
1578
|
-
if (
|
|
1579
|
-
|
|
1580
|
-
return mcpResult && mcpResult.ok;
|
|
1649
|
+
const docs = docsBundle(ctx);
|
|
1650
|
+
const relevant = os.platform() === 'win32' || /\bwsl\b|\bwindows\b/i.test(docs);
|
|
1651
|
+
if (!relevant) return null;
|
|
1652
|
+
return /\bwsl\b.{0,40}\b(crash|unstable|avoid|native windows)\b|\bnative windows\b|\bavoid wsl\b/i.test(docs);
|
|
1581
1653
|
},
|
|
1582
1654
|
impact: 'medium',
|
|
1583
1655
|
rating: 3,
|
|
1584
1656
|
category: 'advisory',
|
|
1585
|
-
fix: '
|
|
1657
|
+
fix: 'Add a Windows note that Windsurf has known WSL crashes/path-resolution issues and is more stable in native Windows or native Linux than under WSL.',
|
|
1586
1658
|
template: null,
|
|
1587
|
-
file: () => '.windsurf/
|
|
1659
|
+
file: () => '.windsurf/rules/',
|
|
1588
1660
|
line: () => null,
|
|
1589
1661
|
},
|
|
1590
1662
|
|
|
@@ -1612,15 +1684,16 @@ const WINDSURF_TECHNIQUES = {
|
|
|
1612
1684
|
id: 'WS-N02',
|
|
1613
1685
|
name: 'MCP packs recommended based on project signals',
|
|
1614
1686
|
check: (ctx) => {
|
|
1615
|
-
const
|
|
1687
|
+
const mcp = mcpJsonData(ctx);
|
|
1688
|
+
const servers = mcp && mcp.mcpServers ? mcp.mcpServers : {};
|
|
1616
1689
|
return Object.keys(servers).length > 0;
|
|
1617
1690
|
},
|
|
1618
1691
|
impact: 'low',
|
|
1619
1692
|
rating: 2,
|
|
1620
1693
|
category: 'advisory',
|
|
1621
|
-
fix: 'Add recommended MCP
|
|
1694
|
+
fix: 'Add recommended MCP servers to `%USERPROFILE%/.codeium/windsurf/mcp_config.json` based on the project domain.',
|
|
1622
1695
|
template: 'windsurf-mcp',
|
|
1623
|
-
file: () =>
|
|
1696
|
+
file: () => windsurfMcpConfigPath(),
|
|
1624
1697
|
line: () => null,
|
|
1625
1698
|
},
|
|
1626
1699
|
|