@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
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Reference: self-governing AI coding agent loop using @nerviq/cli/sdk.
4
+ *
5
+ * This file is the AI-07 reference example for the docs/for-agents page on
6
+ * nerviq.net. It implements the "Self-governing agent pattern" documented
7
+ * there: an agent that audits the repo before acting, runs targeted fixes,
8
+ * makes the actual code change, re-audits to detect regression, and records
9
+ * outcomes back into the local learning loop.
10
+ *
11
+ * Usage (after `npm install @nerviq/cli`):
12
+ * node node_modules/@nerviq/cli/sdk/examples/self-governing-agent.js [repo-dir]
13
+ *
14
+ * Default repo-dir is process.cwd().
15
+ *
16
+ * NOT autonomous in the dangerous sense:
17
+ * - This loop NEVER calls --apply --auto silently. Mutations of governance
18
+ * posture (deny rules, MCP permissions, hooks) require explicit user
19
+ * consent. The example surfaces a plan and waits for the next human
20
+ * decision; that's by design and matches the docs/for-agents constraint.
21
+ */
22
+
23
+ 'use strict';
24
+
25
+ // Resolve the SDK from npm install OR from the in-repo path (for running this
26
+ // example directly from a checkout of nerviq/nerviq).
27
+ function loadSdk() {
28
+ try {
29
+ return require('@nerviq/cli/sdk');
30
+ } catch {
31
+ return require('../index.js');
32
+ }
33
+ }
34
+ const { audit, harmonyAudit, detectPlatforms } = loadSdk();
35
+ const path = require('path');
36
+
37
+ async function selfGoverningLoop(repoDir, opts = {}) {
38
+ const dir = path.resolve(repoDir);
39
+ const log = opts.log || console.log;
40
+ const HARMONY_DRIFT_THRESHOLD = 60;
41
+
42
+ // ── Step 1. Pre-task audit ──────────────────────────────────────────────
43
+ log('[1/5] pre-task audit…');
44
+ const platforms = detectPlatforms(dir);
45
+ log(` platforms: ${platforms.join(', ') || '(none detected — single-platform repo)'}`);
46
+
47
+ const pre = await audit(dir, opts.platform || (platforms[0] || 'claude'));
48
+ log(` score: ${pre.score}/100 organic: ${pre.organicScore}/100 failed: ${pre.failed}`);
49
+
50
+ // The headline value: stale references. Surface BEFORE doing anything.
51
+ if (pre.staleReferences && pre.staleReferences.count > 0) {
52
+ log(` 📌 ${pre.staleReferences.headline}`);
53
+ for (const sample of pre.staleReferences.topSample) {
54
+ log(` · ${sample.file}:${sample.line} — ${sample.fix.split('\n')[0].slice(0, 100)}`);
55
+ }
56
+ log(' ↳ recommend: surface to user, ask whether to proceed despite stale refs');
57
+ }
58
+
59
+ // ── Step 2. Cross-platform Harmony check (only if multi-platform) ──────
60
+ if (platforms.length >= 2) {
61
+ log('[2/5] harmony check (cross-platform drift)…');
62
+ const harmony = await harmonyAudit(dir);
63
+ log(` harmonyScore: ${harmony.harmonyScore}/100`);
64
+
65
+ if (harmony.harmonyScore < HARMONY_DRIFT_THRESHOLD) {
66
+ log(` ⚠️ harmony below ${HARMONY_DRIFT_THRESHOLD} — drift between platforms is forming`);
67
+ log(' ↳ recommend: surface drifts to user, decide whether to harmony-sync before proceeding');
68
+ // NOTE: a real agent would surface harmony.drift list here and wait for
69
+ // user approval before running `nerviq harmony-sync --fix`. We do NOT
70
+ // call it silently from the agent loop.
71
+ }
72
+ } else {
73
+ log('[2/5] harmony check skipped (single-platform repo)');
74
+ }
75
+
76
+ // ── Step 3. Make the actual code change ─────────────────────────────────
77
+ log('[3/5] doing the actual task… (placeholder — replace with the real agent action)');
78
+ // In a real agent: execute the user's task. Edit files, run tests, etc.
79
+ // For this example, simulate a small change so step 4's diff-only audit has
80
+ // something to compare against.
81
+ if (typeof opts.doWork === 'function') {
82
+ await opts.doWork({ dir, preAuditScore: pre.score });
83
+ } else {
84
+ log(' (no doWork callback provided — skipping)');
85
+ }
86
+
87
+ // ── Step 4. Post-task diff-only re-audit ────────────────────────────────
88
+ log('[4/5] post-task diff-only audit…');
89
+ const post = await audit(dir, opts.platform || (platforms[0] || 'claude'));
90
+ const delta = post.score - pre.score;
91
+ const arrow = delta > 0 ? `+${delta}` : delta < 0 ? `${delta}` : '0';
92
+ log(` score: ${post.score}/100 Δ ${arrow} failed: ${post.failed}`);
93
+
94
+ if (delta < -3) {
95
+ log(' 🔴 score dropped materially — recommend: rollback or human review');
96
+ } else if (delta > 0) {
97
+ log(' ✓ score improved');
98
+ }
99
+
100
+ // ── Step 5. Record outcome (learning loop) ─────────────────────────────
101
+ log('[5/5] outcome recording — call `nerviq feedback --key <K> --status accepted|rejected|deferred --score-delta <delta>`');
102
+ log(' (the agent should invoke this once per recommendation it acted on, so suggest-rules can learn the team\'s actual preferences)');
103
+
104
+ return {
105
+ pre,
106
+ post,
107
+ delta,
108
+ platforms,
109
+ recommendation:
110
+ delta < -3
111
+ ? 'rollback-or-review'
112
+ : pre.staleReferences && pre.staleReferences.count > 0
113
+ ? 'surface-stale-references-to-user'
114
+ : delta > 0
115
+ ? 'continue'
116
+ : 'no-change',
117
+ };
118
+ }
119
+
120
+ if (require.main === module) {
121
+ const dir = process.argv[2] || process.cwd();
122
+ selfGoverningLoop(dir, { log: console.log })
123
+ .then((result) => {
124
+ console.log('\n=== Loop complete ===');
125
+ console.log(`Recommendation: ${result.recommendation}`);
126
+ console.log(`Pre/post score: ${result.pre.score} → ${result.post.score} (Δ ${result.delta})`);
127
+ process.exitCode = result.recommendation === 'rollback-or-review' ? 1 : 0;
128
+ })
129
+ .catch((err) => {
130
+ console.error(`error: ${err.message}`);
131
+ process.exitCode = 1;
132
+ });
133
+ }
134
+
135
+ module.exports = { selfGoverningLoop };
package/sdk/index.d.ts ADDED
@@ -0,0 +1,115 @@
1
+ export type NerviqPlatform =
2
+ | 'claude'
3
+ | 'codex'
4
+ | 'gemini'
5
+ | 'copilot'
6
+ | 'cursor'
7
+ | 'windsurf'
8
+ | 'aider'
9
+ | 'opencode';
10
+
11
+ export interface AuditFinding {
12
+ key: string;
13
+ id?: string | null;
14
+ name: string;
15
+ category?: string | null;
16
+ impact?: 'critical' | 'high' | 'medium' | 'low' | null;
17
+ fix?: string | null;
18
+ passed: boolean | null;
19
+ file?: string | null;
20
+ line?: number | null;
21
+ sourceUrl?: string | null;
22
+ confidence?: number | string | null;
23
+ }
24
+
25
+ export interface AuditAction {
26
+ key: string;
27
+ id?: string | null;
28
+ name: string;
29
+ impact?: 'critical' | 'high' | 'medium' | 'low' | null;
30
+ category?: string | null;
31
+ fix?: string | null;
32
+ why?: string | null;
33
+ sourceUrl?: string | null;
34
+ }
35
+
36
+ export interface AuditResult {
37
+ platform: string;
38
+ platformLabel: string;
39
+ score: number;
40
+ passed: number;
41
+ failed: number;
42
+ skipped: number;
43
+ checkCount: number;
44
+ results: AuditFinding[];
45
+ quickWins: AuditAction[];
46
+ topNextActions: AuditAction[];
47
+ suggestedNextCommand?: string;
48
+ /** Convenience alias for `passed` */
49
+ passing: number;
50
+ /** Convenience alias for `passed + failed` */
51
+ total: number;
52
+ }
53
+
54
+ export interface HarmonyResult {
55
+ harmonyScore: number;
56
+ platformScores: Record<string, number | null>;
57
+ platformResults: Record<string, AuditResult | null>;
58
+ drift: {
59
+ drifts: Array<Record<string, unknown>>;
60
+ harmonyScore: number;
61
+ };
62
+ recommendations: Array<Record<string, unknown>>;
63
+ activePlatforms: Array<Record<string, unknown>>;
64
+ model: Record<string, unknown>;
65
+ /** Convenience alias for `harmonyScore` */
66
+ average: number;
67
+ }
68
+
69
+ export interface Check {
70
+ platform: string;
71
+ id: string | null;
72
+ key: string;
73
+ name: string | null;
74
+ category: string | null;
75
+ impact: string | null;
76
+ rating: string | null;
77
+ fix: string | null;
78
+ sourceUrl: string | null;
79
+ confidence: number | null;
80
+ lastVerified?: string | null;
81
+ template?: string | null;
82
+ deprecated?: boolean;
83
+ }
84
+
85
+ export interface RoutingChoice {
86
+ platform: string;
87
+ confidence: number;
88
+ reasoning: string;
89
+ }
90
+
91
+ export interface RoutingResult {
92
+ recommended: RoutingChoice | null;
93
+ alternatives: RoutingChoice[];
94
+ taskType: string;
95
+ }
96
+
97
+ export interface SynergyResult {
98
+ dir: string;
99
+ activePlatforms: string[];
100
+ platformAudits: Record<string, AuditResult>;
101
+ compound: Record<string, unknown>;
102
+ amplification: number;
103
+ compensation: Record<string, unknown>;
104
+ patterns: Array<Record<string, unknown>>;
105
+ recommendations: Array<Record<string, unknown>>;
106
+ errors: Array<{ platform: string; message: string }>;
107
+ report: string;
108
+ }
109
+
110
+ export declare function audit(dir: string, platform?: NerviqPlatform): Promise<AuditResult>;
111
+ export declare function harmonyAudit(dir: string): Promise<HarmonyResult>;
112
+ export declare function synergyReport(dir: string): Promise<SynergyResult>;
113
+ export declare function detectPlatforms(dir: string): string[];
114
+ export declare function getCatalog(): Check[];
115
+ export declare function routeTask(description: string, platforms?: string[]): RoutingResult;
package/sdk/index.js ADDED
@@ -0,0 +1,94 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ const VALID_PLATFORMS = ['claude', 'codex', 'cursor', 'copilot', 'gemini', 'windsurf', 'aider', 'opencode'];
5
+
6
+ function loadCore() {
7
+ try {
8
+ return require('@nerviq/cli');
9
+ } catch {
10
+ return require('..');
11
+ }
12
+ }
13
+
14
+ function validateDir(dir) {
15
+ if (!dir || typeof dir !== 'string') {
16
+ throw new Error('dir is required and must be a string. Pass a valid directory path.');
17
+ }
18
+ const resolved = path.resolve(dir);
19
+ if (!fs.existsSync(resolved)) {
20
+ throw new Error(`Directory not found: ${resolved}. Pass an existing directory path.`);
21
+ }
22
+ return resolved;
23
+ }
24
+
25
+ function validatePlatform(platform) {
26
+ if (platform && !VALID_PLATFORMS.includes(platform)) {
27
+ throw new Error(`Unsupported platform '${platform}'. Use one of: ${VALID_PLATFORMS.join(', ')}`);
28
+ }
29
+ }
30
+
31
+ async function audit(dir, platform = 'claude') {
32
+ const resolved = validateDir(dir);
33
+ validatePlatform(platform);
34
+ const core = loadCore();
35
+ const result = await core.audit({
36
+ dir: resolved,
37
+ platform,
38
+ silent: true,
39
+ });
40
+ // Add convenience aliases for SDK consumers
41
+ if (result) {
42
+ result.passing = result.passed;
43
+ result.total = (result.passed || 0) + (result.failed || 0);
44
+ }
45
+ return result;
46
+ }
47
+
48
+ async function harmonyAudit(dir) {
49
+ const resolved = validateDir(dir);
50
+ const core = loadCore();
51
+ const result = await core.harmonyAudit({
52
+ dir: resolved,
53
+ silent: true,
54
+ });
55
+ // Add convenience alias for SDK consumers
56
+ if (result) {
57
+ result.average = result.harmonyScore;
58
+ }
59
+ return result;
60
+ }
61
+
62
+ async function synergyReport(dir) {
63
+ const resolved = validateDir(dir);
64
+ const core = loadCore();
65
+ return core.synergyReport(resolved);
66
+ }
67
+
68
+ function detectPlatforms(dir) {
69
+ const resolved = validateDir(dir);
70
+ const core = loadCore();
71
+ return core.detectPlatforms(resolved);
72
+ }
73
+
74
+ function getCatalog() {
75
+ const core = loadCore();
76
+ return core.getCatalog();
77
+ }
78
+
79
+ function routeTask(description, platforms) {
80
+ if (!description || typeof description !== 'string') {
81
+ throw new Error('description is required and must be a non-empty string.');
82
+ }
83
+ const core = loadCore();
84
+ return core.routeTask(description, platforms || []);
85
+ }
86
+
87
+ module.exports = {
88
+ audit,
89
+ harmonyAudit,
90
+ synergyReport,
91
+ detectPlatforms,
92
+ getCatalog,
93
+ routeTask,
94
+ };
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "@nerviq/sdk",
3
+ "version": "0.9.5",
4
+ "main": "index.js",
5
+ "types": "index.d.ts",
6
+ "description": "Programmatic SDK for Nerviq — audit, harmony, synergy for AI coding agents",
7
+ "license": "AGPL-3.0",
8
+ "dependencies": {
9
+ "@nerviq/cli": "^0.9.5"
10
+ }
11
+ }
package/src/activity.js CHANGED
@@ -683,6 +683,19 @@ function recordRecommendationOutcome(dir, payload) {
683
683
  relativePath: path.relative(dir, filePath),
684
684
  });
685
685
 
686
+ // BUG-07 fix: also bump the usage-patterns counter so suggest-rules
687
+ // sees the recorded outcome. Previously feedback wrote to outcomes/
688
+ // and suggest-rules read from feedback/patterns.json — two stores
689
+ // that never cross-fed. The map: accepted→accepted, rejected→rejected,
690
+ // deferred→skipped (matching usage-patterns vocabulary).
691
+ try {
692
+ const { recordPattern } = require('./usage-patterns');
693
+ const patternAction = status === 'deferred' ? 'skipped' : status;
694
+ recordPattern(dir, key, patternAction);
695
+ } catch {
696
+ // Non-fatal — outcomes file already written; pattern bump is additive.
697
+ }
698
+
686
699
  return {
687
700
  id,
688
701
  filePath,
package/src/audit.js CHANGED
@@ -63,8 +63,14 @@ const COLORS = {
63
63
  magenta: '\x1b[35m',
64
64
  };
65
65
 
66
+ // MEMO-16: route every CLI string through safe-glyph before colorize so
67
+ // Windows consoles without UTF-8 codepage 65001 get ASCII-safe fallbacks
68
+ // (`[OK]`, `[X]`, `[!]`, etc.) instead of mojibake. No-op on UTF-8 capable
69
+ // terminals (modern macOS / Linux / Windows Terminal / VS Code / WSL).
70
+ const { safeText } = require('./safe-glyph');
71
+
66
72
  function colorize(text, color) {
67
- return `${COLORS[color] || ''}${text}${COLORS.reset}`;
73
+ return `${COLORS[color] || ''}${safeText(text)}${COLORS.reset}`;
68
74
  }
69
75
 
70
76
  function progressBar(score, max = 100, width = 20) {
@@ -371,6 +377,27 @@ function printLiteAudit(result, dir) {
371
377
  console.log('');
372
378
  }
373
379
 
380
+ // PROD-03: stale-reference headline. When the new BUG-04 patterns fire,
381
+ // surface them BEFORE "Top 3 things to fix" because:
382
+ // 1. They are deterministic (file X says Y, package.json says Z) and
383
+ // a buyer can verify them in 30 seconds.
384
+ // 2. cursor-doctor + AgentLinter market signals show this is the
385
+ // highest-leverage user-visible value in this category.
386
+ // 3. The user-lab found these get buried among 70+ failed checks.
387
+ if (result.staleReferences && result.staleReferences.count > 0) {
388
+ console.log(colorize(` 📌 Stale references in agent docs: ${result.staleReferences.count}`, 'yellow'));
389
+ for (const sample of result.staleReferences.topSample) {
390
+ const labelTag = sample.key.replace('agent-config-', '').replace(/-/g, ' ');
391
+ console.log(colorize(` [${labelTag}] ${sample.file || '(unknown)'}:${sample.line || '?'}`, 'dim'));
392
+ console.log(colorize(` → ${sample.fix}`, 'dim'));
393
+ }
394
+ if (result.staleReferences.count > result.staleReferences.topSample.length) {
395
+ const remaining = result.staleReferences.count - result.staleReferences.topSample.length;
396
+ console.log(colorize(` ... and ${remaining} more (run with --shallow-risk to see all)`, 'dim'));
397
+ }
398
+ console.log('');
399
+ }
400
+
374
401
  console.log(colorize(' Top 3 things to fix right now:', 'magenta'));
375
402
  console.log('');
376
403
  let usagePatterns;
@@ -451,24 +478,43 @@ async function audit(options) {
451
478
  ]);
452
479
  const includeGenericQuality = options.verbose;
453
480
 
454
- // Run all technique checks
481
+ // Run all technique checks.
482
+ //
483
+ // AI-12a optimization (2026-04-29): partition techniques into applicable
484
+ // vs not-applicable BEFORE the hot loop. Previously the loop iterated all
485
+ // ~2,441 techniques and skipped ~85% via the in-loop guard, which still
486
+ // paid the Object.entries iteration + spread + push cost per skipped
487
+ // technique. After partition the hot loop only runs `check(ctx)` on
488
+ // genuinely applicable techniques; not-applicable ones are batched into
489
+ // a single fast push at the end. Target: ≥40% cut on first-run audit
490
+ // for repos > 200 files. See ai-12-governance-budget-tracking memo for
491
+ // the budget context.
455
492
  if (!shallowRiskOnly) {
456
- for (const [key, technique] of Object.entries(techniques)) {
457
- // Skip entire stack category if the stack is not detected at a core location
458
- // Skip generic quality categories unless --verbose is set
459
- const cat = technique.category;
493
+ const techniqueEntries = Object.entries(techniques);
494
+ const applicable = [];
495
+ const notApplicable = [];
496
+ for (const entry of techniqueEntries) {
497
+ const cat = entry[1].category;
460
498
  if ((!includeGenericQuality && GENERIC_QUALITY_CATEGORIES.has(cat)) ||
461
499
  (STACK_CATEGORY_DETECTORS[cat] && !activeStackCategories.has(cat))) {
462
- results.push({
463
- key,
464
- ...technique,
465
- file: null,
466
- line: null,
467
- passed: null, // not applicable
468
- });
469
- continue;
500
+ notApplicable.push(entry);
501
+ } else {
502
+ applicable.push(entry);
470
503
  }
471
-
504
+ }
505
+ // Fast push for not-applicable techniques (no check() call needed).
506
+ for (let i = 0; i < notApplicable.length; i++) {
507
+ const [key, technique] = notApplicable[i];
508
+ results.push({
509
+ key,
510
+ ...technique,
511
+ file: null,
512
+ line: null,
513
+ passed: null,
514
+ });
515
+ }
516
+ // Hot loop: only applicable techniques.
517
+ for (const [key, technique] of applicable) {
472
518
  const passed = technique.check(ctx);
473
519
  let file = typeof technique.file === 'function' ? (technique.file(ctx) ?? null) : (technique.file ?? null);
474
520
  let line = typeof technique.line === 'function' ? (technique.line(ctx) ?? null) : (technique.line ?? null);
@@ -707,6 +753,61 @@ async function audit(options) {
707
753
  if (shallowRiskEnabled) {
708
754
  result.shallowRiskHints = runShallowRisk(ctx);
709
755
  }
756
+ // PROD-03: stale-reference HEADLINE — runs default-on (separately from
757
+ // the full shallow-risk pipeline) because the two new BUG-04 patterns
758
+ // (`agent-config-script-not-in-package-json` and
759
+ // `agent-config-framework-version-mismatch`) are deterministic, fast,
760
+ // and have near-zero FP. The user-lab found these are the highest-
761
+ // leverage user-visible value in this category (cursor-doctor /
762
+ // AgentLinter market signal); they shouldn't be gated behind a flag.
763
+ // Reuse the broader hint list when shallow-risk is already enabled.
764
+ if (shallowRiskOnly !== true) {
765
+ const STALE_REFERENCE_KEYS = new Set([
766
+ 'agent-config-script-not-in-package-json',
767
+ 'agent-config-framework-version-mismatch',
768
+ ]);
769
+ let staleHints;
770
+ if (shallowRiskEnabled && Array.isArray(result.shallowRiskHints)) {
771
+ staleHints = result.shallowRiskHints.filter((h) => STALE_REFERENCE_KEYS.has(h.key));
772
+ } else {
773
+ // Default-on mini-scan: run only the 2 stale-reference patterns.
774
+ try {
775
+ const minimalPatterns = [
776
+ require('./shallow-risk/patterns/agent-config-script-not-in-package-json'),
777
+ require('./shallow-risk/patterns/agent-config-framework-version-mismatch'),
778
+ ];
779
+ const { buildFinding } = require('./shallow-risk/shared');
780
+ staleHints = [];
781
+ for (const p of minimalPatterns) {
782
+ let raw = [];
783
+ try { raw = p.run(ctx) || []; } catch { raw = []; }
784
+ for (const f of raw) {
785
+ staleHints.push(buildFinding(p, ctx, f));
786
+ }
787
+ }
788
+ } catch {
789
+ staleHints = [];
790
+ }
791
+ }
792
+ if (staleHints && staleHints.length > 0) {
793
+ result.staleReferences = {
794
+ count: staleHints.length,
795
+ byKey: staleHints.reduce((acc, h) => {
796
+ acc[h.key] = (acc[h.key] || 0) + 1;
797
+ return acc;
798
+ }, {}),
799
+ topSample: staleHints.slice(0, 3).map((h) => ({
800
+ key: h.key,
801
+ file: h.file,
802
+ line: h.line,
803
+ fix: h.fix,
804
+ })),
805
+ headline: staleHints.length === 1
806
+ ? '1 stale reference found in agent docs.'
807
+ : `${staleHints.length} stale references found in agent docs.`,
808
+ };
809
+ }
810
+ }
710
811
  // Detect which AI config files are present
711
812
  const configFiles = [];
712
813
  const configChecks = [
@@ -56,13 +56,20 @@ function analyzeSuggestions(dir) {
56
56
  let bootstrap = { ready: true, state: 'ready', message: null, steps: [] };
57
57
 
58
58
  if (totalEvents === 0 && auditSnapshots.length === 0) {
59
+ // BUG-07 fix: when feedback was just recorded but pattern events are 0,
60
+ // the user already ran `nerviq feedback` and got "No local usage..."
61
+ // back. After the activity.js fix, feedback now bumps usage-patterns,
62
+ // so this state means feedback never ran AT ALL. Be explicit about
63
+ // what's missing so the user doesn't think the loop is broken.
59
64
  bootstrap = {
60
65
  ready: false,
61
66
  state: 'empty',
62
- message: 'No local usage or snapshot history exists yet.',
67
+ message: 'No local usage or snapshot history exists yet. Need at least 2 snapshots and/or 2 recorded outcomes per check before suggestions surface.',
68
+ missingSignals: ['snapshots', 'recorded-outcomes'],
69
+ threshold: { minEventsPerCheck: MIN_EVENTS, minSnapshotsForPriority: 2 },
63
70
  steps: [
64
71
  'Run `nerviq audit --snapshot` to save the baseline.',
65
- 'Use `nerviq fix`, `nerviq fix --all-critical`, or `nerviq feedback` to record recommendation outcomes.',
72
+ 'Use `nerviq fix`, `nerviq fix --all-critical`, or `nerviq feedback --key <K> --status accepted` to record recommendation outcomes.',
66
73
  'Run `nerviq audit --snapshot` again after a meaningful repo change.',
67
74
  'Re-run `nerviq suggest-rules`.',
68
75
  ],
@@ -685,12 +685,29 @@ function analyzeBehavioralDrift(dir, options = {}) {
685
685
  const structuralSignals = buildStructuralSignals(dir, options);
686
686
  const intentSignals = collectIntentSignals(dir);
687
687
  const { findings, labels } = deriveBehavioralFindings(structuralSignals, intentSignals);
688
- const score = buildBehavioralScore(structuralSignals, findings);
688
+
689
+ // BUG-05 fix: a perfect 100 alignment score on a repo with no source files
690
+ // is misleading — the buildBehavioralScore math starts at 100 and subtracts
691
+ // penalties; with zero structural signal there's nothing to subtract, so
692
+ // empty repos look "perfectly aligned." Surface insufficient-signal status
693
+ // explicitly instead of returning the bogus 100. Threshold: <5 source files
694
+ // is treated as insufficient signal (calibrated from the user-lab fixture
695
+ // where 0 source files returned 100).
696
+ const SUFFICIENT_SIGNAL_FLOOR = 5;
697
+ const insufficientSignal = structuralSignals.sourceFiles < SUFFICIENT_SIGNAL_FLOOR;
698
+
699
+ const score = insufficientSignal
700
+ ? null
701
+ : buildBehavioralScore(structuralSignals, findings);
689
702
 
690
703
  return {
691
704
  mode: 'behavioral-drift',
692
705
  scoreType: 'behavioral-alignment-score',
693
706
  score,
707
+ status: insufficientSignal ? 'insufficient-signal' : 'ok',
708
+ insufficientSignalReason: insufficientSignal
709
+ ? `Need ≥${SUFFICIENT_SIGNAL_FLOOR} source files to compute a meaningful alignment score; found ${structuralSignals.sourceFiles}.`
710
+ : null,
694
711
  scope: SCOPE_CONTRACT,
695
712
  repoSummary: {
696
713
  project: path.basename(dir),
@@ -703,7 +720,13 @@ function analyzeBehavioralDrift(dir, options = {}) {
703
720
  intentSignals,
704
721
  driftLabels: labels,
705
722
  findings,
706
- nextSteps: buildBehavioralNextSteps(findings),
723
+ nextSteps: insufficientSignal
724
+ ? [{
725
+ key: 'add-source-code',
726
+ title: `Add at least ${SUFFICIENT_SIGNAL_FLOOR} source files before re-running behavioral review.`,
727
+ severity: 'low',
728
+ }]
729
+ : buildBehavioralNextSteps(findings),
707
730
  };
708
731
  }
709
732
 
@@ -713,6 +736,18 @@ function writeBehavioralSnapshot(dir, report, meta = {}) {
713
736
 
714
737
  function formatBehavioralReport(report, options = {}) {
715
738
  const lines = [];
739
+ // BUG-05 fix: handle insufficient-signal status — the score is null and a
740
+ // human-friendly explanation replaces the colored gauge.
741
+ if (report.status === 'insufficient-signal') {
742
+ lines.push('');
743
+ lines.push(c(' nerviq behavioral drift review', 'bold'));
744
+ lines.push(c(' ═══════════════════════════════════════', 'dim'));
745
+ lines.push(c(' Alignment score: insufficient signal', 'yellow'));
746
+ lines.push(c(` ${report.insufficientSignalReason || 'Not enough source files to compute a meaningful score.'}`, 'dim'));
747
+ lines.push(c(' No score returned to avoid a misleading 100 on empty repos.', 'dim'));
748
+ lines.push('');
749
+ return lines.join('\n');
750
+ }
716
751
  const scoreColor = report.score >= 75 ? 'green' : report.score >= 55 ? 'yellow' : 'red';
717
752
 
718
753
  lines.push('');
@@ -75,6 +75,13 @@ const P0_SOURCES = [
75
75
  stalenessThresholdDays: 14,
76
76
  verifiedAt: '2026-04-07',
77
77
  },
78
+ {
79
+ key: 'codex-models-docs',
80
+ label: 'Codex Supported Models',
81
+ url: 'https://developers.openai.com/codex/models',
82
+ stalenessThresholdDays: 14,
83
+ verifiedAt: '2026-04-16',
84
+ },
78
85
  ];
79
86
 
80
87
  /**
@@ -97,6 +97,13 @@ const P0_SOURCES = [
97
97
  stalenessThresholdDays: 30,
98
98
  verifiedAt: '2026-04-07',
99
99
  },
100
+ {
101
+ key: 'copilot-models-docs',
102
+ label: 'Copilot Supported AI Models',
103
+ url: 'https://docs.github.com/en/copilot/reference/ai-models/supported-models',
104
+ stalenessThresholdDays: 14,
105
+ verifiedAt: '2026-04-16',
106
+ },
100
107
  ];
101
108
 
102
109
  /**
package/src/freshness.js CHANGED
@@ -98,6 +98,13 @@ const P0_SOURCES = [
98
98
  stalenessThresholdDays: 14,
99
99
  verifiedAt: '2026-04-07',
100
100
  },
101
+ {
102
+ key: 'anthropic-models-overview',
103
+ label: 'Anthropic Claude Models Overview',
104
+ url: 'https://platform.claude.com/docs/en/docs/about-claude/models',
105
+ stalenessThresholdDays: 14,
106
+ verifiedAt: '2026-04-16',
107
+ },
101
108
  ];
102
109
 
103
110
  /**