speclock 5.5.3 → 5.5.4
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 +3 -3
- package/package.json +217 -1
- package/src/cli/index.js +390 -16
- package/src/core/auth.js +8 -0
- package/src/core/compliance.js +1 -1
- package/src/core/enforcer.js +7 -1
- package/src/core/guardian.js +78 -5
- package/src/core/lock-author.js +8 -0
- package/src/core/mcp-install.js +484 -0
- package/src/dashboard/index.html +2 -2
- package/src/mcp/http-server.js +2 -2
- package/src/mcp/server.js +1 -1
package/src/cli/index.js
CHANGED
|
@@ -67,7 +67,16 @@ import { getReplay, listSessions, formatReplay } from "../core/replay.js";
|
|
|
67
67
|
import { computeDriftScore, formatDriftScore } from "../core/drift-score.js";
|
|
68
68
|
import { computeCoverage, formatCoverage } from "../core/coverage.js";
|
|
69
69
|
import { analyzeLockStrength, formatStrength } from "../core/strengthen.js";
|
|
70
|
-
import { protect, formatProtectReport } from "../core/guardian.js";
|
|
70
|
+
import { protect, formatProtectReport, discoverRuleFiles, extractConstraints, RULE_FILES } from "../core/guardian.js";
|
|
71
|
+
import {
|
|
72
|
+
installForClient,
|
|
73
|
+
uninstallForClient,
|
|
74
|
+
installAll,
|
|
75
|
+
uninstallAll,
|
|
76
|
+
formatResult,
|
|
77
|
+
nextStepsFor,
|
|
78
|
+
SUPPORTED_CLIENTS,
|
|
79
|
+
} from "../core/mcp-install.js";
|
|
71
80
|
|
|
72
81
|
// --- Argument parsing ---
|
|
73
82
|
|
|
@@ -123,7 +132,7 @@ function refreshContext(root) {
|
|
|
123
132
|
|
|
124
133
|
function printHelp() {
|
|
125
134
|
console.log(`
|
|
126
|
-
SpecLock v5.5.
|
|
135
|
+
SpecLock v5.5.4 — Your AI has rules. SpecLock makes them unbreakable.
|
|
127
136
|
Developed by Sandeep Roy (github.com/sgroy10)
|
|
128
137
|
|
|
129
138
|
Usage: speclock <command> [options]
|
|
@@ -134,7 +143,12 @@ Commands:
|
|
|
134
143
|
goal <text> Set or update the project goal
|
|
135
144
|
lock <text> [--tags a,b] Add a non-negotiable constraint
|
|
136
145
|
lock remove <id> Remove a lock by ID
|
|
137
|
-
protect
|
|
146
|
+
protect [--strict] Zero-config: read rule files, extract locks, install hook
|
|
147
|
+
(default: warn mode — violations print but DON'T block commits.
|
|
148
|
+
Add --strict for hard blocks.)
|
|
149
|
+
mcp install <client> Auto-install SpecLock MCP server into an AI client
|
|
150
|
+
(claude-code, cursor, windsurf, cline, codex, all)
|
|
151
|
+
mcp uninstall <client> Remove SpecLock MCP server from an AI client
|
|
138
152
|
guard <file> [--lock "text"] Inject lock warning into a file
|
|
139
153
|
unguard <file> Remove lock warning from a file
|
|
140
154
|
decide <text> [--tags a,b] Record a decision
|
|
@@ -146,8 +160,10 @@ Commands:
|
|
|
146
160
|
report Show violation report + stats
|
|
147
161
|
hook install Install git pre-commit hook
|
|
148
162
|
hook remove Remove git pre-commit hook
|
|
149
|
-
audit
|
|
150
|
-
|
|
163
|
+
audit [--strict] Audit staged files against locks (warn mode default;
|
|
164
|
+
--strict or SPECLOCK_STRICT=1 exits 1 on violation)
|
|
165
|
+
audit-semantic [--strict] Semantic audit: analyze code changes vs locks
|
|
166
|
+
(warn mode default; use --strict for hard blocks)
|
|
151
167
|
audit-verify Verify HMAC audit chain integrity
|
|
152
168
|
enforce <advisory|hard> Set enforcement mode (advisory=warn, hard=block)
|
|
153
169
|
override <lockId> <reason> Override a lock with justification
|
|
@@ -168,6 +184,7 @@ Commands:
|
|
|
168
184
|
watch Start file watcher (live dashboard)
|
|
169
185
|
serve [--project <path>] Start MCP stdio server
|
|
170
186
|
status Show project brain summary
|
|
187
|
+
doctor Diagnostic health check (install, git, rules, MCP)
|
|
171
188
|
|
|
172
189
|
Options:
|
|
173
190
|
--tags <a,b,c> Comma-separated tags
|
|
@@ -550,12 +567,30 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
|
|
|
550
567
|
// --- PROTECT (zero-config guardian mode) ---
|
|
551
568
|
if (cmd === "protect") {
|
|
552
569
|
const flags = parseFlags(args);
|
|
570
|
+
const strict = flags.strict === true || flags.block === true;
|
|
553
571
|
const opts = {
|
|
554
572
|
skipHook: flags["no-hook"] === true,
|
|
555
573
|
skipSync: flags["no-sync"] === true,
|
|
574
|
+
strict,
|
|
556
575
|
};
|
|
557
576
|
const report = protect(root, opts);
|
|
558
577
|
console.log(formatProtectReport(report));
|
|
578
|
+
|
|
579
|
+
// Set persistent enforcement mode on the brain so the hook honours it.
|
|
580
|
+
// Default is "advisory" (warn). Users opt in to hard blocks with --strict.
|
|
581
|
+
try {
|
|
582
|
+
setEnforcementMode(root, strict ? "hard" : "advisory");
|
|
583
|
+
} catch (_) { /* ignore — brain may not exist yet */ }
|
|
584
|
+
|
|
585
|
+
if (strict) {
|
|
586
|
+
console.log(" Hard enforcement active. Every commit that violates a lock will be BLOCKED.");
|
|
587
|
+
console.log(" To relax: speclock protect (without --strict)");
|
|
588
|
+
} else {
|
|
589
|
+
console.log(" Warning mode active. To enforce hard blocks, run: speclock protect --strict");
|
|
590
|
+
console.log(" Violations will be printed at commit time but commits will NOT be blocked.");
|
|
591
|
+
}
|
|
592
|
+
console.log("");
|
|
593
|
+
|
|
559
594
|
if (report.errors.length > 0 && report.discovered.length === 0) {
|
|
560
595
|
process.exit(1);
|
|
561
596
|
}
|
|
@@ -612,6 +647,101 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
|
|
|
612
647
|
return;
|
|
613
648
|
}
|
|
614
649
|
|
|
650
|
+
// --- MCP INSTALL / UNINSTALL ---
|
|
651
|
+
// One-command autoinstaller: wires SpecLock into Claude Code, Cursor,
|
|
652
|
+
// Windsurf, Cline, Codex (or all of them) without any JSON hand-editing.
|
|
653
|
+
if (cmd === "mcp") {
|
|
654
|
+
const sub = args[0];
|
|
655
|
+
const client = args[1];
|
|
656
|
+
const flags = parseFlags(args.slice(2));
|
|
657
|
+
|
|
658
|
+
const supportedLabel = SUPPORTED_CLIENTS.join(", ");
|
|
659
|
+
|
|
660
|
+
if (!sub || (sub !== "install" && sub !== "uninstall")) {
|
|
661
|
+
console.error("Usage:");
|
|
662
|
+
console.error(` speclock mcp install <client> (${supportedLabel})`);
|
|
663
|
+
console.error(` speclock mcp uninstall <client>`);
|
|
664
|
+
console.error("");
|
|
665
|
+
console.error("Flags:");
|
|
666
|
+
console.error(" --no-project Skip project-scoped config (.mcp.json, .cursor/mcp.json)");
|
|
667
|
+
process.exit(1);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (!client) {
|
|
671
|
+
console.error(`Error: <client> is required.`);
|
|
672
|
+
console.error(`Supported: ${supportedLabel}`);
|
|
673
|
+
process.exit(1);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (!SUPPORTED_CLIENTS.includes(client)) {
|
|
677
|
+
console.error(`Unknown client "${client}".`);
|
|
678
|
+
console.error(`Supported: ${supportedLabel}`);
|
|
679
|
+
process.exit(1);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const options = {
|
|
683
|
+
includeProject: flags["no-project"] !== true,
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
const isInstall = sub === "install";
|
|
687
|
+
const header = isInstall
|
|
688
|
+
? "\nSpecLock MCP — Autoinstaller"
|
|
689
|
+
: "\nSpecLock MCP — Uninstaller";
|
|
690
|
+
console.log(header);
|
|
691
|
+
console.log("=".repeat(50));
|
|
692
|
+
|
|
693
|
+
let results;
|
|
694
|
+
if (client === "all") {
|
|
695
|
+
results = isInstall
|
|
696
|
+
? installAll(root, options)
|
|
697
|
+
: uninstallAll(root, options);
|
|
698
|
+
} else {
|
|
699
|
+
results = [
|
|
700
|
+
isInstall
|
|
701
|
+
? installForClient(client, root, options)
|
|
702
|
+
: uninstallForClient(client, root, options),
|
|
703
|
+
];
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
let anySuccess = false;
|
|
707
|
+
let anyError = false;
|
|
708
|
+
|
|
709
|
+
for (const r of results) {
|
|
710
|
+
console.log(`\n ${r.client}:`);
|
|
711
|
+
console.log(formatResult(r, sub));
|
|
712
|
+
if (r.errors.length > 0) anyError = true;
|
|
713
|
+
if (
|
|
714
|
+
r.writes.some(
|
|
715
|
+
(w) => w.status === "installed" || w.status === "removed"
|
|
716
|
+
)
|
|
717
|
+
) {
|
|
718
|
+
anySuccess = true;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
console.log("");
|
|
723
|
+
if (isInstall && anySuccess) {
|
|
724
|
+
console.log(" Next steps:");
|
|
725
|
+
if (client === "all") {
|
|
726
|
+
console.log(" Restart any AI clients that were updated.");
|
|
727
|
+
} else {
|
|
728
|
+
console.log(` ${nextStepsFor(client)}`);
|
|
729
|
+
}
|
|
730
|
+
console.log("");
|
|
731
|
+
console.log(" Verify: speclock status");
|
|
732
|
+
} else if (!isInstall && anySuccess) {
|
|
733
|
+
console.log(" SpecLock MCP server removed. Restart your AI client to apply.");
|
|
734
|
+
} else if (!anySuccess && !anyError) {
|
|
735
|
+
console.log(
|
|
736
|
+
isInstall
|
|
737
|
+
? " SpecLock was already installed everywhere. Nothing to do."
|
|
738
|
+
: " SpecLock was not installed anywhere. Nothing to do."
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
process.exit(anyError ? 1 : 0);
|
|
743
|
+
}
|
|
744
|
+
|
|
615
745
|
// --- TEMPLATE ---
|
|
616
746
|
if (cmd === "template") {
|
|
617
747
|
const sub = args[0];
|
|
@@ -706,23 +836,47 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
|
|
|
706
836
|
|
|
707
837
|
// --- AUDIT ---
|
|
708
838
|
if (cmd === "audit") {
|
|
839
|
+
const flags = parseFlags(args);
|
|
840
|
+
// Warn mode is the default (investor audit: hard-block had too many false positives).
|
|
841
|
+
// Users opt in to hard blocking with --strict, SPECLOCK_STRICT=1, or by running
|
|
842
|
+
// `speclock enforce hard` (which sets the persistent brain enforcement mode).
|
|
843
|
+
const brain = readBrain(root);
|
|
844
|
+
const brainMode = brain ? (getEnforcementConfig(brain).mode || "advisory") : "advisory";
|
|
845
|
+
const strict =
|
|
846
|
+
flags.strict === true ||
|
|
847
|
+
flags.block === true ||
|
|
848
|
+
process.env.SPECLOCK_STRICT === "1" ||
|
|
849
|
+
process.env.SPECLOCK_STRICT === "true" ||
|
|
850
|
+
brainMode === "hard";
|
|
851
|
+
|
|
709
852
|
const result = auditStagedFiles(root);
|
|
710
853
|
if (result.passed) {
|
|
711
854
|
console.log(result.message);
|
|
712
855
|
process.exit(0);
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
}
|
|
722
|
-
console.log(
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Violations found — print them for both warn and strict modes.
|
|
859
|
+
const header = strict ? "SPECLOCK AUDIT FAILED" : "SPECLOCK WARNINGS";
|
|
860
|
+
console.log(`\n${header}`);
|
|
861
|
+
console.log("=".repeat(50));
|
|
862
|
+
for (const v of result.violations) {
|
|
863
|
+
console.log(` [${v.severity}] ${v.file}`);
|
|
864
|
+
console.log(` Lock: ${v.lockText}`);
|
|
865
|
+
console.log(` Reason: ${v.reason}`);
|
|
866
|
+
console.log("");
|
|
867
|
+
}
|
|
868
|
+
console.log(result.message);
|
|
869
|
+
|
|
870
|
+
if (strict) {
|
|
723
871
|
console.log("Commit blocked. Unlock files or unstage them to proceed.");
|
|
724
872
|
process.exit(1);
|
|
725
873
|
}
|
|
874
|
+
|
|
875
|
+
console.log("Warning mode active — commit allowed. To enforce hard blocks, run:");
|
|
876
|
+
console.log(" speclock audit --strict");
|
|
877
|
+
console.log(" SPECLOCK_STRICT=1 git commit ...");
|
|
878
|
+
console.log(" speclock enforce hard (persistent, project-wide)");
|
|
879
|
+
process.exit(0);
|
|
726
880
|
}
|
|
727
881
|
|
|
728
882
|
// --- AUDIT-VERIFY (v2.1 enterprise) ---
|
|
@@ -858,7 +1012,18 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
|
|
|
858
1012
|
|
|
859
1013
|
// --- AUDIT-SEMANTIC (v2.5) ---
|
|
860
1014
|
if (cmd === "audit-semantic") {
|
|
1015
|
+
const flags = parseFlags(args);
|
|
861
1016
|
const result = semanticAudit(root);
|
|
1017
|
+
|
|
1018
|
+
// Warn mode default: only exit 1 if --strict, SPECLOCK_STRICT=1, or brain is in "hard" mode
|
|
1019
|
+
// (result.blocked already reflects "hard" mode from brain config).
|
|
1020
|
+
const strict =
|
|
1021
|
+
flags.strict === true ||
|
|
1022
|
+
flags.block === true ||
|
|
1023
|
+
process.env.SPECLOCK_STRICT === "1" ||
|
|
1024
|
+
process.env.SPECLOCK_STRICT === "true" ||
|
|
1025
|
+
result.blocked;
|
|
1026
|
+
|
|
862
1027
|
console.log(`\nSemantic Pre-Commit Audit`);
|
|
863
1028
|
console.log("=".repeat(50));
|
|
864
1029
|
console.log(`Mode: ${result.mode} | Threshold: ${result.threshold}%`);
|
|
@@ -877,7 +1042,15 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
|
|
|
877
1042
|
}
|
|
878
1043
|
}
|
|
879
1044
|
console.log(`\n${result.message}`);
|
|
880
|
-
|
|
1045
|
+
|
|
1046
|
+
if (result.violations.length > 0 && !strict) {
|
|
1047
|
+
console.log("\nWarning mode active — commit allowed. To enforce hard blocks, run:");
|
|
1048
|
+
console.log(" speclock audit-semantic --strict");
|
|
1049
|
+
console.log(" SPECLOCK_STRICT=1 git commit ...");
|
|
1050
|
+
console.log(" speclock enforce hard (persistent, project-wide)");
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
process.exit(strict && result.violations.length > 0 ? 1 : 0);
|
|
881
1054
|
}
|
|
882
1055
|
|
|
883
1056
|
// --- AUTH (v3.0) ---
|
|
@@ -1287,6 +1460,207 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
|
|
|
1287
1460
|
return;
|
|
1288
1461
|
}
|
|
1289
1462
|
|
|
1463
|
+
// --- DOCTOR: Diagnostic health check ---
|
|
1464
|
+
if (cmd === "doctor") {
|
|
1465
|
+
const fs = await import("fs");
|
|
1466
|
+
const os = await import("os");
|
|
1467
|
+
const lines = [];
|
|
1468
|
+
const fixes = [];
|
|
1469
|
+
let issueCount = 0;
|
|
1470
|
+
|
|
1471
|
+
lines.push("");
|
|
1472
|
+
lines.push("SpecLock Doctor — Health Check");
|
|
1473
|
+
lines.push("================================");
|
|
1474
|
+
lines.push("");
|
|
1475
|
+
|
|
1476
|
+
// --- 1. Installation ---
|
|
1477
|
+
lines.push("Installation");
|
|
1478
|
+
let pkgVersion = "unknown";
|
|
1479
|
+
try {
|
|
1480
|
+
// Find our own package.json — walk up from this module
|
|
1481
|
+
const selfPkgPath = path.join(root, "node_modules", "speclock", "package.json");
|
|
1482
|
+
if (fs.existsSync(selfPkgPath)) {
|
|
1483
|
+
pkgVersion = JSON.parse(fs.readFileSync(selfPkgPath, "utf-8")).version;
|
|
1484
|
+
} else {
|
|
1485
|
+
// Maybe running from the repo itself
|
|
1486
|
+
const localPkg = path.join(root, "package.json");
|
|
1487
|
+
if (fs.existsSync(localPkg)) {
|
|
1488
|
+
const p = JSON.parse(fs.readFileSync(localPkg, "utf-8"));
|
|
1489
|
+
if (p.name === "speclock") pkgVersion = p.version;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
lines.push(` ✓ SpecLock v${pkgVersion} installed`);
|
|
1493
|
+
} catch (e) {
|
|
1494
|
+
lines.push(` ✗ SpecLock version check failed: ${e.message}`);
|
|
1495
|
+
issueCount++;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
const speclockDir = path.join(root, ".speclock");
|
|
1499
|
+
if (fs.existsSync(speclockDir)) {
|
|
1500
|
+
lines.push(` ✓ .speclock/ directory present`);
|
|
1501
|
+
} else {
|
|
1502
|
+
lines.push(` ✗ .speclock/ directory missing`);
|
|
1503
|
+
fixes.push("Run: speclock setup");
|
|
1504
|
+
issueCount++;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
const brainPath = path.join(speclockDir, "brain.json");
|
|
1508
|
+
let brain = null;
|
|
1509
|
+
let activeLockCount = 0;
|
|
1510
|
+
if (fs.existsSync(brainPath)) {
|
|
1511
|
+
try {
|
|
1512
|
+
brain = JSON.parse(fs.readFileSync(brainPath, "utf-8"));
|
|
1513
|
+
activeLockCount = (brain.specLock?.items || []).filter((l) => l.active !== false).length;
|
|
1514
|
+
lines.push(` ✓ brain.json valid (${activeLockCount} locks)`);
|
|
1515
|
+
} catch (e) {
|
|
1516
|
+
lines.push(` ✗ brain.json is not valid JSON: ${e.message}`);
|
|
1517
|
+
fixes.push("Delete .speclock/brain.json and run: speclock setup");
|
|
1518
|
+
issueCount++;
|
|
1519
|
+
}
|
|
1520
|
+
} else if (fs.existsSync(speclockDir)) {
|
|
1521
|
+
lines.push(` ✗ brain.json missing`);
|
|
1522
|
+
fixes.push("Run: speclock init");
|
|
1523
|
+
issueCount++;
|
|
1524
|
+
}
|
|
1525
|
+
lines.push("");
|
|
1526
|
+
|
|
1527
|
+
// --- 2. Git Integration ---
|
|
1528
|
+
lines.push("Git Integration");
|
|
1529
|
+
const gitDir = path.join(root, ".git");
|
|
1530
|
+
const isGitRepo = fs.existsSync(gitDir);
|
|
1531
|
+
if (isGitRepo) {
|
|
1532
|
+
lines.push(` ✓ Git repository detected`);
|
|
1533
|
+
} else {
|
|
1534
|
+
lines.push(` ✗ Not a git repository`);
|
|
1535
|
+
fixes.push("Run: git init");
|
|
1536
|
+
issueCount++;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
if (isGitRepo) {
|
|
1540
|
+
const hookPath = path.join(gitDir, "hooks", "pre-commit");
|
|
1541
|
+
if (fs.existsSync(hookPath)) {
|
|
1542
|
+
const hookContent = fs.readFileSync(hookPath, "utf-8");
|
|
1543
|
+
const hasMarker = hookContent.includes("SPECLOCK-HOOK");
|
|
1544
|
+
const runsSpeclock = /speclock\s+audit/.test(hookContent) || /speclock/.test(hookContent);
|
|
1545
|
+
if (hasMarker && runsSpeclock) {
|
|
1546
|
+
lines.push(` ✓ Pre-commit hook installed`);
|
|
1547
|
+
lines.push(` ✓ Hook runs speclock`);
|
|
1548
|
+
} else if (hasMarker) {
|
|
1549
|
+
lines.push(` ⚠ Pre-commit hook has SpecLock marker but does not run speclock`);
|
|
1550
|
+
fixes.push("Run: speclock hook install");
|
|
1551
|
+
issueCount++;
|
|
1552
|
+
} else {
|
|
1553
|
+
lines.push(` ✗ Pre-commit hook exists but was not installed by SpecLock`);
|
|
1554
|
+
fixes.push("Run: speclock hook install (will append to existing hook)");
|
|
1555
|
+
issueCount++;
|
|
1556
|
+
}
|
|
1557
|
+
} else {
|
|
1558
|
+
lines.push(` ✗ Pre-commit hook not installed`);
|
|
1559
|
+
fixes.push("Run: speclock hook install");
|
|
1560
|
+
issueCount++;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// Enforcement mode (if brain exists)
|
|
1565
|
+
if (brain) {
|
|
1566
|
+
const mode = brain.enforcement?.mode || "advisory";
|
|
1567
|
+
const modeLabel = mode === "hard" ? "hard (block)" : "warn (advisory)";
|
|
1568
|
+
lines.push(` ✓ Mode: ${modeLabel}` + (mode !== "hard" ? " (use 'speclock enforce hard' for hard enforcement)" : ""));
|
|
1569
|
+
}
|
|
1570
|
+
lines.push("");
|
|
1571
|
+
|
|
1572
|
+
// --- 3. Rule Files ---
|
|
1573
|
+
lines.push("Rule Files");
|
|
1574
|
+
const discovered = discoverRuleFiles(root);
|
|
1575
|
+
const discoveredMap = new Map(discovered.map((f) => [f.file, f]));
|
|
1576
|
+
let totalRuleFilesFound = 0;
|
|
1577
|
+
for (const entry of RULE_FILES) {
|
|
1578
|
+
const found = discoveredMap.get(entry.file);
|
|
1579
|
+
if (found) {
|
|
1580
|
+
const extracted = extractConstraints(found.content, found.file);
|
|
1581
|
+
lines.push(` ✓ ${entry.file} (${extracted.locks.length} locks extracted)`);
|
|
1582
|
+
totalRuleFilesFound++;
|
|
1583
|
+
} else {
|
|
1584
|
+
lines.push(` ✗ ${entry.file} (not found)`);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
if (totalRuleFilesFound === 0) {
|
|
1588
|
+
fixes.push("Run: speclock protect (auto-creates a starter CLAUDE.md)");
|
|
1589
|
+
issueCount++;
|
|
1590
|
+
}
|
|
1591
|
+
lines.push("");
|
|
1592
|
+
|
|
1593
|
+
// --- 4. MCP Integration ---
|
|
1594
|
+
lines.push("MCP Integration");
|
|
1595
|
+
const home = os.homedir();
|
|
1596
|
+
|
|
1597
|
+
function checkMcpConfig(label, filePath, fixCmd) {
|
|
1598
|
+
if (fs.existsSync(filePath)) {
|
|
1599
|
+
try {
|
|
1600
|
+
const cfg = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1601
|
+
const servers = cfg.mcpServers || cfg.servers || {};
|
|
1602
|
+
const hasSpeclock = Object.keys(servers).some((k) => /speclock/i.test(k)) ||
|
|
1603
|
+
JSON.stringify(cfg).toLowerCase().includes("speclock");
|
|
1604
|
+
if (hasSpeclock) {
|
|
1605
|
+
lines.push(` ✓ ${label} (${filePath.replace(home, "~")})`);
|
|
1606
|
+
return true;
|
|
1607
|
+
}
|
|
1608
|
+
lines.push(` ✗ ${label} (config exists at ${filePath.replace(home, "~")}, but SpecLock not configured)`);
|
|
1609
|
+
lines.push(` Fix: ${fixCmd}`);
|
|
1610
|
+
issueCount++;
|
|
1611
|
+
return false;
|
|
1612
|
+
} catch (_) {
|
|
1613
|
+
lines.push(` ✗ ${label} (${filePath.replace(home, "~")}: invalid JSON)`);
|
|
1614
|
+
lines.push(` Fix: ${fixCmd}`);
|
|
1615
|
+
issueCount++;
|
|
1616
|
+
return false;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
lines.push(` ✗ ${label} (${filePath.replace(home, "~")})`);
|
|
1620
|
+
lines.push(` Fix: ${fixCmd}`);
|
|
1621
|
+
issueCount++;
|
|
1622
|
+
return false;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
checkMcpConfig(
|
|
1626
|
+
"Claude Code project (.mcp.json)",
|
|
1627
|
+
path.join(root, ".mcp.json"),
|
|
1628
|
+
"speclock mcp install claude-code"
|
|
1629
|
+
);
|
|
1630
|
+
checkMcpConfig(
|
|
1631
|
+
"Claude Code global (~/.claude/mcp.json)",
|
|
1632
|
+
path.join(home, ".claude", "mcp.json"),
|
|
1633
|
+
"speclock mcp install claude-code --global"
|
|
1634
|
+
);
|
|
1635
|
+
checkMcpConfig(
|
|
1636
|
+
"Cursor project (.cursor/mcp.json)",
|
|
1637
|
+
path.join(root, ".cursor", "mcp.json"),
|
|
1638
|
+
"speclock mcp install cursor"
|
|
1639
|
+
);
|
|
1640
|
+
checkMcpConfig(
|
|
1641
|
+
"Cursor global (~/.cursor/mcp.json)",
|
|
1642
|
+
path.join(home, ".cursor", "mcp.json"),
|
|
1643
|
+
"speclock mcp install cursor --global"
|
|
1644
|
+
);
|
|
1645
|
+
checkMcpConfig(
|
|
1646
|
+
"Windsurf (~/.codeium/windsurf/mcp_config.json)",
|
|
1647
|
+
path.join(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
1648
|
+
"speclock mcp install windsurf"
|
|
1649
|
+
);
|
|
1650
|
+
lines.push("");
|
|
1651
|
+
|
|
1652
|
+
// --- 5. Summary ---
|
|
1653
|
+
if (issueCount === 0) {
|
|
1654
|
+
lines.push("VERDICT: ✓ HEALTHY — all checks passed");
|
|
1655
|
+
} else {
|
|
1656
|
+
lines.push(`VERDICT: ⚠ ${issueCount} issue${issueCount === 1 ? "" : "s"} found (see fixes above)`);
|
|
1657
|
+
}
|
|
1658
|
+
lines.push("");
|
|
1659
|
+
|
|
1660
|
+
console.log(lines.join("\n"));
|
|
1661
|
+
process.exit(issueCount === 0 ? 0 : 1);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1290
1664
|
// --- RELEASE: Automated version bump + publish + deploy ---
|
|
1291
1665
|
if (cmd === "release") {
|
|
1292
1666
|
const bump = args[0]; // "patch", "minor", or "major"
|
package/src/core/auth.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// SPECLOCK-GUARD — DO NOT MODIFY THIS FILE
|
|
3
|
+
// LOCKED: NEVER modify auth files
|
|
4
|
+
// THIS FILE IS LOCKED. DO NOT EDIT, CHANGE, OR REWRITE ANY PART OF IT.
|
|
5
|
+
// The user must say "unlock" before this file can be changed.
|
|
6
|
+
// A question is NOT permission. Asking about features is NOT permission.
|
|
7
|
+
// ONLY "unlock" or "remove the lock" is permission to edit this file.
|
|
8
|
+
// ============================================================
|
|
1
9
|
/**
|
|
2
10
|
* SpecLock API Key Authentication
|
|
3
11
|
* Provides API key generation, validation, rotation, and revocation.
|
package/src/core/compliance.js
CHANGED
package/src/core/enforcer.js
CHANGED
|
@@ -29,10 +29,16 @@ import { analyzeConflict } from "./semantics.js";
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Get enforcement config from brain, with defaults.
|
|
32
|
+
*
|
|
33
|
+
* Default mode is "advisory" (warn only). Users opt in to hard blocking
|
|
34
|
+
* with `speclock protect --strict`, `speclock enforce hard`, the --strict
|
|
35
|
+
* flag on audit commands, or SPECLOCK_STRICT=1 env var. The investor audit
|
|
36
|
+
* found hard-block-by-default caused uninstalls within an hour due to the
|
|
37
|
+
* heuristic false-positive rate on things like "Refactor login page".
|
|
32
38
|
*/
|
|
33
39
|
export function getEnforcementConfig(brain) {
|
|
34
40
|
const defaults = {
|
|
35
|
-
mode: "advisory", // "advisory" | "hard"
|
|
41
|
+
mode: "advisory", // "advisory" (warn — default) | "hard" (block)
|
|
36
42
|
blockThreshold: 70, // minimum confidence % to block in hard mode
|
|
37
43
|
allowOverride: true, // whether overrides are permitted
|
|
38
44
|
escalationLimit: 3, // overrides before auto-note
|
package/src/core/guardian.js
CHANGED
|
@@ -16,9 +16,46 @@ import { installHook, isHookInstalled } from "./hooks.js";
|
|
|
16
16
|
import { syncRules } from "./rules-sync.js";
|
|
17
17
|
import { generateContext } from "./context.js";
|
|
18
18
|
|
|
19
|
+
// --- Starter CLAUDE.md for greenfield projects ---
|
|
20
|
+
|
|
21
|
+
const STARTER_CLAUDE_MD = `# Project Rules
|
|
22
|
+
|
|
23
|
+
These rules are enforced by SpecLock — your AI coding assistant will respect them.
|
|
24
|
+
|
|
25
|
+
## Database & Storage
|
|
26
|
+
- NEVER delete user data without explicit confirmation
|
|
27
|
+
- NEVER modify production database schema without migration
|
|
28
|
+
|
|
29
|
+
## Authentication & Security
|
|
30
|
+
- NEVER modify authentication files without security review
|
|
31
|
+
- NEVER commit secrets, API keys, or credentials
|
|
32
|
+
- NEVER disable security checks "temporarily"
|
|
33
|
+
|
|
34
|
+
## Code Quality
|
|
35
|
+
- ALWAYS write tests for new features
|
|
36
|
+
- NEVER push directly to main branch
|
|
37
|
+
- NEVER skip code review on critical paths
|
|
38
|
+
|
|
39
|
+
## Edit these rules to match your project. Add your own with:
|
|
40
|
+
## speclock add-lock "Your rule here"
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create a starter CLAUDE.md with safe defaults for greenfield projects.
|
|
45
|
+
* Used when `protect` is called on a project with no existing rule files.
|
|
46
|
+
*/
|
|
47
|
+
export function createStarterClaudeMd(root) {
|
|
48
|
+
const filePath = path.join(root, "CLAUDE.md");
|
|
49
|
+
if (fs.existsSync(filePath)) {
|
|
50
|
+
return { created: false, path: filePath, reason: "already exists" };
|
|
51
|
+
}
|
|
52
|
+
fs.writeFileSync(filePath, STARTER_CLAUDE_MD);
|
|
53
|
+
return { created: true, path: filePath };
|
|
54
|
+
}
|
|
55
|
+
|
|
19
56
|
// --- Rule file discovery ---
|
|
20
57
|
|
|
21
|
-
const RULE_FILES = [
|
|
58
|
+
export const RULE_FILES = [
|
|
22
59
|
{ file: ".cursorrules", tool: "Cursor" },
|
|
23
60
|
{ file: ".cursor/rules/rules.mdc", tool: "Cursor (MDC)" },
|
|
24
61
|
{ file: "CLAUDE.md", tool: "Claude Code" },
|
|
@@ -209,13 +246,29 @@ export function protect(root, options = {}) {
|
|
|
209
246
|
hookStatus: "",
|
|
210
247
|
synced: [],
|
|
211
248
|
errors: [],
|
|
249
|
+
starterCreated: false,
|
|
250
|
+
starterPath: null,
|
|
251
|
+
strict: options.strict === true,
|
|
212
252
|
};
|
|
213
253
|
|
|
214
254
|
// 1. Init
|
|
215
255
|
const brain = ensureInit(root);
|
|
216
256
|
|
|
217
257
|
// 2. Discover
|
|
218
|
-
|
|
258
|
+
let ruleFiles = discoverRuleFiles(root);
|
|
259
|
+
|
|
260
|
+
// 2b. Greenfield support: if no rule files found, auto-create a starter
|
|
261
|
+
// CLAUDE.md with safe defaults (unless explicitly disabled).
|
|
262
|
+
if (ruleFiles.length === 0 && !options.skipStarter) {
|
|
263
|
+
const starter = createStarterClaudeMd(root);
|
|
264
|
+
if (starter.created) {
|
|
265
|
+
report.starterCreated = true;
|
|
266
|
+
report.starterPath = "CLAUDE.md";
|
|
267
|
+
// Re-run discovery so the flow continues normally with the new file.
|
|
268
|
+
ruleFiles = discoverRuleFiles(root);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
219
272
|
report.discovered = ruleFiles.map((f) => ({
|
|
220
273
|
file: f.file,
|
|
221
274
|
tool: f.tool,
|
|
@@ -321,13 +374,20 @@ export function formatProtectReport(report) {
|
|
|
321
374
|
lines.push(" " + "=".repeat(50));
|
|
322
375
|
lines.push("");
|
|
323
376
|
|
|
377
|
+
// Starter CLAUDE.md was auto-created (greenfield support)
|
|
378
|
+
if (report.starterCreated) {
|
|
379
|
+
lines.push(" No rule files found.");
|
|
380
|
+
lines.push(` [+] Created starter CLAUDE.md with safe defaults — edit it to match your project.`);
|
|
381
|
+
lines.push("");
|
|
382
|
+
}
|
|
383
|
+
|
|
324
384
|
// Discovered files
|
|
325
385
|
if (report.discovered.length > 0) {
|
|
326
386
|
lines.push(" Rule files found:");
|
|
327
387
|
for (const f of report.discovered) {
|
|
328
388
|
lines.push(` [+] ${f.file} (${f.tool}, ${f.lines} lines)`);
|
|
329
389
|
}
|
|
330
|
-
} else {
|
|
390
|
+
} else if (!report.starterCreated) {
|
|
331
391
|
lines.push(" [!] No rule files found.");
|
|
332
392
|
}
|
|
333
393
|
lines.push("");
|
|
@@ -375,8 +435,21 @@ export function formatProtectReport(report) {
|
|
|
375
435
|
// Final message
|
|
376
436
|
const total = report.added.locks + report.added.skipped;
|
|
377
437
|
if (total > 0) {
|
|
378
|
-
|
|
379
|
-
|
|
438
|
+
if (report.strict) {
|
|
439
|
+
lines.push(" Your rules are now ENFORCED (strict mode).");
|
|
440
|
+
lines.push(" Commits that violate constraints will be BLOCKED.");
|
|
441
|
+
} else {
|
|
442
|
+
lines.push(" Your rules are now TRACKED (warning mode — default).");
|
|
443
|
+
lines.push(" Violations will be printed loudly, but commits will NOT be blocked.");
|
|
444
|
+
lines.push(" Opt in to hard enforcement any time with: speclock protect --strict");
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Greenfield guidance — tell the user to edit the starter file
|
|
449
|
+
if (report.starterCreated) {
|
|
450
|
+
lines.push("");
|
|
451
|
+
lines.push(" Next: edit CLAUDE.md to add project-specific rules, then run:");
|
|
452
|
+
lines.push(' speclock check "your action here"');
|
|
380
453
|
}
|
|
381
454
|
lines.push("");
|
|
382
455
|
|
package/src/core/lock-author.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// SPECLOCK-GUARD — DO NOT MODIFY THIS FILE
|
|
3
|
+
// LOCKED: NEVER modify auth files
|
|
4
|
+
// THIS FILE IS LOCKED. DO NOT EDIT, CHANGE, OR REWRITE ANY PART OF IT.
|
|
5
|
+
// The user must say "unlock" before this file can be changed.
|
|
6
|
+
// A question is NOT permission. Asking about features is NOT permission.
|
|
7
|
+
// ONLY "unlock" or "remove the lock" is permission to edit this file.
|
|
8
|
+
// ============================================================
|
|
1
9
|
// ===================================================================
|
|
2
10
|
// SpecLock Smart Lock Authoring Engine
|
|
3
11
|
// Auto-rewrites user locks to prevent verb contamination.
|