speclock 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,730 @@
1
+ import os from "os";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import {
6
+ ensureInit,
7
+ setGoal,
8
+ addLock,
9
+ removeLock,
10
+ addDecision,
11
+ addNote,
12
+ updateDeployFacts,
13
+ logChange,
14
+ checkConflict,
15
+ getSessionBriefing,
16
+ endSession,
17
+ suggestLocks,
18
+ detectDrift,
19
+ } from "../core/engine.js";
20
+ import { generateContext, generateContextPack } from "../core/context.js";
21
+ import {
22
+ readBrain,
23
+ readEvents,
24
+ newId,
25
+ nowIso,
26
+ appendEvent,
27
+ bumpEvents,
28
+ writeBrain,
29
+ } from "../core/storage.js";
30
+ import {
31
+ captureStatus,
32
+ createTag,
33
+ getDiffSummary,
34
+ } from "../core/git.js";
35
+
36
+ // --- Project root resolution ---
37
+ function parseArgs(argv) {
38
+ const args = { project: null };
39
+ for (let i = 2; i < argv.length; i++) {
40
+ if (argv[i] === "--project" && argv[i + 1]) {
41
+ args.project = argv[i + 1];
42
+ i++;
43
+ }
44
+ }
45
+ return args;
46
+ }
47
+
48
+ const args = parseArgs(process.argv);
49
+ const PROJECT_ROOT =
50
+ args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
51
+
52
+ // --- MCP Server ---
53
+ const VERSION = "1.1.0";
54
+ const AUTHOR = "Sandeep Roy";
55
+
56
+ const server = new McpServer(
57
+ { name: "speclock", version: VERSION },
58
+ {
59
+ instructions:
60
+ `SpecLock is an AI continuity engine. Developed by ${AUTHOR}. Call speclock_session_briefing at the start of a new session, and speclock_session_summary before ending. Use speclock_get_context to refresh your project understanding at any time.`,
61
+ }
62
+ );
63
+
64
+ // ========================================
65
+ // MEMORY MANAGEMENT TOOLS
66
+ // ========================================
67
+
68
+ // Tool 1: speclock_init
69
+ server.tool(
70
+ "speclock_init",
71
+ "Initialize SpecLock in the current project directory. Creates .speclock/ with brain.json, events.log, and supporting directories.",
72
+ {},
73
+ async () => {
74
+ const brain = ensureInit(PROJECT_ROOT);
75
+ return {
76
+ content: [
77
+ {
78
+ type: "text",
79
+ text: `SpecLock initialized for "${brain.project.name}" at ${brain.project.root}`,
80
+ },
81
+ ],
82
+ };
83
+ }
84
+ );
85
+
86
+ // Tool 2: speclock_get_context — THE KEY TOOL
87
+ server.tool(
88
+ "speclock_get_context",
89
+ "THE KEY TOOL. Returns the full structured context pack including goal, locks, decisions, recent changes, deploy facts, reverts, session history, and notes. Call this at the start of every session or whenever you need to refresh your understanding of the project.",
90
+ {
91
+ format: z
92
+ .enum(["markdown", "json"])
93
+ .optional()
94
+ .default("markdown")
95
+ .describe("Output format: markdown (readable) or json (structured)"),
96
+ },
97
+ async ({ format }) => {
98
+ if (format === "json") {
99
+ const pack = generateContextPack(PROJECT_ROOT);
100
+ return {
101
+ content: [{ type: "text", text: JSON.stringify(pack, null, 2) }],
102
+ };
103
+ }
104
+ const md = generateContext(PROJECT_ROOT);
105
+ return { content: [{ type: "text", text: md }] };
106
+ }
107
+ );
108
+
109
+ // Tool 3: speclock_set_goal
110
+ server.tool(
111
+ "speclock_set_goal",
112
+ "Set or update the project goal. This is the high-level objective that guides all work.",
113
+ {
114
+ text: z.string().min(1).describe("The project goal text"),
115
+ },
116
+ async ({ text }) => {
117
+ setGoal(PROJECT_ROOT, text);
118
+ return {
119
+ content: [{ type: "text", text: `Goal set: "${text}"` }],
120
+ };
121
+ }
122
+ );
123
+
124
+ // Tool 4: speclock_add_lock
125
+ server.tool(
126
+ "speclock_add_lock",
127
+ "Add a non-negotiable constraint (SpecLock). These are rules that must NEVER be violated during development.",
128
+ {
129
+ text: z.string().min(1).describe("The constraint text"),
130
+ tags: z
131
+ .array(z.string())
132
+ .optional()
133
+ .default([])
134
+ .describe("Category tags"),
135
+ source: z
136
+ .enum(["user", "agent"])
137
+ .optional()
138
+ .default("agent")
139
+ .describe("Who created this lock"),
140
+ },
141
+ async ({ text, tags, source }) => {
142
+ const { lockId } = addLock(PROJECT_ROOT, text, tags, source);
143
+ return {
144
+ content: [
145
+ { type: "text", text: `Lock added (${lockId}): "${text}"` },
146
+ ],
147
+ };
148
+ }
149
+ );
150
+
151
+ // Tool 5: speclock_remove_lock
152
+ server.tool(
153
+ "speclock_remove_lock",
154
+ "Remove (deactivate) a SpecLock by its ID. The lock is soft-deleted and kept in history.",
155
+ {
156
+ lockId: z.string().min(1).describe("The lock ID to remove"),
157
+ },
158
+ async ({ lockId }) => {
159
+ const result = removeLock(PROJECT_ROOT, lockId);
160
+ if (!result.removed) {
161
+ return {
162
+ content: [{ type: "text", text: result.error }],
163
+ isError: true,
164
+ };
165
+ }
166
+ return {
167
+ content: [
168
+ {
169
+ type: "text",
170
+ text: `Lock removed: "${result.lockText}"`,
171
+ },
172
+ ],
173
+ };
174
+ }
175
+ );
176
+
177
+ // Tool 6: speclock_add_decision
178
+ server.tool(
179
+ "speclock_add_decision",
180
+ "Record an architectural or design decision. Decisions guide future work and prevent contradictory changes.",
181
+ {
182
+ text: z.string().min(1).describe("The decision text"),
183
+ tags: z.array(z.string()).optional().default([]),
184
+ source: z
185
+ .enum(["user", "agent"])
186
+ .optional()
187
+ .default("agent"),
188
+ },
189
+ async ({ text, tags, source }) => {
190
+ const { decId } = addDecision(PROJECT_ROOT, text, tags, source);
191
+ return {
192
+ content: [
193
+ { type: "text", text: `Decision recorded (${decId}): "${text}"` },
194
+ ],
195
+ };
196
+ }
197
+ );
198
+
199
+ // Tool 7: speclock_add_note
200
+ server.tool(
201
+ "speclock_add_note",
202
+ "Add a pinned note for reference. Notes persist across sessions as reminders.",
203
+ {
204
+ text: z.string().min(1).describe("The note text"),
205
+ pinned: z
206
+ .boolean()
207
+ .optional()
208
+ .default(true)
209
+ .describe("Whether to pin this note in context"),
210
+ },
211
+ async ({ text, pinned }) => {
212
+ const { noteId } = addNote(PROJECT_ROOT, text, pinned);
213
+ return {
214
+ content: [
215
+ { type: "text", text: `Note added (${noteId}): "${text}"` },
216
+ ],
217
+ };
218
+ }
219
+ );
220
+
221
+ // Tool 8: speclock_set_deploy_facts
222
+ server.tool(
223
+ "speclock_set_deploy_facts",
224
+ "Record deployment configuration facts (provider, branch, auto-deploy settings).",
225
+ {
226
+ provider: z
227
+ .string()
228
+ .optional()
229
+ .describe("Deploy provider (vercel, railway, aws, netlify, etc.)"),
230
+ branch: z.string().optional().describe("Deploy branch"),
231
+ autoDeploy: z
232
+ .boolean()
233
+ .optional()
234
+ .describe("Whether auto-deploy is enabled"),
235
+ url: z.string().optional().describe("Deployment URL"),
236
+ notes: z.string().optional().describe("Additional deploy notes"),
237
+ },
238
+ async (params) => {
239
+ updateDeployFacts(PROJECT_ROOT, params);
240
+ return {
241
+ content: [{ type: "text", text: "Deploy facts updated." }],
242
+ };
243
+ }
244
+ );
245
+
246
+ // ========================================
247
+ // CHANGE TRACKING TOOLS
248
+ // ========================================
249
+
250
+ // Tool 9: speclock_log_change
251
+ server.tool(
252
+ "speclock_log_change",
253
+ "Manually log a significant change. Use this when you make an important modification that should be tracked in the context.",
254
+ {
255
+ summary: z
256
+ .string()
257
+ .min(1)
258
+ .describe("Brief description of the change"),
259
+ files: z
260
+ .array(z.string())
261
+ .optional()
262
+ .default([])
263
+ .describe("Files affected"),
264
+ },
265
+ async ({ summary, files }) => {
266
+ const { eventId } = logChange(PROJECT_ROOT, summary, files);
267
+ return {
268
+ content: [
269
+ { type: "text", text: `Change logged (${eventId}): "${summary}"` },
270
+ ],
271
+ };
272
+ }
273
+ );
274
+
275
+ // Tool 10: speclock_get_changes
276
+ server.tool(
277
+ "speclock_get_changes",
278
+ "Get recent file changes tracked by SpecLock.",
279
+ {
280
+ limit: z
281
+ .number()
282
+ .int()
283
+ .min(1)
284
+ .max(100)
285
+ .optional()
286
+ .default(20),
287
+ },
288
+ async ({ limit }) => {
289
+ const brain = readBrain(PROJECT_ROOT);
290
+ if (!brain) {
291
+ return {
292
+ content: [
293
+ {
294
+ type: "text",
295
+ text: "SpecLock not initialized. Run speclock_init first.",
296
+ },
297
+ ],
298
+ isError: true,
299
+ };
300
+ }
301
+ const changes = brain.state.recentChanges.slice(0, limit);
302
+ if (changes.length === 0) {
303
+ return {
304
+ content: [{ type: "text", text: "No changes tracked yet." }],
305
+ };
306
+ }
307
+ const formatted = changes
308
+ .map((ch) => {
309
+ const files =
310
+ ch.files && ch.files.length > 0
311
+ ? ` (${ch.files.join(", ")})`
312
+ : "";
313
+ return `- [${ch.at.substring(0, 19)}] ${ch.summary}${files}`;
314
+ })
315
+ .join("\n");
316
+ return {
317
+ content: [
318
+ { type: "text", text: `Recent changes (${changes.length}):\n${formatted}` },
319
+ ],
320
+ };
321
+ }
322
+ );
323
+
324
+ // Tool 11: speclock_get_events
325
+ server.tool(
326
+ "speclock_get_events",
327
+ "Get the event log, optionally filtered by type. Event types: init, goal_updated, lock_added, lock_removed, decision_added, note_added, fact_updated, file_created, file_changed, file_deleted, revert_detected, context_generated, session_started, session_ended, manual_change, checkpoint_created.",
328
+ {
329
+ type: z.string().optional().describe("Filter by event type"),
330
+ limit: z
331
+ .number()
332
+ .int()
333
+ .min(1)
334
+ .max(200)
335
+ .optional()
336
+ .default(50),
337
+ since: z
338
+ .string()
339
+ .optional()
340
+ .describe("ISO timestamp; only return events after this time"),
341
+ },
342
+ async ({ type, limit, since }) => {
343
+ const events = readEvents(PROJECT_ROOT, { type, limit, since });
344
+ if (events.length === 0) {
345
+ return {
346
+ content: [{ type: "text", text: "No events found." }],
347
+ };
348
+ }
349
+ const formatted = events
350
+ .map(
351
+ (e) =>
352
+ `- [${e.at.substring(0, 19)}] ${e.type}: ${e.summary || ""}`
353
+ )
354
+ .join("\n");
355
+ return {
356
+ content: [
357
+ {
358
+ type: "text",
359
+ text: `Events (${events.length}):\n${formatted}`,
360
+ },
361
+ ],
362
+ };
363
+ }
364
+ );
365
+
366
+ // ========================================
367
+ // CONTINUITY PROTECTION TOOLS
368
+ // ========================================
369
+
370
+ // Tool 12: speclock_check_conflict
371
+ server.tool(
372
+ "speclock_check_conflict",
373
+ "Check if a proposed action conflicts with any active SpecLock. Use before making significant changes.",
374
+ {
375
+ proposedAction: z
376
+ .string()
377
+ .min(1)
378
+ .describe("Description of the action you plan to take"),
379
+ },
380
+ async ({ proposedAction }) => {
381
+ const result = checkConflict(PROJECT_ROOT, proposedAction);
382
+ return {
383
+ content: [{ type: "text", text: result.analysis }],
384
+ };
385
+ }
386
+ );
387
+
388
+ // Tool 13: speclock_session_briefing
389
+ server.tool(
390
+ "speclock_session_briefing",
391
+ "Start a new session and get a full briefing. Returns context pack plus what happened since the last session. Call this at the very beginning of a new conversation.",
392
+ {
393
+ toolName: z
394
+ .enum(["claude-code", "cursor", "codex", "windsurf", "cline", "unknown"])
395
+ .optional()
396
+ .default("unknown")
397
+ .describe("Which AI tool is being used"),
398
+ },
399
+ async ({ toolName }) => {
400
+ const briefing = getSessionBriefing(PROJECT_ROOT, toolName);
401
+ const contextMd = generateContext(PROJECT_ROOT);
402
+
403
+ const parts = [];
404
+
405
+ // Session info
406
+ parts.push(`# SpecLock Session Briefing`);
407
+ parts.push(`Session started (${toolName}). ID: ${briefing.session.id}`);
408
+ parts.push("");
409
+
410
+ // Last session summary
411
+ if (briefing.lastSession) {
412
+ parts.push("## Last Session");
413
+ parts.push(`- Tool: **${briefing.lastSession.toolUsed}**`);
414
+ parts.push(`- Ended: ${briefing.lastSession.endedAt || "unknown"}`);
415
+ if (briefing.lastSession.summary)
416
+ parts.push(`- Summary: ${briefing.lastSession.summary}`);
417
+ parts.push(
418
+ `- Events: ${briefing.lastSession.eventsInSession || 0}`
419
+ );
420
+ parts.push(
421
+ `- Changes since then: ${briefing.changesSinceLastSession}`
422
+ );
423
+ parts.push("");
424
+ }
425
+
426
+ // Warnings
427
+ if (briefing.warnings.length > 0) {
428
+ parts.push("## ⚠ Warnings");
429
+ for (const w of briefing.warnings) {
430
+ parts.push(`- ${w}`);
431
+ }
432
+ parts.push("");
433
+ }
434
+
435
+ // Full context
436
+ parts.push("---");
437
+ parts.push(contextMd);
438
+
439
+ return {
440
+ content: [{ type: "text", text: parts.join("\n") }],
441
+ };
442
+ }
443
+ );
444
+
445
+ // Tool 14: speclock_session_summary
446
+ server.tool(
447
+ "speclock_session_summary",
448
+ "End the current session and record what was accomplished. Call this before ending a conversation.",
449
+ {
450
+ summary: z
451
+ .string()
452
+ .min(1)
453
+ .describe("Summary of what was accomplished in this session"),
454
+ },
455
+ async ({ summary }) => {
456
+ const result = endSession(PROJECT_ROOT, summary);
457
+ if (!result.ended) {
458
+ return {
459
+ content: [{ type: "text", text: result.error }],
460
+ isError: true,
461
+ };
462
+ }
463
+ const session = result.session;
464
+ const duration =
465
+ session.startedAt && session.endedAt
466
+ ? Math.round(
467
+ (new Date(session.endedAt) - new Date(session.startedAt)) /
468
+ 60000
469
+ )
470
+ : 0;
471
+ return {
472
+ content: [
473
+ {
474
+ type: "text",
475
+ text: `Session ended. Duration: ${duration} min. Events: ${session.eventsInSession}. Summary: "${summary}"`,
476
+ },
477
+ ],
478
+ };
479
+ }
480
+ );
481
+
482
+ // ========================================
483
+ // GIT INTEGRATION TOOLS
484
+ // ========================================
485
+
486
+ // Tool 15: speclock_checkpoint
487
+ server.tool(
488
+ "speclock_checkpoint",
489
+ "Create a named git tag checkpoint for easy rollback.",
490
+ {
491
+ name: z
492
+ .string()
493
+ .min(1)
494
+ .describe("Checkpoint name (alphanumeric, hyphens, underscores)"),
495
+ },
496
+ async ({ name }) => {
497
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "_");
498
+ const tag = `sl_${safeName}_${Date.now()}`;
499
+ const result = createTag(PROJECT_ROOT, tag);
500
+
501
+ if (!result.ok) {
502
+ return {
503
+ content: [
504
+ {
505
+ type: "text",
506
+ text: `Failed to create checkpoint: ${result.error}`,
507
+ },
508
+ ],
509
+ isError: true,
510
+ };
511
+ }
512
+
513
+ // Record event
514
+ const brain = readBrain(PROJECT_ROOT);
515
+ if (brain) {
516
+ const eventId = newId("evt");
517
+ const event = {
518
+ eventId,
519
+ type: "checkpoint_created",
520
+ at: nowIso(),
521
+ files: [],
522
+ summary: `Checkpoint: ${tag}`,
523
+ patchPath: "",
524
+ };
525
+ bumpEvents(brain, eventId);
526
+ appendEvent(PROJECT_ROOT, event);
527
+ writeBrain(PROJECT_ROOT, brain);
528
+ }
529
+
530
+ return {
531
+ content: [
532
+ { type: "text", text: `Checkpoint created: ${tag}` },
533
+ ],
534
+ };
535
+ }
536
+ );
537
+
538
+ // Tool 16: speclock_repo_status
539
+ server.tool(
540
+ "speclock_repo_status",
541
+ "Get current git repository status including branch, commit, changed files, and diff summary.",
542
+ {},
543
+ async () => {
544
+ const status = captureStatus(PROJECT_ROOT);
545
+ const diffSummary = getDiffSummary(PROJECT_ROOT);
546
+
547
+ const lines = [
548
+ `Branch: ${status.branch}`,
549
+ `Commit: ${status.commit}`,
550
+ "",
551
+ `Changed files (${status.changedFiles.length}):`,
552
+ ];
553
+
554
+ if (status.changedFiles.length > 0) {
555
+ for (const f of status.changedFiles) {
556
+ lines.push(` ${f.status} ${f.file}`);
557
+ }
558
+ } else {
559
+ lines.push(" (clean working tree)");
560
+ }
561
+
562
+ if (diffSummary) {
563
+ lines.push("");
564
+ lines.push("Diff summary:");
565
+ lines.push(diffSummary);
566
+ }
567
+
568
+ return {
569
+ content: [{ type: "text", text: lines.join("\n") }],
570
+ };
571
+ }
572
+ );
573
+
574
+ // ========================================
575
+ // INTELLIGENCE TOOLS
576
+ // ========================================
577
+
578
+ // Tool 17: speclock_suggest_locks
579
+ server.tool(
580
+ "speclock_suggest_locks",
581
+ "Analyze project decisions, notes, and patterns to suggest new SpecLock constraints. Returns auto-generated lock suggestions based on commitment language, prohibitive patterns, and common best practices.",
582
+ {},
583
+ async () => {
584
+ const result = suggestLocks(PROJECT_ROOT);
585
+
586
+ if (result.suggestions.length === 0) {
587
+ return {
588
+ content: [
589
+ {
590
+ type: "text",
591
+ text: `No suggestions at this time. You have ${result.totalLocks} active lock(s). Add more decisions and notes to get AI-powered lock suggestions.`,
592
+ },
593
+ ],
594
+ };
595
+ }
596
+
597
+ const formatted = result.suggestions
598
+ .map(
599
+ (s, i) =>
600
+ `${i + 1}. **"${s.text}"**\n Source: ${s.source}${s.sourceId ? ` (${s.sourceId})` : ""}\n Reason: ${s.reason}`
601
+ )
602
+ .join("\n\n");
603
+
604
+ return {
605
+ content: [
606
+ {
607
+ type: "text",
608
+ text: `## Lock Suggestions (${result.suggestions.length})\n\nCurrent active locks: ${result.totalLocks}\n\n${formatted}\n\nTo add any suggestion as a lock, call \`speclock_add_lock\` with the text.`,
609
+ },
610
+ ],
611
+ };
612
+ }
613
+ );
614
+
615
+ // Tool 18: speclock_detect_drift
616
+ server.tool(
617
+ "speclock_detect_drift",
618
+ "Scan recent changes and events against active SpecLock constraints to detect potential violations or drift. Use this proactively to ensure project integrity.",
619
+ {},
620
+ async () => {
621
+ const result = detectDrift(PROJECT_ROOT);
622
+
623
+ if (result.status === "no_locks") {
624
+ return {
625
+ content: [{ type: "text", text: result.message }],
626
+ };
627
+ }
628
+
629
+ if (result.status === "clean") {
630
+ return {
631
+ content: [{ type: "text", text: result.message }],
632
+ };
633
+ }
634
+
635
+ const formatted = result.drifts
636
+ .map(
637
+ (d) =>
638
+ `- [${d.severity.toUpperCase()}] Change: "${d.changeSummary}" (${d.changeAt.substring(0, 19)})\n Lock: "${d.lockText}"\n Matched: ${d.matchedTerms.join(", ")}`
639
+ )
640
+ .join("\n\n");
641
+
642
+ return {
643
+ content: [
644
+ {
645
+ type: "text",
646
+ text: `## Drift Report\n\n${result.message}\n\n${formatted}\n\nReview each drift and take corrective action if needed.`,
647
+ },
648
+ ],
649
+ };
650
+ }
651
+ );
652
+
653
+ // Tool 19: speclock_health
654
+ server.tool(
655
+ "speclock_health",
656
+ "Get a health check of the SpecLock setup including completeness score, missing recommended items, and multi-agent session timeline.",
657
+ {},
658
+ async () => {
659
+ const brain = ensureInit(PROJECT_ROOT);
660
+ const activeLocks = brain.specLock.items.filter((l) => l.active !== false);
661
+
662
+ // Calculate health score
663
+ let score = 0;
664
+ const checks = [];
665
+
666
+ if (brain.goal.text) { score += 20; checks.push("[PASS] Goal is set"); }
667
+ else checks.push("[MISS] No project goal set");
668
+
669
+ if (activeLocks.length > 0) { score += 25; checks.push(`[PASS] ${activeLocks.length} active lock(s)`); }
670
+ else checks.push("[MISS] No SpecLock constraints defined");
671
+
672
+ if (brain.decisions.length > 0) { score += 15; checks.push(`[PASS] ${brain.decisions.length} decision(s) recorded`); }
673
+ else checks.push("[MISS] No decisions recorded");
674
+
675
+ if (brain.notes.length > 0) { score += 10; checks.push(`[PASS] ${brain.notes.length} note(s)`); }
676
+ else checks.push("[MISS] No notes added");
677
+
678
+ if (brain.sessions.history.length > 0) { score += 15; checks.push(`[PASS] ${brain.sessions.history.length} session(s) in history`); }
679
+ else checks.push("[MISS] No session history yet");
680
+
681
+ if (brain.state.recentChanges.length > 0) { score += 10; checks.push(`[PASS] ${brain.state.recentChanges.length} change(s) tracked`); }
682
+ else checks.push("[MISS] No changes tracked");
683
+
684
+ if (brain.facts.deploy.provider !== "unknown") { score += 5; checks.push("[PASS] Deploy facts configured"); }
685
+ else checks.push("[MISS] Deploy facts not configured");
686
+
687
+ // Multi-agent timeline
688
+ const agentMap = {};
689
+ for (const session of brain.sessions.history) {
690
+ const tool = session.toolUsed || "unknown";
691
+ if (!agentMap[tool]) agentMap[tool] = { count: 0, lastUsed: "", summaries: [] };
692
+ agentMap[tool].count++;
693
+ if (!agentMap[tool].lastUsed || session.endedAt > agentMap[tool].lastUsed) {
694
+ agentMap[tool].lastUsed = session.endedAt || session.startedAt;
695
+ }
696
+ if (session.summary && agentMap[tool].summaries.length < 3) {
697
+ agentMap[tool].summaries.push(session.summary.substring(0, 80));
698
+ }
699
+ }
700
+
701
+ let agentTimeline = "";
702
+ if (Object.keys(agentMap).length > 0) {
703
+ agentTimeline = "\n\n## Multi-Agent Timeline\n" +
704
+ Object.entries(agentMap)
705
+ .map(([tool, info]) =>
706
+ `- **${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)"}`
707
+ )
708
+ .join("\n");
709
+ }
710
+
711
+ const grade = score >= 80 ? "A" : score >= 60 ? "B" : score >= 40 ? "C" : score >= 20 ? "D" : "F";
712
+
713
+ return {
714
+ content: [
715
+ {
716
+ type: "text",
717
+ 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}*`,
718
+ },
719
+ ],
720
+ };
721
+ }
722
+ );
723
+
724
+ // --- Start server ---
725
+ const transport = new StdioServerTransport();
726
+ await server.connect(transport);
727
+
728
+ process.stderr.write(
729
+ `SpecLock MCP v${VERSION} running (stdio) — Developed by ${AUTHOR}. Root: ${PROJECT_ROOT}${os.EOL}`
730
+ );