diff --git a/README.md b/README.md index 9550deb..c784318 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ CraftKit 是一组面向 Codex 插件市场的中性 Skill 工具。项目从通 | --- | --- | --- | | `dev` | 软件设计、编码、审查与测试 | 已初始化,暂无 Skill | | `doc` | 文档转换、整理与写作 | 已迁移 `format-md`、`docx-to-md`、`md-to-docx`、`xlsx-to-md`、`archive` | -| `git` | 分支、提交、变更提取与集成 | 已迁移 `commit-msg`、`branch`、`identity`、`export` | +| `git` | 分支、提交、变更提取与集成 | 已迁移 `commit-msg`、`branch`、`identity`、`export`、`integrate` | | `knowledge` | 项目初始化、交接、复盘与经验 | 已迁移 `handoff`、`init` | | `skill` | 项目规范及 Skill 创建、迁移与维护 | 已迁移 `guidance` | diff --git a/migration/MIGRATION_PLAN.md b/migration/MIGRATION_PLAN.md index 27f1721..4e60932 100644 --- a/migration/MIGRATION_PLAN.md +++ b/migration/MIGRATION_PLAN.md @@ -91,7 +91,7 @@ 1. `branch`(已完成) 2. `identity`(已完成,只管理 Git 提交用户名和邮箱) 3. `export`(已完成,环境配置默认排除,规范明确要求并再次确认后才可导出) -4. `integrate`(待独立重建,不沿用特定组织的分支和审批模型) +4. `integrate`(已完成,按项目规范通过隔离 worktree 评估、准备和发布预集成分支) `commit-msg` 将作为只读 Git 特殊样本先行完成。涉及提交、合并和远端操作的 Skill 必须保留明确授权边界,并保护脏工作区。 @@ -234,3 +234,4 @@ plan-change - [ ] 按插件和风险类型继续推进同质批量迁移。 - [x] 完成首个同质批量:`md-to-docx`、`xlsx-to-md`、`archive`。 - [x] 完成 Git 本地操作批次:`identity`、`export`。 +- [x] 完成 Git 高风险隔离集成样本:`integrate`。 diff --git a/migration/source-lock.json b/migration/source-lock.json index f306d06..18b06b2 100644 --- a/migration/source-lock.json +++ b/migration/source-lock.json @@ -262,7 +262,10 @@ "source-b:4ca0940d73b4b626": { "sourcePathHash": "4ca0940d73b4b6264a37e1a344d669367dc313364a0077abf8bcaf97cfed42a1", "sourceSha256": "92b3cea563f544806493245bdf099e2357112ccbce15007593c94d62c26c258c", - "status": "pending" + "status": "migrated", + "target": "plugins/git/skills/integrate", + "targetVersion": "0.1.0", + "reviewedAt": "2026-08-25" }, "source-b:0b917cc63232dff3": { "sourcePathHash": "0b917cc63232dff38c375d3bec3fabe57390201c34c3ce96f7885eb23bca91d7", diff --git a/migration/tests/test_git_integrate.py b/migration/tests/test_git_integrate.py new file mode 100644 index 0000000..921fc58 --- /dev/null +++ b/migration/tests/test_git_integrate.py @@ -0,0 +1,79 @@ +"""验证预集成 Skill 的文档契约和隔离 Git 行为。""" + +from __future__ import annotations + +import subprocess +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +SKILL = ROOT / "plugins/git/skills/integrate/SKILL.md" + + +class GitIntegrateTests(unittest.TestCase): + """在临时仓库中验证 worktree 集成不会移动输入引用。""" + + def git(self, repo: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]: + """执行测试仓库中的 Git 命令并保留失败状态供冲突断言。""" + + return subprocess.run(["git", "-C", str(repo), *args], capture_output=True, text=True, encoding="utf-8", check=check) + + def make_repo(self, root: Path, conflict: bool = False) -> tuple[Path, str, str]: + """创建具有源、目标分叉的隔离仓库。""" + + repo = root / "repo"; repo.mkdir(); self.git(repo, "init", "-q") + self.git(repo, "config", "user.name", "Test User"); self.git(repo, "config", "user.email", "test@example.invalid") + (repo / "base.txt").write_text("base\n", encoding="utf-8") + self.git(repo, "add", "base.txt"); self.git(repo, "commit", "-q", "-m", "base") + self.git(repo, "branch", "target"); self.git(repo, "switch", "-q", "-c", "source") + source_file = repo / ("base.txt" if conflict else "source.txt") + source_file.write_text("source\n", encoding="utf-8") + self.git(repo, "add", "-A"); self.git(repo, "commit", "-q", "-m", "source") + source_hash = self.git(repo, "rev-parse", "source").stdout.strip() + self.git(repo, "switch", "-q", "target") + target_file = repo / ("base.txt" if conflict else "target.txt") + target_file.write_text("target\n", encoding="utf-8") + self.git(repo, "add", "-A"); self.git(repo, "commit", "-q", "-m", "target") + target_hash = self.git(repo, "rev-parse", "target").stdout.strip() + return repo, source_hash, target_hash + + def test_isolated_merge_preserves_source_and_target_refs(self) -> None: + """预集成分支发生 merge 后,源和目标引用必须保持原哈希。""" + + with tempfile.TemporaryDirectory() as temp: + root = Path(temp); repo, source_hash, target_hash = self.make_repo(root) + worktree = root / "integration" + self.git(repo, "worktree", "add", "-q", "-b", "pre/integration", str(worktree), "target") + self.git(worktree, "merge", "--no-edit", "source") + self.assertEqual(source_hash, self.git(repo, "rev-parse", "source").stdout.strip()) + self.assertEqual(target_hash, self.git(repo, "rev-parse", "target").stdout.strip()) + self.assertTrue((worktree / "source.txt").is_file()) + self.assertTrue((worktree / "target.txt").is_file()) + + def test_conflict_stops_before_automatic_resolution(self) -> None: + """真实冲突应停留在未合并状态,不移动源或目标引用。""" + + with tempfile.TemporaryDirectory() as temp: + root = Path(temp); repo, source_hash, target_hash = self.make_repo(root, conflict=True) + worktree = root / "integration" + self.git(repo, "worktree", "add", "-q", "-b", "pre/conflict", str(worktree), "target") + result = self.git(worktree, "merge", "--no-edit", "source", check=False) + self.assertNotEqual(0, result.returncode) + self.assertIn("UU base.txt", self.git(worktree, "status", "--short").stdout) + self.assertEqual(source_hash, self.git(repo, "rev-parse", "source").stdout.strip()) + self.assertEqual(target_hash, self.git(repo, "rev-parse", "target").stdout.strip()) + + def test_skill_separates_stages_and_protects_delivery(self) -> None: + """Skill 必须保留阶段授权、环境配置阻断和强推禁用边界。""" + + text = SKILL.read_text(encoding="utf-8") + for value in ("assess", "prepare", "publish", "独立 worktree", "环境配置", "私钥"): + self.assertIn(value, text) + publish = (SKILL.parent / "references/publish.md").read_text(encoding="utf-8") + self.assertIn("默认不改用 `--force`", publish) + + +if __name__ == "__main__": + unittest.main() diff --git a/plugins/git/.codex-plugin/plugin.json b/plugins/git/.codex-plugin/plugin.json index f88c36d..b78f3ec 100644 --- a/plugins/git/.codex-plugin/plugin.json +++ b/plugins/git/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "git", - "version": "0.2.0", + "version": "0.3.0", "description": "安全、可复核的通用 Git 工作流。", "author": { "name": "CraftKit" @@ -9,7 +9,7 @@ "interface": { "displayName": "Git", "shortDescription": "安全、可复核的 Git 工具", - "longDescription": "提供分支创建、提交信息、身份配置和变更导出相关的安全 Git 工作流。", + "longDescription": "提供分支创建、提交信息、身份配置、变更导出和隔离集成相关的安全 Git 工作流。", "developerName": "CraftKit", "category": "Productivity", "capabilities": ["Read", "Write"], diff --git a/plugins/git/skills/integrate/SKILL.md b/plugins/git/skills/integrate/SKILL.md new file mode 100644 index 0000000..ac81aa3 --- /dev/null +++ b/plugins/git/skills/integrate/SKILL.md @@ -0,0 +1,41 @@ +--- +name: integrate +description: 评估两个 Git 分支的集成风险,并在用户逐步确认后通过隔离 worktree 准备、验证和发布预集成分支。适用于合并前验证或准备 MR/PR;直接修改目标分支、一键推送或未经审查处理冲突不应触发本 Skill。 +--- + +# Git 分支集成 + +在不修改源分支和目标分支引用的前提下准备可审查的预集成分支。优先读取当前项目的 `AGENTS.md`、`.craftkit/project.json`、`.craftkit/standards/` 和贡献指南;不得假设默认分支、版本分支、远程、托管平台或审批角色。 + +## 选择阶段 + +- `assess`:只读比较分支、预判冲突和识别交付风险。执行时读取 [评估规则](references/assess.md)。 +- `prepare`:在独立 worktree 中创建预集成分支、执行 merge 并验证。执行时读取 [准备规则](references/prepare.md)。 +- `publish`:推送预集成分支并准备 MR/PR。执行时读取 [发布规则](references/publish.md)。 + +用户要求“集成”但未明确阶段时,从 `assess` 开始。前一阶段的授权不能自动扩展到后一阶段。 + +## 不可跨越的边界 + +- 源分支和目标分支只作为输入引用;记录操作前哈希,结束时复核均未变化。 +- 不在用户当前主工作区执行 merge,使用明确路径的独立 worktree。 +- fetch、worktree 创建、分支创建、merge、冲突暂存、commit、push 和创建 MR/PR 分别展示准确命令并取得对应确认。 +- 工作区不干净、引用不可解析、已有 worktree 冲突或分支归属不清时停止写操作。 +- 不自动使用 `ours`、`theirs`、rebase、reset、stash、强制推送或历史改写。 +- 不直接把预集成分支合入目标分支,也不自动删除 worktree 或分支。 + +## 环境和本地配置 + +评估源目标差异中的环境配置、本地配置、机器配置、`.craftkit/local/**` 和 `.craftkit/cache/**`。命中后默认暂停 `prepare` 或 `publish`,列出文件和风险。 + +只有适用的 `AGENTS.md`、`.craftkit/agents/` 或 `.craftkit/standards/` 明确要求交付,并经用户再次确认,才可继续;私钥、真实密钥和检测到的凭据始终阻断。共享的 `.craftkit/agents/**`、`standards/**`、`knowledge/**`、`handoff/**` 和 `project.json` 按普通项目变更审查。 + +## 完成标准 + +交付结果只能是以下之一: + +- 只读集成评估报告; +- 本地隔离且已验证的预集成分支; +- 经独立授权推送的预集成分支及 MR/PR 建议。 + +报告源、目标、预集成分支和对应哈希,列出冲突处理、验证结果、环境配置判断及仍需人工完成的动作。 diff --git a/plugins/git/skills/integrate/agents/openai.yaml b/plugins/git/skills/integrate/agents/openai.yaml new file mode 100644 index 0000000..1b27ee9 --- /dev/null +++ b/plugins/git/skills/integrate/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Integrate Branches" + short_description: "隔离评估、准备并发布可审查的预集成分支" + default_prompt: "使用 $integrate 先只读评估这两个分支的集成风险,再决定是否准备预集成分支。" diff --git a/plugins/git/skills/integrate/references/assess.md b/plugins/git/skills/integrate/references/assess.md new file mode 100644 index 0000000..a6aab6c --- /dev/null +++ b/plugins/git/skills/integrate/references/assess.md @@ -0,0 +1,20 @@ +# 只读集成评估 + +## 输入确认 + +明确仓库、源分支、目标分支、远程,以及项目是否要求预集成分支。不要从当前分支名猜测合并方向。 + +## 检查顺序 + +1. 读取项目 Git 规范和适用的 Agent 说明。 +2. 运行 `git status --short --branch`、`git worktree list --porcelain`、`git remote -v` 和 `git branch -vv`。 +3. 使用 `git rev-parse --verify "^{commit}"` 与目标分支对应命令解析引用并记录完整哈希。 +4. 使用 `git merge-base`、`git rev-list --left-right --count`、`git log --left-right --cherry-pick` 和 `git diff --stat` 说明分叉、独有提交和文件范围。 +5. Git 版本支持时使用 `git merge-tree "" ""` 预判冲突;该结果只是静态预判,不替代真实 merge。 +6. 检查差异中的环境配置、本地配置、二进制文件、子模块、生成物、迁移脚本和 `.craftkit` 内容。 + +只读评估不得自动 fetch。用户要求远端最新状态时,先展示将执行的远程和 refspec,取得单独确认后 fetch,再重新执行全部引用与差异检查。 + +## 输出 + +报告源和目标哈希、共同基线、双方独有提交数、预计冲突、保护文件、建议的预集成方向和所需验证命令。证据不足时停止,不生成写操作命令序列冒充可执行结论。 diff --git a/plugins/git/skills/integrate/references/prepare.md b/plugins/git/skills/integrate/references/prepare.md new file mode 100644 index 0000000..1a19f04 --- /dev/null +++ b/plugins/git/skills/integrate/references/prepare.md @@ -0,0 +1,35 @@ +# 准备预集成分支 + +## 前置条件 + +- `assess` 已完成且用户确认源、目标、基准方向、预集成分支名和 worktree 绝对路径。 +- 源、目标引用仍等于评估时记录的哈希。 +- 当前工作区和目标 worktree 路径没有未处理状态或归属冲突。 +- 环境配置风险已按项目规范处理;凭据风险为零。 + +## 隔离创建 + +根据项目规范决定预集成分支基于源还是目标。没有规范时说明两种历史形状,由用户选择,不能默认套用固定组织模型。 + +执行前展示准确的 `git worktree add` 命令、创建的分支和基准哈希。远程跟踪引用作为基准时避免意外设置错误 upstream。创建后在新 worktree 中复核: + +```text +git branch --show-current +git rev-parse HEAD +git status --short --branch +``` + +## 合并确认 + +再次展示当前预集成分支、待合入引用、双方哈希和准确 merge 命令,取得确认后才执行。禁止把 worktree 创建授权视为 merge 授权。 + +- 无冲突:检查 merge 结果和提交图,再按项目要求运行构建、测试或静态检查。 +- 有冲突:列出 `git diff --name-only --diff-filter=U`,逐文件分析双方意图。 +- 语义冲突必须由用户决定,不能机械执行 `checkout --ours` 或 `--theirs`。 +- 冲突文件的 `git add` 和最终 merge commit 需要在展示解决方案与 staged diff 后确认。 + +## 结束复核 + +确认源、目标引用仍等于操作前哈希;只有预集成分支发生变化。记录 merge 提交、测试结果和未验证边界。 + +保留 worktree 供用户复核。清理 worktree、删除分支或中止 merge 都是独立操作,不属于自动收尾。 diff --git a/plugins/git/skills/integrate/references/publish.md b/plugins/git/skills/integrate/references/publish.md new file mode 100644 index 0000000..6d73068 --- /dev/null +++ b/plugins/git/skills/integrate/references/publish.md @@ -0,0 +1,25 @@ +# 发布预集成分支 + +## 推送前 + +1. 确认当前位于预集成 worktree,状态干净。 +2. 复核源、目标和预集成分支哈希。 +3. 展示验证结果、目标远程、远程同名分支状态和将执行的 push 命令。 +4. 重新检查环境配置和凭据风险。 + +首次推送只能在用户确认后执行明确的远程和分支命令。发现远程同名分支、非快进、历史分叉或他人提交时停止;默认不改用 `--force` 或 `--force-with-lease`。 + +## MR/PR + +根据仓库远程地址、项目规范和实际可用工具判断 GitHub、GitLab 或纯 Git 流程,不把任一平台设为默认。 + +基于目标分支到预集成分支的真实 diff 编写: + +- 目的和范围; +- 核心改动; +- 冲突及其处理依据; +- 测试结果和未验证项; +- 环境配置与部署注意事项; +- 关联任务或项目文档。 + +默认只给出标题、描述、命令或网页入口。实际创建 MR/PR 需要新的明确授权;审核、合并、删除远程分支和删除 worktree 均不在本阶段自动执行。