speclock 4.5.3 → 4.5.5

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/src/mcp/server.js CHANGED
@@ -1,1355 +1,1355 @@
1
- import os from "os";
2
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { z } from "zod";
5
- import {
6
- ensureInit,
7
- setGoal,
8
- addLock,
9
- removeLock,
10
- addDecision,
11
- addNote,
12
- updateDeployFacts,
13
- logChange,
14
- checkConflict,
15
- checkConflictAsync,
16
- getSessionBriefing,
17
- endSession,
18
- suggestLocks,
19
- detectDrift,
20
- syncLocksToPackageJson,
21
- autoGuardRelatedFiles,
22
- listTemplates,
23
- applyTemplate,
24
- generateReport,
25
- auditStagedFiles,
26
- verifyAuditChain,
27
- exportCompliance,
28
- checkLimits,
29
- getLicenseInfo,
30
- enforceConflictCheck,
31
- setEnforcementMode,
32
- overrideLock,
33
- getOverrideHistory,
34
- getEnforcementConfig,
35
- semanticAudit,
36
- evaluatePolicy,
37
- listPolicyRules,
38
- addPolicyRule,
39
- removePolicyRule,
40
- initPolicy,
41
- exportPolicy,
42
- importPolicy,
43
- isTelemetryEnabled,
44
- getTelemetrySummary,
45
- trackToolUsage,
46
- } from "../core/engine.js";
47
- import { generateContext, generateContextPack } from "../core/context.js";
48
- import {
49
- readBrain,
50
- readEvents,
51
- newId,
52
- nowIso,
53
- appendEvent,
54
- bumpEvents,
55
- writeBrain,
56
- } from "../core/storage.js";
57
- import {
58
- captureStatus,
59
- createTag,
60
- getDiffSummary,
61
- } from "../core/git.js";
62
- import {
63
- isAuthEnabled,
64
- validateApiKey,
65
- checkPermission,
66
- } from "../core/auth.js";
67
-
68
- // --- Auth via env var (v3.0) ---
69
- function getAuthRole() {
70
- if (!isAuthEnabled(PROJECT_ROOT)) return "admin";
71
- const key = process.env.SPECLOCK_API_KEY;
72
- if (!key) return "admin"; // No key env var = local use, allow all
73
- const result = validateApiKey(PROJECT_ROOT, key);
74
- return result.valid ? result.role : null;
75
- }
76
-
77
- function requirePermission(toolName) {
78
- const role = getAuthRole();
79
- if (!role) return { allowed: false, error: "Invalid SPECLOCK_API_KEY." };
80
- if (!checkPermission(role, toolName)) {
81
- return { allowed: false, error: `Permission denied. Role "${role}" cannot access "${toolName}".` };
82
- }
83
- return { allowed: true, role };
84
- }
85
-
86
- // --- Project root resolution ---
87
- function parseArgs(argv) {
88
- const args = { project: null };
89
- for (let i = 2; i < argv.length; i++) {
90
- if (argv[i] === "--project" && argv[i + 1]) {
91
- args.project = argv[i + 1];
92
- i++;
93
- }
94
- }
95
- return args;
96
- }
97
-
98
- const args = parseArgs(process.argv);
99
- const PROJECT_ROOT =
100
- args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
101
-
102
- // --- MCP Server ---
103
- const VERSION = "4.5.3";
104
- const AUTHOR = "Sandeep Roy";
105
-
106
- const server = new McpServer(
107
- { name: "speclock", version: VERSION },
108
- {
109
- instructions: [
110
- `CRITICAL: You have SpecLock connected — an AI Continuity Engine that remembers project context across sessions. You MUST follow these rules:`,
111
- ``,
112
- `## STEP 1 — FIRST THING IN EVERY CONVERSATION (MANDATORY):`,
113
- `Before doing ANYTHING else, call speclock_session_briefing. This loads the project's goal, constraints, decisions, and history from previous sessions.`,
114
- `- If it returns "not initialized", call speclock_init first, then tell the user: "I've set up project memory for this workspace. I'll remember your decisions and constraints across sessions."`,
115
- `- If it returns context, start your response with a brief one-line status like: "🔒 Project memory loaded — [X] constraints, [Y] decisions tracked from previous sessions." Then proceed to answer the user's actual question.`,
116
- `- If there is no meaningful context yet (new project), say: "🔒 Project memory active. I'll automatically remember your key decisions and constraints across sessions." Then proceed normally.`,
117
- ``,
118
- `## STEP 2 — LISTEN AND CAPTURE (do this continuously as you work):`,
119
- `- When the user describes what they're building → call speclock_set_goal.`,
120
- `- When the user says something is non-negotiable or uses words like "always", "never", "must", "don't ever change", "make sure to keep", "this is critical" → call speclock_add_lock. Then briefly confirm: "🔒 Locked: [constraint]"`,
121
- `- When the user makes a technology or design choice → call speclock_add_decision.`,
122
- `- When you complete significant work → call speclock_log_change.`,
123
- ``,
124
- `## STEP 3 — PROTECT (before making significant changes):`,
125
- `- Before removing features, changing architecture, switching technologies, or modifying core logic → call speclock_check_conflict.`,
126
- `- If a conflict is found (confidence HIGH), STOP and warn: "⚠️ This would conflict with a constraint you set: [constraint]. Should I proceed anyway or keep the current approach?"`,
127
- `- Do NOT proceed unless the user explicitly confirms.`,
128
- ``,
129
- `## STEP 4 — REMEMBER (when the user wants to change their mind):`,
130
- `- If they want to remove or change something they previously locked, remind them: "You previously locked: [constraint]. Want me to unlock this and proceed?"`,
131
- `- If confirmed, call speclock_remove_lock, then proceed.`,
132
- ``,
133
- `## STEP 5 — CLOSE (when conversation ends):`,
134
- `- Call speclock_session_summary with a brief description of what was done.`,
135
- ``,
136
- `## KEY BEHAVIOR:`,
137
- `- The 🔒 emoji is your SpecLock indicator. Users learn to recognize it means their project memory is active.`,
138
- `- Keep SpecLock confirmations SHORT — one line max. Don't dump data.`,
139
- `- Present remembered context NATURALLY. Instead of "SpecLock says your goal is X", just say "Based on our previous work, the goal is X."`,
140
- `- If the user asks "how do you remember this?" — explain that SpecLock is connected as a project memory layer that persists across sessions.`,
141
- ].join("\n"),
142
- }
143
- );
144
-
145
- // ========================================
146
- // MEMORY MANAGEMENT TOOLS
147
- // ========================================
148
-
149
- // Tool 1: speclock_init
150
- server.tool(
151
- "speclock_init",
152
- "Initialize SpecLock in the current project directory. Creates .speclock/ with brain.json, events.log, and supporting directories.",
153
- {},
154
- async () => {
155
- const brain = ensureInit(PROJECT_ROOT);
156
- return {
157
- content: [
158
- {
159
- type: "text",
160
- text: `SpecLock initialized for "${brain.project.name}" at ${brain.project.root}`,
161
- },
162
- ],
163
- };
164
- }
165
- );
166
-
167
- // Tool 2: speclock_get_context — THE KEY TOOL
168
- server.tool(
169
- "speclock_get_context",
170
- "THE KEY TOOL. Returns the full structured context pack including goal, locks, decisions, recent changes, deploy facts, reverts, session history, and notes. Call this at the start of every session or whenever you need to refresh your understanding of the project.",
171
- {
172
- format: z
173
- .enum(["markdown", "json"])
174
- .optional()
175
- .default("markdown")
176
- .describe("Output format: markdown (readable) or json (structured)"),
177
- },
178
- async ({ format }) => {
179
- if (format === "json") {
180
- const pack = generateContextPack(PROJECT_ROOT);
181
- return {
182
- content: [{ type: "text", text: JSON.stringify(pack, null, 2) }],
183
- };
184
- }
185
- const md = generateContext(PROJECT_ROOT);
186
- return { content: [{ type: "text", text: md }] };
187
- }
188
- );
189
-
190
- // Tool 3: speclock_set_goal
191
- server.tool(
192
- "speclock_set_goal",
193
- "Set or update the project goal. This is the high-level objective that guides all work.",
194
- {
195
- text: z.string().min(1).describe("The project goal text"),
196
- },
197
- async ({ text }) => {
198
- setGoal(PROJECT_ROOT, text);
199
- return {
200
- content: [{ type: "text", text: `Goal set: "${text}"` }],
201
- };
202
- }
203
- );
204
-
205
- // Tool 4: speclock_add_lock
206
- server.tool(
207
- "speclock_add_lock",
208
- "Add a non-negotiable constraint (SpecLock). These are rules that must NEVER be violated during development.",
209
- {
210
- text: z.string().min(1).describe("The constraint text"),
211
- tags: z
212
- .array(z.string())
213
- .optional()
214
- .default([])
215
- .describe("Category tags"),
216
- source: z
217
- .enum(["user", "agent"])
218
- .optional()
219
- .default("agent")
220
- .describe("Who created this lock"),
221
- },
222
- async ({ text, tags, source }) => {
223
- const { lockId, rewritten, rewriteReason } = addLock(PROJECT_ROOT, text, tags, source);
224
-
225
- // Read the stored lock to get the normalized text
226
- const brain = readBrain(PROJECT_ROOT);
227
- const storedLock = brain?.specLock?.items?.find(l => l.id === lockId);
228
- const storedText = storedLock?.text || text;
229
-
230
- // Auto-guard related files
231
- const guardResult = autoGuardRelatedFiles(PROJECT_ROOT, storedText);
232
- const guardMsg = guardResult.guarded.length > 0
233
- ? `\nAuto-guarded ${guardResult.guarded.length} file(s): ${guardResult.guarded.join(", ")}`
234
- : "";
235
-
236
- // Sync active locks to package.json
237
- syncLocksToPackageJson(PROJECT_ROOT);
238
-
239
- // Report rewrite if it happened
240
- const rewriteMsg = rewritten
241
- ? `\n\nSmart Lock Authoring: Rewritten for accuracy.\n Original: "${text}"\n Stored as: "${storedText}"\n Reason: ${rewriteReason}`
242
- : "";
243
-
244
- return {
245
- content: [
246
- { type: "text", text: `Lock added (${lockId}): "${storedText}"${guardMsg}${rewriteMsg}` },
247
- ],
248
- };
249
- }
250
- );
251
-
252
- // Tool 5: speclock_remove_lock
253
- server.tool(
254
- "speclock_remove_lock",
255
- "Remove (deactivate) a SpecLock by its ID. The lock is soft-deleted and kept in history.",
256
- {
257
- lockId: z.string().min(1).describe("The lock ID to remove"),
258
- },
259
- async ({ lockId }) => {
260
- const result = removeLock(PROJECT_ROOT, lockId);
261
- if (!result.removed) {
262
- return {
263
- content: [{ type: "text", text: result.error }],
264
- isError: true,
265
- };
266
- }
267
- // Sync updated locks to package.json
268
- syncLocksToPackageJson(PROJECT_ROOT);
269
- return {
270
- content: [
271
- {
272
- type: "text",
273
- text: `Lock removed: "${result.lockText}"`,
274
- },
275
- ],
276
- };
277
- }
278
- );
279
-
280
- // Tool 6: speclock_add_decision
281
- server.tool(
282
- "speclock_add_decision",
283
- "Record an architectural or design decision. Decisions guide future work and prevent contradictory changes.",
284
- {
285
- text: z.string().min(1).describe("The decision text"),
286
- tags: z.array(z.string()).optional().default([]),
287
- source: z
288
- .enum(["user", "agent"])
289
- .optional()
290
- .default("agent"),
291
- },
292
- async ({ text, tags, source }) => {
293
- const { decId } = addDecision(PROJECT_ROOT, text, tags, source);
294
- return {
295
- content: [
296
- { type: "text", text: `Decision recorded (${decId}): "${text}"` },
297
- ],
298
- };
299
- }
300
- );
301
-
302
- // Tool 7: speclock_add_note
303
- server.tool(
304
- "speclock_add_note",
305
- "Add a pinned note for reference. Notes persist across sessions as reminders.",
306
- {
307
- text: z.string().min(1).describe("The note text"),
308
- pinned: z
309
- .boolean()
310
- .optional()
311
- .default(true)
312
- .describe("Whether to pin this note in context"),
313
- },
314
- async ({ text, pinned }) => {
315
- const { noteId } = addNote(PROJECT_ROOT, text, pinned);
316
- return {
317
- content: [
318
- { type: "text", text: `Note added (${noteId}): "${text}"` },
319
- ],
320
- };
321
- }
322
- );
323
-
324
- // Tool 8: speclock_set_deploy_facts
325
- server.tool(
326
- "speclock_set_deploy_facts",
327
- "Record deployment configuration facts (provider, branch, auto-deploy settings).",
328
- {
329
- provider: z
330
- .string()
331
- .optional()
332
- .describe("Deploy provider (vercel, railway, aws, netlify, etc.)"),
333
- branch: z.string().optional().describe("Deploy branch"),
334
- autoDeploy: z
335
- .boolean()
336
- .optional()
337
- .describe("Whether auto-deploy is enabled"),
338
- url: z.string().optional().describe("Deployment URL"),
339
- notes: z.string().optional().describe("Additional deploy notes"),
340
- },
341
- async (params) => {
342
- updateDeployFacts(PROJECT_ROOT, params);
343
- return {
344
- content: [{ type: "text", text: "Deploy facts updated." }],
345
- };
346
- }
347
- );
348
-
349
- // ========================================
350
- // CHANGE TRACKING TOOLS
351
- // ========================================
352
-
353
- // Tool 9: speclock_log_change
354
- server.tool(
355
- "speclock_log_change",
356
- "Manually log a significant change. Use this when you make an important modification that should be tracked in the context.",
357
- {
358
- summary: z
359
- .string()
360
- .min(1)
361
- .describe("Brief description of the change"),
362
- files: z
363
- .array(z.string())
364
- .optional()
365
- .default([])
366
- .describe("Files affected"),
367
- },
368
- async ({ summary, files }) => {
369
- const { eventId } = logChange(PROJECT_ROOT, summary, files);
370
- return {
371
- content: [
372
- { type: "text", text: `Change logged (${eventId}): "${summary}"` },
373
- ],
374
- };
375
- }
376
- );
377
-
378
- // Tool 10: speclock_get_changes
379
- server.tool(
380
- "speclock_get_changes",
381
- "Get recent file changes tracked by SpecLock.",
382
- {
383
- limit: z
384
- .number()
385
- .int()
386
- .min(1)
387
- .max(100)
388
- .optional()
389
- .default(20),
390
- },
391
- async ({ limit }) => {
392
- const brain = readBrain(PROJECT_ROOT);
393
- if (!brain) {
394
- return {
395
- content: [
396
- {
397
- type: "text",
398
- text: "SpecLock not initialized. Run speclock_init first.",
399
- },
400
- ],
401
- isError: true,
402
- };
403
- }
404
- const changes = brain.state.recentChanges.slice(0, limit);
405
- if (changes.length === 0) {
406
- return {
407
- content: [{ type: "text", text: "No changes tracked yet." }],
408
- };
409
- }
410
- const formatted = changes
411
- .map((ch) => {
412
- const files =
413
- ch.files && ch.files.length > 0
414
- ? ` (${ch.files.join(", ")})`
415
- : "";
416
- return `- [${ch.at.substring(0, 19)}] ${ch.summary}${files}`;
417
- })
418
- .join("\n");
419
- return {
420
- content: [
421
- { type: "text", text: `Recent changes (${changes.length}):\n${formatted}` },
422
- ],
423
- };
424
- }
425
- );
426
-
427
- // Tool 11: speclock_get_events
428
- server.tool(
429
- "speclock_get_events",
430
- "Get the event log, optionally filtered by type. Event types: init, goal_updated, lock_added, lock_removed, decision_added, note_added, fact_updated, file_created, file_changed, file_deleted, revert_detected, context_generated, session_started, session_ended, manual_change, checkpoint_created.",
431
- {
432
- type: z.string().optional().describe("Filter by event type"),
433
- limit: z
434
- .number()
435
- .int()
436
- .min(1)
437
- .max(200)
438
- .optional()
439
- .default(50),
440
- since: z
441
- .string()
442
- .optional()
443
- .describe("ISO timestamp; only return events after this time"),
444
- },
445
- async ({ type, limit, since }) => {
446
- const events = readEvents(PROJECT_ROOT, { type, limit, since });
447
- if (events.length === 0) {
448
- return {
449
- content: [{ type: "text", text: "No events found." }],
450
- };
451
- }
452
- const formatted = events
453
- .map(
454
- (e) =>
455
- `- [${e.at.substring(0, 19)}] ${e.type}: ${e.summary || ""}`
456
- )
457
- .join("\n");
458
- return {
459
- content: [
460
- {
461
- type: "text",
462
- text: `Events (${events.length}):\n${formatted}`,
463
- },
464
- ],
465
- };
466
- }
467
- );
468
-
469
- // ========================================
470
- // CONTINUITY PROTECTION TOOLS
471
- // ========================================
472
-
473
- // Tool 12: speclock_check_conflict (v4.1: hybrid heuristic + Gemini LLM)
474
- server.tool(
475
- "speclock_check_conflict",
476
- "Check if a proposed action conflicts with any active SpecLock. Uses fast heuristic + Gemini LLM for universal domain coverage. In hard enforcement mode, conflicts above the threshold will BLOCK the action (isError: true).",
477
- {
478
- proposedAction: z
479
- .string()
480
- .min(1)
481
- .describe("Description of the action you plan to take"),
482
- },
483
- async ({ proposedAction }) => {
484
- // Hybrid check: heuristic first, LLM for grey-zone (1-70%)
485
- let result = await checkConflictAsync(PROJECT_ROOT, proposedAction);
486
-
487
- // If async hybrid returned no conflict, also check enforcer for hard mode
488
- if (!result.hasConflict) {
489
- const enforced = enforceConflictCheck(PROJECT_ROOT, proposedAction);
490
- if (enforced.blocked) {
491
- return {
492
- content: [{ type: "text", text: enforced.analysis }],
493
- isError: true,
494
- };
495
- }
496
- }
497
-
498
- // In hard mode with blocking conflict, return isError: true
499
- if (result.blocked) {
500
- return {
501
- content: [{ type: "text", text: result.analysis }],
502
- isError: true,
503
- };
504
- }
505
-
506
- return {
507
- content: [{ type: "text", text: result.analysis }],
508
- };
509
- }
510
- );
511
-
512
- // Tool 13: speclock_session_briefing
513
- server.tool(
514
- "speclock_session_briefing",
515
- "Start a new session and get a full briefing. Returns context pack plus what happened since the last session. Call this at the very beginning of a new conversation.",
516
- {
517
- toolName: z
518
- .enum(["claude-code", "cursor", "codex", "windsurf", "cline", "unknown"])
519
- .optional()
520
- .default("unknown")
521
- .describe("Which AI tool is being used"),
522
- },
523
- async ({ toolName }) => {
524
- try {
525
- const briefing = getSessionBriefing(PROJECT_ROOT, toolName);
526
- const contextMd = generateContext(PROJECT_ROOT);
527
-
528
- const parts = [];
529
-
530
- // Session info
531
- parts.push(`# SpecLock Session Briefing`);
532
- parts.push(`Session started (${toolName}). ID: ${briefing.session?.id || "new"}`);
533
- parts.push("");
534
-
535
- // Last session summary
536
- if (briefing.lastSession) {
537
- parts.push("## Last Session");
538
- parts.push(`- Tool: **${briefing.lastSession.toolUsed || "unknown"}**`);
539
- parts.push(`- Ended: ${briefing.lastSession.endedAt || "unknown"}`);
540
- if (briefing.lastSession.summary)
541
- parts.push(`- Summary: ${briefing.lastSession.summary}`);
542
- parts.push(
543
- `- Events: ${briefing.lastSession.eventsInSession || 0}`
544
- );
545
- parts.push(
546
- `- Changes since then: ${briefing.changesSinceLastSession || 0}`
547
- );
548
- parts.push("");
549
- }
550
-
551
- // Warnings
552
- if (briefing.warnings?.length > 0) {
553
- parts.push("## Warnings");
554
- for (const w of briefing.warnings) {
555
- parts.push(`- ${w}`);
556
- }
557
- parts.push("");
558
- }
559
-
560
- // Full context
561
- parts.push("---");
562
- parts.push(contextMd);
563
-
564
- return {
565
- content: [{ type: "text", text: parts.join("\n") }],
566
- };
567
- } catch (err) {
568
- return {
569
- content: [{ type: "text", text: `# SpecLock Session Briefing\n\nError loading session: ${err.message}\n\nTry running speclock_init first.\n\n---\n*SpecLock v${VERSION}*` }],
570
- };
571
- }
572
- }
573
- );
574
-
575
- // Tool 14: speclock_session_summary
576
- server.tool(
577
- "speclock_session_summary",
578
- "End the current session and record what was accomplished. Call this before ending a conversation.",
579
- {
580
- summary: z
581
- .string()
582
- .min(1)
583
- .describe("Summary of what was accomplished in this session"),
584
- },
585
- async ({ summary }) => {
586
- const result = endSession(PROJECT_ROOT, summary);
587
- if (!result.ended) {
588
- return {
589
- content: [{ type: "text", text: result.error }],
590
- isError: true,
591
- };
592
- }
593
- const session = result.session;
594
- const duration =
595
- session.startedAt && session.endedAt
596
- ? Math.round(
597
- (new Date(session.endedAt) - new Date(session.startedAt)) /
598
- 60000
599
- )
600
- : 0;
601
- return {
602
- content: [
603
- {
604
- type: "text",
605
- text: `Session ended. Duration: ${duration} min. Events: ${session.eventsInSession}. Summary: "${summary}"`,
606
- },
607
- ],
608
- };
609
- }
610
- );
611
-
612
- // ========================================
613
- // GIT INTEGRATION TOOLS
614
- // ========================================
615
-
616
- // Tool 15: speclock_checkpoint
617
- server.tool(
618
- "speclock_checkpoint",
619
- "Create a named git tag checkpoint for easy rollback.",
620
- {
621
- name: z
622
- .string()
623
- .min(1)
624
- .describe("Checkpoint name (alphanumeric, hyphens, underscores)"),
625
- },
626
- async ({ name }) => {
627
- const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "_");
628
- const tag = `sl_${safeName}_${Date.now()}`;
629
- const result = createTag(PROJECT_ROOT, tag);
630
-
631
- if (!result.ok) {
632
- return {
633
- content: [
634
- {
635
- type: "text",
636
- text: `Failed to create checkpoint: ${result.error}`,
637
- },
638
- ],
639
- isError: true,
640
- };
641
- }
642
-
643
- // Record event
644
- const brain = readBrain(PROJECT_ROOT);
645
- if (brain) {
646
- const eventId = newId("evt");
647
- const event = {
648
- eventId,
649
- type: "checkpoint_created",
650
- at: nowIso(),
651
- files: [],
652
- summary: `Checkpoint: ${tag}`,
653
- patchPath: "",
654
- };
655
- bumpEvents(brain, eventId);
656
- appendEvent(PROJECT_ROOT, event);
657
- writeBrain(PROJECT_ROOT, brain);
658
- }
659
-
660
- return {
661
- content: [
662
- { type: "text", text: `Checkpoint created: ${tag}` },
663
- ],
664
- };
665
- }
666
- );
667
-
668
- // Tool 16: speclock_repo_status
669
- server.tool(
670
- "speclock_repo_status",
671
- "Get current git repository status including branch, commit, changed files, and diff summary.",
672
- {},
673
- async () => {
674
- const status = captureStatus(PROJECT_ROOT);
675
- const diffSummary = getDiffSummary(PROJECT_ROOT);
676
-
677
- const lines = [
678
- `Branch: ${status.branch}`,
679
- `Commit: ${status.commit}`,
680
- "",
681
- `Changed files (${status.changedFiles.length}):`,
682
- ];
683
-
684
- if (status.changedFiles.length > 0) {
685
- for (const f of status.changedFiles) {
686
- lines.push(` ${f.status} ${f.file}`);
687
- }
688
- } else {
689
- lines.push(" (clean working tree)");
690
- }
691
-
692
- if (diffSummary) {
693
- lines.push("");
694
- lines.push("Diff summary:");
695
- lines.push(diffSummary);
696
- }
697
-
698
- return {
699
- content: [{ type: "text", text: lines.join("\n") }],
700
- };
701
- }
702
- );
703
-
704
- // ========================================
705
- // INTELLIGENCE TOOLS
706
- // ========================================
707
-
708
- // Tool 17: speclock_suggest_locks
709
- server.tool(
710
- "speclock_suggest_locks",
711
- "Analyze project decisions, notes, and patterns to suggest new SpecLock constraints. Returns auto-generated lock suggestions based on commitment language, prohibitive patterns, and common best practices.",
712
- {},
713
- async () => {
714
- const result = suggestLocks(PROJECT_ROOT);
715
-
716
- if (result.suggestions.length === 0) {
717
- return {
718
- content: [
719
- {
720
- type: "text",
721
- text: `No suggestions at this time. You have ${result.totalLocks} active lock(s). Add more decisions and notes to get AI-powered lock suggestions.`,
722
- },
723
- ],
724
- };
725
- }
726
-
727
- const formatted = result.suggestions
728
- .map(
729
- (s, i) =>
730
- `${i + 1}. **"${s.text}"**\n Source: ${s.source}${s.sourceId ? ` (${s.sourceId})` : ""}\n Reason: ${s.reason}`
731
- )
732
- .join("\n\n");
733
-
734
- return {
735
- content: [
736
- {
737
- type: "text",
738
- text: `## Lock Suggestions (${result.suggestions.length})\n\nCurrent active locks: ${result.totalLocks}\n\n${formatted}\n\nTo add any suggestion as a lock, call \`speclock_add_lock\` with the text.`,
739
- },
740
- ],
741
- };
742
- }
743
- );
744
-
745
- // Tool 18: speclock_detect_drift
746
- server.tool(
747
- "speclock_detect_drift",
748
- "Scan recent changes and events against active SpecLock constraints to detect potential violations or drift. Use this proactively to ensure project integrity.",
749
- {},
750
- async () => {
751
- const result = detectDrift(PROJECT_ROOT);
752
-
753
- if (result.status === "no_locks") {
754
- return {
755
- content: [{ type: "text", text: result.message }],
756
- };
757
- }
758
-
759
- if (result.status === "clean") {
760
- return {
761
- content: [{ type: "text", text: result.message }],
762
- };
763
- }
764
-
765
- const formatted = result.drifts
766
- .map(
767
- (d) =>
768
- `- [${d.severity.toUpperCase()}] Change: "${d.changeSummary}" (${d.changeAt.substring(0, 19)})\n Lock: "${d.lockText}"\n Matched: ${d.matchedTerms.join(", ")}`
769
- )
770
- .join("\n\n");
771
-
772
- return {
773
- content: [
774
- {
775
- type: "text",
776
- text: `## Drift Report\n\n${result.message}\n\n${formatted}\n\nReview each drift and take corrective action if needed.`,
777
- },
778
- ],
779
- };
780
- }
781
- );
782
-
783
- // Tool 19: speclock_health
784
- server.tool(
785
- "speclock_health",
786
- "Get a health check of the SpecLock setup including completeness score, missing recommended items, and multi-agent session timeline.",
787
- {},
788
- async () => {
789
- try {
790
- const brain = ensureInit(PROJECT_ROOT);
791
- const activeLocks = (brain.specLock?.items || []).filter((l) => l.active !== false);
792
-
793
- // Calculate health score
794
- let score = 0;
795
- const checks = [];
796
-
797
- if (brain.goal?.text) { score += 20; checks.push("[PASS] Goal is set"); }
798
- else checks.push("[MISS] No project goal set");
799
-
800
- if (activeLocks.length > 0) { score += 25; checks.push(`[PASS] ${activeLocks.length} active lock(s)`); }
801
- else checks.push("[MISS] No SpecLock constraints defined");
802
-
803
- if ((brain.decisions || []).length > 0) { score += 15; checks.push(`[PASS] ${brain.decisions.length} decision(s) recorded`); }
804
- else checks.push("[MISS] No decisions recorded");
805
-
806
- if ((brain.notes || []).length > 0) { score += 10; checks.push(`[PASS] ${brain.notes.length} note(s)`); }
807
- else checks.push("[MISS] No notes added");
808
-
809
- const sessionHistory = brain.sessions?.history || [];
810
- if (sessionHistory.length > 0) { score += 15; checks.push(`[PASS] ${sessionHistory.length} session(s) in history`); }
811
- else checks.push("[MISS] No session history yet");
812
-
813
- const recentChanges = brain.state?.recentChanges || [];
814
- if (recentChanges.length > 0) { score += 10; checks.push(`[PASS] ${recentChanges.length} change(s) tracked`); }
815
- else checks.push("[MISS] No changes tracked");
816
-
817
- if (brain.facts?.deploy?.provider && brain.facts.deploy.provider !== "unknown") { score += 5; checks.push("[PASS] Deploy facts configured"); }
818
- else checks.push("[MISS] Deploy facts not configured");
819
-
820
- // Multi-agent timeline
821
- const agentMap = {};
822
- for (const session of sessionHistory) {
823
- const tool = session.toolUsed || "unknown";
824
- if (!agentMap[tool]) agentMap[tool] = { count: 0, lastUsed: "", summaries: [] };
825
- agentMap[tool].count++;
826
- if (!agentMap[tool].lastUsed || (session.endedAt && session.endedAt > agentMap[tool].lastUsed)) {
827
- agentMap[tool].lastUsed = session.endedAt || session.startedAt || "";
828
- }
829
- if (session.summary && agentMap[tool].summaries.length < 3) {
830
- agentMap[tool].summaries.push(session.summary.substring(0, 80));
831
- }
832
- }
833
-
834
- let agentTimeline = "";
835
- if (Object.keys(agentMap).length > 0) {
836
- agentTimeline = "\n\n## Multi-Agent Timeline\n" +
837
- Object.entries(agentMap)
838
- .map(([tool, info]) =>
839
- `- **${tool}**: ${info.count} session(s), last active ${info.lastUsed ? info.lastUsed.substring(0, 16) : "unknown"}\n Recent: ${info.summaries.length > 0 ? info.summaries.map(s => `"${s}"`).join(", ") : "(no summaries)"}`
840
- )
841
- .join("\n");
842
- }
843
-
844
- const grade = score >= 80 ? "A" : score >= 60 ? "B" : score >= 40 ? "C" : score >= 20 ? "D" : "F";
845
- const evtCount = brain.events?.count || 0;
846
- const revertCount = (brain.state?.reverts || []).length;
847
-
848
- return {
849
- content: [
850
- {
851
- type: "text",
852
- text: `## SpecLock Health Check\n\nScore: **${score}/100** (Grade: ${grade})\nEvents: ${evtCount} | Reverts: ${revertCount}\n\n### Checks\n${checks.join("\n")}${agentTimeline}\n\n---\n*SpecLock v${VERSION} — Developed by ${AUTHOR}*`,
853
- },
854
- ],
855
- };
856
- } catch (err) {
857
- return {
858
- content: [{ type: "text", text: `## SpecLock Health Check\n\nError: ${err.message}\n\nTry running speclock_init first to initialize the project.\n\n---\n*SpecLock v${VERSION}*` }],
859
- };
860
- }
861
- }
862
- );
863
-
864
- // ========================================
865
- // TEMPLATE, REPORT & AUDIT TOOLS (v1.7.0)
866
- // ========================================
867
-
868
- // Tool 20: speclock_apply_template
869
- server.tool(
870
- "speclock_apply_template",
871
- "Apply a pre-built constraint template (e.g., nextjs, react, express, supabase, stripe, security-hardened). Templates add recommended locks and decisions for common frameworks.",
872
- {
873
- name: z
874
- .string()
875
- .optional()
876
- .describe("Template name to apply. Omit to list available templates."),
877
- },
878
- async ({ name }) => {
879
- if (!name) {
880
- const templates = listTemplates();
881
- const formatted = templates
882
- .map((t) => `- **${t.name}** (${t.displayName}): ${t.description} — ${t.lockCount} locks, ${t.decisionCount} decisions`)
883
- .join("\n");
884
- return {
885
- content: [
886
- {
887
- type: "text",
888
- text: `## Available Templates\n\n${formatted}\n\nCall again with a name to apply.`,
889
- },
890
- ],
891
- };
892
- }
893
- const result = applyTemplate(PROJECT_ROOT, name);
894
- if (!result.applied) {
895
- return {
896
- content: [{ type: "text", text: result.error }],
897
- isError: true,
898
- };
899
- }
900
- return {
901
- content: [
902
- {
903
- type: "text",
904
- text: `Template "${result.displayName}" applied: ${result.locksAdded} lock(s) + ${result.decisionsAdded} decision(s) added.`,
905
- },
906
- ],
907
- };
908
- }
909
- );
910
-
911
- // Tool 21: speclock_report
912
- server.tool(
913
- "speclock_report",
914
- "Get a violation report showing how many times SpecLock blocked constraint violations, which locks were tested most, and recent violations.",
915
- {},
916
- async () => {
917
- const report = generateReport(PROJECT_ROOT);
918
-
919
- const parts = [`## SpecLock Violation Report`, ``, `Total violations blocked: **${report.totalViolations}**`];
920
-
921
- if (report.timeRange) {
922
- parts.push(`Period: ${report.timeRange.from.substring(0, 10)} to ${report.timeRange.to.substring(0, 10)}`);
923
- }
924
-
925
- if (report.mostTestedLocks.length > 0) {
926
- parts.push("", "### Most Tested Locks");
927
- for (const lock of report.mostTestedLocks) {
928
- parts.push(`- ${lock.count}x — "${lock.text}"`);
929
- }
930
- }
931
-
932
- if (report.recentViolations.length > 0) {
933
- parts.push("", "### Recent Violations");
934
- for (const v of report.recentViolations) {
935
- parts.push(`- [${v.at.substring(0, 19)}] ${v.topLevel} (${v.topConfidence}%) — "${v.action}"`);
936
- }
937
- }
938
-
939
- parts.push("", `---`, report.summary);
940
-
941
- return {
942
- content: [{ type: "text", text: parts.join("\n") }],
943
- };
944
- }
945
- );
946
-
947
- // Tool 22: speclock_audit
948
- server.tool(
949
- "speclock_audit",
950
- "Audit git staged files against active SpecLock constraints. Returns pass/fail with details on which files violate locks. Used by the pre-commit hook.",
951
- {},
952
- async () => {
953
- const result = auditStagedFiles(PROJECT_ROOT);
954
-
955
- if (result.passed) {
956
- return {
957
- content: [{ type: "text", text: result.message }],
958
- };
959
- }
960
-
961
- const formatted = result.violations
962
- .map((v) => `- [${v.severity}] **${v.file}** — ${v.reason}\n Lock: "${v.lockText}"`)
963
- .join("\n");
964
-
965
- return {
966
- content: [
967
- {
968
- type: "text",
969
- text: `## Audit Failed\n\n${formatted}\n\n${result.message}`,
970
- },
971
- ],
972
- };
973
- }
974
- );
975
-
976
- // ========================================
977
- // ENTERPRISE TOOLS (v2.1)
978
- // ========================================
979
-
980
- // Tool 23: speclock_verify_audit
981
- server.tool(
982
- "speclock_verify_audit",
983
- "Verify the integrity of the HMAC audit chain. Detects tampering or corruption in the event log. Returns chain status, total events, and any broken links.",
984
- {},
985
- async () => {
986
- ensureInit(PROJECT_ROOT);
987
- const result = verifyAuditChain(PROJECT_ROOT);
988
-
989
- const status = result.valid ? "VALID" : "BROKEN";
990
- const parts = [
991
- `## Audit Chain Verification`,
992
- ``,
993
- `Status: **${status}**`,
994
- `Total events: ${result.totalEvents}`,
995
- `Hashed events: ${result.hashedEvents}`,
996
- `Legacy events (pre-v2.1): ${result.unhashedEvents}`,
997
- ];
998
-
999
- if (!result.valid && result.errors) {
1000
- parts.push(``, `### Errors`);
1001
- for (const err of result.errors) {
1002
- parts.push(`- Line ${err.line}: ${err.error}${err.eventId ? ` (${err.eventId})` : ""}`);
1003
- }
1004
- }
1005
-
1006
- parts.push(``, result.message);
1007
- parts.push(``, `Verified at: ${result.verifiedAt}`);
1008
-
1009
- return {
1010
- content: [{ type: "text", text: parts.join("\n") }],
1011
- };
1012
- }
1013
- );
1014
-
1015
- // Tool 24: speclock_export_compliance
1016
- server.tool(
1017
- "speclock_export_compliance",
1018
- "Generate compliance reports for enterprise auditing. Supports SOC 2 Type II, HIPAA, and CSV formats. Reports include constraint management, access logs, audit chain integrity, and violation history.",
1019
- {
1020
- format: z
1021
- .enum(["soc2", "hipaa", "csv"])
1022
- .describe("Export format: soc2 (JSON), hipaa (JSON), csv (spreadsheet)"),
1023
- },
1024
- async ({ format }) => {
1025
- ensureInit(PROJECT_ROOT);
1026
- const result = exportCompliance(PROJECT_ROOT, format);
1027
-
1028
- if (result.error) {
1029
- return {
1030
- content: [{ type: "text", text: result.error }],
1031
- isError: true,
1032
- };
1033
- }
1034
-
1035
- if (format === "csv") {
1036
- return {
1037
- content: [{ type: "text", text: `## Compliance Export (CSV)\n\n\`\`\`csv\n${result.data}\n\`\`\`` }],
1038
- };
1039
- }
1040
-
1041
- return {
1042
- content: [{ type: "text", text: `## Compliance Export (${format.toUpperCase()})\n\n\`\`\`json\n${JSON.stringify(result.data, null, 2)}\n\`\`\`` }],
1043
- };
1044
- }
1045
- );
1046
-
1047
- // ========================================
1048
- // HARD ENFORCEMENT TOOLS (v2.5)
1049
- // ========================================
1050
-
1051
- // Tool 25: speclock_set_enforcement
1052
- server.tool(
1053
- "speclock_set_enforcement",
1054
- "Set the enforcement mode for this project. 'advisory' (default) warns about conflicts. 'hard' blocks actions that violate locks above the confidence threshold — the AI cannot proceed.",
1055
- {
1056
- mode: z
1057
- .enum(["advisory", "hard"])
1058
- .describe("Enforcement mode: advisory (warn) or hard (block)"),
1059
- blockThreshold: z
1060
- .number()
1061
- .int()
1062
- .min(0)
1063
- .max(100)
1064
- .optional()
1065
- .default(70)
1066
- .describe("Minimum confidence % to block in hard mode (default: 70)"),
1067
- allowOverride: z
1068
- .boolean()
1069
- .optional()
1070
- .default(true)
1071
- .describe("Whether lock overrides are permitted"),
1072
- },
1073
- async ({ mode, blockThreshold, allowOverride }) => {
1074
- const result = setEnforcementMode(PROJECT_ROOT, mode, { blockThreshold, allowOverride });
1075
- if (!result.success) {
1076
- return {
1077
- content: [{ type: "text", text: result.error }],
1078
- isError: true,
1079
- };
1080
- }
1081
- return {
1082
- content: [
1083
- {
1084
- type: "text",
1085
- text: `Enforcement mode set to **${mode}**. Threshold: ${result.config.blockThreshold}%. Overrides: ${result.config.allowOverride ? "allowed" : "disabled"}.`,
1086
- },
1087
- ],
1088
- };
1089
- }
1090
- );
1091
-
1092
- // Tool 26: speclock_override_lock
1093
- server.tool(
1094
- "speclock_override_lock",
1095
- "Override a lock for a specific action. Requires a reason which is logged to the audit trail. Use when a locked action must proceed with justification. Triggers escalation after repeated overrides.",
1096
- {
1097
- lockId: z.string().min(1).describe("The lock ID to override"),
1098
- action: z.string().min(1).describe("The action that conflicts with the lock"),
1099
- reason: z.string().min(1).describe("Justification for the override"),
1100
- },
1101
- async ({ lockId, action, reason }) => {
1102
- const result = overrideLock(PROJECT_ROOT, lockId, action, reason);
1103
- if (!result.success) {
1104
- return {
1105
- content: [{ type: "text", text: result.error }],
1106
- isError: true,
1107
- };
1108
- }
1109
-
1110
- const parts = [
1111
- `Lock overridden: "${result.lockText}"`,
1112
- `Override count: ${result.overrideCount}`,
1113
- `Reason: ${reason}`,
1114
- ];
1115
-
1116
- if (result.escalated) {
1117
- parts.push("", result.escalationMessage);
1118
- }
1119
-
1120
- return {
1121
- content: [{ type: "text", text: parts.join("\n") }],
1122
- };
1123
- }
1124
- );
1125
-
1126
- // Tool 27: speclock_semantic_audit
1127
- server.tool(
1128
- "speclock_semantic_audit",
1129
- "Run semantic pre-commit audit. Parses the staged git diff, analyzes actual code changes against active locks using semantic analysis. Much more powerful than filename-only audit — catches violations in code content.",
1130
- {},
1131
- async () => {
1132
- const result = semanticAudit(PROJECT_ROOT);
1133
-
1134
- if (result.passed) {
1135
- return {
1136
- content: [{ type: "text", text: result.message }],
1137
- };
1138
- }
1139
-
1140
- const formatted = result.violations
1141
- .map((v) => {
1142
- const lines = [`- [${v.level}] **${v.file}** (confidence: ${v.confidence}%)`];
1143
- lines.push(` Lock: "${v.lockText}"`);
1144
- lines.push(` Reason: ${v.reason}`);
1145
- if (v.addedLines) lines.push(` Changes: +${v.addedLines} / -${v.removedLines} lines`);
1146
- return lines.join("\n");
1147
- })
1148
- .join("\n\n");
1149
-
1150
- return {
1151
- content: [
1152
- {
1153
- type: "text",
1154
- text: `## Semantic Audit Result\n\nMode: ${result.mode} | Threshold: ${result.threshold}%\n\n${formatted}\n\n${result.message}`,
1155
- },
1156
- ],
1157
- isError: result.blocked || false,
1158
- };
1159
- }
1160
- );
1161
-
1162
- // Tool 28: speclock_override_history
1163
- server.tool(
1164
- "speclock_override_history",
1165
- "Get the history of lock overrides. Shows which locks have been overridden, by whom, and the reasons given. Useful for audit review and identifying locks that may need updating.",
1166
- {
1167
- lockId: z
1168
- .string()
1169
- .optional()
1170
- .describe("Filter by specific lock ID. Omit to see all overrides."),
1171
- },
1172
- async ({ lockId }) => {
1173
- const result = getOverrideHistory(PROJECT_ROOT, lockId);
1174
-
1175
- if (result.total === 0) {
1176
- return {
1177
- content: [{ type: "text", text: "No overrides recorded." }],
1178
- };
1179
- }
1180
-
1181
- const formatted = result.overrides
1182
- .map(
1183
- (o) =>
1184
- `- [${o.at.substring(0, 19)}] Lock: "${o.lockText}" (${o.lockId})\n Action: ${o.action}\n Reason: ${o.reason}`
1185
- )
1186
- .join("\n\n");
1187
-
1188
- return {
1189
- content: [
1190
- {
1191
- type: "text",
1192
- text: `## Override History (${result.total})\n\n${formatted}`,
1193
- },
1194
- ],
1195
- };
1196
- }
1197
- );
1198
-
1199
- // ========================================
1200
- // POLICY-AS-CODE TOOLS (v3.5)
1201
- // ========================================
1202
-
1203
- // Tool 29: speclock_policy_evaluate
1204
- server.tool(
1205
- "speclock_policy_evaluate",
1206
- "Evaluate policy-as-code rules against a proposed action. Returns violations for any matching rules. Use alongside speclock_check_conflict for comprehensive protection.",
1207
- {
1208
- description: z.string().min(1).describe("Description of the action to evaluate"),
1209
- files: z.array(z.string()).optional().default([]).describe("Files affected by the action"),
1210
- type: z.enum(["modify", "delete", "create", "export"]).optional().default("modify").describe("Action type"),
1211
- },
1212
- async ({ description, files, type }) => {
1213
- const result = evaluatePolicy(PROJECT_ROOT, { description, text: description, files, type });
1214
-
1215
- if (result.passed) {
1216
- return {
1217
- content: [{ type: "text", text: `Policy check passed. ${result.rulesChecked} rule(s) evaluated, no violations.` }],
1218
- };
1219
- }
1220
-
1221
- const formatted = result.violations
1222
- .map(v => `- [${v.severity.toUpperCase()}] **${v.ruleName}** (${v.enforce})\n ${v.description}\n Files: ${v.matchedFiles.join(", ") || "(pattern match)"}`)
1223
- .join("\n\n");
1224
-
1225
- return {
1226
- content: [{ type: "text", text: `## Policy Violations (${result.violations.length})\n\n${formatted}` }],
1227
- isError: result.blocked,
1228
- };
1229
- }
1230
- );
1231
-
1232
- // Tool 30: speclock_policy_manage
1233
- server.tool(
1234
- "speclock_policy_manage",
1235
- "Manage policy-as-code rules. Actions: list (show all rules), add (create new rule), remove (delete rule), init (create default policy), export (portable YAML).",
1236
- {
1237
- action: z.enum(["list", "add", "remove", "init", "export"]).describe("Policy action"),
1238
- rule: z.object({
1239
- name: z.string().optional(),
1240
- description: z.string().optional(),
1241
- match: z.object({
1242
- files: z.array(z.string()).optional(),
1243
- actions: z.array(z.string()).optional(),
1244
- }).optional(),
1245
- enforce: z.enum(["block", "warn", "log"]).optional(),
1246
- severity: z.enum(["critical", "high", "medium", "low"]).optional(),
1247
- notify: z.array(z.string()).optional(),
1248
- }).optional().describe("Rule definition (for add action)"),
1249
- ruleId: z.string().optional().describe("Rule ID (for remove action)"),
1250
- },
1251
- async ({ action, rule, ruleId }) => {
1252
- switch (action) {
1253
- case "list": {
1254
- const result = listPolicyRules(PROJECT_ROOT);
1255
- if (result.total === 0) {
1256
- return { content: [{ type: "text", text: "No policy rules defined. Use action 'init' to create a default policy." }] };
1257
- }
1258
- const formatted = result.rules.map(r =>
1259
- `- **${r.name}** (${r.id}) [${r.enforce}/${r.severity}]\n Files: ${(r.match?.files || []).join(", ")}\n Actions: ${(r.match?.actions || []).join(", ")}`
1260
- ).join("\n\n");
1261
- return { content: [{ type: "text", text: `## Policy Rules (${result.active}/${result.total} active)\n\n${formatted}` }] };
1262
- }
1263
- case "add": {
1264
- if (!rule || !rule.name) {
1265
- return { content: [{ type: "text", text: "Rule name is required." }], isError: true };
1266
- }
1267
- const result = addPolicyRule(PROJECT_ROOT, rule);
1268
- if (!result.success) return { content: [{ type: "text", text: result.error }], isError: true };
1269
- return { content: [{ type: "text", text: `Policy rule added: "${result.rule.name}" (${result.ruleId}) [${result.rule.enforce}]` }] };
1270
- }
1271
- case "remove": {
1272
- if (!ruleId) return { content: [{ type: "text", text: "ruleId is required." }], isError: true };
1273
- const result = removePolicyRule(PROJECT_ROOT, ruleId);
1274
- if (!result.success) return { content: [{ type: "text", text: result.error }], isError: true };
1275
- return { content: [{ type: "text", text: `Policy rule removed: "${result.removed.name}"` }] };
1276
- }
1277
- case "init": {
1278
- const result = initPolicy(PROJECT_ROOT);
1279
- if (!result.success) return { content: [{ type: "text", text: result.error }], isError: true };
1280
- return { content: [{ type: "text", text: "Policy-as-code initialized. Edit .speclock/policy.yml to add rules." }] };
1281
- }
1282
- case "export": {
1283
- const result = exportPolicy(PROJECT_ROOT);
1284
- if (!result.success) return { content: [{ type: "text", text: result.error }], isError: true };
1285
- return { content: [{ type: "text", text: `## Exported Policy\n\n\`\`\`yaml\n${result.yaml}\`\`\`` }] };
1286
- }
1287
- default:
1288
- return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
1289
- }
1290
- }
1291
- );
1292
-
1293
- // ========================================
1294
- // TELEMETRY TOOLS (v3.5)
1295
- // ========================================
1296
-
1297
- // Tool 31: speclock_telemetry
1298
- server.tool(
1299
- "speclock_telemetry",
1300
- "Get telemetry and analytics summary. Shows tool usage counts, conflict rates, response times, and feature adoption. Opt-in only (SPECLOCK_TELEMETRY=true).",
1301
- {},
1302
- async () => {
1303
- const summary = getTelemetrySummary(PROJECT_ROOT);
1304
- if (!summary.enabled) {
1305
- return { content: [{ type: "text", text: summary.message }] };
1306
- }
1307
-
1308
- const parts = [
1309
- `## Telemetry Summary`,
1310
- ``,
1311
- `Total API calls: **${summary.totalCalls}**`,
1312
- `Avg response: **${summary.avgResponseMs}ms**`,
1313
- `Sessions: **${summary.sessions.total}**`,
1314
- ``,
1315
- `### Conflicts`,
1316
- `Total: ${summary.conflicts.total} | Blocked: ${summary.conflicts.blocked} | Advisory: ${summary.conflicts.advisory}`,
1317
- ];
1318
-
1319
- if (summary.topTools.length > 0) {
1320
- parts.push(``, `### Top Tools`);
1321
- for (const t of summary.topTools.slice(0, 5)) {
1322
- parts.push(`- ${t.name}: ${t.count} calls (avg ${t.avgMs}ms)`);
1323
- }
1324
- }
1325
-
1326
- if (summary.features.length > 0) {
1327
- parts.push(``, `### Feature Adoption`);
1328
- for (const f of summary.features) {
1329
- parts.push(`- ${f.name}: ${f.count} uses`);
1330
- }
1331
- }
1332
-
1333
- return { content: [{ type: "text", text: parts.join("\n") }] };
1334
- }
1335
- );
1336
-
1337
- // --- Smithery sandbox export ---
1338
- export default function createSandboxServer() {
1339
- return server;
1340
- }
1341
-
1342
- // --- Start server (skip when bundled as CJS for Smithery scanning) ---
1343
- const isScanMode = typeof import.meta.url === "undefined";
1344
-
1345
- if (!isScanMode) {
1346
- const transport = new StdioServerTransport();
1347
- server.connect(transport).then(() => {
1348
- process.stderr.write(
1349
- `SpecLock MCP v${VERSION} running (stdio) — Developed by ${AUTHOR}. Root: ${PROJECT_ROOT}${os.EOL}`
1350
- );
1351
- }).catch((err) => {
1352
- process.stderr.write(`SpecLock fatal: ${err.message}${os.EOL}`);
1353
- process.exit(1);
1354
- });
1355
- }
1
+ import os from "os";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import {
6
+ ensureInit,
7
+ setGoal,
8
+ addLock,
9
+ removeLock,
10
+ addDecision,
11
+ addNote,
12
+ updateDeployFacts,
13
+ logChange,
14
+ checkConflict,
15
+ checkConflictAsync,
16
+ getSessionBriefing,
17
+ endSession,
18
+ suggestLocks,
19
+ detectDrift,
20
+ syncLocksToPackageJson,
21
+ autoGuardRelatedFiles,
22
+ listTemplates,
23
+ applyTemplate,
24
+ generateReport,
25
+ auditStagedFiles,
26
+ verifyAuditChain,
27
+ exportCompliance,
28
+ checkLimits,
29
+ getLicenseInfo,
30
+ enforceConflictCheck,
31
+ setEnforcementMode,
32
+ overrideLock,
33
+ getOverrideHistory,
34
+ getEnforcementConfig,
35
+ semanticAudit,
36
+ evaluatePolicy,
37
+ listPolicyRules,
38
+ addPolicyRule,
39
+ removePolicyRule,
40
+ initPolicy,
41
+ exportPolicy,
42
+ importPolicy,
43
+ isTelemetryEnabled,
44
+ getTelemetrySummary,
45
+ trackToolUsage,
46
+ } from "../core/engine.js";
47
+ import { generateContext, generateContextPack } from "../core/context.js";
48
+ import {
49
+ readBrain,
50
+ readEvents,
51
+ newId,
52
+ nowIso,
53
+ appendEvent,
54
+ bumpEvents,
55
+ writeBrain,
56
+ } from "../core/storage.js";
57
+ import {
58
+ captureStatus,
59
+ createTag,
60
+ getDiffSummary,
61
+ } from "../core/git.js";
62
+ import {
63
+ isAuthEnabled,
64
+ validateApiKey,
65
+ checkPermission,
66
+ } from "../core/auth.js";
67
+
68
+ // --- Auth via env var (v3.0) ---
69
+ function getAuthRole() {
70
+ if (!isAuthEnabled(PROJECT_ROOT)) return "admin";
71
+ const key = process.env.SPECLOCK_API_KEY;
72
+ if (!key) return "admin"; // No key env var = local use, allow all
73
+ const result = validateApiKey(PROJECT_ROOT, key);
74
+ return result.valid ? result.role : null;
75
+ }
76
+
77
+ function requirePermission(toolName) {
78
+ const role = getAuthRole();
79
+ if (!role) return { allowed: false, error: "Invalid SPECLOCK_API_KEY." };
80
+ if (!checkPermission(role, toolName)) {
81
+ return { allowed: false, error: `Permission denied. Role "${role}" cannot access "${toolName}".` };
82
+ }
83
+ return { allowed: true, role };
84
+ }
85
+
86
+ // --- Project root resolution ---
87
+ function parseArgs(argv) {
88
+ const args = { project: null };
89
+ for (let i = 2; i < argv.length; i++) {
90
+ if (argv[i] === "--project" && argv[i + 1]) {
91
+ args.project = argv[i + 1];
92
+ i++;
93
+ }
94
+ }
95
+ return args;
96
+ }
97
+
98
+ const args = parseArgs(process.argv);
99
+ const PROJECT_ROOT =
100
+ args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
101
+
102
+ // --- MCP Server ---
103
+ const VERSION = "4.5.5";
104
+ const AUTHOR = "Sandeep Roy";
105
+
106
+ const server = new McpServer(
107
+ { name: "speclock", version: VERSION },
108
+ {
109
+ instructions: [
110
+ `CRITICAL: You have SpecLock connected — an AI Continuity Engine that remembers project context across sessions. You MUST follow these rules:`,
111
+ ``,
112
+ `## STEP 1 — FIRST THING IN EVERY CONVERSATION (MANDATORY):`,
113
+ `Before doing ANYTHING else, call speclock_session_briefing. This loads the project's goal, constraints, decisions, and history from previous sessions.`,
114
+ `- If it returns "not initialized", call speclock_init first, then tell the user: "I've set up project memory for this workspace. I'll remember your decisions and constraints across sessions."`,
115
+ `- If it returns context, start your response with a brief one-line status like: "🔒 Project memory loaded — [X] constraints, [Y] decisions tracked from previous sessions." Then proceed to answer the user's actual question.`,
116
+ `- If there is no meaningful context yet (new project), say: "🔒 Project memory active. I'll automatically remember your key decisions and constraints across sessions." Then proceed normally.`,
117
+ ``,
118
+ `## STEP 2 — LISTEN AND CAPTURE (do this continuously as you work):`,
119
+ `- When the user describes what they're building → call speclock_set_goal.`,
120
+ `- When the user says something is non-negotiable or uses words like "always", "never", "must", "don't ever change", "make sure to keep", "this is critical" → call speclock_add_lock. Then briefly confirm: "🔒 Locked: [constraint]"`,
121
+ `- When the user makes a technology or design choice → call speclock_add_decision.`,
122
+ `- When you complete significant work → call speclock_log_change.`,
123
+ ``,
124
+ `## STEP 3 — PROTECT (before making significant changes):`,
125
+ `- Before removing features, changing architecture, switching technologies, or modifying core logic → call speclock_check_conflict.`,
126
+ `- If a conflict is found (confidence HIGH), STOP and warn: "⚠️ This would conflict with a constraint you set: [constraint]. Should I proceed anyway or keep the current approach?"`,
127
+ `- Do NOT proceed unless the user explicitly confirms.`,
128
+ ``,
129
+ `## STEP 4 — REMEMBER (when the user wants to change their mind):`,
130
+ `- If they want to remove or change something they previously locked, remind them: "You previously locked: [constraint]. Want me to unlock this and proceed?"`,
131
+ `- If confirmed, call speclock_remove_lock, then proceed.`,
132
+ ``,
133
+ `## STEP 5 — CLOSE (when conversation ends):`,
134
+ `- Call speclock_session_summary with a brief description of what was done.`,
135
+ ``,
136
+ `## KEY BEHAVIOR:`,
137
+ `- The 🔒 emoji is your SpecLock indicator. Users learn to recognize it means their project memory is active.`,
138
+ `- Keep SpecLock confirmations SHORT — one line max. Don't dump data.`,
139
+ `- Present remembered context NATURALLY. Instead of "SpecLock says your goal is X", just say "Based on our previous work, the goal is X."`,
140
+ `- If the user asks "how do you remember this?" — explain that SpecLock is connected as a project memory layer that persists across sessions.`,
141
+ ].join("\n"),
142
+ }
143
+ );
144
+
145
+ // ========================================
146
+ // MEMORY MANAGEMENT TOOLS
147
+ // ========================================
148
+
149
+ // Tool 1: speclock_init
150
+ server.tool(
151
+ "speclock_init",
152
+ "Initialize SpecLock in the current project directory. Creates .speclock/ with brain.json, events.log, and supporting directories.",
153
+ {},
154
+ async () => {
155
+ const brain = ensureInit(PROJECT_ROOT);
156
+ return {
157
+ content: [
158
+ {
159
+ type: "text",
160
+ text: `SpecLock initialized for "${brain.project.name}" at ${brain.project.root}`,
161
+ },
162
+ ],
163
+ };
164
+ }
165
+ );
166
+
167
+ // Tool 2: speclock_get_context — THE KEY TOOL
168
+ server.tool(
169
+ "speclock_get_context",
170
+ "THE KEY TOOL. Returns the full structured context pack including goal, locks, decisions, recent changes, deploy facts, reverts, session history, and notes. Call this at the start of every session or whenever you need to refresh your understanding of the project.",
171
+ {
172
+ format: z
173
+ .enum(["markdown", "json"])
174
+ .optional()
175
+ .default("markdown")
176
+ .describe("Output format: markdown (readable) or json (structured)"),
177
+ },
178
+ async ({ format }) => {
179
+ if (format === "json") {
180
+ const pack = generateContextPack(PROJECT_ROOT);
181
+ return {
182
+ content: [{ type: "text", text: JSON.stringify(pack, null, 2) }],
183
+ };
184
+ }
185
+ const md = generateContext(PROJECT_ROOT);
186
+ return { content: [{ type: "text", text: md }] };
187
+ }
188
+ );
189
+
190
+ // Tool 3: speclock_set_goal
191
+ server.tool(
192
+ "speclock_set_goal",
193
+ "Set or update the project goal. This is the high-level objective that guides all work.",
194
+ {
195
+ text: z.string().min(1).describe("The project goal text"),
196
+ },
197
+ async ({ text }) => {
198
+ setGoal(PROJECT_ROOT, text);
199
+ return {
200
+ content: [{ type: "text", text: `Goal set: "${text}"` }],
201
+ };
202
+ }
203
+ );
204
+
205
+ // Tool 4: speclock_add_lock
206
+ server.tool(
207
+ "speclock_add_lock",
208
+ "Add a non-negotiable constraint (SpecLock). These are rules that must NEVER be violated during development.",
209
+ {
210
+ text: z.string().min(1).describe("The constraint text"),
211
+ tags: z
212
+ .array(z.string())
213
+ .optional()
214
+ .default([])
215
+ .describe("Category tags"),
216
+ source: z
217
+ .enum(["user", "agent"])
218
+ .optional()
219
+ .default("agent")
220
+ .describe("Who created this lock"),
221
+ },
222
+ async ({ text, tags, source }) => {
223
+ const { lockId, rewritten, rewriteReason } = addLock(PROJECT_ROOT, text, tags, source);
224
+
225
+ // Read the stored lock to get the normalized text
226
+ const brain = readBrain(PROJECT_ROOT);
227
+ const storedLock = brain?.specLock?.items?.find(l => l.id === lockId);
228
+ const storedText = storedLock?.text || text;
229
+
230
+ // Auto-guard related files
231
+ const guardResult = autoGuardRelatedFiles(PROJECT_ROOT, storedText);
232
+ const guardMsg = guardResult.guarded.length > 0
233
+ ? `\nAuto-guarded ${guardResult.guarded.length} file(s): ${guardResult.guarded.join(", ")}`
234
+ : "";
235
+
236
+ // Sync active locks to package.json
237
+ syncLocksToPackageJson(PROJECT_ROOT);
238
+
239
+ // Report rewrite if it happened
240
+ const rewriteMsg = rewritten
241
+ ? `\n\nSmart Lock Authoring: Rewritten for accuracy.\n Original: "${text}"\n Stored as: "${storedText}"\n Reason: ${rewriteReason}`
242
+ : "";
243
+
244
+ return {
245
+ content: [
246
+ { type: "text", text: `Lock added (${lockId}): "${storedText}"${guardMsg}${rewriteMsg}` },
247
+ ],
248
+ };
249
+ }
250
+ );
251
+
252
+ // Tool 5: speclock_remove_lock
253
+ server.tool(
254
+ "speclock_remove_lock",
255
+ "Remove (deactivate) a SpecLock by its ID. The lock is soft-deleted and kept in history.",
256
+ {
257
+ lockId: z.string().min(1).describe("The lock ID to remove"),
258
+ },
259
+ async ({ lockId }) => {
260
+ const result = removeLock(PROJECT_ROOT, lockId);
261
+ if (!result.removed) {
262
+ return {
263
+ content: [{ type: "text", text: result.error }],
264
+ isError: true,
265
+ };
266
+ }
267
+ // Sync updated locks to package.json
268
+ syncLocksToPackageJson(PROJECT_ROOT);
269
+ return {
270
+ content: [
271
+ {
272
+ type: "text",
273
+ text: `Lock removed: "${result.lockText}"`,
274
+ },
275
+ ],
276
+ };
277
+ }
278
+ );
279
+
280
+ // Tool 6: speclock_add_decision
281
+ server.tool(
282
+ "speclock_add_decision",
283
+ "Record an architectural or design decision. Decisions guide future work and prevent contradictory changes.",
284
+ {
285
+ text: z.string().min(1).describe("The decision text"),
286
+ tags: z.array(z.string()).optional().default([]),
287
+ source: z
288
+ .enum(["user", "agent"])
289
+ .optional()
290
+ .default("agent"),
291
+ },
292
+ async ({ text, tags, source }) => {
293
+ const { decId } = addDecision(PROJECT_ROOT, text, tags, source);
294
+ return {
295
+ content: [
296
+ { type: "text", text: `Decision recorded (${decId}): "${text}"` },
297
+ ],
298
+ };
299
+ }
300
+ );
301
+
302
+ // Tool 7: speclock_add_note
303
+ server.tool(
304
+ "speclock_add_note",
305
+ "Add a pinned note for reference. Notes persist across sessions as reminders.",
306
+ {
307
+ text: z.string().min(1).describe("The note text"),
308
+ pinned: z
309
+ .boolean()
310
+ .optional()
311
+ .default(true)
312
+ .describe("Whether to pin this note in context"),
313
+ },
314
+ async ({ text, pinned }) => {
315
+ const { noteId } = addNote(PROJECT_ROOT, text, pinned);
316
+ return {
317
+ content: [
318
+ { type: "text", text: `Note added (${noteId}): "${text}"` },
319
+ ],
320
+ };
321
+ }
322
+ );
323
+
324
+ // Tool 8: speclock_set_deploy_facts
325
+ server.tool(
326
+ "speclock_set_deploy_facts",
327
+ "Record deployment configuration facts (provider, branch, auto-deploy settings).",
328
+ {
329
+ provider: z
330
+ .string()
331
+ .optional()
332
+ .describe("Deploy provider (vercel, railway, aws, netlify, etc.)"),
333
+ branch: z.string().optional().describe("Deploy branch"),
334
+ autoDeploy: z
335
+ .boolean()
336
+ .optional()
337
+ .describe("Whether auto-deploy is enabled"),
338
+ url: z.string().optional().describe("Deployment URL"),
339
+ notes: z.string().optional().describe("Additional deploy notes"),
340
+ },
341
+ async (params) => {
342
+ updateDeployFacts(PROJECT_ROOT, params);
343
+ return {
344
+ content: [{ type: "text", text: "Deploy facts updated." }],
345
+ };
346
+ }
347
+ );
348
+
349
+ // ========================================
350
+ // CHANGE TRACKING TOOLS
351
+ // ========================================
352
+
353
+ // Tool 9: speclock_log_change
354
+ server.tool(
355
+ "speclock_log_change",
356
+ "Manually log a significant change. Use this when you make an important modification that should be tracked in the context.",
357
+ {
358
+ summary: z
359
+ .string()
360
+ .min(1)
361
+ .describe("Brief description of the change"),
362
+ files: z
363
+ .array(z.string())
364
+ .optional()
365
+ .default([])
366
+ .describe("Files affected"),
367
+ },
368
+ async ({ summary, files }) => {
369
+ const { eventId } = logChange(PROJECT_ROOT, summary, files);
370
+ return {
371
+ content: [
372
+ { type: "text", text: `Change logged (${eventId}): "${summary}"` },
373
+ ],
374
+ };
375
+ }
376
+ );
377
+
378
+ // Tool 10: speclock_get_changes
379
+ server.tool(
380
+ "speclock_get_changes",
381
+ "Get recent file changes tracked by SpecLock.",
382
+ {
383
+ limit: z
384
+ .number()
385
+ .int()
386
+ .min(1)
387
+ .max(100)
388
+ .optional()
389
+ .default(20),
390
+ },
391
+ async ({ limit }) => {
392
+ const brain = readBrain(PROJECT_ROOT);
393
+ if (!brain) {
394
+ return {
395
+ content: [
396
+ {
397
+ type: "text",
398
+ text: "SpecLock not initialized. Run speclock_init first.",
399
+ },
400
+ ],
401
+ isError: true,
402
+ };
403
+ }
404
+ const changes = brain.state.recentChanges.slice(0, limit);
405
+ if (changes.length === 0) {
406
+ return {
407
+ content: [{ type: "text", text: "No changes tracked yet." }],
408
+ };
409
+ }
410
+ const formatted = changes
411
+ .map((ch) => {
412
+ const files =
413
+ ch.files && ch.files.length > 0
414
+ ? ` (${ch.files.join(", ")})`
415
+ : "";
416
+ return `- [${ch.at.substring(0, 19)}] ${ch.summary}${files}`;
417
+ })
418
+ .join("\n");
419
+ return {
420
+ content: [
421
+ { type: "text", text: `Recent changes (${changes.length}):\n${formatted}` },
422
+ ],
423
+ };
424
+ }
425
+ );
426
+
427
+ // Tool 11: speclock_get_events
428
+ server.tool(
429
+ "speclock_get_events",
430
+ "Get the event log, optionally filtered by type. Event types: init, goal_updated, lock_added, lock_removed, decision_added, note_added, fact_updated, file_created, file_changed, file_deleted, revert_detected, context_generated, session_started, session_ended, manual_change, checkpoint_created.",
431
+ {
432
+ type: z.string().optional().describe("Filter by event type"),
433
+ limit: z
434
+ .number()
435
+ .int()
436
+ .min(1)
437
+ .max(200)
438
+ .optional()
439
+ .default(50),
440
+ since: z
441
+ .string()
442
+ .optional()
443
+ .describe("ISO timestamp; only return events after this time"),
444
+ },
445
+ async ({ type, limit, since }) => {
446
+ const events = readEvents(PROJECT_ROOT, { type, limit, since });
447
+ if (events.length === 0) {
448
+ return {
449
+ content: [{ type: "text", text: "No events found." }],
450
+ };
451
+ }
452
+ const formatted = events
453
+ .map(
454
+ (e) =>
455
+ `- [${e.at.substring(0, 19)}] ${e.type}: ${e.summary || ""}`
456
+ )
457
+ .join("\n");
458
+ return {
459
+ content: [
460
+ {
461
+ type: "text",
462
+ text: `Events (${events.length}):\n${formatted}`,
463
+ },
464
+ ],
465
+ };
466
+ }
467
+ );
468
+
469
+ // ========================================
470
+ // CONTINUITY PROTECTION TOOLS
471
+ // ========================================
472
+
473
+ // Tool 12: speclock_check_conflict (v4.1: hybrid heuristic + Gemini LLM)
474
+ server.tool(
475
+ "speclock_check_conflict",
476
+ "Check if a proposed action conflicts with any active SpecLock. Uses fast heuristic + Gemini LLM for universal domain coverage. In hard enforcement mode, conflicts above the threshold will BLOCK the action (isError: true).",
477
+ {
478
+ proposedAction: z
479
+ .string()
480
+ .min(1)
481
+ .describe("Description of the action you plan to take"),
482
+ },
483
+ async ({ proposedAction }) => {
484
+ // Hybrid check: heuristic first, LLM for grey-zone (1-70%)
485
+ let result = await checkConflictAsync(PROJECT_ROOT, proposedAction);
486
+
487
+ // If async hybrid returned no conflict, also check enforcer for hard mode
488
+ if (!result.hasConflict) {
489
+ const enforced = enforceConflictCheck(PROJECT_ROOT, proposedAction);
490
+ if (enforced.blocked) {
491
+ return {
492
+ content: [{ type: "text", text: enforced.analysis }],
493
+ isError: true,
494
+ };
495
+ }
496
+ }
497
+
498
+ // In hard mode with blocking conflict, return isError: true
499
+ if (result.blocked) {
500
+ return {
501
+ content: [{ type: "text", text: result.analysis }],
502
+ isError: true,
503
+ };
504
+ }
505
+
506
+ return {
507
+ content: [{ type: "text", text: result.analysis }],
508
+ };
509
+ }
510
+ );
511
+
512
+ // Tool 13: speclock_session_briefing
513
+ server.tool(
514
+ "speclock_session_briefing",
515
+ "Start a new session and get a full briefing. Returns context pack plus what happened since the last session. Call this at the very beginning of a new conversation.",
516
+ {
517
+ toolName: z
518
+ .enum(["claude-code", "cursor", "codex", "windsurf", "cline", "unknown"])
519
+ .optional()
520
+ .default("unknown")
521
+ .describe("Which AI tool is being used"),
522
+ },
523
+ async ({ toolName }) => {
524
+ try {
525
+ const briefing = getSessionBriefing(PROJECT_ROOT, toolName);
526
+ const contextMd = generateContext(PROJECT_ROOT);
527
+
528
+ const parts = [];
529
+
530
+ // Session info
531
+ parts.push(`# SpecLock Session Briefing`);
532
+ parts.push(`Session started (${toolName}). ID: ${briefing.session?.id || "new"}`);
533
+ parts.push("");
534
+
535
+ // Last session summary
536
+ if (briefing.lastSession) {
537
+ parts.push("## Last Session");
538
+ parts.push(`- Tool: **${briefing.lastSession.toolUsed || "unknown"}**`);
539
+ parts.push(`- Ended: ${briefing.lastSession.endedAt || "unknown"}`);
540
+ if (briefing.lastSession.summary)
541
+ parts.push(`- Summary: ${briefing.lastSession.summary}`);
542
+ parts.push(
543
+ `- Events: ${briefing.lastSession.eventsInSession || 0}`
544
+ );
545
+ parts.push(
546
+ `- Changes since then: ${briefing.changesSinceLastSession || 0}`
547
+ );
548
+ parts.push("");
549
+ }
550
+
551
+ // Warnings
552
+ if (briefing.warnings?.length > 0) {
553
+ parts.push("## Warnings");
554
+ for (const w of briefing.warnings) {
555
+ parts.push(`- ${w}`);
556
+ }
557
+ parts.push("");
558
+ }
559
+
560
+ // Full context
561
+ parts.push("---");
562
+ parts.push(contextMd);
563
+
564
+ return {
565
+ content: [{ type: "text", text: parts.join("\n") }],
566
+ };
567
+ } catch (err) {
568
+ return {
569
+ content: [{ type: "text", text: `# SpecLock Session Briefing\n\nError loading session: ${err.message}\n\nTry running speclock_init first.\n\n---\n*SpecLock v${VERSION}*` }],
570
+ };
571
+ }
572
+ }
573
+ );
574
+
575
+ // Tool 14: speclock_session_summary
576
+ server.tool(
577
+ "speclock_session_summary",
578
+ "End the current session and record what was accomplished. Call this before ending a conversation.",
579
+ {
580
+ summary: z
581
+ .string()
582
+ .min(1)
583
+ .describe("Summary of what was accomplished in this session"),
584
+ },
585
+ async ({ summary }) => {
586
+ const result = endSession(PROJECT_ROOT, summary);
587
+ if (!result.ended) {
588
+ return {
589
+ content: [{ type: "text", text: result.error }],
590
+ isError: true,
591
+ };
592
+ }
593
+ const session = result.session;
594
+ const duration =
595
+ session.startedAt && session.endedAt
596
+ ? Math.round(
597
+ (new Date(session.endedAt) - new Date(session.startedAt)) /
598
+ 60000
599
+ )
600
+ : 0;
601
+ return {
602
+ content: [
603
+ {
604
+ type: "text",
605
+ text: `Session ended. Duration: ${duration} min. Events: ${session.eventsInSession}. Summary: "${summary}"`,
606
+ },
607
+ ],
608
+ };
609
+ }
610
+ );
611
+
612
+ // ========================================
613
+ // GIT INTEGRATION TOOLS
614
+ // ========================================
615
+
616
+ // Tool 15: speclock_checkpoint
617
+ server.tool(
618
+ "speclock_checkpoint",
619
+ "Create a named git tag checkpoint for easy rollback.",
620
+ {
621
+ name: z
622
+ .string()
623
+ .min(1)
624
+ .describe("Checkpoint name (alphanumeric, hyphens, underscores)"),
625
+ },
626
+ async ({ name }) => {
627
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "_");
628
+ const tag = `sl_${safeName}_${Date.now()}`;
629
+ const result = createTag(PROJECT_ROOT, tag);
630
+
631
+ if (!result.ok) {
632
+ return {
633
+ content: [
634
+ {
635
+ type: "text",
636
+ text: `Failed to create checkpoint: ${result.error}`,
637
+ },
638
+ ],
639
+ isError: true,
640
+ };
641
+ }
642
+
643
+ // Record event
644
+ const brain = readBrain(PROJECT_ROOT);
645
+ if (brain) {
646
+ const eventId = newId("evt");
647
+ const event = {
648
+ eventId,
649
+ type: "checkpoint_created",
650
+ at: nowIso(),
651
+ files: [],
652
+ summary: `Checkpoint: ${tag}`,
653
+ patchPath: "",
654
+ };
655
+ bumpEvents(brain, eventId);
656
+ appendEvent(PROJECT_ROOT, event);
657
+ writeBrain(PROJECT_ROOT, brain);
658
+ }
659
+
660
+ return {
661
+ content: [
662
+ { type: "text", text: `Checkpoint created: ${tag}` },
663
+ ],
664
+ };
665
+ }
666
+ );
667
+
668
+ // Tool 16: speclock_repo_status
669
+ server.tool(
670
+ "speclock_repo_status",
671
+ "Get current git repository status including branch, commit, changed files, and diff summary.",
672
+ {},
673
+ async () => {
674
+ const status = captureStatus(PROJECT_ROOT);
675
+ const diffSummary = getDiffSummary(PROJECT_ROOT);
676
+
677
+ const lines = [
678
+ `Branch: ${status.branch}`,
679
+ `Commit: ${status.commit}`,
680
+ "",
681
+ `Changed files (${status.changedFiles.length}):`,
682
+ ];
683
+
684
+ if (status.changedFiles.length > 0) {
685
+ for (const f of status.changedFiles) {
686
+ lines.push(` ${f.status} ${f.file}`);
687
+ }
688
+ } else {
689
+ lines.push(" (clean working tree)");
690
+ }
691
+
692
+ if (diffSummary) {
693
+ lines.push("");
694
+ lines.push("Diff summary:");
695
+ lines.push(diffSummary);
696
+ }
697
+
698
+ return {
699
+ content: [{ type: "text", text: lines.join("\n") }],
700
+ };
701
+ }
702
+ );
703
+
704
+ // ========================================
705
+ // INTELLIGENCE TOOLS
706
+ // ========================================
707
+
708
+ // Tool 17: speclock_suggest_locks
709
+ server.tool(
710
+ "speclock_suggest_locks",
711
+ "Analyze project decisions, notes, and patterns to suggest new SpecLock constraints. Returns auto-generated lock suggestions based on commitment language, prohibitive patterns, and common best practices.",
712
+ {},
713
+ async () => {
714
+ const result = suggestLocks(PROJECT_ROOT);
715
+
716
+ if (result.suggestions.length === 0) {
717
+ return {
718
+ content: [
719
+ {
720
+ type: "text",
721
+ text: `No suggestions at this time. You have ${result.totalLocks} active lock(s). Add more decisions and notes to get AI-powered lock suggestions.`,
722
+ },
723
+ ],
724
+ };
725
+ }
726
+
727
+ const formatted = result.suggestions
728
+ .map(
729
+ (s, i) =>
730
+ `${i + 1}. **"${s.text}"**\n Source: ${s.source}${s.sourceId ? ` (${s.sourceId})` : ""}\n Reason: ${s.reason}`
731
+ )
732
+ .join("\n\n");
733
+
734
+ return {
735
+ content: [
736
+ {
737
+ type: "text",
738
+ text: `## Lock Suggestions (${result.suggestions.length})\n\nCurrent active locks: ${result.totalLocks}\n\n${formatted}\n\nTo add any suggestion as a lock, call \`speclock_add_lock\` with the text.`,
739
+ },
740
+ ],
741
+ };
742
+ }
743
+ );
744
+
745
+ // Tool 18: speclock_detect_drift
746
+ server.tool(
747
+ "speclock_detect_drift",
748
+ "Scan recent changes and events against active SpecLock constraints to detect potential violations or drift. Use this proactively to ensure project integrity.",
749
+ {},
750
+ async () => {
751
+ const result = detectDrift(PROJECT_ROOT);
752
+
753
+ if (result.status === "no_locks") {
754
+ return {
755
+ content: [{ type: "text", text: result.message }],
756
+ };
757
+ }
758
+
759
+ if (result.status === "clean") {
760
+ return {
761
+ content: [{ type: "text", text: result.message }],
762
+ };
763
+ }
764
+
765
+ const formatted = result.drifts
766
+ .map(
767
+ (d) =>
768
+ `- [${d.severity.toUpperCase()}] Change: "${d.changeSummary}" (${d.changeAt.substring(0, 19)})\n Lock: "${d.lockText}"\n Matched: ${d.matchedTerms.join(", ")}`
769
+ )
770
+ .join("\n\n");
771
+
772
+ return {
773
+ content: [
774
+ {
775
+ type: "text",
776
+ text: `## Drift Report\n\n${result.message}\n\n${formatted}\n\nReview each drift and take corrective action if needed.`,
777
+ },
778
+ ],
779
+ };
780
+ }
781
+ );
782
+
783
+ // Tool 19: speclock_health
784
+ server.tool(
785
+ "speclock_health",
786
+ "Get a health check of the SpecLock setup including completeness score, missing recommended items, and multi-agent session timeline.",
787
+ {},
788
+ async () => {
789
+ try {
790
+ const brain = ensureInit(PROJECT_ROOT);
791
+ const activeLocks = (brain.specLock?.items || []).filter((l) => l.active !== false);
792
+
793
+ // Calculate health score
794
+ let score = 0;
795
+ const checks = [];
796
+
797
+ if (brain.goal?.text) { score += 20; checks.push("[PASS] Goal is set"); }
798
+ else checks.push("[MISS] No project goal set");
799
+
800
+ if (activeLocks.length > 0) { score += 25; checks.push(`[PASS] ${activeLocks.length} active lock(s)`); }
801
+ else checks.push("[MISS] No SpecLock constraints defined");
802
+
803
+ if ((brain.decisions || []).length > 0) { score += 15; checks.push(`[PASS] ${brain.decisions.length} decision(s) recorded`); }
804
+ else checks.push("[MISS] No decisions recorded");
805
+
806
+ if ((brain.notes || []).length > 0) { score += 10; checks.push(`[PASS] ${brain.notes.length} note(s)`); }
807
+ else checks.push("[MISS] No notes added");
808
+
809
+ const sessionHistory = brain.sessions?.history || [];
810
+ if (sessionHistory.length > 0) { score += 15; checks.push(`[PASS] ${sessionHistory.length} session(s) in history`); }
811
+ else checks.push("[MISS] No session history yet");
812
+
813
+ const recentChanges = brain.state?.recentChanges || [];
814
+ if (recentChanges.length > 0) { score += 10; checks.push(`[PASS] ${recentChanges.length} change(s) tracked`); }
815
+ else checks.push("[MISS] No changes tracked");
816
+
817
+ if (brain.facts?.deploy?.provider && brain.facts.deploy.provider !== "unknown") { score += 5; checks.push("[PASS] Deploy facts configured"); }
818
+ else checks.push("[MISS] Deploy facts not configured");
819
+
820
+ // Multi-agent timeline
821
+ const agentMap = {};
822
+ for (const session of sessionHistory) {
823
+ const tool = session.toolUsed || "unknown";
824
+ if (!agentMap[tool]) agentMap[tool] = { count: 0, lastUsed: "", summaries: [] };
825
+ agentMap[tool].count++;
826
+ if (!agentMap[tool].lastUsed || (session.endedAt && session.endedAt > agentMap[tool].lastUsed)) {
827
+ agentMap[tool].lastUsed = session.endedAt || session.startedAt || "";
828
+ }
829
+ if (session.summary && agentMap[tool].summaries.length < 3) {
830
+ agentMap[tool].summaries.push(session.summary.substring(0, 80));
831
+ }
832
+ }
833
+
834
+ let agentTimeline = "";
835
+ if (Object.keys(agentMap).length > 0) {
836
+ agentTimeline = "\n\n## Multi-Agent Timeline\n" +
837
+ Object.entries(agentMap)
838
+ .map(([tool, info]) =>
839
+ `- **${tool}**: ${info.count} session(s), last active ${info.lastUsed ? info.lastUsed.substring(0, 16) : "unknown"}\n Recent: ${info.summaries.length > 0 ? info.summaries.map(s => `"${s}"`).join(", ") : "(no summaries)"}`
840
+ )
841
+ .join("\n");
842
+ }
843
+
844
+ const grade = score >= 80 ? "A" : score >= 60 ? "B" : score >= 40 ? "C" : score >= 20 ? "D" : "F";
845
+ const evtCount = brain.events?.count || 0;
846
+ const revertCount = (brain.state?.reverts || []).length;
847
+
848
+ return {
849
+ content: [
850
+ {
851
+ type: "text",
852
+ text: `## SpecLock Health Check\n\nScore: **${score}/100** (Grade: ${grade})\nEvents: ${evtCount} | Reverts: ${revertCount}\n\n### Checks\n${checks.join("\n")}${agentTimeline}\n\n---\n*SpecLock v${VERSION} — Developed by ${AUTHOR}*`,
853
+ },
854
+ ],
855
+ };
856
+ } catch (err) {
857
+ return {
858
+ content: [{ type: "text", text: `## SpecLock Health Check\n\nError: ${err.message}\n\nTry running speclock_init first to initialize the project.\n\n---\n*SpecLock v${VERSION}*` }],
859
+ };
860
+ }
861
+ }
862
+ );
863
+
864
+ // ========================================
865
+ // TEMPLATE, REPORT & AUDIT TOOLS (v1.7.0)
866
+ // ========================================
867
+
868
+ // Tool 20: speclock_apply_template
869
+ server.tool(
870
+ "speclock_apply_template",
871
+ "Apply a pre-built constraint template (e.g., nextjs, react, express, supabase, stripe, security-hardened). Templates add recommended locks and decisions for common frameworks.",
872
+ {
873
+ name: z
874
+ .string()
875
+ .optional()
876
+ .describe("Template name to apply. Omit to list available templates."),
877
+ },
878
+ async ({ name }) => {
879
+ if (!name) {
880
+ const templates = listTemplates();
881
+ const formatted = templates
882
+ .map((t) => `- **${t.name}** (${t.displayName}): ${t.description} — ${t.lockCount} locks, ${t.decisionCount} decisions`)
883
+ .join("\n");
884
+ return {
885
+ content: [
886
+ {
887
+ type: "text",
888
+ text: `## Available Templates\n\n${formatted}\n\nCall again with a name to apply.`,
889
+ },
890
+ ],
891
+ };
892
+ }
893
+ const result = applyTemplate(PROJECT_ROOT, name);
894
+ if (!result.applied) {
895
+ return {
896
+ content: [{ type: "text", text: result.error }],
897
+ isError: true,
898
+ };
899
+ }
900
+ return {
901
+ content: [
902
+ {
903
+ type: "text",
904
+ text: `Template "${result.displayName}" applied: ${result.locksAdded} lock(s) + ${result.decisionsAdded} decision(s) added.`,
905
+ },
906
+ ],
907
+ };
908
+ }
909
+ );
910
+
911
+ // Tool 21: speclock_report
912
+ server.tool(
913
+ "speclock_report",
914
+ "Get a violation report showing how many times SpecLock blocked constraint violations, which locks were tested most, and recent violations.",
915
+ {},
916
+ async () => {
917
+ const report = generateReport(PROJECT_ROOT);
918
+
919
+ const parts = [`## SpecLock Violation Report`, ``, `Total violations blocked: **${report.totalViolations}**`];
920
+
921
+ if (report.timeRange) {
922
+ parts.push(`Period: ${report.timeRange.from.substring(0, 10)} to ${report.timeRange.to.substring(0, 10)}`);
923
+ }
924
+
925
+ if (report.mostTestedLocks.length > 0) {
926
+ parts.push("", "### Most Tested Locks");
927
+ for (const lock of report.mostTestedLocks) {
928
+ parts.push(`- ${lock.count}x — "${lock.text}"`);
929
+ }
930
+ }
931
+
932
+ if (report.recentViolations.length > 0) {
933
+ parts.push("", "### Recent Violations");
934
+ for (const v of report.recentViolations) {
935
+ parts.push(`- [${v.at.substring(0, 19)}] ${v.topLevel} (${v.topConfidence}%) — "${v.action}"`);
936
+ }
937
+ }
938
+
939
+ parts.push("", `---`, report.summary);
940
+
941
+ return {
942
+ content: [{ type: "text", text: parts.join("\n") }],
943
+ };
944
+ }
945
+ );
946
+
947
+ // Tool 22: speclock_audit
948
+ server.tool(
949
+ "speclock_audit",
950
+ "Audit git staged files against active SpecLock constraints. Returns pass/fail with details on which files violate locks. Used by the pre-commit hook.",
951
+ {},
952
+ async () => {
953
+ const result = auditStagedFiles(PROJECT_ROOT);
954
+
955
+ if (result.passed) {
956
+ return {
957
+ content: [{ type: "text", text: result.message }],
958
+ };
959
+ }
960
+
961
+ const formatted = result.violations
962
+ .map((v) => `- [${v.severity}] **${v.file}** — ${v.reason}\n Lock: "${v.lockText}"`)
963
+ .join("\n");
964
+
965
+ return {
966
+ content: [
967
+ {
968
+ type: "text",
969
+ text: `## Audit Failed\n\n${formatted}\n\n${result.message}`,
970
+ },
971
+ ],
972
+ };
973
+ }
974
+ );
975
+
976
+ // ========================================
977
+ // ENTERPRISE TOOLS (v2.1)
978
+ // ========================================
979
+
980
+ // Tool 23: speclock_verify_audit
981
+ server.tool(
982
+ "speclock_verify_audit",
983
+ "Verify the integrity of the HMAC audit chain. Detects tampering or corruption in the event log. Returns chain status, total events, and any broken links.",
984
+ {},
985
+ async () => {
986
+ ensureInit(PROJECT_ROOT);
987
+ const result = verifyAuditChain(PROJECT_ROOT);
988
+
989
+ const status = result.valid ? "VALID" : "BROKEN";
990
+ const parts = [
991
+ `## Audit Chain Verification`,
992
+ ``,
993
+ `Status: **${status}**`,
994
+ `Total events: ${result.totalEvents}`,
995
+ `Hashed events: ${result.hashedEvents}`,
996
+ `Legacy events (pre-v2.1): ${result.unhashedEvents}`,
997
+ ];
998
+
999
+ if (!result.valid && result.errors) {
1000
+ parts.push(``, `### Errors`);
1001
+ for (const err of result.errors) {
1002
+ parts.push(`- Line ${err.line}: ${err.error}${err.eventId ? ` (${err.eventId})` : ""}`);
1003
+ }
1004
+ }
1005
+
1006
+ parts.push(``, result.message);
1007
+ parts.push(``, `Verified at: ${result.verifiedAt}`);
1008
+
1009
+ return {
1010
+ content: [{ type: "text", text: parts.join("\n") }],
1011
+ };
1012
+ }
1013
+ );
1014
+
1015
+ // Tool 24: speclock_export_compliance
1016
+ server.tool(
1017
+ "speclock_export_compliance",
1018
+ "Generate compliance reports for enterprise auditing. Supports SOC 2 Type II, HIPAA, and CSV formats. Reports include constraint management, access logs, audit chain integrity, and violation history.",
1019
+ {
1020
+ format: z
1021
+ .enum(["soc2", "hipaa", "csv"])
1022
+ .describe("Export format: soc2 (JSON), hipaa (JSON), csv (spreadsheet)"),
1023
+ },
1024
+ async ({ format }) => {
1025
+ ensureInit(PROJECT_ROOT);
1026
+ const result = exportCompliance(PROJECT_ROOT, format);
1027
+
1028
+ if (result.error) {
1029
+ return {
1030
+ content: [{ type: "text", text: result.error }],
1031
+ isError: true,
1032
+ };
1033
+ }
1034
+
1035
+ if (format === "csv") {
1036
+ return {
1037
+ content: [{ type: "text", text: `## Compliance Export (CSV)\n\n\`\`\`csv\n${result.data}\n\`\`\`` }],
1038
+ };
1039
+ }
1040
+
1041
+ return {
1042
+ content: [{ type: "text", text: `## Compliance Export (${format.toUpperCase()})\n\n\`\`\`json\n${JSON.stringify(result.data, null, 2)}\n\`\`\`` }],
1043
+ };
1044
+ }
1045
+ );
1046
+
1047
+ // ========================================
1048
+ // HARD ENFORCEMENT TOOLS (v2.5)
1049
+ // ========================================
1050
+
1051
+ // Tool 25: speclock_set_enforcement
1052
+ server.tool(
1053
+ "speclock_set_enforcement",
1054
+ "Set the enforcement mode for this project. 'advisory' (default) warns about conflicts. 'hard' blocks actions that violate locks above the confidence threshold — the AI cannot proceed.",
1055
+ {
1056
+ mode: z
1057
+ .enum(["advisory", "hard"])
1058
+ .describe("Enforcement mode: advisory (warn) or hard (block)"),
1059
+ blockThreshold: z
1060
+ .number()
1061
+ .int()
1062
+ .min(0)
1063
+ .max(100)
1064
+ .optional()
1065
+ .default(70)
1066
+ .describe("Minimum confidence % to block in hard mode (default: 70)"),
1067
+ allowOverride: z
1068
+ .boolean()
1069
+ .optional()
1070
+ .default(true)
1071
+ .describe("Whether lock overrides are permitted"),
1072
+ },
1073
+ async ({ mode, blockThreshold, allowOverride }) => {
1074
+ const result = setEnforcementMode(PROJECT_ROOT, mode, { blockThreshold, allowOverride });
1075
+ if (!result.success) {
1076
+ return {
1077
+ content: [{ type: "text", text: result.error }],
1078
+ isError: true,
1079
+ };
1080
+ }
1081
+ return {
1082
+ content: [
1083
+ {
1084
+ type: "text",
1085
+ text: `Enforcement mode set to **${mode}**. Threshold: ${result.config.blockThreshold}%. Overrides: ${result.config.allowOverride ? "allowed" : "disabled"}.`,
1086
+ },
1087
+ ],
1088
+ };
1089
+ }
1090
+ );
1091
+
1092
+ // Tool 26: speclock_override_lock
1093
+ server.tool(
1094
+ "speclock_override_lock",
1095
+ "Override a lock for a specific action. Requires a reason which is logged to the audit trail. Use when a locked action must proceed with justification. Triggers escalation after repeated overrides.",
1096
+ {
1097
+ lockId: z.string().min(1).describe("The lock ID to override"),
1098
+ action: z.string().min(1).describe("The action that conflicts with the lock"),
1099
+ reason: z.string().min(1).describe("Justification for the override"),
1100
+ },
1101
+ async ({ lockId, action, reason }) => {
1102
+ const result = overrideLock(PROJECT_ROOT, lockId, action, reason);
1103
+ if (!result.success) {
1104
+ return {
1105
+ content: [{ type: "text", text: result.error }],
1106
+ isError: true,
1107
+ };
1108
+ }
1109
+
1110
+ const parts = [
1111
+ `Lock overridden: "${result.lockText}"`,
1112
+ `Override count: ${result.overrideCount}`,
1113
+ `Reason: ${reason}`,
1114
+ ];
1115
+
1116
+ if (result.escalated) {
1117
+ parts.push("", result.escalationMessage);
1118
+ }
1119
+
1120
+ return {
1121
+ content: [{ type: "text", text: parts.join("\n") }],
1122
+ };
1123
+ }
1124
+ );
1125
+
1126
+ // Tool 27: speclock_semantic_audit
1127
+ server.tool(
1128
+ "speclock_semantic_audit",
1129
+ "Run semantic pre-commit audit. Parses the staged git diff, analyzes actual code changes against active locks using semantic analysis. Much more powerful than filename-only audit — catches violations in code content.",
1130
+ {},
1131
+ async () => {
1132
+ const result = semanticAudit(PROJECT_ROOT);
1133
+
1134
+ if (result.passed) {
1135
+ return {
1136
+ content: [{ type: "text", text: result.message }],
1137
+ };
1138
+ }
1139
+
1140
+ const formatted = result.violations
1141
+ .map((v) => {
1142
+ const lines = [`- [${v.level}] **${v.file}** (confidence: ${v.confidence}%)`];
1143
+ lines.push(` Lock: "${v.lockText}"`);
1144
+ lines.push(` Reason: ${v.reason}`);
1145
+ if (v.addedLines) lines.push(` Changes: +${v.addedLines} / -${v.removedLines} lines`);
1146
+ return lines.join("\n");
1147
+ })
1148
+ .join("\n\n");
1149
+
1150
+ return {
1151
+ content: [
1152
+ {
1153
+ type: "text",
1154
+ text: `## Semantic Audit Result\n\nMode: ${result.mode} | Threshold: ${result.threshold}%\n\n${formatted}\n\n${result.message}`,
1155
+ },
1156
+ ],
1157
+ isError: result.blocked || false,
1158
+ };
1159
+ }
1160
+ );
1161
+
1162
+ // Tool 28: speclock_override_history
1163
+ server.tool(
1164
+ "speclock_override_history",
1165
+ "Get the history of lock overrides. Shows which locks have been overridden, by whom, and the reasons given. Useful for audit review and identifying locks that may need updating.",
1166
+ {
1167
+ lockId: z
1168
+ .string()
1169
+ .optional()
1170
+ .describe("Filter by specific lock ID. Omit to see all overrides."),
1171
+ },
1172
+ async ({ lockId }) => {
1173
+ const result = getOverrideHistory(PROJECT_ROOT, lockId);
1174
+
1175
+ if (result.total === 0) {
1176
+ return {
1177
+ content: [{ type: "text", text: "No overrides recorded." }],
1178
+ };
1179
+ }
1180
+
1181
+ const formatted = result.overrides
1182
+ .map(
1183
+ (o) =>
1184
+ `- [${o.at.substring(0, 19)}] Lock: "${o.lockText}" (${o.lockId})\n Action: ${o.action}\n Reason: ${o.reason}`
1185
+ )
1186
+ .join("\n\n");
1187
+
1188
+ return {
1189
+ content: [
1190
+ {
1191
+ type: "text",
1192
+ text: `## Override History (${result.total})\n\n${formatted}`,
1193
+ },
1194
+ ],
1195
+ };
1196
+ }
1197
+ );
1198
+
1199
+ // ========================================
1200
+ // POLICY-AS-CODE TOOLS (v3.5)
1201
+ // ========================================
1202
+
1203
+ // Tool 29: speclock_policy_evaluate
1204
+ server.tool(
1205
+ "speclock_policy_evaluate",
1206
+ "Evaluate policy-as-code rules against a proposed action. Returns violations for any matching rules. Use alongside speclock_check_conflict for comprehensive protection.",
1207
+ {
1208
+ description: z.string().min(1).describe("Description of the action to evaluate"),
1209
+ files: z.array(z.string()).optional().default([]).describe("Files affected by the action"),
1210
+ type: z.enum(["modify", "delete", "create", "export"]).optional().default("modify").describe("Action type"),
1211
+ },
1212
+ async ({ description, files, type }) => {
1213
+ const result = evaluatePolicy(PROJECT_ROOT, { description, text: description, files, type });
1214
+
1215
+ if (result.passed) {
1216
+ return {
1217
+ content: [{ type: "text", text: `Policy check passed. ${result.rulesChecked} rule(s) evaluated, no violations.` }],
1218
+ };
1219
+ }
1220
+
1221
+ const formatted = result.violations
1222
+ .map(v => `- [${v.severity.toUpperCase()}] **${v.ruleName}** (${v.enforce})\n ${v.description}\n Files: ${v.matchedFiles.join(", ") || "(pattern match)"}`)
1223
+ .join("\n\n");
1224
+
1225
+ return {
1226
+ content: [{ type: "text", text: `## Policy Violations (${result.violations.length})\n\n${formatted}` }],
1227
+ isError: result.blocked,
1228
+ };
1229
+ }
1230
+ );
1231
+
1232
+ // Tool 30: speclock_policy_manage
1233
+ server.tool(
1234
+ "speclock_policy_manage",
1235
+ "Manage policy-as-code rules. Actions: list (show all rules), add (create new rule), remove (delete rule), init (create default policy), export (portable YAML).",
1236
+ {
1237
+ action: z.enum(["list", "add", "remove", "init", "export"]).describe("Policy action"),
1238
+ rule: z.object({
1239
+ name: z.string().optional(),
1240
+ description: z.string().optional(),
1241
+ match: z.object({
1242
+ files: z.array(z.string()).optional(),
1243
+ actions: z.array(z.string()).optional(),
1244
+ }).optional(),
1245
+ enforce: z.enum(["block", "warn", "log"]).optional(),
1246
+ severity: z.enum(["critical", "high", "medium", "low"]).optional(),
1247
+ notify: z.array(z.string()).optional(),
1248
+ }).optional().describe("Rule definition (for add action)"),
1249
+ ruleId: z.string().optional().describe("Rule ID (for remove action)"),
1250
+ },
1251
+ async ({ action, rule, ruleId }) => {
1252
+ switch (action) {
1253
+ case "list": {
1254
+ const result = listPolicyRules(PROJECT_ROOT);
1255
+ if (result.total === 0) {
1256
+ return { content: [{ type: "text", text: "No policy rules defined. Use action 'init' to create a default policy." }] };
1257
+ }
1258
+ const formatted = result.rules.map(r =>
1259
+ `- **${r.name}** (${r.id}) [${r.enforce}/${r.severity}]\n Files: ${(r.match?.files || []).join(", ")}\n Actions: ${(r.match?.actions || []).join(", ")}`
1260
+ ).join("\n\n");
1261
+ return { content: [{ type: "text", text: `## Policy Rules (${result.active}/${result.total} active)\n\n${formatted}` }] };
1262
+ }
1263
+ case "add": {
1264
+ if (!rule || !rule.name) {
1265
+ return { content: [{ type: "text", text: "Rule name is required." }], isError: true };
1266
+ }
1267
+ const result = addPolicyRule(PROJECT_ROOT, rule);
1268
+ if (!result.success) return { content: [{ type: "text", text: result.error }], isError: true };
1269
+ return { content: [{ type: "text", text: `Policy rule added: "${result.rule.name}" (${result.ruleId}) [${result.rule.enforce}]` }] };
1270
+ }
1271
+ case "remove": {
1272
+ if (!ruleId) return { content: [{ type: "text", text: "ruleId is required." }], isError: true };
1273
+ const result = removePolicyRule(PROJECT_ROOT, ruleId);
1274
+ if (!result.success) return { content: [{ type: "text", text: result.error }], isError: true };
1275
+ return { content: [{ type: "text", text: `Policy rule removed: "${result.removed.name}"` }] };
1276
+ }
1277
+ case "init": {
1278
+ const result = initPolicy(PROJECT_ROOT);
1279
+ if (!result.success) return { content: [{ type: "text", text: result.error }], isError: true };
1280
+ return { content: [{ type: "text", text: "Policy-as-code initialized. Edit .speclock/policy.yml to add rules." }] };
1281
+ }
1282
+ case "export": {
1283
+ const result = exportPolicy(PROJECT_ROOT);
1284
+ if (!result.success) return { content: [{ type: "text", text: result.error }], isError: true };
1285
+ return { content: [{ type: "text", text: `## Exported Policy\n\n\`\`\`yaml\n${result.yaml}\`\`\`` }] };
1286
+ }
1287
+ default:
1288
+ return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
1289
+ }
1290
+ }
1291
+ );
1292
+
1293
+ // ========================================
1294
+ // TELEMETRY TOOLS (v3.5)
1295
+ // ========================================
1296
+
1297
+ // Tool 31: speclock_telemetry
1298
+ server.tool(
1299
+ "speclock_telemetry",
1300
+ "Get telemetry and analytics summary. Shows tool usage counts, conflict rates, response times, and feature adoption. Opt-in only (SPECLOCK_TELEMETRY=true).",
1301
+ {},
1302
+ async () => {
1303
+ const summary = getTelemetrySummary(PROJECT_ROOT);
1304
+ if (!summary.enabled) {
1305
+ return { content: [{ type: "text", text: summary.message }] };
1306
+ }
1307
+
1308
+ const parts = [
1309
+ `## Telemetry Summary`,
1310
+ ``,
1311
+ `Total API calls: **${summary.totalCalls}**`,
1312
+ `Avg response: **${summary.avgResponseMs}ms**`,
1313
+ `Sessions: **${summary.sessions.total}**`,
1314
+ ``,
1315
+ `### Conflicts`,
1316
+ `Total: ${summary.conflicts.total} | Blocked: ${summary.conflicts.blocked} | Advisory: ${summary.conflicts.advisory}`,
1317
+ ];
1318
+
1319
+ if (summary.topTools.length > 0) {
1320
+ parts.push(``, `### Top Tools`);
1321
+ for (const t of summary.topTools.slice(0, 5)) {
1322
+ parts.push(`- ${t.name}: ${t.count} calls (avg ${t.avgMs}ms)`);
1323
+ }
1324
+ }
1325
+
1326
+ if (summary.features.length > 0) {
1327
+ parts.push(``, `### Feature Adoption`);
1328
+ for (const f of summary.features) {
1329
+ parts.push(`- ${f.name}: ${f.count} uses`);
1330
+ }
1331
+ }
1332
+
1333
+ return { content: [{ type: "text", text: parts.join("\n") }] };
1334
+ }
1335
+ );
1336
+
1337
+ // --- Smithery sandbox export ---
1338
+ export default function createSandboxServer() {
1339
+ return server;
1340
+ }
1341
+
1342
+ // --- Start server (skip when bundled as CJS for Smithery scanning) ---
1343
+ const isScanMode = typeof import.meta.url === "undefined";
1344
+
1345
+ if (!isScanMode) {
1346
+ const transport = new StdioServerTransport();
1347
+ server.connect(transport).then(() => {
1348
+ process.stderr.write(
1349
+ `SpecLock MCP v${VERSION} running (stdio) — Developed by ${AUTHOR}. Root: ${PROJECT_ROOT}${os.EOL}`
1350
+ );
1351
+ }).catch((err) => {
1352
+ process.stderr.write(`SpecLock fatal: ${err.message}${os.EOL}`);
1353
+ process.exit(1);
1354
+ });
1355
+ }