@neurcode-ai/cli 0.8.8 → 0.8.13

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.
@@ -41,6 +41,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
41
41
  exports.verifyCommand = verifyCommand;
42
42
  const child_process_1 = require("child_process");
43
43
  const diff_parser_1 = require("@neurcode-ai/diff-parser");
44
+ const policy_engine_1 = require("@neurcode-ai/policy-engine");
44
45
  const config_1 = require("../config");
45
46
  const api_client_1 = require("../api-client");
46
47
  const path_1 = require("path");
@@ -48,6 +49,7 @@ const fs_1 = require("fs");
48
49
  const state_1 = require("../utils/state");
49
50
  const ROILogger_1 = require("../utils/ROILogger");
50
51
  const box_1 = require("../utils/box");
52
+ const ignore_1 = require("../utils/ignore");
51
53
  // Import chalk with fallback
52
54
  let chalk;
53
55
  try {
@@ -112,6 +114,67 @@ function isExcludedFile(filePath) {
112
114
  }
113
115
  return false;
114
116
  }
117
+ /**
118
+ * Map a dashboard custom policy (natural language) to a policy-engine Rule.
119
+ * Supports "No console.log", "No debugger", and generic line patterns.
120
+ */
121
+ function customPolicyToRule(p) {
122
+ const sev = p.severity === 'high' ? 'block' : 'warn';
123
+ const text = (p.rule_text || '').trim().toLowerCase();
124
+ if (!text)
125
+ return null;
126
+ // Known patterns -> suspicious-keywords (checks added lines for these strings)
127
+ if (/console\.log|no\s+console\.log|do not use console\.log|ban console\.log/.test(text)) {
128
+ return {
129
+ id: `custom-${p.id}`,
130
+ name: 'No console.log',
131
+ description: p.rule_text,
132
+ enabled: true,
133
+ severity: sev,
134
+ type: 'suspicious-keywords',
135
+ keywords: ['console.log'],
136
+ };
137
+ }
138
+ if (/debugger|no\s+debugger|do not use debugger/.test(text)) {
139
+ return {
140
+ id: `custom-${p.id}`,
141
+ name: 'No debugger',
142
+ description: p.rule_text,
143
+ enabled: true,
144
+ severity: sev,
145
+ type: 'suspicious-keywords',
146
+ keywords: ['debugger'],
147
+ };
148
+ }
149
+ if (/eval\s*\(|no\s+eval|do not use eval/.test(text)) {
150
+ return {
151
+ id: `custom-${p.id}`,
152
+ name: 'No eval',
153
+ description: p.rule_text,
154
+ enabled: true,
155
+ severity: sev,
156
+ type: 'suspicious-keywords',
157
+ keywords: ['eval('],
158
+ };
159
+ }
160
+ // Fallback: line-pattern on added lines using a safe regex from the rule text
161
+ // Use first quoted phrase or first alphanumeric phrase as pattern
162
+ const quoted = /['"`]([^'"`]+)['"`]/.exec(p.rule_text);
163
+ const phrase = quoted?.[1] ?? p.rule_text.replace(/^(no|don't|do not use|ban|avoid)\s+/i, '').trim().slice(0, 80);
164
+ if (!phrase)
165
+ return null;
166
+ const escaped = phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
167
+ return {
168
+ id: `custom-${p.id}`,
169
+ name: `Custom: ${p.rule_text.slice(0, 50)}`,
170
+ description: p.rule_text,
171
+ enabled: true,
172
+ severity: sev,
173
+ type: 'line-pattern',
174
+ pattern: escaped,
175
+ matchType: 'added',
176
+ };
177
+ }
115
178
  async function verifyCommand(options) {
116
179
  try {
117
180
  // Load configuration
@@ -187,8 +250,10 @@ async function verifyCommand(options) {
187
250
  else {
188
251
  console.log(JSON.stringify({
189
252
  grade: 'F',
190
- adherenceScore: 0,
253
+ score: 0,
191
254
  verdict: 'FAIL',
255
+ violations: [],
256
+ adherenceScore: 0,
192
257
  bloatCount: 0,
193
258
  bloatFiles: [],
194
259
  plannedFilesModified: 0,
@@ -217,8 +282,10 @@ async function verifyCommand(options) {
217
282
  else {
218
283
  console.log(JSON.stringify({
219
284
  grade: 'F',
220
- adherenceScore: 0,
285
+ score: 0,
221
286
  verdict: 'FAIL',
287
+ violations: [],
288
+ adherenceScore: 0,
222
289
  bloatCount: 0,
223
290
  bloatFiles: [],
224
291
  plannedFilesModified: 0,
@@ -229,11 +296,93 @@ async function verifyCommand(options) {
229
296
  }
230
297
  process.exit(0);
231
298
  }
299
+ const ignoreFilter = (0, ignore_1.loadIgnore)(process.cwd());
232
300
  if (!options.json) {
233
301
  console.log(chalk.cyan('\n📊 Analyzing changes against plan...'));
234
302
  console.log(chalk.dim(` Found ${summary.totalFiles} file(s) changed`));
235
303
  console.log(chalk.dim(` ${summary.totalAdded} lines added, ${summary.totalRemoved} lines removed\n`));
236
304
  }
305
+ // ============================================
306
+ // --policy-only: General Governance (policy only, no plan enforcement)
307
+ // ============================================
308
+ if (options.policyOnly) {
309
+ if (!options.json) {
310
+ console.log(chalk.cyan('🛡️ General Governance mode (policy only, no plan linked)\n'));
311
+ }
312
+ let policyViolations = [];
313
+ let policyDecision = 'allow';
314
+ const defaultPolicy = (0, policy_engine_1.createDefaultPolicy)();
315
+ let allRules = [...defaultPolicy.rules];
316
+ if (config.apiKey) {
317
+ try {
318
+ const customPolicies = await client.getActiveCustomPolicies();
319
+ const customRules = [];
320
+ for (const p of customPolicies) {
321
+ const r = customPolicyToRule(p);
322
+ if (r)
323
+ customRules.push(r);
324
+ }
325
+ allRules = [...defaultPolicy.rules, ...customRules];
326
+ if (!options.json && customRules.length > 0) {
327
+ console.log(chalk.dim(` Evaluating ${customRules.length} custom policy rule(s) from dashboard`));
328
+ }
329
+ }
330
+ catch {
331
+ if (!options.json) {
332
+ console.log(chalk.dim(' Could not load custom policies, using default policy only'));
333
+ }
334
+ }
335
+ }
336
+ const diffFilesForPolicy = diffFiles.filter((f) => !ignoreFilter(f.path));
337
+ const policyResult = (0, policy_engine_1.evaluateRules)(diffFilesForPolicy, allRules);
338
+ policyViolations = (policyResult.violations || []);
339
+ policyViolations = policyViolations.filter((v) => !ignoreFilter(v.file));
340
+ policyDecision = policyViolations.length > 0 ? policyResult.decision : 'allow';
341
+ const effectiveVerdict = policyDecision === 'block' ? 'FAIL' : policyDecision === 'warn' ? 'WARN' : 'PASS';
342
+ const grade = effectiveVerdict === 'PASS' ? 'A' : effectiveVerdict === 'WARN' ? 'C' : 'F';
343
+ const score = effectiveVerdict === 'PASS' ? 100 : effectiveVerdict === 'WARN' ? 50 : 0;
344
+ const violationsOutput = policyViolations.map((v) => ({
345
+ file: v.file,
346
+ rule: v.rule,
347
+ severity: v.severity,
348
+ message: v.message,
349
+ ...(v.line != null ? { startLine: v.line } : {}),
350
+ }));
351
+ const message = effectiveVerdict === 'PASS'
352
+ ? '✅ Policy check passed (General Governance mode)'
353
+ : policyViolations.length > 0
354
+ ? `Policy violations: ${policyViolations.map((v) => `${v.file}: ${v.message || v.rule}`).join('; ')}`
355
+ : 'Policy check completed';
356
+ if (options.json) {
357
+ console.log(JSON.stringify({
358
+ grade,
359
+ score,
360
+ verdict: effectiveVerdict,
361
+ violations: violationsOutput,
362
+ message,
363
+ scopeGuardPassed: true, // N/A in policy-only mode
364
+ bloatCount: 0,
365
+ bloatFiles: [],
366
+ plannedFilesModified: 0,
367
+ totalPlannedFiles: 0,
368
+ adherenceScore: score,
369
+ policyOnly: true,
370
+ }, null, 2));
371
+ }
372
+ else {
373
+ if (effectiveVerdict === 'PASS') {
374
+ console.log(chalk.green('✅ Policy check passed'));
375
+ }
376
+ else {
377
+ console.log(chalk.red(`❌ Policy violations detected: ${policyViolations.length}`));
378
+ policyViolations.forEach((v) => {
379
+ console.log(chalk.red(` • ${v.file}: ${v.message || v.rule}`));
380
+ });
381
+ }
382
+ console.log(chalk.dim(`\n${message}`));
383
+ }
384
+ process.exit(effectiveVerdict === 'FAIL' ? 2 : effectiveVerdict === 'WARN' ? 1 : 0);
385
+ }
237
386
  // Get planId: Priority 1: options flag, Priority 2: state file (.neurcode/config.json), Priority 3: legacy config
238
387
  let planId = options.planId;
239
388
  if (!planId) {
@@ -278,8 +427,10 @@ async function verifyCommand(options) {
278
427
  else {
279
428
  console.log(JSON.stringify({
280
429
  grade: 'F',
281
- adherenceScore: 0,
430
+ score: 0,
282
431
  verdict: 'FAIL',
432
+ violations: [],
433
+ adherenceScore: 0,
283
434
  bloatCount: 0,
284
435
  bloatFiles: [],
285
436
  plannedFilesModified: 0,
@@ -353,19 +504,28 @@ async function verifyCommand(options) {
353
504
  // Step C: The Intersection Logic
354
505
  const approvedSet = new Set([...planFiles, ...allowedFiles]);
355
506
  const violations = modifiedFiles.filter(f => !approvedSet.has(f));
356
- // Step D: The Block
357
- if (violations.length > 0) {
507
+ const filteredViolations = violations.filter((p) => !ignoreFilter(p));
508
+ // Step D: The Block (only report scope violations for non-ignored files)
509
+ if (filteredViolations.length > 0) {
358
510
  if (options.json) {
359
- // Output JSON for scope violation BEFORE exit
511
+ // Output JSON for scope violation BEFORE exit. Must include violations for GitHub Action annotations.
512
+ const violationsOutput = filteredViolations.map((file) => ({
513
+ file,
514
+ rule: 'scope_guard',
515
+ severity: 'block',
516
+ message: 'File modified outside the plan',
517
+ }));
360
518
  const jsonOutput = {
361
519
  grade: 'F',
362
- adherenceScore: 0,
520
+ score: 0,
363
521
  verdict: 'FAIL',
364
- bloatCount: violations.length,
365
- bloatFiles: violations,
522
+ violations: violationsOutput,
523
+ adherenceScore: 0,
524
+ bloatCount: filteredViolations.length,
525
+ bloatFiles: filteredViolations,
366
526
  plannedFilesModified: 0,
367
527
  totalPlannedFiles: planFiles.length,
368
- message: `Scope violation: ${violations.length} file(s) modified outside the plan`,
528
+ message: `Scope violation: ${filteredViolations.length} file(s) modified outside the plan`,
369
529
  scopeGuardPassed: false,
370
530
  };
371
531
  // CRITICAL: Print JSON first, then exit
@@ -378,12 +538,12 @@ async function verifyCommand(options) {
378
538
  console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
379
539
  console.log(chalk.red('The following files were modified but are not in the plan:'));
380
540
  console.log('');
381
- violations.forEach(file => {
541
+ filteredViolations.forEach(file => {
382
542
  console.log(chalk.red(` • ${file}`));
383
543
  });
384
544
  console.log('');
385
545
  console.log(chalk.yellow('To unblock these files, run:'));
386
- violations.forEach(file => {
546
+ filteredViolations.forEach(file => {
387
547
  console.log(chalk.dim(` neurcode allow ${file}`));
388
548
  });
389
549
  console.log('');
@@ -424,8 +584,10 @@ async function verifyCommand(options) {
424
584
  else {
425
585
  console.log(JSON.stringify({
426
586
  grade: 'N/A',
427
- adherenceScore: 0,
587
+ score: 0,
428
588
  verdict: 'INFO',
589
+ violations: [],
590
+ adherenceScore: 0,
429
591
  bloatCount: 0,
430
592
  bloatFiles: [],
431
593
  plannedFilesModified: 0,
@@ -437,6 +599,33 @@ async function verifyCommand(options) {
437
599
  }
438
600
  process.exit(0);
439
601
  }
602
+ let policyViolations = [];
603
+ let policyDecision = 'allow';
604
+ if (config.apiKey) {
605
+ try {
606
+ const customPolicies = await client.getActiveCustomPolicies();
607
+ const defaultPolicy = (0, policy_engine_1.createDefaultPolicy)();
608
+ const customRules = [];
609
+ for (const p of customPolicies) {
610
+ const r = customPolicyToRule(p);
611
+ if (r)
612
+ customRules.push(r);
613
+ }
614
+ const allRules = [...defaultPolicy.rules, ...customRules];
615
+ const diffFilesForPolicy = diffFiles.filter((f) => !ignoreFilter(f.path));
616
+ const policyResult = (0, policy_engine_1.evaluateRules)(diffFilesForPolicy, allRules);
617
+ policyViolations = policyResult.violations.filter((v) => !ignoreFilter(v.file));
618
+ policyDecision = policyViolations.length > 0 ? policyResult.decision : 'allow';
619
+ if (!options.json && customRules.length > 0) {
620
+ console.log(chalk.dim(` Evaluating ${customRules.length} custom policy rule(s) from dashboard`));
621
+ }
622
+ }
623
+ catch {
624
+ if (!options.json) {
625
+ console.log(chalk.dim(' Could not load custom policies, continuing without them'));
626
+ }
627
+ }
628
+ }
440
629
  // Prepare diff stats and changed files for API
441
630
  const diffStats = {
442
631
  totalAdded: summary.totalAdded,
@@ -478,7 +667,13 @@ async function verifyCommand(options) {
478
667
  }
479
668
  // Call verifyPlan with intentConstraints
480
669
  const verifyResult = await client.verifyPlan(finalPlanId, diffStats, changedFiles, projectId, intentConstraints);
481
- // Calculate grade from verdict and score
670
+ // Apply custom policy verdict: block from dashboard overrides API verdict
671
+ const policyBlock = policyDecision === 'block' && policyViolations.length > 0;
672
+ const effectiveVerdict = policyBlock ? 'FAIL' : verifyResult.verdict;
673
+ const effectiveMessage = policyBlock
674
+ ? `Custom policy violations: ${policyViolations.map(v => `${v.file}: ${v.message || v.rule}`).join('; ')}. ${verifyResult.message}`
675
+ : verifyResult.message;
676
+ // Calculate grade from effective verdict and score
482
677
  // CRITICAL: 0/0 planned files = 'F' (Incomplete), not 'B'
483
678
  // Bloat automatically drops grade by at least one letter
484
679
  let grade;
@@ -486,7 +681,7 @@ async function verifyCommand(options) {
486
681
  if (verifyResult.totalPlannedFiles === 0 && verifyResult.plannedFilesModified === 0) {
487
682
  grade = 'F';
488
683
  }
489
- else if (verifyResult.verdict === 'PASS') {
684
+ else if (effectiveVerdict === 'PASS') {
490
685
  grade = 'A';
491
686
  // Log ROI event for PASS verification (Grade A) - non-blocking
492
687
  try {
@@ -503,7 +698,7 @@ async function verifyCommand(options) {
503
698
  // Silently ignore - ROI logging should never block user workflows
504
699
  }
505
700
  }
506
- else if (verifyResult.verdict === 'WARN') {
701
+ else if (effectiveVerdict === 'WARN') {
507
702
  // Base grade calculation
508
703
  let baseGrade = verifyResult.adherenceScore >= 70 ? 'B' : verifyResult.adherenceScore >= 50 ? 'C' : 'D';
509
704
  // Bloat drops grade by one letter (B -> C, C -> D, D -> F)
@@ -522,49 +717,82 @@ async function verifyCommand(options) {
522
717
  }
523
718
  // If JSON output requested, output JSON and exit
524
719
  if (options.json) {
720
+ const filteredBloatFiles = (verifyResult.bloatFiles || []).filter((f) => !ignoreFilter(f));
721
+ const scopeViolations = filteredBloatFiles.map((file) => ({
722
+ file,
723
+ rule: 'scope_guard',
724
+ severity: 'block',
725
+ message: 'File modified outside the plan',
726
+ }));
727
+ const policyViolationItems = policyViolations.map((v) => ({
728
+ file: v.file,
729
+ rule: v.rule,
730
+ severity: v.severity,
731
+ message: v.message,
732
+ ...(v.line != null ? { startLine: v.line } : {}),
733
+ }));
734
+ const violations = [...scopeViolations, ...policyViolationItems];
525
735
  const jsonOutput = {
526
736
  grade,
737
+ score: verifyResult.adherenceScore,
738
+ verdict: effectiveVerdict,
739
+ violations,
740
+ message: effectiveMessage,
527
741
  adherenceScore: verifyResult.adherenceScore,
528
- verdict: verifyResult.verdict,
529
- bloatCount: verifyResult.bloatCount,
530
- bloatFiles: verifyResult.bloatFiles,
742
+ scopeGuardPassed,
743
+ bloatCount: filteredBloatFiles.length,
744
+ bloatFiles: filteredBloatFiles,
531
745
  plannedFilesModified: verifyResult.plannedFilesModified,
532
746
  totalPlannedFiles: verifyResult.totalPlannedFiles,
533
- message: verifyResult.message,
534
- scopeGuardPassed,
747
+ ...(policyViolations.length > 0 && { policyDecision }),
535
748
  };
536
749
  console.log(JSON.stringify(jsonOutput, null, 2));
537
750
  // Report to Neurcode Cloud if --record flag is set (after JSON output)
538
751
  if (options.record && config.apiKey) {
539
- const violations = verifyResult.bloatFiles.map(file => ({
540
- rule: 'scope_guard',
541
- file: file,
542
- severity: 'block',
543
- message: 'File modified outside the plan',
544
- }));
752
+ const violations = [
753
+ ...filteredBloatFiles.map((file) => ({
754
+ rule: 'scope_guard',
755
+ file: file,
756
+ severity: 'block',
757
+ message: 'File modified outside the plan',
758
+ })),
759
+ ...policyViolations.map(v => ({
760
+ rule: v.rule,
761
+ file: v.file,
762
+ severity: v.severity,
763
+ message: v.message,
764
+ })),
765
+ ];
545
766
  // Report in background (don't await to avoid blocking JSON output)
546
767
  reportVerification(grade, violations, verifyResult, config.apiKey, config.apiUrl, projectId || undefined, true // jsonMode = true
547
768
  ).catch(() => {
548
769
  // Error already logged in reportVerification
549
770
  });
550
771
  }
551
- // Exit based on verdict (same logic as below)
552
- if (scopeGuardPassed) {
772
+ // Exit based on effective verdict (same logic as below)
773
+ if (scopeGuardPassed && !policyBlock) {
553
774
  process.exit(0);
554
775
  }
555
- if (verifyResult.verdict === 'FAIL') {
776
+ if (effectiveVerdict === 'FAIL') {
556
777
  process.exit(2);
557
778
  }
558
- else if (verifyResult.verdict === 'WARN') {
779
+ else if (effectiveVerdict === 'WARN') {
559
780
  process.exit(1);
560
781
  }
561
782
  else {
562
783
  process.exit(0);
563
784
  }
564
785
  }
565
- // Display results (only if not in json mode)
786
+ // Display results (only if not in json mode; exclude ignored paths from bloat)
566
787
  if (!options.json) {
567
- displayVerifyResults(verifyResult);
788
+ const displayBloatFiles = (verifyResult.bloatFiles || []).filter((f) => !ignoreFilter(f));
789
+ displayVerifyResults({
790
+ ...verifyResult,
791
+ verdict: effectiveVerdict,
792
+ message: effectiveMessage,
793
+ bloatFiles: displayBloatFiles,
794
+ bloatCount: displayBloatFiles.length,
795
+ }, policyViolations);
568
796
  }
569
797
  // Report to Neurcode Cloud if --record flag is set
570
798
  if (options.record) {
@@ -575,23 +803,30 @@ async function verifyCommand(options) {
575
803
  }
576
804
  }
577
805
  else {
578
- // Extract violations from scope guard check or create empty array
579
- // Note: For plan verifications, violations are typically scope-related
580
- // We'll create a simplified violations array based on bloat files
581
- const violations = verifyResult.bloatFiles.map(file => ({
582
- rule: 'scope_guard',
583
- file: file,
584
- severity: 'block',
585
- message: 'File modified outside the plan',
586
- }));
806
+ // Include scope bloat and custom policy violations in the report (excluding .neurcodeignore'd paths)
807
+ const filteredBloatForReport = (verifyResult.bloatFiles || []).filter((f) => !ignoreFilter(f));
808
+ const violations = [
809
+ ...filteredBloatForReport.map((file) => ({
810
+ rule: 'scope_guard',
811
+ file: file,
812
+ severity: 'block',
813
+ message: 'File modified outside the plan',
814
+ })),
815
+ ...policyViolations.map(v => ({
816
+ rule: v.rule,
817
+ file: v.file,
818
+ severity: v.severity,
819
+ message: v.message,
820
+ })),
821
+ ];
587
822
  await reportVerification(grade, violations, verifyResult, config.apiKey, config.apiUrl, projectId || undefined, false // jsonMode = false
588
823
  );
589
824
  }
590
825
  }
591
826
  // Governance takes priority over Grading
592
- // If Scope Guard passed (all files approved or allowed), always PASS
593
- if (scopeGuardPassed) {
594
- if (verifyResult.verdict === 'FAIL' || verifyResult.verdict === 'WARN') {
827
+ // If Scope Guard passed (all files approved or allowed) and no policy block, always PASS
828
+ if (scopeGuardPassed && !policyBlock) {
829
+ if ((verifyResult.verdict === 'FAIL' || verifyResult.verdict === 'WARN') && policyViolations.length === 0) {
595
830
  if (!options.json) {
596
831
  console.log(chalk.yellow('\n⚠️ Plan deviation allowed'));
597
832
  console.log(chalk.dim(' Some files were modified outside the plan, but they were explicitly allowed.'));
@@ -600,12 +835,12 @@ async function verifyCommand(options) {
600
835
  }
601
836
  process.exit(0);
602
837
  }
603
- // If scope guard didn't pass (or failed to check), use AI verdict
604
- // Exit with appropriate code based on AI verification
605
- if (verifyResult.verdict === 'FAIL') {
838
+ // If scope guard didn't pass (or failed to check) or policy blocked, use effective verdict
839
+ // Exit with appropriate code based on AI verification and custom policies
840
+ if (effectiveVerdict === 'FAIL') {
606
841
  process.exit(2);
607
842
  }
608
- else if (verifyResult.verdict === 'WARN') {
843
+ else if (effectiveVerdict === 'WARN') {
609
844
  process.exit(1);
610
845
  }
611
846
  else {
@@ -617,8 +852,10 @@ async function verifyCommand(options) {
617
852
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
618
853
  console.log(JSON.stringify({
619
854
  grade: 'F',
620
- adherenceScore: 0,
855
+ score: 0,
621
856
  verdict: 'FAIL',
857
+ violations: [],
858
+ adherenceScore: 0,
622
859
  bloatCount: 0,
623
860
  bloatFiles: [],
624
861
  plannedFilesModified: 0,
@@ -650,8 +887,10 @@ async function verifyCommand(options) {
650
887
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
651
888
  console.log(JSON.stringify({
652
889
  grade: 'F',
653
- adherenceScore: 0,
890
+ score: 0,
654
891
  verdict: 'FAIL',
892
+ violations: [],
893
+ adherenceScore: 0,
655
894
  bloatCount: 0,
656
895
  bloatFiles: [],
657
896
  plannedFilesModified: 0,
@@ -812,7 +1051,7 @@ async function reportVerification(grade, violations, verifyResult, apiKey, apiUr
812
1051
  /**
813
1052
  * Display verification results in a formatted report card
814
1053
  */
815
- function displayVerifyResults(result) {
1054
+ function displayVerifyResults(result, policyViolations) {
816
1055
  // Calculate grade/score
817
1056
  // CRITICAL: 0/0 planned files = 'F' (Incomplete)
818
1057
  // Bloat automatically drops grade by at least one letter
@@ -898,6 +1137,19 @@ function displayVerifyResults(result) {
898
1137
  console.log('');
899
1138
  console.log(chalk.green('✅ No bloat detected - all changes match the plan'));
900
1139
  }
1140
+ // Display custom policy violations from dashboard
1141
+ if (policyViolations && policyViolations.length > 0) {
1142
+ console.log('');
1143
+ const blockCount = policyViolations.filter(v => v.severity === 'block').length;
1144
+ const label = blockCount > 0
1145
+ ? chalk.bold.red(`🚫 Custom Policy Violations: ${policyViolations.length} (${blockCount} blocking)`)
1146
+ : chalk.bold.yellow(`⚠️ Custom Policy Warnings: ${policyViolations.length}`);
1147
+ console.log(label);
1148
+ policyViolations.forEach(v => {
1149
+ const lineColor = v.severity === 'block' ? chalk.red : chalk.yellow;
1150
+ console.log(lineColor(` • ${v.file}: ${v.message || v.rule}`));
1151
+ });
1152
+ }
901
1153
  console.log('');
902
1154
  console.log('━'.repeat(50));
903
1155
  console.log(chalk.dim(result.message));