@nerviq/cli 1.18.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/LICENSE +23 -23
- package/README.md +2 -2
- package/bin/cli.js +131 -130
- package/package.json +2 -1
- package/src/activity.js +1039 -1039
- package/src/adoption-advisor.js +299 -299
- package/src/aider/config-parser.js +166 -166
- package/src/aider/context.js +158 -158
- package/src/aider/deep-review.js +316 -316
- package/src/aider/domain-packs.js +303 -303
- package/src/aider/freshness.js +93 -93
- package/src/aider/governance.js +253 -253
- package/src/aider/interactive.js +334 -334
- package/src/aider/mcp-packs.js +329 -329
- package/src/aider/patch.js +214 -214
- package/src/aider/plans.js +186 -186
- package/src/aider/premium.js +360 -360
- package/src/aider/setup.js +404 -404
- package/src/aider/techniques.js +16 -16
- package/src/analyze.js +951 -951
- package/src/anti-patterns.js +485 -485
- package/src/audit/instruction-files.js +180 -180
- package/src/audit/recommendations.js +577 -577
- package/src/auto-suggest.js +154 -154
- package/src/badge.js +13 -13
- package/src/behavioral-drift.js +801 -801
- package/src/benchmark.js +67 -67
- package/src/catalog.js +103 -103
- package/src/certification.js +128 -128
- package/src/codex/config-parser.js +183 -183
- package/src/codex/context.js +223 -223
- package/src/codex/deep-review.js +493 -493
- package/src/codex/domain-packs.js +394 -394
- package/src/codex/freshness.js +84 -84
- package/src/codex/governance.js +192 -192
- package/src/codex/interactive.js +618 -618
- package/src/codex/mcp-packs.js +914 -914
- package/src/codex/patch.js +209 -209
- package/src/codex/plans.js +251 -251
- package/src/codex/premium.js +614 -614
- package/src/codex/setup.js +591 -591
- package/src/context.js +320 -320
- package/src/continuous-ops.js +681 -681
- package/src/copilot/activity.js +309 -309
- package/src/copilot/deep-review.js +346 -346
- package/src/copilot/domain-packs.js +372 -372
- package/src/copilot/freshness.js +57 -57
- package/src/copilot/governance.js +222 -222
- package/src/copilot/interactive.js +406 -406
- package/src/copilot/mcp-packs.js +826 -826
- package/src/copilot/plans.js +253 -253
- package/src/copilot/premium.js +451 -451
- package/src/copilot/setup.js +488 -488
- package/src/cost-tracking.js +61 -61
- package/src/cursor/activity.js +301 -301
- package/src/cursor/config-parser.js +265 -265
- package/src/cursor/context.js +256 -256
- package/src/cursor/deep-review.js +334 -334
- package/src/cursor/domain-packs.js +368 -368
- package/src/cursor/freshness.js +65 -65
- package/src/cursor/governance.js +229 -229
- package/src/cursor/interactive.js +391 -391
- package/src/cursor/mcp-packs.js +828 -828
- package/src/cursor/plans.js +254 -254
- package/src/cursor/premium.js +469 -469
- package/src/cursor/setup.js +488 -488
- package/src/dashboard.js +493 -493
- package/src/deep-review.js +428 -428
- package/src/deprecation.js +98 -98
- package/src/diff-only.js +280 -280
- package/src/doctor.js +119 -119
- package/src/domain-pack-expansion.js +1033 -1033
- package/src/domain-packs.js +387 -387
- package/src/feedback.js +178 -178
- package/src/fix-engine.js +783 -783
- package/src/fix-prompts.js +122 -122
- package/src/formatters/sarif.js +115 -115
- package/src/freshness.js +74 -74
- package/src/gemini/config-parser.js +275 -275
- package/src/gemini/context.js +290 -221
- package/src/gemini/deep-review.js +559 -559
- package/src/gemini/domain-packs.js +393 -393
- package/src/gemini/freshness.js +66 -66
- package/src/gemini/governance.js +201 -201
- package/src/gemini/interactive.js +860 -860
- package/src/gemini/mcp-packs.js +915 -915
- package/src/gemini/plans.js +269 -269
- package/src/gemini/premium.js +760 -760
- package/src/gemini/setup.js +692 -692
- package/src/gemini/techniques.js +105 -33
- package/src/governance.js +72 -72
- package/src/harmony/add.js +68 -68
- package/src/harmony/advisor.js +333 -333
- package/src/harmony/canon.js +565 -565
- package/src/harmony/cli.js +591 -591
- package/src/harmony/drift.js +401 -401
- package/src/harmony/governance.js +313 -313
- package/src/harmony/memory.js +239 -239
- package/src/harmony/sync.js +475 -475
- package/src/harmony/watch.js +370 -370
- package/src/hook-validation.js +342 -342
- package/src/index.js +271 -271
- package/src/init.js +184 -184
- package/src/instruction-surfaces.js +185 -185
- package/src/integrations.js +144 -144
- package/src/interactive.js +118 -118
- package/src/locales/en.json +1 -1
- package/src/locales/es.json +1 -1
- package/src/mcp-packs.js +830 -830
- package/src/mcp-server.js +726 -726
- package/src/mcp-validation.js +337 -337
- package/src/nerviq-sync.json +7 -7
- package/src/opencode/config-parser.js +109 -109
- package/src/opencode/context.js +247 -247
- package/src/opencode/deep-review.js +313 -313
- package/src/opencode/domain-packs.js +262 -262
- package/src/opencode/freshness.js +66 -66
- package/src/opencode/governance.js +159 -159
- package/src/opencode/interactive.js +392 -392
- package/src/opencode/mcp-packs.js +705 -705
- package/src/opencode/patch.js +184 -184
- package/src/opencode/plans.js +231 -231
- package/src/opencode/premium.js +413 -413
- package/src/opencode/setup.js +449 -449
- package/src/opencode/techniques.js +27 -27
- package/src/operating-profile.js +574 -574
- package/src/org.js +152 -152
- package/src/permission-rules.js +218 -218
- package/src/plans.js +839 -839
- package/src/platform-change-manifest.js +86 -86
- package/src/plugins.js +110 -110
- package/src/policy-layers.js +210 -210
- package/src/profiles.js +124 -124
- package/src/prompt-injection.js +74 -74
- package/src/public-api.js +173 -173
- package/src/recommendation-rules.js +84 -84
- package/src/repo-archetype.js +386 -386
- package/src/secret-patterns.js +39 -39
- package/src/server.js +527 -527
- package/src/setup/analysis.js +607 -607
- package/src/setup/runtime.js +172 -172
- package/src/setup.js +677 -677
- package/src/shared/capabilities.js +194 -194
- package/src/source-urls.js +132 -132
- package/src/stack-checks.js +565 -565
- package/src/supplemental-checks.js +13 -13
- package/src/synergy/adaptive.js +261 -261
- package/src/synergy/compensation.js +137 -137
- package/src/synergy/evidence.js +193 -193
- package/src/synergy/learning.js +199 -199
- package/src/synergy/patterns.js +227 -227
- package/src/synergy/ranking.js +83 -83
- package/src/synergy/report.js +165 -165
- package/src/synergy/routing.js +146 -146
- package/src/techniques/api.js +407 -407
- package/src/techniques/automation.js +316 -316
- package/src/techniques/compliance.js +257 -257
- package/src/techniques/hygiene.js +294 -294
- package/src/techniques/instructions.js +243 -243
- package/src/techniques/observability.js +226 -226
- package/src/techniques/optimization.js +142 -142
- package/src/techniques/quality.js +318 -318
- package/src/techniques/security.js +237 -237
- package/src/techniques/shared.js +443 -443
- package/src/techniques/stacks.js +2294 -2294
- package/src/techniques/tools.js +106 -106
- package/src/techniques/workflow.js +413 -413
- package/src/techniques.js +81 -81
- package/src/terminology.js +73 -73
- package/src/token-estimate.js +35 -35
- package/src/usage-patterns.js +99 -99
- package/src/verification-metadata.js +145 -145
- package/src/watch.js +247 -247
- package/src/windsurf/activity.js +302 -302
- package/src/windsurf/config-parser.js +267 -267
- package/src/windsurf/context.js +249 -249
- package/src/windsurf/deep-review.js +337 -337
- package/src/windsurf/domain-packs.js +370 -370
- package/src/windsurf/freshness.js +36 -36
- package/src/windsurf/governance.js +231 -231
- package/src/windsurf/interactive.js +388 -388
- package/src/windsurf/mcp-packs.js +792 -792
- package/src/windsurf/plans.js +247 -247
- package/src/windsurf/premium.js +468 -468
- package/src/windsurf/setup.js +471 -471
- package/src/windsurf/techniques.js +17 -17
- package/src/workspace.js +375 -375
package/src/gemini/techniques.js
CHANGED
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
const os = require('os');
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const { GeminiProjectContext } = require('./context');
|
|
17
|
-
const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
|
|
18
|
-
const { attachSourceUrls } = require('../source-urls');
|
|
19
|
-
const { buildSupplementalChecks } = require('../supplemental-checks');
|
|
20
|
-
const { buildStackChecks } = require('../stack-checks');
|
|
21
|
-
const { resolveProjectStateReadPath } = require('../state-paths');
|
|
17
|
+
const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
|
|
18
|
+
const { attachSourceUrls } = require('../source-urls');
|
|
19
|
+
const { buildSupplementalChecks } = require('../supplemental-checks');
|
|
20
|
+
const { buildStackChecks } = require('../stack-checks');
|
|
21
|
+
const { resolveProjectStateReadPath } = require('../state-paths');
|
|
22
22
|
|
|
23
23
|
const GEMINI_SUPPLEMENTAL_SOURCE_URLS = {
|
|
24
24
|
'testing-strategy': 'https://geminicli.com/docs/get-started/',
|
|
@@ -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
|
-
|
|
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: '
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
423
|
-
|
|
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: '
|
|
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
|
|
487
|
-
|
|
488
|
-
|
|
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
|
|
1637
|
-
|
|
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
|
|
1891
|
+
name: 'Token usage awareness documented',
|
|
1840
1892
|
check: (ctx) => {
|
|
1841
|
-
const
|
|
1842
|
-
if (!
|
|
1843
|
-
return /\btoken\b|\bcontext window\b|\bcontext length\b|\b1M\b|\btruncat/i.test(
|
|
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
|
-
|
|
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,
|
|
@@ -2063,23 +2120,23 @@ const GEMINI_TECHNIQUES = {
|
|
|
2063
2120
|
},
|
|
2064
2121
|
|
|
2065
2122
|
// CP-08: O. Repeat-Usage Hygiene (3 checks)
|
|
2066
|
-
geminiSnapshotRetention: {
|
|
2067
|
-
id: 'GM-O01', name: 'At least one prior audit snapshot exists',
|
|
2068
|
-
check: (ctx) => { try { const fs = require('fs'); const p = resolveProjectStateReadPath(ctx.dir, 'snapshots', 'index.json'); if (!fs.existsSync(p)) return null; const e = JSON.parse(fs.readFileSync(p, 'utf8')); return Array.isArray(e) && e.length > 0; } catch { return null; } },
|
|
2123
|
+
geminiSnapshotRetention: {
|
|
2124
|
+
id: 'GM-O01', name: 'At least one prior audit snapshot exists',
|
|
2125
|
+
check: (ctx) => { try { const fs = require('fs'); const p = resolveProjectStateReadPath(ctx.dir, 'snapshots', 'index.json'); if (!fs.existsSync(p)) return null; const e = JSON.parse(fs.readFileSync(p, 'utf8')); return Array.isArray(e) && e.length > 0; } catch { return null; } },
|
|
2069
2126
|
impact: 'medium', rating: 3, category: 'repeat-usage',
|
|
2070
2127
|
fix: 'Run `npx nerviq --platform gemini --snapshot` to save your first snapshot.',
|
|
2071
2128
|
template: null, file: () => null, line: () => null,
|
|
2072
2129
|
},
|
|
2073
|
-
geminiFeedbackLoopHealth: {
|
|
2074
|
-
id: 'GM-O02', name: 'Feedback loop functional when feedback submitted',
|
|
2075
|
-
check: (ctx) => { try { const fs = require('fs'); const p = resolveProjectStateReadPath(ctx.dir, 'outcomes', 'index.json'); if (!fs.existsSync(p)) return null; const e = JSON.parse(fs.readFileSync(p, 'utf8')); return Array.isArray(e) && e.length > 0; } catch { return null; } },
|
|
2130
|
+
geminiFeedbackLoopHealth: {
|
|
2131
|
+
id: 'GM-O02', name: 'Feedback loop functional when feedback submitted',
|
|
2132
|
+
check: (ctx) => { try { const fs = require('fs'); const p = resolveProjectStateReadPath(ctx.dir, 'outcomes', 'index.json'); if (!fs.existsSync(p)) return null; const e = JSON.parse(fs.readFileSync(p, 'utf8')); return Array.isArray(e) && e.length > 0; } catch { return null; } },
|
|
2076
2133
|
impact: 'medium', rating: 3, category: 'repeat-usage',
|
|
2077
2134
|
fix: 'Submit feedback using `npx nerviq --platform gemini feedback`.',
|
|
2078
2135
|
template: null, file: () => null, line: () => null,
|
|
2079
2136
|
},
|
|
2080
|
-
geminiTrendDataAvailability: {
|
|
2081
|
-
id: 'GM-O03', name: 'Trend data computable (2+ snapshots)',
|
|
2082
|
-
check: (ctx) => { try { const fs = require('fs'); const p = resolveProjectStateReadPath(ctx.dir, 'snapshots', 'index.json'); if (!fs.existsSync(p)) return null; const e = JSON.parse(fs.readFileSync(p, 'utf8')); return (Array.isArray(e) ? e : []).filter(x => x.snapshotKind === 'audit').length >= 2; } catch { return null; } },
|
|
2137
|
+
geminiTrendDataAvailability: {
|
|
2138
|
+
id: 'GM-O03', name: 'Trend data computable (2+ snapshots)',
|
|
2139
|
+
check: (ctx) => { try { const fs = require('fs'); const p = resolveProjectStateReadPath(ctx.dir, 'snapshots', 'index.json'); if (!fs.existsSync(p)) return null; const e = JSON.parse(fs.readFileSync(p, 'utf8')); return (Array.isArray(e) ? e : []).filter(x => x.snapshotKind === 'audit').length >= 2; } catch { return null; } },
|
|
2083
2140
|
impact: 'low', rating: 2, category: 'repeat-usage',
|
|
2084
2141
|
fix: 'Run at least 2 audits with --snapshot for trend tracking.',
|
|
2085
2142
|
template: null, file: () => null, line: () => null,
|
|
@@ -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) => {
|
|
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,
|
package/src/governance.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
const { DOMAIN_PACKS } = require('./domain-packs');
|
|
2
|
-
const { MCP_PACKS, mergeMcpServers, normalizeMcpPackKeys } = require('./mcp-packs');
|
|
3
|
-
const { getCodexGovernanceSummary } = require('./codex/governance');
|
|
4
|
-
const { formatTerminologyLines } = require('./terminology');
|
|
1
|
+
const { DOMAIN_PACKS } = require('./domain-packs');
|
|
2
|
+
const { MCP_PACKS, mergeMcpServers, normalizeMcpPackKeys } = require('./mcp-packs');
|
|
3
|
+
const { getCodexGovernanceSummary } = require('./codex/governance');
|
|
4
|
+
const { formatTerminologyLines } = require('./terminology');
|
|
5
5
|
|
|
6
6
|
const PERMISSION_PROFILES = [
|
|
7
7
|
{
|
|
@@ -104,19 +104,19 @@ const HOOK_REGISTRY = [
|
|
|
104
104
|
dryRunExample: 'Edit a catalog file and verify duplicate check runs without blocking.',
|
|
105
105
|
rollbackPath: 'Remove the PostToolUse hook entry from settings.',
|
|
106
106
|
},
|
|
107
|
-
{
|
|
108
|
-
key: 'injection-defense',
|
|
109
|
-
file: '.claude/hooks/injection-defense.js',
|
|
110
|
-
triggerPoint: 'PostToolUse',
|
|
111
|
-
matcher: 'WebFetch|WebSearch|Read|Grep|Glob|mcp__.*',
|
|
112
|
-
purpose: 'Scans external content flows for common prompt injection patterns and logs suspicious findings.',
|
|
113
|
-
filesTouched: ['.claude/logs/prompt-injection-alerts.log'],
|
|
114
|
-
sideEffects: ['Appends an alert line when suspicious external content is detected.'],
|
|
115
|
-
risk: 'low',
|
|
116
|
-
riskLevel: 'low',
|
|
117
|
-
dryRunExample: 'Run a WebFetch or MCP-backed tool call and verify suspicious content is logged for review.',
|
|
118
|
-
rollbackPath: 'Remove the PostToolUse hook entry from settings.',
|
|
119
|
-
},
|
|
107
|
+
{
|
|
108
|
+
key: 'injection-defense',
|
|
109
|
+
file: '.claude/hooks/injection-defense.js',
|
|
110
|
+
triggerPoint: 'PostToolUse',
|
|
111
|
+
matcher: 'WebFetch|WebSearch|Read|Grep|Glob|mcp__.*',
|
|
112
|
+
purpose: 'Scans external content flows for common prompt injection patterns and logs suspicious findings.',
|
|
113
|
+
filesTouched: ['.claude/logs/prompt-injection-alerts.log'],
|
|
114
|
+
sideEffects: ['Appends an alert line when suspicious external content is detected.'],
|
|
115
|
+
risk: 'low',
|
|
116
|
+
riskLevel: 'low',
|
|
117
|
+
dryRunExample: 'Run a WebFetch or MCP-backed tool call and verify suspicious content is logged for review.',
|
|
118
|
+
rollbackPath: 'Remove the PostToolUse hook entry from settings.',
|
|
119
|
+
},
|
|
120
120
|
{
|
|
121
121
|
key: 'trust-drift-check',
|
|
122
122
|
file: '.claude/hooks/trust-drift-check.sh',
|
|
@@ -299,24 +299,24 @@ function buildHookConfig(hookFiles, profileKey) {
|
|
|
299
299
|
return {};
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
-
// Detect hook runtime: .js files use node, .sh files use bash
|
|
303
|
-
const hookCommand = (file) => {
|
|
304
|
-
if (file.endsWith('.js')) return `node .claude/hooks/${file}`;
|
|
305
|
-
return `bash .claude/hooks/${file}`;
|
|
306
|
-
};
|
|
307
|
-
const isSecrets = (f) => f === 'protect-secrets.sh' || f === 'protect-secrets.js';
|
|
308
|
-
const isSession = (f) => f === 'session-start.sh' || f === 'session-start.js';
|
|
309
|
-
const isInjection = (f) => f === 'injection-defense.sh' || f === 'injection-defense.js';
|
|
310
|
-
|
|
311
|
-
const hookConfig = {
|
|
312
|
-
PostToolUse: [{
|
|
313
|
-
matcher: 'Write|Edit',
|
|
314
|
-
hooks: uniqueFiles
|
|
315
|
-
.filter(file => !isSecrets(file) && !isSession(file) && !isInjection(file))
|
|
316
|
-
.map(file => ({
|
|
317
|
-
type: 'command',
|
|
318
|
-
command: hookCommand(file),
|
|
319
|
-
timeout: 10,
|
|
302
|
+
// Detect hook runtime: .js files use node, .sh files use bash
|
|
303
|
+
const hookCommand = (file) => {
|
|
304
|
+
if (file.endsWith('.js')) return `node .claude/hooks/${file}`;
|
|
305
|
+
return `bash .claude/hooks/${file}`;
|
|
306
|
+
};
|
|
307
|
+
const isSecrets = (f) => f === 'protect-secrets.sh' || f === 'protect-secrets.js';
|
|
308
|
+
const isSession = (f) => f === 'session-start.sh' || f === 'session-start.js';
|
|
309
|
+
const isInjection = (f) => f === 'injection-defense.sh' || f === 'injection-defense.js';
|
|
310
|
+
|
|
311
|
+
const hookConfig = {
|
|
312
|
+
PostToolUse: [{
|
|
313
|
+
matcher: 'Write|Edit',
|
|
314
|
+
hooks: uniqueFiles
|
|
315
|
+
.filter(file => !isSecrets(file) && !isSession(file) && !isInjection(file))
|
|
316
|
+
.map(file => ({
|
|
317
|
+
type: 'command',
|
|
318
|
+
command: hookCommand(file),
|
|
319
|
+
timeout: 10,
|
|
320
320
|
})),
|
|
321
321
|
}],
|
|
322
322
|
};
|
|
@@ -334,29 +334,29 @@ function buildHookConfig(hookFiles, profileKey) {
|
|
|
334
334
|
}
|
|
335
335
|
|
|
336
336
|
const sessionFile = uniqueFiles.find(isSession);
|
|
337
|
-
if (sessionFile) {
|
|
338
|
-
hookConfig.SessionStart = [{
|
|
339
|
-
matcher: '*',
|
|
340
|
-
hooks: [{
|
|
341
|
-
type: 'command',
|
|
337
|
+
if (sessionFile) {
|
|
338
|
+
hookConfig.SessionStart = [{
|
|
339
|
+
matcher: '*',
|
|
340
|
+
hooks: [{
|
|
341
|
+
type: 'command',
|
|
342
342
|
command: hookCommand(sessionFile),
|
|
343
343
|
timeout: 5,
|
|
344
|
-
}],
|
|
345
|
-
}];
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const injectionFile = uniqueFiles.find(isInjection);
|
|
349
|
-
if (injectionFile) {
|
|
350
|
-
hookConfig.PostToolUse = hookConfig.PostToolUse || [];
|
|
351
|
-
hookConfig.PostToolUse.push({
|
|
352
|
-
matcher: 'WebFetch|WebSearch|Read|Grep|Glob|mcp__.*',
|
|
353
|
-
hooks: [{
|
|
354
|
-
type: 'command',
|
|
355
|
-
command: hookCommand(injectionFile),
|
|
356
|
-
timeout: 5,
|
|
357
|
-
}],
|
|
358
|
-
});
|
|
359
|
-
}
|
|
344
|
+
}],
|
|
345
|
+
}];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const injectionFile = uniqueFiles.find(isInjection);
|
|
349
|
+
if (injectionFile) {
|
|
350
|
+
hookConfig.PostToolUse = hookConfig.PostToolUse || [];
|
|
351
|
+
hookConfig.PostToolUse.push({
|
|
352
|
+
matcher: 'WebFetch|WebSearch|Read|Grep|Glob|mcp__.*',
|
|
353
|
+
hooks: [{
|
|
354
|
+
type: 'command',
|
|
355
|
+
command: hookCommand(injectionFile),
|
|
356
|
+
timeout: 5,
|
|
357
|
+
}],
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
360
|
|
|
361
361
|
if ((hookConfig.PostToolUse[0].hooks || []).length === 0) {
|
|
362
362
|
delete hookConfig.PostToolUse;
|
|
@@ -421,15 +421,15 @@ function printGovernanceSummary(summary, options = {}) {
|
|
|
421
421
|
console.log('');
|
|
422
422
|
console.log(` nerviq ${summary.platformLabel.toLowerCase()} governance`);
|
|
423
423
|
console.log(' ═══════════════════════════════════════');
|
|
424
|
-
console.log(` Safe defaults, hook transparency, and pilot guidance for ${summary.platformLabel}.`);
|
|
425
|
-
console.log('');
|
|
426
|
-
|
|
427
|
-
for (const line of formatTerminologyLines(['governance', 'hooks', 'denyRules', 'mcp'])) {
|
|
428
|
-
console.log(line);
|
|
429
|
-
}
|
|
430
|
-
console.log('');
|
|
431
|
-
|
|
432
|
-
console.log(' Permission Profiles');
|
|
424
|
+
console.log(` Safe defaults, hook transparency, and pilot guidance for ${summary.platformLabel}.`);
|
|
425
|
+
console.log('');
|
|
426
|
+
|
|
427
|
+
for (const line of formatTerminologyLines(['governance', 'hooks', 'denyRules', 'mcp'])) {
|
|
428
|
+
console.log(line);
|
|
429
|
+
}
|
|
430
|
+
console.log('');
|
|
431
|
+
|
|
432
|
+
console.log(' Permission Profiles');
|
|
433
433
|
for (const profile of summary.permissionProfiles) {
|
|
434
434
|
console.log(` - ${profile.label} [${profile.risk}]`);
|
|
435
435
|
console.log(` ${profile.useWhen}`);
|
|
@@ -590,13 +590,13 @@ function renderGovernanceMarkdown(summary) {
|
|
|
590
590
|
return lines.join('\n');
|
|
591
591
|
}
|
|
592
592
|
|
|
593
|
-
module.exports = {
|
|
594
|
-
PERMISSION_PROFILES,
|
|
595
|
-
HOOK_REGISTRY,
|
|
596
|
-
POLICY_PACKS,
|
|
597
|
-
getPermissionProfile,
|
|
598
|
-
isWritableProfile,
|
|
599
|
-
ensureWritableProfile,
|
|
593
|
+
module.exports = {
|
|
594
|
+
PERMISSION_PROFILES,
|
|
595
|
+
HOOK_REGISTRY,
|
|
596
|
+
POLICY_PACKS,
|
|
597
|
+
getPermissionProfile,
|
|
598
|
+
isWritableProfile,
|
|
599
|
+
ensureWritableProfile,
|
|
600
600
|
buildSettingsForProfile,
|
|
601
601
|
getGovernanceSummary,
|
|
602
602
|
printGovernanceSummary,
|
package/src/harmony/add.js
CHANGED
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Platform Addition Wizard
|
|
3
|
-
* Helps users add a new platform config to their project.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const fs = require('fs');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
const { detectActivePlatforms, PLATFORM_SIGNATURES } = require('./canon');
|
|
9
|
-
const { applyHarmonySync } = require('./sync');
|
|
10
|
-
|
|
11
|
-
const PLATFORM_BOOTSTRAPS = {
|
|
12
|
-
claude: { files: [{ path: 'CLAUDE.md', content: '# Project Instructions\n\nAdd your Claude Code instructions here.\n' }] },
|
|
13
|
-
codex: { files: [{ path: 'AGENTS.md', content: '# Agents Instructions\n\nAdd your Codex instructions here.\n' }] },
|
|
14
|
-
gemini: { files: [{ path: 'GEMINI.md', content: '# Gemini Instructions\n\nAdd your Gemini CLI instructions here.\n' }] },
|
|
15
|
-
copilot: { files: [{ path: '.github/copilot-instructions.md', content: '# Copilot Instructions\n\nAdd your GitHub Copilot instructions here.\n' }] },
|
|
16
|
-
cursor: { files: [{ path: '.cursorrules', content: '# Cursor Rules\n\nAdd your Cursor rules here.\n' }] },
|
|
17
|
-
windsurf: { files: [{ path: '.windsurfrules', content: '# Windsurf Rules\n\nAdd your Windsurf rules here.\n' }] },
|
|
18
|
-
aider: { files: [{ path: '.aider.conf.yml', content: '# Aider Configuration\n# See: https://aider.chat/docs/config/aider_conf.html\n' }] },
|
|
19
|
-
opencode: { files: [{ path: 'opencode.json', content: '{\n "instructions": "Add your OpenCode instructions here."\n}\n' }] },
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
function addPlatform(dir, platformKey) {
|
|
23
|
-
// Validate platform
|
|
24
|
-
if (!PLATFORM_SIGNATURES[platformKey]) {
|
|
25
|
-
return { success: false, error: `Unknown platform: ${platformKey}. Available: ${Object.keys(PLATFORM_SIGNATURES).join(', ')}` };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Check if already active
|
|
29
|
-
const active = detectActivePlatforms(dir);
|
|
30
|
-
const alreadyActive = active.find(p => p.platform === platformKey);
|
|
31
|
-
if (alreadyActive) {
|
|
32
|
-
return { success: false, error: `${platformKey} is already active in this project.` };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const beforeCount = active.length;
|
|
36
|
-
const bootstrap = PLATFORM_BOOTSTRAPS[platformKey];
|
|
37
|
-
const created = [];
|
|
38
|
-
|
|
39
|
-
// Create bootstrap files
|
|
40
|
-
for (const file of bootstrap.files) {
|
|
41
|
-
const fullPath = path.join(dir, file.path);
|
|
42
|
-
if (fs.existsSync(fullPath)) continue;
|
|
43
|
-
const dirName = path.dirname(fullPath);
|
|
44
|
-
fs.mkdirSync(dirName, { recursive: true });
|
|
45
|
-
fs.writeFileSync(fullPath, file.content, 'utf8');
|
|
46
|
-
created.push(file.path);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Run harmony sync to populate managed blocks
|
|
50
|
-
let syncResult = null;
|
|
51
|
-
try {
|
|
52
|
-
syncResult = applyHarmonySync(dir);
|
|
53
|
-
} catch { /* sync is optional */ }
|
|
54
|
-
|
|
55
|
-
const afterActive = detectActivePlatforms(dir);
|
|
56
|
-
const afterCount = afterActive.length;
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
success: true,
|
|
60
|
-
platform: platformKey,
|
|
61
|
-
created,
|
|
62
|
-
beforeCount,
|
|
63
|
-
afterCount,
|
|
64
|
-
syncApplied: syncResult ? syncResult.applied.length : 0,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
module.exports = { addPlatform, PLATFORM_BOOTSTRAPS };
|
|
1
|
+
/**
|
|
2
|
+
* Platform Addition Wizard
|
|
3
|
+
* Helps users add a new platform config to their project.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { detectActivePlatforms, PLATFORM_SIGNATURES } = require('./canon');
|
|
9
|
+
const { applyHarmonySync } = require('./sync');
|
|
10
|
+
|
|
11
|
+
const PLATFORM_BOOTSTRAPS = {
|
|
12
|
+
claude: { files: [{ path: 'CLAUDE.md', content: '# Project Instructions\n\nAdd your Claude Code instructions here.\n' }] },
|
|
13
|
+
codex: { files: [{ path: 'AGENTS.md', content: '# Agents Instructions\n\nAdd your Codex instructions here.\n' }] },
|
|
14
|
+
gemini: { files: [{ path: 'GEMINI.md', content: '# Gemini Instructions\n\nAdd your Gemini CLI instructions here.\n' }] },
|
|
15
|
+
copilot: { files: [{ path: '.github/copilot-instructions.md', content: '# Copilot Instructions\n\nAdd your GitHub Copilot instructions here.\n' }] },
|
|
16
|
+
cursor: { files: [{ path: '.cursorrules', content: '# Cursor Rules\n\nAdd your Cursor rules here.\n' }] },
|
|
17
|
+
windsurf: { files: [{ path: '.windsurfrules', content: '# Windsurf Rules\n\nAdd your Windsurf rules here.\n' }] },
|
|
18
|
+
aider: { files: [{ path: '.aider.conf.yml', content: '# Aider Configuration\n# See: https://aider.chat/docs/config/aider_conf.html\n' }] },
|
|
19
|
+
opencode: { files: [{ path: 'opencode.json', content: '{\n "instructions": "Add your OpenCode instructions here."\n}\n' }] },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function addPlatform(dir, platformKey) {
|
|
23
|
+
// Validate platform
|
|
24
|
+
if (!PLATFORM_SIGNATURES[platformKey]) {
|
|
25
|
+
return { success: false, error: `Unknown platform: ${platformKey}. Available: ${Object.keys(PLATFORM_SIGNATURES).join(', ')}` };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check if already active
|
|
29
|
+
const active = detectActivePlatforms(dir);
|
|
30
|
+
const alreadyActive = active.find(p => p.platform === platformKey);
|
|
31
|
+
if (alreadyActive) {
|
|
32
|
+
return { success: false, error: `${platformKey} is already active in this project.` };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const beforeCount = active.length;
|
|
36
|
+
const bootstrap = PLATFORM_BOOTSTRAPS[platformKey];
|
|
37
|
+
const created = [];
|
|
38
|
+
|
|
39
|
+
// Create bootstrap files
|
|
40
|
+
for (const file of bootstrap.files) {
|
|
41
|
+
const fullPath = path.join(dir, file.path);
|
|
42
|
+
if (fs.existsSync(fullPath)) continue;
|
|
43
|
+
const dirName = path.dirname(fullPath);
|
|
44
|
+
fs.mkdirSync(dirName, { recursive: true });
|
|
45
|
+
fs.writeFileSync(fullPath, file.content, 'utf8');
|
|
46
|
+
created.push(file.path);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Run harmony sync to populate managed blocks
|
|
50
|
+
let syncResult = null;
|
|
51
|
+
try {
|
|
52
|
+
syncResult = applyHarmonySync(dir);
|
|
53
|
+
} catch { /* sync is optional */ }
|
|
54
|
+
|
|
55
|
+
const afterActive = detectActivePlatforms(dir);
|
|
56
|
+
const afterCount = afterActive.length;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
success: true,
|
|
60
|
+
platform: platformKey,
|
|
61
|
+
created,
|
|
62
|
+
beforeCount,
|
|
63
|
+
afterCount,
|
|
64
|
+
syncApplied: syncResult ? syncResult.applied.length : 0,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { addPlatform, PLATFORM_BOOTSTRAPS };
|