dev-manager-tauri/.blueprint/modules/agent-client-parity.md
lanrtop 7ef924127a
All checks were successful
Push & PR Check / check (push) Successful in 1m3s
Push & PR Check / check (pull_request) Successful in 1m3s
docs(blueprint/agent-client-parity): P8c 补审查处置与 known_limits,P8d 记审查转入项(P8c)
- review:独立审查 9 条发现逐条处置(7 条已修并补测试,2 条信息项转入 known_limits / P8d)。
- 自查条目「任一目标文件变化 digest 就变 ✅」改为准确语义(只绑定计划会写入或核对的文件,
  update 另绑定写入前哈希);按纪律报备:改的是验收表述,断言只增不减。
- known_limits:onboard 反向依赖投影层(过渡态)、Codex 信任静态预判的边界、shim unavailable 不阻断、
  bypassPermissions 未单列、plan_machine_steps 组装未单测。
- P8d 卡新增「审查转入」:bypass_permissions 单列、machine_changes 补产品组与 buff、.git/hooks,待统筹决定是否并入 acceptance。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:54:04 +09:00

337 lines
82 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
id: agent-client-parity
name: 三端客户端对等
status: in_progress
progress: 0
---
## 概述
子项目在 Claude Code、Codex、DSH(DeepSeek Harness 桌面版)三端都能继续开发:项目状态只存在于 Git 与炼境,客户端是可替换的投影。炼境适配的是**客户端层**(规则文件、skill 位置、MCP 连接、认证、无头模式),不是模型层——在某客户端内切换 DeepSeek 等模型属 `codex-model-switch`,与本模块正交。
## 定位:agent 客户端兼容层(类比 Web 框架之于浏览器)
炼境对 agent 客户端的角色,如同 Web 框架对浏览器:内容按开放标准写一次(AGENTS.md、MCP、SKILL.md),各客户端私有机制的差异由炼境的适配层吸收,并持续实测"这些机制此刻是否仍有效"。
| Web 世界 | 炼境 |
| --- | --- |
| 浏览器 | agent 客户端(Claude Code、Codex、DSH) |
| Web 标准 | AGENTS.md、MCP、SKILL.md 格式 |
| 浏览器私有实现差异 | skill 目录、MCP 配置位置、helper 执行 shell、DSH `!!js` 等 |
| 框架兼容层 / polyfill | 炼境各客户端适配器(skill 双投影、统一 shim、各端注入) |
| Can I Use 兼容性表 | 三端对等矩阵(A–F) |
| 跨浏览器自动化测试 | 三端无头探针(P4) |
skill 只是五个接入面之一,兼容层覆盖全部五面。
类比不成立处(也是本模块的特殊难点):agent 客户端迭代远快于浏览器且机制常无文档,所以更依赖持续实测而非读规范;"浏览器"本身是智能的,部分兼容可用 skill 里的降级说明解决,但可靠性低于代码适配,只作兜底。
## 设计原则
- **标准优先,私有机制只进适配层**:项目内容只写标准形态,客户端差异不得渗入项目内容。
- **检测行为而非嗅探版本**:用无头实测判定机制是否有效,不凭版本号或文档推断(H1–H4 即此法)。
- **兼容性表必须可自动重跑**:客户端几乎每周更新,人工结论很快过期。
- **polyfill 可退役**:客户端原生支持标准后撤掉对应适配(例如 Claude 若开始读 `.agents/skills`,即撤 `.claude/skills` 投影)——对应 AGENTS.md「管家准入与退出」的退出规则。
- **渐进增强**:核心能力在最弱客户端也可用(落地文件兜底),高级能力只在支持的客户端启用(明文读取有界例外)。
- **五个接入面**逐一对齐:① 项目规则(AGENTS.md)② 工作流 skill ③ MCP 连接炼境 ④ 敏感取值 ⑤ 交付信号(Git + lefthook + Gitea,已与客户端无关)。
- **先路由、后认证**:客户端必须连项目专用地址 `/mcp/projects/<project_id>`;全局 `/mcp` 只作兼容,且暴露跨项目管理工具,不应作为子项目日常入口(最小权限)。
- **无头模式 = 各客户端的 agent 按钮**:`claude -p`、`codex exec`、`dsh headless`。对等性用同一任务、同一断言在三端无头实测,不靠文档推断。
- 不写静态 token 进任何配置文件;认证靠各客户端的动态机制或不需认证的落地路径。
## 基线证据(2026-09-30,enterprise-system,三端无头同一只读任务)
| 检查项 | Codex 0.155 | Claude Code 2.1.173 | DSH 桌面版 0.2.0-rc.2 |
| --- | --- | --- | --- |
| A 加载项目规则 | ✅ 摘录 AGENTS.md | ✅ CLAUDE.md 经 `@AGENTS.md` 引用 | ✅ 原生读取 AGENTS.md |
| B lian-jing skill | ✅ | ❌ 看不到(未投影到 `.claude/skills/`) | ✅ 读取 `.agents/skills`,与 Codex 共用 |
| C MCP 工具 | ✅ 32 个,含敏感读取 | ⚠️ 35 个全局工具,无开发变量工具 | ⚠️ 35 个全局工具,无开发变量工具 |
| D get_diagnostics | ✅ | ✅ | ✅ |
| E 列出开发变量 | ✅ | ❌ 工具不存在 | ❌ 工具不存在 |
| F 敏感读取 | ✅ | ❌ 工具不存在 | ❌ 工具不存在 |
| 耗时 | 56 s | 138 s | 25 s |
三端均未泄露凭据。连接现状:Codex 用项目 `.codex/config.toml`(项目地址 + helper);Claude 仅在 `~/.claude.json` user scope 注册全局 `/mcp`(SSE);DSH 在机器级 `~/.dsh/cordis.patch.yml` 注册全局 `/mcp`,配置无法按项目拆分,但支持 `!!js` 求值与每次调用的 `--patch` 叠加层。
## 实施前待验证假设(闸门)
- **H1 DSH**:`!!js` 能否在加载时读取当前目录的 `.lian-jing/context.json` 算出项目地址,并执行 helper 生成认证头。
- **H2 Claude**:Windows 上 `headersHelper` 的执行方式与调用写法;local scope 条目覆盖 user scope 全局注册。
- **H3 Claude skill**:项目 `.claude/skills/<name>/SKILL.md` 能被无头会话发现。
- **H4 Codex**:`http_headers_helper` 中 `%LOCALAPPDATA%` 能被 `cmd /C` 展开。
任一假设不成立:回到设计,不退回写静态 token 或本机绝对路径。
### 假设实测结果(2026-09-30,Workflow 并行实验 + 独立对抗复核)
- **H1 ✅ 成立(复核未推翻)**:DSH `--patch` 叠加层即可,不改 `~/.dsh`。`!!js` 在主进程加载时求值,`require` 不可用但 `process.getBuiltinModule('fs'|'path'|'crypto'|'child_process')` 可用;`process.cwd()` = 启动目录,向上查找 `.lian-jing/context.json` 算项目路由(子目录启动亦可);`execFileSync` 直接执行 helper(不经 shell)生成 Authorization;`disabled: !!js` 在项目外不挂载且不求值 config。helper 失败时安全降级(只写一行 stderr、读取 403、会话不崩)。**限制**:桌面 GUI 模式下 cwd 不随工作区切换,本方案目前只证实适用于在项目目录启动的 headless/CLI。
- **H3 ✅ 成立(复核未推翻)**:Claude 发现 `<项目>/.claude/skills/<name>/SKILL.md`(向上查找到 git 根为止,git 根以上不加载);**不读 `.agents/skills`**,所以必须投影一份到 `.claude/skills`。`-p` 模式下目录 trust 不影响 skill 加载。
- **H4 ✅ 成立(第二轮零凭据实测)**:首轮仅 cmd 层模拟、被复核判证据不足;第二轮用哑 helper + 本地请求记录器直接观测 Codex 0.155 自身行为:`"%LOCALAPPDATA%\...\hdr.cmd"` 被执行且输出成为请求头(记录器收到 `codex-mcp-client` 携带的标记头);`%NOT_A_REAL_VAR%` 不执行;父进程导出的自定义变量 `%LJPROBE_BASE%` 不执行(证实 env_clear 白名单);把 `LOCALAPPDATA` 指向替身目录后执行的是替身目录里的脚本(因果对照);cmd 独有的 `%LOCALAPPDATA:~0%` 子串语法也生效(展开由 cmd.exe 完成);helper 非零退出时连接不建立。
- **H2 ✅ 成立(第二轮零凭据实测)**:Claude Code 2.1.173 在 Windows 经 **cmd.exe** 执行 headersHelper——带引号反斜杠路径、正斜杠路径、`%TEMP%`、显式 `bash x.sh` 均执行且输出成为请求头;`$TEMP/...` 不执行,`%USERNAME%` 展开而 `$USERNAME` 保持字面。**local scope 覆盖 user scope**:init 事件中 `lian-jing` 工具数 35(全局)→ 加 local 条目后 32(项目路由)→ 删除后回到 35。经 `--mcp-config` 传入的 helper 在未信任目录也执行(文档所述 trust 限制针对项目 `.mcp.json` 来源)。`-p` 需设 `MCP_CONNECTION_NONBLOCKING=false` 才会等 MCP 连上再发 init。
- 第二轮两项的对抗复核 agent 未能运行(见下条),结论由主会话直接核对原始日志(记录器日志由独立进程写入)得出,未经第二个 agent 复现。
- **结论:Codex 与 Claude 都经 cmd.exe 执行 helper,可共用同一个 shim 写法**;DSH 经 `!!js` + `execFileSync` 直接调用同一 shim。(入口位置 2026-10-01 由 `%LOCALAPPDATA%\lian-jing\bin\` 迁至 `%USERPROFILE%\.lian-jing\bin\`,原因见 P3 返工记录。)
### 实验附带发现
- **服务端缺陷**:`/mcp/projects/<任意字符串>` 都返回完整 32 个项目工具,不校验项目是否存在;应对未知 project_id 明确拒绝。
- **零凭据验证法**(后续实验与 P4 探针采用):认证链路用 `read_project_test_account(name="LJ_PROBE_NONEXISTENT")` 判定——认证通过返回"开发需求未登记",失败返回 403,凭据不进任何会话;客户端 helper 机制用"哑 helper(写运行标记 + 输出假请求头)+ 本地 HTTP 记录器"判定,全程不碰真 token。
- 模型自报的工具数量不可靠(同一路由报出 27~38 个),计数以 `curl tools/list` 真值为准(项目路由 32、全局 35)。
- 在 Claude 会话中嵌套无头 Claude 须先去掉父会话注入的 `CLAUDECODE`、`CLAUDE_CODE_*` 环境变量并 `< /dev/null`;在 Git Bash 中传 `/斜杠命令` 需设 `MSYS_NO_PATHCONV=1`,否则提示词被改写成 Windows 路径导致误判。
- **实验编排教训**:Workflow 子 agent 超过 180 秒无进展即判卡死并从头重试,而单次真实客户端无头会话常需 1~3 分钟,导致 H2/H4 共 4 次整轮重跑、无一产出结论(实验本身已完成并落盘日志)。需要启动真实客户端会话的实验应由主会话直接执行,或把长调用拆到后台任务、agent 只读结果。
## 任务拆解
### ✅ P1 — 统一连接项目专用地址(Claude + DSH)
- status: done
- complexity: M
- files: src-tauri/src/mcp_inject.rs, docs/ai-context/agent-button-system.md
- depends: H1, H2
- acceptance: "agent_verify: [三端无头探针] 在 enterprise 目录 Claude 与 DSH 的 C 项均看到开发变量工具(项目工具集),E 项成功;项目目录外的会话不获得他项目工具。"
- 注:吸收原 project-dev-variables V17(Claude 以 local scope + headersHelper 取值)。
- verification: "2026-10-01 Enterprise 三端无头验收(零凭据:F 项以 read_project_test_account(name=LJ_PROBE_NONEXISTENT) 判定认证,返回「开发需求未登记」即认证通过):Codex 59s、Claude 40~56s、DSH 11~14s,三端 B/C/D/E/F 全部通过(基线时 Claude、DSH 的 C/E/F 失败、Claude 的 B 失败)。Claude 以 init 事件客观确认 lian-jing 32 个项目工具且含开发变量工具;claude mcp list 显示项目地址 ✔ Connected。DSH 托管块 url 表达式在项目根、子目录、带尾分隔符路径解析为项目地址,在上级/无关目录回退全局;控制面 null 哨兵命中即停。未知 project_id 返回 404。"
- review: "六轮对抗审查(首轮 9 项、第二轮 13 条、第三轮 10 条、最终 10 条、收尾 13 条,均已处理)。Claude 条目写入方式三度调整:CLI+后台队列(cwd 依赖与竞态)→ 直接编辑 ~/.claude.json(须复刻 Claude 的 git 根/worktree/NFC 键推导、.claude.json.lock、CLAUDE_CONFIG_DIR 等私有机制)→ 只读扫描认领 + CLI 写入(扫描结论不代表本目录实际生效条目,共享键下会误删他项目)→ 最终定为『Claude 自己的回答为准』:快路径只认项目根或其祖先键下的一致托管条目,其余在项目目录执行 claude mcp get 取实际生效条目,按归属(本项目托管/他项目托管/手写/无)决策,写入经 claude CLI、失败回滚;涉及 CLI 的 Tauri 命令改到线程池执行,删组/移出组先删成员关系再清配置。实测(enterprise):稳态注入 174ms 不起 CLI;缺失 → 写入、过期 helper → 刷新、手写条目 → 保留,claude mcp get 均为 Local 且 ✔ Connected;经 shim 零凭据探针认证通过。"
- known_limits: "DSH 项目路由只对在项目目录启动的 headless 生效(2026-10-01 收紧:web/acp/tui 一个进程可服务多个工作区,按启动目录会让别的工作区会话连到该项目并带上认证,属跨项目越权,故与桌面版一样回退全局;判定用 profileContext.name,DSH 0.1.x 无该服务时退回看启动参数)。桌面 GUI 只有一个 Host 进程、启动目录固定为 profile 目录,按工作区路由需会话级插件,见 P9;claude mcp list 会提示 lian-jing 同时存在 user 与 local 两个作用域(user 级全局为有意保留的兜底);项目目录已被移动或删除后,Claude 中的残留托管条目无法经 CLI 定位,只记录 warn 与手工清理命令,不直接改写 ~/.claude.json;共享 Claude 项目键的嵌套项目/worktree 只有键所属项目的条目生效,他项目不改动、只记 warn;非稳态同步依赖 claude mcp get 的人类可读输出(格式无法识别时按未知处理、不写入),Claude 改输出格式会退化为只告警。"
### ✅ P2 — Claude skill 投影
- status: done
- complexity: S
- files: src-tauri/src/commands/onboarding.rs, src-tauri/resources/onboarding/
- depends: H3
- acceptance: "agent_verify: [cargo test onboarding] 接入包同时写 .claude/skills/lian-jing/SKILL.md,内容与 .agents/skills 单一来源一致;[Claude 无头探针] B 项看到 lian-jing。"
- verification: "2026-10-01:同一模板双投影到 .agents/skills 与 .claude/skills;verify_onboarding 新增「Claude 炼境技能已分发」(检查链 12 项);base-onboarding 升至 1.6.0,supported_clients 增 dsh;skill 降级步骤改为三端通用并提示「无开发变量工具即连的是全局地址」。新增测试 skill_is_projected_to_codex_and_claude_from_one_source;cargo test --locked 229 passed / 5 ignored。Enterprise 分发后 verify_onboarding 14/14,两份 skill 逐字节一致;Claude 无头会话 init 事件 skills 与 slash_commands 均含 lian-jing(基线探针时为不存在)。"
### ✅ P3 — 稳定 helper 入口,项目配置去本机路径
- status: done
- complexity: M
- files: src-tauri/src/mcp_inject.rs, src-tauri/src/lib.rs, src-tauri/src/commands/onboarding.rs
- depends: H4
- acceptance: "agent_verify: [cargo test] 炼境启动写/更新 %USERPROFILE%\.lian-jing\bin 下 shim 指向当前 exe;inject 后受跟踪文件不含本机绝对路径或用户名;[Codex 无头探针] F 项经 shim 成功,且在 Codex 包上下文(Invoke-CommandInDesktopPackage 运行 codex exec,等价 Codex Desktop)同样成功;换构建目录启动后无需重注入仍成功。"
- 注:即原 project-dev-variables V18;Claude headersHelper 与 DSH 共用同一 shim。
- verification: "2026-10-01:shim 写入 %LOCALAPPDATA%\lian-jing\bin\lianjing-mcp-header.cmd(CRLF,exe 路径 % 转义),Enterprise 的 .codex/config.toml 托管块 helper 为 %LOCALAPPDATA% 形式、不含本机路径与用户名;Codex 无头 F 项经 shim 认证通过。H4 实测证实 Codex 经 cmd 展开 %LOCALAPPDATA%(替身目录因果对照)。换构建目录启动后免重注入:shim 每次启动重写指向当前 exe(实测在 worktree 构建启动时自动改指向)。
返工(2026-10-01 人终验发现):Codex Desktop 会话里 lian-jing 完全未加载,日志为 headers helper exit 1。修复:入口迁至 %USERPROFILE%\.lian-jing\bin;旧入口迁移期同步保留,verify_onboarding 把 %LOCALAPPDATA% 写法报 Legacy。复验:四路零凭据探针(Codex 包内、Codex 包外、Claude、DSH 无头)均返回「开发需求未登记」,证实 USERPROFILE 在 Codex helper 环境白名单内。
根因更正(2026-10-02):首次判断「Codex Desktop(MSIX)看不见其安装后在 AppData 下新建的顶层目录」是错的——当时的对照目录本身也是在 Claude 桌面版的包环境里建的。真正原因:worktree 版炼境由 Claude 桌面版(MSIX,Claude_pzs8sxrjxfjjc)里的 agent 启动,继承了其包环境,写到 %LOCALAPPDATA%\lian-jing 的入口被 Windows 重定向进 %LOCALAPPDATA%\Packages\Claude_pzs8sxrjxfjjc\LocalCache\Local\,只有 Claude 的进程树看得到。对照:经 WMI 启动的非包进程在 AppData 下新建的顶层目录,Codex 包内可见;%USERPROFILE% 根不做这种重定向。入口迁至 %USERPROFILE% 因此仍是正解(与炼境在什么环境里启动无关);据错误前提加的「exe 在 AppData 下就复制副本」逻辑已按「退出」原则撤销(exe 总在真实路径上)。"
- known_limits: "旧 %LOCALAPPDATA% 入口仅为迁移期兼容,各子项目需重注入(会改动其受跟踪的 .codex/config.toml,需在子项目提交)。炼境若在 MSIX 应用进程树里启动,它写的其他 AppData 内容(如安装目录外的缓存)同样只对该应用可见,凡给其他客户端读的文件都不应放在 AppData。"
### 📋 P4 — 三端对等探针固化为炼境能力(2026-10-03 拆为 P4-0、P4a–P4e)
- status: todo
- complexity: L(已拆,见下)
- depends: P1, P2, P3
- acceptance: "由子卡承担:L1 零模型检查见 P4a,L3 无头会话的 [MCP 全局] 与 [CLI] pnpm probe:clients 见 P4e。原口径「[CLI] 一条命令对指定项目依次跑三端无头探针,输出 A–F × 三端矩阵与耗时,断言无凭据泄露;接入包升级后重跑结果可比对」整体保留在 P4e。"
- 设计:`docs/tasks/2026-10-03-p8-onboarding-oneclick-design.md` §6。分三层,便宜的先跑、前一层失败不跑后一层:L0 静态(verify_onboarding + clients 静态状态);L1 零模型(客户端自省 + 经真实 shim 的认证链重放,在项目路由上调 C–F 工具,约 5–10 秒,零额度);L3 无头会话(三端同一只读任务、同一断言,2–3 分钟、消耗额度,只在显式触发时跑,只开放在全局路由,须 confirmed=true)。每格标证据等级(static < client_introspection < protocol < session_event/server_trace),失败按环境 / 投影 / 适配器 / 模型归因。
- 准入证据:本机 %TEMP% 下已有约 20 个手工探针目录(probe、probe2、lj-p7-probe、lj-p9-* 等),每次重写探针是实测的重复摩擦。退出条件:三端都原生支持标准、且连续多个版本矩阵不变时,降低 L3 频率或撤销。
### ✅ P4-0 — 探针前置实测(spike)
- status: done
- complexity: S
- files: .blueprint/modules/agent-client-parity.md, src-tauri/tests/fixtures/client_probe/
- depends: P8-0
- acceptance: "agent_verify: [主会话 bash] 逐项给出结论并附原始日志路径——H-P8-1 新建的未信任目录里 Codex 是否加载项目 .codex/config.toml,codex mcp get lian-jing --json 能否反映项目配置与信任状态;H-P8-2 项目 settings 为 bypassPermissions 时,哪组参数(--permission-mode / --tools / --allowedTools / --disallowedTools)能把可调用工具收窄到 lian-jing 只读三件加 Skill 且 MCP 仍可调用;H-P8-3 Codex 在 -s read-only 下 MCP 调用是否被拦截,被拦截时可用的最窄参数组合;H-P8-4 claude -p 读到 system/init 后立即终止是否零模型调用(可行则 Claude 的 B、C 并入 L1);H-P8-5 Codex MCP 工具调用默认超时;H-P8-6 DSH headless 入口的稳定定位方法与 --json 事件字段。每端一份脱敏夹具(事件流、stderr、版本、参数),grep 夹具不含 `Bearer ` 与项目 token;任一假设不成立就回到设计,不硬编码绕过。"
- 约束:零凭据;真实客户端单次可能超过 180 秒,只在主会话或后台任务里执行,不交给 Workflow 子 agent。
- 结论(2026-10-03,六项假设全部给出结论,均未推翻设计):
- **H-P8-1 成立**:Codex 只在目录受信任时加载项目 .codex/config.toml。对照用隔离 CODEX_HOME(不碰用户 ~/.codex):配置里信任该目录时 codex mcp list/get 能看到项目里的 ljprobe,空配置时完全看不到(get 报 No MCP server named 'ljprobe' found);命令行 -c projects.'<path>'.trust_level 覆盖不生效。用户真实配置下 enterprise(已信任)列出 lian-jing,新建的临时目录没有。codex mcp get <name> --json 零模型即可反映「项目配置 + 信任」,字段含 enabled、disabled_reason、transport、enabled_tools、disabled_tools、startup_timeout_sec、tool_timeout_sec,可作 L1 Codex 自省;缺失归因 codex_project_untrusted。Q5 推荐项(信任列入 next_actions,炼境不写信任表)有了实证依据。
- **H-P8-2 成立(R8 证实)**:项目 settings 为 bypassPermissions 时,单加 --allowedTools 不构成限制(probe_write 照样被调用);--disallowedTools 能拦,但 Windows 上内置 shell 工具名是 PowerShell 不是 Bash,黑名单靠不住;--permission-mode default 能覆盖项目 settings,但安全命令(echo)仍会被自动放行。L3 Claude 写死的收窄组合:--permission-mode default --tools Skill --allowedTools <lian-jing 只读工具> Skill——实测写工具被拒、无任何 shell、只读工具可调,内置工具只剩 Skill 与 LSP(LSP 只读,随 --tools Skill 一并出现)。
- **H-P8-3**:codex exec(审批策略 never)下,带 annotations.readOnlyHint=true 的 MCP 工具在 -s read-only 中照常调用;不带的在 read-only 与 workspace-write 下都失败,错误「MCP tool call requires approval, but approval policy is never」。炼境工具已按读写声明 readOnlyHint,L3 Codex 臂用 -s read-only 即可,无须放宽沙箱。P7 中 probe_ping 被拦,原因是那个桩工具没声明 readOnlyHint。
- **H-P8-4 成立**:claude -p 用不存在的模型名(--model lj-no-such-model)时,3 秒内先发出 system/init(mcp_servers 连接状态、全部工具名、skills 列表),随后 result 报模型不可用,total_cost_usd=0、各项 token 为 0;桩服务收到 initialize → tools/list。Claude 的 B(skills 可见)、C(工具可见)可并入 L1,零额度。
- **H-P8-5**:Codex 对一次 75 秒的 MCP 工具调用未超时(status completed)——默认工具超时 ≥75 秒,精确值未测;mcp get 的 tool_timeout_sec 为 null(用默认)。R12「apply 超过 Codex 工具超时」在 75 秒内不成立。
- **H-P8-6**:DSH headless 稳定入口为 ELECTRON_RUN_AS_NODE=1 "<安装目录>\DeepSeek Harness.exe" --expose-internals "<安装目录>\resources\app.asar\dsh\node_modules\@deepseek-ai\dsh-desktop-host\lib\cli.js" headless [--patch <overlay>] --json <任务>;--json 事件为 session{sessionId,cwd}、status{phase,turn}、thinking{text}、tool_call{callId,tool,input}、tool_result{callId,status,result}、text{text}、final{text},工具调用可客观判定。客户端名 dsh-mcp-client,initialize 前先发 server/discover。
- verification: "2026-10-03 主会话后台实测,零凭据(全部连本地桩服务,桩只记方法、工具名、clientInfo、UA):Claude Code 2.1.173、codex-cli 0.155.0-alpha.9.2、DeepSeek Harness 桌面版内置运行时。每臂独立桩端口与日志,调用归属不靠时间窗。脱敏夹具入库 src-tauri/tests/fixtures/client_probe/(10 个文件:claude-init-badmodel、claude-narrow、claude-bypass-allowedtools、codex-ro-read/ro-write/ro-slow、codex-mcp-get-trusted/untrusted、dsh-read)——用户路径、会话 ID、UUID 换占位符,长文本截断到 200 字符,grep 无用户名、无 Bearer、无 64 位十六进制串。原始日志在本机 %TEMP%\\lj-p4-0(未入库)。"
- known_limits: "只验了无头 CLI;Codex 首轮四臂因脚本误加 -c mcp_servers.cua_repl.enabled=false(cua_repl 不在用户配置里,生成了缺 transport 的条目,配置加载失败)全部未运行,第二轮去掉后重跑。Claude 收窄组合只用桩工具验证,接 lian-jing 时只读工具白名单须取自 tools_list_result_for_project 中 readOnlyHint=true 的工具。实验附带的流程问题:前两轮脚本在脚本文件里用 kill 结束自己启动的桩进程,等于绕过了用户 deny 规则 Bash(kill *);之后桩服务改为到时自动退出,agent 会话里的实验脚本不再用 kill。"
### 📋 P4a — L1 零模型三端检查
- status: todo
- complexity: M
- files: src-tauri/src/onboard/probe_l1.rs, src-tauri/src/onboard/clients.rs, src-tauri/src/mcp_server.rs, src-tauri/src/mcp_tools.rs, src-tauri/src/commands/onboarding.rs, src/lib/commands.ts
- depends: P4-0, P8b
- acceptance: "agent_verify: [cargo test onboard::probe_l1] 进程内桩 MCP 服务 + 哑 shim:适配器取配置、按客户端方式执行 helper,再依次 initialize、tools/list、tools/call,C/D/E/F 均 ready 且每格带证据等级;shim 输出错误 token 记 auth_failed,shim 缺失记 shim_missing,端口不可达记 unreachable,客户端未安装记 not_installed(不计失败);期望工具集取自 tools_list_result_for_project,不写死数字;结果 JSON 与日志不含桩 token。mcp_server 识别 lian-jing-l1-probe 并排除出 has_initialized_project_client。[MCP 全局] probe_client_parity(enterprise, tier=l1) 10 秒内返回,三端适用格 ready 并附版本号;对 dev-manager-tauri 调用时 Claude 与 DSH 记 not_applicable(control_plane)。"
### 📋 P4b — MCP 服务端调用轨迹环
- status: todo
- complexity: S
- files: src-tauri/src/mcp_observe.rs, src-tauri/src/mcp_server.rs
- depends: architecture-evolution A4
- acceptance: "agent_verify: [cargo test mcp_observe] 项目路由上依次 initialize(codex-mcp-client)、tools/list、read_project_test_account(name=LJ_PROBE_A1B2C3D4),分别用无认证、错误认证、正确认证:依次记录 auth=none/invalid/valid、route、method、tool、截断后的 clientInfo/UA 与 nonce;Authorization 值、bearer、其他参数值(如 SECRET_X)都不进入记录;容量上限按 FIFO 淘汰;readiness 与 l1-probe 请求不进入轨迹;没有任何工具暴露原始轨迹。"
- 排序(Q8):A4(rmcp)先合并,P4b 挂在 rmcp 的 dispatch/认证边界上;扩展 mcp_server.rs 已有的 INITIALIZED_PROJECT_CLIENTS 记录点,单一挂点。P8 不受影响。
### 📋 P4c — 探针判定本体(纯函数)
- status: todo
- complexity: M
- files: src-tauri/src/onboard/probe/evaluate.rs, src-tauri/src/onboard/probe/adapters.rs, src-tauri/src/onboard/probe/redact.rs, src-tauri/src/envoy.rs
- depends: P4-0
- acceptance: "agent_verify: [cargo test onboard::probe] P4-0 真实夹具解析出的矩阵与期望一致;合成夹具逐一命中 route_global、auth_header_missing、token_mismatch、adapter_drift(判 unknown)、model_noncompliant、client_auth_or_quota(判 unknown)、rules_not_loaded、credential_leak(落盘文本已脱敏);控制面画像下 E、F 为 not_applicable;compare 只列变化的格子,probe_spec_version 不同时标记为不可比;判定函数没有任何 IO。L3 的 C–F 只认客观信号(Claude init 事件的 tools/skills、Codex --json 的 mcp_tool_call、DSH --json 事件、服务端轨迹的 clientInfo/认证结果/nonce),模型自报只记 model_fidelity。"
- 说明:P4-0 夹具在 src-tauri/tests/fixtures/client_probe/;仓库 core.autocrlf=true,Windows 检出为 CRLF,逐行解析须去掉行尾 \r。
### 📋 P4d — 探针执行器 + 异步任务 + 持久化
- status: todo
- complexity: M
- files: src-tauri/src/onboard/probe/runner.rs, src-tauri/src/onboard/probe/job.rs, src-tauri/src/db.rs
- depends: P4a, P4b, P4c
- acceptance: "agent_verify: [cargo test onboard::probe::runner] 假 ProcessRunner:超时时进程树全部退出(Windows Job Object)、该端记 timeout、其他端照常;未安装记 not_installed 且耗时 <1 秒;L1 判定未注册的一端零进程;子进程环境不含 CLAUDECODE 与 CLAUDE_CODE_*、含 MCP_CONNECTION_NONBLOCKING=false、stdin 为空、参数等于 P4-0 写死的组合;运行中再次 start 返回同一 run_id,启动时遗留的 running 记录改标 interrupted;同一指纹 7 天内命中缓存、force=true 时重跑;migrate() 新增 client_parity_runs 表且在旧库上重复执行幂等;探针前后 git status 不一致记 probe_side_effect、整次失败。"
- 成本控制:串行 DSH → Claude → Codex、每端独立 nonce、全机单飞锁、不自动重试、默认用各客户端默认模型(设置里可为探针单独指定更便宜的模型——用户的决定,不是炼境选模型);产物脱敏后存 %USERPROFILE%\.lian-jing\probe-runs\<run_id>\,每项目保留最近 5 次,不放 AppData;泄露扫描在炼境进程内用真实 token 比对,token 不经过 CLI 或 agent。
### 📋 P4e — 探针投影 + CLI 薄客户端 + UI 按钮
- status: todo
- complexity: M
- files: src-tauri/src/mcp_tools.rs, scripts/probe-clients.mjs, package.json, src/components/dashboard/OnboardPanel.tsx, src/lib/commands.ts, docs/ai-context/agent-button-system.md, docs/ai-context/project-context.yaml, AGENTS.md
- depends: P4d, P8g
- acceptance: "agent_verify: [MCP 全局] start_parity_probe confirmed=false 时拒绝且不起任何子进程,confirmed=true 时 1 秒内返回 run_id;轮询 enterprise 6 分钟内 done,三端 B–F 通过、G(无凭据泄露、探针前后 git status 一致、无非白名单写调用)通过,与 P1 基线一致;不做任何改动用 force 立即重跑,diff 为空;clients=[\"dsh\"] 时 60 秒内完成。[MCP 项目路由] 看不到 start_parity_probe,只有只读 get_client_parity。[CLI] pnpm probe:clients <id> --wait:全部适用格通过 exit 0,否则 exit 1,炼境不可达 exit 3。human_verify: 人看一次矩阵,确认证据等级和回退标注可读。"
- CLI 形态(Q9):只做调用全局 MCP 的薄脚本,不新增协议面;Rust console lianjing.exe 与 skill 降级见 architecture-evolution A1b(按需)。
### 📋 P5 — 测试账户落地到被 Git 忽略文件
- status: todo
- complexity: M×3
- 见 project-dev-variables V14–V16 与 `docs/tasks/2026-09-30-dev-variables-materialize.md`;落地不返回值、无需认证,是三端都可用的兜底路径。
### 📋 P6 — 技能能力包:通用 skill 单一来源、三端投影
- status: todo
- complexity: M
- files: src-tauri/src/commands/onboarding.rs, src-tauri/resources/(实现时定)
- depends: P2, P8c(2026-10-03 Q3:P8c 先落地 WriteSink,P6 的分发基于它实现,避免两边同时改 onboarding.rs 分发逻辑)
- acceptance: "agent_verify: [cargo test] 通用 skill(architect、splitter、code-review 等)由一处模板按版本投影到各客户端目录(Claude user/project skills、Codex/DSH 的 .agents/skills);[三端无头探针] B 项三端看到同一组 skill 且内容一致。"
- 为什么:2026-09-30 探针显示同一批 skill 在三端各维护一份(Claude `~/.claude/skills`、Codex `source-command-*` 副本、DSH 手写 `.dsh/skills`),是实际的重复与漂移源。先把 skill 中的确定性步骤下沉为炼境 MCP 工具,让 skill 变薄,再做投影。
- 不做:炼境不执行推理类 skill(无模型、会丢失会话代码上下文、越过「不替 agent 思考」边界);不以运行时拉取 skill 正文作为主路径(炼境离线即失效、脱离 Git 分支)。
### ✅ P7 — MCP 服务器说明的三端支持验证
- status: done
- complexity: S
- files: .blueprint/modules/agent-client-parity.md, docs/ai-context/agent-button-system.md(结论为部分支持,src-tauri/src/mcp_server.rs 与 lian-jing skill 均不改)
- depends: P1
- acceptance: "agent_verify: [三端无头探针] 在 initialize 下发唯一标记的 instructions,判定三端是否把它带入模型上下文。全部支持则把引导提示迁到服务器说明,本地 skill 只保留 MCP 不可达时的排障步骤;部分支持则如实记录、保留 skill 引导。"
- 为什么:Claude 已实证会把 lian-jing 的服务器说明带入上下文;若三端皆支持,引导提示可由炼境集中维护。
- 结论(2026-10-03):部分支持。Claude Code 与 DSH 会把 initialize 下发的 instructions 带入模型上下文,Codex 不会。按 acceptance 不迁移:引导提示仍以 lian-jing skill(及 AGENTS.md 托管块)为三端单一来源,服务器说明保持现状,只作 Claude/DSH 的增强层;三端都须遵守的规则不得只写在服务器说明里(已写入 agent-button-system.md「本地 Agent 控制面的稳定边界」)。
- verification: "2026-10-03 三端无头探针(零凭据;不改用户配置,不连运行中的炼境):node 写的最小 MCP 桩服务(streamable-http,纯 JSON 响应),每端两个端口——实验组 initialize 带 instructions,内含该端专属随机标记;对照组不带 instructions 字段,其余完全相同。标记只出现在 instructions 里,工具 probe_ping 的名字、描述和返回值都不含标记。同一问题:「MCP 服务器说明里有什么标记字符串?只回答标记,不调用工具,没收到就答 NONE」。桩服务记录收到的请求,用来证明客户端确实完成了 initialize → tools/list。
① Claude Code 2.1.173(claude-sonnet-4-6;claude -p --mcp-config --strict-mcp-config --tools '',cwd 为空目录):支持。实验组 init 显示 ljprobe connected,答出 LJ-P7-3067CE459B;对照组答 NONE;实验组输入 token 多 99(10774 对 10675),与多出的说明长度相符。
② Codex CLI 0.155.0-alpha.9.2(codex exec --json -s read-only,-c mcp_servers.ljprobe.url 临时覆盖,用户默认模型,cwd 为空目录):不支持。实验组、对照组都答 NONE;桩服务日志证明 initialize(codex-mcp-client,协议 2025-06-18)与 tools/list 均成功,即说明已送达客户端、但没进入模型;两组输入 token 完全相同(16320 = 16320;换宽问法「全部上下文里是否有 LJ-P7- 开头的字符串」为 16335 = 16335);该会话 rollout 文件中标记出现 0 次。正向对照:让模型调用 probe_ping,它确实发起 mcp_tool_call(server=ljprobe, tool=probe_ping)(被 approval policy never 拦下),说明桩服务的工具在模型视野里,NONE 不是没连上。加开发中特性 --enable mcp_2026_07_28 后仍答 NONE,输入 token 仍为 16320。
③ DSH(桌面版内置运行时 headless;--patch 叠加层停用用户已有的三个 MCP 条目、插入指向桩服务的 dsh-mcp-client 条目,用户 ~/.dsh 与 DSH_HOME 均未改):支持。实验组答出 LJ-P7-B0628021BF,思考过程原样复述了说明全文;对照组答 NONE;实验组输入 token 多 53(6487 对 6434)。DSH 在 initialize 前先发 server/discover,桩服务与炼境一样回 -32601,DSH 随即回退到 initialize → tools/list;其 mcp-client 配置有 maxInstructionBytes(默认 32768)。
探针脚本、桩服务与原始日志留在本机 %TEMP%\lj-p7-probe(未入库,P4 固化时可参考)。"
- known_limits: "Claude 与 DSH 只验了无头模式,Claude 只用了 sonnet-4-6;Codex 未验 TUI 与桌面版(桌面版可能走 app-server 路径,结论可能不同),也未验 SSE 传输。探针用的是桩服务的中英混合短说明,未用炼境真实说明(中文 238 字、UTF-8 500 字节,远低于 DSH 默认上限 32768 字节)实测。Codex 后续版本若开始带入服务器说明,按「检测行为而非嗅探版本」重跑本探针(P4 固化后纳入矩阵),三端都支持时再迁移。现有服务器说明五条要点中,「先读状态」「canonical project_id」「开发变量先查后登记」在 skill 有对应条目;「其他写操作须用户明确确认」与「unavailable/unknown 不得当作成功」在 skill 与 AGENTS.md 中只有分场景表述(合并 PR、CI workflow、工具链安装;MCP 不可用时不伪造诊断),Codex 会话只能从这些条目与工具描述获知——是否补成通用条目留待下次改 skill 时一并处理(需升接入包版本),本卡不改。
已补齐(2026-10-03,分支 feat/agent-client-parity-p7-skill-rules):两条已作为通用边界写进 lian-jing skill 模板开头(双投影到 .agents 与 .claude),两条规则的核心句与服务器说明逐字一致;只读查询、登记开发需求元数据与回传炼境明确列为无需确认的例外,与服务器说明的「其他写操作」及 skill「回传不必逐条征得同意」相容。服务器说明的例外是泛称「不含秘密的开发需求元数据」,skill 例外括注同时列出两个登记工具 register_project_test_account 与 register_project_dev_variable(审查修复:初版只列前者,会把开发变量登记误归入须确认的写操作);同批修正 skill 第 8 条——开发变量改用 register_project_dev_variable 登记名称与用途,测试账号仍用 register_project_test_account(初版两者都指向后者)。接入包 base-onboarding 1.7.0→1.8.0(修复在 1.8.0 发布前,未再升版本,只更新指纹);mcp_server 新增 server_instruction_universal_rules_are_in_skill,服务器说明与 skill 任一侧缺这两条即失败,skill 例外行缺任一登记工具也失败。AGENTS.md 托管块未改,仍只有分场景表述;存量项目需重建安装炼境后重跑 apply_onboarding_pack 才拿到新 skill。"
### 🔵 P8 — 一键接入:创建/添加子项目即完成三端可用
- status: in_progress
- complexity: L(2026-10-03 设计评审后拆为 P8-0、P8a–P8g,均为 S/M)
- depends: P4a(原为 P4;P8 只依赖 L1 零模型检查,不等 L3)
- acceptance: "agent_verify: [Tauri command / MCP] 对新登记项目 plan_onboarding → 用户确认 → onboard_project(expected_digest, confirmed=true),依次完成 canonical context、接入包分发(规则、skill 双投影、Codex 托管块)、本机三端注册(Claude local、DSH 白名单与会话插件、shim、token),并以 L1 零模型三端矩阵(每格带证据等级)验收,附最近一次 L3 结果及是否过期;重复执行收敛(全部 converged、文件 mtime 不变);接入包对项目文件的改动只报告 diff、不自动提交。human_verify: 在 Claude Code、Codex、DSH 桌面 GUI 各新开会话调 read_project_test_account(name=LJ_PROBE_NONEXISTENT),得「开发需求未登记」。"
- 验收口径变更(2026-10-03,Q1,按自验报告纪律报备):原口径「以三端无头探针给出 A–F 兼容矩阵」改为「L1 零模型矩阵 + 可选 L3 结果及是否过期」。理由:L3 每次消耗三端模型额度、需 2–3 分钟且有随机性,不宜作为每次接入的门槛;L1 经真实 shim 重放认证链、在项目路由上实际调用 C–F 工具,给出协议级证据;「L1 全绿而真实会话失败」的风险用证据等级标注、客户端版本变化时提示跑 L3(P4e)兜底;GUI 形态归 human_verify。决策方式:用户以 /goal 授权按推荐方向代为推进,11 个决策点全部采纳推荐项、未逐项确认,用户可随时推翻。
- 「完成」的判定:只按三端可用子集(身份、托管块版本、两份 skill、Codex 块、Claude local、DSH 白名单、lefthook、L1 适用格全部 ready)。verify_onboarding 的 origin 指向 Gitea、CI workflow、飞轮 commit 三项对新项目必然不通过,进入 next_actions,不算失败。
- 设计:`docs/tasks/2026-10-03-p8-onboarding-oneclick-design.md`(三角度方案 + 综合评审、16 条已核实代码事实、风险 R1–R18、决策 Q1–Q11)。主方案为最小编排本体(同步、零模型的接入 + 可选异步探针,A1 不前置),嫁接 plan/digest 写前确认、经真实 shim 的认证链重放、「已接入」单一判定、客户端适配器注册表与 A1a 适应度测试。
- 决策(Q2–Q11 均采纳推荐项):写入前预览走 plan → digest 确认 → apply;bypassPermissions 默认值保持现状但在 plan 与报告中单列、可按项目取消;Codex 信任列入 next_actions,炼境不写 ~/.codex 信任表;L3 只在显式触发时跑,建议在 canary(enterprise)项目、指纹变化时跑;做 register_project 的全局路由投影;A4 先于 P4b;CLI 只做 pnpm probe:clients 薄脚本;A1 改写为三段;探针默认用各客户端默认模型。
- 非目标:不自动 commit/push;不自动建 Gitea 仓库、webhook、CI,不自动装工具链(只列 next_actions,Gitea 一项注明「工具默认建公开仓库」);不改客户端全局安全设置,不覆盖手写同名 MCP 配置;L3 不作接入门槛、不作 CI 闸门;不做离线 CLI 写入,不拆 core crate;P8 不改接入包模板;批量登记不自动接入。
- 为什么:2026-10-01 Enterprise 验收时,接入散落在 inject_mcp、apply_onboarding_pack、verify_onboarding 与手工探针多个步骤。按「分发 vs 查询」:随仓库走的文件由炼境分发一次、按版本升级;与机器相关的客户端注册由炼境在本机维护、不进仓库;易变信息走 MCP 查询。默认三端都接入(成本小,保留随时换客户端的能力);能用标准的不做专门适配(Codex、DSH 原生读 AGENTS.md 与 .agents/skills,只有 Claude 需要 CLAUDE.md 与 .claude/skills 投影);炼境离线时规则与 skill 仍可用。
### ✅ P8-0 — 蓝图与依赖修正
- status: done
- complexity: S
- files: .blueprint/modules/agent-client-parity.md, .blueprint/modules/architecture-evolution.md, .blueprint/manifest.yaml, docs/tasks/2026-10-03-p8-onboarding-oneclick-design.md
- acceptance: "agent_verify: [YAML 解析] manifest.yaml 可解析;manifest 不再写「P4/P8 依赖 A1」。[读文件] P8 depends 改为 P4a,acceptance 按 Q1 改写并记录理由;P4 拆为 P4-0、P4a–P4e;A1 拆为 A1a/A1b/A1c,A1c 写明四个触发条件。"
- 依赖顺序:P8-0 → P8a → P8b → P8c → P4a(另依赖 P4-0)→ P8d → P8e → P8g;P8f 可选、独立,P8e 端到端验收前完成。P8 关键路径不依赖 L3。
- verification: "2026-10-03:manifest.yaml 经 PyYAML safe_load 解析通过(agent-client-parity progress 35、edges 95 条);manifest 全文「P4/P8 依赖」「依赖 architecture-evolution A1」「A1 的 CLI 投影」0 处;设计文档入库 docs/tasks/2026-10-03-p8-onboarding-oneclick-design.md,§9 十一项推荐项已勾选。"
### ✅ P8a — 「已接入」单一判定
- status: done
- complexity: S
- files: src-tauri/src/mcp_inject.rs, src-tauri/src/mcp_tools.rs
- depends: P8-0
- acceptance: "agent_verify: [cargo test mcp_inject] in-memory DB + 假 CredentialStore:有 token、不在任何组的项目进入 reinject_all_mcp 的目标集合;组成员仍在;token 已删的项目不在(锁内复查语义不变);新函数的连接由调用方传入。[cargo test mcp_tools] list_all_projects 的「已注入」与 DSH 白名单、根目录迁移调用同一个函数:删除 .claude/liangjing.json 不改变判定,删除 token 后判为未注入。"
- 起因:「已接入」现有三套判定——reinject_all_mcp 按组成员、DSH 白名单与根目录迁移按 token、list_all_projects 按 .claude/liangjing.json 是否存在;ensure_standalone_group 只在项目不在任何组时才注入,已在组内的项目重跑接入包不会修复本机注册。
- 实现(2026-10-03):mcp_inject 新增 is_project_injected(store, project_id)(有项目 MCP token 即已接入),DSH 白名单、on_project_root_changed、list_all_projects 改为调用它;reinject_all_mcp 的目标由「组成员」扩为「组成员 ∪ 有 token 的登记项目」(reinject_targets),锁内复查改为 is_reinject_target(仍是组成员或仍有 token),连接与凭据存储由调用方注入。list_all_projects 拆出 render_all_projects(conn, store),顺带去掉循环内的二次取池。行为变化:独立接入(不在任何组)的项目在端口变更后也会被重注入;list_all_projects 对只剩兼容文件、token 已删的项目改报「未注入」。ensure_standalone_group 的「已在组内不注入」由 P8d 编排本体解决,本卡不动。
- verification: "2026-10-03:cargo test 286 passed / 0 failed / 5 ignored;新增 reinject_targets_follow_single_injected_judgment(组成员无 token 仍是目标、独立有 token 进入目标、删 token 后锁内复查为否且移出目标)与 list_all_projects_judges_injection_by_token_not_compat_file(只有 liangjing.json 无 token → 未注入;有 token 删兼容文件 → 已注入;删 token → 未注入)。变异:屏蔽 reinject_targets 的 token 分支、list_all_projects 改回看 liangjing.json,两条新测试均失败。契约同步 docs/ai-context/project-context.yaml「被管理项目身份上下文」。"
### ✅ P8b — 客户端适配器注册表 + 结构化注入结果 + A1a 适应度测试
- status: done
- complexity: M
- files: src-tauri/src/onboard/mod.rs, src-tauri/src/onboard/clients.rs, src-tauri/src/mcp_inject.rs, src-tauri/src/commands/onboarding.rs, src-tauri/src/mcp_tools.rs, src-tauri/src/lib.rs, src/lib/commands.ts
- depends: P8a
- acceptance: "agent_verify: [cargo test onboard::clients] 注册表恰好列出三端,每端声明 detect、static_status、apply;sync_claude_local 的每条分支映射到一种 ClientOutcome(converged / applied / refreshed / manual_kept / other_project / unavailable{reason} / skipped{control_plane|wsl});DSH 未安装记 not_installed,块外有手写条目记 manual_kept,缺 claude CLI 时 claude 记 unavailable、其余照常;原有 log_event 的类别和文案不变。[cargo test] onboard_module_is_projection_free:src/onboard/ 下源码不含 tauri::、db::pool(、#[tauri::command]。[cargo test onboarding] 新增 clients 字段不改变 checks 计数和 VerifyState,控制面和 WSL 记 not_applicable、不拉低 pass。[MCP] 对 enterprise 连续两次 inject_mcp:第二次三端都是 converged,耗时 <1 秒(未起 claude CLI),四个项目文件 mtime 不变。"
- 起因:inject_project_mcp 返回 Result<(),String>,sync_claude_local 返回 (),refresh_dsh_patch 失败只写日志——Claude 与 DSH 的成败没有结构化结果,直接编排会把失败报成成功(R4),所以 P8d 必须排在本卡之后。
- 实现(2026-10-03,分支 feat/agent-client-parity-p8):新增 src-tauri/src/onboard/(mod.rs 含适应度测试;clients.rs 定义 ClientId、ClientOutcome、ClientReport、ProjectFacts、DesiredMcp、ClientAdapter trait 与 registry())。三端适配器实现在 mcp_inject(需要其私有辅助函数),注册表反向接入。inject_project_mcp_locked 写完项目身份与 Claude 兼容投影后经 registry() 逐端 apply 并收集报告;新增 inject_project_mcp_report 返回逐端结果,inject_project_mcp 签名不变,只有 Codex 托管块写入失败算整体失败(沿用旧语义)。sync_claude_local 抽出依赖注入版 sync_claude_local_with(Claude 配置与 CLI 执行器由调用方给出),每条分支返回一种 ClientOutcome,日志类别与文案不变;ensure_dsh_patch 抽出 ensure_dsh_patch_at(DSH 目录、凭据存储、会话插件引用注入),refresh_dsh_patch 返回结果;upsert_codex_mcp_config 返回 converged/applied/manual_kept。只读判定 claude_static_outcome / codex_static_outcome / dsh_static_outcome + client_static_status 供 verify_onboarding 的 clients 附加字段(MCP 投影)。inject_mcp 回执改为逐端列出实际结果。
- 行为变化(有意为之,R5):① 项目 .codex/config.toml 已手写 [mcp_servers.lian-jing] 时由整体报错改为 Codex 记 manual_kept、Claude 与 DSH 照常接入;② Codex 托管块写入失败时 Claude 与 DSH 仍会完成(旧代码先写 Codex、失败即中止),inject_project_mcp 仍返回 Err;③ WSL 项目不再对 Claude local 起 CLI,记 not_applicable{wsl}(2026-07-04 放弃 WSL 路径项目裁决;DSH 白名单本就排除 WSL);④ Codex 托管块的写入顺序移到 Claude 兼容投影之后。
- verification: "2026-10-03:cargo test 295 passed / 0 failed / 5 ignored(新增 9 条:onboard::clients 4 条——注册表恰好三端且顺序固定、settled 判定、扁平序列化、WSL 根识别;onboard 适应度测试;mcp_inject 4 条——claude_sync_maps_every_branch_to_an_outcome 用假 CLI 覆盖 converged×2/unavailable×3/applied/failed×3(含回滚调用序列 get→remove→add-json→add-json)/refreshed/removed/other_project/manual_kept/needs_human,static_outcomes_are_read_only_judgments,dsh_patch_reports_not_installed_applied_converged_and_manual_kept(临时 DSH 目录,手写条目不被覆盖),adapters_skip_control_plane_and_wsl_without_touching_disk;原有 Codex 手写冲突测试改断言 manual_kept)。[MCP] 两次 inject_mcp 的耗时与 mtime 断言须在合并后、运行中的炼境重编译生效时实测,通过后本卡标 ✅。"
- review: "2026-10-03 独立审查 agent 只读审查 d1b9b05(未发现死锁、重入或崩溃),10 条发现处置:① Claude apply 未装时报 unavailable → 无全局配置且找不到 CLI 记 not_installed(与 DSH、static 同口径);② 手写条目保留时丢日志 → DSH 照原文案补记 mcp_client_sync warn,Codex 记一条带 project_id 的 warn;③「MCP 配置注入成功」写在 Codex 之前,Codex 失败时诊断仍显示成功 → 移到三端收敛之后,整体失败时改记 warn「注入失败」;④ 控制面 Claude 静态判定掩盖遮蔽全局注册的残留 local 条目 → 有残留报 missing;⑤ 嵌套项目误判 other_project → 根目录自身是 git 根时只认等于根目录的键,否则取最具体的键;⑥ 适应度测试不递归 → 改为递归,并在 registry() 注明适配器暂寄 mcp_inject 的传递依赖(绞杀者过渡);⑦ 状态名与设计不一致 → skipped 改为 not_applicable{control_plane|wsl} 与 deferred;⑧ 只读判定读失败当未配置、块与手写并存、路径来源不同 → 读失败与 YAML 解析失败报 unavailable,Codex 块与手写同名表并存报 needs_human,DSH 块外手写报 manual_kept,标记错序报 needs_human,client_static_status 改用与注入同源的 project_root;⑨「目录不存在」用例为假阳性 → 改为 CLI 存在仍不起 CLI 并断言原因,另补 hard_failure、inject 回执、apply 结果映射、clients 不影响 pass 的测试;⑩ 记入 known_limits。修复后 cargo test 300 passed / 0 failed / 5 ignored(较首版新增 5 条),变异验证:删目录守卫、去掉自身 git 根判定,对应用例均失败。"
- mcp_verify: "2026-10-03 PR #33 合并(ddd729c)、运行中的炼境重编译生效后:verify_onboarding(enterprise) 的 clients 三端均 converged;连续两次 inject_mcp 回执三端均「已一致(未改动)」,耗时 26 ms、112 ms,.lian-jing/context.json、.codex/config.toml、.claude/settings.json、.claude/liangjing.json 四个文件 mtime 不变,git status 前后均干净。"
- known_limits: "WSL 项目已有的 Claude local 托管条目在端口变更后不再刷新(重注入对 WSL 记 not_applicable),而 remove_project_mcp 与 on_project_root_changed 对 WSL 根仍会起 CLI 清理——口径不完全一致,WSL 路径项目已按 2026-07-04 裁决放弃,暂不处理。Codex 托管块已存在时 upsert 不检查块外手写同名表(存量问题),只读判定会报 needs_human 提醒人工清理。注册表与适配器的端到端组合(缺 CLI 时其余两端照常)只在单元层分别验证,未做真实客户端的组合实测。"
### 🔵 P8c — 写入前计划:WriteSink + plan_onboarding
- status: in_progress
- complexity: M
- files: src-tauri/src/onboard/plan.rs, src-tauri/src/lib.rs, src-tauri/src/commands/onboarding.rs, src-tauri/src/commands/blueprint.rs, src-tauri/src/mcp_inject.rs, src-tauri/src/credential_store.rs(审查修复新增)
- depends: P8b
- acceptance: "agent_verify: [cargo test onboard::plan] 临时 git 仓库:空仓库做 plan 全部是 create、目录快照前后字节一致;已接入仓库连续两次 plan 全部 noop、digest 相同,任一目标文件变化 digest 就变;块外有未提交改动时该文件带预警且 plan.blocked 非空;内容相同仅行尾为 CRLF 判 noop;超长 diff 截断并标注;context.json 是别的 project_id 时记 blocked(identity_conflict);machine_changes 列出各本机步骤的预计动作,计划阶段假 claude CLI 只收到 mcp get。[cargo test] 原有 onboarding、blueprint、mcp_inject 测试全部通过、行为不变。"
- 说明:WriteSink 以 write_if_content_changed 作为真实写入汇;P10 的混合文件检测前移到写入之前;与 CI 的 plan/apply 同一模式(Q2)。
- 实现(2026-10-03,分支 feat/agent-client-parity-p8c,提交 ef2c7fd):lib.rs 新增 WriteSink trait(read/write,内容忽略行尾未变不写)与 DiskSink(真实写入,父目录按需创建)。接入包模板分发(dispatch_templates_with)、蓝图规则同步(sync_blueprint_rules_with)、项目身份与 MCP 投影(write_project_mcp_files_with:context.json、settings.json 兼容条目、liangjing.json、Codex 托管块)全部改经写入汇,原入口改为 DiskSink 薄包装。新增 onboard/plan.rs:PlanSink 只记录不落盘,后续步骤读到前面步骤的待写内容(AGENTS.md 先后被模板分发与蓝图规则改写,计划与实际写入算出同一结果);write_project_files 为 plan 与 apply(P8d)共用的写入序列;plan_onboarding 给出逐文件 create/update/noop、有上限的 diff(60 行)、混合文件预警(P10 前移到写入前,记 blocked(mixed_file))、身份冲突、手写 Codex 条目与本机 manual_kept/other_project/needs_human 阻断项,并对(项目 ID、逐文件路径/动作/内容哈希、本机步骤/状态、阻断项)算 sha256 digest。mcp_inject::plan_onboarding 为生产入口:本机侧预计动作 token(读受保护存储判断有无)、shim(比对稳定入口内容)、claude/codex/dsh(复用 P8b 的只读 client_static_status)、codex_trust(只读解析 ~/.codex/config.toml 的 [projects.'<路径>'] trust_level,含祖先目录;审查后改为精确目录 + git 主仓库根、移出本机步骤单列,见 review)。2026-10-03 合并 master(PR #34 重复登记修复 U1/U2,同改 mcp_inject.rs、commands/onboarding.rs、mcp_tools.rs)自动合并无冲突。收尾补测试 plan_predicts_exactly_what_apply_writes(计划的文件集合与内容哈希 = 随后 DiskSink 实际写入,git status 文件集合一致),修正 mcp_inject::plan_onboarding 注释「不起子进程」为「不起客户端 CLI,混合文件预警会起只读 git」。
- verification: "2026-10-03:合并 master 后全量 cargo test 310 passed / 0 failed / 5 ignored(ef2c7fd 时 307,PR #34 带入 3 条);补测试后 311 passed / 0 failed / 5 ignored,编译 0 warning。onboard::plan 7 条:空仓库全 create 且目录快照(含 .git)字节不变;apply 后连续两次计划全 noop、digest 相同,本机动作变化与目标文件变化 digest 都变;CRLF 检出同内容判 noop;混合文件带 warning 且 blocked(mixed_file)、context.json 属别的项目记 blocked(identity_conflict);手写 Codex 条目记 manual_entry、本机 other_project 记 shared_key;PlanSink 覆盖读与改回撤销;diff 截断与标注;计划=实际写入。mcp_inject 新增 codex_trust_reads_projects_table_including_ancestors。变异:PlanSink::read 忽略待写内容 → plan_predicts_exactly_what_apply_writes(AGENTS.md 哈希不符)与 plan_sink_overlays_pending_writes_and_reverts_to_noop 失败,原有 5 条 plan 测试仍通过(补测试前此变异只被单元测试捕获)。"
- 自查(对照 acceptance 逐条):空仓库全 create + 快照字节一致 ✅;两次 plan 全 noop、digest 相同 ✅;任一目标文件变化 digest 变——原标 ✅ 说过头(审查 ⑥):只对计划会写入或核对的文件成立,已存在的只建不改文件、语义相同的 JSON、同版本托管块不列出、不进 digest(它们怎么变 apply 都不写);审查修复后 update 另绑定写入前哈希,准确语义写在 plan.rs 模块文档,测试补「写入前内容变 → digest 变」「改 lefthook.yml → digest 不变」(按纪律报备:改的是验收表述以符合实际语义,断言只增不减);块外未提交改动带预警且 blocked 非空 ✅;CRLF 判 noop ✅;超长 diff 截断并标注——在 bounded_diff 单元层测试,未经 plan 端到端构造超长差异 ✅;identity_conflict ✅;machine_changes 列出各本机步骤 ✅(plan_machine_steps 依赖真实凭据存储与用户目录,无注入点,未做单元测试;审查修复后其中的映射抽成纯函数 token_step、machine_step_blocker、codex_trust_next_action 分别单测,组装本身仍未单测);原有 onboarding、blueprint、mcp_inject 测试全部通过 ✅。
- 有意偏差:① 计划阶段完全不起 claude CLI(Claude 只读 ~/.claude.json 判定,沿用 P8b 的 static_status),比 acceptance「假 claude CLI 只收到 mcp get」更严;因此没有假 CLI 断言,由结构保证(static_status 路径无 CLI 调用)。② 文件动作只有 create/update/noop,设计文档 §1 的 append/skip 合并为 update/noop(acceptance 只要求 create/noop)。③ DiskSink 与 write_if_content_changed 同语义但未直接复用:另加按需建父目录,且读失败(非「不存在」)时报错而不是覆盖写。
- 自查发现(留待独立审查处置,本次未改代码):① 行为变化——dispatch_one 原以 target.exists() 判「已存在跳过」,现经 sink.read 判定,已存在但非 UTF-8(如 UTF-16/GBK 的 lefthook.yml、mise.toml)由 skipped 变 failed,plan 中成为 needs_human 阻断;全托管文件读失败由覆盖写变 failed(更安全)。② 设计 §1 要求计划单列 bypassPermissions 默认值(P8 决策 Q4、P8g 视图需要),当前只体现在 .claude/settings.json 的 create 概要里,未单列;本机变更清单也还没有产品组与 buff、.git/hooks(lefthook install)两步——宜在 P8d 编排时补进 machine_changes。③ codex_config_trusts 只认 [projects.'…'] 段头,TOML 基本字符串里的转义反斜杠(如 `[projects."D:\\Work"]`)与内联表写法不识别(本机 Codex 实际写的是字面量字符串小写路径,已核对 13 个段头均为此格式)。
- T1:plan_onboarding 目前无 MCP / Tauri 投影(P8e 才加),输出尚不是外部契约,docs/ai-context/project-context.yaml 未改;P8e 的 acceptance 已含「project-context.yaml 补 plan、digest 与状态语义」。
- review: "2026-10-03 独立只读审查 04a0069,结论 fix_then_merge,9 条发现处置:① [中] Codex 未信任记 needs_human 被并入 blocked(设计 §4 blocked 只有四类,Q5/C4 应进 next_actions;新项目几乎都未信任,一键接入会全部卡住)→ 已修:codex_trust 移出本机步骤,计划新增 codex_trust(trusted/untrusted/not_installed/unknown)与 next_actions 两个字段,未信任或读不出 Codex 配置只进 next_actions,不阻断、不进 digest(去 Codex 信任后无需重新确认);「本机步骤 → 阻断项」抽成纯函数 machine_step_blocker。补 codex_trust_is_next_action_not_blocker_and_outside_digest、machine_step_blocker_maps_statuses。② [中] dispatch_one 改用 sink.read 判存在,已存在的非 UTF-8 文件由 skipped 变 failed(生产路径 apply_onboarding_pack 回归)→ 已修:WriteSink 新增 exists()(PlanSink 为待写或磁盘存在),dispatch_one 只判存在;全托管文件读不出保持 failed(改为显式分支,不再借道 dispatch_one)。补回归 existing_non_utf8_files_skip_or_fail_by_ownership:UTF-16LE 的 lefthook.yml 在计划与真实写入中都是 skipped、字节不变、计划无阻断;UTF-16 的 envoy.md 记 failed,blocked 只有它一条。③ [中] token 步把「未配置」与 needs_repair 合成 apply → 已修:credential_store 新增只读 mcp_auth_token_state(Present/Missing/NeedsRepair,分支与 get_or_create_mcp_auth_token 一一对应),token_step 映射为 noop/apply/needs_human(阻断);is_project_injected 仍只作「已接入」判据。补 mcp_auth_token_state_matches_get_or_create_branches、token_step_tells_missing_from_needs_repair。④ [低] 祖先目录信任未经验证、测试里的 TOML 非法(假阳性)→ 已修:去掉祖先规则,候选改为项目根 + git 主仓库根(git rev-parse --path-format=absolute --git-common-dir 的上级;worktree 继承主仓库信任、仓库子目录取仓库根——按审查所述的 Codex 判定方式,未核对 Codex 源码);键解析按 TOML 规则:字面量键、基本字符串反转义(非法转义的键忽略)、[projects] 段内联表与点号键。删除非法 TOML 用例,改为 codex_trust_matches_exact_dirs_with_real_toml_keys(含真实转义写法 \"D:\\\\Work\"、祖先目录不覆盖子目录、\\\\?\\ 前缀)与 git_main_worktree_root_resolves_worktree_and_subdir(真实 git worktree)。装了 Codex 但没有 config.toml 记 untrusted(原记 not_installed)。⑤ [低] 写入序列维护两份、注释不实 → 已修:抽出 write_project_identity_files_with(context.json → settings.json → liangjing.json),inject_project_mcp_locked 与 write_project_files 都只调它;Codex 托管块两边都经 upsert_codex_mcp_config_with(inject 由 CodexAdapter 调);删除 write_project_mcp_files_with 及其注释。⑥ [低] digest「任一文件变化就变」说过头,且不绑定写入前内容 → 已修并改口径:PlannedFile 新增 before_sha256(update 的写入前哈希)并进 digest——人确认后文件又被改、写入后内容不变也会 digest 不符;plan.rs 模块文档写明准确语义,自查条目同步改写(见上)。P8e 写 project-context.yaml 时沿用此语义。⑦ [低] 阻断项重复、target 误导 → 已修:lib.rs 新增 WriteFailure(path + reason;转 String 只取文案,sync_blueprint_rules 等原入口返回值不变),蓝图规则同步、身份与兼容投影失败都指向真正出错的文件;Codex 块失败指向 .codex/config.toml;本机 codex 步的阻断项指向 .codex/config.toml,blocked 按(target, reason)去重、文件级在前。补 blocked_targets_point_at_failing_files_without_duplicates;manual_codex 用例加入本机 codex manual_kept,断言 .codex/config.toml 只留一条 manual_entry。⑧ [信息] onboard 本体反向依赖 commands::* 与 mcp_inject → 本卡不改,记入 known_limits,建议 P8d 起下沉。⑨ [信息] P8d/P8g 的 acceptance 缺 bypassPermissions、Codex 信任单列与产品组/buff、.git/hooks 两步 → 本卡已单列 codex_trust 字段;其余写在 P8d 卡「审查转入」一行,待统筹合并时并入 acceptance。修复后全量 cargo test 318 passed / 0 failed / 5 ignored(较修复前 311 新增 7 条),编译 0 warning。变异验证(先暂存、后从暂存区还原):第一轮同时注入 6 处——dispatch_one 读失败记 failed、digest 混入 codex_trust、digest 去掉写入前哈希、needs_repair 记 apply、基本字符串不反转义、取消去重——恰好 7 条对应用例失败、其余 311 条通过;第二轮注入 codex 步阻断项不改指向、未信任重新并入 blocked,machine_step_blocker_maps_statuses、blocked_targets_point_at_failing_files_without_duplicates、codex_trust_is_next_action_not_blocker_and_outside_digest 失败。"
- known_limits: "① onboard/plan.rs 直接调 commands::onboarding、commands::blueprint、mcp_inject 的写入函数,适应度测试 onboard_module_is_projection_free 只做文本匹配、抓不到这种依赖方向——绞杀者过渡态,P8d 起把被调用的写入函数下沉到 onboard/ 或中立模块,commands 层做薄包装。② Codex 信任是计划阶段不起客户端的静态预判(只起只读 git rev-parse):候选只有项目根与 git 主仓库根(依审查所述 Codex 判定方式,未核对 Codex 源码);路径比较不区分大小写、统一分隔符;只解析 [projects.<键>] 段与 [projects] 段的内联表/点号键,顶层点号键 projects.'x'.trust_level 不识别;权威结论以 P4a 的 codex mcp get --json 自省为准。③ shim 步在 USERPROFILE 缺失或找不到炼境可执行文件时记 unavailable、不阻断,apply 时 ensure_header_shim 会失败(与 token 步不同口径,极少见,未改)。④ bypassPermissions 默认值仍未单列(P8d)。⑤ plan_machine_steps 的组装依赖真实凭据存储与用户目录,未单测;其中的映射函数已单测。"
- 待办:开 PR(M 级);合并后标 ✅。
### 📋 P8d — onboard_project 编排本体
- status: todo
- complexity: M
- files: src-tauri/src/onboard/apply.rs, src-tauri/src/onboard/report.rs, src-tauri/src/commands/onboarding.rs
- depends: P8c, P4a
- acceptance: "agent_verify: [cargo test onboard::apply] 临时 git 仓库、in-memory DB,假 CredentialStore、ClientAdapter、ProcessRunner:① plan 后 apply(expected_digest, confirmed=true) → status=done,HEAD 不变,repo_changes 与 git status --porcelain 一致且都未提交;② 立即再次 plan 全部 noop,再次 apply 全部 converged、mtime 不变;③ digest 不符或 confirmed=false → refused、零写入;④ Claude 适配器失败 → partial,Codex 和 DSH 照常完成,L1 的 Claude 列记 blocked(dep=claude_local),修复后重跑得 done;⑤ 未登记的 project_id、空路径或相对路径 → failed、零写入;⑥ 非 git 目录 → repo_changes 为 unavailable、status 不是 done;⑦ 新项目没有 origin、CI、飞轮数据仍可 done,三项进 next_actions,Gitea 一项注明「默认公开」;⑧ 同一项目并发 apply,第二个返回 busy。"
- 说明:存量 apply_onboarding_pack 一并改为 #[tauri::command(async)](现为同步,内部可能起 claude CLI,会卡界面)。某步失败只把依赖它的步骤标 blocked,其他照常,整体记 partial;不做自动回滚——项目文件靠 git 撤销,本机配置由下次收敛修复,整体撤销走 remove_project_mcp。
- 审查转入(P8c 独立审查 ⑧⑨,待统筹合并时决定是否并入 acceptance):P8c 已在计划里单列 codex_trust 与 next_actions;P8d 还需单列 bypass_permissions(项目 .claude/settings.json 将写入 permissions.defaultMode=bypassPermissions 时),machine_changes 补产品组与 buff、.git/hooks(lefthook install)两步,否则 P8g 视图拿不到数据。apply 只走 onboard::plan::write_project_files(与计划同一写入序列),并把它调用的 commands::* / mcp_inject 写入函数逐步下沉到 onboard/ 或中立模块。
### 📋 P8e — 投影收口与契约同步
- status: todo
- complexity: S
- files: src-tauri/src/mcp_tools.rs, src-tauri/src/lib.rs, src/lib/commands.ts, docs/ai-context/project-context.yaml, docs/ai-context/agent-button-system.md, AGENTS.md
- depends: P8d
- acceptance: "agent_verify: [cargo test mcp_tools] 全局和项目路由的 tools/list 都含 plan_onboarding 与 onboard_project;项目路由上强制本项目,传跨项目参数返回 scope_violation;onboard_project 的 required 含 expected_digest 和 confirmed;annotations 中 plan 为 readOnly、apply 为 idempotent。[pnpm typecheck] 通过。[MCP 全局] 对 enterprise 做 plan 全部 noop,做 apply 结果 done 且各步都是 converged。[读文件] project-context.yaml 已补 plan、digest 与状态语义(T1)。"
- 说明:两个工具的 schema 单独成函数(避开 json! 递归上限),dispatch 里只留一行委托,减少与 A4(rmcp)迁移的冲突面;AGENTS.md 架构速查补 src-tauri/src/onboard/。
### 📋 P8f — register_project 控制面投影
- status: todo
- complexity: S
- files: src-tauri/src/commands/projects.rs, src-tauri/src/mcp_tools.rs
- depends: P8-0
- acceptance: "agent_verify: [cargo test] 空路径、相对路径、不存在的路径、重复登记都被拒绝,不建目录、不执行 git init。[MCP 全局] 对临时 git 仓库依次调用 register_project、plan_onboarding、onboard_project,结果为 done;项目路由的 tools/list 不含 register_project。"
- 为什么(Q7):create_project 只写 DB、MCP 里也没有登记项目的工具,agent 复刻不了人「新建 → 接入」的操作路径(违反同源同边界)。只开放在全局路由,写操作须用户确认;抽出 create_project 本体,连接由调用方注入。
### 📋 P8g — UI 一键接入
- status: todo
- complexity: M
- files: src/components/dashboard/OnboardPanel.tsx, src/components/dashboard/onboardView.ts, src/components/dashboard/onboardView.test.ts, src/components/dashboard/ProjectModal.tsx, src/components/dashboard/BatchAddModal.tsx, src/components/manage/NewProjectModal.tsx, src/components/dashboard/BlueprintModal.tsx, src/lib/commands.ts
- depends: P8e
- acceptance: "agent_verify: [pnpm typecheck && pnpm test] 视图纯函数:blocked 非空时确认按钮禁用并列出文件和原因;not_installed 和 not_applicable 显示为灰色说明、不显示为红色失败;bypassPermissions 和 Codex 信任单独列出;确认时携带 expected_digest。human_verify: ① 用临时 git 仓库新建项目,弹出接入预览;② 确认后 30 秒内看到分步结果、L1 矩阵和「待你提交的 N 个文件」;③ 另试一次在预览里取消,目录零改动;④ 在该目录新开 Claude Code、Codex、DSH 桌面 GUI 会话调 LJ_PROBE_NONEXISTENT,得「开发需求未登记」。"
- 说明:新建 / 模板创建成功后弹出「接入预览」,create_project 本身保持无副作用;批量添加只提供批量入口、不自动接入;BlueprintModal 的「应用接入包」改为先 plan 再 apply。
### ✅ P9 — DSH 桌面 GUI 按工作区连项目地址(会话级插件)
- status: done
- human_verify: "2026-10-02 用户在 DSH 桌面 GUI 终验通过:重启 DSH 后在 enterprise 工作区新建会话,实际调用 list_project_dev_variables(返回 1 条)与 read_project_test_account(name=LJ_PROBE_NONEXISTENT)(返回「开发需求未登记」,即项目地址与认证均生效);在 dev-manager-tauri 工作区(控制面,null 停止哨兵)新建会话,无 list_project_dev_variables 工具。"
- complexity: M
- files: src-tauri/src/mcp_inject.rs(render_dsh_block 追加插件行), src-tauri/resources/dsh/lianjing-session-mcp.v1.mjs(新增,随二进制打包), src/lib/dshSessionPlugin.test.ts
- depends: P1
- acceptance: "agent_verify: [cargo test] 托管块同时含 mcp-lian-jing 与 lianjing-session-mcp 两行、插件路径为绝对路径、roots 与 normalize_dsh_root 一致;[vitest] 假 ctx 下按会话 cwd 命中项目才挂载、null 哨兵与未命中不挂、shim 失败/超时不抛错且无未处理 rejection;[DSH 0.2 与 0.1 运行时] 临时 DSH_HOME + stub MCP 服务,ACP session/new 两个不同 cwd 的会话只有项目会话产生 /mcp/projects/<id> 请求。human_verify: DSH GUI 在 enterprise 工作区新建会话能看到开发变量工具,在其他工作区看不到。"
- 为什么:2026-10-01 人终验确认 DSH 桌面版会话只连全局地址。只读调研(两路独立 + 对抗复核)结论:DSH 桌面版只有一个 Host 进程,启动目录固定为 profile 目录,!!js 只在启动时求值、拿不到会话信息,mcp-client 的地址在实例化时固定,也不读任何项目级 MCP 配置,所以只改托管块做不到。可行路径是 DSH 自带的 Agent 作用域:每个 GUI 会话的 session.header.cwd 即工作区路径,会话创建时串行发出 agent/created;在 agent.ctx 上挂 dsh-mcp-client,工具只对该会话可见并覆盖同名全局工具(DSH 内置的 ACP 就这样按会话挂 MCP)。
- 风险(实施前需人确认是否值得):依赖 DSH 内部接口(agent/created、agent.ctx.plugin、ctx.loader.import、按 scope 分账),DSH 升级可能失效,须特性探测并降级为全局;Host 有 installFailLoud,插件任何未处理异常都会打崩桌面 Host;agent/created 是串行事件,挂载须加短超时,炼境离线时静默降级;Node 按 URL 缓存 ESM,插件升级须换文件名;子代理看不到父会话作用域的工具,是否给子代理单独挂载需人定;属于往 DSH 进程注入代码,信任边界与现有 !!js + execFileSync 相同,但需按「管家准入」论证。
- 可行性实测(2026-10-02,用户确认「先实测再实现」):临时 DSH_HOME + 记录请求的桩 MCP 服务 + 原型插件(%TEMP%\lj-p9-spike),经 ACP 在同一进程内新建两个不同 cwd 的会话。DSH 0.2.0-rc.2(桌面版内置)与 0.1.5-rc.2(enterprise 本地)结果一致:插件按绝对路径从 home patch 加载;ctx.loader.import('@deepseek-ai/dsh-mcp-client') 成功;每个会话触发 agent/created 且带 session.header.cwd;项目内会话挂载后桩服务收到 /mcp/projects/<id> 的 initialize 与 tools/list(带认证头),另一会话无任何请求。0.1.x 没有 profileContext,事件里也没有 source 字段。未实测:挂载工具对该会话模型的可见性、桌面 GUI 热加载(只有代码阅读结论)。实现需在 PR #24 合并后从 master 开分支。
- 实现(2026-10-02,分支 feat/agent-client-parity-p9):插件随二进制打包(include_str!),ensure_dsh_patch 刷新时写到 %USERPROFILE%\.lian-jing\dsh\(内容不变不写),托管块在全局行之后追加 lianjing-session-mcp 条目(port、shim、roots 以 JSON 写入,路径含引号空格也合法);插件文件写失败时只保留全局行。插件行为:headless 交给全局行(不重复挂载);特性探测 agents/on/loader.import 与 mcp-client 的 Config/apply,不符即降级为全局;按会话 cwd 匹配白名单,命中才在 agent.ctx 挂 serverName=lian-jing 的 streamable-http 客户端,经稳定入口异步取认证(≤3 秒,失败则不带认证头挂载),整个挂载 ≤5 秒;所有回调吞掉异常;会话销毁时释放;子代理(header.origin=subagent)按 cwd 挂载,但带工具限制(toolFilter)或无法判断时不挂——挂在子代理自己作用域的工具不受限制约束。
- 对抗审查(11 个 agent,6 条成立均为 low、已修):受限子代理会绕过 toolFilter → 经 tools.view 判断,受限即不挂;版本号只编进文件名时运行中的 DSH 不会重载 → 条目 id 同带版本号(lianjing-session-mcp-v1),并加内容哈希测试拦漏升版本;插件文件缺失时 DSH 0.1.x 启动失败 → 条目加 `disabled: !!js` 文件存在性守卫;GUI 工作目录为 realpath → 白名单同时写入登记路径的规范形态(junction/8.3 短名);补分层停止哨兵、取认证失败/超时用例;验收记录更正(首轮端到端的补丁只有插件行,见下)。
- verification: "2026-10-02:cargo test 247 passed(托管块双条目、版本化 id 与存在性守卫、插件内容哈希、规范路径键);vitest 8 条插件用例通过(含分层哨兵、取认证失败与超时、受限/未受限/无法判断的子代理),typecheck 通过。端到端第一轮的补丁只有插件一行;审查后补测完整托管块(从真实 ~/.dsh 复制的全局行 + 插件行 + 存在性守卫 + 停止哨兵,临时 DSH_HOME + 桩服务,认证经真实稳定入口):DSH 0.2.0-rc.2 与 0.1.5-rc.2 上项目子目录会话产生 /mcp/projects/<enterprise id> 的 initialize 与 tools/list 且带认证头,其他目录会话无任何请求;插件文件缺失时两个版本都正常启动、会话正常、插件条目不加载。审查者另测:全局行与插件共存时 headless 只有一组连接;项目地址 tools/list 很慢时会话创建在约 5 秒上限内返回。"
- known_limits: "未实测桌面 GUI 内:挂载工具对会话模型的可见性(DSH 的 scope 覆盖只有代码阅读与 ACP 探针结论)、改托管块后的热加载(审查者在 ACP 开启 hmr 实测 id 变化会重载;GUI 不生效时重启 DSH)。0.1.x 上工具在会话创建后约几十毫秒才就绪,极端情况下首轮请求可能只看到全局工具。依赖 DSH 内部接口,升级后可能失效(插件会降级并在 stderr 打 [lian-jing] 提示)。flaky:mcp_server 的 scoped_mcp_create_pull_request_uses_synthetic_credential 在全量并行测试中偶发失败(单跑通过,与本卡无关)。"
### ✅ P3b — 接入写入幂等:同内容不重写、Codex 托管块不累积空行
- status: done
- complexity: S
- files: src-tauri/src/lib.rs, src-tauri/src/mcp_inject.rs, src-tauri/src/commands/blueprint.rs, src-tauri/src/commands/cicd.rs, src-tauri/src/commands/agent_infra.rs, src-tauri/src/mcp_tools.rs
- acceptance: "agent_verify: [cargo test] 连续 upsert Codex 托管块两次字节不变,已累积的空行被吸收,块外后续内容保留;CRLF 检出的同内容 AGENTS/CLAUDE 托管块返回 unchanged 且不被 LF 重写;语义相同但键序/行尾/末尾换行不同的 settings.json 不被重写;CRLF 检出的同内容 CI workflow 的 plan 为 noop;[MCP inject_mcp ×2] enterprise 的 .codex/config.toml、.claude/settings.json、.lian-jing/context.json、.claude/liangjing.json 第二次注入修改时间不变。"
- 为什么:2026-10-01 enterprise 未提交改动归属梳理发现两处炼境侧缺陷。① Codex 托管块自带结尾换行,又原样保留 END 之后的换行,每注入一次多一个空行(enterprise 已累积 16 行)。② sync_blueprint_rules 与注入在内容不变时也用 LF 重写文件:子项目开 core.autocrlf,CRLF 检出的同内容文件因大小变化被 git status 误报为改动(CONVENTIONS.md、context.json),修改时间也被刷新,干扰了「这次改动是谁做的」的判断。
- verification: "2026-10-02:新增 lib.rs write_if_content_changed(忽略行尾比较,内容不变不写);sync_blueprint_rules 的 CONVENTIONS 与托管块、context.json、.claude/settings.json、Codex 托管块统一改用它,托管块未变时返回 unchanged。cargo test 246 passed。实测 enterprise:注入后 .codex/config.toml 末尾空行 16→0;再次注入四个文件修改时间均不变。已存在的伪改动(context.json、CONVENTIONS.md)需在子项目里由人刷新一次索引。
对抗审查(9 个 agent,2 条成立、5 条驳回)后补:JSON 写入(settings.json、context.json)改为按语义比较——Claude Code 改写后键序与末尾换行不同,按文本比较仍会整文件重排;CI workflow 的 plan 改为忽略行尾判 noop(原按字节比较,CRLF 检出的同内容文件会被判 update 并请用户确认一次无变化写入),apply 与 generate_workflow 落盘同样只在内容变化时写;同类遗漏 agent_infra 生成文件与 notes-archive 回填也统一改用 write_if_content_changed。cargo test 245 passed(新增 JSON 语义与 CI CRLF 回归断言)。"
### ✅ P10 — 接入包升级前检测目标文件的块外未提交改动
- status: done
- complexity: S
- files: src-tauri/src/commands/onboarding.rs(apply 预览 / 结果), src-tauri/src/commands/blueprint.rs(BLUEPRINT_MANAGED 标记常量), src-tauri/src/mcp_tools.rs(MCP 输出), src/lib/commands.ts, src/components/dashboard/BlueprintModal.tsx, docs/ai-context/project-context.yaml
- depends: P8
- acceptance: "agent_verify: [cargo test] 目标文件在 git 中有托管块之外的未提交改动时,apply_onboarding_pack 的预览与结果对该文件给出 warning(含文件名与「块外有未提交改动,升级后将成为混合文件」提示),默认仍只改托管块、不改块外内容;无未提交改动或非 git 仓库时无此提示。"
- 为什么:2026-10-01 enterprise 的 docs/ai-context/git-workflow.md 在有项目未提交改写的情况下被升级了托管块(1.6.0→1.7.0),产生「同一文件混有炼境与项目两种来源改动」的混合文件。按「禁止字符串手术」铁律它无法拆分提交,只能交人定案,verify_onboarding 也因此持续报版本不符。管家应在制造混合文件之前提示,而不是事后由人收拾。
- 实现(2026-10-03,分支 feat/agent-client-parity-p10):TemplateResult 新增可选 warning。apply_onboarding_pack 在模板分发与 buff 蓝图规则同步前后夹一层检测:写入前对 AGENTS.md、docs/ai-context/git-workflow.md、CLAUDE.md 跑 `git --no-optional-locks status --porcelain -uall -- <文件>`,有改动再取 `git show HEAD:./<文件>`(不在 HEAD 中按空内容算,即未跟踪/仅暂存/尚无提交),两边去掉 ONBOARDING_MANAGED 与 BLUEPRINT_MANAGED 块后比较(只忽略 CRLF/LF 与文末托管块连同其前空白,见下方审查修复);写入后只对本次确被改写的文件给出 warning(含文件名与「块外有未提交改动,升级后将成为混合文件」),CLAUDE.md 不在模板结果里,预警时补一条结果。git 不可用、非仓库或命令失败一律静默。只提示不阻断,块外内容照旧不动。MCP 单项目输出在该文件行下加 ⚠️ 行,批量输出追加「混合文件」清单;炼境 UI 接入包结果行下显示琥珀色预警。
- verification: "2026-10-03:新增 3 个测试——outside_managed_content_strips_blocks_and_ignores_eol_and_blank_lines(纯函数:剔除两种块、忽略 CRLF 与空行、不在 HEAD 时有块外内容才算、残缺块按块外处理);mixed_file_warning_only_for_uncommitted_changes_outside_managed_blocks(临时 git 仓库,走与 apply 相同的检测夹层 + 真实 sync_blueprint_rules:git-workflow.md 块外未提交改写 → updated 且带预警、块外改写原样保留、托管块升到模板版本;未跟踪 CLAUDE.md 含项目内容 → 补 appended 结果并预警;与 HEAD 一致的 AGENTS.md 照常升级无预警;全部提交后只改托管块版本并整体转 CRLF → 升级且全部无预警、不补 CLAUDE.md 行);mixed_file_warning_is_silent_outside_git(非 git 目录升级无预警,--nocapture 确认未走跳过分支)。变异自检:把探测函数强制返回 true,后两个测试均失败,已还原。cargo test --locked:lib 270 passed / 5 ignored,0 failed;pnpm typecheck 通过;pnpm test 16 passed。"
- review_fixes: "2026-10-03 审查修复三条:① 原比较逐行 trim_end 并丢弃全部空行,块外删空行(Markdown 并段)或加行尾空格(硬换行)会漏报——改为托管块原位留占位符,只把文末的占位符连同其前空白按 trim_end 归一(抵消炼境追加时的 trim_end + 空行),文中块两侧空行与行尾空格照常比较;② 后续步骤(buff 先同步蓝图规则、再写 DB)报错时预警随 Err 丢失——改为先标注再返回,错误信息并入混合文件名;③ 补「块外有未提交改动但托管块已是最新、本次不改写」情形,测住 annotate 的改写判断。新增测试 outside_managed_whitespace_edits_count_as_changes、mixed_file_warning_survives_failure_after_write,主场景测试补情形三。"
- known_limits: "仓库里没有 apply_onboarding_pack 的预览命令(acceptance 写的「预览」无处可挂),预警随结果返回:检测在写入前完成,但升级照常进行,调用方拿到结果时混合文件已经产生——真正「先提示再决定」要等 P8 的只报告 diff 模式复用本检测。从未提交过的炼境新建文件(块外整段是模板 TODO)在模板升版后重跑也会预警(不在 HEAD 中、无法区分块外内容来源),属安全方向的误报。buff 之外单独调用 sync_blueprint_rules(UI 同步蓝图规则、批量同步)不经此检测。仅比较 HEAD 与工作区:暂存区与工作区不同的中间状态不单独判断。文末托管块之前、之后的纯空白增删(如块前空行数、文件末尾多出空行)为抵消炼境追加差异仍被忽略。"