This is the canonical task guide for isolated contributor work. Keep the main checkout untouched
when it contains user changes; create a sibling linked worktree from the current origin/main.
MAIN_REPO_ROOT="$(git rev-parse --show-toplevel)"
WORKTREE_PARENT="$(dirname "$MAIN_REPO_ROOT")/$(basename "$MAIN_REPO_ROOT").worktrees"
git fetch origin main
scripts/dev/create_worktree.sh \
--branch issue-123-short-description \
--path "$WORKTREE_PARENT/issue-123-short-description" \
--base origin/main \
--task-id issue-123
cd "$WORKTREE_PARENT/issue-123-short-description"
scripts/dev/bootstrap_worktree.shThe capacity-guarded helper must create the worktree before editing, running PR validation,
pushing, or publishing. Active task-owned worktrees should pass --task-id; the creator writes a
path-scoped ownership lease before it releases the repository worktree mutation lock. This makes
creation and repository-owned cleanup atomic with respect to task ownership. Short-lived human
scratch worktrees may omit the task lease when no autonomous worker can race their teardown.
Use --exec when the first command must be bound to the new directory:
scripts/dev/create_worktree.sh \
--branch issue-123-short-description \
--path "$WORKTREE_PARENT/issue-123-short-description" \
--base origin/main \
--task-id issue-123 \
--exec git rev-parse --show-toplevelImplementation branches fetch the latest origin/main and merge it early:
git fetch origin main && git merge origin/main.
That early merge is for development only. The pre-publication ancestry gate requires the branch's
merge base to equal the live origin/main tip at check time; if origin/main advances after that
merge, the branch is classified undeclared_stack and publication stops. Rebase the intended
commits onto the current tip before the final readiness run and publication, because the rebase
moves the head and invalidates an existing readiness stamp. See the pre-publication ancestry note
in docs/dev_guide_reference.md and issue #8864.
Read-only review worktrees or passes record target/base/head SHAs and inspect or fetch as needed.
Never merge origin/main into the implementation branch or push to it during review. Ordinary Git
invocations use the machine guard (scripts/dev/review_worktree_guard.py, issue #8321); deliberate
override or alternate receive-pack probes must run as descendants of its Linux Landlock run
boundary, because raw commands launched outside that process are not adversarially isolated.
Repository-owned automatic cleanup is fail-closed: a linked worktree is not removable when its
tracked or untracked state is dirty, ignored content is present, an open pull request (PR) covers
its branch, push state cannot be proven safe, or an active task/gate lease exists. Use the reaper's
JSON output as the decision record; refused candidates include reason_codes, the path/branch/HEAD
identity, lease owner fields when available, and a recovery object that requires preservation and
review before reclaim.
uv run python scripts/dev/stale_worktree_reaper.py \
--path "$WORKTREE_PATH" \
--apply \
--jsonThe apply path repeats every preservation read while holding the shared worktree lifecycle lock.
This closes the gap between a dry-run plan and removal: a new dirty file, ignored output, open PR,
unpushed state, unreadable check, or lease acquired after planning becomes a structured refusal and
the worktree remains present. --skip-pr-check is therefore also a refusal signal, not permission
to delete offline.
The incident's separate external/app cleanup caller has not been located in this repository. This change does not claim to repair or intercept that caller. Until it is identified and routed through this contract, direct cleanup outside the reaper is unsupported and must be treated as unsafe; the repository can only guarantee the behavior of its supported reaper and lifecycle helpers.
For --mode review, the creator launches that optional first command through the Linux Landlock
process boundary. Keep later review commands as descendants of it, or invoke the guard's run -- ...
form explicitly; the mode marker and Git hook do not attach an OS policy to future raw processes.
New branches are created without automatic upstream tracking. This avoids concurrent workers
contending on the shared repository configuration while they create linked worktrees. Configure a
remote explicitly when publishing a branch, for example with git push -u origin <branch>.
The creation helper clears any config.worktree copied from the invoking checkout before applying
the requested mode. Review-only push barriers therefore cannot leak into a newly created
implementation worktree, and implementation worktrees do not inherit arbitrary per-worktree
settings from a protected review checkout.
The fleet grows between manual reclaims, so run the TTL prune on a recurring cadence (for example weekly) instead of waiting for disk pressure:
uv run python scripts/dev/worktree_ttl_prune.py --ttl-days 7 --json # dry run
uv run python scripts/dev/worktree_ttl_prune.py --ttl-days 7 --apply --json # pruneThe helper composes the read-only hygiene retirement plan (--hygiene-json can reuse a prior scan
snapshot) and selects a path only when the assessment already says removeable, no active claims or
preservation evidence exist, and the worktree's newest activity is older than the TTL. Dirty,
unpushed, claimed, review-only, and young worktrees are reported with stable skip reasons and are
never touched. --apply delegates each selected path to stale_worktree_reaper.py --apply --path,
so the reaper's fail-closed checks and lifecycle lock remain authoritative; this helper never
removes a worktree itself.
Review and synthetic-integration worktrees must opt into the protected mode explicitly:
scripts/dev/create_worktree.sh \
--branch review/pr-123 \
--path "$WORKTREE_PARENT/review-pr-123" \
--base origin/main \
--mode review \
--task-id review-pr-123
scripts/dev/run_worktree_shared_venv.sh -- uv run python scripts/dev/review_worktree_guard.py integrate \
--worktree "$WORKTREE_PARENT/review-pr-123" \
--source-ref origin/main \
--remote originFocused test runs inside the Landlock boundary must keep their cache and log outputs inside the worktree; otherwise numba aborts collection and pytest's logging handler fails on an unwritable path, which are tooling-isolation failures and never pass evidence. Print the exact recipe with:
scripts/dev/run_worktree_shared_venv.sh -- uv run python scripts/dev/review_worktree_guard.py \
pytest-isolation --worktree "$WORKTREE_PARENT/review-pr-123"Apply its env and pytest_arguments to the guarded invocation; the process exit status remains
authoritative over any printed test summary.
The creator writes the worktree-local robot-sf.worktree-mode=review marker and installs the
tracked pre-push guard. Configured remote names also receive inert worktree-local push destinations
and a nonexistent worktree-local receive-pack command. Push-specific URL rewrites (plus exact
configured-URL rules) route push URLs to an inert path, covering direct pushes, equivalent local-path
spellings, explicit destination refspecs, and --no-verify for configured remotes. Push rejection is
separated from read-only URL resolution: direct git fetch and git ls-remote commands remain
operational for inspection and ref verification. A remote added after activation with an explicit
remote.<name>.pushurl is still protected on the ordinary hook path, but Git does not apply
pushInsteadOf to that explicit value when --no-verify bypasses the hook. That deliberate
configuration/command-line override belongs to the stronger process boundary described below. This
is a Git-level workflow guard, not an operating-system sandbox; a deliberate per-command Git
configuration override can bypass it.
Review setup never masks url.*.pushInsteadOf entries by editing the shared Git config. If an
effective repository, global, system, or pre-existing worktree url.*.pushInsteadOf alias could
outrank the worktree barrier, setup fails closed before enabling review mode; remove or relocate
the alias and retry. This refusal is required because a guard-specific lock would not serialize
arbitrary Git processes in other linked worktrees. Generic url.*.insteadOf entries remain intact
for read URL resolution.
The stronger adversarial process boundary is explicit and must wrap any command that may reach a local remote or invoke an alternate receive-pack:
python "$MAIN_REPO_ROOT/scripts/dev/review_worktree_guard.py" run \
--worktree "$WORKTREE_PARENT/review-pr-123" -- \
git -c url.<actual-file-url>.insteadOf=<blocked-file-url> push \
--no-verify --receive-pack=git-receive-pack origin HEAD:refs/heads/exampleOn Linux with Landlock application binary interface (ABI) 4 or newer, run is a real
descendant-inherited operating-system boundary: it allows reads and execution throughout the host,
permits filesystem mutation only below the review worktree and its linked Git admin directory,
closes inherited file descriptors,
and denies TCP bind/connect. It therefore protects a temporary bare remote outside those writable
roots even when Git URL configuration, --receive-pack, and --no-verify are supplied on the
command line. Direct alternate receive-pack descendants inherit the same policy. The command's
output and exit status are preserved.
run fails closed on non-Linux hosts, unsupported architectures, kernels without the required
Landlock ABI, unavailable /proc/self/fd inspection, or any policy-installation error. Landlock
is Linux-specific, so this is not a portable all-host guarantee. The process boundary is not
attached to a directory: a new terminal or a raw /usr/bin/git invocation launched outside run
is outside the contract. Use run -- ... bash for a bounded multi-command session. The strict
boundary denies TCP, so stage network refs before entering it; Unix-domain/existing privileged
helper channels, remotes placed inside the two writable roots, and privileged host escape remain
outside this bounded threat model. This paragraph describes the OS policy; the Git configuration
above remains only a defense-in-depth workflow guard.
For a local bare-remote or otherwise network-free synthetic integration probe, the integration helper can itself be made a descendant of the boundary:
python "$MAIN_REPO_ROOT/scripts/dev/review_worktree_guard.py" run \
--worktree "$WORKTREE_PARENT/review-pr-123" -- \
python "$MAIN_REPO_ROOT/scripts/dev/review_worktree_guard.py" integrate \
--worktree "$WORKTREE_PARENT/review-pr-123" --source-ref origin/main --remote originIf the selected base predates the guard files, create_worktree.sh --mode review keeps the target
clean and temporarily points its worktree-local hooks path at the invoking checkout's tracked
guard and hook. Keep that invoking checkout available until the review worktree is restored or
removed; once the guard is present in the base, the target uses its own tracked files. In this
fallback, invoke the integration helper through the invoking checkout's wrapper, for example
"$MAIN_REPO_ROOT/scripts/dev/run_worktree_shared_venv.sh" --standalone -- python "$MAIN_REPO_ROOT/scripts/dev/review_worktree_guard.py" integrate --worktree <review-worktree> --source-ref origin/main --remote origin; the target does not contain the helper yet.
The integration helper snapshots every ref from git ls-remote --refs, runs
git merge --no-commit --no-ff, always attempts git merge --abort, restores the pre-probe
ORIG_HEAD pseudo-ref, and exits nonzero unless the worktree is clean and the before/after remote
snapshots are identical.
Ordinary implementation worktrees retain the default pushable behavior. To deliberately restore a
previously protected worktree, run scripts/dev/run_worktree_shared_venv.sh -- uv run python scripts/dev/review_worktree_guard.py configure --worktree <path> --mode implementation; the helper restores the worktree-local hook and push
configuration captured when review mode was enabled. Re-fetch origin/main before creating a new
review worktree so its source ref is explicit and current.
--task-id is the owner-visible lifecycle marker for an autonomous task. The lease lives in the
shared Git common directory, not inside the linked worktree, so it remains inspectable if the
worktree directory disappears. The compatibility schema is still pr_gate_lease.v1; for ordinary
task-owned worktrees the creator records the task id in the existing gate/owner fields instead of
introducing a second lease format.
The default lease lifetime is two hours. Long-running controllers should heartbeat at phase or handoff boundaries before the current lease expires:
uv run python scripts/dev/pr_gate_lease.py heartbeat \
--worktree "$WORKTREE_PATH" \
--extend-hours 2Run heartbeat/release commands from any surviving checkout of the same repository. Do not depend on the leased worktree as the command's current directory: the recovery path exists specifically for a missing worktree directory. When the task has completed or emitted a durable handoff, release its lease explicitly:
uv run python scripts/dev/pr_gate_lease.py release --worktree "$WORKTREE_PATH"The script is also directly executable (scripts/dev/pr_gate_lease.py release --worktree "$WORKTREE_PATH")
and accepts --worktree either before or after the subcommand (scripts/dev/pr_gate_lease.py --worktree "$WORKTREE_PATH" release).
Lease creation, heartbeat, release, and guarded cleanup all serialize on the existing
robot-sf-create-worktree.lock identity. Cleanup therefore cannot observe "no lease" and then
remove the worktree while a task concurrently claims it. Expired leases are deliberately non-live:
a crashed owner that never releases eventually stops blocking safe cleanup after its TTL.
Malformed or unreadable lease state fails closed.
If an active task reports that its worktree path disappeared while its branch ref survived, do not continue commands from the deleted current working directory. Move to a surviving checkout and inspect the leased path:
uv run python scripts/dev/gate_worktree_guard.py verify \
--path "$WORKTREE_PATH" \
--jsonThe JSON health result is the compact recovery handoff: for a live lease it names the owner/task, PR/gate identifiers when present, expiry, path, branch, and HEAD. A missing result always includes a recovery loss boundary: branch recreation can restore the checkout only, while dirty, untracked, and ignored local state cannot be recovered by this guard. To recreate the checkout from the persisted branch/commit metadata, use:
uv run python scripts/dev/gate_worktree_guard.py ensure \
--path "$WORKTREE_PATH" \
--jsonThe recovery.loss_boundary value is dirty_untracked_ignored_state_not_recoverable, and
local_state_restored remains false even when recreation succeeds. Recover that state from a
durable handoff or backup before continuing. The guard never chooses a replacement branch or
transport policy.
Repository-owned delegated workers can additionally opt into an immutable, credential-free receipt and bind their first command to the new worktree:
scripts/dev/create_worktree.sh \
--branch issue-123-short-description \
--path "$WORKTREE_PARENT/issue-123-short-description" \
--base origin/main \
--receipt "/path/to/private/issue-123.receipt.json" \
--task-id issue-123 \
--exec <worker-command>--task-id creates the lifecycle lease regardless of whether --receipt is supplied. When a
receipt is requested, create_worktree.sh validates the receipt path before creating the worktree
and writes it atomically after the linked worktree exists. Relative paths starting with .git/ or
./.git/ resolve through the linked-worktree .git file indirection to the repository's shared
common Git directory, matching primary-checkout behavior. Other relative paths resolve relative to
the invoking working directory. Validation fails closed with exit code 2 before git worktree add
if the receipt path contains symlink components, references an existing directory or file, has
non-directory parent components, or target directories that are not writable. Paths can be verified
independently with python3 scripts/dev/worktree_receipt.py resolve-path --receipt PATH.
The --exec command is guarded before it starts; the read-only receipt check exits nonzero with one
JSON result when the current working directory, top-level, shared Git directory, branch/ref, or base
ancestry differs. Workers started separately must run the equivalent check from inside the assigned
worktree with scripts/dev/run_worktree_shared_venv.sh -- uv run python scripts/dev/worktree_receipt.py check.
The receipt proves assignment identity; the lease protects the active path from repository-owned
cleanup. They are deliberately separate contracts.
Bootstrap symlinks the local machine context and creates a worktree-local .venv. Do not run a
bare uv run ... first in a fresh worktree: it can materialize a partial local environment, which
then shadows the shared environment selected by later commands. For a cheap targeted check, route
the command through the shared environment wrapper instead:
scripts/dev/run_worktree_shared_venv.sh -- \
uv run python scripts/dev/check_worktree_optional_deps.py --profile all-extrasIf a worktree-local environment is intentional, create and sync it with
scripts/dev/bootstrap_worktree.sh before using it. If a bare invocation has already created an
accidental partial .venv, stop using that environment and follow the bootstrap/wrapper path after
confirming it contains no worktree-local state that needs preserving.
If the shared wrapper reports stale fast-pysf, use its explicit linked-worktree recovery option:
scripts/dev/run_worktree_shared_venv.sh --recover-stale-fast-pysf -- <command>. It creates or
refreshes only the current worktree's .venv, applies the capacity and repository recovery-lock
gates, rejects nested environment links that could redirect package writes outside the worktree, and
rejects broken or owning-checkout bin/python* aliases, and fails closed if a nested environment
subtree cannot be inspected. It never repairs the main checkout implicitly. See the local CI
recovery contract.
Never edit .venv by hand; manage dependencies through pyproject.toml and uv sync. Never use a bare
git stash pop in a linked worktree because all worktrees share one stash namespace. Prefer a
temporary commit or scripts/dev/safe_stash_pop.sh.
Before retirement, inspect the exact worktree and enumerate ignored outputs:
git worktree list --porcelain
git -C "$WORKTREE_PATH" status --short --branch
uv run python scripts/dev/worktree_hygiene_snapshot.py \
--repo-status --retirement-plan --jsonPreserve tracked changes, unpushed commits, and ignored-but-important evidence before removal.
Classify output/ as temporary scratch, durable evidence, or handoff-needed; worktree-local output
is not durable storage. Once the owning task has completed, release its lease and use the targeted
reaper path rather than a bare git worktree remove:
uv run python scripts/dev/pr_gate_lease.py release --worktree "$WORKTREE_PATH"
uv run python scripts/dev/stale_worktree_reaper.py \
--path "$WORKTREE_PATH" \
--apply \
--json
git worktree pruneSee also: Artifact Retention, Preservation, and Cleanup Guide for retention classes, preservation proof, and cleanup-eligibility gates.
The targeted reaper retains the existing dirty/unpushed/open-PR/ignored-output checks. Immediately before removal it reacquires the shared lifecycle lock and re-reads the lease. If another task claimed the worktree after planning, cleanup refuses it and the audit log names the live task/owner. An unreadable lease also refuses removal. An expired lease does not block cleanup, providing the bounded stale-owner recovery path.
Do not remove a dirty worktree, an unpushed branch, a live-leased worktree, or a durable artifact without an explicit preservation record.
When a completed pull request (PR) has had its remote branch deleted, an explicitly supplied proof bundle can authorize retirement of that one clean worktree without recreating the remote reference. The default reaper remains conservative: a missing upstream is still refused unless all verified inputs are supplied. GitHub's pull-request REST metadata does not provide an authoritative historical merge-method field, so this mode deliberately proves exact merged-tree identity and ancestry without claiming that the PR used squash, rebase, or regular merge.
Run the verified mode from the registered main worktree and name the target's canonical,
non-symlink path, surviving local branch, full head commit, and merged PR:
uv run python scripts/dev/stale_worktree_reaper.py \
--path "$WORKTREE_PATH" \
--verified-merged-pr 8658 \
--verified-branch feature/example \
--verified-head-sha <40-character-head-sha> \
--jsonThe read-only proof requires the target path to be an exact registered non-current worktree with a
surviving local branch. That branch must retain branch.<name>.remote=origin and the matching
branch.<name>.merge configuration while its refs/remotes/origin/<name> object is absent;
never-published branches and branches with a still-resolvable upstream remain refused. Fresh
authoritative PR metadata must identify this repository, the exact branch and head, a merged PR
to main, and a full merge commit that resolves locally. The checked-out main, local
origin/main, authoritative origin/main from a fresh git ls-remote --heads read, target head,
PR merge commit, and all three complete Git tree IDs must agree as required by the plan, and the
merge commit must be an ancestor of current main. The same authoritative read must show the
candidate branch absent; a stale local remote-tracking ref is not treated as proof of deletion.
The resulting
verification evidence records merge_method_scope=method_agnostic_exact_tree; it is a content
and ancestry proof, not a merge-method classifier.
The command performs only bounded GitHub reads (gh pr list and gh api) and a read-only
authoritative git ls-remote; it never posts, edits, fetches, mutates local refs, recreates refs,
changes configuration, releases leases, or removes another worktree. The
JSON candidate's verification object and audit log expose the exact repository, PR, branch,
head, merge, main, tracking-ref, and tree identities plus the narrowly discharged
missing origin upstream only risk. Existing dirty/untracked/ignored-content, open-PR,
current-worktree, lease, and lifecycle-lock gates still apply. A verified candidate is safe to
remove only through the canonical apply path:
uv run python scripts/dev/stale_worktree_reaper.py \
--path "$WORKTREE_PATH" \
--verified-merged-pr 8658 \
--verified-branch feature/example \
--verified-head-sha <40-character-head-sha> \
--apply --jsonImmediately before the normal, non-force git worktree remove, the reaper reacquires the shared
lifecycle lock and repeats registration, path, branch, head, ref, tree, main, authoritative remote,
PR, cleanliness, ignored-state, open-PR, upstream-absence, and lease reads. It then performs a
final registration/path, branch, HEAD/ref-identity, cleanliness, and ignored-state preservation
recheck after the last remote/PR read and immediately before removal, closing the remaining window
in which an ignored artifact or target identity could drift during verification. Any deletion,
symlink/path alias, identity drift, new content, lease, lookup error, or lock failure refuses
removal. Normal
worktree removal preserves the local branch and its commits; artifact preservation remains the
owner's responsibility before retirement.