@imdeadpool/guardex 7.0.19 → 7.0.21
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/README.md +51 -13
- package/bin/multiagent-safety.js +2 -7861
- package/package.json +2 -1
- package/src/cli/args.js +809 -0
- package/src/cli/dispatch.js +86 -0
- package/src/cli/main.js +5564 -0
- package/src/context.js +517 -0
- package/src/core/runtime.js +119 -0
- package/src/finish/index.js +425 -0
- package/src/git/index.js +122 -0
- package/src/hooks/index.js +74 -0
- package/src/output/index.js +398 -0
- package/src/sandbox/index.js +68 -0
- package/src/scaffold/index.js +148 -0
- package/src/toolchain/index.js +223 -0
- package/templates/scripts/agent-branch-start.sh +52 -8
- package/templates/scripts/codex-agent.sh +155 -7
- package/templates/vscode/guardex-active-agents/README.md +16 -11
- package/templates/vscode/guardex-active-agents/extension.js +908 -65
- package/templates/vscode/guardex-active-agents/package.json +63 -1
- package/templates/vscode/guardex-active-agents/session-schema.js +440 -42
|
@@ -5,8 +5,30 @@ const cp = require('node:child_process');
|
|
|
5
5
|
const ACTIVE_SESSIONS_RELATIVE_DIR = path.join('.omx', 'state', 'active-sessions');
|
|
6
6
|
const SESSION_SCHEMA_VERSION = 1;
|
|
7
7
|
const LOCK_FILE_RELATIVE = path.join('.omx', 'state', 'agent-file-locks.json');
|
|
8
|
+
const AGENT_WORKTREE_LOCK_FILE = 'AGENT.lock';
|
|
9
|
+
const MANAGED_WORKTREE_ROOTS = [
|
|
10
|
+
path.join('.omx', 'agent-worktrees'),
|
|
11
|
+
path.join('.omc', 'agent-worktrees'),
|
|
12
|
+
];
|
|
8
13
|
const MAX_CHANGED_PATH_PREVIEW = 3;
|
|
9
14
|
const ACTIVE_SESSIONS_FILTER_PREFIX = ACTIVE_SESSIONS_RELATIVE_DIR.split(path.sep).join('/');
|
|
15
|
+
const LOCK_FILE_FILTER_PATH = LOCK_FILE_RELATIVE.split(path.sep).join('/');
|
|
16
|
+
const IDLE_ACTIVITY_WINDOW_MS = 2 * 60 * 1000;
|
|
17
|
+
const STALLED_ACTIVITY_WINDOW_MS = 15 * 60 * 1000;
|
|
18
|
+
const BLOCKING_GIT_STATES = [
|
|
19
|
+
{
|
|
20
|
+
label: 'Rebase in progress.',
|
|
21
|
+
markers: ['REBASE_HEAD', 'rebase-apply', 'rebase-merge'],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: 'Merge in progress.',
|
|
25
|
+
markers: ['MERGE_HEAD'],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
label: 'Cherry-pick in progress.',
|
|
29
|
+
markers: ['CHERRY_PICK_HEAD'],
|
|
30
|
+
},
|
|
31
|
+
];
|
|
10
32
|
|
|
11
33
|
function toNonEmptyString(value, fallback = '') {
|
|
12
34
|
const normalized = typeof value === 'string' ? value.trim() : String(value || '').trim();
|
|
@@ -18,6 +40,16 @@ function toPositiveInteger(value) {
|
|
|
18
40
|
return Number.isInteger(normalized) && normalized > 0 ? normalized : null;
|
|
19
41
|
}
|
|
20
42
|
|
|
43
|
+
function normalizeTaskMode(value) {
|
|
44
|
+
const normalized = toNonEmptyString(value).toLowerCase();
|
|
45
|
+
return normalized === 'caveman' || normalized === 'omx' ? normalized : '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeOpenSpecTier(value) {
|
|
49
|
+
const normalized = toNonEmptyString(value).toUpperCase();
|
|
50
|
+
return ['T0', 'T1', 'T2', 'T3'].includes(normalized) ? normalized : '';
|
|
51
|
+
}
|
|
52
|
+
|
|
21
53
|
function sanitizeBranchForFile(branch) {
|
|
22
54
|
const normalized = toNonEmptyString(branch, 'session');
|
|
23
55
|
return normalized.replace(/[^a-zA-Z0-9._-]+/g, '__').replace(/^_+|_+$/g, '') || 'session';
|
|
@@ -35,6 +67,10 @@ function sessionFilePathForBranch(repoRoot, branch) {
|
|
|
35
67
|
return path.join(activeSessionsDirForRepo(repoRoot), sessionFileNameForBranch(branch));
|
|
36
68
|
}
|
|
37
69
|
|
|
70
|
+
function resolveManagedWorktreeRoots(repoRoot) {
|
|
71
|
+
return MANAGED_WORKTREE_ROOTS.map((relativeRoot) => path.join(path.resolve(repoRoot), relativeRoot));
|
|
72
|
+
}
|
|
73
|
+
|
|
38
74
|
function splitOutputLines(output) {
|
|
39
75
|
if (typeof output !== 'string') {
|
|
40
76
|
return null;
|
|
@@ -45,6 +81,28 @@ function splitOutputLines(output) {
|
|
|
45
81
|
.filter((line) => line.trim().length > 0);
|
|
46
82
|
}
|
|
47
83
|
|
|
84
|
+
function normalizeRelativePath(value) {
|
|
85
|
+
return toNonEmptyString(value).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function readJsonFile(filePath) {
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
91
|
+
} catch (_error) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function normalizeIsoString(value, fallback = '') {
|
|
97
|
+
const normalized = toNonEmptyString(value);
|
|
98
|
+
if (!normalized) {
|
|
99
|
+
return fallback;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const timestamp = Date.parse(normalized);
|
|
103
|
+
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : fallback;
|
|
104
|
+
}
|
|
105
|
+
|
|
48
106
|
function runGitLines(worktreePath, args) {
|
|
49
107
|
try {
|
|
50
108
|
const output = cp.execFileSync('git', ['-C', worktreePath, ...args], {
|
|
@@ -150,7 +208,9 @@ function parseRepoChangeLine(repoRoot, line) {
|
|
|
150
208
|
|
|
151
209
|
const normalizedRelativePath = relativePath.split(path.sep).join('/');
|
|
152
210
|
if (
|
|
153
|
-
normalizedRelativePath ===
|
|
211
|
+
normalizedRelativePath === LOCK_FILE_FILTER_PATH
|
|
212
|
+
|| normalizedRelativePath.startsWith(`${LOCK_FILE_FILTER_PATH}/`)
|
|
213
|
+
|| normalizedRelativePath === ACTIVE_SESSIONS_FILTER_PREFIX
|
|
154
214
|
|| normalizedRelativePath.startsWith(`${ACTIVE_SESSIONS_FILTER_PREFIX}/`)
|
|
155
215
|
) {
|
|
156
216
|
return null;
|
|
@@ -167,8 +227,8 @@ function parseRepoChangeLine(repoRoot, line) {
|
|
|
167
227
|
|
|
168
228
|
function collectWorktreeChangedPaths(worktreePath) {
|
|
169
229
|
const changedGroups = [
|
|
170
|
-
runGitLines(worktreePath, ['diff', '--name-only', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`]),
|
|
171
|
-
runGitLines(worktreePath, ['diff', '--cached', '--name-only', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`]),
|
|
230
|
+
runGitLines(worktreePath, ['diff', '--name-only', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`, `:(exclude)${AGENT_WORKTREE_LOCK_FILE}`]),
|
|
231
|
+
runGitLines(worktreePath, ['diff', '--cached', '--name-only', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`, `:(exclude)${AGENT_WORKTREE_LOCK_FILE}`]),
|
|
172
232
|
runGitLines(worktreePath, ['ls-files', '--others', '--exclude-standard']),
|
|
173
233
|
];
|
|
174
234
|
|
|
@@ -177,41 +237,196 @@ function collectWorktreeChangedPaths(worktreePath) {
|
|
|
177
237
|
}
|
|
178
238
|
|
|
179
239
|
return [...new Set(changedGroups.flat())]
|
|
180
|
-
.filter((relativePath) =>
|
|
240
|
+
.filter((relativePath) => (
|
|
241
|
+
relativePath
|
|
242
|
+
&& relativePath !== LOCK_FILE_RELATIVE
|
|
243
|
+
&& relativePath !== AGENT_WORKTREE_LOCK_FILE
|
|
244
|
+
))
|
|
245
|
+
.sort((left, right) => left.localeCompare(right));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function resolveWorktreeGitDir(worktreePath) {
|
|
249
|
+
const gitPath = path.join(path.resolve(worktreePath), '.git');
|
|
250
|
+
try {
|
|
251
|
+
if (fs.statSync(gitPath).isDirectory()) {
|
|
252
|
+
return gitPath;
|
|
253
|
+
}
|
|
254
|
+
} catch (_error) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
const gitPointer = fs.readFileSync(gitPath, 'utf8');
|
|
260
|
+
const match = gitPointer.match(/^gitdir:\s*(.+)$/m);
|
|
261
|
+
if (match?.[1]) {
|
|
262
|
+
return path.resolve(worktreePath, match[1].trim());
|
|
263
|
+
}
|
|
264
|
+
} catch (_error) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function deriveBlockingGitLabel(worktreePath) {
|
|
272
|
+
const gitDir = resolveWorktreeGitDir(worktreePath);
|
|
273
|
+
if (!gitDir) {
|
|
274
|
+
return '';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for (const blockingState of BLOCKING_GIT_STATES) {
|
|
278
|
+
if (blockingState.markers.some((marker) => fs.existsSync(path.join(gitDir, marker)))) {
|
|
279
|
+
return blockingState.label;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return '';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function collectWorktreeTrackedPaths(worktreePath) {
|
|
287
|
+
const trackedPaths = runGitLines(worktreePath, ['ls-files', '--cached', '--others', '--exclude-standard']);
|
|
288
|
+
if (!trackedPaths) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return [...new Set(trackedPaths)]
|
|
293
|
+
.filter(Boolean)
|
|
181
294
|
.sort((left, right) => left.localeCompare(right));
|
|
182
295
|
}
|
|
183
296
|
|
|
184
|
-
function
|
|
185
|
-
const
|
|
186
|
-
if (!
|
|
297
|
+
function deriveLatestWorktreeFileActivity(worktreePath) {
|
|
298
|
+
const trackedPaths = collectWorktreeTrackedPaths(worktreePath);
|
|
299
|
+
if (!trackedPaths) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let latestMtimeMs = null;
|
|
304
|
+
for (const relativePath of trackedPaths) {
|
|
305
|
+
const absolutePath = path.join(worktreePath, relativePath);
|
|
306
|
+
try {
|
|
307
|
+
const stats = fs.statSync(absolutePath);
|
|
308
|
+
if (!stats.isFile() || !Number.isFinite(stats.mtimeMs)) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
latestMtimeMs = latestMtimeMs === null
|
|
312
|
+
? stats.mtimeMs
|
|
313
|
+
: Math.max(latestMtimeMs, stats.mtimeMs);
|
|
314
|
+
} catch (_error) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return latestMtimeMs;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function deriveSessionActivity(session, options = {}) {
|
|
323
|
+
const now = Number.isFinite(options.now) ? options.now : Date.now();
|
|
324
|
+
const pid = toPositiveInteger(session?.pid);
|
|
325
|
+
const pidAlive = pid ? isPidAlive(pid) : null;
|
|
326
|
+
const blockingLabel = deriveBlockingGitLabel(session.worktreePath);
|
|
327
|
+
if (blockingLabel) {
|
|
187
328
|
return {
|
|
188
|
-
activityKind: '
|
|
189
|
-
activityLabel: '
|
|
329
|
+
activityKind: 'blocked',
|
|
330
|
+
activityLabel: 'blocked',
|
|
331
|
+
activityCountLabel: '',
|
|
332
|
+
activitySummary: blockingLabel,
|
|
333
|
+
changeCount: 0,
|
|
334
|
+
changedPaths: [],
|
|
335
|
+
pidAlive,
|
|
336
|
+
lastFileActivityAt: '',
|
|
337
|
+
lastFileActivityLabel: '',
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (pid && !pidAlive) {
|
|
342
|
+
return {
|
|
343
|
+
activityKind: 'dead',
|
|
344
|
+
activityLabel: 'dead',
|
|
345
|
+
activityCountLabel: '',
|
|
346
|
+
activitySummary: 'Recorded PID is not alive.',
|
|
347
|
+
changeCount: 0,
|
|
348
|
+
changedPaths: [],
|
|
349
|
+
pidAlive,
|
|
350
|
+
lastFileActivityAt: '',
|
|
351
|
+
lastFileActivityLabel: '',
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const worktreeChangedPaths = collectWorktreeChangedPaths(session.worktreePath);
|
|
356
|
+
if (!worktreeChangedPaths) {
|
|
357
|
+
return {
|
|
358
|
+
activityKind: 'idle',
|
|
359
|
+
activityLabel: 'idle',
|
|
190
360
|
activityCountLabel: '',
|
|
191
361
|
activitySummary: 'Worktree activity unavailable.',
|
|
192
362
|
changeCount: 0,
|
|
193
363
|
changedPaths: [],
|
|
364
|
+
pidAlive,
|
|
365
|
+
lastFileActivityAt: '',
|
|
366
|
+
lastFileActivityLabel: '',
|
|
194
367
|
};
|
|
195
368
|
}
|
|
196
369
|
|
|
197
|
-
if (
|
|
370
|
+
if (worktreeChangedPaths.length > 0) {
|
|
371
|
+
const changedPaths = [...new Set(worktreeChangedPaths
|
|
372
|
+
.map((relativePath) => normalizeRelativePath(
|
|
373
|
+
path.relative(session.repoRoot, path.resolve(session.worktreePath, relativePath)),
|
|
374
|
+
))
|
|
375
|
+
.filter(Boolean))]
|
|
376
|
+
.sort((left, right) => left.localeCompare(right));
|
|
377
|
+
|
|
198
378
|
return {
|
|
199
|
-
activityKind: '
|
|
200
|
-
activityLabel: '
|
|
379
|
+
activityKind: 'working',
|
|
380
|
+
activityLabel: 'working',
|
|
381
|
+
activityCountLabel: formatFileCount(worktreeChangedPaths.length),
|
|
382
|
+
activitySummary: previewChangedPaths(worktreeChangedPaths),
|
|
383
|
+
changeCount: worktreeChangedPaths.length,
|
|
384
|
+
changedPaths,
|
|
385
|
+
pidAlive,
|
|
386
|
+
lastFileActivityAt: '',
|
|
387
|
+
lastFileActivityLabel: '',
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const latestFileActivityMs = deriveLatestWorktreeFileActivity(session.worktreePath);
|
|
392
|
+
const lastFileActivityAt = Number.isFinite(latestFileActivityMs)
|
|
393
|
+
? new Date(latestFileActivityMs).toISOString()
|
|
394
|
+
: '';
|
|
395
|
+
const lastFileActivityLabel = lastFileActivityAt
|
|
396
|
+
? formatElapsedFrom(lastFileActivityAt, now)
|
|
397
|
+
: '';
|
|
398
|
+
const lastFileActivityAgeMs = Number.isFinite(latestFileActivityMs)
|
|
399
|
+
? Math.max(0, now - latestFileActivityMs)
|
|
400
|
+
: null;
|
|
401
|
+
|
|
402
|
+
if (lastFileActivityAgeMs !== null && lastFileActivityAgeMs > STALLED_ACTIVITY_WINDOW_MS) {
|
|
403
|
+
return {
|
|
404
|
+
activityKind: 'stalled',
|
|
405
|
+
activityLabel: 'stalled',
|
|
201
406
|
activityCountLabel: '',
|
|
202
|
-
activitySummary:
|
|
407
|
+
activitySummary: `Worktree clean. No file activity for ${lastFileActivityLabel}.`,
|
|
203
408
|
changeCount: 0,
|
|
204
409
|
changedPaths: [],
|
|
410
|
+
pidAlive,
|
|
411
|
+
lastFileActivityAt,
|
|
412
|
+
lastFileActivityLabel,
|
|
205
413
|
};
|
|
206
414
|
}
|
|
207
415
|
|
|
208
416
|
return {
|
|
209
|
-
activityKind: '
|
|
210
|
-
activityLabel: '
|
|
211
|
-
activityCountLabel:
|
|
212
|
-
activitySummary:
|
|
213
|
-
|
|
214
|
-
|
|
417
|
+
activityKind: 'idle',
|
|
418
|
+
activityLabel: 'idle',
|
|
419
|
+
activityCountLabel: '',
|
|
420
|
+
activitySummary: lastFileActivityAgeMs !== null && lastFileActivityAgeMs <= IDLE_ACTIVITY_WINDOW_MS
|
|
421
|
+
? `Worktree clean. Recent file activity ${lastFileActivityLabel} ago.`
|
|
422
|
+
: lastFileActivityLabel
|
|
423
|
+
? `Worktree clean. Last file activity ${lastFileActivityLabel} ago.`
|
|
424
|
+
: 'Worktree clean.',
|
|
425
|
+
changeCount: 0,
|
|
426
|
+
changedPaths: [],
|
|
427
|
+
pidAlive,
|
|
428
|
+
lastFileActivityAt,
|
|
429
|
+
lastFileActivityLabel,
|
|
215
430
|
};
|
|
216
431
|
}
|
|
217
432
|
|
|
@@ -243,10 +458,14 @@ function buildSessionRecord(input) {
|
|
|
243
458
|
repoRoot,
|
|
244
459
|
branch,
|
|
245
460
|
taskName: toNonEmptyString(input.taskName, 'task'),
|
|
461
|
+
latestTaskPreview: '',
|
|
246
462
|
agentName: toNonEmptyString(input.agentName, 'agent'),
|
|
247
463
|
worktreePath,
|
|
248
464
|
pid,
|
|
249
465
|
cliName: toNonEmptyString(input.cliName, 'codex'),
|
|
466
|
+
taskMode: normalizeTaskMode(input.taskMode),
|
|
467
|
+
openspecTier: normalizeOpenSpecTier(input.openspecTier),
|
|
468
|
+
taskRoutingReason: toNonEmptyString(input.taskRoutingReason),
|
|
250
469
|
startedAt: startedAt.toISOString(),
|
|
251
470
|
};
|
|
252
471
|
}
|
|
@@ -279,13 +498,24 @@ function normalizeSessionRecord(input, options = {}) {
|
|
|
279
498
|
repoRoot: path.resolve(repoRoot),
|
|
280
499
|
branch,
|
|
281
500
|
taskName: toNonEmptyString(input.taskName, 'task'),
|
|
501
|
+
latestTaskPreview: '',
|
|
282
502
|
agentName: toNonEmptyString(input.agentName, 'agent'),
|
|
283
503
|
worktreePath: path.resolve(worktreePath),
|
|
284
504
|
pid,
|
|
285
505
|
cliName: toNonEmptyString(input.cliName, 'codex'),
|
|
506
|
+
taskMode: normalizeTaskMode(input.taskMode),
|
|
507
|
+
openspecTier: normalizeOpenSpecTier(input.openspecTier),
|
|
508
|
+
taskRoutingReason: toNonEmptyString(input.taskRoutingReason),
|
|
286
509
|
startedAt: startedAt.toISOString(),
|
|
287
510
|
filePath: toNonEmptyString(options.filePath),
|
|
288
511
|
label: deriveSessionLabel(branch, worktreePath),
|
|
512
|
+
changedPaths: [],
|
|
513
|
+
sourceKind: 'active-session',
|
|
514
|
+
telemetryUpdatedAt: '',
|
|
515
|
+
telemetrySource: '',
|
|
516
|
+
lockSnapshotCount: 0,
|
|
517
|
+
lockSessionCount: 0,
|
|
518
|
+
collaboration: false,
|
|
289
519
|
};
|
|
290
520
|
}
|
|
291
521
|
|
|
@@ -327,49 +557,212 @@ function isPidAlive(pid) {
|
|
|
327
557
|
}
|
|
328
558
|
}
|
|
329
559
|
|
|
330
|
-
function
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
560
|
+
function readWorktreeBranch(worktreePath) {
|
|
561
|
+
const lines = runGitLines(worktreePath, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
562
|
+
return Array.isArray(lines) && typeof lines[0] === 'string' ? lines[0].trim() : '';
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function deriveAgentNameFromBranch(branch) {
|
|
566
|
+
const parts = toNonEmptyString(branch).split('/').filter(Boolean);
|
|
567
|
+
if (parts.length >= 2 && parts[0] === 'agent') {
|
|
568
|
+
return parts[1];
|
|
334
569
|
}
|
|
570
|
+
return 'agent';
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function flattenTelemetrySnapshotSessions(lockPayload) {
|
|
574
|
+
const flattened = [];
|
|
575
|
+
const snapshots = Array.isArray(lockPayload?.snapshots) ? lockPayload.snapshots : [];
|
|
576
|
+
for (const snapshot of snapshots) {
|
|
577
|
+
const snapshotSessions = Array.isArray(snapshot?.sessions) ? snapshot.sessions : [];
|
|
578
|
+
for (const session of snapshotSessions) {
|
|
579
|
+
flattened.push({
|
|
580
|
+
taskPreview: toNonEmptyString(session?.taskPreview),
|
|
581
|
+
taskUpdatedAt: normalizeIsoString(session?.taskUpdatedAt),
|
|
582
|
+
projectName: toNonEmptyString(session?.projectName),
|
|
583
|
+
projectPath: toNonEmptyString(session?.projectPath),
|
|
584
|
+
snapshotName: toNonEmptyString(snapshot?.snapshotName),
|
|
585
|
+
email: toNonEmptyString(snapshot?.email),
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return flattened;
|
|
590
|
+
}
|
|
335
591
|
|
|
592
|
+
function sortSessionsByTimestamp(sessions) {
|
|
593
|
+
sessions.sort((left, right) => {
|
|
594
|
+
const timeDelta = Date.parse(right.startedAt) - Date.parse(left.startedAt);
|
|
595
|
+
if (timeDelta !== 0) {
|
|
596
|
+
return timeDelta;
|
|
597
|
+
}
|
|
598
|
+
return left.label.localeCompare(right.label);
|
|
599
|
+
});
|
|
600
|
+
return sessions;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function deriveLockTaskAnchor(entries, fallbackTaskName, fallbackTimestamp) {
|
|
604
|
+
const sortedEntries = [...entries].sort((left, right) => {
|
|
605
|
+
const timeDelta = Date.parse(right.taskUpdatedAt || '') - Date.parse(left.taskUpdatedAt || '');
|
|
606
|
+
if (timeDelta !== 0) {
|
|
607
|
+
return timeDelta;
|
|
608
|
+
}
|
|
609
|
+
if (Boolean(right.taskPreview) !== Boolean(left.taskPreview)) {
|
|
610
|
+
return Number(Boolean(right.taskPreview)) - Number(Boolean(left.taskPreview));
|
|
611
|
+
}
|
|
612
|
+
return (right.projectPath || '').localeCompare(left.projectPath || '');
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
const latestEntry = sortedEntries[0] || null;
|
|
616
|
+
return {
|
|
617
|
+
taskName: latestEntry?.taskPreview || fallbackTaskName || 'task',
|
|
618
|
+
latestTaskPreview: latestEntry?.taskPreview || '',
|
|
619
|
+
timestamp: latestEntry?.taskUpdatedAt || fallbackTimestamp || '',
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function buildWorktreeLockSession(repoRoot, worktreePath, lockPayload, options = {}) {
|
|
336
624
|
const now = options.now || Date.now();
|
|
625
|
+
const telemetryEntries = flattenTelemetrySnapshotSessions(lockPayload);
|
|
626
|
+
const telemetryUpdatedAt = normalizeIsoString(lockPayload?.updatedAt);
|
|
627
|
+
const branch = readWorktreeBranch(worktreePath);
|
|
628
|
+
const effectiveBranch = branch && branch !== 'HEAD'
|
|
629
|
+
? branch
|
|
630
|
+
: `agent/telemetry/${path.basename(worktreePath)}`;
|
|
631
|
+
const label = deriveSessionLabel(effectiveBranch, worktreePath);
|
|
632
|
+
const taskAnchor = deriveLockTaskAnchor(telemetryEntries, label, telemetryUpdatedAt);
|
|
633
|
+
const startedAt = taskAnchor.timestamp || telemetryUpdatedAt || new Date(now).toISOString();
|
|
634
|
+
|
|
635
|
+
const session = {
|
|
636
|
+
schemaVersion: toPositiveInteger(lockPayload?.schemaVersion) || SESSION_SCHEMA_VERSION,
|
|
637
|
+
repoRoot: path.resolve(repoRoot),
|
|
638
|
+
branch: effectiveBranch,
|
|
639
|
+
taskName: taskAnchor.taskName,
|
|
640
|
+
latestTaskPreview: taskAnchor.latestTaskPreview,
|
|
641
|
+
agentName: deriveAgentNameFromBranch(effectiveBranch),
|
|
642
|
+
worktreePath: path.resolve(worktreePath),
|
|
643
|
+
pid: null,
|
|
644
|
+
cliName: 'codex',
|
|
645
|
+
taskMode: '',
|
|
646
|
+
openspecTier: '',
|
|
647
|
+
taskRoutingReason: '',
|
|
648
|
+
startedAt,
|
|
649
|
+
filePath: path.join(worktreePath, AGENT_WORKTREE_LOCK_FILE),
|
|
650
|
+
label,
|
|
651
|
+
changedPaths: [],
|
|
652
|
+
sourceKind: 'worktree-lock',
|
|
653
|
+
telemetryUpdatedAt: telemetryUpdatedAt || startedAt,
|
|
654
|
+
telemetrySource: toNonEmptyString(lockPayload?.source, 'worktree-lock'),
|
|
655
|
+
lockSnapshotCount: toPositiveInteger(lockPayload?.snapshotCount) || 0,
|
|
656
|
+
lockSessionCount: toPositiveInteger(lockPayload?.sessionCount) || telemetryEntries.length,
|
|
657
|
+
collaboration: Boolean(lockPayload?.collaboration),
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
session.elapsedLabel = formatElapsedFrom(session.startedAt, now);
|
|
661
|
+
Object.assign(session, deriveSessionActivity(session, { now }));
|
|
662
|
+
return session;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function readWorktreeLockSessions(repoRoot, options = {}) {
|
|
337
666
|
const sessions = [];
|
|
338
|
-
for (const
|
|
339
|
-
if (!
|
|
667
|
+
for (const managedRoot of resolveManagedWorktreeRoots(repoRoot)) {
|
|
668
|
+
if (!fs.existsSync(managedRoot)) {
|
|
340
669
|
continue;
|
|
341
670
|
}
|
|
342
671
|
|
|
343
|
-
|
|
344
|
-
let parsed;
|
|
672
|
+
let entries;
|
|
345
673
|
try {
|
|
346
|
-
|
|
674
|
+
entries = fs.readdirSync(managedRoot, { withFileTypes: true });
|
|
347
675
|
} catch (_error) {
|
|
348
676
|
continue;
|
|
349
677
|
}
|
|
350
678
|
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
679
|
+
for (const entry of entries) {
|
|
680
|
+
if (!entry.isDirectory()) {
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const worktreePath = path.join(managedRoot, entry.name);
|
|
685
|
+
const lockPath = path.join(worktreePath, AGENT_WORKTREE_LOCK_FILE);
|
|
686
|
+
if (!fs.existsSync(lockPath)) {
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const lockPayload = readJsonFile(lockPath);
|
|
691
|
+
if (!lockPayload || typeof lockPayload !== 'object' || Array.isArray(lockPayload)) {
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const telemetryEntries = flattenTelemetrySnapshotSessions(lockPayload);
|
|
696
|
+
if (telemetryEntries.length === 0 && !toPositiveInteger(lockPayload.sessionCount)) {
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
sessions.push(buildWorktreeLockSession(repoRoot, worktreePath, lockPayload, options));
|
|
354
701
|
}
|
|
355
|
-
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return sortSessionsByTimestamp(sessions);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function mergeSessionSources(primarySessions, lockSessions) {
|
|
708
|
+
const lockSessionsByWorktree = new Map(
|
|
709
|
+
lockSessions.map((session) => [path.resolve(session.worktreePath), session]),
|
|
710
|
+
);
|
|
711
|
+
const consumedLockWorktrees = new Set();
|
|
712
|
+
const merged = [];
|
|
713
|
+
|
|
714
|
+
for (const session of primarySessions) {
|
|
715
|
+
const worktreeKey = path.resolve(session.worktreePath);
|
|
716
|
+
const lockSession = lockSessionsByWorktree.get(worktreeKey);
|
|
717
|
+
if (lockSession && session.activityKind === 'dead') {
|
|
356
718
|
continue;
|
|
357
719
|
}
|
|
720
|
+
if (lockSession) {
|
|
721
|
+
consumedLockWorktrees.add(worktreeKey);
|
|
722
|
+
}
|
|
723
|
+
merged.push(session);
|
|
724
|
+
}
|
|
358
725
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
726
|
+
for (const lockSession of lockSessions) {
|
|
727
|
+
const worktreeKey = path.resolve(lockSession.worktreePath);
|
|
728
|
+
if (!consumedLockWorktrees.has(worktreeKey)) {
|
|
729
|
+
merged.push(lockSession);
|
|
730
|
+
}
|
|
362
731
|
}
|
|
363
732
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
733
|
+
return sortSessionsByTimestamp(merged);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function readActiveSessions(repoRoot, options = {}) {
|
|
737
|
+
const activeSessionsDir = activeSessionsDirForRepo(repoRoot);
|
|
738
|
+
const now = options.now || Date.now();
|
|
739
|
+
const sessionFileSessions = [];
|
|
740
|
+
if (fs.existsSync(activeSessionsDir)) {
|
|
741
|
+
for (const entry of fs.readdirSync(activeSessionsDir, { withFileTypes: true })) {
|
|
742
|
+
if (!entry.isFile() || !entry.name.endsWith('.json')) {
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const filePath = path.join(activeSessionsDir, entry.name);
|
|
747
|
+
const parsed = readJsonFile(filePath);
|
|
748
|
+
const normalized = normalizeSessionRecord(parsed, { filePath });
|
|
749
|
+
if (!normalized) {
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
if (!options.includeStale && !isPidAlive(normalized.pid)) {
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
normalized.elapsedLabel = formatElapsedFrom(normalized.startedAt, now);
|
|
757
|
+
Object.assign(normalized, deriveSessionActivity(normalized, { now }));
|
|
758
|
+
sessionFileSessions.push(normalized);
|
|
368
759
|
}
|
|
369
|
-
|
|
370
|
-
});
|
|
760
|
+
}
|
|
371
761
|
|
|
372
|
-
return
|
|
762
|
+
return mergeSessionSources(
|
|
763
|
+
sortSessionsByTimestamp(sessionFileSessions),
|
|
764
|
+
readWorktreeLockSessions(repoRoot, { now }),
|
|
765
|
+
);
|
|
373
766
|
}
|
|
374
767
|
|
|
375
768
|
function readRepoChanges(repoRoot) {
|
|
@@ -390,6 +783,9 @@ module.exports = {
|
|
|
390
783
|
activeSessionsDirForRepo,
|
|
391
784
|
buildSessionRecord,
|
|
392
785
|
collectWorktreeChangedPaths,
|
|
786
|
+
collectWorktreeTrackedPaths,
|
|
787
|
+
deriveBlockingGitLabel,
|
|
788
|
+
deriveLatestWorktreeFileActivity,
|
|
393
789
|
deriveSessionLabel,
|
|
394
790
|
deriveSessionActivity,
|
|
395
791
|
formatElapsedFrom,
|
|
@@ -399,8 +795,10 @@ module.exports = {
|
|
|
399
795
|
parseRepoChangeLine,
|
|
400
796
|
previewChangedPaths,
|
|
401
797
|
readActiveSessions,
|
|
798
|
+
readWorktreeLockSessions,
|
|
402
799
|
readRepoChanges,
|
|
403
800
|
deriveRepoChangeStatus,
|
|
801
|
+
resolveWorktreeGitDir,
|
|
404
802
|
sanitizeBranchForFile,
|
|
405
803
|
sessionFileNameForBranch,
|
|
406
804
|
sessionFilePathForBranch,
|