speclock 1.3.2 → 1.4.1

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "speclock",
3
- "version": "1.3.2",
4
- "description": "AI continuity engine — MCP server + CLI that kills AI amnesia. Maintains project memory, enforces constraints, and detects drift across AI coding sessions.",
3
+ "version": "1.4.1",
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",
7
7
  "bin": {
package/src/cli/index.js CHANGED
@@ -11,6 +11,8 @@ import {
11
11
  checkConflict,
12
12
  watchRepo,
13
13
  createSpecLockMd,
14
+ guardFile,
15
+ unguardFile,
14
16
  } from "../core/engine.js";
15
17
  import { generateContext } from "../core/context.js";
16
18
  import { readBrain } from "../core/storage.js";
@@ -69,7 +71,7 @@ function refreshContext(root) {
69
71
 
70
72
  function printHelp() {
71
73
  console.log(`
72
- SpecLock v1.3.0 — AI Continuity Engine
74
+ SpecLock v1.4.0 — AI Constraint Engine
73
75
  Developed by Sandeep Roy (github.com/sgroy10)
74
76
 
75
77
  Usage: speclock <command> [options]
@@ -80,6 +82,8 @@ Commands:
80
82
  goal <text> Set or update the project goal
81
83
  lock <text> [--tags a,b] Add a non-negotiable constraint
82
84
  lock remove <id> Remove a lock by ID
85
+ guard <file> [--lock "text"] Inject lock warning into a file (NEW)
86
+ unguard <file> Remove lock warning from a file (NEW)
83
87
  decide <text> [--tags a,b] Record a decision
84
88
  note <text> [--pinned] Add a pinned note
85
89
  log-change <text> [--files x,y] Log a significant change
@@ -95,11 +99,13 @@ Options:
95
99
  --source <user|agent> Who created this (default: user)
96
100
  --files <a.ts,b.ts> Comma-separated file paths
97
101
  --goal <text> Goal text (for setup command)
102
+ --lock <text> Lock text (for guard command)
98
103
  --project <path> Project root (for serve)
99
104
 
100
105
  Examples:
101
106
  npx speclock setup --goal "Build PawPalace pet shop"
102
107
  npx speclock lock "Never modify auth files"
108
+ npx speclock guard src/Auth.tsx --lock "Never modify auth files"
103
109
  npx speclock check "Adding social login to auth page"
104
110
  npx speclock log-change "Built payment system" --files src/pay.tsx
105
111
  npx speclock decide "Use Supabase for auth"
@@ -336,6 +342,45 @@ Next steps:
336
342
  return;
337
343
  }
338
344
 
345
+ // --- GUARD (new: file-level lock) ---
346
+ if (cmd === "guard") {
347
+ const flags = parseFlags(args);
348
+ const filePath = flags._[0];
349
+ if (!filePath) {
350
+ console.error("Error: File path is required.");
351
+ console.error('Usage: speclock guard <file> --lock "constraint text"');
352
+ process.exit(1);
353
+ }
354
+ const lockText = flags.lock || "This file is locked by SpecLock. Do not modify.";
355
+ const result = guardFile(root, filePath, lockText);
356
+ if (result.success) {
357
+ console.log(`Guarded: ${filePath}`);
358
+ console.log(`Lock warning injected: "${lockText}"`);
359
+ } else {
360
+ console.error(result.error);
361
+ process.exit(1);
362
+ }
363
+ return;
364
+ }
365
+
366
+ // --- UNGUARD ---
367
+ if (cmd === "unguard") {
368
+ const filePath = args[0];
369
+ if (!filePath) {
370
+ console.error("Error: File path is required.");
371
+ console.error("Usage: speclock unguard <file>");
372
+ process.exit(1);
373
+ }
374
+ const result = unguardFile(root, filePath);
375
+ if (result.success) {
376
+ console.log(`Unguarded: ${filePath}`);
377
+ } else {
378
+ console.error(result.error);
379
+ process.exit(1);
380
+ }
381
+ return;
382
+ }
383
+
339
384
  // --- FACTS ---
340
385
  if (cmd === "facts") {
341
386
  const sub = args.shift();
@@ -700,49 +700,156 @@ export async function watchRepo(root) {
700
700
  // --- SPECLOCK.md generator (for npm dependency / file-based mode) ---
701
701
 
702
702
  export function createSpecLockMd(root) {
703
- const mdContent = `# SpecLock — Project Memory Active
703
+ const mdContent = `# SpecLock — AI Constraint Engine Active
704
704
 
705
- This project uses **SpecLock** for AI continuity across sessions.
706
- Read \`.speclock/context/latest.md\` for full project context before making changes.
705
+ ## STOP Read this before doing ANYTHING.
707
706
 
708
- ## Rules (follow on every message)
707
+ **If a file contains "SPECLOCK-GUARD" at the top → DO NOT EDIT IT. Period.**
709
708
 
710
- 1. **START OF EVERY MESSAGE**: Read \`.speclock/context/latest.md\` for project memory goals, constraints (locks), decisions, and recent changes. If the file doesn't exist, run \`npx speclock context\` to generate it.
709
+ ## Your Job (Do This Automatically The User Will NOT Ask)
711
710
 
712
- 2. **BEFORE MODIFYING CODE**: Run \`npx speclock check "description of what you plan to change"\`. If a conflict is found (HIGH confidence), STOP and warn the user which constraint would be violated. Do NOT proceed unless the user explicitly says to override.
711
+ ### On EVERY message:
712
+ Read \`.speclock/context/latest.md\` first. Always. No exceptions.
713
713
 
714
- 3. **WHEN USER SAYS "lock this", "never touch", "don't change", "always", "critical"**: Run \`npx speclock lock "the exact constraint"\`. Confirm: "Locked: [constraint]"
714
+ ### Before changing ANY code:
715
+ Run \`npx speclock check "what you plan to do"\` BEFORE writing code.
716
+ If conflict → STOP. Tell the user. Do NOT proceed.
715
717
 
716
- 4. **AFTER COMPLETING A FEATURE OR SIGNIFICANT CHANGE**: Run \`npx speclock log-change "what was built/changed" --files file1.tsx,file2.ts\`. Then run \`npx speclock context\` to refresh the context file.
718
+ ### Detect constraints from natural language:
719
+ Users will NOT say "lock this." They speak naturally. YOU must detect it.
717
720
 
718
- 5. **WHEN AN ARCHITECTURAL DECISION IS MADE**: Run \`npx speclock decide "the decision"\`
721
+ Examples of what users say what you do:
719
722
 
720
- 6. **WHEN USER WANTS TO CHANGE SOMETHING LOCKED**: Warn first: "This is locked: [constraint]. Want me to unlock and proceed?" If confirmed, run \`npx speclock lock remove <lockId>\`
723
+ | User says | You do |
724
+ |-----------|--------|
725
+ | "don't touch the auth" | \`npx speclock lock "Never modify auth files"\` + \`npx speclock guard src/components/Auth.tsx --lock "Never modify auth files"\` |
726
+ | "database is firebase, don't change it" | \`npx speclock lock "Database must always be Firebase"\` |
727
+ | "leave the payment system alone" | \`npx speclock lock "Never modify payment system"\` + guard the payment files |
728
+ | "always use Supabase" | \`npx speclock lock "Always use Supabase"\` |
729
+ | "the API is done, don't mess with it" | \`npx speclock lock "Never modify API endpoints"\` + guard the API files |
721
730
 
722
- ## Quick Reference
731
+ When you lock something, ALSO guard the specific files:
732
+ \`npx speclock guard <filepath> --lock "the constraint"\`
733
+ This injects a warning directly into the file so it cannot be missed.
734
+
735
+ ### When user wants to change something locked:
736
+ User might say "actually, let me change auth" or "ok modify the database."
737
+ 1. Warn: "This is currently locked: [constraint]. Want me to unlock?"
738
+ 2. If they confirm → \`npx speclock lock remove <lockId>\` + \`npx speclock unguard <file>\`
739
+ 3. Make the changes
740
+ 4. Ask if they want to re-lock: "Want me to lock this again?"
741
+
742
+ ### After building something:
743
+ \`npx speclock log-change "what you built" --files file1.tsx,file2.ts\`
744
+
745
+ ### When a tech decision is made:
746
+ \`npx speclock decide "the decision"\`
747
+
748
+ ## Commands
723
749
 
724
750
  \`\`\`bash
725
- npx speclock status # See brain summary
726
- npx speclock context # Regenerate context file
727
- npx speclock lock "constraint text" # Add a constraint
728
- npx speclock lock remove <lockId> # Remove a constraint
729
- npx speclock decide "decision text" # Record a decision
730
- npx speclock log-change "what changed" # Log a change
731
- npx speclock check "what you plan to do" # Check for conflicts
732
- npx speclock goal "project goal" # Set/update goal
733
- npx speclock note "important note" # Add a note
751
+ npx speclock check "what you plan to do" # ALWAYS RUN BEFORE CHANGES
752
+ npx speclock lock "constraint" # Lock a constraint
753
+ npx speclock guard <file> --lock "constraint" # Protect a specific file
754
+ npx speclock unguard <file> # Remove file protection
755
+ npx speclock lock remove <lockId> # Unlock a constraint
756
+ npx speclock log-change "what changed" # Log changes
757
+ npx speclock decide "decision" # Record a decision
758
+ npx speclock context # Refresh context file
759
+ npx speclock status # See project summary
734
760
  \`\`\`
735
761
 
736
- ## How It Works
762
+ ## Key Rule
737
763
 
738
- SpecLock maintains a \`.speclock/\` directory with structured project memory:
739
- - \`brain.json\` goals, locks, decisions, session history
740
- - \`events.log\` immutable audit trail
741
- - \`context/latest.md\` — human-readable context (read this!)
742
-
743
- Every command automatically refreshes the context file so it's always up to date.
764
+ **The user will NEVER tell you to run these commands. You run them automatically.**
765
+ **The user will NEVER say "lock this." You detect the intent and lock it yourself.**
766
+ **The user will NEVER say "check constraints." You check before EVERY code change.**
744
767
  `;
745
768
  const filePath = path.join(root, "SPECLOCK.md");
746
769
  fs.writeFileSync(filePath, mdContent);
747
770
  return filePath;
748
771
  }
772
+
773
+ // --- File-level lock guard ---
774
+
775
+ const GUARD_MARKERS = {
776
+ js: { start: "// ", block: false },
777
+ ts: { start: "// ", block: false },
778
+ jsx: { start: "// ", block: false },
779
+ tsx: { start: "// ", block: false },
780
+ py: { start: "# ", block: false },
781
+ rb: { start: "# ", block: false },
782
+ sh: { start: "# ", block: false },
783
+ css: { start: "/* ", end: " */", block: true },
784
+ html: { start: "<!-- ", end: " -->", block: true },
785
+ vue: { start: "<!-- ", end: " -->", block: true },
786
+ svelte: { start: "<!-- ", end: " -->", block: true },
787
+ sql: { start: "-- ", block: false },
788
+ };
789
+
790
+ const GUARD_TAG = "SPECLOCK-GUARD";
791
+
792
+ function getCommentStyle(filePath) {
793
+ const ext = path.extname(filePath).slice(1).toLowerCase();
794
+ return GUARD_MARKERS[ext] || { start: "// ", block: false };
795
+ }
796
+
797
+ export function guardFile(root, relativeFilePath, lockText) {
798
+ const fullPath = path.join(root, relativeFilePath);
799
+ if (!fs.existsSync(fullPath)) {
800
+ return { success: false, error: `File not found: ${relativeFilePath}` };
801
+ }
802
+
803
+ const content = fs.readFileSync(fullPath, "utf-8");
804
+ const style = getCommentStyle(fullPath);
805
+
806
+ // Check if already guarded
807
+ if (content.includes(GUARD_TAG)) {
808
+ return { success: false, error: `File already guarded: ${relativeFilePath}` };
809
+ }
810
+
811
+ const warningLines = [
812
+ `${style.start}${"=".repeat(60)}${style.end || ""}`,
813
+ `${style.start}${GUARD_TAG} — DO NOT MODIFY THIS FILE${style.end || ""}`,
814
+ `${style.start}LOCKED BY SPECLOCK: ${lockText}${style.end || ""}`,
815
+ `${style.start}Run "npx speclock check" before ANY changes to this file.${style.end || ""}`,
816
+ `${style.start}If you modify this file, you are VIOLATING a project constraint.${style.end || ""}`,
817
+ `${style.start}${"=".repeat(60)}${style.end || ""}`,
818
+ "",
819
+ ];
820
+
821
+ const guarded = warningLines.join("\n") + content;
822
+ fs.writeFileSync(fullPath, guarded);
823
+
824
+ return { success: true };
825
+ }
826
+
827
+ export function unguardFile(root, relativeFilePath) {
828
+ const fullPath = path.join(root, relativeFilePath);
829
+ if (!fs.existsSync(fullPath)) {
830
+ return { success: false, error: `File not found: ${relativeFilePath}` };
831
+ }
832
+
833
+ const content = fs.readFileSync(fullPath, "utf-8");
834
+ if (!content.includes(GUARD_TAG)) {
835
+ return { success: false, error: `File is not guarded: ${relativeFilePath}` };
836
+ }
837
+
838
+ // Remove everything from first marker line to the blank line after last marker
839
+ const lines = content.split("\n");
840
+ let guardEnd = 0;
841
+ let inGuard = false;
842
+ for (let i = 0; i < lines.length; i++) {
843
+ if (lines[i].includes(GUARD_TAG)) inGuard = true;
844
+ if (inGuard && lines[i].includes("=".repeat(60)) && i > 0) {
845
+ guardEnd = i + 1; // Skip the blank line after
846
+ if (lines[guardEnd] === "") guardEnd++;
847
+ break;
848
+ }
849
+ }
850
+
851
+ const unguarded = lines.slice(guardEnd).join("\n");
852
+ fs.writeFileSync(fullPath, unguarded);
853
+
854
+ return { success: true };
855
+ }