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.
- package/README.md +32 -4
- package/package.json +4 -3
- package/src/cli/index.js +107 -3
- package/src/core/compliance.js +1 -1
- package/src/core/conflict.js +363 -0
- package/src/core/enforcer.js +314 -0
- package/src/core/engine.js +87 -781
- package/src/core/memory.js +191 -0
- package/src/core/pre-commit-semantic.js +284 -0
- package/src/core/sessions.js +128 -0
- package/src/core/tracking.js +98 -0
- package/src/mcp/http-server.js +42 -5
- package/src/mcp/server.js +183 -4
package/src/mcp/http-server.js
CHANGED
|
@@ -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.
|
|
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 =
|
|
226
|
-
|
|
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.
|
|
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
|
-
|
|
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;
|