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 +2 -2
- package/src/cli/index.js +46 -1
- package/src/core/engine.js +109 -16
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "speclock",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "AI
|
|
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.
|
|
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,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 —
|
|
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
|
+
## MANDATORY: Read this BEFORE doing anything.
|
|
707
706
|
|
|
708
|
-
|
|
707
|
+
This project uses **SpecLock** for AI constraint enforcement.
|
|
709
708
|
|
|
710
|
-
|
|
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
|
-
|
|
714
|
+
**If a file starts with "SPECLOCK-GUARD — DO NOT MODIFY" → DO NOT EDIT IT.**
|
|
713
715
|
|
|
714
|
-
|
|
716
|
+
## Rules (MANDATORY — every message, no exceptions)
|
|
715
717
|
|
|
716
|
-
|
|
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
|
|
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
|
|
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
|
|
732
|
-
npx speclock
|
|
733
|
-
npx speclock
|
|
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
|
-
|
|
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
|
+
}
|