@neurcode-ai/cli 0.9.29 → 0.9.30

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.
@@ -58,6 +58,8 @@ const custom_policy_rules_1 = require("../utils/custom-policy-rules");
58
58
  const policy_exceptions_1 = require("../utils/policy-exceptions");
59
59
  const policy_governance_1 = require("../utils/policy-governance");
60
60
  const policy_audit_1 = require("../utils/policy-audit");
61
+ const governance_1 = require("../utils/governance");
62
+ const policy_1 = require("@neurcode-ai/policy");
61
63
  // Import chalk with fallback
62
64
  let chalk;
63
65
  try {
@@ -294,10 +296,56 @@ function resolveAuditIntegrityStatus(requireIntegrity, auditIntegrity) {
294
296
  * Execute policy-only verification (General Governance mode)
295
297
  * Returns the exit code to use
296
298
  */
297
- async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRoot, config, client, source) {
299
+ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRoot, config, client, source, orgGovernanceSettings, aiLogSigningKey, aiLogSigner) {
298
300
  if (!options.json) {
299
301
  console.log(chalk.cyan('šŸ›”ļø General Governance mode (policy only, no plan linked)\n'));
300
302
  }
303
+ const governanceAnalysis = (0, governance_1.evaluateGovernance)({
304
+ projectRoot,
305
+ task: 'Policy-only verification',
306
+ expectedFiles: [],
307
+ diffFiles,
308
+ contextCandidates: diffFiles.map((file) => file.path),
309
+ orgGovernance: orgGovernanceSettings,
310
+ signingKey: aiLogSigningKey,
311
+ signer: aiLogSigner,
312
+ });
313
+ const contextPolicyViolations = governanceAnalysis.contextPolicy.violations.filter((item) => !ignoreFilter(item.file));
314
+ if (contextPolicyViolations.length > 0) {
315
+ const message = `Context policy violation: ${contextPolicyViolations.map((item) => item.file).join(', ')}`;
316
+ if (options.json) {
317
+ console.log(JSON.stringify({
318
+ grade: 'F',
319
+ score: 0,
320
+ verdict: 'FAIL',
321
+ violations: contextPolicyViolations.map((item) => ({
322
+ file: item.file,
323
+ rule: `context_policy:${item.rule}`,
324
+ severity: 'block',
325
+ message: item.reason,
326
+ })),
327
+ message,
328
+ scopeGuardPassed: false,
329
+ bloatCount: 0,
330
+ bloatFiles: [],
331
+ plannedFilesModified: 0,
332
+ totalPlannedFiles: 0,
333
+ adherenceScore: 0,
334
+ mode: 'policy_only',
335
+ policyOnly: true,
336
+ policyOnlySource: source,
337
+ ...buildGovernancePayload(governanceAnalysis, orgGovernanceSettings),
338
+ }, null, 2));
339
+ }
340
+ else {
341
+ console.log(chalk.red('āŒ Context policy violation detected (policy-only mode).'));
342
+ contextPolicyViolations.forEach((item) => {
343
+ console.log(chalk.red(` • ${item.file}: ${item.reason}`));
344
+ });
345
+ console.log(chalk.dim(`\n${message}`));
346
+ }
347
+ return 2;
348
+ }
301
349
  let policyViolations = [];
302
350
  let policyDecision = 'allow';
303
351
  const requirePolicyLock = options.requirePolicyLock === true || isEnabledFlag(process.env.NEURCODE_VERIFY_REQUIRE_POLICY_LOCK);
@@ -385,6 +433,7 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
385
433
  mode: 'policy_only',
386
434
  policyOnly: true,
387
435
  policyOnlySource: source,
436
+ ...buildGovernancePayload(governanceAnalysis, orgGovernanceSettings),
388
437
  policyLock: {
389
438
  enforced: true,
390
439
  matched: false,
@@ -516,6 +565,7 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
516
565
  mode: 'policy_only',
517
566
  policyOnly: true,
518
567
  policyOnlySource: source,
568
+ ...buildGovernancePayload(governanceAnalysis, orgGovernanceSettings),
519
569
  policyLock: {
520
570
  enforced: policyLockEvaluation.enforced,
521
571
  matched: policyLockEvaluation.matched,
@@ -555,6 +605,7 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
555
605
  if (governance.audit.requireIntegrity && !auditIntegrityStatus.valid) {
556
606
  console.log(chalk.red(' Policy audit integrity check failed'));
557
607
  }
608
+ displayGovernanceInsights(governanceAnalysis, { explain: options.explain });
558
609
  console.log(chalk.dim(`\n${message}`));
559
610
  }
560
611
  return effectiveVerdict === 'FAIL' ? 2 : effectiveVerdict === 'WARN' ? 1 : 0;
@@ -610,6 +661,31 @@ async function verifyCommand(options) {
610
661
  orgId: (0, state_1.getOrgId)(),
611
662
  projectId: projectId || null,
612
663
  };
664
+ const aiLogSigningKey = process.env.NEURCODE_GOVERNANCE_SIGNING_KEY ||
665
+ process.env.NEURCODE_AI_LOG_SIGNING_KEY ||
666
+ null;
667
+ const aiLogSigner = process.env.NEURCODE_GOVERNANCE_SIGNER || process.env.USER || 'neurcode-cli';
668
+ let orgGovernanceSettings = null;
669
+ if (config.apiKey) {
670
+ try {
671
+ const remoteSettings = await client.getOrgGovernanceSettings();
672
+ if (remoteSettings) {
673
+ orgGovernanceSettings = {
674
+ contextPolicy: (0, policy_1.normalizeContextPolicy)(remoteSettings.contextPolicy),
675
+ requireSignedAiLogs: remoteSettings.requireSignedAiLogs === true,
676
+ requireManualApproval: remoteSettings.requireManualApproval !== false,
677
+ minimumManualApprovals: Math.max(1, Math.min(5, Math.floor(remoteSettings.minimumManualApprovals || 1))),
678
+ updatedAt: remoteSettings.updatedAt,
679
+ };
680
+ }
681
+ }
682
+ catch (error) {
683
+ if (!options.json) {
684
+ const message = error instanceof Error ? error.message : 'Unknown error';
685
+ console.log(chalk.dim(` Org governance settings unavailable, using local policy only (${message})`));
686
+ }
687
+ }
688
+ }
613
689
  const recordVerifyEvent = (verdict, note, changedFiles, planId) => {
614
690
  if (!brainScope.orgId || !brainScope.projectId) {
615
691
  return;
@@ -727,13 +803,65 @@ async function verifyCommand(options) {
727
803
  const normalized = toUnixPath(filePath || '');
728
804
  return ignoreFilter(normalized) || runtimeIgnoreSet.has(normalized);
729
805
  };
806
+ const baselineContextPolicyLocal = (0, policy_1.loadContextPolicy)(projectRoot);
807
+ const baselineContextPolicy = orgGovernanceSettings?.contextPolicy
808
+ ? (0, policy_1.mergeContextPolicies)(baselineContextPolicyLocal, orgGovernanceSettings.contextPolicy)
809
+ : baselineContextPolicyLocal;
810
+ const baselineContextPolicyEvaluation = (0, policy_1.evaluateContextPolicyForChanges)(diffFiles.map((file) => file.path), baselineContextPolicy, diffFiles.map((file) => file.path));
811
+ const baselineContextViolations = baselineContextPolicyEvaluation.violations.filter((item) => !shouldIgnore(item.file));
812
+ if (baselineContextViolations.length > 0) {
813
+ const baselineGovernance = (0, governance_1.evaluateGovernance)({
814
+ projectRoot,
815
+ task: 'Context policy validation',
816
+ expectedFiles: [],
817
+ diffFiles,
818
+ contextCandidates: diffFiles.map((file) => file.path),
819
+ orgGovernance: orgGovernanceSettings,
820
+ signingKey: aiLogSigningKey,
821
+ signer: aiLogSigner,
822
+ });
823
+ const message = `Context access policy violation: ${baselineContextViolations.map((item) => item.file).join(', ')}`;
824
+ recordVerifyEvent('FAIL', `context_policy_violations=${baselineContextViolations.length}`, diffFiles.map((f) => f.path));
825
+ if (options.json) {
826
+ console.log(JSON.stringify({
827
+ grade: 'F',
828
+ score: 0,
829
+ verdict: 'FAIL',
830
+ violations: baselineContextViolations.map((item) => ({
831
+ file: item.file,
832
+ rule: `context_policy:${item.rule}`,
833
+ severity: 'block',
834
+ message: item.reason,
835
+ })),
836
+ adherenceScore: 0,
837
+ bloatCount: 0,
838
+ bloatFiles: [],
839
+ plannedFilesModified: 0,
840
+ totalPlannedFiles: 0,
841
+ message,
842
+ scopeGuardPassed: false,
843
+ ...buildGovernancePayload(baselineGovernance, orgGovernanceSettings),
844
+ mode: 'policy_violation',
845
+ policyOnly: false,
846
+ }, null, 2));
847
+ }
848
+ else {
849
+ console.log(chalk.red('\nā›” Context Policy Violation'));
850
+ baselineContextViolations.forEach((item) => {
851
+ console.log(chalk.red(` • ${item.file}: ${item.reason}`));
852
+ });
853
+ console.log(chalk.dim(`\nRisk level: ${baselineGovernance.blastRadius.riskScore.toUpperCase()}`));
854
+ console.log(chalk.red('\nAction blocked.\n'));
855
+ }
856
+ process.exit(2);
857
+ }
730
858
  if (!options.json) {
731
859
  console.log(chalk.cyan('\nšŸ“Š Analyzing changes against plan...'));
732
860
  console.log(chalk.dim(` Found ${summary.totalFiles} file(s) changed`));
733
861
  console.log(chalk.dim(` ${summary.totalAdded} lines added, ${summary.totalRemoved} lines removed\n`));
734
862
  }
735
863
  const runPolicyOnlyModeAndExit = async (source) => {
736
- const exitCode = await executePolicyOnlyMode(options, diffFiles, shouldIgnore, projectRoot, config, client, source);
864
+ const exitCode = await executePolicyOnlyMode(options, diffFiles, shouldIgnore, projectRoot, config, client, source, orgGovernanceSettings, aiLogSigningKey, aiLogSigner);
737
865
  const changedFiles = diffFiles.map((f) => f.path);
738
866
  const verdict = exitCode === 2 ? 'FAIL' : exitCode === 1 ? 'WARN' : 'PASS';
739
867
  recordVerifyEvent(verdict, `policy_only_source=${source};exit=${exitCode}`, changedFiles);
@@ -824,6 +952,7 @@ async function verifyCommand(options) {
824
952
  }
825
953
  // Track if scope guard passed - this takes priority over AI grading
826
954
  let scopeGuardPassed = false;
955
+ let governanceResult = null;
827
956
  try {
828
957
  // Step A: Get Modified Files (already have from diffFiles)
829
958
  const modifiedFiles = diffFiles.map(f => f.path);
@@ -831,10 +960,29 @@ async function verifyCommand(options) {
831
960
  const planData = await client.getPlan(finalPlanId);
832
961
  // Extract original intent from plan (for constraint checking)
833
962
  const originalIntent = planData.intent || '';
963
+ const planTitle = typeof planData.content.title === 'string'
964
+ ? planData.content.title?.trim()
965
+ : '';
966
+ const planSummary = typeof planData.content.summary === 'string' ? planData.content.summary.trim() : '';
967
+ const governanceTask = planTitle || planSummary || originalIntent || 'Plan verification';
834
968
  // Get approved files from plan (only files with action CREATE or MODIFY)
835
969
  const planFiles = planData.content.files
836
970
  .filter(f => f.action === 'CREATE' || f.action === 'MODIFY')
837
971
  .map(f => f.path);
972
+ const planDependencies = Array.isArray(planData.content.dependencies)
973
+ ? planData.content.dependencies.filter((item) => typeof item === 'string')
974
+ : [];
975
+ governanceResult = (0, governance_1.evaluateGovernance)({
976
+ projectRoot,
977
+ task: governanceTask,
978
+ expectedFiles: planFiles,
979
+ expectedDependencies: planDependencies,
980
+ diffFiles,
981
+ contextCandidates: planFiles,
982
+ orgGovernance: orgGovernanceSettings,
983
+ signingKey: aiLogSigningKey,
984
+ signer: aiLogSigner,
985
+ });
838
986
  // Get sessionId from state file (.neurcode/state.json) first, then fallback to config
839
987
  // Fallback to sessionId from plan if not in state/config
840
988
  // This is the session_id string needed to fetch the session
@@ -903,6 +1051,9 @@ async function verifyCommand(options) {
903
1051
  scopeGuardPassed: false,
904
1052
  mode: 'plan_enforced',
905
1053
  policyOnly: false,
1054
+ ...(governanceResult
1055
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1056
+ : {}),
906
1057
  };
907
1058
  // CRITICAL: Print JSON first, then exit
908
1059
  console.log(JSON.stringify(jsonOutput, null, 2));
@@ -922,6 +1073,9 @@ async function verifyCommand(options) {
922
1073
  filteredViolations.forEach(file => {
923
1074
  console.log(chalk.dim(` neurcode allow ${file}`));
924
1075
  });
1076
+ if (governanceResult) {
1077
+ displayGovernanceInsights(governanceResult, { explain: options.explain });
1078
+ }
925
1079
  console.log('');
926
1080
  process.exit(1);
927
1081
  }
@@ -1026,6 +1180,9 @@ async function verifyCommand(options) {
1026
1180
  scopeGuardPassed,
1027
1181
  mode: 'plan_enforced',
1028
1182
  policyOnly: false,
1183
+ ...(governanceResult
1184
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1185
+ : {}),
1029
1186
  policyLock: {
1030
1187
  enforced: true,
1031
1188
  matched: false,
@@ -1079,6 +1236,9 @@ async function verifyCommand(options) {
1079
1236
  mode: 'plan_enforced',
1080
1237
  policyOnly: false,
1081
1238
  tier: 'FREE',
1239
+ ...(governanceResult
1240
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1241
+ : {}),
1082
1242
  policyLock: {
1083
1243
  enforced: policyLockEvaluation.enforced,
1084
1244
  matched: policyLockEvaluation.matched,
@@ -1309,6 +1469,9 @@ async function verifyCommand(options) {
1309
1469
  totalPlannedFiles: verifyResult.totalPlannedFiles,
1310
1470
  mode: 'plan_enforced',
1311
1471
  policyOnly: false,
1472
+ ...(governanceResult
1473
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1474
+ : {}),
1312
1475
  policyLock: {
1313
1476
  enforced: policyLockEvaluation.enforced,
1314
1477
  matched: policyLockEvaluation.matched,
@@ -1347,8 +1510,10 @@ async function verifyCommand(options) {
1347
1510
  })),
1348
1511
  ];
1349
1512
  // Report in background (don't await to avoid blocking JSON output)
1350
- reportVerification(grade, violations, verifyResult, config.apiKey, config.apiUrl, projectId || undefined, true // jsonMode = true
1351
- ).catch(() => {
1513
+ reportVerification(grade, violations, verifyResult, config.apiKey, config.apiUrl, projectId || undefined, true, // jsonMode = true
1514
+ governanceResult
1515
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1516
+ : undefined).catch(() => {
1352
1517
  // Error already logged in reportVerification
1353
1518
  });
1354
1519
  }
@@ -1376,6 +1541,9 @@ async function verifyCommand(options) {
1376
1541
  bloatFiles: displayBloatFiles,
1377
1542
  bloatCount: displayBloatFiles.length,
1378
1543
  }, policyViolations);
1544
+ if (governanceResult) {
1545
+ displayGovernanceInsights(governanceResult, { explain: options.explain });
1546
+ }
1379
1547
  if (policyExceptionsSummary.suppressed > 0) {
1380
1548
  console.log(chalk.yellow(`\nāš ļø Policy exceptions applied: ${policyExceptionsSummary.suppressed}`));
1381
1549
  if (policyExceptionsSummary.matchedExceptionIds.length > 0) {
@@ -1421,8 +1589,10 @@ async function verifyCommand(options) {
1421
1589
  message: v.message,
1422
1590
  })),
1423
1591
  ];
1424
- await reportVerification(grade, violations, verifyResult, config.apiKey, config.apiUrl, projectId || undefined, false // jsonMode = false
1425
- );
1592
+ await reportVerification(grade, violations, verifyResult, config.apiKey, config.apiUrl, projectId || undefined, false, // jsonMode = false
1593
+ governanceResult
1594
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1595
+ : undefined);
1426
1596
  }
1427
1597
  }
1428
1598
  // Governance override: keep PASS only when scope guard passes and failure is due
@@ -1613,7 +1783,7 @@ function collectCIContext() {
1613
1783
  /**
1614
1784
  * Report verification results to Neurcode Cloud
1615
1785
  */
1616
- async function reportVerification(grade, violations, verifyResult, apiKey, apiUrl, projectId, jsonMode) {
1786
+ async function reportVerification(grade, violations, verifyResult, apiKey, apiUrl, projectId, jsonMode, governance) {
1617
1787
  try {
1618
1788
  const ciContext = collectCIContext();
1619
1789
  const payload = {
@@ -1629,6 +1799,7 @@ async function reportVerification(grade, violations, verifyResult, apiKey, apiUr
1629
1799
  branch: ciContext.branch,
1630
1800
  workflowRunId: ciContext.workflowRunId,
1631
1801
  projectId,
1802
+ governance,
1632
1803
  };
1633
1804
  const response = await fetch(`${apiUrl}/api/v1/action/verifications`, {
1634
1805
  method: 'POST',
@@ -1657,6 +1828,68 @@ async function reportVerification(grade, violations, verifyResult, apiKey, apiUr
1657
1828
  }
1658
1829
  }
1659
1830
  }
1831
+ function buildGovernancePayload(governance, orgGovernanceSettings) {
1832
+ return {
1833
+ contextPolicy: governance.contextPolicy,
1834
+ blastRadius: governance.blastRadius,
1835
+ suspiciousChange: governance.suspiciousChange,
1836
+ changeJustification: governance.changeJustification,
1837
+ governanceDecision: governance.governanceDecision,
1838
+ aiChangeLog: {
1839
+ path: governance.aiChangeLogPath,
1840
+ auditPath: governance.aiChangeLogAuditPath,
1841
+ integrity: governance.aiChangeLogIntegrity,
1842
+ },
1843
+ policySources: governance.policySources,
1844
+ orgGovernance: orgGovernanceSettings
1845
+ ? {
1846
+ requireSignedAiLogs: orgGovernanceSettings.requireSignedAiLogs,
1847
+ requireManualApproval: orgGovernanceSettings.requireManualApproval,
1848
+ minimumManualApprovals: orgGovernanceSettings.minimumManualApprovals,
1849
+ updatedAt: orgGovernanceSettings.updatedAt || null,
1850
+ }
1851
+ : null,
1852
+ };
1853
+ }
1854
+ function displayGovernanceInsights(governance, options = {}) {
1855
+ const maxUnexpectedFiles = options.maxUnexpectedFiles ?? 20;
1856
+ const decision = governance.governanceDecision;
1857
+ console.log(chalk.bold.white('\nBlast Radius:'));
1858
+ console.log(chalk.dim(` Files touched: ${governance.blastRadius.filesChanged}`));
1859
+ console.log(chalk.dim(` Functions impacted: ${governance.blastRadius.functionsAffected}`));
1860
+ console.log(chalk.dim(` Modules impacted: ${governance.blastRadius.modulesAffected.join(', ') || 'none'}`));
1861
+ if (governance.blastRadius.dependenciesAdded.length > 0) {
1862
+ console.log(chalk.dim(` Dependencies added: ${governance.blastRadius.dependenciesAdded.join(', ')}`));
1863
+ }
1864
+ console.log(chalk.dim(` Risk level: ${governance.blastRadius.riskScore.toUpperCase()}`));
1865
+ console.log(chalk.dim(` Governance decision: ${decision.decision.toUpperCase().replace('_', ' ')} | Avg relevance: ${decision.averageRelevanceScore}`));
1866
+ console.log(chalk.dim(` Policy source: ${governance.policySources.mode}${governance.policySources.orgPolicy ? ' (org + local)' : ' (local)'}`));
1867
+ console.log(governance.aiChangeLogIntegrity.valid
1868
+ ? chalk.dim(` AI change-log integrity: valid (${governance.aiChangeLogIntegrity.signed ? 'signed' : 'unsigned'})`)
1869
+ : chalk.red(` AI change-log integrity: invalid (${governance.aiChangeLogIntegrity.issues.join('; ') || 'unknown'})`));
1870
+ if (governance.suspiciousChange.flagged) {
1871
+ console.log(chalk.red('\nSuspicious Change Detected'));
1872
+ console.log(chalk.red(` Plan expected files: ${governance.suspiciousChange.expectedFiles} | AI modified files: ${governance.suspiciousChange.actualFiles}`));
1873
+ governance.suspiciousChange.unexpectedFiles.slice(0, maxUnexpectedFiles).forEach((filePath) => {
1874
+ console.log(chalk.red(` • ${filePath}`));
1875
+ });
1876
+ console.log(chalk.red(` Confidence: ${governance.suspiciousChange.confidence}`));
1877
+ }
1878
+ if (decision.lowRelevanceFiles.length > 0) {
1879
+ console.log(chalk.yellow('\nLow Relevance Files'));
1880
+ decision.lowRelevanceFiles.slice(0, 10).forEach((item) => {
1881
+ console.log(chalk.yellow(` • ${item.file} (score ${item.relevanceScore}, ${item.planLink.replace('_', ' ')})`));
1882
+ });
1883
+ }
1884
+ if (options.explain) {
1885
+ console.log(chalk.bold.white('\nAI Change Justification:'));
1886
+ console.log(chalk.dim(` Task: ${governance.changeJustification.task}`));
1887
+ governance.changeJustification.changes.forEach((item) => {
1888
+ const relevance = typeof item.relevanceScore === 'number' ? ` [score ${item.relevanceScore}]` : '';
1889
+ console.log(chalk.dim(` • ${item.file} — ${item.reason}${relevance}`));
1890
+ });
1891
+ }
1892
+ }
1660
1893
  /**
1661
1894
  * Display verification results in a formatted report card
1662
1895
  */