@neurcode-ai/cli 0.9.31 → 0.9.32

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 (51) hide show
  1. package/README.md +22 -0
  2. package/dist/commands/apply.d.ts.map +1 -1
  3. package/dist/commands/apply.js +45 -3
  4. package/dist/commands/apply.js.map +1 -1
  5. package/dist/commands/map.d.ts.map +1 -1
  6. package/dist/commands/map.js +78 -1
  7. package/dist/commands/map.js.map +1 -1
  8. package/dist/commands/plan-slo.d.ts +7 -0
  9. package/dist/commands/plan-slo.d.ts.map +1 -0
  10. package/dist/commands/plan-slo.js +205 -0
  11. package/dist/commands/plan-slo.js.map +1 -0
  12. package/dist/commands/plan.d.ts.map +1 -1
  13. package/dist/commands/plan.js +665 -29
  14. package/dist/commands/plan.js.map +1 -1
  15. package/dist/commands/repo.d.ts +3 -0
  16. package/dist/commands/repo.d.ts.map +1 -0
  17. package/dist/commands/repo.js +166 -0
  18. package/dist/commands/repo.js.map +1 -0
  19. package/dist/commands/ship.d.ts.map +1 -1
  20. package/dist/commands/ship.js +29 -0
  21. package/dist/commands/ship.js.map +1 -1
  22. package/dist/commands/verify.d.ts.map +1 -1
  23. package/dist/commands/verify.js +261 -9
  24. package/dist/commands/verify.js.map +1 -1
  25. package/dist/index.js +17 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/services/mapper/ProjectScanner.d.ts +76 -2
  28. package/dist/services/mapper/ProjectScanner.d.ts.map +1 -1
  29. package/dist/services/mapper/ProjectScanner.js +545 -40
  30. package/dist/services/mapper/ProjectScanner.js.map +1 -1
  31. package/dist/services/security/SecurityGuard.d.ts +21 -2
  32. package/dist/services/security/SecurityGuard.d.ts.map +1 -1
  33. package/dist/services/security/SecurityGuard.js +130 -27
  34. package/dist/services/security/SecurityGuard.js.map +1 -1
  35. package/dist/utils/governance.d.ts +2 -0
  36. package/dist/utils/governance.d.ts.map +1 -1
  37. package/dist/utils/governance.js +2 -0
  38. package/dist/utils/governance.js.map +1 -1
  39. package/dist/utils/plan-slo.d.ts +73 -0
  40. package/dist/utils/plan-slo.d.ts.map +1 -0
  41. package/dist/utils/plan-slo.js +271 -0
  42. package/dist/utils/plan-slo.js.map +1 -0
  43. package/dist/utils/project-root.d.ts +5 -4
  44. package/dist/utils/project-root.d.ts.map +1 -1
  45. package/dist/utils/project-root.js +82 -7
  46. package/dist/utils/project-root.js.map +1 -1
  47. package/dist/utils/repo-links.d.ts +17 -0
  48. package/dist/utils/repo-links.d.ts.map +1 -0
  49. package/dist/utils/repo-links.js +136 -0
  50. package/dist/utils/repo-links.js.map +1 -0
  51. package/package.json +3 -3
@@ -241,6 +241,57 @@ function isEnabledFlag(value) {
241
241
  const normalized = value.trim().toLowerCase();
242
242
  return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
243
243
  }
244
+ function parseSigningKeyRing(raw) {
245
+ if (!raw || !raw.trim()) {
246
+ return {};
247
+ }
248
+ const out = {};
249
+ for (const token of raw.split(/[,\n;]+/)) {
250
+ const trimmed = token.trim();
251
+ if (!trimmed)
252
+ continue;
253
+ const separator = trimmed.indexOf('=');
254
+ if (separator <= 0)
255
+ continue;
256
+ const keyId = trimmed.slice(0, separator).trim();
257
+ const key = trimmed.slice(separator + 1).trim();
258
+ if (!keyId || !key)
259
+ continue;
260
+ out[keyId] = key;
261
+ }
262
+ return out;
263
+ }
264
+ function resolveGovernanceSigningConfig() {
265
+ const signingKeys = parseSigningKeyRing(process.env.NEURCODE_GOVERNANCE_SIGNING_KEYS);
266
+ const envSigningKey = process.env.NEURCODE_GOVERNANCE_SIGNING_KEY?.trim() ||
267
+ process.env.NEURCODE_AI_LOG_SIGNING_KEY?.trim() ||
268
+ '';
269
+ const requestedKeyId = process.env.NEURCODE_GOVERNANCE_SIGNING_KEY_ID?.trim() || '';
270
+ const signer = process.env.NEURCODE_GOVERNANCE_SIGNER || process.env.USER || 'neurcode-cli';
271
+ let signingKey = envSigningKey || null;
272
+ let signingKeyId = requestedKeyId || null;
273
+ if (!signingKey && Object.keys(signingKeys).length > 0) {
274
+ if (signingKeyId && signingKeys[signingKeyId]) {
275
+ signingKey = signingKeys[signingKeyId];
276
+ }
277
+ else {
278
+ const fallbackKeyId = Object.keys(signingKeys).sort((a, b) => a.localeCompare(b))[0];
279
+ signingKey = signingKeys[fallbackKeyId];
280
+ signingKeyId = signingKeyId || fallbackKeyId;
281
+ }
282
+ }
283
+ return {
284
+ signingKey,
285
+ signingKeyId,
286
+ signingKeys,
287
+ signer,
288
+ };
289
+ }
290
+ function isSignedAiLogsRequired(orgGovernanceSettings) {
291
+ return (orgGovernanceSettings?.requireSignedAiLogs === true ||
292
+ isEnabledFlag(process.env.NEURCODE_GOVERNANCE_REQUIRE_SIGNED_LOGS) ||
293
+ isEnabledFlag(process.env.NEURCODE_AI_LOG_REQUIRE_SIGNED));
294
+ }
244
295
  function policyLockMismatchMessage(mismatches) {
245
296
  if (mismatches.length === 0) {
246
297
  return 'Policy lock baseline check failed';
@@ -309,7 +360,7 @@ async function recordVerificationIfRequested(options, config, payload) {
309
360
  * Execute policy-only verification (General Governance mode)
310
361
  * Returns the exit code to use
311
362
  */
312
- async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRoot, config, client, source, projectId, orgGovernanceSettings, aiLogSigningKey, aiLogSigner) {
363
+ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRoot, config, client, source, projectId, orgGovernanceSettings, aiLogSigningKey, aiLogSigningKeyId, aiLogSigningKeys, aiLogSigner) {
313
364
  if (!options.json) {
314
365
  console.log(chalk.cyan('🛡️ General Governance mode (policy only, no plan linked)\n'));
315
366
  }
@@ -321,10 +372,128 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
321
372
  contextCandidates: diffFiles.map((file) => file.path),
322
373
  orgGovernance: orgGovernanceSettings,
323
374
  signingKey: aiLogSigningKey,
375
+ signingKeyId: aiLogSigningKeyId,
376
+ signingKeys: aiLogSigningKeys,
324
377
  signer: aiLogSigner,
325
378
  });
326
379
  const governancePayload = buildGovernancePayload(governanceAnalysis, orgGovernanceSettings);
327
380
  const contextPolicyViolations = governanceAnalysis.contextPolicy.violations.filter((item) => !ignoreFilter(item.file));
381
+ const signedLogsRequired = isSignedAiLogsRequired(orgGovernanceSettings);
382
+ if (signedLogsRequired && !governanceAnalysis.aiChangeLogIntegrity.valid) {
383
+ const message = `AI change-log integrity check failed: ${governanceAnalysis.aiChangeLogIntegrity.issues.join('; ') || 'unknown issue'}`;
384
+ if (options.json) {
385
+ console.log(JSON.stringify({
386
+ grade: 'F',
387
+ score: 0,
388
+ verdict: 'FAIL',
389
+ violations: [
390
+ {
391
+ file: '.neurcode/ai-change-log.json',
392
+ rule: 'ai_change_log_integrity',
393
+ severity: 'block',
394
+ message,
395
+ },
396
+ ],
397
+ message,
398
+ scopeGuardPassed: false,
399
+ bloatCount: 0,
400
+ bloatFiles: [],
401
+ plannedFilesModified: 0,
402
+ totalPlannedFiles: 0,
403
+ adherenceScore: 0,
404
+ mode: 'policy_only',
405
+ policyOnly: true,
406
+ policyOnlySource: source,
407
+ ...governancePayload,
408
+ }, null, 2));
409
+ }
410
+ else {
411
+ console.log(chalk.red('❌ AI change-log integrity validation failed (policy-only mode).'));
412
+ console.log(chalk.red(` ${message}`));
413
+ }
414
+ await recordVerificationIfRequested(options, config, {
415
+ grade: 'F',
416
+ violations: [
417
+ {
418
+ file: '.neurcode/ai-change-log.json',
419
+ rule: 'ai_change_log_integrity',
420
+ severity: 'block',
421
+ message,
422
+ },
423
+ ],
424
+ verifyResult: {
425
+ adherenceScore: 0,
426
+ verdict: 'FAIL',
427
+ bloatCount: 0,
428
+ bloatFiles: [],
429
+ message,
430
+ },
431
+ projectId,
432
+ jsonMode: Boolean(options.json),
433
+ governance: governancePayload,
434
+ });
435
+ return 2;
436
+ }
437
+ if (governanceAnalysis.governanceDecision.decision === 'block') {
438
+ const message = governanceAnalysis.governanceDecision.summary
439
+ || 'Governance decision matrix returned BLOCK.';
440
+ const reasonCodes = governanceAnalysis.governanceDecision.reasonCodes || [];
441
+ if (options.json) {
442
+ console.log(JSON.stringify({
443
+ grade: 'F',
444
+ score: 0,
445
+ verdict: 'FAIL',
446
+ violations: [
447
+ {
448
+ file: '.neurcode/ai-change-log.json',
449
+ rule: 'governance_decision_block',
450
+ severity: 'block',
451
+ message,
452
+ },
453
+ ],
454
+ message,
455
+ scopeGuardPassed: false,
456
+ bloatCount: 0,
457
+ bloatFiles: [],
458
+ plannedFilesModified: 0,
459
+ totalPlannedFiles: 0,
460
+ adherenceScore: 0,
461
+ mode: 'policy_only',
462
+ policyOnly: true,
463
+ policyOnlySource: source,
464
+ ...governancePayload,
465
+ }, null, 2));
466
+ }
467
+ else {
468
+ console.log(chalk.red('❌ Governance decision blocked this change set (policy-only mode).'));
469
+ if (reasonCodes.length > 0) {
470
+ console.log(chalk.red(` Reasons: ${reasonCodes.join(', ')}`));
471
+ }
472
+ console.log(chalk.red(` ${message}`));
473
+ }
474
+ await recordVerificationIfRequested(options, config, {
475
+ grade: 'F',
476
+ violations: [
477
+ {
478
+ file: '.neurcode/ai-change-log.json',
479
+ rule: 'governance_decision_block',
480
+ severity: 'block',
481
+ message,
482
+ },
483
+ ],
484
+ verifyResult: {
485
+ adherenceScore: 0,
486
+ verdict: 'FAIL',
487
+ bloatCount: 0,
488
+ bloatFiles: [],
489
+ message,
490
+ },
491
+ projectId,
492
+ jsonMode: Boolean(options.json),
493
+ governance: governancePayload,
494
+ });
495
+ return 2;
496
+ }
328
497
  if (contextPolicyViolations.length > 0) {
329
498
  const message = `Context policy violation: ${contextPolicyViolations.map((item) => item.file).join(', ')}`;
330
499
  const contextPolicyViolationItems = contextPolicyViolations.map((item) => ({
@@ -719,10 +888,11 @@ async function verifyCommand(options) {
719
888
  orgId: (0, state_1.getOrgId)(),
720
889
  projectId: projectId || null,
721
890
  };
722
- const aiLogSigningKey = process.env.NEURCODE_GOVERNANCE_SIGNING_KEY ||
723
- process.env.NEURCODE_AI_LOG_SIGNING_KEY ||
724
- null;
725
- const aiLogSigner = process.env.NEURCODE_GOVERNANCE_SIGNER || process.env.USER || 'neurcode-cli';
891
+ const signingConfig = resolveGovernanceSigningConfig();
892
+ const aiLogSigningKey = signingConfig.signingKey;
893
+ const aiLogSigningKeyId = signingConfig.signingKeyId;
894
+ const aiLogSigningKeys = signingConfig.signingKeys;
895
+ const aiLogSigner = signingConfig.signer;
726
896
  let orgGovernanceSettings = null;
727
897
  if (config.apiKey) {
728
898
  try {
@@ -744,6 +914,8 @@ async function verifyCommand(options) {
744
914
  }
745
915
  }
746
916
  }
917
+ const signedLogsRequired = isSignedAiLogsRequired(orgGovernanceSettings);
918
+ const hasSigningMaterial = Boolean(aiLogSigningKey) || Object.keys(aiLogSigningKeys).length > 0;
747
919
  const recordVerifyEvent = (verdict, note, changedFiles, planId) => {
748
920
  if (!brainScope.orgId || !brainScope.projectId) {
749
921
  return;
@@ -765,6 +937,60 @@ async function verifyCommand(options) {
765
937
  // Never block verify flow on Brain persistence failures.
766
938
  }
767
939
  };
940
+ if (signedLogsRequired && !hasSigningMaterial) {
941
+ const message = 'Signed AI change-logs are required but no signing key is configured. Set NEURCODE_GOVERNANCE_SIGNING_KEY or NEURCODE_GOVERNANCE_SIGNING_KEYS.';
942
+ recordVerifyEvent('FAIL', 'missing_signing_key_material');
943
+ if (options.json) {
944
+ console.log(JSON.stringify({
945
+ grade: 'F',
946
+ score: 0,
947
+ verdict: 'FAIL',
948
+ violations: [
949
+ {
950
+ file: '.neurcode/ai-change-log.json',
951
+ rule: 'ai_change_log_signing_required',
952
+ severity: 'block',
953
+ message,
954
+ },
955
+ ],
956
+ adherenceScore: 0,
957
+ bloatCount: 0,
958
+ bloatFiles: [],
959
+ plannedFilesModified: 0,
960
+ totalPlannedFiles: 0,
961
+ message,
962
+ scopeGuardPassed: false,
963
+ mode: 'plan_enforced',
964
+ policyOnly: false,
965
+ }, null, 2));
966
+ }
967
+ else {
968
+ console.log(chalk.red('\n⛔ Governance Signing Key Missing'));
969
+ console.log(chalk.red(` ${message}`));
970
+ console.log(chalk.dim(' Recommended: set NEURCODE_GOVERNANCE_SIGNING_KEY_ID and key ring via NEURCODE_GOVERNANCE_SIGNING_KEYS.'));
971
+ }
972
+ await recordVerificationIfRequested(options, config, {
973
+ grade: 'F',
974
+ violations: [
975
+ {
976
+ file: '.neurcode/ai-change-log.json',
977
+ rule: 'ai_change_log_signing_required',
978
+ severity: 'block',
979
+ message,
980
+ },
981
+ ],
982
+ verifyResult: {
983
+ adherenceScore: 0,
984
+ verdict: 'FAIL',
985
+ bloatCount: 0,
986
+ bloatFiles: [],
987
+ message,
988
+ },
989
+ projectId: projectId || undefined,
990
+ jsonMode: Boolean(options.json),
991
+ });
992
+ process.exit(2);
993
+ }
768
994
  // Determine which diff to capture (staged + unstaged for full current work)
769
995
  let diffText;
770
996
  if (options.staged) {
@@ -876,6 +1102,8 @@ async function verifyCommand(options) {
876
1102
  contextCandidates: diffFiles.map((file) => file.path),
877
1103
  orgGovernance: orgGovernanceSettings,
878
1104
  signingKey: aiLogSigningKey,
1105
+ signingKeyId: aiLogSigningKeyId,
1106
+ signingKeys: aiLogSigningKeys,
879
1107
  signer: aiLogSigner,
880
1108
  });
881
1109
  const baselineGovernancePayload = buildGovernancePayload(baselineGovernance, orgGovernanceSettings);
@@ -935,7 +1163,7 @@ async function verifyCommand(options) {
935
1163
  console.log(chalk.dim(` ${summary.totalAdded} lines added, ${summary.totalRemoved} lines removed\n`));
936
1164
  }
937
1165
  const runPolicyOnlyModeAndExit = async (source) => {
938
- const exitCode = await executePolicyOnlyMode(options, diffFiles, shouldIgnore, projectRoot, config, client, source, projectId || undefined, orgGovernanceSettings, aiLogSigningKey, aiLogSigner);
1166
+ const exitCode = await executePolicyOnlyMode(options, diffFiles, shouldIgnore, projectRoot, config, client, source, projectId || undefined, orgGovernanceSettings, aiLogSigningKey, aiLogSigningKeyId, aiLogSigningKeys, aiLogSigner);
939
1167
  const changedFiles = diffFiles.map((f) => f.path);
940
1168
  const verdict = exitCode === 2 ? 'FAIL' : exitCode === 1 ? 'WARN' : 'PASS';
941
1169
  recordVerifyEvent(verdict, `policy_only_source=${source};exit=${exitCode}`, changedFiles);
@@ -1069,6 +1297,8 @@ async function verifyCommand(options) {
1069
1297
  contextCandidates: planFiles,
1070
1298
  orgGovernance: orgGovernanceSettings,
1071
1299
  signingKey: aiLogSigningKey,
1300
+ signingKeyId: aiLogSigningKeyId,
1301
+ signingKeys: aiLogSigningKeys,
1072
1302
  signer: aiLogSigner,
1073
1303
  });
1074
1304
  // Get sessionId from state file (.neurcode/state.json) first, then fallback to config
@@ -1517,13 +1747,23 @@ async function verifyCommand(options) {
1517
1747
  const verifyResult = await client.verifyPlan(finalPlanId, diffStats, changedFiles, projectId, intentConstraints);
1518
1748
  // Apply custom policy verdict: block from dashboard overrides API verdict
1519
1749
  const policyBlock = policyDecision === 'block' && policyViolations.length > 0;
1520
- const effectiveVerdict = policyBlock ? 'FAIL' : verifyResult.verdict;
1750
+ const governanceDecisionBlock = governanceResult?.governanceDecision?.decision === 'block';
1751
+ const governanceIntegrityBlock = signedLogsRequired && governanceResult ? governanceResult.aiChangeLogIntegrity.valid !== true : false;
1752
+ const governanceHardBlock = governanceDecisionBlock || governanceIntegrityBlock;
1753
+ const effectiveVerdict = policyBlock || governanceHardBlock
1754
+ ? 'FAIL'
1755
+ : verifyResult.verdict;
1521
1756
  const policyMessageBase = policyBlock
1522
1757
  ? `Custom policy violations: ${policyViolations.map(v => `${v.file}: ${v.message || v.rule}`).join('; ')}. ${verifyResult.message}`
1523
1758
  : verifyResult.message;
1524
- const effectiveMessage = policyExceptionsSummary.suppressed > 0
1759
+ const governanceBlockReason = governanceIntegrityBlock
1760
+ ? `AI change-log integrity failed: ${governanceResult?.aiChangeLogIntegrity?.issues?.join('; ') || 'unknown issue'}`
1761
+ : governanceDecisionBlock
1762
+ ? governanceResult?.governanceDecision?.summary || 'Governance decision matrix returned BLOCK.'
1763
+ : null;
1764
+ const effectiveMessage = (policyExceptionsSummary.suppressed > 0
1525
1765
  ? `${policyMessageBase} Policy exceptions suppressed ${policyExceptionsSummary.suppressed} violation(s).`
1526
- : policyMessageBase;
1766
+ : policyMessageBase) + (governanceBlockReason ? ` ${governanceBlockReason}` : '');
1527
1767
  // Calculate grade from effective verdict and score
1528
1768
  // CRITICAL: 0/0 planned files = 'F' (Incomplete), not 'B'
1529
1769
  // Bloat automatically drops grade by at least one letter
@@ -1572,6 +1812,7 @@ async function verifyCommand(options) {
1572
1812
  recordVerifyEvent(effectiveVerdict, `adherence=${verifyResult.adherenceScore};bloat=${verifyResult.bloatCount};scopeGuard=${scopeGuardPassed ? 1 : 0};policy=${policyDecision};policyExceptions=${policyExceptionsSummary.suppressed}`, changedPathsForBrain, finalPlanId);
1573
1813
  const shouldForceGovernancePass = scopeGuardPassed &&
1574
1814
  !policyBlock &&
1815
+ !governanceHardBlock &&
1575
1816
  (effectiveVerdict === 'PASS' ||
1576
1817
  ((verifyResult.verdict === 'FAIL' || verifyResult.verdict === 'WARN') &&
1577
1818
  policyViolations.length === 0 &&
@@ -2069,6 +2310,17 @@ function displayGovernanceInsights(governance, options = {}) {
2069
2310
  console.log(governance.aiChangeLogIntegrity.valid
2070
2311
  ? chalk.dim(` AI change-log integrity: valid (${governance.aiChangeLogIntegrity.signed ? 'signed' : 'unsigned'})`)
2071
2312
  : chalk.red(` AI change-log integrity: invalid (${governance.aiChangeLogIntegrity.issues.join('; ') || 'unknown'})`));
2313
+ if (governance.aiChangeLogIntegrity.signed) {
2314
+ const keyId = typeof governance.aiChangeLogIntegrity.keyId === 'string'
2315
+ ? governance.aiChangeLogIntegrity.keyId
2316
+ : null;
2317
+ const verifiedWithKeyId = typeof governance.aiChangeLogIntegrity.verifiedWithKeyId === 'string'
2318
+ ? governance.aiChangeLogIntegrity.verifiedWithKeyId
2319
+ : null;
2320
+ if (keyId || verifiedWithKeyId) {
2321
+ console.log(chalk.dim(` Signing key: ${keyId || 'n/a'}${verifiedWithKeyId ? ` (verified via ${verifiedWithKeyId})` : ''}`));
2322
+ }
2323
+ }
2072
2324
  if (governance.suspiciousChange.flagged) {
2073
2325
  console.log(chalk.red('\nSuspicious Change Detected'));
2074
2326
  console.log(chalk.red(` Plan expected files: ${governance.suspiciousChange.expectedFiles} | AI modified files: ${governance.suspiciousChange.actualFiles}`));