speclock 2.1.1 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,6 +30,11 @@ import {
30
30
  auditStagedFiles,
31
31
  verifyAuditChain,
32
32
  exportCompliance,
33
+ enforceConflictCheck,
34
+ setEnforcementMode,
35
+ overrideLock,
36
+ getOverrideHistory,
37
+ semanticAudit,
33
38
  } from "../core/engine.js";
34
39
  import { generateContext, generateContextPack } from "../core/context.js";
35
40
  import {
@@ -48,7 +53,7 @@ import {
48
53
  } from "../core/git.js";
49
54
 
50
55
  const PROJECT_ROOT = process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
51
- const VERSION = "2.1.1";
56
+ const VERSION = "2.5.0";
52
57
  const AUTHOR = "Sandeep Roy";
53
58
  const START_TIME = Date.now();
54
59
 
@@ -219,11 +224,14 @@ function createSpecLockServer() {
219
224
  return { content: [{ type: "text", text: events.length ? JSON.stringify(events, null, 2) : "No matching events." }] };
220
225
  });
221
226
 
222
- // Tool 12: speclock_check_conflict
223
- server.tool("speclock_check_conflict", "Check if a proposed action conflicts with any active SpecLock.", { proposedAction: z.string().min(1).describe("Description of the action") }, async ({ proposedAction }) => {
227
+ // Tool 12: speclock_check_conflict (v2.5: uses enforcer)
228
+ server.tool("speclock_check_conflict", "Check if a proposed action conflicts with any active SpecLock. In hard mode, blocks above threshold.", { proposedAction: z.string().min(1).describe("Description of the action") }, async ({ proposedAction }) => {
224
229
  ensureInit(PROJECT_ROOT);
225
- const result = await checkConflictAsync(PROJECT_ROOT, proposedAction);
226
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
230
+ const result = enforceConflictCheck(PROJECT_ROOT, proposedAction);
231
+ if (result.blocked) {
232
+ return { content: [{ type: "text", text: result.analysis }], isError: true };
233
+ }
234
+ return { content: [{ type: "text", text: result.analysis }] };
227
235
  });
228
236
 
229
237
  // Tool 13: speclock_session_briefing
@@ -354,6 +362,35 @@ function createSpecLockServer() {
354
362
  return { content: [{ type: "text", text: output }] };
355
363
  });
356
364
 
365
+ // Tool 25: speclock_set_enforcement (v2.5)
366
+ server.tool("speclock_set_enforcement", "Set enforcement mode: advisory (warn) or hard (block).", { mode: z.enum(["advisory", "hard"]).describe("Enforcement mode"), blockThreshold: z.number().int().min(0).max(100).optional().default(70).describe("Block threshold %") }, async ({ mode, blockThreshold }) => {
367
+ const result = setEnforcementMode(PROJECT_ROOT, mode, { blockThreshold });
368
+ if (!result.success) return { content: [{ type: "text", text: result.error }], isError: true };
369
+ return { content: [{ type: "text", text: `Enforcement: ${mode} (threshold: ${result.config.blockThreshold}%)` }] };
370
+ });
371
+
372
+ // Tool 26: speclock_override_lock (v2.5)
373
+ server.tool("speclock_override_lock", "Override a lock with justification. Logged to audit trail.", { lockId: z.string().min(1), action: z.string().min(1), reason: z.string().min(1) }, async ({ lockId, action, reason }) => {
374
+ const result = overrideLock(PROJECT_ROOT, lockId, action, reason);
375
+ if (!result.success) return { content: [{ type: "text", text: result.error }], isError: true };
376
+ const msg = result.escalated ? `\n${result.escalationMessage}` : "";
377
+ return { content: [{ type: "text", text: `Override: "${result.lockText}" (${result.overrideCount}x)${msg}` }] };
378
+ });
379
+
380
+ // Tool 27: speclock_semantic_audit (v2.5)
381
+ server.tool("speclock_semantic_audit", "Semantic pre-commit: analyzes code changes vs locks.", {}, async () => {
382
+ const result = semanticAudit(PROJECT_ROOT);
383
+ return { content: [{ type: "text", text: result.message }], isError: result.blocked || false };
384
+ });
385
+
386
+ // Tool 28: speclock_override_history (v2.5)
387
+ server.tool("speclock_override_history", "Show lock override history.", { lockId: z.string().optional() }, async ({ lockId }) => {
388
+ const result = getOverrideHistory(PROJECT_ROOT, lockId);
389
+ if (result.total === 0) return { content: [{ type: "text", text: "No overrides recorded." }] };
390
+ const lines = result.overrides.map(o => `[${o.at.substring(0,19)}] "${o.lockText}" — ${o.reason}`).join("\n");
391
+ return { content: [{ type: "text", text: `Overrides (${result.total}):\n${lines}` }] };
392
+ });
393
+
357
394
  return server;
358
395
  }
359
396
 
package/src/mcp/server.js CHANGED
@@ -27,6 +27,12 @@ import {
27
27
  exportCompliance,
28
28
  checkLimits,
29
29
  getLicenseInfo,
30
+ enforceConflictCheck,
31
+ setEnforcementMode,
32
+ overrideLock,
33
+ getOverrideHistory,
34
+ getEnforcementConfig,
35
+ semanticAudit,
30
36
  } from "../core/engine.js";
31
37
  import { generateContext, generateContextPack } from "../core/context.js";
32
38
  import {
@@ -61,7 +67,7 @@ const PROJECT_ROOT =
61
67
  args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
62
68
 
63
69
  // --- MCP Server ---
64
- const VERSION = "2.1.1";
70
+ const VERSION = "2.5.0";
65
71
  const AUTHOR = "Sandeep Roy";
66
72
 
67
73
  const server = new McpServer(
@@ -421,10 +427,10 @@ server.tool(
421
427
  // CONTINUITY PROTECTION TOOLS
422
428
  // ========================================
423
429
 
424
- // Tool 12: speclock_check_conflict
430
+ // Tool 12: speclock_check_conflict (v2.5: uses enforcer — hard mode returns isError)
425
431
  server.tool(
426
432
  "speclock_check_conflict",
427
- "Check if a proposed action conflicts with any active SpecLock. Use before making significant changes.",
433
+ "Check if a proposed action conflicts with any active SpecLock. Use before making significant changes. In hard enforcement mode, conflicts above the threshold will BLOCK the action (isError: true).",
428
434
  {
429
435
  proposedAction: z
430
436
  .string()
@@ -432,7 +438,28 @@ server.tool(
432
438
  .describe("Description of the action you plan to take"),
433
439
  },
434
440
  async ({ proposedAction }) => {
435
- const result = await checkConflictAsync(PROJECT_ROOT, proposedAction);
441
+ // Try LLM-enhanced check first, fall back to heuristic enforcer
442
+ let result;
443
+ try {
444
+ const { llmCheckConflict } = await import("../core/llm-checker.js");
445
+ const llmResult = await llmCheckConflict(PROJECT_ROOT, proposedAction);
446
+ if (llmResult) {
447
+ result = llmResult;
448
+ }
449
+ } catch (_) {}
450
+
451
+ if (!result) {
452
+ result = enforceConflictCheck(PROJECT_ROOT, proposedAction);
453
+ }
454
+
455
+ // In hard mode with blocking conflict, return isError: true
456
+ if (result.blocked) {
457
+ return {
458
+ content: [{ type: "text", text: result.analysis }],
459
+ isError: true,
460
+ };
461
+ }
462
+
436
463
  return {
437
464
  content: [{ type: "text", text: result.analysis }],
438
465
  };
@@ -958,6 +985,158 @@ server.tool(
958
985
  }
959
986
  );
960
987
 
988
+ // ========================================
989
+ // HARD ENFORCEMENT TOOLS (v2.5)
990
+ // ========================================
991
+
992
+ // Tool 25: speclock_set_enforcement
993
+ server.tool(
994
+ "speclock_set_enforcement",
995
+ "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.",
996
+ {
997
+ mode: z
998
+ .enum(["advisory", "hard"])
999
+ .describe("Enforcement mode: advisory (warn) or hard (block)"),
1000
+ blockThreshold: z
1001
+ .number()
1002
+ .int()
1003
+ .min(0)
1004
+ .max(100)
1005
+ .optional()
1006
+ .default(70)
1007
+ .describe("Minimum confidence % to block in hard mode (default: 70)"),
1008
+ allowOverride: z
1009
+ .boolean()
1010
+ .optional()
1011
+ .default(true)
1012
+ .describe("Whether lock overrides are permitted"),
1013
+ },
1014
+ async ({ mode, blockThreshold, allowOverride }) => {
1015
+ const result = setEnforcementMode(PROJECT_ROOT, mode, { blockThreshold, allowOverride });
1016
+ if (!result.success) {
1017
+ return {
1018
+ content: [{ type: "text", text: result.error }],
1019
+ isError: true,
1020
+ };
1021
+ }
1022
+ return {
1023
+ content: [
1024
+ {
1025
+ type: "text",
1026
+ text: `Enforcement mode set to **${mode}**. Threshold: ${result.config.blockThreshold}%. Overrides: ${result.config.allowOverride ? "allowed" : "disabled"}.`,
1027
+ },
1028
+ ],
1029
+ };
1030
+ }
1031
+ );
1032
+
1033
+ // Tool 26: speclock_override_lock
1034
+ server.tool(
1035
+ "speclock_override_lock",
1036
+ "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.",
1037
+ {
1038
+ lockId: z.string().min(1).describe("The lock ID to override"),
1039
+ action: z.string().min(1).describe("The action that conflicts with the lock"),
1040
+ reason: z.string().min(1).describe("Justification for the override"),
1041
+ },
1042
+ async ({ lockId, action, reason }) => {
1043
+ const result = overrideLock(PROJECT_ROOT, lockId, action, reason);
1044
+ if (!result.success) {
1045
+ return {
1046
+ content: [{ type: "text", text: result.error }],
1047
+ isError: true,
1048
+ };
1049
+ }
1050
+
1051
+ const parts = [
1052
+ `Lock overridden: "${result.lockText}"`,
1053
+ `Override count: ${result.overrideCount}`,
1054
+ `Reason: ${reason}`,
1055
+ ];
1056
+
1057
+ if (result.escalated) {
1058
+ parts.push("", result.escalationMessage);
1059
+ }
1060
+
1061
+ return {
1062
+ content: [{ type: "text", text: parts.join("\n") }],
1063
+ };
1064
+ }
1065
+ );
1066
+
1067
+ // Tool 27: speclock_semantic_audit
1068
+ server.tool(
1069
+ "speclock_semantic_audit",
1070
+ "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.",
1071
+ {},
1072
+ async () => {
1073
+ const result = semanticAudit(PROJECT_ROOT);
1074
+
1075
+ if (result.passed) {
1076
+ return {
1077
+ content: [{ type: "text", text: result.message }],
1078
+ };
1079
+ }
1080
+
1081
+ const formatted = result.violations
1082
+ .map((v) => {
1083
+ const lines = [`- [${v.level}] **${v.file}** (confidence: ${v.confidence}%)`];
1084
+ lines.push(` Lock: "${v.lockText}"`);
1085
+ lines.push(` Reason: ${v.reason}`);
1086
+ if (v.addedLines) lines.push(` Changes: +${v.addedLines} / -${v.removedLines} lines`);
1087
+ return lines.join("\n");
1088
+ })
1089
+ .join("\n\n");
1090
+
1091
+ return {
1092
+ content: [
1093
+ {
1094
+ type: "text",
1095
+ text: `## Semantic Audit Result\n\nMode: ${result.mode} | Threshold: ${result.threshold}%\n\n${formatted}\n\n${result.message}`,
1096
+ },
1097
+ ],
1098
+ isError: result.blocked || false,
1099
+ };
1100
+ }
1101
+ );
1102
+
1103
+ // Tool 28: speclock_override_history
1104
+ server.tool(
1105
+ "speclock_override_history",
1106
+ "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.",
1107
+ {
1108
+ lockId: z
1109
+ .string()
1110
+ .optional()
1111
+ .describe("Filter by specific lock ID. Omit to see all overrides."),
1112
+ },
1113
+ async ({ lockId }) => {
1114
+ const result = getOverrideHistory(PROJECT_ROOT, lockId);
1115
+
1116
+ if (result.total === 0) {
1117
+ return {
1118
+ content: [{ type: "text", text: "No overrides recorded." }],
1119
+ };
1120
+ }
1121
+
1122
+ const formatted = result.overrides
1123
+ .map(
1124
+ (o) =>
1125
+ `- [${o.at.substring(0, 19)}] Lock: "${o.lockText}" (${o.lockId})\n Action: ${o.action}\n Reason: ${o.reason}`
1126
+ )
1127
+ .join("\n\n");
1128
+
1129
+ return {
1130
+ content: [
1131
+ {
1132
+ type: "text",
1133
+ text: `## Override History (${result.total})\n\n${formatted}`,
1134
+ },
1135
+ ],
1136
+ };
1137
+ }
1138
+ );
1139
+
961
1140
  // --- Smithery sandbox export ---
962
1141
  export default function createSandboxServer() {
963
1142
  return server;