@nerviq/cli 1.11.0 → 1.12.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 (49) hide show
  1. package/README.md +97 -19
  2. package/bin/cli.js +618 -182
  3. package/package.json +2 -2
  4. package/src/activity.js +49 -9
  5. package/src/adoption-advisor.js +299 -0
  6. package/src/aider/techniques.js +16 -11
  7. package/src/analyze.js +128 -0
  8. package/src/anti-patterns.js +13 -0
  9. package/src/audit.js +97 -22
  10. package/src/behavioral-drift.js +801 -0
  11. package/src/continuous-ops.js +681 -0
  12. package/src/cost-tracking.js +61 -0
  13. package/src/cursor/techniques.js +17 -12
  14. package/src/deep-review.js +83 -0
  15. package/src/diff-only.js +280 -0
  16. package/src/doctor.js +118 -55
  17. package/src/governance.js +59 -43
  18. package/src/hook-validation.js +342 -0
  19. package/src/index.js +5 -0
  20. package/src/integrations.js +42 -5
  21. package/src/mcp-validation.js +337 -0
  22. package/src/opencode/techniques.js +12 -7
  23. package/src/operating-profile.js +574 -0
  24. package/src/org.js +97 -13
  25. package/src/plans.js +192 -8
  26. package/src/platform-change-manifest.js +86 -0
  27. package/src/policy-layers.js +210 -0
  28. package/src/profiles.js +4 -1
  29. package/src/prompt-injection.js +74 -0
  30. package/src/repo-archetype.js +386 -0
  31. package/src/setup.js +34 -0
  32. package/src/source-urls.js +132 -132
  33. package/src/supplemental-checks.js +13 -12
  34. package/src/techniques/api.js +407 -0
  35. package/src/techniques/automation.js +316 -0
  36. package/src/techniques/compliance.js +257 -0
  37. package/src/techniques/hygiene.js +294 -0
  38. package/src/techniques/instructions.js +243 -0
  39. package/src/techniques/observability.js +226 -0
  40. package/src/techniques/optimization.js +142 -0
  41. package/src/techniques/quality.js +317 -0
  42. package/src/techniques/security.js +237 -0
  43. package/src/techniques/shared.js +443 -0
  44. package/src/techniques/stacks.js +2294 -0
  45. package/src/techniques/tools.js +106 -0
  46. package/src/techniques/workflow.js +413 -0
  47. package/src/techniques.js +78 -5607
  48. package/src/watch.js +18 -0
  49. 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',
package/src/audit.js CHANGED
@@ -64,8 +64,9 @@ function formatLocation(file, line) {
64
64
  return line ? `${file}:${line}` : file;
65
65
  }
66
66
 
67
- const IMPACT_ORDER = { critical: 3, high: 2, medium: 1, low: 0 };
68
- const WEIGHTS = { critical: 15, high: 10, medium: 5, low: 2 };
67
+ const IMPACT_ORDER = { critical: 3, high: 2, medium: 1, low: 0 };
68
+ const WEIGHTS = { critical: 15, high: 10, medium: 5, low: 2 };
69
+ const SCORE_MILESTONES = [50, 70, 90, 100];
69
70
  const LARGE_INSTRUCTION_WARN_TOKENS = 12000;
70
71
  const LARGE_INSTRUCTION_SKIP_TOKENS = 240000;
71
72
  const CATEGORY_MODULES = {
@@ -659,7 +660,7 @@ function getRecommendationPriorityScore(item, outcomeSummaryByKey = {}, fpFeedba
659
660
  return raw * getFpFeedbackMultiplier(fpFeedbackByKey, item.key);
660
661
  }
661
662
 
662
- function buildTopNextActions(failed, limit = 5, outcomeSummaryByKey = {}, options = {}) {
663
+ function buildTopNextActions(failed, limit = 5, outcomeSummaryByKey = {}, options = {}) {
663
664
  const pool = getPrioritizedFailed(failed);
664
665
  const fpByKey = options.fpFeedbackByKey || null;
665
666
 
@@ -724,11 +725,65 @@ function buildTopNextActions(failed, limit = 5, outcomeSummaryByKey = {}, option
724
725
  avgScoreDelta: feedback.avgScoreDelta,
725
726
  } : null,
726
727
  });
727
- });
728
- }
729
-
730
- function computeCategoryScores(applicable, passed) {
731
- const grouped = {};
728
+ });
729
+ }
730
+
731
+ function getNextScoreMilestone(score) {
732
+ return SCORE_MILESTONES.find((milestone) => score < milestone) || null;
733
+ }
734
+
735
+ function buildScoreCoaching({ score, earnedPoints, maxPoints, failed, outcomeSummaryByKey = {}, platform, fpFeedbackByKey = null }) {
736
+ if (!Array.isArray(failed) || failed.length === 0 || !Number.isFinite(maxPoints) || maxPoints <= 0) {
737
+ return null;
738
+ }
739
+
740
+ const nextMilestone = getNextScoreMilestone(score);
741
+ if (!nextMilestone) return null;
742
+
743
+ const targetEarnedPoints = Math.ceil((nextMilestone / 100) * maxPoints);
744
+ const pointsNeeded = Math.max(0, targetEarnedPoints - earnedPoints);
745
+ if (pointsNeeded <= 0) return null;
746
+
747
+ const rankedActions = buildTopNextActions(failed, failed.length, outcomeSummaryByKey, { platform, fpFeedbackByKey });
748
+ if (rankedActions.length === 0) return null;
749
+
750
+ const failedByKey = new Map(failed.map((item) => [item.key, item]));
751
+ const selected = [];
752
+ let recoveredPoints = 0;
753
+
754
+ for (const action of rankedActions) {
755
+ const source = failedByKey.get(action.key);
756
+ if (!source) continue;
757
+ selected.push({
758
+ key: source.key,
759
+ name: source.name,
760
+ impact: source.impact,
761
+ weight: WEIGHTS[source.impact] || 0,
762
+ });
763
+ recoveredPoints += WEIGHTS[source.impact] || 0;
764
+ if (recoveredPoints >= pointsNeeded) break;
765
+ }
766
+
767
+ if (selected.length === 0) return null;
768
+
769
+ const fixesNeeded = selected.length;
770
+ const projectedScore = Math.round(((earnedPoints + recoveredPoints) / maxPoints) * 100);
771
+ const summary = `You're ${fixesNeeded} ${fixesNeeded === 1 ? 'fix' : 'fixes'} away from ${nextMilestone}/100.`;
772
+
773
+ return {
774
+ currentScore: score,
775
+ nextMilestone,
776
+ pointsNeeded,
777
+ fixesNeeded,
778
+ projectedScore: Math.min(100, projectedScore),
779
+ summary,
780
+ recommendedKeys: selected.map((item) => item.key),
781
+ recommendedNames: selected.map((item) => item.name),
782
+ };
783
+ }
784
+
785
+ function computeCategoryScores(applicable, passed) {
786
+ const grouped = {};
732
787
 
733
788
  for (const item of applicable) {
734
789
  const category = item.category || 'unknown';
@@ -915,6 +970,9 @@ function printLiteAudit(result, dir) {
915
970
  scoreExplanation = t('audit.early');
916
971
  }
917
972
  console.log(colorize(` ${scoreExplanation}`, 'dim'));
973
+ if (result.scoreCoaching) {
974
+ console.log(colorize(` Milestone: ${result.scoreCoaching.summary}`, 'magenta'));
975
+ }
918
976
  console.log(colorize(' Score type: live repo audit (current files only, not snapshot history or benchmark projection).', 'dim'));
919
977
 
920
978
  if (result.platformScopeNote) {
@@ -1159,7 +1217,7 @@ async function audit(options) {
1159
1217
  const recommendedDomainPacks = spec.platform === 'codex'
1160
1218
  ? detectCodexDomainPacks(ctx, stacks, getCodexDomainPackSignals(ctx))
1161
1219
  : [];
1162
- const result = {
1220
+ const result = {
1163
1221
  platform: spec.platform,
1164
1222
  platformLabel: spec.platformLabel,
1165
1223
  platformVersion: spec.platformVersion,
@@ -1182,9 +1240,18 @@ async function audit(options) {
1182
1240
  deprecatedReason: r.deprecatedReason || null,
1183
1241
  sunsetDate: r.sunsetDate || null,
1184
1242
  })),
1185
- categoryScores,
1186
- quickWins: quickWins.map(({ key, name, impact, fix, category, sourceUrl }) => ({ key, name, impact, category, fix, sourceUrl })),
1187
- topNextActions,
1243
+ categoryScores,
1244
+ scoreCoaching: buildScoreCoaching({
1245
+ score,
1246
+ earnedPoints: earnedScore,
1247
+ maxPoints: maxScore,
1248
+ failed,
1249
+ outcomeSummaryByKey: outcomeSummary.byKey,
1250
+ platform: spec.platform,
1251
+ fpFeedbackByKey: fpFeedback.byKey,
1252
+ }),
1253
+ quickWins: quickWins.map(({ key, name, impact, fix, category, sourceUrl }) => ({ key, name, impact, category, fix, sourceUrl })),
1254
+ topNextActions,
1188
1255
  recommendationOutcomes: {
1189
1256
  totalEntries: outcomeSummary.totalEntries,
1190
1257
  keysTracked: outcomeSummary.keys,
@@ -1214,11 +1281,12 @@ async function audit(options) {
1214
1281
  result.detectedConfigFiles = configFiles;
1215
1282
 
1216
1283
  result.suggestedNextCommand = inferSuggestedNextCommand(result);
1217
- result.liteSummary = {
1218
- topNextActions: topNextActions.slice(0, 3),
1219
- nextCommand: result.suggestedNextCommand,
1220
- platformCaveats: platformCaveats.slice(0, 2),
1221
- };
1284
+ result.liteSummary = {
1285
+ topNextActions: topNextActions.slice(0, 3),
1286
+ nextCommand: result.suggestedNextCommand,
1287
+ platformCaveats: platformCaveats.slice(0, 2),
1288
+ scoreCoaching: result.scoreCoaching,
1289
+ };
1222
1290
 
1223
1291
  // Silent mode: skip all output, just return result
1224
1292
  if (silent) {
@@ -1313,11 +1381,18 @@ async function audit(options) {
1313
1381
  console.log('');
1314
1382
 
1315
1383
  // Score
1316
- console.log(` ${progressBar(score)} ${colorize(`${score}/100`, 'bold')}`);
1317
- if (isScaffolded && scaffoldedPassed.length > 0) {
1318
- console.log(colorize(` Organic: ${organicScore}/100 (without nerviq generated files)`, 'dim'));
1319
- }
1320
- console.log('');
1384
+ console.log(` ${progressBar(score)} ${colorize(`${score}/100`, 'bold')}`);
1385
+ if (isScaffolded && scaffoldedPassed.length > 0) {
1386
+ console.log(colorize(` Organic: ${organicScore}/100 (without nerviq generated files)`, 'dim'));
1387
+ }
1388
+ if (result.scoreCoaching) {
1389
+ const fastestPath = result.scoreCoaching.recommendedNames.slice(0, 3).join(', ');
1390
+ console.log(colorize(` Milestone: ${result.scoreCoaching.summary}`, 'magenta'));
1391
+ if (fastestPath) {
1392
+ console.log(colorize(` Fastest path: ${fastestPath}`, 'dim'));
1393
+ }
1394
+ }
1395
+ console.log('');
1321
1396
 
1322
1397
  // Passed
1323
1398
  if (passed.length > 0) {