@neurcode-ai/cli 0.9.36 → 0.9.38

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 (49) hide show
  1. package/README.md +4 -2
  2. package/dist/api-client.d.ts +300 -1
  3. package/dist/api-client.d.ts.map +1 -1
  4. package/dist/api-client.js +225 -9
  5. package/dist/api-client.js.map +1 -1
  6. package/dist/commands/audit.d.ts +3 -0
  7. package/dist/commands/audit.d.ts.map +1 -0
  8. package/dist/commands/audit.js +133 -0
  9. package/dist/commands/audit.js.map +1 -0
  10. package/dist/commands/contract.d.ts +3 -0
  11. package/dist/commands/contract.d.ts.map +1 -0
  12. package/dist/commands/contract.js +235 -0
  13. package/dist/commands/contract.js.map +1 -0
  14. package/dist/commands/feedback.d.ts +3 -0
  15. package/dist/commands/feedback.d.ts.map +1 -0
  16. package/dist/commands/feedback.js +208 -0
  17. package/dist/commands/feedback.js.map +1 -0
  18. package/dist/commands/plan.d.ts.map +1 -1
  19. package/dist/commands/plan.js +19 -3
  20. package/dist/commands/plan.js.map +1 -1
  21. package/dist/commands/policy.d.ts.map +1 -1
  22. package/dist/commands/policy.js +329 -6
  23. package/dist/commands/policy.js.map +1 -1
  24. package/dist/commands/remediate.d.ts +17 -0
  25. package/dist/commands/remediate.d.ts.map +1 -0
  26. package/dist/commands/remediate.js +252 -0
  27. package/dist/commands/remediate.js.map +1 -0
  28. package/dist/commands/ship.d.ts.map +1 -1
  29. package/dist/commands/ship.js +67 -14
  30. package/dist/commands/ship.js.map +1 -1
  31. package/dist/commands/verify.d.ts +12 -0
  32. package/dist/commands/verify.d.ts.map +1 -1
  33. package/dist/commands/verify.js +477 -13
  34. package/dist/commands/verify.js.map +1 -1
  35. package/dist/index.js +60 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/utils/artifact-signature.d.ts +34 -0
  38. package/dist/utils/artifact-signature.d.ts.map +1 -0
  39. package/dist/utils/artifact-signature.js +229 -0
  40. package/dist/utils/artifact-signature.js.map +1 -0
  41. package/dist/utils/change-contract.d.ts +2 -0
  42. package/dist/utils/change-contract.d.ts.map +1 -1
  43. package/dist/utils/change-contract.js +21 -1
  44. package/dist/utils/change-contract.js.map +1 -1
  45. package/dist/utils/policy-compiler.d.ts +2 -0
  46. package/dist/utils/policy-compiler.d.ts.map +1 -1
  47. package/dist/utils/policy-compiler.js +15 -0
  48. package/dist/utils/policy-compiler.js.map +1 -1
  49. package/package.json +7 -7
@@ -44,6 +44,7 @@ const git_1 = require("../utils/git");
44
44
  const diff_parser_1 = require("@neurcode-ai/diff-parser");
45
45
  const policy_engine_1 = require("@neurcode-ai/policy-engine");
46
46
  const governance_runtime_1 = require("@neurcode-ai/governance-runtime");
47
+ const contracts_1 = require("@neurcode-ai/contracts");
47
48
  const config_1 = require("../config");
48
49
  const api_client_1 = require("../api-client");
49
50
  const path_1 = require("path");
@@ -63,6 +64,7 @@ const policy_audit_1 = require("../utils/policy-audit");
63
64
  const governance_1 = require("../utils/governance");
64
65
  const policy_compiler_1 = require("../utils/policy-compiler");
65
66
  const change_contract_1 = require("../utils/change-contract");
67
+ const artifact_signature_1 = require("../utils/artifact-signature");
66
68
  const policy_1 = require("@neurcode-ai/policy");
67
69
  // Import chalk with fallback
68
70
  let chalk;
@@ -85,6 +87,155 @@ catch {
85
87
  };
86
88
  }
87
89
  ;
90
+ function toArtifactSignatureSummary(status) {
91
+ return {
92
+ required: status.required,
93
+ present: status.present,
94
+ valid: status.valid,
95
+ keyId: status.keyId,
96
+ verifiedWithKeyId: status.verifiedWithKeyId,
97
+ issues: [...status.issues],
98
+ };
99
+ }
100
+ function resolveCliComponentVersion() {
101
+ const fromEnv = process.env.NEURCODE_CLI_VERSION || process.env.npm_package_version;
102
+ if (fromEnv && fromEnv.trim()) {
103
+ return fromEnv.trim();
104
+ }
105
+ try {
106
+ const packagePath = (0, path_1.join)(__dirname, '../../package.json');
107
+ const raw = (0, fs_1.readFileSync)(packagePath, 'utf-8');
108
+ const parsed = JSON.parse(raw);
109
+ if (typeof parsed.version === 'string' && parsed.version.trim()) {
110
+ return parsed.version.trim();
111
+ }
112
+ }
113
+ catch {
114
+ // Ignore and fall back.
115
+ }
116
+ return '0.0.0';
117
+ }
118
+ const CLI_COMPONENT_VERSION = resolveCliComponentVersion();
119
+ function asCompatibilityRecord(value) {
120
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
121
+ return null;
122
+ }
123
+ return value;
124
+ }
125
+ async function probeApiRuntimeCompatibility(apiUrl) {
126
+ const normalizedApiUrl = apiUrl.replace(/\/$/, '');
127
+ const healthUrl = `${normalizedApiUrl}/health`;
128
+ const controller = new AbortController();
129
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
130
+ try {
131
+ const response = await fetch(healthUrl, {
132
+ method: 'GET',
133
+ signal: controller.signal,
134
+ headers: {
135
+ 'User-Agent': 'neurcode-cli-verify/compat-probe',
136
+ },
137
+ });
138
+ if (!response.ok) {
139
+ return {
140
+ healthUrl,
141
+ apiVersion: null,
142
+ status: 'warn',
143
+ messages: [`Health endpoint returned status ${response.status}; skipping runtime compatibility handshake.`],
144
+ };
145
+ }
146
+ const payload = (await response.json().catch(() => ({})));
147
+ const compatibility = asCompatibilityRecord(payload.compatibility);
148
+ const apiVersionFromHealth = typeof payload.version === 'string' && payload.version.trim()
149
+ ? payload.version.trim()
150
+ : null;
151
+ if (!compatibility) {
152
+ return {
153
+ healthUrl,
154
+ apiVersion: apiVersionFromHealth,
155
+ status: 'warn',
156
+ messages: ['API health payload is missing compatibility metadata.'],
157
+ };
158
+ }
159
+ const contractId = typeof compatibility.contractId === 'string' ? compatibility.contractId.trim() : '';
160
+ const runtimeContractVersion = typeof compatibility.runtimeContractVersion === 'string'
161
+ ? compatibility.runtimeContractVersion.trim()
162
+ : '';
163
+ const cliJsonContractVersion = typeof compatibility.cliJsonContractVersion === 'string'
164
+ ? compatibility.cliJsonContractVersion.trim()
165
+ : '';
166
+ const component = typeof compatibility.component === 'string' ? compatibility.component.trim() : '';
167
+ const componentVersion = typeof compatibility.componentVersion === 'string'
168
+ ? compatibility.componentVersion.trim()
169
+ : '';
170
+ const minimumPeerVersions = asCompatibilityRecord(compatibility.minimumPeerVersions) || {};
171
+ const apiRequiresCli = typeof minimumPeerVersions.cli === 'string' && minimumPeerVersions.cli.trim()
172
+ ? minimumPeerVersions.cli.trim()
173
+ : undefined;
174
+ const errors = [];
175
+ if (contractId !== contracts_1.RUNTIME_COMPATIBILITY_CONTRACT_ID) {
176
+ errors.push(`API compatibility contractId mismatch (expected ${contracts_1.RUNTIME_COMPATIBILITY_CONTRACT_ID}, got ${contractId || 'missing'}).`);
177
+ }
178
+ if (runtimeContractVersion !== contracts_1.RUNTIME_COMPATIBILITY_CONTRACT_VERSION) {
179
+ errors.push(`API runtimeContractVersion mismatch (expected ${contracts_1.RUNTIME_COMPATIBILITY_CONTRACT_VERSION}, got ${runtimeContractVersion || 'missing'}).`);
180
+ }
181
+ if (cliJsonContractVersion !== contracts_1.CLI_JSON_CONTRACT_VERSION) {
182
+ errors.push(`API cliJsonContractVersion mismatch (expected ${contracts_1.CLI_JSON_CONTRACT_VERSION}, got ${cliJsonContractVersion || 'missing'}).`);
183
+ }
184
+ if (component !== 'api') {
185
+ errors.push(`API compatibility payload component must be "api" (received ${component || 'missing'}).`);
186
+ }
187
+ const resolvedApiVersion = componentVersion || apiVersionFromHealth;
188
+ const minimumApiForCli = (0, contracts_1.getMinimumCompatiblePeerVersion)('cli', 'api');
189
+ if (minimumApiForCli && resolvedApiVersion) {
190
+ const cliRequiresApi = (0, contracts_1.isSemverAtLeast)(resolvedApiVersion, minimumApiForCli);
191
+ if (cliRequiresApi === null) {
192
+ errors.push(`Unable to compare API version "${resolvedApiVersion}" against required minimum "${minimumApiForCli}".`);
193
+ }
194
+ else if (!cliRequiresApi) {
195
+ errors.push(`API version ${resolvedApiVersion} is below CLI required minimum ${minimumApiForCli}.`);
196
+ }
197
+ }
198
+ if (apiRequiresCli) {
199
+ const apiRequiresThisCli = (0, contracts_1.isSemverAtLeast)(CLI_COMPONENT_VERSION, apiRequiresCli);
200
+ if (apiRequiresThisCli === null) {
201
+ errors.push(`Unable to compare CLI version "${CLI_COMPONENT_VERSION}" against API required minimum "${apiRequiresCli}".`);
202
+ }
203
+ else if (!apiRequiresThisCli) {
204
+ errors.push(`CLI version ${CLI_COMPONENT_VERSION} is below API required minimum ${apiRequiresCli}.`);
205
+ }
206
+ }
207
+ if (errors.length > 0) {
208
+ return {
209
+ healthUrl,
210
+ apiVersion: resolvedApiVersion || null,
211
+ status: 'error',
212
+ messages: errors,
213
+ };
214
+ }
215
+ return {
216
+ healthUrl,
217
+ apiVersion: resolvedApiVersion || null,
218
+ status: 'ok',
219
+ messages: [],
220
+ };
221
+ }
222
+ catch (error) {
223
+ const message = error instanceof Error && error.name === 'AbortError'
224
+ ? 'Health endpoint timed out after 5s.'
225
+ : error instanceof Error
226
+ ? error.message
227
+ : 'Unknown error';
228
+ return {
229
+ healthUrl,
230
+ apiVersion: null,
231
+ status: 'warn',
232
+ messages: [`Runtime compatibility probe failed: ${message}`],
233
+ };
234
+ }
235
+ finally {
236
+ clearTimeout(timeoutId);
237
+ }
238
+ }
88
239
  /**
89
240
  * Check if a file path should be excluded from verification analysis
90
241
  * Excludes internal/system files that should not count towards plan adherence
@@ -391,6 +542,94 @@ function resolveAuditIntegrityStatus(requireIntegrity, auditIntegrity) {
391
542
  issues,
392
543
  };
393
544
  }
545
+ function describePolicyExceptionSource(mode) {
546
+ switch (mode) {
547
+ case 'org':
548
+ return 'org control plane';
549
+ case 'org_fallback_local':
550
+ return 'local file fallback (org unavailable)';
551
+ case 'local':
552
+ default:
553
+ return 'local file';
554
+ }
555
+ }
556
+ function pickExceptionIdentity(userId, email, allowSet) {
557
+ const normalizedUserId = typeof userId === 'string' ? userId.trim() : '';
558
+ const normalizedEmail = typeof email === 'string' ? email.trim() : '';
559
+ if (allowSet.size > 0) {
560
+ if (normalizedEmail && allowSet.has(normalizedEmail.toLowerCase())) {
561
+ return normalizedEmail;
562
+ }
563
+ if (normalizedUserId && allowSet.has(normalizedUserId.toLowerCase())) {
564
+ return normalizedUserId;
565
+ }
566
+ }
567
+ if (normalizedEmail)
568
+ return normalizedEmail;
569
+ if (normalizedUserId)
570
+ return normalizedUserId;
571
+ return 'unknown';
572
+ }
573
+ function mapOrgPolicyExceptionToLocalEntry(exception, allowSet) {
574
+ const createdBy = pickExceptionIdentity(exception.createdBy, exception.requestedByEmail, allowSet);
575
+ const requestedBy = pickExceptionIdentity(exception.requestedBy, exception.requestedByEmail, allowSet);
576
+ return {
577
+ id: exception.id,
578
+ rulePattern: exception.rulePattern,
579
+ filePattern: exception.filePattern,
580
+ reason: exception.reason,
581
+ ticket: exception.ticket,
582
+ createdAt: exception.createdAt,
583
+ createdBy,
584
+ requestedBy,
585
+ expiresAt: exception.expiresAt,
586
+ severity: exception.severity,
587
+ active: exception.active === true
588
+ && exception.workflowState !== 'revoked'
589
+ && exception.workflowState !== 'rejected',
590
+ approvals: (exception.approvals || []).map((approval) => ({
591
+ approver: pickExceptionIdentity(approval.approverUserId, approval.approverEmail, allowSet),
592
+ approvedAt: approval.createdAt,
593
+ comment: approval.note || null,
594
+ })),
595
+ };
596
+ }
597
+ async function resolveEffectivePolicyExceptions(input) {
598
+ const localExceptions = (0, policy_exceptions_1.readPolicyExceptions)(input.projectRoot);
599
+ if (!input.useOrgControlPlane) {
600
+ return {
601
+ mode: 'local',
602
+ exceptions: localExceptions,
603
+ localConfigured: localExceptions.length,
604
+ orgConfigured: 0,
605
+ warning: null,
606
+ };
607
+ }
608
+ try {
609
+ const orgExceptions = await input.client.listOrgPolicyExceptions({ limit: 250 });
610
+ const allowSet = new Set((input.governance.exceptionApprovals.allowedApprovers || []).map((item) => item.toLowerCase()));
611
+ const mapped = orgExceptions
612
+ .map((entry) => mapOrgPolicyExceptionToLocalEntry(entry, allowSet))
613
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt));
614
+ return {
615
+ mode: 'org',
616
+ exceptions: mapped,
617
+ localConfigured: localExceptions.length,
618
+ orgConfigured: mapped.length,
619
+ warning: null,
620
+ };
621
+ }
622
+ catch (error) {
623
+ const message = error instanceof Error ? error.message : 'Unknown error';
624
+ return {
625
+ mode: 'org_fallback_local',
626
+ exceptions: localExceptions,
627
+ localConfigured: localExceptions.length,
628
+ orgConfigured: 0,
629
+ warning: `Org policy exceptions unavailable; falling back to local exceptions (${message})`,
630
+ };
631
+ }
632
+ }
394
633
  async function recordVerificationIfRequested(options, config, payload) {
395
634
  if (!options.record) {
396
635
  return;
@@ -746,10 +985,20 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
746
985
  const policyResult = (0, policy_engine_1.evaluateRules)(diffFilesForPolicy, effectiveRules.allRules);
747
986
  policyViolations = (policyResult.violations || []);
748
987
  policyViolations = policyViolations.filter((v) => !ignoreFilter(v.file));
749
- const governance = (0, policy_governance_1.readPolicyGovernanceConfig)(projectRoot);
988
+ const localPolicyGovernance = (0, policy_governance_1.readPolicyGovernanceConfig)(projectRoot);
989
+ const governance = (0, policy_governance_1.mergePolicyGovernanceWithOrgOverrides)(localPolicyGovernance, orgGovernanceSettings?.policyGovernance);
750
990
  const auditIntegrity = (0, policy_audit_1.verifyPolicyAuditIntegrity)(projectRoot);
751
991
  const auditIntegrityStatus = resolveAuditIntegrityStatus(governance.audit.requireIntegrity, auditIntegrity);
752
- const configuredPolicyExceptions = (0, policy_exceptions_1.readPolicyExceptions)(projectRoot);
992
+ const policyExceptionResolution = await resolveEffectivePolicyExceptions({
993
+ client,
994
+ projectRoot,
995
+ useOrgControlPlane: Boolean(config.apiKey),
996
+ governance,
997
+ });
998
+ if (policyExceptionResolution.warning && !options.json) {
999
+ console.log(chalk.dim(` ${policyExceptionResolution.warning}`));
1000
+ }
1001
+ const configuredPolicyExceptions = policyExceptionResolution.exceptions;
753
1002
  const exceptionDecision = (0, policy_exceptions_1.applyPolicyExceptions)(policyViolations, configuredPolicyExceptions, {
754
1003
  requireApproval: governance.exceptionApprovals.required,
755
1004
  minApprovals: governance.exceptionApprovals.minApprovals,
@@ -803,6 +1052,10 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
803
1052
  ? `Policy violations: ${policyViolations.map((v) => `${v.file}: ${v.message || v.rule}`).join('; ')}`
804
1053
  : 'Policy check completed';
805
1054
  const policyExceptionsSummary = {
1055
+ sourceMode: policyExceptionResolution.mode,
1056
+ sourceWarning: policyExceptionResolution.warning,
1057
+ localConfigured: policyExceptionResolution.localConfigured,
1058
+ orgConfigured: policyExceptionResolution.orgConfigured,
806
1059
  configured: configuredPolicyExceptions.length,
807
1060
  active: exceptionDecision.activeExceptions.length,
808
1061
  usable: exceptionDecision.usableExceptions.length,
@@ -885,6 +1138,7 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
885
1138
  console.log(chalk.red(` • ${v.file}: ${v.message || v.rule}`));
886
1139
  });
887
1140
  }
1141
+ console.log(chalk.dim(` Policy exceptions source: ${describePolicyExceptionSource(policyExceptionsSummary.sourceMode)}`));
888
1142
  if (policyExceptionsSummary.suppressed > 0) {
889
1143
  console.log(chalk.yellow(` Policy exceptions applied: ${policyExceptionsSummary.suppressed}`));
890
1144
  }
@@ -927,12 +1181,48 @@ async function verifyCommand(options) {
927
1181
  };
928
1182
  const enforceChangeContract = options.enforceChangeContract === true ||
929
1183
  isEnabledFlag(process.env.NEURCODE_VERIFY_ENFORCE_CHANGE_CONTRACT);
930
- const strictArtifactMode = options.strictArtifacts === true ||
1184
+ const explicitStrictArtifactMode = options.strictArtifacts === true ||
931
1185
  isEnabledFlag(process.env.NEURCODE_VERIFY_STRICT_ARTIFACTS) ||
932
1186
  isEnabledFlag(process.env.NEURCODE_ENTERPRISE_MODE);
1187
+ const ciEnterpriseDefaultStrict = process.env.CI === 'true'
1188
+ && !isEnabledFlag(process.env.NEURCODE_VERIFY_ALLOW_NON_STRICT_CI)
1189
+ && Boolean(options.apiKey || process.env.NEURCODE_API_KEY);
1190
+ const strictArtifactMode = explicitStrictArtifactMode || ciEnterpriseDefaultStrict;
1191
+ const signingConfig = resolveGovernanceSigningConfig();
1192
+ const aiLogSigningKey = signingConfig.signingKey;
1193
+ const aiLogSigningKeyId = signingConfig.signingKeyId;
1194
+ const aiLogSigningKeys = signingConfig.signingKeys;
1195
+ const aiLogSigner = signingConfig.signer;
1196
+ const hasSigningMaterial = Boolean(aiLogSigningKey) || Object.keys(aiLogSigningKeys).length > 0;
1197
+ const allowUnsignedArtifacts = isEnabledFlag(process.env.NEURCODE_VERIFY_ALLOW_UNSIGNED_ARTIFACTS)
1198
+ || isEnabledFlag(process.env.NEURCODE_VERIFY_DISABLE_SIGNED_ARTIFACTS);
1199
+ const requireSignedArtifacts = options.requireSignedArtifacts === true
1200
+ || isEnabledFlag(process.env.NEURCODE_VERIFY_REQUIRE_SIGNED_ARTIFACTS)
1201
+ || (!allowUnsignedArtifacts && strictArtifactMode && hasSigningMaterial);
933
1202
  const changeContractRead = (0, change_contract_1.readChangeContract)(projectRoot, options.changeContract);
934
1203
  const compiledPolicyRead = (0, policy_compiler_1.readCompiledPolicyArtifact)(projectRoot, options.compiledPolicy);
935
1204
  let compiledPolicyMetadata = resolveCompiledPolicyMetadata(compiledPolicyRead.artifact, compiledPolicyRead.exists ? compiledPolicyRead.path : null);
1205
+ const compiledPolicySignatureStatus = compiledPolicyRead.artifact
1206
+ ? (0, artifact_signature_1.verifyGovernanceArtifactSignature)({
1207
+ artifact: compiledPolicyRead.artifact,
1208
+ requireSigned: requireSignedArtifacts,
1209
+ signingKey: aiLogSigningKey,
1210
+ signingKeyId: aiLogSigningKeyId,
1211
+ signingKeys: aiLogSigningKeys,
1212
+ })
1213
+ : null;
1214
+ if (compiledPolicyMetadata && compiledPolicySignatureStatus) {
1215
+ compiledPolicyMetadata.signature = toArtifactSignatureSummary(compiledPolicySignatureStatus);
1216
+ }
1217
+ const changeContractSignatureStatus = changeContractRead.contract
1218
+ ? (0, artifact_signature_1.verifyGovernanceArtifactSignature)({
1219
+ artifact: changeContractRead.contract,
1220
+ requireSigned: requireSignedArtifacts,
1221
+ signingKey: aiLogSigningKey,
1222
+ signingKeyId: aiLogSigningKeyId,
1223
+ signingKeys: aiLogSigningKeys,
1224
+ })
1225
+ : null;
936
1226
  let changeContractSummary = {
937
1227
  path: changeContractRead.path,
938
1228
  exists: changeContractRead.exists,
@@ -940,6 +1230,9 @@ async function verifyCommand(options) {
940
1230
  valid: changeContractRead.contract ? null : changeContractRead.exists ? false : null,
941
1231
  planId: changeContractRead.contract?.planId || null,
942
1232
  contractId: changeContractRead.contract?.contractId || null,
1233
+ signature: changeContractSignatureStatus
1234
+ ? toArtifactSignatureSummary(changeContractSignatureStatus)
1235
+ : undefined,
943
1236
  violations: changeContractRead.error
944
1237
  ? [
945
1238
  {
@@ -961,6 +1254,16 @@ async function verifyCommand(options) {
961
1254
  ? `Change contract artifact invalid (${changeContractRead.error})`
962
1255
  : `Change contract artifact missing (${changeContractRead.path})`);
963
1256
  }
1257
+ if (compiledPolicySignatureStatus
1258
+ && !compiledPolicySignatureStatus.valid
1259
+ && (requireSignedArtifacts || compiledPolicySignatureStatus.present)) {
1260
+ strictErrors.push(`Compiled policy artifact signature validation failed (${compiledPolicySignatureStatus.issues.join('; ') || 'unknown issue'})`);
1261
+ }
1262
+ if (changeContractSignatureStatus
1263
+ && !changeContractSignatureStatus.valid
1264
+ && (requireSignedArtifacts || changeContractSignatureStatus.present)) {
1265
+ strictErrors.push(`Change contract artifact signature validation failed (${changeContractSignatureStatus.issues.join('; ') || 'unknown issue'})`);
1266
+ }
964
1267
  if (strictErrors.length > 0) {
965
1268
  const message = `Strict artifact mode requires deterministic compiled-policy + change-contract artifacts.\n- ${strictErrors.join('\n- ')}`;
966
1269
  if (options.json) {
@@ -995,7 +1298,60 @@ async function verifyCommand(options) {
995
1298
  strictErrors.forEach((entry) => {
996
1299
  console.log(chalk.red(` • ${entry}`));
997
1300
  });
998
- console.log(chalk.dim('\nSet --compiled-policy and --change-contract with valid artifacts before verify.\n'));
1301
+ console.log(chalk.dim('\nSet --compiled-policy and --change-contract with valid artifacts before verify.'));
1302
+ if (requireSignedArtifacts) {
1303
+ console.log(chalk.dim('Enable signing keys via NEURCODE_GOVERNANCE_SIGNING_KEY or NEURCODE_GOVERNANCE_SIGNING_KEYS to generate signed artifacts.\n'));
1304
+ }
1305
+ else {
1306
+ console.log('');
1307
+ }
1308
+ }
1309
+ process.exit(2);
1310
+ }
1311
+ }
1312
+ if (!strictArtifactMode && requireSignedArtifacts) {
1313
+ const signatureErrors = [];
1314
+ if (compiledPolicyRead.artifact && compiledPolicySignatureStatus && !compiledPolicySignatureStatus.valid) {
1315
+ signatureErrors.push(`Compiled policy artifact signature validation failed (${compiledPolicySignatureStatus.issues.join('; ') || 'unknown issue'})`);
1316
+ }
1317
+ if (changeContractRead.contract && changeContractSignatureStatus && !changeContractSignatureStatus.valid) {
1318
+ signatureErrors.push(`Change contract artifact signature validation failed (${changeContractSignatureStatus.issues.join('; ') || 'unknown issue'})`);
1319
+ }
1320
+ if (signatureErrors.length > 0) {
1321
+ const message = `Signed artifact enforcement failed.\n- ${signatureErrors.join('\n- ')}`;
1322
+ if (options.json) {
1323
+ emitVerifyJson({
1324
+ grade: 'F',
1325
+ score: 0,
1326
+ verdict: 'FAIL',
1327
+ violations: signatureErrors.map((entry) => ({
1328
+ file: entry.toLowerCase().includes('compiled policy') ? compiledPolicyRead.path : changeContractRead.path,
1329
+ rule: 'signed_artifacts_required',
1330
+ severity: 'block',
1331
+ message: entry,
1332
+ })),
1333
+ adherenceScore: 0,
1334
+ bloatCount: 0,
1335
+ bloatFiles: [],
1336
+ plannedFilesModified: 0,
1337
+ totalPlannedFiles: 0,
1338
+ message,
1339
+ scopeGuardPassed: false,
1340
+ mode: 'signed_artifacts_required',
1341
+ policyOnly: options.policyOnly === true,
1342
+ changeContract: changeContractSummary,
1343
+ ...(compiledPolicyMetadata ? { policyCompilation: compiledPolicyMetadata } : {}),
1344
+ });
1345
+ }
1346
+ else {
1347
+ (0, scope_telemetry_1.printScopeTelemetry)(chalk, scopeTelemetry, {
1348
+ includeBlockedWarning: true,
1349
+ });
1350
+ console.log(chalk.red('\n⛔ Signed Artifact Requirements Failed'));
1351
+ signatureErrors.forEach((entry) => {
1352
+ console.log(chalk.red(` • ${entry}`));
1353
+ });
1354
+ console.log(chalk.dim('\nEnable signing keys via NEURCODE_GOVERNANCE_SIGNING_KEY or NEURCODE_GOVERNANCE_SIGNING_KEYS and regenerate artifacts.\n'));
999
1355
  }
1000
1356
  process.exit(2);
1001
1357
  }
@@ -1016,6 +1372,28 @@ async function verifyCommand(options) {
1016
1372
  else if (changeContractRead.contract) {
1017
1373
  console.log(chalk.dim(` Change contract loaded: ${changeContractRead.path}`));
1018
1374
  }
1375
+ if (compiledPolicySignatureStatus) {
1376
+ if (compiledPolicySignatureStatus.valid) {
1377
+ console.log(chalk.dim(` Compiled policy signature: valid${compiledPolicySignatureStatus.verifiedWithKeyId ? ` (key ${compiledPolicySignatureStatus.verifiedWithKeyId})` : ''}`));
1378
+ }
1379
+ else if (compiledPolicySignatureStatus.present || requireSignedArtifacts) {
1380
+ console.log(chalk.yellow(` Compiled policy signature: invalid (${compiledPolicySignatureStatus.issues.join('; ') || 'unknown issue'})`));
1381
+ }
1382
+ }
1383
+ if (changeContractSignatureStatus) {
1384
+ if (changeContractSignatureStatus.valid) {
1385
+ console.log(chalk.dim(` Change contract signature: valid${changeContractSignatureStatus.verifiedWithKeyId ? ` (key ${changeContractSignatureStatus.verifiedWithKeyId})` : ''}`));
1386
+ }
1387
+ else if (changeContractSignatureStatus.present || requireSignedArtifacts) {
1388
+ console.log(chalk.yellow(` Change contract signature: invalid (${changeContractSignatureStatus.issues.join('; ') || 'unknown issue'})`));
1389
+ }
1390
+ }
1391
+ if (ciEnterpriseDefaultStrict && !explicitStrictArtifactMode) {
1392
+ console.log(chalk.dim(' CI enterprise mode detected: strict deterministic artifact enforcement is auto-enabled (set NEURCODE_VERIFY_ALLOW_NON_STRICT_CI=1 to opt out).'));
1393
+ }
1394
+ if (requireSignedArtifacts) {
1395
+ console.log(chalk.dim(' Artifact signature enforcement: enabled (set NEURCODE_VERIFY_ALLOW_UNSIGNED_ARTIFACTS=1 to relax)'));
1396
+ }
1019
1397
  }
1020
1398
  // Load configuration
1021
1399
  const config = (0, config_1.loadConfig)();
@@ -1043,6 +1421,72 @@ async function verifyCommand(options) {
1043
1421
  // Ensure no trailing slash
1044
1422
  config.apiUrl = config.apiUrl.replace(/\/$/, '');
1045
1423
  }
1424
+ const enforceCompatibilityHandshake = isEnabledFlag(process.env.NEURCODE_VERIFY_ENFORCE_COMPAT_HANDSHAKE)
1425
+ || strictArtifactMode
1426
+ || (process.env.CI === 'true' && Boolean(config.apiKey));
1427
+ if (config.apiKey && config.apiUrl) {
1428
+ const compatibilityProbe = await probeApiRuntimeCompatibility(config.apiUrl);
1429
+ if (compatibilityProbe.status !== 'ok' && enforceCompatibilityHandshake) {
1430
+ const failureMessages = compatibilityProbe.messages.length > 0
1431
+ ? compatibilityProbe.messages
1432
+ : ['Runtime compatibility handshake did not return a successful result.'];
1433
+ const message = `Runtime compatibility handshake failed against ${compatibilityProbe.healthUrl}.\n` +
1434
+ failureMessages.map((entry) => `- ${entry}`).join('\n');
1435
+ if (options.json) {
1436
+ emitVerifyJson({
1437
+ grade: 'F',
1438
+ score: 0,
1439
+ verdict: 'FAIL',
1440
+ violations: failureMessages.map((entry) => ({
1441
+ file: 'runtime-compatibility',
1442
+ rule: 'runtime_compatibility_handshake',
1443
+ severity: 'block',
1444
+ message: entry,
1445
+ })),
1446
+ adherenceScore: 0,
1447
+ bloatCount: 0,
1448
+ bloatFiles: [],
1449
+ plannedFilesModified: 0,
1450
+ totalPlannedFiles: 0,
1451
+ message,
1452
+ scopeGuardPassed: false,
1453
+ mode: 'runtime_compatibility_failed',
1454
+ policyOnly: options.policyOnly === true,
1455
+ changeContract: changeContractSummary,
1456
+ ...(compiledPolicyMetadata ? { policyCompilation: compiledPolicyMetadata } : {}),
1457
+ });
1458
+ }
1459
+ else {
1460
+ console.log(chalk.red('\n⛔ Runtime Compatibility Handshake Failed'));
1461
+ failureMessages.forEach((entry) => {
1462
+ console.log(chalk.red(` • ${entry}`));
1463
+ });
1464
+ console.log(chalk.dim(` Health endpoint: ${compatibilityProbe.healthUrl}`));
1465
+ if (compatibilityProbe.apiVersion) {
1466
+ console.log(chalk.dim(` API version: ${compatibilityProbe.apiVersion}`));
1467
+ }
1468
+ console.log(chalk.dim(` CLI version: ${CLI_COMPONENT_VERSION}`));
1469
+ console.log(chalk.dim(' Upgrade/downgrade CLI, Action, or API to satisfy the runtime compatibility contract before running verify.\n'));
1470
+ }
1471
+ process.exit(2);
1472
+ }
1473
+ if (compatibilityProbe.status === 'error' && !options.json) {
1474
+ console.log(chalk.yellow('\n⚠️ Runtime compatibility mismatch detected (advisory mode).'));
1475
+ compatibilityProbe.messages.forEach((entry) => {
1476
+ console.log(chalk.yellow(` • ${entry}`));
1477
+ });
1478
+ }
1479
+ else if (compatibilityProbe.status === 'warn' && !options.json) {
1480
+ compatibilityProbe.messages.forEach((entry) => {
1481
+ console.log(chalk.dim(` ${entry}`));
1482
+ });
1483
+ }
1484
+ else if (compatibilityProbe.status === 'ok'
1485
+ && !options.json
1486
+ && isEnabledFlag(process.env.NEURCODE_VERIFY_VERBOSE_COMPAT_HANDSHAKE)) {
1487
+ console.log(chalk.dim(` Runtime compatibility check passed (CLI ${CLI_COMPONENT_VERSION}, API ${compatibilityProbe.apiVersion || 'unknown'})`));
1488
+ }
1489
+ }
1046
1490
  // Explicitly load config file to get sessionId and lastSessionId
1047
1491
  const configPath = (0, path_1.join)(projectRoot, 'neurcode.config.json');
1048
1492
  let configData = {};
@@ -1065,11 +1509,6 @@ async function verifyCommand(options) {
1065
1509
  orgId: (0, state_1.getOrgId)(),
1066
1510
  projectId: projectId || null,
1067
1511
  };
1068
- const signingConfig = resolveGovernanceSigningConfig();
1069
- const aiLogSigningKey = signingConfig.signingKey;
1070
- const aiLogSigningKeyId = signingConfig.signingKeyId;
1071
- const aiLogSigningKeys = signingConfig.signingKeys;
1072
- const aiLogSigner = signingConfig.signer;
1073
1512
  let orgGovernanceSettings = null;
1074
1513
  if (config.apiKey) {
1075
1514
  try {
@@ -1095,7 +1534,6 @@ async function verifyCommand(options) {
1095
1534
  }
1096
1535
  }
1097
1536
  const signedLogsRequired = isSignedAiLogsRequired(orgGovernanceSettings);
1098
- const hasSigningMaterial = Boolean(aiLogSigningKey) || Object.keys(aiLogSigningKeys).length > 0;
1099
1537
  const recordVerifyEvent = (verdict, note, changedFiles, planId) => {
1100
1538
  if (!brainScope.orgId || !brainScope.projectId) {
1101
1539
  return;
@@ -1358,7 +1796,9 @@ async function verifyCommand(options) {
1358
1796
  if (options.policyOnly) {
1359
1797
  await runPolicyOnlyModeAndExit('explicit');
1360
1798
  }
1361
- const requirePlan = options.requirePlan === true || process.env.NEURCODE_VERIFY_REQUIRE_PLAN === '1';
1799
+ const requirePlan = options.requirePlan === true
1800
+ || process.env.NEURCODE_VERIFY_REQUIRE_PLAN === '1'
1801
+ || strictArtifactMode;
1362
1802
  // Get planId: Priority 1: options flag, Priority 2: state file (.neurcode/config.json), Priority 3: legacy config
1363
1803
  let planId = options.planId;
1364
1804
  if (!planId) {
@@ -1865,7 +2305,16 @@ async function verifyCommand(options) {
1865
2305
  const diffFilesForPolicy = diffFiles.filter((f) => !shouldIgnore(f.path));
1866
2306
  const policyResult = (0, policy_engine_1.evaluateRules)(diffFilesForPolicy, effectiveRules.allRules);
1867
2307
  policyViolations = policyResult.violations.filter((v) => !shouldIgnore(v.file));
1868
- const configuredPolicyExceptions = (0, policy_exceptions_1.readPolicyExceptions)(projectRoot);
2308
+ const policyExceptionResolution = await resolveEffectivePolicyExceptions({
2309
+ client,
2310
+ projectRoot,
2311
+ useOrgControlPlane: Boolean(config.apiKey),
2312
+ governance,
2313
+ });
2314
+ if (policyExceptionResolution.warning && !options.json) {
2315
+ console.log(chalk.dim(` ${policyExceptionResolution.warning}`));
2316
+ }
2317
+ const configuredPolicyExceptions = policyExceptionResolution.exceptions;
1869
2318
  const exceptionDecision = (0, policy_exceptions_1.applyPolicyExceptions)(policyViolations, configuredPolicyExceptions, {
1870
2319
  requireApproval: governance.exceptionApprovals.required,
1871
2320
  minApprovals: governance.exceptionApprovals.minApprovals,
@@ -1904,6 +2353,10 @@ async function verifyCommand(options) {
1904
2353
  }
1905
2354
  policyDecision = resolvePolicyDecisionFromViolations(policyViolations);
1906
2355
  const policyExceptionsSummary = {
2356
+ sourceMode: policyExceptionResolution.mode,
2357
+ sourceWarning: policyExceptionResolution.warning,
2358
+ localConfigured: policyExceptionResolution.localConfigured,
2359
+ orgConfigured: policyExceptionResolution.orgConfigured,
1907
2360
  configured: configuredPolicyExceptions.length,
1908
2361
  active: exceptionDecision.activeExceptions.length,
1909
2362
  usable: exceptionDecision.usableExceptions.length,
@@ -1986,6 +2439,7 @@ async function verifyCommand(options) {
1986
2439
  valid: changeContractEvaluation.valid,
1987
2440
  planId: changeContractRead.contract?.planId || null,
1988
2441
  contractId: changeContractRead.contract?.contractId || null,
2442
+ signature: changeContractSummary.signature,
1989
2443
  coverage: changeContractEvaluation.coverage,
1990
2444
  violations: changeContractEvaluation.violations.map((item) => ({
1991
2445
  code: item.code,
@@ -2065,6 +2519,9 @@ async function verifyCommand(options) {
2065
2519
  // Call verify API
2066
2520
  if (!options.json) {
2067
2521
  console.log(chalk.dim(' Sending to Neurcode API...\n'));
2522
+ if (options.asyncMode) {
2523
+ console.log(chalk.dim(' Queue-backed verification enabled (async job mode).'));
2524
+ }
2068
2525
  }
2069
2526
  try {
2070
2527
  let verifySource = 'api';
@@ -2075,7 +2532,13 @@ async function verifyCommand(options) {
2075
2532
  .map((policy) => policy.rule_text)
2076
2533
  .filter((ruleText) => typeof ruleText === 'string' && ruleText.trim().length > 0);
2077
2534
  try {
2078
- verifyResult = await client.verifyPlan(finalPlanId, diffStats, changedFiles, projectId, intentConstraintsForVerification, deterministicPolicyRules, 'api', compiledPolicyMetadata);
2535
+ verifyResult = await client.verifyPlan(finalPlanId, diffStats, changedFiles, projectId, intentConstraintsForVerification, deterministicPolicyRules, 'api', compiledPolicyMetadata, {
2536
+ async: options.asyncMode === true,
2537
+ pollIntervalMs: Number.isFinite(options.verifyJobPollMs) ? options.verifyJobPollMs : undefined,
2538
+ timeoutMs: Number.isFinite(options.verifyJobTimeoutMs) ? options.verifyJobTimeoutMs : undefined,
2539
+ idempotencyKey: options.verifyIdempotencyKey,
2540
+ maxAttempts: Number.isFinite(options.verifyJobMaxAttempts) ? options.verifyJobMaxAttempts : undefined,
2541
+ });
2079
2542
  }
2080
2543
  catch (verifyApiError) {
2081
2544
  if (planFilesForVerification.length === 0) {
@@ -2291,6 +2754,7 @@ async function verifyCommand(options) {
2291
2754
  if (governanceResult) {
2292
2755
  displayGovernanceInsights(governanceResult, { explain: options.explain });
2293
2756
  }
2757
+ console.log(chalk.dim(`\n Policy exceptions source: ${describePolicyExceptionSource(policyExceptionsSummary.sourceMode)}`));
2294
2758
  if (policyExceptionsSummary.suppressed > 0) {
2295
2759
  console.log(chalk.yellow(`\n⚠️ Policy exceptions applied: ${policyExceptionsSummary.suppressed}`));
2296
2760
  if (policyExceptionsSummary.matchedExceptionIds.length > 0) {