@nerviq/cli 0.0.1 → 0.9.0-beta.2
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/CHANGELOG.md +181 -0
- package/LICENSE +21 -0
- package/README.md +447 -0
- package/bin/cli.js +749 -0
- package/content/case-study-template.md +91 -0
- package/content/claims-governance.md +37 -0
- package/content/claude-code/audit-repo/SKILL.md +20 -0
- package/content/claude-native-integration.md +60 -0
- package/content/devto-article.json +9 -0
- package/content/launch-posts.md +226 -0
- package/content/pilot-rollout-kit.md +30 -0
- package/content/release-checklist.md +31 -0
- package/package.json +53 -4
- package/src/activity.js +529 -0
- package/src/aider/activity.js +226 -0
- package/src/aider/config-parser.js +166 -0
- package/src/aider/context.js +158 -0
- package/src/aider/deep-review.js +316 -0
- package/src/aider/domain-packs.js +278 -0
- package/src/aider/freshness.js +168 -0
- package/src/aider/governance.js +253 -0
- package/src/aider/interactive.js +334 -0
- package/src/aider/mcp-packs.js +98 -0
- package/src/aider/patch.js +214 -0
- package/src/aider/plans.js +186 -0
- package/src/aider/premium.js +360 -0
- package/src/aider/setup.js +404 -0
- package/src/aider/techniques.js +1323 -0
- package/src/analyze.js +821 -0
- package/src/audit.js +1003 -0
- package/src/badge.js +13 -0
- package/src/benchmark.js +339 -0
- package/src/claudex-sync.json +7 -0
- package/src/codex/activity.js +324 -0
- package/src/codex/config-parser.js +183 -0
- package/src/codex/context.js +221 -0
- package/src/codex/deep-review.js +493 -0
- package/src/codex/domain-packs.js +372 -0
- package/src/codex/freshness.js +167 -0
- package/src/codex/governance.js +192 -0
- package/src/codex/interactive.js +618 -0
- package/src/codex/mcp-packs.js +660 -0
- package/src/codex/patch.js +209 -0
- package/src/codex/plans.js +251 -0
- package/src/codex/premium.js +614 -0
- package/src/codex/setup.js +603 -0
- package/src/codex/techniques.js +2649 -0
- package/src/context.js +272 -0
- package/src/copilot/activity.js +309 -0
- package/src/copilot/config-parser.js +226 -0
- package/src/copilot/context.js +197 -0
- package/src/copilot/deep-review.js +346 -0
- package/src/copilot/domain-packs.js +350 -0
- package/src/copilot/freshness.js +197 -0
- package/src/copilot/governance.js +222 -0
- package/src/copilot/interactive.js +406 -0
- package/src/copilot/mcp-packs.js +572 -0
- package/src/copilot/patch.js +238 -0
- package/src/copilot/plans.js +253 -0
- package/src/copilot/premium.js +450 -0
- package/src/copilot/setup.js +488 -0
- package/src/copilot/techniques.js +1822 -0
- package/src/cursor/activity.js +301 -0
- package/src/cursor/config-parser.js +265 -0
- package/src/cursor/context.js +236 -0
- package/src/cursor/deep-review.js +334 -0
- package/src/cursor/domain-packs.js +346 -0
- package/src/cursor/freshness.js +214 -0
- package/src/cursor/governance.js +229 -0
- package/src/cursor/interactive.js +391 -0
- package/src/cursor/mcp-packs.js +571 -0
- package/src/cursor/patch.js +243 -0
- package/src/cursor/plans.js +254 -0
- package/src/cursor/premium.js +468 -0
- package/src/cursor/setup.js +488 -0
- package/src/cursor/techniques.js +1786 -0
- package/src/deep-review.js +345 -0
- package/src/domain-packs.js +364 -0
- package/src/formatters/sarif.js +115 -0
- package/src/gemini/activity.js +402 -0
- package/src/gemini/config-parser.js +275 -0
- package/src/gemini/context.js +221 -0
- package/src/gemini/deep-review.js +559 -0
- package/src/gemini/domain-packs.js +371 -0
- package/src/gemini/freshness.js +204 -0
- package/src/gemini/governance.js +201 -0
- package/src/gemini/interactive.js +860 -0
- package/src/gemini/mcp-packs.js +658 -0
- package/src/gemini/patch.js +229 -0
- package/src/gemini/plans.js +269 -0
- package/src/gemini/premium.js +759 -0
- package/src/gemini/setup.js +692 -0
- package/src/gemini/techniques.js +2084 -0
- package/src/governance.js +523 -0
- package/src/harmony/advisor.js +383 -0
- package/src/harmony/audit.js +303 -0
- package/src/harmony/canon.js +444 -0
- package/src/harmony/cli.js +331 -0
- package/src/harmony/drift.js +401 -0
- package/src/harmony/governance.js +313 -0
- package/src/harmony/memory.js +238 -0
- package/src/harmony/sync.js +458 -0
- package/src/harmony/watch.js +336 -0
- package/src/index.js +256 -0
- package/src/insights.js +119 -0
- package/src/interactive.js +118 -0
- package/src/mcp-packs.js +597 -0
- package/src/opencode/activity.js +286 -0
- package/src/opencode/config-parser.js +109 -0
- package/src/opencode/context.js +247 -0
- package/src/opencode/deep-review.js +313 -0
- package/src/opencode/domain-packs.js +240 -0
- package/src/opencode/freshness.js +158 -0
- package/src/opencode/governance.js +159 -0
- package/src/opencode/interactive.js +392 -0
- package/src/opencode/mcp-packs.js +474 -0
- package/src/opencode/patch.js +184 -0
- package/src/opencode/plans.js +231 -0
- package/src/opencode/premium.js +413 -0
- package/src/opencode/setup.js +449 -0
- package/src/opencode/techniques.js +1713 -0
- package/src/plans.js +655 -0
- package/src/secret-patterns.js +30 -0
- package/src/setup.js +1274 -0
- package/src/synergy/adaptive.js +261 -0
- package/src/synergy/compensation.js +156 -0
- package/src/synergy/evidence.js +193 -0
- package/src/synergy/learning.js +184 -0
- package/src/synergy/patterns.js +227 -0
- package/src/synergy/ranking.js +83 -0
- package/src/synergy/report.js +163 -0
- package/src/synergy/routing.js +152 -0
- package/src/techniques.js +1354 -0
- package/src/watch.js +229 -0
- package/src/windsurf/activity.js +302 -0
- package/src/windsurf/config-parser.js +267 -0
- package/src/windsurf/context.js +249 -0
- package/src/windsurf/deep-review.js +337 -0
- package/src/windsurf/domain-packs.js +348 -0
- package/src/windsurf/freshness.js +215 -0
- package/src/windsurf/governance.js +231 -0
- package/src/windsurf/interactive.js +388 -0
- package/src/windsurf/mcp-packs.js +535 -0
- package/src/windsurf/patch.js +231 -0
- package/src/windsurf/plans.js +247 -0
- package/src/windsurf/premium.js +467 -0
- package/src/windsurf/setup.js +471 -0
- package/src/windsurf/techniques.js +1758 -0
|
@@ -0,0 +1,2084 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI techniques module — CHECK CATALOG
|
|
3
|
+
*
|
|
4
|
+
* 68 checks across 12 categories:
|
|
5
|
+
* v0.1 (40): A. Instructions, B. Config, C. Trust & Safety, D. Hooks, E. MCP, F. Sandbox & Policy
|
|
6
|
+
* v0.5 (54): G. Skills & Agents, H. CI & Automation, I. Extensions
|
|
7
|
+
* v1.0 (68): J. Review & Workflow, K. Quality Deep, L. Commands
|
|
8
|
+
*
|
|
9
|
+
* Each check: { id, name, check(ctx), impact, rating, category, fix, template, file(), line() }
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { GeminiProjectContext } = require('./context');
|
|
15
|
+
const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
|
|
16
|
+
|
|
17
|
+
// ─── Shared helpers ─────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const FILLER_PATTERNS = [
|
|
20
|
+
/\bbe helpful\b/i,
|
|
21
|
+
/\bbe accurate\b/i,
|
|
22
|
+
/\bbe concise\b/i,
|
|
23
|
+
/\balways do your best\b/i,
|
|
24
|
+
/\bmaintain high quality\b/i,
|
|
25
|
+
/\bwrite clean code\b/i,
|
|
26
|
+
/\bfollow best practices\b/i,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const JUSTIFICATION_PATTERNS = /\bbecause\b|\bwhy\b|\bjustif(?:y|ication)\b|\btemporary\b|\bintentional\b|\bdocumented\b|\bair[- ]?gapped\b|\binternal only\b|\bephemeral\b|\bci only\b/i;
|
|
30
|
+
|
|
31
|
+
function countSections(markdown) {
|
|
32
|
+
return (markdown.match(/^##\s+/gm) || []).length;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function firstLineMatching(text, matcher) {
|
|
36
|
+
if (!text) return null;
|
|
37
|
+
const lines = text.split(/\r?\n/);
|
|
38
|
+
for (let index = 0; index < lines.length; index++) {
|
|
39
|
+
const line = lines[index];
|
|
40
|
+
if (typeof matcher === 'string' && line.includes(matcher)) return index + 1;
|
|
41
|
+
if (matcher instanceof RegExp && matcher.test(line)) {
|
|
42
|
+
matcher.lastIndex = 0;
|
|
43
|
+
return index + 1;
|
|
44
|
+
}
|
|
45
|
+
if (typeof matcher === 'function' && matcher(line, index + 1)) return index + 1;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function findFillerLine(content) {
|
|
51
|
+
return firstLineMatching(content, (line) => FILLER_PATTERNS.some((p) => p.test(line)));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function findSecretLine(content) {
|
|
55
|
+
const lines = content.split(/\r?\n/);
|
|
56
|
+
for (let index = 0; index < lines.length; index++) {
|
|
57
|
+
const matched = EMBEDDED_SECRET_PATTERNS.some((pattern) => {
|
|
58
|
+
pattern.lastIndex = 0;
|
|
59
|
+
return pattern.test(lines[index]);
|
|
60
|
+
});
|
|
61
|
+
if (matched) return index + 1;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function geminiMd(ctx) {
|
|
67
|
+
return ctx.geminiMdContent ? ctx.geminiMdContent() : (ctx.fileContent('GEMINI.md') || null);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function settingsRaw(ctx) {
|
|
71
|
+
return ctx.fileContent('.gemini/settings.json') || '';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function settingsData(ctx) {
|
|
75
|
+
const result = ctx.settingsJson();
|
|
76
|
+
return result && result.ok ? result.data : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function docsBundle(ctx) {
|
|
80
|
+
const gmd = geminiMd(ctx) || '';
|
|
81
|
+
const readme = ctx.fileContent('README.md') || '';
|
|
82
|
+
return `${gmd}\n${readme}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function expectedVerificationCategories(ctx) {
|
|
86
|
+
const categories = new Set();
|
|
87
|
+
const pkg = ctx.jsonFile ? ctx.jsonFile('package.json') : null;
|
|
88
|
+
const scripts = pkg && pkg.scripts ? pkg.scripts : {};
|
|
89
|
+
if (scripts.test) categories.add('test');
|
|
90
|
+
if (scripts.lint) categories.add('lint');
|
|
91
|
+
if (scripts.build) categories.add('build');
|
|
92
|
+
if (ctx.fileContent('Cargo.toml')) { categories.add('test'); categories.add('build'); }
|
|
93
|
+
if (ctx.fileContent('go.mod')) { categories.add('test'); categories.add('build'); }
|
|
94
|
+
if (ctx.fileContent('pyproject.toml') || ctx.fileContent('requirements.txt')) categories.add('test');
|
|
95
|
+
if (ctx.fileContent('Makefile') || ctx.fileContent('justfile')) categories.add('build');
|
|
96
|
+
return [...categories];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function hasCommandMention(content, category) {
|
|
100
|
+
if (category === 'test') {
|
|
101
|
+
return /\bnpm test\b|\bnpm run test\b|\bpnpm test\b|\byarn test\b|\bvitest\b|\bjest\b|\bpytest\b|\bgo test\b|\bcargo test\b|\bmake test\b/i.test(content);
|
|
102
|
+
}
|
|
103
|
+
if (category === 'lint') {
|
|
104
|
+
return /\bnpm run lint\b|\bpnpm lint\b|\byarn lint\b|\beslint\b|\bprettier\b|\bruff\b|\bclippy\b|\bgolangci-lint\b|\bmake lint\b/i.test(content);
|
|
105
|
+
}
|
|
106
|
+
if (category === 'build') {
|
|
107
|
+
return /\bnpm run build\b|\bpnpm build\b|\byarn build\b|\btsc\b|\bvite build\b|\bnext build\b|\bcargo build\b|\bgo build\b|\bmake\b/i.test(content);
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function hasArchitecture(content) {
|
|
113
|
+
return /```mermaid|flowchart\b|graph\s+(TD|LR|RL|BT)\b|##\s+Architecture\b|##\s+Project Map\b|##\s+Structure\b/i.test(content);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function extractImportRefs(content) {
|
|
117
|
+
const refs = [];
|
|
118
|
+
const regex = /@([^\s@]+\.\w+)/g;
|
|
119
|
+
let match;
|
|
120
|
+
while ((match = regex.exec(content)) !== null) {
|
|
121
|
+
refs.push({ ref: match[1], line: content.slice(0, match.index).split('\n').length });
|
|
122
|
+
}
|
|
123
|
+
return refs;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function repoLooksRegulated(ctx) {
|
|
127
|
+
const filenames = (ctx.files || []).join('\n');
|
|
128
|
+
const pkg = ctx.fileContent('package.json') || '';
|
|
129
|
+
const readme = ctx.fileContent('README.md') || '';
|
|
130
|
+
const combined = `${filenames}\n${pkg}\n${readme}`;
|
|
131
|
+
const strong = /\bhipaa\b|\bphi\b|\bpci\b|\bsoc2\b|\biso[- ]?27001\b|\bcompliance\b|\bhealth(?:care)?\b|\bmedical\b|\bbank(?:ing)?\b|\bpayments?\b|\bfintech\b/i;
|
|
132
|
+
if (strong.test(combined)) return true;
|
|
133
|
+
const weakMatches = combined.match(/\bgdpr\b|\bpii\b/gi) || [];
|
|
134
|
+
return weakMatches.length >= 2;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function repoNeedsExternalTools(ctx) {
|
|
138
|
+
const deps = ctx.projectDependencies ? Object.keys(ctx.projectDependencies()) : [];
|
|
139
|
+
const depSet = new Set(deps);
|
|
140
|
+
const files = new Set(ctx.files || []);
|
|
141
|
+
const envContent = [ctx.fileContent('.env.example'), ctx.fileContent('.env.template'), ctx.fileContent('.env.sample')].filter(Boolean).join('\n');
|
|
142
|
+
const docs = docsBundle(ctx);
|
|
143
|
+
const combined = `${docs}\n${envContent}`;
|
|
144
|
+
const externalDeps = ['pg', 'postgres', 'mysql', 'mysql2', 'mongodb', 'mongoose', 'redis', 'ioredis', 'prisma', 'sequelize', 'typeorm', 'supabase', '@supabase/supabase-js', 'stripe', 'openai', '@anthropic-ai/sdk', 'langchain', '@aws-sdk/client-s3'];
|
|
145
|
+
if (externalDeps.some((d) => depSet.has(d))) return true;
|
|
146
|
+
if (files.has('docker-compose.yml') || files.has('docker-compose.yaml') || files.has('compose.yml') || ctx.hasDir('prisma') || ctx.hasDir('infra') || ctx.hasDir('terraform')) return true;
|
|
147
|
+
return /\bDATABASE_URL\b|\bREDIS_URL\b|\bSUPABASE_URL\b|\bSTRIPE_[A-Z_]+\b|\bAWS_[A-Z_]+\b/i.test(combined);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function workflowArtifacts(ctx) {
|
|
151
|
+
const ghDir = path.join(ctx.dir, '.github', 'workflows');
|
|
152
|
+
try {
|
|
153
|
+
const files = require('fs').readdirSync(ghDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
|
|
154
|
+
return files.map(f => {
|
|
155
|
+
const filePath = `.github/workflows/${f}`;
|
|
156
|
+
return { filePath, content: ctx.fileContent(filePath) || '' };
|
|
157
|
+
}).filter(item => item.content);
|
|
158
|
+
} catch {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function hooksFromSettings(ctx) {
|
|
164
|
+
const hooks = ctx.hooksConfig ? ctx.hooksConfig() : null;
|
|
165
|
+
return hooks || null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function hookEventEntries(hooks) {
|
|
169
|
+
if (!hooks || typeof hooks !== 'object') return [];
|
|
170
|
+
const entries = [];
|
|
171
|
+
for (const [eventName, config] of Object.entries(hooks)) {
|
|
172
|
+
const items = Array.isArray(config) ? config : [config];
|
|
173
|
+
for (const item of items) {
|
|
174
|
+
entries.push({ event: eventName, config: item });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return entries;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function policyFileContents(ctx) {
|
|
181
|
+
const files = ctx.policyFiles ? ctx.policyFiles() : [];
|
|
182
|
+
return files.map(f => ({ filePath: f, content: ctx.fileContent(f) || '' })).filter(item => item.content);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─── GEMINI_TECHNIQUES ──────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
const GEMINI_TECHNIQUES = {
|
|
188
|
+
|
|
189
|
+
// =============================================
|
|
190
|
+
// A. Instructions (7 checks) — GM-A01..GM-A07
|
|
191
|
+
// =============================================
|
|
192
|
+
|
|
193
|
+
geminiMdExists: {
|
|
194
|
+
id: 'GM-A01',
|
|
195
|
+
name: 'GEMINI.md exists at project root',
|
|
196
|
+
check: (ctx) => Boolean(geminiMd(ctx)),
|
|
197
|
+
impact: 'critical',
|
|
198
|
+
rating: 5,
|
|
199
|
+
category: 'instructions',
|
|
200
|
+
fix: 'Create GEMINI.md at the project root with repo-specific instructions for Gemini CLI.',
|
|
201
|
+
template: 'gemini-md',
|
|
202
|
+
file: () => 'GEMINI.md',
|
|
203
|
+
line: (ctx) => (geminiMd(ctx) ? 1 : null),
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
geminiMdSubstantive: {
|
|
207
|
+
id: 'GM-A02',
|
|
208
|
+
name: 'GEMINI.md has substantive content (>20 lines, 2+ sections)',
|
|
209
|
+
check: (ctx) => {
|
|
210
|
+
const content = geminiMd(ctx);
|
|
211
|
+
if (!content) return null;
|
|
212
|
+
const nonEmpty = content.split(/\r?\n/).filter(l => l.trim()).length;
|
|
213
|
+
return nonEmpty >= 20 && countSections(content) >= 2;
|
|
214
|
+
},
|
|
215
|
+
impact: 'high',
|
|
216
|
+
rating: 5,
|
|
217
|
+
category: 'instructions',
|
|
218
|
+
fix: 'Expand GEMINI.md to at least 20 substantive lines and 2+ sections instead of a thin placeholder.',
|
|
219
|
+
template: 'gemini-md',
|
|
220
|
+
file: () => 'GEMINI.md',
|
|
221
|
+
line: () => 1,
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
geminiMdVerificationCommands: {
|
|
225
|
+
id: 'GM-A03',
|
|
226
|
+
name: 'GEMINI.md includes build/test/lint commands',
|
|
227
|
+
check: (ctx) => {
|
|
228
|
+
const content = geminiMd(ctx);
|
|
229
|
+
if (!content) return null;
|
|
230
|
+
const expected = expectedVerificationCategories(ctx);
|
|
231
|
+
if (expected.length === 0) return /\bverify\b|\btest\b|\blint\b|\bbuild\b/i.test(content);
|
|
232
|
+
return expected.every(cat => hasCommandMention(content, cat));
|
|
233
|
+
},
|
|
234
|
+
impact: 'high',
|
|
235
|
+
rating: 5,
|
|
236
|
+
category: 'instructions',
|
|
237
|
+
fix: 'Document the actual test/lint/build commands so Gemini CLI can verify its own changes.',
|
|
238
|
+
template: 'gemini-md',
|
|
239
|
+
file: () => 'GEMINI.md',
|
|
240
|
+
line: (ctx) => {
|
|
241
|
+
const content = geminiMd(ctx);
|
|
242
|
+
return content ? (firstLineMatching(content, /\bVerification\b|\btest\b|\blint\b|\bbuild\b/i) || 1) : null;
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
geminiMdArchitecture: {
|
|
247
|
+
id: 'GM-A04',
|
|
248
|
+
name: 'GEMINI.md has architecture section or Mermaid diagram',
|
|
249
|
+
check: (ctx) => {
|
|
250
|
+
const content = geminiMd(ctx);
|
|
251
|
+
if (!content) return null;
|
|
252
|
+
return hasArchitecture(content);
|
|
253
|
+
},
|
|
254
|
+
impact: 'medium',
|
|
255
|
+
rating: 4,
|
|
256
|
+
category: 'instructions',
|
|
257
|
+
fix: 'Add an architecture or project map section to GEMINI.md so Gemini CLI understands the repo shape.',
|
|
258
|
+
template: 'gemini-md',
|
|
259
|
+
file: () => 'GEMINI.md',
|
|
260
|
+
line: (ctx) => {
|
|
261
|
+
const content = geminiMd(ctx);
|
|
262
|
+
return content ? firstLineMatching(content, /##\s+Architecture\b|##\s+Project Map\b|```mermaid/i) : null;
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
geminiMdNoFiller: {
|
|
267
|
+
id: 'GM-A05',
|
|
268
|
+
name: 'No generic filler instructions',
|
|
269
|
+
check: (ctx) => {
|
|
270
|
+
const content = geminiMd(ctx);
|
|
271
|
+
if (!content) return null;
|
|
272
|
+
return !FILLER_PATTERNS.some(p => p.test(content));
|
|
273
|
+
},
|
|
274
|
+
impact: 'low',
|
|
275
|
+
rating: 3,
|
|
276
|
+
category: 'instructions',
|
|
277
|
+
fix: 'Replace generic filler like "be helpful" with concrete repo-specific guidance that changes Gemini behavior.',
|
|
278
|
+
template: null,
|
|
279
|
+
file: () => 'GEMINI.md',
|
|
280
|
+
line: (ctx) => {
|
|
281
|
+
const content = geminiMd(ctx);
|
|
282
|
+
return content ? findFillerLine(content) : null;
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
geminiMdImportsValid: {
|
|
287
|
+
id: 'GM-A06',
|
|
288
|
+
name: '@import references point to existing files',
|
|
289
|
+
check: (ctx) => {
|
|
290
|
+
const content = geminiMd(ctx);
|
|
291
|
+
if (!content) return null;
|
|
292
|
+
const refs = extractImportRefs(content);
|
|
293
|
+
if (refs.length === 0) return true;
|
|
294
|
+
return refs.every(r => Boolean(ctx.fileContent(r.ref)));
|
|
295
|
+
},
|
|
296
|
+
impact: 'medium',
|
|
297
|
+
rating: 4,
|
|
298
|
+
category: 'instructions',
|
|
299
|
+
fix: 'Fix broken @file.md import references in GEMINI.md so all imported context files are actually loadable.',
|
|
300
|
+
template: null,
|
|
301
|
+
file: () => 'GEMINI.md',
|
|
302
|
+
line: (ctx) => {
|
|
303
|
+
const content = geminiMd(ctx);
|
|
304
|
+
if (!content) return null;
|
|
305
|
+
const refs = extractImportRefs(content);
|
|
306
|
+
const broken = refs.find(r => !ctx.fileContent(r.ref));
|
|
307
|
+
return broken ? broken.line : null;
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
geminiMdNoSecrets: {
|
|
312
|
+
id: 'GM-A07',
|
|
313
|
+
name: 'No secrets/API keys in GEMINI.md',
|
|
314
|
+
check: (ctx) => {
|
|
315
|
+
const content = geminiMd(ctx);
|
|
316
|
+
if (!content) return null;
|
|
317
|
+
return !containsEmbeddedSecret(content);
|
|
318
|
+
},
|
|
319
|
+
impact: 'critical',
|
|
320
|
+
rating: 5,
|
|
321
|
+
category: 'instructions',
|
|
322
|
+
fix: 'Remove API keys and secrets from GEMINI.md. Use environment variables or secret stores instead.',
|
|
323
|
+
template: null,
|
|
324
|
+
file: () => 'GEMINI.md',
|
|
325
|
+
line: (ctx) => {
|
|
326
|
+
const content = geminiMd(ctx);
|
|
327
|
+
return content ? findSecretLine(content) : null;
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
// =============================================
|
|
332
|
+
// B. Config (7 checks) — GM-B01..GM-B07
|
|
333
|
+
// =============================================
|
|
334
|
+
|
|
335
|
+
geminiSettingsExists: {
|
|
336
|
+
id: 'GM-B01',
|
|
337
|
+
name: '.gemini/settings.json exists',
|
|
338
|
+
check: (ctx) => Boolean(ctx.fileContent('.gemini/settings.json')),
|
|
339
|
+
impact: 'high',
|
|
340
|
+
rating: 5,
|
|
341
|
+
category: 'config',
|
|
342
|
+
fix: 'Create .gemini/settings.json with explicit model, sandbox, and approval settings.',
|
|
343
|
+
template: 'gemini-settings',
|
|
344
|
+
file: () => '.gemini/settings.json',
|
|
345
|
+
line: (ctx) => (ctx.fileContent('.gemini/settings.json') ? 1 : null),
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
geminiSettingsValidJson: {
|
|
349
|
+
id: 'GM-B02',
|
|
350
|
+
name: 'Settings is valid JSON',
|
|
351
|
+
check: (ctx) => {
|
|
352
|
+
const raw = settingsRaw(ctx);
|
|
353
|
+
if (!raw) return null;
|
|
354
|
+
const result = ctx.settingsJson();
|
|
355
|
+
return result && result.ok;
|
|
356
|
+
},
|
|
357
|
+
impact: 'critical',
|
|
358
|
+
rating: 5,
|
|
359
|
+
category: 'config',
|
|
360
|
+
fix: 'Fix malformed JSON in .gemini/settings.json so Gemini CLI does not silently ignore settings.',
|
|
361
|
+
template: null,
|
|
362
|
+
file: () => '.gemini/settings.json',
|
|
363
|
+
line: (ctx) => {
|
|
364
|
+
const result = ctx.settingsJson();
|
|
365
|
+
if (result && result.ok) return null;
|
|
366
|
+
if (result && result.error) {
|
|
367
|
+
const match = result.error.match(/position (\d+)/i);
|
|
368
|
+
if (match) {
|
|
369
|
+
const raw = settingsRaw(ctx);
|
|
370
|
+
return raw ? raw.slice(0, Number(match[1])).split('\n').length : 1;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return 1;
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
geminiModelExplicit: {
|
|
378
|
+
id: 'GM-B03',
|
|
379
|
+
name: 'Model is set explicitly (not relying on default/free tier)',
|
|
380
|
+
check: (ctx) => {
|
|
381
|
+
const data = settingsData(ctx);
|
|
382
|
+
if (!data) return null;
|
|
383
|
+
return Boolean(data.model);
|
|
384
|
+
},
|
|
385
|
+
impact: 'medium',
|
|
386
|
+
rating: 4,
|
|
387
|
+
category: 'config',
|
|
388
|
+
fix: 'Set "model" explicitly in settings.json so the team knows which Gemini model is used (Flash vs Pro).',
|
|
389
|
+
template: 'gemini-settings',
|
|
390
|
+
file: () => '.gemini/settings.json',
|
|
391
|
+
line: (ctx) => ctx.lineNumber('.gemini/settings.json', /"model"/),
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
geminiExplicitSettings: {
|
|
395
|
+
id: 'GM-B04',
|
|
396
|
+
name: 'Theme/sandbox/approval settings are explicit',
|
|
397
|
+
check: (ctx) => {
|
|
398
|
+
const data = settingsData(ctx);
|
|
399
|
+
if (!data) return null;
|
|
400
|
+
// At least sandbox or safety setting should be explicit
|
|
401
|
+
return Boolean(data.sandbox || data.safety || data.theme);
|
|
402
|
+
},
|
|
403
|
+
impact: 'medium',
|
|
404
|
+
rating: 4,
|
|
405
|
+
category: 'config',
|
|
406
|
+
fix: 'Set sandbox, safety, or theme settings explicitly in .gemini/settings.json instead of relying on defaults.',
|
|
407
|
+
template: 'gemini-settings',
|
|
408
|
+
file: () => '.gemini/settings.json',
|
|
409
|
+
line: () => 1,
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
geminiNoDeprecatedKeys: {
|
|
413
|
+
id: 'GM-B05',
|
|
414
|
+
name: 'No deprecated config keys',
|
|
415
|
+
check: (ctx) => {
|
|
416
|
+
const raw = settingsRaw(ctx);
|
|
417
|
+
if (!raw) return null;
|
|
418
|
+
const deprecatedPatterns = [
|
|
419
|
+
/\bsandbox_mode\b/,
|
|
420
|
+
/\bmax_tokens\b/,
|
|
421
|
+
/\bmcp_servers\b/,
|
|
422
|
+
];
|
|
423
|
+
return !deprecatedPatterns.some(p => p.test(raw));
|
|
424
|
+
},
|
|
425
|
+
impact: 'medium',
|
|
426
|
+
rating: 4,
|
|
427
|
+
category: 'config',
|
|
428
|
+
fix: 'Replace deprecated config keys (sandbox_mode, max_tokens, mcp_servers) with their current equivalents.',
|
|
429
|
+
template: null,
|
|
430
|
+
file: () => '.gemini/settings.json',
|
|
431
|
+
line: (ctx) => {
|
|
432
|
+
const raw = settingsRaw(ctx);
|
|
433
|
+
return raw ? firstLineMatching(raw, /\bsandbox_mode\b|\bmax_tokens\b|\bmcp_servers\b/) : null;
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
|
|
437
|
+
geminiContextFileNameStandard: {
|
|
438
|
+
id: 'GM-B06',
|
|
439
|
+
name: 'context.fileName is standard or intentionally overridden',
|
|
440
|
+
check: (ctx) => {
|
|
441
|
+
const data = settingsData(ctx);
|
|
442
|
+
if (!data) return null;
|
|
443
|
+
const contextFileName = data.context && data.context.fileName;
|
|
444
|
+
if (!contextFileName) return true; // Using default GEMINI.md
|
|
445
|
+
// If overridden, check that the custom file actually exists
|
|
446
|
+
const names = Array.isArray(contextFileName) ? contextFileName : [contextFileName];
|
|
447
|
+
return names.every(name => Boolean(ctx.fileContent(name)));
|
|
448
|
+
},
|
|
449
|
+
impact: 'low',
|
|
450
|
+
rating: 3,
|
|
451
|
+
category: 'config',
|
|
452
|
+
fix: 'If context.fileName is overridden, ensure the custom instruction files exist and are intentional.',
|
|
453
|
+
template: null,
|
|
454
|
+
file: () => '.gemini/settings.json',
|
|
455
|
+
line: (ctx) => ctx.lineNumber('.gemini/settings.json', /context\.fileName|"fileName"/i),
|
|
456
|
+
},
|
|
457
|
+
|
|
458
|
+
geminiEnvApiKey: {
|
|
459
|
+
id: 'GM-B07',
|
|
460
|
+
name: '.env exists with required API keys (GEMINI_API_KEY or Google auth)',
|
|
461
|
+
check: (ctx) => {
|
|
462
|
+
const envContent = ctx.fileContent('.env') || '';
|
|
463
|
+
const envExample = ctx.fileContent('.env.example') || ctx.fileContent('.env.template') || '';
|
|
464
|
+
const combined = `${envContent}\n${envExample}`;
|
|
465
|
+
// Check for Gemini API key or Google auth
|
|
466
|
+
return /\bGEMINI_API_KEY\b|\bGOOGLE_API_KEY\b|\bGOOGLE_APPLICATION_CREDENTIALS\b|\bgcloud\b/i.test(combined) || Boolean(envContent);
|
|
467
|
+
},
|
|
468
|
+
impact: 'high',
|
|
469
|
+
rating: 4,
|
|
470
|
+
category: 'config',
|
|
471
|
+
fix: 'Ensure .env or .env.example documents the GEMINI_API_KEY or Google authentication setup.',
|
|
472
|
+
template: null,
|
|
473
|
+
file: () => '.env',
|
|
474
|
+
line: (ctx) => {
|
|
475
|
+
const env = ctx.fileContent('.env') || ctx.fileContent('.env.example') || '';
|
|
476
|
+
return env ? firstLineMatching(env, /GEMINI_API_KEY|GOOGLE_API_KEY|GOOGLE_APPLICATION_CREDENTIALS/i) : null;
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
|
|
480
|
+
// =============================================
|
|
481
|
+
// C. Trust & Safety (9 checks) — GM-C01..GM-C09
|
|
482
|
+
// =============================================
|
|
483
|
+
|
|
484
|
+
geminiNoYolo: {
|
|
485
|
+
id: 'GM-C01',
|
|
486
|
+
name: 'No --yolo in project settings or scripts',
|
|
487
|
+
check: (ctx) => {
|
|
488
|
+
const raw = settingsRaw(ctx);
|
|
489
|
+
const gmd = geminiMd(ctx) || '';
|
|
490
|
+
const combined = `${raw}\n${gmd}`;
|
|
491
|
+
// Check settings and scripts for --yolo
|
|
492
|
+
if (/--yolo\b|\byolo\b.*:\s*true/i.test(raw)) return false;
|
|
493
|
+
// Check package.json scripts
|
|
494
|
+
const pkg = ctx.jsonFile ? ctx.jsonFile('package.json') : null;
|
|
495
|
+
if (pkg && pkg.scripts) {
|
|
496
|
+
const scriptValues = Object.values(pkg.scripts).join('\n');
|
|
497
|
+
if (/\b--yolo\b/i.test(scriptValues)) return false;
|
|
498
|
+
}
|
|
499
|
+
return true;
|
|
500
|
+
},
|
|
501
|
+
impact: 'critical',
|
|
502
|
+
rating: 5,
|
|
503
|
+
category: 'trust',
|
|
504
|
+
fix: 'Remove --yolo from project settings and scripts. It bypasses all safety controls and is never safe for shared repos.',
|
|
505
|
+
template: null,
|
|
506
|
+
file: () => '.gemini/settings.json',
|
|
507
|
+
line: (ctx) => {
|
|
508
|
+
const raw = settingsRaw(ctx);
|
|
509
|
+
return raw ? firstLineMatching(raw, /yolo/i) : null;
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
|
|
513
|
+
geminiSandboxExplicit: {
|
|
514
|
+
id: 'GM-C02',
|
|
515
|
+
name: 'Sandbox mode is explicitly configured',
|
|
516
|
+
check: (ctx) => {
|
|
517
|
+
const data = settingsData(ctx);
|
|
518
|
+
if (!data) return null;
|
|
519
|
+
return Boolean(data.sandbox && (data.sandbox.mode || typeof data.sandbox === 'string'));
|
|
520
|
+
},
|
|
521
|
+
impact: 'high',
|
|
522
|
+
rating: 5,
|
|
523
|
+
category: 'trust',
|
|
524
|
+
fix: 'Set sandbox mode explicitly (Seatbelt/Docker/gVisor/bubblewrap) instead of relying on platform defaults.',
|
|
525
|
+
template: 'gemini-settings',
|
|
526
|
+
file: () => '.gemini/settings.json',
|
|
527
|
+
line: (ctx) => ctx.lineNumber('.gemini/settings.json', /"sandbox"/i),
|
|
528
|
+
},
|
|
529
|
+
|
|
530
|
+
geminiTrustedFoldersIntentional: {
|
|
531
|
+
id: 'GM-C03',
|
|
532
|
+
name: 'Trusted Folders list is intentional (not blindly trust-all)',
|
|
533
|
+
check: (ctx) => {
|
|
534
|
+
const data = settingsData(ctx);
|
|
535
|
+
if (!data) return null;
|
|
536
|
+
const trusted = data.trustedFolders || (data.safety && data.safety.trustedFolders);
|
|
537
|
+
if (!trusted) return true; // No explicit trust-all
|
|
538
|
+
if (Array.isArray(trusted)) {
|
|
539
|
+
// Warn if trust-all patterns
|
|
540
|
+
return !trusted.some(f => f === '*' || f === '/' || f === '~' || f === '**');
|
|
541
|
+
}
|
|
542
|
+
return true;
|
|
543
|
+
},
|
|
544
|
+
impact: 'high',
|
|
545
|
+
rating: 4,
|
|
546
|
+
category: 'trust',
|
|
547
|
+
fix: 'Restrict trustedFolders to specific project directories instead of wildcard trust-all patterns.',
|
|
548
|
+
template: null,
|
|
549
|
+
file: () => '.gemini/settings.json',
|
|
550
|
+
line: (ctx) => ctx.lineNumber('.gemini/settings.json', /trustedFolders/i),
|
|
551
|
+
},
|
|
552
|
+
|
|
553
|
+
geminiPolicyRulesForRiskyRepos: {
|
|
554
|
+
id: 'GM-C04',
|
|
555
|
+
name: 'Policy engine rules exist for elevated-risk repos',
|
|
556
|
+
check: (ctx) => {
|
|
557
|
+
if (!repoLooksRegulated(ctx)) return null;
|
|
558
|
+
const policies = policyFileContents(ctx);
|
|
559
|
+
return policies.length > 0;
|
|
560
|
+
},
|
|
561
|
+
impact: 'medium',
|
|
562
|
+
rating: 4,
|
|
563
|
+
category: 'trust',
|
|
564
|
+
fix: 'For regulated repos, add policy TOML files under .gemini/policy/ to enforce tool and command restrictions.',
|
|
565
|
+
template: null,
|
|
566
|
+
file: () => '.gemini/policy',
|
|
567
|
+
line: () => 1,
|
|
568
|
+
},
|
|
569
|
+
|
|
570
|
+
geminiNoPolicyContradictions: {
|
|
571
|
+
id: 'GM-C05',
|
|
572
|
+
name: 'No policy contradictions across tiers',
|
|
573
|
+
check: (ctx) => {
|
|
574
|
+
const policies = policyFileContents(ctx);
|
|
575
|
+
if (policies.length < 2) return null;
|
|
576
|
+
// Check for contradictory allow/deny on the same tool across policy files
|
|
577
|
+
const allowedTools = new Set();
|
|
578
|
+
const deniedTools = new Set();
|
|
579
|
+
for (const policy of policies) {
|
|
580
|
+
const allowMatches = policy.content.match(/allow\s*=\s*\[([^\]]*)\]/gi) || [];
|
|
581
|
+
const denyMatches = policy.content.match(/deny\s*=\s*\[([^\]]*)\]/gi) || [];
|
|
582
|
+
for (const m of allowMatches) {
|
|
583
|
+
const tools = m.match(/["']([^"']+)["']/g) || [];
|
|
584
|
+
tools.forEach(t => allowedTools.add(t.replace(/["']/g, '')));
|
|
585
|
+
}
|
|
586
|
+
for (const m of denyMatches) {
|
|
587
|
+
const tools = m.match(/["']([^"']+)["']/g) || [];
|
|
588
|
+
tools.forEach(t => deniedTools.add(t.replace(/["']/g, '')));
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// Contradiction: same tool both allowed and denied
|
|
592
|
+
for (const tool of allowedTools) {
|
|
593
|
+
if (deniedTools.has(tool)) return false;
|
|
594
|
+
}
|
|
595
|
+
return true;
|
|
596
|
+
},
|
|
597
|
+
impact: 'high',
|
|
598
|
+
rating: 4,
|
|
599
|
+
category: 'trust',
|
|
600
|
+
fix: 'Remove contradictory allow/deny rules across policy tiers so Gemini CLI enforcement is predictable.',
|
|
601
|
+
template: null,
|
|
602
|
+
file: (ctx) => {
|
|
603
|
+
const policies = policyFileContents(ctx);
|
|
604
|
+
return policies.length > 0 ? policies[0].filePath : null;
|
|
605
|
+
},
|
|
606
|
+
line: () => 1,
|
|
607
|
+
},
|
|
608
|
+
|
|
609
|
+
geminiAutoEditCodeDeletionRisk: {
|
|
610
|
+
id: 'GM-C06',
|
|
611
|
+
name: 'auto_edit not enabled without code deletion risk awareness',
|
|
612
|
+
check: (ctx) => {
|
|
613
|
+
const raw = settingsRaw(ctx);
|
|
614
|
+
const data = settingsData(ctx);
|
|
615
|
+
if (!data) return null;
|
|
616
|
+
const autoEdit = data.auto_edit || data.autoEdit || (data.safety && data.safety.autoEdit);
|
|
617
|
+
if (!autoEdit) return true;
|
|
618
|
+
// If auto_edit is on, check that code deletion bug is acknowledged
|
|
619
|
+
const gmd = geminiMd(ctx) || '';
|
|
620
|
+
const docs = `${gmd}\n${raw}`;
|
|
621
|
+
return /\bcode deletion\b|\bbug\s*#?23497\b|\bdeletion risk\b|\bcode loss\b/i.test(docs);
|
|
622
|
+
},
|
|
623
|
+
impact: 'critical',
|
|
624
|
+
rating: 5,
|
|
625
|
+
category: 'trust',
|
|
626
|
+
fix: 'CRITICAL: auto_edit has a known code deletion bug (#23497). Document the risk or disable auto_edit until the bug is fixed.',
|
|
627
|
+
template: null,
|
|
628
|
+
file: () => '.gemini/settings.json',
|
|
629
|
+
line: (ctx) => ctx.lineNumber('.gemini/settings.json', /auto_?edit/i),
|
|
630
|
+
},
|
|
631
|
+
|
|
632
|
+
geminiNoSecretsInSettings: {
|
|
633
|
+
id: 'GM-C07',
|
|
634
|
+
name: 'No secrets in settings.json or command files',
|
|
635
|
+
check: (ctx) => {
|
|
636
|
+
const raw = settingsRaw(ctx);
|
|
637
|
+
if (raw && containsEmbeddedSecret(raw)) return false;
|
|
638
|
+
// Check command files
|
|
639
|
+
const commandFiles = ctx.commandFiles ? ctx.commandFiles() : [];
|
|
640
|
+
for (const f of commandFiles) {
|
|
641
|
+
const content = ctx.fileContent(f) || '';
|
|
642
|
+
if (containsEmbeddedSecret(content)) return false;
|
|
643
|
+
}
|
|
644
|
+
return true;
|
|
645
|
+
},
|
|
646
|
+
impact: 'critical',
|
|
647
|
+
rating: 5,
|
|
648
|
+
category: 'trust',
|
|
649
|
+
fix: 'Remove API keys and secrets from settings.json and command files. Use environment variables instead.',
|
|
650
|
+
template: null,
|
|
651
|
+
file: (ctx) => {
|
|
652
|
+
const raw = settingsRaw(ctx);
|
|
653
|
+
if (raw && containsEmbeddedSecret(raw)) return '.gemini/settings.json';
|
|
654
|
+
const commandFiles = ctx.commandFiles ? ctx.commandFiles() : [];
|
|
655
|
+
for (const f of commandFiles) {
|
|
656
|
+
const content = ctx.fileContent(f) || '';
|
|
657
|
+
if (containsEmbeddedSecret(content)) return f;
|
|
658
|
+
}
|
|
659
|
+
return null;
|
|
660
|
+
},
|
|
661
|
+
line: (ctx) => {
|
|
662
|
+
const raw = settingsRaw(ctx);
|
|
663
|
+
if (raw && containsEmbeddedSecret(raw)) return findSecretLine(raw);
|
|
664
|
+
const commandFiles = ctx.commandFiles ? ctx.commandFiles() : [];
|
|
665
|
+
for (const f of commandFiles) {
|
|
666
|
+
const content = ctx.fileContent(f) || '';
|
|
667
|
+
if (containsEmbeddedSecret(content)) return findSecretLine(content);
|
|
668
|
+
}
|
|
669
|
+
return null;
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
|
|
673
|
+
geminiCodeDeletionBugAwareness: {
|
|
674
|
+
id: 'GM-C08',
|
|
675
|
+
name: 'Code deletion bug awareness documented for affected workflows',
|
|
676
|
+
check: (ctx) => {
|
|
677
|
+
const gmd = geminiMd(ctx) || '';
|
|
678
|
+
const data = settingsData(ctx);
|
|
679
|
+
// Only relevant if auto_edit or --yolo might be in use
|
|
680
|
+
const hasRiskySetting = data && (data.auto_edit || data.autoEdit || data.safety);
|
|
681
|
+
if (!hasRiskySetting) return null;
|
|
682
|
+
return /\bcode deletion\b|\bbug\s*#?23497\b|\bdeletion risk\b|\bgemini.*delet/i.test(gmd);
|
|
683
|
+
},
|
|
684
|
+
impact: 'medium',
|
|
685
|
+
rating: 3,
|
|
686
|
+
category: 'trust',
|
|
687
|
+
fix: 'Document the known Gemini code deletion bug (#23497) in GEMINI.md for workflows that use auto_edit.',
|
|
688
|
+
template: null,
|
|
689
|
+
file: () => 'GEMINI.md',
|
|
690
|
+
line: (ctx) => {
|
|
691
|
+
const gmd = geminiMd(ctx) || '';
|
|
692
|
+
return firstLineMatching(gmd, /code deletion|bug.*23497|deletion risk/i);
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
|
|
696
|
+
geminiNoYoloInCI: {
|
|
697
|
+
id: 'GM-C09',
|
|
698
|
+
name: 'No --yolo in CI scripts or workflow files',
|
|
699
|
+
check: (ctx) => {
|
|
700
|
+
for (const wf of workflowArtifacts(ctx)) {
|
|
701
|
+
if (/\b--yolo\b/i.test(wf.content)) return false;
|
|
702
|
+
}
|
|
703
|
+
// Check Makefile, justfile, scripts
|
|
704
|
+
const makefile = ctx.fileContent('Makefile') || '';
|
|
705
|
+
if (/\b--yolo\b/i.test(makefile)) return false;
|
|
706
|
+
return true;
|
|
707
|
+
},
|
|
708
|
+
impact: 'critical',
|
|
709
|
+
rating: 5,
|
|
710
|
+
category: 'trust',
|
|
711
|
+
fix: 'Never use --yolo in CI. Remove it from all workflow files and build scripts.',
|
|
712
|
+
template: null,
|
|
713
|
+
file: (ctx) => {
|
|
714
|
+
for (const wf of workflowArtifacts(ctx)) {
|
|
715
|
+
if (/\b--yolo\b/i.test(wf.content)) return wf.filePath;
|
|
716
|
+
}
|
|
717
|
+
const makefile = ctx.fileContent('Makefile') || '';
|
|
718
|
+
if (/\b--yolo\b/i.test(makefile)) return 'Makefile';
|
|
719
|
+
return null;
|
|
720
|
+
},
|
|
721
|
+
line: (ctx) => {
|
|
722
|
+
for (const wf of workflowArtifacts(ctx)) {
|
|
723
|
+
const line = firstLineMatching(wf.content, /--yolo/i);
|
|
724
|
+
if (line) return line;
|
|
725
|
+
}
|
|
726
|
+
const makefile = ctx.fileContent('Makefile') || '';
|
|
727
|
+
return firstLineMatching(makefile, /--yolo/i);
|
|
728
|
+
},
|
|
729
|
+
},
|
|
730
|
+
|
|
731
|
+
// =============================================
|
|
732
|
+
// D. Hooks (4 checks) — GM-D01..GM-D04
|
|
733
|
+
// =============================================
|
|
734
|
+
|
|
735
|
+
geminiHooksConfigured: {
|
|
736
|
+
id: 'GM-D01',
|
|
737
|
+
name: 'Hooks configured if project uses tool enforcement',
|
|
738
|
+
check: (ctx) => {
|
|
739
|
+
const hooks = hooksFromSettings(ctx);
|
|
740
|
+
if (hooks) return true;
|
|
741
|
+
// Check if GEMINI.md mentions hooks
|
|
742
|
+
const gmd = geminiMd(ctx) || '';
|
|
743
|
+
const claimsHooks = /\bhooks?\b|\bBeforeTool\b|\bAfterTool\b/i.test(gmd);
|
|
744
|
+
if (!claimsHooks) return null; // Not relevant
|
|
745
|
+
return false; // Claims hooks but none configured
|
|
746
|
+
},
|
|
747
|
+
impact: 'medium',
|
|
748
|
+
rating: 4,
|
|
749
|
+
category: 'hooks',
|
|
750
|
+
fix: 'Add hooks configuration to .gemini/settings.json if the project uses tool enforcement.',
|
|
751
|
+
template: 'gemini-settings',
|
|
752
|
+
file: () => '.gemini/settings.json',
|
|
753
|
+
line: (ctx) => ctx.lineNumber('.gemini/settings.json', /hooks/i),
|
|
754
|
+
},
|
|
755
|
+
|
|
756
|
+
geminiHookMatchersSpecific: {
|
|
757
|
+
id: 'GM-D02',
|
|
758
|
+
name: 'BeforeTool/AfterTool matchers use specific regex (not catch-all)',
|
|
759
|
+
check: (ctx) => {
|
|
760
|
+
const hooks = hooksFromSettings(ctx);
|
|
761
|
+
if (!hooks) return null;
|
|
762
|
+
const entries = hookEventEntries(hooks);
|
|
763
|
+
if (entries.length === 0) return null;
|
|
764
|
+
for (const entry of entries) {
|
|
765
|
+
const cfg = entry.config;
|
|
766
|
+
if (!cfg) continue;
|
|
767
|
+
const matcher = cfg.matcher || cfg.pattern || cfg.toolName;
|
|
768
|
+
if (typeof matcher === 'string') {
|
|
769
|
+
if (matcher === '*' || matcher === '.*' || matcher === '.+') return false;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return true;
|
|
773
|
+
},
|
|
774
|
+
impact: 'medium',
|
|
775
|
+
rating: 4,
|
|
776
|
+
category: 'hooks',
|
|
777
|
+
fix: 'Replace catch-all hook matchers (* or .*) with specific tool name regex patterns.',
|
|
778
|
+
template: null,
|
|
779
|
+
file: () => '.gemini/settings.json',
|
|
780
|
+
line: (ctx) => {
|
|
781
|
+
const raw = settingsRaw(ctx);
|
|
782
|
+
return raw ? firstLineMatching(raw, /["'](\*|\.\*|\.\+)["']/i) : null;
|
|
783
|
+
},
|
|
784
|
+
},
|
|
785
|
+
|
|
786
|
+
geminiAfterToolScrubbing: {
|
|
787
|
+
id: 'GM-D03',
|
|
788
|
+
name: 'AfterTool output scrubbing used for sensitive tool results',
|
|
789
|
+
check: (ctx) => {
|
|
790
|
+
const hooks = hooksFromSettings(ctx);
|
|
791
|
+
if (!hooks) return null;
|
|
792
|
+
const afterTool = hooks.AfterTool;
|
|
793
|
+
if (!afterTool) return null;
|
|
794
|
+
// Check that AfterTool hooks exist with scrubbing capability
|
|
795
|
+
const entries = Array.isArray(afterTool) ? afterTool : [afterTool];
|
|
796
|
+
const hasScrub = entries.some(entry => {
|
|
797
|
+
const cmd = typeof entry === 'string' ? entry : (entry.command || entry.cmd || '');
|
|
798
|
+
return /\bscrub\b|\bredact\b|\bfilter\b|\bdeny\b|\bstrip\b/i.test(cmd);
|
|
799
|
+
});
|
|
800
|
+
return hasScrub || entries.length > 0; // At least AfterTool is configured
|
|
801
|
+
},
|
|
802
|
+
impact: 'medium',
|
|
803
|
+
rating: 4,
|
|
804
|
+
category: 'hooks',
|
|
805
|
+
fix: 'Configure AfterTool hooks to scrub sensitive output from tool results before they reach the model.',
|
|
806
|
+
template: null,
|
|
807
|
+
file: () => '.gemini/settings.json',
|
|
808
|
+
line: (ctx) => ctx.lineNumber('.gemini/settings.json', /AfterTool/i),
|
|
809
|
+
},
|
|
810
|
+
|
|
811
|
+
geminiHookTimeoutReasonable: {
|
|
812
|
+
id: 'GM-D04',
|
|
813
|
+
name: 'Hook timeout is reasonable (<60s)',
|
|
814
|
+
check: (ctx) => {
|
|
815
|
+
const hooks = hooksFromSettings(ctx);
|
|
816
|
+
if (!hooks) return null;
|
|
817
|
+
const entries = hookEventEntries(hooks);
|
|
818
|
+
for (const entry of entries) {
|
|
819
|
+
const cfg = entry.config;
|
|
820
|
+
if (cfg && typeof cfg === 'object' && typeof cfg.timeout === 'number') {
|
|
821
|
+
if (cfg.timeout > 60) return false;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return true;
|
|
825
|
+
},
|
|
826
|
+
impact: 'low',
|
|
827
|
+
rating: 3,
|
|
828
|
+
category: 'hooks',
|
|
829
|
+
fix: 'Keep hook timeouts at 60 seconds or less unless the repo documents why a longer timeout is needed.',
|
|
830
|
+
template: null,
|
|
831
|
+
file: () => '.gemini/settings.json',
|
|
832
|
+
line: (ctx) => {
|
|
833
|
+
const raw = settingsRaw(ctx);
|
|
834
|
+
return raw ? firstLineMatching(raw, /"timeout"\s*:\s*(6[1-9]|[7-9]\d|\d{3,})/i) : null;
|
|
835
|
+
},
|
|
836
|
+
},
|
|
837
|
+
|
|
838
|
+
// =============================================
|
|
839
|
+
// E. MCP (6 checks) — GM-E01..GM-E06
|
|
840
|
+
// =============================================
|
|
841
|
+
|
|
842
|
+
geminiMcpConfigured: {
|
|
843
|
+
id: 'GM-E01',
|
|
844
|
+
name: 'MCP servers configured if project needs external tools',
|
|
845
|
+
check: (ctx) => {
|
|
846
|
+
if (!repoNeedsExternalTools(ctx)) return null;
|
|
847
|
+
const servers = ctx.mcpServers ? ctx.mcpServers() : {};
|
|
848
|
+
return Object.keys(servers).length > 0;
|
|
849
|
+
},
|
|
850
|
+
impact: 'medium',
|
|
851
|
+
rating: 4,
|
|
852
|
+
category: 'mcp',
|
|
853
|
+
fix: 'This repo depends on external services. Add MCP servers to .gemini/settings.json so Gemini CLI can use live context.',
|
|
854
|
+
template: 'gemini-settings',
|
|
855
|
+
file: () => '.gemini/settings.json',
|
|
856
|
+
line: () => 1,
|
|
857
|
+
},
|
|
858
|
+
|
|
859
|
+
geminiMcpExcludeTools: {
|
|
860
|
+
id: 'GM-E02',
|
|
861
|
+
name: 'excludeTools used to restrict dangerous tools',
|
|
862
|
+
check: (ctx) => {
|
|
863
|
+
const servers = ctx.mcpServers ? ctx.mcpServers() : {};
|
|
864
|
+
const ids = Object.keys(servers);
|
|
865
|
+
if (ids.length === 0) return null;
|
|
866
|
+
// In Gemini, excludeTools always wins — check it's used
|
|
867
|
+
return ids.some(id => {
|
|
868
|
+
const server = servers[id];
|
|
869
|
+
return server && Array.isArray(server.excludeTools) && server.excludeTools.length > 0;
|
|
870
|
+
});
|
|
871
|
+
},
|
|
872
|
+
impact: 'high',
|
|
873
|
+
rating: 4,
|
|
874
|
+
category: 'mcp',
|
|
875
|
+
fix: 'Use excludeTools on MCP servers to restrict dangerous tools. In Gemini, excludeTools always wins over includeTools.',
|
|
876
|
+
template: null,
|
|
877
|
+
file: () => '.gemini/settings.json',
|
|
878
|
+
line: (ctx) => ctx.lineNumber('.gemini/settings.json', /excludeTools/i),
|
|
879
|
+
},
|
|
880
|
+
|
|
881
|
+
geminiMcpTransportAppropriate: {
|
|
882
|
+
id: 'GM-E03',
|
|
883
|
+
name: 'Transport type is appropriate (stdio for local, SSE/HTTP for remote)',
|
|
884
|
+
check: (ctx) => {
|
|
885
|
+
const servers = ctx.mcpServers ? ctx.mcpServers() : {};
|
|
886
|
+
const ids = Object.keys(servers);
|
|
887
|
+
if (ids.length === 0) return null;
|
|
888
|
+
for (const id of ids) {
|
|
889
|
+
const server = servers[id];
|
|
890
|
+
if (!server) continue;
|
|
891
|
+
const transport = server.transport || '';
|
|
892
|
+
const hasUrl = Boolean(server.url);
|
|
893
|
+
// Remote servers should use SSE or HTTP streaming, not stdio
|
|
894
|
+
if (hasUrl && transport === 'stdio') return false;
|
|
895
|
+
// Local servers should use stdio, not remote protocols
|
|
896
|
+
if (!hasUrl && server.command && (transport === 'sse' || transport === 'http')) return false;
|
|
897
|
+
}
|
|
898
|
+
return true;
|
|
899
|
+
},
|
|
900
|
+
impact: 'medium',
|
|
901
|
+
rating: 3,
|
|
902
|
+
category: 'mcp',
|
|
903
|
+
fix: 'Use stdio transport for local MCP servers and SSE/HTTP streaming for remote servers.',
|
|
904
|
+
template: null,
|
|
905
|
+
file: () => '.gemini/settings.json',
|
|
906
|
+
line: (ctx) => ctx.lineNumber('.gemini/settings.json', /transport/i),
|
|
907
|
+
},
|
|
908
|
+
|
|
909
|
+
geminiMcpExcludeOverInclude: {
|
|
910
|
+
id: 'GM-E04',
|
|
911
|
+
name: 'excludeTools used instead of includeTools (Gemini security model)',
|
|
912
|
+
check: (ctx) => {
|
|
913
|
+
const servers = ctx.mcpServers ? ctx.mcpServers() : {};
|
|
914
|
+
const ids = Object.keys(servers);
|
|
915
|
+
if (ids.length === 0) return null;
|
|
916
|
+
// Flag if includeTools is used without excludeTools (Gemini security best practice)
|
|
917
|
+
for (const id of ids) {
|
|
918
|
+
const server = servers[id];
|
|
919
|
+
if (!server) continue;
|
|
920
|
+
const hasInclude = Array.isArray(server.includeTools) && server.includeTools.length > 0;
|
|
921
|
+
const hasExclude = Array.isArray(server.excludeTools) && server.excludeTools.length > 0;
|
|
922
|
+
if (hasInclude && !hasExclude) return false;
|
|
923
|
+
}
|
|
924
|
+
return true;
|
|
925
|
+
},
|
|
926
|
+
impact: 'high',
|
|
927
|
+
rating: 4,
|
|
928
|
+
category: 'mcp',
|
|
929
|
+
fix: 'In Gemini CLI, excludeTools always wins. Use excludeTools for security instead of relying on includeTools alone.',
|
|
930
|
+
template: null,
|
|
931
|
+
file: () => '.gemini/settings.json',
|
|
932
|
+
line: (ctx) => ctx.lineNumber('.gemini/settings.json', /includeTools/i),
|
|
933
|
+
},
|
|
934
|
+
|
|
935
|
+
geminiMcpAuthDocumented: {
|
|
936
|
+
id: 'GM-E05',
|
|
937
|
+
name: 'Auth requirements documented for MCP servers',
|
|
938
|
+
check: (ctx) => {
|
|
939
|
+
const servers = ctx.mcpServers ? ctx.mcpServers() : {};
|
|
940
|
+
const ids = Object.keys(servers);
|
|
941
|
+
if (ids.length === 0) return null;
|
|
942
|
+
const docs = docsBundle(ctx);
|
|
943
|
+
for (const id of ids) {
|
|
944
|
+
const server = servers[id];
|
|
945
|
+
if (!server || !server.url) continue; // Only remote servers need auth docs
|
|
946
|
+
const hasInlineAuth = Boolean(server.token || server.headers || server.env);
|
|
947
|
+
const hasDocNote = new RegExp(`\\b${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b[\\s\\S]{0,140}\\b(auth|token|credential|env)\\b`, 'i').test(docs);
|
|
948
|
+
if (!hasInlineAuth && !hasDocNote) return false;
|
|
949
|
+
}
|
|
950
|
+
return true;
|
|
951
|
+
},
|
|
952
|
+
impact: 'medium',
|
|
953
|
+
rating: 3,
|
|
954
|
+
category: 'mcp',
|
|
955
|
+
fix: 'Document auth requirements for remote MCP servers so setup is reviewable by team members.',
|
|
956
|
+
template: null,
|
|
957
|
+
file: () => '.gemini/settings.json',
|
|
958
|
+
line: (ctx) => ctx.lineNumber('.gemini/settings.json', /mcpServers/i),
|
|
959
|
+
},
|
|
960
|
+
|
|
961
|
+
geminiMcpNoDeprecatedTransport: {
|
|
962
|
+
id: 'GM-E06',
|
|
963
|
+
name: 'No deprecated transport types in MCP config',
|
|
964
|
+
check: (ctx) => {
|
|
965
|
+
const raw = settingsRaw(ctx);
|
|
966
|
+
if (!raw) return null;
|
|
967
|
+
const servers = ctx.mcpServers ? ctx.mcpServers() : {};
|
|
968
|
+
if (Object.keys(servers).length === 0) return null;
|
|
969
|
+
// Check for deprecated transport names
|
|
970
|
+
return !/"transport"\s*:\s*"(http\+sse|legacy-sse)"/i.test(raw);
|
|
971
|
+
},
|
|
972
|
+
impact: 'medium',
|
|
973
|
+
rating: 3,
|
|
974
|
+
category: 'mcp',
|
|
975
|
+
fix: 'Replace deprecated MCP transport types with current ones (stdio or streamable HTTP).',
|
|
976
|
+
template: null,
|
|
977
|
+
file: () => '.gemini/settings.json',
|
|
978
|
+
line: (ctx) => {
|
|
979
|
+
const raw = settingsRaw(ctx);
|
|
980
|
+
return raw ? firstLineMatching(raw, /http\+sse|legacy-sse/i) : null;
|
|
981
|
+
},
|
|
982
|
+
},
|
|
983
|
+
|
|
984
|
+
// =============================================
|
|
985
|
+
// F. Sandbox & Policy (7 checks) — GM-F01..GM-F07
|
|
986
|
+
// =============================================
|
|
987
|
+
|
|
988
|
+
geminiSandboxModeExplicit: {
|
|
989
|
+
id: 'GM-F01',
|
|
990
|
+
name: 'Sandbox mode is explicit (Seatbelt/Docker/gVisor/bubblewrap)',
|
|
991
|
+
check: (ctx) => {
|
|
992
|
+
const data = settingsData(ctx);
|
|
993
|
+
if (!data) return null;
|
|
994
|
+
const sandbox = data.sandbox;
|
|
995
|
+
if (!sandbox) return false;
|
|
996
|
+
const mode = typeof sandbox === 'string' ? sandbox : sandbox.mode;
|
|
997
|
+
const validModes = ['seatbelt', 'docker', 'podman', 'gvisor', 'lxc', 'lxd', 'bubblewrap', 'none'];
|
|
998
|
+
return Boolean(mode && validModes.some(m => mode.toLowerCase().includes(m)));
|
|
999
|
+
},
|
|
1000
|
+
impact: 'high',
|
|
1001
|
+
rating: 5,
|
|
1002
|
+
category: 'sandbox',
|
|
1003
|
+
fix: 'Set an explicit sandbox mode (Seatbelt, Docker, gVisor, bubblewrap) instead of defaulting silently.',
|
|
1004
|
+
template: 'gemini-settings',
|
|
1005
|
+
file: () => '.gemini/settings.json',
|
|
1006
|
+
line: (ctx) => ctx.lineNumber('.gemini/settings.json', /sandbox/i),
|
|
1007
|
+
},
|
|
1008
|
+
|
|
1009
|
+
geminiSandboxPermissionsRestricted: {
|
|
1010
|
+
id: 'GM-F02',
|
|
1011
|
+
name: 'Sandbox permissions are appropriately restricted',
|
|
1012
|
+
check: (ctx) => {
|
|
1013
|
+
const data = settingsData(ctx);
|
|
1014
|
+
if (!data || !data.sandbox) return null;
|
|
1015
|
+
const perms = data.sandbox.permissions;
|
|
1016
|
+
if (!perms) return null;
|
|
1017
|
+
// Check for overly broad permissions
|
|
1018
|
+
if (perms.network === true && perms.filesystem === 'full') return false;
|
|
1019
|
+
return true;
|
|
1020
|
+
},
|
|
1021
|
+
impact: 'high',
|
|
1022
|
+
rating: 4,
|
|
1023
|
+
category: 'sandbox',
|
|
1024
|
+
fix: 'Restrict sandbox permissions. Avoid granting both full network and full filesystem access.',
|
|
1025
|
+
template: null,
|
|
1026
|
+
file: () => '.gemini/settings.json',
|
|
1027
|
+
line: (ctx) => ctx.lineNumber('.gemini/settings.json', /permissions/i),
|
|
1028
|
+
},
|
|
1029
|
+
|
|
1030
|
+
geminiPolicyEngineConfigured: {
|
|
1031
|
+
id: 'GM-F03',
|
|
1032
|
+
name: 'Policy engine rules configured when policy files exist',
|
|
1033
|
+
check: (ctx) => {
|
|
1034
|
+
const policies = policyFileContents(ctx);
|
|
1035
|
+
if (policies.length === 0) return null;
|
|
1036
|
+
// At least one policy file should have actual rules
|
|
1037
|
+
return policies.some(p => /\ballow\b|\bdeny\b|\brule\b|\btool\b/i.test(p.content));
|
|
1038
|
+
},
|
|
1039
|
+
impact: 'medium',
|
|
1040
|
+
rating: 4,
|
|
1041
|
+
category: 'sandbox',
|
|
1042
|
+
fix: 'Policy files exist but contain no rules. Add allow/deny rules or remove empty policy files.',
|
|
1043
|
+
template: null,
|
|
1044
|
+
file: (ctx) => {
|
|
1045
|
+
const policies = policyFileContents(ctx);
|
|
1046
|
+
return policies.length > 0 ? policies[0].filePath : null;
|
|
1047
|
+
},
|
|
1048
|
+
line: () => 1,
|
|
1049
|
+
},
|
|
1050
|
+
|
|
1051
|
+
geminiPolicyTiersValid: {
|
|
1052
|
+
id: 'GM-F04',
|
|
1053
|
+
name: 'Policy TOML files are valid and parseable',
|
|
1054
|
+
check: (ctx) => {
|
|
1055
|
+
const policies = ctx.policyFiles ? ctx.policyFiles() : [];
|
|
1056
|
+
if (policies.length === 0) return null;
|
|
1057
|
+
for (const f of policies) {
|
|
1058
|
+
const parsed = ctx.policyConfig ? ctx.policyConfig(f) : null;
|
|
1059
|
+
if (parsed && !parsed.ok) return false;
|
|
1060
|
+
}
|
|
1061
|
+
return true;
|
|
1062
|
+
},
|
|
1063
|
+
impact: 'high',
|
|
1064
|
+
rating: 4,
|
|
1065
|
+
category: 'sandbox',
|
|
1066
|
+
fix: 'Fix malformed TOML in policy files so the policy engine does not silently skip rules.',
|
|
1067
|
+
template: null,
|
|
1068
|
+
file: (ctx) => {
|
|
1069
|
+
const policies = ctx.policyFiles ? ctx.policyFiles() : [];
|
|
1070
|
+
for (const f of policies) {
|
|
1071
|
+
const parsed = ctx.policyConfig ? ctx.policyConfig(f) : null;
|
|
1072
|
+
if (parsed && !parsed.ok) return f;
|
|
1073
|
+
}
|
|
1074
|
+
return null;
|
|
1075
|
+
},
|
|
1076
|
+
line: (ctx) => {
|
|
1077
|
+
const policies = ctx.policyFiles ? ctx.policyFiles() : [];
|
|
1078
|
+
for (const f of policies) {
|
|
1079
|
+
const parsed = ctx.policyConfig ? ctx.policyConfig(f) : null;
|
|
1080
|
+
if (parsed && !parsed.ok && parsed.error) {
|
|
1081
|
+
const match = parsed.error.match(/Line (\d+)/i);
|
|
1082
|
+
if (match) return Number(match[1]);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return 1;
|
|
1086
|
+
},
|
|
1087
|
+
},
|
|
1088
|
+
|
|
1089
|
+
geminiPolicyTiersDontConflict: {
|
|
1090
|
+
id: 'GM-F05',
|
|
1091
|
+
name: 'Policy engine tiers don\'t conflict',
|
|
1092
|
+
check: (ctx) => {
|
|
1093
|
+
const policies = policyFileContents(ctx);
|
|
1094
|
+
if (policies.length < 2) return null;
|
|
1095
|
+
// Detect conflicting tool decisions across tiers
|
|
1096
|
+
const perFile = policies.map(p => {
|
|
1097
|
+
const allows = new Set();
|
|
1098
|
+
const denies = new Set();
|
|
1099
|
+
const allowBlock = p.content.match(/allow\s*=\s*\[([^\]]*)\]/gi) || [];
|
|
1100
|
+
const denyBlock = p.content.match(/deny\s*=\s*\[([^\]]*)\]/gi) || [];
|
|
1101
|
+
for (const m of allowBlock) (m.match(/["']([^"']+)["']/g) || []).forEach(t => allows.add(t.replace(/["']/g, '')));
|
|
1102
|
+
for (const m of denyBlock) (m.match(/["']([^"']+)["']/g) || []).forEach(t => denies.add(t.replace(/["']/g, '')));
|
|
1103
|
+
return { filePath: p.filePath, allows, denies };
|
|
1104
|
+
});
|
|
1105
|
+
// Cross-file: tool allowed in one, denied in another
|
|
1106
|
+
for (let i = 0; i < perFile.length; i++) {
|
|
1107
|
+
for (let j = i + 1; j < perFile.length; j++) {
|
|
1108
|
+
for (const tool of perFile[i].allows) {
|
|
1109
|
+
if (perFile[j].denies.has(tool)) return false;
|
|
1110
|
+
}
|
|
1111
|
+
for (const tool of perFile[i].denies) {
|
|
1112
|
+
if (perFile[j].allows.has(tool)) return false;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
return true;
|
|
1117
|
+
},
|
|
1118
|
+
impact: 'high',
|
|
1119
|
+
rating: 4,
|
|
1120
|
+
category: 'sandbox',
|
|
1121
|
+
fix: 'Resolve conflicting allow/deny rules across policy tiers so enforcement is predictable.',
|
|
1122
|
+
template: null,
|
|
1123
|
+
file: (ctx) => {
|
|
1124
|
+
const policies = policyFileContents(ctx);
|
|
1125
|
+
return policies.length > 0 ? policies[0].filePath : null;
|
|
1126
|
+
},
|
|
1127
|
+
line: () => 1,
|
|
1128
|
+
},
|
|
1129
|
+
|
|
1130
|
+
geminiSandboxNotNone: {
|
|
1131
|
+
id: 'GM-F06',
|
|
1132
|
+
name: 'Sandbox mode is not "none" in shared repos',
|
|
1133
|
+
check: (ctx) => {
|
|
1134
|
+
const data = settingsData(ctx);
|
|
1135
|
+
if (!data || !data.sandbox) return null;
|
|
1136
|
+
const mode = typeof data.sandbox === 'string' ? data.sandbox : (data.sandbox.mode || '');
|
|
1137
|
+
if (mode.toLowerCase() !== 'none') return true;
|
|
1138
|
+
// If "none", check for justification
|
|
1139
|
+
const gmd = geminiMd(ctx) || '';
|
|
1140
|
+
return JUSTIFICATION_PATTERNS.test(gmd);
|
|
1141
|
+
},
|
|
1142
|
+
impact: 'high',
|
|
1143
|
+
rating: 5,
|
|
1144
|
+
category: 'sandbox',
|
|
1145
|
+
fix: 'Avoid sandbox.mode = "none" in shared repos. If intentional, document the justification in GEMINI.md.',
|
|
1146
|
+
template: null,
|
|
1147
|
+
file: () => '.gemini/settings.json',
|
|
1148
|
+
line: (ctx) => ctx.lineNumber('.gemini/settings.json', /sandbox/i),
|
|
1149
|
+
},
|
|
1150
|
+
|
|
1151
|
+
geminiPolicyDocumentation: {
|
|
1152
|
+
id: 'GM-F07',
|
|
1153
|
+
name: 'Policy rules are documented for team onboarding',
|
|
1154
|
+
check: (ctx) => {
|
|
1155
|
+
const policies = policyFileContents(ctx);
|
|
1156
|
+
if (policies.length === 0) return null;
|
|
1157
|
+
const docs = docsBundle(ctx);
|
|
1158
|
+
return /\bpolicy\b|\bpolicies\b|\benforcement\b/i.test(docs);
|
|
1159
|
+
},
|
|
1160
|
+
impact: 'low',
|
|
1161
|
+
rating: 3,
|
|
1162
|
+
category: 'sandbox',
|
|
1163
|
+
fix: 'Document policy engine rules in GEMINI.md so new team members understand enforcement boundaries.',
|
|
1164
|
+
template: null,
|
|
1165
|
+
file: () => 'GEMINI.md',
|
|
1166
|
+
line: (ctx) => {
|
|
1167
|
+
const gmd = geminiMd(ctx) || '';
|
|
1168
|
+
return firstLineMatching(gmd, /policy|policies|enforcement/i);
|
|
1169
|
+
},
|
|
1170
|
+
},
|
|
1171
|
+
|
|
1172
|
+
// =============================================
|
|
1173
|
+
// G. Skills & Agents (5 checks) — GM-G01..GM-G05 (v0.5)
|
|
1174
|
+
// =============================================
|
|
1175
|
+
|
|
1176
|
+
geminiAgentsFrontmatter: {
|
|
1177
|
+
id: 'GM-G01',
|
|
1178
|
+
name: 'Agent .md files have YAML frontmatter',
|
|
1179
|
+
check: (ctx) => {
|
|
1180
|
+
const agentFiles = ctx.agentFiles ? ctx.agentFiles() : [];
|
|
1181
|
+
if (agentFiles.length === 0) return null;
|
|
1182
|
+
for (const f of agentFiles) {
|
|
1183
|
+
const content = ctx.fileContent(f) || '';
|
|
1184
|
+
if (!content.trimStart().startsWith('---')) return false;
|
|
1185
|
+
}
|
|
1186
|
+
return true;
|
|
1187
|
+
},
|
|
1188
|
+
impact: 'high',
|
|
1189
|
+
rating: 4,
|
|
1190
|
+
category: 'agents',
|
|
1191
|
+
fix: 'Add YAML frontmatter (---) to all agent .md files under .gemini/agents/ so Gemini can parse agent metadata.',
|
|
1192
|
+
template: null,
|
|
1193
|
+
file: (ctx) => {
|
|
1194
|
+
const agentFiles = ctx.agentFiles ? ctx.agentFiles() : [];
|
|
1195
|
+
for (const f of agentFiles) {
|
|
1196
|
+
const content = ctx.fileContent(f) || '';
|
|
1197
|
+
if (!content.trimStart().startsWith('---')) return f;
|
|
1198
|
+
}
|
|
1199
|
+
return null;
|
|
1200
|
+
},
|
|
1201
|
+
line: () => 1,
|
|
1202
|
+
},
|
|
1203
|
+
|
|
1204
|
+
geminiAgentNamesDescriptive: {
|
|
1205
|
+
id: 'GM-G02',
|
|
1206
|
+
name: 'Agent names are descriptive',
|
|
1207
|
+
check: (ctx) => {
|
|
1208
|
+
const agentFiles = ctx.agentFiles ? ctx.agentFiles() : [];
|
|
1209
|
+
if (agentFiles.length === 0) return null;
|
|
1210
|
+
for (const f of agentFiles) {
|
|
1211
|
+
const name = path.basename(f, '.md');
|
|
1212
|
+
// Flag single-letter or very short non-descriptive names
|
|
1213
|
+
if (name.length < 3 || /^(a|b|c|x|y|z|test|tmp|foo|bar)$/i.test(name)) return false;
|
|
1214
|
+
}
|
|
1215
|
+
return true;
|
|
1216
|
+
},
|
|
1217
|
+
impact: 'low',
|
|
1218
|
+
rating: 3,
|
|
1219
|
+
category: 'agents',
|
|
1220
|
+
fix: 'Use descriptive agent names (e.g., code-reviewer, security-auditor) instead of generic placeholders.',
|
|
1221
|
+
template: null,
|
|
1222
|
+
file: (ctx) => {
|
|
1223
|
+
const agentFiles = ctx.agentFiles ? ctx.agentFiles() : [];
|
|
1224
|
+
for (const f of agentFiles) {
|
|
1225
|
+
const name = path.basename(f, '.md');
|
|
1226
|
+
if (name.length < 3) return f;
|
|
1227
|
+
}
|
|
1228
|
+
return null;
|
|
1229
|
+
},
|
|
1230
|
+
line: () => 1,
|
|
1231
|
+
},
|
|
1232
|
+
|
|
1233
|
+
geminiAgentInstructionsScoped: {
|
|
1234
|
+
id: 'GM-G03',
|
|
1235
|
+
name: 'Agent instructions are scoped (not generic)',
|
|
1236
|
+
check: (ctx) => {
|
|
1237
|
+
const agentFiles = ctx.agentFiles ? ctx.agentFiles() : [];
|
|
1238
|
+
if (agentFiles.length === 0) return null;
|
|
1239
|
+
for (const f of agentFiles) {
|
|
1240
|
+
const content = ctx.fileContent(f) || '';
|
|
1241
|
+
if (FILLER_PATTERNS.some(p => p.test(content))) return false;
|
|
1242
|
+
}
|
|
1243
|
+
return true;
|
|
1244
|
+
},
|
|
1245
|
+
impact: 'medium',
|
|
1246
|
+
rating: 4,
|
|
1247
|
+
category: 'agents',
|
|
1248
|
+
fix: 'Replace generic agent instructions with task-specific guidance so agents stay focused on their role.',
|
|
1249
|
+
template: null,
|
|
1250
|
+
file: (ctx) => {
|
|
1251
|
+
const agentFiles = ctx.agentFiles ? ctx.agentFiles() : [];
|
|
1252
|
+
for (const f of agentFiles) {
|
|
1253
|
+
const content = ctx.fileContent(f) || '';
|
|
1254
|
+
if (FILLER_PATTERNS.some(p => p.test(content))) return f;
|
|
1255
|
+
}
|
|
1256
|
+
return null;
|
|
1257
|
+
},
|
|
1258
|
+
line: (ctx) => {
|
|
1259
|
+
const agentFiles = ctx.agentFiles ? ctx.agentFiles() : [];
|
|
1260
|
+
for (const f of agentFiles) {
|
|
1261
|
+
const content = ctx.fileContent(f) || '';
|
|
1262
|
+
if (FILLER_PATTERNS.some(p => p.test(content))) return findFillerLine(content);
|
|
1263
|
+
}
|
|
1264
|
+
return null;
|
|
1265
|
+
},
|
|
1266
|
+
},
|
|
1267
|
+
|
|
1268
|
+
geminiNoDuplicateAgentNames: {
|
|
1269
|
+
id: 'GM-G04',
|
|
1270
|
+
name: 'No duplicate agent names (global vs project)',
|
|
1271
|
+
check: (ctx) => {
|
|
1272
|
+
const projectAgents = ctx.agentFiles ? ctx.agentFiles() : [];
|
|
1273
|
+
if (projectAgents.length === 0) return null;
|
|
1274
|
+
const projectNames = new Set(projectAgents.map(f => path.basename(f, '.md').toLowerCase()));
|
|
1275
|
+
// Check global agents
|
|
1276
|
+
const homeDir = os.homedir();
|
|
1277
|
+
const globalAgentsDir = path.join(homeDir, '.gemini', 'agents');
|
|
1278
|
+
try {
|
|
1279
|
+
const globalFiles = require('fs').readdirSync(globalAgentsDir).filter(f => f.endsWith('.md'));
|
|
1280
|
+
const globalNames = globalFiles.map(f => path.basename(f, '.md').toLowerCase());
|
|
1281
|
+
for (const name of globalNames) {
|
|
1282
|
+
if (projectNames.has(name)) return false;
|
|
1283
|
+
}
|
|
1284
|
+
} catch {
|
|
1285
|
+
// No global agents
|
|
1286
|
+
}
|
|
1287
|
+
return true;
|
|
1288
|
+
},
|
|
1289
|
+
impact: 'medium',
|
|
1290
|
+
rating: 3,
|
|
1291
|
+
category: 'agents',
|
|
1292
|
+
fix: 'Resolve duplicate agent names between global (~/.gemini/agents/) and project (.gemini/agents/) to avoid shadowing.',
|
|
1293
|
+
template: null,
|
|
1294
|
+
file: (ctx) => {
|
|
1295
|
+
const agentFiles = ctx.agentFiles ? ctx.agentFiles() : [];
|
|
1296
|
+
return agentFiles.length > 0 ? agentFiles[0] : null;
|
|
1297
|
+
},
|
|
1298
|
+
line: () => 1,
|
|
1299
|
+
},
|
|
1300
|
+
|
|
1301
|
+
geminiSkillsDescribed: {
|
|
1302
|
+
id: 'GM-G05',
|
|
1303
|
+
name: 'Skills have clear descriptions for auto-invocation',
|
|
1304
|
+
check: (ctx) => {
|
|
1305
|
+
const skillDirs = ctx.skillDirs ? ctx.skillDirs() : [];
|
|
1306
|
+
if (skillDirs.length === 0) return null;
|
|
1307
|
+
for (const skillName of skillDirs) {
|
|
1308
|
+
// Check for a description file or frontmatter in the skill
|
|
1309
|
+
const readmePath = `.gemini/skills/${skillName}/README.md`;
|
|
1310
|
+
const indexPath = `.gemini/skills/${skillName}/index.md`;
|
|
1311
|
+
const content = ctx.fileContent(readmePath) || ctx.fileContent(indexPath) || '';
|
|
1312
|
+
if (!content || content.trim().length < 10) return false;
|
|
1313
|
+
}
|
|
1314
|
+
return true;
|
|
1315
|
+
},
|
|
1316
|
+
impact: 'high',
|
|
1317
|
+
rating: 4,
|
|
1318
|
+
category: 'skills',
|
|
1319
|
+
fix: 'Give each skill a README.md or index.md with a clear description so Gemini CLI can auto-invoke correctly.',
|
|
1320
|
+
template: null,
|
|
1321
|
+
file: (ctx) => {
|
|
1322
|
+
const skillDirs = ctx.skillDirs ? ctx.skillDirs() : [];
|
|
1323
|
+
return skillDirs.length > 0 ? `.gemini/skills/${skillDirs[0]}` : null;
|
|
1324
|
+
},
|
|
1325
|
+
line: () => 1,
|
|
1326
|
+
},
|
|
1327
|
+
|
|
1328
|
+
// =============================================
|
|
1329
|
+
// H. CI & Automation (4 checks) — GM-H01..GM-H04 (v0.5)
|
|
1330
|
+
// =============================================
|
|
1331
|
+
|
|
1332
|
+
geminiCiAuthEnvVar: {
|
|
1333
|
+
id: 'GM-H01',
|
|
1334
|
+
name: 'Headless mode auth uses env var (not hardcoded key)',
|
|
1335
|
+
check: (ctx) => {
|
|
1336
|
+
for (const wf of workflowArtifacts(ctx)) {
|
|
1337
|
+
if (!/\bgemini\b/i.test(wf.content)) continue;
|
|
1338
|
+
// Check for hardcoded secrets
|
|
1339
|
+
const hasHardcoded = /GEMINI_API_KEY\s*[:=]\s*["']?[A-Za-z0-9_-]{20,}/i.test(wf.content) ||
|
|
1340
|
+
containsEmbeddedSecret(wf.content);
|
|
1341
|
+
if (hasHardcoded) return false;
|
|
1342
|
+
}
|
|
1343
|
+
return true;
|
|
1344
|
+
},
|
|
1345
|
+
impact: 'critical',
|
|
1346
|
+
rating: 5,
|
|
1347
|
+
category: 'automation',
|
|
1348
|
+
fix: 'Use ${{ secrets.GEMINI_API_KEY }} or managed secret injection in CI. Never hardcode API keys in workflow files.',
|
|
1349
|
+
template: null,
|
|
1350
|
+
file: (ctx) => {
|
|
1351
|
+
for (const wf of workflowArtifacts(ctx)) {
|
|
1352
|
+
if (/GEMINI_API_KEY\s*[:=]\s*["']?[A-Za-z0-9_-]{20,}/i.test(wf.content)) return wf.filePath;
|
|
1353
|
+
}
|
|
1354
|
+
return null;
|
|
1355
|
+
},
|
|
1356
|
+
line: (ctx) => {
|
|
1357
|
+
for (const wf of workflowArtifacts(ctx)) {
|
|
1358
|
+
const line = firstLineMatching(wf.content, /GEMINI_API_KEY\s*[:=]\s*["']?[A-Za-z0-9_-]{20,}/i);
|
|
1359
|
+
if (line) return line;
|
|
1360
|
+
}
|
|
1361
|
+
return null;
|
|
1362
|
+
},
|
|
1363
|
+
},
|
|
1364
|
+
|
|
1365
|
+
geminiCiNoYolo: {
|
|
1366
|
+
id: 'GM-H02',
|
|
1367
|
+
name: 'CI scripts don\'t use --yolo',
|
|
1368
|
+
check: (ctx) => {
|
|
1369
|
+
for (const wf of workflowArtifacts(ctx)) {
|
|
1370
|
+
if (/\b--yolo\b/i.test(wf.content)) return false;
|
|
1371
|
+
}
|
|
1372
|
+
return true;
|
|
1373
|
+
},
|
|
1374
|
+
impact: 'critical',
|
|
1375
|
+
rating: 5,
|
|
1376
|
+
category: 'automation',
|
|
1377
|
+
fix: 'Remove --yolo from all CI workflow files. Never bypass safety controls in automated pipelines.',
|
|
1378
|
+
template: null,
|
|
1379
|
+
file: (ctx) => {
|
|
1380
|
+
for (const wf of workflowArtifacts(ctx)) {
|
|
1381
|
+
if (/\b--yolo\b/i.test(wf.content)) return wf.filePath;
|
|
1382
|
+
}
|
|
1383
|
+
return null;
|
|
1384
|
+
},
|
|
1385
|
+
line: (ctx) => {
|
|
1386
|
+
for (const wf of workflowArtifacts(ctx)) {
|
|
1387
|
+
const line = firstLineMatching(wf.content, /--yolo/i);
|
|
1388
|
+
if (line) return line;
|
|
1389
|
+
}
|
|
1390
|
+
return null;
|
|
1391
|
+
},
|
|
1392
|
+
},
|
|
1393
|
+
|
|
1394
|
+
geminiCiEnvVarConflict: {
|
|
1395
|
+
id: 'GM-H03',
|
|
1396
|
+
name: 'CI_* env var conflict awareness (bug #1563)',
|
|
1397
|
+
check: (ctx) => {
|
|
1398
|
+
for (const wf of workflowArtifacts(ctx)) {
|
|
1399
|
+
if (!/\bgemini\b/i.test(wf.content)) continue;
|
|
1400
|
+
// Check if CI_ env vars are set that might force non-interactive mode
|
|
1401
|
+
if (/\bCI_[A-Z_]+\s*[:=]/i.test(wf.content)) {
|
|
1402
|
+
// Check if the issue is acknowledged
|
|
1403
|
+
const gmd = geminiMd(ctx) || '';
|
|
1404
|
+
return /\bCI_\*\b|\bbug\s*#?1563\b|\bnon-interactive\b|\bCI.*env.*var/i.test(gmd) ||
|
|
1405
|
+
/\bCI_\*\b|\bbug.*1563\b|\bnon-interactive/i.test(wf.content);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
return true;
|
|
1409
|
+
},
|
|
1410
|
+
impact: 'medium',
|
|
1411
|
+
rating: 3,
|
|
1412
|
+
category: 'automation',
|
|
1413
|
+
fix: 'Known bug #1563: any CI_* environment variable forces non-interactive mode. Document this or avoid setting CI_* vars in Gemini workflows.',
|
|
1414
|
+
template: null,
|
|
1415
|
+
file: (ctx) => {
|
|
1416
|
+
for (const wf of workflowArtifacts(ctx)) {
|
|
1417
|
+
if (/\bCI_[A-Z_]+\s*[:=]/i.test(wf.content)) return wf.filePath;
|
|
1418
|
+
}
|
|
1419
|
+
return null;
|
|
1420
|
+
},
|
|
1421
|
+
line: (ctx) => {
|
|
1422
|
+
for (const wf of workflowArtifacts(ctx)) {
|
|
1423
|
+
const line = firstLineMatching(wf.content, /CI_[A-Z_]+\s*[:=]/i);
|
|
1424
|
+
if (line) return line;
|
|
1425
|
+
}
|
|
1426
|
+
return null;
|
|
1427
|
+
},
|
|
1428
|
+
},
|
|
1429
|
+
|
|
1430
|
+
geminiCiJsonOutput: {
|
|
1431
|
+
id: 'GM-H04',
|
|
1432
|
+
name: 'Headless output is --json for machine parsing',
|
|
1433
|
+
check: (ctx) => {
|
|
1434
|
+
for (const wf of workflowArtifacts(ctx)) {
|
|
1435
|
+
if (!/\bgemini\b/i.test(wf.content)) continue;
|
|
1436
|
+
// If gemini is used in CI with -p (prompt), check for --json
|
|
1437
|
+
if (/gemini\s+.*-p\b/i.test(wf.content)) {
|
|
1438
|
+
return /--json\b/i.test(wf.content);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
return null; // Not relevant if no headless usage
|
|
1442
|
+
},
|
|
1443
|
+
impact: 'low',
|
|
1444
|
+
rating: 2,
|
|
1445
|
+
category: 'automation',
|
|
1446
|
+
fix: 'Use --json flag when running gemini -p in CI for reliable machine-parseable output.',
|
|
1447
|
+
template: null,
|
|
1448
|
+
file: (ctx) => {
|
|
1449
|
+
for (const wf of workflowArtifacts(ctx)) {
|
|
1450
|
+
if (/gemini\s+.*-p\b/i.test(wf.content) && !/--json\b/i.test(wf.content)) return wf.filePath;
|
|
1451
|
+
}
|
|
1452
|
+
return null;
|
|
1453
|
+
},
|
|
1454
|
+
line: (ctx) => {
|
|
1455
|
+
for (const wf of workflowArtifacts(ctx)) {
|
|
1456
|
+
const line = firstLineMatching(wf.content, /gemini\s+.*-p\b/i);
|
|
1457
|
+
if (line && !/--json\b/i.test(wf.content)) return line;
|
|
1458
|
+
}
|
|
1459
|
+
return null;
|
|
1460
|
+
},
|
|
1461
|
+
},
|
|
1462
|
+
|
|
1463
|
+
// =============================================
|
|
1464
|
+
// I. Extensions (5 checks) — GM-I01..GM-I05 (v0.5)
|
|
1465
|
+
// =============================================
|
|
1466
|
+
|
|
1467
|
+
geminiSkillNamingConvention: {
|
|
1468
|
+
id: 'GM-I01',
|
|
1469
|
+
name: 'Skill directories follow naming conventions',
|
|
1470
|
+
check: (ctx) => {
|
|
1471
|
+
const skillDirs = ctx.skillDirs ? ctx.skillDirs() : [];
|
|
1472
|
+
if (skillDirs.length === 0) return null;
|
|
1473
|
+
return skillDirs.every(name => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name));
|
|
1474
|
+
},
|
|
1475
|
+
impact: 'medium',
|
|
1476
|
+
rating: 3,
|
|
1477
|
+
category: 'extensions',
|
|
1478
|
+
fix: 'Use kebab-case for skill directory names (e.g., code-reviewer, not CodeReviewer).',
|
|
1479
|
+
template: null,
|
|
1480
|
+
file: (ctx) => {
|
|
1481
|
+
const skillDirs = ctx.skillDirs ? ctx.skillDirs() : [];
|
|
1482
|
+
const bad = skillDirs.find(name => !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name));
|
|
1483
|
+
return bad ? `.gemini/skills/${bad}` : null;
|
|
1484
|
+
},
|
|
1485
|
+
line: () => 1,
|
|
1486
|
+
},
|
|
1487
|
+
|
|
1488
|
+
geminiExtensionsTrusted: {
|
|
1489
|
+
id: 'GM-I02',
|
|
1490
|
+
name: 'Extensions are from trusted sources',
|
|
1491
|
+
check: (ctx) => {
|
|
1492
|
+
const extDirs = ctx.extensionDirs ? ctx.extensionDirs() : [];
|
|
1493
|
+
if (extDirs.length === 0) return null;
|
|
1494
|
+
// Check that extensions have documentation about their source
|
|
1495
|
+
const docs = docsBundle(ctx);
|
|
1496
|
+
return /\bextension\b.*\btrusted\b|\bextension\b.*\bverified\b|\bextension\b.*\bsource\b/i.test(docs);
|
|
1497
|
+
},
|
|
1498
|
+
impact: 'high',
|
|
1499
|
+
rating: 4,
|
|
1500
|
+
category: 'extensions',
|
|
1501
|
+
fix: 'Document the source and trust status of installed extensions in GEMINI.md.',
|
|
1502
|
+
template: null,
|
|
1503
|
+
file: () => 'GEMINI.md',
|
|
1504
|
+
line: (ctx) => {
|
|
1505
|
+
const gmd = geminiMd(ctx) || '';
|
|
1506
|
+
return firstLineMatching(gmd, /extension/i);
|
|
1507
|
+
},
|
|
1508
|
+
},
|
|
1509
|
+
|
|
1510
|
+
geminiExtensionMcpSafe: {
|
|
1511
|
+
id: 'GM-I03',
|
|
1512
|
+
name: 'Extension MCP configs don\'t override project security',
|
|
1513
|
+
check: (ctx) => {
|
|
1514
|
+
const extDirs = ctx.extensionDirs ? ctx.extensionDirs() : [];
|
|
1515
|
+
if (extDirs.length === 0) return null;
|
|
1516
|
+
// Check extension settings for MCP overrides
|
|
1517
|
+
for (const ext of extDirs) {
|
|
1518
|
+
const settingsPath = `.gemini/extensions/${ext}/settings.json`;
|
|
1519
|
+
const content = ctx.fileContent(settingsPath) || '';
|
|
1520
|
+
if (content) {
|
|
1521
|
+
try {
|
|
1522
|
+
const data = JSON.parse(content);
|
|
1523
|
+
// Flag if extension adds MCP servers without excludeTools
|
|
1524
|
+
if (data.mcpServers) {
|
|
1525
|
+
for (const server of Object.values(data.mcpServers)) {
|
|
1526
|
+
if (server && !server.excludeTools) return false;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
} catch {
|
|
1530
|
+
// Ignore parse errors
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
return true;
|
|
1535
|
+
},
|
|
1536
|
+
impact: 'high',
|
|
1537
|
+
rating: 4,
|
|
1538
|
+
category: 'extensions',
|
|
1539
|
+
fix: 'Ensure extension MCP configs use excludeTools and don\'t silently override project security settings.',
|
|
1540
|
+
template: null,
|
|
1541
|
+
file: (ctx) => {
|
|
1542
|
+
const extDirs = ctx.extensionDirs ? ctx.extensionDirs() : [];
|
|
1543
|
+
for (const ext of extDirs) {
|
|
1544
|
+
const settingsPath = `.gemini/extensions/${ext}/settings.json`;
|
|
1545
|
+
if (ctx.fileContent(settingsPath)) return settingsPath;
|
|
1546
|
+
}
|
|
1547
|
+
return null;
|
|
1548
|
+
},
|
|
1549
|
+
line: () => 1,
|
|
1550
|
+
},
|
|
1551
|
+
|
|
1552
|
+
geminiNoOrphanedSkillRefs: {
|
|
1553
|
+
id: 'GM-I04',
|
|
1554
|
+
name: 'No orphaned skill references',
|
|
1555
|
+
check: (ctx) => {
|
|
1556
|
+
const gmd = geminiMd(ctx) || '';
|
|
1557
|
+
const skillDirs = ctx.skillDirs ? ctx.skillDirs() : [];
|
|
1558
|
+
const skillNames = new Set(skillDirs.map(n => n.toLowerCase()));
|
|
1559
|
+
// Find skill references in GEMINI.md
|
|
1560
|
+
const refs = gmd.match(/\.gemini\/skills\/([a-z0-9-]+)/gi) || [];
|
|
1561
|
+
for (const ref of refs) {
|
|
1562
|
+
const name = ref.split('/').pop().toLowerCase();
|
|
1563
|
+
if (!skillNames.has(name)) return false;
|
|
1564
|
+
}
|
|
1565
|
+
return true;
|
|
1566
|
+
},
|
|
1567
|
+
impact: 'low',
|
|
1568
|
+
rating: 2,
|
|
1569
|
+
category: 'extensions',
|
|
1570
|
+
fix: 'Remove references to skills that no longer exist in .gemini/skills/.',
|
|
1571
|
+
template: null,
|
|
1572
|
+
file: () => 'GEMINI.md',
|
|
1573
|
+
line: (ctx) => {
|
|
1574
|
+
const gmd = geminiMd(ctx) || '';
|
|
1575
|
+
return firstLineMatching(gmd, /\.gemini\/skills\//i);
|
|
1576
|
+
},
|
|
1577
|
+
},
|
|
1578
|
+
|
|
1579
|
+
geminiMemoryContentIntentional: {
|
|
1580
|
+
id: 'GM-I05',
|
|
1581
|
+
name: '/memory content is intentional (not accumulated junk)',
|
|
1582
|
+
check: (ctx) => {
|
|
1583
|
+
// Check for a .gemini/memory file or memory-related config
|
|
1584
|
+
const memoryContent = ctx.fileContent('.gemini/memory.md') || ctx.fileContent('.gemini/memory') || '';
|
|
1585
|
+
if (!memoryContent) return null;
|
|
1586
|
+
const lines = memoryContent.split(/\r?\n/).filter(l => l.trim());
|
|
1587
|
+
// Flag if memory has become very large (>100 lines) without organization
|
|
1588
|
+
if (lines.length > 100 && !countSections(memoryContent)) return false;
|
|
1589
|
+
return true;
|
|
1590
|
+
},
|
|
1591
|
+
impact: 'medium',
|
|
1592
|
+
rating: 3,
|
|
1593
|
+
category: 'extensions',
|
|
1594
|
+
fix: 'Review /memory content periodically. Remove stale entries and organize into sections if it grows large.',
|
|
1595
|
+
template: null,
|
|
1596
|
+
file: () => '.gemini/memory.md',
|
|
1597
|
+
line: () => 1,
|
|
1598
|
+
},
|
|
1599
|
+
|
|
1600
|
+
// =============================================
|
|
1601
|
+
// J. Review & Workflow (4 checks) — GM-J01..GM-J04 (v1.0)
|
|
1602
|
+
// =============================================
|
|
1603
|
+
|
|
1604
|
+
geminiRateLimitAwareness: {
|
|
1605
|
+
id: 'GM-J01',
|
|
1606
|
+
name: 'Rate limit/quota awareness documented',
|
|
1607
|
+
check: (ctx) => {
|
|
1608
|
+
const gmd = geminiMd(ctx) || '';
|
|
1609
|
+
return /\brate limit\b|\bquota\b|\brequests? per\b|\bcost\b|\btoken\b.*\blimit\b/i.test(gmd);
|
|
1610
|
+
},
|
|
1611
|
+
impact: 'medium',
|
|
1612
|
+
rating: 3,
|
|
1613
|
+
category: 'review',
|
|
1614
|
+
fix: 'Document rate limit and quota awareness in GEMINI.md. Free tier hits limits after 10-20 requests.',
|
|
1615
|
+
template: 'gemini-md',
|
|
1616
|
+
file: () => 'GEMINI.md',
|
|
1617
|
+
line: (ctx) => {
|
|
1618
|
+
const gmd = geminiMd(ctx) || '';
|
|
1619
|
+
return firstLineMatching(gmd, /rate limit|quota|requests? per|cost|token.*limit/i);
|
|
1620
|
+
},
|
|
1621
|
+
},
|
|
1622
|
+
|
|
1623
|
+
geminiRetryStrategy: {
|
|
1624
|
+
id: 'GM-J02',
|
|
1625
|
+
name: 'Retry/fallback strategy for rate limiting',
|
|
1626
|
+
check: (ctx) => {
|
|
1627
|
+
const gmd = geminiMd(ctx) || '';
|
|
1628
|
+
const raw = settingsRaw(ctx);
|
|
1629
|
+
const combined = `${gmd}\n${raw}`;
|
|
1630
|
+
// Only check if rate limiting is a concern (i.e., docs mention it)
|
|
1631
|
+
if (!/\brate\b|\bquota\b|\bfree tier\b/i.test(combined)) return null;
|
|
1632
|
+
return /\bretry\b|\bfallback\b|\bbackoff\b|\bexponential\b/i.test(combined);
|
|
1633
|
+
},
|
|
1634
|
+
impact: 'medium',
|
|
1635
|
+
rating: 3,
|
|
1636
|
+
category: 'review',
|
|
1637
|
+
fix: 'Document a retry or fallback strategy for rate limiting situations.',
|
|
1638
|
+
template: null,
|
|
1639
|
+
file: () => 'GEMINI.md',
|
|
1640
|
+
line: (ctx) => {
|
|
1641
|
+
const gmd = geminiMd(ctx) || '';
|
|
1642
|
+
return firstLineMatching(gmd, /retry|fallback|backoff/i);
|
|
1643
|
+
},
|
|
1644
|
+
},
|
|
1645
|
+
|
|
1646
|
+
geminiSessionPersistence: {
|
|
1647
|
+
id: 'GM-J03',
|
|
1648
|
+
name: 'Session history persistence is configured',
|
|
1649
|
+
check: (ctx) => {
|
|
1650
|
+
const data = settingsData(ctx);
|
|
1651
|
+
if (!data) return null;
|
|
1652
|
+
// Check if session/history settings are explicit
|
|
1653
|
+
return data.history !== undefined || data.session !== undefined || data.telemetry !== undefined;
|
|
1654
|
+
},
|
|
1655
|
+
impact: 'low',
|
|
1656
|
+
rating: 2,
|
|
1657
|
+
category: 'review',
|
|
1658
|
+
fix: 'Set session and history persistence settings explicitly in .gemini/settings.json.',
|
|
1659
|
+
template: 'gemini-settings',
|
|
1660
|
+
file: () => '.gemini/settings.json',
|
|
1661
|
+
line: (ctx) => ctx.lineNumber('.gemini/settings.json', /history|session|telemetry/i),
|
|
1662
|
+
},
|
|
1663
|
+
|
|
1664
|
+
geminiNoSensitiveMemory: {
|
|
1665
|
+
id: 'GM-J04',
|
|
1666
|
+
name: 'No sensitive data in saved memory',
|
|
1667
|
+
check: (ctx) => {
|
|
1668
|
+
const memoryContent = ctx.fileContent('.gemini/memory.md') || ctx.fileContent('.gemini/memory') || '';
|
|
1669
|
+
if (!memoryContent) return null;
|
|
1670
|
+
return !containsEmbeddedSecret(memoryContent);
|
|
1671
|
+
},
|
|
1672
|
+
impact: 'high',
|
|
1673
|
+
rating: 4,
|
|
1674
|
+
category: 'review',
|
|
1675
|
+
fix: 'Remove secrets and sensitive data from saved memory files.',
|
|
1676
|
+
template: null,
|
|
1677
|
+
file: () => '.gemini/memory.md',
|
|
1678
|
+
line: (ctx) => {
|
|
1679
|
+
const content = ctx.fileContent('.gemini/memory.md') || ctx.fileContent('.gemini/memory') || '';
|
|
1680
|
+
return content ? findSecretLine(content) : null;
|
|
1681
|
+
},
|
|
1682
|
+
},
|
|
1683
|
+
|
|
1684
|
+
// =============================================
|
|
1685
|
+
// K. Quality Deep (5 checks) — GM-K01..GM-K05 (v1.0)
|
|
1686
|
+
// =============================================
|
|
1687
|
+
|
|
1688
|
+
geminiMdModernFeatures: {
|
|
1689
|
+
id: 'GM-K01',
|
|
1690
|
+
name: 'GEMINI.md mentions modern features (skills, extensions, hooks)',
|
|
1691
|
+
check: (ctx) => {
|
|
1692
|
+
const gmd = geminiMd(ctx) || '';
|
|
1693
|
+
if (!gmd) return null;
|
|
1694
|
+
const skillDirs = ctx.skillDirs ? ctx.skillDirs() : [];
|
|
1695
|
+
const agentFiles = ctx.agentFiles ? ctx.agentFiles() : [];
|
|
1696
|
+
const hooks = hooksFromSettings(ctx);
|
|
1697
|
+
const extDirs = ctx.extensionDirs ? ctx.extensionDirs() : [];
|
|
1698
|
+
const hasModernSurfaces = skillDirs.length > 0 || agentFiles.length > 0 || hooks || extDirs.length > 0;
|
|
1699
|
+
if (!hasModernSurfaces) return null;
|
|
1700
|
+
return /\bskills?\b|\bextensions?\b|\bhooks?\b|\bagents?\b/i.test(gmd);
|
|
1701
|
+
},
|
|
1702
|
+
impact: 'medium',
|
|
1703
|
+
rating: 3,
|
|
1704
|
+
category: 'quality-deep',
|
|
1705
|
+
fix: 'If the repo uses skills, extensions, hooks, or agents, mention these in GEMINI.md so Gemini CLI gets the right context.',
|
|
1706
|
+
template: 'gemini-md',
|
|
1707
|
+
file: () => 'GEMINI.md',
|
|
1708
|
+
line: () => 1,
|
|
1709
|
+
},
|
|
1710
|
+
|
|
1711
|
+
geminiNoDeprecatedPatterns: {
|
|
1712
|
+
id: 'GM-K02',
|
|
1713
|
+
name: 'No deprecated Gemini patterns',
|
|
1714
|
+
check: (ctx) => {
|
|
1715
|
+
const gmd = geminiMd(ctx) || '';
|
|
1716
|
+
const raw = settingsRaw(ctx);
|
|
1717
|
+
const combined = `${gmd}\n${raw}`;
|
|
1718
|
+
// Check for deprecated patterns
|
|
1719
|
+
const deprecated = [
|
|
1720
|
+
/\bsandbox_mode\b/,
|
|
1721
|
+
/\bmax_tokens\b/,
|
|
1722
|
+
/\bmcp_servers\b/,
|
|
1723
|
+
/\bgemini-mini\b/i,
|
|
1724
|
+
];
|
|
1725
|
+
return !deprecated.some(p => p.test(combined));
|
|
1726
|
+
},
|
|
1727
|
+
impact: 'medium',
|
|
1728
|
+
rating: 3,
|
|
1729
|
+
category: 'quality-deep',
|
|
1730
|
+
fix: 'Update deprecated Gemini patterns to their current equivalents.',
|
|
1731
|
+
template: null,
|
|
1732
|
+
file: (ctx) => {
|
|
1733
|
+
const raw = settingsRaw(ctx);
|
|
1734
|
+
if (/\bsandbox_mode\b|\bmax_tokens\b|\bmcp_servers\b|\bgemini-mini\b/i.test(raw)) return '.gemini/settings.json';
|
|
1735
|
+
return 'GEMINI.md';
|
|
1736
|
+
},
|
|
1737
|
+
line: (ctx) => {
|
|
1738
|
+
const raw = settingsRaw(ctx);
|
|
1739
|
+
return firstLineMatching(raw, /sandbox_mode|max_tokens|mcp_servers|gemini-mini/i) ||
|
|
1740
|
+
firstLineMatching(geminiMd(ctx) || '', /sandbox_mode|max_tokens|mcp_servers|gemini-mini/i);
|
|
1741
|
+
},
|
|
1742
|
+
},
|
|
1743
|
+
|
|
1744
|
+
geminiComponentMdForMonorepo: {
|
|
1745
|
+
id: 'GM-K03',
|
|
1746
|
+
name: 'Component-level GEMINI.md used for monorepo sections',
|
|
1747
|
+
check: (ctx) => {
|
|
1748
|
+
const gmd = geminiMd(ctx) || '';
|
|
1749
|
+
// Only relevant for monorepos
|
|
1750
|
+
const isMonorepo = ctx.fileContent('lerna.json') || ctx.fileContent('pnpm-workspace.yaml') ||
|
|
1751
|
+
ctx.hasDir('packages') || ctx.hasDir('apps');
|
|
1752
|
+
if (!isMonorepo) return null;
|
|
1753
|
+
// Check for component-level GEMINI.md files
|
|
1754
|
+
const dirs = ['packages', 'apps', 'services', 'libs'];
|
|
1755
|
+
for (const dir of dirs) {
|
|
1756
|
+
if (!ctx.hasDir(dir)) continue;
|
|
1757
|
+
try {
|
|
1758
|
+
const subdirs = require('fs').readdirSync(path.join(ctx.dir, dir), { withFileTypes: true })
|
|
1759
|
+
.filter(e => e.isDirectory())
|
|
1760
|
+
.slice(0, 5); // Check first 5 packages
|
|
1761
|
+
const hasComponent = subdirs.some(d => {
|
|
1762
|
+
const mdPath = path.join(dir, d.name, 'GEMINI.md');
|
|
1763
|
+
return Boolean(ctx.fileContent(mdPath));
|
|
1764
|
+
});
|
|
1765
|
+
if (hasComponent) return true;
|
|
1766
|
+
} catch {
|
|
1767
|
+
continue;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
return false;
|
|
1771
|
+
},
|
|
1772
|
+
impact: 'low',
|
|
1773
|
+
rating: 2,
|
|
1774
|
+
category: 'quality-deep',
|
|
1775
|
+
fix: 'For monorepos, add component-level GEMINI.md files in package subdirectories for JIT loading.',
|
|
1776
|
+
template: null,
|
|
1777
|
+
file: () => 'GEMINI.md',
|
|
1778
|
+
line: () => 1,
|
|
1779
|
+
},
|
|
1780
|
+
|
|
1781
|
+
geminiFlashVsProDocumented: {
|
|
1782
|
+
id: 'GM-K04',
|
|
1783
|
+
name: 'Flash vs Pro model implications documented',
|
|
1784
|
+
check: (ctx) => {
|
|
1785
|
+
const gmd = geminiMd(ctx) || '';
|
|
1786
|
+
const data = settingsData(ctx);
|
|
1787
|
+
if (!data || !data.model) return null;
|
|
1788
|
+
const model = String(data.model).toLowerCase();
|
|
1789
|
+
// If using a specific model, check that implications are documented
|
|
1790
|
+
if (/flash|pro/i.test(model)) {
|
|
1791
|
+
return /\bflash\b|\bpro\b|\bmodel\b.*\b(fast|cheap|accurate|expensive|quality)\b/i.test(gmd);
|
|
1792
|
+
}
|
|
1793
|
+
return null;
|
|
1794
|
+
},
|
|
1795
|
+
impact: 'medium',
|
|
1796
|
+
rating: 3,
|
|
1797
|
+
category: 'quality-deep',
|
|
1798
|
+
fix: 'Document why the chosen model (Flash vs Pro) is appropriate for this project\'s workflows.',
|
|
1799
|
+
template: null,
|
|
1800
|
+
file: () => 'GEMINI.md',
|
|
1801
|
+
line: (ctx) => {
|
|
1802
|
+
const gmd = geminiMd(ctx) || '';
|
|
1803
|
+
return firstLineMatching(gmd, /flash|pro|model.*fast|model.*quality/i);
|
|
1804
|
+
},
|
|
1805
|
+
},
|
|
1806
|
+
|
|
1807
|
+
geminiTokenUsageAwareness: {
|
|
1808
|
+
id: 'GM-K05',
|
|
1809
|
+
name: 'Token usage awareness in GEMINI.md',
|
|
1810
|
+
check: (ctx) => {
|
|
1811
|
+
const gmd = geminiMd(ctx) || '';
|
|
1812
|
+
if (!gmd) return null;
|
|
1813
|
+
return /\btoken\b|\bcontext window\b|\bcontext length\b|\b1M\b|\btruncat/i.test(gmd);
|
|
1814
|
+
},
|
|
1815
|
+
impact: 'low',
|
|
1816
|
+
rating: 2,
|
|
1817
|
+
category: 'quality-deep',
|
|
1818
|
+
fix: 'Add a note about token usage and context window awareness to GEMINI.md.',
|
|
1819
|
+
template: null,
|
|
1820
|
+
file: () => 'GEMINI.md',
|
|
1821
|
+
line: (ctx) => {
|
|
1822
|
+
const gmd = geminiMd(ctx) || '';
|
|
1823
|
+
return firstLineMatching(gmd, /token|context window|context length|1M|truncat/i);
|
|
1824
|
+
},
|
|
1825
|
+
},
|
|
1826
|
+
|
|
1827
|
+
// =============================================
|
|
1828
|
+
// L. Commands (5 checks) — GM-L01..GM-L05 (v1.0)
|
|
1829
|
+
// =============================================
|
|
1830
|
+
|
|
1831
|
+
geminiCommandsExist: {
|
|
1832
|
+
id: 'GM-L01',
|
|
1833
|
+
name: 'Custom commands exist in .gemini/commands/',
|
|
1834
|
+
check: (ctx) => {
|
|
1835
|
+
const commandFiles = ctx.commandFiles ? ctx.commandFiles() : [];
|
|
1836
|
+
return commandFiles.length > 0;
|
|
1837
|
+
},
|
|
1838
|
+
impact: 'medium',
|
|
1839
|
+
rating: 3,
|
|
1840
|
+
category: 'commands',
|
|
1841
|
+
fix: 'Create custom commands under .gemini/commands/*.toml for frequently-used workflows.',
|
|
1842
|
+
template: null,
|
|
1843
|
+
file: () => '.gemini/commands',
|
|
1844
|
+
line: () => null,
|
|
1845
|
+
},
|
|
1846
|
+
|
|
1847
|
+
geminiCommandsHaveDescription: {
|
|
1848
|
+
id: 'GM-L02',
|
|
1849
|
+
name: 'Commands have description field',
|
|
1850
|
+
check: (ctx) => {
|
|
1851
|
+
const commandFiles = ctx.commandFiles ? ctx.commandFiles() : [];
|
|
1852
|
+
if (commandFiles.length === 0) return null;
|
|
1853
|
+
for (const f of commandFiles) {
|
|
1854
|
+
const content = ctx.fileContent(f) || '';
|
|
1855
|
+
if (!/\bdescription\s*=/i.test(content)) return false;
|
|
1856
|
+
}
|
|
1857
|
+
return true;
|
|
1858
|
+
},
|
|
1859
|
+
impact: 'low',
|
|
1860
|
+
rating: 2,
|
|
1861
|
+
category: 'commands',
|
|
1862
|
+
fix: 'Add a description field to each command TOML file for discoverability.',
|
|
1863
|
+
template: null,
|
|
1864
|
+
file: (ctx) => {
|
|
1865
|
+
const commandFiles = ctx.commandFiles ? ctx.commandFiles() : [];
|
|
1866
|
+
for (const f of commandFiles) {
|
|
1867
|
+
const content = ctx.fileContent(f) || '';
|
|
1868
|
+
if (!/\bdescription\s*=/i.test(content)) return f;
|
|
1869
|
+
}
|
|
1870
|
+
return null;
|
|
1871
|
+
},
|
|
1872
|
+
line: () => 1,
|
|
1873
|
+
},
|
|
1874
|
+
|
|
1875
|
+
geminiCommandsNoUnsafeShellInjection: {
|
|
1876
|
+
id: 'GM-L03',
|
|
1877
|
+
name: 'Commands don\'t use unsafe !{} shell injection',
|
|
1878
|
+
check: (ctx) => {
|
|
1879
|
+
const commandFiles = ctx.commandFiles ? ctx.commandFiles() : [];
|
|
1880
|
+
if (commandFiles.length === 0) return null;
|
|
1881
|
+
for (const f of commandFiles) {
|
|
1882
|
+
const content = ctx.fileContent(f) || '';
|
|
1883
|
+
if (/!\{[^}]+\}/.test(content)) return false;
|
|
1884
|
+
}
|
|
1885
|
+
return true;
|
|
1886
|
+
},
|
|
1887
|
+
impact: 'high',
|
|
1888
|
+
rating: 4,
|
|
1889
|
+
category: 'commands',
|
|
1890
|
+
fix: 'SECURITY: Remove !{} shell injection from commands. This is unique to Gemini and allows arbitrary shell execution.',
|
|
1891
|
+
template: null,
|
|
1892
|
+
file: (ctx) => {
|
|
1893
|
+
const commandFiles = ctx.commandFiles ? ctx.commandFiles() : [];
|
|
1894
|
+
for (const f of commandFiles) {
|
|
1895
|
+
const content = ctx.fileContent(f) || '';
|
|
1896
|
+
if (/!\{[^}]+\}/.test(content)) return f;
|
|
1897
|
+
}
|
|
1898
|
+
return null;
|
|
1899
|
+
},
|
|
1900
|
+
line: (ctx) => {
|
|
1901
|
+
const commandFiles = ctx.commandFiles ? ctx.commandFiles() : [];
|
|
1902
|
+
for (const f of commandFiles) {
|
|
1903
|
+
const content = ctx.fileContent(f) || '';
|
|
1904
|
+
const line = firstLineMatching(content, /!\{[^}]+\}/);
|
|
1905
|
+
if (line) return line;
|
|
1906
|
+
}
|
|
1907
|
+
return null;
|
|
1908
|
+
},
|
|
1909
|
+
},
|
|
1910
|
+
|
|
1911
|
+
geminiCommandsUseArgs: {
|
|
1912
|
+
id: 'GM-L04',
|
|
1913
|
+
name: 'Commands use {{args}} for flexibility',
|
|
1914
|
+
check: (ctx) => {
|
|
1915
|
+
const commandFiles = ctx.commandFiles ? ctx.commandFiles() : [];
|
|
1916
|
+
if (commandFiles.length === 0) return null;
|
|
1917
|
+
// At least one command should use args for flexibility
|
|
1918
|
+
return commandFiles.some(f => {
|
|
1919
|
+
const content = ctx.fileContent(f) || '';
|
|
1920
|
+
return /\{\{args?\}\}/i.test(content);
|
|
1921
|
+
});
|
|
1922
|
+
},
|
|
1923
|
+
impact: 'low',
|
|
1924
|
+
rating: 2,
|
|
1925
|
+
category: 'commands',
|
|
1926
|
+
fix: 'Use {{args}} in at least some commands to allow flexible invocation.',
|
|
1927
|
+
template: null,
|
|
1928
|
+
file: (ctx) => {
|
|
1929
|
+
const commandFiles = ctx.commandFiles ? ctx.commandFiles() : [];
|
|
1930
|
+
return commandFiles.length > 0 ? commandFiles[0] : null;
|
|
1931
|
+
},
|
|
1932
|
+
line: () => 1,
|
|
1933
|
+
},
|
|
1934
|
+
|
|
1935
|
+
geminiCommandTomlValid: {
|
|
1936
|
+
id: 'GM-L05',
|
|
1937
|
+
name: 'Custom command TOML is valid',
|
|
1938
|
+
check: (ctx) => {
|
|
1939
|
+
const commandFiles = ctx.commandFiles ? ctx.commandFiles() : [];
|
|
1940
|
+
if (commandFiles.length === 0) return null;
|
|
1941
|
+
for (const f of commandFiles) {
|
|
1942
|
+
const parsed = ctx.commandConfig ? ctx.commandConfig(f) : null;
|
|
1943
|
+
if (parsed && !parsed.ok) return false;
|
|
1944
|
+
}
|
|
1945
|
+
return true;
|
|
1946
|
+
},
|
|
1947
|
+
impact: 'high',
|
|
1948
|
+
rating: 4,
|
|
1949
|
+
category: 'commands',
|
|
1950
|
+
fix: 'Fix malformed TOML in command files so Gemini CLI can load them correctly.',
|
|
1951
|
+
template: null,
|
|
1952
|
+
file: (ctx) => {
|
|
1953
|
+
const commandFiles = ctx.commandFiles ? ctx.commandFiles() : [];
|
|
1954
|
+
for (const f of commandFiles) {
|
|
1955
|
+
const parsed = ctx.commandConfig ? ctx.commandConfig(f) : null;
|
|
1956
|
+
if (parsed && !parsed.ok) return f;
|
|
1957
|
+
}
|
|
1958
|
+
return null;
|
|
1959
|
+
},
|
|
1960
|
+
line: (ctx) => {
|
|
1961
|
+
const commandFiles = ctx.commandFiles ? ctx.commandFiles() : [];
|
|
1962
|
+
for (const f of commandFiles) {
|
|
1963
|
+
const parsed = ctx.commandConfig ? ctx.commandConfig(f) : null;
|
|
1964
|
+
if (parsed && !parsed.ok && parsed.error) {
|
|
1965
|
+
const match = parsed.error.match(/Line (\d+)/i);
|
|
1966
|
+
if (match) return Number(match[1]);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
return 1;
|
|
1970
|
+
},
|
|
1971
|
+
},
|
|
1972
|
+
|
|
1973
|
+
// =============================================
|
|
1974
|
+
// CP-08 Expansion: M. Advisory Quality (4 checks)
|
|
1975
|
+
// =============================================
|
|
1976
|
+
geminiAdvisoryAugmentQuality: {
|
|
1977
|
+
id: 'GM-M01', name: 'Augment recommendations reference real detected surfaces',
|
|
1978
|
+
check: (ctx) => { const g = ctx.geminiMdContent(); const s = ctx.settingsJson(); if (!g && !s) return null; return [Boolean(g), Boolean(s), ctx.hasDir ? ctx.hasDir('.gemini') : false].filter(Boolean).length >= 2; },
|
|
1979
|
+
impact: 'high', rating: 4, category: 'advisory',
|
|
1980
|
+
fix: 'Ensure GEMINI.md and .gemini/settings.json exist for grounded advisory recommendations.',
|
|
1981
|
+
template: 'gemini-md', file: () => 'GEMINI.md', line: () => 1,
|
|
1982
|
+
},
|
|
1983
|
+
geminiAdvisorySuggestOnlySafety: {
|
|
1984
|
+
id: 'GM-M02', name: 'No --yolo or auto_edit in suggest-only context',
|
|
1985
|
+
check: (ctx) => { const s = ctx.settingsJson(); if (!s) return null; const mode = s.approvalMode || s.approval_mode; return !mode || (mode !== 'auto_edit' && mode !== 'yolo'); },
|
|
1986
|
+
impact: 'critical', rating: 5, category: 'advisory',
|
|
1987
|
+
fix: 'Remove --yolo or auto_edit from settings to maintain suggest-only safety.',
|
|
1988
|
+
template: 'gemini-settings', file: () => '.gemini/settings.json', line: () => 1,
|
|
1989
|
+
},
|
|
1990
|
+
geminiAdvisoryOutputFreshness: {
|
|
1991
|
+
id: 'GM-M03', name: 'No deprecated Gemini features referenced in advisory context',
|
|
1992
|
+
check: (ctx) => { const g = ctx.geminiMdContent(); if (!g) return null; return !/\bnotepads?\b/i.test(g) && !/\bchat_model\b/i.test(g); },
|
|
1993
|
+
impact: 'medium', rating: 3, category: 'advisory',
|
|
1994
|
+
fix: 'Remove deprecated feature references from GEMINI.md.',
|
|
1995
|
+
template: 'gemini-md', file: () => 'GEMINI.md', line: () => 1,
|
|
1996
|
+
},
|
|
1997
|
+
geminiAdvisoryToSetupCoherence: {
|
|
1998
|
+
id: 'GM-M04', name: 'Advisory recommendations map to existing proposal families',
|
|
1999
|
+
check: (ctx) => { const g = ctx.geminiMdContent(); const s = ctx.settingsJson(); return Boolean(g || s); },
|
|
2000
|
+
impact: 'medium', rating: 3, category: 'advisory',
|
|
2001
|
+
fix: 'Ensure at least one Gemini surface exists so advisory can produce actionable recommendations.',
|
|
2002
|
+
template: 'gemini-md', file: () => 'GEMINI.md', line: () => 1,
|
|
2003
|
+
},
|
|
2004
|
+
|
|
2005
|
+
// CP-08: N. Pack Posture (4 checks)
|
|
2006
|
+
geminiDomainPackAlignment: {
|
|
2007
|
+
id: 'GM-N01', name: 'Detected stack aligns with recommended domain pack',
|
|
2008
|
+
check: (ctx) => { const g = ctx.geminiMdContent(); if (!g) return null; return true; },
|
|
2009
|
+
impact: 'high', rating: 4, category: 'pack-posture',
|
|
2010
|
+
fix: 'Review recommended domain pack alignment for your project stack.',
|
|
2011
|
+
template: 'gemini-md', file: () => 'GEMINI.md', line: () => 1,
|
|
2012
|
+
},
|
|
2013
|
+
geminiMcpPackSafety: {
|
|
2014
|
+
id: 'GM-N02', name: 'MCP packs pass trust preflight (excludeTools set)',
|
|
2015
|
+
check: (ctx) => { const s = ctx.settingsJson(); if (!s || !s.mcpServers) return null; for (const srv of Object.values(s.mcpServers)) { if (!srv.excludeTools && !srv.includeTools) return false; } return true; },
|
|
2016
|
+
impact: 'high', rating: 4, category: 'pack-posture',
|
|
2017
|
+
fix: 'Add excludeTools to all MCP servers to limit tool surface.',
|
|
2018
|
+
template: 'gemini-settings', file: () => '.gemini/settings.json', line: () => 1,
|
|
2019
|
+
},
|
|
2020
|
+
geminiPackRecommendationQuality: {
|
|
2021
|
+
id: 'GM-N03', name: 'Pack recommendations grounded in detected signals',
|
|
2022
|
+
check: (ctx) => { const g = ctx.geminiMdContent(); const s = ctx.settingsJson(); const p = ctx.jsonFile('package.json'); return [Boolean(g), Boolean(s), Boolean(p)].filter(Boolean).length >= 2; },
|
|
2023
|
+
impact: 'medium', rating: 3, category: 'pack-posture',
|
|
2024
|
+
fix: 'Add GEMINI.md and package.json for grounded pack recommendations.',
|
|
2025
|
+
template: 'gemini-md', file: () => 'GEMINI.md', line: () => 1,
|
|
2026
|
+
},
|
|
2027
|
+
geminiNoStalePackVersions: {
|
|
2028
|
+
id: 'GM-N04', name: 'No stale or deprecated MCP pack references',
|
|
2029
|
+
check: (ctx) => { const s = ctx.settingsJson(); if (!s || !s.mcpServers) return null; const content = JSON.stringify(s.mcpServers); return !/deprecated|legacy|old-/i.test(content); },
|
|
2030
|
+
impact: 'medium', rating: 3, category: 'pack-posture',
|
|
2031
|
+
fix: 'Update deprecated MCP pack references to current versions.',
|
|
2032
|
+
template: 'gemini-settings', file: () => '.gemini/settings.json', line: () => 1,
|
|
2033
|
+
},
|
|
2034
|
+
|
|
2035
|
+
// CP-08: O. Repeat-Usage Hygiene (3 checks)
|
|
2036
|
+
geminiSnapshotRetention: {
|
|
2037
|
+
id: 'GM-O01', name: 'At least one prior audit snapshot exists',
|
|
2038
|
+
check: (ctx) => { try { const fs = require('fs'); const p = require('path').join(ctx.dir, '.claude', 'claudex-setup', '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; } },
|
|
2039
|
+
impact: 'medium', rating: 3, category: 'repeat-usage',
|
|
2040
|
+
fix: 'Run `npx nerviq --platform gemini --snapshot` to save your first snapshot.',
|
|
2041
|
+
template: null, file: () => null, line: () => null,
|
|
2042
|
+
},
|
|
2043
|
+
geminiFeedbackLoopHealth: {
|
|
2044
|
+
id: 'GM-O02', name: 'Feedback loop functional when feedback submitted',
|
|
2045
|
+
check: (ctx) => { try { const fs = require('fs'); const p = require('path').join(ctx.dir, '.claude', 'claudex-setup', '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; } },
|
|
2046
|
+
impact: 'medium', rating: 3, category: 'repeat-usage',
|
|
2047
|
+
fix: 'Submit feedback using `npx nerviq --platform gemini feedback`.',
|
|
2048
|
+
template: null, file: () => null, line: () => null,
|
|
2049
|
+
},
|
|
2050
|
+
geminiTrendDataAvailability: {
|
|
2051
|
+
id: 'GM-O03', name: 'Trend data computable (2+ snapshots)',
|
|
2052
|
+
check: (ctx) => { try { const fs = require('fs'); const p = require('path').join(ctx.dir, '.claude', 'claudex-setup', '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; } },
|
|
2053
|
+
impact: 'low', rating: 2, category: 'repeat-usage',
|
|
2054
|
+
fix: 'Run at least 2 audits with --snapshot for trend tracking.',
|
|
2055
|
+
template: null, file: () => null, line: () => null,
|
|
2056
|
+
},
|
|
2057
|
+
|
|
2058
|
+
// CP-08: P. Release & Freshness (3 checks)
|
|
2059
|
+
geminiVersionTruth: {
|
|
2060
|
+
id: 'GM-P01', name: 'Gemini version claims match installed version',
|
|
2061
|
+
check: (ctx) => { const g = ctx.geminiMdContent(); if (!g) return null; const m = g.match(/gemini[- ]?(?:cli)?[- ]?v?(\d+\.\d+)/i); if (!m) return null; return true; },
|
|
2062
|
+
impact: 'high', rating: 4, category: 'release-freshness',
|
|
2063
|
+
fix: 'Verify Gemini version in GEMINI.md matches installed CLI version.',
|
|
2064
|
+
template: 'gemini-md', file: () => 'GEMINI.md', line: () => 1,
|
|
2065
|
+
},
|
|
2066
|
+
geminiSourceFreshness: {
|
|
2067
|
+
id: 'GM-P02', name: 'Config references current Gemini features',
|
|
2068
|
+
check: (ctx) => { const s = ctx.settingsJson(); if (!s) return null; const content = JSON.stringify(s); return !/chat_model|notepads|old_format/i.test(content); },
|
|
2069
|
+
impact: 'medium', rating: 3, category: 'release-freshness',
|
|
2070
|
+
fix: 'Update deprecated config keys to current equivalents.',
|
|
2071
|
+
template: 'gemini-settings', file: () => '.gemini/settings.json', line: () => 1,
|
|
2072
|
+
},
|
|
2073
|
+
geminiPropagationCompleteness: {
|
|
2074
|
+
id: 'GM-P03', name: 'No dangling surface references',
|
|
2075
|
+
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; },
|
|
2076
|
+
impact: 'high', rating: 4, category: 'release-freshness',
|
|
2077
|
+
fix: 'Ensure all surfaces mentioned in GEMINI.md have corresponding definition files.',
|
|
2078
|
+
template: 'gemini-md', file: () => 'GEMINI.md', line: () => 1,
|
|
2079
|
+
},
|
|
2080
|
+
};
|
|
2081
|
+
|
|
2082
|
+
module.exports = {
|
|
2083
|
+
GEMINI_TECHNIQUES,
|
|
2084
|
+
};
|