qwen-code/docs/users/features/worktree.md
易良 1b46d63fb7
fix(core): reap stale worktrees holding only symlinks or nested build output (#12785)
* fix(core): reap stale worktrees holding only symlinks or nested build output

Follow-up to #12763. The shared is-there-work predicate exempted
disposable build output only at the repo root and counted symlinks as
content, so a stale worktree holding only regenerable output was kept
forever: every isolation:'worktree' agent run leaked a worktree and a
worktree-agent-* branch for worktree.symlinkDirectories users, and a
monorepo install (packages/app/node_modules/) pinned the worktree
permanently.

- Skip untracked/ignored status entries that are symlinks: removing the
  checkout only unlinks them, the target lives outside.
- Exempt an ignored directory whose last path segment is in
  DISPOSABLE_IGNORED_ROOTS, so workspace-package installs stay reapable.
  Both reapers keep sharing the one list.
- Log a debug breadcrumb when the status probe itself fails, so the
  sweep's keeping line no longer mislabels a timeout or EACCES as
  uncommitted changes.
- Refresh the stale sweep comment in config.ts.

* fix(core): complete the worktree sweep's symlink waiver and name what it waives

Round-1 review follow-ups on the stale-worktree sweep exemptions:

- A nested `worktree.symlinkDirectories` value (`tools/cache`) never became
  reapable. `symlinkConfiguredDirectories` accepts nested values and has to
  mkdir the untracked parent (`git worktree add` does not create it), and
  nothing writes an exclude rule for linked paths, so git collapses the whole
  subtree to a single `?? tools/` entry that never names the link -- which the
  symlink waiver rejected on its trailing slash. A collapsed directory is now
  exempt only when every entry inside it is itself a symlink; a real file or
  subdirectory beside the links still pins the checkout, and any read error
  fails closed like the status probe does.
- Every waiver now leaves a debug breadcrumb naming the status entry and the
  reason. A waiver authorizes `git worktree remove --force` plus a branch
  delete and destroys the checkout that was its only evidence, so the
  authorizing state has to be readable while it still exists. Stays at debug
  for the reason the sweep's own breadcrumb records.
- The three exemptions are now enumerated consistently at every site that
  lists them: config.ts's sweep comment, both worktreeCleanup.ts comments, and
  worktree.md's guard table, exemption note and troubleshooting entry. The note
  also stops describing the disposable rule as first-segment-only, which the
  nested branch already falsified, and names the collapsed-directory limit.

Tests pin each guard, measured by mutation (all six mutants killed, each by
the case named here): an ignored `.turbo` symlink under a bare rule, so git
reports `!! .turbo`, covers the ignored arm with a name the disposable set
does not; a nested ignored FILE named `coverage` pins the `entry.endsWith('/')`
guard that keeps it from reading as build output; the collapsed-directory pair
pins both the new waiver and its symlink-only limit; and two assertions pin the
waiver breadcrumb.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmuis49v07v

* fix(core): gate the worktree sweep's session-marker waiver on git status

`worktreeHasWork` waived any entry named `.qwen-session` without reading the
status the same loop already computes, so a marker the repository tracks — a
committed fixture, or one staged after the best-effort `info/exclude` write
failed — was waived as though it were the daemon's untracked marker, and that
waiver authorized `git worktree remove --force` plus a branch delete over the
uncommitted edit. Restrict the arm to `!!`/`??`, the two shapes
`writeWorktreeSessionMarker` produces; this only narrows what the startup sweep
and the daemon reaper may delete.

Also corrects the docs bullet, which described the disposable-output exemption
as name-based while `isDisposableIgnoredEntry` waives on the first path segment
alone (`!! dist/archive.zip` is waived by that branch today), and adds the
three fixtures that make the predicate's branches load-bearing: an edit to a
tracked marker, ignored work beside disposable output in one checkout (the loop
must keep scanning past a waiver), and a single ignored file under a disposable
root (the first-segment branch).

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmuj2u2s78e

* fix(core): require a link before waiving a collapsed worktree dir

The collapsed-directory arm of the symlink waiver answered "every entry
inside it is a symlink", which an empty directory satisfies vacuously.
git does list empty *ignored* directories (`!! build/`), so an aged
checkout whose only residue was one got waived under the reason
`symlink` and reaped: the removal breadcrumb named an exemption the
checkout had nothing to do with, contradicting both the predicate's own
JSDoc and docs/users/features/worktree.md. Require at least one link;
the polarity stays preserve-on-doubt.

Pin the two arms no fixture reached, each measured to survive the whole
suite before this commit:

- `?? .qwen-session`. `addWorktreeSessionMarkerExclude` is best-effort
  and swallows every error, so the daemon's own marker can render
  untracked. Dropping the `??` disjunct of the marker arm no longer
  stays green, and the existing `!!` case now records its real git shape
  instead of assuming it.
- `!! .cache/`. A repo that ignores the parent plus a nested
  `worktree.symlinkDirectories` value collapses to an ignored entry that
  never names the link. Narrowing the collapsed arm to `??` no longer
  stays green.

Also correct two comments: a marker merely staged after the exclude
write failed renders `A `, not ` M`; and a tracked marker is preserved
because an edit to a tracked path is indistinguishable from user work,
not because the daemon cannot be its author -- `writeWorktreeSessionMarker`
overwrites a tracked marker unconditionally, so that preserve can be
permanent.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmujjzd0e9a

* docs: align the worktree reap spec with the shared predicate

Step 9 of docs/design/channel-worktree-orphan-reap.md still specified a
strictly stricter gate than the shipped one ("Full `git status
--porcelain` inside the checkout must be empty", with `.qwen-session`
named as "the one exemption"), even though the daemon reaper it
describes calls the same `worktreeHasWork` this PR widens, and the
symlink waiver makes that sentence wrong about the symlink shape. Point
step 9 at the predicate as the enumeration of record instead of
restating an exemption list that can drift, and keep the "untracked
files included" rationale, which is still accurate.

In docs/users/features/worktree.md, qualify the disposable-output bullet
with the status condition the code actually requires (`status === '!!'`
at gitWorktreeService.ts:117): the same tree left untracked -- no ignore
rule reaches it, or the rule was committed after the worktree branch was
cut -- is not exempt and preserves the checkout. Drop the "two shapes"
count for the marker, which `writeWorktreeSessionMarker` contradicts by
producing a third (` M`) on a tracked path, and state the real premise
for preserving that one. Record the at-least-one-link requirement in the
symlink bullet so the three statements of the collapsed rule agree.

No zh-CN counterpart of the design doc exists, so there is nothing to
keep in sync; that translation gap predates this change.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmujjzd0e9a

* fix(core): fail the worktree status probe closed on stderr warnings

Addresses the R4-1 review on #12785. git status answers a directory it
cannot open with a stderr warning and exit 0, so the content is absent
from stdout with no line to keep it — and runGit only keeps stdout.
Before the waivers existed that gap was masked by the entries that
counted as work anyway; with them, a checkout holding an unreadable
directory plus only waivable entries (a node_modules symlink, a nested
disposable install) read as clean and was swept.

Read the probe through runGitCapture and treat any stderr output as
work, with a debug breadcrumb; the catch stays as the timeout and
over-buffer path. The acceptance fixture combines a waivable symlink
with a mode-000 directory and asserts the checkout keeps its .git, its
tracked files and its registration; with the stderr check removed the
fixture loses all three.

* docs: add the zh-CN twin of the orphan-reap worktree design

This PR rewrote step 9 of docs/design/channel-worktree-orphan-reap.md, and
both AGENTS.md ("keep both versions complete and synchronized in the same
change, including when updating an existing design") and
docs/design/README.md ("When updating an existing single-language design,
add the missing version and align the complete pair") assign the missing
language half to the updating change.

Add channel-worktree-orphan-reap.zh-CN.md as a complete translation of the
current English content — same section order and heading levels, technical
identifiers, paths, commands and the leak-map table preserved verbatim — plus
the reciprocal language links immediately below each document title.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmuk39szta9

* docs: name the ignore-rule spelling the disposable waiver depends on

Two statements presented a rule-spelling-dependent rendering as the general
one. isDisposableIgnoredEntry's docblock said "!! packages/app/node_modules/"
is "how git lists the output of an install or build in a monorepo", and
docs/users/features/worktree.md explained a "??"-rendered tree with exactly
two causes ("no ignore rule reaches it" / "the rule was committed after the
worktree branch was cut").

Measured on git 2.45.2 with the probe's own argv
(status --porcelain --untracked-files=normal --ignored=matching), each rule
committed before the tree existed:

  node_modules/            -> !! packages/app/node_modules/     (waived)
  node_modules (bare)      -> !! packages/app/node_modules/     (waived)
  packages/app/.gitignore:
    node_modules/**        -> !! packages/app/node_modules/x/   (preserved)
  **/node_modules/*        -> !! packages/app/node_modules/x/   (preserved)
  **/dist/*.js + an untracked sibling under packages/
                           -> ?? packages/ AND !! packages/app/dist/main.js
                              (preserved, though a rule does reach the file)

So git collapses to "!! <dir>/" only when a rule matches the directory
itself, and a "??" entry describes the rendered line rather than the absence
of a rule. Both sites now say that. Wording only — no behaviour change, the
predicate and both reapers are untouched.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmuk39szta9

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-28 10:07:37 +00:00

26 KiB

Worktrees

Isolate experimental work in a temporary git worktree without leaving your current session. Useful when the model is about to make wide-ranging edits you want to keep separate from your main checkout, or when you want a subagent to work in a sandbox of its own.

Quick Start

Start the session inside a worktree (--worktree flag)

If you know up front that the entire session should run inside a worktree, pass --worktree at launch:

# Auto-generated slug (e.g. tender-jemison-037f0a)
qwen --worktree

# Explicit name
qwen --worktree my-feature

# `=` form (recommended when also passing a positional prompt — see tip below)
qwen --worktree=my-feature

# PR reference — fetches refs/pull/<N>/head from `origin`
qwen --worktree=#4174
qwen --worktree https://github.com/QwenLM/qwen-code/pull/4174

# Continue a previous --worktree session — re-attaches to the existing dir
qwen --resume <session-id> --worktree=my-feature

Tip — bare --worktree followed by a positional prompt is ambiguous. Because --worktree takes an optional value, qwen --worktree "say hi" makes yargs consume "say hi" as the slug (and reject it because of the space). Use one of:

  • qwen --worktree=my-feature "say hi" (always works — explicit slug via =)
  • qwen "say hi" --worktree (positional first, flag at the end → auto slug)
  • qwen --worktree --approval-mode yolo "say hi" (any flag between them anchors the bare form)

Tip — qwen --resume --worktree foo (no session ID) shows an empty picker on first use. The picker scopes to the chosen worktree's session storage; sessions started outside that worktree are not listed. To resume a session that was started inside foo, use qwen --resume <id> --worktree foo directly — the CLI re-attaches to the existing foo/ directory rather than re-creating it.

process.cwd() and the model's workspace are switched to the worktree before the first turn runs. Exit with Ctrl+C twice and the Exit Dialog prompts to keep or remove the worktree.

The --worktree flag cannot be combined with --acp/--experimental-acp — for ACP hosts (like Zed), pass the worktree path as the cwd of the loadSession/newSession request instead.

Or ask mid-session

Alternatively, ask Qwen Code in plain language to create a worktree from inside an existing session:

> start a worktree called experiment-a
Worktree experiment-a created on branch worktree-experiment-a
.qwen/worktrees/experiment-a

From this point on, the model routes every file edit and shell command through .qwen/worktrees/experiment-a/. Your original working directory is untouched.

When you are done:

> exit the worktree and remove it
Removed worktree experiment-a (branch worktree-experiment-a)

If you want to come back later, ask to exit with the worktree kept on disk instead:

> exit the worktree but keep it
Kept worktree experiment-a at .qwen/worktrees/experiment-a

When Worktrees Are Used

Worktrees are activated in four independent paths:

Trigger What happens
You launch with --worktree The CLI creates the worktree before any model turn runs and chdirs the session into it. PR forms (#N, full URL) fetch first.
You explicitly ask for a worktree mid-session Model calls enter_worktree; subsequent file edits go inside it.
You explicitly ask to leave Model calls exit_worktree with keep or remove.
Model spawns a sub-agent with isolation enabled A throwaway worktree (agent-<hex>) is created automatically and cleaned up if the agent has no diffs.

The two mid-session tools (enter_worktree / exit_worktree) are deliberately gated behind explicit phrasing — saying "fix this bug" or "create a branch" will not trigger them. You must say something like "use a worktree", "start a worktree", or "in a worktree". The --worktree CLI flag has no such guard; it always creates one when present.

What Gets Created

Every Qwen-managed worktree is placed under your project's .qwen directory:

<repoRoot>/.qwen/worktrees/<slug>/         # Working directory
                          ↳ branch worktree-<slug>   # Created off your current branch
  • Slug — letters, digits, dot, underscore, hyphen; max 64 chars. If you don't specify a name, an <adjective>-<noun>-<6hex> slug is auto-generated (e.g. tender-jemison-037f0a). PR references produce pr-<N>.
  • Branch — always worktree-<slug>, branched from whichever branch you have checked out when you ask for the worktree (not necessarily the main working tree's HEAD). For PR worktrees the branch is worktree-pr-<N> and is based on FETCH_HEAD (the PR's tip on the GitHub side) rather than your local branch.
  • Hooks — the worktree's core.hooksPath is automatically pointed at the main repo's .husky/ (preferred) or .git/hooks/ so commits inside the worktree still trigger your existing pre-commit / commit-msg hooks.
  • Optional symlinks — directories listed in worktree.symlinkDirectories (see Settings) are symlinked from the main repo into the new worktree so heavy dirs like node_modules can be reused without reinstalling.

The general-purpose worktree path is not configurable — it must live under <repoRoot>/.qwen/worktrees/ so the CLI can find it on restart and on stale-cleanup sweeps. (The unrelated agents.arena.worktreeBaseDir setting controls only Agent Arena worktrees, which use a separate path tree under ~/.qwen/arena/.)

When a worktree is active, the Footer shows a dim indicator on its own row:

⎇ worktree-experiment-a (experiment-a)

If you use a custom status line script, it also receives a worktree object in the JSON payload piped to stdin:

{
  "worktree": {
    "name": "experiment-a",
    "path": "/path/to/repo/.qwen/worktrees/experiment-a",
    "branch": "worktree-experiment-a",
    "original_cwd": "/path/to/repo",
    "original_branch": "main"
  }
}

The payload field is present only when a worktree is active, so a null-check (input.worktree?.name) is enough.

If your custom status line already renders worktree info, you can hide the built-in Footer row to avoid duplication — see Settings below.

Exit Dialog (Ctrl+C / Ctrl+D)

Pressing the quit shortcut twice while a worktree is active opens the Worktree Exit Dialog instead of closing the CLI:

⎇ Active worktree: "experiment-a" (worktree-experiment-a)

  • 2 new commit(s) on worktree-experiment-a
  • 3 uncommitted file(s)
  Removing the worktree will discard everything above.

What would you like to do?
  ○ Keep worktree (exit without deleting)
  ○ Remove worktree and branch (discards 2 commit(s), 3 file(s))
  ○ Cancel (stay in session)

The dialog inspects the worktree on open (git status --porcelain + git rev-list <baseHEAD>..HEAD) and surfaces both counts so you know exactly what you'd be discarding. ESC cancels.

If git status itself fails (e.g. corrupt index, worktree directory was removed under the CLI), the dialog shows a ⚠ Could not measure worktree state warning and the counts may be unreliable — choose Keep or Cancel until you've diagnosed the underlying repo problem.

--resume Restore

The active worktree binding is persisted to a sidecar file alongside your session transcript:

<chatsDir>/<sessionId>.worktree.json

When you launch the CLI with --resume <sessionId> (or pick the session from /resume), three things happen consistently across interactive TUI, headless -p, and ACP/Zed modes:

  1. The sidecar is loaded and the worktree directory is verified to still exist on disk.
  2. If alive, the model receives a one-shot reminder on its very next prompt:
    [Resumed] Active worktree: "<slug>" at <path> (branch: <branch>). Continue using this path for all file operations.
    
  3. If the worktree directory was deleted between sessions, the stale sidecar is cleaned up automatically — no error, the resume just continues without worktree context.

Each mode chooses its own injection mechanism, but the user-visible behavior is identical:

Mode Mechanism
Interactive (TUI) INFO history item + system-reminder prefix on the next user prompt.
Headless (-p) <system-reminder> prefix on the prompt + worktree_restored JSON system event in the output stream.
ACP (e.g. Zed) Pending notice attached to the next prompt() call.

The model is not automatically chdir'd into the worktree — the reminder is what keeps it routing edits through the worktree path.

Sub-Agent Isolation

The agent tool accepts an optional isolation: "worktree" parameter. When set, Qwen Code creates an ephemeral worktree at <repoRoot>/.qwen/worktrees/agent-<7hex>/ before the sub-agent starts, and:

  • No changes → the worktree is automatically removed when the agent finishes.
  • Has changes → the worktree is preserved; its path and branch are appended to the agent's result, e.g.
    …agent output…
    [worktree preserved: /path/to/.qwen/worktrees/agent-3f2a1b9 (branch worktree-agent-3f2a1b9)]
    
    Review the diff and merge or delete it manually.

Two constraints:

  • isolation: "worktree" requires a non-fork subagent_type — forked sub-agents (subagent_type: "fork") reuse the parent's full conversation context, so isolating them would split intent from working tree.
  • Agents using isolation: "worktree" follow the default background behavior; the cleanup runs when the agent reports completion. Set run_in_background: false for an inline result. Unnamed caller-owned working_dir launches run in the foreground; explicit background execution is rejected, while configured background execution (background: true in a subagent definition) is rejected at the top level and downgraded to a foreground run when nested because their lifecycle is managed externally.

Automatic Stale Cleanup

Ephemeral agent worktrees that survived a crash or --no-cleanup shutdown are reaped on every CLI startup, with conservative fail-closed rules:

Guard Behavior
Slug must match agent-<7hex> pattern Other names are never touched. A worktree you named exactly agent-<7hex> is indistinguishable from an ephemeral agent worktree and is a sweep candidate.
Directory mtime > 30 days Newer entries are skipped.
Any uncommitted work — tracked, untracked, or git-ignored Skip the entry (don't delete). Only disposable build output, symlinks and the session marker are exempt — see the note below.
Any commit not reachable from a remote Skip the entry (don't delete).
Any error reading git state Skip the entry (don't delete).

Named user worktrees (enter_worktree slugs) are never auto-cleaned, with one exception: the reserved agent-<7hex> shape, which the sweep cannot tell apart from an ephemeral agent worktree, is reaped after 30 days once it holds no uncommitted content and no unmerged commits. Pick any other name for work you want kept.

The uncommitted-content guard reads git status --porcelain --untracked-files=normal --ignored=matching, so even content your ignore rules hide — a .env, .qwen/pr-drafts/ — preserves an aged agent-<7hex> worktree. The only exemptions are:

  • Disposable build output — an ignored entry whose first path segment is node_modules, dist or coverage, so that output and anything git lists under it, a single ignored file included (!! dist/archive.zip under a *.zip rule). Also an ignored directory nested inside the checkout whose own name is one of those three (a monorepo's packages/app/node_modules/) — but only when git collapses the entry at that directory, which needs an ignore rule matching the directory itself (node_modules/, or a bare node_modules). A rule matching the directory's contents instead (**/node_modules/*, or a package-local node_modules/**) makes git list the files individually (!! packages/app/node_modules/x/), which is not exempt and preserves the checkout. A nested ignored file with one of those names is not build output and still preserves the worktree, so go test -coverprofile=coverage writing packages/app/coverage keeps the checkout — but nothing stored under a root dist/, coverage/ or node_modules/ is kept. The same tree left untracked, so git says ?? dist/, is not exempt and preserves the checkout. Note that ?? describes the rendered entry, not the absence of a rule: git collapses an untracked parent (?? packages/) whenever it holds any non-ignored untracked file, and lists ignored files inside it separately as !! at the same time. The usual reasons an entry renders ?? are that no ignore rule reaches it — including a rule whose spelling does not reach that path, since a root node_modules/** covers only the root-level tree — or that the rule was committed after the worktree branch was cut.
  • Symlinks — removing the checkout unlinks them and never touches what they point at, which is what keeps worktree.symlinkDirectories checkouts reapable. A nested value (tools/cache) makes git collapse the subtree to a single ?? tools/ entry, so a collapsed directory qualifies only when it holds at least one entry and everything inside it is itself a symlink; one real file beside the links — or an empty directory, which holds no link to speak of — preserves the worktree.
  • The daemon's .qwen-session marker, as long as git lists it as untracked or ignored. A marker the repository actually tracks is not exempt: an edit to a tracked path is indistinguishable from user work, and writeWorktreeSessionMarker overwrites a tracked marker unconditionally, so the daemon itself can be the author of that edit — the preserve then holds until the marker is committed or reverted.

A path git quotes because it contains special characters is not resolved and counts as work.

Safety Guards on exit_worktree action="remove"

Three independent guards trigger before the directory and branch are deleted:

  1. Session ownership — each worktree carries a sidecar marker with the session ID that created it. A different session trying to remove it is refused with a clear error pointing at git worktree remove for the manual escape hatch.
  2. Dirty working tree — uncommitted tracked or untracked changes block removal. Pass discard_changes: true to override. (Bypass requires explicit user confirmation — action: "remove" is never auto-approved in AUTO_EDIT mode.)
  3. Unmerged commits — commits on worktree-<slug> that no other local branch or remote ref points at block removal unconditionally; there is no "discard commits" flag because losing committed work is rarely what users mean. Merge, push, or rename the branch elsewhere first.

The same three guards apply to the WorktreeExitDialog → Remove button.

Settings

Two settings shape the general-purpose worktree experience:

Key Type Default Effect
ui.hideBuiltinWorktreeIndicator boolean false Hides the built-in ⎇ worktree-… (…) Footer row. The worktree field is still delivered to custom status line scripts. Set to true only if your status line already renders the worktree — otherwise you lose all UI affordance.
worktree.symlinkDirectories string[] undefined Directories under the main repo to symlink into every general-purpose worktree on creation. Paths are relative to the repo root; absolute paths and any entry containing .. are rejected. Missing sources and existing destinations are silently skipped (no overwrite).

Example:

// ~/.qwen/settings.json or <repo>/.qwen/settings.json
{
  "worktree": {
    "symlinkDirectories": ["node_modules", ".turbo", "dist"],
  },
}

Applies to ALL worktree-creation paths: --worktree flag, enter_worktree tool, and agent isolation: "worktree".

Settings unrelated to general worktrees but worth knowing about:

  • agents.arena.worktreeBaseDir — controls Agent Arena worktree placement (default ~/.qwen/arena). Does not affect general-purpose worktrees, which always live under <repoRoot>/.qwen/worktrees/.

There is no schema for worktree.sparsePaths yet — that's a roadmap item (see Limitations).

Tool Reference

enter_worktree

{ "name": "experiment-a" }
Field Type Required Notes
name string no Slug. Letters, digits, dot, underscore, hyphen; max 64 chars. Auto-generated when omitted.

Refuses to run when:

  • The CLI is not in a git repository.
  • The current working directory is already inside .qwen/worktrees/ (no nested worktrees).

exit_worktree

{ "name": "experiment-a", "action": "remove", "discard_changes": false }
Field Type Required Notes
name string yes Must match the slug used in enter_worktree.
action "keep" | "remove" yes keep preserves dir + branch; remove deletes both.
discard_changes boolean only when action="remove" and dirty Overrides the dirty-tree guard. Has no effect for action="keep".

action: "remove" always prompts for confirmation, including under AUTO_EDIT approval mode — it is treated as a destructive shell operation, not an info-only tool.

agent — isolation parameter

{
  "subagent_type": "my-agent",
  "description": "…",
  "prompt": "…",
  "isolation": "worktree"
}
Field Type Required Notes
isolation "worktree" no Runs the agent in a fresh agent-<7hex> worktree. Requires subagent_type to be set (no forks).

See Sub-Agents for the rest of the agent tool reference.

CLI Reference

--worktree [name | #N | url]

qwen --worktree                                               # auto-generate slug
qwen --worktree my-feature                                    # explicit slug
qwen --worktree=my-feature                                    # = form
qwen --worktree=#123                                          # PR reference
qwen --worktree https://github.com/owner/repo/pull/123        # PR URL
Input Result
Bare flag (no value) Auto slug <adjective>-<noun>-<6hex>, branch worktree-<slug>, base = current branch.
Plain slug Branch worktree-<slug>, base = current branch. Slug validation: letters/digits/dot/underscore/hyphen, max 64 chars.
#N or <github-url>/pull/N Slug pr-<N>, branch worktree-pr-<N>, base = FETCH_HEAD after git fetch origin pull/<N>/head (30s timeout).

--worktree cannot be combined with --acp / --experimental-acp.

When --worktree is combined with --resume <session-id>, the worktree wins: the resumed session's saved worktree (if any) is overridden and a stderr line + first-prompt reminder report the override.

For interactive (TUI) and headless (-p) modes the worktree is automatically created and the session chdirs into it before the first turn.

PR-fetch failure modes (exit code != 0, no worktree created):

Cause Message excerpt
Missing origin remote requires an "origin" remote that points at GitHub
PR doesn't exist on origin Failed to fetch PR #<N>: the PR does not exist on origin
30s network timeout Failed to fetch PR #<N>: timed out after 30s
PR number out of range / zero Invalid PR number

Limitations

The following items are intentionally not implemented in the current phase:

  • No sparse checkout. Large monorepos check out the full tree. (worktree.sparsePaths is a roadmap item.)
  • No tmux integration. The CLI does not spawn worktree sessions in new tmux windows.
  • Worktrees are separate "projects" for session storage. Sessions started with --worktree foo are saved under that worktree's chats dir; to resume them later you must pass --worktree foo again. Sessions started without --worktree are saved under the main checkout and won't appear in the worktree's resume picker.
  • No cross-slug session override. qwen --resume <sid> --worktree second where <sid> was created with --worktree first will fail to find the session — sessions and worktrees are tightly bound by projectHash(cwd). To switch worktrees on an existing session you must exit, then re-launch with the new --worktree and a fresh prompt. A future architectural change (anchoring storage at the repo root instead of cwd) would lift this constraint.
  • Mid-session enter_worktree does NOT switch process.cwd() or Config.targetDir. That tool uses the model-context-only convention (see Sub-Agents). Only the startup --worktree flag actually switches the process working directory.
  • Relative paths in other arg fields are resolved BEFORE the worktree chdir. Path-taking flags (--mcp-config, --openai-logging-dir, --json-file, --input-file, --telemetry-outfile, --include-directories) are normalized to absolute paths against the launch cwd when --worktree is set. Other path-shaped argv fields not in this list still resolve against the worktree cwd — use absolute paths to be safe.

Track the roadmap in docs/design/worktree.md.

Troubleshooting

The Footer shows no worktree indicator even though I just created one. Check that ui.hideBuiltinWorktreeIndicator is not set to true. Also confirm the slug is non-empty in the tool's success message.

--resume does not restore my worktree. Check <chatsDir>/<sessionId>.worktree.json exists. The CLI deletes the sidecar automatically when the worktree directory is gone, so a missing sidecar plus a missing directory is the normal "no worktree to restore" state — not a bug. Run with --debug and grep for restoreWorktreeContext to see the reason.

exit_worktree says "created by a different session". This is the session-ownership guard. Resume the original session and exit from there, or run the suggested git worktree remove … command manually.

Stale agent-<hex> worktrees keep piling up. The 30-day cutoff is conservative; sweep manually with git worktree list && git worktree remove <path>, or wait — the next CLI startup after the 30-day mark will reap them as long as they hold no uncommitted work (tracked, untracked, or git-ignored beyond disposable build output and symlinks) and no unmerged commits. Run with --debug and grep for cleanupStaleAgentWorktrees: keeping to see which entry the sweep refused and why, or worktreeHasWork: waiving to see which entries authorized a removal and under which exemption.