Skip to content

chore: move worktrees in-repo, ship hooks with the skill - #77

Merged
thecodedrift merged 4 commits into
mainfrom
docs/worktree-gotchas
Jul 30, 2026
Merged

chore: move worktrees in-repo, ship hooks with the skill#77
thecodedrift merged 4 commits into
mainfrom
docs/worktree-gotchas

Conversation

@thecodedrift

Copy link
Copy Markdown
Member

Follow-up to #75, which landed the worktree hooks. This adds the operational knowledge that only came out of actually running agents in worktrees — every entry is something hit for real, with the remediation rather than just the warning.

Worktree gotchas: a branch can live in only one worktree, so editing a branch an agent holds needs a decision (hand it to the agent, work in its worktree only while idle, or reclaim it); an idle agent's worktree keeps its branch locked, with the checks to run before reclaiming it; the worktree-<id> branch in git worktree list is a placeholder and not where the work is; mv breaks a worktree where git worktree move preserves it and its node_modules; and a worktree survives between agent runs, so a resumed agent pays no second install.

The more valuable half is the second section: failures that look like worktree problems and are not. Misdiagnosing these is what actually cost time.

  • --force-with-lease failing stale info on every branch is a shallow or single-branch clone — there is no remote-tracking ref to lease against. Nothing to do with worktrees.
  • A sudden fan-out of rebases and force-pushes is main moving under an open stack, which happens every time a PR merges.
  • A rejected non-fast-forward push may be a remote branch that was rebased independently. If the remote already holds your commits under new SHAs, replay only what is missing (git rebase --onto origin/<branch> <old-base> <branch>) instead of force-pushing over it.

That last one earned its place twice over while preparing this change. main moved to #75's own merge commit mid-session, so a routine git rebase origin/main replayed commits that main already contained and --force-with-lease correctly refused the result. The fix was not a force-push — it was recognising that the branch was merged history and moving the one unlanded commit to a fresh branch, which is what this PR is.

Every entry is something hit for real this session: a branch locked by
an idle agent's worktree, the placeholder worktree-<id> branch that
isn't where the work is, `mv` breaking a worktree where `git worktree
move` preserves it, and worktrees surviving between agent runs.

Also separates the failures that merely surfaced during worktree work
from the ones worktrees actually cause. --force-with-lease failing on
every branch is a shallow clone; a fan-out of rebases is main moving
under an open stack; a rejected push may be a remote that was rebased
independently, where replaying the missing commit beats force-pushing
over it. Misdiagnosing those as worktree problems is what cost the most
time, so they are named rather than left to be rediscovered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDv57zHq7abms3RReSQw6q
@thecodedrift thecodedrift added the skip-changeset PR intentionally ships no release note (bypasses the changeset requirement) label Jul 30, 2026
Worktrees live at ../<repo>-worktrees/ now, which is outside the
project root, so a fresh session or a teammate would be prompted for
every file operation there — the placement fixed the tooling problem
but left the permission scope behind it.

Scoped to that one sibling directory rather than the parent, and the
read.deny globs are all **/-prefixed so keys, .env files, and
credentials stay denied inside worktrees exactly as they are inside
the repo.

Note the path is literal: it matches a clone directory named `skills`,
which is what the create hook derives `skills-worktrees` from. A clone
under a different directory name gets a correspondingly different
worktree directory and would need this entry adjusted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDv57zHq7abms3RReSQw6q
@thecodedrift thecodedrift changed the title docs: add worktree gotchas and remediations to the skill chore: worktree permission scope, gotchas, and remediations Jul 30, 2026
The sibling location could not be referenced portably from committed
config: its path derives from the clone directory name, so a teammate
whose checkout is not named `skills` gets permission prompts for every
file operation — silently, and per person. `<repo>/worktrees/` is the
same path in every clone.

The cost is that root-level tooling walks into it, so `worktrees/` is
excluded in .gitignore, .prettierignore, and eslint.config.js. Prettier
is a new entry rather than an oversight: its globs skip dotfiles, so
`.claude/worktrees/` had been invisible to it. Measured as NOT needing
one: pnpm workspaces (the glob is root-anchored) and tsc (per-package
via turbo). All three ignores verified against a live worktree.

The hook scripts move to `scripts/` under the skill, the way iterate-pr
ships its own, so a repo installing this skill through dotagents gets
them. settings.json references them by project-relative path, which is
stable across repos.

The skill gains a Setup section with the settings.json block and a way
to test the hooks by piping them a payload, plus ignore examples for
eslint (flat and legacy), prettier, biome, ruff/black, tsconfig, and a
verification recipe — since which tools traverse differs per repo and
guessing is what caused the prettier miss here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDv57zHq7abms3RReSQw6q
@thecodedrift thecodedrift changed the title chore: worktree permission scope, gotchas, and remediations chore: move worktrees in-repo, ship hooks with the skill Jul 30, 2026
@thecodedrift
thecodedrift marked this pull request as ready for review July 30, 2026 17:47
@thecodedrift

Copy link
Copy Markdown
Member Author

@claude /review

@claude

claude Bot commented Jul 30, 2026

Copy link
Copy Markdown

Claude finished @thecodedrift's task in 3m 21s —— View job


Review of PR #77

  • Gather context (diff, changed files)
  • Understand the request (code review)
  • Review diff for correctness, security, performance, clarity
  • Post inline comments for concrete issues
  • Post final top-level assessment

Summary

Good change overall — the hook scripts, ignore entries, and the new gotchas/remediation sections in SKILL.md are accurate and well-reasoned. One real inconsistency needs fixing before merge, plus one small stale string.

Findings

1. SKILL.md intro and "Cleaning up" sections still describe the old sibling-directory model (not inline-commentable — those lines sit outside the diff hunks, so posted here instead)

  • Lines 17–18, 22, 24: git worktree add ../skills-worktrees/<name>, "Worktrees go beside the repo, in ../skills-worktrees/, never inside it", and hook scripts described as living "under .claude/hooks/".
  • Line 262 (## Cleaning up): git worktree remove ../skills-worktrees/<name>.

This directly contradicts the "Setup (per repo)" section this same PR adds (lines 63–195), which correctly documents the new in-repo $repo_root/worktrees/<id> placement and the new hook script path .agents/skills/worktrees-pnpm/scripts/. It also contradicts what worktree-create.sh actually does now (worktree_dir="$repo_root/worktrees/$worktree_id") and the new .gitignore/.prettierignore/eslint.config.js entries, all of which assume in-repo worktrees/.

As written, the doc opens by confidently telling readers the opposite of what the hooks now do, and closes the same way. Since the whole point of this PR is the operational knowledge in this skill, this self-contradiction is the one thing worth fixing before merge — please update the intro and cleanup examples to use worktrees/<name> (repo-relative) and drop the .claude/hooks/ reference.

2. Stale statusMessage in .claude/settings.json (posted inline)

Line 62: "statusMessage": "Creating worktree beside the repo" wasn't updated when the hook moved to in-repo placement — it'll show a misleading status during worktree creation. The doc's own example JSON (SKILL.md line 81) already uses "Creating worktree"; worth matching.

What looks solid

  • worktree-create.sh / worktree-remove.sh: the diff vs. the previous .claude/hooks/ versions is minimal and correct — just the path change ($repo_root/worktrees/$worktree_id instead of the sibling dir) and updated comments. Validation, idempotency check, and the "leave in place on failure" behavior in worktree-remove.sh are unchanged and still sound.
  • .gitignore / .prettierignore / eslint.config.js: all three correctly add worktrees/, and the eslint comment accurately explains why .claude/worktrees/ stays as a backstop.
  • The new "Gotchas and remediations" and "Problems that look like worktree problems and are not" sections read as genuine, hard-won operational knowledge (branch locking, the worktree-<id> placeholder branch, mv vs git worktree move, the --force-with-lease/stale-info and non-fast-forward remediations) — no factual issues found there.
  • Confirmed .claude/hooks/worktree-create.sh and worktree-remove.sh are properly deleted (not just left behind) — git diff origin/main...HEAD -M shows clean renames into .agents/skills/worktrees-pnpm/scripts/.

Did not run build/lint/test locally per instructions — CI will report those separately.

Branch: docs/worktree-gotchas

Comment thread .claude/settings.json Outdated
Review caught the doc opening by telling readers the opposite of the
setup section further down. I rewrote the intro for the sibling model,
then moved worktrees in-repo and only *added* the Setup section without
going back — so the intro, its rationale, and the cleanup example all
still described `../skills-worktrees/`, and the intro credited the hook
scripts to `.claude/hooks/` after they moved under the skill.

For a skill whose entire purpose is telling people where worktrees go,
contradicting itself is the worst available bug.

The sibling model is now recorded as the rejected alternative with the
reason it lost — its path depends on the clone directory name, which
committed config cannot know — rather than deleted, so the next person
does not re-propose it.

Also fixes the `statusMessage` left reading "Creating worktree beside
the repo".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDv57zHq7abms3RReSQw6q
@thecodedrift

Copy link
Copy Markdown
Member Author

Re: @claude[bot] — "Claude finished @thecodedrift's task in 3m 21s — Review of PR #77…"
#77 (comment)

Both fixed. Finding 1 was the important one and you characterised it correctly — the doc opened by telling readers the opposite of its own setup section.

The cause: I rewrote the intro for the sibling-directory model, then moved worktrees in-repo and only added the Setup section without going back through what was already there. So the intro, its rationale, the cleanup example, and the .claude/hooks/ script path were all describing a model the code no longer implements. For a skill whose whole job is telling people where worktrees go, that is the worst available bug, and it would have shipped to other repos through dotagents.

I swept for the pattern rather than fixing only the lines you cited — five stale references in total across SKILL.md, plus the statusMessage. grep for skills-worktrees, beside the repo, and .claude/hooks now returns nothing.

The sibling model is kept as a recorded rejected alternative with the reason it lost — its path depends on the clone directory name, which committed config cannot reference portably — rather than deleted, so it does not get re-proposed later.

— AI Coding Agent

@thecodedrift
thecodedrift merged commit 8376641 into main Jul 30, 2026
5 checks passed
@thecodedrift
thecodedrift deleted the docs/worktree-gotchas branch July 30, 2026 20:45
thecodedrift added a commit that referenced this pull request Jul 30, 2026
A top-level review comment has no thread to resolve, so the skill had
to dedupe by scanning existing comments for a reference marker citing
the same author and snippet. That is fragile, and it failed in practice
— on #75 and again on #77 the summary comment kept reporting as
unaddressed after it had been answered, leaving a permanent
needs_attention to reason about by hand every pass.

A hooray reaction on the original is a machine-readable acknowledgement.
fetch_pr_feedback now reads reactions.hooray, marks the item
acknowledged, and buckets it as resolved, so a re-run reports zero
instead of re-surfacing it. Verified on #77: needs_attention went 2 to 0
with no other change.

The reaction step is documented against the PR-scoped comments endpoint
specifically. The repo-wide repos/{owner}/{repo}/issues/comments returns
every comment in the repository, and selecting from it will eventually
react on another PR's comment — I did exactly that here and got the
right answer by luck.

Items now carry comment_id so the id comes from the feedback data rather
than from matching body text.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDv57zHq7abms3RReSQw6q
thecodedrift added a commit that referenced this pull request Aug 26, 2026
A top-level review comment has no thread to resolve, so the skill had
to dedupe by scanning existing comments for a reference marker citing
the same author and snippet. That is fragile, and it failed in practice
— on #75 and again on #77 the summary comment kept reporting as
unaddressed after it had been answered, leaving a permanent
needs_attention to reason about by hand every pass.

A hooray reaction on the original is a machine-readable acknowledgement.
fetch_pr_feedback now reads reactions.hooray, marks the item
acknowledged, and buckets it as resolved, so a re-run reports zero
instead of re-surfacing it. Verified on #77: needs_attention went 2 to 0
with no other change.

The reaction step is documented against the PR-scoped comments endpoint
specifically. The repo-wide repos/{owner}/{repo}/issues/comments returns
every comment in the repository, and selecting from it will eventually
react on another PR's comment — I did exactly that here and got the
right answer by luck.

Items now carry comment_id so the id comes from the feedback data rather
than from matching body text.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changeset PR intentionally ships no release note (bypasses the changeset requirement)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant