@imdeadpool/guardex 7.0.19 → 7.0.20

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.
@@ -19,13 +19,42 @@
19
19
  "main": "./extension.js",
20
20
  "contributes": {
21
21
  "commands": [
22
+ {
23
+ "command": "gitguardex.activeAgents.startAgent",
24
+ "title": "Start Guardex Agent"
25
+ },
22
26
  {
23
27
  "command": "gitguardex.activeAgents.refresh",
24
28
  "title": "Refresh Active Agents"
25
29
  },
30
+ {
31
+ "command": "gitguardex.activeAgents.commitSelectedSession",
32
+ "title": "Commit Selected Session",
33
+ "icon": "$(check)"
34
+ },
26
35
  {
27
36
  "command": "gitguardex.activeAgents.openWorktree",
28
37
  "title": "Open Agent Worktree"
38
+ },
39
+ {
40
+ "command": "gitguardex.activeAgents.finishSession",
41
+ "title": "Finish",
42
+ "icon": "$(check)"
43
+ },
44
+ {
45
+ "command": "gitguardex.activeAgents.syncSession",
46
+ "title": "Sync",
47
+ "icon": "$(sync)"
48
+ },
49
+ {
50
+ "command": "gitguardex.activeAgents.stopSession",
51
+ "title": "Stop",
52
+ "icon": "$(debug-stop)"
53
+ },
54
+ {
55
+ "command": "gitguardex.activeAgents.openSessionDiff",
56
+ "title": "Open Diff",
57
+ "icon": "$(diff)"
29
58
  }
30
59
  ],
31
60
  "views": {
@@ -37,12 +66,23 @@
37
66
  }
38
67
  ]
39
68
  },
69
+ "viewsWelcome": [
70
+ {
71
+ "view": "gitguardex.activeAgents",
72
+ "contents": "No active Guardex agents are visible in this workspace yet.\n\n[Start agent](command:gitguardex.activeAgents.startAgent)\n[Open guide](https://github.com/recodeee/gitguardex/blob/main/vscode/guardex-active-agents/README.md#quick-start)\n[Refresh](command:gitguardex.activeAgents.refresh)"
73
+ }
74
+ ],
40
75
  "menus": {
41
76
  "view/title": [
77
+ {
78
+ "command": "gitguardex.activeAgents.commitSelectedSession",
79
+ "when": "view == gitguardex.activeAgents",
80
+ "group": "navigation@1"
81
+ },
42
82
  {
43
83
  "command": "gitguardex.activeAgents.refresh",
44
84
  "when": "view == gitguardex.activeAgents",
45
- "group": "navigation"
85
+ "group": "navigation@9"
46
86
  }
47
87
  ],
48
88
  "view/item/context": [
@@ -50,6 +90,26 @@
50
90
  "command": "gitguardex.activeAgents.openWorktree",
51
91
  "when": "view == gitguardex.activeAgents && viewItem == gitguardex.session",
52
92
  "group": "inline"
93
+ },
94
+ {
95
+ "command": "gitguardex.activeAgents.finishSession",
96
+ "when": "view == gitguardex.activeAgents && viewItem == gitguardex.session",
97
+ "group": "inline"
98
+ },
99
+ {
100
+ "command": "gitguardex.activeAgents.syncSession",
101
+ "when": "view == gitguardex.activeAgents && viewItem == gitguardex.session",
102
+ "group": "inline"
103
+ },
104
+ {
105
+ "command": "gitguardex.activeAgents.stopSession",
106
+ "when": "view == gitguardex.activeAgents && viewItem == gitguardex.session",
107
+ "group": "inline"
108
+ },
109
+ {
110
+ "command": "gitguardex.activeAgents.openSessionDiff",
111
+ "when": "view == gitguardex.activeAgents && viewItem == gitguardex.session",
112
+ "group": "inline"
53
113
  }
54
114
  ]
55
115
  }
@@ -7,6 +7,23 @@ const SESSION_SCHEMA_VERSION = 1;
7
7
  const LOCK_FILE_RELATIVE = path.join('.omx', 'state', 'agent-file-locks.json');
8
8
  const MAX_CHANGED_PATH_PREVIEW = 3;
9
9
  const ACTIVE_SESSIONS_FILTER_PREFIX = ACTIVE_SESSIONS_RELATIVE_DIR.split(path.sep).join('/');
10
+ const LOCK_FILE_FILTER_PATH = LOCK_FILE_RELATIVE.split(path.sep).join('/');
11
+ const IDLE_ACTIVITY_WINDOW_MS = 2 * 60 * 1000;
12
+ const STALLED_ACTIVITY_WINDOW_MS = 15 * 60 * 1000;
13
+ const BLOCKING_GIT_STATES = [
14
+ {
15
+ label: 'Rebase in progress.',
16
+ markers: ['REBASE_HEAD', 'rebase-apply', 'rebase-merge'],
17
+ },
18
+ {
19
+ label: 'Merge in progress.',
20
+ markers: ['MERGE_HEAD'],
21
+ },
22
+ {
23
+ label: 'Cherry-pick in progress.',
24
+ markers: ['CHERRY_PICK_HEAD'],
25
+ },
26
+ ];
10
27
 
11
28
  function toNonEmptyString(value, fallback = '') {
12
29
  const normalized = typeof value === 'string' ? value.trim() : String(value || '').trim();
@@ -18,6 +35,16 @@ function toPositiveInteger(value) {
18
35
  return Number.isInteger(normalized) && normalized > 0 ? normalized : null;
19
36
  }
20
37
 
38
+ function normalizeTaskMode(value) {
39
+ const normalized = toNonEmptyString(value).toLowerCase();
40
+ return normalized === 'caveman' || normalized === 'omx' ? normalized : '';
41
+ }
42
+
43
+ function normalizeOpenSpecTier(value) {
44
+ const normalized = toNonEmptyString(value).toUpperCase();
45
+ return ['T0', 'T1', 'T2', 'T3'].includes(normalized) ? normalized : '';
46
+ }
47
+
21
48
  function sanitizeBranchForFile(branch) {
22
49
  const normalized = toNonEmptyString(branch, 'session');
23
50
  return normalized.replace(/[^a-zA-Z0-9._-]+/g, '__').replace(/^_+|_+$/g, '') || 'session';
@@ -45,6 +72,10 @@ function splitOutputLines(output) {
45
72
  .filter((line) => line.trim().length > 0);
46
73
  }
47
74
 
75
+ function normalizeRelativePath(value) {
76
+ return toNonEmptyString(value).replace(/\\/g, '/').replace(/^\.\//, '');
77
+ }
78
+
48
79
  function runGitLines(worktreePath, args) {
49
80
  try {
50
81
  const output = cp.execFileSync('git', ['-C', worktreePath, ...args], {
@@ -150,7 +181,9 @@ function parseRepoChangeLine(repoRoot, line) {
150
181
 
151
182
  const normalizedRelativePath = relativePath.split(path.sep).join('/');
152
183
  if (
153
- normalizedRelativePath === ACTIVE_SESSIONS_FILTER_PREFIX
184
+ normalizedRelativePath === LOCK_FILE_FILTER_PATH
185
+ || normalizedRelativePath.startsWith(`${LOCK_FILE_FILTER_PATH}/`)
186
+ || normalizedRelativePath === ACTIVE_SESSIONS_FILTER_PREFIX
154
187
  || normalizedRelativePath.startsWith(`${ACTIVE_SESSIONS_FILTER_PREFIX}/`)
155
188
  ) {
156
189
  return null;
@@ -181,37 +214,187 @@ function collectWorktreeChangedPaths(worktreePath) {
181
214
  .sort((left, right) => left.localeCompare(right));
182
215
  }
183
216
 
184
- function deriveSessionActivity(session) {
185
- const changedPaths = collectWorktreeChangedPaths(session.worktreePath);
186
- if (!changedPaths) {
217
+ function resolveWorktreeGitDir(worktreePath) {
218
+ const gitPath = path.join(path.resolve(worktreePath), '.git');
219
+ try {
220
+ if (fs.statSync(gitPath).isDirectory()) {
221
+ return gitPath;
222
+ }
223
+ } catch (_error) {
224
+ return null;
225
+ }
226
+
227
+ try {
228
+ const gitPointer = fs.readFileSync(gitPath, 'utf8');
229
+ const match = gitPointer.match(/^gitdir:\s*(.+)$/m);
230
+ if (match?.[1]) {
231
+ return path.resolve(worktreePath, match[1].trim());
232
+ }
233
+ } catch (_error) {
234
+ return null;
235
+ }
236
+
237
+ return null;
238
+ }
239
+
240
+ function deriveBlockingGitLabel(worktreePath) {
241
+ const gitDir = resolveWorktreeGitDir(worktreePath);
242
+ if (!gitDir) {
243
+ return '';
244
+ }
245
+
246
+ for (const blockingState of BLOCKING_GIT_STATES) {
247
+ if (blockingState.markers.some((marker) => fs.existsSync(path.join(gitDir, marker)))) {
248
+ return blockingState.label;
249
+ }
250
+ }
251
+
252
+ return '';
253
+ }
254
+
255
+ function collectWorktreeTrackedPaths(worktreePath) {
256
+ const trackedPaths = runGitLines(worktreePath, ['ls-files', '--cached', '--others', '--exclude-standard']);
257
+ if (!trackedPaths) {
258
+ return null;
259
+ }
260
+
261
+ return [...new Set(trackedPaths)]
262
+ .filter(Boolean)
263
+ .sort((left, right) => left.localeCompare(right));
264
+ }
265
+
266
+ function deriveLatestWorktreeFileActivity(worktreePath) {
267
+ const trackedPaths = collectWorktreeTrackedPaths(worktreePath);
268
+ if (!trackedPaths) {
269
+ return null;
270
+ }
271
+
272
+ let latestMtimeMs = null;
273
+ for (const relativePath of trackedPaths) {
274
+ const absolutePath = path.join(worktreePath, relativePath);
275
+ try {
276
+ const stats = fs.statSync(absolutePath);
277
+ if (!stats.isFile() || !Number.isFinite(stats.mtimeMs)) {
278
+ continue;
279
+ }
280
+ latestMtimeMs = latestMtimeMs === null
281
+ ? stats.mtimeMs
282
+ : Math.max(latestMtimeMs, stats.mtimeMs);
283
+ } catch (_error) {
284
+ continue;
285
+ }
286
+ }
287
+
288
+ return latestMtimeMs;
289
+ }
290
+
291
+ function deriveSessionActivity(session, options = {}) {
292
+ const now = Number.isFinite(options.now) ? options.now : Date.now();
293
+ const blockingLabel = deriveBlockingGitLabel(session.worktreePath);
294
+ if (blockingLabel) {
187
295
  return {
188
- activityKind: 'thinking',
189
- activityLabel: 'thinking',
296
+ activityKind: 'blocked',
297
+ activityLabel: 'blocked',
298
+ activityCountLabel: '',
299
+ activitySummary: blockingLabel,
300
+ changeCount: 0,
301
+ changedPaths: [],
302
+ pidAlive: isPidAlive(session.pid),
303
+ lastFileActivityAt: '',
304
+ lastFileActivityLabel: '',
305
+ };
306
+ }
307
+
308
+ const pidAlive = isPidAlive(session.pid);
309
+ if (!pidAlive) {
310
+ return {
311
+ activityKind: 'dead',
312
+ activityLabel: 'dead',
313
+ activityCountLabel: '',
314
+ activitySummary: 'Recorded PID is not alive.',
315
+ changeCount: 0,
316
+ changedPaths: [],
317
+ pidAlive,
318
+ lastFileActivityAt: '',
319
+ lastFileActivityLabel: '',
320
+ };
321
+ }
322
+
323
+ const worktreeChangedPaths = collectWorktreeChangedPaths(session.worktreePath);
324
+ if (!worktreeChangedPaths) {
325
+ return {
326
+ activityKind: 'idle',
327
+ activityLabel: 'idle',
190
328
  activityCountLabel: '',
191
329
  activitySummary: 'Worktree activity unavailable.',
192
330
  changeCount: 0,
193
331
  changedPaths: [],
332
+ pidAlive,
333
+ lastFileActivityAt: '',
334
+ lastFileActivityLabel: '',
335
+ };
336
+ }
337
+
338
+ if (worktreeChangedPaths.length > 0) {
339
+ const changedPaths = [...new Set(worktreeChangedPaths
340
+ .map((relativePath) => normalizeRelativePath(
341
+ path.relative(session.repoRoot, path.resolve(session.worktreePath, relativePath)),
342
+ ))
343
+ .filter(Boolean))]
344
+ .sort((left, right) => left.localeCompare(right));
345
+
346
+ return {
347
+ activityKind: 'working',
348
+ activityLabel: 'working',
349
+ activityCountLabel: formatFileCount(worktreeChangedPaths.length),
350
+ activitySummary: previewChangedPaths(worktreeChangedPaths),
351
+ changeCount: worktreeChangedPaths.length,
352
+ changedPaths,
353
+ pidAlive,
354
+ lastFileActivityAt: '',
355
+ lastFileActivityLabel: '',
194
356
  };
195
357
  }
196
358
 
197
- if (changedPaths.length === 0) {
359
+ const latestFileActivityMs = deriveLatestWorktreeFileActivity(session.worktreePath);
360
+ const lastFileActivityAt = Number.isFinite(latestFileActivityMs)
361
+ ? new Date(latestFileActivityMs).toISOString()
362
+ : '';
363
+ const lastFileActivityLabel = lastFileActivityAt
364
+ ? formatElapsedFrom(lastFileActivityAt, now)
365
+ : '';
366
+ const lastFileActivityAgeMs = Number.isFinite(latestFileActivityMs)
367
+ ? Math.max(0, now - latestFileActivityMs)
368
+ : null;
369
+
370
+ if (lastFileActivityAgeMs !== null && lastFileActivityAgeMs > STALLED_ACTIVITY_WINDOW_MS) {
198
371
  return {
199
- activityKind: 'thinking',
200
- activityLabel: 'thinking',
372
+ activityKind: 'stalled',
373
+ activityLabel: 'stalled',
201
374
  activityCountLabel: '',
202
- activitySummary: 'Worktree clean.',
375
+ activitySummary: `Worktree clean. No file activity for ${lastFileActivityLabel}.`,
203
376
  changeCount: 0,
204
377
  changedPaths: [],
378
+ pidAlive,
379
+ lastFileActivityAt,
380
+ lastFileActivityLabel,
205
381
  };
206
382
  }
207
383
 
208
384
  return {
209
- activityKind: 'working',
210
- activityLabel: 'working',
211
- activityCountLabel: formatFileCount(changedPaths.length),
212
- activitySummary: previewChangedPaths(changedPaths),
213
- changeCount: changedPaths.length,
214
- changedPaths,
385
+ activityKind: 'idle',
386
+ activityLabel: 'idle',
387
+ activityCountLabel: '',
388
+ activitySummary: lastFileActivityAgeMs !== null && lastFileActivityAgeMs <= IDLE_ACTIVITY_WINDOW_MS
389
+ ? `Worktree clean. Recent file activity ${lastFileActivityLabel} ago.`
390
+ : lastFileActivityLabel
391
+ ? `Worktree clean. Last file activity ${lastFileActivityLabel} ago.`
392
+ : 'Worktree clean.',
393
+ changeCount: 0,
394
+ changedPaths: [],
395
+ pidAlive,
396
+ lastFileActivityAt,
397
+ lastFileActivityLabel,
215
398
  };
216
399
  }
217
400
 
@@ -247,6 +430,9 @@ function buildSessionRecord(input) {
247
430
  worktreePath,
248
431
  pid,
249
432
  cliName: toNonEmptyString(input.cliName, 'codex'),
433
+ taskMode: normalizeTaskMode(input.taskMode),
434
+ openspecTier: normalizeOpenSpecTier(input.openspecTier),
435
+ taskRoutingReason: toNonEmptyString(input.taskRoutingReason),
250
436
  startedAt: startedAt.toISOString(),
251
437
  };
252
438
  }
@@ -283,9 +469,13 @@ function normalizeSessionRecord(input, options = {}) {
283
469
  worktreePath: path.resolve(worktreePath),
284
470
  pid,
285
471
  cliName: toNonEmptyString(input.cliName, 'codex'),
472
+ taskMode: normalizeTaskMode(input.taskMode),
473
+ openspecTier: normalizeOpenSpecTier(input.openspecTier),
474
+ taskRoutingReason: toNonEmptyString(input.taskRoutingReason),
286
475
  startedAt: startedAt.toISOString(),
287
476
  filePath: toNonEmptyString(options.filePath),
288
477
  label: deriveSessionLabel(branch, worktreePath),
478
+ changedPaths: [],
289
479
  };
290
480
  }
291
481
 
@@ -357,7 +547,7 @@ function readActiveSessions(repoRoot, options = {}) {
357
547
  }
358
548
 
359
549
  normalized.elapsedLabel = formatElapsedFrom(normalized.startedAt, now);
360
- Object.assign(normalized, deriveSessionActivity(normalized));
550
+ Object.assign(normalized, deriveSessionActivity(normalized, { now }));
361
551
  sessions.push(normalized);
362
552
  }
363
553
 
@@ -390,6 +580,9 @@ module.exports = {
390
580
  activeSessionsDirForRepo,
391
581
  buildSessionRecord,
392
582
  collectWorktreeChangedPaths,
583
+ collectWorktreeTrackedPaths,
584
+ deriveBlockingGitLabel,
585
+ deriveLatestWorktreeFileActivity,
393
586
  deriveSessionLabel,
394
587
  deriveSessionActivity,
395
588
  formatElapsedFrom,
@@ -401,6 +594,7 @@ module.exports = {
401
594
  readActiveSessions,
402
595
  readRepoChanges,
403
596
  deriveRepoChangeStatus,
597
+ resolveWorktreeGitDir,
404
598
  sanitizeBranchForFile,
405
599
  sessionFileNameForBranch,
406
600
  sessionFilePathForBranch,