@neurcode-ai/cli 0.9.29 → 0.9.31

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.
@@ -13,6 +13,7 @@ const config_1 = require("../config");
13
13
  const project_root_1 = require("../utils/project-root");
14
14
  const state_1 = require("../utils/state");
15
15
  const breakage_simulator_1 = require("../utils/breakage-simulator");
16
+ const manual_approvals_1 = require("../utils/manual-approvals");
16
17
  let chalk;
17
18
  try {
18
19
  chalk = require('chalk');
@@ -108,6 +109,7 @@ function createShipCheckpoint(input) {
108
109
  requirePass: input.requirePass,
109
110
  requirePolicyLock: input.requirePolicyLock,
110
111
  skipPolicyLock: input.skipPolicyLock,
112
+ manualApproveHighRisk: input.options.manualApproveHighRisk === true,
111
113
  publishCard: input.options.publishCard !== false,
112
114
  },
113
115
  baselineDirtyPaths: [],
@@ -542,6 +544,152 @@ function parseVerifyPayload(output) {
542
544
  };
543
545
  })()
544
546
  : undefined;
547
+ const contextPolicy = record.contextPolicy && typeof record.contextPolicy === 'object' && !Array.isArray(record.contextPolicy)
548
+ ? (() => {
549
+ const raw = record.contextPolicy;
550
+ const violations = Array.isArray(raw.violations)
551
+ ? raw.violations
552
+ .filter((item) => !!item && typeof item === 'object')
553
+ .map((item) => ({
554
+ file: typeof item.file === 'string' ? item.file : 'unknown',
555
+ rule: typeof item.rule === 'string' ? item.rule : 'unknown',
556
+ reason: typeof item.reason === 'string' ? item.reason : '',
557
+ }))
558
+ : [];
559
+ return {
560
+ deniedModifyTouched: Array.isArray(raw.deniedModifyTouched)
561
+ ? raw.deniedModifyTouched.filter((item) => typeof item === 'string')
562
+ : [],
563
+ violations,
564
+ };
565
+ })()
566
+ : undefined;
567
+ const blastRadius = record.blastRadius && typeof record.blastRadius === 'object' && !Array.isArray(record.blastRadius)
568
+ ? (() => {
569
+ const raw = record.blastRadius;
570
+ const risk = typeof raw.riskScore === 'string' ? raw.riskScore.toLowerCase() : 'low';
571
+ const riskScore = risk === 'high' || risk === 'medium' ? risk : 'low';
572
+ return {
573
+ filesChanged: typeof raw.filesChanged === 'number' ? raw.filesChanged : 0,
574
+ functionsAffected: typeof raw.functionsAffected === 'number' ? raw.functionsAffected : 0,
575
+ modulesAffected: Array.isArray(raw.modulesAffected)
576
+ ? raw.modulesAffected.filter((item) => typeof item === 'string')
577
+ : [],
578
+ dependenciesAdded: Array.isArray(raw.dependenciesAdded)
579
+ ? raw.dependenciesAdded.filter((item) => typeof item === 'string')
580
+ : [],
581
+ riskScore: riskScore,
582
+ };
583
+ })()
584
+ : undefined;
585
+ const suspiciousChange = record.suspiciousChange && typeof record.suspiciousChange === 'object' && !Array.isArray(record.suspiciousChange)
586
+ ? (() => {
587
+ const raw = record.suspiciousChange;
588
+ const confidenceRaw = typeof raw.confidence === 'string' ? raw.confidence.toLowerCase() : 'low';
589
+ const confidence = confidenceRaw === 'high' || confidenceRaw === 'medium' ? confidenceRaw : 'low';
590
+ return {
591
+ expectedFiles: typeof raw.expectedFiles === 'number' ? raw.expectedFiles : 0,
592
+ actualFiles: typeof raw.actualFiles === 'number' ? raw.actualFiles : 0,
593
+ unexpectedFiles: Array.isArray(raw.unexpectedFiles)
594
+ ? raw.unexpectedFiles.filter((item) => typeof item === 'string')
595
+ : [],
596
+ flagged: raw.flagged === true,
597
+ confidence: confidence,
598
+ };
599
+ })()
600
+ : undefined;
601
+ const changeJustification = record.changeJustification && typeof record.changeJustification === 'object' && !Array.isArray(record.changeJustification)
602
+ ? (() => {
603
+ const raw = record.changeJustification;
604
+ return {
605
+ task: typeof raw.task === 'string' ? raw.task : '',
606
+ changes: Array.isArray(raw.changes)
607
+ ? raw.changes
608
+ .filter((item) => !!item && typeof item === 'object')
609
+ .map((item) => ({
610
+ file: typeof item.file === 'string' ? item.file : 'unknown',
611
+ reason: typeof item.reason === 'string' ? item.reason : '',
612
+ }))
613
+ : [],
614
+ };
615
+ })()
616
+ : undefined;
617
+ const governanceDecision = record.governanceDecision && typeof record.governanceDecision === 'object' && !Array.isArray(record.governanceDecision)
618
+ ? (() => {
619
+ const raw = record.governanceDecision;
620
+ const decisionRaw = typeof raw.decision === 'string' ? raw.decision.toLowerCase() : 'allow';
621
+ const decision = decisionRaw === 'warn' || decisionRaw === 'manual_approval' || decisionRaw === 'block'
622
+ ? decisionRaw
623
+ : 'allow';
624
+ return {
625
+ decision: decision,
626
+ reasonCodes: Array.isArray(raw.reasonCodes)
627
+ ? raw.reasonCodes.filter((item) => typeof item === 'string')
628
+ : [],
629
+ summary: typeof raw.summary === 'string' ? raw.summary : undefined,
630
+ averageRelevanceScore: typeof raw.averageRelevanceScore === 'number' ? raw.averageRelevanceScore : undefined,
631
+ lowRelevanceFiles: Array.isArray(raw.lowRelevanceFiles)
632
+ ? raw.lowRelevanceFiles
633
+ .filter((item) => !!item && typeof item === 'object')
634
+ .map((item) => ({
635
+ file: typeof item.file === 'string' ? item.file : 'unknown',
636
+ relevanceScore: typeof item.relevanceScore === 'number' ? item.relevanceScore : 0,
637
+ planLink: typeof item.planLink === 'string' ? item.planLink : 'unknown',
638
+ }))
639
+ : undefined,
640
+ requiresManualApproval: raw.requiresManualApproval === true,
641
+ };
642
+ })()
643
+ : undefined;
644
+ const aiChangeLog = record.aiChangeLog && typeof record.aiChangeLog === 'object' && !Array.isArray(record.aiChangeLog)
645
+ ? (() => {
646
+ const raw = record.aiChangeLog;
647
+ const integrityRaw = raw.integrity && typeof raw.integrity === 'object' && !Array.isArray(raw.integrity)
648
+ ? raw.integrity
649
+ : null;
650
+ return {
651
+ path: typeof raw.path === 'string' ? raw.path : undefined,
652
+ auditPath: typeof raw.auditPath === 'string' ? raw.auditPath : undefined,
653
+ integrity: integrityRaw
654
+ ? {
655
+ valid: integrityRaw.valid === true,
656
+ required: integrityRaw.required === true,
657
+ signed: integrityRaw.signed === true,
658
+ issues: Array.isArray(integrityRaw.issues)
659
+ ? integrityRaw.issues.filter((item) => typeof item === 'string')
660
+ : [],
661
+ payloadHash: typeof integrityRaw.payloadHash === 'string' ? integrityRaw.payloadHash : null,
662
+ chainHash: typeof integrityRaw.chainHash === 'string' ? integrityRaw.chainHash : null,
663
+ }
664
+ : undefined,
665
+ };
666
+ })()
667
+ : undefined;
668
+ const policySources = record.policySources && typeof record.policySources === 'object' && !Array.isArray(record.policySources)
669
+ ? (() => {
670
+ const raw = record.policySources;
671
+ const modeRaw = typeof raw.mode === 'string' ? raw.mode.toLowerCase() : 'local';
672
+ const mode = modeRaw === 'merged' || modeRaw === 'org_only' ? modeRaw : 'local';
673
+ return {
674
+ localPolicy: raw.localPolicy !== false,
675
+ orgPolicy: raw.orgPolicy === true,
676
+ mode: mode,
677
+ };
678
+ })()
679
+ : undefined;
680
+ const orgGovernance = record.orgGovernance && typeof record.orgGovernance === 'object' && !Array.isArray(record.orgGovernance)
681
+ ? (() => {
682
+ const raw = record.orgGovernance;
683
+ return {
684
+ requireSignedAiLogs: raw.requireSignedAiLogs === true,
685
+ requireManualApproval: raw.requireManualApproval !== false,
686
+ minimumManualApprovals: typeof raw.minimumManualApprovals === 'number'
687
+ ? Math.max(1, Math.min(5, Math.floor(raw.minimumManualApprovals)))
688
+ : 1,
689
+ updatedAt: typeof raw.updatedAt === 'string' ? raw.updatedAt : null,
690
+ };
691
+ })()
692
+ : undefined;
545
693
  return {
546
694
  grade: record.grade,
547
695
  score: typeof record.score === 'number' ? record.score : 0,
@@ -561,6 +709,14 @@ function parseVerifyPayload(output) {
561
709
  policyLock,
562
710
  policyExceptions,
563
711
  policyGovernance,
712
+ contextPolicy,
713
+ blastRadius,
714
+ suspiciousChange,
715
+ changeJustification,
716
+ governanceDecision,
717
+ aiChangeLog,
718
+ policySources,
719
+ orgGovernance,
564
720
  };
565
721
  }
566
722
  function parsePlanPayload(output) {
@@ -644,6 +800,20 @@ function runGit(cwd, args) {
644
800
  stderr: result.stderr || '',
645
801
  };
646
802
  }
803
+ function getHeadSha(cwd) {
804
+ const head = runGit(cwd, ['rev-parse', 'HEAD']);
805
+ if (head.code !== 0)
806
+ return null;
807
+ const value = head.stdout.trim().toLowerCase();
808
+ return value || null;
809
+ }
810
+ function resolveDistinctManualApprovers(cwd, commitSha) {
811
+ const approvals = (0, manual_approvals_1.getManualApprovalsForCommit)(cwd, commitSha);
812
+ return {
813
+ approvals,
814
+ distinctApprovers: (0, manual_approvals_1.countDistinctApprovers)(approvals),
815
+ };
816
+ }
647
817
  function getPrimaryGitRemoteUrl(cwd) {
648
818
  const remote = runGit(cwd, ['remote', 'get-url', 'origin']);
649
819
  if (remote.code !== 0)
@@ -1613,8 +1783,64 @@ async function shipCommand(goal, options) {
1613
1783
  });
1614
1784
  }
1615
1785
  const finalVerifyPayload = verifyPayload;
1616
- const verifyPassedFinal = verifyExitCode === 0 &&
1786
+ let verifyPassedFinal = verifyExitCode === 0 &&
1617
1787
  (finalVerifyPayload.verdict === 'PASS' || (!requirePass && isInfoOnlyGovernanceResult(finalVerifyPayload)));
1788
+ const manualApprovalBypass = options.manualApproveHighRisk === true || process.env.NEURCODE_MANUAL_APPROVE_HIGH_RISK === '1';
1789
+ const governanceDecision = finalVerifyPayload.governanceDecision?.decision;
1790
+ const orgGovernance = finalVerifyPayload.orgGovernance || null;
1791
+ const orgManualApprovalRequired = orgGovernance?.requireManualApproval === true;
1792
+ const minimumManualApprovals = orgGovernance
1793
+ ? Math.max(1, Math.min(5, Math.floor(orgGovernance.minimumManualApprovals || 1)))
1794
+ : 1;
1795
+ const approvalHeadSha = getHeadSha(cwd);
1796
+ const manualApprovalState = approvalHeadSha
1797
+ ? resolveDistinctManualApprovers(cwd, approvalHeadSha)
1798
+ : { approvals: [], distinctApprovers: 0 };
1799
+ if (verifyPassedFinal && governanceDecision === 'block') {
1800
+ verifyPassedFinal = false;
1801
+ verifyExitCode = verifyExitCode === 0 ? 2 : verifyExitCode;
1802
+ remediationActions.push('governance_decision_block');
1803
+ const summary = finalVerifyPayload.governanceDecision?.summary || 'Governance decision matrix returned BLOCK.';
1804
+ finalVerifyPayload.message = `${finalVerifyPayload.message || 'Governance verification completed.'} ${summary}`;
1805
+ console.log(chalk.red('\n⛔ Ship blocked by governance decision matrix (BLOCK).'));
1806
+ if (finalVerifyPayload.governanceDecision?.reasonCodes?.length) {
1807
+ console.log(chalk.dim(` Reasons: ${finalVerifyPayload.governanceDecision.reasonCodes.join(', ')}`));
1808
+ }
1809
+ }
1810
+ const requiresManualApproval = governanceDecision === 'manual_approval' ||
1811
+ (!governanceDecision && finalVerifyPayload.blastRadius?.riskScore === 'high');
1812
+ let effectiveDistinctApprovers = manualApprovalState.distinctApprovers;
1813
+ if (orgManualApprovalRequired && manualApprovalBypass) {
1814
+ // Backward-compatible flag counts as the current operator approval in enterprise mode.
1815
+ effectiveDistinctApprovers += 1;
1816
+ }
1817
+ const manualApprovalSatisfied = orgManualApprovalRequired
1818
+ ? effectiveDistinctApprovers >= minimumManualApprovals
1819
+ : manualApprovalBypass;
1820
+ if (verifyPassedFinal && requiresManualApproval && !manualApprovalSatisfied) {
1821
+ verifyPassedFinal = false;
1822
+ verifyExitCode = verifyExitCode === 0 ? 2 : verifyExitCode;
1823
+ remediationActions.push('governance_manual_approval_required');
1824
+ const summary = finalVerifyPayload.governanceDecision?.summary ||
1825
+ 'High blast-radius risk requires manual approval before ship can continue.';
1826
+ finalVerifyPayload.message = `${finalVerifyPayload.message || 'Governance verification completed.'} ${summary}`;
1827
+ console.log(chalk.red('\n⛔ Ship blocked: governance requires manual approval before deploy.'));
1828
+ if (orgManualApprovalRequired) {
1829
+ const recordedCount = manualApprovalState.distinctApprovers;
1830
+ console.log(chalk.dim(` Manual approvals required: ${minimumManualApprovals}, recorded: ${recordedCount}${manualApprovalBypass ? ' (+1 current operator flag)' : ''}`));
1831
+ if (approvalHeadSha) {
1832
+ console.log(chalk.dim(` Commit: ${approvalHeadSha}`));
1833
+ }
1834
+ console.log(chalk.dim(' Record approvals with `neurcode approve --reason "<review context>"`.'));
1835
+ console.log(chalk.dim(' Then re-run `neurcode ship ...` to continue.'));
1836
+ }
1837
+ else {
1838
+ console.log(chalk.dim(' Re-run with --manual-approve-high-risk after manual review.'));
1839
+ }
1840
+ if (typeof finalVerifyPayload.governanceDecision?.averageRelevanceScore === 'number') {
1841
+ console.log(chalk.dim(` Avg relevance score: ${finalVerifyPayload.governanceDecision.averageRelevanceScore}`));
1842
+ }
1843
+ }
1618
1844
  if (verifyPassedFinal && !options.skipTests) {
1619
1845
  testsAttempts += 1;
1620
1846
  if (!testCommand) {
@@ -2182,6 +2408,7 @@ async function shipResumeCommand(runId, options) {
2182
2408
  requirePass: options.requirePass ?? checkpoint.options.requirePass,
2183
2409
  requirePolicyLock: options.requirePolicyLock ?? checkpoint.options.requirePolicyLock,
2184
2410
  skipPolicyLock: options.skipPolicyLock ?? checkpoint.options.skipPolicyLock,
2411
+ manualApproveHighRisk: options.manualApproveHighRisk ?? checkpoint.options.manualApproveHighRisk,
2185
2412
  publishCard: options.publishCard ?? checkpoint.options.publishCard,
2186
2413
  json: options.json === true,
2187
2414
  resumeRunId: checkpoint.runId,