@vibecheckai/cli 3.2.5 → 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 (197) hide show
  1. package/bin/.generated +25 -25
  2. package/bin/dev/run-v2-torture.js +30 -30
  3. package/bin/registry.js +192 -5
  4. package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
  5. package/bin/runners/lib/agent-firewall/change-packet/builder.js +280 -6
  6. package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
  7. package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
  8. package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
  9. package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
  10. package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
  11. package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
  12. package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
  13. package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
  14. package/bin/runners/lib/agent-firewall/logger.js +141 -0
  15. package/bin/runners/lib/agent-firewall/policy/loader.js +312 -4
  16. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +113 -1
  17. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +133 -6
  18. package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
  19. package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
  20. package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
  21. package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
  22. package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
  23. package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
  24. package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
  25. package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
  26. package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
  27. package/bin/runners/lib/agent-firewall/risk/thresholds.js +321 -0
  28. package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
  29. package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
  30. package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
  31. package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
  32. package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
  33. package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
  34. package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
  35. package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
  36. package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
  37. package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
  38. package/bin/runners/lib/analyzers.js +81 -18
  39. package/bin/runners/lib/api-client.js +269 -0
  40. package/bin/runners/lib/auth-truth.js +193 -193
  41. package/bin/runners/lib/authority-badge.js +425 -0
  42. package/bin/runners/lib/backup.js +62 -62
  43. package/bin/runners/lib/billing.js +107 -107
  44. package/bin/runners/lib/claims.js +118 -118
  45. package/bin/runners/lib/cli-output.js +7 -1
  46. package/bin/runners/lib/cli-ui.js +540 -540
  47. package/bin/runners/lib/contracts/auth-contract.js +202 -202
  48. package/bin/runners/lib/contracts/env-contract.js +181 -181
  49. package/bin/runners/lib/contracts/external-contract.js +206 -206
  50. package/bin/runners/lib/contracts/guard.js +168 -168
  51. package/bin/runners/lib/contracts/index.js +89 -89
  52. package/bin/runners/lib/contracts/plan-validator.js +311 -311
  53. package/bin/runners/lib/contracts/route-contract.js +199 -199
  54. package/bin/runners/lib/contracts.js +804 -804
  55. package/bin/runners/lib/detect.js +89 -89
  56. package/bin/runners/lib/doctor/autofix.js +254 -254
  57. package/bin/runners/lib/doctor/index.js +37 -37
  58. package/bin/runners/lib/doctor/modules/dependencies.js +325 -325
  59. package/bin/runners/lib/doctor/modules/index.js +46 -46
  60. package/bin/runners/lib/doctor/modules/network.js +250 -250
  61. package/bin/runners/lib/doctor/modules/project.js +312 -312
  62. package/bin/runners/lib/doctor/modules/runtime.js +224 -224
  63. package/bin/runners/lib/doctor/modules/security.js +348 -348
  64. package/bin/runners/lib/doctor/modules/system.js +213 -213
  65. package/bin/runners/lib/doctor/modules/vibecheck.js +394 -394
  66. package/bin/runners/lib/doctor/reporter.js +262 -262
  67. package/bin/runners/lib/doctor/service.js +262 -262
  68. package/bin/runners/lib/doctor/types.js +113 -113
  69. package/bin/runners/lib/doctor/ui.js +263 -263
  70. package/bin/runners/lib/doctor-v2.js +608 -608
  71. package/bin/runners/lib/drift.js +425 -425
  72. package/bin/runners/lib/enforcement.js +72 -72
  73. package/bin/runners/lib/enterprise-detect.js +603 -603
  74. package/bin/runners/lib/enterprise-init.js +942 -942
  75. package/bin/runners/lib/env-resolver.js +417 -417
  76. package/bin/runners/lib/env-template.js +66 -66
  77. package/bin/runners/lib/env.js +189 -189
  78. package/bin/runners/lib/error-handler.js +16 -9
  79. package/bin/runners/lib/exit-codes.js +275 -0
  80. package/bin/runners/lib/extractors/client-calls.js +990 -990
  81. package/bin/runners/lib/extractors/fastify-route-dump.js +573 -573
  82. package/bin/runners/lib/extractors/fastify-routes.js +426 -426
  83. package/bin/runners/lib/extractors/index.js +363 -363
  84. package/bin/runners/lib/extractors/next-routes.js +524 -524
  85. package/bin/runners/lib/extractors/proof-graph.js +431 -431
  86. package/bin/runners/lib/extractors/route-matcher.js +451 -451
  87. package/bin/runners/lib/extractors/truthpack-v2.js +377 -377
  88. package/bin/runners/lib/extractors/ui-bindings.js +547 -547
  89. package/bin/runners/lib/findings-schema.js +281 -281
  90. package/bin/runners/lib/firewall-prompt.js +50 -50
  91. package/bin/runners/lib/global-flags.js +37 -0
  92. package/bin/runners/lib/graph/graph-builder.js +265 -265
  93. package/bin/runners/lib/graph/html-renderer.js +413 -413
  94. package/bin/runners/lib/graph/index.js +32 -32
  95. package/bin/runners/lib/graph/runtime-collector.js +215 -215
  96. package/bin/runners/lib/graph/static-extractor.js +518 -518
  97. package/bin/runners/lib/help-formatter.js +413 -0
  98. package/bin/runners/lib/html-report.js +650 -650
  99. package/bin/runners/lib/llm.js +75 -75
  100. package/bin/runners/lib/logger.js +38 -0
  101. package/bin/runners/lib/meter.js +61 -61
  102. package/bin/runners/lib/missions/evidence.js +126 -126
  103. package/bin/runners/lib/patch.js +40 -40
  104. package/bin/runners/lib/permissions/auth-model.js +213 -213
  105. package/bin/runners/lib/permissions/idor-prover.js +205 -205
  106. package/bin/runners/lib/permissions/index.js +45 -45
  107. package/bin/runners/lib/permissions/matrix-builder.js +198 -198
  108. package/bin/runners/lib/pkgjson.js +28 -28
  109. package/bin/runners/lib/policy.js +295 -295
  110. package/bin/runners/lib/preflight.js +142 -142
  111. package/bin/runners/lib/reality/correlation-detectors.js +359 -359
  112. package/bin/runners/lib/reality/index.js +318 -318
  113. package/bin/runners/lib/reality/request-hashing.js +416 -416
  114. package/bin/runners/lib/reality/request-mapper.js +453 -453
  115. package/bin/runners/lib/reality/safety-rails.js +463 -463
  116. package/bin/runners/lib/reality/semantic-snapshot.js +408 -408
  117. package/bin/runners/lib/reality/toast-detector.js +393 -393
  118. package/bin/runners/lib/reality-findings.js +84 -84
  119. package/bin/runners/lib/receipts.js +179 -179
  120. package/bin/runners/lib/redact.js +29 -29
  121. package/bin/runners/lib/replay/capsule-manager.js +154 -154
  122. package/bin/runners/lib/replay/index.js +263 -263
  123. package/bin/runners/lib/replay/player.js +348 -348
  124. package/bin/runners/lib/replay/recorder.js +331 -331
  125. package/bin/runners/lib/report.js +135 -135
  126. package/bin/runners/lib/route-detection.js +1140 -1140
  127. package/bin/runners/lib/sandbox/index.js +59 -59
  128. package/bin/runners/lib/sandbox/proof-chain.js +399 -399
  129. package/bin/runners/lib/sandbox/sandbox-runner.js +205 -205
  130. package/bin/runners/lib/sandbox/worktree.js +174 -174
  131. package/bin/runners/lib/schema-validator.js +350 -350
  132. package/bin/runners/lib/schemas/contracts.schema.json +160 -160
  133. package/bin/runners/lib/schemas/finding.schema.json +100 -100
  134. package/bin/runners/lib/schemas/mission-pack.schema.json +206 -206
  135. package/bin/runners/lib/schemas/proof-graph.schema.json +176 -176
  136. package/bin/runners/lib/schemas/reality-report.schema.json +162 -162
  137. package/bin/runners/lib/schemas/share-pack.schema.json +180 -180
  138. package/bin/runners/lib/schemas/ship-report.schema.json +117 -117
  139. package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -303
  140. package/bin/runners/lib/schemas/validator.js +438 -438
  141. package/bin/runners/lib/score-history.js +282 -282
  142. package/bin/runners/lib/share-pack.js +239 -239
  143. package/bin/runners/lib/snippets.js +67 -67
  144. package/bin/runners/lib/unified-cli-output.js +604 -0
  145. package/bin/runners/lib/upsell.js +658 -510
  146. package/bin/runners/lib/usage.js +153 -153
  147. package/bin/runners/lib/validate-patch.js +156 -156
  148. package/bin/runners/lib/verdict-engine.js +628 -628
  149. package/bin/runners/reality/engine.js +917 -917
  150. package/bin/runners/reality/flows.js +122 -122
  151. package/bin/runners/reality/report.js +378 -378
  152. package/bin/runners/reality/session.js +193 -193
  153. package/bin/runners/runAgent.d.ts +5 -0
  154. package/bin/runners/runApprove.js +1200 -0
  155. package/bin/runners/runAuth.js +324 -95
  156. package/bin/runners/runCheckpoint.js +39 -21
  157. package/bin/runners/runClassify.js +859 -0
  158. package/bin/runners/runContext.js +136 -24
  159. package/bin/runners/runDoctor.js +108 -68
  160. package/bin/runners/runFirewall.d.ts +5 -0
  161. package/bin/runners/runFirewallHook.d.ts +5 -0
  162. package/bin/runners/runFix.js +6 -5
  163. package/bin/runners/runGuard.js +262 -168
  164. package/bin/runners/runInit.js +3 -2
  165. package/bin/runners/runMcp.js +130 -52
  166. package/bin/runners/runPolish.js +43 -20
  167. package/bin/runners/runProve.js +1 -2
  168. package/bin/runners/runReport.js +3 -2
  169. package/bin/runners/runScan.js +145 -44
  170. package/bin/runners/runShip.js +3 -4
  171. package/bin/runners/runTruth.d.ts +5 -0
  172. package/bin/runners/runValidate.js +19 -2
  173. package/bin/runners/runWatch.js +104 -53
  174. package/bin/vibecheck.js +106 -19
  175. package/mcp-server/HARDENING_SUMMARY.md +299 -0
  176. package/mcp-server/agent-firewall-interceptor.js +367 -31
  177. package/mcp-server/authority-tools.js +569 -0
  178. package/mcp-server/conductor/conflict-resolver.js +588 -0
  179. package/mcp-server/conductor/execution-planner.js +544 -0
  180. package/mcp-server/conductor/index.js +377 -0
  181. package/mcp-server/conductor/lock-manager.js +615 -0
  182. package/mcp-server/conductor/request-queue.js +550 -0
  183. package/mcp-server/conductor/session-manager.js +500 -0
  184. package/mcp-server/conductor/tools.js +510 -0
  185. package/mcp-server/index.js +1199 -208
  186. package/mcp-server/lib/api-client.cjs +305 -0
  187. package/mcp-server/lib/logger.cjs +30 -0
  188. package/mcp-server/logger.js +173 -0
  189. package/mcp-server/package.json +2 -2
  190. package/mcp-server/premium-tools.js +2 -2
  191. package/mcp-server/tier-auth.js +351 -136
  192. package/mcp-server/tools/index.js +72 -72
  193. package/mcp-server/truth-firewall-tools.js +145 -15
  194. package/mcp-server/vibecheck-tools.js +2 -2
  195. package/package.json +2 -3
  196. package/mcp-server/index.old.js +0 -4137
  197. 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
+ };