@nerviq/cli 1.2.3 → 1.2.6

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.
@@ -8,69 +8,9 @@
8
8
  * Zero external dependencies - imports only from sibling/parent modules.
9
9
  */
10
10
 
11
- // ─── Platform Strength Matrix (from CLAUDEX research) ─────────────────────────
12
-
13
- const PLATFORM_STRENGTHS = {
14
- claude: {
15
- label: 'Claude Code',
16
- reasoning: 5,
17
- refactoring: 5,
18
- ci: 2,
19
- ide: 2,
20
- sandbox: 3,
21
- inline: 3,
22
- context: 4,
23
- automation: 4,
24
- },
25
- codex: {
26
- label: 'Codex',
27
- reasoning: 4,
28
- ci: 5,
29
- cloud: 5,
30
- ide: 3,
31
- sandbox: 4,
32
- refactoring: 4,
33
- inline: 2,
34
- context: 3,
35
- automation: 4,
36
- },
37
- gemini: {
38
- label: 'Gemini CLI',
39
- reasoning: 4,
40
- context: 5,
41
- sandbox: 5,
42
- ci: 3,
43
- ide: 3,
44
- refactoring: 3,
45
- inline: 2,
46
- cloud: 4,
47
- automation: 3,
48
- },
49
- copilot: {
50
- label: 'GitHub Copilot',
51
- inline: 5,
52
- 'cloud-agent': 4,
53
- ide: 4,
54
- ci: 4,
55
- governance: 3,
56
- reasoning: 3,
57
- refactoring: 3,
58
- context: 3,
59
- automation: 3,
60
- },
61
- cursor: {
62
- label: 'Cursor',
63
- ide: 5,
64
- ui: 5,
65
- background: 4,
66
- automation: 4,
67
- reasoning: 3,
68
- refactoring: 3,
69
- inline: 4,
70
- context: 3,
71
- ci: 2,
72
- },
73
- };
11
+ // ─── Platform Strength Matrix (canonical source: shared/capabilities.js) ─────
12
+
13
+ const { PLATFORM_CAPABILITIES: PLATFORM_STRENGTHS } = require('../shared/capabilities');
74
14
 
75
15
  // ─── Task-type to platform-strength mapping ───────────────────────────────────
76
16
 
@@ -82,7 +22,7 @@ const TASK_TYPE_PROFILES = {
82
22
  },
83
23
  'ci-review': {
84
24
  label: 'CI / Async Review',
85
- requiredStrengths: { ci: 0.5, cloud: 0.3, automation: 0.2 },
25
+ requiredStrengths: { ci: 0.4, cloudTasks: 0.3, async: 0.2, automation: 0.1 },
86
26
  description: 'Asynchronous code review and CI-integrated workflows.',
87
27
  },
88
28
  'ui-work': {
@@ -97,7 +37,7 @@ const TASK_TYPE_PROFILES = {
97
37
  },
98
38
  'infrastructure': {
99
39
  label: 'Infrastructure',
100
- requiredStrengths: { sandbox: 0.4, cloud: 0.3, reasoning: 0.3 },
40
+ requiredStrengths: { sandbox: 0.4, cloudTasks: 0.3, reasoning: 0.3 },
101
41
  description: 'Infrastructure, DevOps, and sandbox-heavy workflows.',
102
42
  },
103
43
  'refactoring': {
@@ -115,6 +55,16 @@ const TASK_TYPE_PROFILES = {
115
55
  requiredStrengths: { reasoning: 0.3, automation: 0.3, ide: 0.2, refactoring: 0.2 },
116
56
  description: 'Starting new projects or major new features from scratch.',
117
57
  },
58
+ 'harness-optimization': {
59
+ label: 'Config Optimization',
60
+ requiredStrengths: { governance: 0.4, automation: 0.3, reasoning: 0.3 },
61
+ description: 'Optimizing AI coding agent configuration and harness settings.',
62
+ },
63
+ 'phased-migration': {
64
+ label: 'Phased Migration',
65
+ requiredStrengths: { reasoning: 0.4, refactoring: 0.3, architecture: 0.3 },
66
+ description: 'Multi-phase migrations requiring sequential execution with validation gates.',
67
+ },
118
68
  };
119
69
 
120
70
  // ─── Scoring ──────────────────────────────────────────────────────────────────
@@ -42,6 +42,9 @@ const PLATFORM_AUDIT_MAP = {
42
42
  gemini: 'gemini',
43
43
  copilot: 'copilot',
44
44
  cursor: 'cursor',
45
+ windsurf: 'windsurf',
46
+ aider: 'aider',
47
+ opencode: 'opencode',
45
48
  };
46
49
 
47
50
  // ─── Cross-platform recommendations ────────────────────────────────────────
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Reads ALL platform config files from a single project and builds a unified
5
5
  * understanding of instructions, MCP servers, trust posture, and governance
6
- * across Claude, Codex, Gemini, Copilot, and Cursor.
6
+ * across Claude, Codex, Gemini, Copilot, Cursor, Windsurf, Aider, and OpenCode.
7
7
  */
8
8
 
9
9
  const fs = require('fs');
@@ -13,10 +13,17 @@ const { CodexProjectContext } = require('../codex/context');
13
13
  const { GeminiProjectContext } = require('../gemini/context');
14
14
  const { CopilotProjectContext } = require('../copilot/context');
15
15
  const { CursorProjectContext } = require('../cursor/context');
16
+ const { WindsurfProjectContext } = require('../windsurf/context');
17
+ const { AiderProjectContext } = require('../aider/context');
18
+ const { OpenCodeProjectContext } = require('../opencode/context');
16
19
  const { getCodexGovernanceSummary } = require('../codex/governance');
17
20
  const { getGeminiGovernanceSummary } = require('../gemini/governance');
18
21
  const { getCopilotGovernanceSummary } = require('../copilot/governance');
19
22
  const { getCursorGovernanceSummary } = require('../cursor/governance');
23
+ const { getWindsurfGovernanceSummary } = require('../windsurf/governance');
24
+ const { getAiderGovernanceSummary } = require('../aider/governance');
25
+ const { getOpenCodeGovernanceSummary } = require('../opencode/governance');
26
+ const { tryParseJsonc } = require('../opencode/config-parser');
20
27
 
21
28
  // ─── Platform detection signatures ──────────────────────────────────────────
22
29
 
@@ -96,6 +103,52 @@ const PLATFORM_SIGNATURES = {
96
103
  rulesDir: '.cursor/rules',
97
104
  hooksDir: null,
98
105
  },
106
+ windsurf: {
107
+ label: 'Windsurf',
108
+ detect: (dir) => {
109
+ try {
110
+ if (fs.existsSync(path.join(dir, '.windsurfrules'))) return true;
111
+ if (fs.existsSync(path.join(dir, '.windsurf'))) return true;
112
+ return false;
113
+ } catch { return false; }
114
+ },
115
+ instructionFiles: ['.windsurfrules'],
116
+ configFiles: ['.windsurfrules', '.windsurf/mcp.json', '.cascadeignore'],
117
+ mcpFiles: ['.windsurf/mcp.json'],
118
+ rulesDir: '.windsurf/rules',
119
+ hooksDir: '.windsurf/workflows',
120
+ },
121
+ aider: {
122
+ label: 'Aider',
123
+ detect: (dir) => {
124
+ try {
125
+ if (fs.existsSync(path.join(dir, '.aider.conf.yml'))) return true;
126
+ if (fs.existsSync(path.join(dir, '.aiderignore'))) return true;
127
+ return false;
128
+ } catch { return false; }
129
+ },
130
+ instructionFiles: ['CONVENTIONS.md', '.aider.conventions.md'],
131
+ configFiles: ['.aider.conf.yml', '.aider.model.settings.yml', '.aiderignore'],
132
+ mcpFiles: [],
133
+ rulesDir: null,
134
+ hooksDir: null,
135
+ },
136
+ opencode: {
137
+ label: 'OpenCode',
138
+ detect: (dir) => {
139
+ try {
140
+ if (fs.existsSync(path.join(dir, 'opencode.json'))) return true;
141
+ if (fs.existsSync(path.join(dir, 'opencode.jsonc'))) return true;
142
+ if (fs.existsSync(path.join(dir, '.opencode'))) return true;
143
+ return false;
144
+ } catch { return false; }
145
+ },
146
+ instructionFiles: ['AGENTS.md', 'CLAUDE.md'],
147
+ configFiles: ['opencode.json', 'opencode.jsonc'],
148
+ mcpFiles: ['opencode.json', 'opencode.jsonc'],
149
+ rulesDir: null,
150
+ hooksDir: null,
151
+ },
99
152
  };
100
153
 
101
154
  // ─── Context builders per platform ──────────────────────────────────────────
@@ -106,6 +159,9 @@ const CONTEXT_BUILDERS = {
106
159
  gemini: (dir) => new GeminiProjectContext(dir),
107
160
  copilot: (dir) => new CopilotProjectContext(dir),
108
161
  cursor: (dir) => new CursorProjectContext(dir),
162
+ windsurf: (dir) => new WindsurfProjectContext(dir),
163
+ aider: (dir) => new AiderProjectContext(dir),
164
+ opencode: (dir) => new OpenCodeProjectContext(dir),
109
165
  };
110
166
 
111
167
  const GOVERNANCE_GETTERS = {
@@ -113,6 +169,9 @@ const GOVERNANCE_GETTERS = {
113
169
  gemini: getGeminiGovernanceSummary,
114
170
  copilot: getCopilotGovernanceSummary,
115
171
  cursor: getCursorGovernanceSummary,
172
+ windsurf: getWindsurfGovernanceSummary,
173
+ aider: getAiderGovernanceSummary,
174
+ opencode: getOpenCodeGovernanceSummary,
116
175
  };
117
176
 
118
177
  // ─── Helpers ────────────────────────────────────────────────────────────────
@@ -133,6 +192,11 @@ function safeParseJson(content) {
133
192
  }
134
193
  }
135
194
 
195
+ function safeParseJsonc(content) {
196
+ const parsed = tryParseJsonc(content);
197
+ return parsed.ok ? parsed.data : null;
198
+ }
199
+
136
200
  /**
137
201
  * Extract instruction text from a platform's instruction files.
138
202
  * Returns array of { file, content } for each found file.
@@ -177,6 +241,17 @@ function extractMcpFromMcpJson(content) {
177
241
  }));
178
242
  }
179
243
 
244
+ function extractMcpFromOpenCodeConfig(content) {
245
+ const json = safeParseJsonc(content);
246
+ if (!json) return [];
247
+ const servers = json.mcpServers || json.servers || {};
248
+ return Object.keys(servers).map(name => ({
249
+ name,
250
+ command: servers[name].command || null,
251
+ args: servers[name].args || [],
252
+ }));
253
+ }
254
+
180
255
  /**
181
256
  * Extract MCP server names from Gemini settings.json.
182
257
  */
@@ -205,6 +280,8 @@ function readMcpServers(dir, platform, mcpFiles) {
205
280
  extracted = extractMcpFromClaudeSettings(content);
206
281
  } else if (platform === 'gemini') {
207
282
  extracted = extractMcpFromGeminiSettings(content);
283
+ } else if (platform === 'opencode') {
284
+ extracted = extractMcpFromOpenCodeConfig(content);
208
285
  } else {
209
286
  extracted = extractMcpFromMcpJson(content);
210
287
  }
@@ -268,6 +345,27 @@ function detectTrustPosture(platform, ctx) {
268
345
  return 'no-sandbox';
269
346
  }
270
347
 
348
+ if (platform === 'windsurf') {
349
+ if (ctx.fileContent('.cascadeignore')) return 'guarded';
350
+ if (ctx.fileContent('.windsurf/mcp.json')) return 'team-managed';
351
+ return 'foreground';
352
+ }
353
+
354
+ if (platform === 'aider') {
355
+ const config = ctx.configContent ? (ctx.configContent() || '') : '';
356
+ if (/^\s*(yes|yes-always)\s*:\s*true\b/m.test(config)) return 'full-auto';
357
+ if (/^\s*auto-commits\s*:\s*true\b/m.test(config)) return 'git-guarded';
358
+ return 'manual-review';
359
+ }
360
+
361
+ if (platform === 'opencode') {
362
+ const config = ctx.configContent ? (ctx.configContent() || '') : '';
363
+ if (/"\*"\s*:\s*"allow"/.test(config)) return 'unrestricted';
364
+ if (/"(?:bash|edit|task|external_directory)"\s*:\s*"deny"/.test(config)) return 'locked-down';
365
+ if (/"(?:bash|edit|task|external_directory)"\s*:\s*"ask"/.test(config)) return 'prompted';
366
+ return 'standard';
367
+ }
368
+
271
369
  return 'unknown';
272
370
  }
273
371
 
@@ -415,10 +513,33 @@ function buildCanonicalModel(dir) {
415
513
  governanceSummary[key] = platformDetails[key].governance;
416
514
  }
417
515
 
516
+ // SD2: Adaptive project signals — infrastructure & tooling detection
517
+ const projectSignals = {};
518
+ const signalChecks = [
519
+ { key: 'docker', label: 'Docker', files: ['Dockerfile', 'docker-compose.yml', 'docker-compose.yaml', '.dockerignore'] },
520
+ { key: 'terraform', label: 'Terraform', files: ['main.tf', 'terraform.tf', '.terraform.lock.hcl'] },
521
+ { key: 'kubernetes', label: 'Kubernetes', files: ['k8s/', 'kubernetes/', 'helm/', 'Chart.yaml'] },
522
+ { key: 'ci-github', label: 'GitHub Actions', files: ['.github/workflows/'] },
523
+ { key: 'ci-gitlab', label: 'GitLab CI', files: ['.gitlab-ci.yml'] },
524
+ { key: 'pytest', label: 'pytest', files: ['pytest.ini', 'conftest.py', 'pyproject.toml'] },
525
+ { key: 'jest', label: 'Jest', files: ['jest.config.js', 'jest.config.ts', 'jest.config.mjs'] },
526
+ { key: 'migrations', label: 'DB Migrations', files: ['migrations/', 'alembic/', 'prisma/migrations/', 'db/migrate/'] },
527
+ { key: 'monorepo', label: 'Monorepo', files: ['pnpm-workspace.yaml', 'lerna.json', 'nx.json', 'turbo.json'] },
528
+ { key: 'openapi', label: 'OpenAPI', files: ['openapi.yaml', 'openapi.json', 'swagger.yaml', 'swagger.json'] },
529
+ ];
530
+ for (const signal of signalChecks) {
531
+ const detected = signal.files.some(f => {
532
+ const full = path.join(dir, f);
533
+ try { return fs.existsSync(full); } catch { return false; }
534
+ });
535
+ if (detected) projectSignals[signal.key] = signal.label;
536
+ }
537
+
418
538
  return {
419
539
  projectName,
420
540
  dir,
421
541
  stacks: stacks.map(s => s.key),
542
+ projectSignals,
422
543
  activePlatforms: platformKeys.map(key => ({
423
544
  platform: key,
424
545
  label: platformDetails[key].label,
@@ -68,6 +68,27 @@ function collectPlatformAudits(dir) {
68
68
  if (result) results.push({ platform: 'cursor', ...result });
69
69
  } catch (_e) { /* platform not available */ }
70
70
 
71
+ // Try Windsurf audit
72
+ try {
73
+ const { audit } = require('../audit');
74
+ const result = audit({ dir, silent: true, platform: 'windsurf' });
75
+ if (result) results.push({ platform: 'windsurf', ...result });
76
+ } catch (_e) { /* platform not available */ }
77
+
78
+ // Try Aider audit
79
+ try {
80
+ const { audit } = require('../audit');
81
+ const result = audit({ dir, silent: true, platform: 'aider' });
82
+ if (result) results.push({ platform: 'aider', ...result });
83
+ } catch (_e) { /* platform not available */ }
84
+
85
+ // Try OpenCode audit
86
+ try {
87
+ const { audit } = require('../audit');
88
+ const result = audit({ dir, silent: true, platform: 'opencode' });
89
+ if (result) results.push({ platform: 'opencode', ...result });
90
+ } catch (_e) { /* platform not available */ }
91
+
71
92
  return results;
72
93
  }
73
94
 
@@ -97,7 +97,7 @@ function detectMcpDrift(model) {
97
97
  const drifts = [];
98
98
  const allPlatforms = model.activePlatforms.map(p => p.platform);
99
99
  const mcpCapablePlatforms = allPlatforms.filter(p =>
100
- p === 'claude' || p === 'gemini' || p === 'copilot' || p === 'cursor'
100
+ p === 'claude' || p === 'gemini' || p === 'copilot' || p === 'cursor' || p === 'windsurf' || p === 'opencode'
101
101
  );
102
102
 
103
103
  if (mcpCapablePlatforms.length < 2) return drifts;
@@ -150,7 +150,7 @@ function detectRuleDrift(model) {
150
150
 
151
151
  // Platforms that support rules: claude (.claude/rules), copilot (.github/instructions), cursor (.cursor/rules)
152
152
  const ruleCapable = allPlatforms.filter(p =>
153
- p === 'claude' || p === 'copilot' || p === 'cursor'
153
+ p === 'claude' || p === 'copilot' || p === 'cursor' || p === 'windsurf'
154
154
  );
155
155
 
156
156
  if (ruleCapable.length < 2) return drifts;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * H7. Shared Memory / Knowledge Layer
3
3
  *
4
- * Cross-platform knowledge storage in .claudex/harmony/ directory.
4
+ * Cross-platform knowledge storage in .nerviq/harmony/ directory.
5
5
  * Persists canonical models, drift history, platform scores,
6
6
  * recommendation outcomes, and routing history.
7
7
  *
@@ -10,8 +10,12 @@
10
10
 
11
11
  const fs = require('fs');
12
12
  const path = require('path');
13
+ const {
14
+ resolveHarmonyStateReadPath,
15
+ ensureHarmonyStateDir,
16
+ } = require('../state-paths');
13
17
 
14
- const HARMONY_DIR = '.claudex/harmony';
18
+ const HARMONY_DIR = '.nerviq/harmony';
15
19
 
16
20
  const STATE_FILES = {
17
21
  canon: 'canon.json',
@@ -24,9 +28,7 @@ const STATE_FILES = {
24
28
  // ─── File I/O helpers ─────────────────────────────────────────────────────────
25
29
 
26
30
  function ensureHarmonyDir(dir) {
27
- const harmonyPath = path.join(dir, HARMONY_DIR);
28
- fs.mkdirSync(harmonyPath, { recursive: true });
29
- return harmonyPath;
31
+ return ensureHarmonyStateDir(dir);
30
32
  }
31
33
 
32
34
  function readJsonSafe(filePath) {
@@ -78,11 +80,10 @@ function saveHarmonyState(dir, state) {
78
80
  * @returns {Object} State object with all available keys populated
79
81
  */
80
82
  function loadHarmonyState(dir) {
81
- const harmonyPath = path.join(dir, HARMONY_DIR);
82
83
  const state = {};
83
84
 
84
85
  for (const [key, filename] of Object.entries(STATE_FILES)) {
85
- const filePath = path.join(harmonyPath, filename);
86
+ const filePath = resolveHarmonyStateReadPath(dir, filename);
86
87
  const data = readJsonSafe(filePath);
87
88
  if (data !== null) {
88
89
  state[key] = data;
@@ -90,7 +91,7 @@ function loadHarmonyState(dir) {
90
91
  }
91
92
 
92
93
  // Load manifest for metadata
93
- const manifest = readJsonSafe(path.join(harmonyPath, 'manifest.json'));
94
+ const manifest = readJsonSafe(resolveHarmonyStateReadPath(dir, 'manifest.json'));
94
95
  if (manifest) {
95
96
  state._manifest = manifest;
96
97
  }
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * Generates aligned configs for ALL active platforms from a shared canonical
5
5
  * understanding. Ensures instructions, MCP servers, and trust posture are
6
- * consistent across Claude, Codex, Gemini, Copilot, and Cursor.
6
+ * consistent across Claude, Codex, Gemini, Copilot, Cursor, Windsurf,
7
+ * Aider, and OpenCode.
7
8
  *
8
9
  * Uses managed blocks from each platform's patch module so hand-authored
9
10
  * content is always preserved.
@@ -37,6 +38,12 @@ const MANAGED_MARKERS = {
37
38
  start: '<!-- nerviq:managed:start -->',
38
39
  end: '<!-- nerviq:managed:end -->',
39
40
  },
41
+ windsurf: {
42
+ start: '<!-- nerviq:managed:start -->',
43
+ end: '<!-- nerviq:managed:end -->',
44
+ },
45
+ // aider: uses .aider.conf.yml (YAML) — managed HTML-comment blocks not supported
46
+ // opencode: uses opencode.json (JSON) — managed HTML-comment blocks not supported
40
47
  };
41
48
 
42
49
  // ─── Helpers ────────────────────────────────────────────────────────────────
@@ -144,6 +151,9 @@ function getInstructionPath(platform) {
144
151
  case 'gemini': return 'GEMINI.md';
145
152
  case 'copilot': return '.github/copilot-instructions.md';
146
153
  case 'cursor': return '.cursorrules';
154
+ case 'windsurf': return '.windsurfrules';
155
+ // aider: .aider.conf.yml is YAML config — no managed block support, skip instruction sync
156
+ // opencode: opencode.json is JSON config — no managed block support, skip instruction sync
147
157
  default: return null;
148
158
  }
149
159
  }
@@ -178,6 +188,11 @@ function buildMcpConfig(platform, mcpServers) {
178
188
  return { mcpServers: servers };
179
189
  }
180
190
 
191
+ if (platform === 'windsurf') {
192
+ // Windsurf mcp.json format
193
+ return { mcpServers: servers };
194
+ }
195
+
181
196
  return null;
182
197
  }
183
198
 
@@ -190,6 +205,8 @@ function getMcpConfigPath(platform) {
190
205
  case 'gemini': return '.gemini/settings.json';
191
206
  case 'copilot': return '.vscode/mcp.json';
192
207
  case 'cursor': return '.cursor/mcp.json';
208
+ case 'windsurf': return '.windsurf/mcp.json';
209
+ // aider & opencode: no MCP config support
193
210
  default: return null;
194
211
  }
195
212
  }
package/src/insights.js CHANGED
@@ -21,7 +21,7 @@
21
21
  const https = require('https');
22
22
  const os = require('os');
23
23
 
24
- const INSIGHTS_ENDPOINT = 'https://claudex-insights.claudex.workers.dev/v1/report';
24
+ const INSIGHTS_ENDPOINT = 'https://insights.nerviq.net/v1/report';
25
25
  const TIMEOUT_MS = 3000;
26
26
 
27
27
  function shouldCollect() {
@@ -5,7 +5,7 @@
5
5
  * Provides: history, compare, trend, watch, feedback, insights.
6
6
  *
7
7
  * OpenCode snapshots are stored alongside Claude snapshots in
8
- * .claude/claudex-setup/snapshots/ but filtered by platform='opencode'.
8
+ * .nerviq/snapshots/ (legacy: .claude/claudex-setup/snapshots/) but filtered by platform='opencode'.
9
9
  */
10
10
 
11
11
  const path = require('path');
@@ -142,7 +142,7 @@ function applyPatch(dir, filePath, patchFn, options = {}) {
142
142
  return { success: true, reason: 'dry run', preview, unchanged: false };
143
143
  }
144
144
 
145
- const backupPath = fullPath + '.claudex-backup';
145
+ const backupPath = fullPath + '.nerviq-backup';
146
146
  fs.writeFileSync(backupPath, original, 'utf8');
147
147
  fs.writeFileSync(fullPath, patched, 'utf8');
148
148
 
@@ -21,11 +21,12 @@
21
21
  */
22
22
 
23
23
  const os = require('os');
24
- const path = require('path');
25
- const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
26
- const { attachSourceUrls } = require('../source-urls');
27
- const { buildStackChecks } = require('../stack-checks');
28
- const { isApiProject, isDatabaseProject, isAuthProject, isMonitoringRelevant } = require('../supplemental-checks');
24
+ const path = require('path');
25
+ const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
26
+ const { attachSourceUrls } = require('../source-urls');
27
+ const { buildStackChecks } = require('../stack-checks');
28
+ const { isApiProject, isDatabaseProject, isAuthProject, isMonitoringRelevant } = require('../supplemental-checks');
29
+ const { resolveProjectStateReadPath } = require('../state-paths');
29
30
 
30
31
  const DEFAULT_PROJECT_DOC_MAX_BYTES = 32768;
31
32
 
@@ -1458,14 +1459,15 @@ const OPENCODE_TECHNIQUES = {
1458
1459
  line: () => null,
1459
1460
  },
1460
1461
 
1461
- opencodePilotEvidence: {
1462
- id: 'OC-M03',
1463
- name: 'OpenCode setup has been audited at least once',
1464
- check: (ctx) => {
1465
- // Check for nerviq activity artifacts
1466
- const hasArtifacts = ctx.hasDir('.claude/claudex-setup');
1467
- return hasArtifacts ? true : null;
1468
- },
1462
+ opencodePilotEvidence: {
1463
+ id: 'OC-M03',
1464
+ name: 'OpenCode setup has been audited at least once',
1465
+ check: (ctx) => {
1466
+ // Check for nerviq activity artifacts
1467
+ const fs = require('fs');
1468
+ const hasArtifacts = fs.existsSync(resolveProjectStateReadPath(ctx.dir));
1469
+ return hasArtifacts ? true : null;
1470
+ },
1469
1471
  impact: 'low',
1470
1472
  rating: 2,
1471
1473
  category: 'governance',