@vibecheckai/cli 3.2.6 → 3.3.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.
Files changed (84) hide show
  1. package/bin/registry.js +192 -5
  2. package/bin/runners/lib/agent-firewall/change-packet/builder.js +280 -6
  3. package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
  4. package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
  5. package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
  6. package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
  7. package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
  8. package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
  9. package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
  10. package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
  11. package/bin/runners/lib/agent-firewall/logger.js +141 -0
  12. package/bin/runners/lib/agent-firewall/policy/loader.js +312 -4
  13. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +113 -1
  14. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +133 -6
  15. package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
  16. package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
  17. package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
  18. package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
  19. package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
  20. package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
  21. package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
  22. package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
  23. package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
  24. package/bin/runners/lib/agent-firewall/risk/thresholds.js +321 -0
  25. package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
  26. package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
  27. package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
  28. package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
  29. package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
  30. package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
  31. package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
  32. package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
  33. package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
  34. package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
  35. package/bin/runners/lib/analyzers.js +81 -18
  36. package/bin/runners/lib/authority-badge.js +425 -0
  37. package/bin/runners/lib/cli-output.js +7 -1
  38. package/bin/runners/lib/error-handler.js +16 -9
  39. package/bin/runners/lib/exit-codes.js +275 -0
  40. package/bin/runners/lib/global-flags.js +37 -0
  41. package/bin/runners/lib/help-formatter.js +413 -0
  42. package/bin/runners/lib/logger.js +38 -0
  43. package/bin/runners/lib/unified-cli-output.js +604 -0
  44. package/bin/runners/lib/upsell.js +148 -0
  45. package/bin/runners/runApprove.js +1200 -0
  46. package/bin/runners/runAuth.js +324 -95
  47. package/bin/runners/runCheckpoint.js +39 -21
  48. package/bin/runners/runClassify.js +859 -0
  49. package/bin/runners/runContext.js +136 -24
  50. package/bin/runners/runDoctor.js +108 -68
  51. package/bin/runners/runFix.js +6 -5
  52. package/bin/runners/runGuard.js +212 -118
  53. package/bin/runners/runInit.js +3 -2
  54. package/bin/runners/runMcp.js +130 -52
  55. package/bin/runners/runPolish.js +43 -20
  56. package/bin/runners/runProve.js +1 -2
  57. package/bin/runners/runReport.js +3 -2
  58. package/bin/runners/runScan.js +63 -44
  59. package/bin/runners/runShip.js +3 -4
  60. package/bin/runners/runValidate.js +19 -2
  61. package/bin/runners/runWatch.js +104 -53
  62. package/bin/vibecheck.js +106 -19
  63. package/mcp-server/HARDENING_SUMMARY.md +299 -0
  64. package/mcp-server/agent-firewall-interceptor.js +367 -31
  65. package/mcp-server/authority-tools.js +569 -0
  66. package/mcp-server/conductor/conflict-resolver.js +588 -0
  67. package/mcp-server/conductor/execution-planner.js +544 -0
  68. package/mcp-server/conductor/index.js +377 -0
  69. package/mcp-server/conductor/lock-manager.js +615 -0
  70. package/mcp-server/conductor/request-queue.js +550 -0
  71. package/mcp-server/conductor/session-manager.js +500 -0
  72. package/mcp-server/conductor/tools.js +510 -0
  73. package/mcp-server/index.js +1149 -243
  74. package/mcp-server/lib/{api-client.js → api-client.cjs} +40 -4
  75. package/mcp-server/lib/logger.cjs +30 -0
  76. package/mcp-server/logger.js +173 -0
  77. package/mcp-server/package.json +2 -2
  78. package/mcp-server/premium-tools.js +2 -2
  79. package/mcp-server/tier-auth.js +245 -35
  80. package/mcp-server/truth-firewall-tools.js +145 -15
  81. package/mcp-server/vibecheck-tools.js +2 -2
  82. package/package.json +2 -3
  83. package/mcp-server/index.old.js +0 -4137
  84. package/mcp-server/package-lock.json +0 -165
@@ -0,0 +1,615 @@
1
+ /**
2
+ * Conductor Lock Manager
3
+ *
4
+ * Manages file-level and folder-level locks for multi-agent coordination.
5
+ * Prevents concurrent modifications and detects deadlocks.
6
+ *
7
+ * Codename: Conductor
8
+ */
9
+
10
+ "use strict";
11
+
12
+ import fs from "fs";
13
+ import path from "path";
14
+ import crypto from "crypto";
15
+ import { conductorLogger as log, getErrorMessage } from "../logger.js";
16
+
17
+ /**
18
+ * @typedef {Object} Lock
19
+ * @property {string} lockId - Unique lock ID
20
+ * @property {string} path - Locked path (file or folder)
21
+ * @property {string} type - Lock type (exclusive, shared)
22
+ * @property {string} sessionId - Owning session ID
23
+ * @property {string} agentId - Owning agent ID
24
+ * @property {Date} acquiredAt - When lock was acquired
25
+ * @property {Date} expiresAt - When lock expires
26
+ * @property {string} reason - Reason for lock
27
+ */
28
+
29
+ /**
30
+ * Lock types
31
+ */
32
+ const LOCK_TYPES = {
33
+ EXCLUSIVE: "exclusive", // Write lock - only one holder
34
+ SHARED: "shared", // Read lock - multiple holders allowed
35
+ };
36
+
37
+ /**
38
+ * Default lock timeout (5 minutes)
39
+ */
40
+ const DEFAULT_LOCK_TIMEOUT_MS = 5 * 60 * 1000;
41
+
42
+ /**
43
+ * Lock Manager class
44
+ */
45
+ class LockManager {
46
+ constructor(options = {}) {
47
+ this.locks = new Map(); // lockId -> Lock
48
+ this.pathLocks = new Map(); // normalizedPath -> Set<lockId>
49
+ this.sessionLocks = new Map(); // sessionId -> Set<lockId>
50
+ this.lockTimeout = options.lockTimeout || DEFAULT_LOCK_TIMEOUT_MS;
51
+ this.persistPath = options.persistPath || null;
52
+
53
+ // Cleanup interval
54
+ this.cleanupInterval = setInterval(() => {
55
+ this.cleanupExpiredLocks();
56
+ }, 30000); // Check every 30 seconds
57
+
58
+ // Load persisted state
59
+ if (this.persistPath) {
60
+ this.loadState();
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Generate a unique lock ID
66
+ * @returns {string} Lock ID
67
+ */
68
+ generateLockId() {
69
+ return `lock_${crypto.randomBytes(8).toString("hex")}`;
70
+ }
71
+
72
+ /**
73
+ * Normalize a file path for consistent comparison
74
+ * @param {string} filePath - Path to normalize
75
+ * @returns {string} Normalized path
76
+ */
77
+ normalizePath(filePath) {
78
+ return path.resolve(filePath).replace(/\\/g, "/").toLowerCase();
79
+ }
80
+
81
+ /**
82
+ * Check if a path is a parent of another
83
+ * @param {string} parent - Potential parent path
84
+ * @param {string} child - Potential child path
85
+ * @returns {boolean} Is parent
86
+ */
87
+ isParentPath(parent, child) {
88
+ const normalizedParent = this.normalizePath(parent);
89
+ const normalizedChild = this.normalizePath(child);
90
+
91
+ if (normalizedParent === normalizedChild) return true;
92
+
93
+ return normalizedChild.startsWith(normalizedParent + "/");
94
+ }
95
+
96
+ /**
97
+ * Get all locks that conflict with a path
98
+ * @param {string} filePath - Path to check
99
+ * @param {string} lockType - Type of lock being requested
100
+ * @returns {Lock[]} Conflicting locks
101
+ */
102
+ getConflictingLocks(filePath, lockType) {
103
+ const normalizedPath = this.normalizePath(filePath);
104
+ const conflicts = [];
105
+
106
+ for (const lock of this.locks.values()) {
107
+ // Check if lock is expired
108
+ if (this.isLockExpired(lock)) continue;
109
+
110
+ const lockPath = this.normalizePath(lock.path);
111
+
112
+ // Check for path overlap
113
+ const pathOverlap = this.isParentPath(lockPath, normalizedPath) ||
114
+ this.isParentPath(normalizedPath, lockPath);
115
+
116
+ if (!pathOverlap) continue;
117
+
118
+ // Shared locks don't conflict with other shared locks
119
+ if (lockType === LOCK_TYPES.SHARED && lock.type === LOCK_TYPES.SHARED) {
120
+ continue;
121
+ }
122
+
123
+ conflicts.push(lock);
124
+ }
125
+
126
+ return conflicts;
127
+ }
128
+
129
+ /**
130
+ * Acquire a lock
131
+ * @param {Object} params - Lock parameters
132
+ * @returns {Object} Result with lock or conflict info
133
+ */
134
+ acquireLock({
135
+ path: filePath,
136
+ type = LOCK_TYPES.EXCLUSIVE,
137
+ sessionId,
138
+ agentId,
139
+ reason = "",
140
+ timeout = null,
141
+ }) {
142
+ const normalizedPath = this.normalizePath(filePath);
143
+
144
+ // Check for conflicting locks
145
+ const conflicts = this.getConflictingLocks(filePath, type);
146
+
147
+ // Filter out locks owned by the same session
148
+ const externalConflicts = conflicts.filter(l => l.sessionId !== sessionId);
149
+
150
+ if (externalConflicts.length > 0) {
151
+ return {
152
+ acquired: false,
153
+ conflict: true,
154
+ conflictingLocks: externalConflicts,
155
+ message: `Path is locked by ${externalConflicts.length} other session(s)`,
156
+ };
157
+ }
158
+
159
+ // Check if this session already has a lock on this path
160
+ const existingLock = this.getSessionLockForPath(sessionId, filePath);
161
+ if (existingLock) {
162
+ // Upgrade lock if needed (shared -> exclusive)
163
+ if (existingLock.type === LOCK_TYPES.SHARED && type === LOCK_TYPES.EXCLUSIVE) {
164
+ existingLock.type = LOCK_TYPES.EXCLUSIVE;
165
+ existingLock.expiresAt = new Date(Date.now() + (timeout || this.lockTimeout));
166
+ this.saveState();
167
+ return {
168
+ acquired: true,
169
+ upgraded: true,
170
+ lock: existingLock,
171
+ };
172
+ }
173
+
174
+ // Refresh existing lock
175
+ existingLock.expiresAt = new Date(Date.now() + (timeout || this.lockTimeout));
176
+ this.saveState();
177
+ return {
178
+ acquired: true,
179
+ refreshed: true,
180
+ lock: existingLock,
181
+ };
182
+ }
183
+
184
+ // Create new lock
185
+ const lockId = this.generateLockId();
186
+ const now = new Date();
187
+
188
+ const lock = {
189
+ lockId,
190
+ path: normalizedPath,
191
+ type,
192
+ sessionId,
193
+ agentId,
194
+ acquiredAt: now,
195
+ expiresAt: new Date(now.getTime() + (timeout || this.lockTimeout)),
196
+ reason,
197
+ };
198
+
199
+ // Store lock
200
+ this.locks.set(lockId, lock);
201
+
202
+ // Index by path
203
+ if (!this.pathLocks.has(normalizedPath)) {
204
+ this.pathLocks.set(normalizedPath, new Set());
205
+ }
206
+ this.pathLocks.get(normalizedPath).add(lockId);
207
+
208
+ // Index by session
209
+ if (!this.sessionLocks.has(sessionId)) {
210
+ this.sessionLocks.set(sessionId, new Set());
211
+ }
212
+ this.sessionLocks.get(sessionId).add(lockId);
213
+
214
+ // Persist state
215
+ this.saveState();
216
+
217
+ return {
218
+ acquired: true,
219
+ lock,
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Release a lock
225
+ * @param {string} lockId - Lock ID to release
226
+ * @param {string} sessionId - Session ID (for validation)
227
+ * @returns {boolean} Success
228
+ */
229
+ releaseLock(lockId, sessionId = null) {
230
+ const lock = this.locks.get(lockId);
231
+ if (!lock) return false;
232
+
233
+ // Validate session if provided
234
+ if (sessionId && lock.sessionId !== sessionId) {
235
+ return false;
236
+ }
237
+
238
+ // Remove from locks
239
+ this.locks.delete(lockId);
240
+
241
+ // Remove from path index
242
+ const pathLocks = this.pathLocks.get(lock.path);
243
+ if (pathLocks) {
244
+ pathLocks.delete(lockId);
245
+ if (pathLocks.size === 0) {
246
+ this.pathLocks.delete(lock.path);
247
+ }
248
+ }
249
+
250
+ // Remove from session index
251
+ const sessionLocks = this.sessionLocks.get(lock.sessionId);
252
+ if (sessionLocks) {
253
+ sessionLocks.delete(lockId);
254
+ if (sessionLocks.size === 0) {
255
+ this.sessionLocks.delete(lock.sessionId);
256
+ }
257
+ }
258
+
259
+ // Persist state
260
+ this.saveState();
261
+
262
+ return true;
263
+ }
264
+
265
+ /**
266
+ * Release all locks for a session
267
+ * @param {string} sessionId - Session ID
268
+ * @returns {number} Number of locks released
269
+ */
270
+ releaseSessionLocks(sessionId) {
271
+ const sessionLockIds = this.sessionLocks.get(sessionId);
272
+ if (!sessionLockIds) return 0;
273
+
274
+ const lockIds = Array.from(sessionLockIds);
275
+ let released = 0;
276
+
277
+ for (const lockId of lockIds) {
278
+ if (this.releaseLock(lockId)) {
279
+ released++;
280
+ }
281
+ }
282
+
283
+ return released;
284
+ }
285
+
286
+ /**
287
+ * Get a session's lock for a specific path
288
+ * @param {string} sessionId - Session ID
289
+ * @param {string} filePath - File path
290
+ * @returns {Lock|null} Lock or null
291
+ */
292
+ getSessionLockForPath(sessionId, filePath) {
293
+ const sessionLockIds = this.sessionLocks.get(sessionId);
294
+ if (!sessionLockIds) return null;
295
+
296
+ const normalizedPath = this.normalizePath(filePath);
297
+
298
+ for (const lockId of sessionLockIds) {
299
+ const lock = this.locks.get(lockId);
300
+ if (lock && lock.path === normalizedPath && !this.isLockExpired(lock)) {
301
+ return lock;
302
+ }
303
+ }
304
+
305
+ return null;
306
+ }
307
+
308
+ /**
309
+ * Get all locks for a path
310
+ * @param {string} filePath - File path
311
+ * @returns {Lock[]} Locks
312
+ */
313
+ getLocksForPath(filePath) {
314
+ const normalizedPath = this.normalizePath(filePath);
315
+ const lockIds = this.pathLocks.get(normalizedPath);
316
+ if (!lockIds) return [];
317
+
318
+ const locks = [];
319
+ for (const lockId of lockIds) {
320
+ const lock = this.locks.get(lockId);
321
+ if (lock && !this.isLockExpired(lock)) {
322
+ locks.push(lock);
323
+ }
324
+ }
325
+
326
+ return locks;
327
+ }
328
+
329
+ /**
330
+ * Get all locks for a session
331
+ * @param {string} sessionId - Session ID
332
+ * @returns {Lock[]} Locks
333
+ */
334
+ getSessionLocks(sessionId) {
335
+ const lockIds = this.sessionLocks.get(sessionId);
336
+ if (!lockIds) return [];
337
+
338
+ const locks = [];
339
+ for (const lockId of lockIds) {
340
+ const lock = this.locks.get(lockId);
341
+ if (lock && !this.isLockExpired(lock)) {
342
+ locks.push(lock);
343
+ }
344
+ }
345
+
346
+ return locks;
347
+ }
348
+
349
+ /**
350
+ * Check if a lock is expired
351
+ * @param {Lock} lock - Lock to check
352
+ * @returns {boolean} Is expired
353
+ */
354
+ isLockExpired(lock) {
355
+ return new Date(lock.expiresAt).getTime() < Date.now();
356
+ }
357
+
358
+ /**
359
+ * Refresh a lock's expiration
360
+ * @param {string} lockId - Lock ID
361
+ * @param {string} sessionId - Session ID (for validation)
362
+ * @returns {Lock|null} Refreshed lock or null
363
+ */
364
+ refreshLock(lockId, sessionId = null) {
365
+ const lock = this.locks.get(lockId);
366
+ if (!lock) return null;
367
+
368
+ if (sessionId && lock.sessionId !== sessionId) {
369
+ return null;
370
+ }
371
+
372
+ lock.expiresAt = new Date(Date.now() + this.lockTimeout);
373
+ this.saveState();
374
+
375
+ return lock;
376
+ }
377
+
378
+ /**
379
+ * Detect potential deadlocks
380
+ * @returns {Object[]} Potential deadlock situations
381
+ */
382
+ detectDeadlocks() {
383
+ const deadlocks = [];
384
+ const sessionDeps = new Map(); // sessionId -> Set<sessionId> (waiting on)
385
+
386
+ // Build dependency graph
387
+ for (const lock of this.locks.values()) {
388
+ if (this.isLockExpired(lock)) continue;
389
+
390
+ // Find sessions waiting for this lock
391
+ // (This is a simplified detection - in practice, you'd track actual wait queues)
392
+ const conflicts = this.getConflictingLocks(lock.path, LOCK_TYPES.EXCLUSIVE);
393
+
394
+ for (const conflict of conflicts) {
395
+ if (conflict.sessionId !== lock.sessionId) {
396
+ if (!sessionDeps.has(lock.sessionId)) {
397
+ sessionDeps.set(lock.sessionId, new Set());
398
+ }
399
+ sessionDeps.get(lock.sessionId).add(conflict.sessionId);
400
+ }
401
+ }
402
+ }
403
+
404
+ // Detect cycles using DFS
405
+ const visited = new Set();
406
+ const inStack = new Set();
407
+
408
+ const dfs = (sessionId, path) => {
409
+ if (inStack.has(sessionId)) {
410
+ // Found cycle
411
+ const cycleStart = path.indexOf(sessionId);
412
+ deadlocks.push({
413
+ type: "cycle",
414
+ sessions: path.slice(cycleStart),
415
+ });
416
+ return;
417
+ }
418
+
419
+ if (visited.has(sessionId)) return;
420
+
421
+ visited.add(sessionId);
422
+ inStack.add(sessionId);
423
+ path.push(sessionId);
424
+
425
+ const deps = sessionDeps.get(sessionId);
426
+ if (deps) {
427
+ for (const dep of deps) {
428
+ dfs(dep, [...path]);
429
+ }
430
+ }
431
+
432
+ inStack.delete(sessionId);
433
+ };
434
+
435
+ for (const sessionId of sessionDeps.keys()) {
436
+ if (!visited.has(sessionId)) {
437
+ dfs(sessionId, []);
438
+ }
439
+ }
440
+
441
+ return deadlocks;
442
+ }
443
+
444
+ /**
445
+ * Force release a lock (admin operation)
446
+ * @param {string} lockId - Lock ID
447
+ * @param {string} reason - Reason for force release
448
+ * @returns {boolean} Success
449
+ */
450
+ forceReleaseLock(lockId, reason = "Admin force release") {
451
+ const lock = this.locks.get(lockId);
452
+ if (!lock) return false;
453
+
454
+ log.warn(`Force releasing lock ${lockId}: ${reason}`);
455
+
456
+ return this.releaseLock(lockId);
457
+ }
458
+
459
+ /**
460
+ * Cleanup expired locks
461
+ */
462
+ cleanupExpiredLocks() {
463
+ const expiredIds = [];
464
+
465
+ for (const [lockId, lock] of this.locks) {
466
+ if (this.isLockExpired(lock)) {
467
+ expiredIds.push(lockId);
468
+ }
469
+ }
470
+
471
+ for (const lockId of expiredIds) {
472
+ this.releaseLock(lockId);
473
+ }
474
+
475
+ if (expiredIds.length > 0) {
476
+ log.info(`Cleaned up ${expiredIds.length} expired locks`);
477
+ }
478
+ }
479
+
480
+ /**
481
+ * Get lock statistics
482
+ * @returns {Object} Statistics
483
+ */
484
+ getStatistics() {
485
+ let exclusiveCount = 0;
486
+ let sharedCount = 0;
487
+ let expiredCount = 0;
488
+
489
+ for (const lock of this.locks.values()) {
490
+ if (this.isLockExpired(lock)) {
491
+ expiredCount++;
492
+ } else if (lock.type === LOCK_TYPES.EXCLUSIVE) {
493
+ exclusiveCount++;
494
+ } else {
495
+ sharedCount++;
496
+ }
497
+ }
498
+
499
+ return {
500
+ totalLocks: this.locks.size,
501
+ exclusiveLocks: exclusiveCount,
502
+ sharedLocks: sharedCount,
503
+ expiredLocks: expiredCount,
504
+ lockedPaths: this.pathLocks.size,
505
+ sessionsWithLocks: this.sessionLocks.size,
506
+ };
507
+ }
508
+
509
+ /**
510
+ * Save state to disk
511
+ */
512
+ saveState() {
513
+ if (!this.persistPath) return;
514
+
515
+ try {
516
+ const dir = path.dirname(this.persistPath);
517
+ if (!fs.existsSync(dir)) {
518
+ fs.mkdirSync(dir, { recursive: true });
519
+ }
520
+
521
+ const state = {
522
+ locks: Array.from(this.locks.entries()),
523
+ timestamp: new Date().toISOString(),
524
+ };
525
+
526
+ fs.writeFileSync(this.persistPath, JSON.stringify(state, null, 2));
527
+ } catch (error) {
528
+ log.warn(`Failed to save lock state: ${getErrorMessage(error)}`);
529
+ }
530
+ }
531
+
532
+ /**
533
+ * Load state from disk
534
+ */
535
+ loadState() {
536
+ if (!this.persistPath || !fs.existsSync(this.persistPath)) return;
537
+
538
+ try {
539
+ const content = fs.readFileSync(this.persistPath, "utf-8");
540
+ const state = JSON.parse(content);
541
+
542
+ for (const [lockId, lock] of state.locks || []) {
543
+ // Convert date strings
544
+ lock.acquiredAt = new Date(lock.acquiredAt);
545
+ lock.expiresAt = new Date(lock.expiresAt);
546
+
547
+ // Only restore non-expired locks
548
+ if (!this.isLockExpired(lock)) {
549
+ this.locks.set(lockId, lock);
550
+
551
+ // Rebuild path index
552
+ if (!this.pathLocks.has(lock.path)) {
553
+ this.pathLocks.set(lock.path, new Set());
554
+ }
555
+ this.pathLocks.get(lock.path).add(lockId);
556
+
557
+ // Rebuild session index
558
+ if (!this.sessionLocks.has(lock.sessionId)) {
559
+ this.sessionLocks.set(lock.sessionId, new Set());
560
+ }
561
+ this.sessionLocks.get(lock.sessionId).add(lockId);
562
+ }
563
+ }
564
+
565
+ log.info(`Restored ${this.locks.size} locks from disk`);
566
+ } catch (error) {
567
+ log.warn(`Failed to load lock state: ${getErrorMessage(error)}`);
568
+ }
569
+ }
570
+
571
+ /**
572
+ * Shutdown the lock manager
573
+ */
574
+ shutdown() {
575
+ if (this.cleanupInterval) {
576
+ clearInterval(this.cleanupInterval);
577
+ }
578
+ this.saveState();
579
+ }
580
+ }
581
+
582
+ /**
583
+ * Create a lock manager instance
584
+ * @param {Object} options - Options
585
+ * @returns {LockManager} Manager instance
586
+ */
587
+ function createLockManager(options = {}) {
588
+ return new LockManager(options);
589
+ }
590
+
591
+ // Default instance
592
+ let defaultManager = null;
593
+
594
+ /**
595
+ * Get the default lock manager
596
+ * @param {string} projectRoot - Project root for persist path
597
+ * @returns {LockManager} Default manager
598
+ */
599
+ function getLockManager(projectRoot) {
600
+ if (!defaultManager) {
601
+ const persistPath = projectRoot
602
+ ? path.join(projectRoot, ".vibecheck", "conductor", "locks.json")
603
+ : null;
604
+ defaultManager = createLockManager({ persistPath });
605
+ }
606
+ return defaultManager;
607
+ }
608
+
609
+ export {
610
+ LockManager,
611
+ createLockManager,
612
+ getLockManager,
613
+ LOCK_TYPES,
614
+ DEFAULT_LOCK_TIMEOUT_MS,
615
+ };