From 29589d3ea4819a18171c9e8c0ec4751e396f1401 Mon Sep 17 00:00:00 2001 From: "hiwepy@gmail.com" Date: Sat, 3 Oct 2026 23:44:40 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=20CodeGuard=20?= =?UTF-8?q?=E8=8B=B1=E6=96=87=E6=98=BE=E7=A4=BA=E5=90=8D=E7=A7=B0=E5=B9=B6?= =?UTF-8?q?=E8=A1=A5=E9=BD=90=E6=A0=87=E5=87=86=20MCP=20=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/plugins/marketplace.json | 12 ++++++------ .claude-plugin/marketplace.json | 2 +- .codex-plugin/plugin.json | 4 ++-- .zcode-plugin/plugin.json | 6 +++--- CHANGELOG.md | 8 ++++++++ README.md | 2 ++ README.zh-CN.md | 4 ++++ kimi.plugin.json | 4 ++-- mcp.json | 16 ++++++++++++++++ openspec/specs/mcp-tool-server/spec.md | 10 ++++++++++ plugin.json | 6 +++--- tests/test_plugin_manifests.py | 19 +++++++++++++++++++ 12 files changed, 76 insertions(+), 17 deletions(-) create mode 100644 mcp.json diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 58e0cdd..8f4e576 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -1,7 +1,7 @@ { "name": "partme-ai-codeguard", "interface": { - "displayName": "代码规范守卫" + "displayName": "CodeGuard" }, "plugins": [ { @@ -9,20 +9,20 @@ "source": { "source": "url", "url": "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/full-stack-plugins/codeguard-plugin.git", - "ref": "v0.18.2" + "ref": "v0.18.3" }, "policy": { "installation": "AVAILABLE", "authentication": "ON_USE" }, "category": "Developer Tools", - "version": "0.18.2", + "version": "0.18.3", "description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.", - "icon": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.18.2/assets/official-logo.png", + "icon": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.18.3/assets/official-logo.png", "interface": { - "displayName": "代码规范守卫", + "displayName": "CodeGuard", "shortDescription": "Trustworthy code checks and Java impact analysis", - "logo": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.18.2/assets/official-logo.png" + "logo": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.18.3/assets/official-logo.png" } } ] diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index e433022..92e2cbb 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -7,7 +7,7 @@ { "name": "codeguard", "description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.", - "version": "0.18.2", + "version": "0.18.3", "source": "./" } ] diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 9ec0568..bd6116a 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "codeguard", - "version": "0.18.2+codex.20260929", + "version": "0.18.3+codex.20261003", "description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.", "author": { "name": "Full Stack Skills / PartMe.AI", @@ -23,7 +23,7 @@ ], "skills": "./skills/", "interface": { - "displayName": "代码规范守卫", + "displayName": "CodeGuard", "shortDescription": "Trustworthy code checks and Java impact analysis", "longDescription": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit. Java planning is read-only; executing project checks runs trusted build/test commands, not a security sandbox.", "developerName": "Full Stack Skills / PartMe.AI", diff --git a/.zcode-plugin/plugin.json b/.zcode-plugin/plugin.json index 6d26778..cb29dab 100644 --- a/.zcode-plugin/plugin.json +++ b/.zcode-plugin/plugin.json @@ -1,11 +1,11 @@ { "name": "codeguard", - "displayName": "代码规范守卫", + "displayName": "CodeGuard", "displayName_i18n": { - "en": "代码规范守卫", + "en": "CodeGuard", "zh-CN": "代码规范检查" }, - "version": "0.18.2", + "version": "0.18.3", "description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.", "description_i18n": { "en": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.", diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dba32f..558645f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.18.3 — 2026-10-03 + +- 各宿主和市场清单的显示名称统一为 `CodeGuard`,保留中文本地化翻译。 + +- 补齐标准根目录 `mcp.json`,让可移植客户端发现已有 CodeGuard stdio MCP 服务。 +- 增加配置发现回归测试,明确依赖安装与工具调用的目标项目路径要求。 + + 按版本段落提炼的主题摘要(生成于 2026-09-23,来源:git 历史 212 个提交与各 release 提交)。逐提交细节以 `git log` 与 GitHub Releases 为准;本文件按主题归纳,不逐条罗列。 ## v0.16.1 — 测试可移植性收口与汇流 diff --git a/README.md b/README.md index 464e188..8717037 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,8 @@ CVE exits: 0 pass, 1 unverified, 2 findings, 3 invalid ecosystem. Maven/npm/pip- ### MCP server +The root `mcp.json` provides portable Agent Plugins 1.0.0 discovery of the existing CodeGuard stdio server. The host needs Python and the dependencies in `requirements.txt`; installation does not install them automatically. The configured working directory is the plugin root. For `check_code_style`, `auto_fix`, and `analyze_java_impact`, supply the target project’s absolute `path` in each tool call; the plugin directory is not the user project. `list_languages` needs no project path. + ```bash # Requires the dependencies declared in requirements.txt. python3 scripts/run_check.py --mcp /path/to/project diff --git a/README.zh-CN.md b/README.zh-CN.md index e38f5c4..2ff2783 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -168,3 +168,7 @@ ruff check hooks scripts tests ## 许可与隐私 Apache-2.0 — [LICENSE](./LICENSE)。原生构建器/扫描器可能访问依赖仓库和漏洞数据库,请查看 [PRIVACY.md](./PRIVACY.md) 与 [TERMS.md](./TERMS.md)。 + +### 标准 MCP 配置 + +根目录 `mcp.json` 按 Agent Plugins 1.0.0 声明已有 CodeGuard stdio 服务。宿主需要 Python 及 `requirements.txt` 中的依赖,插件不会自动安装。配置工作目录是插件根目录;调用 `check_code_style`、`auto_fix`、`analyze_java_impact` 时必须显式传入目标项目的绝对 `path`,不得把插件目录当作用户项目。`list_languages` 不需要项目路径。 diff --git a/kimi.plugin.json b/kimi.plugin.json index 6b5f369..33f801d 100644 --- a/kimi.plugin.json +++ b/kimi.plugin.json @@ -1,6 +1,6 @@ { "name": "codeguard", - "version": "0.18.2", + "version": "0.18.3", "description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.", "author": { "name": "Full Stack Skills / PartMe.AI" @@ -20,7 +20,7 @@ "pre-commit" ], "interface": { - "displayName": "代码规范守卫", + "displayName": "CodeGuard", "shortDescription": "Trustworthy code checks and Java impact analysis", "longDescription": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit. Java planning is read-only; executing project checks runs trusted build/test commands, not a security sandbox.", "developerName": "Full Stack Skills / PartMe.AI", diff --git a/mcp.json b/mcp.json new file mode 100644 index 0000000..4bb3139 --- /dev/null +++ b/mcp.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", + "mcpServers": { + "codeguard": { + "type": "stdio", + "command": "python", + "args": [ + "-X", + "utf8", + "./scripts/run_check.py", + "--mcp" + ], + "cwd": "./" + } + } +} diff --git a/openspec/specs/mcp-tool-server/spec.md b/openspec/specs/mcp-tool-server/spec.md index 50e4f8b..be16d6f 100644 --- a/openspec/specs/mcp-tool-server/spec.md +++ b/openspec/specs/mcp-tool-server/spec.md @@ -52,3 +52,13 @@ auto_fix MUST 默认仅处理 Git 改动文件并按相同范围复检;无法 - **WHEN** the client calls `list_languages` - **THEN** the response enumerates every entry in `scripts/languages.json` with exactly two fields (`id`, `name`) per record + +### Requirement: Portable MCP discovery SHALL use root mcp.json + +提供已有 MCP 服务的发布包 MUST 在根目录包含标准 `mcp.json`,声明 Agent Plugins 1.0.0 MCP Schema、`stdio` 传输、单独的 Python 可执行命令与参数,指向包内 `scripts/run_check.py --mcp`。不得在可移植 `plugin.json` 中内嵌 `mcpServers`。 + +#### Scenario: A portable client discovers CodeGuard +- **WHEN** 客户端读取根目录 `mcp.json` +- **THEN** 可发现 `codeguard` stdio 服务,配置中的脚本存在于插件包内,Schema 版本与 `plugin.json` 一致 +- **AND** 项目工具调用由客户端显式传入目标项目的绝对 `path`;配置工作目录为插件根,不代表用户项目 +- **AND** MCP SDK 依赖仍通过 `requirements.txt` 声明,不自动安装 diff --git a/plugin.json b/plugin.json index 751db65..761c030 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "codeguard", - "version": "0.18.2", + "version": "0.18.3", "description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.", "author": { "name": "Full Stack Skills / PartMe.AI", @@ -25,7 +25,7 @@ "extensions": { "com.openai": { "interface": { - "displayName": "代码规范守卫", + "displayName": "CodeGuard", "shortDescription": "Trustworthy code checks and Java impact analysis", "longDescription": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit. Java planning is read-only; executing project checks runs trusted build/test commands, not a security sandbox.", "developerName": "Full Stack Skills / PartMe.AI", @@ -50,7 +50,7 @@ } }, "ai.nanoco.nanoclaw": { - "agentName": "代码规范守卫" + "agentName": "CodeGuard" } } } diff --git a/tests/test_plugin_manifests.py b/tests/test_plugin_manifests.py index 0f71e04..378ed8a 100644 --- a/tests/test_plugin_manifests.py +++ b/tests/test_plugin_manifests.py @@ -11,6 +11,25 @@ class PluginManifestTest(unittest.TestCase): + def test_portable_mcp_config_discovers_the_existing_server(self): + config = json.loads((ROOT / "mcp.json").read_text(encoding="utf-8")) + self.assertEqual( + "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", config["$schema"]) + self.assertEqual({"$schema", "mcpServers"}, set(config)) + server = config["mcpServers"]["codeguard"] + self.assertEqual("stdio", server["type"]) + self.assertIn(server["command"], ("python", "python3")) + self.assertEqual("./", server["cwd"]) + self.assertIn("--mcp", server["args"]) + scripts = [arg for arg in server["args"] if arg.endswith(".py")] + self.assertEqual(1, len(scripts)) + script = (ROOT / scripts[0]).resolve() + self.assertTrue(script.is_relative_to(ROOT.resolve())) + self.assertTrue(script.is_file()) + self.assertEqual(ROOT / "scripts" / "run_check.py", script) + self.assertNotIn("PLUGIN_ROOT", server.get("env", {})) + self.assertNotIn("PLUGIN_DATA", server.get("env", {})) + def test_released_manifests_do_not_publish_placeholder_mcp_server(self): self.assertFalse((ROOT / ".mcp.json").exists()) for manifest in MANIFESTS: From 229efb5b490c483160b000a3263741dbf1b1df18 Mon Sep 17 00:00:00 2001 From: "hiwepy@gmail.com" Date: Sat, 3 Oct 2026 23:49:47 +0800 Subject: [PATCH 2/5] =?UTF-8?q?docs:=20=E5=B0=86=E6=A0=87=E5=87=86=20MCP?= =?UTF-8?q?=20=E8=AF=B4=E6=98=8E=E7=BA=B3=E5=85=A5=E7=8E=B0=E6=9C=89?= =?UTF-8?q?=E5=8F=8C=E8=AF=AD=E7=AB=A0=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.zh-CN.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.zh-CN.md b/README.zh-CN.md index 2ff2783..9623fa1 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -115,6 +115,8 @@ CVE 退出码:0 通过、1 未验证、2 发现漏洞、3 非法生态。Maven ### MCP 服务 +根目录 `mcp.json` 按 Agent Plugins 1.0.0 声明已有 CodeGuard stdio 服务。宿主需要 Python 及 `requirements.txt` 中的依赖,插件不会自动安装。配置工作目录是插件根目录;调用 `check_code_style`、`auto_fix`、`analyze_java_impact` 时必须显式传入目标项目的绝对 `path`,不得把插件目录当作用户项目。`list_languages` 不需要项目路径。 + ```bash # 需要 requirements.txt 已声明的依赖。 python3 scripts/run_check.py --mcp /path/to/project @@ -168,7 +170,3 @@ ruff check hooks scripts tests ## 许可与隐私 Apache-2.0 — [LICENSE](./LICENSE)。原生构建器/扫描器可能访问依赖仓库和漏洞数据库,请查看 [PRIVACY.md](./PRIVACY.md) 与 [TERMS.md](./TERMS.md)。 - -### 标准 MCP 配置 - -根目录 `mcp.json` 按 Agent Plugins 1.0.0 声明已有 CodeGuard stdio 服务。宿主需要 Python 及 `requirements.txt` 中的依赖,插件不会自动安装。配置工作目录是插件根目录;调用 `check_code_style`、`auto_fix`、`analyze_java_impact` 时必须显式传入目标项目的绝对 `path`,不得把插件目录当作用户项目。`list_languages` 不需要项目路径。 From ea2d3affb0ab3a29ad27ee4676a6563790f724e2 Mon Sep 17 00:00:00 2001 From: loong10k <20489781+loong10k@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:16:00 +0800 Subject: [PATCH 3/5] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85=E5=85=A8?= =?UTF-8?q?=E6=A0=88=E5=BC=80=E5=8F=91=E4=B8=8E=20AIGC=20=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E5=B8=82=E5=9C=BA=E5=AF=BC=E8=88=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 +++++++++ README.zh-CN.md | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/README.md b/README.md index 8717037..2880f5e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,14 @@ # CodeGuard plugin +## Plugin marketplaces + +This plugin belongs to **Full-stack development**. + +| Category | Marketplace | Purpose | +| --- | --- | --- | +| Full-stack development | [Full Stack Plugins](https://github.com/partme-ai/full-stack-plugins) | Architecture and UI design, code understanding, quality checks, code review, workflow governance, and server operations | +| AIGC content creation | [Full AIGC Plugins](https://github.com/partme-ai/full-aigc-plugins) | Image, video, audio, music, 3D, and multimodal content creation | + > Parity: README.md and README.zh-CN.md must keep the same heading structure, local links and version strings; enforced by tests/test_readme_parity.py. [English](README.md) · [简体中文](README.zh-CN.md) diff --git a/README.zh-CN.md b/README.zh-CN.md index 9623fa1..3200455 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,5 +1,14 @@ # CodeGuard 插件 +## 插件市场导航 + +本插件所属分类:**全栈开发**。 + +| 分类 | 插件市场入口 | 用途 | +| --- | --- | --- | +| 全栈开发 | [Full Stack Plugins](https://github.com/partme-ai/full-stack-plugins) | 架构与 UI 设计、代码理解、质量检查、代码审查、流程治理与服务器运维 | +| AIGC 内容创作 | [Full AIGC Plugins](https://github.com/partme-ai/full-aigc-plugins) | 图像、视频、音频、音乐、3D 与多模态内容创作 | + > 结构对齐:README.md 与 README.zh-CN.md 的标题层级、本地链接和版本串必须一致,由 tests/test_readme_parity.py 检查。 [English](README.md) · [简体中文](README.zh-CN.md) From 70ebd9441152398db076953e8ac1eba3e818d6d5 Mon Sep 17 00:00:00 2001 From: loong10k <20489781+loong10k@users.noreply.github.com> Date: Sat, 17 Oct 2026 11:25:44 +0800 Subject: [PATCH 4/5] fix(release): make vendored skills portable and prepare plugin 0.20.1 --- .agents/plugins/marketplace.json | 8 +- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- .gitattributes | 3 + .zcode-plugin/plugin.json | 2 +- CHANGELOG.md | 6 + docs/vendor-portability.md | 53 ++++ kimi.plugin.json | 2 +- .../.openspec.yaml | 2 + .../design.md | 33 +++ .../proposal.md | 28 +++ .../specs/engineering-host-bridge/spec.md | 77 ++++++ .../integrate-engineering-guard-host/tasks.md | 27 ++ plugin.json | 2 +- scripts/bump-plugin.mjs | 232 +++++++++++------- scripts/vendor/skill_vendor.py | 12 +- tests/test_skill_vendor_portability.py | 105 ++++++++ 18 files changed, 496 insertions(+), 102 deletions(-) create mode 100644 .gitattributes create mode 100644 docs/vendor-portability.md create mode 100644 openspec/changes/integrate-engineering-guard-host/.openspec.yaml create mode 100644 openspec/changes/integrate-engineering-guard-host/design.md create mode 100644 openspec/changes/integrate-engineering-guard-host/proposal.md create mode 100644 openspec/changes/integrate-engineering-guard-host/specs/engineering-host-bridge/spec.md create mode 100644 openspec/changes/integrate-engineering-guard-host/tasks.md create mode 100644 tests/test_skill_vendor_portability.py diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 58fa387..af0ceb1 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -9,20 +9,20 @@ "source": { "source": "url", "url": "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/full-stack-plugins/codeguard-plugin.git", - "ref": "v0.20.0" + "ref": "v0.20.1" }, "policy": { "installation": "AVAILABLE", "authentication": "ON_USE" }, "category": "Developer Tools", - "version": "0.20.0", + "version": "0.20.1", "description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.", - "icon": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.20.0/assets/official-logo.png", + "icon": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.20.1/assets/official-logo.png", "interface": { "displayName": "CodeGuard", "shortDescription": "Trustworthy code checks and Java impact analysis", - "logo": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.20.0/assets/official-logo.png" + "logo": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.20.1/assets/official-logo.png" } } ] diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index bc07f3b..477c38c 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -7,7 +7,7 @@ { "name": "codeguard", "description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.", - "version": "0.20.0", + "version": "0.20.1", "source": "./" } ] diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index ee10efb..f2681b9 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "codeguard", - "version": "0.20.0", + "version": "0.20.1", "description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.", "author": { "name": "Full Stack Skills / PartMe.AI", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index e7d6ea0..4eeaced 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "codeguard", - "version": "0.20.0+codex.20261004", + "version": "0.20.1+codex.20261017", "description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.", "author": { "name": "Full Stack Skills / PartMe.AI", diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..24095ed --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# Vendored text bytes must remain identical to the LF source snapshots. +# Auto detection keeps binary skill artifacts byte-for-byte unchanged. +skills/** text=auto eol=lf diff --git a/.zcode-plugin/plugin.json b/.zcode-plugin/plugin.json index 81de658..09013e6 100644 --- a/.zcode-plugin/plugin.json +++ b/.zcode-plugin/plugin.json @@ -5,7 +5,7 @@ "en": "CodeGuard", "zh-CN": "代码规范检查" }, - "version": "0.20.0", + "version": "0.20.1", "description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.", "description_i18n": { "en": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.", diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cec458..9f09802 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.20.1 — 2026-10-17 + +- 修复 Windows 与 Unix 路径排序不同导致的受管技能摘要误报;上游检出固定 LF,保留二进制原件和既有来源锁。 +- 补齐公共发版器的 Claude 清单、Python 项目版本预检及写后核对,版本漂移在写入前拒绝。 +- 登记工程守卫插件接入规划,所有新增任务仍待实施;不声明新增原生检查或真实宿主资格。 + ## 0.20.0 — 2026-10-04 - 固定 Rust CLI 0.1.4 与全部 32 份可执行、未验收 grammar 的公开制品身份。 diff --git a/docs/vendor-portability.md b/docs/vendor-portability.md new file mode 100644 index 0000000..0d65ce4 --- /dev/null +++ b/docs/vendor-portability.md @@ -0,0 +1,53 @@ +# Portable vendor byte verification + +Managed skills remain owned by their locked source repositories. The verifier +fix does not update a source ref, lock digest or skill blob. Plugin versioning +and publication are separate release steps. + +The directory digest sorts case-sensitive POSIX **relative path strings** and +hashes each relative path plus the SHA256 of the file's original bytes. It does +not sort native Path objects, case-fold paths, normalize Unicode, or rewrite +CRLF during validation. Binary data is hashed unchanged. Mixed case and file / +directory prefixes have explicit regression coverage. + +The private upstream checkout uses command-local `core.autocrlf=false` and +`core.eol=lf`; it does not change global / repository Git configuration or +replace inherited `GIT_CONFIG_COUNT` options. Tests use a real temporary Git +source and prove LF text, exact binary bytes and unchanged caller configuration +even when inherited settings request CRLF. + +`.gitattributes` declares `skills/** text=auto eol=lf`. Git detects binary files +and preserves their bytes; text snapshots use LF on future checkouts. Adding +this policy does **not** repair bytes already present in a Windows working tree. +A current CRLF snapshot must continue to fail raw-byte vendor validation. Do +not regenerate the lock, silently normalize during hashing, overwrite local +changes, or reset the whole skills directory to make that failure disappear. +Use a separately verified clean LF checkout when local bytes differ. Any repair +of an existing checkout requires per-file verification and preservation of all +real modifications and untracked files. + +Validation commands (temporary files must be on the approved development drive): + +```text +python -B -m unittest discover -s tests -p test_skill_vendor_portability.py -v +python -B scripts/vendor/skill_vendor.py check --offline +python -B scripts/vendor/skill_vendor.py check +``` + +Offline checks prove local byte equality. Online checks additionally verify the +pinned upstream ref / commit and byte content; they do not prove installed-agent +loading, actual model invocation, or release publication. + +## Existing Windows checkout verification + +On 2026-10-17 the existing checkout initially failed its raw-byte checks. Each +tracked skill file was compared with its Git blob before any repair; only exact +CRLF equivalents were restored to the already locked source bytes. All 484 +restored text files were backed up with before/source SHA256 records on the +approved development drive. Real modifications and untracked files were not +included. Skill blobs and the source lock remain unchanged in Git. + +After restoration, both offline and online checks in this actual checkout +passed; the earlier failures and isolated LF results remain separate evidence. +The five portability tests also passed independently on Windows. These checks +do not claim a plugin release or installed-host acceptance. diff --git a/kimi.plugin.json b/kimi.plugin.json index 7ffac32..9c78773 100644 --- a/kimi.plugin.json +++ b/kimi.plugin.json @@ -1,6 +1,6 @@ { "name": "codeguard", - "version": "0.20.0", + "version": "0.20.1", "description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.", "author": { "name": "Full Stack Skills / PartMe.AI" diff --git a/openspec/changes/integrate-engineering-guard-host/.openspec.yaml b/openspec/changes/integrate-engineering-guard-host/.openspec.yaml new file mode 100644 index 0000000..b699b88 --- /dev/null +++ b/openspec/changes/integrate-engineering-guard-host/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-17 diff --git a/openspec/changes/integrate-engineering-guard-host/design.md b/openspec/changes/integrate-engineering-guard-host/design.md new file mode 100644 index 0000000..f6751b0 --- /dev/null +++ b/openspec/changes/integrate-engineering-guard-host/design.md @@ -0,0 +1,33 @@ +# 集成设计与验收边界 + +来源:2026-10-17 按既有总体规划重建;原引用路径在当前检出及本地可用历史中未找到。本变更是新重建的任务定义,不声称恢复了历史原文,也不声明实现或用户验收已完成。依据为 [traceability](https://github.com/full-stack-plugins/codeguard/blob/8d7c0e997bf21b3306573b3be89ff8c312c9de38/docs/engineering-guard/traceability.md)、[acceptance](https://github.com/full-stack-plugins/codeguard/blob/8d7c0e997bf21b3306573b3be89ff8c312c9de38/docs/engineering-guard/acceptance.md)、[protocol](https://github.com/full-stack-plugins/codeguard/blob/8d7c0e997bf21b3306573b3be89ff8c312c9de38/docs/engineering-guard/protocol.md) 与本仓现有维护约束。 + +## Context + +本仓已有实现、公共接口和兼容义务,属于 Brownfield。当前变更沿用既有 OpenSpec,只定义总体规划已要求的增量,不建立第二套领域规则或宿主 Runtime。 + +## Goals / Non-Goals + +目标:维持现有行为,以版本化输入、来源身份与明确覆盖向专业守卫提供可核验事实;在原生账本保留稳定任务及真实验收证据。 + +非目标:在插件内复制公共内核、持久数据库、专业规则判定或授权签发;仅凭模型结论、Hook 或本地文件声明对抗性强制。 + +## Decisions + +1. 本仓 specs/tasks 是 H 系列定义及状态的唯一事实源,总体追溯表只引用。现有原生规格继续适用。 +2. 适配器固定 producer/adapter/protocol/制品身份,保留原始报告。解析不赋予可信身份;确定性结果、建议、覆盖与执行状态分离。 +3. 跨仓依赖只使用任务 ID 与发布接口,不在已安装插件中要求相邻源码目录。当前临时账本仍在 CodeGuard,公共内核实际实现仓库为 guardengine(逻辑 GuardCore)。 +4. provider 兼容任务可在最终产品发布前独立验收,但不能以离线协议样本替代任务明确要求的真实工具、宿主或服务端验收。最终迁移/发布任务保留全部组合验收责任。 +5. 授权只接受独立受信来源;已有同范围授权复用,扩大权限另核验。输入报告视为数据,不执行建议文本。 + +## Risks / Trade-offs + +旧引用只说明规划意图,不能证明旧任务实现。缺工具、宿主、身份或权限时保持 pending/blocked 证据,不编造完成节点。安装与发布时按本仓版本、技能来源和市场规则执行,规划文件存在不构成安装或发布。 + +## Migration Plan + +先固定现有 provider 行为与黄金反例,再接入公共协议和消费者,最后完成真实宿主/平台/升级回滚与规则单一 owner 交接。迁移前旧实现继续唯一拥有既有语义,迁移后不保留第二判定核;历史证据保留原身份,不自动升权。 + +## Validation + +每个任务先使目标反例失败,再实现并执行目标测试与受影响回归。证据记录输入/候选/契约/工具/平台/宿主/命令/原始输出及摘要。全部 required 场景满足后才能勾选、同步和归档。 diff --git a/openspec/changes/integrate-engineering-guard-host/proposal.md b/openspec/changes/integrate-engineering-guard-host/proposal.md new file mode 100644 index 0000000..8b73a45 --- /dev/null +++ b/openspec/changes/integrate-engineering-guard-host/proposal.md @@ -0,0 +1,28 @@ +# 工程守卫集成:codeguard-plugin + +来源:2026-10-17 按既有总体规划重建;原引用路径在当前检出及本地可用历史中未找到。本变更是新重建的任务定义,不声称恢复了历史原文,也不声明实现或用户验收已完成。依据为 [traceability](https://github.com/full-stack-plugins/codeguard/blob/8d7c0e997bf21b3306573b3be89ff8c312c9de38/docs/engineering-guard/traceability.md)、[acceptance](https://github.com/full-stack-plugins/codeguard/blob/8d7c0e997bf21b3306573b3be89ff8c312c9de38/docs/engineering-guard/acceptance.md)、[protocol](https://github.com/full-stack-plugins/codeguard/blob/8d7c0e997bf21b3306573b3be89ff8c312c9de38/docs/engineering-guard/protocol.md) 与本仓现有维护约束。 + +## Why + +总体规划引用的 H 系列任务缺少原生定义,无法校验依赖与验收归属。需要补齐本插件的唯一实施账本,并把已有插件能力接到七产品的版本化证据协议;不得把规划补齐等同于产品交付。 + +## What Changes + +- 在本仓保留 EG-CGH-001、EG-CGH-002、EG-CGH-003、EG-CGH-004、EG-CGH-005 的职责及可观察行为。 +- 定义 EG-H01、EG-H02、EG-H03、EG-H04、EG-H05、EG-H06、EG-H07、EG-H08、EG-H09、EG-H10,全部待实施;保留总体规划的稳定引用。 +- provider 兼容证明先于消费方最终发布;最终迁移验收仍要求实际产品、宿主、版本与保护证据,不减少验收范围。 +- 不初始化新规格体系,不改变既有运行时、技能来源锁、Hook 语义、批准策略或发布流程。 + +## Capabilities + +### New Capabilities + +- `engineering-host-bridge`:本插件面向公共工程协议的来源、范围、覆盖、权限与生命周期边界。 + +### Modified Capabilities + +无;既有规格继续适用,不通过本变更改写已确认行为。 + +## Impact + +计划影响本插件的适配入口、证据投影、测试与未来发布。实施时遵守 AGENTS.md;受管技能不在插件内手改,真实宿主和托管保护不得由离线模拟替代。普通验证不顺带安装、初始化、索引、外发或 Git 写动作。 diff --git a/openspec/changes/integrate-engineering-guard-host/specs/engineering-host-bridge/spec.md b/openspec/changes/integrate-engineering-guard-host/specs/engineering-host-bridge/spec.md new file mode 100644 index 0000000..03c8ae3 --- /dev/null +++ b/openspec/changes/integrate-engineering-guard-host/specs/engineering-host-bridge/spec.md @@ -0,0 +1,77 @@ +## Purpose + +定义 codeguard-plugin 在七产品工程守卫体系中的证据和集成契约;沿用现有插件边界,不以报告、模型或本地 Hook 自称交付授权。 + +来源:2026-10-17 按既有总体规划重建;原引用路径在当前检出及本地可用历史中未找到。本变更是新重建的任务定义,不声称恢复了历史原文,也不声明实现或用户验收已完成。依据为 [traceability](https://github.com/full-stack-plugins/codeguard/blob/8d7c0e997bf21b3306573b3be89ff8c312c9de38/docs/engineering-guard/traceability.md)、[acceptance](https://github.com/full-stack-plugins/codeguard/blob/8d7c0e997bf21b3306573b3be89ff8c312c9de38/docs/engineering-guard/acceptance.md)、[protocol](https://github.com/full-stack-plugins/codeguard/blob/8d7c0e997bf21b3306573b3be89ff8c312c9de38/docs/engineering-guard/protocol.md) 与本仓现有维护约束。 + +## ADDED Requirements + +### Requirement: EG-CGH-001 Pinned bridge + +系统 SHALL 宿主桥必须调用固定版本及摘要的 CodeGuard/公共执行入口,保留当前 CLI 与旧 Hook 行为;缺核心、策略或平台资格时不得回退为工程通过。 + +#### Scenario: EG-CGH-001 reject unsupported or unsafe input + +- **WHEN** 固定运行时损坏或不支持当前平台 +- **THEN** 返回准备阻塞,不选择 PATH 上另一工具 + +#### Scenario: EG-CGH-001 preserve supported behavior + +- **WHEN** 已验证组合处理同一请求 +- **THEN** 跨入口产生同一领域结果并保留运行身份 + +### Requirement: EG-CGH-002 Correct event subject + +系统 SHALL 每类事件必须绑定实际 session/worktree/task、动作和候选类型;保存、暂存、提交、合入报告不得互用,真正执行前重新核对目标身份。 + +#### Scenario: EG-CGH-002 reject unsupported or unsafe input + +- **WHEN** index 报告用于不同 HEAD 或合入候选 +- **THEN** 拒绝复用并要求验证实际候选 + +#### Scenario: EG-CGH-002 preserve supported behavior + +- **WHEN** 事件与绑定候选完全相符 +- **THEN** 仅使用该候选的有效结果 + +### Requirement: EG-CGH-003 Singleflight interaction + +系统 SHALL 相同身份和输入的并发事件可单飞;不同内容、会话、工作树不可误去重。最新失败/取消不能被迟到旧 PASS 覆盖。 + +#### Scenario: EG-CGH-003 reject unsupported or unsafe input + +- **WHEN** 两个会话并发且一个新运行失败 +- **THEN** 分别保留归属,新失败不被旧 PASS 清除 + +#### Scenario: EG-CGH-003 preserve supported behavior + +- **WHEN** 完全相同的重复事件到达 +- **THEN** 复用同一次受管运行,不重复申请相同范围授权 + +### Requirement: EG-CGH-004 Nonmisleading feedback + +系统 SHALL 反馈必须区分执行、规则结论、覆盖和信任;保存后反馈不声称 Git 放行;报告内容只作为数据,公开摘要脱敏,违规与 incomplete 同时保留。 + +#### Scenario: EG-CGH-004 reject unsupported or unsafe input + +- **WHEN** 报告含指令、秘密或超时前违规 +- **THEN** 不执行报告内容,保留违规与限制,公开内容脱敏 + +#### Scenario: EG-CGH-004 preserve supported behavior + +- **WHEN** 有效检查无违规但范围局部 +- **THEN** 仅报告局部结果,不称全面通过 + +### Requirement: EG-CGH-005 Host and distribution gates + +系统 SHALL 安装、事件适配、取消、重入、卸载和升级回滚须按宿主/平台逐格验收;候选桥与正式启用必须区分,缺格明确 unverified。 + +#### Scenario: EG-CGH-005 reject unsupported or unsafe input + +- **WHEN** 只有离线 Hook 重放通过 +- **THEN** 不声明真实宿主验收完成 + +#### Scenario: EG-CGH-005 preserve supported behavior + +- **WHEN** 某宿主真实安装及允许/拒绝/取消/回滚均完成 +- **THEN** 只登记该宿主版本和平台的证据 diff --git a/openspec/changes/integrate-engineering-guard-host/tasks.md b/openspec/changes/integrate-engineering-guard-host/tasks.md new file mode 100644 index 0000000..e6ffcd5 --- /dev/null +++ b/openspec/changes/integrate-engineering-guard-host/tasks.md @@ -0,0 +1,27 @@ +# codeguard-plugin 工程守卫集成任务 + +来源:2026-10-17 按既有总体规划重建;原引用路径在当前检出及本地可用历史中未找到。本变更是新重建的任务定义,不声称恢复了历史原文,也不声明实现或用户验收已完成。依据为 [traceability](https://github.com/full-stack-plugins/codeguard/blob/8d7c0e997bf21b3306573b3be89ff8c312c9de38/docs/engineering-guard/traceability.md)、[acceptance](https://github.com/full-stack-plugins/codeguard/blob/8d7c0e997bf21b3306573b3be89ff8c312c9de38/docs/engineering-guard/acceptance.md)、[protocol](https://github.com/full-stack-plugins/codeguard/blob/8d7c0e997bf21b3306573b3be89ff8c312c9de38/docs/engineering-guard/protocol.md) 与本仓现有维护约束。 + +状态:全部待实施。完成状态只在本账本维护;实现与所需真实环境验证齐备后才能勾选。每行列出的依赖是前置验收义务,不把文档创建当成前置实现完成。 + +## 1. 实施与验证 + +- [ ] 1.1 **EG-H01** [owner=codeguard-plugin] 固定现有 Rust/Python 入口、发布制品及协议身份和候选/正式资格;缺核心不弱化回退。 依赖:EG-K01。规格:EG-CGH-001。 + +- [ ] 1.2 **EG-H02** [owner=codeguard-plugin] 为发现、保存、提交、推送等事件建立 session/worktree/task/action/subject 映射;真实暂存与HEAD差异反例验证。 依赖:EG-H01, EG-K04。规格:EG-CGH-002, EG-CGH-004。 + +- [ ] 1.3 **EG-H03** [owner=codeguard-plugin] 接公共单飞、运行序列与取消结果;双会话/双工作树/迟到PASS反例保持隔离,不扩用户授权。 依赖:EG-H02, EG-K05, EG-K06。规格:EG-CGH-003。 + +- [ ] 1.4 **EG-H04** [owner=codeguard-plugin] 建立可独立验收的宿主反馈与必需检查桥;缺策略/缺结果/超时不能放行,CodeGuard原始结果映射保持诚实。 依赖:EG-H01, EG-H02, EG-K07。规格:EG-CGH-001, EG-CGH-004。 + +- [ ] 1.5 **EG-H05** [owner=codeguard-plugin] 对诊断和恢复建议做数据化呈现、输出预算及脱敏;恶意报告不能执行,PostTool反馈不冒充操作授权。 依赖:EG-H04。规格:EG-CGH-004。 + +- [ ] 1.6 **EG-H06** [owner=codeguard-plugin] 接 CLI/MCP/Hook 同请求及动作候选复核,授权由控制端独立核验;重放不同候选必须拒绝。 依赖:EG-H02, EG-K10, EG-K18。规格:EG-CGH-001, EG-CGH-002。 + +- [ ] 1.7 **EG-H07** [owner=codeguard-plugin] 完成宿主能力和事件兼容矩阵的独立真实试验;记录支持/未知平台,不以K19整体完成作为试验前置。 依赖:EG-H04, EG-K18。规格:EG-CGH-005。 + +- [ ] 1.8 **EG-H08** [owner=codeguard-plugin] 完成宿主桥与CLI/MCP N/N-1兼容和未知major负例,形成固定制品组合,供总体发布使用。 依赖:EG-H07。规格:EG-CGH-001, EG-CGH-005。 + +- [ ] 1.9 **EG-H09** [owner=codeguard-plugin] 验证会话授权复用、取消/重入/恢复与损坏回执;既有授权范围不变不重复问,最新失败不能恢复旧通过。 依赖:EG-H03, EG-H05, EG-K09。规格:EG-CGH-003, EG-CGH-004。 + +- [ ] 1.10 **EG-H10** [owner=codeguard-plugin] 执行真实宿主全链路及升级/卸载/回滚、受管技能来源验证与市场版本同步;只有证据齐备才启用候选桥。 依赖:EG-H06, EG-H08, EG-H09, EG-K20。规格:EG-CGH-001, EG-CGH-005。 diff --git a/plugin.json b/plugin.json index ef73baa..74ed11f 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "codeguard", - "version": "0.20.0", + "version": "0.20.1", "description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.", "author": { "name": "Full Stack Skills / PartMe.AI", diff --git a/scripts/bump-plugin.mjs b/scripts/bump-plugin.mjs index 994524e..9bb4e6a 100644 --- a/scripts/bump-plugin.mjs +++ b/scripts/bump-plugin.mjs @@ -11,8 +11,8 @@ * * 做的事: * 1. catalog.json 该插件 version bump - * 2. 插件仓 4 个 manifest 同步(.zcode-plugin / kimi.plugin.json / - * .agents/plugins/marketplace.json 精确改;.codex-plugin 带当日 +codex.日期 后缀) + * 2. 插件现有清单与可选[project].version同步;Claude plugin/marketplace分别处理, + * .codex-plugin 带当日 +codex.日期 后缀,所有漂移在写入前拒绝 * 3. 重跑 sync-marketplaces.mjs --write 重新生成三平台清单并校验 * 4. 打印两仓待提交提示 */ @@ -72,17 +72,20 @@ function resolvePluginsRoot() { } -if (!pluginId || !["major", "minor", "patch"].includes(level)) { +if (!pluginId || !["major", "minor", "patch"].includes(level) + || process.argv.slice(2).length > 3 || (dryRunFlag !== undefined && !dryRun)) { console.error("用法: node scripts/bump-plugin.mjs [--dry-run]"); process.exit(1); } const bump = (v) => { - const [major, minor, patch] = v.split(".").map(Number); - if ([major, minor, patch].some(Number.isNaN)) throw new Error(`无法解析版本号: ${v}`); - if (level === "major") return `${major + 1}.0.0`; - if (level === "minor") return `${major}.${minor + 1}.0`; - return `${major}.${minor}.${patch + 1}`; + if (typeof v !== "string" || !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(v)) throw new Error(`无法解析版本号: ${v}`); + const parts = v.split(".").map(Number); + const index = {major: 0, minor: 1, patch: 2}[level]; + if (parts.some(n => !Number.isSafeInteger(n)) || !Number.isSafeInteger(parts[index] + 1)) throw new Error(`版本号超出安全范围: ${v}`); + parts[index] += 1; + for (let i = index + 1; i < parts.length; i++) parts[i] = 0; + return parts.join("."); }; const catalogPath = path.join(root, "catalog.json"); @@ -104,100 +107,151 @@ const today = new Intl.DateTimeFormat("en-CA", { }).format(new Date()).replaceAll("-", ""); const repoDir = path.join(workspace, plugin.localDirectory); -const edits = [{ file: catalogPath, description: `${pluginId}: ${oldVersion} -> ${newVersion}` }]; -const plainManifestRels = [ - ".zcode-plugin/plugin.json", - "kimi.plugin.json", - // Claude 宿主市场清单(plugins[0].version 单点,bumpPlain 首个 version 匹配即覆盖) - ".claude-plugin/marketplace.json", -]; -if (fs.existsSync(path.join(repoDir, "plugin.json"))) plainManifestRels.push("plugin.json"); -for (const rel of plainManifestRels) { - edits.push({ file: path.join(repoDir, rel), description: `${rel}: ${oldVersion} -> ${newVersion}` }); +// 所有目标先解析和核对,再写入任何文件;dry-run也必须拒绝漂移。 +const writes = new Map(); +const checks = []; +const edits = []; +const format = value => `${JSON.stringify(value, null, 2)}\n`; +const parse = file => JSON.parse(fs.readFileSync(file, "utf8")); +function expectVersion(actual, expected, label) { + if (actual !== expected) throw new Error(`${label}: 版本 ${actual ?? "(未找到)"} 与计划 ${expected} 漂移;拒绝写入,请先对齐已发布版本`); } -edits.push({ - file: path.join(repoDir, ".agents/plugins/marketplace.json"), - description: `.agents/plugins/marketplace.json: ${oldVersion} -> ${newVersion} + release URLs`, -}); -// codex manifest 允许 +codex. 后缀(sync 校验认可的形状) -edits.push({ - file: path.join(repoDir, ".codex-plugin/plugin.json"), - description: `.codex-plugin/plugin.json: -> ${newVersion}+codex.${today}`, -}); - -console.log(`发版计划: ${pluginId} ${oldVersion} -> ${newVersion}${dryRun ? "(dry-run,不写文件)" : ""}`); -for (const e of edits) console.log(` - ${path.relative(workspace, e.file)} ${e.description}`); - -if (dryRun) process.exit(0); - -// 1) catalog 精确改插件 version(保持文件其余部分不动) -let catalogText = fs.readFileSync(catalogPath, "utf8"); -const catalogNeedle = `"id": "${pluginId}"`; -const idx = catalogText.indexOf(catalogNeedle); -if (idx === -1) throw new Error(`catalog.json 中找不到 ${pluginId}`); -const segStart = catalogText.indexOf('"version": "', idx); -const segEnd = catalogText.indexOf('"', segStart + 12); -catalogText = catalogText.slice(0, segStart) + `"version": "${newVersion}"` + catalogText.slice(segEnd + 1); -fs.writeFileSync(catalogPath, catalogText); - -// 2) 各仓 manifest -const bumpPlain = (text, rel) => { - const updated = text.replace(`"version": "${oldVersion}"`, `"version": "${newVersion}"`); - if (updated === text) { - const actual = /"version": "([^"]+)"/.exec(text)?.[1] ?? "(未找到)"; - throw new Error(`${rel}: manifest 版本 ${actual} 与 catalog ${oldVersion} 漂移;拒绝静默跳过,请先对齐版本再发版`); +function planJson(rel, readVersion, update, expected = oldVersion, target = newVersion) { + const file = path.join(repoDir, rel); + const value = parse(file); + expectVersion(readVersion(value), expected, rel); + update(value); + expectVersion(readVersion(value), target, rel); + writes.set(file, format(value)); + checks.push(() => expectVersion(readVersion(parse(file)), target, `${rel} 写后回读`)); + edits.push({file, description: `${rel}: ${expected} -> ${target}`}); +} +const onePlugin = (value, label) => { + if (!Array.isArray(value.plugins) || value.plugins.length !== 1 || !value.plugins[0] || typeof value.plugins[0] !== "object") { + throw new Error(`${label}: repository marketplace must contain exactly one plugin`); } - return updated; + return value.plugins[0]; }; -const bumpCodex = (text) => text.replace(/"version": "\d+\.\d+\.\d+(?:\+codex\.\d+)?"/, `"version": "${newVersion}+codex.${today}"`); +const plainManifestRels = [".zcode-plugin/plugin.json", "kimi.plugin.json"]; +for (const optional of ["plugin.json", ".claude-plugin/plugin.json"]) { + if (fs.existsSync(path.join(repoDir, optional))) plainManifestRels.push(optional); +} +for (const rel of plainManifestRels) planJson(rel, v => v.version, v => {v.version = newVersion;}); -for (const rel of plainManifestRels) { - const manifest = path.join(repoDir, rel); - fs.writeFileSync(manifest, bumpPlain(fs.readFileSync(manifest, "utf8"), rel)); +const claudeMarketplace = ".claude-plugin/marketplace.json"; +if (fs.existsSync(path.join(repoDir, claudeMarketplace))) { + planJson(claudeMarketplace, v => onePlugin(v, claudeMarketplace).version, + v => {onePlugin(v, claudeMarketplace).version = newVersion;}); } -const repositoryMarketplace = path.join(repoDir, ".agents/plugins/marketplace.json"); -const marketplace = JSON.parse(fs.readFileSync(repositoryMarketplace, "utf8")); -if (!Array.isArray(marketplace.plugins) || marketplace.plugins.length !== 1) { - throw new Error(`${pluginId}: repository marketplace must contain exactly one plugin`); +const agents = ".agents/plugins/marketplace.json"; +planJson(agents, v => onePlugin(v, agents).version, v => { + const entry = onePlugin(v, agents); + if (!entry.source || typeof entry.source !== "object" || !entry.interface || typeof entry.interface !== "object") { + throw new Error(`${agents}: source/interface object required`); + } + entry.version = newVersion; + entry.description = plugin.description; + entry.interface.shortDescription = plugin.shortDescription; + entry.source.ref = `v${newVersion}`; + entry.icon = `https://cdn.jsdelivr.net/gh/${plugin.repository}@v${newVersion}/${plugin.logo}`; + entry.interface.logo = entry.icon; +}); +const codex = ".codex-plugin/plugin.json"; +const codexOld = parse(path.join(repoDir, codex)).version; +const codexPrefix = `${oldVersion}+codex.`; +if (typeof codexOld !== "string" || !(codexOld === oldVersion || (codexOld.startsWith(codexPrefix) && /^\d{8}$/.test(codexOld.slice(codexPrefix.length))))) { + throw new Error(`${codex}: 版本 ${codexOld} 与 catalog ${oldVersion} 漂移;拒绝写入`); } -const marketplacePlugin = marketplace.plugins[0]; -const releaseRef = `v${newVersion}`; -const logoUrl = `https://cdn.jsdelivr.net/gh/${plugin.repository}@${releaseRef}/${plugin.logo}`; -marketplacePlugin.version = newVersion; -marketplacePlugin.description = plugin.description; -marketplacePlugin.interface.shortDescription = plugin.shortDescription; -marketplacePlugin.source.ref = releaseRef; -marketplacePlugin.icon = logoUrl; -marketplacePlugin.interface.logo = logoUrl; -fs.writeFileSync(repositoryMarketplace, `${JSON.stringify(marketplace, null, 2)}\n`); -const codexManifest = path.join(repoDir, ".codex-plugin/plugin.json"); -fs.writeFileSync(codexManifest, bumpCodex(fs.readFileSync(codexManifest, "utf8"))); +planJson(codex, v => v.version, v => {v.version = `${newVersion}+codex.${today}`;}, codexOld, `${newVersion}+codex.${today}`); -// 写后回读:五文件版本必须与计划一致——写后不验等于没写(防半程假成功) -for (const rel of plainManifestRels) { - const data = JSON.parse(fs.readFileSync(path.join(repoDir, rel), "utf8")); - // 版本路径按清单形态分流:Claude 市场清单的版本在 plugins[0].version - // (bumpPlain 的文本替换同样只改那一处),其余清单在顶层 version。 - const readback = rel.startsWith(".claude-plugin/") - ? data.plugins?.[0]?.version - : data.version; - if (readback !== newVersion) throw new Error(`${rel}: 写后回读 ${readback} != ${newVersion}`); +// 只编辑[project]的唯一静态version字符串;保留其他表、引号、注释和换行。 +// 扫描字符串状态,不能将多行description里的伪[project]/version当成元数据。 +function pythonProjectVersion(text) { + let offset = 0, project = false, seen = false, seenTable = false, quote = null, depth = 0; + const versions = []; + for (const line of text.match(/[^\n]*\n|[^\n]+$/g) ?? []) { + const eligible = quote === null && depth === 0; + if (eligible && /^\s*\[/.test(line)) { + seenTable = true; + project = /^\s*\[\s*(?:project|"project"|'project')\s*\]\s*(?:#.*)?(?:\r?\n)?$/.test(line); + if (project && seen) throw new Error("pyproject.toml: duplicate [project] table"); + seen ||= project; + } else if (eligible && project && /^\s*(?:version|"version"|'version')\s*=/.test(line)) { + const match = /^(\s*(?:version|"version"|'version')\s*=\s*)(["'])([^"'\r\n]*)\2\s*(?:#.*)?(?:\r?\n)?$/.exec(line); + if (!match) throw new Error("pyproject.toml: one static project version string required"); + versions.push({value: match[3], start: offset + match[1].length + 1}); + } else if (eligible && !seenTable && /^\s*(?:project\.|project\s*=|"project"\s*=|'project'\s*=)/.test(line)) { + throw new Error("pyproject.toml: unsupported project table form; explicit [project] version required"); + } + for (let i = 0; i < line.length; i++) { + if (quote !== null) { + if (quote[0] === '"' && line.charCodeAt(i) === 92) { i++; continue; } + if (line.startsWith(quote, i)) { i += quote.length - 1; quote = null; } + } else { + if (line[i] === "#") break; + if (line[i] === '"' || line[i] === "'") { + const triple = line[i].repeat(3); + quote = line.startsWith(triple, i) ? triple : line[i]; + i += quote.length - 1; + } else if (line[i] === "[" || line[i] === "{") depth++; + else if (line[i] === "]" || line[i] === "}") depth--; + } + } + if (quote !== null && quote.length === 1) throw new Error("pyproject.toml: malformed single-line string"); + offset += line.length; + } + if (quote !== null || depth !== 0) throw new Error("pyproject.toml: unclosed string or collection"); + if (seen && versions.length !== 1) throw new Error("pyproject.toml: exactly one static [project].version required"); + return seen ? versions[0] : null; } -const codexReadback = fs.readFileSync(codexManifest, "utf8"); -if (!codexReadback.includes(`"version": "${newVersion}+codex.${today}"`)) { - throw new Error(`.codex-plugin/plugin.json: 写后回读版本不符`); +const pyproject = path.join(repoDir, "pyproject.toml"); +if (fs.existsSync(pyproject)) { + const original = fs.readFileSync(pyproject, "utf8"); + const entry = pythonProjectVersion(original); + if (entry) { + expectVersion(entry.value, oldVersion, "pyproject.toml [project].version"); + writes.set(pyproject, original.slice(0, entry.start) + newVersion + original.slice(entry.start + entry.value.length)); + checks.push(() => expectVersion(pythonProjectVersion(fs.readFileSync(pyproject, "utf8"))?.value, newVersion, "pyproject.toml 写后回读")); + edits.push({file: pyproject, description: `[project].version: ${oldVersion} -> ${newVersion}`}); + } } -if (!catalogText.includes(`"version": "${newVersion}"`)) { - throw new Error(`catalog.json: 写后回读版本不符`); + +// 保留现有catalog文本布局,同时核对改动恰好是目标插件version,避免误改邻项。 +let catalogText = fs.readFileSync(catalogPath, "utf8"); +const needle = `"id": "${pluginId}"`; +const index = catalogText.indexOf(needle); +if (index === -1) throw new Error(`catalog.json 中找不到 ${pluginId}`); +const begin = catalogText.indexOf('"version": "', index); +const end = catalogText.indexOf('"', begin + 12); +if (begin === -1 || end === -1) throw new Error("catalog.json: version token unavailable"); +catalogText = catalogText.slice(0, begin) + `"version": "${newVersion}"` + catalogText.slice(end + 1); +const expectedCatalog = structuredClone(catalog); +expectedCatalog.plugins.find(p => p.id === pluginId).version = newVersion; +if (JSON.stringify(JSON.parse(catalogText)) !== JSON.stringify(expectedCatalog)) throw new Error("catalog.json: targeted version edit ambiguous; refusing mutation"); +writes.set(catalogPath, catalogText); +checks.push(() => expectVersion(parse(catalogPath).plugins.find(p => p.id === pluginId)?.version, newVersion, "catalog.json 写后回读")); +edits.unshift({file: catalogPath, description: `${pluginId}: ${oldVersion} -> ${newVersion}`}); + +const bumpReadme = text => { + let next = text.split(`v${oldVersion}`).join(`v${newVersion}`); + next = next.split(oldVersion).join(newVersion); + return next.replace(/(\d+\.\d+\.\d+)\+codex\.\d+/g, + (match, version) => version === newVersion ? `${newVersion}+codex.${today}` : match); +}; +for (const name of ["README.md", "README.zh-CN.md"]) { + const file = path.join(repoDir, name); + if (fs.existsSync(file)) writes.set(file, bumpReadme(fs.readFileSync(file, "utf8"))); } +console.log(`发版计划: ${pluginId} ${oldVersion} -> ${newVersion}${dryRun ? "(dry-run,不写文件)" : ""}`); +for (const edit of edits) console.log(` - ${path.relative(workspace, edit.file)} ${edit.description}`); +if (dryRun) process.exit(0); +for (const [file, text] of writes) fs.writeFileSync(file, text); +for (const check of checks) check(); -// 3) 重新生成三平台清单 + 全量校验 const pluginFilter = `--plugin=${pluginId}`; -execFileSync(process.execPath, [path.join(root, "scripts/sync-marketplaces.mjs"), "--write", pluginFilter], { stdio: "inherit" }); -execFileSync(process.execPath, [path.join(root, "scripts/sync-marketplaces.mjs"), pluginFilter], { stdio: "inherit" }); - -// 4) 提交提示 +execFileSync(process.execPath, [path.join(root, "scripts/sync-marketplaces.mjs"), "--write", pluginFilter], {stdio: "inherit"}); +execFileSync(process.execPath, [path.join(root, "scripts/sync-marketplaces.mjs"), pluginFilter], {stdio: "inherit"}); console.log(` ✅ ${pluginId} ${newVersion} 本地元数据已准备;尚未提交、推送或发布。剩余步骤: cd ${root} && git add -A && git commit -m "release: ${pluginId} ${newVersion}" && git push diff --git a/scripts/vendor/skill_vendor.py b/scripts/vendor/skill_vendor.py index b434174..d143152 100755 --- a/scripts/vendor/skill_vendor.py +++ b/scripts/vendor/skill_vendor.py @@ -38,9 +38,12 @@ def fail(message: str) -> None: def hash_skill_dir(skill_dir: Path) -> str: - """Return a deterministic digest over every file in one skill.""" + """Hash raw file bytes in case-sensitive POSIX relative-path order.""" digest = hashlib.sha256() - for path in sorted(p for p in skill_dir.rglob("*") if p.is_file()): + for path in sorted( + (p for p in skill_dir.rglob("*") if p.is_file()), + key=lambda p: p.relative_to(skill_dir).as_posix(), + ): relative = path.relative_to(skill_dir).as_posix() digest.update(relative.encode("utf-8")) digest.update(b"\0") @@ -81,7 +84,10 @@ def fetch_checkout(repo: str, ref: str, workdir: Path) -> Path: check=True, ) subprocess.run( - ["git", "-C", str(checkout), "checkout", "--quiet", "--detach", "FETCH_HEAD"], + [ + "git", "-c", "core.autocrlf=false", "-c", "core.eol=lf", + "-C", str(checkout), "checkout", "--quiet", "--detach", "FETCH_HEAD", + ], check=True, ) head = subprocess.run( diff --git a/tests/test_skill_vendor_portability.py b/tests/test_skill_vendor_portability.py new file mode 100644 index 0000000..cad0d4b --- /dev/null +++ b/tests/test_skill_vendor_portability.py @@ -0,0 +1,105 @@ +"""Cross-platform invariants for raw-byte, locked skill snapshots.""" +from __future__ import annotations + +import hashlib +import importlib.util +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location("vendor_portability", ROOT / "scripts/vendor/skill_vendor.py") +assert SPEC and SPEC.loader +VENDOR = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(VENDOR) + + +class SkillVendorPortabilityTest(unittest.TestCase): + def test_digest_uses_case_sensitive_posix_relative_string_order(self): + # Explicit order is identical on NTFS and POSIX, including file/directory prefixes. + ordered = ["A-long.md", "B.md", "a.md", "q-early.md", "q/nested.md", "Å.md"] + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + expected = hashlib.sha256() + for name in reversed(ordered): + path = root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes((name + "\n").encode("utf-8")) + for name in ordered: + expected.update(name.encode("utf-8") + b"\0") + expected.update(hashlib.sha256((name + "\n").encode("utf-8")).hexdigest().encode("ascii") + b"\n") + self.assertEqual(VENDOR.hash_skill_dir(root), expected.hexdigest()) + + def test_hash_keeps_crlf_distinct_from_locked_lf_bytes(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + path = root / "SKILL.md" + path.write_bytes(b"line one\nline two\n") + lf = VENDOR.hash_skill_dir(root) + path.write_bytes(b"line one\r\nline two\r\n") + self.assertNotEqual(VENDOR.hash_skill_dir(root), lf) + + def test_hash_keeps_binary_bytes_exact(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + value = b"\0binary\r\n\xff" + (root / "data.bin").write_bytes(value) + expected = hashlib.sha256(b"data.bin\0" + hashlib.sha256(value).hexdigest().encode("ascii") + b"\n").hexdigest() + self.assertEqual(VENDOR.hash_skill_dir(root), expected) + + +class SkillVendorCheckoutTest(unittest.TestCase): + def test_upstream_checkout_is_lf_despite_inherited_autocrlf(self): + import os + import subprocess + import sys + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + upstream = root / "upstream" + upstream.mkdir() + text = b"controlled upstream\nsecond line\n" + binary = b"\0binary\r\n\xff" + (upstream / "SKILL.md").write_bytes(text) + (upstream / "data.bin").write_bytes(binary) + def git(*arguments): + result = subprocess.run(["git", "-c", "core.autocrlf=false", "-C", str(upstream), *arguments], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + git("init", "--quiet") + git("add", "SKILL.md", "data.bin") + git("-c", "user.name=Vendor Test", "-c", "user.email=vendor@example.invalid", "commit", "--quiet", "-m", "fixture") + git("tag", "v1.0.0") + config = root / "user.gitconfig" + config_bytes = b"[core]\n\tautocrlf = true\n\teol = crlf\n" + config.write_bytes(config_bytes) + environment = {**os.environ, "GIT_CONFIG_GLOBAL": str(config), "GIT_CONFIG_COUNT": "1", "GIT_CONFIG_KEY_0": "core.autocrlf", "GIT_CONFIG_VALUE_0": "true"} + runner = ("import importlib.util,sys;from pathlib import Path;" + "s=importlib.util.spec_from_file_location('vendor',sys.argv[1]);" + "m=importlib.util.module_from_spec(s);s.loader.exec_module(m);" + "m.fetch_checkout(sys.argv[2],'v1.0.0',Path(sys.argv[3]))") + result = subprocess.run([sys.executable, "-B", "-c", runner, str(ROOT / "scripts/vendor/skill_vendor.py"), str(upstream), str(root / "fetch")], env=environment, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual((root / "fetch/checkout/SKILL.md").read_bytes(), text) + self.assertEqual((root / "fetch/checkout/data.bin").read_bytes(), binary) + self.assertEqual(config.read_bytes(), config_bytes) + + def test_skill_attributes_preserve_lf_text_and_binary_bytes(self): + import subprocess + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + repo = root / "repo" + repo.mkdir() + (repo / ".gitattributes").write_bytes((ROOT / ".gitattributes").read_bytes()) + (repo / "skills/example").mkdir(parents=True) + text = b"controlled skill\nsecond line\n" + binary = b"\0binary\r\n\xff" + (repo / "skills/example/SKILL.md").write_bytes(text) + (repo / "skills/example/data.bin").write_bytes(binary) + for arguments in [["init", "--quiet"], ["add", ".gitattributes", "skills"], ["checkout-index", "--all", "--prefix=" + (root / "export").as_posix() + "/"]]: + result = subprocess.run(["git", "-c", "core.autocrlf=true", "-C", str(repo), *arguments], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual((root / "export/skills/example/SKILL.md").read_bytes(), text) + self.assertEqual((root / "export/skills/example/data.bin").read_bytes(), binary) + + +if __name__ == "__main__": + unittest.main() From efdbd2ba6487a7e09e3e01b547be196013f413ed Mon Sep 17 00:00:00 2001 From: loong10k <20489781+loong10k@users.noreply.github.com> Date: Sat, 17 Oct 2026 11:30:48 +0800 Subject: [PATCH 5/5] ci: verify portable vendor bytes on Windows and satisfy subprocess lint --- .github/workflows/skills-check.yml | 26 ++++++++++++++++++++++++++ tests/test_skill_vendor_portability.py | 6 +++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/.github/workflows/skills-check.yml b/.github/workflows/skills-check.yml index 25089ed..a07fc24 100644 --- a/.github/workflows/skills-check.yml +++ b/.github/workflows/skills-check.yml @@ -77,3 +77,29 @@ jobs: print("Edit codeguard-skills, release a new tag, then run scripts/vendor/skill_vendor.py update.") sys.exit(1) PY + + vendor-portability-windows: + runs-on: windows-latest + permissions: + contents: read + env: + PYTHONUTF8: "1" + PYTHONDONTWRITEBYTECODE: "1" + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 + with: + python-version: '3.13' + - name: Prepare isolated test temporary directory + shell: pwsh + run: | + $taskTmp = Join-Path $env:RUNNER_TEMP 'vendor-portability/tmp' + New-Item -ItemType Directory -Force -Path $taskTmp | Out-Null + "TEMP=$taskTmp" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "TMP=$taskTmp" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + - name: Verify Windows path ordering and LF checkout behavior + run: python -B -m unittest discover -s tests -p test_skill_vendor_portability.py -v + - name: Verify checkout bytes against locked skill digests + run: python -B scripts/vendor/skill_vendor.py check --offline + - name: Verify upstream pins and bytes with ordinary Git configuration + run: python -B scripts/vendor/skill_vendor.py check diff --git a/tests/test_skill_vendor_portability.py b/tests/test_skill_vendor_portability.py index cad0d4b..e8c1e5d 100644 --- a/tests/test_skill_vendor_portability.py +++ b/tests/test_skill_vendor_portability.py @@ -62,7 +62,7 @@ def test_upstream_checkout_is_lf_despite_inherited_autocrlf(self): (upstream / "SKILL.md").write_bytes(text) (upstream / "data.bin").write_bytes(binary) def git(*arguments): - result = subprocess.run(["git", "-c", "core.autocrlf=false", "-C", str(upstream), *arguments], capture_output=True, text=True) + result = subprocess.run(["git", "-c", "core.autocrlf=false", "-C", str(upstream), *arguments], capture_output=True, text=True, check=False) self.assertEqual(result.returncode, 0, result.stderr) git("init", "--quiet") git("add", "SKILL.md", "data.bin") @@ -76,7 +76,7 @@ def git(*arguments): "s=importlib.util.spec_from_file_location('vendor',sys.argv[1]);" "m=importlib.util.module_from_spec(s);s.loader.exec_module(m);" "m.fetch_checkout(sys.argv[2],'v1.0.0',Path(sys.argv[3]))") - result = subprocess.run([sys.executable, "-B", "-c", runner, str(ROOT / "scripts/vendor/skill_vendor.py"), str(upstream), str(root / "fetch")], env=environment, capture_output=True, text=True) + result = subprocess.run([sys.executable, "-B", "-c", runner, str(ROOT / "scripts/vendor/skill_vendor.py"), str(upstream), str(root / "fetch")], env=environment, capture_output=True, text=True, check=False) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assertEqual((root / "fetch/checkout/SKILL.md").read_bytes(), text) self.assertEqual((root / "fetch/checkout/data.bin").read_bytes(), binary) @@ -95,7 +95,7 @@ def test_skill_attributes_preserve_lf_text_and_binary_bytes(self): (repo / "skills/example/SKILL.md").write_bytes(text) (repo / "skills/example/data.bin").write_bytes(binary) for arguments in [["init", "--quiet"], ["add", ".gitattributes", "skills"], ["checkout-index", "--all", "--prefix=" + (root / "export").as_posix() + "/"]]: - result = subprocess.run(["git", "-c", "core.autocrlf=true", "-C", str(repo), *arguments], capture_output=True, text=True) + result = subprocess.run(["git", "-c", "core.autocrlf=true", "-C", str(repo), *arguments], capture_output=True, text=True, check=False) self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual((root / "export/skills/example/SKILL.md").read_bytes(), text) self.assertEqual((root / "export/skills/example/data.bin").read_bytes(), binary)