@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.
Files changed (148) hide show
  1. package/CHANGELOG.md +181 -0
  2. package/LICENSE +21 -0
  3. package/README.md +447 -0
  4. package/bin/cli.js +749 -0
  5. package/content/case-study-template.md +91 -0
  6. package/content/claims-governance.md +37 -0
  7. package/content/claude-code/audit-repo/SKILL.md +20 -0
  8. package/content/claude-native-integration.md +60 -0
  9. package/content/devto-article.json +9 -0
  10. package/content/launch-posts.md +226 -0
  11. package/content/pilot-rollout-kit.md +30 -0
  12. package/content/release-checklist.md +31 -0
  13. package/package.json +53 -4
  14. package/src/activity.js +529 -0
  15. package/src/aider/activity.js +226 -0
  16. package/src/aider/config-parser.js +166 -0
  17. package/src/aider/context.js +158 -0
  18. package/src/aider/deep-review.js +316 -0
  19. package/src/aider/domain-packs.js +278 -0
  20. package/src/aider/freshness.js +168 -0
  21. package/src/aider/governance.js +253 -0
  22. package/src/aider/interactive.js +334 -0
  23. package/src/aider/mcp-packs.js +98 -0
  24. package/src/aider/patch.js +214 -0
  25. package/src/aider/plans.js +186 -0
  26. package/src/aider/premium.js +360 -0
  27. package/src/aider/setup.js +404 -0
  28. package/src/aider/techniques.js +1323 -0
  29. package/src/analyze.js +821 -0
  30. package/src/audit.js +1003 -0
  31. package/src/badge.js +13 -0
  32. package/src/benchmark.js +339 -0
  33. package/src/claudex-sync.json +7 -0
  34. package/src/codex/activity.js +324 -0
  35. package/src/codex/config-parser.js +183 -0
  36. package/src/codex/context.js +221 -0
  37. package/src/codex/deep-review.js +493 -0
  38. package/src/codex/domain-packs.js +372 -0
  39. package/src/codex/freshness.js +167 -0
  40. package/src/codex/governance.js +192 -0
  41. package/src/codex/interactive.js +618 -0
  42. package/src/codex/mcp-packs.js +660 -0
  43. package/src/codex/patch.js +209 -0
  44. package/src/codex/plans.js +251 -0
  45. package/src/codex/premium.js +614 -0
  46. package/src/codex/setup.js +603 -0
  47. package/src/codex/techniques.js +2649 -0
  48. package/src/context.js +272 -0
  49. package/src/copilot/activity.js +309 -0
  50. package/src/copilot/config-parser.js +226 -0
  51. package/src/copilot/context.js +197 -0
  52. package/src/copilot/deep-review.js +346 -0
  53. package/src/copilot/domain-packs.js +350 -0
  54. package/src/copilot/freshness.js +197 -0
  55. package/src/copilot/governance.js +222 -0
  56. package/src/copilot/interactive.js +406 -0
  57. package/src/copilot/mcp-packs.js +572 -0
  58. package/src/copilot/patch.js +238 -0
  59. package/src/copilot/plans.js +253 -0
  60. package/src/copilot/premium.js +450 -0
  61. package/src/copilot/setup.js +488 -0
  62. package/src/copilot/techniques.js +1822 -0
  63. package/src/cursor/activity.js +301 -0
  64. package/src/cursor/config-parser.js +265 -0
  65. package/src/cursor/context.js +236 -0
  66. package/src/cursor/deep-review.js +334 -0
  67. package/src/cursor/domain-packs.js +346 -0
  68. package/src/cursor/freshness.js +214 -0
  69. package/src/cursor/governance.js +229 -0
  70. package/src/cursor/interactive.js +391 -0
  71. package/src/cursor/mcp-packs.js +571 -0
  72. package/src/cursor/patch.js +243 -0
  73. package/src/cursor/plans.js +254 -0
  74. package/src/cursor/premium.js +468 -0
  75. package/src/cursor/setup.js +488 -0
  76. package/src/cursor/techniques.js +1786 -0
  77. package/src/deep-review.js +345 -0
  78. package/src/domain-packs.js +364 -0
  79. package/src/formatters/sarif.js +115 -0
  80. package/src/gemini/activity.js +402 -0
  81. package/src/gemini/config-parser.js +275 -0
  82. package/src/gemini/context.js +221 -0
  83. package/src/gemini/deep-review.js +559 -0
  84. package/src/gemini/domain-packs.js +371 -0
  85. package/src/gemini/freshness.js +204 -0
  86. package/src/gemini/governance.js +201 -0
  87. package/src/gemini/interactive.js +860 -0
  88. package/src/gemini/mcp-packs.js +658 -0
  89. package/src/gemini/patch.js +229 -0
  90. package/src/gemini/plans.js +269 -0
  91. package/src/gemini/premium.js +759 -0
  92. package/src/gemini/setup.js +692 -0
  93. package/src/gemini/techniques.js +2084 -0
  94. package/src/governance.js +523 -0
  95. package/src/harmony/advisor.js +383 -0
  96. package/src/harmony/audit.js +303 -0
  97. package/src/harmony/canon.js +444 -0
  98. package/src/harmony/cli.js +331 -0
  99. package/src/harmony/drift.js +401 -0
  100. package/src/harmony/governance.js +313 -0
  101. package/src/harmony/memory.js +238 -0
  102. package/src/harmony/sync.js +458 -0
  103. package/src/harmony/watch.js +336 -0
  104. package/src/index.js +256 -0
  105. package/src/insights.js +119 -0
  106. package/src/interactive.js +118 -0
  107. package/src/mcp-packs.js +597 -0
  108. package/src/opencode/activity.js +286 -0
  109. package/src/opencode/config-parser.js +109 -0
  110. package/src/opencode/context.js +247 -0
  111. package/src/opencode/deep-review.js +313 -0
  112. package/src/opencode/domain-packs.js +240 -0
  113. package/src/opencode/freshness.js +158 -0
  114. package/src/opencode/governance.js +159 -0
  115. package/src/opencode/interactive.js +392 -0
  116. package/src/opencode/mcp-packs.js +474 -0
  117. package/src/opencode/patch.js +184 -0
  118. package/src/opencode/plans.js +231 -0
  119. package/src/opencode/premium.js +413 -0
  120. package/src/opencode/setup.js +449 -0
  121. package/src/opencode/techniques.js +1713 -0
  122. package/src/plans.js +655 -0
  123. package/src/secret-patterns.js +30 -0
  124. package/src/setup.js +1274 -0
  125. package/src/synergy/adaptive.js +261 -0
  126. package/src/synergy/compensation.js +156 -0
  127. package/src/synergy/evidence.js +193 -0
  128. package/src/synergy/learning.js +184 -0
  129. package/src/synergy/patterns.js +227 -0
  130. package/src/synergy/ranking.js +83 -0
  131. package/src/synergy/report.js +163 -0
  132. package/src/synergy/routing.js +152 -0
  133. package/src/techniques.js +1354 -0
  134. package/src/watch.js +229 -0
  135. package/src/windsurf/activity.js +302 -0
  136. package/src/windsurf/config-parser.js +267 -0
  137. package/src/windsurf/context.js +249 -0
  138. package/src/windsurf/deep-review.js +337 -0
  139. package/src/windsurf/domain-packs.js +348 -0
  140. package/src/windsurf/freshness.js +215 -0
  141. package/src/windsurf/governance.js +231 -0
  142. package/src/windsurf/interactive.js +388 -0
  143. package/src/windsurf/mcp-packs.js +535 -0
  144. package/src/windsurf/patch.js +231 -0
  145. package/src/windsurf/plans.js +247 -0
  146. package/src/windsurf/premium.js +467 -0
  147. package/src/windsurf/setup.js +471 -0
  148. package/src/windsurf/techniques.js +1758 -0
@@ -0,0 +1,759 @@
1
+ /**
2
+ * Gemini CLI Premium Operator UX
3
+ *
4
+ * Three subsystems:
5
+ * 1. Multi-Pack Composition Engine — merge domain + MCP + policy + extension packs with dedup, conflict resolution, dependency ordering
6
+ * 2. CI Template Library — 5 GitHub Actions workflow templates for Gemini CLI automation (using run-gemini-cli action)
7
+ * 3. Adoption Signal Gate — activate features based on local usage telemetry (5 gates)
8
+ */
9
+
10
+ const path = require('path');
11
+ const { GEMINI_DOMAIN_PACKS } = require('./domain-packs');
12
+ const { GEMINI_MCP_PACKS } = require('./mcp-packs');
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // 1. Multi-Pack Composition Engine
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /** Pack dependency order — earlier = more foundational */
19
+ const PACK_DEPENDENCY_ORDER = [
20
+ 'baseline-general',
21
+ 'backend-api',
22
+ 'frontend-ui',
23
+ 'infra-platform',
24
+ 'monorepo',
25
+ 'enterprise-governed',
26
+ ];
27
+
28
+ /** Specificity rank: higher = more specific, wins conflicts */
29
+ const PACK_SPECIFICITY = {
30
+ 'baseline-general': 0,
31
+ 'backend-api': 2,
32
+ 'frontend-ui': 2,
33
+ 'infra-platform': 3,
34
+ 'monorepo': 3,
35
+ 'enterprise-governed': 4,
36
+ };
37
+
38
+ /** Policy pack definitions — Gemini-unique */
39
+ const POLICY_PACKS = [
40
+ {
41
+ key: 'policy-file-restrictions',
42
+ label: 'File Restrictions',
43
+ description: 'Protect sensitive files from unreviewed edits.',
44
+ policyContent: {
45
+ 'file-restrictions': {
46
+ deny_edit: ['.env', '.env.*', '*.pem', '*.key', 'credentials.*'],
47
+ deny_delete: ['.env', '.env.*', '*.pem', '*.key', '*.lock'],
48
+ read_only_dirs: ['.git', 'node_modules', '.gemini/policy'],
49
+ },
50
+ },
51
+ },
52
+ {
53
+ key: 'policy-tool-restrictions',
54
+ label: 'Tool Restrictions',
55
+ description: 'Limit which tools the agent can invoke without approval.',
56
+ policyContent: {
57
+ 'tool-restrictions': {
58
+ deny_tools: ['shell_exec_unsafe', 'network_raw'],
59
+ require_approval: ['file_delete', 'git_push', 'deploy'],
60
+ },
61
+ },
62
+ },
63
+ {
64
+ key: 'policy-governance',
65
+ label: 'Governance',
66
+ description: 'Strict audit and escalation policies for enterprise use.',
67
+ policyContent: {
68
+ governance: {
69
+ audit_trail: true,
70
+ escalation_on_deny: true,
71
+ max_auto_edits_per_session: 50,
72
+ require_justification_for: ['security-override', 'policy-bypass'],
73
+ },
74
+ },
75
+ },
76
+ ];
77
+
78
+ /** Extension pack definitions — Gemini-unique */
79
+ const EXTENSION_PACKS = [
80
+ {
81
+ key: 'ext-code-review',
82
+ label: 'Code Review Extension',
83
+ description: 'Structured code review with severity ratings and inline suggestions.',
84
+ },
85
+ {
86
+ key: 'ext-test-gen',
87
+ label: 'Test Generation Extension',
88
+ description: 'Auto-generate test stubs for new or modified files.',
89
+ },
90
+ {
91
+ key: 'ext-docs-sync',
92
+ label: 'Docs Sync Extension',
93
+ description: 'Detect stale documentation and suggest updates.',
94
+ },
95
+ ];
96
+
97
+ const DEFAULT_SIZE_BUDGET = 16000; // characters for combined instruction content
98
+
99
+ function lookupDomainPack(key) {
100
+ return GEMINI_DOMAIN_PACKS.find(p => p.key === key) || null;
101
+ }
102
+
103
+ function lookupMcpPack(key) {
104
+ return GEMINI_MCP_PACKS.find(p => p.key === key) || null;
105
+ }
106
+
107
+ function lookupPolicyPack(key) {
108
+ return POLICY_PACKS.find(p => p.key === key) || null;
109
+ }
110
+
111
+ function lookupExtensionPack(key) {
112
+ return EXTENSION_PACKS.find(p => p.key === key) || null;
113
+ }
114
+
115
+ /**
116
+ * Compose domain packs, MCP packs, policy packs, and extension packs into a unified,
117
+ * deduplicated, ordered result.
118
+ *
119
+ * @param {string[]} domainPackKeys - Array of domain pack keys to compose
120
+ * @param {string[]} mcpPackKeys - Array of MCP pack keys to compose
121
+ * @param {object} [options]
122
+ * @param {number} [options.sizeBudget] - Max characters for combined instructions (default 16000)
123
+ * @param {string[]} [options.policyPackKeys] - Array of policy pack keys (Gemini-unique)
124
+ * @param {string[]} [options.extensionPackKeys] - Array of extension pack keys (Gemini-unique)
125
+ * @returns {object} Composition report
126
+ */
127
+ function composePacks(domainPackKeys = [], mcpPackKeys = [], options = {}) {
128
+ const sizeBudget = options.sizeBudget || DEFAULT_SIZE_BUDGET;
129
+ const policyPackKeys = options.policyPackKeys || [];
130
+ const extensionPackKeys = options.extensionPackKeys || [];
131
+ const warnings = [];
132
+
133
+ // --- Resolve domain packs ---
134
+ const seenDomainKeys = new Set();
135
+ const rawDomainPacks = [];
136
+ for (const key of domainPackKeys) {
137
+ if (seenDomainKeys.has(key)) continue;
138
+ seenDomainKeys.add(key);
139
+ const pack = lookupDomainPack(key);
140
+ if (!pack) {
141
+ warnings.push(`Domain pack "${key}" not found, skipped.`);
142
+ continue;
143
+ }
144
+ rawDomainPacks.push(pack);
145
+ }
146
+
147
+ // Order by dependency (base -> framework -> tool-specific)
148
+ rawDomainPacks.sort((a, b) => {
149
+ const orderA = PACK_DEPENDENCY_ORDER.indexOf(a.key);
150
+ const orderB = PACK_DEPENDENCY_ORDER.indexOf(b.key);
151
+ return (orderA === -1 ? 99 : orderA) - (orderB === -1 ? 99 : orderB);
152
+ });
153
+
154
+ // Deduplicate overlapping recommendedModules; more specific pack wins
155
+ const moduleOwner = new Map(); // module -> { key, specificity }
156
+ for (const pack of rawDomainPacks) {
157
+ const specificity = PACK_SPECIFICITY[pack.key] ?? 1;
158
+ for (const mod of pack.recommendedModules || []) {
159
+ const existing = moduleOwner.get(mod);
160
+ if (!existing || specificity > existing.specificity) {
161
+ moduleOwner.set(mod, { key: pack.key, specificity });
162
+ }
163
+ }
164
+ }
165
+
166
+ // Deduplicate surfaces
167
+ const allSurfaces = new Set();
168
+ for (const pack of rawDomainPacks) {
169
+ for (const surface of pack.recommendedSurfaces || []) {
170
+ allSurfaces.add(surface);
171
+ }
172
+ }
173
+
174
+ // Deduplicate proposal families
175
+ const allProposalFamilies = new Set();
176
+ for (const pack of rawDomainPacks) {
177
+ for (const family of pack.recommendedProposalFamilies || []) {
178
+ allProposalFamilies.add(family);
179
+ }
180
+ }
181
+
182
+ // --- Resolve MCP packs ---
183
+ const seenMcpKeys = new Set();
184
+ const resolvedMcpPacks = [];
185
+ for (const key of mcpPackKeys) {
186
+ if (seenMcpKeys.has(key)) continue;
187
+ seenMcpKeys.add(key);
188
+ const pack = lookupMcpPack(key);
189
+ if (!pack) {
190
+ warnings.push(`MCP pack "${key}" not found, skipped.`);
191
+ continue;
192
+ }
193
+ resolvedMcpPacks.push(pack);
194
+ }
195
+
196
+ // Merge excludeTools across all MCP packs (union — Gemini uses deny-list, not allow-list)
197
+ const mergedExcludeTools = new Set();
198
+ for (const pack of resolvedMcpPacks) {
199
+ for (const tool of pack.excludeTools || []) {
200
+ mergedExcludeTools.add(tool);
201
+ }
202
+ }
203
+
204
+ // Collect required auth (union)
205
+ const mergedRequiredAuth = new Set();
206
+ for (const pack of resolvedMcpPacks) {
207
+ for (const auth of pack.requiredAuth || []) {
208
+ mergedRequiredAuth.add(auth);
209
+ }
210
+ }
211
+
212
+ // --- Resolve policy packs (Gemini-unique) ---
213
+ const seenPolicyKeys = new Set();
214
+ const resolvedPolicyPacks = [];
215
+ const mergedPolicyContent = {};
216
+ for (const key of policyPackKeys) {
217
+ if (seenPolicyKeys.has(key)) continue;
218
+ seenPolicyKeys.add(key);
219
+ const pack = lookupPolicyPack(key);
220
+ if (!pack) {
221
+ warnings.push(`Policy pack "${key}" not found, skipped.`);
222
+ continue;
223
+ }
224
+ resolvedPolicyPacks.push(pack);
225
+ // Merge policy content sections
226
+ if (pack.policyContent) {
227
+ for (const [section, rules] of Object.entries(pack.policyContent)) {
228
+ if (!mergedPolicyContent[section]) {
229
+ mergedPolicyContent[section] = { ...rules };
230
+ } else {
231
+ // Merge arrays, overwrite scalars
232
+ for (const [ruleKey, ruleValue] of Object.entries(rules)) {
233
+ if (Array.isArray(ruleValue) && Array.isArray(mergedPolicyContent[section][ruleKey])) {
234
+ const merged = new Set([...mergedPolicyContent[section][ruleKey], ...ruleValue]);
235
+ mergedPolicyContent[section][ruleKey] = [...merged];
236
+ } else {
237
+ mergedPolicyContent[section][ruleKey] = ruleValue;
238
+ }
239
+ }
240
+ }
241
+ }
242
+ }
243
+ }
244
+
245
+ // --- Resolve extension packs (Gemini-unique) ---
246
+ const seenExtKeys = new Set();
247
+ const resolvedExtensionPacks = [];
248
+ for (const key of extensionPackKeys) {
249
+ if (seenExtKeys.has(key)) continue;
250
+ seenExtKeys.add(key);
251
+ const pack = lookupExtensionPack(key);
252
+ if (!pack) {
253
+ warnings.push(`Extension pack "${key}" not found, skipped.`);
254
+ continue;
255
+ }
256
+ resolvedExtensionPacks.push(pack);
257
+ }
258
+
259
+ // --- Size budget tracking ---
260
+ const estimatedSize = estimateCompositionSize(rawDomainPacks, resolvedMcpPacks, resolvedPolicyPacks, resolvedExtensionPacks);
261
+ const overBudget = estimatedSize > sizeBudget;
262
+ if (overBudget) {
263
+ warnings.push(
264
+ `Combined instruction size (~${estimatedSize} chars) exceeds budget (${sizeBudget}). ` +
265
+ `Consider removing lower-priority packs.`
266
+ );
267
+ }
268
+
269
+ return {
270
+ domainPacks: rawDomainPacks.map(p => ({
271
+ key: p.key,
272
+ label: p.label,
273
+ modulesOwned: [...moduleOwner.entries()]
274
+ .filter(([, owner]) => owner.key === p.key)
275
+ .map(([mod]) => mod),
276
+ })),
277
+ mcpPacks: resolvedMcpPacks.map(p => ({
278
+ key: p.key,
279
+ label: p.label,
280
+ serverName: p.serverName,
281
+ trustLevel: p.trustLevel,
282
+ })),
283
+ policyPacks: resolvedPolicyPacks.map(p => ({
284
+ key: p.key,
285
+ label: p.label,
286
+ description: p.description,
287
+ })),
288
+ extensionPacks: resolvedExtensionPacks.map(p => ({
289
+ key: p.key,
290
+ label: p.label,
291
+ description: p.description,
292
+ })),
293
+ merged: {
294
+ surfaces: [...allSurfaces],
295
+ proposalFamilies: [...allProposalFamilies],
296
+ modules: [...moduleOwner.entries()].map(([mod, owner]) => ({ module: mod, owner: owner.key })),
297
+ excludeTools: [...mergedExcludeTools].sort(),
298
+ requiredAuth: [...mergedRequiredAuth].sort(),
299
+ policyContent: mergedPolicyContent,
300
+ },
301
+ budget: {
302
+ estimatedSize,
303
+ limit: sizeBudget,
304
+ overBudget,
305
+ utilization: Math.round((estimatedSize / sizeBudget) * 100),
306
+ },
307
+ warnings,
308
+ };
309
+ }
310
+
311
+ function estimateCompositionSize(domainPacks, mcpPacks, policyPacks = [], extensionPacks = []) {
312
+ let size = 0;
313
+ for (const pack of domainPacks) {
314
+ size += (pack.label || '').length + (pack.useWhen || '').length + (pack.adoption || '').length;
315
+ size += JSON.stringify(pack.recommendedModules || []).length;
316
+ size += JSON.stringify(pack.benchmarkFocus || []).length;
317
+ }
318
+ for (const pack of mcpPacks) {
319
+ size += (pack.label || '').length + (pack.description || '').length;
320
+ size += JSON.stringify(pack.jsonProjection || {}).length;
321
+ size += JSON.stringify(pack.excludeTools || []).length;
322
+ }
323
+ for (const pack of policyPacks) {
324
+ size += (pack.label || '').length + (pack.description || '').length;
325
+ size += JSON.stringify(pack.policyContent || {}).length;
326
+ }
327
+ for (const pack of extensionPacks) {
328
+ size += (pack.label || '').length + (pack.description || '').length;
329
+ }
330
+ return size;
331
+ }
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // 2. CI Template Library (5 templates using run-gemini-cli action)
335
+ // ---------------------------------------------------------------------------
336
+
337
+ const CI_TEMPLATES = [
338
+ {
339
+ key: 'gemini-pr-review',
340
+ label: 'Gemini PR Review',
341
+ filename: 'gemini-pr-review.yml',
342
+ description: 'Runs Gemini CLI review on pull request diffs.',
343
+ trigger: 'pull_request',
344
+ },
345
+ {
346
+ key: 'gemini-issue-triage',
347
+ label: 'Gemini Issue Triage',
348
+ filename: 'gemini-issue-triage.yml',
349
+ description: 'Classifies and labels new issues using Gemini CLI.',
350
+ trigger: 'issues.opened',
351
+ },
352
+ {
353
+ key: 'gemini-scheduled-audit',
354
+ label: 'Gemini Scheduled Audit',
355
+ filename: 'gemini-scheduled-audit.yml',
356
+ description: 'Weekly deep review of the codebase with Gemini CLI.',
357
+ trigger: 'schedule (cron)',
358
+ },
359
+ {
360
+ key: 'gemini-test-gen',
361
+ label: 'Gemini Test Generation',
362
+ filename: 'gemini-test-gen.yml',
363
+ description: 'Generates test stubs for newly added files using Gemini CLI.',
364
+ trigger: 'pull_request',
365
+ },
366
+ {
367
+ key: 'gemini-docs-sync',
368
+ label: 'Gemini Docs Sync',
369
+ filename: 'gemini-docs-sync.yml',
370
+ description: 'Checks for documentation staleness using Gemini CLI.',
371
+ trigger: 'schedule (cron)',
372
+ },
373
+ ];
374
+
375
+ const TEMPLATE_CONTENT = {
376
+ 'gemini-pr-review': `# Gemini PR Review — generated by nerviq
377
+ # Runs Gemini CLI to review pull request diffs and post suggestions.
378
+ #
379
+ # CUSTOMIZE: Adjust the model, sandbox, and timeout to match your team.
380
+
381
+ name: Gemini PR Review
382
+
383
+ on:
384
+ pull_request:
385
+ types: [opened, synchronize]
386
+
387
+ # CUSTOMIZE: Add path filters if you only want reviews on certain directories
388
+ # paths: ['src/**', 'lib/**']
389
+
390
+ permissions:
391
+ contents: read
392
+ pull-requests: write
393
+
394
+ jobs:
395
+ gemini-review:
396
+ runs-on: ubuntu-latest
397
+ timeout-minutes: 15 # CUSTOMIZE: Adjust timeout for your repo size
398
+ env:
399
+ GEMINI_API_KEY: \${{ secrets.GEMINI_API_KEY }}
400
+ # CUSTOMIZE: Set DRY_RUN=true to preview without posting comments
401
+ DRY_RUN: \${{ inputs.dry_run || 'false' }}
402
+ steps:
403
+ - uses: actions/checkout@v4
404
+ with:
405
+ fetch-depth: 0
406
+
407
+ - name: Run Gemini CLI Review
408
+ uses: google/run-gemini-cli@v1
409
+ with:
410
+ prompt: |
411
+ Review the following pull request diff for issues, improvements, and best practices.
412
+ Focus on security, performance, and correctness.
413
+ sandbox: docker
414
+ env:
415
+ GEMINI_API_KEY: \${{ secrets.GEMINI_API_KEY }}
416
+ `,
417
+
418
+ 'gemini-issue-triage': `# Gemini Issue Triage — generated by nerviq
419
+ # Classifies newly opened issues and applies labels using Gemini CLI.
420
+ #
421
+ # CUSTOMIZE: Edit the label mapping and classification prompt below.
422
+
423
+ name: Gemini Issue Triage
424
+
425
+ on:
426
+ issues:
427
+ types: [opened]
428
+
429
+ permissions:
430
+ issues: write
431
+
432
+ jobs:
433
+ triage:
434
+ runs-on: ubuntu-latest
435
+ timeout-minutes: 5 # CUSTOMIZE: Issues are small; 5 min is usually enough
436
+ env:
437
+ GEMINI_API_KEY: \${{ secrets.GEMINI_API_KEY }}
438
+ DRY_RUN: \${{ inputs.dry_run || 'false' }}
439
+ steps:
440
+ - uses: actions/checkout@v4
441
+
442
+ - name: Classify Issue with Gemini
443
+ uses: google/run-gemini-cli@v1
444
+ with:
445
+ prompt: |
446
+ Classify this issue and suggest labels from: bug, feature, question, docs, security.
447
+ Title: \${{ github.event.issue.title }}
448
+ Body: \${{ github.event.issue.body }}
449
+ sandbox: docker
450
+ env:
451
+ GEMINI_API_KEY: \${{ secrets.GEMINI_API_KEY }}
452
+ `,
453
+
454
+ 'gemini-scheduled-audit': `# Gemini Scheduled Audit — generated by nerviq
455
+ # Weekly deep review of the codebase using Gemini CLI.
456
+ #
457
+ # CUSTOMIZE: Adjust the cron schedule and audit scope.
458
+
459
+ name: Gemini Scheduled Audit
460
+
461
+ on:
462
+ schedule:
463
+ - cron: '0 9 * * 1' # CUSTOMIZE: Every Monday at 09:00 UTC
464
+ workflow_dispatch:
465
+ inputs:
466
+ dry_run:
467
+ description: 'Run in dry-run mode (no changes)'
468
+ required: false
469
+ default: 'false'
470
+
471
+ permissions:
472
+ contents: read
473
+ issues: write
474
+
475
+ jobs:
476
+ audit:
477
+ runs-on: ubuntu-latest
478
+ timeout-minutes: 30 # CUSTOMIZE: Deep audits may need more time
479
+ env:
480
+ GEMINI_API_KEY: \${{ secrets.GEMINI_API_KEY }}
481
+ DRY_RUN: \${{ inputs.dry_run || 'false' }}
482
+ steps:
483
+ - uses: actions/checkout@v4
484
+ with:
485
+ fetch-depth: 0
486
+
487
+ - name: Run Deep Audit with Gemini
488
+ uses: google/run-gemini-cli@v1
489
+ with:
490
+ prompt: |
491
+ Perform a deep audit of this codebase. Check for:
492
+ - Security vulnerabilities
493
+ - Performance issues
494
+ - Code quality concerns
495
+ - Dependency health
496
+ Output a structured markdown report.
497
+ sandbox: docker
498
+ env:
499
+ GEMINI_API_KEY: \${{ secrets.GEMINI_API_KEY }}
500
+
501
+ - name: Upload Report
502
+ if: env.DRY_RUN != 'true'
503
+ uses: actions/upload-artifact@v4
504
+ with:
505
+ name: gemini-audit-report
506
+ path: audit-report.md
507
+ retention-days: 30
508
+ `,
509
+
510
+ 'gemini-test-gen': `# Gemini Test Generation — generated by nerviq
511
+ # Generates test stubs for new files in a pull request using Gemini CLI.
512
+ #
513
+ # CUSTOMIZE: Adjust file patterns, test framework, and output directory.
514
+
515
+ name: Gemini Test Generation
516
+
517
+ on:
518
+ pull_request:
519
+ types: [opened, synchronize]
520
+
521
+ permissions:
522
+ contents: read
523
+ pull-requests: write
524
+
525
+ jobs:
526
+ test-gen:
527
+ runs-on: ubuntu-latest
528
+ timeout-minutes: 10 # CUSTOMIZE: Adjust for repo size
529
+ env:
530
+ GEMINI_API_KEY: \${{ secrets.GEMINI_API_KEY }}
531
+ DRY_RUN: \${{ inputs.dry_run || 'false' }}
532
+ steps:
533
+ - uses: actions/checkout@v4
534
+ with:
535
+ fetch-depth: 0
536
+
537
+ - name: Detect New Files
538
+ id: new-files
539
+ run: |
540
+ FILES=$(git diff --name-only --diff-filter=A \${{ github.event.pull_request.base.sha }}..\${{ github.sha }})
541
+ # CUSTOMIZE: Filter to source files only
542
+ SRC_FILES=$(echo "$FILES" | grep -E '\\.(js|ts|py|go|rs|java)$' || true)
543
+ echo "files=$SRC_FILES" >> $GITHUB_OUTPUT
544
+
545
+ - name: Generate Test Stubs with Gemini
546
+ if: steps.new-files.outputs.files != ''
547
+ uses: google/run-gemini-cli@v1
548
+ with:
549
+ prompt: |
550
+ Generate test stubs for the following new files:
551
+ \${{ steps.new-files.outputs.files }}
552
+ Use the project's existing test framework and conventions.
553
+ sandbox: docker
554
+ env:
555
+ GEMINI_API_KEY: \${{ secrets.GEMINI_API_KEY }}
556
+ `,
557
+
558
+ 'gemini-docs-sync': `# Gemini Docs Sync — generated by nerviq
559
+ # Checks for documentation staleness and suggests updates using Gemini CLI.
560
+ #
561
+ # CUSTOMIZE: Adjust the staleness threshold and doc paths.
562
+
563
+ name: Gemini Docs Sync
564
+
565
+ on:
566
+ schedule:
567
+ - cron: '0 10 * * 3' # CUSTOMIZE: Every Wednesday at 10:00 UTC
568
+ workflow_dispatch:
569
+ inputs:
570
+ dry_run:
571
+ description: 'Run in dry-run mode (no changes)'
572
+ required: false
573
+ default: 'false'
574
+
575
+ permissions:
576
+ contents: read
577
+ issues: write
578
+
579
+ jobs:
580
+ docs-check:
581
+ runs-on: ubuntu-latest
582
+ timeout-minutes: 10 # CUSTOMIZE: Usually fast
583
+ env:
584
+ GEMINI_API_KEY: \${{ secrets.GEMINI_API_KEY }}
585
+ DRY_RUN: \${{ inputs.dry_run || 'false' }}
586
+ steps:
587
+ - uses: actions/checkout@v4
588
+ with:
589
+ fetch-depth: 0
590
+
591
+ - name: Check Doc Staleness
592
+ run: |
593
+ # CUSTOMIZE: Adjust paths and staleness threshold (days)
594
+ STALE_THRESHOLD=30
595
+ DOC_PATHS="README.md docs/ CONTRIBUTING.md"
596
+
597
+ for doc_path in $DOC_PATHS; do
598
+ [ ! -e "$doc_path" ] && continue
599
+ LAST_MODIFIED=$(git log -1 --format="%ct" -- "$doc_path" 2>/dev/null || echo 0)
600
+ NOW=$(date +%s)
601
+ DAYS_OLD=$(( (NOW - LAST_MODIFIED) / 86400 ))
602
+ if [ "$DAYS_OLD" -gt "$STALE_THRESHOLD" ]; then
603
+ echo "STALE: $doc_path ($DAYS_OLD days old)"
604
+ fi
605
+ done
606
+
607
+ - name: Suggest Doc Updates with Gemini
608
+ uses: google/run-gemini-cli@v1
609
+ with:
610
+ prompt: |
611
+ Review the project documentation for staleness and accuracy.
612
+ Compare docs against the current codebase and suggest updates.
613
+ Focus on README.md, CONTRIBUTING.md, and files in docs/.
614
+ sandbox: docker
615
+ env:
616
+ GEMINI_API_KEY: \${{ secrets.GEMINI_API_KEY }}
617
+ `,
618
+ };
619
+
620
+ /**
621
+ * Get a CI template by key.
622
+ *
623
+ * @param {string} templateKey - One of the CI_TEMPLATES keys
624
+ * @returns {string|null} Template content string or null if not found
625
+ */
626
+ function getCiTemplate(templateKey) {
627
+ const meta = CI_TEMPLATES.find(t => t.key === templateKey);
628
+ if (!meta) return null;
629
+ return TEMPLATE_CONTENT[templateKey] || null;
630
+ }
631
+
632
+ // ---------------------------------------------------------------------------
633
+ // 3. Adoption Signal Gate (5 gates)
634
+ // ---------------------------------------------------------------------------
635
+
636
+ /**
637
+ * Gate thresholds: each gate defines a minimum usage signal before activation.
638
+ * Uses local snapshot history for privacy-preserving telemetry.
639
+ */
640
+ const GATE_THRESHOLDS = {
641
+ 'ci-templates': {
642
+ metric: 'auditCount',
643
+ threshold: 3,
644
+ description: 'Activate CI templates after 3+ successful audits.',
645
+ },
646
+ 'policy-engine': {
647
+ metric: 'auditCount',
648
+ threshold: 2,
649
+ description: 'Activate policy engine packs after 2+ audits.',
650
+ },
651
+ 'multi-pack': {
652
+ metric: 'auditCount',
653
+ threshold: 2,
654
+ description: 'Activate multi-pack composition after 2+ audits.',
655
+ },
656
+ 'extension-marketplace': {
657
+ metric: 'auditCount',
658
+ threshold: 5,
659
+ description: 'Activate extension marketplace recommendations after 5+ audits.',
660
+ },
661
+ 'governance-upgrade': {
662
+ metric: 'averageScore',
663
+ threshold: 70,
664
+ description: 'Suggest governance upgrade when average score exceeds 70.',
665
+ },
666
+ };
667
+
668
+ /**
669
+ * Read Gemini audit snapshot history from the local .gemini/.claudex/ directory.
670
+ * @param {string} dir - Project directory
671
+ * @param {number} limit - Max snapshots to read
672
+ * @returns {object[]} Array of snapshot objects
673
+ */
674
+ function getGeminiHistory(dir, limit = 20) {
675
+ const fs = require('fs');
676
+ const snapshotDir = path.join(dir, '.gemini', '.claudex', 'snapshots');
677
+ try {
678
+ const files = fs.readdirSync(snapshotDir)
679
+ .filter(f => f.endsWith('.json'))
680
+ .sort()
681
+ .slice(-limit);
682
+
683
+ return files.map(f => {
684
+ try {
685
+ return JSON.parse(fs.readFileSync(path.join(snapshotDir, f), 'utf8'));
686
+ } catch {
687
+ return null;
688
+ }
689
+ }).filter(Boolean);
690
+ } catch {
691
+ return [];
692
+ }
693
+ }
694
+
695
+ /**
696
+ * Check if an adoption gate should be activated based on local telemetry.
697
+ *
698
+ * @param {string} gateKey - Key from GATE_THRESHOLDS
699
+ * @param {string} dir - Project directory to read snapshots from
700
+ * @returns {{ activated: boolean, current: number, threshold: number, gate: string, description: string }}
701
+ */
702
+ function checkAdoptionGate(gateKey, dir) {
703
+ const gate = GATE_THRESHOLDS[gateKey];
704
+ if (!gate) {
705
+ return {
706
+ activated: false,
707
+ current: 0,
708
+ threshold: 0,
709
+ gate: gateKey,
710
+ description: `Unknown gate "${gateKey}".`,
711
+ };
712
+ }
713
+
714
+ const history = getGeminiHistory(dir, 100);
715
+ let current = 0;
716
+
717
+ switch (gate.metric) {
718
+ case 'auditCount':
719
+ current = history.length;
720
+ break;
721
+ case 'averageScore': {
722
+ const scores = history
723
+ .map(e => e.summary?.score)
724
+ .filter(s => typeof s === 'number');
725
+ current = scores.length > 0
726
+ ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length)
727
+ : 0;
728
+ break;
729
+ }
730
+ default:
731
+ current = 0;
732
+ }
733
+
734
+ return {
735
+ activated: current >= gate.threshold,
736
+ current,
737
+ threshold: gate.threshold,
738
+ gate: gateKey,
739
+ description: gate.description,
740
+ };
741
+ }
742
+
743
+ // ---------------------------------------------------------------------------
744
+ // Exports
745
+ // ---------------------------------------------------------------------------
746
+
747
+ module.exports = {
748
+ composePacks,
749
+ getCiTemplate,
750
+ CI_TEMPLATES,
751
+ checkAdoptionGate,
752
+ // Internals exposed for testing
753
+ GATE_THRESHOLDS,
754
+ PACK_DEPENDENCY_ORDER,
755
+ PACK_SPECIFICITY,
756
+ DEFAULT_SIZE_BUDGET,
757
+ POLICY_PACKS,
758
+ EXTENSION_PACKS,
759
+ };