speclock 1.6.0 → 1.7.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 CHANGED
@@ -174,10 +174,10 @@ Result: [HIGH] Conflict detected (confidence: 85%)
174
174
  | Mode | Platforms | How It Works |
175
175
  |------|-----------|--------------|
176
176
  | **MCP Remote** | Lovable, bolt.diy, Base44 | Connect via URL — no install needed |
177
- | **MCP Local** | Claude Code, Cursor, Windsurf, Cline | `npx speclock serve` — 19 tools via MCP |
177
+ | **MCP Local** | Claude Code, Cursor, Windsurf, Cline | `npx speclock serve` — 22 tools via MCP |
178
178
  | **npm File-Based** | Bolt.new, Aider, Rocket.new | `npx speclock setup` — AI reads SPECLOCK.md + uses CLI |
179
179
 
180
- ## 19 MCP Tools
180
+ ## 22 MCP Tools
181
181
 
182
182
  ### Memory Management
183
183
  | Tool | Purpose |
@@ -218,6 +218,13 @@ Result: [HIGH] Conflict detected (confidence: 85%)
218
218
  | `speclock_detect_drift` | Scan changes for constraint violations |
219
219
  | `speclock_health` | Health score + multi-agent timeline |
220
220
 
221
+ ### Templates, Reports & Enforcement (v1.7.0)
222
+ | Tool | Purpose |
223
+ |------|---------|
224
+ | `speclock_apply_template` | Apply pre-built constraint templates (nextjs, react, express, etc.) |
225
+ | `speclock_report` | Violation report — blocked change stats |
226
+ | `speclock_audit` | Audit staged files against active locks |
227
+
221
228
  ## Auto-Guard: Locks That Actually Work
222
229
 
223
230
  When you add a lock, SpecLock **automatically finds and guards related files**:
@@ -251,7 +258,7 @@ Active locks are also embedded in `package.json` — so the AI sees your constra
251
258
 
252
259
  ```bash
253
260
  # Setup
254
- speclock setup --goal "Build my app" # One-shot: init + rules + context
261
+ speclock setup --goal "Build my app" --template nextjs # One-shot setup + template
255
262
 
256
263
  # Memory
257
264
  speclock goal <text> # Set project goal
@@ -265,6 +272,18 @@ speclock check <text> # Check for lock conflicts
265
272
  speclock guard <file> --lock "text" # Manually guard a specific file
266
273
  speclock unguard <file> # Remove guard from file
267
274
 
275
+ # Templates (v1.7.0)
276
+ speclock template list # List available templates
277
+ speclock template apply <name> # Apply: nextjs, react, express, supabase, stripe, security-hardened
278
+
279
+ # Violation Report (v1.7.0)
280
+ speclock report # Show violation stats + most tested locks
281
+
282
+ # Git Pre-commit Hook (v1.7.0)
283
+ speclock hook install # Install pre-commit hook
284
+ speclock hook remove # Remove pre-commit hook
285
+ speclock audit # Audit staged files against locks
286
+
268
287
  # Tracking
269
288
  speclock log-change <text> --files x # Log a change
270
289
  speclock context # Regenerate context file
@@ -283,7 +302,7 @@ speclock watch # Start file watcher
283
302
  └──────────────┬──────────────────┬────────────────────┘
284
303
  │ │
285
304
  MCP Protocol File-Based (npm)
286
- (19 tool calls) (reads SPECLOCK.md +
305
+ (22 tool calls) (reads SPECLOCK.md +
287
306
  .speclock/context/latest.md,
288
307
  runs CLI commands)
289
308
  │ │
@@ -318,4 +337,4 @@ MIT License - see [LICENSE](LICENSE) file.
318
337
 
319
338
  ---
320
339
 
321
- *SpecLock v1.6.0 — Because remembering isn't enough. AI needs to respect boundaries.*
340
+ *SpecLock v1.7.0 — Because remembering isn't enough. AI needs to respect boundaries.*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "speclock",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "AI constraint engine — MCP server + CLI with active enforcement. Memory + guardrails for AI coding tools. Works with Bolt.new, Claude Code, Cursor, Lovable.",
5
5
  "type": "module",
6
6
  "main": "src/mcp/server.js",
package/src/cli/index.js CHANGED
@@ -16,9 +16,14 @@ import {
16
16
  injectPackageJsonMarker,
17
17
  syncLocksToPackageJson,
18
18
  autoGuardRelatedFiles,
19
+ listTemplates,
20
+ applyTemplate,
21
+ generateReport,
22
+ auditStagedFiles,
19
23
  } from "../core/engine.js";
20
24
  import { generateContext } from "../core/context.js";
21
25
  import { readBrain } from "../core/storage.js";
26
+ import { installHook, removeHook } from "../core/hooks.js";
22
27
 
23
28
  // --- Argument parsing ---
24
29
 
@@ -74,23 +79,29 @@ function refreshContext(root) {
74
79
 
75
80
  function printHelp() {
76
81
  console.log(`
77
- SpecLock v1.6.0 — AI Constraint Engine
82
+ SpecLock v1.7.0 — AI Constraint Engine
78
83
  Developed by Sandeep Roy (github.com/sgroy10)
79
84
 
80
85
  Usage: speclock <command> [options]
81
86
 
82
87
  Commands:
83
- setup [--goal <text>] Full setup: init + SPECLOCK.md + context
88
+ setup [--goal <text>] [--template <name>] Full setup: init + SPECLOCK.md + context
84
89
  init Initialize SpecLock in current directory
85
90
  goal <text> Set or update the project goal
86
91
  lock <text> [--tags a,b] Add a non-negotiable constraint
87
92
  lock remove <id> Remove a lock by ID
88
- guard <file> [--lock "text"] Inject lock warning into a file (NEW)
89
- unguard <file> Remove lock warning from a file (NEW)
93
+ guard <file> [--lock "text"] Inject lock warning into a file
94
+ unguard <file> Remove lock warning from a file
90
95
  decide <text> [--tags a,b] Record a decision
91
96
  note <text> [--pinned] Add a pinned note
92
97
  log-change <text> [--files x,y] Log a significant change
93
98
  check <text> Check if action conflicts with locks
99
+ template list List available constraint templates
100
+ template apply <name> Apply a template (nextjs, react, etc.)
101
+ report Show violation report + stats
102
+ hook install Install git pre-commit hook
103
+ hook remove Remove git pre-commit hook
104
+ audit Audit staged files against locks
94
105
  context Generate and print context pack
95
106
  facts deploy [--provider X] Set deployment facts
96
107
  watch Start file watcher (auto-track changes)
@@ -102,17 +113,20 @@ Options:
102
113
  --source <user|agent> Who created this (default: user)
103
114
  --files <a.ts,b.ts> Comma-separated file paths
104
115
  --goal <text> Goal text (for setup command)
116
+ --template <name> Template to apply during setup
105
117
  --lock <text> Lock text (for guard command)
106
118
  --project <path> Project root (for serve)
107
119
 
120
+ Templates: nextjs, react, express, supabase, stripe, security-hardened
121
+
108
122
  Examples:
109
- npx speclock setup --goal "Build PawPalace pet shop"
123
+ npx speclock setup --goal "Build PawPalace pet shop" --template nextjs
110
124
  npx speclock lock "Never modify auth files"
111
- npx speclock guard src/Auth.tsx --lock "Never modify auth files"
125
+ npx speclock template apply supabase
112
126
  npx speclock check "Adding social login to auth page"
113
- npx speclock log-change "Built payment system" --files src/pay.tsx
114
- npx speclock decide "Use Supabase for auth"
115
- npx speclock context
127
+ npx speclock report
128
+ npx speclock hook install
129
+ npx speclock audit
116
130
  npx speclock status
117
131
  `);
118
132
  }
@@ -190,11 +204,21 @@ async function main() {
190
204
  console.log("Injected SpecLock marker into package.json.");
191
205
  }
192
206
 
193
- // 5. Generate context
207
+ // 5. Apply template if specified
208
+ if (flags.template) {
209
+ const result = applyTemplate(root, flags.template);
210
+ if (result.applied) {
211
+ console.log(`Applied template "${result.displayName}": ${result.locksAdded} lock(s), ${result.decisionsAdded} decision(s).`);
212
+ } else {
213
+ console.error(`Template error: ${result.error}`);
214
+ }
215
+ }
216
+
217
+ // 6. Generate context
194
218
  generateContext(root);
195
219
  console.log("Generated .speclock/context/latest.md");
196
220
 
197
- // 6. Print summary
221
+ // 7. Print summary
198
222
  console.log(`
199
223
  SpecLock is ready!
200
224
 
@@ -355,8 +379,8 @@ Tip: When starting a new chat, tell the AI:
355
379
  console.log(`\nCONFLICT DETECTED`);
356
380
  console.log("=".repeat(50));
357
381
  for (const lock of result.conflictingLocks) {
358
- console.log(` [${lock.confidence}] "${lock.text}"`);
359
- console.log(` Confidence: ${lock.score}%`);
382
+ console.log(` [${lock.level}] "${lock.text}"`);
383
+ console.log(` Confidence: ${lock.confidence}%`);
360
384
  if (lock.reasons && lock.reasons.length > 0) {
361
385
  for (const reason of lock.reasons) {
362
386
  console.log(` - ${reason}`);
@@ -460,6 +484,119 @@ Tip: When starting a new chat, tell the AI:
460
484
  return;
461
485
  }
462
486
 
487
+ // --- TEMPLATE ---
488
+ if (cmd === "template") {
489
+ const sub = args[0];
490
+ if (sub === "list" || !sub) {
491
+ const templates = listTemplates();
492
+ console.log("\nAvailable Templates:");
493
+ console.log("=".repeat(50));
494
+ for (const t of templates) {
495
+ console.log(` ${t.name.padEnd(20)} ${t.displayName} — ${t.description}`);
496
+ console.log(` ${"".padEnd(20)} ${t.lockCount} lock(s), ${t.decisionCount} decision(s)`);
497
+ console.log("");
498
+ }
499
+ console.log("Apply: npx speclock template apply <name>");
500
+ return;
501
+ }
502
+ if (sub === "apply") {
503
+ const name = args[1];
504
+ if (!name) {
505
+ console.error("Error: Template name is required.");
506
+ console.error("Usage: speclock template apply <name>");
507
+ console.error("Run 'speclock template list' to see available templates.");
508
+ process.exit(1);
509
+ }
510
+ const result = applyTemplate(root, name);
511
+ if (result.applied) {
512
+ refreshContext(root);
513
+ console.log(`Template "${result.displayName}" applied successfully!`);
514
+ console.log(` Locks added: ${result.locksAdded}`);
515
+ console.log(` Decisions added: ${result.decisionsAdded}`);
516
+ } else {
517
+ console.error(result.error);
518
+ process.exit(1);
519
+ }
520
+ return;
521
+ }
522
+ console.error(`Unknown template command: ${sub}`);
523
+ console.error("Usage: speclock template list | speclock template apply <name>");
524
+ process.exit(1);
525
+ }
526
+
527
+ // --- REPORT ---
528
+ if (cmd === "report") {
529
+ const report = generateReport(root);
530
+ console.log("\nSpecLock Violation Report");
531
+ console.log("=".repeat(50));
532
+ console.log(`Total violations blocked: ${report.totalViolations}`);
533
+ if (report.timeRange) {
534
+ console.log(`Period: ${report.timeRange.from.substring(0, 10)} to ${report.timeRange.to.substring(0, 10)}`);
535
+ }
536
+ if (report.mostTestedLocks.length > 0) {
537
+ console.log("\nMost tested locks:");
538
+ for (const lock of report.mostTestedLocks) {
539
+ console.log(` ${lock.count}x — "${lock.text}"`);
540
+ }
541
+ }
542
+ if (report.recentViolations.length > 0) {
543
+ console.log("\nRecent violations:");
544
+ for (const v of report.recentViolations) {
545
+ console.log(` [${v.at.substring(0, 19)}] ${v.topLevel} (${v.topConfidence}%) — "${v.action.substring(0, 60)}"`);
546
+ }
547
+ }
548
+ console.log(`\n${report.summary}`);
549
+ return;
550
+ }
551
+
552
+ // --- HOOK ---
553
+ if (cmd === "hook") {
554
+ const sub = args[0];
555
+ if (sub === "install") {
556
+ const result = installHook(root);
557
+ if (result.success) {
558
+ console.log(result.message);
559
+ } else {
560
+ console.error(result.error);
561
+ process.exit(1);
562
+ }
563
+ return;
564
+ }
565
+ if (sub === "remove") {
566
+ const result = removeHook(root);
567
+ if (result.success) {
568
+ console.log(result.message);
569
+ } else {
570
+ console.error(result.error);
571
+ process.exit(1);
572
+ }
573
+ return;
574
+ }
575
+ console.error("Usage: speclock hook install | speclock hook remove");
576
+ process.exit(1);
577
+ }
578
+
579
+ // --- AUDIT ---
580
+ if (cmd === "audit") {
581
+ const result = auditStagedFiles(root);
582
+ if (result.passed) {
583
+ console.log(result.message);
584
+ process.exit(0);
585
+ } else {
586
+ console.log("\nSPECLOCK AUDIT FAILED");
587
+ console.log("=".repeat(50));
588
+ for (const v of result.violations) {
589
+ console.log(` [${v.severity}] ${v.file}`);
590
+ console.log(` Lock: ${v.lockText}`);
591
+ console.log(` Reason: ${v.reason}`);
592
+ console.log("");
593
+ }
594
+ console.log(result.message);
595
+ console.log("Commit blocked. Unlock files or unstage them to proceed.");
596
+ process.exit(1);
597
+ }
598
+ }
599
+
463
600
  // --- STATUS ---
464
601
  if (cmd === "status") {
465
602
  showStatus(root);
@@ -13,8 +13,10 @@ import {
13
13
  addRecentChange,
14
14
  addRevert,
15
15
  readEvents,
16
+ addViolation,
16
17
  } from "./storage.js";
17
- import { hasGit, getHead, getDefaultBranch, captureDiff } from "./git.js";
18
+ import { hasGit, getHead, getDefaultBranch, captureDiff, getStagedFiles } from "./git.js";
19
+ import { getTemplateNames, getTemplate } from "./templates.js";
18
20
 
19
21
  // --- Internal helpers ---
20
22
 
@@ -385,11 +387,23 @@ export function checkConflict(root, proposedAction) {
385
387
  )
386
388
  .join("\n");
387
389
 
388
- return {
390
+ const result = {
389
391
  hasConflict: true,
390
392
  conflictingLocks: conflicting,
391
393
  analysis: `Potential conflict with ${conflicting.length} lock(s):\n${details}\nReview before proceeding.`,
392
394
  };
395
+
396
+ // Record violation for reporting
397
+ addViolation(brain, {
398
+ at: nowIso(),
399
+ action: proposedAction,
400
+ locks: conflicting.map((c) => ({ id: c.id, text: c.text, confidence: c.confidence, level: c.level })),
401
+ topLevel: conflicting[0].level,
402
+ topConfidence: conflicting[0].confidence,
403
+ });
404
+ writeBrain(root, brain);
405
+
406
+ return result;
393
407
  }
394
408
 
395
409
  // --- Auto-lock suggestions ---
@@ -777,6 +791,11 @@ npx speclock unguard <file> # Remove file protection
777
791
  npx speclock lock remove <lockId> # Unlock (only after explicit permission)
778
792
  npx speclock log-change "what changed" # Log changes
779
793
  npx speclock decide "decision" # Record a decision
794
+ npx speclock template list # List constraint templates
795
+ npx speclock template apply <name> # Apply a template (nextjs, react, etc.)
796
+ npx speclock report # Show violation stats
797
+ npx speclock hook install # Install git pre-commit hook
798
+ npx speclock audit # Audit staged files vs locks
780
799
  npx speclock context # Refresh context file
781
800
  \`\`\`
782
801
 
@@ -1042,3 +1061,188 @@ export function unguardFile(root, relativeFilePath) {
1042
1061
 
1043
1062
  return { success: true };
1044
1063
  }
1064
+
1065
+ // --- Constraint Templates ---
1066
+
1067
+ export function listTemplates() {
1068
+ const names = getTemplateNames();
1069
+ return names.map((name) => {
1070
+ const t = getTemplate(name);
1071
+ return {
1072
+ name: t.name,
1073
+ displayName: t.displayName,
1074
+ description: t.description,
1075
+ lockCount: t.locks.length,
1076
+ decisionCount: t.decisions.length,
1077
+ };
1078
+ });
1079
+ }
1080
+
1081
+ export function applyTemplate(root, templateName) {
1082
+ const template = getTemplate(templateName);
1083
+ if (!template) {
1084
+ return { applied: false, error: `Template not found: "${templateName}". Available: ${getTemplateNames().join(", ")}` };
1085
+ }
1086
+
1087
+ ensureInit(root);
1088
+
1089
+ let locksAdded = 0;
1090
+ let decisionsAdded = 0;
1091
+
1092
+ for (const lockText of template.locks) {
1093
+ addLock(root, lockText, [template.name], "agent");
1094
+ autoGuardRelatedFiles(root, lockText);
1095
+ locksAdded++;
1096
+ }
1097
+
1098
+ for (const decText of template.decisions) {
1099
+ addDecision(root, decText, [template.name], "agent");
1100
+ decisionsAdded++;
1101
+ }
1102
+
1103
+ syncLocksToPackageJson(root);
1104
+
1105
+ return {
1106
+ applied: true,
1107
+ templateName: template.name,
1108
+ displayName: template.displayName,
1109
+ locksAdded,
1110
+ decisionsAdded,
1111
+ };
1112
+ }
1113
+
1114
+ // --- Violation Report ---
1115
+
1116
+ export function generateReport(root) {
1117
+ const brain = ensureInit(root);
1118
+ const violations = brain.state.violations || [];
1119
+
1120
+ if (violations.length === 0) {
1121
+ return {
1122
+ totalViolations: 0,
1123
+ violationsByLock: {},
1124
+ mostTestedLocks: [],
1125
+ recentViolations: [],
1126
+ summary: "No violations recorded yet. SpecLock is watching.",
1127
+ };
1128
+ }
1129
+
1130
+ // Count violations per lock
1131
+ const byLock = {};
1132
+ for (const v of violations) {
1133
+ for (const lock of v.locks) {
1134
+ if (!byLock[lock.text]) {
1135
+ byLock[lock.text] = { count: 0, lockId: lock.id, text: lock.text };
1136
+ }
1137
+ byLock[lock.text].count++;
1138
+ }
1139
+ }
1140
+
1141
+ // Sort by count descending
1142
+ const mostTested = Object.values(byLock).sort((a, b) => b.count - a.count);
1143
+
1144
+ // Recent 10
1145
+ const recent = violations.slice(0, 10).map((v) => ({
1146
+ at: v.at,
1147
+ action: v.action,
1148
+ topLevel: v.topLevel,
1149
+ topConfidence: v.topConfidence,
1150
+ lockCount: v.locks.length,
1151
+ }));
1152
+
1153
+ // Time range
1154
+ const oldest = violations[violations.length - 1];
1155
+ const newest = violations[0];
1156
+
1157
+ return {
1158
+ totalViolations: violations.length,
1159
+ timeRange: { from: oldest.at, to: newest.at },
1160
+ violationsByLock: byLock,
1161
+ mostTestedLocks: mostTested.slice(0, 5),
1162
+ recentViolations: recent,
1163
+ summary: `SpecLock blocked ${violations.length} violation(s). Most tested lock: "${mostTested[0].text}" (${mostTested[0].count} blocks).`,
1164
+ };
1165
+ }
1166
+
1167
+ // --- Pre-commit Audit ---
1168
+
1169
+ export function auditStagedFiles(root) {
1170
+ const brain = ensureInit(root);
1171
+ const activeLocks = brain.specLock.items.filter((l) => l.active !== false);
1172
+
1173
+ if (activeLocks.length === 0) {
1174
+ return { passed: true, violations: [], checkedFiles: 0, activeLocks: 0, message: "No active locks. Audit passed." };
1175
+ }
1176
+
1177
+ const stagedFiles = getStagedFiles(root);
1178
+ if (stagedFiles.length === 0) {
1179
+ return { passed: true, violations: [], checkedFiles: 0, activeLocks: activeLocks.length, message: "No staged files. Audit passed." };
1180
+ }
1181
+
1182
+ const violations = [];
1183
+
1184
+ for (const file of stagedFiles) {
1185
+ // Check 1: Does the file have a SPECLOCK-GUARD header?
1186
+ const fullPath = path.join(root, file);
1187
+ if (fs.existsSync(fullPath)) {
1188
+ try {
1189
+ const content = fs.readFileSync(fullPath, "utf-8");
1190
+ if (content.includes(GUARD_TAG)) {
1191
+ violations.push({
1192
+ file,
1193
+ reason: "File has SPECLOCK-GUARD header — it is locked and must not be modified",
1194
+ lockText: "(file-level guard)",
1195
+ severity: "HIGH",
1196
+ });
1197
+ continue;
1198
+ }
1199
+ } catch (_) {}
1200
+ }
1201
+
1202
+ // Check 2: Does the file path match any lock keywords?
1203
+ const fileLower = file.toLowerCase();
1204
+ for (const lock of activeLocks) {
1205
+ const lockLower = lock.text.toLowerCase();
1206
+ const lockHasNegation = hasNegation(lockLower);
1207
+ if (!lockHasNegation) continue;
1208
+
1209
+ // Check if any FILE_KEYWORD_PATTERNS keywords from the lock match this file
1210
+ for (const group of FILE_KEYWORD_PATTERNS) {
1211
+ const lockMatchesKeyword = group.keywords.some((kw) => lockLower.includes(kw));
1212
+ if (!lockMatchesKeyword) continue;
1213
+
1214
+ const fileMatchesPattern = group.patterns.some((pattern) => patternMatchesFile(pattern, fileLower) || patternMatchesFile(pattern, fileLower.split("/").pop()));
1215
+ if (fileMatchesPattern) {
1216
+ violations.push({
1217
+ file,
1218
+ reason: `File matches lock keyword pattern`,
1219
+ lockText: lock.text,
1220
+ severity: "MEDIUM",
1221
+ });
1222
+ break;
1223
+ }
1224
+ }
1225
+ }
1226
+ }
1227
+
1228
+ // Deduplicate by file
1229
+ const seen = new Set();
1230
+ const unique = violations.filter((v) => {
1231
+ if (seen.has(v.file)) return false;
1232
+ seen.add(v.file);
1233
+ return true;
1234
+ });
1235
+
1236
+ const passed = unique.length === 0;
1237
+ const message = passed
1238
+ ? `Audit passed. ${stagedFiles.length} file(s) checked against ${activeLocks.length} lock(s).`
1239
+ : `AUDIT FAILED: ${unique.length} violation(s) in ${stagedFiles.length} staged file(s).`;
1240
+
1241
+ return {
1242
+ passed,
1243
+ violations: unique,
1244
+ checkedFiles: stagedFiles.length,
1245
+ activeLocks: activeLocks.length,
1246
+ message,
1247
+ };
1248
+ }
package/src/core/git.js CHANGED
@@ -108,3 +108,9 @@ export function getDiffSummary(root) {
108
108
  if (!res.ok) return "";
109
109
  return res.stdout;
110
110
  }
111
+
112
+ export function getStagedFiles(root) {
113
+ const res = safeGit(root, ["diff", "--cached", "--name-only"]);
114
+ if (!res.ok || !res.stdout) return [];
115
+ return res.stdout.split("\n").filter(Boolean);
116
+ }
@@ -0,0 +1,87 @@
1
+ // SpecLock Git Hook Management
2
+
3
+ import fs from "fs";
4
+ import path from "path";
5
+
6
+ const HOOK_MARKER = "# SPECLOCK-HOOK";
7
+
8
+ const HOOK_SCRIPT = `#!/bin/sh
9
+ ${HOOK_MARKER} — Do not remove this line
10
+ # SpecLock pre-commit hook: checks staged files against active locks
11
+ # Install: npx speclock hook install
12
+ # Remove: npx speclock hook remove
13
+
14
+ npx speclock audit
15
+ exit $?
16
+ `;
17
+
18
+ export function installHook(root) {
19
+ const hooksDir = path.join(root, ".git", "hooks");
20
+ if (!fs.existsSync(path.join(root, ".git"))) {
21
+ return { success: false, error: "Not a git repository. Run 'git init' first." };
22
+ }
23
+
24
+ // Ensure hooks directory exists
25
+ fs.mkdirSync(hooksDir, { recursive: true });
26
+
27
+ const hookPath = path.join(hooksDir, "pre-commit");
28
+
29
+ // Check if existing hook exists (not ours)
30
+ if (fs.existsSync(hookPath)) {
31
+ const existing = fs.readFileSync(hookPath, "utf-8");
32
+ if (existing.includes(HOOK_MARKER)) {
33
+ return { success: false, error: "SpecLock pre-commit hook is already installed." };
34
+ }
35
+ // Append to existing hook
36
+ const appended = existing.trimEnd() + "\n\n" + HOOK_SCRIPT;
37
+ fs.writeFileSync(hookPath, appended, { mode: 0o755 });
38
+ return { success: true, message: "SpecLock hook appended to existing pre-commit hook." };
39
+ }
40
+
41
+ fs.writeFileSync(hookPath, HOOK_SCRIPT, { mode: 0o755 });
42
+ return { success: true, message: "SpecLock pre-commit hook installed." };
43
+ }
44
+
45
+ export function removeHook(root) {
46
+ const hookPath = path.join(root, ".git", "hooks", "pre-commit");
47
+ if (!fs.existsSync(hookPath)) {
48
+ return { success: false, error: "No pre-commit hook found." };
49
+ }
50
+
51
+ const content = fs.readFileSync(hookPath, "utf-8");
52
+ if (!content.includes(HOOK_MARKER)) {
53
+ return { success: false, error: "Pre-commit hook exists but was not installed by SpecLock." };
54
+ }
55
+
56
+ // Check if lines other than our speclock block exist
57
+ const lines = content.split("\n");
58
+ const nonSpeclockLines = lines.filter((line) => {
59
+ const trimmed = line.trim();
60
+ const lower = trimmed.toLowerCase();
61
+ if (!trimmed || trimmed === "#!/bin/sh") return false;
62
+ if (lower.includes("speclock")) return false;
63
+ if (lower.includes("npx speclock")) return false;
64
+ if (trimmed === "exit $?") return false;
65
+ return true;
66
+ });
67
+
68
+ if (nonSpeclockLines.length === 0) {
69
+ // Entire hook was ours — remove file
70
+ fs.unlinkSync(hookPath);
71
+ return { success: true, message: "SpecLock pre-commit hook removed." };
72
+ }
73
+
74
+ // Other hook content exists — remove our block, keep the rest
75
+ const cleaned = content
76
+ .replace(/\n*# SPECLOCK-HOOK[^\n]*\n.*?exit \$\?\n?/s, "\n")
77
+ .trim();
78
+ fs.writeFileSync(hookPath, cleaned + "\n", { mode: 0o755 });
79
+ return { success: true, message: "SpecLock hook removed. Other hook content preserved." };
80
+ }
81
+
82
+ export function isHookInstalled(root) {
83
+ const hookPath = path.join(root, ".git", "hooks", "pre-commit");
84
+ if (!fs.existsSync(hookPath)) return false;
85
+ const content = fs.readFileSync(hookPath, "utf-8");
86
+ return content.includes(HOOK_MARKER);
87
+ }
@@ -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
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * SpecLock MCP HTTP Server — for Railway / remote deployment
3
- * Wraps the same 19 tools as the stdio server using Streamable HTTP transport.
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
 
@@ -23,6 +23,10 @@ import {
23
23
  endSession,
24
24
  suggestLocks,
25
25
  detectDrift,
26
+ listTemplates,
27
+ applyTemplate,
28
+ generateReport,
29
+ auditStagedFiles,
26
30
  } from "../core/engine.js";
27
31
  import { generateContext, generateContextPack } from "../core/context.js";
28
32
  import {
@@ -41,7 +45,7 @@ import {
41
45
  } from "../core/git.js";
42
46
 
43
47
  const PROJECT_ROOT = process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
44
- const VERSION = "1.2.0";
48
+ const VERSION = "1.7.0";
45
49
  const AUTHOR = "Sandeep Roy";
46
50
 
47
51
  function createSpecLockServer() {
@@ -253,6 +257,41 @@ function createSpecLockServer() {
253
257
  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
258
  });
255
259
 
260
+ // Tool 20: speclock_apply_template
261
+ 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 }) => {
262
+ ensureInit(PROJECT_ROOT);
263
+ if (!name) {
264
+ const templates = listTemplates();
265
+ const text = templates.map(t => `- **${t.name}** (${t.displayName}): ${t.description} — ${t.lockCount} locks, ${t.decisionCount} decisions`).join("\n");
266
+ return { content: [{ type: "text", text: `## Available Templates\n\n${text}\n\nCall again with a name to apply.` }] };
267
+ }
268
+ const result = applyTemplate(PROJECT_ROOT, name);
269
+ if (!result.applied) return { content: [{ type: "text", text: result.error }], isError: true };
270
+ return { content: [{ type: "text", text: `Template "${result.displayName}" applied: ${result.locksAdded} lock(s) + ${result.decisionsAdded} decision(s).` }] };
271
+ });
272
+
273
+ // Tool 21: speclock_report
274
+ server.tool("speclock_report", "Violation report — how many times SpecLock blocked changes.", {}, async () => {
275
+ ensureInit(PROJECT_ROOT);
276
+ const report = generateReport(PROJECT_ROOT);
277
+ const parts = [`## Violation Report`, `Total blocked: **${report.totalViolations}**`];
278
+ if (report.mostTestedLocks.length > 0) {
279
+ parts.push("", "### Most Tested Locks");
280
+ for (const l of report.mostTestedLocks) parts.push(`- ${l.count}x — "${l.text}"`);
281
+ }
282
+ parts.push("", report.summary);
283
+ return { content: [{ type: "text", text: parts.join("\n") }] };
284
+ });
285
+
286
+ // Tool 22: speclock_audit
287
+ server.tool("speclock_audit", "Audit staged files against active locks.", {}, async () => {
288
+ ensureInit(PROJECT_ROOT);
289
+ const result = auditStagedFiles(PROJECT_ROOT);
290
+ if (result.passed) return { content: [{ type: "text", text: result.message }] };
291
+ const text = result.violations.map(v => `- [${v.severity}] **${v.file}** — ${v.reason}\n Lock: "${v.lockText}"`).join("\n");
292
+ return { content: [{ type: "text", text: `## Audit Failed\n\n${text}\n\n${result.message}` }] };
293
+ });
294
+
256
295
  return server;
257
296
  }
258
297
 
@@ -292,7 +331,7 @@ app.get("/", (req, res) => {
292
331
  version: VERSION,
293
332
  author: AUTHOR,
294
333
  description: "AI Continuity Engine — Kill AI amnesia",
295
- tools: 19,
334
+ tools: 22,
296
335
  mcp_endpoint: "/mcp",
297
336
  npm: "https://www.npmjs.com/package/speclock",
298
337
  github: "https://github.com/sgroy10/speclock",
package/src/mcp/server.js CHANGED
@@ -18,6 +18,10 @@ import {
18
18
  detectDrift,
19
19
  syncLocksToPackageJson,
20
20
  autoGuardRelatedFiles,
21
+ listTemplates,
22
+ applyTemplate,
23
+ generateReport,
24
+ auditStagedFiles,
21
25
  } from "../core/engine.js";
22
26
  import { generateContext, generateContextPack } from "../core/context.js";
23
27
  import {
@@ -52,7 +56,7 @@ const PROJECT_ROOT =
52
56
  args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
53
57
 
54
58
  // --- MCP Server ---
55
- const VERSION = "1.2.0";
59
+ const VERSION = "1.7.0";
56
60
  const AUTHOR = "Sandeep Roy";
57
61
 
58
62
  const server = new McpServer(
@@ -766,6 +770,118 @@ server.tool(
766
770
  }
767
771
  );
768
772
 
773
+ // ========================================
774
+ // TEMPLATE, REPORT & AUDIT TOOLS (v1.7.0)
775
+ // ========================================
776
+
777
+ // Tool 20: speclock_apply_template
778
+ server.tool(
779
+ "speclock_apply_template",
780
+ "Apply a pre-built constraint template (e.g., nextjs, react, express, supabase, stripe, security-hardened). Templates add recommended locks and decisions for common frameworks.",
781
+ {
782
+ name: z
783
+ .string()
784
+ .optional()
785
+ .describe("Template name to apply. Omit to list available templates."),
786
+ },
787
+ async ({ name }) => {
788
+ if (!name) {
789
+ const templates = listTemplates();
790
+ const formatted = templates
791
+ .map((t) => `- **${t.name}** (${t.displayName}): ${t.description} — ${t.lockCount} locks, ${t.decisionCount} decisions`)
792
+ .join("\n");
793
+ return {
794
+ content: [
795
+ {
796
+ type: "text",
797
+ text: `## Available Templates\n\n${formatted}\n\nCall again with a name to apply.`,
798
+ },
799
+ ],
800
+ };
801
+ }
802
+ const result = applyTemplate(PROJECT_ROOT, name);
803
+ if (!result.applied) {
804
+ return {
805
+ content: [{ type: "text", text: result.error }],
806
+ isError: true,
807
+ };
808
+ }
809
+ return {
810
+ content: [
811
+ {
812
+ type: "text",
813
+ text: `Template "${result.displayName}" applied: ${result.locksAdded} lock(s) + ${result.decisionsAdded} decision(s) added.`,
814
+ },
815
+ ],
816
+ };
817
+ }
818
+ );
819
+
820
+ // Tool 21: speclock_report
821
+ server.tool(
822
+ "speclock_report",
823
+ "Get a violation report showing how many times SpecLock blocked constraint violations, which locks were tested most, and recent violations.",
824
+ {},
825
+ async () => {
826
+ const report = generateReport(PROJECT_ROOT);
827
+
828
+ const parts = [`## SpecLock Violation Report`, ``, `Total violations blocked: **${report.totalViolations}**`];
829
+
830
+ if (report.timeRange) {
831
+ parts.push(`Period: ${report.timeRange.from.substring(0, 10)} to ${report.timeRange.to.substring(0, 10)}`);
832
+ }
833
+
834
+ if (report.mostTestedLocks.length > 0) {
835
+ parts.push("", "### Most Tested Locks");
836
+ for (const lock of report.mostTestedLocks) {
837
+ parts.push(`- ${lock.count}x — "${lock.text}"`);
838
+ }
839
+ }
840
+
841
+ if (report.recentViolations.length > 0) {
842
+ parts.push("", "### Recent Violations");
843
+ for (const v of report.recentViolations) {
844
+ parts.push(`- [${v.at.substring(0, 19)}] ${v.topLevel} (${v.topConfidence}%) — "${v.action}"`);
845
+ }
846
+ }
847
+
848
+ parts.push("", `---`, report.summary);
849
+
850
+ return {
851
+ content: [{ type: "text", text: parts.join("\n") }],
852
+ };
853
+ }
854
+ );
855
+
856
+ // Tool 22: speclock_audit
857
+ server.tool(
858
+ "speclock_audit",
859
+ "Audit git staged files against active SpecLock constraints. Returns pass/fail with details on which files violate locks. Used by the pre-commit hook.",
860
+ {},
861
+ async () => {
862
+ const result = auditStagedFiles(PROJECT_ROOT);
863
+
864
+ if (result.passed) {
865
+ return {
866
+ content: [{ type: "text", text: result.message }],
867
+ };
868
+ }
869
+
870
+ const formatted = result.violations
871
+ .map((v) => `- [${v.severity}] **${v.file}** — ${v.reason}\n Lock: "${v.lockText}"`)
872
+ .join("\n");
873
+
874
+ return {
875
+ content: [
876
+ {
877
+ type: "text",
878
+ text: `## Audit Failed\n\n${formatted}\n\n${result.message}`,
879
+ },
880
+ ],
881
+ };
882
+ }
883
+ );
884
+
769
885
  // --- Smithery sandbox export ---
770
886
  export default function createSandboxServer() {
771
887
  return server;