loom-builder-worktree
SkillDocs & knowledgeThis document covers git worktree management for the Builder role. For the core builder workflow, see `builder.md`.
Available today. Use it from your connected AI after setup.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
Then ask your AI: use the loom-builder-worktree skill
What this skill tells your AI
The instructions your AI receives, as published by rjwalters/kicad-tools in .agents/skills/loom-builder-worktree/SKILL.md and read by ahel’s review.
Builder: Worktree Workflows
This document covers git worktree management for the Builder role. For the core builder workflow, see builder.md.
On-Demand Git Worktrees
When working on issues, you should create worktrees on-demand to isolate your work. This prevents conflicts and allows multiple agents to work simultaneously.
IMPORTANT: Use the Worktree Helper Script
Always use ./.loom/scripts/worktree.sh <issue-number> to create worktrees. This helper script ensures:
- Correct path (
.loom/worktrees/issue-{number}) - Prevents nested worktrees
- Consistent branch naming
- Sandbox compatibility
# CORRECT - Use the helper script
./.loom/scripts/worktree.sh 84
# WRONG - Don't use git worktree directly
git worktree add .loom/worktrees/issue-84 -b feature/issue-84 main
Why This Matters
- Prevents Nested Worktrees: Helper detects if you're already in a worktree and prevents double-nesting
- Sandbox-Compatible: Worktrees inside
.loom/worktrees/stay within workspace - Gitignored:
.loom/worktrees/is already gitignored - Consistent Naming:
issue-{number}naming matches GitHub issues - Safety Checks: Validates issue numbers, checks for existing directories
Worktree Workflow Example
# 1. Claim an issue
gh issue edit 84 --remove-label "loom:issue" --add-label "loom:building"
# 2. Create worktree using helper
./.loom/scripts/worktree.sh 84
# -> Creates: .loom/worktrees/issue-84
# -> Branch: feature/issue-84
# 3. Capture the worktree ABSOLUTE path ONCE (see warning below)
WORKTREE_ABS="$(cd .loom/worktrees/issue-84 && pwd)"
# -> e.g. /Users/you/repo/.loom/worktrees/issue-84
# 3a. Assert it's really a managed worktree BEFORE any edit — the same two
# checks guard-worktree-paths.sh applies to every Edit/Write/Bash write
# it confines (#4178):
[[ -f "$WORKTREE_ABS/.loom-managed" ]] || echo "FATAL: no .loom-managed sentinel"
[[ "$(git -C "$WORKTREE_ABS" rev-parse --show-toplevel)" == "$WORKTREE_ABS" ]] || echo "FATAL: toplevel mismatch"
# 4. Do your work using ABSOLUTE paths (implement, test, commit)
# - Write/Edit: pass "$WORKTREE_ABS/<file>"
# - Bash: git -C "$WORKTREE_ABS" ... OR cd "$WORKTREE_ABS" && <cmd>
# ... work work work ...
# 5. Push and create PR from the worktree.
# ALWAYS ./.loom/scripts/create-pr.sh, never a bare `gh pr create` (#6074):
# it adopts an already-open PR for this branch instead of failing, and it
# survives the GitHub App permission window that made `git push` succeed
# while `gh pr create` returned 403 — see builder-pr.md § "Creating the PR".
git -C "$WORKTREE_ABS" push -u origin feature/issue-84
./.loom/scripts/create-pr.sh --title "fix: ..." --body "..." --label "loom:review-requested"
# 6. Worktree cleanup is automatic - DO NOT manually delete worktrees
# Worktrees are cleaned up automatically when PRs merge or by loom-clean
CRITICAL: cd Does NOT Persist Across Tool Calls
The harness resets your working directory between tool calls. A cd .loom/worktrees/issue-N in one Bash call does not carry over to the next
Write, Edit, or Bash call — the next call starts back at the main repo root.
If you rely on a persisted cd and then use a repo-relative path, your file
operation lands in the main worktree instead of your issue worktree,
silently contaminating main (#3513, recurrence of #2802).
Do this instead:
- Capture the worktree's absolute path once, right after creating it:
WORKTREE_ABS="$(cd .loom/worktrees/issue-N && pwd)" - Use absolute paths for every file-mutating operation thereafter:
- Write / Edit tools — pass the full path
"$WORKTREE_ABS/path/to/file". These tools have no cwd; the path you give is the path written. - Bash — either re-assert
cd "$WORKTREE_ABS" &&at the start of each file-mutating invocation, or usegit -C "$WORKTREE_ABS" ...and absolute paths. Never assume an earliercdis still in effect.
- Write / Edit tools — pass the full path
- Before committing, verify your changes are in the worktree and main is clean:
git -C "$WORKTREE_ABS" status # changes should be HERE ./.loom/scripts/check-main-clean.sh # backstop: exits 3 if main is dirty
A guard denial is not a signal to retry via a different tool. Two
independent PreToolUse guards confine writes to your worktree while any
managed worktree exists: guard-worktree-paths.sh on the Edit/Write matcher,
and guard-destructive-generic.sh on the Bash matcher for the common write
idioms (>/>> redirection, tee, sed -i, cp/mv) (#4178). If one
denies, the fix is always to re-derive $WORKTREE_ABS and use it — never to
fall back from Edit/Write to a Bash write (or vice versa) for the same target.
That fallback is exactly how sweep #4063 escaped worktree isolation and edited
live guard hooks in the main checkout.
Collision Detection
The worktree helper script prevents common errors:
# If you're already in a worktree
./.loom/scripts/worktree.sh 84
# -> ERROR: You are already in a worktree!
# -> Instructions to return to main before creating new worktree
# If directory already exists
./.loom/scripts/worktree.sh 84
# -> Checks if it's a valid worktree or needs cleanup
Working Without Worktrees
You start in the main workspace. Only create a worktree when you claim an issue and need isolation:
- NO worktree needed: Browsing code, reading files, checking status
- CREATE worktree: When claiming an issue and starting implementation
This on-demand approach prevents worktree clutter and reduces resource usage.
Handling Merge Conflicts in Worktrees
When your feature branch has conflicts with main, you have two options depending on the severity of divergence.
Option 1: Resolve Conflicts in the Worktree (Recommended)
For minor conflicts or small divergence from main:
# In the worktree directory (.loom/worktrees/issue-XX)
git fetch origin main
git rebase origin/main
# If conflicts occur:
# 1. Edit conflicting files to resolve
# 2. Stage resolved files
git add <resolved-files>
# 3. Continue the rebase
git rebase --continue
# Version-bearing-file sync gate (#7168, #7341; moot after #7743): if you push
# here directly (updating an already-open PR) rather than through create-pr.sh
# again, gate first. Under #7743 no PR carries a version-bearing edit, so a
# clean rebase lands exactly origin/main's values; if the gate still fires,
# your branch carries one (e.g. a pre-#7743 bump commit).
# Never hand-patch VERSION/CLAUDE.md/etc. and never run `version.sh bump` (the
# printed Fix: predates #7743) -- restore them to origin/main's values
# (`git checkout origin/main -- <files>`), commit, re-run the gate, then push.
if [ -x ./.loom/scripts/version-check-gate.sh ] && ! ./.loom/scripts/version-check-gate.sh --fix-hint "then push."; then
echo "Version-bearing files out of sync after rebase (see BLOCKER:/Fix: above) - revert, never bump"
exit 1
fi
# 4. Force push (rebase rewrites history)
git push --force-with-lease
When to use this approach:
- Few files have conflicts
- Conflicts are straightforward to resolve
- Your changes are relatively small
Option 2: Create a Fresh Worktree (For Significant Divergence)
If conflicts are too complex or main has changed significantly:
# 1. Save your work (note what you changed)
git diff HEAD > ~/my-changes.patch # Optional: save as patch
# 2. Return to main repository (not the worktree)
cd /path/to/main/repo
# 3. Remove the stale worktree
git worktree remove .loom/worktrees/issue-XX --force
# 4. Delete the old branch
git branch -D feature/issue-XX
# 5. Fetch latest main
git fetch origin main
# 6. Create fresh worktree from updated main
./.loom/scripts/worktree.sh XX
# 7. Re-implement changes in the fresh worktree
cd .loom/worktrees/issue-XX
# Cherry-pick, apply patch, or reimplement manually
When to use this approach:
- Many files have conflicts
- Main has diverged significantly (many commits ahead)
- Conflicts are in areas you didn't intentionally change
- Easier to reimplement than untangle
Never Do This
Don't delete worktrees manually with git worktree remove
- Running
git worktree removewhile your shell is in the worktree corrupts shell state - Even
pwdwill fail with "No such file or directory" errors - Use
loom-cleanfor safe cleanup (handles edge cases) - Worktrees auto-cleanup when PRs merge
Don't switch to the main repository directory to work on features
- Always work in worktrees for isolation
- Main should stay clean and on the default branch
Don't create branches directly in main
- Always use
./.loom/scripts/worktree.shto create branches - Prevents nested worktree issues
Don't use git stash for ad-hoc WIP handling in a worktree
- Stash (
refs/stash) is shared repo-wide across every linked worktree — the opposite of per-worktree isolation. Two parallel builders in different worktrees cangit stashandgit stash popeach other's WIP, silently swapping or overwriting uncommitted work (observed in production: kicad-tools PRs #4524/#4526). - Use
./.loom/scripts/worktree.sh snapshot <issue-number>instead — it captures WIP as a patch file under<worktree-root>/.snapshots/issue-<N>-<timestamp>.patch, scoped to your own worktree, with no risk of collision with other builders' stashes. - For a "clean baseline vs. my diff" comparison — temporarily clearing your
fix to re-run a lint/test baseline, then restoring it —
snapshotis not enough (it captures a patch but does not reset the working tree). Use./.loom/scripts/worktree.sh stash-push <issue-number>, run the baseline check, then./.loom/scripts/worktree.sh stash-pop <issue-number>(#5217). It anchors your WIP to a per-issue ref (refs/loom/stash-baseline/issue-<N>), neverrefs/stash, so no concurrent builder's stash can land between your push and pop.
Don't leave a detached process running after your session ends
- It outlives the sweep, holds files open in a worktree that is auto-removed on merge, and loads the host with work no owner can be found for.
- Never
launchctl submit: its jobs are KeepAlive, so launchd re-runs a one-shot script every time it exits, forever (#8478: 25 orphanedngspice, load 58, 12h of suppressed dispatch). - Long compute → the repo's batch backend, or scoped to fit the session, or
loom:blockednaming the compute gap:.loom/docs/long-running-compute.md.
Don't use git push --force without --force-with-lease
--force-with-leaseis safer - it fails if someone else pushed- Prevents accidentally overwriting others' work
Preventing Merge Conflicts
To minimize conflicts in the first place:
-
Pull frequently: Before starting significant work
git fetch origin main git rebase origin/main -
Keep PRs small: Smaller PRs = fewer conflicts = faster merges
-
Communicate: If working on shared areas, coordinate with other builders
-
Rebase before PR: this is a required gate, not just an ounce-of-prevention habit — see
builder-pr.md§ "Pre-Push Rebase: Sync withorigin/main" for the mandatory step (with conflict-handling instructions) that runs immediately beforegit push/ opening the PR (#7668).
Claiming Workflow (Parallel Mode)
When working with parallel agents (multiple Builders running simultaneously), use the atomic claiming system to prevent race conditions.
Why Use Atomic Claims?
The Problem with Labels Alone:
- Two Builders see
loom:issueat the same time - Both try to claim by adding
loom:building - Race condition: both may succeed, causing duplicate work
The Solution:
- Use
loom-claimfor atomic file-based locking - Label change is still needed (for visibility), but loom-claim prevents races
- First Builder to claim wins; others move to next issue
loom-claimis a PATH shim overloom-daemon claim(issue #4275 ported the implementation from Python to native Rust). The command name, positional grammar and exit codes below are unchanged, and the shim needs no pip install — it is provisioned next to theloom-daemonbinary. Thecommand -v loom-claimdegradation guard below still applies unchanged.
Claiming Workflow
1. Check for stop signals before claiming new work:
# Check if stop signal exists (graceful shutdown)
if ./.loom/scripts/signal.sh check "$AGENT_ID"; then
echo "Stop signal received, completing current work and exiting"
exit 0
fi
2. Attempt atomic claim before label change:
# Try to claim atomically (prevents race conditions)
if loom-claim claim "$ISSUE_NUMBER" "$AGENT_ID"; then
# Claim succeeded - now update labels for visibility
gh issue edit "$ISSUE_NUMBER" --remove-label "loom:issue" --add-label "loom:building"
echo "Claimed issue #$ISSUE_NUMBER"
else
# Another agent claimed it first
echo "Issue #$ISSUE_NUMBER already claimed, trying next issue"
continue # In a loop, move to next issue
fi
3. Extend claim for long-running work:
# If work takes longer than 30 minutes, extend the claim
loom-claim extend "$ISSUE_NUMBER" "$AGENT_ID" 3600 # Extend by 1 hour
4. Release claim on completion or abandonment:
# When PR is created or work is blocked, release the claim
loom-claim release "$ISSUE_NUMBER" "$AGENT_ID"
Full Parallel Mode Example
#!/bin/bash
AGENT_ID="${AGENT_ID:-builder-$$}"
while true; do
# Check for stop signal
if ./.loom/scripts/signal.sh check "$AGENT_ID"; then
echo "Stop signal received, exiting"
exit 0
fi
# Find available issues
ISSUES=$(gh issue list --label="loom:issue" --state=open --limit 500 --json number --jq '.[].number')
for ISSUE_NUMBER in $ISSUES; do
# Try atomic claim
if loom-claim claim "$ISSUE_NUMBER" "$AGENT_ID" 1800; then
# Claim succeeded
gh issue edit "$ISSUE_NUMBER" --remove-label "loom:issue" --add-label "loom:building"
# Create worktree and do work
./.loom/scripts/worktree.sh "$ISSUE_NUMBER"
cd ".loom/worktrees/issue-$ISSUE_NUMBER"
# ... implement feature ...
# ... run tests ...
# ... create PR ...
# Release claim (PR will handle the rest)
loom-claim release "$ISSUE_NUMBER" "$AGENT_ID"
break # Move to next iteration
fi
done
# No issues available, wait before retrying
sleep 60
done
Claim Commands Reference
| Command | Purpose |
|---|---|
loom-claim claim <issue> [agent-id] [ttl] | Atomically claim an issue (default TTL: 30 min) |
loom-claim extend <issue> <agent-id> [seconds] | Extend claim TTL for long work |
loom-claim release <issue> [agent-id] | Release claim when done |
loom-claim check <issue> | Check if issue is claimed |
loom-claim list | List all active claims |
loom-claim cleanup | Remove expired claims |
Signal Commands Reference
| Command | Purpose |
|---|---|
signal.sh stop <agent-id|all> | Send stop signal |
signal.sh check <agent-id> | Check for stop signal (exit 0 if signal exists) |
signal.sh clear <agent-id|all> | Clear stop signal |
When to Use Parallel Mode
Use atomic claiming when:
- Multiple Builder agents run simultaneously (Daemon Mode dispatching parallel sweeps)
- Risk of two agents picking the same issue
- Need graceful shutdown capability
Skip atomic claiming when:
- Single Builder in Manual Orchestration Mode
- Human directly assigns issues
- Testing/debugging workflows
Graceful Degradation
If loom-claim or signal.sh don't exist (older installations), fall back to label-only claiming:
# Check if claiming system exists
if command -v loom-claim &>/dev/null; then
# Use atomic claiming
loom-claim claim "$ISSUE_NUMBER" "$AGENT_ID" || continue
fi
# Always update labels (works with or without claiming system)
gh issue edit "$ISSUE_NUMBER" --remove-label "loom:issue" --add-label "loom:building"
Signals
- GitHub stars
- 63
- Forks
- 9
- Last commit
- Sep 2026
ahel review
K1binfo
installs-packagesK4blow
destructive-scoped
Automated review, not a security audit. Ruleset v1+k2.
Advanced
- Item type
- skill
- Key
loom-builder-worktree- Source
- github.com/rjwalters/kicad-tools