@neurcode-ai/cli 0.9.35 → 0.9.36

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.
@@ -63,6 +63,16 @@ function resolveActor(explicit) {
63
63
  return explicit.trim();
64
64
  return process.env.NEURCODE_ACTOR || process.env.GITHUB_ACTOR || process.env.USER || 'unknown';
65
65
  }
66
+ function validateExceptionWindowByGovernance(expiresAt, maxExpiryDays) {
67
+ const expiryMs = Date.parse(expiresAt);
68
+ if (!Number.isFinite(expiryMs)) {
69
+ throw new Error('expiresAt must be a valid ISO datetime');
70
+ }
71
+ const maxWindowMs = Math.max(1, maxExpiryDays) * 24 * 60 * 60 * 1000;
72
+ if (expiryMs - Date.now() > maxWindowMs) {
73
+ throw new Error(`exception expiry exceeds governance max window (${maxExpiryDays} days)`);
74
+ }
75
+ }
66
76
  async function resolveCustomPolicies(client, includeDashboardPolicies, requireDashboardPolicies) {
67
77
  if (!includeDashboardPolicies) {
68
78
  return {
@@ -153,7 +163,7 @@ function policyCommand(program) {
153
163
  policy
154
164
  .command('install')
155
165
  .description('Install a policy pack for this repository')
156
- .argument('<pack-id>', 'Policy pack ID (fintech|hipaa|soc2|startup-fast)')
166
+ .argument('<pack-id>', 'Policy pack ID (run `neurcode policy list` for all available stacks)')
157
167
  .option('--force', 'Replace any existing installed policy pack')
158
168
  .option('--json', 'Output as JSON')
159
169
  .action((packId, options) => {
@@ -466,8 +476,61 @@ function policyCommand(program) {
466
476
  governance
467
477
  .command('status')
468
478
  .description('Show policy governance settings for this repository')
479
+ .option('--org', 'Fetch centralized organization governance settings from Neurcode Cloud')
469
480
  .option('--json', 'Output as JSON')
470
- .action((options) => {
481
+ .action(async (options) => {
482
+ if (options.org) {
483
+ try {
484
+ const config = loadPolicyRuntimeConfig();
485
+ const client = new api_client_1.ApiClient(config);
486
+ const settings = await client.getOrgGovernanceSettings();
487
+ if (!settings) {
488
+ throw new Error('Organization governance settings not found');
489
+ }
490
+ const policyGovernance = settings.policyGovernance || null;
491
+ if (options.json) {
492
+ console.log(JSON.stringify({
493
+ source: 'org',
494
+ settings,
495
+ }, null, 2));
496
+ return;
497
+ }
498
+ console.log(chalk.bold('\n🏢 Org Policy Governance\n'));
499
+ console.log(chalk.dim('Source: Neurcode Cloud (/api/v1/org/governance/settings)'));
500
+ if (!policyGovernance) {
501
+ console.log(chalk.yellow('No org-level policy governance configured.\n'));
502
+ return;
503
+ }
504
+ console.log(chalk.dim(`Exception approvals required: ${policyGovernance.exceptionApprovals?.required ? 'yes' : 'no'}`));
505
+ console.log(chalk.dim(`Minimum approvals: ${policyGovernance.exceptionApprovals?.minApprovals ?? 1}`));
506
+ console.log(chalk.dim(`Disallow self approval: ${policyGovernance.exceptionApprovals?.disallowSelfApproval !== false ? 'yes' : 'no'}`));
507
+ console.log(chalk.dim(`Reason required: ${policyGovernance.exceptionApprovals?.requireReason !== false ? 'yes' : 'no'}`));
508
+ console.log(chalk.dim(`Minimum reason length: ${policyGovernance.exceptionApprovals?.minReasonLength ?? 12}`));
509
+ console.log(chalk.dim(`Maximum exception window (days): ${policyGovernance.exceptionApprovals?.maxExpiryDays ?? 30}`));
510
+ console.log(chalk.dim(`Allowed approvers: ${Array.isArray(policyGovernance.exceptionApprovals?.allowedApprovers)
511
+ && policyGovernance.exceptionApprovals.allowedApprovers.length > 0
512
+ ? policyGovernance.exceptionApprovals.allowedApprovers.join(', ')
513
+ : '(any)'}`));
514
+ console.log(chalk.dim(`Critical rule patterns: ${Array.isArray(policyGovernance.exceptionApprovals?.criticalRulePatterns)
515
+ && policyGovernance.exceptionApprovals.criticalRulePatterns.length > 0
516
+ ? policyGovernance.exceptionApprovals.criticalRulePatterns.join(', ')
517
+ : '(none)'}`));
518
+ console.log(chalk.dim(`Critical minimum approvals: ${policyGovernance.exceptionApprovals?.criticalMinApprovals ?? 2}`));
519
+ console.log(chalk.dim(`Require audit integrity: ${policyGovernance.audit?.requireIntegrity ? 'yes' : 'no'}`));
520
+ console.log(chalk.dim(`Updated at: ${settings.updatedAt || '(unknown)'}`));
521
+ console.log('');
522
+ return;
523
+ }
524
+ catch (error) {
525
+ const message = error instanceof Error ? error.message : 'Unknown error';
526
+ if (options.json) {
527
+ console.log(JSON.stringify({ error: message }, null, 2));
528
+ process.exit(1);
529
+ }
530
+ console.error(chalk.red(`\n❌ ${message}\n`));
531
+ process.exit(1);
532
+ }
533
+ }
471
534
  const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
472
535
  const config = (0, policy_governance_1.readPolicyGovernanceConfig)(cwd);
473
536
  if (options.json) {
@@ -482,37 +545,155 @@ function policyCommand(program) {
482
545
  console.log(chalk.dim(`Exception approvals required: ${config.exceptionApprovals.required ? 'yes' : 'no'}`));
483
546
  console.log(chalk.dim(`Minimum approvals: ${config.exceptionApprovals.minApprovals}`));
484
547
  console.log(chalk.dim(`Disallow self approval: ${config.exceptionApprovals.disallowSelfApproval ? 'yes' : 'no'}`));
548
+ console.log(chalk.dim(`Reason required: ${config.exceptionApprovals.requireReason ? 'yes' : 'no'}`));
549
+ console.log(chalk.dim(`Minimum reason length: ${config.exceptionApprovals.minReasonLength}`));
550
+ console.log(chalk.dim(`Maximum exception window (days): ${config.exceptionApprovals.maxExpiryDays}`));
485
551
  console.log(chalk.dim(`Allowed approvers: ${config.exceptionApprovals.allowedApprovers.length > 0 ? config.exceptionApprovals.allowedApprovers.join(', ') : '(any)'}`));
552
+ console.log(chalk.dim(`Critical rule patterns: ${config.exceptionApprovals.criticalRulePatterns.length > 0 ? config.exceptionApprovals.criticalRulePatterns.join(', ') : '(none)'}`));
553
+ console.log(chalk.dim(`Critical minimum approvals: ${config.exceptionApprovals.criticalMinApprovals}`));
486
554
  console.log(chalk.dim(`Require audit integrity: ${config.audit.requireIntegrity ? 'yes' : 'no'}`));
487
555
  console.log('');
488
556
  });
489
557
  governance
490
558
  .command('set')
491
559
  .description('Update policy governance settings')
560
+ .option('--org', 'Update centralized organization governance settings in Neurcode Cloud')
492
561
  .option('--require-approval', 'Require approvals before exceptions become effective')
493
562
  .option('--no-require-approval', 'Do not require approvals for exceptions')
494
563
  .option('--min-approvals <n>', 'Minimum approvals required when approval mode is enabled', (value) => parseInt(value, 10))
495
564
  .option('--allow-self-approval', 'Allow requester to approve their own exception')
496
565
  .option('--restrict-approvers <csv>', 'Comma-separated allow-list of approver identities')
497
566
  .option('--clear-approvers', 'Clear approver allow-list (allow any approver)')
567
+ .option('--require-reason', 'Require non-trivial exception reason text')
568
+ .option('--no-require-reason', 'Do not enforce minimum reason text length')
569
+ .option('--min-reason-length <n>', 'Minimum exception reason length (default: 12)', (value) => parseInt(value, 10))
570
+ .option('--max-expiry-days <n>', 'Maximum exception expiry window in days (default: 30)', (value) => parseInt(value, 10))
571
+ .option('--critical-rules <csv>', 'Comma-separated critical rule patterns requiring elevated approvals')
572
+ .option('--clear-critical-rules', 'Clear critical rule patterns')
573
+ .option('--critical-min-approvals <n>', 'Minimum approvals for critical rule exceptions (default: 2)', (value) => parseInt(value, 10))
498
574
  .option('--require-audit-integrity', 'Fail verify if policy audit chain integrity is broken')
499
575
  .option('--no-require-audit-integrity', 'Do not enforce policy audit integrity in verify')
500
576
  .option('--json', 'Output as JSON')
501
- .action((options) => {
577
+ .action(async (options) => {
578
+ const hasRestrictApprovers = typeof options.restrictApprovers === 'string';
579
+ const hasCriticalRules = typeof options.criticalRules === 'string';
580
+ const parsedApprovers = options.clearApprovers
581
+ ? []
582
+ : hasRestrictApprovers
583
+ ? options.restrictApprovers
584
+ .split(',')
585
+ .map((item) => item.trim())
586
+ .filter(Boolean)
587
+ : undefined;
588
+ const parsedCriticalRules = options.clearCriticalRules
589
+ ? []
590
+ : hasCriticalRules
591
+ ? options.criticalRules
592
+ .split(',')
593
+ .map((item) => item.trim())
594
+ .filter(Boolean)
595
+ : undefined;
596
+ if (options.org) {
597
+ try {
598
+ const exceptionApprovalsPatch = {};
599
+ if (typeof options.requireApproval === 'boolean') {
600
+ exceptionApprovalsPatch.required = options.requireApproval;
601
+ }
602
+ if (Number.isFinite(options.minApprovals)) {
603
+ exceptionApprovalsPatch.minApprovals = options.minApprovals;
604
+ }
605
+ if (typeof options.allowSelfApproval === 'boolean') {
606
+ exceptionApprovalsPatch.disallowSelfApproval = !options.allowSelfApproval;
607
+ }
608
+ if (parsedApprovers) {
609
+ exceptionApprovalsPatch.allowedApprovers = parsedApprovers;
610
+ }
611
+ if (typeof options.requireReason === 'boolean') {
612
+ exceptionApprovalsPatch.requireReason = options.requireReason;
613
+ }
614
+ if (Number.isFinite(options.minReasonLength)) {
615
+ exceptionApprovalsPatch.minReasonLength = options.minReasonLength;
616
+ }
617
+ if (Number.isFinite(options.maxExpiryDays)) {
618
+ exceptionApprovalsPatch.maxExpiryDays = options.maxExpiryDays;
619
+ }
620
+ if (parsedCriticalRules) {
621
+ exceptionApprovalsPatch.criticalRulePatterns = parsedCriticalRules;
622
+ }
623
+ if (Number.isFinite(options.criticalMinApprovals)) {
624
+ exceptionApprovalsPatch.criticalMinApprovals = options.criticalMinApprovals;
625
+ }
626
+ const auditPatch = {};
627
+ if (typeof options.requireAuditIntegrity === 'boolean') {
628
+ auditPatch.requireIntegrity = options.requireAuditIntegrity;
629
+ }
630
+ const policyGovernancePatch = {};
631
+ if (Object.keys(exceptionApprovalsPatch).length > 0) {
632
+ policyGovernancePatch.exceptionApprovals = exceptionApprovalsPatch;
633
+ }
634
+ if (Object.keys(auditPatch).length > 0) {
635
+ policyGovernancePatch.audit = auditPatch;
636
+ }
637
+ const config = loadPolicyRuntimeConfig();
638
+ const client = new api_client_1.ApiClient(config);
639
+ const settings = await client.updateOrgGovernanceSettings({
640
+ policyGovernance: policyGovernancePatch,
641
+ });
642
+ if (!settings) {
643
+ throw new Error('Failed to update org governance settings');
644
+ }
645
+ const next = settings.policyGovernance;
646
+ if (options.json) {
647
+ console.log(JSON.stringify({
648
+ source: 'org',
649
+ settings,
650
+ }, null, 2));
651
+ return;
652
+ }
653
+ console.log(chalk.green('\n✅ Organization policy governance updated.\n'));
654
+ console.log(chalk.dim('Source: Neurcode Cloud (/api/v1/org/governance/settings)'));
655
+ if (!next) {
656
+ console.log(chalk.yellow('No org-level policy governance payload returned.\n'));
657
+ return;
658
+ }
659
+ console.log(chalk.dim(`Approval required: ${next.exceptionApprovals?.required ? 'yes' : 'no'}`));
660
+ console.log(chalk.dim(`Min approvals: ${next.exceptionApprovals?.minApprovals ?? 1}`));
661
+ console.log(chalk.dim(`Disallow self approval: ${next.exceptionApprovals?.disallowSelfApproval !== false ? 'yes' : 'no'}`));
662
+ console.log(chalk.dim(`Reason required: ${next.exceptionApprovals?.requireReason !== false ? 'yes' : 'no'}`));
663
+ console.log(chalk.dim(`Min reason length: ${next.exceptionApprovals?.minReasonLength ?? 12}`));
664
+ console.log(chalk.dim(`Max expiry days: ${next.exceptionApprovals?.maxExpiryDays ?? 30}`));
665
+ console.log(chalk.dim(`Critical min approvals: ${next.exceptionApprovals?.criticalMinApprovals ?? 2}`));
666
+ console.log(chalk.dim(`Critical rule patterns: ${Array.isArray(next.exceptionApprovals?.criticalRulePatterns)
667
+ && next.exceptionApprovals.criticalRulePatterns.length > 0
668
+ ? next.exceptionApprovals.criticalRulePatterns.join(', ')
669
+ : '(none)'}`));
670
+ console.log(chalk.dim(`Require audit integrity: ${next.audit?.requireIntegrity ? 'yes' : 'no'}`));
671
+ console.log(chalk.dim(`Updated at: ${settings.updatedAt || '(unknown)'}`));
672
+ console.log('');
673
+ return;
674
+ }
675
+ catch (error) {
676
+ const message = error instanceof Error ? error.message : 'Unknown error';
677
+ if (options.json) {
678
+ console.log(JSON.stringify({ error: message }, null, 2));
679
+ process.exit(1);
680
+ }
681
+ console.error(chalk.red(`\n❌ ${message}\n`));
682
+ process.exit(1);
683
+ }
684
+ }
502
685
  const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
503
686
  try {
504
687
  const next = (0, policy_governance_1.updatePolicyGovernanceConfig)(cwd, {
505
688
  required: typeof options.requireApproval === 'boolean' ? options.requireApproval : undefined,
506
689
  minApprovals: Number.isFinite(options.minApprovals) ? options.minApprovals : undefined,
507
690
  disallowSelfApproval: typeof options.allowSelfApproval === 'boolean' ? !options.allowSelfApproval : undefined,
508
- allowedApprovers: options.clearApprovers
509
- ? []
510
- : typeof options.restrictApprovers === 'string'
511
- ? options.restrictApprovers
512
- .split(',')
513
- .map((item) => item.trim())
514
- .filter(Boolean)
515
- : undefined,
691
+ allowedApprovers: parsedApprovers,
692
+ requireReason: typeof options.requireReason === 'boolean' ? options.requireReason : undefined,
693
+ minReasonLength: Number.isFinite(options.minReasonLength) ? options.minReasonLength : undefined,
694
+ maxExpiryDays: Number.isFinite(options.maxExpiryDays) ? options.maxExpiryDays : undefined,
695
+ criticalRulePatterns: parsedCriticalRules,
696
+ criticalMinApprovals: Number.isFinite(options.criticalMinApprovals) ? options.criticalMinApprovals : undefined,
516
697
  requireAuditIntegrity: typeof options.requireAuditIntegrity === 'boolean' ? options.requireAuditIntegrity : undefined,
517
698
  });
518
699
  try {
@@ -526,6 +707,11 @@ function policyCommand(program) {
526
707
  minApprovals: next.exceptionApprovals.minApprovals,
527
708
  disallowSelfApproval: next.exceptionApprovals.disallowSelfApproval,
528
709
  allowedApprovers: next.exceptionApprovals.allowedApprovers,
710
+ requireReason: next.exceptionApprovals.requireReason,
711
+ minReasonLength: next.exceptionApprovals.minReasonLength,
712
+ maxExpiryDays: next.exceptionApprovals.maxExpiryDays,
713
+ criticalRulePatterns: next.exceptionApprovals.criticalRulePatterns,
714
+ criticalMinApprovals: next.exceptionApprovals.criticalMinApprovals,
529
715
  requireAuditIntegrity: next.audit.requireIntegrity,
530
716
  },
531
717
  });
@@ -542,6 +728,11 @@ function policyCommand(program) {
542
728
  console.log(chalk.dim(`Approval required: ${next.exceptionApprovals.required ? 'yes' : 'no'}`));
543
729
  console.log(chalk.dim(`Min approvals: ${next.exceptionApprovals.minApprovals}`));
544
730
  console.log(chalk.dim(`Disallow self approval: ${next.exceptionApprovals.disallowSelfApproval ? 'yes' : 'no'}`));
731
+ console.log(chalk.dim(`Reason required: ${next.exceptionApprovals.requireReason ? 'yes' : 'no'}`));
732
+ console.log(chalk.dim(`Min reason length: ${next.exceptionApprovals.minReasonLength}`));
733
+ console.log(chalk.dim(`Max expiry days: ${next.exceptionApprovals.maxExpiryDays}`));
734
+ console.log(chalk.dim(`Critical min approvals: ${next.exceptionApprovals.criticalMinApprovals}`));
735
+ console.log(chalk.dim(`Critical rule patterns: ${next.exceptionApprovals.criticalRulePatterns.length > 0 ? next.exceptionApprovals.criticalRulePatterns.join(', ') : '(none)'}`));
545
736
  console.log(chalk.dim(`Require audit integrity: ${next.audit.requireIntegrity ? 'yes' : 'no'}`));
546
737
  console.log(chalk.dim('Commit governance + audit files so CI can enforce approval and integrity rules.'));
547
738
  console.log('');
@@ -609,17 +800,27 @@ function policyCommand(program) {
609
800
  if (allowedApprovers.size > 0) {
610
801
  effectiveApprovals = effectiveApprovals.filter((item) => allowedApprovers.has(item.approver.toLowerCase()));
611
802
  }
803
+ const requiredApprovalResolution = (0, policy_governance_1.resolveRequiredApprovalsForRule)(entry.rulePattern, governance);
804
+ const requiredApprovals = governance.exceptionApprovals.required
805
+ ? requiredApprovalResolution.requiredApprovals
806
+ : 0;
807
+ const reasonValid = !governance.exceptionApprovals.requireReason
808
+ || (entry.reason || '').trim().length >= governance.exceptionApprovals.minReasonLength;
612
809
  const status = !entry.active || !unexpired
613
810
  ? 'inactive'
614
- : !governance.exceptionApprovals.required
615
- ? 'active'
616
- : effectiveApprovals.length >= governance.exceptionApprovals.minApprovals
617
- ? 'approved'
618
- : 'pending';
811
+ : !reasonValid
812
+ ? 'invalid_reason'
813
+ : !governance.exceptionApprovals.required
814
+ ? 'active'
815
+ : effectiveApprovals.length >= requiredApprovals
816
+ ? 'approved'
817
+ : 'pending';
619
818
  return {
620
819
  ...entry,
621
820
  status,
622
821
  effectiveApprovals: effectiveApprovals.length,
822
+ requiredApprovals,
823
+ criticalRule: requiredApprovalResolution.critical,
623
824
  };
624
825
  });
625
826
  const items = options.all ? withStatus : withStatus.filter((entry) => entry.status !== 'inactive');
@@ -645,7 +846,10 @@ function policyCommand(program) {
645
846
  console.log(chalk.dim(` status=${entry.status || (entry.active ? 'active' : 'inactive')}`));
646
847
  console.log(chalk.dim(` rule=${entry.rulePattern} file=${entry.filePattern}`));
647
848
  console.log(chalk.dim(` expires=${entry.expiresAt} active=${entry.active ? 'yes' : 'no'}`));
648
- console.log(chalk.dim(` approvals=${entry.approvals.length}${typeof entry.effectiveApprovals === 'number' ? ` (effective=${entry.effectiveApprovals})` : ''}`));
849
+ console.log(chalk.dim(` approvals=${entry.approvals.length}` +
850
+ `${typeof entry.effectiveApprovals === 'number' ? ` (effective=${entry.effectiveApprovals})` : ''}` +
851
+ `${typeof entry.requiredApprovals === 'number' && entry.requiredApprovals > 0 ? ` required=${entry.requiredApprovals}` : ''}` +
852
+ `${entry.criticalRule ? ' critical=yes' : ''}`));
649
853
  console.log(chalk.dim(` reason=${entry.reason}`));
650
854
  console.log(chalk.dim(` requestedBy=${entry.requestedBy || entry.createdBy || 'unknown'}`));
651
855
  if (entry.ticket) {
@@ -682,6 +886,12 @@ function policyCommand(program) {
682
886
  expiresAt: options.expiresAt,
683
887
  expiresInDays: options.expiresInDays,
684
888
  });
889
+ const governance = (0, policy_governance_1.readPolicyGovernanceConfig)(cwd);
890
+ if (governance.exceptionApprovals.requireReason
891
+ && options.reason.trim().length < governance.exceptionApprovals.minReasonLength) {
892
+ throw new Error(`reason must be at least ${governance.exceptionApprovals.minReasonLength} characters (governance policy)`);
893
+ }
894
+ validateExceptionWindowByGovernance(expiresAt, governance.exceptionApprovals.maxExpiryDays);
685
895
  const createdBy = resolveActor();
686
896
  const created = (0, policy_exceptions_1.addPolicyException)(cwd, {
687
897
  rulePattern: options.rule,
@@ -693,7 +903,7 @@ function policyCommand(program) {
693
903
  createdBy,
694
904
  requestedBy: createdBy,
695
905
  });
696
- const governance = (0, policy_governance_1.readPolicyGovernanceConfig)(cwd);
906
+ const approvalResolution = (0, policy_governance_1.resolveRequiredApprovalsForRule)(created.rulePattern, governance);
697
907
  try {
698
908
  (0, policy_audit_1.appendPolicyAuditEvent)(cwd, {
699
909
  actor: createdBy,
@@ -705,6 +915,10 @@ function policyCommand(program) {
705
915
  filePattern: created.filePattern,
706
916
  expiresAt: created.expiresAt,
707
917
  requireApproval: governance.exceptionApprovals.required,
918
+ requiredApprovals: governance.exceptionApprovals.required
919
+ ? approvalResolution.requiredApprovals
920
+ : 0,
921
+ criticalRule: approvalResolution.critical,
708
922
  },
709
923
  });
710
924
  }
@@ -716,6 +930,10 @@ function policyCommand(program) {
716
930
  created,
717
931
  path: (0, policy_exceptions_1.getPolicyExceptionsPath)(cwd),
718
932
  requiresApproval: governance.exceptionApprovals.required,
933
+ requiredApprovals: governance.exceptionApprovals.required
934
+ ? approvalResolution.requiredApprovals
935
+ : 0,
936
+ criticalRule: approvalResolution.critical,
719
937
  }, null, 2));
720
938
  return;
721
939
  }
@@ -726,7 +944,8 @@ function policyCommand(program) {
726
944
  console.log(chalk.dim(`Expires: ${created.expiresAt}`));
727
945
  console.log(chalk.dim(`Reason: ${created.reason}`));
728
946
  if (governance.exceptionApprovals.required) {
729
- console.log(chalk.yellow(`Approval required: ${governance.exceptionApprovals.minApprovals} approver(s) before this exception is active.`));
947
+ const requiredApprovals = approvalResolution.requiredApprovals;
948
+ console.log(chalk.yellow(`Approval required: ${requiredApprovals} approver(s) before this exception is active${approvalResolution.critical ? ' (critical rule gate)' : ''}.`));
730
949
  }
731
950
  if (created.ticket) {
732
951
  console.log(chalk.dim(`Ticket: ${created.ticket}`));
@@ -754,6 +973,25 @@ function policyCommand(program) {
754
973
  const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
755
974
  const approver = resolveActor(options.by);
756
975
  try {
976
+ const governance = (0, policy_governance_1.readPolicyGovernanceConfig)(cwd);
977
+ const target = (0, policy_exceptions_1.listPolicyExceptions)(cwd).all.find((entry) => entry.id === String(id).trim());
978
+ if (!target) {
979
+ if (options.json) {
980
+ console.log(JSON.stringify({ error: 'Exception not found' }, null, 2));
981
+ process.exit(1);
982
+ }
983
+ console.log(chalk.yellow('\n⚠️ Exception not found.\n'));
984
+ process.exit(1);
985
+ }
986
+ const normalizedApprover = approver.toLowerCase();
987
+ const requestedBy = (target.requestedBy || target.createdBy || '').toLowerCase();
988
+ if (governance.exceptionApprovals.disallowSelfApproval && requestedBy && normalizedApprover === requestedBy) {
989
+ throw new Error('self-approval is disallowed by governance policy');
990
+ }
991
+ if (governance.exceptionApprovals.allowedApprovers.length > 0
992
+ && !governance.exceptionApprovals.allowedApprovers.map((item) => item.toLowerCase()).includes(normalizedApprover)) {
993
+ throw new Error('approver is not in governance allow-list');
994
+ }
757
995
  const updated = (0, policy_exceptions_1.approvePolicyException)(cwd, String(id).trim(), {
758
996
  approver,
759
997
  comment: options.comment,
@@ -766,6 +1004,19 @@ function policyCommand(program) {
766
1004
  console.log(chalk.yellow('\n⚠️ Exception not found.\n'));
767
1005
  process.exit(1);
768
1006
  }
1007
+ const requiredApprovalResolution = (0, policy_governance_1.resolveRequiredApprovalsForRule)(updated.rulePattern, governance);
1008
+ const acceptedApprovals = updated.approvals.filter((item) => {
1009
+ const actor = item.approver.toLowerCase();
1010
+ if (governance.exceptionApprovals.allowedApprovers.length > 0
1011
+ && !governance.exceptionApprovals.allowedApprovers.map((entry) => entry.toLowerCase()).includes(actor)) {
1012
+ return false;
1013
+ }
1014
+ if (governance.exceptionApprovals.disallowSelfApproval && requestedBy && actor === requestedBy) {
1015
+ return false;
1016
+ }
1017
+ return true;
1018
+ });
1019
+ const effectiveApprovals = acceptedApprovals.length;
769
1020
  try {
770
1021
  (0, policy_audit_1.appendPolicyAuditEvent)(cwd, {
771
1022
  actor: approver,
@@ -775,6 +1026,9 @@ function policyCommand(program) {
775
1026
  metadata: {
776
1027
  comment: options.comment || null,
777
1028
  approvals: updated.approvals.length,
1029
+ effectiveApprovals,
1030
+ requiredApprovals: requiredApprovalResolution.requiredApprovals,
1031
+ criticalRule: requiredApprovalResolution.critical,
778
1032
  },
779
1033
  });
780
1034
  }
@@ -785,12 +1039,20 @@ function policyCommand(program) {
785
1039
  console.log(JSON.stringify({
786
1040
  approved: true,
787
1041
  exception: updated,
1042
+ effectiveApprovals,
1043
+ requiredApprovals: requiredApprovalResolution.requiredApprovals,
1044
+ approvalSatisfied: !governance.exceptionApprovals.required
1045
+ || effectiveApprovals >= requiredApprovalResolution.requiredApprovals,
1046
+ criticalRule: requiredApprovalResolution.critical,
788
1047
  }, null, 2));
789
1048
  return;
790
1049
  }
791
1050
  console.log(chalk.green('\n✅ Policy exception approval recorded.\n'));
792
1051
  console.log(chalk.dim(`ID: ${updated.id}`));
793
- console.log(chalk.dim(`Approvals: ${updated.approvals.length}`));
1052
+ console.log(chalk.dim(`Approvals: ${updated.approvals.length} (effective=${effectiveApprovals})`));
1053
+ if (governance.exceptionApprovals.required) {
1054
+ console.log(chalk.dim(`Required approvals: ${requiredApprovalResolution.requiredApprovals}${requiredApprovalResolution.critical ? ' (critical rule gate)' : ''}`));
1055
+ }
794
1056
  console.log(chalk.dim(`Approver: ${approver}`));
795
1057
  console.log('');
796
1058
  }