delimit-cli 1.0.0 → 2.1.1

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 (95) hide show
  1. package/.github/workflows/api-governance.yml +43 -0
  2. package/README.md +70 -113
  3. package/adapters/codex-skill.js +87 -0
  4. package/adapters/cursor-extension.js +190 -0
  5. package/adapters/gemini-action.js +93 -0
  6. package/adapters/openai-function.js +112 -0
  7. package/adapters/xai-plugin.js +151 -0
  8. package/bin/delimit-cli.js +921 -0
  9. package/bin/delimit.js +237 -1
  10. package/delimit.yml +19 -0
  11. package/hooks/evidence-status.sh +12 -0
  12. package/hooks/git/commit-msg +4 -0
  13. package/hooks/git/pre-commit +4 -0
  14. package/hooks/git/pre-push +4 -0
  15. package/hooks/install-hooks.sh +583 -0
  16. package/hooks/message-auth-hook.js +9 -0
  17. package/hooks/message-governance-hook.js +9 -0
  18. package/hooks/models/claude-post.js +4 -0
  19. package/hooks/models/claude-pre.js +4 -0
  20. package/hooks/models/codex-post.js +4 -0
  21. package/hooks/models/codex-pre.js +4 -0
  22. package/hooks/models/cursor-post.js +4 -0
  23. package/hooks/models/cursor-pre.js +4 -0
  24. package/hooks/models/gemini-post.js +4 -0
  25. package/hooks/models/gemini-pre.js +4 -0
  26. package/hooks/models/openai-post.js +4 -0
  27. package/hooks/models/openai-pre.js +4 -0
  28. package/hooks/models/windsurf-post.js +4 -0
  29. package/hooks/models/windsurf-pre.js +4 -0
  30. package/hooks/models/xai-post.js +4 -0
  31. package/hooks/models/xai-pre.js +4 -0
  32. package/hooks/post-bash-hook.js +13 -0
  33. package/hooks/post-mcp-hook.js +13 -0
  34. package/hooks/post-response-hook.js +4 -0
  35. package/hooks/post-tool-hook.js +126 -0
  36. package/hooks/post-write-hook.js +13 -0
  37. package/hooks/pre-bash-hook.js +30 -0
  38. package/hooks/pre-mcp-hook.js +13 -0
  39. package/hooks/pre-read-hook.js +13 -0
  40. package/hooks/pre-search-hook.js +13 -0
  41. package/hooks/pre-submit-hook.js +4 -0
  42. package/hooks/pre-task-hook.js +13 -0
  43. package/hooks/pre-tool-hook.js +121 -0
  44. package/hooks/pre-web-hook.js +13 -0
  45. package/hooks/pre-write-hook.js +31 -0
  46. package/hooks/test-hooks.sh +12 -0
  47. package/hooks/update-delimit.sh +6 -0
  48. package/lib/agent.js +509 -0
  49. package/lib/api-engine.js +156 -0
  50. package/lib/auth-setup.js +891 -0
  51. package/lib/decision-engine.js +474 -0
  52. package/lib/hooks-installer.js +416 -0
  53. package/lib/platform-adapters.js +353 -0
  54. package/lib/proxy-handler.js +114 -0
  55. package/package.json +38 -30
  56. package/scripts/infect.js +128 -0
  57. package/test-decision-engine.js +181 -0
  58. package/test-hook.js +27 -0
  59. package/dist/commands/validate.d.ts +0 -2
  60. package/dist/commands/validate.d.ts.map +0 -1
  61. package/dist/commands/validate.js +0 -106
  62. package/dist/commands/validate.js.map +0 -1
  63. package/dist/index.d.ts +0 -3
  64. package/dist/index.d.ts.map +0 -1
  65. package/dist/index.js +0 -71
  66. package/dist/index.js.map +0 -1
  67. package/dist/types/index.d.ts +0 -39
  68. package/dist/types/index.d.ts.map +0 -1
  69. package/dist/types/index.js +0 -3
  70. package/dist/types/index.js.map +0 -1
  71. package/dist/utils/api.d.ts +0 -3
  72. package/dist/utils/api.d.ts.map +0 -1
  73. package/dist/utils/api.js +0 -64
  74. package/dist/utils/api.js.map +0 -1
  75. package/dist/utils/file.d.ts +0 -7
  76. package/dist/utils/file.d.ts.map +0 -1
  77. package/dist/utils/file.js +0 -69
  78. package/dist/utils/file.js.map +0 -1
  79. package/dist/utils/logger.d.ts +0 -14
  80. package/dist/utils/logger.d.ts.map +0 -1
  81. package/dist/utils/logger.js +0 -28
  82. package/dist/utils/logger.js.map +0 -1
  83. package/dist/utils/masker.d.ts +0 -14
  84. package/dist/utils/masker.d.ts.map +0 -1
  85. package/dist/utils/masker.js +0 -89
  86. package/dist/utils/masker.js.map +0 -1
  87. package/src/commands/validate.ts +0 -150
  88. package/src/index.ts +0 -80
  89. package/src/types/index.ts +0 -41
  90. package/src/utils/api.ts +0 -68
  91. package/src/utils/file.ts +0 -71
  92. package/src/utils/logger.ts +0 -27
  93. package/src/utils/masker.ts +0 -101
  94. package/test-sensitive.yaml +0 -109
  95. package/tsconfig.json +0 -23
@@ -0,0 +1,474 @@
1
+ class DecisionEngine {
2
+ constructor() {
3
+ this.lastDecision = null;
4
+ this.decisionHistory = [];
5
+ }
6
+
7
+ makeDecision(context, policies, sessionMode) {
8
+ const decision = {
9
+ timestamp: new Date().toISOString(),
10
+ id: this.generateId(),
11
+
12
+ // Modes
13
+ configuredMode: sessionMode,
14
+ defaultMode: policies.defaultMode || 'advisory',
15
+ effectiveMode: null,
16
+
17
+ // Reasoning
18
+ matchedRules: [],
19
+ escalationPath: [],
20
+ policySource: this.getPolicySources(policies),
21
+
22
+ // Decision
23
+ action: null,
24
+ message: null,
25
+
26
+ // Explanation
27
+ explanation: {
28
+ why: null,
29
+ trigger: null,
30
+ rule: null,
31
+ source: null
32
+ },
33
+
34
+ // Context
35
+ context: {
36
+ command: context.command,
37
+ pwd: context.pwd,
38
+ gitBranch: context.gitBranch,
39
+ files: context.files || [],
40
+ user: process.env.USER
41
+ }
42
+ };
43
+
44
+ // Start with base mode
45
+ let currentMode = sessionMode === 'auto' ?
46
+ (policies.defaultMode || 'advisory') :
47
+ sessionMode;
48
+
49
+ decision.escalationPath.push({
50
+ mode: currentMode,
51
+ reason: sessionMode === 'auto' ?
52
+ `Default mode from ${this.findPolicySource(policies, 'defaultMode')}` :
53
+ 'User-configured session mode'
54
+ });
55
+
56
+ // Evaluate all rules
57
+ if (policies.rules) {
58
+ for (const rule of policies.rules) {
59
+ const match = this.evaluateRule(rule, context);
60
+ if (match) {
61
+ decision.matchedRules.push({
62
+ name: rule.name,
63
+ mode: rule.mode,
64
+ trigger: match.trigger,
65
+ triggerType: match.type,
66
+ source: match.source || 'local'
67
+ });
68
+
69
+ // Check if this rule escalates
70
+ if (this.isStrongerMode(rule.mode, currentMode)) {
71
+ currentMode = rule.mode;
72
+ decision.escalationPath.push({
73
+ mode: rule.mode,
74
+ reason: `Rule "${rule.name}" matched`,
75
+ trigger: match.trigger,
76
+ source: match.source
77
+ });
78
+ }
79
+
80
+ if (rule.final) {
81
+ decision.explanation.why = `Final rule "${rule.name}" matched`;
82
+ break;
83
+ }
84
+ }
85
+ }
86
+ }
87
+
88
+ decision.effectiveMode = currentMode;
89
+
90
+ // Build explanation
91
+ if (decision.matchedRules.length > 0) {
92
+ const strongestRule = decision.matchedRules.reduce((a, b) =>
93
+ this.isStrongerMode(b.mode, a.mode) ? b : a
94
+ );
95
+
96
+ decision.explanation = {
97
+ why: `Mode escalated to ${currentMode.toUpperCase()}`,
98
+ trigger: strongestRule.trigger,
99
+ rule: strongestRule.name,
100
+ source: strongestRule.source || this.findPolicySource(policies, 'rules')
101
+ };
102
+ } else {
103
+ decision.explanation = {
104
+ why: `Using ${currentMode} mode`,
105
+ trigger: 'No matching rules',
106
+ rule: null,
107
+ source: sessionMode === 'auto' ?
108
+ this.findPolicySource(policies, 'defaultMode') :
109
+ 'user configuration'
110
+ };
111
+ }
112
+
113
+ // Determine action based on effective mode
114
+ switch (currentMode) {
115
+ case 'advisory':
116
+ decision.action = 'allow';
117
+ decision.message = this.formatMessage('advisory', decision);
118
+ break;
119
+
120
+ case 'guarded':
121
+ decision.action = 'prompt';
122
+ decision.message = this.formatMessage('guarded', decision);
123
+ break;
124
+
125
+ case 'enforce':
126
+ decision.action = 'block';
127
+ decision.message = this.formatMessage('enforce', decision);
128
+ break;
129
+ }
130
+
131
+ // Store for later retrieval
132
+ this.lastDecision = decision;
133
+ this.decisionHistory.push(decision);
134
+ if (this.decisionHistory.length > 100) {
135
+ this.decisionHistory.shift();
136
+ }
137
+
138
+ return decision;
139
+ }
140
+
141
+ evaluateRule(rule, context) {
142
+ if (!rule.triggers) return null;
143
+
144
+ for (const trigger of rule.triggers) {
145
+ // Check path patterns
146
+ if (trigger.path && context.files) {
147
+ for (const file of context.files) {
148
+ if (this.matchesPattern(file, trigger.path)) {
149
+ return {
150
+ type: 'path',
151
+ trigger: `path matches "${trigger.path}"`,
152
+ matched: file,
153
+ source: rule.source
154
+ };
155
+ }
156
+ }
157
+ }
158
+
159
+ // Check content patterns
160
+ if (trigger.content && context.diff) {
161
+ for (const pattern of trigger.content) {
162
+ if (context.diff.includes(pattern)) {
163
+ return {
164
+ type: 'content',
165
+ trigger: `content contains "${pattern}"`,
166
+ matched: pattern,
167
+ source: rule.source
168
+ };
169
+ }
170
+ }
171
+ }
172
+
173
+ // Check git branch
174
+ if (trigger.gitBranch && context.gitBranch) {
175
+ if (trigger.gitBranch.includes(context.gitBranch)) {
176
+ return {
177
+ type: 'branch',
178
+ trigger: `branch is "${context.gitBranch}"`,
179
+ matched: context.gitBranch,
180
+ source: rule.source
181
+ };
182
+ }
183
+ }
184
+
185
+ // Check command patterns
186
+ if (trigger.command && context.command) {
187
+ if (this.matchesPattern(context.command, trigger.command)) {
188
+ return {
189
+ type: 'command',
190
+ trigger: `command matches "${trigger.command}"`,
191
+ matched: context.command,
192
+ source: rule.source
193
+ };
194
+ }
195
+ }
196
+ }
197
+
198
+ return null;
199
+ }
200
+
201
+ formatMessage(mode, decision) {
202
+ const colors = {
203
+ advisory: '\x1b[34m', // blue
204
+ guarded: '\x1b[33m', // yellow
205
+ enforce: '\x1b[31m' // red
206
+ };
207
+ const reset = '\x1b[0m';
208
+ const bold = '\x1b[1m';
209
+
210
+ let msg = `${colors[mode]}${bold}[Delimit ${mode.toUpperCase()}]${reset}\n`;
211
+
212
+ if (decision.matchedRules.length > 0) {
213
+ const rule = decision.matchedRules[0];
214
+ msg += `📋 Rule: "${rule.name}"\n`;
215
+ msg += `🎯 Trigger: ${rule.trigger}\n`;
216
+ msg += `📁 Policy: ${rule.source || 'local'}\n`;
217
+ }
218
+
219
+ if (decision.configuredMode !== decision.effectiveMode) {
220
+ msg += `⚡ Mode escalated from ${decision.configuredMode} → ${decision.effectiveMode}\n`;
221
+ }
222
+
223
+ // Add specific reason and action guidance
224
+ switch (mode) {
225
+ case 'advisory':
226
+ msg += `✅ Proceeding with standard checks\n`;
227
+ msg += `💡 Recommendation: Review changes before committing`;
228
+ break;
229
+ case 'guarded':
230
+ msg += `⚠️ Confirmation required to proceed\n`;
231
+ msg += `💡 To proceed: Confirm when prompted, or use --force flag\n`;
232
+ msg += `💡 To avoid: Switch to a feature branch or request override`;
233
+ break;
234
+ case 'enforce':
235
+ msg += `🛑 BLOCKED: ${this.getBlockReason(decision)}\n`;
236
+ msg += `\n💡 HOW TO PROCEED:\n`;
237
+ if (decision.context.gitBranch && ['main', 'master', 'production'].includes(decision.context.gitBranch)) {
238
+ msg += ` 1. Switch to a feature branch: git checkout -b feature/your-change\n`;
239
+ msg += ` 2. Commit your changes there\n`;
240
+ msg += ` 3. Create a pull request for review\n`;
241
+ }
242
+ if (decision.matchedRules.some(r => r.trigger.includes('payment'))) {
243
+ msg += ` 1. Payment code requires security review\n`;
244
+ msg += ` 2. Request review from security team\n`;
245
+ msg += ` 3. Use approved payment SDK methods only\n`;
246
+ }
247
+ msg += `\n📖 Policy location: ${this.getPolicyLocation(decision)}`;
248
+ break;
249
+ }
250
+
251
+ return msg;
252
+ }
253
+
254
+ getBlockReason(decision) {
255
+ if (decision.matchedRules.length === 0) {
256
+ return 'Enforce mode is active (no specific rule matched)';
257
+ }
258
+ const rule = decision.matchedRules[0];
259
+ if (rule.name === 'Production Protection') {
260
+ return `Direct commits to ${decision.context.gitBranch} branch are prohibited`;
261
+ }
262
+ if (rule.name === 'Payment Code Security') {
263
+ return 'Payment/billing code changes require security review';
264
+ }
265
+ return `Rule "${rule.name}" prohibits this action`;
266
+ }
267
+
268
+ getPolicyLocation(decision) {
269
+ const sources = [];
270
+ if (decision.matchedRules.length > 0) {
271
+ const rule = decision.matchedRules[0];
272
+ if (rule.source === 'project policy') {
273
+ sources.push('./delimit.yml');
274
+ } else if (rule.source === 'user policy') {
275
+ sources.push('~/.config/delimit/delimit.yml');
276
+ } else if (rule.source === 'org policy') {
277
+ sources.push('Organization policy (contact admin)');
278
+ }
279
+ }
280
+ return sources.length > 0 ? sources.join(', ') : 'delimit.yml (default location)';
281
+ }
282
+
283
+ explainDecision(decisionId) {
284
+ const decision = decisionId ?
285
+ this.decisionHistory.find(d => d.id === decisionId) :
286
+ this.lastDecision;
287
+
288
+ if (!decision) {
289
+ return 'No decision found';
290
+ }
291
+
292
+ let explanation = '\n📊 GOVERNANCE DECISION EXPLANATION\n';
293
+ explanation += '═══════════════════════════════════\n\n';
294
+
295
+ explanation += `Decision ID: ${decision.id}\n`;
296
+ explanation += `Timestamp: ${decision.timestamp}\n\n`;
297
+
298
+ // PRIMARY REASON - Clear and upfront
299
+ explanation += '❌ WHY BLOCKED\n';
300
+ if (decision.action === 'block') {
301
+ explanation += this.getDetailedBlockReason(decision) + '\n\n';
302
+ } else if (decision.action === 'prompt') {
303
+ explanation += `Confirmation required: ${decision.explanation.why}\n\n`;
304
+ } else {
305
+ explanation += `Allowed: ${decision.explanation.why}\n\n`;
306
+ }
307
+
308
+ // HOW TO PROCEED
309
+ if (decision.action !== 'allow') {
310
+ explanation += '✅ HOW TO PROCEED\n';
311
+ explanation += this.getActionGuidance(decision) + '\n';
312
+ }
313
+
314
+ // TRIGGERING CONTEXT
315
+ explanation += '📍 WHAT TRIGGERED THIS\n';
316
+ if (decision.context.files && decision.context.files.length > 0) {
317
+ explanation += `├─ Files modified:\n`;
318
+ decision.context.files.slice(0, 5).forEach(f => {
319
+ explanation += `│ • ${f}\n`;
320
+ });
321
+ if (decision.context.files.length > 5) {
322
+ explanation += `│ • ... and ${decision.context.files.length - 5} more\n`;
323
+ }
324
+ }
325
+ explanation += `├─ Branch: ${decision.context.gitBranch || 'unknown'}\n`;
326
+ explanation += `├─ Directory: ${decision.context.pwd}\n`;
327
+ explanation += `└─ User: ${decision.context.user}\n\n`;
328
+
329
+ // POLICY SOURCE
330
+ explanation += '📖 POLICY DETAILS\n';
331
+ if (decision.matchedRules.length > 0) {
332
+ const rule = decision.matchedRules[0];
333
+ explanation += `├─ Rule: "${rule.name}"\n`;
334
+ explanation += `├─ Location: ${this.getPolicyFileLocation(rule.source)}\n`;
335
+ explanation += `├─ Trigger type: ${rule.triggerType}\n`;
336
+ explanation += `└─ Exact trigger: ${rule.trigger}\n`;
337
+ } else {
338
+ explanation += `├─ No specific rule matched\n`;
339
+ explanation += `└─ Using default mode: ${decision.defaultMode}\n`;
340
+ }
341
+ explanation += '\n';
342
+
343
+ // MODE DETAILS (less prominent)
344
+ explanation += '🎚️ MODE DETAILS\n';
345
+ explanation += `├─ Session mode: ${decision.configuredMode}\n`;
346
+ explanation += `├─ Default mode: ${decision.defaultMode}\n`;
347
+ explanation += `└─ Effective mode: ${decision.effectiveMode}\n`;
348
+
349
+ if (decision.escalationPath.length > 1) {
350
+ explanation += '\n📈 ESCALATION HISTORY\n';
351
+ decision.escalationPath.forEach((step, i) => {
352
+ const prefix = i === decision.escalationPath.length - 1 ? '└─' : '├─';
353
+ explanation += `${prefix} ${step.mode}: ${step.reason}\n`;
354
+ });
355
+ }
356
+
357
+ // AUDIT TRAIL
358
+ explanation += '\n🔍 AUDIT\n';
359
+ explanation += `├─ Decision ID: ${decision.id}\n`;
360
+ explanation += `├─ Timestamp: ${decision.timestamp}\n`;
361
+ explanation += `└─ Audit log: ~/.delimit/audit/${new Date(decision.timestamp).toISOString().split('T')[0]}.jsonl\n`;
362
+
363
+ return explanation;
364
+ }
365
+
366
+ getDetailedBlockReason(decision) {
367
+ if (decision.matchedRules.length === 0) {
368
+ return 'Enforce mode is active but no specific rule matched.\nThis suggests a configuration issue.';
369
+ }
370
+
371
+ const rule = decision.matchedRules[0];
372
+ let reason = '';
373
+
374
+ if (rule.name === 'Production Protection') {
375
+ reason = `Direct commits to the ${decision.context.gitBranch} branch are prohibited.\n`;
376
+ reason += `This branch is protected to prevent accidental production changes.`;
377
+ } else if (rule.name === 'Payment Code Security') {
378
+ reason = `Changes to payment/billing code require security review.\n`;
379
+ reason += `Files matching "${rule.trigger}" triggered this protection.`;
380
+ } else if (rule.name === 'AI-Generated Code Review') {
381
+ reason = `AI-generated code requires human review before committing.\n`;
382
+ reason += `Detected by: ${rule.trigger}`;
383
+ } else {
384
+ reason = `Rule "${rule.name}" prohibits this action.\n`;
385
+ reason += `Trigger: ${rule.trigger}`;
386
+ }
387
+
388
+ return reason;
389
+ }
390
+
391
+ getActionGuidance(decision) {
392
+ let guidance = '';
393
+
394
+ if (decision.action === 'block') {
395
+ if (decision.context.gitBranch && ['main', 'master', 'production'].includes(decision.context.gitBranch)) {
396
+ guidance += `1. Switch to a feature branch:\n`;
397
+ guidance += ` git checkout -b feature/your-change\n`;
398
+ guidance += `2. Commit your changes there\n`;
399
+ guidance += `3. Create a pull request for review\n`;
400
+ } else if (decision.matchedRules.some(r => r.trigger && r.trigger.includes('payment'))) {
401
+ guidance += `1. Request security review for payment code\n`;
402
+ guidance += `2. Use approved payment SDK methods\n`;
403
+ guidance += `3. Add security tests for payment flows\n`;
404
+ } else {
405
+ guidance += `1. Review the policy rules in ${this.getPolicyFileLocation()}\n`;
406
+ guidance += `2. Adjust your changes to comply\n`;
407
+ guidance += `3. Or request an exception from the policy owner\n`;
408
+ }
409
+
410
+ if (decision.effectiveMode === 'enforce') {
411
+ guidance += `\nNote: Enforce mode cannot be overridden locally.\n`;
412
+ guidance += `Contact your administrator for exceptions.\n`;
413
+ }
414
+ } else if (decision.action === 'prompt') {
415
+ guidance += `• Type 'y' to proceed with the action\n`;
416
+ guidance += `• Type 'n' to cancel\n`;
417
+ guidance += `• Or use --force flag to skip confirmation\n`;
418
+ }
419
+
420
+ return guidance;
421
+ }
422
+
423
+ getPolicyFileLocation(source) {
424
+ if (!source) return './delimit.yml';
425
+ if (source === 'project policy') return './delimit.yml';
426
+ if (source === 'user policy') return '~/.config/delimit/delimit.yml';
427
+ if (source === 'system policy') return '/etc/delimit/delimit.yml';
428
+ if (source === 'org policy') return 'Organization policy (contact admin)';
429
+ if (source === 'defaults') return 'Built-in defaults';
430
+ // Handle rule-specific sources with paths
431
+ if (source && source._sourcePath) return source._sourcePath;
432
+ return source;
433
+ }
434
+
435
+ isStrongerMode(mode1, mode2) {
436
+ const strength = { advisory: 1, guarded: 2, enforce: 3 };
437
+ return strength[mode1] > strength[mode2];
438
+ }
439
+
440
+ matchesPattern(str, pattern) {
441
+ // Simple glob matching - in production use minimatch
442
+ const regex = pattern
443
+ .replace(/\*/g, '.*')
444
+ .replace(/\?/g, '.');
445
+ return new RegExp(`^${regex}$`).test(str);
446
+ }
447
+
448
+ getPolicySources(policies) {
449
+ const sources = [];
450
+ if (policies._sources) {
451
+ if (policies._sources.org) sources.push('org');
452
+ if (policies._sources.user) sources.push('user');
453
+ if (policies._sources.project) sources.push('project');
454
+ }
455
+ return sources.length > 0 ? sources : ['local'];
456
+ }
457
+
458
+ findPolicySource(policies, field) {
459
+ // Handle the new policy structure with defaultModeSource
460
+ if (field === 'defaultMode' && policies.defaultModeSource) {
461
+ return policies.defaultModeSource;
462
+ }
463
+ if (policies._sources && policies._sources[field]) {
464
+ return policies._sources[field];
465
+ }
466
+ return 'defaults';
467
+ }
468
+
469
+ generateId() {
470
+ return Date.now().toString(36) + Math.random().toString(36).substr(2);
471
+ }
472
+ }
473
+
474
+ module.exports = DecisionEngine;