diff --git a/openspec/changes/agent-codex-vscode-extension-version-bump-guard-2026-04-22-16-18/.openspec.yaml b/openspec/changes/agent-codex-vscode-extension-version-bump-guard-2026-04-22-16-18/.openspec.yaml new file mode 100644 index 0000000..25345f4 --- /dev/null +++ b/openspec/changes/agent-codex-vscode-extension-version-bump-guard-2026-04-22-16-18/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-22 diff --git a/openspec/changes/agent-codex-vscode-extension-version-bump-guard-2026-04-22-16-18/proposal.md b/openspec/changes/agent-codex-vscode-extension-version-bump-guard-2026-04-22-16-18/proposal.md new file mode 100644 index 0000000..079d712 --- /dev/null +++ b/openspec/changes/agent-codex-vscode-extension-version-bump-guard-2026-04-22-16-18/proposal.md @@ -0,0 +1,22 @@ +## Why + +- The Active Agents VS Code companion still showed version `0.0.1` after plugin edits because nothing enforced a visible version bump when the shipped extension changed. +- That makes local reinstall verification ambiguous in VS Code and makes it too easy to ship plugin edits behind a stale installed version label. +- The companion also depended on workspace marker discovery or the first view open before activation, so a freshly reloaded VS Code window could still look stale even after installing the newest extension files. +- Even after the repo had a newer companion build, the installed extension still needed a manual install-script run, so the newest repo copy was easy to miss. + +## What Changes + +- Bump the shipped Active Agents extension manifest version. +- Add a focused regression that requires a higher extension version whenever plugin-shipping files change on a branch. +- Keep the live and template extension manifests aligned so installs and scaffolds report the same version. +- Add `onStartupFinished` to the shipped Active Agents manifests and lock that startup activation contract in the focused regression suite. +- Auto-install the newest workspace companion build when the running extension version is older, then offer a Reload Window prompt so the new version boots immediately. + +## Impact + +- Local VS Code installs show a new extension version after plugin edits. +- Reloaded VS Code windows activate the Active Agents companion immediately instead of waiting for view-open or marker-discovery triggers. +- When a workspace ships a newer companion version than the running extension, the companion updates itself and offers an immediate reload path. +- Future plugin branches fail fast in tests if they forget to bump the extension version. +- Runtime behavior changes are limited to earlier extension activation after startup, one-shot auto-update/reload prompting, and extension metadata/install-path visibility. diff --git a/openspec/changes/agent-codex-vscode-extension-version-bump-guard-2026-04-22-16-18/specs/vscode-active-agents-extension/spec.md b/openspec/changes/agent-codex-vscode-extension-version-bump-guard-2026-04-22-16-18/specs/vscode-active-agents-extension/spec.md new file mode 100644 index 0000000..5b0e43f --- /dev/null +++ b/openspec/changes/agent-codex-vscode-extension-version-bump-guard-2026-04-22-16-18/specs/vscode-active-agents-extension/spec.md @@ -0,0 +1,35 @@ +## MODIFIED Requirements + +### Requirement: Active Agents extension installs a visible current plugin version + +The Active Agents VS Code companion SHALL expose a higher extension version whenever shipped plugin files change on a branch, and the live/template manifests SHALL stay aligned. + +#### Scenario: Plugin edits require a version bump + +- **GIVEN** a branch changes any shipped Active Agents extension files under `vscode/guardex-active-agents/**`, `templates/vscode/guardex-active-agents/**`, or `scripts/install-vscode-active-agents-extension.js` +- **WHEN** the focused extension regression suite runs +- **THEN** `vscode/guardex-active-agents/package.json` SHALL declare a version greater than the base branch version +- **AND** `templates/vscode/guardex-active-agents/package.json` SHALL match that same version exactly + +### Requirement: Active Agents extension activates on VS Code startup + +The Active Agents VS Code companion SHALL declare startup activation in both shipped manifests so the companion can initialize immediately after the VS Code window reloads. + +#### Scenario: Startup activation stays in sync across shipped manifests + +- **GIVEN** the live and template Active Agents extension manifests ship in the repo +- **WHEN** the extension install regression suite reads those manifests +- **THEN** both manifests SHALL include `onStartupFinished` +- **AND** the installed extension manifest SHALL preserve that same activation event list + +### Requirement: Active Agents extension installs a newer workspace build and prompts reload + +The Active Agents VS Code companion SHALL install a newer workspace-shipped build of itself when one is available and SHALL offer a reload action so the updated companion takes effect immediately. + +#### Scenario: Running companion sees a newer workspace build + +- **GIVEN** the running Active Agents extension version is older than `vscode/guardex-active-agents/package.json` in the open workspace +- **AND** the workspace contains `scripts/install-vscode-active-agents-extension.js` +- **WHEN** the companion activates +- **THEN** it SHALL run that install script with the workspace root as the current working directory +- **AND** it SHALL show a message offering `Reload Window` diff --git a/openspec/changes/agent-codex-vscode-extension-version-bump-guard-2026-04-22-16-18/tasks.md b/openspec/changes/agent-codex-vscode-extension-version-bump-guard-2026-04-22-16-18/tasks.md new file mode 100644 index 0000000..69d5163 --- /dev/null +++ b/openspec/changes/agent-codex-vscode-extension-version-bump-guard-2026-04-22-16-18/tasks.md @@ -0,0 +1,32 @@ +## Definition of Done + +This change is complete only when **all** of the following are true: + +- Every checkbox below is checked. +- The agent branch reaches `MERGED` state on `origin` and the PR URL + state are recorded in the completion handoff. +- If any step blocks (test failure, conflict, ambiguous result), append a `BLOCKED:` line under section 4 explaining the blocker and **STOP**. Do not tick remaining cleanup boxes; do not silently skip the cleanup pipeline. + +## 1. Specification + +- [x] 1.1 Finalize the scope around bumping the VS Code extension version whenever shipped plugin files change, activating the companion on VS Code startup, and auto-installing newer workspace builds. +- [x] 1.2 Define the normative version + startup + auto-update requirements in `specs/vscode-active-agents-extension/spec.md`. + +## 2. Implementation + +- [x] 2.1 Bump the live/template Active Agents extension manifests from `0.0.1` to `0.0.3`. +- [x] 2.2 Make the extension install regression read the current manifest version instead of hardcoding `0.0.1`. +- [x] 2.3 Add a focused regression that fails when extension-shipping files change without a higher version than the base branch. +- [x] 2.4 Add `onStartupFinished` to the live/template manifests and lock the installed-manifest/startup contract in the focused regression suite. +- [x] 2.5 Auto-install a newer workspace companion build on activate and offer a `Reload Window` action after the update lands. + +## 3. Verification + +- [x] 3.1 Run `node --test test/vscode-active-agents-session-state.test.js`. +- [x] 3.2 Run `openspec validate agent-codex-vscode-extension-version-bump-guard-2026-04-22-16-18 --type change --strict`. +- [x] 3.3 Run `openspec validate --specs`. + +## 4. Cleanup (mandatory; run before claiming completion) + +- [ ] 4.1 Run `gx branch finish --branch agent/codex/always-active-vscode-extension-2026-04-22-16-18 --base main --via-pr --wait-for-merge --cleanup`. +- [ ] 4.2 Record the PR URL and final merge state (`MERGED`) in the completion handoff. +- [ ] 4.3 Confirm the sandbox worktree is gone (`git worktree list` no longer shows the agent path; `git branch -a` shows no surviving local/remote refs for the branch). diff --git a/templates/vscode/guardex-active-agents/extension.js b/templates/vscode/guardex-active-agents/extension.js index 947e008..f5ef86e 100644 --- a/templates/vscode/guardex-active-agents/extension.js +++ b/templates/vscode/guardex-active-agents/extension.js @@ -20,6 +20,10 @@ const SESSION_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git,.omx/agent-worktrees,.o const WORKTREE_LOCK_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git}/**'; const SESSION_SCAN_LIMIT = 200; const REFRESH_DEBOUNCE_MS = 250; +const ACTIVE_AGENTS_MANIFEST_RELATIVE = path.join('vscode', 'guardex-active-agents', 'package.json'); +const ACTIVE_AGENTS_INSTALL_SCRIPT_RELATIVE = path.join('scripts', 'install-vscode-active-agents-extension.js'); +const RELOAD_WINDOW_ACTION = 'Reload Window'; +const UPDATE_LATER_ACTION = 'Later'; const SESSION_ACTIVITY_GROUPS = [ { kind: 'blocked', label: 'BLOCKED' }, { kind: 'working', label: 'WORKING NOW' }, @@ -565,6 +569,120 @@ function readCurrentBranch(repoRoot) { } } +function parseSimpleSemver(version) { + const parts = String(version || '') + .split('.') + .map((part) => Number.parseInt(part, 10)); + if (parts.length !== 3 || parts.some((part) => Number.isNaN(part))) { + return null; + } + return parts; +} + +function compareSimpleSemver(left, right) { + const leftParts = parseSimpleSemver(left); + const rightParts = parseSimpleSemver(right); + if (!leftParts || !rightParts) { + return 0; + } + + for (let index = 0; index < leftParts.length; index += 1) { + if (leftParts[index] !== rightParts[index]) { + return leftParts[index] - rightParts[index]; + } + } + + return 0; +} + +function readJsonFile(filePath) { + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } catch (_error) { + return null; + } +} + +function resolveActiveAgentsAutoUpdateCandidate(installedVersion) { + const candidates = []; + + for (const workspaceFolder of vscode.workspace.workspaceFolders || []) { + const repoRoot = workspaceFolder?.uri?.fsPath; + if (!repoRoot) { + continue; + } + + const manifestPath = path.join(repoRoot, ACTIVE_AGENTS_MANIFEST_RELATIVE); + const installScriptPath = path.join(repoRoot, ACTIVE_AGENTS_INSTALL_SCRIPT_RELATIVE); + if (!fs.existsSync(manifestPath) || !fs.existsSync(installScriptPath)) { + continue; + } + + const manifest = readJsonFile(manifestPath); + const nextVersion = typeof manifest?.version === 'string' ? manifest.version.trim() : ''; + if (!nextVersion || compareSimpleSemver(nextVersion, installedVersion) <= 0) { + continue; + } + + candidates.push({ repoRoot, installScriptPath, version: nextVersion }); + } + + candidates.sort((left, right) => compareSimpleSemver(right.version, left.version)); + return candidates[0] || null; +} + +function runActiveAgentsInstallScript(repoRoot, installScriptPath) { + return new Promise((resolve, reject) => { + cp.execFile( + process.execPath, + [installScriptPath], + { cwd: repoRoot, encoding: 'utf8' }, + (error, stdout, stderr) => { + if (error) { + reject(new Error(String(stderr || stdout || error.message || '').trim() || 'install failed')); + return; + } + resolve({ stdout, stderr }); + }, + ); + }); +} + +async function maybeAutoUpdateActiveAgentsExtension(context) { + const installedVersion = typeof context?.extension?.packageJSON?.version === 'string' + ? context.extension.packageJSON.version.trim() + : ''; + if (!installedVersion) { + return; + } + + const candidate = resolveActiveAgentsAutoUpdateCandidate(installedVersion); + if (!candidate) { + return; + } + + try { + await runActiveAgentsInstallScript(candidate.repoRoot, candidate.installScriptPath); + } catch (error) { + const failure = typeof error?.message === 'string' && error.message.trim() + ? error.message.trim() + : 'install failed'; + vscode.window.showWarningMessage?.( + `GitGuardex Active Agents could not auto-update to ${candidate.version}: ${failure}`, + ); + return; + } + + const selection = await vscode.window.showInformationMessage?.( + `GitGuardex Active Agents updated to ${candidate.version}. Reload Window to use the newest companion.`, + RELOAD_WINDOW_ACTION, + UPDATE_LATER_ACTION, + ); + if (selection === RELOAD_WINDOW_ACTION) { + await vscode.commands.executeCommand('workbench.action.reloadWindow'); + } +} + function decorateSession(session, lockRegistry) { return { ...session, @@ -1349,6 +1467,7 @@ function activate(context) { ...bindRefreshWatcher(worktreeLockWatcher, scheduleRefresh), ); void refreshController.refreshNow(); + void maybeAutoUpdateActiveAgentsExtension(context); } function deactivate() {} diff --git a/templates/vscode/guardex-active-agents/package.json b/templates/vscode/guardex-active-agents/package.json index 56c3698..feaf5b0 100644 --- a/templates/vscode/guardex-active-agents/package.json +++ b/templates/vscode/guardex-active-agents/package.json @@ -3,7 +3,7 @@ "displayName": "GitGuardex Active Agents", "description": "Shows live Guardex sandbox sessions and repo changes inside VS Code Source Control.", "publisher": "recodeee", - "version": "0.0.1", + "version": "0.0.3", "license": "MIT", "icon": "icon.png", "engines": { @@ -14,6 +14,7 @@ "Other" ], "activationEvents": [ + "onStartupFinished", "workspaceContains:.omx/state/active-sessions", "workspaceContains:.omx/agent-worktrees", "workspaceContains:.omc/agent-worktrees", diff --git a/test/vscode-active-agents-session-state.test.js b/test/vscode-active-agents-session-state.test.js index 6dd0077..bd75733 100644 --- a/test/vscode-active-agents-session-state.test.js +++ b/test/vscode-active-agents-session-state.test.js @@ -8,6 +8,19 @@ const cp = require('node:child_process'); const repoRoot = path.resolve(__dirname, '..'); const sessionScript = path.join(repoRoot, 'scripts', 'agent-session-state.js'); const installScript = path.join(repoRoot, 'scripts', 'install-vscode-active-agents-extension.js'); +const extensionManifestPath = path.join( + repoRoot, + 'vscode', + 'guardex-active-agents', + 'package.json', +); +const templateExtensionManifestPath = path.join( + repoRoot, + 'templates', + 'vscode', + 'guardex-active-agents', + 'package.json', +); const sessionSchema = require(path.join( repoRoot, 'templates', @@ -40,6 +53,83 @@ function initGitRepo(repoPath) { runGit(repoPath, ['config', 'user.name', 'Guardex Tests']); } +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +function parseSimpleSemver(version) { + const parts = version.split('.').map((part) => Number.parseInt(part, 10)); + assert.equal(parts.length, 3, `Expected simple semver, received ${version}`); + for (const part of parts) { + assert.equal(Number.isNaN(part), false, `Expected numeric semver, received ${version}`); + } + return parts; +} + +function compareSimpleSemver(left, right) { + const leftParts = parseSimpleSemver(left); + const rightParts = parseSimpleSemver(right); + for (let index = 0; index < leftParts.length; index += 1) { + if (leftParts[index] !== rightParts[index]) { + return leftParts[index] - rightParts[index]; + } + } + return 0; +} + +function resolveRepoBaseRef() { + for (const candidate of ['origin/main', 'main']) { + const result = cp.spawnSync('git', ['-C', repoRoot, 'rev-parse', '--verify', candidate], { + encoding: 'utf8', + }); + if (result.status === 0) { + return candidate; + } + } + throw new Error('Could not resolve a base ref for the extension version guard.'); +} + +function readExtensionManifest(filePath = extensionManifestPath) { + return readJson(filePath); +} + +function readBaseExtensionManifest(baseRef) { + const result = cp.spawnSync( + 'git', + ['-C', repoRoot, 'show', `${baseRef}:vscode/guardex-active-agents/package.json`], + { + encoding: 'utf8', + }, + ); + assert.equal(result.status, 0, result.stderr || result.stdout); + return JSON.parse(result.stdout); +} + +function readChangedExtensionPaths(baseRef) { + const result = cp.spawnSync( + 'git', + [ + '-C', + repoRoot, + 'diff', + '--name-only', + `${baseRef}...HEAD`, + '--', + 'vscode/guardex-active-agents', + 'templates/vscode/guardex-active-agents', + 'scripts/install-vscode-active-agents-extension.js', + ], + { + encoding: 'utf8', + }, + ); + assert.equal(result.status, 0, result.stderr || result.stdout); + return result.stdout + .split('\n') + .map((entry) => entry.trim()) + .filter(Boolean); +} + function setPathMtime(filePath, whenMs) { const when = new Date(whenMs); fs.utimesSync(filePath, when, when); @@ -131,6 +221,7 @@ function createMockVscode(tempRoot) { openedDocuments: [], shownDocuments: [], infoMessages: [], + infoResponses: [], inputResponses: [], quickPickCalls: [], quickPickResponse: undefined, @@ -312,7 +403,7 @@ function createMockVscode(tempRoot) { if (typeof args[0] === 'string') { registrations.informationMessages.push(args[0]); } - return undefined; + return registrations.infoResponses.shift(); }, showErrorMessage: async (message) => { registrations.errorMessages.push(message); @@ -728,6 +819,7 @@ test('session-schema derives repo change rows from root git status', () => { test('install-vscode-active-agents-extension installs the current extension version and prunes older copies', () => { const tempExtensionsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-ext-')); + const manifest = readExtensionManifest(); const staleDir = path.join(tempExtensionsDir, 'recodeee.gitguardex-active-agents-0.0.0'); fs.mkdirSync(staleDir, { recursive: true }); fs.writeFileSync(path.join(staleDir, 'stale.txt'), 'old', 'utf8'); @@ -737,19 +829,116 @@ test('install-vscode-active-agents-extension installs the current extension vers }); assert.equal(result.status, 0, result.stderr); - const installedDir = path.join(tempExtensionsDir, 'recodeee.gitguardex-active-agents-0.0.1'); - const installedManifest = JSON.parse( - fs.readFileSync(path.join(installedDir, 'package.json'), 'utf8'), + const installedDir = path.join( + tempExtensionsDir, + `recodeee.gitguardex-active-agents-${manifest.version}`, ); + const installedManifest = readJson(path.join(installedDir, 'package.json')); assert.equal(fs.existsSync(installedDir), true); assert.equal(fs.existsSync(path.join(installedDir, 'extension.js')), true); assert.equal(fs.existsSync(path.join(installedDir, 'session-schema.js')), true); assert.equal(installedManifest.icon, 'icon.png'); + assert.equal(installedManifest.version, manifest.version); + assert.deepEqual(installedManifest.activationEvents, manifest.activationEvents); + assert.equal(installedManifest.activationEvents.includes('onStartupFinished'), true); assert.equal(fs.existsSync(path.join(installedDir, 'icon.png')), true); assert.equal(fs.existsSync(staleDir), false); assert.match(result.stdout, /Reload the VS Code window/); }); +test('active-agents extension edits require a higher manifest version than the base branch', () => { + const baseRef = resolveRepoBaseRef(); + const changedPaths = readChangedExtensionPaths(baseRef); + + if (changedPaths.length === 0) { + return; + } + + const liveManifest = readExtensionManifest(); + const templateManifest = readExtensionManifest(templateExtensionManifestPath); + const baseManifest = readBaseExtensionManifest(baseRef); + + assert.equal( + liveManifest.version, + templateManifest.version, + 'Live and template Active Agents manifests must stay in sync.', + ); + assert.deepEqual( + liveManifest.activationEvents, + templateManifest.activationEvents, + 'Live and template Active Agents activation events must stay in sync.', + ); + assert.equal( + liveManifest.activationEvents.includes('onStartupFinished'), + true, + 'Active Agents manifests must activate on VS Code startup.', + ); + assert.ok( + compareSimpleSemver(liveManifest.version, baseManifest.version) > 0, + [ + `Active Agents extension files changed (${changedPaths.join(', ')})`, + `but version ${liveManifest.version} did not increase above ${baseManifest.version}.`, + ].join(' '), + ); +}); + +test('active-agents extension auto-installs a newer workspace build and offers reload', async () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-autoupdate-')); + const repoManifest = { + ...readExtensionManifest(), + version: '9.9.9', + }; + const repoManifestPath = path.join(tempRoot, 'vscode', 'guardex-active-agents', 'package.json'); + const repoInstallScriptPath = path.join(tempRoot, 'scripts', 'install-vscode-active-agents-extension.js'); + fs.mkdirSync(path.dirname(repoManifestPath), { recursive: true }); + fs.writeFileSync(repoManifestPath, `${JSON.stringify(repoManifest, null, 2)}\n`, 'utf8'); + fs.mkdirSync(path.dirname(repoInstallScriptPath), { recursive: true }); + fs.writeFileSync(repoInstallScriptPath, '#!/usr/bin/env node\n', 'utf8'); + + const execCalls = []; + const originalExecFile = cp.execFile; + cp.execFile = (file, args, options, callback) => { + execCalls.push({ file, args, options }); + callback(null, '[guardex-active-agents] ok\n', ''); + }; + + try { + const { registrations, vscode } = createMockVscode(tempRoot); + registrations.infoResponses.push('Reload Window'); + const extension = loadExtensionWithMockVscode(vscode); + const context = { + subscriptions: [], + extension: { + packageJSON: { + version: '0.0.2', + }, + }, + }; + + extension.activate(context); + await flushAsyncWork(); + + assert.equal(execCalls.length, 1); + assert.equal(execCalls[0].file, process.execPath); + assert.deepEqual(execCalls[0].args, [repoInstallScriptPath]); + assert.equal(execCalls[0].options.cwd, tempRoot); + assert.equal(execCalls[0].options.encoding, 'utf8'); + assert.match( + registrations.informationMessages.at(-1), + /GitGuardex Active Agents updated to 9\.9\.9/, + ); + assert.deepEqual(registrations.infoMessages.at(-1).slice(1), ['Reload Window', 'Later']); + assert.equal( + registrations.executedCommands.some( + (entry) => entry.command === 'workbench.action.reloadWindow', + ), + true, + ); + } finally { + cp.execFile = originalExecFile; + } +}); + test('active-agents extension registers tree and decoration providers', async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-view-')); const { registrations, vscode } = createMockVscode(tempRoot); diff --git a/vscode/guardex-active-agents/extension.js b/vscode/guardex-active-agents/extension.js index 4718b6c..be733a2 100644 --- a/vscode/guardex-active-agents/extension.js +++ b/vscode/guardex-active-agents/extension.js @@ -20,6 +20,10 @@ const SESSION_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git,.omx/agent-worktrees,.o const WORKTREE_LOCK_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git}/**'; const SESSION_SCAN_LIMIT = 200; const REFRESH_DEBOUNCE_MS = 250; +const ACTIVE_AGENTS_MANIFEST_RELATIVE = path.join('vscode', 'guardex-active-agents', 'package.json'); +const ACTIVE_AGENTS_INSTALL_SCRIPT_RELATIVE = path.join('scripts', 'install-vscode-active-agents-extension.js'); +const RELOAD_WINDOW_ACTION = 'Reload Window'; +const UPDATE_LATER_ACTION = 'Later'; const SESSION_ACTIVITY_GROUPS = [ { kind: 'blocked', label: 'BLOCKED' }, { kind: 'working', label: 'WORKING NOW' }, @@ -451,6 +455,120 @@ function readCurrentBranch(repoRoot) { } } +function parseSimpleSemver(version) { + const parts = String(version || '') + .split('.') + .map((part) => Number.parseInt(part, 10)); + if (parts.length !== 3 || parts.some((part) => Number.isNaN(part))) { + return null; + } + return parts; +} + +function compareSimpleSemver(left, right) { + const leftParts = parseSimpleSemver(left); + const rightParts = parseSimpleSemver(right); + if (!leftParts || !rightParts) { + return 0; + } + + for (let index = 0; index < leftParts.length; index += 1) { + if (leftParts[index] !== rightParts[index]) { + return leftParts[index] - rightParts[index]; + } + } + + return 0; +} + +function readJsonFile(filePath) { + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } catch (_error) { + return null; + } +} + +function resolveActiveAgentsAutoUpdateCandidate(installedVersion) { + const candidates = []; + + for (const workspaceFolder of vscode.workspace.workspaceFolders || []) { + const repoRoot = workspaceFolder?.uri?.fsPath; + if (!repoRoot) { + continue; + } + + const manifestPath = path.join(repoRoot, ACTIVE_AGENTS_MANIFEST_RELATIVE); + const installScriptPath = path.join(repoRoot, ACTIVE_AGENTS_INSTALL_SCRIPT_RELATIVE); + if (!fs.existsSync(manifestPath) || !fs.existsSync(installScriptPath)) { + continue; + } + + const manifest = readJsonFile(manifestPath); + const nextVersion = typeof manifest?.version === 'string' ? manifest.version.trim() : ''; + if (!nextVersion || compareSimpleSemver(nextVersion, installedVersion) <= 0) { + continue; + } + + candidates.push({ repoRoot, installScriptPath, version: nextVersion }); + } + + candidates.sort((left, right) => compareSimpleSemver(right.version, left.version)); + return candidates[0] || null; +} + +function runActiveAgentsInstallScript(repoRoot, installScriptPath) { + return new Promise((resolve, reject) => { + cp.execFile( + process.execPath, + [installScriptPath], + { cwd: repoRoot, encoding: 'utf8' }, + (error, stdout, stderr) => { + if (error) { + reject(new Error(String(stderr || stdout || error.message || '').trim() || 'install failed')); + return; + } + resolve({ stdout, stderr }); + }, + ); + }); +} + +async function maybeAutoUpdateActiveAgentsExtension(context) { + const installedVersion = typeof context?.extension?.packageJSON?.version === 'string' + ? context.extension.packageJSON.version.trim() + : ''; + if (!installedVersion) { + return; + } + + const candidate = resolveActiveAgentsAutoUpdateCandidate(installedVersion); + if (!candidate) { + return; + } + + try { + await runActiveAgentsInstallScript(candidate.repoRoot, candidate.installScriptPath); + } catch (error) { + const failure = typeof error?.message === 'string' && error.message.trim() + ? error.message.trim() + : 'install failed'; + vscode.window.showWarningMessage?.( + `GitGuardex Active Agents could not auto-update to ${candidate.version}: ${failure}`, + ); + return; + } + + const selection = await vscode.window.showInformationMessage?.( + `GitGuardex Active Agents updated to ${candidate.version}. Reload Window to use the newest companion.`, + RELOAD_WINDOW_ACTION, + UPDATE_LATER_ACTION, + ); + if (selection === RELOAD_WINDOW_ACTION) { + await vscode.commands.executeCommand('workbench.action.reloadWindow'); + } +} + function decorateSession(session, lockRegistry) { return { ...session, @@ -1190,6 +1308,7 @@ function activate(context) { ...bindRefreshWatcher(worktreeLockWatcher, scheduleRefresh), ); void refreshController.refreshNow(); + void maybeAutoUpdateActiveAgentsExtension(context); } function deactivate() {} diff --git a/vscode/guardex-active-agents/package.json b/vscode/guardex-active-agents/package.json index 56c3698..feaf5b0 100644 --- a/vscode/guardex-active-agents/package.json +++ b/vscode/guardex-active-agents/package.json @@ -3,7 +3,7 @@ "displayName": "GitGuardex Active Agents", "description": "Shows live Guardex sandbox sessions and repo changes inside VS Code Source Control.", "publisher": "recodeee", - "version": "0.0.1", + "version": "0.0.3", "license": "MIT", "icon": "icon.png", "engines": { @@ -14,6 +14,7 @@ "Other" ], "activationEvents": [ + "onStartupFinished", "workspaceContains:.omx/state/active-sessions", "workspaceContains:.omx/agent-worktrees", "workspaceContains:.omc/agent-worktrees",