delimit-cli 4.1.53 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +34 -3
  3. package/bin/delimit-cli.js +150 -2
  4. package/bin/delimit-setup.js +22 -7
  5. package/gateway/ai/agent_dispatch.py +79 -0
  6. package/gateway/ai/daily_digest.py +386 -0
  7. package/gateway/ai/ledger_manager.py +32 -0
  8. package/gateway/ai/license_core.py +2 -0
  9. package/gateway/ai/notify.py +17 -11
  10. package/gateway/ai/reddit_proxy.py +28 -9
  11. package/gateway/ai/sensing/__init__.py +35 -0
  12. package/gateway/ai/sensing/schema.py +107 -0
  13. package/gateway/ai/sensing/signal_store.py +348 -0
  14. package/gateway/ai/server.py +419 -6
  15. package/gateway/ai/supabase_sync.py +308 -0
  16. package/gateway/ai/work_order.py +216 -0
  17. package/gateway/ai/workers/__init__.py +32 -0
  18. package/gateway/ai/workers/base.py +154 -0
  19. package/gateway/ai/workers/executor.py +861 -0
  20. package/gateway/ai/workers/outreach_drafter.py +161 -0
  21. package/gateway/ai/workers/pr_drafter.py +148 -0
  22. package/lib/ai-sbom-engine.js +154 -0
  23. package/lib/trust-page-engine.js +179 -0
  24. package/lib/wrap-engine.js +431 -0
  25. package/package.json +14 -1
  26. package/adapters/codex-security.js +0 -64
  27. package/adapters/codex-skill.js +0 -78
  28. package/adapters/cursor-rules.js +0 -73
  29. package/gateway/ai/continuity.py +0 -462
  30. package/gateway/ai/inbox_daemon_runner.py +0 -217
  31. package/gateway/ai/loop_engine.py +0 -1303
  32. package/gateway/ai/social_cache.py +0 -341
  33. package/gateway/ai/social_daemon.py +0 -483
  34. package/gateway/ai/tweet_corpus_schema.sql +0 -76
  35. package/scripts/crosspost_devto.py +0 -304
  36. package/scripts/demo-v420-clean.sh +0 -267
  37. package/scripts/demo-v420-deliberation.sh +0 -217
  38. package/scripts/demo-v420.sh +0 -55
  39. package/scripts/sync-gateway.sh +0 -112
@@ -0,0 +1,431 @@
1
+ // lib/wrap-engine.js
2
+ //
3
+ // LED-1048: `delimit wrap` — Surface 1 CLI-pipe extension
4
+ // LED-1052: Kill Switch + cross-model handoff extension
5
+ //
6
+ // Runs an arbitrary command (typically `claude -p` or `cursor` or `codex`),
7
+ // snapshots repo state before/after, runs governance gates on the diff,
8
+ // emits a signed attestation JSON + replay URL reference.
9
+ //
10
+ // Advisory-first: exit 0 unless --enforce is set AND gates fail.
11
+ // Cross-model-agnostic: the wrapped command is arbitrary, not bound to Claude.
12
+ //
13
+ // Kill Switch (LED-1052): --max-time <seconds> caps wall-clock; on SIGKILL
14
+ // the attestation emitted is typed as kind=liability_incident and includes
15
+ // a handoff_suggestion field pointing the user at an alternative producer.
16
+
17
+ const { spawn, spawnSync, execSync } = require('child_process');
18
+ const crypto = require('crypto');
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const os = require('os');
22
+
23
+ // ----------------------------------------------------------------------------
24
+ // Git helpers — snapshot before/after a wrapped command
25
+ // ----------------------------------------------------------------------------
26
+
27
+ function safeExec(cmd, opts = {}) {
28
+ try {
29
+ return execSync(cmd, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], ...opts }).trim();
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ function getRepoRoot(cwd) {
36
+ return safeExec('git rev-parse --show-toplevel', { cwd }) || cwd;
37
+ }
38
+
39
+ function getCurrentHead(cwd) {
40
+ return safeExec('git rev-parse HEAD', { cwd });
41
+ }
42
+
43
+ function getDirtyFiles(cwd) {
44
+ // Don't use safeExec because .trim() mangles the leading-space porcelain format.
45
+ try {
46
+ const raw = execSync('git status --porcelain', { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
47
+ // Porcelain format: XY<space><path> (XY is always 2 chars, e.g. " M", "??", "MM")
48
+ return raw.split('\n').filter(Boolean).map(l => l.slice(3));
49
+ } catch {
50
+ return [];
51
+ }
52
+ }
53
+
54
+ function getUnifiedDiff(cwd, fromHead) {
55
+ if (!fromHead) return '';
56
+ // Diff against the snapshot: tracked changes + untracked files (as if added)
57
+ return safeExec(`git diff ${fromHead} -- .`, { cwd }) || '';
58
+ }
59
+
60
+ // ----------------------------------------------------------------------------
61
+ // Governance gate composition — reuse existing CLI subcommands where possible
62
+ // ----------------------------------------------------------------------------
63
+
64
+ function detectOpenAPISpecChanges(changedFiles, cwd) {
65
+ // Simple heuristic: files matching openapi*.yaml / openapi*.json / swagger.*
66
+ const specs = changedFiles.filter(f => {
67
+ const base = path.basename(f).toLowerCase();
68
+ return /(^openapi|\.openapi|swagger)\.(ya?ml|json)$/.test(base) || base === 'openapi.yaml';
69
+ });
70
+ return specs;
71
+ }
72
+
73
+ function runDelimitCLI(args, cwd) {
74
+ // Invoke the sibling CLI entry point directly to avoid PATH assumptions.
75
+ const cliPath = path.join(__dirname, '..', 'bin', 'delimit-cli.js');
76
+ try {
77
+ const result = spawnSync('node', [cliPath, ...args, '--json'], {
78
+ cwd,
79
+ encoding: 'utf-8',
80
+ timeout: 60000,
81
+ stdio: ['ignore', 'pipe', 'pipe'],
82
+ });
83
+ const stdout = result.stdout || '';
84
+ const stderr = result.stderr || '';
85
+ let parsed = null;
86
+ try {
87
+ // Find last JSON object in stdout (cli may print banner before)
88
+ const m = stdout.match(/\{[\s\S]*\}\s*$/);
89
+ if (m) parsed = JSON.parse(m[0]);
90
+ } catch { /* leave null */ }
91
+ return { exit: result.status ?? 1, stdout, stderr, parsed };
92
+ } catch (e) {
93
+ return { exit: 1, stdout: '', stderr: String(e), parsed: null };
94
+ }
95
+ }
96
+
97
+ function runTestSmoke(cwd) {
98
+ // Minimal heuristic: if pytest is available and tests/ exists, run it.
99
+ // If package.json has a test script, run `npm test`.
100
+ // Time-bounded. Advisory. Never the block criterion alone.
101
+ const results = [];
102
+ const pkgPath = path.join(cwd, 'package.json');
103
+ if (fs.existsSync(pkgPath)) {
104
+ try {
105
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
106
+ if (pkg.scripts && pkg.scripts.test && pkg.scripts.test !== 'echo "Error: no test specified" && exit 1') {
107
+ const r = spawnSync('npm', ['test', '--silent'], { cwd, encoding: 'utf-8', timeout: 120000, stdio: ['ignore', 'pipe', 'pipe'] });
108
+ results.push({ runner: 'npm test', exit: r.status ?? 1, stdout: (r.stdout || '').slice(-2000), stderr: (r.stderr || '').slice(-1000) });
109
+ }
110
+ } catch { /* ignore */ }
111
+ }
112
+ if (fs.existsSync(path.join(cwd, 'tests')) || fs.existsSync(path.join(cwd, 'pytest.ini')) || fs.existsSync(path.join(cwd, 'pyproject.toml'))) {
113
+ const r = spawnSync('python3', ['-m', 'pytest', '--tb=short', '-q'], { cwd, encoding: 'utf-8', timeout: 180000, stdio: ['ignore', 'pipe', 'pipe'] });
114
+ results.push({ runner: 'pytest', exit: r.status ?? 1, stdout: (r.stdout || '').slice(-2000), stderr: (r.stderr || '').slice(-1000) });
115
+ }
116
+ return results;
117
+ }
118
+
119
+ // ----------------------------------------------------------------------------
120
+ // Attestation bundling + signing
121
+ // ----------------------------------------------------------------------------
122
+
123
+ function computeAttestationId(bundle) {
124
+ const canonical = JSON.stringify(bundle, Object.keys(bundle).sort());
125
+ const hash = crypto.createHash('sha256').update(canonical).digest('hex');
126
+ return 'att_' + hash.slice(0, 16);
127
+ }
128
+
129
+ function loadOrCreateHmacKey() {
130
+ // Local HMAC key for attestation signing.
131
+ // Cloud-sync + verifiable signature is a Pro/Premium feature (deferred MVP).
132
+ const keyPath = path.join(os.homedir(), '.delimit', 'wrap-hmac.key');
133
+ if (fs.existsSync(keyPath)) return fs.readFileSync(keyPath);
134
+ const key = crypto.randomBytes(32);
135
+ fs.mkdirSync(path.dirname(keyPath), { recursive: true });
136
+ fs.writeFileSync(keyPath, key, { mode: 0o600 });
137
+ return key;
138
+ }
139
+
140
+ function signAttestation(bundle) {
141
+ const key = loadOrCreateHmacKey();
142
+ const canonical = JSON.stringify(bundle, Object.keys(bundle).sort());
143
+ return crypto.createHmac('sha256', key).update(canonical).digest('hex');
144
+ }
145
+
146
+ // ----------------------------------------------------------------------------
147
+ // Quota enforcement — free tier 3 lifetime, Pro unlimited
148
+ // ----------------------------------------------------------------------------
149
+
150
+ function checkQuota() {
151
+ const counterPath = path.join(os.homedir(), '.delimit', 'wrap-lifetime-count');
152
+ const licensePath = path.join(os.homedir(), '.delimit', 'license.json');
153
+ let tier = 'free';
154
+ try {
155
+ if (fs.existsSync(licensePath)) {
156
+ const lic = JSON.parse(fs.readFileSync(licensePath, 'utf-8'));
157
+ if (lic.valid && ['pro', 'premium', 'enterprise'].includes(lic.tier)) {
158
+ tier = lic.tier;
159
+ }
160
+ }
161
+ } catch { /* treat as free */ }
162
+ if (tier !== 'free') return { ok: true, tier, count: null };
163
+ let count = 0;
164
+ try {
165
+ if (fs.existsSync(counterPath)) count = parseInt(fs.readFileSync(counterPath, 'utf-8').trim(), 10) || 0;
166
+ } catch { /* start at 0 */ }
167
+ return { ok: count < 3, tier: 'free', count, limit: 3 };
168
+ }
169
+
170
+ function incrementQuota() {
171
+ const counterPath = path.join(os.homedir(), '.delimit', 'wrap-lifetime-count');
172
+ let count = 0;
173
+ try {
174
+ if (fs.existsSync(counterPath)) count = parseInt(fs.readFileSync(counterPath, 'utf-8').trim(), 10) || 0;
175
+ } catch {}
176
+ count += 1;
177
+ fs.mkdirSync(path.dirname(counterPath), { recursive: true });
178
+ fs.writeFileSync(counterPath, String(count));
179
+ return count;
180
+ }
181
+
182
+ // ----------------------------------------------------------------------------
183
+ // Persistence — save attestation to local ledger
184
+ // ----------------------------------------------------------------------------
185
+
186
+ function saveAttestation(att) {
187
+ const dir = path.join(os.homedir(), '.delimit', 'attestations');
188
+ fs.mkdirSync(dir, { recursive: true });
189
+ const file = path.join(dir, `${att.id}.json`);
190
+ fs.writeFileSync(file, JSON.stringify(att, null, 2));
191
+ return file;
192
+ }
193
+
194
+ function replayUrl(attId) {
195
+ // Public replay surface is served by app.delimit.ai (reuses the trust-page
196
+ // pattern from LED-1018). For MVP, just returns the URL; rendering + upload
197
+ // is the Pro-tier feature and not part of this MVP.
198
+ return `https://delimit.ai/att/${attId}`;
199
+ }
200
+
201
+ // ----------------------------------------------------------------------------
202
+ // Main wrap flow
203
+ // ----------------------------------------------------------------------------
204
+
205
+ // LED-1052: map a wrapped command's base binary to a handoff suggestion
206
+ // for the remaining producers. Advisory only — prints the command a user
207
+ // could run to resume with a different model.
208
+ function suggestHandoff(rawCmd) {
209
+ const bin = (rawCmd && rawCmd[0]) || '';
210
+ const base = path.basename(bin).toLowerCase();
211
+ const prompt = extractPromptArg(rawCmd);
212
+ const fallbacks = {
213
+ claude: ['codex', 'gemini', 'cursor'],
214
+ 'claude-code': ['codex', 'gemini', 'cursor'],
215
+ cursor: ['claude', 'codex', 'gemini'],
216
+ 'cursor-cli': ['claude', 'codex', 'gemini'],
217
+ aider: ['claude', 'codex', 'gemini'],
218
+ codex: ['claude', 'gemini', 'cursor'],
219
+ gemini: ['claude', 'codex', 'cursor'],
220
+ };
221
+ const key = Object.keys(fallbacks).find(k => base.includes(k));
222
+ if (!key) return null;
223
+ const alt = fallbacks[key][0];
224
+ const altPrompt = prompt || '<your-goal>';
225
+ return {
226
+ kill_source: key,
227
+ handoff_target: alt,
228
+ suggested_command: `delimit wrap -- ${alt} -p "${altPrompt}"`,
229
+ alternates: fallbacks[key],
230
+ };
231
+ }
232
+
233
+ function extractPromptArg(rawCmd) {
234
+ // Best-effort scrape: -p <prompt> or --prompt <prompt>
235
+ if (!rawCmd) return null;
236
+ for (let i = 0; i < rawCmd.length - 1; i++) {
237
+ if (rawCmd[i] === '-p' || rawCmd[i] === '--prompt') return rawCmd[i + 1];
238
+ }
239
+ return null;
240
+ }
241
+
242
+ // LED-1052: spawn a child with wall-clock timeout + SIGKILL on breach.
243
+ // Returns { status, killed_by_timeout, ms }.
244
+ function spawnWithKillSwitch(bin, args, spawnOpts, maxTimeSeconds) {
245
+ return new Promise((resolve) => {
246
+ const child = spawn(bin, args, { ...spawnOpts, stdio: 'inherit' });
247
+ const started = Date.now();
248
+ let killed = false;
249
+ const timer = maxTimeSeconds && maxTimeSeconds > 0
250
+ ? setTimeout(() => {
251
+ killed = true;
252
+ try { child.kill('SIGKILL'); } catch { /* ignore */ }
253
+ }, maxTimeSeconds * 1000)
254
+ : null;
255
+ child.on('close', (code, signal) => {
256
+ if (timer) clearTimeout(timer);
257
+ resolve({
258
+ status: (code !== null ? code : (signal === 'SIGKILL' ? 137 : 1)),
259
+ killed_by_timeout: killed,
260
+ signal,
261
+ ms: Date.now() - started,
262
+ });
263
+ });
264
+ child.on('error', (err) => {
265
+ if (timer) clearTimeout(timer);
266
+ resolve({ status: 1, killed_by_timeout: false, error: String(err), ms: Date.now() - started });
267
+ });
268
+ });
269
+ }
270
+
271
+ async function runWrap(rawCmd, options = {}) {
272
+ const {
273
+ enforce = false,
274
+ deliberate = false,
275
+ attest = true,
276
+ cwd = process.cwd(),
277
+ maxTimeSeconds = 0, // LED-1052: 0 disables kill switch
278
+ } = options;
279
+
280
+ const repoRoot = getRepoRoot(cwd);
281
+ const isGitRepo = !!safeExec('git rev-parse --is-inside-work-tree', { cwd: repoRoot });
282
+
283
+ // Quota check (only if attestation will be emitted)
284
+ let quotaInfo = null;
285
+ if (attest) {
286
+ quotaInfo = checkQuota();
287
+ if (!quotaInfo.ok) {
288
+ return {
289
+ exit: 1,
290
+ error: 'quota_exceeded',
291
+ message: `Free tier: ${quotaInfo.count}/${quotaInfo.limit} lifetime attestations used. Upgrade to Pro ($10/mo) for unlimited — visit https://delimit.ai/pricing`,
292
+ tier: quotaInfo.tier,
293
+ };
294
+ }
295
+ }
296
+
297
+ // Snapshot before
298
+ const beforeHead = isGitRepo ? getCurrentHead(repoRoot) : null;
299
+ const beforeDirty = isGitRepo ? getDirtyFiles(repoRoot) : [];
300
+ const startedAt = new Date().toISOString();
301
+
302
+ // Execute the wrapped command
303
+ // The wrapped command runs in the user's shell so `claude -p "..."` / `cursor edit` / etc. work natively.
304
+ // LED-1052: if maxTimeSeconds > 0, use async spawnWithKillSwitch; otherwise spawnSync for back-compat.
305
+ let wrappedExit;
306
+ let killedByTimeout = false;
307
+ let killSignal = null;
308
+ if (maxTimeSeconds > 0) {
309
+ const res = await spawnWithKillSwitch(rawCmd[0], rawCmd.slice(1), { cwd: repoRoot, shell: false }, maxTimeSeconds);
310
+ wrappedExit = res.status;
311
+ killedByTimeout = res.killed_by_timeout;
312
+ killSignal = res.signal || null;
313
+ } else {
314
+ const child = spawnSync(rawCmd[0], rawCmd.slice(1), { cwd: repoRoot, stdio: 'inherit', shell: false });
315
+ wrappedExit = child.status ?? 1;
316
+ }
317
+ const completedAt = new Date().toISOString();
318
+
319
+ // Snapshot after
320
+ const afterHead = isGitRepo ? getCurrentHead(repoRoot) : null;
321
+ const afterDirty = isGitRepo ? getDirtyFiles(repoRoot) : [];
322
+ const changedFiles = isGitRepo
323
+ ? Array.from(new Set([...beforeDirty, ...afterDirty]))
324
+ : [];
325
+
326
+ // Governance chain
327
+ const governance = { gates: [], violations: [], advisory: !enforce };
328
+
329
+ // 1) OpenAPI spec changes → delimit lint / diff
330
+ const specChanges = detectOpenAPISpecChanges(changedFiles, repoRoot);
331
+ if (specChanges.length > 0) {
332
+ governance.gates.push({ name: 'openapi_detect', result: 'ran', specs: specChanges });
333
+ // Try running delimit lint on each (zero-spec mode against baseline)
334
+ for (const spec of specChanges) {
335
+ const lintResult = runDelimitCLI(['lint'], path.dirname(path.join(repoRoot, spec)));
336
+ governance.gates.push({
337
+ name: 'delimit_lint',
338
+ spec,
339
+ exit: lintResult.exit,
340
+ summary: (lintResult.stdout || '').slice(-500),
341
+ });
342
+ if (lintResult.exit !== 0) governance.violations.push(`lint failed on ${spec}`);
343
+ }
344
+ }
345
+
346
+ // 2) Test smoke
347
+ const testResults = runTestSmoke(repoRoot);
348
+ for (const t of testResults) {
349
+ governance.gates.push({ name: 'test_smoke', runner: t.runner, exit: t.exit });
350
+ if (t.exit !== 0) governance.violations.push(`${t.runner} failed`);
351
+ }
352
+ if (testResults.length === 0) {
353
+ governance.gates.push({ name: 'test_smoke', result: 'no_tests_detected' });
354
+ }
355
+
356
+ // 3) Multi-model deliberate (optional)
357
+ if (deliberate) {
358
+ governance.gates.push({ name: 'deliberate', result: 'deferred', note: 'use `delimit deliberate` standalone for multi-model verdict — v1 wrap emits local attestation only' });
359
+ }
360
+
361
+ // Build attestation bundle
362
+ // LED-1052: if killed by timeout, attestation is typed as liability_incident
363
+ const kind = killedByTimeout ? 'liability_incident' : 'merge_attestation';
364
+ const handoffSuggestion = killedByTimeout ? suggestHandoff(rawCmd) : null;
365
+ const bundle = {
366
+ schema: 'delimit.attestation.v1',
367
+ kind,
368
+ wrapped_command: rawCmd.join(' '),
369
+ repo_root: repoRoot,
370
+ is_git_repo: isGitRepo,
371
+ before_head: beforeHead,
372
+ after_head: afterHead,
373
+ started_at: startedAt,
374
+ completed_at: completedAt,
375
+ wrapped_exit: wrappedExit,
376
+ changed_files: changedFiles,
377
+ governance,
378
+ delimit_wrap_version: '1.1.0',
379
+ ...(killedByTimeout ? {
380
+ kill_switch: {
381
+ kind: 'timeout',
382
+ max_time_seconds: maxTimeSeconds,
383
+ signal: killSignal,
384
+ handoff_suggestion: handoffSuggestion,
385
+ },
386
+ } : {}),
387
+ };
388
+ const attId = computeAttestationId(bundle);
389
+ const signature = signAttestation(bundle);
390
+ const attestation = {
391
+ id: attId,
392
+ bundle,
393
+ signature,
394
+ signature_alg: 'HMAC-SHA256',
395
+ };
396
+
397
+ let filePath = null;
398
+ if (attest) {
399
+ filePath = saveAttestation(attestation);
400
+ if (quotaInfo && quotaInfo.tier === 'free') incrementQuota();
401
+ }
402
+
403
+ const hasViolations = governance.violations.length > 0;
404
+ const shouldFail = enforce && hasViolations;
405
+
406
+ return {
407
+ exit: shouldFail ? 2 : wrappedExit,
408
+ attestation_id: attId,
409
+ attestation_path: filePath,
410
+ replay_url: replayUrl(attId),
411
+ kind,
412
+ violations: governance.violations,
413
+ gates: governance.gates,
414
+ wrapped_exit: wrappedExit,
415
+ advisory: !enforce,
416
+ tier: quotaInfo ? quotaInfo.tier : null,
417
+ // LED-1052: kill-switch metadata surfaced at the top level for CLI rendering
418
+ ...(killedByTimeout ? {
419
+ killed_by_timeout: true,
420
+ handoff_suggestion: handoffSuggestion,
421
+ } : {}),
422
+ };
423
+ }
424
+
425
+ module.exports = {
426
+ runWrap,
427
+ computeAttestationId,
428
+ signAttestation,
429
+ checkQuota,
430
+ replayUrl,
431
+ };
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.1.53",
4
+ "version": "4.3.0",
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": [
@@ -11,17 +11,30 @@
11
11
  "gateway/",
12
12
  "!gateway/ai/social_target.py",
13
13
  "!gateway/ai/social.py",
14
+ "!gateway/ai/social_daemon.py",
15
+ "!gateway/ai/social_cache.py",
14
16
  "!gateway/ai/founding_users.py",
15
17
  "!gateway/ai/inbox_daemon.py",
18
+ "!gateway/ai/inbox_daemon_runner.py",
16
19
  "!gateway/ai/deliberation.py",
17
20
  "!gateway/ai/dv_mention_tracker.py",
18
21
  "!gateway/ai/sensor_twttr.py",
19
22
  "!gateway/ai/tweet_corpus.py",
23
+ "!gateway/ai/tweet_corpus_schema.sql",
20
24
  "!gateway/ai/twttr241_budget.py",
21
25
  "!gateway/ai/wireintel_x.py",
22
26
  "!gateway/ai/content_intel.py",
23
27
  "!gateway/ai/loop_daemon.py",
28
+ "!gateway/ai/loop_engine.py",
24
29
  "scripts/",
30
+ "!scripts/crosspost_devto.py",
31
+ "!scripts/repo_targeting.py",
32
+ "!scripts/outreach_report_generator.py",
33
+ "!scripts/demo-v420.sh",
34
+ "!scripts/demo-v420-clean.sh",
35
+ "!scripts/demo-v420-deliberation.sh",
36
+ "!scripts/sync-gateway.sh",
37
+ "!gateway/ai/continuity.py",
25
38
  "server.json",
26
39
  "README.md",
27
40
  "LICENSE",
@@ -1,64 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Delimit Security Skill for Codex CLI
4
- *
5
- * Validates that Codex-generated code doesn't introduce security anti-patterns.
6
- * Runs as a security validation skill.
7
- */
8
-
9
- const fs = require('fs');
10
- const path = require('path');
11
-
12
- const SECURITY_PATTERNS = [
13
- { pattern: /eval\s*\(/g, severity: 'high', message: 'eval() usage detected — potential code injection' },
14
- { pattern: /exec\s*\(/g, severity: 'medium', message: 'exec() usage — verify input sanitization' },
15
- { pattern: /shell\s*=\s*True/g, severity: 'high', message: 'subprocess with shell=True — command injection risk' },
16
- { pattern: /dangerouslySetInnerHTML/g, severity: 'medium', message: 'dangerouslySetInnerHTML — XSS risk' },
17
- { pattern: /password\s*=\s*["'][^"']+["']/gi, severity: 'high', message: 'Hardcoded password detected' },
18
- { pattern: /api[_-]?key\s*=\s*["'][A-Za-z0-9]{10,}["']/gi, severity: 'high', message: 'Hardcoded API key detected' },
19
- ];
20
-
21
- function checkSecurity(context) {
22
- const code = context.code || context.content || '';
23
- if (!code) return { status: 'clean', findings: [] };
24
-
25
- const findings = [];
26
- for (const { pattern, severity, message } of SECURITY_PATTERNS) {
27
- const matches = code.match(pattern);
28
- if (matches) {
29
- findings.push({ severity, message, count: matches.length });
30
- }
31
- }
32
-
33
- // Audit
34
- try {
35
- const auditDir = path.join(process.env.HOME || '', '.delimit', 'audit');
36
- fs.mkdirSync(auditDir, { recursive: true });
37
- const record = {
38
- timestamp: new Date().toISOString(),
39
- source: 'codex-security',
40
- findings_count: findings.length,
41
- high_count: findings.filter(f => f.severity === 'high').length,
42
- };
43
- const auditFile = path.join(auditDir, `${new Date().toISOString().split('T')[0]}.jsonl`);
44
- fs.appendFileSync(auditFile, JSON.stringify(record) + '\n');
45
- } catch {}
46
-
47
- const hasHigh = findings.some(f => f.severity === 'high');
48
- return {
49
- status: hasHigh ? 'flagged' : findings.length > 0 ? 'warnings' : 'clean',
50
- findings,
51
- };
52
- }
53
-
54
- const context = process.argv[2] ? JSON.parse(process.argv[2]) : {};
55
- const result = checkSecurity(context);
56
-
57
- if (result.status === 'flagged') {
58
- for (const f of result.findings) {
59
- console.error(`[Delimit Security] ${f.severity.toUpperCase()}: ${f.message}`);
60
- }
61
- process.exit(1);
62
- }
63
-
64
- process.exit(0);
@@ -1,78 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Delimit Governance Skill for Codex CLI
4
- *
5
- * Runs as a validation skill triggered on pre-code-generation and pre-suggestion.
6
- * Checks governance state and policy compliance before Codex executes actions.
7
- */
8
-
9
- const fs = require('fs');
10
- const path = require('path');
11
-
12
- const DELIMIT_HOME = path.join(process.env.HOME || '', '.delimit');
13
- const MODE_FILE = path.join(DELIMIT_HOME, 'enforcement_mode');
14
-
15
- function getMode() {
16
- try {
17
- return fs.readFileSync(MODE_FILE, 'utf-8').trim();
18
- } catch {
19
- return 'guarded'; // Default
20
- }
21
- }
22
-
23
- function checkGovernance(context) {
24
- const mode = getMode();
25
- const warnings = [];
26
-
27
- // Check if governance is initialized
28
- const policiesFile = path.join(process.cwd(), '.delimit', 'policies.yml');
29
- if (!fs.existsSync(policiesFile)) {
30
- warnings.push('No .delimit/policies.yml — run: delimit init');
31
- }
32
-
33
- // Check for sensitive file access
34
- const sensitivePatterns = ['.env', 'credentials', '.ssh', 'secrets'];
35
- const target = context.target || context.file || '';
36
- for (const pattern of sensitivePatterns) {
37
- if (target.includes(pattern)) {
38
- if (mode === 'enforce') {
39
- return { status: 'blocked', reason: `Access to sensitive path: ${target}` };
40
- }
41
- warnings.push(`Accessing sensitive path: ${target}`);
42
- }
43
- }
44
-
45
- // Audit log
46
- try {
47
- const auditDir = path.join(DELIMIT_HOME, 'audit');
48
- fs.mkdirSync(auditDir, { recursive: true });
49
- const record = {
50
- timestamp: new Date().toISOString(),
51
- source: 'codex-skill',
52
- mode,
53
- context: typeof context === 'object' ? JSON.stringify(context).slice(0, 200) : String(context).slice(0, 200),
54
- warnings,
55
- };
56
- const auditFile = path.join(auditDir, `${new Date().toISOString().split('T')[0]}.jsonl`);
57
- fs.appendFileSync(auditFile, JSON.stringify(record) + '\n');
58
- } catch {}
59
-
60
- return { status: 'allowed', mode, warnings };
61
- }
62
-
63
- // Entry point — read context from stdin or args
64
- const context = process.argv[2] ? JSON.parse(process.argv[2]) : {};
65
- const result = checkGovernance(context);
66
-
67
- if (result.status === 'blocked') {
68
- console.error(`[Delimit] BLOCKED: ${result.reason}`);
69
- process.exit(1);
70
- }
71
-
72
- if (result.warnings.length > 0) {
73
- for (const w of result.warnings) {
74
- console.error(`[Delimit] Warning: ${w}`);
75
- }
76
- }
77
-
78
- process.exit(0);
@@ -1,73 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Delimit Governance Rules for Cursor
4
- *
5
- * Cursor doesn't have a hook system like Claude Code or Codex,
6
- * so governance enforcement happens server-side via MCP tool calls.
7
- * This adapter manages the .cursorrules and .cursor/rules/ files
8
- * that guide Cursor's behavior.
9
- */
10
-
11
- const fs = require('fs');
12
- const path = require('path');
13
-
14
- // LED-213: Import canonical template for cross-model parity
15
- const { getDelimitSection } = require('../lib/delimit-template');
16
-
17
- const HOME = process.env.HOME || '';
18
- const CURSOR_DIR = path.join(HOME, '.cursor');
19
- const CURSOR_RULES_DIR = path.join(CURSOR_DIR, 'rules');
20
- const CURSORRULES_FILE = path.join(HOME, '.cursorrules');
21
-
22
- /**
23
- * Install Delimit governance rules into Cursor.
24
- * Creates both .cursorrules (legacy) and .cursor/rules/delimit.md (new).
25
- */
26
- function installRules(version) {
27
- const rules = getDelimitRules(version);
28
-
29
- // Install to .cursor/rules/delimit.md (new location, Cursor 0.45+)
30
- if (fs.existsSync(CURSOR_DIR)) {
31
- fs.mkdirSync(CURSOR_RULES_DIR, { recursive: true });
32
- const rulesFile = path.join(CURSOR_RULES_DIR, 'delimit.md');
33
- fs.writeFileSync(rulesFile, rules);
34
- }
35
-
36
- return { installed: true, paths: [CURSORRULES_FILE, path.join(CURSOR_RULES_DIR, 'delimit.md')] };
37
- }
38
-
39
- /**
40
- * Remove Delimit rules from Cursor.
41
- */
42
- function uninstallRules() {
43
- const removed = [];
44
-
45
- // Remove from .cursor/rules/
46
- const rulesFile = path.join(CURSOR_RULES_DIR, 'delimit.md');
47
- if (fs.existsSync(rulesFile)) {
48
- fs.unlinkSync(rulesFile);
49
- removed.push(rulesFile);
50
- }
51
-
52
- return { removed };
53
- }
54
-
55
- function getDelimitRules(version) {
56
- // LED-213: Use canonical Consensus 123 template for Cursor parity
57
- return getDelimitSection();
58
- }
59
-
60
- module.exports = { installRules, uninstallRules, getDelimitRules };
61
-
62
- // CLI entry point
63
- if (require.main === module) {
64
- const action = process.argv[2] || 'install';
65
- const version = process.argv[3] || '3.11.9';
66
- if (action === 'install') {
67
- const result = installRules(version);
68
- console.log(`Installed Delimit rules to Cursor: ${result.paths.join(', ')}`);
69
- } else if (action === 'uninstall') {
70
- const result = uninstallRules();
71
- console.log(`Removed: ${result.removed.join(', ') || 'nothing to remove'}`);
72
- }
73
- }