@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.
- package/CHANGELOG.md +238 -1
- package/README.md +24 -6
- package/SECURITY.md +4 -8
- package/bin/cli.js +281 -5
- package/docs/integration-contracts.md +1 -1
- package/package.json +10 -2
- package/sdk/README.md +12 -3
- package/sdk/examples/langchain-integration.md +128 -0
- package/sdk/examples/self-governing-agent.js +135 -0
- package/sdk/index.d.ts +115 -0
- package/sdk/index.js +94 -0
- package/sdk/package.json +11 -0
- package/src/activity.js +13 -0
- package/src/audit.js +116 -15
- package/src/auto-suggest.js +9 -2
- package/src/behavioral-drift.js +37 -2
- package/src/codex/freshness.js +7 -0
- package/src/copilot/freshness.js +7 -0
- package/src/freshness.js +7 -0
- package/src/gemini/freshness.js +9 -9
- package/src/safe-glyph.js +97 -0
- package/src/setup.js +6 -0
- package/src/shallow-risk/index.js +60 -3
- package/src/shallow-risk/patterns/agent-config-cross-platform-drift.js +1 -0
- package/src/shallow-risk/patterns/agent-config-dangerous-autoapprove.js +1 -0
- package/src/shallow-risk/patterns/agent-config-deprecated-keys.js +1 -0
- package/src/shallow-risk/patterns/agent-config-framework-version-mismatch.js +138 -0
- package/src/shallow-risk/patterns/agent-config-missing-file.js +1 -0
- package/src/shallow-risk/patterns/agent-config-script-not-in-package-json.js +108 -0
- package/src/shallow-risk/patterns/agent-config-secret-literal.js +3 -0
- package/src/shallow-risk/patterns/agent-config-stack-contradiction.js +1 -0
- package/src/shallow-risk/patterns/hook-script-missing.js +1 -0
- package/src/shallow-risk/patterns/mcp-server-no-allowlist.js +1 -0
- package/src/shallow-risk/shared.js +5 -0
- 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
|
+
? '[](https://nerviq.net)'
|
|
1685
|
+
: verdict === 'agent-ready-with-caveats'
|
|
1686
|
+
? '[](https://nerviq.net)'
|
|
1687
|
+
: '[](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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nerviq/cli",
|
|
3
|
-
"version": "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
|
-
#
|
|
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/
|
|
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.
|