Skip to content

Development worktrees

Implementation work happens in a git worktree, not in the primary checkout. The repository enforces this: a reference-transaction hook blocks direct feature-branch creation. The workflow exists so several agents (and several humans) can edit the same repository concurrently without stepping on each other's files or branches.

The driver is scripts/mini_ork_worktree.py; the make targets wrap it.

The loop

flowchart LR
    C[make worktree SLUG=x] --> W[edit + commit in the worktree]
    W --> M[make worktree-merge SLUG=x]
    M --> R[rebase origin/main]
    R --> G[green gate]
    G --> P[push HEAD:main]
    P --> F[fetch + ff-only local main]
    F --> D[make worktree-clean SLUG=x]
make install-hooks                    # once per clone; activates .githooks/
make worktree SLUG=my-change          # create worktree + branch
#   ... edit and commit inside the worktree ...
make worktree-merge SLUG=my-change    # rebase, gate, push to main
make worktree-clean SLUG=my-change    # remove worktree, delete branch, release claims

Worktrees live under /Volumes/docker-ssd/ps/mini-ork-worktrees/<slug> by default. Override the parent directory with MINI_ORK_WORKTREES_DIR. Branches are named wt/<slug>.

What create does

  1. Takes a lock around the claim check and registration.
  2. Refuses if the slug already has a live worktree or if a requested ownership claim overlaps one already held.
  3. Creates the worktree and branch from the current origin/main.
  4. Records ownership claims in the worktree's .ownership TSV.
make worktree SLUG=retry-fix OWNS="mini_ork/dispatch/retry.py tests/unit/test_retry.py"

--owns (repeatable) claims a file surface. Claims are path-prefix aware: a claim on mini_ork/dispatch overlaps a claim on mini_ork/dispatch/retry.py. A second worktree whose claim overlaps a live one is refused, so two agents cannot race the same file. Claims are released by clean, by release, or automatically when the worktree directory disappears.

Inspecting claims

python3 scripts/mini_ork_worktree.py owners
python3 scripts/mini_ork_worktree.py owners --json
python3 scripts/mini_ork_worktree.py list
python3 scripts/mini_ork_worktree.py release <slug>

make wraps only create, merge and clean; owners, release and list are invoked through the driver directly.

owners shows which slug holds which path. When a worktree is abandoned, its claims block new work until released; check owners before assuming a path is free.

What merge does

  1. Takes the same lock.
  2. Fetches origin/main — the fetch happens inside the critical section, so a rebase never targets a ref that went stale during a long gate.
  3. Rebases the branch onto origin/main.
  4. Runs the green gate outside the lock: python3 -m pytest -q by default.
  5. Pushes HEAD:main with a bounded retry (MO_MERGE_PUSH_ATTEMPTS, default 5).

The gate runs outside the lock so a long suite does not block another worktree command. The push retry handles a concurrent push: after a rejected push the driver re-fetches and rebases onto the moved main inside the lock, then pushes again. The gate is deliberately not re-run after that rebase — re-gating reopens the very window it was meant to close. The accepted trade-off is that a late rebase can combine your branch with upstream commits that landed during the gate; those upstream commits were gated when they landed.

Scoping the gate

Scope the gate per task with MINI_ORK_TEST_CMD:

MINI_ORK_TEST_CMD="python3 -m pytest tests/unit/test_retry.py -q" \
  make worktree-merge SLUG=retry-fix

A scoped gate is evidence for the files it ran, not for the repository. Use it when the change is genuinely local; run the broader unit layer for changes to shared modules. See Testing.

Merge discipline

The mechanical part is automated; these rules are the part that still goes wrong.

Stage explicit paths

Never git add -A or git add .. Either sweeps other sessions' work into your commit. Stage the files you changed:

git add mini_ork/dispatch/retry.py tests/unit/test_retry.py
git commit -m "retry: bound the backoff"

git add <dir> is acceptable only for a directory you own end to end.

Check the diff before committing

git diff --stat

If the diff contains files you never touched — especially deletions of files you did not author — your base is stale. Rebase; do not merge your way past it.

Refresh local main after a merge

push HEAD:main updates the remote; your local main stays where it was. Run:

git fetch origin main
git merge --ff-only origin/main

A worktree created from a stale local main starts behind and reintroduces the divergence.

If --ff-only refuses, stop

That refusal means local main and origin/main diverged — someone pushed while you worked. Do not run a plain git merge origin/main, do not git reset --hard, do not bypass hooks. Reconcile so every commit on both sides survives. A merge commit keeps all of them; a rebase rewrites them, and you must never rewrite a commit that is not yours.

Fix forward, never revert main

If a post-merge smoke check fails, fix it from a new worktree off the latest main. Do not revert main and do not reset it.

Cleaning up

make worktree-clean SLUG=my-change

This removes the worktree directory, deletes the local branch, and releases the slug's claims. It does not push anything; merge first.

Clean removes the directory

clean deletes the worktree. Commit and merge first, or copy out anything you need. Do not clean a worktree while a run is using it — the run's engine pointer and artifacts live under that path.

Concurrency and locking

The driver takes an mkdir lock — $MINI_ORK_WORKTREE_LOCK, default $TMPDIR/mini-ork-worktree.lockdir — around two check-then-act pairs:

  • claim check plus registration on create;
  • fetch, rebase and push on merge.

It waits up to 30 seconds for the lock. Two concurrent agents therefore cannot both pass an ownership check or interleave a stale push. If the lock times out, another worktree command is running; wait and retry rather than deleting the lock directory.

The lock does not cover your edit-commit loop. It covers the operations that read and write shared refs.

Working inside the worktree

A worktree is a full checkout with its own working tree but a shared git object store. Two consequences:

  • Do not run git stash inside a worktree. Stashes are shared across worktrees, and a stash from one branch can unexpectedly apply in another. Commit or discard; do not stash.
  • The branch is shared, the files are not. Editing files in one worktree does not change another's checkout, but committing on wt/<slug> is visible to anyone who checks out that branch.

Each worktree may also have its own .mini-ork/ home. That is the point: two runs and two gates do not share a state database or run directory.

When create is refused

Message Cause Fix
claim overlaps Another live worktree owns that path Finish or release it, or narrow your claim
worktree exists The slug is already live Use the existing one or pick a new slug
reference-transaction blocked Hooks are not installed or a direct branch was attempted make install-hooks; create through make worktree
lock timeout Another worktree command is running Wait and retry

Shipping

Per the repository's operating rules: a finished, verified change is committed, merged and pushed — not left parked in a worktree. Commit and merge without being asked, and keep main clean.

Next