Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
## Why

- The Guardex Active Agents tree in `vscode/guardex-active-agents/extension.js` surfaces session files + file locks, but has no window into colony task threads. Colony already tracks cross-agent tasks, participants, and pending handoffs in `~/.colony/data.db`; the extension currently shows none of that.
- Agents working the same repo with colony MCP tooling split-brain into two UIs: colony viewer at `http://127.0.0.1:37777` and the Active Agents tree in VS Code. Handoffs that need attention are only visible via the viewer.
- Reading colony's SQLite directly from the VS Code extension would pull in a native `better-sqlite3` dep and couple the extension to colony's schema. The colony worker already exposes an HTTP API on `127.0.0.1`; adding thin read-only endpoints keeps the extension dependency-free.

## What Changes

- `vscode/guardex-active-agents/extension.js`:
- Add `node:http` + `node:os` imports and a colony read helper with 5s per-repo cache and 800ms fetch timeout. Silent fallback to empty results when the worker is off.
- Port resolution reads `~/.colony/settings.json#workerPort` (honours `COLONY_HOME` / `CAVEMEM_HOME`), falls back to `37777`.
- Extend `buildRepoOverview` + `buildOverviewDescription` with `colonyTaskCount` + `pendingHandoffCount`.
- Extend `RepoItem` to carry `colonyTasks`. In `getChildren(RepoItem)` add a collapsed `Colony tasks` section under `Advanced details` listing one `DetailItem` per task (label `#id · branch`, description `participants · pending handoffs|quiet`).
- `loadRepoEntries` fans out one colony fetch per repo in parallel with the existing decoration.
- No backend or settings changes in this repo; the three new `/api/colony/*` endpoints live in the colony worker (`agents-hivemind` repo) and are consumed here.

## Impact

- **New behavior**: When the colony worker is running, each repo card's summary line includes `N colony tasks · M pending handoffs` when non-zero; expanding `Advanced details` shows a `Colony tasks` list with a warning icon on tasks with pending handoffs.
- **Compat**: When the worker is down the fetch resolves null, `colonyTasks` is `[]`, and the tree renders exactly as today. No new dependencies added to the extension.
- **Surfaces touched**: `vscode/guardex-active-agents/extension.js`.
- **Out of scope**: Clicking into a task to drill into its `/api/colony/tasks/:id/attention` payload (follow-up).
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
## ADDED Requirements

### Requirement: Active Agents tree surfaces colony task counts per repo

When the local colony worker is reachable, the Active Agents tree in `vscode/guardex-active-agents/extension.js` SHALL enrich each repo's Overview summary with the count of colony tasks and the count of pending handoffs known to that repo, and SHALL NOT alter the tree when no tasks or handoffs are known.

#### Scenario: Repo with colony tasks includes counts in the Overview summary
- **GIVEN** the colony worker is listening on `127.0.0.1:<workerPort>` and `/api/colony/tasks?repo_root=<repoRoot>` returns at least one task with a `pending_handoff_count` > 0
- **WHEN** the Active Agents tree refreshes
- **THEN** the repo's Overview `Summary` description contains the segments `N colony task(s)` and `M pending handoff(s)` joined by ` · ` with the existing working/finished/idle/unassigned/locked/conflict segments.

#### Scenario: Worker unavailable falls back silently
- **GIVEN** no process is listening on the configured colony port, or the fetch times out within `COLONY_FETCH_TIMEOUT_MS`
- **WHEN** the Active Agents tree refreshes
- **THEN** the Overview description and tree structure are identical to the pre-change rendering and no error is surfaced to the user.

#### Scenario: Fetch is cached per repo root for a short TTL
- **GIVEN** the tree has fetched `/api/colony/tasks?repo_root=<repoRoot>` within the last `COLONY_SNAPSHOT_TTL_MS`
- **WHEN** the tree refreshes again for the same repo root
- **THEN** the extension reuses the cached response rather than issuing another HTTP request.

### Requirement: Advanced details expose a Colony tasks section

When at least one colony task is known for a repo, `getChildren(RepoItem)` SHALL append a collapsed `Colony tasks` section inside `Advanced details` listing one row per task.

#### Scenario: Task with pending handoff renders a warning row
- **GIVEN** a colony task with `pending_handoff_count` >= 1 and at least one participant
- **WHEN** the user expands `Advanced details` then `Colony tasks`
- **THEN** the task row's label is `#<id> · <compact branch label>`, its description is `<participant agents, comma-separated> · N pending handoff(s)`, and its icon id is `warning`.

#### Scenario: Task without pending handoffs renders a quiet row
- **GIVEN** a colony task with `pending_handoff_count` === 0
- **WHEN** the user expands `Colony tasks`
- **THEN** the task row's description ends in `quiet` and its icon id is `comment-discussion`.

### Requirement: Colony port is resolved from the colony data dir settings file

The extension SHALL resolve the colony worker port from `$COLONY_HOME/settings.json` (or `$CAVEMEM_HOME/settings.json`, or `~/.colony/settings.json` when neither env var is set), defaulting to `37777` when the file is absent, unreadable, or does not contain a positive `workerPort`.

#### Scenario: Settings file contains an explicit port
- **GIVEN** the resolved settings path contains `{"workerPort": 38000}`
- **WHEN** the extension issues a colony fetch
- **THEN** it connects to `127.0.0.1:38000`.

#### Scenario: Settings file is absent
- **GIVEN** no settings file exists at the resolved path
- **WHEN** the extension issues a colony fetch
- **THEN** it connects to `127.0.0.1:37777`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## 1. Specification

- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-claude-wire-colony-tasks-into-active-agents-vie-2026-04-24-18-21`.
- [x] 1.2 Define normative requirements in `specs/wire-colony-tasks-into-active-agents-view/spec.md`.

## 2. Implementation

- [x] 2.1 Add `node:http` + `node:os` imports and colony config helpers (`colonyDataDir`, `readColonyPort`, `fetchColonyJson`, `readColonyTasksForRepo`, `compactColonyBranchLabel`) to `vscode/guardex-active-agents/extension.js`.
- [x] 2.2 Thread `colonyTasks` through `buildRepoOverview`, `buildOverviewDescription`, `annotateRepoEntries`, `RepoItem`, and the `loadRepoEntries` fan-out in `vscode/guardex-active-agents/extension.js`.
- [x] 2.3 Render a collapsed `Colony tasks` section inside `Advanced details` in `getChildren(RepoItem)` with per-task `DetailItem`s showing participants and pending handoff count.

## 3. Verification

- [x] 3.1 `node --check vscode/guardex-active-agents/extension.js` exits 0.
- [ ] 3.2 Manual smoke: run extension against a repo with an active colony worker and confirm the colony task counts appear in the Overview description and the `Colony tasks` section lists tasks correctly.

## 4. Cleanup

- [ ] 4.1 Run `gx branch finish --branch agent/claude/wire-colony-tasks-into-active-agents-vie-2026-04-24-18-21 --base main --via-pr --wait-for-merge --cleanup`.
- [ ] 4.2 Record PR URL + `MERGED` state and confirm sandbox worktree removed.
168 changes: 151 additions & 17 deletions vscode/guardex-active-agents/extension.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const fs = require('node:fs');
const path = require('node:path');
const cp = require('node:child_process');
const http = require('node:http');
const os = require('node:os');
const vscode = require('vscode');
const {
clearWorktreeActivityCache,
Expand Down Expand Up @@ -40,6 +42,84 @@ const ACTIVE_AGENTS_EXTENSION_ID = 'Recodee.gitguardex-active-agents';
const RESTART_EXTENSION_HOST_COMMAND = 'workbench.action.restartExtensionHost';
const REFRESH_POLL_INTERVAL_MS = 30_000;
const INSPECT_PANEL_VIEW_TYPE = 'gitguardex.activeAgents.inspect';
const COLONY_DEFAULT_PORT = 37777;
const COLONY_SNAPSHOT_TTL_MS = 5_000;
const COLONY_FETCH_TIMEOUT_MS = 800;

function colonyDataDir() {
return process.env.COLONY_HOME
|| process.env.CAVEMEM_HOME
|| path.join(os.homedir(), '.colony');
}

function readColonyPort() {
try {
const raw = fs.readFileSync(path.join(colonyDataDir(), 'settings.json'), 'utf8');
const parsed = JSON.parse(raw);
const port = Number(parsed?.workerPort);
return Number.isFinite(port) && port > 0 ? port : COLONY_DEFAULT_PORT;
} catch (_error) {
return COLONY_DEFAULT_PORT;
}
}

function fetchColonyJson(urlPath) {
return new Promise((resolve) => {
const req = http.get(
{
hostname: '127.0.0.1',
port: readColonyPort(),
path: urlPath,
timeout: COLONY_FETCH_TIMEOUT_MS,
},
(res) => {
if (res.statusCode !== 200) {
res.resume();
resolve(null);
return;
}
let body = '';
res.setEncoding('utf8');
res.on('data', (chunk) => {
body += chunk;
});
res.on('end', () => {
try {
resolve(JSON.parse(body));
} catch (_error) {
resolve(null);
}
});
},
);
req.on('error', () => resolve(null));
req.on('timeout', () => {
req.destroy();
resolve(null);
});
});
}

const colonyTasksCache = new Map();

async function readColonyTasksForRepo(repoRoot) {
const cached = colonyTasksCache.get(repoRoot);
if (cached && Date.now() - cached.at < COLONY_SNAPSHOT_TTL_MS) {
return cached.tasks;
}
const tasks = await fetchColonyJson(
`/api/colony/tasks?repo_root=${encodeURIComponent(repoRoot)}`,
);
const resolved = Array.isArray(tasks) ? tasks : [];
colonyTasksCache.set(repoRoot, { at: Date.now(), tasks: resolved });
return resolved;
}

function compactColonyBranchLabel(branch) {
if (typeof branch !== 'string' || !branch) return 'unknown';
const parts = branch.split('/').filter(Boolean);
return parts.length > 2 ? parts.slice(-2).join('/') : branch;
}
const GIT_CONFIGURATION_SECTION = 'git';
const REPO_SCAN_IGNORED_FOLDERS_SETTING = 'repositoryScanIgnoredFolders';
const BUNDLED_FILE_ICONS_MANIFEST_RELATIVE = path.join('fileicons', 'gitguardex-fileicons.json');
Expand Down Expand Up @@ -840,10 +920,18 @@ function buildOverviewDescription(summary) {
formatCountLabel(summary?.workingCount || 0, 'working agent'),
formatCountLabel(summary?.finishedCount || 0, 'finished agent'),
formatCountLabel(summary?.idleCount || 0, 'idle agent'),
summary?.colonyTaskCount
? formatCountLabel(summary.colonyTaskCount, 'colony task')
: '',
summary?.pendingHandoffCount
? formatCountLabel(summary.pendingHandoffCount, 'pending handoff')
: '',
formatCountLabel(summary?.unassignedChangeCount || 0, 'unassigned change'),
formatCountLabel(summary?.lockedFileCount || 0, 'locked file'),
formatCountLabel(summary?.conflictCount || 0, 'conflict'),
].join(' · ');
]
.filter(Boolean)
.join(' · ');
}

function buildRepoDescription(summary) {
Expand Down Expand Up @@ -1252,7 +1340,9 @@ class RepoItem extends vscode.TreeItem {
this.changes = changes;
this.unassignedChanges = options.unassignedChanges || [];
this.lockEntries = options.lockEntries || [];
this.overview = options.overview || buildRepoOverview(sessions, this.unassignedChanges, this.lockEntries);
this.colonyTasks = Array.isArray(options.colonyTasks) ? options.colonyTasks : [];
this.overview = options.overview
|| buildRepoOverview(sessions, this.unassignedChanges, this.lockEntries, this.colonyTasks);
this.description = buildRepoDescription(this.overview);
this.tooltip = buildRepoTooltip(repoRoot, this.overview);
this.iconPath = themeIcon('repo');
Expand Down Expand Up @@ -2524,7 +2614,8 @@ function countChangedPaths(repoRoot, sessions, changes) {
return changedKeys.size;
}

function buildRepoOverview(sessions, unassignedChanges, lockEntries) {
function buildRepoOverview(sessions, unassignedChanges, lockEntries, colonyTasks = []) {
const colonyTaskList = Array.isArray(colonyTasks) ? colonyTasks : [];
return {
sessionCount: sessions.length,
workingCount: countWorkingSessions(sessions),
Expand All @@ -2536,6 +2627,11 @@ function buildRepoOverview(sessions, unassignedChanges, lockEntries) {
(total, session) => total + (session.conflictCount || 0),
0,
) + (unassignedChanges || []).filter((change) => change.hasForeignLock).length,
colonyTaskCount: colonyTaskList.length,
pendingHandoffCount: colonyTaskList.reduce(
(total, task) => total + (task.pending_handoff_count || 0),
0,
),
};
}

Expand Down Expand Up @@ -3023,12 +3119,14 @@ class ActiveAgentsProvider {

const { repoRootChanges } = partitionChangesByOwnership(sessions, changes);
const unassignedChanges = sortUnassignedChanges(repoRootChanges);
const colonyTasks = Array.isArray(entry.colonyTasks) ? entry.colonyTasks : [];
return {
...entry,
sessions,
changes,
unassignedChanges,
overview: buildRepoOverview(sessions, unassignedChanges, entry.lockEntries),
colonyTasks,
overview: buildRepoOverview(sessions, unassignedChanges, entry.lockEntries, colonyTasks),
};
});

Expand Down Expand Up @@ -3140,6 +3238,37 @@ class ActiveAgentsProvider {
iconId: 'file-directory',
}));
}
const colonyTaskList = Array.isArray(element.colonyTasks) ? element.colonyTasks : [];
if (colonyTaskList.length > 0) {
const colonyItems = colonyTaskList.map((task) => {
const pendingLabel = task.pending_handoff_count > 0
? formatCountLabel(task.pending_handoff_count, 'pending handoff')
: 'quiet';
const participantLabel =
(task.participants || []).map((p) => p.agent).filter(Boolean).join(', ')
|| 'no participants';
return new DetailItem(
`#${task.id} · ${compactColonyBranchLabel(task.branch)}`,
`${participantLabel} · ${pendingLabel}`,
{
iconId: task.pending_handoff_count > 0 ? 'warning' : 'comment-discussion',
tooltip: [
task.branch,
`task #${task.id}`,
participantLabel,
task.pending_handoff_count > 0
? formatCountLabel(task.pending_handoff_count, 'pending handoff')
: '',
].filter(Boolean).join('\n'),
},
);
});
advancedItems.push(new SectionItem('Colony tasks', colonyItems, {
description: String(colonyItems.length),
collapsedState: vscode.TreeItemCollapsibleState.Collapsed,
iconId: 'organization',
}));
}
if (advancedItems.length > 0) {
sectionItems.push(new SectionItem('Advanced details', advancedItems, {
description: String(advancedItems.length),
Expand All @@ -3166,24 +3295,29 @@ class ActiveAgentsProvider {
overview: entry.overview,
unassignedChanges: entry.unassignedChanges,
lockEntries: entry.lockEntries,
colonyTasks: entry.colonyTasks,
}));
}

async loadRepoEntries() {
const repoEntries = await findRepoSessionEntries();
return repoEntries.map((entry) => {
const repoRoot = entry.repoRoot;
const lockRegistry = this.getLockRegistryForRepo(repoRoot);
const currentBranch = readCurrentBranch(repoRoot);
return {
repoRoot,
sessions: entry.sessions.map((session) => decorateSession(session, lockRegistry)),
changes: readRepoChanges(repoRoot).map((change) => (
decorateChange(change, lockRegistry, currentBranch)
)),
lockEntries: Array.from(lockRegistry.entriesByPath.entries()),
};
});
return Promise.all(
repoEntries.map(async (entry) => {
const repoRoot = entry.repoRoot;
const lockRegistry = this.getLockRegistryForRepo(repoRoot);
const currentBranch = readCurrentBranch(repoRoot);
const colonyTasks = await readColonyTasksForRepo(repoRoot);
return {
repoRoot,
sessions: entry.sessions.map((session) => decorateSession(session, lockRegistry)),
changes: readRepoChanges(repoRoot).map((change) => (
decorateChange(change, lockRegistry, currentBranch)
)),
lockEntries: Array.from(lockRegistry.entriesByPath.entries()),
colonyTasks,
};
}),
);
}
}

Expand Down