Skip to content

Commit e2d7651

Browse files
NagyViktNagyVikt
andauthored
Keep overlapping agent integration off the protected base branch (#232)
Guardex needs a managed merge lane when multiple agent branches touch the same files. This adds the agent-branch-merge workflow, wires gx merge into setup-managed files and package scripts, and makes overlap/conflict handling resumable without merging directly onto main. Constraint: Protected base branches must remain untouched while overlapping agent work is integrated Rejected: Merge helper branches directly onto main | breaks the sandboxed integration workflow Confidence: high Scope-risk: moderate Directive: Keep conflict stops resumable and do not auto-resolve overlapping merges beyond ordered git merge behavior Tested: node --test test/merge-workflow.test.js Tested: node --check bin/multiagent-safety.js Tested: openspec validate agent-codex-merge-overlapping-agent-branches-2026-04-21-14-28 --type change --strict Tested: openspec validate --specs Tested: git diff --check Not-tested: npm test still hangs after early passing output in this environment Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
1 parent b145636 commit e2d7651

9 files changed

Lines changed: 1340 additions & 7 deletions

File tree

bin/multiagent-safety.js

Lines changed: 133 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ const TEMPLATE_ROOT = path.resolve(__dirname, '..', 'templates');
8989
const TEMPLATE_FILES = [
9090
'scripts/agent-branch-start.sh',
9191
'scripts/agent-branch-finish.sh',
92+
'scripts/agent-branch-merge.sh',
9293
'scripts/codex-agent.sh',
9394
'scripts/guardex-docker-loader.sh',
9495
'scripts/review-bot-watch.sh',
@@ -112,6 +113,7 @@ const TEMPLATE_FILES = [
112113
const REQUIRED_WORKFLOW_FILES = [
113114
'scripts/agent-branch-start.sh',
114115
'scripts/agent-branch-finish.sh',
116+
'scripts/agent-branch-merge.sh',
115117
'scripts/guardex-docker-loader.sh',
116118
'scripts/agent-worktree-prune.sh',
117119
'scripts/agent-file-locks.py',
@@ -126,6 +128,7 @@ const REQUIRED_PACKAGE_SCRIPTS = {
126128
'agent:codex': 'bash ./scripts/codex-agent.sh',
127129
'agent:branch:start': 'bash ./scripts/agent-branch-start.sh',
128130
'agent:branch:finish': 'bash ./scripts/agent-branch-finish.sh',
131+
'agent:branch:merge': 'bash ./scripts/agent-branch-merge.sh',
129132
'agent:cleanup': 'gx cleanup',
130133
'agent:hooks:install': 'bash ./scripts/install-agent-git-hooks.sh',
131134
'agent:locks:claim': 'python3 ./scripts/agent-file-locks.py claim',
@@ -149,6 +152,7 @@ const REQUIRED_PACKAGE_SCRIPTS = {
149152
const EXECUTABLE_RELATIVE_PATHS = new Set([
150153
'scripts/agent-branch-start.sh',
151154
'scripts/agent-branch-finish.sh',
155+
'scripts/agent-branch-merge.sh',
152156
'scripts/codex-agent.sh',
153157
'scripts/guardex-docker-loader.sh',
154158
'scripts/review-bot-watch.sh',
@@ -171,6 +175,7 @@ const CRITICAL_GUARDRAIL_PATHS = new Set([
171175
'.githooks/post-checkout',
172176
'scripts/agent-branch-start.sh',
173177
'scripts/agent-branch-finish.sh',
178+
'scripts/agent-branch-merge.sh',
174179
'scripts/agent-worktree-prune.sh',
175180
'scripts/codex-agent.sh',
176181
'scripts/agent-file-locks.py',
@@ -233,6 +238,7 @@ const SUGGESTIBLE_COMMANDS = [
233238
'setup',
234239
'doctor',
235240
'agents',
241+
'merge',
236242
'finish',
237243
'report',
238244
'protect',
@@ -257,6 +263,7 @@ const CLI_COMMAND_DESCRIPTIONS = [
257263
['setup', 'Install, repair, and verify guardrails (flags: --repair, --install-only, --target)'],
258264
['doctor', 'Repair drift + verify (auto-sandboxes on protected main)'],
259265
['protect', 'Manage protected branches (list/add/remove/set/reset)'],
266+
['merge', 'Create/reuse an integration lane and merge overlapping agent branches'],
260267
['sync', 'Sync agent branches with origin/<base>'],
261268
['finish', 'Commit + PR + merge completed agent branches (--all, --branch)'],
262269
['cleanup', 'Prune merged/stale agent branches and worktrees'],
@@ -304,13 +311,14 @@ const AI_SETUP_PROMPT = `GitGuardex (gx) setup checklist for Codex/Claude in thi
304311
3) Repair: gx doctor
305312
4) Task loop: bash scripts/codex-agent.sh "<task>" "<agent>"
306313
or branch-start -> python3 scripts/agent-file-locks.py claim -> branch-finish
307-
5) Finish: gx finish --all
308-
6) Cleanup: gx cleanup
309-
7) OpenSpec: /opsx:propose -> /opsx:apply -> /opsx:archive
310-
8) Optional: gx protect add release staging
311-
9) Optional: gx sync --check && gx sync
312-
10) Review bot: install https://github.com/apps/cr-gpt + set OPENAI_API_KEY
313-
11) Fork sync: install https://github.com/apps/pull + cp .github/pull.yml.example .github/pull.yml
314+
5) Integrate: gx merge --branch <agent-a> --branch <agent-b>
315+
6) Finish: gx finish --all
316+
7) Cleanup: gx cleanup
317+
8) OpenSpec: /opsx:propose -> /opsx:apply -> /opsx:archive
318+
9) Optional: gx protect add release staging
319+
10) Optional: gx sync --check && gx sync
320+
11) Review bot: install https://github.com/apps/cr-gpt + set OPENAI_API_KEY
321+
12) Fork sync: install https://github.com/apps/pull + cp .github/pull.yml.example .github/pull.yml
314322
`;
315323

316324
const AI_SETUP_COMMANDS = `npm i -g @imdeadpool/guardex
@@ -319,6 +327,7 @@ gx setup
319327
gx doctor
320328
bash scripts/codex-agent.sh "<task>" "<agent>"
321329
python3 scripts/agent-file-locks.py claim --branch "<agent-branch>" <file...>
330+
gx merge --branch "<agent-a>" --branch "<agent-b>"
322331
gx finish --all
323332
gx cleanup
324333
gx protect add release staging
@@ -3543,6 +3552,82 @@ function parseCleanupArgs(rawArgs) {
35433552
return options;
35443553
}
35453554

3555+
function parseMergeArgs(rawArgs) {
3556+
const options = {
3557+
target: process.cwd(),
3558+
base: '',
3559+
into: '',
3560+
branches: [],
3561+
task: '',
3562+
agent: '',
3563+
};
3564+
3565+
for (let index = 0; index < rawArgs.length; index += 1) {
3566+
const arg = rawArgs[index];
3567+
if (arg === '--target') {
3568+
const next = rawArgs[index + 1];
3569+
if (!next) {
3570+
throw new Error('--target requires a path value');
3571+
}
3572+
options.target = next;
3573+
index += 1;
3574+
continue;
3575+
}
3576+
if (arg === '--base') {
3577+
const next = rawArgs[index + 1];
3578+
if (!next) {
3579+
throw new Error('--base requires a branch value');
3580+
}
3581+
options.base = next;
3582+
index += 1;
3583+
continue;
3584+
}
3585+
if (arg === '--into') {
3586+
const next = rawArgs[index + 1];
3587+
if (!next) {
3588+
throw new Error('--into requires an agent/* branch value');
3589+
}
3590+
options.into = next;
3591+
index += 1;
3592+
continue;
3593+
}
3594+
if (arg === '--branch') {
3595+
const next = rawArgs[index + 1];
3596+
if (!next) {
3597+
throw new Error('--branch requires an agent/* branch value');
3598+
}
3599+
options.branches.push(next);
3600+
index += 1;
3601+
continue;
3602+
}
3603+
if (arg === '--task') {
3604+
const next = rawArgs[index + 1];
3605+
if (!next) {
3606+
throw new Error('--task requires a task value');
3607+
}
3608+
options.task = next;
3609+
index += 1;
3610+
continue;
3611+
}
3612+
if (arg === '--agent') {
3613+
const next = rawArgs[index + 1];
3614+
if (!next) {
3615+
throw new Error('--agent requires an agent value');
3616+
}
3617+
options.agent = next;
3618+
index += 1;
3619+
continue;
3620+
}
3621+
throw new Error(`Unknown option: ${arg}`);
3622+
}
3623+
3624+
if (options.branches.length === 0) {
3625+
throw new Error('merge requires at least one --branch <agent/*> input');
3626+
}
3627+
3628+
return options;
3629+
}
3630+
35463631
function parseFinishArgs(rawArgs) {
35473632
const options = {
35483633
target: process.cwd(),
@@ -6587,6 +6672,46 @@ function cleanup(rawArgs) {
65876672
process.exitCode = 0;
65886673
}
65896674

6675+
function merge(rawArgs) {
6676+
const options = parseMergeArgs(rawArgs);
6677+
const repoRoot = resolveRepoRoot(options.target);
6678+
const mergeScript = path.join(repoRoot, 'scripts', 'agent-branch-merge.sh');
6679+
6680+
if (!fs.existsSync(mergeScript)) {
6681+
throw new Error(`Missing merge script: ${mergeScript}. Run '${SHORT_TOOL_NAME} setup' first.`);
6682+
}
6683+
6684+
const args = [mergeScript];
6685+
if (options.base) {
6686+
args.push('--base', options.base);
6687+
}
6688+
if (options.into) {
6689+
args.push('--into', options.into);
6690+
}
6691+
if (options.task) {
6692+
args.push('--task', options.task);
6693+
}
6694+
if (options.agent) {
6695+
args.push('--agent', options.agent);
6696+
}
6697+
for (const branch of options.branches) {
6698+
args.push('--branch', branch);
6699+
}
6700+
6701+
const mergeResult = run('bash', args, { cwd: repoRoot, stdio: 'pipe' });
6702+
if (mergeResult.stdout) {
6703+
process.stdout.write(mergeResult.stdout);
6704+
}
6705+
if (mergeResult.stderr) {
6706+
process.stderr.write(mergeResult.stderr);
6707+
}
6708+
if (mergeResult.status !== 0) {
6709+
throw new Error(`merge command failed with status ${mergeResult.status}`);
6710+
}
6711+
6712+
process.exitCode = 0;
6713+
}
6714+
65906715
function finish(rawArgs) {
65916716
const options = parseFinishArgs(rawArgs);
65926717
const repoRoot = resolveRepoRoot(options.target);
@@ -7073,6 +7198,7 @@ function main() {
70737198
if (command === 'prompt') return prompt(rest);
70747199
if (command === 'doctor') return doctor(rest);
70757200
if (command === 'agents') return agents(rest);
7201+
if (command === 'merge') return merge(rest);
70767202
if (command === 'finish') return finish(rest);
70777203
if (command === 'report') return report(rest);
70787204
if (command === 'protect') return protect(rest);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-04-21
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
## Why
2+
3+
- GitGuardex currently finishes one `agent/*` branch into the base branch at a time, but it does not provide an integration workflow for the common case where multiple agent worktrees touched the same implementation files.
4+
- In that situation users end up staring at several parallel worktrees with overlapping edits and no first-class Guardex command that creates the right integration branch/worktree, reports overlap, and gives a safe place to resolve conflicts.
5+
- OpenSpec already treats implementation as owner/helper lanes with durable artifacts, so the merge workflow should preserve that model instead of pushing users back to ad hoc manual git work on the protected base.
6+
7+
## What Changes
8+
9+
- Add a first-class `gx merge` command plus a managed `scripts/agent-branch-merge.sh` workflow.
10+
- Let the workflow either create a fresh integration `agent/*` branch/worktree from the configured base branch or merge helper branches into an existing owner branch via `--into`.
11+
- Report overlapping changed files across the requested source branches before merging so users can see where collisions are expected, especially inside shared implementation files and OpenSpec surfaces.
12+
- Merge branches in explicit order, stop on conflicts without touching the protected base branch, and print resumable instructions that keep conflict resolution inside the integration worktree.
13+
- Ship the new script through the setup/templates/package metadata path so downstream repos get the same capability.
14+
15+
## Impact
16+
17+
- Affected surfaces: `gx` CLI command catalog, managed workflow scripts/templates, setup/doctor script expectations, and regression tests.
18+
- Risk: merge automation is sensitive to dirty worktrees and stale branches, so the implementation needs strict preflight checks and clear conflict-stop behavior.
19+
- Rollout: local CLI/script addition only; no data migration.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
## ADDED Requirements
2+
3+
### Requirement: integration merge runs inside an agent worktree
4+
GitGuardex SHALL merge overlapping `agent/*` branches inside an integration `agent/*` branch/worktree instead of merging directly on the protected base branch.
5+
6+
#### Scenario: create a fresh integration lane
7+
- **WHEN** a user runs `gx merge` with one or more `--branch agent/...` inputs and no `--into`
8+
- **THEN** the system creates a new integration `agent/*` branch/worktree from the configured base branch
9+
- **AND** all requested merges run inside that integration worktree
10+
- **AND** the command prints the integration branch and worktree path.
11+
12+
#### Scenario: reuse an existing owner lane
13+
- **WHEN** a user runs `gx merge --into <agent-branch>` with additional source branches
14+
- **THEN** the system reuses that owner branch as the merge target
15+
- **AND** it refuses to proceed if the target worktree has uncommitted changes or an in-progress merge operation.
16+
17+
### Requirement: overlapping file edits are reported before merge
18+
GitGuardex SHALL detect and report files changed by more than one requested source branch before applying the merges.
19+
20+
#### Scenario: overlapping implementation files exist
21+
- **WHEN** two or more requested source branches changed the same file relative to the merge base
22+
- **THEN** the command prints each overlapping file
23+
- **AND** it identifies the source branches that changed that file
24+
- **AND** it still allows the user to continue into the integration lane unless another hard preflight check fails.
25+
26+
### Requirement: conflicts stop with resumable guidance
27+
GitGuardex SHALL stop on merge conflicts inside the integration worktree and provide resumable next-step guidance without mutating the protected base branch.
28+
29+
#### Scenario: sequential merge hits a conflict
30+
- **WHEN** `gx merge` successfully merges earlier source branches and then encounters a conflict on a later source branch
31+
- **THEN** the command exits non-zero
32+
- **AND** it prints the target integration branch/worktree, the source branch that conflicted, and the conflicting files
33+
- **AND** it tells the user how to resolve or abort the conflict inside the integration worktree
34+
- **AND** it prints the remaining branches so the merge sequence can be resumed intentionally afterward.
35+
36+
### Requirement: setup-managed repos receive the merge workflow
37+
GitGuardex setup/doctor SHALL install the managed merge workflow files and package script entry needed to run `gx merge`.
38+
39+
#### Scenario: setup bootstraps a repo
40+
- **WHEN** `gx setup` or `gx doctor --repair` installs managed workflow files
41+
- **THEN** the repo contains `scripts/agent-branch-merge.sh`
42+
- **AND** the repo package scripts include a stable merge entry point for the managed workflow.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
## 1. Specification
2+
3+
- [x] 1.1 Finalize acceptance criteria for the overlapping-agent merge workflow.
4+
- [x] 1.2 Define normative requirements for integration-branch creation, overlap reporting, and conflict-stop behavior.
5+
6+
## 2. Implementation
7+
8+
- [x] 2.1 Add a managed `agent-branch-merge` script that can create or reuse an integration worktree and merge multiple `agent/*` branches in order.
9+
- [x] 2.2 Add `gx merge` CLI wiring, package metadata, and template/setup propagation for the new workflow.
10+
- [x] 2.3 Keep the protected base branch untouched while merging and print resumable instructions for conflict resolution.
11+
12+
## 3. Verification
13+
14+
- [x] 3.1 Add/update focused regression coverage for clean merges, overlap reporting, and conflict-stop behavior.
15+
- [ ] 3.2 Run `npm test`. BLOCKED: full suite produced early passing output but then stayed silent/hung in this environment; focused `node --test test/merge-workflow.test.js` passed.
16+
- [x] 3.3 Run `node --check bin/multiagent-safety.js`.
17+
- [x] 3.4 Run `openspec validate agent-codex-merge-overlapping-agent-branches-2026-04-21-14-28 --type change --strict`.
18+
- [x] 3.5 Run `openspec validate --specs`.
19+
- [x] 3.6 Run `git diff --check`.
20+
21+
## 4. Completion
22+
23+
- [ ] 4.1 Finish the agent branch via PR merge + cleanup (`gx finish --via-pr --wait-for-merge --cleanup` or `bash scripts/agent-branch-finish.sh --branch <agent-branch> --base <base-branch> --via-pr --wait-for-merge --cleanup`).
24+
- [ ] 4.2 Record PR URL + final `MERGED` state in the completion handoff.
25+
- [ ] 4.3 Confirm sandbox cleanup (`git worktree list`, `git branch -a`) or capture a `BLOCKED:` handoff if merge/cleanup is pending.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"agent:codex": "bash ./scripts/codex-agent.sh",
1616
"agent:branch:start": "bash ./scripts/agent-branch-start.sh",
1717
"agent:branch:finish": "bash ./scripts/agent-branch-finish.sh",
18+
"agent:branch:merge": "bash ./scripts/agent-branch-merge.sh",
1819
"agent:cleanup": "gx cleanup",
1920
"agent:hooks:install": "bash ./scripts/install-agent-git-hooks.sh",
2021
"agent:locks:claim": "python3 ./scripts/agent-file-locks.py claim",

0 commit comments

Comments
 (0)