@neurcode-ai/cli 0.9.27 → 0.9.29

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 (60) hide show
  1. package/dist/api-client.d.ts +58 -0
  2. package/dist/api-client.d.ts.map +1 -1
  3. package/dist/api-client.js +38 -0
  4. package/dist/api-client.js.map +1 -1
  5. package/dist/commands/allow.d.ts.map +1 -1
  6. package/dist/commands/allow.js +5 -19
  7. package/dist/commands/allow.js.map +1 -1
  8. package/dist/commands/apply.d.ts +1 -0
  9. package/dist/commands/apply.d.ts.map +1 -1
  10. package/dist/commands/apply.js +105 -46
  11. package/dist/commands/apply.js.map +1 -1
  12. package/dist/commands/init.d.ts +2 -0
  13. package/dist/commands/init.d.ts.map +1 -1
  14. package/dist/commands/init.js +83 -24
  15. package/dist/commands/init.js.map +1 -1
  16. package/dist/commands/plan.d.ts +4 -0
  17. package/dist/commands/plan.d.ts.map +1 -1
  18. package/dist/commands/plan.js +518 -42
  19. package/dist/commands/plan.js.map +1 -1
  20. package/dist/commands/policy.d.ts.map +1 -1
  21. package/dist/commands/policy.js +629 -0
  22. package/dist/commands/policy.js.map +1 -1
  23. package/dist/commands/prompt.d.ts +7 -1
  24. package/dist/commands/prompt.d.ts.map +1 -1
  25. package/dist/commands/prompt.js +130 -26
  26. package/dist/commands/prompt.js.map +1 -1
  27. package/dist/commands/ship.d.ts +32 -0
  28. package/dist/commands/ship.d.ts.map +1 -1
  29. package/dist/commands/ship.js +1404 -75
  30. package/dist/commands/ship.js.map +1 -1
  31. package/dist/commands/verify.d.ts +6 -0
  32. package/dist/commands/verify.d.ts.map +1 -1
  33. package/dist/commands/verify.js +542 -115
  34. package/dist/commands/verify.js.map +1 -1
  35. package/dist/index.js +89 -3
  36. package/dist/index.js.map +1 -1
  37. package/dist/utils/custom-policy-rules.d.ts +21 -0
  38. package/dist/utils/custom-policy-rules.d.ts.map +1 -0
  39. package/dist/utils/custom-policy-rules.js +71 -0
  40. package/dist/utils/custom-policy-rules.js.map +1 -0
  41. package/dist/utils/plan-cache.d.ts.map +1 -1
  42. package/dist/utils/plan-cache.js +4 -0
  43. package/dist/utils/plan-cache.js.map +1 -1
  44. package/dist/utils/policy-audit.d.ts +29 -0
  45. package/dist/utils/policy-audit.d.ts.map +1 -0
  46. package/dist/utils/policy-audit.js +208 -0
  47. package/dist/utils/policy-audit.js.map +1 -0
  48. package/dist/utils/policy-exceptions.d.ts +96 -0
  49. package/dist/utils/policy-exceptions.d.ts.map +1 -0
  50. package/dist/utils/policy-exceptions.js +389 -0
  51. package/dist/utils/policy-exceptions.js.map +1 -0
  52. package/dist/utils/policy-governance.d.ts +24 -0
  53. package/dist/utils/policy-governance.d.ts.map +1 -0
  54. package/dist/utils/policy-governance.js +124 -0
  55. package/dist/utils/policy-governance.js.map +1 -0
  56. package/dist/utils/policy-packs.d.ts +72 -1
  57. package/dist/utils/policy-packs.d.ts.map +1 -1
  58. package/dist/utils/policy-packs.js +285 -0
  59. package/dist/utils/policy-packs.js.map +1 -1
  60. package/package.json +1 -1
@@ -54,6 +54,10 @@ const ignore_1 = require("../utils/ignore");
54
54
  const project_root_1 = require("../utils/project-root");
55
55
  const brain_context_1 = require("../utils/brain-context");
56
56
  const policy_packs_1 = require("../utils/policy-packs");
57
+ const custom_policy_rules_1 = require("../utils/custom-policy-rules");
58
+ const policy_exceptions_1 = require("../utils/policy-exceptions");
59
+ const policy_governance_1 = require("../utils/policy-governance");
60
+ const policy_audit_1 = require("../utils/policy-audit");
57
61
  // Import chalk with fallback
58
62
  let chalk;
59
63
  try {
@@ -78,21 +82,21 @@ catch {
78
82
  * Check if a file path should be excluded from verification analysis
79
83
  * Excludes internal/system files that should not count towards plan adherence
80
84
  */
81
- const IGNORED_METADATA_FILES = new Set(['neurcode.config.json']);
82
- const IGNORED_DIRECTORIES = ['.neurcode/'];
85
+ const IGNORED_METADATA_FILE_PATTERN = /(^|\/)neurcode\.config\.json$/i;
86
+ const IGNORED_DIRECTORIES = ['.git/', 'node_modules/'];
83
87
  function isExcludedFile(filePath) {
84
88
  // Normalize path separators (handle both / and \)
85
89
  const normalizedPath = filePath.replace(/\\/g, '/');
86
90
  // Ignore specific metadata files (these should never be scope/policy violations)
87
- if (IGNORED_METADATA_FILES.has(normalizedPath)) {
91
+ if (IGNORED_METADATA_FILE_PATTERN.test(normalizedPath)) {
92
+ return true;
93
+ }
94
+ // Ignore .neurcode internals at any nesting level (monorepo-safe)
95
+ if (/(^|\/)\.neurcode\//.test(normalizedPath)) {
88
96
  return true;
89
97
  }
90
98
  // Check if path starts with any excluded prefix
91
- const excludedPrefixes = [
92
- ...IGNORED_DIRECTORIES,
93
- '.git/',
94
- 'node_modules/',
95
- ];
99
+ const excludedPrefixes = [...IGNORED_DIRECTORIES];
96
100
  // Check prefixes
97
101
  for (const prefix of excludedPrefixes) {
98
102
  if (normalizedPath.startsWith(prefix)) {
@@ -203,76 +207,17 @@ function getRuntimeIgnoreSetFromEnv() {
203
207
  .map((item) => toUnixPath(item.trim()))
204
208
  .filter(Boolean));
205
209
  }
206
- /**
207
- * Map a dashboard custom policy (natural language) to a policy-engine Rule.
208
- * Supports "No console.log", "No debugger", and generic line patterns.
209
- */
210
- function customPolicyToRule(p) {
211
- const sev = p.severity === 'high' ? 'block' : 'warn';
212
- const text = (p.rule_text || '').trim().toLowerCase();
213
- if (!text)
214
- return null;
215
- // Known patterns -> suspicious-keywords (checks added lines for these strings)
216
- if (/console\.log|no\s+console\.log|do not use console\.log|ban console\.log/.test(text)) {
217
- return {
218
- id: `custom-${p.id}`,
219
- name: 'No console.log',
220
- description: p.rule_text,
221
- enabled: true,
222
- severity: sev,
223
- type: 'suspicious-keywords',
224
- keywords: ['console.log'],
225
- };
226
- }
227
- if (/debugger|no\s+debugger|do not use debugger/.test(text)) {
228
- return {
229
- id: `custom-${p.id}`,
230
- name: 'No debugger',
231
- description: p.rule_text,
232
- enabled: true,
233
- severity: sev,
234
- type: 'suspicious-keywords',
235
- keywords: ['debugger'],
236
- };
237
- }
238
- if (/eval\s*\(|no\s+eval|do not use eval/.test(text)) {
239
- return {
240
- id: `custom-${p.id}`,
241
- name: 'No eval',
242
- description: p.rule_text,
243
- enabled: true,
244
- severity: sev,
245
- type: 'suspicious-keywords',
246
- keywords: ['eval('],
247
- };
248
- }
249
- // Fallback: line-pattern on added lines using a safe regex from the rule text
250
- // Use first quoted phrase or first alphanumeric phrase as pattern
251
- const quoted = /['"`]([^'"`]+)['"`]/.exec(p.rule_text);
252
- const phrase = quoted?.[1] ?? p.rule_text.replace(/^(no|don't|do not use|ban|avoid)\s+/i, '').trim().slice(0, 80);
253
- if (!phrase)
254
- return null;
255
- const escaped = phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
256
- return {
257
- id: `custom-${p.id}`,
258
- name: `Custom: ${p.rule_text.slice(0, 50)}`,
259
- description: p.rule_text,
260
- enabled: true,
261
- severity: sev,
262
- type: 'line-pattern',
263
- pattern: escaped,
264
- matchType: 'added',
265
- };
266
- }
267
210
  async function buildEffectivePolicyRules(client, projectRoot, useDashboardPolicies) {
268
211
  const defaultPolicy = (0, policy_engine_1.createDefaultPolicy)();
269
212
  const customRules = [];
213
+ const customPolicies = [];
270
214
  const installedPack = (0, policy_packs_1.getInstalledPolicyPackRules)(projectRoot);
271
215
  const policyPackRules = installedPack?.rules ? [...installedPack.rules] : [];
272
216
  if (useDashboardPolicies) {
273
- const customPolicies = await client.getActiveCustomPolicies();
274
- for (const p of customPolicies) {
275
- const r = customPolicyToRule(p);
217
+ const loadedCustomPolicies = await client.getActiveCustomPolicies();
218
+ for (const p of loadedCustomPolicies) {
219
+ customPolicies.push(p);
220
+ const r = (0, custom_policy_rules_1.customPolicyToRule)(p);
276
221
  if (r) {
277
222
  customRules.push(r);
278
223
  }
@@ -281,34 +226,183 @@ async function buildEffectivePolicyRules(client, projectRoot, useDashboardPolici
281
226
  return {
282
227
  allRules: [...defaultPolicy.rules, ...policyPackRules, ...customRules],
283
228
  customRules,
229
+ customPolicies,
284
230
  policyPackRules,
285
231
  policyPack: installedPack,
232
+ includeDashboardPolicies: useDashboardPolicies,
233
+ };
234
+ }
235
+ const POLICY_AUDIT_FILE = 'neurcode.policy.audit.log.jsonl';
236
+ function isEnabledFlag(value) {
237
+ if (!value)
238
+ return false;
239
+ const normalized = value.trim().toLowerCase();
240
+ return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
241
+ }
242
+ function policyLockMismatchMessage(mismatches) {
243
+ if (mismatches.length === 0) {
244
+ return 'Policy lock baseline check failed';
245
+ }
246
+ return `Policy lock mismatch: ${mismatches.map((item) => `[${item.code}] ${item.message}`).join('; ')}`;
247
+ }
248
+ function toPolicyLockViolations(mismatches) {
249
+ return mismatches.map((item) => ({
250
+ file: 'neurcode.policy.lock.json',
251
+ rule: `policy_lock:${item.code.toLowerCase()}`,
252
+ severity: 'block',
253
+ message: item.message,
254
+ }));
255
+ }
256
+ function resolvePolicyDecisionFromViolations(violations) {
257
+ let hasWarn = false;
258
+ for (const violation of violations) {
259
+ const severity = String(violation.severity || '').toLowerCase();
260
+ if (severity === 'block') {
261
+ return 'block';
262
+ }
263
+ if (severity === 'warn') {
264
+ hasWarn = true;
265
+ }
266
+ }
267
+ return hasWarn ? 'warn' : 'allow';
268
+ }
269
+ function explainExceptionEligibilityReason(reason) {
270
+ switch (reason) {
271
+ case 'approval_required':
272
+ return 'exception exists but approvals are required';
273
+ case 'insufficient_approvals':
274
+ return 'exception exists but approval threshold is not met';
275
+ case 'self_approval_only':
276
+ return 'exception only has requester self-approval';
277
+ case 'approver_not_allowed':
278
+ return 'exception approvals are from non-allowlisted approvers';
279
+ default:
280
+ return 'exception is inactive or expired';
281
+ }
282
+ }
283
+ function resolveAuditIntegrityStatus(requireIntegrity, auditIntegrity) {
284
+ const issues = [...auditIntegrity.issues];
285
+ if (requireIntegrity && auditIntegrity.count === 0) {
286
+ issues.push('audit chain has no events; commit neurcode.policy.audit.log.jsonl');
287
+ }
288
+ return {
289
+ valid: issues.length === 0,
290
+ issues,
286
291
  };
287
292
  }
288
293
  /**
289
294
  * Execute policy-only verification (General Governance mode)
290
295
  * Returns the exit code to use
291
296
  */
292
- async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRoot, config, client) {
297
+ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRoot, config, client, source) {
293
298
  if (!options.json) {
294
299
  console.log(chalk.cyan('🛡️ General Governance mode (policy only, no plan linked)\n'));
295
300
  }
296
301
  let policyViolations = [];
297
302
  let policyDecision = 'allow';
303
+ const requirePolicyLock = options.requirePolicyLock === true || isEnabledFlag(process.env.NEURCODE_VERIFY_REQUIRE_POLICY_LOCK);
304
+ const skipPolicyLock = options.skipPolicyLock === true || isEnabledFlag(process.env.NEURCODE_VERIFY_SKIP_POLICY_LOCK);
305
+ const lockRead = (0, policy_packs_1.readPolicyLockFile)(projectRoot);
306
+ const includeDashboardPolicies = lockRead.lock
307
+ ? lockRead.lock.customPolicies.mode === 'dashboard'
308
+ : Boolean(config.apiKey);
309
+ let effectiveRulesLoadError = null;
298
310
  let effectiveRules = {
299
311
  allRules: (0, policy_engine_1.createDefaultPolicy)().rules,
300
312
  customRules: [],
313
+ customPolicies: [],
301
314
  policyPackRules: [],
302
315
  policyPack: null,
316
+ includeDashboardPolicies,
303
317
  };
304
318
  try {
305
- effectiveRules = await buildEffectivePolicyRules(client, projectRoot, Boolean(config.apiKey));
319
+ effectiveRules = await buildEffectivePolicyRules(client, projectRoot, includeDashboardPolicies);
306
320
  }
307
321
  catch (error) {
322
+ effectiveRulesLoadError = error instanceof Error ? error.message : 'Unknown error';
323
+ const installedPack = (0, policy_packs_1.getInstalledPolicyPackRules)(projectRoot);
324
+ const fallbackPolicyPackRules = installedPack?.rules ? [...installedPack.rules] : [];
325
+ effectiveRules = {
326
+ allRules: [...(0, policy_engine_1.createDefaultPolicy)().rules, ...fallbackPolicyPackRules],
327
+ customRules: [],
328
+ customPolicies: [],
329
+ policyPackRules: fallbackPolicyPackRules,
330
+ policyPack: installedPack,
331
+ includeDashboardPolicies,
332
+ };
308
333
  if (!options.json) {
309
334
  console.log(chalk.dim(' Could not load dashboard custom policies, using local/default policy rules only'));
310
335
  }
311
336
  }
337
+ let policyLockEvaluation = {
338
+ enforced: false,
339
+ matched: true,
340
+ lockPresent: lockRead.lock !== null,
341
+ lockPath: lockRead.path,
342
+ mismatches: [],
343
+ };
344
+ if (!skipPolicyLock) {
345
+ const currentSnapshot = (0, policy_packs_1.buildPolicyStateSnapshot)({
346
+ policyPack: effectiveRules.policyPack,
347
+ policyPackRules: effectiveRules.policyPackRules,
348
+ customPolicies: effectiveRules.customPolicies,
349
+ customRules: effectiveRules.customRules,
350
+ includeDashboardPolicies: effectiveRules.includeDashboardPolicies,
351
+ });
352
+ const lockValidation = (0, policy_packs_1.evaluatePolicyLock)(projectRoot, currentSnapshot, {
353
+ requireLock: requirePolicyLock,
354
+ });
355
+ policyLockEvaluation = {
356
+ enforced: lockValidation.enforced,
357
+ matched: lockValidation.matched,
358
+ lockPresent: lockValidation.lockPresent,
359
+ lockPath: lockValidation.lockPath,
360
+ mismatches: [...lockValidation.mismatches],
361
+ };
362
+ if (effectiveRulesLoadError && includeDashboardPolicies) {
363
+ policyLockEvaluation.mismatches.unshift({
364
+ code: 'POLICY_LOCK_CUSTOM_POLICIES_MISMATCH',
365
+ message: `Failed to load dashboard custom policies: ${effectiveRulesLoadError}`,
366
+ });
367
+ policyLockEvaluation.matched = false;
368
+ }
369
+ }
370
+ if (policyLockEvaluation.enforced && !policyLockEvaluation.matched) {
371
+ const message = policyLockMismatchMessage(policyLockEvaluation.mismatches);
372
+ if (options.json) {
373
+ console.log(JSON.stringify({
374
+ grade: 'F',
375
+ score: 0,
376
+ verdict: 'FAIL',
377
+ violations: toPolicyLockViolations(policyLockEvaluation.mismatches),
378
+ message,
379
+ scopeGuardPassed: true,
380
+ bloatCount: 0,
381
+ bloatFiles: [],
382
+ plannedFilesModified: 0,
383
+ totalPlannedFiles: 0,
384
+ adherenceScore: 0,
385
+ mode: 'policy_only',
386
+ policyOnly: true,
387
+ policyOnlySource: source,
388
+ policyLock: {
389
+ enforced: true,
390
+ matched: false,
391
+ path: policyLockEvaluation.lockPath,
392
+ mismatches: policyLockEvaluation.mismatches,
393
+ },
394
+ }, null, 2));
395
+ }
396
+ else {
397
+ console.log(chalk.red('❌ Policy lock baseline mismatch.'));
398
+ console.log(chalk.dim(` Lock file: ${policyLockEvaluation.lockPath}`));
399
+ policyLockEvaluation.mismatches.forEach((item) => {
400
+ console.log(chalk.red(` • [${item.code}] ${item.message}`));
401
+ });
402
+ console.log(chalk.dim('\n If drift is intentional, regenerate baseline with `neurcode policy lock`.\n'));
403
+ }
404
+ return 2;
405
+ }
312
406
  if (!options.json && effectiveRules.customRules.length > 0) {
313
407
  console.log(chalk.dim(` Evaluating ${effectiveRules.customRules.length} custom policy rule(s) from dashboard`));
314
408
  }
@@ -322,7 +416,39 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
322
416
  const policyResult = (0, policy_engine_1.evaluateRules)(diffFilesForPolicy, effectiveRules.allRules);
323
417
  policyViolations = (policyResult.violations || []);
324
418
  policyViolations = policyViolations.filter((v) => !ignoreFilter(v.file));
325
- policyDecision = policyViolations.length > 0 ? policyResult.decision : 'allow';
419
+ const governance = (0, policy_governance_1.readPolicyGovernanceConfig)(projectRoot);
420
+ const auditIntegrity = (0, policy_audit_1.verifyPolicyAuditIntegrity)(projectRoot);
421
+ const auditIntegrityStatus = resolveAuditIntegrityStatus(governance.audit.requireIntegrity, auditIntegrity);
422
+ const configuredPolicyExceptions = (0, policy_exceptions_1.readPolicyExceptions)(projectRoot);
423
+ const exceptionDecision = (0, policy_exceptions_1.applyPolicyExceptions)(policyViolations, configuredPolicyExceptions, {
424
+ requireApproval: governance.exceptionApprovals.required,
425
+ minApprovals: governance.exceptionApprovals.minApprovals,
426
+ disallowSelfApproval: governance.exceptionApprovals.disallowSelfApproval,
427
+ allowedApprovers: governance.exceptionApprovals.allowedApprovers,
428
+ });
429
+ const suppressedViolations = exceptionDecision.suppressedViolations.filter((item) => !ignoreFilter(item.file));
430
+ const blockedViolations = exceptionDecision.blockedViolations
431
+ .filter((item) => !ignoreFilter(item.file))
432
+ .map((item) => ({
433
+ file: item.file,
434
+ rule: item.rule,
435
+ severity: 'block',
436
+ message: `Exception ${item.exceptionId} cannot be applied: ${explainExceptionEligibilityReason(item.eligibilityReason)}`,
437
+ ...(item.line != null ? { line: item.line } : {}),
438
+ }));
439
+ policyViolations = [
440
+ ...exceptionDecision.remainingViolations.filter((item) => !ignoreFilter(item.file)),
441
+ ...blockedViolations,
442
+ ];
443
+ if (governance.audit.requireIntegrity && !auditIntegrityStatus.valid) {
444
+ policyViolations.push({
445
+ file: POLICY_AUDIT_FILE,
446
+ rule: 'policy_audit_integrity',
447
+ severity: 'block',
448
+ message: `Policy audit chain is invalid: ${auditIntegrityStatus.issues.join('; ') || 'unknown issue'}`,
449
+ });
450
+ }
451
+ policyDecision = resolvePolicyDecisionFromViolations(policyViolations);
326
452
  const effectiveVerdict = policyDecision === 'block' ? 'FAIL' : policyDecision === 'warn' ? 'WARN' : 'PASS';
327
453
  const grade = effectiveVerdict === 'PASS' ? 'A' : effectiveVerdict === 'WARN' ? 'C' : 'F';
328
454
  const score = effectiveVerdict === 'PASS' ? 100 : effectiveVerdict === 'WARN' ? 50 : 0;
@@ -338,6 +464,42 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
338
464
  : policyViolations.length > 0
339
465
  ? `Policy violations: ${policyViolations.map((v) => `${v.file}: ${v.message || v.rule}`).join('; ')}`
340
466
  : 'Policy check completed';
467
+ const policyExceptionsSummary = {
468
+ configured: configuredPolicyExceptions.length,
469
+ active: exceptionDecision.activeExceptions.length,
470
+ usable: exceptionDecision.usableExceptions.length,
471
+ matched: exceptionDecision.matchedExceptionIds.length,
472
+ suppressed: suppressedViolations.length,
473
+ blocked: blockedViolations.length,
474
+ matchedExceptionIds: exceptionDecision.matchedExceptionIds,
475
+ suppressedViolations: suppressedViolations.map((item) => ({
476
+ file: item.file,
477
+ rule: item.rule,
478
+ severity: item.severity,
479
+ message: item.message,
480
+ exceptionId: item.exceptionId,
481
+ reason: item.reason,
482
+ expiresAt: item.expiresAt,
483
+ ...(item.line != null ? { startLine: item.line } : {}),
484
+ })),
485
+ blockedViolations: blockedViolations.map((item) => ({
486
+ file: item.file,
487
+ rule: item.rule,
488
+ severity: item.severity,
489
+ message: item.message,
490
+ ...(item.line != null ? { startLine: item.line } : {}),
491
+ })),
492
+ };
493
+ const policyGovernanceSummary = {
494
+ exceptionApprovals: governance.exceptionApprovals,
495
+ audit: {
496
+ requireIntegrity: governance.audit.requireIntegrity,
497
+ valid: auditIntegrityStatus.valid,
498
+ issues: auditIntegrityStatus.issues,
499
+ lastHash: auditIntegrity.lastHash,
500
+ eventCount: auditIntegrity.count,
501
+ },
502
+ };
341
503
  if (options.json) {
342
504
  console.log(JSON.stringify({
343
505
  grade,
@@ -351,7 +513,17 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
351
513
  plannedFilesModified: 0,
352
514
  totalPlannedFiles: 0,
353
515
  adherenceScore: score,
516
+ mode: 'policy_only',
354
517
  policyOnly: true,
518
+ policyOnlySource: source,
519
+ policyLock: {
520
+ enforced: policyLockEvaluation.enforced,
521
+ matched: policyLockEvaluation.matched,
522
+ path: policyLockEvaluation.lockPath,
523
+ mismatches: policyLockEvaluation.mismatches,
524
+ },
525
+ policyExceptions: policyExceptionsSummary,
526
+ policyGovernance: policyGovernanceSummary,
355
527
  ...(effectiveRules.policyPack
356
528
  ? {
357
529
  policyPack: {
@@ -374,6 +546,15 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
374
546
  console.log(chalk.red(` • ${v.file}: ${v.message || v.rule}`));
375
547
  });
376
548
  }
549
+ if (policyExceptionsSummary.suppressed > 0) {
550
+ console.log(chalk.yellow(` Policy exceptions applied: ${policyExceptionsSummary.suppressed}`));
551
+ }
552
+ if (policyExceptionsSummary.blocked > 0) {
553
+ console.log(chalk.red(` Policy exceptions blocked by approval governance: ${policyExceptionsSummary.blocked}`));
554
+ }
555
+ if (governance.audit.requireIntegrity && !auditIntegrityStatus.valid) {
556
+ console.log(chalk.red(' Policy audit integrity check failed'));
557
+ }
377
558
  console.log(chalk.dim(`\n${message}`));
378
559
  }
379
560
  return effectiveVerdict === 'FAIL' ? 2 : effectiveVerdict === 'WARN' ? 1 : 0;
@@ -551,16 +732,20 @@ async function verifyCommand(options) {
551
732
  console.log(chalk.dim(` Found ${summary.totalFiles} file(s) changed`));
552
733
  console.log(chalk.dim(` ${summary.totalAdded} lines added, ${summary.totalRemoved} lines removed\n`));
553
734
  }
735
+ const runPolicyOnlyModeAndExit = async (source) => {
736
+ const exitCode = await executePolicyOnlyMode(options, diffFiles, shouldIgnore, projectRoot, config, client, source);
737
+ const changedFiles = diffFiles.map((f) => f.path);
738
+ const verdict = exitCode === 2 ? 'FAIL' : exitCode === 1 ? 'WARN' : 'PASS';
739
+ recordVerifyEvent(verdict, `policy_only_source=${source};exit=${exitCode}`, changedFiles);
740
+ process.exit(exitCode);
741
+ };
554
742
  // ============================================
555
743
  // --policy-only: General Governance (policy only, no plan enforcement)
556
744
  // ============================================
557
745
  if (options.policyOnly) {
558
- const exitCode = await executePolicyOnlyMode(options, diffFiles, shouldIgnore, projectRoot, config, client);
559
- const changedFiles = diffFiles.map((f) => f.path);
560
- const verdict = exitCode === 2 ? 'FAIL' : exitCode === 1 ? 'WARN' : 'PASS';
561
- recordVerifyEvent(verdict, `policy_only=true;exit=${exitCode}`, changedFiles);
562
- process.exit(exitCode);
746
+ await runPolicyOnlyModeAndExit('explicit');
563
747
  }
748
+ const requirePlan = options.requirePlan === true || process.env.NEURCODE_VERIFY_REQUIRE_PLAN === '1';
564
749
  // Get planId: Priority 1: options flag, Priority 2: state file (.neurcode/config.json), Priority 3: legacy config
565
750
  let planId = options.planId;
566
751
  if (!planId) {
@@ -593,19 +778,43 @@ async function verifyCommand(options) {
593
778
  }
594
779
  }
595
780
  }
596
- // If no planId found, fall back to General Governance (Policy Only) mode
781
+ // If no planId found, either enforce strict requirement or fall back to policy-only mode.
597
782
  if (!planId) {
783
+ if (requirePlan) {
784
+ const changedFiles = diffFiles.map((f) => f.path);
785
+ recordVerifyEvent('FAIL', 'missing_plan_id;require_plan=true', changedFiles);
786
+ if (options.json) {
787
+ console.log(JSON.stringify({
788
+ grade: 'F',
789
+ score: 0,
790
+ verdict: 'FAIL',
791
+ violations: [],
792
+ adherenceScore: 0,
793
+ bloatCount: 0,
794
+ bloatFiles: [],
795
+ plannedFilesModified: 0,
796
+ totalPlannedFiles: 0,
797
+ message: 'Plan ID is required in strict mode. Run "neurcode plan" first or pass --plan-id.',
798
+ scopeGuardPassed: false,
799
+ mode: 'plan_required',
800
+ policyOnly: false,
801
+ }, null, 2));
802
+ }
803
+ else {
804
+ console.log(chalk.red('❌ Plan ID is required in strict mode.'));
805
+ console.log(chalk.dim(' Run "neurcode plan" first or pass --plan-id <id>.'));
806
+ console.log(chalk.dim(' Use --policy-only only when intentionally running general governance checks.'));
807
+ }
808
+ process.exit(1);
809
+ }
598
810
  if (!options.json) {
599
811
  console.log(chalk.yellow('⚠️ No Plan ID found. Falling back to General Governance (Policy Only).'));
600
812
  }
601
- options.policyOnly = true;
602
- const exitCode = await executePolicyOnlyMode(options, diffFiles, shouldIgnore, projectRoot, config, client);
603
- const changedFiles = diffFiles.map((f) => f.path);
604
- const verdict = exitCode === 2 ? 'FAIL' : exitCode === 1 ? 'WARN' : 'PASS';
605
- recordVerifyEvent(verdict, `policy_only=fallback;exit=${exitCode}`, changedFiles);
606
- process.exit(exitCode);
813
+ await runPolicyOnlyModeAndExit('fallback_missing_plan');
814
+ }
815
+ if (!planId) {
816
+ throw new Error('Plan ID resolution failed unexpectedly');
607
817
  }
608
- // At this point, planId is guaranteed to be defined
609
818
  const finalPlanId = planId;
610
819
  // ============================================
611
820
  // STRICT SCOPE GUARD - Deterministic Check
@@ -692,6 +901,8 @@ async function verifyCommand(options) {
692
901
  totalPlannedFiles: planFiles.length,
693
902
  message: `Scope violation: ${filteredViolations.length} file(s) modified outside the plan`,
694
903
  scopeGuardPassed: false,
904
+ mode: 'plan_enforced',
905
+ policyOnly: false,
695
906
  };
696
907
  // CRITICAL: Print JSON first, then exit
697
908
  console.log(JSON.stringify(jsonOutput, null, 2));
@@ -731,7 +942,113 @@ async function verifyCommand(options) {
731
942
  console.log('');
732
943
  }
733
944
  }
945
+ const requirePolicyLock = options.requirePolicyLock === true || isEnabledFlag(process.env.NEURCODE_VERIFY_REQUIRE_POLICY_LOCK);
946
+ const skipPolicyLock = options.skipPolicyLock === true || isEnabledFlag(process.env.NEURCODE_VERIFY_SKIP_POLICY_LOCK);
947
+ const lockRead = (0, policy_packs_1.readPolicyLockFile)(projectRoot);
948
+ const useDashboardPolicies = lockRead.lock
949
+ ? lockRead.lock.customPolicies.mode === 'dashboard'
950
+ : Boolean(config.apiKey);
951
+ let effectiveRulesLoadError = null;
952
+ let effectiveRules = {
953
+ allRules: (0, policy_engine_1.createDefaultPolicy)().rules,
954
+ customRules: [],
955
+ customPolicies: [],
956
+ policyPackRules: [],
957
+ policyPack: null,
958
+ includeDashboardPolicies: useDashboardPolicies,
959
+ };
960
+ try {
961
+ effectiveRules = await buildEffectivePolicyRules(client, projectRoot, useDashboardPolicies);
962
+ }
963
+ catch (error) {
964
+ effectiveRulesLoadError = error instanceof Error ? error.message : 'Unknown error';
965
+ const installedPack = (0, policy_packs_1.getInstalledPolicyPackRules)(projectRoot);
966
+ const fallbackPolicyPackRules = installedPack?.rules ? [...installedPack.rules] : [];
967
+ effectiveRules = {
968
+ allRules: [...(0, policy_engine_1.createDefaultPolicy)().rules, ...fallbackPolicyPackRules],
969
+ customRules: [],
970
+ customPolicies: [],
971
+ policyPackRules: fallbackPolicyPackRules,
972
+ policyPack: installedPack,
973
+ includeDashboardPolicies: useDashboardPolicies,
974
+ };
975
+ if (!options.json) {
976
+ console.log(chalk.dim(' Could not load dashboard custom policies, continuing with local/default rules only'));
977
+ }
978
+ }
979
+ let policyLockEvaluation = {
980
+ enforced: false,
981
+ matched: true,
982
+ lockPresent: lockRead.lock !== null,
983
+ lockPath: lockRead.path,
984
+ mismatches: [],
985
+ };
986
+ if (!skipPolicyLock) {
987
+ const currentSnapshot = (0, policy_packs_1.buildPolicyStateSnapshot)({
988
+ policyPack: effectiveRules.policyPack,
989
+ policyPackRules: effectiveRules.policyPackRules,
990
+ customPolicies: effectiveRules.customPolicies,
991
+ customRules: effectiveRules.customRules,
992
+ includeDashboardPolicies: effectiveRules.includeDashboardPolicies,
993
+ });
994
+ const lockValidation = (0, policy_packs_1.evaluatePolicyLock)(projectRoot, currentSnapshot, {
995
+ requireLock: requirePolicyLock,
996
+ });
997
+ policyLockEvaluation = {
998
+ enforced: lockValidation.enforced,
999
+ matched: lockValidation.matched,
1000
+ lockPresent: lockValidation.lockPresent,
1001
+ lockPath: lockValidation.lockPath,
1002
+ mismatches: [...lockValidation.mismatches],
1003
+ };
1004
+ if (effectiveRulesLoadError && useDashboardPolicies) {
1005
+ policyLockEvaluation.mismatches.unshift({
1006
+ code: 'POLICY_LOCK_CUSTOM_POLICIES_MISMATCH',
1007
+ message: `Failed to load dashboard custom policies: ${effectiveRulesLoadError}`,
1008
+ });
1009
+ policyLockEvaluation.matched = false;
1010
+ }
1011
+ if (policyLockEvaluation.enforced && !policyLockEvaluation.matched) {
1012
+ const message = policyLockMismatchMessage(policyLockEvaluation.mismatches);
1013
+ recordVerifyEvent('FAIL', 'policy_lock_mismatch', diffFiles.map((f) => f.path), finalPlanId);
1014
+ if (options.json) {
1015
+ console.log(JSON.stringify({
1016
+ grade: 'F',
1017
+ score: 0,
1018
+ verdict: 'FAIL',
1019
+ violations: toPolicyLockViolations(policyLockEvaluation.mismatches),
1020
+ adherenceScore: 0,
1021
+ bloatCount: 0,
1022
+ bloatFiles: [],
1023
+ plannedFilesModified: 0,
1024
+ totalPlannedFiles: 0,
1025
+ message,
1026
+ scopeGuardPassed,
1027
+ mode: 'plan_enforced',
1028
+ policyOnly: false,
1029
+ policyLock: {
1030
+ enforced: true,
1031
+ matched: false,
1032
+ path: policyLockEvaluation.lockPath,
1033
+ mismatches: policyLockEvaluation.mismatches,
1034
+ },
1035
+ }, null, 2));
1036
+ }
1037
+ else {
1038
+ console.log(chalk.red('\n❌ Policy lock baseline mismatch'));
1039
+ console.log(chalk.dim(` Lock file: ${policyLockEvaluation.lockPath}`));
1040
+ policyLockEvaluation.mismatches.forEach((item) => {
1041
+ console.log(chalk.red(` • [${item.code}] ${item.message}`));
1042
+ });
1043
+ console.log(chalk.dim('\n If this drift is intentional, regenerate baseline with `neurcode policy lock`.\n'));
1044
+ }
1045
+ process.exit(2);
1046
+ }
1047
+ }
734
1048
  // Check user tier - Policy Compliance and A-F Grading are PRO features
1049
+ const governance = (0, policy_governance_1.readPolicyGovernanceConfig)(projectRoot);
1050
+ const auditIntegrity = (0, policy_audit_1.verifyPolicyAuditIntegrity)(projectRoot);
1051
+ const auditIntegrityStatus = resolveAuditIntegrityStatus(governance.audit.requireIntegrity, auditIntegrity);
735
1052
  const { getUserTier } = await Promise.resolve().then(() => __importStar(require('../utils/tier')));
736
1053
  const tier = await getUserTier();
737
1054
  if (tier === 'FREE') {
@@ -759,36 +1076,105 @@ async function verifyCommand(options) {
759
1076
  totalPlannedFiles: 0,
760
1077
  message: 'Basic file change summary (PRO required for policy verification)',
761
1078
  scopeGuardPassed: false,
1079
+ mode: 'plan_enforced',
1080
+ policyOnly: false,
762
1081
  tier: 'FREE',
1082
+ policyLock: {
1083
+ enforced: policyLockEvaluation.enforced,
1084
+ matched: policyLockEvaluation.matched,
1085
+ path: policyLockEvaluation.lockPath,
1086
+ mismatches: policyLockEvaluation.mismatches,
1087
+ },
1088
+ policyGovernance: {
1089
+ exceptionApprovals: governance.exceptionApprovals,
1090
+ audit: {
1091
+ requireIntegrity: governance.audit.requireIntegrity,
1092
+ valid: auditIntegrityStatus.valid,
1093
+ issues: auditIntegrityStatus.issues,
1094
+ lastHash: auditIntegrity.lastHash,
1095
+ eventCount: auditIntegrity.count,
1096
+ },
1097
+ },
763
1098
  }, null, 2));
764
1099
  }
765
1100
  process.exit(0);
766
1101
  }
767
1102
  let policyViolations = [];
768
1103
  let policyDecision = 'allow';
769
- let effectiveRules = {
770
- allRules: (0, policy_engine_1.createDefaultPolicy)().rules,
771
- customRules: [],
772
- policyPackRules: [],
773
- policyPack: null,
1104
+ const diffFilesForPolicy = diffFiles.filter((f) => !shouldIgnore(f.path));
1105
+ const policyResult = (0, policy_engine_1.evaluateRules)(diffFilesForPolicy, effectiveRules.allRules);
1106
+ policyViolations = policyResult.violations.filter((v) => !shouldIgnore(v.file));
1107
+ const configuredPolicyExceptions = (0, policy_exceptions_1.readPolicyExceptions)(projectRoot);
1108
+ const exceptionDecision = (0, policy_exceptions_1.applyPolicyExceptions)(policyViolations, configuredPolicyExceptions, {
1109
+ requireApproval: governance.exceptionApprovals.required,
1110
+ minApprovals: governance.exceptionApprovals.minApprovals,
1111
+ disallowSelfApproval: governance.exceptionApprovals.disallowSelfApproval,
1112
+ allowedApprovers: governance.exceptionApprovals.allowedApprovers,
1113
+ });
1114
+ const suppressedPolicyViolations = exceptionDecision.suppressedViolations.filter((item) => !shouldIgnore(item.file));
1115
+ const blockedPolicyViolations = exceptionDecision.blockedViolations
1116
+ .filter((item) => !shouldIgnore(item.file))
1117
+ .map((item) => ({
1118
+ file: item.file,
1119
+ rule: item.rule,
1120
+ severity: 'block',
1121
+ message: `Exception ${item.exceptionId} cannot be applied: ${explainExceptionEligibilityReason(item.eligibilityReason)}`,
1122
+ ...(item.line != null ? { line: item.line } : {}),
1123
+ }));
1124
+ policyViolations = [
1125
+ ...exceptionDecision.remainingViolations.filter((item) => !shouldIgnore(item.file)),
1126
+ ...blockedPolicyViolations,
1127
+ ];
1128
+ if (governance.audit.requireIntegrity && !auditIntegrityStatus.valid) {
1129
+ policyViolations.push({
1130
+ file: POLICY_AUDIT_FILE,
1131
+ rule: 'policy_audit_integrity',
1132
+ severity: 'block',
1133
+ message: `Policy audit chain is invalid: ${auditIntegrityStatus.issues.join('; ') || 'unknown issue'}`,
1134
+ });
1135
+ }
1136
+ policyDecision = resolvePolicyDecisionFromViolations(policyViolations);
1137
+ const policyExceptionsSummary = {
1138
+ configured: configuredPolicyExceptions.length,
1139
+ active: exceptionDecision.activeExceptions.length,
1140
+ usable: exceptionDecision.usableExceptions.length,
1141
+ matched: exceptionDecision.matchedExceptionIds.length,
1142
+ suppressed: suppressedPolicyViolations.length,
1143
+ blocked: blockedPolicyViolations.length,
1144
+ matchedExceptionIds: exceptionDecision.matchedExceptionIds,
1145
+ suppressedViolations: suppressedPolicyViolations.map((item) => ({
1146
+ file: item.file,
1147
+ rule: item.rule,
1148
+ severity: item.severity,
1149
+ message: item.message,
1150
+ exceptionId: item.exceptionId,
1151
+ reason: item.reason,
1152
+ expiresAt: item.expiresAt,
1153
+ ...(item.line != null ? { startLine: item.line } : {}),
1154
+ })),
1155
+ blockedViolations: blockedPolicyViolations.map((item) => ({
1156
+ file: item.file,
1157
+ rule: item.rule,
1158
+ severity: item.severity,
1159
+ message: item.message,
1160
+ ...(item.line != null ? { startLine: item.line } : {}),
1161
+ })),
774
1162
  };
775
- try {
776
- effectiveRules = await buildEffectivePolicyRules(client, projectRoot, Boolean(config.apiKey));
777
- const diffFilesForPolicy = diffFiles.filter((f) => !shouldIgnore(f.path));
778
- const policyResult = (0, policy_engine_1.evaluateRules)(diffFilesForPolicy, effectiveRules.allRules);
779
- policyViolations = policyResult.violations.filter((v) => !shouldIgnore(v.file));
780
- policyDecision = policyViolations.length > 0 ? policyResult.decision : 'allow';
781
- if (!options.json && effectiveRules.customRules.length > 0) {
782
- console.log(chalk.dim(` Evaluating ${effectiveRules.customRules.length} custom policy rule(s) from dashboard`));
783
- }
784
- if (!options.json && effectiveRules.policyPack && effectiveRules.policyPackRules.length > 0) {
785
- console.log(chalk.dim(` Evaluating policy pack: ${effectiveRules.policyPack.packName} (${effectiveRules.policyPack.packId}@${effectiveRules.policyPack.version}, ${effectiveRules.policyPackRules.length} rule(s))`));
786
- }
1163
+ const policyGovernanceSummary = {
1164
+ exceptionApprovals: governance.exceptionApprovals,
1165
+ audit: {
1166
+ requireIntegrity: governance.audit.requireIntegrity,
1167
+ valid: auditIntegrityStatus.valid,
1168
+ issues: auditIntegrityStatus.issues,
1169
+ lastHash: auditIntegrity.lastHash,
1170
+ eventCount: auditIntegrity.count,
1171
+ },
1172
+ };
1173
+ if (!options.json && effectiveRules.customRules.length > 0) {
1174
+ console.log(chalk.dim(` Evaluating ${effectiveRules.customRules.length} custom policy rule(s) from dashboard`));
787
1175
  }
788
- catch (error) {
789
- if (!options.json) {
790
- console.log(chalk.dim(' Could not load dashboard custom policies, continuing with local/default rules only'));
791
- }
1176
+ if (!options.json && effectiveRules.policyPack && effectiveRules.policyPackRules.length > 0) {
1177
+ console.log(chalk.dim(` Evaluating policy pack: ${effectiveRules.policyPack.packName} (${effectiveRules.policyPack.packId}@${effectiveRules.policyPack.version}, ${effectiveRules.policyPackRules.length} rule(s))`));
792
1178
  }
793
1179
  // Prepare diff stats and changed files for API
794
1180
  const diffStats = {
@@ -834,9 +1220,12 @@ async function verifyCommand(options) {
834
1220
  // Apply custom policy verdict: block from dashboard overrides API verdict
835
1221
  const policyBlock = policyDecision === 'block' && policyViolations.length > 0;
836
1222
  const effectiveVerdict = policyBlock ? 'FAIL' : verifyResult.verdict;
837
- const effectiveMessage = policyBlock
1223
+ const policyMessageBase = policyBlock
838
1224
  ? `Custom policy violations: ${policyViolations.map(v => `${v.file}: ${v.message || v.rule}`).join('; ')}. ${verifyResult.message}`
839
1225
  : verifyResult.message;
1226
+ const effectiveMessage = policyExceptionsSummary.suppressed > 0
1227
+ ? `${policyMessageBase} Policy exceptions suppressed ${policyExceptionsSummary.suppressed} violation(s).`
1228
+ : policyMessageBase;
840
1229
  // Calculate grade from effective verdict and score
841
1230
  // CRITICAL: 0/0 planned files = 'F' (Incomplete), not 'B'
842
1231
  // Bloat automatically drops grade by at least one letter
@@ -882,7 +1271,13 @@ async function verifyCommand(options) {
882
1271
  const changedPathsForBrain = diffFiles
883
1272
  .flatMap((file) => [file.path, file.oldPath])
884
1273
  .filter((value) => Boolean(value));
885
- recordVerifyEvent(effectiveVerdict, `adherence=${verifyResult.adherenceScore};bloat=${verifyResult.bloatCount};scopeGuard=${scopeGuardPassed ? 1 : 0};policy=${policyDecision}`, changedPathsForBrain, finalPlanId);
1274
+ recordVerifyEvent(effectiveVerdict, `adherence=${verifyResult.adherenceScore};bloat=${verifyResult.bloatCount};scopeGuard=${scopeGuardPassed ? 1 : 0};policy=${policyDecision};policyExceptions=${policyExceptionsSummary.suppressed}`, changedPathsForBrain, finalPlanId);
1275
+ const shouldForceGovernancePass = scopeGuardPassed &&
1276
+ !policyBlock &&
1277
+ (effectiveVerdict === 'PASS' ||
1278
+ ((verifyResult.verdict === 'FAIL' || verifyResult.verdict === 'WARN') &&
1279
+ policyViolations.length === 0 &&
1280
+ verifyResult.bloatCount > 0));
886
1281
  // If JSON output requested, output JSON and exit
887
1282
  if (options.json) {
888
1283
  const filteredBloatFiles = (verifyResult.bloatFiles || []).filter((f) => !shouldIgnore(f));
@@ -912,6 +1307,16 @@ async function verifyCommand(options) {
912
1307
  bloatFiles: filteredBloatFiles,
913
1308
  plannedFilesModified: verifyResult.plannedFilesModified,
914
1309
  totalPlannedFiles: verifyResult.totalPlannedFiles,
1310
+ mode: 'plan_enforced',
1311
+ policyOnly: false,
1312
+ policyLock: {
1313
+ enforced: policyLockEvaluation.enforced,
1314
+ matched: policyLockEvaluation.matched,
1315
+ path: policyLockEvaluation.lockPath,
1316
+ mismatches: policyLockEvaluation.mismatches,
1317
+ },
1318
+ policyExceptions: policyExceptionsSummary,
1319
+ policyGovernance: policyGovernanceSummary,
915
1320
  ...(policyViolations.length > 0 && { policyDecision }),
916
1321
  ...(effectiveRules.policyPack
917
1322
  ? {
@@ -948,7 +1353,7 @@ async function verifyCommand(options) {
948
1353
  });
949
1354
  }
950
1355
  // Exit based on effective verdict (same logic as below)
951
- if (scopeGuardPassed && !policyBlock) {
1356
+ if (shouldForceGovernancePass) {
952
1357
  process.exit(0);
953
1358
  }
954
1359
  if (effectiveVerdict === 'FAIL') {
@@ -971,6 +1376,25 @@ async function verifyCommand(options) {
971
1376
  bloatFiles: displayBloatFiles,
972
1377
  bloatCount: displayBloatFiles.length,
973
1378
  }, policyViolations);
1379
+ if (policyExceptionsSummary.suppressed > 0) {
1380
+ console.log(chalk.yellow(`\n⚠️ Policy exceptions applied: ${policyExceptionsSummary.suppressed}`));
1381
+ if (policyExceptionsSummary.matchedExceptionIds.length > 0) {
1382
+ console.log(chalk.dim(` Exception IDs: ${policyExceptionsSummary.matchedExceptionIds.join(', ')}`));
1383
+ }
1384
+ }
1385
+ if (policyExceptionsSummary.blocked > 0) {
1386
+ console.log(chalk.red(`\n⛔ Policy exceptions blocked by approval governance: ${policyExceptionsSummary.blocked}`));
1387
+ const sample = policyExceptionsSummary.blockedViolations.slice(0, 5);
1388
+ sample.forEach((item) => {
1389
+ console.log(chalk.red(` • ${item.file}: ${item.message || item.rule}`));
1390
+ });
1391
+ }
1392
+ if (governance.audit.requireIntegrity && !auditIntegrityStatus.valid) {
1393
+ console.log(chalk.red('\n⛔ Policy audit integrity enforcement is enabled and chain verification failed.'));
1394
+ auditIntegrityStatus.issues.slice(0, 5).forEach((issue) => {
1395
+ console.log(chalk.red(` • ${issue}`));
1396
+ });
1397
+ }
974
1398
  }
975
1399
  // Report to Neurcode Cloud if --record flag is set
976
1400
  if (options.record) {
@@ -1001,9 +1425,9 @@ async function verifyCommand(options) {
1001
1425
  );
1002
1426
  }
1003
1427
  }
1004
- // Governance takes priority over Grading
1005
- // If Scope Guard passed (all files approved or allowed) and no policy block, always PASS
1006
- if (scopeGuardPassed && !policyBlock) {
1428
+ // Governance override: keep PASS only when scope guard passes and failure is due
1429
+ // to server-side bloat mismatch (allowed files unknown to verify API).
1430
+ if (shouldForceGovernancePass) {
1007
1431
  if ((verifyResult.verdict === 'FAIL' || verifyResult.verdict === 'WARN') && policyViolations.length === 0) {
1008
1432
  if (!options.json) {
1009
1433
  console.log(chalk.yellow('\n⚠️ Plan deviation allowed'));
@@ -1011,6 +1435,9 @@ async function verifyCommand(options) {
1011
1435
  console.log(chalk.dim(' Governance check passed - proceeding with exit code 0.\n'));
1012
1436
  }
1013
1437
  }
1438
+ if (!options.json && policyExceptionsSummary.suppressed > 0) {
1439
+ console.log(chalk.yellow(` Policy exceptions applied: ${policyExceptionsSummary.suppressed}`));
1440
+ }
1014
1441
  process.exit(0);
1015
1442
  }
1016
1443
  // If scope guard didn't pass (or failed to check) or policy blocked, use effective verdict