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