Worktrees

SkillDev tools

Manage .worktrees/ lifecycle: create worktree, list worktrees, remove worktree, clean worktrees, stale worktrees audit, isolate work, project clone. TRIGGER when: any git worktree operation, branch isolation needed, stale worktrees review, project clone under projects/ (e.g. "clone <owner>/<repo> to projects", "add <repo> to projects", "clone this repo into projects"), worktree cleanup. A leading-slash harness dir like "/worktrees" still means the repo-relative .worktrees/ — never a literal filesystem-root path.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Worktrees skill

What this skill tells your AI

The instructions your AI receives, as published by mifunedev/agro in .agro/skills/worktrees/SKILL.md and read by ahel’s review.

Manage .worktrees/ and projects/. Full policy: /git § Worktrees.

A repository's worktrees live at that repository's own root, in .worktrees/. The harness is the first instance of that rule; a project clone under projects/ follows it too, in projects/<owner>/<repo>/.worktrees/.

CHANGE ROUTING — TRACK VS RESET

~/harness and $PROJECTS_ROOT/<owner>/<repo> are different Git boundaries. The parent harness ignores the latter; a project clone's files are never changes to the harness. Classify changes before committing, resetting, or removing anything. A dirty checkout is not disposable merely because it lives under .worktrees/.

~/harness — harness control plane

Track/preserve deliberate, durable harness changes, including:

  • .agro/skills/, .agro/hooks/, .agro/scripts/, .devcontainer/, .github/, docs, templates, and supported configuration defaults.
  • .worktrees/AGENTS.md, projects/AGENTS.md, and other lifecycle documentation.
  • Curated wiki entries and the wiki index when intentionally promoted. The wiki corpus is ignored by default; use git add -f only for reviewed entries. Never promote corpus/raw/ snapshots by accident.
  • A handoff only when it is deliberately shared, scrubbed of secrets, and meant to be durable in this harness checkout or its private operator fork.

Reset/remove only known local, generated, or abandoned state, such as:

  • worktree contents, task progress, memory/log state, plans/spec scratch, screenshots, build output, dependencies, local env/auth files, and caches covered by .gitignore;
  • an explicit operator-local setting or a tracked edit that was reviewed and rejected.

Do not reset a tracked source/config/doc change just because it is unstaged, and do not delete an untracked file before inspecting it. For root state, inspect both index and worktree changes:

git -C ~/harness status --short --untracked-files=all
git -C ~/harness diff --name-status
git -C ~/harness diff --cached --name-status

Use git restore <path> only for identified files. Preview cleanup with git clean -nd (or git clean -ndX for ignored files); route destructive reset/clean operations through .agro/scripts/git-maintenance.sh as required by /git.

$PROJECTS_ROOT/<owner>/<repo> — independent project clone

These directories have their own .git and their own branch/remote policy. Run status and diffs with git -C "$PROJECT", never from ~/harness.

Track/preserve in the project repository intentional source, tests, migrations, documentation, CI, deployment, and project configuration changes. A reviewed .example.env or environment-variable guide is project source; real .env*, credentials, auth files, and local overrides are not. Commit preserved work on a project branch and push it to that project's remote, not to the harness remote.

Reset/remove in the project repository only generated output, dependency and build caches, logs, local secret/config files, nested worktree state after confirming no child worktree is active, or changes the owner explicitly abandons. An active nested worktree is operational state: preserve it, do not add its path to the parent clone, and inspect the child repository separately. Do not blanket-reset source or docs because a clone is being cleaned.

Before removing or resetting a project clone:

PROJECT="$PROJECTS_ROOT/<owner>/<repo>"
git -C "$PROJECT" status --short --untracked-files=all
git -C "$PROJECT" diff --name-status
git -C "$PROJECT" ls-files --others --exclude-standard
git -C "$PROJECT" worktree list --porcelain

If status is non-empty, stop and classify each path. Preserve intentional work by committing it on the project branch, or by making an explicit patch/backup; discard only after the owner confirms. A project clone with dirty, unclassified work must not be removed with rm -rf.

DETECT BASE

Run first. Every create/remove op needs $BASE and $WORKTREES_ROOT.

$WORKTREES_ROOT is always .worktrees/ inside the repository you are standing in — run this from the harness root for harness branches, or from projects/<owner>/<repo>/ to cut a worktree of that project. oh-path resolves the fixed root and only exists at the harness root, so the project case falls through to the repository toplevel.

BASE=$(git show-ref --verify --quiet refs/heads/development && echo development || \
       git show-ref --verify --quiet refs/heads/main && echo main || echo master)
TOPLEVEL="$(git rev-parse --show-toplevel)"
if [ -x "$TOPLEVEL/.agro/scripts/oh-path" ]; then
  WORKTREES_ROOT="$(bash "$TOPLEVEL/.agro/scripts/oh-path" worktrees --no-create 2>/dev/null || printf '%s' "$TOPLEVEL/.worktrees")"
  PROJECTS_ROOT="$(bash "$TOPLEVEL/.agro/scripts/oh-path" projects --no-create 2>/dev/null || printf '%s' "$TOPLEVEL/projects")"
else
  WORKTREES_ROOT="$TOPLEVEL/.worktrees"
  PROJECTS_ROOT=""
fi
echo "$BASE"
echo "$WORKTREES_ROOT"

CREATE — new branch

PREFIX=feat   # feat bug task audit skill agent
ISSUE=42
DESC=short-desc
BRANCH="$PREFIX/$ISSUE-$DESC"
mkdir -p "$WORKTREES_ROOT"
git worktree add -b "$BRANCH" "$WORKTREES_ROOT/$BRANCH" "$BASE"

CREATE — existing branch

BRANCH=feat/42-short-desc
mkdir -p "$WORKTREES_ROOT"
git worktree add "$WORKTREES_ROOT/$BRANCH" "$BRANCH"

LIST — all worktrees + age + PR status

git worktree list --porcelain | awk '/^worktree /{wt=$2} /^branch /{
  sub("refs/heads/",""); br=$2; print wt, br}' | while read -r path branch; do
  age=$(( ( $(date +%s) - $(git -C "$path" log -1 --format=%ct 2>/dev/null || echo $(date +%s)) ) / 86400 ))
  pr=$(gh pr list --head "$branch" --state open --json number,title \
       --jq '.[0] | if . then "#\(.number) \(.title)" else "no PR" end' 2>/dev/null)
  printf "%-50s  %3dd  %s\n" "$branch" "$age" "$pr"
done

ISOLATE — in-flight work

Main checkout has loose files. Don't stash. Don't switch.

# 1. Cut worktree off base
BRANCH=feat/42-my-work
git worktree add -b "$BRANCH" "$WORKTREES_ROOT/$BRANCH" "$BASE"

# 2. Copy files in
cp path/to/file1 path/to/file2 "$WORKTREES_ROOT/$BRANCH/<destination>/"

# 3. Commit in worktree
cd "$WORKTREES_ROOT/$BRANCH"
git add . && git commit -m "feat: ..."

Before cleaning main checkout — byte-check every file first:

for f in path/to/file1 path/to/file2; do
  a=$(md5sum "$f" | awk '{print $1}')
  b=$(git show "$BRANCH:$f" | md5sum | awk '{print $1}')
  [ "$a" = "$b" ] && echo "same:  $f" || echo "DRIFT: $f"
done

All same:? Then clean:

git restore path/to/file1 path/to/file2

Any DRIFT:? Stop. File not committed right. Fix first.

REMOVE — clean

BRANCH=feat/42-short-desc
git worktree remove "$WORKTREES_ROOT/$BRANCH"
git worktree prune

Corrupted (not in git worktree list):

rm -rf "$WORKTREES_ROOT/$BRANCH"
git worktree prune

Forced removal under the cc-safety-net guard

git worktree remove --force (and git branch -D) are denied inline by cc-safety-net. In agent (hook-mediated) contexts route them through the file-invoked shim:

bash .agro/scripts/git-maintenance.sh worktree-remove "$WORKTREES_ROOT/$BRANCH"
bash .agro/scripts/git-maintenance.sh branch-delete "$BRANCH"

Scope: only non-agent-mediated invocations (raw scheduler/tmux shell scripts) bypass PreToolUse hooks. Agent-driven crons do not bypass them, so they must use the shim too. Plain git worktree remove (no --force) stays allowed.

STALE AUDIT — review only, no auto-delete

List worktrees older than 30 days with no open PR. Surface. Don't remove.

git worktree list --porcelain | awk '/^worktree /{wt=$2} /^branch /{
  sub("refs/heads/",""); br=$2; print wt, br}' | while read -r path branch; do
  age=$(( ( $(date +%s) - $(git -C "$path" log -1 --format=%ct 2>/dev/null || echo $(date +%s)) ) / 86400 ))
  [ "$age" -lt 30 ] && continue
  pr=$(gh pr list --head "$branch" --state open --json number \
       --jq 'length' 2>/dev/null)
  [ "$pr" = "0" ] && printf "STALE %3dd  %s\n" "$age" "$branch"
done

Review each STALE line. Remove manually if safe (see REMOVE above).

PROJECT CLONE

Independent repo — not a harness branch. Has its own .git.

# Clone
OWNER=ryaneggz
REPO=some-project
mkdir -p "$PROJECTS_ROOT/$OWNER"
git clone "https://github.com/$OWNER/$REPO.git" "$PROJECTS_ROOT/$OWNER/$REPO"

# Remove only after the CHANGE ROUTING preflight above.
PROJECT="$PROJECTS_ROOT/$OWNER/$REPO"
if ! git -C "$PROJECT" status --porcelain=v1 --untracked-files=all; then
  echo "Cannot inspect project clone; refusing removal: $PROJECT" >&2
  exit 1
fi
if [ -n "$(git -C "$PROJECT" status --porcelain=v1 --untracked-files=all)" ]; then
  echo "Dirty project clone; preserve or explicitly classify changes first: $PROJECT" >&2
  exit 1
fi
rm -rf "$PROJECT"

No git worktree for these. Plain git clone / rm -rf.

To cut a worktree of a project clone, re-run DETECT BASE from inside it — $WORKTREES_ROOT then resolves to $PROJECT/.worktrees and every CREATE, REMOVE, and STALE AUDIT block above applies unchanged.

Signals

GitHub stars
38
Forks
3
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
worktrees
Source
github.com/mifunedev/agro