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/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.3 — Your AI has rules. SpecLock makes them unbreakable.
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 Zero-config: read rule files, extract locks, enforce
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 Audit staged files against locks
150
- audit-semantic Semantic audit: analyze code changes vs locks
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
- } else {
714
- console.log("\nSPECLOCK AUDIT FAILED");
715
- console.log("=".repeat(50));
716
- for (const v of result.violations) {
717
- console.log(` [${v.severity}] ${v.file}`);
718
- console.log(` Lock: ${v.lockText}`);
719
- console.log(` Reason: ${v.reason}`);
720
- console.log("");
721
- }
722
- console.log(result.message);
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
- process.exit(result.blocked ? 1 : 0);
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.
@@ -9,7 +9,7 @@
9
9
  import { readBrain, readEvents } from "./storage.js";
10
10
  import { verifyAuditChain } from "./audit.js";
11
11
 
12
- const VERSION = "5.5.3";
12
+ const VERSION = "5.5.4";
13
13
 
14
14
  // PHI-related keywords for HIPAA filtering
15
15
  const PHI_KEYWORDS = [
@@ -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
@@ -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
- const ruleFiles = discoverRuleFiles(root);
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
- lines.push(" Your rules are now ENFORCED, not just suggested.");
379
- lines.push(" AI agents that violate constraints will be blocked.");
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
 
@@ -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.