@neurcode-ai/cli 0.9.48 → 0.9.50

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 (78) hide show
  1. package/dist/commands/fix.d.ts +13 -0
  2. package/dist/commands/fix.d.ts.map +1 -0
  3. package/dist/commands/fix.js +785 -0
  4. package/dist/commands/fix.js.map +1 -0
  5. package/dist/commands/generate.d.ts +7 -0
  6. package/dist/commands/generate.d.ts.map +1 -0
  7. package/dist/commands/generate.js +132 -0
  8. package/dist/commands/generate.js.map +1 -0
  9. package/dist/commands/patch-apply.d.ts +7 -0
  10. package/dist/commands/patch-apply.d.ts.map +1 -0
  11. package/dist/commands/patch-apply.js +85 -0
  12. package/dist/commands/patch-apply.js.map +1 -0
  13. package/dist/commands/plan-show.d.ts +6 -0
  14. package/dist/commands/plan-show.d.ts.map +1 -0
  15. package/dist/commands/plan-show.js +33 -0
  16. package/dist/commands/plan-show.js.map +1 -0
  17. package/dist/commands/start-intent.d.ts +6 -0
  18. package/dist/commands/start-intent.d.ts.map +1 -0
  19. package/dist/commands/start-intent.js +80 -0
  20. package/dist/commands/start-intent.js.map +1 -0
  21. package/dist/commands/verify.d.ts.map +1 -1
  22. package/dist/commands/verify.js +703 -186
  23. package/dist/commands/verify.js.map +1 -1
  24. package/dist/context-engine/graph.d.ts +6 -0
  25. package/dist/context-engine/graph.d.ts.map +1 -0
  26. package/dist/context-engine/graph.js +55 -0
  27. package/dist/context-engine/graph.js.map +1 -0
  28. package/dist/context-engine/index.d.ts +14 -0
  29. package/dist/context-engine/index.d.ts.map +1 -0
  30. package/dist/context-engine/index.js +26 -0
  31. package/dist/context-engine/index.js.map +1 -0
  32. package/dist/context-engine/scanner.d.ts +6 -0
  33. package/dist/context-engine/scanner.d.ts.map +1 -0
  34. package/dist/context-engine/scanner.js +62 -0
  35. package/dist/context-engine/scanner.js.map +1 -0
  36. package/dist/context-engine/scorer.d.ts +9 -0
  37. package/dist/context-engine/scorer.d.ts.map +1 -0
  38. package/dist/context-engine/scorer.js +112 -0
  39. package/dist/context-engine/scorer.js.map +1 -0
  40. package/dist/context-engine/suggestions.d.ts +12 -0
  41. package/dist/context-engine/suggestions.d.ts.map +1 -0
  42. package/dist/context-engine/suggestions.js +22 -0
  43. package/dist/context-engine/suggestions.js.map +1 -0
  44. package/dist/index.js +129 -55
  45. package/dist/index.js.map +1 -1
  46. package/dist/mcp/context-injector.d.ts +45 -0
  47. package/dist/mcp/context-injector.d.ts.map +1 -0
  48. package/dist/mcp/context-injector.js +587 -0
  49. package/dist/mcp/context-injector.js.map +1 -0
  50. package/dist/mcp/proximity.d.ts +3 -0
  51. package/dist/mcp/proximity.d.ts.map +1 -0
  52. package/dist/mcp/proximity.js +135 -0
  53. package/dist/mcp/proximity.js.map +1 -0
  54. package/dist/patch-engine/diff.d.ts +12 -0
  55. package/dist/patch-engine/diff.d.ts.map +1 -0
  56. package/dist/patch-engine/diff.js +74 -0
  57. package/dist/patch-engine/diff.js.map +1 -0
  58. package/dist/patch-engine/generator.d.ts +13 -0
  59. package/dist/patch-engine/generator.d.ts.map +1 -0
  60. package/dist/patch-engine/generator.js +51 -0
  61. package/dist/patch-engine/generator.js.map +1 -0
  62. package/dist/patch-engine/index.d.ts +47 -0
  63. package/dist/patch-engine/index.d.ts.map +1 -0
  64. package/dist/patch-engine/index.js +182 -0
  65. package/dist/patch-engine/index.js.map +1 -0
  66. package/dist/patch-engine/patterns.d.ts +4 -0
  67. package/dist/patch-engine/patterns.d.ts.map +1 -0
  68. package/dist/patch-engine/patterns.js +99 -0
  69. package/dist/patch-engine/patterns.js.map +1 -0
  70. package/dist/utils/git.d.ts +8 -0
  71. package/dist/utils/git.d.ts.map +1 -1
  72. package/dist/utils/git.js +55 -0
  73. package/dist/utils/git.js.map +1 -1
  74. package/dist/utils/plan-sync.d.ts +34 -0
  75. package/dist/utils/plan-sync.d.ts.map +1 -0
  76. package/dist/utils/plan-sync.js +265 -0
  77. package/dist/utils/plan-sync.js.map +1 -0
  78. package/package.json +1 -1
@@ -55,6 +55,7 @@ const ignore_1 = require("../utils/ignore");
55
55
  const project_root_1 = require("../utils/project-root");
56
56
  const brain_context_1 = require("../utils/brain-context");
57
57
  const scope_telemetry_1 = require("../utils/scope-telemetry");
58
+ const plan_sync_1 = require("../utils/plan-sync");
58
59
  const policy_packs_1 = require("../utils/policy-packs");
59
60
  const custom_policy_rules_1 = require("../utils/custom-policy-rules");
60
61
  const policy_exceptions_1 = require("../utils/policy-exceptions");
@@ -672,6 +673,313 @@ function asNumberValue(value) {
672
673
  function asStringValue(value) {
673
674
  return typeof value === 'string' && value.trim().length > 0 ? value : null;
674
675
  }
676
+ const EXPEDITE_FOLLOW_UP_CHECKLIST = [
677
+ 'Add validation back',
678
+ 'Move logic to proper layer',
679
+ 'Remove temporary code',
680
+ ];
681
+ function containsAnyToken(value, tokens) {
682
+ const normalized = value.toLowerCase();
683
+ return tokens.some((token) => normalized.includes(token));
684
+ }
685
+ function isSecurityOrAuthViolation(fileRaw, policyRaw, messageRaw) {
686
+ const combined = `${fileRaw} ${policyRaw} ${messageRaw}`.toLowerCase();
687
+ return containsAnyToken(combined, [
688
+ 'auth',
689
+ 'authentication',
690
+ 'authorization',
691
+ 'security',
692
+ 'permission',
693
+ 'access control',
694
+ 'access_control',
695
+ 'token',
696
+ 'secret',
697
+ 'credential',
698
+ 'encryption',
699
+ 'encrypt',
700
+ 'decrypt',
701
+ 'csrf',
702
+ 'xss',
703
+ 'sql injection',
704
+ 'sqli',
705
+ 'insecure',
706
+ 'vulnerability',
707
+ ]);
708
+ }
709
+ function isCriticalScopeBreach(fileRaw, messageRaw) {
710
+ const combined = `${fileRaw} ${messageRaw}`.toLowerCase();
711
+ return containsAnyToken(combined, [
712
+ 'auth',
713
+ 'security',
714
+ 'secret',
715
+ 'token',
716
+ 'credential',
717
+ 'permission',
718
+ 'infra/terraform',
719
+ 'terraform',
720
+ 'k8s',
721
+ 'helm',
722
+ 'migration',
723
+ 'database/migration',
724
+ 'policy',
725
+ 'contract',
726
+ ]);
727
+ }
728
+ function resolveExpediteModeFromPayload(payload) {
729
+ const explicit = asBooleanFlag(payload.expediteMode);
730
+ if (explicit !== null) {
731
+ return explicit;
732
+ }
733
+ const message = asStringValue(payload.message) || '';
734
+ return containsAnyToken(message, ['hotfix', 'urgent', 'prod down', 'incident', 'expedite']);
735
+ }
736
+ function toVerifySeverity(value) {
737
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
738
+ if (normalized === 'critical' || normalized === 'block')
739
+ return 'critical';
740
+ if (normalized === 'high')
741
+ return 'high';
742
+ if (normalized === 'warn'
743
+ || normalized === 'warning'
744
+ || normalized === 'medium'
745
+ || normalized === 'low') {
746
+ return 'warning';
747
+ }
748
+ return 'info';
749
+ }
750
+ function toVerifyVerdict(value) {
751
+ const normalized = typeof value === 'string' ? value.trim().toUpperCase() : '';
752
+ if (normalized === 'PASS' || normalized === 'WARN' || normalized === 'FAIL') {
753
+ return normalized;
754
+ }
755
+ return 'FAIL';
756
+ }
757
+ function normalizeScopeIssueMessage(rawMessage) {
758
+ const message = asStringValue(rawMessage);
759
+ return message || 'File modified outside intended scope';
760
+ }
761
+ function pushVerifyIssue(target, seen, key, value) {
762
+ if (seen.has(key))
763
+ return;
764
+ seen.add(key);
765
+ target.push(value);
766
+ }
767
+ function dedupeTriageItems(items) {
768
+ const seen = new Set();
769
+ const output = [];
770
+ for (const item of items) {
771
+ const key = `${item.source}|${item.file.toLowerCase()}|${item.policy.toLowerCase()}|${item.message.toLowerCase()}`;
772
+ if (seen.has(key))
773
+ continue;
774
+ seen.add(key);
775
+ output.push(item);
776
+ }
777
+ return output;
778
+ }
779
+ function toCanonicalVerifyOutput(payload) {
780
+ const verdict = toVerifyVerdict(payload.verdict);
781
+ const violations = [];
782
+ const warnings = [];
783
+ const scopeIssues = [];
784
+ const seenViolations = new Set();
785
+ const seenWarnings = new Set();
786
+ const seenScopeIssues = new Set();
787
+ const addScopeIssue = (fileRaw, messageRaw) => {
788
+ const file = asStringValue(fileRaw) || 'unknown';
789
+ const message = normalizeScopeIssueMessage(messageRaw);
790
+ const key = file.toLowerCase();
791
+ pushVerifyIssue(scopeIssues, seenScopeIssues, key, { file, message });
792
+ };
793
+ const addWarning = (fileRaw, messageRaw, policyRaw) => {
794
+ const file = asStringValue(fileRaw) || 'unknown';
795
+ const message = asStringValue(messageRaw) || 'Warning detected';
796
+ const policy = asStringValue(policyRaw) || 'warning';
797
+ const key = `${file.toLowerCase()}|${message.toLowerCase()}|${policy.toLowerCase()}`;
798
+ pushVerifyIssue(warnings, seenWarnings, key, { file, message, policy });
799
+ };
800
+ const addViolation = (fileRaw, messageRaw, policyRaw, severityRaw) => {
801
+ const file = asStringValue(fileRaw) || 'unknown';
802
+ const message = asStringValue(messageRaw) || 'Policy violation detected';
803
+ const policy = asStringValue(policyRaw) || 'unknown_policy';
804
+ const severity = toVerifySeverity(severityRaw);
805
+ const key = `${file.toLowerCase()}|${message.toLowerCase()}|${policy.toLowerCase()}|${severity}`;
806
+ pushVerifyIssue(violations, seenViolations, key, { file, message, policy, severity });
807
+ };
808
+ const rawScopeIssues = Array.isArray(payload.scopeIssues) ? payload.scopeIssues : [];
809
+ for (const item of rawScopeIssues) {
810
+ const record = asObjectRecord(item);
811
+ if (record) {
812
+ addScopeIssue(record.file, record.message);
813
+ }
814
+ else {
815
+ addScopeIssue(item, null);
816
+ }
817
+ }
818
+ const rawBloatFiles = Array.isArray(payload.bloatFiles) ? payload.bloatFiles : [];
819
+ for (const item of rawBloatFiles) {
820
+ addScopeIssue(item, null);
821
+ }
822
+ const rawWarnings = Array.isArray(payload.warnings) ? payload.warnings : [];
823
+ for (const item of rawWarnings) {
824
+ const record = asObjectRecord(item);
825
+ if (record) {
826
+ addWarning(record.file, record.message, record.policy ?? record.rule);
827
+ }
828
+ else if (typeof item === 'string') {
829
+ addWarning('unknown', item, 'warning');
830
+ }
831
+ }
832
+ const rawViolations = Array.isArray(payload.violations) ? payload.violations : [];
833
+ for (const item of rawViolations) {
834
+ const record = asObjectRecord(item);
835
+ if (!record)
836
+ continue;
837
+ const file = record.file;
838
+ const message = record.message;
839
+ const policy = record.policy ?? record.rule;
840
+ const severity = toVerifySeverity(record.severity);
841
+ const combined = `${String(policy || '').toLowerCase()} ${String(message || '').toLowerCase()}`;
842
+ const isScopeIssue = combined.includes('scope_guard')
843
+ || combined.includes('scope')
844
+ || combined.includes('outside the plan')
845
+ || combined.includes('out of scope');
846
+ if (isScopeIssue) {
847
+ addScopeIssue(file, message);
848
+ continue;
849
+ }
850
+ // Artifact presence/signature checks are advisory — they must never block a PR.
851
+ // Real governance signal (policy violations, scope drift) should not be obscured
852
+ // by infrastructure setup state.
853
+ const policyStr = String(policy || '').toLowerCase();
854
+ const isArtifactCheck = policyStr === 'deterministic_artifacts_required'
855
+ || policyStr === 'signed_artifacts_required';
856
+ if (isArtifactCheck) {
857
+ addWarning(file, message, policy);
858
+ continue;
859
+ }
860
+ if (severity === 'warning' || severity === 'info') {
861
+ addWarning(file, message, policy);
862
+ continue;
863
+ }
864
+ addViolation(file, message, policy, severity);
865
+ }
866
+ const payloadMessage = asStringValue(payload.message);
867
+ if (payloadMessage
868
+ && violations.length === 0
869
+ && warnings.length === 0
870
+ && scopeIssues.length === 0) {
871
+ addWarning('unknown', payloadMessage, 'verify_result');
872
+ }
873
+ const summaryRecord = asObjectRecord(payload.summary);
874
+ const fileSet = new Set();
875
+ for (const violation of violations)
876
+ fileSet.add(violation.file);
877
+ for (const warning of warnings)
878
+ fileSet.add(warning.file);
879
+ for (const scopeIssue of scopeIssues)
880
+ fileSet.add(scopeIssue.file);
881
+ const totalFilesChanged = (() => {
882
+ const fromSummary = summaryRecord ? asNumberValue(summaryRecord.totalFilesChanged) : null;
883
+ if (fromSummary !== null)
884
+ return Math.max(0, Math.floor(fromSummary));
885
+ const blastRadius = asObjectRecord(payload.blastRadius);
886
+ const fromBlastRadius = blastRadius ? asNumberValue(blastRadius.filesChanged) : null;
887
+ if (fromBlastRadius !== null)
888
+ return Math.max(0, Math.floor(fromBlastRadius));
889
+ return fileSet.size;
890
+ })();
891
+ const driftScoreRaw = asNumberValue(payload.driftScore);
892
+ const driftScore = driftScoreRaw === null
893
+ ? undefined
894
+ : Math.max(0, Math.min(100, Math.round(driftScoreRaw)));
895
+ const expediteModeUsed = resolveExpediteModeFromPayload(payload);
896
+ const scopeTriageItems = scopeIssues.map((item) => ({
897
+ file: item.file,
898
+ message: item.message,
899
+ policy: 'scope_guard',
900
+ severity: 'block',
901
+ source: 'scope',
902
+ }));
903
+ const violationTriageItems = violations.map((item) => ({
904
+ file: item.file,
905
+ message: item.message,
906
+ policy: item.policy,
907
+ severity: item.severity,
908
+ source: 'violation',
909
+ }));
910
+ const warningTriageItems = warnings.map((item) => ({
911
+ file: item.file,
912
+ message: item.message,
913
+ policy: item.policy,
914
+ severity: 'warning',
915
+ source: 'warning',
916
+ }));
917
+ const defaultBlockingItems = dedupeTriageItems([
918
+ ...scopeTriageItems,
919
+ ...violationTriageItems.filter((item) => item.severity === 'critical' || item.severity === 'high'),
920
+ ]);
921
+ const defaultAdvisoryItems = dedupeTriageItems([
922
+ ...warningTriageItems,
923
+ ...violationTriageItems.filter((item) => item.severity === 'warning' || item.severity === 'info'),
924
+ ]);
925
+ const expediteBlockingItems = dedupeTriageItems([
926
+ ...scopeTriageItems.filter((item) => isCriticalScopeBreach(item.file, item.message)),
927
+ ...violationTriageItems.filter((item) => isSecurityOrAuthViolation(item.file, item.policy, item.message)),
928
+ ...warningTriageItems
929
+ .filter((item) => isSecurityOrAuthViolation(item.file, item.policy, item.message))
930
+ .map((item) => ({
931
+ ...item,
932
+ source: 'violation',
933
+ })),
934
+ ]);
935
+ const expediteItems = dedupeTriageItems([
936
+ ...scopeTriageItems
937
+ .filter((item) => !isCriticalScopeBreach(item.file, item.message))
938
+ .map((item) => ({
939
+ ...item,
940
+ source: 'expedite',
941
+ })),
942
+ ...violationTriageItems
943
+ .filter((item) => !isSecurityOrAuthViolation(item.file, item.policy, item.message))
944
+ .map((item) => ({
945
+ ...item,
946
+ source: 'expedite',
947
+ })),
948
+ ...warningTriageItems
949
+ .filter((item) => !isSecurityOrAuthViolation(item.file, item.policy, item.message))
950
+ .map((item) => ({
951
+ ...item,
952
+ source: 'expedite',
953
+ })),
954
+ ]);
955
+ const blockingItems = expediteModeUsed ? expediteBlockingItems : defaultBlockingItems;
956
+ const advisoryItems = expediteModeUsed ? expediteItems : defaultAdvisoryItems;
957
+ return {
958
+ verdict,
959
+ summary: {
960
+ totalFilesChanged,
961
+ totalViolations: violations.length,
962
+ totalWarnings: warnings.length,
963
+ totalScopeIssues: scopeIssues.length,
964
+ },
965
+ violations,
966
+ warnings,
967
+ scopeIssues,
968
+ blockingCount: blockingItems.length,
969
+ advisoryCount: advisoryItems.length,
970
+ blockingItems,
971
+ advisoryItems,
972
+ expediteModeUsed,
973
+ expediteCount: expediteModeUsed ? expediteItems.length : 0,
974
+ expediteItems: expediteModeUsed ? expediteItems : [],
975
+ expediteFollowUpChecklist: expediteModeUsed ? [...EXPEDITE_FOLLOW_UP_CHECKLIST] : [],
976
+ ...(expediteModeUsed ? { expediteNote: 'Expedite Mode used' } : {}),
977
+ ...(typeof driftScore === 'number' ? { driftScore } : {}),
978
+ };
979
+ }
980
+ function emitCanonicalVerifyJson(payload) {
981
+ console.log(JSON.stringify(toCanonicalVerifyOutput(payload), null, 2));
982
+ }
675
983
  function buildDeterministicLayerSummary(payload) {
676
984
  const verdict = asStringValue(payload.verdict) || 'UNKNOWN';
677
985
  const mode = asStringValue(payload.mode) || 'unknown';
@@ -841,6 +1149,13 @@ function isGitRepository(cwd) {
841
1149
  return false;
842
1150
  }
843
1151
  }
1152
+ function resolveVerifyExpediteMode(projectRoot) {
1153
+ if (isEnabledFlag(process.env.NEURCODE_EXPEDITE_MODE) || isEnabledFlag(process.env.NEURCODE_MCP_EXPEDITE_MODE)) {
1154
+ return true;
1155
+ }
1156
+ const branchName = (0, git_1.detectCurrentGitBranch)(projectRoot) || process.env.GITHUB_REF_NAME || '';
1157
+ return containsAnyToken(branchName, ['hotfix', 'urgent', 'prod-down', 'prod_down', 'prod down', 'incident', 'expedite']);
1158
+ }
844
1159
  function isSignedAiLogsRequired(orgGovernanceSettings) {
845
1160
  const explicitRequirement = isEnabledFlag(process.env.NEURCODE_GOVERNANCE_REQUIRE_SIGNED_LOGS) ||
846
1161
  isEnabledFlag(process.env.NEURCODE_AI_LOG_REQUIRE_SIGNED);
@@ -1012,18 +1327,12 @@ async function recordVerificationIfRequested(options, config, payload) {
1012
1327
  * Execute policy-only verification (General Governance mode)
1013
1328
  * Returns the exit code to use
1014
1329
  */
1015
- async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRoot, config, client, source, scopeTelemetry, projectId, orgGovernanceSettings, aiLogSigningKey, aiLogSigningKeyId, aiLogSigningKeys, aiLogSigner, compiledPolicyArtifact, compiledPolicyMetadata, changeContractSummary) {
1330
+ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRoot, config, client, source, scopeTelemetry, projectId, orgGovernanceSettings, aiLogSigningKey, aiLogSigningKeyId, aiLogSigningKeys, aiLogSigner, expediteModeEnabled, compiledPolicyArtifact, compiledPolicyMetadata, changeContractSummary) {
1016
1331
  const emitPolicyOnlyJson = (payload) => {
1017
- const enrichedPayload = {
1332
+ emitCanonicalVerifyJson({
1018
1333
  ...payload,
1019
- deterministicLayers: buildDeterministicLayerSummary(payload),
1020
- };
1021
- console.log(JSON.stringify({
1022
- ...enrichedPayload,
1023
- ...(compiledPolicyMetadata ? { policyCompilation: compiledPolicyMetadata } : {}),
1024
- changeContract: changeContractSummary,
1025
- scope: scopeTelemetry,
1026
- }, null, 2));
1334
+ expediteMode: expediteModeEnabled,
1335
+ });
1027
1336
  };
1028
1337
  const policyOnlyVerificationSource = 'policy_only';
1029
1338
  const recordPolicyOnlyVerification = async (payload) => recordVerificationIfRequested(options, config, {
@@ -1350,6 +1659,9 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
1350
1659
  if (!options.json && effectiveRules.policyPack && effectiveRules.policyPackRules.length > 0) {
1351
1660
  console.log(chalk.dim(` Evaluating policy pack: ${effectiveRules.policyPack.packName} (${effectiveRules.policyPack.packId}@${effectiveRules.policyPack.version}, ${effectiveRules.policyPackRules.length} rule(s))`));
1352
1661
  }
1662
+ else if (!options.json && !effectiveRules.policyPack) {
1663
+ console.log(chalk.dim(' No policy pack installed — run `neurcode policy install <pack>` to add governance rules'));
1664
+ }
1353
1665
  const policyResult = (0, policy_engine_1.evaluateRules)(diffFilesForPolicy, effectiveRules.allRules);
1354
1666
  policyViolations = (policyResult.violations || []);
1355
1667
  policyViolations = policyViolations.filter((v) => !ignoreFilter(v.file));
@@ -1582,14 +1894,15 @@ async function verifyCommand(options) {
1582
1894
  try {
1583
1895
  const rootResolution = (0, project_root_1.resolveNeurcodeProjectRootWithTrace)(process.cwd());
1584
1896
  const projectRoot = rootResolution.projectRoot;
1897
+ const localPlanSync = (0, plan_sync_1.ensureLocalPlan)(projectRoot);
1898
+ const localPlanExpectedFiles = [...localPlanSync.expectedFiles];
1899
+ const expediteModeEnabled = resolveVerifyExpediteMode(projectRoot);
1585
1900
  const scopeTelemetry = (0, scope_telemetry_1.buildScopeTelemetryPayload)(rootResolution);
1586
1901
  const emitVerifyJson = (payload) => {
1587
- const jsonPayload = {
1902
+ emitCanonicalVerifyJson({
1588
1903
  ...payload,
1589
- deterministicLayers: buildDeterministicLayerSummary(payload),
1590
- scope: scopeTelemetry,
1591
- };
1592
- console.log(JSON.stringify(jsonPayload, null, 2));
1904
+ expediteMode: expediteModeEnabled,
1905
+ });
1593
1906
  };
1594
1907
  if (!isGitRepository(projectRoot)) {
1595
1908
  const message = 'Verify requires a git repository. Initialize git (`git init`) or run this command inside an existing git project.';
@@ -1630,10 +1943,10 @@ async function verifyCommand(options) {
1630
1943
  const explicitStrictArtifactMode = options.strictArtifacts === true ||
1631
1944
  isEnabledFlag(process.env.NEURCODE_VERIFY_STRICT_ARTIFACTS) ||
1632
1945
  isEnabledFlag(process.env.NEURCODE_ENTERPRISE_MODE);
1633
- const ciEnterpriseDefaultStrict = process.env.CI === 'true'
1634
- && !isEnabledFlag(process.env.NEURCODE_VERIFY_ALLOW_NON_STRICT_CI)
1635
- && Boolean(options.apiKey || process.env.NEURCODE_API_KEY);
1636
- const strictArtifactMode = explicitStrictArtifactMode || ciEnterpriseDefaultStrict;
1946
+ // Strict artifact mode is only engaged when explicitly requested.
1947
+ // Auto-enabling it in CI based on API key presence masked real violations
1948
+ // by blocking early on missing artifacts before policy checks could run.
1949
+ const strictArtifactMode = explicitStrictArtifactMode;
1637
1950
  const runtimeGuardArtifactPath = (0, runtime_guard_1.resolveRuntimeGuardPath)(projectRoot, options.runtimeGuard);
1638
1951
  const autoRuntimeGuardInStrict = strictArtifactMode
1639
1952
  && (0, fs_1.existsSync)(runtimeGuardArtifactPath)
@@ -1699,38 +2012,57 @@ async function verifyCommand(options) {
1699
2012
  ]
1700
2013
  : [],
1701
2014
  };
2015
+ // Artifact presence warnings (advisory — missing artifacts fall back to runtime compilation).
2016
+ // These must never cause an early exit; real governance signal should always be evaluated.
2017
+ const artifactPresenceWarnings = [];
1702
2018
  if (strictArtifactMode) {
1703
- const strictErrors = [];
1704
2019
  if (!compiledPolicyRead.artifact) {
1705
- strictErrors.push(compiledPolicyRead.error
2020
+ artifactPresenceWarnings.push(compiledPolicyRead.error
1706
2021
  ? `Compiled policy artifact invalid (${compiledPolicyRead.error})`
1707
2022
  : `Compiled policy artifact missing (${compiledPolicyRead.path})`);
1708
2023
  }
1709
2024
  if (!changeContractRead.contract) {
1710
- strictErrors.push(changeContractRead.error
2025
+ artifactPresenceWarnings.push(changeContractRead.error
1711
2026
  ? `Change contract artifact invalid (${changeContractRead.error})`
1712
2027
  : `Change contract artifact missing (${changeContractRead.path})`);
1713
2028
  }
1714
- if (compiledPolicySignatureStatus
1715
- && !compiledPolicySignatureStatus.valid
1716
- && (requireSignedArtifacts || compiledPolicySignatureStatus.present)) {
1717
- strictErrors.push(`Compiled policy artifact signature validation failed (${compiledPolicySignatureStatus.issues.join('; ') || 'unknown issue'})`);
2029
+ if (!options.json && artifactPresenceWarnings.length > 0) {
2030
+ console.log(chalk.yellow('\n⚠️ Deterministic artifact(s) unavailable — falling back to runtime compilation'));
2031
+ artifactPresenceWarnings.forEach((entry) => {
2032
+ console.log(chalk.yellow(` ${entry}`));
2033
+ });
2034
+ console.log(chalk.dim(' Governance will continue using runtime compilation. Artifact checks are advisory.\n'));
1718
2035
  }
1719
- if (changeContractSignatureStatus
1720
- && !changeContractSignatureStatus.valid
1721
- && (requireSignedArtifacts || changeContractSignatureStatus.present)) {
1722
- strictErrors.push(`Change contract artifact signature validation failed (${changeContractSignatureStatus.issues.join('; ') || 'unknown issue'})`);
2036
+ }
2037
+ // Signature blocking distinguishes two cases:
2038
+ // - Artifact has a signature that is INVALID (present=true, valid=false): this is a tamper
2039
+ // indicator and blocks when requireSignedArtifacts is set.
2040
+ // - Artifact has NO signature (present=false): this is an unsigned artifact; advisory only,
2041
+ // never blocks — an unsigned artifact cannot be "tampered", only "not signed yet".
2042
+ if (strictArtifactMode) {
2043
+ const signatureBlockErrors = [];
2044
+ if (requireSignedArtifacts
2045
+ && compiledPolicySignatureStatus
2046
+ && compiledPolicySignatureStatus.present
2047
+ && !compiledPolicySignatureStatus.valid) {
2048
+ signatureBlockErrors.push(`Compiled policy artifact signature validation failed (${compiledPolicySignatureStatus.issues.join('; ') || 'unknown issue'})`);
1723
2049
  }
1724
- if (strictErrors.length > 0) {
1725
- const message = `Strict artifact mode requires deterministic compiled-policy + change-contract artifacts.\n- ${strictErrors.join('\n- ')}`;
2050
+ if (requireSignedArtifacts
2051
+ && changeContractSignatureStatus
2052
+ && changeContractSignatureStatus.present
2053
+ && !changeContractSignatureStatus.valid) {
2054
+ signatureBlockErrors.push(`Change contract artifact signature validation failed (${changeContractSignatureStatus.issues.join('; ') || 'unknown issue'})`);
2055
+ }
2056
+ if (signatureBlockErrors.length > 0) {
2057
+ const message = `Signed artifact enforcement failed — tampered or invalid signatures detected.\n- ${signatureBlockErrors.join('\n- ')}`;
1726
2058
  if (options.json) {
1727
2059
  emitVerifyJson({
1728
2060
  grade: 'F',
1729
2061
  score: 0,
1730
2062
  verdict: 'FAIL',
1731
- violations: strictErrors.map((entry) => ({
2063
+ violations: signatureBlockErrors.map((entry) => ({
1732
2064
  file: entry.toLowerCase().includes('compiled policy') ? compiledPolicyRead.path : changeContractRead.path,
1733
- rule: 'deterministic_artifacts_required',
2065
+ rule: 'signed_artifacts_required',
1734
2066
  severity: 'block',
1735
2067
  message: entry,
1736
2068
  })),
@@ -1741,32 +2073,34 @@ async function verifyCommand(options) {
1741
2073
  totalPlannedFiles: 0,
1742
2074
  message,
1743
2075
  scopeGuardPassed: false,
1744
- mode: 'strict_artifacts_required',
2076
+ mode: 'signed_artifacts_required',
1745
2077
  policyOnly: options.policyOnly === true,
1746
2078
  changeContract: changeContractSummary,
1747
2079
  ...(compiledPolicyMetadata ? { policyCompilation: compiledPolicyMetadata } : {}),
1748
2080
  });
1749
2081
  }
1750
2082
  else {
1751
- (0, scope_telemetry_1.printScopeTelemetry)(chalk, scopeTelemetry, {
1752
- includeBlockedWarning: true,
1753
- });
1754
- console.log(chalk.red('\n⛔ Deterministic Artifact Requirements Failed'));
1755
- strictErrors.forEach((entry) => {
2083
+ (0, scope_telemetry_1.printScopeTelemetry)(chalk, scopeTelemetry, { includeBlockedWarning: true });
2084
+ console.log(chalk.red('\n⛔ Signed Artifact Validation Failed'));
2085
+ signatureBlockErrors.forEach((entry) => {
1756
2086
  console.log(chalk.red(` • ${entry}`));
1757
2087
  });
1758
- console.log(chalk.dim('\nSet --compiled-policy and --change-contract with valid artifacts before verify.'));
1759
- if (requireSignedArtifacts) {
1760
- console.log(chalk.dim('Enable signing keys via NEURCODE_GOVERNANCE_SIGNING_KEY or NEURCODE_GOVERNANCE_SIGNING_KEYS to generate signed artifacts.\n'));
1761
- }
1762
- else {
1763
- console.log('');
1764
- }
2088
+ console.log(chalk.dim('\nRegenerate artifacts with valid signing keys: NEURCODE_GOVERNANCE_SIGNING_KEY or NEURCODE_GOVERNANCE_SIGNING_KEYS.\n'));
1765
2089
  }
1766
2090
  process.exit(2);
1767
2091
  }
2092
+ // Advisory notice when artifact has a signature but signing is not required in this context.
2093
+ if (!options.json) {
2094
+ if (compiledPolicySignatureStatus && !compiledPolicySignatureStatus.valid && !requireSignedArtifacts) {
2095
+ console.log(chalk.yellow(` ⚠️ Compiled policy signature could not be verified (${compiledPolicySignatureStatus.issues.join('; ') || 'key unavailable'}) — advisory only`));
2096
+ }
2097
+ if (changeContractSignatureStatus && !changeContractSignatureStatus.valid && !requireSignedArtifacts) {
2098
+ console.log(chalk.yellow(` ⚠️ Change contract signature could not be verified (${changeContractSignatureStatus.issues.join('; ') || 'key unavailable'}) — advisory only`));
2099
+ }
2100
+ }
1768
2101
  }
1769
2102
  if (!strictArtifactMode && requireSignedArtifacts) {
2103
+ // Non-strict mode with signing required: same signature gate applies.
1770
2104
  const signatureErrors = [];
1771
2105
  if (compiledPolicyRead.artifact && compiledPolicySignatureStatus && !compiledPolicySignatureStatus.valid) {
1772
2106
  signatureErrors.push(`Compiled policy artifact signature validation failed (${compiledPolicySignatureStatus.issues.join('; ') || 'unknown issue'})`);
@@ -1801,13 +2135,9 @@ async function verifyCommand(options) {
1801
2135
  });
1802
2136
  }
1803
2137
  else {
1804
- (0, scope_telemetry_1.printScopeTelemetry)(chalk, scopeTelemetry, {
1805
- includeBlockedWarning: true,
1806
- });
2138
+ (0, scope_telemetry_1.printScopeTelemetry)(chalk, scopeTelemetry, { includeBlockedWarning: true });
1807
2139
  console.log(chalk.red('\n⛔ Signed Artifact Requirements Failed'));
1808
- signatureErrors.forEach((entry) => {
1809
- console.log(chalk.red(` • ${entry}`));
1810
- });
2140
+ signatureErrors.forEach((entry) => console.log(chalk.red(` • ${entry}`)));
1811
2141
  console.log(chalk.dim('\nEnable signing keys via NEURCODE_GOVERNANCE_SIGNING_KEY or NEURCODE_GOVERNANCE_SIGNING_KEYS and regenerate artifacts.\n'));
1812
2142
  }
1813
2143
  process.exit(2);
@@ -1845,9 +2175,6 @@ async function verifyCommand(options) {
1845
2175
  console.log(chalk.yellow(` Change contract signature: invalid (${changeContractSignatureStatus.issues.join('; ') || 'unknown issue'})`));
1846
2176
  }
1847
2177
  }
1848
- if (ciEnterpriseDefaultStrict && !explicitStrictArtifactMode) {
1849
- 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).'));
1850
- }
1851
2178
  if (autoRuntimeGuardInStrict && !options.requireRuntimeGuard && !isEnabledFlag(process.env.NEURCODE_VERIFY_REQUIRE_RUNTIME_GUARD)) {
1852
2179
  console.log(chalk.dim(` Strict mode detected runtime guard artifact: auto-enforcing runtime guard (${runtimeGuardArtifactPath}).`));
1853
2180
  }
@@ -2069,34 +2396,44 @@ async function verifyCommand(options) {
2069
2396
  });
2070
2397
  process.exit(2);
2071
2398
  }
2072
- // Determine which diff to capture (staged + unstaged for full current work)
2399
+ // Determine which diff to capture.
2073
2400
  let diffText;
2401
+ let diffContextLabel = '';
2074
2402
  if (options.staged) {
2075
2403
  diffText = (0, child_process_1.execSync)('git diff --cached', { maxBuffer: 1024 * 1024 * 1024, encoding: 'utf-8' });
2404
+ diffContextLabel = 'staged changes';
2076
2405
  }
2077
2406
  else if (options.base) {
2078
2407
  diffText = (0, git_1.getDiffFromBase)(options.base);
2408
+ diffContextLabel = `working tree vs ${options.base}`;
2079
2409
  }
2080
2410
  else if (options.head) {
2081
2411
  diffText = (0, child_process_1.execSync)('git diff HEAD', { maxBuffer: 1024 * 1024 * 1024, encoding: 'utf-8' });
2412
+ diffContextLabel = 'working tree vs HEAD';
2082
2413
  }
2083
2414
  else {
2084
- // Default: combine staged + unstaged to capture all current work
2085
- try {
2086
- const stagedDiff = (0, child_process_1.execSync)('git diff --cached', { maxBuffer: 1024 * 1024 * 1024, encoding: 'utf-8' });
2087
- const unstagedDiff = (0, child_process_1.execSync)('git diff', { maxBuffer: 1024 * 1024 * 1024, encoding: 'utf-8' });
2088
- diffText = stagedDiff + (stagedDiff && unstagedDiff ? '\n' : '') + unstagedDiff;
2415
+ // Default: resolve a PR-like base context first (origin/main or origin/master).
2416
+ // Fallback to staged diff when base context cannot be resolved.
2417
+ const defaultContext = (0, git_1.resolveDefaultDiffContext)(projectRoot);
2418
+ if (defaultContext.mode === 'base' && defaultContext.baseRef) {
2419
+ diffText = (0, git_1.getDiffFromBase)(defaultContext.baseRef);
2420
+ diffContextLabel = defaultContext.currentBranch
2421
+ ? `${defaultContext.currentBranch} vs ${defaultContext.baseRef}`
2422
+ : `working tree vs ${defaultContext.baseRef}`;
2089
2423
  }
2090
- catch {
2091
- // Fallback to HEAD if git commands fail
2092
- diffText = (0, child_process_1.execSync)('git diff HEAD', { maxBuffer: 1024 * 1024 * 1024, encoding: 'utf-8' });
2424
+ else {
2425
+ diffText = (0, child_process_1.execSync)('git diff --cached', { maxBuffer: 1024 * 1024 * 1024, encoding: 'utf-8' });
2426
+ diffContextLabel = 'staged changes (fallback)';
2093
2427
  }
2094
2428
  }
2429
+ if (!options.json && diffContextLabel) {
2430
+ console.log(chalk.dim(` Diff context: ${diffContextLabel}`));
2431
+ }
2095
2432
  const untrackedDiffFiles = getUntrackedDiffFiles(projectRoot);
2096
2433
  if (!diffText.trim() && untrackedDiffFiles.length === 0) {
2097
2434
  if (!options.json) {
2098
- console.log(chalk.yellow('⚠️ No changes detected'));
2099
- console.log(chalk.dim(' Make sure you have staged or unstaged changes to verify'));
2435
+ console.log(chalk.yellow('⚠️ No changes detected in current diff context.'));
2436
+ console.log(chalk.dim(' Tip: Ensure changes are staged or run against a base branch.'));
2100
2437
  }
2101
2438
  else {
2102
2439
  emitVerifyJson({
@@ -2109,7 +2446,7 @@ async function verifyCommand(options) {
2109
2446
  bloatFiles: [],
2110
2447
  plannedFilesModified: 0,
2111
2448
  totalPlannedFiles: 0,
2112
- message: 'No changes detected',
2449
+ message: 'No changes detected in current diff context.',
2113
2450
  scopeGuardPassed: false,
2114
2451
  });
2115
2452
  }
@@ -2139,7 +2476,8 @@ async function verifyCommand(options) {
2139
2476
  const summary = (0, diff_parser_1.getDiffSummary)(diffFiles);
2140
2477
  if (diffFiles.length === 0) {
2141
2478
  if (!options.json) {
2142
- console.log(chalk.yellow('⚠️ No file changes detected in diff'));
2479
+ console.log(chalk.yellow('⚠️ No changes detected in current diff context.'));
2480
+ console.log(chalk.dim(' Tip: Ensure changes are staged or run against a base branch.'));
2143
2481
  }
2144
2482
  else {
2145
2483
  emitVerifyJson({
@@ -2152,7 +2490,7 @@ async function verifyCommand(options) {
2152
2490
  bloatFiles: [],
2153
2491
  plannedFilesModified: 0,
2154
2492
  totalPlannedFiles: 0,
2155
- message: 'No file changes detected in diff',
2493
+ message: 'No changes detected in current diff context.',
2156
2494
  scopeGuardPassed: false,
2157
2495
  });
2158
2496
  }
@@ -2382,7 +2720,7 @@ async function verifyCommand(options) {
2382
2720
  }
2383
2721
  }
2384
2722
  const runPolicyOnlyModeAndExit = async (source) => {
2385
- const exitCode = await executePolicyOnlyMode(options, diffFiles, shouldIgnore, projectRoot, config, client, source, scopeTelemetry, projectId || undefined, orgGovernanceSettings, aiLogSigningKey, aiLogSigningKeyId, aiLogSigningKeys, aiLogSigner, compiledPolicyRead.artifact, compiledPolicyMetadata, changeContractSummary);
2723
+ const exitCode = await executePolicyOnlyMode(options, diffFiles, shouldIgnore, projectRoot, config, client, source, scopeTelemetry, projectId || undefined, orgGovernanceSettings, aiLogSigningKey, aiLogSigningKeyId, aiLogSigningKeys, aiLogSigner, expediteModeEnabled, compiledPolicyRead.artifact, compiledPolicyMetadata, changeContractSummary);
2386
2724
  const changedFiles = diffFiles.map((f) => f.path);
2387
2725
  const verdict = exitCode === 2 ? 'FAIL' : exitCode === 1 ? 'WARN' : 'PASS';
2388
2726
  recordVerifyEvent(verdict, `policy_only_source=${source};exit=${exitCode}`, changedFiles);
@@ -2397,6 +2735,7 @@ async function verifyCommand(options) {
2397
2735
  const requirePlan = options.requirePlan === true
2398
2736
  || process.env.NEURCODE_VERIFY_REQUIRE_PLAN === '1'
2399
2737
  || strictArtifactMode;
2738
+ let useLocalPlanSync = false;
2400
2739
  // Get planId: Priority 1: options flag, Priority 2: state file (.neurcode/config.json), Priority 3: legacy config
2401
2740
  let planId = options.planId;
2402
2741
  if (!planId) {
@@ -2429,6 +2768,19 @@ async function verifyCommand(options) {
2429
2768
  }
2430
2769
  }
2431
2770
  }
2771
+ if (planId === 'local-plan-sync' && localPlanExpectedFiles.length > 0) {
2772
+ useLocalPlanSync = true;
2773
+ if (!options.json) {
2774
+ console.log(chalk.dim(` Using Plan Sync from .neurcode/plan.json (${localPlanExpectedFiles.length} expected file(s))`));
2775
+ }
2776
+ }
2777
+ if (!planId && localPlanExpectedFiles.length > 0) {
2778
+ planId = 'local-plan-sync';
2779
+ useLocalPlanSync = true;
2780
+ if (!options.json) {
2781
+ console.log(chalk.dim(` Using Plan Sync from .neurcode/plan.json (${localPlanExpectedFiles.length} expected file(s))`));
2782
+ }
2783
+ }
2432
2784
  // If no planId found, either enforce strict requirement or fall back to policy-only mode.
2433
2785
  if (!planId) {
2434
2786
  if (requirePlan) {
@@ -2583,37 +2935,60 @@ async function verifyCommand(options) {
2583
2935
  }
2584
2936
  // Track if scope guard passed - this takes priority over AI grading
2585
2937
  let scopeGuardPassed = false;
2938
+ let scopeGuardExpediteBypass = false;
2586
2939
  let governanceResult = null;
2587
2940
  let planFilesForVerification = [];
2588
2941
  let intentConstraintsForVerification;
2589
2942
  try {
2590
2943
  // Step A: Get Modified Files (already have from diffFiles)
2591
2944
  const modifiedFiles = diffFiles.map(f => f.path);
2592
- // Step B: Fetch Plan and Session Data
2593
- const planData = await client.getPlan(finalPlanId);
2594
- // Extract original intent from plan (for constraint checking)
2595
- const originalIntent = planData.intent || '';
2596
- const planTitle = typeof planData.content.title === 'string'
2597
- ? planData.content.title?.trim()
2598
- : '';
2599
- const planSummary = typeof planData.content.summary === 'string' ? planData.content.summary.trim() : '';
2600
- const governanceTask = planTitle || planSummary || originalIntent || 'Plan verification';
2601
- // Get approved files from plan (only files with action CREATE or MODIFY)
2602
- const planFiles = planData.content.files
2603
- .filter(f => f.action === 'CREATE' || f.action === 'MODIFY')
2604
- .map(f => f.path);
2605
- planFilesForVerification = [...planFiles];
2945
+ // Step B: Resolve plan scope from remote plan or local Plan Sync.
2946
+ let originalIntent = '';
2947
+ let governanceTask = 'Plan verification';
2948
+ let planFiles = [];
2949
+ let planDependencies = [];
2950
+ let remotePlanSessionId = null;
2951
+ if (useLocalPlanSync) {
2952
+ const localIntent = (localPlanSync.intent || '').trim();
2953
+ const localConstraintText = localPlanSync.constraints.length > 0
2954
+ ? localPlanSync.constraints.join('; ')
2955
+ : '';
2956
+ planFiles = [...localPlanExpectedFiles];
2957
+ originalIntent = localIntent || localConstraintText;
2958
+ governanceTask = localIntent
2959
+ ? `Local Plan Sync: ${localIntent}`
2960
+ : 'Local Plan Sync verification';
2961
+ if (!options.json) {
2962
+ console.log(chalk.dim(` Plan Sync scope loaded: ${planFiles.length} file(s)`));
2963
+ }
2964
+ }
2965
+ else {
2966
+ const planData = await client.getPlan(finalPlanId);
2967
+ // Extract original intent from plan (for constraint checking)
2968
+ originalIntent = planData.intent || '';
2969
+ const planTitle = typeof planData.content.title === 'string'
2970
+ ? planData.content.title?.trim()
2971
+ : '';
2972
+ const planSummary = typeof planData.content.summary === 'string' ? planData.content.summary.trim() : '';
2973
+ governanceTask = planTitle || planSummary || originalIntent || 'Plan verification';
2974
+ // Get approved files from plan (only files with action CREATE or MODIFY)
2975
+ planFiles = planData.content.files
2976
+ .filter((f) => f.action === 'CREATE' || f.action === 'MODIFY')
2977
+ .map((f) => f.path);
2978
+ planDependencies = Array.isArray(planData.content.dependencies)
2979
+ ? planData.content.dependencies.filter((item) => typeof item === 'string')
2980
+ : [];
2981
+ remotePlanSessionId = planData.sessionId || null;
2982
+ }
2983
+ planFilesForVerification = [...new Set([...planFiles, ...localPlanExpectedFiles])];
2606
2984
  intentConstraintsForVerification = originalIntent || undefined;
2607
- const planDependencies = Array.isArray(planData.content.dependencies)
2608
- ? planData.content.dependencies.filter((item) => typeof item === 'string')
2609
- : [];
2610
2985
  governanceResult = (0, governance_1.evaluateGovernance)({
2611
2986
  projectRoot,
2612
2987
  task: governanceTask,
2613
- expectedFiles: planFiles,
2988
+ expectedFiles: planFilesForVerification,
2614
2989
  expectedDependencies: planDependencies,
2615
2990
  diffFiles,
2616
- contextCandidates: planFiles,
2991
+ contextCandidates: planFilesForVerification,
2617
2992
  orgGovernance: orgGovernanceSettings,
2618
2993
  requireSignedAiLogs: signedLogsRequired,
2619
2994
  signingKey: aiLogSigningKey,
@@ -2626,8 +3001,8 @@ async function verifyCommand(options) {
2626
3001
  // This is the session_id string needed to fetch the session
2627
3002
  let sessionIdString = (0, state_1.getSessionId)() || configData.sessionId || configData.lastSessionId || null;
2628
3003
  // Fallback: Use sessionId from plan if not in config
2629
- if (!sessionIdString && planData.sessionId) {
2630
- sessionIdString = planData.sessionId;
3004
+ if (!sessionIdString && remotePlanSessionId) {
3005
+ sessionIdString = remotePlanSessionId;
2631
3006
  if ((process.env.DEBUG || process.env.VERBOSE) && !options.json) {
2632
3007
  console.log(chalk.dim(` Using sessionId from plan: ${sessionIdString.substring(0, 8)}...`));
2633
3008
  }
@@ -2661,17 +3036,24 @@ async function verifyCommand(options) {
2661
3036
  }
2662
3037
  }
2663
3038
  // Step C: The Intersection Logic
2664
- const approvedSet = new Set([...planFiles, ...allowedFiles]);
3039
+ const approvedSet = new Set([...planFilesForVerification, ...allowedFiles]);
2665
3040
  const violations = modifiedFiles.filter(f => !approvedSet.has(f));
2666
3041
  const filteredViolations = violations.filter((p) => !shouldIgnore(p));
2667
3042
  // Step D: The Block (only report scope violations for non-ignored files)
2668
3043
  if (filteredViolations.length > 0) {
3044
+ const criticalScopeViolations = expediteModeEnabled
3045
+ ? filteredViolations.filter((file) => isCriticalScopeBreach(file, 'File modified outside the plan'))
3046
+ : filteredViolations;
3047
+ const expediteScopeViolations = expediteModeEnabled
3048
+ ? filteredViolations.filter((file) => !criticalScopeViolations.includes(file))
3049
+ : [];
3050
+ const shouldBlockForScope = !expediteModeEnabled || criticalScopeViolations.length > 0;
2669
3051
  const aiDebtSummaryForScope = toAiDebtSummary((0, ai_debt_budget_1.evaluateAiDebtBudget)({
2670
3052
  diffFiles,
2671
3053
  bloatCount: filteredViolations.length,
2672
3054
  config: aiDebtConfig,
2673
3055
  }));
2674
- recordVerifyEvent('FAIL', `scope_violation=${filteredViolations.length}`, modifiedFiles, finalPlanId);
3056
+ recordVerifyEvent(shouldBlockForScope ? 'FAIL' : 'WARN', `${shouldBlockForScope ? 'scope_violation' : 'scope_expedite'}=${filteredViolations.length}`, modifiedFiles, finalPlanId);
2675
3057
  const scopeViolationItems = filteredViolations.map((file) => ({
2676
3058
  file,
2677
3059
  rule: 'scope_guard',
@@ -2683,8 +3065,10 @@ async function verifyCommand(options) {
2683
3065
  ...scopeViolationItems,
2684
3066
  ...aiDebtViolationItems,
2685
3067
  ];
2686
- const scopeViolationMessage = `Scope violation: ${filteredViolations.length} file(s) modified outside the plan`;
2687
- if (options.json) {
3068
+ const scopeViolationMessage = shouldBlockForScope
3069
+ ? `Scope violation: ${criticalScopeViolations.length} critical file(s) modified outside the plan`
3070
+ : `Expedite scope warning: ${expediteScopeViolations.length} non-critical file(s) modified outside the plan`;
3071
+ if (shouldBlockForScope && options.json) {
2688
3072
  // Output JSON for scope violation BEFORE exit. Must include violations for GitHub Action annotations.
2689
3073
  const jsonOutput = {
2690
3074
  grade: 'F',
@@ -2695,12 +3079,13 @@ async function verifyCommand(options) {
2695
3079
  bloatCount: filteredViolations.length,
2696
3080
  bloatFiles: filteredViolations,
2697
3081
  plannedFilesModified: 0,
2698
- totalPlannedFiles: planFiles.length,
3082
+ totalPlannedFiles: planFilesForVerification.length,
2699
3083
  message: scopeViolationMessage,
2700
3084
  scopeGuardPassed: false,
2701
3085
  mode: 'plan_enforced',
2702
3086
  policyOnly: false,
2703
3087
  aiDebt: aiDebtSummaryForScope,
3088
+ ...(expediteModeEnabled ? { expediteMode: true } : {}),
2704
3089
  ...(governanceResult
2705
3090
  ? buildGovernancePayload(governanceResult, orgGovernanceSettings, {
2706
3091
  changeContract: changeContractSummary,
@@ -2733,18 +3118,25 @@ async function verifyCommand(options) {
2733
3118
  });
2734
3119
  process.exit(1);
2735
3120
  }
2736
- else {
3121
+ else if (shouldBlockForScope) {
2737
3122
  // Human-readable output only when NOT in json mode
2738
3123
  console.log(chalk.red('\n⛔ SCOPE VIOLATION'));
2739
3124
  console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
2740
3125
  console.log(chalk.red('The following files were modified but are not in the plan:'));
2741
3126
  console.log('');
2742
- filteredViolations.forEach(file => {
3127
+ criticalScopeViolations.forEach(file => {
2743
3128
  console.log(chalk.red(` • ${file}`));
2744
3129
  });
3130
+ if (expediteModeEnabled && expediteScopeViolations.length > 0) {
3131
+ console.log('');
3132
+ console.log(chalk.yellow('Non-critical scope files (can be followed up under expedite mode):'));
3133
+ expediteScopeViolations.forEach((file) => {
3134
+ console.log(chalk.yellow(` • ${file}`));
3135
+ });
3136
+ }
2745
3137
  console.log('');
2746
3138
  console.log(chalk.yellow('To unblock these files, run:'));
2747
- filteredViolations.forEach(file => {
3139
+ criticalScopeViolations.forEach(file => {
2748
3140
  console.log(chalk.dim(` neurcode allow ${file}`));
2749
3141
  });
2750
3142
  if (aiDebtSummaryForScope.mode !== 'off') {
@@ -2791,11 +3183,30 @@ async function verifyCommand(options) {
2791
3183
  });
2792
3184
  process.exit(1);
2793
3185
  }
3186
+ else {
3187
+ scopeGuardExpediteBypass = true;
3188
+ if (!options.json) {
3189
+ console.log(chalk.yellow('\n⚠️ Expedite scope relaxation applied (non-critical scope only).'));
3190
+ expediteScopeViolations.forEach((file) => {
3191
+ console.log(chalk.yellow(` • ${file}`));
3192
+ });
3193
+ console.log(chalk.dim(' Follow-up checklist:'));
3194
+ EXPEDITE_FOLLOW_UP_CHECKLIST.forEach((item) => {
3195
+ console.log(chalk.dim(` - ${item}`));
3196
+ });
3197
+ console.log(chalk.dim(' Note: Expedite Mode used\n'));
3198
+ }
3199
+ }
2794
3200
  }
2795
3201
  // Scope guard passed - all files are approved or allowed
2796
3202
  scopeGuardPassed = true;
2797
3203
  if (!options.json) {
2798
- console.log(chalk.green('✅ All modified files are approved or allowed'));
3204
+ if (scopeGuardExpediteBypass) {
3205
+ console.log(chalk.green('✅ Scope guard passed with expedite relaxation for non-critical scope changes'));
3206
+ }
3207
+ else {
3208
+ console.log(chalk.green('✅ All modified files are approved or allowed'));
3209
+ }
2799
3210
  console.log('');
2800
3211
  }
2801
3212
  }
@@ -2879,7 +3290,7 @@ async function verifyCommand(options) {
2879
3290
  const lockViolationItems = toPolicyLockViolations(policyLockEvaluation.mismatches);
2880
3291
  recordVerifyEvent('FAIL', 'policy_lock_mismatch', diffFiles.map((f) => f.path), finalPlanId);
2881
3292
  if (options.json) {
2882
- console.log(JSON.stringify({
3293
+ emitVerifyJson({
2883
3294
  grade: 'F',
2884
3295
  score: 0,
2885
3296
  verdict: 'FAIL',
@@ -2907,7 +3318,7 @@ async function verifyCommand(options) {
2907
3318
  path: policyLockEvaluation.lockPath,
2908
3319
  mismatches: policyLockEvaluation.mismatches,
2909
3320
  },
2910
- }, null, 2));
3321
+ });
2911
3322
  }
2912
3323
  else {
2913
3324
  console.log(chalk.red('\n❌ Policy lock baseline mismatch'));
@@ -3139,6 +3550,9 @@ async function verifyCommand(options) {
3139
3550
  if (!options.json && effectiveRules.policyPack && effectiveRules.policyPackRules.length > 0) {
3140
3551
  console.log(chalk.dim(` Evaluating policy pack: ${effectiveRules.policyPack.packName} (${effectiveRules.policyPack.packId}@${effectiveRules.policyPack.version}, ${effectiveRules.policyPackRules.length} rule(s))`));
3141
3552
  }
3553
+ else if (!options.json && !effectiveRules.policyPack) {
3554
+ console.log(chalk.dim(' No policy pack installed — run `neurcode policy install <pack>` to add governance rules'));
3555
+ }
3142
3556
  // Prepare diff stats and changed files for API
3143
3557
  const diffStats = {
3144
3558
  totalAdded: summary.totalAdded,
@@ -3326,35 +3740,20 @@ async function verifyCommand(options) {
3326
3740
  displayChangeContractDrift(changeContractSummary, { advisory: true });
3327
3741
  }
3328
3742
  }
3329
- // Call verify API
3743
+ // Call verify API (or deterministic local evaluation for Plan Sync scope mode)
3330
3744
  if (!options.json) {
3331
- console.log(chalk.dim(' Sending to Neurcode API...\n'));
3332
- if (options.asyncMode) {
3333
- console.log(chalk.dim(' Queue-backed verification enabled (async job mode).'));
3745
+ if (useLocalPlanSync) {
3746
+ console.log(chalk.dim(' Using local Plan Sync deterministic verification (no API plan lookup).\n'));
3747
+ }
3748
+ else {
3749
+ console.log(chalk.dim(' Sending to Neurcode API...\n'));
3750
+ if (options.asyncMode) {
3751
+ console.log(chalk.dim(' Queue-backed verification enabled (async job mode).'));
3752
+ }
3334
3753
  }
3335
3754
  }
3336
3755
  try {
3337
- let verifySource = 'api';
3338
- let verifyResult;
3339
- try {
3340
- verifyResult = await client.verifyPlan(finalPlanId, diffStats, changedFiles, projectId, intentConstraintsForVerification, deterministicPolicyRules, 'api', compiledPolicyMetadata, {
3341
- async: options.asyncMode === true,
3342
- pollIntervalMs: Number.isFinite(options.verifyJobPollMs) ? options.verifyJobPollMs : undefined,
3343
- timeoutMs: Number.isFinite(options.verifyJobTimeoutMs) ? options.verifyJobTimeoutMs : undefined,
3344
- idempotencyKey: options.verifyIdempotencyKey,
3345
- maxAttempts: Number.isFinite(options.verifyJobMaxAttempts) ? options.verifyJobMaxAttempts : undefined,
3346
- });
3347
- }
3348
- catch (verifyApiError) {
3349
- if (planFilesForVerification.length === 0) {
3350
- throw verifyApiError;
3351
- }
3352
- verifySource = 'local_fallback';
3353
- if (!options.json) {
3354
- const fallbackReason = verifyApiError instanceof Error ? verifyApiError.message : String(verifyApiError);
3355
- console.log(chalk.yellow('⚠️ Verify API unavailable, using local deterministic fallback.'));
3356
- console.log(chalk.dim(` Reason: ${fallbackReason}`));
3357
- }
3756
+ const runLocalDeterministicVerification = () => {
3358
3757
  const localFileContents = {};
3359
3758
  for (const file of changedFiles) {
3360
3759
  const absolutePath = (0, path_1.join)(projectRoot, file.path);
@@ -3380,7 +3779,7 @@ async function verifyCommand(options) {
3380
3779
  extraConstraintRules: hydratedCompiledPolicyRules.length > 0 ? hydratedCompiledPolicyRules : undefined,
3381
3780
  fileContents: localFileContents,
3382
3781
  });
3383
- verifyResult = {
3782
+ return {
3384
3783
  verificationId: `local-fallback-${Date.now()}`,
3385
3784
  adherenceScore: localEvaluation.adherenceScore,
3386
3785
  bloatCount: localEvaluation.bloatCount,
@@ -3391,6 +3790,35 @@ async function verifyCommand(options) {
3391
3790
  diffSummary: localEvaluation.diffSummary,
3392
3791
  message: localEvaluation.message,
3393
3792
  };
3793
+ };
3794
+ let verifySource = 'api';
3795
+ let verifyResult;
3796
+ if (useLocalPlanSync) {
3797
+ verifySource = 'local_fallback';
3798
+ verifyResult = runLocalDeterministicVerification();
3799
+ }
3800
+ else {
3801
+ try {
3802
+ verifyResult = await client.verifyPlan(finalPlanId, diffStats, changedFiles, projectId, intentConstraintsForVerification, deterministicPolicyRules, 'api', compiledPolicyMetadata, {
3803
+ async: options.asyncMode === true,
3804
+ pollIntervalMs: Number.isFinite(options.verifyJobPollMs) ? options.verifyJobPollMs : undefined,
3805
+ timeoutMs: Number.isFinite(options.verifyJobTimeoutMs) ? options.verifyJobTimeoutMs : undefined,
3806
+ idempotencyKey: options.verifyIdempotencyKey,
3807
+ maxAttempts: Number.isFinite(options.verifyJobMaxAttempts) ? options.verifyJobMaxAttempts : undefined,
3808
+ });
3809
+ }
3810
+ catch (verifyApiError) {
3811
+ if (planFilesForVerification.length === 0) {
3812
+ throw verifyApiError;
3813
+ }
3814
+ verifySource = 'local_fallback';
3815
+ if (!options.json) {
3816
+ const fallbackReason = verifyApiError instanceof Error ? verifyApiError.message : String(verifyApiError);
3817
+ console.log(chalk.yellow('⚠️ Verify API unavailable, using local deterministic fallback.'));
3818
+ console.log(chalk.dim(` Reason: ${fallbackReason}`));
3819
+ }
3820
+ verifyResult = runLocalDeterministicVerification();
3821
+ }
3394
3822
  }
3395
3823
  const aiDebtEvaluation = (0, ai_debt_budget_1.evaluateAiDebtBudget)({
3396
3824
  diffFiles,
@@ -3586,7 +4014,7 @@ async function verifyCommand(options) {
3586
4014
  message: effectiveMessage,
3587
4015
  bloatFiles: displayBloatFiles,
3588
4016
  bloatCount: displayBloatFiles.length,
3589
- }, policyViolations);
4017
+ }, policyViolations, expediteModeEnabled);
3590
4018
  if (governanceResult) {
3591
4019
  displayGovernanceInsights(governanceResult, { explain: options.explain });
3592
4020
  }
@@ -3738,6 +4166,10 @@ async function verifyCommand(options) {
3738
4166
  });
3739
4167
  }
3740
4168
  else {
4169
+ console.error(chalk.red('\n❌ Verification failed before completion.'));
4170
+ if (diffFiles.length > 0) {
4171
+ console.log(chalk.dim(` Partial context captured: ${diffFiles.length} changed file(s) in diff.`));
4172
+ }
3741
4173
  if (error instanceof Error) {
3742
4174
  if (error.message.includes('404') || error.message.includes('not found')) {
3743
4175
  console.error(chalk.red(`❌ Error: Plan not found`));
@@ -3758,27 +4190,24 @@ async function verifyCommand(options) {
3758
4190
  catch (error) {
3759
4191
  if (options.json) {
3760
4192
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
3761
- console.log(JSON.stringify({
3762
- grade: 'F',
3763
- score: 0,
4193
+ emitCanonicalVerifyJson({
3764
4194
  verdict: 'FAIL',
3765
- violations: [],
3766
- adherenceScore: 0,
3767
- bloatCount: 0,
3768
- bloatFiles: [],
3769
- plannedFilesModified: 0,
3770
- totalPlannedFiles: 0,
3771
- message: `Unexpected error: ${errorMessage}`,
3772
- scopeGuardPassed: false,
3773
- scope: {
3774
- scanRoot: process.cwd(),
3775
- startDir: process.cwd(),
3776
- gitRoot: null,
3777
- linkedRepoOverrideUsed: false,
3778
- linkedRepos: [],
3779
- blockedOverride: null,
4195
+ summary: {
4196
+ totalFilesChanged: 0,
4197
+ totalViolations: 0,
4198
+ totalWarnings: 1,
4199
+ totalScopeIssues: 0,
3780
4200
  },
3781
- }, null, 2));
4201
+ violations: [],
4202
+ warnings: [
4203
+ {
4204
+ file: 'unknown',
4205
+ message: `Unexpected error: ${errorMessage}`,
4206
+ policy: 'verify_runtime',
4207
+ },
4208
+ ],
4209
+ scopeIssues: [],
4210
+ });
3782
4211
  }
3783
4212
  else {
3784
4213
  console.error(chalk.red('\n❌ Unexpected error:'));
@@ -4115,51 +4544,139 @@ function displayChangeContractDrift(summary, options = { advisory: false }) {
4115
4544
  /**
4116
4545
  * Display verification results in a formatted report card
4117
4546
  */
4118
- function displayVerifyResults(result, policyViolations) {
4119
- const verdictLabel = result.verdict === 'PASS'
4120
- ? chalk.green('PASS')
4547
+ function displayVerifyResults(result, policyViolations, expediteModeUsed = false) {
4548
+ // ── Header ────────────────────────────────────────────────────────────────
4549
+ const headerLabel = result.verdict === 'PASS'
4550
+ ? chalk.bold.green('\n✅ VERIFICATION PASSED')
4121
4551
  : result.verdict === 'WARN'
4122
- ? chalk.yellow('WARN ⚠️')
4123
- : chalk.red('FAIL ');
4124
- const plannedText = `${result.plannedFilesModified}/${result.totalPlannedFiles}`;
4125
- console.log(`\n${verdictLabel}`);
4126
- console.log(chalk.dim(`Plan adherence: ${plannedText} files (${result.adherenceScore}%)`));
4127
- const maxItems = 20;
4128
- if (result.bloatCount > 0) {
4129
- console.log(chalk.red('\nOut-of-scope changes:'));
4130
- result.bloatFiles.slice(0, maxItems).forEach((file) => {
4131
- console.log(` - ${file}`);
4552
+ ? chalk.bold.yellow('\n⚠️ VERIFICATION PASSED WITH WARNINGS')
4553
+ : chalk.bold.red('\n❌ VERIFICATION FAILED');
4554
+ console.log(headerLabel);
4555
+ // ── Triage items ──────────────────────────────────────────────────────────
4556
+ const maxBlockingItems = 20;
4557
+ const maxAdvisoryItems = 8;
4558
+ const maxExpediteItems = 12;
4559
+ const policyItems = policyViolations || [];
4560
+ const isBlockingSeverity = (severityRaw) => {
4561
+ const normalized = String(severityRaw || '').toLowerCase();
4562
+ return normalized === 'block' || normalized === 'critical' || normalized === 'high';
4563
+ };
4564
+ const scopeItems = result.bloatFiles.map((file) => ({
4565
+ file,
4566
+ message: 'File modified outside intended scope',
4567
+ policy: 'scope_guard',
4568
+ }));
4569
+ const policyTriageItems = policyItems.map((item) => ({
4570
+ file: item.file,
4571
+ message: item.message || item.rule,
4572
+ policy: item.rule || 'policy_violation',
4573
+ severity: item.severity,
4574
+ }));
4575
+ let blockingItems = [
4576
+ ...scopeItems.map((item) => ({
4577
+ file: item.file,
4578
+ message: item.message,
4579
+ })),
4580
+ ...policyTriageItems
4581
+ .filter((item) => isBlockingSeverity(item.severity))
4582
+ .map((item) => ({
4583
+ file: item.file,
4584
+ message: item.message,
4585
+ })),
4586
+ ];
4587
+ let advisoryItems = policyTriageItems
4588
+ .filter((item) => !isBlockingSeverity(item.severity))
4589
+ .map((item) => ({
4590
+ file: item.file,
4591
+ message: item.message,
4592
+ }));
4593
+ let expediteItems = [];
4594
+ if (expediteModeUsed) {
4595
+ blockingItems = [
4596
+ ...scopeItems
4597
+ .filter((item) => isCriticalScopeBreach(item.file, item.message))
4598
+ .map((item) => ({ file: item.file, message: item.message })),
4599
+ ...policyTriageItems
4600
+ .filter((item) => isSecurityOrAuthViolation(item.file, item.policy, item.message))
4601
+ .map((item) => ({ file: item.file, message: item.message })),
4602
+ ];
4603
+ expediteItems = [
4604
+ ...scopeItems
4605
+ .filter((item) => !isCriticalScopeBreach(item.file, item.message))
4606
+ .map((item) => ({ file: item.file, message: item.message })),
4607
+ ...policyTriageItems
4608
+ .filter((item) => !isSecurityOrAuthViolation(item.file, item.policy, item.message))
4609
+ .map((item) => ({ file: item.file, message: item.message })),
4610
+ ];
4611
+ advisoryItems = [];
4612
+ }
4613
+ // ── Counts ────────────────────────────────────────────────────────────────
4614
+ console.log(blockingItems.length > 0
4615
+ ? chalk.red(`Blocking Issues: ${blockingItems.length}`)
4616
+ : chalk.dim('Blocking Issues: 0'));
4617
+ if (expediteModeUsed) {
4618
+ console.log(chalk.yellow(`Expedite Issues: ${expediteItems.length}`));
4619
+ }
4620
+ else {
4621
+ console.log(advisoryItems.length > 0
4622
+ ? chalk.yellow(`Advisory Issues: ${advisoryItems.length}`)
4623
+ : chalk.dim('Advisory Issues: 0'));
4624
+ }
4625
+ console.log(chalk.dim(`Plan adherence: ${result.plannedFilesModified}/${result.totalPlannedFiles} files (${result.adherenceScore}%)`));
4626
+ // ── Top issues ────────────────────────────────────────────────────────────
4627
+ const topIssues = [
4628
+ ...blockingItems,
4629
+ ...(expediteModeUsed ? expediteItems : advisoryItems),
4630
+ ].slice(0, 2);
4631
+ if (topIssues.length > 0) {
4632
+ console.log(chalk.bold('\nTop Issues:'));
4633
+ topIssues.forEach((item, i) => {
4634
+ console.log(` ${i + 1}. ${item.message} → ${chalk.cyan(item.file)}`);
4635
+ });
4636
+ }
4637
+ // ── Detailed lists ────────────────────────────────────────────────────────
4638
+ if (blockingItems.length > 0) {
4639
+ console.log(chalk.red(`\nBLOCKING (${blockingItems.length})`));
4640
+ blockingItems.slice(0, maxBlockingItems).forEach((item) => {
4641
+ console.log(` - ${item.file}: ${item.message}`);
4132
4642
  });
4133
- if (result.bloatFiles.length > maxItems) {
4134
- console.log(chalk.dim(` - ... ${result.bloatFiles.length - maxItems} more`));
4643
+ if (blockingItems.length > maxBlockingItems) {
4644
+ console.log(chalk.dim(` - ... ${blockingItems.length - maxBlockingItems} more`));
4135
4645
  }
4136
4646
  }
4137
- if (policyViolations && policyViolations.length > 0) {
4138
- const blocking = policyViolations.filter((item) => item.severity === 'block');
4139
- const warnings = policyViolations.filter((item) => item.severity !== 'block');
4140
- if (blocking.length > 0) {
4141
- console.log(chalk.red('\nBlocking policy violations:'));
4142
- blocking.slice(0, maxItems).forEach((item) => {
4143
- console.log(` - ${item.file}: ${item.message || item.rule}`);
4144
- });
4145
- if (blocking.length > maxItems) {
4146
- console.log(chalk.dim(` - ... ${blocking.length - maxItems} more`));
4147
- }
4647
+ if (advisoryItems.length > 0) {
4648
+ console.log(chalk.yellow(`\nADVISORY (${advisoryItems.length})`));
4649
+ advisoryItems.slice(0, maxAdvisoryItems).forEach((item) => {
4650
+ console.log(` - ${item.file}: ${item.message}`);
4651
+ });
4652
+ if (advisoryItems.length > maxAdvisoryItems) {
4653
+ console.log(chalk.dim(` - ... ${advisoryItems.length - maxAdvisoryItems} more (summarized)`));
4148
4654
  }
4149
- if (warnings.length > 0) {
4150
- console.log(chalk.yellow('\nPolicy warnings:'));
4151
- warnings.slice(0, maxItems).forEach((item) => {
4152
- console.log(` - ${item.file}: ${item.message || item.rule}`);
4153
- });
4154
- if (warnings.length > maxItems) {
4155
- console.log(chalk.dim(` - ... ${warnings.length - maxItems} more`));
4156
- }
4655
+ }
4656
+ if (expediteModeUsed && expediteItems.length > 0) {
4657
+ console.log(chalk.yellow(`\nEXPEDITE (requires follow-up) (${expediteItems.length})`));
4658
+ expediteItems.slice(0, maxExpediteItems).forEach((item) => {
4659
+ console.log(` - ${item.file}: ${item.message}`);
4660
+ });
4661
+ if (expediteItems.length > maxExpediteItems) {
4662
+ console.log(chalk.dim(` - ... ${expediteItems.length - maxExpediteItems} more (summarized)`));
4157
4663
  }
4664
+ console.log(chalk.dim(' Follow-up checklist:'));
4665
+ EXPEDITE_FOLLOW_UP_CHECKLIST.forEach((checkItem) => {
4666
+ console.log(chalk.dim(` - ${checkItem}`));
4667
+ });
4668
+ console.log(chalk.dim(' Note: Expedite Mode used'));
4669
+ }
4670
+ if (blockingItems.length === 0 && advisoryItems.length === 0 && expediteItems.length === 0) {
4671
+ console.log(chalk.green('\nNo issues detected.'));
4158
4672
  }
4159
- if (result.bloatCount === 0 && (!policyViolations || policyViolations.length === 0)) {
4160
- console.log(chalk.green('\nNo drift detected.'));
4673
+ // ── Next step ─────────────────────────────────────────────────────────────
4674
+ if (blockingItems.length > 0 || advisoryItems.length > 0 || expediteItems.length > 0) {
4675
+ console.log(chalk.bold('\nNext step:'));
4676
+ console.log(` ${chalk.cyan('neurcode fix')}`);
4677
+ console.log(chalk.dim(' or: neurcode fix --apply-safe (auto-apply high-confidence patches)'));
4161
4678
  }
4162
- console.log(chalk.dim(`\nSummary: ${result.message}\n`));
4679
+ console.log(chalk.dim(`\nDetails: ${result.message}\n`));
4163
4680
  }
4164
4681
  function printFirstRunAdvisoryMessage(demoMode) {
4165
4682
  console.log(chalk.cyan('\nNeurcode first-run advisory mode'));