delimit-cli 4.1.44 → 4.1.47

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.1.45] - 2026-04-09
4
+
5
+ ### Fixed
6
+ - **Shim rename-hack removed** — install no longer races with npm reinstalls that clobbered `/usr/bin/claude` back to a symlink, causing `[Delimit] claude not found in PATH` mid-session. Shim now relies purely on `$HOME/.delimit/shims` being first in `PATH` plus a PATH-strip lookup for the real binary. Fixes regressions from the claude-real rename+wrap install mechanism.
7
+ - Shim exit screen parity and CLI lint output parity (LED-078, LED-087).
8
+
3
9
  ## [4.20.0] - 2026-04-20
4
10
 
5
11
  *The highest state of AI governance.*
@@ -3704,17 +3704,127 @@ program
3704
3704
  .description('Verify Delimit setup and diagnose common issues')
3705
3705
  .option('--ci', 'Output JSON and exit non-zero on failures (for pipelines)')
3706
3706
  .option('--fix', 'Automatically fix issues that have safe auto-fixes')
3707
+ .option('--dry-run', 'Preview what doctor --fix would create/modify without making changes')
3708
+ .option('--undo', 'Revert changes made by the last doctor --fix run')
3707
3709
  .action(async (opts) => {
3708
3710
  const ciMode = !!opts.ci;
3709
3711
  const fixMode = !!opts.fix;
3712
+ const dryRunMode = !!opts.dryRun;
3713
+ const undoMode = !!opts.undo;
3710
3714
  const homeDir = os.homedir();
3711
3715
  const delimitHome = path.join(homeDir, '.delimit');
3716
+ const manifestPath = path.join(process.cwd(), '.delimit', 'doctor-manifest.json');
3717
+
3718
+ // --- Undo mode: revert last doctor --fix changes ---
3719
+ if (undoMode) {
3720
+ if (!fs.existsSync(manifestPath)) {
3721
+ console.log(chalk.yellow('\n No doctor-manifest.json found. Nothing to undo.\n'));
3722
+ return;
3723
+ }
3724
+ try {
3725
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
3726
+ const actions = manifest.actions || [];
3727
+ let reverted = 0;
3728
+ let skipped = 0;
3729
+ console.log(chalk.bold('\n Delimit Doctor — Undo\n'));
3730
+ for (const entry of actions) {
3731
+ const targetPath = entry.path;
3732
+ if (entry.action === 'created') {
3733
+ if (fs.existsSync(targetPath)) {
3734
+ const stat = fs.statSync(targetPath);
3735
+ if (stat.isDirectory()) {
3736
+ fs.rmSync(targetPath, { recursive: true, force: true });
3737
+ } else {
3738
+ fs.unlinkSync(targetPath);
3739
+ }
3740
+ console.log(chalk.red(` - Removed: ${targetPath}`));
3741
+ reverted++;
3742
+ } else {
3743
+ console.log(chalk.gray(` - Already gone: ${targetPath}`));
3744
+ skipped++;
3745
+ }
3746
+ } else {
3747
+ console.log(chalk.yellow(` - Skipped (${entry.action}): ${targetPath}`));
3748
+ skipped++;
3749
+ }
3750
+ }
3751
+ fs.unlinkSync(manifestPath);
3752
+ console.log(chalk.green(`\n Reverted ${reverted} item(s), skipped ${skipped}.\n`));
3753
+ } catch (e) {
3754
+ console.log(chalk.red(`\n Failed to read manifest: ${e.message}\n`));
3755
+ process.exitCode = 1;
3756
+ }
3757
+ return;
3758
+ }
3759
+
3760
+ // --- Dry-run mode: preview what --fix would create/modify ---
3761
+ if (dryRunMode) {
3762
+ console.log(chalk.bold('\n Delimit Doctor — Dry Run Preview\n'));
3763
+ const planned = [];
3764
+ const delimitDir = path.join(process.cwd(), '.delimit');
3765
+ const policyFile = path.join(delimitDir, 'policies.yml');
3766
+ const ledgerDir = path.join(delimitDir, 'ledger');
3767
+ const evidenceDir = path.join(delimitDir, 'evidence');
3768
+ const memoryDir = path.join(delimitHome, 'memory');
3769
+ const mcpServerPath = path.join(delimitHome, 'server', 'ai', 'server.py');
3770
+
3771
+ if (!fs.existsSync(policyFile)) {
3772
+ if (!fs.existsSync(delimitDir)) {
3773
+ planned.push({ path: delimitDir, action: 'create_dir', description: '.delimit/ governance directory' });
3774
+ }
3775
+ planned.push({ path: policyFile, action: 'create_file', description: 'Governance policy rules (via delimit init)' });
3776
+ }
3777
+ if (!fs.existsSync(ledgerDir)) {
3778
+ planned.push({ path: ledgerDir, action: 'create_dir', description: 'Operations ledger directory' });
3779
+ }
3780
+ if (!fs.existsSync(evidenceDir)) {
3781
+ planned.push({ path: evidenceDir, action: 'create_dir', description: 'Audit trail events directory' });
3782
+ }
3783
+ if (!fs.existsSync(memoryDir)) {
3784
+ planned.push({ path: memoryDir, action: 'create_dir', description: '~/.delimit/memory/ directory' });
3785
+ }
3786
+ if (!fs.existsSync(mcpServerPath)) {
3787
+ planned.push({ path: mcpServerPath, action: 'create_file', description: 'MCP server (via delimit setup --all)' });
3788
+ }
3789
+ // GitHub workflow
3790
+ const workflowDir = path.join(process.cwd(), '.github', 'workflows');
3791
+ if (fs.existsSync(path.join(process.cwd(), '.github'))) {
3792
+ const wf = path.join(workflowDir, 'api-governance.yml');
3793
+ if (!fs.existsSync(wf)) {
3794
+ planned.push({ path: wf, action: 'create_file', description: 'API governance GitHub Action workflow' });
3795
+ }
3796
+ }
3797
+
3798
+ if (planned.length === 0) {
3799
+ console.log(chalk.green(' No changes needed. Everything looks good.\n'));
3800
+ } else {
3801
+ console.log(chalk.gray(` doctor --fix would create/modify ${planned.length} item(s):\n`));
3802
+ for (const p of planned) {
3803
+ const icon = p.action.startsWith('create') ? '+' : '~';
3804
+ console.log(chalk.gray(` ${icon} ${p.path}`));
3805
+ console.log(chalk.gray(` ${p.description}`));
3806
+ }
3807
+ console.log(chalk.gray(`\n Run ${chalk.bold('delimit doctor --fix')} to apply these changes.\n`));
3808
+ }
3809
+
3810
+ if (ciMode) {
3811
+ console.log(JSON.stringify({ status: 'dry_run', planned_changes: planned, change_count: planned.length }, null, 2));
3812
+ }
3813
+ return;
3814
+ }
3815
+
3712
3816
  const results = []; // { name, status: 'pass'|'warn'|'fail', message, fix? }
3817
+ const manifestActions = []; // track what --fix creates
3713
3818
 
3714
3819
  function addResult(name, status, message, fix) {
3715
3820
  results.push({ name, status, message, fix: fix || null });
3716
3821
  }
3717
3822
 
3823
+ // Helper: record a created file/dir in the manifest
3824
+ function trackCreated(filePath) {
3825
+ manifestActions.push({ path: filePath, action: 'created', timestamp: new Date().toISOString() });
3826
+ }
3827
+
3718
3828
  // --- Check 1: Policy file ---
3719
3829
  const policyPath = path.join(process.cwd(), '.delimit', 'policies.yml');
3720
3830
  if (fs.existsSync(policyPath)) {
@@ -3733,10 +3843,13 @@ program
3733
3843
  addResult('policy-file', 'fail', 'No .delimit/policies.yml', 'delimit init');
3734
3844
  if (fixMode) {
3735
3845
  try {
3846
+ const delimitDirPre = fs.existsSync(path.join(process.cwd(), '.delimit'));
3736
3847
  execSync('delimit init --dry-run', { stdio: 'pipe', cwd: process.cwd() });
3737
3848
  // If dry-run works, run real init
3738
3849
  execSync('delimit init', { stdio: 'pipe', cwd: process.cwd() });
3739
3850
  addResult('policy-file-fix', 'pass', 'Auto-fixed: ran delimit init');
3851
+ if (!delimitDirPre) trackCreated(path.join(process.cwd(), '.delimit'));
3852
+ trackCreated(policyPath);
3740
3853
  } catch {
3741
3854
  addResult('policy-file-fix', 'warn', 'Auto-fix failed: run delimit init manually');
3742
3855
  }
@@ -3834,6 +3947,7 @@ program
3834
3947
  try {
3835
3948
  execSync('delimit setup --all', { stdio: 'pipe' });
3836
3949
  addResult('mcp-server-fix', 'pass', 'Auto-fixed: ran delimit setup --all');
3950
+ trackCreated(mcpServerPath);
3837
3951
  } catch {
3838
3952
  addResult('mcp-server-fix', 'warn', 'Auto-fix failed: run delimit setup --all manually');
3839
3953
  }
@@ -3862,6 +3976,7 @@ program
3862
3976
  try {
3863
3977
  fs.mkdirSync(memoryDir, { recursive: true });
3864
3978
  addResult('memory-health-fix', 'pass', 'Auto-fixed: created ~/.delimit/memory/');
3979
+ trackCreated(memoryDir);
3865
3980
  } catch {
3866
3981
  addResult('memory-health-fix', 'warn', `Auto-fix failed: run mkdir -p ${memoryDir}`);
3867
3982
  }
@@ -4000,10 +4115,30 @@ program
4000
4115
  }
4001
4116
  }
4002
4117
 
4003
- // Undo instruction (LED-265)
4004
- console.log(chalk.bold('\n Undo:'));
4005
- console.log(chalk.gray(' rm -rf .delimit — remove all Delimit files'));
4006
- console.log(chalk.gray(' delimit uninstall --dry-run — preview MCP removal\n'));
4118
+ // Save manifest if --fix made changes (LED-265)
4119
+ if (fixMode && manifestActions.length > 0) {
4120
+ const manifestDir = path.join(process.cwd(), '.delimit');
4121
+ if (!fs.existsSync(manifestDir)) {
4122
+ fs.mkdirSync(manifestDir, { recursive: true });
4123
+ }
4124
+ const manifest = {
4125
+ version: 1,
4126
+ created: new Date().toISOString(),
4127
+ actions: manifestActions,
4128
+ };
4129
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
4130
+ console.log(chalk.bold('\n Manifest:'));
4131
+ console.log(chalk.gray(` Saved ${manifestActions.length} action(s) to .delimit/doctor-manifest.json`));
4132
+ console.log(chalk.gray(' Run: delimit doctor --undo to revert\n'));
4133
+ } else {
4134
+ // Undo instruction (LED-265)
4135
+ console.log(chalk.bold('\n Undo:'));
4136
+ if (fs.existsSync(manifestPath)) {
4137
+ console.log(chalk.gray(' delimit doctor --undo — revert last doctor --fix changes'));
4138
+ }
4139
+ console.log(chalk.gray(' rm -rf .delimit — remove all Delimit files'));
4140
+ console.log(chalk.gray(' delimit uninstall --dry-run — preview MCP removal\n'));
4141
+ }
4007
4142
 
4008
4143
  // Health score and summary
4009
4144
  const ok = results.filter(r => r.status === 'pass').length;
@@ -4955,50 +5090,250 @@ program
4955
5090
  return;
4956
5091
  }
4957
5092
 
4958
- // Decision banner
5093
+ // Detect CI environment — use plain output (no color) when not a TTY
5094
+ const isCI = !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.JENKINS_URL || process.env.GITLAB_CI || process.env.CIRCLECI || process.env.TRAVIS);
5095
+ const isTTY = process.stdout.isTTY;
5096
+ const useColor = isTTY && !isCI && !process.env.NO_COLOR;
5097
+
5098
+ // Severity classification for violations (mirrors Action's ci_formatter.py)
5099
+ const SEVERITY_MAP = {
5100
+ 'no_endpoint_removal': { label: 'Critical', color: 'red' },
5101
+ 'no_method_removal': { label: 'Critical', color: 'red' },
5102
+ 'no_field_removal': { label: 'Critical', color: 'red' },
5103
+ 'no_response_field_removal': { label: 'Critical', color: 'red' },
5104
+ 'no_required_param_addition': { label: 'High', color: 'yellow' },
5105
+ 'no_type_changes': { label: 'High', color: 'yellow' },
5106
+ 'warn_type_change': { label: 'High', color: 'yellow' },
5107
+ 'no_enum_removal': { label: 'High', color: 'yellow' },
5108
+ };
5109
+
5110
+ // Teachings — WHY each rule matters (mirrors Action's ci_formatter.py TEACHINGS)
5111
+ const TEACHINGS = {
5112
+ 'no_endpoint_removal': 'Removing an endpoint breaks existing clients actively calling it. Their requests will return 404.',
5113
+ 'no_method_removal': 'Removing an HTTP method breaks clients using that verb. They will receive 405 Method Not Allowed.',
5114
+ 'no_required_param_addition': 'Adding a required parameter breaks every existing request that omits it. Clients get 400 Bad Request.',
5115
+ 'no_field_removal': 'Removing a request field breaks clients sending it if the server rejects the payload or silently drops data.',
5116
+ 'no_response_field_removal': 'Removing a response field breaks clients reading it. Their code hits undefined/null.',
5117
+ 'no_type_changes': 'Changing a field type breaks serialization. Clients parsing the old type will fail.',
5118
+ 'warn_type_change': 'Changing a field type breaks serialization. Clients parsing the old type will fail.',
5119
+ 'no_enum_removal': 'Removing an enum value breaks clients that send or compare against it.',
5120
+ };
5121
+
5122
+ // Fix hints — HOW to fix each rule (mirrors Action's ci_formatter.py FIX_HINTS)
5123
+ const FIX_HINTS = {
5124
+ 'no_endpoint_removal': 'Deprecate the endpoint first, then remove in a future major version.',
5125
+ 'no_method_removal': 'Keep the old method available or redirect it. Remove only after a deprecation period.',
5126
+ 'no_required_param_addition': 'Make the new parameter optional with a sensible default value.',
5127
+ 'no_field_removal': 'Keep the field in the schema. Mark it deprecated and stop populating in a future version.',
5128
+ 'no_response_field_removal': 'Restore the field. If removing is intentional, version the endpoint (e.g., /v2/).',
5129
+ 'no_type_changes': 'Revert the type change, or introduce a new field with the desired type and deprecate the old one.',
5130
+ 'warn_type_change': 'Revert the type change, or introduce a new field with the desired type and deprecate the old one.',
5131
+ 'no_enum_removal': 'Keep the enum value and mark it deprecated. Remove only in a coordinated major release.',
5132
+ };
5133
+
5134
+ // Helper: colorize or plain text
5135
+ const c = {
5136
+ red: (s) => useColor ? chalk.red(s) : s,
5137
+ green: (s) => useColor ? chalk.green(s) : s,
5138
+ yellow: (s) => useColor ? chalk.yellow(s) : s,
5139
+ gray: (s) => useColor ? chalk.gray(s) : s,
5140
+ bold: (s) => useColor ? chalk.bold(s) : s,
5141
+ redBold: (s) => useColor ? chalk.red.bold(s) : s,
5142
+ greenBold: (s) => useColor ? chalk.green.bold(s) : s,
5143
+ yellowBold: (s) => useColor ? chalk.yellow.bold(s) : s,
5144
+ dim: (s) => useColor ? chalk.dim(s) : s,
5145
+ cyan: (s) => useColor ? chalk.cyan(s) : s,
5146
+ };
5147
+
4959
5148
  const decision = result.decision;
4960
5149
  const semver = result.semver;
4961
- const banner = decision === 'fail'
4962
- ? chalk.red.bold('FAIL')
4963
- : decision === 'warn'
4964
- ? chalk.yellow.bold('WARN')
4965
- : chalk.green.bold('PASS');
5150
+ const s = result.summary;
5151
+ const violations = result.violations || [];
5152
+ const allChanges = result.all_changes || [];
5153
+ const errors = violations.filter(v => v.severity === 'error');
5154
+ const warnings = violations.filter(v => v.severity === 'warning');
5155
+ const safe = allChanges.filter(ch => !ch.is_breaking);
4966
5156
 
4967
- const bump = semver ? ` — ${chalk.bold(semver.bump.toUpperCase())}` : '';
4968
- const nextVer = semver && semver.next_version ? ` (${semver.next_version})` : '';
5157
+ // ── Header Banner ──
5158
+ const divider = useColor ? chalk.dim('─'.repeat(60)) : '-'.repeat(60);
5159
+ console.log('');
5160
+ console.log(divider);
4969
5161
 
4970
- console.log(`\n${banner}${bump}${nextVer}\n`);
5162
+ if (decision === 'fail') {
5163
+ console.log(c.redBold(' GOVERNANCE FAILED'));
5164
+ } else if (decision === 'warn') {
5165
+ console.log(c.yellowBold(' GOVERNANCE PASSED WITH WARNINGS'));
5166
+ } else {
5167
+ console.log(c.greenBold(' GOVERNANCE PASSED'));
5168
+ }
4971
5169
 
4972
- // Summary
4973
- const s = result.summary;
4974
- console.log(` Changes: ${s.total_changes} total, ${s.breaking_changes} breaking`);
5170
+ // Semver line
5171
+ const bumpLabel = semver ? semver.bump.toUpperCase() : 'NONE';
5172
+ const nextVerStr = semver && semver.next_version ? ` Next: ${semver.next_version}` : '';
5173
+ console.log(` Semver: ${c.bold(bumpLabel)}${nextVerStr}`);
5174
+ console.log(divider);
5175
+
5176
+ // ── Summary Stats ──
5177
+ console.log('');
5178
+ console.log(` Total changes: ${s.total_changes}`);
5179
+ console.log(` Breaking changes: ${s.breaking_changes > 0 ? c.red(String(s.breaking_changes)) : c.green('0')}`);
5180
+ console.log(` Policy violations: ${s.violations > 0 ? c.red(String(s.violations)) : c.green('0')}`);
4975
5181
  if (s.violations > 0) {
4976
- console.log(` Violations: ${s.errors} error(s), ${s.warnings} warning(s)`);
5182
+ console.log(` Errors: ${s.errors}`);
5183
+ console.log(` Warnings: ${s.warnings}`);
4977
5184
  }
4978
5185
  console.log('');
4979
5186
 
4980
- // Violations
4981
- const violations = result.violations || [];
4982
- if (violations.length > 0) {
4983
- violations.forEach(v => {
4984
- const icon = v.severity === 'error' ? chalk.red('ERR') : chalk.yellow('WRN');
4985
- console.log(` ${icon} ${v.message}`);
4986
- if (v.path) console.log(` ${chalk.gray(v.path)}`);
5187
+ // ── Breaking Changes Table ──
5188
+ if (errors.length > 0 || warnings.length > 0) {
5189
+ console.log(c.bold(' Breaking Changes'));
5190
+ console.log(divider);
5191
+ console.log('');
5192
+
5193
+ // Table header
5194
+ const colSev = 10;
5195
+ const colLoc = 32;
5196
+ const colMsg = 50;
5197
+ const pad = (str, len) => {
5198
+ const stripped = str.replace(/\x1b\[[0-9;]*m/g, '');
5199
+ const diff = len - stripped.length;
5200
+ return diff > 0 ? str + ' '.repeat(diff) : str;
5201
+ };
5202
+
5203
+ console.log(` ${pad(c.bold('Severity'), colSev)} ${pad(c.bold('Location'), colLoc)} ${c.bold('Description')}`);
5204
+ console.log(` ${'-'.repeat(colSev)} ${'-'.repeat(colLoc)} ${'-'.repeat(colMsg)}`);
5205
+
5206
+ errors.forEach(v => {
5207
+ const sev = SEVERITY_MAP[v.rule] || { label: 'Error', color: 'red' };
5208
+ const sevStr = sev.color === 'red' ? c.red(sev.label) : c.yellow(sev.label);
5209
+ const loc = v.path || '-';
5210
+ const truncLoc = loc.length > colLoc ? loc.substring(0, colLoc - 3) + '...' : loc;
5211
+ console.log(` ${pad(sevStr, colSev)} ${pad(c.cyan(truncLoc), colLoc)} ${v.message}`);
5212
+ });
5213
+
5214
+ warnings.forEach(v => {
5215
+ const sev = SEVERITY_MAP[v.rule] || { label: 'Medium', color: 'yellow' };
5216
+ const sevStr = c.yellow(sev.label);
5217
+ const loc = v.path || '-';
5218
+ const truncLoc = loc.length > colLoc ? loc.substring(0, colLoc - 3) + '...' : loc;
5219
+ console.log(` ${pad(sevStr, colSev)} ${pad(c.cyan(truncLoc), colLoc)} ${v.message}`);
5220
+ });
5221
+
5222
+ console.log('');
5223
+ }
5224
+
5225
+ // ── Why This Breaks (Teachings) ──
5226
+ if (errors.length > 0) {
5227
+ console.log(c.bold(' Why This Breaks'));
5228
+ console.log(divider);
5229
+ console.log('');
5230
+
5231
+ // Deduplicate by rule
5232
+ const seenRules = new Set();
5233
+ errors.forEach(v => {
5234
+ if (v.rule && TEACHINGS[v.rule] && !seenRules.has(v.rule)) {
5235
+ seenRules.add(v.rule);
5236
+ const ruleName = v.rule.replace(/^no_/, '').replace(/_/g, ' ');
5237
+ console.log(` ${c.red('*')} ${c.bold(ruleName)}`);
5238
+ console.log(` ${c.gray(TEACHINGS[v.rule])}`);
5239
+ console.log('');
5240
+ }
5241
+ });
5242
+ }
5243
+
5244
+ // ── How to Fix (Migration Hints) ──
5245
+ if (errors.length > 0) {
5246
+ console.log(c.bold(' How to Fix'));
5247
+ console.log(divider);
5248
+ console.log('');
5249
+
5250
+ errors.forEach((v, i) => {
5251
+ const loc = v.path || '-';
5252
+ const hint = FIX_HINTS[v.rule] || 'Review this change and update consumers accordingly.';
5253
+ console.log(` ${c.bold(`${i + 1}. ${loc}`)}`);
5254
+ console.log(` ${hint}`);
5255
+ console.log('');
5256
+ });
5257
+ }
5258
+
5259
+ // ── Migration Guide (if available from engine) ──
5260
+ if (result.migration && decision === 'fail') {
5261
+ console.log(c.bold(' Migration Guide'));
5262
+ console.log(divider);
5263
+ console.log('');
5264
+ // Indent migration text
5265
+ const migrationLines = result.migration.split('\n');
5266
+ migrationLines.forEach(line => {
5267
+ console.log(` ${line}`);
4987
5268
  });
4988
5269
  console.log('');
4989
5270
  }
4990
5271
 
4991
- // Non-breaking changes
4992
- const safe = (result.all_changes || []).filter(c => !c.is_breaking);
4993
- if (safe.length > 0) {
4994
- console.log(chalk.green(' Additions:'));
4995
- safe.forEach(c => console.log(` + ${c.message}`));
5272
+ // ── Non-Breaking Additions ──
5273
+ if (safe.length > 0 && safe.length <= 20) {
5274
+ console.log(c.bold(` Non-Breaking Additions (${safe.length})`));
5275
+ console.log(divider);
5276
+ console.log('');
5277
+ safe.forEach(ch => {
5278
+ console.log(` ${c.green('+')} ${ch.message}`);
5279
+ if (ch.path) console.log(` ${c.gray(ch.path)}`);
5280
+ });
5281
+ console.log('');
5282
+ } else if (safe.length > 20) {
5283
+ console.log(c.bold(` Non-Breaking Additions (${safe.length})`));
5284
+ console.log(divider);
5285
+ console.log('');
5286
+ safe.slice(0, 10).forEach(ch => {
5287
+ console.log(` ${c.green('+')} ${ch.message}`);
5288
+ });
5289
+ console.log(c.gray(` ... and ${safe.length - 10} more additions`));
4996
5290
  console.log('');
4997
5291
  }
4998
5292
 
5293
+ // ── Governance Gates ──
5294
+ console.log(c.bold(' Governance Gates'));
5295
+ console.log(divider);
5296
+ console.log('');
5297
+
5298
+ const lintPass = s.breaking_changes === 0;
5299
+ const policyPass = violations.length === 0;
5300
+ const deployReady = lintPass && policyPass;
5301
+
5302
+ const gateIcon = (pass) => pass ? c.green('PASS') : c.red('FAIL');
5303
+ const gates = [
5304
+ ['API Lint', lintPass],
5305
+ ['Policy Compliance', policyPass],
5306
+ ['Deploy Readiness', deployReady],
5307
+ ];
5308
+
5309
+ const gateCol = 22;
5310
+ console.log(` ${c.bold('Gate'.padEnd(gateCol))} ${c.bold('Status')}`);
5311
+ console.log(` ${'-'.repeat(gateCol)} ${'-'.repeat(10)}`);
5312
+ gates.forEach(([name, pass]) => {
5313
+ const status = pass ? gateIcon(true) : gateIcon(false);
5314
+ if (name === 'Policy Compliance' && !policyPass) {
5315
+ console.log(` ${name.padEnd(gateCol)} ${status} (${violations.length} violation${violations.length !== 1 ? 's' : ''})`);
5316
+ } else if (name === 'Deploy Readiness' && !deployReady) {
5317
+ console.log(` ${name.padEnd(gateCol)} ${c.yellow('BLOCKED')}`);
5318
+ } else {
5319
+ console.log(` ${name.padEnd(gateCol)} ${status}`);
5320
+ }
5321
+ });
5322
+ console.log('');
5323
+
5324
+ if (!deployReady) {
5325
+ console.log(c.yellow(' Deploy blocked until all gates pass.'));
5326
+ console.log('');
5327
+ }
5328
+
5329
+ // ── Footer ──
5330
+ console.log(divider);
4999
5331
  if (decision === 'pass') {
5000
- console.log('Keep Building.\n');
5332
+ console.log(c.green(' Keep Building.'));
5333
+ } else {
5334
+ console.log(c.gray(' Fix the issues above, then re-run: npx delimit-cli lint'));
5001
5335
  }
5336
+ console.log('');
5002
5337
 
5003
5338
  process.exit(result.exit_code || 0);
5004
5339
  } catch (err) {