speclock 1.3.2 → 1.4.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/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.0",
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,37 +700,45 @@ 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
+ ## MANDATORY: Read this BEFORE doing anything.
707
706
 
708
- ## Rules (follow on every message)
707
+ This project uses **SpecLock** for AI constraint enforcement.
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
+ **STOP. Before writing or modifying ANY code, you MUST:**
710
+ 1. Read \`.speclock/context/latest.md\` for project memory
711
+ 2. Run \`npx speclock check "what you plan to change"\`
712
+ 3. If conflict found → STOP and warn the user
711
713
 
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.
714
+ **If a file starts with "SPECLOCK-GUARD DO NOT MODIFY" DO NOT EDIT IT.**
713
715
 
714
- 3. **WHEN USER SAYS "lock this", "never touch", "don't change", "always", "critical"**: Run \`npx speclock lock "the exact constraint"\`. Confirm: "Locked: [constraint]"
716
+ ## Rules (MANDATORY every message, no exceptions)
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
+ 1. **FIRST ACTION OF EVERY MESSAGE**: Read \`.speclock/context/latest.md\`. This contains all project constraints (locks), decisions, and goals. Show: "Memory loaded — X locks, Y decisions." If the file doesn't exist, run \`npx speclock context\`.
719
+
720
+ 2. **BEFORE MODIFYING ANY CODE — EVERY TIME**: Run \`npx speclock check "description of what you plan to change"\`. If a conflict is found (HIGH confidence), STOP IMMEDIATELY. Tell the user which constraint would be violated. Do NOT proceed unless the user explicitly overrides. THIS IS THE MOST IMPORTANT RULE.
721
+
722
+ 3. **WHEN USER SAYS "lock this", "never touch", "don't change", "always", "critical"**: Run \`npx speclock lock "the exact constraint"\`. Then run \`npx speclock guard <file> --lock "constraint"\` on the specific files to inject a lock warning directly into the file. Confirm: "Locked: [constraint]"
723
+
724
+ 4. **AFTER COMPLETING A FEATURE OR CHANGE**: Run \`npx speclock log-change "what was built" --files file1.tsx,file2.ts\`.
717
725
 
718
726
  5. **WHEN AN ARCHITECTURAL DECISION IS MADE**: Run \`npx speclock decide "the decision"\`
719
727
 
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>\`
728
+ 6. **WHEN USER WANTS TO CHANGE SOMETHING LOCKED**: Warn first. If confirmed, run \`npx speclock lock remove <lockId>\` and \`npx speclock unguard <file>\` to remove the file guard.
721
729
 
722
730
  ## Quick Reference
723
731
 
724
732
  \`\`\`bash
725
- npx speclock status # See brain summary
726
- npx speclock context # Regenerate context file
733
+ npx speclock check "what you plan to do" # CHECK BEFORE CHANGES
727
734
  npx speclock lock "constraint text" # Add a constraint
735
+ npx speclock guard src/Auth.tsx --lock "text" # Inject lock into file
736
+ npx speclock unguard src/Auth.tsx # Remove file lock
728
737
  npx speclock lock remove <lockId> # Remove a constraint
729
- npx speclock decide "decision text" # Record a decision
730
738
  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
739
+ npx speclock decide "decision text" # Record a decision
740
+ npx speclock context # Regenerate context file
741
+ npx speclock status # See brain summary
734
742
  \`\`\`
735
743
 
736
744
  ## How It Works
@@ -740,9 +748,94 @@ SpecLock maintains a \`.speclock/\` directory with structured project memory:
740
748
  - \`events.log\` — immutable audit trail
741
749
  - \`context/latest.md\` — human-readable context (read this!)
742
750
 
743
- Every command automatically refreshes the context file so it's always up to date.
751
+ **Guarded files** have a lock warning header injected directly into the source code.
752
+ When you see "SPECLOCK-GUARD" at the top of a file, that file is LOCKED.
744
753
  `;
745
754
  const filePath = path.join(root, "SPECLOCK.md");
746
755
  fs.writeFileSync(filePath, mdContent);
747
756
  return filePath;
748
757
  }
758
+
759
+ // --- File-level lock guard ---
760
+
761
+ const GUARD_MARKERS = {
762
+ js: { start: "// ", block: false },
763
+ ts: { start: "// ", block: false },
764
+ jsx: { start: "// ", block: false },
765
+ tsx: { start: "// ", block: false },
766
+ py: { start: "# ", block: false },
767
+ rb: { start: "# ", block: false },
768
+ sh: { start: "# ", block: false },
769
+ css: { start: "/* ", end: " */", block: true },
770
+ html: { start: "<!-- ", end: " -->", block: true },
771
+ vue: { start: "<!-- ", end: " -->", block: true },
772
+ svelte: { start: "<!-- ", end: " -->", block: true },
773
+ sql: { start: "-- ", block: false },
774
+ };
775
+
776
+ const GUARD_TAG = "SPECLOCK-GUARD";
777
+
778
+ function getCommentStyle(filePath) {
779
+ const ext = path.extname(filePath).slice(1).toLowerCase();
780
+ return GUARD_MARKERS[ext] || { start: "// ", block: false };
781
+ }
782
+
783
+ export function guardFile(root, relativeFilePath, lockText) {
784
+ const fullPath = path.join(root, relativeFilePath);
785
+ if (!fs.existsSync(fullPath)) {
786
+ return { success: false, error: `File not found: ${relativeFilePath}` };
787
+ }
788
+
789
+ const content = fs.readFileSync(fullPath, "utf-8");
790
+ const style = getCommentStyle(fullPath);
791
+
792
+ // Check if already guarded
793
+ if (content.includes(GUARD_TAG)) {
794
+ return { success: false, error: `File already guarded: ${relativeFilePath}` };
795
+ }
796
+
797
+ const warningLines = [
798
+ `${style.start}${"=".repeat(60)}${style.end || ""}`,
799
+ `${style.start}${GUARD_TAG} — DO NOT MODIFY THIS FILE${style.end || ""}`,
800
+ `${style.start}LOCKED BY SPECLOCK: ${lockText}${style.end || ""}`,
801
+ `${style.start}Run "npx speclock check" before ANY changes to this file.${style.end || ""}`,
802
+ `${style.start}If you modify this file, you are VIOLATING a project constraint.${style.end || ""}`,
803
+ `${style.start}${"=".repeat(60)}${style.end || ""}`,
804
+ "",
805
+ ];
806
+
807
+ const guarded = warningLines.join("\n") + content;
808
+ fs.writeFileSync(fullPath, guarded);
809
+
810
+ return { success: true };
811
+ }
812
+
813
+ export function unguardFile(root, relativeFilePath) {
814
+ const fullPath = path.join(root, relativeFilePath);
815
+ if (!fs.existsSync(fullPath)) {
816
+ return { success: false, error: `File not found: ${relativeFilePath}` };
817
+ }
818
+
819
+ const content = fs.readFileSync(fullPath, "utf-8");
820
+ if (!content.includes(GUARD_TAG)) {
821
+ return { success: false, error: `File is not guarded: ${relativeFilePath}` };
822
+ }
823
+
824
+ // Remove everything from first marker line to the blank line after last marker
825
+ const lines = content.split("\n");
826
+ let guardEnd = 0;
827
+ let inGuard = false;
828
+ for (let i = 0; i < lines.length; i++) {
829
+ if (lines[i].includes(GUARD_TAG)) inGuard = true;
830
+ if (inGuard && lines[i].includes("=".repeat(60)) && i > 0) {
831
+ guardEnd = i + 1; // Skip the blank line after
832
+ if (lines[guardEnd] === "") guardEnd++;
833
+ break;
834
+ }
835
+ }
836
+
837
+ const unguarded = lines.slice(guardEnd).join("\n");
838
+ fs.writeFileSync(fullPath, unguarded);
839
+
840
+ return { success: true };
841
+ }