@nerviq/cli 1.17.2 → 1.18.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/README.md CHANGED
@@ -119,7 +119,7 @@ npx @nerviq/cli --beginner
119
119
  | Tools & MCP | ~40 | .mcp.json, multi-server, Context7 |
120
120
  | Governance & Compliance | ~30 | permission profiles, audit trails |
121
121
  | DevOps & Infrastructure | ~30 | Docker, CI, Terraform, monitoring |
122
- | Cross-Platform Intelligence | ~25 | harmony, synergy, drift detection |
122
+ | Cross-Platform Intelligence | ~25 | harmony (GA), synergy (Experimental), drift detection |
123
123
  | Enterprise & Freshness | ~20 | freshness tracking, deprecation, SBOM |
124
124
  | Memory & Context | ~15 | context management, compaction, @path |
125
125
  | Prompting & Design | ~10 | XML tags, constraints, frontend patterns |
@@ -223,7 +223,7 @@ All successful operational responses are wrapped in a JSON envelope:
223
223
  {
224
224
  "data": {},
225
225
  "meta": {
226
- "version": "1.17.2",
226
+ "version": "1.18.0",
227
227
  "timestamp": "2026-04-12T12:00:00.000Z"
228
228
  }
229
229
  }
@@ -353,7 +353,7 @@ Levels:
353
353
  | `nerviq harmony-watch` | Live drift monitoring |
354
354
  | `nerviq harmony-governance` | Unified platform governance |
355
355
  | `nerviq harmony-add <platform>` | Add a new platform to your project |
356
- | `nerviq synergy-report` | Multi-agent synergy analysis |
356
+ | `nerviq synergy-report` | Multi-agent synergy analysis (Experimental) |
357
357
  | `nerviq catalog` | Show check catalog for all 8 platforms |
358
358
  | `nerviq doctor` | Self-diagnostics for install health, freshness, platform detection, declared MCP servers, and hook runtime |
359
359
  | `nerviq convert` | Convert config between platforms |
package/bin/cli.js CHANGED
@@ -1,4 +1,13 @@
1
- #!/usr/bin/env node
1
+ // macOS pipe-flush guard: when stdout is a pipe, Node defaults to
2
+ // non-blocking writes. `console.log(...) + process.exit(N)` then drops
3
+ // the trailing write (empty stdout on macOS Node 18, truncation at the
4
+ // 8192-byte pipe buffer on macOS Node 20). Flipping stdout+stderr to
5
+ // blocking mode at the *very start* — before any writes queue up —
6
+ // makes every subsequent console.log a synchronous syscall, so the
7
+ // data is in the kernel before we ever reach process.exit. Runs before
8
+ // require() so it covers warnings from Node's module loader too.
9
+ try { if (process.stdout?._handle?.setBlocking) process.stdout._handle.setBlocking(true); } catch {}
10
+ try { if (process.stderr?._handle?.setBlocking) process.stderr._handle.setBlocking(true); } catch {}
2
11
 
3
12
  const { audit, detectPlatforms, getCatalog } = require('../src/public-api');
4
13
  const { setup } = require('../src/setup');
@@ -567,10 +576,12 @@ const HELP = `
567
576
  Audit, align, and amplify every platform on every project.
568
577
  New here? Run: nerviq --beginner
569
578
 
570
- DISCOVER
571
- nerviq audit Quick scan: score + top 3 gaps (Harmony-first when 2+ platforms detected)
572
- nerviq audit --no-harmony-first Skip the cross-platform Harmony header
573
- nerviq audit --full Full audit with all checks, weakest areas, badge
579
+ DISCOVER
580
+ nerviq audit Quick scan: score + top 3 gaps (Harmony-first when 2+ platforms detected)
581
+ nerviq audit --fix Audit, apply fixable critical fixes, then re-audit
582
+ nerviq audit --fix --dry-run Show proposed autofix diff without writing
583
+ nerviq audit --no-harmony-first Skip the cross-platform Harmony header
584
+ nerviq audit --full Full audit with all checks, weakest areas, badge
574
585
  nerviq audit --platform X Audit specific platform (claude|codex|cursor|copilot|gemini|windsurf|aider|opencode)
575
586
  nerviq audit --json Machine-readable JSON output (for CI)
576
587
  nerviq audit --workspace packages/* Audit monorepo workspaces with stack-specific package profiles
@@ -591,9 +602,9 @@ const HELP = `
591
602
  nerviq check-health Detect regressions + platform format changes between snapshots
592
603
  nerviq doctor Self-diagnostics: Node, deps, freshness, MCP, hook runtime
593
604
 
594
- FIX
595
- nerviq fix Show fixable checks and manual-fix guidance
596
- nerviq fix <key> Auto-fix a specific check (with score impact)
605
+ FIX
606
+ nerviq fix Show fixable checks and manual-fix guidance
607
+ nerviq fix <key> Auto-fix a specific check (with score impact)
597
608
  nerviq fix <key> --prompt Show AI agent prompt for a check (no auto-fix)
598
609
  nerviq fix --all-critical Fix all critical issues at once
599
610
  nerviq fix --dry-run Preview fixes without writing
@@ -764,12 +775,12 @@ const BEGINNER_HELP = `
764
775
  nerviq augment Show an improvement plan without writing
765
776
  nerviq doctor Check install health, freshness, platform detection, MCP, and hook runtime
766
777
 
767
- SIMPLE PATH
768
- 1. nerviq audit
769
- 2. nerviq setup --auto
770
- 3. nerviq fix --all-critical --auto
771
- 4. nerviq augment
772
- 5. nerviq doctor
778
+ SIMPLE PATH
779
+ 1. nerviq audit
780
+ 2. nerviq setup --auto
781
+ 3. nerviq audit --fix --auto
782
+ 4. nerviq augment
783
+ 5. nerviq doctor
773
784
 
774
785
  WHEN YOU ARE READY
775
786
  nerviq --help Show the full command set
@@ -2084,73 +2095,34 @@ async function main() {
2084
2095
  });
2085
2096
  console.log(output);
2086
2097
  process.exit(0);
2087
- } else if (normalizedCommand === 'fix') {
2088
- // nerviq fix [key] [--all-critical] [--dry-run] [--auto] [--prompt]
2089
- const fixKey = parsed.extraArgs[0] || null;
2090
- const allCritical = flags.includes('--all-critical');
2091
- const promptOnly = flags.includes('--prompt');
2092
- const autoApply = options.auto;
2093
- const isDryRun = options.dryRun;
2094
-
2095
- // Step 1: Run silent audit to find failed checks (only actual failures, not skipped/null)
2096
- const auditResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
2097
- const failedResults = (auditResult.results || []).filter(r => r.passed === false);
2098
+ } else if (normalizedCommand === 'fix') {
2099
+ // nerviq fix [key] [--all-critical] [--dry-run] [--auto] [--prompt]
2100
+ const fixKey = parsed.extraArgs[0] || null;
2101
+ const allCritical = flags.includes('--all-critical');
2102
+ const promptOnly = flags.includes('--prompt');
2103
+ const autoApply = options.auto;
2104
+ const isDryRun = options.dryRun;
2105
+ const { getFixableFailedResults, isFixableKey, applyFixes } = require('../src/fix-engine');
2106
+
2107
+ // Step 1: Run silent audit to find failed checks (only actual failures, not skipped/null)
2108
+ const auditResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
2109
+ const failedResults = (auditResult.results || []).filter(r => r.passed === false);
2098
2110
 
2099
2111
  if (failedResults.length === 0) {
2100
2112
  console.log('\n ✅ All checks passing — nothing to fix.\n');
2101
2113
  process.exit(0);
2102
2114
  }
2103
2115
 
2104
- // Step 2: Determine which checks to fix
2105
- const { TECHNIQUES } = require('../src/techniques');
2106
- const { FIX_PROMPTS, formatFixPrompt } = require('../src/fix-prompts');
2107
- const fs = require('fs');
2108
- const pathMod = require('path');
2109
-
2110
- // Inline fixers for checks without templates but with trivial auto-fixes
2111
- const INLINE_FIXERS = {
2112
- gitIgnoreEnv: (dir) => {
2113
- const gitignorePath = pathMod.join(dir, '.gitignore');
2114
- const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
2115
- if (!existing.includes('.env')) {
2116
- const lines = existing.endsWith('\n') || existing === '' ? '' : '\n';
2117
- fs.appendFileSync(gitignorePath, `${lines}.env\n.env.*\n`, 'utf8');
2118
- return true;
2119
- }
2120
- return false;
2121
- },
2122
- secretsProtection: (dir) => {
2123
- const settingsPath = pathMod.join(dir, '.claude', 'settings.json');
2124
- const settingsDir = pathMod.join(dir, '.claude');
2125
- if (!fs.existsSync(settingsDir)) fs.mkdirSync(settingsDir, { recursive: true });
2126
- let settings = {};
2127
- if (fs.existsSync(settingsPath)) {
2128
- try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch { settings = {}; }
2129
- }
2130
- if (!settings.permissions) settings.permissions = {};
2131
- if (!settings.permissions.deny) settings.permissions.deny = [];
2132
- const denyEntries = ['.env', '.env.*', '**/.env', '**/*.pem', '**/secrets/**'];
2133
- for (const entry of denyEntries) {
2134
- if (!settings.permissions.deny.includes(entry)) settings.permissions.deny.push(entry);
2135
- }
2136
- // Remove overly broad allow:["*"] if present
2137
- if (Array.isArray(settings.permissions.allow) && settings.permissions.allow.includes('*')) {
2138
- settings.permissions.allow = settings.permissions.allow.filter(a => a !== '*');
2139
- if (settings.permissions.allow.length === 0) {
2140
- delete settings.permissions.allow;
2141
- }
2142
- }
2143
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
2144
- return true;
2145
- },
2146
- };
2147
-
2148
- let targetKeys = [];
2149
-
2150
- if (fixKey) {
2151
- // Fix a specific check
2152
- if (!failedResults.find(r => r.key === fixKey)) {
2153
- const passed = (auditResult.results || []).find(r => r.key === fixKey && r.passed);
2116
+ // Step 2: Determine which checks to fix
2117
+ const { FIX_PROMPTS, formatFixPrompt } = require('../src/fix-prompts');
2118
+
2119
+ let targetKeys = [];
2120
+ const fixableFailedResults = getFixableFailedResults(failedResults, { mode: 'fix' });
2121
+
2122
+ if (fixKey) {
2123
+ // Fix a specific check
2124
+ if (!failedResults.find(r => r.key === fixKey)) {
2125
+ const passed = (auditResult.results || []).find(r => r.key === fixKey && r.passed);
2154
2126
  if (passed) {
2155
2127
  console.log(`\n ✅ '${fixKey}' is already passing.\n`);
2156
2128
  } else {
@@ -2168,26 +2140,37 @@ async function main() {
2168
2140
  const failedCheck = failedResults.find(r => r.key === fixKey);
2169
2141
  console.log(`\n No AI prompt available for '${fixKey}'.`);
2170
2142
  console.log(` Manual fix: ${failedCheck ? failedCheck.fix : 'See nerviq audit --full.'}\n`);
2171
- }
2172
- process.exit(0);
2173
- }
2174
- targetKeys = [fixKey];
2175
- } else if (allCritical) {
2176
- targetKeys = failedResults.filter(r => r.impact === 'critical').map(r => r.key);
2177
- if (targetKeys.length === 0) {
2178
- console.log('\n ✅ No critical issues found.\n');
2179
- process.exit(0);
2180
- }
2181
- } else {
2182
- // No key specified — show fixable checks and exit
2183
- const INLINE_FIX_KEYS = new Set(Object.keys(INLINE_FIXERS));
2184
- const fixable = failedResults.filter(r => (TECHNIQUES[r.key] && TECHNIQUES[r.key].template) || INLINE_FIX_KEYS.has(r.key));
2185
- const nonFixable = failedResults.filter(r => !(TECHNIQUES[r.key] && TECHNIQUES[r.key].template) && !INLINE_FIX_KEYS.has(r.key));
2186
- console.log('');
2187
- console.log(` nerviq fix ${failedResults.length} failed checks\n`);
2188
- if (fixable.length > 0) {
2189
- console.log(` Auto-fixable (${fixable.length}):`);
2190
- for (const r of fixable) {
2143
+ }
2144
+ process.exit(0);
2145
+ }
2146
+ if (!isFixableKey(fixKey, { mode: 'fix' })) {
2147
+ const aiPrompt = FIX_PROMPTS[fixKey];
2148
+ const failedCheck = failedResults.find(r => r.key === fixKey);
2149
+ if (aiPrompt) {
2150
+ console.log(formatFixPrompt(fixKey, aiPrompt));
2151
+ } else {
2152
+ console.log(`\n 📋 ${failedCheck.name} (manual fix needed)`);
2153
+ console.log(` ${failedCheck.fix}\n`);
2154
+ }
2155
+ process.exit(0);
2156
+ }
2157
+ targetKeys = [fixKey];
2158
+ } else if (allCritical) {
2159
+ targetKeys = getFixableFailedResults(failedResults, { mode: 'fix', criticalOnly: true }).map(r => r.key);
2160
+ if (targetKeys.length === 0) {
2161
+ console.log('\n No critical issues found.\n');
2162
+ process.exit(0);
2163
+ }
2164
+ } else {
2165
+ // No key specified — show fixable checks and exit
2166
+ const fixable = fixableFailedResults;
2167
+ const fixableKeySet = new Set(fixable.map(r => r.key));
2168
+ const nonFixable = failedResults.filter(r => !fixableKeySet.has(r.key));
2169
+ console.log('');
2170
+ console.log(` nerviq fix — ${failedResults.length} failed checks\n`);
2171
+ if (fixable.length > 0) {
2172
+ console.log(` Auto-fixable (${fixable.length}):`);
2173
+ for (const r of fixable) {
2191
2174
  const tier = r.impact === 'critical' ? '🔴' : r.impact === 'high' ? '🟡' : '🔵';
2192
2175
  console.log(` ${tier} nerviq fix ${r.key}`);
2193
2176
  }
@@ -2224,241 +2207,23 @@ async function main() {
2224
2207
  console.log(` nerviq fix ${fixable[0].key} Fix the first auto-fixable check`);
2225
2208
  console.log(` nerviq fix --all-critical Fix all critical issues at once`);
2226
2209
  }
2227
- console.log('');
2228
- process.exit(0);
2229
- }
2230
-
2231
- // Step 2.5: Predict impact and show preview before applying
2232
- const IMPACT_WEIGHTS = { critical: 15, high: 10, medium: 5, low: 2 };
2233
- const preScore = auditResult.score;
2234
- const applicableResults = (auditResult.results || []).filter(r => r.passed !== null);
2235
- const maxScore = applicableResults.reduce((sum, r) => sum + (IMPACT_WEIGHTS[r.impact] || 5), 0);
2236
-
2237
- // Compute predicted score by simulating target fixes as passing
2238
- const targetKeySet = new Set(targetKeys);
2239
- const INLINE_FIX_KEYS = new Set(Object.keys(INLINE_FIXERS));
2240
- const fixableTargets = targetKeys.filter(k => {
2241
- const tech = TECHNIQUES[k];
2242
- return (tech && tech.template) || INLINE_FIX_KEYS.has(k);
2243
- });
2244
- const fixableTargetSet = new Set(fixableTargets);
2245
- const simulatedEarned = applicableResults.reduce((sum, r) => {
2246
- const w = IMPACT_WEIGHTS[r.impact] || 5;
2247
- if (r.passed) return sum + w;
2248
- if (fixableTargetSet.has(r.key)) return sum + w;
2249
- return sum;
2250
- }, 0);
2251
- const predictedScore = maxScore > 0 ? Math.round((simulatedEarned / maxScore) * 100) : 0;
2252
- const predictedDelta = predictedScore - preScore;
2253
-
2254
- if (!autoApply && !isDryRun) {
2255
- console.log('');
2256
- if (allCritical && fixableTargets.length > 1) {
2257
- // Multi-fix summary
2258
- console.log(` ${fixableTargets.length} critical fixes available:`);
2259
- let runningEarned = applicableResults.reduce((s, r) => s + (r.passed ? (IMPACT_WEIGHTS[r.impact] || 5) : 0), 0);
2260
- let runningScore = maxScore > 0 ? Math.round((runningEarned / maxScore) * 100) : 0;
2261
- fixableTargets.forEach((k, idx) => {
2262
- const r = failedResults.find(fr => fr.key === k);
2263
- const w = IMPACT_WEIGHTS[r.impact] || 5;
2264
- const nextEarned = runningEarned + w;
2265
- const nextScore = maxScore > 0 ? Math.round((nextEarned / maxScore) * 100) : 0;
2266
- const d = nextScore - runningScore;
2267
- console.log(` ${idx + 1}. ${(r.key).padEnd(18)} ${runningScore} → ${nextScore} (+${d})`);
2268
- runningEarned = nextEarned;
2269
- runningScore = nextScore;
2270
- });
2271
- console.log('');
2272
- console.log(` Total: ${preScore} → ${predictedScore} (+${predictedDelta})`);
2273
- } else {
2274
- // Single fix preview
2275
- const targetCheck = failedResults.find(r => r.key === fixableTargets[0]) || failedResults.find(r => r.key === targetKeys[0]);
2276
- if (targetCheck) {
2277
- console.log(` Predicted impact: ${preScore} → ${predictedScore} (+${predictedDelta})`);
2278
- }
2279
- }
2280
-
2281
- // Prompt for confirmation
2282
- const readline = require('readline');
2283
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2284
- const answer = await new Promise(resolve => {
2285
- rl.question(' Apply? (Y/n) ', resolve);
2286
- });
2287
- rl.close();
2288
- if (answer && answer.trim().toLowerCase() === 'n') {
2289
- for (const key of targetKeys) {
2290
- recordPattern(options.dir, key, 'rejected');
2291
- }
2292
- console.log('\n Aborted.\n');
2293
- process.exit(0);
2294
- }
2295
- }
2296
-
2297
- // Step 3: Create rollback snapshot before applying fixes
2298
- const isBatch = allCritical && targetKeys.length > 1;
2299
- let rollbackId = null;
2300
- const allCreatedFiles = [];
2301
- const fixResults = []; // { key, name, status, delta }
2302
-
2303
- const snapshotFiles = {};
2304
- if (!isDryRun && targetKeys.length > 0) {
2305
- // Snapshot existing files for rollback (before applying fixes)
2306
- for (const key of targetKeys) {
2307
- const technique = TECHNIQUES[key];
2308
- if (technique && technique.template && technique.template.path) {
2309
- const tplPath = pathMod.join(options.dir, technique.template.path);
2310
- if (fs.existsSync(tplPath)) {
2311
- snapshotFiles[technique.template.path] = fs.readFileSync(tplPath, 'utf8');
2312
- }
2313
- }
2314
- }
2315
- }
2316
-
2317
- // Step 3b: Apply fixes sequentially with progress
2318
- let fixed = 0;
2319
- let manual = 0;
2320
- let runningScore = preScore;
2321
-
2322
- for (let i = 0; i < targetKeys.length; i++) {
2323
- const key = targetKeys[i];
2324
- const technique = TECHNIQUES[key];
2325
- const failedCheck = failedResults.find(r => r.key === key);
2326
- const progress = isBatch ? `${i + 1}/${targetKeys.length}: ` : '';
2327
-
2328
- if (technique && technique.template) {
2329
- if (isDryRun) {
2330
- console.log(` [dry-run] Would fix: ${progress}${failedCheck.name} (${key})`);
2331
- fixResults.push({ key, name: failedCheck.name, status: 'dry-run', delta: 0 });
2332
- fixed++;
2333
- } else {
2334
- try {
2335
- if (isBatch) console.log(` Fixing ${progress}${key}...`);
2336
- const setupResult = await setup({ ...options, only: [key], silent: true });
2337
- if (setupResult && setupResult.writtenFiles) {
2338
- allCreatedFiles.push(...setupResult.writtenFiles);
2339
- }
2340
- const midResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
2341
- const delta = midResult.score - runningScore;
2342
- fixResults.push({ key, name: failedCheck.name, status: 'fixed', delta });
2343
- runningScore = midResult.score;
2344
- if (!isBatch) console.log(` ✅ Fixed: ${failedCheck.name}`);
2345
- fixed++;
2346
- } catch (err) {
2347
- fixResults.push({ key, name: failedCheck.name, status: 'failed', delta: 0 });
2348
- if (isBatch) {
2349
- console.log(` ❌ Failed: ${key} — ${err.message}`);
2350
- console.log(` Stopping batch. ${fixed} fixes applied so far.`);
2351
- console.log(` Rollback: nerviq rollback --id ${rollbackId}`);
2352
- break;
2353
- } else {
2354
- console.log(` ❌ Failed: ${failedCheck.name} — ${err.message}`);
2355
- }
2356
- }
2357
- }
2358
- } else if (INLINE_FIXERS[key]) {
2359
- if (isDryRun) {
2360
- console.log(` [dry-run] Would fix: ${progress}${failedCheck.name} (${key})`);
2361
- fixResults.push({ key, name: failedCheck.name, status: 'dry-run', delta: 0 });
2362
- fixed++;
2363
- } else {
2364
- try {
2365
- if (isBatch) console.log(` Fixing ${progress}${key}...`);
2366
- const didFix = INLINE_FIXERS[key](options.dir);
2367
- if (didFix) {
2368
- const midResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
2369
- const delta = midResult.score - runningScore;
2370
- fixResults.push({ key, name: failedCheck.name, status: 'fixed', delta });
2371
- runningScore = midResult.score;
2372
- if (!isBatch) console.log(` ✅ Fixed: ${failedCheck.name}`);
2373
- fixed++;
2374
- } else {
2375
- fixResults.push({ key, name: failedCheck.name, status: 'skipped', delta: 0 });
2376
- if (!isBatch) console.log(` ⏭️ Already fixed: ${failedCheck.name}`);
2377
- }
2378
- } catch (err) {
2379
- fixResults.push({ key, name: failedCheck.name, status: 'failed', delta: 0 });
2380
- if (isBatch) {
2381
- console.log(` ❌ Failed: ${key} — ${err.message}`);
2382
- console.log(` Stopping batch. ${fixed} fixes applied so far.`);
2383
- console.log(` Rollback: nerviq rollback --id ${rollbackId}`);
2384
- break;
2385
- }
2386
- }
2387
- }
2388
- } else {
2389
- if (!isBatch) {
2390
- const aiPrompt = FIX_PROMPTS[key];
2391
- if (aiPrompt) {
2392
- console.log(formatFixPrompt(key, aiPrompt));
2393
- } else {
2394
- console.log(` 📋 ${failedCheck.name} (manual fix needed)`);
2395
- console.log(` ${failedCheck.fix}`);
2396
- }
2397
- }
2398
- fixResults.push({ key, name: failedCheck.name, status: 'skipped', delta: 0 });
2399
- manual++;
2400
- }
2401
- }
2402
-
2403
- // Record accepted patterns for successfully fixed checks
2404
- if (!isDryRun) {
2405
- for (const key of targetKeys) {
2406
- const fr = fixResults.find(r => r.key === key);
2407
- recordPattern(options.dir, key, fr && fr.status === 'fixed' ? 'accepted' : 'rejected');
2408
- }
2409
- }
2410
-
2411
- // Write rollback artifact AFTER fixes are applied (with actual file lists)
2412
- if (!isDryRun && targetKeys.length > 0 && fixed > 0) {
2413
- const allPatchedFiles = Object.keys(snapshotFiles);
2414
- // Also track inline-fixer patched files
2415
- for (const fr of fixResults) {
2416
- if (fr.status === 'fixed' && INLINE_FIXERS[fr.key]) {
2417
- const inlinePath = fr.key === 'gitIgnoreEnv' ? '.gitignore' : fr.key === 'secretsProtection' ? '.claude/settings.json' : null;
2418
- if (inlinePath && !allPatchedFiles.includes(inlinePath)) {
2419
- allPatchedFiles.push(inlinePath);
2420
- }
2421
- }
2422
- }
2423
- const rollbackArtifact = writeRollbackArtifact(options.dir, {
2424
- sourcePlan: 'fix-batch',
2425
- preSnapshot: snapshotFiles,
2426
- createdFiles: allCreatedFiles,
2427
- patchedFiles: allPatchedFiles,
2428
- rollbackInstructions: ['Use nerviq rollback to undo these fixes'],
2429
- });
2430
- rollbackId = rollbackArtifact.id;
2431
- }
2432
-
2433
- // Step 4: Show batch summary or simple score impact
2434
- if (isBatch && fixResults.length > 0) {
2435
- console.log('');
2436
- console.log(' Batch fix complete:');
2437
- for (let i = 0; i < fixResults.length; i++) {
2438
- const r = fixResults[i];
2439
- const icon = r.status === 'fixed' ? '✅' : r.status === 'failed' ? '❌' : '⚠ ';
2440
- const deltaStr = r.status === 'fixed' ? ` (+${r.delta})` : r.status === 'skipped' ? ' (skipped — no auto-fix)' : r.status === 'failed' ? ' (failed)' : ' (dry-run)';
2441
- console.log(` ${icon} ${i + 1}. ${r.key.padEnd(20)}${deltaStr}`);
2442
- }
2443
- const totalDelta = runningScore - preScore;
2444
- console.log('');
2445
- console.log(` Score: ${preScore} → ${runningScore} (${totalDelta >= 0 ? '+' : ''}${totalDelta})`);
2446
- if (rollbackId && !isDryRun) {
2447
- console.log(` Rollback available: nerviq rollback --id ${rollbackId}`);
2448
- }
2449
- } else if (fixed > 0 && !isDryRun) {
2450
- const postResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
2451
- const delta = postResult.score - preScore;
2452
- console.log('');
2453
- console.log(` Score: ${preScore} → ${postResult.score} (${delta >= 0 ? '+' : ''}${delta})`);
2454
- if (rollbackId) {
2455
- console.log(` Rollback available: nerviq rollback --id ${rollbackId}`);
2456
- }
2457
- }
2458
-
2459
- console.log(`\n ${fixed} fixed, ${manual} need manual action.\n`);
2460
-
2461
- } else if (normalizedCommand === 'init') {
2210
+ console.log('');
2211
+ process.exit(0);
2212
+ }
2213
+
2214
+ const outcome = await applyFixes({
2215
+ dir: options.dir,
2216
+ platform: options.platform,
2217
+ auditResult,
2218
+ targetKeys,
2219
+ auto: autoApply,
2220
+ dryRun: isDryRun,
2221
+ mode: 'fix',
2222
+ recordOutcomes: true,
2223
+ });
2224
+ process.exit(outcome.exitCode);
2225
+
2226
+ } else if (normalizedCommand === 'init') {
2462
2227
  const { runInit } = require('../src/init');
2463
2228
  await runInit(options.dir, flags);
2464
2229
  process.exit(0);
@@ -2508,11 +2273,11 @@ async function main() {
2508
2273
  }
2509
2274
  // MOAT-01: Harmony-first default — when 2+ platforms and platform not explicit
2510
2275
  let harmonyFirstResult = null;
2511
- if (!options.platformExplicit && !options.noHarmonyFirst && !options.diffOnly && !options.driftMode && !options.workspace) {
2512
- const detected = detectPlatforms(options.dir) || [];
2513
- if (detected.length >= 2) {
2514
- try {
2515
- const { harmonyAudit } = require('../src/harmony/audit');
2276
+ if (!options.platformExplicit && !options.noHarmonyFirst && !options.diffOnly && !options.driftMode && !options.workspace) {
2277
+ const detected = detectPlatforms(options.dir) || [];
2278
+ if (detected.length >= 2) {
2279
+ try {
2280
+ const { harmonyAudit } = require('../src/harmony/audit');
2516
2281
  harmonyFirstResult = await harmonyAudit({ dir: options.dir, silent: true });
2517
2282
  if (!options.json && harmonyFirstResult) {
2518
2283
  const hs = harmonyFirstResult.harmonyScore;
@@ -2526,15 +2291,46 @@ async function main() {
2526
2291
  }
2527
2292
  } catch {
2528
2293
  harmonyFirstResult = null;
2529
- }
2530
- }
2531
- }
2532
-
2533
- let result;
2534
- const renderAuditJsonLocally = options.json && (Boolean(options.driftMode) || Boolean(harmonyFirstResult));
2535
- if (options.diffOnly) {
2536
- const { getChangedFiles, buildDiffOnlyAuditView, printDiffOnlyAudit } = require('../src/diff-only');
2537
- const fullResult = await audit({ ...options, silent: true });
2294
+ }
2295
+ }
2296
+ }
2297
+
2298
+ if (options.fix) {
2299
+ if (options.diffOnly) {
2300
+ console.error('\n Error: --diff-only cannot be combined with --fix.\n');
2301
+ process.exit(2);
2302
+ }
2303
+
2304
+ const { getFixableFailedResults, applyFixes } = require('../src/fix-engine');
2305
+ const auditResult = await audit({ ...options, silent: true });
2306
+ const failedResults = (auditResult.results || []).filter((item) => item.passed === false);
2307
+ const targetKeys = getFixableFailedResults(failedResults, { mode: 'audit', criticalOnly: true }).map((item) => item.key);
2308
+
2309
+ if (targetKeys.length === 0) {
2310
+ console.log('');
2311
+ console.log(' אין תיקוני critical אוטומטיים זמינים למסלול audit --fix.');
2312
+ console.log(' נסו `nerviq fix <key> --prompt` עבור תיקון ידני, או הריצו audit מלא כדי לראות את כל הפערים.');
2313
+ console.log('');
2314
+ process.exit(2);
2315
+ }
2316
+
2317
+ const outcome = await applyFixes({
2318
+ dir: options.dir,
2319
+ platform: options.platform,
2320
+ auditResult,
2321
+ targetKeys,
2322
+ auto: options.auto,
2323
+ dryRun: options.dryRun,
2324
+ mode: 'audit',
2325
+ });
2326
+ process.exit(outcome.exitCode);
2327
+ }
2328
+
2329
+ let result;
2330
+ const renderAuditJsonLocally = options.json && (Boolean(options.driftMode) || Boolean(harmonyFirstResult));
2331
+ if (options.diffOnly) {
2332
+ const { getChangedFiles, buildDiffOnlyAuditView, printDiffOnlyAudit } = require('../src/diff-only');
2333
+ const fullResult = await audit({ ...options, silent: true });
2538
2334
  const diffInfo = getChangedFiles(options.dir, {
2539
2335
  diffBase: options.diffBase,
2540
2336
  diffHead: options.diffHead,