speclock 4.3.0 → 4.3.2

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "speclock",
3
- "version": "4.3.0",
3
+ "version": "4.3.2",
4
4
  "description": "AI constraint engine with Gemini LLM universal detection, Policy-as-Code DSL, OAuth/OIDC SSO, admin dashboard, telemetry, API key auth, RBAC, AES-256-GCM encryption, hard enforcement, semantic pre-commit, HMAC audit chain, SOC 2/HIPAA compliance. Cross-platform: MCP + direct API. 31 MCP tools + CLI. Enterprise platform.",
5
5
  "type": "module",
6
6
  "main": "src/mcp/server.js",
package/src/cli/index.js CHANGED
@@ -116,7 +116,7 @@ function refreshContext(root) {
116
116
 
117
117
  function printHelp() {
118
118
  console.log(`
119
- SpecLock v4.3.0 — AI Constraint Engine (Gemini LLM + Policy-as-Code + SSO + Dashboard + Telemetry + Auth + RBAC + Encryption)
119
+ SpecLock v4.3.2 — AI Constraint Engine (Gemini LLM + Policy-as-Code + SSO + Dashboard + Telemetry + Auth + RBAC + Encryption)
120
120
  Developed by Sandeep Roy (github.com/sgroy10)
121
121
 
122
122
  Usage: speclock <command> [options]
@@ -9,7 +9,7 @@
9
9
  import { readBrain, readEvents } from "./storage.js";
10
10
  import { verifyAuditChain } from "./audit.js";
11
11
 
12
- const VERSION = "4.3.0";
12
+ const VERSION = "4.3.2";
13
13
 
14
14
  // PHI-related keywords for HIPAA filtering
15
15
  const PHI_KEYWORDS = [
@@ -257,7 +257,7 @@ export async function flushToRemote(root) {
257
257
  // Build anonymized payload
258
258
  const payload = {
259
259
  instanceId: summary.instanceId,
260
- version: "4.3.0",
260
+ version: "4.3.2",
261
261
  totalCalls: summary.totalCalls,
262
262
  avgResponseMs: summary.avgResponseMs,
263
263
  conflicts: summary.conflicts,
@@ -89,7 +89,7 @@
89
89
  <div class="header">
90
90
  <div>
91
91
  <h1><span>SpecLock</span> Dashboard</h1>
92
- <div class="meta">v4.3.0 &mdash; AI Constraint Engine</div>
92
+ <div class="meta">v4.3.2 &mdash; AI Constraint Engine</div>
93
93
  </div>
94
94
  <div style="display:flex;align-items:center;gap:12px;">
95
95
  <span id="health-badge" class="status-badge healthy">Loading...</span>
@@ -182,7 +182,7 @@
182
182
  </div>
183
183
 
184
184
  <div style="text-align:center;padding:24px;color:var(--muted);font-size:12px;">
185
- SpecLock v4.3.0 &mdash; Developed by Sandeep Roy &mdash; <a href="https://github.com/sgroy10/speclock" style="color:var(--accent)">GitHub</a>
185
+ SpecLock v4.3.2 &mdash; Developed by Sandeep Roy &mdash; <a href="https://github.com/sgroy10/speclock" style="color:var(--accent)">GitHub</a>
186
186
  </div>
187
187
 
188
188
  <script>
@@ -91,7 +91,7 @@ import { fileURLToPath } from "url";
91
91
  import _path from "path";
92
92
 
93
93
  const PROJECT_ROOT = process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
94
- const VERSION = "4.3.0";
94
+ const VERSION = "4.3.2";
95
95
  const AUTHOR = "Sandeep Roy";
96
96
  const START_TIME = Date.now();
97
97
 
@@ -201,7 +201,7 @@ function createSpecLockServer() {
201
201
  server.tool("speclock_add_lock", "Add a non-negotiable constraint (SpecLock).", { text: z.string().min(1).describe("The constraint text"), tags: z.array(z.string()).default([]).describe("Category tags"), source: z.enum(["user", "agent"]).default("agent").describe("Who created this lock") }, async ({ text, tags, source }) => {
202
202
  ensureInit(PROJECT_ROOT);
203
203
  const lock = addLock(PROJECT_ROOT, text, tags, source);
204
- return { content: [{ type: "text", text: `Lock added [${lock.id}]: ${text}` }] };
204
+ return { content: [{ type: "text", text: `Lock added [${lock.lockId}]: ${text}` }] };
205
205
  });
206
206
 
207
207
  // Tool 5: speclock_remove_lock
@@ -215,7 +215,7 @@ function createSpecLockServer() {
215
215
  server.tool("speclock_add_decision", "Record an architectural or design decision.", { text: z.string().min(1).describe("The decision text"), tags: z.array(z.string()).default([]), source: z.enum(["user", "agent"]).default("agent") }, async ({ text, tags, source }) => {
216
216
  ensureInit(PROJECT_ROOT);
217
217
  const d = addDecision(PROJECT_ROOT, text, tags, source);
218
- return { content: [{ type: "text", text: `Decision recorded [${d.id}]: ${text}` }] };
218
+ return { content: [{ type: "text", text: `Decision recorded [${d.decId}]: ${text}` }] };
219
219
  });
220
220
 
221
221
  // Tool 7: speclock_add_note
@@ -262,21 +262,63 @@ function createSpecLockServer() {
262
262
  return { content: [{ type: "text", text: events.length ? JSON.stringify(events, null, 2) : "No matching events." }] };
263
263
  });
264
264
 
265
- // Tool 12: speclock_check_conflict (v2.5: uses enforcer)
266
- server.tool("speclock_check_conflict", "Check if a proposed action conflicts with any active SpecLock. In hard mode, blocks above threshold.", { proposedAction: z.string().min(1).describe("Description of the action") }, async ({ proposedAction }) => {
265
+ // Tool 12: speclock_check_conflict (v4.3: hybrid heuristic + Gemini LLM)
266
+ server.tool("speclock_check_conflict", "Check if a proposed action conflicts with any active SpecLock. Uses fast heuristic + Gemini LLM for universal domain coverage. In hard enforcement mode, conflicts above the threshold will BLOCK the action.", { proposedAction: z.string().min(1).describe("Description of the action") }, async ({ proposedAction }) => {
267
267
  ensureInit(PROJECT_ROOT);
268
- const result = enforceConflictCheck(PROJECT_ROOT, proposedAction);
268
+ // Hybrid check: heuristic first, LLM for grey-zone
269
+ let result = await checkConflictAsync(PROJECT_ROOT, proposedAction);
270
+
271
+ // If async hybrid returned no conflict, also check enforcer for hard mode
272
+ if (!result.hasConflict) {
273
+ const enforced = enforceConflictCheck(PROJECT_ROOT, proposedAction);
274
+ if (enforced.blocked) {
275
+ return { content: [{ type: "text", text: enforced.analysis }], isError: true };
276
+ }
277
+ }
278
+
279
+ // In hard mode with blocking conflict, return isError: true
269
280
  if (result.blocked) {
270
281
  return { content: [{ type: "text", text: result.analysis }], isError: true };
271
282
  }
283
+
272
284
  return { content: [{ type: "text", text: result.analysis }] };
273
285
  });
274
286
 
275
- // Tool 13: speclock_session_briefing
287
+ // Tool 13: speclock_session_briefing (v4.3: try-catch + rich output)
276
288
  server.tool("speclock_session_briefing", "Start a new session and get a full briefing.", { toolName: z.enum(["claude-code", "cursor", "codex", "windsurf", "cline", "unknown"]).default("unknown") }, async ({ toolName }) => {
277
- ensureInit(PROJECT_ROOT);
278
- const briefing = getSessionBriefing(PROJECT_ROOT, toolName);
279
- return { content: [{ type: "text", text: briefing }] };
289
+ try {
290
+ ensureInit(PROJECT_ROOT);
291
+ const briefing = getSessionBriefing(PROJECT_ROOT, toolName);
292
+ const contextMd = generateContext(PROJECT_ROOT);
293
+
294
+ const parts = [];
295
+ parts.push(`# SpecLock Session Briefing`);
296
+ parts.push(`Session started (${toolName}). ID: ${briefing.session?.id || "new"}`);
297
+ parts.push("");
298
+
299
+ if (briefing.lastSession) {
300
+ parts.push("## Last Session");
301
+ parts.push(`- Tool: **${briefing.lastSession.toolUsed || "unknown"}**`);
302
+ parts.push(`- Ended: ${briefing.lastSession.endedAt || "unknown"}`);
303
+ if (briefing.lastSession.summary) parts.push(`- Summary: ${briefing.lastSession.summary}`);
304
+ parts.push(`- Events: ${briefing.lastSession.eventsInSession || 0}`);
305
+ parts.push(`- Changes since then: ${briefing.changesSinceLastSession || 0}`);
306
+ parts.push("");
307
+ }
308
+
309
+ if (briefing.warnings?.length > 0) {
310
+ parts.push("## Warnings");
311
+ for (const w of briefing.warnings) parts.push(`- ${w}`);
312
+ parts.push("");
313
+ }
314
+
315
+ parts.push("---");
316
+ parts.push(contextMd);
317
+
318
+ return { content: [{ type: "text", text: parts.join("\n") }] };
319
+ } catch (err) {
320
+ return { content: [{ type: "text", text: `# SpecLock Session Briefing\n\nError loading session: ${err.message}\n\nTry running speclock_init first.\n\n---\n*SpecLock v${VERSION}*` }] };
321
+ }
280
322
  });
281
323
 
282
324
  // Tool 14: speclock_session_summary
@@ -329,24 +371,70 @@ function createSpecLockServer() {
329
371
  return { content: [{ type: "text", text: `## Drift Detected\n\n${text}` }] };
330
372
  });
331
373
 
332
- // Tool 19: speclock_health
374
+ // Tool 19: speclock_health (v4.3: null-safe)
333
375
  server.tool("speclock_health", "Health check with completeness score and multi-agent timeline.", {}, async () => {
334
- ensureInit(PROJECT_ROOT);
335
- const brain = readBrain(PROJECT_ROOT);
336
- const events = readEvents(PROJECT_ROOT);
337
- let score = 0;
338
- const checks = [];
339
- if (brain.project.goal) { score += 20; checks.push("- [x] Goal set"); } else checks.push("- [ ] Goal missing");
340
- if (brain.state.locks.filter(l => l.active).length > 0) { score += 20; checks.push("- [x] Locks defined"); } else checks.push("- [ ] No active locks");
341
- if (brain.state.decisions.length > 0) { score += 15; checks.push("- [x] Decisions recorded"); } else checks.push("- [ ] No decisions");
342
- if (brain.state.sessions.length > 0) { score += 15; checks.push("- [x] Sessions tracked"); } else checks.push("- [ ] No sessions");
343
- if (brain.deploy?.provider) { score += 10; checks.push("- [x] Deploy facts set"); } else checks.push("- [ ] Deploy facts missing");
344
- if (brain.state.notes.length > 0) { score += 10; checks.push("- [x] Notes present"); } else checks.push("- [ ] No notes");
345
- if (events.length > 10) { score += 10; checks.push("- [x] Rich event history"); } else checks.push("- [ ] Limited events");
346
- const grade = score >= 90 ? "A" : score >= 70 ? "B" : score >= 50 ? "C" : score >= 30 ? "D" : "F";
347
- const sessions = brain.state.sessions.slice(-5);
348
- const agentTimeline = sessions.length ? "\n\n### Recent Sessions\n" + sessions.map(s => `- **${s.tool || "unknown"}** @ ${s.startedAt}${s.summary ? ": " + s.summary : ""}`).join("\n") : "";
349
- return { content: [{ type: "text", text: `## SpecLock Health Check\n\nScore: **${score}/100** (Grade: ${grade})\nEvents: ${brain.events.count} | Reverts: ${brain.state.reverts.length}\n\n### Checks\n${checks.join("\n")}${agentTimeline}\n\n---\n*SpecLock v${VERSION} — Developed by ${AUTHOR}*` }] };
376
+ try {
377
+ const brain = ensureInit(PROJECT_ROOT);
378
+ const activeLocks = (brain.specLock?.items || []).filter((l) => l.active !== false);
379
+
380
+ let score = 0;
381
+ const checks = [];
382
+
383
+ if (brain.goal?.text) { score += 20; checks.push("[PASS] Goal is set"); }
384
+ else checks.push("[MISS] No project goal set");
385
+
386
+ if (activeLocks.length > 0) { score += 25; checks.push(`[PASS] ${activeLocks.length} active lock(s)`); }
387
+ else checks.push("[MISS] No SpecLock constraints defined");
388
+
389
+ if ((brain.decisions || []).length > 0) { score += 15; checks.push(`[PASS] ${brain.decisions.length} decision(s) recorded`); }
390
+ else checks.push("[MISS] No decisions recorded");
391
+
392
+ if ((brain.notes || []).length > 0) { score += 10; checks.push(`[PASS] ${brain.notes.length} note(s)`); }
393
+ else checks.push("[MISS] No notes added");
394
+
395
+ const sessionHistory = brain.sessions?.history || [];
396
+ if (sessionHistory.length > 0) { score += 15; checks.push(`[PASS] ${sessionHistory.length} session(s) in history`); }
397
+ else checks.push("[MISS] No session history yet");
398
+
399
+ const recentChanges = brain.state?.recentChanges || [];
400
+ if (recentChanges.length > 0) { score += 10; checks.push(`[PASS] ${recentChanges.length} change(s) tracked`); }
401
+ else checks.push("[MISS] No changes tracked");
402
+
403
+ if (brain.facts?.deploy?.provider && brain.facts.deploy.provider !== "unknown") { score += 5; checks.push("[PASS] Deploy facts configured"); }
404
+ else checks.push("[MISS] Deploy facts not configured");
405
+
406
+ // Multi-agent timeline
407
+ const agentMap = {};
408
+ for (const session of sessionHistory) {
409
+ const tool = session.toolUsed || "unknown";
410
+ if (!agentMap[tool]) agentMap[tool] = { count: 0, lastUsed: "", summaries: [] };
411
+ agentMap[tool].count++;
412
+ if (!agentMap[tool].lastUsed || (session.endedAt && session.endedAt > agentMap[tool].lastUsed)) {
413
+ agentMap[tool].lastUsed = session.endedAt || session.startedAt || "";
414
+ }
415
+ if (session.summary && agentMap[tool].summaries.length < 3) {
416
+ agentMap[tool].summaries.push(session.summary.substring(0, 80));
417
+ }
418
+ }
419
+
420
+ let agentTimeline = "";
421
+ if (Object.keys(agentMap).length > 0) {
422
+ agentTimeline = "\n\n## Multi-Agent Timeline\n" +
423
+ Object.entries(agentMap)
424
+ .map(([tool, info]) =>
425
+ `- **${tool}**: ${info.count} session(s), last active ${info.lastUsed ? info.lastUsed.substring(0, 16) : "unknown"}\n Recent: ${info.summaries.length > 0 ? info.summaries.map(s => `"${s}"`).join(", ") : "(no summaries)"}`
426
+ )
427
+ .join("\n");
428
+ }
429
+
430
+ const grade = score >= 80 ? "A" : score >= 60 ? "B" : score >= 40 ? "C" : score >= 20 ? "D" : "F";
431
+ const evtCount = brain.events?.count || 0;
432
+ const revertCount = (brain.state?.reverts || []).length;
433
+
434
+ return { content: [{ type: "text", text: `## SpecLock Health Check\n\nScore: **${score}/100** (Grade: ${grade})\nEvents: ${evtCount} | Reverts: ${revertCount}\n\n### Checks\n${checks.join("\n")}${agentTimeline}\n\n---\n*SpecLock v${VERSION} — Developed by ${AUTHOR}*` }] };
435
+ } catch (err) {
436
+ return { content: [{ type: "text", text: `## SpecLock Health Check\n\nError: ${err.message}\n\nTry running speclock_init first to initialize the project.\n\n---\n*SpecLock v${VERSION}*` }] };
437
+ }
350
438
  });
351
439
 
352
440
  // Tool 20: speclock_apply_template
@@ -612,7 +700,7 @@ app.get("/.well-known/mcp/server-card.json", (req, res) => {
612
700
  res.json({
613
701
  name: "SpecLock",
614
702
  version: VERSION,
615
- description: "AI Constraint Engine — memory + enforcement for AI coding tools. Policy-as-Code DSL, OAuth/OIDC SSO, admin dashboard, telemetry, API key auth, RBAC, AES-256-GCM encryption, hard enforcement, semantic pre-commit, HMAC audit chain, SOC 2/HIPAA compliance. 100% detection, 0% false positives. 31 MCP tools + CLI. Works with Claude Code, Cursor, Windsurf, Cline, Bolt.new, Lovable.",
703
+ description: "AI Constraint Engine — memory + enforcement for AI coding tools. Hybrid heuristic + Gemini LLM for universal domain coverage. Policy-as-Code DSL, OAuth/OIDC SSO, admin dashboard, telemetry, API key auth, RBAC, AES-256-GCM encryption, hard enforcement, semantic pre-commit, HMAC audit chain, SOC 2/HIPAA compliance. 31 MCP tools + CLI. Works with Claude Code, Cursor, Windsurf, Cline, Bolt.new, Lovable.",
616
704
  author: {
617
705
  name: "Sandeep Roy",
618
706
  url: "https://github.com/sgroy10",
package/src/mcp/server.js CHANGED
@@ -100,7 +100,7 @@ const PROJECT_ROOT =
100
100
  args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
101
101
 
102
102
  // --- MCP Server ---
103
- const VERSION = "4.3.0";
103
+ const VERSION = "4.3.2";
104
104
  const AUTHOR = "Sandeep Roy";
105
105
 
106
106
  const server = new McpServer(