Skip to content

utils: add agent_worktree.py for isolated PR/branch checkouts - #3228

Open
doomedraven wants to merge 3 commits into
masterfrom
agent-worktree-helper
Open

doomedraven wants to merge 3 commits into
masterfrom
agent-worktree-helper

Conversation

@doomedraven

Copy link
Copy Markdown
Collaborator

What

Adds utils/agent_worktree.py, a wrapper around git worktree for reviewing PRs and testing branches without touching your working clone, plus the tests and documentation for it.

Why

Checking out someone else's branch currently means either polluting your clone or doing the git worktree plumbing by hand: working out which fork the head branch lives in, fetching it under a name that will not collide, setting upstream, and remembering to tear it down. Most clones carry uncommitted work, so a stray git checkout/git stash loses it. This is a recurring hazard both for humans reviewing PRs and for automation (CI helpers, review bots, coding agents) that cannot know whether a clone is clean.

Usage

python utils/agent_worktree.py new --pr 3219        # resolve head fork via gh, fetch, branch, check out
python utils/agent_worktree.py new --branch topic
python utils/agent_worktree.py new --from origin/master --name scratch

python utils/agent_worktree.py list                 # flags: main / managed / dirty / pr#N
python utils/agent_worktree.py path pr3219          # for use in $(...)
python utils/agent_worktree.py update pr3219        # re-fetch after the author pushes
python utils/agent_worktree.py remove pr3219        # also drops the branch it created
python utils/agent_worktree.py cleanup
python utils/agent_worktree.py info                 # repo, remotes, virtualenv, dirty state

--json on any command for scripted use. --repo PATH to operate on a repository other than the current directory.

Behaviour worth reviewing

  • Nothing is ever written to the main working tree. No checkout, reset, stash or clean. remove refuses the main worktree outright.
  • Only its own worktrees are removed. Each one is tagged with an agent-meta.json written into .git/worktrees/<id>/, i.e. inside the git admin directory, so git status in the worktree stays empty and a hand-made git worktree add is never touched by cleanup.
  • Refuses to destroy work. remove and cleanup bail on uncommitted changes or unpushed commits; cleanup skips those and prints the reason. --force overrides.
  • Fork resolution. new --pr asks gh for the head repository. If a matching remote exists it fetches from it and sets upstream; if not, it fetches straight from the fork URL into a private ref namespace and warns that no upstream was configured.
  • Branch collisions get a numeric suffix and a warning rather than an error.
  • Default location is ~/.cache/agent-worktrees/<repo>/<name>, overridable with --base-dir, --path or $AGENT_WORKTREE_DIR. Deliberately not /tmp, which gets reaped.

Implementation notes: standard library only, no new dependencies, and nothing CAPE-specific — it works in any git repository. gh is needed only for --pr.

Tests

tests/test_agent_worktree.py — 17 tests building throwaway local repositories in tmp_path. No network, no GitHub credentials, no CAPE configuration. The --pr path is the only uncovered branch since it needs a live API.

17 passed

ruff check and black --check (line-length 132) are clean on both new files.

Docs

SKILLS.md gains three sections under Development Guides:

  1. Local Development Environment — Poetry, ruff as the fast gate, black/ruff format at 132 columns, and pytest -p no:warnings to stop several hundred third-party deprecation warnings from burying results.
  2. Isolated Checkouts for Review and Testing — the tool above.
  3. Codebase Gotchas — behaviours that produce silent failures rather than errors:
    • Dictionary.__getattr__ returns None for missing keys, so getattr(section, "key", default) never applies its default.
    • _BaseConfig.get(section) takes exactly one argument and raises on unknown sections; conf.get(name, default) is a TypeError, not a fallback.
    • run_tool() returns stdout only.
    • extractor_ctx() wraps extractors in except Exception: log.exception(...), so a TypeError in an extractor looks like an empty result.

Also adds a one-line entry to the Important Commands list and a changelog.md entry.

Reviewing a PR or reproducing a bug against another branch currently means
checking it out in your working clone. Most clones carry uncommitted work, so
that is either unsafe or requires manual git worktree plumbing: figuring out
which fork the head branch lives in, fetching it under a sane name, setting
upstream, and cleaning up afterwards.

utils/agent_worktree.py wraps that:

  new --pr <id>       resolve the head fork via gh, fetch, branch, check out
  new --branch <name> / --from <ref>
  list / path / update / remove / cleanup / info

It is stdlib-only and repository-agnostic. Worktrees it creates are tagged
with metadata inside the git admin directory, so nothing extra appears in
git status and cleanup only ever touches its own worktrees. remove and
cleanup refuse to discard uncommitted changes or unpushed commits without
--force, and the main worktree can never be removed.

tests/test_agent_worktree.py covers the CLI against throwaway local
repositories; no network, credentials or CAPE configuration required.

SKILLS.md gains sections on the local development environment, isolated
checkouts, and codebase behaviours that cause silent failures.
Clones that follow the fork convention have origin pointing at the
contributor's fork and upstream at the canonical repository. Two things broke
there:

* repo_slug() preferred origin, so 'gh pr view' was asked about the fork
  instead of the repository the PR was opened against. upstream now wins when
  it is configured; clones made straight from the canonical repo are
  unaffected because they have no upstream.
* 'new --branch' fetched from origin only and failed outright if the branch
  lived upstream. It now tries origin, then upstream, then any other remote,
  and reports which one it used.

A missing branch is now a clear error naming the remotes that were tried.
…mote

The removal guard compared HEAD against the tracked upstream only. A review
branch normally tracks the branch it will merge into while its commits are
pushed to a fork, so 'rev-list @{upstream}..HEAD' is non-zero even though
nothing is at risk, and cleanup refused to remove a worktree whose work was
safely published.

The guard now treats work as unpushed only when no remote-tracking ref
contains HEAD at all, which is the condition that actually matters.
doomedraven added a commit to doomedraven/capemon that referenced this pull request Sep 14, 2026
Mirrors kevoreilly/CAPEv2#3228. The removal guard compared HEAD against the
tracked upstream only, so a branch tracking 'upstream/capemon' while its
commits were pushed to a fork looked unpushed and cleanup refused to remove
it. Work now counts as unpushed only when no remote-tracking ref contains
HEAD.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant