@nerviq/cli 1.14.0 → 1.16.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
@@ -217,7 +217,7 @@ All successful operational responses are wrapped in a JSON envelope:
217
217
  {
218
218
  "data": {},
219
219
  "meta": {
220
- "version": "1.14.0",
220
+ "version": "1.16.0",
221
221
  "timestamp": "2026-04-11T12:00:00.000Z"
222
222
  }
223
223
  }
package/bin/cli.js CHANGED
@@ -8,6 +8,7 @@ const { getGovernanceSummary, printGovernanceSummary, ensureWritableProfile, ren
8
8
  const { runBenchmark, printBenchmark, writeBenchmarkReport } = require('../src/benchmark');
9
9
  const { writeSnapshotArtifact, writeRollbackArtifact, recordRecommendationOutcome, formatRecommendationOutcomeSummary, getRecommendationOutcomeSummary } = require('../src/activity');
10
10
  const { collectFeedback } = require('../src/feedback');
11
+ const { collectAnonymousEvent } = require('../src/telemetry');
11
12
  const { recordPattern, getPriorityAdjustment, formatUsageSummary } = require('../src/usage-patterns');
12
13
  const { startServer } = require('../src/server');
13
14
  const { auditWorkspaces } = require('../src/workspace');
@@ -104,6 +105,7 @@ function parseArgs(rawArgs) {
104
105
  let format = null;
105
106
  let port = null;
106
107
  let workspace = null;
108
+ let targetDir = null;
107
109
  let webhookUrl = null;
108
110
  let webhookHeaders = [];
109
111
  let webhookRetries = null;
@@ -134,7 +136,7 @@ function parseArgs(rawArgs) {
134
136
  for (let i = 0; i < rawArgs.length; i++) {
135
137
  const arg = rawArgs[i];
136
138
 
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') {
139
+ 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 === '--dir' || 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
140
  const value = rawArgs[i + 1];
139
141
  if (!value || value.startsWith('--')) {
140
142
  throw new Error(`${arg} requires a value`);
@@ -153,6 +155,7 @@ function parseArgs(rawArgs) {
153
155
  if (arg === '--source') feedbackSource = value.trim();
154
156
  if (arg === '--score-delta') feedbackScoreDelta = value.trim();
155
157
  if (arg === '--platform') { platform = value.trim().toLowerCase(); platformExplicit = true; }
158
+ if (arg === '--dir') targetDir = require('path').resolve(value.trim());
156
159
  if (arg === '--format') format = value.trim().toLowerCase();
157
160
  if (arg === '--from') { convertFrom = value.trim(); migrateFrom = value.trim(); }
158
161
  if (arg === '--to') { convertTo = value.trim(); migrateTo = value.trim(); }
@@ -337,6 +340,11 @@ function parseArgs(rawArgs) {
337
340
  continue;
338
341
  }
339
342
 
343
+ if (arg.startsWith('--dir=')) {
344
+ targetDir = require('path').resolve(arg.split('=').slice(1).join('=').trim());
345
+ continue;
346
+ }
347
+
340
348
  if (arg.startsWith('--format=')) {
341
349
  format = arg.split('=').slice(1).join('=').trim().toLowerCase();
342
350
  continue;
@@ -388,7 +396,7 @@ function parseArgs(rawArgs) {
388
396
 
389
397
  const normalizedCommand = COMMAND_ALIASES[command] || command;
390
398
 
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 };
399
+ return { flags, command, commandExplicit, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks, feedbackKey, feedbackStatus, feedbackEffect, feedbackNotes, feedbackSource, feedbackScoreDelta, platform, platformExplicit, format, port, workspace, targetDir, 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
400
  }
393
401
 
394
402
  function printWorkspaceSummary(summary, options) {
@@ -560,7 +568,8 @@ const HELP = `
560
568
  New here? Run: nerviq --beginner
561
569
 
562
570
  DISCOVER
563
- nerviq audit Quick scan: score + top 3 gaps (default)
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
564
573
  nerviq audit --full Full audit with all checks, weakest areas, badge
565
574
  nerviq audit --platform X Audit specific platform (claude|codex|cursor|copilot|gemini|windsurf|aider|opencode)
566
575
  nerviq audit --json Machine-readable JSON output (for CI)
@@ -663,6 +672,7 @@ const HELP = `
663
672
 
664
673
  OPTIONS
665
674
  --platform NAME Platform: claude (default), codex, cursor, copilot, gemini, windsurf, aider, opencode
675
+ --dir PATH Target directory to audit (default: current directory)
666
676
  --threshold N Exit code 1 if score < N (CI gate)
667
677
  --require A,B Exit code 1 if named checks fail
668
678
  --out FILE Write output to file (JSON or markdown)
@@ -694,6 +704,7 @@ const HELP = `
694
704
  --verbose Full audit + medium-priority recommendations
695
705
  --show-deprecated Show deprecated checks (excluded from scoring)
696
706
  --json Output as JSON
707
+ --agent-mode Non-interactive JSON output for AI agents (setup/audit)
697
708
  --auto Apply all generated files without prompting
698
709
  --beginner Show only the 5 starter commands for first-time users
699
710
  --key NAME Feedback: recommendation key (e.g. permissionDeny)
@@ -735,8 +746,9 @@ const HELP = `
735
746
  npx nerviq feedback --key permissionDeny --status accepted --effect positive
736
747
 
737
748
  EXIT CODES
738
- 0 Success
739
- 1 Error, unknown command, or score below --threshold
749
+ 0 Success (score meets threshold, or no threshold set)
750
+ 1 Threshold not met (score below --threshold)
751
+ 2 Runtime error (unknown command, missing files, crash)
740
752
  `;
741
753
 
742
754
  const BEGINNER_HELP = `
@@ -770,7 +782,7 @@ async function main() {
770
782
  parsed = parseArgs(args);
771
783
  } catch (err) {
772
784
  console.error(`\n Error: ${err.message}\n`);
773
- process.exit(1);
785
+ process.exit(2);
774
786
  }
775
787
 
776
788
  const { flags, command, commandExplicit, normalizedCommand } = parsed;
@@ -807,6 +819,7 @@ async function main() {
807
819
  fix: flags.includes('--fix'),
808
820
  badge: flags.includes('--badge'),
809
821
  quiet: flags.includes('--quiet'),
822
+ agentMode: flags.includes('--agent-mode'),
810
823
  autoSync: flags.includes('--auto-sync'),
811
824
  dryRun: flags.includes('--dry-run'),
812
825
  configOnly: flags.includes('--config-only'),
@@ -834,6 +847,7 @@ async function main() {
834
847
  historyView: flags.includes('--history'),
835
848
  compareView: flags.includes('--compare'),
836
849
  diffOnly: flags.includes('--diff-only'),
850
+ noHarmonyFirst: flags.includes('--no-harmony-first'),
837
851
  diffBase: parsed.diffBase || null,
838
852
  diffHead: parsed.diffHead || null,
839
853
  driftMode: parsed.driftMode || null,
@@ -842,32 +856,32 @@ async function main() {
842
856
  exceptionExpires: parsed.exceptionExpires || null,
843
857
  exceptionScope: parsed.exceptionScope || null,
844
858
  exceptionClass: parsed.exceptionClass || null,
845
- dir: process.cwd()
859
+ dir: parsed.targetDir || process.cwd()
846
860
  };
847
861
 
848
862
  if (options.snapshotTags.length > 0 && !options.snapshot) {
849
863
  console.error('\n Error: --tag requires --snapshot.\n');
850
- process.exit(1);
864
+ process.exit(2);
851
865
  }
852
866
 
853
867
  if (options.snapshotMilestone && !options.snapshot) {
854
868
  console.error('\n Error: --milestone requires --snapshot.\n');
855
- process.exit(1);
869
+ process.exit(2);
856
870
  }
857
871
 
858
872
  if (options.snapshotMilestone && !SNAPSHOT_MILESTONES.includes(options.snapshotMilestone)) {
859
873
  console.error(`\n Error: Unsupported milestone '${options.snapshotMilestone}'. Use one of: ${SNAPSHOT_MILESTONES.join(', ')}.\n`);
860
- process.exit(1);
874
+ process.exit(2);
861
875
  }
862
876
 
863
877
  if (options.diffOnly && options.snapshot) {
864
878
  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);
879
+ process.exit(2);
866
880
  }
867
881
 
868
882
  if (options.driftMode && !['ci', 'pr', 'watch'].includes(options.driftMode)) {
869
883
  console.error(`\n Error: Unsupported drift mode '${options.driftMode}'. Use ci, pr, or watch.\n`);
870
- process.exit(1);
884
+ process.exit(2);
871
885
  }
872
886
 
873
887
  if (parsed.checkVersion) {
@@ -967,7 +981,7 @@ async function main() {
967
981
  console.error(' Fix: Run nerviq --help to see all available commands.');
968
982
  }
969
983
  console.error(' Docs: https://github.com/nerviq/nerviq#readme\n');
970
- process.exit(1);
984
+ process.exit(2);
971
985
  }
972
986
 
973
987
  if (!require('fs').existsSync(options.dir)) {
@@ -975,7 +989,7 @@ async function main() {
975
989
  console.error(' Why: The current working directory does not exist or is not accessible.');
976
990
  console.error(' Fix: cd into your project directory first, then run nerviq.');
977
991
  console.error(' Docs: https://github.com/nerviq/nerviq#getting-started\n');
978
- process.exit(1);
992
+ process.exit(2);
979
993
  }
980
994
 
981
995
  if (['setup', 'apply', 'benchmark'].includes(normalizedCommand)) {
@@ -1002,7 +1016,7 @@ async function main() {
1002
1016
  if (!FULL_COMMAND_SET.has(normalizedCommand)) {
1003
1017
  console.error(`\n Error: '${normalizedCommand}' is not supported for --platform codex.`);
1004
1018
  console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
1005
- process.exit(1);
1019
+ process.exit(2);
1006
1020
  }
1007
1021
  }
1008
1022
 
@@ -1010,7 +1024,7 @@ async function main() {
1010
1024
  if (!FULL_COMMAND_SET.has(normalizedCommand)) {
1011
1025
  console.error(`\n Error: '${normalizedCommand}' is not supported for --platform gemini.`);
1012
1026
  console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
1013
- process.exit(1);
1027
+ process.exit(2);
1014
1028
  }
1015
1029
  }
1016
1030
 
@@ -1018,7 +1032,7 @@ async function main() {
1018
1032
  if (!FULL_COMMAND_SET.has(normalizedCommand)) {
1019
1033
  console.error(`\n Error: '${normalizedCommand}' is not supported for --platform copilot.`);
1020
1034
  console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
1021
- process.exit(1);
1035
+ process.exit(2);
1022
1036
  }
1023
1037
  }
1024
1038
 
@@ -1026,7 +1040,7 @@ async function main() {
1026
1040
  if (!FULL_COMMAND_SET.has(normalizedCommand)) {
1027
1041
  console.error(`\n Error: '${normalizedCommand}' is not supported for --platform cursor.`);
1028
1042
  console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
1029
- process.exit(1);
1043
+ process.exit(2);
1030
1044
  }
1031
1045
  }
1032
1046
 
@@ -1035,7 +1049,7 @@ async function main() {
1035
1049
  if (!FULL_COMMAND_SET.has(normalizedCommand)) {
1036
1050
  console.error(`\n Error: '${normalizedCommand}' is not supported for --platform ${plat}.`);
1037
1051
  console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
1038
- process.exit(1);
1052
+ process.exit(2);
1039
1053
  }
1040
1054
  }
1041
1055
  }
@@ -1045,7 +1059,7 @@ async function main() {
1045
1059
  if (scanDirs.length === 0) {
1046
1060
  console.error('\n Error: scan requires at least one directory argument.');
1047
1061
  console.error(' Usage: npx nerviq scan dir1 dir2 dir3\n');
1048
- process.exit(1);
1062
+ process.exit(2);
1049
1063
  }
1050
1064
  const summary = await scanOrg(scanDirs, options);
1051
1065
  printScanDetail(summary, options);
@@ -1073,7 +1087,7 @@ async function main() {
1073
1087
  console.error('\n Error: org requires `scan` or `policy`.');
1074
1088
  console.error(' Usage: npx nerviq org scan dir1 dir2 dir3');
1075
1089
  console.error(' npx nerviq org policy [dir]\n');
1076
- process.exit(1);
1090
+ process.exit(2);
1077
1091
  }
1078
1092
  const summary = await scanOrg(scanDirs, options);
1079
1093
  if (options.json) {
@@ -1617,6 +1631,7 @@ async function main() {
1617
1631
  return;
1618
1632
  } else if (normalizedCommand === 'harmony-audit') {
1619
1633
  const { runHarmonyAudit } = require('../src/harmony/cli');
1634
+ collectAnonymousEvent('harmony-audit', { dir: options.dir });
1620
1635
  await runHarmonyAudit(options);
1621
1636
  process.exit(0);
1622
1637
  } else if (normalizedCommand === 'harmony-sync') {
@@ -2448,7 +2463,29 @@ async function main() {
2448
2463
  await runInit(options.dir, flags);
2449
2464
  process.exit(0);
2450
2465
  } else if (normalizedCommand === 'setup') {
2451
- await setup(options);
2466
+ collectAnonymousEvent('setup', { platform: options.platform, dir: options.dir });
2467
+ const setupResult = await setup({ ...options, silent: options.agentMode || options.json });
2468
+ if (options.agentMode) {
2469
+ // Agent-mode: structured JSON output with next steps
2470
+ const postAudit = await audit({ dir: options.dir, silent: true, platform: options.platform });
2471
+ const agentOutput = {
2472
+ status: setupResult.created > 0 ? 'files_created' : 'already_configured',
2473
+ created: setupResult.created,
2474
+ skipped: setupResult.skipped,
2475
+ written_files: setupResult.writtenFiles,
2476
+ preserved_files: setupResult.preservedFiles,
2477
+ detected_stacks: setupResult.stacks.map(s => s.key),
2478
+ rollback_id: setupResult.rollbackId,
2479
+ post_setup_score: postAudit.score,
2480
+ next_commands: [
2481
+ 'npx @nerviq/cli audit --json',
2482
+ setupResult.created > 0 ? `npx @nerviq/cli augment --platform ${options.platform}` : null,
2483
+ postAudit.score < 70 ? 'npx @nerviq/cli plan' : null,
2484
+ ].filter(Boolean),
2485
+ };
2486
+ console.log(JSON.stringify(agentOutput, null, 2));
2487
+ process.exit(0);
2488
+ }
2452
2489
  if (options.snapshot) {
2453
2490
  const postSetupResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
2454
2491
  const snapshot = writeSnapshotArtifact(options.dir, 'audit', postSetupResult, {
@@ -2469,8 +2506,32 @@ async function main() {
2469
2506
  }
2470
2507
  process.exit(0);
2471
2508
  }
2509
+ // MOAT-01: Harmony-first default — when 2+ platforms and platform not explicit
2510
+ 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');
2516
+ harmonyFirstResult = await harmonyAudit({ dir: options.dir, silent: true });
2517
+ if (!options.json && harmonyFirstResult) {
2518
+ const hs = harmonyFirstResult.harmonyScore;
2519
+ const driftCount = (harmonyFirstResult.drift && harmonyFirstResult.drift.drifts) ? harmonyFirstResult.drift.drifts.length : 0;
2520
+ const platformLabels = (harmonyFirstResult.activePlatforms || []).map(p => p.label || p.platform).join(' + ');
2521
+ const color = hs >= 70 ? '\x1b[32m' : hs >= 40 ? '\x1b[33m' : '\x1b[31m';
2522
+ const issueWord = driftCount === 1 ? 'issue' : 'issues';
2523
+ console.log('');
2524
+ console.log(`\x1b[1m Harmony Score: ${color}${hs}/100\x1b[0m — ${driftCount} drift ${issueWord} across ${detected.length} platforms (${platformLabels})`);
2525
+ console.log('\x1b[2m Run `nerviq harmony-audit` for the full cross-platform report. Use --no-harmony-first to hide.\x1b[0m');
2526
+ }
2527
+ } catch {
2528
+ harmonyFirstResult = null;
2529
+ }
2530
+ }
2531
+ }
2532
+
2472
2533
  let result;
2473
- const renderAuditJsonLocally = options.json && Boolean(options.driftMode);
2534
+ const renderAuditJsonLocally = options.json && (Boolean(options.driftMode) || Boolean(harmonyFirstResult));
2474
2535
  if (options.diffOnly) {
2475
2536
  const { getChangedFiles, buildDiffOnlyAuditView, printDiffOnlyAudit } = require('../src/diff-only');
2476
2537
  const fullResult = await audit({ ...options, silent: true });
@@ -2485,6 +2546,14 @@ async function main() {
2485
2546
  : await audit(options);
2486
2547
  }
2487
2548
 
2549
+ // ── Telemetry (opt-in, local only) ──
2550
+ collectAnonymousEvent('audit', {
2551
+ platform: result.platform || options.platform,
2552
+ score: result.score,
2553
+ checkCount: Array.isArray(result.results) ? result.results.length : null,
2554
+ dir: options.dir,
2555
+ });
2556
+
2488
2557
  if (options.driftMode) {
2489
2558
  const { buildContinuousStatus, formatContinuousStatus } = require('../src/continuous-ops');
2490
2559
  let campaigns = [];
@@ -2537,9 +2606,17 @@ async function main() {
2537
2606
  }
2538
2607
  }
2539
2608
  } else if (renderAuditJsonLocally) {
2609
+ const harmonyEnvelope = harmonyFirstResult ? {
2610
+ harmony: {
2611
+ score: harmonyFirstResult.harmonyScore,
2612
+ driftCount: (harmonyFirstResult.drift && harmonyFirstResult.drift.drifts) ? harmonyFirstResult.drift.drifts.length : 0,
2613
+ platforms: (harmonyFirstResult.activePlatforms || []).map(p => p.platform),
2614
+ },
2615
+ } : {};
2540
2616
  console.log(JSON.stringify({
2541
2617
  version,
2542
2618
  timestamp: new Date().toISOString(),
2619
+ ...harmonyEnvelope,
2543
2620
  ...result,
2544
2621
  }, null, 2));
2545
2622
  } else {
@@ -2664,7 +2741,7 @@ async function main() {
2664
2741
  } catch (err) {
2665
2742
  console.error(`\n Error: ${err.message}`);
2666
2743
  console.error(' Fix: Run `npx nerviq doctor` to diagnose common issues, or check https://github.com/nerviq/nerviq#troubleshooting');
2667
- process.exit(1);
2744
+ process.exit(2);
2668
2745
  }
2669
2746
  }
2670
2747
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nerviq/cli",
3
- "version": "1.14.0",
3
+ "version": "1.16.0",
4
4
  "description": "The intelligent nervous system for AI coding agents — 2,441 checks (8 platforms × ~300 governance rules), 10 languages, 62 domain packs. Audit, align, and amplify.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -379,6 +379,7 @@ function buildTopNextActions(failed, limit = 5, outcomeSummaryByKey = {}, option
379
379
  sourceUrl,
380
380
  module: CATEGORY_MODULES[category] || category,
381
381
  fix,
382
+ remediation_command: getRemediationCommand(key, category, options.platform),
382
383
  priorityScore,
383
384
  why: ACTION_RATIONALES[key] || fix,
384
385
  risk: riskFromImpact(impact),
@@ -399,6 +400,51 @@ function buildTopNextActions(failed, limit = 5, outcomeSummaryByKey = {}, option
399
400
  });
400
401
  }
401
402
 
403
+ /**
404
+ * Map check keys/categories to a shell command an agent can run to fix the issue.
405
+ * Returns null when no automated fix is available.
406
+ */
407
+ function getRemediationCommand(key, category, platform) {
408
+ const plat = platform || 'claude';
409
+
410
+ // Key-specific remediation commands
411
+ const KEY_COMMANDS = {
412
+ claudeMd: 'npx @nerviq/cli setup',
413
+ agentsMd: 'npx @nerviq/cli setup --platform codex',
414
+ geminiMd: 'npx @nerviq/cli setup --platform gemini',
415
+ copilotInstructions: 'npx @nerviq/cli setup --platform copilot',
416
+ cursorRules: 'npx @nerviq/cli setup --platform cursor',
417
+ windsurfRules: 'npx @nerviq/cli setup --platform windsurf',
418
+ aiderConfig: 'npx @nerviq/cli setup --platform aider',
419
+ opencodeConfig: 'npx @nerviq/cli setup --platform opencode',
420
+ settingsPermissions: 'npx @nerviq/cli plan --only permissions',
421
+ permissionDeny: 'npx @nerviq/cli plan --only permissions',
422
+ noBypassPermissions: 'npx @nerviq/cli plan --only permissions',
423
+ secretsProtection: 'npx @nerviq/cli plan --only permissions',
424
+ verificationLoop: `npx @nerviq/cli augment --platform ${plat}`,
425
+ lintCommand: `npx @nerviq/cli augment --platform ${plat}`,
426
+ testCommand: `npx @nerviq/cli augment --platform ${plat}`,
427
+ buildCommand: `npx @nerviq/cli augment --platform ${plat}`,
428
+ hookExists: 'npx @nerviq/cli plan --only hooks',
429
+ preCommitHook: 'npx @nerviq/cli plan --only hooks',
430
+ commandsExist: 'npx @nerviq/cli plan --only commands',
431
+ mcpServers: 'npx @nerviq/cli plan --mcp-pack context7',
432
+ };
433
+
434
+ if (KEY_COMMANDS[key]) return KEY_COMMANDS[key];
435
+
436
+ // Category-level fallback
437
+ const CATEGORY_COMMANDS = {
438
+ memory: `npx @nerviq/cli setup --platform ${plat}`,
439
+ security: `npx @nerviq/cli plan --only permissions --platform ${plat}`,
440
+ automation: `npx @nerviq/cli plan --only hooks --platform ${plat}`,
441
+ workflow: `npx @nerviq/cli plan --only commands --platform ${plat}`,
442
+ tools: `npx @nerviq/cli plan --mcp-pack context7 --platform ${plat}`,
443
+ };
444
+
445
+ return CATEGORY_COMMANDS[category] || `npx @nerviq/cli augment --platform ${plat}`;
446
+ }
447
+
402
448
  function getNextScoreMilestone(score) {
403
449
  return SCORE_MILESTONES.find((milestone) => score < milestone) || null;
404
450
  }
package/src/context.js CHANGED
@@ -74,10 +74,32 @@ class ProjectContext {
74
74
 
75
75
  /**
76
76
  * Return the contents of the project's CLAUDE.md (root or .claude/ location).
77
+ * If CLAUDE.md contains only a reference to another file (e.g., "AGENTS.md"),
78
+ * follows that reference and returns the referenced file's content appended.
77
79
  * @returns {string|null} File content or null if not found.
78
80
  */
79
81
  claudeMdContent() {
80
- return this.fileContent('CLAUDE.md') || this.fileContent('.claude/CLAUDE.md');
82
+ const raw = this.fileContent('CLAUDE.md') || this.fileContent('.claude/CLAUDE.md');
83
+ if (!raw) return null;
84
+
85
+ // If the file is very short and looks like a file reference, follow it.
86
+ // Pattern: a single line that is just a filename (e.g., "AGENTS.md" or "docs/CODING.md")
87
+ const trimmed = raw.trim();
88
+ if (trimmed.length < 200 && /^[a-zA-Z0-9_./-]+\.(md|txt|rst)$/m.test(trimmed)) {
89
+ const lines = trimmed.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
90
+ let combined = raw;
91
+ for (const line of lines) {
92
+ if (/^[a-zA-Z0-9_./-]+\.(md|txt|rst)$/.test(line)) {
93
+ const referenced = this.fileContent(line);
94
+ if (referenced) {
95
+ combined += '\n' + referenced;
96
+ }
97
+ }
98
+ }
99
+ return combined;
100
+ }
101
+
102
+ return raw;
81
103
  }
82
104
 
83
105
  /**