Проектирование универсальной возможности worktree
Постановка проблемы
В qwen-code в настоящее время существует только внутренняя реализация worktree для сценария сравнения нескольких моделей Arena (GitWorktreeService). Пользователи не могут использовать worktree для изоляции работы в обычных сессиях, а AgentTool не поддерживает создание изолированной среды worktree для суб-агентов.
Цель — сделать worktree универсальной возможностью, поддерживающей изоляцию на уровне пользовательских сессий и на уровне агентов, при этом полностью сохраняя существующий функционал Arena.
Текущее состояние сравнения
| Функция | qwen-code | claude-code | Фаза |
|---|---|---|---|
Инструмент EnterWorktree | ✅ (Фаза A) | ✅ | — |
Инструмент ExitWorktree | ✅ (Фаза A) | ✅ | — |
AgentTool isolation: 'worktree' | ✅ (Фаза B) | ✅ | — |
| Автоматическая очистка просроченных worktree | ✅ (Фаза B) | ✅ | — |
| Сохранение и восстановление состояния сессии worktree | ❌ | ✅ | Фаза C |
| Настройка после создания (конфигурация hooks) | ❌ | ✅ | Фаза C |
| Отображение состояния worktree в StatusLine | ❌ | ✅ | Фаза C |
| WorktreeExitDialog (диалог выхода) | ❌ | ✅ | Фаза C |
Флаг запуска CLI --worktree | ✅ (Фаза D) | ✅ | — |
| Символические ссылки на каталоги (node_modules и т.д.) | ✅ (Фаза D) | ✅ | — |
Ссылка на PR (--worktree=#123) | ✅ (Фаза D) | ✅ | — |
| sparse checkout | ❌ | ✅ | Future |
| Интеграция с tmux | ❌ | ✅ | Future |
| Изоляция worktree для нескольких моделей Arena | ✅ (уникально для qwen) | ❌ | — |
| Обработка грязного состояния (stash + copy) | ✅ | ✅ | — |
| Отслеживание базового коммита | ✅ (уникально для qwen) | ❌ | — |
Принципы проектирования
Worktree — универсальная возможность, Arena — её вышестоящее приложение.
- Универсальный слой worktree: инструменты
EnterWorktree/ExitWorktree, параметрisolationдля AgentTool, управление состоянием сессии, автоматическая очистка. - Слой Arena: многомодельное параллельное планирование, пользовательский путь
worktreeBaseDir, массовое создание и сравнение diff, продолжает использовать существующую логикуGitWorktreeService.setupWorktrees()без изменений из-за изменений универсального слоя.
isolation: 'worktree' для AgentTool использует только универсальный путь. Arena не создаёт worktree через этот параметр — пути независимы.
Пути и конфигурация
Универсальный путь worktree
Worktree, созданные инструментом EnterWorktree или через isolation: 'worktree' для AgentTool, фиксированно размещаются:
{корень git-репозитория}/.qwen/worktrees/{slug}Путь не настраивается. Правила именования slug:
- Worktree пользовательской сессии: имя задаётся пользователем или генерируется автоматически (формат:
{прилагательное}-{существительное}-{4 случайных символа}) - Worktree агента:
agent-{7 случайных hex-символов}
Путь worktree для Arena (уже существует, остаётся без изменений)
Путь worktree для Arena управляется agents.arena.worktreeBaseDir, по умолчанию ~/.qwen/arena (ArenaManager.ts:125), полностью независим от универсального пути, никаких изменений не вносится.
Расширяемая конфигурация
| Параметр конфигурации | Тип | Назначение | Фаза |
|---|---|---|---|
ui.hideBuiltinWorktreeIndicator | boolean | Скрыть встроенную строку ⎇ worktree-… (…) в подвале, оставить кастомному statusline | Фаза C |
worktree.symlinkDirectories | string[] | Символически связать указанные каталоги (например, node_modules) с worktree, чтобы избежать расхода диска | Фаза D |
worktree.sparsePaths | string[] | Режим cone git sparse-checkout: записывать только указанные пути в больших monorepo | Future |
Фазы A / B не добавляют никаких новых параметров конфигурации.
Проектирование инструментов
EnterWorktree
Условие запуска: Пользователь явно говорит «start a worktree», «use a worktree», «create a worktree» или подобные фразы. Инструмент не должен автоматически срабатывать, когда пользователь говорит «исправить баг» или «разработать функцию».
Схема входных данных:
name?: string // Необязательно, формат slug: буквы/цифры/точка/подчёркивание/дефис, макс. 64 символаПоведение:
- Проверить, что в данный момент не находимся в worktree (предотвращение вложенности)
- Определить корень git-репозитория (обрабатывать случай нахождения в подкаталоге)
- Вызвать
GitWorktreeServiceдля создания worktree по пути.qwen/worktrees/{slug} - Записать сессию worktree в
SessionService - Переключить рабочий каталог на путь worktree
- Очистить кеш файлов
Выходные данные: worktreePath, worktreeBranch, message
ExitWorktree
Условие запуска: Пользователь говорит «exit the worktree», «leave the worktree», «go back» и т.п.
Схема входных данных:
action: 'keep' | 'remove'
discard_changes?: boolean // Действителен только при action='remove'Защита безопасности:
- Работает только с worktree, созданным данной сессией через
EnterWorktree. - При
action='remove'и наличии незафиксированных изменений — отказ от выполнения (если толькоdiscard_changes: true).
Поведение:
keep: очистить состояние worktree в сессии, сохранить каталог и ветку worktree, восстановить исходный рабочий каталог.remove: удалить каталог worktree, удалить соответствующую git-ветку, очистить состояние сессии, восстановить исходный рабочий каталог.
Выходные данные: action, originalCwd, worktreePath, worktreeBranch
Способы инициирования пользователем
| Способ | Пример | Фаза реализации |
|---|---|---|
| Явный запрос в сессии | Пользователь говорит «начать работу в worktree» → модель вызывает EnterWorktree | Фаза A |
| Изоляция агента | Модель устанавливает для суб-агента isolation: 'worktree' | Фаза B |
| Флаг запуска CLI | qwen --worktree my-feature | Фаза D |
Нет команд с косой чертой. Запуск worktree в сессии зависит от явного упоминания пользователем; isolation: 'worktree' — это сценарий, когда модель принимает решение самостоятельно.
План поэтапной реализации
Фаза A: Основные инструменты (worktree на уровне пользовательской сессии)
Цель: Пользователь может войти / выйти из worktree в сессии.
Реализуемый функционал:
- Инструмент
EnterWorktree: создание worktree, переключение рабочего каталога, запись состояния сессии. - Инструмент
ExitWorktree: два способа выхода (keep / remove), защита безопасности. - Расширение
GitWorktreeService: добавление методовcreateUserWorktree()/removeUserWorktree(), ориентированных на одну пользовательскую сессию, с повторным использованием существующей логики git, без изменения массовых интерфейсов, используемых Arena. - Расширение
SessionService: добавление поляWorktreeSessionдля записи{ slug, worktreePath, worktreeBranch, originalCwd, originalBranch }; при--resumeвосстановление рабочего каталога worktree. - Подсказки для инструментов: написание инструкций по использованию для каждого инструмента с указанием, когда его вызывать, а когда нет.
Затрагиваемые файлы:
| Файл | Тип изменения |
|---|---|
packages/core/src/tools/tool-names.ts | Добавление констант ENTER_WORKTREE, EXIT_WORKTREE |
packages/core/src/tools/EnterWorktreeTool/ | Новые каталоги: EnterWorktreeTool.ts, prompt.ts |
packages/core/src/tools/ExitWorktreeTool/ | Новые каталоги: ExitWorktreeTool.ts, prompt.ts |
packages/core/src/services/gitWorktreeService.ts | Добавление интерфейсов для пользовательской сессии (без изменения интерфейсов Arena) |
packages/core/src/services/sessionService.ts | Добавление поля WorktreeSession и методов чтения/записи |
packages/core/src/tools/ точка регистрации | Регистрация новых инструментов |
| Не входит в рамки Phase A: |
- Изоляция агентов (Phase B)
- Настройка post-creation (хуки и т.д.) (Phase C)
- Отображение состояния в UI (Phase C)
Phase B: Изоляция агентов (AgentTool isolation: 'worktree') + обновление описаний
Цель: Модель может создавать временные изолированные worktree для subагентов, которые автоматически удаляются после завершения агента; синхронно обновить описания затронутых инструментов и промпты.
Реализуемые функции:
Ядро изоляции агентов:
AgentToolполучает новый параметрisolation?: 'worktree'- При запуске агента создаётся временный worktree (slug:
agent-{7hex}, путь:.qwen/worktrees/agent-{7hex}) - После завершения агента: если изменений нет – автоматическое удаление; если есть изменения – worktree сохраняется, путь и ветка возвращаются в результате
- Автоматическая очистка устаревших worktree: сканирование
.qwen/worktrees/, поиск по шаблонуagent-{7hex}, удаление, если прошло более 30 дней и нет непереданных коммитов (стратегия fail-closed)
Обновление описаний и промптов:
- В description
AgentToolдобавлено описание параметраisolation: 'worktree'(см.AgentTool/prompt.ts:272в claude-code) - Добавлен
buildWorktreeNotice(): при запуске fork subagent в worktree в него внедряется контекстная подсказка о том, что он работает в изолированном worktree, путь наследуется от родительского агента, перед редактированием необходимо перечитать файл (см.forkSubagent.ts:buildWorktreeNoticeв claude-code)
Не требует изменений:
- review skill (
SKILL.md): review использует отдельный механизм (путь.qwen/tmp/review-pr-<n>, создаётся командойqwen review fetch-pr), полностью отличается от общего пути и механизма worktree, путаницы нет
Гарантия совместимости с Arena: Arena не создаёт worktree через параметр isolation, данное изменение не затрагивает код Arena.
Затрагиваемые файлы:
| Файл | Тип изменения |
|---|---|
packages/core/src/tools/agent/agent.ts | Добавлен параметр isolation и логика создания/очистки worktree |
packages/core/src/tools/agent/fork-subagent.ts | Добавлен buildWorktreeNotice() и внедрение в режиме worktree |
packages/core/src/services/gitWorktreeService.ts | Добавлены createAgentWorktree() / removeAgentWorktree() |
packages/core/src/services/worktreeCleanup.ts | Новый файл: логика автоматической очистки устаревших worktree |
Phase C: Полнота сессии (Persist SessionService + UI безопасность)
Цель: Состояние worktree может быть восстановлено после разрыва сессии; пользователь всегда видит, в каком worktree находится, а при выходе из сессии отображается предупреждение безопасности.
Реализуемые функции:
Персистентность состояния worktree в SessionService + восстановление через --resume:
SessionServiceрасширен полемWorktreeSession, записывающим{ slug, worktreePath, worktreeBranch, originalCwd, originalBranch }EnterWorktreeToolвызываетsessionService.setWorktreeSession()для записи состоянияExitWorktreeToolвызываетsessionService.clearWorktreeSession()для очистки состояния- При запуске через
--resumeсчитывается это поле, восстанавливаетсяtargetDirи модели передаётся контекстная подсказка
Post-creation setup:
- После создания worktree автоматически выполняется
git config core.hooksPath <mainRepo>/.git/hooks, чтобы коммиты внутри worktree соответствовали поведению хуков основного репозитория
Отображение worktree в StatusLine:
- В
UIStateContextдобавлено полеactiveWorktree(читается из состояния сессии), обновляется при входе/выходе из worktree - В
StatusLineCommandInputдобавлено полеworktree?: { slug: string; branch: string }для использования пользовательским скриптом статус-строки Footerпри непустомactiveWorktreeотображает встроенную строку⎇ <branch> (<slug>), обеспечивая базовую видимость без необходимости настройки пользовательского скрипта
WorktreeExitDialog:
- Добавлен новый компонент
WorktreeExitDialog.tsxпо аналогии с существующими Dialog - Изменена обработка клавиш выхода (Ctrl+C / Ctrl+D): при непустом
activeWorktreeперехватывается второе подтверждение и отображается диалог с предложением оставить (keep) или удалить (remove) - Действия keep/remove используют существующие пути
ExitWorktreeTool
Затрагиваемые файлы:
| Файл | Тип изменения |
|---|---|
packages/core/src/services/sessionService.ts | Добавлено поле WorktreeSession и методы чтения/записи |
packages/core/src/tools/enter-worktree.ts | Вызов sessionService.setWorktreeSession() |
packages/core/src/tools/exit-worktree.ts | Вызов sessionService.clearWorktreeSession() |
packages/core/src/services/gitWorktreeService.ts | После createUserWorktree() / createAgentWorktree() добавлена настройка core.hooksPath |
packages/cli/src/ui/contexts/UIStateContext.tsx | Добавлено поле activeWorktree и action set/clear |
packages/cli/src/ui/hooks/useStatusLine.ts | В StatusLineCommandInput добавлено поле worktree |
packages/cli/src/ui/components/Footer.tsx | Встроенная строка отображения worktree |
packages/cli/src/ui/components/WorktreeExitDialog.tsx | Новый файл |
packages/cli/src/ui/components/DialogManager.tsx | Регистрация WorktreeExitDialog |
packages/cli/src/ui/components/ExitWarning.tsx или обработка клавиш выхода | Проверка activeWorktree и перехват выхода |
Phase D: Конфигурация при запуске (CLI-флаг --worktree + символьные ссылки на каталоги + PR-ссылки)
Цель: Поддержка входа в worktree непосредственно при запуске, уменьшение дисковых затрат для больших проектов с помощью символьных ссылок на каталоги, а также быстрого создания worktree на основе pull request через PR-ссылки.
Область: Три функции реализуются в одной фазе, так как все они привязаны к одной точке входа при запуске, а symlink и PR fetch должны выполняться сразу после создания worktree — разделение привело бы к повторной модификации последовательности bootstrap.
D-1: CLI-флаг --worktree [name]
Форма параметра: Опция yargs принимает три формы:
| Форма | Поведение |
|---|---|
qwen --worktree | Голый флаг, автоматическая генерация slug ({прилагательное}-{существительное}-{6hex}) |
qwen --worktree my-name | Явный slug, используются те же правила проверки slug, что и в EnterWorktreeTool |
qwen --worktree=my-name | Эквивалентно предыдущей форме |
Не предоставляется короткий псевдоним -w (короткие псевдонимы qwen-code зарезервированы только для самых частых параметров, чтобы избежать конфликтов имён). |
Последовательность запуска: worktree создаётся в следующих местах:
parseArguments()разбирает argv (уже есть)- resume picker (уже есть, строки 588–629 в
gemini.tsx) loadCliConfig()инициализирует Config + auth (уже есть, строки 643–653)- Новое: если
argv.worktree !== undefined, вызываетсяcreateUserWorktree()- запись sidecar (
writeWorktreeSession()) - установка
process.chdir(worktreePath)вместе сConfig.setTargetDir(worktreePath) - путь повторного подключения к тому же worktree: пропустить
git worktree addи выполнить chdir на месте (исправление Phase 6). Комбинации--resume×--worktreeс разными projectHash будут неудачны на этапе поиска сессии, подробнее ниже в разделе «Приоритет с--resume».
- запись sidecar (
- Главный цикл (все три точки входа — TUI / headless
-p/ ACP — проходят шаг 4)
Отличие от упрощённой версии Phase A: EnterWorktreeTool в Phase A не изменяет Config.targetDir, полагаясь на то, что модель читает абсолютный путь из результата инструмента и продолжает его использовать. Флаг CLI Phase D вступает в силу на этапе запуска, нет необходимости совместимости с контекстом работающей модели, поэтому напрямую переключает targetDir и process.cwd() — это более сильная гарантия изоляции. Два пути ведут себя по-разному, это нужно объяснить в документации для пользователя.
Поведение при выходе: используется существующий WorktreeExitDialog (реализован в Phase C). Двойное нажатие Ctrl+C/D → пользователь выбирает между keep / remove / cancel. Новые пути кода не требуются.
Приоритет с --resume:
Поскольку сессии хранятся по ключу projectHash(process.cwd()), а --worktree выполняет chdir в worktree до resume picker / loadCliConfig, возобновление сессии, запущенной в worktree X, из worktree Y архитектурно недостижимо (у них разные projectHash, файлы сессий лежат в разных каталогах). Таблица ниже отражает фактическое поведение после реализации D-1 + исправления Phase 6 re-attach:
Состояние --resume | Состояние --worktree | Результат |
|---|---|---|
| Нет | Нет | Обычная сессия, без worktree |
| Нет | Да (новый slug) | Создать новый worktree |
| Нет | Да (существующий slug) | Повторное подключение к существующему worktree (исправление Phase 6) |
| Да | Нет | Восстановление старого worktree (поведение Phase C, если sidecar найден — вставка напоминания) |
| Да (sid из того же worktree) | Да (тот же slug, re-attach) | Повторное подключение + сессия найдена: нормальное возобновление |
| Да (sid из main checkout) | Да (любой slug) | Поиск сессии не удался: No saved session found with ID …, exit 1. documented limitation |
| Да (sid из worktree X) | Да (slug Y, X != Y) | То же самое, сессия недоступна между разными projectHash |
Семантика переопределения projectHash (перенос --worktree между сессиями в разных worktree / главной копии) потребовала бы привязки хранилища к корню репозитория, а не к projectHash, полученному из cwd. Это относится к будущей реструктуризации Config. Код ветки overrodeResumedWorktree внутри persistStartupWorktreeSidecar сохранён для того, чтобы автоматически вступить в силу после этой реструктуризации; в настоящее время он не срабатывает в производственном пути.
D-2: Параметр конфигурации worktree.symlinkDirectories
schema:
{
"worktree": {
"symlinkDirectories": ["node_modules", "dist", ".turbo"],
},
}- Тип:
string[], по умолчаниюundefined(не включено, opt-in) - Пространство имён верхнего уровня
worktreeявляется новым (вsettingsSchema.tsоно вставляется по алфавиту междуtoolsиui) - Пути относительно корня главного репозитория, абсолютные пути или пути, содержащие
.., отклоняются защитой от path traversal
Область действия: Все worktree, создаваемые универсальным уровнем, включая:
EnterWorktreeTool(Phase A)AgentToolсisolation: 'worktree'(Phase B)- Флаг CLI
--worktree(Phase D-1)
Worktree Arena не используют универсальный уровень и не затрагиваются этой настройкой.
Место реализации: GitWorktreeService.performPostCreationSetup() — сразу после существующего configureHooksPath() (шаблон, заведённый в Phase C). Добавляется метод symlinkConfiguredDirectories(), который перебирает элементы настройки и вызывает fs.symlink(absSource, absDest, 'dir').
Обработка ошибок (fail-open):
| Сценарий | Поведение |
|---|---|
| Исходный каталог не существует (ENOENT) | Пропустить молча, debug log |
| Целевой путь уже существует (EEXIST) | Пропустить молча, debug log (не перезаписывать) |
Path traversal (../, абсолютные пути и т.п.) | Отклонить элемент, debug log warn |
| Другие I/O ошибки | debug log warn, продолжить обработку следующих элементов |
Само создание worktree не прерывается из-за неудачи создания симлинка — тот же принцип «best-effort post-creation setup», что и у configureHooksPath().
D-3: Разбор ссылок на PR (--worktree=#<N> / полный URL)
Поддерживаемые формы:
| Форма | Разобранный номер PR |
|---|---|
--worktree=#123 | 123 |
--worktree '#123' | 123 |
--worktree https://github.com/foo/bar/pull/123 | 123 |
--worktree https://gh.enterprise.com/foo/bar/pull/123?baz=qux | 123 |
Именование slug и ветки:
- slug:
pr-<N>(специальный зарезервированный префикс, отличный от пользовательских slug) - ветка:
worktree-pr-<N>(следует существующему правилу именования qwen-codeworktree-<slug>; не использует прямое именованиеpr-<N>как claude-code, чтобы избежать конфликта с локальной веткойpr-<N>)
Стратегия fetch:
git fetch origin pull/<N>/head
→ использовать FETCH_HEAD как основу нового worktreeНе требует утилиты gh — чистый git fetch, поддерживает любые экземпляры GitHub (публичные или корпоративные), если origin указывает на GitHub.
Пути ошибок:
| Сценарий | Сообщение об ошибке |
|---|---|
Отсутствует удалённый origin | --worktree=#<N> requires an "origin" remote that points at GitHub. |
git fetch не удался | Failed to fetch PR #<N>: PR may not exist or origin remote is unreachable. |
| Таймаут сети (30 с) | То же самое, с добавлением (timeout) |
origin не указывает на GitHub | Активная проверка не делается, git fetch естественным образом выдаст ошибку (протокол PR специфичен для GitHub) |
Отношение к D-2: PR worktree также применяет symlinkDirectories (пользователь ожидает, что сразу после открытия PR можно запустить тесты, директории зависимостей должны быть переиспользованы). |
Затронутые файлы
| Файл | Тип изменения |
|---|---|
packages/cli/src/config/config.ts | Добавлена опция --worktree в yargs; интерфейс CliArgs дополнен полем worktree?: string | boolean |
packages/cli/src/gemini.tsx | После loadCliConfig() и перед основным циклом вызывается новый хелпер setupStartupWorktree() |
packages/cli/src/startup/worktreeStartup.ts | Новый файл: setupStartupWorktree() обрабатывает разбор slug, fetch PR, запись sidecar, смену cwd |
packages/cli/src/nonInteractiveCli.ts | Переиспользует тот же хелпер (уже есть логика restoreWorktreeContext, изменять не требуется) |
packages/cli/src/acp-integration/acpAgent.ts | Переиспользует тот же хелпер |
packages/core/src/services/gitWorktreeService.ts | Добавлены parsePRReference(), fetchPullRequestRef(), symlinkConfiguredDirectories(); createUserWorktree() принимает опциональный параметр baseBranchRef |
packages/cli/src/config/settingsSchema.ts | Добавлен корневой элемент worktree.symlinkDirectories: string[] |
packages/vscode-ide-companion/schemas/settings.schema.json | Перегенерирован |
docs/users/features/worktree.md | Добавлен раздел “Quick Start CLI flag”, в таблицу Settings добавлена новая строка |
Безопасность и откат
- fail-open vs fail-close: Ошибка symlink / hooks не прерывает создание worktree (как в Phase C); ошибка fetch PR прерывает запуск (без base ref worktree не создать); ошибка валидации slug прерывает запуск (как в
EnterWorktreeTool). - path traversal: Элементы
symlinkDirectoriesпосле разрешения должны оставаться внутриrepoRoot, иначе элемент отвергается с записью в лог. - Таймаут fetch PR: Жёсткий таймаут 30 секунд, чтобы предотвратить зависание запуска из-за неотвечающей сети.
- Побочные эффекты смены cwd: После смены
process.cwd()может нарушиться разрешение относительных путей (например,--prompt-file ./foo.txt). Контрмера: перед сменой cwd все относительные аргументы сначала нормализуются (это делается в началеsetupStartupWorktree()).
Открытые вопросы
--worktree-keep-on-exit? В claude-code нет. Нужен ли флаг CLI для qwen-code, чтобы диалог выхода по умолчанию выбирал “оставить”? Предлагается пока не добавлять, дождаться отзывов пользователей.- Требуется ли
worktree.symlinkDirectoriesпереопределять для каждого проекта? Текущие настройки уже поддерживают слияние на уровнях user/workspace/project, дополнительной обработки не требуется. - Должен ли fetch PR получать реф
merge(pull/<N>/merge, т.е. после слияния с base), а неhead? claude-code выбираетhead, аргументируя тем, что пользователь обычно хочет увидеть фактические изменения PR. Следуем этому выбору.
Future: Продвинутые функции (будут реализованы по мере необходимости)
Следующие функции предназначены для более специфических сценариев использования, в настоящее время не запланированы, будут оценены после появления явных требований.
| Функция | Описание |
|---|---|
| sparse checkout | Опция worktree.sparsePaths; для больших monorepo позволяет checkout только указанные пути, сокращая время создания и дисковое потребление |
Файл .worktreeinclude | Автоматическое копирование файлов из .gitignore (.env, secrets.json и т.п.) в worktree |
| Интеграция с tmux | --worktree --tmux запускает сессию worktree в новом окне tmux |