mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-03 21:34:40 +00:00
* docs(design): define default background subagents * feat(core): improve subagent delegation defaults * docs(core): cross-reference the three background-classification sites Add pointer comments linking the core dispatch source of truth (AgentTool.execute) and its two UI mirrors (web-shell isBackgroundSubAgentToolCall, desktop detectBackgroundEvents) so the replicated top-level-agent background heuristic is not changed in isolation. Addresses PR review feedback. * fix(core): align background classification for fork and named-teammate launches Address review feedback on the background-classification rule so core dispatch and the two UI classifiers stay consistent: - core: exclude a name-without-active-team launch from the default-background path so it stays foreground, matching both UI classifiers (which exclude name). Previously such a launch was backgrounded by core but tracked as foreground by the UIs. - web-shell and desktop classifiers: exclude subagent_type "fork" from the default-background heuristic, mirroring core's !isForkRequested guard. A top-level fork request with an omitted flag runs foreground in core but was classified as background by the UIs. - add a core dispatch test asserting a working_dir launch with an omitted run_in_background flag stays in the foreground. * test: cover fork/background classification and precedence per review feedback Address unresolved review threads on PR #7048: - Add web-shell and desktop UI classifier tests asserting an omitted-flag `subagent_type: "fork"` launch stays in the foreground, verifying the documented `!isForkRequested` parity with core dispatch. - Add a core AgentTool test asserting an explicit `run_in_background: false` overrides a subagent config with `background: true`, locking in the `run_in_background ?? config` precedence against a `||` regression. - Harden the Explore read-only prompt: pipelines must not send data to a network endpoint (no curl/wget/nc), closing the `cat file | curl` exfiltration gap. * fix(core): restore general no-unnecessary-files guard in general-purpose prompt Address review feedback: the rewritten general-purpose prompt dropped the broad guard against creating unrequested files, keeping only the documentation-specific line. Restore a general 'do not create files unless necessary' guard so speculative utility/config files are not created. * test(desktop): cover named-teammate foreground guard in detectBackgroundEvents Add a desktop tool-matching test asserting a top-level Agent with a `name` set (named teammate) stays foreground and emits no task_backgrounded event, mirroring the web-shell classifier's named-teammate coverage and the existing fork-exclusion test. * test(core): cover named-teammate foreground dispatch when flag omitted Add a core-dispatch test asserting a top-level Agent launch with `name` set and `run_in_background` omitted stays foreground when no team is active, guarding the `this.params.name === undefined` exclusion in backgroundRequested directly (previously only covered by the UI classifiers). --------- Co-authored-by: Claude <noreply@anthropic.com>
345 lines
22 KiB
Markdown
345 lines
22 KiB
Markdown
# Worktrees
|
|
|
|
> Isolate experimental work in a temporary [git worktree](https://git-scm.com/docs/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:
|
|
|
|
```bash
|
|
# 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](#exit-dialog-ctrlc--ctrld) 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:
|
|
|
|
```text
|
|
> 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:
|
|
|
|
```text
|
|
> 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:
|
|
|
|
```text
|
|
> 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](#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](./arena.md) worktrees, which use a separate path tree under `~/.qwen/arena/`.)
|
|
|
|
## Footer and Status Line
|
|
|
|
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](./status-line.md), it also receives a `worktree` object in the JSON payload piped to stdin:
|
|
|
|
```json
|
|
{
|
|
"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](#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. Caller-owned `working_dir` launches remain foreground by default 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 | Named worktrees you created are never touched. |
|
|
| Directory `mtime` > 30 days | Newer entries are skipped. |
|
|
| Any uncommitted tracked change | Skip the entry (don't delete). |
|
|
| 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 — you keep them around until you ask to remove them.
|
|
|
|
## 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:
|
|
|
|
```jsonc
|
|
// ~/.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](#limitations)).
|
|
|
|
## Tool Reference
|
|
|
|
### `enter_worktree`
|
|
|
|
```json
|
|
{ "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`
|
|
|
|
```json
|
|
{ "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
|
|
|
|
```json
|
|
{
|
|
"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](./sub-agents.md) for the rest of the agent tool reference.
|
|
|
|
## CLI Reference
|
|
|
|
### `--worktree [name | #N | url]`
|
|
|
|
```bash
|
|
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](./sub-agents.md)). 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 are clean and pushed.
|