@nerviq/cli 1.0.1 → 1.2.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.
Files changed (39) hide show
  1. package/bin/cli.js +170 -73
  2. package/package.json +1 -1
  3. package/src/activity.js +20 -0
  4. package/src/aider/domain-packs.js +27 -2
  5. package/src/aider/mcp-packs.js +231 -0
  6. package/src/aider/techniques.js +3211 -1397
  7. package/src/audit.js +257 -2
  8. package/src/catalog.js +18 -2
  9. package/src/codex/domain-packs.js +23 -1
  10. package/src/codex/mcp-packs.js +254 -0
  11. package/src/codex/techniques.js +4738 -3257
  12. package/src/copilot/domain-packs.js +23 -1
  13. package/src/copilot/mcp-packs.js +254 -0
  14. package/src/copilot/techniques.js +3433 -1936
  15. package/src/cursor/domain-packs.js +23 -1
  16. package/src/cursor/mcp-packs.js +257 -0
  17. package/src/cursor/techniques.js +3698 -1869
  18. package/src/deprecation.js +98 -0
  19. package/src/domain-pack-expansion.js +571 -0
  20. package/src/domain-packs.js +25 -2
  21. package/src/formatters/otel.js +151 -0
  22. package/src/gemini/domain-packs.js +23 -1
  23. package/src/gemini/mcp-packs.js +257 -0
  24. package/src/gemini/techniques.js +3734 -2238
  25. package/src/integrations.js +194 -0
  26. package/src/mcp-packs.js +233 -0
  27. package/src/opencode/domain-packs.js +23 -1
  28. package/src/opencode/mcp-packs.js +231 -0
  29. package/src/opencode/techniques.js +3501 -1687
  30. package/src/org.js +68 -0
  31. package/src/source-urls.js +410 -260
  32. package/src/stack-checks.js +565 -0
  33. package/src/supplemental-checks.js +817 -0
  34. package/src/techniques.js +2929 -1449
  35. package/src/telemetry.js +160 -0
  36. package/src/windsurf/domain-packs.js +23 -1
  37. package/src/windsurf/mcp-packs.js +257 -0
  38. package/src/windsurf/techniques.js +3648 -1834
  39. package/src/workspace.js +233 -0
package/bin/cli.js CHANGED
@@ -9,6 +9,8 @@ const { runBenchmark, printBenchmark, writeBenchmarkReport } = require('../src/b
9
9
  const { writeSnapshotArtifact, recordRecommendationOutcome, formatRecommendationOutcomeSummary, getRecommendationOutcomeSummary } = require('../src/activity');
10
10
  const { collectFeedback } = require('../src/feedback');
11
11
  const { startServer } = require('../src/server');
12
+ const { auditWorkspaces } = require('../src/workspace');
13
+ const { scanOrg } = require('../src/org');
12
14
  const { version } = require('../package.json');
13
15
 
14
16
  const args = process.argv.slice(2);
@@ -22,7 +24,7 @@ const COMMAND_ALIASES = {
22
24
  gov: 'governance',
23
25
  outcome: 'feedback',
24
26
  };
25
- const KNOWN_COMMANDS = ['audit', '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', 'help', 'version'];
26
28
 
27
29
  function levenshtein(a, b) {
28
30
  const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
@@ -74,17 +76,20 @@ function parseArgs(rawArgs) {
74
76
  let platform = 'claude';
75
77
  let format = null;
76
78
  let port = null;
79
+ let workspace = null;
80
+ let webhookUrl = null;
77
81
  let commandSet = false;
78
82
  let extraArgs = [];
79
83
  let convertFrom = null;
80
84
  let convertTo = null;
81
85
  let migrateFrom = null;
82
86
  let migrateTo = null;
87
+ let checkVersion = null;
83
88
 
84
89
  for (let i = 0; i < rawArgs.length; i++) {
85
90
  const arg = rawArgs[i];
86
91
 
87
- if (arg === '--threshold' || arg === '--out' || arg === '--plan' || arg === '--only' || arg === '--profile' || arg === '--mcp-pack' || arg === '--require' || arg === '--key' || arg === '--status' || arg === '--effect' || arg === '--notes' || arg === '--source' || arg === '--score-delta' || arg === '--platform' || arg === '--format' || arg === '--from' || arg === '--to' || arg === '--port') {
92
+ if (arg === '--threshold' || arg === '--out' || arg === '--plan' || arg === '--only' || arg === '--profile' || arg === '--mcp-pack' || arg === '--require' || arg === '--key' || arg === '--status' || arg === '--effect' || arg === '--notes' || arg === '--source' || arg === '--score-delta' || arg === '--platform' || arg === '--format' || arg === '--from' || arg === '--to' || arg === '--port' || arg === '--workspace' || arg === '--check-version' || arg === '--webhook') {
88
93
  const value = rawArgs[i + 1];
89
94
  if (!value || value.startsWith('--')) {
90
95
  throw new Error(`${arg} requires a value`);
@@ -107,6 +112,9 @@ function parseArgs(rawArgs) {
107
112
  if (arg === '--from') { convertFrom = value.trim(); migrateFrom = value.trim(); }
108
113
  if (arg === '--to') { convertTo = value.trim(); migrateTo = value.trim(); }
109
114
  if (arg === '--port') port = value.trim();
115
+ if (arg === '--workspace') workspace = value.trim();
116
+ if (arg === '--check-version') checkVersion = value.trim();
117
+ if (arg === '--webhook') webhookUrl = value.trim();
110
118
  i++;
111
119
  continue;
112
120
  }
@@ -191,6 +199,16 @@ function parseArgs(rawArgs) {
191
199
  continue;
192
200
  }
193
201
 
202
+ if (arg.startsWith('--workspace=')) {
203
+ workspace = arg.split('=').slice(1).join('=').trim();
204
+ continue;
205
+ }
206
+
207
+ if (arg.startsWith('--check-version=')) {
208
+ checkVersion = arg.split('=').slice(1).join('=').trim();
209
+ continue;
210
+ }
211
+
194
212
  if (arg.startsWith('--')) {
195
213
  flags.push(arg);
196
214
  continue;
@@ -206,7 +224,54 @@ function parseArgs(rawArgs) {
206
224
 
207
225
  const normalizedCommand = COMMAND_ALIASES[command] || command;
208
226
 
209
- return { flags, command, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks, feedbackKey, feedbackStatus, feedbackEffect, feedbackNotes, feedbackSource, feedbackScoreDelta, platform, format, port, extraArgs, convertFrom, convertTo, migrateFrom, migrateTo };
227
+ return { flags, command, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks, feedbackKey, feedbackStatus, feedbackEffect, feedbackNotes, feedbackSource, feedbackScoreDelta, platform, format, port, workspace, extraArgs, convertFrom, convertTo, migrateFrom, migrateTo, checkVersion, webhookUrl };
228
+ }
229
+
230
+ function printWorkspaceSummary(summary, options) {
231
+ if (options.json) {
232
+ console.log(JSON.stringify(summary, null, 2));
233
+ return;
234
+ }
235
+
236
+ console.log('');
237
+ console.log('\x1b[1m nerviq workspace audit\x1b[0m');
238
+ console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
239
+ console.log(` Root: ${summary.rootDir}`);
240
+ console.log(` Platform: ${summary.platform}`);
241
+ console.log(` Workspaces: ${summary.workspaceCount}`);
242
+ console.log(` Average score: \x1b[1m${summary.averageScore}/100\x1b[0m`);
243
+ console.log('');
244
+ console.log('\x1b[1m Workspace Score Pass Total Top action\x1b[0m');
245
+ console.log(' ' + '─'.repeat(72));
246
+ for (const item of summary.workspaces) {
247
+ const score = item.score === null ? 'ERR' : String(item.score);
248
+ const topAction = item.error || item.topAction || '-';
249
+ console.log(` ${item.workspace.padEnd(26)} ${score.padStart(5)} ${String(item.passed).padStart(5)} ${String(item.total).padStart(6)} ${topAction}`);
250
+ }
251
+ console.log('');
252
+ }
253
+
254
+ function printOrgSummary(summary, options) {
255
+ if (options.json) {
256
+ console.log(JSON.stringify(summary, null, 2));
257
+ return;
258
+ }
259
+
260
+ console.log('');
261
+ console.log('\x1b[1m nerviq org scan\x1b[0m');
262
+ console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
263
+ console.log(` Platform: ${summary.platform}`);
264
+ console.log(` Repos: ${summary.repoCount}`);
265
+ console.log(` Average score: \x1b[1m${summary.averageScore}/100\x1b[0m`);
266
+ console.log('');
267
+ console.log('\x1b[1m Repo Platform Score Top action\x1b[0m');
268
+ console.log(' ' + '─'.repeat(72));
269
+ for (const item of summary.repos) {
270
+ const score = item.score === null ? 'ERR' : String(item.score);
271
+ const topAction = item.error || item.topAction || '-';
272
+ console.log(` ${item.name.padEnd(18)} ${item.platform.padEnd(8)} ${score.padStart(5)} ${topAction}`);
273
+ }
274
+ console.log('');
210
275
  }
211
276
 
212
277
  const HELP = `
@@ -219,7 +284,9 @@ const HELP = `
219
284
  nerviq audit --platform X Audit specific platform (claude|codex|cursor|copilot|gemini|windsurf|aider|opencode)
220
285
  nerviq audit --lite Quick scan: top 3 gaps + next command
221
286
  nerviq audit --json Machine-readable JSON output (for CI)
287
+ nerviq audit --workspace packages/* Audit each workspace in a monorepo
222
288
  nerviq scan dir1 dir2 Compare multiple repos side-by-side
289
+ nerviq org scan dir1 dir2 Aggregate multiple repos into one score table
223
290
  nerviq catalog Full check catalog (all 8 platforms)
224
291
  nerviq catalog --json Export full check catalog as JSON
225
292
 
@@ -272,8 +339,11 @@ const HELP = `
272
339
  --only A,B Limit plan/apply to selected proposal IDs
273
340
  --profile NAME Permission profile: read-only | suggest-only | safe-write | power-user
274
341
  --mcp-pack A,B Merge MCP packs into setup (e.g. context7-docs,next-devtools)
275
- --format NAME Output format: json | sarif
342
+ --check-version V Pin catalog to a specific version (warn on mismatch)
343
+ --format NAME Output format: json | sarif | otel
344
+ --webhook URL Send audit results to a webhook (Slack/Discord/generic JSON)
276
345
  --port N Port for \`serve\` (default: 3000)
346
+ --workspace GLOBS Audit workspaces separately (e.g. packages/* or apps/web,apps/api)
277
347
  --snapshot Save snapshot artifact under .claude/nerviq/snapshots/
278
348
  --lite Short top-3 scan with one clear next step
279
349
  --dry-run Preview changes without writing files
@@ -291,7 +361,9 @@ const HELP = `
291
361
  npx nerviq
292
362
  npx nerviq --lite
293
363
  npx nerviq --platform cursor
364
+ npx nerviq audit --workspace packages/*
294
365
  npx nerviq --platform codex augment
366
+ npx nerviq org scan ./app ./api ./infra
295
367
  npx nerviq scan ./app ./api ./infra
296
368
  npx nerviq harmony-audit
297
369
  npx nerviq convert --from claude --to codex
@@ -347,16 +419,31 @@ async function main() {
347
419
  platform: parsed.platform || 'claude',
348
420
  format: parsed.format || null,
349
421
  port: parsed.port !== null ? Number(parsed.port) : null,
422
+ workspace: parsed.workspace || null,
423
+ webhookUrl: parsed.webhookUrl || null,
350
424
  dir: process.cwd()
351
425
  };
352
426
 
353
- if (!['claude', 'codex'].includes(options.platform)) {
354
- console.error(`\n Error: Unsupported platform '${options.platform}'. Use 'claude' or 'codex'.\n`);
427
+ if (parsed.checkVersion) {
428
+ if (parsed.checkVersion !== version) {
429
+ console.error(`\n Warning: --check-version ${parsed.checkVersion} does not match installed nerviq version ${version}.`);
430
+ console.error(` Check catalog may differ between versions. To align, run: npm install @nerviq/cli@${parsed.checkVersion}`);
431
+ console.error('');
432
+ }
433
+ options.checkVersion = parsed.checkVersion;
434
+ }
435
+
436
+ const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'copilot', 'cursor', 'windsurf', 'aider', 'opencode'];
437
+ if (!SUPPORTED_PLATFORMS.includes(options.platform)) {
438
+ console.error(`\n Error: Unsupported platform '${options.platform}'.`);
439
+ console.error(` Why: Only the following platforms are supported: ${SUPPORTED_PLATFORMS.join(', ')}.`);
440
+ console.error(` Fix: Use --platform with one of the supported values, e.g.: npx nerviq --platform claude`);
441
+ console.error(' Docs: https://github.com/nerviq/nerviq#cross-platform\n');
355
442
  process.exit(1);
356
443
  }
357
444
 
358
- if (options.format !== null && !['json', 'sarif'].includes(options.format)) {
359
- console.error(`\n Error: Unsupported format '${options.format}'. Use 'json' or 'sarif'.\n`);
445
+ if (options.format !== null && !['json', 'sarif', 'otel'].includes(options.format)) {
446
+ console.error(`\n Error: Unsupported format '${options.format}'. Use 'json', 'sarif', or 'otel'.\n`);
360
447
  process.exit(1);
361
448
  }
362
449
 
@@ -366,7 +453,10 @@ async function main() {
366
453
  }
367
454
 
368
455
  if (options.threshold !== null && (!Number.isFinite(options.threshold) || options.threshold < 0 || options.threshold > 100)) {
369
- console.error('\n Error: --threshold must be a number between 0 and 100.\n');
456
+ console.error(`\n Error: Invalid threshold value '${parsed.threshold}'.`);
457
+ console.error(' Why: --threshold must be a number between 0 and 100 representing the minimum passing score.');
458
+ console.error(' Fix: Use a valid number, e.g.: npx nerviq --threshold 70');
459
+ console.error(' Docs: https://github.com/nerviq/nerviq#ci-integration\n');
370
460
  process.exit(1);
371
461
  }
372
462
 
@@ -377,16 +467,21 @@ async function main() {
377
467
  if (!KNOWN_COMMANDS.includes(normalizedCommand)) {
378
468
  const suggestion = suggestCommand(command);
379
469
  console.error(`\n Error: Unknown command '${command}'.`);
470
+ console.error(` Why: '${command}' is not a recognized nerviq command or alias.`);
380
471
  if (suggestion) {
381
- console.error(` Did you mean '${suggestion}'?`);
472
+ console.error(` Fix: Did you mean '${suggestion}'? Run: npx nerviq ${suggestion}`);
473
+ } else {
474
+ console.error(' Fix: Run nerviq --help to see all available commands.');
382
475
  }
383
- console.error(' Run nerviq --help for usage.\n');
476
+ console.error(' Docs: https://github.com/nerviq/nerviq#readme\n');
384
477
  process.exit(1);
385
478
  }
386
479
 
387
480
  if (!require('fs').existsSync(options.dir)) {
388
481
  console.error(`\n Error: Directory not found: ${options.dir}`);
389
- console.error(' Run nerviq from inside your project directory.\n');
482
+ console.error(' Why: The current working directory does not exist or is not accessible.');
483
+ console.error(' Fix: cd into your project directory first, then run nerviq.');
484
+ console.error(' Docs: https://github.com/nerviq/nerviq#getting-started\n');
390
485
  process.exit(1);
391
486
  }
392
487
 
@@ -401,7 +496,7 @@ async function main() {
401
496
 
402
497
  try {
403
498
  const FULL_COMMAND_SET = new Set([
404
- 'audit', 'scan', 'badge', 'augment', 'suggest-only', 'setup', 'plan', 'apply',
499
+ 'audit', 'org', 'scan', 'badge', 'augment', 'suggest-only', 'setup', 'plan', 'apply',
405
500
  'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'insights',
406
501
  'history', 'compare', 'trend', 'feedback', 'catalog', 'certify', 'serve', 'help', 'version',
407
502
  // Harmony + Synergy (cross-platform)
@@ -458,64 +553,24 @@ async function main() {
458
553
  console.error(' Usage: npx nerviq scan dir1 dir2 dir3\n');
459
554
  process.exit(1);
460
555
  }
461
- const fs = require('fs');
462
- const pathMod = require('path');
463
- const rows = [];
464
- for (const rawDir of scanDirs) {
465
- const dir = pathMod.resolve(rawDir);
466
- if (!fs.existsSync(dir)) {
467
- rows.push({ name: pathMod.basename(rawDir), dir: rawDir, score: null, passed: '-', failed: '-', suggested: '-', error: 'directory not found' });
468
- continue;
469
- }
470
- try {
471
- const result = await audit({ dir, silent: true, platform: options.platform });
472
- rows.push({
473
- name: pathMod.basename(dir),
474
- dir: rawDir,
475
- score: result.score,
476
- passed: result.passed,
477
- failed: result.failed,
478
- suggested: result.suggestedNextCommand || '-',
479
- error: null,
480
- });
481
- } catch (err) {
482
- rows.push({ name: pathMod.basename(dir), dir: rawDir, score: null, passed: '-', failed: '-', suggested: '-', error: err.message });
483
- }
556
+ const summary = await scanOrg(scanDirs, options.platform);
557
+ printOrgSummary(summary, options);
558
+ if (options.threshold !== null && summary.averageScore < options.threshold) {
559
+ process.exit(1);
484
560
  }
485
-
486
- if (options.json) {
487
- console.log(JSON.stringify(rows, null, 2));
488
- } else {
489
- // Find weakest
490
- const validRows = rows.filter(r => r.score !== null);
491
- const minScore = validRows.length > 0 ? Math.min(...validRows.map(r => r.score)) : null;
492
- const weakest = validRows.length > 1 && validRows.filter(r => r.score > minScore).length > 0
493
- ? validRows.find(r => r.score === minScore)
494
- : null;
495
-
496
- console.log('');
497
- console.log('\x1b[1m nerviq multi-repo scan\x1b[0m');
498
- console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
499
- console.log('');
500
-
501
- // Table header
502
- const nameW = Math.max(8, ...rows.map(r => r.name.length)) + 2;
503
- const header = ` ${'Project'.padEnd(nameW)} ${'Score'.padStart(5)} ${'Pass'.padStart(4)} ${'Fail'.padStart(4)} Suggested Command`;
504
- console.log('\x1b[1m' + header + '\x1b[0m');
505
- console.log(' ' + '─'.repeat(header.trim().length));
506
-
507
- for (const row of rows) {
508
- if (row.error) {
509
- console.log(` ${row.name.padEnd(nameW)} \x1b[31m${('ERR').padStart(5)}\x1b[0m ${String(row.passed).padStart(4)} ${String(row.failed).padStart(4)} ${row.error}`);
510
- continue;
511
- }
512
- const isWeak = weakest && row.name === weakest.name && row.dir === weakest.dir;
513
- const scoreColor = row.score >= 70 ? '\x1b[32m' : row.score >= 40 ? '\x1b[33m' : '\x1b[31m';
514
- const prefix = isWeak ? '\x1b[31m⚠ ' : ' ';
515
- const suffix = isWeak ? ' ← weakest\x1b[0m' : '';
516
- console.log(`${prefix}${row.name.padEnd(nameW)} ${scoreColor}${String(row.score).padStart(5)}\x1b[0m ${String(row.passed).padStart(4)} ${String(row.failed).padStart(4)} ${row.suggested}${suffix}`);
517
- }
518
- console.log('');
561
+ process.exit(0);
562
+ } else if (normalizedCommand === 'org') {
563
+ const subcommand = parsed.extraArgs[0];
564
+ const scanDirs = parsed.extraArgs.slice(1);
565
+ if (subcommand !== 'scan' || scanDirs.length === 0) {
566
+ console.error('\n Error: org requires the scan subcommand and at least one directory.');
567
+ console.error(' Usage: npx nerviq org scan dir1 dir2 dir3\n');
568
+ process.exit(1);
569
+ }
570
+ const summary = await scanOrg(scanDirs, options.platform);
571
+ printOrgSummary(summary, options);
572
+ if (options.threshold !== null && summary.averageScore < options.threshold) {
573
+ process.exit(1);
519
574
  }
520
575
  process.exit(0);
521
576
  } else if (normalizedCommand === 'history') {
@@ -726,7 +781,7 @@ async function main() {
726
781
  const { watch } = require('../src/watch');
727
782
  await watch(options);
728
783
  } else if (normalizedCommand === 'catalog') {
729
- const { generateCatalog, writeCatalogJson } = require('../src/catalog');
784
+ const { generateCatalog, generateCatalogWithVersion, writeCatalogJson } = require('../src/catalog');
730
785
  if (options.out) {
731
786
  const result = writeCatalogJson(options.out);
732
787
  if (options.json) {
@@ -737,7 +792,9 @@ async function main() {
737
792
  } else {
738
793
  const catalog = generateCatalog();
739
794
  if (options.json) {
740
- console.log(JSON.stringify(catalog, null, 2));
795
+ const envelope = generateCatalogWithVersion();
796
+ if (options.checkVersion) envelope.requestedVersion = options.checkVersion;
797
+ console.log(JSON.stringify(envelope, null, 2));
741
798
  } else {
742
799
  // Print summary table
743
800
  const platforms = {};
@@ -845,7 +902,43 @@ async function main() {
845
902
  }
846
903
  }
847
904
  } else {
905
+ if (options.workspace) {
906
+ const summary = await auditWorkspaces(options.dir, options.workspace, options.platform);
907
+ printWorkspaceSummary(summary, options);
908
+ if (options.threshold !== null && summary.averageScore < options.threshold) {
909
+ process.exit(1);
910
+ }
911
+ process.exit(0);
912
+ }
848
913
  const result = await audit(options);
914
+ if (options.webhookUrl) {
915
+ try {
916
+ const { sendWebhook, formatSlackMessage } = require('../src/integrations');
917
+ // Auto-detect Slack vs generic by URL pattern
918
+ const isSlack = options.webhookUrl.includes('hooks.slack.com');
919
+ const isDiscord = options.webhookUrl.includes('discord.com/api/webhooks');
920
+ let payload;
921
+ if (isSlack) {
922
+ payload = formatSlackMessage(result);
923
+ } else if (isDiscord) {
924
+ const { formatDiscordMessage } = require('../src/integrations');
925
+ payload = formatDiscordMessage(result);
926
+ } else {
927
+ // Generic webhook: send full JSON audit result
928
+ payload = { platform: result.platform, score: result.score, passed: result.passed, failed: result.failed, results: result.results };
929
+ }
930
+ const webhookResp = await sendWebhook(options.webhookUrl, payload);
931
+ if (!options.json) {
932
+ if (webhookResp.ok) {
933
+ console.log(` Webhook sent: ${options.webhookUrl} (${webhookResp.status})`);
934
+ } else {
935
+ console.error(` Webhook failed: ${webhookResp.status} — ${webhookResp.body.slice(0, 200)}`);
936
+ }
937
+ }
938
+ } catch (webhookErr) {
939
+ if (!options.json) console.error(` Webhook error: ${webhookErr.message}`);
940
+ }
941
+ }
849
942
  if (options.feedback && !options.json && options.format === null) {
850
943
  const feedbackTargets = options.lite
851
944
  ? (result.liteSummary?.topNextActions || [])
@@ -875,7 +968,10 @@ async function main() {
875
968
  }
876
969
  if (options.threshold !== null && result.score < options.threshold) {
877
970
  if (!options.json) {
878
- console.error(` Threshold failed: score ${result.score}/100 is below required ${options.threshold}/100.\n`);
971
+ console.error(`\n Error: Threshold not met — score ${result.score}/100 is below required ${options.threshold}/100.`);
972
+ console.error(' Why: Your project audit score is lower than the minimum threshold set via --threshold.');
973
+ console.error(' Fix: Run `npx nerviq augment` to see improvement suggestions, then re-audit.');
974
+ console.error(' Docs: https://github.com/nerviq/nerviq#ci-integration\n');
879
975
  }
880
976
  process.exit(1);
881
977
  }
@@ -895,6 +991,7 @@ async function main() {
895
991
  }
896
992
  } catch (err) {
897
993
  console.error(`\n Error: ${err.message}`);
994
+ console.error(' Fix: Run `npx nerviq doctor` to diagnose common issues, or check https://github.com/nerviq/nerviq#troubleshooting');
898
995
  process.exit(1);
899
996
  }
900
997
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nerviq/cli",
3
- "version": "1.0.1",
3
+ "version": "1.2.1",
4
4
  "description": "The intelligent nervous system for AI coding agents — 673 checks across 8 platforms. Audit, align, and amplify.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/activity.js CHANGED
@@ -1,7 +1,23 @@
1
1
  const fs = require('fs');
2
+ const os = require('os');
2
3
  const path = require('path');
3
4
  const { version } = require('../package.json');
4
5
 
6
+ /**
7
+ * Generate a machine-level user identity for audit tracking.
8
+ * Format: hostname:username — no PII, just machine identity.
9
+ * @returns {string}
10
+ */
11
+ function getUserId() {
12
+ try {
13
+ const hostname = os.hostname();
14
+ const username = os.userInfo().username;
15
+ return `${hostname}:${username}`;
16
+ } catch {
17
+ return 'unknown:unknown';
18
+ }
19
+ }
20
+
5
21
  let _lastTimestamp = '';
6
22
  let _counter = 0;
7
23
 
@@ -42,6 +58,7 @@ function writeActivityArtifact(dir, type, payload) {
42
58
  id,
43
59
  type,
44
60
  createdAt: new Date().toISOString(),
61
+ userId: getUserId(),
45
62
  ...payload,
46
63
  });
47
64
  return {
@@ -58,6 +75,7 @@ function writeRollbackArtifact(dir, payload) {
58
75
  writeJson(filePath, {
59
76
  id,
60
77
  createdAt: new Date().toISOString(),
78
+ userId: getUserId(),
61
79
  rollbackType: 'delete-created-files',
62
80
  ...payload,
63
81
  });
@@ -161,6 +179,7 @@ function writeSnapshotArtifact(dir, snapshotKind, payload, meta = {}) {
161
179
  snapshotKind,
162
180
  id,
163
181
  createdAt: new Date().toISOString(),
182
+ userId: getUserId(),
164
183
  generatedBy: `nerviq@${version}`,
165
184
  directory: dir,
166
185
  summary,
@@ -511,6 +530,7 @@ function formatRecommendationOutcomeSummary(dir) {
511
530
  }
512
531
 
513
532
  module.exports = {
533
+ getUserId,
514
534
  ensureArtifactDirs,
515
535
  writeActivityArtifact,
516
536
  writeRollbackArtifact,
@@ -7,7 +7,9 @@
7
7
  * - Git is the only safety mechanism
8
8
  */
9
9
 
10
- const AIDER_DOMAIN_PACKS = [
10
+ const { buildAdditionalDomainPacks, detectAdditionalDomainPacks } = require('../domain-pack-expansion');
11
+
12
+ const BASE_AIDER_DOMAIN_PACKS = [
11
13
  {
12
14
  key: 'baseline-general',
13
15
  label: 'Baseline General',
@@ -170,6 +172,13 @@ const AIDER_DOMAIN_PACKS = [
170
172
  },
171
173
  ];
172
174
 
175
+ const AIDER_DOMAIN_PACKS = [
176
+ ...BASE_AIDER_DOMAIN_PACKS,
177
+ ...buildAdditionalDomainPacks('aider', {
178
+ existingKeys: new Set(BASE_AIDER_DOMAIN_PACKS.map((pack) => pack.key)),
179
+ }),
180
+ ];
181
+
173
182
  function uniqueByKey(matches) {
174
183
  const seen = new Set();
175
184
  return matches.filter(m => {
@@ -181,9 +190,10 @@ function uniqueByKey(matches) {
181
190
 
182
191
  function detectAiderDomainPacks(ctx) {
183
192
  const matches = [];
193
+ const pkg = typeof ctx.jsonFile === 'function' ? (ctx.jsonFile('package.json') || {}) : {};
184
194
  const deps = ctx.allDependencies ? ctx.allDependencies() : {};
185
195
  const files = ctx.files || [];
186
- const filenames = files.join('\n');
196
+ const stackKeys = new Set();
187
197
 
188
198
  function addMatch(key, reasons) {
189
199
  const pack = AIDER_DOMAIN_PACKS.find(p => p.key === key);
@@ -263,6 +273,21 @@ function detectAiderDomainPacks(ctx) {
263
273
  addMatch('security-focused', ['Detected security-focused repo signals.']);
264
274
  }
265
275
 
276
+ const hasCi = Boolean(ctx.fileContent('.github/workflows')) || files.some((file) => /^\.github\/workflows\//.test(file));
277
+
278
+ detectAdditionalDomainPacks({
279
+ ctx,
280
+ pkg,
281
+ deps,
282
+ stackKeys,
283
+ addMatch,
284
+ hasBackend: isBackend,
285
+ hasFrontend: isFrontend,
286
+ hasInfra: isInfra,
287
+ hasCi,
288
+ isEnterpriseGoverned: hasPolicyFiles,
289
+ });
290
+
266
291
  if (matches.length === 0) {
267
292
  addMatch('baseline-general', [
268
293
  'No stronger domain signal detected — safe general Aider baseline is the best starting point.',