vibecheck-mcp-server 2.0.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.
@@ -0,0 +1,873 @@
1
+ /**
2
+ * Intent Drift Guard - MCP Tools
3
+ *
4
+ * Tools for AI agents (Cursor, Windsurf, etc.) to interact with Intent Drift Guard.
5
+ * These tools intercept agent actions and enforce intent alignment.
6
+ */
7
+
8
+ import path from "path";
9
+ import fs from "fs";
10
+ import crypto from "crypto";
11
+
12
+ // State file paths
13
+ const getStateDir = (projectRoot) => path.join(projectRoot, ".guardrail");
14
+ const getIntentFile = (projectRoot) =>
15
+ path.join(getStateDir(projectRoot), "current-intent.json");
16
+ const getLockFile = (projectRoot) =>
17
+ path.join(getStateDir(projectRoot), "intent-lock-state.json");
18
+ const getFixOnlyFile = (projectRoot) =>
19
+ path.join(getStateDir(projectRoot), "fix-only-state.json");
20
+
21
+ /**
22
+ * MCP Tool: guardrail_intent_start
23
+ * Start a new step with intent
24
+ */
25
+ const intentStartTool = {
26
+ name: "guardrail_intent_start",
27
+ description: `[FREE] Start a new coding step with explicit intent. This captures what you're trying to build before you write code. Intent Drift Guard will then monitor if your code matches this intent.
28
+
29
+ Example prompts:
30
+ - "Add email/password signup with validation and error handling"
31
+ - "Fix the login redirect bug in the auth middleware"
32
+ - "Refactor the user service to use the repository pattern"
33
+
34
+ After starting, use guardrail_intent_check after making changes to verify alignment.`,
35
+ inputSchema: {
36
+ type: "object",
37
+ properties: {
38
+ prompt: {
39
+ type: "string",
40
+ description:
41
+ "The intent prompt describing what you want to build/fix/change",
42
+ },
43
+ lock: {
44
+ type: "boolean",
45
+ description: "Whether to lock the intent (prevents scope expansion)",
46
+ default: false,
47
+ },
48
+ projectRoot: {
49
+ type: "string",
50
+ description: "Project root directory (defaults to current directory)",
51
+ },
52
+ },
53
+ required: ["prompt"],
54
+ },
55
+ handler: async ({ prompt, lock = false, projectRoot = process.cwd() }) => {
56
+ const intent = extractIntent(prompt);
57
+
58
+ // Save intent
59
+ const stateDir = getStateDir(projectRoot);
60
+ if (!fs.existsSync(stateDir)) {
61
+ fs.mkdirSync(stateDir, { recursive: true });
62
+ }
63
+
64
+ if (lock) {
65
+ intent.status = "locked";
66
+ intent.lockedAt = new Date().toISOString();
67
+
68
+ const lockState = {
69
+ enabled: true,
70
+ lockedIntent: intent,
71
+ lockStartedAt: new Date().toISOString(),
72
+ violationCount: 0,
73
+ violations: [],
74
+ };
75
+ fs.writeFileSync(
76
+ getLockFile(projectRoot),
77
+ JSON.stringify(lockState, null, 2),
78
+ );
79
+ }
80
+
81
+ fs.writeFileSync(
82
+ getIntentFile(projectRoot),
83
+ JSON.stringify(intent, null, 2),
84
+ );
85
+ fs.writeFileSync(
86
+ path.join(stateDir, "step-start.json"),
87
+ JSON.stringify({ startTime: new Date().toISOString() }),
88
+ );
89
+
90
+ return {
91
+ success: true,
92
+ message: `šŸŽÆ Intent captured: "${prompt.slice(0, 60)}..."`,
93
+ intent: {
94
+ id: intent.id,
95
+ type: intent.intentType,
96
+ locked: lock,
97
+ expectedArtifacts: intent.expectedArtifacts,
98
+ },
99
+ nextStep:
100
+ "Make your code changes, then call guardrail_intent_check to verify alignment.",
101
+ };
102
+ },
103
+ };
104
+
105
+ /**
106
+ * MCP Tool: guardrail_intent_check
107
+ * Check if current code changes align with the stated intent
108
+ */
109
+ const intentCheckTool = {
110
+ name: "guardrail_intent_check",
111
+ description: `[FREE] Check if your code changes align with the stated intent. Call this AFTER making changes to detect drift.
112
+
113
+ Returns:
114
+ - āœ… ALIGNED: Code matches intent, continue
115
+ - āš ļø PARTIAL: Some intent missing, may need additions
116
+ - āŒ DRIFTED: Code is doing something else, enters Fix-Only Mode
117
+
118
+ If drifted, you'll be restricted to only fixing the alignment issues.`,
119
+ inputSchema: {
120
+ type: "object",
121
+ properties: {
122
+ changedFiles: {
123
+ type: "array",
124
+ items: { type: "string" },
125
+ description: "List of files that were changed",
126
+ },
127
+ addedFiles: {
128
+ type: "array",
129
+ items: { type: "string" },
130
+ description: "List of files that were added",
131
+ },
132
+ projectRoot: {
133
+ type: "string",
134
+ description: "Project root directory",
135
+ },
136
+ },
137
+ },
138
+ handler: async ({
139
+ changedFiles = [],
140
+ addedFiles = [],
141
+ projectRoot = process.cwd(),
142
+ }) => {
143
+ const intent = loadIntent(projectRoot);
144
+ if (!intent) {
145
+ return {
146
+ success: false,
147
+ error: "No active intent. Call guardrail_intent_start first.",
148
+ };
149
+ }
150
+
151
+ // Simple drift detection
152
+ const result = detectDrift(intent, changedFiles, addedFiles, projectRoot);
153
+
154
+ // Enter Fix-Only Mode if drifted
155
+ if (result.status === "drifted") {
156
+ const fixOnlyState = {
157
+ enabled: true,
158
+ reason: result.summary.verdict,
159
+ allowedFiles: [...changedFiles, ...addedFiles],
160
+ forbiddenActions: [
161
+ "add_new_files",
162
+ "change_unrelated_files",
163
+ "add_routes",
164
+ "refactor",
165
+ ],
166
+ enteredAt: new Date().toISOString(),
167
+ driftResult: result,
168
+ };
169
+ fs.writeFileSync(
170
+ getFixOnlyFile(projectRoot),
171
+ JSON.stringify(fixOnlyState, null, 2),
172
+ );
173
+ }
174
+
175
+ const emoji =
176
+ result.status === "aligned"
177
+ ? "āœ…"
178
+ : result.status === "partial"
179
+ ? "āš ļø"
180
+ : "āŒ";
181
+
182
+ return {
183
+ success: result.status !== "drifted",
184
+ status: result.status,
185
+ message: `${emoji} ${result.status.toUpperCase()}: ${result.summary.verdict}`,
186
+ scores: result.scores,
187
+ missingArtifacts: result.missingArtifacts,
188
+ recommendations: result.recommendations.slice(0, 3),
189
+ fixOnlyMode: result.status === "drifted",
190
+ };
191
+ },
192
+ };
193
+
194
+ /**
195
+ * MCP Tool: guardrail_intent_validate_prompt
196
+ * Validate a new prompt against the locked intent
197
+ */
198
+ const intentValidatePromptTool = {
199
+ name: "guardrail_intent_validate_prompt",
200
+ description: `[FREE] Validate a new prompt/instruction against the locked intent. Use this BEFORE processing a new user request to check if it would violate the intent lock.
201
+
202
+ If the prompt would expand scope or change intent, this will block it.`,
203
+ inputSchema: {
204
+ type: "object",
205
+ properties: {
206
+ newPrompt: {
207
+ type: "string",
208
+ description: "The new prompt to validate",
209
+ },
210
+ projectRoot: {
211
+ type: "string",
212
+ description: "Project root directory",
213
+ },
214
+ },
215
+ required: ["newPrompt"],
216
+ },
217
+ handler: async ({ newPrompt, projectRoot = process.cwd() }) => {
218
+ const lockState = loadLockState(projectRoot);
219
+
220
+ if (!lockState || !lockState.enabled) {
221
+ return {
222
+ allowed: true,
223
+ message: "No intent lock active. Prompt allowed.",
224
+ };
225
+ }
226
+
227
+ const result = validatePromptAgainstLock(newPrompt, lockState.lockedIntent);
228
+
229
+ if (!result.allowed) {
230
+ // Record violation
231
+ lockState.violationCount++;
232
+ lockState.violations.push({
233
+ type: result.violationType,
234
+ description: result.message,
235
+ timestamp: new Date().toISOString(),
236
+ blocked: true,
237
+ });
238
+ fs.writeFileSync(
239
+ getLockFile(projectRoot),
240
+ JSON.stringify(lockState, null, 2),
241
+ );
242
+ }
243
+
244
+ return result;
245
+ },
246
+ };
247
+
248
+ /**
249
+ * MCP Tool: guardrail_intent_status
250
+ * Get current Intent Drift Guard status
251
+ */
252
+ const intentStatusTool = {
253
+ name: "guardrail_intent_status",
254
+ description: `[FREE] Get the current status of Intent Drift Guard, including active intent, lock status, and Fix-Only Mode status.`,
255
+ inputSchema: {
256
+ type: "object",
257
+ properties: {
258
+ projectRoot: {
259
+ type: "string",
260
+ description: "Project root directory",
261
+ },
262
+ },
263
+ },
264
+ handler: async ({ projectRoot = process.cwd() }) => {
265
+ const intent = loadIntent(projectRoot);
266
+ const lockState = loadLockState(projectRoot);
267
+ const fixOnlyState = loadFixOnlyState(projectRoot);
268
+
269
+ return {
270
+ hasActiveIntent: !!intent,
271
+ intent: intent
272
+ ? {
273
+ id: intent.id,
274
+ type: intent.intentType,
275
+ prompt: intent.rawPrompt.slice(0, 100),
276
+ status: intent.status,
277
+ }
278
+ : null,
279
+ intentLock: {
280
+ enabled: lockState?.enabled || false,
281
+ violationCount: lockState?.violationCount || 0,
282
+ },
283
+ fixOnlyMode: {
284
+ enabled: fixOnlyState?.enabled || false,
285
+ reason: fixOnlyState?.reason,
286
+ allowedFiles: fixOnlyState?.allowedFiles?.slice(0, 5),
287
+ },
288
+ };
289
+ },
290
+ };
291
+
292
+ /**
293
+ * MCP Tool: guardrail_intent_complete
294
+ * Complete the current step
295
+ */
296
+ const intentCompleteTool = {
297
+ name: "guardrail_intent_complete",
298
+ description: `[FREE] Complete the current step and generate a proof artifact. Call this when the intent has been fully implemented.`,
299
+ inputSchema: {
300
+ type: "object",
301
+ properties: {
302
+ force: {
303
+ type: "boolean",
304
+ description: "Force complete even if drift detected",
305
+ default: false,
306
+ },
307
+ projectRoot: {
308
+ type: "string",
309
+ description: "Project root directory",
310
+ },
311
+ },
312
+ },
313
+ handler: async ({ force = false, projectRoot = process.cwd() }) => {
314
+ const intent = loadIntent(projectRoot);
315
+ if (!intent) {
316
+ return {
317
+ success: false,
318
+ error: "No active intent to complete.",
319
+ };
320
+ }
321
+
322
+ // Generate simple proof
323
+ const proof = {
324
+ intentId: intent.id,
325
+ intent: intent.rawPrompt,
326
+ status: "aligned",
327
+ checks: {
328
+ intent: "pass",
329
+ lint: "skipped",
330
+ types: "skipped",
331
+ tests: "skipped",
332
+ forbiddenTokens: "skipped",
333
+ scopeCompliance: "pass",
334
+ },
335
+ filesChanged: 0,
336
+ timestamp: new Date().toISOString(),
337
+ duration: 0,
338
+ signature: generateSignature(intent),
339
+ };
340
+
341
+ // Save proof
342
+ const proofsDir = path.join(projectRoot, ".guardrail", "intent-proofs");
343
+ if (!fs.existsSync(proofsDir)) {
344
+ fs.mkdirSync(proofsDir, { recursive: true });
345
+ }
346
+
347
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
348
+ const proofFile = path.join(proofsDir, `proof-${timestamp}.json`);
349
+ fs.writeFileSync(proofFile, JSON.stringify(proof, null, 2));
350
+
351
+ // Update ledger
352
+ updateLedger(projectRoot, proof);
353
+
354
+ // Clean up state
355
+ cleanupState(projectRoot);
356
+
357
+ return {
358
+ success: true,
359
+ message: "āœ… Step completed successfully!",
360
+ proof: {
361
+ id: proof.intentId,
362
+ status: proof.status,
363
+ signature: proof.signature,
364
+ },
365
+ };
366
+ },
367
+ };
368
+
369
+ /**
370
+ * MCP Tool: guardrail_intent_lock
371
+ * Lock the current intent
372
+ */
373
+ const intentLockTool = {
374
+ name: "guardrail_intent_lock",
375
+ description: `[PRO] Lock the current intent to prevent scope expansion. Once locked, any attempt to add new features or change direction will be blocked.`,
376
+ inputSchema: {
377
+ type: "object",
378
+ properties: {
379
+ projectRoot: {
380
+ type: "string",
381
+ description: "Project root directory",
382
+ },
383
+ },
384
+ },
385
+ handler: async ({ projectRoot = process.cwd() }) => {
386
+ const intent = loadIntent(projectRoot);
387
+ if (!intent) {
388
+ return {
389
+ success: false,
390
+ error: "No active intent to lock.",
391
+ };
392
+ }
393
+
394
+ intent.status = "locked";
395
+ intent.lockedAt = new Date().toISOString();
396
+ fs.writeFileSync(
397
+ getIntentFile(projectRoot),
398
+ JSON.stringify(intent, null, 2),
399
+ );
400
+
401
+ const lockState = {
402
+ enabled: true,
403
+ lockedIntent: intent,
404
+ lockStartedAt: new Date().toISOString(),
405
+ violationCount: 0,
406
+ violations: [],
407
+ };
408
+ fs.writeFileSync(
409
+ getLockFile(projectRoot),
410
+ JSON.stringify(lockState, null, 2),
411
+ );
412
+
413
+ return {
414
+ success: true,
415
+ message: `šŸ”’ Intent locked: "${intent.rawPrompt.slice(0, 50)}..."`,
416
+ lockedAt: intent.lockedAt,
417
+ };
418
+ },
419
+ };
420
+
421
+ /**
422
+ * MCP Tool: guardrail_intent_unlock
423
+ * Unlock the current intent
424
+ */
425
+ const intentUnlockTool = {
426
+ name: "guardrail_intent_unlock",
427
+ description: `[FREE] Unlock the current intent, allowing scope changes again.`,
428
+ inputSchema: {
429
+ type: "object",
430
+ properties: {
431
+ projectRoot: {
432
+ type: "string",
433
+ description: "Project root directory",
434
+ },
435
+ },
436
+ },
437
+ handler: async ({ projectRoot = process.cwd() }) => {
438
+ const lockFile = getLockFile(projectRoot);
439
+ if (fs.existsSync(lockFile)) {
440
+ fs.unlinkSync(lockFile);
441
+ }
442
+
443
+ const intent = loadIntent(projectRoot);
444
+ if (intent) {
445
+ intent.status = "active";
446
+ delete intent.lockedAt;
447
+ fs.writeFileSync(
448
+ getIntentFile(projectRoot),
449
+ JSON.stringify(intent, null, 2),
450
+ );
451
+ }
452
+
453
+ return {
454
+ success: true,
455
+ message: "šŸ”“ Intent unlocked.",
456
+ };
457
+ },
458
+ };
459
+
460
+ // ============================================================================
461
+ // Helper Functions
462
+ // ============================================================================
463
+
464
+ function extractIntent(prompt) {
465
+ const id = `intent-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
466
+ const normalizedPrompt = prompt.toLowerCase();
467
+ const intentType = detectIntentType(normalizedPrompt);
468
+ const expectedArtifacts = extractExpectedArtifacts(normalizedPrompt);
469
+
470
+ return {
471
+ id,
472
+ rawPrompt: prompt,
473
+ normalizedPrompt,
474
+ intentType,
475
+ expectedArtifacts,
476
+ completionCriteria: extractCompletionCriteria(normalizedPrompt),
477
+ scope: {
478
+ allowedDirectories: [],
479
+ allowedFilePatterns: [],
480
+ allowNewDependencies: true,
481
+ allowSchemaChanges: false,
482
+ allowEnvChanges: false,
483
+ },
484
+ createdAt: new Date().toISOString(),
485
+ status: "active",
486
+ };
487
+ }
488
+
489
+ function detectIntentType(prompt) {
490
+ if (/fix|bug|repair|resolve|patch|debug/.test(prompt)) return "bugfix";
491
+ if (/refactor|restructure|reorganize|cleanup|simplify/.test(prompt))
492
+ return "refactor";
493
+ if (/test|spec|verify|validate/.test(prompt)) return "test";
494
+ if (/document|describe|explain|readme/.test(prompt)) return "docs";
495
+ if (/remove|delete|deprecate|drop/.test(prompt)) return "cleanup";
496
+ return "feature";
497
+ }
498
+
499
+ function extractExpectedArtifacts(prompt) {
500
+ const artifacts = {};
501
+
502
+ // Routes
503
+ const routeMatch = prompt.match(
504
+ /(get|post|put|patch|delete)\s+([\/\w-:{}]+)/gi,
505
+ );
506
+ if (routeMatch) {
507
+ artifacts.routes = routeMatch.map((m) => {
508
+ const [method, path] = m.split(/\s+/);
509
+ return { method: method.toUpperCase(), path };
510
+ });
511
+ }
512
+
513
+ // Auth-related inference
514
+ if (/signup|sign up|register/.test(prompt)) {
515
+ artifacts.routes = artifacts.routes || [];
516
+ artifacts.routes.push({ method: "POST", path: "/api/signup" });
517
+ artifacts.components = ["SignupForm"];
518
+ artifacts.exports = ["createUser", "hashPassword", "validateSignup"];
519
+ }
520
+
521
+ if (/login|sign in/.test(prompt)) {
522
+ artifacts.routes = artifacts.routes || [];
523
+ artifacts.routes.push({ method: "POST", path: "/api/login" });
524
+ artifacts.components = ["LoginForm"];
525
+ artifacts.exports = ["authenticateUser", "verifyPassword"];
526
+ }
527
+
528
+ // Component inference
529
+ const componentMatch = prompt.match(/component\s+(?:called\s+)?(\w+)/gi);
530
+ if (componentMatch) {
531
+ artifacts.components = componentMatch.map((m) => m.split(/\s+/).pop());
532
+ }
533
+
534
+ return artifacts;
535
+ }
536
+
537
+ function extractCompletionCriteria(prompt) {
538
+ const criteria = [];
539
+
540
+ if (/password/.test(prompt)) {
541
+ criteria.push({
542
+ id: "c1",
543
+ description: "Password hashing implemented",
544
+ checkType: "pattern_present",
545
+ satisfied: false,
546
+ });
547
+ }
548
+ if (/validat/.test(prompt)) {
549
+ criteria.push({
550
+ id: "c2",
551
+ description: "Input validation exists",
552
+ checkType: "pattern_present",
553
+ satisfied: false,
554
+ });
555
+ }
556
+ if (/error/.test(prompt)) {
557
+ criteria.push({
558
+ id: "c3",
559
+ description: "Error handling implemented",
560
+ checkType: "pattern_present",
561
+ satisfied: false,
562
+ });
563
+ }
564
+
565
+ return criteria;
566
+ }
567
+
568
+ function detectDrift(intent, changedFiles, addedFiles, projectRoot) {
569
+ let alignmentScore = 100;
570
+ const missingArtifacts = [];
571
+ const recommendations = [];
572
+
573
+ // Check expected routes
574
+ if (intent.expectedArtifacts.routes?.length) {
575
+ const allFiles = [...changedFiles, ...addedFiles];
576
+ let routesFound = 0;
577
+
578
+ for (const expected of intent.expectedArtifacts.routes) {
579
+ const found = allFiles.some((f) => {
580
+ if (!f.endsWith(".ts") && !f.endsWith(".js")) return false;
581
+ try {
582
+ const content = fs.readFileSync(path.join(projectRoot, f), "utf-8");
583
+ return content
584
+ .toLowerCase()
585
+ .includes(expected.path.replace(/:\w+/g, "").toLowerCase());
586
+ } catch {
587
+ return false;
588
+ }
589
+ });
590
+ if (found) routesFound++;
591
+ else missingArtifacts.push(`Route: ${expected.method} ${expected.path}`);
592
+ }
593
+
594
+ const routeCompletion =
595
+ routesFound / intent.expectedArtifacts.routes.length;
596
+ alignmentScore = Math.round(routeCompletion * 100);
597
+ }
598
+
599
+ // Check expected exports
600
+ if (intent.expectedArtifacts.exports?.length) {
601
+ for (const expected of intent.expectedArtifacts.exports) {
602
+ let found = false;
603
+ for (const f of [...changedFiles, ...addedFiles]) {
604
+ try {
605
+ const content = fs.readFileSync(path.join(projectRoot, f), "utf-8");
606
+ if (
607
+ new RegExp(
608
+ `export\\s+(?:const|function|class)\\s+${expected}`,
609
+ "i",
610
+ ).test(content)
611
+ ) {
612
+ found = true;
613
+ break;
614
+ }
615
+ } catch {}
616
+ }
617
+ if (!found) {
618
+ missingArtifacts.push(`Export: ${expected}`);
619
+ alignmentScore -= 10;
620
+ }
621
+ }
622
+ }
623
+
624
+ // Determine status
625
+ let status = "aligned";
626
+ if (alignmentScore < 70) status = "partial";
627
+ if (alignmentScore < 50 || missingArtifacts.length > 3) status = "drifted";
628
+
629
+ // Generate recommendations
630
+ for (const missing of missingArtifacts) {
631
+ recommendations.push({
632
+ type: "add",
633
+ priority: "high",
634
+ description: `Add missing ${missing}`,
635
+ });
636
+ }
637
+
638
+ return {
639
+ status,
640
+ scores: {
641
+ alignment: alignmentScore,
642
+ scopeViolation: 0,
643
+ noiseRatio: 0,
644
+ overall: alignmentScore,
645
+ },
646
+ missingArtifacts,
647
+ scopeViolations: [],
648
+ completionStatus: {
649
+ totalCriteria: 0,
650
+ satisfiedCriteria: 0,
651
+ percentage: 100,
652
+ missing: [],
653
+ },
654
+ summary: {
655
+ intended: intent.rawPrompt,
656
+ implemented: `${changedFiles.length + addedFiles.length} files changed`,
657
+ gap:
658
+ missingArtifacts.length > 0
659
+ ? `Missing: ${missingArtifacts.slice(0, 3).join(", ")}`
660
+ : "",
661
+ verdict:
662
+ status === "aligned"
663
+ ? "Code matches intent"
664
+ : status === "partial"
665
+ ? "Some intent missing"
666
+ : "Code drifted from intent",
667
+ },
668
+ recommendations,
669
+ timestamp: new Date().toISOString(),
670
+ };
671
+ }
672
+
673
+ function validatePromptAgainstLock(newPrompt, lockedIntent) {
674
+ const normalizedNew = newPrompt.toLowerCase();
675
+ const normalizedOriginal = lockedIntent.normalizedPrompt;
676
+
677
+ // Check for scope expansion phrases
678
+ const expansionPatterns = [
679
+ {
680
+ pattern: /\balso\s+(?:add|create|implement|build)/i,
681
+ reason: "Adding new feature",
682
+ },
683
+ {
684
+ pattern: /\bwhile\s+(?:you're|we're|i'm)\s+at\s+it/i,
685
+ reason: "Scope creep phrase",
686
+ },
687
+ {
688
+ pattern: /\band\s+then\s+(?:add|create|implement)/i,
689
+ reason: "Chaining new features",
690
+ },
691
+ { pattern: /\blet's\s+also\b/i, reason: "Adding to scope" },
692
+ {
693
+ pattern: /\bactually,?\s+(?:let's|can\s+you)/i,
694
+ reason: "Changing direction",
695
+ },
696
+ ];
697
+
698
+ for (const { pattern, reason } of expansionPatterns) {
699
+ if (pattern.test(newPrompt)) {
700
+ return {
701
+ allowed: false,
702
+ violationType: "scope_expansion",
703
+ message: `šŸ”’ INTENT LOCKED: ${reason} not allowed. Complete current step first.\n\nOriginal intent: "${lockedIntent.rawPrompt.slice(0, 60)}..."\n\nšŸ’” Run "guardrail intent complete" or "guardrail intent unlock" to proceed.`,
704
+ };
705
+ }
706
+ }
707
+
708
+ // Check keyword overlap
709
+ const originalKeywords = extractKeywords(normalizedOriginal);
710
+ const newKeywords = extractKeywords(normalizedNew);
711
+ const overlap = originalKeywords.filter((k) => newKeywords.includes(k));
712
+
713
+ if (overlap.length < 2 && newKeywords.length > 3) {
714
+ return {
715
+ allowed: false,
716
+ violationType: "intent_change",
717
+ message: `šŸ”’ INTENT LOCKED: This appears to be a new task.\n\nOriginal intent: "${lockedIntent.rawPrompt.slice(0, 60)}..."\n\nšŸ’” Complete or unlock the current intent first.`,
718
+ };
719
+ }
720
+
721
+ return {
722
+ allowed: true,
723
+ message: "Prompt aligns with locked intent.",
724
+ };
725
+ }
726
+
727
+ function extractKeywords(text) {
728
+ const stopWords = new Set([
729
+ "the",
730
+ "a",
731
+ "an",
732
+ "and",
733
+ "or",
734
+ "but",
735
+ "in",
736
+ "on",
737
+ "at",
738
+ "to",
739
+ "for",
740
+ "of",
741
+ "with",
742
+ "by",
743
+ "from",
744
+ "as",
745
+ "is",
746
+ "was",
747
+ "are",
748
+ "were",
749
+ "be",
750
+ ]);
751
+
752
+ return text
753
+ .replace(/[^a-z0-9\s]/g, " ")
754
+ .split(/\s+/)
755
+ .filter((w) => w.length > 2 && !stopWords.has(w));
756
+ }
757
+
758
+ function loadIntent(projectRoot) {
759
+ try {
760
+ const file = getIntentFile(projectRoot);
761
+ if (fs.existsSync(file)) {
762
+ return JSON.parse(fs.readFileSync(file, "utf-8"));
763
+ }
764
+ } catch {}
765
+ return null;
766
+ }
767
+
768
+ function loadLockState(projectRoot) {
769
+ try {
770
+ const file = getLockFile(projectRoot);
771
+ if (fs.existsSync(file)) {
772
+ return JSON.parse(fs.readFileSync(file, "utf-8"));
773
+ }
774
+ } catch {}
775
+ return null;
776
+ }
777
+
778
+ function loadFixOnlyState(projectRoot) {
779
+ try {
780
+ const file = getFixOnlyFile(projectRoot);
781
+ if (fs.existsSync(file)) {
782
+ return JSON.parse(fs.readFileSync(file, "utf-8"));
783
+ }
784
+ } catch {}
785
+ return null;
786
+ }
787
+
788
+ function generateSignature(intent) {
789
+ const data = JSON.stringify({
790
+ id: intent.id,
791
+ prompt: intent.rawPrompt,
792
+ time: Date.now(),
793
+ });
794
+ return crypto.createHash("sha256").update(data).digest("hex").slice(0, 16);
795
+ }
796
+
797
+ function updateLedger(projectRoot, proof) {
798
+ const ledgerFile = path.join(
799
+ projectRoot,
800
+ ".guardrail",
801
+ "intent-proofs",
802
+ "ledger.json",
803
+ );
804
+ let ledger;
805
+
806
+ try {
807
+ if (fs.existsSync(ledgerFile)) {
808
+ ledger = JSON.parse(fs.readFileSync(ledgerFile, "utf-8"));
809
+ }
810
+ } catch {}
811
+
812
+ if (!ledger) {
813
+ ledger = {
814
+ projectId: path.basename(projectRoot),
815
+ steps: [],
816
+ totalSteps: 0,
817
+ alignedSteps: 0,
818
+ partialSteps: 0,
819
+ driftedSteps: 0,
820
+ lastUpdated: new Date().toISOString(),
821
+ };
822
+ }
823
+
824
+ ledger.steps.push(proof);
825
+ ledger.totalSteps++;
826
+ if (proof.status === "aligned") ledger.alignedSteps++;
827
+ else if (proof.status === "partial") ledger.partialSteps++;
828
+ else ledger.driftedSteps++;
829
+ ledger.lastUpdated = new Date().toISOString();
830
+
831
+ fs.writeFileSync(ledgerFile, JSON.stringify(ledger, null, 2));
832
+ }
833
+
834
+ function cleanupState(projectRoot) {
835
+ const files = [
836
+ getIntentFile(projectRoot),
837
+ getLockFile(projectRoot),
838
+ getFixOnlyFile(projectRoot),
839
+ path.join(getStateDir(projectRoot), "step-start.json"),
840
+ ];
841
+
842
+ for (const file of files) {
843
+ try {
844
+ if (fs.existsSync(file)) fs.unlinkSync(file);
845
+ } catch {}
846
+ }
847
+ }
848
+
849
+ // ============================================================================
850
+ // Export tools for MCP server
851
+ // ============================================================================
852
+
853
+ // Export all tools as array
854
+ const intentDriftTools = [
855
+ intentStartTool,
856
+ intentCheckTool,
857
+ intentValidatePromptTool,
858
+ intentStatusTool,
859
+ intentCompleteTool,
860
+ intentLockTool,
861
+ intentUnlockTool,
862
+ ];
863
+
864
+ export {
865
+ intentStartTool,
866
+ intentCheckTool,
867
+ intentValidatePromptTool,
868
+ intentStatusTool,
869
+ intentCompleteTool,
870
+ intentLockTool,
871
+ intentUnlockTool,
872
+ intentDriftTools,
873
+ };