Skip to content

feat(cli): reload project runtime after /cd - #10263

Open
qqqys wants to merge 8 commits into
QwenLM:mainfrom
qqqys:feat/cd-project-runtime-reload
Open

feat(cli): reload project runtime after /cd#10263
qqqys wants to merge 8 commits into
QwenLM:mainfrom
qqqys:feat/cd-project-runtime-reload

Conversation

@qqqys

@qqqys qqqys commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR reloads project-scoped runtime state when an existing session changes its working directory with /cd. The switch is transactional for settings and file watching, then refreshes context files, permissions, tools, hooks, skills, subagents, MCP servers, system instructions, and durable cron scheduling for the destination project. Session-owned tools remain registered, while stale resources from the previous project are removed.

Why it's needed

Previously, /cd changed the process working directory but left runtime capabilities derived from the original project active until restart. That could expose the new project to permissions, hooks, tools, context, or background work that belonged to the previous project. Reloading the complete project runtime keeps the active session aligned with its current directory.

Reviewer Test Plan

How to verify

  1. Start a session in project A with distinct project settings, context files, permissions, hooks, skills, command tools, MCP servers, and durable cron tasks.
  2. Run /cd <project-b> where project B defines different values, and verify project-B capabilities become active without starting a new conversation.
  3. Verify project-A project-scoped tools and resources are removed while session-owned tools remain available.
  4. Verify the working-directory-change hook receives both canonical paths and the model receives the directory-change context.
  5. Try a destination with invalid project configuration and verify the switch aborts while project-A runtime state and settings watching remain active.
  6. Repeat the relocation through both the interactive UI and ACP session paths, including durable cron scheduling after the switch.

Evidence (Before & After)

Before: /cd changed the working directory while retaining project-scoped runtime capabilities from the original project.

After: the destination project runtime is loaded transactionally; blocking preparation failures roll back the switch, and non-blocking refresh failures report warnings without restoring stale capabilities.

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

Node.js 22 on macOS. Full build, typecheck, lint, and 4,114 focused and downstream tests passed after rebasing onto the latest upstream main.

Risk & Scope

  • Main risk or tradeoff: this touches shared session runtime infrastructure, so regressions could affect project-scoped capability refresh or cleanup after a directory change.
  • Not validated / out of scope: authentication, provider and model reconfiguration, sandbox/container relocation, and manual verification on Windows or Linux.
  • Breaking changes / migration notes: none.

Linked Issues

Closes #10173

中文说明

本 PR 做了什么

本 PR 在现有会话通过 /cd 更改工作目录时重新加载项目级运行时状态。切换过程会以事务方式更新设置和文件监听,然后刷新目标项目的上下文文件、权限、工具、Hooks、Skills、子智能体、MCP 服务、系统指令和持久化定时任务。会话级工具会继续保留,旧项目遗留的项目级资源会被移除。

为什么需要

此前 /cd 只会改变进程工作目录,启动时从原项目加载的运行时能力会一直保留到重启。这可能让新项目继续使用旧项目的权限、Hooks、工具、上下文或后台任务。完整重载项目运行时后,当前会话的能力会与当前目录保持一致。

Reviewer Test Plan

如何验证

  1. 在项目 A 启动会话,并配置有明显区别的项目设置、上下文文件、权限、Hooks、Skills、命令工具、MCP 服务和持久化定时任务。
  2. 执行 /cd <project-b>,让项目 B 提供不同配置,确认无需新建会话即可启用项目 B 的能力。
  3. 确认项目 A 的项目级工具和资源已被移除,同时会话级工具仍然可用。
  4. 确认工作目录变更 Hook 收到两个规范化路径,模型也收到目录变更上下文。
  5. 切换到包含无效项目配置的目录,确认切换被中止,项目 A 的运行时状态和设置监听保持有效。
  6. 分别通过交互界面和 ACP 会话路径验证切换,并确认切换后的持久化定时任务调度正常。

证据(修改前后)

修改前:/cd 改变工作目录后,原项目的项目级运行时能力仍然保留。

修改后:目标项目运行时以事务方式加载;阻断性的准备失败会回滚切换,非阻断刷新失败会报告警告且不会恢复旧项目能力。

测试平台

系统 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境(可选)

macOS、Node.js 22。在变基到最新上游主干后,全量构建、类型检查、Lint 以及 4,114 项重点和下游测试均通过。

风险与范围

  • 主要风险或权衡:改动涉及共享的会话运行时基础设施,回归可能影响目录切换后的项目级能力刷新或资源清理。
  • 未验证或超出范围:认证、Provider 和模型重新配置,沙箱或容器迁移,以及 Windows、Linux 上的人工验证。
  • 破坏性变更或迁移说明:无。

关联 Issue

Closes #10173

@qqqys

qqqys commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

/cd project runtime reload E2E plan

Baseline

  1. Start the CLI in project A with project-specific settings, context files, permissions, hooks, skills, command tools, MCP servers, and durable cron tasks.
  2. Run /cd <project-b> where project B defines visibly different values for each capability.
  3. Confirm the existing release keeps some project-A capabilities or does not activate the project-B capabilities until restart.

Expected behavior after the change

  1. /cd <project-b> changes the working directory without starting a new conversation.
  2. Project-B settings, context files, permissions, hooks, skills, command tools, MCP servers, and durable cron tasks become active.
  3. Project-A project-scoped tools and resources are no longer available.
  4. Session-owned tools remain registered.
  5. The model receives working-directory-change context and the CwdChanged hook receives both canonical paths.
  6. Invalid project-B configuration aborts the switch and preserves the project-A runtime and settings watcher.
  7. A non-blocking refresh failure reports a warning and does not restore stale project-A capabilities.
  8. Repeat the checks through both the interactive UI and ACP session relocation paths.

Verification record

  • Unit and downstream suites cover the transactional reload, resource cleanup, session-tool preservation, custom context names, watcher retargeting, TUI scheduler restart, and ACP scheduler restart.
  • Verified after rebasing onto the latest upstream main on macOS with Node.js 22.
  • npm run build: passed.
  • npm run typecheck: passed.
  • npm run lint: passed.
  • Core focused/downstream tests: 13 files, 1,908 tests passed.
  • CLI focused/downstream tests: 12 files, 2,206 tests passed.
  • Total: 25 files, 4,114 tests passed.

@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Aug 27, 2026
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

⚠️ Deferred approval withheld — 1 PR CI workflow run(s) on 7580e80 did not finish green; see the updated table in the Stage 2 comment. Re-run @qwen-code /triage after fixes. finalize run

⚠️ 延迟审批已搁置 —— 7580e80 有 1 个 PR CI workflow 未以绿色完成,详见 Stage 2 评论中已更新的表格。修复后可重新运行 @qwen-code /triage查看 finalize 运行

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

  • Template: complete ✓ — all required sections present, including the bilingual body.
  • Problem: real, not theoretical. Linked issue feat(cd): Reload project-scoped runtime configuration after /cd #10173 was verified in source during issue triage: Config.relocateWorkingDirectory() already refreshes workspace roots, memory, and MCP servers, but not settings, hooks, skills, or agent definitions — after /cd the session keeps running the previous project's hooks and permission/tool surface. Since hooks execute commands and settings control permissions, that's an actual policy-boundary gap, not a hypothetical one.
  • Direction: aligned. This fixes the semantics of an existing core command rather than adding new surface, and the issue's scope discipline is good (no credential/auth hot-swap, doesn't reopen the design(serve): Define multi-workspace session cd ownership semantics #7015 daemon-ownership question). Direct behavior reference found: Claude Code 2.1.246 changelog — "Improved /cd: the new directory's project settings, hooks, .mcp.json servers … skills, and agents now take effect right after the move instead of on --resume".
  • Size: ~1559 production lines vs ~840 test lines (2399 total across 52 files; no generated/schema files), spanning packages/core, packages/cli, and packages/acp-bridge. That's past the 1000-line large-PR advisory — worth considering a split if feasible, though the prepare/commit/rollback transaction reads most naturally as one unit, so this is informational only. For visibility: the two-tier core gate exempts maintainer-authored PRs and the author is a CODEOWNER for packages/core, so no escalation — just naming the size.
  • Approach: the prepare-and-commit design matches what the issue asked for: target settings load read-only before anything moves (a corrupt target config fails closed without touching the file), commit swaps LoadedSettings in place while the workspace watcher is paused, and chdir/realpath/artifact-migration failures roll back. Issue triage had suggested phasing (security-relevant reload first, then skills/agents/memory cleanup); this lands one-shot instead — defensible given the shared transaction, but worth naming. Also note the diff makes relative skills.directories / context.includeDirectories resolve against cwd at startup: equivalent today (cwd == target dir) but a startup-behavior change that rides along with the /cd work.
  • Risk: Stage 1e high-risk path match — packages/cli/src/acp-integration/acpAgent.ts and session/Session.ts (acp-integration paths correlate with post-merge reverts in this repo's history). Not a gate stop, but it means full Stage 2 enrichment and CI evidence before approval.

Moving on to code review. 🔍

中文说明

感谢贡献!

  • 模板:完整 ✓ —— 所有必需章节齐全,包含双语正文。
  • 问题:真实存在,并非理论推演。关联 issue feat(cd): Reload project-scoped runtime configuration after /cd #10173 在 issue 分诊时已在源码中核实:Config.relocateWorkingDirectory() 目前会刷新 workspace 根目录、memory 和 MCP servers,但不会重载 settings、hooks、skills 和 agent 定义——/cd 之后会话仍沿用上一个项目的 hooks 和权限/工具面。由于 hooks 会执行命令、settings 控制权限,这是真实的策略边界缺口,而非假设性问题。
  • 方向:一致。这是修正现有核心命令的语义,而不是新增产品面,且 issue 的范围约束良好(不热切换凭据/认证、不重新打开 design(serve): Define multi-workspace session cd ownership semantics #7015 的 daemon 归属问题)。找到直接行为参照:Claude Code 2.1.246 changelog——"Improved /cd: the new directory's project settings, hooks, .mcp.json servers … skills, and agents now take effect right after the move instead of on --resume"。
  • 规模:生产逻辑约 1559 行、测试约 840 行(52 个文件共 2399 行;无生成/schema 文件),横跨 packages/corepackages/clipackages/acp-bridge。超过 1000 行大 PR 建议线——如可行建议考虑拆分,但 prepare/commit/rollback 事务作为整体读起来最自然,故仅作提示。说明:两级核心门禁对维护者作者的 PR 豁免,且作者是 packages/core 的 CODEOWNER,因此不做升级——仅提示规模以保证可见性。
  • 方案:prepare-and-commit 设计符合 issue 要求:目标设置在任何状态移动之前以只读方式加载(损坏的目标配置会 fail-closed 且不改动文件),commit 在 workspace watcher 暂停期间原位替换 LoadedSettings,chdir/realpath/产物迁移失败会回滚。issue 分诊时曾建议分阶段落地(先安全相关重载,再做 skills/agents/memory 清理);本 PR 选择一次到位——鉴于共享事务这可以成立,但值得指出。另注意:diff 使相对的 skills.directories / context.includeDirectories 在启动时相对 cwd 解析——目前等价(cwd == 目标目录),但属于随 /cd 工作夹带的启动行为变化。
  • 风险:Stage 1e 高风险路径命中——packages/cli/src/acp-integration/acpAgent.tssession/Session.ts(acp-integration 路径与本仓库合并后回滚的历史相关)。不是门禁拦截,但意味着 approval 前需要完整 Stage 2 enrichment 和 CI 证据。

进入代码审查。🔍

Qwen Code · qwen3.8-max

Reviewed at 7580e80d0d51847cba55821fc401fd544c274acf · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Code review

Independent proposal first (before reading the diff): extend Config.relocateWorkingDirectory() with a prepare/commit/rollback phase driven by the CLI-owned settings loader, refresh permissions → tools → hooks → skills/subagents → memory → MCP in the new project's context, track session-owned vs project-scoped tools so the former survive the swap, fire a CwdChanged hook, and wire TUI (cdCommand) and ACP (acpAgent/Session) through the same core transaction. That is exactly what this PR does — the match is close, and it goes further in the right places: fail-closed hook reload (a failed reload drops configured hooks rather than keep running the previous project's), settings-watcher pause across the swap, and managed vs runtime-added workspace directories handled separately.

No critical blockers found. Transaction boundaries are right: prepare() loads target settings read-only and rejects before any state moves (corrupt target config throws FatalConfigError without touching the file — new test covers it); chdir/realpath/artifact-migration failures roll the settings swap back; once committed, per-subsystem refresh errors are collected as warnings instead of leaving cwd and settings diverged, which is what the issue's fail-closed spec asks for. Spot-verified against base code: recomputeMcpGating is reused from hot-reload.ts (no parallel gating logic), createToolRegistry / reinitializeMcpServers / Session.startCronScheduler / CronScheduler.destroy are pre-existing APIs, and channel sessions keep cron disabled via projectRuntimeCronEnabled (asserted in the acpAgent test).

Two follow-ups, both non-blocking:

  • Leftover process-global context-filename reads. Context filenames are threaded through the security-relevant surfaces (auto-mode protected-write checks, file exclusions, memory discovery, /memory dialog), but a few read sites still consult the process-global getCurrentGeminiMdFilename() / getAllGeminiMdFilenames(): writeContextFile.ts (serve /workspace/memory route), resolveQwenMemoryPaths in acpAgent (ACP getMemoryPaths), and serve/workspace-memory.ts. After /cd into a project with a custom context.fileName these would resolve the startup-time name. Narrow edge (custom filenames are rare; serve-managed workspaces don't relocate the same way) — fine as a follow-up.
  • Startup-behavior change riding along. Relative skills.directories and context.includeDirectories now resolve against cwd (resolveProjectSkillPath / resolveProjectPath) — needed so a target project's relative paths resolve correctly on /cd, and equivalent at startup since cwd == target dir there, but worth knowing it's in this diff. Test expectations updated accordingly.

Relocation flow

sequenceDiagram
    participant P1 as cd or ACP caller
    participant P2 as Config relocateWorkingDirectory
    participant P3 as ProjectRuntimeReloader
    participant P4 as LoadedSettings
    participant P5 as tools hooks permissions skills MCP
    P1->>P2: relocate to target dir
    P2->>P3: prepare target settings read-only
    P3-->>P2: prepared config with commit and rollback
    Note over P2,P3: prepare failure rejects before any state moves
    P2->>P4: commit settings swap, pause workspace watcher
    P2->>P2: chdir and verify realpath, roll back on mismatch
    P2->>P2: apply project runtime config
    P2->>P5: reload permissions, tools, hooks fail-closed, skills, subagents
    P2->>P5: reset and refresh memory, reinitialize MCP servers
    P2->>P5: refresh system instruction and tool declarations
    P2->>P5: fire CwdChanged hook with old and new cwd
    P2->>P3: complete, resume watcher on the new project
    P2-->>P1: success, non-blocking failures reported as warnings
Loading
Files changed (30 of 52 shown)
File What changed
packages/core/src/config/config.ts Core relocation transaction: prepare/commit/rollback wiring, applyProjectRuntimeConfig for ~50 fields, cron scheduler recycle, CwdChanged firing
packages/cli/src/config/config.ts Builds the ProjectRuntimeReloader from target-directory settings, including commit/rollback/complete with watcher pause
packages/cli/src/config/settings.ts LoadedSettings.replaceWith in-place identity swap; loadSettings gains readOnly mode (no on-disk corruption recovery during prepare)
packages/core/src/tools/tool-registry.ts Session-owned tool tracking; replaceCoreToolsFrom / clearProjectRuntimeTools / rediscoverCommandTools; explicit spawn cwd for discovered command tools
packages/core/src/hooks/hookRegistry.ts reloadConfiguredHooks gains failClosed: a failed reload drops configured hooks instead of restoring the previous project's
packages/core/src/hooks/hookSystem.ts reload options passthrough, fireCwdChangedEvent, updateHttpSecurity
packages/core/src/hooks/hookEventHandler.ts fireCwdChangedEvent with old_cwd / new_cwd payload
packages/core/src/hooks/types.ts CwdChanged event name and CwdChangedInput
packages/core/src/hooks/hookPlanner.ts Matcher-target case for CwdChanged
packages/core/src/hooks/hookRunner.ts updateHttpSecurity passthrough to the HTTP runner
packages/core/src/hooks/httpHookRunner.ts Private-network policy becomes mutable (updateSecurity)
packages/core/src/permissions/permission-manager.ts reloadForProjectChange: replaces project rules, preserves session-added rules
packages/core/src/permissions/autoMode.ts Per-config context filenames threaded through protected-write detection
packages/core/src/memory/memoryDiscovery.ts Optional contextFileNames instead of the process-global list
packages/core/src/memory/refresh.ts didWriteProjectContextFile accepts context filenames
packages/core/src/utils/workspaceContext.ts applyRootDirectories replaces managed include dirs, keeps runtime-added ones
packages/core/src/utils/ignorePatterns.ts Exclusion patterns from the config's context filenames
packages/core/src/skills/skill-manager.ts refreshForProjectChange (cache refresh + watcher retarget)
packages/core/src/subagents/subagent-manager.ts refreshForProjectChange with fail-closed project-cache drop
packages/core/src/core/coreToolScheduler.ts Passes context filenames into auto-mode review decisions
packages/cli/src/config/settingsWatcher.ts pauseWorkspaceWatching plus a processing-drain promise so pause waits for in-flight work
packages/cli/src/ui/commands/cdCommand.ts Passes trustedFolder after the trust confirmation, surfaces runtime-refresh warnings
packages/cli/src/acp-integration/acpAgent.ts ACP relocation: trust flag, refresh warnings, commands refresh, cron restart; channel sessions pin cron disabled
packages/cli/src/acp-integration/session/Session.ts Session-owned tool registration; context filenames in memory-write and auto-mode checks
packages/cli/src/ui/hooks/useGeminiStream.ts Cron scheduler restarts when the working directory changes
packages/cli/src/ui/AppContainer.tsx Context filenames from config instead of the global helper
packages/cli/src/ui/components/MemoryDialog.tsx Memory file resolution from config context filenames
packages/cli/src/ui/commands/initCommand.ts Primary context filename from config
packages/cli/src/gemini.tsx Passes LoadedSettings into loadCliConfig so the reloader exists
packages/acp-bridge/src/status.ts CwdChanged exposed in the serve hook-events surface
…and 22 more files

Test evidence

Unattended CI run — no PR code is built or executed in triage; the evidence below is the PR's own CI fetched via the API at the reviewed commit. At review time the two material checks are still running: the main Node suite (Test (ubuntu-latest, Node 22.x)) and the SDK Java daemon E2E (Real daemon E2E / Java 11). Everything completed so far is green. The macOS/Windows Node test jobs show as skipped by design — ci.yml runs them only on merge_group / schedule / workflow_dispatch, not on pull_request. No failing check to analyze. The table updates in place once CI settles.

Final CI results for 7580e80 (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Test (ubuntu-latest, Node 22.x) ❌ failure
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
macos-latest / Java 21 ✅ success
Real daemon E2E / Java 11 ✅ success
Secret scan (TruffleHog) ✅ success
ubuntu-latest / Java 11 ✅ success
ubuntu-latest / Java 17 ✅ success
ubuntu-latest / Java 21 ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
windows-latest / Java 21 ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

Sandboxed verification would settle this: @qwen-code /verify — the central claim is behavioural (after a live /cd, the destination project's hooks, permissions, tools, and skills become active while the previous project's are removed, and an invalid target config aborts the switch with the old runtime intact), which the diff alone cannot show and the unit suite would still pass with the reload wiring stubbed out. @qwen-code /tmux can drive the interactive /cd surface as a real user.

Real-scenario testing: not driven in this run (unattended CI path — live TUI testing is reserved for the isolated /tmux job). Not verified: live before/after /cd behaviour; the author reports macOS-only manual testing in the PR body (author's claim, not independently re-run).

中文说明

代码审查:先独立给出方案再对照 diff——本 PR 的做法与独立方案高度一致:以 prepare/commit/rollback 事务扩展 relocateWorkingDirectory,由 CLI 侧 settings loader 驱动,依次刷新权限、工具、hooks(fail-closed)、skills/子智能体、memory、MCP,并区分会话级与项目级工具,TUI 与 ACP 共用同一核心事务。未发现阻断性问题。事务边界正确:prepare 以只读方式加载目标设置、在任何状态移动前失败即拒绝(损坏配置抛 FatalConfigError 且不改动文件,有新测试覆盖);chdir/realpath/产物迁移失败会回滚;commit 之后各子系统刷新失败仅收集为警告,避免 cwd 与设置分裂——符合 issue 的 fail-closed 要求。两个非阻断跟进项:一是仍有少量读点使用进程级全局的上下文文件名(writeContextFile.ts、acpAgent 的 resolveQwenMemoryPathsserve/workspace-memory.ts),/cd 到自定义 context.fileName 的项目后这些路径会解析到启动时的文件名,边缘场景、可作后续跟进;二是相对的 skills.directories / context.includeDirectories 现在启动时相对 cwd 解析,属于为 /cd 正确性所需、启动时等价的夹带变化。上方有时序图说明迁移流程,文件总览表列出 30/52 个文件。

测试证据:本次为无人值守 CI 运行,不在分诊中构建或执行 PR 代码;证据来自 API 拉取的 PR 自身 CI。评审时两个关键检查仍在运行(ubuntu Node 22 主套件、SDK Java daemon E2E);已完成的全部通过;macOS/Windows Node 测试按 ci.yml 设计仅在 merge queue/定时/手动触发时运行,PR 上为 skipped,属预期。CI 落定后表格会原地更新。沙盒验证建议:@qwen-code /verify 可 A/B 证明目录切换后目标项目能力真正生效且旧项目资源被移除(这是 diff 与单测都无法钉住的行为性主张);@qwen-code /tmux 可驱动交互式 /cd 场景。实时 before/after 未在本次运行中执行;作者在 PR 正文中自述仅在 macOS 上人工测试(作者声明,非独立复跑)。

Qwen Code · qwen3.8-max

Reviewed at 7580e80d0d51847cba55821fc401fd544c274acf · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — clean review across every stage; only non-blocking follow-ups, and the approval waits on CI, not on doubt.

Stepping back: my independent proposal for this problem was, almost step for step, what this PR implements — a prepare/commit/rollback relocation transaction owned by core, driven by the CLI's settings loader, refreshing permissions → tools → hooks → skills/subagents → memory → MCP, with session-owned tools surviving the swap and both TUI and ACP on the same path. Where it goes beyond that baseline it does so in the right places: a fail-closed hook reload (a broken reload drops configured hooks rather than silently keep executing the previous project's), the settings watcher paused across the swap so the old directory is never watched mid-transition, and managed include directories replaced while runtime-added ones survive. The test suite (~840 lines) pins exactly where this kind of change leaks: apply/reject/rollback paths, session-tool preservation, permission-rule replacement with session rules intact, the CwdChanged payload, cron restart on relocation. The problem itself was source-verified during issue triage, and the direction has a direct external reference (Claude Code 2.1.246 shipped the same /cd semantics).

What keeps this at 4 rather than 5: a handful of read sites still consult the process-global context filename instead of the per-config one (serve memory route, ACP getMemoryPaths — noted in the review comment, fine as a follow-up), and ~1559 production lines across three packages is a large unit to land even when the transaction reads coherently. Neither blocks.

CI status: the main Node suite and the daemon E2E were still running at review time, so no approval is posted in this run. Approval is deferred until CI lands green on the reviewed commit; the marker below carries it.

中文说明

置信度:4/5 —— 各阶段审查均干净;只有非阻断的跟进项,等待的是 CI 而不是因为存疑。

退一步看:我对这个问题的独立方案与本 PR 几乎逐步一致——由 core 拥有、CLI settings loader 驱动的 prepare/commit/rollback 迁移事务,依次刷新权限 → 工具 → hooks → skills/子智能体 → memory → MCP,会话级工具在切换中保留,TUI 与 ACP 走同一路径。超出基线的部分也都用在正确的地方:hooks 以 fail-closed 方式重载(重载失败时丢弃已配置 hooks,而不是悄悄继续执行上一个项目的)、切换期间暂停设置监听以避免过渡期仍监听旧目录、受管 include 目录被替换而运行时新增目录得以保留。测试套件(约 840 行)恰好钉住了这类改动最容易泄漏的位置:应用/拒绝/回滚路径、会话工具保留、权限规则替换且会话规则不受影响、CwdChanged 载荷、迁移后 cron 重启。问题本身已在 issue 分诊时于源码中核实,方向也有直接外部参照(Claude Code 2.1.246 已上线相同的 /cd 语义)。

之所以是 4 而不是 5:少数读点仍读取进程级全局上下文文件名而非按配置的(serve memory 路由、ACP getMemoryPaths——已在审查评论中说明,可作后续跟进);约 1559 行生产逻辑横跨三个包,即便事务读起来连贯,仍是较大的落地单元。两者都不阻断。

CI 状态:评审时主 Node 套件与 daemon E2E 仍在运行,因此本次不发布 approve。批准延迟到 CI 在评审的 commit 上全绿;由下方标记承接。

Qwen Code · qwen3.8-max

Reviewed at 7580e80d0d51847cba55821fc401fd544c274acf · re-run with @qwen-code /triage

@qqqys

qqqys commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Aug 27, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-ci-bot qwen-code-ci-bot 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.

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

中文说明

仅完成部分审查,审查缺口已披露。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

— qwen3.8-max via Qwen Code /review (v0.22.2)

Comment thread packages/cli/src/config/config.ts Outdated
Comment thread packages/cli/src/config/config.ts Outdated
Comment thread packages/cli/src/config/config.ts
Comment thread packages/cli/src/config/config.ts Outdated
Comment thread packages/cli/src/config/settings.ts
Comment thread packages/core/src/config/config.ts
Comment thread packages/core/src/config/config.ts
Comment thread packages/core/src/permissions/permission-manager.ts
Comment on lines +665 to +667
async refreshForProjectChange(): Promise<void> {
try {
await this.refreshCache();

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] SubagentManager.refreshForProjectChange and SkillManager.refreshForProjectChange (skill-manager.ts:516) have no direct tests — they appear in test code only as bare vi.fn().mockResolvedValue(undefined) mocks in config.test.ts, whose new relocation test does not even assert their call count.

The subagent variant has a specific error path: on refreshCache() failure it deletes the 'project' cache entry, notifies listeners, and rethrows. Mutant: remove the catch block — the rethrow still propagates into projectRuntimeRefreshErrors via config.ts:5826-5829, so error reporting is unchanged, but subagentsCache keeps the old project's subagents enumerated in the new project; no test turns red. The skill variant's conditional updateWatchersFromCache() (only when watchStarted) is equally unwitnessed.

Add a subagent-manager unit test where refreshCache rejects, asserting the project cache entry is removed, change listeners fire, and the error rethrows; add a skill-manager case asserting updateWatchersFromCache runs only when watching has started. Removing the catch cleanup (or the watcher condition) must turn the corresponding test red.

中文说明

SubagentManager.refreshForProjectChangeSkillManager.refreshForProjectChange(skill-manager.ts:516)没有直接测试——它们在测试代码中只是 config.test.ts 里的裸 vi.fn().mockResolvedValue(undefined) mock,新的迁移测试甚至没有断言其调用次数。

subagent 变体有特定错误路径:refreshCache() 失败时删除 'project' 缓存条目、通知监听者并重新抛出。突变:移除 catch 块——重抛仍会经 config.ts:5826-5829 传进 projectRuntimeRefreshErrors,错误报告不变,但 subagentsCache 会在新项目里继续枚举旧项目的子智能体;没有测试变红。skill 变体的条件式 updateWatchersFromCache()(仅当 watchStarted)同样没有见证。

建议新增 subagent-manager 单元测试(refreshCache 拒绝时断言项目缓存条目被移除、监听者被通知、错误重抛)和 skill-manager 用例(断言仅在监听启动时运行 updateWatchersFromCache);删除对应清理后应变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

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.

Not in this commit — these two helpers' own unit tests are coverage work on code this PR did not change in this round, and I kept the commit to the findings with a runtime effect. Leaving the thread open.

中文说明

本次提交未处理——这两个 helper 各自的单元测试属于对本轮未改动代码的覆盖工作,本次提交只包含有运行时影响的发现。该讨论保持打开。

Comment thread packages/core/src/utils/workspaceContext.ts
@qqqys

qqqys commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover stop

@qwen-code-dev-bot qwen-code-dev-bot removed the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Aug 27, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

👋 Takeover released: the autofix loop will no longer engage this PR (an in-flight round, if any, completes its bounded work). Re-apply autofix/takeover (or comment @qwen-code /takeover) to re-engage.

中文说明

👋 已释放:autofix 循环不再介入此 PR(在飞的一轮如有,将完成其有界工作)。重新打上 autofix/takeover 标签(或评论 @qwen-code /takeover)即可再次接管。

qqqys added 2 commits August 28, 2026 01:38
Resolves the context-filename conflicts by keeping this branch's session-scoped
`config.getContextFileNames()` design and adopting main's renamed globals
(`getAllMemoryFilenames` / `setMemoryFilename` / `memoryFileCount`) as the
fallbacks. Test mocks of memory-constants export both spellings.
…e reload

The eleven Critical findings, plus the Suggestions that describe runtime
behaviour rather than coverage alone.

Critical:
- R1-1  `commit()` now applies the target's `.env`/`settings.env` to
        `process.env` (`reloadEnvironment`, as serve's workspaceReload does)
        and `rollback()` restores the previous directory's. `prepare()`
        takes the directory being left for that; the core interface gained
        the parameter.
- R1-2  A bare session's `/cd` projects from `createMinimalSettings()`
        instead of loading the real user-scope files.
- R1-3  `resolveProjectSkillPath` expands home spellings first, so
        `%userprofile%` skill directories survive (they were nailed under
        the project).
- R1-4  Permission-rule persistence never routes through a minimal
        LoadedSettings (`resolvePersistenceSettings`).
- R1-5  `reloadScopeFromDisk` re-runs the migration for a scope that was
        migrated in memory, so the first hot-reload after the move no longer
        regresses to the legacy layout.
- R1-6  Non-string `context.fileName` entries are dropped instead of throwing
        out of a half-committed relocation; the apply step is also wrapped so
        `complete()` (which resumes the watcher) is always reached.
- R1-7  Session-scoped context names threaded through the remaining
        consumers: `/directory add`, the TUI memory refresh, the ACP
        `getMemoryPaths` request (answered from the session owning the cwd),
        and optional parameters on the two daemon memory helpers.
- R1-8  `applyProjectRuntimeConfig` clears the legacy `hooks` fallback so a
        hook-less target cannot revive the previous project's hooks.
- R1-9  The CwdChanged fire site checks `getDisableAllHooks()` like every
        other fire site.
- R1-10 `clearProjectRuntimeTools` removes command-discovered tools only;
        core tools and factories survive a failed refresh.
- R1-11 Command discovery skips names the session owns.

Suggestions with a runtime effect:
- R1-14 the reloader replicates startup's `tool_search` denial (explicit
        setting, or the session model captured once at startup)
- R1-15 `agents` goes through the same projection startup uses
        (`projectAgentsSettings`), so `team.*` no longer appears after /cd
- R1-17 the resumed workspace watcher reconciles against disk
- R1-23 the cron work the swap cancelled is reported (`cronExitSummary`)
        by both consumers instead of vanishing with the destroyed scheduler
- R1-26 runtime-added directories are not absorbed into the managed set
        when an intermediate project happens to list them

Coverage the review asked for: R1-12 (session-owned registration), R1-13
(a real-file reloader suite), R1-16, R1-18, R1-19, R1-20, R1-22 (both
rollback sites), R1-24. Left as-is: R1-21 (the wider settings-parity audit
is a separate change) and R1-25 (refresh helpers' own unit tests).

Mutation-verified in three batches, 19 reverts against the full affected
suites: each revert reddens exactly its own test(s) and nothing else.

Claude-Session: https://claude.ai/code/session_01VXsC4f71S6U6YkW82NRw7m
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head 6d1bdd6, drove a fixed endpoint set against each, and diffed the JSON responses. Only fields that changed are shown.

No response changes against the PR base across 12 scenario(s).

Qwen Code · serve A/B

@qwen-code-ci-bot qwen-code-ci-bot 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.

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 2)": verifying the settings merge layer ( mergeSettings in packages/cli/src/config/settings.ts) passes a non-array security.allowedHttpHookUrls value through to …; "agent reverse-audit (round 2)": checking whether resolveDisabledSlashCommands and the remaining applyProjectRuntimeConfig spreads share the same unguarded shape.

Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/config/config.ts:1922 — [probe] complete() success-path watcher resume is never executed by any test
  • packages/cli/src/config/config.ts:1708 — [probe] cronEnabledOverride wired but never exercised through the real reloader
  • packages/core/src/config/config.test.ts:242 (+3 locations) — [review] Three relocate call sites unpinned: reload failClosed, tool-swap catch, client refresh
  • packages/cli/src/config/settings.ts:757 — [review] Target-project settings warnings collected on /cd but never surfaced
中文说明

仅完成部分审查,审查缺口已披露。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 2)"verifying the settings merge layer ( mergeSettings in packages/cli/src/config/settings.ts) passes a non-array security.allowedHttpHookUrls value through to …"agent reverse-audit (round 2)"checking whether resolveDisabledSlashCommands and the remaining applyProjectRuntimeConfig spreads share the same unguarded shape

收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 4 条(原文未翻译,列表见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.22.2)

this.enableTeamMemorySync = runtime.enableTeamMemorySync ?? false;
this.enableAutoSkill = runtime.enableAutoSkill ?? false;
this.autoSkillConfirm = runtime.autoSkillConfirm ?? true;
this.agentsSettings = runtime.agents ?? {};

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] R2-1: /cd stores the target project's agents settings here, but the background-agent concurrency caps derived from them (agents.maxParallelAgents / maxParallelAgentsByModel) are baked into BackgroundTaskRegistry at Config construction — private readonly fields with no setter, and the registry field itself is readonly. A session starting in project A (default cap 10) that /cds into project B with agents.maxParallelAgents: 1 (e.g. to bound parallel background agents against a rate-limited model) keeps admitting up to project A's cap: getAgentsSettings() reports 1 while tryReserveBackgroundSlot / waitForBackgroundSlot enforce 10; symmetrically a restrictive startup cap keeps throttling a project that loosens it. Before this PR agentsSettings was never mutated post-construction, so this staleness is newly introduced. Same class as round 1's R1-21.

Witness (probe against the real relocateWorkingDirectory):

capBefore=10 capAfter=10 agentsSettings.maxParallelAgents=1 registryIdentityPreserved=true

Adding a registry setter + sync in applyProjectRuntimeConfig flips the probe to capAfter=1.

Suggested fix: add a BackgroundTaskRegistry.setConcurrencyLimits(options) mirroring the constructor's validation (re-pumping waitQueue when a cap rises) and call it here after the agentsSettings assignment.

Fix witness: extend the relocation suite to prepare a runtime with agents: { maxParallelAgents: 2 } and assert the registry's max-concurrent value is 2 after relocate — it must go red when the sync is removed.

中文说明

/cd 在此存储目标项目的 agents 设置,但由其派生的后台智能体并发上限(agents.maxParallelAgents / maxParallelAgentsByModel)在 Config 构造时就已固化进 BackgroundTaskRegistry——这些是无 setter 的 private readonly 字段,registry 字段本身也是 readonly。在项目 A(默认上限 10)启动的会话 /cd 到设置了 agents.maxParallelAgents: 1 的项目 B(例如为限速模型约束并行后台智能体数量)后,仍会按项目 A 的上限放行:getAgentsSettings() 报告 1,而 tryReserveBackgroundSlot / waitForBackgroundSlot 实际执行 10;反向同理,启动时的严格上限会继续限制放宽了设置的项目。本 PR 之前 agentsSettings 从不在构造后被修改,因此这一过期状态是本 PR 新引入的。与第 1 轮的 R1-21 同类。

证据(针对真实 relocateWorkingDirectory 的探针):迁移后 capBefore=10 capAfter=10 agentsSettings.maxParallelAgents=1;为 registry 增加 setter 并在 applyProjectRuntimeConfig 中同步后,探针翻转为 capAfter=1

建议修复:为 BackgroundTaskRegistry 增加 setConcurrencyLimits(options)(复用构造函数的校验,上限放宽时重新唤醒 waitQueue),并在此处 agentsSettings 赋值后调用。

修复见证:在迁移测试套件中准备 agents: { maxParallelAgents: 2 } 的运行时,断言迁移后 registry 的最大并发值为 2;移除同步逻辑后该测试应变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

mcpServerCommand?: string;
mcpToolIdleTimeoutMs?: number;
disabledSkillLevels?: readonly SkillLevel[];
customSkillDirs?: readonly string[];

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] R2-15: ProjectRuntimeConfig — the reload surface this PR defines — omits worktree.symlinkDirectories, a project-scoped setting read into Config at construction only (private readonly worktreeSettings) yet consumed dynamically at tool-call time by enter_worktree and agent worktree isolation. The schema marks it requiresRestart: false, and this reloader already applies settings marked requiresRestart: true (cron, plansDirectory), so no restart-flag rationale justifies the omission. Trigger: project A sets worktree.symlinkDirectories: ["shared-secrets"]; after /cd to project B (["node_modules"]), enter_worktree in B symlinks B's shared-secrets directory into the new worktree — against B's declared settings — while B's node_modules entry is silently ignored. Same class as R2-1 / R1-21. (The sibling memory.agentTimeoutMinutes/agentMaxTurns knobs declare requiresRestart: true and are excluded from this finding.)

Witness (probe against the real relocateWorkingDirectory):

AssertionError: expected [ 'shared-secrets' ] to deeply equal [ 'node_modules' ]

With worktree added to ProjectRuntimeConfig and assigned in applyProjectRuntimeConfig the same probe passes.

Suggested fix: add worktree?: WorktreeSettings to the interface, populate it in createProjectRuntimeReloader mirroring startup (runtimeSettings.worktree ? { symlinkDirectories: runtimeSettings.worktree.symlinkDirectories } : undefined, bare/safe → undefined), drop readonly from worktreeSettings and assign this.worktreeSettings = runtime.worktree ?? {} in applyProjectRuntimeConfig.

Fix witness: a relocation test constructing Config with worktree: { symlinkDirectories: ['a'] }, relocating with a runtime overriding ['b'], asserting getWorktreeSymlinkDirectories() is ['b'] — red without the assignment.

中文说明

本 PR 定义的重载面 ProjectRuntimeConfig 遗漏了 worktree.symlinkDirectories:该项目级设置只在构造时读入 Config(private readonly worktreeSettings),却在工具调用时被 enter_worktree 和智能体 worktree 隔离动态读取。Schema 将其标为 requiresRestart: false,且本 reloader 已经会应用标为 requiresRestart: true 的设置(cron、plansDirectory),因此没有重启标志上的理由可以解释这一遗漏。触发场景:项目 A 设置 worktree.symlinkDirectories: ["shared-secrets"]/cd 到项目 B(["node_modules"])后,在 B 中执行 enter_worktree 会把 B 的 shared-secrets 目录软链进新 worktree——与 B 的声明设置相悖——而 B 的 node_modules 条目被静默忽略。与 R2-1 / R1-21 同类。(同类兄弟字段 memory.agentTimeoutMinutes/agentMaxTurns 声明为 requiresRestart: true,不在本条范围内。)

证据(针对真实 relocateWorkingDirectory 的探针):AssertionError: expected [ 'shared-secrets' ] to deeply equal [ 'node_modules' ];在接口中加入 worktree 并在 applyProjectRuntimeConfig 赋值后同一探针通过。

建议修复:接口增加 worktree?: WorktreeSettings,在 createProjectRuntimeReloader 中按启动逻辑填充(runtimeSettings.worktree ? { symlinkDirectories: runtimeSettings.worktree.symlinkDirectories } : undefined,bare/safe 下为 undefined),去掉 worktreeSettingsreadonly 并在 applyProjectRuntimeConfig 中赋值 this.worktreeSettings = runtime.worktree ?? {}

修复见证:迁移测试中以 worktree: { symlinkDirectories: ['a'] } 构造 Config,用覆盖为 ['b'] 的运行时迁移,断言 getWorktreeSymlinkDirectories()['b'];移除该赋值后变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

Comment thread packages/core/src/config/config.ts Outdated
this.webSearchSettings = runtime.webSearch;
this.webSearchNoticeEmitted = false;
this.imageModel = runtime.imageModel || undefined;
this.allowedHttpHookUrls = [...runtime.allowedHttpHookUrls];

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] R2-23: this spread is unguarded: settings load performs no type validation and the CLI builder passes runtimeSettings.security?.allowedHttpHookUrls through raw, so a target project whose .qwen/settings.json has "security": { "allowedHttpHookUrls": 42 } makes [...42] throw TypeError here, mid-apply. The error-collection wrapper (added by the round-1 R1-6 fix) catches it, records it in projectRuntimeRefreshErrors, and relocation continues — silently skipping every later assignment in this method: permissionsDeny, contextFileNames, disableAllHooks, userHooks/projectHooks, and the this.hooks = undefined legacy reset. The session then runs in the target directory under the SOURCE project's permission and hook rules: the target's permissions.deny is never enforced, the previous project's legacy hooks are never cleared, and the context file names never update. The PR's own test anchored near here defends context.fileName from this exact failure class, leaving this sibling unguarded. A sibling sweep confirms allowedHttpHookUrls is the only field that passes through prepare() raw and spreads at apply time — the others (explicitIncludeDirectories, disabledSlashCommands, customSkillDirs) are constructed inside prepare() and fail closed before any commit.

Witness (probes):

merged.security.allowedHttpHookUrls = 42 isArray: false
prepared.config.allowedHttpHookUrls = 42
spread THREW -> TypeError: ... is not iterable
full relocate: move completes; getPermissionsDeny() still ['source-project-deny'];
getContextFileNames() still ['PROJECT-A.md']; legacy hooks intact

With Array.isArray(...) ? [...] : [] the same probe passes with the target deny applied, PROJECT-B.md active, and hooks cleared.

Suggested change
this.allowedHttpHookUrls = [...runtime.allowedHttpHookUrls];
this.allowedHttpHookUrls = Array.isArray(runtime.allowedHttpHookUrls)
? [...runtime.allowedHttpHookUrls]
: [];

Fix witness: a sibling of the context.fileName test — relocate with allowedHttpHookUrls: 42 and permissions: { deny: ['target-project-deny'] }, assert the target deny applies with no refresh errors; red without the coercion.

中文说明

此展开未加防护:设置加载不做类型校验,CLI 构建端原样透传 runtimeSettings.security?.allowedHttpHookUrls,因此目标项目的 .qwen/settings.json 若含 "security": { "allowedHttpHookUrls": 42 }[...42] 会在 apply 中途抛出 TypeError。(第 1 轮 R1-6 修复加入的)错误收集包装会捕获它、记入 projectRuntimeRefreshErrors 并继续迁移——从而静默跳过本方法之后的全部赋值:permissionsDenycontextFileNamesdisableAllHooksuserHooks/projectHooks 以及 this.hooks = undefined 的 legacy 重置。会话随后在目标目录中按“源”项目的权限与 hooks 规则运行:目标的 permissions.deny 永不生效,旧项目的 legacy hooks 永不清除,上下文文件名也不更新。本 PR 在附近为 context.fileName 加的测试恰好防御了同一失败类别,却遗漏了这个兄弟字段。兄弟字段排查确认 allowedHttpHookUrls 是唯一原样穿过 prepare() 并在 apply 时展开的字段——其余(explicitIncludeDirectoriesdisabledSlashCommandscustomSkillDirs)都在 prepare() 内构造,会在 commit 前失败关闭。

证据(探针):merged.security.allowedHttpHookUrls = 42 isArray: false,展开抛出 TypeError: ... is not iterable;完整迁移后 getPermissionsDeny() 仍为 ['source-project-deny']getContextFileNames() 仍为 ['PROJECT-A.md']、legacy hooks 仍在。改为 Array.isArray(...) ? [...] : [] 后同一探针通过:目标 deny 生效、PROJECT-B.md 激活、hooks 被清除。

修复见证:仿照 context.fileName 测试新增用例——以 allowedHttpHookUrls: 42permissions: { deny: ['target-project-deny'] } 迁移,断言目标 deny 生效且无刷新错误;去掉该防护后变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

loadedSettings: LoadedSettings | undefined,
cwd: string,
): LoadedSettings {
return loadedSettings?.user.path ? loadedSettings : loadSettings(cwd);

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] R1-4: (fix-induced) The round-1 crash — bare-mode "Always allow" persisting through createMinimalSettings()'s '' paths — is closed by this fallback, but the fallback opened a new defect at the same site: onPersistPermissionRule (config.ts:2684) is a construction-time closure capturing the startup cwd, never refreshed by /cd (Config.onPersistPermissionRuleCallback is readonly). In bare mode loadedSettings.user.path is always '', so every persistence takes this loadSettings(cwd) branch with the STALE startup cwd, and setValue(Workspace, ...) writes the rule into the project that was LEFT, not the relocation target. Concretely: qwen --bare in project A (bare mode keeps manual approval, so prompts fire), /cd to project B, approve a tool with "Always allow" — the grant lands in project A's .qwen/settings.json, never takes effect in project B (the user is re-prompted in every future session there), and project A's settings file is silently modified. Non-bare sessions are unaffected (replaceWith swaps scopes in place on the same instance).

Witness (probe):

projectA permissions: {"allow":["run_shell_command(ls *)"]}
projectB permissions: null

The flip arm resolving against the session's actual directory puts the rule in project B.

Suggested fix: make the persistence path relocation-aware — re-supply onPersistPermissionRule through ProjectRuntimeConfig (passing the live loadedSettings instance commit() already swaps and the target cwd), or have the closure resolve the directory lazily via a Config-bound getter instead of the captured startup cwd.

Fix witness: build a CLI config in bare mode, relocate via relocateWorkingDirectory, invoke config.getOnPersistPermissionRule() with scope 'project', and assert the rule lands in <targetDir>/.qwen/settings.json — red without the live-cwd rewiring.

中文说明

(修复引入)第 1 轮的崩溃——bare 模式下“始终允许”经由 createMinimalSettings()'' 路径持久化——已被此回退分支修复,但该回退在同一位置引入了新缺陷:onPersistPermissionRule(config.ts:2684)是构造时捕获启动 cwd 的闭包,/cd 从不刷新它(Config.onPersistPermissionRuleCallback 为 readonly)。bare 模式下 loadedSettings.user.path 恒为 '',因此每次持久化都会走这个 loadSettings(cwd) 分支,而 cwd 是过期的启动目录,setValue(Workspace, ...) 会把规则写进“已离开”的项目,而不是迁移目标。具体场景:在项目 A 中 qwen --bare(bare 模式保留人工审批,确认弹窗会出现),/cd 到项目 B,用“始终允许”批准某工具——授权落进项目 A 的 .qwen/settings.json,在项目 B 永不生效(此后每个会话都会重复询问),且项目 A 的设置文件被静默修改。非 bare 会话不受影响(replaceWith 在同一实例上原位切换作用域)。

证据(探针):projectA permissions: {"allow":["run_shell_command(ls *)"]} / projectB permissions: null;将会话实际目录作为解析目标的对照分支则把规则写入项目 B。

建议修复:让持久化路径感知迁移——通过 ProjectRuntimeConfig 重新提供 onPersistPermissionRule(传入 commit() 已在原位切换的活 loadedSettings 实例与目标 cwd),或让闭包通过绑定到 Config 的 getter 惰性解析目录,而不是捕获的启动 cwd

修复见证:以 bare 模式构建 CLI 配置,执行 relocateWorkingDirectory 迁移,调用 config.getOnPersistPermissionRule()(scope 为 'project'),断言规则落入 <targetDir>/.qwen/settings.json;去掉活 cwd 改造后变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

Comment thread packages/cli/src/config/config.ts Outdated
Comment on lines +1867 to +1870
agents:
bareMode || safeMode
? undefined
: projectAgentsSettings(nextSettings.merged.agents),

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] R2-25: the reloader gates agents on bareMode || safeMode, but startup's configParams passes agents: projectAgentsSettings(settings.agents) with NO safe-mode gate — the one field where the two projections diverge, despite projectAgentsSettings' docblock claiming the two "cannot drift". In safe mode the gate is operative (user-scope agents survive skipWorkspaceSettings); in bare mode it is redundant (createMinimalSettings() already yields undefined). A safe-mode session whose user settings define agents: { allowedGrades: ['fast'], modelGrades: {...}, arena: { worktreeBaseDir: ... } } starts with those applied; after /cd ../other (allowed — cdCommand has no safe-mode restriction), prepare() emits agents: undefined, applyProjectRuntimeConfig sets agentsSettings = {}, and SubagentManager.getAvailableModelGrades() (reads config.getAgentsSettings() at spawn) returns an empty map — the user's grade restriction is silently LOOSENED to all grades mid-session, and arena worktrees relocate to the default base dir.

Witness (probe, real loadCliConfig + real reloader):

startup safe-mode getAgentsSettings() = {modelGrades..., allowedGrades:['fast'], arena:{worktreeBaseDir...}} (isSafeMode()=true)
safe-mode prepared.config.agents = undefined   <- after /cd
normal-mode prepared.config.agents = {full projection}   <- control arm
Suggested change
agents:
bareMode || safeMode
? undefined
: projectAgentsSettings(nextSettings.merged.agents),
agents:
bareMode
? undefined
: projectAgentsSettings(nextSettings.merged.agents),

Fix witness: a reloader test with user settings agents: { allowedGrades: ['fast'] } and makeReloader(loaded, { safeMode: true }) asserting prepared.config.agents?.allowedGrades equals ['fast'] — red if the safeMode gate is re-added.

中文说明

reloader 用 bareMode || safeMode 门控 agents,但启动时的 configParams 传递 agents: projectAgentsSettings(settings.agents) 时并没有 safe 模式门控——尽管 projectAgentsSettings 的文档注释声称两者“不会漂移”,这正是两个投影唯一分叉的字段。safe 模式下该门控实际生效(用户级 agentsskipWorkspaceSettings 下仍然存在);bare 模式下则是冗余的(createMinimalSettings() 本来就得到 undefined)。用户设置定义了 agents: { allowedGrades: ['fast'], ... } 的 safe 模式会话启动时这些设置会生效;执行 /cd ../other(允许——cdCommand 不限制 safe 模式)后,prepare() 输出 agents: undefinedapplyProjectRuntimeConfigagentsSettings = {}SubagentManager.getAvailableModelGrades()(spawn 时读取 config.getAgentsSettings())返回空映射——用户的等级限制在会话中途被静默“放宽”为全部等级,arena worktree 也会回落到默认基础目录。

证据(探针,真实 loadCliConfig + 真实 reloader):启动 safe 模式下 getAgentsSettings() 含完整投影(allowedGrades:['fast'] 等,isSafeMode()=true);/cd 后 safe 模式 prepared.config.agents = undefined;非 safe 对照分支得到完整投影。

修复见证:reloader 测试中用户设置含 agents: { allowedGrades: ['fast'] }makeReloader(loaded, { safeMode: true }),断言 prepared.config.agents?.allowedGrades 等于 ['fast'];重新加上 safeMode 门控后变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

return this.targetDir;
}

private applyProjectRuntimeConfig(runtime: ProjectRuntimeConfig): void {

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] R1-21: round 1 flagged that applyProjectRuntimeConfig re-applies ~50 settings-derived knobs but silently skips other settings-derived requiresRestart: false fields (maxToolCallsPerTurn, gitCoAuthor, …), so after /cd the session keeps the OLD project's values. The answer was "Not in this commit — closing it means auditing every requiresRestart: false settings-derived field"; the thread was left open and nothing changed this round, so the entry still stands. The class remains open and grew two probe-confirmed Critical members this round: R2-1 (agents concurrency caps stored but never enforced by BackgroundTaskRegistry) and R2-15 (worktree.symlinkDirectories not even stored), plus chatCompression, autoCompactThreshold, skipLoopDetection, preventSystemSleep in the same omission class. Any /cd into a project that tunes one of the skipped fields keeps the source project's value mid-session, contradicting the reload's stated goal of aligning the session with its current directory.

Suggested fix: audit the requiresRestart: false, settings-derived fields and either add them to ProjectRuntimeConfig/applyProjectRuntimeConfig or document each exclusion with a restart-flag rationale.

Fix witness: per-field relocation assertions mirroring the existing "apply the prepared project runtime" suite — each red without the corresponding assignment.

中文说明

第 1 轮曾指出 applyProjectRuntimeConfig 重新应用约 50 个派生自设置的旋钮,却静默跳过其他派生自设置且 requiresRestart: false 的字段(maxToolCallsPerTurngitCoAuthor 等),导致 /cd 后会话保留“旧”项目的取值。当时的答复是“本次提交不处理——收尾意味着审计所有 requiresRestart: false 的设置派生字段”;线程保持开放,本轮也没有任何变化,因此该条目仍然存在。这个类别依然开放,且本轮新增了两个经探针确认的 Critical 成员:R2-1(agents 并发上限被存储但 BackgroundTaskRegistry 从不执行)与 R2-15(worktree.symlinkDirectories 甚至未被存储),另有 chatCompressionautoCompactThresholdskipLoopDetectionpreventSystemSleep 同属遗漏类别。任何 /cd 进入调整了被跳过字段的项目,都会在会话中途沿用源项目的取值,与重载“让会话与当前目录对齐”的声明目标相悖。

建议修复:审计 requiresRestart: false 的设置派生字段,要么加入 ProjectRuntimeConfig/applyProjectRuntimeConfig,要么为每个排除项给出重启标志层面的理由。

修复见证:仿照现有“应用准备好的项目运行时”套件逐字段添加迁移断言——缺少对应赋值时各自变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

* caller acting for a session whose `/cd` scoped the names should pass
* that session's primary name.
*/
contextFileName?: string;

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] R2-9: this new contextFileName option is a dead switch — declared, documented, and read by resolveContextFilePath, but no production caller sets it: all three writeWorkspaceContextFile call sites (serve/workspace-memory.ts:303 and :444, serve/acp-http/dispatch.ts:3849) pass object literals without contextFileName; the only setter in the tree is the new unit test. Meanwhile applyProjectRuntimeConfig updates the session's contextFileNames but never calls setMemoryFilename (deliberately, pinned by test), so after /cd into a project with context.fileName: PROJECT-B.md, discovery, write-detection, the /memory dialog, and collectWorkspaceMemoryStatus all think in session-scoped names, while every daemon/ACP memory-write route still resolves the stale process-global getCurrentMemoryFilename() — a client that saves memory through those routes after /cd writes to the wrong file: the exact GET-vs-POST filename divergence the comment block above this option warns about.

Suggested fix: pass the session/workspace runtime's primary context filename at the three write call sites (the workspace runtime already carries contextFilename, workspace-service/types.ts), or delete the option until a consumer exists.

Fix witness: a route-level test asserting a memory-write request with a session-scoped context filename resolves and writes <projectRoot>/<session filename> — none exists today; the only pin is on the unwired helper.

中文说明

这个新的 contextFileName 选项是死开关——已声明、有文档、被 resolveContextFilePath 读取,但没有任何生产调用方设置它:writeWorkspaceContextFile 的全部三个调用点(serve/workspace-memory.ts:303:444serve/acp-http/dispatch.ts:3849)传入的对象字面量都不含 contextFileName;整棵树中唯一的设置者是新的单元测试。与此同时 applyProjectRuntimeConfig 会更新会话的 contextFileNames 但从不调用 setMemoryFilename(刻意为之,有测试钉住),因此 /cd 进入 context.fileName: PROJECT-B.md 的项目后,发现、写入检测、/memory 对话框、collectWorkspaceMemoryStatus 都按会话作用域名称思考,而每个守护进程/ACP 记忆写入路由仍解析过期的进程级全局 getCurrentMemoryFilename()——客户端在 /cd 后通过这些路由保存记忆会写错文件:正是本选项上方注释块所警告的 GET 与 POST 文件名分叉。

建议修复:在三个写入调用点传入会话/工作区运行时的主上下文文件名(工作区运行时本就携带 contextFilename,见 workspace-service/types.ts),或在出现消费者之前删除该选项。

修复见证:路由级测试断言携带会话作用域上下文文件名的记忆写入请求解析并写入 <projectRoot>/<会话文件名>——目前不存在这样的测试,唯一的钉点在这个未接线的助手函数上。

— qwen3.8-max via Qwen Code /review (v0.22.2)

Comment on lines +249 to +251
if (contextFileName) {
return scope === 'workspace'
? path.join(projectRoot, contextFileName)

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] R2-10: this new contextFileName join has no containment validation, and the option's doc comment invites callers to pass session names that originate in unvalidated project settings: resolveContextFileNames (core config.ts) only trims and type-filters, so '../../evil' and absolute paths from a target project's context.fileName survive. Once any caller wires this option (as the doc comment directs), resolveContextFilePath returns path.join(projectRoot, '../../evil'), and runWrite then does fs.mkdir(dirname, recursive) + fs.writeFile outside the workspace — a write-what-where primitive fed by a semi-trusted repo's settings. The daemon facade already validates the same input class for init (workspace-service/index.ts rejects with "resolves outside the bound workspace", pinned by facade.test.ts), but this helper bypasses that check. Not reachable by production callers today (see the companion dead-switch finding) — the risk materializes exactly when the documented wiring lands.

Witness (probe): writeWorkspaceContextFile({scope:'workspace', mode:'append', projectRoot: <tmp>/workspace, contextFileName: '../escape.md'})

ESCAPE: filePath=/tmp/qwen-r210-.../escape.md projectRoot=/tmp/qwen-r210-.../workspace

The file landed outside the workspace; with a containment guard patched in, the same probe is refused and all 18 existing tests stay green.

Suggested fix: before returning, resolve the candidate path and assert it stays inside the base directory (mirroring the facade's "resolves outside" check) for both scopes; alternatively validate in resolveContextFileNames so every consumer is covered.

Fix witness: a writeContextFile.test.ts case asserting contextFileName: '../escape.md' rejects (writing nothing outside projectRoot) — red if the containment check is removed.

中文说明

这个新的 contextFileName 拼接没有包含性校验,而该选项的文档注释正邀请调用方传入源自未校验项目设置的会话名称:resolveContextFileNames(core config.ts)只做 trim 和类型过滤,因此来自目标项目 context.fileName'../../evil' 和绝对路径可以存活。一旦有调用方接上该选项(文档注释正是这么指引的),resolveContextFilePath 会返回 path.join(projectRoot, '../../evil')runWrite 随即执行 fs.mkdir(dirname, recursive) + fs.writeFile 写到工作区之外——一个由半信任仓库设置喂入的任意写原语。守护进程 facade 在 init 路径上已对同类输入做校验(workspace-service/index.ts 以 "resolves outside the bound workspace" 拒绝,有 facade.test.ts 钉住),但本助手函数绕过了该校验。当前生产调用方不可达(见配套的死开关发现)——风险恰好在文档所述接线落地时成形。

证据(探针):writeWorkspaceContextFile({scope:'workspace', mode:'append', projectRoot: <tmp>/workspace, contextFileName: '../escape.md'})ESCAPE: filePath=/tmp/qwen-r210-.../escape.md projectRoot=/tmp/qwen-r210-.../workspace,文件落在工作区之外;打上包含性守卫后同一探针被拒绝,且现有 18 个测试保持为绿。

建议修复:返回前解析候选路径并断言其保持在基础目录内(两种 scope 都套用,仿照 facade 的 "resolves outside" 检查);或在 resolveContextFileNames 中校验,使所有消费者都被覆盖。

修复见证:writeContextFile.test.ts 用例断言 contextFileName: '../escape.md' 被拒绝(projectRoot 外无写入);移除包含性检查后变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

Comment on lines +665 to +667
async refreshForProjectChange(): Promise<void> {
try {
await this.refreshCache();

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] R1-25: round 1 flagged that SubagentManager.refreshForProjectChange (here) and SkillManager.refreshForProjectChange (skill-manager.ts:516) have no direct tests — they appear in test code only as bare vi.fn() mocks in config.test.ts. The answer was "Not in this commit — these helpers' own unit tests are coverage work on code this PR did not change; leaving the thread open"; nothing changed this round, so the entry still stands. Verified by mutation this round: (a) deleting the updateWatchersFromCache() branch in the skill manager survives the whole suite — after /cd the skill watcher would keep watching the previous project's skill directories, so skill additions/changes in the new project never surface until restart; (b) dropping this catch body survives too — on a /cd whose target subagent directory read fails, the manager would keep serving the previous project's subagents while relocateWorkingDirectory records the error and moves on. Both new probes flip (pristine code green; under mutation the skill probe finds no watcher for the new project's skills dir and the subagent probe finds change listeners unnotified).

Fix witness: add direct unit tests — subagent-manager: refreshCache rejects, assert the 'project' cache entry is evicted, listeners are notified, and the error propagates; skill-manager: start watching, call refreshForProjectChange, assert watchers are rebuilt from the refreshed cache (and a not-started watcher stays not-started). Each must go red when the corresponding branch is removed.

中文说明

第 1 轮曾指出 SubagentManager.refreshForProjectChange(此处)与 SkillManager.refreshForProjectChange(skill-manager.ts:516)没有直接测试——它们在测试代码中仅以 config.test.ts 里的裸 vi.fn() mock 出现。当时的答复是“本次提交不做——这两个助手函数自身的单元测试属于对本 PR 未改动代码的覆盖工作;线程保持开放”;本轮没有任何变化,因此该条目仍然存在。本轮经变异验证:(a) 删除 skill manager 中的 updateWatchersFromCache() 分支,整个套件仍为绿——/cd 后 skill 监视器会继续监视上一个项目的 skill 目录,新项目中的 skill 新增/变更在重启前永不浮现;(b) 删除此处 catch 体,同样存活——当 /cd 的目标子智能体目录读取失败时,管理器会继续提供上一个项目的子智能体,而 relocateWorkingDirectory 仅记录错误并继续。两个新探针均可翻转(原始代码为绿;变异后 skill 探针发现新项目 skills 目录没有监视器、子智能体探针发现变更监听器未被通知)。

修复见证:补充直接单元测试——subagent-manager:refreshCache 拒绝时断言 'project' 缓存条目被清除、监听器被通知、错误向外传播;skill-manager:先启动监视,调用 refreshForProjectChange,断言监视器按刷新后的缓存重建(未启动的监视器保持未启动)。移除对应分支后各自必须变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

Comment on lines +516 to +517
async refreshForProjectChange(): Promise<void> {
await this.refreshCache();

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] R1-25 (second location): round 1 flagged that this refreshForProjectChange and its twin in SubagentManager (subagent-manager.ts:665) have no direct tests; the author deferred ("Not in this commit … leaving the thread open") and nothing changed this round. This location is the watcher half: deleting the updateWatchersFromCache() branch survives the whole suite (verified by mutation), leaving the skill watcher on the previous project's directories after /cd — skill additions/changes in the new project never surface until restart. See the companion comment on subagent-manager.ts for the full entry and fix witness.

中文说明

R1-25(第二处):第 1 轮曾指出本 refreshForProjectChange 与其在 SubagentManager 中的孪生方法(subagent-manager.ts:665)没有直接测试;作者推迟处理(“本次提交不做……线程保持开放”),本轮没有任何变化。本处是监视器侧:删除 updateWatchersFromCache() 分支后整个套件仍为绿(已经变异验证),/cd 后 skill 监视器仍停留在上一个项目的目录上——新项目中的 skill 新增/变更在重启前永不浮现。完整条目与修复见证见 subagent-manager.ts 上的配套评论。

— qwen3.8-max via Qwen Code /review (v0.22.2)

@qqqys

qqqys commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

已修复本轮 5 条 Critical:目录切换后会同步后台智能体并发限制、worktree 软链设置和权限持久化目标,safe mode 保留用户级 agents 设置;非法 allowedHttpHookUrls 会安全降级且不再中断后续运行时配置应用。验证通过:全仓 build、typecheck;core 747 项相关测试、CLI 366 项相关测试;目标 ESLint、Prettier 与 git diff --check。修复提交 afbf358

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully. The review pipeline failed before a review could be posted. A transient error is retried automatically; if you are seeing this, retry with @qwen-code /review. See workflow logs.

@qqqys

qqqys commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /review

@github-actions

Copy link
Copy Markdown
Contributor

Qwen Code review request accepted. Review is queued in workflow run.

@qqqys

qqqys commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

已修复 upstream 合并后的编译回归:项目运行时刷新改用当前 LlmClient 实例,消除 core 中两处 geminiClient 不存在错误。

验证:Node 22 下仓库 build、typecheck 通过;core config.test.ts 599/599;目标 ESLint、Prettier 与独立 relocation 路径验证通过。提交 6d1bdd6

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully. The review pipeline failed before a review could be posted. A transient error is retried automatically; if you are seeing this, retry with @qwen-code /review. See workflow logs.

@qwen-code-ci-bot qwen-code-ci-bot 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.

Partially reviewed — gaps disclosed.

4 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R2-18 bare-mode persistence fallback re-enters loadSettings(cwd) with env loading enabled — already reported (comment 3875850392)
  • cronEnabledOverride never exercised through the real reloader (test gap) — already recorded in the round-2 deferral list (review 5045650144)
  • complete() success-path watcher resume never executed by any test — already recorded in the round-2 deferral list (review 5045650144)
  • tool-swap catch / reload failClosed / client-refresh call sites unpinned — already recorded in the round-2 deferral list (review 5045650144)

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Convergence: round 3 posted 26 inline comment(s), 15 of them reported for the first time; the previous round posted 21 (18 new). Findings keep coming back to the same files: packages/cli/src/config/config.ts (findings in rounds 1, 2; 4 more now); packages/core/src/config/config.ts (findings in rounds 1, 2; 1 more now); packages/cli/src/config/config.projectRuntimeReloader.test.ts (findings in round 2; 1 more now), and 1 more file(s). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 4 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

收敛情况:第 3 轮发布了 26 条行内评论,其中 15 条是首次提出;上一轮发布了 21 条(其中 18 条首次提出)。发现反复回到同一批文件:packages/cli/src/config/config.ts(第 1、2 轮已出过发现,本轮又有 4 条);packages/core/src/config/config.ts(第 1、2 轮已出过发现,本轮又有 1 条);packages/cli/src/config/config.projectRuntimeReloader.test.ts(第 2 轮已出过发现,本轮又有 1 条),另有 1 个文件。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)

— qwen3.8-max via Qwen Code /review (v0.22.2)

loadedSettings: LoadedSettings | undefined,
cwd: string,
): LoadedSettings {
return loadedSettings?.user.path ? loadedSettings : loadSettings(cwd);

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] R1-4: (fix-induced) The round-2 fix for the stale-cwd persistence closure (afbf358) re-created the callback per move, but switched it to read currentRules from the captured in-memory nextSettings instead of fresh-loading from disk — and that opens two holes at this site. In --safe-mode, prepare() loads the target with skipWorkspaceSettings, so the workspace scope is empty in memory while user.path stays real and resolvePersistenceSettings returns that instance: after /cd, approving a tool with "Always allow in this project" writes setValue(Workspace, 'permissions.allow', [newRule]) over the empty in-memory array, so the target project's existing allow rules are silently wiped from its .qwen/settings.json — deterministic data loss for every future session in that project. In normal mode the same in-memory read leaves a ~300 ms race: two sessions in one project, session A persists a rule, session B persists before its settings watcher refreshes, and B's stale read writes [...[], ruleB], silently deleting A's rule. The round-1 crash this site originally fixed stays fixed; both holes above are introduced by the callback extraction.

Witness (probe, real reloader + settings stack at head 6d1bdd6):

safe-mode arm: disk after onPersistPermissionRule('project','allow',…) = ["run_shell_command(ls *)"]
               (fixture had ["rule-A","rule-B"]); workspace-active-gated flip keeps all three
race arm:      window open -> disk = ["run_shell_command(rule-b)"] (rule-a deleted before any watcher event)
               watcher catch-up measured at 305 ms; fresh-load-per-persist mutant keeps both rules

Re-read the target scope from disk before the read-modify-write — at the top of createPermissionRulePersistenceCallback:

    currentSettings.reloadScopeFromDisk(settingScope);
    const currentRules: string[] =
      currentSettings.forScope(settingScope).settings.permissions?.[ruleType] ??
      [];

(or gate resolvePersistenceSettings on workspaceSettingsActive so a skipWorkspaceSettings load falls back to a fresh loadSettings(targetDir) — that also fixes the safe-mode arm without touching the callback).

Fix witness: a reloader test — write project B with existing permissions.allow, prepare/commit with { safeMode: true }, invoke onPersistPermissionRule('project','allow','R'), assert the file on disk contains the existing rules AND R; removing the disk reload must turn it red.

中文说明

(修复引入)第 2 轮对"持久化闭包捕获过期 cwd"的修复(afbf358)把回调改为每次迁移重建,但 currentRules 从捕获的内存态 nextSettings 读取,而不再从磁盘新鲜加载——在同一位置引入了两个漏洞。--safe-modeprepare()skipWorkspaceSettings 加载目标,工作区作用域在内存中为空而 user.path 仍为真实路径,resolvePersistenceSettings 返回该实例:/cd 后用"始终允许(本项目)"批准工具会以 setValue(Workspace, 'permissions.allow', [newRule]) 覆盖空数组,把目标项目 .qwen/settings.json 里已有的 allow 规则静默清空——对该项目后续所有会话都是确定性的数据丢失。普通模式下同一内存读取留下约 300ms 竞态:同项目两个会话,A 持久化一条规则,B 在其设置监听刷新前持久化,B 的过期读到 [...[], ruleB],静默删除 A 的规则。该位置原先修复的第 1 轮崩溃仍然保持修复;以上两个漏洞均由回调抽取引入。

证据(探针,真实 reloader + 设置栈,head 6d1bdd6):safe 模式分支——持久化后磁盘 = ["run_shell_command(ls *)"](fixture 原有 ["rule-A","rule-B"]);按工作区激活态门控的对照保留全部三条。竞态分支——窗口打开时磁盘 = ["run_shell_command(rule-b)"](rule-a 在任何监听事件前被删除);监听追平实测 305ms;"每次持久化新鲜加载"的突变体在两个分支都保留两条规则。

建议:在 createPermissionRulePersistenceCallback 的读-改-写之前先从磁盘重读目标作用域(currentSettings.reloadScopeFromDisk(settingScope));或让 resolvePersistenceSettingsworkspaceSettingsActive 门控,使 skipWorkspaceSettings 的加载回退到新鲜的 loadSettings(targetDir)。修复见证:reloader 测试——目标项目预置 permissions.allow,以 { safeMode: true } prepare/commit 后调用 onPersistPermissionRule,断言磁盘同时包含原有规则与新规则;去掉磁盘重读后应变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

// and expand their commands against it. Same step serve's
// `workspaceReload` performs; bare mode never loads env at all.
if (!bareMode) {
reloadEnvironment(nextSettings.merged, targetDir, effectiveTrust);

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] R3-1: commit()'s reloadEnvironment(nextSettings.merged, targetDir, effectiveTrust) rewrites process-global process.env from a per-session /cd — deleting previously-tracked keys absent from the target's env files and force-writing the target's keys. The ACP child under qwen serve is long-lived and hosts multiple sessions (the this.sessions map; the PR's own contextFileNamesForCwd only makes sense when sessions in one child have different cwds), and sessionCd has no sibling-session gate. A sibling session still bound to the old workspace then spawns an MCP stdio server or a shell child (mcp-client inherits process.env at spawn) and inherits the relocated session's environment while missing its own keys — cross-project secret crossover that did not exist pre-PR (only the child-wide workspaceReload could touch env before, so sibling divergence was impossible).

Witness (probe driving the real reloader + environment modules at head 6d1bdd6):

PROBE afterCommit: {"KEY_X":"secret-from-x"}        (session B's /cd deleted boot-workspace KEY_W process-wide)
PROBE sibling child inherited: {"KEY_X":"KEY_X=secret-from-x"}   (sibling bound to W inherits X's secret, misses KEY_W)

Do not mutate process.env from a per-session /cd when the process can host sibling sessions — keep the target env in a per-session snapshot used when expanding that session's MCP/shell spawn env, or gate relocation on single-session processes; if the shared-process env churn is an accepted design decision, document it at the reloader.

Fix witness: a two-session ACP-child test — session A bound to W with KEY_W in env, session B /cds to X, then A spawns a child; assert the child's env still carries KEY_W and no X-only keys; removing the per-session isolation must turn it red.

中文说明

commit()reloadEnvironment(nextSettings.merged, targetDir, effectiveTrust) 以"每会话 /cd"为粒度改写进程级全局 process.env——删除目标 env 文件中不存在的既有跟踪键、强制写入目标的键。qwen serve 的 ACP 子进程长期存活并承载多个会话(this.sessions 映射;本 PR 自己的 contextFileNamesForCwd 只有在同一子进程内会话 cwd 各不相同时才有意义),而 sessionCd 没有兄弟会话门控。仍绑定旧工作区的兄弟会话随后启动 MCP stdio 服务器或 shell 子进程(mcp-client 在 spawn 时继承 process.env),会继承被迁移会话的环境、丢失自己的键——跨项目密钥串扰。PR 之前不存在该问题:只有整个子进程级的 workspaceReload 能触碰 env,兄弟分歧不可能发生。

证据(探针驱动真实 reloader + environment 模块,head 6d1bdd6):迁移提交后 {"KEY_X":"secret-from-x"}(会话 B 的 /cd 在进程范围删除了启动工作区的 KEY_W);兄弟会话启动的子进程继承 {"KEY_X":"KEY_X=secret-from-x"}(仍绑定 W 的兄弟继承了 X 的密钥、丢失 KEY_W)。

建议:当进程可能承载兄弟会话时,不要以每会话 /cd 改写 process.env——把目标 env 存为每会话快照、在展开该会话的 MCP/shell spawn env 时使用;或将迁移限制在单会话进程。若接受共享进程的 env 变动为设计决策,请在 reloader 处文档化。修复见证:双会话 ACP 子进程测试——A 绑定 W(env 含 KEY_W),B /cd 到 X 后 A 启动子进程,断言子进程 env 仍含 KEY_W 且无 X 专属键;去掉每会话隔离后应变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

Comment on lines +7024 to +7029
expect(prepare).toHaveBeenCalledWith(
newDir,
true,
ApprovalMode.AUTO,
expect.any(String),
);

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] R3-2: the new relocation suite leaves three apply/prepare contracts unpinned — this is one of its three locations. (1) prepare's 4th argument (the previous directory) is asserted only as expect.any(String) while cwdSpy is installed to return newDir before the move, so at test runtime oldDir === newDir: a future edit passing expected instead of oldDir stays green, and rollback() would then call reloadEnvironment(previousSettings.merged, previousDir=wrong) — reloading the TARGET's .env while settings restore to the old project. (2) The fixture sets loadMemoryFromIncludeDirectories: true (constructor default is false) but no test asserts getLoadMemoryFromIncludeDirectories(): deleting the assignment in applyProjectRuntimeConfig keeps all 599 core config tests green, and after /cd into a project with memory.loadFromIncludeDirectories: true the include-dir memory silently does not load. (3) Every fixture passes trustedFolder: true — exactly the constructor default — and nothing asserts isTrustedFolder(): deleting the trustedFolder re-application keeps the session trusted in a workspace the runtime declared untrusted.

Mutations verified green at head 6d1bdd6: oldDirexpected in the prepare call ('Tests 3 passed | 596 skipped'); deleted loadMemoryFromIncludeDirectories assignment ('Tests 599 passed').

Tighten the happy-path test: make cwdSpy return a distinct old directory first (mockReturnValueOnce(oldDir).mockReturnValue(newDir)) and assert expect(prepare).toHaveBeenCalledWith(newDir, true, ApprovalMode.AUTO, oldDir); add expect(config.getLoadMemoryFromIncludeDirectories()).toBe(true); add a trustedFolder: false fixture with expect(config.isTrustedFolder()).toBe(false).

Fix witness: the tightened test — each of the three mutations goes red against it.

中文说明

新增的迁移测试套件留下三个 apply/prepare 契约未固定——本条是其中一处。(1) prepare 的第 4 个参数(上一目录)只断言为 expect.any(String),且 cwdSpy 在迁移前就被设为返回 newDir,测试运行时 oldDir === newDir:未来把 expected 当作 oldDir 传入也不会变红,rollback() 会以错误的 previousDirreloadEnvironment——设置已回滚到旧项目、env 却重载了目标项目的 .env。(2) fixture 设了 loadMemoryFromIncludeDirectories: true(构造默认 false)但没有测试断言 getLoadMemoryFromIncludeDirectories():删除 applyProjectRuntimeConfig 中的赋值,599 个测试依旧全绿;/cd 到开启该选项的项目后,include 目录记忆静默不加载。(3) 所有 fixture 传 trustedFolder: true——恰为构造默认值——也没有断言 isTrustedFolder():删除 trustedFolder 再应用后,会话在运行时声明为不受信任的工作区里仍被视为可信。

在 head 6d1bdd6 验证突变均为绿:oldDirexpected('Tests 3 passed | 596 skipped');删除 loadMemoryFromIncludeDirectories 赋值('Tests 599 passed')。

建议:收紧 happy-path 测试——cwdSpy 首次返回不同的旧目录并断言 prepare 第 4 参的精确值;补 getLoadMemoryFromIncludeDirectories()).toBe(true);以 trustedFolder: false fixture 断言 isTrustedFolder()).toBe(false)。修复见证:收紧后的测试使上述三个突变全部变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

config: {
trustedFolder: true,
includeDirectories: ['/path/to/project-b-include'],
loadMemoryFromIncludeDirectories: 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] R3-2: the new relocation suite leaves three apply/prepare contracts unpinned — this is one of its three locations. (1) prepare's 4th argument (the previous directory) is asserted only as expect.any(String) while cwdSpy is installed to return newDir before the move, so at test runtime oldDir === newDir: a future edit passing expected instead of oldDir stays green, and rollback() would then call reloadEnvironment(previousSettings.merged, previousDir=wrong) — reloading the TARGET's .env while settings restore to the old project. (2) The fixture sets loadMemoryFromIncludeDirectories: true (constructor default is false) but no test asserts getLoadMemoryFromIncludeDirectories(): deleting the assignment in applyProjectRuntimeConfig keeps all 599 core config tests green, and after /cd into a project with memory.loadFromIncludeDirectories: true the include-dir memory silently does not load. (3) Every fixture passes trustedFolder: true — exactly the constructor default — and nothing asserts isTrustedFolder(): deleting the trustedFolder re-application keeps the session trusted in a workspace the runtime declared untrusted.

Mutations verified green at head 6d1bdd6: oldDirexpected in the prepare call ('Tests 3 passed | 596 skipped'); deleted loadMemoryFromIncludeDirectories assignment ('Tests 599 passed').

Tighten the happy-path test: make cwdSpy return a distinct old directory first (mockReturnValueOnce(oldDir).mockReturnValue(newDir)) and assert expect(prepare).toHaveBeenCalledWith(newDir, true, ApprovalMode.AUTO, oldDir); add expect(config.getLoadMemoryFromIncludeDirectories()).toBe(true); add a trustedFolder: false fixture with expect(config.isTrustedFolder()).toBe(false).

Fix witness: the tightened test — each of the three mutations goes red against it.

中文说明

新增的迁移测试套件留下三个 apply/prepare 契约未固定——本条是其中一处。(1) prepare 的第 4 个参数(上一目录)只断言为 expect.any(String),且 cwdSpy 在迁移前就被设为返回 newDir,测试运行时 oldDir === newDir:未来把 expected 当作 oldDir 传入也不会变红,rollback() 会以错误的 previousDirreloadEnvironment——设置已回滚到旧项目、env 却重载了目标项目的 .env。(2) fixture 设了 loadMemoryFromIncludeDirectories: true(构造默认 false)但没有测试断言 getLoadMemoryFromIncludeDirectories():删除 applyProjectRuntimeConfig 中的赋值,599 个测试依旧全绿;/cd 到开启该选项的项目后,include 目录记忆静默不加载。(3) 所有 fixture 传 trustedFolder: true——恰为构造默认值——也没有断言 isTrustedFolder():删除 trustedFolder 再应用后,会话在运行时声明为不受信任的工作区里仍被视为可信。

在 head 6d1bdd6 验证突变均为绿:oldDirexpected('Tests 3 passed | 596 skipped');删除 loadMemoryFromIncludeDirectories 赋值('Tests 599 passed')。

建议:收紧 happy-path 测试——cwdSpy 首次返回不同的旧目录并断言 prepare 第 4 参的精确值;补 getLoadMemoryFromIncludeDirectories()).toBe(true);以 trustedFolder: false fixture 断言 isTrustedFolder()).toBe(false)。修复见证:收紧后的测试使上述三个突变全部变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

const complete = vi.fn().mockResolvedValue(undefined);
const prepare = vi.fn().mockResolvedValue({
config: {
trustedFolder: 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] R3-2: the new relocation suite leaves three apply/prepare contracts unpinned — this is one of its three locations. (1) prepare's 4th argument (the previous directory) is asserted only as expect.any(String) while cwdSpy is installed to return newDir before the move, so at test runtime oldDir === newDir: a future edit passing expected instead of oldDir stays green, and rollback() would then call reloadEnvironment(previousSettings.merged, previousDir=wrong) — reloading the TARGET's .env while settings restore to the old project. (2) The fixture sets loadMemoryFromIncludeDirectories: true (constructor default is false) but no test asserts getLoadMemoryFromIncludeDirectories(): deleting the assignment in applyProjectRuntimeConfig keeps all 599 core config tests green, and after /cd into a project with memory.loadFromIncludeDirectories: true the include-dir memory silently does not load. (3) Every fixture passes trustedFolder: true — exactly the constructor default — and nothing asserts isTrustedFolder(): deleting the trustedFolder re-application keeps the session trusted in a workspace the runtime declared untrusted.

Mutations verified green at head 6d1bdd6: oldDirexpected in the prepare call ('Tests 3 passed | 596 skipped'); deleted loadMemoryFromIncludeDirectories assignment ('Tests 599 passed').

Tighten the happy-path test: make cwdSpy return a distinct old directory first (mockReturnValueOnce(oldDir).mockReturnValue(newDir)) and assert expect(prepare).toHaveBeenCalledWith(newDir, true, ApprovalMode.AUTO, oldDir); add expect(config.getLoadMemoryFromIncludeDirectories()).toBe(true); add a trustedFolder: false fixture with expect(config.isTrustedFolder()).toBe(false).

Fix witness: the tightened test — each of the three mutations goes red against it.

中文说明

新增的迁移测试套件留下三个 apply/prepare 契约未固定——本条是其中一处。(1) prepare 的第 4 个参数(上一目录)只断言为 expect.any(String),且 cwdSpy 在迁移前就被设为返回 newDir,测试运行时 oldDir === newDir:未来把 expected 当作 oldDir 传入也不会变红,rollback() 会以错误的 previousDirreloadEnvironment——设置已回滚到旧项目、env 却重载了目标项目的 .env。(2) fixture 设了 loadMemoryFromIncludeDirectories: true(构造默认 false)但没有测试断言 getLoadMemoryFromIncludeDirectories():删除 applyProjectRuntimeConfig 中的赋值,599 个测试依旧全绿;/cd 到开启该选项的项目后,include 目录记忆静默不加载。(3) 所有 fixture 传 trustedFolder: true——恰为构造默认值——也没有断言 isTrustedFolder():删除 trustedFolder 再应用后,会话在运行时声明为不受信任的工作区里仍被视为可信。

在 head 6d1bdd6 验证突变均为绿:oldDirexpected('Tests 3 passed | 596 skipped');删除 loadMemoryFromIncludeDirectories 赋值('Tests 599 passed')。

建议:收紧 happy-path 测试——cwdSpy 首次返回不同的旧目录并断言 prepare 第 4 参的精确值;补 getLoadMemoryFromIncludeDirectories()).toBe(true);以 trustedFolder: false fixture 断言 isTrustedFolder()).toBe(false)。修复见证:收紧后的测试使上述三个突变全部变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

// known. Same split as `collectWorkspaceMemoryStatus`.
if (contextFileName) {
return scope === 'workspace'
? path.join(projectRoot, contextFileName)

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] R2-10: still stands — this contextFileName join has no containment validation, and the option's doc comment invites callers to pass session names that originate in unvalidated project settings: resolveContextFileNames trims and filters non-strings but rejects no separators. A project's .qwen/settings.json sets context.fileName: '../escape.md' (settings load performs no schema validation); any wiring of the session name into writeWorkspaceContextFile (the wiring R2-9 asks for) resolves path.join(projectRoot, '../escape.md') outside the workspace root. Round-2 probe: writeWorkspaceContextFile({scope:'workspace', mode:'append', projectRoot: <tmp>/workspace, contextFileName: '../escape.md'})ESCAPE: filePath=<tmp>/escape.md. Validate containment before the join — reject (or strip to basename) any name containing a separator or resolving outside projectRoot, and apply the same check in resolveContextFileNames where settings-sourced names enter; fix witness: a test passing '../escape.md' asserting the write is refused or stays inside projectRoot — red without the check.

中文说明

仍然成立——该 contextFileName 拼接没有包含性校验,且选项的文档注释邀请调用方传入源自未校验项目设置的会话名:resolveContextFileNames 会 trim 并过滤非字符串,但不拒绝路径分隔符。项目的 .qwen/settings.jsoncontext.fileName: '../escape.md'(设置加载不做 schema 校验);任何把会话名接入 writeWorkspaceContextFile 的接线(正是 R2-9 所要求的)都会把 path.join(projectRoot, '../escape.md') 解析到工作区根之外。第 2 轮探针:writeWorkspaceContextFile({scope:'workspace', mode:'append', projectRoot: <tmp>/workspace, contextFileName: '../escape.md'})ESCAPE: filePath=<tmp>/escape.md。建议在拼接前做包含性校验——拒绝(或剥成 basename)任何含分隔符或解析到 projectRoot 之外的名字,并在设置来源名字进入的 resolveContextFileNames 处做同样检查;修复见证:传入 '../escape.md' 的测试断言写入被拒绝或保持在 projectRoot 内——没有该校验时应为红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

return;
}
toolRegistry.registerTool(new CreateSubSessionTool(config));
toolRegistry.registerSessionTool(new CreateSubSessionTool(config));

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] R1-12: still stands — Session's switch from registerTool/registerPermissionDeferredFactory to the session-owned variants is pinned only for capture_screen_context; create_sub_session (here and the deferred branch), speak_to_user, and the live-voice tool batch assert only the plain delegate mocks (Session.test.ts maps registerSessionTool/registerSessionPermissionDeferredFactory to their delegates). Reverting create_sub_session or speak_to_user to plain registerTool keeps Session.test.ts green, yet the next /cd's replaceCoreToolsFrom would then dispose the tool mid-session and drop the model's speak_to_user/create_sub_session surface. Mirror the capture-screen-context assertion for these registrations, asserting the session variants directly (not their delegates); fix witness: reverting Session.ts to the plain registrations at those sites must turn the new assertions red.

中文说明

仍然成立——Session 从 registerTool/registerPermissionDeferredFactory 切换到会话自有变体,目前只为 capture_screen_context 固定;create_sub_session(此处及延迟分支)、speak_to_user 与实时语音工具批只断言了普通的委托 mock(Session.test.ts 把 registerSessionTool/registerSessionPermissionDeferredFactory 映射到其委托)。把 create_sub_sessionspeak_to_user 回退为普通 registerTool,Session.test.ts 仍为绿,而下一次 /cdreplaceCoreToolsFrom 会在会话中途处置该工具,模型将失去 speak_to_user/create_sub_session 入口。建议仿照 capture-screen-context 的断言为这些注册直接断言会话变体(而非委托);修复见证:把 Session.ts 这些位置回退为普通注册后,新断言应变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

return this.targetDir;
}

private applyProjectRuntimeConfig(runtime: ProjectRuntimeConfig): void {

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] R1-21: still stands — class: applyProjectRuntimeConfig re-applies ~50 settings-derived knobs but silently skips other settings-derived requiresRestart: false fields (maxToolCallsPerTurn, gitCoAuthor, and siblings), so after /cd the session keeps the OLD project's values for them — the user sees project B as active while behavior still comes from project A, the exact mixed runtime issue #10173 asks to avoid. The author acknowledged the class is real and deferred the audit ('Not in this commit… leaving the thread open'). Audit the requiresRestart: false, settings-derived fields and either add them to ProjectRuntimeConfig/applyProjectRuntimeConfig or document each deliberate exclusion at the interface; fix witness: per-field relocation assertions mirroring the 'apply the prepared project runtime' suite — each red without the corresponding assignment.

中文说明

仍然成立——类级问题:applyProjectRuntimeConfig 重新应用约 50 个来自设置的旋钮,但静默跳过其他来自设置且 requiresRestart: false 的字段(maxToolCallsPerTurngitCoAuthor 等),/cd 后这些字段仍是"旧"项目的值——用户看到项目 B 已生效、部分行为却仍来自项目 A,正是 issue #10173 要求避免的混合运行时。作者已确认该类问题真实存在并推迟审计("本提交不做……保留线程")。建议审计所有 requiresRestart: false 的设置派生字段,要么加入 ProjectRuntimeConfig/applyProjectRuntimeConfig,要么在接口处逐一文档化有意的排除;修复见证:仿照 'apply the prepared project runtime' 套件的逐字段迁移断言——缺少对应赋值时各自为红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

this.notifyChangeListeners();
}

async refreshForProjectChange(): Promise<void> {

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] R1-25: still stands — SubagentManager.refreshForProjectChange and its twin SkillManager.refreshForProjectChange (skill-manager.ts:516) have no direct tests — they appear in test code only as bare vi.fn().mockResolvedValue(undefined) mocks in the relocation suite, which never asserts either is called. Deleting either call from relocateWorkingDirectory (or breaking a method body — e.g. dropping the subagent failure branch that deletes the stale 'project' cache) leaves every existing test green, and a session that /cds keeps project A's skills/subagents launchable in project B. The author deferred ('coverage work on code this PR did not change… leaving the thread open'). Add direct unit tests — subagent-manager: refreshCache rejects, assert the 'project' cache entry is evicted, listeners notified, and the error re-thrown; skill-manager: cache re-scan for the new project and watcher re-rooting — plus expect(...refreshForProjectChange).toHaveBeenCalledOnce() in the relocation test; deleting either call site must turn them red.

中文说明

仍然成立——SubagentManager.refreshForProjectChange 与其孪生方法 SkillManager.refreshForProjectChange(skill-manager.ts:516)没有直接测试——在迁移测试套件中它们只以裸 vi.fn().mockResolvedValue(undefined) mock 出现,且从不断言它们被调用。从 relocateWorkingDirectory 删除任一调用(或破坏方法体——例如丢弃 subagent 失败分支中删除过期 'project' 缓存的逻辑)都不会让任何现有测试变红,/cd 的会话将继续在项目 B 中启动项目 A 的 skills/subagents。作者已推迟("属于本 PR 未改动代码的覆盖工作……保留线程")。建议补直接单测——subagent-manager:refreshCache 拒绝时断言 'project' 缓存条目被清除、监听者被通知、错误被重新抛出;skill-manager:新项目的缓存重扫与 watcher 重定根——并在迁移测试中补 expect(...refreshForProjectChange).toHaveBeenCalledOnce();删除任一调用点后应变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

await this.notifyChangeListeners();
}

async refreshForProjectChange(): Promise<void> {

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] R1-25: still stands — SubagentManager.refreshForProjectChange and its twin SkillManager.refreshForProjectChange (skill-manager.ts:516) have no direct tests — they appear in test code only as bare vi.fn().mockResolvedValue(undefined) mocks in the relocation suite, which never asserts either is called. Deleting either call from relocateWorkingDirectory (or breaking a method body — e.g. dropping the subagent failure branch that deletes the stale 'project' cache) leaves every existing test green, and a session that /cds keeps project A's skills/subagents launchable in project B. The author deferred ('coverage work on code this PR did not change… leaving the thread open'). Add direct unit tests — subagent-manager: refreshCache rejects, assert the 'project' cache entry is evicted, listeners notified, and the error re-thrown; skill-manager: cache re-scan for the new project and watcher re-rooting — plus expect(...refreshForProjectChange).toHaveBeenCalledOnce() in the relocation test; deleting either call site must turn them red.

中文说明

仍然成立——SubagentManager.refreshForProjectChange 与其孪生方法 SkillManager.refreshForProjectChange(skill-manager.ts:516)没有直接测试——在迁移测试套件中它们只以裸 vi.fn().mockResolvedValue(undefined) mock 出现,且从不断言它们被调用。从 relocateWorkingDirectory 删除任一调用(或破坏方法体——例如丢弃 subagent 失败分支中删除过期 'project' 缓存的逻辑)都不会让任何现有测试变红,/cd 的会话将继续在项目 B 中启动项目 A 的 skills/subagents。作者已推迟("属于本 PR 未改动代码的覆盖工作……保留线程")。建议补直接单测——subagent-manager:refreshCache 拒绝时断言 'project' 缓存条目被清除、监听者被通知、错误被重新抛出;skill-manager:新项目的缓存重扫与 watcher 重定根——并在迁移测试中补 expect(...refreshForProjectChange).toHaveBeenCalledOnce();删除任一调用点后应变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/self-reported The linked issue was opened by the PR author (self-reported)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(cd): Reload project-scoped runtime configuration after /cd

3 participants