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,2 @@
schema: spec-driven
created: 2026-04-27
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Proposal: reuse the active agent worktree on branch start

`gx branch start` currently creates a fresh timestamped branch even when it is invoked from inside an existing `agent/*` worktree. That copies the active sandbox into a nested sandbox and splits follow-up work away from the lane the user selected.

- reuse the current `agent/*` worktree by default when `branch start` runs inside it
- keep an explicit `--new` / `--no-reuse` escape hatch for intentional child lanes
- preserve downstream parser compatibility for reused branch-start output
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## ADDED Requirements

### Requirement: Branch start reuses current agent worktree
When `gx branch start` runs from a worktree whose current branch starts with `agent/`, the command SHALL reuse that existing branch and worktree by default instead of creating another timestamped branch/worktree from it.

#### Scenario: Follow-up agent starts inside an existing sandbox
- **GIVEN** the current working tree is an agent worktree on branch `agent/codex/example`
- **WHEN** `gx branch start "continue work" "codex"` is run from that worktree
- **THEN** the command reports `Reusing existing branch: agent/codex/example`
- **AND** the reported worktree path is the current worktree
- **AND** no nested agent worktree is created.

### Requirement: Branch start keeps an explicit new-lane escape hatch
When a caller intentionally needs a child or parallel lane from inside an existing agent worktree, the command SHALL provide an explicit option to bypass reuse and create a new branch/worktree.

#### Scenario: Caller opts out of reuse
- **GIVEN** the current working tree is an agent worktree
- **WHEN** `gx branch start --new "parallel work" "codex"` is run
- **THEN** the command may create a new isolated branch/worktree using the existing startup behavior.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## 1. Spec

- [x] 1.1 Capture why branch start inside an active agent sandbox should attach instead of cloning the sandbox.
- [x] 1.2 Define the reuse behavior and explicit new-lane escape hatch.

## 2. Implementation

- [x] 2.1 Update branch-start runtime and template scripts to reuse the current `agent/*` worktree by default.
- [x] 2.2 Keep parser surfaces compatible with reused branch-start output.
- [x] 2.3 Add focused regression coverage for the existing-worktree reuse path.

## 3. Verification

- [x] 3.1 Run targeted branch-start regression tests.
- [x] 3.2 Run template parity and script syntax checks.
- [x] 3.3 Run `openspec validate agent-codex-reuse-existing-agent-worktree-on-repeate-2026-04-27-18-17 --type change --strict`.
- [x] 3.4 Run `openspec validate --specs`.

Verification evidence:
- `node --test test/branch.test.js` (pass, 30/30)
- `node --test test/metadata.test.js` (pass, 24/24)
- `npm test` (pass, 288 passed, 1 skipped)
- `bash -n scripts/agent-branch-start.sh`, `bash -n templates/scripts/agent-branch-start.sh`, `bash -n scripts/agent-branch-merge.sh`, `bash -n templates/scripts/agent-branch-merge.sh` (pass)
- `git diff --check` (pass)
- `openspec validate agent-codex-reuse-existing-agent-worktree-on-repeate-2026-04-27-18-17 --type change --strict` (pass)
- `openspec validate --specs` (pass; no spec items found)

## 4. Cleanup

- [ ] 4.1 Commit, push, open/update PR, merge, and clean up the worktree.
- [ ] 4.2 Record PR URL + final `MERGED` state in the completion handoff.
- [ ] 4.3 Confirm the sandbox worktree is gone.
2 changes: 1 addition & 1 deletion scripts/agent-branch-merge.sh
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ if [[ -z "$TARGET_BRANCH" ]]; then
fi

printf '%s\n' "$start_output"
TARGET_BRANCH="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Created branch: //p' | head -n 1)"
TARGET_BRANCH="$(printf '%s\n' "$start_output" | sed -n -E 's/^\[agent-branch-start\] (Created branch|Reusing existing branch): //p' | head -n 1)"
target_worktree="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Worktree: //p' | head -n 1)"
if [[ -z "$TARGET_BRANCH" || -z "$target_worktree" ]]; then
echo "[agent-branch-merge] Unable to parse target branch/worktree from agent-branch-start output." >&2
Expand Down
36 changes: 34 additions & 2 deletions scripts/agent-branch-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ OPENSPEC_CHANGE_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CHANGE_SLUG:-}"
OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CAPABILITY_SLUG:-}"
OPENSPEC_MASTERPLAN_LABEL_RAW="${GUARDEX_OPENSPEC_MASTERPLAN_LABEL-masterplan}"
OPENSPEC_TIER_RAW="${GUARDEX_OPENSPEC_TIER:-T3}"
REUSE_EXISTING_RAW="${GUARDEX_BRANCH_START_REUSE_EXISTING:-true}"
PRINT_NAME_ONLY=0
POSITIONAL_ARGS=()

Expand Down Expand Up @@ -58,6 +59,14 @@ while [[ $# -gt 0 ]]; do
OPENSPEC_TIER_RAW="${2:-$OPENSPEC_TIER_RAW}"
shift 2
;;
--reuse-existing|--reuse)
REUSE_EXISTING_RAW="true"
shift
;;
--new|--no-reuse|--no-reuse-existing)
REUSE_EXISTING_RAW="false"
shift
;;
--in-place|--allow-in-place)
echo "[agent-branch-start] In-place branch mode is disabled." >&2
echo "[agent-branch-start] This command always creates an isolated worktree to keep your active checkout unchanged." >&2
Expand All @@ -78,7 +87,7 @@ while [[ $# -gt 0 ]]; do
;;
-*)
echo "[agent-branch-start] Unknown option: $1" >&2
echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>] [--print-name-only]" >&2
echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>] [--reuse-existing|--new] [--print-name-only]" >&2
exit 1
;;
*)
Expand All @@ -90,7 +99,7 @@ done

if [[ "${#POSITIONAL_ARGS[@]}" -gt 3 ]]; then
echo "[agent-branch-start] Too many positional arguments." >&2
echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>]" >&2
echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>] [--reuse-existing|--new]" >&2
exit 1
fi

Expand Down Expand Up @@ -254,6 +263,7 @@ normalize_bool() {
}

OPENSPEC_AUTO_INIT="$(normalize_bool "$OPENSPEC_AUTO_INIT_RAW" "1")"
REUSE_EXISTING_WORKTREE="$(normalize_bool "$REUSE_EXISTING_RAW" "1")"

normalize_tier() {
local raw="${1:-}"
Expand Down Expand Up @@ -370,6 +380,22 @@ resolve_worktree_leaf() {
printf '%s' "${branch_name//\//__}"
}

print_reused_agent_worktree() {
local branch_name="$1"
local worktree_path="$2"

echo "[agent-branch-start] Reusing existing branch: ${branch_name}"
echo "[agent-branch-start] Worktree: ${worktree_path}"
echo "[agent-branch-start] OpenSpec tier: ${OPENSPEC_TIER}"
echo "[agent-branch-start] OpenSpec change: existing worktree"
echo "[agent-branch-start] OpenSpec plan: existing worktree"
echo "[agent-branch-start] Next steps:"
echo " cd \"${worktree_path}\""
echo " gx locks claim --branch \"${branch_name}\" <file...>"
echo " # continue work in this existing sandbox"
echo " gx branch finish --branch \"${branch_name}\" --via-pr --wait-for-merge"
}

has_local_changes() {
local root="$1"
if ! git -C "$root" diff --quiet; then
Expand Down Expand Up @@ -550,6 +576,12 @@ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
exit 1
fi

current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
if [[ "$REUSE_EXISTING_WORKTREE" -eq 1 && "$current_branch" == agent/* ]]; then
print_reused_agent_worktree "$current_branch" "$repo_root"
exit 0
fi

task_slug="$(sanitize_slug "$TASK_NAME" "task")"
agent_slug="$(normalize_role "$AGENT_NAME")"
if [[ "$WORKTREE_ROOT_EXPLICIT" -eq 0 ]]; then
Expand Down
7 changes: 4 additions & 3 deletions src/cli/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -408,8 +408,9 @@ function runSetupBootstrapInternal(options) {
}

function extractAgentBranchStartMetadata(output) {
const branchMatch = String(output || '').match(/^\[agent-branch-start\] Created branch: (.+)$/m);
const worktreeMatch = String(output || '').match(/^\[agent-branch-start\] Worktree: (.+)$/m);
const outputText = String(output || '');
const branchMatch = outputText.match(/^\[agent-branch-start\] (?:Created branch|Reusing existing branch): (.+)$/m);
const worktreeMatch = outputText.match(/^\[agent-branch-start\] Worktree: (.+)$/m);
return {
branch: branchMatch ? branchMatch[1].trim() : '',
worktreePath: worktreeMatch ? worktreeMatch[1].trim() : '',
Expand Down Expand Up @@ -3466,7 +3467,7 @@ function pivot(rawArgs) {
}
const stdoutText = String(result.stdout || '');
const wtMatch = stdoutText.match(/^\[agent-branch-start\] Worktree:\s+(.+)$/m);
const branchMatch = stdoutText.match(/^\[agent-branch-start\] Created branch:\s+(.+)$/m);
const branchMatch = stdoutText.match(/^\[agent-branch-start\] (?:Created branch|Reusing existing branch):\s+(.+)$/m);
if (wtMatch) {
const wtPath = wtMatch[1].trim();
process.stdout.write('\n');
Expand Down
5 changes: 3 additions & 2 deletions src/sandbox/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ function assertProtectedMainWriteAllowed(options, commandName) {
}

function extractAgentBranchStartMetadata(output) {
const branchMatch = String(output || '').match(/^\[agent-branch-start\] Created branch: (.+)$/m);
const worktreeMatch = String(output || '').match(/^\[agent-branch-start\] Worktree: (.+)$/m);
const outputText = String(output || '');
const branchMatch = outputText.match(/^\[agent-branch-start\] (?:Created branch|Reusing existing branch): (.+)$/m);
const worktreeMatch = outputText.match(/^\[agent-branch-start\] Worktree: (.+)$/m);
return {
branch: branchMatch ? branchMatch[1].trim() : '',
worktreePath: worktreeMatch ? worktreeMatch[1].trim() : '',
Expand Down
2 changes: 1 addition & 1 deletion templates/scripts/agent-branch-merge.sh
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ if [[ -z "$TARGET_BRANCH" ]]; then
fi

printf '%s\n' "$start_output"
TARGET_BRANCH="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Created branch: //p' | head -n 1)"
TARGET_BRANCH="$(printf '%s\n' "$start_output" | sed -n -E 's/^\[agent-branch-start\] (Created branch|Reusing existing branch): //p' | head -n 1)"
target_worktree="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Worktree: //p' | head -n 1)"
if [[ -z "$TARGET_BRANCH" || -z "$target_worktree" ]]; then
echo "[agent-branch-merge] Unable to parse target branch/worktree from agent-branch-start output." >&2
Expand Down
36 changes: 34 additions & 2 deletions templates/scripts/agent-branch-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ OPENSPEC_CHANGE_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CHANGE_SLUG:-}"
OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CAPABILITY_SLUG:-}"
OPENSPEC_MASTERPLAN_LABEL_RAW="${GUARDEX_OPENSPEC_MASTERPLAN_LABEL-masterplan}"
OPENSPEC_TIER_RAW="${GUARDEX_OPENSPEC_TIER:-T3}"
REUSE_EXISTING_RAW="${GUARDEX_BRANCH_START_REUSE_EXISTING:-true}"
PRINT_NAME_ONLY=0
POSITIONAL_ARGS=()

Expand Down Expand Up @@ -58,6 +59,14 @@ while [[ $# -gt 0 ]]; do
OPENSPEC_TIER_RAW="${2:-$OPENSPEC_TIER_RAW}"
shift 2
;;
--reuse-existing|--reuse)
REUSE_EXISTING_RAW="true"
shift
;;
--new|--no-reuse|--no-reuse-existing)
REUSE_EXISTING_RAW="false"
shift
;;
--in-place|--allow-in-place)
echo "[agent-branch-start] In-place branch mode is disabled." >&2
echo "[agent-branch-start] This command always creates an isolated worktree to keep your active checkout unchanged." >&2
Expand All @@ -78,7 +87,7 @@ while [[ $# -gt 0 ]]; do
;;
-*)
echo "[agent-branch-start] Unknown option: $1" >&2
echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>] [--print-name-only]" >&2
echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>] [--reuse-existing|--new] [--print-name-only]" >&2
exit 1
;;
*)
Expand All @@ -90,7 +99,7 @@ done

if [[ "${#POSITIONAL_ARGS[@]}" -gt 3 ]]; then
echo "[agent-branch-start] Too many positional arguments." >&2
echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>]" >&2
echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>] [--reuse-existing|--new]" >&2
exit 1
fi

Expand Down Expand Up @@ -254,6 +263,7 @@ normalize_bool() {
}

OPENSPEC_AUTO_INIT="$(normalize_bool "$OPENSPEC_AUTO_INIT_RAW" "1")"
REUSE_EXISTING_WORKTREE="$(normalize_bool "$REUSE_EXISTING_RAW" "1")"

normalize_tier() {
local raw="${1:-}"
Expand Down Expand Up @@ -370,6 +380,22 @@ resolve_worktree_leaf() {
printf '%s' "${branch_name//\//__}"
}

print_reused_agent_worktree() {
local branch_name="$1"
local worktree_path="$2"

echo "[agent-branch-start] Reusing existing branch: ${branch_name}"
echo "[agent-branch-start] Worktree: ${worktree_path}"
echo "[agent-branch-start] OpenSpec tier: ${OPENSPEC_TIER}"
echo "[agent-branch-start] OpenSpec change: existing worktree"
echo "[agent-branch-start] OpenSpec plan: existing worktree"
echo "[agent-branch-start] Next steps:"
echo " cd \"${worktree_path}\""
echo " gx locks claim --branch \"${branch_name}\" <file...>"
echo " # continue work in this existing sandbox"
echo " gx branch finish --branch \"${branch_name}\" --via-pr --wait-for-merge"
}

has_local_changes() {
local root="$1"
if ! git -C "$root" diff --quiet; then
Expand Down Expand Up @@ -550,6 +576,12 @@ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
exit 1
fi

current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
if [[ "$REUSE_EXISTING_WORKTREE" -eq 1 && "$current_branch" == agent/* ]]; then
print_reused_agent_worktree "$current_branch" "$repo_root"
exit 0
fi

task_slug="$(sanitize_slug "$TASK_NAME" "task")"
agent_slug="$(normalize_role "$AGENT_NAME")"
if [[ "$WORKTREE_ROOT_EXPLICIT" -eq 0 ]]; then
Expand Down
32 changes: 32 additions & 0 deletions test/branch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,38 @@ test('agent-branch-start prefers current protected branch over stale configured
});


test('agent-branch-start reuses the current agent worktree instead of cloning it', () => {
const { repoDir } = createBootstrappedRepo({ committed: true });

let result = runBranchStart(['--tier', 'T1', 'rust repair snapshot selection', 'bot'], repoDir, {
GUARDEX_OPENSPEC_AUTO_INIT: 'true',
});
assert.equal(result.status, 0, result.stderr || result.stdout);
const firstBranch = extractCreatedBranch(result.stdout);
const firstWorktree = extractCreatedWorktree(result.stdout);

result = runBranchStart(['--tier', 'T1', 'continue rust worktree', 'bot'], firstWorktree, {
GUARDEX_OPENSPEC_AUTO_INIT: 'true',
});
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.match(result.stdout, new RegExp(`Reusing existing branch: ${escapeRegexLiteral(firstBranch)}`));
assert.equal(extractCreatedWorktree(result.stdout), firstWorktree);
assert.equal(
fs.existsSync(path.join(firstWorktree, '.omx', 'agent-worktrees')),
false,
'branch start inside an agent worktree must not create nested worktrees',
);

const worktreeList = runCmd('git', ['worktree', 'list', '--porcelain'], repoDir);
assert.equal(worktreeList.status, 0, worktreeList.stderr || worktreeList.stdout);
assert.equal(
(worktreeList.stdout.match(/^branch refs\/heads\/agent\//gm) || []).length,
1,
'only the original agent branch should remain registered',
);
});


test('agent-branch-start moves protected-branch local changes into the new agent worktree', () => {
const repoDir = initRepoOnBranch('main');
seedCommit(repoDir);
Expand Down