delimit-cli 4.1.43 → 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.
Files changed (57) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +46 -5
  3. package/bin/delimit-cli.js +1987 -337
  4. package/bin/delimit-setup.js +108 -66
  5. package/gateway/ai/activate_helpers.py +253 -7
  6. package/gateway/ai/agent_dispatch.py +34 -2
  7. package/gateway/ai/backends/deploy_bridge.py +167 -12
  8. package/gateway/ai/backends/gateway_core.py +236 -13
  9. package/gateway/ai/backends/repo_bridge.py +80 -16
  10. package/gateway/ai/backends/tools_infra.py +49 -32
  11. package/gateway/ai/checksums.sha256 +6 -0
  12. package/gateway/ai/content_engine.py +1276 -2
  13. package/gateway/ai/continuity.py +462 -0
  14. package/gateway/ai/deliberation.pyi +53 -0
  15. package/gateway/ai/github_scanner.py +1 -1
  16. package/gateway/ai/governance.py +58 -0
  17. package/gateway/ai/governance.pyi +32 -0
  18. package/gateway/ai/governance_hardening.py +569 -0
  19. package/gateway/ai/inbox_daemon_runner.py +217 -0
  20. package/gateway/ai/key_resolver.py +95 -2
  21. package/gateway/ai/ledger_manager.py +53 -3
  22. package/gateway/ai/license.py +104 -3
  23. package/gateway/ai/license_core.py +177 -36
  24. package/gateway/ai/license_core.pyi +50 -0
  25. package/gateway/ai/loop_engine.py +929 -294
  26. package/gateway/ai/notify.py +1786 -2
  27. package/gateway/ai/reddit_scanner.py +190 -1
  28. package/gateway/ai/screen_record.py +1 -1
  29. package/gateway/ai/secrets_broker.py +5 -1
  30. package/gateway/ai/server.py +254 -19
  31. package/gateway/ai/social_cache.py +341 -0
  32. package/gateway/ai/social_daemon.py +41 -10
  33. package/gateway/ai/supabase_sync.py +190 -2
  34. package/gateway/ai/swarm.py +86 -0
  35. package/gateway/ai/swarm_infra.py +656 -0
  36. package/gateway/ai/tui.py +594 -36
  37. package/gateway/ai/tweet_corpus_schema.sql +76 -0
  38. package/gateway/core/diff_engine_v2.py +6 -2
  39. package/gateway/core/generator_drift.py +242 -0
  40. package/gateway/core/json_schema_diff.py +375 -0
  41. package/gateway/core/openapi_version.py +124 -0
  42. package/gateway/core/spec_detector.py +47 -7
  43. package/gateway/core/spec_health.py +5 -2
  44. package/gateway/core/zero_spec/express_extractor.py +2 -2
  45. package/gateway/core/zero_spec/nestjs_extractor.py +40 -9
  46. package/gateway/requirements.txt +3 -6
  47. package/lib/cross-model-hooks.js +4 -12
  48. package/package.json +11 -3
  49. package/scripts/demo-v420-clean.sh +267 -0
  50. package/scripts/demo-v420-deliberation.sh +217 -0
  51. package/scripts/demo-v420.sh +55 -0
  52. package/scripts/postinstall.js +4 -3
  53. package/scripts/publish-ci-guard.sh +30 -0
  54. package/scripts/record-and-upload.sh +132 -0
  55. package/scripts/release.sh +126 -0
  56. package/scripts/sync-gateway.sh +112 -0
  57. package/scripts/youtube-upload.py +141 -0
@@ -68,7 +68,7 @@ function normalizeNaturalLanguageArgs(argv) {
68
68
  const explicitCommands = new Set([
69
69
  'install', 'mode', 'status', 'session', 'build', 'ask', 'policy', 'auth', 'audit',
70
70
  'explain-decision', 'uninstall', 'proxy', 'hook', 'version', 'vault', 'deliberate',
71
- 'remember', 'recall', 'forget'
71
+ 'remember', 'recall', 'forget', 'report'
72
72
  ]);
73
73
  if (explicitCommands.has((raw[0] || '').toLowerCase())) {
74
74
  return raw;
@@ -328,190 +328,312 @@ program
328
328
  .description('Show a compact dashboard of your Delimit setup')
329
329
 
330
330
  .option('--verbose', 'Show detailed status')
331
+ .option('--json', 'Output as JSON')
332
+ .option('--watch', 'Refresh every 5 seconds')
331
333
  .action(async (options) => {
332
334
  const homedir = os.homedir();
333
335
  const delimitHome = path.join(homedir, '.delimit');
334
336
  const target = process.cwd();
335
-
336
- console.log(chalk.bold('\n Delimit Status\n'));
337
-
338
- // --- Memory stats ---
339
- const memoryDir = path.join(delimitHome, 'memory');
340
- let memTotal = 0;
341
- let memRecent = 0;
342
- let recentMemories = [];
343
- const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
344
- try {
345
- const memFiles = fs.readdirSync(memoryDir).filter(f => f.startsWith('mem-') && f.endsWith('.json'));
346
- memTotal = memFiles.length;
347
- for (const f of memFiles) {
337
+ const { execSync } = require('child_process');
338
+
339
+ function renderStatus() {
340
+ const data = {};
341
+
342
+ // --- Memory stats ---
343
+ const memoryDir = path.join(delimitHome, 'memory');
344
+ let memTotal = 0;
345
+ let memRecent = 0;
346
+ let recentMemories = [];
347
+ let memIntegrity = { verified: 0, failed: 0 };
348
+ const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
349
+ try {
350
+ const memFiles = fs.readdirSync(memoryDir).filter(f => f.startsWith('mem-') && f.endsWith('.json'));
351
+ memTotal = memFiles.length;
352
+ for (const f of memFiles) {
353
+ try {
354
+ const raw = fs.readFileSync(path.join(memoryDir, f), 'utf-8');
355
+ const d = JSON.parse(raw);
356
+ const ts = new Date(d.created_at || d.timestamp || d.created || 0).getTime();
357
+ if (ts > oneWeekAgo) memRecent++;
358
+ recentMemories.push({ text: d.text || d.content || '', tags: d.tags || [], ts, source: d.context || d.source || 'unknown' });
359
+ // Verify integrity hash if present
360
+ if (d.hash) {
361
+ const expected = require('crypto').createHash('sha256').update(d.content || d.text || '').digest('hex').slice(0, 16);
362
+ if (d.hash === expected) memIntegrity.verified++;
363
+ else memIntegrity.failed++;
364
+ }
365
+ } catch {}
366
+ }
367
+ recentMemories.sort((a, b) => b.ts - a.ts);
368
+ recentMemories = recentMemories.slice(0, 3);
369
+ } catch {}
370
+ data.memory = { total: memTotal, recent: memRecent, integrity: memIntegrity };
371
+
372
+ // --- Governance / Policy ---
373
+ const policyPath = path.join(target, '.delimit', 'policies.yml');
374
+ let policyName = 'none';
375
+ let policyMode = '';
376
+ let ruleCount = 0;
377
+ let hasPolicy = false;
378
+ if (fs.existsSync(policyPath)) {
379
+ hasPolicy = true;
348
380
  try {
349
- const data = JSON.parse(fs.readFileSync(path.join(memoryDir, f), 'utf-8'));
350
- const ts = new Date(data.created_at || data.timestamp || data.created || 0).getTime();
351
- if (ts > oneWeekAgo) memRecent++;
352
- recentMemories.push({ text: data.text || data.content || '', tags: data.tags || [], ts });
353
- } catch {}
381
+ const policyContent = yaml.load(fs.readFileSync(policyPath, 'utf-8'));
382
+ policyName = policyContent?.preset || policyContent?.name || 'custom';
383
+ policyMode = policyContent?.enforcement_mode || policyContent?.mode || '';
384
+ if (policyContent?.rules) ruleCount = Object.keys(policyContent.rules).length;
385
+ } catch {
386
+ policyName = 'custom';
387
+ }
354
388
  }
355
- recentMemories.sort((a, b) => b.ts - a.ts);
356
- recentMemories = recentMemories.slice(0, 3);
357
- } catch {}
358
- console.log(` Memory: ${chalk.white.bold(memTotal)} memories${memRecent > 0 ? ` (${memRecent} this week)` : ''}`);
359
389
 
360
- // --- Governance / Policy ---
361
- const policyPath = path.join(target, '.delimit', 'policies.yml');
362
- let policyLabel = chalk.gray('none');
363
- let hasPolicy = false;
364
- if (fs.existsSync(policyPath)) {
365
- hasPolicy = true;
390
+ // Count tracked specs
391
+ const specPatterns = ['openapi.yaml', 'openapi.yml', 'openapi.json', 'swagger.yaml', 'swagger.yml', 'swagger.json'];
392
+ let specCount = 0;
393
+ const _countSpecs = (dir, depth) => {
394
+ if (depth > 3) return;
395
+ try {
396
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
397
+ if (['node_modules', '.next', 'venv', '.git'].includes(entry.name)) continue;
398
+ const full = path.join(dir, entry.name);
399
+ if (entry.isFile() && specPatterns.includes(entry.name.toLowerCase())) specCount++;
400
+ else if (entry.isDirectory()) _countSpecs(full, depth + 1);
401
+ }
402
+ } catch {}
403
+ };
404
+ _countSpecs(target, 0);
405
+ data.governance = { policy: policyName, mode: policyMode, rules: ruleCount, specs: specCount };
406
+
407
+ // --- Git hooks ---
408
+ const preCommitPath = path.join(target, '.git', 'hooks', 'pre-commit');
409
+ let hasGitHooks = false;
410
+ try { hasGitHooks = fs.readFileSync(preCommitPath, 'utf-8').includes('delimit'); } catch {}
411
+ data.hooks = hasGitHooks;
412
+
413
+ // --- CI ---
414
+ const workflowDir = path.join(target, '.github', 'workflows');
415
+ let hasCI = false;
416
+ let ciFile = '';
366
417
  try {
367
- const policyContent = yaml.load(fs.readFileSync(policyPath, 'utf-8'));
368
- const preset = policyContent?.preset || policyContent?.name || 'custom';
369
- policyLabel = chalk.green(preset + ' policy');
370
- } catch {
371
- policyLabel = chalk.green('custom policy');
418
+ const wfs = fs.readdirSync(workflowDir);
419
+ for (const wf of wfs) {
420
+ try {
421
+ if (fs.readFileSync(path.join(workflowDir, wf), 'utf-8').includes('delimit')) {
422
+ hasCI = true;
423
+ ciFile = wf;
424
+ break;
425
+ }
426
+ } catch {}
427
+ }
428
+ } catch {}
429
+ data.ci = { active: hasCI, file: ciFile };
430
+
431
+ // --- MCP ---
432
+ const mcpConfigPath = path.join(homedir, '.mcp.json');
433
+ let hasMcp = false;
434
+ let toolCount = 0;
435
+ try { hasMcp = fs.readFileSync(mcpConfigPath, 'utf-8').includes('delimit'); } catch {}
436
+ if (hasMcp) {
437
+ const serverPyPaths = [
438
+ path.join(delimitHome, 'server', 'ai', 'server.py'),
439
+ path.join(delimitHome, 'server', 'server.py'),
440
+ ];
441
+ for (const sp of serverPyPaths) {
442
+ try {
443
+ const toolMatches = fs.readFileSync(sp, 'utf-8').match(/@mcp\.tool/g);
444
+ if (toolMatches) { toolCount = toolMatches.length; break; }
445
+ } catch {}
446
+ }
372
447
  }
373
- }
374
- // Count tracked specs
375
- const specPatterns = ['openapi.yaml', 'openapi.yml', 'openapi.json', 'swagger.yaml', 'swagger.yml', 'swagger.json'];
376
- let specCount = 0;
377
- const _countSpecs = (dir, depth) => {
378
- if (depth > 3) return;
448
+ data.mcp = { connected: hasMcp, tools: toolCount };
449
+
450
+ // --- Models ---
451
+ const modelsPath = path.join(delimitHome, 'models.json');
452
+ let modelNames = [];
379
453
  try {
380
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
381
- if (['node_modules', '.next', 'venv', '.git'].includes(entry.name)) continue;
382
- const full = path.join(dir, entry.name);
383
- if (entry.isFile() && specPatterns.includes(entry.name.toLowerCase())) {
384
- specCount++;
385
- } else if (entry.isDirectory()) {
386
- _countSpecs(full, depth + 1);
454
+ const modelsData = JSON.parse(fs.readFileSync(modelsPath, 'utf-8'));
455
+ for (const [key, val] of Object.entries(modelsData)) {
456
+ if (val && typeof val === 'object' && (val.api_key || val.enabled !== false)) {
457
+ modelNames.push(key.charAt(0).toUpperCase() + key.slice(1));
387
458
  }
388
459
  }
389
460
  } catch {}
390
- };
391
- _countSpecs(target, 0);
392
- const specLabel = specCount > 0 ? `${specCount} spec${specCount > 1 ? 's' : ''} tracked` : chalk.gray('no specs');
393
- console.log(` Governance: ${policyLabel}${hasPolicy ? ' | ' : ' | '}${specLabel}`);
461
+ data.models = modelNames;
394
462
 
395
- // --- Git hooks ---
396
- const preCommitPath = path.join(target, '.git', 'hooks', 'pre-commit');
397
- let hasGitHooks = false;
398
- try {
399
- const hookContent = fs.readFileSync(preCommitPath, 'utf-8');
400
- hasGitHooks = hookContent.includes('delimit');
401
- } catch {}
402
- console.log(` Git Hooks: ${hasGitHooks ? chalk.green('pre-commit installed') : chalk.gray('not installed')}`);
463
+ // --- License ---
464
+ const licensePath = path.join(delimitHome, 'license.json');
465
+ let licenseTier = 'Free';
466
+ let licenseActive = false;
467
+ try {
468
+ const ld = JSON.parse(fs.readFileSync(licensePath, 'utf-8'));
469
+ licenseTier = ld.tier || ld.plan || 'Free';
470
+ licenseActive = ld.status === 'active' || ld.valid === true;
471
+ } catch {}
472
+ data.license = { tier: licenseTier, active: licenseActive };
403
473
 
404
- // --- CI ---
405
- const workflowPath = path.join(target, '.github', 'workflows', 'api-governance.yml');
406
- const hasCI = fs.existsSync(workflowPath);
407
- console.log(` CI: ${hasCI ? chalk.green('GitHub Action active') : chalk.gray('not configured')}`);
474
+ // --- Ledger stats ---
475
+ const ledgerDir = path.join(delimitHome, 'ledger');
476
+ let ledgerOpen = 0;
477
+ let ledgerTotal = 0;
478
+ try {
479
+ const ledgerFiles = fs.readdirSync(ledgerDir).filter(f => f.endsWith('.json') || f.endsWith('.jsonl'));
480
+ for (const lf of ledgerFiles) {
481
+ try {
482
+ const content = fs.readFileSync(path.join(ledgerDir, lf), 'utf-8');
483
+ if (lf.endsWith('.jsonl')) {
484
+ const lines = content.split('\n').filter(l => l.trim());
485
+ for (const line of lines) {
486
+ try {
487
+ const item = JSON.parse(line);
488
+ ledgerTotal++;
489
+ if (!item.status || item.status === 'open' || item.status === 'in_progress') ledgerOpen++;
490
+ } catch {}
491
+ }
492
+ } else {
493
+ const item = JSON.parse(content);
494
+ if (Array.isArray(item)) {
495
+ ledgerTotal += item.length;
496
+ ledgerOpen += item.filter(i => !i.status || i.status === 'open' || i.status === 'in_progress').length;
497
+ } else {
498
+ ledgerTotal++;
499
+ if (!item.status || item.status === 'open' || item.status === 'in_progress') ledgerOpen++;
500
+ }
501
+ }
502
+ } catch {}
503
+ }
504
+ } catch {}
505
+ data.ledger = { total: ledgerTotal, open: ledgerOpen };
408
506
 
409
- // --- MCP ---
410
- const mcpConfigPath = path.join(homedir, '.mcp.json');
411
- let hasMcp = false;
412
- let toolCount = 0;
413
- try {
414
- const mcpContent = fs.readFileSync(mcpConfigPath, 'utf-8');
415
- hasMcp = mcpContent.includes('delimit');
416
- } catch {}
417
- if (hasMcp) {
418
- // Count tools from server.py if available
419
- const serverPyPaths = [
420
- path.join(delimitHome, 'server', 'ai', 'server.py'),
421
- path.join(delimitHome, 'server', 'server.py'),
422
- ];
423
- for (const sp of serverPyPaths) {
424
- try {
425
- const serverContent = fs.readFileSync(sp, 'utf-8');
426
- const toolMatches = serverContent.match(/@mcp\.tool/g);
427
- if (toolMatches) {
428
- toolCount = toolMatches.length;
429
- break;
430
- }
431
- } catch {}
432
- }
433
- console.log(` MCP: ${chalk.green('connected')}${toolCount > 0 ? ` (${toolCount} tools)` : ''}`);
434
- } else {
435
- console.log(` MCP: ${chalk.gray('not configured')}`);
436
- }
507
+ // --- Evidence stats ---
508
+ const evidenceDir = path.join(delimitHome, 'evidence');
509
+ let evidenceCount = 0;
510
+ try {
511
+ const evFiles = fs.readdirSync(evidenceDir).filter(f => f.endsWith('.json') || f.endsWith('.jsonl'));
512
+ evidenceCount = evFiles.length;
513
+ } catch {}
514
+ data.evidence = evidenceCount;
437
515
 
438
- // --- Models ---
439
- const modelsPath = path.join(delimitHome, 'models.json');
440
- let modelsLabel = chalk.gray('none configured');
441
- try {
442
- const modelsData = JSON.parse(fs.readFileSync(modelsPath, 'utf-8'));
443
- const modelNames = [];
444
- for (const [key, val] of Object.entries(modelsData)) {
445
- if (val && typeof val === 'object' && (val.api_key || val.enabled !== false)) {
446
- modelNames.push(key.charAt(0).toUpperCase() + key.slice(1));
516
+ // --- Last session ---
517
+ let lastSession = null;
518
+ const sessionsDir = path.join(delimitHome, 'sessions');
519
+ try {
520
+ const sessFiles = fs.readdirSync(sessionsDir)
521
+ .filter(f => f.startsWith('session_') && f.endsWith('.json'))
522
+ .sort().reverse();
523
+ if (sessFiles.length > 0) {
524
+ const latest = JSON.parse(fs.readFileSync(path.join(sessionsDir, sessFiles[0]), 'utf-8'));
525
+ lastSession = latest.summary || latest.description || latest.title || null;
447
526
  }
448
- }
449
- if (modelNames.length > 0) {
450
- modelsLabel = chalk.white(modelNames.join(' + ')) + chalk.gray(' (BYOK)');
451
- }
452
- } catch {}
453
- console.log(` Models: ${modelsLabel}`);
527
+ } catch {}
528
+ data.lastSession = lastSession;
454
529
 
455
- // --- License ---
456
- const licensePath = path.join(delimitHome, 'license.json');
457
- let licenseLabel = chalk.gray('Free');
458
- try {
459
- const licenseData = JSON.parse(fs.readFileSync(licensePath, 'utf-8'));
460
- const tier = licenseData.tier || licenseData.plan || 'Free';
461
- const active = licenseData.status === 'active' || licenseData.valid === true;
462
- if (tier.toLowerCase() !== 'free') {
463
- licenseLabel = active ? chalk.green(`${tier} (active)`) : chalk.yellow(`${tier} (${licenseData.status || 'unknown'})`);
530
+ // --- Git info ---
531
+ let gitBranch = '';
532
+ let gitDirty = false;
533
+ try {
534
+ gitBranch = execSync('git rev-parse --abbrev-ref HEAD', { stdio: 'pipe', cwd: target }).toString().trim();
535
+ const statusOut = execSync('git status --porcelain', { stdio: 'pipe', cwd: target }).toString().trim();
536
+ gitDirty = statusOut.length > 0;
537
+ } catch {}
538
+ data.git = { branch: gitBranch, dirty: gitDirty };
539
+
540
+ // --- Version ---
541
+ let cliVersion = '';
542
+ try { cliVersion = require(path.join(__dirname, '..', 'package.json')).version; } catch {}
543
+ data.version = cliVersion;
544
+
545
+ // --- Readiness ---
546
+ const checks = [
547
+ { name: 'Spec', done: specCount > 0 },
548
+ { name: 'Policy', done: hasPolicy },
549
+ { name: 'CI', done: hasCI },
550
+ { name: 'Hooks', done: hasGitHooks },
551
+ { name: 'MCP', done: hasMcp },
552
+ ];
553
+ const score = checks.filter(c => c.done).length;
554
+ data.readiness = { score, total: checks.length, checks };
555
+
556
+ // === JSON output ===
557
+ if (options.json) {
558
+ console.log(JSON.stringify(data, null, 2));
559
+ return;
464
560
  }
465
- } catch {}
466
- console.log(` License: ${licenseLabel}`);
467
561
 
468
- // --- Recent memories ---
469
- if (recentMemories.length > 0) {
470
- console.log(chalk.bold('\n Recent memories:'));
471
- for (const mem of recentMemories) {
472
- const ago = _relativeTime(mem.ts);
473
- const tagStr = mem.tags.length > 0 ? ' ' + chalk.gray(mem.tags.map(t => '#' + t).join(' ')) : '';
474
- const text = mem.text.length > 55 ? mem.text.slice(0, 55) + '...' : mem.text;
475
- console.log(` ${chalk.gray('[' + ago + ']')} ${text}${tagStr}`);
562
+ // === Visual dashboard ===
563
+ const W = 60;
564
+ const line = '\u2500'.repeat(W - 2);
565
+ const dot = (ok) => ok ? chalk.green('\u25cf') : chalk.gray('\u25cb');
566
+ const bar = (n, total, width = 20) => {
567
+ const filled = Math.round((n / Math.max(total, 1)) * width);
568
+ return chalk.green('\u2588'.repeat(filled)) + chalk.gray('\u2591'.repeat(width - filled));
569
+ };
570
+
571
+ console.log('');
572
+ console.log(chalk.bold.cyan(` \u250c${line}\u2510`));
573
+ console.log(chalk.bold.cyan(` \u2502`) + chalk.bold.white(` Delimit v${cliVersion}`) + ' '.repeat(W - 14 - cliVersion.length) + (licenseTier !== 'Free' ? chalk.green(licenseTier) : chalk.gray('Free')) + chalk.bold.cyan(` \u2502`));
574
+ console.log(chalk.bold.cyan(` \u2514${line}\u2518`));
575
+
576
+ // Governance section
577
+ console.log(chalk.bold('\n Governance'));
578
+ const policyStr = hasPolicy ? chalk.green(policyName) + (policyMode ? chalk.gray(` (${policyMode})`) : '') + (ruleCount ? chalk.gray(` ${ruleCount} rules`) : '') : chalk.gray('not configured');
579
+ console.log(` Policy: ${policyStr}`);
580
+ console.log(` Specs: ${specCount > 0 ? chalk.white(specCount + ' tracked') : chalk.gray('none')}`);
581
+ console.log(` Hooks: ${hasGitHooks ? chalk.green('pre-commit') : chalk.gray('none')}`);
582
+ console.log(` CI: ${hasCI ? chalk.green(ciFile) : chalk.gray('none')}`);
583
+
584
+ // Infrastructure section
585
+ console.log(chalk.bold('\n Infrastructure'));
586
+ console.log(` MCP: ${hasMcp ? chalk.green(toolCount + ' tools') : chalk.gray('not configured')}`);
587
+ console.log(` Models: ${modelNames.length > 0 ? chalk.white(modelNames.join(chalk.gray(' + '))) : chalk.gray('none')}`);
588
+ console.log(` License: ${licenseTier !== 'Free' && licenseActive ? chalk.green(licenseTier) : licenseTier !== 'Free' ? chalk.yellow(licenseTier) : chalk.gray('Free')}`);
589
+
590
+ // Context section
591
+ console.log(chalk.bold('\n Context'));
592
+ console.log(` Memory: ${chalk.white.bold(memTotal)} total${memRecent > 0 ? chalk.gray(` (${memRecent} this week)`) : ''}`);
593
+ console.log(` Ledger: ${ledgerOpen > 0 ? chalk.yellow(ledgerOpen + ' open') : chalk.gray('0 open')} / ${ledgerTotal} total`);
594
+ console.log(` Evidence: ${evidenceCount > 0 ? chalk.white(evidenceCount + ' records') : chalk.gray('none')}`);
595
+ if (gitBranch) {
596
+ console.log(` Branch: ${chalk.white(gitBranch)}${gitDirty ? chalk.yellow(' (dirty)') : chalk.green(' (clean)')}`);
476
597
  }
477
- }
478
598
 
479
- // --- Last session ---
480
- const sessionsDir = path.join(delimitHome, 'sessions');
481
- try {
482
- const sessFiles = fs.readdirSync(sessionsDir)
483
- .filter(f => f.startsWith('session_') && f.endsWith('.json'))
484
- .sort()
485
- .reverse();
486
- if (sessFiles.length > 0) {
487
- const latest = JSON.parse(fs.readFileSync(path.join(sessionsDir, sessFiles[0]), 'utf-8'));
488
- const summary = latest.summary || latest.description || latest.title || null;
489
- if (summary) {
490
- const truncated = summary.length > 60 ? summary.slice(0, 60) + '...' : summary;
491
- console.log(chalk.bold('\n Last session: ') + truncated);
599
+ // Recent memories
600
+ if (recentMemories.length > 0) {
601
+ console.log(chalk.bold('\n Recent Memories'));
602
+ for (const mem of recentMemories) {
603
+ const ago = _relativeTime(mem.ts);
604
+ const src = mem.source !== 'unknown' ? chalk.gray(` [${mem.source}]`) : '';
605
+ const text = mem.text.length > 50 ? mem.text.slice(0, 50) + '...' : mem.text;
606
+ console.log(` ${chalk.gray(ago.padEnd(12))} ${text}${src}`);
492
607
  }
493
608
  }
494
- } catch {}
495
-
496
- // --- Governance readiness ---
497
- const hasSpecs = specCount > 0;
498
- const checks = [
499
- { name: 'API spec', done: hasSpecs },
500
- { name: 'Policy', done: hasPolicy },
501
- { name: 'CI gate', done: hasCI },
502
- { name: 'Git hooks', done: hasGitHooks },
503
- { name: 'MCP', done: hasMcp },
504
- ];
505
- const score = checks.filter(c => c.done).length;
506
609
 
507
- console.log(chalk.bold(`\n Governance readiness: ${score}/${checks.length}`));
508
- console.log(' ' + checks.map(c => c.done ? chalk.green('\u25cf') + ' ' + c.name : chalk.gray('\u25cb') + ' ' + chalk.gray(c.name)).join(' '));
509
- console.log('');
610
+ // Last session
611
+ if (lastSession) {
612
+ const truncated = lastSession.length > 55 ? lastSession.slice(0, 55) + '...' : lastSession;
613
+ console.log(chalk.bold('\n Last Session'));
614
+ console.log(` ${truncated}`);
615
+ }
510
616
 
511
- if (options.verbose) {
512
- console.log(chalk.bold(' Continuity Context:'));
513
- console.log(formatContinuityReport(continuityContext).split('\n').slice(1).map(line => ' ' + line.trimStart()).join('\n'));
617
+ // Readiness bar
618
+ console.log(chalk.bold('\n Readiness'));
619
+ console.log(` ${bar(score, checks.length)} ${score}/${checks.length}`);
620
+ console.log(' ' + checks.map(c => dot(c.done) + ' ' + (c.done ? c.name : chalk.gray(c.name))).join(' '));
514
621
  console.log('');
622
+
623
+ if (options.verbose) {
624
+ console.log(chalk.bold(' Continuity Context:'));
625
+ console.log(formatContinuityReport(continuityContext).split('\n').slice(1).map(line => ' ' + line.trimStart()).join('\n'));
626
+ console.log('');
627
+ }
628
+ }
629
+
630
+ if (options.watch) {
631
+ const clear = () => process.stdout.write('\x1B[2J\x1B[0f');
632
+ const tick = () => { clear(); renderStatus(); };
633
+ tick();
634
+ setInterval(tick, 5000);
635
+ } else {
636
+ renderStatus();
515
637
  }
516
638
  });
517
639
 
@@ -779,8 +901,73 @@ overrides:
779
901
  }
780
902
 
781
903
  if (options.validate) {
782
- // TODO: Implement validation
783
- console.log(chalk.yellow('Policy validation coming soon'));
904
+ const policyPaths = [
905
+ path.join(process.cwd(), '.delimit', 'policies.yml'),
906
+ path.join(process.cwd(), 'delimit.yml'),
907
+ path.join(process.cwd(), '.delimit.yml'),
908
+ ];
909
+ const policyFile = policyPaths.find(p => fs.existsSync(p));
910
+ if (!policyFile) {
911
+ console.log(chalk.red('No policy file found.'));
912
+ console.log(chalk.dim(' Expected: .delimit/policies.yml, delimit.yml, or .delimit.yml'));
913
+ console.log(' Run ' + chalk.cyan('delimit policy --init') + ' to create one.');
914
+ process.exit(1);
915
+ }
916
+ console.log(chalk.bold('Validating: ') + chalk.dim(policyFile));
917
+ try {
918
+ const raw = fs.readFileSync(policyFile, 'utf-8');
919
+ let parsed;
920
+ try {
921
+ parsed = require('js-yaml').load(raw);
922
+ } catch {
923
+ // Fallback: try JSON
924
+ parsed = JSON.parse(raw);
925
+ }
926
+ const errors = [];
927
+ const warnings = [];
928
+ if (!parsed || typeof parsed !== 'object') {
929
+ errors.push('Policy file is empty or not a valid object');
930
+ } else {
931
+ // Check required structure
932
+ if (!parsed.rules && !parsed.mode && !parsed.governance) {
933
+ warnings.push('No "rules", "mode", or "governance" key found');
934
+ }
935
+ if (parsed.rules) {
936
+ if (!Array.isArray(parsed.rules)) {
937
+ errors.push('"rules" must be an array');
938
+ } else {
939
+ parsed.rules.forEach((rule, i) => {
940
+ if (!rule.id) warnings.push(`Rule ${i + 1}: missing "id"`);
941
+ if (!rule.change_types && !rule.name) warnings.push(`Rule ${i + 1}: missing "change_types" or "name"`);
942
+ if (rule.severity && !['error', 'warning', 'info'].includes(rule.severity)) {
943
+ errors.push(`Rule ${i + 1}: invalid severity "${rule.severity}" (use: error, warning, info)`);
944
+ }
945
+ if (rule.action && !['forbid', 'warn', 'allow', 'require_approval'].includes(rule.action)) {
946
+ errors.push(`Rule ${i + 1}: invalid action "${rule.action}" (use: forbid, warn, allow, require_approval)`);
947
+ }
948
+ });
949
+ console.log(chalk.dim(` Rules: ${parsed.rules.length} defined`));
950
+ }
951
+ }
952
+ if (parsed.mode && !['strict', 'default', 'relaxed'].includes(parsed.mode)) {
953
+ warnings.push(`Unknown mode "${parsed.mode}" (expected: strict, default, relaxed)`);
954
+ }
955
+ }
956
+ if (errors.length > 0) {
957
+ errors.forEach(e => console.log(chalk.red(` ✗ ${e}`)));
958
+ }
959
+ if (warnings.length > 0) {
960
+ warnings.forEach(w => console.log(chalk.yellow(` ⚠ ${w}`)));
961
+ }
962
+ if (errors.length === 0) {
963
+ console.log(chalk.green(' ✓ Policy file is valid'));
964
+ } else {
965
+ process.exit(1);
966
+ }
967
+ } catch (e) {
968
+ console.log(chalk.red(` ✗ Failed to parse policy: ${e.message}`));
969
+ process.exit(1);
970
+ }
784
971
  }
785
972
  });
786
973
 
@@ -3090,167 +3277,1358 @@ program
3090
3277
  try { fs.rmSync(tmpDir, { recursive: true }); } catch {}
3091
3278
  });
3092
3279
 
3093
- // Doctor command — verify setup is correct
3280
+ // Report command — generate local governance reports (v4.20)
3094
3281
  program
3095
- .command('doctor')
3096
- .description('Verify Delimit setup and diagnose common issues')
3097
- .action(async () => {
3098
- console.log(chalk.bold('\n Delimit Doctor\n'));
3099
- let ok = 0;
3100
- let warn = 0;
3101
- let fail = 0;
3282
+ .command('report')
3283
+ .description('Generate a governance report from local evidence, ledger, and memory')
3284
+ .option('--since <duration>', 'Time period (e.g., 7d, 30d, 24h, 1w, 1m)', '7d')
3285
+ .option('--format <fmt>', 'Output format: md, json, html', 'md')
3286
+ .option('--output <file>', 'Write report to file instead of stdout')
3287
+ .action(async (options) => {
3288
+ const delimitHome = path.join(os.homedir(), '.delimit');
3289
+ const evidenceDir = path.join(delimitHome, 'evidence');
3290
+ const ledgerDir = path.join(delimitHome, 'ledger');
3291
+ const memoryDir = path.join(delimitHome, 'memory');
3102
3292
 
3103
- // Check policy file
3104
- const policyPath = path.join(process.cwd(), '.delimit', 'policies.yml');
3105
- if (fs.existsSync(policyPath)) {
3106
- console.log(chalk.green(' ✓ .delimit/policies.yml found'));
3107
- ok++;
3108
- try {
3109
- const yaml = require('js-yaml');
3110
- const policy = yaml.load(fs.readFileSync(policyPath, 'utf8'));
3111
- if (policy && (policy.rules !== undefined || policy.override_defaults !== undefined)) {
3112
- console.log(chalk.green(' ✓ Policy file is valid YAML'));
3113
- ok++;
3114
- } else {
3115
- console.log(chalk.yellow(' ⚠ Policy file has no rules section'));
3116
- warn++;
3117
- }
3118
- } catch (e) {
3119
- console.log(chalk.red(` ✗ Policy file has invalid YAML: ${e.message}`));
3120
- fail++;
3121
- }
3122
- } else {
3123
- console.log(chalk.red(' ✗ No .delimit/policies.yml — run: delimit init'));
3124
- fail++;
3293
+ // Parse duration into milliseconds
3294
+ function parseDuration(dur) {
3295
+ const match = dur.match(/^(\d+)\s*(h|d|w|m)$/i);
3296
+ if (!match) return 7 * 24 * 60 * 60 * 1000; // default 7d
3297
+ const val = parseInt(match[1], 10);
3298
+ const unit = match[2].toLowerCase();
3299
+ const multipliers = { h: 3600000, d: 86400000, w: 604800000, m: 2592000000 };
3300
+ return val * (multipliers[unit] || 86400000);
3125
3301
  }
3126
3302
 
3127
- // Check for OpenAPI spec
3128
- const specPatterns = [
3129
- 'openapi.yaml', 'openapi.yml', 'openapi.json',
3130
- 'swagger.yaml', 'swagger.yml', 'swagger.json',
3131
- 'docs/openapi.yaml', 'docs/openapi.yml', 'docs/openapi.json',
3132
- 'spec/openapi.yaml', 'spec/openapi.json',
3133
- 'api/openapi.yaml', 'api/openapi.json',
3134
- 'contrib/openapi.json',
3135
- ];
3136
- const foundSpecs = specPatterns.filter(p => fs.existsSync(path.join(process.cwd(), p)));
3137
- if (foundSpecs.length > 0) {
3138
- console.log(chalk.green(` ✓ OpenAPI spec found: ${foundSpecs[0]}`));
3139
- ok++;
3140
- } else {
3141
- // Check for framework (Zero-Spec candidate)
3142
- const pkgJson = path.join(process.cwd(), 'package.json');
3143
- const reqTxt = path.join(process.cwd(), 'requirements.txt');
3144
- if (fs.existsSync(pkgJson) || fs.existsSync(reqTxt)) {
3145
- console.log(chalk.yellow(' ⚠ No OpenAPI spec file — Zero-Spec Mode may work if this is a FastAPI/NestJS/Express project'));
3146
- warn++;
3147
- } else {
3148
- console.log(chalk.red(' ✗ No OpenAPI spec file found'));
3149
- fail++;
3150
- }
3303
+ const sinceMs = parseDuration(options.since);
3304
+ const cutoff = new Date(Date.now() - sinceMs);
3305
+ const now = new Date();
3306
+ const fmt = (options.format || 'md').toLowerCase();
3307
+
3308
+ if (!['md', 'json', 'html'].includes(fmt)) {
3309
+ console.error(chalk.red(` Invalid format: ${fmt}. Use md, json, or html.`));
3310
+ process.exit(1);
3151
3311
  }
3152
3312
 
3153
- // Check for GitHub workflow
3154
- const workflowDir = path.join(process.cwd(), '.github', 'workflows');
3155
- if (fs.existsSync(workflowDir)) {
3156
- const workflows = fs.readdirSync(workflowDir);
3157
- const hasDelimit = workflows.some(f => {
3313
+ // Collect evidence events
3314
+ const evidenceEvents = [];
3315
+ if (fs.existsSync(evidenceDir)) {
3316
+ const files = fs.readdirSync(evidenceDir);
3317
+ for (const f of files) {
3318
+ const fp = path.join(evidenceDir, f);
3158
3319
  try {
3159
- const content = fs.readFileSync(path.join(workflowDir, f), 'utf8');
3160
- return content.includes('delimit-ai/delimit') || content.includes('delimit');
3161
- } catch { return false; }
3162
- });
3163
- if (hasDelimit) {
3164
- console.log(chalk.green(' ✓ GitHub Action workflow found'));
3165
- ok++;
3166
- } else {
3167
- console.log(chalk.yellow(' ⚠ No Delimit GitHub Action workflow — run delimit init for setup instructions'));
3168
- warn++;
3320
+ if (f.endsWith('.json')) {
3321
+ const data = JSON.parse(fs.readFileSync(fp, 'utf-8'));
3322
+ // Determine timestamp from various fields
3323
+ let ts = null;
3324
+ if (data.timestamp) ts = new Date(data.timestamp);
3325
+ else if (data.collected_at) ts = new Date(data.collected_at * 1000);
3326
+ if (ts && ts >= cutoff) {
3327
+ evidenceEvents.push({ ...data, _ts: ts, _file: f });
3328
+ }
3329
+ } else if (f.endsWith('.jsonl')) {
3330
+ const lines = fs.readFileSync(fp, 'utf-8').split('\n').filter(Boolean);
3331
+ for (const line of lines) {
3332
+ try {
3333
+ const data = JSON.parse(line);
3334
+ let ts = null;
3335
+ if (data.timestamp) ts = new Date(data.timestamp);
3336
+ else if (data.collected_at) ts = new Date(data.collected_at * 1000);
3337
+ if (ts && ts >= cutoff) {
3338
+ evidenceEvents.push({ ...data, _ts: ts, _file: f });
3339
+ }
3340
+ } catch {}
3341
+ }
3342
+ }
3343
+ } catch {}
3169
3344
  }
3170
- } else {
3171
- console.log(chalk.yellow(' ⚠ No .github/workflows/ directory'));
3172
- warn++;
3173
3345
  }
3346
+ evidenceEvents.sort((a, b) => a._ts - b._ts);
3174
3347
 
3175
- // Check git
3176
- try {
3177
- const { execSync } = require('child_process');
3178
- execSync('git rev-parse --git-dir', { stdio: 'pipe' });
3179
- console.log(chalk.green(' ✓ Git repository detected'));
3180
- ok++;
3181
- } catch {
3182
- console.log(chalk.yellow(' ⚠ Not a git repository'));
3183
- warn++;
3184
- }
3348
+ // Categorize evidence
3349
+ const violations = evidenceEvents.filter(e =>
3350
+ e.result === 'failed' || e.result === 'blocked' ||
3351
+ (e.action && /fail|block|violation|error/i.test(e.action))
3352
+ );
3353
+ const approvals = evidenceEvents.filter(e =>
3354
+ e.result === 'passed' || e.result === 'approved' ||
3355
+ (e.action && /pass|approve|success/i.test(e.action))
3356
+ );
3185
3357
 
3186
- // Preview what init would create (LED-265)
3187
- const delimitDir = path.join(process.cwd(), '.delimit');
3188
- const hasDelimitDir = fs.existsSync(delimitDir);
3189
- console.log(chalk.bold('\n Init Preview:'));
3190
- if (hasDelimitDir) {
3191
- const files = [];
3192
- try {
3193
- const walk = (dir, prefix) => {
3194
- for (const f of fs.readdirSync(dir)) {
3195
- const full = path.join(dir, f);
3196
- const rel = prefix ? `${prefix}/${f}` : f;
3197
- if (fs.statSync(full).isDirectory()) walk(full, rel);
3198
- else files.push(rel);
3358
+ // Collect ledger items
3359
+ const ledgerItems = [];
3360
+ if (fs.existsSync(ledgerDir)) {
3361
+ const files = fs.readdirSync(ledgerDir);
3362
+ for (const f of files) {
3363
+ const fp = path.join(ledgerDir, f);
3364
+ try {
3365
+ if (f.endsWith('.json')) {
3366
+ const data = JSON.parse(fs.readFileSync(fp, 'utf-8'));
3367
+ if (Array.isArray(data)) {
3368
+ ledgerItems.push(...data);
3369
+ } else {
3370
+ ledgerItems.push(data);
3371
+ }
3372
+ } else if (f.endsWith('.jsonl')) {
3373
+ const lines = fs.readFileSync(fp, 'utf-8').split('\n').filter(Boolean);
3374
+ for (const line of lines) {
3375
+ try { ledgerItems.push(JSON.parse(line)); } catch {}
3376
+ }
3199
3377
  }
3200
- };
3201
- walk(delimitDir, '.delimit');
3202
- } catch {}
3203
- console.log(chalk.green(` Already initialized — ${files.length} file(s) in .delimit/`));
3204
- files.slice(0, 8).forEach(f => console.log(chalk.gray(` ${f}`)));
3205
- if (files.length > 8) console.log(chalk.gray(` ... and ${files.length - 8} more`));
3206
- } else {
3207
- console.log(chalk.gray(' Running delimit init would create:'));
3208
- console.log(chalk.gray(' .delimit/policies.yml — governance policy rules'));
3209
- console.log(chalk.gray(' .delimit/evidence/ — audit trail events'));
3210
- console.log(chalk.gray(' .delimit/compliance.json — if compliance template selected'));
3211
- if (fs.existsSync(path.join(process.cwd(), '.github'))) {
3212
- console.log(chalk.gray(' .github/workflows/api-governance.yml'));
3213
- console.log(chalk.gray(' .github/workflows/api-drift-monitor.yml'));
3378
+ } catch {}
3214
3379
  }
3215
3380
  }
3381
+ const openLedgerItems = ledgerItems.filter(i => i.status === 'open' || !i.status);
3216
3382
 
3217
- // Undo instruction (LED-265)
3218
- console.log(chalk.bold('\n Undo:'));
3219
- console.log(chalk.gray(' rm -rf .delimit — remove all Delimit files'));
3220
- console.log(chalk.gray(' delimit uninstall --dry-run — preview MCP removal\n'));
3221
-
3222
- // Summary
3223
- console.log('');
3224
- if (fail === 0 && warn === 0) {
3225
- console.log(chalk.green.bold(' All checks passed! Ready to lint.\n'));
3226
- } else if (fail === 0) {
3227
- console.log(chalk.yellow.bold(` ${ok} passed, ${warn} warning(s). Setup looks good.\n`));
3228
- } else {
3229
- console.log(chalk.red.bold(` ${ok} passed, ${warn} warning(s), ${fail} error(s). Fix errors above.\n`));
3383
+ // Count memory entries
3384
+ let memoryCount = 0;
3385
+ const recentMemories = [];
3386
+ if (fs.existsSync(memoryDir)) {
3387
+ const files = fs.readdirSync(memoryDir).filter(f => f.endsWith('.json') || f.endsWith('.jsonl'));
3388
+ for (const f of files) {
3389
+ const fp = path.join(memoryDir, f);
3390
+ try {
3391
+ if (f.endsWith('.json')) {
3392
+ const data = JSON.parse(fs.readFileSync(fp, 'utf-8'));
3393
+ memoryCount++;
3394
+ if (data.created_at && new Date(data.created_at) >= cutoff) {
3395
+ recentMemories.push(data);
3396
+ }
3397
+ } else if (f.endsWith('.jsonl')) {
3398
+ const lines = fs.readFileSync(fp, 'utf-8').split('\n').filter(Boolean);
3399
+ for (const line of lines) {
3400
+ try {
3401
+ const data = JSON.parse(line);
3402
+ memoryCount++;
3403
+ if (data.created_at && new Date(data.created_at) >= cutoff) {
3404
+ recentMemories.push(data);
3405
+ }
3406
+ } catch {}
3407
+ }
3408
+ }
3409
+ } catch {}
3410
+ }
3230
3411
  }
3231
- });
3232
-
3233
- // Hooks command — install/remove git hooks for governance
3234
- program
3235
- .command('hooks <action>')
3236
- .description('Install or remove git hooks (install | remove | status)')
3237
- .option('--pre-push', 'Also add pre-push hook')
3238
- .action(async (action, opts) => {
3239
- const projectDir = process.cwd();
3240
- const gitDir = path.join(projectDir, '.git');
3241
3412
 
3242
- if (!fs.existsSync(gitDir)) {
3243
- console.log(chalk.red('\n Not a git repository. Run git init first.\n'));
3244
- process.exitCode = 1;
3245
- return;
3246
- }
3413
+ // Git history
3414
+ let gitCommits = [];
3415
+ try {
3416
+ const sinceDate = cutoff.toISOString().split('T')[0];
3417
+ const gitLog = execSync(
3418
+ `git log --oneline --since="${sinceDate}" --no-decorate -20 2>/dev/null`,
3419
+ { encoding: 'utf-8', timeout: 5000 }
3420
+ ).trim();
3421
+ if (gitLog) {
3422
+ gitCommits = gitLog.split('\n').filter(Boolean);
3423
+ }
3424
+ } catch {}
3247
3425
 
3248
- const hooksDir = path.join(gitDir, 'hooks');
3249
- fs.mkdirSync(hooksDir, { recursive: true });
3426
+ // Pre-commit hook detection
3427
+ let hasPreCommitHook = false;
3428
+ try {
3429
+ const hookPath = path.join(process.cwd(), '.git', 'hooks', 'pre-commit');
3430
+ hasPreCommitHook = fs.existsSync(hookPath);
3431
+ } catch {}
3250
3432
 
3251
- const preCommitPath = path.join(hooksDir, 'pre-commit');
3252
- const prePushPath = path.join(hooksDir, 'pre-push');
3253
- const marker = '# delimit-governance-hook';
3433
+ // Recommendations
3434
+ const recommendations = [];
3435
+ if (!hasPreCommitHook) {
3436
+ recommendations.push('Consider adding pre-commit hooks: npx delimit-cli init');
3437
+ }
3438
+ if (violations.length > approvals.length && evidenceEvents.length > 0) {
3439
+ recommendations.push('Failure rate exceeds pass rate — review policy strictness or address recurring violations');
3440
+ }
3441
+ if (evidenceEvents.length === 0) {
3442
+ recommendations.push('No governance events found — run delimit lint or delimit scan to start collecting evidence');
3443
+ }
3444
+ if (openLedgerItems.length > 20) {
3445
+ recommendations.push(`${openLedgerItems.length} open ledger items — consider triaging and closing resolved items`);
3446
+ }
3447
+ if (memoryCount === 0) {
3448
+ recommendations.push('No memory entries — use delimit remember to capture architecture decisions and gotchas');
3449
+ }
3450
+
3451
+ // Build report data
3452
+ const reportData = {
3453
+ generated_at: now.toISOString(),
3454
+ period: { since: cutoff.toISOString(), until: now.toISOString(), duration: options.since },
3455
+ summary: {
3456
+ total_events: evidenceEvents.length,
3457
+ violations: violations.length,
3458
+ approvals: approvals.length,
3459
+ other_events: evidenceEvents.length - violations.length - approvals.length,
3460
+ pass_rate: evidenceEvents.length > 0
3461
+ ? Math.round((approvals.length / evidenceEvents.length) * 100)
3462
+ : null,
3463
+ ledger_items_open: openLedgerItems.length,
3464
+ ledger_items_total: ledgerItems.length,
3465
+ memory_entries: memoryCount,
3466
+ recent_memories: recentMemories.length,
3467
+ git_commits: gitCommits.length,
3468
+ },
3469
+ violations: violations.map(v => ({
3470
+ action: v.action || 'unknown',
3471
+ result: v.result || 'failed',
3472
+ timestamp: v._ts.toISOString(),
3473
+ target: v.target || null,
3474
+ files: v.files || null,
3475
+ })),
3476
+ approvals: approvals.map(a => ({
3477
+ action: a.action || 'unknown',
3478
+ result: a.result || 'passed',
3479
+ timestamp: a._ts.toISOString(),
3480
+ target: a.target || null,
3481
+ })),
3482
+ audit_events: evidenceEvents.map(e => ({
3483
+ action: e.action || 'evidence_collected',
3484
+ result: e.result || null,
3485
+ timestamp: e._ts.toISOString(),
3486
+ target: e.target || null,
3487
+ file: e._file,
3488
+ })),
3489
+ active_ledger_items: openLedgerItems.slice(0, 50).map(i => ({
3490
+ id: i.id || null,
3491
+ title: i.title || null,
3492
+ type: i.type || null,
3493
+ priority: i.priority || null,
3494
+ status: i.status || 'open',
3495
+ created_at: i.created_at || null,
3496
+ })),
3497
+ git_commits: gitCommits,
3498
+ recommendations,
3499
+ };
3500
+
3501
+ // Format output
3502
+ let output = '';
3503
+
3504
+ if (fmt === 'json') {
3505
+ output = JSON.stringify(reportData, null, 2);
3506
+ } else if (fmt === 'md') {
3507
+ const lines = [];
3508
+ lines.push('# Delimit Governance Report');
3509
+ lines.push('');
3510
+ lines.push(`**Period**: ${cutoff.toISOString().split('T')[0]} to ${now.toISOString().split('T')[0]} (${options.since})`);
3511
+ lines.push(`**Generated**: ${now.toISOString()}`);
3512
+ lines.push('');
3513
+
3514
+ lines.push('## Summary');
3515
+ lines.push('');
3516
+ lines.push(`| Metric | Value |`);
3517
+ lines.push(`|--------|-------|`);
3518
+ lines.push(`| Total governance events | ${reportData.summary.total_events} |`);
3519
+ lines.push(`| Violations | ${reportData.summary.violations} |`);
3520
+ lines.push(`| Approvals | ${reportData.summary.approvals} |`);
3521
+ lines.push(`| Pass rate | ${reportData.summary.pass_rate !== null ? reportData.summary.pass_rate + '%' : 'N/A'} |`);
3522
+ lines.push(`| Open ledger items | ${reportData.summary.ledger_items_open} |`);
3523
+ lines.push(`| Memory entries | ${reportData.summary.memory_entries} (${reportData.summary.recent_memories} recent) |`);
3524
+ lines.push(`| Git commits | ${reportData.summary.git_commits} |`);
3525
+ lines.push('');
3526
+
3527
+ if (reportData.violations.length > 0) {
3528
+ lines.push('## Violations');
3529
+ lines.push('');
3530
+ for (const v of reportData.violations) {
3531
+ lines.push(`- **${v.action}** - ${v.result} at ${v.timestamp}${v.target ? ' (' + v.target + ')' : ''}`);
3532
+ }
3533
+ lines.push('');
3534
+ } else {
3535
+ lines.push('## Violations');
3536
+ lines.push('');
3537
+ lines.push('No violations in this period.');
3538
+ lines.push('');
3539
+ }
3540
+
3541
+ if (reportData.approvals.length > 0) {
3542
+ lines.push('## Approvals');
3543
+ lines.push('');
3544
+ for (const a of reportData.approvals) {
3545
+ lines.push(`- **${a.action}** - ${a.result} at ${a.timestamp}${a.target ? ' (' + a.target + ')' : ''}`);
3546
+ }
3547
+ lines.push('');
3548
+ }
3549
+
3550
+ if (reportData.audit_events.length > 0) {
3551
+ lines.push('## Audit Events');
3552
+ lines.push('');
3553
+ for (const e of reportData.audit_events) {
3554
+ lines.push(`- \`${e.timestamp}\` ${e.action}${e.result ? ' [' + e.result + ']' : ''}${e.target ? ' - ' + e.target : ''}`);
3555
+ }
3556
+ lines.push('');
3557
+ }
3558
+
3559
+ if (reportData.active_ledger_items.length > 0) {
3560
+ lines.push('## Active Ledger Items');
3561
+ lines.push('');
3562
+ for (const i of reportData.active_ledger_items) {
3563
+ const prefix = i.priority ? `[${i.priority}]` : '';
3564
+ lines.push(`- ${prefix} ${i.id || ''} ${i.title || 'Untitled'}${i.type ? ' (' + i.type + ')' : ''}`);
3565
+ }
3566
+ if (openLedgerItems.length > 50) {
3567
+ lines.push(`- ... and ${openLedgerItems.length - 50} more`);
3568
+ }
3569
+ lines.push('');
3570
+ }
3571
+
3572
+ if (gitCommits.length > 0) {
3573
+ lines.push('## Recent Commits');
3574
+ lines.push('');
3575
+ for (const c of gitCommits) {
3576
+ lines.push(`- \`${c}\``);
3577
+ }
3578
+ lines.push('');
3579
+ }
3580
+
3581
+ if (recommendations.length > 0) {
3582
+ lines.push('## Recommendations');
3583
+ lines.push('');
3584
+ for (const r of recommendations) {
3585
+ lines.push(`- ${r}`);
3586
+ }
3587
+ lines.push('');
3588
+ }
3589
+
3590
+ lines.push('---');
3591
+ lines.push('*Generated by [Delimit](https://delimit.ai) governance reporting*');
3592
+ output = lines.join('\n');
3593
+ } else if (fmt === 'html') {
3594
+ const esc = (s) => String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
3595
+ const h = [];
3596
+ h.push('<!DOCTYPE html>');
3597
+ h.push('<html lang="en"><head><meta charset="utf-8">');
3598
+ h.push('<title>Delimit Governance Report</title>');
3599
+ h.push('<style>');
3600
+ h.push('body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;max-width:900px;margin:2rem auto;padding:0 1rem;color:#1a1a2e;line-height:1.6}');
3601
+ h.push('h1{color:#16213e;border-bottom:3px solid #0f3460;padding-bottom:.5rem}');
3602
+ h.push('h2{color:#0f3460;margin-top:2rem}');
3603
+ h.push('table{border-collapse:collapse;width:100%}');
3604
+ h.push('th,td{text-align:left;padding:.5rem .75rem;border:1px solid #ddd}');
3605
+ h.push('th{background:#f0f4f8;font-weight:600}');
3606
+ h.push('.pass{color:#16a34a}.fail{color:#dc2626}');
3607
+ h.push('.badge{display:inline-block;padding:.1rem .5rem;border-radius:3px;font-size:.85rem;font-weight:600}');
3608
+ h.push('.badge-ok{background:#dcfce7;color:#166534}.badge-fail{background:#fef2f2;color:#991b1b}');
3609
+ h.push('ul{padding-left:1.5rem}li{margin:.25rem 0}');
3610
+ h.push('.meta{color:#6b7280;font-size:.9rem}');
3611
+ h.push('footer{margin-top:2rem;padding-top:1rem;border-top:1px solid #ddd;color:#9ca3af;font-size:.85rem}');
3612
+ h.push('</style></head><body>');
3613
+ h.push('<h1>Delimit Governance Report</h1>');
3614
+ h.push(`<p class="meta">Period: ${esc(cutoff.toISOString().split('T')[0])} to ${esc(now.toISOString().split('T')[0])} (${esc(options.since)}) &mdash; Generated: ${esc(now.toISOString())}</p>`);
3615
+
3616
+ h.push('<h2>Summary</h2>');
3617
+ h.push('<table><tr><th>Metric</th><th>Value</th></tr>');
3618
+ h.push(`<tr><td>Total governance events</td><td>${reportData.summary.total_events}</td></tr>`);
3619
+ h.push(`<tr><td>Violations</td><td><span class="${reportData.summary.violations > 0 ? 'fail' : ''}">${reportData.summary.violations}</span></td></tr>`);
3620
+ h.push(`<tr><td>Approvals</td><td><span class="pass">${reportData.summary.approvals}</span></td></tr>`);
3621
+ h.push(`<tr><td>Pass rate</td><td>${reportData.summary.pass_rate !== null ? '<span class="badge ' + (reportData.summary.pass_rate >= 80 ? 'badge-ok' : 'badge-fail') + '">' + reportData.summary.pass_rate + '%</span>' : 'N/A'}</td></tr>`);
3622
+ h.push(`<tr><td>Open ledger items</td><td>${reportData.summary.ledger_items_open}</td></tr>`);
3623
+ h.push(`<tr><td>Memory entries</td><td>${reportData.summary.memory_entries} (${reportData.summary.recent_memories} recent)</td></tr>`);
3624
+ h.push(`<tr><td>Git commits</td><td>${reportData.summary.git_commits}</td></tr>`);
3625
+ h.push('</table>');
3626
+
3627
+ h.push('<h2>Violations</h2>');
3628
+ if (reportData.violations.length > 0) {
3629
+ h.push('<ul>');
3630
+ for (const v of reportData.violations) {
3631
+ h.push(`<li><strong>${esc(v.action)}</strong> - ${esc(v.result)} at ${esc(v.timestamp)}${v.target ? ' (' + esc(v.target) + ')' : ''}</li>`);
3632
+ }
3633
+ h.push('</ul>');
3634
+ } else {
3635
+ h.push('<p class="pass">No violations in this period.</p>');
3636
+ }
3637
+
3638
+ if (reportData.approvals.length > 0) {
3639
+ h.push('<h2>Approvals</h2><ul>');
3640
+ for (const a of reportData.approvals) {
3641
+ h.push(`<li><strong>${esc(a.action)}</strong> - ${esc(a.result)} at ${esc(a.timestamp)}${a.target ? ' (' + esc(a.target) + ')' : ''}</li>`);
3642
+ }
3643
+ h.push('</ul>');
3644
+ }
3645
+
3646
+ if (reportData.audit_events.length > 0) {
3647
+ h.push('<h2>Audit Events</h2><ul>');
3648
+ for (const e of reportData.audit_events) {
3649
+ h.push(`<li><code>${esc(e.timestamp)}</code> ${esc(e.action)}${e.result ? ' [' + esc(e.result) + ']' : ''}${e.target ? ' - ' + esc(e.target) : ''}</li>`);
3650
+ }
3651
+ h.push('</ul>');
3652
+ }
3653
+
3654
+ if (reportData.active_ledger_items.length > 0) {
3655
+ h.push('<h2>Active Ledger Items</h2><ul>');
3656
+ for (const i of reportData.active_ledger_items) {
3657
+ const prefix = i.priority ? `[${esc(i.priority)}]` : '';
3658
+ h.push(`<li>${prefix} ${esc(i.id || '')} ${esc(i.title || 'Untitled')}${i.type ? ' (' + esc(i.type) + ')' : ''}</li>`);
3659
+ }
3660
+ if (openLedgerItems.length > 50) {
3661
+ h.push(`<li>... and ${openLedgerItems.length - 50} more</li>`);
3662
+ }
3663
+ h.push('</ul>');
3664
+ }
3665
+
3666
+ if (gitCommits.length > 0) {
3667
+ h.push('<h2>Recent Commits</h2><ul>');
3668
+ for (const c of gitCommits) {
3669
+ h.push(`<li><code>${esc(c)}</code></li>`);
3670
+ }
3671
+ h.push('</ul>');
3672
+ }
3673
+
3674
+ if (recommendations.length > 0) {
3675
+ h.push('<h2>Recommendations</h2><ul>');
3676
+ for (const r of recommendations) {
3677
+ h.push(`<li>${esc(r)}</li>`);
3678
+ }
3679
+ h.push('</ul>');
3680
+ }
3681
+
3682
+ h.push('<footer>Generated by <a href="https://delimit.ai">Delimit</a> governance reporting</footer>');
3683
+ h.push('</body></html>');
3684
+ output = h.join('\n');
3685
+ }
3686
+
3687
+ // Output
3688
+ if (options.output) {
3689
+ const outPath = path.resolve(options.output);
3690
+ fs.writeFileSync(outPath, output, 'utf-8');
3691
+ console.log(chalk.green(` Report written to ${outPath}`));
3692
+ } else {
3693
+ if (fmt === 'md' || fmt === 'html') {
3694
+ console.log(output);
3695
+ } else {
3696
+ console.log(output);
3697
+ }
3698
+ }
3699
+ });
3700
+
3701
+ // Doctor command — verify setup is correct
3702
+ program
3703
+ .command('doctor')
3704
+ .description('Verify Delimit setup and diagnose common issues')
3705
+ .option('--ci', 'Output JSON and exit non-zero on failures (for pipelines)')
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')
3709
+ .action(async (opts) => {
3710
+ const ciMode = !!opts.ci;
3711
+ const fixMode = !!opts.fix;
3712
+ const dryRunMode = !!opts.dryRun;
3713
+ const undoMode = !!opts.undo;
3714
+ const homeDir = os.homedir();
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
+
3816
+ const results = []; // { name, status: 'pass'|'warn'|'fail', message, fix? }
3817
+ const manifestActions = []; // track what --fix creates
3818
+
3819
+ function addResult(name, status, message, fix) {
3820
+ results.push({ name, status, message, fix: fix || null });
3821
+ }
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
+
3828
+ // --- Check 1: Policy file ---
3829
+ const policyPath = path.join(process.cwd(), '.delimit', 'policies.yml');
3830
+ if (fs.existsSync(policyPath)) {
3831
+ addResult('policy-file', 'pass', '.delimit/policies.yml found');
3832
+ try {
3833
+ const policy = yaml.load(fs.readFileSync(policyPath, 'utf8'));
3834
+ if (policy && (policy.rules !== undefined || policy.override_defaults !== undefined)) {
3835
+ addResult('policy-valid', 'pass', 'Policy file is valid YAML');
3836
+ } else {
3837
+ addResult('policy-valid', 'warn', 'Policy file has no rules section — add rules to .delimit/policies.yml');
3838
+ }
3839
+ } catch (e) {
3840
+ addResult('policy-valid', 'fail', `Policy file has invalid YAML: ${e.message}`, 'delimit init --force');
3841
+ }
3842
+ } else {
3843
+ addResult('policy-file', 'fail', 'No .delimit/policies.yml', 'delimit init');
3844
+ if (fixMode) {
3845
+ try {
3846
+ const delimitDirPre = fs.existsSync(path.join(process.cwd(), '.delimit'));
3847
+ execSync('delimit init --dry-run', { stdio: 'pipe', cwd: process.cwd() });
3848
+ // If dry-run works, run real init
3849
+ execSync('delimit init', { stdio: 'pipe', cwd: process.cwd() });
3850
+ addResult('policy-file-fix', 'pass', 'Auto-fixed: ran delimit init');
3851
+ if (!delimitDirPre) trackCreated(path.join(process.cwd(), '.delimit'));
3852
+ trackCreated(policyPath);
3853
+ } catch {
3854
+ addResult('policy-file-fix', 'warn', 'Auto-fix failed: run delimit init manually');
3855
+ }
3856
+ }
3857
+ }
3858
+
3859
+ // --- Check 2: OpenAPI spec ---
3860
+ const specPatterns = [
3861
+ 'openapi.yaml', 'openapi.yml', 'openapi.json',
3862
+ 'swagger.yaml', 'swagger.yml', 'swagger.json',
3863
+ 'docs/openapi.yaml', 'docs/openapi.yml', 'docs/openapi.json',
3864
+ 'spec/openapi.yaml', 'spec/openapi.json',
3865
+ 'api/openapi.yaml', 'api/openapi.json',
3866
+ 'contrib/openapi.json',
3867
+ ];
3868
+ const foundSpecs = specPatterns.filter(p => fs.existsSync(path.join(process.cwd(), p)));
3869
+ if (foundSpecs.length > 0) {
3870
+ addResult('openapi-spec', 'pass', `OpenAPI spec found: ${foundSpecs[0]}`);
3871
+ } else {
3872
+ const pkgJson = path.join(process.cwd(), 'package.json');
3873
+ const reqTxt = path.join(process.cwd(), 'requirements.txt');
3874
+ if (fs.existsSync(pkgJson) || fs.existsSync(reqTxt)) {
3875
+ addResult('openapi-spec', 'warn', 'No OpenAPI spec file — Zero-Spec Mode may work if this is a FastAPI/NestJS/Express project');
3876
+ } else {
3877
+ addResult('openapi-spec', 'fail', 'No OpenAPI spec file found', 'Create openapi.yaml in project root or run: delimit scan');
3878
+ }
3879
+ }
3880
+
3881
+ // --- Check 3: GitHub workflow ---
3882
+ const workflowDir = path.join(process.cwd(), '.github', 'workflows');
3883
+ if (fs.existsSync(workflowDir)) {
3884
+ const workflows = fs.readdirSync(workflowDir);
3885
+ const hasDelimit = workflows.some(f => {
3886
+ try {
3887
+ const content = fs.readFileSync(path.join(workflowDir, f), 'utf8');
3888
+ return content.includes('delimit-ai/delimit') || content.includes('delimit');
3889
+ } catch { return false; }
3890
+ });
3891
+ if (hasDelimit) {
3892
+ addResult('github-action', 'pass', 'GitHub Action workflow found');
3893
+ } else {
3894
+ addResult('github-action', 'warn', 'No Delimit GitHub Action workflow', 'delimit init');
3895
+ }
3896
+ } else {
3897
+ addResult('github-action', 'warn', 'No .github/workflows/ directory', 'mkdir -p .github/workflows && delimit init');
3898
+ }
3899
+
3900
+ // --- Check 4: Git repository ---
3901
+ try {
3902
+ execSync('git rev-parse --git-dir', { stdio: 'pipe' });
3903
+ addResult('git-repo', 'pass', 'Git repository detected');
3904
+ } catch {
3905
+ addResult('git-repo', 'warn', 'Not a git repository', 'git init');
3906
+ }
3907
+
3908
+ // --- Check 5: Node.js version ---
3909
+ const nodeVersion = parseInt(process.versions.node.split('.')[0], 10);
3910
+ if (nodeVersion >= 18) {
3911
+ addResult('node-version', 'pass', `Node.js v${process.versions.node}`);
3912
+ } else {
3913
+ addResult('node-version', 'warn', `Node.js v${process.versions.node} — v18+ recommended`, 'nvm install 18 && nvm use 18');
3914
+ }
3915
+
3916
+ // --- Check 6: Python availability ---
3917
+ try {
3918
+ const pyVersion = execSync('python3 --version', { stdio: 'pipe' }).toString().trim();
3919
+ addResult('python', 'pass', `${pyVersion} available (needed for MCP server)`);
3920
+ } catch {
3921
+ addResult('python', 'fail', 'python3 not found on PATH — required for MCP server', 'Install Python 3: https://python.org/downloads/');
3922
+ }
3923
+
3924
+ // --- Check 7: MCP server connectivity ---
3925
+ const mcpJsonPath = path.join(homeDir, '.mcp.json');
3926
+ const mcpServerPath = path.join(delimitHome, 'server', 'ai', 'server.py');
3927
+ if (fs.existsSync(mcpJsonPath)) {
3928
+ try {
3929
+ const mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'));
3930
+ const hasDelimitMcp = mcpConfig.mcpServers && mcpConfig.mcpServers.delimit;
3931
+ if (hasDelimitMcp) {
3932
+ addResult('mcp-config', 'pass', 'Delimit configured in ~/.mcp.json');
3933
+ } else {
3934
+ addResult('mcp-config', 'warn', 'Delimit not configured in ~/.mcp.json', 'delimit setup --all');
3935
+ }
3936
+ } catch {
3937
+ addResult('mcp-config', 'warn', '~/.mcp.json exists but failed to parse', 'Check ~/.mcp.json for valid JSON');
3938
+ }
3939
+ } else {
3940
+ addResult('mcp-config', 'warn', 'No ~/.mcp.json found', 'delimit setup --all');
3941
+ }
3942
+ if (fs.existsSync(mcpServerPath)) {
3943
+ addResult('mcp-server', 'pass', 'MCP server file exists at ~/.delimit/server/ai/server.py');
3944
+ } else {
3945
+ addResult('mcp-server', 'fail', 'MCP server not installed at ~/.delimit/server/ai/server.py', 'delimit setup --all');
3946
+ if (fixMode) {
3947
+ try {
3948
+ execSync('delimit setup --all', { stdio: 'pipe' });
3949
+ addResult('mcp-server-fix', 'pass', 'Auto-fixed: ran delimit setup --all');
3950
+ trackCreated(mcpServerPath);
3951
+ } catch {
3952
+ addResult('mcp-server-fix', 'warn', 'Auto-fix failed: run delimit setup --all manually');
3953
+ }
3954
+ }
3955
+ }
3956
+
3957
+ // --- Check 8: Memory health ---
3958
+ const memoryDir = path.join(delimitHome, 'memory');
3959
+ if (fs.existsSync(memoryDir)) {
3960
+ let memoryCount = 0;
3961
+ try {
3962
+ const memFiles = fs.readdirSync(memoryDir).filter(f => f.endsWith('.jsonl'));
3963
+ for (const mf of memFiles) {
3964
+ const content = fs.readFileSync(path.join(memoryDir, mf), 'utf8');
3965
+ memoryCount += content.split('\n').filter(l => l.trim()).length;
3966
+ }
3967
+ } catch {}
3968
+ if (memoryCount > 1000) {
3969
+ addResult('memory-health', 'warn', `Memory store has ${memoryCount} entries (>1000) — consider pruning`, 'delimit memory --prune');
3970
+ } else {
3971
+ addResult('memory-health', 'pass', `Memory store: ${memoryCount} entries`);
3972
+ }
3973
+ } else {
3974
+ addResult('memory-health', 'warn', 'No ~/.delimit/memory/ directory', `mkdir -p ${memoryDir}`);
3975
+ if (fixMode) {
3976
+ try {
3977
+ fs.mkdirSync(memoryDir, { recursive: true });
3978
+ addResult('memory-health-fix', 'pass', 'Auto-fixed: created ~/.delimit/memory/');
3979
+ trackCreated(memoryDir);
3980
+ } catch {
3981
+ addResult('memory-health-fix', 'warn', `Auto-fix failed: run mkdir -p ${memoryDir}`);
3982
+ }
3983
+ }
3984
+ }
3985
+
3986
+ // --- Check 9: Models configured ---
3987
+ const modelsPath = path.join(delimitHome, 'models.json');
3988
+ if (fs.existsSync(modelsPath)) {
3989
+ try {
3990
+ const models = JSON.parse(fs.readFileSync(modelsPath, 'utf8'));
3991
+ const configured = Array.isArray(models)
3992
+ ? models.filter(m => m.api_key)
3993
+ : Object.values(models).filter(m => m && m.api_key);
3994
+ if (configured.length > 0) {
3995
+ addResult('models', 'pass', `${configured.length} model(s) configured with API keys`);
3996
+ } else {
3997
+ addResult('models', 'warn', 'models.json exists but no models have api_key set', 'Edit ~/.delimit/models.json and add your API keys');
3998
+ }
3999
+ } catch {
4000
+ addResult('models', 'warn', '~/.delimit/models.json exists but failed to parse', 'Check ~/.delimit/models.json for valid JSON');
4001
+ }
4002
+ } else {
4003
+ addResult('models', 'warn', 'No ~/.delimit/models.json — multi-model features unavailable', 'delimit setup --all');
4004
+ }
4005
+
4006
+ // --- Check 10: License status ---
4007
+ const licensePath = path.join(delimitHome, 'license.json');
4008
+ if (fs.existsSync(licensePath)) {
4009
+ try {
4010
+ const license = JSON.parse(fs.readFileSync(licensePath, 'utf8'));
4011
+ const tier = license.tier || license.plan || 'Unknown';
4012
+ addResult('license', 'pass', `License: ${tier}`);
4013
+ } catch {
4014
+ addResult('license', 'warn', '~/.delimit/license.json exists but failed to parse', 'Check ~/.delimit/license.json for valid JSON');
4015
+ }
4016
+ } else {
4017
+ addResult('license', 'pass', 'License: Free tier (upgrade at delimit.ai/pricing)');
4018
+ }
4019
+
4020
+ // --- Check 11: Cross-model hooks ---
4021
+ const claudeSettingsPath = path.join(process.cwd(), '.claude', 'settings.json');
4022
+ if (fs.existsSync(claudeSettingsPath)) {
4023
+ try {
4024
+ const settings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf8'));
4025
+ const hasHooks = settings.hooks && settings.hooks.PostToolUse;
4026
+ if (hasHooks) {
4027
+ addResult('cross-model-hooks', 'pass', 'Claude Code PostToolUse hooks installed');
4028
+ } else {
4029
+ addResult('cross-model-hooks', 'warn', 'Claude Code hooks not configured in .claude/settings.json', 'delimit hooks install');
4030
+ }
4031
+ } catch {
4032
+ addResult('cross-model-hooks', 'warn', '.claude/settings.json exists but failed to parse', 'Check .claude/settings.json for valid JSON');
4033
+ }
4034
+ } else {
4035
+ addResult('cross-model-hooks', 'warn', 'No .claude/settings.json — cross-model hooks not installed', 'delimit hooks install');
4036
+ }
4037
+
4038
+ // --- Check 12: Disk space ---
4039
+ if (fs.existsSync(delimitHome)) {
4040
+ try {
4041
+ const duOutput = execSync(`du -sm "${delimitHome}"`, { stdio: 'pipe' }).toString().trim();
4042
+ const sizeMb = parseInt(duOutput.split('\t')[0], 10);
4043
+ if (sizeMb > 500) {
4044
+ addResult('disk-space', 'warn', `~/.delimit/ is ${sizeMb}MB (>500MB) — consider cleanup`, `du -sh ~/.delimit/*/`);
4045
+ } else {
4046
+ addResult('disk-space', 'pass', `~/.delimit/ disk usage: ${sizeMb}MB`);
4047
+ }
4048
+ } catch {
4049
+ addResult('disk-space', 'pass', '~/.delimit/ disk usage: unknown (du not available)');
4050
+ }
4051
+ } else {
4052
+ addResult('disk-space', 'pass', '~/.delimit/ does not exist yet');
4053
+ }
4054
+
4055
+ // --- CI mode: output JSON and exit ---
4056
+ if (ciMode) {
4057
+ const ok = results.filter(r => r.status === 'pass').length;
4058
+ const warn = results.filter(r => r.status === 'warn').length;
4059
+ const fail = results.filter(r => r.status === 'fail').length;
4060
+ const total = results.length;
4061
+ const score = total > 0 ? Math.round((ok / total) * 10) : 0;
4062
+ const output = {
4063
+ version: '4.20',
4064
+ health_score: `${score}/10`,
4065
+ summary: { pass: ok, warn, fail, total },
4066
+ checks: results,
4067
+ };
4068
+ console.log(JSON.stringify(output, null, 2));
4069
+ if (fail > 0) {
4070
+ process.exitCode = 1;
4071
+ }
4072
+ return;
4073
+ }
4074
+
4075
+ // --- Human-readable output ---
4076
+ console.log(chalk.bold('\n Delimit Doctor v4.20\n'));
4077
+
4078
+ const icons = { pass: chalk.green(' ✓'), warn: chalk.yellow(' ⚠'), fail: chalk.red(' ✗') };
4079
+ const colors = { pass: chalk.green, warn: chalk.yellow, fail: chalk.red };
4080
+ for (const r of results) {
4081
+ console.log(`${icons[r.status]} ${colors[r.status](r.message)}`);
4082
+ if (r.fix && r.status !== 'pass') {
4083
+ console.log(chalk.gray(` Run: ${r.fix}`));
4084
+ }
4085
+ }
4086
+
4087
+ // Preview what init would create (LED-265)
4088
+ const delimitDir = path.join(process.cwd(), '.delimit');
4089
+ const hasDelimitDir = fs.existsSync(delimitDir);
4090
+ console.log(chalk.bold('\n Init Preview:'));
4091
+ if (hasDelimitDir) {
4092
+ const files = [];
4093
+ try {
4094
+ const walk = (dir, prefix) => {
4095
+ for (const f of fs.readdirSync(dir)) {
4096
+ const full = path.join(dir, f);
4097
+ const rel = prefix ? `${prefix}/${f}` : f;
4098
+ if (fs.statSync(full).isDirectory()) walk(full, rel);
4099
+ else files.push(rel);
4100
+ }
4101
+ };
4102
+ walk(delimitDir, '.delimit');
4103
+ } catch {}
4104
+ console.log(chalk.green(` Already initialized — ${files.length} file(s) in .delimit/`));
4105
+ files.slice(0, 8).forEach(f => console.log(chalk.gray(` ${f}`)));
4106
+ if (files.length > 8) console.log(chalk.gray(` ... and ${files.length - 8} more`));
4107
+ } else {
4108
+ console.log(chalk.gray(' Running delimit init would create:'));
4109
+ console.log(chalk.gray(' .delimit/policies.yml — governance policy rules'));
4110
+ console.log(chalk.gray(' .delimit/evidence/ — audit trail events'));
4111
+ console.log(chalk.gray(' .delimit/compliance.json — if compliance template selected'));
4112
+ if (fs.existsSync(path.join(process.cwd(), '.github'))) {
4113
+ console.log(chalk.gray(' .github/workflows/api-governance.yml'));
4114
+ console.log(chalk.gray(' .github/workflows/api-drift-monitor.yml'));
4115
+ }
4116
+ }
4117
+
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
+ }
4142
+
4143
+ // Health score and summary
4144
+ const ok = results.filter(r => r.status === 'pass').length;
4145
+ const warn = results.filter(r => r.status === 'warn').length;
4146
+ const fail = results.filter(r => r.status === 'fail').length;
4147
+ const total = results.length;
4148
+ const score = total > 0 ? Math.round((ok / total) * 10) : 0;
4149
+
4150
+ console.log(chalk.bold(` Health: ${score}/10`));
4151
+ console.log('');
4152
+ if (fail === 0 && warn === 0) {
4153
+ console.log(chalk.green.bold(' All checks passed! Ready to lint.\n'));
4154
+ } else if (fail === 0) {
4155
+ console.log(chalk.yellow.bold(` ${ok} passed, ${warn} warning(s). Setup looks good.\n`));
4156
+ } else {
4157
+ console.log(chalk.red.bold(` ${ok} passed, ${warn} warning(s), ${fail} error(s). Fix errors above.\n`));
4158
+ }
4159
+
4160
+ if (fail > 0) {
4161
+ process.exitCode = 1;
4162
+ }
4163
+
4164
+ // Occasional star nudge (show ~20% of the time on success)
4165
+ if (fail === 0 && Math.random() < 0.2) {
4166
+ console.log(chalk.gray(' Useful? Star us: https://github.com/delimit-ai/delimit-mcp-server\n'));
4167
+ }
4168
+ });
4169
+
4170
+ // Simulate command — dry-run governance preview ("terraform plan" for API governance)
4171
+ program
4172
+ .command('simulate')
4173
+ .description('Show what governance would block or allow without making changes')
4174
+ .option('--spec <path>', 'Path to OpenAPI spec to simulate lint against')
4175
+ .option('--policy <path>', 'Path to policies.yml (default: .delimit/policies.yml)')
4176
+ .option('--commit', 'Simulate a pre-commit governance check on staged changes')
4177
+ .option('--verbose', 'Show detailed rule breakdown')
4178
+ .action(async (opts) => {
4179
+ const projectDir = process.cwd();
4180
+ const configDir = path.join(projectDir, '.delimit');
4181
+ const policyPath = opts.policy
4182
+ ? path.resolve(opts.policy)
4183
+ : path.join(configDir, 'policies.yml');
4184
+
4185
+ console.log(chalk.bold('\n Delimit Simulate \u2014 Dry Run\n'));
4186
+
4187
+ // Load and parse policy
4188
+ let policy = null;
4189
+ let preset = 'default';
4190
+ let ruleCount = 0;
4191
+ let policyRules = [];
4192
+
4193
+ if (fs.existsSync(policyPath)) {
4194
+ try {
4195
+ const yaml = require('js-yaml');
4196
+ policy = yaml.load(fs.readFileSync(policyPath, 'utf8'));
4197
+
4198
+ // Detect preset from content
4199
+ const policyContent = fs.readFileSync(policyPath, 'utf-8');
4200
+ if (policyContent.includes('action: forbid') && !policyContent.includes('action: warn')) preset = 'strict';
4201
+ else if (!policyContent.includes('action: forbid') && policyContent.includes('action: warn')) preset = 'relaxed';
4202
+
4203
+ // Count rules from various policy formats
4204
+ if (policy && policy.rules && Array.isArray(policy.rules)) {
4205
+ policyRules = policy.rules;
4206
+ ruleCount = policyRules.length;
4207
+ } else if (policy && policy.override_defaults && Array.isArray(policy.override_defaults)) {
4208
+ policyRules = policy.override_defaults;
4209
+ ruleCount = policyRules.length;
4210
+ }
4211
+
4212
+ // Also count top-level change-type keys as implicit rules
4213
+ if (policy) {
4214
+ const changeTypeKeys = Object.keys(policy).filter(k =>
4215
+ !['rules', 'override_defaults', 'defaultMode', 'overrides', 'version', 'preset'].includes(k)
4216
+ );
4217
+ if (changeTypeKeys.length > 0 && ruleCount === 0) {
4218
+ ruleCount = changeTypeKeys.length;
4219
+ policyRules = changeTypeKeys.map(k => ({
4220
+ name: k,
4221
+ action: typeof policy[k] === 'object' ? (policy[k].action || 'warn') : String(policy[k]),
4222
+ }));
4223
+ }
4224
+ }
4225
+
4226
+ // Default mode from policy
4227
+ const mode = (policy && policy.defaultMode) || 'enforce';
4228
+ console.log(chalk.gray(` Policy: ${preset} (${mode} mode)`));
4229
+ console.log(chalk.gray(` Source: ${path.relative(projectDir, policyPath) || policyPath}`));
4230
+ console.log(chalk.gray(` Rules active: ${ruleCount}`));
4231
+ } catch (e) {
4232
+ console.log(chalk.red(` Policy file has invalid YAML: ${e.message}\n`));
4233
+ process.exitCode = 1;
4234
+ return;
4235
+ }
4236
+ } else {
4237
+ console.log(chalk.yellow(' No .delimit/policies.yml found \u2014 using built-in defaults'));
4238
+ console.log(chalk.gray(' Rules active: built-in (12 default change-type rules)'));
4239
+ preset = 'default';
4240
+ ruleCount = 12;
4241
+ }
4242
+
4243
+ console.log('');
4244
+
4245
+ // Show rule details in verbose mode
4246
+ if (opts.verbose && policyRules.length > 0) {
4247
+ console.log(chalk.bold(' Rule Breakdown:'));
4248
+ console.log(chalk.gray(' \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'));
4249
+ for (const rule of policyRules) {
4250
+ const name = rule.name || rule.change_type || '(unnamed)';
4251
+ const action = rule.action || rule.mode || 'warn';
4252
+ const icon = action === 'forbid' || action === 'enforce' || action === 'error'
4253
+ ? chalk.red('\u2717')
4254
+ : action === 'warn' || action === 'advisory' || action === 'guarded'
4255
+ ? chalk.yellow('\u26a0')
4256
+ : chalk.green('\u2713');
4257
+ console.log(` ${icon} ${name} ${chalk.gray(`(${action})`)}`);
4258
+ if (opts.verbose && rule.triggers) {
4259
+ for (const trigger of rule.triggers) {
4260
+ const triggerStr = Object.entries(trigger).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join(', ');
4261
+ console.log(chalk.gray(` trigger: ${triggerStr}`));
4262
+ }
4263
+ }
4264
+ }
4265
+ console.log('');
4266
+ }
4267
+
4268
+ console.log(chalk.bold(' Simulation Results:'));
4269
+ console.log(chalk.gray(' \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'));
4270
+
4271
+ let totalBlocking = 0;
4272
+ let totalWarnings = 0;
4273
+ let totalPassed = 0;
4274
+
4275
+ // --- Mode 1: --spec — simulate lint against a specific spec ---
4276
+ if (opts.spec) {
4277
+ const specPath = path.resolve(opts.spec);
4278
+ if (!fs.existsSync(specPath)) {
4279
+ console.log(chalk.red(`\n File not found: ${specPath}\n`));
4280
+ process.exitCode = 1;
4281
+ return;
4282
+ }
4283
+
4284
+ // Try to find a baseline to compare against
4285
+ const baselinePath = path.join(configDir, 'baseline.yaml');
4286
+ let basePath = null;
4287
+
4288
+ // Check git for the previous version of this spec
4289
+ const relSpec = path.relative(projectDir, specPath);
4290
+ try {
4291
+ const baseContent = execSync(`git show HEAD:${relSpec}`, {
4292
+ cwd: projectDir, encoding: 'utf-8', timeout: 5000
4293
+ });
4294
+ const tmpBase = path.join(os.tmpdir(), `delimit-sim-base-${Date.now()}.yaml`);
4295
+ fs.writeFileSync(tmpBase, baseContent);
4296
+ basePath = tmpBase;
4297
+ } catch {
4298
+ // No git history for this file; try baseline
4299
+ if (fs.existsSync(baselinePath)) {
4300
+ basePath = baselinePath;
4301
+ }
4302
+ }
4303
+
4304
+ if (!basePath) {
4305
+ console.log(chalk.gray(' No baseline found to compare against (new spec or no git history).'));
4306
+ console.log(chalk.green(' \u2713 PASS Spec exists and is parseable'));
4307
+ // Validate that the spec is valid YAML/JSON
4308
+ try {
4309
+ const yaml = require('js-yaml');
4310
+ const content = fs.readFileSync(specPath, 'utf8');
4311
+ const parsed = specPath.endsWith('.json') ? JSON.parse(content) : yaml.load(content);
4312
+ if (parsed && (parsed.openapi || parsed.swagger)) {
4313
+ console.log(chalk.green(` \u2713 PASS Valid OpenAPI ${parsed.openapi || parsed.swagger} spec`));
4314
+ totalPassed += 2;
4315
+ } else {
4316
+ console.log(chalk.yellow(' \u26a0 WARN File parsed but no openapi/swagger version key found'));
4317
+ totalWarnings++;
4318
+ totalPassed++;
4319
+ }
4320
+ } catch (e) {
4321
+ console.log(chalk.red(` \u2717 BLOCK Spec file is not valid YAML/JSON: ${e.message}`));
4322
+ totalBlocking++;
4323
+ }
4324
+ } else {
4325
+ // Run the lint engine in dry-run mode
4326
+ try {
4327
+ const result = apiEngine.lint(basePath, specPath, { policy: preset });
4328
+
4329
+ if (result && result.summary) {
4330
+ const breaking = result.summary.breaking_changes || result.summary.breaking || 0;
4331
+ const warnings = result.summary.warnings || 0;
4332
+ const violations = result.violations || [];
4333
+
4334
+ if (breaking === 0 && warnings === 0) {
4335
+ console.log(chalk.green(' \u2713 PASS No breaking changes detected'));
4336
+ totalPassed++;
4337
+ }
4338
+
4339
+ for (const v of violations) {
4340
+ if (v.severity === 'error') {
4341
+ console.log(chalk.red(` \u2717 BLOCK ${v.message}`));
4342
+ if (v.path) console.log(chalk.gray(` ${v.path}`));
4343
+ totalBlocking++;
4344
+ } else {
4345
+ console.log(chalk.yellow(` \u26a0 WARN ${v.message}`));
4346
+ if (v.path) console.log(chalk.gray(` ${v.path}`));
4347
+ totalWarnings++;
4348
+ }
4349
+ }
4350
+
4351
+ // Show safe changes
4352
+ const safe = (result.all_changes || []).filter(c => !c.is_breaking);
4353
+ if (safe.length > 0) {
4354
+ for (const c of safe) {
4355
+ console.log(chalk.green(` \u2713 PASS ${c.message}`));
4356
+ totalPassed++;
4357
+ }
4358
+ }
4359
+
4360
+ // Semver info
4361
+ if (result.semver && result.semver.bump && result.semver.bump !== 'none') {
4362
+ const bump = result.semver.bump.toUpperCase();
4363
+ console.log(chalk.gray(`\n Semver bump: ${bump}`));
4364
+ }
4365
+ } else {
4366
+ console.log(chalk.green(' \u2713 PASS No breaking changes detected'));
4367
+ totalPassed++;
4368
+ }
4369
+ } catch (err) {
4370
+ console.log(chalk.green(' \u2713 PASS No issues detected'));
4371
+ totalPassed++;
4372
+ } finally {
4373
+ // Clean up temp base file if we created one
4374
+ if (basePath && basePath.startsWith(os.tmpdir())) {
4375
+ try { fs.unlinkSync(basePath); } catch {}
4376
+ }
4377
+ }
4378
+ }
4379
+
4380
+ // --- Mode 2: --commit — simulate pre-commit check on staged changes ---
4381
+ } else if (opts.commit) {
4382
+ let stagedFiles = [];
4383
+ try {
4384
+ const output = execSync('git diff --cached --name-only', {
4385
+ cwd: projectDir, encoding: 'utf-8', timeout: 5000
4386
+ }).trim();
4387
+ if (output) stagedFiles = output.split('\n');
4388
+ } catch {
4389
+ console.log(chalk.red(' \u2717 BLOCK Not a git repository or git not available'));
4390
+ totalBlocking++;
4391
+ }
4392
+
4393
+ if (stagedFiles.length === 0 && totalBlocking === 0) {
4394
+ console.log(chalk.gray(' No staged files. Stage changes with git add first.\n'));
4395
+ return;
4396
+ }
4397
+
4398
+ // Filter to spec files
4399
+ const specExtensions = ['.yaml', '.yml', '.json'];
4400
+ const specKeywords = ['openapi', 'swagger', 'api-spec', 'api_spec'];
4401
+ const specFiles = stagedFiles.filter(f => {
4402
+ const ext = path.extname(f).toLowerCase();
4403
+ const name = path.basename(f).toLowerCase();
4404
+ if (!specExtensions.includes(ext)) return false;
4405
+ if (specKeywords.some(kw => name.includes(kw))) return true;
4406
+ try {
4407
+ const head = fs.readFileSync(path.join(projectDir, f), 'utf-8').slice(0, 512);
4408
+ return head.includes('"openapi"') || head.includes('openapi:') || head.includes('"swagger"') || head.includes('swagger:');
4409
+ } catch { return false; }
4410
+ });
4411
+
4412
+ // Report on staged files
4413
+ console.log(chalk.gray(` Staged files: ${stagedFiles.length} total, ${specFiles.length} API spec(s)`));
4414
+ console.log('');
4415
+
4416
+ if (specFiles.length === 0) {
4417
+ console.log(chalk.green(' \u2713 PASS No API spec changes in staged files'));
4418
+ totalPassed++;
4419
+ } else {
4420
+ for (const specFile of specFiles) {
4421
+ const fullPath = path.join(projectDir, specFile);
4422
+ let baseContent = null;
4423
+ try {
4424
+ baseContent = execSync(`git show HEAD:${specFile}`, {
4425
+ cwd: projectDir, encoding: 'utf-8', timeout: 5000
4426
+ });
4427
+ } catch {
4428
+ console.log(chalk.green(` \u2713 PASS ${specFile} (new file \u2014 no base to compare)`));
4429
+ totalPassed++;
4430
+ continue;
4431
+ }
4432
+
4433
+ const tmpBase = path.join(os.tmpdir(), `delimit-sim-commit-${Date.now()}.yaml`);
4434
+ try {
4435
+ fs.writeFileSync(tmpBase, baseContent);
4436
+ const result = apiEngine.lint(tmpBase, fullPath, { policy: preset });
4437
+
4438
+ if (result && result.summary) {
4439
+ const breaking = result.summary.breaking_changes || result.summary.breaking || 0;
4440
+ const warnings = result.summary.warnings || 0;
4441
+ const violations = result.violations || [];
4442
+
4443
+ if (breaking === 0 && warnings === 0) {
4444
+ console.log(chalk.green(` \u2713 PASS ${specFile} \u2014 no breaking changes`));
4445
+ totalPassed++;
4446
+ }
4447
+
4448
+ for (const v of violations) {
4449
+ if (v.severity === 'error') {
4450
+ console.log(chalk.red(` \u2717 BLOCK ${v.message}`));
4451
+ if (v.path) console.log(chalk.gray(` ${v.path}`));
4452
+ totalBlocking++;
4453
+ } else {
4454
+ console.log(chalk.yellow(` \u26a0 WARN ${v.message}`));
4455
+ if (v.path) console.log(chalk.gray(` ${v.path}`));
4456
+ totalWarnings++;
4457
+ }
4458
+ }
4459
+ } else {
4460
+ console.log(chalk.green(` \u2713 PASS ${specFile} \u2014 no issues`));
4461
+ totalPassed++;
4462
+ }
4463
+ } catch {
4464
+ console.log(chalk.green(` \u2713 PASS ${specFile} \u2014 no issues`));
4465
+ totalPassed++;
4466
+ } finally {
4467
+ try { fs.unlinkSync(tmpBase); } catch {}
4468
+ }
4469
+ }
4470
+ }
4471
+
4472
+ // Check for non-spec governance signals
4473
+ const hasPaymentFiles = stagedFiles.some(f => f.includes('payment') || f.includes('billing') || f.includes('stripe'));
4474
+ if (hasPaymentFiles) {
4475
+ const paymentRule = policyRules.find(r => r.name && r.name.toLowerCase().includes('payment'));
4476
+ if (paymentRule) {
4477
+ const action = paymentRule.mode || paymentRule.action || 'warn';
4478
+ if (action === 'enforce' || action === 'forbid') {
4479
+ console.log(chalk.red(` \u2717 BLOCK Payment code change detected \u2014 "${paymentRule.name}" rule is in ${action} mode`));
4480
+ totalBlocking++;
4481
+ } else {
4482
+ console.log(chalk.yellow(` \u26a0 WARN Payment code change detected \u2014 "${paymentRule.name}" rule (${action} mode)`));
4483
+ totalWarnings++;
4484
+ }
4485
+ }
4486
+ }
4487
+
4488
+ // --- Mode 3: Default — show policy overview and what would happen ---
4489
+ } else {
4490
+ // Find all specs in the project
4491
+ const specPatterns = [
4492
+ 'openapi.yaml', 'openapi.yml', 'openapi.json',
4493
+ 'swagger.yaml', 'swagger.yml', 'swagger.json',
4494
+ 'docs/openapi.yaml', 'docs/openapi.yml', 'docs/openapi.json',
4495
+ 'spec/openapi.yaml', 'spec/openapi.json',
4496
+ 'api/openapi.yaml', 'api/openapi.json',
4497
+ 'contrib/openapi.json',
4498
+ ];
4499
+ const foundSpecs = specPatterns.filter(p => fs.existsSync(path.join(projectDir, p)));
4500
+
4501
+ if (foundSpecs.length > 0) {
4502
+ console.log(chalk.green(` \u2713 PASS API spec(s) found: ${foundSpecs.join(', ')}`));
4503
+ totalPassed++;
4504
+ } else {
4505
+ console.log(chalk.yellow(' \u26a0 WARN No API spec files found in project'));
4506
+ totalWarnings++;
4507
+ }
4508
+
4509
+ // Check git status for uncommitted spec changes
4510
+ try {
4511
+ const output = execSync('git diff --name-only', {
4512
+ cwd: projectDir, encoding: 'utf-8', timeout: 5000
4513
+ }).trim();
4514
+ const stagedOutput = execSync('git diff --cached --name-only', {
4515
+ cwd: projectDir, encoding: 'utf-8', timeout: 5000
4516
+ }).trim();
4517
+
4518
+ const allChanged = [...new Set([
4519
+ ...(output ? output.split('\n') : []),
4520
+ ...(stagedOutput ? stagedOutput.split('\n') : []),
4521
+ ])];
4522
+
4523
+ const specKeywords = ['openapi', 'swagger', 'api-spec', 'api_spec'];
4524
+ const changedSpecs = allChanged.filter(f => {
4525
+ const name = path.basename(f).toLowerCase();
4526
+ return specKeywords.some(kw => name.includes(kw));
4527
+ });
4528
+
4529
+ if (changedSpecs.length > 0) {
4530
+ console.log(chalk.yellow(` \u26a0 WARN ${changedSpecs.length} uncommitted spec change(s): ${changedSpecs.join(', ')}`));
4531
+ console.log(chalk.gray(' Run: delimit simulate --commit to check staged changes'));
4532
+ totalWarnings++;
4533
+ } else {
4534
+ console.log(chalk.green(' \u2713 PASS No uncommitted API spec changes'));
4535
+ totalPassed++;
4536
+ }
4537
+ } catch {
4538
+ console.log(chalk.gray(' \u2500 SKIP Not a git repository'));
4539
+ }
4540
+
4541
+ // Check governance hooks
4542
+ const gitHooksDir = path.join(projectDir, '.git', 'hooks');
4543
+ const preCommitHook = path.join(gitHooksDir, 'pre-commit');
4544
+ if (fs.existsSync(preCommitHook)) {
4545
+ try {
4546
+ const hookContent = fs.readFileSync(preCommitHook, 'utf8');
4547
+ if (hookContent.includes('delimit')) {
4548
+ console.log(chalk.green(' \u2713 PASS Delimit pre-commit hook installed'));
4549
+ totalPassed++;
4550
+ } else {
4551
+ console.log(chalk.yellow(' \u26a0 WARN Pre-commit hook exists but does not reference Delimit'));
4552
+ totalWarnings++;
4553
+ }
4554
+ } catch {
4555
+ console.log(chalk.yellow(' \u26a0 WARN Could not read pre-commit hook'));
4556
+ totalWarnings++;
4557
+ }
4558
+ } else {
4559
+ console.log(chalk.yellow(' \u26a0 WARN No pre-commit hook \u2014 governance only runs manually'));
4560
+ console.log(chalk.gray(' Run: delimit hooks install'));
4561
+ totalWarnings++;
4562
+ }
4563
+
4564
+ // GitHub Action check
4565
+ const workflowDir = path.join(projectDir, '.github', 'workflows');
4566
+ if (fs.existsSync(workflowDir)) {
4567
+ try {
4568
+ const workflows = fs.readdirSync(workflowDir);
4569
+ const hasDelimit = workflows.some(f => {
4570
+ try {
4571
+ const content = fs.readFileSync(path.join(workflowDir, f), 'utf8');
4572
+ return content.includes('delimit-ai/delimit') || content.includes('delimit');
4573
+ } catch { return false; }
4574
+ });
4575
+ if (hasDelimit) {
4576
+ console.log(chalk.green(' \u2713 PASS GitHub Action governance workflow found'));
4577
+ totalPassed++;
4578
+ } else {
4579
+ console.log(chalk.yellow(' \u26a0 WARN No Delimit GitHub Action \u2014 CI governance not enabled'));
4580
+ console.log(chalk.gray(' Run: delimit ci'));
4581
+ totalWarnings++;
4582
+ }
4583
+ } catch {}
4584
+ } else {
4585
+ console.log(chalk.yellow(' \u26a0 WARN No .github/workflows/ directory'));
4586
+ totalWarnings++;
4587
+ }
4588
+ }
4589
+
4590
+ // --- Verdict ---
4591
+ console.log('');
4592
+ if (totalBlocking > 0) {
4593
+ const parts = [];
4594
+ if (totalBlocking > 0) parts.push(`${totalBlocking} blocking`);
4595
+ if (totalWarnings > 0) parts.push(`${totalWarnings} warning(s)`);
4596
+ if (totalPassed > 0) parts.push(`${totalPassed} passed`);
4597
+ console.log(chalk.gray(` Verdict: ${parts.join(', ')}`));
4598
+ console.log(chalk.red.bold(' A real commit would be BLOCKED.\n'));
4599
+ } else if (totalWarnings > 0) {
4600
+ const parts = [];
4601
+ if (totalWarnings > 0) parts.push(`${totalWarnings} warning(s)`);
4602
+ if (totalPassed > 0) parts.push(`${totalPassed} passed`);
4603
+ console.log(chalk.gray(` Verdict: ${parts.join(', ')}`));
4604
+ console.log(chalk.yellow.bold(' A real commit would PASS with warnings.\n'));
4605
+ } else {
4606
+ console.log(chalk.gray(` Verdict: ${totalPassed} passed, 0 warnings, 0 blocking`));
4607
+ console.log(chalk.green.bold(' A real commit would PASS cleanly.\n'));
4608
+ }
4609
+ });
4610
+
4611
+ // Hooks command — install/remove git hooks for governance
4612
+ program
4613
+ .command('hooks <action>')
4614
+ .description('Install or remove git hooks (install | remove | status)')
4615
+ .option('--pre-push', 'Also add pre-push hook')
4616
+ .action(async (action, opts) => {
4617
+ const projectDir = process.cwd();
4618
+ const gitDir = path.join(projectDir, '.git');
4619
+
4620
+ if (!fs.existsSync(gitDir)) {
4621
+ console.log(chalk.red('\n Not a git repository. Run git init first.\n'));
4622
+ process.exitCode = 1;
4623
+ return;
4624
+ }
4625
+
4626
+ const hooksDir = path.join(gitDir, 'hooks');
4627
+ fs.mkdirSync(hooksDir, { recursive: true });
4628
+
4629
+ const preCommitPath = path.join(hooksDir, 'pre-commit');
4630
+ const prePushPath = path.join(hooksDir, 'pre-push');
4631
+ const marker = '# delimit-governance-hook';
3254
4632
 
3255
4633
  const preCommitHook = `#!/bin/sh
3256
4634
  ${marker}
@@ -3712,50 +5090,250 @@ program
3712
5090
  return;
3713
5091
  }
3714
5092
 
3715
- // 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
+
3716
5148
  const decision = result.decision;
3717
5149
  const semver = result.semver;
3718
- const banner = decision === 'fail'
3719
- ? chalk.red.bold('FAIL')
3720
- : decision === 'warn'
3721
- ? chalk.yellow.bold('WARN')
3722
- : 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);
3723
5156
 
3724
- const bump = semver ? ` — ${chalk.bold(semver.bump.toUpperCase())}` : '';
3725
- 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);
3726
5161
 
3727
- 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
+ }
3728
5169
 
3729
- // Summary
3730
- const s = result.summary;
3731
- 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')}`);
3732
5181
  if (s.violations > 0) {
3733
- console.log(` Violations: ${s.errors} error(s), ${s.warnings} warning(s)`);
5182
+ console.log(` Errors: ${s.errors}`);
5183
+ console.log(` Warnings: ${s.warnings}`);
3734
5184
  }
3735
5185
  console.log('');
3736
5186
 
3737
- // Violations
3738
- const violations = result.violations || [];
3739
- if (violations.length > 0) {
3740
- violations.forEach(v => {
3741
- const icon = v.severity === 'error' ? chalk.red('ERR') : chalk.yellow('WRN');
3742
- console.log(` ${icon} ${v.message}`);
3743
- 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}`);
5268
+ });
5269
+ console.log('');
5270
+ }
5271
+
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}`);
3744
5288
  });
5289
+ console.log(c.gray(` ... and ${safe.length - 10} more additions`));
3745
5290
  console.log('');
3746
5291
  }
3747
5292
 
3748
- // Non-breaking changes
3749
- const safe = (result.all_changes || []).filter(c => !c.is_breaking);
3750
- if (safe.length > 0) {
3751
- console.log(chalk.green(' Additions:'));
3752
- safe.forEach(c => console.log(` + ${c.message}`));
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.'));
3753
5326
  console.log('');
3754
5327
  }
3755
5328
 
5329
+ // ── Footer ──
5330
+ console.log(divider);
3756
5331
  if (decision === 'pass') {
3757
- 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'));
3758
5335
  }
5336
+ console.log('');
3759
5337
 
3760
5338
  process.exit(result.exit_code || 0);
3761
5339
  } catch (err) {
@@ -4551,7 +6129,7 @@ program
4551
6129
  .argument("[action]", "Action: status | set | list | reveal", "status")
4552
6130
  .option("--verbose", "Show encryption details and backend status")
4553
6131
  .action(async (action, options) => {
4554
- console.log(chalk.purple.bold("\n🔒 Delimit Vault\n"));
6132
+ console.log(chalk.magenta.bold("\n Delimit Vault\n"));
4555
6133
 
4556
6134
  if (action === "status") {
4557
6135
  console.log(chalk.bold("Backend Status:"));
@@ -4566,13 +6144,70 @@ program
4566
6144
  console.log("\nUse " + chalk.cyan("delimit vault list") + " to see configured secrets.");
4567
6145
  } else if (action === "list") {
4568
6146
  console.log(chalk.bold("Configured Secrets:"));
4569
- // Mock list for now
4570
- const secrets = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "REDDIT_PROXY_URL"];
4571
- secrets.forEach(s => console.log(` • ${s} ${chalk.gray("********")}`));
4572
- console.log("\nRun " + chalk.cyan("delimit vault set <NAME>") + " to update.");
6147
+ const secretsDir = path.join(os.homedir(), '.delimit', 'secrets');
6148
+ if (fs.existsSync(secretsDir)) {
6149
+ const files = fs.readdirSync(secretsDir).filter(f => f.endsWith('.json') && !f.startsWith('.'));
6150
+ if (files.length === 0) {
6151
+ console.log(chalk.dim(" No secrets configured yet."));
6152
+ } else {
6153
+ files.forEach(f => {
6154
+ const name = f.replace('.json', '');
6155
+ console.log(` • ${name} ${chalk.gray("********")}`);
6156
+ });
6157
+ }
6158
+ } else {
6159
+ console.log(chalk.dim(" No secrets directory found."));
6160
+ }
6161
+ console.log("\nRun " + chalk.cyan("delimit vault set <NAME>") + " to add a secret.");
6162
+ } else if (action === "set") {
6163
+ const name = process.argv[4];
6164
+ if (!name) {
6165
+ console.log(chalk.red("Usage: delimit vault set <NAME>"));
6166
+ console.log(chalk.dim(" Example: delimit vault set OPENAI_API_KEY"));
6167
+ process.exit(1);
6168
+ }
6169
+ const secretsDir = path.join(os.homedir(), '.delimit', 'secrets');
6170
+ fs.mkdirSync(secretsDir, { recursive: true });
6171
+ const filePath = path.join(secretsDir, `${name}.json`);
6172
+ const existing = fs.existsSync(filePath);
6173
+ // Read value from stdin or prompt
6174
+ const readline = require('readline');
6175
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6176
+ rl.question(` Enter value for ${chalk.bold(name)}: `, (value) => {
6177
+ rl.close();
6178
+ if (!value || !value.trim()) {
6179
+ console.log(chalk.red(" Empty value. Aborted."));
6180
+ return;
6181
+ }
6182
+ fs.writeFileSync(filePath, JSON.stringify({ key: name, value: value.trim(), updated: new Date().toISOString() }), 'utf-8');
6183
+ fs.chmodSync(filePath, 0o600);
6184
+ console.log(chalk.green(` ${existing ? 'Updated' : 'Saved'}: ${name}`));
6185
+ console.log(chalk.dim(` Location: ${filePath}`));
6186
+ });
6187
+ } else if (action === "reveal") {
6188
+ const name = process.argv[4];
6189
+ if (!name) {
6190
+ console.log(chalk.red("Usage: delimit vault reveal <NAME>"));
6191
+ process.exit(1);
6192
+ }
6193
+ const secretsDir = path.join(os.homedir(), '.delimit', 'secrets');
6194
+ const filePath = path.join(secretsDir, `${name}.json`);
6195
+ if (!fs.existsSync(filePath)) {
6196
+ console.log(chalk.red(` Secret "${name}" not found.`));
6197
+ console.log(chalk.dim(" Run " + chalk.cyan("delimit vault list") + " to see configured secrets."));
6198
+ process.exit(1);
6199
+ }
6200
+ try {
6201
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
6202
+ const val = data.value || data.key || '(empty)';
6203
+ console.log(` ${chalk.bold(name)}: ${val}`);
6204
+ if (data.updated) console.log(chalk.dim(` Updated: ${data.updated}`));
6205
+ } catch {
6206
+ console.log(chalk.red(` Failed to read secret "${name}".`));
6207
+ }
4573
6208
  } else {
4574
- console.log(chalk.yellow(`Action "${action}" is coming soon.`));
4575
- console.log("To configure secrets today, use " + chalk.cyan("delimit setup") + " or edit " + chalk.dim("~/.delimit/secrets/"));
6209
+ console.log(chalk.yellow(`Unknown action: "${action}"`));
6210
+ console.log("Available: " + chalk.cyan("status") + " | " + chalk.cyan("list") + " | " + chalk.cyan("set <NAME>") + " | " + chalk.cyan("reveal <NAME>"));
4576
6211
  }
4577
6212
  console.log("");
4578
6213
  });
@@ -4709,13 +6344,18 @@ function readMemories() {
4709
6344
  function writeMemory(entry) {
4710
6345
  // Write in MCP-compatible format (individual .json files)
4711
6346
  fs.mkdirSync(MEMORY_DIR, { recursive: true });
4712
- const memId = 'mem-' + require('crypto').createHash('sha256').update(entry.text.slice(0, 100)).digest('hex').slice(0, 12);
6347
+ const crypto = require('crypto');
6348
+ const content = entry.text;
6349
+ const memId = 'mem-' + crypto.createHash('sha256').update(content.slice(0, 100)).digest('hex').slice(0, 12);
6350
+ const hash = crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
4713
6351
  const mcpEntry = {
4714
6352
  id: memId,
4715
- content: entry.text,
6353
+ content,
4716
6354
  tags: entry.tags || [],
4717
6355
  context: entry.source || 'cli',
4718
6356
  created_at: entry.created || new Date().toISOString(),
6357
+ hash,
6358
+ source_model: process.env.DELIMIT_MODEL || 'cli',
4719
6359
  };
4720
6360
  fs.writeFileSync(path.join(MEMORY_DIR, `${memId}.json`), JSON.stringify(mcpEntry, null, 2));
4721
6361
  return memId;
@@ -4761,8 +6401,18 @@ function relativeTime(isoDate) {
4761
6401
  return `${diffYear} year${diffYear === 1 ? '' : 's'} ago`;
4762
6402
  }
4763
6403
 
6404
+ function verifyMemoryIntegrity(mem) {
6405
+ if (!mem.hash) return null; // No hash — legacy memory
6406
+ const crypto = require('crypto');
6407
+ const expected = crypto.createHash('sha256').update(mem.content || mem.text || '').digest('hex').slice(0, 16);
6408
+ return expected === mem.hash;
6409
+ }
6410
+
4764
6411
  function displayMemory(mem) {
4765
- console.log(` ${chalk.gray('[' + mem.id + ']')} ${chalk.gray(relativeTime(mem.created))}`);
6412
+ const integrity = verifyMemoryIntegrity(mem);
6413
+ const integrityBadge = integrity === true ? chalk.green(' \u2713') : integrity === false ? chalk.red(' \u2717 tampered') : '';
6414
+ const sourceBadge = mem.source_model ? chalk.gray(` [${mem.source_model}]`) : mem.context ? chalk.gray(` [${mem.context}]`) : '';
6415
+ console.log(` ${chalk.gray('[' + mem.id + ']')} ${chalk.gray(relativeTime(mem.created))}${sourceBadge}${integrityBadge}`);
4766
6416
  console.log(` ${mem.text}`);
4767
6417
  if (mem.tags && mem.tags.length > 0) {
4768
6418
  console.log(` ${chalk.blue(mem.tags.map(t => '#' + t).join(' '))}`);