@nerviq/cli 0.9.2 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js CHANGED
@@ -7,6 +7,7 @@ const { buildProposalBundle, printProposalBundle, writePlanFile, applyProposalBu
7
7
  const { getGovernanceSummary, printGovernanceSummary, ensureWritableProfile, renderGovernanceMarkdown } = require('../src/governance');
8
8
  const { runBenchmark, printBenchmark, writeBenchmarkReport } = require('../src/benchmark');
9
9
  const { writeSnapshotArtifact, recordRecommendationOutcome, formatRecommendationOutcomeSummary, getRecommendationOutcomeSummary } = require('../src/activity');
10
+ const { collectFeedback } = require('../src/feedback');
10
11
  const { version } = require('../package.json');
11
12
 
12
13
  const args = process.argv.slice(2);
@@ -20,7 +21,7 @@ const COMMAND_ALIASES = {
20
21
  gov: 'governance',
21
22
  outcome: 'feedback',
22
23
  };
23
- const KNOWN_COMMANDS = ['audit', 'setup', 'augment', 'suggest-only', 'plan', 'apply', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'help', 'version'];
24
+ const KNOWN_COMMANDS = ['audit', 'setup', 'augment', 'suggest-only', 'plan', 'apply', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'doctor', 'convert', 'migrate', 'help', 'version'];
24
25
 
25
26
  function levenshtein(a, b) {
26
27
  const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
@@ -73,11 +74,15 @@ function parseArgs(rawArgs) {
73
74
  let format = null;
74
75
  let commandSet = false;
75
76
  let extraArgs = [];
77
+ let convertFrom = null;
78
+ let convertTo = null;
79
+ let migrateFrom = null;
80
+ let migrateTo = null;
76
81
 
77
82
  for (let i = 0; i < rawArgs.length; i++) {
78
83
  const arg = rawArgs[i];
79
84
 
80
- 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') {
85
+ 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') {
81
86
  const value = rawArgs[i + 1];
82
87
  if (!value || value.startsWith('--')) {
83
88
  throw new Error(`${arg} requires a value`);
@@ -97,6 +102,8 @@ function parseArgs(rawArgs) {
97
102
  if (arg === '--score-delta') feedbackScoreDelta = value.trim();
98
103
  if (arg === '--platform') platform = value.trim().toLowerCase();
99
104
  if (arg === '--format') format = value.trim().toLowerCase();
105
+ if (arg === '--from') { convertFrom = value.trim(); migrateFrom = value.trim(); }
106
+ if (arg === '--to') { convertTo = value.trim(); migrateTo = value.trim(); }
100
107
  i++;
101
108
  continue;
102
109
  }
@@ -191,7 +198,7 @@ function parseArgs(rawArgs) {
191
198
 
192
199
  const normalizedCommand = COMMAND_ALIASES[command] || command;
193
200
 
194
- return { flags, command, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks, feedbackKey, feedbackStatus, feedbackEffect, feedbackNotes, feedbackSource, feedbackScoreDelta, platform, format, extraArgs };
201
+ return { flags, command, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks, feedbackKey, feedbackStatus, feedbackEffect, feedbackNotes, feedbackSource, feedbackScoreDelta, platform, format, extraArgs, convertFrom, convertTo, migrateFrom, migrateTo };
195
202
  }
196
203
 
197
204
  const HELP = `
@@ -230,6 +237,11 @@ const HELP = `
230
237
  npx nerviq badge Generate shields.io badge markdown
231
238
  npx nerviq feedback Record recommendation outcomes or show local outcome summary
232
239
 
240
+ Utilities:
241
+ npx nerviq doctor Self-diagnostics: Node version, deps, freshness gates, platform detection
242
+ npx nerviq convert --from claude --to codex Convert config between platforms
243
+ npx nerviq migrate --platform cursor --from v2 --to v3 Migrate platform config to newer version
244
+
233
245
  Options:
234
246
  --threshold N Exit with code 1 if score is below N (useful for CI)
235
247
  --require A,B Exit with code 1 if named checks fail (e.g. --require secretsProtection,permissionDeny)
@@ -246,6 +258,7 @@ const HELP = `
246
258
  --score-delta N Optional observed score delta tied to the outcome
247
259
  --platform NAME Choose platform surface (claude default, codex advisory/build preview)
248
260
  --format NAME Output format for audit results (json, sarif)
261
+ --feedback After audit output, prompt "Was this helpful? (y/n)" for each displayed top action and save answers locally
249
262
  --snapshot Save a normalized snapshot artifact under .claude/nerviq/snapshots/
250
263
  --lite Show a short top-3 quick scan with one clear next command
251
264
  --dry-run Preview apply without writing files
@@ -316,6 +329,7 @@ async function main() {
316
329
  auto: flags.includes('--auto'),
317
330
  lite: flags.includes('--lite'),
318
331
  snapshot: flags.includes('--snapshot'),
332
+ feedback: flags.includes('--feedback'),
319
333
  dryRun: flags.includes('--dry-run'),
320
334
  threshold: parsed.threshold !== null ? Number(parsed.threshold) : null,
321
335
  out: parsed.out,
@@ -699,6 +713,34 @@ async function main() {
699
713
  } else if (normalizedCommand === 'watch') {
700
714
  const { watch } = require('../src/watch');
701
715
  await watch(options);
716
+ } else if (normalizedCommand === 'doctor') {
717
+ const { runDoctor } = require('../src/doctor');
718
+ const output = await runDoctor({ dir: options.dir, json: options.json, verbose: options.verbose });
719
+ console.log(output);
720
+ process.exit(0);
721
+ } else if (normalizedCommand === 'convert') {
722
+ const { runConvert } = require('../src/convert');
723
+ const output = await runConvert({
724
+ dir: options.dir,
725
+ from: parsed.convertFrom,
726
+ to: parsed.convertTo,
727
+ dryRun: options.dryRun,
728
+ json: options.json,
729
+ });
730
+ console.log(output);
731
+ process.exit(0);
732
+ } else if (normalizedCommand === 'migrate') {
733
+ const { runMigrate } = require('../src/migrate');
734
+ const output = await runMigrate({
735
+ dir: options.dir,
736
+ platform: options.platform || parsed.platform || 'claude',
737
+ from: parsed.migrateFrom,
738
+ to: parsed.migrateTo,
739
+ dryRun: options.dryRun,
740
+ json: options.json,
741
+ });
742
+ console.log(output);
743
+ process.exit(0);
702
744
  } else if (normalizedCommand === 'setup') {
703
745
  await setup(options);
704
746
  if (options.snapshot) {
@@ -712,6 +754,25 @@ async function main() {
712
754
  }
713
755
  } else {
714
756
  const result = await audit(options);
757
+ if (options.feedback && !options.json && options.format === null) {
758
+ const feedbackTargets = options.lite
759
+ ? (result.liteSummary?.topNextActions || [])
760
+ : (result.topNextActions || []);
761
+ const feedbackResult = await collectFeedback(options.dir, {
762
+ findings: feedbackTargets,
763
+ platform: result.platform,
764
+ sourceCommand: normalizedCommand,
765
+ score: result.score,
766
+ });
767
+ if (feedbackResult.mode === 'skipped-noninteractive') {
768
+ console.log(' Feedback prompt skipped: interactive terminal required.');
769
+ console.log('');
770
+ } else if (feedbackResult.saved > 0) {
771
+ console.log(` Feedback saved: ${feedbackResult.relativeDir}`);
772
+ console.log(` Helpful: ${feedbackResult.helpful} | Not helpful: ${feedbackResult.unhelpful}`);
773
+ console.log('');
774
+ }
775
+ }
715
776
  const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'audit', result, {
716
777
  sourceCommand: normalizedCommand,
717
778
  }) : null;
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@nerviq/cli",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
4
4
  "description": "The intelligent nervous system for AI coding agents — audit, align, and amplify every platform on every project.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
- "nerviq": "bin/cli.js"
7
+ "nerviq": "bin/cli.js",
8
+ "nerviq-mcp": "src/mcp-server.js"
8
9
  },
9
10
  "files": [
10
11
  "bin",
@@ -1,22 +1,26 @@
1
1
  /**
2
- * Aider Technique Database — 68 checks (AD-A01 through AD-P03)
2
+ * Aider Technique Database — 71 checks (AD-A01 through AD-P06)
3
3
  *
4
4
  * Aider is fundamentally different from IDE platforms:
5
5
  * - Git-first CLI tool: git is the ONLY safety mechanism
6
6
  * - No hooks, no MCP, no skills, no agents
7
7
  * - Config: .aider.conf.yml (YAML), .aider.model.settings.yml, .env
8
8
  * - 3 model roles: main (coding), editor (applying), weak (commit messages)
9
- * - Architect mode (2-model workflow)
10
- * - Convention files must be explicitly passed (no auto-discovery)
9
+ * - Architect mode (2-model workflow, ~1.73x cost vs standard)
10
+ * - Convention files must be EXPLICITLY passed AND referenced in prompts (no auto-discovery)
11
11
  * - 4-level config precedence: env vars > CLI args > .aider.conf.yml > defaults
12
+ * - Key gotcha: default auto-commit bypasses pre-commit hooks (use --git-commit-verify)
13
+ * - Key gotcha: exit code 0 returned even on auth failure in headless mode
14
+ * - Key gotcha: Playwright auto-scrapes URLs in messages (unexpected side effect)
12
15
  *
13
- * Categories: Config(8), Git Safety(8), Model Config(8), Conventions(6),
14
- * Architecture(4), Security(6), CI(4), Quality(6) + M/N/O/P expansion (18)
16
+ * Categories: Config(8), Git Safety(10), Model Config(8), Conventions(6),
17
+ * Architecture(4), Security(6), CI(4), Quality(6) + M/N/O/P expansion (19)
15
18
  *
16
19
  * Check ID prefix: AD-
17
20
  */
18
21
 
19
22
  const { containsEmbeddedSecret } = require('../secret-patterns');
23
+ const { attachSourceUrls } = require('../source-urls');
20
24
 
21
25
  const FILLER_PATTERNS = [
22
26
  /\bbe helpful\b/i,
@@ -421,7 +425,7 @@ const AIDER_TECHNIQUES = {
421
425
  impact: 'high',
422
426
  rating: 4,
423
427
  category: 'model-config',
424
- fix: 'Set `architect: true` to use a 2-model workflow (architect plans, editor applies).',
428
+ fix: 'Set `architect: true` to use a 2-model workflow (architect plans, editor applies). NOTE: architect mode costs ~1.73x standard mode per edit ($0.00026 vs $0.00015 measured in live experiment). auto_accept_architect is on by default — no confirmation between steps.',
425
429
  template: 'aider-conf-yml',
426
430
  file: () => '.aider.conf.yml',
427
431
  line: (ctx) => firstLineMatching(configContent(ctx), /architect\s*:/i),
@@ -540,7 +544,7 @@ const AIDER_TECHNIQUES = {
540
544
  impact: 'high',
541
545
  rating: 4,
542
546
  category: 'conventions',
543
- fix: 'Add `read: [CONVENTIONS.md]` to .aider.conf.yml — Aider has NO auto-discovery.',
547
+ fix: 'Add `read: [CONVENTIONS.md]` to .aider.conf.yml — Aider has NO auto-discovery. Additionally, convention files are only followed when EXPLICITLY referenced in the prompt itself (confirmed by live experiment with gpt-4o-mini). Just loading them via --read is not enough; your prompts must say "follow the conventions in CONVENTIONS.md".',
544
548
  template: 'aider-conf-yml',
545
549
  file: () => '.aider.conf.yml',
546
550
  line: (ctx) => firstLineMatching(configContent(ctx), /read\s*:/i),
@@ -867,15 +871,64 @@ const AIDER_TECHNIQUES = {
867
871
  Boolean(ctx.fileContent('.husky/pre-commit')) ||
868
872
  Boolean(ctx.fileContent('.lefthook.yml'));
869
873
  },
870
- impact: 'medium',
871
- rating: 3,
874
+ impact: 'high',
875
+ rating: 4,
872
876
  category: 'ci',
873
- fix: 'Add pre-commit hooks (pre-commit, husky, lefthook) to validate Aider changes.',
877
+ fix: 'Add pre-commit hooks (pre-commit, husky, lefthook). IMPORTANT: Aider default auto-commit BYPASSES pre-commit hooks (confirmed by live experiment). If hooks are critical, pass --git-commit-verify to Aider to restore hook enforcement.',
874
878
  template: null,
875
879
  file: () => null,
876
880
  line: () => null,
877
881
  },
878
882
 
883
+ aiderGitCommitVerify: {
884
+ id: 'AD-G05',
885
+ name: '--git-commit-verify recommended when pre-commit hooks exist',
886
+ check: (ctx) => {
887
+ // Only relevant if pre-commit hooks exist
888
+ const hasHooks = Boolean(ctx.fileContent('.pre-commit-config.yaml')) ||
889
+ Boolean(ctx.fileContent('.husky/pre-commit')) ||
890
+ Boolean(ctx.fileContent('.lefthook.yml'));
891
+ if (!hasHooks) return null;
892
+ const config = configContent(ctx);
893
+ if (!config) return false;
894
+ // Check if git-commit-verify is set in config or documented in conventions
895
+ return /git-commit-verify/i.test(config) ||
896
+ /git-commit-verify/i.test(conventionContent(ctx));
897
+ },
898
+ impact: 'high',
899
+ rating: 4,
900
+ category: 'ci',
901
+ fix: 'When pre-commit hooks exist, add --git-commit-verify to Aider invocations. Default Aider auto-commits SKIP pre-commit hooks entirely (experimentally confirmed). Without this flag, hooks that enforce security or quality checks are silently bypassed.',
902
+ template: 'aider-conf-yml',
903
+ file: () => '.aider.conf.yml',
904
+ line: (ctx) => firstLineMatching(configContent(ctx), /git-commit-verify/i),
905
+ },
906
+
907
+ aiderCiExitCodeUnreliable: {
908
+ id: 'AD-G06',
909
+ name: 'CI scripts handle exit code 0 on auth failure (unreliable exit code)',
910
+ check: (ctx) => {
911
+ const workflows = ctx.workflowFiles ? ctx.workflowFiles() : [];
912
+ if (workflows.length === 0) return null;
913
+ // Check if any workflow mentions aider and has output checking
914
+ for (const wf of workflows) {
915
+ const content = ctx.fileContent(wf) || '';
916
+ if (/aider/i.test(content)) {
917
+ // Good if it checks output, not just exit code
918
+ return /grep|check.*output|--json|error.*detect/i.test(content);
919
+ }
920
+ }
921
+ return null;
922
+ },
923
+ impact: 'high',
924
+ rating: 4,
925
+ category: 'ci',
926
+ fix: 'Aider returns exit code 0 even on auth failure (experimentally confirmed). CI scripts that use Aider MUST NOT rely solely on exit codes to detect failure. Check Aider output for error strings or use output parsing to detect real failures.',
927
+ template: null,
928
+ file: () => '.github/workflows/',
929
+ line: () => null,
930
+ },
931
+
879
932
  // =========================================================================
880
933
  // H — Quality (6 checks: AD-H01 .. AD-H06)
881
934
  // =========================================================================
@@ -1128,6 +1181,25 @@ const AIDER_TECHNIQUES = {
1128
1181
  line: (ctx) => firstLineMatching(configContent(ctx), /voice-language\s*:/i),
1129
1182
  },
1130
1183
 
1184
+ aiderPlaywrightUrlScraping: {
1185
+ id: 'AD-N05',
1186
+ name: 'Playwright URL auto-scraping side effect is expected',
1187
+ check: (ctx) => {
1188
+ const conventions = conventionContent(ctx);
1189
+ const config = configContent(ctx);
1190
+ // Check if team is aware of the Playwright auto-scraping behavior
1191
+ return /playwright|url.*scrap|scrape.*url|auto.*fetch|web.*fetch/i.test(conventions) ||
1192
+ /playwright|url.*scrap/i.test(config);
1193
+ },
1194
+ impact: 'medium',
1195
+ rating: 3,
1196
+ category: 'workflow-patterns',
1197
+ fix: 'Aider automatically scrapes URLs found in messages using Playwright (experimentally confirmed side effect). This causes unexpected network requests and delays. Document this in conventions, and avoid putting real URLs in messages unless scraping is intentional.',
1198
+ template: 'aider-conventions',
1199
+ file: () => 'CONVENTIONS.md',
1200
+ line: () => null,
1201
+ },
1202
+
1131
1203
  // =========================================================================
1132
1204
  // O — Editor Integration (4 checks: AD-O01 .. AD-O04)
1133
1205
  // =========================================================================
@@ -1294,7 +1366,7 @@ const AIDER_TECHNIQUES = {
1294
1366
  impact: 'medium',
1295
1367
  rating: 3,
1296
1368
  category: 'release-readiness',
1297
- fix: 'Enable prompt caching or configure separate weak/editor models for cost optimization.',
1369
+ fix: 'Enable prompt caching or configure separate weak/editor models for cost optimization. Cost reference (measured): standard edit ~$0.00015, architect mode edit ~$0.00026 (~1.73x). Set cache-prompts: true for repeated context, and weak-model for commit messages to reduce costs.',
1298
1370
  template: 'aider-conf-yml',
1299
1371
  file: () => '.aider.conf.yml',
1300
1372
  line: (ctx) => firstLineMatching(configContent(ctx), /cache-prompts\s*:|weak-model\s*:|editor-model\s*:/i),
@@ -1318,6 +1390,8 @@ const AIDER_TECHNIQUES = {
1318
1390
  },
1319
1391
  };
1320
1392
 
1393
+ attachSourceUrls('aider', AIDER_TECHNIQUES);
1394
+
1321
1395
  module.exports = {
1322
1396
  AIDER_TECHNIQUES,
1323
1397
  };
package/src/audit.js CHANGED
@@ -461,7 +461,7 @@ function buildTopNextActions(failed, limit = 5, outcomeSummaryByKey = {}, option
461
461
  return scoreB - scoreA;
462
462
  })
463
463
  .slice(0, limit)
464
- .map(({ key, id, name, impact, fix, category }) => {
464
+ .map(({ key, id, name, impact, fix, category, sourceUrl }) => {
465
465
  const feedback = outcomeSummaryByKey[key] || null;
466
466
  const rankingAdjustment = getRecommendationAdjustment(outcomeSummaryByKey, key);
467
467
  const signals = [
@@ -491,6 +491,7 @@ function buildTopNextActions(failed, limit = 5, outcomeSummaryByKey = {}, option
491
491
  name,
492
492
  impact,
493
493
  category,
494
+ sourceUrl,
494
495
  module: CATEGORY_MODULES[category] || category,
495
496
  fix,
496
497
  priorityScore,
@@ -810,7 +811,7 @@ async function audit(options) {
810
811
  stacks,
811
812
  results,
812
813
  categoryScores,
813
- quickWins: quickWins.map(({ key, name, impact, fix, category }) => ({ key, name, impact, category, fix })),
814
+ quickWins: quickWins.map(({ key, name, impact, fix, category, sourceUrl }) => ({ key, name, impact, category, fix, sourceUrl })),
814
815
  topNextActions,
815
816
  recommendationOutcomes: {
816
817
  totalEntries: outcomeSummary.totalEntries,
@@ -1,6 +1,7 @@
1
1
  const os = require('os');
2
2
  const path = require('path');
3
3
  const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
4
+ const { attachSourceUrls } = require('../source-urls');
4
5
 
5
6
  const DEFAULT_PROJECT_DOC_MAX_BYTES = 32768;
6
7
  const SUPPORTED_HOOK_EVENTS = new Set(['SessionStart', 'PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'Stop']);
@@ -3249,6 +3250,8 @@ const CODEX_TECHNIQUES = {
3249
3250
  },
3250
3251
  };
3251
3252
 
3253
+ attachSourceUrls('codex', CODEX_TECHNIQUES);
3254
+
3252
3255
  module.exports = {
3253
3256
  CODEX_TECHNIQUES,
3254
3257
  };