worktree-agent
Mandatory rules for agents in git worktree isolation.
Install
npx skills add https://github.com/notque/vexjoy-agent/tree/main/skills/process/worktree-agent
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install notque-vexjoy-agent@llmmart
git clone https://github.com/notque/vexjoy-agent.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole notque/vexjoy-agent collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Worktree Agent Rules
Mandatory rules for any agent dispatched with isolation: "worktree".
Rule 1: Verify Your Working Directory
On start, run pwd. Your path MUST contain .claude/worktrees/.
If your CWD is the main repo path, STOP and report the error.
Rule 2: Create Feature Branch First
git checkout -b <branch-name>
Never commit on the default worktree-agent-* branch. Create your feature branch FIRST.
If git checkout -b <branch-name> fails with "a branch named X already exists":
# Option A: the branch has no commits beyond main — safe to reset and reuse
git branch -D <branch-name>
git checkout -b <branch-name>
# Option B: the branch is checked out in another active worktree — use a unique name
git checkout -b <branch-name>-2 # or append timestamp: $(date +%s)
If git checkout -b <branch-name> fails with "X is already used by worktree at Y":
# Branch is live in another worktree — use a unique suffix
git checkout -b <branch-name>-$(date +%s)
To update a branch held by another worktree (e.g. an existing PR branch): work detached from origin/<branch> and push with git push origin HEAD:<branch>. gh pr merge's post-merge local-checkout errors are harmless.
Rule 3: Use Worktree-Relative Paths
Never hardcode absolute paths from the main repo. Use $(git rev-parse --show-toplevel)/path.
Exception: Reading gitignored ADR files requires the main repo absolute path.
Rule 4: Ignore Auto-Plan Hooks
Keep planning inline instead of creating task_plan.md. If the auto-plan hook fires, continue with the current task and keep your attention on implementation.
Rule 5: Stage Specific Files Only
git add path/to/specific/file.py
Never git add ., git add -A, or git add --all. Verify with git diff --cached --stat.
Rule 6: Do Not Touch the Main Worktree
Never write to paths outside your worktree directory. Never run git checkout in the main repo.
Rule 7: Commit with Conventional Format
Use the commit message specified in your prompt. No attribution lines.
Rule 8: Run Both ruff Checks Before Declaring CI-Ready
For any Python code changes, run both checks before pushing or creating a PR:
ruff check . --config pyproject.toml
ruff format --check . --config pyproject.toml
Running only ruff check misses formatting violations. The Tests / lint CI job runs both — if you skip ruff format --check, the PR will fail CI and cannot merge due to branch protection.
Rule 9: Run Preflight Check on Start
Run the preflight script at the start of any worktree task to confirm clean state:
bash scripts/worktree-preflight.sh <intended-branch-name>
If it exits 1, fix the reported issue before proceeding.
Rule 10: Reserve disk capacity before creating a checkout
The dispatcher runs this before each implementation worktree:
python3 ~/.claude/skills/process/worktree-agent/scripts/worktree_capacity.py \
--repo "$(git rev-parse --show-toplevel)" --strict
The JSON report has three states:
| State | Dispatcher action |
|---|---|
ready (<80% used) |
Create the one implementation checkout for the candidate. |
cleanup-soon (80–<85%) |
Reclaim accepted clean checkouts before adding another. Use root read-only review work where possible. |
blocked (≥85%) |
Integrate, deploy, verify, or reclaim; create no new checkout. |
The report lists clean candidates only. The dispatcher confirms their task is inactive before removal because Git cleanliness alone does not prove that fact.
Rule 11: Assign checkout roles deliberately
| Task | Checkout policy |
|---|---|
| Source implementation or repair | One writable task worktree, reused through review corrections. |
| Code review, test-plan review, or read-only investigation | Read the candidate through git diff or git show from the repository root; allocate no checkout. |
| Large repository implementation | Create a sparse worktree containing declared source/test/config scopes; include whole-repository content only when the task requires it. |
For a sparse implementation checkout:
git worktree add --no-checkout <worktree-path> -b <branch> <base-sha>
git -C <worktree-path> sparse-checkout init --no-cone
git -C <worktree-path> sparse-checkout set --no-cone <declared-path>...
git -C <worktree-path> checkout
Record any full-checkout reason in the dispatch handoff.
Post-Merge Cleanup
After integration or a PR merge, the dispatcher first confirms that the task is inactive and the checkout is clean, then runs:
git worktree remove -- <accepted-worktree-path>
bash scripts/worktree-cleanup.sh --force
git worktree remove frees the materialized checkout while preserving its branch for recovery. The cleanup script then prunes stale .git/worktrees entries and removes merged harness branches.
Failure Modes This Prevents
| Failure | Rule | Without It |
|---|---|---|
| Agent edits main repo files | 1, 6 | Changes leak to main, get stashed/lost |
| Context wasted on task_plan.md | 4 | Implementation budget consumed by planning |
| Commit on wrong branch | 2 | Orchestrator merges wrong content |
| PR has changes from 2 ADRs | 5, 6 | Cross-contamination between agents |
| Branch locked by worktree | 2 | Fatal error on checkout |
| PR fails CI on format | 8 | Merge blocked; ruff format --check was skipped |
| New task fails to create worktree | 9 | Branch name collision from prior stale run |
Files (vexjoy-agent)
-
scripts
-
tests
-
test_worktree_capacity.py 2.6 KB
"""Regression tests for the deterministic worktree-capacity guard.""" from __future__ import annotations import importlib.util from pathlib import Path from types import SimpleNamespace SCRIPT_PATH = Path(__file__).resolve().parents[1] / "worktree_capacity.py" TOOLKIT_ROOT = Path(__file__).resolve().parents[5] SPEC = importlib.util.spec_from_file_location("worktree_capacity", SCRIPT_PATH) assert SPEC is not None and SPEC.loader is not None worktree_capacity = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(worktree_capacity) def test_capacity_status_reserves_hard_stop_for_new_worktrees(): assert worktree_capacity.capacity_status(79, soft_percent=80, hard_percent=85) == "ready" assert worktree_capacity.capacity_status(80, soft_percent=80, hard_percent=85) == "cleanup-soon" assert worktree_capacity.capacity_status(85, soft_percent=80, hard_percent=85) == "blocked" def test_usable_disk_percent_counts_filesystem_reserved_space_as_unavailable(): stats = SimpleNamespace(f_blocks=100, f_bfree=30, f_bavail=20) assert worktree_capacity.usable_disk_percent(stats) == 77.8 def test_parse_worktree_porcelain_keeps_only_paths_under_agent_root(tmp_path): agent_root = tmp_path / ".claude" / "worktrees" kept = agent_root / "active" outside = tmp_path / "manual-checkout" payload = ( f"worktree {tmp_path}\nHEAD abc\nbranch refs/heads/main\n\n" f"worktree {kept}\nHEAD def\nbranch refs/heads/fix/active\n\n" f"worktree {outside}\nHEAD ghi\ndetached\n" ) assert worktree_capacity.agent_worktree_paths(payload, agent_root) == [kept] def test_capacity_report_marks_clean_checkouts_for_dispatcher_review(tmp_path): report = worktree_capacity.capacity_report( used_percent=81, paths=[tmp_path / "clean", tmp_path / "dirty"], clean_paths={tmp_path / "clean"}, soft_percent=80, hard_percent=85, ) assert report["status"] == "cleanup-soon" assert report["worktrees"] == {"count": 2, "clean_candidates": [str(tmp_path / "clean")]} def test_dispatch_rules_require_capacity_preflight_and_checkout_roles(): canonical_rules = (TOOLKIT_ROOT / "skills/meta/do/references/worktree-rules.md").read_text() quality_loop = (TOOLKIT_ROOT / "skills/meta/do/references/quality-loop.md").read_text() assert "worktree_capacity.py" in canonical_rules assert "allocate no checkout" in canonical_rules assert "sparse-checkout" in canonical_rules assert "git worktree remove -- <accepted-worktree-path>" in canonical_rules assert "read-only reviewers" in quality_loop
-
-
worktree_capacity.py 4.8 KB
#!/usr/bin/env python3 """Report task-worktree capacity without deleting any checkout or branch.""" from __future__ import annotations import argparse import json import os import subprocess import sys from pathlib import Path from typing import Iterable DEFAULT_SOFT_PERCENT = 80 DEFAULT_HARD_PERCENT = 85 def capacity_status(used_percent: float, *, soft_percent: int, hard_percent: int) -> str: """Classify disk pressure for task-worktree creation.""" if not 0 <= soft_percent < hard_percent <= 100: raise ValueError("thresholds require 0 <= soft < hard <= 100") if used_percent >= hard_percent: return "blocked" if used_percent >= soft_percent: return "cleanup-soon" return "ready" def usable_disk_percent(stats: os.statvfs_result) -> float: """Match `df` capacity by excluding filesystem-reserved blocks.""" used_blocks = stats.f_blocks - stats.f_bfree usable_blocks = used_blocks + stats.f_bavail if usable_blocks <= 0: return 100.0 return round(used_blocks * 100 / usable_blocks, 1) def _porcelain_worktree_paths(porcelain: str) -> Iterable[Path]: for record in porcelain.split("\n\n"): for line in record.splitlines(): if line.startswith("worktree "): yield Path(line.removeprefix("worktree ")) break def agent_worktree_paths(porcelain: str, agent_root: Path) -> list[Path]: """Return only registered worktrees below the designated agent root.""" root = agent_root.resolve() result: list[Path] = [] for path in _porcelain_worktree_paths(porcelain): resolved = path.resolve() try: resolved.relative_to(root) except ValueError: continue result.append(resolved) return sorted(result) def _git_output(repo: Path, *args: str) -> str: completed = subprocess.run( ["git", "-C", str(repo), *args], check=True, capture_output=True, text=True, timeout=10, ) return completed.stdout def clean_worktree_paths(paths: Iterable[Path]) -> set[Path]: """Return checkouts with no tracked or untracked changes. This does not establish whether a task is still active. Dispatchers review these candidates before calling `git worktree remove`. """ clean: set[Path] = set() for path in paths: try: if not _git_output(path, "status", "--porcelain"): clean.add(path) except (OSError, subprocess.CalledProcessError, subprocess.TimeoutExpired): continue return clean def capacity_report( *, used_percent: float, paths: Iterable[Path], clean_paths: set[Path], soft_percent: int, hard_percent: int, ) -> dict[str, object]: """Build the stable, machine-readable capacity report.""" ordered_paths = sorted(paths) return { "status": capacity_status(used_percent, soft_percent=soft_percent, hard_percent=hard_percent), "disk": {"used_percent": round(used_percent, 1), "soft_percent": soft_percent, "hard_percent": hard_percent}, "worktrees": { "count": len(ordered_paths), "clean_candidates": [str(path) for path in ordered_paths if path in clean_paths], }, } def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--repo", type=Path, required=True, help="repository root") parser.add_argument( "--worktree-root", type=Path, default=Path(".claude/worktrees"), help="agent checkout root, relative to --repo unless absolute", ) parser.add_argument("--soft-percent", type=int, default=DEFAULT_SOFT_PERCENT) parser.add_argument("--hard-percent", type=int, default=DEFAULT_HARD_PERCENT) parser.add_argument( "--strict", action="store_true", help="exit 2 when hard capacity is reached; default mode reports only", ) return parser.parse_args() def main() -> int: args = parse_args() repo = args.repo.resolve() agent_root = args.worktree_root if args.worktree_root.is_absolute() else repo / args.worktree_root used_percent = usable_disk_percent(os.statvfs(repo)) try: paths = agent_worktree_paths(_git_output(repo, "worktree", "list", "--porcelain"), agent_root) except (OSError, subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc: print(f"worktree-capacity: cannot inspect {repo}: {exc}", file=sys.stderr) return 1 report = capacity_report( used_percent=used_percent, paths=paths, clean_paths=clean_worktree_paths(paths), soft_percent=args.soft_percent, hard_percent=args.hard_percent, ) print(json.dumps(report, sort_keys=True)) return 2 if args.strict and report["status"] == "blocked" else 0 if __name__ == "__main__": raise SystemExit(main())
-
-
SKILL.md 5.7 KB
--- name: worktree-agent promoted_to: do description: "Mandatory rules for agents in git worktree isolation." user-invocable: false context: fork tags: [worktree, isolation, parallel, agent] routing: triggers: - "worktree agent" - "git worktree" - "git worktree rules" - "isolated agent" category: git-workflow --- # Worktree Agent Rules Mandatory rules for any agent dispatched with `isolation: "worktree"`. ## Rule 1: Verify Your Working Directory On start, run `pwd`. Your path MUST contain `.claude/worktrees/`. If your CWD is the main repo path, **STOP** and report the error. ## Rule 2: Create Feature Branch First ```bash git checkout -b <branch-name> ``` Never commit on the default `worktree-agent-*` branch. Create your feature branch FIRST. If `git checkout -b <branch-name>` fails with "a branch named X already exists": ```bash # Option A: the branch has no commits beyond main — safe to reset and reuse git branch -D <branch-name> git checkout -b <branch-name> # Option B: the branch is checked out in another active worktree — use a unique name git checkout -b <branch-name>-2 # or append timestamp: $(date +%s) ``` If `git checkout -b <branch-name>` fails with "X is already used by worktree at Y": ```bash # Branch is live in another worktree — use a unique suffix git checkout -b <branch-name>-$(date +%s) ``` To update a branch held by another worktree (e.g. an existing PR branch): work detached from `origin/<branch>` and push with `git push origin HEAD:<branch>`. `gh pr merge`'s post-merge local-checkout errors are harmless. ## Rule 3: Use Worktree-Relative Paths Never hardcode absolute paths from the main repo. Use `$(git rev-parse --show-toplevel)/path`. **Exception**: Reading gitignored ADR files requires the main repo absolute path. ## Rule 4: Ignore Auto-Plan Hooks Keep planning inline instead of creating `task_plan.md`. If the auto-plan hook fires, continue with the current task and keep your attention on implementation. ## Rule 5: Stage Specific Files Only ```bash git add path/to/specific/file.py ``` Never `git add .`, `git add -A`, or `git add --all`. Verify with `git diff --cached --stat`. ## Rule 6: Do Not Touch the Main Worktree Never write to paths outside your worktree directory. Never run `git checkout` in the main repo. ## Rule 7: Commit with Conventional Format Use the commit message specified in your prompt. No attribution lines. ## Rule 8: Run Both ruff Checks Before Declaring CI-Ready For any Python code changes, run both checks before pushing or creating a PR: ```bash ruff check . --config pyproject.toml ruff format --check . --config pyproject.toml ``` Running only `ruff check` misses formatting violations. The `Tests / lint` CI job runs both — if you skip `ruff format --check`, the PR will fail CI and cannot merge due to branch protection. ## Rule 9: Run Preflight Check on Start Run the preflight script at the start of any worktree task to confirm clean state: ```bash bash scripts/worktree-preflight.sh <intended-branch-name> ``` If it exits 1, fix the reported issue before proceeding. ## Rule 10: Reserve disk capacity before creating a checkout The dispatcher runs this before each implementation worktree: ```bash python3 ~/.claude/skills/process/worktree-agent/scripts/worktree_capacity.py \ --repo "$(git rev-parse --show-toplevel)" --strict ``` The JSON report has three states: | State | Dispatcher action | |---|---| | `ready` (<80% used) | Create the one implementation checkout for the candidate. | | `cleanup-soon` (80–<85%) | Reclaim accepted clean checkouts before adding another. Use root read-only review work where possible. | | `blocked` (≥85%) | Integrate, deploy, verify, or reclaim; create no new checkout. | The report lists clean candidates only. The dispatcher confirms their task is inactive before removal because Git cleanliness alone does not prove that fact. ## Rule 11: Assign checkout roles deliberately | Task | Checkout policy | |---|---| | Source implementation or repair | One writable task worktree, reused through review corrections. | | Code review, test-plan review, or read-only investigation | Read the candidate through `git diff` or `git show` from the repository root; allocate no checkout. | | Large repository implementation | Create a sparse worktree containing declared source/test/config scopes; include whole-repository content only when the task requires it. | For a sparse implementation checkout: ```bash git worktree add --no-checkout <worktree-path> -b <branch> <base-sha> git -C <worktree-path> sparse-checkout init --no-cone git -C <worktree-path> sparse-checkout set --no-cone <declared-path>... git -C <worktree-path> checkout ``` Record any full-checkout reason in the dispatch handoff. ## Post-Merge Cleanup After integration or a PR merge, the dispatcher first confirms that the task is inactive and the checkout is clean, then runs: ```bash git worktree remove -- <accepted-worktree-path> bash scripts/worktree-cleanup.sh --force ``` `git worktree remove` frees the materialized checkout while preserving its branch for recovery. The cleanup script then prunes stale `.git/worktrees` entries and removes merged harness branches. ## Failure Modes This Prevents | Failure | Rule | Without It | |---------|------|-----------| | Agent edits main repo files | 1, 6 | Changes leak to main, get stashed/lost | | Context wasted on task_plan.md | 4 | Implementation budget consumed by planning | | Commit on wrong branch | 2 | Orchestrator merges wrong content | | PR has changes from 2 ADRs | 5, 6 | Cross-contamination between agents | | Branch locked by worktree | 2 | Fatal error on checkout | | PR fails CI on format | 8 | Merge blocked; `ruff format --check` was skipped | | New task fails to create worktree | 9 | Branch name collision from prior stale run |
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.