@neurcode-ai/cli 0.8.9 → 0.8.14

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,
@@ -201,12 +266,17 @@ async function verifyCommand(options) {
201
266
  }
202
267
  // Parse the diff
203
268
  const allDiffFiles = (0, diff_parser_1.parseDiff)(diffText);
269
+ console.log('DEBUG: Raw Diff Files:', JSON.stringify(allDiffFiles.map(f => f.path), null, 2));
204
270
  // Filter out internal/system files before analysis
205
271
  // This prevents self-interference where the tool flags its own files as bloat
206
272
  const diffFiles = allDiffFiles.filter(file => {
207
273
  // Check both path and oldPath (for renames) against exclusion list
208
274
  const excludePath = isExcludedFile(file.path);
209
275
  const excludeOldPath = file.oldPath ? isExcludedFile(file.oldPath) : false;
276
+ if (excludePath)
277
+ console.log(`DEBUG: System Excluded: ${file.path}`);
278
+ if (excludeOldPath)
279
+ console.log(`DEBUG: System Excluded (oldPath): ${file.oldPath}`);
210
280
  return !excludePath && !excludeOldPath;
211
281
  });
212
282
  const summary = (0, diff_parser_1.getDiffSummary)(diffFiles);
@@ -217,8 +287,10 @@ async function verifyCommand(options) {
217
287
  else {
218
288
  console.log(JSON.stringify({
219
289
  grade: 'F',
220
- adherenceScore: 0,
290
+ score: 0,
221
291
  verdict: 'FAIL',
292
+ violations: [],
293
+ adherenceScore: 0,
222
294
  bloatCount: 0,
223
295
  bloatFiles: [],
224
296
  plannedFilesModified: 0,
@@ -229,11 +301,99 @@ async function verifyCommand(options) {
229
301
  }
230
302
  process.exit(0);
231
303
  }
304
+ const ignoreFilter = (0, ignore_1.loadIgnore)(process.cwd());
232
305
  if (!options.json) {
233
306
  console.log(chalk.cyan('\n📊 Analyzing changes against plan...'));
234
307
  console.log(chalk.dim(` Found ${summary.totalFiles} file(s) changed`));
235
308
  console.log(chalk.dim(` ${summary.totalAdded} lines added, ${summary.totalRemoved} lines removed\n`));
236
309
  }
310
+ // ============================================
311
+ // --policy-only: General Governance (policy only, no plan enforcement)
312
+ // ============================================
313
+ if (options.policyOnly) {
314
+ if (!options.json) {
315
+ console.log(chalk.cyan('🛡️ General Governance mode (policy only, no plan linked)\n'));
316
+ }
317
+ let policyViolations = [];
318
+ let policyDecision = 'allow';
319
+ const defaultPolicy = (0, policy_engine_1.createDefaultPolicy)();
320
+ let allRules = [...defaultPolicy.rules];
321
+ if (config.apiKey) {
322
+ try {
323
+ const customPolicies = await client.getActiveCustomPolicies();
324
+ const customRules = [];
325
+ for (const p of customPolicies) {
326
+ const r = customPolicyToRule(p);
327
+ if (r)
328
+ customRules.push(r);
329
+ }
330
+ allRules = [...defaultPolicy.rules, ...customRules];
331
+ if (!options.json && customRules.length > 0) {
332
+ console.log(chalk.dim(` Evaluating ${customRules.length} custom policy rule(s) from dashboard`));
333
+ }
334
+ }
335
+ catch {
336
+ if (!options.json) {
337
+ console.log(chalk.dim(' Could not load custom policies, using default policy only'));
338
+ }
339
+ }
340
+ }
341
+ const diffFilesForPolicy = diffFiles.filter((f) => {
342
+ const ignored = ignoreFilter(f.path);
343
+ if (ignored)
344
+ console.log(`DEBUG: User Ignored: ${f.path}`);
345
+ return !ignored;
346
+ });
347
+ console.log('DEBUG: Files sending to Policy Engine:', JSON.stringify(diffFilesForPolicy.map(f => f.path), null, 2));
348
+ const policyResult = (0, policy_engine_1.evaluateRules)(diffFilesForPolicy, allRules);
349
+ policyViolations = (policyResult.violations || []);
350
+ policyViolations = policyViolations.filter((v) => !ignoreFilter(v.file));
351
+ policyDecision = policyViolations.length > 0 ? policyResult.decision : 'allow';
352
+ const effectiveVerdict = policyDecision === 'block' ? 'FAIL' : policyDecision === 'warn' ? 'WARN' : 'PASS';
353
+ const grade = effectiveVerdict === 'PASS' ? 'A' : effectiveVerdict === 'WARN' ? 'C' : 'F';
354
+ const score = effectiveVerdict === 'PASS' ? 100 : effectiveVerdict === 'WARN' ? 50 : 0;
355
+ const violationsOutput = policyViolations.map((v) => ({
356
+ file: v.file,
357
+ rule: v.rule,
358
+ severity: v.severity,
359
+ message: v.message,
360
+ ...(v.line != null ? { startLine: v.line } : {}),
361
+ }));
362
+ const message = effectiveVerdict === 'PASS'
363
+ ? '✅ Policy check passed (General Governance mode)'
364
+ : policyViolations.length > 0
365
+ ? `Policy violations: ${policyViolations.map((v) => `${v.file}: ${v.message || v.rule}`).join('; ')}`
366
+ : 'Policy check completed';
367
+ if (options.json) {
368
+ console.log(JSON.stringify({
369
+ grade,
370
+ score,
371
+ verdict: effectiveVerdict,
372
+ violations: violationsOutput,
373
+ message,
374
+ scopeGuardPassed: true, // N/A in policy-only mode
375
+ bloatCount: 0,
376
+ bloatFiles: [],
377
+ plannedFilesModified: 0,
378
+ totalPlannedFiles: 0,
379
+ adherenceScore: score,
380
+ policyOnly: true,
381
+ }, null, 2));
382
+ }
383
+ else {
384
+ if (effectiveVerdict === 'PASS') {
385
+ console.log(chalk.green('✅ Policy check passed'));
386
+ }
387
+ else {
388
+ console.log(chalk.red(`❌ Policy violations detected: ${policyViolations.length}`));
389
+ policyViolations.forEach((v) => {
390
+ console.log(chalk.red(` • ${v.file}: ${v.message || v.rule}`));
391
+ });
392
+ }
393
+ console.log(chalk.dim(`\n${message}`));
394
+ }
395
+ process.exit(effectiveVerdict === 'FAIL' ? 2 : effectiveVerdict === 'WARN' ? 1 : 0);
396
+ }
237
397
  // Get planId: Priority 1: options flag, Priority 2: state file (.neurcode/config.json), Priority 3: legacy config
238
398
  let planId = options.planId;
239
399
  if (!planId) {
@@ -278,8 +438,10 @@ async function verifyCommand(options) {
278
438
  else {
279
439
  console.log(JSON.stringify({
280
440
  grade: 'F',
281
- adherenceScore: 0,
441
+ score: 0,
282
442
  verdict: 'FAIL',
443
+ violations: [],
444
+ adherenceScore: 0,
283
445
  bloatCount: 0,
284
446
  bloatFiles: [],
285
447
  plannedFilesModified: 0,
@@ -353,19 +515,28 @@ async function verifyCommand(options) {
353
515
  // Step C: The Intersection Logic
354
516
  const approvedSet = new Set([...planFiles, ...allowedFiles]);
355
517
  const violations = modifiedFiles.filter(f => !approvedSet.has(f));
356
- // Step D: The Block
357
- if (violations.length > 0) {
518
+ const filteredViolations = violations.filter((p) => !ignoreFilter(p));
519
+ // Step D: The Block (only report scope violations for non-ignored files)
520
+ if (filteredViolations.length > 0) {
358
521
  if (options.json) {
359
- // Output JSON for scope violation BEFORE exit
522
+ // Output JSON for scope violation BEFORE exit. Must include violations for GitHub Action annotations.
523
+ const violationsOutput = filteredViolations.map((file) => ({
524
+ file,
525
+ rule: 'scope_guard',
526
+ severity: 'block',
527
+ message: 'File modified outside the plan',
528
+ }));
360
529
  const jsonOutput = {
361
530
  grade: 'F',
362
- adherenceScore: 0,
531
+ score: 0,
363
532
  verdict: 'FAIL',
364
- bloatCount: violations.length,
365
- bloatFiles: violations,
533
+ violations: violationsOutput,
534
+ adherenceScore: 0,
535
+ bloatCount: filteredViolations.length,
536
+ bloatFiles: filteredViolations,
366
537
  plannedFilesModified: 0,
367
538
  totalPlannedFiles: planFiles.length,
368
- message: `Scope violation: ${violations.length} file(s) modified outside the plan`,
539
+ message: `Scope violation: ${filteredViolations.length} file(s) modified outside the plan`,
369
540
  scopeGuardPassed: false,
370
541
  };
371
542
  // CRITICAL: Print JSON first, then exit
@@ -378,12 +549,12 @@ async function verifyCommand(options) {
378
549
  console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
379
550
  console.log(chalk.red('The following files were modified but are not in the plan:'));
380
551
  console.log('');
381
- violations.forEach(file => {
552
+ filteredViolations.forEach(file => {
382
553
  console.log(chalk.red(` • ${file}`));
383
554
  });
384
555
  console.log('');
385
556
  console.log(chalk.yellow('To unblock these files, run:'));
386
- violations.forEach(file => {
557
+ filteredViolations.forEach(file => {
387
558
  console.log(chalk.dim(` neurcode allow ${file}`));
388
559
  });
389
560
  console.log('');
@@ -424,8 +595,10 @@ async function verifyCommand(options) {
424
595
  else {
425
596
  console.log(JSON.stringify({
426
597
  grade: 'N/A',
427
- adherenceScore: 0,
598
+ score: 0,
428
599
  verdict: 'INFO',
600
+ violations: [],
601
+ adherenceScore: 0,
429
602
  bloatCount: 0,
430
603
  bloatFiles: [],
431
604
  plannedFilesModified: 0,
@@ -437,6 +610,39 @@ async function verifyCommand(options) {
437
610
  }
438
611
  process.exit(0);
439
612
  }
613
+ let policyViolations = [];
614
+ let policyDecision = 'allow';
615
+ if (config.apiKey) {
616
+ try {
617
+ const customPolicies = await client.getActiveCustomPolicies();
618
+ const defaultPolicy = (0, policy_engine_1.createDefaultPolicy)();
619
+ const customRules = [];
620
+ for (const p of customPolicies) {
621
+ const r = customPolicyToRule(p);
622
+ if (r)
623
+ customRules.push(r);
624
+ }
625
+ const allRules = [...defaultPolicy.rules, ...customRules];
626
+ const diffFilesForPolicy = diffFiles.filter((f) => {
627
+ const ignored = ignoreFilter(f.path);
628
+ if (ignored)
629
+ console.log(`DEBUG: User Ignored: ${f.path}`);
630
+ return !ignored;
631
+ });
632
+ console.log('DEBUG: Files sending to Policy Engine:', JSON.stringify(diffFilesForPolicy.map(f => f.path), null, 2));
633
+ const policyResult = (0, policy_engine_1.evaluateRules)(diffFilesForPolicy, allRules);
634
+ policyViolations = policyResult.violations.filter((v) => !ignoreFilter(v.file));
635
+ policyDecision = policyViolations.length > 0 ? policyResult.decision : 'allow';
636
+ if (!options.json && customRules.length > 0) {
637
+ console.log(chalk.dim(` Evaluating ${customRules.length} custom policy rule(s) from dashboard`));
638
+ }
639
+ }
640
+ catch {
641
+ if (!options.json) {
642
+ console.log(chalk.dim(' Could not load custom policies, continuing without them'));
643
+ }
644
+ }
645
+ }
440
646
  // Prepare diff stats and changed files for API
441
647
  const diffStats = {
442
648
  totalAdded: summary.totalAdded,
@@ -478,7 +684,13 @@ async function verifyCommand(options) {
478
684
  }
479
685
  // Call verifyPlan with intentConstraints
480
686
  const verifyResult = await client.verifyPlan(finalPlanId, diffStats, changedFiles, projectId, intentConstraints);
481
- // Calculate grade from verdict and score
687
+ // Apply custom policy verdict: block from dashboard overrides API verdict
688
+ const policyBlock = policyDecision === 'block' && policyViolations.length > 0;
689
+ const effectiveVerdict = policyBlock ? 'FAIL' : verifyResult.verdict;
690
+ const effectiveMessage = policyBlock
691
+ ? `Custom policy violations: ${policyViolations.map(v => `${v.file}: ${v.message || v.rule}`).join('; ')}. ${verifyResult.message}`
692
+ : verifyResult.message;
693
+ // Calculate grade from effective verdict and score
482
694
  // CRITICAL: 0/0 planned files = 'F' (Incomplete), not 'B'
483
695
  // Bloat automatically drops grade by at least one letter
484
696
  let grade;
@@ -486,7 +698,7 @@ async function verifyCommand(options) {
486
698
  if (verifyResult.totalPlannedFiles === 0 && verifyResult.plannedFilesModified === 0) {
487
699
  grade = 'F';
488
700
  }
489
- else if (verifyResult.verdict === 'PASS') {
701
+ else if (effectiveVerdict === 'PASS') {
490
702
  grade = 'A';
491
703
  // Log ROI event for PASS verification (Grade A) - non-blocking
492
704
  try {
@@ -503,7 +715,7 @@ async function verifyCommand(options) {
503
715
  // Silently ignore - ROI logging should never block user workflows
504
716
  }
505
717
  }
506
- else if (verifyResult.verdict === 'WARN') {
718
+ else if (effectiveVerdict === 'WARN') {
507
719
  // Base grade calculation
508
720
  let baseGrade = verifyResult.adherenceScore >= 70 ? 'B' : verifyResult.adherenceScore >= 50 ? 'C' : 'D';
509
721
  // Bloat drops grade by one letter (B -> C, C -> D, D -> F)
@@ -522,49 +734,82 @@ async function verifyCommand(options) {
522
734
  }
523
735
  // If JSON output requested, output JSON and exit
524
736
  if (options.json) {
737
+ const filteredBloatFiles = (verifyResult.bloatFiles || []).filter((f) => !ignoreFilter(f));
738
+ const scopeViolations = filteredBloatFiles.map((file) => ({
739
+ file,
740
+ rule: 'scope_guard',
741
+ severity: 'block',
742
+ message: 'File modified outside the plan',
743
+ }));
744
+ const policyViolationItems = policyViolations.map((v) => ({
745
+ file: v.file,
746
+ rule: v.rule,
747
+ severity: v.severity,
748
+ message: v.message,
749
+ ...(v.line != null ? { startLine: v.line } : {}),
750
+ }));
751
+ const violations = [...scopeViolations, ...policyViolationItems];
525
752
  const jsonOutput = {
526
753
  grade,
754
+ score: verifyResult.adherenceScore,
755
+ verdict: effectiveVerdict,
756
+ violations,
757
+ message: effectiveMessage,
527
758
  adherenceScore: verifyResult.adherenceScore,
528
- verdict: verifyResult.verdict,
529
- bloatCount: verifyResult.bloatCount,
530
- bloatFiles: verifyResult.bloatFiles,
759
+ scopeGuardPassed,
760
+ bloatCount: filteredBloatFiles.length,
761
+ bloatFiles: filteredBloatFiles,
531
762
  plannedFilesModified: verifyResult.plannedFilesModified,
532
763
  totalPlannedFiles: verifyResult.totalPlannedFiles,
533
- message: verifyResult.message,
534
- scopeGuardPassed,
764
+ ...(policyViolations.length > 0 && { policyDecision }),
535
765
  };
536
766
  console.log(JSON.stringify(jsonOutput, null, 2));
537
767
  // Report to Neurcode Cloud if --record flag is set (after JSON output)
538
768
  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
- }));
769
+ const violations = [
770
+ ...filteredBloatFiles.map((file) => ({
771
+ rule: 'scope_guard',
772
+ file: file,
773
+ severity: 'block',
774
+ message: 'File modified outside the plan',
775
+ })),
776
+ ...policyViolations.map(v => ({
777
+ rule: v.rule,
778
+ file: v.file,
779
+ severity: v.severity,
780
+ message: v.message,
781
+ })),
782
+ ];
545
783
  // Report in background (don't await to avoid blocking JSON output)
546
784
  reportVerification(grade, violations, verifyResult, config.apiKey, config.apiUrl, projectId || undefined, true // jsonMode = true
547
785
  ).catch(() => {
548
786
  // Error already logged in reportVerification
549
787
  });
550
788
  }
551
- // Exit based on verdict (same logic as below)
552
- if (scopeGuardPassed) {
789
+ // Exit based on effective verdict (same logic as below)
790
+ if (scopeGuardPassed && !policyBlock) {
553
791
  process.exit(0);
554
792
  }
555
- if (verifyResult.verdict === 'FAIL') {
793
+ if (effectiveVerdict === 'FAIL') {
556
794
  process.exit(2);
557
795
  }
558
- else if (verifyResult.verdict === 'WARN') {
796
+ else if (effectiveVerdict === 'WARN') {
559
797
  process.exit(1);
560
798
  }
561
799
  else {
562
800
  process.exit(0);
563
801
  }
564
802
  }
565
- // Display results (only if not in json mode)
803
+ // Display results (only if not in json mode; exclude ignored paths from bloat)
566
804
  if (!options.json) {
567
- displayVerifyResults(verifyResult);
805
+ const displayBloatFiles = (verifyResult.bloatFiles || []).filter((f) => !ignoreFilter(f));
806
+ displayVerifyResults({
807
+ ...verifyResult,
808
+ verdict: effectiveVerdict,
809
+ message: effectiveMessage,
810
+ bloatFiles: displayBloatFiles,
811
+ bloatCount: displayBloatFiles.length,
812
+ }, policyViolations);
568
813
  }
569
814
  // Report to Neurcode Cloud if --record flag is set
570
815
  if (options.record) {
@@ -575,23 +820,30 @@ async function verifyCommand(options) {
575
820
  }
576
821
  }
577
822
  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
- }));
823
+ // Include scope bloat and custom policy violations in the report (excluding .neurcodeignore'd paths)
824
+ const filteredBloatForReport = (verifyResult.bloatFiles || []).filter((f) => !ignoreFilter(f));
825
+ const violations = [
826
+ ...filteredBloatForReport.map((file) => ({
827
+ rule: 'scope_guard',
828
+ file: file,
829
+ severity: 'block',
830
+ message: 'File modified outside the plan',
831
+ })),
832
+ ...policyViolations.map(v => ({
833
+ rule: v.rule,
834
+ file: v.file,
835
+ severity: v.severity,
836
+ message: v.message,
837
+ })),
838
+ ];
587
839
  await reportVerification(grade, violations, verifyResult, config.apiKey, config.apiUrl, projectId || undefined, false // jsonMode = false
588
840
  );
589
841
  }
590
842
  }
591
843
  // 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') {
844
+ // If Scope Guard passed (all files approved or allowed) and no policy block, always PASS
845
+ if (scopeGuardPassed && !policyBlock) {
846
+ if ((verifyResult.verdict === 'FAIL' || verifyResult.verdict === 'WARN') && policyViolations.length === 0) {
595
847
  if (!options.json) {
596
848
  console.log(chalk.yellow('\n⚠️ Plan deviation allowed'));
597
849
  console.log(chalk.dim(' Some files were modified outside the plan, but they were explicitly allowed.'));
@@ -600,12 +852,12 @@ async function verifyCommand(options) {
600
852
  }
601
853
  process.exit(0);
602
854
  }
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') {
855
+ // If scope guard didn't pass (or failed to check) or policy blocked, use effective verdict
856
+ // Exit with appropriate code based on AI verification and custom policies
857
+ if (effectiveVerdict === 'FAIL') {
606
858
  process.exit(2);
607
859
  }
608
- else if (verifyResult.verdict === 'WARN') {
860
+ else if (effectiveVerdict === 'WARN') {
609
861
  process.exit(1);
610
862
  }
611
863
  else {
@@ -617,8 +869,10 @@ async function verifyCommand(options) {
617
869
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
618
870
  console.log(JSON.stringify({
619
871
  grade: 'F',
620
- adherenceScore: 0,
872
+ score: 0,
621
873
  verdict: 'FAIL',
874
+ violations: [],
875
+ adherenceScore: 0,
622
876
  bloatCount: 0,
623
877
  bloatFiles: [],
624
878
  plannedFilesModified: 0,
@@ -650,8 +904,10 @@ async function verifyCommand(options) {
650
904
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
651
905
  console.log(JSON.stringify({
652
906
  grade: 'F',
653
- adherenceScore: 0,
907
+ score: 0,
654
908
  verdict: 'FAIL',
909
+ violations: [],
910
+ adherenceScore: 0,
655
911
  bloatCount: 0,
656
912
  bloatFiles: [],
657
913
  plannedFilesModified: 0,
@@ -812,7 +1068,7 @@ async function reportVerification(grade, violations, verifyResult, apiKey, apiUr
812
1068
  /**
813
1069
  * Display verification results in a formatted report card
814
1070
  */
815
- function displayVerifyResults(result) {
1071
+ function displayVerifyResults(result, policyViolations) {
816
1072
  // Calculate grade/score
817
1073
  // CRITICAL: 0/0 planned files = 'F' (Incomplete)
818
1074
  // Bloat automatically drops grade by at least one letter
@@ -898,6 +1154,19 @@ function displayVerifyResults(result) {
898
1154
  console.log('');
899
1155
  console.log(chalk.green('✅ No bloat detected - all changes match the plan'));
900
1156
  }
1157
+ // Display custom policy violations from dashboard
1158
+ if (policyViolations && policyViolations.length > 0) {
1159
+ console.log('');
1160
+ const blockCount = policyViolations.filter(v => v.severity === 'block').length;
1161
+ const label = blockCount > 0
1162
+ ? chalk.bold.red(`🚫 Custom Policy Violations: ${policyViolations.length} (${blockCount} blocking)`)
1163
+ : chalk.bold.yellow(`⚠️ Custom Policy Warnings: ${policyViolations.length}`);
1164
+ console.log(label);
1165
+ policyViolations.forEach(v => {
1166
+ const lineColor = v.severity === 'block' ? chalk.red : chalk.yellow;
1167
+ console.log(lineColor(` • ${v.file}: ${v.message || v.rule}`));
1168
+ });
1169
+ }
901
1170
  console.log('');
902
1171
  console.log('━'.repeat(50));
903
1172
  console.log(chalk.dim(result.message));