@neurcode-ai/cli 0.9.34 → 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.
Files changed (53) hide show
  1. package/README.md +2 -1
  2. package/dist/api-client.d.ts +59 -1
  3. package/dist/api-client.d.ts.map +1 -1
  4. package/dist/api-client.js +16 -1
  5. package/dist/api-client.js.map +1 -1
  6. package/dist/commands/ask.d.ts.map +1 -1
  7. package/dist/commands/ask.js +15 -2
  8. package/dist/commands/ask.js.map +1 -1
  9. package/dist/commands/plan.d.ts.map +1 -1
  10. package/dist/commands/plan.js +81 -2
  11. package/dist/commands/plan.js.map +1 -1
  12. package/dist/commands/policy.d.ts.map +1 -1
  13. package/dist/commands/policy.js +377 -20
  14. package/dist/commands/policy.js.map +1 -1
  15. package/dist/commands/verify.d.ts +8 -0
  16. package/dist/commands/verify.d.ts.map +1 -1
  17. package/dist/commands/verify.js +454 -59
  18. package/dist/commands/verify.js.map +1 -1
  19. package/dist/index.js +102 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/utils/change-contract.d.ts +55 -0
  22. package/dist/utils/change-contract.d.ts.map +1 -0
  23. package/dist/utils/change-contract.js +158 -0
  24. package/dist/utils/change-contract.js.map +1 -0
  25. package/dist/utils/policy-audit.d.ts +2 -2
  26. package/dist/utils/policy-audit.d.ts.map +1 -1
  27. package/dist/utils/policy-audit.js.map +1 -1
  28. package/dist/utils/policy-compiler.d.ts +68 -0
  29. package/dist/utils/policy-compiler.d.ts.map +1 -0
  30. package/dist/utils/policy-compiler.js +170 -0
  31. package/dist/utils/policy-compiler.js.map +1 -0
  32. package/dist/utils/policy-exceptions.d.ts +11 -1
  33. package/dist/utils/policy-exceptions.d.ts.map +1 -1
  34. package/dist/utils/policy-exceptions.js +94 -6
  35. package/dist/utils/policy-exceptions.js.map +1 -1
  36. package/dist/utils/policy-governance.d.ts +22 -1
  37. package/dist/utils/policy-governance.d.ts.map +1 -1
  38. package/dist/utils/policy-governance.js +178 -14
  39. package/dist/utils/policy-governance.js.map +1 -1
  40. package/dist/utils/policy-packs.d.ts +1 -1
  41. package/dist/utils/policy-packs.d.ts.map +1 -1
  42. package/dist/utils/policy-packs.js +185 -0
  43. package/dist/utils/policy-packs.js.map +1 -1
  44. package/dist/utils/project-root.d.ts +16 -0
  45. package/dist/utils/project-root.d.ts.map +1 -1
  46. package/dist/utils/project-root.js +123 -9
  47. package/dist/utils/project-root.js.map +1 -1
  48. package/dist/utils/scope-telemetry.d.ts +21 -0
  49. package/dist/utils/scope-telemetry.d.ts.map +1 -0
  50. package/dist/utils/scope-telemetry.js +35 -0
  51. package/dist/utils/scope-telemetry.js.map +1 -0
  52. package/package.json +15 -12
  53. package/LICENSE +0 -201
@@ -9,6 +9,7 @@ const policy_exceptions_1 = require("../utils/policy-exceptions");
9
9
  const policy_governance_1 = require("../utils/policy-governance");
10
10
  const policy_audit_1 = require("../utils/policy-audit");
11
11
  const policy_packs_1 = require("../utils/policy-packs");
12
+ const policy_compiler_1 = require("../utils/policy-compiler");
12
13
  // Import chalk with fallback
13
14
  let chalk;
14
15
  try {
@@ -62,6 +63,16 @@ function resolveActor(explicit) {
62
63
  return explicit.trim();
63
64
  return process.env.NEURCODE_ACTOR || process.env.GITHUB_ACTOR || process.env.USER || 'unknown';
64
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
+ }
65
76
  async function resolveCustomPolicies(client, includeDashboardPolicies, requireDashboardPolicies) {
66
77
  if (!includeDashboardPolicies) {
67
78
  return {
@@ -152,7 +163,7 @@ function policyCommand(program) {
152
163
  policy
153
164
  .command('install')
154
165
  .description('Install a policy pack for this repository')
155
- .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)')
156
167
  .option('--force', 'Replace any existing installed policy pack')
157
168
  .option('--json', 'Output as JSON')
158
169
  .action((packId, options) => {
@@ -365,14 +376,161 @@ function policyCommand(program) {
365
376
  }
366
377
  process.exit(pass ? 0 : 1);
367
378
  });
379
+ policy
380
+ .command('compile')
381
+ .description('Compile deterministic policy constraints into a committed artifact')
382
+ .option('--intent <text>', 'Optional intent constraints to compile alongside policy rules')
383
+ .option('--no-dashboard', 'Exclude dashboard custom policies from compiled artifact')
384
+ .option('--require-dashboard', 'Fail if dashboard custom policies cannot be loaded')
385
+ .option('--output <path>', 'Output file path (default: neurcode.policy.compiled.json)')
386
+ .option('--json', 'Output as JSON')
387
+ .action(async (options) => {
388
+ const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
389
+ const config = loadPolicyRuntimeConfig();
390
+ const client = new api_client_1.ApiClient(config);
391
+ const includeDashboard = options.dashboard !== false;
392
+ try {
393
+ const customPolicyResolution = await resolveCustomPolicies(client, includeDashboard, options.requireDashboard === true);
394
+ const installedPack = (0, policy_packs_1.getInstalledPolicyPackRules)(cwd);
395
+ const customRules = customPolicyResolution.includeDashboardPolicies
396
+ ? (0, custom_policy_rules_1.mapActiveCustomPoliciesToRules)(customPolicyResolution.customPolicies)
397
+ : [];
398
+ const snapshot = (0, policy_packs_1.buildPolicyStateSnapshot)({
399
+ policyPack: installedPack,
400
+ policyPackRules: installedPack?.rules || [],
401
+ customPolicies: customPolicyResolution.customPolicies,
402
+ customRules,
403
+ includeDashboardPolicies: customPolicyResolution.includeDashboardPolicies,
404
+ });
405
+ const compiled = (0, policy_compiler_1.buildCompiledPolicyArtifact)({
406
+ includeDashboardPolicies: customPolicyResolution.includeDashboardPolicies,
407
+ policyLockPath: (0, policy_packs_1.getPolicyLockPath)(cwd),
408
+ policyLockFingerprint: snapshot.effective.fingerprint,
409
+ policyPack: installedPack
410
+ ? {
411
+ id: installedPack.packId,
412
+ name: installedPack.packName,
413
+ version: installedPack.version,
414
+ }
415
+ : null,
416
+ defaultRuleCount: snapshot.defaultRules.count,
417
+ policyPackRuleCount: installedPack?.rules.length || 0,
418
+ customRuleCount: customRules.length,
419
+ effectiveRuleCount: snapshot.effective.ruleCount,
420
+ intentConstraints: options.intent,
421
+ policyRules: customPolicyResolution.customPolicies.map((policy) => policy.rule_text),
422
+ });
423
+ const outputPath = (0, policy_compiler_1.writeCompiledPolicyArtifact)(cwd, compiled, options.output);
424
+ const readBack = (0, policy_compiler_1.readCompiledPolicyArtifact)(cwd, options.output);
425
+ try {
426
+ (0, policy_audit_1.appendPolicyAuditEvent)(cwd, {
427
+ actor: resolveActor(),
428
+ action: 'policy_compiled',
429
+ entityType: 'policy_compiled_artifact',
430
+ entityId: outputPath,
431
+ metadata: {
432
+ fingerprint: compiled.fingerprint,
433
+ deterministicRuleCount: compiled.compilation.deterministicRuleCount,
434
+ unmatchedStatements: compiled.compilation.unmatchedStatements.length,
435
+ dashboardMode: compiled.source.includeDashboardPolicies ? 'dashboard' : 'disabled',
436
+ },
437
+ });
438
+ }
439
+ catch {
440
+ // Non-blocking audit write.
441
+ }
442
+ if (options.json) {
443
+ console.log(JSON.stringify({
444
+ artifact: compiled,
445
+ path: outputPath,
446
+ resolvedPath: (0, policy_compiler_1.resolveCompiledPolicyPath)(cwd, options.output),
447
+ verified: readBack.artifact !== null,
448
+ warning: customPolicyResolution.dashboardWarning || null,
449
+ }, null, 2));
450
+ return;
451
+ }
452
+ console.log(chalk.green('\n✅ Policy compilation complete\n'));
453
+ console.log(chalk.cyan(`Path: ${outputPath}`));
454
+ console.log(chalk.dim(`Fingerprint: ${compiled.fingerprint}`));
455
+ console.log(chalk.dim(`Deterministic rules: ${compiled.compilation.deterministicRuleCount}`));
456
+ console.log(chalk.dim(`Unmatched statements: ${compiled.compilation.unmatchedStatements.length}`));
457
+ console.log(chalk.dim(`Policy source: ${compiled.source.includeDashboardPolicies ? 'dashboard + local packs' : 'local packs only'}`));
458
+ if (customPolicyResolution.dashboardWarning) {
459
+ console.log(chalk.yellow(`\n⚠️ ${customPolicyResolution.dashboardWarning}`));
460
+ }
461
+ console.log(chalk.dim('Run `neurcode verify --enforce-change-contract` to enforce this compiled contract.\n'));
462
+ }
463
+ catch (error) {
464
+ const message = error instanceof Error ? error.message : 'Unknown error';
465
+ if (options.json) {
466
+ console.log(JSON.stringify({ error: message }, null, 2));
467
+ process.exit(1);
468
+ }
469
+ console.error(chalk.red(`\n❌ ${message}\n`));
470
+ process.exit(1);
471
+ }
472
+ });
368
473
  const governance = policy
369
474
  .command('governance')
370
475
  .description('Configure exception approval and policy audit governance');
371
476
  governance
372
477
  .command('status')
373
478
  .description('Show policy governance settings for this repository')
479
+ .option('--org', 'Fetch centralized organization governance settings from Neurcode Cloud')
374
480
  .option('--json', 'Output as JSON')
375
- .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
+ }
376
534
  const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
377
535
  const config = (0, policy_governance_1.readPolicyGovernanceConfig)(cwd);
378
536
  if (options.json) {
@@ -387,37 +545,155 @@ function policyCommand(program) {
387
545
  console.log(chalk.dim(`Exception approvals required: ${config.exceptionApprovals.required ? 'yes' : 'no'}`));
388
546
  console.log(chalk.dim(`Minimum approvals: ${config.exceptionApprovals.minApprovals}`));
389
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}`));
390
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}`));
391
554
  console.log(chalk.dim(`Require audit integrity: ${config.audit.requireIntegrity ? 'yes' : 'no'}`));
392
555
  console.log('');
393
556
  });
394
557
  governance
395
558
  .command('set')
396
559
  .description('Update policy governance settings')
560
+ .option('--org', 'Update centralized organization governance settings in Neurcode Cloud')
397
561
  .option('--require-approval', 'Require approvals before exceptions become effective')
398
562
  .option('--no-require-approval', 'Do not require approvals for exceptions')
399
563
  .option('--min-approvals <n>', 'Minimum approvals required when approval mode is enabled', (value) => parseInt(value, 10))
400
564
  .option('--allow-self-approval', 'Allow requester to approve their own exception')
401
565
  .option('--restrict-approvers <csv>', 'Comma-separated allow-list of approver identities')
402
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))
403
574
  .option('--require-audit-integrity', 'Fail verify if policy audit chain integrity is broken')
404
575
  .option('--no-require-audit-integrity', 'Do not enforce policy audit integrity in verify')
405
576
  .option('--json', 'Output as JSON')
406
- .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
+ }
407
685
  const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
408
686
  try {
409
687
  const next = (0, policy_governance_1.updatePolicyGovernanceConfig)(cwd, {
410
688
  required: typeof options.requireApproval === 'boolean' ? options.requireApproval : undefined,
411
689
  minApprovals: Number.isFinite(options.minApprovals) ? options.minApprovals : undefined,
412
690
  disallowSelfApproval: typeof options.allowSelfApproval === 'boolean' ? !options.allowSelfApproval : undefined,
413
- allowedApprovers: options.clearApprovers
414
- ? []
415
- : typeof options.restrictApprovers === 'string'
416
- ? options.restrictApprovers
417
- .split(',')
418
- .map((item) => item.trim())
419
- .filter(Boolean)
420
- : 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,
421
697
  requireAuditIntegrity: typeof options.requireAuditIntegrity === 'boolean' ? options.requireAuditIntegrity : undefined,
422
698
  });
423
699
  try {
@@ -431,6 +707,11 @@ function policyCommand(program) {
431
707
  minApprovals: next.exceptionApprovals.minApprovals,
432
708
  disallowSelfApproval: next.exceptionApprovals.disallowSelfApproval,
433
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,
434
715
  requireAuditIntegrity: next.audit.requireIntegrity,
435
716
  },
436
717
  });
@@ -447,6 +728,11 @@ function policyCommand(program) {
447
728
  console.log(chalk.dim(`Approval required: ${next.exceptionApprovals.required ? 'yes' : 'no'}`));
448
729
  console.log(chalk.dim(`Min approvals: ${next.exceptionApprovals.minApprovals}`));
449
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)'}`));
450
736
  console.log(chalk.dim(`Require audit integrity: ${next.audit.requireIntegrity ? 'yes' : 'no'}`));
451
737
  console.log(chalk.dim('Commit governance + audit files so CI can enforce approval and integrity rules.'));
452
738
  console.log('');
@@ -514,17 +800,27 @@ function policyCommand(program) {
514
800
  if (allowedApprovers.size > 0) {
515
801
  effectiveApprovals = effectiveApprovals.filter((item) => allowedApprovers.has(item.approver.toLowerCase()));
516
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;
517
809
  const status = !entry.active || !unexpired
518
810
  ? 'inactive'
519
- : !governance.exceptionApprovals.required
520
- ? 'active'
521
- : effectiveApprovals.length >= governance.exceptionApprovals.minApprovals
522
- ? 'approved'
523
- : 'pending';
811
+ : !reasonValid
812
+ ? 'invalid_reason'
813
+ : !governance.exceptionApprovals.required
814
+ ? 'active'
815
+ : effectiveApprovals.length >= requiredApprovals
816
+ ? 'approved'
817
+ : 'pending';
524
818
  return {
525
819
  ...entry,
526
820
  status,
527
821
  effectiveApprovals: effectiveApprovals.length,
822
+ requiredApprovals,
823
+ criticalRule: requiredApprovalResolution.critical,
528
824
  };
529
825
  });
530
826
  const items = options.all ? withStatus : withStatus.filter((entry) => entry.status !== 'inactive');
@@ -550,7 +846,10 @@ function policyCommand(program) {
550
846
  console.log(chalk.dim(` status=${entry.status || (entry.active ? 'active' : 'inactive')}`));
551
847
  console.log(chalk.dim(` rule=${entry.rulePattern} file=${entry.filePattern}`));
552
848
  console.log(chalk.dim(` expires=${entry.expiresAt} active=${entry.active ? 'yes' : 'no'}`));
553
- 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' : ''}`));
554
853
  console.log(chalk.dim(` reason=${entry.reason}`));
555
854
  console.log(chalk.dim(` requestedBy=${entry.requestedBy || entry.createdBy || 'unknown'}`));
556
855
  if (entry.ticket) {
@@ -587,6 +886,12 @@ function policyCommand(program) {
587
886
  expiresAt: options.expiresAt,
588
887
  expiresInDays: options.expiresInDays,
589
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);
590
895
  const createdBy = resolveActor();
591
896
  const created = (0, policy_exceptions_1.addPolicyException)(cwd, {
592
897
  rulePattern: options.rule,
@@ -598,7 +903,7 @@ function policyCommand(program) {
598
903
  createdBy,
599
904
  requestedBy: createdBy,
600
905
  });
601
- const governance = (0, policy_governance_1.readPolicyGovernanceConfig)(cwd);
906
+ const approvalResolution = (0, policy_governance_1.resolveRequiredApprovalsForRule)(created.rulePattern, governance);
602
907
  try {
603
908
  (0, policy_audit_1.appendPolicyAuditEvent)(cwd, {
604
909
  actor: createdBy,
@@ -610,6 +915,10 @@ function policyCommand(program) {
610
915
  filePattern: created.filePattern,
611
916
  expiresAt: created.expiresAt,
612
917
  requireApproval: governance.exceptionApprovals.required,
918
+ requiredApprovals: governance.exceptionApprovals.required
919
+ ? approvalResolution.requiredApprovals
920
+ : 0,
921
+ criticalRule: approvalResolution.critical,
613
922
  },
614
923
  });
615
924
  }
@@ -621,6 +930,10 @@ function policyCommand(program) {
621
930
  created,
622
931
  path: (0, policy_exceptions_1.getPolicyExceptionsPath)(cwd),
623
932
  requiresApproval: governance.exceptionApprovals.required,
933
+ requiredApprovals: governance.exceptionApprovals.required
934
+ ? approvalResolution.requiredApprovals
935
+ : 0,
936
+ criticalRule: approvalResolution.critical,
624
937
  }, null, 2));
625
938
  return;
626
939
  }
@@ -631,7 +944,8 @@ function policyCommand(program) {
631
944
  console.log(chalk.dim(`Expires: ${created.expiresAt}`));
632
945
  console.log(chalk.dim(`Reason: ${created.reason}`));
633
946
  if (governance.exceptionApprovals.required) {
634
- 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)' : ''}.`));
635
949
  }
636
950
  if (created.ticket) {
637
951
  console.log(chalk.dim(`Ticket: ${created.ticket}`));
@@ -659,6 +973,25 @@ function policyCommand(program) {
659
973
  const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
660
974
  const approver = resolveActor(options.by);
661
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
+ }
662
995
  const updated = (0, policy_exceptions_1.approvePolicyException)(cwd, String(id).trim(), {
663
996
  approver,
664
997
  comment: options.comment,
@@ -671,6 +1004,19 @@ function policyCommand(program) {
671
1004
  console.log(chalk.yellow('\n⚠️ Exception not found.\n'));
672
1005
  process.exit(1);
673
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;
674
1020
  try {
675
1021
  (0, policy_audit_1.appendPolicyAuditEvent)(cwd, {
676
1022
  actor: approver,
@@ -680,6 +1026,9 @@ function policyCommand(program) {
680
1026
  metadata: {
681
1027
  comment: options.comment || null,
682
1028
  approvals: updated.approvals.length,
1029
+ effectiveApprovals,
1030
+ requiredApprovals: requiredApprovalResolution.requiredApprovals,
1031
+ criticalRule: requiredApprovalResolution.critical,
683
1032
  },
684
1033
  });
685
1034
  }
@@ -690,12 +1039,20 @@ function policyCommand(program) {
690
1039
  console.log(JSON.stringify({
691
1040
  approved: true,
692
1041
  exception: updated,
1042
+ effectiveApprovals,
1043
+ requiredApprovals: requiredApprovalResolution.requiredApprovals,
1044
+ approvalSatisfied: !governance.exceptionApprovals.required
1045
+ || effectiveApprovals >= requiredApprovalResolution.requiredApprovals,
1046
+ criticalRule: requiredApprovalResolution.critical,
693
1047
  }, null, 2));
694
1048
  return;
695
1049
  }
696
1050
  console.log(chalk.green('\n✅ Policy exception approval recorded.\n'));
697
1051
  console.log(chalk.dim(`ID: ${updated.id}`));
698
- 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
+ }
699
1056
  console.log(chalk.dim(`Approver: ${approver}`));
700
1057
  console.log('');
701
1058
  }