speclock 5.5.5 → 5.5.7

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  "name": "speclock",
4
4
 
5
- "version": "5.5.5",
5
+ "version": "5.5.7",
6
6
 
7
7
  "mcpName": "io.github.sgroy10/speclock",
8
8
 
package/src/cli/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import { fileURLToPath } from "url";
4
- import { getStagedDiff, parseDiff } from "../core/pre-commit-semantic.js";
4
+ import { getStagedDiff, parseDiff, shouldSkipForSemanticAudit } from "../core/pre-commit-semantic.js";
5
5
  import {
6
6
  ensureInit,
7
7
  setGoal,
@@ -68,6 +68,7 @@ import {
68
68
  ensureTelemetryDecision,
69
69
  recordCommand,
70
70
  TELEMETRY_DEFAULT_ENDPOINT,
71
+ buildUsageStats,
71
72
  } from "../core/telemetry.js";
72
73
  import {
73
74
  isSSOEnabled,
@@ -286,7 +287,7 @@ export function initFromRulePack(root, framework) {
286
287
 
287
288
  function printHelp() {
288
289
  console.log(`
289
- SpecLock v5.5.5 — Your AI has rules. SpecLock makes them unbreakable.
290
+ SpecLock v5.5.7 — Your AI has rules. SpecLock makes them unbreakable.
290
291
  Developed by Sandeep Roy (github.com/sgroy10)
291
292
 
292
293
  Usage: speclock <command> [options]
@@ -340,6 +341,7 @@ Commands:
340
341
  watch Start file watcher (live dashboard)
341
342
  serve [--project <path>] Start MCP stdio server
342
343
  status Show project brain summary
344
+ stats Show YOUR usage dashboard from local telemetry log
343
345
  doctor Diagnostic health check (install, git, rules, MCP)
344
346
 
345
347
  Options:
@@ -442,6 +444,152 @@ function showStatus(root) {
442
444
  console.log("");
443
445
  }
444
446
 
447
+ // --- Stats dashboard (speclock stats) ---
448
+
449
+ /**
450
+ * Build a self-contained view-model for the `speclock stats` dashboard.
451
+ * Pure data — no console I/O — so tests can assert against it.
452
+ *
453
+ * Combines three sources:
454
+ * 1. Local telemetry log (~/.speclock/telemetry.jsonl) via buildUsageStats
455
+ * 2. Current project brain.json (enforcement mode, lock count)
456
+ * 3. Rule file discovery + MCP client detection (from a sample telemetry event)
457
+ *
458
+ * Falls back gracefully when telemetry is disabled or the log is missing —
459
+ * the "Current State" section is still populated from brain.json.
460
+ *
461
+ * @param {string} root - project root
462
+ * @param {object} [opts] - passed through to buildUsageStats (e.g. { events, now })
463
+ */
464
+ export function buildStatsView(root, opts = {}) {
465
+ const usage = buildUsageStats(opts);
466
+
467
+ let lockCount = 0;
468
+ let enforcementMode = "unknown";
469
+ let brainExists = false;
470
+ try {
471
+ const brain = readBrain(root);
472
+ if (brain) {
473
+ brainExists = true;
474
+ const items = brain.specLock && Array.isArray(brain.specLock.items)
475
+ ? brain.specLock.items
476
+ : [];
477
+ lockCount = items.filter((l) => l && l.active !== false).length;
478
+ const cfg = getEnforcementConfig(brain);
479
+ // Map internal ("advisory" | "hard") to user-facing ("warn" | "hard").
480
+ enforcementMode = cfg.mode === "hard" ? "hard" : "warn";
481
+ }
482
+ } catch (_) { /* swallow */ }
483
+
484
+ // Rule files (from the same list guardian.js uses).
485
+ let ruleFiles = [];
486
+ try {
487
+ const discovered = discoverRuleFiles(root);
488
+ ruleFiles = discovered.map((f) => f.file);
489
+ } catch (_) { /* swallow */ }
490
+
491
+ // MCP clients — reuse the detector embedded in the sample telemetry event.
492
+ let mcpClients = [];
493
+ try {
494
+ const sample = getOptInTelemetryStatus({ eventLimit: 0 }).sampleEvent;
495
+ if (sample && Array.isArray(sample.mcpClientsConfigured)) {
496
+ mcpClients = sample.mcpClientsConfigured;
497
+ }
498
+ } catch (_) { /* swallow */ }
499
+
500
+ return {
501
+ ...usage,
502
+ brainExists,
503
+ enforcementMode,
504
+ lockCount,
505
+ ruleFiles,
506
+ mcpClients,
507
+ };
508
+ }
509
+
510
+ /**
511
+ * Render the stats view-model as a human-readable dashboard string.
512
+ * Kept separate from console output so tests can assert on the rendered
513
+ * text without capturing stdout.
514
+ */
515
+ export function formatStatsDashboard(view) {
516
+ const lines = [];
517
+ const firstInstallDate = view.firstInstallIso
518
+ ? view.firstInstallIso.slice(0, 10)
519
+ : "(unknown)";
520
+ const installIdShort = view.installId && view.installId !== "unknown"
521
+ ? view.installId.slice(0, 8) + "..."
522
+ : "(none)";
523
+
524
+ lines.push("");
525
+ lines.push("SpecLock Stats — Your Usage");
526
+ lines.push("=".repeat(32));
527
+ lines.push("");
528
+ lines.push("Installation");
529
+ lines.push(` First install: ${firstInstallDate}`);
530
+ lines.push(` Days active: ${view.daysActive}`);
531
+ lines.push(` Total events: ${view.totalEvents}`);
532
+ lines.push(` Install ID: ${installIdShort}`);
533
+ lines.push("");
534
+
535
+ lines.push("Commands Used");
536
+ const entries = Object.entries(view.commandsByType).sort(
537
+ ([, a], [, b]) => b - a
538
+ );
539
+ if (entries.length === 0) {
540
+ if (view.telemetryEnabled) {
541
+ lines.push(" (no events recorded yet — run some commands!)");
542
+ } else {
543
+ lines.push(" (telemetry disabled — enable with 'speclock telemetry on' to track usage)");
544
+ }
545
+ } else {
546
+ const maxName = Math.max(...entries.map(([n]) => n.length));
547
+ for (const [name, count] of entries) {
548
+ lines.push(` ${(name + ":").padEnd(maxName + 2)}${count}`);
549
+ }
550
+ }
551
+ lines.push("");
552
+
553
+ lines.push("Current State");
554
+ lines.push(` Enforcement: ${view.enforcementMode}`);
555
+ lines.push(` Locks: ${view.lockCount}`);
556
+ if (view.ruleFiles.length > 0) {
557
+ lines.push(` Rule files: ${view.ruleFiles.length} (${view.ruleFiles.join(", ")})`);
558
+ } else {
559
+ lines.push(" Rule files: 0");
560
+ }
561
+ if (view.mcpClients.length > 0) {
562
+ lines.push(` MCP clients: ${view.mcpClients.join(", ")}`);
563
+ } else {
564
+ lines.push(" MCP clients: (none detected)");
565
+ }
566
+ lines.push("");
567
+
568
+ lines.push(`Recent Activity (last ${view.recentEvents.length})`);
569
+ if (view.recentEvents.length === 0) {
570
+ lines.push(" (no activity recorded)");
571
+ } else {
572
+ // Most recent first in the dashboard.
573
+ const sorted = view.recentEvents.slice().reverse();
574
+ for (const e of sorted) {
575
+ const ts = (e.timestamp || "").replace("T", " ").slice(0, 16);
576
+ const cmd = (e.command || "unknown").padEnd(10);
577
+ const exit = typeof e.exitCode === "number" ? e.exitCode : "?";
578
+ lines.push(` ${ts} ${cmd} exit ${exit}`);
579
+ }
580
+ }
581
+ lines.push("");
582
+
583
+ if (!view.telemetryEnabled) {
584
+ lines.push("Note: telemetry is DISABLED — stats above reflect any pre-existing log");
585
+ lines.push(" plus the current project state from .speclock/brain.json.");
586
+ }
587
+ lines.push("Tip: Run 'speclock telemetry status' to see telemetry settings");
588
+ lines.push("");
589
+
590
+ return lines.join("\n");
591
+ }
592
+
445
593
  // --- Main ---
446
594
 
447
595
  async function main() {
@@ -491,9 +639,16 @@ async function main() {
491
639
  console.log(`Created SPECLOCK.md (AI instructions file).`);
492
640
 
493
641
  // 4. Inject marker into package.json (so AI tools auto-discover SpecLock)
494
- const pkgResult = injectPackageJsonMarker(root);
495
- if (pkgResult.success) {
496
- console.log("Injected SpecLock marker into package.json.");
642
+ // — only when package.json actually exists. We do NOT create one.
643
+ const pkgJsonPath = path.join(root, "package.json");
644
+ const pkgJsonExistedBefore = fs.existsSync(pkgJsonPath);
645
+ let pkgJsonUpdated = false;
646
+ if (pkgJsonExistedBefore) {
647
+ const pkgResult = injectPackageJsonMarker(root);
648
+ if (pkgResult.success) {
649
+ pkgJsonUpdated = true;
650
+ console.log("Injected SpecLock marker into package.json.");
651
+ }
497
652
  }
498
653
 
499
654
  // 5. Apply template if specified
@@ -511,6 +666,9 @@ async function main() {
511
666
  console.log("Generated .speclock/context/latest.md");
512
667
 
513
668
  // 7. Print summary
669
+ const pkgJsonLine = pkgJsonUpdated
670
+ ? " package.json — Active locks embedded (AI auto-discovery)"
671
+ : " package.json (skipped — not found)";
514
672
  console.log(`
515
673
  SpecLock is ready!
516
674
 
@@ -518,7 +676,7 @@ Files created/updated:
518
676
  .speclock/brain.json — Project memory
519
677
  .speclock/context/latest.md — Context for AI (read this)
520
678
  SPECLOCK.md — AI rules (read this)
521
- package.json — Active locks embedded (AI auto-discovery)
679
+ ${pkgJsonLine}
522
680
 
523
681
  Next steps:
524
682
  To add constraints: npx speclock lock "Never touch auth files"
@@ -1243,6 +1401,10 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
1243
1401
  // --- AUDIT-SEMANTIC (v2.5) ---
1244
1402
  if (cmd === "audit-semantic") {
1245
1403
  const flags = parseFlags(args);
1404
+ const verbose =
1405
+ flags.verbose === true ||
1406
+ process.env.SPECLOCK_VERBOSE === "1" ||
1407
+ process.env.SPECLOCK_VERBOSE === "true";
1246
1408
  const result = semanticAudit(root);
1247
1409
 
1248
1410
  // --- Commit-message + diff-content semantic check ---
@@ -1269,9 +1431,15 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
1269
1431
  } catch { /* ignore */ }
1270
1432
  }
1271
1433
 
1272
- // 2. Collect added lines from the staged diff.
1434
+ // 2. Collect added lines from the staged diff, skipping SpecLock-internal
1435
+ // files (see shouldSkipForSemanticAudit). Those files' contents literally
1436
+ // restate the locks, so scanning their added lines produces 100%
1437
+ // false-positive matches for every lock in the brain.
1273
1438
  const diffText = getStagedDiff(root);
1274
- const fileChanges = diffText ? parseDiff(diffText) : [];
1439
+ const allParsedChanges = diffText ? parseDiff(diffText) : [];
1440
+ const fileChanges = allParsedChanges.filter(
1441
+ (fc) => !shouldSkipForSemanticAudit(fc.file, root)
1442
+ );
1275
1443
  const addedSnippets = [];
1276
1444
  for (const fc of fileChanges) {
1277
1445
  for (const line of fc.addedLines) {
@@ -1345,24 +1513,85 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
1345
1513
  : `WARNING: ${result.violations.length} violation(s) detected. Review before proceeding.`;
1346
1514
  }
1347
1515
 
1348
- // Warn mode default: only exit 1 if --strict, SPECLOCK_STRICT=1, or brain is in "hard" mode
1349
- // (result.blocked already reflects "hard" mode from brain config).
1350
- const strict =
1351
- flags.strict === true ||
1352
- flags.block === true ||
1516
+ // Warn mode default: only exit 1 if --strict, SPECLOCK_STRICT=1, or brain is in "hard" mode.
1517
+ //
1518
+ // Enforcement mode precedence (first match wins):
1519
+ // 1. --strict / --block CLI flag
1520
+ // 2. SPECLOCK_STRICT=1 env var
1521
+ // 3. brain.enforcement.mode === "hard" from .speclock/brain.json
1522
+ // 4. Default: warn mode (exit 0)
1523
+ //
1524
+ // We re-read brain.json here as a belt-and-braces fallback because
1525
+ // semanticAudit() may early-return (no staged diff, no locks, brain
1526
+ // missing) WITHOUT setting result.mode/result.blocked, and git hooks
1527
+ // can run in sanitized environments where SPECLOCK_STRICT=1 on the
1528
+ // `git commit` command line gets stripped by some shells. The
1529
+ // persistent brain mode (set by `speclock enforce hard`) is the only
1530
+ // reliable way to enforce hard blocking across all git/shell combos.
1531
+ const cliStrict = flags.strict === true || flags.block === true;
1532
+ const envStrict =
1353
1533
  process.env.SPECLOCK_STRICT === "1" ||
1354
- process.env.SPECLOCK_STRICT === "true" ||
1355
- result.blocked;
1534
+ process.env.SPECLOCK_STRICT === "true";
1535
+
1536
+ let brainHardMode = false;
1537
+ try {
1538
+ const brainForMode = readBrain(root);
1539
+ if (brainForMode) {
1540
+ const cfg = getEnforcementConfig(brainForMode);
1541
+ brainHardMode = cfg.mode === "hard";
1542
+ }
1543
+ } catch { /* brain unreadable — treat as advisory */ }
1544
+
1545
+ // If brain is in hard mode but semanticAudit() returned without
1546
+ // reflecting that (early-return path), retro-fit the result so the
1547
+ // downstream printing + blocking decision stays consistent.
1548
+ if (brainHardMode && result.mode !== "hard") {
1549
+ result.mode = "hard";
1550
+ if (result.threshold === undefined) result.threshold = 70;
1551
+ }
1552
+ if (brainHardMode && !result.blocked) {
1553
+ const thresh = result.threshold || 70;
1554
+ result.blocked = (result.violations || []).some(
1555
+ (v) => (v.confidence || 0) >= thresh
1556
+ );
1557
+ }
1558
+
1559
+ const strict = cliStrict || envStrict || brainHardMode || result.blocked;
1560
+
1561
+ // --- Three-tier output filter (v5.5.7) ---
1562
+ // Investor audit: walls of LOW-confidence matches are user-hostile.
1563
+ // Only HIGH and MEDIUM print by default. LOW rolls up into a one-liner.
1564
+ // --verbose / SPECLOCK_VERBOSE=1 shows everything.
1565
+ const OUTPUT_MIN_CONFIDENCE = 40; // below this = "LOW", hidden by default
1566
+ const MAX_VISIBLE_VIOLATIONS = 10; // hard cap on printed items
1567
+
1568
+ const allViolations = result.violations || [];
1569
+ const highViolations = allViolations.filter((v) => (v.confidence || 0) >= 70);
1570
+ const mediumViolations = allViolations.filter(
1571
+ (v) => (v.confidence || 0) >= OUTPUT_MIN_CONFIDENCE && (v.confidence || 0) < 70
1572
+ );
1573
+ const lowViolations = allViolations.filter(
1574
+ (v) => (v.confidence || 0) < OUTPUT_MIN_CONFIDENCE
1575
+ );
1576
+
1577
+ // What actually gets printed
1578
+ const visibleViolations = verbose
1579
+ ? allViolations
1580
+ : [...highViolations, ...mediumViolations];
1356
1581
 
1357
1582
  console.log(`\nSemantic Pre-Commit Audit`);
1358
1583
  console.log("=".repeat(50));
1359
1584
  console.log(`Mode: ${result.mode} | Threshold: ${result.threshold}%`);
1360
- console.log(`Files analyzed: ${result.filesChecked}`);
1585
+ const filesLine = result.filesSkipped
1586
+ ? `Files analyzed: ${result.filesChecked} (${result.filesSkipped} skipped)`
1587
+ : `Files analyzed: ${result.filesChecked}`;
1588
+ console.log(filesLine);
1361
1589
  console.log(`Active locks: ${result.activeLocks}`);
1362
- console.log(`Violations: ${result.violations.length}`);
1363
- if (result.violations.length > 0) {
1590
+
1591
+ if (visibleViolations.length > 0) {
1364
1592
  console.log("");
1365
- for (const v of result.violations) {
1593
+ const toPrint = visibleViolations.slice(0, MAX_VISIBLE_VIOLATIONS);
1594
+ for (const v of toPrint) {
1366
1595
  console.log(` [${v.level}] ${v.file} (confidence: ${v.confidence}%)`);
1367
1596
  console.log(` Lock: "${v.lockText}"`);
1368
1597
  console.log(` Reason: ${v.reason}`);
@@ -1370,17 +1599,48 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
1370
1599
  console.log(` Changes: +${v.addedLines} / -${v.removedLines} lines`);
1371
1600
  }
1372
1601
  }
1602
+ const hiddenByCap = visibleViolations.length - toPrint.length;
1603
+ if (hiddenByCap > 0) {
1604
+ console.log(` ... + ${hiddenByCap} more (output capped at ${MAX_VISIBLE_VIOLATIONS})`);
1605
+ }
1606
+ }
1607
+
1608
+ // LOW-confidence rollup
1609
+ if (!verbose && lowViolations.length > 0) {
1610
+ console.log(
1611
+ ` + ${lowViolations.length} low-confidence match(es) hidden (use --verbose or SPECLOCK_VERBOSE=1 to see)`
1612
+ );
1613
+ }
1614
+
1615
+ // --- New summary line ---
1616
+ console.log("");
1617
+ let summaryLine;
1618
+ if (highViolations.length === 0 && mediumViolations.length === 0) {
1619
+ summaryLine = `[OK] ${result.filesChecked} file(s) checked, no concerns.`;
1620
+ } else if (highViolations.length > 0) {
1621
+ summaryLine = `[!] ${highViolations.length} HIGH-confidence concern(s) — review before merging.`;
1622
+ } else {
1623
+ summaryLine = `[i] ${mediumViolations.length} medium-confidence note(s) (informational).`;
1624
+ }
1625
+ console.log(summaryLine);
1626
+
1627
+ // Preserve the machine-readable status message for any callers that grep for it
1628
+ if (result.blocked) {
1629
+ console.log(
1630
+ `BLOCKED: ${highViolations.length} high-confidence violation(s) — hard enforcement active.`
1631
+ );
1373
1632
  }
1374
- console.log(`\n${result.message}`);
1375
1633
 
1376
- if (result.violations.length > 0 && !strict) {
1634
+ if (highViolations.length > 0 && !strict) {
1377
1635
  console.log("\nWarning mode active — commit allowed. To enforce hard blocks, run:");
1378
1636
  console.log(" speclock audit-semantic --strict");
1379
1637
  console.log(" SPECLOCK_STRICT=1 git commit ...");
1380
1638
  console.log(" speclock enforce hard (persistent, project-wide)");
1381
1639
  }
1382
1640
 
1383
- process.exit(strict && result.violations.length > 0 ? 1 : 0);
1641
+ // Blocking decision is still driven by the engine's 70% threshold, so
1642
+ // only HIGH-confidence matches can ever cause a non-zero exit.
1643
+ process.exit(strict && highViolations.length > 0 ? 1 : 0);
1384
1644
  }
1385
1645
 
1386
1646
  // --- AUTH (v3.0) ---
@@ -1563,6 +1823,18 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
1563
1823
  process.exit(1);
1564
1824
  }
1565
1825
 
1826
+ // --- STATS (user-facing usage dashboard) ---
1827
+ if (cmd === "stats") {
1828
+ try {
1829
+ const view = buildStatsView(root);
1830
+ console.log(formatStatsDashboard(view));
1831
+ } catch (err) {
1832
+ console.error(`Failed to build stats: ${err.message}`);
1833
+ process.exit(1);
1834
+ }
1835
+ return;
1836
+ }
1837
+
1566
1838
  // --- TELEMETRY (opt-in, v5.5) ---
1567
1839
  if (cmd === "telemetry") {
1568
1840
  const sub = args[0];
@@ -1987,21 +2259,79 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
1987
2259
  lines.push("");
1988
2260
 
1989
2261
  // --- 3. Rule Files ---
2262
+ // Doctor checks both:
2263
+ // (a) ORIGINAL rule files (.cursorrules, CLAUDE.md, etc.) that users author
2264
+ // (b) SYNCED files written by `speclock protect` / `speclock sync`
2265
+ // (.cursor/rules/speclock.mdc, .windsurf/rules/speclock.md, AGENTS.md
2266
+ // with SpecLock marker, GEMINI.md, .github/copilot-instructions.md,
2267
+ // .aider.conf.yml)
2268
+ // A file is considered "synced" if it has a SpecLock auto-gen marker in its header.
1990
2269
  lines.push("Rule Files");
2270
+
2271
+ // All the files `speclock sync` can produce (mirrors FORMATS in rules-sync.js)
2272
+ const SYNCED_OUTPUT_FILES = [
2273
+ ".cursor/rules/speclock.mdc",
2274
+ ".windsurf/rules/speclock.md",
2275
+ ".github/copilot-instructions.md",
2276
+ "GEMINI.md",
2277
+ ".aider.conf.yml",
2278
+ "AGENTS.md",
2279
+ ];
2280
+
2281
+ // Markers that indicate a file was written by SpecLock's sync pipeline.
2282
+ // Must match the markers used by isSpeclockGenerated() in guardian.js.
2283
+ const SPECLOCK_DOCTOR_SYNC_MARKERS = [
2284
+ "Auto-synced from SpecLock",
2285
+ "Auto-synced by SpecLock",
2286
+ "Auto-synced.",
2287
+ "(SpecLock)",
2288
+ "# SpecLock Constraints",
2289
+ "Do not edit manually — run `speclock sync`",
2290
+ "speclock sync --format",
2291
+ "speclock_session_briefing",
2292
+ ];
2293
+
2294
+ function isSyncedFile(absPath) {
2295
+ try {
2296
+ const content = fs.readFileSync(absPath, "utf-8");
2297
+ const header = content.split("\n").slice(0, 10).join("\n");
2298
+ return SPECLOCK_DOCTOR_SYNC_MARKERS.some((m) => header.includes(m));
2299
+ } catch (_) {
2300
+ return false;
2301
+ }
2302
+ }
2303
+
1991
2304
  const discovered = discoverRuleFiles(root);
1992
2305
  const discoveredMap = new Map(discovered.map((f) => [f.file, f]));
2306
+ const shownFiles = new Set();
1993
2307
  let totalRuleFilesFound = 0;
2308
+
2309
+ // (a) Original/authored rule files
1994
2310
  for (const entry of RULE_FILES) {
1995
2311
  const found = discoveredMap.get(entry.file);
1996
2312
  if (found) {
1997
2313
  const extracted = extractConstraints(found.content, found.file);
1998
2314
  lines.push(` ✓ ${entry.file} (${extracted.locks.length} locks extracted)`);
2315
+ shownFiles.add(entry.file);
2316
+ totalRuleFilesFound++;
2317
+ }
2318
+ }
2319
+
2320
+ // (b) Synced files written by `speclock protect` / `speclock sync`
2321
+ for (const relPath of SYNCED_OUTPUT_FILES) {
2322
+ if (shownFiles.has(relPath)) continue; // already shown as an authored file
2323
+ const abs = path.join(root, relPath);
2324
+ if (!fs.existsSync(abs)) continue;
2325
+ if (isSyncedFile(abs)) {
2326
+ lines.push(` ✓ ${relPath} (synced)`);
2327
+ shownFiles.add(relPath);
1999
2328
  totalRuleFilesFound++;
2000
- } else {
2001
- lines.push(` ✗ ${entry.file} (not found)`);
2002
2329
  }
2003
2330
  }
2331
+
2332
+ // If nothing at all was found, show a clear diagnostic.
2004
2333
  if (totalRuleFilesFound === 0) {
2334
+ lines.push(` ✗ No rule files found`);
2005
2335
  fixes.push("Run: speclock protect (auto-creates a starter CLAUDE.md)");
2006
2336
  issueCount++;
2007
2337
  }
@@ -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.5";
12
+ const VERSION = "5.5.7";
13
13
 
14
14
  // PHI-related keywords for HIPAA filtering
15
15
  const PHI_KEYWORDS = [