@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.
- package/bin/registry.js +192 -5
- package/bin/runners/lib/agent-firewall/change-packet/builder.js +280 -6
- package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
- package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
- package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
- package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
- package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
- package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
- package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
- package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
- package/bin/runners/lib/agent-firewall/logger.js +141 -0
- package/bin/runners/lib/agent-firewall/policy/loader.js +312 -4
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +113 -1
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +133 -6
- package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
- package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
- package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
- package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
- package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
- package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
- package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
- package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
- package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
- package/bin/runners/lib/agent-firewall/risk/thresholds.js +321 -0
- package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
- package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
- package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
- package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
- package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
- package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
- package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
- package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
- package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
- package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
- package/bin/runners/lib/analyzers.js +81 -18
- package/bin/runners/lib/authority-badge.js +425 -0
- package/bin/runners/lib/cli-output.js +7 -1
- package/bin/runners/lib/error-handler.js +16 -9
- package/bin/runners/lib/exit-codes.js +275 -0
- package/bin/runners/lib/global-flags.js +37 -0
- package/bin/runners/lib/help-formatter.js +413 -0
- package/bin/runners/lib/logger.js +38 -0
- package/bin/runners/lib/unified-cli-output.js +604 -0
- package/bin/runners/lib/upsell.js +148 -0
- package/bin/runners/runApprove.js +1200 -0
- package/bin/runners/runAuth.js +324 -95
- package/bin/runners/runCheckpoint.js +39 -21
- package/bin/runners/runClassify.js +859 -0
- package/bin/runners/runContext.js +136 -24
- package/bin/runners/runDoctor.js +108 -68
- package/bin/runners/runFix.js +6 -5
- package/bin/runners/runGuard.js +212 -118
- package/bin/runners/runInit.js +3 -2
- package/bin/runners/runMcp.js +130 -52
- package/bin/runners/runPolish.js +43 -20
- package/bin/runners/runProve.js +1 -2
- package/bin/runners/runReport.js +3 -2
- package/bin/runners/runScan.js +63 -44
- package/bin/runners/runShip.js +3 -4
- package/bin/runners/runValidate.js +19 -2
- package/bin/runners/runWatch.js +104 -53
- package/bin/vibecheck.js +106 -19
- package/mcp-server/HARDENING_SUMMARY.md +299 -0
- package/mcp-server/agent-firewall-interceptor.js +367 -31
- package/mcp-server/authority-tools.js +569 -0
- package/mcp-server/conductor/conflict-resolver.js +588 -0
- package/mcp-server/conductor/execution-planner.js +544 -0
- package/mcp-server/conductor/index.js +377 -0
- package/mcp-server/conductor/lock-manager.js +615 -0
- package/mcp-server/conductor/request-queue.js +550 -0
- package/mcp-server/conductor/session-manager.js +500 -0
- package/mcp-server/conductor/tools.js +510 -0
- package/mcp-server/index.js +1149 -243
- package/mcp-server/lib/{api-client.js → api-client.cjs} +40 -4
- package/mcp-server/lib/logger.cjs +30 -0
- package/mcp-server/logger.js +173 -0
- package/mcp-server/package.json +2 -2
- package/mcp-server/premium-tools.js +2 -2
- package/mcp-server/tier-auth.js +245 -35
- package/mcp-server/truth-firewall-tools.js +145 -15
- package/mcp-server/vibecheck-tools.js +2 -2
- package/package.json +2 -3
- package/mcp-server/index.old.js +0 -4137
- 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
|
+
};
|