diff --git a/.blueprint/modules/agent-client-parity.md b/.blueprint/modules/agent-client-parity.md index 7caf3a2..d6aaf99 100644 --- a/.blueprint/modules/agent-client-parity.md +++ b/.blueprint/modules/agent-client-parity.md @@ -145,7 +145,8 @@ skill 只是五个接入面之一,兼容层覆盖全部五面。 ② 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 时一并处理(需升接入包版本),本卡不改。" +- 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「回传不必逐条征得同意」相容。接入包 base-onboarding 1.7.0→1.8.0;mcp_server 新增 server_instruction_universal_rules_are_in_skill,服务器说明与 skill 任一侧缺这两条即失败。AGENTS.md 托管块未改,仍只有分场景表述;存量项目需重建安装炼境后重跑 apply_onboarding_pack 才拿到新 skill。" ### 📋 P8 — 一键接入:创建/添加子项目即完成三端可用 - status: todo diff --git a/src-tauri/resources/onboarding/.agents/skills/lian-jing/SKILL.md b/src-tauri/resources/onboarding/.agents/skills/lian-jing/SKILL.md index fdd9c12..38d96f4 100644 --- a/src-tauri/resources/onboarding/.agents/skills/lian-jing/SKILL.md +++ b/src-tauri/resources/onboarding/.agents/skills/lian-jing/SKILL.md @@ -7,7 +7,12 @@ description: 读取炼境项目健康状态,并使用炼境 MCP 的项目管 开始项目工作时,先读取根 `AGENTS.md` 与 `.lian-jing/context.json` 取得 `project_id`。`.claude/liangjing.json` 仅为旧项目的 Claude 兼容投影,不能作为长期身份来源。 -如果当前客户端已发现 `lian-jing` MCP: +如果当前客户端已发现 `lian-jing` MCP,所有调用都遵守两条通用边界(与炼境 MCP 服务器说明一致): + +- 只读查询、登记不含秘密的开发需求元数据(`register_project_test_account`)与回传炼境(`report_envoy_return`)可直接调用;其他写操作仅在用户明确确认目标与影响后调用,下文安装工具链、合并 PR、生成 CI workflow 是它的具体场景。 +- 状态 `unavailable` / `unknown` 不得当作成功:如实报告实际状态,不据此宣称完成。 + +具体用法: 1. scoped MCP 连接已由 URL 绑定当前 `project_id`,项目级调用可省略 `project_id`;若 server initialize 明确报告 legacy,迁移前才显式传入 canonical `project_id`,绝不传其他项目 ID。 2. 优先调用 `get_diagnostics`,将其作为当前项目健康状态的事实来源。 diff --git a/src-tauri/resources/onboarding/.lian-jing/capability-packs.json b/src-tauri/resources/onboarding/.lian-jing/capability-packs.json index 3dd52bc..4e41785 100644 --- a/src-tauri/resources/onboarding/.lian-jing/capability-packs.json +++ b/src-tauri/resources/onboarding/.lian-jing/capability-packs.json @@ -3,7 +3,7 @@ "packs": [ { "pack_id": "base-onboarding", - "version": "1.7.0", + "version": "1.8.0", "supported_clients": ["codex", "claude", "dsh"], "project_artifacts": [ "AGENTS.md", diff --git a/src-tauri/src/commands/onboarding.rs b/src-tauri/src/commands/onboarding.rs index e993373..0043ba2 100644 --- a/src-tauri/src/commands/onboarding.rs +++ b/src-tauri/src/commands/onboarding.rs @@ -19,14 +19,14 @@ const TMPL_GIT_WORKFLOW: &str = include_str!("../../resources/onboarding/git-wor const TMPL_ENVOY: &str = include_str!("../../resources/onboarding/.claude/commands/envoy.md"); const TMPL_BLUEPRINT_GATE: &str = include_str!("../../resources/onboarding/.claude/hooks/blueprint-gate.cjs"); -const TMPL_LIANJING_SKILL: &str = +pub(crate) const TMPL_LIANJING_SKILL: &str = include_str!("../../resources/onboarding/.agents/skills/lian-jing/SKILL.md"); const TMPL_CAPABILITY_PACKS: &str = include_str!("../../resources/onboarding/.lian-jing/capability-packs.json"); const TMPL_MISE: &str = include_str!("../../resources/onboarding/mise.toml.tmpl"); const PROJECT_CONTEXT_SCHEMA_VERSION: u64 = 1; const CAPABILITY_PACK_SCHEMA_VERSION: u64 = 1; -const BASE_ONBOARDING_PACK_VERSION: &str = "1.7.0"; +const BASE_ONBOARDING_PACK_VERSION: &str = "1.8.0"; const TOOLCHAIN_PACK_VERSION: &str = "1.0.0"; /// 单个模板的分发结果。 @@ -1947,8 +1947,8 @@ mod tests { const TEMPLATE_FINGERPRINTS: [(&str, &str, &str); 3] = [ ( "resources/onboarding/**", - "1.7.0", - "0c39acd7c0513a1063ea0c2de5da06ba2baa69b7eb85b167499859ab6ad0eb45", + "1.8.0", + "68846285b70344cfdf6c364fba112c28cbaa042f64358a91b872a601336b27ff", ), ( "AGENTS.md.tmpl#ONBOARDING_MANAGED", diff --git a/src-tauri/src/mcp_server.rs b/src-tauri/src/mcp_server.rs index 955caff..8e04ab6 100644 --- a/src-tauri/src/mcp_server.rs +++ b/src-tauri/src/mcp_server.rs @@ -533,6 +533,25 @@ mod tests { ); } + /// 服务器说明只有部分客户端带入模型上下文(Codex 不带,见蓝图 agent-client-parity P7), + /// 其中三端都须遵守的通用规则必须同时写在接入包 lian-jing skill 里。 + #[test] + fn server_instruction_universal_rules_are_in_skill() { + let skill = crate::commands::onboarding::TMPL_LIANJING_SKILL; + for phrase in [ + "其他写操作仅在用户明确确认目标与影响后调用", + "unavailable", + "unknown", + "不得当作成功", + ] { + assert!( + MCP_SERVER_INSTRUCTIONS.contains(phrase), + "服务器说明缺少「{phrase}」" + ); + assert!(skill.contains(phrase), "lian-jing skill 缺少「{phrase}」"); + } + } + #[tokio::test] async fn scoped_handshake_binds_project_and_filters_global_tools() { let init = process_rpc(&rpc("initialize"), Some("p1")).await.unwrap();