loom-builder-worktree

SkillDocs & knowledge

This 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.

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

  1. Prevents Nested Worktrees: Helper detects if you're already in a worktree and prevents double-nesting
  2. Sandbox-Compatible: Worktrees inside .loom/worktrees/ stay within workspace
  3. Gitignored: .loom/worktrees/ is already gitignored
  4. Consistent Naming: issue-{number} naming matches GitHub issues
  5. 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:

  1. Capture the worktree's absolute path once, right after creating it:
    WORKTREE_ABS="$(cd .loom/worktrees/issue-N && pwd)"
    
  2. 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 use git -C "$WORKTREE_ABS" ... and absolute paths. Never assume an earlier cd is still in effect.
  3. 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 remove while your shell is in the worktree corrupts shell state
  • Even pwd will fail with "No such file or directory" errors
  • Use loom-clean for 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.sh to 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 can git stash and git stash pop each 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 — snapshot is 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>), never refs/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 orphaned ngspice, load 58, 12h of suppressed dispatch).
  • Long compute → the repo's batch backend, or scoped to fit the session, or loom:blocked naming the compute gap: .loom/docs/long-running-compute.md.

Don't use git push --force without --force-with-lease

  • --force-with-lease is safer - it fails if someone else pushed
  • Prevents accidentally overwriting others' work

Preventing Merge Conflicts

To minimize conflicts in the first place:

  1. Pull frequently: Before starting significant work

    git fetch origin main
    git rebase origin/main
    
  2. Keep PRs small: Smaller PRs = fewer conflicts = faster merges

  3. Communicate: If working on shared areas, coordinate with other builders

  4. Rebase before PR: this is a required gate, not just an ounce-of-prevention habit — see builder-pr.md § "Pre-Push Rebase: Sync with origin/main" for the mandatory step (with conflict-handling instructions) that runs immediately before git 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:issue at the same time
  • Both try to claim by adding loom:building
  • Race condition: both may succeed, causing duplicate work

The Solution:

  • Use loom-claim for 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-claim is a PATH shim over loom-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 the loom-daemon binary. The command -v loom-claim degradation 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

CommandPurpose
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 listList all active claims
loom-claim cleanupRemove expired claims

Signal Commands Reference

CommandPurpose
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-packages
  • K4blow
    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