@nerviq/cli 1.16.0 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,28 +42,34 @@ Nerviq audits, sets up, and governs AI coding agent configurations for **8 platf
42
42
 
43
43
  ## What Nerviq Does
44
44
 
45
- Nerviq scores your AI coding agent setup from 0 to 100, finds what's missing, and fixes it with rollback for every change.
45
+ Most repos use **more than one** AI coding agent Claude and Cursor, Copilot and Codex, Gemini and Windsurf. Their configs drift silently. Nerviq is the neutral control plane that detects that drift, scores it, and helps align it.
46
+
47
+ When your repo has 2+ platforms configured, `nerviq audit` leads with the Harmony Score — cross-platform alignment — before any single-platform results. Single-platform repos still get a normal per-platform audit.
46
48
 
47
49
  ```
48
- nerviq audit
49
- ═══════════════════════════════════════
50
- Detected: React, TypeScript, Docker
50
+ $ nerviq audit
51
+ Detected: Next.js + TypeScript + Claude + Cursor
52
+
53
+ Harmony Score: 78/100 — 3 drift issues across 2 platforms (Claude Code + Cursor)
54
+ Run `nerviq harmony-audit` for the full cross-platform report.
51
55
 
52
- ████████████████░░░░ 78/100
56
+ Platform: Claude Code
57
+ CLAUDE.md .............. 23/25 checks passed
58
+ .claude/settings.json .. 6/9 checks passed
53
59
 
54
- CLAUDE.md with architecture diagram
55
- Hooks (PreToolUse + PostToolUse)
56
- Custom skills (3 skills)
57
- ✅ MCP servers configured
60
+ Platform: Cursor
61
+ .cursor/rules/ ......... 14/16 checks passed
62
+ .cursorrules ........... 4/7 checks passed
58
63
 
59
- Top 3 Next Actions
60
- 1. Add verification commands to CLAUDE.md
61
- 2. Configure deny rules for dangerous operations
62
- 3. Add path-specific rules in .claude/rules/
64
+ Drift: trust-mode mismatch (Claude relaxed / Cursor strict)
65
+ MCP coverage gap 3 servers in Claude, 1 in Cursor
66
+ format-on-save hook missing from Cursor
63
67
 
64
- Next: nerviq setup
68
+ 3 suggestions → `nerviq harmony-sync` to align.
65
69
  ```
66
70
 
71
+ Single-platform repos still work the same way — the Harmony Score only appears when 2+ platforms are detected. Use `--no-harmony-first` to suppress it even on multi-platform repos.
72
+
67
73
  ## Quick Start
68
74
 
69
75
  ```bash
@@ -217,8 +223,8 @@ All successful operational responses are wrapped in a JSON envelope:
217
223
  {
218
224
  "data": {},
219
225
  "meta": {
220
- "version": "1.16.0",
221
- "timestamp": "2026-04-11T12:00:00.000Z"
226
+ "version": "1.17.1",
227
+ "timestamp": "2026-04-12T12:00:00.000Z"
222
228
  }
223
229
  }
224
230
  ```
package/package.json CHANGED
@@ -1,60 +1,60 @@
1
- {
2
- "name": "@nerviq/cli",
3
- "version": "1.16.0",
4
- "description": "The intelligent nervous system for AI coding agents — 2,441 checks (8 platforms × ~300 governance rules), 10 languages, 62 domain packs. Audit, align, and amplify.",
5
- "main": "src/index.js",
6
- "bin": {
7
- "nerviq": "bin/cli.js",
8
- "@nerviq/cli": "bin/cli.js",
9
- "nerviq-mcp": "src/mcp-server.js"
10
- },
11
- "files": [
12
- "bin",
13
- "src",
14
- "README.md"
15
- ],
16
- "scripts": {
17
- "start": "node bin/cli.js",
18
- "build": "npm pack --dry-run",
19
- "test": "node test/run.js",
20
- "verify:release-metadata": "node tools/validate-release-metadata.js",
21
- "test:jest": "jest",
22
- "test:coverage": "jest --coverage",
23
- "test:all": "npm test && npx jest && node test/check-matrix.js && node test/codex-check-matrix.js && node test/gemini-check-matrix.js && node test/copilot-check-matrix.js && node test/cursor-check-matrix.js && node test/windsurf-check-matrix.js && node test/aider-check-matrix.js && node test/opencode-check-matrix.js && node test/golden-matrix.js && node test/codex-golden-matrix.js && node test/gemini-golden-matrix.js && node test/copilot-golden-matrix.js && node test/cursor-golden-matrix.js && node test/windsurf-golden-matrix.js && node test/aider-golden-matrix.js && node test/opencode-golden-matrix.js",
24
- "benchmark:perf": "node tools/benchmark.js",
25
- "catalog": "node -e \"const {generateCatalog}=require('./src/catalog');console.log(JSON.stringify(generateCatalog(),null,2))\""
26
- },
27
- "keywords": [
28
- "nerviq",
29
- "ai-agents",
30
- "agent-governance",
31
- "agent-config",
32
- "harmony",
33
- "synergy",
34
- "audit",
35
- "claude",
36
- "codex",
37
- "gemini",
38
- "copilot",
39
- "cursor",
40
- "windsurf",
41
- "aider",
42
- "developer-tools",
43
- "cli",
44
- "mcp",
45
- "multi-agent"
46
- ],
47
- "author": "Nerviq <hello@nerviq.net>",
48
- "license": "AGPL-3.0",
49
- "repository": {
50
- "type": "git",
51
- "url": "git+https://github.com/nerviq/nerviq.git"
52
- },
53
- "homepage": "https://nerviq.net",
54
- "engines": {
55
- "node": ">=18.0.0"
56
- },
57
- "devDependencies": {
58
- "jest": "^30.3.0"
59
- }
60
- }
1
+ {
2
+ "name": "@nerviq/cli",
3
+ "version": "1.17.1",
4
+ "description": "The intelligent nervous system for AI coding agents — 2,441 checks (8 platforms × ~300 governance rules), 10 languages, 62 domain packs. Audit, align, and amplify.",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "nerviq": "bin/cli.js",
8
+ "@nerviq/cli": "bin/cli.js",
9
+ "nerviq-mcp": "src/mcp-server.js"
10
+ },
11
+ "files": [
12
+ "bin",
13
+ "src",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "start": "node bin/cli.js",
18
+ "build": "npm pack --dry-run",
19
+ "test": "node test/run.js",
20
+ "verify:release-metadata": "node tools/validate-release-metadata.js",
21
+ "test:jest": "jest",
22
+ "test:coverage": "jest --coverage",
23
+ "test:all": "npm test && npx jest && node test/check-matrix.js && node test/codex-check-matrix.js && node test/gemini-check-matrix.js && node test/copilot-check-matrix.js && node test/cursor-check-matrix.js && node test/windsurf-check-matrix.js && node test/aider-check-matrix.js && node test/opencode-check-matrix.js && node test/golden-matrix.js && node test/codex-golden-matrix.js && node test/gemini-golden-matrix.js && node test/copilot-golden-matrix.js && node test/cursor-golden-matrix.js && node test/windsurf-golden-matrix.js && node test/aider-golden-matrix.js && node test/opencode-golden-matrix.js",
24
+ "benchmark:perf": "node tools/benchmark.js",
25
+ "catalog": "node -e \"const {generateCatalog}=require('./src/catalog');console.log(JSON.stringify(generateCatalog(),null,2))\""
26
+ },
27
+ "keywords": [
28
+ "nerviq",
29
+ "ai-agents",
30
+ "agent-governance",
31
+ "agent-config",
32
+ "harmony",
33
+ "synergy",
34
+ "audit",
35
+ "claude",
36
+ "codex",
37
+ "gemini",
38
+ "copilot",
39
+ "cursor",
40
+ "windsurf",
41
+ "aider",
42
+ "developer-tools",
43
+ "cli",
44
+ "mcp",
45
+ "multi-agent"
46
+ ],
47
+ "author": "Nerviq <hello@nerviq.net>",
48
+ "license": "AGPL-3.0",
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/nerviq/nerviq.git"
52
+ },
53
+ "homepage": "https://nerviq.net",
54
+ "engines": {
55
+ "node": ">=18.0.0"
56
+ },
57
+ "devDependencies": {
58
+ "jest": "^30.3.0"
59
+ }
60
+ }
package/src/audit.js CHANGED
@@ -514,6 +514,42 @@ async function audit(options) {
514
514
  const recommendedDomainPacks = spec.platform === 'codex'
515
515
  ? detectCodexDomainPacks(ctx, stacks, getCodexDomainPackSignals(ctx))
516
516
  : [];
517
+
518
+ // FB-05: framework-aware fix rewriting — don't recommend `npm test` on a
519
+ // Python/Go/Rust-only repo. Only rewrites when Node/JS stacks are absent.
520
+ const stackKeys = new Set(stacks.map(s => s.key));
521
+ const hasNodeStack = stackKeys.has('node') || stackKeys.has('react') || stackKeys.has('vue') ||
522
+ stackKeys.has('nextjs') || stackKeys.has('angular') || stackKeys.has('svelte') ||
523
+ stackKeys.has('nestjs') || stackKeys.has('remix') || stackKeys.has('astro') ||
524
+ stackKeys.has('typescript') || stackKeys.has('deno') || stackKeys.has('bun');
525
+ if (!hasNodeStack) {
526
+ let preferredTest = null;
527
+ let preferredInstall = null;
528
+ if (stackKeys.has('python') || stackKeys.has('django') || stackKeys.has('fastapi')) {
529
+ preferredTest = 'pytest'; preferredInstall = 'pip install -r requirements.txt';
530
+ } else if (stackKeys.has('go')) {
531
+ preferredTest = 'go test ./...'; preferredInstall = 'go mod download';
532
+ } else if (stackKeys.has('rust')) {
533
+ preferredTest = 'cargo test'; preferredInstall = 'cargo fetch';
534
+ } else if (stackKeys.has('ruby')) {
535
+ preferredTest = 'bundle exec rspec'; preferredInstall = 'bundle install';
536
+ } else if (stackKeys.has('java') || stackKeys.has('kotlin')) {
537
+ preferredTest = './gradlew test'; preferredInstall = './gradlew build';
538
+ } else if (stackKeys.has('elixir')) {
539
+ preferredTest = 'mix test'; preferredInstall = 'mix deps.get';
540
+ } else if (stackKeys.has('dotnet')) {
541
+ preferredTest = 'dotnet test'; preferredInstall = 'dotnet restore';
542
+ }
543
+ if (preferredTest) {
544
+ for (const r of results) {
545
+ if (typeof r.fix !== 'string') continue;
546
+ if (/\bnpm\s+test\b/i.test(r.fix)) r.fix = r.fix.replace(/`npm\s+test`/gi, '`' + preferredTest + '`').replace(/\bnpm\s+test\b/gi, preferredTest);
547
+ if (/\bnpm\s+ci\b/i.test(r.fix) && preferredInstall) r.fix = r.fix.replace(/`npm\s+ci`/gi, '`' + preferredInstall + '`').replace(/\bnpm\s+ci\b/gi, preferredInstall);
548
+ if (/\bnpm\s+install\b/i.test(r.fix) && preferredInstall) r.fix = r.fix.replace(/`npm\s+install`/gi, '`' + preferredInstall + '`').replace(/\bnpm\s+install\b/gi, preferredInstall);
549
+ }
550
+ }
551
+ }
552
+
517
553
  const result = {
518
554
  platform: spec.platform,
519
555
  platformLabel: spec.platformLabel,
@@ -1,9 +1,9 @@
1
1
  const os = require('os');
2
- const path = require('path');
3
- const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
4
- const { attachSourceUrls } = require('../source-urls');
5
- const { buildSupplementalChecks } = require('../supplemental-checks');
6
- const { resolveProjectStateReadPath } = require('../state-paths');
2
+ const path = require('path');
3
+ const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
4
+ const { attachSourceUrls } = require('../source-urls');
5
+ const { buildSupplementalChecks } = require('../supplemental-checks');
6
+ const { resolveProjectStateReadPath } = require('../state-paths');
7
7
 
8
8
  const CODEX_SUPPLEMENTAL_SOURCE_URLS = {
9
9
  'testing-strategy': 'https://developers.openai.com/codex/cli',
@@ -157,7 +157,14 @@ function hasCommandMention(content, category) {
157
157
  }
158
158
 
159
159
  function agentsHasArchitecture(content) {
160
- return /```mermaid|flowchart\b|graph\s+(TD|LR|RL|BT)\b|##\s+Architecture\b|##\s+Project Map\b|##\s+Structure\b/i.test(content);
160
+ // Explicit architecture/structure markers
161
+ if (/```mermaid|flowchart\b|graph\s+(TD|LR|RL|BT)\b/i.test(content)) return true;
162
+ // Heading variants seen in real repos (openai-agents-python, etc.)
163
+ if (/^#{1,4}\s+(Architecture|Project Map|Structure|Project Structure( Guide)?|Repo Structure( & Important Files)?|Repository Layout|Codebase (Guide|Map|Overview|Structure)|Repo Map|Key Directories|Module Map|Directory Layout|Folder Structure|Package Structure)\b/im.test(content)) return true;
164
+ // Enumerated file/directory maps (3+ backtick-wrapped paths in a row)
165
+ const pathList = content.match(/^[-*]?\s*`[^`]+\/`?/gm);
166
+ if (pathList && pathList.length >= 3) return true;
167
+ return false;
161
168
  }
162
169
 
163
170
  function findFillerLine(content) {
@@ -345,7 +352,31 @@ function projectMcpServers(ctx) {
345
352
  return config.data.mcp_servers;
346
353
  }
347
354
 
355
+ function isSdkOrLibraryRepo(ctx) {
356
+ const pkg = ctx.fileContent('package.json');
357
+ if (pkg) {
358
+ try {
359
+ const parsed = JSON.parse(pkg);
360
+ // Library if it declares a main/exports/module entry AND no start script that runs a server
361
+ const hasEntry = parsed.main || parsed.exports || parsed.module;
362
+ const scripts = parsed.scripts || {};
363
+ const hasServerStart = /(node|uvicorn|gunicorn).*(server|app|index\.js)/i.test(JSON.stringify(scripts));
364
+ if (hasEntry && !hasServerStart) return true;
365
+ } catch { /* fall through */ }
366
+ }
367
+ const pyproject = ctx.fileContent('pyproject.toml') || '';
368
+ if (/\[project\][^\[]*\b(packages|py_modules)\s*=/i.test(pyproject) && !ctx.hasDir('app') && !ctx.hasDir('server')) return true;
369
+ // README signals
370
+ const readme = ctx.fileContent('README.md') || '';
371
+ if (/\b(npm install|pip install|pnpm add|yarn add)\b.*\b(this|the)? ?(package|library|sdk)/i.test(readme)) return true;
372
+ if (/^# .*\bSDK\b/im.test(readme)) return true;
373
+ return false;
374
+ }
375
+
348
376
  function repoNeedsExternalTools(ctx) {
377
+ // SDK/library repos document integrations but don't need project-scoped MCP.
378
+ if (isSdkOrLibraryRepo(ctx)) return false;
379
+
349
380
  const deps = ctx.projectDependencies ? Object.keys(ctx.projectDependencies()) : [];
350
381
  const depSet = new Set(deps);
351
382
  const files = new Set(ctx.files || []);
@@ -639,12 +670,27 @@ function skillArtifacts(ctx) {
639
670
  }));
640
671
  }
641
672
 
673
+ function extractFrontmatter(content) {
674
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
675
+ if (!match) return null;
676
+ const fm = {};
677
+ for (const line of match[1].split(/\r?\n/)) {
678
+ const m = line.match(/^([a-zA-Z_-]+)\s*:\s*(.+)$/);
679
+ if (m) fm[m[1].trim()] = m[2].trim().replace(/^["']|["']$/g, '');
680
+ }
681
+ return fm;
682
+ }
683
+
642
684
  function extractSkillTitle(content) {
685
+ const fm = extractFrontmatter(content);
686
+ if (fm && fm.name) return fm.name;
643
687
  const match = content.match(/^#\s+(.+)$/m);
644
688
  return match ? match[1].trim() : '';
645
689
  }
646
690
 
647
691
  function extractSkillDescription(content) {
692
+ const fm = extractFrontmatter(content);
693
+ if (fm && fm.description) return fm.description;
648
694
  const lines = content.split(/\r?\n/);
649
695
  const meaningful = [];
650
696
  let seenHeading = false;
@@ -1116,8 +1162,16 @@ function requirementsTomlIssue(ctx) {
1116
1162
 
1117
1163
  function sharedOrManagedMachineSignals(ctx) {
1118
1164
  const docs = docsBundle(ctx);
1119
- return Boolean(ctx.fileContent('requirements.toml')) ||
1120
- /\bshared\b|\bmanaged\b|\badmin[- ]enforced\b|\bmulti-user\b|\benterprise\b|\bkiosk\b|\bvdi\b/i.test(docs);
1165
+ if (ctx.fileContent('requirements.toml')) return true;
1166
+ // Explicit workstation/admin context only — not generic "shared" or "managed" words.
1167
+ // Must match specific managed-device or multi-user terminology.
1168
+ if (/\bmanaged[- ](device|laptop|workstation|host)\b/i.test(docs)) return true;
1169
+ if (/\bshared[- ](workstation|host|laptop|machine|computer)\b/i.test(docs)) return true;
1170
+ if (/\bmulti-user[- ](host|machine|workstation)\b/i.test(docs)) return true;
1171
+ if (/\b(kiosk|vdi|virtual desktop)\b/i.test(docs)) return true;
1172
+ if (/\benterprise[- ](managed|deployment|workstation)\b/i.test(docs)) return true;
1173
+ if (/\badmin[- ]enforced\b/i.test(docs)) return true;
1174
+ return false;
1121
1175
  }
1122
1176
 
1123
1177
  function authCredentialsStoreIssue(ctx) {
@@ -3049,17 +3103,30 @@ const CODEX_TECHNIQUES = {
3049
3103
  id: 'CX-N03',
3050
3104
  name: 'Pack recommendations are grounded in detected signals',
3051
3105
  check: (ctx) => {
3052
- // This check validates that the project has enough signals for meaningful pack recommendation
3106
+ // Ecosystem-neutral grounding: any primary manifest counts.
3053
3107
  const agents = agentsContent(ctx);
3054
3108
  const config = ctx.configContent ? (ctx.configContent() || '') : (ctx.fileContent('.codex/config.toml') || '');
3055
- const hasPkg = Boolean(ctx.jsonFile('package.json'));
3109
+ const manifestFiles = [
3110
+ 'package.json', 'pyproject.toml', 'Cargo.toml', 'go.mod',
3111
+ 'Gemfile', 'composer.json', 'pom.xml', 'build.gradle',
3112
+ 'flake.nix', 'shard.yml', 'mix.exs', 'rebar.config',
3113
+ 'Makefile', 'CMakeLists.txt', 'Package.swift', 'pubspec.yaml',
3114
+ ];
3115
+ const hasManifest = manifestFiles.some(f => ctx.files.includes(f));
3116
+ // Dotfiles/config-only repos: they don't ship code, so pack recommendations
3117
+ // aren't meaningful — N/A is the correct answer.
3118
+ const dotfilesSignals = ['.zshrc', '.bashrc', '.vimrc', '.tmux.conf', '.gitconfig', 'install.sh', 'bootstrap.sh'];
3119
+ const looksLikeDotfiles = dotfilesSignals.filter(f => ctx.files.includes(f)).length >= 2;
3120
+ if (looksLikeDotfiles) return null;
3121
+ // If no signals at all, N/A rather than fail
3122
+ if (!agents && !config && !hasManifest) return null;
3056
3123
  // At least 2 signal sources for grounded recommendation
3057
- return [Boolean(agents), Boolean(config), hasPkg].filter(Boolean).length >= 2;
3124
+ return [Boolean(agents), Boolean(config), hasManifest].filter(Boolean).length >= 2;
3058
3125
  },
3059
3126
  impact: 'medium',
3060
3127
  rating: 3,
3061
3128
  category: 'pack-posture',
3062
- fix: 'Add package.json and AGENTS.md so pack recommendations can be grounded in real project signals.',
3129
+ fix: 'Add AGENTS.md and ensure a primary manifest (package.json, pyproject.toml, Cargo.toml, go.mod, etc.) is present so pack recommendations can be grounded in real project signals.',
3063
3130
  template: 'codex-agents-md',
3064
3131
  file: () => 'AGENTS.md',
3065
3132
  line: () => 1,
@@ -3098,15 +3165,15 @@ const CODEX_TECHNIQUES = {
3098
3165
  // CP-08: New checks (O. Repeat-Usage Hygiene)
3099
3166
  // =============================================
3100
3167
 
3101
- codexSnapshotRetention: {
3102
- id: 'CX-O01',
3103
- name: 'At least one prior audit snapshot exists for repeat-usage',
3104
- check: (ctx) => {
3105
- try {
3106
- const indexPath = resolveProjectStateReadPath(ctx.dir, 'snapshots', 'index.json');
3107
- const fs = require('fs');
3108
- if (!fs.existsSync(indexPath)) return null; // No snapshots yet, not a failure
3109
- const entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
3168
+ codexSnapshotRetention: {
3169
+ id: 'CX-O01',
3170
+ name: 'At least one prior audit snapshot exists for repeat-usage',
3171
+ check: (ctx) => {
3172
+ try {
3173
+ const indexPath = resolveProjectStateReadPath(ctx.dir, 'snapshots', 'index.json');
3174
+ const fs = require('fs');
3175
+ if (!fs.existsSync(indexPath)) return null; // No snapshots yet, not a failure
3176
+ const entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
3110
3177
  return Array.isArray(entries) && entries.length > 0;
3111
3178
  } catch {
3112
3179
  return null;
@@ -3121,15 +3188,15 @@ const CODEX_TECHNIQUES = {
3121
3188
  line: () => null,
3122
3189
  },
3123
3190
 
3124
- codexFeedbackLoopHealth: {
3125
- id: 'CX-O02',
3126
- name: 'Feedback loop is functional when feedback has been submitted',
3127
- check: (ctx) => {
3128
- try {
3129
- const indexPath = resolveProjectStateReadPath(ctx.dir, 'outcomes', 'index.json');
3130
- const fs = require('fs');
3131
- if (!fs.existsSync(indexPath)) return null; // No feedback yet, not a failure
3132
- const entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
3191
+ codexFeedbackLoopHealth: {
3192
+ id: 'CX-O02',
3193
+ name: 'Feedback loop is functional when feedback has been submitted',
3194
+ check: (ctx) => {
3195
+ try {
3196
+ const indexPath = resolveProjectStateReadPath(ctx.dir, 'outcomes', 'index.json');
3197
+ const fs = require('fs');
3198
+ if (!fs.existsSync(indexPath)) return null; // No feedback yet, not a failure
3199
+ const entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
3133
3200
  return Array.isArray(entries) && entries.length > 0;
3134
3201
  } catch {
3135
3202
  return null;
@@ -3144,15 +3211,15 @@ const CODEX_TECHNIQUES = {
3144
3211
  line: () => null,
3145
3212
  },
3146
3213
 
3147
- codexTrendDataAvailability: {
3148
- id: 'CX-O03',
3149
- name: 'Trend data is computable (2+ snapshots with compatible schemas)',
3150
- check: (ctx) => {
3151
- try {
3152
- const indexPath = resolveProjectStateReadPath(ctx.dir, 'snapshots', 'index.json');
3153
- const fs = require('fs');
3154
- if (!fs.existsSync(indexPath)) return null;
3155
- const entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
3214
+ codexTrendDataAvailability: {
3215
+ id: 'CX-O03',
3216
+ name: 'Trend data is computable (2+ snapshots with compatible schemas)',
3217
+ check: (ctx) => {
3218
+ try {
3219
+ const indexPath = resolveProjectStateReadPath(ctx.dir, 'snapshots', 'index.json');
3220
+ const fs = require('fs');
3221
+ if (!fs.existsSync(indexPath)) return null;
3222
+ const entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
3156
3223
  const audits = (Array.isArray(entries) ? entries : []).filter(e => e.snapshotKind === 'audit');
3157
3224
  return audits.length >= 2;
3158
3225
  } catch {
@@ -3342,8 +3409,25 @@ const CODEX_TECHNIQUES = {
3342
3409
 
3343
3410
  codexPythonFormatterConfigured: {
3344
3411
  id: 'CX-PY08',
3345
- name: 'Formatter configured (black / isort / ruff format)',
3346
- check: (ctx) => { const hasPy = ctx.files.some(f => /pyproject\.toml$|requirements\.txt$|setup\.py$|manage\.py$/.test(f)); if (!hasPy) return null; const pp = ctx.fileContent('pyproject.toml') || ''; return /\[tool\.black|\[tool\.isort|\[tool\.ruff\.format/i.test(pp); },
3412
+ name: 'Formatter configured (black / isort / ruff / yapf)',
3413
+ check: (ctx) => {
3414
+ const hasPy = ctx.files.some(f => /pyproject\.toml$|requirements\.txt$|setup\.py$|manage\.py$/.test(f));
3415
+ if (!hasPy) return null;
3416
+ const pp = ctx.fileContent('pyproject.toml') || '';
3417
+ // Explicit formatter sections
3418
+ if (/\[tool\.black|\[tool\.isort|\[tool\.ruff\.format|\[tool\.yapf|\[tool\.autopep8/i.test(pp)) return true;
3419
+ // Ruff config implies formatting capability in modern setups (ruff 0.3+)
3420
+ if (/\[tool\.ruff(\.lint)?\]/i.test(pp)) return true;
3421
+ // Standalone config files
3422
+ if (ctx.files.some(f => /^(ruff\.toml|\.ruff\.toml|\.isort\.cfg|\.yapfrc|setup\.cfg)$/i.test(f))) {
3423
+ const setupCfg = ctx.fileContent('setup.cfg') || '';
3424
+ if (/\[isort\]|\[yapf\]|\[flake8\]/i.test(setupCfg)) return true;
3425
+ if (f => /ruff|yapf|isort/i.test(f)) return true;
3426
+ }
3427
+ // Dev dependency signal
3428
+ if (/\b(black|isort|ruff|yapf|autopep8)\b/i.test(pp)) return true;
3429
+ return false;
3430
+ },
3347
3431
  impact: 'medium',
3348
3432
  category: 'python',
3349
3433
  fix: 'Configure black, isort, or ruff format in pyproject.toml.',
@@ -3365,7 +3449,24 @@ const CODEX_TECHNIQUES = {
3365
3449
  codexPythonFastapiEntryDocumented: {
3366
3450
  id: 'CX-PY10',
3367
3451
  name: 'FastAPI entry point documented if FastAPI project',
3368
- check: (ctx) => { const hasPy = ctx.files.some(f => /pyproject\.toml$|requirements\.txt$|setup\.py$|manage\.py$/.test(f)); if (!hasPy) return null; const deps = (ctx.fileContent('pyproject.toml') || '') + (ctx.fileContent('requirements.txt') || ''); if (!/fastapi/i.test(deps)) return null; const docs = (ctx.claudeMdContent ? ctx.claudeMdContent() : ctx.fileContent('CLAUDE.md')) || ctx.fileContent('README.md') || ''; return /fastapi|uvicorn|app\.py|main\.py/i.test(docs); },
3452
+ check: (ctx) => {
3453
+ const hasPy = ctx.files.some(f => /pyproject\.toml$|requirements\.txt$|setup\.py$|manage\.py$/.test(f));
3454
+ if (!hasPy) return null;
3455
+ const pp = ctx.fileContent('pyproject.toml') || '';
3456
+ const reqs = ctx.fileContent('requirements.txt') || '';
3457
+ // FastAPI only in dev/optional/example deps → N/A (SDK with example server)
3458
+ const inMain = /^\s*fastapi\b/im.test(reqs) ||
3459
+ /\[project\.dependencies\][\s\S]*?fastapi/i.test(pp) ||
3460
+ /\[tool\.poetry\.dependencies\][\s\S]*?fastapi/i.test(pp) ||
3461
+ /^\s*dependencies\s*=\s*\[[^\]]*"fastapi/im.test(pp);
3462
+ if (!inMain) return null;
3463
+ const docs = [
3464
+ ctx.claudeMdContent ? ctx.claudeMdContent() : ctx.fileContent('CLAUDE.md'),
3465
+ ctx.fileContent('README.md'),
3466
+ ctx.fileContent('AGENTS.md'),
3467
+ ].filter(Boolean).join('\n');
3468
+ return /fastapi|uvicorn|app\.py|main\.py/i.test(docs);
3469
+ },
3369
3470
  impact: 'high',
3370
3471
  category: 'python',
3371
3472
  fix: 'Document FastAPI entry point and how to run the development server.',
@@ -3376,7 +3477,18 @@ const CODEX_TECHNIQUES = {
3376
3477
  codexPythonMigrationsDocumented: {
3377
3478
  id: 'CX-PY11',
3378
3479
  name: 'Database migrations mentioned (alembic / Django migrations)',
3379
- check: (ctx) => { const hasPy = ctx.files.some(f => /pyproject\.toml$|requirements\.txt$|setup\.py$|manage\.py$/.test(f)); if (!hasPy) return null; const docs = (ctx.claudeMdContent ? ctx.claudeMdContent() : ctx.fileContent('CLAUDE.md')) || ctx.fileContent('README.md') || ''; return /alembic|migrate|makemigrations|django.{0,10}migration/i.test(docs) || ctx.files.some(f => /alembic[.]ini$|alembic[/]/.test(f)); },
3480
+ check: (ctx) => {
3481
+ const hasPy = ctx.files.some(f => /pyproject\.toml$|requirements\.txt$|setup\.py$|manage\.py$/.test(f));
3482
+ if (!hasPy) return null;
3483
+ // SDK/library repos don't need migration docs
3484
+ if (isSdkOrLibraryRepo(ctx)) return null;
3485
+ // Only applicable when repo actually uses a database
3486
+ const deps = (ctx.fileContent('pyproject.toml') || '') + (ctx.fileContent('requirements.txt') || '');
3487
+ const hasDb = /sqlalchemy|django|peewee|tortoise|asyncpg|psycopg|pymongo|pymysql|alembic/i.test(deps);
3488
+ if (!hasDb) return null;
3489
+ const docs = (ctx.claudeMdContent ? ctx.claudeMdContent() : ctx.fileContent('CLAUDE.md')) || ctx.fileContent('README.md') || '';
3490
+ return /alembic|migrate|makemigrations|django.{0,10}migration/i.test(docs) || ctx.files.some(f => /alembic[.]ini$|alembic[/]/.test(f));
3491
+ },
3380
3492
  impact: 'medium',
3381
3493
  category: 'python',
3382
3494
  fix: 'Document database migration workflow (alembic or Django migrations).',
@@ -3464,7 +3576,31 @@ const CODEX_TECHNIQUES = {
3464
3576
  codexPythonPackageStructure: {
3465
3577
  id: 'CX-PY19',
3466
3578
  name: 'Python package has proper structure (src/ layout or __init__.py)',
3467
- check: (ctx) => { const hasPy = ctx.files.some(f => /pyproject\.toml$|requirements\.txt$|setup\.py$|manage\.py$/.test(f)); if (!hasPy) return null; return ctx.files.some(f => /src[/].*[/]__init__\.py$|^[^/]+[/]__init__\.py$/.test(f)); },
3579
+ check: (ctx) => {
3580
+ const hasPy = ctx.files.some(f => /pyproject\.toml$|requirements\.txt$|setup\.py$|manage\.py$/.test(f));
3581
+ if (!hasPy) return null;
3582
+ const fs = require('fs');
3583
+ const path = require('path');
3584
+ // ctx.files only lists root — probe common package layouts directly
3585
+ try {
3586
+ // src/ layout: look for any src/*/__init__.py
3587
+ const srcDir = path.join(ctx.dir, 'src');
3588
+ if (fs.existsSync(srcDir) && fs.statSync(srcDir).isDirectory()) {
3589
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
3590
+ for (const e of entries) {
3591
+ if (e.isDirectory() && fs.existsSync(path.join(srcDir, e.name, '__init__.py'))) return true;
3592
+ }
3593
+ }
3594
+ // Flat layout: <package>/__init__.py at root
3595
+ const rootEntries = fs.readdirSync(ctx.dir, { withFileTypes: true });
3596
+ for (const e of rootEntries) {
3597
+ if (e.isDirectory() && !e.name.startsWith('.') && e.name !== 'tests' && e.name !== 'docs') {
3598
+ if (fs.existsSync(path.join(ctx.dir, e.name, '__init__.py'))) return true;
3599
+ }
3600
+ }
3601
+ } catch { /* fall through */ }
3602
+ return false;
3603
+ },
3468
3604
  impact: 'medium',
3469
3605
  category: 'python',
3470
3606
  fix: 'Use src/ layout or ensure packages have __init__.py files.',
@@ -36,16 +36,36 @@ class CursorProjectContext extends ProjectContext {
36
36
  * NOTE: Subdirectories inside .cursor/rules/ are silently ignored by Cursor.
37
37
  */
38
38
  cursorRules() {
39
- const dir = path.join(this.dir, '.cursor', 'rules');
40
- const files = listFiles(dir, f => f.endsWith('.mdc'));
39
+ const fs = require('fs');
40
+ const rulesPath = path.join(this.dir, '.cursor', 'rules');
41
+ let dir = rulesPath;
42
+ let basePath = '.cursor/rules';
43
+
44
+ // File-redirect pattern: .cursor/rules is a file pointing to another path
45
+ // (e.g., cal.com uses agents/rules/ with .cursor/rules as a text pointer).
46
+ try {
47
+ const stat = fs.statSync(rulesPath);
48
+ if (stat.isFile()) {
49
+ const redirect = fs.readFileSync(rulesPath, 'utf8').trim();
50
+ if (redirect && redirect.length < 500) {
51
+ const resolved = path.resolve(path.dirname(rulesPath), redirect);
52
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
53
+ dir = resolved;
54
+ basePath = path.relative(this.dir, resolved).replace(/\\/g, '/');
55
+ }
56
+ }
57
+ }
58
+ } catch { /* .cursor/rules may not exist — fall through to empty list */ }
59
+
60
+ const files = listFiles(dir, f => f.endsWith('.mdc') || f.endsWith('.md'));
41
61
  return files.map(f => {
42
- const relPath = `.cursor/rules/${f}`;
62
+ const relPath = `${basePath}/${f}`;
43
63
  const content = this.fileContent(relPath);
44
64
  if (!content) return null;
45
65
  const parsed = parseMdc(content);
46
66
  const ruleType = detectRuleType(parsed.frontmatter);
47
67
  return {
48
- name: f.replace('.mdc', ''),
68
+ name: f.replace(/\.(mdc|md)$/, ''),
49
69
  path: relPath,
50
70
  frontmatter: parsed.frontmatter,
51
71
  body: parsed.body,
@@ -14,12 +14,12 @@
14
14
  const os = require('os');
15
15
  const path = require('path');
16
16
  const { CursorProjectContext } = require('./context');
17
- const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
18
- const { attachSourceUrls } = require('../source-urls');
19
- const { buildStackChecks } = require('../stack-checks');
20
- const { isApiProject, isDatabaseProject, isAuthProject, isMonitoringRelevant } = require('../supplemental-checks');
21
- const { hasCostBudgetOrUsageTracking } = require('../cost-tracking');
22
- const { validateMdcFrontmatter, validateMcpEnvVars } = require('./config-parser');
17
+ const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
18
+ const { attachSourceUrls } = require('../source-urls');
19
+ const { buildStackChecks } = require('../stack-checks');
20
+ const { isApiProject, isDatabaseProject, isAuthProject, isMonitoringRelevant } = require('../supplemental-checks');
21
+ const { hasCostBudgetOrUsageTracking } = require('../cost-tracking');
22
+ const { validateMdcFrontmatter, validateMcpEnvVars } = require('./config-parser');
23
23
 
24
24
  // ─── Shared helpers ─────────────────────────────────────────────────────────
25
25
 
@@ -180,6 +180,10 @@ const CURSOR_TECHNIQUES = {
180
180
  name: 'No .cursorrules without migration warning',
181
181
  check: (ctx) => {
182
182
  const hasLegacy = ctx.hasLegacyRules ? ctx.hasLegacyRules() : Boolean(ctx.fileContent('.cursorrules'));
183
+ const hasNewRules = ctx.cursorRules ? ctx.cursorRules().length > 0 : false;
184
+ const hasMcp = Boolean(ctx.fileContent('.cursor/mcp.json'));
185
+ // N/A when repo has no Cursor configuration at all — don't reward absence
186
+ if (!hasLegacy && !hasNewRules && !hasMcp) return null;
183
187
  return !hasLegacy;
184
188
  },
185
189
  impact: 'critical',
@@ -507,17 +511,21 @@ const CURSOR_TECHNIQUES = {
507
511
 
508
512
  cursorPrivacyMode: {
509
513
  id: 'CU-C01',
510
- name: 'Privacy Mode enabled (or explicitly documented as off)',
514
+ name: 'Privacy Mode documented in rules/docs',
511
515
  check: (ctx) => {
512
- // Cannot detect Privacy Mode from files (stored in SQLite state.vscdb)
513
- // Check if rules/docs document the privacy mode status
516
+ // Privacy Mode is an IDE setting stored in SQLite state.vscdb — not auditable
517
+ // from repo files. This check validates that the repo documents its stance.
518
+ const hasNewRules = ctx.cursorRules ? ctx.cursorRules().length > 0 : false;
519
+ const hasLegacy = ctx.hasLegacyRules ? ctx.hasLegacyRules() : Boolean(ctx.fileContent('.cursorrules'));
520
+ // N/A when no rules exist to check against
521
+ if (!hasNewRules && !hasLegacy) return null;
514
522
  const docs = docsBundle(ctx);
515
523
  return /privacy mode|zero.?retention|data retention|privacy.*enabled/i.test(docs);
516
524
  },
517
- impact: 'critical',
518
- rating: 5,
525
+ impact: 'low',
526
+ rating: 3,
519
527
  category: 'trust',
520
- fix: 'Privacy Mode is OFF by default — code is sent to all third-party providers (OpenAI, Anthropic, etc.) unless explicitly enabled. Enable in Cursor Settings → Privacy → Privacy Mode, or document the deliberate decision to keep it off.',
528
+ fix: 'Privacy Mode is OFF by default in Cursor — code is sent to providers unless enabled. Document your stance in a rules file so contributors know whether Privacy Mode is expected on/off. Enable in Cursor Settings → Privacy → Privacy Mode.',
521
529
  template: null,
522
530
  file: () => '.cursor/rules/',
523
531
  line: () => null,
@@ -1323,12 +1331,32 @@ const CURSOR_TECHNIQUES = {
1323
1331
  check: (ctx) => {
1324
1332
  const docs = docsBundle(ctx);
1325
1333
  if (!docs.trim()) return null;
1326
- return /bugbot|bug.?bot|automated.*pr.*review/i.test(docs);
1334
+ // BugBot is an optional Cursor enterprise feature requiring separate installation.
1335
+ // Return N/A unless there is actual evidence the repo uses or intends to use BugBot.
1336
+ const files = new Set(ctx.files || []);
1337
+ const hasBugbotConfigFile = ['bugbot.yml', 'bugbot.yaml', '.bugbot.yml', '.bugbot.yaml'].some(f => files.has(f));
1338
+ let hasBugbotWorkflow = false;
1339
+ try {
1340
+ const fs = require('fs');
1341
+ const path = require('path');
1342
+ const wfDir = path.join(ctx.dir, '.github', 'workflows');
1343
+ if (fs.existsSync(wfDir)) {
1344
+ const wfFiles = fs.readdirSync(wfDir).filter(f => /\.ya?ml$/i.test(f));
1345
+ for (const f of wfFiles) {
1346
+ const content = fs.readFileSync(path.join(wfDir, f), 'utf8');
1347
+ if (/bugbot|bug.?bot/i.test(content)) { hasBugbotWorkflow = true; break; }
1348
+ }
1349
+ }
1350
+ } catch { /* no workflows dir */ }
1351
+ const mentionedInDocs = /bugbot|bug.?bot|automated.*pr.*review/i.test(docs);
1352
+ // N/A when no signal at all that this repo uses/wants BugBot
1353
+ if (!hasBugbotConfigFile && !hasBugbotWorkflow && !mentionedInDocs) return null;
1354
+ return hasBugbotConfigFile || hasBugbotWorkflow || mentionedInDocs;
1327
1355
  },
1328
- impact: 'medium',
1356
+ impact: 'low',
1329
1357
  rating: 3,
1330
1358
  category: 'bugbot',
1331
- fix: 'Enable BugBot for automated PR code review on critical repos.',
1359
+ fix: 'If you use BugBot for automated PR code review, document it in your rules or add the BugBot workflow. Otherwise ignore this check.',
1332
1360
  template: null,
1333
1361
  file: () => '.cursor/rules/',
1334
1362
  line: () => null,
@@ -2212,17 +2240,17 @@ const CURSOR_TECHNIQUES = {
2212
2240
  fix: 'Enable prompt caching in MCP configuration or document caching strategy for repeated prompts.',
2213
2241
  template: null, file: () => '.cursor/mcp.json', line: () => null,
2214
2242
  },
2215
- cursorCostBudgetDefined: {
2216
- id: 'CU-T48', name: 'AI cost budget or per-run usage tracking documented',
2217
- check: (ctx) => {
2218
- const docs = docsBundle(ctx) + (ctx.fileContent('CLAUDE.md') || '');
2219
- if (!docs.trim() && !hasCostBudgetOrUsageTracking('', ctx)) return null;
2220
- return hasCostBudgetOrUsageTracking(docs, ctx);
2221
- },
2222
- impact: 'low', rating: 2, category: 'cost-optimization',
2223
- fix: 'Document AI cost guardrails or per-run usage tracking in README.md or Cursor rules so spend is observable, not guessed.',
2224
- template: null, file: () => 'README.md', line: () => null,
2225
- },
2243
+ cursorCostBudgetDefined: {
2244
+ id: 'CU-T48', name: 'AI cost budget or per-run usage tracking documented',
2245
+ check: (ctx) => {
2246
+ const docs = docsBundle(ctx) + (ctx.fileContent('CLAUDE.md') || '');
2247
+ if (!docs.trim() && !hasCostBudgetOrUsageTracking('', ctx)) return null;
2248
+ return hasCostBudgetOrUsageTracking(docs, ctx);
2249
+ },
2250
+ impact: 'low', rating: 2, category: 'cost-optimization',
2251
+ fix: 'Document AI cost guardrails or per-run usage tracking in README.md or Cursor rules so spend is observable, not guessed.',
2252
+ template: null, file: () => 'README.md', line: () => null,
2253
+ },
2226
2254
 
2227
2255
  // ============================================================
2228
2256
  // === PYTHON STACK CHECKS (category: 'python') ===============