delimit-cli 4.0.4 → 4.0.6
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/bin/delimit-cli.js +228 -11
- package/bin/delimit-os.sh +105 -0
- package/gateway/ai/tui.py +296 -0
- package/package.json +1 -1
package/bin/delimit-cli.js
CHANGED
|
@@ -1318,7 +1318,7 @@ program
|
|
|
1318
1318
|
}
|
|
1319
1319
|
}
|
|
1320
1320
|
|
|
1321
|
-
// Auto-detect OpenAPI spec files
|
|
1321
|
+
// Auto-detect OpenAPI spec files — flat patterns + recursive scan
|
|
1322
1322
|
const specPatterns = [
|
|
1323
1323
|
'openapi.yaml', 'openapi.yml', 'openapi.json',
|
|
1324
1324
|
'swagger.yaml', 'swagger.yml', 'swagger.json',
|
|
@@ -1329,9 +1329,146 @@ program
|
|
|
1329
1329
|
'api/openapi.yaml', 'api/openapi.json',
|
|
1330
1330
|
'contrib/openapi.json',
|
|
1331
1331
|
];
|
|
1332
|
-
|
|
1332
|
+
let foundSpecs = specPatterns.filter(p => fs.existsSync(path.join(projectDir, p)));
|
|
1333
|
+
|
|
1334
|
+
// Recursive scan: search common directories for OpenAPI/Swagger files
|
|
1335
|
+
const specDirs = ['swagger', 'api', 'docs', 'spec', 'specs', 'openapi', 'schema', 'schemas', 'config', 'src'];
|
|
1336
|
+
const specExtensions = ['.yaml', '.yml', '.json'];
|
|
1337
|
+
const specKeywords = ['openapi', 'swagger', 'api-spec', 'api_spec'];
|
|
1338
|
+
function scanDirForSpecs(dir, depth = 0) {
|
|
1339
|
+
if (depth > 2) return []; // limit recursion depth
|
|
1340
|
+
const results = [];
|
|
1341
|
+
try {
|
|
1342
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1343
|
+
for (const entry of entries) {
|
|
1344
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'vendor') continue;
|
|
1345
|
+
const fullPath = path.join(dir, entry.name);
|
|
1346
|
+
if (entry.isDirectory() && depth < 2) {
|
|
1347
|
+
results.push(...scanDirForSpecs(fullPath, depth + 1));
|
|
1348
|
+
} else if (entry.isFile() && specExtensions.includes(path.extname(entry.name).toLowerCase())) {
|
|
1349
|
+
// Check if file looks like an OpenAPI/Swagger spec
|
|
1350
|
+
const nameLower = entry.name.toLowerCase();
|
|
1351
|
+
const isLikelySpec = specKeywords.some(kw => nameLower.includes(kw)) || nameLower === 'api.yaml' || nameLower === 'api.yml' || nameLower === 'api.json';
|
|
1352
|
+
if (isLikelySpec) {
|
|
1353
|
+
results.push(path.relative(projectDir, fullPath));
|
|
1354
|
+
} else {
|
|
1355
|
+
// Peek inside to check for openapi/swagger key
|
|
1356
|
+
try {
|
|
1357
|
+
const head = fs.readFileSync(fullPath, 'utf-8').slice(0, 512);
|
|
1358
|
+
if (head.includes('"openapi"') || head.includes("openapi:") || head.includes('"swagger"') || head.includes("swagger:")) {
|
|
1359
|
+
results.push(path.relative(projectDir, fullPath));
|
|
1360
|
+
}
|
|
1361
|
+
} catch {}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
} catch {}
|
|
1366
|
+
return results;
|
|
1367
|
+
}
|
|
1368
|
+
for (const sd of specDirs) {
|
|
1369
|
+
const sdPath = path.join(projectDir, sd);
|
|
1370
|
+
if (fs.existsSync(sdPath)) {
|
|
1371
|
+
const deepSpecs = scanDirForSpecs(sdPath);
|
|
1372
|
+
for (const ds of deepSpecs) {
|
|
1373
|
+
if (!foundSpecs.includes(ds)) foundSpecs.push(ds);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
// Also scan root-level yaml/json files for OpenAPI markers
|
|
1378
|
+
try {
|
|
1379
|
+
const rootEntries = fs.readdirSync(projectDir, { withFileTypes: true });
|
|
1380
|
+
for (const entry of rootEntries) {
|
|
1381
|
+
if (!entry.isFile()) continue;
|
|
1382
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
1383
|
+
if (!specExtensions.includes(ext)) continue;
|
|
1384
|
+
const rel = entry.name;
|
|
1385
|
+
if (foundSpecs.includes(rel)) continue;
|
|
1386
|
+
try {
|
|
1387
|
+
const head = fs.readFileSync(path.join(projectDir, rel), 'utf-8').slice(0, 512);
|
|
1388
|
+
if (head.includes('"openapi"') || head.includes("openapi:") || head.includes('"swagger"') || head.includes("swagger:")) {
|
|
1389
|
+
foundSpecs.push(rel);
|
|
1390
|
+
}
|
|
1391
|
+
} catch {}
|
|
1392
|
+
}
|
|
1393
|
+
} catch {}
|
|
1333
1394
|
const specPath = foundSpecs.length > 0 ? foundSpecs[0] : null;
|
|
1334
1395
|
|
|
1396
|
+
// Detect test files and count them
|
|
1397
|
+
let testFileCount = 0;
|
|
1398
|
+
let testFramework = null;
|
|
1399
|
+
function countTestFiles(dir, depth = 0) {
|
|
1400
|
+
if (depth > 3) return;
|
|
1401
|
+
try {
|
|
1402
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1403
|
+
for (const entry of entries) {
|
|
1404
|
+
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'vendor' || entry.name === '__pycache__') continue;
|
|
1405
|
+
const fullPath = path.join(dir, entry.name);
|
|
1406
|
+
if (entry.isDirectory()) {
|
|
1407
|
+
if (entry.name === '__tests__' || entry.name === 'test' || entry.name === 'tests' || entry.name === 'spec') {
|
|
1408
|
+
countTestFiles(fullPath, depth + 1);
|
|
1409
|
+
} else if (depth < 2) {
|
|
1410
|
+
countTestFiles(fullPath, depth + 1);
|
|
1411
|
+
}
|
|
1412
|
+
} else if (entry.isFile()) {
|
|
1413
|
+
const name = entry.name.toLowerCase();
|
|
1414
|
+
if (name.endsWith('.test.js') || name.endsWith('.test.ts') || name.endsWith('.test.tsx') || name.endsWith('.spec.js') || name.endsWith('.spec.ts')) {
|
|
1415
|
+
testFileCount++;
|
|
1416
|
+
} else if (name.startsWith('test_') && name.endsWith('.py')) {
|
|
1417
|
+
testFileCount++;
|
|
1418
|
+
} else if (name.endsWith('_test.go')) {
|
|
1419
|
+
testFileCount++;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
} catch {}
|
|
1424
|
+
}
|
|
1425
|
+
countTestFiles(projectDir);
|
|
1426
|
+
// Detect test framework
|
|
1427
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
1428
|
+
try {
|
|
1429
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
1430
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1431
|
+
if (allDeps['jest'] || (pkg.scripts && pkg.scripts.test && pkg.scripts.test.includes('jest'))) testFramework = 'jest';
|
|
1432
|
+
else if (allDeps['vitest']) testFramework = 'vitest';
|
|
1433
|
+
else if (allDeps['mocha']) testFramework = 'mocha';
|
|
1434
|
+
} catch {}
|
|
1435
|
+
}
|
|
1436
|
+
if (!testFramework && fs.existsSync(requirementsPath)) {
|
|
1437
|
+
try {
|
|
1438
|
+
const content = fs.readFileSync(requirementsPath, 'utf-8').toLowerCase();
|
|
1439
|
+
if (content.includes('pytest')) testFramework = 'pytest';
|
|
1440
|
+
} catch {}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// Quick security scan
|
|
1444
|
+
const securityFindings = [];
|
|
1445
|
+
// Check for common secret patterns in spec files
|
|
1446
|
+
for (const sp of foundSpecs.slice(0, 5)) {
|
|
1447
|
+
try {
|
|
1448
|
+
const content = fs.readFileSync(path.join(projectDir, sp), 'utf-8');
|
|
1449
|
+
if (/(?:api[_-]?key|secret|password|token)\s*[:=]\s*["'][^"']{8,}/i.test(content)) {
|
|
1450
|
+
securityFindings.push({ severity: 'high', file: sp, issue: 'Possible hardcoded secret in spec file' });
|
|
1451
|
+
}
|
|
1452
|
+
if (/http:\/\/(?!localhost|127\.0\.0\.1)/i.test(content)) {
|
|
1453
|
+
securityFindings.push({ severity: 'medium', file: sp, issue: 'Non-localhost HTTP URL in spec (should use HTTPS)' });
|
|
1454
|
+
}
|
|
1455
|
+
} catch {}
|
|
1456
|
+
}
|
|
1457
|
+
// Check for .env files committed (not in .gitignore)
|
|
1458
|
+
const envFiles = ['.env', '.env.local', '.env.production'];
|
|
1459
|
+
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
1460
|
+
let gitignoreContent = '';
|
|
1461
|
+
try { gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8'); } catch {}
|
|
1462
|
+
for (const envFile of envFiles) {
|
|
1463
|
+
if (fs.existsSync(path.join(projectDir, envFile))) {
|
|
1464
|
+
if (!gitignoreContent.includes(envFile)) {
|
|
1465
|
+
securityFindings.push({ severity: 'high', file: envFile, issue: `${envFile} exists and is not in .gitignore` });
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
// Check for package-lock.json / yarn.lock (dependency lockfile)
|
|
1470
|
+
const hasLockfile = fs.existsSync(path.join(projectDir, 'package-lock.json')) || fs.existsSync(path.join(projectDir, 'yarn.lock')) || fs.existsSync(path.join(projectDir, 'pnpm-lock.yaml'));
|
|
1471
|
+
|
|
1335
1472
|
// Check for CI
|
|
1336
1473
|
const hasGitHub = fs.existsSync(path.join(projectDir, '.github'));
|
|
1337
1474
|
const hasGitLabCI = fs.existsSync(path.join(projectDir, '.gitlab-ci.yml'));
|
|
@@ -1340,11 +1477,33 @@ program
|
|
|
1340
1477
|
// Display detection results
|
|
1341
1478
|
console.log(` Project: ${chalk.bold(projectName)}`);
|
|
1342
1479
|
if (frameworkLabel) console.log(` Framework: ${chalk.bold(frameworkLabel)}`);
|
|
1343
|
-
if (
|
|
1344
|
-
|
|
1480
|
+
if (foundSpecs.length > 1) {
|
|
1481
|
+
console.log(` Specs: ${chalk.bold(foundSpecs.length + ' found')}`);
|
|
1482
|
+
for (const sp of foundSpecs.slice(0, 5)) {
|
|
1483
|
+
console.log(` ${chalk.gray('-')} ${sp}`);
|
|
1484
|
+
}
|
|
1485
|
+
if (foundSpecs.length > 5) console.log(` ${chalk.gray(`... and ${foundSpecs.length - 5} more`)}`);
|
|
1486
|
+
} else if (specPath) {
|
|
1487
|
+
console.log(` Spec: ${chalk.bold(specPath)}`);
|
|
1488
|
+
} else if (['fastapi', 'nestjs', 'express'].includes(framework)) {
|
|
1345
1489
|
console.log(` Spec: ${chalk.gray('none found')} (Zero-Spec Mode available for ${frameworkLabel})`);
|
|
1346
|
-
else
|
|
1490
|
+
} else {
|
|
1491
|
+
console.log(` Spec: ${chalk.gray('none found')}`);
|
|
1492
|
+
}
|
|
1493
|
+
if (testFileCount > 0) {
|
|
1494
|
+
console.log(` Tests: ${chalk.bold(testFileCount + ' file' + (testFileCount !== 1 ? 's' : ''))}${testFramework ? chalk.gray(' (' + testFramework + ')') : ''}`);
|
|
1495
|
+
} else {
|
|
1496
|
+
console.log(` Tests: ${chalk.gray('none detected')}`);
|
|
1497
|
+
}
|
|
1347
1498
|
if (ciProvider !== 'none') console.log(` CI: ${chalk.bold(ciProvider === 'github' ? 'GitHub Actions' : 'GitLab CI')}`);
|
|
1499
|
+
if (securityFindings.length > 0) {
|
|
1500
|
+
console.log(` Security: ${chalk.yellow(securityFindings.length + ' finding' + (securityFindings.length !== 1 ? 's' : ''))}`);
|
|
1501
|
+
} else {
|
|
1502
|
+
console.log(` Security: ${chalk.green('clean (quick scan)')}`);
|
|
1503
|
+
}
|
|
1504
|
+
if (!hasLockfile && fs.existsSync(pkgJsonPath)) {
|
|
1505
|
+
console.log(` Lockfile: ${chalk.yellow('missing — consider committing a lockfile')}`);
|
|
1506
|
+
}
|
|
1348
1507
|
console.log('');
|
|
1349
1508
|
|
|
1350
1509
|
// Step 2: Choose preset
|
|
@@ -1596,7 +1755,7 @@ jobs:
|
|
|
1596
1755
|
}
|
|
1597
1756
|
}
|
|
1598
1757
|
|
|
1599
|
-
// Step 6: Save first evidence event (LED-258)
|
|
1758
|
+
// Step 6: Save first evidence event + comprehensive report (LED-258)
|
|
1600
1759
|
const evidenceDir = path.join(configDir, 'evidence');
|
|
1601
1760
|
fs.mkdirSync(evidenceDir, { recursive: true });
|
|
1602
1761
|
const evidenceEvent = {
|
|
@@ -1605,23 +1764,67 @@ jobs:
|
|
|
1605
1764
|
type: 'governance_init',
|
|
1606
1765
|
tool: 'delimit_init',
|
|
1607
1766
|
model: 'cli',
|
|
1608
|
-
status: 'pass',
|
|
1767
|
+
status: securityFindings.some(f => f.severity === 'high') ? 'warn' : 'pass',
|
|
1609
1768
|
summary: `Governance initialized with ${preset} preset`,
|
|
1610
1769
|
detail: [
|
|
1611
1770
|
`Project: ${projectName}`,
|
|
1612
1771
|
frameworkLabel ? `Framework: ${frameworkLabel}` : null,
|
|
1613
|
-
|
|
1772
|
+
foundSpecs.length > 0 ? `Specs found: ${foundSpecs.length} (${foundSpecs.join(', ')})` : 'Mode: Zero-Spec',
|
|
1614
1773
|
`Preset: ${preset}`,
|
|
1774
|
+
`Test files: ${testFileCount}`,
|
|
1775
|
+
testFramework ? `Test framework: ${testFramework}` : null,
|
|
1615
1776
|
ciProvider !== 'none' ? `CI: ${ciProvider}` : null,
|
|
1777
|
+
securityFindings.length > 0 ? `Security findings: ${securityFindings.length}` : 'Security: clean',
|
|
1616
1778
|
].filter(Boolean).join('\n'),
|
|
1617
1779
|
venture: projectName,
|
|
1618
1780
|
};
|
|
1619
1781
|
try {
|
|
1620
1782
|
const evidenceFile = path.join(evidenceDir, 'events.jsonl');
|
|
1621
1783
|
fs.appendFileSync(evidenceFile, JSON.stringify(evidenceEvent) + '\n');
|
|
1622
|
-
console.log(chalk.green(' Evidence recorded — first governance event saved'));
|
|
1623
1784
|
} catch {}
|
|
1624
1785
|
|
|
1786
|
+
// Generate first evidence report (LED-258: zero-config onboarding)
|
|
1787
|
+
const firstReport = {
|
|
1788
|
+
generated_at: new Date().toISOString(),
|
|
1789
|
+
project: projectName,
|
|
1790
|
+
framework: frameworkLabel || 'unknown',
|
|
1791
|
+
specs: {
|
|
1792
|
+
count: foundSpecs.length,
|
|
1793
|
+
files: foundSpecs,
|
|
1794
|
+
primary: specPath,
|
|
1795
|
+
},
|
|
1796
|
+
tests: {
|
|
1797
|
+
file_count: testFileCount,
|
|
1798
|
+
framework: testFramework,
|
|
1799
|
+
},
|
|
1800
|
+
security: {
|
|
1801
|
+
findings_count: securityFindings.length,
|
|
1802
|
+
findings: securityFindings,
|
|
1803
|
+
lockfile_present: hasLockfile,
|
|
1804
|
+
},
|
|
1805
|
+
governance: {
|
|
1806
|
+
preset: preset,
|
|
1807
|
+
compliance_template: complianceTemplate,
|
|
1808
|
+
ci_provider: ciProvider,
|
|
1809
|
+
gates_active: specPath || ['fastapi', 'nestjs', 'express'].includes(framework) ? ['api_lint'] : [],
|
|
1810
|
+
gates_ready: ['security_audit', 'deploy_plan', 'release_validate'],
|
|
1811
|
+
},
|
|
1812
|
+
};
|
|
1813
|
+
try {
|
|
1814
|
+
const reportFile = path.join(evidenceDir, 'first-report.json');
|
|
1815
|
+
fs.writeFileSync(reportFile, JSON.stringify(firstReport, null, 2));
|
|
1816
|
+
console.log(chalk.green(' Evidence recorded — first governance report saved'));
|
|
1817
|
+
} catch {}
|
|
1818
|
+
|
|
1819
|
+
// Display security findings if any
|
|
1820
|
+
if (securityFindings.length > 0) {
|
|
1821
|
+
console.log(chalk.bold('\n Security Findings:'));
|
|
1822
|
+
for (const finding of securityFindings) {
|
|
1823
|
+
const icon = finding.severity === 'high' ? chalk.red('!') : chalk.yellow('~');
|
|
1824
|
+
console.log(` ${icon} ${chalk.bold(finding.severity.toUpperCase())} ${finding.file} — ${finding.issue}`);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1625
1828
|
// Step 7: Show gate status (LED-258)
|
|
1626
1829
|
console.log(chalk.bold('\n Governance Gates:'));
|
|
1627
1830
|
const gates = [
|
|
@@ -1639,15 +1842,29 @@ jobs:
|
|
|
1639
1842
|
// Summary
|
|
1640
1843
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
1641
1844
|
console.log(chalk.bold(`\n Setup complete in ${elapsed}s`));
|
|
1642
|
-
console.log(chalk.gray(` Evidence saved to .delimit/evidence
|
|
1845
|
+
console.log(chalk.gray(` Evidence saved to .delimit/evidence/\n`));
|
|
1643
1846
|
console.log(' Next steps:');
|
|
1644
1847
|
if (specPath) {
|
|
1645
1848
|
console.log(` ${chalk.bold('delimit lint')} ${specPath} ${specPath} — lint on every PR`);
|
|
1849
|
+
} else if (['fastapi', 'nestjs', 'express'].includes(framework)) {
|
|
1850
|
+
console.log(` ${chalk.bold('delimit lint')} — zero-spec mode (${frameworkLabel})`);
|
|
1646
1851
|
} else {
|
|
1647
|
-
console.log(` ${chalk.bold('delimit lint')} —
|
|
1852
|
+
console.log(` ${chalk.bold('delimit lint')} — add an OpenAPI spec first`);
|
|
1648
1853
|
}
|
|
1649
1854
|
console.log(` ${chalk.bold('delimit doctor')} — verify setup`);
|
|
1650
1855
|
console.log(` ${chalk.bold('delimit explain')} — human-readable report`);
|
|
1856
|
+
if (securityFindings.length > 0) {
|
|
1857
|
+
console.log(` ${chalk.yellow('Fix security findings above')} — ${securityFindings.length} issue${securityFindings.length !== 1 ? 's' : ''} found`);
|
|
1858
|
+
}
|
|
1859
|
+
if (testFileCount === 0) {
|
|
1860
|
+
console.log(` ${chalk.gray('Add tests')} — no test files detected`);
|
|
1861
|
+
}
|
|
1862
|
+
if (foundSpecs.length > 1) {
|
|
1863
|
+
console.log(` ${chalk.gray('Review all ' + foundSpecs.length + ' specs')} — multiple specs detected`);
|
|
1864
|
+
}
|
|
1865
|
+
if (ciProvider === 'none') {
|
|
1866
|
+
console.log(` ${chalk.gray('Add CI')} — no CI detected; consider GitHub Actions`);
|
|
1867
|
+
}
|
|
1651
1868
|
|
|
1652
1869
|
// Beta capture after init (LED-263)
|
|
1653
1870
|
if (!options.yes) {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Delimit OS — the AI developer operating system
|
|
3
|
+
# Type 'delimit' to launch the TUI, or 'delimit <command>' for CLI tools
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# delimit → Launch TUI (interactive terminal dashboard)
|
|
7
|
+
# delimit --quick → Quick status (non-interactive)
|
|
8
|
+
# delimit think → Trigger deliberation
|
|
9
|
+
# delimit build → Start autonomous build loop
|
|
10
|
+
# delimit ask <query> → Ask the swarm
|
|
11
|
+
# delimit lint <spec> → Lint an API spec
|
|
12
|
+
# delimit init → Initialize governance in current repo
|
|
13
|
+
# delimit setup → Configure AI assistants
|
|
14
|
+
|
|
15
|
+
set -e
|
|
16
|
+
|
|
17
|
+
DELIMIT_HOME="${DELIMIT_HOME:-$HOME/.delimit}"
|
|
18
|
+
GATEWAY="$DELIMIT_HOME/server/ai"
|
|
19
|
+
|
|
20
|
+
# If no args, launch TUI (interactive if terminal, quick if piped)
|
|
21
|
+
if [ $# -eq 0 ]; then
|
|
22
|
+
if [ -f "$GATEWAY/tui.py" ]; then
|
|
23
|
+
if [ -t 1 ] && [ -t 0 ]; then
|
|
24
|
+
cd "$DELIMIT_HOME/server" && exec python3 -m ai.tui
|
|
25
|
+
else
|
|
26
|
+
cd "$DELIMIT_HOME/server" && exec python3 -m ai.tui --quick
|
|
27
|
+
fi
|
|
28
|
+
else
|
|
29
|
+
# Fallback to npm CLI
|
|
30
|
+
exec delimit-cli "$@"
|
|
31
|
+
fi
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Route commands
|
|
35
|
+
case "$1" in
|
|
36
|
+
--quick|-q)
|
|
37
|
+
if [ -f "$GATEWAY/tui.py" ]; then
|
|
38
|
+
cd "$DELIMIT_HOME/server" && exec python3 -m ai.tui --quick
|
|
39
|
+
else
|
|
40
|
+
exec delimit-cli status
|
|
41
|
+
fi
|
|
42
|
+
;;
|
|
43
|
+
think|deliberate)
|
|
44
|
+
shift
|
|
45
|
+
QUESTION="${*:-What should we build next based on the current ledger and signals?}"
|
|
46
|
+
echo "[Delimit OS] Triggering deliberation..."
|
|
47
|
+
cd "$DELIMIT_HOME/server" && python3 -c "
|
|
48
|
+
from ai.deliberation import deliberate
|
|
49
|
+
import json
|
|
50
|
+
result = deliberate('''$QUESTION''', mode='dialogue', max_rounds=3)
|
|
51
|
+
if 'error' in result:
|
|
52
|
+
print(f'Error: {result[\"error\"]}')
|
|
53
|
+
elif result.get('mode') == 'single_model_reflection':
|
|
54
|
+
print(f'Model: {result.get(\"model\", \"?\")}')
|
|
55
|
+
print(f'\\nAdvocate:\\n{result.get(\"advocate\", \"\")[:500]}')
|
|
56
|
+
print(f'\\nCritic:\\n{result.get(\"critic\", \"\")[:500]}')
|
|
57
|
+
print(f'\\nSynthesis:\\n{result.get(\"synthesis\", \"\")}')
|
|
58
|
+
else:
|
|
59
|
+
print(f'Verdict: {result.get(\"final_verdict\", \"no consensus\")[:500]}')
|
|
60
|
+
print(f'Rounds: {result.get(\"rounds\", 0)}')
|
|
61
|
+
" 2>&1
|
|
62
|
+
;;
|
|
63
|
+
build|loop)
|
|
64
|
+
shift
|
|
65
|
+
echo "[Delimit OS] Starting autonomous build loop..."
|
|
66
|
+
echo "Checking ledger for next task..."
|
|
67
|
+
cd "$DELIMIT_HOME/server" && python3 -c "
|
|
68
|
+
from ai.ledger_manager import get_context
|
|
69
|
+
import json
|
|
70
|
+
result = get_context()
|
|
71
|
+
items = result.get('next_up', [])
|
|
72
|
+
if items:
|
|
73
|
+
print(f'Next up: {items[0].get(\"id\", \"?\")} [{items[0].get(\"priority\", \"?\")}] {items[0].get(\"title\", \"?\")[:60]}')
|
|
74
|
+
print(f'Total open: {result.get(\"open_items\", 0)}')
|
|
75
|
+
else:
|
|
76
|
+
print('Ledger is clear — nothing to build.')
|
|
77
|
+
" 2>&1
|
|
78
|
+
;;
|
|
79
|
+
ask)
|
|
80
|
+
shift
|
|
81
|
+
QUERY="$*"
|
|
82
|
+
if [ -z "$QUERY" ]; then
|
|
83
|
+
echo "Usage: delimit ask <question>"
|
|
84
|
+
exit 1
|
|
85
|
+
fi
|
|
86
|
+
echo "[Delimit OS] Checking context..."
|
|
87
|
+
cd "$DELIMIT_HOME/server" && python3 -c "
|
|
88
|
+
from ai.ledger_manager import get_context
|
|
89
|
+
import json
|
|
90
|
+
result = get_context()
|
|
91
|
+
print(json.dumps(result, indent=2)[:2000])
|
|
92
|
+
" 2>&1
|
|
93
|
+
;;
|
|
94
|
+
status)
|
|
95
|
+
if [ -f "$GATEWAY/tui.py" ]; then
|
|
96
|
+
cd "$GATEWAY/.." && exec python3 -m ai.tui --quick
|
|
97
|
+
else
|
|
98
|
+
exec delimit-cli status
|
|
99
|
+
fi
|
|
100
|
+
;;
|
|
101
|
+
*)
|
|
102
|
+
# Pass through to delimit-cli for all other commands
|
|
103
|
+
exec delimit-cli "$@"
|
|
104
|
+
;;
|
|
105
|
+
esac
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Delimit TUI — Terminal User Interface (Phase 2 of Delimit OS).
|
|
2
|
+
|
|
3
|
+
The proprietary terminal experience. Type 'delimit' and get an OS-like
|
|
4
|
+
environment with panels for ledger, swarm, memory, and live logs.
|
|
5
|
+
|
|
6
|
+
Enterprise-ready: zero JS, pure Python, works over SSH, sub-2s boot.
|
|
7
|
+
Designed for devs who hate browser-based tools.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python -m ai.tui # Full TUI
|
|
11
|
+
python -m ai.tui --quick # Quick status (no interactive mode)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from textual.app import App, ComposeResult
|
|
15
|
+
from textual.containers import Container, Horizontal, Vertical
|
|
16
|
+
from textual.widgets import (
|
|
17
|
+
Header, Footer, Static, DataTable, Log, TabbedContent, TabPane,
|
|
18
|
+
Label, ProgressBar, Button, Input,
|
|
19
|
+
)
|
|
20
|
+
from textual.timer import Timer
|
|
21
|
+
from textual import work
|
|
22
|
+
import json
|
|
23
|
+
import time
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any, Dict, List
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ── Data loaders ─────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
LEDGER_DIR = Path.home() / ".delimit" / "ledger"
|
|
31
|
+
SWARM_DIR = Path.home() / ".delimit" / "swarm"
|
|
32
|
+
MEMORY_DIR = Path.home() / ".delimit" / "memory"
|
|
33
|
+
SESSIONS_DIR = Path.home() / ".delimit" / "sessions"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _load_ledger_items(status: str = "open", limit: int = 20) -> List[Dict]:
|
|
37
|
+
items = []
|
|
38
|
+
for fname in ("operations.jsonl", "strategy.jsonl"):
|
|
39
|
+
path = LEDGER_DIR / fname
|
|
40
|
+
if not path.exists():
|
|
41
|
+
continue
|
|
42
|
+
for line in path.read_text().strip().split("\n")[-200:]:
|
|
43
|
+
try:
|
|
44
|
+
d = json.loads(line)
|
|
45
|
+
if d.get("status") == status:
|
|
46
|
+
items.append(d)
|
|
47
|
+
except json.JSONDecodeError:
|
|
48
|
+
continue
|
|
49
|
+
items.sort(key=lambda x: (0 if x.get("priority") == "P0" else 1 if x.get("priority") == "P1" else 2))
|
|
50
|
+
return items[:limit]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _load_swarm_status() -> Dict[str, Any]:
|
|
54
|
+
registry = SWARM_DIR / "agent_registry.json"
|
|
55
|
+
if not registry.exists():
|
|
56
|
+
return {"agents": 0, "ventures": 0}
|
|
57
|
+
try:
|
|
58
|
+
data = json.loads(registry.read_text())
|
|
59
|
+
agents = data.get("agents", {})
|
|
60
|
+
ventures = set(a.get("venture", "") for a in agents.values())
|
|
61
|
+
return {
|
|
62
|
+
"agents": len(agents),
|
|
63
|
+
"ventures": len(ventures),
|
|
64
|
+
"by_venture": {v: sum(1 for a in agents.values() if a.get("venture") == v) for v in ventures},
|
|
65
|
+
}
|
|
66
|
+
except (json.JSONDecodeError, KeyError):
|
|
67
|
+
return {"agents": 0, "ventures": 0}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _load_recent_sessions(limit: int = 5) -> List[Dict]:
|
|
71
|
+
if not SESSIONS_DIR.exists():
|
|
72
|
+
return []
|
|
73
|
+
sessions = []
|
|
74
|
+
for f in sorted(SESSIONS_DIR.glob("*.json"), reverse=True)[:limit]:
|
|
75
|
+
try:
|
|
76
|
+
sessions.append(json.loads(f.read_text()))
|
|
77
|
+
except (json.JSONDecodeError, KeyError):
|
|
78
|
+
continue
|
|
79
|
+
return sessions
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ── Widgets ──────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
class LedgerPanel(Static):
|
|
85
|
+
"""Live ledger view — shows open items sorted by priority."""
|
|
86
|
+
|
|
87
|
+
def compose(self) -> ComposeResult:
|
|
88
|
+
yield DataTable(id="ledger-table")
|
|
89
|
+
|
|
90
|
+
def on_mount(self) -> None:
|
|
91
|
+
table = self.query_one("#ledger-table", DataTable)
|
|
92
|
+
table.add_columns("ID", "P", "Title", "Venture", "Type")
|
|
93
|
+
self._refresh_data()
|
|
94
|
+
self.set_interval(30, self._refresh_data)
|
|
95
|
+
|
|
96
|
+
def _refresh_data(self) -> None:
|
|
97
|
+
table = self.query_one("#ledger-table", DataTable)
|
|
98
|
+
table.clear()
|
|
99
|
+
for item in _load_ledger_items("open", 25):
|
|
100
|
+
table.add_row(
|
|
101
|
+
item.get("id", ""),
|
|
102
|
+
item.get("priority", ""),
|
|
103
|
+
item.get("title", "")[:60],
|
|
104
|
+
item.get("venture", "")[:15],
|
|
105
|
+
item.get("type", ""),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class SwarmPanel(Static):
|
|
110
|
+
"""Swarm status — agents, ventures, health."""
|
|
111
|
+
|
|
112
|
+
def compose(self) -> ComposeResult:
|
|
113
|
+
yield Static(id="swarm-content")
|
|
114
|
+
|
|
115
|
+
def on_mount(self) -> None:
|
|
116
|
+
self._refresh_data()
|
|
117
|
+
self.set_interval(15, self._refresh_data)
|
|
118
|
+
|
|
119
|
+
def _refresh_data(self) -> None:
|
|
120
|
+
content = self.query_one("#swarm-content", Static)
|
|
121
|
+
swarm = _load_swarm_status()
|
|
122
|
+
lines = [
|
|
123
|
+
f"[bold cyan]Agents:[/] {swarm['agents']} | [bold cyan]Ventures:[/] {swarm['ventures']}",
|
|
124
|
+
"",
|
|
125
|
+
]
|
|
126
|
+
for venture, count in swarm.get("by_venture", {}).items():
|
|
127
|
+
lines.append(f" [green]{venture}[/]: {count} agents")
|
|
128
|
+
content.update("\n".join(lines))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class SessionPanel(Static):
|
|
132
|
+
"""Recent sessions — handoff history."""
|
|
133
|
+
|
|
134
|
+
def compose(self) -> ComposeResult:
|
|
135
|
+
yield Static(id="session-content")
|
|
136
|
+
|
|
137
|
+
def on_mount(self) -> None:
|
|
138
|
+
self._refresh_data()
|
|
139
|
+
|
|
140
|
+
def _refresh_data(self) -> None:
|
|
141
|
+
content = self.query_one("#session-content", Static)
|
|
142
|
+
sessions = _load_recent_sessions(5)
|
|
143
|
+
if not sessions:
|
|
144
|
+
content.update("[dim]No sessions recorded yet.[/]")
|
|
145
|
+
return
|
|
146
|
+
lines = []
|
|
147
|
+
for s in sessions:
|
|
148
|
+
ts = s.get("timestamp", s.get("closed_at", ""))[:16]
|
|
149
|
+
summary = s.get("summary", "")[:80]
|
|
150
|
+
completed = len(s.get("items_completed", []))
|
|
151
|
+
lines.append(f"[dim]{ts}[/] — {summary}")
|
|
152
|
+
if completed:
|
|
153
|
+
lines.append(f" [green]✓ {completed} items completed[/]")
|
|
154
|
+
content.update("\n".join(lines))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class GovernanceBar(Static):
|
|
158
|
+
"""Top status bar — governance health at a glance."""
|
|
159
|
+
|
|
160
|
+
def compose(self) -> ComposeResult:
|
|
161
|
+
yield Static(id="gov-bar")
|
|
162
|
+
|
|
163
|
+
def on_mount(self) -> None:
|
|
164
|
+
self._refresh()
|
|
165
|
+
self.set_interval(60, self._refresh)
|
|
166
|
+
|
|
167
|
+
def _refresh(self) -> None:
|
|
168
|
+
bar = self.query_one("#gov-bar", Static)
|
|
169
|
+
ledger_count = len(_load_ledger_items("open", 999))
|
|
170
|
+
swarm = _load_swarm_status()
|
|
171
|
+
mode_file = Path.home() / ".delimit" / "enforcement_mode"
|
|
172
|
+
mode = mode_file.read_text().strip() if mode_file.exists() else "default"
|
|
173
|
+
|
|
174
|
+
bar.update(
|
|
175
|
+
f" [bold magenta]</>[/] [bold]Delimit OS[/] | "
|
|
176
|
+
f"[cyan]Ledger:[/] {ledger_count} open | "
|
|
177
|
+
f"[cyan]Swarm:[/] {swarm['agents']} agents / {swarm['ventures']} ventures | "
|
|
178
|
+
f"[cyan]Mode:[/] {mode} | "
|
|
179
|
+
f"[dim]{time.strftime('%H:%M')}[/]"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ── Main App ─────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
class DelimitOS(App):
|
|
186
|
+
"""Delimit OS — the AI developer operating system."""
|
|
187
|
+
|
|
188
|
+
CSS = """
|
|
189
|
+
Screen {
|
|
190
|
+
background: $surface;
|
|
191
|
+
}
|
|
192
|
+
#gov-bar {
|
|
193
|
+
height: 1;
|
|
194
|
+
background: $primary-background;
|
|
195
|
+
color: $text;
|
|
196
|
+
padding: 0 1;
|
|
197
|
+
}
|
|
198
|
+
TabbedContent {
|
|
199
|
+
height: 1fr;
|
|
200
|
+
}
|
|
201
|
+
DataTable {
|
|
202
|
+
height: 1fr;
|
|
203
|
+
}
|
|
204
|
+
#swarm-content, #session-content {
|
|
205
|
+
padding: 1;
|
|
206
|
+
}
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
TITLE = "Delimit OS"
|
|
210
|
+
SUB_TITLE = "AI Developer Operating System"
|
|
211
|
+
|
|
212
|
+
BINDINGS = [
|
|
213
|
+
("q", "quit", "Quit"),
|
|
214
|
+
("l", "focus_ledger", "Ledger"),
|
|
215
|
+
("s", "focus_swarm", "Swarm"),
|
|
216
|
+
("h", "focus_sessions", "History"),
|
|
217
|
+
("r", "refresh", "Refresh"),
|
|
218
|
+
("t", "think", "Think"),
|
|
219
|
+
("b", "build", "Build"),
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
def compose(self) -> ComposeResult:
|
|
223
|
+
yield GovernanceBar()
|
|
224
|
+
with TabbedContent():
|
|
225
|
+
with TabPane("Ledger", id="tab-ledger"):
|
|
226
|
+
yield LedgerPanel()
|
|
227
|
+
with TabPane("Swarm", id="tab-swarm"):
|
|
228
|
+
yield SwarmPanel()
|
|
229
|
+
with TabPane("Sessions", id="tab-sessions"):
|
|
230
|
+
yield SessionPanel()
|
|
231
|
+
yield Footer()
|
|
232
|
+
|
|
233
|
+
def action_focus_ledger(self) -> None:
|
|
234
|
+
self.query_one(TabbedContent).active = "tab-ledger"
|
|
235
|
+
|
|
236
|
+
def action_focus_swarm(self) -> None:
|
|
237
|
+
self.query_one(TabbedContent).active = "tab-swarm"
|
|
238
|
+
|
|
239
|
+
def action_focus_sessions(self) -> None:
|
|
240
|
+
self.query_one(TabbedContent).active = "tab-sessions"
|
|
241
|
+
|
|
242
|
+
def action_refresh(self) -> None:
|
|
243
|
+
for panel in self.query(LedgerPanel):
|
|
244
|
+
panel._refresh_data()
|
|
245
|
+
for panel in self.query(SwarmPanel):
|
|
246
|
+
panel._refresh_data()
|
|
247
|
+
for panel in self.query(SessionPanel):
|
|
248
|
+
panel._refresh_data()
|
|
249
|
+
self.query_one(GovernanceBar)._refresh()
|
|
250
|
+
|
|
251
|
+
def action_think(self) -> None:
|
|
252
|
+
self.notify("Deliberation triggered — check ledger for results", title="Think")
|
|
253
|
+
|
|
254
|
+
def action_build(self) -> None:
|
|
255
|
+
self.notify("Build loop started — swarm dispatching tasks", title="Build")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def main():
|
|
259
|
+
"""Entry point for 'delimit' command."""
|
|
260
|
+
import sys
|
|
261
|
+
if "--quick" in sys.argv:
|
|
262
|
+
# Quick status mode — no interactive TUI
|
|
263
|
+
from rich.console import Console
|
|
264
|
+
from rich.table import Table
|
|
265
|
+
|
|
266
|
+
console = Console()
|
|
267
|
+
console.print("\n[bold magenta]</>[/] [bold]Delimit OS[/]\n")
|
|
268
|
+
|
|
269
|
+
swarm = _load_swarm_status()
|
|
270
|
+
items = _load_ledger_items("open", 10)
|
|
271
|
+
|
|
272
|
+
console.print(f"[cyan]Swarm:[/] {swarm['agents']} agents across {swarm['ventures']} ventures")
|
|
273
|
+
console.print(f"[cyan]Ledger:[/] {len(items)} open items\n")
|
|
274
|
+
|
|
275
|
+
if items:
|
|
276
|
+
table = Table(title="Open Items")
|
|
277
|
+
table.add_column("ID", style="dim")
|
|
278
|
+
table.add_column("P", style="bold")
|
|
279
|
+
table.add_column("Title")
|
|
280
|
+
table.add_column("Venture", style="green")
|
|
281
|
+
for item in items[:10]:
|
|
282
|
+
table.add_row(
|
|
283
|
+
item.get("id", ""),
|
|
284
|
+
item.get("priority", ""),
|
|
285
|
+
item.get("title", "")[:60],
|
|
286
|
+
item.get("venture", "")[:15],
|
|
287
|
+
)
|
|
288
|
+
console.print(table)
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
app = DelimitOS()
|
|
292
|
+
app.run()
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
if __name__ == "__main__":
|
|
296
|
+
main()
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "delimit-cli",
|
|
3
3
|
"mcpName": "io.github.delimit-ai/delimit-mcp-server",
|
|
4
|
-
"version": "4.0.
|
|
4
|
+
"version": "4.0.6",
|
|
5
5
|
"description": "Unify Claude Code, Codex, Cursor, and Gemini CLI with persistent context, governance, and multi-model debate.",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"files": [
|