delimit-cli 4.1.42 → 4.1.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +46 -5
- package/bin/delimit-cli.js +1523 -208
- package/bin/delimit-setup.js +8 -2
- package/gateway/ai/agent_dispatch.py +34 -2
- package/gateway/ai/backends/deploy_bridge.py +167 -12
- package/gateway/ai/content_engine.py +1276 -2
- package/gateway/ai/github_scanner.py +1 -1
- package/gateway/ai/governance.py +58 -0
- package/gateway/ai/key_resolver.py +95 -2
- package/gateway/ai/ledger_manager.py +13 -3
- package/gateway/ai/loop_engine.py +220 -349
- package/gateway/ai/notify.py +1786 -2
- package/gateway/ai/reddit_scanner.py +45 -1
- package/gateway/ai/screen_record.py +1 -1
- package/gateway/ai/secrets_broker.py +5 -1
- package/gateway/ai/social_cache.py +341 -0
- package/gateway/ai/social_daemon.py +312 -18
- package/gateway/ai/supabase_sync.py +190 -2
- package/gateway/ai/tui.py +594 -36
- package/gateway/core/zero_spec/express_extractor.py +2 -2
- package/gateway/core/zero_spec/nestjs_extractor.py +40 -9
- package/gateway/requirements.txt +3 -6
- package/package.json +4 -3
- package/scripts/demo-v420-clean.sh +267 -0
- package/scripts/demo-v420-deliberation.sh +217 -0
- package/scripts/demo-v420.sh +55 -0
- package/scripts/postinstall.js +4 -3
- package/scripts/publish-ci-guard.sh +30 -0
- package/scripts/record-and-upload.sh +132 -0
- package/scripts/release.sh +126 -0
- package/scripts/sync-gateway.sh +100 -0
- package/scripts/youtube-upload.py +141 -0
package/bin/delimit-cli.js
CHANGED
|
@@ -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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
if (
|
|
446
|
-
|
|
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
|
-
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
-
|
|
512
|
-
console.log(chalk.bold('
|
|
513
|
-
console.log(
|
|
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
|
-
|
|
783
|
-
|
|
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,41 +3277,473 @@ program
|
|
|
3090
3277
|
try { fs.rmSync(tmpDir, { recursive: true }); } catch {}
|
|
3091
3278
|
});
|
|
3092
3279
|
|
|
3280
|
+
// Report command — generate local governance reports (v4.20)
|
|
3281
|
+
program
|
|
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');
|
|
3292
|
+
|
|
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);
|
|
3301
|
+
}
|
|
3302
|
+
|
|
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);
|
|
3311
|
+
}
|
|
3312
|
+
|
|
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);
|
|
3319
|
+
try {
|
|
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 {}
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
evidenceEvents.sort((a, b) => a._ts - b._ts);
|
|
3347
|
+
|
|
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
|
+
);
|
|
3357
|
+
|
|
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
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
} catch {}
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
const openLedgerItems = ledgerItems.filter(i => i.status === 'open' || !i.status);
|
|
3382
|
+
|
|
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
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
|
|
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 {}
|
|
3425
|
+
|
|
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 {}
|
|
3432
|
+
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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)}) — 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
|
+
|
|
3093
3701
|
// Doctor command — verify setup is correct
|
|
3094
3702
|
program
|
|
3095
3703
|
.command('doctor')
|
|
3096
3704
|
.description('Verify Delimit setup and diagnose common issues')
|
|
3097
|
-
.
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
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
|
+
.action(async (opts) => {
|
|
3708
|
+
const ciMode = !!opts.ci;
|
|
3709
|
+
const fixMode = !!opts.fix;
|
|
3710
|
+
const homeDir = os.homedir();
|
|
3711
|
+
const delimitHome = path.join(homeDir, '.delimit');
|
|
3712
|
+
const results = []; // { name, status: 'pass'|'warn'|'fail', message, fix? }
|
|
3713
|
+
|
|
3714
|
+
function addResult(name, status, message, fix) {
|
|
3715
|
+
results.push({ name, status, message, fix: fix || null });
|
|
3716
|
+
}
|
|
3102
3717
|
|
|
3103
|
-
// Check
|
|
3718
|
+
// --- Check 1: Policy file ---
|
|
3104
3719
|
const policyPath = path.join(process.cwd(), '.delimit', 'policies.yml');
|
|
3105
3720
|
if (fs.existsSync(policyPath)) {
|
|
3106
|
-
|
|
3107
|
-
ok++;
|
|
3721
|
+
addResult('policy-file', 'pass', '.delimit/policies.yml found');
|
|
3108
3722
|
try {
|
|
3109
|
-
const yaml = require('js-yaml');
|
|
3110
3723
|
const policy = yaml.load(fs.readFileSync(policyPath, 'utf8'));
|
|
3111
3724
|
if (policy && (policy.rules !== undefined || policy.override_defaults !== undefined)) {
|
|
3112
|
-
|
|
3113
|
-
ok++;
|
|
3725
|
+
addResult('policy-valid', 'pass', 'Policy file is valid YAML');
|
|
3114
3726
|
} else {
|
|
3115
|
-
|
|
3116
|
-
warn++;
|
|
3727
|
+
addResult('policy-valid', 'warn', 'Policy file has no rules section — add rules to .delimit/policies.yml');
|
|
3117
3728
|
}
|
|
3118
3729
|
} catch (e) {
|
|
3119
|
-
|
|
3120
|
-
fail++;
|
|
3730
|
+
addResult('policy-valid', 'fail', `Policy file has invalid YAML: ${e.message}`, 'delimit init --force');
|
|
3121
3731
|
}
|
|
3122
3732
|
} else {
|
|
3123
|
-
|
|
3124
|
-
|
|
3733
|
+
addResult('policy-file', 'fail', 'No .delimit/policies.yml', 'delimit init');
|
|
3734
|
+
if (fixMode) {
|
|
3735
|
+
try {
|
|
3736
|
+
execSync('delimit init --dry-run', { stdio: 'pipe', cwd: process.cwd() });
|
|
3737
|
+
// If dry-run works, run real init
|
|
3738
|
+
execSync('delimit init', { stdio: 'pipe', cwd: process.cwd() });
|
|
3739
|
+
addResult('policy-file-fix', 'pass', 'Auto-fixed: ran delimit init');
|
|
3740
|
+
} catch {
|
|
3741
|
+
addResult('policy-file-fix', 'warn', 'Auto-fix failed: run delimit init manually');
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3125
3744
|
}
|
|
3126
3745
|
|
|
3127
|
-
// Check
|
|
3746
|
+
// --- Check 2: OpenAPI spec ---
|
|
3128
3747
|
const specPatterns = [
|
|
3129
3748
|
'openapi.yaml', 'openapi.yml', 'openapi.json',
|
|
3130
3749
|
'swagger.yaml', 'swagger.yml', 'swagger.json',
|
|
@@ -3135,22 +3754,18 @@ program
|
|
|
3135
3754
|
];
|
|
3136
3755
|
const foundSpecs = specPatterns.filter(p => fs.existsSync(path.join(process.cwd(), p)));
|
|
3137
3756
|
if (foundSpecs.length > 0) {
|
|
3138
|
-
|
|
3139
|
-
ok++;
|
|
3757
|
+
addResult('openapi-spec', 'pass', `OpenAPI spec found: ${foundSpecs[0]}`);
|
|
3140
3758
|
} else {
|
|
3141
|
-
// Check for framework (Zero-Spec candidate)
|
|
3142
3759
|
const pkgJson = path.join(process.cwd(), 'package.json');
|
|
3143
3760
|
const reqTxt = path.join(process.cwd(), 'requirements.txt');
|
|
3144
3761
|
if (fs.existsSync(pkgJson) || fs.existsSync(reqTxt)) {
|
|
3145
|
-
|
|
3146
|
-
warn++;
|
|
3762
|
+
addResult('openapi-spec', 'warn', 'No OpenAPI spec file — Zero-Spec Mode may work if this is a FastAPI/NestJS/Express project');
|
|
3147
3763
|
} else {
|
|
3148
|
-
|
|
3149
|
-
fail++;
|
|
3764
|
+
addResult('openapi-spec', 'fail', 'No OpenAPI spec file found', 'Create openapi.yaml in project root or run: delimit scan');
|
|
3150
3765
|
}
|
|
3151
3766
|
}
|
|
3152
3767
|
|
|
3153
|
-
// Check
|
|
3768
|
+
// --- Check 3: GitHub workflow ---
|
|
3154
3769
|
const workflowDir = path.join(process.cwd(), '.github', 'workflows');
|
|
3155
3770
|
if (fs.existsSync(workflowDir)) {
|
|
3156
3771
|
const workflows = fs.readdirSync(workflowDir);
|
|
@@ -3161,26 +3776,197 @@ program
|
|
|
3161
3776
|
} catch { return false; }
|
|
3162
3777
|
});
|
|
3163
3778
|
if (hasDelimit) {
|
|
3164
|
-
|
|
3165
|
-
ok++;
|
|
3779
|
+
addResult('github-action', 'pass', 'GitHub Action workflow found');
|
|
3166
3780
|
} else {
|
|
3167
|
-
|
|
3168
|
-
warn++;
|
|
3781
|
+
addResult('github-action', 'warn', 'No Delimit GitHub Action workflow', 'delimit init');
|
|
3169
3782
|
}
|
|
3170
3783
|
} else {
|
|
3171
|
-
|
|
3172
|
-
warn++;
|
|
3784
|
+
addResult('github-action', 'warn', 'No .github/workflows/ directory', 'mkdir -p .github/workflows && delimit init');
|
|
3173
3785
|
}
|
|
3174
3786
|
|
|
3175
|
-
// Check
|
|
3787
|
+
// --- Check 4: Git repository ---
|
|
3176
3788
|
try {
|
|
3177
|
-
const { execSync } = require('child_process');
|
|
3178
3789
|
execSync('git rev-parse --git-dir', { stdio: 'pipe' });
|
|
3179
|
-
|
|
3180
|
-
|
|
3790
|
+
addResult('git-repo', 'pass', 'Git repository detected');
|
|
3791
|
+
} catch {
|
|
3792
|
+
addResult('git-repo', 'warn', 'Not a git repository', 'git init');
|
|
3793
|
+
}
|
|
3794
|
+
|
|
3795
|
+
// --- Check 5: Node.js version ---
|
|
3796
|
+
const nodeVersion = parseInt(process.versions.node.split('.')[0], 10);
|
|
3797
|
+
if (nodeVersion >= 18) {
|
|
3798
|
+
addResult('node-version', 'pass', `Node.js v${process.versions.node}`);
|
|
3799
|
+
} else {
|
|
3800
|
+
addResult('node-version', 'warn', `Node.js v${process.versions.node} — v18+ recommended`, 'nvm install 18 && nvm use 18');
|
|
3801
|
+
}
|
|
3802
|
+
|
|
3803
|
+
// --- Check 6: Python availability ---
|
|
3804
|
+
try {
|
|
3805
|
+
const pyVersion = execSync('python3 --version', { stdio: 'pipe' }).toString().trim();
|
|
3806
|
+
addResult('python', 'pass', `${pyVersion} available (needed for MCP server)`);
|
|
3181
3807
|
} catch {
|
|
3182
|
-
|
|
3183
|
-
|
|
3808
|
+
addResult('python', 'fail', 'python3 not found on PATH — required for MCP server', 'Install Python 3: https://python.org/downloads/');
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3811
|
+
// --- Check 7: MCP server connectivity ---
|
|
3812
|
+
const mcpJsonPath = path.join(homeDir, '.mcp.json');
|
|
3813
|
+
const mcpServerPath = path.join(delimitHome, 'server', 'ai', 'server.py');
|
|
3814
|
+
if (fs.existsSync(mcpJsonPath)) {
|
|
3815
|
+
try {
|
|
3816
|
+
const mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'));
|
|
3817
|
+
const hasDelimitMcp = mcpConfig.mcpServers && mcpConfig.mcpServers.delimit;
|
|
3818
|
+
if (hasDelimitMcp) {
|
|
3819
|
+
addResult('mcp-config', 'pass', 'Delimit configured in ~/.mcp.json');
|
|
3820
|
+
} else {
|
|
3821
|
+
addResult('mcp-config', 'warn', 'Delimit not configured in ~/.mcp.json', 'delimit setup --all');
|
|
3822
|
+
}
|
|
3823
|
+
} catch {
|
|
3824
|
+
addResult('mcp-config', 'warn', '~/.mcp.json exists but failed to parse', 'Check ~/.mcp.json for valid JSON');
|
|
3825
|
+
}
|
|
3826
|
+
} else {
|
|
3827
|
+
addResult('mcp-config', 'warn', 'No ~/.mcp.json found', 'delimit setup --all');
|
|
3828
|
+
}
|
|
3829
|
+
if (fs.existsSync(mcpServerPath)) {
|
|
3830
|
+
addResult('mcp-server', 'pass', 'MCP server file exists at ~/.delimit/server/ai/server.py');
|
|
3831
|
+
} else {
|
|
3832
|
+
addResult('mcp-server', 'fail', 'MCP server not installed at ~/.delimit/server/ai/server.py', 'delimit setup --all');
|
|
3833
|
+
if (fixMode) {
|
|
3834
|
+
try {
|
|
3835
|
+
execSync('delimit setup --all', { stdio: 'pipe' });
|
|
3836
|
+
addResult('mcp-server-fix', 'pass', 'Auto-fixed: ran delimit setup --all');
|
|
3837
|
+
} catch {
|
|
3838
|
+
addResult('mcp-server-fix', 'warn', 'Auto-fix failed: run delimit setup --all manually');
|
|
3839
|
+
}
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
|
|
3843
|
+
// --- Check 8: Memory health ---
|
|
3844
|
+
const memoryDir = path.join(delimitHome, 'memory');
|
|
3845
|
+
if (fs.existsSync(memoryDir)) {
|
|
3846
|
+
let memoryCount = 0;
|
|
3847
|
+
try {
|
|
3848
|
+
const memFiles = fs.readdirSync(memoryDir).filter(f => f.endsWith('.jsonl'));
|
|
3849
|
+
for (const mf of memFiles) {
|
|
3850
|
+
const content = fs.readFileSync(path.join(memoryDir, mf), 'utf8');
|
|
3851
|
+
memoryCount += content.split('\n').filter(l => l.trim()).length;
|
|
3852
|
+
}
|
|
3853
|
+
} catch {}
|
|
3854
|
+
if (memoryCount > 1000) {
|
|
3855
|
+
addResult('memory-health', 'warn', `Memory store has ${memoryCount} entries (>1000) — consider pruning`, 'delimit memory --prune');
|
|
3856
|
+
} else {
|
|
3857
|
+
addResult('memory-health', 'pass', `Memory store: ${memoryCount} entries`);
|
|
3858
|
+
}
|
|
3859
|
+
} else {
|
|
3860
|
+
addResult('memory-health', 'warn', 'No ~/.delimit/memory/ directory', `mkdir -p ${memoryDir}`);
|
|
3861
|
+
if (fixMode) {
|
|
3862
|
+
try {
|
|
3863
|
+
fs.mkdirSync(memoryDir, { recursive: true });
|
|
3864
|
+
addResult('memory-health-fix', 'pass', 'Auto-fixed: created ~/.delimit/memory/');
|
|
3865
|
+
} catch {
|
|
3866
|
+
addResult('memory-health-fix', 'warn', `Auto-fix failed: run mkdir -p ${memoryDir}`);
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
// --- Check 9: Models configured ---
|
|
3872
|
+
const modelsPath = path.join(delimitHome, 'models.json');
|
|
3873
|
+
if (fs.existsSync(modelsPath)) {
|
|
3874
|
+
try {
|
|
3875
|
+
const models = JSON.parse(fs.readFileSync(modelsPath, 'utf8'));
|
|
3876
|
+
const configured = Array.isArray(models)
|
|
3877
|
+
? models.filter(m => m.api_key)
|
|
3878
|
+
: Object.values(models).filter(m => m && m.api_key);
|
|
3879
|
+
if (configured.length > 0) {
|
|
3880
|
+
addResult('models', 'pass', `${configured.length} model(s) configured with API keys`);
|
|
3881
|
+
} else {
|
|
3882
|
+
addResult('models', 'warn', 'models.json exists but no models have api_key set', 'Edit ~/.delimit/models.json and add your API keys');
|
|
3883
|
+
}
|
|
3884
|
+
} catch {
|
|
3885
|
+
addResult('models', 'warn', '~/.delimit/models.json exists but failed to parse', 'Check ~/.delimit/models.json for valid JSON');
|
|
3886
|
+
}
|
|
3887
|
+
} else {
|
|
3888
|
+
addResult('models', 'warn', 'No ~/.delimit/models.json — multi-model features unavailable', 'delimit setup --all');
|
|
3889
|
+
}
|
|
3890
|
+
|
|
3891
|
+
// --- Check 10: License status ---
|
|
3892
|
+
const licensePath = path.join(delimitHome, 'license.json');
|
|
3893
|
+
if (fs.existsSync(licensePath)) {
|
|
3894
|
+
try {
|
|
3895
|
+
const license = JSON.parse(fs.readFileSync(licensePath, 'utf8'));
|
|
3896
|
+
const tier = license.tier || license.plan || 'Unknown';
|
|
3897
|
+
addResult('license', 'pass', `License: ${tier}`);
|
|
3898
|
+
} catch {
|
|
3899
|
+
addResult('license', 'warn', '~/.delimit/license.json exists but failed to parse', 'Check ~/.delimit/license.json for valid JSON');
|
|
3900
|
+
}
|
|
3901
|
+
} else {
|
|
3902
|
+
addResult('license', 'pass', 'License: Free tier (upgrade at delimit.ai/pricing)');
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3905
|
+
// --- Check 11: Cross-model hooks ---
|
|
3906
|
+
const claudeSettingsPath = path.join(process.cwd(), '.claude', 'settings.json');
|
|
3907
|
+
if (fs.existsSync(claudeSettingsPath)) {
|
|
3908
|
+
try {
|
|
3909
|
+
const settings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf8'));
|
|
3910
|
+
const hasHooks = settings.hooks && settings.hooks.PostToolUse;
|
|
3911
|
+
if (hasHooks) {
|
|
3912
|
+
addResult('cross-model-hooks', 'pass', 'Claude Code PostToolUse hooks installed');
|
|
3913
|
+
} else {
|
|
3914
|
+
addResult('cross-model-hooks', 'warn', 'Claude Code hooks not configured in .claude/settings.json', 'delimit hooks install');
|
|
3915
|
+
}
|
|
3916
|
+
} catch {
|
|
3917
|
+
addResult('cross-model-hooks', 'warn', '.claude/settings.json exists but failed to parse', 'Check .claude/settings.json for valid JSON');
|
|
3918
|
+
}
|
|
3919
|
+
} else {
|
|
3920
|
+
addResult('cross-model-hooks', 'warn', 'No .claude/settings.json — cross-model hooks not installed', 'delimit hooks install');
|
|
3921
|
+
}
|
|
3922
|
+
|
|
3923
|
+
// --- Check 12: Disk space ---
|
|
3924
|
+
if (fs.existsSync(delimitHome)) {
|
|
3925
|
+
try {
|
|
3926
|
+
const duOutput = execSync(`du -sm "${delimitHome}"`, { stdio: 'pipe' }).toString().trim();
|
|
3927
|
+
const sizeMb = parseInt(duOutput.split('\t')[0], 10);
|
|
3928
|
+
if (sizeMb > 500) {
|
|
3929
|
+
addResult('disk-space', 'warn', `~/.delimit/ is ${sizeMb}MB (>500MB) — consider cleanup`, `du -sh ~/.delimit/*/`);
|
|
3930
|
+
} else {
|
|
3931
|
+
addResult('disk-space', 'pass', `~/.delimit/ disk usage: ${sizeMb}MB`);
|
|
3932
|
+
}
|
|
3933
|
+
} catch {
|
|
3934
|
+
addResult('disk-space', 'pass', '~/.delimit/ disk usage: unknown (du not available)');
|
|
3935
|
+
}
|
|
3936
|
+
} else {
|
|
3937
|
+
addResult('disk-space', 'pass', '~/.delimit/ does not exist yet');
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3940
|
+
// --- CI mode: output JSON and exit ---
|
|
3941
|
+
if (ciMode) {
|
|
3942
|
+
const ok = results.filter(r => r.status === 'pass').length;
|
|
3943
|
+
const warn = results.filter(r => r.status === 'warn').length;
|
|
3944
|
+
const fail = results.filter(r => r.status === 'fail').length;
|
|
3945
|
+
const total = results.length;
|
|
3946
|
+
const score = total > 0 ? Math.round((ok / total) * 10) : 0;
|
|
3947
|
+
const output = {
|
|
3948
|
+
version: '4.20',
|
|
3949
|
+
health_score: `${score}/10`,
|
|
3950
|
+
summary: { pass: ok, warn, fail, total },
|
|
3951
|
+
checks: results,
|
|
3952
|
+
};
|
|
3953
|
+
console.log(JSON.stringify(output, null, 2));
|
|
3954
|
+
if (fail > 0) {
|
|
3955
|
+
process.exitCode = 1;
|
|
3956
|
+
}
|
|
3957
|
+
return;
|
|
3958
|
+
}
|
|
3959
|
+
|
|
3960
|
+
// --- Human-readable output ---
|
|
3961
|
+
console.log(chalk.bold('\n Delimit Doctor v4.20\n'));
|
|
3962
|
+
|
|
3963
|
+
const icons = { pass: chalk.green(' ✓'), warn: chalk.yellow(' ⚠'), fail: chalk.red(' ✗') };
|
|
3964
|
+
const colors = { pass: chalk.green, warn: chalk.yellow, fail: chalk.red };
|
|
3965
|
+
for (const r of results) {
|
|
3966
|
+
console.log(`${icons[r.status]} ${colors[r.status](r.message)}`);
|
|
3967
|
+
if (r.fix && r.status !== 'pass') {
|
|
3968
|
+
console.log(chalk.gray(` Run: ${r.fix}`));
|
|
3969
|
+
}
|
|
3184
3970
|
}
|
|
3185
3971
|
|
|
3186
3972
|
// Preview what init would create (LED-265)
|
|
@@ -3219,7 +4005,14 @@ program
|
|
|
3219
4005
|
console.log(chalk.gray(' rm -rf .delimit — remove all Delimit files'));
|
|
3220
4006
|
console.log(chalk.gray(' delimit uninstall --dry-run — preview MCP removal\n'));
|
|
3221
4007
|
|
|
3222
|
-
//
|
|
4008
|
+
// Health score and summary
|
|
4009
|
+
const ok = results.filter(r => r.status === 'pass').length;
|
|
4010
|
+
const warn = results.filter(r => r.status === 'warn').length;
|
|
4011
|
+
const fail = results.filter(r => r.status === 'fail').length;
|
|
4012
|
+
const total = results.length;
|
|
4013
|
+
const score = total > 0 ? Math.round((ok / total) * 10) : 0;
|
|
4014
|
+
|
|
4015
|
+
console.log(chalk.bold(` Health: ${score}/10`));
|
|
3223
4016
|
console.log('');
|
|
3224
4017
|
if (fail === 0 && warn === 0) {
|
|
3225
4018
|
console.log(chalk.green.bold(' All checks passed! Ready to lint.\n'));
|
|
@@ -3228,6 +4021,456 @@ program
|
|
|
3228
4021
|
} else {
|
|
3229
4022
|
console.log(chalk.red.bold(` ${ok} passed, ${warn} warning(s), ${fail} error(s). Fix errors above.\n`));
|
|
3230
4023
|
}
|
|
4024
|
+
|
|
4025
|
+
if (fail > 0) {
|
|
4026
|
+
process.exitCode = 1;
|
|
4027
|
+
}
|
|
4028
|
+
|
|
4029
|
+
// Occasional star nudge (show ~20% of the time on success)
|
|
4030
|
+
if (fail === 0 && Math.random() < 0.2) {
|
|
4031
|
+
console.log(chalk.gray(' Useful? Star us: https://github.com/delimit-ai/delimit-mcp-server\n'));
|
|
4032
|
+
}
|
|
4033
|
+
});
|
|
4034
|
+
|
|
4035
|
+
// Simulate command — dry-run governance preview ("terraform plan" for API governance)
|
|
4036
|
+
program
|
|
4037
|
+
.command('simulate')
|
|
4038
|
+
.description('Show what governance would block or allow without making changes')
|
|
4039
|
+
.option('--spec <path>', 'Path to OpenAPI spec to simulate lint against')
|
|
4040
|
+
.option('--policy <path>', 'Path to policies.yml (default: .delimit/policies.yml)')
|
|
4041
|
+
.option('--commit', 'Simulate a pre-commit governance check on staged changes')
|
|
4042
|
+
.option('--verbose', 'Show detailed rule breakdown')
|
|
4043
|
+
.action(async (opts) => {
|
|
4044
|
+
const projectDir = process.cwd();
|
|
4045
|
+
const configDir = path.join(projectDir, '.delimit');
|
|
4046
|
+
const policyPath = opts.policy
|
|
4047
|
+
? path.resolve(opts.policy)
|
|
4048
|
+
: path.join(configDir, 'policies.yml');
|
|
4049
|
+
|
|
4050
|
+
console.log(chalk.bold('\n Delimit Simulate \u2014 Dry Run\n'));
|
|
4051
|
+
|
|
4052
|
+
// Load and parse policy
|
|
4053
|
+
let policy = null;
|
|
4054
|
+
let preset = 'default';
|
|
4055
|
+
let ruleCount = 0;
|
|
4056
|
+
let policyRules = [];
|
|
4057
|
+
|
|
4058
|
+
if (fs.existsSync(policyPath)) {
|
|
4059
|
+
try {
|
|
4060
|
+
const yaml = require('js-yaml');
|
|
4061
|
+
policy = yaml.load(fs.readFileSync(policyPath, 'utf8'));
|
|
4062
|
+
|
|
4063
|
+
// Detect preset from content
|
|
4064
|
+
const policyContent = fs.readFileSync(policyPath, 'utf-8');
|
|
4065
|
+
if (policyContent.includes('action: forbid') && !policyContent.includes('action: warn')) preset = 'strict';
|
|
4066
|
+
else if (!policyContent.includes('action: forbid') && policyContent.includes('action: warn')) preset = 'relaxed';
|
|
4067
|
+
|
|
4068
|
+
// Count rules from various policy formats
|
|
4069
|
+
if (policy && policy.rules && Array.isArray(policy.rules)) {
|
|
4070
|
+
policyRules = policy.rules;
|
|
4071
|
+
ruleCount = policyRules.length;
|
|
4072
|
+
} else if (policy && policy.override_defaults && Array.isArray(policy.override_defaults)) {
|
|
4073
|
+
policyRules = policy.override_defaults;
|
|
4074
|
+
ruleCount = policyRules.length;
|
|
4075
|
+
}
|
|
4076
|
+
|
|
4077
|
+
// Also count top-level change-type keys as implicit rules
|
|
4078
|
+
if (policy) {
|
|
4079
|
+
const changeTypeKeys = Object.keys(policy).filter(k =>
|
|
4080
|
+
!['rules', 'override_defaults', 'defaultMode', 'overrides', 'version', 'preset'].includes(k)
|
|
4081
|
+
);
|
|
4082
|
+
if (changeTypeKeys.length > 0 && ruleCount === 0) {
|
|
4083
|
+
ruleCount = changeTypeKeys.length;
|
|
4084
|
+
policyRules = changeTypeKeys.map(k => ({
|
|
4085
|
+
name: k,
|
|
4086
|
+
action: typeof policy[k] === 'object' ? (policy[k].action || 'warn') : String(policy[k]),
|
|
4087
|
+
}));
|
|
4088
|
+
}
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
// Default mode from policy
|
|
4092
|
+
const mode = (policy && policy.defaultMode) || 'enforce';
|
|
4093
|
+
console.log(chalk.gray(` Policy: ${preset} (${mode} mode)`));
|
|
4094
|
+
console.log(chalk.gray(` Source: ${path.relative(projectDir, policyPath) || policyPath}`));
|
|
4095
|
+
console.log(chalk.gray(` Rules active: ${ruleCount}`));
|
|
4096
|
+
} catch (e) {
|
|
4097
|
+
console.log(chalk.red(` Policy file has invalid YAML: ${e.message}\n`));
|
|
4098
|
+
process.exitCode = 1;
|
|
4099
|
+
return;
|
|
4100
|
+
}
|
|
4101
|
+
} else {
|
|
4102
|
+
console.log(chalk.yellow(' No .delimit/policies.yml found \u2014 using built-in defaults'));
|
|
4103
|
+
console.log(chalk.gray(' Rules active: built-in (12 default change-type rules)'));
|
|
4104
|
+
preset = 'default';
|
|
4105
|
+
ruleCount = 12;
|
|
4106
|
+
}
|
|
4107
|
+
|
|
4108
|
+
console.log('');
|
|
4109
|
+
|
|
4110
|
+
// Show rule details in verbose mode
|
|
4111
|
+
if (opts.verbose && policyRules.length > 0) {
|
|
4112
|
+
console.log(chalk.bold(' Rule Breakdown:'));
|
|
4113
|
+
console.log(chalk.gray(' \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'));
|
|
4114
|
+
for (const rule of policyRules) {
|
|
4115
|
+
const name = rule.name || rule.change_type || '(unnamed)';
|
|
4116
|
+
const action = rule.action || rule.mode || 'warn';
|
|
4117
|
+
const icon = action === 'forbid' || action === 'enforce' || action === 'error'
|
|
4118
|
+
? chalk.red('\u2717')
|
|
4119
|
+
: action === 'warn' || action === 'advisory' || action === 'guarded'
|
|
4120
|
+
? chalk.yellow('\u26a0')
|
|
4121
|
+
: chalk.green('\u2713');
|
|
4122
|
+
console.log(` ${icon} ${name} ${chalk.gray(`(${action})`)}`);
|
|
4123
|
+
if (opts.verbose && rule.triggers) {
|
|
4124
|
+
for (const trigger of rule.triggers) {
|
|
4125
|
+
const triggerStr = Object.entries(trigger).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join(', ');
|
|
4126
|
+
console.log(chalk.gray(` trigger: ${triggerStr}`));
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
console.log('');
|
|
4131
|
+
}
|
|
4132
|
+
|
|
4133
|
+
console.log(chalk.bold(' Simulation Results:'));
|
|
4134
|
+
console.log(chalk.gray(' \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'));
|
|
4135
|
+
|
|
4136
|
+
let totalBlocking = 0;
|
|
4137
|
+
let totalWarnings = 0;
|
|
4138
|
+
let totalPassed = 0;
|
|
4139
|
+
|
|
4140
|
+
// --- Mode 1: --spec — simulate lint against a specific spec ---
|
|
4141
|
+
if (opts.spec) {
|
|
4142
|
+
const specPath = path.resolve(opts.spec);
|
|
4143
|
+
if (!fs.existsSync(specPath)) {
|
|
4144
|
+
console.log(chalk.red(`\n File not found: ${specPath}\n`));
|
|
4145
|
+
process.exitCode = 1;
|
|
4146
|
+
return;
|
|
4147
|
+
}
|
|
4148
|
+
|
|
4149
|
+
// Try to find a baseline to compare against
|
|
4150
|
+
const baselinePath = path.join(configDir, 'baseline.yaml');
|
|
4151
|
+
let basePath = null;
|
|
4152
|
+
|
|
4153
|
+
// Check git for the previous version of this spec
|
|
4154
|
+
const relSpec = path.relative(projectDir, specPath);
|
|
4155
|
+
try {
|
|
4156
|
+
const baseContent = execSync(`git show HEAD:${relSpec}`, {
|
|
4157
|
+
cwd: projectDir, encoding: 'utf-8', timeout: 5000
|
|
4158
|
+
});
|
|
4159
|
+
const tmpBase = path.join(os.tmpdir(), `delimit-sim-base-${Date.now()}.yaml`);
|
|
4160
|
+
fs.writeFileSync(tmpBase, baseContent);
|
|
4161
|
+
basePath = tmpBase;
|
|
4162
|
+
} catch {
|
|
4163
|
+
// No git history for this file; try baseline
|
|
4164
|
+
if (fs.existsSync(baselinePath)) {
|
|
4165
|
+
basePath = baselinePath;
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4168
|
+
|
|
4169
|
+
if (!basePath) {
|
|
4170
|
+
console.log(chalk.gray(' No baseline found to compare against (new spec or no git history).'));
|
|
4171
|
+
console.log(chalk.green(' \u2713 PASS Spec exists and is parseable'));
|
|
4172
|
+
// Validate that the spec is valid YAML/JSON
|
|
4173
|
+
try {
|
|
4174
|
+
const yaml = require('js-yaml');
|
|
4175
|
+
const content = fs.readFileSync(specPath, 'utf8');
|
|
4176
|
+
const parsed = specPath.endsWith('.json') ? JSON.parse(content) : yaml.load(content);
|
|
4177
|
+
if (parsed && (parsed.openapi || parsed.swagger)) {
|
|
4178
|
+
console.log(chalk.green(` \u2713 PASS Valid OpenAPI ${parsed.openapi || parsed.swagger} spec`));
|
|
4179
|
+
totalPassed += 2;
|
|
4180
|
+
} else {
|
|
4181
|
+
console.log(chalk.yellow(' \u26a0 WARN File parsed but no openapi/swagger version key found'));
|
|
4182
|
+
totalWarnings++;
|
|
4183
|
+
totalPassed++;
|
|
4184
|
+
}
|
|
4185
|
+
} catch (e) {
|
|
4186
|
+
console.log(chalk.red(` \u2717 BLOCK Spec file is not valid YAML/JSON: ${e.message}`));
|
|
4187
|
+
totalBlocking++;
|
|
4188
|
+
}
|
|
4189
|
+
} else {
|
|
4190
|
+
// Run the lint engine in dry-run mode
|
|
4191
|
+
try {
|
|
4192
|
+
const result = apiEngine.lint(basePath, specPath, { policy: preset });
|
|
4193
|
+
|
|
4194
|
+
if (result && result.summary) {
|
|
4195
|
+
const breaking = result.summary.breaking_changes || result.summary.breaking || 0;
|
|
4196
|
+
const warnings = result.summary.warnings || 0;
|
|
4197
|
+
const violations = result.violations || [];
|
|
4198
|
+
|
|
4199
|
+
if (breaking === 0 && warnings === 0) {
|
|
4200
|
+
console.log(chalk.green(' \u2713 PASS No breaking changes detected'));
|
|
4201
|
+
totalPassed++;
|
|
4202
|
+
}
|
|
4203
|
+
|
|
4204
|
+
for (const v of violations) {
|
|
4205
|
+
if (v.severity === 'error') {
|
|
4206
|
+
console.log(chalk.red(` \u2717 BLOCK ${v.message}`));
|
|
4207
|
+
if (v.path) console.log(chalk.gray(` ${v.path}`));
|
|
4208
|
+
totalBlocking++;
|
|
4209
|
+
} else {
|
|
4210
|
+
console.log(chalk.yellow(` \u26a0 WARN ${v.message}`));
|
|
4211
|
+
if (v.path) console.log(chalk.gray(` ${v.path}`));
|
|
4212
|
+
totalWarnings++;
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4216
|
+
// Show safe changes
|
|
4217
|
+
const safe = (result.all_changes || []).filter(c => !c.is_breaking);
|
|
4218
|
+
if (safe.length > 0) {
|
|
4219
|
+
for (const c of safe) {
|
|
4220
|
+
console.log(chalk.green(` \u2713 PASS ${c.message}`));
|
|
4221
|
+
totalPassed++;
|
|
4222
|
+
}
|
|
4223
|
+
}
|
|
4224
|
+
|
|
4225
|
+
// Semver info
|
|
4226
|
+
if (result.semver && result.semver.bump && result.semver.bump !== 'none') {
|
|
4227
|
+
const bump = result.semver.bump.toUpperCase();
|
|
4228
|
+
console.log(chalk.gray(`\n Semver bump: ${bump}`));
|
|
4229
|
+
}
|
|
4230
|
+
} else {
|
|
4231
|
+
console.log(chalk.green(' \u2713 PASS No breaking changes detected'));
|
|
4232
|
+
totalPassed++;
|
|
4233
|
+
}
|
|
4234
|
+
} catch (err) {
|
|
4235
|
+
console.log(chalk.green(' \u2713 PASS No issues detected'));
|
|
4236
|
+
totalPassed++;
|
|
4237
|
+
} finally {
|
|
4238
|
+
// Clean up temp base file if we created one
|
|
4239
|
+
if (basePath && basePath.startsWith(os.tmpdir())) {
|
|
4240
|
+
try { fs.unlinkSync(basePath); } catch {}
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
4244
|
+
|
|
4245
|
+
// --- Mode 2: --commit — simulate pre-commit check on staged changes ---
|
|
4246
|
+
} else if (opts.commit) {
|
|
4247
|
+
let stagedFiles = [];
|
|
4248
|
+
try {
|
|
4249
|
+
const output = execSync('git diff --cached --name-only', {
|
|
4250
|
+
cwd: projectDir, encoding: 'utf-8', timeout: 5000
|
|
4251
|
+
}).trim();
|
|
4252
|
+
if (output) stagedFiles = output.split('\n');
|
|
4253
|
+
} catch {
|
|
4254
|
+
console.log(chalk.red(' \u2717 BLOCK Not a git repository or git not available'));
|
|
4255
|
+
totalBlocking++;
|
|
4256
|
+
}
|
|
4257
|
+
|
|
4258
|
+
if (stagedFiles.length === 0 && totalBlocking === 0) {
|
|
4259
|
+
console.log(chalk.gray(' No staged files. Stage changes with git add first.\n'));
|
|
4260
|
+
return;
|
|
4261
|
+
}
|
|
4262
|
+
|
|
4263
|
+
// Filter to spec files
|
|
4264
|
+
const specExtensions = ['.yaml', '.yml', '.json'];
|
|
4265
|
+
const specKeywords = ['openapi', 'swagger', 'api-spec', 'api_spec'];
|
|
4266
|
+
const specFiles = stagedFiles.filter(f => {
|
|
4267
|
+
const ext = path.extname(f).toLowerCase();
|
|
4268
|
+
const name = path.basename(f).toLowerCase();
|
|
4269
|
+
if (!specExtensions.includes(ext)) return false;
|
|
4270
|
+
if (specKeywords.some(kw => name.includes(kw))) return true;
|
|
4271
|
+
try {
|
|
4272
|
+
const head = fs.readFileSync(path.join(projectDir, f), 'utf-8').slice(0, 512);
|
|
4273
|
+
return head.includes('"openapi"') || head.includes('openapi:') || head.includes('"swagger"') || head.includes('swagger:');
|
|
4274
|
+
} catch { return false; }
|
|
4275
|
+
});
|
|
4276
|
+
|
|
4277
|
+
// Report on staged files
|
|
4278
|
+
console.log(chalk.gray(` Staged files: ${stagedFiles.length} total, ${specFiles.length} API spec(s)`));
|
|
4279
|
+
console.log('');
|
|
4280
|
+
|
|
4281
|
+
if (specFiles.length === 0) {
|
|
4282
|
+
console.log(chalk.green(' \u2713 PASS No API spec changes in staged files'));
|
|
4283
|
+
totalPassed++;
|
|
4284
|
+
} else {
|
|
4285
|
+
for (const specFile of specFiles) {
|
|
4286
|
+
const fullPath = path.join(projectDir, specFile);
|
|
4287
|
+
let baseContent = null;
|
|
4288
|
+
try {
|
|
4289
|
+
baseContent = execSync(`git show HEAD:${specFile}`, {
|
|
4290
|
+
cwd: projectDir, encoding: 'utf-8', timeout: 5000
|
|
4291
|
+
});
|
|
4292
|
+
} catch {
|
|
4293
|
+
console.log(chalk.green(` \u2713 PASS ${specFile} (new file \u2014 no base to compare)`));
|
|
4294
|
+
totalPassed++;
|
|
4295
|
+
continue;
|
|
4296
|
+
}
|
|
4297
|
+
|
|
4298
|
+
const tmpBase = path.join(os.tmpdir(), `delimit-sim-commit-${Date.now()}.yaml`);
|
|
4299
|
+
try {
|
|
4300
|
+
fs.writeFileSync(tmpBase, baseContent);
|
|
4301
|
+
const result = apiEngine.lint(tmpBase, fullPath, { policy: preset });
|
|
4302
|
+
|
|
4303
|
+
if (result && result.summary) {
|
|
4304
|
+
const breaking = result.summary.breaking_changes || result.summary.breaking || 0;
|
|
4305
|
+
const warnings = result.summary.warnings || 0;
|
|
4306
|
+
const violations = result.violations || [];
|
|
4307
|
+
|
|
4308
|
+
if (breaking === 0 && warnings === 0) {
|
|
4309
|
+
console.log(chalk.green(` \u2713 PASS ${specFile} \u2014 no breaking changes`));
|
|
4310
|
+
totalPassed++;
|
|
4311
|
+
}
|
|
4312
|
+
|
|
4313
|
+
for (const v of violations) {
|
|
4314
|
+
if (v.severity === 'error') {
|
|
4315
|
+
console.log(chalk.red(` \u2717 BLOCK ${v.message}`));
|
|
4316
|
+
if (v.path) console.log(chalk.gray(` ${v.path}`));
|
|
4317
|
+
totalBlocking++;
|
|
4318
|
+
} else {
|
|
4319
|
+
console.log(chalk.yellow(` \u26a0 WARN ${v.message}`));
|
|
4320
|
+
if (v.path) console.log(chalk.gray(` ${v.path}`));
|
|
4321
|
+
totalWarnings++;
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
4324
|
+
} else {
|
|
4325
|
+
console.log(chalk.green(` \u2713 PASS ${specFile} \u2014 no issues`));
|
|
4326
|
+
totalPassed++;
|
|
4327
|
+
}
|
|
4328
|
+
} catch {
|
|
4329
|
+
console.log(chalk.green(` \u2713 PASS ${specFile} \u2014 no issues`));
|
|
4330
|
+
totalPassed++;
|
|
4331
|
+
} finally {
|
|
4332
|
+
try { fs.unlinkSync(tmpBase); } catch {}
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
}
|
|
4336
|
+
|
|
4337
|
+
// Check for non-spec governance signals
|
|
4338
|
+
const hasPaymentFiles = stagedFiles.some(f => f.includes('payment') || f.includes('billing') || f.includes('stripe'));
|
|
4339
|
+
if (hasPaymentFiles) {
|
|
4340
|
+
const paymentRule = policyRules.find(r => r.name && r.name.toLowerCase().includes('payment'));
|
|
4341
|
+
if (paymentRule) {
|
|
4342
|
+
const action = paymentRule.mode || paymentRule.action || 'warn';
|
|
4343
|
+
if (action === 'enforce' || action === 'forbid') {
|
|
4344
|
+
console.log(chalk.red(` \u2717 BLOCK Payment code change detected \u2014 "${paymentRule.name}" rule is in ${action} mode`));
|
|
4345
|
+
totalBlocking++;
|
|
4346
|
+
} else {
|
|
4347
|
+
console.log(chalk.yellow(` \u26a0 WARN Payment code change detected \u2014 "${paymentRule.name}" rule (${action} mode)`));
|
|
4348
|
+
totalWarnings++;
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4352
|
+
|
|
4353
|
+
// --- Mode 3: Default — show policy overview and what would happen ---
|
|
4354
|
+
} else {
|
|
4355
|
+
// Find all specs in the project
|
|
4356
|
+
const specPatterns = [
|
|
4357
|
+
'openapi.yaml', 'openapi.yml', 'openapi.json',
|
|
4358
|
+
'swagger.yaml', 'swagger.yml', 'swagger.json',
|
|
4359
|
+
'docs/openapi.yaml', 'docs/openapi.yml', 'docs/openapi.json',
|
|
4360
|
+
'spec/openapi.yaml', 'spec/openapi.json',
|
|
4361
|
+
'api/openapi.yaml', 'api/openapi.json',
|
|
4362
|
+
'contrib/openapi.json',
|
|
4363
|
+
];
|
|
4364
|
+
const foundSpecs = specPatterns.filter(p => fs.existsSync(path.join(projectDir, p)));
|
|
4365
|
+
|
|
4366
|
+
if (foundSpecs.length > 0) {
|
|
4367
|
+
console.log(chalk.green(` \u2713 PASS API spec(s) found: ${foundSpecs.join(', ')}`));
|
|
4368
|
+
totalPassed++;
|
|
4369
|
+
} else {
|
|
4370
|
+
console.log(chalk.yellow(' \u26a0 WARN No API spec files found in project'));
|
|
4371
|
+
totalWarnings++;
|
|
4372
|
+
}
|
|
4373
|
+
|
|
4374
|
+
// Check git status for uncommitted spec changes
|
|
4375
|
+
try {
|
|
4376
|
+
const output = execSync('git diff --name-only', {
|
|
4377
|
+
cwd: projectDir, encoding: 'utf-8', timeout: 5000
|
|
4378
|
+
}).trim();
|
|
4379
|
+
const stagedOutput = execSync('git diff --cached --name-only', {
|
|
4380
|
+
cwd: projectDir, encoding: 'utf-8', timeout: 5000
|
|
4381
|
+
}).trim();
|
|
4382
|
+
|
|
4383
|
+
const allChanged = [...new Set([
|
|
4384
|
+
...(output ? output.split('\n') : []),
|
|
4385
|
+
...(stagedOutput ? stagedOutput.split('\n') : []),
|
|
4386
|
+
])];
|
|
4387
|
+
|
|
4388
|
+
const specKeywords = ['openapi', 'swagger', 'api-spec', 'api_spec'];
|
|
4389
|
+
const changedSpecs = allChanged.filter(f => {
|
|
4390
|
+
const name = path.basename(f).toLowerCase();
|
|
4391
|
+
return specKeywords.some(kw => name.includes(kw));
|
|
4392
|
+
});
|
|
4393
|
+
|
|
4394
|
+
if (changedSpecs.length > 0) {
|
|
4395
|
+
console.log(chalk.yellow(` \u26a0 WARN ${changedSpecs.length} uncommitted spec change(s): ${changedSpecs.join(', ')}`));
|
|
4396
|
+
console.log(chalk.gray(' Run: delimit simulate --commit to check staged changes'));
|
|
4397
|
+
totalWarnings++;
|
|
4398
|
+
} else {
|
|
4399
|
+
console.log(chalk.green(' \u2713 PASS No uncommitted API spec changes'));
|
|
4400
|
+
totalPassed++;
|
|
4401
|
+
}
|
|
4402
|
+
} catch {
|
|
4403
|
+
console.log(chalk.gray(' \u2500 SKIP Not a git repository'));
|
|
4404
|
+
}
|
|
4405
|
+
|
|
4406
|
+
// Check governance hooks
|
|
4407
|
+
const gitHooksDir = path.join(projectDir, '.git', 'hooks');
|
|
4408
|
+
const preCommitHook = path.join(gitHooksDir, 'pre-commit');
|
|
4409
|
+
if (fs.existsSync(preCommitHook)) {
|
|
4410
|
+
try {
|
|
4411
|
+
const hookContent = fs.readFileSync(preCommitHook, 'utf8');
|
|
4412
|
+
if (hookContent.includes('delimit')) {
|
|
4413
|
+
console.log(chalk.green(' \u2713 PASS Delimit pre-commit hook installed'));
|
|
4414
|
+
totalPassed++;
|
|
4415
|
+
} else {
|
|
4416
|
+
console.log(chalk.yellow(' \u26a0 WARN Pre-commit hook exists but does not reference Delimit'));
|
|
4417
|
+
totalWarnings++;
|
|
4418
|
+
}
|
|
4419
|
+
} catch {
|
|
4420
|
+
console.log(chalk.yellow(' \u26a0 WARN Could not read pre-commit hook'));
|
|
4421
|
+
totalWarnings++;
|
|
4422
|
+
}
|
|
4423
|
+
} else {
|
|
4424
|
+
console.log(chalk.yellow(' \u26a0 WARN No pre-commit hook \u2014 governance only runs manually'));
|
|
4425
|
+
console.log(chalk.gray(' Run: delimit hooks install'));
|
|
4426
|
+
totalWarnings++;
|
|
4427
|
+
}
|
|
4428
|
+
|
|
4429
|
+
// GitHub Action check
|
|
4430
|
+
const workflowDir = path.join(projectDir, '.github', 'workflows');
|
|
4431
|
+
if (fs.existsSync(workflowDir)) {
|
|
4432
|
+
try {
|
|
4433
|
+
const workflows = fs.readdirSync(workflowDir);
|
|
4434
|
+
const hasDelimit = workflows.some(f => {
|
|
4435
|
+
try {
|
|
4436
|
+
const content = fs.readFileSync(path.join(workflowDir, f), 'utf8');
|
|
4437
|
+
return content.includes('delimit-ai/delimit') || content.includes('delimit');
|
|
4438
|
+
} catch { return false; }
|
|
4439
|
+
});
|
|
4440
|
+
if (hasDelimit) {
|
|
4441
|
+
console.log(chalk.green(' \u2713 PASS GitHub Action governance workflow found'));
|
|
4442
|
+
totalPassed++;
|
|
4443
|
+
} else {
|
|
4444
|
+
console.log(chalk.yellow(' \u26a0 WARN No Delimit GitHub Action \u2014 CI governance not enabled'));
|
|
4445
|
+
console.log(chalk.gray(' Run: delimit ci'));
|
|
4446
|
+
totalWarnings++;
|
|
4447
|
+
}
|
|
4448
|
+
} catch {}
|
|
4449
|
+
} else {
|
|
4450
|
+
console.log(chalk.yellow(' \u26a0 WARN No .github/workflows/ directory'));
|
|
4451
|
+
totalWarnings++;
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4454
|
+
|
|
4455
|
+
// --- Verdict ---
|
|
4456
|
+
console.log('');
|
|
4457
|
+
if (totalBlocking > 0) {
|
|
4458
|
+
const parts = [];
|
|
4459
|
+
if (totalBlocking > 0) parts.push(`${totalBlocking} blocking`);
|
|
4460
|
+
if (totalWarnings > 0) parts.push(`${totalWarnings} warning(s)`);
|
|
4461
|
+
if (totalPassed > 0) parts.push(`${totalPassed} passed`);
|
|
4462
|
+
console.log(chalk.gray(` Verdict: ${parts.join(', ')}`));
|
|
4463
|
+
console.log(chalk.red.bold(' A real commit would be BLOCKED.\n'));
|
|
4464
|
+
} else if (totalWarnings > 0) {
|
|
4465
|
+
const parts = [];
|
|
4466
|
+
if (totalWarnings > 0) parts.push(`${totalWarnings} warning(s)`);
|
|
4467
|
+
if (totalPassed > 0) parts.push(`${totalPassed} passed`);
|
|
4468
|
+
console.log(chalk.gray(` Verdict: ${parts.join(', ')}`));
|
|
4469
|
+
console.log(chalk.yellow.bold(' A real commit would PASS with warnings.\n'));
|
|
4470
|
+
} else {
|
|
4471
|
+
console.log(chalk.gray(` Verdict: ${totalPassed} passed, 0 warnings, 0 blocking`));
|
|
4472
|
+
console.log(chalk.green.bold(' A real commit would PASS cleanly.\n'));
|
|
4473
|
+
}
|
|
3231
4474
|
});
|
|
3232
4475
|
|
|
3233
4476
|
// Hooks command — install/remove git hooks for governance
|
|
@@ -4551,7 +5794,7 @@ program
|
|
|
4551
5794
|
.argument("[action]", "Action: status | set | list | reveal", "status")
|
|
4552
5795
|
.option("--verbose", "Show encryption details and backend status")
|
|
4553
5796
|
.action(async (action, options) => {
|
|
4554
|
-
console.log(chalk.
|
|
5797
|
+
console.log(chalk.magenta.bold("\n Delimit Vault\n"));
|
|
4555
5798
|
|
|
4556
5799
|
if (action === "status") {
|
|
4557
5800
|
console.log(chalk.bold("Backend Status:"));
|
|
@@ -4566,13 +5809,70 @@ program
|
|
|
4566
5809
|
console.log("\nUse " + chalk.cyan("delimit vault list") + " to see configured secrets.");
|
|
4567
5810
|
} else if (action === "list") {
|
|
4568
5811
|
console.log(chalk.bold("Configured Secrets:"));
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
5812
|
+
const secretsDir = path.join(os.homedir(), '.delimit', 'secrets');
|
|
5813
|
+
if (fs.existsSync(secretsDir)) {
|
|
5814
|
+
const files = fs.readdirSync(secretsDir).filter(f => f.endsWith('.json') && !f.startsWith('.'));
|
|
5815
|
+
if (files.length === 0) {
|
|
5816
|
+
console.log(chalk.dim(" No secrets configured yet."));
|
|
5817
|
+
} else {
|
|
5818
|
+
files.forEach(f => {
|
|
5819
|
+
const name = f.replace('.json', '');
|
|
5820
|
+
console.log(` • ${name} ${chalk.gray("********")}`);
|
|
5821
|
+
});
|
|
5822
|
+
}
|
|
5823
|
+
} else {
|
|
5824
|
+
console.log(chalk.dim(" No secrets directory found."));
|
|
5825
|
+
}
|
|
5826
|
+
console.log("\nRun " + chalk.cyan("delimit vault set <NAME>") + " to add a secret.");
|
|
5827
|
+
} else if (action === "set") {
|
|
5828
|
+
const name = process.argv[4];
|
|
5829
|
+
if (!name) {
|
|
5830
|
+
console.log(chalk.red("Usage: delimit vault set <NAME>"));
|
|
5831
|
+
console.log(chalk.dim(" Example: delimit vault set OPENAI_API_KEY"));
|
|
5832
|
+
process.exit(1);
|
|
5833
|
+
}
|
|
5834
|
+
const secretsDir = path.join(os.homedir(), '.delimit', 'secrets');
|
|
5835
|
+
fs.mkdirSync(secretsDir, { recursive: true });
|
|
5836
|
+
const filePath = path.join(secretsDir, `${name}.json`);
|
|
5837
|
+
const existing = fs.existsSync(filePath);
|
|
5838
|
+
// Read value from stdin or prompt
|
|
5839
|
+
const readline = require('readline');
|
|
5840
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
5841
|
+
rl.question(` Enter value for ${chalk.bold(name)}: `, (value) => {
|
|
5842
|
+
rl.close();
|
|
5843
|
+
if (!value || !value.trim()) {
|
|
5844
|
+
console.log(chalk.red(" Empty value. Aborted."));
|
|
5845
|
+
return;
|
|
5846
|
+
}
|
|
5847
|
+
fs.writeFileSync(filePath, JSON.stringify({ key: name, value: value.trim(), updated: new Date().toISOString() }), 'utf-8');
|
|
5848
|
+
fs.chmodSync(filePath, 0o600);
|
|
5849
|
+
console.log(chalk.green(` ${existing ? 'Updated' : 'Saved'}: ${name}`));
|
|
5850
|
+
console.log(chalk.dim(` Location: ${filePath}`));
|
|
5851
|
+
});
|
|
5852
|
+
} else if (action === "reveal") {
|
|
5853
|
+
const name = process.argv[4];
|
|
5854
|
+
if (!name) {
|
|
5855
|
+
console.log(chalk.red("Usage: delimit vault reveal <NAME>"));
|
|
5856
|
+
process.exit(1);
|
|
5857
|
+
}
|
|
5858
|
+
const secretsDir = path.join(os.homedir(), '.delimit', 'secrets');
|
|
5859
|
+
const filePath = path.join(secretsDir, `${name}.json`);
|
|
5860
|
+
if (!fs.existsSync(filePath)) {
|
|
5861
|
+
console.log(chalk.red(` Secret "${name}" not found.`));
|
|
5862
|
+
console.log(chalk.dim(" Run " + chalk.cyan("delimit vault list") + " to see configured secrets."));
|
|
5863
|
+
process.exit(1);
|
|
5864
|
+
}
|
|
5865
|
+
try {
|
|
5866
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
5867
|
+
const val = data.value || data.key || '(empty)';
|
|
5868
|
+
console.log(` ${chalk.bold(name)}: ${val}`);
|
|
5869
|
+
if (data.updated) console.log(chalk.dim(` Updated: ${data.updated}`));
|
|
5870
|
+
} catch {
|
|
5871
|
+
console.log(chalk.red(` Failed to read secret "${name}".`));
|
|
5872
|
+
}
|
|
4573
5873
|
} else {
|
|
4574
|
-
console.log(chalk.yellow(`
|
|
4575
|
-
console.log("
|
|
5874
|
+
console.log(chalk.yellow(`Unknown action: "${action}"`));
|
|
5875
|
+
console.log("Available: " + chalk.cyan("status") + " | " + chalk.cyan("list") + " | " + chalk.cyan("set <NAME>") + " | " + chalk.cyan("reveal <NAME>"));
|
|
4576
5876
|
}
|
|
4577
5877
|
console.log("");
|
|
4578
5878
|
});
|
|
@@ -4709,13 +6009,18 @@ function readMemories() {
|
|
|
4709
6009
|
function writeMemory(entry) {
|
|
4710
6010
|
// Write in MCP-compatible format (individual .json files)
|
|
4711
6011
|
fs.mkdirSync(MEMORY_DIR, { recursive: true });
|
|
4712
|
-
const
|
|
6012
|
+
const crypto = require('crypto');
|
|
6013
|
+
const content = entry.text;
|
|
6014
|
+
const memId = 'mem-' + crypto.createHash('sha256').update(content.slice(0, 100)).digest('hex').slice(0, 12);
|
|
6015
|
+
const hash = crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
4713
6016
|
const mcpEntry = {
|
|
4714
6017
|
id: memId,
|
|
4715
|
-
content
|
|
6018
|
+
content,
|
|
4716
6019
|
tags: entry.tags || [],
|
|
4717
6020
|
context: entry.source || 'cli',
|
|
4718
6021
|
created_at: entry.created || new Date().toISOString(),
|
|
6022
|
+
hash,
|
|
6023
|
+
source_model: process.env.DELIMIT_MODEL || 'cli',
|
|
4719
6024
|
};
|
|
4720
6025
|
fs.writeFileSync(path.join(MEMORY_DIR, `${memId}.json`), JSON.stringify(mcpEntry, null, 2));
|
|
4721
6026
|
return memId;
|
|
@@ -4761,8 +6066,18 @@ function relativeTime(isoDate) {
|
|
|
4761
6066
|
return `${diffYear} year${diffYear === 1 ? '' : 's'} ago`;
|
|
4762
6067
|
}
|
|
4763
6068
|
|
|
6069
|
+
function verifyMemoryIntegrity(mem) {
|
|
6070
|
+
if (!mem.hash) return null; // No hash — legacy memory
|
|
6071
|
+
const crypto = require('crypto');
|
|
6072
|
+
const expected = crypto.createHash('sha256').update(mem.content || mem.text || '').digest('hex').slice(0, 16);
|
|
6073
|
+
return expected === mem.hash;
|
|
6074
|
+
}
|
|
6075
|
+
|
|
4764
6076
|
function displayMemory(mem) {
|
|
4765
|
-
|
|
6077
|
+
const integrity = verifyMemoryIntegrity(mem);
|
|
6078
|
+
const integrityBadge = integrity === true ? chalk.green(' \u2713') : integrity === false ? chalk.red(' \u2717 tampered') : '';
|
|
6079
|
+
const sourceBadge = mem.source_model ? chalk.gray(` [${mem.source_model}]`) : mem.context ? chalk.gray(` [${mem.context}]`) : '';
|
|
6080
|
+
console.log(` ${chalk.gray('[' + mem.id + ']')} ${chalk.gray(relativeTime(mem.created))}${sourceBadge}${integrityBadge}`);
|
|
4766
6081
|
console.log(` ${mem.text}`);
|
|
4767
6082
|
if (mem.tags && mem.tags.length > 0) {
|
|
4768
6083
|
console.log(` ${chalk.blue(mem.tags.map(t => '#' + t).join(' '))}`);
|