Skip to content

feat(tools): add generic worktree support — EnterWorktree/ExitWorktree + Agent isolation#4073

Merged
LaZzyMan merged 15 commits into
mainfrom
claude/trusting-euclid-6fdfb9
May 14, 2026
Merged

feat(tools): add generic worktree support — EnterWorktree/ExitWorktree + Agent isolation#4073
LaZzyMan merged 15 commits into
mainfrom
claude/trusting-euclid-6fdfb9

Conversation

@LaZzyMan

Copy link
Copy Markdown
Collaborator

Summary

  • What changed: Added first-class git worktree as a general-purpose capability. Two new tools (enter_worktree, exit_worktree) plus an isolation: 'worktree' parameter on the agent tool. Stale ephemeral worktree cleanup utility included.
  • Why it changed: qwen-code only had Arena-internal worktree support; users couldn't isolate experimental work, and agents had no sandbox. Closes Phase A + B of feat: Add generic git worktree support (EnterWorktree/ExitWorktree tools + Agent isolation) #4056.
  • Reviewer focus: (1) Arena code paths (`GitWorktreeService.setupWorktrees`, `ArenaManager`) must remain untouched — verify the new methods live alongside without touching the existing batch APIs; (2) the AgentTool isolation wiring across foreground + background paths in `agent.ts`; (3) the dirty-state guard in `exit_worktree` and the fail-closed conditions in `worktreeCleanup.ts`.

Validation

  • Commands run:
    ```bash

    unit tests

    npx vitest run src/tools/enter-worktree.test.ts \
    src/services/worktreeCleanup.test.ts \
    src/tools/agent/agent.test.ts

    → 80 passed (16 new + 64 existing agent tests, no regressions)

    npm run typecheck # → passes across all workspaces
    npm run build # → passes
    npm run bundle # → produces dist/cli.js

    E2E in a temp git repo against the local build

    TEST_DIR=$(mktemp -d) && cd "$TEST_DIR" && git init -q && \
    git config user.email t@example.com && git config user.name t && \
    echo hi > README.md && git add . && git commit -qm initial
    QWEN=/path/to/dist/cli.js
    node $QWEN "…tool prompt…" --approval-mode yolo --output-format json
    ```

  • Prompts / inputs used: see `docs/e2e-tests/worktree.md`

  • Expected vs Observed: see "Reproduction report" table in `docs/e2e-tests/worktree.md`

  • Quickest reviewer verification path: `npx vitest run src/tools/enter-worktree.test.ts src/services/worktreeCleanup.test.ts` from `packages/core`, then start a session in any git repo and ask the model to "create a worktree named test, then exit it with action='remove'".

  • Evidence:

    Group Result Notes
    A1 (tool registration) both tools listed in `system.tools`
    A3 (custom name) `.qwen/worktrees/my-feature` + branch `worktree-my-feature`
    A4 (slug validation) ✅ unit `validateUserWorktreeSlug` rejects `../`, dots, etc.
    B1 (keep) dir + branch preserved
    B2 (remove, no changes) dir + branch removed
    B3 (remove with dirty state) refused: "Refusing to remove worktree \"dirty-test\" — it has 0 tracked change(s) and 1 untracked file(s)."
    D1 (isolation accepted) agent ran in `agent-2c4e759` worktree
    D2 (auto-clean on no changes) worktrees dir empty after agent finished
    D3 (preserve on changes) `agent-bad55bd` preserved, result suffixed with `[worktree preserved: … (branch …)]`
    E1 (cleanup pattern matching) ✅ unit `isEphemeralSlug` matches only `agent-<7hex>`

Scope / Risk

  • Main risk or tradeoff: The Phase A simplification does NOT mechanically switch `Config.targetDir` on `enter_worktree`. Instead, the tool returns the absolute worktree path, and the model is instructed (via tool description) to use that path for subsequent file operations. This avoids invasive Config refactoring but is a weaker guarantee than claude-code's session-level cwd switch. Documented in `docs/design/worktree.md`.
  • Not covered / not validated:
    • C1 (SessionService persistence + `--resume`): scope-out for this PR
    • Phase C (post-creation setup, StatusLine, WorktreeExitDialog) and Phase D (`--worktree` CLI flag, sparse checkout, tmux) are deferred to follow-up PRs
    • Arena worktrees not exercised by E2E (code paths verifiably unchanged: `ArenaManager.ts:125` `worktreeBaseDir` usage and `GitWorktreeService.setupWorktrees()` are not touched)
  • Breaking changes / migration notes: None. The new tools are additive. `Agent` tool gains an optional `isolation` field; existing callers are unaffected.

Testing Matrix

🍏 🪟 🐧
npm run ⚠️ ⚠️
npx ⚠️ ⚠️ ⚠️
Docker N/A N/A N/A
Podman ⚠️ N/A N/A
Seatbelt ⚠️ N/A N/A

Testing matrix notes:

  • Verified on macOS (darwin 24.6.0) with Node 22.21.1. Cross-platform behavior depends on `simple-git` and `fs/promises` which are platform-agnostic; no platform-specific code paths added.

Linked Issues / Bugs

Refs #4056 (this PR delivers Phase A + B; Phases C and D will follow in separate PRs)

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Summary

Package Lines Statements Functions Branches
CLI 75.74% 75.74% 76.98% 80.48%
Core 78.36% 78.36% 81.17% 82.8%
CLI Package - Full Text Report
-------------------|---------|----------|---------|---------|-------------------
File               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-------------------|---------|----------|---------|---------|-------------------
All files          |   75.74 |    80.48 |   76.98 |   75.74 |                   
 src               |   73.49 |    66.66 |   76.47 |   73.49 |                   
  gemini.tsx       |   61.04 |    57.29 |   66.66 |   61.04 | ...96,913-916,924 
  ...ractiveCli.ts |   80.02 |    68.61 |   78.57 |   80.02 | ...1021,1059,1162 
  ...liCommands.ts |   76.17 |    73.33 |     100 |   76.17 | ...50-274,299,401 
  ...ActiveAuth.ts |     100 |     87.5 |     100 |     100 | 66-80             
 ...cp-integration |   54.45 |    66.34 |   58.82 |   54.45 |                   
  acpAgent.ts      |   56.74 |    66.66 |   65.51 |   56.74 | ...12-914,928-936 
  authMethods.ts   |   12.19 |      100 |       0 |   12.19 | 11-31,34-38,41-50 
  errorCodes.ts    |       0 |        0 |       0 |       0 | 1-22              
  ...DirContext.ts |     100 |      100 |     100 |     100 |                   
 ...ration/service |   68.65 |    83.33 |   66.66 |   68.65 |                   
  filesystem.ts    |   68.65 |    83.33 |   66.66 |   68.65 | ...32,77-94,97-98 
 ...ration/session |   76.02 |    70.59 |      84 |   76.02 |                   
  ...ryReplayer.ts |   65.93 |    75.67 |   81.81 |   65.93 | ...40-255,268-269 
  Session.ts       |   75.12 |    68.89 |    85.1 |   75.12 | ...2456,2462-2465 
  ...entTracker.ts |   90.85 |    84.84 |      90 |   90.85 | ...35,199,251-260 
  index.ts         |       0 |        0 |       0 |       0 | 1-40              
  ...ssionUtils.ts |   84.21 |    77.77 |     100 |   84.21 | ...37-153,209-211 
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...ssion/emitters |   96.01 |    90.75 |    92.3 |   96.01 |                   
  BaseEmitter.ts   |   76.92 |    66.66 |      80 |   76.92 | 23-24,39-40,55-56 
  ...ageEmitter.ts |     100 |    89.47 |     100 |     100 | 109,111           
  PlanEmitter.ts   |     100 |      100 |     100 |     100 |                   
  ...allEmitter.ts |   98.06 |     92.3 |     100 |   98.06 | 227-228,327,335   
  index.ts         |       0 |        0 |       0 |       0 | 1-10              
 ...ession/rewrite |   90.36 |    87.83 |   94.11 |   90.36 |                   
  LlmRewriter.ts   |      81 |       84 |     100 |      81 | ...,88-89,155-159 
  ...Middleware.ts |   95.83 |    85.71 |     100 |   95.83 | 119,127-129       
  TurnBuffer.ts    |     100 |      100 |     100 |     100 |                   
  config.ts        |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 src/auth          |   97.68 |    94.85 |   95.45 |   97.68 |                   
  allProviders.ts  |     100 |      100 |     100 |     100 |                   
  ...iderConfig.ts |    97.6 |    95.04 |     100 |    97.6 | ...61,411,433-434 
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 src/auth/install  |   98.57 |    88.88 |     100 |   98.57 |                   
  ...nstallPlan.ts |   98.57 |    88.88 |     100 |   98.57 | 80,93             
 ...viders/alibaba |   96.96 |    66.66 |   66.66 |   96.96 |                   
  ...baStandard.ts |     100 |      100 |     100 |     100 |                   
  codingPlan.ts    |   93.67 |    66.66 |   66.66 |   93.67 | 83,87-89,94       
  tokenPlan.ts     |     100 |      100 |     100 |     100 |                   
 ...oviders/custom |     100 |      100 |     100 |     100 |                   
  ...omProvider.ts |     100 |      100 |     100 |     100 |                   
 ...roviders/oauth |    91.5 |    77.03 |   97.05 |    91.5 |                   
  openrouter.ts    |   84.37 |    33.33 |     100 |   84.37 | 43-48             
  ...outerOAuth.ts |    91.9 |    79.06 |   96.87 |    91.9 | ...53-655,699-701 
 ...ers/thirdParty |     100 |      100 |     100 |     100 |                   
  deepseek.ts      |     100 |      100 |     100 |     100 |                   
  idealab.ts       |     100 |      100 |     100 |     100 |                   
  minimax.ts       |     100 |      100 |     100 |     100 |                   
  zai.ts           |     100 |      100 |     100 |     100 |                   
 src/commands      |   59.83 |    85.71 |   43.47 |   59.83 |                   
  auth.ts          |     100 |    83.33 |     100 |     100 | 11,14             
  channel.ts       |   56.66 |      100 |       0 |   56.66 | 15-19,27-34       
  extensions.tsx   |   96.55 |      100 |      50 |   96.55 | 37                
  hooks.tsx        |   66.66 |      100 |       0 |   66.66 | 20-24             
  mcp.ts           |   94.73 |      100 |      50 |   94.73 | 28                
  review.ts        |   51.85 |      100 |       0 |   51.85 | 24-35,38          
  serve.ts         |   11.84 |      100 |       0 |   11.84 | ...5,44-85,87-123 
 ...mmands/channel |   39.25 |    79.45 |      50 |   39.25 |                   
  ...l-registry.ts |    8.57 |      100 |       0 |    8.57 | 6-21,24-42        
  config-utils.ts  |      92 |      100 |   66.66 |      92 | 21-26             
  configure.ts     |    14.7 |      100 |       0 |    14.7 | 18-21,23-84       
  pairing.ts       |   26.31 |      100 |       0 |   26.31 | ...30,40-50,52-65 
  pidfile.ts       |   96.34 |    86.95 |     100 |   96.34 | 49,59,91          
  start.ts         |   30.98 |       52 |   69.23 |   30.98 | ...72-475,484-486 
  status.ts        |   17.85 |      100 |       0 |   17.85 | 15-26,32-76       
  stop.ts          |      20 |      100 |       0 |      20 | 14-48             
 ...nds/extensions |   84.53 |    88.95 |   81.81 |   84.53 |                   
  consent.ts       |   71.65 |    89.28 |   42.85 |   71.65 | ...85-141,156-162 
  disable.ts       |     100 |      100 |     100 |     100 |                   
  enable.ts        |     100 |      100 |     100 |     100 |                   
  install.ts       |    75.6 |    66.66 |   66.66 |    75.6 | ...39-142,145-153 
  link.ts          |     100 |      100 |     100 |     100 |                   
  list.ts          |     100 |      100 |     100 |     100 |                   
  new.ts           |     100 |      100 |     100 |     100 |                   
  settings.ts      |   99.15 |      100 |   83.33 |   99.15 | 151               
  uninstall.ts     |    37.5 |      100 |   33.33 |    37.5 | 23-45,57-64,67-70 
  update.ts        |   96.32 |      100 |     100 |   96.32 | 101-105           
  utils.ts         |   60.24 |    28.57 |     100 |   60.24 | ...81,83-87,89-93 
 ...les/mcp-server |       0 |        0 |       0 |       0 |                   
  example.ts       |       0 |        0 |       0 |       0 | 1-60              
 src/commands/mcp  |   92.29 |    86.08 |   88.88 |   92.29 |                   
  add.ts           |     100 |    98.03 |     100 |     100 | 293               
  list.ts          |   91.22 |    80.76 |      80 |   91.22 | ...19-121,146-147 
  reconnect.ts     |   76.72 |    71.42 |   85.71 |   76.72 | 35-48,153-175     
  remove.ts        |     100 |       80 |     100 |     100 | 21-25             
 ...ommands/review |   11.57 |      100 |       0 |   11.57 |                   
  cleanup.ts       |   17.94 |      100 |       0 |   17.94 | ...01-106,108-109 
  deterministic.ts |   13.75 |      100 |       0 |   13.75 | ...22-738,740-741 
  fetch-pr.ts      |   11.36 |      100 |       0 |   11.36 | ...80-201,203-204 
  load-rules.ts    |   11.32 |      100 |       0 |   11.32 | ...41-153,155-156 
  pr-context.ts    |    6.22 |      100 |       0 |    6.22 | ...97-312,314-315 
  presubmit.ts     |    9.35 |      100 |       0 |    9.35 | ...62-287,289-290 
 ...nds/review/lib |      30 |      100 |       0 |      30 |                   
  gh.ts            |   22.58 |      100 |       0 |   22.58 | ...49,53-54,62-69 
  git.ts           |   22.72 |      100 |       0 |   22.72 | 15-18,29-39,43-44 
  paths.ts         |   52.94 |      100 |       0 |   52.94 | ...26,37-38,42-43 
 src/config        |   92.72 |    85.31 |   85.71 |   92.72 |                   
  auth.ts          |   86.98 |    80.32 |     100 |   86.98 | ...26-227,243-244 
  config.ts        |   88.32 |    85.52 |      76 |   88.32 | ...1719,1743-1744 
  keyBindings.ts   |   96.11 |       50 |     100 |   96.11 | 169-172           
  ...idersScope.ts |      92 |       90 |     100 |      92 | 11-12             
  sandboxConfig.ts |    58.9 |    61.53 |   66.66 |    58.9 | ...54-68,73,77-89 
  settings.ts      |   85.51 |    87.19 |   86.48 |   85.51 | ...1148,1153-1156 
  ...ingsSchema.ts |     100 |      100 |     100 |     100 |                   
  ...tedFolders.ts |   96.22 |       94 |     100 |   96.22 | ...88-190,205-206 
 ...nfig/migration |   94.89 |    78.94 |   83.33 |   94.89 |                   
  index.ts         |   94.87 |    88.88 |     100 |   94.87 | 91-92             
  scheduler.ts     |   96.55 |    77.77 |     100 |   96.55 | 19-20             
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...ation/versions |   94.74 |       96 |     100 |   94.74 |                   
  ...-v2-shared.ts |     100 |      100 |     100 |     100 |                   
  v1-to-v2.ts      |   81.75 |    90.19 |     100 |   81.75 | ...28-229,231-247 
  v2-to-v3.ts      |     100 |      100 |     100 |     100 |                   
  v3-to-v4.ts      |     100 |      100 |     100 |     100 |                   
 src/core          |     100 |      100 |     100 |     100 |                   
  auth.ts          |     100 |      100 |     100 |     100 |                   
  initializer.ts   |     100 |      100 |     100 |     100 |                   
  theme.ts         |     100 |      100 |     100 |     100 |                   
 src/dualOutput    |   63.09 |    64.51 |   55.55 |   63.09 |                   
  ...tputBridge.ts |   62.94 |    65.51 |   56.25 |   62.94 | ...22-323,331-334 
  ...utContext.tsx |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-8               
 src/export        |       0 |        0 |       0 |       0 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-7               
 src/generated     |     100 |      100 |     100 |     100 |                   
  git-commit.ts    |     100 |      100 |     100 |     100 |                   
 src/i18n          |   84.88 |    78.88 |   66.66 |   84.88 |                   
  index.ts         |   70.44 |       74 |   53.84 |   70.44 | ...71-272,282-287 
  languages.ts     |   95.33 |    86.48 |     100 |   95.33 | ...67,195-198,213 
  ...nslateKeys.ts |     100 |      100 |     100 |     100 |                   
  ...lationDict.ts |   93.33 |    66.66 |     100 |   93.33 | 15                
 src/i18n/locales  |     100 |      100 |     100 |     100 |                   
  ca.js            |     100 |      100 |     100 |     100 |                   
  de.js            |     100 |      100 |     100 |     100 |                   
  en.js            |     100 |      100 |     100 |     100 |                   
  fr.js            |     100 |      100 |     100 |     100 |                   
  ja.js            |     100 |      100 |     100 |     100 |                   
  pt.js            |     100 |      100 |     100 |     100 |                   
  ru.js            |     100 |      100 |     100 |     100 |                   
  zh-TW.js         |     100 |      100 |     100 |     100 |                   
  zh.js            |     100 |      100 |     100 |     100 |                   
 ...nonInteractive |   72.57 |    71.12 |   74.07 |   72.57 |                   
  session.ts       |   76.64 |     69.4 |   85.71 |   76.64 | ...23-824,833-843 
  types.ts         |    42.5 |      100 |   33.33 |    42.5 | ...80-581,584-585 
 ...active/control |   77.04 |    88.23 |      80 |   77.04 |                   
  ...rolContext.ts |    7.14 |        0 |       0 |    7.14 | 49-84             
  ...Dispatcher.ts |   91.66 |    91.83 |   88.88 |   91.66 | ...54-372,388,391 
  ...rolService.ts |       8 |        0 |       0 |       8 | 46-179            
 ...ol/controllers |    7.04 |       80 |   13.33 |    7.04 |                   
  ...Controller.ts |   19.32 |      100 |      60 |   19.32 | 81-118,127-210    
  ...Controller.ts |       0 |        0 |       0 |       0 | 1-56              
  ...Controller.ts |    3.96 |      100 |   11.11 |    3.96 | ...61-379,389-494 
  ...Controller.ts |   14.06 |      100 |       0 |   14.06 | ...82-117,130-133 
  ...Controller.ts |     5.2 |      100 |       0 |     5.2 | ...21-433,442-472 
 .../control/types |       0 |        0 |       0 |       0 |                   
  serviceAPIs.ts   |       0 |        0 |       0 |       0 | 1                 
 ...Interactive/io |   97.98 |    93.72 |   95.18 |   97.98 |                   
  ...putAdapter.ts |   97.89 |    92.82 |   98.07 |   97.89 | ...1303,1398-1399 
  ...putAdapter.ts |      96 |    91.66 |   85.71 |      96 | 51-52             
  ...nputReader.ts |     100 |    94.73 |     100 |     100 | 67                
  ...putAdapter.ts |   98.28 |      100 |      90 |   98.28 | 81-82,122-123     
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/patches       |       0 |        0 |       0 |       0 |                   
  is-in-ci.ts      |       0 |        0 |       0 |       0 | 1-17              
 src/remoteInput   |   86.98 |       75 |   85.71 |   86.98 |                   
  ...utContext.tsx |     100 |      100 |     100 |     100 |                   
  ...putWatcher.ts |   88.12 |    76.08 |   91.66 |   88.12 | ...21-222,233-236 
  index.ts         |       0 |        0 |       0 |       0 | 1-8               
 src/serve         |   80.24 |    80.11 |   87.91 |   80.24 |                   
  auth.ts          |   85.86 |    83.87 |      80 |   85.86 | ...47-148,151-153 
  eventBus.ts      |   87.07 |    84.21 |      85 |   87.07 | ...46-354,415-417 
  httpAcpBridge.ts |   78.04 |    77.36 |   95.12 |   78.04 | ...2392,2423-2464 
  index.ts         |       0 |        0 |       0 |       0 | 1-32              
  loopbackBinds.ts |     100 |      100 |     100 |     100 |                   
  runQwenServe.ts  |    76.4 |    89.36 |   83.33 |    76.4 | ...28-344,369-371 
  server.ts        |   82.52 |    79.69 |   82.35 |   82.52 | ...53-758,845-854 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/services      |   92.84 |     90.9 |   98.36 |   92.84 |                   
  ...mandLoader.ts |     100 |     92.3 |     100 |     100 | 91                
  ...killLoader.ts |     100 |    96.15 |     100 |     100 | 45                
  ...andService.ts |   98.75 |      100 |     100 |   98.75 | 111               
  ...ionService.ts |   97.19 |    89.77 |     100 |   97.19 | ...85,423-424,428 
  ...mandLoader.ts |   86.83 |    83.87 |     100 |   86.83 | ...30-335,340-345 
  ...omptLoader.ts |   76.05 |    80.64 |   83.33 |   76.05 | ...12-213,279-280 
  ...mandLoader.ts |     100 |      100 |     100 |     100 |                   
  ...nd-factory.ts |    91.5 |    91.66 |     100 |    91.5 | 129,138-145       
  ...ation-tool.ts |     100 |    95.45 |     100 |     100 | 125               
  ...ndMetadata.ts |   98.21 |    96.66 |     100 |   98.21 | 83,87             
  commandUtils.ts  |      96 |    91.66 |     100 |      96 | 48                
  ...and-parser.ts |   90.69 |    85.71 |     100 |   90.69 | 63-66             
  ...ionService.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...ght/generators |    85.9 |    85.61 |   90.47 |    85.9 |                   
  DataProcessor.ts |   85.63 |     85.6 |   92.85 |   85.63 | ...1122,1126-1133 
  ...tGenerator.ts |   98.21 |    85.71 |     100 |   98.21 | 46                
  ...teRenderer.ts |   45.45 |      100 |       0 |   45.45 | 13-51             
 .../insight/types |       0 |       50 |      50 |       0 |                   
  ...sightTypes.ts |       0 |        0 |       0 |       0 |                   
  ...sightTypes.ts |       0 |        0 |       0 |       0 | 1                 
 ...mpt-processors |   97.27 |    94.04 |     100 |   97.27 |                   
  ...tProcessor.ts |     100 |      100 |     100 |     100 |                   
  ...eProcessor.ts |   94.52 |    84.21 |     100 |   94.52 | 46-47,93-94       
  ...tionParser.ts |     100 |      100 |     100 |     100 |                   
  ...lProcessor.ts |   97.41 |    95.65 |     100 |   97.41 | 95-98             
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/services/tips |   97.35 |    83.07 |     100 |   97.35 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  tipHistory.ts    |   92.45 |       70 |     100 |   92.45 | ...22,144,151,160 
  tipRegistry.ts   |     100 |    95.23 |     100 |     100 | 33                
  tipScheduler.ts  |     100 |    91.66 |     100 |     100 | 55                
 src/test-utils    |   93.75 |    83.33 |      80 |   93.75 |                   
  ...omMatchers.ts |   69.69 |       50 |      50 |   69.69 | 32-35,37-39,45-47 
  ...andContext.ts |     100 |      100 |     100 |     100 |                   
  render.tsx       |     100 |      100 |     100 |     100 |                   
 src/ui            |   64.47 |    69.23 |   48.93 |   64.47 |                   
  App.tsx          |     100 |      100 |     100 |     100 |                   
  AppContainer.tsx |   66.99 |     64.6 |   52.94 |   66.99 | ...2770,2774-2778 
  ...tionNudge.tsx |    9.58 |      100 |       0 |    9.58 | 24-94             
  ...ackDialog.tsx |   29.23 |      100 |       0 |   29.23 | 25-75             
  ...tionNudge.tsx |    7.69 |      100 |       0 |    7.69 | 25-103            
  colors.ts        |   52.72 |      100 |   23.52 |   52.72 | ...52,54-55,60-61 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  keyMatchers.ts   |   95.91 |    96.42 |     100 |   95.91 | 25-26             
  ...tic-colors.ts |     100 |      100 |     100 |     100 |                   
  textConstants.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/ui/auth       |   48.01 |    58.73 |   21.42 |   48.01 |                   
  AuthDialog.tsx   |   64.26 |    44.44 |   16.66 |   64.26 | ...59,366-388,392 
  ...nProgress.tsx |       0 |        0 |       0 |       0 | 1-64              
  ...etupSteps.tsx |    9.61 |      100 |       0 |    9.61 | ...35-352,391-476 
  useAuth.ts       |   76.63 |    68.29 |     100 |   76.63 | ...48,493-499,560 
  ...rSetupFlow.ts |   44.61 |    33.33 |      50 |   44.61 | ...57-378,395-438 
 src/ui/commands   |   70.19 |    79.57 |   80.45 |   70.19 |                   
  aboutCommand.ts  |     100 |    85.71 |     100 |     100 | 36                
  agentsCommand.ts |   83.78 |      100 |      60 |   83.78 | 30-32,42-44       
  ...odeCommand.ts |     100 |      100 |     100 |     100 |                   
  arenaCommand.ts  |   62.81 |    58.73 |   65.21 |   62.81 | ...91-596,681-689 
  authCommand.ts   |     100 |      100 |     100 |     100 |                   
  branchCommand.ts |     100 |      100 |     100 |     100 |                   
  btwCommand.ts    |   95.59 |    71.42 |     100 |   95.59 | 72,154-159        
  bugCommand.ts    |   81.13 |    71.42 |     100 |   81.13 | 60-69             
  clearCommand.ts  |   92.94 |       75 |     100 |   92.94 | 45-46,74-75,93-94 
  ...essCommand.ts |    64.7 |       50 |      75 |    64.7 | ...48-149,163-166 
  ...extCommand.ts |   34.78 |    22.22 |   45.45 |   34.78 | ...86-521,532-533 
  copyCommand.ts   |   98.28 |    94.89 |     100 |   98.28 | ...80,280,321,327 
  deleteCommand.ts |     100 |      100 |     100 |     100 |                   
  diffCommand.ts   |   99.02 |    86.11 |     100 |   99.02 | 222,226           
  ...ryCommand.tsx |   68.09 |    77.77 |   77.77 |   68.09 | ...56-261,315-323 
  docsCommand.ts   |     100 |    88.88 |     100 |     100 | 25                
  doctorCommand.ts |     100 |    93.33 |     100 |     100 | 21                
  dreamCommand.ts  |      75 |    66.66 |   66.66 |      75 | 22-27,44-47       
  editorCommand.ts |     100 |      100 |     100 |     100 |                   
  exportCommand.ts |      60 |    92.85 |   77.77 |      60 | 176-317           
  ...onsCommand.ts |   48.66 |     90.9 |   63.63 |   48.66 | ...05-109,159-211 
  forgetCommand.ts |   26.82 |      100 |      50 |   26.82 | 18-51             
  helpCommand.ts   |     100 |      100 |     100 |     100 |                   
  hooksCommand.ts  |    20.4 |       40 |      40 |    20.4 | ...48-180,204-205 
  ideCommand.ts    |   60.75 |    64.28 |   41.17 |   60.75 | ...05-306,310-324 
  initCommand.ts   |   84.33 |    72.72 |     100 |   84.33 | 68,82-87,89-94    
  ...ghtCommand.ts |   74.56 |    68.42 |     100 |   74.56 | ...31-245,250-273 
  ...ageCommand.ts |   85.76 |    82.82 |     100 |   85.76 | ...51-658,687-694 
  ...elsCommand.ts |     100 |      100 |     100 |     100 |                   
  mcpCommand.ts    |     100 |      100 |     100 |     100 |                   
  memoryCommand.ts |     100 |      100 |     100 |     100 |                   
  modelCommand.ts  |   74.56 |    79.06 |   71.42 |   74.56 | ...91-200,223-228 
  ...onsCommand.ts |     100 |      100 |     100 |     100 |                   
  planCommand.ts   |   78.82 |    76.92 |     100 |   78.82 | 30-35,51-56,68-73 
  quitCommand.ts   |     100 |      100 |     100 |     100 |                   
  recapCommand.ts  |   21.81 |      100 |      50 |   21.81 | 24-73             
  ...berCommand.ts |   32.43 |      100 |      50 |   32.43 | 23-57             
  renameCommand.ts |   85.29 |    77.77 |     100 |   85.29 | ...06-313,320-325 
  ...oreCommand.ts |    92.3 |    87.87 |     100 |    92.3 | ...,83-88,129-130 
  resumeCommand.ts |     100 |      100 |     100 |     100 |                   
  rewindCommand.ts |      80 |      100 |      50 |      80 | 19-21             
  ...ngsCommand.ts |     100 |      100 |     100 |     100 |                   
  ...hubCommand.ts |   81.43 |    65.21 |      80 |   81.43 | ...70-173,176-179 
  skillsCommand.ts |   15.04 |      100 |      25 |   15.04 | ...90-106,109-136 
  statsCommand.ts  |   88.19 |    84.21 |     100 |   88.19 | ...,58-61,143-146 
  ...ineCommand.ts |     100 |      100 |     100 |     100 |                   
  ...aryCommand.ts |    6.46 |      100 |      50 |    6.46 | 31-329            
  tasksCommand.ts  |   77.45 |    73.43 |     100 |   77.45 | ...55-159,181-186 
  ...tupCommand.ts |     100 |      100 |     100 |     100 |                   
  themeCommand.ts  |     100 |      100 |     100 |     100 |                   
  toolsCommand.ts  |     100 |      100 |     100 |     100 |                   
  trustCommand.ts  |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
  vimCommand.ts    |   54.54 |      100 |      50 |   54.54 | 19-29             
 src/ui/components |   61.36 |    75.11 |   66.66 |   61.36 |                   
  AboutBox.tsx     |     100 |      100 |     100 |     100 |                   
  AnsiOutput.tsx   |   65.57 |      100 |      50 |   65.57 | 69-90             
  ApiKeyInput.tsx  |       0 |        0 |       0 |       0 | 1-97              
  AppHeader.tsx    |   89.39 |       75 |     100 |   89.39 | 35,37-42,44       
  ...odeDialog.tsx |     9.7 |      100 |       0 |     9.7 | 35-47,50-182      
  AsciiArt.ts      |     100 |      100 |     100 |     100 |                   
  ...Indicator.tsx |   14.63 |      100 |       0 |   14.63 | 18-56             
  ...TextInput.tsx |   77.01 |       76 |     100 |   77.01 | ...20,234-236,263 
  Composer.tsx     |    80.8 |     64.7 |     100 |    80.8 | ...85,103,154,167 
  ...entPrompt.tsx |     100 |      100 |     100 |     100 |                   
  ...ryDisplay.tsx |   75.89 |    62.06 |     100 |   75.89 | ...,88,93-108,113 
  ...geDisplay.tsx |   68.42 |    57.14 |     100 |   68.42 | 16-17,31-32,42-50 
  ...ification.tsx |   28.57 |      100 |       0 |   28.57 | 16-36             
  ...gProfiler.tsx |       0 |        0 |       0 |       0 | 1-36              
  ...ogManager.tsx |    12.4 |      100 |       0 |    12.4 | 63-474            
  ...ngsDialog.tsx |    8.44 |      100 |       0 |    8.44 | 37-195            
  ExitWarning.tsx  |     100 |      100 |     100 |     100 |                   
  ...hProgress.tsx |    87.8 |    33.33 |     100 |    87.8 | 28-31,56          
  ...ustDialog.tsx |     100 |      100 |     100 |     100 |                   
  Footer.tsx       |   79.67 |    58.06 |     100 |   79.67 | ...98-102,104-108 
  ...ngSpinner.tsx |   68.42 |       80 |      50 |   68.42 | 35-52,73,80-81    
  Header.tsx       |   98.62 |    94.28 |     100 |   98.62 | 162,164           
  Help.tsx         |   98.32 |    89.88 |     100 |   98.32 | ...24,381,447-448 
  ...emDisplay.tsx |   63.27 |    36.73 |     100 |   63.27 | ...29-338,341,344 
  ...ngeDialog.tsx |     100 |      100 |     100 |     100 |                   
  InputPrompt.tsx  |   82.25 |    77.43 |   83.33 |   82.25 | ...1347,1412,1462 
  ...Shortcuts.tsx |   20.87 |      100 |       0 |   20.87 | ...6,49-51,67-125 
  ...Indicator.tsx |     100 |    91.42 |     100 |     100 | 65,74             
  ...firmation.tsx |   91.42 |      100 |      50 |   91.42 | 26-31             
  MainContent.tsx  |   81.75 |       75 |     100 |   81.75 | ...70-274,282-286 
  ...elsDialog.tsx |   16.07 |    89.18 |      50 |   16.07 | ...58-159,162-648 
  MemoryDialog.tsx |   53.21 |    51.21 |   57.14 |   53.21 | ...54,366,379-381 
  ...geDisplay.tsx |       0 |        0 |       0 |       0 | 1-41              
  ModelDialog.tsx  |   76.31 |    54.94 |     100 |   76.31 | ...05-521,578-582 
  ...tsDisplay.tsx |     100 |    97.22 |     100 |     100 | 270               
  ...fications.tsx |   18.18 |      100 |       0 |   18.18 | 15-58             
  ...onsDialog.tsx |    2.13 |      100 |       0 |    2.13 | 62-133,148-1004   
  ...ryDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...icePrompt.tsx |   88.14 |    83.87 |     100 |   88.14 | ...01-105,133-138 
  PrepareLabel.tsx |   91.66 |    77.27 |     100 |   91.66 | 73-75,77-79,110   
  ...atePrompt.tsx |    8.57 |      100 |       0 |    8.57 | 24-55,58-134      
  ...geDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...ngDisplay.tsx |   21.42 |      100 |       0 |   21.42 | 13-39             
  ...hProgress.tsx |   85.25 |    88.46 |     100 |   85.25 | 121-147           
  ...dSelector.tsx |    4.45 |      100 |       0 |    4.45 | 28-92,100-328     
  ...ionPicker.tsx |   78.43 |    66.66 |     100 |   78.43 | ...20-422,444-466 
  ...onPreview.tsx |   92.42 |    84.37 |     100 |   92.42 | ...,70-71,143-145 
  ...ryDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...putPrompt.tsx |   72.56 |       80 |      40 |   72.56 | ...06-109,114-117 
  ...ngsDialog.tsx |    69.1 |    73.65 |     100 |    69.1 | ...11-819,825-826 
  ...ionDialog.tsx |    87.8 |      100 |   33.33 |    87.8 | 36-39,44-51       
  ...putPrompt.tsx |    15.9 |      100 |       0 |    15.9 | 20-63             
  ...Indicator.tsx |   57.14 |      100 |       0 |   57.14 | 12-15             
  ...MoreLines.tsx |      28 |      100 |       0 |      28 | 18-40             
  ...ionPicker.tsx |   17.59 |      100 |       0 |   17.59 | 55-172            
  StatsDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...yTodoList.tsx |   94.17 |       80 |     100 |   94.17 | 56-57,131-134     
  ...nsDisplay.tsx |   87.25 |       64 |     100 |   87.25 | ...45-147,154-156 
  ThemeDialog.tsx  |   89.95 |    46.15 |      75 |   89.95 | ...71-173,243-245 
  Tips.tsx         |   93.54 |       75 |     100 |   93.54 | 39-40             
  TodoDisplay.tsx  |     100 |      100 |     100 |     100 |                   
  ...tsDisplay.tsx |     100 |     87.5 |     100 |     100 | 31-32             
  TrustDialog.tsx  |     100 |    81.81 |     100 |     100 | 71-86             
  ...ification.tsx |   36.36 |      100 |       0 |   36.36 | 15-22             
  ...ackDialog.tsx |    7.84 |      100 |       0 |    7.84 | 24-134            
 ...nts/agent-view |    25.2 |       90 |      10 |    25.2 |                   
  ...atContent.tsx |    8.79 |      100 |       0 |    8.79 | 53-265,271-273    
  ...tChatView.tsx |   21.05 |      100 |       0 |   21.05 | 21-39             
  ...tComposer.tsx |    9.95 |      100 |       0 |    9.95 | 57-308            
  AgentFooter.tsx  |   17.07 |      100 |       0 |   17.07 | 28-66             
  AgentHeader.tsx  |   15.38 |      100 |       0 |   15.38 | 27-64             
  AgentTabBar.tsx  |    8.13 |      100 |       0 |    8.13 | 39-59,64-187      
  ...oryAdapter.ts |     100 |    91.83 |     100 |     100 | 103,109-110,138   
  index.ts         |       0 |        0 |       0 |       0 | 1-12              
 ...mponents/arena |   45.72 |    70.53 |   60.86 |   45.72 |                   
  ArenaCards.tsx   |   73.06 |    71.79 |   85.71 |   73.06 | ...83-185,321-326 
  ...ectDialog.tsx |   83.48 |    69.86 |   88.88 |   83.48 | ...88-392,409-410 
  ...artDialog.tsx |   10.15 |      100 |       0 |   10.15 | 27-161            
  ...tusDialog.tsx |    5.63 |      100 |       0 |    5.63 | 33-75,80-288      
  ...topDialog.tsx |    6.17 |      100 |       0 |    6.17 | 33-213            
 ...ackground-view |   75.44 |     83.6 |   85.29 |   75.44 |                   
  ...sksDialog.tsx |   70.05 |       79 |   76.19 |   70.05 | ...1119,1195-1197 
  ...TasksPill.tsx |   70.83 |    86.95 |     100 |   70.83 | 44,84-96,104-112  
  ...gentPanel.tsx |   99.52 |    93.18 |     100 |   99.52 | 123               
 ...nts/extensions |   45.28 |    33.33 |      60 |   45.28 |                   
  ...gerDialog.tsx |   44.31 |    34.14 |      75 |   44.31 | ...71-480,483-488 
  index.ts         |       0 |        0 |       0 |       0 | 1-9               
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...tensions/steps |   54.77 |    94.23 |   66.66 |   54.77 |                   
  ...ctionStep.tsx |   95.12 |    92.85 |   85.71 |   95.12 | 84-86,89          
  ...etailStep.tsx |    6.18 |      100 |       0 |    6.18 | 17-128            
  ...nListStep.tsx |   88.35 |    94.73 |      80 |   88.35 | 51-52,58-71,105   
  ...electStep.tsx |   13.46 |      100 |       0 |   13.46 | 20-70             
  ...nfirmStep.tsx |   19.56 |      100 |       0 |   19.56 | 23-65             
  index.ts         |     100 |      100 |     100 |     100 |                   
 ...mponents/hooks |   72.24 |    70.52 |      80 |   72.24 |                   
  ...etailStep.tsx |   96.52 |       75 |     100 |   96.52 | 33,37,50,59       
  ...etailStep.tsx |   93.27 |    73.68 |     100 |   93.27 | 41-42,99-104,110  
  ...abledStep.tsx |     100 |      100 |     100 |     100 |                   
  ...sListStep.tsx |     100 |      100 |     100 |     100 |                   
  ...entDialog.tsx |   36.09 |    47.05 |      50 |   36.09 | ...49,453-466,470 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-13              
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...components/mcp |   20.83 |    83.72 |   83.33 |   20.83 |                   
  ...ealthPill.tsx |   68.42 |    85.71 |     100 |   68.42 | 40-46             
  ...entDialog.tsx |    3.64 |      100 |       0 |    3.64 | 41-717            
  constants.ts     |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-30              
  types.ts         |     100 |      100 |     100 |     100 |                   
  utils.ts         |   94.79 |    85.71 |     100 |   94.79 | 16,20,35,109-110  
 ...ents/mcp/steps |    6.88 |      100 |       0 |    6.88 |                   
  ...icateStep.tsx |    5.88 |      100 |       0 |    5.88 | 40-55,58-296      
  ...electStep.tsx |   10.95 |      100 |       0 |   10.95 | 16-88             
  ...etailStep.tsx |    5.26 |      100 |       0 |    5.26 | 31-247            
  ...rListStep.tsx |    5.88 |      100 |       0 |    5.88 | 20-176            
  ...etailStep.tsx |   10.41 |      100 |       0 |   10.41 | ...1,67-79,82-139 
  ToolListStep.tsx |    7.14 |      100 |       0 |    7.14 | 16-146            
 ...nents/messages |   82.15 |    80.23 |   72.85 |   82.15 |                   
  ...ionDialog.tsx |   77.35 |    74.54 |    62.5 |   77.35 | ...90,508,526-528 
  BtwMessage.tsx   |     100 |      100 |     100 |     100 |                   
  ...upDisplay.tsx |   97.67 |    83.72 |     100 |   97.67 | 119,142,150       
  ...onMessage.tsx |   91.93 |    82.35 |     100 |   91.93 | 57-59,61,63       
  ...nMessages.tsx |   79.06 |      100 |      70 |   79.06 | ...51-264,268-280 
  DiffRenderer.tsx |   93.19 |    86.17 |     100 |   93.19 | ...09,237-238,304 
  ...tsDisplay.tsx |   97.82 |    77.27 |     100 |   97.82 | 87,89             
  ...ssMessage.tsx |    12.5 |      100 |       0 |    12.5 | 18-59             
  ...edMessage.tsx |   16.66 |      100 |       0 |   16.66 | 22-38             
  ...sMessages.tsx |   55.67 |       40 |   28.57 |   55.67 | ...20-125,133-145 
  ...ryMessage.tsx |   14.28 |      100 |       0 |   14.28 | 23-62             
  ...onMessage.tsx |   81.02 |    69.23 |   33.33 |   81.02 | ...24-426,433-435 
  ...upMessage.tsx |      84 |    93.61 |     100 |      84 | ...56-383,405-420 
  ToolMessage.tsx  |   88.84 |    75.71 |    92.3 |   88.84 | ...44-749,776-778 
 ...ponents/shared |   82.37 |    77.36 |   92.75 |   82.37 |                   
  ...ctionList.tsx |   99.03 |    95.65 |     100 |   99.03 | 85                
  ...tonSelect.tsx |     100 |      100 |     100 |     100 |                   
  EnumSelector.tsx |     100 |    96.42 |     100 |     100 | 58                
  MaxSizedBox.tsx  |   83.01 |    86.25 |   88.88 |   83.01 | ...12-513,618-619 
  MultiSelect.tsx  |    6.29 |      100 |       0 |    6.29 | 35-42,45-176      
  ...tonSelect.tsx |     100 |      100 |     100 |     100 |                   
  ...eSelector.tsx |     100 |       60 |     100 |     100 | 40-45             
  TextInput.tsx    |   72.98 |    55.55 |      80 |   72.98 | ...08-212,224-230 
  ...apsedTime.tsx |     100 |      100 |     100 |     100 |                   
  ...Indicator.tsx |     100 |      100 |     100 |     100 |                   
  text-buffer.ts   |   83.62 |    75.62 |   97.61 |   83.62 | ...2272,2300,2368 
  ...er-actions.ts |   86.71 |    67.79 |     100 |   86.71 | ...07-608,809-811 
 ...ents/subagents |   30.87 |        0 |       0 |   30.87 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-11              
  reducers.tsx     |    12.1 |      100 |       0 |    12.1 | 33-190            
  types.ts         |     100 |      100 |     100 |     100 |                   
  utils.ts         |   10.95 |      100 |       0 |   10.95 | ...1,56-57,60-102 
 ...bagents/create |    9.13 |      100 |       0 |    9.13 |                   
  ...ionWizard.tsx |    7.28 |      100 |       0 |    7.28 | 34-299            
  ...rSelector.tsx |   14.75 |      100 |       0 |   14.75 | 26-85             
  ...onSummary.tsx |    4.26 |      100 |       0 |    4.26 | 27-331            
  ...tionInput.tsx |    8.63 |      100 |       0 |    8.63 | 23-177            
  ...dSelector.tsx |   33.33 |      100 |       0 |   33.33 | 20-21,26-27,36-63 
  ...nSelector.tsx |    37.5 |      100 |       0 |    37.5 | 20-21,26-27,36-58 
  ...EntryStep.tsx |   12.76 |      100 |       0 |   12.76 | 34-78             
  ToolSelector.tsx |    4.16 |      100 |       0 |    4.16 | 31-253            
 ...bagents/manage |    8.39 |      100 |       0 |    8.39 |                   
  ...ctionStep.tsx |   10.25 |      100 |       0 |   10.25 | 21-103            
  ...eleteStep.tsx |   20.93 |      100 |       0 |   20.93 | 23-62             
  ...tEditStep.tsx |   25.53 |      100 |       0 |   25.53 | ...2,37-38,51-124 
  ...ctionStep.tsx |    2.29 |      100 |       0 |    2.29 | 28-449            
  ...iewerStep.tsx |   13.72 |      100 |       0 |   13.72 | 18-73             
  ...gerDialog.tsx |    6.74 |      100 |       0 |    6.74 | 35-341            
 ...mponents/views |   42.16 |    69.23 |   21.42 |   42.16 |                   
  ContextUsage.tsx |     4.7 |      100 |       0 |     4.7 | ...52-167,170-456 
  DoctorReport.tsx |     9.8 |      100 |       0 |     9.8 | 25-54,57-131      
  ...sionsList.tsx |   87.69 |    73.68 |     100 |   87.69 | 65-72             
  McpStatus.tsx    |   89.53 |    60.52 |     100 |   89.53 | ...72,175-177,262 
  SkillsList.tsx   |   27.27 |      100 |       0 |   27.27 | 18-35             
  ToolsList.tsx    |     100 |      100 |     100 |     100 |                   
 src/ui/contexts   |   77.05 |    78.24 |   82.14 |   77.05 |                   
  ...ewContext.tsx |   65.77 |      100 |      75 |   65.77 | ...22-225,231-241 
  AppContext.tsx   |      80 |       50 |     100 |      80 | 19-20             
  ...ewContext.tsx |   93.37 |    68.57 |      50 |   93.37 | ...94-195,222-226 
  ...deContext.tsx |     100 |      100 |     100 |     100 |                   
  ...igContext.tsx |   81.81 |       50 |     100 |   81.81 | 15-16             
  ...ssContext.tsx |   81.88 |    82.26 |     100 |   81.88 | ...1153,1159-1161 
  ...owContext.tsx |   89.28 |       80 |   66.66 |   89.28 | 34,47-48,60-62    
  ...deContext.tsx |     100 |      100 |      50 |     100 |                   
  ...onContext.tsx |   43.28 |     62.5 |    62.5 |   43.28 | ...56-259,263-266 
  ...gsContext.tsx |   83.33 |       50 |     100 |   83.33 | 17-18             
  ...usContext.tsx |     100 |      100 |     100 |     100 |                   
  ...ngContext.tsx |   71.42 |       50 |     100 |   71.42 | 17-20             
  ...utContext.tsx |   85.71 |      100 |   66.66 |   85.71 | 13-14             
  ...nsContext.tsx |   88.23 |       50 |     100 |   88.23 | 109-110           
  ...teContext.tsx |   86.66 |       50 |     100 |   86.66 | 173-174           
  ...deContext.tsx |   76.08 |    72.72 |     100 |   76.08 | 47-48,52-59,77-78 
 src/ui/editors    |   93.33 |    85.71 |   66.66 |   93.33 |                   
  ...ngsManager.ts |   93.33 |    85.71 |   66.66 |   93.33 | 49,63-64          
 src/ui/hooks      |   81.93 |    82.03 |   86.53 |   81.93 |                   
  ...dProcessor.ts |   83.12 |    82.56 |     100 |   83.12 | ...88-389,408-435 
  keyToAnsi.ts     |    3.92 |      100 |       0 |    3.92 | 19-77             
  ...dProcessor.ts |    94.8 |    70.58 |     100 |    94.8 | ...76-277,282-283 
  ...dProcessor.ts |    75.9 |    63.44 |   61.53 |    75.9 | ...84,908,927-931 
  ...amingState.ts |   12.22 |      100 |       0 |   12.22 | 54-158            
  ...agerDialog.ts |   88.23 |      100 |     100 |   88.23 | 20,24             
  ...ationFrame.ts |      32 |       60 |     100 |      32 | 42-44,51-90       
  ...odeCommand.ts |   58.82 |      100 |     100 |   58.82 | 28,33-48          
  ...enaCommand.ts |      85 |      100 |     100 |      85 | 23-24,29          
  ...aInProcess.ts |   19.81 |    66.66 |      25 |   19.81 | 57-175            
  ...Completion.ts |   92.77 |    89.09 |     100 |   92.77 | ...86-187,220-223 
  ...ifications.ts |   92.07 |    96.29 |     100 |   92.07 | 116-124           
  ...tIndicator.ts |     100 |    93.75 |     100 |     100 | 63                
  ...waySummary.ts |   96.22 |    69.69 |     100 |   96.22 | 125-127,169       
  ...ndTaskView.ts |   94.11 |    76.92 |     100 |   94.11 | 119-123,216,222   
  ...ketedPaste.ts |    23.8 |      100 |       0 |    23.8 | 19-37             
  ...nchCommand.ts |   93.75 |    73.17 |     100 |   93.75 | ...68-169,221-222 
  ...ompletion.tsx |   95.95 |    82.75 |     100 |   95.95 | ...22-223,225-226 
  ...dMigration.ts |   90.62 |       75 |     100 |   90.62 | 38-40             
  useCompletion.ts |    92.4 |     87.5 |     100 |    92.4 | 68-69,93-94,98-99 
  ...nitMessage.ts |     100 |      100 |     100 |     100 |                   
  ...extualTips.ts |   76.92 |       50 |     100 |   76.92 | 55,68,71-75,88-96 
  ...eteCommand.ts |   78.53 |    88.57 |     100 |   78.53 | ...96-104,112-113 
  ...ialogClose.ts |   16.66 |      100 |     100 |   16.66 | 79-139            
  ...oublePress.ts |   53.12 |       75 |     100 |   53.12 | 33-35,41-54       
  ...orSettings.ts |     100 |      100 |     100 |     100 |                   
  ...Completion.ts |   99.12 |     97.7 |     100 |   99.12 | 182-183           
  ...ionUpdates.ts |   93.45 |     92.3 |     100 |   93.45 | ...83-287,300-306 
  ...agerDialog.ts |   88.88 |      100 |     100 |   88.88 | 21,25             
  ...backDialog.ts |   54.47 |       50 |   33.33 |   54.47 | ...69-171,193-194 
  useFocus.ts      |     100 |      100 |     100 |     100 |                   
  ...olderTrust.ts |     100 |      100 |     100 |     100 |                   
  ...ggestions.tsx |   89.15 |     62.5 |      50 |   89.15 | ...22-124,149-150 
  ...miniStream.ts |   76.64 |    73.93 |   91.66 |   76.64 | ...2425,2438-2446 
  ...BranchName.ts |    90.9 |     92.3 |     100 |    90.9 | 19-20,55-58       
  ...oryManager.ts |   93.15 |    93.75 |     100 |   93.15 | 44,107-110        
  ...ooksDialog.ts |    87.5 |      100 |     100 |    87.5 | 19,23             
  ...stListener.ts |     100 |      100 |     100 |     100 |                   
  ...nAuthError.ts |   76.19 |       50 |     100 |   76.19 | 39-40,43-45       
  ...putHistory.ts |   92.59 |    85.71 |     100 |   92.59 | 63-64,72,94-96    
  ...storyStore.ts |     100 |    94.11 |     100 |     100 | 69                
  useKeypress.ts   |     100 |      100 |     100 |     100 |                   
  ...rdProtocol.ts |   36.36 |      100 |       0 |   36.36 | 24-31             
  ...unchEditor.ts |    9.67 |      100 |       0 |    9.67 | 11-32,39-90       
  ...gIndicator.ts |     100 |      100 |     100 |     100 |                   
  useLogger.ts     |   21.05 |      100 |       0 |   21.05 | 15-37             
  useMCPHealth.ts  |   63.15 |       75 |      50 |   63.15 | 42-52,64-67       
  ...elsCommand.ts |     100 |      100 |     100 |     100 |                   
  useMcpDialog.ts  |    87.5 |      100 |     100 |    87.5 | 19,23             
  ...moryDialog.ts |    87.5 |      100 |     100 |    87.5 | 19,23             
  ...oryMonitor.ts |     100 |      100 |     100 |     100 |                   
  ...ssageQueue.ts |     100 |      100 |     100 |     100 |                   
  ...delCommand.ts |     100 |       75 |     100 |     100 | 22                
  ...raseCycler.ts |   84.74 |    76.47 |     100 |   84.74 | ...49,52-53,69-71 
  ...derUpdates.ts |   86.38 |    77.19 |     100 |   86.38 | ...22,281-293,341 
  useQwenAuth.ts   |     100 |      100 |     100 |     100 |                   
  ...lScheduler.ts |    84.7 |    93.33 |     100 |    84.7 | ...71-276,372-382 
  ...oryCommand.ts |       0 |        0 |       0 |       0 | 1-7               
  ...umeCommand.ts |   97.24 |    76.92 |     100 |   97.24 | 104-105,145       
  ...ompletion.tsx |   90.59 |    83.33 |     100 |   90.59 | ...01,104,137-140 
  ...ectionList.ts |   96.96 |    95.69 |     100 |   96.96 | ...82-183,237-240 
  ...sionPicker.ts |   85.67 |    81.37 |     100 |   85.67 | ...25-527,536-538 
  ...earchInput.ts |     100 |      100 |     100 |     100 |                   
  ...ngsCommand.ts |   18.75 |      100 |       0 |   18.75 | 10-25             
  ...ellHistory.ts |   91.74 |    79.41 |     100 |   91.74 | ...74,122-123,133 
  ...oryCommand.ts |       0 |        0 |       0 |       0 | 1-73              
  ...Completion.ts |   82.67 |    85.41 |   94.73 |   82.67 | ...68-670,678-714 
  ...tateAndRef.ts |     100 |      100 |     100 |     100 |                   
  useStatusLine.ts |     100 |    98.79 |     100 |     100 | 257               
  ...eateDialog.ts |   88.23 |      100 |     100 |   88.23 | 14,18             
  ...tification.ts |     100 |    85.71 |     100 |     100 | 47                
  ...alProgress.ts |   53.06 |       50 |   66.66 |   53.06 | ...53,61-68,79-85 
  ...rminalSize.ts |   76.19 |      100 |      50 |   76.19 | 21-25             
  ...emeCommand.ts |   67.01 |    29.41 |     100 |   67.01 | ...10-111,115-116 
  useTimer.ts      |   88.09 |    85.71 |     100 |   88.09 | 44-45,51-53       
  ...lMigration.ts |       0 |        0 |       0 |       0 |                   
  ...rustModify.ts |     100 |      100 |     100 |     100 |                   
  ...elcomeBack.ts |   87.36 |     90.9 |     100 |   87.36 | ...,94-96,114-115 
  vim.ts           |   83.77 |    80.31 |     100 |   83.77 | ...55,759-767,776 
 src/ui/layouts    |   89.72 |     87.5 |     100 |   89.72 |                   
  ...AppLayout.tsx |   89.88 |     87.5 |     100 |   89.88 | 51-53,93-98       
  ...AppLayout.tsx |   89.47 |     87.5 |     100 |   89.47 | 58-63             
 ...i/manageModels |   93.61 |       48 |     100 |   93.61 |                   
  manageModels.ts  |   93.61 |       48 |     100 |   93.61 | ...63-166,179,209 
 src/ui/models     |   80.24 |    79.16 |   71.42 |   80.24 |                   
  ...ableModels.ts |   80.24 |    79.16 |   71.42 |   80.24 | ...,61-71,123-125 
 ...noninteractive |     100 |      100 |    7.14 |     100 |                   
  ...eractiveUi.ts |     100 |      100 |    7.14 |     100 |                   
 src/ui/state      |   94.91 |    81.81 |     100 |   94.91 |                   
  extensions.ts    |   94.91 |    81.81 |     100 |   94.91 | 68-69,88          
 src/ui/themes     |   98.53 |    70.58 |     100 |   98.53 |                   
  ansi-light.ts    |     100 |      100 |     100 |     100 |                   
  ansi.ts          |     100 |      100 |     100 |     100 |                   
  atom-one-dark.ts |     100 |      100 |     100 |     100 |                   
  ayu-light.ts     |     100 |      100 |     100 |     100 |                   
  ayu.ts           |     100 |      100 |     100 |     100 |                   
  color-utils.ts   |     100 |      100 |     100 |     100 |                   
  default-light.ts |     100 |      100 |     100 |     100 |                   
  default.ts       |     100 |      100 |     100 |     100 |                   
  ...inal-theme.ts |   88.59 |    85.96 |     100 |   88.59 | ...57-261,266-270 
  dracula.ts       |     100 |      100 |     100 |     100 |                   
  github-dark.ts   |     100 |      100 |     100 |     100 |                   
  github-light.ts  |     100 |      100 |     100 |     100 |                   
  googlecode.ts    |     100 |      100 |     100 |     100 |                   
  no-color.ts      |     100 |      100 |     100 |     100 |                   
  qwen-dark.ts     |     100 |      100 |     100 |     100 |                   
  qwen-light.ts    |     100 |      100 |     100 |     100 |                   
  ...tic-tokens.ts |     100 |      100 |     100 |     100 |                   
  ...-of-purple.ts |     100 |      100 |     100 |     100 |                   
  theme-manager.ts |   87.98 |    82.89 |     100 |   87.98 | ...48-357,362-363 
  theme.ts         |     100 |    38.02 |     100 |     100 | ...34-449,457-461 
  xcode.ts         |     100 |      100 |     100 |     100 |                   
 src/ui/utils      |   83.69 |    82.69 |    92.3 |   83.69 |                   
  ...Colorizer.tsx |   82.78 |    88.23 |     100 |   82.78 | ...10-111,197-223 
  ...nRenderer.tsx |   68.83 |    70.14 |      50 |   68.83 | ...52-254,274-293 
  ...wnDisplay.tsx |   86.01 |    87.41 |     100 |   86.01 | ...87,704,729-754 
  ...idDiagram.tsx |   87.79 |    95.34 |     100 |   87.79 | 156-179           
  ...eRenderer.tsx |   92.08 |    80.45 |      95 |   92.08 | ...76-679,723-728 
  ...dWorkUtils.ts |     100 |      100 |     100 |     100 |                   
  ...boardUtils.ts |   59.61 |    58.82 |     100 |   59.61 | ...,86-88,107-149 
  commandUtils.ts  |    95.9 |    88.29 |     100 |    95.9 | ...62,164-165,289 
  computeStats.ts  |     100 |      100 |     100 |     100 |                   
  customBanner.ts  |   90.68 |    91.22 |     100 |   90.68 | ...13,324-327,334 
  displayUtils.ts  |   88.37 |    72.22 |     100 |   88.37 | 23,25,29,31,33    
  formatters.ts    |   95.23 |    98.27 |     100 |   95.23 | 117-120           
  gradientUtils.ts |     100 |      100 |     100 |     100 |                   
  highlight.ts     |     100 |      100 |     100 |     100 |                   
  ...oryMapping.ts |     100 |    94.28 |     100 |     100 | 29,51             
  historyUtils.ts  |   94.02 |    93.87 |     100 |   94.02 | 93-96             
  isNarrowWidth.ts |     100 |      100 |     100 |     100 |                   
  ...olDetector.ts |    8.23 |      100 |       0 |    8.23 | ...31-132,135-136 
  latexRenderer.ts |   94.95 |     73.8 |     100 |   94.95 | ...76-178,184-187 
  layoutUtils.ts   |     100 |      100 |     100 |     100 |                   
  ...nUtilities.ts |   69.84 |    85.71 |     100 |   69.84 | 75-91,100-101     
  ...ToolGroups.ts |   98.66 |    96.77 |     100 |   98.66 | 48-49             
  ...geRenderer.ts |   86.23 |    69.06 |   95.12 |   86.23 | ...1284,1324-1330 
  ...alRenderer.ts |   86.69 |     71.9 |     100 |   86.69 | ...1476,1513-1519 
  ...lsBySource.ts |     100 |    95.23 |     100 |     100 | 84                
  osc8.ts          |   94.71 |    87.41 |     100 |   94.71 | ...43,428,432-433 
  ...mConstants.ts |     100 |      100 |     100 |     100 |                   
  ...storyUtils.ts |   61.06 |    69.62 |      90 |   61.06 | ...64,412,417-439 
  ...ickerUtils.ts |     100 |      100 |     100 |     100 |                   
  ...izedOutput.ts |   94.94 |      100 |   88.88 |   94.94 | 112-117           
  ...wOptimizer.ts |     100 |    96.77 |     100 |     100 | 69                
  terminalSetup.ts |    4.37 |      100 |       0 |    4.37 | 44-393            
  textUtils.ts     |   97.35 |    94.38 |   91.66 |   97.35 | ...50-251,386-387 
  todoSnapshot.ts  |   89.11 |    93.33 |     100 |   89.11 | ...,66-78,180-181 
  updateCheck.ts   |     100 |    80.95 |     100 |     100 | 30-42             
 ...i/utils/export |   56.77 |     40.8 |   79.41 |   56.77 |                   
  collect.ts       |   55.92 |    50.58 |   86.36 |   55.92 | ...25-640,642-647 
  index.ts         |     100 |      100 |     100 |     100 |                   
  normalize.ts     |   57.47 |    20.51 |      80 |   57.47 | ...09-310,324-359 
  types.ts         |       0 |        0 |       0 |       0 | 1                 
  utils.ts         |      40 |      100 |       0 |      40 | 11-13             
 ...ort/formatters |    3.38 |      100 |       0 |    3.38 |                   
  html.ts          |    9.61 |      100 |       0 |    9.61 | ...28,34-76,82-84 
  json.ts          |      50 |      100 |       0 |      50 | 14-15             
  jsonl.ts         |     3.5 |      100 |       0 |     3.5 | 14-76             
  markdown.ts      |    0.94 |      100 |       0 |    0.94 | 13-295            
 src/utils         |      74 |    90.17 |   93.89 |      74 |                   
  acpModelUtils.ts |     100 |      100 |     100 |     100 |                   
  apiPreconnect.ts |   96.72 |    97.14 |     100 |   96.72 | 165-168           
  checks.ts        |   33.33 |      100 |       0 |   33.33 | 23-28             
  cleanup.ts       |   84.12 |    93.33 |      80 |   84.12 | 75,106-115        
  commands.ts      |     100 |      100 |     100 |     100 |                   
  commentJson.ts   |   87.17 |    90.47 |     100 |   87.17 | 64-73             
  ...Calculator.ts |     100 |      100 |     100 |     100 |                   
  deepMerge.ts     |     100 |       90 |     100 |     100 | 41-43,49          
  ...ScopeUtils.ts |   97.56 |    88.88 |     100 |   97.56 | 67                
  doctorChecks.ts  |   71.06 |       75 |     100 |   71.06 | ...95-301,325-341 
  ...putCapture.ts |   90.65 |    86.17 |     100 |   90.65 | ...72,370,372-373 
  ...arResolver.ts |   94.28 |       88 |     100 |   94.28 | 28-29,125-126     
  errors.ts        |   98.67 |    96.36 |     100 |   98.67 | 67-68             
  events.ts        |     100 |      100 |     100 |     100 |                   
  gitUtils.ts      |   91.91 |    84.61 |     100 |   91.91 | 78-81,124-127     
  ...AutoUpdate.ts |   90.76 |    93.33 |   88.88 |   90.76 | 103-114           
  ...lationInfo.ts |     100 |      100 |     100 |     100 |                   
  languageUtils.ts |   97.89 |    96.42 |     100 |   97.89 | 132-133           
  math.ts          |       0 |        0 |       0 |       0 | 1-15              
  ...onfigUtils.ts |     100 |      100 |     100 |     100 |                   
  ...iveHelpers.ts |   96.82 |    93.28 |     100 |   96.82 | ...84-485,583,596 
  osc.ts           |    97.5 |      100 |   88.88 |    97.5 | 195-196           
  package.ts       |   88.88 |       80 |     100 |   88.88 | 33-34             
  processUtils.ts  |     100 |      100 |     100 |     100 |                   
  readStdin.ts     |   79.62 |       90 |      80 |   79.62 | 33-40,52-54       
  relaunch.ts      |   98.07 |    76.92 |     100 |   98.07 | 70                
  resolvePath.ts   |   66.66 |       25 |     100 |   66.66 | 12-13,16,18-19    
  sandbox.ts       |       0 |        0 |       0 |       0 | 1-1047            
  settingsUtils.ts |   82.89 |    90.67 |   89.47 |   82.89 | ...52-663,670-678 
  spawnWrapper.ts  |     100 |      100 |     100 |     100 |                   
  ...upProfiler.ts |   98.46 |    94.52 |     100 |   98.46 | 130-131,305       
  ...upWarnings.ts |     100 |      100 |     100 |     100 |                   
  stdioHelpers.ts  |     100 |       60 |     100 |     100 | 23,32             
  systemInfo.ts    |   92.52 |     90.9 |   83.33 |   92.52 | 63-69,184         
  ...InfoFields.ts |    87.5 |     64.1 |     100 |    87.5 | ...21-122,143-144 
  ...iffPreview.ts |   94.11 |    83.33 |     100 |   94.11 | 13                
  ...entEmitter.ts |     100 |      100 |     100 |     100 |                   
  ...upWarnings.ts |   91.17 |    82.35 |     100 |   91.17 | 67-68,73-74,77-78 
  version.ts       |     100 |       50 |     100 |     100 | 11                
  windowTitle.ts   |     100 |      100 |     100 |     100 |                   
  ...WithBackup.ts |   63.15 |    81.25 |     100 |   63.15 | 93,118-157        
-------------------|---------|----------|---------|---------|-------------------
Core Package - Full Text Report
-------------------|---------|----------|---------|---------|-------------------
File               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-------------------|---------|----------|---------|---------|-------------------
All files          |   78.36 |     82.8 |   81.17 |   78.36 |                   
 src               |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/__mocks__/fs  |       0 |        0 |       0 |       0 |                   
  promises.ts      |       0 |        0 |       0 |       0 | 1-48              
 src/agents        |   86.11 |    76.88 |   91.66 |   86.11 |                   
  ...transcript.ts |   88.92 |    76.66 |     100 |   88.92 | ...82,306-307,438 
  ...ent-resume.ts |   81.23 |    69.89 |   77.41 |   81.23 | ...1021,1024-1026 
  ...ound-tasks.ts |   95.13 |    86.61 |     100 |   95.13 | ...06-707,733-734 
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/agents/arena  |   76.98 |    67.72 |   78.72 |   76.98 |                   
  ...gentClient.ts |   79.47 |    88.88 |   81.81 |   79.47 | ...68-183,189-204 
  ArenaManager.ts  |   75.92 |    64.19 |   78.26 |   75.92 | ...1860,1866-1867 
  arena-events.ts  |   64.44 |      100 |      50 |   64.44 | ...71-175,178-183 
  diff-summary.ts  |    87.5 |    73.46 |     100 |    87.5 | ...32-133,137-138 
  index.ts         |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...gents/backends |   76.29 |    86.15 |   73.04 |   76.29 |                   
  ITermBackend.ts  |   97.97 |    93.93 |     100 |   97.97 | ...78-180,255,307 
  ...essBackend.ts |   91.25 |    90.62 |   86.66 |   91.25 | ...94,249-269,328 
  TmuxBackend.ts   |    90.7 |    76.55 |   97.36 |    90.7 | ...87,697,743-747 
  detect.ts        |   31.25 |      100 |       0 |   31.25 | 34-88             
  index.ts         |     100 |      100 |     100 |     100 |                   
  iterm-it2.ts     |     100 |     92.1 |     100 |     100 | 37-38,106         
  tmux-commands.ts |    6.64 |      100 |    3.03 |    6.64 | ...93-363,386-503 
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...agents/runtime |   81.14 |     76.7 |   71.42 |   81.14 |                   
  agent-context.ts |     100 |      100 |     100 |     100 |                   
  agent-core.ts    |   76.49 |    72.35 |   60.86 |   76.49 | ...1608,1635-1681 
  agent-events.ts  |     100 |      100 |     100 |     100 |                   
  ...t-headless.ts |   81.19 |    71.73 |   60.86 |   81.19 | ...98-399,402-403 
  ...nteractive.ts |   79.71 |    79.62 |      75 |   79.71 | ...54,456,458,461 
  ...statistics.ts |   98.19 |    82.35 |     100 |   98.19 | 127,151,192,225   
  agent-types.ts   |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/config        |   76.59 |    78.25 |   62.55 |   76.59 |                   
  config.ts        |   74.58 |     76.1 |   57.56 |   74.58 | ...3345,3356-3368 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  models.ts        |     100 |      100 |     100 |     100 |                   
  storage.ts       |   95.07 |    93.44 |   89.47 |   95.07 | ...66-267,270-271 
 ...nfirmation-bus |   98.29 |    97.14 |     100 |   98.29 |                   
  message-bus.ts   |   98.14 |    97.05 |     100 |   98.14 | 42-43             
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/core          |   84.88 |    83.12 |   89.16 |   84.88 |                   
  baseLlmClient.ts |   91.63 |    84.37 |   84.61 |   91.63 | ...91,299-313,380 
  client.ts        |      80 |    79.67 |   85.71 |      80 | ...1536,1573-1576 
  ...tGenerator.ts |    72.1 |    61.11 |     100 |    72.1 | ...63,365,372-375 
  ...lScheduler.ts |   81.12 |    82.25 |   93.47 |   81.12 | ...2334,2386-2390 
  geminiChat.ts    |   88.81 |    84.36 |    87.5 |   88.81 | ...1304,1371-1372 
  geminiRequest.ts |     100 |      100 |     100 |     100 |                   
  ...htProtocol.ts |    9.09 |      100 |       0 |    9.09 | 34-42,45-49,52-87 
  logger.ts        |   87.33 |    87.02 |     100 |   87.33 | ...61-565,611-625 
  ...tyDefaults.ts |     100 |      100 |     100 |     100 |                   
  ...olExecutor.ts |   92.59 |       75 |      50 |   92.59 | 41-42             
  ...on-helpers.ts |   85.71 |    70.58 |     100 |   85.71 | ...90-191,205-214 
  ...issionFlow.ts |   98.59 |    94.73 |     100 |   98.59 | 93                
  prompts.ts       |   89.16 |    86.41 |   76.92 |   89.16 | ...-965,1168-1169 
  tokenLimits.ts   |     100 |    89.47 |     100 |     100 | 51-52             
  ...okTriggers.ts |   99.31 |    90.41 |     100 |   99.31 | 124,135           
  turn.ts          |   96.42 |    88.88 |     100 |   96.42 | ...00,413-414,462 
 ...ntentGenerator |   95.21 |    82.36 |   93.75 |   95.21 |                   
  ...tGenerator.ts |   97.23 |    84.46 |   92.59 |   97.23 | ...36,728,896,952 
  converter.ts     |   94.51 |    80.52 |     100 |   94.51 | ...06-607,617,823 
  index.ts         |       0 |        0 |       0 |       0 | 1-21              
 ...ntentGenerator |   91.53 |    71.64 |   93.33 |   91.53 |                   
  ...tGenerator.ts |      90 |    70.96 |   92.85 |      90 | ...80-286,304-305 
  index.ts         |     100 |       80 |     100 |     100 | 50                
 ...ntentGenerator |   92.11 |     82.7 |   90.32 |   92.11 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...tGenerator.ts |   92.09 |     82.7 |   90.32 |   92.09 | ...33,843-844,872 
 ...ntentGenerator |   81.66 |    84.08 |    90.9 |   81.66 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  converter.ts     |   76.88 |    82.25 |    87.5 |   76.88 | ...1589,1610-1616 
  errorHandler.ts  |     100 |      100 |     100 |     100 |                   
  index.ts         |   52.38 |    44.44 |      50 |   52.38 | ...77,81-85,89-93 
  ...tGenerator.ts |    66.4 |    70.58 |   88.88 |    66.4 | ...51-157,168-169 
  pipeline.ts      |   93.67 |     84.9 |     100 |   93.67 | ...80-481,489,554 
  ...ureContext.ts |     100 |      100 |     100 |     100 |                   
  ...ingOptions.ts |       0 |        0 |       0 |       0 | 1                 
  ...CallParser.ts |   90.66 |    88.57 |     100 |   90.66 | ...15-319,349-350 
  ...kingParser.ts |     100 |    96.87 |     100 |     100 | 42                
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...rator/provider |   96.62 |    88.82 |   95.45 |   96.62 |                   
  dashscope.ts     |   97.14 |    89.02 |   93.33 |   97.14 | ...53-254,330-331 
  deepseek.ts      |   95.55 |    90.56 |     100 |   95.55 | ...31-132,145-146 
  default.ts       |   94.62 |    86.36 |   85.71 |   94.62 | 86-87,157-159     
  index.ts         |     100 |      100 |     100 |     100 |                   
  minimax.ts       |     100 |      100 |     100 |     100 |                   
  mistral.ts       |   96.07 |    73.33 |     100 |   96.07 | 32-33             
  modelscope.ts    |     100 |      100 |     100 |     100 |                   
  openrouter.ts    |     100 |      100 |     100 |     100 |                   
  types.ts         |       0 |        0 |       0 |       0 |                   
 src/extension     |   60.56 |    79.46 |    78.4 |   60.56 |                   
  ...-converter.ts |   62.35 |    47.82 |      90 |   62.35 | ...90-791,800-832 
  ...ionManager.ts |   47.04 |    82.06 |    65.9 |   47.04 | ...1398,1408-1427 
  ...onSettings.ts |   93.46 |    93.05 |     100 |   93.46 | ...17-221,228-232 
  ...-converter.ts |   54.88 |    94.44 |      60 |   54.88 | ...35-146,158-192 
  github.ts        |   44.94 |    88.52 |      60 |   44.94 | ...53-359,398-451 
  index.ts         |     100 |      100 |     100 |     100 |                   
  marketplace.ts   |   97.29 |    93.75 |     100 |   97.29 | ...64,184-185,274 
  npm.ts           |   48.66 |    76.08 |      75 |   48.66 | ...18-420,427-431 
  override.ts      |   94.11 |    88.88 |     100 |   94.11 | 63-64,81-82       
  settings.ts      |   66.26 |      100 |      50 |   66.26 | 81-108,143-149    
  storage.ts       |     100 |      100 |     100 |     100 |                   
  ...ableSchema.ts |     100 |      100 |     100 |     100 |                   
  variables.ts     |   88.75 |    83.33 |     100 |   88.75 | ...28-231,234-237 
 src/followup      |   46.91 |     92.3 |   71.87 |   46.91 |                   
  followupState.ts |      96 |    89.74 |     100 |      96 | 159-161,218-219   
  index.ts         |     100 |      100 |     100 |     100 |                   
  overlayFs.ts     |   95.06 |       84 |     100 |   95.06 | 78,108,122,133    
  speculation.ts   |   13.22 |      100 |   16.66 |   13.22 | 88-458,518-568    
  ...onToolGate.ts |     100 |    96.29 |     100 |     100 | 93                
  ...nGenerator.ts |    38.4 |    95.12 |   33.33 |    38.4 | ...16-318,353-383 
 src/generated     |       0 |        0 |       0 |       0 |                   
  git-commit.ts    |       0 |        0 |       0 |       0 | 1-10              
 src/hooks         |   80.63 |    84.35 |   84.16 |   80.63 |                   
  ...okRegistry.ts |   86.48 |    77.08 |     100 |   86.48 | ...41-344,362-369 
  ...bortSignal.ts |     100 |      100 |     100 |     100 |                   
  ...terpolator.ts |   96.66 |    93.33 |     100 |   96.66 | 66-67             
  ...HookRunner.ts |   96.68 |    87.23 |     100 |   96.68 | 110-112,231-233   
  ...Aggregator.ts |   96.37 |    90.54 |     100 |   96.37 | ...89,291-292,365 
  ...entHandler.ts |   95.58 |    84.37 |   92.59 |   95.58 | ...29,682-683,693 
  hookPlanner.ts   |   84.13 |    76.59 |      90 |   84.13 | ...38,144,162-173 
  hookRegistry.ts  |   88.83 |    86.36 |     100 |   88.83 | ...21,326,330,334 
  hookRunner.ts    |   53.94 |     72.6 |   61.11 |   53.94 | ...27-728,737-738 
  hookSystem.ts    |   75.47 |      100 |   56.41 |   75.47 | ...75-576,582-583 
  ...HookRunner.ts |   75.51 |     61.9 |      80 |   75.51 | ...05-406,424-425 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...SkillHooks.ts |   78.75 |       75 |   66.66 |   78.75 | 62-66,137-152     
  ...oksManager.ts |    96.5 |     91.8 |     100 |    96.5 | ...90,209-210,223 
  ssrfGuard.ts     |   77.22 |    85.36 |     100 |   77.22 | ...57,261-267,273 
  trustedHooks.ts  |       0 |        0 |       0 |       0 | 1-124             
  types.ts         |   90.18 |    90.78 |   85.18 |   90.18 | ...91-392,452-456 
  urlValidator.ts  |     100 |      100 |     100 |     100 |                   
 src/ide           |   74.28 |    83.39 |   78.33 |   74.28 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  detect-ide.ts    |     100 |      100 |     100 |     100 |                   
  ide-client.ts    |    64.2 |    81.48 |   66.66 |    64.2 | ...9-970,999-1007 
  ide-installer.ts |   89.06 |    79.31 |     100 |   89.06 | ...36,143-147,160 
  ideContext.ts    |     100 |      100 |     100 |     100 |                   
  process-utils.ts |   84.84 |    71.79 |     100 |   84.84 | ...37,151,193-194 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/lsp           |   33.92 |    44.97 |   45.76 |   33.92 |                   
  ...nfigLoader.ts |   70.27 |    35.89 |   94.73 |   70.27 | ...20-422,426-432 
  ...ionFactory.ts |    4.29 |        0 |       0 |    4.29 | ...20-371,377-394 
  ...Normalizer.ts |   23.09 |    13.72 |   30.43 |   23.09 | ...04-905,909-924 
  ...verManager.ts |   13.52 |    81.25 |   29.16 |   13.52 | ...75-694,700-730 
  ...eLspClient.ts |   17.89 |      100 |       0 |   17.89 | ...37-244,254-258 
  ...LspService.ts |   45.87 |    62.13 |   66.66 |   45.87 | ...1282,1299-1309 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/mcp           |   78.69 |    75.34 |   75.92 |   78.69 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...h-provider.ts |   86.95 |      100 |   33.33 |   86.95 | ...,93,97,101-102 
  ...h-provider.ts |   73.82 |    53.92 |     100 |   73.82 | ...88-895,902-904 
  ...en-storage.ts |   98.62 |    97.72 |     100 |   98.62 | 87-88             
  oauth-utils.ts   |   70.58 |    85.29 |    90.9 |   70.58 | ...70-290,315-344 
  ...n-provider.ts |   89.83 |    95.83 |   45.45 |   89.83 | ...43,147,151-152 
 .../token-storage |   79.52 |    86.66 |   86.36 |   79.52 |                   
  ...en-storage.ts |     100 |      100 |     100 |     100 |                   
  ...en-storage.ts |   82.87 |    82.35 |   92.85 |   82.87 | ...63-173,181-182 
  ...en-storage.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...en-storage.ts |   68.14 |    82.35 |   64.28 |   68.14 | ...81-295,298-314 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/memory        |   67.44 |       76 |   65.62 |   67.44 |                   
  const.ts         |     100 |      100 |     100 |     100 |                   
  dream.ts         |   65.65 |    73.33 |      50 |   65.65 | 50,107-148        
  ...entPlanner.ts |   57.84 |    72.72 |   33.33 |   57.84 | ...35,140-147,152 
  entries.ts       |   63.77 |    79.16 |      50 |   63.77 | ...72-180,183-189 
  extract.ts       |    95.2 |    79.16 |     100 |    95.2 | 81-86,125         
  ...entPlanner.ts |   63.08 |    65.71 |   41.17 |   63.08 | ...17,222-223,332 
  ...ionPlanner.ts |       0 |        0 |       0 |       0 | 1                 
  forget.ts        |    45.8 |    61.53 |   44.44 |    45.8 | ...04,211,214-346 
  indexer.ts       |   83.87 |    45.45 |     100 |   83.87 | ...50,56-57,69-70 
  manager.ts       |   75.31 |    81.04 |    75.6 |   75.31 | ...1278,1291-1293 
  memoryAge.ts     |   90.47 |    77.77 |     100 |   90.47 | 50-51             
  paths.ts         |   55.47 |    89.47 |   85.71 |   55.47 | ...,89-90,106-114 
  prompt.ts        |   93.36 |    71.42 |     100 |   93.36 | ...58,161,228-229 
  recall.ts        |   79.56 |    69.38 |   88.88 |   79.56 | ...40-245,269-280 
  ...ceSelector.ts |   91.95 |    77.27 |     100 |   91.95 | ...08,110-111,119 
  scan.ts          |   87.91 |    68.42 |     100 |   87.91 | ...47-48,58,82-87 
  ...entPlanner.ts |    11.5 |      100 |       0 |    11.5 | ...57-192,210-298 
  status.ts        |   10.52 |      100 |       0 |   10.52 | 41-98             
  store.ts         |   94.44 |    83.33 |     100 |   94.44 | 56-57,92-93       
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/mocks         |       0 |        0 |       0 |       0 |                   
  msw.ts           |       0 |        0 |       0 |       0 | 1-9               
 src/models        |   89.31 |    85.95 |    87.5 |   89.31 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...tor-config.ts |   90.24 |    91.42 |     100 |   90.24 | 142,148,151-160   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...nfigErrors.ts |   74.22 |    47.82 |   84.61 |   74.22 | ...,67-74,106-117 
  ...igResolver.ts |   98.63 |    92.53 |     100 |   98.63 | 161,323,329       
  modelRegistry.ts |     100 |    98.59 |     100 |     100 | 222               
  modelsConfig.ts  |   84.57 |    81.92 |   81.57 |   84.57 | ...1223,1252-1253 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/output        |     100 |      100 |     100 |     100 |                   
  ...-formatter.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/permissions   |   71.18 |    88.73 |   48.57 |   71.18 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...on-manager.ts |   81.42 |    86.66 |      80 |   81.42 | ...29-830,837-846 
  rule-parser.ts   |   95.99 |    93.18 |     100 |   95.99 | ...-864,1013-1015 
  ...-semantics.ts |   58.28 |    85.27 |    30.2 |   58.28 | ...1604-1614,1643 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/prompts       |   83.63 |      100 |    87.5 |   83.63 |                   
  mcp-prompts.ts   |   18.18 |      100 |       0 |   18.18 | 11-19             
  ...t-registry.ts |     100 |      100 |     100 |     100 |                   
 src/qwen          |   86.01 |    79.48 |   97.18 |   86.01 |                   
  ...tGenerator.ts |   98.64 |    98.18 |     100 |   98.64 | 105-106           
  qwenOAuth2.ts    |   84.99 |    74.81 |   93.33 |   84.99 | ...,985-1001,1031 
  ...kenManager.ts |   83.76 |    76.22 |     100 |   83.76 | ...62-767,788-793 
 src/services      |   85.54 |    85.02 |   90.41 |   85.54 |                   
  ...ionTrailer.ts |     100 |      100 |     100 |     100 |                   
  ...llRegistry.ts |   97.82 |    94.73 |     100 |   97.82 | 172-173           
  ...ionService.ts |   95.75 |    95.49 |     100 |   95.75 | ...23,391,393-397 
  ...ingService.ts |    84.1 |    84.35 |   82.85 |    84.1 | ...1240,1257-1258 
  ...ttribution.ts |   91.73 |    87.71 |      90 |   91.73 | ...80-685,826-827 
  ...utSlimming.ts |     100 |    96.77 |     100 |     100 | 133,182           
  cronScheduler.ts |   97.56 |    92.98 |     100 |   97.56 | 62-63,77,155      
  ...eryService.ts |   80.43 |    95.45 |      75 |   80.43 | ...19-134,140-141 
  fileReadCache.ts |     100 |      100 |     100 |     100 |                   
  ...temService.ts |   89.76 |     85.1 |   88.88 |   89.76 | ...89,191,266-273 
  ...ratedFiles.ts |      96 |    88.23 |     100 |      96 | 119-120,146-147   
  gitInit.ts       |     100 |      100 |     100 |     100 |                   
  gitService.ts    |   68.75 |     92.3 |   55.55 |   68.75 | ...12-122,125-129 
  ...reeService.ts |   73.79 |       70 |   94.87 |   73.79 | ...1365,1393-1394 
  ...ionService.ts |   98.13 |     97.8 |   95.45 |   98.13 | ...32-333,380-381 
  ...orRegistry.ts |   96.34 |    91.66 |     100 |   96.34 | ...90-391,542-543 
  sessionRecap.ts  |   12.34 |      100 |       0 |   12.34 | 49-158            
  ...ionService.ts |   90.05 |    79.14 |   96.55 |   90.05 | ...1272,1276-1277 
  sessionTitle.ts  |   93.91 |    71.15 |     100 |   93.91 | ...34-237,268-269 
  ...ionService.ts |   83.01 |    78.66 |   87.75 |   83.01 | ...1482,1488-1493 
  ...UseSummary.ts |   94.63 |    88.67 |     100 |   94.63 | ...69-171,221-222 
  ...reeCleanup.ts |   14.56 |      100 |   33.33 |   14.56 | 58-185            
 ...icrocompaction |   97.69 |    89.79 |     100 |   97.69 |                   
  microcompact.ts  |   97.69 |    89.79 |     100 |   97.69 | ...68,229,233,314 
 src/skills        |    87.5 |     83.8 |   94.23 |    87.5 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...activation.ts |     100 |     93.1 |     100 |     100 | 93,112            
  skill-load.ts    |   92.94 |    81.63 |     100 |   92.94 | ...06,226,238-240 
  skill-manager.ts |   83.31 |    79.66 |   90.32 |   83.31 | ...1115,1122-1126 
  skill-paths.ts   |   86.74 |    77.77 |     100 |   86.74 | ...00-101,106-107 
  symlinkScope.ts  |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/subagents     |   83.13 |    80.24 |   95.23 |   83.13 |                   
  ...tin-agents.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...-selection.ts |     100 |      100 |     100 |     100 |                   
  ...nt-manager.ts |   77.21 |    72.09 |   92.85 |   77.21 | ...1180,1202-1203 
  types.ts         |     100 |      100 |     100 |     100 |                   
  validation.ts    |   92.46 |    95.18 |     100 |   92.46 | 51-56,69-74,78-83 
 src/telemetry     |   73.63 |    87.18 |   77.73 |   73.63 |                   
  config.ts        |     100 |      100 |     100 |     100 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...-exporters.ts |   46.37 |      100 |   44.44 |   46.37 | ...85,88-89,92-93 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...t.circular.ts |       0 |        0 |       0 |       0 | 1-111             
  ...-processor.ts |   93.93 |    90.21 |   94.11 |   93.93 | ...75-280,299-300 
  ...t.circular.ts |       0 |        0 |       0 |       0 | 1-128             
  loggers.ts       |    51.9 |       64 |   57.77 |    51.9 | ...1214,1231-1251 
  metrics.ts       |    74.9 |    82.95 |   74.54 |    74.9 | ...58-978,981-992 
  sanitize.ts      |      80 |    83.33 |     100 |      80 | 35-36,41-42       
  sdk.ts           |   90.45 |    83.56 |   76.92 |   90.45 | ...17-318,338-342 
  ...on-context.ts |     100 |      100 |     100 |     100 |                   
  ...on-tracing.ts |   92.77 |     87.3 |     100 |   92.77 | 79-93,379-383     
  ...etry-utils.ts |     100 |      100 |     100 |     100 |                   
  ...l-decision.ts |     100 |      100 |     100 |     100 |                   
  ...e-id-utils.ts |     100 |      100 |     100 |     100 |                   
  tracer.ts        |    99.3 |    91.83 |     100 |    99.3 | 53                
  types.ts         |   79.17 |    94.49 |   83.33 |   79.17 | ...1149,1152-1181 
  uiTelemetry.ts   |   92.97 |    96.96 |   81.25 |   92.97 | ...93-194,200-207 
 ...ry/qwen-logger |   68.24 |    79.56 |   64.91 |   68.24 |                   
  event-types.ts   |       0 |        0 |       0 |       0 |                   
  qwen-logger.ts   |   68.24 |    79.34 |   64.28 |   68.24 | ...1055,1093-1094 
 src/test-utils    |   93.16 |    95.83 |   73.52 |   93.16 |                   
  config.ts        |     100 |      100 |     100 |     100 |                   
  ...st-helpers.ts |   94.11 |       90 |     100 |   94.11 | 69-70             
  index.ts         |     100 |      100 |     100 |     100 |                   
  mock-tool.ts     |   91.19 |    97.05 |   68.96 |   91.19 | ...38,202-203,216 
  ...aceContext.ts |     100 |      100 |     100 |     100 |                   
 src/tools         |   77.36 |    81.41 |   85.77 |   77.36 |                   
  ...erQuestion.ts |   88.93 |    76.74 |    90.9 |   88.93 | ...39-340,347-348 
  cron-create.ts   |   97.75 |    88.88 |   83.33 |   97.75 | 30-31             
  cron-delete.ts   |   96.82 |      100 |   83.33 |   96.82 | 26-27             
  cron-list.ts     |   96.66 |      100 |   83.33 |   96.66 | 25-26             
  diffOptions.ts   |     100 |      100 |     100 |     100 |                   
  edit.ts          |   78.01 |    84.76 |   73.33 |   78.01 | ...86-687,774-824 
  ...r-worktree.ts |   82.43 |    68.75 |    87.5 |   82.43 | ...67-170,236-237 
  exit-worktree.ts |   83.47 |       84 |    90.9 |   83.47 | ...84-285,290-303 
  exitPlanMode.ts  |   85.09 |    85.71 |     100 |   85.09 | ...60-163,177-189 
  glob.ts          |   90.63 |    88.33 |   84.61 |   90.63 | ...28,171,302,305 
  grep.ts          |   79.19 |    85.71 |   78.94 |   79.19 | ...20,560,569-576 
  ls.ts            |   96.74 |    90.27 |     100 |   96.74 | 176-181,212,216   
  lsp.ts           |   72.77 |    60.09 |   90.32 |   72.77 | ...1211,1213-1214 
  ...nt-manager.ts |   69.73 |    75.29 |   71.42 |   69.73 | ...29-732,749-786 
  mcp-client.ts    |   33.18 |    77.41 |   66.66 |   33.18 | ...1490,1494-1497 
  mcp-tool.ts      |   90.98 |    88.88 |   96.42 |   90.98 | ...95-596,646-647 
  memory-config.ts |       0 |        0 |       0 |       0 | 1-47              
  ...iable-tool.ts |     100 |    84.61 |     100 |     100 | 102,109           
  monitor.ts       |   92.27 |    83.94 |      92 |   92.27 | ...18,547-550,563 
  ...nforcement.ts |   82.44 |       90 |     100 |   82.44 | 174-185,234-247   
  read-file.ts     |   95.07 |     88.6 |      90 |   95.07 | ...99,290-293,296 
  ripGrep.ts       |   94.59 |    85.71 |   93.33 |   94.59 | ...60,463,541-542 
  ...-transport.ts |    6.34 |      100 |       0 |    6.34 | 47-145            
  send-message.ts  |   89.32 |    91.66 |   83.33 |   89.32 | 44-45,68-76       
  shell.ts         |   72.18 |    80.23 |   89.65 |   72.18 | ...3659,3708-3714 
  skill-utils.ts   |     100 |      100 |     100 |     100 |                   
  skill.ts         |   88.11 |    91.17 |   84.61 |   88.11 | ...95,399,422-444 
  ...eticOutput.ts |   95.12 |      100 |      80 |   95.12 | 87-88             
  task-stop.ts     |   93.14 |    96.15 |   85.71 |   93.14 | 39-40,54-64       
  todoWrite.ts     |   85.42 |    84.09 |   84.61 |   85.42 | ...05-410,432-433 
  tool-error.ts    |     100 |      100 |     100 |     100 |                   
  tool-names.ts    |     100 |      100 |     100 |     100 |                   
  tool-registry.ts |   74.79 |       75 |   80.48 |   74.79 | ...92-793,801-802 
  tool-search.ts   |   95.19 |    86.48 |    92.3 |   95.19 | ...47-153,208-213 
  tools.ts         |   91.98 |    90.19 |   88.88 |   91.98 | ...50-451,467-473 
  web-fetch.ts     |   88.59 |    79.48 |    92.3 |   88.59 | ...12-313,315-316 
  write-file.ts    |    79.2 |    79.26 |   83.33 |    79.2 | ...39-642,654-689 
 src/tools/agent   |   73.19 |    82.18 |   74.24 |   73.19 |                   
  agent.ts         |   73.39 |     82.5 |      75 |   73.39 | ...2095,2131-2138 
  fork-subagent.ts |   69.62 |    71.42 |   66.66 |   69.62 | ...04-105,140-151 
 src/utils         |   88.67 |    87.25 |   93.48 |   88.67 |                   
  LruCache.ts      |       0 |        0 |       0 |       0 | 1-41              
  ...ssageQueue.ts |     100 |      100 |     100 |     100 |                   
  ...cFileWrite.ts |   76.08 |    44.44 |     100 |   76.08 | 61-70,72          
  bareMode.ts      |   27.27 |      100 |       0 |   27.27 | 9-15,18-19        
  browser.ts       |    7.69 |      100 |       0 |    7.69 | 17-56             
  ...igResolver.ts |     100 |      100 |     100 |     100 |                   
  ...engthError.ts |   89.11 |    86.66 |     100 |   89.11 | ...28-129,132-133 
  cronDisplay.ts   |   42.85 |    23.07 |     100 |   42.85 | 26-31,33-45,47-54 
  cronParser.ts    |   89.74 |    85.71 |     100 |   89.74 | ...,63-64,183-186 
  debugLogger.ts   |    95.9 |    93.84 |   94.73 |    95.9 | 106-107,214-218   
  editHelper.ts    |   93.63 |    83.52 |     100 |   93.63 | ...28-429,463-464 
  editor.ts        |   97.61 |    95.71 |     100 |   97.61 | ...70-271,273-274 
  ...arResolver.ts |   94.28 |    88.88 |     100 |   94.28 | 28-29,125-126     
  ...entContext.ts |     100 |    95.45 |     100 |     100 | 83                
  errorParsing.ts  |    97.7 |    97.05 |     100 |    97.7 | 72-73             
  ...rReporting.ts |   88.46 |       90 |     100 |   88.46 | 69-74             
  errors.ts        |   70.92 |    79.59 |   53.33 |   70.92 | ...03-219,223-229 
  fetch.ts         |   70.18 |    71.42 |   71.42 |   70.18 | ...42,148,161,186 
  fileUtils.ts     |   91.41 |    86.07 |      95 |   91.41 | ...1182,1186-1192 
  forkedAgent.ts   |    78.5 |    70.73 |   85.71 |    78.5 | ...30-436,441-447 
  formatters.ts    |   54.54 |       50 |     100 |   54.54 | 12-16             
  ...eUtilities.ts |   89.21 |    86.66 |     100 |   89.21 | 16-17,49-55,65-66 
  ...rStructure.ts |   94.36 |    94.28 |     100 |   94.36 | ...17-120,330-335 
  getPty.ts        |    12.5 |      100 |       0 |    12.5 | 21-34             
  gitDiff.ts       |   92.36 |    79.53 |     100 |   92.36 | ...55-856,928-929 
  ...noreParser.ts |    92.3 |    89.36 |     100 |    92.3 | ...15-116,186-187 
  gitUtils.ts      |   56.66 |    85.71 |      75 |   56.66 | ...2,72-73,97-148 
  iconvHelper.ts   |     100 |      100 |     100 |     100 |                   
  ...rePatterns.ts |     100 |      100 |     100 |     100 |                   
  ...ionManager.ts |     100 |     90.9 |     100 |     100 | 26                
  ...lPromptIds.ts |     100 |      100 |     100 |     100 |                   
  jsonl-utils.ts   |    74.1 |    90.76 |   58.33 |    74.1 | ...23-326,336-342 
  ...-detection.ts |     100 |      100 |     100 |     100 |                   
  ...yDiscovery.ts |    83.9 |    79.36 |     100 |    83.9 | ...16,319,411-414 
  ...tProcessor.ts |   93.63 |       90 |     100 |   93.63 | ...96-302,384-385 
  ...Inspectors.ts |   61.53 |      100 |      50 |   61.53 | 18-23             
  ...kerChecker.ts |   82.55 |    78.57 |     100 |   82.55 | 68-69,79-84,92-98 
  notebook.ts      |   94.35 |    84.78 |     100 |   94.35 | ...10,122,174-176 
  openaiLogger.ts  |   88.05 |    84.09 |     100 |   88.05 | ...44-146,169-174 
  partUtils.ts     |     100 |    98.61 |     100 |     100 | 206               
  pathReader.ts    |     100 |      100 |     100 |     100 |                   
  paths.ts         |   93.21 |    91.86 |     100 |   93.21 | ...89-390,392-394 
  pdf.ts           |   93.68 |    87.05 |     100 |   93.68 | ...96-297,321-325 
  projectPath.ts   |     100 |      100 |     100 |     100 |                   
  ...ectSummary.ts |   89.39 |    72.41 |     100 |   89.39 | ...37-142,193-196 
  ...tIdContext.ts |     100 |      100 |     100 |     100 |                   
  proxyUtils.ts    |     100 |      100 |     100 |     100 |                   
  ...rDetection.ts |   58.57 |       76 |     100 |   58.57 | ...4,88-89,95-100 
  ...noreParser.ts |   85.45 |    85.18 |     100 |   85.45 | ...59,65-66,72-73 
  rateLimit.ts     |   92.55 |    85.92 |     100 |   92.55 | ...70-272,309-310 
  readManyFiles.ts |   87.96 |    86.95 |     100 |   87.96 | ...05-207,223-234 
  retry.ts         |   89.81 |    88.05 |     100 |   89.81 | ...29,350,357-358 
  ripgrepUtils.ts  |   46.53 |    84.37 |   66.66 |   46.53 | ...32-233,245-322 
  ...sDiscovery.ts |   97.42 |    92.85 |     100 |   97.42 | ...04,182-183,202 
  ...tchOptions.ts |   81.72 |    85.04 |   95.23 |   81.72 | ...11,536,565-574 
  runtimeStatus.ts |   85.58 |    82.05 |     100 |   85.58 | ...81,231-237,239 
  safeJsonParse.ts |   74.07 |    83.33 |     100 |   74.07 | 40-46             
  ...nStringify.ts |     100 |      100 |     100 |     100 |                   
  ...aConverter.ts |   90.78 |    88.23 |     100 |   90.78 | ...41-42,93,95-96 
  ...aValidator.ts |   94.57 |    80.26 |     100 |   94.57 | ...04,213-216,270 
  ...r-launcher.ts |   76.92 |     91.3 |   66.66 |   76.92 | ...34,136,157-195 
  ...orageUtils.ts |   96.89 |    85.84 |     100 |   96.89 | ...51,367,447,466 
  shell-utils.ts   |   82.93 |    89.55 |     100 |   82.93 | ...1522,1529-1533 
  ...lAstParser.ts |   95.58 |    85.79 |     100 |   95.58 | ...1059-1061,1071 
  ...nlyChecker.ts |   95.75 |    92.39 |     100 |   95.75 | ...00-301,313-314 
  sideQuery.ts     |   98.71 |    97.14 |     100 |   98.71 | 106               
  ...pEventSink.ts |     100 |       80 |     100 |     100 | 61                
  ...tGenerator.ts |     100 |      100 |     100 |     100 |                   
  ...ameContext.ts |     100 |      100 |     100 |     100 |                   
  symlink.ts       |   77.77 |       50 |     100 |   77.77 | 44,54-59          
  ...emEncoding.ts |   96.36 |    91.17 |     100 |   96.36 | 59-60,124-125     
  terminalSafe.ts  |     100 |      100 |     100 |     100 |                   
  ...Serializer.ts |   98.72 |       90 |     100 |   98.72 | 42-43,134,201-203 
  testUtils.ts     |   53.33 |      100 |   33.33 |   53.33 | ...53,59-64,70-72 
  textUtils.ts     |      60 |      100 |   66.66 |      60 | 36-55             
  thoughtUtils.ts  |     100 |    92.85 |     100 |     100 | 71                
  ...-converter.ts |   94.59 |    85.71 |     100 |   94.59 | 35-36             
  tool-utils.ts    |    93.6 |     91.3 |     100 |    93.6 | ...58-159,162-163 
  truncation.ts    |     100 |       92 |     100 |     100 | 52,71             
  windowsPath.ts   |   89.47 |    79.31 |     100 |   89.47 | ...57-58,62,90-91 
  ...aceContext.ts |   93.71 |    89.28 |   93.33 |   93.71 | ...24-225,249-251 
  xml.ts           |     100 |      100 |     100 |     100 |                   
  yaml-parser.ts   |      92 |    84.31 |     100 |      92 | 49-53,65-69       
 ...ils/filesearch |   85.77 |    81.06 |   96.42 |   85.77 |                   
  crawlCache.ts    |     100 |      100 |     100 |     100 |                   
  crawler.ts       |   82.84 |    77.49 |   94.82 |   82.84 | ...1451,1485-1486 
  fileSearch.ts    |   93.58 |    87.32 |     100 |   93.58 | ...46-247,249-250 
  ignore.ts        |     100 |      100 |     100 |     100 |                   
  result-cache.ts  |     100 |     92.3 |     100 |     100 | 46                
 ...uest-tokenizer |   56.63 |    74.52 |   74.19 |   56.63 |                   
  ...eTokenizer.ts |   41.86 |    76.47 |   69.23 |   41.86 | ...70-443,453-507 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...tTokenizer.ts |   68.39 |    69.49 |    90.9 |   68.39 | ...24-325,327-328 
  ...ageFormats.ts |      76 |      100 |   33.33 |      76 | 45-48,55-56       
  textTokenizer.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |       0 |        0 |       0 |       0 | 1                 
-------------------|---------|----------|---------|---------|-------------------

For detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run.

@LaZzyMan

Copy link
Copy Markdown
Collaborator Author

手动测试

创建worktree

image ### 拒绝非法名称 image ### 退出worktree image ### dirty状态拒绝直接删除worktree image ### agent tool创建的worktree无变更自动清理 image

@LaZzyMan LaZzyMan marked this pull request as ready for review May 12, 2026 06:57
@LaZzyMan LaZzyMan enabled auto-merge (squash) May 12, 2026 07:01
@LaZzyMan LaZzyMan requested a review from wenshao May 12, 2026 07:08
@tanzhenxin tanzhenxin added the type/feature-request New feature or enhancement request label May 12, 2026

@tanzhenxin tanzhenxin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Nice scoping — Arena's existing worktree code is genuinely untouched, slug validation is tight, and the design doc is refreshingly honest about the enter_worktree → returned-path → model-must-use-it contract. That said, the same trust-the-model contract is the source of every load-bearing issue below: the isolation guarantees the PR promises don't hold once the subagent uses any relative path or unqualified shell command, and the cleanup paths quietly destroy evidence (and unpushed commits) when they shouldn't.

I'd like fixes for items 1–3 before merge; 5 and 6 amplify them, and 4 is a one-line hygiene fix that brings the new tools in line with every other niche tool in this codebase.

1. isolation: 'worktree' does not actually isolate; cleanup silently destroys evidence (severity: high · confidence: very high)

When isolation: 'worktree' is set, the worktree is provisioned and a notice is prepended to the task prompt, but the subagent's Config (and therefore its Edit / Write / Read / Shell tool bindings) still resolves to the parent project root. Any edit via a relative path, or any shell command without an explicit working-directory argument (npm test, rg foo, etc.), runs against the parent checkout. The cleanup helper then inspects only the worktree's git status, sees it clean, and removes the worktree plus its branch. End result: silent edits in the main checkout, no [worktree preserved: …] suffix in the result, no signal to the user that isolation was bypassed. Worth either actually switching the subagent's targetDir to the worktree, or downgrading the feature's framing so users know the isolation is advisory.

2. exit_worktree has no permission override and can delete a worktree + branch without confirmation (severity: high · confidence: very high)

ExitWorktreeInvocation inherits the default allow permission used for read-only tools. In default, auto-edit, and yolo modes it bypasses the confirmation dialog entirely, so a model that hallucinates the wrong slug and calls it with action: 'remove' and discard_changes: true destroys the worktree and force-deletes its branch with no user prompt. This is asymmetric with edit / write_file / run_shell_command, all of which prompt by default — a destructive tool of this class should override getDefaultPermission() so it shows up in the normal confirmation flow.

3. Force-deleting the branch loses unpushed commits from a clean isolated run (severity: high · confidence: very high)

Both the agent-isolation cleanup and exit_worktree action=remove pass deleteBranch: true, which runs git branch -D worktree-<slug> — that's an unconditional force-delete that discards unmerged commits. The "are there changes?" guard checks only working-tree state via git status, so a subagent that commits its work and ends with a clean tree falls through the "no changes → safe to delete" branch and loses its commits along with the branch. The [worktree preserved] path is never reached for committed work. Either drop to -d (which refuses force-delete on unmerged branches) or check git rev-list worktree-<slug> ^<base> before removing.

4. The new tools aren't deferred to ToolSearch and ship in every API request (severity: medium · confidence: high)

Both new tools are constructed without shouldDefer: true, so their function declarations are included in the initial function-declaration list on every API request, every turn, regardless of whether the user is doing worktree work. registerLazy defers module import, not declaration inclusion — those are separate axes. Every other niche / explicit-trigger tool in this codebase is deferred behind ToolSearch: AskUserQuestion ("rarely needed"), ExitPlanMode ("only used when leaving plan mode"), Monitor, SendMessage, TaskStop, the Cron* family, LspTool, McpTool. The class doc on shouldDefer explicitly says it mirrors Claude Code's framework — where these same two tools are deferred. exit_worktree's own description tells the model "Only invoke this tool when the user explicitly asks to leave or clean up a worktree", which is textbook deferral criteria. One-line fix: pass shouldDefer: true for both, and add a searchHint so ToolSearch can match them by keyword.

5. The stale-worktree cleanup utility is dead code (severity: medium · confidence: very high)

cleanupStaleAgentWorktrees is exported and unit-tested but has no caller anywhere in packages/core or packages/cli — only its own test references it. The PR description's "stale ephemeral worktree cleanup utility included" implies it runs somewhere; in practice nothing invokes it, so leaked ephemeral worktrees accumulate under .qwen/worktrees/ indefinitely. Either wire it into CLI startup / shutdown, or remove it from the PR.

6. The foreground isolation path leaks the worktree on uncaught throw (severity: medium · confidence: high)

The background path correctly invokes the cleanup helper in both the success block and the catch block. The foreground path only calls it inside the try block — the matching finally covers event listeners but not the worktree, so any throw before the cleanup call leaves the worktree and branch behind. Combined with #5, those orphans never get reaped. Mirroring the background path's pattern (cleanup in both success and error paths) closes this.

Verdict

REQUEST_CHANGES — issues 1, 2, and 3 are data-loss class. 4 is a token / hygiene fix. 5 and 6 ensure mis-cleaned worktrees stay forever once they occur.

LaZzyMan added a commit that referenced this pull request May 12, 2026
Six issues raised on the initial review; each addressed with a verifiable
guarantee.

1. Real isolation for `agent isolation: 'worktree'`
   Before: subagent's Config still resolved `getTargetDir()` to the parent
   project root, so Edit/Write/Read workspace checks and Shell's default cwd
   silently operated on the parent tree. The cleanup helper then saw a
   "clean" worktree and removed it — destroying the evidence.
   After: the worktree is provisioned BEFORE `createApprovalModeOverride`,
   and the resulting agent Config has `getTargetDir`/`getCwd`/`getWorkingDir`
   rebound to the worktree path. Relative paths, unqualified shell
   commands, and glob/grep roots all confine to the worktree.

2. `exit_worktree action='remove'` now prompts in default/auto-edit modes
   Added `getDefaultPermission()` on the invocation: `'ask'` when action is
   `remove`, `'allow'` when `keep`. Brings it in line with edit, write_file,
   and run_shell_command.

3. Force-delete no longer silently destroys unpushed commits
   `removeUserWorktree` now uses `git branch -d` (refuses unmerged) by
   default and surfaces `branchPreserved: true` when git refuses. Added
   `hasUnmergedWorktreeCommits` (checks if branch tip is reachable from any
   other local branch or remote ref). Both the agent isolation cleanup and
   `exit_worktree action='remove'` use this check: if the branch has work
   not covered elsewhere, the worktree+branch are preserved even when
   `discard_changes: true` is set (there is no `discard_commits` flag —
   committed work is rarely what `remove` means to discard).

4. Both new tools are now deferred behind ToolSearch
   `shouldDefer: true` + `searchHint` on both. Verified via openai-logging:
   `enter_worktree` and `exit_worktree` no longer appear in the function-
   declaration list sent on every API request.

5. Stale-worktree cleanup is wired in
   `Config.initialize()` fires `cleanupStaleAgentWorktrees(targetDir)` as a
   non-awaited startup sweep (skipped in bare mode). Picks up orphaned
   `agent-<7hex>` worktrees left by crashed runs.

6. Foreground isolation no longer leaks on uncaught throw
   The foreground try block tracks whether the cleanup helper ran on the
   success path; the finally block invokes it as a fallback when the try
   bailed early. Mirrors the background path's pattern.

Verification:
- Unit tests: 83 passed (16 worktree + 64 existing agent + 3 cleanup) — no
  regressions.
- E2E #1: agent told to write `hello.txt` via RELATIVE path — file landed
  at `.qwen/worktrees/agent-XXXXXXX/hello.txt`, NOT at the parent root.
- E2E #3: created worktree, committed work inside it, called exit_worktree
  with `discard_changes=true` — refused with clear message; worktree and
  branch both preserved.
- E2E #4: openai-logging confirms worktree tools absent from API tool list
  (7 tools sent instead of 9).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@LaZzyMan

Copy link
Copy Markdown
Collaborator Author

Pushed 4358d6087 addressing all six findings.

1. Real isolation — worktree is now provisioned before createApprovalModeOverride, and the resulting agent Config has getTargetDir / getCwd / getWorkingDir rebound to the worktree path. Verified mechanically with the failure case you described: an agent told to write hello.txt via a RELATIVE path now lands at .qwen/worktrees/agent-XXXXXXX/hello.txt, never at the parent root.

2. Permission promptExitWorktreeInvocation.getDefaultPermission() returns 'ask' when action === 'remove' and 'allow' when action === 'keep'. Now matches edit/write_file/run_shell_command.

3. Force-delete protectionremoveUserWorktree now uses git branch -d (refuses unmerged) by default and surfaces branchPreserved: true when git refuses. New hasUnmergedWorktreeCommits(slug) checks if the branch tip is reachable from any other local branch or remote ref. Both the agent cleanup and exit_worktree action='remove' use this guard: even with discard_changes: true, removal refuses if commits would be lost. There is no discard_commits flag — losing committed work is rarely what remove means to discard. Verified: created a worktree, committed inside it, called exit_worktree with discard_changes: true → refused with Refusing to remove worktree … its branch \worktree-X` has commits that no other branch or remote ref points at`; worktree and branch both preserved.

4. Deferred behind ToolSearch — both tools now pass shouldDefer: true with searchHint. Verified via --openai-logging: the API request's function-declaration list went from 9 to 7 tools and enter_worktree/exit_worktree are absent.

5. Stale-worktree sweep wired inConfig.initialize() fires cleanupStaleAgentWorktrees(targetDir) as a non-awaited startup task (skipped in bare mode). Picks up orphaned agent-<7hex> worktrees left by crashed runs; fail-closed on tracked changes / unpushed commits is unchanged.

6. Foreground leak on throw — the foreground try block now tracks a worktreeCleanupRan flag; the matching finally invokes the cleanup helper as a fallback when the try bailed before the success-path call. Mirrors the existing background-path pattern.

Unit tests: 83 passed (16 new worktree tests + 64 existing agent tests untouched + 3 cleanup tests). E2E sanity checks for #1, #3, #4 all pass against the local build.

@LaZzyMan

Copy link
Copy Markdown
Collaborator Author

CI failure on Ubuntu/macOS is the TableRenderer ANSI-color test — pre-existing on main (same failure on main run 25721936110, job 75527691412), unrelated to this PR. Specifically:

src/ui/utils/TableRenderer.test.tsx > <TableRenderer /> > does not preserve foreground after an explicit foreground reset
  expected '\nColor: colored reset\n' not to contain 'colored reset'

I didn't touch packages/cli/src/ui/utils/TableRenderer*.

LaZzyMan and others added 4 commits May 12, 2026 17:48
Adds first-class git worktree as a general-purpose capability:

Phase A — User-facing tools
- enter_worktree: creates `<projectRoot>/.qwen/worktrees/<slug>` on a
  `worktree-<slug>` branch and returns the absolute path. Slug auto-generated
  when omitted; validated against path traversal and disallowed characters.
- exit_worktree: keeps or removes the worktree (and its branch). Refuses to
  remove a worktree with uncommitted tracked changes or untracked files
  unless `discard_changes: true` is set.

Phase B — Agent isolation
- Agent tool gains an `isolation: 'worktree'` parameter that provisions a
  temporary `agent-<7hex>` worktree, prepends a worktree notice to the task
  prompt, and on completion either removes the worktree (no changes) or
  preserves it and reports its path/branch in the result. Background and
  foreground execution paths both wired up; rejected for fork agents.
- worktreeCleanup.cleanupStaleAgentWorktrees: fail-closed sweep for
  ephemeral `agent-<7hex>` worktrees older than 30 days with no tracked
  changes and no unpushed commits. User-named worktrees are never swept.
- buildWorktreeNotice helper for fork subagents (parity with claude-code).

Arena compatibility
- The existing Arena worktree implementation (GitWorktreeService.setupWorktrees,
  ArenaManager, agents.arena.worktreeBaseDir) is untouched. Arena uses its
  own batch APIs and `~/.qwen/arena` base dir; the new general-purpose APIs
  live alongside under `<projectRoot>/.qwen/worktrees/`.

Subagent safety
- enter_worktree / exit_worktree are added to EXCLUDED_TOOLS_FOR_SUBAGENTS
  so a subagent cannot mutate the parent session's worktree state.

Refs #4056

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… Windows

The Windows CI run reported `enter-worktree.test.ts` failing because the
expected string was hardcoded with `/` while `getUserWorktreesDir()` uses
`path.join`, which returns `\\` on Windows. Build the expected path via
`path.join` so the platform-correct separator is compared.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Some models pass `{ "name": "" }` when calling EnterWorktree, because the
schema marks `name` as optional and they emit an empty placeholder. The
previous validation rejected the empty string with "Worktree name must be
a non-empty string", which surprised users running the auto-slug path.

Now both `validateToolParams` and `execute` treat `name: ""` as equivalent
to `name: undefined` and fall back to the auto-generated `{adj}-{noun}-{4hex}`
slug. Explicit invalid slugs (`'../etc'`, `'a/b'`, etc.) are still rejected
as before.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Six issues raised on the initial review; each addressed with a verifiable
guarantee.

1. Real isolation for `agent isolation: 'worktree'`
   Before: subagent's Config still resolved `getTargetDir()` to the parent
   project root, so Edit/Write/Read workspace checks and Shell's default cwd
   silently operated on the parent tree. The cleanup helper then saw a
   "clean" worktree and removed it — destroying the evidence.
   After: the worktree is provisioned BEFORE `createApprovalModeOverride`,
   and the resulting agent Config has `getTargetDir`/`getCwd`/`getWorkingDir`
   rebound to the worktree path. Relative paths, unqualified shell
   commands, and glob/grep roots all confine to the worktree.

2. `exit_worktree action='remove'` now prompts in default/auto-edit modes
   Added `getDefaultPermission()` on the invocation: `'ask'` when action is
   `remove`, `'allow'` when `keep`. Brings it in line with edit, write_file,
   and run_shell_command.

3. Force-delete no longer silently destroys unpushed commits
   `removeUserWorktree` now uses `git branch -d` (refuses unmerged) by
   default and surfaces `branchPreserved: true` when git refuses. Added
   `hasUnmergedWorktreeCommits` (checks if branch tip is reachable from any
   other local branch or remote ref). Both the agent isolation cleanup and
   `exit_worktree action='remove'` use this check: if the branch has work
   not covered elsewhere, the worktree+branch are preserved even when
   `discard_changes: true` is set (there is no `discard_commits` flag —
   committed work is rarely what `remove` means to discard).

4. Both new tools are now deferred behind ToolSearch
   `shouldDefer: true` + `searchHint` on both. Verified via openai-logging:
   `enter_worktree` and `exit_worktree` no longer appear in the function-
   declaration list sent on every API request.

5. Stale-worktree cleanup is wired in
   `Config.initialize()` fires `cleanupStaleAgentWorktrees(targetDir)` as a
   non-awaited startup sweep (skipped in bare mode). Picks up orphaned
   `agent-<7hex>` worktrees left by crashed runs.

6. Foreground isolation no longer leaks on uncaught throw
   The foreground try block tracks whether the cleanup helper ran on the
   success path; the finally block invokes it as a fallback when the try
   bailed early. Mirrors the background path's pattern.

Verification:
- Unit tests: 83 passed (16 worktree + 64 existing agent + 3 cleanup) — no
  regressions.
- E2E #1: agent told to write `hello.txt` via RELATIVE path — file landed
  at `.qwen/worktrees/agent-XXXXXXX/hello.txt`, NOT at the parent root.
- E2E #3: created worktree, committed work inside it, called exit_worktree
  with `discard_changes=true` — refused with clear message; worktree and
  branch both preserved.
- E2E #4: openai-logging confirms worktree tools absent from API tool list
  (7 tools sent instead of 9).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@LaZzyMan LaZzyMan force-pushed the claude/trusting-euclid-6fdfb9 branch from 4358d60 to 5d07cbe Compare May 12, 2026 09:50

@tanzhenxin tanzhenxin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Re-reviewing the fix commit against the prior REQUEST_CHANGES round. The three data-loss-class findings (illusory isolation, auto-approved exit_worktree action='remove' in default mode, and force-delete of unmerged commits) are all genuinely closed, along with the deferred-tools wiring and the foreground-throw leak. Nice work — the choice to provision the worktree before the agent-config override and rebind the working-dir getters as own properties threads cleanly through the existing tool-registry rebuild path. One material residual remains.

1. The newly-wired cleanup sweep is inert on common-case repos (severity: medium · confidence: very high)

The stale-worktree sweep is now invoked at startup, which closes the prior dead-code finding. But the unpushed-commits guard inside it runs git log --branches --not --remotes --oneline from the worktree directory, which lists unpushed commits across every local branch in the repo, not just the worktree's own branch. On a repo with no remote configured, or any repo with stray unpushed local branches anywhere, the command always returns non-empty and the sweep continues past every candidate. Net effect: leaked ephemeral worktrees still accumulate indefinitely on the common case the sweep was meant to address. Reproducer: in a fresh git init repo with two branches and no remote, the command prints every commit regardless of which worktree dir you cd into. Scoping it to the specific branch (git log <branch> --not --remotes --oneline), or reusing the for-each-ref --contains <tip> pattern already in hasUnmergedWorktreeCommits, would do the right thing.

Verdict

COMMENT — Fixes for the data-loss-class findings landed cleanly and the architecture is sound. The cleanup-sweep scope bug above is a one-line fix worth landing before merge. Smaller residual items (AUTO_EDIT still auto-approves exit_worktree action='remove' via the default info confirmation type, the override is asymmetric on getProjectRoot/getWorkspaceContext/getFileService, no unit-test coverage on the new isolation rebinding) are reasonable to address in a follow-up.

`Failed to create isolation worktree: ${created.error ?? 'unknown error'}`,
);
}
worktreeIsolation = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Worktree isolation 设置(L1220 worktreeIsolation = {...})之后、try/finally 块(L1880)才注册 cleanupWorktreeIsolation。中间约 660 行代码(createApprovalModeOverride、子 agent 创建、fork 处理等)如果抛出异常,worktree 目录和分支成为孤儿——外层 catch 没有访问清理闭包的途径。每次此类异常泄露一个完整的 git worktree 副本,只有 30 天后的 cleanupStaleAgentWorktrees 才能回收。

Suggested change
worktreeIsolation = {
// 将 worktreeIsolation 提升到外层 try 之前,在外层 catch 中调用清理
let worktreeIsolation: {...} | null = null;
try {
// ... provision, createApprovalModeOverride, agent creation, runFramed ...
} catch (e) {
if (worktreeIsolation) {
await cleanupWorktreeIsolation();
}
throw e;
}

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Comment thread packages/core/src/tools/agent/agent.ts Outdated
// Best-effort: preserve or remove the isolation worktree before
// the registry record reflects the failure.
try {
await cleanupWorktreeIsolation();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] 后台 agent 错误捕获路径中 await cleanupWorktreeIsolation() 的返回值被丢弃。如果 agent 崩溃前在 worktree 中做了部分工作,worktree 被保留但 registry.fail() / finalizeCancelled() 的消息中不包含 worktree 路径和分支名——用户完全不知道有残留的 worktree 可恢复。

Suggested change
await cleanupWorktreeIsolation();
const wtSuffix = formatWorktreeSuffix(await cleanupWorktreeIsolation());
// 将 wtSuffix 附加到 registry.fail() / finalizeCancelled() 的消息中

— DeepSeek/deepseek-v4-pro via Qwen Code /review

path: string;
branch: string;
} | null = null;
const failWorktreeProvisioning = (reason: string): ToolResult => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] failWorktreeProvisioningenter-worktree.ts 中所有 worktree 创建失败路径(git 不可用、非 git 仓库、createUserWorktree 失败)均无日志输出。同样,cleanupWorktreeIsolation 返回 preserved path 时也无日志。生产环境排查 worktree 相关问题时,grep 日志找不到任何线索。

Suggested change
const failWorktreeProvisioning = (reason: string): ToolResult => {
// 在每个 return failWorktreeProvisioning(...) / return errorResult(...) 前添加
debugLogger.warn('Worktree creation failed:', reason);
// 在 preserved return 前添加
debugLogger.info(`Worktree preserved: ${isolation.path} (branch=${isolation.branch})`);

— DeepSeek/deepseek-v4-pro via Qwen Code /review

// commits" flag because losing committed work is rarely what the
// user means by "remove worktree".
if (!this.params.discard_changes) {
const counts = await service.countWorktreeChanges(worktreePath);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion]countWorktreeChanges 返回 null(git 检查失败——可能是权限问题、索引损坏等),错误消息建议用户传 discard_changes: true 来绕过。但 null 未必意味着存在变更,可能是非内容错误。不应建议绕过安全检查而不了解根因。

Suggested change
const counts = await service.countWorktreeChanges(worktreePath);
// 不提示 discard_changes,只报告检查失败
return errorResult(
`Cannot inspect worktree "${this.params.name}" — git status failed. ` +
`Check permissions and repository integrity, then try again.`,
);

— DeepSeek/deepseek-v4-pro via Qwen Code /review

.map((s) => s.trim())
.filter((s) => s.length > 0 && s !== `refs/heads/${branchName}`);
return refs.length === 0;
} catch {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] hasUnmergedWorktreeCommits 的 catch 块吞没 git 错误——直接 return true(fail-closed 是正确的),但未记录实际的 git 错误。如果 git for-each-ref 因仓库损坏而失败,用户只看到 "has unmerged commits" 但不知道真正原因。

Suggested change
} catch {
} catch (error) {
debugLogger.warn(
`hasUnmergedWorktreeCommits failed for ${slug}: ${error}`,
);
return true;
}

— DeepSeek/deepseek-v4-pro via Qwen Code /review

const worktreePath = this.getUserWorktreePath(slug);
const branchName = `worktree-${slug}`;

const removed = await this.removeWorktree(worktreePath);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] removeUserWorktree 先调用 removeWorktree(删除 worktree 目录),然后才尝试 git branch -d。虽有前置的 unmerged commits 检查,但存在 TOCTOU 竞态窗口——检查与删除之间如果分支被推送了新提交,目录已被删除但分支被保留。建议交换顺序:先检查分支可删除性,再删除目录。

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Comment thread packages/core/src/tools/agent/agent.ts Outdated
};
}
try {
const result = await wtService.removeUserWorktree(isolation.slug, {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] cleanupWorktreeIsolation 只检查了 result.branchPreserved,未检查 result.success。当 removeUserWorktree 返回 { success: false }(目录删除失败),代码静默落到 return {}——worktree 泄露且无日志。

Suggested change
const result = await wtService.removeUserWorktree(isolation.slug, {
const result = await wtService.removeUserWorktree(isolation.slug, {
deleteBranch: true,
});
if (!result.success) {
debugLogger.warn(`Failed to remove worktree ${isolation.path}: ${result.error}`);
return { preservedPath: isolation.path, preservedBranch: isolation.branch };
}
if (result.branchPreserved) { ... }

— DeepSeek/deepseek-v4-pro via Qwen Code /review

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

补充一轮深入审计后的 8 条问题(不与已有 7 条 deepseek 评论重复)。Critical 两条涉及 isolation 实际不成立,建议作为 blocking 项处理。

const ov = agentConfig as any;
ov.getTargetDir = () => wtPath;
ov.getCwd = () => wtPath;
ov.getWorkingDir = () => wtPath;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Override 不完整。Config.getProjectRoot()(config.ts:1630)与 Config.getFileService()(config.ts:2147)直接读字段 this.targetDir,不经过 getTargetDir() getter。在 Object.create(base) 的原型委托下:agentConfig.getTargetDir() 返回 worktree 路径,但 agentConfig.getProjectRoot() 仍走原型链回到父的 targetDir —— 两个 getter 返回值不一致。

实际影响:

  • edit.ts:360 / write-file.ts:109getProjectRoot() 做 auto-memory 权限判断 → 用的是父项目根
  • read-file.ts:107getWorkspaceContext() 做工作区成员检查(PR 同样未 override 此 getter)→ 用的是父 workspace
  • glob.ts:96 在构造时缓存 config.getFileService()(父的 FileDiscoveryService,绑定父的 targetDir);glob.ts:159 用被 override 的 getTargetDir()path.relative() —— 与 fileService 的项目根不一致 → ignore 规则按错误的根评估

最坏路径:subagent 写出绝对/上溯相对路径,文件落在父工作区;run 结束时 hasWorktreeChanges(worktreePath) 看到 worktree 干净,cleanup 删除 worktree,agent 真实改动散落在父树且无任何 worktree 痕迹。这与 PR 描述里的 "tools confined to the worktree even when the model emits relative paths or unqualified shell commands" 相矛盾。

修复建议(按完整性递增):

  1. 同步 override getProjectRoot / getFileService / getWorkspaceContext
  2. 改写为 ov.targetDir = wtPath(own-property 字段 shadow),让所有读字段者一并生效
  3. 至少把 cleanup 改成同时 git status 父工作区,检测到外溢就保留 worktree 并报告

}

async execute(_signal: AbortSignal): Promise<ToolResult> {
const projectRoot = this.config.getTargetDir();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] 设计文档明确写"解析到 git 仓库根(处理已在子目录的情况)",实现直接用 getTargetDir()。在 monorepo 里从 monorepo/packages/cli/ 启动 qwen 时,worktree 落在 monorepo/packages/cli/.qwen/worktrees/<slug>,而不是 monorepo/.qwen/worktrees/<slug>

后果:Config.initializecleanupStaleAgentWorktrees(this.targetDir) 永远扫不到从其他 cwd 创建的 worktree,过期 agent worktree 永久堆积在每个子目录里。agent.ts:1204 同样存在此问题。

应在 GitWorktreeService 构造前用 git rev-parse --show-toplevel 解析到仓库根。

await this.git.raw([
'worktree',
'add',
'-B',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[High] -B 无条件重置已存在分支。注释说"用于复用同名 slug 旧分支",但若用户碰巧有一个无关的 worktree-foo 分支(之前手动 git checkout -b worktree-foo 留下,或队友推过),新建同 slug worktree 会静默把那个分支 reset 到当前 base —— commits 全部丢失。exit_worktree 的 unmerged check 只在删除时保护,创建路径无对称检查。

建议改为 -b(仅创建),分支已存在则报错并提示用户改名或先 git branch -d;或先检查分支是否存在再决定是否 -B

* repo: `<projectRoot>/.qwen/worktrees`.
*/
getUserWorktreesDir(): string {
return path.join(this.sourceRepoPath, '.qwen', WORKTREES_DIR);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[High] <projectRoot>/.qwen/worktrees/ 在项目树内部,但 PR 没有自动写入 gitignore。两个后果:

  1. 项目若没把 .qwen/ 整体 gitignore,每次 enter_worktree 都污染父工作区的 git status
  2. Glob/Grep/RipGrep 从父根递归搜索时会下钻到 .qwen/worktrees/,把 worktree 内文件混入父的搜索结果(与上面 C1 叠加更危险 —— subagent 看到的"父文件"里其实有自己 worktree 的副本)

建议首次创建时写入 .qwen/.gitignore 包含 worktrees/,与 claude-code 行为对齐。

if (!/^[a-zA-Z0-9._-]+$/.test(slug)) {
return 'Worktree name may only contain letters, digits, dots, underscores, and hyphens.';
}
if (slug.includes('..') || slug.startsWith('.') || slug.startsWith('-')) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] 验证规则在 Windows 上有边界漏洞:

  • 允许尾部 .(如 foo.),Windows 会规范化为 foo,导致创建/删除路径不一致
  • 不拒绝 Windows 保留名(con aux nul prn com1-com9 lpt1-lpt9),创建会硬失败或行为异常

建议参考 claude-code 的等价 helper 补全这两类检查。

const NOUNS = ['fox', 'owl', 'elm', 'oak', 'ray', 'sky', 'leaf', 'pine'];
const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
const suffix = Math.random().toString(16).slice(2, 6).padEnd(4, '0');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] generateAutoSlugMath.random(),agent slug 用 randomBytes —— 同一文件里两套随机源。空间 8 adj × 8 noun × 65536 hex ≈ 4M,长期项目里几百次 unnamed worktree 后碰撞概率明显,createUserWorktree 会返回 "Worktree already exists" 让用户重试。

randomBytes 已经被 import(agent slug 用),统一用它,并扩大词表或后缀位数。Math.random() 不该用作任何标识符的来源。

const result = await service.removeUserWorktree(entry.name, {
deleteBranch: true,
});
if (result.success) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] 仅检查 result.success,未检查 result.branchPreserved。当分支 tip 有未合并 commit 时 git branch -d 拒绝删除,removeUserWorktree 返回 {success: true, branchPreserved: true} —— cleanup 计数为成功,但 worktree-agent-<7hex> 分支永远残留(前置 hasUnpushedCommits 检查应该挡住,但浅克隆/分离 ref 等边界场景能漏过去)。

建议在 branchPreserved 时打 warn 日志,方便运维定期清理孤儿分支。

Comment thread packages/core/src/config/config.ts Outdated
// await this: it is a hygiene task that must never delay the
// first model turn.
if (!this.getBareMode()) {
void import('../services/worktreeCleanup.js')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] 每次 session 启动都跑 sweep(动态 import + readdir),即使 99% 的用户从未用过 worktree。绝对开销小,但完全不必要。

建议先 fs.access(<projectRoot>/.qwen/worktrees) 短路:目录不存在直接返回。另外 .then(...).catch(...) 链会丢失原始错误堆栈上下文,建议改成 try/await import/catch 形式。

…8 from wenshao)

The first round closed the data-loss-class issues. This round addresses
follow-ups from a deeper audit:

1. Stale-worktree sweep was inert on common-case repos
   `cleanupStaleAgentWorktrees` previously ran `git log --branches --not
   --remotes --oneline` from each worktree's directory — that lists
   unpushed commits across EVERY local branch, not just the worktree's
   own branch. On any repo with no remote configured (or with stray
   unpushed branches), the sweep refused to remove every candidate.
   Replaced with `service.hasUnmergedWorktreeCommits(slug)` which scopes
   the check to the worktree branch via `for-each-ref --contains <tip>`.
   Also added the `branchPreserved` warn log requested in M7 and an
   `fs.access` shortcut for the empty-worktrees-dir case (M8).

2. `cleanupWorktreeIsolation` and `worktreeIsolation` were inside the
   inner try (~660 lines from the outer catch). Hoisted both to the top
   of `execute()` so the outer catch can reap or preserve the worktree
   when anything between provisioning and the inner try throws (e.g.
   `createApprovalModeOverride`, agent creation). Closure carries the
   resolved `repoRoot` so cleanup never has to re-resolve.

3. Background error path discarded the cleanup result. Now captures
   `formatWorktreeSuffix(...)` and appends it to the registry's failure
   /cancel message, so users see the preserved path/branch even when
   the agent crashed before reporting.

4. `cleanupWorktreeIsolation` now treats `result.success === false` as
   "worktree still on disk" and surfaces it as preserved instead of
   silently dropping it from the result.

5. Override was incomplete. Several Config methods read `this.targetDir`
   directly (`getProjectRoot`, `getFileService`, etc.) — own-property
   getter overrides did not redirect them. Now also shadows `targetDir`
   and `cwd` as own properties on the agent's Config override, swaps in
   a `FileDiscoveryService` rooted at the worktree, and rebuilds
   `WorkspaceContext` to point at the worktree only. Verified
   end-to-end: shell `pwd > pwd-record.txt` (no directory arg) lands at
   `.qwen/worktrees/agent-<7hex>/pwd-record.txt`, not the parent root.

6. monorepo subdir issue. Both `enter_worktree` and the agent isolation
   path now resolve `git rev-parse --show-toplevel` first and anchor
   `.qwen/worktrees/<slug>` at the repo root. Worktrees created from
   any subdirectory now end up where the startup sweep can find them.

7. Replaced `git worktree add -B` (silent force-reset of pre-existing
   branches) with `git worktree add -b` plus an explicit existence
   check via `git for-each-ref` (NOT `show-ref --quiet`, which
   simple-git swallows). Pre-existing `worktree-<slug>` branches now
   trigger a clear error instead of clobbering committed work.

8. First worktree creation in a repo writes `<projectRoot>/.qwen/.gitignore`
   with `worktrees/` so worktree contents stay out of the parent's
   `git status`, glob/grep results, and bundle tools. Idempotent: never
   overwrites an existing file.

9. Logging across the failure paths (`enter_worktree` errors,
   `agent.ts:failWorktreeProvisioning`, `cleanupWorktreeIsolation`,
   `hasUnmergedWorktreeCommits` swallowed errors,
   `cleanupStaleAgentWorktrees`'s `branchPreserved` race).

10. `exit_worktree` no longer suggests `discard_changes: true` when the
    git status check itself fails — that would be advising the user to
    bypass a safety check whose precondition is unknown. Now points at
    the underlying repo problem.

11. `generateAutoSlug` switched from `Math.random()` (4 hex, weak RNG,
    one-in-65k collision) to `randomBytes` (6 hex, ~16M combinations).
    Two RNG sources in this file collapsed to one.

Pushed back: the TOCTOU swap in `removeUserWorktree` (S6 round 1) is
left as-is — `git branch -d` is the real safety, and reordering does
not eliminate the window. Windows reserved-name validation (M5 round 2)
deferred to a follow-up; the current allowlist already rejects path
separators, `..`, leading dot/dash, and the >64-char case.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@LaZzyMan

Copy link
Copy Markdown
Collaborator Author

Pushed `d777ff37f` addressing the round-2 findings (1 from @tanzhenxin, 7+8 from @wenshao via DeepSeek). Verified end-to-end against the failure cases described.

tanzhenxin

  • Sweep is inert on common-case repos — fixed. Replaced the broad `git log --branches --not --remotes` with `service.hasUnmergedWorktreeCommits(slug)`, which uses `for-each-ref --contains ` scoped to the worktree branch. Also added the `branchPreserved` warn log + `fs.access` shortcut for empty-worktrees-dir startups (your smaller residual + M7-r2 + M8-r2).

wenshao — round 1 (deepseek inline)

  • C1 (1220): `worktreeIsolation` + `cleanupWorktreeIsolation` hoisted to the top of `execute()`. The outer catch now invokes the helper as a final fallback, surfaces the preserved path/branch in the error message, and the closure carries `repoRoot` so cleanup never has to re-resolve.
  • C2 (1669): background catch path now captures `formatWorktreeSuffix(...)` and appends it to `registry.fail()` / `finalizeCancelled()` messages.
  • C3 (1187): added `debugLogger.warn` on every worktree creation failure and `debugLogger.info` on the preserved path.
  • S4 (exit-worktree:131): status-check failure no longer suggests `discard_changes: true`. Now points at filesystem permissions / repo integrity.
  • S5 (gitWorktreeService:1041): `hasUnmergedWorktreeCommits` now logs the swallowed git error before its fail-closed return.
  • S7 (agent:1345): cleanup helper now treats `result.success === false` as preserved instead of silently dropping the worktree.
  • S6 (973): pushed back. `git branch -d` is the real safety; reordering does not eliminate the TOCTOU window because `removeWorktree` does not lock either. Documented why in the commit body.

wenshao — round 2 (deeper audit)

  • C1 (agent:1271 — override incomplete): fixed properly. Now also shadows `targetDir` and `cwd` as own properties, swaps in a `FileDiscoveryService` rooted at the worktree, and rebuilds `WorkspaceContext` to point at the worktree only. Verified: `run_shell_command` with `pwd > pwd-record.txt` (no `directory` arg) now lands at `.qwen/worktrees/agent-<7hex>/pwd-record.txt`, not the parent root.
  • C2 (enter-worktree:67 — monorepo subdir): both `enter_worktree` and the agent isolation path now run `git rev-parse --show-toplevel` first and anchor `.qwen/worktrees/` at the repo root. Worktrees from any subdirectory now end up where the startup sweep can find them.
  • H3 (929 — `-B` reset): replaced with `git worktree add -b` plus an explicit existence check. Note: my first attempt used `git show-ref --verify --quiet`, but `simple-git.raw` swallows `--quiet`'s non-zero exit and resolves with `""`, which made `localBranchExists` always return `true` and blocked all worktree creation. Now uses `git for-each-ref --count=1 --format=%(refname) refs/heads/` which is exit-0 with output for present and empty for absent.
  • H4 (840 — gitignore): writes `/.qwen/.gitignore` containing `worktrees/` on first creation. Idempotent — never overwrites a user-curated file.
  • M5 (889 — Windows reserved names): deferred. The current allowlist already rejects path separators, `..`, leading dot/dash, and >64 chars. Adding the Windows-reserved-name and trailing-dot list is reasonable but worth its own change with Windows CI proof.
  • M6 (867 — `Math.random`): replaced with `randomBytes`-derived 6-hex suffix (~16M combinations × 8 adj × 8 noun ≈ 1B). Two RNG sources in this file collapsed to one. Test now also asserts 100 consecutive slugs are unique.
  • M7 (worktreeCleanup:94 — `branchPreserved`): added a warn log when the sweep removes the worktree but git refuses the safe-delete.
  • M8 (config:1249 — sweep on every startup): added an `fs.access` shortcut so the sweep is a single syscall in the common case.

Verification

  • Unit tests: 84 passed (17 worktree + 64 existing agent + 3 cleanup) — no regressions.
  • E2E sanity (against local `dist/cli.js`):
    • Shell isolation: `pwd > pwd-record.txt` (no `directory`) lands inside `agent-<7hex>/`, not parent.
    • `-b` refusal: pre-creating `worktree-collision-test` then calling `enter_worktree name='collision-test'` returns `branch worktree-collision-test already exists. Choose a different name…`.
    • `.qwen/.gitignore` written with `worktrees/`.
    • Unmerged-commit guard from round 1 still refuses `exit_worktree action='remove' discard_changes=true` when the worktree branch holds work.

Comment thread packages/core/src/services/gitWorktreeService.ts Fixed
Comment thread packages/core/src/services/gitWorktreeService.ts Fixed
CodeQL's `js/biased-cryptographic-random` flagged
`randomBytes(4)[i] % ARRAY.length` in `generateAutoSlug`. The math is
actually exact for the current word-list lengths (256 % 8 == 0), but
the lint rule does not know that — and a future contributor changing
the list to a non-power-of-two length would silently introduce bias.

Switched the index lookups to `crypto.randomInt(0, length)`, which uses
rejection sampling and is uniform by construction. Suffix still uses
`randomBytes(3).toString('hex')` since hex encoding is unbiased.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
}

async execute(_signal: AbortSignal): Promise<ToolResult> {
const projectRoot = this.config.getTargetDir();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] ExitWorktree 使用 this.config.getTargetDir()(当前工作目录)构建 GitWorktreeService,而 EnterWorktree 已正确通过 getRepoTopLevel() 解析到仓库根目录。从 monorepo 子目录(如 packages/core/)启动时,worktree 创建在 <repoRoot>/.qwen/worktrees/<slug>,但 ExitWorktree<subdir>/.qwen/worktrees/<slug> 查找,永远返回 "Worktree not found"。

Suggested change
const projectRoot = this.config.getTargetDir();
const cwd = this.config.getTargetDir();
const probe = new GitWorktreeService(cwd);
const projectRoot = (await probe.getRepoTopLevel()) ?? cwd;
const service = new GitWorktreeService(projectRoot);

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Comment thread packages/core/src/tools/agent/agent.ts Outdated
// the try block bailed before reaching it.
if (!worktreeCleanupRan) {
try {
await cleanupWorktreeIsolation();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] 双重清理竞态:当 runFramed() 抛出异常时,finally 块调用 cleanupWorktreeIsolation() 清理 worktree(删除目录),但未将 worktreeIsolation 置为 null。外层 catch(line ~2048)检测到 worktreeIsolation 仍为 truthy,再次调用清理——此时目录已不存在,hasWorktreeChanges() 在已删除路径上失败(fail-closed 返回 true),生成虚假的 [worktree preserved: /nonexistent/path] 消息。

Suggested change
await cleanupWorktreeIsolation();
if (!worktreeCleanupRan) {
try {
await cleanupWorktreeIsolation();
} catch {
// Helper logs its own failures; never mask the original
// error path with cleanup noise.
}
worktreeIsolation = null;
}

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Comment thread packages/core/src/config/config.ts Outdated
// first model turn.
if (!this.getBareMode()) {
void import('../services/worktreeCleanup.js')
.then(({ cleanupStaleAgentWorktrees }) =>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] 启动清理扫描传入 this.targetDir(cwd,可能是子目录),但 agent worktree 创建在仓库根目录下(getRepoTopLevel())。从子目录启动 qwen 时,清理扫描查找 <subdir>/.qwen/worktrees/——该目录不存在——始终返回 0,30 天过期清理完全失效。

Suggested change
.then(({ cleanupStaleAgentWorktrees }) =>
if (!this.getBareMode()) {
void import('../services/worktreeCleanup.js')
.then(async ({ cleanupStaleAgentWorktrees }) => {
const { GitWorktreeService } = await import('../services/gitWorktreeService.js');
const svc = new GitWorktreeService(this.targetDir);
const root = (await svc.getRepoTopLevel()) ?? this.targetDir;
return cleanupStaleAgentWorktrees(root);
})
.catch((error: unknown) => {
this.debugLogger.debug(
`Stale worktree sweep failed (non-fatal): ${error}`,
);
});
}

— DeepSeek/deepseek-v4-pro via Qwen Code /review

// above guarantees correctness; the notice reduces user-visible
// surprises when the model summarises file paths.
if (worktreeIsolation) {
const notice = buildWorktreeNotice(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] buildWorktreeNotice() 使用 this.config.getTargetDir() 作为 parentCwd,该值可能是子目录而非仓库根目录。worktreeIsolation.repoRoot 在此处已可用,应使用它以确保路径转换指引在子目录场景下正确。

Suggested change
const notice = buildWorktreeNotice(
const notice = buildWorktreeNotice(
worktreeIsolation.repoRoot,
worktreeIsolation.path,
);

— DeepSeek/deepseek-v4-pro via Qwen Code /review

/**
* Lists all user worktrees in this repo by reading the worktrees directory.
*/
async listUserWorktrees(): Promise<WorktreeInfo[]> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] listUserWorktrees() 方法定义后从未被任何代码调用,是死代码。且使用同步 execSync 在循环中执行 git 命令,如果将来被调用会阻塞事件循环。建议删除此方法,或在有调用方时改为异步实现。

— DeepSeek/deepseek-v4-pro via Qwen Code /review

]);
return out.trim().length > 0;
} catch {
return false;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] localBranchExists() 的 catch 块完全为空(} catch { return false; })。如果 for-each-ref 因磁盘满/权限/仓库损坏等原因失败,调用方误以为分支不存在继续尝试创建,且日志中无任何线索——凌晨 3 点排障时完全看不到根因。

Suggested change
return false;
} catch (error) {
debugLogger.warn(
`localBranchExists failed for ${branchName}: ${error}`,
);
return false;
}

— DeepSeek/deepseek-v4-pro via Qwen Code /review

The previous round added `getRepoTopLevel` for `enter_worktree`'s
provisioning, but missed three sibling call sites that still used the
raw cwd. The double-cleanup race in the foreground path also leaked
stale `[worktree preserved]` suffixes on rejected promises. All six
findings from the deeper audit are addressed:

1. exit_worktree now resolves through `getRepoTopLevel()` before
   building its `GitWorktreeService`, mirroring `enter_worktree`. Without
   this, launching `qwen` from a monorepo subdirectory created the
   worktree under the repo root but exit_worktree looked under the
   subdir's `.qwen/worktrees/` and always returned "Worktree not found".
   Verified end-to-end: enter + exit from `packages/core/` works.

2. agent.ts cleanup helper now nulls `worktreeIsolation` immediately
   after capturing the closure value. The previous structure could
   reach the helper twice — once in the foreground try's success path
   and once in the foreground finally fallback (or once in the inner
   try and once in the outer catch on a thrown rejection). The second
   call would `hasWorktreeChanges()` against a directory the first
   call already removed, fail-closed, and emit a bogus
   `[worktree preserved: <missing path>]` suffix.

3. Config.initialize's startup sweep now resolves `getRepoTopLevel()`
   before invoking `cleanupStaleAgentWorktrees`. Without this, every
   subdir launch scanned a non-existent `<subdir>/.qwen/worktrees/`
   and the 30-day expiry sweep was permanently a no-op.

4. agent.ts's `buildWorktreeNotice` now passes
   `worktreeIsolation.repoRoot` as `parentCwd` instead of
   `this.config.getTargetDir()`. The notice's path-translation
   guidance (≈ "translate paths from <parent> to <worktree>") would
   otherwise misdirect the subagent in a monorepo subdir launch.

5. Removed dead method `GitWorktreeService.listUserWorktrees`. It had
   no callers anywhere in the codebase and used `execSync` in a loop
   (would have blocked the event loop if anyone wired it up).

6. `localBranchExists` no longer swallows git failures silently. The
   defensive `false` default is preserved (so `git worktree add -b`
   itself surfaces the conflict if the check missed an existing
   branch), but the catch now logs via `debugLogger.warn` so disk-full
   / permission / ref-store-corruption cases are visible in debug
   output instead of being invisible.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@LaZzyMan

Copy link
Copy Markdown
Collaborator Author

Pushed `ae5aa557e` addressing the round-3 findings. The previous round added `getRepoTopLevel` for `enter_worktree`'s provisioning but missed three sibling call sites — the audit caught all of them.

Critical

  • exit-worktree:85 — now resolves through `getRepoTopLevel()` before constructing `GitWorktreeService`, mirroring `enter_worktree`. Verified end-to-end: launched `qwen` from `packages/core/` of a fake monorepo, ran `enter_worktree name='subdir-test'` then `exit_worktree name='subdir-test' action='remove'` → worktree was created at `/.qwen/worktrees/subdir-test`, exit found it and removed cleanly. Without the fix, exit would have searched `packages/core/.qwen/worktrees/` and returned "Worktree not found".
  • agent.ts:2006 (double-cleanup race) — fixed at the source: `cleanupWorktreeIsolation` now nulls `worktreeIsolation` immediately after capturing the closure value, so any subsequent re-entry (foreground-finally fallback firing in parallel with the outer catch on a thrown rejection) sees no isolation and bails. The previous structure had two paths that could each invoke cleanup; the second would `hasWorktreeChanges()` against a removed directory, fail-closed, and emit a bogus `[worktree preserved: ]` suffix.
  • config.ts:1250 (sweep root) — `Config.initialize` now resolves `getRepoTopLevel()` before calling `cleanupStaleAgentWorktrees`. Without this, every subdir launch scanned a non-existent `/.qwen/worktrees/` and the 30-day sweep was permanently a no-op.

Suggestions

  • agent.ts:1439 (notice parentCwd) — `buildWorktreeNotice` now receives `worktreeIsolation.repoRoot` instead of `this.config.getTargetDir()`. The path-translation guidance now points at the actual repo root.
  • gitWorktreeService.ts:1178 (dead code) — removed `listUserWorktrees`. Confirmed no callers; also avoided the `execSync`-in-loop event-loop hazard.
  • gitWorktreeService.ts:1034 (silent catch) — `localBranchExists` now logs via `debugLogger.warn` before returning the defensive `false`. The defensive default itself is preserved (the actual create still surfaces a duplicate via `git worktree add -b`'s own error).

84 unit tests pass, typecheck clean, monorepo-subdir E2E sanity checked against the local build.

@LaZzyMan LaZzyMan requested review from tanzhenxin and wenshao May 13, 2026 06:50
return removed;
}

async function hasTrackedChanges(worktreePath: string): Promise<boolean> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you expand on this? The comment is just "test", and hasTrackedChanges at line 138 doesn't have an obvious issue I can act on without more context. If this was a draft / accidental save, I'll resolve the thread; if you intended specific feedback (e.g. about the helper's behaviour, missing test coverage, the status field choice, or fail-closed handling), please point me at it and I'll address it.

* - Only `[a-zA-Z0-9._-]` characters; no path separators
* - No `..` or leading/trailing dots (would resolve outside the worktrees dir)
*/
static validateUserWorktreeSlug(slug: string): string | null {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test 912

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the other 'test' comment — could you expand? The line touches createUserWorktree near where the worktree directory is created. If this is a draft, feel free to resolve; if you intended specific feedback, please point me at it.

@LaZzyMan

Copy link
Copy Markdown
Collaborator Author

Pushed `3e2002588` addressing the round-4 findings.

Critical

  • `agent-` prefix collision with cleanup pattern — `validateUserWorktreeSlug` now rejects names starting with `agent-`. A user-named `agent-1234567` previously matched the cleanup regex and would have been silently swept after 30 days along with whatever work it contained. Test added.
  • Slug producer/consumer string-coupled across files — lifted `AGENT_WORKTREE_PREFIX`, `AGENT_WORKTREE_HEX_LENGTH`, `AGENT_WORKTREE_SLUG_PATTERN`, and `generateAgentWorktreeSlug()` to `gitWorktreeService.ts`. Both `agent.ts` (producer) and `worktreeCleanup.ts` (consumer) now import the same constants. A future hex-length change is a single edit.
  • Startup sweep invisible at default log level — errors promoted from `debug` to `warn`; successful removals (count > 0) now log at `info` with the count and the root scanned. Operators chasing a worktree leak have a breadcrumb.
  • `getRepoTopLevel` silent catch — logs the underlying error before returning `null`. Without this the worktree creators and the startup sweep could disagree silently about which dir to use.
  • `hasTrackedChanges` silent catch — logs before its fail-closed `return true`. Distinguishes "real changes — leave alone" from "git index unreadable — repo may be corrupt".
  • `cleanupWorktreeIsolation` claimed `preservedPath` for a removed directory — the `branchPreserved` race path now returns only `preservedBranch`. `formatWorktreeSuffix` emits a distinct message: `[worktree directory removed; branch X preserved — recover with `git worktree add X`]`. No more pointing the user at a path that doesn't exist.

Suggestion

  • `removeUserWorktree` swallowed branch-delete failures — both the `-d` and `-D` catch blocks now `debugLogger.warn` with the underlying error. Locked refs / permissions / disk-full are no longer indistinguishable from "unmerged commits".

Two "test" comments

Replied on both discussion 3232474087 and discussion 3232485143 asking for clarification — both look like stray drafts (just the literal word "test"). Happy to act once the intent is clear.

86 unit tests pass; typecheck clean.

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test coverage gaps (no specific diff line):

[Critical] exit-worktree.ts (294 new lines, 3 safety guards + branch deletion coordination) has no test file. [Critical] Agent worktree isolation (~200 new lines in agent.ts: config override, cleanup coordination, foreground/background paths) has zero coverage in agent.test.ts. [Suggestion] gitWorktreeService.ts new methods (~400 lines, 8 methods) lack dedicated tests (pre-existing test file not updated). [Suggestion] Duplicate errorResult() function in enter-worktree.ts and exit-worktree.ts.

Comment thread packages/core/src/tools/agent/agent.ts Outdated
const wtService =
projectRoot === cwd ? probe : new GitWorktreeService(projectRoot);
const slug = `agent-${randomBytes(4).toString('hex').slice(0, 7)}`;
const created = await wtService.createUserWorktree(slug);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Agent isolation worktree is created from the wrong branch.

wtService.createUserWorktree(slug) is called without a baseBranch argument. createUserWorktree falls back to getCurrentBranch(), which runs git rev-parse --abbrev-ref HEAD on the main repo root — returning main (or whatever the primary working tree has checked out). If the user is on feature-x, inside a user worktree (worktree-my-feature), or any non-main branch, the isolated subagent starts from main and sees the wrong code.

Suggested change
const created = await wtService.createUserWorktree(slug);
const parentBranch = await wtService.getCurrentBranch();
const created = await wtService.createUserWorktree(slug, parentBranch);

— DeepSeek/deepseek-v4-pro via Qwen Code /review

* Counts uncommitted file changes in a worktree. Returns null if the
* worktree can't be inspected (which the caller should treat as "dirty").
*/
async countWorktreeChanges(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] countWorktreeChanges misses conflicted files — bypasses the dirty-state guard in exit-worktree.

countWorktreeChanges manually sums staged + modified + deleted + renamed + created but omits status.conflicted. In simple-git, conflicted files appear only in conflicted[] (mutually exclusive with other arrays). A worktree with only merge conflicts (no other modifications) returns {tracked: 0, untracked: 0} — the dirty guard passes, exit_worktree proceeds to delete without requiring discard_changes: true. Conflict resolution progress is lost.

Suggested change
async countWorktreeChanges(
const tracked =
status.staged.length +
status.modified.length +
status.deleted.length +
status.renamed.length +
status.created.length +
status.conflicted.length;

— DeepSeek/deepseek-v4-pro via Qwen Code /review

const service =
projectRoot === cwd ? probe : new GitWorktreeService(projectRoot);

const worktreePath = service.getUserWorktreePath(this.params.name);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] exit_worktree lacks session ownership validation — any session can delete any worktree.

The design doc guarantees: 仅操作本会话通过 EnterWorktree 创建的 worktree. However, execute() only checks that the worktree directory exists — it never verifies that the current session created it. In yolo mode, a malicious prompt injection can enumerate .qwen/worktrees/, then call exit_worktree with any discovered name to delete another session's worktree.

Phase A mitigation: write a .session metadata file into the worktree directory at creation time, check it here before allowing action='remove'.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

: 'Enter a new worktree';
}

async execute(_signal: AbortSignal): Promise<ToolResult> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Missing nested worktree prevention.

The design doc requires: 验证当前未在 worktree 中(防止嵌套). Neither enter_worktree nor agent isolation checks whether the current working directory is already inside a worktree. The model can call enter_worktree again from within a worktree, creating nested .qwen/worktrees/<slug>/.qwen/worktrees/<slug> — path resolution breaks, nested worktrees are orphaned on exit.

Suggested change
async execute(_signal: AbortSignal): Promise<ToolResult> {
// At the top of execute(), before any worktree creation:
if (cwd.includes('/.qwen/worktrees/')) {
return errorResult('Already inside a worktree. Exit the current worktree first.');
}

— DeepSeek/deepseek-v4-pro via Qwen Code /review

// Reached here when the branch had unmerged commits and the caller
// did not opt into force-delete. Surface this so callers can leave
// a note for the user.
return { success: true, branchPreserved: true };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] removeUserWorktree reports preservedPath for an already-deleted directory.

removeUserWorktree deletes the worktree directory first (removeWorktree), then attempts branch deletion. If branch deletion fails (unmerged commits), it returns { success: true, branchPreserved: true }. Callers (cleanupWorktreeIsolation, cleanupStaleAgentWorktrees) then surface a preservedPath pointing to a directory that no longer exists — misleading users into thinking work is recoverable at that path (the commits are safe on the branch, but the message is wrong).

When branchPreserved: true, the preservedPath should be null or accompanied by a note that the directory was removed but the branch retains the commits.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

return removed;
}

async function hasTrackedChanges(worktreePath: string): Promise<boolean> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] hasTrackedChanges misses conflicted files and unnecessarily scans untracked files.

Same conflicted[] gap as countWorktreeChanges: conflicted-only worktrees appear clean and get swept. Also, simpleGit(worktreePath).status() runs full git status --porcelain including untracked-file discovery — the slowest part of git status on large repos — but only staged/modified/deleted/renamed/created arrays are checked. The comment says Skip the untracked-files scan, but status() still does it.

Suggested change
async function hasTrackedChanges(worktreePath: string): Promise<boolean> {
// Add conflicted check and use --untracked-files=no:
const result = await wtGit.raw(['diff', '--name-only', '--exit-code']);
if (result.trim().length > 0) return true;
// Also check for staged changes:
const stagedCheck = await wtGit.raw(['diff', '--name-only', '--cached', '--exit-code']);
return stagedCheck.trim().length > 0;

— DeepSeek/deepseek-v4-pro via Qwen Code /review

// guidance would tell the agent to map worktree paths back to a
// monorepo subdir that may not contain the parent file at all.
if (worktreeIsolation) {
const notice = buildWorktreeNotice(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] buildWorktreeNotice uses repoRoot instead of the parent agent's actual CWD as parentCwd.

const notice = buildWorktreeNotice(
  worktreeIsolation.repoRoot,  // repo top-level, not parent's actual cwd
  worktreeIsolation.path,
);

If the parent agent is running from a monorepo subdirectory (e.g. packages/core/) or is itself inside a user worktree, the notice tells the subagent to translate paths relative to the repo root — but inherited context references paths relative to the parent's actual working directory. This mismatch can cause path-translation errors.

Suggested change
const notice = buildWorktreeNotice(
const notice = buildWorktreeNotice(
this.config.getTargetDir(), // parent agent's actual working directory
worktreeIsolation.path,
);

— DeepSeek/deepseek-v4-pro via Qwen Code /review

logStartSession(this, new StartSessionEvent(this));
this.debugLogger.info('Config initialization completed');

// Fire-and-forget sweep of stale ephemeral worktrees left behind by

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Startup sweep runs git rev-parse --show-toplevel before checking if the worktrees directory exists.

The sweep dynamically imports GitWorktreeService, constructs an instance, and calls getRepoTopLevel() — all before cleanupStaleAgentWorktrees does fs.access(worktreesDir) which fast-bails if the directory is absent. For 99% of users who never use worktrees, a git subprocess is spawned on every session startup unnecessarily.

Suggested change
// Fire-and-forget sweep of stale ephemeral worktrees left behind by
// Check directory existence first, skip git entirely if absent:
const worktreesDir = path.join(this.targetDir, '.qwen', 'worktrees');
try { await fs.promises.access(worktreesDir); } catch { return; }
// ... then proceed with existing logic

— DeepSeek/deepseek-v4-pro via Qwen Code /review

LaZzyMan and others added 2 commits May 13, 2026 16:53
Self-review caught a handful of issues across three categories:

Reuse:
- `pathExists` in the new code now uses the existing `fileExists` from
  `utils/fileUtils.ts` instead of duplicating an `fs.access` wrapper.
- `worktree-` branch prefix was string-literalled in five places. Added
  `WORKTREE_BRANCH_PREFIX` and `worktreeBranchForSlug(slug)` exports in
  `gitWorktreeService.ts`; updated `gitWorktreeService.ts`,
  `worktreeCleanup.ts`, and `exit-worktree.ts` to use them. Future
  prefix changes are a single edit.

Efficiency:
- `Config.initialize` used two `await import(...)` calls inside the
  startup-sweep IIFE, paying that cost on every CLI start. Switched to
  static imports at the top of `config.ts` — the modules are tiny and
  the dynamic indirection bought nothing.
- `cleanupWorktreeIsolation` in `agent.ts` ran `hasWorktreeChanges` and
  `hasUnmergedWorktreeCommits` sequentially. They have no data
  dependency on each other and each spawns its own `git` invocation;
  `Promise.all` halves the cleanup wall-clock on the common path.
  Same fix in `worktreeCleanup.ts`'s per-entry loop.
- `ensureWorktreesGitignored` used `fs.access` then `fs.writeFile`, a
  TOCTOU race when two agent invocations created worktrees concurrently
  (both could pass the `access` check and the second would clobber the
  first's `.gitignore`). Now writes with `flag: 'wx'` and treats
  `EEXIST` as the no-op case — atomic in one syscall.

Quality:
- Dropped the `worktreeCleanupRan` boolean in the foreground execution
  path. `cleanupWorktreeIsolation` already nulls its closure variable
  at the top of every call (see the comment at its definition), so
  re-entries are no-ops. The boolean and its tracking were dead weight
  that obscured the real guard.
- Trimmed the Phase-2 override comment block to drop the WHAT-stating
  enumerations (items 3 and 4 just narrated the lines below) and
  removed a navigation comment about hoisted helpers — the helpers are
  visible at the top of the same method.

84 unit tests pass; typecheck clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ectness

Five critical findings + four suggestions, all closed.

Critical:
1. Wrong base branch for agent isolation. `createUserWorktree(slug)` with
   no `baseBranch` arg fell back to `getCurrentBranch()` on the **main**
   working tree, returning `main` regardless of which branch the user
   was actually on. A subagent invoked from `feature-x` would silently
   start from `main` and produce diffs against the wrong baseline.
   `enter_worktree` had the same bug. Both now resolve the parent's
   current branch first and pass it explicitly. Verified end-to-end:
   `git checkout feature-x` → `enter_worktree` → worktree HEAD includes
   the feature-x commit.

2. `countWorktreeChanges` (used by `exit_worktree`'s dirty-state guard)
   missed `status.conflicted[]`. In simple-git that array is mutually
   exclusive with the staged/modified/etc. arrays, so a worktree
   mid-merge with only conflicts looked `{tracked: 0, untracked: 0}`
   to the guard and `action='remove'` would proceed without
   `discard_changes: true`. Added `+ status.conflicted.length`.

3. `exit_worktree` had no session-ownership check, contradicting the
   design doc's "only operates on worktrees created by THIS session".
   In yolo mode a prompt injection could enumerate `.qwen/worktrees/`
   and pass any name to drop another session's work. Now:
   `enter_worktree` and agent isolation write a `.qwen-session`
   marker into the worktree at provisioning time; `exit_worktree
   action='remove'` reads it and refuses if it does not match the
   current `Config.getSessionId()`. Worktrees from before this guard
   (no marker file) are treated as "owner unknown" — allowed with a
   warn log so the change is observable.

4. `enter_worktree` did not refuse nested invocations from inside an
   existing worktree, contradicting the design doc. Now rejects any
   cwd containing `.qwen/worktrees/` as a path component, with a
   clear "Already inside a git worktree…" message. Verified: enter
   from inside a worktree returns is_error with that text.

6. `hasTrackedChanges` (cleanup sweep) had the same `conflicted[]`
   gap. Rewrote to use raw `git status --porcelain --untracked-files=no`
   which lists every tracked change including `UU` conflict markers
   in a single git call and explicitly skips the untracked walk
   (the prior comment claimed to skip it, but `status()` always
   does the scan).

Suggestion:
7. `buildWorktreeNotice` now receives the parent agent's actual
   `getTargetDir()` again (was switched to `repoRoot` in round 3 on
   a different reviewer's suggestion; round-5 caught that the model's
   inherited paths reference the parent's cwd, not necessarily the
   repo root, so the prior behaviour was correct).

8. Startup sweep now does `fs.access(<targetDir>/.qwen/worktrees)`
   *before* importing GitWorktreeService and spawning `git
   rev-parse --show-toplevel`. The git probe is reserved for users
   who actually have a worktrees directory locally — 99% of users
   pay only one syscall on startup.

9. Tests:
   - New `exit-worktree.test.ts` covers metadata, validation,
     `getDefaultPermission` (ask vs allow), and getDescription.
   - `agent.test.ts` adds three `validateToolParams` cases for the
     `isolation` parameter (accepted with subagent_type, rejected
     without, rejected for non-"worktree" values).
   - `enter-worktree.test.ts` adds round-trip tests for
     `writeWorktreeSessionMarker` / `readWorktreeSessionMarker` plus
     a `worktreeBranchForSlug` sanity check.
   - Total: 101 tests pass (was 86 → +15).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@LaZzyMan

Copy link
Copy Markdown
Collaborator Author

Pushed `1d7da201d` addressing round-5 findings. Five critical + four suggestions, all closed.

Critical

  • Wrong base branch for agent isolation (and `enter_worktree`): `createUserWorktree(slug)` was falling back to the main working tree's branch via `getCurrentBranch()`. Both call sites now resolve the parent's current branch first and pass it explicitly. Verified: `git checkout feature-x` → `enter_worktree` → worktree HEAD includes the feature-x commit.
  • `countWorktreeChanges` missing `conflicted[]`: simple-git puts conflicted files in a separate mutually-exclusive array; a worktree mid-merge with only conflicts read as `{tracked: 0, untracked: 0}` and slipped past the dirty-state guard. Added `+ status.conflicted.length`.
  • `exit_worktree` lacked session-ownership validation (design-doc commitment): a prompt injection in yolo mode could enumerate `.qwen/worktrees/` and drop another session's work. Added a `.qwen-session` marker written by `enter_worktree` and agent isolation at creation time; `exit_worktree action='remove'` reads and validates it. Worktrees from before this guard (no marker) are treated as "owner unknown" — allowed with a warn log so the bypass is observable.
  • `enter_worktree` did not refuse nested invocations (design-doc commitment): now rejects any cwd containing `.qwen/worktrees/` as a path component. Verified: enter from inside a worktree returns is_error with a clear message.
  • `hasTrackedChanges` had the same `conflicted[]` gap: rewrote using raw `git status --porcelain --untracked-files=no` — single call that includes `UU` markers and actually skips the untracked walk the prior comment claimed to skip.

Suggestion

  • `buildWorktreeNotice` parentCwd back to `getTargetDir()`: switched away from `repoRoot` (round-3 suggestion) per round-5 reasoning that inherited model context speaks from the parent's cwd, not the repo root.
  • Startup sweep `fs.access` before git: 99% of users now pay just one syscall on startup; the `git rev-parse` probe runs only when a `.qwen/worktrees/` directory exists locally.
  • Test coverage:
    • New `exit-worktree.test.ts` (8 tests): metadata, validation, default permission, getDescription.
    • `agent.test.ts` (+3 tests): `isolation` parameter accepted with `subagent_type`, rejected without, rejected for non-`worktree` values.
    • `enter-worktree.test.ts` (+4 tests): `writeWorktreeSessionMarker` / `readWorktreeSessionMarker` round-trip + missing/empty fallthrough + `worktreeBranchForSlug` sanity.
    • Total: 101 tests pass (was 86, +15).
  • `removeUserWorktree` API ambiguity (your S5): kept as-is. `cleanupWorktreeIsolation` now returns only `preservedBranch` (not the stale `preservedPath`) when the directory was removed but the branch was kept; the format helper emits a distinct `[worktree directory removed; branch X preserved — recover with `git worktree add`]` message. Surface ambiguity at the caller, not the service.

Typecheck clean. Bundle built. E2E sanity check covered #1, #4, and the session marker write.

LaZzyMan and others added 2 commits May 13, 2026 17:16
Empty string `''` is a valid `string` type, so the @ts-expect-error
directive on `validateToolParams({ name: '', action: 'keep' })` did
nothing — TypeScript correctly accepted the line, and `tsc --build`
in CI reported TS2578 ("Unused '@ts-expect-error' directive"). The
runtime assertion already covers the case; the directive was leftover
from an earlier draft.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Arena test mocks `gitWorktreeService.js` with a factory that
returns only `{ GitWorktreeService }`. PR #4073 added several other
exports to that module (`AGENT_WORKTREE_SLUG_PATTERN`,
`WORKTREE_BRANCH_PREFIX`, `worktreeBranchForSlug`,
`generateAgentWorktreeSlug`, `writeWorktreeSessionMarker`,
`readWorktreeSessionMarker`, `WORKTREE_SESSION_FILE`).

Other modules in the dep graph reach the mocked surface — most
notably `worktreeCleanup.ts` imports `AGENT_WORKTREE_SLUG_PATTERN`
and `worktreeBranchForSlug`, and now reaches the mock via the static
`config.ts` → `worktreeCleanup.ts` import chain added in the
self-review pass. The Arena test failed at module-load with:

  Caused by: Error: [vitest] No "AGENT_WORKTREE_SLUG_PATTERN" export
  is defined on the "../../services/gitWorktreeService.js" mock. Did
  you forget to return it from "vi.mock"?

Use `importOriginal` to capture every real export, spread it into
the return object, and only replace `GitWorktreeService` (the class
the test actually needs to mock). The class-level mock keeps its
existing static-method shims.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test coverage gaps:

[Critical] exit-worktree.test.tsexecute() has zero test coverage. The session ownership guard (lines 136-147), owner-null fallback, keep action, and all three remove guards are untested.

[Critical] enter-worktree.test.tsexecute() has zero test coverage. Nested prevention regex, git-unavailable path, getCurrentBranch() fallback, and createUserWorktree failure paths are untested.

[Critical] agent.test.ts — isolation execution path has zero test coverage. provisionWorktreeIsolation, cleanupWorktreeIsolation, formatWorktreeSuffix (preservedBranch-only path), and Config override layer are untested.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

return 'Worktree name must not start with "." or "-" or contain "..".';
}
if (slug.startsWith(`${AGENT_WORKTREE_PREFIX}-`)) {
return (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] validateUserWorktreeSlug rejects all agent- prefixed slugs, but generateAgentWorktreeSlug() produces exactly agent-<7hex> slugs. createUserWorktree() calls validateUserWorktreeSlug unconditionally before creating the worktree, so every AgentTool isolation:'worktree' invocation will fail with "Worktree name must not start with 'agent-'...". The agent isolation feature is completely broken by its own validation.

Suggested change
return (
if (slug.startsWith(`${AGENT_WORKTREE_PREFIX}-`)) {
// Allow exact agent-<7hex> slugs generated by generateAgentWorktreeSlug();
// reject user-chosen names that would collide with the ephemeral cleanup pattern.
if (!AGENT_WORKTREE_SLUG_PATTERN.test(slug)) {
return (
`Worktree name must not start with "${AGENT_WORKTREE_PREFIX}-": that prefix ` +
`is reserved for ephemeral agent worktrees and is subject to ` +
`automatic cleanup after 30 days.`
);
}
}

Also update enter-worktree.test.ts:71agent-1234567 (7 hex chars) should now pass validation instead of being rejected.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

const wtGit = simpleGit(worktreePath);
const status = await wtGit.status();
// `not_added` covers untracked; `staged`/`modified`/etc. cover the rest.
return !status.isClean();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] hasWorktreeChanges uses status.isClean() which does not check status.conflicted. In simple-git, conflicted is mutually exclusive with staged/modified/etc. — a worktree with only merge conflicts appears clean and gets silently deleted by cleanupWorktreeIsolation in agent.ts. This same PR correctly fixed countWorktreeChanges (used by exit_worktree) and hasTrackedChanges (used by cleanupStaleAgentWorktrees) to include conflicted, but missed this function which is used by the agent cleanup path.

Suggested change
return !status.isClean();
const status = await wtGit.status();
return !status.isClean() || status.conflicted.length > 0;

Or switch to git status --porcelain like hasTrackedChanges does for consistency.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

const trimmed = raw.trim();
return trimmed.length > 0 ? trimmed : null;
} catch {
return null;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] readWorktreeSessionMarkercatch { return null; } silently treats every I/O error (disk failure, permission error, corrupt NFS) as "marker file not found". When storage is failing, exit_worktree's ownership guard is bypassed, allowing a different session to delete worktrees it shouldn't. The only log message produced is the misleading "no session marker; allowing removal" — not the actual I/O error.

Suggested change
return null;
} catch (error) {
debugLogger.warn(
`readWorktreeSessionMarker: cannot read ${path.join(worktreePath, WORKTREE_SESSION_FILE)}: ${error}`,
);
return null;
}

— DeepSeek/deepseek-v4-pro via Qwen Code /review

let exists = false;
try {
const stat = await fs.stat(worktreePath);
exists = stat.isDirectory();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] fs.stat catch block — catch { exists = false; } maps EACCES, EIO, ENOTDIR and every other error to "Worktree not found", with zero logging. An operator sees "Worktree not found" in the tool output while ls shows the directory clearly exists — the root cause (permissions, disk error) is invisible.

Suggested change
exists = stat.isDirectory();
} catch (error) {
if (isNodeError(error) && error.code === 'ENOENT') {
exists = false;
} else {
debugLogger.warn(
`exit_worktree: cannot stat ${worktreePath}: ${error}`,
);
return errorResult(
`Cannot access worktree at ${worktreePath} (${error}).`,
);
}
}

— DeepSeek/deepseek-v4-pro via Qwen Code /review


let mtimeMs: number;
try {
const stats = await fs.stat(worktreePath);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] cleanupStaleAgentWorktreescatch { continue; } on fs.stat silently skips any entry where stat fails (permission error, disk error, unmounted filesystem). Stale worktrees accumulate permanently with no log trail. Operators grep for "Removed stale" and find nothing, but the worktree directories keep consuming disk.

Suggested change
const stats = await fs.stat(worktreePath);
} catch (error) {
debugLogger.warn(
`Cannot inspect worktree ${worktreePath} — skipping cleanup: ${error}`,
);
continue;
}

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Comment thread packages/core/src/config/config.ts Outdated
this.targetDir,
'.qwen',
'worktrees',
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Fast-bail fs.access(targetDir/.qwen/worktrees) runs before resolving the repo root. When qwen is launched from a monorepo subdirectory (e.g. packages/core/), targetDir/.qwen/worktrees/ never exists — worktrees live at <repoRoot>/.qwen/worktrees/. The code returns early, never calling cleanupStaleAgentWorktrees. The comment says "cleanupStaleAgentWorktrees will fast-bail there too" but it never reaches that code. Stale agent worktrees accumulate indefinitely for monorepo-subdir users.

Suggested change
);
// Resolve the repo root first, then fast-bail against the actual worktreesDir.
const probe = new GitWorktreeService(this.targetDir);
const root = (await probe.getRepoTopLevel()) ?? this.targetDir;
const worktreesDir = path.join(root, '.qwen', 'worktrees');
try {
await fsPromises.access(worktreesDir);
} catch {
return;
}
const removed = await cleanupStaleAgentWorktrees(root);

— DeepSeek/deepseek-v4-pro via Qwen Code /review

* file lives outside the working tree (it is .gitignored as part of
* `.qwen/worktrees/.gitignore`) so it cannot leak into commits.
*/
export const WORKTREE_SESSION_FILE = '.qwen-session';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] .qwen-session is written inside the worktree directory but nothing in the worktree's own .gitignore ignores it. The parent repo's .qwen/.gitignore only hides worktrees/ from the parent — inside the child worktree, .qwen-session is a visible untracked file. If a subagent runs git add -A or git add ., the session ID gets committed to the worktree branch.

Fix: write a per-worktree .gitignore containing .qwen-session during worktree creation, or store the marker outside the worktree directory.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

};
};

if (this.params.isolation === 'worktree') {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] enter_worktree has a nested worktree guard (regex /\\.qwen[\\\\/]worktrees[\\\\/]/ at enter-worktree.ts:82), but the agent isolation path at this line has no equivalent guard. If the parent Config's targetDir is already inside a worktree, creating another isolation worktree would produce two worktrees at the same level with the model's mental map still referencing the outer worktree — the same path-confusion rationale that motivated the enter_worktree guard.

Add the same check before getRepoTopLevel:

if (/\.qwen[\\/]worktrees[\\/]/.test(cwd)) {
  return failWorktreeProvisioning(
    'Already inside a git worktree — cannot create nested isolation.',
  );
}

— DeepSeek/deepseek-v4-pro via Qwen Code /review

}

let hasUnmerged = true;
try {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] hasUnmergedWorktreeCommits catch block has zero logging. A real git error (corrupt ref store, permission problem, disk full) looks identical to legitimate unmerged commits — the tool returns "Refusing to remove... its branch has commits that no other branch... points at". Operators manually inspect the branch, find no unmerged commits, and are confused. The actual git error that caused the refusal is invisible.

Suggested change
try {
} catch (error) {
debugLogger.warn(
`exit_worktree: hasUnmergedWorktreeCommits failed for ${branch}: ${error}`,
);
}

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Comment thread packages/core/src/config/config.ts Outdated
// Promote to `info` so operators chasing a worktree leak in
// production can grep the logs and see the sweep actually
// ran. The cleanup helper itself uses `debug`-level
// per-entry logs, which are silent at default verbosity.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The startup sweep only logs when removed > 0. When the sweep runs and finds 0 stale worktrees, nothing is logged. Operators grepping for "Stale worktree" or "sweep" can't distinguish between "sweep was skipped entirely (fast-bail)" and "sweep ran but found nothing". Log something unconditionally so the sweep's execution (or non-execution) is always visible in logs.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

The biggest item — #1 — is a self-inflicted regression from round 5:
the new agent- prefix reservation in `validateUserWorktreeSlug`
rejected EVERY slug that `generateAgentWorktreeSlug` produces, since
that helper emits exactly `agent-<7hex>`. Net effect: every
`AgentTool isolation: 'worktree'` invocation failed at validation.
The reservation now allows the canonical pattern through (everything
the helper can produce) and only rejects user-chosen `agent-*` names
that don't match it. Added a round-trip regression guard: 50
`generateAgentWorktreeSlug()` outputs are fed back through
`validateUserWorktreeSlug` and must all pass.

Other critical fixes:

2. `hasWorktreeChanges` (used by agent isolation cleanup) was the
   one remaining caller relying solely on `status.isClean()`.
   Defensive `|| status.conflicted.length > 0` so a future simple-git
   bookkeeping change can't let a mid-merge worktree appear clean and
   get auto-deleted.

3. `readWorktreeSessionMarker` swallowed every I/O error as "marker
   missing", which let a disk error / EACCES silently bypass the
   session-ownership guard. ENOENT is still treated as missing
   (legitimate); every other code now logs.

4. `exit_worktree` `fs.stat` catch was the same shape — every error
   collapsed to "Worktree not found". ENOENT → not found; everything
   else logs and returns a distinct "cannot access" error.

5. `cleanupStaleAgentWorktrees` `fs.stat` catch was again the same.
   ENOENT → silently skip (entry vanished between readdir and stat);
   everything else logs.

Suggestions:

6. Startup sweep fast-bail was running BEFORE resolving the repo
   top-level. For monorepo subdir launches, `targetDir/.qwen/worktrees`
   never exists and the sweep early-returned — permanently a no-op.
   Now resolves the root first, then fast-bails against the resolved
   `<root>/.qwen/worktrees`. Also logs the skip case so operators can
   tell "skipped" from "ran, found nothing".

7. `.qwen-session` marker was visible to `git add -A` inside the
   worktree. Now writes a `.git/info/exclude` rule (resolved via
   `git rev-parse --git-dir`, since worktree `.git` is a file
   pointing at the parent repo's `.git/worktrees/<name>/`).
   Best-effort: failure to write the rule does not abort
   provisioning.

8. Agent isolation now refuses to provision when the parent's cwd is
   already inside a worktree — same regex guard as `enter_worktree`.

9. `exit_worktree`'s wrapper around `hasUnmergedWorktreeCommits` now
   logs at the call site so the chain (caller → reason it asked →
   underlying git error) is complete in operator logs.

10. Sweep now logs unconditionally at `info`. Three distinct messages:
    "skipped (no worktrees dir)", "ran, nothing to remove", "removed N".

Tests:

11. New `execute()` coverage:
    • exit-worktree: session-ownership refusal, keep happy path,
      legacy/no-marker fallthrough with warn log, missing-worktree
      error, unmerged-commits guard with `discard_changes: true`,
      `writeWorktreeSessionMarker` round-trip.
    • enter-worktree: nested-guard rejection, non-git-repo error.
    These spin up real temp git repos (no filesystem mocking) and
    drive the actual tool invocation pipeline.

   Total: 135 tests pass (was 101 → +34).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@LaZzyMan

Copy link
Copy Markdown
Collaborator Author

Pushed `b374187fa`. Five critical + six suggestions, all closed.

Critical

  • pre-release: fix ci #1: agent isolation completely broken by its own validation — your highest-impact catch. The round-5 prefix reservation used startsWith(\agent-`), which rejected EVERY slug \generateAgentWorktreeSlug` produces (`agent-<7hex>`). Net effect: every `isolation: 'worktree'` invocation failed at validation. Fixed by allowing slugs that match `AGENT_WORKTREE_SLUG_PATTERN` and only rejecting user-chosen `agent-*` names that don't. Added a round-trip regression guard (50 generated slugs must round-trip through the validator) so this exact regression cannot recur silently.
  • Where is the config saved? #2: `hasWorktreeChanges` defensive `conflicted` check — even though current simple-git counts conflicted in `isClean()`, OR'd it explicitly so a future bookkeeping change can't silently let mid-merge worktrees get auto-deleted. Same shape as the round-5 fixes for `countWorktreeChanges` and `hasTrackedChanges`.
  • 如何自定义密钥文件 .env可能与其他文件冲突 #3: `readWorktreeSessionMarker` silent on disk errors — ENOENT still returns null silently (legitimate "no marker" path), every other error logs at `warn` so a failing storage layer can't bypass the ownership guard invisibly.
  • Are you interested in AI Terminal? #4: `exit_worktree fs.stat` swallowed everything as "not found" — same fix shape: ENOENT → not found, everything else logs and returns a distinct "cannot access" error with the underlying cause.
  • TypeError in Authentication Selection Interface #5: `cleanupStaleAgentWorktrees fs.stat` silently skipped — same fix shape: ENOENT silent (entry vanished between readdir and stat), everything else logs.

Suggestion

  • OpenAI API Error: 401 Incorecct API Key provided #6: startup sweep fast-bail in wrong place — the round-5 `fs.access` short-circuit ran BEFORE `getRepoTopLevel()`. For monorepo subdir launches the local `.qwen/worktrees` never exists and the sweep early-returned. Fixed by resolving the root first, then fast-bailing against `/.qwen/worktrees`. Operators now get a distinct "skipped (no worktrees dir)" log so this case is observable.
  • API Key是要设成阿里云的API Key吗? #7: `.qwen-session` visible to `git add -A` — `writeWorktreeSessionMarker` now writes a `.git/info/exclude` rule for it. Resolved via `git rev-parse --git-dir` since worktree `.git` is a file pointing at `/.git/worktrees//`. Best-effort: failure to write the rule does not abort provisioning (the marker is still functional, the ownership guard intact).
  • report error when try to auth #8: agent isolation lacked the nested-worktree guard — added the same `/.qwen[\\/]worktrees[\\/]/` regex check as `enter_worktree`, with a `failWorktreeProvisioning` exit so the parent message reflects the failure shape.
  • API Error: Streaming setup timeout after 45s #9: silent `hasUnmergedWorktreeCommits` catch in `exit_worktree` — now logs at the call site so the chain (caller → reason it asked → underlying git error) is intact in operator logs.
  • 这个怎么调用 Qwen3-Coder,配置界面只有openai #10: sweep ran-but-found-nothing case — three distinct `info` logs now: "skipped (no worktrees dir)", "ran, nothing to remove", "removed N". Operators can correlate.

Test coverage (#11)

Real-temp-git-repo `execute()` coverage:

  • exit-worktree (+6): session-ownership refusal across sessions, keep happy path, legacy-no-marker fallthrough with warn log, missing-worktree error, unmerged-commits guard with `discard_changes: true`, marker round-trip
  • enter-worktree (+2): nested-guard rejection, non-git-repo error
  • Plus the round-trip regression guard mentioned above

135 tests pass (was 101 → +34).

Self-review pass applying the round-6 review-triage framework
(filter #5: "If a log only fires on the happy path, it's noise.")
to my own round-6 changes:

- "Stale worktree sweep skipped: <dir> does not exist" — fires on
  every CLI start for ~99% of users who never use worktrees.
- "Stale worktree sweep ran under <root>: nothing to remove" —
  fires on every CLI start for users who have any worktrees but
  no stale ones at the moment.

Both are happy-path noise at `info`. Demoted to `debug` so an
operator can opt in via `--debug` when they want to confirm the
sweep is wired up, but normal output stays clean.

Only the actually-actionable case ("removed N worktrees") stays at
`info` — that's the signal someone chasing a worktree leak would
grep for.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@LaZzyMan

Copy link
Copy Markdown
Collaborator Author

Self-review pass on round 6: pushed `772082df3` demoting two of my own `info` logs to `debug`.

The round-6 fix for #10 (sweep visibility) added `info` logs on every code path so operators could distinguish "didn't run" from "ran, found nothing". On reflection that's noise: those messages fire on every CLI start for ~99% of users and drown the actually-actionable "removed N worktrees" message that someone chasing a leak would grep for.

Demoted both happy-path messages to `debug` (still available with `--debug` for confirming the sweep is wired up). The actionable case stays at `info`. Net behaviour change: removes recurring info noise without losing any signal an operator chasing a real problem would want.

This is a partial walk-back of suggestion #10 from the round-6 review — the underlying observation was valid but the original `info`-everywhere implementation was over-defensive. Flagging explicitly so the change isn't mysterious.

@tanzhenxin tanzhenxin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Six rounds of follow-up commits land sweeping fixes for the prior round's findings — all data-loss-class issues are closed, and the round-by-round commit discipline (each commit naming the findings it addresses) makes verification straightforward. Two items remain worth flagging: one user-visible bug and one correctness gap with a clear failure mode. Several low-severity follow-ups (nested-worktree guard via --git-common-dir, untracked-only sweep tradeoff, agent-<7hex> slug collision, getProjectRoot rebinding scope, session-marker TOCTOU, detached-HEAD baseBranch propagation) are noted but not blocking.

1. exit_worktree action='remove' is still auto-approved in AUTO_EDIT mode (severity: high · confidence: very high)

The default-permission switch to ask only takes effect in DEFAULT mode. In AUTO_EDIT, the confirmation type falls through to the generic info form, which the auto-approval allow-list lets through silently. A model running in AUTO_EDIT can drop a worktree (with deleteBranch: true) without ever prompting. Overriding the confirmation-details factory to return a non-info type — anything in the exec/destructive bucket — closes it.

2. Agent isolation worktrees start from the parent's HEAD, not its working tree (severity: medium · confidence: high)

When isolation provisions a worktree, the new branch is created directly from the base ref. Any tracked or untracked changes already present in the parent checkout don't appear in the subagent's worktree. A common workflow — edit some code, then launch a review or test agent before committing — silently runs the subagent against the prior HEAD and returns results that look authoritative but reflect stale code. The Arena worktree-setup precedent overlays the parent's dirty state via git stash --include-untracked before launching; doing the same here, or refusing isolation when the parent is dirty, would close the gap.

Verdict

COMMENT — issue 1 is the only must-address before merge. Issue 2 is a correctness gap worth fixing soon. The low-severity items can be follow-ups.

@LaZzyMan LaZzyMan requested review from tanzhenxin and wenshao May 14, 2026 03:53
Round-7 review caught two correctness gaps:

1. exit_worktree action='remove' was still auto-approved in AUTO_EDIT
   `getDefaultPermission` returning 'ask' is necessary but not
   sufficient. `permissionFlow.isAutoEditApproved` auto-approves any
   tool whose `confirmationDetails.type` is 'edit' OR 'info', and
   `BaseToolInvocation` returns 'info' by default. So a session in
   AUTO_EDIT could silently destroy a worktree (with branch deletion)
   without a confirmation prompt — the data-loss path the round-1
   `'ask'` switch was meant to close. Now overrides
   `getConfirmationDetails` to return `type: 'exec'` for action=remove,
   which keeps the prompt in AUTO_EDIT. The `keep` action still falls
   through to the base info-type since it is non-destructive.

   Regression-guard test asserts the type is 'exec' (not 'info') for
   remove and that the command field describes both the worktree-remove
   and branch-delete operations.

2. Agent isolation worktrees ran against parent's HEAD, not its
   working tree
   `git worktree add -b <branch> <path> <base>` only checks out the
   base ref's tip — uncommitted edits in the parent's working tree do
   NOT propagate. The "edit code → ask review/test agent before
   committing" workflow silently ran the subagent against the
   pre-edit HEAD and returned results that looked authoritative but
   reflected stale code.

   Reviewer offered two options: overlay parent's dirty state à la
   Arena (~50 LOC, edge cases), or refuse isolation when parent is
   dirty (~10 LOC, clear UX). Chose the latter for Phase B scope —
   simpler, decisive, and matches the design-doc's explicit
   commitment that dirty-state overlay is Arena-specific. Users can
   commit/stash before re-invoking agent isolation; overlay can be a
   follow-up if users complain about the friction.

   Fail-closed on the dirty-check itself (assume dirty rather than
   silently launch on a possibly-stale tree).

   Test exercises both "dirty parent → guard fires" and
   "clean parent → guard passes" against real temp git repos.

139 unit tests pass (was 135, +4 regression guards).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@LaZzyMan

Copy link
Copy Markdown
Collaborator Author

Pushed `c91458792`. Both must-address findings closed; the six low-severity follow-ups triaged below.

Issue 1 (must-address) — fixed

`exit_worktree action='remove'` was indeed silently auto-approved in `AUTO_EDIT`. Verified the path: `permissionFlow.isAutoEditApproved(AUTO_EDIT, 'info')` returns true, and `BaseToolInvocation.getConfirmationDetails` defaults to `type: 'info'`. Now overrides `getConfirmationDetails` for `action='remove'` to return `type: 'exec'` (same bucket as `run_shell_command`), which is NOT in the AUTO_EDIT auto-approval list. `keep` still uses the base info-type since it's non-destructive. Added a regression-guard test asserting `type === 'exec'` for remove.

Issue 2 (worth fixing soon) — fixed by refusing

Picked the `refuse-when-dirty` option you offered. Reasoning: the design doc explicitly tagged the dirty-state overlay as Arena-specific (✅ Arena, not Phase-A/B generic worktrees), and overlay introduces edge cases (stash conflicts, untracked-file copy) that aren't worth chasing for Phase B scope. Refusal closes the silent stale-code hazard with ~10 LOC and gives the user a clear path: commit/stash, then re-invoke the agent. If users complain about the friction, an overlay-style upgrade is a straightforward follow-up. Fail-closed on the dirty-check itself so a flaky `git status` doesn't silently launch on a possibly-stale tree.

Six low-severity follow-ups — triaged

Each evaluated against the same five filters (legitimacy → design intent → user impact → reuse value → defensive-overdo); declined ones get reasoning, not silence.

# Item Verdict Reasoning
a Nested-worktree guard via `--git-common-dir` (not regex) declined-for-now Current path-regex catches the canonical case (`.qwen/worktrees/` segment). `--git-common-dir` would be more robust against pathological setups (custom `worktree.path`, etc.) but those don't occur naturally. Filter 3 fails — no observable user impact at the canonical layout. Tracked as a hardening follow-up.
b Untracked-only sweep tradeoff declined Filter 5 — the `--untracked-files=no` choice is a deliberate documented trade-off (untracked files in a 30-day-old crashed agent worktree are typically build artifacts; the scan is the slowest part of `git status` on large repos). Re-evaluating it is reasonable but doesn't change the answer for the agent-cleanup use case.
c `agent-<7hex>` slug collision declined Filter 1 borderline — 16M-combinations × deletion-on-completion makes practical collision essentially impossible. The createUserWorktree path also refuses pre-existing branches, so a collision (if it ever did happen) would surface as a clear error rather than silent data loss.
d `getProjectRoot` rebinding scope declined-for-now The round-2 override completion shadowed `targetDir`, `cwd` (fields), `getTargetDir`/`getCwd`/`getWorkingDir`/`getProjectRoot` (getters), `fileDiscoveryService` + getter, and `workspaceContext` + getter. If there's a specific Config method I missed that reads `this.targetDir` directly, please point me at it and I'll add it; my grep didn't surface another caller, but I'm willing to be wrong.
e Session-marker TOCTOU declined Filter 1 — the window between marker write and read is sub-millisecond at session start, and exploiting it would require a concurrent attacker session running with the same UUID. Threat model doesn't justify the lock complexity.
f Detached-`HEAD` `baseBranch` propagation declined-for-now `getCurrentBranch()` returns "HEAD" on detached HEAD, which `git worktree add -b X path HEAD` interprets as "branch off the current commit" — that's actually the right behavior for the rare case. The truly broken edge case (worktree at literal "HEAD" branch name) would be caught by git itself. Tracked as edge-case hardening.

Verification

  • 139 unit tests pass (was 135, +4 regression guards for the two issues).
  • `npm run typecheck` clean.
  • `npm run build --workspace=packages/core` clean.

@tanzhenxin tanzhenxin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Targeted re-review against round 7 (c9145879) — the two must-fix items from the prior round are genuinely closed. The AUTO_EDIT bypass on exit_worktree action='remove' no longer fires because the confirmation prompt is now classified as an exec action, which sits outside the AUTO_EDIT allow-list. The agent isolation path now refuses to provision a worktree when the parent checkout has uncommitted edits, so a subagent can't be silently run against stale code.

Verdict

APPROVE — must-fixes from the prior round are closed; remaining items are low-severity follow-ups.

@LaZzyMan LaZzyMan merged commit 609e05b into main May 14, 2026
8 checks passed
// at its definition), so this fallback fires once at most even
// when the success path already ran it.
try {
await cleanupWorktreeIsolation();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Background agent isolation worktree is destroyed before the background agent starts.

When run_in_background: true, execute() returns early with FORK_PLACEHOLDER_RESULT (line ~1951), but the outer finally block at this line unconditionally calls cleanupWorktreeIsolation(). This function nulls worktreeIsolation and — finding no changes in the freshly created worktree — removes the directory and branch.

Meanwhile, the background subagent (bgBody()) has not yet started executing (or has just begun), and finds itself operating without its isolated worktree — it either crashes or pollutes the parent working tree.

Impact: Every agent invocation with isolation='worktree' + run_in_background: true silently loses its sandbox. The background agent operates in the parent's working directory without any isolation.

Suggested change
await cleanupWorktreeIsolation();
} finally {
// Only reap the isolation worktree on the foreground path.
// Background agents manage cleanup inside their own completion
// and error handlers (bgBody).
if (!shouldRunInBackground) {
try {
await cleanupWorktreeIsolation();
} catch {
// Helper logs its own failures.
}
}

— DeepSeek/deepseek-v4-pro via Qwen Code /review

TaimoorSiddiquiOfficial pushed a commit to TaimoorSiddiquiOfficial/HopCode that referenced this pull request May 15, 2026
…e + Agent isolation (QwenLM#4073)

* feat(tools): add generic worktree support (Phase A + B of QwenLM#4056)

Adds first-class git worktree as a general-purpose capability:

Phase A — User-facing tools
- enter_worktree: creates `<projectRoot>/.qwen/worktrees/<slug>` on a
  `worktree-<slug>` branch and returns the absolute path. Slug auto-generated
  when omitted; validated against path traversal and disallowed characters.
- exit_worktree: keeps or removes the worktree (and its branch). Refuses to
  remove a worktree with uncommitted tracked changes or untracked files
  unless `discard_changes: true` is set.

Phase B — Agent isolation
- Agent tool gains an `isolation: 'worktree'` parameter that provisions a
  temporary `agent-<7hex>` worktree, prepends a worktree notice to the task
  prompt, and on completion either removes the worktree (no changes) or
  preserves it and reports its path/branch in the result. Background and
  foreground execution paths both wired up; rejected for fork agents.
- worktreeCleanup.cleanupStaleAgentWorktrees: fail-closed sweep for
  ephemeral `agent-<7hex>` worktrees older than 30 days with no tracked
  changes and no unpushed commits. User-named worktrees are never swept.
- buildWorktreeNotice helper for fork subagents (parity with claude-code).

Arena compatibility
- The existing Arena worktree implementation (GitWorktreeService.setupWorktrees,
  ArenaManager, agents.arena.worktreeBaseDir) is untouched. Arena uses its
  own batch APIs and `~/.qwen/arena` base dir; the new general-purpose APIs
  live alongside under `<projectRoot>/.qwen/worktrees/`.

Subagent safety
- enter_worktree / exit_worktree are added to EXCLUDED_TOOLS_FOR_SUBAGENTS
  so a subagent cannot mutate the parent session's worktree state.

Refs QwenLM#4056

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(worktree): use path.join in expected paths so the test passes on Windows

The Windows CI run reported `enter-worktree.test.ts` failing because the
expected string was hardcoded with `/` while `getUserWorktreesDir()` uses
`path.join`, which returns `\\` on Windows. Build the expected path via
`path.join` so the platform-correct separator is compared.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(enter-worktree): treat empty name as auto-generate

Some models pass `{ "name": "" }` when calling EnterWorktree, because the
schema marks `name` as optional and they emit an empty placeholder. The
previous validation rejected the empty string with "Worktree name must be
a non-empty string", which surprised users running the auto-slug path.

Now both `validateToolParams` and `execute` treat `name: ""` as equivalent
to `name: undefined` and fall back to the auto-generated `{adj}-{noun}-{4hex}`
slug. Explicit invalid slugs (`'../etc'`, `'a/b'`, etc.) are still rejected
as before.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(worktree): address review findings 1-6 from PR QwenLM#4073

Six issues raised on the initial review; each addressed with a verifiable
guarantee.

1. Real isolation for `agent isolation: 'worktree'`
   Before: subagent's Config still resolved `getTargetDir()` to the parent
   project root, so Edit/Write/Read workspace checks and Shell's default cwd
   silently operated on the parent tree. The cleanup helper then saw a
   "clean" worktree and removed it — destroying the evidence.
   After: the worktree is provisioned BEFORE `createApprovalModeOverride`,
   and the resulting agent Config has `getTargetDir`/`getCwd`/`getWorkingDir`
   rebound to the worktree path. Relative paths, unqualified shell
   commands, and glob/grep roots all confine to the worktree.

2. `exit_worktree action='remove'` now prompts in default/auto-edit modes
   Added `getDefaultPermission()` on the invocation: `'ask'` when action is
   `remove`, `'allow'` when `keep`. Brings it in line with edit, write_file,
   and run_shell_command.

3. Force-delete no longer silently destroys unpushed commits
   `removeUserWorktree` now uses `git branch -d` (refuses unmerged) by
   default and surfaces `branchPreserved: true` when git refuses. Added
   `hasUnmergedWorktreeCommits` (checks if branch tip is reachable from any
   other local branch or remote ref). Both the agent isolation cleanup and
   `exit_worktree action='remove'` use this check: if the branch has work
   not covered elsewhere, the worktree+branch are preserved even when
   `discard_changes: true` is set (there is no `discard_commits` flag —
   committed work is rarely what `remove` means to discard).

4. Both new tools are now deferred behind ToolSearch
   `shouldDefer: true` + `searchHint` on both. Verified via openai-logging:
   `enter_worktree` and `exit_worktree` no longer appear in the function-
   declaration list sent on every API request.

5. Stale-worktree cleanup is wired in
   `Config.initialize()` fires `cleanupStaleAgentWorktrees(targetDir)` as a
   non-awaited startup sweep (skipped in bare mode). Picks up orphaned
   `agent-<7hex>` worktrees left by crashed runs.

6. Foreground isolation no longer leaks on uncaught throw
   The foreground try block tracks whether the cleanup helper ran on the
   success path; the finally block invokes it as a fallback when the try
   bailed early. Mirrors the background path's pattern.

Verification:
- Unit tests: 83 passed (16 worktree + 64 existing agent + 3 cleanup) — no
  regressions.
- E2E #1: agent told to write `hello.txt` via RELATIVE path — file landed
  at `.qwen/worktrees/agent-XXXXXXX/hello.txt`, NOT at the parent root.
- E2E #3: created worktree, committed work inside it, called exit_worktree
  with `discard_changes=true` — refused with clear message; worktree and
  branch both preserved.
- E2E #4: openai-logging confirms worktree tools absent from API tool list
  (7 tools sent instead of 9).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(worktree): address review round 2 findings (1 from tanzhenxin, 7+8 from wenshao)

The first round closed the data-loss-class issues. This round addresses
follow-ups from a deeper audit:

1. Stale-worktree sweep was inert on common-case repos
   `cleanupStaleAgentWorktrees` previously ran `git log --branches --not
   --remotes --oneline` from each worktree's directory — that lists
   unpushed commits across EVERY local branch, not just the worktree's
   own branch. On any repo with no remote configured (or with stray
   unpushed branches), the sweep refused to remove every candidate.
   Replaced with `service.hasUnmergedWorktreeCommits(slug)` which scopes
   the check to the worktree branch via `for-each-ref --contains <tip>`.
   Also added the `branchPreserved` warn log requested in M7 and an
   `fs.access` shortcut for the empty-worktrees-dir case (M8).

2. `cleanupWorktreeIsolation` and `worktreeIsolation` were inside the
   inner try (~660 lines from the outer catch). Hoisted both to the top
   of `execute()` so the outer catch can reap or preserve the worktree
   when anything between provisioning and the inner try throws (e.g.
   `createApprovalModeOverride`, agent creation). Closure carries the
   resolved `repoRoot` so cleanup never has to re-resolve.

3. Background error path discarded the cleanup result. Now captures
   `formatWorktreeSuffix(...)` and appends it to the registry's failure
   /cancel message, so users see the preserved path/branch even when
   the agent crashed before reporting.

4. `cleanupWorktreeIsolation` now treats `result.success === false` as
   "worktree still on disk" and surfaces it as preserved instead of
   silently dropping it from the result.

5. Override was incomplete. Several Config methods read `this.targetDir`
   directly (`getProjectRoot`, `getFileService`, etc.) — own-property
   getter overrides did not redirect them. Now also shadows `targetDir`
   and `cwd` as own properties on the agent's Config override, swaps in
   a `FileDiscoveryService` rooted at the worktree, and rebuilds
   `WorkspaceContext` to point at the worktree only. Verified
   end-to-end: shell `pwd > pwd-record.txt` (no directory arg) lands at
   `.qwen/worktrees/agent-<7hex>/pwd-record.txt`, not the parent root.

6. monorepo subdir issue. Both `enter_worktree` and the agent isolation
   path now resolve `git rev-parse --show-toplevel` first and anchor
   `.qwen/worktrees/<slug>` at the repo root. Worktrees created from
   any subdirectory now end up where the startup sweep can find them.

7. Replaced `git worktree add -B` (silent force-reset of pre-existing
   branches) with `git worktree add -b` plus an explicit existence
   check via `git for-each-ref` (NOT `show-ref --quiet`, which
   simple-git swallows). Pre-existing `worktree-<slug>` branches now
   trigger a clear error instead of clobbering committed work.

8. First worktree creation in a repo writes `<projectRoot>/.qwen/.gitignore`
   with `worktrees/` so worktree contents stay out of the parent's
   `git status`, glob/grep results, and bundle tools. Idempotent: never
   overwrites an existing file.

9. Logging across the failure paths (`enter_worktree` errors,
   `agent.ts:failWorktreeProvisioning`, `cleanupWorktreeIsolation`,
   `hasUnmergedWorktreeCommits` swallowed errors,
   `cleanupStaleAgentWorktrees`'s `branchPreserved` race).

10. `exit_worktree` no longer suggests `discard_changes: true` when the
    git status check itself fails — that would be advising the user to
    bypass a safety check whose precondition is unknown. Now points at
    the underlying repo problem.

11. `generateAutoSlug` switched from `Math.random()` (4 hex, weak RNG,
    one-in-65k collision) to `randomBytes` (6 hex, ~16M combinations).
    Two RNG sources in this file collapsed to one.

Pushed back: the TOCTOU swap in `removeUserWorktree` (S6 round 1) is
left as-is — `git branch -d` is the real safety, and reordering does
not eliminate the window. Windows reserved-name validation (M5 round 2)
deferred to a follow-up; the current allowlist already rejects path
separators, `..`, leading dot/dash, and the >64-char case.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(worktree): use randomInt to silence CodeQL biased-modulo finding

CodeQL's `js/biased-cryptographic-random` flagged
`randomBytes(4)[i] % ARRAY.length` in `generateAutoSlug`. The math is
actually exact for the current word-list lengths (256 % 8 == 0), but
the lint rule does not know that — and a future contributor changing
the list to a non-power-of-two length would silently introduce bias.

Switched the index lookups to `crypto.randomInt(0, length)`, which uses
rejection sampling and is uniform by construction. Suffix still uses
`randomBytes(3).toString('hex')` since hex encoding is unbiased.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(worktree): address review round 3 findings 1-6 from PR QwenLM#4073

The previous round added `getRepoTopLevel` for `enter_worktree`'s
provisioning, but missed three sibling call sites that still used the
raw cwd. The double-cleanup race in the foreground path also leaked
stale `[worktree preserved]` suffixes on rejected promises. All six
findings from the deeper audit are addressed:

1. exit_worktree now resolves through `getRepoTopLevel()` before
   building its `GitWorktreeService`, mirroring `enter_worktree`. Without
   this, launching `qwen` from a monorepo subdirectory created the
   worktree under the repo root but exit_worktree looked under the
   subdir's `.qwen/worktrees/` and always returned "Worktree not found".
   Verified end-to-end: enter + exit from `packages/core/` works.

2. agent.ts cleanup helper now nulls `worktreeIsolation` immediately
   after capturing the closure value. The previous structure could
   reach the helper twice — once in the foreground try's success path
   and once in the foreground finally fallback (or once in the inner
   try and once in the outer catch on a thrown rejection). The second
   call would `hasWorktreeChanges()` against a directory the first
   call already removed, fail-closed, and emit a bogus
   `[worktree preserved: <missing path>]` suffix.

3. Config.initialize's startup sweep now resolves `getRepoTopLevel()`
   before invoking `cleanupStaleAgentWorktrees`. Without this, every
   subdir launch scanned a non-existent `<subdir>/.qwen/worktrees/`
   and the 30-day expiry sweep was permanently a no-op.

4. agent.ts's `buildWorktreeNotice` now passes
   `worktreeIsolation.repoRoot` as `parentCwd` instead of
   `this.config.getTargetDir()`. The notice's path-translation
   guidance (≈ "translate paths from <parent> to <worktree>") would
   otherwise misdirect the subagent in a monorepo subdir launch.

5. Removed dead method `GitWorktreeService.listUserWorktrees`. It had
   no callers anywhere in the codebase and used `execSync` in a loop
   (would have blocked the event loop if anyone wired it up).

6. `localBranchExists` no longer swallows git failures silently. The
   defensive `false` default is preserved (so `git worktree add -b`
   itself surfaces the conflict if the check missed an existing
   branch), but the catch now logs via `debugLogger.warn` so disk-full
   / permission / ref-store-corruption cases are visible in debug
   output instead of being invisible.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(worktree): address review round 4 findings (data-loss + visibility)

Seven actionable findings from a deeper audit, all closed:

1. User worktree slugs could collide with ephemeral-agent shape
   `validateUserWorktreeSlug` did not reject names starting with
   `agent-`, so a user-named `agent-1234567` matched the cleanup regex
   `/^agent-[0-9a-f]{7}$/` and would be silently swept after 30 days
   along with whatever work was in it. Now reserved — clear error
   message points users at the cause.

2. Slug producer and consumer were string-coupled across files
   `agent.ts` hardcoded `agent-${hex(7)}` and `worktreeCleanup.ts`
   independently hardcoded `/^agent-[0-9a-f]{7}$/`. Future change to
   hex length on one side would silently break the other. Lifted
   `AGENT_WORKTREE_PREFIX`, `AGENT_WORKTREE_HEX_LENGTH`,
   `AGENT_WORKTREE_SLUG_PATTERN`, and `generateAgentWorktreeSlug()` to
   `gitWorktreeService.ts`; both call sites import them.

3. Startup sweep was invisible at default log level
   Fire-and-forget sweep used `debug` for errors and discarded the
   success count. A leak-chasing operator had no log breadcrumb.
   Errors promoted to `warn`; successful removals (count > 0) logged
   at `info`.

4. `getRepoTopLevel()` silent catch
   Returned `null` on any git failure with no log. Combined with
   `?? cwd` fallback in callers, a flaky git would have made worktree
   creators and the startup sweep disagree silently about which dir to
   use. Now logs the underlying error.

5. `hasTrackedChanges()` silent catch
   Cleanup's fail-closed `return true` had no log. Couldn't tell
   "has real changes — leave alone" from "git index unreadable — repo
   may be corrupt". Now logs.

6. `cleanupWorktreeIsolation` claimed `preservedPath` for a removed dir
   When `removeUserWorktree` returns `{ success: true, branchPreserved:
   true }` it has already deleted the directory and failed only on
   `git branch -d`. The helper still reported the (now non-existent)
   path as preserved. Now returns only `preservedBranch` for that
   case; `formatWorktreeSuffix` emits a distinct message instructing
   recovery via `git worktree add <new-path> <branch>`.

7. `removeUserWorktree` swallowed branch-delete failures
   Both `-d` and `-D` catch blocks were empty. Locked refs, perms,
   disk full all looked identical to "unmerged commits". Both now
   `debugLogger.warn` with the underlying error.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(worktree): self-review pass — reuse, parallelism, dead code

Self-review caught a handful of issues across three categories:

Reuse:
- `pathExists` in the new code now uses the existing `fileExists` from
  `utils/fileUtils.ts` instead of duplicating an `fs.access` wrapper.
- `worktree-` branch prefix was string-literalled in five places. Added
  `WORKTREE_BRANCH_PREFIX` and `worktreeBranchForSlug(slug)` exports in
  `gitWorktreeService.ts`; updated `gitWorktreeService.ts`,
  `worktreeCleanup.ts`, and `exit-worktree.ts` to use them. Future
  prefix changes are a single edit.

Efficiency:
- `Config.initialize` used two `await import(...)` calls inside the
  startup-sweep IIFE, paying that cost on every CLI start. Switched to
  static imports at the top of `config.ts` — the modules are tiny and
  the dynamic indirection bought nothing.
- `cleanupWorktreeIsolation` in `agent.ts` ran `hasWorktreeChanges` and
  `hasUnmergedWorktreeCommits` sequentially. They have no data
  dependency on each other and each spawns its own `git` invocation;
  `Promise.all` halves the cleanup wall-clock on the common path.
  Same fix in `worktreeCleanup.ts`'s per-entry loop.
- `ensureWorktreesGitignored` used `fs.access` then `fs.writeFile`, a
  TOCTOU race when two agent invocations created worktrees concurrently
  (both could pass the `access` check and the second would clobber the
  first's `.gitignore`). Now writes with `flag: 'wx'` and treats
  `EEXIST` as the no-op case — atomic in one syscall.

Quality:
- Dropped the `worktreeCleanupRan` boolean in the foreground execution
  path. `cleanupWorktreeIsolation` already nulls its closure variable
  at the top of every call (see the comment at its definition), so
  re-entries are no-ops. The boolean and its tracking were dead weight
  that obscured the real guard.
- Trimmed the Phase-2 override comment block to drop the WHAT-stating
  enumerations (items 3 and 4 just narrated the lines below) and
  removed a navigation comment about hoisted helpers — the helpers are
  visible at the top of the same method.

84 unit tests pass; typecheck clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(worktree): address review round 5 — design-doc commitments + correctness

Five critical findings + four suggestions, all closed.

Critical:
1. Wrong base branch for agent isolation. `createUserWorktree(slug)` with
   no `baseBranch` arg fell back to `getCurrentBranch()` on the **main**
   working tree, returning `main` regardless of which branch the user
   was actually on. A subagent invoked from `feature-x` would silently
   start from `main` and produce diffs against the wrong baseline.
   `enter_worktree` had the same bug. Both now resolve the parent's
   current branch first and pass it explicitly. Verified end-to-end:
   `git checkout feature-x` → `enter_worktree` → worktree HEAD includes
   the feature-x commit.

2. `countWorktreeChanges` (used by `exit_worktree`'s dirty-state guard)
   missed `status.conflicted[]`. In simple-git that array is mutually
   exclusive with the staged/modified/etc. arrays, so a worktree
   mid-merge with only conflicts looked `{tracked: 0, untracked: 0}`
   to the guard and `action='remove'` would proceed without
   `discard_changes: true`. Added `+ status.conflicted.length`.

3. `exit_worktree` had no session-ownership check, contradicting the
   design doc's "only operates on worktrees created by THIS session".
   In yolo mode a prompt injection could enumerate `.qwen/worktrees/`
   and pass any name to drop another session's work. Now:
   `enter_worktree` and agent isolation write a `.qwen-session`
   marker into the worktree at provisioning time; `exit_worktree
   action='remove'` reads it and refuses if it does not match the
   current `Config.getSessionId()`. Worktrees from before this guard
   (no marker file) are treated as "owner unknown" — allowed with a
   warn log so the change is observable.

4. `enter_worktree` did not refuse nested invocations from inside an
   existing worktree, contradicting the design doc. Now rejects any
   cwd containing `.qwen/worktrees/` as a path component, with a
   clear "Already inside a git worktree…" message. Verified: enter
   from inside a worktree returns is_error with that text.

6. `hasTrackedChanges` (cleanup sweep) had the same `conflicted[]`
   gap. Rewrote to use raw `git status --porcelain --untracked-files=no`
   which lists every tracked change including `UU` conflict markers
   in a single git call and explicitly skips the untracked walk
   (the prior comment claimed to skip it, but `status()` always
   does the scan).

Suggestion:
7. `buildWorktreeNotice` now receives the parent agent's actual
   `getTargetDir()` again (was switched to `repoRoot` in round 3 on
   a different reviewer's suggestion; round-5 caught that the model's
   inherited paths reference the parent's cwd, not necessarily the
   repo root, so the prior behaviour was correct).

8. Startup sweep now does `fs.access(<targetDir>/.qwen/worktrees)`
   *before* importing GitWorktreeService and spawning `git
   rev-parse --show-toplevel`. The git probe is reserved for users
   who actually have a worktrees directory locally — 99% of users
   pay only one syscall on startup.

9. Tests:
   - New `exit-worktree.test.ts` covers metadata, validation,
     `getDefaultPermission` (ask vs allow), and getDescription.
   - `agent.test.ts` adds three `validateToolParams` cases for the
     `isolation` parameter (accepted with subagent_type, rejected
     without, rejected for non-"worktree" values).
   - `enter-worktree.test.ts` adds round-trip tests for
     `writeWorktreeSessionMarker` / `readWorktreeSessionMarker` plus
     a `worktreeBranchForSlug` sanity check.
   - Total: 101 tests pass (was 86 → +15).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(test): drop unused @ts-expect-error in exit-worktree.test.ts

Empty string `''` is a valid `string` type, so the @ts-expect-error
directive on `validateToolParams({ name: '', action: 'keep' })` did
nothing — TypeScript correctly accepted the line, and `tsc --build`
in CI reported TS2578 ("Unused '@ts-expect-error' directive"). The
runtime assertion already covers the case; the directive was leftover
from an earlier draft.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(test): use importActual in ArenaManager mock to preserve new exports

The Arena test mocks `gitWorktreeService.js` with a factory that
returns only `{ GitWorktreeService }`. PR QwenLM#4073 added several other
exports to that module (`AGENT_WORKTREE_SLUG_PATTERN`,
`WORKTREE_BRANCH_PREFIX`, `worktreeBranchForSlug`,
`generateAgentWorktreeSlug`, `writeWorktreeSessionMarker`,
`readWorktreeSessionMarker`, `WORKTREE_SESSION_FILE`).

Other modules in the dep graph reach the mocked surface — most
notably `worktreeCleanup.ts` imports `AGENT_WORKTREE_SLUG_PATTERN`
and `worktreeBranchForSlug`, and now reaches the mock via the static
`config.ts` → `worktreeCleanup.ts` import chain added in the
self-review pass. The Arena test failed at module-load with:

  Caused by: Error: [vitest] No "AGENT_WORKTREE_SLUG_PATTERN" export
  is defined on the "../../services/gitWorktreeService.js" mock. Did
  you forget to return it from "vi.mock"?

Use `importOriginal` to capture every real export, spread it into
the return object, and only replace `GitWorktreeService` (the class
the test actually needs to mock). The class-level mock keeps its
existing static-method shims.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(worktree): address review round 6 (5 critical + 6 suggestions)

The biggest item — #1 — is a self-inflicted regression from round 5:
the new agent- prefix reservation in `validateUserWorktreeSlug`
rejected EVERY slug that `generateAgentWorktreeSlug` produces, since
that helper emits exactly `agent-<7hex>`. Net effect: every
`AgentTool isolation: 'worktree'` invocation failed at validation.
The reservation now allows the canonical pattern through (everything
the helper can produce) and only rejects user-chosen `agent-*` names
that don't match it. Added a round-trip regression guard: 50
`generateAgentWorktreeSlug()` outputs are fed back through
`validateUserWorktreeSlug` and must all pass.

Other critical fixes:

2. `hasWorktreeChanges` (used by agent isolation cleanup) was the
   one remaining caller relying solely on `status.isClean()`.
   Defensive `|| status.conflicted.length > 0` so a future simple-git
   bookkeeping change can't let a mid-merge worktree appear clean and
   get auto-deleted.

3. `readWorktreeSessionMarker` swallowed every I/O error as "marker
   missing", which let a disk error / EACCES silently bypass the
   session-ownership guard. ENOENT is still treated as missing
   (legitimate); every other code now logs.

4. `exit_worktree` `fs.stat` catch was the same shape — every error
   collapsed to "Worktree not found". ENOENT → not found; everything
   else logs and returns a distinct "cannot access" error.

5. `cleanupStaleAgentWorktrees` `fs.stat` catch was again the same.
   ENOENT → silently skip (entry vanished between readdir and stat);
   everything else logs.

Suggestions:

6. Startup sweep fast-bail was running BEFORE resolving the repo
   top-level. For monorepo subdir launches, `targetDir/.qwen/worktrees`
   never exists and the sweep early-returned — permanently a no-op.
   Now resolves the root first, then fast-bails against the resolved
   `<root>/.qwen/worktrees`. Also logs the skip case so operators can
   tell "skipped" from "ran, found nothing".

7. `.qwen-session` marker was visible to `git add -A` inside the
   worktree. Now writes a `.git/info/exclude` rule (resolved via
   `git rev-parse --git-dir`, since worktree `.git` is a file
   pointing at the parent repo's `.git/worktrees/<name>/`).
   Best-effort: failure to write the rule does not abort
   provisioning.

8. Agent isolation now refuses to provision when the parent's cwd is
   already inside a worktree — same regex guard as `enter_worktree`.

9. `exit_worktree`'s wrapper around `hasUnmergedWorktreeCommits` now
   logs at the call site so the chain (caller → reason it asked →
   underlying git error) is complete in operator logs.

10. Sweep now logs unconditionally at `info`. Three distinct messages:
    "skipped (no worktrees dir)", "ran, nothing to remove", "removed N".

Tests:

11. New `execute()` coverage:
    • exit-worktree: session-ownership refusal, keep happy path,
      legacy/no-marker fallthrough with warn log, missing-worktree
      error, unmerged-commits guard with `discard_changes: true`,
      `writeWorktreeSessionMarker` round-trip.
    • enter-worktree: nested-guard rejection, non-git-repo error.
    These spin up real temp git repos (no filesystem mocking) and
    drive the actual tool invocation pipeline.

   Total: 135 tests pass (was 101 → +34).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(worktree): demote noise startup-sweep logs to debug

Self-review pass applying the round-6 review-triage framework
(filter #5: "If a log only fires on the happy path, it's noise.")
to my own round-6 changes:

- "Stale worktree sweep skipped: <dir> does not exist" — fires on
  every CLI start for ~99% of users who never use worktrees.
- "Stale worktree sweep ran under <root>: nothing to remove" —
  fires on every CLI start for users who have any worktrees but
  no stale ones at the moment.

Both are happy-path noise at `info`. Demoted to `debug` so an
operator can opt in via `--debug` when they want to confirm the
sweep is wired up, but normal output stays clean.

Only the actually-actionable case ("removed N worktrees") stays at
`info` — that's the signal someone chasing a worktree leak would
grep for.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(worktree): close AUTO_EDIT bypass + parent-dirty stale-code hazard

Round-7 review caught two correctness gaps:

1. exit_worktree action='remove' was still auto-approved in AUTO_EDIT
   `getDefaultPermission` returning 'ask' is necessary but not
   sufficient. `permissionFlow.isAutoEditApproved` auto-approves any
   tool whose `confirmationDetails.type` is 'edit' OR 'info', and
   `BaseToolInvocation` returns 'info' by default. So a session in
   AUTO_EDIT could silently destroy a worktree (with branch deletion)
   without a confirmation prompt — the data-loss path the round-1
   `'ask'` switch was meant to close. Now overrides
   `getConfirmationDetails` to return `type: 'exec'` for action=remove,
   which keeps the prompt in AUTO_EDIT. The `keep` action still falls
   through to the base info-type since it is non-destructive.

   Regression-guard test asserts the type is 'exec' (not 'info') for
   remove and that the command field describes both the worktree-remove
   and branch-delete operations.

2. Agent isolation worktrees ran against parent's HEAD, not its
   working tree
   `git worktree add -b <branch> <path> <base>` only checks out the
   base ref's tip — uncommitted edits in the parent's working tree do
   NOT propagate. The "edit code → ask review/test agent before
   committing" workflow silently ran the subagent against the
   pre-edit HEAD and returned results that looked authoritative but
   reflected stale code.

   Reviewer offered two options: overlay parent's dirty state à la
   Arena (~50 LOC, edge cases), or refuse isolation when parent is
   dirty (~10 LOC, clear UX). Chose the latter for Phase B scope —
   simpler, decisive, and matches the design-doc's explicit
   commitment that dirty-state overlay is Arena-specific. Users can
   commit/stash before re-invoking agent isolation; overlay can be a
   follow-up if users complain about the friction.

   Fail-closed on the dirty-check itself (assume dirty rather than
   silently launch on a possibly-stale tree).

   Test exercises both "dirty parent → guard fires" and
   "clean parent → guard passes" against real temp git repos.

139 unit tests pass (was 135, +4 regression guards).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
(cherry picked from commit 609e05b)
LaZzyMan added a commit that referenced this pull request May 21, 2026
…s + PR refs

Three cross-cutting capabilities on top of the Phase A-C worktree
foundation (PRs #4073, #4174).

D-1: --worktree [name] CLI flag creates a worktree (or re-attaches to
one that already exists) before any model turn runs. Supports bare,
plain-slug, `=`, and PR-reference forms; --worktree + --acp rejected
with a clear error; --worktree + --resume overrides the resumed
session's saved sidecar and emits a stderr line.

D-2: worktree.symlinkDirectories: string[] settings key opts into
symlinking main-repo directories (e.g. node_modules) into every
newly-created general-purpose worktree. Applies to all three creation
paths: --worktree flag, EnterWorktreeTool, AgentTool isolation. Path
traversal, absolute paths, and existing destinations all guarded;
missing source dirs and EEXIST silently skipped (fail-open).

D-3: --worktree=#<N> / --worktree <github-url> resolves a PR number,
runs `git fetch origin pull/<N>/head` (30s timeout, no `gh` CLI
dependency, LANG=C for stable error-taxonomy matching), and creates
the worktree off FETCH_HEAD. URL regex tolerates /files, /commits,
/checks sub-paths so users can paste any GitHub PR URL.

Phase 6 verification fixes also included:
- Re-attach to an existing worktree instead of failing with "Worktree
  already exists" — the common `qwen --resume <sid> --worktree foo`
  workflow now succeeds. The session ownership marker is preserved on
  re-attach so cross-session exit_worktree action="remove" still fails
  for non-owners.
- Normalize path-taking argv fields (mcpConfig, jsonSchema @<path>,
  openaiLoggingDir, jsonFile, inputFile, telemetryOutfile,
  includeDirectories) to absolute paths against the launch cwd BEFORE
  the worktree chdir. Otherwise downstream fs.existsSync('./mcp.json')
  resolves into the worktree, where the file doesn't exist.

Phase 7 code-review fixes:
- buildStartupWorktreeNotice differentiates "Active worktree" (fresh
  create) from "Re-attached to worktree" (re-attach path).
- Notice survives sidecar persist failure: set before the try block,
  refreshed inside with override addendum if persist succeeded.
- getRegisteredWorktreeBranch verifies the candidate path's git
  common-dir matches the source repo's — rejects sibling `git init`
  directories that happen to be on a worktree-<slug> branch.

Three-mode parity for the startup notice: TUI consumes via
AppContainer effect, headless prepends a <system-reminder> + emits a
worktree_started JSON event. ACP path is mutually exclusive with
--worktree (ACP hosts supply per-session cwd separately).

Tests (66 + 15 new):
- 15 cli/src/startup/worktreeStartup.test.ts (slug forms, PR fetch
  against local fake remote, re-attach happy + wrong-branch guard)
- 8 core/src/services/gitWorktreeService.test.ts (parsePRReference:
  #N, URLs, malformed, traversal, leading zeros, non-string)
- 10 core/src/services/gitWorktreeService.symlinks.integ.test.ts
  (symlink loop + fetchPullRequestRef error taxonomy)

Known limitations (documented in docs/users/features/worktree.md):
- Cross-slug --resume <sid> --worktree <different-new-slug> is
  unsupported by design (sessions are bound to projectHash(cwd));
  future Config refactor anchoring storage at repo root would lift this.
- Mid-session enter_worktree still does NOT switch cwd/targetDir
  (Phase A's simplification); only the startup --worktree flag does.
- yargs ambiguity: `qwen --worktree "say hi"` consumes the prompt as
  the slug. Quick Start shows the `=` form and reordering workarounds.

Docs:
- docs/users/features/worktree.md (new): Quick Start with --worktree
  flag, CLI Reference table for all four input forms + error codes,
  settings table, Limitations.
- docs/design/worktree.md: Phase D section expanded into D-1/D-2/D-3
  with open questions resolved; capability table updated.
- docs/e2e-tests/worktree-phase-d.md (new): full E2E plan with Phase 4
  dry-run baseline + Phase 6 post-impl reproduction tables.

Refs #4056
LaZzyMan added a commit that referenced this pull request May 27, 2026
…s + PR refs (#4381)

* feat(worktree): Phase D — startup --worktree flag + symlinkDirectories + PR refs

Three cross-cutting capabilities on top of the Phase A-C worktree
foundation (PRs #4073, #4174).

D-1: --worktree [name] CLI flag creates a worktree (or re-attaches to
one that already exists) before any model turn runs. Supports bare,
plain-slug, `=`, and PR-reference forms; --worktree + --acp rejected
with a clear error; --worktree + --resume overrides the resumed
session's saved sidecar and emits a stderr line.

D-2: worktree.symlinkDirectories: string[] settings key opts into
symlinking main-repo directories (e.g. node_modules) into every
newly-created general-purpose worktree. Applies to all three creation
paths: --worktree flag, EnterWorktreeTool, AgentTool isolation. Path
traversal, absolute paths, and existing destinations all guarded;
missing source dirs and EEXIST silently skipped (fail-open).

D-3: --worktree=#<N> / --worktree <github-url> resolves a PR number,
runs `git fetch origin pull/<N>/head` (30s timeout, no `gh` CLI
dependency, LANG=C for stable error-taxonomy matching), and creates
the worktree off FETCH_HEAD. URL regex tolerates /files, /commits,
/checks sub-paths so users can paste any GitHub PR URL.

Phase 6 verification fixes also included:
- Re-attach to an existing worktree instead of failing with "Worktree
  already exists" — the common `qwen --resume <sid> --worktree foo`
  workflow now succeeds. The session ownership marker is preserved on
  re-attach so cross-session exit_worktree action="remove" still fails
  for non-owners.
- Normalize path-taking argv fields (mcpConfig, jsonSchema @<path>,
  openaiLoggingDir, jsonFile, inputFile, telemetryOutfile,
  includeDirectories) to absolute paths against the launch cwd BEFORE
  the worktree chdir. Otherwise downstream fs.existsSync('./mcp.json')
  resolves into the worktree, where the file doesn't exist.

Phase 7 code-review fixes:
- buildStartupWorktreeNotice differentiates "Active worktree" (fresh
  create) from "Re-attached to worktree" (re-attach path).
- Notice survives sidecar persist failure: set before the try block,
  refreshed inside with override addendum if persist succeeded.
- getRegisteredWorktreeBranch verifies the candidate path's git
  common-dir matches the source repo's — rejects sibling `git init`
  directories that happen to be on a worktree-<slug> branch.

Three-mode parity for the startup notice: TUI consumes via
AppContainer effect, headless prepends a <system-reminder> + emits a
worktree_started JSON event. ACP path is mutually exclusive with
--worktree (ACP hosts supply per-session cwd separately).

Tests (66 + 15 new):
- 15 cli/src/startup/worktreeStartup.test.ts (slug forms, PR fetch
  against local fake remote, re-attach happy + wrong-branch guard)
- 8 core/src/services/gitWorktreeService.test.ts (parsePRReference:
  #N, URLs, malformed, traversal, leading zeros, non-string)
- 10 core/src/services/gitWorktreeService.symlinks.integ.test.ts
  (symlink loop + fetchPullRequestRef error taxonomy)

Known limitations (documented in docs/users/features/worktree.md):
- Cross-slug --resume <sid> --worktree <different-new-slug> is
  unsupported by design (sessions are bound to projectHash(cwd));
  future Config refactor anchoring storage at repo root would lift this.
- Mid-session enter_worktree still does NOT switch cwd/targetDir
  (Phase A's simplification); only the startup --worktree flag does.
- yargs ambiguity: `qwen --worktree "say hi"` consumes the prompt as
  the slug. Quick Start shows the `=` form and reordering workarounds.

Docs:
- docs/users/features/worktree.md (new): Quick Start with --worktree
  flag, CLI Reference table for all four input forms + error codes,
  settings table, Limitations.
- docs/design/worktree.md: Phase D section expanded into D-1/D-2/D-3
  with open questions resolved; capability table updated.
- docs/e2e-tests/worktree-phase-d.md (new): full E2E plan with Phase 4
  dry-run baseline + Phase 6 post-impl reproduction tables.

Refs #4056

* refactor(worktree): apply self-review feedback on Phase D

Self-review pass over the Phase D commit (2636f59) catching one real
typecheck regression plus a batch of small quality + efficiency
improvements. No user-visible behavior change beyond fixing the build.

Build fix:
- worktreeStartup.ts imports — pre-commit prettier had reorganized
  `writeWorktreeSession` and `readWorktreeSession` under an
  `import type { ... }` block, erasing them at compile time
  (verbatimModuleSyntax). `tsc --noEmit` was failing with TS1361.
  Bundle path still worked (esbuild is lenient) so this only surfaced
  when running typecheck.

Startup-path efficiency (~10-25 ms saved per --worktree invocation on
macOS; more on Windows):
- Drop redundant `isGitRepository()` probe — `getRepoTopLevel()`
  returns null on non-git paths and covers both gates in one
  subprocess.
- Run `getCurrentBranch()` + `getCurrentCommitHash()` in parallel via
  Promise.all (independent calls).
- Combine the two `git rev-parse` probes inside
  `getRegisteredWorktreeBranch` into a single multi-arg call, and run
  it in parallel with the source-repo common-dir lookup. Saves one
  fork+exec on the re-attach path.

Quality:
- Extract `withReminder()` local helper in nonInteractiveCli.ts so the
  startup-notice and resume-restore branches share the system-reminder
  wrapping.
- Log `readWorktreeSession` failures in `persistStartupWorktreeSidecar`
  with the sidecar path so operators can recover the previous slug
  from a backup. Silent swallow was making "where did my worktree
  binding go?" undebuggable.
- Drop the dead `Config.getWorktreeSettings()` accessor (only
  `getWorktreeSymlinkDirectories()` has callers); keep the underlying
  `WorktreeSettings` interface for future fields.
- Document the `pendingStartupWorktreeNotice` invariant: at most one
  consumer per process; ACP path is gated out earlier so only TUI XOR
  headless reads it.
- Add a maintainer note in the gemini.tsx path-normalization block:
  the argv path-field allowlist is hand-maintained, register new
  path-bearing flags there or `--worktree` silently breaks for them.
- Drop `Phase 6 fix (G1)/(G2)` parenthetical labels from inline
  comments — internal review-cycle identifiers that decay to noise
  post-merge. Substantive prose retained.

Tests: cli 15/15 (unchanged) + core 66/66 (unchanged); bundle smoke
verified fresh / re-attach / invalid slug / non-git cases.

Findings deliberately left for follow-up:
- Larger refactor extracting a shared `provisionUserWorktree` helper
  for the EnterWorktreeTool / startup overlap (~80% duplicate).
- Splitting the re-attach branch out of `setupStartupWorktree` into
  its own function.
- `isPathWithinRoot` / `isInsideManagedWorktree` shared utils.
- `symlinkConfiguredDirectories` loop concurrency (saves 5-15 ms on a
  cold path that runs only when symlinkDirectories is configured).

* docs(worktree): refresh stale docstring in worktreeStartup

Top-of-file docstring still said `{adj}-{noun}-{4hex}` (actual format
is 6 hex chars) and described the PR form as "detected and rejected
with a clear 'coming in D-3' message" — but D-3 shipped in the same
PR. Tighten to reflect what the code actually does.

* fix(worktree): address findings from dual-reviewer self-check

Two real bugs surfaced by an independent dual-reviewer pass (Claude +
Codex) on the Phase D commits. Both correctness-affecting; both
escaped the earlier internal reviews.

P0 — re-attach captured the wrong baseline for the exit dialog
(Codex):
  setupStartupWorktree captured `originalHeadCommit` from the launch
  cwd (main checkout) before any chdir. On the re-attach path the
  WorktreeExitDialog later runs `git rev-list <originalHeadCommit>..HEAD`
  inside the worktree to count "new commits this session". With the
  main-checkout baseline this counted every commit ever made in the
  kept worktree as new work from the current session — misleading the
  keep/remove prompt. Re-capture HEAD from inside the worktree after
  chdir so the count means what the dialog text says it means.

P0 — getRegisteredWorktreeBranch mis-identified plain directories as
registered worktrees (Claude):
  A plain directory at `<repo>/.qwen/worktrees/<slug>/` (e.g. a stale
  artifact from a previous tool) had no `.git` file of its own, so
  `git rev-parse --git-common-dir` walked up to the outer repo and
  returned the outer common-dir — matching the source repo's
  common-dir check and impersonating a registered worktree. If the
  outer repo happened to be on `worktree-<slug>`, setupStartupWorktree
  would silently chdir into the plain directory and treat it as
  attached; subsequent `exit_worktree action="remove"` would then
  delete a directory that was never registered.
  Fix: also probe `--show-toplevel` and require it to equal the
  candidate path (canonicalised via `realpath` so macOS /var → /private/var
  doesn't break the equality check). A plain dir under the main repo
  gets the outer repo's toplevel and is correctly rejected.

Smaller polish from the same review:
- Normalize the literal string `'HEAD'` returned by `getCurrentBranch`
  on detached HEAD to `undefined`, so the `baseRef` handed to
  `git worktree add -b … HEAD` does not implicitly anchor against
  the loose commit when the launch cwd is detached.
- `symlinkConfiguredDirectories`: blocklist `.git` (any nested
  ancestor) and `.qwen/worktrees` (any nested ancestor). Linking
  `.git` would silently break commits inside the worktree; linking
  `.qwen/worktrees` would create a worktrees-inside-worktrees loop
  that confuses the startup sweep.
- `WorktreeSettings.symlinkDirectories` typed `readonly string[]` to
  match the `createUserWorktree(options.symlinkDirectories)` contract
  and the immutable-config convention elsewhere. `Config.getWorktreeSymlinkDirectories()`
  return type updated to match.

Docs:
- design/worktree.md precedence table rewritten. The previous
  `--worktree` 赢 row was unreachable in practice (sessions are bound
  to `projectHash(cwd)`, and the chdir happens before session lookup).
  New table reflects what actually happens for each combination of
  `--resume` × `--worktree`, including the documented
  cross-projectHash limitation. The `persistStartupWorktreeSidecar`
  override branch is now annotated as dead-on-the-current-architecture
  but kept so a future Config refactor (anchor storage at repo root)
  picks it up for free.

Tests: cli 15/15 + core 66/66 unchanged. Bundle smoke confirms both
P0 fixes end-to-end (re-attach captures worktree HEAD = run-1 tip,
plain-dir attempt errors out without clobbering existing content).

* refactor(worktree): consolidate probe + name detached-HEAD sentinel

Second /simplify pass on the dual-reviewer fixes. Three convergent
findings; net effect is one fewer subprocess on the re-attach path
and clearer intent on string handling / blocklist guards.

Efficiency + quality:
- Fold the worktree HEAD SHA into `getRegisteredWorktreeBranch`'s
  combined rev-parse. The probe already requests common-dir,
  toplevel, and abbrev-ref HEAD in a single subprocess; adding a
  leading `HEAD` positional (which must come BEFORE `--abbrev-ref` so
  the flag doesn't apply to it) returns the SHA on its own line.
  Return type widened to `{ branch, headCommit } | null`. Removes
  the second `GitWorktreeService` instantiation and `getCurrentCommitHash`
  call that `setupStartupWorktree`'s re-attach branch used to do.

Quality:
- Hoist `'HEAD'` to a module-level `DETACHED_HEAD` constant in
  `worktreeStartup.ts`. Three uses, two meanings (input filter when
  normalizing `getCurrentBranch` output, fallback metadata for the
  sidecar's `originalBranch` field on detached state). Naming the
  sentinel makes intent self-documenting and pre-empts the "why is
  the value we just stripped re-appearing as a fallback?" reader stall
  flagged by the round-3 quality review.

Reuse + quality:
- `symlinkConfiguredDirectories`: replace two hand-rolled containment
  checks (`startsWith(prefix + sep)` for `.qwen/worktrees`; `path.relative(...).split(sep)[0]`
  for `.git`) with `isWithinRoot` from `utils/fileUtils.ts`, which is
  already imported in this file. Replace the hardcoded
  `path.join(repoRootAbs, '.qwen', 'worktrees')` with `this.getUserWorktreesDir()`
  so the layout lives in one place (the exported `WORKTREES_DIR`
  constant). Split the misleading `sourceAbs === repoRootAbs` clause
  out of the `.git` branch into its own dedicated "empty / repo-root
  path" rejection with a clearer warn message.

Tests: cli 15/15 + core 66/66 unchanged. Bundle smoke verified the
folded probe still captures the worktree's HEAD on re-attach (not
the launch-cwd HEAD).

Skipped from this review pass:
- Moving `'HEAD'` normalization into `GitWorktreeService.getCurrentBranch()`
  itself — would ripple through `enter-worktree.ts` and `agent.ts`
  callers that hand the result verbatim to `git worktree add -b ...`.
  Out of scope for a polish pass; the local const is enough.

* fix(worktree): broaden symlink blocklist from .qwen/worktrees to all of .qwen

Caught by a second pr-tracker dual-reviewer pass (Codex). The previous
guard at `symlinkConfiguredDirectories` only refused paths inside
`<repoRoot>/.qwen/worktrees/` — `.qwen` itself (the parent) sailed
through because `isWithinRoot` is a strict descendant check. A user
setting `symlinkDirectories: ['.qwen']` would therefore symlink the
entire CLI metadata tree into the new worktree, recursively pulling
in `.qwen/worktrees` and recreating the loop the guard was meant to
prevent. Other `.qwen/*` subtrees (`projects`, `tmp`, …) are CLI
state with no legitimate cross-worktree sharing use case either.

Fix: broaden the guard to reject the whole `<repoRoot>/.qwen` tree.
Both `.qwen` itself and any descendant fail closed.

Also synced the user-facing settings schema description (the in-IDE
help text and the published JSON schema) so it mentions the `.git`
and `.qwen` rejection rules. The `WorktreeSettings` interface JSDoc
already mentioned them; the schema description had not been updated.

Tests: cli 15/15 + core 66/66 unchanged. Smoke confirms `--worktree foo`
with `symlinkDirectories: ['.qwen']` configured leaves the worktree
free of any `.qwen` symlink (only the legitimate per-worktree
`.qwen-session` marker file appears).

* fix(worktree): guard fetchPullRequestRef against CodeQL command-injection alert

CodeQL flagged a "Second order command injection" finding (rule 235) on
the `git fetch origin pull/<N>/head` call in `fetchPullRequestRef`. The
taint analyzer doesn't see the type-narrowing at the function entry
(`Number.isSafeInteger(prNumber) && prNumber > 0 && prNumber <= 1e9`),
so it considers `prNumber` library input that could in principle reach
a `--upload-pack=…`-shaped flag and thereby execute an arbitrary
program. In practice the entry guard already prevents that, but the
alert blocks the CodeQL CI check.

Add `--end-of-options` between `origin` and the refspec — git's
canonical "stop parsing flags" marker (git ≥ 2.24). Tells git
definitively that every subsequent argv element is a positional, not
a flag, which (a) satisfies the analyzer, (b) adds defense-in-depth
against a future regression that might relax the entry guard, and
(c) has zero behavior change for any well-formed PR number.

Verified locally: `git fetch --end-of-options origin pull/<N>/head`
against a local bare-remote with a seeded `refs/pull/42/head` still
fetches the ref correctly; the `--worktree=#42` smoke test reads back
the PR content from the materialized worktree.

Tests: cli 15/15 + core 66/66 unchanged.

* fix(worktree): lexical sanitizer for CodeQL + missing test mock entry

Two fixes from the third CI round on PR #4381:

1. CodeQL re-fires (round 2 of the same finding).

`--end-of-options` is a git-runtime defense, not a lexical sanitizer
that CodeQL's `js/second-order-command-line-injection` taint tracker
recognises. The alert re-fired against the same call after the
previous fix.

Switch to a CodeQL-recognised sanitizer: validate the numeric
component against `/^[1-9][0-9]*$/` immediately at the sink. The
regex digit-only check is one of the documented sanitizer patterns
the rule looks for, and proves at the analyzer level that the
resulting argv element cannot resemble a flag (`--foo`). The entry
guard at the top of the function still establishes the same fact
at runtime; this layer makes the proof visible to static analysis.
Keep `--end-of-options` as a runtime fallback against any future
regression that loosens the entry guard.

2. `nonInteractiveCli.test.ts` mock was missing the new
   `consumePendingStartupWorktreeNotice` Config method.

Phase D-1 added the method on `Config` and `nonInteractiveCli`
calls it on every prompt to pick up the one-shot startup-worktree
notice. The test file's `mockConfig` literal was not updated, so
all 19 `runNonInteractive` tests threw
`TypeError: config.consumePendingStartupWorktreeNotice is not a
function` on Ubuntu / macOS CI.

Add a stub returning `null` so the helper short-circuits, matching
the equivalent Phase C stub for `getResumedSessionData`.

Local: cli (worktreeStartup + nonInteractiveCli) 60 passed + 1
skipped; core (gitWorktreeService + symlinks + hooks +
enter-worktree) 66 passed.

* test(worktree): mock getWorktreeSymlinkDirectories in three more test files

Round 4 of the same Phase D-2 mock-drift class. CI surfaced 9 test
failures across three files whose `Config` mocks construct
`EnterWorktreeTool` for setup but lack the new
`getWorktreeSymlinkDirectories` method `createUserWorktree` now
calls:

- enter-worktree.session.integ.test.ts (2 tests)
- exit-worktree.session.integ.test.ts (3 tests) — provisions
  worktrees via EnterWorktreeTool before exercising exit paths
- exit-worktree.test.ts (4 tests) — same provisioning pattern via
  `provisionWorktree()` and the `makeMockConfig` helper

Add a `getWorktreeSymlinkDirectories: () => []` stub to each so
the symlink loop is a no-op in tests.

`enter-worktree.test.ts` and `agent/agent.test.ts` intentionally
skipped — they mock `GitWorktreeService.createUserWorktree` outright,
so the method call never fires in their code paths. Adding the stub
there would be defensive speculation. If a future test exercises
the real path, it'll surface there too and we'll add it then.

Local: core tools tests now 123 passed (was 9 failed / 114 passed
on CI run 26213122427 against commit 000c9f6).

* fix(worktree): normalize repoRoot path separators + disable autocrlf in tests

Round 5 of CI: Windows-only test failures on the latest HEAD. Two
unrelated Windows-specific bugs, both in / around worktreeStartup.

1. `setupStartupWorktree` stored the raw `getRepoTopLevel()` output
   in `context.repoRoot`. git always emits POSIX paths via
   `--show-toplevel` (`C:/Users/...`), so on Windows the value was
   forward-slash where `fs.realpath` and `path.join` produce
   backslash. The sidecar's `originalCwd` field got the
   inconsistent format and a downstream `expect(...).toBe(tempRepo)`
   in the round-trip test compared `C:/Users/.../tmp/...` against
   `C:\Users\.../tmp/...`.

   Wrap the value in `path.resolve()` to normalize to the
   platform-native separator before storing. Downstream consumers
   (`path.join(session.originalCwd, '.qwen', 'worktrees')` in
   `restoreWorktreeContext`, `new GitWorktreeService(originalCwd)`
   in `AppContainer`) already handle either format, so no migration
   concern for older sidecars.

2. `makeTempRepo` in worktreeStartup.test.ts didn't configure
   `core.autocrlf=false`. On Windows runners the default is `true`,
   so files committed and pushed to the test's fake-remote `pull/<N>/head`
   ref get CRLF-converted on the worktree's checkout. The PR-content
   assertion `expect(prFile).toBe('from PR 42\n')` then failed with
   `'from PR 42\r\n'`.

   Add `core.autocrlf=false` + `core.eol=lf` to the temp-repo setup
   so test files round-trip byte-for-byte regardless of host platform.

Local mac: cli worktreeStartup 15/15 still pass. Windows verification
deferred to CI.

* fix(worktree): reject '..' segments + use junction on Windows

Two Copilot findings on symlinkConfiguredDirectories (PR #4381 round 3):

1. The settingsSchema description, docs/users/features/worktree.md, and
   WorktreeSettings JSDoc all promise that entries containing `..` are
   rejected — but the post-resolve isWithinRoot check accepted
   `foo/../bar` (resolves to `bar`, inside the repo). Add a literal `..`
   segment check before path.resolve so the code matches the contract.

2. On Windows, fs.symlink(..., 'dir') requires
   SeCreateSymbolicLinkPrivilege (admin / Developer Mode) and EPERMs on
   default consumer installs. Use 'junction' for directory entries on
   win32 — junctions are reparse points that achieve the same semantics
   without elevation. Keep 'dir' on POSIX and 'file' for non-directory
   sources (no junction-equivalent for files; rare path).

Adds an integration test exercising `foo/../bar` to lock in the
syntactic guard; existing absolute-path and traversal tests already
covered the other rejection forms.

* fix(worktree): PR-worktree HEAD-SHA capture + symlink guard tests

Three findings from wenshao round 4 (PR #4381):

1. For --worktree=#42 (PR worktrees), originalHeadCommit was captured
   from the parent repo's HEAD via getCurrentCommitHash() — but the
   worktree branches off FETCH_HEAD (the PR tip), not main. Downstream,
   WorktreeExitDialog's `rev-list <originalHeadCommit>..HEAD` would
   count every commit in the fetched PR as "new work this session"
   alongside the user's actual commits.

   Same root cause covers the FETCH_HEAD TOCTOU window: between
   `git fetch origin pull/<N>/head` and `git worktree add ... FETCH_HEAD`,
   a concurrent `git fetch` from any other process sharing this repo
   could overwrite .git/FETCH_HEAD, causing the worktree to branch off
   an unrelated commit.

   Fix: add GitWorktreeService.resolveRef(ref) that returns a 40-char
   SHA (or null). In setupStartupWorktree, immediately after
   fetchPullRequestRef succeeds, resolve FETCH_HEAD to an immutable
   SHA; pass that SHA both as the baseRef to createUserWorktree (closes
   the TOCTOU) AND as originalHeadCommit in the returned context
   (closes the exit-dialog miscount). Fail-close on null resolve.

2. Orphaned JSDoc block at gitWorktreeService.ts:1035-1048 — originally
   wrote validateUserWorktreeSlug's docs, stranded above parsePRReference
   after that function was inserted between them. Move the block down to
   sit immediately above validateUserWorktreeSlug at its current line.

3. `.git` / `.qwen` symlink rejection guards (~20 lines of security-
   critical code at gitWorktreeService.ts:1640-1655) had no regression
   tests — only absolute paths, `..` traversal, isWithinRoot escapes,
   and missing sources were covered. Add two integ tests in
   gitWorktreeService.symlinks.integ.test.ts: one asserts `.git/hooks`
   is refused, one asserts `.qwen/projects` is refused.

Also extends the existing PR-worktree integration test in
worktreeStartup.test.ts to assert originalHeadCommit equals the
resolved FETCH_HEAD SHA AND does NOT equal the parent repo's main HEAD
— the assertion would fail loudly if the new SHA-capture path were
reverted.

* fix(worktree): realpath check on symlinkDirectories source + dest paths

Security fix from PR #4381 round 7 (wenshao/qwen3.7-max). The lexical
isWithinRoot + .git/.qwen blocklist checks in symlinkConfiguredDirectories
all operated on path.resolve(repoRoot, raw) — a STRING operation that
doesn't follow symlinks. A committed (or out-of-band) symlink at
<repo>/node_modules pointing into .git would pass every gate:

  1. path.resolve gives `<repo>/node_modules` (lexical, passes
     isWithinRoot against repo root).
  2. The .git/.qwen blocklists also see the lexical path — they don't
     detect that the realpath chains into .git.
  3. fs.stat() follows the symlink and succeeds against .git/.
  4. fs.symlink writes `<worktree>/node_modules → <repo>/node_modules`,
     which OS-side resolves through to <repo>/.git. Any tool inside the
     worktree that writes to node_modules/hooks/post-merge then has RCE
     on the next hook-firing git operation.

Fix: after fs.stat succeeds, fs.realpath the source and RE-RUN the three
containment checks against the realpath. Refuse on any escape. Use the
realpath (not the lexical sourceAbs) as the symlink target so the new
link is one-hop canonical rather than preserving the chain.

Also closes the dest-side variant of the same root cause — flagged in
round 4 thread #5 (declined then as overthinking) but now in scope per
the skill's iteration rule (two consecutive rounds raising the same
root-cause class). path.join(worktreePath, raw) is also lexical: if
git worktree add materialized a committed worktree-level symlink (e.g.
HEAD ships tools → /etc), then fs.mkdir / fs.symlink for a nested entry
like "tools/cache" writes OUTSIDE the worktree. Realpath the dest
parent before mkdir and refuse if it escapes the worktree.

New integ test covers both source-side variants (escape-to-git via
out-of-band symlink + escape-to-outside-dir) in one block. Was RED
against the pre-fix code: <wt>/escape-to-git was created as a symlink
that chained into the source repo's .git. GREEN after the fix.

* fix(worktree): canonicalise repo root before symlinkDirectories checks

Round-7's source-side realpath fix introduced a canonical-vs-lexical
mismatch: `repoRootAbs = path.resolve(this.sourceRepoPath)` is purely
lexical, while `realSource = await fs.realpath(sourceAbs)` is canonical.
On macOS where `/tmp → /private/tmp` and `/var → /private/var` are
ubiquitous, and on any Linux/Windows setup where the user's checkout
sits behind a symlink, the prefixes diverge at the symlink boundary and
`isWithinRoot(realSource, repoRootAbs)` silently rejects every
configured entry.

Production callers (worktreeStartup.ts, EnterWorktreeTool,
agent isolation) all pass the lexical path returned by
`git rev-parse --show-toplevel`. The integ tests masked the bug because
the shared `beforeEach` did `repoRoot = await fs.realpath(dir)` upfront.

Round 8 fix:

- Hoist `repoRootAbs`, `gitDirAbs`, `qwenDirAbs`, and `realWorktreePath`
  outside the for-loop — they're loop invariants and were being
  recomputed once per entry.
- `await fs.realpath(this.sourceRepoPath)` for `repoRootAbs` so every
  containment check below is canonical-vs-canonical. The derived
  `gitDirAbs` / `qwenDirAbs` blocklist paths inherit the canonical
  prefix automatically. `sourceAbs = path.resolve(repoRootAbs, raw)`
  inherits it too, so the early lexical reject paths (absolute, `..`,
  repo-root equality, isWithinRoot) stay self-consistent.
- Fail-close: if the repo root itself doesn't realpath (deleted /
  inaccessible), bail out of the entire symlink loop rather than
  continuing with comparisons we can't trust. Non-destructive — the
  worktree was created earlier by `git worktree add`.

New integ test provisions the production shape: a symlink path used
as `sourceRepoPath`, distinct from its canonical realpath. RED on the
pre-fix code (assertion fired with "symlinkDirectories entry was
silently rejected — canonical vs lexical isWithinRoot mismatch"),
GREEN after.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type/feature-request New feature or enhancement request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants