@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/src/convert.js ADDED
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Nerviq Convert
3
+ *
4
+ * Converts configuration files between AI coding platforms.
5
+ * Reads the source platform's config and emits equivalent config
6
+ * for the target platform, preserving intent where possible.
7
+ *
8
+ * Supported conversions:
9
+ * claude → codex, cursor, copilot, gemini, windsurf, aider
10
+ * codex → claude, cursor, copilot, gemini, windsurf, aider
11
+ * cursor → claude, codex, copilot, gemini, windsurf, aider
12
+ * (any) → (any) using canonical model as intermediary
13
+ */
14
+
15
+ 'use strict';
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ const COLORS = {
21
+ reset: '\x1b[0m',
22
+ bold: '\x1b[1m',
23
+ dim: '\x1b[2m',
24
+ red: '\x1b[31m',
25
+ green: '\x1b[32m',
26
+ yellow: '\x1b[33m',
27
+ blue: '\x1b[36m',
28
+ };
29
+
30
+ function c(text, color) {
31
+ return `${COLORS[color] || ''}${text}${COLORS.reset}`;
32
+ }
33
+
34
+ // ─── Platform config readers ─────────────────────────────────────────────────
35
+
36
+ /**
37
+ * Read the canonical "intent" from a source platform.
38
+ * Returns a normalized object with: name, description, rules[], mcpServers{}, hooks[]
39
+ */
40
+ function readSourceConfig(dir, from) {
41
+ const canonical = {
42
+ platform: from,
43
+ name: path.basename(dir),
44
+ description: null,
45
+ rules: [], // Array of { name, content, alwaysOn, glob, description }
46
+ mcpServers: {}, // { serverName: { command, args, env, url, type } }
47
+ hooks: [], // Array of { event, command, matcher }
48
+ techStack: [], // Detected languages/frameworks
49
+ lintCmd: null,
50
+ testCmd: null,
51
+ buildCmd: null,
52
+ };
53
+
54
+ if (from === 'claude') {
55
+ const claudeMd = fs.existsSync(path.join(dir, 'CLAUDE.md'))
56
+ ? fs.readFileSync(path.join(dir, 'CLAUDE.md'), 'utf8')
57
+ : null;
58
+ if (claudeMd) {
59
+ canonical.description = claudeMd.slice(0, 500);
60
+ canonical.rules.push({ name: 'CLAUDE.md', content: claudeMd, alwaysOn: true });
61
+ }
62
+ // Read .claude/settings.json for MCP
63
+ const settingsPath = path.join(dir, '.claude', 'settings.json');
64
+ if (fs.existsSync(settingsPath)) {
65
+ try {
66
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
67
+ if (settings.mcpServers) canonical.mcpServers = settings.mcpServers;
68
+ } catch {}
69
+ }
70
+ }
71
+
72
+ if (from === 'codex') {
73
+ const agentsMd = fs.existsSync(path.join(dir, 'AGENTS.md'))
74
+ ? fs.readFileSync(path.join(dir, 'AGENTS.md'), 'utf8')
75
+ : null;
76
+ if (agentsMd) {
77
+ canonical.description = agentsMd.slice(0, 500);
78
+ canonical.rules.push({ name: 'AGENTS.md', content: agentsMd, alwaysOn: true });
79
+ }
80
+ }
81
+
82
+ if (from === 'cursor') {
83
+ const rulesDir = path.join(dir, '.cursor', 'rules');
84
+ if (fs.existsSync(rulesDir)) {
85
+ const files = fs.readdirSync(rulesDir).filter(f => f.endsWith('.mdc'));
86
+ for (const file of files) {
87
+ const content = fs.readFileSync(path.join(rulesDir, file), 'utf8');
88
+ // Parse frontmatter
89
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
90
+ let alwaysOn = false;
91
+ let glob = null;
92
+ let desc = null;
93
+ if (fmMatch) {
94
+ alwaysOn = /alwaysApply\s*:\s*true/i.test(fmMatch[1]);
95
+ const globMatch = fmMatch[1].match(/globs?\s*:\s*(.+)/i);
96
+ if (globMatch) glob = globMatch[1].trim();
97
+ const descMatch = fmMatch[1].match(/description\s*:\s*"?([^"\n]+)"?/i);
98
+ if (descMatch) desc = descMatch[1].trim();
99
+ }
100
+ canonical.rules.push({
101
+ name: file.replace('.mdc', ''),
102
+ content,
103
+ alwaysOn,
104
+ glob,
105
+ description: desc,
106
+ });
107
+ }
108
+ }
109
+ // Cursor MCP
110
+ const mcpPath = path.join(dir, '.cursor', 'mcp.json');
111
+ if (fs.existsSync(mcpPath)) {
112
+ try {
113
+ const mcp = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
114
+ if (mcp.mcpServers) canonical.mcpServers = mcp.mcpServers;
115
+ } catch {}
116
+ }
117
+ }
118
+
119
+ if (from === 'gemini') {
120
+ const geminiMd = fs.existsSync(path.join(dir, 'GEMINI.md'))
121
+ ? fs.readFileSync(path.join(dir, 'GEMINI.md'), 'utf8')
122
+ : null;
123
+ if (geminiMd) {
124
+ canonical.description = geminiMd.slice(0, 500);
125
+ canonical.rules.push({ name: 'GEMINI.md', content: geminiMd, alwaysOn: true });
126
+ }
127
+ }
128
+
129
+ if (from === 'windsurf') {
130
+ const windsurfRulesDir = path.join(dir, '.windsurf', 'rules');
131
+ if (fs.existsSync(windsurfRulesDir)) {
132
+ const files = fs.readdirSync(windsurfRulesDir).filter(f => f.endsWith('.md'));
133
+ for (const file of files) {
134
+ const content = fs.readFileSync(path.join(windsurfRulesDir, file), 'utf8');
135
+ canonical.rules.push({ name: file.replace('.md', ''), content, alwaysOn: true });
136
+ }
137
+ }
138
+ }
139
+
140
+ if (from === 'aider') {
141
+ const aiderConf = fs.existsSync(path.join(dir, '.aider.conf.yml'))
142
+ ? fs.readFileSync(path.join(dir, '.aider.conf.yml'), 'utf8')
143
+ : null;
144
+ if (aiderConf) {
145
+ canonical.rules.push({ name: '.aider.conf.yml', content: aiderConf, alwaysOn: false });
146
+ const lintMatch = aiderConf.match(/lint-cmd\s*:\s*(.+)/);
147
+ if (lintMatch) canonical.lintCmd = lintMatch[1].trim().replace(/^['"]|['"]$/g, '');
148
+ const testMatch = aiderConf.match(/test-cmd\s*:\s*(.+)/);
149
+ if (testMatch) canonical.testCmd = testMatch[1].trim().replace(/^['"]|['"]$/g, '');
150
+ }
151
+ }
152
+
153
+ if (from === 'copilot') {
154
+ const copilotPath = path.join(dir, '.github', 'copilot-instructions.md');
155
+ if (fs.existsSync(copilotPath)) {
156
+ const content = fs.readFileSync(copilotPath, 'utf8');
157
+ canonical.rules.push({ name: 'copilot-instructions', content, alwaysOn: true });
158
+ }
159
+ }
160
+
161
+ return canonical;
162
+ }
163
+
164
+ // ─── Platform config writers ─────────────────────────────────────────────────
165
+
166
+ function buildTargetOutput(canonical, to, { dryRun = false } = {}) {
167
+ const outputs = []; // Array of { path, content }
168
+ const combinedContent = canonical.rules.map(r => r.content).join('\n\n');
169
+
170
+ if (to === 'claude') {
171
+ // Extract or create CLAUDE.md from combined rules
172
+ const content = `# ${canonical.name}\n\n${combinedContent}\n`;
173
+ outputs.push({ file: 'CLAUDE.md', content });
174
+
175
+ if (Object.keys(canonical.mcpServers).length > 0) {
176
+ const settings = { mcpServers: canonical.mcpServers };
177
+ outputs.push({ file: '.claude/settings.json', content: JSON.stringify(settings, null, 2) + '\n' });
178
+ }
179
+ }
180
+
181
+ if (to === 'codex') {
182
+ const content = `# ${canonical.name}\n\n${combinedContent}\n`;
183
+ outputs.push({ file: 'AGENTS.md', content });
184
+ }
185
+
186
+ if (to === 'cursor') {
187
+ // Write each rule as an .mdc file
188
+ if (canonical.rules.length === 0) {
189
+ const content = `---\nalwaysApply: true\n---\n\n# ${canonical.name}\n\n${combinedContent}\n`;
190
+ outputs.push({ file: '.cursor/rules/core.mdc', content });
191
+ } else {
192
+ for (const rule of canonical.rules) {
193
+ const fm = rule.alwaysOn
194
+ ? `---\nalwaysApply: true\n---\n`
195
+ : rule.glob
196
+ ? `---\nglobs: ${rule.glob}\nalwaysApply: false\n---\n`
197
+ : `---\nalwaysApply: false\n---\n`;
198
+ outputs.push({ file: `.cursor/rules/${rule.name}.mdc`, content: `${fm}\n${rule.content}\n` });
199
+ }
200
+ }
201
+ if (Object.keys(canonical.mcpServers).length > 0) {
202
+ const mcp = { mcpServers: canonical.mcpServers };
203
+ outputs.push({ file: '.cursor/mcp.json', content: JSON.stringify(mcp, null, 2) + '\n' });
204
+ }
205
+ }
206
+
207
+ if (to === 'gemini') {
208
+ const content = `# ${canonical.name}\n\n${combinedContent}\n`;
209
+ outputs.push({ file: 'GEMINI.md', content });
210
+ if (Object.keys(canonical.mcpServers).length > 0) {
211
+ const settings = { mcpServers: canonical.mcpServers };
212
+ outputs.push({ file: '.gemini/settings.json', content: JSON.stringify(settings, null, 2) + '\n' });
213
+ }
214
+ }
215
+
216
+ if (to === 'windsurf') {
217
+ if (canonical.rules.length === 0) {
218
+ outputs.push({ file: '.windsurf/rules/core.md', content: `---\ntrigger: always_on\n---\n\n${combinedContent}\n` });
219
+ } else {
220
+ for (const rule of canonical.rules) {
221
+ const fm = `---\ntrigger: always_on\n---\n`;
222
+ const safeContent = rule.content.replace(/^---[\s\S]*?---\n/m, '').trim();
223
+ outputs.push({ file: `.windsurf/rules/${rule.name}.md`, content: `${fm}\n${safeContent}\n` });
224
+ }
225
+ }
226
+ }
227
+
228
+ if (to === 'aider') {
229
+ const confLines = ['# Generated by nerviq convert'];
230
+ if (canonical.lintCmd) confLines.push(`lint-cmd: '${canonical.lintCmd}'`);
231
+ if (canonical.testCmd) confLines.push(`test-cmd: '${canonical.testCmd}'`);
232
+ confLines.push('auto-commits: true');
233
+ confLines.push('auto-lint: true');
234
+ outputs.push({ file: '.aider.conf.yml', content: confLines.join('\n') + '\n' });
235
+ if (combinedContent.trim()) {
236
+ outputs.push({ file: 'CONVENTIONS.md', content: `# ${canonical.name} Conventions\n\n${combinedContent}\n` });
237
+ }
238
+ }
239
+
240
+ if (to === 'copilot') {
241
+ const content = `# ${canonical.name}\n\n${combinedContent}\n`;
242
+ outputs.push({ file: '.github/copilot-instructions.md', content });
243
+ }
244
+
245
+ return outputs;
246
+ }
247
+
248
+ // ─── Main convert function ────────────────────────────────────────────────────
249
+
250
+ async function runConvert({ dir = process.cwd(), from, to, dryRun = false, json = false } = {}) {
251
+ if (!from || !to) {
252
+ throw new Error('Both --from and --to are required. Example: nerviq convert --from claude --to codex');
253
+ }
254
+
255
+ const SUPPORTED = ['claude', 'codex', 'cursor', 'copilot', 'gemini', 'windsurf', 'aider', 'opencode'];
256
+ if (!SUPPORTED.includes(from)) throw new Error(`Unsupported source platform '${from}'. Use: ${SUPPORTED.join(', ')}`);
257
+ if (!SUPPORTED.includes(to)) throw new Error(`Unsupported target platform '${to}'. Use: ${SUPPORTED.join(', ')}`);
258
+ if (from === to) throw new Error(`Source and target platform are the same: '${from}'`);
259
+
260
+ const canonical = readSourceConfig(dir, from);
261
+ const outputs = buildTargetOutput(canonical, to, { dryRun });
262
+
263
+ const written = [];
264
+ const skipped = [];
265
+
266
+ if (!dryRun) {
267
+ for (const out of outputs) {
268
+ const outPath = path.join(dir, out.file);
269
+ const outDir = path.dirname(outPath);
270
+ if (!fs.existsSync(outPath)) {
271
+ fs.mkdirSync(outDir, { recursive: true });
272
+ fs.writeFileSync(outPath, out.content, 'utf8');
273
+ written.push(out.file);
274
+ } else {
275
+ skipped.push(out.file);
276
+ }
277
+ }
278
+ }
279
+
280
+ const result = {
281
+ from,
282
+ to,
283
+ dir,
284
+ dryRun,
285
+ sourceRulesFound: canonical.rules.length,
286
+ mcpServersFound: Object.keys(canonical.mcpServers).length,
287
+ outputFiles: outputs.map(o => o.file),
288
+ written: dryRun ? [] : written,
289
+ skipped: dryRun ? [] : skipped,
290
+ wouldWrite: dryRun ? outputs.map(o => o.file) : [],
291
+ };
292
+
293
+ if (json) return JSON.stringify(result, null, 2);
294
+
295
+ const lines = [''];
296
+ lines.push(c(` nerviq convert ${from} → ${to}`, 'bold'));
297
+ lines.push(c(' ═══════════════════════════════════════', 'dim'));
298
+ lines.push('');
299
+ lines.push(` Source platform: ${c(from, 'blue')} (${canonical.rules.length} rule(s) found)`);
300
+ lines.push(` Target platform: ${c(to, 'blue')}`);
301
+ lines.push(` Directory: ${dir}`);
302
+ lines.push(` MCP servers: ${Object.keys(canonical.mcpServers).length}`);
303
+ lines.push('');
304
+
305
+ if (dryRun) {
306
+ lines.push(c(' Dry run — no files written', 'yellow'));
307
+ lines.push('');
308
+ lines.push(' Would generate:');
309
+ for (const f of outputs) {
310
+ lines.push(` ${c('→', 'dim')} ${f.file}`);
311
+ }
312
+ } else if (written.length > 0 || skipped.length > 0) {
313
+ if (written.length > 0) {
314
+ lines.push(' Written:');
315
+ for (const f of written) lines.push(` ${c('✓', 'green')} ${f}`);
316
+ }
317
+ if (skipped.length > 0) {
318
+ lines.push(' Skipped (already exists):');
319
+ for (const f of skipped) lines.push(` ${c('-', 'dim')} ${f}`);
320
+ }
321
+ }
322
+
323
+ lines.push('');
324
+ if (!dryRun && written.length > 0) {
325
+ lines.push(c(` ✓ Conversion complete. Run \`nerviq audit --platform ${to}\` to verify.`, 'green'));
326
+ } else if (dryRun) {
327
+ lines.push(c(` Run without --dry-run to write files.`, 'dim'));
328
+ } else {
329
+ lines.push(c(` No new files written (all already exist).`, 'dim'));
330
+ }
331
+ lines.push('');
332
+
333
+ return lines.join('\n');
334
+ }
335
+
336
+ module.exports = { runConvert };
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Copilot techniques module — CHECK CATALOG
3
3
  *
4
- * 82 checks across 16 categories:
4
+ * 86 checks across 17 categories:
5
5
  * v0.1 (38): A. Instructions(8), B. Config(6), C. Trust & Safety(9), D. MCP(5), E. Cloud Agent(5), F. Organization(5)
6
6
  * v0.5 (54): G. Prompt Files(4), H. Agents & Skills(4), I. VS Code IDE(4), J. CLI(4)
7
7
  * v1.0 (70): K. Cross-Surface(5), L. Enterprise(5), M. Quality Deep(6)
8
- * CP-08 (82): N. Advisory(4), O. Pack(4), P. Repeat(3), Q. Freshness(3)
8
+ * CP-08 (82): N. Advisory(4), O. Pack(4), P. Repeat(3)
9
+ * v1.1 (87): Q. Experiment-Verified CLI Fixes (CLI ingests AGENTS.md/CLAUDE.md, mcpServers key, VS Code settings not CLI-relevant, org policy MCP blocks, BYOK MCP caveat)
9
10
  *
10
11
  * Each check: { id, name, check(ctx), impact, rating, category, fix, template, file(), line() }
11
12
  */
@@ -14,6 +15,7 @@ const os = require('os');
14
15
  const path = require('path');
15
16
  const { CopilotProjectContext } = require('./context');
16
17
  const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
18
+ const { attachSourceUrls } = require('../source-urls');
17
19
  const { extractFrontmatter, validateInstructionFrontmatter, validatePromptFrontmatter } = require('./config-parser');
18
20
 
19
21
  // ─── Shared helpers ─────────────────────────────────────────────────────────
@@ -313,18 +315,19 @@ const COPILOT_TECHNIQUES = {
313
315
 
314
316
  copilotVscodeSettingsExists: {
315
317
  id: 'CP-B01',
316
- name: '.vscode/settings.json has Copilot agent settings',
318
+ name: '.vscode/settings.json has Copilot agent settings (VS Code-only)',
317
319
  check: (ctx) => {
318
320
  const data = vscodeSettingsData(ctx);
319
321
  if (!data) return false;
320
322
  // Check for any Copilot or chat-related key
323
+ // NOTE: These settings affect VS Code only. Copilot CLI ignores them.
321
324
  const raw = vscodeSettingsRaw(ctx);
322
325
  return /github\.copilot|chat\./.test(raw);
323
326
  },
324
327
  impact: 'medium',
325
328
  rating: 4,
326
329
  category: 'config',
327
- fix: 'Add Copilot agent settings to .vscode/settings.json.',
330
+ fix: 'Add Copilot agent settings to .vscode/settings.json. NOTE: These are VS Code-only — Copilot CLI has its own configuration surface.',
328
331
  template: 'copilot-vscode-settings',
329
332
  file: () => '.vscode/settings.json',
330
333
  line: () => 1,
@@ -491,19 +494,20 @@ const COPILOT_TECHNIQUES = {
491
494
 
492
495
  copilotTerminalSandboxEnabled: {
493
496
  id: 'CP-C03',
494
- name: 'Terminal sandbox enabled (VS Code agent)',
497
+ name: 'Terminal sandbox enabled (VS Code-only — does NOT affect CLI)',
495
498
  check: (ctx) => {
496
499
  const data = vscodeSettingsData(ctx);
497
500
  if (!data) return false;
498
501
  const raw = vscodeSettingsRaw(ctx);
499
502
  // Check for chat.tools.terminal.sandbox.enabled = true
503
+ // NOTE: This setting is VS Code-specific. Copilot CLI ignores it entirely.
500
504
  if (raw.includes('terminal.sandbox') && raw.includes('true')) return true;
501
505
  return getCopilotSetting(ctx, 'chat.tools.terminal.sandbox.enabled') === true;
502
506
  },
503
507
  impact: 'high',
504
508
  rating: 5,
505
509
  category: 'trust',
506
- fix: 'Add "chat.tools.terminal.sandbox.enabled": true to .vscode/settings.json.',
510
+ fix: 'Add "chat.tools.terminal.sandbox.enabled": true to .vscode/settings.json. NOTE: This is VS Code-only — Copilot CLI uses its own permission flags, not VS Code settings.',
507
511
  template: 'copilot-vscode-settings',
508
512
  file: () => '.vscode/settings.json',
509
513
  line: (ctx) => {
@@ -533,12 +537,14 @@ const COPILOT_TECHNIQUES = {
533
537
 
534
538
  copilotAutoApprovalSpecific: {
535
539
  id: 'CP-C05',
536
- name: 'Auto-approval rules are specific (not wildcard)',
540
+ name: 'Auto-approval rules are specific (VS Code-only — CLI uses permission flags)',
537
541
  check: (ctx) => {
538
542
  const data = vscodeSettingsData(ctx);
539
543
  if (!data) return null;
540
544
  const raw = vscodeSettingsRaw(ctx);
541
545
  // Check for auto-approval patterns
546
+ // NOTE: autoApproval.terminalCommands is VS Code-specific.
547
+ // Copilot CLI uses its own --permission flags, not this setting.
542
548
  const autoApproval = getCopilotSetting(ctx, 'chat.agent.autoApproval.terminalCommands');
543
549
  if (!autoApproval || !Array.isArray(autoApproval)) return null;
544
550
  // Fail if any wildcard patterns
@@ -547,7 +553,7 @@ const COPILOT_TECHNIQUES = {
547
553
  impact: 'high',
548
554
  rating: 5,
549
555
  category: 'trust',
550
- fix: 'Replace wildcard auto-approval patterns with specific command patterns (e.g., "npm test", "npm run lint").',
556
+ fix: 'Replace wildcard auto-approval patterns with specific command patterns (e.g., "npm test", "npm run lint"). NOTE: This setting only affects VS Code — Copilot CLI approval is controlled by CLI permission flags.',
551
557
  template: null,
552
558
  file: () => '.vscode/settings.json',
553
559
  line: (ctx) => {
@@ -1051,11 +1057,13 @@ const COPILOT_TECHNIQUES = {
1051
1057
 
1052
1058
  copilotAgentsMdEnabled: {
1053
1059
  id: 'CP-H01',
1054
- name: 'If AGENTS.md exists, verify it is enabled in VS Code settings',
1060
+ name: 'If AGENTS.md exists, verify it is enabled in VS Code (CLI reads it automatically)',
1055
1061
  check: (ctx) => {
1056
1062
  const agentsMd = ctx.fileContent('AGENTS.md');
1057
1063
  if (!agentsMd) return null; // N/A
1058
- // AGENTS.md support needs explicit enabling
1064
+ // AGENTS.md support needs explicit enabling in VS Code
1065
+ // WARNING: Copilot CLI reads AGENTS.md (and CLAUDE.md) automatically without any setting!
1066
+ // Use --no-custom-instructions in CLI to prevent this
1059
1067
  const data = vscodeSettingsData(ctx);
1060
1068
  if (!data) return false;
1061
1069
  const raw = vscodeSettingsRaw(ctx);
@@ -1064,7 +1072,7 @@ const COPILOT_TECHNIQUES = {
1064
1072
  impact: 'critical',
1065
1073
  rating: 5,
1066
1074
  category: 'skills-agents',
1067
- fix: 'Enable AGENTS.md support in VS Code settings. It is off by default and silently ignored.',
1075
+ fix: 'Enable AGENTS.md in VS Code settings (off by default). WARNING: Copilot CLI reads AGENTS.md and CLAUDE.md automatically — use --no-custom-instructions to prevent cross-platform instruction leakage.',
1068
1076
  template: 'copilot-vscode-settings',
1069
1077
  file: () => '.vscode/settings.json',
1070
1078
  line: (ctx) => {
@@ -1815,8 +1823,114 @@ const COPILOT_TECHNIQUES = {
1815
1823
  file: () => null,
1816
1824
  line: () => null,
1817
1825
  },
1826
+
1827
+ // =============================================
1828
+ // Q. Experiment-Verified CLI Fixes (5 checks) — CP-Q01..CP-Q05
1829
+ // Added from runtime experiment findings (2026-04-05)
1830
+ // =============================================
1831
+
1832
+ copilotCliIngestsNonCopilotFiles: {
1833
+ id: 'CP-Q01',
1834
+ name: 'Aware that Copilot CLI ingests AGENTS.md and CLAUDE.md',
1835
+ check: (ctx) => {
1836
+ const agentsMd = ctx.fileContent('AGENTS.md');
1837
+ const claudeMd = ctx.fileContent('CLAUDE.md');
1838
+ if (!agentsMd && !claudeMd) return null; // No cross-platform files
1839
+ const instr = copilotInstructions(ctx) || '';
1840
+ // If non-Copilot instruction files exist, check that instructions acknowledge this
1841
+ return /copilot cli|--no-custom-instructions|cross.platform|AGENTS\.md|CLAUDE\.md/i.test(instr);
1842
+ },
1843
+ impact: 'high',
1844
+ rating: 4,
1845
+ category: 'quality-deep',
1846
+ fix: 'WARNING: Copilot CLI ingests AGENTS.md and CLAUDE.md alongside copilot-instructions.md. Document this or use --no-custom-instructions for clean runs.',
1847
+ template: null,
1848
+ file: () => '.github/copilot-instructions.md',
1849
+ line: () => null,
1850
+ },
1851
+
1852
+ copilotCliMcpUsesServerKey: {
1853
+ id: 'CP-Q02',
1854
+ name: 'CLI MCP config uses mcpServers key (not servers)',
1855
+ check: (ctx) => {
1856
+ const mcpData = mcpJsonData(ctx);
1857
+ if (!mcpData) return null;
1858
+ // CLI expects mcpServers, not servers
1859
+ if (mcpData.servers && !mcpData.mcpServers) return false;
1860
+ return true;
1861
+ },
1862
+ impact: 'high',
1863
+ rating: 4,
1864
+ category: 'ci-automation',
1865
+ fix: 'Copilot CLI MCP config expects the "mcpServers" key. "servers" alone may not work in CLI context.',
1866
+ template: null,
1867
+ file: () => '.vscode/mcp.json',
1868
+ line: () => 1,
1869
+ },
1870
+
1871
+ copilotVscodeSettingsNotCliRelevant: {
1872
+ id: 'CP-Q03',
1873
+ name: 'VS Code-specific settings not assumed to affect CLI',
1874
+ check: (ctx) => {
1875
+ const instr = copilotInstructions(ctx) || '';
1876
+ if (!instr) return null;
1877
+ // If instructions reference VS Code settings as if they affect CLI, flag it
1878
+ const mentionsCli = /copilot cli|gh copilot/i.test(instr);
1879
+ const mentionsVscodeForCli = /chat\.tools.*cli|terminal\.sandbox.*cli|autoApproval.*cli/i.test(instr);
1880
+ if (mentionsCli && mentionsVscodeForCli) return false;
1881
+ return true;
1882
+ },
1883
+ impact: 'medium',
1884
+ rating: 3,
1885
+ category: 'quality-deep',
1886
+ fix: 'VS Code settings (sandbox, autoApproval, instructionsFilesLocations) do not affect Copilot CLI. Document CLI-specific configuration separately.',
1887
+ template: null,
1888
+ file: () => '.github/copilot-instructions.md',
1889
+ line: () => null,
1890
+ },
1891
+
1892
+ copilotOrgPolicyBlocksMcp: {
1893
+ id: 'CP-Q04',
1894
+ name: 'Org policy MCP restrictions documented if applicable',
1895
+ check: (ctx) => {
1896
+ const instr = copilotInstructions(ctx) || '';
1897
+ const mcpData = mcpJsonData(ctx);
1898
+ if (!mcpData) return null;
1899
+ const servers = mcpData.servers || mcpData.mcpServers || {};
1900
+ if (Object.keys(servers).length === 0) return null;
1901
+ // If MCP servers are configured, check that org policy restrictions are documented
1902
+ return /org.policy|policy.block|third.party.*mcp|mcp.*restrict|Access denied/i.test(instr);
1903
+ },
1904
+ impact: 'medium',
1905
+ rating: 3,
1906
+ category: 'quality-deep',
1907
+ fix: 'Document that org policies can block third-party MCP servers even in local CLI sessions. Error: "Access denied by policy settings".',
1908
+ template: null,
1909
+ file: () => '.github/copilot-instructions.md',
1910
+ line: () => null,
1911
+ },
1912
+
1913
+ copilotByokMcpCaveat: {
1914
+ id: 'CP-Q05',
1915
+ name: 'BYOK mode MCP limitations documented',
1916
+ check: (ctx) => {
1917
+ const instr = copilotInstructions(ctx) || '';
1918
+ // Only relevant if BYOK is mentioned
1919
+ if (!/byok|bring your own key|openai.*key|COPILOT_.*KEY/i.test(instr)) return null;
1920
+ return /byok.*mcp|mcp.*byok|oauth.*broken|built.in.*github.*mcp/i.test(instr);
1921
+ },
1922
+ impact: 'medium',
1923
+ rating: 3,
1924
+ category: 'quality-deep',
1925
+ fix: 'Document that BYOK mode breaks built-in GitHub MCP server (OAuth auth unavailable). Third-party MCP may also be restricted by org policy.',
1926
+ template: null,
1927
+ file: () => '.github/copilot-instructions.md',
1928
+ line: () => null,
1929
+ },
1818
1930
  };
1819
1931
 
1932
+ attachSourceUrls('copilot', COPILOT_TECHNIQUES);
1933
+
1820
1934
  module.exports = {
1821
1935
  COPILOT_TECHNIQUES,
1822
1936
  };