Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .agents/skills/worktrees-pnpm/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ the skill gets them; wiring them up is two edits.
replaces the default placement logic entirely — it runs `git worktree add` itself and prints the
path it made — and `WorktreeRemove` is its counterpart.

**Every worktree starts from the remote's default branch as of the moment it is made.** The
hook fetches `origin`'s default branch and branches from `origin/<default>`. It never uses the
main checkout's `HEAD`, which is whatever that checkout was last pulled to: a worktree cut from it
can start dozens of commits behind `main`, and nothing flags it until someone reads stale code as
current. If the fetch fails (offline, no `origin`), creation fails with a message rather than
falling back to a stale base. The new branch has no upstream; push it with an explicit refspec.

```json
{
"hooks": {
Expand Down Expand Up @@ -107,7 +114,8 @@ Verify without waiting for an agent, by feeding a hook its payload directly:
```bash
printf '{"hook_event_name":"WorktreeCreate","cwd":"%s","worktree_id":"probe"}' "$PWD" \
| .agents/skills/worktrees-pnpm/scripts/worktree-create.sh
# expect: <repo>/worktrees/probe on stdout, git chatter on stderr, exit 0
# expect: <repo>/worktrees/probe on stdout, git chatter on stderr, exit 0,
# and `git -C worktrees/probe rev-parse HEAD` equal to `origin/main` just fetched

# The id field is not stable across callers, so .worktreeId and .name are read
# too — `.name` is what a background agent's payload actually carries.
Expand Down
47 changes: 43 additions & 4 deletions .agents/skills/worktrees-pnpm/scripts/worktree-create.sh
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,49 @@ fi

mkdir -p "$(dirname "$worktree_dir")"

# Mirror the default: a fresh branch per worktree, based on current HEAD. The
# agent checks out whatever branch it actually needs once inside. -B rather than
# -b so a leftover branch from a removed worktree does not wedge creation.
# A fresh branch per worktree, based on the remote's default branch as of NOW.
# Not HEAD: the main checkout's HEAD is whatever branch it last had checked out,
# at whatever commit it was last pulled to, so a worktree cut from it can start
# dozens of commits behind and nothing says so until someone reads stale code
# as current. Fetch first, every time, and fail rather than fall back: a
# worktree on a stale base is the failure this exists to prevent, and "could
# not fetch" is a far clearer thing to be told than "your base was old".
# The agent checks out whatever branch it actually needs once inside. -B rather
# than -b so a leftover branch from a removed worktree does not wedge creation.
# All git chatter goes to stderr; stdout carries the path and nothing else.
git -C "$repo_root" worktree add -B "worktree-$worktree_id" "$worktree_dir" >&2
remote=origin
default_branch=$(git -C "$repo_root" ls-remote --symref "$remote" HEAD 2>/dev/null \
| awk '$1 == "ref:" && $3 == "HEAD" { sub("^refs/heads/", "", $2); print $2 }') \
|| true # pipefail would exit here silently; the check below says why
if [ -z "$default_branch" ]; then
echo "WorktreeCreate: could not resolve $remote's default branch (offline?)" >&2
exit 1
fi
# The refspec is spelled out because a bare branch name only updates the
# remote-tracking ref when remote.<name>.fetch covers it, and a single-branch
# clone (which --depth implies) covers only the branch it was cloned on: the
# fetch would succeed, write FETCH_HEAD alone, and leave the ref below stale or
# missing. FETCH_HEAD is no substitute, since every concurrent fetch rewrites it.
#
# Concurrent creations race on that ref: each fetch records the value it expects
# to replace, and all but one find it already moved ("cannot lock ref ... is at
# X but expected Y"). Measured at 55 failures in 60 concurrent fetches, and
# core.filesRefLockTimeout does not help, since nothing is waiting on a lock. A
# loser's retry finds the ref already at the tip and succeeds, so retry.
fetched=
for attempt in 1 2 3; do
if git -C "$repo_root" fetch --quiet "$remote" \
"+refs/heads/$default_branch:refs/remotes/$remote/$default_branch" >&2; then
fetched=1
break
fi
[ "$attempt" -lt 3 ] && sleep "$attempt"
done
if [ -z "$fetched" ]; then
echo "WorktreeCreate: fetching $remote/$default_branch failed; refusing to branch from a stale base" >&2
exit 1
fi
git -C "$repo_root" worktree add --no-track -B "worktree-$worktree_id" \
"$worktree_dir" "refs/remotes/$remote/$default_branch" >&2

echo "$worktree_dir"
Loading