@nerviq/cli 1.11.0 → 1.12.0

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 (49) hide show
  1. package/README.md +97 -19
  2. package/bin/cli.js +618 -182
  3. package/package.json +2 -2
  4. package/src/activity.js +49 -9
  5. package/src/adoption-advisor.js +299 -0
  6. package/src/aider/techniques.js +16 -11
  7. package/src/analyze.js +128 -0
  8. package/src/anti-patterns.js +13 -0
  9. package/src/audit.js +97 -22
  10. package/src/behavioral-drift.js +801 -0
  11. package/src/continuous-ops.js +681 -0
  12. package/src/cost-tracking.js +61 -0
  13. package/src/cursor/techniques.js +17 -12
  14. package/src/deep-review.js +83 -0
  15. package/src/diff-only.js +280 -0
  16. package/src/doctor.js +118 -55
  17. package/src/governance.js +59 -43
  18. package/src/hook-validation.js +342 -0
  19. package/src/index.js +5 -0
  20. package/src/integrations.js +42 -5
  21. package/src/mcp-validation.js +337 -0
  22. package/src/opencode/techniques.js +12 -7
  23. package/src/operating-profile.js +574 -0
  24. package/src/org.js +97 -13
  25. package/src/plans.js +192 -8
  26. package/src/platform-change-manifest.js +86 -0
  27. package/src/policy-layers.js +210 -0
  28. package/src/profiles.js +4 -1
  29. package/src/prompt-injection.js +74 -0
  30. package/src/repo-archetype.js +386 -0
  31. package/src/setup.js +34 -0
  32. package/src/source-urls.js +132 -132
  33. package/src/supplemental-checks.js +13 -12
  34. package/src/techniques/api.js +407 -0
  35. package/src/techniques/automation.js +316 -0
  36. package/src/techniques/compliance.js +257 -0
  37. package/src/techniques/hygiene.js +294 -0
  38. package/src/techniques/instructions.js +243 -0
  39. package/src/techniques/observability.js +226 -0
  40. package/src/techniques/optimization.js +142 -0
  41. package/src/techniques/quality.js +317 -0
  42. package/src/techniques/security.js +237 -0
  43. package/src/techniques/shared.js +443 -0
  44. package/src/techniques/stacks.js +2294 -0
  45. package/src/techniques/tools.js +106 -0
  46. package/src/techniques/workflow.js +413 -0
  47. package/src/techniques.js +78 -5607
  48. package/src/watch.js +18 -0
  49. package/src/windsurf/techniques.js +17 -12
package/bin/cli.js CHANGED
@@ -14,8 +14,9 @@ const { auditWorkspaces } = require('../src/workspace');
14
14
  const { scanOrg } = require('../src/org');
15
15
  const { detectAntiPatterns, printAntiPatterns, printAntiPatternCatalog } = require('../src/anti-patterns');
16
16
  const { VERIFICATION_DATES, getVerificationDate, getVerificationStats } = require('../src/verification-metadata');
17
- const { init: initI18n, t } = require('../src/i18n');
18
- const { version } = require('../package.json');
17
+ const { init: initI18n, t } = require('../src/i18n');
18
+ const { version } = require('../package.json');
19
+ const { SNAPSHOT_MILESTONES } = require('../src/activity');
19
20
 
20
21
  const args = process.argv.slice(2);
21
22
  const COMMAND_ALIASES = {
@@ -28,7 +29,7 @@ const COMMAND_ALIASES = {
28
29
  gov: 'governance',
29
30
  outcome: 'feedback',
30
31
  };
31
- const KNOWN_COMMANDS = ['audit', 'org', 'setup', 'init', 'augment', 'suggest-only', 'plan', 'apply', 'fix', 'rollback', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'doctor', 'convert', 'migrate', 'catalog', 'certify', 'serve', 'check-health', 'dashboard', 'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise', 'harmony-watch', 'harmony-governance', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export', 'freshness', 'suggest-rules', 'profile', 'help', 'version'];
32
+ const KNOWN_COMMANDS = ['audit', 'org', 'setup', 'init', 'augment', 'suggest-only', 'plan', 'apply', 'fix', 'rollback', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'doctor', 'convert', 'migrate', 'catalog', 'certify', 'serve', 'check-health', 'dashboard', 'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise', 'harmony-watch', 'harmony-governance', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export', 'freshness', 'suggest-rules', 'profile', 'baseline', 'exception', 'help', 'version'];
32
33
 
33
34
  function levenshtein(a, b) {
34
35
  const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
@@ -97,16 +98,27 @@ function parseArgs(rawArgs) {
97
98
  let feedbackEffect = null;
98
99
  let feedbackNotes = null;
99
100
  let feedbackSource = null;
100
- let feedbackScoreDelta = null;
101
- let platform = 'claude';
102
- let format = null;
101
+ let feedbackScoreDelta = null;
102
+ let platform = 'claude';
103
+ let platformExplicit = false;
104
+ let format = null;
103
105
  let port = null;
104
106
  let workspace = null;
105
107
  let webhookUrl = null;
106
108
  let webhookHeaders = [];
107
109
  let webhookRetries = null;
108
110
  let snapshotTags = [];
109
- let commandSet = false;
111
+ let snapshotMilestone = null;
112
+ let campaigns = [];
113
+ let diffBase = null;
114
+ let diffHead = null;
115
+ let driftMode = null;
116
+ let exceptionOwner = null;
117
+ let exceptionReason = null;
118
+ let exceptionExpires = null;
119
+ let exceptionScope = null;
120
+ let exceptionClass = null;
121
+ let commandSet = false;
110
122
  let extraArgs = [];
111
123
  let convertFrom = null;
112
124
  let convertTo = null;
@@ -122,7 +134,7 @@ function parseArgs(rawArgs) {
122
134
  for (let i = 0; i < rawArgs.length; i++) {
123
135
  const arg = rawArgs[i];
124
136
 
125
- 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' || arg === '--webhook-header' || arg === '--webhook-retries' || arg === '--external' || arg === '--team-profile' || arg === '--lang' || arg === '--tag') {
137
+ 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' || arg === '--webhook-header' || arg === '--webhook-retries' || arg === '--external' || arg === '--team-profile' || arg === '--lang' || arg === '--tag' || arg === '--milestone' || arg === '--campaign' || arg === '--diff-base' || arg === '--diff-head' || arg === '--drift-mode' || arg === '--owner' || arg === '--reason' || arg === '--expires' || arg === '--scope' || arg === '--class') {
126
138
  const value = rawArgs[i + 1];
127
139
  if (!value || value.startsWith('--')) {
128
140
  throw new Error(`${arg} requires a value`);
@@ -140,7 +152,7 @@ function parseArgs(rawArgs) {
140
152
  if (arg === '--notes') feedbackNotes = value;
141
153
  if (arg === '--source') feedbackSource = value.trim();
142
154
  if (arg === '--score-delta') feedbackScoreDelta = value.trim();
143
- if (arg === '--platform') platform = value.trim().toLowerCase();
155
+ if (arg === '--platform') { platform = value.trim().toLowerCase(); platformExplicit = true; }
144
156
  if (arg === '--format') format = value.trim().toLowerCase();
145
157
  if (arg === '--from') { convertFrom = value.trim(); migrateFrom = value.trim(); }
146
158
  if (arg === '--to') { convertTo = value.trim(); migrateTo = value.trim(); }
@@ -154,6 +166,16 @@ function parseArgs(rawArgs) {
154
166
  if (arg === '--team-profile') teamProfile = value.trim();
155
167
  if (arg === '--lang') lang = value.trim().toLowerCase();
156
168
  if (arg === '--tag') snapshotTags.push(value.trim());
169
+ if (arg === '--milestone') snapshotMilestone = value.trim().toLowerCase();
170
+ if (arg === '--campaign') campaigns = value.split(',').map(item => item.trim()).filter(Boolean);
171
+ if (arg === '--diff-base') diffBase = value.trim();
172
+ if (arg === '--diff-head') diffHead = value.trim();
173
+ if (arg === '--drift-mode') driftMode = value.trim().toLowerCase();
174
+ if (arg === '--owner') exceptionOwner = value.trim();
175
+ if (arg === '--reason') exceptionReason = value;
176
+ if (arg === '--expires') exceptionExpires = value.trim();
177
+ if (arg === '--scope') exceptionScope = value.trim().toLowerCase();
178
+ if (arg === '--class') exceptionClass = value.trim().toLowerCase();
157
179
  i++;
158
180
  continue;
159
181
  }
@@ -177,6 +199,56 @@ function parseArgs(rawArgs) {
177
199
  snapshotTags.push(arg.split('=').slice(1).join('=').trim());
178
200
  continue;
179
201
  }
202
+
203
+ if (arg.startsWith('--milestone=')) {
204
+ snapshotMilestone = arg.split('=').slice(1).join('=').trim().toLowerCase();
205
+ continue;
206
+ }
207
+
208
+ if (arg.startsWith('--campaign=')) {
209
+ campaigns = arg.split('=').slice(1).join('=').split(',').map(item => item.trim()).filter(Boolean);
210
+ continue;
211
+ }
212
+
213
+ if (arg.startsWith('--diff-base=')) {
214
+ diffBase = arg.split('=').slice(1).join('=').trim();
215
+ continue;
216
+ }
217
+
218
+ if (arg.startsWith('--diff-head=')) {
219
+ diffHead = arg.split('=').slice(1).join('=').trim();
220
+ continue;
221
+ }
222
+
223
+ if (arg.startsWith('--drift-mode=')) {
224
+ driftMode = arg.split('=').slice(1).join('=').trim().toLowerCase();
225
+ continue;
226
+ }
227
+
228
+ if (arg.startsWith('--owner=')) {
229
+ exceptionOwner = arg.split('=').slice(1).join('=').trim();
230
+ continue;
231
+ }
232
+
233
+ if (arg.startsWith('--reason=')) {
234
+ exceptionReason = arg.split('=').slice(1).join('=');
235
+ continue;
236
+ }
237
+
238
+ if (arg.startsWith('--expires=')) {
239
+ exceptionExpires = arg.split('=').slice(1).join('=').trim();
240
+ continue;
241
+ }
242
+
243
+ if (arg.startsWith('--scope=')) {
244
+ exceptionScope = arg.split('=').slice(1).join('=').trim().toLowerCase();
245
+ continue;
246
+ }
247
+
248
+ if (arg.startsWith('--class=')) {
249
+ exceptionClass = arg.split('=').slice(1).join('=').trim().toLowerCase();
250
+ continue;
251
+ }
180
252
 
181
253
  if (arg === '--repos') {
182
254
  // Collect all following non-flag args as repo paths (supports comma-separated too)
@@ -259,10 +331,11 @@ function parseArgs(rawArgs) {
259
331
  continue;
260
332
  }
261
333
 
262
- if (arg.startsWith('--platform=')) {
263
- platform = arg.split('=').slice(1).join('=').trim().toLowerCase();
264
- continue;
265
- }
334
+ if (arg.startsWith('--platform=')) {
335
+ platform = arg.split('=').slice(1).join('=').trim().toLowerCase();
336
+ platformExplicit = true;
337
+ continue;
338
+ }
266
339
 
267
340
  if (arg.startsWith('--format=')) {
268
341
  format = arg.split('=').slice(1).join('=').trim().toLowerCase();
@@ -315,7 +388,7 @@ function parseArgs(rawArgs) {
315
388
 
316
389
  const normalizedCommand = COMMAND_ALIASES[command] || command;
317
390
 
318
- return { flags, command, commandExplicit, 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, webhookHeaders, webhookRetries, external, repos, teamProfile, lang, snapshotTags };
391
+ return { flags, command, commandExplicit, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks, feedbackKey, feedbackStatus, feedbackEffect, feedbackNotes, feedbackSource, feedbackScoreDelta, platform, platformExplicit, format, port, workspace, extraArgs, convertFrom, convertTo, migrateFrom, migrateTo, checkVersion, webhookUrl, webhookHeaders, webhookRetries, external, repos, teamProfile, lang, snapshotTags, snapshotMilestone, campaigns, diffBase, diffHead, driftMode, exceptionOwner, exceptionReason, exceptionExpires, exceptionScope, exceptionClass };
319
392
  }
320
393
 
321
394
  function printWorkspaceSummary(summary, options) {
@@ -373,16 +446,19 @@ function printCompareCheckSection(title, items, prefix) {
373
446
  }
374
447
 
375
448
  function printScanDetail(summary, options) {
376
- if (options.json) {
377
- console.log(JSON.stringify(summary, null, 2));
378
- return;
449
+ if (options.json) {
450
+ console.log(JSON.stringify(summary, null, 2));
451
+ return;
379
452
  }
380
453
 
381
- console.log('');
382
- console.log('\x1b[1m nerviq scan — per-repo comparison\x1b[0m');
383
- console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
384
- console.log(` Platform: ${summary.platform} | Repos: ${summary.repoCount} | Average: \x1b[1m${summary.averageScore}/100\x1b[0m`);
385
- console.log('');
454
+ console.log('');
455
+ console.log('\x1b[1m nerviq scan — per-repo comparison\x1b[0m');
456
+ console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
457
+ console.log(` Platform: ${summary.platform} | Repos: ${summary.repoCount} | Average: \x1b[1m${summary.averageScore}/100\x1b[0m`);
458
+ if (summary.scoreSemantics?.note) {
459
+ console.log(` Score semantics: ${summary.scoreSemantics.note}`);
460
+ }
461
+ console.log('');
386
462
 
387
463
  for (const item of summary.repos) {
388
464
  if (item.error) {
@@ -391,9 +467,12 @@ function printScanDetail(summary, options) {
391
467
  continue;
392
468
  }
393
469
  const scoreColor = item.score >= 80 ? '\x1b[32m' : item.score >= 50 ? '\x1b[33m' : '\x1b[31m';
394
- console.log(` \x1b[1m${item.name}\x1b[0m ${scoreColor}${item.score}/100\x1b[0m (${item.passed}/${item.total} checks passed)`);
395
-
396
- // Show per-category breakdown if result is available
470
+ console.log(` \x1b[1m${item.name}\x1b[0m ${scoreColor}${item.score}/100\x1b[0m (${item.passed}/${item.total} checks passed)`);
471
+ if (item.policyCoverage?.layerKeys?.length > 0) {
472
+ console.log(` \x1b[2mPolicy layers: ${item.policyCoverage.layerKeys.join(' -> ')}\x1b[0m`);
473
+ }
474
+
475
+ // Show per-category breakdown if result is available
397
476
  if (item.result && item.result.results) {
398
477
  const STACK_LANGUAGES = new Set(['python', 'go', 'rust', 'java', 'ruby', 'dotnet', 'php', 'flutter', 'swift', 'kotlin']);
399
478
  const categories = {};
@@ -422,28 +501,57 @@ function printScanDetail(summary, options) {
422
501
  }
423
502
  }
424
503
 
425
- function printOrgSummary(summary, options) {
426
- if (options.json) {
427
- console.log(JSON.stringify(summary, null, 2));
428
- return;
429
- }
504
+ function printOrgSummary(summary, options) {
505
+ if (options.json) {
506
+ console.log(JSON.stringify(summary, null, 2));
507
+ return;
508
+ }
430
509
 
431
510
  console.log('');
432
- console.log('\x1b[1m nerviq org scan\x1b[0m');
433
- console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
434
- console.log(` Platform: ${summary.platform}`);
435
- console.log(` Repos: ${summary.repoCount}`);
436
- console.log(` Average score: \x1b[1m${summary.averageScore}/100\x1b[0m`);
437
- console.log('');
438
- console.log('\x1b[1m Repo Platform Score Top action\x1b[0m');
439
- console.log(' ' + '─'.repeat(72));
440
- for (const item of summary.repos) {
441
- const score = item.score === null ? 'ERR' : String(item.score);
442
- const topAction = item.error || item.topAction || '-';
443
- console.log(` ${item.name.padEnd(18)} ${item.platform.padEnd(8)} ${score.padStart(5)} ${topAction}`);
444
- }
445
- console.log('');
446
- }
511
+ console.log('\x1b[1m nerviq org scan\x1b[0m');
512
+ console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
513
+ console.log(` Platform: ${summary.platform}`);
514
+ console.log(` Repos: ${summary.repoCount}`);
515
+ console.log(` Average score: \x1b[1m${summary.averageScore}/100\x1b[0m`);
516
+ if (summary.scoreSemantics?.note) {
517
+ console.log(` Score semantics: ${summary.scoreSemantics.note}`);
518
+ }
519
+ if (summary.policyCoverage) {
520
+ console.log(` Policy coverage: org=${summary.policyCoverage.orgPolicyRepos} team=${summary.policyCoverage.teamPolicyRepos} repo=${summary.policyCoverage.repoPolicyRepos}`);
521
+ }
522
+ if (summary.scoreBands) {
523
+ console.log(` Bands: strong=${summary.scoreBands.strong} developing=${summary.scoreBands.developing} bootstrap=${summary.scoreBands.bootstrap} unknown=${summary.scoreBands.unknown}`);
524
+ }
525
+ console.log('');
526
+ console.log('\x1b[1m Repo Platform Score Policy Top action\x1b[0m');
527
+ console.log(' ' + '─'.repeat(72));
528
+ for (const item of summary.repos) {
529
+ const score = item.score === null ? 'ERR' : String(item.score);
530
+ const topAction = item.error || item.topAction || '-';
531
+ const policy = item.policyCoverage?.layerKeys?.length > 0 ? item.policyCoverage.layerKeys.join('/') : '-';
532
+ console.log(` ${item.name.padEnd(18)} ${item.platform.padEnd(8)} ${score.padStart(5)} ${policy.padEnd(12)} ${topAction}`);
533
+ }
534
+ if (Array.isArray(summary.topEvidence) && summary.topEvidence.length > 0) {
535
+ console.log('');
536
+ console.log(' Common top evidence:');
537
+ for (const item of summary.topEvidence) {
538
+ console.log(` - ${item.key} (${item.repoCount} repos)`);
539
+ }
540
+ }
541
+ console.log('');
542
+ }
543
+
544
+ function writeStdout(text) {
545
+ return new Promise((resolve, reject) => {
546
+ process.stdout.write(text, (error) => {
547
+ if (error) {
548
+ reject(error);
549
+ return;
550
+ }
551
+ resolve();
552
+ });
553
+ });
554
+ }
447
555
 
448
556
  const HELP = `
449
557
  nerviq v${version}
@@ -457,19 +565,22 @@ const HELP = `
457
565
  nerviq audit --platform X Audit specific platform (claude|codex|cursor|copilot|gemini|windsurf|aider|opencode)
458
566
  nerviq audit --json Machine-readable JSON output (for CI)
459
567
  nerviq audit --workspace packages/* Audit monorepo workspaces with stack-specific package profiles
460
- nerviq scan dir1 dir2 Compare multiple repos side-by-side
461
- nerviq org scan dir1 dir2 Aggregate multiple repos into one score table
568
+ nerviq scan dir1 dir2 Compare multiple repos side-by-side
569
+ nerviq org scan dir1 dir2 Aggregate multiple repos into one score table
570
+ nerviq org policy [dir] Inspect resolved org/team/repo policy layers
462
571
  nerviq catalog Full check catalog (all 8 platforms)
463
572
  nerviq catalog --json Export full check catalog as JSON
464
573
  nerviq anti-patterns Detect anti-patterns in current project
465
574
  nerviq anti-patterns --all Show full anti-pattern catalog
466
575
 
467
- SETUP
468
- nerviq setup Generate starter-safe baseline config files
469
- nerviq setup --auto Apply all generated files without prompts
470
- nerviq interactive Step-by-step guided wizard
471
- nerviq check-health Detect regressions + platform format changes between snapshots
472
- nerviq doctor Self-diagnostics: Node, deps, freshness, platform detection
576
+ SETUP
577
+ nerviq setup Generate starter-safe baseline config files
578
+ nerviq setup --auto Apply all generated files without prompts
579
+ nerviq interactive Step-by-step guided wizard
580
+ nerviq baseline init Lock the first managed Nerviq baseline for continuous ops
581
+ nerviq baseline status Show the current managed baseline contract
582
+ nerviq check-health Detect regressions + platform format changes between snapshots
583
+ nerviq doctor Self-diagnostics: Node, deps, freshness, MCP, hook runtime
473
584
 
474
585
  FIX
475
586
  nerviq fix Show fixable checks and manual-fix guidance
@@ -482,13 +593,15 @@ const HELP = `
482
593
  nerviq rollback --list Show available rollback points
483
594
  nerviq rollback --dry-run Preview what would be deleted
484
595
 
485
- IMPROVE
486
- nerviq augment Improvement plan (no writes)
487
- nerviq suggest-only Structured report for sharing (no writes)
488
- nerviq plan Export proposal bundles with diffs
489
- nerviq plan --out plan.json Save plan to file
490
- nerviq apply Apply proposals selectively with rollback
491
- nerviq apply --dry-run Preview changes without writing
596
+ IMPROVE
597
+ nerviq augment Improvement plan (no writes)
598
+ nerviq suggest-only Structured report for sharing (no writes)
599
+ nerviq plan Export proposal bundles with diffs
600
+ nerviq plan --campaign X Export a named upgrade campaign slice
601
+ nerviq plan --out plan.json Save plan to file
602
+ nerviq apply Apply proposals selectively with rollback
603
+ nerviq apply --campaign X Apply a named upgrade campaign
604
+ nerviq apply --dry-run Preview changes without writing
492
605
 
493
606
  GOVERN
494
607
  nerviq governance Permission profiles + hooks + policy packs (the rollout safety layer)
@@ -509,17 +622,24 @@ const HELP = `
509
622
  nerviq migrate --platform X Platform version migration helper
510
623
  nerviq migrate --platform cursor --from v2 --to v3
511
624
 
512
- MONITOR
625
+ MONITOR
513
626
  nerviq dashboard Generate static dashboard from latest audit snapshot (or live audit if none)
514
- nerviq dashboard --out F Save dashboard to custom file
515
- nerviq dashboard --open Open dashboard in browser after generating
516
- nerviq watch Live config monitoring (re-audits on file change)
627
+ nerviq dashboard --out F Save dashboard to custom file
628
+ nerviq dashboard --open Open dashboard in browser after generating
629
+ nerviq watch Live config monitoring (re-audits on file change)
630
+ nerviq audit --diff-only --drift-mode ci PR / CI drift review against the managed baseline
517
631
  nerviq history Audit snapshot history from saved snapshots
518
632
  nerviq compare Detailed per-check diff between latest two audit snapshots
519
633
  nerviq trend Audit snapshot trend over time
520
634
  nerviq trend --out report.md Export trend report as markdown
521
- nerviq audit --snapshot --tag "pre-refactor" Save a named audit snapshot
635
+ nerviq audit --snapshot --milestone baseline --tag "baseline" Save a lifecycle checkpoint
522
636
  nerviq feedback Record recommendation outcomes
637
+
638
+ EXCEPTIONS
639
+ nerviq exception add --key permissionDeny --owner team --reason "migration in progress" --expires 2026-05-01
640
+ nerviq exception add --class policy-drift --scope ci --owner team --reason "temporary rollout" --expires 2026-05-01
641
+ nerviq exception list Show active and expired exceptions
642
+ nerviq exception prune Remove expired exceptions
523
643
 
524
644
  TEAM PROFILES
525
645
  nerviq profile save <name> Save current preferences as a named profile
@@ -528,7 +648,8 @@ const HELP = `
528
648
  nerviq profile export <name> Export profile JSON for sharing
529
649
 
530
650
  ADVANCED
531
- nerviq deep-review AI-powered config review (opt-in, uses API key)
651
+ nerviq deep-review AI-powered config review (opt-in, uses API key)
652
+ nerviq deep-review --behavioral Local behavioral drift review (opt-in, no API)
532
653
  nerviq serve --port 3000 Start local Nerviq REST API server + OpenAPI contract
533
654
  nerviq badge Generate shields.io badge markdown
534
655
  nerviq rules-export Export recommendation rules as JSON
@@ -553,8 +674,14 @@ const HELP = `
553
674
  --external PATH Benchmark an external repo instead of cwd
554
675
  --port N Port for \`serve\` (default: 3000)
555
676
  --workspace GLOBS Audit workspaces separately with root/package score semantics and stack-specific profiles
677
+ --diff-only Audit only changed files / linked config surfaces from git diff
678
+ --drift-mode M Continuous posture mode: ci | pr | watch
679
+ --diff-base SHA Base SHA for diff-only mode (defaults to PR env vars when present)
680
+ --diff-head SHA Head SHA for diff-only mode (defaults to GITHUB_SHA or HEAD)
556
681
  --snapshot Save snapshot artifact under .claude/nerviq/snapshots/
557
682
  --tag LABEL Tag the saved snapshot (use with --snapshot; repeat or comma-separate for more)
683
+ --milestone NAME Snapshot lifecycle milestone: baseline | post-fix | pre-upgrade | release
684
+ --campaign A,B Limit plan/apply to named upgrade campaigns
558
685
  --full Show full audit output (all checks, weakest areas, badge)
559
686
  --lite Short top-3 scan (default behavior since v1.5.2)
560
687
  --dry-run Preview changes without writing files
@@ -565,26 +692,38 @@ const HELP = `
565
692
  --auto Apply all generated files without prompting
566
693
  --beginner Show only the 5 starter commands for first-time users
567
694
  --key NAME Feedback: recommendation key (e.g. permissionDeny)
568
- --status VALUE Feedback: accepted | rejected | deferred
569
- --effect VALUE Feedback: positive | neutral | negative
570
- --score-delta N Feedback: observed score delta
571
- --help Show this help
572
- --version Show version
695
+ --status VALUE Feedback: accepted | rejected | deferred
696
+ --effect VALUE Feedback: positive | neutral | negative
697
+ --score-delta N Feedback: observed score delta
698
+ --owner NAME Exception owner
699
+ --reason TEXT Exception reason
700
+ --expires DATE Exception expiry (ISO date or date-time)
701
+ --scope NAME Exception scope: all | ci | watch | pr
702
+ --class NAME Exception target class: policy-drift | config-drift | platform-drift | maturity-opportunity
703
+ --behavioral Run the opt-in local behavioral drift / outcome-layer review
704
+ --history With deep-review --behavioral, show behavioral snapshot history
705
+ --compare With deep-review --behavioral, compare the latest two behavioral snapshots
706
+ --help Show this help
707
+ --version Show version
573
708
 
574
709
  EXAMPLES
575
710
  npx nerviq --beginner
576
711
  npx nerviq
577
712
  npx nerviq --lite
578
713
  npx nerviq --platform cursor
579
- npx nerviq audit --workspace packages/*
580
- npx nerviq --platform codex augment
581
- npx nerviq org scan ./app ./api ./infra
714
+ npx nerviq audit --workspace packages/*
715
+ npx nerviq baseline init
716
+ npx nerviq audit --diff-only --drift-mode ci
717
+ npx nerviq --platform codex augment
718
+ npx nerviq org scan ./app ./api ./infra
719
+ npx nerviq org policy
582
720
  npx nerviq scan ./app ./api ./infra
583
721
  npx nerviq harmony-audit
584
722
  npx nerviq convert --from claude --to codex
585
723
  npx nerviq migrate --platform cursor --from v2 --to v3
586
- npx nerviq setup --mcp-pack context7-docs
587
- npx nerviq apply --plan plan.json --only hooks,commands
724
+ npx nerviq setup --mcp-pack context7-docs
725
+ npx nerviq plan --campaign governance-hardening
726
+ npx nerviq apply --plan plan.json --only hooks,commands
588
727
  npx nerviq serve --port 4000
589
728
  npx nerviq --json --threshold 70
590
729
  npx nerviq catalog --json --out catalog.json
@@ -606,7 +745,7 @@ const BEGINNER_HELP = `
606
745
  nerviq setup Generate a starter-safe baseline
607
746
  nerviq fix Fix what can be fixed or show manual fix guidance
608
747
  nerviq augment Show an improvement plan without writing
609
- nerviq doctor Check install health, freshness, and platform detection
748
+ nerviq doctor Check install health, freshness, platform detection, MCP, and hook runtime
610
749
 
611
750
  SIMPLE PATH
612
751
  1. nerviq audit
@@ -670,9 +809,10 @@ async function main() {
670
809
  only: parsed.only,
671
810
  profile: parsed.profile,
672
811
  mcpPacks: parsed.mcpPacks,
673
- require: parsed.requireChecks,
674
- platform: parsed.platform || 'claude',
675
- format: parsed.format || null,
812
+ require: parsed.requireChecks,
813
+ platform: parsed.platform || 'claude',
814
+ platformExplicit: Boolean(parsed.platformExplicit),
815
+ format: parsed.format || null,
676
816
  port: parsed.port !== null ? Number(parsed.port) : null,
677
817
  workspace: parsed.workspace || null,
678
818
  webhookUrl: parsed.webhookUrl || null,
@@ -681,6 +821,20 @@ async function main() {
681
821
  lang: parsed.lang || null,
682
822
  external: parsed.external || null,
683
823
  snapshotTags: parsed.snapshotTags || [],
824
+ snapshotMilestone: parsed.snapshotMilestone || null,
825
+ campaigns: parsed.campaigns || [],
826
+ behavioral: flags.includes('--behavioral'),
827
+ historyView: flags.includes('--history'),
828
+ compareView: flags.includes('--compare'),
829
+ diffOnly: flags.includes('--diff-only'),
830
+ diffBase: parsed.diffBase || null,
831
+ diffHead: parsed.diffHead || null,
832
+ driftMode: parsed.driftMode || null,
833
+ exceptionOwner: parsed.exceptionOwner || null,
834
+ exceptionReason: parsed.exceptionReason || null,
835
+ exceptionExpires: parsed.exceptionExpires || null,
836
+ exceptionScope: parsed.exceptionScope || null,
837
+ exceptionClass: parsed.exceptionClass || null,
684
838
  dir: process.cwd()
685
839
  };
686
840
 
@@ -688,18 +842,49 @@ async function main() {
688
842
  console.error('\n Error: --tag requires --snapshot.\n');
689
843
  process.exit(1);
690
844
  }
845
+
846
+ if (options.snapshotMilestone && !options.snapshot) {
847
+ console.error('\n Error: --milestone requires --snapshot.\n');
848
+ process.exit(1);
849
+ }
850
+
851
+ if (options.snapshotMilestone && !SNAPSHOT_MILESTONES.includes(options.snapshotMilestone)) {
852
+ console.error(`\n Error: Unsupported milestone '${options.snapshotMilestone}'. Use one of: ${SNAPSHOT_MILESTONES.join(', ')}.\n`);
853
+ process.exit(1);
854
+ }
855
+
856
+ if (options.diffOnly && options.snapshot) {
857
+ console.error('\n Error: --diff-only cannot be combined with --snapshot because diff-only scores are not comparable to full audit snapshots.\n');
858
+ process.exit(1);
859
+ }
860
+
861
+ if (options.driftMode && !['ci', 'pr', 'watch'].includes(options.driftMode)) {
862
+ console.error(`\n Error: Unsupported drift mode '${options.driftMode}'. Use ci, pr, or watch.\n`);
863
+ process.exit(1);
864
+ }
691
865
 
692
- if (parsed.checkVersion) {
866
+ if (parsed.checkVersion) {
693
867
  if (parsed.checkVersion !== version) {
694
868
  console.error(`\n Warning: --check-version ${parsed.checkVersion} does not match installed nerviq version ${version}.`);
695
869
  console.error(` Check catalog may differ between versions. To align, run: npm install @nerviq/cli@${parsed.checkVersion}`);
696
870
  console.error('');
697
871
  }
698
- options.checkVersion = parsed.checkVersion;
699
- }
700
-
701
- if (parsed.teamProfile) {
702
- const { loadProfile, applyProfileToOptions } = require('../src/profiles');
872
+ options.checkVersion = parsed.checkVersion;
873
+ }
874
+
875
+ const {
876
+ resolvePolicyLayers,
877
+ applyPolicyLayersToOptions,
878
+ formatPolicyContract,
879
+ } = require('../src/policy-layers');
880
+ const inheritedPolicyContract = resolvePolicyLayers(options.dir);
881
+ if (inheritedPolicyContract.layers.some((layer) => layer.valid)) {
882
+ Object.assign(options, applyPolicyLayersToOptions(inheritedPolicyContract, options));
883
+ options.policyContract = inheritedPolicyContract;
884
+ }
885
+
886
+ if (parsed.teamProfile) {
887
+ const { loadProfile, applyProfileToOptions } = require('../src/profiles');
703
888
  try {
704
889
  const teamProf = loadProfile(options.dir, parsed.teamProfile);
705
890
  const merged = applyProfileToOptions(teamProf, options);
@@ -738,10 +923,15 @@ async function main() {
738
923
  process.exit(1);
739
924
  }
740
925
 
741
- if (options.format !== null && !['json', 'sarif', 'otel'].includes(options.format)) {
742
- console.error(`\n Error: Unsupported format '${options.format}'. Use 'json', 'sarif', or 'otel'.\n`);
743
- process.exit(1);
744
- }
926
+ if (options.format !== null && !['json', 'sarif', 'otel'].includes(options.format)) {
927
+ console.error(`\n Error: Unsupported format '${options.format}'. Use 'json', 'sarif', or 'otel'.\n`);
928
+ process.exit(1);
929
+ }
930
+
931
+ if (options.driftMode && options.format !== null) {
932
+ console.error('\n Error: --drift-mode is only supported with normal text output or --json.\n');
933
+ process.exit(1);
934
+ }
745
935
 
746
936
  if (options.port !== null && (!Number.isInteger(options.port) || options.port < 0 || options.port > 65535)) {
747
937
  console.error('\n Error: --port must be an integer between 0 and 65535.\n');
@@ -791,13 +981,13 @@ async function main() {
791
981
  }
792
982
 
793
983
  try {
794
- const FULL_COMMAND_SET = new Set([
795
- 'audit', 'org', 'scan', 'badge', 'augment', 'suggest-only', 'setup', 'plan', 'apply',
796
- 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'insights',
797
- 'history', 'compare', 'trend', 'feedback', 'catalog', 'certify', 'serve', 'help', 'version',
798
- // Harmony + Synergy (cross-platform)
799
- 'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise',
800
- 'harmony-watch', 'harmony-governance', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export',
984
+ const FULL_COMMAND_SET = new Set([
985
+ 'audit', 'org', 'scan', 'badge', 'augment', 'suggest-only', 'setup', 'plan', 'apply',
986
+ 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'insights',
987
+ 'history', 'compare', 'trend', 'feedback', 'catalog', 'certify', 'serve', 'baseline', 'exception', 'help', 'version',
988
+ // Harmony + Synergy (cross-platform)
989
+ 'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise',
990
+ 'harmony-watch', 'harmony-governance', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export',
801
991
  'freshness', 'profile', 'migrate',
802
992
  ]);
803
993
 
@@ -843,33 +1033,51 @@ async function main() {
843
1033
  }
844
1034
  }
845
1035
 
846
- if (normalizedCommand === 'scan') {
847
- const scanDirs = parsed.extraArgs;
848
- if (scanDirs.length === 0) {
849
- console.error('\n Error: scan requires at least one directory argument.');
850
- console.error(' Usage: npx nerviq scan dir1 dir2 dir3\n');
851
- process.exit(1);
852
- }
853
- const summary = await scanOrg(scanDirs, options.platform);
854
- printScanDetail(summary, options);
855
- if (options.threshold !== null && summary.averageScore < options.threshold) {
856
- process.exit(1);
857
- }
858
- process.exit(0);
859
- } else if (normalizedCommand === 'org') {
860
- const subcommand = parsed.extraArgs[0];
861
- const scanDirs = parsed.extraArgs.slice(1);
862
- if (subcommand !== 'scan' || scanDirs.length === 0) {
863
- console.error('\n Error: org requires the scan subcommand and at least one directory.');
864
- console.error(' Usage: npx nerviq org scan dir1 dir2 dir3\n');
865
- process.exit(1);
866
- }
867
- const summary = await scanOrg(scanDirs, options.platform);
868
- printOrgSummary(summary, options);
869
- if (options.threshold !== null && summary.averageScore < options.threshold) {
870
- process.exit(1);
871
- }
872
- process.exit(0);
1036
+ if (normalizedCommand === 'scan') {
1037
+ const scanDirs = parsed.extraArgs;
1038
+ if (scanDirs.length === 0) {
1039
+ console.error('\n Error: scan requires at least one directory argument.');
1040
+ console.error(' Usage: npx nerviq scan dir1 dir2 dir3\n');
1041
+ process.exit(1);
1042
+ }
1043
+ const summary = await scanOrg(scanDirs, options);
1044
+ printScanDetail(summary, options);
1045
+ if (options.threshold !== null && summary.averageScore < options.threshold) {
1046
+ process.exit(1);
1047
+ }
1048
+ process.exit(0);
1049
+ } else if (normalizedCommand === 'org') {
1050
+ const subcommand = parsed.extraArgs[0];
1051
+ if (subcommand === 'policy') {
1052
+ const targetDir = parsed.extraArgs[1] ? require('path').resolve(parsed.extraArgs[1]) : options.dir;
1053
+ const contract = resolvePolicyLayers(targetDir);
1054
+ if (options.json) {
1055
+ await writeStdout(JSON.stringify(contract, null, 2) + '\n');
1056
+ } else {
1057
+ console.log('');
1058
+ console.log(formatPolicyContract(contract));
1059
+ console.log('');
1060
+ }
1061
+ process.exit(0);
1062
+ }
1063
+
1064
+ const scanDirs = parsed.extraArgs.slice(1);
1065
+ if (subcommand !== 'scan' || scanDirs.length === 0) {
1066
+ console.error('\n Error: org requires `scan` or `policy`.');
1067
+ console.error(' Usage: npx nerviq org scan dir1 dir2 dir3');
1068
+ console.error(' npx nerviq org policy [dir]\n');
1069
+ process.exit(1);
1070
+ }
1071
+ const summary = await scanOrg(scanDirs, options);
1072
+ if (options.json) {
1073
+ await writeStdout(JSON.stringify(summary, null, 2) + '\n');
1074
+ } else {
1075
+ printOrgSummary(summary, options);
1076
+ }
1077
+ if (options.threshold !== null && summary.averageScore < options.threshold) {
1078
+ process.exit(1);
1079
+ }
1080
+ process.exit(0);
873
1081
  } else if (normalizedCommand === 'history') {
874
1082
  const { formatHistory, readSnapshotIndex } = require('../src/activity');
875
1083
  // Handle --prune N
@@ -900,7 +1108,7 @@ async function main() {
900
1108
  console.log('');
901
1109
  process.exit(0);
902
1110
  } else if (normalizedCommand === 'compare') {
903
- const { compareLatest, formatSnapshotBootstrap, formatSnapshotTags } = require('../src/activity');
1111
+ const { compareLatest, formatSnapshotBootstrap, formatSnapshotTags, formatSnapshotMilestone } = require('../src/activity');
904
1112
  const result = compareLatest(options.dir);
905
1113
  if (!result) {
906
1114
  console.log('');
@@ -913,8 +1121,8 @@ async function main() {
913
1121
  } else {
914
1122
  const sign = result.delta.score >= 0 ? '+' : '';
915
1123
  console.log('');
916
- console.log(` Previous snapshot: ${result.previous.score}/100 (${result.previous.date?.split('T')[0]})${formatSnapshotTags(result.previous.tags)}`);
917
- console.log(` Current snapshot: ${result.current.score}/100 (${result.current.date?.split('T')[0]})${formatSnapshotTags(result.current.tags)}`);
1124
+ console.log(` Previous snapshot: ${result.previous.score}/100 (${result.previous.date?.split('T')[0]})${formatSnapshotMilestone(result.previous.milestone)}${formatSnapshotTags(result.previous.tags)}`);
1125
+ console.log(` Current snapshot: ${result.current.score}/100 (${result.current.date?.split('T')[0]})${formatSnapshotMilestone(result.current.milestone)}${formatSnapshotTags(result.current.tags)}`);
918
1126
  console.log(` Snapshot delta: ${sign}${result.delta.score} points`);
919
1127
  console.log(` Trend: ${result.trend}`);
920
1128
  if (result.detailedDiffAvailable) {
@@ -1066,6 +1274,7 @@ async function main() {
1066
1274
  const report = await analyzeProject({ ...options, mode: normalizedCommand });
1067
1275
  const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, normalizedCommand, report, {
1068
1276
  tags: options.snapshotTags,
1277
+ milestone: options.snapshotMilestone,
1069
1278
  sourceCommand: normalizedCommand,
1070
1279
  }) : null;
1071
1280
  if (options.out && !options.json) {
@@ -1200,6 +1409,7 @@ async function main() {
1200
1409
  printGovernanceSummary(summary, options);
1201
1410
  const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'governance', summary, {
1202
1411
  tags: options.snapshotTags,
1412
+ milestone: options.snapshotMilestone,
1203
1413
  sourceCommand: normalizedCommand,
1204
1414
  }) : null;
1205
1415
  if (options.out && !options.json) {
@@ -1215,6 +1425,7 @@ async function main() {
1215
1425
  const report = await runBenchmark(options);
1216
1426
  const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'benchmark', report, {
1217
1427
  tags: options.snapshotTags,
1428
+ milestone: options.snapshotMilestone,
1218
1429
  sourceCommand: normalizedCommand,
1219
1430
  }) : null;
1220
1431
  if (options.out) {
@@ -1233,13 +1444,87 @@ async function main() {
1233
1444
  } else if (normalizedCommand === 'deep-review') {
1234
1445
  const { deepReview } = require('../src/deep-review');
1235
1446
  await deepReview(options);
1236
- } else if (normalizedCommand === 'interactive') {
1237
- const { interactive } = require('../src/interactive');
1238
- await interactive(options);
1239
- } else if (normalizedCommand === 'watch') {
1240
- const { watch } = require('../src/watch');
1241
- await watch(options);
1242
- } else if (normalizedCommand === 'catalog') {
1447
+ } else if (normalizedCommand === 'interactive') {
1448
+ const { interactive } = require('../src/interactive');
1449
+ await interactive(options);
1450
+ } else if (normalizedCommand === 'baseline') {
1451
+ const {
1452
+ readManagedBaseline,
1453
+ writeManagedBaseline,
1454
+ buildManagedBaselineRecord,
1455
+ formatManagedBaselineStatus,
1456
+ } = require('../src/continuous-ops');
1457
+ const subcommand = parsed.extraArgs[0] || 'status';
1458
+
1459
+ if (subcommand === 'status') {
1460
+ const baseline = readManagedBaseline(options.dir);
1461
+ if (options.json) {
1462
+ console.log(JSON.stringify(baseline, null, 2));
1463
+ } else {
1464
+ console.log('');
1465
+ console.log(formatManagedBaselineStatus(options.dir, baseline));
1466
+ console.log('');
1467
+ }
1468
+ process.exit(0);
1469
+ }
1470
+
1471
+ if (subcommand === 'init') {
1472
+ const existingBaseline = readManagedBaseline(options.dir);
1473
+ if (existingBaseline && !flags.includes('--force')) {
1474
+ console.error('\n Error: Managed baseline already exists. Use `nerviq baseline status` to inspect it, or rerun with --force to replace it.\n');
1475
+ process.exit(1);
1476
+ }
1477
+
1478
+ const auditResult = await audit({ ...options, silent: true });
1479
+ const analysisReport = await analyzeProject({ ...options, mode: 'augment' });
1480
+ const detectedPlatforms = detectPlatforms(options.dir);
1481
+ const snapshot = writeSnapshotArtifact(options.dir, 'audit', auditResult, {
1482
+ tags: [...options.snapshotTags, 'baseline'],
1483
+ milestone: 'baseline',
1484
+ sourceCommand: 'baseline init',
1485
+ managedBaseline: true,
1486
+ });
1487
+ const baselineRecord = buildManagedBaselineRecord({
1488
+ dir: options.dir,
1489
+ platform: options.platform,
1490
+ auditResult,
1491
+ analysisReport,
1492
+ snapshotArtifact: snapshot,
1493
+ currentPlatforms: detectedPlatforms,
1494
+ });
1495
+ const saved = writeManagedBaseline(options.dir, baselineRecord);
1496
+
1497
+ if (options.json) {
1498
+ console.log(JSON.stringify({
1499
+ ...baselineRecord,
1500
+ baselinePath: saved.relativePath,
1501
+ }, null, 2));
1502
+ } else {
1503
+ console.log('');
1504
+ console.log(' nerviq baseline init');
1505
+ console.log(' ═══════════════════════════════════════');
1506
+ console.log(` Managed baseline written: ${saved.relativePath}`);
1507
+ console.log(` Snapshot: ${snapshot.relativePath}`);
1508
+ console.log(` Score: ${baselineRecord.baselineAudit.score}/100`);
1509
+ console.log(` Operating profile: ${baselineRecord.operatingProfile.label || 'n/a'}`);
1510
+ console.log(` Adoption plan: ${baselineRecord.adoptionPlan || 'n/a'}`);
1511
+ console.log(` Active platforms: ${(baselineRecord.detectedPlatforms || []).join(', ') || 'none detected'}`);
1512
+ console.log('');
1513
+ console.log(' Next:');
1514
+ console.log(' - nerviq audit --diff-only --drift-mode ci');
1515
+ console.log(' - nerviq watch');
1516
+ console.log(' - nerviq plan --campaign governance-hardening');
1517
+ console.log('');
1518
+ }
1519
+ process.exit(0);
1520
+ }
1521
+
1522
+ console.error('\n Error: baseline supports `init` and `status`.\n');
1523
+ process.exit(1);
1524
+ } else if (normalizedCommand === 'watch') {
1525
+ const { watch } = require('../src/watch');
1526
+ await watch(options);
1527
+ } else if (normalizedCommand === 'catalog') {
1243
1528
  const { generateCatalogWithVersion, writeCatalogJson } = require('../src/catalog');
1244
1529
  if (options.out) {
1245
1530
  const result = writeCatalogJson(options.out);
@@ -1538,18 +1823,74 @@ async function main() {
1538
1823
  }
1539
1824
  }
1540
1825
  process.exit(0);
1541
- } else if (normalizedCommand === 'suggest-rules') {
1542
- const { analyzeSuggestions, formatSuggestions } = require('../src/auto-suggest');
1543
- const suggestions = analyzeSuggestions(options.dir);
1544
- if (options.json) {
1545
- console.log(JSON.stringify(suggestions, null, 2));
1546
- } else {
1547
- console.log('');
1548
- console.log(formatSuggestions(suggestions));
1549
- console.log('');
1550
- }
1551
- process.exit(0);
1552
- } else if (normalizedCommand === 'profile') {
1826
+ } else if (normalizedCommand === 'suggest-rules') {
1827
+ const { analyzeSuggestions, formatSuggestions } = require('../src/auto-suggest');
1828
+ const suggestions = analyzeSuggestions(options.dir);
1829
+ if (options.json) {
1830
+ console.log(JSON.stringify(suggestions, null, 2));
1831
+ } else {
1832
+ console.log('');
1833
+ console.log(formatSuggestions(suggestions));
1834
+ console.log('');
1835
+ }
1836
+ process.exit(0);
1837
+ } else if (normalizedCommand === 'exception') {
1838
+ const {
1839
+ listExceptions,
1840
+ addException,
1841
+ pruneExpiredExceptions,
1842
+ formatExceptionsList,
1843
+ } = require('../src/continuous-ops');
1844
+ const subcommand = parsed.extraArgs[0] || 'list';
1845
+
1846
+ if (subcommand === 'list') {
1847
+ const records = listExceptions(options.dir);
1848
+ if (options.json) {
1849
+ console.log(JSON.stringify(records, null, 2));
1850
+ } else {
1851
+ console.log('');
1852
+ console.log(formatExceptionsList(records));
1853
+ console.log('');
1854
+ }
1855
+ process.exit(0);
1856
+ }
1857
+
1858
+ if (subcommand === 'add') {
1859
+ const result = addException(options.dir, {
1860
+ key: parsed.feedbackKey || null,
1861
+ watchClass: options.exceptionClass,
1862
+ owner: options.exceptionOwner,
1863
+ reason: options.exceptionReason,
1864
+ expiresAt: options.exceptionExpires,
1865
+ scope: options.exceptionScope || 'all',
1866
+ });
1867
+ if (options.json) {
1868
+ console.log(JSON.stringify(result.record, null, 2));
1869
+ } else {
1870
+ console.log('');
1871
+ console.log(` Exception added: ${result.record.id}`);
1872
+ console.log(` Target: ${result.record.key || result.record.watchClass}`);
1873
+ console.log(` Owner: ${result.record.owner}`);
1874
+ console.log(` Scope: ${result.record.scope}`);
1875
+ console.log(` Expires: ${result.record.expiresAt}`);
1876
+ console.log('');
1877
+ }
1878
+ process.exit(0);
1879
+ }
1880
+
1881
+ if (subcommand === 'prune') {
1882
+ const result = pruneExpiredExceptions(options.dir);
1883
+ if (options.json) {
1884
+ console.log(JSON.stringify(result, null, 2));
1885
+ } else {
1886
+ console.log(`\n Pruned ${result.removedCount} expired exception(s). Kept ${result.keptCount} active record(s).\n`);
1887
+ }
1888
+ process.exit(0);
1889
+ }
1890
+
1891
+ console.error('\n Error: exception supports `add`, `list`, and `prune`.\n');
1892
+ process.exit(1);
1893
+ } else if (normalizedCommand === 'profile') {
1553
1894
  const { saveProfile, loadProfile, listProfiles, exportProfile, formatProfileList, formatProfile } = require('../src/profiles');
1554
1895
  const subcommand = parsed.extraArgs[0];
1555
1896
  const profileArg = parsed.extraArgs[1];
@@ -2095,27 +2436,113 @@ async function main() {
2095
2436
  const postSetupResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
2096
2437
  const snapshot = writeSnapshotArtifact(options.dir, 'audit', postSetupResult, {
2097
2438
  tags: options.snapshotTags,
2439
+ milestone: options.snapshotMilestone,
2098
2440
  sourceCommand: 'setup',
2099
2441
  });
2100
2442
  if (!options.json) {
2101
2443
  console.log(` Snapshot saved: ${snapshot.relativePath}`);
2102
2444
  }
2103
2445
  }
2104
- } else {
2105
- if (options.workspace) {
2106
- const summary = await auditWorkspaces(options.dir, options.workspace, options.platform);
2107
- printWorkspaceSummary(summary, options);
2108
- if (options.threshold !== null && summary.averageScore < options.threshold) {
2109
- process.exit(1);
2110
- }
2111
- process.exit(0);
2112
- }
2113
- const result = await audit(options);
2114
- if (options.out) {
2115
- const fs = require('fs');
2116
- const path = require('path');
2117
- const outPath = path.resolve(options.out);
2118
- fs.mkdirSync(path.dirname(outPath), { recursive: true });
2446
+ } else {
2447
+ if (options.workspace) {
2448
+ const summary = await auditWorkspaces(options.dir, options.workspace, options.platform);
2449
+ printWorkspaceSummary(summary, options);
2450
+ if (options.threshold !== null && summary.averageScore < options.threshold) {
2451
+ process.exit(1);
2452
+ }
2453
+ process.exit(0);
2454
+ }
2455
+ let result;
2456
+ const renderAuditJsonLocally = options.json && Boolean(options.driftMode);
2457
+ if (options.diffOnly) {
2458
+ const { getChangedFiles, buildDiffOnlyAuditView, printDiffOnlyAudit } = require('../src/diff-only');
2459
+ const fullResult = await audit({ ...options, silent: true });
2460
+ const diffInfo = getChangedFiles(options.dir, {
2461
+ diffBase: options.diffBase,
2462
+ diffHead: options.diffHead,
2463
+ });
2464
+ result = buildDiffOnlyAuditView(fullResult, diffInfo);
2465
+ } else {
2466
+ result = renderAuditJsonLocally
2467
+ ? await audit({ ...options, silent: true })
2468
+ : await audit(options);
2469
+ }
2470
+
2471
+ if (options.driftMode) {
2472
+ const { buildContinuousStatus, formatContinuousStatus } = require('../src/continuous-ops');
2473
+ let campaigns = [];
2474
+ try {
2475
+ const planBundle = await buildProposalBundle({
2476
+ dir: options.dir,
2477
+ platform: options.platform,
2478
+ profile: options.profile,
2479
+ mcpPacks: options.mcpPacks,
2480
+ campaigns: [],
2481
+ });
2482
+ campaigns = planBundle.campaigns || [];
2483
+ } catch {
2484
+ campaigns = [];
2485
+ }
2486
+
2487
+ result = {
2488
+ ...result,
2489
+ continuousStatus: buildContinuousStatus({
2490
+ dir: options.dir,
2491
+ auditResult: result,
2492
+ mode: options.driftMode,
2493
+ currentPlatforms: detectPlatforms(options.dir),
2494
+ campaigns,
2495
+ }),
2496
+ };
2497
+ }
2498
+
2499
+ if (options.policyContract && options.policyContract.layers.some((layer) => layer.valid)) {
2500
+ result = {
2501
+ ...result,
2502
+ policyLayers: options.policyContract,
2503
+ };
2504
+ }
2505
+
2506
+ if (options.diffOnly) {
2507
+ const { printDiffOnlyAudit } = require('../src/diff-only');
2508
+ if (options.json) {
2509
+ console.log(JSON.stringify({
2510
+ version,
2511
+ timestamp: new Date().toISOString(),
2512
+ ...result,
2513
+ }, null, 2));
2514
+ } else {
2515
+ console.log(printDiffOnlyAudit(result));
2516
+ if (result.continuousStatus) {
2517
+ const { formatContinuousStatus } = require('../src/continuous-ops');
2518
+ console.log(formatContinuousStatus(result.continuousStatus));
2519
+ console.log('');
2520
+ }
2521
+ }
2522
+ } else if (renderAuditJsonLocally) {
2523
+ console.log(JSON.stringify({
2524
+ version,
2525
+ timestamp: new Date().toISOString(),
2526
+ ...result,
2527
+ }, null, 2));
2528
+ } else {
2529
+ if (!options.json && options.policyContract && options.policyContract.layers.some((layer) => layer.valid)) {
2530
+ console.log('');
2531
+ console.log(formatPolicyContract(options.policyContract));
2532
+ console.log('');
2533
+ }
2534
+ if (!options.json && result.continuousStatus) {
2535
+ const { formatContinuousStatus } = require('../src/continuous-ops');
2536
+ console.log('');
2537
+ console.log(formatContinuousStatus(result.continuousStatus));
2538
+ console.log('');
2539
+ }
2540
+ }
2541
+ if (options.out) {
2542
+ const fs = require('fs');
2543
+ const path = require('path');
2544
+ const outPath = path.resolve(options.out);
2545
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
2119
2546
  fs.writeFileSync(outPath, JSON.stringify(result, null, 2), 'utf8');
2120
2547
  if (!options.json) {
2121
2548
  console.log(`\n Audit report written to ${options.out}\n`);
@@ -2123,20 +2550,19 @@ async function main() {
2123
2550
  }
2124
2551
  if (options.webhookUrl) {
2125
2552
  try {
2126
- const { sendWebhook, formatSlackMessage } = require('../src/integrations');
2553
+ const { sendWebhook, formatSlackMessage, formatGenericAuditWebhookEvent } = require('../src/integrations');
2127
2554
  // Auto-detect Slack vs generic by URL pattern
2128
2555
  const isSlack = options.webhookUrl.includes('hooks.slack.com');
2129
2556
  const isDiscord = options.webhookUrl.includes('discord.com/api/webhooks');
2130
2557
  let payload;
2131
- if (isSlack) {
2132
- payload = formatSlackMessage(result);
2133
- } else if (isDiscord) {
2134
- const { formatDiscordMessage } = require('../src/integrations');
2135
- payload = formatDiscordMessage(result);
2136
- } else {
2137
- // Generic webhook: send full JSON audit result
2138
- payload = { platform: result.platform, score: result.score, passed: result.passed, failed: result.failed, results: result.results };
2139
- }
2558
+ if (isSlack) {
2559
+ payload = formatSlackMessage(result);
2560
+ } else if (isDiscord) {
2561
+ const { formatDiscordMessage } = require('../src/integrations');
2562
+ payload = formatDiscordMessage(result);
2563
+ } else {
2564
+ payload = formatGenericAuditWebhookEvent(result);
2565
+ }
2140
2566
  const webhookResp = await sendWebhook(options.webhookUrl, payload, {
2141
2567
  headers: options.webhookHeaders,
2142
2568
  retries: options.webhookRetries,
@@ -2178,6 +2604,7 @@ async function main() {
2178
2604
  }
2179
2605
  const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'audit', result, {
2180
2606
  tags: options.snapshotTags,
2607
+ milestone: options.snapshotMilestone,
2181
2608
  sourceCommand: normalizedCommand,
2182
2609
  }) : null;
2183
2610
  if (snapshot && !options.json) {
@@ -2185,18 +2612,27 @@ async function main() {
2185
2612
  console.log(` Snapshot index: ${snapshot.indexPath}`);
2186
2613
  console.log('');
2187
2614
  }
2188
- if (options.threshold !== null && result.score < options.threshold) {
2189
- if (!options.json) {
2190
- console.error(`\n Error: Threshold not met — score ${result.score}/100 is below required ${options.threshold}/100.`);
2191
- console.error(' Why: Your project audit score is lower than the minimum threshold set via --threshold.');
2192
- console.error(' Fix: Run `npx nerviq augment` to see improvement suggestions, then re-audit.');
2615
+ if (options.threshold !== null && result.score < options.threshold) {
2616
+ if (!options.json) {
2617
+ console.error(`\n Error: Threshold not met — score ${result.score}/100 is below required ${options.threshold}/100.`);
2618
+ console.error(' Why: Your project audit score is lower than the minimum threshold set via --threshold.');
2619
+ console.error(' Fix: Run `npx nerviq augment` to see improvement suggestions, then re-audit.');
2193
2620
  console.error(' Docs: https://github.com/nerviq/nerviq#ci-integration\n');
2194
- }
2195
- process.exit(1);
2196
- }
2197
- if (options.require && options.require.length > 0) {
2198
- const failedRequired = options.require.filter(key => {
2199
- const check = result.results.find(r => r.key === key);
2621
+ }
2622
+ process.exit(1);
2623
+ }
2624
+ if (result.continuousStatus && result.continuousStatus.gate === 'fail') {
2625
+ if (!options.json) {
2626
+ console.error('\n Error: Continuous drift gate failed.');
2627
+ console.error(` Why: ${result.continuousStatus.gateLabel}.`);
2628
+ console.error(' Fix: review the blocking drift items or add a temporary exception with owner/reason/expiry.');
2629
+ console.error(' Docs: https://github.com/nerviq/nerviq#readme\n');
2630
+ }
2631
+ process.exit(1);
2632
+ }
2633
+ if (options.require && options.require.length > 0) {
2634
+ const failedRequired = options.require.filter(key => {
2635
+ const check = result.results.find(r => r.key === key);
2200
2636
  return !check || check.passed !== true;
2201
2637
  });
2202
2638
  if (failedRequired.length > 0) {