@imdeadpool/guardex 7.0.36 → 7.0.37

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,14 @@ 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.37
270
+ - Bumped `@imdeadpool/guardex` from `7.0.36` to `7.0.37` so the current
271
+ package can publish under a fresh npm version after `7.0.36` reached the
272
+ registry.
273
+ - Synced the shipped Active Agents template with the canonical VS Code
274
+ extension source so Colony task counts and details install with the package.
275
+ - No new CLI command behavior is introduced in this release lane.
276
+
269
277
  ### v7.0.36
270
278
  - Bumped `@imdeadpool/guardex` from `7.0.35` to `7.0.36` so the latest
271
279
  branch-finish cwd-prune fix can ship under a fresh npm version after PR #424.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imdeadpool/guardex",
3
- "version": "7.0.36",
3
+ "version": "7.0.37",
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,
@@ -165,7 +165,11 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
165
165
  fi
166
166
 
167
167
  repo_root="$(git rev-parse --show-toplevel)"
168
- current_worktree="$(pwd -P)"
168
+ # The physical cwd may be a subdirectory inside the source worktree. Cleanup
169
+ # decisions need the enclosing worktree root, otherwise finishing from `src/`
170
+ # can delete the caller's cwd and turn a successful merge into a false shell
171
+ # failure.
172
+ current_worktree="$repo_root"
169
173
  common_git_dir_raw="$(git -C "$repo_root" rev-parse --git-common-dir)"
170
174
  if [[ "$common_git_dir_raw" == /* ]]; then
171
175
  common_git_dir="$common_git_dir_raw"
@@ -223,7 +227,68 @@ if [[ "$SOURCE_BRANCH" == "$BASE_BRANCH" ]]; then
223
227
  exit 1
224
228
  fi
225
229
 
230
+ cleanup_missing_merged_source_branch() {
231
+ local state_line=""
232
+ local parsed_state=""
233
+ local parsed_merged_at=""
234
+ local parsed_url=""
235
+ local remote_delete_output=""
236
+ local prune_args=()
237
+
238
+ if [[ "$MERGE_MODE" != "pr" || "$CLEANUP_AFTER_MERGE" -ne 1 ]]; then
239
+ return 1
240
+ fi
241
+ if ! command -v "$GH_BIN" >/dev/null 2>&1; then
242
+ return 1
243
+ fi
244
+
245
+ state_line="$("$GH_BIN" pr list \
246
+ --state merged \
247
+ --head "$SOURCE_BRANCH" \
248
+ --base "$BASE_BRANCH" \
249
+ --json state,mergedAt,url \
250
+ --jq 'sort_by(.mergedAt // "") | reverse | (.[0] // {}) | [(.state // ""), (.mergedAt // ""), (.url // "")] | join("\u001f")' \
251
+ 2>/dev/null || true)"
252
+ if [[ -z "$state_line" ]]; then
253
+ return 1
254
+ fi
255
+
256
+ IFS=$'\x1f' read -r parsed_state parsed_merged_at parsed_url <<< "$state_line"
257
+ if [[ "$parsed_state" != "MERGED" && -z "$parsed_merged_at" ]]; then
258
+ return 1
259
+ fi
260
+
261
+ echo "[agent-branch-finish] Local source branch '${SOURCE_BRANCH}' is already absent, but a merged PR exists; continuing cleanup." >&2
262
+ if [[ -n "$parsed_url" ]]; then
263
+ echo "[agent-branch-finish] Merged PR: ${parsed_url}" >&2
264
+ fi
265
+
266
+ run_guardex_cli locks release --branch "$SOURCE_BRANCH" >/dev/null 2>&1 || true
267
+
268
+ if [[ "$PUSH_ENABLED" -eq 1 && "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then
269
+ if git -C "$repo_root" ls-remote --exit-code --heads origin "$SOURCE_BRANCH" >/dev/null 2>&1; then
270
+ if ! remote_delete_output="$(git -C "$repo_root" push origin --delete "$SOURCE_BRANCH" 2>&1)"; then
271
+ echo "[agent-branch-finish] Warning: remote branch cleanup failed for '${SOURCE_BRANCH}'." >&2
272
+ [[ -n "$remote_delete_output" ]] && echo "$remote_delete_output" >&2
273
+ fi
274
+ fi
275
+ fi
276
+
277
+ prune_args=(worktree prune --base "$BASE_BRANCH" --only-dirty-worktrees --delete-branches)
278
+ if [[ "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then
279
+ prune_args+=(--delete-remote-branches)
280
+ fi
281
+ if ! run_guardex_cli "${prune_args[@]}"; then
282
+ echo "[agent-branch-finish] Warning: automatic worktree prune failed." >&2
283
+ echo "[agent-branch-finish] You can run manual cleanup: gx cleanup --base ${BASE_BRANCH}" >&2
284
+ fi
285
+
286
+ echo "[agent-branch-finish] Merged '${SOURCE_BRANCH}' into '${BASE_BRANCH}' via pr flow and found source branch/worktree already cleaned."
287
+ exit 0
288
+ }
289
+
226
290
  if ! git -C "$repo_root" show-ref --verify --quiet "refs/heads/${SOURCE_BRANCH}"; then
291
+ cleanup_missing_merged_source_branch
227
292
  echo "[agent-branch-finish] Local source branch does not exist: ${SOURCE_BRANCH}" >&2
228
293
  exit 1
229
294
  fi
@@ -846,10 +911,12 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
846
911
  echo "[agent-branch-finish] You can run manual cleanup: gx cleanup --base ${BASE_BRANCH}" >&2
847
912
  fi
848
913
 
849
- echo "[agent-branch-finish] Merged '${SOURCE_BRANCH}' into '${BASE_BRANCH}' via ${merge_status} flow and cleaned source branch/worktree."
850
914
  if [[ "$source_worktree" == "$current_worktree" && "$source_worktree" == "${agent_worktree_root}"/* && -d "$source_worktree" ]]; then
915
+ echo "[agent-branch-finish] Merged '${SOURCE_BRANCH}' into '${BASE_BRANCH}' via ${merge_status} flow and cleaned source branch/remote."
851
916
  echo "[agent-branch-finish] Current worktree '${source_worktree}' still exists because it is the active shell cwd." >&2
852
917
  echo "[agent-branch-finish] Leave this directory, then run: gx cleanup --base ${BASE_BRANCH}" >&2
918
+ else
919
+ echo "[agent-branch-finish] Merged '${SOURCE_BRANCH}' into '${BASE_BRANCH}' via ${merge_status} flow and cleaned source branch/worktree."
853
920
  fi
854
921
  else
855
922
  pivot_to_repo_root_before_prune
@@ -1,6 +1,8 @@
1
1
  const fs = require('node:fs');
2
2
  const path = require('node:path');
3
3
  const cp = require('node:child_process');
4
+ const http = require('node:http');
5
+ const os = require('node:os');
4
6
  const vscode = require('vscode');
5
7
  const {
6
8
  clearWorktreeActivityCache,
@@ -40,6 +42,84 @@ const ACTIVE_AGENTS_EXTENSION_ID = 'Recodee.gitguardex-active-agents';
40
42
  const RESTART_EXTENSION_HOST_COMMAND = 'workbench.action.restartExtensionHost';
41
43
  const REFRESH_POLL_INTERVAL_MS = 30_000;
42
44
  const INSPECT_PANEL_VIEW_TYPE = 'gitguardex.activeAgents.inspect';
45
+ const COLONY_DEFAULT_PORT = 37777;
46
+ const COLONY_SNAPSHOT_TTL_MS = 5_000;
47
+ const COLONY_FETCH_TIMEOUT_MS = 800;
48
+
49
+ function colonyDataDir() {
50
+ return process.env.COLONY_HOME
51
+ || process.env.CAVEMEM_HOME
52
+ || path.join(os.homedir(), '.colony');
53
+ }
54
+
55
+ function readColonyPort() {
56
+ try {
57
+ const raw = fs.readFileSync(path.join(colonyDataDir(), 'settings.json'), 'utf8');
58
+ const parsed = JSON.parse(raw);
59
+ const port = Number(parsed?.workerPort);
60
+ return Number.isFinite(port) && port > 0 ? port : COLONY_DEFAULT_PORT;
61
+ } catch (_error) {
62
+ return COLONY_DEFAULT_PORT;
63
+ }
64
+ }
65
+
66
+ function fetchColonyJson(urlPath) {
67
+ return new Promise((resolve) => {
68
+ const req = http.get(
69
+ {
70
+ hostname: '127.0.0.1',
71
+ port: readColonyPort(),
72
+ path: urlPath,
73
+ timeout: COLONY_FETCH_TIMEOUT_MS,
74
+ },
75
+ (res) => {
76
+ if (res.statusCode !== 200) {
77
+ res.resume();
78
+ resolve(null);
79
+ return;
80
+ }
81
+ let body = '';
82
+ res.setEncoding('utf8');
83
+ res.on('data', (chunk) => {
84
+ body += chunk;
85
+ });
86
+ res.on('end', () => {
87
+ try {
88
+ resolve(JSON.parse(body));
89
+ } catch (_error) {
90
+ resolve(null);
91
+ }
92
+ });
93
+ },
94
+ );
95
+ req.on('error', () => resolve(null));
96
+ req.on('timeout', () => {
97
+ req.destroy();
98
+ resolve(null);
99
+ });
100
+ });
101
+ }
102
+
103
+ const colonyTasksCache = new Map();
104
+
105
+ async function readColonyTasksForRepo(repoRoot) {
106
+ const cached = colonyTasksCache.get(repoRoot);
107
+ if (cached && Date.now() - cached.at < COLONY_SNAPSHOT_TTL_MS) {
108
+ return cached.tasks;
109
+ }
110
+ const tasks = await fetchColonyJson(
111
+ `/api/colony/tasks?repo_root=${encodeURIComponent(repoRoot)}`,
112
+ );
113
+ const resolved = Array.isArray(tasks) ? tasks : [];
114
+ colonyTasksCache.set(repoRoot, { at: Date.now(), tasks: resolved });
115
+ return resolved;
116
+ }
117
+
118
+ function compactColonyBranchLabel(branch) {
119
+ if (typeof branch !== 'string' || !branch) return 'unknown';
120
+ const parts = branch.split('/').filter(Boolean);
121
+ return parts.length > 2 ? parts.slice(-2).join('/') : branch;
122
+ }
43
123
  const GIT_CONFIGURATION_SECTION = 'git';
44
124
  const REPO_SCAN_IGNORED_FOLDERS_SETTING = 'repositoryScanIgnoredFolders';
45
125
  const BUNDLED_FILE_ICONS_MANIFEST_RELATIVE = path.join('fileicons', 'gitguardex-fileicons.json');
@@ -840,10 +920,18 @@ function buildOverviewDescription(summary) {
840
920
  formatCountLabel(summary?.workingCount || 0, 'working agent'),
841
921
  formatCountLabel(summary?.finishedCount || 0, 'finished agent'),
842
922
  formatCountLabel(summary?.idleCount || 0, 'idle agent'),
923
+ summary?.colonyTaskCount
924
+ ? formatCountLabel(summary.colonyTaskCount, 'colony task')
925
+ : '',
926
+ summary?.pendingHandoffCount
927
+ ? formatCountLabel(summary.pendingHandoffCount, 'pending handoff')
928
+ : '',
843
929
  formatCountLabel(summary?.unassignedChangeCount || 0, 'unassigned change'),
844
930
  formatCountLabel(summary?.lockedFileCount || 0, 'locked file'),
845
931
  formatCountLabel(summary?.conflictCount || 0, 'conflict'),
846
- ].join(' · ');
932
+ ]
933
+ .filter(Boolean)
934
+ .join(' · ');
847
935
  }
848
936
 
849
937
  function buildRepoDescription(summary) {
@@ -1252,7 +1340,9 @@ class RepoItem extends vscode.TreeItem {
1252
1340
  this.changes = changes;
1253
1341
  this.unassignedChanges = options.unassignedChanges || [];
1254
1342
  this.lockEntries = options.lockEntries || [];
1255
- this.overview = options.overview || buildRepoOverview(sessions, this.unassignedChanges, this.lockEntries);
1343
+ this.colonyTasks = Array.isArray(options.colonyTasks) ? options.colonyTasks : [];
1344
+ this.overview = options.overview
1345
+ || buildRepoOverview(sessions, this.unassignedChanges, this.lockEntries, this.colonyTasks);
1256
1346
  this.description = buildRepoDescription(this.overview);
1257
1347
  this.tooltip = buildRepoTooltip(repoRoot, this.overview);
1258
1348
  this.iconPath = themeIcon('repo');
@@ -2524,7 +2614,8 @@ function countChangedPaths(repoRoot, sessions, changes) {
2524
2614
  return changedKeys.size;
2525
2615
  }
2526
2616
 
2527
- function buildRepoOverview(sessions, unassignedChanges, lockEntries) {
2617
+ function buildRepoOverview(sessions, unassignedChanges, lockEntries, colonyTasks = []) {
2618
+ const colonyTaskList = Array.isArray(colonyTasks) ? colonyTasks : [];
2528
2619
  return {
2529
2620
  sessionCount: sessions.length,
2530
2621
  workingCount: countWorkingSessions(sessions),
@@ -2536,6 +2627,11 @@ function buildRepoOverview(sessions, unassignedChanges, lockEntries) {
2536
2627
  (total, session) => total + (session.conflictCount || 0),
2537
2628
  0,
2538
2629
  ) + (unassignedChanges || []).filter((change) => change.hasForeignLock).length,
2630
+ colonyTaskCount: colonyTaskList.length,
2631
+ pendingHandoffCount: colonyTaskList.reduce(
2632
+ (total, task) => total + (task.pending_handoff_count || 0),
2633
+ 0,
2634
+ ),
2539
2635
  };
2540
2636
  }
2541
2637
 
@@ -3023,12 +3119,14 @@ class ActiveAgentsProvider {
3023
3119
 
3024
3120
  const { repoRootChanges } = partitionChangesByOwnership(sessions, changes);
3025
3121
  const unassignedChanges = sortUnassignedChanges(repoRootChanges);
3122
+ const colonyTasks = Array.isArray(entry.colonyTasks) ? entry.colonyTasks : [];
3026
3123
  return {
3027
3124
  ...entry,
3028
3125
  sessions,
3029
3126
  changes,
3030
3127
  unassignedChanges,
3031
- overview: buildRepoOverview(sessions, unassignedChanges, entry.lockEntries),
3128
+ colonyTasks,
3129
+ overview: buildRepoOverview(sessions, unassignedChanges, entry.lockEntries, colonyTasks),
3032
3130
  };
3033
3131
  });
3034
3132
 
@@ -3140,6 +3238,37 @@ class ActiveAgentsProvider {
3140
3238
  iconId: 'file-directory',
3141
3239
  }));
3142
3240
  }
3241
+ const colonyTaskList = Array.isArray(element.colonyTasks) ? element.colonyTasks : [];
3242
+ if (colonyTaskList.length > 0) {
3243
+ const colonyItems = colonyTaskList.map((task) => {
3244
+ const pendingLabel = task.pending_handoff_count > 0
3245
+ ? formatCountLabel(task.pending_handoff_count, 'pending handoff')
3246
+ : 'quiet';
3247
+ const participantLabel =
3248
+ (task.participants || []).map((p) => p.agent).filter(Boolean).join(', ')
3249
+ || 'no participants';
3250
+ return new DetailItem(
3251
+ `#${task.id} · ${compactColonyBranchLabel(task.branch)}`,
3252
+ `${participantLabel} · ${pendingLabel}`,
3253
+ {
3254
+ iconId: task.pending_handoff_count > 0 ? 'warning' : 'comment-discussion',
3255
+ tooltip: [
3256
+ task.branch,
3257
+ `task #${task.id}`,
3258
+ participantLabel,
3259
+ task.pending_handoff_count > 0
3260
+ ? formatCountLabel(task.pending_handoff_count, 'pending handoff')
3261
+ : '',
3262
+ ].filter(Boolean).join('\n'),
3263
+ },
3264
+ );
3265
+ });
3266
+ advancedItems.push(new SectionItem('Colony tasks', colonyItems, {
3267
+ description: String(colonyItems.length),
3268
+ collapsedState: vscode.TreeItemCollapsibleState.Collapsed,
3269
+ iconId: 'organization',
3270
+ }));
3271
+ }
3143
3272
  if (advancedItems.length > 0) {
3144
3273
  sectionItems.push(new SectionItem('Advanced details', advancedItems, {
3145
3274
  description: String(advancedItems.length),
@@ -3166,24 +3295,29 @@ class ActiveAgentsProvider {
3166
3295
  overview: entry.overview,
3167
3296
  unassignedChanges: entry.unassignedChanges,
3168
3297
  lockEntries: entry.lockEntries,
3298
+ colonyTasks: entry.colonyTasks,
3169
3299
  }));
3170
3300
  }
3171
3301
 
3172
3302
  async loadRepoEntries() {
3173
3303
  const repoEntries = await findRepoSessionEntries();
3174
- return repoEntries.map((entry) => {
3175
- const repoRoot = entry.repoRoot;
3176
- const lockRegistry = this.getLockRegistryForRepo(repoRoot);
3177
- const currentBranch = readCurrentBranch(repoRoot);
3178
- return {
3179
- repoRoot,
3180
- sessions: entry.sessions.map((session) => decorateSession(session, lockRegistry)),
3181
- changes: readRepoChanges(repoRoot).map((change) => (
3182
- decorateChange(change, lockRegistry, currentBranch)
3183
- )),
3184
- lockEntries: Array.from(lockRegistry.entriesByPath.entries()),
3185
- };
3186
- });
3304
+ return Promise.all(
3305
+ repoEntries.map(async (entry) => {
3306
+ const repoRoot = entry.repoRoot;
3307
+ const lockRegistry = this.getLockRegistryForRepo(repoRoot);
3308
+ const currentBranch = readCurrentBranch(repoRoot);
3309
+ const colonyTasks = await readColonyTasksForRepo(repoRoot);
3310
+ return {
3311
+ repoRoot,
3312
+ sessions: entry.sessions.map((session) => decorateSession(session, lockRegistry)),
3313
+ changes: readRepoChanges(repoRoot).map((change) => (
3314
+ decorateChange(change, lockRegistry, currentBranch)
3315
+ )),
3316
+ lockEntries: Array.from(lockRegistry.entriesByPath.entries()),
3317
+ colonyTasks,
3318
+ };
3319
+ }),
3320
+ );
3187
3321
  }
3188
3322
  }
3189
3323