Skip to Content
ДизайнПроектирование универсальной возможности worktree

Проектирование универсальной возможности worktree

Постановка проблемы

В qwen-code в настоящее время существует только внутренняя реализация worktree для сценария сравнения нескольких моделей Arena (GitWorktreeService). Пользователи не могут использовать worktree для изоляции работы в обычных сессиях, а AgentTool не поддерживает создание изолированной среды worktree для суб-агентов.

Цель — сделать worktree универсальной возможностью, поддерживающей изоляцию на уровне пользовательских сессий и на уровне агентов, при этом полностью сохраняя существующий функционал Arena.

Текущее состояние сравнения

Функцияqwen-codeclaude-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 checkoutFuture
Интеграция с tmuxFuture
Изоляция 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.hideBuiltinWorktreeIndicatorbooleanСкрыть встроенную строку ⎇ worktree-… (…) в подвале, оставить кастомному statuslineФаза C
worktree.symlinkDirectoriesstring[]Символически связать указанные каталоги (например, node_modules) с worktree, чтобы избежать расхода дискаФаза D
worktree.sparsePathsstring[]Режим cone git sparse-checkout: записывать только указанные пути в больших monorepoFuture

Фазы A / B не добавляют никаких новых параметров конфигурации.

Проектирование инструментов

EnterWorktree

Условие запуска: Пользователь явно говорит «start a worktree», «use a worktree», «create a worktree» или подобные фразы. Инструмент не должен автоматически срабатывать, когда пользователь говорит «исправить баг» или «разработать функцию».

Схема входных данных:

name?: string // Необязательно, формат slug: буквы/цифры/точка/подчёркивание/дефис, макс. 64 символа

Поведение:

  1. Проверить, что в данный момент не находимся в worktree (предотвращение вложенности)
  2. Определить корень git-репозитория (обрабатывать случай нахождения в подкаталоге)
  3. Вызвать GitWorktreeService для создания worktree по пути .qwen/worktrees/{slug}
  4. Записать сессию worktree в SessionService
  5. Переключить рабочий каталог на путь worktree
  6. Очистить кеш файлов

Выходные данные: 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
Флаг запуска CLIqwen --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 создаётся в следующих местах:

  1. parseArguments() разбирает argv (уже есть)
  2. resume picker (уже есть, строки 588–629 в gemini.tsx)
  3. loadCliConfig() инициализирует Config + auth (уже есть, строки 643–653)
  4. Новое: если argv.worktree !== undefined, вызывается createUserWorktree()
    • запись sidecar (writeWorktreeSession())
    • установка process.chdir(worktreePath) вместе с Config.setTargetDir(worktreePath)
    • путь повторного подключения к тому же worktree: пропустить git worktree add и выполнить chdir на месте (исправление Phase 6). Комбинации --resume × --worktree с разными projectHash будут неудачны на этапе поиска сессии, подробнее ниже в разделе «Приоритет с --resume».
  5. Главный цикл (все три точки входа — 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=#123123
--worktree '#123'123
--worktree https://github.com/foo/bar/pull/123123
--worktree https://gh.enterprise.com/foo/bar/pull/123?baz=qux123

Именование slug и ветки:

  • slug: pr-<N> (специальный зарезервированный префикс, отличный от пользовательских slug)
  • ветка: worktree-pr-<N> (следует существующему правилу именования qwen-code worktree-<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()).

Открытые вопросы

  1. --worktree-keep-on-exit? В claude-code нет. Нужен ли флаг CLI для qwen-code, чтобы диалог выхода по умолчанию выбирал “оставить”? Предлагается пока не добавлять, дождаться отзывов пользователей.
  2. Требуется ли worktree.symlinkDirectories переопределять для каждого проекта? Текущие настройки уже поддерживают слияние на уровнях user/workspace/project, дополнительной обработки не требуется.
  3. Должен ли 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
Last updated on