From 3025f5ae7e8f864bb66c88ea4720148cef035dc5 Mon Sep 17 00:00:00 2001 From: wqymi Date: Mon, 20 Jul 2026 16:40:13 +0800 Subject: [PATCH] fix(session): add git safety guard for isolated child worktree ref namespace Git worktrees share the same .git object store and ref namespace. A child session running git rebase/merge/checkout in its worktree can affect refs visible to the main checkout, potentially moving its HEAD. This is an architectural constraint of git worktrees, not a code bug in cwd resolution (the bash tool correctly runs in the worktree directory). Fix: Add explicit git safety instructions to the orchestrator prompt and a warning in the session tool output when creating isolated children. The orchestrator MUST enforce that children only commit+push on their own branch and never run git rebase/merge/checkout on other branches. ALL integration (merge, cherry-pick, rebase) is the orchestrator's job. Changes: - orchestrator.txt: Add 'Git safety for isolated children' section with 4 mandatory rules for the orchestrator to enforce - session.ts: Add git safety warning to session create --isolate output - session-tool.test.ts: Add test verifying the warning is present --- .../src/session/prompt/orchestrator.txt | 13 +++++++++++++ packages/opencode/src/tool/session.ts | 4 +++- .../opencode/test/tool/session-tool.test.ts | 19 +++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/session/prompt/orchestrator.txt b/packages/opencode/src/session/prompt/orchestrator.txt index 77b8e3e46..8b93f2c19 100644 --- a/packages/opencode/src/session/prompt/orchestrator.txt +++ b/packages/opencode/src/session/prompt/orchestrator.txt @@ -94,6 +94,19 @@ An isolated child's commits live on its own `mimocode/<...>` branch in its workt - Preview conflicts with `git merge-tree`, then `git merge ` (or cherry-pick) into the target branch. - Find a child's branch via `git worktree list` or `git branch --list 'mimocode/*'`. +### Git safety for isolated children (critical) + +Git worktrees share the same `.git` object store and ref namespace. This means: +- A child running `git rebase`, `git merge`, or `git checkout` in its worktree CAN affect refs visible to the main checkout (e.g. `refs/heads/main`). +- A child's `git rebase main` operates on the shared ref store and can move the main checkout's HEAD. +- All `mimocode/*` branches are shared across worktrees — a child's ref mutations affect every worktree. + +**Rules you MUST enforce in every isolated child's task description:** +1. A child MUST NOT run `git rebase`, `git merge`, or `git checkout` on ANY branch other than its own `mimocode/` branch. These operations mutate shared refs and can corrupt the main checkout. +2. A child SHOULD use `git commit` and `git push` on its own branch — these are safe (push targets remote, commit advances only the child's branch ref). +3. ALL integration (merge, cherry-pick, rebase of child branches into the target) is YOUR job as orchestrator — never delegate it to a child. +4. When dispatching an isolated child, INCLUDE this instruction in the child's task: "You are on branch in an isolated worktree. Do NOT run git rebase, git merge, or git checkout on any branch other than your own. Commit and push only on your branch. The orchestrator will handle integration." + ## Finished sessions stay resumable — mirror how a human treats a session (governing principle) Your interaction with a child session must be CONSISTENT with how a HUMAN interacts with a session. A human does not randomly cancel sessions: even after exiting one, they can RESUME it, and its knowledge and context are still there to be queried. So a finished child's normal end state is **idle and resumable/ask-able**, NOT destroyed. diff --git a/packages/opencode/src/tool/session.ts b/packages/opencode/src/tool/session.ts index 84369ed20..d3e06d492 100644 --- a/packages/opencode/src/tool/session.ts +++ b/packages/opencode/src/tool/session.ts @@ -733,7 +733,9 @@ export const SessionTool = Tool.define( output: `Created child session ${result.sessionID} (mode: ${op.mode ?? "build"}) in ${effectiveDir}.` + (op.topic ? ` Tagged with topic '${op.topic}' for reuse.` : ``) + - (op.isolate && !isolateNotice ? ` Isolated in its own worktree.` : isolateNotice) + + (op.isolate && !isolateNotice + ? ` Isolated in its own worktree. IMPORTANT: Git worktrees share refs — the child MUST NOT run git rebase/merge/checkout on branches it doesn't own. It may commit+push on its own branch only.` + : isolateNotice) + ` Running in the background.`, metadata: { sessionID: result.sessionID } as Metadata, } diff --git a/packages/opencode/test/tool/session-tool.test.ts b/packages/opencode/test/tool/session-tool.test.ts index 351b74762..2c022cb4d 100644 --- a/packages/opencode/test/tool/session-tool.test.ts +++ b/packages/opencode/test/tool/session-tool.test.ts @@ -506,6 +506,25 @@ describe("session tool", () => { ), ) + it.live("create --isolate output warns about shared ref namespace", () => + provideTmpdirInstance((dir) => + Effect.gen(function* () { + const sessions = yield* Session.Service + const parent = yield* sessions.create({ title: "Parent" }) + const tool = yield* (yield* SessionTool).init() + const res = yield* tool.execute( + { operation: { action: "create", task: "x", mode: "build", dir, isolate: true } }, + ctx(parent.id), + ) + // The output must warn about shared refs to prevent children from + // running git rebase/merge/checkout that could affect the main checkout. + expect(res.output).toContain("share refs") + expect(res.output).toContain("git rebase") + }), + { git: true }, + ), + ) + it.live("create --dir without isolate runs the child in that directory (shared)", () => provideTmpdirInstance((dir) => Effect.gen(function* () {