@nerviq/cli 1.19.0 → 1.20.0

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 CHANGED
@@ -223,7 +223,7 @@ All successful operational responses are wrapped in a JSON envelope:
223
223
  {
224
224
  "data": {},
225
225
  "meta": {
226
- "version": "1.19.0",
226
+ "version": "1.20.0",
227
227
  "timestamp": "2026-04-13T12:00:00.000Z"
228
228
  }
229
229
  }
package/bin/cli.js CHANGED
@@ -2536,6 +2536,7 @@ async function main() {
2536
2536
  }
2537
2537
  } catch (err) {
2538
2538
  console.error(`\n Error: ${err.message}`);
2539
+ if (process.env.NERVIQ_DEBUG) console.error(err.stack);
2539
2540
  console.error(' Fix: Run `npx nerviq doctor` to diagnose common issues, or check https://github.com/nerviq/nerviq#troubleshooting');
2540
2541
  process.exit(2);
2541
2542
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nerviq/cli",
3
- "version": "1.19.0",
3
+ "version": "1.20.0",
4
4
  "description": "The intelligent nervous system for AI coding agents — 2,441 checks (8 platforms × ~300 governance rules), 10 languages, 62 domain packs. Audit, align, and amplify.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -18,6 +18,7 @@
18
18
  "build": "npm pack --dry-run",
19
19
  "test": "node test/run.js",
20
20
  "verify:release-metadata": "node tools/validate-release-metadata.js",
21
+ "prepublish:check": "node tools/pre-publish.js",
21
22
  "test:jest": "jest",
22
23
  "test:coverage": "jest --coverage",
23
24
  "test:all": "npm test && npx jest && node test/check-matrix.js && node test/codex-check-matrix.js && node test/gemini-check-matrix.js && node test/copilot-check-matrix.js && node test/cursor-check-matrix.js && node test/windsurf-check-matrix.js && node test/aider-check-matrix.js && node test/opencode-check-matrix.js && node test/golden-matrix.js && node test/codex-golden-matrix.js && node test/gemini-golden-matrix.js && node test/copilot-golden-matrix.js && node test/cursor-golden-matrix.js && node test/windsurf-golden-matrix.js && node test/aider-golden-matrix.js && node test/opencode-golden-matrix.js",
@@ -54,18 +54,87 @@ class GeminiProjectContext extends ProjectContext {
54
54
 
55
55
  geminiMdContent() {
56
56
  const direct = this.fileContent('GEMINI.md');
57
- if (direct) return direct;
57
+ if (direct) return this._expandGeminiMdImports(direct);
58
58
 
59
- // Fallback: use context.fileName from settings if configured
59
+ // Fallback: use context.fileName from settings if configured.
60
+ // Per Gemini CLI spec, context.fileName may be a string or an array of strings.
60
61
  const contextFileName = this.configValue('context.fileName');
61
- if (contextFileName) {
62
- const content = this.fileContent(contextFileName);
63
- if (content) return content;
62
+ const candidates = Array.isArray(contextFileName)
63
+ ? contextFileName.filter(n => typeof n === 'string' && n.length > 0)
64
+ : (typeof contextFileName === 'string' && contextFileName ? [contextFileName] : []);
65
+ for (const name of candidates) {
66
+ const content = this.fileContent(name);
67
+ if (content) return this._expandGeminiMdImports(content);
68
+ }
69
+
70
+ // Further fallback: recognise common alternate instruction surfaces
71
+ // (AGENTS.md, CLAUDE.md) and Gemini Code Assist styleguides
72
+ // (.gemini/styleguide.md) even when not explicitly declared in settings,
73
+ // mirroring how real Gemini-using repos document guidance.
74
+ for (const alt of ['AGENTS.md', 'CLAUDE.md', '.gemini/styleguide.md']) {
75
+ const content = this.fileContent(alt);
76
+ if (content) return this._expandGeminiMdImports(content);
64
77
  }
65
78
 
66
79
  return null;
67
80
  }
68
81
 
82
+ /**
83
+ * Expand Gemini CLI-style imports inside an instructions file. Gemini CLI
84
+ * supports `@path/to/file.md` imports and treats GEMINI.md files that are
85
+ * a single pointer line as an alias for the referenced file. For audit
86
+ * purposes we concatenate the referenced bodies so substance/architecture/
87
+ * command checks see the effective instructions bundle.
88
+ */
89
+ _expandGeminiMdImports(content, depth = 0) {
90
+ if (!content || depth > 3) return content || '';
91
+ let out = content;
92
+ const importRe = /@([^\s@]+\.(?:md|markdown|MD))/g;
93
+ const seen = new Set();
94
+ let m;
95
+ while ((m = importRe.exec(content)) !== null) {
96
+ const ref = m[1].replace(/^\.\//, '');
97
+ if (seen.has(ref)) continue;
98
+ seen.add(ref);
99
+ const body = this.fileContent(ref);
100
+ if (body) out += '\n\n' + this._expandGeminiMdImports(body, depth + 1);
101
+ }
102
+ // "Pointer" GEMINI.md: the whole file is a single relative path to another
103
+ // markdown doc (no @ prefix). Observed in google/dotprompt.
104
+ const trimmed = content.trim();
105
+ if (/^[\w./-]+\.(md|markdown)$/.test(trimmed) && !trimmed.includes('\n')) {
106
+ const body = this.fileContent(trimmed);
107
+ if (body) out += '\n\n' + this._expandGeminiMdImports(body, depth + 1);
108
+ }
109
+ return out;
110
+ }
111
+
112
+ /**
113
+ * Returns true when the repo exposes any Gemini-recognisable instruction
114
+ * surface — GEMINI.md (directly or via context.fileName override), an
115
+ * imported pointer, AGENTS.md, or CLAUDE.md. Used to gate checks that
116
+ * would otherwise hard-fail on repos that use alternative conventions.
117
+ */
118
+ hasAnyInstructionsSurface() {
119
+ return Boolean(this.geminiMdContent());
120
+ }
121
+
122
+ /**
123
+ * Returns true when the repo exposes any evidence of Gemini CLI usage.
124
+ * This is deliberately narrower than `isGeminiRepo`: it also counts
125
+ * `.idx/airules.md` (Project IDX) and Gemini-specific settings keys.
126
+ */
127
+ hasGeminiCliSurface() {
128
+ if (this.fileContent('.gemini/settings.json')) return true;
129
+ if (this.fileContent('GEMINI.md')) return true;
130
+ const extDirs = this.extensionDirs ? this.extensionDirs() : [];
131
+ if (extDirs.length > 0) return true;
132
+ const cmdFiles = this.commandFiles ? this.commandFiles() : [];
133
+ if (cmdFiles.length > 0) return true;
134
+ if (this.fileContent('.idx/airules.md')) return true;
135
+ return false;
136
+ }
137
+
69
138
  globalGeminiMdContent() {
70
139
  const homeDir = os.homedir();
71
140
  const globalPath = path.join(homeDir, '.gemini', 'GEMINI.md');
@@ -93,10 +93,37 @@ function settingsData(ctx) {
93
93
  return result && result.ok ? result.data : null;
94
94
  }
95
95
 
96
+ /**
97
+ * True when .gemini/settings.json is effectively an MCP-only config — i.e.
98
+ * it configures external tool servers but does not attempt to tune CLI
99
+ * behaviour (model, sandbox, approval, theme, history, etc.). Checks that
100
+ * assert on CLI-behaviour keys should be N/A on such configs.
101
+ */
102
+ function isMcpOnlySettings(data) {
103
+ if (!data || typeof data !== 'object') return false;
104
+ const keys = Object.keys(data).filter(k => k !== '$schema' && k !== 'ide' && k !== 'context');
105
+ if (keys.length === 0) return true;
106
+ const behaviourKeys = new Set(['model', 'sandbox', 'safety', 'theme', 'approval', 'approvalMode', 'history', 'session', 'telemetry', 'hooks', 'tools', 'skills', 'commands', 'extensions', 'security']);
107
+ return keys.every(k => k === 'mcpServers' || !behaviourKeys.has(k));
108
+ }
109
+
96
110
  function docsBundle(ctx) {
97
111
  const gmd = geminiMd(ctx) || '';
98
112
  const readme = ctx.fileContent('README.md') || '';
99
- return `${gmd}\n${readme}`;
113
+ const agents = ctx.fileContent('AGENTS.md') || '';
114
+ const claudeMd = ctx.fileContent('CLAUDE.md') || '';
115
+ const contributing = ctx.fileContent('CONTRIBUTING.md') || '';
116
+ const architecture = ctx.fileContent('ARCHITECTURE.md') || '';
117
+ const development = ctx.fileContent('DEVELOPMENT.md') || ctx.fileContent('docs/development.md') || '';
118
+ return [gmd, readme, agents, claudeMd, contributing, architecture, development].join('\n');
119
+ }
120
+
121
+ // Broader bundle for stack-specific docs discovery (mirrors the Copilot
122
+ // PP-01 stackDocsBundle approach): consults common developer docs in
123
+ // addition to the instruction surfaces so stack checks don't hard-fail
124
+ // when conventions live in CONTRIBUTING/DEVELOPMENT instead of GEMINI.md.
125
+ function stackDocsBundle(ctx) {
126
+ return docsBundle(ctx);
100
127
  }
101
128
 
102
129
  function expectedVerificationCategories(ctx) {
@@ -262,11 +289,18 @@ const GEMINI_TECHNIQUES = {
262
289
 
263
290
  geminiMdArchitecture: {
264
291
  id: 'GM-A04',
265
- name: 'GEMINI.md has architecture section or Mermaid diagram',
292
+ name: 'Instructions have architecture section or Mermaid diagram',
266
293
  check: (ctx) => {
267
294
  const content = geminiMd(ctx);
268
295
  if (!content) return null;
269
- return hasArchitecture(content);
296
+ if (hasArchitecture(content)) return true;
297
+ // Credit an ARCHITECTURE.md at repo root, or architecture content
298
+ // surfaced in README.md — both are legitimate ways Gemini will pick
299
+ // up repo shape, especially when GEMINI.md is a pointer/import.
300
+ const arch = ctx.fileContent('ARCHITECTURE.md') || ctx.fileContent('docs/architecture.md');
301
+ if (arch) return true;
302
+ const readme = ctx.fileContent('README.md') || '';
303
+ return hasArchitecture(readme);
270
304
  },
271
305
  impact: 'medium',
272
306
  rating: 4,
@@ -352,7 +386,15 @@ const GEMINI_TECHNIQUES = {
352
386
  geminiSettingsExists: {
353
387
  id: 'GM-B01',
354
388
  name: '.gemini/settings.json exists',
355
- check: (ctx) => Boolean(ctx.fileContent('.gemini/settings.json')),
389
+ check: (ctx) => {
390
+ if (ctx.fileContent('.gemini/settings.json')) return true;
391
+ // N/A when the repo uses only the GEMINI.md-instruction convention
392
+ // without any .gemini/ configuration directory. settings.json is
393
+ // opt-in for CLI tuning; instruction-only repos should not fail.
394
+ const hasGeminiDir = ctx.hasDir && ctx.hasDir('.gemini');
395
+ if (!hasGeminiDir) return null;
396
+ return false;
397
+ },
356
398
  impact: 'high',
357
399
  rating: 5,
358
400
  category: 'config',
@@ -397,6 +439,7 @@ const GEMINI_TECHNIQUES = {
397
439
  check: (ctx) => {
398
440
  const data = settingsData(ctx);
399
441
  if (!data) return null;
442
+ if (isMcpOnlySettings(data)) return null;
400
443
  if (!data.model) return false;
401
444
  // v0.36.0: model field MUST be an object { name: "..." }, not a string
402
445
  // String format causes exit code 41: "Expected object, received string"
@@ -419,8 +462,9 @@ const GEMINI_TECHNIQUES = {
419
462
  check: (ctx) => {
420
463
  const data = settingsData(ctx);
421
464
  if (!data) return null;
422
- // At least sandbox or safety setting should be explicit
423
- return Boolean(data.sandbox || data.safety || data.theme);
465
+ if (isMcpOnlySettings(data)) return null;
466
+ // At least one CLI-behaviour setting should be explicit.
467
+ return Boolean(data.sandbox || data.safety || data.theme || data.approval || data.approvalMode);
424
468
  },
425
469
  impact: 'medium',
426
470
  rating: 4,
@@ -479,13 +523,17 @@ const GEMINI_TECHNIQUES = {
479
523
 
480
524
  geminiEnvApiKey: {
481
525
  id: 'GM-B07',
482
- name: '.env exists with required API keys (GEMINI_API_KEY or Google auth)',
526
+ name: 'API key / auth documented (env file, README, or GEMINI.md)',
483
527
  check: (ctx) => {
484
528
  const envContent = ctx.fileContent('.env') || '';
485
- const envExample = ctx.fileContent('.env.example') || ctx.fileContent('.env.template') || '';
486
- const combined = `${envContent}\n${envExample}`;
487
- // Check for Gemini API key or Google auth
488
- return /\bGEMINI_API_KEY\b|\bGOOGLE_API_KEY\b|\bGOOGLE_APPLICATION_CREDENTIALS\b|\bgcloud\b/i.test(combined) || Boolean(envContent);
529
+ const envExample = ctx.fileContent('.env.example') || ctx.fileContent('.env.template') || ctx.fileContent('.env.sample') || '';
530
+ const docs = docsBundle(ctx);
531
+ const combined = `${envContent}\n${envExample}\n${docs}`;
532
+ if (!combined.trim()) return null;
533
+ // Credit env files OR documentation that mentions any Gemini/Google auth mechanism,
534
+ // including `gemini auth`, ADC / application default credentials, Vertex AI, or a
535
+ // direct mention of the standard env var names.
536
+ return /\bGEMINI_API_KEY\b|\bGOOGLE_API_KEY\b|\bGOOGLE_APPLICATION_CREDENTIALS\b|\bgcloud\b|\bapplication[- ]default credentials?\b|\bADC\b|\bvertex[- ]?ai\b|\bgemini auth\b|\bservice account\b/i.test(combined);
489
537
  },
490
538
  impact: 'high',
491
539
  rating: 4,
@@ -542,6 +590,7 @@ const GEMINI_TECHNIQUES = {
542
590
  check: (ctx) => {
543
591
  const data = settingsData(ctx);
544
592
  if (!data) return null;
593
+ if (isMcpOnlySettings(data)) return null;
545
594
  return Boolean(data.sandbox && (data.sandbox.mode || typeof data.sandbox === 'string'));
546
595
  },
547
596
  impact: 'high',
@@ -1017,6 +1066,7 @@ const GEMINI_TECHNIQUES = {
1017
1066
  check: (ctx) => {
1018
1067
  const data = settingsData(ctx);
1019
1068
  if (!data) return null;
1069
+ if (isMcpOnlySettings(data)) return null;
1020
1070
  const sandbox = data.sandbox;
1021
1071
  if (!sandbox) return false;
1022
1072
  const mode = typeof sandbox === 'string' ? sandbox : sandbox.mode;
@@ -1633,8 +1683,9 @@ const GEMINI_TECHNIQUES = {
1633
1683
  id: 'GM-J01',
1634
1684
  name: 'Rate limit/quota awareness documented',
1635
1685
  check: (ctx) => {
1636
- const gmd = geminiMd(ctx) || '';
1637
- return /\brate limit\b|\bquota\b|\brequests? per\b|\bcost\b|\btoken\b.*\blimit\b/i.test(gmd);
1686
+ const docs = docsBundle(ctx);
1687
+ if (!docs.trim()) return null;
1688
+ return /\brate[- ]?limit\b|\bquota\b|\brequests? per\b|\bcost\b|\btoken\b.*\blimit\b|\bthrottl|\b429\b/i.test(docs);
1638
1689
  },
1639
1690
  impact: 'medium',
1640
1691
  rating: 3,
@@ -1677,6 +1728,7 @@ const GEMINI_TECHNIQUES = {
1677
1728
  check: (ctx) => {
1678
1729
  const data = settingsData(ctx);
1679
1730
  if (!data) return null;
1731
+ if (isMcpOnlySettings(data)) return null;
1680
1732
  // Check if session/history settings are explicit
1681
1733
  return data.history !== undefined || data.session !== undefined || data.telemetry !== undefined;
1682
1734
  },
@@ -1836,11 +1888,11 @@ const GEMINI_TECHNIQUES = {
1836
1888
 
1837
1889
  geminiTokenUsageAwareness: {
1838
1890
  id: 'GM-K05',
1839
- name: 'Token usage awareness in GEMINI.md',
1891
+ name: 'Token usage awareness documented',
1840
1892
  check: (ctx) => {
1841
- const gmd = geminiMd(ctx) || '';
1842
- if (!gmd) return null;
1843
- return /\btoken\b|\bcontext window\b|\bcontext length\b|\b1M\b|\btruncat/i.test(gmd);
1893
+ const docs = docsBundle(ctx);
1894
+ if (!docs.trim()) return null;
1895
+ return /\btoken\b|\bcontext window\b|\bcontext length\b|\b1M\b|\btruncat/i.test(docs);
1844
1896
  },
1845
1897
  impact: 'low',
1846
1898
  rating: 2,
@@ -1863,7 +1915,12 @@ const GEMINI_TECHNIQUES = {
1863
1915
  name: 'Custom commands exist in .gemini/commands/',
1864
1916
  check: (ctx) => {
1865
1917
  const commandFiles = ctx.commandFiles ? ctx.commandFiles() : [];
1866
- return commandFiles.length > 0;
1918
+ if (commandFiles.length > 0) return true;
1919
+ // Custom commands are opt-in — only fire when the repo already has
1920
+ // a .gemini/commands/ directory (implying the user intends to use
1921
+ // commands but hasn't populated it).
1922
+ const hasCommandsDir = ctx.hasDir && ctx.hasDir('.gemini/commands');
1923
+ return hasCommandsDir ? false : null;
1867
1924
  },
1868
1925
  impact: 'medium',
1869
1926
  rating: 3,
@@ -2109,7 +2166,22 @@ const GEMINI_TECHNIQUES = {
2109
2166
  },
2110
2167
  geminiPropagationCompleteness: {
2111
2168
  id: 'GM-P03', name: 'No dangling surface references',
2112
- check: (ctx) => { const g = ctx.geminiMdContent(); if (!g) return null; const issues = []; if (/\bhooks?\b/i.test(g)) { const s = ctx.settingsJson(); if (!s || (!s.hooks && !s.BeforeTool && !s.AfterTool)) issues.push('hooks'); } if (/\bskills?\b/i.test(g) && !(ctx.hasDir ? ctx.hasDir('.gemini/skills') : false)) issues.push('skills'); if (/\bextensions?\b/i.test(g) && !(ctx.hasDir ? ctx.hasDir('.gemini/extensions') : false)) issues.push('extensions'); return issues.length === 0; },
2169
+ check: (ctx) => {
2170
+ const g = ctx.geminiMdContent();
2171
+ if (!g) return null;
2172
+ const issues = [];
2173
+ // Require specific Gemini-CLI vocabulary before asserting a dangling
2174
+ // reference. "skills" is too generic a word (appears in unrelated
2175
+ // product copy); require `.gemini/skills` path or `gemini skills`
2176
+ // phrasing. Same for hooks/extensions.
2177
+ if (/\.gemini\/hooks\b|\bgemini hooks?\b|\bhooksConfig\b|\bBeforeTool\b|\bAfterTool\b/i.test(g)) {
2178
+ const s = ctx.settingsJson();
2179
+ if (!s || !s.ok || (!s.data || (!s.data.hooks && !s.data.BeforeTool && !s.data.AfterTool))) issues.push('hooks');
2180
+ }
2181
+ if (/\.gemini\/skills\b|\bgemini skills?\b/i.test(g) && !(ctx.hasDir ? ctx.hasDir('.gemini/skills') : false)) issues.push('skills');
2182
+ if (/\.gemini\/extensions\b|\bgemini extensions?\b/i.test(g) && !(ctx.hasDir ? ctx.hasDir('.gemini/extensions') : false)) issues.push('extensions');
2183
+ return issues.length === 0;
2184
+ },
2113
2185
  impact: 'high', rating: 4, category: 'release-freshness',
2114
2186
  fix: 'Ensure all surfaces mentioned in GEMINI.md have corresponding definition files.',
2115
2187
  template: 'gemini-md', file: () => 'GEMINI.md', line: () => 1,