@nerviq/cli 1.12.0 → 1.14.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.
package/bin/cli.js CHANGED
@@ -14,9 +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');
19
- const { SNAPSHOT_MILESTONES } = require('../src/activity');
17
+ const { init: initI18n, t } = require('../src/i18n');
18
+ const { version } = require('../package.json');
19
+ const { SNAPSHOT_MILESTONES } = require('../src/activity');
20
20
 
21
21
  const args = process.argv.slice(2);
22
22
  const COMMAND_ALIASES = {
@@ -29,7 +29,7 @@ const COMMAND_ALIASES = {
29
29
  gov: 'governance',
30
30
  outcome: 'feedback',
31
31
  };
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
+ 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-score', 'harmony-demo', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export', 'freshness', 'suggest-rules', 'profile', 'baseline', 'exception', 'help', 'version'];
33
33
 
34
34
  function levenshtein(a, b) {
35
35
  const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
@@ -48,7 +48,7 @@ function levenshtein(a, b) {
48
48
  return matrix[a.length][b.length];
49
49
  }
50
50
 
51
- function suggestCommand(input) {
51
+ function suggestCommand(input) {
52
52
  const candidates = [...KNOWN_COMMANDS, ...Object.keys(COMMAND_ALIASES)];
53
53
  let best = null;
54
54
  let bestDistance = Infinity;
@@ -59,33 +59,33 @@ function suggestCommand(input) {
59
59
  bestDistance = distance;
60
60
  }
61
61
  }
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
-
86
- function parseArgs(rawArgs) {
87
- const flags = [];
88
- let command = 'audit';
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
+
86
+ function parseArgs(rawArgs) {
87
+ const flags = [];
88
+ let command = 'audit';
89
89
  let threshold = null;
90
90
  let out = null;
91
91
  let planFile = null;
@@ -98,47 +98,47 @@ function parseArgs(rawArgs) {
98
98
  let feedbackEffect = null;
99
99
  let feedbackNotes = null;
100
100
  let feedbackSource = null;
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;
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;
122
122
  let extraArgs = [];
123
123
  let convertFrom = null;
124
124
  let convertTo = null;
125
125
  let migrateFrom = null;
126
126
  let migrateTo = null;
127
127
  let checkVersion = null;
128
- let external = null;
129
- let repos = [];
130
- let teamProfile = null;
131
- let lang = null;
132
- let commandExplicit = false;
128
+ let external = null;
129
+ let repos = [];
130
+ let teamProfile = null;
131
+ let lang = null;
132
+ let commandExplicit = false;
133
133
 
134
134
  for (let i = 0; i < rawArgs.length; i++) {
135
135
  const arg = rawArgs[i];
136
136
 
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
- }
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
+ }
142
142
  if (arg === '--threshold') threshold = value;
143
143
  if (arg === '--out') out = value;
144
144
  if (arg === '--plan') planFile = value;
@@ -152,33 +152,33 @@ function parseArgs(rawArgs) {
152
152
  if (arg === '--notes') feedbackNotes = value;
153
153
  if (arg === '--source') feedbackSource = value.trim();
154
154
  if (arg === '--score-delta') feedbackScoreDelta = value.trim();
155
- if (arg === '--platform') { platform = value.trim().toLowerCase(); platformExplicit = true; }
155
+ if (arg === '--platform') { platform = value.trim().toLowerCase(); platformExplicit = true; }
156
156
  if (arg === '--format') format = value.trim().toLowerCase();
157
157
  if (arg === '--from') { convertFrom = value.trim(); migrateFrom = value.trim(); }
158
158
  if (arg === '--to') { convertTo = value.trim(); migrateTo = value.trim(); }
159
159
  if (arg === '--port') port = value.trim();
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
- }
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
+ }
182
182
 
183
183
  if (arg.startsWith('--lang=')) {
184
184
  lang = arg.split('=').slice(1).join('=').trim().toLowerCase();
@@ -190,65 +190,65 @@ function parseArgs(rawArgs) {
190
190
  continue;
191
191
  }
192
192
 
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
- }
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
+ }
252
252
 
253
253
  if (arg === '--repos') {
254
254
  // Collect all following non-flag args as repo paths (supports comma-separated too)
@@ -331,11 +331,11 @@ function parseArgs(rawArgs) {
331
331
  continue;
332
332
  }
333
333
 
334
- if (arg.startsWith('--platform=')) {
335
- platform = arg.split('=').slice(1).join('=').trim().toLowerCase();
336
- platformExplicit = true;
337
- continue;
338
- }
334
+ if (arg.startsWith('--platform=')) {
335
+ platform = arg.split('=').slice(1).join('=').trim().toLowerCase();
336
+ platformExplicit = true;
337
+ continue;
338
+ }
339
339
 
340
340
  if (arg.startsWith('--format=')) {
341
341
  format = arg.split('=').slice(1).join('=').trim().toLowerCase();
@@ -352,113 +352,113 @@ function parseArgs(rawArgs) {
352
352
  continue;
353
353
  }
354
354
 
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;
355
+ if (arg.startsWith('--check-version=')) {
356
+ checkVersion = arg.split('=').slice(1).join('=').trim();
357
+ continue;
378
358
  }
379
359
 
380
- if (!commandSet) {
381
- command = arg;
382
- commandSet = true;
383
- commandExplicit = true;
384
- } else {
385
- extraArgs.push(arg);
386
- }
387
- }
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;
378
+ }
379
+
380
+ if (!commandSet) {
381
+ command = arg;
382
+ commandSet = true;
383
+ commandExplicit = true;
384
+ } else {
385
+ extraArgs.push(arg);
386
+ }
387
+ }
388
388
 
389
389
  const normalizedCommand = COMMAND_ALIASES[command] || command;
390
390
 
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 };
392
- }
393
-
394
- function printWorkspaceSummary(summary, options) {
395
- if (options.json) {
396
- console.log(JSON.stringify(summary, null, 2));
397
- return;
398
- }
399
-
400
- const rootScore = summary.rootGovernance?.score === null ? 'ERR' : `${summary.rootGovernance?.score ?? 0}/100`;
401
- const workspaceAverage = summary.workspaceAggregate?.score ?? summary.averageScore;
402
-
403
- console.log('');
404
- console.log('\x1b[1m nerviq workspace audit\x1b[0m');
405
- console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
406
- console.log(` Root: ${summary.rootDir}`);
407
- console.log(` Platform: ${summary.platform}`);
408
- console.log(` Workspaces: ${summary.workspaceCount}`);
409
- if (summary.patterns?.length > 0) {
410
- console.log(` Selection: ${summary.patterns.join(', ')}`);
411
- }
412
- console.log(` Root governance audit: \x1b[1m${rootScore}\x1b[0m`);
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
- }
420
- console.log(' Score semantics: root governance shows shared repo policy health; workspace average shows package-level coverage across the selected workspaces.');
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.');
423
- console.log('');
424
- console.log('\x1b[1m Workspace Profile Audit Pass Total Top action\x1b[0m');
425
- console.log(' ' + '─'.repeat(96));
426
- for (const item of summary.workspaces) {
427
- const score = item.score === null ? 'ERR' : String(item.score);
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;
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 };
392
+ }
393
+
394
+ function printWorkspaceSummary(summary, options) {
395
+ if (options.json) {
396
+ console.log(JSON.stringify(summary, null, 2));
397
+ return;
398
+ }
399
+
400
+ const rootScore = summary.rootGovernance?.score === null ? 'ERR' : `${summary.rootGovernance?.score ?? 0}/100`;
401
+ const workspaceAverage = summary.workspaceAggregate?.score ?? summary.averageScore;
402
+
403
+ console.log('');
404
+ console.log('\x1b[1m nerviq workspace audit\x1b[0m');
405
+ console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
406
+ console.log(` Root: ${summary.rootDir}`);
407
+ console.log(` Platform: ${summary.platform}`);
408
+ console.log(` Workspaces: ${summary.workspaceCount}`);
409
+ if (summary.patterns?.length > 0) {
410
+ console.log(` Selection: ${summary.patterns.join(', ')}`);
411
+ }
412
+ console.log(` Root governance audit: \x1b[1m${rootScore}\x1b[0m`);
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
+ }
420
+ console.log(' Score semantics: root governance shows shared repo policy health; workspace average shows package-level coverage across the selected workspaces.');
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.');
423
+ console.log('');
424
+ console.log('\x1b[1m Workspace Profile Audit Pass Total Top action\x1b[0m');
425
+ console.log(' ' + '─'.repeat(96));
426
+ for (const item of summary.workspaces) {
427
+ const score = item.score === null ? 'ERR' : String(item.score);
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}`);
452
445
  }
446
+ }
453
447
 
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('');
448
+ function printScanDetail(summary, options) {
449
+ if (options.json) {
450
+ console.log(JSON.stringify(summary, null, 2));
451
+ return;
452
+ }
453
+
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('');
462
462
 
463
463
  for (const item of summary.repos) {
464
464
  if (item.error) {
@@ -467,12 +467,12 @@ function printScanDetail(summary, options) {
467
467
  continue;
468
468
  }
469
469
  const scoreColor = item.score >= 80 ? '\x1b[32m' : item.score >= 50 ? '\x1b[33m' : '\x1b[31m';
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
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
476
476
  if (item.result && item.result.results) {
477
477
  const STACK_LANGUAGES = new Set(['python', 'go', 'rust', 'java', 'ruby', 'dotnet', 'php', 'flutter', 'swift', 'kotlin']);
478
478
  const categories = {};
@@ -501,86 +501,86 @@ function printScanDetail(summary, options) {
501
501
  }
502
502
  }
503
503
 
504
- function printOrgSummary(summary, options) {
505
- if (options.json) {
506
- console.log(JSON.stringify(summary, null, 2));
507
- return;
508
- }
504
+ function printOrgSummary(summary, options) {
505
+ if (options.json) {
506
+ console.log(JSON.stringify(summary, null, 2));
507
+ return;
508
+ }
509
509
 
510
510
  console.log('');
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
- }
555
-
556
- const HELP = `
557
- nerviq v${version}
558
- The intelligent nervous system for AI coding agents.
559
- Audit, align, and amplify every platform on every project.
560
- New here? Run: nerviq --beginner
561
-
562
- DISCOVER
563
- nerviq audit Quick scan: score + top 3 gaps (default)
564
- nerviq audit --full Full audit with all checks, weakest areas, badge
565
- nerviq audit --platform X Audit specific platform (claude|codex|cursor|copilot|gemini|windsurf|aider|opencode)
566
- nerviq audit --json Machine-readable JSON output (for CI)
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
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
+ }
555
+
556
+ const HELP = `
557
+ nerviq v${version}
558
+ The intelligent nervous system for AI coding agents.
559
+ Audit, align, and amplify every platform on every project.
560
+ New here? Run: nerviq --beginner
561
+
562
+ DISCOVER
563
+ nerviq audit Quick scan: score + top 3 gaps (default)
564
+ nerviq audit --full Full audit with all checks, weakest areas, badge
565
+ nerviq audit --platform X Audit specific platform (claude|codex|cursor|copilot|gemini|windsurf|aider|opencode)
566
+ nerviq audit --json Machine-readable JSON output (for CI)
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
571
571
  nerviq catalog Full check catalog (all 8 platforms)
572
572
  nerviq catalog --json Export full check catalog as JSON
573
573
  nerviq anti-patterns Detect anti-patterns in current project
574
574
  nerviq anti-patterns --all Show full anti-pattern catalog
575
575
 
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
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
584
584
 
585
585
  FIX
586
586
  nerviq fix Show fixable checks and manual-fix guidance
@@ -593,53 +593,58 @@ const HELP = `
593
593
  nerviq rollback --list Show available rollback points
594
594
  nerviq rollback --dry-run Preview what would be deleted
595
595
 
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
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
605
605
 
606
606
  GOVERN
607
- nerviq governance Permission profiles + hooks + policy packs (the rollout safety layer)
607
+ nerviq governance Permission profiles + hooks + policy packs (the rollout safety layer)
608
608
  nerviq governance --json Machine-readable governance summary
609
- nerviq benchmark Baseline vs projected score in isolated temp copy
609
+ nerviq benchmark Baseline vs projected score in isolated temp copy
610
610
  nerviq benchmark --external /path Benchmark an external repo
611
611
  nerviq freshness Show verification freshness for all checks
612
612
  nerviq certify Generate certification badge for your project
613
613
 
614
614
  CROSS-PLATFORM
615
- nerviq harmony-audit Drift detection across all active platforms (GA)
616
- nerviq harmony-sync Preview cross-platform sync (dry run, GA)
617
- nerviq harmony-sync --fix Apply cross-platform sync (write files, GA)
618
- nerviq harmony-sync --json JSON output for CI/automation
619
- nerviq harmony-add <platform> Add a new platform to the project
620
- nerviq synergy-report [EXPERIMENTAL] Static-rule multi-agent amplification report
615
+ nerviq harmony-audit Drift detection across all active platforms (GA)
616
+ nerviq harmony-sync Preview cross-platform sync (dry run, GA)
617
+ nerviq harmony-sync --fix Apply cross-platform sync (write files, GA)
618
+ nerviq harmony-sync --json JSON output for CI/automation
619
+ nerviq harmony-score Standalone Harmony Score (0-100) with badge + CI gate
620
+ nerviq harmony-score --badge Include shields.io badge markdown
621
+ nerviq harmony-score --threshold 70 CI gate: exit 1 if score < threshold
622
+ nerviq harmony-score --quiet Score number only (for piping)
623
+ nerviq harmony-demo Zero-setup demo — see Harmony in action instantly
624
+ nerviq harmony-add <platform> Add a new platform to the project
625
+ nerviq synergy-report [EXPERIMENTAL] Static-rule multi-agent amplification report
621
626
  nerviq convert --from X --to Y Convert configs between platforms
622
627
  nerviq migrate --platform X Platform version migration helper
623
628
  nerviq migrate --platform cursor --from v2 --to v3
624
629
 
625
- MONITOR
626
- nerviq dashboard Generate static dashboard from latest audit snapshot (or live audit if none)
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
631
- nerviq history Audit snapshot history from saved snapshots
632
- nerviq compare Detailed per-check diff between latest two audit snapshots
633
- nerviq trend Audit snapshot trend over time
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
630
+ MONITOR
631
+ nerviq dashboard Generate static dashboard from latest audit snapshot (or live audit if none)
632
+ nerviq dashboard --out F Save dashboard to custom file
633
+ nerviq dashboard --open Open dashboard in browser after generating
634
+ nerviq watch Live config monitoring (re-audits on file change)
635
+ nerviq audit --diff-only --drift-mode ci PR / CI drift review against the managed baseline
636
+ nerviq history Audit snapshot history from saved snapshots
637
+ nerviq compare Detailed per-check diff between latest two audit snapshots
638
+ nerviq trend Audit snapshot trend over time
639
+ nerviq trend --out report.md Export trend report as markdown
640
+ nerviq audit --snapshot --milestone baseline --tag "baseline" Save a lifecycle checkpoint
641
+ nerviq feedback Record recommendation outcomes
642
+
643
+ EXCEPTIONS
644
+ nerviq exception add --key permissionDeny --owner team --reason "migration in progress" --expires 2026-05-01
645
+ nerviq exception add --class policy-drift --scope ci --owner team --reason "temporary rollout" --expires 2026-05-01
646
+ nerviq exception list Show active and expired exceptions
647
+ nerviq exception prune Remove expired exceptions
643
648
 
644
649
  TEAM PROFILES
645
650
  nerviq profile save <name> Save current preferences as a named profile
@@ -648,9 +653,9 @@ const HELP = `
648
653
  nerviq profile export <name> Export profile JSON for sharing
649
654
 
650
655
  ADVANCED
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
656
+ nerviq deep-review AI-powered config review (opt-in, uses API key)
657
+ nerviq deep-review --behavioral Local behavioral drift review (opt-in, no API)
658
+ nerviq serve --port 3000 Start local Nerviq HTTP API server + OpenAPI contract
654
659
  nerviq badge Generate shields.io badge markdown
655
660
  nerviq rules-export Export recommendation rules as JSON
656
661
  nerviq rules-export --out F Save rules to file
@@ -665,99 +670,99 @@ const HELP = `
665
670
  --only A,B Limit plan/apply to selected proposal IDs
666
671
  --profile NAME Permission profile: read-only | suggest-only | safe-write | power-user
667
672
  --team-profile N Load a saved team profile for audit (overrides threshold/platform)
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)
673
+ --mcp-pack A,B Merge MCP packs into setup (live tool connectors; e.g. context7-docs,next-devtools)
674
+ --check-version V Pin catalog to a specific version (warn on mismatch)
675
+ --format NAME Output format: json | sarif | otel
676
+ --webhook URL Send audit results to a webhook (Slack/Discord/generic JSON)
677
+ --webhook-header H Add a custom webhook header (repeat; format: Name: Value)
678
+ --webhook-retries N Retry transient webhook failures N times (default: 2)
679
+ --external PATH Benchmark an external repo instead of cwd
680
+ --port N Port for \`serve\` (default: 3000)
681
+ --workspace GLOBS Audit workspaces separately with root/package score semantics and stack-specific profiles
682
+ --diff-only Audit only changed files / linked config surfaces from git diff
683
+ --drift-mode M Continuous posture mode: ci | pr | watch
684
+ --diff-base SHA Base SHA for diff-only mode (defaults to PR env vars when present)
685
+ --diff-head SHA Head SHA for diff-only mode (defaults to GITHUB_SHA or HEAD)
686
+ --snapshot Save snapshot artifact under .claude/nerviq/snapshots/
687
+ --tag LABEL Tag the saved snapshot (use with --snapshot; repeat or comma-separate for more)
688
+ --milestone NAME Snapshot lifecycle milestone: baseline | post-fix | pre-upgrade | release
689
+ --campaign A,B Limit plan/apply to named upgrade campaigns
690
+ --full Show full audit output (all checks, weakest areas, badge)
686
691
  --lite Short top-3 scan (default behavior since v1.5.2)
687
692
  --dry-run Preview changes without writing files
688
693
  --config-only Only write config files (.claude/, rules, hooks) — never source code
689
694
  --verbose Full audit + medium-priority recommendations
690
- --show-deprecated Show deprecated checks (excluded from scoring)
691
- --json Output as JSON
692
- --auto Apply all generated files without prompting
693
- --beginner Show only the 5 starter commands for first-time users
694
- --key NAME Feedback: recommendation key (e.g. permissionDeny)
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
708
-
709
- EXAMPLES
710
- npx nerviq --beginner
711
- npx nerviq
712
- npx nerviq --lite
695
+ --show-deprecated Show deprecated checks (excluded from scoring)
696
+ --json Output as JSON
697
+ --auto Apply all generated files without prompting
698
+ --beginner Show only the 5 starter commands for first-time users
699
+ --key NAME Feedback: recommendation key (e.g. permissionDeny)
700
+ --status VALUE Feedback: accepted | rejected | deferred
701
+ --effect VALUE Feedback: positive | neutral | negative
702
+ --score-delta N Feedback: observed score delta
703
+ --owner NAME Exception owner
704
+ --reason TEXT Exception reason
705
+ --expires DATE Exception expiry (ISO date or date-time)
706
+ --scope NAME Exception scope: all | ci | watch | pr
707
+ --class NAME Exception target class: policy-drift | config-drift | platform-drift | maturity-opportunity
708
+ --behavioral Run the opt-in local behavioral drift / outcome-layer review
709
+ --history With deep-review --behavioral, show behavioral snapshot history
710
+ --compare With deep-review --behavioral, compare the latest two behavioral snapshots
711
+ --help Show this help
712
+ --version Show version
713
+
714
+ EXAMPLES
715
+ npx nerviq --beginner
716
+ npx nerviq
717
+ npx nerviq --lite
713
718
  npx nerviq --platform cursor
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
719
+ npx nerviq audit --workspace packages/*
720
+ npx nerviq baseline init
721
+ npx nerviq audit --diff-only --drift-mode ci
722
+ npx nerviq --platform codex augment
723
+ npx nerviq org scan ./app ./api ./infra
724
+ npx nerviq org policy
720
725
  npx nerviq scan ./app ./api ./infra
721
726
  npx nerviq harmony-audit
722
727
  npx nerviq convert --from claude --to codex
723
728
  npx nerviq migrate --platform cursor --from v2 --to v3
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
729
+ npx nerviq setup --mcp-pack context7-docs
730
+ npx nerviq plan --campaign governance-hardening
731
+ npx nerviq apply --plan plan.json --only hooks,commands
727
732
  npx nerviq serve --port 4000
728
733
  npx nerviq --json --threshold 70
729
734
  npx nerviq catalog --json --out catalog.json
730
735
  npx nerviq feedback --key permissionDeny --status accepted --effect positive
731
736
 
732
737
  EXIT CODES
733
- 0 Success
734
- 1 Error, unknown command, or score below --threshold
735
- `;
736
-
737
- const BEGINNER_HELP = `
738
- nerviq v${version}
739
- Start here.
740
-
741
- If this is your first time, learn just these 5 commands:
742
-
743
- STARTER COMMANDS
744
- nerviq audit Score the repo and show the top gaps
745
- nerviq setup Generate a starter-safe baseline
746
- nerviq fix Fix what can be fixed or show manual fix guidance
747
- nerviq augment Show an improvement plan without writing
748
- nerviq doctor Check install health, freshness, platform detection, MCP, and hook runtime
749
-
750
- SIMPLE PATH
751
- 1. nerviq audit
752
- 2. nerviq setup --auto
753
- 3. nerviq fix --all-critical --auto
754
- 4. nerviq augment
755
- 5. nerviq doctor
756
-
757
- WHEN YOU ARE READY
758
- nerviq --help Show the full command set
759
- Docs: https://nerviq.net/docs/getting-started
760
- `;
738
+ 0 Success
739
+ 1 Error, unknown command, or score below --threshold
740
+ `;
741
+
742
+ const BEGINNER_HELP = `
743
+ nerviq v${version}
744
+ Start here.
745
+
746
+ If this is your first time, learn just these 5 commands:
747
+
748
+ STARTER COMMANDS
749
+ nerviq audit Score the repo and show the top gaps
750
+ nerviq setup Generate a starter-safe baseline
751
+ nerviq fix Fix what can be fixed or show manual fix guidance
752
+ nerviq augment Show an improvement plan without writing
753
+ nerviq doctor Check install health, freshness, platform detection, MCP, and hook runtime
754
+
755
+ SIMPLE PATH
756
+ 1. nerviq audit
757
+ 2. nerviq setup --auto
758
+ 3. nerviq fix --all-critical --auto
759
+ 4. nerviq augment
760
+ 5. nerviq doctor
761
+
762
+ WHEN YOU ARE READY
763
+ nerviq --help Show the full command set
764
+ Docs: https://nerviq.net/docs/getting-started
765
+ `;
761
766
 
762
767
  async function main() {
763
768
  let parsed;
@@ -768,29 +773,29 @@ async function main() {
768
773
  process.exit(1);
769
774
  }
770
775
 
771
- const { flags, command, commandExplicit, normalizedCommand } = parsed;
776
+ const { flags, command, commandExplicit, normalizedCommand } = parsed;
772
777
 
773
778
  // Initialize i18n with --lang flag or NERVIQ_LANG env var
774
779
  if (parsed.lang) {
775
780
  initI18n(parsed.lang);
776
781
  }
777
782
 
778
- if (flags.includes('--version') || command === 'version') {
779
- console.log(version);
780
- process.exit(0);
781
- }
782
-
783
- if (flags.includes('--beginner') && (!commandExplicit || flags.includes('--help') || command === 'help')) {
784
- console.log(BEGINNER_HELP);
785
- process.exit(0);
786
- }
787
-
788
- if (flags.includes('--help') || command === 'help') {
789
- console.log(HELP);
790
- process.exit(0);
791
- }
792
-
793
- const options = {
783
+ if (flags.includes('--version') || command === 'version') {
784
+ console.log(version);
785
+ process.exit(0);
786
+ }
787
+
788
+ if (flags.includes('--beginner') && (!commandExplicit || flags.includes('--help') || command === 'help')) {
789
+ console.log(BEGINNER_HELP);
790
+ process.exit(0);
791
+ }
792
+
793
+ if (flags.includes('--help') || command === 'help') {
794
+ console.log(HELP);
795
+ process.exit(0);
796
+ }
797
+
798
+ const options = {
794
799
  verbose: flags.includes('--verbose'),
795
800
  json: flags.includes('--json'),
796
801
  auto: flags.includes('--auto'),
@@ -800,6 +805,8 @@ async function main() {
800
805
  snapshot: flags.includes('--snapshot'),
801
806
  feedback: flags.includes('--feedback'),
802
807
  fix: flags.includes('--fix'),
808
+ badge: flags.includes('--badge'),
809
+ quiet: flags.includes('--quiet'),
803
810
  autoSync: flags.includes('--auto-sync'),
804
811
  dryRun: flags.includes('--dry-run'),
805
812
  configOnly: flags.includes('--config-only'),
@@ -809,82 +816,82 @@ async function main() {
809
816
  only: parsed.only,
810
817
  profile: parsed.profile,
811
818
  mcpPacks: parsed.mcpPacks,
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) {
819
+ require: parsed.requireChecks,
820
+ platform: parsed.platform || 'claude',
821
+ platformExplicit: Boolean(parsed.platformExplicit),
822
+ format: parsed.format || null,
823
+ port: parsed.port !== null ? Number(parsed.port) : null,
824
+ workspace: parsed.workspace || null,
825
+ webhookUrl: parsed.webhookUrl || null,
826
+ webhookHeaders: Object.fromEntries((parsed.webhookHeaders || []).map((entry) => [entry.name, entry.value])),
827
+ webhookRetries: parsed.webhookRetries ?? 2,
828
+ lang: parsed.lang || null,
829
+ external: parsed.external || null,
830
+ snapshotTags: parsed.snapshotTags || [],
831
+ snapshotMilestone: parsed.snapshotMilestone || null,
832
+ campaigns: parsed.campaigns || [],
833
+ behavioral: flags.includes('--behavioral'),
834
+ historyView: flags.includes('--history'),
835
+ compareView: flags.includes('--compare'),
836
+ diffOnly: flags.includes('--diff-only'),
837
+ diffBase: parsed.diffBase || null,
838
+ diffHead: parsed.diffHead || null,
839
+ driftMode: parsed.driftMode || null,
840
+ exceptionOwner: parsed.exceptionOwner || null,
841
+ exceptionReason: parsed.exceptionReason || null,
842
+ exceptionExpires: parsed.exceptionExpires || null,
843
+ exceptionScope: parsed.exceptionScope || null,
844
+ exceptionClass: parsed.exceptionClass || null,
845
+ dir: process.cwd()
846
+ };
847
+
848
+ if (options.snapshotTags.length > 0 && !options.snapshot) {
849
+ console.error('\n Error: --tag requires --snapshot.\n');
850
+ process.exit(1);
851
+ }
852
+
853
+ if (options.snapshotMilestone && !options.snapshot) {
854
+ console.error('\n Error: --milestone requires --snapshot.\n');
855
+ process.exit(1);
856
+ }
857
+
858
+ if (options.snapshotMilestone && !SNAPSHOT_MILESTONES.includes(options.snapshotMilestone)) {
859
+ console.error(`\n Error: Unsupported milestone '${options.snapshotMilestone}'. Use one of: ${SNAPSHOT_MILESTONES.join(', ')}.\n`);
860
+ process.exit(1);
861
+ }
862
+
863
+ if (options.diffOnly && options.snapshot) {
864
+ console.error('\n Error: --diff-only cannot be combined with --snapshot because diff-only scores are not comparable to full audit snapshots.\n');
865
+ process.exit(1);
866
+ }
867
+
868
+ if (options.driftMode && !['ci', 'pr', 'watch'].includes(options.driftMode)) {
869
+ console.error(`\n Error: Unsupported drift mode '${options.driftMode}'. Use ci, pr, or watch.\n`);
870
+ process.exit(1);
871
+ }
872
+
873
+ if (parsed.checkVersion) {
867
874
  if (parsed.checkVersion !== version) {
868
875
  console.error(`\n Warning: --check-version ${parsed.checkVersion} does not match installed nerviq version ${version}.`);
869
876
  console.error(` Check catalog may differ between versions. To align, run: npm install @nerviq/cli@${parsed.checkVersion}`);
870
877
  console.error('');
871
878
  }
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');
879
+ options.checkVersion = parsed.checkVersion;
880
+ }
881
+
882
+ const {
883
+ resolvePolicyLayers,
884
+ applyPolicyLayersToOptions,
885
+ formatPolicyContract,
886
+ } = require('../src/policy-layers');
887
+ const inheritedPolicyContract = resolvePolicyLayers(options.dir);
888
+ if (inheritedPolicyContract.layers.some((layer) => layer.valid)) {
889
+ Object.assign(options, applyPolicyLayersToOptions(inheritedPolicyContract, options));
890
+ options.policyContract = inheritedPolicyContract;
891
+ }
892
+
893
+ if (parsed.teamProfile) {
894
+ const { loadProfile, applyProfileToOptions } = require('../src/profiles');
888
895
  try {
889
896
  const teamProf = loadProfile(options.dir, parsed.teamProfile);
890
897
  const merged = applyProfileToOptions(teamProf, options);
@@ -923,15 +930,15 @@ async function main() {
923
930
  process.exit(1);
924
931
  }
925
932
 
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
- }
933
+ if (options.format !== null && !['json', 'sarif', 'otel'].includes(options.format)) {
934
+ console.error(`\n Error: Unsupported format '${options.format}'. Use 'json', 'sarif', or 'otel'.\n`);
935
+ process.exit(1);
936
+ }
937
+
938
+ if (options.driftMode && options.format !== null) {
939
+ console.error('\n Error: --drift-mode is only supported with normal text output or --json.\n');
940
+ process.exit(1);
941
+ }
935
942
 
936
943
  if (options.port !== null && (!Number.isInteger(options.port) || options.port < 0 || options.port > 65535)) {
937
944
  console.error('\n Error: --port must be an integer between 0 and 65535.\n');
@@ -981,13 +988,13 @@ async function main() {
981
988
  }
982
989
 
983
990
  try {
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',
991
+ const FULL_COMMAND_SET = new Set([
992
+ 'audit', 'org', 'scan', 'badge', 'augment', 'suggest-only', 'setup', 'plan', 'apply',
993
+ 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'insights',
994
+ 'history', 'compare', 'trend', 'feedback', 'catalog', 'certify', 'serve', 'baseline', 'exception', 'help', 'version',
995
+ // Harmony + Synergy (cross-platform)
996
+ 'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise',
997
+ 'harmony-watch', 'harmony-governance', 'harmony-score', 'harmony-demo', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export',
991
998
  'freshness', 'profile', 'migrate',
992
999
  ]);
993
1000
 
@@ -1033,54 +1040,54 @@ async function main() {
1033
1040
  }
1034
1041
  }
1035
1042
 
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);
1081
- } else if (normalizedCommand === 'history') {
1082
- const { formatHistory, readSnapshotIndex } = require('../src/activity');
1083
- // Handle --prune N
1043
+ if (normalizedCommand === 'scan') {
1044
+ const scanDirs = parsed.extraArgs;
1045
+ if (scanDirs.length === 0) {
1046
+ console.error('\n Error: scan requires at least one directory argument.');
1047
+ console.error(' Usage: npx nerviq scan dir1 dir2 dir3\n');
1048
+ process.exit(1);
1049
+ }
1050
+ const summary = await scanOrg(scanDirs, options);
1051
+ printScanDetail(summary, options);
1052
+ if (options.threshold !== null && summary.averageScore < options.threshold) {
1053
+ process.exit(1);
1054
+ }
1055
+ process.exit(0);
1056
+ } else if (normalizedCommand === 'org') {
1057
+ const subcommand = parsed.extraArgs[0];
1058
+ if (subcommand === 'policy') {
1059
+ const targetDir = parsed.extraArgs[1] ? require('path').resolve(parsed.extraArgs[1]) : options.dir;
1060
+ const contract = resolvePolicyLayers(targetDir);
1061
+ if (options.json) {
1062
+ await writeStdout(JSON.stringify(contract, null, 2) + '\n');
1063
+ } else {
1064
+ console.log('');
1065
+ console.log(formatPolicyContract(contract));
1066
+ console.log('');
1067
+ }
1068
+ process.exit(0);
1069
+ }
1070
+
1071
+ const scanDirs = parsed.extraArgs.slice(1);
1072
+ if (subcommand !== 'scan' || scanDirs.length === 0) {
1073
+ console.error('\n Error: org requires `scan` or `policy`.');
1074
+ console.error(' Usage: npx nerviq org scan dir1 dir2 dir3');
1075
+ console.error(' npx nerviq org policy [dir]\n');
1076
+ process.exit(1);
1077
+ }
1078
+ const summary = await scanOrg(scanDirs, options);
1079
+ if (options.json) {
1080
+ await writeStdout(JSON.stringify(summary, null, 2) + '\n');
1081
+ } else {
1082
+ printOrgSummary(summary, options);
1083
+ }
1084
+ if (options.threshold !== null && summary.averageScore < options.threshold) {
1085
+ process.exit(1);
1086
+ }
1087
+ process.exit(0);
1088
+ } else if (normalizedCommand === 'history') {
1089
+ const { formatHistory, readSnapshotIndex } = require('../src/activity');
1090
+ // Handle --prune N
1084
1091
  const pruneIdx = flags.indexOf('--prune');
1085
1092
  if (pruneIdx >= 0) {
1086
1093
  const keepCount = parseInt(flags[pruneIdx + 1] || parsed.extraArgs[0], 10) || 10;
@@ -1088,7 +1095,7 @@ async function main() {
1088
1095
  const pathMod = require('path');
1089
1096
  const entries = readSnapshotIndex(options.dir);
1090
1097
  if (entries.length <= keepCount) {
1091
- console.log(`\n Nothing to prune (${entries.length} audit snapshots, keeping ${keepCount}).\n`);
1098
+ console.log(`\n Nothing to prune (${entries.length} audit snapshots, keeping ${keepCount}).\n`);
1092
1099
  } else {
1093
1100
  const toRemove = entries.slice(0, entries.length - keepCount);
1094
1101
  let removed = 0;
@@ -1099,78 +1106,78 @@ async function main() {
1099
1106
  const kept = entries.slice(entries.length - keepCount);
1100
1107
  const indexPath = pathMod.join(options.dir, '.nerviq', 'snapshots', 'index.json');
1101
1108
  try { fsMod.writeFileSync(indexPath, JSON.stringify(kept, null, 2), 'utf8'); } catch {}
1102
- console.log(`\n Pruned ${removed} audit snapshots, kept ${kept.length}.\n`);
1109
+ console.log(`\n Pruned ${removed} audit snapshots, kept ${kept.length}.\n`);
1103
1110
  }
1104
1111
  process.exit(0);
1105
1112
  }
1106
1113
  console.log('');
1107
- console.log(formatHistory(options.dir));
1108
- console.log('');
1109
- process.exit(0);
1110
- } else if (normalizedCommand === 'compare') {
1111
- const { compareLatest, formatSnapshotBootstrap, formatSnapshotTags, formatSnapshotMilestone } = require('../src/activity');
1112
- const result = compareLatest(options.dir);
1113
- if (!result) {
1114
- console.log('');
1115
- console.log(formatSnapshotBootstrap(options.dir, 'compare'));
1116
- console.log('');
1117
- process.exit(0);
1118
- }
1114
+ console.log(formatHistory(options.dir));
1115
+ console.log('');
1116
+ process.exit(0);
1117
+ } else if (normalizedCommand === 'compare') {
1118
+ const { compareLatest, formatSnapshotBootstrap, formatSnapshotTags, formatSnapshotMilestone } = require('../src/activity');
1119
+ const result = compareLatest(options.dir);
1120
+ if (!result) {
1121
+ console.log('');
1122
+ console.log(formatSnapshotBootstrap(options.dir, 'compare'));
1123
+ console.log('');
1124
+ process.exit(0);
1125
+ }
1119
1126
  if (options.json) {
1120
1127
  console.log(JSON.stringify(result, null, 2));
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)}`);
1126
- console.log(` Snapshot delta: ${sign}${result.delta.score} points`);
1127
- console.log(` Trend: ${result.trend}`);
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('');
1156
- }
1157
- process.exit(0);
1158
- } else if (normalizedCommand === 'trend') {
1159
- const { exportTrendReport, getHistory, formatSnapshotBootstrap } = require('../src/activity');
1160
- const auditHistory = getHistory(options.dir, 2);
1161
- if (auditHistory.length < 2) {
1162
- console.log('');
1163
- console.log(formatSnapshotBootstrap(options.dir, 'trend'));
1164
- console.log('');
1165
- process.exit(0);
1166
- }
1167
- const report = exportTrendReport(options.dir);
1168
- if (!report) {
1169
- console.log('');
1170
- console.log(formatSnapshotBootstrap(options.dir, 'trend'));
1171
- console.log('');
1172
- process.exit(0);
1173
- }
1128
+ } else {
1129
+ const sign = result.delta.score >= 0 ? '+' : '';
1130
+ console.log('');
1131
+ console.log(` Previous snapshot: ${result.previous.score}/100 (${result.previous.date?.split('T')[0]})${formatSnapshotMilestone(result.previous.milestone)}${formatSnapshotTags(result.previous.tags)}`);
1132
+ console.log(` Current snapshot: ${result.current.score}/100 (${result.current.date?.split('T')[0]})${formatSnapshotMilestone(result.current.milestone)}${formatSnapshotTags(result.current.tags)}`);
1133
+ console.log(` Snapshot delta: ${sign}${result.delta.score} points`);
1134
+ console.log(` Trend: ${result.trend}`);
1135
+ if (result.detailedDiffAvailable) {
1136
+ console.log('');
1137
+ console.log(' Detailed check diff:');
1138
+ printCompareCheckSection('Regressions', result.regressionDetails, '🔴');
1139
+ printCompareCheckSection('Improvements', result.improvementDetails, '✅');
1140
+ printCompareCheckSection('Newly applicable', result.newlyApplicableDetails, '🆕');
1141
+ printCompareCheckSection('No longer applicable', result.noLongerApplicableDetails, '↩');
1142
+ if (Array.isArray(result.newChecks) && result.newChecks.length > 0) {
1143
+ printCompareCheckSection('New checks', result.newChecks, '➕');
1144
+ }
1145
+ if (Array.isArray(result.removedChecks) && result.removedChecks.length > 0) {
1146
+ printCompareCheckSection('Removed checks', result.removedChecks, '➖');
1147
+ }
1148
+ if (
1149
+ result.regressionDetails.length === 0 &&
1150
+ result.improvementDetails.length === 0 &&
1151
+ result.newlyApplicableDetails.length === 0 &&
1152
+ result.noLongerApplicableDetails.length === 0 &&
1153
+ result.newChecks.length === 0 &&
1154
+ result.removedChecks.length === 0
1155
+ ) {
1156
+ console.log(' No per-check state changes detected.');
1157
+ }
1158
+ } else {
1159
+ if (result.improvements.length > 0) console.log(` Fixed: ${result.improvements.join(', ')}`);
1160
+ if (result.regressions.length > 0) console.log(` New gaps: ${result.regressions.join(', ')}`);
1161
+ }
1162
+ console.log('');
1163
+ }
1164
+ process.exit(0);
1165
+ } else if (normalizedCommand === 'trend') {
1166
+ const { exportTrendReport, getHistory, formatSnapshotBootstrap } = require('../src/activity');
1167
+ const auditHistory = getHistory(options.dir, 2);
1168
+ if (auditHistory.length < 2) {
1169
+ console.log('');
1170
+ console.log(formatSnapshotBootstrap(options.dir, 'trend'));
1171
+ console.log('');
1172
+ process.exit(0);
1173
+ }
1174
+ const report = exportTrendReport(options.dir);
1175
+ if (!report) {
1176
+ console.log('');
1177
+ console.log(formatSnapshotBootstrap(options.dir, 'trend'));
1178
+ console.log('');
1179
+ process.exit(0);
1180
+ }
1174
1181
  if (options.out) {
1175
1182
  require('fs').writeFileSync(options.out, report, 'utf8');
1176
1183
  console.log(`\n Trend report exported to ${options.out}\n`);
@@ -1272,11 +1279,11 @@ async function main() {
1272
1279
  process.exit(0);
1273
1280
  } else if (normalizedCommand === 'augment' || normalizedCommand === 'suggest-only') {
1274
1281
  const report = await analyzeProject({ ...options, mode: normalizedCommand });
1275
- const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, normalizedCommand, report, {
1276
- tags: options.snapshotTags,
1277
- milestone: options.snapshotMilestone,
1278
- sourceCommand: normalizedCommand,
1279
- }) : null;
1282
+ const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, normalizedCommand, report, {
1283
+ tags: options.snapshotTags,
1284
+ milestone: options.snapshotMilestone,
1285
+ sourceCommand: normalizedCommand,
1286
+ }) : null;
1280
1287
  if (options.out && !options.json) {
1281
1288
  const fs = require('fs');
1282
1289
  const md = exportMarkdown(report);
@@ -1407,11 +1414,11 @@ async function main() {
1407
1414
  fs.writeFileSync(options.out, content, 'utf8');
1408
1415
  }
1409
1416
  printGovernanceSummary(summary, options);
1410
- const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'governance', summary, {
1411
- tags: options.snapshotTags,
1412
- milestone: options.snapshotMilestone,
1413
- sourceCommand: normalizedCommand,
1414
- }) : null;
1417
+ const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'governance', summary, {
1418
+ tags: options.snapshotTags,
1419
+ milestone: options.snapshotMilestone,
1420
+ sourceCommand: normalizedCommand,
1421
+ }) : null;
1415
1422
  if (options.out && !options.json) {
1416
1423
  console.log(` Governance report written to ${options.out}`);
1417
1424
  console.log('');
@@ -1423,11 +1430,11 @@ async function main() {
1423
1430
  }
1424
1431
  } else if (normalizedCommand === 'benchmark') {
1425
1432
  const report = await runBenchmark(options);
1426
- const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'benchmark', report, {
1427
- tags: options.snapshotTags,
1428
- milestone: options.snapshotMilestone,
1429
- sourceCommand: normalizedCommand,
1430
- }) : null;
1433
+ const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'benchmark', report, {
1434
+ tags: options.snapshotTags,
1435
+ milestone: options.snapshotMilestone,
1436
+ sourceCommand: normalizedCommand,
1437
+ }) : null;
1431
1438
  if (options.out) {
1432
1439
  writeBenchmarkReport(report, options.out);
1433
1440
  }
@@ -1444,87 +1451,87 @@ async function main() {
1444
1451
  } else if (normalizedCommand === 'deep-review') {
1445
1452
  const { deepReview } = require('../src/deep-review');
1446
1453
  await deepReview(options);
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') {
1454
+ } else if (normalizedCommand === 'interactive') {
1455
+ const { interactive } = require('../src/interactive');
1456
+ await interactive(options);
1457
+ } else if (normalizedCommand === 'baseline') {
1458
+ const {
1459
+ readManagedBaseline,
1460
+ writeManagedBaseline,
1461
+ buildManagedBaselineRecord,
1462
+ formatManagedBaselineStatus,
1463
+ } = require('../src/continuous-ops');
1464
+ const subcommand = parsed.extraArgs[0] || 'status';
1465
+
1466
+ if (subcommand === 'status') {
1467
+ const baseline = readManagedBaseline(options.dir);
1468
+ if (options.json) {
1469
+ console.log(JSON.stringify(baseline, null, 2));
1470
+ } else {
1471
+ console.log('');
1472
+ console.log(formatManagedBaselineStatus(options.dir, baseline));
1473
+ console.log('');
1474
+ }
1475
+ process.exit(0);
1476
+ }
1477
+
1478
+ if (subcommand === 'init') {
1479
+ const existingBaseline = readManagedBaseline(options.dir);
1480
+ if (existingBaseline && !flags.includes('--force')) {
1481
+ console.error('\n Error: Managed baseline already exists. Use `nerviq baseline status` to inspect it, or rerun with --force to replace it.\n');
1482
+ process.exit(1);
1483
+ }
1484
+
1485
+ const auditResult = await audit({ ...options, silent: true });
1486
+ const analysisReport = await analyzeProject({ ...options, mode: 'augment' });
1487
+ const detectedPlatforms = detectPlatforms(options.dir);
1488
+ const snapshot = writeSnapshotArtifact(options.dir, 'audit', auditResult, {
1489
+ tags: [...options.snapshotTags, 'baseline'],
1490
+ milestone: 'baseline',
1491
+ sourceCommand: 'baseline init',
1492
+ managedBaseline: true,
1493
+ });
1494
+ const baselineRecord = buildManagedBaselineRecord({
1495
+ dir: options.dir,
1496
+ platform: options.platform,
1497
+ auditResult,
1498
+ analysisReport,
1499
+ snapshotArtifact: snapshot,
1500
+ currentPlatforms: detectedPlatforms,
1501
+ });
1502
+ const saved = writeManagedBaseline(options.dir, baselineRecord);
1503
+
1504
+ if (options.json) {
1505
+ console.log(JSON.stringify({
1506
+ ...baselineRecord,
1507
+ baselinePath: saved.relativePath,
1508
+ }, null, 2));
1509
+ } else {
1510
+ console.log('');
1511
+ console.log(' nerviq baseline init');
1512
+ console.log(' ═══════════════════════════════════════');
1513
+ console.log(` Managed baseline written: ${saved.relativePath}`);
1514
+ console.log(` Snapshot: ${snapshot.relativePath}`);
1515
+ console.log(` Score: ${baselineRecord.baselineAudit.score}/100`);
1516
+ console.log(` Operating profile: ${baselineRecord.operatingProfile.label || 'n/a'}`);
1517
+ console.log(` Adoption plan: ${baselineRecord.adoptionPlan || 'n/a'}`);
1518
+ console.log(` Active platforms: ${(baselineRecord.detectedPlatforms || []).join(', ') || 'none detected'}`);
1519
+ console.log('');
1520
+ console.log(' Next:');
1521
+ console.log(' - nerviq audit --diff-only --drift-mode ci');
1522
+ console.log(' - nerviq watch');
1523
+ console.log(' - nerviq plan --campaign governance-hardening');
1524
+ console.log('');
1525
+ }
1526
+ process.exit(0);
1527
+ }
1528
+
1529
+ console.error('\n Error: baseline supports `init` and `status`.\n');
1530
+ process.exit(1);
1531
+ } else if (normalizedCommand === 'watch') {
1532
+ const { watch } = require('../src/watch');
1533
+ await watch(options);
1534
+ } else if (normalizedCommand === 'catalog') {
1528
1535
  const { generateCatalogWithVersion, writeCatalogJson } = require('../src/catalog');
1529
1536
  if (options.out) {
1530
1537
  const result = writeCatalogJson(options.out);
@@ -1595,10 +1602,11 @@ async function main() {
1595
1602
  const address = server.address();
1596
1603
  const resolvedPort = address && typeof address === 'object' ? address.port : options.port;
1597
1604
  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('');
1605
+ console.log(` nerviq API listening on http://127.0.0.1:${resolvedPort}`);
1606
+ console.log(' Endpoints: /api/openapi.json, /api/health, /api/catalog, /api/audit, /api/harmony');
1607
+ console.log(` Contract: http://127.0.0.1:${resolvedPort}/api/openapi.json`);
1608
+ console.log(' MCP hosts should use nerviq-mcp (stdio JSON-RPC 2.0), not this HTTP server.');
1609
+ console.log('');
1602
1610
 
1603
1611
  const closeServer = () => {
1604
1612
  server.close(() => process.exit(0));
@@ -1703,6 +1711,15 @@ async function main() {
1703
1711
  const { runHarmonyGovernance } = require('../src/harmony/cli');
1704
1712
  await runHarmonyGovernance(options);
1705
1713
  process.exit(0);
1714
+ } else if (normalizedCommand === 'harmony-score') {
1715
+ const { runHarmonyScore } = require('../src/harmony/cli');
1716
+ const result = await runHarmonyScore(options);
1717
+ const threshold = parseInt(options.threshold, 10) || 0;
1718
+ process.exit(threshold > 0 && !result.pass ? 1 : 0);
1719
+ } else if (normalizedCommand === 'harmony-demo') {
1720
+ const { runHarmonyDemo } = require('../src/harmony/cli');
1721
+ await runHarmonyDemo(options);
1722
+ process.exit(0);
1706
1723
  } else if (normalizedCommand === 'harmony-add') {
1707
1724
  const { addPlatform } = require('../src/harmony/add');
1708
1725
  const platformArg = parsed.extraArgs[0];
@@ -1823,74 +1840,74 @@ async function main() {
1823
1840
  }
1824
1841
  }
1825
1842
  process.exit(0);
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') {
1843
+ } else if (normalizedCommand === 'suggest-rules') {
1844
+ const { analyzeSuggestions, formatSuggestions } = require('../src/auto-suggest');
1845
+ const suggestions = analyzeSuggestions(options.dir);
1846
+ if (options.json) {
1847
+ console.log(JSON.stringify(suggestions, null, 2));
1848
+ } else {
1849
+ console.log('');
1850
+ console.log(formatSuggestions(suggestions));
1851
+ console.log('');
1852
+ }
1853
+ process.exit(0);
1854
+ } else if (normalizedCommand === 'exception') {
1855
+ const {
1856
+ listExceptions,
1857
+ addException,
1858
+ pruneExpiredExceptions,
1859
+ formatExceptionsList,
1860
+ } = require('../src/continuous-ops');
1861
+ const subcommand = parsed.extraArgs[0] || 'list';
1862
+
1863
+ if (subcommand === 'list') {
1864
+ const records = listExceptions(options.dir);
1865
+ if (options.json) {
1866
+ console.log(JSON.stringify(records, null, 2));
1867
+ } else {
1868
+ console.log('');
1869
+ console.log(formatExceptionsList(records));
1870
+ console.log('');
1871
+ }
1872
+ process.exit(0);
1873
+ }
1874
+
1875
+ if (subcommand === 'add') {
1876
+ const result = addException(options.dir, {
1877
+ key: parsed.feedbackKey || null,
1878
+ watchClass: options.exceptionClass,
1879
+ owner: options.exceptionOwner,
1880
+ reason: options.exceptionReason,
1881
+ expiresAt: options.exceptionExpires,
1882
+ scope: options.exceptionScope || 'all',
1883
+ });
1884
+ if (options.json) {
1885
+ console.log(JSON.stringify(result.record, null, 2));
1886
+ } else {
1887
+ console.log('');
1888
+ console.log(` Exception added: ${result.record.id}`);
1889
+ console.log(` Target: ${result.record.key || result.record.watchClass}`);
1890
+ console.log(` Owner: ${result.record.owner}`);
1891
+ console.log(` Scope: ${result.record.scope}`);
1892
+ console.log(` Expires: ${result.record.expiresAt}`);
1893
+ console.log('');
1894
+ }
1895
+ process.exit(0);
1896
+ }
1897
+
1898
+ if (subcommand === 'prune') {
1899
+ const result = pruneExpiredExceptions(options.dir);
1900
+ if (options.json) {
1901
+ console.log(JSON.stringify(result, null, 2));
1902
+ } else {
1903
+ console.log(`\n Pruned ${result.removedCount} expired exception(s). Kept ${result.keptCount} active record(s).\n`);
1904
+ }
1905
+ process.exit(0);
1906
+ }
1907
+
1908
+ console.error('\n Error: exception supports `add`, `list`, and `prune`.\n');
1909
+ process.exit(1);
1910
+ } else if (normalizedCommand === 'profile') {
1894
1911
  const { saveProfile, loadProfile, listProfiles, exportProfile, formatProfileList, formatProfile } = require('../src/profiles');
1895
1912
  const subcommand = parsed.extraArgs[0];
1896
1913
  const profileArg = parsed.extraArgs[1];
@@ -2432,117 +2449,117 @@ async function main() {
2432
2449
  process.exit(0);
2433
2450
  } else if (normalizedCommand === 'setup') {
2434
2451
  await setup(options);
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
- }
2445
- }
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 });
2452
+ if (options.snapshot) {
2453
+ const postSetupResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
2454
+ const snapshot = writeSnapshotArtifact(options.dir, 'audit', postSetupResult, {
2455
+ tags: options.snapshotTags,
2456
+ milestone: options.snapshotMilestone,
2457
+ sourceCommand: 'setup',
2458
+ });
2459
+ if (!options.json) {
2460
+ console.log(` Snapshot saved: ${snapshot.relativePath}`);
2461
+ }
2462
+ }
2463
+ } else {
2464
+ if (options.workspace) {
2465
+ const summary = await auditWorkspaces(options.dir, options.workspace, options.platform);
2466
+ printWorkspaceSummary(summary, options);
2467
+ if (options.threshold !== null && summary.averageScore < options.threshold) {
2468
+ process.exit(1);
2469
+ }
2470
+ process.exit(0);
2471
+ }
2472
+ let result;
2473
+ const renderAuditJsonLocally = options.json && Boolean(options.driftMode);
2474
+ if (options.diffOnly) {
2475
+ const { getChangedFiles, buildDiffOnlyAuditView, printDiffOnlyAudit } = require('../src/diff-only');
2476
+ const fullResult = await audit({ ...options, silent: true });
2477
+ const diffInfo = getChangedFiles(options.dir, {
2478
+ diffBase: options.diffBase,
2479
+ diffHead: options.diffHead,
2480
+ });
2481
+ result = buildDiffOnlyAuditView(fullResult, diffInfo);
2482
+ } else {
2483
+ result = renderAuditJsonLocally
2484
+ ? await audit({ ...options, silent: true })
2485
+ : await audit(options);
2486
+ }
2487
+
2488
+ if (options.driftMode) {
2489
+ const { buildContinuousStatus, formatContinuousStatus } = require('../src/continuous-ops');
2490
+ let campaigns = [];
2491
+ try {
2492
+ const planBundle = await buildProposalBundle({
2493
+ dir: options.dir,
2494
+ platform: options.platform,
2495
+ profile: options.profile,
2496
+ mcpPacks: options.mcpPacks,
2497
+ campaigns: [],
2498
+ });
2499
+ campaigns = planBundle.campaigns || [];
2500
+ } catch {
2501
+ campaigns = [];
2502
+ }
2503
+
2504
+ result = {
2505
+ ...result,
2506
+ continuousStatus: buildContinuousStatus({
2507
+ dir: options.dir,
2508
+ auditResult: result,
2509
+ mode: options.driftMode,
2510
+ currentPlatforms: detectPlatforms(options.dir),
2511
+ campaigns,
2512
+ }),
2513
+ };
2514
+ }
2515
+
2516
+ if (options.policyContract && options.policyContract.layers.some((layer) => layer.valid)) {
2517
+ result = {
2518
+ ...result,
2519
+ policyLayers: options.policyContract,
2520
+ };
2521
+ }
2522
+
2523
+ if (options.diffOnly) {
2524
+ const { printDiffOnlyAudit } = require('../src/diff-only');
2525
+ if (options.json) {
2526
+ console.log(JSON.stringify({
2527
+ version,
2528
+ timestamp: new Date().toISOString(),
2529
+ ...result,
2530
+ }, null, 2));
2531
+ } else {
2532
+ console.log(printDiffOnlyAudit(result));
2533
+ if (result.continuousStatus) {
2534
+ const { formatContinuousStatus } = require('../src/continuous-ops');
2535
+ console.log(formatContinuousStatus(result.continuousStatus));
2536
+ console.log('');
2537
+ }
2538
+ }
2539
+ } else if (renderAuditJsonLocally) {
2540
+ console.log(JSON.stringify({
2541
+ version,
2542
+ timestamp: new Date().toISOString(),
2543
+ ...result,
2544
+ }, null, 2));
2545
+ } else {
2546
+ if (!options.json && options.policyContract && options.policyContract.layers.some((layer) => layer.valid)) {
2547
+ console.log('');
2548
+ console.log(formatPolicyContract(options.policyContract));
2549
+ console.log('');
2550
+ }
2551
+ if (!options.json && result.continuousStatus) {
2552
+ const { formatContinuousStatus } = require('../src/continuous-ops');
2553
+ console.log('');
2554
+ console.log(formatContinuousStatus(result.continuousStatus));
2555
+ console.log('');
2556
+ }
2557
+ }
2558
+ if (options.out) {
2559
+ const fs = require('fs');
2560
+ const path = require('path');
2561
+ const outPath = path.resolve(options.out);
2562
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
2546
2563
  fs.writeFileSync(outPath, JSON.stringify(result, null, 2), 'utf8');
2547
2564
  if (!options.json) {
2548
2565
  console.log(`\n Audit report written to ${options.out}\n`);
@@ -2550,39 +2567,39 @@ async function main() {
2550
2567
  }
2551
2568
  if (options.webhookUrl) {
2552
2569
  try {
2553
- const { sendWebhook, formatSlackMessage, formatGenericAuditWebhookEvent } = require('../src/integrations');
2570
+ const { sendWebhook, formatSlackMessage, formatGenericAuditWebhookEvent } = require('../src/integrations');
2554
2571
  // Auto-detect Slack vs generic by URL pattern
2555
2572
  const isSlack = options.webhookUrl.includes('hooks.slack.com');
2556
2573
  const isDiscord = options.webhookUrl.includes('discord.com/api/webhooks');
2557
2574
  let payload;
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
- }
2575
+ if (isSlack) {
2576
+ payload = formatSlackMessage(result);
2577
+ } else if (isDiscord) {
2578
+ const { formatDiscordMessage } = require('../src/integrations');
2579
+ payload = formatDiscordMessage(result);
2580
+ } else {
2581
+ payload = formatGenericAuditWebhookEvent(result);
2582
+ }
2583
+ const webhookResp = await sendWebhook(options.webhookUrl, payload, {
2584
+ headers: options.webhookHeaders,
2585
+ retries: options.webhookRetries,
2586
+ });
2587
+ if (!options.json) {
2588
+ if (webhookResp.ok) {
2589
+ const retryNote = webhookResp.attempts > 1 ? ` after ${webhookResp.attempts} attempts` : '';
2590
+ console.log(` Webhook sent${retryNote}: ${options.webhookUrl} (${webhookResp.status})`);
2591
+ } else {
2592
+ const retryNote = webhookResp.attempts > 1 ? ` after ${webhookResp.attempts} attempts` : '';
2593
+ console.error(` Webhook failed${retryNote}: ${webhookResp.status} — ${webhookResp.body.slice(0, 200)}`);
2594
+ }
2595
+ }
2596
+ } catch (webhookErr) {
2597
+ if (!options.json) {
2598
+ const retryNote = webhookErr.attempts > 1 ? ` after ${webhookErr.attempts} attempts` : '';
2599
+ console.error(` Webhook error${retryNote}: ${webhookErr.message}`);
2600
+ }
2601
+ }
2602
+ }
2586
2603
  if (options.feedback && !options.json && options.format === null) {
2587
2604
  const feedbackTargets = options.lite
2588
2605
  ? (result.liteSummary?.topNextActions || [])
@@ -2602,37 +2619,37 @@ async function main() {
2602
2619
  console.log('');
2603
2620
  }
2604
2621
  }
2605
- const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'audit', result, {
2606
- tags: options.snapshotTags,
2607
- milestone: options.snapshotMilestone,
2608
- sourceCommand: normalizedCommand,
2609
- }) : null;
2622
+ const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'audit', result, {
2623
+ tags: options.snapshotTags,
2624
+ milestone: options.snapshotMilestone,
2625
+ sourceCommand: normalizedCommand,
2626
+ }) : null;
2610
2627
  if (snapshot && !options.json) {
2611
2628
  console.log(` Snapshot saved: ${snapshot.relativePath}`);
2612
2629
  console.log(` Snapshot index: ${snapshot.indexPath}`);
2613
2630
  console.log('');
2614
2631
  }
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.');
2632
+ if (options.threshold !== null && result.score < options.threshold) {
2633
+ if (!options.json) {
2634
+ console.error(`\n Error: Threshold not met — score ${result.score}/100 is below required ${options.threshold}/100.`);
2635
+ console.error(' Why: Your project audit score is lower than the minimum threshold set via --threshold.');
2636
+ console.error(' Fix: Run `npx nerviq augment` to see improvement suggestions, then re-audit.');
2620
2637
  console.error(' Docs: https://github.com/nerviq/nerviq#ci-integration\n');
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);
2638
+ }
2639
+ process.exit(1);
2640
+ }
2641
+ if (result.continuousStatus && result.continuousStatus.gate === 'fail') {
2642
+ if (!options.json) {
2643
+ console.error('\n Error: Continuous drift gate failed.');
2644
+ console.error(` Why: ${result.continuousStatus.gateLabel}.`);
2645
+ console.error(' Fix: review the blocking drift items or add a temporary exception with owner/reason/expiry.');
2646
+ console.error(' Docs: https://github.com/nerviq/nerviq#readme\n');
2647
+ }
2648
+ process.exit(1);
2649
+ }
2650
+ if (options.require && options.require.length > 0) {
2651
+ const failedRequired = options.require.filter(key => {
2652
+ const check = result.results.find(r => r.key === key);
2636
2653
  return !check || check.passed !== true;
2637
2654
  });
2638
2655
  if (failedRequired.length > 0) {