dev-manager-tauri/.blueprint/modules/agent-client-parity.md
lanrtop 6c69aa2ae3
All checks were successful
Push & PR Check / check (push) Successful in 1m5s
chore(merge): 合并 master(P4a 标 ✅ 与派 P8d 的蓝图更新)到 P8d
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:27:31 +09:00

135 KiB
Raw Blame History

id name status progress
agent-client-parity 三端客户端对等 in_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.''.trust_level 覆盖不生效。用户真实配置下 enterprise(已信任)列出 lian-jing,新建的临时目录没有。codex mcp get --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 ] --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 脱敏成 "",重放用的 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 的写法执行 """ ";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 --wait:全部适用格通过 exit 0,否则 exit 1,炼境不可达 exit 3。human_verify: 人看一次矩阵,确认证据等级和回退标注可读。"
  • CLI 形态(Q9):只做调用全局 MCP 的薄脚本,不新增协议面;Rust console lianjing.exe 与 skill 降级见 architecture-evolution A1b(按需)。

📋 P5 — 测试账户落地到被 Git 忽略文件

  • status: todo
  • complexity: M×3
  • 见 project-dev-variables V14–V16 与 docs/tasks/2026-09-30-dev-variables-materialize.md;落地不返回值、无需认证,是三端都可用的兜底路径。

📋 P6 — 技能能力包:通用 skill 单一来源、三端投影

  • status: todo
  • complexity: M
  • files: src-tauri/src/commands/onboarding.rs, src-tauri/resources/(实现时定)
  • depends: P2, P8c(2026-10-03 Q3:P8c 先落地 WriteSink,P6 的分发基于它实现,避免两边同时改 onboarding.rs 分发逻辑)
  • acceptance: "agent_verify: [cargo test] 通用 skill(architect、splitter、code-review 等)由一处模板按版本投影到各客户端目录(Claude user/project skills、Codex/DSH 的 .agents/skills);[三端无头探针] B 项三端看到同一组 skill 且内容一致。"
  • 为什么:2026-09-30 探针显示同一批 skill 在三端各维护一份(Claude ~/.claude/skills、Codex source-command-* 副本、DSH 手写 .dsh/skills),是实际的重复与漂移源。先把 skill 中的确定性步骤下沉为炼境 MCP 工具,让 skill 变薄,再做投影。
  • 不做:炼境不执行推理类 skill(无模型、会丢失会话代码上下文、越过「不替 agent 思考」边界);不以运行时拉取 skill 正文作为主路径(炼境离线即失效、脱离 Git 分支)。

✅ P7 — MCP 服务器说明的三端支持验证

  • status: done
  • complexity: S
  • files: .blueprint/modules/agent-client-parity.md, docs/ai-context/agent-button-system.md(结论为部分支持,src-tauri/src/mcp_server.rs 与 lian-jing skill 均不改)
  • depends: P1
  • acceptance: "agent_verify: [三端无头探针] 在 initialize 下发唯一标记的 instructions,判定三端是否把它带入模型上下文。全部支持则把引导提示迁到服务器说明,本地 skill 只保留 MCP 不可达时的排障步骤;部分支持则如实记录、保留 skill 引导。"
  • 为什么:Claude 已实证会把 lian-jing 的服务器说明带入上下文;若三端皆支持,引导提示可由炼境集中维护。
  • 结论(2026-10-03):部分支持。Claude Code 与 DSH 会把 initialize 下发的 instructions 带入模型上下文,Codex 不会。按 acceptance 不迁移:引导提示仍以 lian-jing skill(及 AGENTS.md 托管块)为三端单一来源,服务器说明保持现状,只作 Claude/DSH 的增强层;三端都须遵守的规则不得只写在服务器说明里(已写入 agent-button-system.md「本地 Agent 控制面的稳定边界」)。
  • verification: "2026-10-03 三端无头探针(零凭据;不改用户配置,不连运行中的炼境):node 写的最小 MCP 桩服务(streamable-http,纯 JSON 响应),每端两个端口——实验组 initialize 带 instructions,内含该端专属随机标记;对照组不带 instructions 字段,其余完全相同。标记只出现在 instructions 里,工具 probe_ping 的名字、描述和返回值都不含标记。同一问题:「MCP 服务器说明里有什么标记字符串?只回答标记,不调用工具,没收到就答 NONE」。桩服务记录收到的请求,用来证明客户端确实完成了 initialize → tools/list。 ① Claude Code 2.1.173(claude-sonnet-4-6;claude -p --mcp-config --strict-mcp-config --tools '',cwd 为空目录):支持。实验组 init 显示 ljprobe connected,答出 LJ-P7-3067CE459B;对照组答 NONE;实验组输入 token 多 99(10774 对 10675),与多出的说明长度相符。 ② Codex CLI 0.155.0-alpha.9.2(codex exec --json -s read-only,-c mcp_servers.ljprobe.url 临时覆盖,用户默认模型,cwd 为空目录):不支持。实验组、对照组都答 NONE;桩服务日志证明 initialize(codex-mcp-client,协议 2025-06-18)与 tools/list 均成功,即说明已送达客户端、但没进入模型;两组输入 token 完全相同(16320 = 16320;换宽问法「全部上下文里是否有 LJ-P7- 开头的字符串」为 16335 = 16335);该会话 rollout 文件中标记出现 0 次。正向对照:让模型调用 probe_ping,它确实发起 mcp_tool_call(server=ljprobe, tool=probe_ping)(被 approval policy never 拦下),说明桩服务的工具在模型视野里,NONE 不是没连上。加开发中特性 --enable mcp_2026_07_28 后仍答 NONE,输入 token 仍为 16320。 ③ DSH(桌面版内置运行时 headless;--patch 叠加层停用用户已有的三个 MCP 条目、插入指向桩服务的 dsh-mcp-client 条目,用户 ~/.dsh 与 DSH_HOME 均未改):支持。实验组答出 LJ-P7-B0628021BF,思考过程原样复述了说明全文;对照组答 NONE;实验组输入 token 多 53(6487 对 6434)。DSH 在 initialize 前先发 server/discover,桩服务与炼境一样回 -32601,DSH 随即回退到 initialize → tools/list;其 mcp-client 配置有 maxInstructionBytes(默认 32768)。 探针脚本、桩服务与原始日志留在本机 %TEMP%\lj-p7-probe(未入库,P4 固化时可参考)。"
  • known_limits: "Claude 与 DSH 只验了无头模式,Claude 只用了 sonnet-4-6;Codex 未验 TUI 与桌面版(桌面版可能走 app-server 路径,结论可能不同),也未验 SSE 传输。探针用的是桩服务的中英混合短说明,未用炼境真实说明(中文 238 字、UTF-8 500 字节,远低于 DSH 默认上限 32768 字节)实测。Codex 后续版本若开始带入服务器说明,按「检测行为而非嗅探版本」重跑本探针(P4 固化后纳入矩阵),三端都支持时再迁移。现有服务器说明五条要点中,「先读状态」「canonical project_id」「开发变量先查后登记」在 skill 有对应条目;「其他写操作须用户明确确认」与「unavailable/unknown 不得当作成功」在 skill 与 AGENTS.md 中只有分场景表述(合并 PR、CI workflow、工具链安装;MCP 不可用时不伪造诊断),Codex 会话只能从这些条目与工具描述获知——是否补成通用条目留待下次改 skill 时一并处理(需升接入包版本),本卡不改。 已补齐(2026-10-03,分支 feat/agent-client-parity-p7-skill-rules):两条已作为通用边界写进 lian-jing skill 模板开头(双投影到 .agents 与 .claude),两条规则的核心句与服务器说明逐字一致;只读查询、登记开发需求元数据与回传炼境明确列为无需确认的例外,与服务器说明的「其他写操作」及 skill「回传不必逐条征得同意」相容。服务器说明的例外是泛称「不含秘密的开发需求元数据」,skill 例外括注同时列出两个登记工具 register_project_test_account 与 register_project_dev_variable(审查修复:初版只列前者,会把开发变量登记误归入须确认的写操作);同批修正 skill 第 8 条——开发变量改用 register_project_dev_variable 登记名称与用途,测试账号仍用 register_project_test_account(初版两者都指向后者)。接入包 base-onboarding 1.7.0→1.8.0(修复在 1.8.0 发布前,未再升版本,只更新指纹);mcp_server 新增 server_instruction_universal_rules_are_in_skill,服务器说明与 skill 任一侧缺这两条即失败,skill 例外行缺任一登记工具也失败。AGENTS.md 托管块未改,仍只有分场景表述;存量项目需重建安装炼境后重跑 apply_onboarding_pack 才拿到新 skill。"

🔵 P8 — 一键接入:创建/添加子项目即完成三端可用

  • status: in_progress
  • complexity: L(2026-10-03 设计评审后拆为 P8-0、P8a–P8g,均为 S/M)
  • depends: P4a(原为 P4;P8 只依赖 L1 零模型检查,不等 L3)
  • acceptance: "agent_verify: [Tauri command / MCP] 对新登记项目 plan_onboarding → 用户确认 → onboard_project(expected_digest, confirmed=true),依次完成 canonical context、接入包分发(规则、skill 双投影、Codex 托管块)、本机三端注册(Claude local、DSH 白名单与会话插件、shim、token),并以 L1 零模型三端矩阵(每格带证据等级)验收,附最近一次 L3 结果及是否过期;重复执行收敛(全部 converged、文件 mtime 不变);接入包对项目文件的改动只报告 diff、不自动提交。human_verify: 在 Claude Code、Codex、DSH 桌面 GUI 各新开会话调 read_project_test_account(name=LJ_PROBE_NONEXISTENT),得「开发需求未登记」。"
  • 验收口径变更(2026-10-03,Q1,按自验报告纪律报备):原口径「以三端无头探针给出 A–F 兼容矩阵」改为「L1 零模型矩阵 + 可选 L3 结果及是否过期」。理由:L3 每次消耗三端模型额度、需 2–3 分钟且有随机性,不宜作为每次接入的门槛;L1 经真实 shim 重放认证链、在项目路由上实际调用 C–F 工具,给出协议级证据;「L1 全绿而真实会话失败」的风险用证据等级标注、客户端版本变化时提示跑 L3(P4e)兜底;GUI 形态归 human_verify。决策方式:用户以 /goal 授权按推荐方向代为推进,11 个决策点全部采纳推荐项、未逐项确认,用户可随时推翻。
  • 「完成」的判定:只按三端可用子集(身份、托管块版本、两份 skill、Codex 块、Claude local、DSH 白名单、lefthook、L1 适用格全部 ready)。verify_onboarding 的 origin 指向 Gitea、CI workflow、飞轮 commit 三项对新项目必然不通过,进入 next_actions,不算失败。
  • 设计:docs/tasks/2026-10-03-p8-onboarding-oneclick-design.md(三角度方案 + 综合评审、16 条已核实代码事实、风险 R1–R18、决策 Q1–Q11)。主方案为最小编排本体(同步、零模型的接入 + 可选异步探针,A1 不前置),嫁接 plan/digest 写前确认、经真实 shim 的认证链重放、「已接入」单一判定、客户端适配器注册表与 A1a 适应度测试。
  • 决策(Q2–Q11 均采纳推荐项):写入前预览走 plan → digest 确认 → apply;bypassPermissions 默认值保持现状但在 plan 与报告中单列、可按项目取消;Codex 信任列入 next_actions,炼境不写 ~/.codex 信任表;L3 只在显式触发时跑,建议在 canary(enterprise)项目、指纹变化时跑;做 register_project 的全局路由投影;A4 先于 P4b;CLI 只做 pnpm probe:clients 薄脚本;A1 改写为三段;探针默认用各客户端默认模型。
  • 非目标:不自动 commit/push;不自动建 Gitea 仓库、webhook、CI,不自动装工具链(只列 next_actions,Gitea 一项注明「工具默认建公开仓库」);不改客户端全局安全设置,不覆盖手写同名 MCP 配置;L3 不作接入门槛、不作 CI 闸门;不做离线 CLI 写入,不拆 core crate;P8 不改接入包模板;批量登记不自动接入。
  • 为什么:2026-10-01 Enterprise 验收时,接入散落在 inject_mcp、apply_onboarding_pack、verify_onboarding 与手工探针多个步骤。按「分发 vs 查询」:随仓库走的文件由炼境分发一次、按版本升级;与机器相关的客户端注册由炼境在本机维护、不进仓库;易变信息走 MCP 查询。默认三端都接入(成本小,保留随时换客户端的能力);能用标准的不做专门适配(Codex、DSH 原生读 AGENTS.md 与 .agents/skills,只有 Claude 需要 CLAUDE.md 与 .claude/skills 投影);炼境离线时规则与 skill 仍可用。

✅ P8-0 — 蓝图与依赖修正

  • status: done
  • complexity: S
  • files: .blueprint/modules/agent-client-parity.md, .blueprint/modules/architecture-evolution.md, .blueprint/manifest.yaml, docs/tasks/2026-10-03-p8-onboarding-oneclick-design.md
  • acceptance: "agent_verify: [YAML 解析] manifest.yaml 可解析;manifest 不再写「P4/P8 依赖 A1」。[读文件] P8 depends 改为 P4a,acceptance 按 Q1 改写并记录理由;P4 拆为 P4-0、P4a–P4e;A1 拆为 A1a/A1b/A1c,A1c 写明四个触发条件。"
  • 依赖顺序:P8-0 → P8a → P8b → P8c → P4a(另依赖 P4-0)→ P8d → P8e → P8g;P8f 可选、独立,P8e 端到端验收前完成。P8 关键路径不依赖 L3。
  • verification: "2026-10-03:manifest.yaml 经 PyYAML safe_load 解析通过(agent-client-parity progress 35、edges 95 条);manifest 全文「P4/P8 依赖」「依赖 architecture-evolution A1」「A1 的 CLI 投影」0 处;设计文档入库 docs/tasks/2026-10-03-p8-onboarding-oneclick-design.md,§9 十一项推荐项已勾选。"

✅ P8a — 「已接入」单一判定

  • status: done
  • complexity: S
  • files: src-tauri/src/mcp_inject.rs, src-tauri/src/mcp_tools.rs
  • depends: P8-0
  • acceptance: "agent_verify: [cargo test mcp_inject] in-memory DB + 假 CredentialStore:有 token、不在任何组的项目进入 reinject_all_mcp 的目标集合;组成员仍在;token 已删的项目不在(锁内复查语义不变);新函数的连接由调用方传入。[cargo test mcp_tools] list_all_projects 的「已注入」与 DSH 白名单、根目录迁移调用同一个函数:删除 .claude/liangjing.json 不改变判定,删除 token 后判为未注入。"
  • 起因:「已接入」现有三套判定——reinject_all_mcp 按组成员、DSH 白名单与根目录迁移按 token、list_all_projects 按 .claude/liangjing.json 是否存在;ensure_standalone_group 只在项目不在任何组时才注入,已在组内的项目重跑接入包不会修复本机注册。
  • 实现(2026-10-03):mcp_inject 新增 is_project_injected(store, project_id)(有项目 MCP token 即已接入),DSH 白名单、on_project_root_changed、list_all_projects 改为调用它;reinject_all_mcp 的目标由「组成员」扩为「组成员 ∪ 有 token 的登记项目」(reinject_targets),锁内复查改为 is_reinject_target(仍是组成员或仍有 token),连接与凭据存储由调用方注入。list_all_projects 拆出 render_all_projects(conn, store),顺带去掉循环内的二次取池。行为变化:独立接入(不在任何组)的项目在端口变更后也会被重注入;list_all_projects 对只剩兼容文件、token 已删的项目改报「未注入」。ensure_standalone_group 的「已在组内不注入」由 P8d 编排本体解决,本卡不动。
  • verification: "2026-10-03:cargo test 286 passed / 0 failed / 5 ignored;新增 reinject_targets_follow_single_injected_judgment(组成员无 token 仍是目标、独立有 token 进入目标、删 token 后锁内复查为否且移出目标)与 list_all_projects_judges_injection_by_token_not_compat_file(只有 liangjing.json 无 token → 未注入;有 token 删兼容文件 → 已注入;删 token → 未注入)。变异:屏蔽 reinject_targets 的 token 分支、list_all_projects 改回看 liangjing.json,两条新测试均失败。契约同步 docs/ai-context/project-context.yaml「被管理项目身份上下文」。"

✅ P8b — 客户端适配器注册表 + 结构化注入结果 + A1a 适应度测试

  • status: done
  • complexity: M
  • files: src-tauri/src/onboard/mod.rs, src-tauri/src/onboard/clients.rs, src-tauri/src/mcp_inject.rs, src-tauri/src/commands/onboarding.rs, src-tauri/src/mcp_tools.rs, src-tauri/src/lib.rs, src/lib/commands.ts
  • depends: P8a
  • acceptance: "agent_verify: [cargo test onboard::clients] 注册表恰好列出三端,每端声明 detect、static_status、apply;sync_claude_local 的每条分支映射到一种 ClientOutcome(converged / applied / refreshed / manual_kept / other_project / unavailable{reason} / skipped{control_plane|wsl});DSH 未安装记 not_installed,块外有手写条目记 manual_kept,缺 claude CLI 时 claude 记 unavailable、其余照常;原有 log_event 的类别和文案不变。[cargo test] onboard_module_is_projection_free:src/onboard/ 下源码不含 tauri::、db::pool(、#[tauri::command]。[cargo test onboarding] 新增 clients 字段不改变 checks 计数和 VerifyState,控制面和 WSL 记 not_applicable、不拉低 pass。[MCP] 对 enterprise 连续两次 inject_mcp:第二次三端都是 converged,耗时 <1 秒(未起 claude CLI),四个项目文件 mtime 不变。"
  • 起因:inject_project_mcp 返回 Result<(),String>,sync_claude_local 返回 (),refresh_dsh_patch 失败只写日志——Claude 与 DSH 的成败没有结构化结果,直接编排会把失败报成成功(R4),所以 P8d 必须排在本卡之后。
  • 实现(2026-10-03,分支 feat/agent-client-parity-p8):新增 src-tauri/src/onboard/(mod.rs 含适应度测试;clients.rs 定义 ClientId、ClientOutcome、ClientReport、ProjectFacts、DesiredMcp、ClientAdapter trait 与 registry())。三端适配器实现在 mcp_inject(需要其私有辅助函数),注册表反向接入。inject_project_mcp_locked 写完项目身份与 Claude 兼容投影后经 registry() 逐端 apply 并收集报告;新增 inject_project_mcp_report 返回逐端结果,inject_project_mcp 签名不变,只有 Codex 托管块写入失败算整体失败(沿用旧语义)。sync_claude_local 抽出依赖注入版 sync_claude_local_with(Claude 配置与 CLI 执行器由调用方给出),每条分支返回一种 ClientOutcome,日志类别与文案不变;ensure_dsh_patch 抽出 ensure_dsh_patch_at(DSH 目录、凭据存储、会话插件引用注入),refresh_dsh_patch 返回结果;upsert_codex_mcp_config 返回 converged/applied/manual_kept。只读判定 claude_static_outcome / codex_static_outcome / dsh_static_outcome + client_static_status 供 verify_onboarding 的 clients 附加字段(MCP 投影)。inject_mcp 回执改为逐端列出实际结果。
  • 行为变化(有意为之,R5):① 项目 .codex/config.toml 已手写 [mcp_servers.lian-jing] 时由整体报错改为 Codex 记 manual_kept、Claude 与 DSH 照常接入;② Codex 托管块写入失败时 Claude 与 DSH 仍会完成(旧代码先写 Codex、失败即中止),inject_project_mcp 仍返回 Err;③ WSL 项目不再对 Claude local 起 CLI,记 not_applicable{wsl}(2026-07-04 放弃 WSL 路径项目裁决;DSH 白名单本就排除 WSL);④ Codex 托管块的写入顺序移到 Claude 兼容投影之后。
  • verification: "2026-10-03:cargo test 295 passed / 0 failed / 5 ignored(新增 9 条:onboard::clients 4 条——注册表恰好三端且顺序固定、settled 判定、扁平序列化、WSL 根识别;onboard 适应度测试;mcp_inject 4 条——claude_sync_maps_every_branch_to_an_outcome 用假 CLI 覆盖 converged×2/unavailable×3/applied/failed×3(含回滚调用序列 get→remove→add-json→add-json)/refreshed/removed/other_project/manual_kept/needs_human,static_outcomes_are_read_only_judgments,dsh_patch_reports_not_installed_applied_converged_and_manual_kept(临时 DSH 目录,手写条目不被覆盖),adapters_skip_control_plane_and_wsl_without_touching_disk;原有 Codex 手写冲突测试改断言 manual_kept)。[MCP] 两次 inject_mcp 的耗时与 mtime 断言须在合并后、运行中的炼境重编译生效时实测,通过后本卡标 ✅。"
  • review: "2026-10-03 独立审查 agent 只读审查 d1b9b05(未发现死锁、重入或崩溃),10 条发现处置:① Claude apply 未装时报 unavailable → 无全局配置且找不到 CLI 记 not_installed(与 DSH、static 同口径);② 手写条目保留时丢日志 → DSH 照原文案补记 mcp_client_sync warn,Codex 记一条带 project_id 的 warn;③「MCP 配置注入成功」写在 Codex 之前,Codex 失败时诊断仍显示成功 → 移到三端收敛之后,整体失败时改记 warn「注入失败」;④ 控制面 Claude 静态判定掩盖遮蔽全局注册的残留 local 条目 → 有残留报 missing;⑤ 嵌套项目误判 other_project → 根目录自身是 git 根时只认等于根目录的键,否则取最具体的键;⑥ 适应度测试不递归 → 改为递归,并在 registry() 注明适配器暂寄 mcp_inject 的传递依赖(绞杀者过渡);⑦ 状态名与设计不一致 → skipped 改为 not_applicable{control_plane|wsl} 与 deferred;⑧ 只读判定读失败当未配置、块与手写并存、路径来源不同 → 读失败与 YAML 解析失败报 unavailable,Codex 块与手写同名表并存报 needs_human,DSH 块外手写报 manual_kept,标记错序报 needs_human,client_static_status 改用与注入同源的 project_root;⑨「目录不存在」用例为假阳性 → 改为 CLI 存在仍不起 CLI 并断言原因,另补 hard_failure、inject 回执、apply 结果映射、clients 不影响 pass 的测试;⑩ 记入 known_limits。修复后 cargo test 300 passed / 0 failed / 5 ignored(较首版新增 5 条),变异验证:删目录守卫、去掉自身 git 根判定,对应用例均失败。"
  • mcp_verify: "2026-10-03 PR #33 合并(ddd729c)、运行中的炼境重编译生效后:verify_onboarding(enterprise) 的 clients 三端均 converged;连续两次 inject_mcp 回执三端均「已一致(未改动)」,耗时 26 ms、112 ms,.lian-jing/context.json、.codex/config.toml、.claude/settings.json、.claude/liangjing.json 四个文件 mtime 不变,git status 前后均干净。"
  • known_limits: "WSL 项目已有的 Claude local 托管条目在端口变更后不再刷新(重注入对 WSL 记 not_applicable),而 remove_project_mcp 与 on_project_root_changed 对 WSL 根仍会起 CLI 清理——口径不完全一致,WSL 路径项目已按 2026-07-04 裁决放弃,暂不处理。Codex 托管块已存在时 upsert 不检查块外手写同名表(存量问题),只读判定会报 needs_human 提醒人工清理。注册表与适配器的端到端组合(缺 CLI 时其余两端照常)只在单元层分别验证,未做真实客户端的组合实测。"

✅ P8c — 写入前计划:WriteSink + plan_onboarding

  • status: 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: in_progress
  • 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/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 尚未提供)、repo_changes(available{status_porcelain = git status --porcelain --untracked-files=all, diff_stat} / unavailable{reason})、head{before, after, unchanged}(非 git 为空)、suggested_commit_message(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,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 在全部步骤之后。
  • 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 拼接)要改的地方,搬迁冲突面大;本卡只下沉了可注入、无冲突的部分(见「下沉」)。② 测试里客户端一律「未安装」: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 核对与写入在写锁内,只与同样持锁的 inject / remove / 根目录迁移串行)。⑦ 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 同受影响):起不来一律当作缺可执行文件、试下一个候选(原实现只有 NotFound 才试下一个,其余记 failed);新增 60 秒超时,超时只结束直接子进程。⑩ 写入期间别人提交会使 HEAD 断言为 false、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。"
  • 待办:独立审查后开 PR(M 级,分支 feat/agent-client-parity-p8d);P8e 合并、运行中的炼境重编译后由统筹做端到端实测,再标 ✅。

📋 P8e — 投影收口与契约同步

  • status: todo
  • complexity: S
  • files: src-tauri/src/mcp_tools.rs, src-tauri/src/lib.rs, src/lib/commands.ts, docs/ai-context/project-context.yaml, docs/ai-context/agent-button-system.md, AGENTS.md
  • depends: P8d
  • acceptance: "agent_verify: [cargo test mcp_tools] 全局和项目路由的 tools/list 都含 plan_onboarding 与 onboard_project;项目路由上强制本项目,传跨项目参数返回 scope_violation;onboard_project 的 required 含 expected_digest 和 confirmed;annotations 中 plan 为 readOnly、apply 为 idempotent。[pnpm typecheck] 通过。[MCP 全局] 对 enterprise 做 plan 全部 noop,做 apply 结果 done 且各步都是 converged。[读文件] project-context.yaml 已补 plan、digest 与状态语义(T1)。"
  • 说明:两个工具的 schema 单独成函数(避开 json! 递归上限),dispatch 里只留一行委托,减少与 A4(rmcp)迁移的冲突面;AGENTS.md 架构速查补 src-tauri/src/onboard/。

🔵 P8f — register_project 控制面投影

  • status: in_progress
  • 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 并发仍无测试(见没验什么③)。"

📋 P8g — UI 一键接入

  • status: todo
  • complexity: M
  • files: src/components/dashboard/OnboardPanel.tsx, src/components/dashboard/onboardView.ts, src/components/dashboard/onboardView.test.ts, src/components/dashboard/ProjectModal.tsx, src/components/dashboard/BatchAddModal.tsx, src/components/manage/NewProjectModal.tsx, src/components/dashboard/BlueprintModal.tsx, src/lib/commands.ts
  • depends: P8e
  • acceptance: "agent_verify: [pnpm typecheck && pnpm test] 视图纯函数:blocked 非空时确认按钮禁用并列出文件和原因;not_installed 和 not_applicable 显示为灰色说明、不显示为红色失败;bypassPermissions 和 Codex 信任单独列出;确认时携带 expected_digest。human_verify: ① 用临时 git 仓库新建项目,弹出接入预览;② 确认后 30 秒内看到分步结果、L1 矩阵和「待你提交的 N 个文件」;③ 另试一次在预览里取消,目录零改动;④ 在该目录新开 Claude Code、Codex、DSH 桌面 GUI 会话调 LJ_PROBE_NONEXISTENT,得「开发需求未登记」。"
  • 说明:新建 / 模板创建成功后弹出「接入预览」,create_project 本身保持无副作用;批量添加只提供批量入口、不自动接入;BlueprintModal 的「应用接入包」改为先 plan 再 apply。

✅ P9 — DSH 桌面 GUI 按工作区连项目地址(会话级插件)

  • status: done
  • human_verify: "2026-10-02 用户在 DSH 桌面 GUI 终验通过:重启 DSH 后在 enterprise 工作区新建会话,实际调用 list_project_dev_variables(返回 1 条)与 read_project_test_account(name=LJ_PROBE_NONEXISTENT)(返回「开发需求未登记」,即项目地址与认证均生效);在 dev-manager-tauri 工作区(控制面,null 停止哨兵)新建会话,无 list_project_dev_variables 工具。"
  • complexity: M
  • files: src-tauri/src/mcp_inject.rs(render_dsh_block 追加插件行), src-tauri/resources/dsh/lianjing-session-mcp.v1.mjs(新增,随二进制打包), src/lib/dshSessionPlugin.test.ts
  • depends: P1
  • acceptance: "agent_verify: [cargo test] 托管块同时含 mcp-lian-jing 与 lianjing-session-mcp 两行、插件路径为绝对路径、roots 与 normalize_dsh_root 一致;[vitest] 假 ctx 下按会话 cwd 命中项目才挂载、null 哨兵与未命中不挂、shim 失败/超时不抛错且无未处理 rejection;[DSH 0.2 与 0.1 运行时] 临时 DSH_HOME + stub MCP 服务,ACP session/new 两个不同 cwd 的会话只有项目会话产生 /mcp/projects/ 请求。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/ 的 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/ 的 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 与工作区:暂存区与工作区不同的中间状态不单独判断。文末托管块之前、之后的纯空白增删(如块前空行数、文件末尾多出空行)为抵消炼境追加差异仍被忽略。"