@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
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
|
-
*
|
|
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)
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
};
|