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,24 @@
# agent-codex-self-heal-repo-scan-ignores-2026-04-23-12-12 (minimal / T1)

Branch: `agent/codex/self-heal-repo-scan-ignores-2026-04-23-12-12`

Older repos can keep stale `.vscode/settings.json` values for `git.repositoryScanIgnoredFolders` until operators rerun `gx setup` or `gx doctor`. The shipped `Active Agents` extension should self-heal that workspace setting on activation and whenever workspace folders change so nested `.omx/.omc` helper worktrees stop leaking back into the default VS Code repo scan.

Scope:
- Update `vscode/guardex-active-agents/extension.js` to merge the managed repo-scan ignore folders into live workspace Git settings during activation and workspace-folder changes, while tolerating read-only settings.
- Mirror the same change into `templates/vscode/guardex-active-agents/extension.js` so shipped and template sources stay in sync.
- Add one focused regression in `test/vscode-active-agents-session-state.test.js` that proves activation/workspace-folder self-healing preserves existing user entries and avoids duplicate managed paths.

Verification:
- `node --test test/vscode-active-agents-session-state.test.js test/metadata.test.js`
- `openspec validate --specs`

## Handoff

- Handoff: change=`agent-codex-self-heal-repo-scan-ignores-2026-04-23-12-12`; branch=`agent/codex/self-heal-repo-scan-ignores-2026-04-23-12-12`; scope=`vscode/guardex-active-agents/extension.js, templates/vscode/guardex-active-agents/extension.js, test/vscode-active-agents-session-state.test.js, openspec/changes/agent-codex-self-heal-repo-scan-ignores-2026-04-23-12-12/notes.md`; action=`self-heal managed repo-scan ignores from the Active Agents extension, verify with focused node tests plus openspec validation, then finish via PR merge + cleanup`.

## Cleanup

- [ ] Run: `gx branch finish --branch agent/codex/self-heal-repo-scan-ignores-2026-04-23-12-12 --base main --via-pr --wait-for-merge --cleanup`
- [ ] Record PR URL + `MERGED` state in the completion handoff.
- [ ] Confirm sandbox worktree is gone (`git worktree list`, `git branch -a`).
86 changes: 85 additions & 1 deletion templates/vscode/guardex-active-agents/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ const RELOAD_WINDOW_ACTION = 'Reload Window';
const UPDATE_LATER_ACTION = 'Later';
const REFRESH_POLL_INTERVAL_MS = 30_000;
const INSPECT_PANEL_VIEW_TYPE = 'gitguardex.activeAgents.inspect';
const GIT_CONFIGURATION_SECTION = 'git';
const REPO_SCAN_IGNORED_FOLDERS_SETTING = 'repositoryScanIgnoredFolders';
const MANAGED_REPO_SCAN_IGNORED_FOLDERS = [
'.omx/agent-worktrees',
'**/.omx/agent-worktrees',
'.omx/.tmp-worktrees',
'**/.omx/.tmp-worktrees',
'.omc/agent-worktrees',
'**/.omc/agent-worktrees',
'.omc/.tmp-worktrees',
'**/.omc/.tmp-worktrees',
];
const SESSION_ACTIVITY_GROUPS = [
{ kind: 'blocked', label: 'BLOCKED' },
{ kind: 'working', label: 'WORKING NOW' },
Expand Down Expand Up @@ -105,6 +117,73 @@ function formatCountLabel(count, singular, plural = `${singular}s`) {
return `${count} ${count === 1 ? singular : plural}`;
}

function uniqueStringList(values) {
const seen = new Set();
const result = [];

for (const value of values) {
if (typeof value !== 'string' || seen.has(value)) {
continue;
}
seen.add(value);
result.push(value);
}

return result;
}

function stringListsEqual(left, right) {
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
return false;
}

return left.every((value, index) => value === right[index]);
}

async function ensureManagedRepoScanIgnores() {
if (typeof vscode.workspace.getConfiguration !== 'function') {
return;
}

const workspaceFolders = vscode.workspace.workspaceFolders || [];
if (workspaceFolders.length === 0) {
return;
}

const workspaceFolderTarget = workspaceFolders.length > 1
? vscode.ConfigurationTarget?.WorkspaceFolder
: vscode.ConfigurationTarget?.Workspace;
if (workspaceFolderTarget === undefined) {
return;
}

for (const workspaceFolder of workspaceFolders) {
const gitConfig = vscode.workspace.getConfiguration(GIT_CONFIGURATION_SECTION, workspaceFolder);
const configuredIgnoredFolders = gitConfig.get(REPO_SCAN_IGNORED_FOLDERS_SETTING);
const existingIgnoredFolders = Array.isArray(configuredIgnoredFolders)
? configuredIgnoredFolders
: [];
const nextIgnoredFolders = uniqueStringList([
...existingIgnoredFolders,
...MANAGED_REPO_SCAN_IGNORED_FOLDERS,
]);

if (stringListsEqual(existingIgnoredFolders, nextIgnoredFolders)) {
continue;
}

try {
await gitConfig.update(
REPO_SCAN_IGNORED_FOLDERS_SETTING,
nextIgnoredFolders,
workspaceFolderTarget,
);
} catch {
// Leave the extension usable even when the current workspace settings cannot be updated.
}
}
}

function sessionIdentityLabel(session) {
const agentName = typeof session?.agentName === 'string' ? session.agentName.trim() : '';
const taskName = typeof session?.taskName === 'string' ? session.taskName.trim() : '';
Expand Down Expand Up @@ -1777,6 +1856,10 @@ function activate(context) {
activeAgentsStatusItem.command = 'gitguardex.activeAgents.focus';
provider.attachTreeView(treeView);
const scheduleRefresh = () => refreshController.scheduleRefresh();
const handleWorkspaceFoldersChanged = () => {
scheduleRefresh();
void ensureManagedRepoScanIgnores();
};
const refresh = () => void refreshController.refreshNow();
const activeSessionsWatcher = vscode.workspace.createFileSystemWatcher(ACTIVE_SESSION_FILES_GLOB);
const lockWatcher = vscode.workspace.createFileSystemWatcher(AGENT_FILE_LOCKS_GLOB);
Expand Down Expand Up @@ -1902,7 +1985,7 @@ function activate(context) {
vscode.commands.registerCommand('gitguardex.activeAgents.syncSession', syncSession),
vscode.commands.registerCommand('gitguardex.activeAgents.stopSession', (session) => stopSession(session, refresh)),
vscode.commands.registerCommand('gitguardex.activeAgents.openSessionDiff', openSessionDiff),
vscode.workspace.onDidChangeWorkspaceFolders(scheduleRefresh),
vscode.workspace.onDidChangeWorkspaceFolders(handleWorkspaceFoldersChanged),
activeSessionsWatcher,
lockWatcher,
worktreeLockWatcher,
Expand All @@ -1916,6 +1999,7 @@ function activate(context) {
...bindRefreshWatcher(worktreeLockWatcher, scheduleRefresh),
...bindRefreshWatcher(logWatcher, scheduleRefresh),
);
void ensureManagedRepoScanIgnores();
void refreshController.refreshNow();
void maybeAutoUpdateActiveAgentsExtension(context);
}
Expand Down
138 changes: 138 additions & 0 deletions test/vscode-active-agents-session-state.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ function createMockVscode(tempRoot) {
fileWatchers: [],
watchers: [],
workspaceFolderListeners: [],
configurationUpdates: [],
workspaceConfigurationValues: new Map(),
};

class TreeItem {
Expand Down Expand Up @@ -293,6 +295,29 @@ function createMockVscode(tempRoot) {
}

const disposable = (onDispose) => ({ dispose: onDispose || (() => {}) });
const ConfigurationTarget = {
Workspace: 'workspace',
WorkspaceFolder: 'workspaceFolder',
};
const configurationKey = (section, scopePath, key) => `${section}::${scopePath}::${key}`;
const resolveWorkspaceScopePath = (scope) => scope?.uri?.fsPath || tempRoot;
const readConfigurationValue = (section, scope, key) => {
const scopePath = resolveWorkspaceScopePath(scope);
const scopedKey = configurationKey(section, scopePath, key);
if (registrations.workspaceConfigurationValues.has(scopedKey)) {
return registrations.workspaceConfigurationValues.get(scopedKey);
}
return registrations.workspaceConfigurationValues.get(configurationKey(section, tempRoot, key));
};
const writeConfigurationValue = (section, scopePath, key, value) => {
registrations.workspaceConfigurationValues.set(configurationKey(section, scopePath, key), value);
};
registrations.getConfigurationValue = (section, scopePath, key) => (
registrations.workspaceConfigurationValues.get(configurationKey(section, scopePath, key))
);
registrations.setConfigurationValue = (section, scopePath, key, value) => {
writeConfigurationValue(section, scopePath, key, value);
};

function createFileWatcher(pattern) {
const listeners = {
Expand Down Expand Up @@ -553,6 +578,16 @@ function createMockVscode(tempRoot) {
},
createFileSystemWatcher: (pattern) => createFileWatcher(pattern),
findFiles: async () => [],
getConfiguration: (section, scope) => ({
get: (key) => readConfigurationValue(section, scope, key),
update: async (key, value, target) => {
const scopePath = target === ConfigurationTarget.WorkspaceFolder
? resolveWorkspaceScopePath(scope)
: tempRoot;
registrations.configurationUpdates.push({ section, key, scopePath, target, value });
writeConfigurationValue(section, scopePath, key, value);
},
}),
onDidChangeWorkspaceFolders: (listener) => {
registrations.workspaceFolderListeners.push(listener);
return disposable(() => {
Expand All @@ -564,6 +599,7 @@ function createMockVscode(tempRoot) {
},
workspaceFolders: [{ uri: { fsPath: tempRoot } }],
},
ConfigurationTarget,
ThemeColor,
},
};
Expand Down Expand Up @@ -1313,6 +1349,108 @@ test('active-agents extension registers tree and decoration providers', async ()
}
});

test('active-agents extension self-heals managed repo-scan ignores on activation and workspace changes', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-scan-ignores-'));
const secondRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-scan-ignores-second-'));
const { registrations, vscode } = createMockVscode(tempRoot);
const extension = loadExtensionWithMockVscode(vscode);
const context = { subscriptions: [] };
const managedRepoScanIgnoredFolders = [
'.omx/agent-worktrees',
'**/.omx/agent-worktrees',
'.omx/.tmp-worktrees',
'**/.omx/.tmp-worktrees',
'.omc/agent-worktrees',
'**/.omc/agent-worktrees',
'.omc/.tmp-worktrees',
'**/.omc/.tmp-worktrees',
];
const mergeManagedRepoScanIgnores = (values) => Array.from(new Set([
...values,
...managedRepoScanIgnoredFolders,
]));

registrations.setConfigurationValue('git', tempRoot, 'repositoryScanIgnoredFolders', [
'custom-ignore',
'.omx/agent-worktrees',
'.omx/agent-worktrees',
]);

extension.activate(context);
await flushAsyncWork();

assert.deepEqual(
registrations.getConfigurationValue('git', tempRoot, 'repositoryScanIgnoredFolders'),
mergeManagedRepoScanIgnores([
'custom-ignore',
'.omx/agent-worktrees',
'.omx/agent-worktrees',
]),
);
assert.deepEqual(registrations.configurationUpdates, [
{
section: 'git',
key: 'repositoryScanIgnoredFolders',
scopePath: tempRoot,
target: vscode.ConfigurationTarget.Workspace,
value: mergeManagedRepoScanIgnores([
'custom-ignore',
'.omx/agent-worktrees',
'.omx/agent-worktrees',
]),
},
]);

registrations.setConfigurationValue('git', secondRoot, 'repositoryScanIgnoredFolders', [
'second-ignore',
'.omc/agent-worktrees',
]);
vscode.workspace.workspaceFolders = [
{ uri: { fsPath: tempRoot } },
{ uri: { fsPath: secondRoot } },
];
registrations.workspaceFolderListeners[0]({
added: [{ uri: { fsPath: secondRoot } }],
removed: [],
});
await flushAsyncWork();

assert.deepEqual(
registrations.getConfigurationValue('git', secondRoot, 'repositoryScanIgnoredFolders'),
mergeManagedRepoScanIgnores([
'second-ignore',
'.omc/agent-worktrees',
]),
);
assert.deepEqual(registrations.configurationUpdates, [
{
section: 'git',
key: 'repositoryScanIgnoredFolders',
scopePath: tempRoot,
target: vscode.ConfigurationTarget.Workspace,
value: mergeManagedRepoScanIgnores([
'custom-ignore',
'.omx/agent-worktrees',
'.omx/agent-worktrees',
]),
},
{
section: 'git',
key: 'repositoryScanIgnoredFolders',
scopePath: secondRoot,
target: vscode.ConfigurationTarget.WorkspaceFolder,
value: mergeManagedRepoScanIgnores([
'second-ignore',
'.omc/agent-worktrees',
]),
},
]);

for (const subscription of context.subscriptions) {
subscription.dispose?.();
}
});

test('active-agents extension startAgent command prefers the Guardex launcher in a terminal', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-start-agent-'));
fs.mkdirSync(path.join(tempRoot, 'scripts'), { recursive: true });
Expand Down
Loading