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.
- package/README.md +191 -0
- package/agent-checkpoint.js +364 -0
- package/architect-tools.js +707 -0
- package/audit-mcp.js +206 -0
- package/codebase-architect-tools.js +838 -0
- package/guardrail-2.0-tools.js +748 -0
- package/guardrail-tools.js +1075 -0
- package/hygiene-tools.js +428 -0
- package/index-v1.js +698 -0
- package/index.js +1409 -0
- package/index.old.js +4137 -0
- package/intelligence-tools.js +664 -0
- package/intent-drift-tools.js +873 -0
- package/mdc-generator.js +298 -0
- package/package.json +47 -0
- package/premium-tools.js +1275 -0
- package/test-mcp.js +108 -0
- package/test-tools.js +36 -0
- package/tier-auth.js +147 -0
|
@@ -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
|
+
};
|