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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 `../<repo>-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.
Expand Down Expand Up @@ -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 (`../<repo>-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`.
Expand Down
91 changes: 89 additions & 2 deletions bin/multiagent-safety.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)'],
Expand Down Expand Up @@ -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 <repo-path> then ${SHORT_TOOL_NAME} doctor --target <repo-path>
- Optional parent-folder Source Control view: ${SHORT_TOOL_NAME} setup --target <repo-path> --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
Expand Down Expand Up @@ -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 };

Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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({
Expand All @@ -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, {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
38 changes: 38 additions & 0 deletions test/install.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down