Verbs

agentproto worktree

agentproto worktree ls      [--repo <dir>] [--status] [--json]
agentproto worktree new     <slug> [--repo <dir>] [--base <ref>] [--branch <name>]
                                   [--root <dir>] [--no-setup] [--json]
agentproto worktree rm      <path> [--repo <dir>] [--base <ref>] [--keep-branch]
                                   [--discard-untracked] [--discard-modified] [--json]
agentproto worktree archive <path> [--repo <dir>] [--base <ref>] [--keep-branch] [--json]
agentproto worktree gc      [--repo <dir>] [--apply] [--salvage-dirty]
                                   [--include-detached] [--noise <path,...>] [--json]

Create, inspect, and tear down git worktrees. A pure local shell over @agentproto/worktree — no daemon required.

new provisions under a single worktrees.root so worktrees stop sprawling across hand-picked parents. rm, archive, and gc are deliberately root-agnostic on the other side: they take an explicit <path> and derive everything from that path's own git metadata, so they can also tear down worktrees new didn't create.

worktrees.root

Resolved with the same precedence as every other knob (flag > env > config > default):

Source
--root <dir>flag on new
AGENTPROTO_WORKTREES_ROOTenv
worktrees.root~/.agentproto/config.json
~/.agentproto/worktreesdefault

Subverbs

ls

Lists the repo's worktrees. The plain form parses git worktree list --porcelain — a fast local path, no forge round-trip.

FlagDefaultDescription
--repo <dir>cwdAny dir inside the repo; the main repo root is derived from it.
--statusfalseRun the status engine per entry — adds tree/integration/liveness axes, provenance, and a reclaim/salvage/hold class. Needs gh/GITHUB_TOKEN (memoised in ~/.agentproto/worktree-verdicts.json).
--jsonfalseEmit the entries as JSON.

new <slug>

Creates a worktree at <worktrees.root>/<repoName>/<slug> and writes a creation-provenance marker into its private gitdir.

FlagDefaultDescription
--repo <dir>cwdRepo to cut the worktree from.
--base <ref>origin/mainRef the branch is cut from.
--branch <name>wt/<slug>Branch to create.
--root <dir>(see above)Override worktrees.root for this run.
--no-setupfalseSkip the repo's declarative lifecycle entirely — agentproto.json's setup hooks and worktree.depsCmd/worktree.linkPaths defaults, and the local, gitignored <repo>/.agentproto/worktree.json's depsCmd/linkPaths/copyGlobs/cloneGlobs/writeFiles defaults.
--jsonfalseEmit the provisioned descriptor as JSON.

rm <path>

The honest plain-destructive verb. Stops the worktree's services, runs its committed teardown hooks, then removes it — and refuses a dirty tree unless the flag matching the class of change present authorizes it.

FlagDefaultDescription
--repo <dir>(from <path>)Repo that owns the worktree.
--base <ref>origin/mainRef whose committed teardown hooks run.
--keep-branchfalseKeep the branch; by default it's deleted too.
--discard-untrackedfalseAuthorize discarding unignored untracked files.
--discard-modifiedfalseAuthorize discarding modified tracked files.
--jsonfalseEmit {"removed":path,"branch":str|null} as JSON.

archive <path>

Salvage-then-remove. Snapshots the worktree's uncommitted state to ~/.agentproto/worktree-salvage/ (a changes.patch, a copy of every untracked file, and a MANIFEST.json) before running the same removal as rm with both discard flags granted — so nothing still on disk is lost. If the salvage step fails, nothing is removed.

FlagDefaultDescription
--repo <dir>(from <path>)Repo that owns the worktree.
--base <ref>origin/mainRef whose committed teardown hooks run.
--keep-branchfalseKeep the branch.
--jsonfalseEmit {"archived":path,"branch":…,"salvageDir":path} as JSON.

gc

Classifies every linked worktree, then prints the plan. Dry run by default — nothing is touched without --apply.

ClassDefinition--apply does
reclaim(merged or fresh) + clean + idle, or a clean unpushed worktree whose only commits are mechanical dependency bumps (chore(deps) / fix(deps) subjects and the diff touches only lockfiles + package.json), or a clean, idle worktree whose branch content is provably in base by the branch gc ladder (squash-, patch- or content-merged — never for an open PR or an offline forge)Removes it (plain, non-force git worktree remove — refuses if the tree turned dirty since the plan was made) and deletes its branch.
salvagemerged + dirty, and not written to in the last 15 minutesNothing, unless --salvage-dirty — then archives it (snapshot, then remove).
holdeverything else, including a fresh or merged branch with uncommitted workNever touched, with or without flags.
FlagDefaultDescription
--repo <dir>cwdRepo to sweep.
--applyfalseExecute the plan instead of printing it.
--salvage-dirtyfalseAlso archive salvage-class worktrees.
--include-detachedfalseAlso reclaim clean, idle detached worktrees.
--noise <path,...>.opencode/package-lock.jsonWorktree-relative paths whose dirt is known noise: a worktree dirty only on these counts as clean, and removal restores them before a plain non-force remove. --noise "" disables.
--jsonfalseEmit the plan (or the outcomes, with --apply) as JSON.

Between plan and apply, each entry is re-checked: one that reclassified or vanished is aborted rather than acted on stale.

Examples

# What's out there, and what shape is it in
agentproto worktree ls
agentproto worktree ls --status

# Cut a worktree for a branch off origin/main
agentproto worktree new my-feature
agentproto worktree new hotfix --base origin/release --branch fix/urgent

# Tear one down (refuses if dirty)
agentproto worktree rm ~/.agentproto/worktrees/myrepo/my-feature

# Keep the uncommitted work, then remove
agentproto worktree archive ~/.agentproto/worktrees/myrepo/my-feature

# Sweep: look first, then act
agentproto worktree gc
agentproto worktree gc --apply --salvage-dirty

Spawning an agent straight into a worktree

agent_start takes a worktree field so a spawn isolates itself without a separate worktree new first: worktree: true provisions one (branch wt/<slug> cut from origin/main, slug auto-minted from the session label) and lands the session in it; worktree: { slug, base } pins either. Provisioning (git worktree add plus the repo's own setup hooks, which can run minutes) now returns immediately by default — the session descriptor comes back right away with status "starting", and the resolved cwd is backfilled once the tree is ready. Poll the session's status: it flips to "running" on success, or "error" with a readable lastError on failure — it never sits in "starting" forever. Pass worktree: { async: false } for the old blocking contract (wait for the tree before agent_start returns); wait: true falls back to that same synchronous path automatically (there's no first-turn output to block on otherwise), and combining wait: true with an explicit async: true is rejected outright. It bites only for a root spawn whose cwd is inside a git repo — a spawn made through an orchestrator inherits its parent's tree, and a cwd outside any repo spawns plain.

The daemon can force the behaviour with worktrees.isolation in ~/.agentproto/config.json (or AGENTPROTO_WORKTREES_ISOLATION): always isolates every root spawn, never turns it off (and rejects an explicit worktree), on-request (default) honours the field. The provisioned tree is not auto-removed on session exit — it holds the agent's work; reclaim it with rm / archive / gc above.

Provisioning concurrency

The heavy part of provisioning (depsCmd, cloneGlobs/copyGlobs, and the worktree.setup hooks) is throttled by a daemon-wide scheduler, so a burst of agent_start({worktree}) spawns can no longer run five pnpm installs against one store at once. At most worktrees.provisionConcurrency (default 2, 0 = unlimited; env AGENTPROTO_WORKTREES_PROVISION_CONCURRENCY) heavy segments run at a time; the rest queue FIFO, fair across callers (a parent that already holds a slot goes behind one that holds none). git worktree add and other cheap steps are never queued, and a provisioning with no heavy phase skips the queue entirely. See config-schema for provisionConcurrencyByRepo and the optional provisionLoadFactor load guard.

While a spawn waits, agentproto sessions shows starting queued #2, and agent_sessions_list / session_list carry provisioning: { state: "queued" | "running", position, phase, startedAt }. The daemon also emits session:provisioning events (queued, started, phase, done).

Killing a starting session cancels its provisioning: a queued entry is dropped, and a running install has its whole process tree terminated (SIGTERM to the process group, SIGKILL after a grace period), so no orphaned pnpm survives. A cancelled provisioning removes the half-made worktree and branch; a provisioning that merely fails keeps its worktree for inspection, as before.

Two limits: the queue lives in the daemon, so agentproto worktree new (its own short-lived process) throttles only against itself, not against the daemon's spawns; and the synchronous agent_start path (worktree: { async: false } or wait: true) is throttled and emits events but has no session row yet, so it shows no provisioning field and cannot be cancelled mid-provision.

See also