@imdeadpool/guardex 7.0.20 → 7.0.22
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 +66 -30
- package/package.json +1 -1
- package/src/cli/args.js +804 -2
- package/src/cli/main.js +744 -5101
- package/src/context.js +197 -33
- package/src/doctor/index.js +1071 -0
- package/src/finish/index.js +456 -358
- package/src/git/index.js +645 -32
- package/src/output/index.js +8 -1
- package/src/sandbox/index.js +301 -52
- package/src/scaffold/index.js +681 -22
- package/src/toolchain/index.js +622 -178
- package/templates/scripts/agent-branch-finish.sh +56 -5
- package/templates/scripts/agent-worktree-prune.sh +15 -1
- package/templates/scripts/codex-agent.sh +14 -2
- package/templates/vscode/guardex-active-agents/README.md +3 -1
- package/templates/vscode/guardex-active-agents/extension.js +321 -12
- package/templates/vscode/guardex-active-agents/icon.png +0 -0
- package/templates/vscode/guardex-active-agents/package.json +5 -1
- package/templates/vscode/guardex-active-agents/session-schema.js +233 -29
|
@@ -218,18 +218,48 @@ fi
|
|
|
218
218
|
|
|
219
219
|
get_worktree_for_branch() {
|
|
220
220
|
local branch="$1"
|
|
221
|
-
git -C "$repo_root" worktree list --porcelain | awk -v target="refs/heads/${branch}" '
|
|
221
|
+
git -C "$repo_root" worktree list --porcelain | awk -v target="refs/heads/${branch}" -v probe_prefix="${agent_worktree_root}/__source-probe-" '
|
|
222
222
|
$1 == "worktree" { wt = $2 }
|
|
223
|
-
$1 == "branch" && $2 == target {
|
|
223
|
+
$1 == "branch" && $2 == target {
|
|
224
|
+
if (index(wt, probe_prefix) != 1) {
|
|
225
|
+
print wt
|
|
226
|
+
exit
|
|
227
|
+
}
|
|
228
|
+
}
|
|
224
229
|
'
|
|
225
230
|
}
|
|
226
231
|
|
|
232
|
+
remove_stale_source_probe_worktrees() {
|
|
233
|
+
local branch="$1"
|
|
234
|
+
local stale_probe=""
|
|
235
|
+
|
|
236
|
+
while IFS= read -r stale_probe; do
|
|
237
|
+
[[ -z "$stale_probe" ]] && continue
|
|
238
|
+
[[ "$stale_probe" == "$current_worktree" ]] && continue
|
|
239
|
+
|
|
240
|
+
echo "[agent-branch-finish] Removing stale source-probe worktree for '${branch}': ${stale_probe}" >&2
|
|
241
|
+
git -C "$stale_probe" rebase --abort >/dev/null 2>&1 || true
|
|
242
|
+
git -C "$stale_probe" merge --abort >/dev/null 2>&1 || true
|
|
243
|
+
git -C "$repo_root" worktree remove "$stale_probe" --force >/dev/null 2>&1 || true
|
|
244
|
+
done < <(
|
|
245
|
+
git -C "$repo_root" worktree list --porcelain | awk -v target="refs/heads/${branch}" -v probe_prefix="${agent_worktree_root}/__source-probe-" '
|
|
246
|
+
$1 == "worktree" { wt = $2 }
|
|
247
|
+
$1 == "branch" && $2 == target {
|
|
248
|
+
if (index(wt, probe_prefix) == 1) {
|
|
249
|
+
print wt
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
'
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
227
256
|
is_clean_worktree() {
|
|
228
257
|
local wt="$1"
|
|
229
258
|
git -C "$wt" diff --quiet -- . ":(exclude).omx/state/agent-file-locks.json" \
|
|
230
259
|
&& git -C "$wt" diff --cached --quiet -- . ":(exclude).omx/state/agent-file-locks.json"
|
|
231
260
|
}
|
|
232
261
|
|
|
262
|
+
remove_stale_source_probe_worktrees "$SOURCE_BRANCH"
|
|
233
263
|
source_worktree="$(get_worktree_for_branch "$SOURCE_BRANCH")"
|
|
234
264
|
created_source_probe=0
|
|
235
265
|
source_probe_path=""
|
|
@@ -295,8 +325,13 @@ if [[ "$should_require_sync" -eq 1 ]] && git -C "$repo_root" show-ref --verify -
|
|
|
295
325
|
|
|
296
326
|
echo "[agent-sync-guard] Auto-sync failed while rebasing '${SOURCE_BRANCH}' onto origin/${BASE_BRANCH}." >&2
|
|
297
327
|
if [[ "$rebase_active" -eq 1 ]]; then
|
|
298
|
-
|
|
299
|
-
|
|
328
|
+
if [[ "$created_source_probe" -eq 1 ]]; then
|
|
329
|
+
echo "[agent-sync-guard] Temporary source-probe worktree will be cleaned up on exit." >&2
|
|
330
|
+
echo "[agent-sync-guard] Reattach '${SOURCE_BRANCH}' in a regular worktree, then rebase it onto origin/${BASE_BRANCH} manually." >&2
|
|
331
|
+
else
|
|
332
|
+
echo "[agent-sync-guard] Resolve conflicts, then run: git -C \"$source_worktree\" rebase --continue" >&2
|
|
333
|
+
echo "[agent-sync-guard] Or abort: git -C \"$source_worktree\" rebase --abort" >&2
|
|
334
|
+
fi
|
|
300
335
|
fi
|
|
301
336
|
exit 1
|
|
302
337
|
fi
|
|
@@ -366,6 +401,14 @@ is_local_branch_delete_error() {
|
|
|
366
401
|
return 1
|
|
367
402
|
}
|
|
368
403
|
|
|
404
|
+
is_remote_branch_missing_error() {
|
|
405
|
+
local output="$1"
|
|
406
|
+
if [[ "$output" == *"remote ref does not exist"* ]] || [[ "$output" == *"failed to push some refs"* ]]; then
|
|
407
|
+
return 0
|
|
408
|
+
fi
|
|
409
|
+
return 1
|
|
410
|
+
}
|
|
411
|
+
|
|
369
412
|
read_pr_state() {
|
|
370
413
|
local state_line
|
|
371
414
|
state_line="$("$GH_BIN" pr view "$SOURCE_BRANCH" --json state,mergedAt,url --jq '[.state, (.mergedAt // ""), (.url // "")] | join("\u001f")' 2>/dev/null || true)"
|
|
@@ -568,7 +611,15 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
|
|
|
568
611
|
|
|
569
612
|
if [[ "$PUSH_ENABLED" -eq 1 && "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then
|
|
570
613
|
if git -C "$repo_root" ls-remote --exit-code --heads origin "$SOURCE_BRANCH" >/dev/null 2>&1; then
|
|
571
|
-
|
|
614
|
+
remote_delete_output=""
|
|
615
|
+
if ! remote_delete_output="$(git -C "$repo_root" push origin --delete "$SOURCE_BRANCH" 2>&1)"; then
|
|
616
|
+
if is_remote_branch_missing_error "$remote_delete_output"; then
|
|
617
|
+
echo "[agent-branch-finish] Remote branch '${SOURCE_BRANCH}' was already deleted; continuing cleanup." >&2
|
|
618
|
+
else
|
|
619
|
+
echo "$remote_delete_output" >&2
|
|
620
|
+
exit 1
|
|
621
|
+
fi
|
|
622
|
+
fi
|
|
572
623
|
fi
|
|
573
624
|
fi
|
|
574
625
|
|
|
@@ -111,6 +111,13 @@ is_managed_worktree_path() {
|
|
|
111
111
|
return 1
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
is_temporary_worktree_path() {
|
|
115
|
+
local entry="$1"
|
|
116
|
+
local name
|
|
117
|
+
name="$(basename "$entry")"
|
|
118
|
+
[[ "$name" == __agent_integrate-* || "$name" == __source-probe-* ]]
|
|
119
|
+
}
|
|
120
|
+
|
|
114
121
|
resolve_base_branch() {
|
|
115
122
|
local configured=""
|
|
116
123
|
local current=""
|
|
@@ -425,7 +432,9 @@ process_entry() {
|
|
|
425
432
|
local remove_reason=""
|
|
426
433
|
local branch_delete_mode="safe"
|
|
427
434
|
|
|
428
|
-
if
|
|
435
|
+
if is_temporary_worktree_path "$wt"; then
|
|
436
|
+
remove_reason="temporary-worktree"
|
|
437
|
+
elif [[ -z "$branch_ref" ]]; then
|
|
429
438
|
remove_reason="detached-worktree"
|
|
430
439
|
elif ! git -C "$repo_root" show-ref --verify --quiet "refs/heads/${branch}"; then
|
|
431
440
|
remove_reason="missing-branch"
|
|
@@ -452,6 +461,11 @@ process_entry() {
|
|
|
452
461
|
return
|
|
453
462
|
fi
|
|
454
463
|
|
|
464
|
+
if [[ "$remove_reason" == "temporary-worktree" ]]; then
|
|
465
|
+
git -C "$wt" rebase --abort >/dev/null 2>&1 || true
|
|
466
|
+
git -C "$wt" merge --abort >/dev/null 2>&1 || true
|
|
467
|
+
fi
|
|
468
|
+
|
|
455
469
|
if [[ "$FORCE_DIRTY" -ne 1 ]] && ! is_clean_worktree "$wt"; then
|
|
456
470
|
skipped_dirty=$((skipped_dirty + 1))
|
|
457
471
|
echo "[agent-worktree-prune] Skipping dirty worktree (${remove_reason}): ${wt}"
|
|
@@ -90,6 +90,13 @@ string_has_lightweight_prefix() {
|
|
|
90
90
|
return 1
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
task_requires_full_change_workspace() {
|
|
94
|
+
local text="$1"
|
|
95
|
+
string_contains_any "$text" \
|
|
96
|
+
"cleanup evidence" "merged cleanup" "merged state" "pr url" \
|
|
97
|
+
"cleanup pipeline" "finish pipeline" "sandbox cleanup" "tasks.md"
|
|
98
|
+
}
|
|
99
|
+
|
|
93
100
|
derive_task_mode_from_tier() {
|
|
94
101
|
case "$1" in
|
|
95
102
|
T0|T1) printf 'caveman' ;;
|
|
@@ -128,8 +135,13 @@ decide_task_routing() {
|
|
|
128
135
|
fi
|
|
129
136
|
TASK_ROUTING_REASON="explicit tier override"
|
|
130
137
|
elif string_has_lightweight_prefix "$task_lower"; then
|
|
131
|
-
|
|
132
|
-
|
|
138
|
+
if task_requires_full_change_workspace "$task_lower"; then
|
|
139
|
+
OPENSPEC_TIER="T2"
|
|
140
|
+
TASK_ROUTING_REASON="cleanup-evidence artifact wording overrides lightweight prefix"
|
|
141
|
+
else
|
|
142
|
+
OPENSPEC_TIER="T1"
|
|
143
|
+
TASK_ROUTING_REASON="explicit lightweight prefix"
|
|
144
|
+
fi
|
|
133
145
|
elif string_contains_any "$task_lower" \
|
|
134
146
|
"ralph" "autopilot" "ultrawork" "ultraqa" "ralplan" "deep interview" "ouroboros" \
|
|
135
147
|
"migration" "refactor" "architecture" "re-architect" "cross-cutting" "multi-agent" \
|
|
@@ -17,11 +17,13 @@ node scripts/install-vscode-active-agents-extension.js
|
|
|
17
17
|
|
|
18
18
|
What it does:
|
|
19
19
|
|
|
20
|
+
- Bundles a local GitGuardex icon so repo installs show branded extension metadata inside VS Code.
|
|
20
21
|
- Adds an `Active Agents` view to the Source Control container.
|
|
21
22
|
- Renders one repo node per live Guardex workspace with grouped `ACTIVE AGENTS` and `CHANGES` sections.
|
|
22
23
|
- Splits live sessions inside `ACTIVE AGENTS` into `BLOCKED`, `WORKING NOW`, `IDLE`, `STALLED`, and `DEAD` groups so stuck, active, and inactive lanes stand out immediately.
|
|
24
|
+
- Mirrors the same live state in the VS Code status bar so the selected session or active-agent count stays visible outside the tree.
|
|
23
25
|
- Shows one row per live Guardex sandbox session inside those activity groups.
|
|
24
26
|
- Shows repo-root git changes in a sibling `CHANGES` section when the guarded repo itself is dirty.
|
|
25
27
|
- Derives session state from dirty worktree status, git conflict markers, PID liveness, and recent file mtimes, surfaces working/dead counts in the repo/header summary, and shows changed-file counts for active edits.
|
|
26
28
|
- Uses distinct VS Code codicons for each session state: `warning`, `edit`, `loading~spin`, `clock`, and `error`.
|
|
27
|
-
- Reads repo-local presence files from `.omx/state/active-sessions
|
|
29
|
+
- Reads repo-local presence files from `.omx/state/active-sessions/` and falls back to managed worktree-root `AGENT.lock` telemetry when the launcher session file is absent.
|
|
@@ -15,9 +15,15 @@ const IDLE_ERROR_MS = 30 * 60 * 1000;
|
|
|
15
15
|
const LOCK_FILE_RELATIVE = path.join('.omx', 'state', 'agent-file-locks.json');
|
|
16
16
|
const ACTIVE_SESSION_FILES_GLOB = '**/.omx/state/active-sessions/*.json';
|
|
17
17
|
const AGENT_FILE_LOCKS_GLOB = '**/.omx/state/agent-file-locks.json';
|
|
18
|
+
const WORKTREE_AGENT_LOCKS_GLOB = '**/{.omx,.omc}/agent-worktrees/**/AGENT.lock';
|
|
18
19
|
const SESSION_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git,.omx/agent-worktrees,.omc/agent-worktrees}/**';
|
|
20
|
+
const WORKTREE_LOCK_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git}/**';
|
|
19
21
|
const SESSION_SCAN_LIMIT = 200;
|
|
20
22
|
const REFRESH_DEBOUNCE_MS = 250;
|
|
23
|
+
const ACTIVE_AGENTS_MANIFEST_RELATIVE = path.join('vscode', 'guardex-active-agents', 'package.json');
|
|
24
|
+
const ACTIVE_AGENTS_INSTALL_SCRIPT_RELATIVE = path.join('scripts', 'install-vscode-active-agents-extension.js');
|
|
25
|
+
const RELOAD_WINDOW_ACTION = 'Reload Window';
|
|
26
|
+
const UPDATE_LATER_ACTION = 'Later';
|
|
21
27
|
const SESSION_ACTIVITY_GROUPS = [
|
|
22
28
|
{ kind: 'blocked', label: 'BLOCKED' },
|
|
23
29
|
{ kind: 'working', label: 'WORKING NOW' },
|
|
@@ -91,10 +97,83 @@ function sessionIdleDecoration(session, now = Date.now()) {
|
|
|
91
97
|
return undefined;
|
|
92
98
|
}
|
|
93
99
|
|
|
100
|
+
function formatCountLabel(count, singular, plural = `${singular}s`) {
|
|
101
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function sessionIdentityLabel(session) {
|
|
105
|
+
const agentName = typeof session?.agentName === 'string' ? session.agentName.trim() : '';
|
|
106
|
+
const taskName = typeof session?.taskName === 'string' ? session.taskName.trim() : '';
|
|
107
|
+
const label = typeof session?.label === 'string' ? session.label.trim() : '';
|
|
108
|
+
|
|
109
|
+
if (agentName && taskName) {
|
|
110
|
+
return `${agentName} · ${taskName}`;
|
|
111
|
+
}
|
|
112
|
+
if (agentName && label) {
|
|
113
|
+
return `${agentName} · ${label}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return agentName || taskName || label || 'session';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function sessionCommitPlaceholder(session) {
|
|
120
|
+
if (!session?.branch) {
|
|
121
|
+
return 'Pick an Active Agents session to commit its worktree.';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return `Commit ${sessionIdentityLabel(session)} on ${session.branch} · ${formatCountLabel(session.lockCount || 0, 'lock')} (Ctrl+Enter)`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function agentNameFromBranch(branch) {
|
|
128
|
+
const segments = String(branch || '')
|
|
129
|
+
.split('/')
|
|
130
|
+
.map((segment) => segment.trim())
|
|
131
|
+
.filter(Boolean);
|
|
132
|
+
if (segments[0] === 'agent' && segments[1]) {
|
|
133
|
+
return segments[1];
|
|
134
|
+
}
|
|
135
|
+
return segments[0] || 'lock';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function agentBadgeFromBranch(branch) {
|
|
139
|
+
const normalized = agentNameFromBranch(branch).toUpperCase().replace(/[^A-Z0-9]/g, '');
|
|
140
|
+
return normalized.slice(0, 2) || 'LK';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function buildActiveAgentsStatusSummary(summary) {
|
|
144
|
+
const activeCount = Math.max(0, (summary?.sessionCount || 0) - (summary?.deadCount || 0));
|
|
145
|
+
if (activeCount > 0) {
|
|
146
|
+
return `$(git-branch) ${formatCountLabel(activeCount, 'active agent')}`;
|
|
147
|
+
}
|
|
148
|
+
return `$(git-branch) ${formatCountLabel(summary?.sessionCount || 0, 'tracked session')}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function buildActiveAgentsStatusTooltip(selectedSession, summary) {
|
|
152
|
+
if (selectedSession?.branch) {
|
|
153
|
+
return [
|
|
154
|
+
selectedSession.branch,
|
|
155
|
+
sessionIdentityLabel(selectedSession),
|
|
156
|
+
formatCountLabel(selectedSession.lockCount || 0, 'lock'),
|
|
157
|
+
selectedSession.worktreePath,
|
|
158
|
+
'Click to open Source Control.',
|
|
159
|
+
].filter(Boolean).join('\n');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const activeCount = Math.max(0, (summary?.sessionCount || 0) - (summary?.deadCount || 0));
|
|
163
|
+
return [
|
|
164
|
+
formatCountLabel(activeCount, 'active agent'),
|
|
165
|
+
formatCountLabel(summary?.workingCount || 0, 'working now session', 'working now sessions'),
|
|
166
|
+
summary?.deadCount ? formatCountLabel(summary.deadCount, 'dead session') : '',
|
|
167
|
+
'Click to open Source Control.',
|
|
168
|
+
].filter(Boolean).join('\n');
|
|
169
|
+
}
|
|
170
|
+
|
|
94
171
|
class SessionDecorationProvider {
|
|
95
172
|
constructor(nowProvider = () => Date.now()) {
|
|
96
173
|
this.nowProvider = nowProvider;
|
|
97
174
|
this.sessionsByUri = new Map();
|
|
175
|
+
this.lockEntriesByFileUri = new Map();
|
|
176
|
+
this.selectedBranch = '';
|
|
98
177
|
this.onDidChangeFileDecorationsEmitter = new vscode.EventEmitter();
|
|
99
178
|
this.onDidChangeFileDecorations = this.onDidChangeFileDecorationsEmitter.event;
|
|
100
179
|
}
|
|
@@ -105,13 +184,54 @@ class SessionDecorationProvider {
|
|
|
105
184
|
);
|
|
106
185
|
}
|
|
107
186
|
|
|
187
|
+
updateLockEntries(repoEntries) {
|
|
188
|
+
const nextEntriesByUri = new Map();
|
|
189
|
+
for (const entry of repoEntries || []) {
|
|
190
|
+
for (const [relativePath, lockEntry] of entry.lockEntries || []) {
|
|
191
|
+
nextEntriesByUri.set(
|
|
192
|
+
vscode.Uri.file(path.join(entry.repoRoot, relativePath)).toString(),
|
|
193
|
+
{ branch: lockEntry.branch },
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
this.lockEntriesByFileUri = nextEntriesByUri;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
setSelectedBranch(branch) {
|
|
201
|
+
this.selectedBranch = typeof branch === 'string' ? branch.trim() : '';
|
|
202
|
+
}
|
|
203
|
+
|
|
108
204
|
refresh() {
|
|
109
205
|
this.onDidChangeFileDecorationsEmitter.fire();
|
|
110
206
|
}
|
|
111
207
|
|
|
112
208
|
provideFileDecoration(uri) {
|
|
113
209
|
if (!uri || uri.scheme !== SESSION_DECORATION_SCHEME) {
|
|
114
|
-
|
|
210
|
+
if (!uri || uri.scheme !== 'file') {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const lockEntry = this.lockEntriesByFileUri.get(uri.toString());
|
|
215
|
+
if (!lockEntry?.branch) {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const ownsSelectedSession = Boolean(this.selectedBranch) && lockEntry.branch === this.selectedBranch;
|
|
220
|
+
return {
|
|
221
|
+
badge: agentBadgeFromBranch(lockEntry.branch),
|
|
222
|
+
tooltip: ownsSelectedSession
|
|
223
|
+
? `Locked by selected session ${lockEntry.branch}`
|
|
224
|
+
: this.selectedBranch
|
|
225
|
+
? `Locked by ${lockEntry.branch} (selected session: ${this.selectedBranch})`
|
|
226
|
+
: `Locked by ${lockEntry.branch}`,
|
|
227
|
+
color: new vscode.ThemeColor(
|
|
228
|
+
ownsSelectedSession
|
|
229
|
+
? 'gitDecoration.modifiedResourceForeground'
|
|
230
|
+
: this.selectedBranch
|
|
231
|
+
? 'list.errorForeground'
|
|
232
|
+
: 'list.warningForeground',
|
|
233
|
+
),
|
|
234
|
+
};
|
|
115
235
|
}
|
|
116
236
|
|
|
117
237
|
return sessionIdleDecoration(this.sessionsByUri.get(uri.toString()), this.nowProvider());
|
|
@@ -187,14 +307,23 @@ class SessionItem extends vscode.TreeItem {
|
|
|
187
307
|
const tooltipLines = [
|
|
188
308
|
session.branch,
|
|
189
309
|
`${session.agentName} · ${session.taskName}`,
|
|
310
|
+
session.latestTaskPreview && session.latestTaskPreview !== session.taskName
|
|
311
|
+
? `Live task ${session.latestTaskPreview}`
|
|
312
|
+
: '',
|
|
190
313
|
`Status ${this.description}`,
|
|
191
314
|
session.changeCount > 0
|
|
192
315
|
? `Changed ${session.activityCountLabel}: ${session.activitySummary}`
|
|
193
316
|
: session.activitySummary,
|
|
194
317
|
`Locks ${lockCount}`,
|
|
195
|
-
|
|
318
|
+
Number.isInteger(session.pid) && session.pid > 0
|
|
319
|
+
? session.pidAlive === false
|
|
320
|
+
? `PID ${session.pid} not alive`
|
|
321
|
+
: `PID ${session.pid} alive`
|
|
322
|
+
: '',
|
|
196
323
|
session.lastFileActivityAt ? `Last file activity ${session.lastFileActivityAt}` : '',
|
|
197
|
-
|
|
324
|
+
session.sourceKind === 'worktree-lock'
|
|
325
|
+
? `Telemetry updated ${session.telemetryUpdatedAt || session.startedAt}`
|
|
326
|
+
: `Started ${session.startedAt}`,
|
|
198
327
|
session.worktreePath,
|
|
199
328
|
];
|
|
200
329
|
this.tooltip = tooltipLines.filter(Boolean).join('\n');
|
|
@@ -365,6 +494,10 @@ function repoRootFromSessionFile(filePath) {
|
|
|
365
494
|
return path.resolve(path.dirname(filePath), '..', '..', '..');
|
|
366
495
|
}
|
|
367
496
|
|
|
497
|
+
function repoRootFromWorktreeLockFile(filePath) {
|
|
498
|
+
return path.resolve(path.dirname(filePath), '..', '..', '..');
|
|
499
|
+
}
|
|
500
|
+
|
|
368
501
|
function repoRootFromLockFile(filePath) {
|
|
369
502
|
return path.resolve(path.dirname(filePath), '..', '..');
|
|
370
503
|
}
|
|
@@ -436,6 +569,120 @@ function readCurrentBranch(repoRoot) {
|
|
|
436
569
|
}
|
|
437
570
|
}
|
|
438
571
|
|
|
572
|
+
function parseSimpleSemver(version) {
|
|
573
|
+
const parts = String(version || '')
|
|
574
|
+
.split('.')
|
|
575
|
+
.map((part) => Number.parseInt(part, 10));
|
|
576
|
+
if (parts.length !== 3 || parts.some((part) => Number.isNaN(part))) {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
return parts;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function compareSimpleSemver(left, right) {
|
|
583
|
+
const leftParts = parseSimpleSemver(left);
|
|
584
|
+
const rightParts = parseSimpleSemver(right);
|
|
585
|
+
if (!leftParts || !rightParts) {
|
|
586
|
+
return 0;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
for (let index = 0; index < leftParts.length; index += 1) {
|
|
590
|
+
if (leftParts[index] !== rightParts[index]) {
|
|
591
|
+
return leftParts[index] - rightParts[index];
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return 0;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function readJsonFile(filePath) {
|
|
599
|
+
try {
|
|
600
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
601
|
+
} catch (_error) {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function resolveActiveAgentsAutoUpdateCandidate(installedVersion) {
|
|
607
|
+
const candidates = [];
|
|
608
|
+
|
|
609
|
+
for (const workspaceFolder of vscode.workspace.workspaceFolders || []) {
|
|
610
|
+
const repoRoot = workspaceFolder?.uri?.fsPath;
|
|
611
|
+
if (!repoRoot) {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const manifestPath = path.join(repoRoot, ACTIVE_AGENTS_MANIFEST_RELATIVE);
|
|
616
|
+
const installScriptPath = path.join(repoRoot, ACTIVE_AGENTS_INSTALL_SCRIPT_RELATIVE);
|
|
617
|
+
if (!fs.existsSync(manifestPath) || !fs.existsSync(installScriptPath)) {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const manifest = readJsonFile(manifestPath);
|
|
622
|
+
const nextVersion = typeof manifest?.version === 'string' ? manifest.version.trim() : '';
|
|
623
|
+
if (!nextVersion || compareSimpleSemver(nextVersion, installedVersion) <= 0) {
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
candidates.push({ repoRoot, installScriptPath, version: nextVersion });
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
candidates.sort((left, right) => compareSimpleSemver(right.version, left.version));
|
|
631
|
+
return candidates[0] || null;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function runActiveAgentsInstallScript(repoRoot, installScriptPath) {
|
|
635
|
+
return new Promise((resolve, reject) => {
|
|
636
|
+
cp.execFile(
|
|
637
|
+
process.execPath,
|
|
638
|
+
[installScriptPath],
|
|
639
|
+
{ cwd: repoRoot, encoding: 'utf8' },
|
|
640
|
+
(error, stdout, stderr) => {
|
|
641
|
+
if (error) {
|
|
642
|
+
reject(new Error(String(stderr || stdout || error.message || '').trim() || 'install failed'));
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
resolve({ stdout, stderr });
|
|
646
|
+
},
|
|
647
|
+
);
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async function maybeAutoUpdateActiveAgentsExtension(context) {
|
|
652
|
+
const installedVersion = typeof context?.extension?.packageJSON?.version === 'string'
|
|
653
|
+
? context.extension.packageJSON.version.trim()
|
|
654
|
+
: '';
|
|
655
|
+
if (!installedVersion) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const candidate = resolveActiveAgentsAutoUpdateCandidate(installedVersion);
|
|
660
|
+
if (!candidate) {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
try {
|
|
665
|
+
await runActiveAgentsInstallScript(candidate.repoRoot, candidate.installScriptPath);
|
|
666
|
+
} catch (error) {
|
|
667
|
+
const failure = typeof error?.message === 'string' && error.message.trim()
|
|
668
|
+
? error.message.trim()
|
|
669
|
+
: 'install failed';
|
|
670
|
+
vscode.window.showWarningMessage?.(
|
|
671
|
+
`GitGuardex Active Agents could not auto-update to ${candidate.version}: ${failure}`,
|
|
672
|
+
);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const selection = await vscode.window.showInformationMessage?.(
|
|
677
|
+
`GitGuardex Active Agents updated to ${candidate.version}. Reload Window to use the newest companion.`,
|
|
678
|
+
RELOAD_WINDOW_ACTION,
|
|
679
|
+
UPDATE_LATER_ACTION,
|
|
680
|
+
);
|
|
681
|
+
if (selection === RELOAD_WINDOW_ACTION) {
|
|
682
|
+
await vscode.commands.executeCommand('workbench.action.reloadWindow');
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
439
686
|
function decorateSession(session, lockRegistry) {
|
|
440
687
|
return {
|
|
441
688
|
...session,
|
|
@@ -479,16 +726,29 @@ function localizeChangeForSession(session, change) {
|
|
|
479
726
|
}
|
|
480
727
|
|
|
481
728
|
async function findRepoSessionEntries() {
|
|
482
|
-
const sessionFiles = await
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
729
|
+
const [sessionFiles, worktreeLockFiles] = await Promise.all([
|
|
730
|
+
vscode.workspace.findFiles(
|
|
731
|
+
ACTIVE_SESSION_FILES_GLOB,
|
|
732
|
+
SESSION_SCAN_EXCLUDE_GLOB,
|
|
733
|
+
SESSION_SCAN_LIMIT,
|
|
734
|
+
),
|
|
735
|
+
vscode.workspace.findFiles(
|
|
736
|
+
WORKTREE_AGENT_LOCKS_GLOB,
|
|
737
|
+
WORKTREE_LOCK_SCAN_EXCLUDE_GLOB,
|
|
738
|
+
SESSION_SCAN_LIMIT,
|
|
739
|
+
),
|
|
740
|
+
]);
|
|
487
741
|
|
|
488
742
|
const repoRoots = new Set();
|
|
489
743
|
for (const uri of sessionFiles) {
|
|
490
744
|
repoRoots.add(repoRootFromSessionFile(uri.fsPath));
|
|
491
745
|
}
|
|
746
|
+
for (const uri of worktreeLockFiles) {
|
|
747
|
+
if (path.basename(uri.fsPath) !== 'AGENT.lock') {
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
repoRoots.add(repoRootFromWorktreeLockFile(uri.fsPath));
|
|
751
|
+
}
|
|
492
752
|
|
|
493
753
|
if (repoRoots.size === 0) {
|
|
494
754
|
for (const workspaceFolder of vscode.workspace.workspaceFolders || []) {
|
|
@@ -808,6 +1068,11 @@ class ActiveAgentsProvider {
|
|
|
808
1068
|
this.treeView = null;
|
|
809
1069
|
this.lockRegistryByRepoRoot = new Map();
|
|
810
1070
|
this.selectedSession = null;
|
|
1071
|
+
this.viewSummary = {
|
|
1072
|
+
sessionCount: 0,
|
|
1073
|
+
workingCount: 0,
|
|
1074
|
+
deadCount: 0,
|
|
1075
|
+
};
|
|
811
1076
|
}
|
|
812
1077
|
|
|
813
1078
|
getTreeItem(element) {
|
|
@@ -828,6 +1093,7 @@ class ActiveAgentsProvider {
|
|
|
828
1093
|
const currentKey = sessionSelectionKey(this.selectedSession);
|
|
829
1094
|
const nextKey = sessionSelectionKey(nextSession);
|
|
830
1095
|
this.selectedSession = nextSession;
|
|
1096
|
+
this.decorationProvider?.setSelectedBranch(nextSession?.branch || '');
|
|
831
1097
|
if (currentKey !== nextKey) {
|
|
832
1098
|
this.onDidChangeSelectedSessionEmitter.fire(this.selectedSession);
|
|
833
1099
|
}
|
|
@@ -837,6 +1103,10 @@ class ActiveAgentsProvider {
|
|
|
837
1103
|
return this.selectedSession ? { ...this.selectedSession } : null;
|
|
838
1104
|
}
|
|
839
1105
|
|
|
1106
|
+
getViewSummary() {
|
|
1107
|
+
return { ...this.viewSummary };
|
|
1108
|
+
}
|
|
1109
|
+
|
|
840
1110
|
syncSelectedSession(repoEntries) {
|
|
841
1111
|
if (!this.selectedSession) {
|
|
842
1112
|
return;
|
|
@@ -854,6 +1124,11 @@ class ActiveAgentsProvider {
|
|
|
854
1124
|
}
|
|
855
1125
|
|
|
856
1126
|
const activeCount = Math.max(0, sessionCount - deadCount);
|
|
1127
|
+
this.viewSummary = {
|
|
1128
|
+
sessionCount,
|
|
1129
|
+
workingCount,
|
|
1130
|
+
deadCount,
|
|
1131
|
+
};
|
|
857
1132
|
const badgeTooltipParts = [];
|
|
858
1133
|
if (activeCount > 0) {
|
|
859
1134
|
badgeTooltipParts.push(`${activeCount} active agent${activeCount === 1 ? '' : 's'}`);
|
|
@@ -890,6 +1165,7 @@ class ActiveAgentsProvider {
|
|
|
890
1165
|
|
|
891
1166
|
this.updateViewState(sessionCount, workingCount, deadCount);
|
|
892
1167
|
this.decorationProvider?.updateSessions(repoEntries.flatMap((entry) => entry.sessions));
|
|
1168
|
+
this.decorationProvider?.updateLockEntries(repoEntries);
|
|
893
1169
|
return repoEntries;
|
|
894
1170
|
}
|
|
895
1171
|
|
|
@@ -968,6 +1244,7 @@ class ActiveAgentsProvider {
|
|
|
968
1244
|
changes: readRepoChanges(repoRoot).map((change) => (
|
|
969
1245
|
decorateChange(change, lockRegistry, currentBranch)
|
|
970
1246
|
)),
|
|
1247
|
+
lockEntries: Array.from(lockRegistry.entriesByPath.entries()),
|
|
971
1248
|
};
|
|
972
1249
|
});
|
|
973
1250
|
}
|
|
@@ -1052,19 +1329,36 @@ function activate(context) {
|
|
|
1052
1329
|
'gitguardex.activeAgents.commitInput',
|
|
1053
1330
|
'Active Agents Commit',
|
|
1054
1331
|
);
|
|
1332
|
+
const activeAgentsStatusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 10);
|
|
1333
|
+
activeAgentsStatusItem.name = 'GitGuardex Active Agents';
|
|
1334
|
+
activeAgentsStatusItem.command = 'gitguardex.activeAgents.focus';
|
|
1055
1335
|
provider.attachTreeView(treeView);
|
|
1056
1336
|
const scheduleRefresh = () => refreshController.scheduleRefresh();
|
|
1057
1337
|
const refresh = () => void refreshController.refreshNow();
|
|
1058
1338
|
const activeSessionsWatcher = vscode.workspace.createFileSystemWatcher(ACTIVE_SESSION_FILES_GLOB);
|
|
1059
1339
|
const lockWatcher = vscode.workspace.createFileSystemWatcher(AGENT_FILE_LOCKS_GLOB);
|
|
1340
|
+
const worktreeLockWatcher = vscode.workspace.createFileSystemWatcher(WORKTREE_AGENT_LOCKS_GLOB);
|
|
1060
1341
|
const updateCommitInput = (session) => {
|
|
1061
1342
|
sourceControl.inputBox.enabled = true;
|
|
1062
1343
|
sourceControl.inputBox.visible = true;
|
|
1063
|
-
sourceControl.inputBox.placeholder = session
|
|
1064
|
-
|
|
1065
|
-
|
|
1344
|
+
sourceControl.inputBox.placeholder = sessionCommitPlaceholder(session);
|
|
1345
|
+
};
|
|
1346
|
+
const updateStatusBar = () => {
|
|
1347
|
+
const selectedSession = provider.getSelectedSession();
|
|
1348
|
+
const summary = provider.getViewSummary();
|
|
1349
|
+
if ((summary.sessionCount || 0) <= 0) {
|
|
1350
|
+
activeAgentsStatusItem.hide();
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
activeAgentsStatusItem.text = selectedSession?.branch
|
|
1355
|
+
? `$(git-branch) ${sessionIdentityLabel(selectedSession)} · ${formatCountLabel(selectedSession.lockCount || 0, 'lock')}`
|
|
1356
|
+
: buildActiveAgentsStatusSummary(summary);
|
|
1357
|
+
activeAgentsStatusItem.tooltip = buildActiveAgentsStatusTooltip(selectedSession, summary);
|
|
1358
|
+
activeAgentsStatusItem.show();
|
|
1066
1359
|
};
|
|
1067
1360
|
updateCommitInput(null);
|
|
1361
|
+
updateStatusBar();
|
|
1068
1362
|
const commitSelectedSession = async () => {
|
|
1069
1363
|
const selectedSession = provider.getSelectedSession();
|
|
1070
1364
|
if (!selectedSession?.worktreePath) {
|
|
@@ -1111,15 +1405,27 @@ function activate(context) {
|
|
|
1111
1405
|
scheduleRefresh();
|
|
1112
1406
|
};
|
|
1113
1407
|
|
|
1114
|
-
provider.onDidChangeSelectedSession(
|
|
1408
|
+
provider.onDidChangeSelectedSession((session) => {
|
|
1409
|
+
updateCommitInput(session);
|
|
1410
|
+
updateStatusBar();
|
|
1411
|
+
decorationProvider.refresh();
|
|
1412
|
+
});
|
|
1413
|
+
provider.onDidChangeTreeData(() => {
|
|
1414
|
+
updateCommitInput(provider.getSelectedSession());
|
|
1415
|
+
updateStatusBar();
|
|
1416
|
+
});
|
|
1115
1417
|
|
|
1116
1418
|
context.subscriptions.push(
|
|
1117
1419
|
treeView,
|
|
1118
1420
|
sourceControl,
|
|
1421
|
+
activeAgentsStatusItem,
|
|
1119
1422
|
refreshController,
|
|
1120
1423
|
vscode.window.registerFileDecorationProvider(decorationProvider),
|
|
1121
1424
|
vscode.commands.registerCommand('gitguardex.activeAgents.startAgent', () => startAgentFromPrompt(refresh)),
|
|
1122
1425
|
vscode.commands.registerCommand('gitguardex.activeAgents.refresh', refresh),
|
|
1426
|
+
vscode.commands.registerCommand('gitguardex.activeAgents.focus', async () => {
|
|
1427
|
+
await vscode.commands.executeCommand('workbench.view.scm');
|
|
1428
|
+
}),
|
|
1123
1429
|
vscode.commands.registerCommand('gitguardex.activeAgents.commitSelectedSession', commitSelectedSession),
|
|
1124
1430
|
vscode.commands.registerCommand('gitguardex.activeAgents.openWorktree', async (session) => {
|
|
1125
1431
|
if (!session?.worktreePath) {
|
|
@@ -1151,14 +1457,17 @@ function activate(context) {
|
|
|
1151
1457
|
vscode.workspace.onDidChangeWorkspaceFolders(scheduleRefresh),
|
|
1152
1458
|
activeSessionsWatcher,
|
|
1153
1459
|
lockWatcher,
|
|
1460
|
+
worktreeLockWatcher,
|
|
1154
1461
|
{ dispose: () => clearInterval(interval) },
|
|
1155
1462
|
);
|
|
1156
1463
|
|
|
1157
1464
|
context.subscriptions.push(
|
|
1158
1465
|
...bindRefreshWatcher(activeSessionsWatcher, scheduleRefresh),
|
|
1159
1466
|
...bindRefreshWatcher(lockWatcher, refreshLockRegistry),
|
|
1467
|
+
...bindRefreshWatcher(worktreeLockWatcher, scheduleRefresh),
|
|
1160
1468
|
);
|
|
1161
1469
|
void refreshController.refreshNow();
|
|
1470
|
+
void maybeAutoUpdateActiveAgentsExtension(context);
|
|
1162
1471
|
}
|
|
1163
1472
|
|
|
1164
1473
|
function deactivate() {}
|
|
Binary file
|