speclock 1.5.1 → 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,26 +218,76 @@ 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
+
228
+ ## Auto-Guard: Locks That Actually Work
229
+
230
+ When you add a lock, SpecLock **automatically finds and guards related files**:
231
+
232
+ ```
233
+ speclock lock "Never modify auth files"
234
+ → Auto-guarded 2 related file(s):
235
+ 🔒 src/components/Auth.tsx
236
+ 🔒 src/contexts/AuthContext.tsx
237
+
238
+ speclock lock "Database must always be Supabase"
239
+ → Auto-guarded 1 related file(s):
240
+ 🔒 src/lib/supabase.ts
241
+ ```
242
+
243
+ The guard injects a warning **directly inside the file**. When the AI opens the file to edit it, it sees:
244
+ ```
245
+ // ============================================================
246
+ // SPECLOCK-GUARD — DO NOT MODIFY THIS FILE
247
+ // LOCKED: Never modify auth files
248
+ // THIS FILE IS LOCKED. DO NOT EDIT, CHANGE, OR REWRITE ANY PART OF IT.
249
+ // The user must say "unlock" before this file can be changed.
250
+ // A question is NOT permission. Asking about features is NOT permission.
251
+ // ONLY "unlock" or "remove the lock" is permission to edit this file.
252
+ // ============================================================
253
+ ```
254
+
255
+ Active locks are also embedded in `package.json` — so the AI sees your constraints every time it reads the project config.
256
+
221
257
  ## CLI Commands
222
258
 
223
259
  ```bash
224
260
  # Setup
225
- speclock setup --goal "Build my app" # One-shot: init + rules + context
261
+ speclock setup --goal "Build my app" --template nextjs # One-shot setup + template
226
262
 
227
263
  # Memory
228
264
  speclock goal <text> # Set project goal
229
- speclock lock <text> [--tags a,b] # Add a constraint
265
+ speclock lock <text> [--tags a,b] # Add constraint + auto-guard files
230
266
  speclock lock remove <id> # Remove a lock
231
267
  speclock decide <text> # Record a decision
232
268
  speclock note <text> # Add a note
233
269
 
270
+ # Enforcement
271
+ speclock check <text> # Check for lock conflicts
272
+ speclock guard <file> --lock "text" # Manually guard a specific file
273
+ speclock unguard <file> # Remove guard from file
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
+
234
287
  # Tracking
235
288
  speclock log-change <text> --files x # Log a change
236
289
  speclock context # Regenerate context file
237
290
 
238
- # Enforcement
239
- speclock check <text> # Check for lock conflicts
240
-
241
291
  # Other
242
292
  speclock status # Show brain summary
243
293
  speclock serve [--project <path>] # Start MCP server
@@ -252,7 +302,7 @@ speclock watch # Start file watcher
252
302
  └──────────────┬──────────────────┬────────────────────┘
253
303
  │ │
254
304
  MCP Protocol File-Based (npm)
255
- (19 tool calls) (reads SPECLOCK.md +
305
+ (22 tool calls) (reads SPECLOCK.md +
256
306
  .speclock/context/latest.md,
257
307
  runs CLI commands)
258
308
  │ │
@@ -287,4 +337,4 @@ MIT License - see [LICENSE](LICENSE) file.
287
337
 
288
338
  ---
289
339
 
290
- *SpecLock v1.5.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.5.1",
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
@@ -14,9 +14,16 @@ import {
14
14
  guardFile,
15
15
  unguardFile,
16
16
  injectPackageJsonMarker,
17
+ syncLocksToPackageJson,
18
+ autoGuardRelatedFiles,
19
+ listTemplates,
20
+ applyTemplate,
21
+ generateReport,
22
+ auditStagedFiles,
17
23
  } from "../core/engine.js";
18
24
  import { generateContext } from "../core/context.js";
19
25
  import { readBrain } from "../core/storage.js";
26
+ import { installHook, removeHook } from "../core/hooks.js";
20
27
 
21
28
  // --- Argument parsing ---
22
29
 
@@ -72,23 +79,29 @@ function refreshContext(root) {
72
79
 
73
80
  function printHelp() {
74
81
  console.log(`
75
- SpecLock v1.5.0 — AI Constraint Engine
82
+ SpecLock v1.7.0 — AI Constraint Engine
76
83
  Developed by Sandeep Roy (github.com/sgroy10)
77
84
 
78
85
  Usage: speclock <command> [options]
79
86
 
80
87
  Commands:
81
- setup [--goal <text>] Full setup: init + SPECLOCK.md + context
88
+ setup [--goal <text>] [--template <name>] Full setup: init + SPECLOCK.md + context
82
89
  init Initialize SpecLock in current directory
83
90
  goal <text> Set or update the project goal
84
91
  lock <text> [--tags a,b] Add a non-negotiable constraint
85
92
  lock remove <id> Remove a lock by ID
86
- guard <file> [--lock "text"] Inject lock warning into a file (NEW)
87
- 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
88
95
  decide <text> [--tags a,b] Record a decision
89
96
  note <text> [--pinned] Add a pinned note
90
97
  log-change <text> [--files x,y] Log a significant change
91
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
92
105
  context Generate and print context pack
93
106
  facts deploy [--provider X] Set deployment facts
94
107
  watch Start file watcher (auto-track changes)
@@ -100,17 +113,20 @@ Options:
100
113
  --source <user|agent> Who created this (default: user)
101
114
  --files <a.ts,b.ts> Comma-separated file paths
102
115
  --goal <text> Goal text (for setup command)
116
+ --template <name> Template to apply during setup
103
117
  --lock <text> Lock text (for guard command)
104
118
  --project <path> Project root (for serve)
105
119
 
120
+ Templates: nextjs, react, express, supabase, stripe, security-hardened
121
+
106
122
  Examples:
107
- npx speclock setup --goal "Build PawPalace pet shop"
123
+ npx speclock setup --goal "Build PawPalace pet shop" --template nextjs
108
124
  npx speclock lock "Never modify auth files"
109
- npx speclock guard src/Auth.tsx --lock "Never modify auth files"
125
+ npx speclock template apply supabase
110
126
  npx speclock check "Adding social login to auth page"
111
- npx speclock log-change "Built payment system" --files src/pay.tsx
112
- npx speclock decide "Use Supabase for auth"
113
- npx speclock context
127
+ npx speclock report
128
+ npx speclock hook install
129
+ npx speclock audit
114
130
  npx speclock status
115
131
  `);
116
132
  }
@@ -188,11 +204,21 @@ async function main() {
188
204
  console.log("Injected SpecLock marker into package.json.");
189
205
  }
190
206
 
191
- // 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
192
218
  generateContext(root);
193
219
  console.log("Generated .speclock/context/latest.md");
194
220
 
195
- // 6. Print summary
221
+ // 7. Print summary
196
222
  console.log(`
197
223
  SpecLock is ready!
198
224
 
@@ -200,16 +226,16 @@ Files created/updated:
200
226
  .speclock/brain.json — Project memory
201
227
  .speclock/context/latest.md — Context for AI (read this)
202
228
  SPECLOCK.md — AI rules (read this)
203
- package.json — SpecLock marker added (AI auto-discovery)
229
+ package.json — Active locks embedded (AI auto-discovery)
204
230
 
205
231
  Next steps:
206
- The AI should read SPECLOCK.md for rules and
207
- .speclock/context/latest.md for project context.
208
-
209
232
  To add constraints: npx speclock lock "Never touch auth files"
210
233
  To check conflicts: npx speclock check "Modifying auth page"
211
234
  To log changes: npx speclock log-change "Built landing page"
212
235
  To see status: npx speclock status
236
+
237
+ Tip: When starting a new chat, tell the AI:
238
+ "Check speclock status and read the project constraints before doing anything"
213
239
  `);
214
240
  return;
215
241
  }
@@ -250,6 +276,8 @@ Next steps:
250
276
  }
251
277
  const result = removeLock(root, lockId);
252
278
  if (result.removed) {
279
+ // Sync updated locks to package.json
280
+ syncLocksToPackageJson(root);
253
281
  refreshContext(root);
254
282
  console.log(`Lock removed: "${result.lockText}"`);
255
283
  } else {
@@ -267,6 +295,22 @@ Next steps:
267
295
  process.exit(1);
268
296
  }
269
297
  const { lockId } = addLock(root, text, parseTags(flags.tags), flags.source || "user");
298
+
299
+ // Auto-guard related files (Solution 1)
300
+ const guardResult = autoGuardRelatedFiles(root, text);
301
+ if (guardResult.guarded.length > 0) {
302
+ console.log(`Auto-guarded ${guardResult.guarded.length} related file(s):`);
303
+ for (const f of guardResult.guarded) {
304
+ console.log(` 🔒 ${f}`);
305
+ }
306
+ }
307
+
308
+ // Sync locks to package.json (Solution 2)
309
+ const syncResult = syncLocksToPackageJson(root);
310
+ if (syncResult.success) {
311
+ console.log(`Synced ${syncResult.lockCount} lock(s) to package.json.`);
312
+ }
313
+
270
314
  refreshContext(root);
271
315
  console.log(`Locked (${lockId}): "${text}"`);
272
316
  return;
@@ -335,8 +379,8 @@ Next steps:
335
379
  console.log(`\nCONFLICT DETECTED`);
336
380
  console.log("=".repeat(50));
337
381
  for (const lock of result.conflictingLocks) {
338
- console.log(` [${lock.confidence}] "${lock.text}"`);
339
- console.log(` Confidence: ${lock.score}%`);
382
+ console.log(` [${lock.level}] "${lock.text}"`);
383
+ console.log(` Confidence: ${lock.confidence}%`);
340
384
  if (lock.reasons && lock.reasons.length > 0) {
341
385
  for (const reason of lock.reasons) {
342
386
  console.log(` - ${reason}`);
@@ -440,6 +484,119 @@ Next steps:
440
484
  return;
441
485
  }
442
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
+
443
600
  // --- STATUS ---
444
601
  if (cmd === "status") {
445
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
 
@@ -849,9 +868,9 @@ export function guardFile(root, relativeFilePath, lockText) {
849
868
  return { success: true };
850
869
  }
851
870
 
852
- // --- Package.json marker injection (makes Bolt.new auto-discover SpecLock) ---
871
+ // --- Package.json lock sync (Solution 2: embed active locks directly in package.json) ---
853
872
 
854
- export function injectPackageJsonMarker(root) {
873
+ export function syncLocksToPackageJson(root) {
855
874
  const pkgPath = path.join(root, "package.json");
856
875
  if (!fs.existsSync(pkgPath)) {
857
876
  return { success: false, error: "No package.json found in project root." };
@@ -860,23 +879,159 @@ export function injectPackageJsonMarker(root) {
860
879
  try {
861
880
  const raw = fs.readFileSync(pkgPath, "utf-8");
862
881
  const pkg = JSON.parse(raw);
882
+ const brain = readBrain(root);
883
+ if (!brain) {
884
+ return { success: false, error: "SpecLock not initialized." };
885
+ }
886
+
887
+ const activeLocks = brain.specLock.items
888
+ .filter((l) => l.active !== false)
889
+ .map((l) => l.text);
863
890
 
864
- // Add speclock marker
865
891
  pkg.speclock = {
866
892
  active: true,
867
- rules: "IMPORTANT: Read SPECLOCK.md before making ANY changes to this project. Read .speclock/context/latest.md for project memory, goals, locks, and decisions. Run 'npx speclock check' before modifying locked files.",
893
+ message: "STOP This project has SpecLock constraints. Read SPECLOCK.md and .speclock/context/latest.md BEFORE making ANY changes. Run 'npx speclock check' before ALL code changes. If a lock below is violated, STOP and ask user to unlock.",
894
+ locks: activeLocks,
895
+ context: ".speclock/context/latest.md",
896
+ rules: "SPECLOCK.md",
868
897
  };
869
898
 
870
- // Write back with same formatting
871
899
  const indent = raw.match(/^(\s+)/m)?.[1] || " ";
872
900
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, indent) + "\n");
873
901
 
874
- return { success: true };
902
+ return { success: true, lockCount: activeLocks.length };
875
903
  } catch (err) {
876
- return { success: false, error: `Failed to modify package.json: ${err.message}` };
904
+ return { success: false, error: `Failed to sync locks to package.json: ${err.message}` };
877
905
  }
878
906
  }
879
907
 
908
+ // Backward-compatible alias
909
+ export function injectPackageJsonMarker(root) {
910
+ return syncLocksToPackageJson(root);
911
+ }
912
+
913
+ // --- Auto-guard related files (Solution 1: scan project and guard files matching lock keywords) ---
914
+
915
+ const FILE_KEYWORD_PATTERNS = [
916
+ { keywords: ["auth", "authentication", "login", "signup", "signin", "sign-in", "sign-up"], patterns: ["**/Auth*", "**/auth*", "**/Login*", "**/login*", "**/SignUp*", "**/signup*", "**/SignIn*", "**/signin*", "**/*Auth*", "**/*auth*"] },
917
+ { keywords: ["database", "db", "supabase", "firebase", "mongo", "postgres", "sql", "prisma"], patterns: ["**/supabase*", "**/firebase*", "**/database*", "**/db.*", "**/db/**", "**/prisma/**", "**/*Client*", "**/*client*"] },
918
+ { keywords: ["payment", "pay", "stripe", "billing", "checkout", "subscription"], patterns: ["**/payment*", "**/Payment*", "**/pay*", "**/Pay*", "**/stripe*", "**/Stripe*", "**/billing*", "**/Billing*", "**/checkout*", "**/Checkout*"] },
919
+ { keywords: ["api", "endpoint", "route", "routes"], patterns: ["**/api/**", "**/routes/**", "**/endpoints/**"] },
920
+ { keywords: ["config", "configuration", "settings", "env"], patterns: ["**/config*", "**/Config*", "**/settings*", "**/Settings*"] },
921
+ ];
922
+
923
+ function findRelatedFiles(root, lockText) {
924
+ const lockLower = lockText.toLowerCase();
925
+ const matchedFiles = [];
926
+
927
+ // Find which keyword patterns match this lock text
928
+ const matchingPatterns = [];
929
+ for (const group of FILE_KEYWORD_PATTERNS) {
930
+ const hasMatch = group.keywords.some((kw) => lockLower.includes(kw));
931
+ if (hasMatch) {
932
+ matchingPatterns.push(...group.patterns);
933
+ }
934
+ }
935
+
936
+ if (matchingPatterns.length === 0) return matchedFiles;
937
+
938
+ // Scan the src/ directory (and common directories) for matching files
939
+ const searchDirs = ["src", "app", "components", "pages", "lib", "utils", "contexts", "hooks", "services"];
940
+
941
+ for (const dir of searchDirs) {
942
+ const dirPath = path.join(root, dir);
943
+ if (!fs.existsSync(dirPath)) continue;
944
+ scanDirForMatches(root, dirPath, matchingPatterns, matchedFiles);
945
+ }
946
+
947
+ // Also check root-level files
948
+ try {
949
+ const rootFiles = fs.readdirSync(root);
950
+ for (const file of rootFiles) {
951
+ const fullPath = path.join(root, file);
952
+ if (!fs.statSync(fullPath).isFile()) continue;
953
+ const ext = path.extname(file).slice(1).toLowerCase();
954
+ if (!GUARD_MARKERS[ext]) continue;
955
+
956
+ for (const pattern of matchingPatterns) {
957
+ const simpleMatch = patternMatchesFile(pattern, file);
958
+ if (simpleMatch) {
959
+ const rel = path.relative(root, fullPath).replace(/\\/g, "/");
960
+ if (!matchedFiles.includes(rel)) matchedFiles.push(rel);
961
+ }
962
+ }
963
+ }
964
+ } catch (_) {}
965
+
966
+ return matchedFiles;
967
+ }
968
+
969
+ function scanDirForMatches(root, dirPath, patterns, results) {
970
+ try {
971
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
972
+ for (const entry of entries) {
973
+ const fullPath = path.join(dirPath, entry.name);
974
+ if (entry.isDirectory()) {
975
+ if (entry.name === "node_modules" || entry.name === ".speclock" || entry.name === ".git") continue;
976
+ scanDirForMatches(root, fullPath, patterns, results);
977
+ } else if (entry.isFile()) {
978
+ const ext = path.extname(entry.name).slice(1).toLowerCase();
979
+ if (!GUARD_MARKERS[ext]) continue;
980
+ const relPath = path.relative(root, fullPath).replace(/\\/g, "/");
981
+ for (const pattern of patterns) {
982
+ if (patternMatchesFile(pattern, relPath) || patternMatchesFile(pattern, entry.name)) {
983
+ if (!results.includes(relPath)) results.push(relPath);
984
+ break;
985
+ }
986
+ }
987
+ }
988
+ }
989
+ } catch (_) {}
990
+ }
991
+
992
+ function patternMatchesFile(pattern, filePath) {
993
+ // Simple glob matching: convert glob to regex
994
+ // Handle ** (any path), * (any chars in segment)
995
+ const clean = pattern.replace(/\\/g, "/");
996
+ const fileLower = filePath.toLowerCase();
997
+ const patternLower = clean.toLowerCase();
998
+
999
+ // Strip leading **/ for simple name matching
1000
+ const namePattern = patternLower.replace(/^\*\*\//, "");
1001
+
1002
+ // Check if pattern is just a name pattern (no path separators)
1003
+ if (!namePattern.includes("/")) {
1004
+ const fileName = fileLower.split("/").pop();
1005
+ // Convert glob * to regex .*
1006
+ const regex = new RegExp("^" + namePattern.replace(/\*/g, ".*") + "$");
1007
+ if (regex.test(fileName)) return true;
1008
+ // Also check if the pattern appears anywhere in the filename
1009
+ const corePattern = namePattern.replace(/\*/g, "");
1010
+ if (corePattern.length > 2 && fileName.includes(corePattern)) return true;
1011
+ }
1012
+
1013
+ // Full path match
1014
+ const regex = new RegExp("^" + patternLower.replace(/\*\*\//g, "(.*/)?").replace(/\*/g, "[^/]*") + "$");
1015
+ return regex.test(fileLower);
1016
+ }
1017
+
1018
+ export function autoGuardRelatedFiles(root, lockText) {
1019
+ const relatedFiles = findRelatedFiles(root, lockText);
1020
+ const guarded = [];
1021
+ const skipped = [];
1022
+
1023
+ for (const relFile of relatedFiles) {
1024
+ const result = guardFile(root, relFile, lockText);
1025
+ if (result.success) {
1026
+ guarded.push(relFile);
1027
+ } else {
1028
+ skipped.push({ file: relFile, reason: result.error });
1029
+ }
1030
+ }
1031
+
1032
+ return { guarded, skipped, scannedPatterns: relatedFiles.length };
1033
+ }
1034
+
880
1035
  export function unguardFile(root, relativeFilePath) {
881
1036
  const fullPath = path.join(root, relativeFilePath);
882
1037
  if (!fs.existsSync(fullPath)) {
@@ -906,3 +1061,188 @@ export function unguardFile(root, relativeFilePath) {
906
1061
 
907
1062
  return { success: true };
908
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
@@ -16,6 +16,12 @@ import {
16
16
  endSession,
17
17
  suggestLocks,
18
18
  detectDrift,
19
+ syncLocksToPackageJson,
20
+ autoGuardRelatedFiles,
21
+ listTemplates,
22
+ applyTemplate,
23
+ generateReport,
24
+ auditStagedFiles,
19
25
  } from "../core/engine.js";
20
26
  import { generateContext, generateContextPack } from "../core/context.js";
21
27
  import {
@@ -50,7 +56,7 @@ const PROJECT_ROOT =
50
56
  args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
51
57
 
52
58
  // --- MCP Server ---
53
- const VERSION = "1.2.0";
59
+ const VERSION = "1.7.0";
54
60
  const AUTHOR = "Sandeep Roy";
55
61
 
56
62
  const server = new McpServer(
@@ -171,9 +177,19 @@ server.tool(
171
177
  },
172
178
  async ({ text, tags, source }) => {
173
179
  const { lockId } = addLock(PROJECT_ROOT, text, tags, source);
180
+
181
+ // Auto-guard related files
182
+ const guardResult = autoGuardRelatedFiles(PROJECT_ROOT, text);
183
+ const guardMsg = guardResult.guarded.length > 0
184
+ ? `\nAuto-guarded ${guardResult.guarded.length} file(s): ${guardResult.guarded.join(", ")}`
185
+ : "";
186
+
187
+ // Sync active locks to package.json
188
+ syncLocksToPackageJson(PROJECT_ROOT);
189
+
174
190
  return {
175
191
  content: [
176
- { type: "text", text: `Lock added (${lockId}): "${text}"` },
192
+ { type: "text", text: `Lock added (${lockId}): "${text}"${guardMsg}` },
177
193
  ],
178
194
  };
179
195
  }
@@ -194,6 +210,8 @@ server.tool(
194
210
  isError: true,
195
211
  };
196
212
  }
213
+ // Sync updated locks to package.json
214
+ syncLocksToPackageJson(PROJECT_ROOT);
197
215
  return {
198
216
  content: [
199
217
  {
@@ -752,6 +770,118 @@ server.tool(
752
770
  }
753
771
  );
754
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
+
755
885
  // --- Smithery sandbox export ---
756
886
  export default function createSandboxServer() {
757
887
  return server;