@imdeadpool/guardex 7.0.41 → 7.0.43
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 +68 -13
- package/package.json +2 -1
- package/skills/gitguardex/SKILL.md +13 -0
- package/skills/guardex-merge-skills-to-dev/SKILL.md +59 -0
- package/src/agents/cleanup-sessions.js +126 -0
- package/src/agents/detect.js +160 -0
- package/src/agents/finish.js +172 -0
- package/src/agents/inspect.js +189 -0
- package/src/agents/launch.js +240 -0
- package/src/agents/registry.js +133 -0
- package/src/agents/selection-panel.js +571 -0
- package/src/agents/sessions.js +151 -0
- package/src/agents/start.js +591 -0
- package/src/agents/status.js +143 -0
- package/src/agents/terminal.js +152 -0
- package/src/budget/index.js +343 -0
- package/src/ci-init/index.js +265 -0
- package/src/cli/args.js +305 -1
- package/src/cli/main.js +262 -132
- package/src/cockpit/action-runner.js +3 -0
- package/src/cockpit/actions.js +80 -0
- package/src/cockpit/control.js +1121 -0
- package/src/cockpit/index.js +426 -0
- package/src/cockpit/keybindings.js +224 -0
- package/src/cockpit/kitty-layout.js +549 -0
- package/src/cockpit/kitty-tree.js +144 -0
- package/src/cockpit/layout.js +224 -0
- package/src/cockpit/logs-reader.js +182 -0
- package/src/cockpit/menu.js +204 -0
- package/src/cockpit/pane-actions.js +597 -0
- package/src/cockpit/pane-menu.js +387 -0
- package/src/cockpit/projects-finder.js +178 -0
- package/src/cockpit/render.js +215 -0
- package/src/cockpit/settings-render.js +128 -0
- package/src/cockpit/settings.js +124 -0
- package/src/cockpit/shortcuts.js +24 -0
- package/src/cockpit/sidebar.js +311 -0
- package/src/cockpit/state.js +72 -0
- package/src/cockpit/theme.js +128 -0
- package/src/cockpit/welcome.js +266 -0
- package/src/context.js +76 -33
- package/src/doctor/index.js +3 -2
- package/src/finish/index.js +39 -2
- package/src/git/index.js +65 -0
- package/src/kitty/command.js +101 -0
- package/src/kitty/runtime.js +250 -0
- package/src/output/index.js +1 -1
- package/src/pr-review.js +241 -0
- package/src/scaffold/index.js +19 -0
- package/src/submodule/index.js +288 -0
- package/src/terminal/index.js +120 -0
- package/src/terminal/kitty.js +622 -0
- package/src/terminal/tmux.js +126 -0
- package/src/tmux/command.js +27 -0
- package/src/tmux/session.js +89 -0
- package/templates/AGENTS.multiagent-safety.md +27 -1
- package/templates/codex/skills/gitguardex/SKILL.md +2 -0
- package/templates/githooks/pre-commit +22 -1
- package/templates/github/workflows/README.md +87 -0
- package/templates/github/workflows/ci-full.yml +55 -0
- package/templates/github/workflows/ci.yml +56 -0
- package/templates/github/workflows/cr.yml +20 -1
- package/templates/scripts/agent-branch-finish.sh +544 -26
- package/templates/scripts/agent-branch-start.sh +89 -22
- package/templates/scripts/agent-preflight.sh +89 -0
- package/templates/scripts/agent-worktree-prune.sh +96 -5
- package/templates/scripts/codex-agent.sh +41 -6
- package/templates/scripts/openspec/init-plan-workspace.sh +43 -0
- package/templates/scripts/review-bot-watch.sh +31 -2
- package/templates/scripts/agent-session-state.js +0 -171
- package/templates/scripts/install-vscode-active-agents-extension.js +0 -135
- package/templates/vscode/guardex-active-agents/README.md +0 -34
- package/templates/vscode/guardex-active-agents/extension.js +0 -3782
- package/templates/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json +0 -54
- package/templates/vscode/guardex-active-agents/fileicons/icons/agent.svg +0 -5
- package/templates/vscode/guardex-active-agents/fileicons/icons/branch.svg +0 -7
- package/templates/vscode/guardex-active-agents/fileicons/icons/config.svg +0 -4
- package/templates/vscode/guardex-active-agents/fileicons/icons/hook.svg +0 -4
- package/templates/vscode/guardex-active-agents/fileicons/icons/openspec.svg +0 -5
- package/templates/vscode/guardex-active-agents/fileicons/icons/plan.svg +0 -4
- package/templates/vscode/guardex-active-agents/fileicons/icons/spec.svg +0 -5
- package/templates/vscode/guardex-active-agents/icon.png +0 -0
- package/templates/vscode/guardex-active-agents/media/active-agents-hivemind.svg +0 -14
- package/templates/vscode/guardex-active-agents/package.json +0 -169
- package/templates/vscode/guardex-active-agents/session-schema.js +0 -1348
|
@@ -1,1348 +0,0 @@
|
|
|
1
|
-
const fs = require('node:fs');
|
|
2
|
-
const path = require('node:path');
|
|
3
|
-
const cp = require('node:child_process');
|
|
4
|
-
|
|
5
|
-
const ACTIVE_SESSIONS_RELATIVE_DIR = path.join('.omx', 'state', 'active-sessions');
|
|
6
|
-
const SESSION_SCHEMA_VERSION = 1;
|
|
7
|
-
const LOCK_FILE_RELATIVE = path.join('.omx', 'state', 'agent-file-locks.json');
|
|
8
|
-
const LOGS_RELATIVE_DIR = path.join('.omx', 'logs');
|
|
9
|
-
const AGENT_WORKTREE_LOCK_FILE = 'AGENT.lock';
|
|
10
|
-
const MANAGED_WORKTREE_ROOTS = [
|
|
11
|
-
path.join('.omx', 'agent-worktrees'),
|
|
12
|
-
path.join('.omc', 'agent-worktrees'),
|
|
13
|
-
];
|
|
14
|
-
const MAX_CHANGED_PATH_PREVIEW = 3;
|
|
15
|
-
const ACTIVE_SESSIONS_FILTER_PREFIX = ACTIVE_SESSIONS_RELATIVE_DIR.split(path.sep).join('/');
|
|
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(/\/+$/, ''));
|
|
19
|
-
const IDLE_ACTIVITY_WINDOW_MS = 2 * 60 * 1000;
|
|
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
|
-
];
|
|
53
|
-
const BLOCKING_GIT_STATES = [
|
|
54
|
-
{
|
|
55
|
-
label: 'Rebase in progress.',
|
|
56
|
-
markers: ['REBASE_HEAD', 'rebase-apply', 'rebase-merge'],
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
label: 'Merge in progress.',
|
|
60
|
-
markers: ['MERGE_HEAD'],
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
label: 'Cherry-pick in progress.',
|
|
64
|
-
markers: ['CHERRY_PICK_HEAD'],
|
|
65
|
-
},
|
|
66
|
-
];
|
|
67
|
-
const worktreeActivityCache = new Map();
|
|
68
|
-
|
|
69
|
-
function toNonEmptyString(value, fallback = '') {
|
|
70
|
-
const normalized = typeof value === 'string' ? value.trim() : String(value || '').trim();
|
|
71
|
-
return normalized || fallback;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function toPositiveInteger(value) {
|
|
75
|
-
const normalized = Number.parseInt(String(value || ''), 10);
|
|
76
|
-
return Number.isInteger(normalized) && normalized > 0 ? normalized : null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function toBoundedInteger(value, min, max) {
|
|
80
|
-
const normalized = Number.parseInt(String(value ?? ''), 10);
|
|
81
|
-
if (!Number.isInteger(normalized) || normalized < min || normalized > max) {
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
return normalized;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function normalizeStringList(values) {
|
|
88
|
-
if (!Array.isArray(values)) {
|
|
89
|
-
return [];
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return values
|
|
93
|
-
.map((value) => toNonEmptyString(value))
|
|
94
|
-
.filter(Boolean);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function normalizeSessionHealthPayload(input) {
|
|
98
|
-
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const rawScores = input.scores && typeof input.scores === 'object' && !Array.isArray(input.scores)
|
|
103
|
-
? input.scores
|
|
104
|
-
: null;
|
|
105
|
-
const score = toBoundedInteger(input.score ?? input.total ?? rawScores?.total, 0, 100);
|
|
106
|
-
if (score === null) {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
score,
|
|
112
|
-
label: toNonEmptyString(input.label),
|
|
113
|
-
primaryDriver: toNonEmptyString(input.primaryDriver),
|
|
114
|
-
secondaries: normalizeStringList(input.secondaries),
|
|
115
|
-
outputLine: toNonEmptyString(input.outputLine),
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function normalizeTaskMode(value) {
|
|
120
|
-
const normalized = toNonEmptyString(value).toLowerCase();
|
|
121
|
-
return normalized === 'caveman' || normalized === 'omx' ? normalized : '';
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function normalizeOpenSpecTier(value) {
|
|
125
|
-
const normalized = toNonEmptyString(value).toUpperCase();
|
|
126
|
-
return ['T0', 'T1', 'T2', 'T3'].includes(normalized) ? normalized : '';
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function normalizeAdvisoryState(value, fallback = 'working') {
|
|
130
|
-
const normalized = toNonEmptyString(value).toLowerCase();
|
|
131
|
-
return ADVISORY_SESSION_STATES.has(normalized) ? normalized : fallback;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function sanitizeBranchForFile(branch) {
|
|
135
|
-
const normalized = toNonEmptyString(branch, 'session');
|
|
136
|
-
return normalized.replace(/[^a-zA-Z0-9._-]+/g, '__').replace(/^_+|_+$/g, '') || 'session';
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function sessionFileNameForBranch(branch) {
|
|
140
|
-
return `${sanitizeBranchForFile(branch)}.json`;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function activeSessionsDirForRepo(repoRoot) {
|
|
144
|
-
return path.join(path.resolve(repoRoot), ACTIVE_SESSIONS_RELATIVE_DIR);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function sessionFilePathForBranch(repoRoot, branch) {
|
|
148
|
-
return path.join(activeSessionsDirForRepo(repoRoot), sessionFileNameForBranch(branch));
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function resolveManagedWorktreeRoots(repoRoot) {
|
|
152
|
-
return MANAGED_WORKTREE_ROOTS.map((relativeRoot) => path.join(path.resolve(repoRoot), relativeRoot));
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function splitOutputLines(output) {
|
|
156
|
-
if (typeof output !== 'string') {
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return output
|
|
161
|
-
.split(/\r?\n/)
|
|
162
|
-
.filter((line) => line.trim().length > 0);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function normalizeRelativePath(value) {
|
|
166
|
-
return toNonEmptyString(value).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function normalizeProjectPath(value) {
|
|
170
|
-
const normalized = toNonEmptyString(value);
|
|
171
|
-
if (!normalized) {
|
|
172
|
-
return '';
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return path.isAbsolute(normalized)
|
|
176
|
-
? path.resolve(normalized)
|
|
177
|
-
: normalizeRelativePath(normalized);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function readJsonFile(filePath) {
|
|
181
|
-
try {
|
|
182
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
183
|
-
} catch (_error) {
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function readConfiguredBaseBranch(repoRoot) {
|
|
189
|
-
const lines = runGitLines(path.resolve(repoRoot), ['config', '--get', 'multiagent.baseBranch']);
|
|
190
|
-
if (Array.isArray(lines) && typeof lines[0] === 'string' && lines[0].trim()) {
|
|
191
|
-
return lines[0].trim();
|
|
192
|
-
}
|
|
193
|
-
return DEFAULT_BASE_BRANCH;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function readAheadBehindCounts(worktreePath, branch, baseBranch) {
|
|
197
|
-
const normalizedWorktreePath = toNonEmptyString(worktreePath);
|
|
198
|
-
const normalizedBranch = toNonEmptyString(branch);
|
|
199
|
-
const normalizedBaseBranch = toNonEmptyString(baseBranch, DEFAULT_BASE_BRANCH);
|
|
200
|
-
const compareRef = `origin/${normalizedBaseBranch}`;
|
|
201
|
-
|
|
202
|
-
if (!normalizedWorktreePath || !normalizedBranch) {
|
|
203
|
-
return {
|
|
204
|
-
compareRef,
|
|
205
|
-
aheadCount: null,
|
|
206
|
-
behindCount: null,
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const lines = runGitLines(normalizedWorktreePath, [
|
|
211
|
-
'rev-list',
|
|
212
|
-
'--left-right',
|
|
213
|
-
'--count',
|
|
214
|
-
`${normalizedBranch}...${compareRef}`,
|
|
215
|
-
]);
|
|
216
|
-
const match = Array.isArray(lines) && typeof lines[0] === 'string'
|
|
217
|
-
? lines[0].trim().match(/^(\d+)\s+(\d+)$/)
|
|
218
|
-
: null;
|
|
219
|
-
if (!match) {
|
|
220
|
-
return {
|
|
221
|
-
compareRef,
|
|
222
|
-
aheadCount: null,
|
|
223
|
-
behindCount: null,
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return {
|
|
228
|
-
compareRef,
|
|
229
|
-
aheadCount: Number.parseInt(match[1], 10),
|
|
230
|
-
behindCount: Number.parseInt(match[2], 10),
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function sessionLogPath(repoRoot, branch) {
|
|
235
|
-
const normalizedRepoRoot = toNonEmptyString(repoRoot);
|
|
236
|
-
const normalizedBranch = toNonEmptyString(branch);
|
|
237
|
-
if (!normalizedRepoRoot || !normalizedBranch) {
|
|
238
|
-
return '';
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return path.join(
|
|
242
|
-
path.resolve(normalizedRepoRoot),
|
|
243
|
-
LOGS_RELATIVE_DIR,
|
|
244
|
-
`agent-${sanitizeBranchForFile(normalizedBranch)}.log`,
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function readLogTail(filePath, maxLines = DEFAULT_LOG_TAIL_LINE_COUNT) {
|
|
249
|
-
const normalizedFilePath = toNonEmptyString(filePath);
|
|
250
|
-
const normalizedMaxLines = toPositiveInteger(maxLines) || DEFAULT_LOG_TAIL_LINE_COUNT;
|
|
251
|
-
if (!normalizedFilePath || !fs.existsSync(normalizedFilePath)) {
|
|
252
|
-
return [];
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
try {
|
|
256
|
-
const lines = fs.readFileSync(normalizedFilePath, 'utf8').split(/\r?\n/);
|
|
257
|
-
while (lines.length > 0 && lines[lines.length - 1] === '') {
|
|
258
|
-
lines.pop();
|
|
259
|
-
}
|
|
260
|
-
return lines.slice(-normalizedMaxLines);
|
|
261
|
-
} catch (_error) {
|
|
262
|
-
return [];
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function readSessionHeldLocks(repoRoot, branch) {
|
|
267
|
-
const normalizedRepoRoot = toNonEmptyString(repoRoot);
|
|
268
|
-
const normalizedBranch = toNonEmptyString(branch);
|
|
269
|
-
if (!normalizedRepoRoot || !normalizedBranch) {
|
|
270
|
-
return [];
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const parsed = readJsonFile(path.join(path.resolve(normalizedRepoRoot), LOCK_FILE_RELATIVE));
|
|
274
|
-
const locks = parsed?.locks;
|
|
275
|
-
if (!locks || typeof locks !== 'object' || Array.isArray(locks)) {
|
|
276
|
-
return [];
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return Object.entries(locks)
|
|
280
|
-
.map(([rawRelativePath, entry]) => {
|
|
281
|
-
if (!entry || typeof entry !== 'object') {
|
|
282
|
-
return null;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const relativePath = normalizeRelativePath(rawRelativePath);
|
|
286
|
-
const ownerBranch = toNonEmptyString(entry.branch);
|
|
287
|
-
if (!relativePath || ownerBranch !== normalizedBranch) {
|
|
288
|
-
return null;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return {
|
|
292
|
-
relativePath,
|
|
293
|
-
claimedAt: toNonEmptyString(entry.claimed_at),
|
|
294
|
-
allowDelete: Boolean(entry.allow_delete),
|
|
295
|
-
};
|
|
296
|
-
})
|
|
297
|
-
.filter(Boolean)
|
|
298
|
-
.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function readSessionInspectData(session, options = {}) {
|
|
302
|
-
const repoRoot = toNonEmptyString(session?.repoRoot);
|
|
303
|
-
const branch = toNonEmptyString(session?.branch);
|
|
304
|
-
const worktreePath = toNonEmptyString(session?.worktreePath);
|
|
305
|
-
const baseBranch = readConfiguredBaseBranch(repoRoot);
|
|
306
|
-
const logPath = sessionLogPath(repoRoot, branch);
|
|
307
|
-
const logTailLines = readLogTail(logPath, options.logLines);
|
|
308
|
-
|
|
309
|
-
return {
|
|
310
|
-
baseBranch,
|
|
311
|
-
logPath,
|
|
312
|
-
logExists: Boolean(logPath) && fs.existsSync(logPath),
|
|
313
|
-
logTailLines,
|
|
314
|
-
logTailText: logTailLines.join('\n'),
|
|
315
|
-
heldLocks: readSessionHeldLocks(repoRoot, branch),
|
|
316
|
-
...readAheadBehindCounts(worktreePath, branch, baseBranch),
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function normalizeIsoString(value, fallback = '') {
|
|
321
|
-
const normalized = toNonEmptyString(value);
|
|
322
|
-
if (!normalized) {
|
|
323
|
-
return fallback;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const timestamp = Date.parse(normalized);
|
|
327
|
-
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : fallback;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function runGitLines(worktreePath, args) {
|
|
331
|
-
try {
|
|
332
|
-
const output = cp.execFileSync('git', ['-C', worktreePath, ...args], {
|
|
333
|
-
encoding: 'utf8',
|
|
334
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
335
|
-
});
|
|
336
|
-
return splitOutputLines(output);
|
|
337
|
-
} catch (_error) {
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function unquoteGitPath(value) {
|
|
343
|
-
if (typeof value !== 'string') {
|
|
344
|
-
return '';
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const trimmed = value.trim();
|
|
348
|
-
if (!trimmed.startsWith('"') || !trimmed.endsWith('"')) {
|
|
349
|
-
return trimmed;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
try {
|
|
353
|
-
return JSON.parse(trimmed);
|
|
354
|
-
} catch (_error) {
|
|
355
|
-
return trimmed.slice(1, -1);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function formatFileCount(count) {
|
|
360
|
-
return `${count} file${count === 1 ? '' : 's'}`;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function previewChangedPaths(paths) {
|
|
364
|
-
if (!Array.isArray(paths) || paths.length === 0) {
|
|
365
|
-
return '';
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (paths.length <= MAX_CHANGED_PATH_PREVIEW) {
|
|
369
|
-
return paths.join(', ');
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
const preview = paths.slice(0, MAX_CHANGED_PATH_PREVIEW).join(', ');
|
|
373
|
-
return `${preview}, +${paths.length - MAX_CHANGED_PATH_PREVIEW} more`;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function deriveRepoChangeStatus(statusPair) {
|
|
377
|
-
if (statusPair === '??') {
|
|
378
|
-
return {
|
|
379
|
-
statusCode: '??',
|
|
380
|
-
statusLabel: 'U',
|
|
381
|
-
statusText: 'Untracked',
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const code = [statusPair[1], statusPair[0]].find((value) => value && value !== ' ') || 'M';
|
|
386
|
-
const statusTextByCode = {
|
|
387
|
-
A: 'Added',
|
|
388
|
-
C: 'Copied',
|
|
389
|
-
D: 'Deleted',
|
|
390
|
-
M: 'Modified',
|
|
391
|
-
R: 'Renamed',
|
|
392
|
-
T: 'Type changed',
|
|
393
|
-
U: 'Conflicted',
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
return {
|
|
397
|
-
statusCode: code,
|
|
398
|
-
statusLabel: code,
|
|
399
|
-
statusText: statusTextByCode[code] || 'Changed',
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function parseRepoChangeLine(repoRoot, line) {
|
|
404
|
-
if (typeof line !== 'string' || line.length < 4) {
|
|
405
|
-
return null;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const statusPair = line.slice(0, 2);
|
|
409
|
-
if (statusPair === '!!') {
|
|
410
|
-
return null;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const rawPath = line.slice(3).trim();
|
|
414
|
-
if (!rawPath) {
|
|
415
|
-
return null;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
let relativePath = rawPath;
|
|
419
|
-
let originalPath = '';
|
|
420
|
-
if (rawPath.includes(' -> ')) {
|
|
421
|
-
const parts = rawPath.split(' -> ');
|
|
422
|
-
if (parts.length === 2) {
|
|
423
|
-
originalPath = unquoteGitPath(parts[0]);
|
|
424
|
-
relativePath = parts[1];
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
relativePath = unquoteGitPath(relativePath);
|
|
429
|
-
if (!relativePath) {
|
|
430
|
-
return null;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const normalizedRelativePath = relativePath.split(path.sep).join('/');
|
|
434
|
-
if (
|
|
435
|
-
normalizedRelativePath === LOCK_FILE_FILTER_PATH
|
|
436
|
-
|| normalizedRelativePath.startsWith(`${LOCK_FILE_FILTER_PATH}/`)
|
|
437
|
-
|| normalizedRelativePath === ACTIVE_SESSIONS_FILTER_PREFIX
|
|
438
|
-
|| normalizedRelativePath.startsWith(`${ACTIVE_SESSIONS_FILTER_PREFIX}/`)
|
|
439
|
-
|| MANAGED_WORKTREE_FILTER_PREFIXES.some((prefix) => (
|
|
440
|
-
normalizedRelativePath === prefix || normalizedRelativePath.startsWith(`${prefix}/`)
|
|
441
|
-
))
|
|
442
|
-
) {
|
|
443
|
-
return null;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
const status = deriveRepoChangeStatus(statusPair);
|
|
447
|
-
return {
|
|
448
|
-
...status,
|
|
449
|
-
originalPath,
|
|
450
|
-
relativePath,
|
|
451
|
-
absolutePath: path.join(path.resolve(repoRoot), relativePath),
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function collectWorktreeChangedPaths(worktreePath) {
|
|
456
|
-
const changedGroups = [
|
|
457
|
-
runGitLines(worktreePath, ['diff', '--name-only', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`, `:(exclude)${AGENT_WORKTREE_LOCK_FILE}`]),
|
|
458
|
-
runGitLines(worktreePath, ['diff', '--cached', '--name-only', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`, `:(exclude)${AGENT_WORKTREE_LOCK_FILE}`]),
|
|
459
|
-
runGitLines(worktreePath, ['ls-files', '--others', '--exclude-standard']),
|
|
460
|
-
];
|
|
461
|
-
|
|
462
|
-
if (changedGroups.some((group) => group === null)) {
|
|
463
|
-
return null;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
return [...new Set(changedGroups.flat())]
|
|
467
|
-
.filter((relativePath) => (
|
|
468
|
-
relativePath
|
|
469
|
-
&& relativePath !== LOCK_FILE_RELATIVE
|
|
470
|
-
&& relativePath !== AGENT_WORKTREE_LOCK_FILE
|
|
471
|
-
))
|
|
472
|
-
.sort((left, right) => left.localeCompare(right));
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
function resolveWorktreeGitDir(worktreePath) {
|
|
476
|
-
const gitPath = path.join(path.resolve(worktreePath), '.git');
|
|
477
|
-
try {
|
|
478
|
-
if (fs.statSync(gitPath).isDirectory()) {
|
|
479
|
-
return gitPath;
|
|
480
|
-
}
|
|
481
|
-
} catch (_error) {
|
|
482
|
-
return null;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
try {
|
|
486
|
-
const gitPointer = fs.readFileSync(gitPath, 'utf8');
|
|
487
|
-
const match = gitPointer.match(/^gitdir:\s*(.+)$/m);
|
|
488
|
-
if (match?.[1]) {
|
|
489
|
-
return path.resolve(worktreePath, match[1].trim());
|
|
490
|
-
}
|
|
491
|
-
} catch (_error) {
|
|
492
|
-
return null;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
return null;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function deriveBlockingGitLabel(worktreePath) {
|
|
499
|
-
const gitDir = resolveWorktreeGitDir(worktreePath);
|
|
500
|
-
if (!gitDir) {
|
|
501
|
-
return '';
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
for (const blockingState of BLOCKING_GIT_STATES) {
|
|
505
|
-
if (blockingState.markers.some((marker) => fs.existsSync(path.join(gitDir, marker)))) {
|
|
506
|
-
return blockingState.label;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
return '';
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
function collectWorktreeTrackedPaths(worktreePath) {
|
|
514
|
-
const trackedPaths = runGitLines(worktreePath, ['ls-files', '--cached', '--others', '--exclude-standard']);
|
|
515
|
-
if (!trackedPaths) {
|
|
516
|
-
return null;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
return [...new Set(trackedPaths)]
|
|
520
|
-
.filter(Boolean)
|
|
521
|
-
.sort((left, right) => left.localeCompare(right));
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
function shouldSkipWorktreeActivityPath(relativePath) {
|
|
525
|
-
const normalized = normalizeRelativePath(relativePath);
|
|
526
|
-
if (!normalized || normalized === LOCK_FILE_RELATIVE || normalized === AGENT_WORKTREE_LOCK_FILE) {
|
|
527
|
-
return true;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
return WORKTREE_ACTIVITY_SKIP_PREFIXES.some((prefix) => (
|
|
531
|
-
normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
|
|
532
|
-
));
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
function worktreeActivityPathPriority(relativePath, recentPathsSet) {
|
|
536
|
-
if (recentPathsSet.has(relativePath)) {
|
|
537
|
-
return 0;
|
|
538
|
-
}
|
|
539
|
-
if (!relativePath.includes('/')) {
|
|
540
|
-
return 1;
|
|
541
|
-
}
|
|
542
|
-
if (WORKTREE_ACTIVITY_PRIORITY_PREFIXES.some((prefix) => relativePath.startsWith(prefix))) {
|
|
543
|
-
return 2;
|
|
544
|
-
}
|
|
545
|
-
return 3;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
function collectWorktreeActivityCandidatePaths(worktreePath, trackedPaths) {
|
|
549
|
-
const recentPaths = runGitLines(worktreePath, ['log', '-1', '--name-only', '--pretty=format:', '--', '.']) || [];
|
|
550
|
-
const filteredRecentPaths = [...new Set(recentPaths.map(normalizeRelativePath).filter(Boolean))]
|
|
551
|
-
.filter((relativePath) => !shouldSkipWorktreeActivityPath(relativePath));
|
|
552
|
-
const recentPathSet = new Set(filteredRecentPaths);
|
|
553
|
-
const prioritizedTrackedPaths = trackedPaths
|
|
554
|
-
.map(normalizeRelativePath)
|
|
555
|
-
.filter(Boolean)
|
|
556
|
-
.filter((relativePath) => !shouldSkipWorktreeActivityPath(relativePath))
|
|
557
|
-
.sort((left, right) => {
|
|
558
|
-
const priorityDelta = worktreeActivityPathPriority(left, recentPathSet)
|
|
559
|
-
- worktreeActivityPathPriority(right, recentPathSet);
|
|
560
|
-
if (priorityDelta !== 0) {
|
|
561
|
-
return priorityDelta;
|
|
562
|
-
}
|
|
563
|
-
return left.localeCompare(right);
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
return [...new Set([...filteredRecentPaths, ...prioritizedTrackedPaths])]
|
|
567
|
-
.slice(0, MAX_WORKTREE_ACTIVITY_STAT_PATHS);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
function clearWorktreeActivityCache(worktreePath = '') {
|
|
571
|
-
const normalizedWorktreePath = toNonEmptyString(worktreePath);
|
|
572
|
-
if (!normalizedWorktreePath) {
|
|
573
|
-
worktreeActivityCache.clear();
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
worktreeActivityCache.delete(path.resolve(normalizedWorktreePath));
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
function deriveLatestWorktreeFileActivity(worktreePath, options = {}) {
|
|
580
|
-
const now = Number.isFinite(options.now) ? options.now : Date.now();
|
|
581
|
-
const useCache = options.useCache !== false;
|
|
582
|
-
const cacheKey = path.resolve(worktreePath);
|
|
583
|
-
if (useCache) {
|
|
584
|
-
const cached = worktreeActivityCache.get(cacheKey);
|
|
585
|
-
if (cached && (now - cached.checkedAtMs) < WORKTREE_ACTIVITY_CACHE_TTL_MS) {
|
|
586
|
-
return cached.latestMtimeMs;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
const trackedPaths = collectWorktreeTrackedPaths(worktreePath);
|
|
591
|
-
if (!trackedPaths) {
|
|
592
|
-
return null;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
const candidatePaths = collectWorktreeActivityCandidatePaths(worktreePath, trackedPaths);
|
|
596
|
-
let latestMtimeMs = null;
|
|
597
|
-
for (const relativePath of candidatePaths) {
|
|
598
|
-
const absolutePath = path.join(worktreePath, relativePath);
|
|
599
|
-
try {
|
|
600
|
-
const stats = fs.statSync(absolutePath);
|
|
601
|
-
if (!stats.isFile() || !Number.isFinite(stats.mtimeMs)) {
|
|
602
|
-
continue;
|
|
603
|
-
}
|
|
604
|
-
latestMtimeMs = latestMtimeMs === null
|
|
605
|
-
? stats.mtimeMs
|
|
606
|
-
: Math.max(latestMtimeMs, stats.mtimeMs);
|
|
607
|
-
} catch (_error) {
|
|
608
|
-
continue;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
if (useCache) {
|
|
613
|
-
worktreeActivityCache.set(cacheKey, {
|
|
614
|
-
checkedAtMs: now,
|
|
615
|
-
latestMtimeMs,
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
return latestMtimeMs;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
function deriveSessionActivity(session, options = {}) {
|
|
623
|
-
const now = Number.isFinite(options.now) ? options.now : Date.now();
|
|
624
|
-
const pid = toPositiveInteger(session?.pid);
|
|
625
|
-
const pidAlive = pid ? isPidAlive(pid) : null;
|
|
626
|
-
const heartbeatAt = normalizeIsoString(session?.lastHeartbeatAt);
|
|
627
|
-
const heartbeatMs = Date.parse(heartbeatAt);
|
|
628
|
-
if (heartbeatAt && Number.isFinite(heartbeatMs) && now - heartbeatMs > HEARTBEAT_STALE_MS) {
|
|
629
|
-
return {
|
|
630
|
-
activityKind: 'dead',
|
|
631
|
-
activityLabel: 'dead',
|
|
632
|
-
activityCountLabel: '',
|
|
633
|
-
activitySummary: `Heartbeat stale for ${formatElapsedFrom(heartbeatAt, now)}.`,
|
|
634
|
-
changeCount: 0,
|
|
635
|
-
changedPaths: [],
|
|
636
|
-
worktreeChangedPaths: [],
|
|
637
|
-
pidAlive,
|
|
638
|
-
lastFileActivityAt: '',
|
|
639
|
-
lastFileActivityLabel: '',
|
|
640
|
-
};
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
const blockingLabel = deriveBlockingGitLabel(session.worktreePath);
|
|
644
|
-
if (blockingLabel) {
|
|
645
|
-
return {
|
|
646
|
-
activityKind: 'blocked',
|
|
647
|
-
activityLabel: 'blocked',
|
|
648
|
-
activityCountLabel: '',
|
|
649
|
-
activitySummary: blockingLabel,
|
|
650
|
-
changeCount: 0,
|
|
651
|
-
changedPaths: [],
|
|
652
|
-
worktreeChangedPaths: [],
|
|
653
|
-
pidAlive,
|
|
654
|
-
lastFileActivityAt: '',
|
|
655
|
-
lastFileActivityLabel: '',
|
|
656
|
-
};
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
if (pid && !pidAlive) {
|
|
660
|
-
return {
|
|
661
|
-
activityKind: 'dead',
|
|
662
|
-
activityLabel: 'dead',
|
|
663
|
-
activityCountLabel: '',
|
|
664
|
-
activitySummary: 'Recorded PID is not alive.',
|
|
665
|
-
changeCount: 0,
|
|
666
|
-
changedPaths: [],
|
|
667
|
-
worktreeChangedPaths: [],
|
|
668
|
-
pidAlive,
|
|
669
|
-
lastFileActivityAt: '',
|
|
670
|
-
lastFileActivityLabel: '',
|
|
671
|
-
};
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
const worktreeChangedPaths = collectWorktreeChangedPaths(session.worktreePath);
|
|
675
|
-
if (!worktreeChangedPaths) {
|
|
676
|
-
return {
|
|
677
|
-
activityKind: 'idle',
|
|
678
|
-
activityLabel: 'idle',
|
|
679
|
-
activityCountLabel: '',
|
|
680
|
-
activitySummary: 'Worktree activity unavailable.',
|
|
681
|
-
changeCount: 0,
|
|
682
|
-
changedPaths: [],
|
|
683
|
-
worktreeChangedPaths: [],
|
|
684
|
-
pidAlive,
|
|
685
|
-
lastFileActivityAt: '',
|
|
686
|
-
lastFileActivityLabel: '',
|
|
687
|
-
};
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
if (worktreeChangedPaths.length > 0) {
|
|
691
|
-
const worktreeRelativePaths = [...new Set(worktreeChangedPaths
|
|
692
|
-
.map((relativePath) => normalizeRelativePath(relativePath))
|
|
693
|
-
.filter(Boolean))]
|
|
694
|
-
.sort((left, right) => left.localeCompare(right));
|
|
695
|
-
clearWorktreeActivityCache(session.worktreePath);
|
|
696
|
-
const changedPaths = [...new Set(worktreeChangedPaths
|
|
697
|
-
.map((relativePath) => normalizeRelativePath(
|
|
698
|
-
path.relative(session.repoRoot, path.resolve(session.worktreePath, relativePath)),
|
|
699
|
-
))
|
|
700
|
-
.filter(Boolean))]
|
|
701
|
-
.sort((left, right) => left.localeCompare(right));
|
|
702
|
-
|
|
703
|
-
const workingLatestFileActivityMs = deriveLatestWorktreeFileActivity(session.worktreePath, {
|
|
704
|
-
now,
|
|
705
|
-
useCache: options.useCache,
|
|
706
|
-
});
|
|
707
|
-
const workingLastFileActivityAt = Number.isFinite(workingLatestFileActivityMs)
|
|
708
|
-
? new Date(workingLatestFileActivityMs).toISOString()
|
|
709
|
-
: '';
|
|
710
|
-
const workingLastFileActivityLabel = workingLastFileActivityAt
|
|
711
|
-
? formatElapsedFrom(workingLastFileActivityAt, now)
|
|
712
|
-
: '';
|
|
713
|
-
const workingFileActivityAgeMs = Number.isFinite(workingLatestFileActivityMs)
|
|
714
|
-
? Math.max(0, now - workingLatestFileActivityMs)
|
|
715
|
-
: null;
|
|
716
|
-
const isFinishedUncommitted = workingFileActivityAgeMs !== null
|
|
717
|
-
&& workingFileActivityAgeMs > IDLE_ACTIVITY_WINDOW_MS;
|
|
718
|
-
|
|
719
|
-
return {
|
|
720
|
-
activityKind: isFinishedUncommitted ? 'finished' : 'working',
|
|
721
|
-
activityLabel: isFinishedUncommitted ? 'finished' : 'working',
|
|
722
|
-
activityCountLabel: formatFileCount(worktreeChangedPaths.length),
|
|
723
|
-
activitySummary: isFinishedUncommitted && workingLastFileActivityLabel
|
|
724
|
-
? `${previewChangedPaths(worktreeChangedPaths)} · idle ${workingLastFileActivityLabel}`
|
|
725
|
-
: previewChangedPaths(worktreeChangedPaths),
|
|
726
|
-
changeCount: worktreeChangedPaths.length,
|
|
727
|
-
changedPaths,
|
|
728
|
-
worktreeChangedPaths: worktreeRelativePaths,
|
|
729
|
-
pidAlive,
|
|
730
|
-
lastFileActivityAt: workingLastFileActivityAt,
|
|
731
|
-
lastFileActivityLabel: workingLastFileActivityLabel,
|
|
732
|
-
};
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
const latestFileActivityMs = deriveLatestWorktreeFileActivity(session.worktreePath, {
|
|
736
|
-
now,
|
|
737
|
-
useCache: options.useCache,
|
|
738
|
-
});
|
|
739
|
-
const lastFileActivityAt = Number.isFinite(latestFileActivityMs)
|
|
740
|
-
? new Date(latestFileActivityMs).toISOString()
|
|
741
|
-
: '';
|
|
742
|
-
const lastFileActivityLabel = lastFileActivityAt
|
|
743
|
-
? formatElapsedFrom(lastFileActivityAt, now)
|
|
744
|
-
: '';
|
|
745
|
-
const lastFileActivityAgeMs = Number.isFinite(latestFileActivityMs)
|
|
746
|
-
? Math.max(0, now - latestFileActivityMs)
|
|
747
|
-
: null;
|
|
748
|
-
|
|
749
|
-
if (lastFileActivityAgeMs !== null && lastFileActivityAgeMs > STALLED_ACTIVITY_WINDOW_MS) {
|
|
750
|
-
return {
|
|
751
|
-
activityKind: 'stalled',
|
|
752
|
-
activityLabel: 'stalled',
|
|
753
|
-
activityCountLabel: '',
|
|
754
|
-
activitySummary: `Worktree clean. No file activity for ${lastFileActivityLabel}.`,
|
|
755
|
-
changeCount: 0,
|
|
756
|
-
changedPaths: [],
|
|
757
|
-
worktreeChangedPaths: [],
|
|
758
|
-
pidAlive,
|
|
759
|
-
lastFileActivityAt,
|
|
760
|
-
lastFileActivityLabel,
|
|
761
|
-
};
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
return {
|
|
765
|
-
activityKind: 'idle',
|
|
766
|
-
activityLabel: 'idle',
|
|
767
|
-
activityCountLabel: '',
|
|
768
|
-
activitySummary: lastFileActivityAgeMs !== null && lastFileActivityAgeMs <= IDLE_ACTIVITY_WINDOW_MS
|
|
769
|
-
? `Worktree clean. Recent file activity ${lastFileActivityLabel} ago.`
|
|
770
|
-
: lastFileActivityLabel
|
|
771
|
-
? `Worktree clean. Last file activity ${lastFileActivityLabel} ago.`
|
|
772
|
-
: 'Worktree clean.',
|
|
773
|
-
changeCount: 0,
|
|
774
|
-
changedPaths: [],
|
|
775
|
-
worktreeChangedPaths: [],
|
|
776
|
-
pidAlive,
|
|
777
|
-
lastFileActivityAt,
|
|
778
|
-
lastFileActivityLabel,
|
|
779
|
-
};
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
function buildSessionRecord(input) {
|
|
783
|
-
const repoRoot = path.resolve(toNonEmptyString(input.repoRoot));
|
|
784
|
-
const worktreePath = path.resolve(toNonEmptyString(input.worktreePath));
|
|
785
|
-
const branch = toNonEmptyString(input.branch);
|
|
786
|
-
const pid = toPositiveInteger(input.pid);
|
|
787
|
-
const startedAt = input.startedAt ? new Date(input.startedAt) : new Date();
|
|
788
|
-
const lastHeartbeatAt = input.lastHeartbeatAt ? new Date(input.lastHeartbeatAt) : new Date();
|
|
789
|
-
|
|
790
|
-
if (!branch) {
|
|
791
|
-
throw new Error('branch is required');
|
|
792
|
-
}
|
|
793
|
-
if (!repoRoot) {
|
|
794
|
-
throw new Error('repoRoot is required');
|
|
795
|
-
}
|
|
796
|
-
if (!worktreePath) {
|
|
797
|
-
throw new Error('worktreePath is required');
|
|
798
|
-
}
|
|
799
|
-
if (!pid) {
|
|
800
|
-
throw new Error('pid must be a positive integer');
|
|
801
|
-
}
|
|
802
|
-
if (Number.isNaN(startedAt.getTime())) {
|
|
803
|
-
throw new Error('startedAt must be a valid date');
|
|
804
|
-
}
|
|
805
|
-
if (Number.isNaN(lastHeartbeatAt.getTime())) {
|
|
806
|
-
throw new Error('lastHeartbeatAt must be a valid date');
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
return {
|
|
810
|
-
schemaVersion: SESSION_SCHEMA_VERSION,
|
|
811
|
-
repoRoot,
|
|
812
|
-
branch,
|
|
813
|
-
taskName: toNonEmptyString(input.taskName, 'task'),
|
|
814
|
-
latestTaskPreview: toNonEmptyString(input.latestTaskPreview),
|
|
815
|
-
agentName: toNonEmptyString(input.agentName, 'agent'),
|
|
816
|
-
projectName: toNonEmptyString(input.projectName),
|
|
817
|
-
projectPath: normalizeProjectPath(input.projectPath),
|
|
818
|
-
snapshotName: toNonEmptyString(input.snapshotName),
|
|
819
|
-
snapshotEmail: toNonEmptyString(input.snapshotEmail || input.email),
|
|
820
|
-
worktreePath,
|
|
821
|
-
pid,
|
|
822
|
-
cliName: toNonEmptyString(input.cliName, 'codex'),
|
|
823
|
-
taskMode: normalizeTaskMode(input.taskMode),
|
|
824
|
-
openspecTier: normalizeOpenSpecTier(input.openspecTier),
|
|
825
|
-
taskRoutingReason: toNonEmptyString(input.taskRoutingReason),
|
|
826
|
-
startedAt: startedAt.toISOString(),
|
|
827
|
-
lastHeartbeatAt: lastHeartbeatAt.toISOString(),
|
|
828
|
-
state: normalizeAdvisoryState(input.state),
|
|
829
|
-
sessionHealth: normalizeSessionHealthPayload(input.sessionHealth || input.sessionSeverity),
|
|
830
|
-
};
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
function deriveSessionLabel(branch, worktreePath) {
|
|
834
|
-
const worktreeLeaf = toNonEmptyString(path.basename(worktreePath || ''));
|
|
835
|
-
if (worktreeLeaf) {
|
|
836
|
-
return worktreeLeaf;
|
|
837
|
-
}
|
|
838
|
-
return toNonEmptyString(branch).replace(/[\\/]+/g, '-') || 'unknown-agent';
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
function normalizeSessionRecord(input, options = {}) {
|
|
842
|
-
if (!input || typeof input !== 'object') {
|
|
843
|
-
return null;
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
const repoRoot = toNonEmptyString(input.repoRoot);
|
|
847
|
-
const branch = toNonEmptyString(input.branch);
|
|
848
|
-
const worktreePath = toNonEmptyString(input.worktreePath);
|
|
849
|
-
const startedAt = new Date(input.startedAt);
|
|
850
|
-
const lastHeartbeatAt = new Date(input.lastHeartbeatAt || input.startedAt);
|
|
851
|
-
const pid = toPositiveInteger(input.pid);
|
|
852
|
-
|
|
853
|
-
if (
|
|
854
|
-
!repoRoot
|
|
855
|
-
|| !branch
|
|
856
|
-
|| !worktreePath
|
|
857
|
-
|| !pid
|
|
858
|
-
|| Number.isNaN(startedAt.getTime())
|
|
859
|
-
|| Number.isNaN(lastHeartbeatAt.getTime())
|
|
860
|
-
) {
|
|
861
|
-
return null;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
return {
|
|
865
|
-
schemaVersion: toPositiveInteger(input.schemaVersion) || SESSION_SCHEMA_VERSION,
|
|
866
|
-
repoRoot: path.resolve(repoRoot),
|
|
867
|
-
branch,
|
|
868
|
-
taskName: toNonEmptyString(input.taskName, 'task'),
|
|
869
|
-
latestTaskPreview: toNonEmptyString(input.latestTaskPreview),
|
|
870
|
-
agentName: toNonEmptyString(input.agentName, 'agent'),
|
|
871
|
-
projectName: toNonEmptyString(input.projectName),
|
|
872
|
-
projectPath: normalizeProjectPath(input.projectPath),
|
|
873
|
-
snapshotName: toNonEmptyString(input.snapshotName),
|
|
874
|
-
snapshotEmail: toNonEmptyString(input.snapshotEmail || input.email),
|
|
875
|
-
worktreePath: path.resolve(worktreePath),
|
|
876
|
-
pid,
|
|
877
|
-
cliName: toNonEmptyString(input.cliName, 'codex'),
|
|
878
|
-
taskMode: normalizeTaskMode(input.taskMode),
|
|
879
|
-
openspecTier: normalizeOpenSpecTier(input.openspecTier),
|
|
880
|
-
taskRoutingReason: toNonEmptyString(input.taskRoutingReason),
|
|
881
|
-
startedAt: startedAt.toISOString(),
|
|
882
|
-
lastHeartbeatAt: lastHeartbeatAt.toISOString(),
|
|
883
|
-
state: normalizeAdvisoryState(input.state, 'idle'),
|
|
884
|
-
filePath: toNonEmptyString(options.filePath),
|
|
885
|
-
label: deriveSessionLabel(branch, worktreePath),
|
|
886
|
-
changedPaths: [],
|
|
887
|
-
worktreeChangedPaths: [],
|
|
888
|
-
sourceKind: 'active-session',
|
|
889
|
-
telemetryUpdatedAt: '',
|
|
890
|
-
telemetrySource: '',
|
|
891
|
-
lockSnapshotCount: 0,
|
|
892
|
-
lockSessionCount: 0,
|
|
893
|
-
collaboration: false,
|
|
894
|
-
sessionHealth: normalizeSessionHealthPayload(input.sessionHealth || input.sessionSeverity),
|
|
895
|
-
};
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
function formatElapsedFrom(startedAt, now = Date.now()) {
|
|
899
|
-
const startedAtMs = startedAt instanceof Date ? startedAt.getTime() : Date.parse(startedAt);
|
|
900
|
-
if (!Number.isFinite(startedAtMs)) {
|
|
901
|
-
return '0s';
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
const totalSeconds = Math.max(0, Math.floor((now - startedAtMs) / 1000));
|
|
905
|
-
const days = Math.floor(totalSeconds / 86_400);
|
|
906
|
-
const hours = Math.floor((totalSeconds % 86_400) / 3_600);
|
|
907
|
-
const minutes = Math.floor((totalSeconds % 3_600) / 60);
|
|
908
|
-
const seconds = totalSeconds % 60;
|
|
909
|
-
|
|
910
|
-
if (days > 0) {
|
|
911
|
-
return `${days}d ${hours}h`;
|
|
912
|
-
}
|
|
913
|
-
if (hours > 0) {
|
|
914
|
-
return `${hours}h ${minutes}m`;
|
|
915
|
-
}
|
|
916
|
-
if (minutes > 0) {
|
|
917
|
-
return `${minutes}m ${seconds}s`;
|
|
918
|
-
}
|
|
919
|
-
return `${seconds}s`;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
function isPidAlive(pid) {
|
|
923
|
-
const normalizedPid = toPositiveInteger(pid);
|
|
924
|
-
if (!normalizedPid) {
|
|
925
|
-
return false;
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
try {
|
|
929
|
-
process.kill(normalizedPid, 0);
|
|
930
|
-
return true;
|
|
931
|
-
} catch (_error) {
|
|
932
|
-
return false;
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
function readWorktreeBranch(worktreePath) {
|
|
937
|
-
const lines = runGitLines(worktreePath, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
938
|
-
return Array.isArray(lines) && typeof lines[0] === 'string' ? lines[0].trim() : '';
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
function deriveAgentNameFromBranch(branch) {
|
|
942
|
-
const parts = toNonEmptyString(branch).split('/').filter(Boolean);
|
|
943
|
-
if (parts.length >= 2 && parts[0] === 'agent') {
|
|
944
|
-
return parts[1];
|
|
945
|
-
}
|
|
946
|
-
return 'agent';
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
function isManagedAgentBranch(branch) {
|
|
950
|
-
return toNonEmptyString(branch).startsWith('agent/');
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
function deriveManagedWorktreeStartedAt(worktreePath, now = Date.now()) {
|
|
954
|
-
try {
|
|
955
|
-
const stats = fs.statSync(worktreePath);
|
|
956
|
-
if (Number.isFinite(stats.mtimeMs)) {
|
|
957
|
-
return new Date(stats.mtimeMs).toISOString();
|
|
958
|
-
}
|
|
959
|
-
} catch (_error) {
|
|
960
|
-
// Directory mtime is best-effort context only; fall back to current scan time.
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
return new Date(now).toISOString();
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
function flattenTelemetrySnapshotSessions(lockPayload) {
|
|
967
|
-
const flattened = [];
|
|
968
|
-
const snapshots = Array.isArray(lockPayload?.snapshots) ? lockPayload.snapshots : [];
|
|
969
|
-
for (const snapshot of snapshots) {
|
|
970
|
-
const snapshotSessions = Array.isArray(snapshot?.sessions) ? snapshot.sessions : [];
|
|
971
|
-
for (const session of snapshotSessions) {
|
|
972
|
-
flattened.push({
|
|
973
|
-
taskPreview: toNonEmptyString(session?.taskPreview),
|
|
974
|
-
taskUpdatedAt: normalizeIsoString(session?.taskUpdatedAt),
|
|
975
|
-
projectName: toNonEmptyString(session?.projectName),
|
|
976
|
-
projectPath: toNonEmptyString(session?.projectPath),
|
|
977
|
-
snapshotName: toNonEmptyString(snapshot?.snapshotName),
|
|
978
|
-
email: toNonEmptyString(snapshot?.email),
|
|
979
|
-
sessionHealth: normalizeSessionHealthPayload(
|
|
980
|
-
session?.sessionHealth || session?.sessionSeverity || snapshot?.sessionHealth || snapshot?.sessionSeverity,
|
|
981
|
-
),
|
|
982
|
-
});
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
return flattened;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
function sortSessionsByTimestamp(sessions) {
|
|
989
|
-
sessions.sort((left, right) => {
|
|
990
|
-
const timeDelta = Date.parse(right.startedAt) - Date.parse(left.startedAt);
|
|
991
|
-
if (timeDelta !== 0) {
|
|
992
|
-
return timeDelta;
|
|
993
|
-
}
|
|
994
|
-
return left.label.localeCompare(right.label);
|
|
995
|
-
});
|
|
996
|
-
return sessions;
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
function deriveLockTaskAnchor(entries, fallbackTaskName, fallbackTimestamp) {
|
|
1000
|
-
const sortedEntries = sortTelemetryEntriesForAnchor(entries);
|
|
1001
|
-
|
|
1002
|
-
const latestEntry = sortedEntries[0] || null;
|
|
1003
|
-
return {
|
|
1004
|
-
taskName: latestEntry?.taskPreview || fallbackTaskName || 'task',
|
|
1005
|
-
latestTaskPreview: latestEntry?.taskPreview || '',
|
|
1006
|
-
timestamp: latestEntry?.taskUpdatedAt || fallbackTimestamp || '',
|
|
1007
|
-
sessionHealth: latestEntry?.sessionHealth || null,
|
|
1008
|
-
};
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
function sortTelemetryEntriesForAnchor(entries) {
|
|
1012
|
-
return [...entries].sort((left, right) => {
|
|
1013
|
-
const timeDelta = Date.parse(right.taskUpdatedAt || '') - Date.parse(left.taskUpdatedAt || '');
|
|
1014
|
-
if (timeDelta !== 0) {
|
|
1015
|
-
return timeDelta;
|
|
1016
|
-
}
|
|
1017
|
-
if (Boolean(right.taskPreview) !== Boolean(left.taskPreview)) {
|
|
1018
|
-
return Number(Boolean(right.taskPreview)) - Number(Boolean(left.taskPreview));
|
|
1019
|
-
}
|
|
1020
|
-
return (right.projectPath || '').localeCompare(left.projectPath || '');
|
|
1021
|
-
});
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
function deriveLockSnapshotIdentity(entries) {
|
|
1025
|
-
const latestEntry = sortTelemetryEntriesForAnchor(entries)
|
|
1026
|
-
.find((entry) => entry?.snapshotName || entry?.email) || null;
|
|
1027
|
-
return {
|
|
1028
|
-
snapshotName: toNonEmptyString(latestEntry?.snapshotName),
|
|
1029
|
-
snapshotEmail: toNonEmptyString(latestEntry?.email),
|
|
1030
|
-
};
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
function deriveLockProjectMetadata(entries) {
|
|
1034
|
-
const latestEntry = sortTelemetryEntriesForAnchor(entries)
|
|
1035
|
-
.find((entry) => entry?.projectPath || entry?.projectName) || null;
|
|
1036
|
-
return {
|
|
1037
|
-
projectName: toNonEmptyString(latestEntry?.projectName),
|
|
1038
|
-
projectPath: normalizeProjectPath(latestEntry?.projectPath),
|
|
1039
|
-
};
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
function buildWorktreeLockSession(repoRoot, worktreePath, lockPayload, options = {}) {
|
|
1043
|
-
const now = options.now || Date.now();
|
|
1044
|
-
const telemetryEntries = flattenTelemetrySnapshotSessions(lockPayload);
|
|
1045
|
-
const telemetryUpdatedAt = normalizeIsoString(lockPayload?.updatedAt);
|
|
1046
|
-
const branch = readWorktreeBranch(worktreePath);
|
|
1047
|
-
const effectiveBranch = branch && branch !== 'HEAD'
|
|
1048
|
-
? branch
|
|
1049
|
-
: `agent/telemetry/${path.basename(worktreePath)}`;
|
|
1050
|
-
const label = deriveSessionLabel(effectiveBranch, worktreePath);
|
|
1051
|
-
const taskAnchor = deriveLockTaskAnchor(telemetryEntries, label, telemetryUpdatedAt);
|
|
1052
|
-
const snapshotIdentity = deriveLockSnapshotIdentity(telemetryEntries);
|
|
1053
|
-
const projectMetadata = deriveLockProjectMetadata(telemetryEntries);
|
|
1054
|
-
const startedAt = taskAnchor.timestamp || telemetryUpdatedAt || new Date(now).toISOString();
|
|
1055
|
-
|
|
1056
|
-
const session = {
|
|
1057
|
-
schemaVersion: toPositiveInteger(lockPayload?.schemaVersion) || SESSION_SCHEMA_VERSION,
|
|
1058
|
-
repoRoot: path.resolve(repoRoot),
|
|
1059
|
-
branch: effectiveBranch,
|
|
1060
|
-
taskName: taskAnchor.taskName,
|
|
1061
|
-
latestTaskPreview: taskAnchor.latestTaskPreview,
|
|
1062
|
-
agentName: deriveAgentNameFromBranch(effectiveBranch),
|
|
1063
|
-
projectName: projectMetadata.projectName,
|
|
1064
|
-
projectPath: projectMetadata.projectPath,
|
|
1065
|
-
snapshotName: snapshotIdentity.snapshotName,
|
|
1066
|
-
snapshotEmail: snapshotIdentity.snapshotEmail,
|
|
1067
|
-
worktreePath: path.resolve(worktreePath),
|
|
1068
|
-
pid: null,
|
|
1069
|
-
cliName: 'codex',
|
|
1070
|
-
taskMode: '',
|
|
1071
|
-
openspecTier: '',
|
|
1072
|
-
taskRoutingReason: '',
|
|
1073
|
-
startedAt,
|
|
1074
|
-
lastHeartbeatAt: '',
|
|
1075
|
-
state: '',
|
|
1076
|
-
filePath: path.join(worktreePath, AGENT_WORKTREE_LOCK_FILE),
|
|
1077
|
-
label,
|
|
1078
|
-
changedPaths: [],
|
|
1079
|
-
worktreeChangedPaths: [],
|
|
1080
|
-
sourceKind: 'worktree-lock',
|
|
1081
|
-
telemetryUpdatedAt: telemetryUpdatedAt || startedAt,
|
|
1082
|
-
telemetrySource: toNonEmptyString(lockPayload?.source, 'worktree-lock'),
|
|
1083
|
-
lockSnapshotCount: toPositiveInteger(lockPayload?.snapshotCount) || 0,
|
|
1084
|
-
lockSessionCount: toPositiveInteger(lockPayload?.sessionCount) || telemetryEntries.length,
|
|
1085
|
-
collaboration: Boolean(lockPayload?.collaboration),
|
|
1086
|
-
sessionHealth: taskAnchor.sessionHealth || normalizeSessionHealthPayload(
|
|
1087
|
-
lockPayload?.sessionHealth || lockPayload?.sessionSeverity,
|
|
1088
|
-
),
|
|
1089
|
-
};
|
|
1090
|
-
|
|
1091
|
-
session.elapsedLabel = formatElapsedFrom(session.startedAt, now);
|
|
1092
|
-
Object.assign(session, deriveSessionActivity(session, { now }));
|
|
1093
|
-
return session;
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
function buildManagedWorktreeSession(repoRoot, worktreePath, options = {}) {
|
|
1097
|
-
const now = options.now || Date.now();
|
|
1098
|
-
const branch = readWorktreeBranch(worktreePath);
|
|
1099
|
-
if (!branch || branch === 'HEAD' || !isManagedAgentBranch(branch)) {
|
|
1100
|
-
return null;
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
const label = deriveSessionLabel(branch, worktreePath);
|
|
1104
|
-
const startedAt = deriveManagedWorktreeStartedAt(worktreePath, now);
|
|
1105
|
-
const session = {
|
|
1106
|
-
schemaVersion: SESSION_SCHEMA_VERSION,
|
|
1107
|
-
repoRoot: path.resolve(repoRoot),
|
|
1108
|
-
branch,
|
|
1109
|
-
taskName: label,
|
|
1110
|
-
latestTaskPreview: '',
|
|
1111
|
-
agentName: deriveAgentNameFromBranch(branch),
|
|
1112
|
-
projectName: '',
|
|
1113
|
-
projectPath: '',
|
|
1114
|
-
snapshotName: '',
|
|
1115
|
-
snapshotEmail: '',
|
|
1116
|
-
worktreePath: path.resolve(worktreePath),
|
|
1117
|
-
pid: null,
|
|
1118
|
-
cliName: 'gx',
|
|
1119
|
-
taskMode: '',
|
|
1120
|
-
openspecTier: '',
|
|
1121
|
-
taskRoutingReason: '',
|
|
1122
|
-
startedAt,
|
|
1123
|
-
lastHeartbeatAt: '',
|
|
1124
|
-
state: '',
|
|
1125
|
-
filePath: path.join(worktreePath, '.git'),
|
|
1126
|
-
label,
|
|
1127
|
-
changedPaths: [],
|
|
1128
|
-
worktreeChangedPaths: [],
|
|
1129
|
-
sourceKind: 'managed-worktree',
|
|
1130
|
-
telemetryUpdatedAt: '',
|
|
1131
|
-
telemetrySource: 'managed-worktree',
|
|
1132
|
-
lockSnapshotCount: 0,
|
|
1133
|
-
lockSessionCount: 0,
|
|
1134
|
-
collaboration: false,
|
|
1135
|
-
sessionHealth: null,
|
|
1136
|
-
};
|
|
1137
|
-
|
|
1138
|
-
session.elapsedLabel = formatElapsedFrom(session.startedAt, now);
|
|
1139
|
-
Object.assign(session, deriveSessionActivity(session, { now }));
|
|
1140
|
-
return session;
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
function readWorktreeLockSessions(repoRoot, options = {}) {
|
|
1144
|
-
const sessions = [];
|
|
1145
|
-
for (const managedRoot of resolveManagedWorktreeRoots(repoRoot)) {
|
|
1146
|
-
if (!fs.existsSync(managedRoot)) {
|
|
1147
|
-
continue;
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
let entries;
|
|
1151
|
-
try {
|
|
1152
|
-
entries = fs.readdirSync(managedRoot, { withFileTypes: true });
|
|
1153
|
-
} catch (_error) {
|
|
1154
|
-
continue;
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
for (const entry of entries) {
|
|
1158
|
-
if (!entry.isDirectory()) {
|
|
1159
|
-
continue;
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
const worktreePath = path.join(managedRoot, entry.name);
|
|
1163
|
-
const lockPath = path.join(worktreePath, AGENT_WORKTREE_LOCK_FILE);
|
|
1164
|
-
if (!fs.existsSync(lockPath)) {
|
|
1165
|
-
continue;
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
const lockPayload = readJsonFile(lockPath);
|
|
1169
|
-
if (!lockPayload || typeof lockPayload !== 'object' || Array.isArray(lockPayload)) {
|
|
1170
|
-
continue;
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
const telemetryEntries = flattenTelemetrySnapshotSessions(lockPayload);
|
|
1174
|
-
if (telemetryEntries.length === 0 && !toPositiveInteger(lockPayload.sessionCount)) {
|
|
1175
|
-
continue;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
sessions.push(buildWorktreeLockSession(repoRoot, worktreePath, lockPayload, options));
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
return sortSessionsByTimestamp(sessions);
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
function readManagedWorktreeSessions(repoRoot, options = {}) {
|
|
1186
|
-
const lockSessions = readWorktreeLockSessions(repoRoot, options);
|
|
1187
|
-
const lockSessionsByWorktree = new Map(
|
|
1188
|
-
lockSessions.map((session) => [path.resolve(session.worktreePath), session]),
|
|
1189
|
-
);
|
|
1190
|
-
const sessions = [];
|
|
1191
|
-
|
|
1192
|
-
for (const managedRoot of resolveManagedWorktreeRoots(repoRoot)) {
|
|
1193
|
-
if (!fs.existsSync(managedRoot)) {
|
|
1194
|
-
continue;
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
let entries;
|
|
1198
|
-
try {
|
|
1199
|
-
entries = fs.readdirSync(managedRoot, { withFileTypes: true });
|
|
1200
|
-
} catch (_error) {
|
|
1201
|
-
continue;
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
for (const entry of entries) {
|
|
1205
|
-
if (!entry.isDirectory()) {
|
|
1206
|
-
continue;
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
const worktreePath = path.join(managedRoot, entry.name);
|
|
1210
|
-
const worktreeKey = path.resolve(worktreePath);
|
|
1211
|
-
const lockSession = lockSessionsByWorktree.get(worktreeKey);
|
|
1212
|
-
if (lockSession) {
|
|
1213
|
-
sessions.push(lockSession);
|
|
1214
|
-
continue;
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
const managedSession = buildManagedWorktreeSession(repoRoot, worktreePath, options);
|
|
1218
|
-
if (managedSession) {
|
|
1219
|
-
sessions.push(managedSession);
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
return sortSessionsByTimestamp(sessions);
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
function mergeSessionSources(primarySessions, lockSessions) {
|
|
1228
|
-
const lockSessionsByWorktree = new Map(
|
|
1229
|
-
lockSessions.map((session) => [path.resolve(session.worktreePath), session]),
|
|
1230
|
-
);
|
|
1231
|
-
const consumedLockWorktrees = new Set();
|
|
1232
|
-
const merged = [];
|
|
1233
|
-
|
|
1234
|
-
for (const session of primarySessions) {
|
|
1235
|
-
const worktreeKey = path.resolve(session.worktreePath);
|
|
1236
|
-
const lockSession = lockSessionsByWorktree.get(worktreeKey);
|
|
1237
|
-
if (lockSession && session.activityKind === 'dead') {
|
|
1238
|
-
continue;
|
|
1239
|
-
}
|
|
1240
|
-
if (lockSession) {
|
|
1241
|
-
consumedLockWorktrees.add(worktreeKey);
|
|
1242
|
-
merged.push({
|
|
1243
|
-
...session,
|
|
1244
|
-
latestTaskPreview: session.latestTaskPreview || lockSession.latestTaskPreview,
|
|
1245
|
-
projectName: session.projectName || lockSession.projectName,
|
|
1246
|
-
projectPath: session.projectPath || lockSession.projectPath,
|
|
1247
|
-
snapshotName: session.snapshotName || lockSession.snapshotName,
|
|
1248
|
-
snapshotEmail: session.snapshotEmail || lockSession.snapshotEmail,
|
|
1249
|
-
telemetryUpdatedAt: session.telemetryUpdatedAt || lockSession.telemetryUpdatedAt,
|
|
1250
|
-
telemetrySource: session.telemetrySource || lockSession.telemetrySource,
|
|
1251
|
-
lockSnapshotCount: session.lockSnapshotCount || lockSession.lockSnapshotCount,
|
|
1252
|
-
lockSessionCount: session.lockSessionCount || lockSession.lockSessionCount,
|
|
1253
|
-
collaboration: session.collaboration || lockSession.collaboration,
|
|
1254
|
-
sessionHealth: session.sessionHealth || lockSession.sessionHealth,
|
|
1255
|
-
});
|
|
1256
|
-
continue;
|
|
1257
|
-
}
|
|
1258
|
-
merged.push(session);
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
for (const lockSession of lockSessions) {
|
|
1262
|
-
const worktreeKey = path.resolve(lockSession.worktreePath);
|
|
1263
|
-
if (!consumedLockWorktrees.has(worktreeKey)) {
|
|
1264
|
-
merged.push(lockSession);
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
return sortSessionsByTimestamp(merged);
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
function readActiveSessions(repoRoot, options = {}) {
|
|
1272
|
-
const activeSessionsDir = activeSessionsDirForRepo(repoRoot);
|
|
1273
|
-
const now = options.now || Date.now();
|
|
1274
|
-
const sessionFileSessions = [];
|
|
1275
|
-
if (fs.existsSync(activeSessionsDir)) {
|
|
1276
|
-
for (const entry of fs.readdirSync(activeSessionsDir, { withFileTypes: true })) {
|
|
1277
|
-
if (!entry.isFile() || !entry.name.endsWith('.json')) {
|
|
1278
|
-
continue;
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
const filePath = path.join(activeSessionsDir, entry.name);
|
|
1282
|
-
const parsed = readJsonFile(filePath);
|
|
1283
|
-
const normalized = normalizeSessionRecord(parsed, { filePath });
|
|
1284
|
-
if (!normalized) {
|
|
1285
|
-
continue;
|
|
1286
|
-
}
|
|
1287
|
-
if (!options.includeStale && !isPidAlive(normalized.pid)) {
|
|
1288
|
-
continue;
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
normalized.elapsedLabel = formatElapsedFrom(normalized.startedAt, now);
|
|
1292
|
-
Object.assign(normalized, deriveSessionActivity(normalized, { now }));
|
|
1293
|
-
sessionFileSessions.push(normalized);
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
return mergeSessionSources(
|
|
1298
|
-
sortSessionsByTimestamp(sessionFileSessions),
|
|
1299
|
-
readManagedWorktreeSessions(repoRoot, { now }),
|
|
1300
|
-
);
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
function readRepoChanges(repoRoot) {
|
|
1304
|
-
const statusLines = runGitLines(repoRoot, ['status', '--porcelain=v1', '--untracked-files=all']);
|
|
1305
|
-
if (!statusLines) {
|
|
1306
|
-
return [];
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
return statusLines
|
|
1310
|
-
.map((line) => parseRepoChangeLine(repoRoot, line))
|
|
1311
|
-
.filter(Boolean)
|
|
1312
|
-
.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
module.exports = {
|
|
1316
|
-
ACTIVE_SESSIONS_RELATIVE_DIR,
|
|
1317
|
-
SESSION_SCHEMA_VERSION,
|
|
1318
|
-
activeSessionsDirForRepo,
|
|
1319
|
-
buildSessionRecord,
|
|
1320
|
-
clearWorktreeActivityCache,
|
|
1321
|
-
collectWorktreeChangedPaths,
|
|
1322
|
-
collectWorktreeTrackedPaths,
|
|
1323
|
-
deriveBlockingGitLabel,
|
|
1324
|
-
deriveLatestWorktreeFileActivity,
|
|
1325
|
-
deriveSessionLabel,
|
|
1326
|
-
deriveSessionActivity,
|
|
1327
|
-
formatElapsedFrom,
|
|
1328
|
-
formatFileCount,
|
|
1329
|
-
isPidAlive,
|
|
1330
|
-
normalizeSessionRecord,
|
|
1331
|
-
parseRepoChangeLine,
|
|
1332
|
-
previewChangedPaths,
|
|
1333
|
-
readActiveSessions,
|
|
1334
|
-
readManagedWorktreeSessions,
|
|
1335
|
-
readWorktreeLockSessions,
|
|
1336
|
-
readRepoChanges,
|
|
1337
|
-
deriveRepoChangeStatus,
|
|
1338
|
-
readAheadBehindCounts,
|
|
1339
|
-
readConfiguredBaseBranch,
|
|
1340
|
-
readLogTail,
|
|
1341
|
-
resolveWorktreeGitDir,
|
|
1342
|
-
readSessionHeldLocks,
|
|
1343
|
-
readSessionInspectData,
|
|
1344
|
-
sessionLogPath,
|
|
1345
|
-
sanitizeBranchForFile,
|
|
1346
|
-
sessionFileNameForBranch,
|
|
1347
|
-
sessionFilePathForBranch,
|
|
1348
|
-
};
|