dev-manager-tauri/.blueprint/modules/agent-client-parity.md
lanrtop 9d85609287
All checks were successful
Push & PR Check / check (push) Successful in 1m2s
fix(onboard/parity): 处置 P8g 独立审查——新建入口按有效根目录弹出、Toast 置于面板之上、bypass 与错误阶段文案如实
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 02:06:29 +09:00

380 lines
164 KiB
Markdown
Raw 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: done
- 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(实际另含 src-tauri/src/onboard/probe_l1/tests.rs、src-tauri/src/onboard/mod.rs、src-tauri/src/mcp_inject.rs(三端 l1_facts、本体入口、本机定位)、src-tauri/src/lib.rs、docs/ai-context/project-context.yaml)
- 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)。"
- 实现(2026-10-03,分支 feat/agent-client-parity-p4a):新增 src-tauri/src/onboard/probe_l1.rs(本体,受 onboard 适应度测试约束)与 probe_l1/tests.rs。三端并行、整次预算 9 秒,每步取「步骤上限与剩余预算」的较小值,耗尽记 timeout。进程、HTTP、客户端定位经 ProcessRunner / McpHttp / ClientLocator 注入(另注入 %VAR% 展开),生产实现 SystemRunner(不弹窗、stdin 置空、去掉 CLAUDECODE 与 CLAUDE_CODE_*、超时或命中停止标记即结束直接子进程)与 UreqHttp。各端证据:① Claude:项目目录执行 claude mcp get lian-jing 取实际生效条目(Claude 自己的回答为准,不复刻项目键推导);再以该条目作唯一 --mcp-config(--strict-mcp-config)运行 claude -p --model lj-no-such-model --no-session-persistence --output-format stream-json --verbose,读到 system/init 即结束,取版本、lian-jing 连接状态、工具名与 skills(B 升为 client_introspection)。headersHelper 不在 mcp get 输出里,按 URL 回查 ~/.claude.json 覆盖本根目录的条目。② Codex:codex --version 与 codex mcp get lian-jing --json;看不到条目而项目配置有该表 → codex_project_untrusted(H-P8-1)。Codex 输出会把 http_headers_helper 脱敏成 \"<redacted>\",重放用的 helper 从项目 .codex/config.toml 同名表回查(与 P3 托管块写法往返测试)。③ DSH:白名单复用 P8b dsh_static_outcome;会话级插件核对托管块引用当前版本条目且文件 sha256 与内置源一致(忽略行尾);版本读安装包 app.asar 的 package.json,不起进程。认证链重放:Claude/Codex 把配置里的 helper 交给 cmd.exe /d /s /c(原样拼接,%VAR% 由 cmd 展开),DSH 按托管块 !!js 的写法执行 \"\"<shim>\" <digest>\";helper 入口先展开 %VAR% 判存在,不存在记 shim_missing、不起 cmd;Claude/Codex 取不到认证头时 C–F 同记失败(连接不建立),DSH 按其 !!js 的 catch 降级为不带认证连接,只有 F 失败。随后在该端配置的地址上 initialize(clientInfo=lian-jing-l1-probe)→ tools/list(与 tools_list_result_for_project 比对)→ get_diagnostics(D)→ list_project_dev_variables(E)→ read_project_test_account(name=LJ_PROBE_<8 位随机>)(F:返回「开发需求未登记」即认证通过;F 的结果文本一律不进报告)。认证头只存在于重放函数栈上,报告文本另经 scrub 兜底。适配器注册表 ClientAdapter 新增 l1_facts(三端实现在 mcp_inject;不适用时不读客户端配置);本体入口 mcp_inject::probe_client_parity(project_id, tier)(tier 校验在最前,不查登记、不起进程)。claude mcp get 的解析合并为一份(mcp_inject 的同步逻辑改调 probe_l1::parse_claude_mcp_get),has_manual_codex_entry 改调 probe_l1::codex_has_table。投影:MCP probe_client_parity(只在全局路由,readOnly/idempotent,structuredContent + 一行摘要,schema 单独成函数);Tauri async command probe_client_parity(spawn_blocking)并在 lib.rs 注册;commands.ts 补 ClientParityL1Report 等类型与 probeClientParity。mcp_server:clientInfo.name 为 lian-jing-l1-probe 的 initialize 与 lian-jing-readiness 一样不计入 has_initialized_project_client。T1:docs/ai-context/project-context.yaml 的 MCP 工具接口与 Tauri command 契约补述。
- 证据矩阵结构(L1Report):project_id、tier=l1、probe_spec_version=1、profile(project / control_plane)、expected_route、expected_tools(自省值)、nonce、clients[{client, version, route(该端实际会连的地址), cells{A..F: {state, cause?, evidence, detail}}, duration_ms}]、all_applicable_ready、duration_ms、note。state ∈ ready / fail / unknown / not_installed / not_applicable / blocked / timeout;evidence ∈ static < client_introspection < protocol(< session_event / server_trace,留给 L3 与服务端轨迹);cause 按环境(timeout、unreachable、client_error)→ 投影(not_configured、route_global、route_mismatch、not_connected、codex_project_untrusted、server_disabled、manual_config、rules_not_loaded、skill_not_visible、session_plugin_mismatch、shim_missing、helper_failed、auth_header_missing、auth_failed、tools_mismatch、tool_error)→ 适配器(adapter_drift)归因。C 的证据:Claude 在 init 自省与协议重放都通过时记 protocol;重放失败用重放的(更具体)归因,重放通过而 Claude 自己没连上或工具不符时记 client_introspection 的失败。
- verification: "2026-10-03:全量 cargo test 349 passed / 0 failed / 6 ignored(新增 24 条 + 1 条 ignored 实机冒烟),编译 0 warning;pnpm typecheck 通过。onboard::probe_l1 20 条:进程内桩 MCP 服务(std TcpListener,只记方法/工具/clientInfo/认证是否匹配,60 秒自动退出)+ 真实 cmd.exe 执行的哑 shim + 假 claude/codex:三端 C–F ready 且 C–F 为 protocol、Claude 的 B 为 client_introspection、三端附版本、桩只收到 lian-jing-l1-probe 的 initialize 且 F 均带对认证、报告 JSON 与进程参数不含桩 token、Claude init 参数与子进程卫生(坏模型名、strict、no-session-persistence、MCP_CONNECTION_NONBLOCKING=false、git-bash、去 CLAUDECODE/CLAUDE_CODE_*、cwd、停止标记);错误 token → 三端 F auth_failed、C/D/E ready;shim 缺失 → Claude/Codex C–F shim_missing、DSH 只有 F shim_missing、不起 cmd;端口不可达 → C–F unreachable;未安装 → 全部 not_installed、零进程、不计失败;控制面 → Claude/DSH 整列 not_applicable(control_plane)、零 claude 进程、Codex 照常;Codex 看不到条目 → codex_project_untrusted / not_configured;Claude 生效全局条目 → route_global 且不起 init;工具集漂移 → tools_mismatch(期望取 tools_list_result_for_project);Codex 无 helper → F auth_header_missing;预算耗尽 → timeout、零进程;helper 非零退出或输出非 JSON(输出里带 token)→ helper_failed 且 token 不进报告;Claude init 视角覆盖 C、skills 缺失判 B、init 缺失时注明;DSH 会话插件不符判 C、白名单缺失判 route_global;P4-0 夹具解析(claude-init-badmodel 含 CRLF、codex-mcp-get-trusted/untrusted)、claude mcp get 文本、%VAR% 展开、TOML helper 回查、静态 A/B、合成 asar 版本。mcp_inject 2 条(l1_facts 控制面/WSL 不适用且不读客户端配置、Codex helper 与托管块写法逐字往返;Claude 候选条目按键的具体程度排序);mcp_tools 2 条(probe_client_parity 只在全局目录、readOnly、required 只有 project_id、tier 只有 l1、描述不含敏感读取工具名、项目路由调用得 scope_violation、tier=l3 在本体最前被拒;摘要列出版本与归因);mcp_server 既有 readiness 测试补 l1-probe 断言。变异(先 git add 暂存,改后跑测、git checkout 还原):去掉 mcp_server 的探针排除 → readiness 测试失败;403 归因改成 tool_error → 错误 token 测试失败;去掉 shim 存在预判 → shim 缺失测试失败;tools_diff 恒为一致 → 工具集漂移测试失败;DSH 改为 helper 失败即中止 → shim 缺失测试失败(审查 ⑨ 更正:原记「与预算耗尽两条失败」有误——预算为 0 时 run_helper 在起 cmd 前返回 TimedOut,中止与降级两条路径都得到全 timeout,预算耗尽测试不受该变异影响;按代码推演,另一条失败的应是 helper 失败用例(断言 DSH 的 D 为 ready),本次未重跑);把认证头写进 D 的 detail 时 scrub 能兜住(测试仍过),再去掉 scrub → 「结果 JSON 含桩 token」失败;l1_facts 不再判控制面 → mcp_inject l1_facts 测试失败;Claude C 不再采用 init 视角 → init 视角测试失败。实机冒烟(#[ignore] 的 l1_live_probe_against_running_lianjing,后台 bash、运行中的炼境,未经 MCP 投影):enterprise 三端 18 格全部 ready(Claude B 为 client_introspection、C–F 为 protocol),附版本 claude 2.1.90 / codex 0.155.0-alpha.9.2 / dsh 0.2.0-rc.2,1.35–1.64 秒;dev-manager-tauri(控制面)Claude、DSH 整列 not_applicable(control_plane)、Codex 6 格 ready,2.8 秒;未接入的 LiteLLM 归因为 Claude route_global(生效全局条目)、Codex not_configured、DSH route_global、A/B rules_not_loaded/skill_not_visible,0.5 秒;三份输出均无 Bearer 与 64 位十六进制串。"
- known_limits: "① Claude 定位沿用 P1 起的 find_claude_bin:优先 %USERPROFILE%\\.local\\bin\\claude.exe(本机 2.1.90),而用户 PATH 上 pnpm 安装的 claude 是 2.1.173——L1 报告的是炼境实际调用的那一个(注入同用它),与用户终端里的 claude 可能不同;定位顺序影响注入,本卡未改。② Claude init 只挂 claude mcp get 选出的生效条目(--strict-mcp-config):自省的是「Claude 用这条条目能否连上、看到哪些工具与 skills」,不是一次完整的项目配置加载;全量配置下 init 要等 5–6 秒无关 MCP 服务器、自然结束要 13–14 秒(要等一次必然失败的模型请求),超出 10 秒预算。③ 读到 init 即结束只终止直接子进程:只挂 HTTP 条目所以没有 stdio 子进程,但超时时孙进程可能残留(Job Object 归 P4d)。④ Codex 的 helper 被 Codex 脱敏,重放用项目 .codex/config.toml 的同名表;生效配置若来自用户级 ~/.codex/config.toml 记 unknown(not_configured)。Codex 执行 helper 时的环境白名单(H4 env_clear)未复刻,重放继承炼境进程环境;Claude 的 helper 失败按「连接不建立」处理是保守假设(H4 只实测了 Codex)。⑤ codex 只认 CODEX_BIN、Codex 桌面版自带的 %LOCALAPPDATA%\\OpenAI\\Codex\\bin\\<哈希>\\codex.exe(取最新)与 PATH 中的 codex.exe,npm 全局安装的 codex.cmd 记 not_installed。⑥ DSH 没有零模型自省:C 的路由是静态判定(白名单),D–F 为协议重放;桌面 GUI 会话只核对插件哈希,未实测会话。⑦ 探针的 Claude 自省(claude mcp get 的健康检查与零模型 init)以 claude-code 身份连项目地址,服务端无法与真实会话区分:检查期间(≤9 秒)服务端对该项目路由上的 initialize 一律不计入 has_initialized_project_client(审查 ③ 后的时间窗静默),恰好在此期间新开的真实会话也不计入,reload_required 可能多提示一次重启(宁可误报,不漏报);静默只在炼境进程内有效,实机冒烟测试进程里调用不影响运行中的炼境。⑧ A 只判规则文件存在(Claude 另要求 CLAUDE.md 引用 @AGENTS.md),版本归 verify_onboarding;B 对 Codex/DSH 只有静态证据。⑨ [MCP 全局] 两项 acceptance 须在合并、运行中的炼境重编译后实测,本卡保持 🔵。⑩ claude -p 在项目目录启动仍会更新 ~/.claude.json(启动计数;从未打开过的目录新增 projects 键),参数关不掉;hooks 已用 --settings {\"disableAllHooks\":true} 关闭(实测 2.1.90/2.1.173 的项目 hooks),托管策略(managed settings)里的 hooks 是否同样被关闭未验证。⑪ 只执行炼境托管写法的 helper(当前 %USERPROFILE% 或旧 %LOCALAPPDATA% 稳定入口 + 本项目 digest):手写条目与更早的本机 exe 路径写法记 unknown(manual_config),L1 不替它们重放认证链。⑫ 静默守卫在 mcp_inject::probe_client_parity_at 的接线(一行)没有单测覆盖,守卫本身由 mcp_server 测试覆盖。"
- 有意偏差:① 卡片与设计写 Claude 自省用 claude mcp get,派单写以坏模型名 init 为主——两者都做:mcp get 定生效地址,init 取 skills、工具与连接状态;init 加 --strict-mcp-config 只挂生效条目、读到 init 即止(理由见 known_limits ②)。② 格子状态用卡片口径 ready,而非设计 §6 的 pass;auth_failed、shim_missing、unreachable 等是 cause(state 为 fail),not_installed、not_applicable 是 state。③ probe_client_parity 只开放全局路由(与 P4e 的 start_parity_probe 同口径;项目路由的只读 get_client_parity 归 P4e)。④ 卡片未列、新增两个归因:helper_failed(helper 非零退出或输出不是 JSON 请求头)与 auth_header_missing(配置里没有 helper)。⑤ 文件比卡片多 src-tauri/src/lib.rs(注册 Tauri command)与 docs/ai-context/project-context.yaml(T1)。
- review: "2026-10-03 独立只读审查 5b804bb,结论 fix_then_merge,11 条发现处置:① [中] SystemRunner 收到 stdout 行即 continue,跳过 try_wait 与超时检查,持续刷行的子进程永不超时 → 已修:每轮按剩余时间计算接收等待(≤50 毫秒),收到行后同样检查退出与超时;单行读取与 stderr 留存都限 MAX_CAPTURE。补 system_runner_times_out_on_flooding_child_and_stops_at_marker(真实 cmd 持续刷行、1 秒超时 → timed_out 且耗时 < 2 秒;刷行循环有界(40 万行,约 10 秒),回归时测试失败而不是挂死;另验停止标记)与 system_runner_removes_inherited_env_by_name_and_prefix(env_remove 的前缀与全名两种写法、env_set)。② [中] 读超时靠英文子串判定,Windows 上 WSAETIMEDOUT 是本地化消息,记成 tool_error → 已修:is_timeout_error 沿 source 链找 io::Error,kind 为 TimedOut/WouldBlock 即 Timeout;正文读超时(into_json)同样记 Timeout;ConnectionFailed/Dns 仍记 unreachable。补 silent_server_read_timeout_is_timeout_not_tool_error(只进 backlog、不响应的端口:UreqHttp 直接得 Timeout;三端 C–F 记 timeout(timeout))。③ [中] 探针的 Claude 自省把项目记成已有真实客户端,抹掉 reload_required → 已修,口径为时间窗静默:mcp_server 新增 quiet_project_initializes 守卫(按项目计数,并发检查先结束的不提前解除,只作用于被检查项目),probe_client_parity_at 在 run_l1 期间持有,期间该项目路由上的 initialize 一律不计入 has_initialized_project_client。未采用审查建议的标记头:实测 Claude 会把静态 headers 与 headersHelper 合并(标记头对 init 可行),但 claude mcp get 的健康检查无法打标,单靠标记头修不全,时间窗一并覆盖两者;代价写入 known_limits ⑦。补 l1_quiet_window_hides_claude_self_introspection_from_reload_evidence。④ [低] 测试假阳性 → 第一处同 ①;第二处已修:unregistered_need_text_is_bound_to_the_real_read_test_account 在内存库上调用真实 commands::dev_variables::read_test_account(new_nonce()),断言报错含 UNREGISTERED_NEED(nonce 生成抽成 new_nonce,顺带绑定「nonce 能通过开发变量名校验」);没有改成从 dev_variables 导出常量,是为了不让 onboard 本体再新增对 commands 层的依赖(P8c known_limits ①)。⑤ [低] Codex 未采用 enabled_tools/disabled_tools → 已修:解析两个字段,期望工具被过滤时 C 记 tools_mismatch(client_introspection),D/E/F 对应工具被过滤时同记;C 为 ready 时 detail 注明 tools/list 只是服务端视角。补 codex_tool_filters_hide_tools_from_the_session_view(黑名单 read_project_test_account → C、F 失败;白名单只放 get_diagnostics → C、E、F 失败)。⑥ [低] 归因边角 → (a) 已修:claude_l1_entries 按 URL 在全部项目键(覆盖本根目录者在前、越具体越前,其余键随后,覆盖 worktree 与 NFC 键)、项目 .mcp.json(project)、顶层 mcpServers(user)中回查,条目带作用域标签,probe_claude 优先取与 claude mcp get 报告的作用域一致的那条;补 mcp_inject claude_l1_entries_match_project_url_in_every_scope(替换原 claude_entries_covering_root_prefer_most_specific_key)与 probe_l1 claude_helper_is_looked_up_in_the_scope_claude_reports。(b) 已修:L1Source::Codex 新增 project_url,项目表存在且与 Codex 生效地址不同 → codex_project_untrusted(detail 写明多半来自用户级 ~/.codex),不再建议重跑 inject_mcp;补 codex_effective_entry_elsewhere_with_project_table_is_untrusted。(c) 已修:helper 输出中没有 Authorization(如 {})→ F 记 auth_header_missing;补 helper_without_authorization_header_is_auth_header_missing(三端)。⑦ [低] claude -p 的 hooks 与 ~/.claude.json 副作用 → hooks 已修:init 加 --settings {\"disableAllHooks\":true},测试断言该参数。实测(2026-10-03,%TEMP%\\lj-p4a-fix,零凭据桩服务、坏模型名,claude 自然退出,不结束任何进程):2.1.90 与 2.1.173 都接受该参数,init 照常发出(桩 connected,0.7–1.7 秒),total_cost_usd=0;项目 .claude/settings.json 的 SessionStart、UserPromptSubmit hook 不加参数时执行,加上后不执行;Claude 的 clientInfo.name 为 claude-code。~/.claude.json 写入关不掉,记入 known_limits ⑩。⑧ [低] 安全加固 → 第一项已修:init 的 --mcp-config 只带 type、url、headersHelper(测试夹具给 Claude 条目加了带桩 token 的静态认证头,断言进程参数不含 token、条目键只有这三个)。第二项已修:L1Project 新增 managed_helpers(当前 %USERPROFILE% 稳定入口与旧 %LOCALAPPDATA% 入口,均带本项目 digest),Claude/Codex 配置里的 helper 逐字等于其一才执行,否则 C–F 记 unknown(manual_config),Claude 也不起零模型 init(init 会让 Claude 自己执行该 helper);更早的本机 exe 路径写法同样记 manual_config。补 unmanaged_helper_is_manual_config_and_never_executed(只起 DSH 的一个 cmd、不起 init)与 mcp_inject managed_helpers_cover_current_and_legacy_shim_forms。⑨ [信息] 变异记录不准 → 复核成立,verification 已更正(见该字段 DSH 一句)。⑩ [信息] Claude 未连上时 D–F 仍为 ready → 已修:Claude 明确未连上(init 状态不是 connected 或 mcp_servers 里没有 lian-jing;取不到 init 时退用 claude mcp get 的 Status 行,只采信「未连接」)时,D–F 中协议重放为 ready 的格改记 blocked(not_connected, client_introspection),detail 注明服务端视角通过;Claude 已连上但 init 的工具名里没有 D/E/F 对应工具时,该格记 tools_mismatch。ClaudeMcpGet.connected 用作取不到 init 时的退路。改断言报备:claude_init_view_overrides_c_when_replay_passes 中「Claude 未连上时 F 为 ready」改为「D–F 为 blocked(not_connected)」——这是审查要求的判定口径变化,不是为了让实现通过测试;该用例另补两段(工具缺失、mcp get 退路)。⑪ [信息] 定位器加在存量 mcp_inject、AGENTS.md 未提 L1 → 定位器本卡不迁移(已经 trait 注入,P4d 移到 onboard);AGENTS.md 架构速查 onboard 行补上 plan 与 probe_l1。project_root 在函数内取连接池,与 client_static_status 等本体入口写法一致(入口层),未改。T1:probe_client_parity 工具描述(状态补 blocked、只执行托管 helper、init 关 hooks)与 project-context.yaml 同步。修复后全量 cargo test 360 passed / 0 failed / 6 ignored(比 349 多 11 条),编译 0 warning。之后把两条 SystemRunner 测试里的 cmd 死循环改成有界循环(防回归或变异时测试挂死、留下孤儿 cmd),这一改动之后没能重跑。变异验证本轮未做:按 §2 先 git add 暂存、再改源码注入变异时,Claude Code auto mode 安全分类器以 Security Weaken 拦截(命令未执行,git diff 确认工作区没有变异残留);随后 git fetch、cargo test、查看 node_modules 也被拦截,按授权规则停下,未绕过。"
- 待办:本轮被安全分类器拦截后停下,改动已 git add 暂存、未提交、未推送、未开 PR。接手者:重跑全量 cargo test 与 pnpm typecheck(commands.ts 本轮未改)→ fetch 并合并 origin/master 后重测 → 提交推送 feat/agent-client-parity-p4a → 开 PR;合并后统筹会话在重编译的炼境上实测 [MCP 全局] 两项,通过后标 ✅。
- mcp_verify: "2026-10-03 统筹会话:PR #38(a79bcb7,修复阶段 agent 被安全分类器拦截后由统筹接手提交、合并 master 并解决与 P8f 的相邻插入冲突,合并结果 cargo test 377 passed、pnpm typecheck 通过)合并为 cf2b5f0;tauri dev 重编译生效后经全局 MCP 实测:probe_client_parity(enterprise, tier=l1) 往返 3302 ms(服务端 duration_ms 1749),三端 18 格全部 ready(claude 2.1.90 / codex 0.155.0-alpha.9.2 / dsh 0.2.0-rc.2,C–F 证据为 protocol);probe_client_parity(dev-manager-tauri) 往返 411 ms,Claude 与 DSH 整列 not_applicable(control_plane)、Codex 6/6 ready。两次输出扫描无 Bearer 与疑似 token。"
### 📋 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 仍可用。
- e2e_verify: "2026-10-03 22:5x 统筹会话端到端(炼境 d7f4816 重编译后经全局 MCP):新建临时 git 仓库 E:code-projectlj-p8-e2e-20261003 → register_project 得 project_id=ac73a4bb-a601-4e21-bc0f-7eda71fae4aa(目录落库为规范反斜杠形式;换大小写/正斜杠/尾分隔符再登记被拒并给出已有 ID)→ plan_onboarding:文件 create 14、本机 token/claude/codex/dsh/product_group_buff/git_hooks=apply、shim=noop、阻断 0、bypass_permissions=true、codex_trust=untrusted(进 next_actions)→ onboard_project(expected_digest=bb60bf69…, confirmed=true) 7.9 s:status=done,9 步全收敛,completion 全 ready(AGENTS/git-workflow 管理块最新、身份、能力包 1.9.0、两份 skill、Codex MCP 块、lefthook 挂载与检查段、Claude local、Codex 托管块、DSH 白名单、L1 适用格、改动可报告、HEAD 不变),L1:Claude 2.1.90 与 DSH 0.2.0-rc.2 各 6/6 ready,Codex C–F 4 格 codex_project_untrusted 单列 pending_user(统筹决定 ⑩);14 个接入文件留待提交、HEAD 不变 → 再次 plan:文件全 noop、七步全 noop(digest 83479aa5…)→ 再次 onboard_project 3.0 s:status=done,9 步全 converged,written_files 为空,14 个文件 mtime 全不变;verify_onboarding 10/12(origin、CI 两项按设计进 next_actions),clients 三端 converged,runtime ready。 human_verify 待用户:在 Claude Code、Codex(先信任该目录)、DSH 桌面 GUI 各新开会话调 read_project_test_account(name=LJ_PROBE_NONEXISTENT) 得「开发需求未登记」;P8g 界面终验。"
### ✅ 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: done
- 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 级);合并后标 ✅。
- merge_verify: "2026-10-03 统筹会话:PR #36(head 7ef9241,CI passed,独立审查 9 条发现 7 修 2 转 P8d)与 PR #35 先后合并为 04db098;合并结果全量 cargo test 325 passed / 0 failed / 5 ignored。P8c 无 MCP/Tauri 投影(P8e 才加),acceptance 全为 cargo 层,据此标 ✅。"
### ✅ P8d — onboard_project 编排本体
- status: done
- complexity: M
- files: src-tauri/src/onboard/apply.rs, src-tauri/src/onboard/report.rs, src-tauri/src/commands/onboarding.rs(实际另含 src-tauri/src/onboard/apply/tests.rs、onboard/plan.rs、onboard/clients.rs、onboard/probe_l1.rs、onboard/mod.rs、src-tauri/src/mcp_inject.rs、src-tauri/src/commands/groups.rs、src-tauri/src/commands/projects.rs(审查 ①:删除登记持接入写锁)、src/lib/commands.ts、docs/ai-context/project-context.yaml、AGENTS.md(T2 架构速查补 apply 与 report))
- 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。;⑨ 计划结构单列 bypass_permissions(项目 .claude/settings.json 将写入 permissions.defaultMode=bypassPermissions 时为 true,可按项目取消)、codex_trust 与 next_actions(沿用 P8c),machine_changes 含 token、shim、claude、codex、dsh、product_group_buff(产品组与 buff)、git_hooks(lefthook install)七步,各带 noop/apply/not_applicable 等状态(P8c 审查转入,2026-10-03 统筹决定并入)。"
- 说明:存量 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/ 或中立模块。
- 实现(2026-10-03,分支 feat/agent-client-parity-p8d):新增 src-tauri/src/onboard/apply.rs(编排本体)、onboard/report.rs(报告与「完成」判定)、onboard/apply/tests.rs。依赖全部经 OnboardDeps 注入:连接、CredentialStore、ClientAdapter 切片、OnboardHost(项目专用地址与托管 helper 写法、期望工具集、稳定入口状态与写入、Codex 信任预判)、ProcessRunner(lefthook install)、L1Deps、接入写锁。plan_project 与 onboard_project 共用同一份计划演算(本机七步 + plan::plan_onboarding 的项目文件演算)。onboard_project 顺序:项目级运行锁(同一项目第二个调用立即 busy,不排队)→ 预检(未登记、空路径、相对路径、根目录不存在 → failed)→ confirmed=false → refused → 接入写锁内重算计划:digest 不符(附当前计划与 digest)或 blocked 非空(列出阻断项)→ refused;以上零写入。之后按依赖执行:identity 与 project_files(同一写入序列 plan::write_project_files,经记录实际改写的 TrackingSink 落盘,按文件归属拆两步)→ git_hooks(只在 git 仓库根执行 lefthook install,缺可执行文件 needs_human,非 git 或仓库子目录 not_applicable)→ product_group_buff(只写 DB:不在任何组时建独立产品组;buff 缺失或非 active 时登记为 active、保留同步进度;同目录 buff 属别的项目 needs_human、不改写)→ token → shim → claude / codex / dsh(经注册表 apply;token 或 shim 未完成时三端 blocked(dep=token|shim)、适配器不被调用)。ClientOutcome → 步骤状态:manual_kept / deferred → skipped,not_installed 与控制面 / WSL → not_applicable,other_project / unavailable / needs_human → needs_human,failed / missing → failed。释放写锁后跑 L1:客户端步骤未完成的一端整列 blocked(L1 Cell 新增可选字段 dep、Cause 新增 dependency),不进 run_l1、不起进程;再跑 L0(verify_onboarding)与三端只读判定。不自动回滚;炼境不 add、不 commit。
- 报告结构(OnboardReport):project_id、status(done / partial / failed / refused / busy)、summary、reason、digest、plan(refused 时附当前计划)、blocked、bypass_permissions、steps[{step, status(+dep / reason), detail}](identity、project_files、git_hooks、product_group_buff、token、shim、claude、codex、dsh,本机步骤与计划 machine_changes 同名)、completion[{item, source(l0 / clients / l1 / repo), ready, detail}]、l1(L1Report,每格带证据等级)、l3(说明:L3 尚未提供)、written_files(本次实际写入,审查 ③ 后补)、repo_changes(available{status_porcelain = git status --porcelain --untracked-files=all -- .(限定项目根), diff_stat, onboard_files[{path, code}], other_changes} / unavailable{reason})、head{before, after, unchanged}(非 git 为空)、suggested_commit_message(只针对 onboard_files;Conventional Commits,有新文件记「一键接入」、只有修改记「收敛」)、next_actions、duration_ms。done = 全部步骤 converged / applied / skipped / not_applicable,且 completion 全部 ready:L0 中除 origin 指向 Gitea、CI workflow、飞轮 commit、Node/pnpm 工具链 resolver、production delivery profile 之外的检查(这五项未就绪时进 next_actions,检查名由测试在真实检查链上绑定)、三端只读判定已收敛、L1 适用格全部就绪、git 仓库可报告改动、HEAD 未变。next_actions:计划的 codex_trust,commit(有待提交的接入文件时:在项目根 git add -- <onboard_files>,审查 ③ 后补),gitea_repo(注明工具默认建公开仓库,私有须显式 private=true)/ ci / flywheel / toolchain / delivery_profile,lefthook(缺可执行文件),restart_dsh_gui(DSH 托管块有改动),claude_trust(Claude local 新写入),reopen_sessions(项目配置或客户端注册有改动时:已打开的会话需重开,L1 是新起的检查进程,不据 reload_required 推断已重载——P4a 审查 ③)。
- 计划(⑨):OnboardingPlan 新增 bypass_permissions(写入后 permissions.defaultMode=bypassPermissions 而写入前不是,即本次会新写入);OnboardOptions / ProjectWrite 新增 bypass_permissions(默认 true 保持现状;false 即按项目取消:settings.json 缺 permissions 时写显式 defaultMode=default 记住选择,之后的重注入同样只在缺省时写,不会补回;write_settings_compat_with / write_project_identity_files_with 加参数,inject 传 true)。选项不同计划不同,digest 不同。machine_changes 七步固定顺序:token、shim、claude、codex、dsh、product_group_buff、git_hooks。
- 下沉(P8c 审查 ⑧ / known_limits ①⑤):本机七步的组装从 mcp_inject::plan_machine_steps(依赖真实凭据存储与用户目录、无注入点、无单测)移入 onboard::apply(可注入、有测试);token_step、outcome_status 移入 onboard::plan(测试随迁);控制面判定 wants_global_scope / project_facts 移入 onboard::clients(mcp_inject 原函数改为薄包装);lefthook install 本体移入 onboard::apply::lefthook_install(ProcessRunner 注入,60 秒超时),commands::onboarding::run_lefthook_install 只做 HookActivation 映射;独立产品组建组(只写 DB、事务)移入 onboard::apply::ensure_standalone_group_with,groups::ensure_standalone_group 改为「建组 + 注入」薄包装。
- 生产入口(供 P8e 投影调用的内部边界):mcp_inject::plan_onboarding(project_id, options) 与 mcp_inject::onboard_project(project_id, expected_digest, confirmed, options),均 #[allow(dead_code)];P8e 的 MCP 工具与 Tauri command 只在其上加一行委托,不复制任何判定。SystemOnboardHost 实现宿主环境(shim 状态逻辑从原 shim_step 迁入);with_onboard_deps 组装池连接、WindowsCredentialStore、registry()、SystemRunner、UreqHttp、SystemLocator 与 MCP_OP_LOCK;onboard_project 全程持 quiet_project_initializes(注入时 claude mcp get 的健康检查与 L1 的零模型 init 都以 claude-code 身份连项目地址,同 P4a 审查 ③ 口径),结束写一条 runtime_logs(category=onboard,只含 project_id 与 summary)。存量 apply_onboarding_pack 改为 #[tauri::command(async)](入参与返回不变)。T1:commands.ts 的 ParityCell 增 dep、ParityCause 增 dependency;project-context.yaml 补一键接入本体语义与 apply_onboarding_pack async。
- 步骤依赖图:identity、project_files、git_hooks、product_group_buff 互不依赖;token → shim → {claude, codex, dsh}(token 或 shim 未完成则三端 blocked);L1 的 Claude / Codex / DSH 列分别依赖 claude / codex / dsh 步;L0 与 completion 在全部步骤之后。审查后:接入写锁内先重新解析登记,git_hooks 在释放写锁后执行(报告里仍排第 3 位),人确认跳过的一端记 skipped、不调用适配器。
- verification: "2026-10-03:全量 cargo test 394 passed / 0 failed / 6 ignored(新增 onboard::apply 14 条、onboard::report 3 条;token_step 测试随函数迁到 onboard::plan),cargo check 与测试编译 0 warning;pnpm install --frozen-lockfile 后 pnpm typecheck 通过。onboard::apply(in-memory DB、临时 git 仓库、MemoryCredentialStore、假适配器 / 宿主 / lefthook 执行器 / 定位器 / HTTP):① first_apply_is_done_with_head_unchanged_and_uncommitted_changes_reported——plan 后 apply 得 done,九步全 applied,HEAD 与提交数不变,repo_changes 与 git status --porcelain --untracked-files=all 逐行一致且全为 ?? 未跟踪,建议提交信息为 chore(lian-jing),报告 JSON 不含 token,产品组与 active buff 已登记;⑦ 同一用例:新项目无 origin / CI / 飞轮仍 done,三项进 next_actions、不在 completion,Gitea 一项含「默认建公开仓库」,另有 reopen_sessions;② second_plan_is_noop_and_second_apply_converges_without_touching_files——再计划文件全 noop、本机七步全 noop,再 apply 九步全 converged,项目文件 mtime 全不变,lefthook 与稳定入口不再执行,不再建组;③ digest_mismatch_unconfirmed_or_blocked_plan_is_refused_without_writes——错 digest(附当前 digest 与计划)、confirmed=false、选项不同、身份冲突的阻断计划均 refused,目录(含 .git)字节不变,适配器 0 次、无 token、无进程、DB 无组与 buff;④ claude_failure_is_partial_blocks_its_l1_column_and_rerun_after_fix_is_done——Claude 适配器失败 → partial,codex / dsh applied,L1 Claude 列 6 格 blocked(cause=dependency, dep=claude) 且未进 L1 定位,修复后重跑 done;⑤ unregistered_empty_or_relative_paths_fail_without_writes——未登记、空串、空白、相对路径、不存在的绝对路径均 failed、plan 也报错,零写入、无 token、cwd 下未出现相对目录;⑥ non_git_directory_reports_unavailable_changes_and_is_not_done——repo_changes unavailable、head 为空、git_hooks not_applicable,status=partial;⑧ concurrent_apply_on_same_project_returns_busy——第一个 apply 停在 Claude 步时,第二个调用方(另一条连接)立即 busy、零写入,放行后第一个 done,之后再 apply 照常;⑨ plan_lists_seven_machine_steps_bypass_permissions_and_codex_trust——七步名与顺序、全 apply,bypass_permissions=true,Codex 未信任只进 next_actions 不阻断;取消后计划 bypass_permissions=false、digest 不同,应用后 settings.json 为 defaultMode=default,按默认选项再计划不会补回且文件全 noop;项目已有 permissions 时不单列。另:shim 失败 → 三端 blocked(dep=shim)、适配器 0 次、L1 三列 blocked;缺 lefthook → git_hooks needs_human + next_action lefthook + partial;next_actions 检查名在真实 verify_onboarding 检查链上逐一存在;lefthook_install 本地优先、PATH 先裸名后 .cmd、非零 / 超时 / 成功映射;产品组与 buff 步幂等、外项目 buff 不改写;项目根是别的仓库子目录时不在外层仓库执行 lefthook。onboard::report:ClientOutcome → 步骤状态与 settled 映射、StepReport 扁平序列化、HEAD 移动如实报告与非 git 跳过、建议提交信息。变异验证(先 git add 暂存,再用 Edit 注入、跑 cargo test onboard::、git checkout 从暂存区还原,未用 sed):第一轮同时注入 6 处——去掉 digest 核对、运行锁恒放行、L1 不再标 blocked 列、TrackingSink 不记写入、git_hooks 不认已接管、bypass 判定恒 false——恰好 7 条预期用例失败(③、⑧、④、shim、①、②、⑨),其余 55 条通过;第二轮注入 3 处——token/shim 门控失效、next_action 检查全部计入「完成」、HEAD 断言恒为不变——shim、head_check 与全部期望 done 的 5 条用例失败;第三处单独注入「仓库子目录也执行 lefthook」→ 子目录用例失败。"
- known_limits: "① 绞杀者过渡未完:写入序列仍调 commands::onboarding::dispatch_templates_with、commands::blueprint::sync_blueprint_rules_with、mcp_inject::write_project_identity_files_with / upsert_codex_mcp_config_with,登记解析调 mcp_inject::project_root_with_conn,L0 调 commands::onboarding::verify_onboarding,三端适配器仍在 mcp_inject;适应度测试只做文本匹配,抓不到这些依赖方向。没整体搬迁的原因:这些函数正是 P6、P8f 后续小修(L4 路径归一、I2 UNC 拼接)要改的地方,搬迁冲突面大;本卡只下沉了可注入、无冲突的部分(见「下沉」)。git 只读观测(report 的 git status / diff / rev-parse、计划的混合文件检查、git_hooks 的仓库判定)直接起 git、不经 ProcessRunner、没有超时(审查 ⑧;只改 report.rs 会成半迁移,统一归绞杀者后续)。② 测试里客户端一律「未安装」:L1 的 C–F 判定由 P4a 测试覆盖,本卡只证明编排(blocked 列不进 L1、其余端进 L1、all_applicable_ready 重算);真实三端 apply + L1 的端到端须在 P8e 合并、运行中的炼境重编译后由统筹实测,本卡保持 🔵。③ 生产组装(with_onboard_deps、SystemOnboardHost、quiet 守卫与日志)无单测:依赖真实凭据存储、用户目录与连接池,由 P8e 的 [MCP 全局] 实测覆盖;SystemOnboardHost::shim_state 是原 shim_step 的逐行迁移。④ Codex 已安装但未信任目录的新项目:L1 的 Codex 列为 codex_project_untrusted,status=partial(next_actions 有 codex_trust,summary 提示信任后重跑)——「完成只按三端可用」的如实结果。⑤ git_hooks 与 verify 同口径只认项目根 .git/hooks/pre-commit:worktree(.git 是文件)每次都会重跑 lefthook install、verify 的「lefthook hook 挂载」不就绪;core.hooksPath 未考虑;项目根是别的仓库子目录时 git_hooks 记 not_applicable,同样到不了 done。⑥ 项目级运行锁只拦同一项目的并发一键接入;存量 apply_onboarding_pack 的模板分发不持接入写锁,与一键接入并发时仍可能交错(重新解析登记、重算计划、digest 核对与除 git_hooks 外的写入在写锁内,只与同样持锁的 inject / remove / 删除登记 / 根目录迁移 / 批量重注入快照串行)。update_project 改根目录是先提交登记、再由 on_project_root_changed 持锁迁移:锁内重新解析之后才提交的改根,本次仍写旧根,随后的迁移线程清理旧根的 Claude local 条目并在新根重注入(token 已有),项目文件留在旧根。⑦ L3 只给说明字符串(P4d/P4e 未做),没有「最近一次 L3 结果及是否过期」的真实数据。⑧ product_group_buff 步不经 ensure_standalone_group 注入(注入由 token / shim / 三端步骤完成,避免二次注入);buff 同项目非 active 时只改回 active、保留同步进度,存量 apply_blueprint_buff 的 INSERT OR REPLACE 未改。⑨ lefthook install 改经 SystemRunner(apply_onboarding_pack 同受影响):执行器只给错误文本,分不出「不存在」与其他启动失败——项目本地可执行文件存在却起不来记 failed(附启动错误、不回退 PATH,同 P8d 前「本地存在只用本地」),PATH 候选都起不来记 missing_binary(附各候选的启动错误,PATH 上存在但被拦截时会被说成「未找到」,错误文本可辨);新增 60 秒超时,超时只结束直接子进程。⑩ 写入期间别人提交会使 HEAD 断言为 false、status=partial,炼境不判定提交归属,只提示核对。⑪ onboard_files 归属按文件粒度:本次写入的,加上写入序列判过存在、读过或写过且仍未跟踪的;写入序列管理、本次没写的已跟踪文件改动(如上次接入改了托管块却未提交)归 other_changes(宁可少列);用户对 .claude/settings.json 等本次写入的非托管块文件另有未提交改动时,会随该文件进入 onboard_files(不做行级拆分;托管块文件有混合文件阻断兜底)。⑫ skip_targets 只作用于计划时的阻断项;跳过 shared_key 的一端 L1 仍照常检查、多半未就绪,status=partial(跳过不等于该端可用)。"
- 有意偏差:"① acceptance ④ 写 blocked(dep=claude_local),实现记 dep=claude:步骤名与计划 machine_changes 同名(⑨ 后定的 claude / codex / dsh),便于 P8g 对照「预计 → 实际」。② status 比任务列的四态多一个 busy(acceptance ⑧「返回 busy」)。③ repo_changes 的 porcelain 带 --untracked-files=all(逐个列出新文件,否则只列 .claude/ 这样的目录),测试同口径比对。④ 「完成」比卡片列举多要求「基础能力包描述符」ready(接入包托管的版本描述,与托管块版本同类);verify 以后新增的检查默认计入「完成」(保守,宁可 partial)。⑤ bypassPermissions 的「按项目取消」做成计划与应用共用的选项 OnboardOptions.bypass_permissions,取消即写显式 defaultMode=default(否则之后的重注入会补回);P8e 投影时加这个可选参数。⑥ 文件比卡片多,见 files。⑦ 审查 ② 按设计 §4 C1 实现「逐项跳过」:选项 OnboardOptions.skip_targets(进 digest),计划单列 skipped;P8e 投影给 plan_onboarding 与 onboard_project 都加这个可选参数,P8g 用它做逐项「跳过 / 中止」。⑧ git_hooks 在释放接入写锁后执行,报告 steps 中仍排第 3 位(顺序不变,便于 P8g 展示)。⑨ 删除登记持写锁(审查 ①)改在 commands/projects.rs 的 G8 本体 delete_project_registration(注入写锁与清理闭包),超出原 files,是关掉删除竞态的必要配套。"
- review: "2026-10-03 独立只读审查 6c69aa2,结论 fix_then_merge,10 条发现处置:① [中] 锁外解析、digest 不绑根目录 → 已修:写锁内重新解析登记(登记消失 failed;根目录或 McpTarget(端口、地址、helper)与锁外不一致 refused 并附当前计划);OnboardingPlan 新增 root(将写入的目录)并进 digest;SystemOnboardHost 每次 target() 重读端口;delete_project 改为持 MCP_OP_LOCK 依次执行 remove_project_mcp_locked 与删登记的数据库部分(Gitea 网络清理在锁外),关掉「清理释放锁后、删登记前」的窗口;reinject_all_mcp 的目标快照改在写锁内取(正在进行的一键接入结束后才取,避免锁内用旧端口、快照时还没 token 的项目停在旧端口)。补 registration_changes_while_waiting_for_write_lock_are_caught_inside_the_lock(文件 DB 两条连接:apply 停在写锁前时删登记 / 迁根目录 / 改端口 → failed / refused / refused,两个目录零写入、无 token、无组)、digest_confirmed_for_one_root_is_refused_after_root_moves(同名目录迁到另一父目录、文件计划一致仍拒绝)、plan_shows_root_and_binds_it_into_digest;G8 用例 deleting_project_registration_removes_its_webhook_secret 加断言:清理闭包在写锁内、先于删登记执行,结束后释放锁。② [中] 阻断项无跳过入口 → 已修(按设计 §4 C1 实现,未登记为延后):OnboardOptions 新增 skip_targets,只有 manual_entry 与 shared_key 可跳过(BlockReason::skippable),移入计划新增的 skipped 并进 digest;apply 时被跳过的文件级问题不算 project_files 失败(detail 注明保留现状),对应一端记 skipped(manual_kept|other_project)、不调用适配器;identity_conflict、mixed_file、needs_human 列进来照旧阻断,refused 文案提示 skip_targets。OnboardOptions 因含 Vec 不再 Copy,plan_project / onboard_project 改收 &OnboardOptions。补 manual_codex_entry_can_be_skipped_but_identity_conflict_cannot(默认 refused;跳过后 codex skipped、适配器 0 次、手写文件字节不变、project_files applied;手写条目未指向本项目地址,「Codex MCP 已配置」如实未就绪 → partial;跳过与未跳过的 digest 互不通用;身份冲突列进 skip_targets 仍 refused)、identity_conflict_and_needs_human_cannot_be_skipped,并扩充 manual_codex_entry_and_machine_manual_states_block。P8g 接口形态:plan(skip_targets) 展示 blocked(可跳过的给「跳过」)与 skipped → apply 带同一 skip_targets 与 digest。③ [低] 提交建议混入用户改动 → 已修:报告新增 written_files;TrackingSink 另记写入序列管理的文件;repo_changes 用 pathspec「-- .」限定项目根,新增 onboard_files[{path(相对项目根), code}] 与 other_changes;建议提交信息只看 onboard_files,next_actions 新增 commit(项目根 git add -- <onboard_files>,有其他改动时提示勿 git add -A)。补 user_changes_are_kept_out_of_the_onboarding_commit(未跟踪 notes.md 与改了一半的 README.md 计入 other_changes=2、不进 commit 提示);子目录用例补外层仓库未提交文件不出现、onboard_files 相对项目根;report 单测补 porcelain -z 解析与「managed 但本次未写的已跟踪改动不算接入文件」。④ [低] 一键接入不写 mcp_inject 事件 → 已修:onboard::report::inject_event 纯函数(未写入不记;token、shim、三端步骤全部完成记 info,否则 warn 列出未完成步骤),mcp_inject::onboard_project 在 onboard 日志后补写 category=mcp_inject(context 带 project_id)。补 inject_event_follows_injection_steps_only;生产写日志一行未单测。⑤ [低] codex 步「预计 apply → 实际 converged」与 blocked 文案失实 → 已修:ClientAdapter 新增默认方法 project_config_file()(CodexAdapter 返回 .codex/config.toml);本次 project_files 写过该文件且适配器复核 converged 时记 applied(detail 注明由 project_files 写入、本步复核);token / shim 门控未过时 codex 的 blocked 说明如实写「文件已写入、helper 引用的稳定入口或 token 未就绪,修复后重跑生效」。测试的 Codex 替身改为读写真实 .codex/config.toml,① 断言计划 apply → 报告 applied 且 detail 提 project_files,shim 用例断言 codex 文案。⑥ [低] lefthook 启动错误被吞 → 已修:保留各候选启动错误;本地可执行文件存在却起不来记 failed 并附错误(不误报「未找到」、不回退 PATH);PATH 候选都起不来记 missing_binary 并附错误。lefthook_install 用例补两条断言。⑦ [低] lefthook 与建组 / buff 占写锁 → 部分修:git_hooks 移到释放写锁后执行,① 用例经执行器替身断言 install 时写锁空闲;product_group_buff 有意留在锁内(只写 DB、毫秒级,与删除登记串行,避免给已删项目建组或登记 buff)。⑧ [信息] report.rs 的 git 观测绕过 ProcessRunner → 未改代码:计划的混合文件检查、Codex 信任预判同样直接起 git,只改 report.rs 会成半迁移;登记 known_limits ①,apply.rs 模块文档如实改写边界。⑨ [信息] P8e 投影的 async 写法 → 采纳为 P8e 指引(P8e 卡说明已补);project-context.yaml 把 apply_onboarding_pack「在线程池执行」的不实表述改为「在异步运行时任务里执行、占一个 worker」,代码未改(审查认可与 buff.rs、groups.rs 一致)。⑩ [信息] 非 git、仓库子目录、worktree、Codex 未信任到不了 done → 未改,转统筹决定(codex_project_untrusted 是否视为 next_action 不计入 done;非 git / 子目录是否补 git_init / 由外层仓库接入的 next_action);known_limits ④⑤ 已登记。修复后全量 cargo test 402 passed / 0 failed / 6 ignored(较 394 新增 8 条),编译 0 warning;pnpm typecheck 通过。改断言报备:report 单测 head_check 中「只改 a.txt 即给收敛提交信息」改为「a.txt 不是接入文件 → 无接入提交信息、计入其他改动;列入本次写入才给收敛信息」——提交建议语义按审查 ③ 有意收窄;① 用例的建议提交信息断言由前缀 chore(lian-jing) 收紧为「一键接入」,lefthook 缺失断言加启动错误文本(均为加强);① 用例「九步全 applied」未改,codex 步在新语义下仍为 applied(原先靠替身不写文件成立,现由真实文件耦合成立)。变异验证(git add 暂存后用 Edit 注入、跑 cargo test onboard:: commands::projects、git checkout 从暂存区还原,未用 sed):第一轮 5 处——去掉锁内根目录 / 端口比对、git_hooks 移回锁内、digest 去掉 root、接入文件判定放宽、删登记的清理移到锁外——恰好 7 条预期用例失败、79 条通过;第二轮 5 处——codex 两处 project_files 耦合(blocked 文案、applied 映射)、被跳过的文件级问题算失败、本地 lefthook 起不来当缺失、partial 不补写注入事件——恰好 6 条失败、80 条通过;第三轮身份冲突与 needs_human 可跳过 → 2 条失败;第四轮被跳过的一端仍调用适配器 → 1 条失败。"
- 待办:开 PR(M 级,分支 feat/agent-client-parity-p8d);P8e 合并、运行中的炼境重编译后由统筹做端到端实测,再标 ✅。
- merge_verify: "2026-10-03 统筹会话:PR #39(2f2a9f1,CI passed;独立审查 10 条:6 修、1 部分修、2 登记/转指引、1 转统筹)合并为 d018d44,合并结果全量 cargo test 402 passed / 0 failed / 6 ignored。acceptance ①–⑨ 均为 cargo onboard::apply 层,据此标 ✅;端到端(新登记项目 plan → onboard_project)随 P8e 合并后由统筹实测。统筹决定(审查 ⑩):codex_project_untrusted 按设计 Q5 计为 next_action、不计入 done(只放宽这一种原因),非 git 目录维持不能 done(acceptance ⑥),子目录与 worktree 补 next_action 说明——由 P8e 实现。"
- 统筹决定落地(审查 ⑩,2026-10-03,P8e 实现,详见 P8e 卡):codex_project_untrusted 视为 next_action、不阻断 done——L1 的 Codex 列里仅因 cause=codex_project_untrusted 未就绪的格不计入「L1 适用格」,completion 单列一项「Codex 信任」(source=l1、ready=false、新增字段 pending_user=true,不计入 done),next_actions 保留一条 codex_trust(计划已列不重复;计划预判已信任而 L1 仍显示未信任时补一条)——设计 Q5:炼境不写 Codex 信任表。其余原因的 Codex 格(含 blocked)照常计入;L1Report.all_applicable_ready 保持 L1 自身的原始判定。非 git 目录维持不能 done(acceptance ⑥);仓库子目录与 worktree / 子模块(.git 为文件)补 next_action repo_layout,说明「lefthook hook 挂载」只认项目根 .git/hooks、按当前口径到不了 done,并给出外层仓库根或实际 hooks 目录与怎样才能 done。由此 known_limits ④「Codex 未信任 → partial」改为 done + pending_user;⑤ 的判定口径不变,只补说明。
### ✅ P8e — 投影收口与契约同步
- status: done
- 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(实际另含 src-tauri/src/commands/onboarding.rs(Tauri command 本身)、src-tauri/src/mcp_inject.rs(去掉生产入口的 dead_code 标注)、src-tauri/src/onboard/report.rs、onboard/apply.rs、onboard/apply/tests.rs(统筹决定 ⑩))
- 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/。
- P8d 审查转入(2026-10-03):① onboard_project / plan_onboarding 的 Tauri 投影用 async fn + tokio::task::spawn_blocking(同 probe_client_parity),不要用 #[tauri::command(async)] 包同步函数——那会让函数体占住一个 tokio worker,而 MCP 服务端跑在同一运行时、一键接入可能 30 秒以上。② 两个投影都接受可选参数 bypass_permissions 与 skip_targets(OnboardOptions,计划与应用须一致,否则 digest 不符);报告新增 written_files、repo_changes.onboard_files / other_changes 与 next_action commit,计划新增 root 与 skipped,写进 commands.ts 类型与 project-context.yaml。
- 实现(2026-10-03,分支 feat/agent-client-parity-p8e):① MCP:mcp_tools 新增 plan_onboarding_schema / onboard_project_schema(各自单独成函数,选项参数 bypass_permissions(布尔)与 skip_targets(字符串数组,描述写明只有 manual_entry / shared_key 可跳过)由 add_onboard_option_properties 共用,保证两工具选项同名同义),插在旧的分步入口 apply_onboarding_pack 之前;dispatch 各一行委托 plan_onboarding_call / onboard_project_call——只做 JSON 类型解析(选项、expected_digest、confirmed 类型不对在任何 IO 之前报错,缺省 confirmed=false、缺省选项取 OnboardOptions::default),再调 mcp_inject::plan_onboarding / mcp_inject::onboard_project,不复制任何判定。onboard_project 的 required 为 project_id、expected_digest、confirmed;annotations:plan_onboarding 按 plan_ 前缀为 readOnly + idempotent,onboard_project 进幂等列表(非只读、非破坏、非 openWorld)。两者 schema 带 project_id,is_project_scoped_tool 自然放行:项目路由 project_id 改 const 且可省略,跨项目参数经 bind_project_scope 返回 scope_violation。描述写明先把计划给用户看、用户明确确认后再带 expected_digest 与 confirmed=true 调 onboard_project,两次选项须一致;Codex 需用户在 Codex 里信任目录(炼境不写信任表);Gitea 建仓默认公开、私有须 private=true。两者进 STRUCTURED_TOOLS:structuredContent 为 OnboardingPlan / OnboardReport 原样序列化,content 一行摘要(计划:根目录、文件 create/update/noop 计数、本机七步、阻断与跳过、bypassPermissions、Codex 信任、digest 与下一步;报告:本体 summary、待提交接入文件数、next_actions 名);refused / busy / failed / partial 以报告 status 返回,不作工具错误。list_all_projects 与 scan_unregistered_projects 的引导改为 plan_onboarding → 用户确认 → onboard_project。② Tauri:commands::onboarding 新增 plan_onboarding(project_id, options?) 与 onboard_project(project_id, expected_digest, confirmed, options?),均为 async fn + tokio::task::spawn_blocking(同 probe_client_parity),只一行委托 mcp_inject 生产入口;lib.rs 注册;mcp_inject 两个生产入口及 codex_trust 等辅助函数去掉 #[allow(dead_code)]。commands.ts 新增 OnboardOptions、OnboardingPlan(含 root、skipped)、OnboardReport(含 written_files、repo_changes.onboard_files / other_changes、completion[].pending_user)等类型与 planOnboarding / onboardProject 封装,字段与 Rust 序列化一一对应。③ 统筹决定(P8d 审查 ⑩):onboard::report 新增 l1_readiness——L1 的 Codex 列里 cause=codex_project_untrusted 的未就绪格计为「只等 Codex 信任」,不计入「L1 适用格」;有这类格时 completion 单列「Codex 信任」(source=l1、ready=false、CompletionItem 新增字段 pending_user=true),done 判定跳过 pending_user 项,next_actions 已有 codex_trust(计划预判)则不重复,没有(计划预判已信任而 L1 仍显示未信任)则补一条;其他列的同一归因、Codex 列其他归因(含 blocked / dependency)照常计入;L1Report.all_applicable_ready 不改写。新增 RepoLayout(NotGit / RepoRoot / Subdirectory{repo_root} / GitFile{hooks_dir})与只读的 report::repo_layout(.git 为目录 / 文件 / 不存在,git rev-parse --show-toplevel 与 --git-path hooks),apply 在报告阶段计算;仓库子目录与 worktree / 子模块给 next_action repo_layout,说明「lefthook hook 挂载」只认项目根 .git/hooks、按当前口径到不了 done(三端可用不受影响)、外层仓库根或实际 hooks 目录与怎样才能 done,partial 摘要指向它;非 git 维持不能 done(acceptance ⑥,由「项目文件改动可报告」未就绪说明,不另给 repo_layout)。done 摘要在有 pending_user 时注明「Codex 列 N 格待你在 Codex 里信任」。P8d 卡已记「统筹决定落地」一行。④ 契约:project-context.yaml 的 MCP 接口条目补两个工具的路由、schema、选项、确认契约与返回结构,身份上下文条目补 completion.pending_user、Codex 信任口径与 repo_layout,Tauri 契约条目补两个 async 命令与 Err / status 的分工;agent-button-system.md 清单补「一键接入子项目」一行与「只读计划 → 人确认同一 digest → 应用」约定;AGENTS.md 架构速查 onboard/ 行补「生产依赖由 mcp_inject 组装,MCP 与 Tauri 投影只一行委托」。
- verification: "2026-10-03:全量 cargo test 413 passed / 0 failed / 6 ignored(master 402,新增 11 条),编译 0 warning;改动的 Rust 文件 rustfmt --check(skip_children)无差异;pnpm typecheck 通过,pnpm test(vitest)4 文件 16 条通过。cargo test mcp_tools 40 条通过,其中 P8e 新增 5 条:onboarding_tools_are_on_both_routes_with_confirmation_contract(全局与项目路由 tools/list 都含两工具;plan required=[project_id],onboard_project required 含 project_id、expected_digest、confirmed 且 confirmed 为 boolean;plan readOnly+idempotent+非破坏,apply 非只读+idempotent+非破坏+非 openWorld;两工具都有 bypass_permissions / skip_targets 且描述含 manual_entry、shared_key;描述含「给用户看」「用户明确确认」「在 Codex 里信任」「公开仓库」「private=true」;排在 apply_onboarding_pack 之前且相邻;项目路由 project_id 为 const=p1、不在 required,onboard_project required 为 [expected_digest, confirmed])、onboarding_tools_on_project_route_reject_cross_project_before_io(项目路由传 project_id=p2 两工具都 isError + structuredContent.code=scope_violation、文案含「绑定 project_id=p1」)、onboard_options_follow_schema_names_and_default(按 schema 声明的选项名构造入参解析正确;缺省与 null 为默认;两工具选项参数完全一致;三种类型错误文案)、onboarding_tools_reject_malformed_arguments_before_io(全局与项目路由:skip_targets 非数组、confirmed 非布尔、expected_digest 非字符串、bypass_permissions 非布尔均在 IO 前报错)、onboarding_responses_carry_structured_content_and_one_line_summary(序列化真实 OnboardingPlan / OnboardReport:structuredContent 原样、摘要含计数、本机步骤、阻断与 skip_targets 提示、digest、待提交接入文件数与 next_actions;status=done 不带 isError)。onboard::report 新增 4 条:codex_trust_only_cells_are_pending_user_and_do_not_block_done(Codex 列 C–F 只因未信任 → done、L1 适用格 ready、单列 1 项 pending_user、补 1 条 codex_trust、计划已列不重复、全就绪时无 pending 项)、other_not_ready_cells_still_block_done(Codex 列混入 auth_failed、别的列出现同一归因、Codex 列 blocked(dependency)、其他 L0 未就绪 → 均 partial)、repo_layout_next_action_explains_why_done_is_unreachable、repo_layout_detects_root_subdirectory_worktree_and_non_git(真实 git 仓库、子目录、git worktree 与普通目录)。onboard::apply 新增 2 条(测试替身 FakeRunner 增加 Codex 自省模拟、FakeLocator 可定位 Codex,lefthook 替身改为装到 git --git-path hooks 与真实 lefthook 一致):codex_untrusted_cells_are_pending_user_and_do_not_block_done(经真实 L1 判定链:codex mcp get 找不到 → C–F 为 codex_project_untrusted → status=done、pending_user 1 项、codex_trust 1 条;计划预判已信任时 L1 结论补 1 条;生效为全局地址 → partial、无 pending 项)、worktree_project_is_partial_and_next_action_explains_why(真实 git worktree:hook 装在主仓库,未就绪的只有「lefthook hook 挂载」,repo_layout 1 条,重跑同样 partial 且 lefthook 再跑);既有 project_in_repo_subdirectory_does_not_touch_outer_hooks 补断言(唯一障碍为 lefthook hook 挂载,repo_layout 含外层仓库名),first_apply 补断言(仓库根无 repo_layout、无 pending 项)。变异验证(全部改动先已 git add 暂存,用 Edit 注入、跑 cargo test --lib -- mcp_tools onboard:: commands::projects、git checkout 从暂存区还原,未用 sed)三轮 10 处全部被杀且失败集合与预期一致:第一轮 4 处——onboard_project 移出幂等列表、plan_onboarding 移出 STRUCTURED_TOOLS、l1_readiness 不限 Codex 列、repo_layout_next_action 恒为 None——恰好 6 条失败(路由契约、摘要、other_not_ready、repo_layout 说明及子目录 / worktree 两条 apply 用例)、126 条通过;第二轮 3 处——onboard_project 不进项目路由、done 判定不跳过 pending_user、.git 为文件也判仓库根——恰好 7 条失败、125 条通过;第三轮 2 处——L1 结论不补 codex_trust、选项解析丢弃 skip_targets——恰好 3 条失败、129 条通过。避开会绕过 IO 前校验的变异(如去掉 scope 绑定、去掉类型检查),以免测试进程调到生产入口。"
- 有意偏差:"① dispatch 的一行委托指向 mcp_tools 内的 plan_onboarding_call / onboard_project_call,再由它们调 mcp_inject 入口:中间只做 JSON 类型解析(类型错误须在任何 IO 之前报错,且与 register_project 的分发写法一致),判定仍全在本体。② onboard_project 的 annotations 为非只读、非破坏、幂等(重复执行收敛)、非 openWorld(只写本机与子项目文件、不出网);plan_onboarding 靠既有 plan_ 前缀规则得 readOnly。③ 统筹决定 ⑩ 的 pending_user 做成 CompletionItem 的通用布尔字段(serde default,目前只有「Codex 信任」一项会置 true),而不是新增 completion 来源;L1 适用格的 detail 注明有几格单列出去。④ 仓库子目录与 worktree 的 next_action 统一名为 repo_layout,同时提示「origin 指向 Gitea」也只读项目根 .git/config(同类口径问题,只说明、不改 verify);子模块与 worktree 同属「.git 为文件」一类,文案按 worktree 写。⑤ 非 git 目录不给 repo_layout(由 completion「项目文件改动可报告」未就绪说明,acceptance ⑥ 不变)。⑥ files 比卡片多,见 files。"
- 没验什么:"① acceptance 的 [MCP 全局](对 enterprise 做 plan 全部 noop、apply done 且各步 converged)与 P8f 的 register_project → plan_onboarding → onboard_project 链路:需合并后在运行中的炼境重编译再调写工具,按授权规则由统筹会话实测,本卡保持 🔵。② Tauri command 两个薄包装与 commands.ts 封装没有自动化测试、没在 UI 里调过(P8g 才有界面),只由 pnpm typecheck 与类型逐字段对照保证;Tauri 入参 options 的 camelCase / snake_case 映射(顶层参数 camelCase、OnboardOptions 内部字段 snake_case)按 Tauri 约定推断,未实机验证。③ MCP 客户端侧的工具超时:首次一键接入可能 30 秒以上,Codex 默认 tool_timeout_sec 等客户端限制没有实测。④ repo_layout 的 git rev-parse 与报告里其他 git 观测一样直接起 git、无超时(P8d known_limits ① 同类)。⑤ 统筹决定的 done 判定只在测试替身与单元层验证;真实 Codex 未信任目录的端到端(L1 显示 codex_project_untrusted → done + pending_user)待统筹实测。⑥ 本卡未做独立审查(S 级)。"
- mcp_verify: "PR #40(97578b2,CI passed,独立审查可合并:3 低 5 信息转后续)合并为 d7f4816。2026-10-03 22:5x 统筹会话端到端(炼境 d7f4816 重编译后经全局 MCP):新建临时 git 仓库 E:code-projectlj-p8-e2e-20261003 → register_project 得 project_id=ac73a4bb-a601-4e21-bc0f-7eda71fae4aa(目录落库为规范反斜杠形式;换大小写/正斜杠/尾分隔符再登记被拒并给出已有 ID)→ plan_onboarding:文件 create 14、本机 token/claude/codex/dsh/product_group_buff/git_hooks=apply、shim=noop、阻断 0、bypass_permissions=true、codex_trust=untrusted(进 next_actions)→ onboard_project(expected_digest=bb60bf69…, confirmed=true) 7.9 s:status=done,9 步全收敛,completion 全 ready(AGENTS/git-workflow 管理块最新、身份、能力包 1.9.0、两份 skill、Codex MCP 块、lefthook 挂载与检查段、Claude local、Codex 托管块、DSH 白名单、L1 适用格、改动可报告、HEAD 不变),L1:Claude 2.1.90 与 DSH 0.2.0-rc.2 各 6/6 ready,Codex C–F 4 格 codex_project_untrusted 单列 pending_user(统筹决定 ⑩);14 个接入文件留待提交、HEAD 不变 → 再次 plan:文件全 noop、七步全 noop(digest 83479aa5…)→ 再次 onboard_project 3.0 s:status=done,9 步全 converged,written_files 为空,14 个文件 mtime 全不变;verify_onboarding 10/12(origin、CI 两项按设计进 next_actions),clients 三端 converged,runtime ready。 偏差报备:acceptance 写的对 enterprise「plan 全 noop、apply 全 converged」——enterprise 的 plan 如实给出 update 3(两份 skill 与 capability-packs 待从 1.8.0 升 1.9.0,因其主工作区本地 main 落后 origin/main),按执行铁律不对他会话在制的工作区 apply;同一组断言(计划全 noop、apply 全 converged、mtime 不变)改在上述新登记项目上实测通过。enterprise 主工作区同步后再 plan 即应全 noop。"
### ✅ P8f — register_project 控制面投影
- status: done
- complexity: S
- files: src-tauri/src/commands/projects.rs, src-tauri/src/mcp_tools.rs, src-tauri/src/commands/templates.rs(实施扩展), src-tauri/src/commands/git_repos.rs(实施扩展), src/components/dashboard/BatchAddModal.tsx(实施扩展), src-tauri/src/mcp_inject.rs(审查处置:路径键两函数改 pub(crate) 供复用), docs/ai-context/project-context.yaml(T1)
- 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 本体,连接由调用方注入。
- 实现(2026-10-03,分支 feat/agent-client-parity-p8f):projects.rs 抽出登记本体 `register_project_records(conn, input, requirement)`(连接注入),UI `create_project` 只做取连接的薄包装(requirement=ExistingDirOrPlaceholder),MCP `register_project` 经 `register_project_at(conn, path, name)` 换算入参后调同一本体(requirement=ExistingDir)——边界只写一处。本体只写数据库:不建目录、不执行 git init、不接入;依次拒绝空 ID、空路径(UI 不选目录的概念占位除外,见审查 M2)、相对路径、不存在的目录、空名称、同一目录已登记、已存在的 ID;目录校验与比较键在写事务外算(要读磁盘),重复检查与 profile+workspace 插入放在同一 IMMEDIATE 事务(沿用 envoy 先查后插的做法),并发登记同一目录时后到者等前者提交后再查。Windows 目录落库为 canonical 规范形式(审查 M1),本体返回落库的根目录。workspace 插入由 `INSERT OR IGNORE` 改为 `INSERT`,ID 冲突不再静默返回成功。MCP `register_project`(schema 单独成函数,插在 scan_unregistered_projects 之后):入参 path(Windows 绝对路径,必填)与可选 name(缺省取目录名);project_id 由炼境生成 UUID(与 UI 选目录新建一致),归入当前空间(UI 首页按当前空间列项目);返回 JSON 并列入 STRUCTURED_TOOLS,structuredContent 带 project_id、name、path、space_id,摘要写明「只写登记、尚未接入,接入先 plan_onboarding 预览、经用户确认再 onboard_project」。is_project_scoped_tool 显式排除 register_project(与 unregister_project、setup_gitea_repo 并列),项目路由 tools/list 不含它,项目路由调用返回 scope_violation;annotations 为非只读、非破坏、非幂等。描述写明调用前须向用户列出目标目录与名称并取得确认。
- 同目录判定(审查后定稿):字面键 `projects::registration_path_key` = 去首尾空白 → `/` 统一为 `\` → 去 `\\?\` 长路径前缀(`\\?\UNC\x` 还原为 `\\x`)→ 交给 `mcp_inject::normalize_dsh_root`(合并重复分隔符、保留 UNC 开头的 `\\`、去尾部分隔符、小写)——与 DSH 白名单共用同一实现,口径不分叉(审查 L4 的部分处置)。已有登记一侧:每条只取按 platform 的有效根目录(Windows 取 win_path、WSL 取 wsl_path;取列与 Linux 路径换算同 mcp_inject::project_root_with_conn,wsl_path 已是 UNC 时原样使用——project_root_with_conn 对这种值会错误拼接,是已有缺陷,见审查 I2),切换平台残留在另一列的旧路径不参与(审查 L1),只算字面键、不读磁盘。新目录一侧:在写事务外算字面键与 canonical 键(`std::fs::canonicalize` 去 `\\?\` 前缀,消掉 8.3 短名、`.`/`..` 段,审查 L2),任一命中即拒绝,错误写明「该目录已登记为项目 project_id=<已有 ID>(<有效根目录>)……请直接使用该 project_id」。
- 其他登记入口(UI 取舍):① 模板新建(create_project_from_template)要先建目录、再跑初始化命令、最后入库,过不了本体「目录须已存在」,所以保持原流程不变,只在建目录前复用 `ensure_path_not_registered`(目录已删但登记仍在时,不再新建出第二条同目录登记;此时目录不存在,只有字面键);② 批量添加(batch_create_projects)保持「跳过不报错」语义,同一目录已登记(含同批前项)计入 skipped,提示文案改为「ID 已存在或目录已登记」,不加存在性与绝对路径校验(扫描结果必为已存在目录);③ 挂载仓库抽出本体 `git_repos::mount_repo_records(conn, input)`:给出路径时在任何写入之前拒绝同一目录,profile 与 workspace 在同一 IMMEDIATE 事务写入,被拒绝或 workspace 冲突都不留孤儿 profile(审查 L3);④ scan_unregistered_projects 抽出 `scan_unregistered_projects_with(conn)`,已登记判定与父目录去重改用同一字面键(审查 M1 连带),末尾提示改为「经用户确认后 register_project 登记」。
- 行为变化(报备):UI「添加项目」此前允许相对或不存在的路径、ID 冲突时静默成功;现在与 MCP 共用本体,这些情况都报错(UI 只能经文件夹选择器给路径,实际只影响「选过的目录后来被删」与 ID 冲突两种情形)。**不选目录的概念占位保持原行为**(审查 M2:首版曾一并拒绝,与 mcp-server 模块「2 个空路径概念占位按用户意图保留」冲突,已恢复;MCP 入口仍拒绝空路径)。Windows 目录落库由原样改为 canonical 规范形式(UI 选择器给的本就是规范长名,实际只影响 MCP 入参)。WSL 平台以 wsl_path 为准校验,Linux 路径的存在性经 `\\wsl.localhost` 判定,settings.wsl_distro 与实际发行版不符时会被拒绝(此时 project_root 同样会解析错,提前暴露)。
- verification: "2026-10-03 审查处置后:全量 cargo test 342 passed / 0 failed / 5 ignored(首版 335,审查处置新增 7 条),编译 0 warning;pnpm typecheck 通过。审查处置新增:projects 4 条——ui_registration_keeps_pathless_placeholder(UI 入口空串/空白/WSL 空路径占位成功且互不拦截、占位不能复用已存在 ID、MCP 入口拒绝占位)、duplicate_lookup_matches_dotted_or_short_input_by_canonical_key(已有行以正斜杠+尾部分隔符落库,新目录以 `sub\\..` 与本机 8.3 短名 TEMP 写法给出,均靠 canonical 键命中)、duplicate_lookup_ignores_stale_column_of_other_platform(platform=wsl 的行残留 win_path 不挡该 Windows 目录登记)、register_project_at_warns_when_directory_carries_another_project_id(遗留 context.json 的旧 ID 进 warning,文件不被改写);git_repos 1 条 mount_rejections_leave_no_orphan_profile(同目录拒绝挂载与 workspace ID 冲突都不留 profile,正常挂载成功);mcp_tools 2 条——register_project_dispatch_follows_schema_parameter_names(按 schema 声明的参数名构造入参经分发函数登记,回执 path 为规范目录、缺 path 报「路径为空」)、scan_unregistered_matches_registrations_regardless_of_spelling(正斜杠大写带尾分隔符的登记与反斜杠登记都不被列为未登记,两种写法的父目录只扫一次)。改动的既有断言(报备):register_project_at 测试原断言「存入去掉首尾空白的路径」改为「存 canonical 规范形式」,入参改为正斜杠+尾部分隔符+首尾空白(审查 M1 的要求本身);ID 冲突测试的「原登记不被改写」改为与 canonical 形式比较;同目录写法测试补 canonical 大写、`sub\\..`、`sub\\..\\.` 三种写法并断言本体返回规范根目录;WSL Linux 路径查找测试改为直接用字面键查(find_registration_by_path 现会 canonicalize,测试不应访问 \\\\wsl.localhost 唤起本机 WSL);其余测试只补 requirement 参数。变异验证(先暂存,改源码,跑 projects::、git_repos::、mcp_tools::,再从暂存区还原)两轮 9 处全部被杀:第一轮 4 处——落库不规范化(taken_id、register_project_at、同目录写法、分发 4 条失败)、关闭 UI 占位(占位测试失败)、mount 事务改为出错也提交(mount 测试失败)、scan 改回字面比较(scan 测试失败),共 7 条失败;第二轮 5 处——去掉 canonical 键(`..`/8.3 查找、同目录写法、register_project_at 与批量添加的 8.3 再登记失败)、已有行不按 platform 取列(残留列与 WSL Linux 路径两条失败)、warning 恒为空(warning 测试失败)、分发读错参数名(分发测试失败)、字面键不去长路径前缀(键测试失败),共 9 条失败。首版(审查前):全量 cargo test 335 passed / 0 failed / 5 ignored(master 为 325,新增 10 条),编译 0 warning;pnpm typecheck 通过。projects 新增 7 条:registration_path_key 归并大小写、斜杠方向、尾部分隔符、重复分隔符、长路径前缀,UNC 保留双分隔符,不同目录不归并;空串、空白、相对路径(含 cwd 下真实存在的 src)、盘符相对路径、不存在的目录全部拒绝,零行写入、无孤儿 profile、不存在的目录未被创建;同一临时目录以大写、小写、正斜杠、尾部分隔符等 5 种写法及改走 WSL 平台再登记均被拒绝,错误含 project_id=first,表内仍 1 行,目录内没有 .git;WSL Linux 路径的登记能按 UNC 写法查到;ID 冲突报错且原登记不被改写;register_project_at 生成 UUID、名称取目录名、归入当前空间、存去掉首尾空白的路径,同目录再登记的错误含已有 project_id;批量添加跳过已登记目录与同批重复目录。mcp_tools 新增 3 条:全局 tools/list 含 register_project(required=[path]、非只读非破坏、描述含用户确认、不建目录、git init、project_id),项目路由 tools/list 不含,schema 加上 project_id 后 is_project_scoped_tool 仍为 false;项目路由调用 register_project 返回 scope_violation;成功回执 structuredContent 带 project_id、摘要写明尚未接入。变异验证(先暂存,改源码,跑 projects:: 与 mcp_tools::,再从暂存区还原)9 处全部被杀:去掉重复目录检查(改 IMMEDIATE 事务前后各验一次)、比较键不小写、不去尾部分隔符、不统一斜杠方向、去掉目录存在校验、去掉绝对路径校验、批量添加不跳过同目录、is_project_scoped_tool 不排除 register_project。"
- 没验什么:① acceptance 的 [MCP 全局] 链路 register_project → plan_onboarding → onboard_project 依赖 P8e 才有的两个 MCP 工具,本卡只实现与测试 register_project;**链路实测待 P8e 合并后由统筹会话完成**(要在运行中的炼境上登记一个临时 git 仓库,属写运行中 DB,按授权规则处理),卡片保持 🔵。② 没在运行中的炼境上调过 register_project(未重编译安装,且会写运行中 DB)。③ IMMEDIATE 事务防并发重复只有结构保证,没有并发测试。④ 模板新建的重复目录检查没有单测(Tauri command 直接取连接池,还要建目录、跑命令),由共用的 ensure_path_not_registered/find_registration_by_path 测试间接覆盖;挂载仓库已抽本体并补单测。⑤ UI 添加项目的报错与占位登记只看了代码路径(ProjectModal 以 toast 显示错误),没有人工点过。⑥ canonicalize 在 junction、符号链接、subst 盘、映射网络盘上的行为(解析到真实目标、映射盘变 UNC)没有实测,测试只覆盖本机 TEMP 的 8.3 短名与 `..` 段。⑦ 审查 M2 的产品决定(是否保留无目录的概念占位、是否改为显式占位概念)没有问过用户,本 PR 只恢复原行为。
- known_limits: "① 已有登记一侧只算字面键:已有行以 8.3 短名或经 junction 的路径落库、新目录用长名或真实路径给出时查不出(UI 选择器给的是规范长名,P8f 之后 MCP 落库也是规范形式,新增登记不会再产生这种行);WSL 路径也按不区分大小写比较(Linux 上仅大小写不同的两个目录会被误判为同一目录,极少见);`\\\\wsl$` 与 `\\\\wsl.localhost` 两种写法不归一。② update_project 把路径改成一个已登记目录不拦(审查 I1,转后续小修);存量的同目录重复登记(如待用户处理的 beast-mode / 940dfbb4)不受影响,仍按统筹文档 §3 由用户执行数据合并。③ 批量添加的 wsl_convert 模式把 E:\\x 存成 /mnt/e/x,与同目录的 Windows 登记比较键不同,查不出这类跨写法重复。④ scan_unregistered_projects 只按 win_path 推导父目录、已有行只算字面键:8.3 短名登记的目录仍可能被列为未登记,对它调 register_project 会被 canonical 键拒绝并给出已有 project_id。⑤ Windows 目录落库为 canonical 形式,junction、符号链接、subst 盘解析到真实目标,映射网络盘变成 UNC:经 junction 路径打开的客户端会话,与按字面比较登记路径的消费者对不上(DSH 白名单同时写字面键与 canonical 键,不受影响)。⑥ 路径比较键仍有多份实现:normalize_trust_path(Codex 信任判断)未并入,WSL 发行版查询与 Linux 路径转 UNC 在 projects.rs、mcp_inject.rs、publish.rs 各有一份(审查 L4,转后续小修)。"
- 有意偏差:① 未加 confirmed 参数——任务要求是「描述写明写操作须用户确认目标」;登记只写炼境 DB、可经 unregister_project 撤销,按 unregister_project 的先例只在描述里约束。② MCP 不收 project_id,统一由炼境生成 UUID,避免 agent 自拟 ID 与已有命名冲突。③ 重复目录检查扩到批量添加、模板新建、挂载仓库三个新建入口(files 标「实施扩展」):PR #34 审查 L1/L2 的根因是同一目录多条登记,只堵 create_project 和 MCP 两个入口治不了根;编辑入口 update_project 仍未堵(审查 I1,转后续小修)。
- T1:新 MCP 工具 register_project 已同步 docs/ai-context/project-context.yaml「MCP 工具接口」段(paths 补 projects.rs,doc 补路由、本体、同目录判定、落库规范化、UI 占位例外、warning 与入参语义,以及 scan_unregistered_projects 与 mount_repo 的同口径);Tauri command 签名未变,src/lib/commands.ts 无需改动。
- review: "2026-10-03 独立只读审查 bd4ebaf,结论 fix_then_merge,10 条发现处置:M1 [中] MCP 入参原样落库(正斜杠、尾部分隔符、长路径前缀、8.3、`..`),scan_unregistered_projects 按字面比较会自相矛盾 → 已修:本体对 Windows 目录 canonicalize 后去长路径前缀落库,register_project_at 回执 path 为落库值、名称缺省按规范形式取目录名(相对路径不 canonicalize,交本体拒绝);scan 的已登记判定与父目录去重改用 registration_path_key。M2 [中] UI 不选目录的概念占位被取消,超出 P8f 范围且与 mcp-server 模块记录的用户用法冲突 → 已恢复原行为:本体新增 RootRequirement,UI 入口有效路径为空时只写 profile+workspace、不校验目录、不查重复,MCP 入口仍拒绝;是否改为显式占位概念交统筹会话问用户(需求澄清),本 PR 不预设答案。L1 [低] 已有登记两列都比,残留列误拦 → 已修:按 platform 只取有效根目录(registration_root),查询补 platform 列。L2 [低] 8.3、`..`、末尾点等漏判 → 已修:新目录在写事务外另算 canonical 键,两键比对已有行的字面键(已有行不读磁盘,写锁内无 I/O)。L3 [低] mount_repo 检查在插 profile 之后且无事务 → 已修:抽 mount_repo_records(连接注入),检查前移、整体 IMMEDIATE 事务;前端先 gitClone 再 mount 的白克隆属 UI 范围,未改。L4 [低] 路径比较键多份实现 → 部分修:registration_path_key 改为复用 mcp_inject::normalize_dsh_root + strip_verbatim_prefix(两函数改 pub(crate));normalize_trust_path、WSL 发行版查询与 UNC 换算的第三份拷贝统一抽 crate::path_key 转后续小修(会动 P4a/P8d 热点 mcp_inject.rs)。I1 [信息] update_project 仍可造同目录登记 → 转后续小修:UI 编辑整表提交,须「有效根目录实际变化才检查 + 排除自身」,否则存量重复登记(beast-mode)连改名都会被拦,单独做更稳。I2 [信息] project_root_with_conn 对 UNC 形式的 wsl_path 错误拼接(已有缺陷)→ 不在本 PR 改(mcp_inject.rs 热点),转后续小修;卡片与代码注释里「同口径」改为只指取列与 Linux 路径换算。I3 [信息] 重登目录遗留旧 context.json 无提示 → 已修:register_project_at 只读检查,project_id 不同时回执与摘要带 warning(不沿用旧 ID)。I4 [信息] 分发分支无测试 → 已修:分发抽成 register_project_call(conn, args),测试按 schema 声明的参数名构造入参;IMMEDIATE 并发仍无测试(见没验什么③)。"
- mcp_verify: "PR #37 合并(55dc637);P8e 合并后经全局 MCP 实测链路 register_project → plan_onboarding → onboard_project 得 status=done(证据同 P8e mcp_verify);同目录换写法再登记被拒并给出已有 project_id。"
### 🔵 P8g — UI 一键接入
- status: in_progress
- 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。
- 实现(2026-10-04,分支 feat/agent-client-parity-p8g,统筹会话主线程直做——两次委派卡死于一次性生成大文件):onboardView.ts 视图纯函数(计划预览、确认请求、报告、L1 矩阵;判定全取后端字段);OnboardPanel.tsx 全局挂载于 App、经 useUIStore.openOnboard 打开(新建弹窗保存即卸载,面板须独立存在),状态机 planning → preview → applying → report,过期响应按请求序号丢弃、接入中不可关闭,refused 附新计划可回预览重新确认;plan.rs 的 BlockedItem 序列化附派生字段 skippable(可跳过判定只在本体,反序列化忽略、不进 digest),commands.ts 同步;入口:ProjectModal 新建且按 platform 的有效根目录非空才弹(概念占位不弹),NewProjectModal 模板创建成功自动弹、完成页可再开,BatchAddModal 添加后列出有目录的新项目逐个「一键接入」,BlueprintModal「施加 Buff」改为同一面板(buff 已激活时另给「重新检查接入」),关闭后按报告刷新 buff 与 init prompt;project-context.yaml 同步 skippable 与 UI 投影。
- 有意偏差:"① 呈现块拆到 OnboardSections.tsx(files 外新增,控制单文件规模)。② Toast 层级 z-50 → z-[70](审查 ②,否则接入提示被面板遮住)。③ 本地 confirm 函数改名 applyConfirmed:confirmUsage.test.ts 扫描 confirm( 防同步确认框,属正确的守卫。"
- review: "2026-10-04 独立只读审查 5b6c197,结论 merge,5 条:① [低] 新建入口按任一路径非空判是否弹面板,与后端按 platform 取有效根目录不同口径 → 已修。② [低] Toast z-50 被面板 z-[60] 遮住 → 已修。③ [低] 已有 permissions 又取消 bypass 时文案称会写 default → 已改文案(如实说明两种情况)。④ [信息] L1 格子着色在前端写死 codex_project_untrusted=待你操作,轻度复制判定 → 转后续小修(L1 格子带 pending_user 字段)。⑤ [信息] onboard_project 抛错后「取消」提示仍称不写入 → 已修(错误阶段换提示)。审查核实:确认带 plan.digest 与生成计划时的选项;refused 回预览时 planned 与新计划同一组选项(apply.rs 用请求选项重算);四条 agent_verify 有实断言。"
### ✅ 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 与工作区:暂存区与工作区不同的中间状态不单独判断。文末托管块之前、之后的纯空白增删(如块前空行数、文件末尾多出空行)为抵消炼境追加差异仍被忽略。"