@nerviq/cli 1.11.0 → 1.13.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 (62) hide show
  1. package/README.md +216 -124
  2. package/bin/cli.js +620 -183
  3. package/package.json +3 -2
  4. package/src/activity.js +49 -9
  5. package/src/adoption-advisor.js +299 -0
  6. package/src/aider/freshness.js +65 -20
  7. package/src/aider/techniques.js +16 -11
  8. package/src/analyze.js +128 -0
  9. package/src/anti-patterns.js +13 -0
  10. package/src/audit/instruction-files.js +180 -0
  11. package/src/audit/recommendations.js +531 -0
  12. package/src/audit.js +53 -681
  13. package/src/behavioral-drift.js +801 -0
  14. package/src/codex/freshness.js +84 -25
  15. package/src/continuous-ops.js +681 -0
  16. package/src/copilot/freshness.js +57 -20
  17. package/src/cost-tracking.js +61 -0
  18. package/src/cursor/freshness.js +65 -20
  19. package/src/cursor/techniques.js +17 -12
  20. package/src/deep-review.js +83 -0
  21. package/src/diff-only.js +280 -0
  22. package/src/doctor.js +118 -55
  23. package/src/freshness.js +74 -21
  24. package/src/gemini/freshness.js +66 -21
  25. package/src/governance.js +59 -43
  26. package/src/hook-validation.js +342 -0
  27. package/src/index.js +5 -0
  28. package/src/integrations.js +42 -5
  29. package/src/mcp-server.js +95 -59
  30. package/src/mcp-validation.js +337 -0
  31. package/src/opencode/freshness.js +66 -21
  32. package/src/opencode/techniques.js +12 -7
  33. package/src/operating-profile.js +574 -0
  34. package/src/org.js +97 -13
  35. package/src/plans.js +192 -8
  36. package/src/platform-change-manifest.js +86 -0
  37. package/src/policy-layers.js +210 -0
  38. package/src/profiles.js +4 -1
  39. package/src/prompt-injection.js +74 -0
  40. package/src/repo-archetype.js +386 -0
  41. package/src/setup/analysis.js +619 -0
  42. package/src/setup/runtime.js +172 -0
  43. package/src/setup.js +62 -748
  44. package/src/source-urls.js +132 -132
  45. package/src/supplemental-checks.js +13 -12
  46. package/src/techniques/api.js +407 -0
  47. package/src/techniques/automation.js +316 -0
  48. package/src/techniques/compliance.js +257 -0
  49. package/src/techniques/hygiene.js +294 -0
  50. package/src/techniques/instructions.js +243 -0
  51. package/src/techniques/observability.js +226 -0
  52. package/src/techniques/optimization.js +142 -0
  53. package/src/techniques/quality.js +317 -0
  54. package/src/techniques/security.js +237 -0
  55. package/src/techniques/shared.js +443 -0
  56. package/src/techniques/stacks.js +2294 -0
  57. package/src/techniques/tools.js +106 -0
  58. package/src/techniques/workflow.js +413 -0
  59. package/src/techniques.js +78 -5607
  60. package/src/watch.js +18 -0
  61. package/src/windsurf/freshness.js +36 -21
  62. package/src/windsurf/techniques.js +17 -12
package/src/analyze.js CHANGED
@@ -12,6 +12,9 @@ const { detectDomainPacks } = require('./domain-packs');
12
12
  const { detectCodexDomainPacks } = require('./codex/domain-packs');
13
13
  const { recommendMcpPacks } = require('./mcp-packs');
14
14
  const { collectClaudeDenyRules } = require('./permission-rules');
15
+ const { buildRepoArchetypeProfile } = require('./repo-archetype');
16
+ const { buildOperatingProfile } = require('./operating-profile');
17
+ const { buildAdoptionAdvisor } = require('./adoption-advisor');
15
18
 
16
19
  const COLORS = {
17
20
  reset: '\x1b[0m',
@@ -498,6 +501,30 @@ async function analyzeProject(options) {
498
501
  ? detectCodexDomainPacks(ctx, stacks, assets)
499
502
  : detectDomainPacks(ctx, stacks, assets);
500
503
  const recommendedMcpPacks = platform === 'claude' ? recommendMcpPacks(stacks, recommendedDomainPacks, { ctx, assets }) : [];
504
+ const repoArchetype = buildRepoArchetypeProfile({
505
+ ctx,
506
+ platform,
507
+ stacks,
508
+ assets,
509
+ recommendedDomainPacks,
510
+ recommendedMcpPacks,
511
+ maturity,
512
+ });
513
+ const recommendedOperatingProfile = buildOperatingProfile({
514
+ dir: options.dir,
515
+ platform,
516
+ repoArchetype,
517
+ recommendedDomainPacks,
518
+ recommendedMcpPacks,
519
+ });
520
+ const adoptionGuidance = buildAdoptionAdvisor({
521
+ platform,
522
+ repoArchetype,
523
+ recommendedOperatingProfile,
524
+ recommendedDomainPacks,
525
+ recommendedMcpPacks,
526
+ env: options.env || {},
527
+ });
501
528
 
502
529
  const report = {
503
530
  platform,
@@ -511,16 +538,28 @@ async function analyzeProject(options) {
511
538
  stacks: stacks.map(s => s.label),
512
539
  domains: recommendedDomainPacks.map(pack => pack.label),
513
540
  maturity,
541
+ archetype: repoArchetype.label,
542
+ workflow: repoArchetype.primaryWorkflow.label,
543
+ riskLevel: repoArchetype.riskProfile.label,
544
+ operatingProfile: recommendedOperatingProfile.label,
545
+ adoptionPlan: adoptionGuidance.summary.label,
514
546
  score: auditResult.score,
515
547
  organicScore: auditResult.organicScore,
516
548
  checkCount: auditResult.checkCount,
517
549
  },
518
550
  platformScopeNote: auditResult.platformScopeNote || null,
519
551
  platformCaveats: auditResult.platformCaveats || [],
552
+ repoArchetype,
553
+ recommendedOperatingProfile,
554
+ adoptionGuidance,
520
555
  detectedArchitecture: {
521
556
  repoType: stacks.length > 0 ? 'stack-detected repo' : 'generic repo',
522
557
  mainDirectories: mainDirs,
523
558
  stackSignals: stacks.map(s => s.key),
559
+ stackFamily: repoArchetype.stackFamily.label,
560
+ topology: repoArchetype.topology.label,
561
+ workflow: repoArchetype.primaryWorkflow.label,
562
+ riskLevel: repoArchetype.riskProfile.label,
524
563
  },
525
564
  existingPlatformAssets: assets,
526
565
  strengthsPreserved: toStrengths(auditResult.results),
@@ -585,11 +624,14 @@ function printAnalysis(report, options = {}) {
585
624
  } else {
586
625
  console.log(c(` Platform: ${report.platformLabel}`, 'dim'));
587
626
  }
627
+ console.log(c(` Archetype: ${report.repoArchetype.label} | Workflow: ${report.repoArchetype.primaryWorkflow.label} | Risk: ${report.repoArchetype.riskProfile.label}`, 'dim'));
588
628
  console.log(c(` Maturity: ${report.projectSummary.maturity} | Score: ${report.projectSummary.score}/100 | Organic: ${report.projectSummary.organicScore}/100`, 'dim'));
589
629
  console.log('');
590
630
 
591
631
  console.log(c(' Detected Architecture', 'blue'));
632
+ console.log(c(` Stack family: ${report.repoArchetype.stackFamily.label} | Topology: ${report.repoArchetype.topology.label} | Confidence: ${report.repoArchetype.confidence}`, 'dim'));
592
633
  console.log(c(` Main directories: ${report.detectedArchitecture.mainDirectories.join(', ') || 'No strong structure detected yet'}`, 'dim'));
634
+ console.log(c(` Signals: ${report.repoArchetype.signals.join(' | ') || 'No strong archetype signals yet'}`, 'dim'));
593
635
  console.log('');
594
636
 
595
637
  console.log(c(` Existing ${report.existingPlatformAssets.label} Assets`, 'blue'));
@@ -683,6 +725,36 @@ function printAnalysis(report, options = {}) {
683
725
  console.log('');
684
726
  }
685
727
 
728
+ if (report.recommendedOperatingProfile) {
729
+ console.log(c(' Recommended Operating Profile', 'blue'));
730
+ console.log(` ${report.recommendedOperatingProfile.label}`);
731
+ console.log(c(` Permission: ${report.recommendedOperatingProfile.permissionProfile.label} | Governance pack: ${report.recommendedOperatingProfile.governancePack.label}`, 'dim'));
732
+ console.log(c(` CI shape: ${report.recommendedOperatingProfile.ciShape.label} | Platforms: ${(report.recommendedOperatingProfile.platformSupport.recommended || []).join(', ') || report.platformLabel}`, 'dim'));
733
+ console.log(c(` Hooks: ${report.recommendedOperatingProfile.hooks.map((hook) => hook.key).join(', ')}`, 'dim'));
734
+ console.log(c(` Verification: ${report.recommendedOperatingProfile.verification.required.join(', ')}`, 'dim'));
735
+ console.log('');
736
+ }
737
+
738
+ if (report.adoptionGuidance && Array.isArray(report.adoptionGuidance.items) && report.adoptionGuidance.items.length > 0) {
739
+ console.log(c(' Adopt / Defer / Ignore', 'blue'));
740
+ console.log(c(` ${report.adoptionGuidance.summary.label}`, 'dim'));
741
+ const groups = [
742
+ ['adopt', 'Adopt now'],
743
+ ['defer', 'Defer until prerequisites are ready'],
744
+ ['ignore', 'Ignore for this repo shape'],
745
+ ];
746
+ for (const [decision, label] of groups) {
747
+ const items = report.adoptionGuidance.items.filter((item) => item.decision === decision).slice(0, 3);
748
+ if (items.length === 0) continue;
749
+ console.log(` ${label}`);
750
+ for (const item of items) {
751
+ console.log(` - ${item.label}`);
752
+ console.log(c(` ${item.why}`, 'dim'));
753
+ }
754
+ }
755
+ console.log('');
756
+ }
757
+
686
758
  if (report.suggestedRolloutOrder.length > 0) {
687
759
  console.log(c(' Suggested Rollout Order', 'blue'));
688
760
  report.suggestedRolloutOrder.forEach((item, index) => {
@@ -710,6 +782,11 @@ function exportMarkdown(report) {
710
782
  if (report.platform === 'claude') {
711
783
  lines.push(`**Domain Packs:** ${report.projectSummary.domains.join(', ') || 'Baseline General'}`);
712
784
  }
785
+ lines.push(`**Archetype:** ${report.repoArchetype.label}`);
786
+ lines.push(`**Workflow:** ${report.repoArchetype.primaryWorkflow.label}`);
787
+ lines.push(`**Risk posture:** ${report.repoArchetype.riskProfile.label}`);
788
+ lines.push(`**Operating profile:** ${report.recommendedOperatingProfile.label}`);
789
+ lines.push(`**Adoption plan:** ${report.adoptionGuidance.summary.label}`);
713
790
  lines.push(`**Maturity:** ${report.projectSummary.maturity}`);
714
791
  lines.push('');
715
792
 
@@ -730,6 +807,57 @@ function exportMarkdown(report) {
730
807
  }
731
808
  lines.push('');
732
809
 
810
+ lines.push('## Repo Archetype');
811
+ lines.push('');
812
+ lines.push(`- **Label:** ${report.repoArchetype.label}`);
813
+ lines.push(`- **Summary:** ${report.repoArchetype.summary}`);
814
+ lines.push(`- **Stack family:** ${report.repoArchetype.stackFamily.label}`);
815
+ lines.push(`- **Topology:** ${report.repoArchetype.topology.label}`);
816
+ lines.push(`- **Primary workflow:** ${report.repoArchetype.primaryWorkflow.label}`);
817
+ lines.push(`- **Risk posture:** ${report.repoArchetype.riskProfile.label}`);
818
+ lines.push(`- **Confidence:** ${report.repoArchetype.confidence}`);
819
+ if (report.repoArchetype.signals.length > 0) {
820
+ lines.push(`- **Signals:** ${report.repoArchetype.signals.join(', ')}`);
821
+ }
822
+ lines.push('');
823
+
824
+ lines.push('## Recommended Operating Profile');
825
+ lines.push('');
826
+ lines.push(`- **Label:** ${report.recommendedOperatingProfile.label}`);
827
+ lines.push(`- **Summary:** ${report.recommendedOperatingProfile.summary}`);
828
+ lines.push(`- **Permission profile:** ${report.recommendedOperatingProfile.permissionProfile.label}`);
829
+ lines.push(`- **Governance pack:** ${report.recommendedOperatingProfile.governancePack.label}`);
830
+ lines.push(`- **Platform support:** ${(report.recommendedOperatingProfile.platformSupport.recommended || []).join(', ') || report.platformLabel}`);
831
+ if (report.recommendedOperatingProfile.platformSupport.optionalExpansion) {
832
+ lines.push(`- **Optional expansion:** ${report.recommendedOperatingProfile.platformSupport.optionalExpansion}`);
833
+ }
834
+ lines.push(`- **CI shape:** ${report.recommendedOperatingProfile.ciShape.label}`);
835
+ lines.push(`- **Verification:** ${report.recommendedOperatingProfile.verification.required.join(', ')}`);
836
+ lines.push(`- **Hooks:** ${report.recommendedOperatingProfile.hooks.map((hook) => hook.key).join(', ')}`);
837
+ lines.push('');
838
+
839
+ lines.push('## Adopt / Defer / Ignore');
840
+ lines.push('');
841
+ const decisionGroups = [
842
+ ['adopt', 'Adopt now'],
843
+ ['defer', 'Defer'],
844
+ ['ignore', 'Ignore'],
845
+ ];
846
+ for (const [decision, label] of decisionGroups) {
847
+ const items = report.adoptionGuidance.items.filter((item) => item.decision === decision);
848
+ if (items.length === 0) continue;
849
+ lines.push(`### ${label}`);
850
+ lines.push('');
851
+ for (const item of items) {
852
+ lines.push(`- **${item.label}** — ${item.why}`);
853
+ lines.push(` Evidence: ${item.evidence.join(' | ')}`);
854
+ lines.push(` Prerequisites: ${item.prerequisites.length > 0 ? item.prerequisites.join(' | ') : 'None'}`);
855
+ lines.push(` Expected benefit: ${item.expectedBenefit}`);
856
+ lines.push(` Rollback safety: ${item.rollbackSafety}`);
857
+ }
858
+ lines.push('');
859
+ }
860
+
733
861
  if (report.strengthsPreserved.length > 0) {
734
862
  lines.push('## Strengths Preserved (don\'t change these)');
735
863
  lines.push('');
@@ -10,6 +10,7 @@ const {
10
10
  } = require('./instruction-surfaces');
11
11
  const { collectClaudeDenyRules } = require('./permission-rules');
12
12
  const { containsEmbeddedSecret } = require('./secret-patterns');
13
+ const { containsPromptInjectionPattern } = require('./prompt-injection');
13
14
 
14
15
  const ANTI_PATTERNS = [
15
16
  {
@@ -218,6 +219,18 @@ const ANTI_PATTERNS = [
218
219
  return !hasTestInMd && !hasTestScript;
219
220
  },
220
221
  },
222
+ {
223
+ id: 'AP023',
224
+ name: 'Suspicious prompt-injection phrases in repo instructions',
225
+ severity: 'high',
226
+ description: 'Instruction surfaces that say things like "ignore previous instructions", "bypass guardrails", or "score 100/100" create confusion and downstream trust problems, even when the static audit itself is not LLM-driven.',
227
+ platforms: ['claude', 'codex', 'cursor', 'windsurf', 'copilot', 'gemini', 'aider', 'opencode'],
228
+ fix: 'Remove adversarial phrases from repo instructions and replace them with an explicit trust-boundary note about treating repo/web/MCP content as untrusted data.',
229
+ detect: (ctx) => {
230
+ const content = getRepoInstructionBundle(ctx);
231
+ return containsPromptInjectionPattern(content);
232
+ },
233
+ },
221
234
  {
222
235
  id: 'AP015',
223
236
  name: 'All permissions allowed',
@@ -0,0 +1,180 @@
1
+ const path = require('path');
2
+
3
+ const { hasWorkspaceConfig, detectWorkspaceGlobs, detectWorkspaces } = require('../workspace');
4
+ const { estimateTokenCount } = require('../token-estimate');
5
+
6
+ const LARGE_INSTRUCTION_WARN_TOKENS = 12000;
7
+ const LARGE_INSTRUCTION_SKIP_TOKENS = 240000;
8
+
9
+ function normalizeRelativePath(filePath) {
10
+ return String(filePath || '').replace(/\\/g, '/').replace(/^\.\//, '');
11
+ }
12
+
13
+ function formatCount(value) {
14
+ return Number(value || 0).toLocaleString('en-US');
15
+ }
16
+
17
+ function addPath(target, filePath) {
18
+ if (!filePath || typeof filePath !== 'string') return;
19
+ target.add(normalizeRelativePath(filePath));
20
+ }
21
+
22
+ function addDirFiles(ctx, target, dirPath, filter) {
23
+ if (typeof ctx.dirFiles !== 'function') return;
24
+ for (const file of ctx.dirFiles(dirPath)) {
25
+ if (filter && !filter.test(file)) continue;
26
+ addPath(target, path.join(dirPath, file));
27
+ }
28
+ }
29
+
30
+ function instructionFileCandidates(spec, ctx) {
31
+ const candidates = new Set();
32
+
33
+ if (spec.platform === 'claude') {
34
+ addPath(candidates, 'CLAUDE.md');
35
+ addPath(candidates, '.claude/CLAUDE.md');
36
+ addDirFiles(ctx, candidates, '.claude/rules', /\.md$/i);
37
+ addDirFiles(ctx, candidates, '.claude/commands', /\.md$/i);
38
+ addDirFiles(ctx, candidates, '.claude/agents', /\.md$/i);
39
+ if (typeof ctx.dirFiles === 'function') {
40
+ for (const skillDir of ctx.dirFiles('.claude/skills')) {
41
+ addPath(candidates, path.join('.claude', 'skills', skillDir, 'SKILL.md'));
42
+ }
43
+ }
44
+ }
45
+
46
+ if (spec.platform === 'codex') {
47
+ addPath(candidates, 'AGENTS.md');
48
+ addPath(candidates, 'AGENTS.override.md');
49
+ addPath(candidates, typeof ctx.agentsMdPath === 'function' ? ctx.agentsMdPath() : null);
50
+ addDirFiles(ctx, candidates, 'codex/rules');
51
+ addDirFiles(ctx, candidates, '.codex/rules');
52
+ if (typeof ctx.skillDirs === 'function') {
53
+ for (const skillDir of ctx.skillDirs()) {
54
+ addPath(candidates, path.join('.agents', 'skills', skillDir, 'SKILL.md'));
55
+ }
56
+ }
57
+ }
58
+
59
+ if (spec.platform === 'gemini') {
60
+ addPath(candidates, 'GEMINI.md');
61
+ addPath(candidates, '.gemini/GEMINI.md');
62
+ addDirFiles(ctx, candidates, '.gemini/agents', /\.md$/i);
63
+ if (typeof ctx.skillDirs === 'function') {
64
+ for (const skillDir of ctx.skillDirs()) {
65
+ addPath(candidates, path.join('.gemini', 'skills', skillDir, 'SKILL.md'));
66
+ }
67
+ }
68
+ }
69
+
70
+ if (spec.platform === 'copilot') {
71
+ addPath(candidates, '.github/copilot-instructions.md');
72
+ addDirFiles(ctx, candidates, '.github/instructions', /\.instructions\.md$/i);
73
+ addDirFiles(ctx, candidates, '.github/prompts', /\.prompt\.md$/i);
74
+ }
75
+
76
+ if (spec.platform === 'cursor') {
77
+ addPath(candidates, '.cursorrules');
78
+ addDirFiles(ctx, candidates, '.cursor/rules', /\.mdc$/i);
79
+ addDirFiles(ctx, candidates, '.cursor/commands', /\.md$/i);
80
+ }
81
+
82
+ if (spec.platform === 'windsurf') {
83
+ addPath(candidates, '.windsurfrules');
84
+ addDirFiles(ctx, candidates, '.windsurf/rules', /\.md$/i);
85
+ addDirFiles(ctx, candidates, '.windsurf/workflows', /\.md$/i);
86
+ addDirFiles(ctx, candidates, '.windsurf/memories', /\.(md|json)$/i);
87
+ }
88
+
89
+ if (spec.platform === 'aider' && typeof ctx.conventionFiles === 'function') {
90
+ for (const file of ctx.conventionFiles()) {
91
+ addPath(candidates, file);
92
+ }
93
+ }
94
+
95
+ if (spec.platform === 'opencode') {
96
+ addPath(candidates, 'AGENTS.md');
97
+ addPath(candidates, 'CLAUDE.md');
98
+ addDirFiles(ctx, candidates, '.opencode/commands', /\.(md|markdown|ya?ml)$/i);
99
+ if (typeof ctx.skillDirs === 'function') {
100
+ for (const skillDir of ctx.skillDirs()) {
101
+ addPath(candidates, path.join('.opencode', 'commands', skillDir, 'SKILL.md'));
102
+ }
103
+ }
104
+ }
105
+
106
+ return [...candidates];
107
+ }
108
+
109
+ function inspectInstructionFiles(spec, ctx) {
110
+ const warnings = [];
111
+
112
+ for (const filePath of instructionFileCandidates(spec, ctx)) {
113
+ const content = typeof ctx.fileContent === 'function' ? ctx.fileContent(filePath) : null;
114
+ const byteCount = typeof ctx.fileSizeBytes === 'function' ? ctx.fileSizeBytes(filePath) : null;
115
+ const tokenCount = typeof content === 'string' ? estimateTokenCount(content) : null;
116
+ if (!Number.isFinite(tokenCount) || tokenCount <= LARGE_INSTRUCTION_WARN_TOKENS) continue;
117
+
118
+ warnings.push({
119
+ file: normalizeRelativePath(filePath),
120
+ byteCount,
121
+ tokenCount,
122
+ lineCount: typeof content === 'string' ? content.split(/\r?\n/).length : null,
123
+ skipped: tokenCount > LARGE_INSTRUCTION_SKIP_TOKENS,
124
+ severity: tokenCount > LARGE_INSTRUCTION_SKIP_TOKENS ? 'critical' : 'warning',
125
+ message: tokenCount > LARGE_INSTRUCTION_SKIP_TOKENS
126
+ ? 'Instruction file exceeds ~240,000 tokens and will be skipped during audit.'
127
+ : 'Instruction file exceeds ~12,000 tokens. Audit will continue, but this file may reduce runtime clarity.',
128
+ });
129
+ }
130
+
131
+ return warnings;
132
+ }
133
+
134
+ function guardSkippedInstructionFiles(ctx, warnings) {
135
+ const skippedFiles = new Set(
136
+ warnings.filter((item) => item.skipped).map((item) => normalizeRelativePath(item.file))
137
+ );
138
+ if (skippedFiles.size === 0) return;
139
+
140
+ const originalFileContent = typeof ctx.fileContent === 'function' ? ctx.fileContent.bind(ctx) : null;
141
+ const originalLineNumber = typeof ctx.lineNumber === 'function' ? ctx.lineNumber.bind(ctx) : null;
142
+
143
+ if (originalFileContent) {
144
+ ctx.fileContent = (filePath) => {
145
+ if (skippedFiles.has(normalizeRelativePath(filePath))) return null;
146
+ return originalFileContent(filePath);
147
+ };
148
+ }
149
+
150
+ if (originalLineNumber) {
151
+ ctx.lineNumber = (filePath, matcher) => {
152
+ if (skippedFiles.has(normalizeRelativePath(filePath))) return null;
153
+ return originalLineNumber(filePath, matcher);
154
+ };
155
+ }
156
+ }
157
+
158
+ function buildWorkspaceHint(dir) {
159
+ if (!hasWorkspaceConfig(dir)) return null;
160
+
161
+ const patterns = detectWorkspaceGlobs(dir);
162
+ const workspaces = detectWorkspaces(dir);
163
+ if (patterns.length === 0 && workspaces.length === 0) return null;
164
+
165
+ return {
166
+ detected: true,
167
+ patterns,
168
+ workspaces,
169
+ suggestedCommand: patterns.length > 0
170
+ ? `npx nerviq audit --workspace ${patterns.join(',')}`
171
+ : `npx nerviq audit --workspace ${workspaces.join(',')}`,
172
+ };
173
+ }
174
+
175
+ module.exports = {
176
+ buildWorkspaceHint,
177
+ formatCount,
178
+ guardSkippedInstructionFiles,
179
+ inspectInstructionFiles,
180
+ };