@nerviq/cli 1.29.1 → 1.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +238 -1
  2. package/README.md +24 -6
  3. package/SECURITY.md +4 -8
  4. package/bin/cli.js +281 -5
  5. package/docs/integration-contracts.md +1 -1
  6. package/package.json +10 -2
  7. package/sdk/README.md +12 -3
  8. package/sdk/examples/langchain-integration.md +128 -0
  9. package/sdk/examples/self-governing-agent.js +135 -0
  10. package/sdk/index.d.ts +115 -0
  11. package/sdk/index.js +94 -0
  12. package/sdk/package.json +11 -0
  13. package/src/activity.js +13 -0
  14. package/src/audit.js +116 -15
  15. package/src/auto-suggest.js +9 -2
  16. package/src/behavioral-drift.js +37 -2
  17. package/src/codex/freshness.js +7 -0
  18. package/src/copilot/freshness.js +7 -0
  19. package/src/freshness.js +7 -0
  20. package/src/gemini/freshness.js +9 -9
  21. package/src/safe-glyph.js +97 -0
  22. package/src/setup.js +6 -0
  23. package/src/shallow-risk/index.js +60 -3
  24. package/src/shallow-risk/patterns/agent-config-cross-platform-drift.js +1 -0
  25. package/src/shallow-risk/patterns/agent-config-dangerous-autoapprove.js +1 -0
  26. package/src/shallow-risk/patterns/agent-config-deprecated-keys.js +1 -0
  27. package/src/shallow-risk/patterns/agent-config-framework-version-mismatch.js +138 -0
  28. package/src/shallow-risk/patterns/agent-config-missing-file.js +1 -0
  29. package/src/shallow-risk/patterns/agent-config-script-not-in-package-json.js +108 -0
  30. package/src/shallow-risk/patterns/agent-config-secret-literal.js +3 -0
  31. package/src/shallow-risk/patterns/agent-config-stack-contradiction.js +1 -0
  32. package/src/shallow-risk/patterns/hook-script-missing.js +1 -0
  33. package/src/shallow-risk/patterns/mcp-server-no-allowlist.js +1 -0
  34. package/src/shallow-risk/shared.js +5 -0
  35. package/src/watch.js +46 -0
package/bin/cli.js CHANGED
@@ -40,7 +40,7 @@ const COMMAND_ALIASES = {
40
40
  gov: 'governance',
41
41
  outcome: 'feedback',
42
42
  };
43
- 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'];
43
+ 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', 'pr-check', 'help', 'version'];
44
44
 
45
45
  function levenshtein(a, b) {
46
46
  const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
@@ -675,6 +675,12 @@ const HELP = `
675
675
  nerviq exception list Show active and expired exceptions
676
676
  nerviq exception prune Remove expired exceptions
677
677
 
678
+ PR / CI INTEGRATION
679
+ nerviq pr-check Composite PR check: audit + diff-only +
680
+ threshold gate, emits markdown + JSON for
681
+ PR-comment / GitHub-Action consumers
682
+ nerviq pr-check --threshold 80 --diff-base main --diff-head HEAD --json
683
+
678
684
  TEAM PROFILES
679
685
  nerviq profile save <name> Save current preferences as a named profile
680
686
  nerviq profile load <name> Load and display a saved profile
@@ -850,6 +856,7 @@ async function main() {
850
856
  badge: flags.includes('--badge'),
851
857
  quiet: flags.includes('--quiet'),
852
858
  agentMode: flags.includes('--agent-mode'),
859
+ agentReady: flags.includes('--agent-ready'),
853
860
  autoSync: flags.includes('--auto-sync'),
854
861
  dryRun: flags.includes('--dry-run'),
855
862
  apply: flags.includes('--apply'),
@@ -1615,6 +1622,94 @@ async function main() {
1615
1622
  }
1616
1623
  process.exit(0);
1617
1624
  } else if (normalizedCommand === 'certify') {
1625
+ // AI-13: --agent-ready mode runs a focused agent-readiness audit
1626
+ // (separate from the general harmony-based certification). Six pass/fail
1627
+ // criteria specific to whether AI coding agents can safely operate in
1628
+ // this repo. Designed to be agent-callable: returns a clean JSON
1629
+ // verdict object suitable for an agent to consume programmatically.
1630
+ if (options.agentReady) {
1631
+ const dir = options.dir || process.cwd();
1632
+ const fs = require('fs');
1633
+ const path = require('path');
1634
+ const { audit } = require('../src/audit');
1635
+ const result = await audit({ dir, silent: true });
1636
+ const exists = (p) => fs.existsSync(path.join(dir, p));
1637
+ const claudeMdPresent = exists('CLAUDE.md') || exists('.claude/CLAUDE.md');
1638
+ const agentsMdPresent = exists('AGENTS.md');
1639
+ const hasContext = claudeMdPresent || agentsMdPresent;
1640
+ const gitignoreEnv = (() => {
1641
+ if (!exists('.gitignore')) return false;
1642
+ try {
1643
+ const gi = fs.readFileSync(path.join(dir, '.gitignore'), 'utf8');
1644
+ return /\.env\b/.test(gi);
1645
+ } catch { return false; }
1646
+ })();
1647
+ const denyRulesConfigured = (() => {
1648
+ if (!exists('.claude/settings.json')) return false;
1649
+ try {
1650
+ const s = JSON.parse(fs.readFileSync(path.join(dir, '.claude/settings.json'), 'utf8'));
1651
+ const deny = (s.permissions && s.permissions.deny) || [];
1652
+ return Array.isArray(deny) && deny.length > 0;
1653
+ } catch { return false; }
1654
+ })();
1655
+ const hints = Array.isArray(result.shallowRiskHints) ? result.shallowRiskHints : [];
1656
+ const noCriticalShallowRisk = !hints.some((h) => h && h.severity === 'critical');
1657
+ const noStaleReferences = !(result.staleReferences && result.staleReferences.count > 0);
1658
+ const checks = [
1659
+ { id: 'agent-context-present', label: 'CLAUDE.md or AGENTS.md exists', passed: hasContext, critical: true },
1660
+ { id: 'gitignore-blocks-env', label: '.gitignore blocks .env files', passed: gitignoreEnv, critical: true },
1661
+ { id: 'deny-rules-configured', label: '.claude/settings.json has permission deny rules', passed: denyRulesConfigured, critical: false },
1662
+ { id: 'no-critical-shallow-risk', label: 'No critical shallow-risk findings (secrets, dangerous auto-approve)', passed: noCriticalShallowRisk, critical: true },
1663
+ { id: 'no-stale-references', label: 'No stale references in agent docs (PROD-03)', passed: noStaleReferences, critical: false },
1664
+ { id: 'governance-score-floor', label: 'Audit score >= 50', passed: result.score >= 50, critical: false },
1665
+ ];
1666
+ const failedCritical = checks.filter((c) => c.critical && !c.passed);
1667
+ const passedCount = checks.filter((c) => c.passed).length;
1668
+ const verdict = failedCritical.length === 0
1669
+ ? (passedCount === checks.length ? 'agent-ready-full' : 'agent-ready-with-caveats')
1670
+ : 'not-agent-ready';
1671
+ const payload = {
1672
+ command: 'certify --agent-ready',
1673
+ verdict,
1674
+ passedCount,
1675
+ totalChecks: checks.length,
1676
+ score: result.score,
1677
+ checks: checks.map((c) => ({
1678
+ id: c.id,
1679
+ label: c.label,
1680
+ passed: c.passed,
1681
+ critical: c.critical,
1682
+ })),
1683
+ badge: verdict === 'agent-ready-full'
1684
+ ? '[![Nerviq agent-ready](https://img.shields.io/badge/nerviq-agent--ready-green)](https://nerviq.net)'
1685
+ : verdict === 'agent-ready-with-caveats'
1686
+ ? '[![Nerviq agent-ready (caveats)](https://img.shields.io/badge/nerviq-agent--ready--caveats-yellow)](https://nerviq.net)'
1687
+ : '[![Nerviq not agent-ready](https://img.shields.io/badge/nerviq-not--agent--ready-red)](https://nerviq.net)',
1688
+ exitCode: failedCritical.length === 0 ? 0 : 1,
1689
+ };
1690
+ if (options.json) {
1691
+ process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
1692
+ } else {
1693
+ console.log('');
1694
+ console.log(' nerviq certify --agent-ready');
1695
+ console.log(' ═══════════════════════════════════════');
1696
+ console.log('');
1697
+ const verdictColor = verdict === 'agent-ready-full' ? '\x1b[32m' : verdict === 'agent-ready-with-caveats' ? '\x1b[33m' : '\x1b[31m';
1698
+ console.log(` Verdict: ${verdictColor}${verdict}\x1b[0m (${passedCount}/${checks.length} checks passed)`);
1699
+ console.log('');
1700
+ for (const c of checks) {
1701
+ const mark = c.passed ? '\x1b[32m✓\x1b[0m' : (c.critical ? '\x1b[31m✗\x1b[0m' : '\x1b[33m!\x1b[0m');
1702
+ const tag = c.critical ? '\x1b[2m[critical]\x1b[0m' : '\x1b[2m[advisory]\x1b[0m';
1703
+ console.log(` ${mark} ${c.label} ${tag}`);
1704
+ }
1705
+ console.log('');
1706
+ console.log(` Badge:`);
1707
+ console.log(` ${payload.badge}`);
1708
+ console.log('');
1709
+ }
1710
+ process.exit(payload.exitCode);
1711
+ }
1712
+
1618
1713
  const { certifyProject, generateCertBadge } = require('../src/certification');
1619
1714
  const certResult = await certifyProject(options.dir);
1620
1715
  if (options.json) {
@@ -1912,7 +2007,16 @@ async function main() {
1912
2007
  if (subcommand === 'list') {
1913
2008
  const records = listExceptions(options.dir);
1914
2009
  if (options.json) {
1915
- console.log(JSON.stringify(records, null, 2));
2010
+ // BUG-06 fix: stable envelope shape for governance/compliance
2011
+ // automation. Previously emitted raw array; consumers checking
2012
+ // for `.records` (the conventional envelope key) got undefined.
2013
+ // Now: { records: [...], count, generatedAt } — array shape is
2014
+ // the same regardless of count (0, 1, or N).
2015
+ console.log(JSON.stringify({
2016
+ records,
2017
+ count: records.length,
2018
+ generatedAt: new Date().toISOString(),
2019
+ }, null, 2));
1916
2020
  } else {
1917
2021
  console.log('');
1918
2022
  console.log(formatExceptionsList(records));
@@ -1931,7 +2035,14 @@ async function main() {
1931
2035
  scope: options.exceptionScope || 'all',
1932
2036
  });
1933
2037
  if (options.json) {
1934
- console.log(JSON.stringify(result.record, null, 2));
2038
+ // BUG-06 fix: matched envelope shape for symmetry with `list`.
2039
+ // Single-record add returns the same `records: [...]` array so
2040
+ // automation can use one parser for both commands.
2041
+ console.log(JSON.stringify({
2042
+ records: result.record ? [result.record] : [],
2043
+ count: result.record ? 1 : 0,
2044
+ generatedAt: new Date().toISOString(),
2045
+ }, null, 2));
1935
2046
  } else {
1936
2047
  console.log('');
1937
2048
  console.log(` Exception added: ${result.record.id}`);
@@ -1956,6 +2067,110 @@ async function main() {
1956
2067
 
1957
2068
  console.error('\n Error: exception supports `add`, `list`, and `prune`.\n');
1958
2069
  process.exit(1);
2070
+ } else if (normalizedCommand === 'pr-check') {
2071
+ // LOOP-02: composite PR-check command. Runs the right pieces in the
2072
+ // right order with one consolidated PR-comment-friendly output.
2073
+ // The primitives already exist (audit --diff-only --drift-mode ci,
2074
+ // --threshold, --require, --format=markdown); pr-check just unifies
2075
+ // them so Team-tier CI integrations don't have to assemble the
2076
+ // composite themselves.
2077
+ const dir = options.dir || process.cwd();
2078
+ const threshold = options.threshold !== null ? options.threshold : 70;
2079
+ const platform = options.platform;
2080
+
2081
+ // Step 1: full audit (markdown format, no harmony banner contamination
2082
+ // because BUG-02 fixed the suppress).
2083
+ const fullAudit = await audit({
2084
+ dir,
2085
+ platform,
2086
+ silent: true,
2087
+ });
2088
+
2089
+ // Step 2: diff-only audit if a baseline exists or --diff-base/--diff-head
2090
+ // are provided. Otherwise gracefully skip.
2091
+ let diffSection = null;
2092
+ try {
2093
+ const { getChangedFiles, buildDiffOnlyAuditView } = require('../src/diff-only');
2094
+ const diffInfo = getChangedFiles(dir, {
2095
+ diffBase: options.diffBase,
2096
+ diffHead: options.diffHead,
2097
+ });
2098
+ if (diffInfo && Array.isArray(diffInfo.changedFiles) && diffInfo.changedFiles.length > 0) {
2099
+ diffSection = buildDiffOnlyAuditView(fullAudit, diffInfo);
2100
+ }
2101
+ } catch {
2102
+ diffSection = null;
2103
+ }
2104
+
2105
+ // Step 3: build a markdown summary suitable for posting as a PR comment.
2106
+ const lines = [];
2107
+ lines.push('## Nerviq PR Check');
2108
+ lines.push('');
2109
+ lines.push(`- **Score:** ${fullAudit.score}/100 (organic ${fullAudit.organicScore || fullAudit.score}/100)`);
2110
+ lines.push(`- **Threshold:** ${threshold}`);
2111
+ lines.push(`- **Platform:** ${fullAudit.platformLabel || fullAudit.platform || platform || 'auto-detected'}`);
2112
+ lines.push(`- **Failed checks:** ${fullAudit.failed} (passed ${fullAudit.passed})`);
2113
+
2114
+ if (fullAudit.staleReferences && fullAudit.staleReferences.count > 0) {
2115
+ lines.push('');
2116
+ lines.push(`### 📌 Stale references in agent docs: ${fullAudit.staleReferences.count}`);
2117
+ for (const sample of fullAudit.staleReferences.topSample) {
2118
+ lines.push(`- \`${sample.file || '?'}:${sample.line || '?'}\` — ${sample.fix}`);
2119
+ }
2120
+ }
2121
+
2122
+ if (diffSection && Array.isArray(diffSection.changedFiles) && diffSection.changedFiles.length > 0) {
2123
+ lines.push('');
2124
+ lines.push(`### Changed files in this PR: ${diffSection.changedFiles.length}`);
2125
+ for (const cf of diffSection.changedFiles.slice(0, 10)) {
2126
+ lines.push(`- \`${cf}\``);
2127
+ }
2128
+ if (diffSection.changedFiles.length > 10) {
2129
+ lines.push(`- … and ${diffSection.changedFiles.length - 10} more`);
2130
+ }
2131
+ }
2132
+
2133
+ const topActions = (fullAudit.liteSummary && fullAudit.liteSummary.topNextActions) || [];
2134
+ if (topActions.length > 0) {
2135
+ lines.push('');
2136
+ lines.push('### Top next actions');
2137
+ for (const a of topActions.slice(0, 5)) {
2138
+ lines.push(`- **${a.name}** (${a.impact}) — ${a.fix}`);
2139
+ }
2140
+ }
2141
+
2142
+ const gateFailed = fullAudit.score < threshold;
2143
+ lines.push('');
2144
+ lines.push(`### Gate: ${gateFailed ? '❌ FAIL' : '✅ PASS'}`);
2145
+ lines.push(`Score ${fullAudit.score} ${gateFailed ? '<' : '≥'} threshold ${threshold}`);
2146
+
2147
+ const markdown = lines.join('\n');
2148
+
2149
+ if (options.json) {
2150
+ const payload = {
2151
+ command: 'pr-check',
2152
+ gate: gateFailed ? 'fail' : 'pass',
2153
+ threshold,
2154
+ exitCode: gateFailed ? 1 : 0,
2155
+ score: fullAudit.score,
2156
+ organicScore: fullAudit.organicScore,
2157
+ platform: fullAudit.platform,
2158
+ platformLabel: fullAudit.platformLabel,
2159
+ passed: fullAudit.passed,
2160
+ failed: fullAudit.failed,
2161
+ staleReferences: fullAudit.staleReferences || null,
2162
+ changedFiles: diffSection ? diffSection.changedFiles : [],
2163
+ topNextActions: topActions.slice(0, 5),
2164
+ markdown,
2165
+ };
2166
+ process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
2167
+ } else {
2168
+ console.log('');
2169
+ console.log(markdown);
2170
+ console.log('');
2171
+ }
2172
+
2173
+ process.exit(gateFailed ? 1 : 0);
1959
2174
  } else if (normalizedCommand === 'profile') {
1960
2175
  const { saveProfile, loadProfile, listProfiles, exportProfile, formatProfileList, formatProfile } = require('../src/profiles');
1961
2176
  const subcommand = parsed.extraArgs[0];
@@ -2296,6 +2511,10 @@ async function main() {
2296
2511
  process.exit(0);
2297
2512
  }
2298
2513
  // MOAT-01: Harmony-first default — when 2+ platforms and platform not explicit
2514
+ // BUG-02 fix: also suppress the human-readable Harmony banner when a
2515
+ // machine format is requested (sarif/junit/csv/markdown), so parsers
2516
+ // consuming stdout don't have to add `--no-harmony-first` defensively.
2517
+ const machineFormat = options.format && ['sarif', 'junit', 'csv', 'markdown'].includes(String(options.format).toLowerCase());
2299
2518
  let harmonyFirstResult = null;
2300
2519
  if (!options.platformExplicit && !options.noHarmonyFirst && !options.diffOnly && !options.driftMode && !options.workspace) {
2301
2520
  const detected = detectPlatforms(options.dir) || [];
@@ -2303,7 +2522,8 @@ async function main() {
2303
2522
  try {
2304
2523
  const { harmonyAudit } = require('../src/harmony/audit');
2305
2524
  harmonyFirstResult = await harmonyAudit({ dir: options.dir, silent: true });
2306
- if (!options.json && harmonyFirstResult) {
2525
+ const suppressBanner = options.json || machineFormat;
2526
+ if (!suppressBanner && harmonyFirstResult) {
2307
2527
  const hs = harmonyFirstResult.harmonyScore;
2308
2528
  const driftCount = (harmonyFirstResult.drift && harmonyFirstResult.drift.drifts) ? harmonyFirstResult.drift.drifts.length : 0;
2309
2529
  const platformLabels = (harmonyFirstResult.activePlatforms || []).map(p => p.label || p.platform).join(' + ');
@@ -2321,7 +2541,14 @@ async function main() {
2321
2541
 
2322
2542
  if (options.fix) {
2323
2543
  if (options.diffOnly) {
2324
- console.error('\n Error: --diff-only cannot be combined with --fix.\n');
2544
+ if (options.json) {
2545
+ process.stdout.write(JSON.stringify({
2546
+ error: '--diff-only cannot be combined with --fix.',
2547
+ exitCode: 2,
2548
+ }) + '\n');
2549
+ } else {
2550
+ console.error('\n Error: --diff-only cannot be combined with --fix.\n');
2551
+ }
2325
2552
  process.exit(2);
2326
2553
  }
2327
2554
 
@@ -2330,6 +2557,11 @@ async function main() {
2330
2557
  const failedResults = (auditResult.results || []).filter((item) => item.passed === false);
2331
2558
  const targetKeys = getFixableFailedResults(failedResults, { mode: 'audit' }).map((item) => item.key);
2332
2559
 
2560
+ // BUG-01 fix: under --json, suppress human-readable autofix narration
2561
+ // and emit the full outcome shape as one valid JSON document instead.
2562
+ const silentLogger = options.json
2563
+ ? { log() {}, warn() {}, error() {} }
2564
+ : console;
2333
2565
  const outcome = await runAuditFixWorkflow({
2334
2566
  dir: options.dir,
2335
2567
  platform: options.platform,
@@ -2339,7 +2571,51 @@ async function main() {
2339
2571
  apply: options.apply,
2340
2572
  pr: options.pr,
2341
2573
  outputPath: options.out,
2574
+ logger: silentLogger,
2342
2575
  });
2576
+ if (options.json) {
2577
+ // Serialize the outcome as the canonical machine contract for
2578
+ // `audit --fix --json`. Includes plan, exitCode, patchArtifact,
2579
+ // rollbackArtifact, reAudit summary, unresolvedKeys, warnings,
2580
+ // and branchName when --pr was used.
2581
+ const payload = {
2582
+ command: 'audit --fix',
2583
+ mode: outcome.branchName ? 'pr'
2584
+ : (options.apply ? 'apply' : 'dry-run'),
2585
+ exitCode: outcome.exitCode,
2586
+ requestedKeys: outcome.requestedKeys || [],
2587
+ plan: outcome.plan || [],
2588
+ advisoryOnly: outcome.advisoryOnly || [],
2589
+ patchArtifact: outcome.patchArtifact
2590
+ ? {
2591
+ path: outcome.patchArtifact.path,
2592
+ relativePath: outcome.patchArtifact.relativePath,
2593
+ }
2594
+ : null,
2595
+ rollbackArtifact: outcome.rollbackArtifact
2596
+ ? {
2597
+ path: outcome.rollbackArtifact.path,
2598
+ relativePath: outcome.rollbackArtifact.relativePath,
2599
+ }
2600
+ : null,
2601
+ reAudit: outcome.reAudit
2602
+ ? {
2603
+ score: outcome.reAudit.score,
2604
+ organicScore: outcome.reAudit.organicScore,
2605
+ passed: Array.isArray(outcome.reAudit.results)
2606
+ ? outcome.reAudit.results.filter((r) => r.passed === true).length
2607
+ : null,
2608
+ failed: Array.isArray(outcome.reAudit.results)
2609
+ ? outcome.reAudit.results.filter((r) => r.passed === false).length
2610
+ : null,
2611
+ }
2612
+ : null,
2613
+ unresolvedKeys: outcome.unresolvedKeys || [],
2614
+ branchName: outcome.branchName || null,
2615
+ warnings: outcome.warnings || [],
2616
+ };
2617
+ process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
2618
+ }
2343
2619
  process.exit(outcome.exitCode);
2344
2620
  }
2345
2621
 
@@ -92,7 +92,7 @@ Example:
92
92
  "suggestedNextCommand": "npx nerviq fix verificationLoop"
93
93
  },
94
94
  "meta": {
95
- "cliVersion": "1.29.1",
95
+ "cliVersion": "1.30.0",
96
96
  "source": "nerviq-cli",
97
97
  "webhookFormat": "generic-audit-event"
98
98
  }
package/package.json CHANGED
@@ -1,8 +1,14 @@
1
1
  {
2
2
  "name": "@nerviq/cli",
3
- "version": "1.29.1",
3
+ "version": "1.30.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
+ "exports": {
7
+ ".": "./src/index.js",
8
+ "./sdk": "./sdk/index.js",
9
+ "./sdk/types": "./sdk/index.d.ts",
10
+ "./package.json": "./package.json"
11
+ },
6
12
  "bin": {
7
13
  "nerviq": "bin/cli.js",
8
14
  "@nerviq/cli": "bin/cli.js",
@@ -11,10 +17,10 @@
11
17
  "files": [
12
18
  "bin",
13
19
  "src",
20
+ "sdk",
14
21
  "README.md",
15
22
  "docs",
16
23
  "contracts",
17
- "sdk/README.md",
18
24
  "CHANGELOG.md",
19
25
  "SECURITY.md"
20
26
  ],
@@ -25,10 +31,12 @@
25
31
  "verify:release-metadata": "node tools/validate-release-metadata.js",
26
32
  "prepublish:check": "node tools/pre-publish.js",
27
33
  "prepublishOnly": "node tools/pre-publish.js",
34
+ "postinstall": "node tools/postinstall.js || true",
28
35
  "test:jest": "jest",
29
36
  "test:coverage": "jest --coverage",
30
37
  "test:all": "npm test && npx jest && node test/check-matrix.js && node test/codex-check-matrix.js && node test/gemini-check-matrix.js && node test/copilot-check-matrix.js && node test/cursor-check-matrix.js && node test/windsurf-check-matrix.js && node test/aider-check-matrix.js && node test/opencode-check-matrix.js && node test/golden-matrix.js && node test/codex-golden-matrix.js && node test/gemini-golden-matrix.js && node test/copilot-golden-matrix.js && node test/cursor-golden-matrix.js && node test/windsurf-golden-matrix.js && node test/aider-golden-matrix.js && node test/opencode-golden-matrix.js",
31
38
  "benchmark:perf": "node tools/benchmark.js",
39
+ "announce:release": "node tools/announce-release.js",
32
40
  "catalog": "node -e \"const {generateCatalog}=require('./src/catalog');console.log(JSON.stringify(generateCatalog(),null,2))\""
33
41
  },
34
42
  "keywords": [
package/sdk/README.md CHANGED
@@ -1,13 +1,22 @@
1
- # @nerviq/sdk
1
+ # Nerviq SDK (bundled inside `@nerviq/cli`)
2
2
 
3
3
  Programmatic SDK for Nerviq audit, Harmony, catalog access, and experimental Synergy workflows.
4
4
 
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm install @nerviq/sdk
8
+ npm install @nerviq/cli
9
9
  ```
10
10
 
11
+ The SDK ships bundled inside `@nerviq/cli` per MEMO-03 (B = BUNDLE, signed 2026-04-28). There is no separate `@nerviq/sdk` npm package — earlier docs that referenced one were aspirational. Both import paths below work:
12
+
13
+ ```js
14
+ const sdk = require('@nerviq/cli/sdk'); // explicit SDK subpath (typed, input-validated)
15
+ const { audit } = require('@nerviq/cli'); // top-level — same exports
16
+ ```
17
+
18
+ The two paths return identical surface; pick whichever is cleaner for your codebase.
19
+
11
20
  ## Stability
12
21
 
13
22
  - Stable for production workflows: `audit`, `harmonyAudit`, `detectPlatforms`, `getCatalog`
@@ -17,7 +26,7 @@ npm install @nerviq/sdk
17
26
  ## Quick Start
18
27
 
19
28
  ```js
20
- const { audit, harmonyAudit, detectPlatforms } = require('@nerviq/sdk');
29
+ const { audit, harmonyAudit, detectPlatforms } = require('@nerviq/cli/sdk');
21
30
 
22
31
  async function main() {
23
32
  const repoDir = process.cwd();
@@ -0,0 +1,128 @@
1
+ # LangChain integration — using Nerviq as a tool
2
+
3
+ > Reference example for AI-09. Wires `@nerviq/cli/sdk` into a LangChain
4
+ > agent as a callable tool, so an autonomous LangChain agent can audit
5
+ > its own repo, check Harmony Score, and surface stale references mid-task.
6
+ >
7
+ > Pairs with: [`self-governing-agent.js`](./self-governing-agent.js) +
8
+ > [/docs/for-agents](https://nerviq.net/docs/for-agents) on the site.
9
+
10
+ ## Why an agent should call Nerviq
11
+
12
+ A LangChain agent operating inside a developer's repo benefits from knowing whether the agent-config files it's reading are coherent across platforms. Without that awareness, the agent can confidently follow instructions in `CLAUDE.md` that contradict instructions in `AGENTS.md` — and produce code that breaks in someone else's tooling.
13
+
14
+ Wiring Nerviq as a LangChain tool exposes three primitives:
15
+
16
+ - `nerviq_audit` — score the repo on a specific platform
17
+ - `nerviq_harmony` — measure cross-platform drift
18
+ - `nerviq_stale_references` — surface the headline stale-reference findings
19
+
20
+ ## JavaScript / Node example
21
+
22
+ ```js
23
+ const { audit, harmonyAudit } = require('@nerviq/cli/sdk');
24
+ const { DynamicTool } = require('@langchain/core/tools');
25
+
26
+ const nerviqAuditTool = new DynamicTool({
27
+ name: 'nerviq_audit',
28
+ description:
29
+ 'Audit the AI coding agent configuration of the given repo directory. ' +
30
+ 'Returns score (0-100), passed/failed counts, top stale-reference findings, ' +
31
+ 'and topNextActions. Call this before substantive code changes when the ' +
32
+ 'task touches CLAUDE.md, AGENTS.md, .cursor/rules, .mcp.json, or hooks.',
33
+ func: async (dir) => {
34
+ const result = await audit(dir || process.cwd(), 'claude');
35
+ return JSON.stringify({
36
+ score: result.score,
37
+ organicScore: result.organicScore,
38
+ passed: result.passed,
39
+ failed: result.failed,
40
+ staleReferences: result.staleReferences || null,
41
+ topNextActions: (result.liteSummary && result.liteSummary.topNextActions) || [],
42
+ }, null, 2);
43
+ },
44
+ });
45
+
46
+ const nerviqHarmonyTool = new DynamicTool({
47
+ name: 'nerviq_harmony',
48
+ description:
49
+ 'Measure cross-platform configuration drift between AI coding agents in ' +
50
+ 'the given repo. Returns harmonyScore (0-100) plus a list of named ' +
51
+ 'drifts. Only meaningful when 2+ platforms are detected.',
52
+ func: async (dir) => {
53
+ const result = await harmonyAudit(dir || process.cwd());
54
+ return JSON.stringify({
55
+ harmonyScore: result.harmonyScore,
56
+ activePlatforms: result.activePlatforms,
57
+ drifts: (result.drift && result.drift.drifts) || [],
58
+ }, null, 2);
59
+ },
60
+ });
61
+
62
+ // Add to your agent's tool list:
63
+ const tools = [nerviqAuditTool, nerviqHarmonyTool /*, ...your other tools */];
64
+ ```
65
+
66
+ ## Python via subprocess
67
+
68
+ LangChain agents in Python can shell out to the CLI's `--agent-mode --json` surface:
69
+
70
+ ```python
71
+ from langchain_core.tools import tool
72
+ import json
73
+ import subprocess
74
+
75
+ @tool
76
+ def nerviq_audit(dir: str = ".") -> str:
77
+ """Audit AI coding agent configuration. Returns score, stale references,
78
+ top next actions. Call before substantive code changes."""
79
+ result = subprocess.run(
80
+ ["npx", "@nerviq/cli", "audit", "--json", "--agent-mode", "--dir", dir],
81
+ capture_output=True, text=True, timeout=60,
82
+ )
83
+ if result.returncode not in (0, 1, 2):
84
+ return json.dumps({"error": result.stderr})
85
+ return result.stdout # Already JSON
86
+ ```
87
+
88
+ ## CrewAI / AutoGen / generic orchestrators
89
+
90
+ Same pattern: any orchestrator that supports tool definitions can wrap the SDK or shell out to `npx @nerviq/cli audit --json`. The JSON envelope is documented at [/docs/for-agents](https://nerviq.net/docs/for-agents) and stable per CTO-01..05 + BUG-01 (machine-output contract).
91
+
92
+ For CrewAI specifically:
93
+
94
+ ```python
95
+ from crewai.tools import tool
96
+ import subprocess
97
+
98
+ @tool("Nerviq audit tool")
99
+ def nerviq_audit(dir: str = "."):
100
+ """Audit cross-platform AI coding agent configuration."""
101
+ out = subprocess.run(
102
+ ["npx", "@nerviq/cli", "audit", "--json", "--dir", dir],
103
+ capture_output=True, text=True, timeout=60,
104
+ ).stdout
105
+ return out
106
+ ```
107
+
108
+ ## Don't bypass user consent
109
+
110
+ Per the [/docs/for-agents](https://nerviq.net/docs/for-agents) trust-boundary policy: the agent should NOT silently apply `--apply --auto` on critical fixes that materially modify governance posture (deny rules, MCP permissions, hooks). Surface the plan via the audit/harmony tool, let the user approve, then apply. The CLI gates `--apply` on `--auto` for exactly this reason — single-flag bypass is intentionally blocked.
111
+
112
+ ## When to call which tool
113
+
114
+ | Situation | Call |
115
+ |---|---|
116
+ | Task start | `nerviq_audit` (always) |
117
+ | Task touches multiple agents' config | `nerviq_harmony` (drift check) |
118
+ | Stale-reference count > 0 in audit result | Surface to user via the audit response, ask whether to proceed |
119
+ | Task complete | `nerviq_audit` again, compare scores, surface delta to user |
120
+ | User accepts a recommendation | (Optionally) record via `npx @nerviq/cli feedback --key <K> --status accepted` so the local learning loop benefits |
121
+
122
+ ## Reference repo
123
+
124
+ The full self-governing loop reference (5-step pre/harmony/task/post/feedback pattern) lives at [`sdk/examples/self-governing-agent.js`](./self-governing-agent.js). Read that first if you're implementing the orchestration manually rather than letting LangChain/CrewAI drive the loop.
125
+
126
+ ## License
127
+
128
+ CC0 — copy, modify, integrate freely.