delimit-cli 3.11.10 → 3.12.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.
@@ -0,0 +1,706 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * LED-202: Cross-Model Hook System
5
+ *
6
+ * Detects installed AI coding assistants (Claude Code, Codex, Gemini CLI)
7
+ * and installs Delimit governance hooks into each one's native config format.
8
+ *
9
+ * Hook commands:
10
+ * delimit hook session-start -- ledger context + gov health
11
+ * delimit hook pre-tool <name> -- lint/test checks before edits
12
+ * delimit hook pre-commit -- repo diagnostics before commits
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const { execSync } = require('child_process');
18
+ const os = require('os');
19
+
20
+ // Use process.env.HOME to allow test overrides; fall back to os.homedir()
21
+ function getHome() { return process.env.HOME || os.homedir(); }
22
+ function getDelimitHome() { return path.join(getHome(), '.delimit'); }
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Hook configuration (user-overridable via delimit.yml)
26
+ // ---------------------------------------------------------------------------
27
+
28
+ function loadHookConfig() {
29
+ const defaults = {
30
+ session_start: true,
31
+ pre_tool: true,
32
+ pre_commit: true,
33
+ deliberate_on_commit: false,
34
+ show_strategy_items: true,
35
+ };
36
+
37
+ // Check project-level delimit.yml, then global
38
+ const candidates = [
39
+ path.join(process.cwd(), 'delimit.yml'),
40
+ path.join(process.cwd(), '.delimit.yml'),
41
+ path.join(getDelimitHome(), 'delimit.yml'),
42
+ ];
43
+
44
+ for (const candidate of candidates) {
45
+ if (fs.existsSync(candidate)) {
46
+ try {
47
+ const yaml = require('js-yaml');
48
+ const doc = yaml.load(fs.readFileSync(candidate, 'utf-8'));
49
+ if (doc && doc.hooks) {
50
+ return { ...defaults, ...doc.hooks };
51
+ }
52
+ } catch { /* ignore parse errors */ }
53
+ }
54
+ }
55
+ return defaults;
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // AI tool detection
60
+ // ---------------------------------------------------------------------------
61
+
62
+ function detectAITools() {
63
+ const detected = [];
64
+
65
+ // Claude Code
66
+ const claudeSettings = path.join(getHome(), '.claude', 'settings.json');
67
+ const claudeSettingsLocal = path.join(getHome(), '.claude', 'settings.local.json');
68
+ let hasClaude = fs.existsSync(claudeSettings) || fs.existsSync(claudeSettingsLocal);
69
+ if (!hasClaude) {
70
+ try {
71
+ execSync('claude --version 2>/dev/null', { stdio: 'pipe' });
72
+ hasClaude = true;
73
+ } catch { /* not installed */ }
74
+ }
75
+ if (hasClaude) {
76
+ detected.push({
77
+ id: 'claude',
78
+ name: 'Claude Code',
79
+ configPath: claudeSettings,
80
+ format: 'claude-hooks',
81
+ });
82
+ }
83
+
84
+ // Codex CLI
85
+ const codexDir = path.join(getHome(), '.codex');
86
+ let hasCodex = fs.existsSync(codexDir);
87
+ if (!hasCodex) {
88
+ try {
89
+ execSync('codex --version 2>/dev/null', { stdio: 'pipe' });
90
+ hasCodex = true;
91
+ } catch { /* not installed */ }
92
+ }
93
+ if (hasCodex) {
94
+ detected.push({
95
+ id: 'codex',
96
+ name: 'Codex CLI',
97
+ configPath: path.join(codexDir, 'config.json'),
98
+ instructionsPath: path.join(codexDir, 'instructions.md'),
99
+ format: 'codex',
100
+ });
101
+ }
102
+
103
+ // Gemini CLI
104
+ const geminiDir = path.join(getHome(), '.gemini');
105
+ let hasGemini = fs.existsSync(geminiDir);
106
+ if (!hasGemini) {
107
+ try {
108
+ execSync('gemini --version 2>/dev/null', { stdio: 'pipe' });
109
+ hasGemini = true;
110
+ } catch { /* not installed */ }
111
+ }
112
+ if (hasGemini) {
113
+ detected.push({
114
+ id: 'gemini',
115
+ name: 'Gemini CLI',
116
+ configPath: path.join(geminiDir, 'settings.json'),
117
+ format: 'gemini-mcp',
118
+ });
119
+ }
120
+
121
+ return detected;
122
+ }
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // Hook installers per tool
126
+ // ---------------------------------------------------------------------------
127
+
128
+ /**
129
+ * Install hooks into Claude Code's ~/.claude/settings.json
130
+ * Claude Code supports native hooks: SessionStart, PreToolUse, PostToolUse, etc.
131
+ */
132
+ function installClaudeHooks(tool, hookConfig) {
133
+ const configPath = tool.configPath;
134
+ const configDir = path.dirname(configPath);
135
+ fs.mkdirSync(configDir, { recursive: true });
136
+
137
+ let config = {};
138
+ if (fs.existsSync(configPath)) {
139
+ try {
140
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
141
+ } catch { config = {}; }
142
+ }
143
+
144
+ if (!config.hooks) {
145
+ config.hooks = {};
146
+ }
147
+
148
+ const npxCmd = 'npx delimit-cli';
149
+ const changes = [];
150
+
151
+ // SessionStart hook
152
+ if (hookConfig.session_start) {
153
+ const sessionHook = {
154
+ type: 'command',
155
+ command: `${npxCmd} hook session-start`,
156
+ };
157
+ if (!config.hooks.SessionStart) {
158
+ config.hooks.SessionStart = [];
159
+ }
160
+ // Check if already installed
161
+ const existing = config.hooks.SessionStart.find(
162
+ h => h.command && h.command.includes('delimit-cli hook session-start')
163
+ );
164
+ if (!existing) {
165
+ config.hooks.SessionStart.push(sessionHook);
166
+ changes.push('SessionStart');
167
+ }
168
+ }
169
+
170
+ // PreToolUse hook for file edits
171
+ if (hookConfig.pre_tool) {
172
+ const preToolHook = {
173
+ type: 'command',
174
+ command: `${npxCmd} hook pre-tool`,
175
+ matcher: 'Edit|Write|Bash',
176
+ };
177
+ if (!config.hooks.PreToolUse) {
178
+ config.hooks.PreToolUse = [];
179
+ }
180
+ const existing = config.hooks.PreToolUse.find(
181
+ h => h.command && h.command.includes('delimit-cli hook pre-tool')
182
+ );
183
+ if (!existing) {
184
+ config.hooks.PreToolUse.push(preToolHook);
185
+ changes.push('PreToolUse');
186
+ }
187
+ }
188
+
189
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
190
+ return changes;
191
+ }
192
+
193
+ /**
194
+ * Install hooks for Codex CLI.
195
+ * Codex uses instructions.md for session-start equivalent and config.json for settings.
196
+ * We add governance instructions and a pre-commit hook reference.
197
+ */
198
+ function installCodexHooks(tool, hookConfig) {
199
+ const changes = [];
200
+ const codexDir = path.dirname(tool.configPath);
201
+ fs.mkdirSync(codexDir, { recursive: true });
202
+
203
+ // Codex instructions.md -- acts as the session-start equivalent
204
+ if (hookConfig.session_start) {
205
+ const instructionsPath = tool.instructionsPath || path.join(codexDir, 'instructions.md');
206
+ const delimitBlock = `<!-- delimit:hooks-start -->
207
+ ## Delimit Governance
208
+
209
+ On every session start, run this command to check governance status:
210
+ \`\`\`
211
+ npx delimit-cli hook session-start
212
+ \`\`\`
213
+
214
+ Before committing changes, run:
215
+ \`\`\`
216
+ npx delimit-cli hook pre-commit
217
+ \`\`\`
218
+
219
+ After editing OpenAPI specs, run:
220
+ \`\`\`
221
+ npx delimit-cli hook pre-tool Edit
222
+ \`\`\`
223
+ <!-- delimit:hooks-end -->`;
224
+
225
+ let content = '';
226
+ if (fs.existsSync(instructionsPath)) {
227
+ content = fs.readFileSync(instructionsPath, 'utf-8');
228
+ }
229
+
230
+ if (content.includes('delimit:hooks-start')) {
231
+ // Replace existing block
232
+ content = content.replace(
233
+ /<!-- delimit:hooks-start -->[\s\S]*?<!-- delimit:hooks-end -->/,
234
+ delimitBlock
235
+ );
236
+ } else {
237
+ content = content ? content + '\n\n' + delimitBlock : delimitBlock;
238
+ }
239
+
240
+ fs.writeFileSync(instructionsPath, content);
241
+ changes.push('instructions.md');
242
+ }
243
+
244
+ // Codex config.json -- add hook commands
245
+ let config = {};
246
+ if (fs.existsSync(tool.configPath)) {
247
+ try {
248
+ config = JSON.parse(fs.readFileSync(tool.configPath, 'utf-8'));
249
+ } catch { config = {}; }
250
+ }
251
+
252
+ if (!config.hooks) {
253
+ config.hooks = {};
254
+ }
255
+
256
+ if (hookConfig.pre_commit && !config.hooks['pre-commit']) {
257
+ config.hooks['pre-commit'] = 'npx delimit-cli hook pre-commit';
258
+ changes.push('pre-commit hook');
259
+ }
260
+
261
+ fs.writeFileSync(tool.configPath, JSON.stringify(config, null, 2));
262
+ return changes;
263
+ }
264
+
265
+ /**
266
+ * Install hooks for Gemini CLI.
267
+ * Gemini CLI uses MCP (already handled by setup) but we add governance
268
+ * instructions to settings.json and a GEMINI.md equivalent.
269
+ */
270
+ function installGeminiHooks(tool, hookConfig) {
271
+ const changes = [];
272
+ const geminiDir = path.dirname(tool.configPath);
273
+ fs.mkdirSync(geminiDir, { recursive: true });
274
+
275
+ // Update settings.json with custom instructions
276
+ let config = {};
277
+ if (fs.existsSync(tool.configPath)) {
278
+ try {
279
+ config = JSON.parse(fs.readFileSync(tool.configPath, 'utf-8'));
280
+ } catch { config = {}; }
281
+ }
282
+
283
+ const govInstructions = [
284
+ 'Delimit governance is active.',
285
+ 'On session start, run: npx delimit-cli hook session-start',
286
+ 'Before committing, run: npx delimit-cli hook pre-commit',
287
+ 'After editing API specs, run: npx delimit-cli hook pre-tool Edit',
288
+ 'Call delimit_ledger_context and delimit_gov_health via MCP when available.',
289
+ ].join(' ');
290
+
291
+ if (!config.customInstructions || !config.customInstructions.includes('delimit-cli hook')) {
292
+ config.customInstructions = govInstructions;
293
+ changes.push('customInstructions');
294
+ }
295
+
296
+ fs.writeFileSync(tool.configPath, JSON.stringify(config, null, 2));
297
+
298
+ // Write a GEMINI.md governance file (equivalent of CLAUDE.md)
299
+ const geminiMd = path.join(geminiDir, 'GEMINI.md');
300
+ if (!fs.existsSync(geminiMd) || !fs.readFileSync(geminiMd, 'utf-8').includes('delimit')) {
301
+ const content = `# Delimit Governance
302
+
303
+ On every session start:
304
+ 1. Run \`npx delimit-cli hook session-start\` to check open tasks and governance status
305
+
306
+ After editing code:
307
+ - After editing API specs: run \`npx delimit-cli hook pre-tool Edit\`
308
+ - After editing tests: run \`npx delimit-cli hook pre-tool Edit\`
309
+
310
+ Before committing:
311
+ - Run \`npx delimit-cli hook pre-commit\` to check for issues
312
+ `;
313
+ fs.writeFileSync(geminiMd, content);
314
+ changes.push('GEMINI.md');
315
+ }
316
+
317
+ return changes;
318
+ }
319
+
320
+ /**
321
+ * Install hooks for a detected tool.
322
+ * Returns { tool, changes } describing what was installed.
323
+ */
324
+ function installHooksForTool(tool, hookConfig) {
325
+ switch (tool.id) {
326
+ case 'claude':
327
+ return { tool, changes: installClaudeHooks(tool, hookConfig) };
328
+ case 'codex':
329
+ return { tool, changes: installCodexHooks(tool, hookConfig) };
330
+ case 'gemini':
331
+ return { tool, changes: installGeminiHooks(tool, hookConfig) };
332
+ default:
333
+ return { tool, changes: [] };
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Install hooks for all detected AI tools.
339
+ */
340
+ function installAllHooks(hookConfig) {
341
+ const tools = detectAITools();
342
+ const results = [];
343
+ for (const tool of tools) {
344
+ results.push(installHooksForTool(tool, hookConfig));
345
+ }
346
+ return { tools, results };
347
+ }
348
+
349
+ // ---------------------------------------------------------------------------
350
+ // Hook removal (for uninstall)
351
+ // ---------------------------------------------------------------------------
352
+
353
+ function removeClaudeHooks() {
354
+ const configPath = path.join(getHome(), '.claude', 'settings.json');
355
+ if (!fs.existsSync(configPath)) return false;
356
+
357
+ try {
358
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
359
+ if (!config.hooks) return false;
360
+
361
+ let changed = false;
362
+
363
+ for (const event of ['SessionStart', 'PreToolUse', 'PostToolUse']) {
364
+ if (Array.isArray(config.hooks[event])) {
365
+ const before = config.hooks[event].length;
366
+ config.hooks[event] = config.hooks[event].filter(
367
+ h => !(h.command && h.command.includes('delimit-cli'))
368
+ );
369
+ if (config.hooks[event].length === 0) {
370
+ delete config.hooks[event];
371
+ }
372
+ if (config.hooks[event] === undefined || config.hooks[event].length < before) {
373
+ changed = true;
374
+ }
375
+ }
376
+ }
377
+
378
+ if (Object.keys(config.hooks).length === 0) {
379
+ delete config.hooks;
380
+ }
381
+
382
+ if (changed) {
383
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
384
+ }
385
+ return changed;
386
+ } catch {
387
+ return false;
388
+ }
389
+ }
390
+
391
+ function removeCodexHooks() {
392
+ let changed = false;
393
+
394
+ // Remove from instructions.md
395
+ const instructionsPath = path.join(getHome(), '.codex', 'instructions.md');
396
+ if (fs.existsSync(instructionsPath)) {
397
+ let content = fs.readFileSync(instructionsPath, 'utf-8');
398
+ if (content.includes('delimit:hooks-start')) {
399
+ content = content.replace(
400
+ /\n*<!-- delimit:hooks-start -->[\s\S]*?<!-- delimit:hooks-end -->\n*/,
401
+ ''
402
+ );
403
+ fs.writeFileSync(instructionsPath, content);
404
+ changed = true;
405
+ }
406
+ }
407
+
408
+ // Remove hooks from config.json
409
+ const configPath = path.join(getHome(), '.codex', 'config.json');
410
+ if (fs.existsSync(configPath)) {
411
+ try {
412
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
413
+ if (config.hooks) {
414
+ for (const [key, val] of Object.entries(config.hooks)) {
415
+ if (typeof val === 'string' && val.includes('delimit-cli')) {
416
+ delete config.hooks[key];
417
+ changed = true;
418
+ }
419
+ }
420
+ if (Object.keys(config.hooks).length === 0) {
421
+ delete config.hooks;
422
+ }
423
+ if (changed) {
424
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
425
+ }
426
+ }
427
+ } catch { /* ignore */ }
428
+ }
429
+
430
+ return changed;
431
+ }
432
+
433
+ function removeGeminiHooks() {
434
+ let changed = false;
435
+
436
+ // Remove custom instructions referencing delimit
437
+ const configPath = path.join(getHome(), '.gemini', 'settings.json');
438
+ if (fs.existsSync(configPath)) {
439
+ try {
440
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
441
+ if (config.customInstructions && config.customInstructions.includes('delimit-cli hook')) {
442
+ delete config.customInstructions;
443
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
444
+ changed = true;
445
+ }
446
+ } catch { /* ignore */ }
447
+ }
448
+
449
+ // Remove GEMINI.md if it's ours
450
+ const geminiMd = path.join(getHome(), '.gemini', 'GEMINI.md');
451
+ if (fs.existsSync(geminiMd)) {
452
+ const content = fs.readFileSync(geminiMd, 'utf-8');
453
+ if (content.includes('Delimit Governance')) {
454
+ fs.unlinkSync(geminiMd);
455
+ changed = true;
456
+ }
457
+ }
458
+
459
+ return changed;
460
+ }
461
+
462
+ function removeAllHooks() {
463
+ const results = [];
464
+
465
+ if (removeClaudeHooks()) {
466
+ results.push('Claude Code');
467
+ }
468
+ if (removeCodexHooks()) {
469
+ results.push('Codex CLI');
470
+ }
471
+ if (removeGeminiHooks()) {
472
+ results.push('Gemini CLI');
473
+ }
474
+
475
+ return results;
476
+ }
477
+
478
+ // ---------------------------------------------------------------------------
479
+ // Hook execution commands
480
+ // ---------------------------------------------------------------------------
481
+
482
+ /**
483
+ * session-start: Show ledger context and governance health.
484
+ * Output goes to stdout for the AI tool to read.
485
+ */
486
+ async function hookSessionStart() {
487
+ const config = loadHookConfig();
488
+ if (!config.session_start) {
489
+ return;
490
+ }
491
+
492
+ const lines = [];
493
+ lines.push('[Delimit] Governance check');
494
+ lines.push('');
495
+
496
+ // Check for delimit.yml or .delimit.yml
497
+ const cwd = process.cwd();
498
+ const hasPolicy = fs.existsSync(path.join(cwd, 'delimit.yml'))
499
+ || fs.existsSync(path.join(cwd, '.delimit.yml'))
500
+ || fs.existsSync(path.join(cwd, '.delimit', 'policies.yml'));
501
+
502
+ if (hasPolicy) {
503
+ lines.push('[Delimit] Policy file found -- governance active');
504
+ } else {
505
+ lines.push('[Delimit] No policy file found -- run "delimit init" to set up governance');
506
+ }
507
+
508
+ // Check for OpenAPI specs
509
+ const specPatterns = ['openapi.yaml', 'openapi.yml', 'openapi.json', 'swagger.yaml', 'swagger.json'];
510
+ const foundSpecs = [];
511
+ for (const pattern of specPatterns) {
512
+ const specPath = path.join(cwd, pattern);
513
+ if (fs.existsSync(specPath)) {
514
+ foundSpecs.push(pattern);
515
+ }
516
+ }
517
+ // Also check api/ and specs/ directories
518
+ for (const dir of ['api', 'specs', 'spec']) {
519
+ const dirPath = path.join(cwd, dir);
520
+ if (fs.existsSync(dirPath)) {
521
+ try {
522
+ const files = fs.readdirSync(dirPath);
523
+ for (const f of files) {
524
+ if (/\.(yaml|yml|json)$/.test(f) && /openapi|swagger/i.test(f)) {
525
+ foundSpecs.push(path.join(dir, f));
526
+ }
527
+ }
528
+ } catch { /* ignore */ }
529
+ }
530
+ }
531
+
532
+ if (foundSpecs.length > 0) {
533
+ lines.push(`[Delimit] OpenAPI specs detected: ${foundSpecs.join(', ')}`);
534
+ }
535
+
536
+ // Check ledger
537
+ const ledgerDir = path.join(getDelimitHome(), 'ledger');
538
+ if (fs.existsSync(ledgerDir)) {
539
+ try {
540
+ const ledgerFiles = fs.readdirSync(ledgerDir).filter(f => f.endsWith('.json'));
541
+ let openItems = 0;
542
+ for (const f of ledgerFiles) {
543
+ try {
544
+ const items = JSON.parse(fs.readFileSync(path.join(ledgerDir, f), 'utf-8'));
545
+ if (Array.isArray(items)) {
546
+ openItems += items.filter(i => i.status === 'open' || i.status === 'in_progress').length;
547
+ }
548
+ } catch { /* ignore */ }
549
+ }
550
+ if (openItems > 0) {
551
+ lines.push(`[Delimit] Ledger: ${openItems} open item(s)`);
552
+ } else {
553
+ lines.push('[Delimit] Ledger: no open items');
554
+ }
555
+ } catch {
556
+ lines.push('[Delimit] Ledger: empty');
557
+ }
558
+ }
559
+
560
+ // Git branch info
561
+ try {
562
+ const branch = execSync('git branch --show-current 2>/dev/null', { encoding: 'utf-8' }).trim();
563
+ if (branch) {
564
+ lines.push(`[Delimit] Branch: ${branch}`);
565
+ }
566
+ } catch { /* not in git repo */ }
567
+
568
+ lines.push('');
569
+ process.stdout.write(lines.join('\n') + '\n');
570
+ }
571
+
572
+ /**
573
+ * pre-tool: Check before file edits.
574
+ * If editing an OpenAPI spec, run a quick lint.
575
+ * If editing a test file, note it.
576
+ */
577
+ async function hookPreTool(toolName) {
578
+ const config = loadHookConfig();
579
+ if (!config.pre_tool) {
580
+ return;
581
+ }
582
+
583
+ // The tool name comes from the AI tool (e.g., "Edit", "Write", "Bash")
584
+ // We check the DELIMIT_TOOL_INPUT env or just do lightweight checks
585
+ const cwd = process.cwd();
586
+
587
+ // Check if there are staged OpenAPI spec changes
588
+ try {
589
+ const stagedFiles = execSync('git diff --cached --name-only 2>/dev/null', {
590
+ encoding: 'utf-8',
591
+ timeout: 2000,
592
+ }).split('\n').filter(Boolean);
593
+
594
+ const specFiles = stagedFiles.filter(f =>
595
+ /openapi|swagger/i.test(f) && /\.(yaml|yml|json)$/.test(f)
596
+ );
597
+
598
+ if (specFiles.length > 0) {
599
+ process.stderr.write(`[Delimit] Warning: OpenAPI spec(s) staged for commit: ${specFiles.join(', ')}\n`);
600
+ process.stderr.write('[Delimit] Run "delimit lint" before committing to check for breaking changes.\n');
601
+ }
602
+
603
+ const testFiles = stagedFiles.filter(f =>
604
+ /\.(test|spec)\.(js|ts|py|rb)$/.test(f) || /test_.*\.py$/.test(f)
605
+ );
606
+
607
+ if (testFiles.length > 0) {
608
+ process.stderr.write(`[Delimit] Test files staged: ${testFiles.join(', ')}\n`);
609
+ process.stderr.write('[Delimit] Consider running tests before committing.\n');
610
+ }
611
+ } catch {
612
+ // Not in a git repo or no staged changes -- that is fine
613
+ }
614
+ }
615
+
616
+ /**
617
+ * pre-commit: Run repo diagnostics before committing.
618
+ */
619
+ async function hookPreCommit() {
620
+ const config = loadHookConfig();
621
+ if (!config.pre_commit) {
622
+ return;
623
+ }
624
+
625
+ const cwd = process.cwd();
626
+ const warnings = [];
627
+
628
+ // Check for staged OpenAPI spec changes
629
+ try {
630
+ const stagedFiles = execSync('git diff --cached --name-only 2>/dev/null', {
631
+ encoding: 'utf-8',
632
+ timeout: 2000,
633
+ }).split('\n').filter(Boolean);
634
+
635
+ const specFiles = stagedFiles.filter(f =>
636
+ /openapi|swagger/i.test(f) && /\.(yaml|yml|json)$/.test(f)
637
+ );
638
+
639
+ if (specFiles.length > 0) {
640
+ // Try to find a previous version to diff against
641
+ for (const specFile of specFiles) {
642
+ try {
643
+ // Get the HEAD version
644
+ const oldContent = execSync(`git show HEAD:${specFile} 2>/dev/null`, {
645
+ encoding: 'utf-8',
646
+ timeout: 3000,
647
+ });
648
+ if (oldContent) {
649
+ warnings.push(`[Delimit] OpenAPI spec changed: ${specFile}`);
650
+ warnings.push('[Delimit] Run "delimit diff <old> <new>" to review API changes before committing.');
651
+ }
652
+ } catch {
653
+ // New file, no previous version
654
+ }
655
+ }
656
+ }
657
+
658
+ // Check for secrets patterns in staged files
659
+ const sensitivePatterns = [
660
+ /password\s*[:=]\s*['"][^'"]+['"]/i,
661
+ /api[_-]?key\s*[:=]\s*['"][^'"]+['"]/i,
662
+ /secret\s*[:=]\s*['"][^'"]+['"]/i,
663
+ ];
664
+
665
+ for (const file of stagedFiles) {
666
+ if (/\.(env|key|pem|p12|pfx)$/.test(file)) {
667
+ warnings.push(`[Delimit] WARNING: Potentially sensitive file staged: ${file}`);
668
+ }
669
+ }
670
+ } catch {
671
+ // Not in git repo
672
+ }
673
+
674
+ // Check for policy file
675
+ const hasPolicy = fs.existsSync(path.join(cwd, 'delimit.yml'))
676
+ || fs.existsSync(path.join(cwd, '.delimit.yml'));
677
+
678
+ if (!hasPolicy) {
679
+ warnings.push('[Delimit] No governance policy found. Run "delimit init" to create one.');
680
+ }
681
+
682
+ if (warnings.length > 0) {
683
+ process.stderr.write(warnings.join('\n') + '\n');
684
+ }
685
+ }
686
+
687
+ // ---------------------------------------------------------------------------
688
+ // Exports
689
+ // ---------------------------------------------------------------------------
690
+
691
+ module.exports = {
692
+ detectAITools,
693
+ installHooksForTool,
694
+ installAllHooks,
695
+ installClaudeHooks,
696
+ installCodexHooks,
697
+ installGeminiHooks,
698
+ removeAllHooks,
699
+ removeClaudeHooks,
700
+ removeCodexHooks,
701
+ removeGeminiHooks,
702
+ loadHookConfig,
703
+ hookSessionStart,
704
+ hookPreTool,
705
+ hookPreCommit,
706
+ };