@imdeadpool/guardex 7.0.38 → 7.0.39

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 CHANGED
@@ -266,6 +266,12 @@ Being honest about where this still has issues:
266
266
  <details open>
267
267
  <summary><strong>v7.x</strong></summary>
268
268
 
269
+ ### v7.0.39
270
+ - Bumped `@imdeadpool/guardex` from `7.0.38` to `7.0.39` so the current
271
+ `main` payload can publish under a fresh npm version after `7.0.38` reached
272
+ the registry.
273
+ - No new CLI command behavior is introduced in this release lane.
274
+
269
275
  ### v7.0.38
270
276
  - Bumped `@imdeadpool/guardex` from `7.0.37` to `7.0.38` so the current
271
277
  `main` payload can publish under a fresh npm version after `7.0.37` reached
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imdeadpool/guardex",
3
- "version": "7.0.38",
3
+ "version": "7.0.39",
4
4
  "description": "Guardian T-Rex for your multi-agent repo. Isolated worktrees, file locks, and PR-only merges stop parallel Codex & Claude agents from overwriting each other's work. Auto-wires Oh My Codex, Oh My Claude, OpenSpec, and Caveman.",
5
5
  "license": "MIT",
6
6
  "preferGlobal": true,
package/src/cli/main.js CHANGED
@@ -408,8 +408,9 @@ function runSetupBootstrapInternal(options) {
408
408
  }
409
409
 
410
410
  function extractAgentBranchStartMetadata(output) {
411
- const branchMatch = String(output || '').match(/^\[agent-branch-start\] Created branch: (.+)$/m);
412
- const worktreeMatch = String(output || '').match(/^\[agent-branch-start\] Worktree: (.+)$/m);
411
+ const outputText = String(output || '');
412
+ const branchMatch = outputText.match(/^\[agent-branch-start\] (?:Created branch|Reusing existing branch): (.+)$/m);
413
+ const worktreeMatch = outputText.match(/^\[agent-branch-start\] Worktree: (.+)$/m);
413
414
  return {
414
415
  branch: branchMatch ? branchMatch[1].trim() : '',
415
416
  worktreePath: worktreeMatch ? worktreeMatch[1].trim() : '',
@@ -3466,7 +3467,7 @@ function pivot(rawArgs) {
3466
3467
  }
3467
3468
  const stdoutText = String(result.stdout || '');
3468
3469
  const wtMatch = stdoutText.match(/^\[agent-branch-start\] Worktree:\s+(.+)$/m);
3469
- const branchMatch = stdoutText.match(/^\[agent-branch-start\] Created branch:\s+(.+)$/m);
3470
+ const branchMatch = stdoutText.match(/^\[agent-branch-start\] (?:Created branch|Reusing existing branch):\s+(.+)$/m);
3470
3471
  if (wtMatch) {
3471
3472
  const wtPath = wtMatch[1].trim();
3472
3473
  process.stdout.write('\n');
@@ -65,8 +65,9 @@ function assertProtectedMainWriteAllowed(options, commandName) {
65
65
  }
66
66
 
67
67
  function extractAgentBranchStartMetadata(output) {
68
- const branchMatch = String(output || '').match(/^\[agent-branch-start\] Created branch: (.+)$/m);
69
- const worktreeMatch = String(output || '').match(/^\[agent-branch-start\] Worktree: (.+)$/m);
68
+ const outputText = String(output || '');
69
+ const branchMatch = outputText.match(/^\[agent-branch-start\] (?:Created branch|Reusing existing branch): (.+)$/m);
70
+ const worktreeMatch = outputText.match(/^\[agent-branch-start\] Worktree: (.+)$/m);
70
71
  return {
71
72
  branch: branchMatch ? branchMatch[1].trim() : '',
72
73
  worktreePath: worktreeMatch ? worktreeMatch[1].trim() : '',
@@ -508,7 +508,7 @@ is_local_branch_delete_error() {
508
508
 
509
509
  is_remote_branch_missing_error() {
510
510
  local output="$1"
511
- if [[ "$output" == *"remote ref does not exist"* ]] || [[ "$output" == *"failed to push some refs"* ]]; then
511
+ if [[ "$output" == *"remote ref does not exist"* ]]; then
512
512
  return 0
513
513
  fi
514
514
  return 1
@@ -893,8 +893,8 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
893
893
  if is_remote_branch_missing_error "$remote_delete_output"; then
894
894
  echo "[agent-branch-finish] Remote branch '${SOURCE_BRANCH}' was already deleted; continuing cleanup." >&2
895
895
  else
896
+ echo "[agent-branch-finish] Warning: remote branch cleanup failed for '${SOURCE_BRANCH}' after merge; continuing local cleanup." >&2
896
897
  echo "$remote_delete_output" >&2
897
- exit 1
898
898
  fi
899
899
  fi
900
900
  fi
@@ -288,7 +288,7 @@ if [[ -z "$TARGET_BRANCH" ]]; then
288
288
  fi
289
289
 
290
290
  printf '%s\n' "$start_output"
291
- TARGET_BRANCH="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Created branch: //p' | head -n 1)"
291
+ TARGET_BRANCH="$(printf '%s\n' "$start_output" | sed -n -E 's/^\[agent-branch-start\] (Created branch|Reusing existing branch): //p' | head -n 1)"
292
292
  target_worktree="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Worktree: //p' | head -n 1)"
293
293
  if [[ -z "$TARGET_BRANCH" || -z "$target_worktree" ]]; then
294
294
  echo "[agent-branch-merge] Unable to parse target branch/worktree from agent-branch-start output." >&2
@@ -15,6 +15,7 @@ OPENSPEC_CHANGE_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CHANGE_SLUG:-}"
15
15
  OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CAPABILITY_SLUG:-}"
16
16
  OPENSPEC_MASTERPLAN_LABEL_RAW="${GUARDEX_OPENSPEC_MASTERPLAN_LABEL-masterplan}"
17
17
  OPENSPEC_TIER_RAW="${GUARDEX_OPENSPEC_TIER:-T3}"
18
+ REUSE_EXISTING_RAW="${GUARDEX_BRANCH_START_REUSE_EXISTING:-true}"
18
19
  PRINT_NAME_ONLY=0
19
20
  POSITIONAL_ARGS=()
20
21
 
@@ -58,6 +59,14 @@ while [[ $# -gt 0 ]]; do
58
59
  OPENSPEC_TIER_RAW="${2:-$OPENSPEC_TIER_RAW}"
59
60
  shift 2
60
61
  ;;
62
+ --reuse-existing|--reuse)
63
+ REUSE_EXISTING_RAW="true"
64
+ shift
65
+ ;;
66
+ --new|--no-reuse|--no-reuse-existing)
67
+ REUSE_EXISTING_RAW="false"
68
+ shift
69
+ ;;
61
70
  --in-place|--allow-in-place)
62
71
  echo "[agent-branch-start] In-place branch mode is disabled." >&2
63
72
  echo "[agent-branch-start] This command always creates an isolated worktree to keep your active checkout unchanged." >&2
@@ -78,7 +87,7 @@ while [[ $# -gt 0 ]]; do
78
87
  ;;
79
88
  -*)
80
89
  echo "[agent-branch-start] Unknown option: $1" >&2
81
- echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>] [--print-name-only]" >&2
90
+ echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>] [--reuse-existing|--new] [--print-name-only]" >&2
82
91
  exit 1
83
92
  ;;
84
93
  *)
@@ -90,7 +99,7 @@ done
90
99
 
91
100
  if [[ "${#POSITIONAL_ARGS[@]}" -gt 3 ]]; then
92
101
  echo "[agent-branch-start] Too many positional arguments." >&2
93
- echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>]" >&2
102
+ echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>] [--reuse-existing|--new]" >&2
94
103
  exit 1
95
104
  fi
96
105
 
@@ -254,6 +263,7 @@ normalize_bool() {
254
263
  }
255
264
 
256
265
  OPENSPEC_AUTO_INIT="$(normalize_bool "$OPENSPEC_AUTO_INIT_RAW" "1")"
266
+ REUSE_EXISTING_WORKTREE="$(normalize_bool "$REUSE_EXISTING_RAW" "1")"
257
267
 
258
268
  normalize_tier() {
259
269
  local raw="${1:-}"
@@ -370,6 +380,22 @@ resolve_worktree_leaf() {
370
380
  printf '%s' "${branch_name//\//__}"
371
381
  }
372
382
 
383
+ print_reused_agent_worktree() {
384
+ local branch_name="$1"
385
+ local worktree_path="$2"
386
+
387
+ echo "[agent-branch-start] Reusing existing branch: ${branch_name}"
388
+ echo "[agent-branch-start] Worktree: ${worktree_path}"
389
+ echo "[agent-branch-start] OpenSpec tier: ${OPENSPEC_TIER}"
390
+ echo "[agent-branch-start] OpenSpec change: existing worktree"
391
+ echo "[agent-branch-start] OpenSpec plan: existing worktree"
392
+ echo "[agent-branch-start] Next steps:"
393
+ echo " cd \"${worktree_path}\""
394
+ echo " gx locks claim --branch \"${branch_name}\" <file...>"
395
+ echo " # continue work in this existing sandbox"
396
+ echo " gx branch finish --branch \"${branch_name}\" --via-pr --wait-for-merge"
397
+ }
398
+
373
399
  has_local_changes() {
374
400
  local root="$1"
375
401
  if ! git -C "$root" diff --quiet; then
@@ -550,6 +576,12 @@ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
550
576
  exit 1
551
577
  fi
552
578
 
579
+ current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
580
+ if [[ "$REUSE_EXISTING_WORKTREE" -eq 1 && "$current_branch" == agent/* ]]; then
581
+ print_reused_agent_worktree "$current_branch" "$repo_root"
582
+ exit 0
583
+ fi
584
+
553
585
  task_slug="$(sanitize_slug "$TASK_NAME" "task")"
554
586
  agent_slug="$(normalize_role "$AGENT_NAME")"
555
587
  if [[ "$WORKTREE_ROOT_EXPLICIT" -eq 0 ]]; then
@@ -681,6 +713,7 @@ if [[ -n "$auto_transfer_stash_ref" ]]; then
681
713
  fi
682
714
  fi
683
715
 
716
+ hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" ".venv"
684
717
  hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "node_modules"
685
718
  hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "apps/frontend/node_modules"
686
719
  hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "apps/backend/node_modules"
@@ -2395,6 +2395,74 @@ function isPathWithin(parentPath, targetPath) {
2395
2395
  return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
2396
2396
  }
2397
2397
 
2398
+ function normalizeAbsolutePath(value) {
2399
+ return typeof value === 'string' && value.trim() ? path.resolve(value) : '';
2400
+ }
2401
+
2402
+ function isManagedWorktreePath(worktreePath) {
2403
+ const normalizedWorktreePath = normalizeAbsolutePath(worktreePath);
2404
+ if (!normalizedWorktreePath) {
2405
+ return false;
2406
+ }
2407
+
2408
+ return MANAGED_WORKTREE_RELATIVE_ROOTS.some((relativeRoot) => {
2409
+ const normalizedRelativeRoot = path.normalize(relativeRoot);
2410
+ const marker = `${path.sep}${normalizedRelativeRoot}${path.sep}`;
2411
+ return normalizedWorktreePath.includes(marker);
2412
+ });
2413
+ }
2414
+
2415
+ function removeDeletedWorktreeWorkspaceFolder(worktreePath) {
2416
+ if (typeof vscode.workspace.updateWorkspaceFolders !== 'function') {
2417
+ return false;
2418
+ }
2419
+
2420
+ const normalizedWorktreePath = normalizeAbsolutePath(worktreePath);
2421
+ if (!normalizedWorktreePath) {
2422
+ return false;
2423
+ }
2424
+
2425
+ const workspaceFolders = vscode.workspace.workspaceFolders || [];
2426
+ const folderIndex = workspaceFolders.findIndex((folder) => (
2427
+ normalizeAbsolutePath(folder?.uri?.fsPath) === normalizedWorktreePath
2428
+ ));
2429
+ if (folderIndex < 0) {
2430
+ return false;
2431
+ }
2432
+
2433
+ try {
2434
+ return vscode.workspace.updateWorkspaceFolders(folderIndex, 1) === true;
2435
+ } catch (_error) {
2436
+ return false;
2437
+ }
2438
+ }
2439
+
2440
+ async function closeDeletedWorktreeRepository(worktreePath) {
2441
+ const normalizedWorktreePath = normalizeAbsolutePath(worktreePath);
2442
+ if (!normalizedWorktreePath || fs.existsSync(normalizedWorktreePath)) {
2443
+ return false;
2444
+ }
2445
+
2446
+ try {
2447
+ await vscode.commands.executeCommand('git.close', vscode.Uri.file(normalizedWorktreePath));
2448
+ } catch (_error) {
2449
+ // The Git extension may have already removed this repository.
2450
+ }
2451
+
2452
+ removeDeletedWorktreeWorkspaceFolder(normalizedWorktreePath);
2453
+ return true;
2454
+ }
2455
+
2456
+ function findDeletedManagedWorkspaceFolders() {
2457
+ return (vscode.workspace.workspaceFolders || [])
2458
+ .map((folder) => normalizeAbsolutePath(folder?.uri?.fsPath))
2459
+ .filter((workspacePath) => (
2460
+ workspacePath
2461
+ && !fs.existsSync(workspacePath)
2462
+ && isManagedWorktreePath(workspacePath)
2463
+ ));
2464
+ }
2465
+
2398
2466
  function localizeChangeForSession(session, change) {
2399
2467
  if (!change?.absolutePath || !isPathWithin(session.worktreePath, change.absolutePath)) {
2400
2468
  return null;
@@ -3434,6 +3502,8 @@ class ActiveAgentsRefreshController {
3434
3502
  this.inspectPanelManager = inspectPanelManager;
3435
3503
  this.refreshTimer = null;
3436
3504
  this.sessionWatchers = new Map();
3505
+ this.closedMissingWorktreeRepositories = new Set();
3506
+ this.observedWorktreePaths = new Set();
3437
3507
  }
3438
3508
 
3439
3509
  scheduleRefresh() {
@@ -3456,8 +3526,23 @@ class ActiveAgentsRefreshController {
3456
3526
  const repoEntries = await findRepoSessionEntries();
3457
3527
  const liveSessionKeys = new Set();
3458
3528
 
3529
+ for (const workspacePath of findDeletedManagedWorkspaceFolders()) {
3530
+ await this.closeMissingWorktreeRepository(workspacePath);
3531
+ }
3532
+
3459
3533
  for (const entry of repoEntries) {
3460
3534
  for (const session of entry.sessions) {
3535
+ const worktreePath = sessionWorktreePath(session);
3536
+ const normalizedWorktreePath = normalizeAbsolutePath(worktreePath);
3537
+ if (normalizedWorktreePath && !fs.existsSync(normalizedWorktreePath)) {
3538
+ await this.closeMissingWorktreeRepository(normalizedWorktreePath);
3539
+ continue;
3540
+ }
3541
+ if (normalizedWorktreePath) {
3542
+ this.closedMissingWorktreeRepositories.delete(normalizedWorktreePath);
3543
+ this.observedWorktreePaths.add(normalizedWorktreePath);
3544
+ }
3545
+
3461
3546
  const sessionKey = resolveSessionWatcherKey(session);
3462
3547
  liveSessionKeys.add(sessionKey);
3463
3548
  if (this.sessionWatchers.has(sessionKey)) {
@@ -3468,8 +3553,20 @@ class ActiveAgentsRefreshController {
3468
3553
  resolveSessionGitIndexPath(session.worktreePath),
3469
3554
  );
3470
3555
  const disposables = bindRefreshWatcher(watcher, () => this.scheduleRefresh());
3471
- this.sessionWatchers.set(sessionKey, { watcher, disposables });
3556
+ this.sessionWatchers.set(sessionKey, {
3557
+ watcher,
3558
+ disposables,
3559
+ worktreePath: normalizedWorktreePath,
3560
+ });
3561
+ }
3562
+ }
3563
+
3564
+ for (const observedWorktreePath of this.observedWorktreePaths) {
3565
+ if (fs.existsSync(observedWorktreePath)) {
3566
+ this.closedMissingWorktreeRepositories.delete(observedWorktreePath);
3567
+ continue;
3472
3568
  }
3569
+ await this.closeMissingWorktreeRepository(observedWorktreePath);
3473
3570
  }
3474
3571
 
3475
3572
  for (const [sessionKey, entry] of this.sessionWatchers) {
@@ -3477,12 +3574,25 @@ class ActiveAgentsRefreshController {
3477
3574
  continue;
3478
3575
  }
3479
3576
 
3577
+ if (entry.worktreePath && !fs.existsSync(entry.worktreePath)) {
3578
+ await this.closeMissingWorktreeRepository(entry.worktreePath);
3579
+ }
3480
3580
  disposeAll(entry.disposables);
3481
3581
  entry.watcher.dispose();
3482
3582
  this.sessionWatchers.delete(sessionKey);
3483
3583
  }
3484
3584
  }
3485
3585
 
3586
+ async closeMissingWorktreeRepository(worktreePath) {
3587
+ const normalizedWorktreePath = normalizeAbsolutePath(worktreePath);
3588
+ if (!normalizedWorktreePath || this.closedMissingWorktreeRepositories.has(normalizedWorktreePath)) {
3589
+ return;
3590
+ }
3591
+
3592
+ this.closedMissingWorktreeRepositories.add(normalizedWorktreePath);
3593
+ await closeDeletedWorktreeRepository(normalizedWorktreePath);
3594
+ }
3595
+
3486
3596
  dispose() {
3487
3597
  if (this.refreshTimer) {
3488
3598
  clearTimeout(this.refreshTimer);
@@ -3,7 +3,7 @@
3
3
  "displayName": "GitGuardex Active Agents",
4
4
  "description": "Shows live Guardex sandbox sessions and repo changes in a dedicated VS Code Active Agents sidebar.",
5
5
  "publisher": "Recodee",
6
- "version": "0.0.20",
6
+ "version": "0.0.21",
7
7
  "license": "MIT",
8
8
  "icon": "icon.png",
9
9
  "engines": {