@nerviq/cli 1.10.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 (57) hide show
  1. package/README.md +176 -47
  2. package/bin/cli.js +842 -287
  3. package/package.json +2 -2
  4. package/src/activity.js +225 -59
  5. package/src/adoption-advisor.js +299 -0
  6. package/src/aider/freshness.js +28 -25
  7. package/src/aider/techniques.js +16 -11
  8. package/src/analyze.js +131 -1
  9. package/src/anti-patterns.js +17 -2
  10. package/src/audit.js +197 -96
  11. package/src/behavioral-drift.js +801 -0
  12. package/src/benchmark.js +15 -10
  13. package/src/continuous-ops.js +681 -0
  14. package/src/cost-tracking.js +61 -0
  15. package/src/cursor/techniques.js +17 -12
  16. package/src/deep-review.js +83 -0
  17. package/src/diff-only.js +280 -0
  18. package/src/doctor.js +118 -55
  19. package/src/governance.js +72 -50
  20. package/src/hook-validation.js +342 -0
  21. package/src/index.js +7 -1
  22. package/src/integrations.js +144 -60
  23. package/src/mcp-validation.js +337 -0
  24. package/src/opencode/techniques.js +12 -7
  25. package/src/operating-profile.js +574 -0
  26. package/src/org.js +97 -13
  27. package/src/permission-rules.js +218 -0
  28. package/src/plans.js +192 -8
  29. package/src/platform-change-manifest.js +86 -0
  30. package/src/policy-layers.js +210 -0
  31. package/src/profiles.js +4 -1
  32. package/src/prompt-injection.js +74 -0
  33. package/src/repo-archetype.js +386 -0
  34. package/src/secret-patterns.js +9 -0
  35. package/src/server.js +398 -3
  36. package/src/setup.js +36 -2
  37. package/src/source-urls.js +132 -132
  38. package/src/supplemental-checks.js +13 -12
  39. package/src/techniques/api.js +407 -0
  40. package/src/techniques/automation.js +316 -0
  41. package/src/techniques/compliance.js +257 -0
  42. package/src/techniques/hygiene.js +294 -0
  43. package/src/techniques/instructions.js +243 -0
  44. package/src/techniques/observability.js +226 -0
  45. package/src/techniques/optimization.js +142 -0
  46. package/src/techniques/quality.js +317 -0
  47. package/src/techniques/security.js +237 -0
  48. package/src/techniques/shared.js +443 -0
  49. package/src/techniques/stacks.js +2294 -0
  50. package/src/techniques/tools.js +106 -0
  51. package/src/techniques/workflow.js +413 -0
  52. package/src/techniques.js +78 -5611
  53. package/src/terminology.js +73 -0
  54. package/src/token-estimate.js +35 -0
  55. package/src/watch.js +18 -0
  56. package/src/windsurf/techniques.js +17 -12
  57. package/src/workspace.js +105 -8
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));
@@ -47,7 +48,7 @@ function levenshtein(a, b) {
47
48
  return matrix[a.length][b.length];
48
49
  }
49
50
 
50
- function suggestCommand(input) {
51
+ function suggestCommand(input) {
51
52
  const candidates = [...KNOWN_COMMANDS, ...Object.keys(COMMAND_ALIASES)];
52
53
  let best = null;
53
54
  let bestDistance = Infinity;
@@ -58,9 +59,30 @@ function suggestCommand(input) {
58
59
  bestDistance = distance;
59
60
  }
60
61
  }
61
- return bestDistance <= 3 ? best : null;
62
- }
63
-
62
+ return bestDistance <= 3 ? best : null;
63
+ }
64
+
65
+ function parseNonNegativeIntegerFlag(value, flagName) {
66
+ const parsed = Number(value);
67
+ if (!Number.isInteger(parsed) || parsed < 0) {
68
+ throw new Error(`${flagName} requires a non-negative integer`);
69
+ }
70
+ return parsed;
71
+ }
72
+
73
+ function parseWebhookHeader(rawValue) {
74
+ const separator = rawValue.indexOf(':');
75
+ if (separator <= 0) {
76
+ throw new Error('--webhook-header requires NAME: VALUE');
77
+ }
78
+ const name = rawValue.slice(0, separator).trim();
79
+ const value = rawValue.slice(separator + 1).trim();
80
+ if (!name || !value) {
81
+ throw new Error('--webhook-header requires NAME: VALUE');
82
+ }
83
+ return { name, value };
84
+ }
85
+
64
86
  function parseArgs(rawArgs) {
65
87
  const flags = [];
66
88
  let command = 'audit';
@@ -76,13 +98,27 @@ function parseArgs(rawArgs) {
76
98
  let feedbackEffect = null;
77
99
  let feedbackNotes = null;
78
100
  let feedbackSource = null;
79
- let feedbackScoreDelta = null;
80
- let platform = 'claude';
81
- let format = null;
82
- let port = null;
83
- let workspace = null;
84
- let webhookUrl = null;
85
- let commandSet = false;
101
+ let feedbackScoreDelta = null;
102
+ let platform = 'claude';
103
+ let platformExplicit = false;
104
+ let format = null;
105
+ let port = null;
106
+ let workspace = null;
107
+ let webhookUrl = null;
108
+ let webhookHeaders = [];
109
+ let webhookRetries = null;
110
+ let snapshotTags = [];
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;
86
122
  let extraArgs = [];
87
123
  let convertFrom = null;
88
124
  let convertTo = null;
@@ -98,11 +134,11 @@ function parseArgs(rawArgs) {
98
134
  for (let i = 0; i < rawArgs.length; i++) {
99
135
  const arg = rawArgs[i];
100
136
 
101
- 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 === '--external' || arg === '--team-profile' || arg === '--lang') {
102
- const value = rawArgs[i + 1];
103
- if (!value || value.startsWith('--')) {
104
- throw new Error(`${arg} requires a value`);
105
- }
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') {
138
+ const value = rawArgs[i + 1];
139
+ if (!value || value.startsWith('--')) {
140
+ throw new Error(`${arg} requires a value`);
141
+ }
106
142
  if (arg === '--threshold') threshold = value;
107
143
  if (arg === '--out') out = value;
108
144
  if (arg === '--plan') planFile = value;
@@ -116,20 +152,33 @@ function parseArgs(rawArgs) {
116
152
  if (arg === '--notes') feedbackNotes = value;
117
153
  if (arg === '--source') feedbackSource = value.trim();
118
154
  if (arg === '--score-delta') feedbackScoreDelta = value.trim();
119
- if (arg === '--platform') platform = value.trim().toLowerCase();
155
+ if (arg === '--platform') { platform = value.trim().toLowerCase(); platformExplicit = true; }
120
156
  if (arg === '--format') format = value.trim().toLowerCase();
121
157
  if (arg === '--from') { convertFrom = value.trim(); migrateFrom = value.trim(); }
122
158
  if (arg === '--to') { convertTo = value.trim(); migrateTo = value.trim(); }
123
159
  if (arg === '--port') port = value.trim();
124
- if (arg === '--workspace') workspace = value.trim();
125
- if (arg === '--check-version') checkVersion = value.trim();
126
- if (arg === '--webhook') webhookUrl = value.trim();
127
- if (arg === '--external') external = value.trim();
128
- if (arg === '--team-profile') teamProfile = value.trim();
129
- if (arg === '--lang') lang = value.trim().toLowerCase();
130
- i++;
131
- continue;
132
- }
160
+ if (arg === '--workspace') workspace = value.trim();
161
+ if (arg === '--check-version') checkVersion = value.trim();
162
+ if (arg === '--webhook') webhookUrl = value.trim();
163
+ if (arg === '--webhook-header') webhookHeaders.push(parseWebhookHeader(value));
164
+ if (arg === '--webhook-retries') webhookRetries = parseNonNegativeIntegerFlag(value.trim(), '--webhook-retries');
165
+ if (arg === '--external') external = value.trim();
166
+ if (arg === '--team-profile') teamProfile = value.trim();
167
+ if (arg === '--lang') lang = value.trim().toLowerCase();
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();
179
+ i++;
180
+ continue;
181
+ }
133
182
 
134
183
  if (arg.startsWith('--lang=')) {
135
184
  lang = arg.split('=').slice(1).join('=').trim().toLowerCase();
@@ -141,10 +190,65 @@ function parseArgs(rawArgs) {
141
190
  continue;
142
191
  }
143
192
 
144
- if (arg.startsWith('--external=')) {
145
- external = arg.split('=').slice(1).join('=').trim();
146
- continue;
147
- }
193
+ if (arg.startsWith('--external=')) {
194
+ external = arg.split('=').slice(1).join('=').trim();
195
+ continue;
196
+ }
197
+
198
+ if (arg.startsWith('--tag=')) {
199
+ snapshotTags.push(arg.split('=').slice(1).join('=').trim());
200
+ continue;
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
+ }
148
252
 
149
253
  if (arg === '--repos') {
150
254
  // Collect all following non-flag args as repo paths (supports comma-separated too)
@@ -227,10 +331,11 @@ function parseArgs(rawArgs) {
227
331
  continue;
228
332
  }
229
333
 
230
- if (arg.startsWith('--platform=')) {
231
- platform = arg.split('=').slice(1).join('=').trim().toLowerCase();
232
- continue;
233
- }
334
+ if (arg.startsWith('--platform=')) {
335
+ platform = arg.split('=').slice(1).join('=').trim().toLowerCase();
336
+ platformExplicit = true;
337
+ continue;
338
+ }
234
339
 
235
340
  if (arg.startsWith('--format=')) {
236
341
  format = arg.split('=').slice(1).join('=').trim().toLowerCase();
@@ -247,14 +352,29 @@ function parseArgs(rawArgs) {
247
352
  continue;
248
353
  }
249
354
 
250
- if (arg.startsWith('--check-version=')) {
251
- checkVersion = arg.split('=').slice(1).join('=').trim();
252
- continue;
253
- }
254
-
255
- if (arg.startsWith('--')) {
256
- flags.push(arg);
257
- continue;
355
+ if (arg.startsWith('--check-version=')) {
356
+ checkVersion = arg.split('=').slice(1).join('=').trim();
357
+ continue;
358
+ }
359
+
360
+ if (arg.startsWith('--webhook=')) {
361
+ webhookUrl = arg.split('=').slice(1).join('=').trim();
362
+ continue;
363
+ }
364
+
365
+ if (arg.startsWith('--webhook-header=')) {
366
+ webhookHeaders.push(parseWebhookHeader(arg.split('=').slice(1).join('=')));
367
+ continue;
368
+ }
369
+
370
+ if (arg.startsWith('--webhook-retries=')) {
371
+ webhookRetries = parseNonNegativeIntegerFlag(arg.split('=').slice(1).join('=').trim(), '--webhook-retries');
372
+ continue;
373
+ }
374
+
375
+ if (arg.startsWith('--')) {
376
+ flags.push(arg);
377
+ continue;
258
378
  }
259
379
 
260
380
  if (!commandSet) {
@@ -268,7 +388,7 @@ function parseArgs(rawArgs) {
268
388
 
269
389
  const normalizedCommand = COMMAND_ALIASES[command] || command;
270
390
 
271
- 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, external, repos, teamProfile, lang };
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 };
272
392
  }
273
393
 
274
394
  function printWorkspaceSummary(summary, options) {
@@ -291,30 +411,54 @@ function printWorkspaceSummary(summary, options) {
291
411
  }
292
412
  console.log(` Root governance audit: \x1b[1m${rootScore}\x1b[0m`);
293
413
  console.log(` Workspace audit average: \x1b[1m${workspaceAverage}/100\x1b[0m`);
414
+ if (summary.profileBreakdown?.length > 0) {
415
+ const profileLine = summary.profileBreakdown
416
+ .map((item) => `${item.profileLabel} (${item.workspaceCount})`)
417
+ .join(', ');
418
+ console.log(` Workspace profiles: ${profileLine}`);
419
+ }
294
420
  console.log(' Score semantics: root governance shows shared repo policy health; workspace average shows package-level coverage across the selected workspaces.');
295
421
  console.log(' Aggregate vs package: per-workspace scores can legitimately trail the root repo score in a monorepo.');
422
+ console.log(' Stack-specific checks: Go, Python, Node, and other workspace types can have different applicable totals.');
296
423
  console.log('');
297
- console.log('\x1b[1m Workspace Audit Pass Total Top action\x1b[0m');
298
- console.log(' ' + '─'.repeat(72));
424
+ console.log('\x1b[1m Workspace Profile Audit Pass Total Top action\x1b[0m');
425
+ console.log(' ' + '─'.repeat(96));
299
426
  for (const item of summary.workspaces) {
300
427
  const score = item.score === null ? 'ERR' : String(item.score);
301
- const topAction = item.error || item.topAction || '-';
302
- console.log(` ${item.workspace.padEnd(26)} ${score.padStart(5)} ${String(item.passed).padStart(5)} ${String(item.total).padStart(6)} ${topAction}`);
303
- }
304
- console.log('');
305
- }
306
-
307
- function printScanDetail(summary, options) {
308
- if (options.json) {
309
- console.log(JSON.stringify(summary, null, 2));
310
- return;
428
+ const topAction = item.error || item.topAction || '-';
429
+ const profile = (item.workspaceProfile?.label || 'General workspace').slice(0, 20);
430
+ console.log(` ${item.workspace.padEnd(26)} ${profile.padEnd(20)} ${score.padStart(5)} ${String(item.passed).padStart(5)} ${String(item.total).padStart(6)} ${topAction}`);
431
+ if (item.stackLabels?.length > 0) {
432
+ console.log(`\x1b[2m Stacks: ${item.stackLabels.join(', ')}\x1b[0m`);
433
+ }
434
+ }
435
+ console.log('');
436
+ }
437
+
438
+ function printCompareCheckSection(title, items, prefix) {
439
+ if (!Array.isArray(items) || items.length === 0) return;
440
+ console.log(` ${title} (${items.length}):`);
441
+ for (const item of items) {
442
+ const impact = item.impact ? ` [${item.impact}]` : '';
443
+ const category = item.category ? ` — ${item.category}` : '';
444
+ console.log(` ${prefix} ${item.key}${impact}: ${item.name}${category}`);
445
+ }
446
+ }
447
+
448
+ function printScanDetail(summary, options) {
449
+ if (options.json) {
450
+ console.log(JSON.stringify(summary, null, 2));
451
+ return;
311
452
  }
312
453
 
313
- console.log('');
314
- console.log('\x1b[1m nerviq scan — per-repo comparison\x1b[0m');
315
- console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
316
- console.log(` Platform: ${summary.platform} | Repos: ${summary.repoCount} | Average: \x1b[1m${summary.averageScore}/100\x1b[0m`);
317
- 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('');
318
462
 
319
463
  for (const item of summary.repos) {
320
464
  if (item.error) {
@@ -323,9 +467,12 @@ function printScanDetail(summary, options) {
323
467
  continue;
324
468
  }
325
469
  const scoreColor = item.score >= 80 ? '\x1b[32m' : item.score >= 50 ? '\x1b[33m' : '\x1b[31m';
326
- console.log(` \x1b[1m${item.name}\x1b[0m ${scoreColor}${item.score}/100\x1b[0m (${item.passed}/${item.total} checks passed)`);
327
-
328
- // 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
329
476
  if (item.result && item.result.results) {
330
477
  const STACK_LANGUAGES = new Set(['python', 'go', 'rust', 'java', 'ruby', 'dotnet', 'php', 'flutter', 'swift', 'kotlin']);
331
478
  const categories = {};
@@ -354,28 +501,57 @@ function printScanDetail(summary, options) {
354
501
  }
355
502
  }
356
503
 
357
- function printOrgSummary(summary, options) {
358
- if (options.json) {
359
- console.log(JSON.stringify(summary, null, 2));
360
- return;
361
- }
504
+ function printOrgSummary(summary, options) {
505
+ if (options.json) {
506
+ console.log(JSON.stringify(summary, null, 2));
507
+ return;
508
+ }
362
509
 
363
510
  console.log('');
364
- console.log('\x1b[1m nerviq org scan\x1b[0m');
365
- console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
366
- console.log(` Platform: ${summary.platform}`);
367
- console.log(` Repos: ${summary.repoCount}`);
368
- console.log(` Average score: \x1b[1m${summary.averageScore}/100\x1b[0m`);
369
- console.log('');
370
- console.log('\x1b[1m Repo Platform Score Top action\x1b[0m');
371
- console.log(' ' + '─'.repeat(72));
372
- for (const item of summary.repos) {
373
- const score = item.score === null ? 'ERR' : String(item.score);
374
- const topAction = item.error || item.topAction || '-';
375
- console.log(` ${item.name.padEnd(18)} ${item.platform.padEnd(8)} ${score.padStart(5)} ${topAction}`);
376
- }
377
- console.log('');
378
- }
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
+ }
379
555
 
380
556
  const HELP = `
381
557
  nerviq v${version}
@@ -388,20 +564,23 @@ const HELP = `
388
564
  nerviq audit --full Full audit with all checks, weakest areas, badge
389
565
  nerviq audit --platform X Audit specific platform (claude|codex|cursor|copilot|gemini|windsurf|aider|opencode)
390
566
  nerviq audit --json Machine-readable JSON output (for CI)
391
- nerviq audit --workspace packages/* Audit monorepo workspaces with root-vs-package score semantics
392
- nerviq scan dir1 dir2 Compare multiple repos side-by-side
393
- nerviq org scan dir1 dir2 Aggregate multiple repos into one score table
567
+ nerviq audit --workspace packages/* Audit monorepo workspaces with stack-specific package profiles
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
394
571
  nerviq catalog Full check catalog (all 8 platforms)
395
572
  nerviq catalog --json Export full check catalog as JSON
396
573
  nerviq anti-patterns Detect anti-patterns in current project
397
574
  nerviq anti-patterns --all Show full anti-pattern catalog
398
575
 
399
- SETUP
400
- nerviq setup Generate starter-safe baseline config files
401
- nerviq setup --auto Apply all generated files without prompts
402
- nerviq interactive Step-by-step guided wizard
403
- nerviq check-health Detect regressions + platform format changes between snapshots
404
- 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
405
584
 
406
585
  FIX
407
586
  nerviq fix Show fixable checks and manual-fix guidance
@@ -414,16 +593,18 @@ const HELP = `
414
593
  nerviq rollback --list Show available rollback points
415
594
  nerviq rollback --dry-run Preview what would be deleted
416
595
 
417
- IMPROVE
418
- nerviq augment Improvement plan (no writes)
419
- nerviq suggest-only Structured report for sharing (no writes)
420
- nerviq plan Export proposal bundles with diffs
421
- nerviq plan --out plan.json Save plan to file
422
- nerviq apply Apply proposals selectively with rollback
423
- 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
424
605
 
425
606
  GOVERN
426
- nerviq governance Permission profiles + hooks + policy packs
607
+ nerviq governance Permission profiles + hooks + policy packs (the rollout safety layer)
427
608
  nerviq governance --json Machine-readable governance summary
428
609
  nerviq benchmark Baseline vs projected score in isolated temp copy
429
610
  nerviq benchmark --external /path Benchmark an external repo
@@ -441,16 +622,24 @@ const HELP = `
441
622
  nerviq migrate --platform X Platform version migration helper
442
623
  nerviq migrate --platform cursor --from v2 --to v3
443
624
 
444
- MONITOR
625
+ MONITOR
445
626
  nerviq dashboard Generate static dashboard from latest audit snapshot (or live audit if none)
446
- nerviq dashboard --out F Save dashboard to custom file
447
- nerviq dashboard --open Open dashboard in browser after generating
448
- 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
449
631
  nerviq history Audit snapshot history from saved snapshots
450
- nerviq compare Latest vs previous audit snapshot diff
632
+ nerviq compare Detailed per-check diff between latest two audit snapshots
451
633
  nerviq trend Audit snapshot trend over time
452
- nerviq trend --out report.md Export trend report as markdown
453
- nerviq feedback Record recommendation outcomes
634
+ nerviq trend --out report.md Export trend report as markdown
635
+ nerviq audit --snapshot --milestone baseline --tag "baseline" Save a lifecycle checkpoint
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
454
643
 
455
644
  TEAM PROFILES
456
645
  nerviq profile save <name> Save current preferences as a named profile
@@ -459,8 +648,9 @@ const HELP = `
459
648
  nerviq profile export <name> Export profile JSON for sharing
460
649
 
461
650
  ADVANCED
462
- nerviq deep-review AI-powered config review (opt-in, uses API key)
463
- nerviq serve --port 3000 Start local Nerviq REST API server
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)
653
+ nerviq serve --port 3000 Start local Nerviq REST API server + OpenAPI contract
464
654
  nerviq badge Generate shields.io badge markdown
465
655
  nerviq rules-export Export recommendation rules as JSON
466
656
  nerviq rules-export --out F Save rules to file
@@ -475,15 +665,24 @@ const HELP = `
475
665
  --only A,B Limit plan/apply to selected proposal IDs
476
666
  --profile NAME Permission profile: read-only | suggest-only | safe-write | power-user
477
667
  --team-profile N Load a saved team profile for audit (overrides threshold/platform)
478
- --mcp-pack A,B Merge MCP packs into setup (e.g. context7-docs,next-devtools)
479
- --check-version V Pin catalog to a specific version (warn on mismatch)
480
- --format NAME Output format: json | sarif | otel
481
- --webhook URL Send audit results to a webhook (Slack/Discord/generic JSON)
482
- --external PATH Benchmark an external repo instead of cwd
483
- --port N Port for \`serve\` (default: 3000)
484
- --workspace GLOBS Audit workspaces separately and label root governance vs package scores
485
- --snapshot Save snapshot artifact under .claude/nerviq/snapshots/
486
- --full Show full audit output (all checks, weakest areas, badge)
668
+ --mcp-pack A,B Merge MCP packs into setup (live tool connectors; e.g. context7-docs,next-devtools)
669
+ --check-version V Pin catalog to a specific version (warn on mismatch)
670
+ --format NAME Output format: json | sarif | otel
671
+ --webhook URL Send audit results to a webhook (Slack/Discord/generic JSON)
672
+ --webhook-header H Add a custom webhook header (repeat; format: Name: Value)
673
+ --webhook-retries N Retry transient webhook failures N times (default: 2)
674
+ --external PATH Benchmark an external repo instead of cwd
675
+ --port N Port for \`serve\` (default: 3000)
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)
681
+ --snapshot Save snapshot artifact under .claude/nerviq/snapshots/
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
685
+ --full Show full audit output (all checks, weakest areas, badge)
487
686
  --lite Short top-3 scan (default behavior since v1.5.2)
488
687
  --dry-run Preview changes without writing files
489
688
  --config-only Only write config files (.claude/, rules, hooks) — never source code
@@ -493,26 +692,38 @@ const HELP = `
493
692
  --auto Apply all generated files without prompting
494
693
  --beginner Show only the 5 starter commands for first-time users
495
694
  --key NAME Feedback: recommendation key (e.g. permissionDeny)
496
- --status VALUE Feedback: accepted | rejected | deferred
497
- --effect VALUE Feedback: positive | neutral | negative
498
- --score-delta N Feedback: observed score delta
499
- --help Show this help
500
- --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
501
708
 
502
709
  EXAMPLES
503
710
  npx nerviq --beginner
504
711
  npx nerviq
505
712
  npx nerviq --lite
506
713
  npx nerviq --platform cursor
507
- npx nerviq audit --workspace packages/*
508
- npx nerviq --platform codex augment
509
- 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
510
720
  npx nerviq scan ./app ./api ./infra
511
721
  npx nerviq harmony-audit
512
722
  npx nerviq convert --from claude --to codex
513
723
  npx nerviq migrate --platform cursor --from v2 --to v3
514
- npx nerviq setup --mcp-pack context7-docs
515
- 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
516
727
  npx nerviq serve --port 4000
517
728
  npx nerviq --json --threshold 70
518
729
  npx nerviq catalog --json --out catalog.json
@@ -534,7 +745,7 @@ const BEGINNER_HELP = `
534
745
  nerviq setup Generate a starter-safe baseline
535
746
  nerviq fix Fix what can be fixed or show manual fix guidance
536
747
  nerviq augment Show an improvement plan without writing
537
- nerviq doctor Check install health, freshness, and platform detection
748
+ nerviq doctor Check install health, freshness, platform detection, MCP, and hook runtime
538
749
 
539
750
  SIMPLE PATH
540
751
  1. nerviq audit
@@ -579,7 +790,7 @@ async function main() {
579
790
  process.exit(0);
580
791
  }
581
792
 
582
- const options = {
793
+ const options = {
583
794
  verbose: flags.includes('--verbose'),
584
795
  json: flags.includes('--json'),
585
796
  auto: flags.includes('--auto'),
@@ -598,28 +809,82 @@ async function main() {
598
809
  only: parsed.only,
599
810
  profile: parsed.profile,
600
811
  mcpPacks: parsed.mcpPacks,
601
- require: parsed.requireChecks,
602
- platform: parsed.platform || 'claude',
603
- format: parsed.format || null,
604
- port: parsed.port !== null ? Number(parsed.port) : null,
605
- workspace: parsed.workspace || null,
606
- webhookUrl: parsed.webhookUrl || null,
607
- lang: parsed.lang || null,
608
- external: parsed.external || null,
609
- dir: process.cwd()
610
- };
611
-
612
- if (parsed.checkVersion) {
812
+ require: parsed.requireChecks,
813
+ platform: parsed.platform || 'claude',
814
+ platformExplicit: Boolean(parsed.platformExplicit),
815
+ format: parsed.format || null,
816
+ port: parsed.port !== null ? Number(parsed.port) : null,
817
+ workspace: parsed.workspace || null,
818
+ webhookUrl: parsed.webhookUrl || null,
819
+ webhookHeaders: Object.fromEntries((parsed.webhookHeaders || []).map((entry) => [entry.name, entry.value])),
820
+ webhookRetries: parsed.webhookRetries ?? 2,
821
+ lang: parsed.lang || null,
822
+ external: parsed.external || null,
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,
838
+ dir: process.cwd()
839
+ };
840
+
841
+ if (options.snapshotTags.length > 0 && !options.snapshot) {
842
+ console.error('\n Error: --tag requires --snapshot.\n');
843
+ process.exit(1);
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
+ }
865
+
866
+ if (parsed.checkVersion) {
613
867
  if (parsed.checkVersion !== version) {
614
868
  console.error(`\n Warning: --check-version ${parsed.checkVersion} does not match installed nerviq version ${version}.`);
615
869
  console.error(` Check catalog may differ between versions. To align, run: npm install @nerviq/cli@${parsed.checkVersion}`);
616
870
  console.error('');
617
871
  }
618
- options.checkVersion = parsed.checkVersion;
619
- }
620
-
621
- if (parsed.teamProfile) {
622
- 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');
623
888
  try {
624
889
  const teamProf = loadProfile(options.dir, parsed.teamProfile);
625
890
  const merged = applyProfileToOptions(teamProf, options);
@@ -658,10 +923,15 @@ async function main() {
658
923
  process.exit(1);
659
924
  }
660
925
 
661
- if (options.format !== null && !['json', 'sarif', 'otel'].includes(options.format)) {
662
- console.error(`\n Error: Unsupported format '${options.format}'. Use 'json', 'sarif', or 'otel'.\n`);
663
- process.exit(1);
664
- }
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
+ }
665
935
 
666
936
  if (options.port !== null && (!Number.isInteger(options.port) || options.port < 0 || options.port > 65535)) {
667
937
  console.error('\n Error: --port must be an integer between 0 and 65535.\n');
@@ -711,13 +981,13 @@ async function main() {
711
981
  }
712
982
 
713
983
  try {
714
- const FULL_COMMAND_SET = new Set([
715
- 'audit', 'org', 'scan', 'badge', 'augment', 'suggest-only', 'setup', 'plan', 'apply',
716
- 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'insights',
717
- 'history', 'compare', 'trend', 'feedback', 'catalog', 'certify', 'serve', 'help', 'version',
718
- // Harmony + Synergy (cross-platform)
719
- 'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise',
720
- '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',
721
991
  'freshness', 'profile', 'migrate',
722
992
  ]);
723
993
 
@@ -763,33 +1033,51 @@ async function main() {
763
1033
  }
764
1034
  }
765
1035
 
766
- if (normalizedCommand === 'scan') {
767
- const scanDirs = parsed.extraArgs;
768
- if (scanDirs.length === 0) {
769
- console.error('\n Error: scan requires at least one directory argument.');
770
- console.error(' Usage: npx nerviq scan dir1 dir2 dir3\n');
771
- process.exit(1);
772
- }
773
- const summary = await scanOrg(scanDirs, options.platform);
774
- printScanDetail(summary, options);
775
- if (options.threshold !== null && summary.averageScore < options.threshold) {
776
- process.exit(1);
777
- }
778
- process.exit(0);
779
- } else if (normalizedCommand === 'org') {
780
- const subcommand = parsed.extraArgs[0];
781
- const scanDirs = parsed.extraArgs.slice(1);
782
- if (subcommand !== 'scan' || scanDirs.length === 0) {
783
- console.error('\n Error: org requires the scan subcommand and at least one directory.');
784
- console.error(' Usage: npx nerviq org scan dir1 dir2 dir3\n');
785
- process.exit(1);
786
- }
787
- const summary = await scanOrg(scanDirs, options.platform);
788
- printOrgSummary(summary, options);
789
- if (options.threshold !== null && summary.averageScore < options.threshold) {
790
- process.exit(1);
791
- }
792
- 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);
793
1081
  } else if (normalizedCommand === 'history') {
794
1082
  const { formatHistory, readSnapshotIndex } = require('../src/activity');
795
1083
  // Handle --prune N
@@ -820,7 +1108,7 @@ async function main() {
820
1108
  console.log('');
821
1109
  process.exit(0);
822
1110
  } else if (normalizedCommand === 'compare') {
823
- const { compareLatest, formatSnapshotBootstrap } = require('../src/activity');
1111
+ const { compareLatest, formatSnapshotBootstrap, formatSnapshotTags, formatSnapshotMilestone } = require('../src/activity');
824
1112
  const result = compareLatest(options.dir);
825
1113
  if (!result) {
826
1114
  console.log('');
@@ -830,16 +1118,41 @@ async function main() {
830
1118
  }
831
1119
  if (options.json) {
832
1120
  console.log(JSON.stringify(result, null, 2));
833
- } else {
834
- const sign = result.delta.score >= 0 ? '+' : '';
835
- console.log('');
836
- console.log(` Previous snapshot: ${result.previous.score}/100 (${result.previous.date?.split('T')[0]})`);
837
- console.log(` Current snapshot: ${result.current.score}/100 (${result.current.date?.split('T')[0]})`);
1121
+ } else {
1122
+ const sign = result.delta.score >= 0 ? '+' : '';
1123
+ console.log('');
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)}`);
838
1126
  console.log(` Snapshot delta: ${sign}${result.delta.score} points`);
839
1127
  console.log(` Trend: ${result.trend}`);
840
- if (result.improvements.length > 0) console.log(` Fixed: ${result.improvements.join(', ')}`);
841
- if (result.regressions.length > 0) console.log(` New gaps: ${result.regressions.join(', ')}`);
842
- console.log('');
1128
+ if (result.detailedDiffAvailable) {
1129
+ console.log('');
1130
+ console.log(' Detailed check diff:');
1131
+ printCompareCheckSection('Regressions', result.regressionDetails, '🔴');
1132
+ printCompareCheckSection('Improvements', result.improvementDetails, '✅');
1133
+ printCompareCheckSection('Newly applicable', result.newlyApplicableDetails, '🆕');
1134
+ printCompareCheckSection('No longer applicable', result.noLongerApplicableDetails, '↩');
1135
+ if (Array.isArray(result.newChecks) && result.newChecks.length > 0) {
1136
+ printCompareCheckSection('New checks', result.newChecks, '➕');
1137
+ }
1138
+ if (Array.isArray(result.removedChecks) && result.removedChecks.length > 0) {
1139
+ printCompareCheckSection('Removed checks', result.removedChecks, '➖');
1140
+ }
1141
+ if (
1142
+ result.regressionDetails.length === 0 &&
1143
+ result.improvementDetails.length === 0 &&
1144
+ result.newlyApplicableDetails.length === 0 &&
1145
+ result.noLongerApplicableDetails.length === 0 &&
1146
+ result.newChecks.length === 0 &&
1147
+ result.removedChecks.length === 0
1148
+ ) {
1149
+ console.log(' No per-check state changes detected.');
1150
+ }
1151
+ } else {
1152
+ if (result.improvements.length > 0) console.log(` Fixed: ${result.improvements.join(', ')}`);
1153
+ if (result.regressions.length > 0) console.log(` New gaps: ${result.regressions.join(', ')}`);
1154
+ }
1155
+ console.log('');
843
1156
  }
844
1157
  process.exit(0);
845
1158
  } else if (normalizedCommand === 'trend') {
@@ -959,9 +1272,11 @@ async function main() {
959
1272
  process.exit(0);
960
1273
  } else if (normalizedCommand === 'augment' || normalizedCommand === 'suggest-only') {
961
1274
  const report = await analyzeProject({ ...options, mode: normalizedCommand });
962
- const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, normalizedCommand, report, {
963
- sourceCommand: normalizedCommand,
964
- }) : null;
1275
+ const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, normalizedCommand, report, {
1276
+ tags: options.snapshotTags,
1277
+ milestone: options.snapshotMilestone,
1278
+ sourceCommand: normalizedCommand,
1279
+ }) : null;
965
1280
  if (options.out && !options.json) {
966
1281
  const fs = require('fs');
967
1282
  const md = exportMarkdown(report);
@@ -1092,9 +1407,11 @@ async function main() {
1092
1407
  fs.writeFileSync(options.out, content, 'utf8');
1093
1408
  }
1094
1409
  printGovernanceSummary(summary, options);
1095
- const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'governance', summary, {
1096
- sourceCommand: normalizedCommand,
1097
- }) : null;
1410
+ const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'governance', summary, {
1411
+ tags: options.snapshotTags,
1412
+ milestone: options.snapshotMilestone,
1413
+ sourceCommand: normalizedCommand,
1414
+ }) : null;
1098
1415
  if (options.out && !options.json) {
1099
1416
  console.log(` Governance report written to ${options.out}`);
1100
1417
  console.log('');
@@ -1106,9 +1423,11 @@ async function main() {
1106
1423
  }
1107
1424
  } else if (normalizedCommand === 'benchmark') {
1108
1425
  const report = await runBenchmark(options);
1109
- const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'benchmark', report, {
1110
- sourceCommand: normalizedCommand,
1111
- }) : null;
1426
+ const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'benchmark', report, {
1427
+ tags: options.snapshotTags,
1428
+ milestone: options.snapshotMilestone,
1429
+ sourceCommand: normalizedCommand,
1430
+ }) : null;
1112
1431
  if (options.out) {
1113
1432
  writeBenchmarkReport(report, options.out);
1114
1433
  }
@@ -1125,13 +1444,87 @@ async function main() {
1125
1444
  } else if (normalizedCommand === 'deep-review') {
1126
1445
  const { deepReview } = require('../src/deep-review');
1127
1446
  await deepReview(options);
1128
- } else if (normalizedCommand === 'interactive') {
1129
- const { interactive } = require('../src/interactive');
1130
- await interactive(options);
1131
- } else if (normalizedCommand === 'watch') {
1132
- const { watch } = require('../src/watch');
1133
- await watch(options);
1134
- } 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') {
1135
1528
  const { generateCatalogWithVersion, writeCatalogJson } = require('../src/catalog');
1136
1529
  if (options.out) {
1137
1530
  const result = writeCatalogJson(options.out);
@@ -1202,9 +1595,10 @@ async function main() {
1202
1595
  const address = server.address();
1203
1596
  const resolvedPort = address && typeof address === 'object' ? address.port : options.port;
1204
1597
  console.log('');
1205
- console.log(` nerviq API listening on http://127.0.0.1:${resolvedPort}`);
1206
- console.log(' Endpoints: /api/health, /api/catalog, /api/audit, /api/harmony');
1207
- console.log('');
1598
+ console.log(` nerviq API listening on http://127.0.0.1:${resolvedPort}`);
1599
+ console.log(' Endpoints: /api/openapi.json, /api/health, /api/catalog, /api/audit, /api/harmony');
1600
+ console.log(` Contract: http://127.0.0.1:${resolvedPort}/api/openapi.json`);
1601
+ console.log('');
1208
1602
 
1209
1603
  const closeServer = () => {
1210
1604
  server.close(() => process.exit(0));
@@ -1429,18 +1823,74 @@ async function main() {
1429
1823
  }
1430
1824
  }
1431
1825
  process.exit(0);
1432
- } else if (normalizedCommand === 'suggest-rules') {
1433
- const { analyzeSuggestions, formatSuggestions } = require('../src/auto-suggest');
1434
- const suggestions = analyzeSuggestions(options.dir);
1435
- if (options.json) {
1436
- console.log(JSON.stringify(suggestions, null, 2));
1437
- } else {
1438
- console.log('');
1439
- console.log(formatSuggestions(suggestions));
1440
- console.log('');
1441
- }
1442
- process.exit(0);
1443
- } 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') {
1444
1894
  const { saveProfile, loadProfile, listProfiles, exportProfile, formatProfileList, formatProfile } = require('../src/profiles');
1445
1895
  const subcommand = parsed.extraArgs[0];
1446
1896
  const profileArg = parsed.extraArgs[1];
@@ -1982,30 +2432,117 @@ async function main() {
1982
2432
  process.exit(0);
1983
2433
  } else if (normalizedCommand === 'setup') {
1984
2434
  await setup(options);
1985
- if (options.snapshot) {
1986
- const postSetupResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
1987
- const snapshot = writeSnapshotArtifact(options.dir, 'audit', postSetupResult, {
1988
- sourceCommand: 'setup',
1989
- });
1990
- if (!options.json) {
1991
- console.log(` Snapshot saved: ${snapshot.relativePath}`);
1992
- }
1993
- }
1994
- } else {
1995
- if (options.workspace) {
1996
- const summary = await auditWorkspaces(options.dir, options.workspace, options.platform);
1997
- printWorkspaceSummary(summary, options);
1998
- if (options.threshold !== null && summary.averageScore < options.threshold) {
1999
- process.exit(1);
2000
- }
2001
- process.exit(0);
2435
+ if (options.snapshot) {
2436
+ const postSetupResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
2437
+ const snapshot = writeSnapshotArtifact(options.dir, 'audit', postSetupResult, {
2438
+ tags: options.snapshotTags,
2439
+ milestone: options.snapshotMilestone,
2440
+ sourceCommand: 'setup',
2441
+ });
2442
+ if (!options.json) {
2443
+ console.log(` Snapshot saved: ${snapshot.relativePath}`);
2444
+ }
2002
2445
  }
2003
- const result = await audit(options);
2004
- if (options.out) {
2005
- const fs = require('fs');
2006
- const path = require('path');
2007
- const outPath = path.resolve(options.out);
2008
- 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 });
2009
2546
  fs.writeFileSync(outPath, JSON.stringify(result, null, 2), 'utf8');
2010
2547
  if (!options.json) {
2011
2548
  console.log(`\n Audit report written to ${options.out}\n`);
@@ -2013,32 +2550,39 @@ async function main() {
2013
2550
  }
2014
2551
  if (options.webhookUrl) {
2015
2552
  try {
2016
- const { sendWebhook, formatSlackMessage } = require('../src/integrations');
2553
+ const { sendWebhook, formatSlackMessage, formatGenericAuditWebhookEvent } = require('../src/integrations');
2017
2554
  // Auto-detect Slack vs generic by URL pattern
2018
2555
  const isSlack = options.webhookUrl.includes('hooks.slack.com');
2019
2556
  const isDiscord = options.webhookUrl.includes('discord.com/api/webhooks');
2020
2557
  let payload;
2021
- if (isSlack) {
2022
- payload = formatSlackMessage(result);
2023
- } else if (isDiscord) {
2024
- const { formatDiscordMessage } = require('../src/integrations');
2025
- payload = formatDiscordMessage(result);
2026
- } else {
2027
- // Generic webhook: send full JSON audit result
2028
- payload = { platform: result.platform, score: result.score, passed: result.passed, failed: result.failed, results: result.results };
2029
- }
2030
- const webhookResp = await sendWebhook(options.webhookUrl, payload);
2031
- if (!options.json) {
2032
- if (webhookResp.ok) {
2033
- console.log(` Webhook sent: ${options.webhookUrl} (${webhookResp.status})`);
2034
- } else {
2035
- console.error(` Webhook failed: ${webhookResp.status} ${webhookResp.body.slice(0, 200)}`);
2036
- }
2037
- }
2038
- } catch (webhookErr) {
2039
- if (!options.json) console.error(` Webhook error: ${webhookErr.message}`);
2040
- }
2041
- }
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
+ }
2566
+ const webhookResp = await sendWebhook(options.webhookUrl, payload, {
2567
+ headers: options.webhookHeaders,
2568
+ retries: options.webhookRetries,
2569
+ });
2570
+ if (!options.json) {
2571
+ if (webhookResp.ok) {
2572
+ const retryNote = webhookResp.attempts > 1 ? ` after ${webhookResp.attempts} attempts` : '';
2573
+ console.log(` Webhook sent${retryNote}: ${options.webhookUrl} (${webhookResp.status})`);
2574
+ } else {
2575
+ const retryNote = webhookResp.attempts > 1 ? ` after ${webhookResp.attempts} attempts` : '';
2576
+ console.error(` Webhook failed${retryNote}: ${webhookResp.status} — ${webhookResp.body.slice(0, 200)}`);
2577
+ }
2578
+ }
2579
+ } catch (webhookErr) {
2580
+ if (!options.json) {
2581
+ const retryNote = webhookErr.attempts > 1 ? ` after ${webhookErr.attempts} attempts` : '';
2582
+ console.error(` Webhook error${retryNote}: ${webhookErr.message}`);
2583
+ }
2584
+ }
2585
+ }
2042
2586
  if (options.feedback && !options.json && options.format === null) {
2043
2587
  const feedbackTargets = options.lite
2044
2588
  ? (result.liteSummary?.topNextActions || [])
@@ -2058,26 +2602,37 @@ async function main() {
2058
2602
  console.log('');
2059
2603
  }
2060
2604
  }
2061
- const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'audit', result, {
2062
- sourceCommand: normalizedCommand,
2063
- }) : null;
2605
+ const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'audit', result, {
2606
+ tags: options.snapshotTags,
2607
+ milestone: options.snapshotMilestone,
2608
+ sourceCommand: normalizedCommand,
2609
+ }) : null;
2064
2610
  if (snapshot && !options.json) {
2065
2611
  console.log(` Snapshot saved: ${snapshot.relativePath}`);
2066
2612
  console.log(` Snapshot index: ${snapshot.indexPath}`);
2067
2613
  console.log('');
2068
2614
  }
2069
- if (options.threshold !== null && result.score < options.threshold) {
2070
- if (!options.json) {
2071
- console.error(`\n Error: Threshold not met — score ${result.score}/100 is below required ${options.threshold}/100.`);
2072
- console.error(' Why: Your project audit score is lower than the minimum threshold set via --threshold.');
2073
- 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.');
2074
2620
  console.error(' Docs: https://github.com/nerviq/nerviq#ci-integration\n');
2075
- }
2076
- process.exit(1);
2077
- }
2078
- if (options.require && options.require.length > 0) {
2079
- const failedRequired = options.require.filter(key => {
2080
- 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);
2081
2636
  return !check || check.passed !== true;
2082
2637
  });
2083
2638
  if (failedRequired.length > 0) {