| name | unity-batchmode-worktree |
| description | Use when collaborating on a Unity project where the user keeps the Editor open in one git worktree and you work in a second via batch mode. Triggers: running Unity batch-mode compile/test commands, triaging Editor errors, editing Assets files, merging between user's branch and yours, stray test-log.txt / TestResults*.xml / Unity logs in git status. |
Unity Batch Mode in a Git Worktree
Overview
Unity locks Library/ per-Editor, so two Editor processes can't share a project. The workaround: user keeps the Editor open on one worktree (main), you work in a sibling worktree (dev) and run Unity in -batchmode for compile/test. Work flows both directions via git.
Core principle: both worktrees are shared, but at any moment one side may be locked - mid-play-mode, mid-batch-run, or holding uncommitted edits. Writing to a locked worktree clobbers state or breaks a test. Your job: know the lock state before you write. If unsure, ask.
When to Use
- Any Unity project with a sibling
<project>/ + <project>2/ worktree layout
- Running compile checks, EditMode/PlayMode tests, or
-executeMethod via batch mode
- Triaging an error the user saw in their live Editor
- Committing Unity-serialized assets (
.asset, .prefab, .unity, ProjectSettings/*.asset)
- Syncing between branches
Layout
<ProjectRoot>/
├── <project>/ <- user's worktree, branch `main`, Editor lives here
│ └── <project>/ <- Unity project root
└── <project>2/ <- your worktree, branch `dev`, batch mode
├── <project>/ <- Unity project root (mirror)
└── TestResults/ <- gitignored home for ALL batch-mode output
dev is a scratch branch, not long-lived. It gets squash-merged into main at sync points and immediately reset.
The inner <project>/ nesting shown above depends on where the Unity project root sits inside the repo - some repos put the Unity project at the repo root instead, in which case each worktree is the Unity project root with no extra nesting (see the flat <project>2/, <project>3/ naming under Parallel Agents below). Check for Assets//ProjectSettings/ directly under the worktree root before assuming a nested layout.
Lock State
The Editor is open the entire session, so "Editor running" is not a signal. Lock state = git state + conversation.
User's worktree is LOCKED if any of these:
git status in their tree shows uncommitted changes that aren't yours
- They said "hold on", "I'm testing", "don't touch anything", or equivalent
- Default when no handoff has been established this turn - absence of permission is a lock
Handed off (safe to write) only when all hold:
- Explicit authorization this turn ("merge it", "go ahead", "I'm ready")
- Their
git status is clean, or dirty state is approved-to-overwrite
- They haven't since said anything that re-locks
Authorization is per-action, not a standing license. "Yes, merge now" covers the merge, not the next write an hour later. If unsure, ask - a clarifying question is cheap, clobbering a test session is not.
Your worktree: yours by default, but before a long batch run tell the user "batch run in my worktree, ~Nm, don't edit there until I report back." Never start a second Unity process against a worktree while one is running - Library lock fails hard. Track background shell ids.
Sync Workflow - Squash-Merge and Reset
- Commit freely on
dev during a session. Granularity doesn't matter, it all squashes. Small commits are a working tool for your own rollback/bisect, not history the user has to review.
- At a sync point, the user squash-merges in their worktree:
git merge --squash dev && git commit -e. Produces one commit on main, authored by them, with a message they approve.
- Immediately reset
dev: in your worktree, git fetch && git reset --hard main. Old agent commits are orphaned and GC'd. This is expected.
- User → you sync uses the same reset. Never merge
main into dev - always reset.
Mid-session rollback only works before the squash. If you need a pre-squash checkpoint to revisit later, tell the user so they can stash it (tag, branch, patch).
Squash commit message
Lead with the user-facing change. Put agent-specific context in an Agent notes: trailer - design decisions, things tried and discarded, convention fix-ups, known follow-ups. Propose the message before they commit.
Implement full combat system (melee, projectile, dash, block, heal, nuke)
Agent notes:
- Reworked vitals server-authoritative; damage/heal sync via ClientRpc
- Added Projectile NetworkBehaviour + prefab, registered in DefaultNetworkPrefabs
- Tuned Strength on all damaging ability SOs
- Known follow-up: invisibility toggles all renderers, should exclude local view
Sync-point checklist
- Confirm lock state on user's worktree
git log --oneline main..dev and dev..main - read subject lines. Overlap = user may have manually copied work, your commits may be redundant
- Produce proposed squash message with
Agent notes: trailer
- User executes
git merge --squash dev && git commit -e
- Resolve asset conflicts per "Committing Unity Assets" below. Prefer user's on-disk prefab for
fileID references
- If
.cs files changed, batch-mode compile in your worktree before the reset (while dev commits still exist to diff)
git fetch && git reset --hard main in your worktree. Verify git log --oneline main..dev is empty
- Re-run pre-session divergence check before the next write
Pre-Session Checklist
- Read
<user-worktree>/<project>/AGENTS.md - project conventions (naming, architecture, test rules). Skipping it has caused convention-violation cleanup commits in past sessions.
git status in BOTH worktrees. Unstaged changes you didn't make this session = STOP and ask.
git log --oneline main..dev and dev..main. Know how the branches have diverged.
- Confirm
<your-worktree>/TestResults/ exists and is gitignored.
Batch Mode Commands
Route ALL output to <your-worktree>/TestResults/ with descriptive timestamped names - never Unity's default log path, never the Unity project root. See batch-mode-commands.md for copy-paste command templates (compile check, EditMode, PlayMode, executeMethod) and runtime expectations.
Rule of thumb: cold compile 30–120s, EditMode 60–180s, PlayMode 120–300s. Use run_in_background with a recorded shell id.
Reading the User's Editor Log
The user's live Editor log is external to the worktree:
- Windows:
%LOCALAPPDATA%/Unity/Editor/Editor.log (current) + Editor-prev.log
- macOS:
~/Library/Logs/Unity/Editor.log
- Linux:
~/.config/unity3d/Editor.log
tail -n 200 is usually enough - errors are near the end.
Committing Unity Assets
Unity serializes as YAML with guid/fileID references. Pitfalls:
- LF → CRLF churn. ProjectSettings and
.asset files may show as modified with only line-ending changes. git diff -w <file> - if empty, git checkout -- it. Don't commit whitespace noise.
fileID conflicts on merges. Both branches adding the same Resources-loaded SO with different fileIDs → the fileIDs point to different components inside the same prefab. grep -E '&[0-9]+|!u!' <prefab> to see the prefab's internal IDs; match the fileID to the component type the field expects (e.g. a MyProjectile field needs the MonoBehaviour fileID, not the root GameObject).
- Meta files. Stage
<file> + <file>.meta together. Never one without the other.
- Never commit test artifacts.
TestResults*.xml, test-log.txt, anything under TestResults/ stays gitignored.
Parallel Agents via Persistent Warm Worktrees
When dispatching parallel subagents on a Unity project, do not use the Agent tool's isolation: "worktree" option - it spins up a fresh worktree per agent, which means each one pays a 5–30 minute cold Library/ reimport before doing any work. The cost dwarfs any wall-clock savings from parallelism.
Instead, pre-provision a small pool of long-lived sibling worktrees, each with its own warm Library/, and have agents git checkout the branch they need:
<ProjectRoot>/
├── <project>/ <- user's, branch main, warm Library
├── <project>2/ <- yours, branch dev, warm Library
├── <project>3/ <- agent pool slot, warm Library
└── <project>4/ <- agent pool slot, warm Library
Each pool slot is created once with git worktree add, opened in Unity once to populate Library/, and then reused across sessions. Agents do git fetch && git checkout <branch> inside their assigned slot - Unity reimports only the files that actually changed, which is fast.
Rules for the pool:
- One agent per slot at a time. The Unity Library lock still applies - never run two batch-mode processes against the same slot.
- Agents must reset/clean their slot before checking out a new branch (
git reset --hard && git clean -fd) so leftover state from a previous session doesn't leak.
- Don't delete the pool slots between sessions. The whole point is keeping
Library/ warm.
- Plans that branch into truly independent tasks benefit most. Plans with hard sequential dependencies (Task N+1 imports symbols Task N defines) should still run sequentially in a single worktree - parallelism buys nothing there.
Reserving a warm worktree
Pool slots are shared across sessions. Before doing anything in a slot, claim it with a reservation marker so a parallel session (or future-you) doesn't pick the same slot and stomp on in-progress work. The marker is the baseline mechanism and a complete protocol on its own - survey, claim, prep, release - with no other tooling required.
If the project-lock skill is installed, layer it in as an optional extra step. The two are complementary, not redundant: .worktree-reserved is an advisory sticky note saying "this pool slot is spoken for", self-documenting and surviving across sessions, so an abandoned marker still leaves a breadcrumb; project-lock is stronger write coordination that other agents' tooling checks before editing. A slot can carry a stale-but-harmless marker with no lock at all - but when a lock does exist, the lock, not the marker, is the authority on whether it is currently safe to write.
-
Survey. git worktree list to enumerate slots; for each candidate check ls <slot>/.worktree-reserved and, if it exists, read its contents to see who holds it (reserved-by), when it was claimed (reserved-at), and whether it's past its own stale-after window; also check git -C <slot> status -sb. A slot is free if there's no marker, or the marker is past stale-after with no live owner (see the reap note below) - and the working tree is clean. If the project-lock skill is installed, also check for an active lock on the slot (python <project-lock script> check <slot>, or the absence of a <slot>/.agent-lock/ directory) and treat a locked slot as not free even if it has no marker. Detached HEAD at an older commit is fine - that's the warm-pool resting state.
-
Claim. Write <slot>/.worktree-reserved with content, not just an empty touch, so the marker is self-documenting - identifying who reserved the slot and leaving a breadcrumb if the session is abandoned:
worktree pool-slot reservation (unity-batchmode-worktree skill)
reserved-by: <agent harness and session id, e.g. "opencode session 7f3a9c12">
reserved-at: <ISO 8601 UTC timestamp, e.g. 2026-08-04T21:40:00Z>
branch: <branch about to be checked out>
task: <plan/task reference>
stale-after: 24h - if this is older and the owning session is gone, it is safe to delete
reserved-by is agent-agnostic: any harness name plus its session/instance id (claude-code session 83c40206, opencode session 7f3a9c12), or user@host for a manual, non-agent reservation. reserved-at is always ISO 8601 UTC in YYYY-MM-DDTHH:MM:SSZ form, so markers compare correctly across machines in different time zones.
Filled-in example:
worktree pool-slot reservation (unity-batchmode-worktree skill)
reserved-by: opencode session 7f3a9c12
reserved-at: 2026-08-04T21:40:00Z
branch: fix/worktree-reserved-marker
task: finalize .worktree-reserved marker spec across three PRs
stale-after: 24h - safe to delete if reserved-at is older and the owning session is gone
One short file. Never stage or commit it (add .worktree-reserved to .git/info/exclude if it isn't already globally ignored). This step can happen before or independently of acquiring a lock - creating the marker doesn't touch the slot's tracked working tree.
Reaping a stale marker. If you find one whose reserved-at is older than its stale-after (default 24h) with no activity on its named branch and no live project-lock on that slot (or no project-lock installed at all), the reserved-by session is probably gone; reap it - delete the marker and treat the slot as free - but flag the reap to the user before overwriting the slot's working tree.
Gotchas
sed -i on Git Bash for Windows silently empties files. Use the Edit tool for in-place text replacement.
- Never delete
Library/, Temp/, or UserSettings/ - machine-local caches; deleting Library/ forces a 30-minute reimport.
Quick Reference
| Task | Do | Don't |
|---|
| Check user's worktree lock state | git status in their tree + recent conversation; ask if unsure | Rely on Library/UnityLockfile (Editor is always open) |
| Write to user's worktree | Only with explicit this-turn authorization | Treat prior authorization as standing |
| Run Unity batch mode | -logFile <TestResults>/<name>.log, record shell id | Default log path, or second Unity against same worktree |
| Read user's Editor error | tail the external Editor.log | Grep the user's source to localize an error |
| Sync dev ↔ main | Squash-merge then git reset --hard main on dev | Long-lived dev, rebases, force-pushes, git merge main into dev |
| In-place text replace | Edit tool | sed -i on Windows Git Bash |
| Commit LF/CRLF-only diff | git checkout -- to discard | Commit the churn |
Resolve fileID conflict | Match fileID → component type in prefab YAML | Pick one side blindly |
| New asset | Stage <file> + <file>.meta | Commit one without the other |
| Start a session | Read AGENTS.md, git status both trees, check divergence | Start editing blind |