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 +2 -2
- package/src/cli/index.js +46 -1
- package/src/core/engine.js +134 -27
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "speclock",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "AI
|
|
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.
|
|
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();
|
package/src/core/engine.js
CHANGED
|
@@ -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 —
|
|
703
|
+
const mdContent = `# SpecLock — AI Constraint Engine Active
|
|
704
704
|
|
|
705
|
-
|
|
706
|
-
Read \`.speclock/context/latest.md\` for full project context before making changes.
|
|
705
|
+
## STOP — Read this before doing ANYTHING.
|
|
707
706
|
|
|
708
|
-
|
|
707
|
+
**If a file contains "SPECLOCK-GUARD" at the top → DO NOT EDIT IT. Period.**
|
|
709
708
|
|
|
710
|
-
|
|
709
|
+
## Your Job (Do This Automatically — The User Will NOT Ask)
|
|
711
710
|
|
|
712
|
-
|
|
711
|
+
### On EVERY message:
|
|
712
|
+
Read \`.speclock/context/latest.md\` first. Always. No exceptions.
|
|
713
713
|
|
|
714
|
-
|
|
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
|
-
|
|
718
|
+
### Detect constraints from natural language:
|
|
719
|
+
Users will NOT say "lock this." They speak naturally. YOU must detect it.
|
|
717
720
|
|
|
718
|
-
|
|
721
|
+
Examples of what users say → what you do:
|
|
719
722
|
|
|
720
|
-
|
|
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
|
-
|
|
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
|
|
726
|
-
npx speclock
|
|
727
|
-
npx speclock lock "constraint
|
|
728
|
-
npx speclock
|
|
729
|
-
npx speclock
|
|
730
|
-
npx speclock log-change "what changed" # Log
|
|
731
|
-
npx speclock
|
|
732
|
-
npx speclock
|
|
733
|
-
npx speclock
|
|
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
|
-
##
|
|
762
|
+
## Key Rule
|
|
737
763
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
+
}
|