@imdeadpool/guardex 7.0.22 → 7.0.23
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 +6 -0
- package/package.json +5 -1
- package/src/cli/args.js +89 -0
- package/src/cli/main.js +49 -75
- package/src/context.js +16 -2
- package/src/core/stdin.js +52 -0
- package/src/core/versions.js +33 -0
- package/src/hooks/index.js +64 -0
- package/src/output/index.js +64 -4
- package/src/report/session-severity.js +213 -0
- package/src/scaffold/index.js +78 -131
- package/src/toolchain/index.js +6 -70
- package/templates/AGENTS.multiagent-safety.md +25 -0
- package/templates/scripts/agent-branch-finish.sh +30 -1
- package/templates/scripts/agent-session-state.js +62 -1
- package/templates/scripts/codex-agent.sh +38 -0
- package/templates/scripts/install-vscode-active-agents-extension.js +38 -11
- package/templates/scripts/openspec/init-plan-workspace.sh +34 -3
- package/templates/vscode/guardex-active-agents/README.md +7 -6
- package/templates/vscode/guardex-active-agents/extension.js +523 -73
- package/templates/vscode/guardex-active-agents/icon.png +0 -0
- package/templates/vscode/guardex-active-agents/package.json +13 -3
- package/templates/vscode/guardex-active-agents/session-schema.js +311 -4
|
Binary file
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"displayName": "GitGuardex Active Agents",
|
|
4
4
|
"description": "Shows live Guardex sandbox sessions and repo changes inside VS Code Source Control.",
|
|
5
5
|
"publisher": "recodeee",
|
|
6
|
-
"version": "0.0.
|
|
6
|
+
"version": "0.0.7",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"icon": "icon.png",
|
|
9
9
|
"engines": {
|
|
@@ -36,6 +36,11 @@
|
|
|
36
36
|
"title": "Commit Selected Session",
|
|
37
37
|
"icon": "$(check)"
|
|
38
38
|
},
|
|
39
|
+
{
|
|
40
|
+
"command": "gitguardex.activeAgents.inspect",
|
|
41
|
+
"title": "Inspect Session",
|
|
42
|
+
"icon": "$(info)"
|
|
43
|
+
},
|
|
39
44
|
{
|
|
40
45
|
"command": "gitguardex.activeAgents.openWorktree",
|
|
41
46
|
"title": "Open Agent Worktree"
|
|
@@ -73,14 +78,14 @@
|
|
|
73
78
|
"viewsWelcome": [
|
|
74
79
|
{
|
|
75
80
|
"view": "gitguardex.activeAgents",
|
|
76
|
-
"contents": "No
|
|
81
|
+
"contents": "No live Guardex agents are visible in this workspace yet.\n\nThis view tracks Guardex session files and managed worktree telemetry, not every repo visible in Source Control.\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)"
|
|
77
82
|
}
|
|
78
83
|
],
|
|
79
84
|
"menus": {
|
|
80
85
|
"view/title": [
|
|
81
86
|
{
|
|
82
87
|
"command": "gitguardex.activeAgents.commitSelectedSession",
|
|
83
|
-
"when": "view == gitguardex.activeAgents",
|
|
88
|
+
"when": "view == gitguardex.activeAgents && guardex.hasAgents",
|
|
84
89
|
"group": "navigation@1"
|
|
85
90
|
},
|
|
86
91
|
{
|
|
@@ -95,6 +100,11 @@
|
|
|
95
100
|
"when": "view == gitguardex.activeAgents && viewItem == gitguardex.session",
|
|
96
101
|
"group": "inline"
|
|
97
102
|
},
|
|
103
|
+
{
|
|
104
|
+
"command": "gitguardex.activeAgents.inspect",
|
|
105
|
+
"when": "view == gitguardex.activeAgents && viewItem == gitguardex.session",
|
|
106
|
+
"group": "inline"
|
|
107
|
+
},
|
|
98
108
|
{
|
|
99
109
|
"command": "gitguardex.activeAgents.finishSession",
|
|
100
110
|
"when": "view == gitguardex.activeAgents && viewItem == gitguardex.session",
|
|
@@ -5,6 +5,7 @@ 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 LOGS_RELATIVE_DIR = path.join('.omx', 'logs');
|
|
8
9
|
const AGENT_WORKTREE_LOCK_FILE = 'AGENT.lock';
|
|
9
10
|
const MANAGED_WORKTREE_ROOTS = [
|
|
10
11
|
path.join('.omx', 'agent-worktrees'),
|
|
@@ -13,8 +14,42 @@ const MANAGED_WORKTREE_ROOTS = [
|
|
|
13
14
|
const MAX_CHANGED_PATH_PREVIEW = 3;
|
|
14
15
|
const ACTIVE_SESSIONS_FILTER_PREFIX = ACTIVE_SESSIONS_RELATIVE_DIR.split(path.sep).join('/');
|
|
15
16
|
const LOCK_FILE_FILTER_PATH = LOCK_FILE_RELATIVE.split(path.sep).join('/');
|
|
17
|
+
const MANAGED_WORKTREE_FILTER_PREFIXES = MANAGED_WORKTREE_ROOTS
|
|
18
|
+
.map((relativeRoot) => relativeRoot.split(path.sep).join('/').replace(/\/+$/, ''));
|
|
16
19
|
const IDLE_ACTIVITY_WINDOW_MS = 2 * 60 * 1000;
|
|
17
20
|
const STALLED_ACTIVITY_WINDOW_MS = 15 * 60 * 1000;
|
|
21
|
+
const HEARTBEAT_STALE_MS = 5 * 60 * 1000;
|
|
22
|
+
const DEFAULT_BASE_BRANCH = 'dev';
|
|
23
|
+
const DEFAULT_LOG_TAIL_LINE_COUNT = 200;
|
|
24
|
+
const ADVISORY_SESSION_STATES = new Set(['working', 'thinking', 'idle']);
|
|
25
|
+
const WORKTREE_ACTIVITY_CACHE_TTL_MS = 3_000;
|
|
26
|
+
const MAX_WORKTREE_ACTIVITY_STAT_PATHS = 200;
|
|
27
|
+
const WORKTREE_ACTIVITY_SKIP_PREFIXES = [
|
|
28
|
+
'.git/',
|
|
29
|
+
'.omx/',
|
|
30
|
+
'.omc/',
|
|
31
|
+
'node_modules/',
|
|
32
|
+
'dist/',
|
|
33
|
+
'build/',
|
|
34
|
+
'coverage/',
|
|
35
|
+
'.next/',
|
|
36
|
+
'out/',
|
|
37
|
+
'vendor/',
|
|
38
|
+
];
|
|
39
|
+
const WORKTREE_ACTIVITY_PRIORITY_PREFIXES = [
|
|
40
|
+
'src/',
|
|
41
|
+
'app/',
|
|
42
|
+
'apps/',
|
|
43
|
+
'lib/',
|
|
44
|
+
'packages/',
|
|
45
|
+
'scripts/',
|
|
46
|
+
'test/',
|
|
47
|
+
'tests/',
|
|
48
|
+
'vscode/',
|
|
49
|
+
'templates/',
|
|
50
|
+
'openspec/',
|
|
51
|
+
'docs/',
|
|
52
|
+
];
|
|
18
53
|
const BLOCKING_GIT_STATES = [
|
|
19
54
|
{
|
|
20
55
|
label: 'Rebase in progress.',
|
|
@@ -29,6 +64,7 @@ const BLOCKING_GIT_STATES = [
|
|
|
29
64
|
markers: ['CHERRY_PICK_HEAD'],
|
|
30
65
|
},
|
|
31
66
|
];
|
|
67
|
+
const worktreeActivityCache = new Map();
|
|
32
68
|
|
|
33
69
|
function toNonEmptyString(value, fallback = '') {
|
|
34
70
|
const normalized = typeof value === 'string' ? value.trim() : String(value || '').trim();
|
|
@@ -50,6 +86,11 @@ function normalizeOpenSpecTier(value) {
|
|
|
50
86
|
return ['T0', 'T1', 'T2', 'T3'].includes(normalized) ? normalized : '';
|
|
51
87
|
}
|
|
52
88
|
|
|
89
|
+
function normalizeAdvisoryState(value, fallback = 'working') {
|
|
90
|
+
const normalized = toNonEmptyString(value).toLowerCase();
|
|
91
|
+
return ADVISORY_SESSION_STATES.has(normalized) ? normalized : fallback;
|
|
92
|
+
}
|
|
93
|
+
|
|
53
94
|
function sanitizeBranchForFile(branch) {
|
|
54
95
|
const normalized = toNonEmptyString(branch, 'session');
|
|
55
96
|
return normalized.replace(/[^a-zA-Z0-9._-]+/g, '__').replace(/^_+|_+$/g, '') || 'session';
|
|
@@ -93,6 +134,138 @@ function readJsonFile(filePath) {
|
|
|
93
134
|
}
|
|
94
135
|
}
|
|
95
136
|
|
|
137
|
+
function readConfiguredBaseBranch(repoRoot) {
|
|
138
|
+
const lines = runGitLines(path.resolve(repoRoot), ['config', '--get', 'multiagent.baseBranch']);
|
|
139
|
+
if (Array.isArray(lines) && typeof lines[0] === 'string' && lines[0].trim()) {
|
|
140
|
+
return lines[0].trim();
|
|
141
|
+
}
|
|
142
|
+
return DEFAULT_BASE_BRANCH;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function readAheadBehindCounts(worktreePath, branch, baseBranch) {
|
|
146
|
+
const normalizedWorktreePath = toNonEmptyString(worktreePath);
|
|
147
|
+
const normalizedBranch = toNonEmptyString(branch);
|
|
148
|
+
const normalizedBaseBranch = toNonEmptyString(baseBranch, DEFAULT_BASE_BRANCH);
|
|
149
|
+
const compareRef = `origin/${normalizedBaseBranch}`;
|
|
150
|
+
|
|
151
|
+
if (!normalizedWorktreePath || !normalizedBranch) {
|
|
152
|
+
return {
|
|
153
|
+
compareRef,
|
|
154
|
+
aheadCount: null,
|
|
155
|
+
behindCount: null,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const lines = runGitLines(normalizedWorktreePath, [
|
|
160
|
+
'rev-list',
|
|
161
|
+
'--left-right',
|
|
162
|
+
'--count',
|
|
163
|
+
`${normalizedBranch}...${compareRef}`,
|
|
164
|
+
]);
|
|
165
|
+
const match = Array.isArray(lines) && typeof lines[0] === 'string'
|
|
166
|
+
? lines[0].trim().match(/^(\d+)\s+(\d+)$/)
|
|
167
|
+
: null;
|
|
168
|
+
if (!match) {
|
|
169
|
+
return {
|
|
170
|
+
compareRef,
|
|
171
|
+
aheadCount: null,
|
|
172
|
+
behindCount: null,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
compareRef,
|
|
178
|
+
aheadCount: Number.parseInt(match[1], 10),
|
|
179
|
+
behindCount: Number.parseInt(match[2], 10),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function sessionLogPath(repoRoot, branch) {
|
|
184
|
+
const normalizedRepoRoot = toNonEmptyString(repoRoot);
|
|
185
|
+
const normalizedBranch = toNonEmptyString(branch);
|
|
186
|
+
if (!normalizedRepoRoot || !normalizedBranch) {
|
|
187
|
+
return '';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return path.join(
|
|
191
|
+
path.resolve(normalizedRepoRoot),
|
|
192
|
+
LOGS_RELATIVE_DIR,
|
|
193
|
+
`agent-${sanitizeBranchForFile(normalizedBranch)}.log`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function readLogTail(filePath, maxLines = DEFAULT_LOG_TAIL_LINE_COUNT) {
|
|
198
|
+
const normalizedFilePath = toNonEmptyString(filePath);
|
|
199
|
+
const normalizedMaxLines = toPositiveInteger(maxLines) || DEFAULT_LOG_TAIL_LINE_COUNT;
|
|
200
|
+
if (!normalizedFilePath || !fs.existsSync(normalizedFilePath)) {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const lines = fs.readFileSync(normalizedFilePath, 'utf8').split(/\r?\n/);
|
|
206
|
+
while (lines.length > 0 && lines[lines.length - 1] === '') {
|
|
207
|
+
lines.pop();
|
|
208
|
+
}
|
|
209
|
+
return lines.slice(-normalizedMaxLines);
|
|
210
|
+
} catch (_error) {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function readSessionHeldLocks(repoRoot, branch) {
|
|
216
|
+
const normalizedRepoRoot = toNonEmptyString(repoRoot);
|
|
217
|
+
const normalizedBranch = toNonEmptyString(branch);
|
|
218
|
+
if (!normalizedRepoRoot || !normalizedBranch) {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const parsed = readJsonFile(path.join(path.resolve(normalizedRepoRoot), LOCK_FILE_RELATIVE));
|
|
223
|
+
const locks = parsed?.locks;
|
|
224
|
+
if (!locks || typeof locks !== 'object' || Array.isArray(locks)) {
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return Object.entries(locks)
|
|
229
|
+
.map(([rawRelativePath, entry]) => {
|
|
230
|
+
if (!entry || typeof entry !== 'object') {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const relativePath = normalizeRelativePath(rawRelativePath);
|
|
235
|
+
const ownerBranch = toNonEmptyString(entry.branch);
|
|
236
|
+
if (!relativePath || ownerBranch !== normalizedBranch) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
relativePath,
|
|
242
|
+
claimedAt: toNonEmptyString(entry.claimed_at),
|
|
243
|
+
allowDelete: Boolean(entry.allow_delete),
|
|
244
|
+
};
|
|
245
|
+
})
|
|
246
|
+
.filter(Boolean)
|
|
247
|
+
.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function readSessionInspectData(session, options = {}) {
|
|
251
|
+
const repoRoot = toNonEmptyString(session?.repoRoot);
|
|
252
|
+
const branch = toNonEmptyString(session?.branch);
|
|
253
|
+
const worktreePath = toNonEmptyString(session?.worktreePath);
|
|
254
|
+
const baseBranch = readConfiguredBaseBranch(repoRoot);
|
|
255
|
+
const logPath = sessionLogPath(repoRoot, branch);
|
|
256
|
+
const logTailLines = readLogTail(logPath, options.logLines);
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
baseBranch,
|
|
260
|
+
logPath,
|
|
261
|
+
logExists: Boolean(logPath) && fs.existsSync(logPath),
|
|
262
|
+
logTailLines,
|
|
263
|
+
logTailText: logTailLines.join('\n'),
|
|
264
|
+
heldLocks: readSessionHeldLocks(repoRoot, branch),
|
|
265
|
+
...readAheadBehindCounts(worktreePath, branch, baseBranch),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
96
269
|
function normalizeIsoString(value, fallback = '') {
|
|
97
270
|
const normalized = toNonEmptyString(value);
|
|
98
271
|
if (!normalized) {
|
|
@@ -212,6 +385,9 @@ function parseRepoChangeLine(repoRoot, line) {
|
|
|
212
385
|
|| normalizedRelativePath.startsWith(`${LOCK_FILE_FILTER_PATH}/`)
|
|
213
386
|
|| normalizedRelativePath === ACTIVE_SESSIONS_FILTER_PREFIX
|
|
214
387
|
|| normalizedRelativePath.startsWith(`${ACTIVE_SESSIONS_FILTER_PREFIX}/`)
|
|
388
|
+
|| MANAGED_WORKTREE_FILTER_PREFIXES.some((prefix) => (
|
|
389
|
+
normalizedRelativePath === prefix || normalizedRelativePath.startsWith(`${prefix}/`)
|
|
390
|
+
))
|
|
215
391
|
) {
|
|
216
392
|
return null;
|
|
217
393
|
}
|
|
@@ -294,14 +470,80 @@ function collectWorktreeTrackedPaths(worktreePath) {
|
|
|
294
470
|
.sort((left, right) => left.localeCompare(right));
|
|
295
471
|
}
|
|
296
472
|
|
|
297
|
-
function
|
|
473
|
+
function shouldSkipWorktreeActivityPath(relativePath) {
|
|
474
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
475
|
+
if (!normalized || normalized === LOCK_FILE_RELATIVE || normalized === AGENT_WORKTREE_LOCK_FILE) {
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return WORKTREE_ACTIVITY_SKIP_PREFIXES.some((prefix) => (
|
|
480
|
+
normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
|
|
481
|
+
));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function worktreeActivityPathPriority(relativePath, recentPathsSet) {
|
|
485
|
+
if (recentPathsSet.has(relativePath)) {
|
|
486
|
+
return 0;
|
|
487
|
+
}
|
|
488
|
+
if (!relativePath.includes('/')) {
|
|
489
|
+
return 1;
|
|
490
|
+
}
|
|
491
|
+
if (WORKTREE_ACTIVITY_PRIORITY_PREFIXES.some((prefix) => relativePath.startsWith(prefix))) {
|
|
492
|
+
return 2;
|
|
493
|
+
}
|
|
494
|
+
return 3;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function collectWorktreeActivityCandidatePaths(worktreePath, trackedPaths) {
|
|
498
|
+
const recentPaths = runGitLines(worktreePath, ['log', '-1', '--name-only', '--pretty=format:', '--', '.']) || [];
|
|
499
|
+
const filteredRecentPaths = [...new Set(recentPaths.map(normalizeRelativePath).filter(Boolean))]
|
|
500
|
+
.filter((relativePath) => !shouldSkipWorktreeActivityPath(relativePath));
|
|
501
|
+
const recentPathSet = new Set(filteredRecentPaths);
|
|
502
|
+
const prioritizedTrackedPaths = trackedPaths
|
|
503
|
+
.map(normalizeRelativePath)
|
|
504
|
+
.filter(Boolean)
|
|
505
|
+
.filter((relativePath) => !shouldSkipWorktreeActivityPath(relativePath))
|
|
506
|
+
.sort((left, right) => {
|
|
507
|
+
const priorityDelta = worktreeActivityPathPriority(left, recentPathSet)
|
|
508
|
+
- worktreeActivityPathPriority(right, recentPathSet);
|
|
509
|
+
if (priorityDelta !== 0) {
|
|
510
|
+
return priorityDelta;
|
|
511
|
+
}
|
|
512
|
+
return left.localeCompare(right);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
return [...new Set([...filteredRecentPaths, ...prioritizedTrackedPaths])]
|
|
516
|
+
.slice(0, MAX_WORKTREE_ACTIVITY_STAT_PATHS);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function clearWorktreeActivityCache(worktreePath = '') {
|
|
520
|
+
const normalizedWorktreePath = toNonEmptyString(worktreePath);
|
|
521
|
+
if (!normalizedWorktreePath) {
|
|
522
|
+
worktreeActivityCache.clear();
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
worktreeActivityCache.delete(path.resolve(normalizedWorktreePath));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function deriveLatestWorktreeFileActivity(worktreePath, options = {}) {
|
|
529
|
+
const now = Number.isFinite(options.now) ? options.now : Date.now();
|
|
530
|
+
const useCache = options.useCache !== false;
|
|
531
|
+
const cacheKey = path.resolve(worktreePath);
|
|
532
|
+
if (useCache) {
|
|
533
|
+
const cached = worktreeActivityCache.get(cacheKey);
|
|
534
|
+
if (cached && (now - cached.checkedAtMs) < WORKTREE_ACTIVITY_CACHE_TTL_MS) {
|
|
535
|
+
return cached.latestMtimeMs;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
298
539
|
const trackedPaths = collectWorktreeTrackedPaths(worktreePath);
|
|
299
540
|
if (!trackedPaths) {
|
|
300
541
|
return null;
|
|
301
542
|
}
|
|
302
543
|
|
|
544
|
+
const candidatePaths = collectWorktreeActivityCandidatePaths(worktreePath, trackedPaths);
|
|
303
545
|
let latestMtimeMs = null;
|
|
304
|
-
for (const relativePath of
|
|
546
|
+
for (const relativePath of candidatePaths) {
|
|
305
547
|
const absolutePath = path.join(worktreePath, relativePath);
|
|
306
548
|
try {
|
|
307
549
|
const stats = fs.statSync(absolutePath);
|
|
@@ -316,6 +558,13 @@ function deriveLatestWorktreeFileActivity(worktreePath) {
|
|
|
316
558
|
}
|
|
317
559
|
}
|
|
318
560
|
|
|
561
|
+
if (useCache) {
|
|
562
|
+
worktreeActivityCache.set(cacheKey, {
|
|
563
|
+
checkedAtMs: now,
|
|
564
|
+
latestMtimeMs,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
319
568
|
return latestMtimeMs;
|
|
320
569
|
}
|
|
321
570
|
|
|
@@ -323,6 +572,23 @@ function deriveSessionActivity(session, options = {}) {
|
|
|
323
572
|
const now = Number.isFinite(options.now) ? options.now : Date.now();
|
|
324
573
|
const pid = toPositiveInteger(session?.pid);
|
|
325
574
|
const pidAlive = pid ? isPidAlive(pid) : null;
|
|
575
|
+
const heartbeatAt = normalizeIsoString(session?.lastHeartbeatAt);
|
|
576
|
+
const heartbeatMs = Date.parse(heartbeatAt);
|
|
577
|
+
if (heartbeatAt && Number.isFinite(heartbeatMs) && now - heartbeatMs > HEARTBEAT_STALE_MS) {
|
|
578
|
+
return {
|
|
579
|
+
activityKind: 'dead',
|
|
580
|
+
activityLabel: 'dead',
|
|
581
|
+
activityCountLabel: '',
|
|
582
|
+
activitySummary: `Heartbeat stale for ${formatElapsedFrom(heartbeatAt, now)}.`,
|
|
583
|
+
changeCount: 0,
|
|
584
|
+
changedPaths: [],
|
|
585
|
+
worktreeChangedPaths: [],
|
|
586
|
+
pidAlive,
|
|
587
|
+
lastFileActivityAt: '',
|
|
588
|
+
lastFileActivityLabel: '',
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
326
592
|
const blockingLabel = deriveBlockingGitLabel(session.worktreePath);
|
|
327
593
|
if (blockingLabel) {
|
|
328
594
|
return {
|
|
@@ -332,6 +598,7 @@ function deriveSessionActivity(session, options = {}) {
|
|
|
332
598
|
activitySummary: blockingLabel,
|
|
333
599
|
changeCount: 0,
|
|
334
600
|
changedPaths: [],
|
|
601
|
+
worktreeChangedPaths: [],
|
|
335
602
|
pidAlive,
|
|
336
603
|
lastFileActivityAt: '',
|
|
337
604
|
lastFileActivityLabel: '',
|
|
@@ -346,6 +613,7 @@ function deriveSessionActivity(session, options = {}) {
|
|
|
346
613
|
activitySummary: 'Recorded PID is not alive.',
|
|
347
614
|
changeCount: 0,
|
|
348
615
|
changedPaths: [],
|
|
616
|
+
worktreeChangedPaths: [],
|
|
349
617
|
pidAlive,
|
|
350
618
|
lastFileActivityAt: '',
|
|
351
619
|
lastFileActivityLabel: '',
|
|
@@ -361,6 +629,7 @@ function deriveSessionActivity(session, options = {}) {
|
|
|
361
629
|
activitySummary: 'Worktree activity unavailable.',
|
|
362
630
|
changeCount: 0,
|
|
363
631
|
changedPaths: [],
|
|
632
|
+
worktreeChangedPaths: [],
|
|
364
633
|
pidAlive,
|
|
365
634
|
lastFileActivityAt: '',
|
|
366
635
|
lastFileActivityLabel: '',
|
|
@@ -368,6 +637,11 @@ function deriveSessionActivity(session, options = {}) {
|
|
|
368
637
|
}
|
|
369
638
|
|
|
370
639
|
if (worktreeChangedPaths.length > 0) {
|
|
640
|
+
const worktreeRelativePaths = [...new Set(worktreeChangedPaths
|
|
641
|
+
.map((relativePath) => normalizeRelativePath(relativePath))
|
|
642
|
+
.filter(Boolean))]
|
|
643
|
+
.sort((left, right) => left.localeCompare(right));
|
|
644
|
+
clearWorktreeActivityCache(session.worktreePath);
|
|
371
645
|
const changedPaths = [...new Set(worktreeChangedPaths
|
|
372
646
|
.map((relativePath) => normalizeRelativePath(
|
|
373
647
|
path.relative(session.repoRoot, path.resolve(session.worktreePath, relativePath)),
|
|
@@ -382,13 +656,17 @@ function deriveSessionActivity(session, options = {}) {
|
|
|
382
656
|
activitySummary: previewChangedPaths(worktreeChangedPaths),
|
|
383
657
|
changeCount: worktreeChangedPaths.length,
|
|
384
658
|
changedPaths,
|
|
659
|
+
worktreeChangedPaths: worktreeRelativePaths,
|
|
385
660
|
pidAlive,
|
|
386
661
|
lastFileActivityAt: '',
|
|
387
662
|
lastFileActivityLabel: '',
|
|
388
663
|
};
|
|
389
664
|
}
|
|
390
665
|
|
|
391
|
-
const latestFileActivityMs = deriveLatestWorktreeFileActivity(session.worktreePath
|
|
666
|
+
const latestFileActivityMs = deriveLatestWorktreeFileActivity(session.worktreePath, {
|
|
667
|
+
now,
|
|
668
|
+
useCache: options.useCache,
|
|
669
|
+
});
|
|
392
670
|
const lastFileActivityAt = Number.isFinite(latestFileActivityMs)
|
|
393
671
|
? new Date(latestFileActivityMs).toISOString()
|
|
394
672
|
: '';
|
|
@@ -407,6 +685,7 @@ function deriveSessionActivity(session, options = {}) {
|
|
|
407
685
|
activitySummary: `Worktree clean. No file activity for ${lastFileActivityLabel}.`,
|
|
408
686
|
changeCount: 0,
|
|
409
687
|
changedPaths: [],
|
|
688
|
+
worktreeChangedPaths: [],
|
|
410
689
|
pidAlive,
|
|
411
690
|
lastFileActivityAt,
|
|
412
691
|
lastFileActivityLabel,
|
|
@@ -424,6 +703,7 @@ function deriveSessionActivity(session, options = {}) {
|
|
|
424
703
|
: 'Worktree clean.',
|
|
425
704
|
changeCount: 0,
|
|
426
705
|
changedPaths: [],
|
|
706
|
+
worktreeChangedPaths: [],
|
|
427
707
|
pidAlive,
|
|
428
708
|
lastFileActivityAt,
|
|
429
709
|
lastFileActivityLabel,
|
|
@@ -436,6 +716,7 @@ function buildSessionRecord(input) {
|
|
|
436
716
|
const branch = toNonEmptyString(input.branch);
|
|
437
717
|
const pid = toPositiveInteger(input.pid);
|
|
438
718
|
const startedAt = input.startedAt ? new Date(input.startedAt) : new Date();
|
|
719
|
+
const lastHeartbeatAt = input.lastHeartbeatAt ? new Date(input.lastHeartbeatAt) : new Date();
|
|
439
720
|
|
|
440
721
|
if (!branch) {
|
|
441
722
|
throw new Error('branch is required');
|
|
@@ -452,6 +733,9 @@ function buildSessionRecord(input) {
|
|
|
452
733
|
if (Number.isNaN(startedAt.getTime())) {
|
|
453
734
|
throw new Error('startedAt must be a valid date');
|
|
454
735
|
}
|
|
736
|
+
if (Number.isNaN(lastHeartbeatAt.getTime())) {
|
|
737
|
+
throw new Error('lastHeartbeatAt must be a valid date');
|
|
738
|
+
}
|
|
455
739
|
|
|
456
740
|
return {
|
|
457
741
|
schemaVersion: SESSION_SCHEMA_VERSION,
|
|
@@ -467,6 +751,8 @@ function buildSessionRecord(input) {
|
|
|
467
751
|
openspecTier: normalizeOpenSpecTier(input.openspecTier),
|
|
468
752
|
taskRoutingReason: toNonEmptyString(input.taskRoutingReason),
|
|
469
753
|
startedAt: startedAt.toISOString(),
|
|
754
|
+
lastHeartbeatAt: lastHeartbeatAt.toISOString(),
|
|
755
|
+
state: normalizeAdvisoryState(input.state),
|
|
470
756
|
};
|
|
471
757
|
}
|
|
472
758
|
|
|
@@ -487,9 +773,17 @@ function normalizeSessionRecord(input, options = {}) {
|
|
|
487
773
|
const branch = toNonEmptyString(input.branch);
|
|
488
774
|
const worktreePath = toNonEmptyString(input.worktreePath);
|
|
489
775
|
const startedAt = new Date(input.startedAt);
|
|
776
|
+
const lastHeartbeatAt = new Date(input.lastHeartbeatAt || input.startedAt);
|
|
490
777
|
const pid = toPositiveInteger(input.pid);
|
|
491
778
|
|
|
492
|
-
if (
|
|
779
|
+
if (
|
|
780
|
+
!repoRoot
|
|
781
|
+
|| !branch
|
|
782
|
+
|| !worktreePath
|
|
783
|
+
|| !pid
|
|
784
|
+
|| Number.isNaN(startedAt.getTime())
|
|
785
|
+
|| Number.isNaN(lastHeartbeatAt.getTime())
|
|
786
|
+
) {
|
|
493
787
|
return null;
|
|
494
788
|
}
|
|
495
789
|
|
|
@@ -507,9 +801,12 @@ function normalizeSessionRecord(input, options = {}) {
|
|
|
507
801
|
openspecTier: normalizeOpenSpecTier(input.openspecTier),
|
|
508
802
|
taskRoutingReason: toNonEmptyString(input.taskRoutingReason),
|
|
509
803
|
startedAt: startedAt.toISOString(),
|
|
804
|
+
lastHeartbeatAt: lastHeartbeatAt.toISOString(),
|
|
805
|
+
state: normalizeAdvisoryState(input.state, 'idle'),
|
|
510
806
|
filePath: toNonEmptyString(options.filePath),
|
|
511
807
|
label: deriveSessionLabel(branch, worktreePath),
|
|
512
808
|
changedPaths: [],
|
|
809
|
+
worktreeChangedPaths: [],
|
|
513
810
|
sourceKind: 'active-session',
|
|
514
811
|
telemetryUpdatedAt: '',
|
|
515
812
|
telemetrySource: '',
|
|
@@ -646,9 +943,12 @@ function buildWorktreeLockSession(repoRoot, worktreePath, lockPayload, options =
|
|
|
646
943
|
openspecTier: '',
|
|
647
944
|
taskRoutingReason: '',
|
|
648
945
|
startedAt,
|
|
946
|
+
lastHeartbeatAt: '',
|
|
947
|
+
state: '',
|
|
649
948
|
filePath: path.join(worktreePath, AGENT_WORKTREE_LOCK_FILE),
|
|
650
949
|
label,
|
|
651
950
|
changedPaths: [],
|
|
951
|
+
worktreeChangedPaths: [],
|
|
652
952
|
sourceKind: 'worktree-lock',
|
|
653
953
|
telemetryUpdatedAt: telemetryUpdatedAt || startedAt,
|
|
654
954
|
telemetrySource: toNonEmptyString(lockPayload?.source, 'worktree-lock'),
|
|
@@ -782,6 +1082,7 @@ module.exports = {
|
|
|
782
1082
|
SESSION_SCHEMA_VERSION,
|
|
783
1083
|
activeSessionsDirForRepo,
|
|
784
1084
|
buildSessionRecord,
|
|
1085
|
+
clearWorktreeActivityCache,
|
|
785
1086
|
collectWorktreeChangedPaths,
|
|
786
1087
|
collectWorktreeTrackedPaths,
|
|
787
1088
|
deriveBlockingGitLabel,
|
|
@@ -798,7 +1099,13 @@ module.exports = {
|
|
|
798
1099
|
readWorktreeLockSessions,
|
|
799
1100
|
readRepoChanges,
|
|
800
1101
|
deriveRepoChangeStatus,
|
|
1102
|
+
readAheadBehindCounts,
|
|
1103
|
+
readConfiguredBaseBranch,
|
|
1104
|
+
readLogTail,
|
|
801
1105
|
resolveWorktreeGitDir,
|
|
1106
|
+
readSessionHeldLocks,
|
|
1107
|
+
readSessionInspectData,
|
|
1108
|
+
sessionLogPath,
|
|
802
1109
|
sanitizeBranchForFile,
|
|
803
1110
|
sessionFileNameForBranch,
|
|
804
1111
|
sessionFilePathForBranch,
|