speclock 1.5.0 → 1.6.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 +36 -5
- package/package.json +1 -1
- package/src/cli/index.js +25 -5
- package/src/core/engine.js +196 -52
- package/src/mcp/server.js +15 -1
package/README.md
CHANGED
|
@@ -218,6 +218,35 @@ 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
|
+
## Auto-Guard: Locks That Actually Work
|
|
222
|
+
|
|
223
|
+
When you add a lock, SpecLock **automatically finds and guards related files**:
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
speclock lock "Never modify auth files"
|
|
227
|
+
→ Auto-guarded 2 related file(s):
|
|
228
|
+
🔒 src/components/Auth.tsx
|
|
229
|
+
🔒 src/contexts/AuthContext.tsx
|
|
230
|
+
|
|
231
|
+
speclock lock "Database must always be Supabase"
|
|
232
|
+
→ Auto-guarded 1 related file(s):
|
|
233
|
+
🔒 src/lib/supabase.ts
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The guard injects a warning **directly inside the file**. When the AI opens the file to edit it, it sees:
|
|
237
|
+
```
|
|
238
|
+
// ============================================================
|
|
239
|
+
// SPECLOCK-GUARD — DO NOT MODIFY THIS FILE
|
|
240
|
+
// LOCKED: Never modify auth files
|
|
241
|
+
// THIS FILE IS LOCKED. DO NOT EDIT, CHANGE, OR REWRITE ANY PART OF IT.
|
|
242
|
+
// The user must say "unlock" before this file can be changed.
|
|
243
|
+
// A question is NOT permission. Asking about features is NOT permission.
|
|
244
|
+
// ONLY "unlock" or "remove the lock" is permission to edit this file.
|
|
245
|
+
// ============================================================
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Active locks are also embedded in `package.json` — so the AI sees your constraints every time it reads the project config.
|
|
249
|
+
|
|
221
250
|
## CLI Commands
|
|
222
251
|
|
|
223
252
|
```bash
|
|
@@ -226,18 +255,20 @@ speclock setup --goal "Build my app" # One-shot: init + rules + context
|
|
|
226
255
|
|
|
227
256
|
# Memory
|
|
228
257
|
speclock goal <text> # Set project goal
|
|
229
|
-
speclock lock <text> [--tags a,b] # Add
|
|
258
|
+
speclock lock <text> [--tags a,b] # Add constraint + auto-guard files
|
|
230
259
|
speclock lock remove <id> # Remove a lock
|
|
231
260
|
speclock decide <text> # Record a decision
|
|
232
261
|
speclock note <text> # Add a note
|
|
233
262
|
|
|
263
|
+
# Enforcement
|
|
264
|
+
speclock check <text> # Check for lock conflicts
|
|
265
|
+
speclock guard <file> --lock "text" # Manually guard a specific file
|
|
266
|
+
speclock unguard <file> # Remove guard from file
|
|
267
|
+
|
|
234
268
|
# Tracking
|
|
235
269
|
speclock log-change <text> --files x # Log a change
|
|
236
270
|
speclock context # Regenerate context file
|
|
237
271
|
|
|
238
|
-
# Enforcement
|
|
239
|
-
speclock check <text> # Check for lock conflicts
|
|
240
|
-
|
|
241
272
|
# Other
|
|
242
273
|
speclock status # Show brain summary
|
|
243
274
|
speclock serve [--project <path>] # Start MCP server
|
|
@@ -287,4 +318,4 @@ MIT License - see [LICENSE](LICENSE) file.
|
|
|
287
318
|
|
|
288
319
|
---
|
|
289
320
|
|
|
290
|
-
*SpecLock v1.
|
|
321
|
+
*SpecLock v1.6.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.
|
|
3
|
+
"version": "1.6.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,6 +14,8 @@ import {
|
|
|
14
14
|
guardFile,
|
|
15
15
|
unguardFile,
|
|
16
16
|
injectPackageJsonMarker,
|
|
17
|
+
syncLocksToPackageJson,
|
|
18
|
+
autoGuardRelatedFiles,
|
|
17
19
|
} from "../core/engine.js";
|
|
18
20
|
import { generateContext } from "../core/context.js";
|
|
19
21
|
import { readBrain } from "../core/storage.js";
|
|
@@ -72,7 +74,7 @@ function refreshContext(root) {
|
|
|
72
74
|
|
|
73
75
|
function printHelp() {
|
|
74
76
|
console.log(`
|
|
75
|
-
SpecLock v1.
|
|
77
|
+
SpecLock v1.6.0 — AI Constraint Engine
|
|
76
78
|
Developed by Sandeep Roy (github.com/sgroy10)
|
|
77
79
|
|
|
78
80
|
Usage: speclock <command> [options]
|
|
@@ -200,16 +202,16 @@ Files created/updated:
|
|
|
200
202
|
.speclock/brain.json — Project memory
|
|
201
203
|
.speclock/context/latest.md — Context for AI (read this)
|
|
202
204
|
SPECLOCK.md — AI rules (read this)
|
|
203
|
-
package.json —
|
|
205
|
+
package.json — Active locks embedded (AI auto-discovery)
|
|
204
206
|
|
|
205
207
|
Next steps:
|
|
206
|
-
The AI should read SPECLOCK.md for rules and
|
|
207
|
-
.speclock/context/latest.md for project context.
|
|
208
|
-
|
|
209
208
|
To add constraints: npx speclock lock "Never touch auth files"
|
|
210
209
|
To check conflicts: npx speclock check "Modifying auth page"
|
|
211
210
|
To log changes: npx speclock log-change "Built landing page"
|
|
212
211
|
To see status: npx speclock status
|
|
212
|
+
|
|
213
|
+
Tip: When starting a new chat, tell the AI:
|
|
214
|
+
"Check speclock status and read the project constraints before doing anything"
|
|
213
215
|
`);
|
|
214
216
|
return;
|
|
215
217
|
}
|
|
@@ -250,6 +252,8 @@ Next steps:
|
|
|
250
252
|
}
|
|
251
253
|
const result = removeLock(root, lockId);
|
|
252
254
|
if (result.removed) {
|
|
255
|
+
// Sync updated locks to package.json
|
|
256
|
+
syncLocksToPackageJson(root);
|
|
253
257
|
refreshContext(root);
|
|
254
258
|
console.log(`Lock removed: "${result.lockText}"`);
|
|
255
259
|
} else {
|
|
@@ -267,6 +271,22 @@ Next steps:
|
|
|
267
271
|
process.exit(1);
|
|
268
272
|
}
|
|
269
273
|
const { lockId } = addLock(root, text, parseTags(flags.tags), flags.source || "user");
|
|
274
|
+
|
|
275
|
+
// Auto-guard related files (Solution 1)
|
|
276
|
+
const guardResult = autoGuardRelatedFiles(root, text);
|
|
277
|
+
if (guardResult.guarded.length > 0) {
|
|
278
|
+
console.log(`Auto-guarded ${guardResult.guarded.length} related file(s):`);
|
|
279
|
+
for (const f of guardResult.guarded) {
|
|
280
|
+
console.log(` 🔒 ${f}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Sync locks to package.json (Solution 2)
|
|
285
|
+
const syncResult = syncLocksToPackageJson(root);
|
|
286
|
+
if (syncResult.success) {
|
|
287
|
+
console.log(`Synced ${syncResult.lockCount} lock(s) to package.json.`);
|
|
288
|
+
}
|
|
289
|
+
|
|
270
290
|
refreshContext(root);
|
|
271
291
|
console.log(`Locked (${lockId}): "${text}"`);
|
|
272
292
|
return;
|
package/src/core/engine.js
CHANGED
|
@@ -702,85 +702,91 @@ export async function watchRepo(root) {
|
|
|
702
702
|
export function createSpecLockMd(root) {
|
|
703
703
|
const mdContent = `# SpecLock — AI Constraint Engine Active
|
|
704
704
|
|
|
705
|
-
##
|
|
705
|
+
## CRITICAL: LOCKED FILES CANNOT BE MODIFIED
|
|
706
706
|
|
|
707
|
-
|
|
707
|
+
If a lock says "never modify auth files" then you CANNOT modify those files.
|
|
708
|
+
Not now. Not later. Not if the user asks a question. Not for any reason.
|
|
708
709
|
|
|
709
|
-
**
|
|
710
|
-
|
|
710
|
+
**The ONLY way to modify a locked file:** The user must explicitly say "unlock" or "remove the lock."
|
|
711
|
+
Asking a question is NOT permission. Talking about a feature is NOT permission.
|
|
712
|
+
ONLY the words "unlock", "remove the lock", or "I changed my mind about the lock" count.
|
|
711
713
|
|
|
712
|
-
**If a file contains "SPECLOCK-GUARD" at the top → DO NOT EDIT IT.
|
|
714
|
+
**If a file contains "SPECLOCK-GUARD" at the top → DO NOT EDIT IT. No exceptions.**
|
|
713
715
|
|
|
714
|
-
##
|
|
716
|
+
## On EVERY message — do this FIRST:
|
|
717
|
+
|
|
718
|
+
1. Read \`.speclock/context/latest.md\` for project memory
|
|
719
|
+
2. Run \`npx speclock check "what you plan to do"\` BEFORE any code changes
|
|
720
|
+
3. If conflict found → STOP. Tell user. Do NOT proceed. Do NOT ask "should I proceed?"
|
|
721
|
+
4. Wait for user to explicitly say "unlock" or "remove the lock" before touching locked files
|
|
715
722
|
|
|
716
|
-
|
|
717
|
-
1. SpecLock is ALREADY installed — do NOT reinstall it
|
|
718
|
-
2. The \`.speclock/\` directory contains all project memory
|
|
719
|
-
3. Run \`npx speclock status\` to see the current state
|
|
720
|
-
4. Read \`.speclock/context/latest.md\` for full context (goals, locks, decisions, history)
|
|
721
|
-
5. Respect ALL existing locks and constraints before making any changes
|
|
723
|
+
## Returning to This Project? (New Chat Session)
|
|
722
724
|
|
|
723
|
-
|
|
725
|
+
SpecLock is ALREADY installed. Do NOT reinstall it. Do NOT say "it's not installed."
|
|
726
|
+
1. Run \`npx speclock status\` to see current locks and decisions
|
|
727
|
+
2. Read \`.speclock/context/latest.md\` for full project context
|
|
728
|
+
3. Respect ALL existing locks — they carry over between sessions
|
|
724
729
|
|
|
725
|
-
##
|
|
730
|
+
## What Counts as Permission to Bypass a Lock?
|
|
726
731
|
|
|
727
|
-
|
|
728
|
-
|
|
732
|
+
| User says | Is this permission? | What you do |
|
|
733
|
+
|-----------|-------------------|-------------|
|
|
734
|
+
| "add google auth" | NO — lock still active | Say "Auth files are locked. Want me to unlock them first?" |
|
|
735
|
+
| "is speclock initialized?" | NO — just a question | Answer the question. Do NOT change any code. |
|
|
736
|
+
| "yeah go ahead" (after you warned about lock) | NO — too vague | Say "To modify locked files, please confirm: unlock the auth files" |
|
|
737
|
+
| "ok unlock the auth files" | YES — explicit unlock | Run \`npx speclock lock remove <lockId>\`, then make changes |
|
|
738
|
+
| "remove the lock on auth" | YES — explicit unlock | Run \`npx speclock lock remove <lockId>\`, then make changes |
|
|
739
|
+
| "I changed my mind, you can edit auth" | YES — explicit unlock | Run \`npx speclock lock remove <lockId>\`, then make changes |
|
|
729
740
|
|
|
730
|
-
|
|
731
|
-
Run \`npx speclock check "what you plan to do"\` BEFORE writing code.
|
|
732
|
-
If conflict → STOP. Tell the user. Do NOT proceed.
|
|
741
|
+
**When in doubt: DO NOT modify locked files. Ask for clarification instead.**
|
|
733
742
|
|
|
734
|
-
|
|
735
|
-
Users will NOT say "lock this." They speak naturally. YOU must detect it.
|
|
743
|
+
## Detect Constraints from Natural Language
|
|
736
744
|
|
|
737
|
-
|
|
745
|
+
Users will NOT say "lock this." They speak naturally. YOU must detect intent and lock automatically.
|
|
738
746
|
|
|
739
747
|
| User says | You do |
|
|
740
748
|
|-----------|--------|
|
|
741
|
-
| "don't touch the auth" | \`npx speclock lock "Never modify auth files"\` + \`npx speclock guard
|
|
749
|
+
| "don't touch the auth" | \`npx speclock lock "Never modify auth files"\` + \`npx speclock guard\` the auth files |
|
|
742
750
|
| "database is firebase, don't change it" | \`npx speclock lock "Database must always be Firebase"\` |
|
|
743
|
-
| "leave the payment system alone" | \`npx speclock lock "Never modify payment system"\` + guard
|
|
751
|
+
| "leave the payment system alone" | \`npx speclock lock "Never modify payment system"\` + guard payment files |
|
|
744
752
|
| "always use Supabase" | \`npx speclock lock "Always use Supabase"\` |
|
|
745
|
-
| "the API is done, don't mess with it" | \`npx speclock lock "Never modify API endpoints"\` + guard
|
|
753
|
+
| "the API is done, don't mess with it" | \`npx speclock lock "Never modify API endpoints"\` + guard API files |
|
|
746
754
|
|
|
747
755
|
When you lock something, ALSO guard the specific files:
|
|
748
756
|
\`npx speclock guard <filepath> --lock "the constraint"\`
|
|
749
|
-
This injects a warning directly into the file so it cannot be missed.
|
|
750
757
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
1.
|
|
754
|
-
2.
|
|
758
|
+
## Unlocking (When User Gives Explicit Permission)
|
|
759
|
+
|
|
760
|
+
1. User says "unlock auth" or "remove the auth lock" or "I changed my mind about auth"
|
|
761
|
+
2. Run \`npx speclock lock remove <lockId>\` + \`npx speclock unguard <file>\`
|
|
755
762
|
3. Make the changes
|
|
756
|
-
4. Ask
|
|
763
|
+
4. Ask: "Want me to re-lock this?"
|
|
757
764
|
|
|
758
|
-
|
|
759
|
-
\`npx speclock log-change "what you built" --files file1.tsx,file2.ts\`
|
|
765
|
+
## After Building Something
|
|
760
766
|
|
|
761
|
-
|
|
762
|
-
\`npx speclock decide "the decision"\`
|
|
767
|
+
\`npx speclock log-change "what you built" --files file1.tsx,file2.ts\`
|
|
763
768
|
|
|
764
769
|
## Commands
|
|
765
770
|
|
|
766
771
|
\`\`\`bash
|
|
767
|
-
npx speclock status #
|
|
772
|
+
npx speclock status # Check current state
|
|
768
773
|
npx speclock check "what you plan to do" # ALWAYS RUN BEFORE CHANGES
|
|
769
774
|
npx speclock lock "constraint" # Lock a constraint
|
|
770
775
|
npx speclock guard <file> --lock "constraint" # Protect a specific file
|
|
771
776
|
npx speclock unguard <file> # Remove file protection
|
|
772
|
-
npx speclock lock remove <lockId> # Unlock
|
|
777
|
+
npx speclock lock remove <lockId> # Unlock (only after explicit permission)
|
|
773
778
|
npx speclock log-change "what changed" # Log changes
|
|
774
779
|
npx speclock decide "decision" # Record a decision
|
|
775
780
|
npx speclock context # Refresh context file
|
|
776
781
|
\`\`\`
|
|
777
782
|
|
|
778
|
-
## Key
|
|
783
|
+
## Key Rules
|
|
779
784
|
|
|
780
|
-
**
|
|
781
|
-
**
|
|
782
|
-
**
|
|
783
|
-
**
|
|
785
|
+
1. **A question is NOT permission to bypass a lock.**
|
|
786
|
+
2. **Run speclock check before EVERY code change, EVERY time, EVERY message.**
|
|
787
|
+
3. **If you already warned about a conflict, the lock is STILL active until explicitly removed.**
|
|
788
|
+
4. **The user will NEVER tell you to run these commands. You run them automatically.**
|
|
789
|
+
5. **If this file exists, SpecLock is installed. Do NOT reinstall.**
|
|
784
790
|
`;
|
|
785
791
|
const filePath = path.join(root, "SPECLOCK.md");
|
|
786
792
|
fs.writeFileSync(filePath, mdContent);
|
|
@@ -828,9 +834,11 @@ export function guardFile(root, relativeFilePath, lockText) {
|
|
|
828
834
|
const warningLines = [
|
|
829
835
|
`${style.start}${"=".repeat(60)}${style.end || ""}`,
|
|
830
836
|
`${style.start}${GUARD_TAG} — DO NOT MODIFY THIS FILE${style.end || ""}`,
|
|
831
|
-
`${style.start}LOCKED
|
|
832
|
-
`${style.start}
|
|
833
|
-
`${style.start}
|
|
837
|
+
`${style.start}LOCKED: ${lockText}${style.end || ""}`,
|
|
838
|
+
`${style.start}THIS FILE IS LOCKED. DO NOT EDIT, CHANGE, OR REWRITE ANY PART OF IT.${style.end || ""}`,
|
|
839
|
+
`${style.start}The user must say "unlock" before this file can be changed.${style.end || ""}`,
|
|
840
|
+
`${style.start}A question is NOT permission. Asking about features is NOT permission.${style.end || ""}`,
|
|
841
|
+
`${style.start}ONLY "unlock" or "remove the lock" is permission to edit this file.${style.end || ""}`,
|
|
834
842
|
`${style.start}${"=".repeat(60)}${style.end || ""}`,
|
|
835
843
|
"",
|
|
836
844
|
];
|
|
@@ -841,9 +849,9 @@ export function guardFile(root, relativeFilePath, lockText) {
|
|
|
841
849
|
return { success: true };
|
|
842
850
|
}
|
|
843
851
|
|
|
844
|
-
// --- Package.json
|
|
852
|
+
// --- Package.json lock sync (Solution 2: embed active locks directly in package.json) ---
|
|
845
853
|
|
|
846
|
-
export function
|
|
854
|
+
export function syncLocksToPackageJson(root) {
|
|
847
855
|
const pkgPath = path.join(root, "package.json");
|
|
848
856
|
if (!fs.existsSync(pkgPath)) {
|
|
849
857
|
return { success: false, error: "No package.json found in project root." };
|
|
@@ -852,21 +860,157 @@ export function injectPackageJsonMarker(root) {
|
|
|
852
860
|
try {
|
|
853
861
|
const raw = fs.readFileSync(pkgPath, "utf-8");
|
|
854
862
|
const pkg = JSON.parse(raw);
|
|
863
|
+
const brain = readBrain(root);
|
|
864
|
+
if (!brain) {
|
|
865
|
+
return { success: false, error: "SpecLock not initialized." };
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const activeLocks = brain.specLock.items
|
|
869
|
+
.filter((l) => l.active !== false)
|
|
870
|
+
.map((l) => l.text);
|
|
855
871
|
|
|
856
|
-
// Add speclock marker
|
|
857
872
|
pkg.speclock = {
|
|
858
873
|
active: true,
|
|
859
|
-
|
|
874
|
+
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.",
|
|
875
|
+
locks: activeLocks,
|
|
876
|
+
context: ".speclock/context/latest.md",
|
|
877
|
+
rules: "SPECLOCK.md",
|
|
860
878
|
};
|
|
861
879
|
|
|
862
|
-
// Write back with same formatting
|
|
863
880
|
const indent = raw.match(/^(\s+)/m)?.[1] || " ";
|
|
864
881
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, indent) + "\n");
|
|
865
882
|
|
|
866
|
-
return { success: true };
|
|
883
|
+
return { success: true, lockCount: activeLocks.length };
|
|
867
884
|
} catch (err) {
|
|
868
|
-
return { success: false, error: `Failed to
|
|
885
|
+
return { success: false, error: `Failed to sync locks to package.json: ${err.message}` };
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Backward-compatible alias
|
|
890
|
+
export function injectPackageJsonMarker(root) {
|
|
891
|
+
return syncLocksToPackageJson(root);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// --- Auto-guard related files (Solution 1: scan project and guard files matching lock keywords) ---
|
|
895
|
+
|
|
896
|
+
const FILE_KEYWORD_PATTERNS = [
|
|
897
|
+
{ keywords: ["auth", "authentication", "login", "signup", "signin", "sign-in", "sign-up"], patterns: ["**/Auth*", "**/auth*", "**/Login*", "**/login*", "**/SignUp*", "**/signup*", "**/SignIn*", "**/signin*", "**/*Auth*", "**/*auth*"] },
|
|
898
|
+
{ keywords: ["database", "db", "supabase", "firebase", "mongo", "postgres", "sql", "prisma"], patterns: ["**/supabase*", "**/firebase*", "**/database*", "**/db.*", "**/db/**", "**/prisma/**", "**/*Client*", "**/*client*"] },
|
|
899
|
+
{ keywords: ["payment", "pay", "stripe", "billing", "checkout", "subscription"], patterns: ["**/payment*", "**/Payment*", "**/pay*", "**/Pay*", "**/stripe*", "**/Stripe*", "**/billing*", "**/Billing*", "**/checkout*", "**/Checkout*"] },
|
|
900
|
+
{ keywords: ["api", "endpoint", "route", "routes"], patterns: ["**/api/**", "**/routes/**", "**/endpoints/**"] },
|
|
901
|
+
{ keywords: ["config", "configuration", "settings", "env"], patterns: ["**/config*", "**/Config*", "**/settings*", "**/Settings*"] },
|
|
902
|
+
];
|
|
903
|
+
|
|
904
|
+
function findRelatedFiles(root, lockText) {
|
|
905
|
+
const lockLower = lockText.toLowerCase();
|
|
906
|
+
const matchedFiles = [];
|
|
907
|
+
|
|
908
|
+
// Find which keyword patterns match this lock text
|
|
909
|
+
const matchingPatterns = [];
|
|
910
|
+
for (const group of FILE_KEYWORD_PATTERNS) {
|
|
911
|
+
const hasMatch = group.keywords.some((kw) => lockLower.includes(kw));
|
|
912
|
+
if (hasMatch) {
|
|
913
|
+
matchingPatterns.push(...group.patterns);
|
|
914
|
+
}
|
|
869
915
|
}
|
|
916
|
+
|
|
917
|
+
if (matchingPatterns.length === 0) return matchedFiles;
|
|
918
|
+
|
|
919
|
+
// Scan the src/ directory (and common directories) for matching files
|
|
920
|
+
const searchDirs = ["src", "app", "components", "pages", "lib", "utils", "contexts", "hooks", "services"];
|
|
921
|
+
|
|
922
|
+
for (const dir of searchDirs) {
|
|
923
|
+
const dirPath = path.join(root, dir);
|
|
924
|
+
if (!fs.existsSync(dirPath)) continue;
|
|
925
|
+
scanDirForMatches(root, dirPath, matchingPatterns, matchedFiles);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Also check root-level files
|
|
929
|
+
try {
|
|
930
|
+
const rootFiles = fs.readdirSync(root);
|
|
931
|
+
for (const file of rootFiles) {
|
|
932
|
+
const fullPath = path.join(root, file);
|
|
933
|
+
if (!fs.statSync(fullPath).isFile()) continue;
|
|
934
|
+
const ext = path.extname(file).slice(1).toLowerCase();
|
|
935
|
+
if (!GUARD_MARKERS[ext]) continue;
|
|
936
|
+
|
|
937
|
+
for (const pattern of matchingPatterns) {
|
|
938
|
+
const simpleMatch = patternMatchesFile(pattern, file);
|
|
939
|
+
if (simpleMatch) {
|
|
940
|
+
const rel = path.relative(root, fullPath).replace(/\\/g, "/");
|
|
941
|
+
if (!matchedFiles.includes(rel)) matchedFiles.push(rel);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
} catch (_) {}
|
|
946
|
+
|
|
947
|
+
return matchedFiles;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function scanDirForMatches(root, dirPath, patterns, results) {
|
|
951
|
+
try {
|
|
952
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
953
|
+
for (const entry of entries) {
|
|
954
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
955
|
+
if (entry.isDirectory()) {
|
|
956
|
+
if (entry.name === "node_modules" || entry.name === ".speclock" || entry.name === ".git") continue;
|
|
957
|
+
scanDirForMatches(root, fullPath, patterns, results);
|
|
958
|
+
} else if (entry.isFile()) {
|
|
959
|
+
const ext = path.extname(entry.name).slice(1).toLowerCase();
|
|
960
|
+
if (!GUARD_MARKERS[ext]) continue;
|
|
961
|
+
const relPath = path.relative(root, fullPath).replace(/\\/g, "/");
|
|
962
|
+
for (const pattern of patterns) {
|
|
963
|
+
if (patternMatchesFile(pattern, relPath) || patternMatchesFile(pattern, entry.name)) {
|
|
964
|
+
if (!results.includes(relPath)) results.push(relPath);
|
|
965
|
+
break;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
} catch (_) {}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function patternMatchesFile(pattern, filePath) {
|
|
974
|
+
// Simple glob matching: convert glob to regex
|
|
975
|
+
// Handle ** (any path), * (any chars in segment)
|
|
976
|
+
const clean = pattern.replace(/\\/g, "/");
|
|
977
|
+
const fileLower = filePath.toLowerCase();
|
|
978
|
+
const patternLower = clean.toLowerCase();
|
|
979
|
+
|
|
980
|
+
// Strip leading **/ for simple name matching
|
|
981
|
+
const namePattern = patternLower.replace(/^\*\*\//, "");
|
|
982
|
+
|
|
983
|
+
// Check if pattern is just a name pattern (no path separators)
|
|
984
|
+
if (!namePattern.includes("/")) {
|
|
985
|
+
const fileName = fileLower.split("/").pop();
|
|
986
|
+
// Convert glob * to regex .*
|
|
987
|
+
const regex = new RegExp("^" + namePattern.replace(/\*/g, ".*") + "$");
|
|
988
|
+
if (regex.test(fileName)) return true;
|
|
989
|
+
// Also check if the pattern appears anywhere in the filename
|
|
990
|
+
const corePattern = namePattern.replace(/\*/g, "");
|
|
991
|
+
if (corePattern.length > 2 && fileName.includes(corePattern)) return true;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// Full path match
|
|
995
|
+
const regex = new RegExp("^" + patternLower.replace(/\*\*\//g, "(.*/)?").replace(/\*/g, "[^/]*") + "$");
|
|
996
|
+
return regex.test(fileLower);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
export function autoGuardRelatedFiles(root, lockText) {
|
|
1000
|
+
const relatedFiles = findRelatedFiles(root, lockText);
|
|
1001
|
+
const guarded = [];
|
|
1002
|
+
const skipped = [];
|
|
1003
|
+
|
|
1004
|
+
for (const relFile of relatedFiles) {
|
|
1005
|
+
const result = guardFile(root, relFile, lockText);
|
|
1006
|
+
if (result.success) {
|
|
1007
|
+
guarded.push(relFile);
|
|
1008
|
+
} else {
|
|
1009
|
+
skipped.push({ file: relFile, reason: result.error });
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
return { guarded, skipped, scannedPatterns: relatedFiles.length };
|
|
870
1014
|
}
|
|
871
1015
|
|
|
872
1016
|
export function unguardFile(root, relativeFilePath) {
|
package/src/mcp/server.js
CHANGED
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
endSession,
|
|
17
17
|
suggestLocks,
|
|
18
18
|
detectDrift,
|
|
19
|
+
syncLocksToPackageJson,
|
|
20
|
+
autoGuardRelatedFiles,
|
|
19
21
|
} from "../core/engine.js";
|
|
20
22
|
import { generateContext, generateContextPack } from "../core/context.js";
|
|
21
23
|
import {
|
|
@@ -171,9 +173,19 @@ server.tool(
|
|
|
171
173
|
},
|
|
172
174
|
async ({ text, tags, source }) => {
|
|
173
175
|
const { lockId } = addLock(PROJECT_ROOT, text, tags, source);
|
|
176
|
+
|
|
177
|
+
// Auto-guard related files
|
|
178
|
+
const guardResult = autoGuardRelatedFiles(PROJECT_ROOT, text);
|
|
179
|
+
const guardMsg = guardResult.guarded.length > 0
|
|
180
|
+
? `\nAuto-guarded ${guardResult.guarded.length} file(s): ${guardResult.guarded.join(", ")}`
|
|
181
|
+
: "";
|
|
182
|
+
|
|
183
|
+
// Sync active locks to package.json
|
|
184
|
+
syncLocksToPackageJson(PROJECT_ROOT);
|
|
185
|
+
|
|
174
186
|
return {
|
|
175
187
|
content: [
|
|
176
|
-
{ type: "text", text: `Lock added (${lockId}): "${text}"` },
|
|
188
|
+
{ type: "text", text: `Lock added (${lockId}): "${text}"${guardMsg}` },
|
|
177
189
|
],
|
|
178
190
|
};
|
|
179
191
|
}
|
|
@@ -194,6 +206,8 @@ server.tool(
|
|
|
194
206
|
isError: true,
|
|
195
207
|
};
|
|
196
208
|
}
|
|
209
|
+
// Sync updated locks to package.json
|
|
210
|
+
syncLocksToPackageJson(PROJECT_ROOT);
|
|
197
211
|
return {
|
|
198
212
|
content: [
|
|
199
213
|
{
|