From f3e52bffe0f5d14b1706d272e17fe04a2e8ef629 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Fri, 17 Apr 2026 14:57:17 +0200 Subject: [PATCH] Enable parent-folder worktree management view and ship next patch release Users managing multiple agent worktrees from one directory above the repo need an explicit VS Code Source Control workspace that includes the base checkout and .omx/agent-worktrees. This change adds an opt-in setup flag (--parent-workspace-view), writes the parent workspace file with stable folder settings, covers the behavior with tests, and bumps npm package metadata to the next patch version with synced release notes. Constraint: Keep setup default behavior unchanged unless explicitly requested Rejected: Always create parent workspace on every setup | unexpected file write outside repo root Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep parent workspace generation opt-in unless repo policy changes Tested: node --check bin/multiagent-safety.js; npm test Not-tested: Interactive VS Code UI rendering check of Source Control panel --- README.md | 13 ++++++ bin/multiagent-safety.js | 91 +++++++++++++++++++++++++++++++++++++++- package-lock.json | 4 +- package.json | 2 +- test/install.test.js | 38 +++++++++++++++++ 5 files changed, 143 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9e58891..b1ce42a 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,11 @@ gx doctor # setup + repair another repo without switching your current repo checkout gx setup --target /path/to/repo gx doctor --target /path/to/repo +# optional: from parent folder, generate VS Code workspace view for repo + agent worktrees +cd /path/to +gx setup --target ./repo --parent-workspace-view +# open this in VS Code to manage both base repo and .omx/agent-worktrees +code ./repo-branches.code-workspace # protected branch management gx protect list @@ -198,6 +203,7 @@ gx agents stop - `gx init` is alias of `gx setup`. - Setup/doctor can install missing global OMX/OpenSpec/codex-auth with explicit Y/N confirmation. - `gx setup` checks GitHub CLI (`gh`) and prints install guidance if missing. +- Optional parent-folder VS Code Source Control view: `gx setup --target /path/to/repo --parent-workspace-view` creates `../-branches.code-workspace`. - Interactive self-update prompt defaults to **No** (`[y/N]`). - In initialized repos, `setup`/`install`/`fix` block protected-base writes unless explicitly overridden. - Direct commits/pushes to protected branches are blocked by default. @@ -366,6 +372,13 @@ npm pack --dry-run ## Release notes +### v5.0.15 + +- Added `gx setup --parent-workspace-view` to generate a parent-folder VS Code workspace (`../-branches.code-workspace`) that shows both the base repo and `.omx/agent-worktrees` in Source Control. +- Added dry-run-safe parent workspace operations (`would-create` / `would-update`) and setup output that prints the created workspace path. +- Added regression coverage for parent workspace generation and dry-run behavior. +- Bumped package version from `5.0.14` to `5.0.15`. + ### v5.0.14 - Changed release metadata for the next npm publish by bumping package version from `5.0.13` to `5.0.14`. diff --git a/bin/multiagent-safety.js b/bin/multiagent-safety.js index 895535a..59e11eb 100755 --- a/bin/multiagent-safety.js +++ b/bin/multiagent-safety.js @@ -184,7 +184,7 @@ const SUGGESTIBLE_COMMANDS = [ ]; const CLI_COMMAND_DESCRIPTIONS = [ ['status', 'Show GuardeX CLI + service health without modifying files'], - ['setup', 'Install + repair guardrails in a git repo (supports --no-gitignore)'], + ['setup', 'Install + repair guardrails in a git repo (supports --no-gitignore, --parent-workspace-view)'], ['init', 'Alias of setup (bootstrap + repair guardrails in a git repo)'], ['doctor', 'Repair safety setup drift, then verify repo safety'], ['report', 'Generate security/safety reports (for example: OpenSSF scorecard)'], @@ -445,6 +445,7 @@ NOTES - ${TOOL_NAME} setup asks for Y/N approval before global installs - ${TOOL_NAME} setup checks GitHub CLI (gh) and prints install guidance if missing - For other repos: ${SHORT_TOOL_NAME} setup --target then ${SHORT_TOOL_NAME} doctor --target + - Optional parent-folder Source Control view: ${SHORT_TOOL_NAME} setup --target --parent-workspace-view - In initialized repos, setup/install/fix block in-place writes on protected main by default - setup/doctor auto-finish clean pending agent/* branches via PR flow into the current local base branch - doctor auto-runs in a sandbox agent branch/worktree on protected main and tries auto-finish PR flow @@ -838,6 +839,14 @@ function configureHooks(repoRoot, dryRun) { return { status: 'set', key: 'core.hooksPath', value: '.githooks' }; } +function requireValue(rawArgs, index, flagName) { + const value = rawArgs[index + 1]; + if (!value || value.startsWith('-')) { + throw new Error(`${flagName} requires a value`); + } + return value; +} + function parseCommonArgs(rawArgs, defaults) { const options = { ...defaults }; @@ -899,6 +908,76 @@ function parseCommonArgs(rawArgs, defaults) { return options; } +function parseSetupArgs(rawArgs, defaults) { + const setupDefaults = { ...defaults, parentWorkspaceView: false }; + const forwardedArgs = []; + + for (const arg of rawArgs) { + if (arg === '--parent-workspace-view') { + setupDefaults.parentWorkspaceView = true; + continue; + } + if (arg === '--no-parent-workspace-view') { + setupDefaults.parentWorkspaceView = false; + continue; + } + forwardedArgs.push(arg); + } + + return parseCommonArgs(forwardedArgs, setupDefaults); +} + +function normalizeWorkspacePath(relativePath) { + return String(relativePath || '.').replace(/\\/g, '/'); +} + +function buildParentWorkspaceView(repoRoot) { + const parentDir = path.dirname(repoRoot); + const workspaceFileName = `${path.basename(repoRoot)}-branches.code-workspace`; + const workspacePath = path.join(parentDir, workspaceFileName); + const repoRelativePath = normalizeWorkspacePath(path.relative(parentDir, repoRoot) || '.'); + const worktreesRelativePath = normalizeWorkspacePath( + path.join(repoRelativePath === '.' ? '' : repoRelativePath, '.omx', 'agent-worktrees'), + ); + + return { + workspacePath, + payload: { + folders: [ + { path: repoRelativePath }, + { path: worktreesRelativePath }, + ], + settings: { + 'scm.alwaysShowRepositories': true, + }, + }, + }; +} + +function ensureParentWorkspaceView(repoRoot, dryRun) { + const { workspacePath, payload } = buildParentWorkspaceView(repoRoot); + const operationFile = path.relative(repoRoot, workspacePath) || path.basename(workspacePath); + const nextContent = `${JSON.stringify(payload, null, 2)}\n`; + const note = 'parent VS Code workspace view'; + + if (!fs.existsSync(workspacePath)) { + if (!dryRun) { + fs.writeFileSync(workspacePath, nextContent, 'utf8'); + } + return { status: dryRun ? 'would-create' : 'created', file: operationFile, note }; + } + + const currentContent = fs.readFileSync(workspacePath, 'utf8'); + if (currentContent === nextContent) { + return { status: 'unchanged', file: operationFile, note }; + } + + if (!dryRun) { + fs.writeFileSync(workspacePath, nextContent, 'utf8'); + } + return { status: dryRun ? 'would-update' : 'updated', file: operationFile, note }; +} + function hasGuardexBootstrapFiles(repoRoot) { const required = [ 'AGENTS.md', @@ -4284,7 +4363,7 @@ function report(rawArgs) { } function setup(rawArgs) { - const options = parseCommonArgs(rawArgs, { + const options = parseSetupArgs(rawArgs, { target: process.cwd(), force: false, skipAgents: false, @@ -4331,6 +4410,9 @@ function setup(rawArgs) { assertProtectedMainWriteAllowed(options, 'setup'); const installPayload = runInstallInternal(options); installPayload.operations.push(ensureSetupProtectedBranches(installPayload.repoRoot, Boolean(options.dryRun))); + if (options.parentWorkspaceView) { + installPayload.operations.push(ensureParentWorkspaceView(installPayload.repoRoot, Boolean(options.dryRun))); + } printOperations('Setup/install', installPayload, options.dryRun); const fixPayload = runFixInternal({ @@ -4349,6 +4431,11 @@ function setup(rawArgs) { return; } + if (options.parentWorkspaceView) { + const parentWorkspace = buildParentWorkspaceView(installPayload.repoRoot); + console.log(`[${TOOL_NAME}] Parent workspace view: ${parentWorkspace.workspacePath}`); + } + const scanResult = runScanInternal({ target: options.target, json: false }); const currentBaseBranch = currentBranchName(scanResult.repoRoot); const autoFinishSummary = autoFinishReadyAgentBranches(scanResult.repoRoot, { diff --git a/package-lock.json b/package-lock.json index 45ff00a..7aedcc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imdeadpool/guardex", - "version": "5.0.14", + "version": "5.0.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imdeadpool/guardex", - "version": "5.0.14", + "version": "5.0.15", "license": "MIT", "bin": { "guardex": "bin/multiagent-safety.js", diff --git a/package.json b/package.json index 3510278..8ed2944 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imdeadpool/guardex", - "version": "5.0.14", + "version": "5.0.15", "description": "GuardeX: the Guardian T-Rex for your repo, with hardened multi-agent git guardrails.", "license": "MIT", "preferGlobal": true, diff --git a/test/install.test.js b/test/install.test.js index 6e642ea..d37546e 100644 --- a/test/install.test.js +++ b/test/install.test.js @@ -356,6 +356,44 @@ test('setup provisions workflow files and repo config', () => { assert.equal(secondRun.status, 0, secondRun.stderr || secondRun.stdout); }); +test('setup --parent-workspace-view creates one-level-up VS Code workspace for repo + agent worktrees', () => { + const repoDir = initRepo(); + const parentDir = path.dirname(repoDir); + const workspacePath = path.join(parentDir, `${path.basename(repoDir)}-branches.code-workspace`); + + assert.equal(fs.existsSync(workspacePath), false, 'workspace file should not exist before setup'); + + const result = runNode( + ['setup', '--target', repoDir, '--no-global-install', '--parent-workspace-view'], + repoDir, + ); + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.match(result.stdout, /parent VS Code workspace view/); + assert.match(result.stdout, /Parent workspace view:/); + + assert.equal(fs.existsSync(workspacePath), true, 'setup should create parent workspace file'); + const workspace = JSON.parse(fs.readFileSync(workspacePath, 'utf8')); + assert.deepEqual(workspace.folders, [ + { path: path.basename(repoDir) }, + { path: `${path.basename(repoDir)}/.omx/agent-worktrees` }, + ]); + assert.equal(workspace.settings['scm.alwaysShowRepositories'], true); +}); + +test('setup --parent-workspace-view respects dry-run and does not write parent workspace file', () => { + const repoDir = initRepo(); + const parentDir = path.dirname(repoDir); + const workspacePath = path.join(parentDir, `${path.basename(repoDir)}-branches.code-workspace`); + + const result = runNode( + ['setup', '--target', repoDir, '--no-global-install', '--parent-workspace-view', '--dry-run'], + repoDir, + ); + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.match(result.stdout, /would-create\s+\.\.\/repo-branches\.code-workspace \(parent VS Code workspace view\)/); + assert.equal(fs.existsSync(workspacePath), false, 'dry run must not create parent workspace file'); +}); + test('setup refreshes existing managed AGENTS block to latest template policy', () => { const repoDir = initRepo(); const legacyAgents = `# AGENTS