@nerviq/cli 1.2.5 → 1.2.7

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/bin/cli.js CHANGED
@@ -24,7 +24,7 @@ const COMMAND_ALIASES = {
24
24
  gov: 'governance',
25
25
  outcome: 'feedback',
26
26
  };
27
- const KNOWN_COMMANDS = ['audit', 'org', 'setup', 'augment', 'suggest-only', 'plan', 'apply', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'doctor', 'convert', 'migrate', 'catalog', 'certify', 'serve', 'help', 'version'];
27
+ const KNOWN_COMMANDS = ['audit', 'org', 'setup', 'augment', 'suggest-only', 'plan', 'apply', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'doctor', 'convert', 'migrate', 'catalog', 'certify', 'serve', 'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise', 'harmony-watch', 'harmony-governance', 'harmony-add', 'synergy-report', 'help', 'version'];
28
28
 
29
29
  function levenshtein(a, b) {
30
30
  const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
@@ -312,6 +312,10 @@ const HELP = `
312
312
 
313
313
  CROSS-PLATFORM
314
314
  nerviq harmony-audit Drift detection across all active platforms
315
+ nerviq harmony-sync Preview cross-platform sync (dry run)
316
+ nerviq harmony-sync --fix Apply cross-platform sync (write files)
317
+ nerviq harmony-sync --json JSON output for CI/automation
318
+ nerviq harmony-add <platform> Add a new platform to the project
315
319
  nerviq synergy-report Multi-agent amplification opportunities
316
320
  nerviq convert --from X --to Y Convert configs between platforms
317
321
  nerviq migrate --platform X Platform version migration helper
@@ -408,6 +412,8 @@ async function main() {
408
412
  lite: flags.includes('--lite'),
409
413
  snapshot: flags.includes('--snapshot'),
410
414
  feedback: flags.includes('--feedback'),
415
+ fix: flags.includes('--fix'),
416
+ autoSync: flags.includes('--auto-sync'),
411
417
  dryRun: flags.includes('--dry-run'),
412
418
  threshold: parsed.threshold !== null ? Number(parsed.threshold) : null,
413
419
  out: parsed.out,
@@ -501,7 +507,7 @@ async function main() {
501
507
  'history', 'compare', 'trend', 'feedback', 'catalog', 'certify', 'serve', 'help', 'version',
502
508
  // Harmony + Synergy (cross-platform)
503
509
  'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise',
504
- 'harmony-watch', 'harmony-governance', 'synergy-report',
510
+ 'harmony-watch', 'harmony-governance', 'harmony-add', 'synergy-report',
505
511
  ]);
506
512
 
507
513
  if (options.platform === 'codex') {
@@ -862,6 +868,129 @@ async function main() {
862
868
  process.on('SIGINT', closeServer);
863
869
  process.on('SIGTERM', closeServer);
864
870
  return;
871
+ } else if (normalizedCommand === 'harmony-audit') {
872
+ const { runHarmonyAudit } = require('../src/harmony/cli');
873
+ await runHarmonyAudit(options);
874
+ process.exit(0);
875
+ } else if (normalizedCommand === 'harmony-sync') {
876
+ const { previewHarmonySync, applyHarmonySync } = require('../src/harmony/sync');
877
+ const dir = options.dir || process.cwd();
878
+
879
+ if (options.fix) {
880
+ // Apply mode: write files
881
+ const result = applyHarmonySync(dir);
882
+ if (options.json) {
883
+ console.log(JSON.stringify(result, null, 2));
884
+ } else {
885
+ console.log('');
886
+ console.log('\x1b[1m Harmony Sync — Apply\x1b[0m');
887
+ console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
888
+ console.log('');
889
+ if (result.applied.length === 0 && result.skipped.length === 0) {
890
+ console.log(' \x1b[32mAll platforms are already in sync. Nothing to apply.\x1b[0m');
891
+ } else {
892
+ for (const item of result.applied) {
893
+ console.log(` \x1b[32m✓\x1b[0m ${item.action.padEnd(8)} ${item.platform.padEnd(12)} ${item.path}`);
894
+ }
895
+ for (const item of result.skipped) {
896
+ const reason = typeof item === 'string' ? item : (item.reason || item.path);
897
+ console.log(` \x1b[33m⚠\x1b[0m skipped ${reason}`);
898
+ }
899
+ console.log('');
900
+ if (result.summary) {
901
+ console.log(` Files: ${result.summary.totalFiles} (${result.summary.creates} created, ${result.summary.patches} patched)`);
902
+ console.log(` Platforms: ${result.summary.platforms.join(', ')}`);
903
+ }
904
+ }
905
+ if (result.warnings && result.warnings.length > 0) {
906
+ console.log('');
907
+ for (const w of result.warnings) {
908
+ console.log(` \x1b[33m⚠\x1b[0m ${w}`);
909
+ }
910
+ }
911
+ console.log('');
912
+ }
913
+ } else {
914
+ // Preview mode (dry run)
915
+ const plan = previewHarmonySync(dir);
916
+ if (options.json) {
917
+ console.log(JSON.stringify(plan, null, 2));
918
+ } else {
919
+ console.log('');
920
+ console.log('\x1b[1m Harmony Sync — Preview\x1b[0m');
921
+ console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
922
+ console.log('');
923
+ if (plan.files.length === 0) {
924
+ console.log(' \x1b[32mAll platforms are already in sync. No changes needed.\x1b[0m');
925
+ } else {
926
+ for (const file of plan.files) {
927
+ const actionColor = file.action === 'create' ? '\x1b[32m' : '\x1b[36m';
928
+ console.log(` ${actionColor}${file.action.padEnd(8)}\x1b[0m ${file.platform.padEnd(12)} ${file.path}`);
929
+ if (file.preview) {
930
+ console.log(` \x1b[2m${file.preview}\x1b[0m`);
931
+ }
932
+ }
933
+ console.log('');
934
+ console.log(` Total: ${plan.summary.totalFiles} file(s) — ${plan.summary.creates} create, ${plan.summary.patches} patch`);
935
+ console.log(` Platforms: ${plan.summary.platforms.join(', ')}`);
936
+ if (plan.summary.recommendedTrust) {
937
+ console.log(` Recommended trust: ${plan.summary.recommendedTrust}`);
938
+ }
939
+ console.log('');
940
+ console.log(' Run \x1b[1mnerviq harmony-sync --fix\x1b[0m to apply these changes.');
941
+ }
942
+ if (plan.warnings && plan.warnings.length > 0) {
943
+ console.log('');
944
+ for (const w of plan.warnings) {
945
+ console.log(` \x1b[33m⚠\x1b[0m ${w}`);
946
+ }
947
+ }
948
+ console.log('');
949
+ }
950
+ }
951
+ process.exit(0);
952
+ } else if (normalizedCommand === 'harmony-drift') {
953
+ const { runHarmonyDrift } = require('../src/harmony/cli');
954
+ await runHarmonyDrift(options);
955
+ process.exit(0);
956
+ } else if (normalizedCommand === 'harmony-advise') {
957
+ const { runHarmonyAdvise } = require('../src/harmony/cli');
958
+ await runHarmonyAdvise(options);
959
+ process.exit(0);
960
+ } else if (normalizedCommand === 'harmony-watch') {
961
+ const { runHarmonyWatch } = require('../src/harmony/cli');
962
+ await runHarmonyWatch(options);
963
+ } else if (normalizedCommand === 'harmony-governance') {
964
+ const { runHarmonyGovernance } = require('../src/harmony/cli');
965
+ await runHarmonyGovernance(options);
966
+ process.exit(0);
967
+ } else if (normalizedCommand === 'harmony-add') {
968
+ const { addPlatform } = require('../src/harmony/add');
969
+ const platformArg = parsed.extraArgs[0];
970
+ if (!platformArg) {
971
+ console.log('\n Usage: nerviq harmony-add <platform>');
972
+ console.log(' Available: claude, codex, gemini, copilot, cursor, windsurf, aider, opencode\n');
973
+ process.exit(1);
974
+ }
975
+ const dir = options.dir || process.cwd();
976
+ const result = addPlatform(dir, platformArg.toLowerCase());
977
+ if (options.json) {
978
+ console.log(JSON.stringify(result, null, 2));
979
+ } else if (result.success) {
980
+ console.log(`\n \x1b[32m\u2713\x1b[0m Added ${result.platform} to project`);
981
+ result.created.forEach(f => console.log(` Created: ${f}`));
982
+ console.log(` Platforms: ${result.beforeCount} \u2192 ${result.afterCount}`);
983
+ if (result.syncApplied > 0) console.log(` Harmony sync: ${result.syncApplied} file(s) updated`);
984
+ console.log('');
985
+ } else {
986
+ console.log(`\n \x1b[31m\u2717\x1b[0m ${result.error}\n`);
987
+ process.exit(1);
988
+ }
989
+ process.exit(0);
990
+ } else if (normalizedCommand === 'synergy-report') {
991
+ // Placeholder — synergy report is referenced but may not be implemented yet
992
+ console.log('\n Synergy report: coming soon.\n');
993
+ process.exit(0);
865
994
  } else if (normalizedCommand === 'doctor') {
866
995
  const { runDoctor } = require('../src/doctor');
867
996
  const output = await runDoctor({ dir: options.dir, json: options.json, verbose: options.verbose });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nerviq/cli",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
4
4
  "description": "The intelligent nervous system for AI coding agents — 2,306 checks across 8 platforms and 10 languages. Audit, align, and amplify.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/activity.js CHANGED
@@ -228,7 +228,11 @@ function getHistory(dir, limit = 20) {
228
228
  const entries = readSnapshotIndex(dir);
229
229
  return entries
230
230
  .filter(e => e.snapshotKind === 'audit')
231
- .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
231
+ .sort((a, b) => {
232
+ const dateDiff = new Date(b.createdAt) - new Date(a.createdAt);
233
+ if (dateDiff !== 0) return dateDiff;
234
+ return (b.id || '').localeCompare(a.id || '');
235
+ })
232
236
  .slice(0, limit);
233
237
  }
234
238
 
package/src/audit.js CHANGED
@@ -1279,7 +1279,25 @@ async function audit(options) {
1279
1279
  console.log('');
1280
1280
  }
1281
1281
 
1282
- console.log(colorize(` Backed by CLAUDEX research and evidence for ${spec.platformLabel}`, 'dim'));
1282
+ // Cross-platform synergy hint
1283
+ try {
1284
+ const { detectActivePlatforms } = require('./harmony/canon');
1285
+ const { analyzeCompensation } = require('./synergy/compensation');
1286
+ const { calculateSynergyScore } = require('./synergy/ranking');
1287
+ const detected = detectActivePlatforms(options.dir);
1288
+ const activePlatforms = (detected || []).filter(p => p.detected).map(p => p.platform);
1289
+ if (activePlatforms.length >= 2) {
1290
+ const comp = analyzeCompensation(activePlatforms);
1291
+ const synergyScore = calculateSynergyScore(activePlatforms);
1292
+ console.log(colorize(` Cross-platform synergy: ${activePlatforms.length} platforms detected`, 'blue'));
1293
+ console.log(colorize(` Platforms: ${activePlatforms.join(', ')}`, 'dim'));
1294
+ console.log(colorize(` Compensations: ${comp.compensations.length} | Gaps: ${comp.uncoveredGaps.length}`, 'dim'));
1295
+ console.log(colorize(` Run: npx nerviq harmony-audit for full cross-platform analysis`, 'dim'));
1296
+ console.log('');
1297
+ }
1298
+ } catch { /* synergy display is optional */ }
1299
+
1300
+ console.log(colorize(` Backed by NERVIQ research and evidence for ${spec.platformLabel}`, 'dim'));
1283
1301
  console.log(colorize(' https://github.com/nerviq/nerviq', 'dim'));
1284
1302
  console.log('');
1285
1303
 
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Platform Addition Wizard
3
+ * Helps users add a new platform config to their project.
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { detectActivePlatforms, PLATFORM_SIGNATURES } = require('./canon');
9
+ const { applyHarmonySync } = require('./sync');
10
+
11
+ const PLATFORM_BOOTSTRAPS = {
12
+ claude: { files: [{ path: 'CLAUDE.md', content: '# Project Instructions\n\nAdd your Claude Code instructions here.\n' }] },
13
+ codex: { files: [{ path: 'AGENTS.md', content: '# Agents Instructions\n\nAdd your Codex instructions here.\n' }] },
14
+ gemini: { files: [{ path: 'GEMINI.md', content: '# Gemini Instructions\n\nAdd your Gemini CLI instructions here.\n' }] },
15
+ copilot: { files: [{ path: '.github/copilot-instructions.md', content: '# Copilot Instructions\n\nAdd your GitHub Copilot instructions here.\n' }] },
16
+ cursor: { files: [{ path: '.cursorrules', content: '# Cursor Rules\n\nAdd your Cursor rules here.\n' }] },
17
+ windsurf: { files: [{ path: '.windsurfrules', content: '# Windsurf Rules\n\nAdd your Windsurf rules here.\n' }] },
18
+ aider: { files: [{ path: '.aider.conf.yml', content: '# Aider Configuration\n# See: https://aider.chat/docs/config/aider_conf.html\n' }] },
19
+ opencode: { files: [{ path: 'opencode.json', content: '{\n "instructions": "Add your OpenCode instructions here."\n}\n' }] },
20
+ };
21
+
22
+ function addPlatform(dir, platformKey) {
23
+ // Validate platform
24
+ if (!PLATFORM_SIGNATURES[platformKey]) {
25
+ return { success: false, error: `Unknown platform: ${platformKey}. Available: ${Object.keys(PLATFORM_SIGNATURES).join(', ')}` };
26
+ }
27
+
28
+ // Check if already active
29
+ const active = detectActivePlatforms(dir);
30
+ const alreadyActive = active.find(p => p.platform === platformKey);
31
+ if (alreadyActive) {
32
+ return { success: false, error: `${platformKey} is already active in this project.` };
33
+ }
34
+
35
+ const beforeCount = active.length;
36
+ const bootstrap = PLATFORM_BOOTSTRAPS[platformKey];
37
+ const created = [];
38
+
39
+ // Create bootstrap files
40
+ for (const file of bootstrap.files) {
41
+ const fullPath = path.join(dir, file.path);
42
+ if (fs.existsSync(fullPath)) continue;
43
+ const dirName = path.dirname(fullPath);
44
+ fs.mkdirSync(dirName, { recursive: true });
45
+ fs.writeFileSync(fullPath, file.content, 'utf8');
46
+ created.push(file.path);
47
+ }
48
+
49
+ // Run harmony sync to populate managed blocks
50
+ let syncResult = null;
51
+ try {
52
+ syncResult = applyHarmonySync(dir);
53
+ } catch { /* sync is optional */ }
54
+
55
+ const afterActive = detectActivePlatforms(dir);
56
+ const afterCount = afterActive.length;
57
+
58
+ return {
59
+ success: true,
60
+ platform: platformKey,
61
+ created,
62
+ beforeCount,
63
+ afterCount,
64
+ syncApplied: syncResult ? syncResult.applied.length : 0,
65
+ };
66
+ }
67
+
68
+ module.exports = { addPlatform, PLATFORM_BOOTSTRAPS };
@@ -55,6 +55,16 @@ const TASK_TYPE_PROFILES = {
55
55
  requiredStrengths: { reasoning: 0.3, automation: 0.3, ide: 0.2, refactoring: 0.2 },
56
56
  description: 'Starting new projects or major new features from scratch.',
57
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
+ },
58
68
  };
59
69
 
60
70
  // ─── Scoring ──────────────────────────────────────────────────────────────────
@@ -513,10 +513,33 @@ function buildCanonicalModel(dir) {
513
513
  governanceSummary[key] = platformDetails[key].governance;
514
514
  }
515
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
+
516
538
  return {
517
539
  projectName,
518
540
  dir,
519
541
  stacks: stacks.map(s => s.key),
542
+ projectSignals,
520
543
  activePlatforms: platformKeys.map(key => ({
521
544
  platform: key,
522
545
  label: platformDetails[key].label,
@@ -300,6 +300,7 @@ async function runHarmonyWatch(options) {
300
300
 
301
301
  await startHarmonyWatch({
302
302
  dir,
303
+ autoSync: !!options.autoSync,
303
304
  debounceMs: options.debounce || 800,
304
305
  onDriftDetected: (platform, details) => {
305
306
  console.log(c(` DRIFT ALERT: ${platform} score dropped by ${Math.abs(details.delta)}`, 'red'));
@@ -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
  }
@@ -36,6 +36,13 @@ const PLATFORM_WATCH_FILES = [
36
36
  '.github/copilot-review-instructions.md',
37
37
  // Cursor
38
38
  '.cursorrules',
39
+ // Windsurf
40
+ '.windsurfrules',
41
+ // Aider
42
+ '.aider.conf.yml',
43
+ '.aiderignore',
44
+ // OpenCode
45
+ 'opencode.json',
39
46
  // Shared
40
47
  '.gitignore',
41
48
  'package.json',
@@ -53,6 +60,9 @@ const PLATFORM_WATCH_DIRS = [
53
60
  '.github',
54
61
  '.cursor',
55
62
  '.cursor/rules',
63
+ '.windsurf',
64
+ '.windsurf/rules',
65
+ '.opencode',
56
66
  ];
57
67
 
58
68
  // ─── fs.watch helpers (mirror pattern from src/watch.js) ──────────────────────
@@ -174,6 +184,9 @@ function identifyPlatform(filePath) {
174
184
  if (normalized.includes('.gemini') || normalized.includes('gemini.md')) return 'gemini';
175
185
  if (normalized.includes('copilot') || normalized.includes('.github')) return 'copilot';
176
186
  if (normalized.includes('.cursor') || normalized.includes('cursorrules')) return 'cursor';
187
+ if (normalized.includes('.windsurf') || normalized.includes('windsurfrules')) return 'windsurf';
188
+ if (normalized.includes('.aider') || normalized.includes('aiderignore')) return 'aider';
189
+ if (normalized.includes('.opencode') || normalized.includes('opencode.json')) return 'opencode';
177
190
  return 'unknown';
178
191
  }
179
192
 
@@ -187,6 +200,7 @@ function identifyPlatform(filePath) {
187
200
  * @param {Function} [options.onDriftDetected] - Callback when drift increases: (platform, details) => void
188
201
  * @param {Function} [options.onPlatformChange] - Callback on any platform config change: (platform, file) => void
189
202
  * @param {Function} [options.runAudit] - Optional audit function to re-run on changes
203
+ * @param {boolean} [options.autoSync=false] - Auto-apply harmony sync when drift is detected
190
204
  * @param {number} [options.debounceMs=800] - Debounce interval in ms
191
205
  */
192
206
  async function startHarmonyWatch(options) {
@@ -195,6 +209,7 @@ async function startHarmonyWatch(options) {
195
209
  onDriftDetected,
196
210
  onPlatformChange,
197
211
  runAudit,
212
+ autoSync = false,
198
213
  debounceMs = 800,
199
214
  } = options;
200
215
 
@@ -204,7 +219,10 @@ async function startHarmonyWatch(options) {
204
219
  console.log(c(' nerviq harmony watch', 'bold'));
205
220
  console.log(c(' ═══════════════════════════════════════', 'dim'));
206
221
  console.log(c(` Watching: ${dir}`, 'dim'));
207
- console.log(c(` Platforms: Claude, Codex, Gemini, Copilot, Cursor`, 'dim'));
222
+ console.log(c(` Platforms: Claude, Codex, Gemini, Copilot, Cursor, Windsurf, Aider, OpenCode`, 'dim'));
223
+ if (autoSync) {
224
+ console.log(c(` Auto-sync: ON — drift will be auto-corrected`, 'green'));
225
+ }
208
226
  console.log(c(` Mode: ${recursiveSupported ? 'native recursive' : 'expanded directory fallback'}`, 'dim'));
209
227
  console.log(c(' Press Ctrl+C to stop', 'dim'));
210
228
  console.log('');
@@ -293,6 +311,22 @@ async function startHarmonyWatch(options) {
293
311
  }
294
312
  }
295
313
 
314
+ // Auto-sync on drift
315
+ if (delta < 0 && autoSync) {
316
+ try {
317
+ const { applyHarmonySync } = require('./sync');
318
+ const syncResult = applyHarmonySync(dir);
319
+ if (syncResult.applied.length > 0) {
320
+ console.log(c(` Auto-sync: applied ${syncResult.applied.length} fix(es)`, 'green'));
321
+ for (const item of syncResult.applied) {
322
+ console.log(c(` ✓ ${item.action} ${item.path}`, 'dim'));
323
+ }
324
+ }
325
+ } catch (_e) {
326
+ console.log(c(` Auto-sync failed: ${_e.message}`, 'yellow'));
327
+ }
328
+ }
329
+
296
330
  lastScores[p] = newScore;
297
331
  }
298
332
  }
package/src/techniques.js CHANGED
@@ -4,12 +4,203 @@
4
4
  * Each technique includes: what to check, how to fix, impact level.
5
5
  */
6
6
 
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
7
10
  function hasFrontendSignals(ctx) {
8
11
  const pkg = ctx.fileContent('package.json') || '';
9
12
  return /react|vue|angular|next|svelte|tailwind|vite|astro/i.test(pkg) ||
10
13
  ctx.files.some(f => /tailwind\.config|vite\.config|next\.config|svelte\.config|nuxt\.config|pages\/|components\/|app\//i.test(f));
11
14
  }
12
15
 
16
+ function getClaudeHookContents(ctx) {
17
+ const hookFiles = ctx.dirFiles('.claude/hooks').filter(f => /\.(js|cjs|mjs|ts|sh|py)$/.test(f));
18
+ return hookFiles.map(f => ctx.fileContent(`.claude/hooks/${f}`) || '');
19
+ }
20
+
21
+ function matchesPattern(value, pattern) {
22
+ if (pattern instanceof RegExp) {
23
+ pattern.lastIndex = 0;
24
+ return pattern.test(value);
25
+ }
26
+ return value === pattern;
27
+ }
28
+
29
+ function getProjectEntries(ctx) {
30
+ if (ctx.__nerviqProjectEntries) return ctx.__nerviqProjectEntries;
31
+
32
+ const entries = [];
33
+ const skippedDirs = new Set([
34
+ '.git',
35
+ '.hg',
36
+ '.svn',
37
+ 'node_modules',
38
+ '__pycache__',
39
+ '.pytest_cache',
40
+ '.mypy_cache',
41
+ '.ruff_cache',
42
+ '.venv',
43
+ 'venv',
44
+ 'env',
45
+ '.tox',
46
+ '.nox',
47
+ 'vendor',
48
+ 'dist',
49
+ 'build',
50
+ 'coverage',
51
+ ]);
52
+
53
+ const walk = (relPath = '') => {
54
+ const fullPath = relPath
55
+ ? path.join(ctx.dir, ...relPath.split('/'))
56
+ : ctx.dir;
57
+
58
+ let dirents = [];
59
+ try {
60
+ dirents = fs.readdirSync(fullPath, { withFileTypes: true });
61
+ } catch {
62
+ return;
63
+ }
64
+
65
+ for (const dirent of dirents) {
66
+ if (dirent.name === '.DS_Store') continue;
67
+
68
+ const entryPath = relPath ? `${relPath}/${dirent.name}` : dirent.name;
69
+ if (dirent.isDirectory()) {
70
+ if (skippedDirs.has(dirent.name)) continue;
71
+ entries.push(`${entryPath}/`);
72
+ walk(entryPath);
73
+ } else {
74
+ entries.push(entryPath);
75
+ }
76
+ }
77
+ };
78
+
79
+ walk();
80
+ ctx.__nerviqProjectEntries = entries;
81
+ return entries;
82
+ }
83
+
84
+ function getProjectFiles(ctx) {
85
+ if (ctx.__nerviqProjectFiles) return ctx.__nerviqProjectFiles;
86
+ ctx.__nerviqProjectFiles = getProjectEntries(ctx).filter(entry => !entry.endsWith('/'));
87
+ return ctx.__nerviqProjectFiles;
88
+ }
89
+
90
+ function findProjectFiles(ctx, pattern) {
91
+ return getProjectFiles(ctx).filter(file => matchesPattern(file, pattern));
92
+ }
93
+
94
+ function hasProjectFile(ctx, pattern) {
95
+ return findProjectFiles(ctx, pattern).length > 0;
96
+ }
97
+
98
+ function readProjectFiles(ctx, pattern, limit = 60) {
99
+ return findProjectFiles(ctx, pattern)
100
+ .slice(0, limit)
101
+ .map(file => ctx.fileContent(file) || '')
102
+ .filter(Boolean)
103
+ .join('\n');
104
+ }
105
+
106
+ function isPythonProject(ctx) {
107
+ if (ctx.__nerviqIsPython !== undefined) return ctx.__nerviqIsPython;
108
+ ctx.__nerviqIsPython =
109
+ hasProjectFile(ctx, /(^|\/)(pyproject\.toml|requirements[^/]*\.txt|setup\.py)$/i) ||
110
+ hasProjectFile(ctx, /\.py$/i);
111
+ return ctx.__nerviqIsPython;
112
+ }
113
+
114
+ function isGoProject(ctx) {
115
+ if (ctx.__nerviqIsGo !== undefined) return ctx.__nerviqIsGo;
116
+ ctx.__nerviqIsGo = hasProjectFile(ctx, /(^|\/)go\.mod$/i);
117
+ return ctx.__nerviqIsGo;
118
+ }
119
+
120
+ function getPythonFiles(ctx) {
121
+ if (ctx.__nerviqPythonFiles) return ctx.__nerviqPythonFiles;
122
+ ctx.__nerviqPythonFiles = findProjectFiles(ctx, /\.py$/i);
123
+ return ctx.__nerviqPythonFiles;
124
+ }
125
+
126
+ function getMainPythonFiles(ctx) {
127
+ if (ctx.__nerviqMainPythonFiles) return ctx.__nerviqMainPythonFiles;
128
+ ctx.__nerviqMainPythonFiles = getPythonFiles(ctx)
129
+ .filter(file => !/(^|\/)(tests?|__pycache__|migrations)\//i.test(file))
130
+ .filter(file => !/(^|\/)(test_[^/]+|conftest)\.py$/i.test(file))
131
+ .slice(0, 50);
132
+ return ctx.__nerviqMainPythonFiles;
133
+ }
134
+
135
+ function getPythonProjectText(ctx) {
136
+ if (ctx.__nerviqPythonProjectText) return ctx.__nerviqPythonProjectText;
137
+ ctx.__nerviqPythonProjectText = [
138
+ readProjectFiles(ctx, /(^|\/)pyproject\.toml$/i),
139
+ readProjectFiles(ctx, /(^|\/)requirements[^/]*\.txt$/i),
140
+ readProjectFiles(ctx, /(^|\/)setup\.py$/i),
141
+ readProjectFiles(ctx, /(^|\/)setup\.cfg$/i),
142
+ readProjectFiles(ctx, /(^|\/)Pipfile$/i),
143
+ ].filter(Boolean).join('\n');
144
+ return ctx.__nerviqPythonProjectText;
145
+ }
146
+
147
+ function getGoFiles(ctx) {
148
+ if (ctx.__nerviqGoFiles) return ctx.__nerviqGoFiles;
149
+ ctx.__nerviqGoFiles = findProjectFiles(ctx, /\.go$/i);
150
+ return ctx.__nerviqGoFiles;
151
+ }
152
+
153
+ function getMainGoFiles(ctx) {
154
+ if (ctx.__nerviqMainGoFiles) return ctx.__nerviqMainGoFiles;
155
+ ctx.__nerviqMainGoFiles = getGoFiles(ctx).filter(file => !/_test\.go$/i.test(file)).slice(0, 60);
156
+ return ctx.__nerviqMainGoFiles;
157
+ }
158
+
159
+ function getWorkflowContent(ctx) {
160
+ if (ctx.__nerviqWorkflowContent !== undefined) return ctx.__nerviqWorkflowContent;
161
+ ctx.__nerviqWorkflowContent = readProjectFiles(ctx, /^\.github\/workflows\/.*\.ya?ml$/i);
162
+ return ctx.__nerviqWorkflowContent;
163
+ }
164
+
165
+ function getPreCommitContent(ctx) {
166
+ if (ctx.__nerviqPreCommitContent !== undefined) return ctx.__nerviqPreCommitContent;
167
+ ctx.__nerviqPreCommitContent = readProjectFiles(ctx, /(^|\/)\.pre-commit-config\.ya?ml$/i);
168
+ return ctx.__nerviqPreCommitContent;
169
+ }
170
+
171
+ function getGoProjectText(ctx) {
172
+ if (ctx.__nerviqGoProjectText) return ctx.__nerviqGoProjectText;
173
+ ctx.__nerviqGoProjectText = [
174
+ readProjectFiles(ctx, /(^|\/)go\.mod$/i),
175
+ getWorkflowContent(ctx),
176
+ readProjectFiles(ctx, /(^|\/)Makefile$/),
177
+ getPreCommitContent(ctx),
178
+ getMainGoFiles(ctx).slice(0, 25).map(file => ctx.fileContent(file) || '').filter(Boolean).join('\n'),
179
+ ].filter(Boolean).join('\n');
180
+ return ctx.__nerviqGoProjectText;
181
+ }
182
+
183
+ function getGoInterfaceBlocks(ctx) {
184
+ if (ctx.__nerviqGoInterfaces) return ctx.__nerviqGoInterfaces;
185
+ const blocks = [];
186
+ for (const file of getMainGoFiles(ctx)) {
187
+ const content = ctx.fileContent(file) || '';
188
+ for (const match of content.matchAll(/type\s+\w+\s+interface\s*\{([\s\S]*?)\}/g)) {
189
+ blocks.push(match[1]);
190
+ }
191
+ }
192
+ ctx.__nerviqGoInterfaces = blocks;
193
+ return ctx.__nerviqGoInterfaces;
194
+ }
195
+
196
+ function countGoInterfaceMethods(block) {
197
+ return block
198
+ .split(/\r?\n/)
199
+ .map(line => line.trim())
200
+ .filter(line => line && !line.startsWith('//') && !line.startsWith('/*'))
201
+ .length;
202
+ }
203
+
13
204
  const { containsEmbeddedSecret } = require('./secret-patterns');
14
205
  const { attachSourceUrls } = require('./source-urls');
15
206
  const { buildSupplementalChecks } = require('./supplemental-checks');
@@ -2914,15 +3105,9 @@ const TECHNIQUES = {
2914
3105
  id: 110003,
2915
3106
  name: 'Hook scripts handle exit codes correctly',
2916
3107
  check: (ctx) => {
2917
- const hooksDir = ctx.dirFiles('.claude/hooks');
2918
- if (!hooksDir || hooksDir.length === 0) return null;
2919
- const hookFiles = hooksDir.filter(f => /\.(js|sh|py)$/.test(f));
2920
- if (hookFiles.length === 0) return null;
2921
- const hasExitHandling = hookFiles.some(f => {
2922
- const content = ctx.fileContent('.claude/hooks/' + f) || '';
2923
- return /process\.exit|exit\s+[012]|sys\.exit|return\s+[012]/i.test(content);
2924
- });
2925
- return hasExitHandling;
3108
+ const hookContents = getClaudeHookContents(ctx);
3109
+ if (hookContents.length === 0) return null;
3110
+ return hookContents.some(content => /process\.exit|exit\s+[012]|sys\.exit|return\s+[012]/i.test(content));
2926
3111
  },
2927
3112
  impact: 'low', rating: 3, category: 'governance',
2928
3113
  fix: 'Hooks should use explicit exit codes: 0=success, 1=warning, 2=block. See Claude Code docs.',
@@ -2930,6 +3115,71 @@ const TECHNIQUES = {
2930
3115
  confidence: 0.7,
2931
3116
  },
2932
3117
 
3118
+ loopSafetyBoundaries: {
3119
+ id: 110004,
3120
+ name: 'Loop safety boundaries configured',
3121
+ check: (ctx) => {
3122
+ const md = ctx.claudeMdContent() || '';
3123
+ const settings = ctx.fileContent('.claude/settings.json') || '';
3124
+ const hookContents = getClaudeHookContents(ctx).join('\n');
3125
+ const loopSafetyConfig = [md, settings, hookContents].filter(Boolean).join('\n');
3126
+
3127
+ return /max[-_ ]?turns|maxTurns|max[-_ ]?tokens|maxTokens|loop(?:[-_ ]?(?:limit|limits|safety|guard|budget|boundary|boundaries))|iteration(?:[-_ ]?(?:limit|limits|guard|budget|cap|caps|count|max(?:imum)?))/i.test(loopSafetyConfig);
3128
+ },
3129
+ impact: 'medium', rating: 4, category: 'governance',
3130
+ fix: 'Document loop safety limits such as maxTurns, maxTokens, or iteration caps in CLAUDE.md, settings, or hook guards.',
3131
+ template: null,
3132
+ confidence: 0.8,
3133
+ },
3134
+
3135
+ consistencyPassAtK: {
3136
+ id: 110005,
3137
+ name: 'Consistency/pass@k evaluation mentioned',
3138
+ check: (ctx) => {
3139
+ const md = ctx.claudeMdContent() || '';
3140
+ const configPaths = [
3141
+ 'package.json',
3142
+ 'jest.config.js',
3143
+ 'jest.config.cjs',
3144
+ 'jest.config.mjs',
3145
+ 'vitest.config.js',
3146
+ 'vitest.config.ts',
3147
+ 'playwright.config.js',
3148
+ 'playwright.config.ts',
3149
+ 'pytest.ini',
3150
+ 'pyproject.toml',
3151
+ 'tox.ini',
3152
+ '.github/workflows/ci.yml',
3153
+ '.github/workflows/ci.yaml',
3154
+ '.github/workflows/test.yml',
3155
+ '.github/workflows/test.yaml',
3156
+ ];
3157
+ const configContent = configPaths
3158
+ .map(file => ctx.fileContent(file) || '')
3159
+ .filter(Boolean)
3160
+ .join('\n');
3161
+
3162
+ return /pass@k|consistency|multiple runs?|reproducib/i.test(`${md}\n${configContent}`);
3163
+ },
3164
+ impact: 'low', rating: 3, category: 'quality',
3165
+ fix: 'Mention pass@k or consistency testing in CLAUDE.md or test configuration so repeated-run quality evaluation is explicit.',
3166
+ template: null,
3167
+ confidence: 0.7,
3168
+ },
3169
+
3170
+ instinctToSkillProgression: {
3171
+ id: 110006,
3172
+ name: 'Instinct-to-skill progression documented',
3173
+ check: (ctx) => {
3174
+ const md = ctx.claudeMdContent() || '';
3175
+ return /progressive learning|instinct[- ]to[- ]skill|instinct.{0,40}skill|skill.{0,40}instinct|graduated|phased approach/i.test(md);
3176
+ },
3177
+ impact: 'low', rating: 3, category: 'features',
3178
+ fix: 'Document a progressive learning path that turns repeated instincts into reusable skills or phased practices.',
3179
+ template: null,
3180
+ confidence: 0.7,
3181
+ },
3182
+
2933
3183
 
2934
3184
  };
2935
3185