speclock 1.6.0 → 2.0.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 +47 -13
- package/package.json +2 -2
- package/src/cli/index.js +150 -13
- package/src/core/engine.js +243 -70
- package/src/core/git.js +6 -0
- package/src/core/hooks.js +87 -0
- package/src/core/llm-checker.js +239 -0
- package/src/core/semantics.js +1096 -0
- package/src/core/storage.js +18 -0
- package/src/core/templates.js +114 -0
- package/src/mcp/http-server.js +44 -4
- package/src/mcp/server.js +119 -2
package/src/core/storage.js
CHANGED
|
@@ -73,6 +73,7 @@ export function makeBrain(root, hasGitRepo, defaultBranch) {
|
|
|
73
73
|
},
|
|
74
74
|
recentChanges: [],
|
|
75
75
|
reverts: [],
|
|
76
|
+
violations: [],
|
|
76
77
|
},
|
|
77
78
|
events: { lastEventId: "", count: 0 },
|
|
78
79
|
};
|
|
@@ -104,6 +105,11 @@ export function migrateBrainV1toV2(brain) {
|
|
|
104
105
|
brain.facts.deploy.url = "";
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
// Add violations array if missing
|
|
109
|
+
if (brain.state && !brain.state.violations) {
|
|
110
|
+
brain.state.violations = [];
|
|
111
|
+
}
|
|
112
|
+
|
|
107
113
|
// Remove old importance field
|
|
108
114
|
delete brain.importance;
|
|
109
115
|
|
|
@@ -120,6 +126,10 @@ export function readBrain(root) {
|
|
|
120
126
|
brain = migrateBrainV1toV2(brain);
|
|
121
127
|
writeBrain(root, brain);
|
|
122
128
|
}
|
|
129
|
+
// Ensure violations array exists (added in v1.7.0)
|
|
130
|
+
if (brain.state && !brain.state.violations) {
|
|
131
|
+
brain.state.violations = [];
|
|
132
|
+
}
|
|
123
133
|
return brain;
|
|
124
134
|
}
|
|
125
135
|
|
|
@@ -184,3 +194,11 @@ export function addRecentChange(brain, item) {
|
|
|
184
194
|
export function addRevert(brain, item) {
|
|
185
195
|
brain.state.reverts.unshift(item);
|
|
186
196
|
}
|
|
197
|
+
|
|
198
|
+
export function addViolation(brain, item) {
|
|
199
|
+
if (!brain.state.violations) brain.state.violations = [];
|
|
200
|
+
brain.state.violations.unshift(item);
|
|
201
|
+
if (brain.state.violations.length > 100) {
|
|
202
|
+
brain.state.violations = brain.state.violations.slice(0, 100);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// SpecLock Constraint Templates — Pre-built lock packs for common frameworks
|
|
2
|
+
|
|
3
|
+
export const TEMPLATES = {
|
|
4
|
+
nextjs: {
|
|
5
|
+
name: "nextjs",
|
|
6
|
+
displayName: "Next.js",
|
|
7
|
+
description: "Constraints for Next.js applications — protects routing, API routes, and middleware",
|
|
8
|
+
locks: [
|
|
9
|
+
"Never modify the authentication system without explicit permission",
|
|
10
|
+
"Never change the Next.js routing structure (app/ or pages/ directory layout)",
|
|
11
|
+
"API routes must not expose internal server logic to the client",
|
|
12
|
+
"Middleware must not be modified without review",
|
|
13
|
+
"Environment variables must not be hardcoded in source files",
|
|
14
|
+
],
|
|
15
|
+
decisions: [
|
|
16
|
+
"Framework: Next.js (App Router or Pages Router as configured)",
|
|
17
|
+
"Server components are the default; client components require 'use client' directive",
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
react: {
|
|
22
|
+
name: "react",
|
|
23
|
+
displayName: "React",
|
|
24
|
+
description: "Constraints for React applications — protects state management, component architecture",
|
|
25
|
+
locks: [
|
|
26
|
+
"Never modify the authentication system without explicit permission",
|
|
27
|
+
"Global state management pattern must not change without review",
|
|
28
|
+
"Component prop interfaces must maintain backward compatibility",
|
|
29
|
+
"Shared utility functions must not have breaking changes",
|
|
30
|
+
"Environment variables must not be hardcoded in source files",
|
|
31
|
+
],
|
|
32
|
+
decisions: [
|
|
33
|
+
"Framework: React with functional components and hooks",
|
|
34
|
+
"Styling approach must remain consistent across the project",
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
express: {
|
|
39
|
+
name: "express",
|
|
40
|
+
displayName: "Express.js API",
|
|
41
|
+
description: "Constraints for Express.js backends — protects middleware, routes, and database layer",
|
|
42
|
+
locks: [
|
|
43
|
+
"Never modify authentication or authorization middleware without explicit permission",
|
|
44
|
+
"Database connection configuration must not change without review",
|
|
45
|
+
"No breaking changes to public API endpoints",
|
|
46
|
+
"Rate limiting and security middleware must not be disabled",
|
|
47
|
+
"Environment variables and secrets must not be hardcoded",
|
|
48
|
+
],
|
|
49
|
+
decisions: [
|
|
50
|
+
"Backend: Express.js with REST API pattern",
|
|
51
|
+
"Error handling follows centralized middleware pattern",
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
supabase: {
|
|
56
|
+
name: "supabase",
|
|
57
|
+
displayName: "Supabase",
|
|
58
|
+
description: "Constraints for Supabase projects — protects auth, RLS policies, and database schema",
|
|
59
|
+
locks: [
|
|
60
|
+
"Database must always be Supabase — never switch to another provider",
|
|
61
|
+
"Row Level Security (RLS) policies must not be disabled or weakened",
|
|
62
|
+
"Supabase auth configuration must not change without explicit permission",
|
|
63
|
+
"Database schema migrations must not drop tables or columns without review",
|
|
64
|
+
"Supabase client initialization must not be modified",
|
|
65
|
+
],
|
|
66
|
+
decisions: [
|
|
67
|
+
"Database and auth provider: Supabase",
|
|
68
|
+
"All database access must go through Supabase client (no direct SQL in application code)",
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
stripe: {
|
|
73
|
+
name: "stripe",
|
|
74
|
+
displayName: "Stripe Payments",
|
|
75
|
+
description: "Constraints for Stripe integration — protects payment logic, webhooks, and pricing",
|
|
76
|
+
locks: [
|
|
77
|
+
"Payment processing logic must not be modified without explicit permission",
|
|
78
|
+
"Stripe webhook handlers must not change without review",
|
|
79
|
+
"Pricing and subscription tier definitions must not change without permission",
|
|
80
|
+
"Stripe API keys must never be hardcoded or exposed to the client",
|
|
81
|
+
"Payment error handling must not be weakened or removed",
|
|
82
|
+
],
|
|
83
|
+
decisions: [
|
|
84
|
+
"Payment provider: Stripe",
|
|
85
|
+
"All payment operations must be server-side only",
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
"security-hardened": {
|
|
90
|
+
name: "security-hardened",
|
|
91
|
+
displayName: "Security Hardened",
|
|
92
|
+
description: "Strict security constraints — protects auth, secrets, CORS, input validation",
|
|
93
|
+
locks: [
|
|
94
|
+
"Never modify authentication or authorization without explicit permission",
|
|
95
|
+
"No secrets, API keys, or credentials in source code",
|
|
96
|
+
"CORS configuration must not be loosened without review",
|
|
97
|
+
"Input validation must not be weakened or bypassed",
|
|
98
|
+
"Security headers and CSP must not be removed or weakened",
|
|
99
|
+
"Dependencies must not be downgraded without security review",
|
|
100
|
+
],
|
|
101
|
+
decisions: [
|
|
102
|
+
"Security-first development: all inputs validated, all outputs encoded",
|
|
103
|
+
"Authentication changes require explicit user approval",
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export function getTemplateNames() {
|
|
109
|
+
return Object.keys(TEMPLATES);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function getTemplate(name) {
|
|
113
|
+
return TEMPLATES[name] || null;
|
|
114
|
+
}
|
package/src/mcp/http-server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SpecLock MCP HTTP Server — for Railway / remote deployment
|
|
3
|
-
* Wraps the same
|
|
3
|
+
* Wraps the same 22 tools as the stdio server using Streamable HTTP transport.
|
|
4
4
|
* Developed by Sandeep Roy (https://github.com/sgroy10)
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -19,10 +19,15 @@ import {
|
|
|
19
19
|
updateDeployFacts,
|
|
20
20
|
logChange,
|
|
21
21
|
checkConflict,
|
|
22
|
+
checkConflictAsync,
|
|
22
23
|
getSessionBriefing,
|
|
23
24
|
endSession,
|
|
24
25
|
suggestLocks,
|
|
25
26
|
detectDrift,
|
|
27
|
+
listTemplates,
|
|
28
|
+
applyTemplate,
|
|
29
|
+
generateReport,
|
|
30
|
+
auditStagedFiles,
|
|
26
31
|
} from "../core/engine.js";
|
|
27
32
|
import { generateContext, generateContextPack } from "../core/context.js";
|
|
28
33
|
import {
|
|
@@ -41,7 +46,7 @@ import {
|
|
|
41
46
|
} from "../core/git.js";
|
|
42
47
|
|
|
43
48
|
const PROJECT_ROOT = process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
|
|
44
|
-
const VERSION = "
|
|
49
|
+
const VERSION = "2.0.0";
|
|
45
50
|
const AUTHOR = "Sandeep Roy";
|
|
46
51
|
|
|
47
52
|
function createSpecLockServer() {
|
|
@@ -172,7 +177,7 @@ function createSpecLockServer() {
|
|
|
172
177
|
// Tool 12: speclock_check_conflict
|
|
173
178
|
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 }) => {
|
|
174
179
|
ensureInit(PROJECT_ROOT);
|
|
175
|
-
const result =
|
|
180
|
+
const result = await checkConflictAsync(PROJECT_ROOT, proposedAction);
|
|
176
181
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
177
182
|
});
|
|
178
183
|
|
|
@@ -253,6 +258,41 @@ function createSpecLockServer() {
|
|
|
253
258
|
return { content: [{ type: "text", text: `## SpecLock Health Check\n\nScore: **${score}/100** (Grade: ${grade})\nEvents: ${brain.events.count} | Reverts: ${brain.state.reverts.length}\n\n### Checks\n${checks.join("\n")}${agentTimeline}\n\n---\n*SpecLock v${VERSION} — Developed by ${AUTHOR}*` }] };
|
|
254
259
|
});
|
|
255
260
|
|
|
261
|
+
// Tool 20: speclock_apply_template
|
|
262
|
+
server.tool("speclock_apply_template", "Apply a pre-built constraint template (nextjs, react, express, supabase, stripe, security-hardened).", { name: z.string().optional().describe("Template name. Omit to list.") }, async ({ name }) => {
|
|
263
|
+
ensureInit(PROJECT_ROOT);
|
|
264
|
+
if (!name) {
|
|
265
|
+
const templates = listTemplates();
|
|
266
|
+
const text = templates.map(t => `- **${t.name}** (${t.displayName}): ${t.description} — ${t.lockCount} locks, ${t.decisionCount} decisions`).join("\n");
|
|
267
|
+
return { content: [{ type: "text", text: `## Available Templates\n\n${text}\n\nCall again with a name to apply.` }] };
|
|
268
|
+
}
|
|
269
|
+
const result = applyTemplate(PROJECT_ROOT, name);
|
|
270
|
+
if (!result.applied) return { content: [{ type: "text", text: result.error }], isError: true };
|
|
271
|
+
return { content: [{ type: "text", text: `Template "${result.displayName}" applied: ${result.locksAdded} lock(s) + ${result.decisionsAdded} decision(s).` }] };
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Tool 21: speclock_report
|
|
275
|
+
server.tool("speclock_report", "Violation report — how many times SpecLock blocked changes.", {}, async () => {
|
|
276
|
+
ensureInit(PROJECT_ROOT);
|
|
277
|
+
const report = generateReport(PROJECT_ROOT);
|
|
278
|
+
const parts = [`## Violation Report`, `Total blocked: **${report.totalViolations}**`];
|
|
279
|
+
if (report.mostTestedLocks.length > 0) {
|
|
280
|
+
parts.push("", "### Most Tested Locks");
|
|
281
|
+
for (const l of report.mostTestedLocks) parts.push(`- ${l.count}x — "${l.text}"`);
|
|
282
|
+
}
|
|
283
|
+
parts.push("", report.summary);
|
|
284
|
+
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Tool 22: speclock_audit
|
|
288
|
+
server.tool("speclock_audit", "Audit staged files against active locks.", {}, async () => {
|
|
289
|
+
ensureInit(PROJECT_ROOT);
|
|
290
|
+
const result = auditStagedFiles(PROJECT_ROOT);
|
|
291
|
+
if (result.passed) return { content: [{ type: "text", text: result.message }] };
|
|
292
|
+
const text = result.violations.map(v => `- [${v.severity}] **${v.file}** — ${v.reason}\n Lock: "${v.lockText}"`).join("\n");
|
|
293
|
+
return { content: [{ type: "text", text: `## Audit Failed\n\n${text}\n\n${result.message}` }] };
|
|
294
|
+
});
|
|
295
|
+
|
|
256
296
|
return server;
|
|
257
297
|
}
|
|
258
298
|
|
|
@@ -292,7 +332,7 @@ app.get("/", (req, res) => {
|
|
|
292
332
|
version: VERSION,
|
|
293
333
|
author: AUTHOR,
|
|
294
334
|
description: "AI Continuity Engine — Kill AI amnesia",
|
|
295
|
-
tools:
|
|
335
|
+
tools: 22,
|
|
296
336
|
mcp_endpoint: "/mcp",
|
|
297
337
|
npm: "https://www.npmjs.com/package/speclock",
|
|
298
338
|
github: "https://github.com/sgroy10/speclock",
|
package/src/mcp/server.js
CHANGED
|
@@ -12,12 +12,17 @@ import {
|
|
|
12
12
|
updateDeployFacts,
|
|
13
13
|
logChange,
|
|
14
14
|
checkConflict,
|
|
15
|
+
checkConflictAsync,
|
|
15
16
|
getSessionBriefing,
|
|
16
17
|
endSession,
|
|
17
18
|
suggestLocks,
|
|
18
19
|
detectDrift,
|
|
19
20
|
syncLocksToPackageJson,
|
|
20
21
|
autoGuardRelatedFiles,
|
|
22
|
+
listTemplates,
|
|
23
|
+
applyTemplate,
|
|
24
|
+
generateReport,
|
|
25
|
+
auditStagedFiles,
|
|
21
26
|
} from "../core/engine.js";
|
|
22
27
|
import { generateContext, generateContextPack } from "../core/context.js";
|
|
23
28
|
import {
|
|
@@ -52,7 +57,7 @@ const PROJECT_ROOT =
|
|
|
52
57
|
args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
|
|
53
58
|
|
|
54
59
|
// --- MCP Server ---
|
|
55
|
-
const VERSION = "
|
|
60
|
+
const VERSION = "2.0.0";
|
|
56
61
|
const AUTHOR = "Sandeep Roy";
|
|
57
62
|
|
|
58
63
|
const server = new McpServer(
|
|
@@ -423,7 +428,7 @@ server.tool(
|
|
|
423
428
|
.describe("Description of the action you plan to take"),
|
|
424
429
|
},
|
|
425
430
|
async ({ proposedAction }) => {
|
|
426
|
-
const result =
|
|
431
|
+
const result = await checkConflictAsync(PROJECT_ROOT, proposedAction);
|
|
427
432
|
return {
|
|
428
433
|
content: [{ type: "text", text: result.analysis }],
|
|
429
434
|
};
|
|
@@ -766,6 +771,118 @@ server.tool(
|
|
|
766
771
|
}
|
|
767
772
|
);
|
|
768
773
|
|
|
774
|
+
// ========================================
|
|
775
|
+
// TEMPLATE, REPORT & AUDIT TOOLS (v1.7.0)
|
|
776
|
+
// ========================================
|
|
777
|
+
|
|
778
|
+
// Tool 20: speclock_apply_template
|
|
779
|
+
server.tool(
|
|
780
|
+
"speclock_apply_template",
|
|
781
|
+
"Apply a pre-built constraint template (e.g., nextjs, react, express, supabase, stripe, security-hardened). Templates add recommended locks and decisions for common frameworks.",
|
|
782
|
+
{
|
|
783
|
+
name: z
|
|
784
|
+
.string()
|
|
785
|
+
.optional()
|
|
786
|
+
.describe("Template name to apply. Omit to list available templates."),
|
|
787
|
+
},
|
|
788
|
+
async ({ name }) => {
|
|
789
|
+
if (!name) {
|
|
790
|
+
const templates = listTemplates();
|
|
791
|
+
const formatted = templates
|
|
792
|
+
.map((t) => `- **${t.name}** (${t.displayName}): ${t.description} — ${t.lockCount} locks, ${t.decisionCount} decisions`)
|
|
793
|
+
.join("\n");
|
|
794
|
+
return {
|
|
795
|
+
content: [
|
|
796
|
+
{
|
|
797
|
+
type: "text",
|
|
798
|
+
text: `## Available Templates\n\n${formatted}\n\nCall again with a name to apply.`,
|
|
799
|
+
},
|
|
800
|
+
],
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
const result = applyTemplate(PROJECT_ROOT, name);
|
|
804
|
+
if (!result.applied) {
|
|
805
|
+
return {
|
|
806
|
+
content: [{ type: "text", text: result.error }],
|
|
807
|
+
isError: true,
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
return {
|
|
811
|
+
content: [
|
|
812
|
+
{
|
|
813
|
+
type: "text",
|
|
814
|
+
text: `Template "${result.displayName}" applied: ${result.locksAdded} lock(s) + ${result.decisionsAdded} decision(s) added.`,
|
|
815
|
+
},
|
|
816
|
+
],
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
// Tool 21: speclock_report
|
|
822
|
+
server.tool(
|
|
823
|
+
"speclock_report",
|
|
824
|
+
"Get a violation report showing how many times SpecLock blocked constraint violations, which locks were tested most, and recent violations.",
|
|
825
|
+
{},
|
|
826
|
+
async () => {
|
|
827
|
+
const report = generateReport(PROJECT_ROOT);
|
|
828
|
+
|
|
829
|
+
const parts = [`## SpecLock Violation Report`, ``, `Total violations blocked: **${report.totalViolations}**`];
|
|
830
|
+
|
|
831
|
+
if (report.timeRange) {
|
|
832
|
+
parts.push(`Period: ${report.timeRange.from.substring(0, 10)} to ${report.timeRange.to.substring(0, 10)}`);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (report.mostTestedLocks.length > 0) {
|
|
836
|
+
parts.push("", "### Most Tested Locks");
|
|
837
|
+
for (const lock of report.mostTestedLocks) {
|
|
838
|
+
parts.push(`- ${lock.count}x — "${lock.text}"`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (report.recentViolations.length > 0) {
|
|
843
|
+
parts.push("", "### Recent Violations");
|
|
844
|
+
for (const v of report.recentViolations) {
|
|
845
|
+
parts.push(`- [${v.at.substring(0, 19)}] ${v.topLevel} (${v.topConfidence}%) — "${v.action}"`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
parts.push("", `---`, report.summary);
|
|
850
|
+
|
|
851
|
+
return {
|
|
852
|
+
content: [{ type: "text", text: parts.join("\n") }],
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
);
|
|
856
|
+
|
|
857
|
+
// Tool 22: speclock_audit
|
|
858
|
+
server.tool(
|
|
859
|
+
"speclock_audit",
|
|
860
|
+
"Audit git staged files against active SpecLock constraints. Returns pass/fail with details on which files violate locks. Used by the pre-commit hook.",
|
|
861
|
+
{},
|
|
862
|
+
async () => {
|
|
863
|
+
const result = auditStagedFiles(PROJECT_ROOT);
|
|
864
|
+
|
|
865
|
+
if (result.passed) {
|
|
866
|
+
return {
|
|
867
|
+
content: [{ type: "text", text: result.message }],
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const formatted = result.violations
|
|
872
|
+
.map((v) => `- [${v.severity}] **${v.file}** — ${v.reason}\n Lock: "${v.lockText}"`)
|
|
873
|
+
.join("\n");
|
|
874
|
+
|
|
875
|
+
return {
|
|
876
|
+
content: [
|
|
877
|
+
{
|
|
878
|
+
type: "text",
|
|
879
|
+
text: `## Audit Failed\n\n${formatted}\n\n${result.message}`,
|
|
880
|
+
},
|
|
881
|
+
],
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
);
|
|
885
|
+
|
|
769
886
|
// --- Smithery sandbox export ---
|
|
770
887
|
export default function createSandboxServer() {
|
|
771
888
|
return server;
|