From f4e75dc91fe469677c25fb3ffce7976b7ce854b0 Mon Sep 17 00:00:00 2001 From: lanrtop Date: Thu, 1 Oct 2026 09:11:32 +0900 Subject: [PATCH] =?UTF-8?q?feat(onboarding):=20lian-jing=20skill=20?= =?UTF-8?q?=E5=8F=8C=E6=8A=95=E5=BD=B1=E5=88=B0=20Claude=EF=BC=8C=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=E5=8C=85=E5=8D=87=E8=87=B3=201.6.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 同一模板同时写 .agents/skills(Codex、DSH)与 .claude/skills(Claude Code) - verify_onboarding 新增「Claude 炼境技能已分发」检查 - skill 降级步骤改为三端通用,提示无开发变量工具即连的是全局地址 - capability-packs supported_clients 增加 dsh Feature-Confirmed: true Co-Authored-By: Claude Opus 5.5 --- .blueprint/modules/agent-client-parity.md | 5 +-- docs/ai-context/project-context.yaml | 2 +- .../.agents/skills/lian-jing/SKILL.md | 6 ++-- .../.lian-jing/capability-packs.json | 5 +-- src-tauri/src/commands/onboarding.rs | 31 +++++++++++++++++-- 5 files changed, 39 insertions(+), 10 deletions(-) diff --git a/.blueprint/modules/agent-client-parity.md b/.blueprint/modules/agent-client-parity.md index 5e9ff32..8ef7026 100644 --- a/.blueprint/modules/agent-client-parity.md +++ b/.blueprint/modules/agent-client-parity.md @@ -66,12 +66,13 @@ progress: 0 - acceptance: "agent_verify: [三端无头探针] 在 enterprise 目录 Claude 与 DSH 的 C 项均看到开发变量工具(项目工具集),E 项成功;项目目录外的会话不获得他项目工具。" - 注:吸收原 project-dev-variables V17(Claude 以 local scope + headersHelper 取值)。 -### 🔵 P2 — Claude skill 投影 -- status: in_progress +### ✅ 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: todo diff --git a/docs/ai-context/project-context.yaml b/docs/ai-context/project-context.yaml index 6cd13c0..62ded75 100644 --- a/docs/ai-context/project-context.yaml +++ b/docs/ai-context/project-context.yaml @@ -25,7 +25,7 @@ contracts: - ".lian-jing/context.json" - ".lian-jing/capability-packs.json" - ".claude/liangjing.json" - doc: "`.lian-jing/context.json` 是 project_id 的工具无关单一事实来源;`.claude/liangjing.json` 仅为兼容投影;`.lian-jing/capability-packs.json` 是炼境分发基线的版本描述。迁移必须保留未知字段;验证必须返回显式状态与 upgrade_hint;升级仅覆盖炼境托管文件,绝不覆盖项目定制或手工 MCP 配置,且不得以客户端全局配置或项目 UI 身份替代项目根目录上下文。" + doc: "`.lian-jing/context.json` 是 project_id 的工具无关单一事实来源;`.claude/liangjing.json` 仅为兼容投影;`.lian-jing/capability-packs.json` 是炼境分发基线的版本描述。lian-jing skill 以同一模板双投影:`.agents/skills/lian-jing/`(Codex、DSH 读取)与 `.claude/skills/lian-jing/`(Claude Code 不读 .agents/skills),两份内容必须一致。迁移必须保留未知字段;验证必须返回显式状态与 upgrade_hint;升级仅覆盖炼境托管文件,绝不覆盖项目定制或手工 MCP 配置,且不得以客户端全局配置或项目 UI 身份替代项目根目录上下文。" - label: Tauri command 接口(前后端契约) paths: 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 84770e6..55765d5 100644 --- a/src-tauri/resources/onboarding/.agents/skills/lian-jing/SKILL.md +++ b/src-tauri/resources/onboarding/.agents/skills/lian-jing/SKILL.md @@ -16,11 +16,11 @@ description: 读取炼境项目健康状态,并使用炼境 MCP 的项目管 5. 交付、Gitea、CI、蓝图等平台状态优先通过 MCP 查询,不凭静态文档猜测。 6. 合并 PR 前调用 `get_delivery_status(project_id)`,向用户展示 PR 编号、完整 head SHA、CI 与 review。只有当前用户明确确认该 PR 与 SHA 后,才调用 `merge_pull_request(project_id, pr_number, expected_head_sha, confirmed=true)`;合并后再次读取交付状态。不得自行推断确认。 7. 生成 CI workflow 时先调用 `plan_cicd_workflow(project_id, workflow_type)`,向用户展示目标文件、变更类型、`content_digest`、工具链来源与所需 secret 名称;只有用户明确确认这份摘要后,才以同一 digest 调用 `apply_cicd_workflow(..., confirmed=true)`。该流程只写项目 workflow,不配置 secret、不触发 CI 或部署;不得通过 MCP 传服务器地址、命令、路径或 secret 值,部署类配置先由用户在炼境 UI 保存。 -8. 需要测试账号或开发变量(登录自验、沙箱 key 等)时,先调用 `list_project_dev_variables` 查找:已配置则用 `read_project_test_account` / `read_project_dev_variable` 单项读取;没有则用 `register_project_test_account` 登记名称、目标系统、非生产环境与用途,并请用户在炼境项目 UI「开发变量」中填写。不得在对话中向用户索要明文密码,不得登记生产账号,不得把读到的值写入受 Git 跟踪的文件。读取因缺少项目认证被拒时(当前仅 Codex 经 helper 验证可读),如实报告并改用项目原有配置方式,不得绕过认证。 +8. 需要测试账号或开发变量(登录自验、沙箱 key 等)时,先调用 `list_project_dev_variables` 查找:已配置则用 `read_project_test_account` / `read_project_dev_variable` 单项读取;没有则用 `register_project_test_account` 登记名称、目标系统、非生产环境与用途,并请用户在炼境项目 UI「开发变量」中填写。不得在对话中向用户索要明文密码,不得登记生产账号,不得把读到的值写入受 Git 跟踪的文件。读取因缺少项目认证被拒时,如实报告并改用项目原有配置方式,不得绕过认证。若 lian-jing 工具中根本没有 `list_project_dev_variables`,说明当前客户端连的是全局地址 `/mcp` 而非本项目地址,按下方 `configured` 一步处理。 如果 MCP 未出现,按以下顺序降级,不猜测状态: -1. `configured`:读取 `.codex/config.toml`,确认 `lian-jing` 指向 `http://127.0.0.1:/mcp/projects/`;缺失或 legacy 时通过炼境重新 `inject_mcp`,手工同名 server 不覆盖。 +1. `configured`:确认当前客户端的 `lian-jing` 指向 `http://127.0.0.1:/mcp/projects/`——Codex 看项目 `.codex/config.toml`,Claude Code 看 `~/.claude.json` 中本项目的 local 条目(优先于 user 级全局条目),DSH 看 `~/.dsh/cordis.patch.yml`。缺失或仍指向全局 `/mcp` 时通过炼境重新 `inject_mcp`,手工同名 server 不覆盖。 2. `reachable`:配置正确后检查 loopback 服务;连接拒绝表示炼境未运行,先启动正式安装版并保持托盘进程。 -3. `reload`:服务可达但当前任务看不到工具时,新建 Codex 任务或重载客户端;不要反复改配置或复制凭证。 +3. `reload`:服务可达但当前会话看不到工具时,新建会话或重载客户端;不要反复改配置或复制凭证。 4. 仍不可用时,明确报告停在哪一层,继续完成不依赖 MCP 的本地工作;不得伪造诊断、项目 ID、凭证或远端状态。 diff --git a/src-tauri/resources/onboarding/.lian-jing/capability-packs.json b/src-tauri/resources/onboarding/.lian-jing/capability-packs.json index f3e1d44..444c79a 100644 --- a/src-tauri/resources/onboarding/.lian-jing/capability-packs.json +++ b/src-tauri/resources/onboarding/.lian-jing/capability-packs.json @@ -3,11 +3,12 @@ "packs": [ { "pack_id": "base-onboarding", - "version": "1.5.0", - "supported_clients": ["codex", "claude"], + "version": "1.6.0", + "supported_clients": ["codex", "claude", "dsh"], "project_artifacts": [ "AGENTS.md", ".agents/skills/lian-jing/SKILL.md", + ".claude/skills/lian-jing/SKILL.md", ".lian-jing/context.json" ], "self_check": "verify_onboarding", diff --git a/src-tauri/src/commands/onboarding.rs b/src-tauri/src/commands/onboarding.rs index 5255cd9..7411b06 100644 --- a/src-tauri/src/commands/onboarding.rs +++ b/src-tauri/src/commands/onboarding.rs @@ -26,7 +26,7 @@ const TMPL_CAPABILITY_PACKS: &str = 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.5.0"; +const BASE_ONBOARDING_PACK_VERSION: &str = "1.6.0"; const TOOLCHAIN_PACK_VERSION: &str = "1.0.0"; /// 单个模板的分发结果。 @@ -230,6 +230,12 @@ pub fn dispatch_templates(project_path: &str) -> Vec { ".agents/skills/lian-jing/SKILL.md", TMPL_LIANJING_SKILL, ), + // Claude Code 不读 .agents/skills,同一模板投影一份(单一来源,双投影)。 + dispatch_fully_managed( + root, + ".claude/skills/lian-jing/SKILL.md", + TMPL_LIANJING_SKILL, + ), dispatch_fully_managed( root, ".lian-jing/capability-packs.json", @@ -885,6 +891,8 @@ pub fn verify_onboarding( let (pass, detail) = check_skill(read(".agents/skills/lian-jing/SKILL.md").as_deref()); checks.push(VerifyCheck::from_pass("Codex 炼境技能已分发", pass, detail)); + let (pass, detail) = check_skill(read(".claude/skills/lian-jing/SKILL.md").as_deref()); + checks.push(VerifyCheck::from_pass("Claude 炼境技能已分发", pass, detail)); checks.push(check_codex_mcp( read(".codex/config.toml").as_deref(), project_id, @@ -1310,7 +1318,7 @@ mod tests { // 空目录:所有检查均不通过(真实反映"什么都没接") let checks = verify_onboarding(&conn, "p1", &base); - assert_eq!(checks.len(), 11, "检查链应为十一项"); + assert_eq!(checks.len(), 12, "检查链应为十二项"); assert!(checks.iter().all(|c| !c.pass), "空项目应全部不通过"); // 铺齐全部生效条件后:所有检查全通过 @@ -1407,6 +1415,25 @@ mod tests { ); } + #[test] + fn skill_is_projected_to_codex_and_claude_from_one_source() { + let base = std::env::temp_dir().join(format!( + "lianjing_skill_projection_{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&base); + std::fs::create_dir_all(&base).unwrap(); + dispatch_templates(base.to_str().unwrap()); + let codex = std::fs::read_to_string(base.join(".agents/skills/lian-jing/SKILL.md")).unwrap(); + let claude = std::fs::read_to_string(base.join(".claude/skills/lian-jing/SKILL.md")).unwrap(); + assert_eq!(codex, claude, "两端 skill 必须来自同一模板"); + assert!(check_skill(Some(&claude)).0); + let packs: serde_json::Value = serde_json::from_str(TMPL_CAPABILITY_PACKS).unwrap(); + let artifacts = packs["packs"][0]["project_artifacts"].to_string(); + assert!(artifacts.contains(".claude/skills/lian-jing/SKILL.md"), "能力包清单须登记 Claude 投影"); + let _ = std::fs::remove_dir_all(&base); + } + #[test] fn skill_check_requires_canonical_context() { let legacy = "---\nname: lian-jing\n---\n读取 .claude/liangjing.json";