diff --git a/hooks/__protocol__.md b/hooks/__protocol__.md new file mode 100644 index 0000000..4d22ee0 --- /dev/null +++ b/hooks/__protocol__.md @@ -0,0 +1,111 @@ +# hooks/ 与宿主 CLI 之间的契约(canonical cheat-sheet) + +> 这份文档是 codeguard-plugin 与 Codex CLI / ZCode / Kimi Code 三端宿主 +> 交互契约的**单源事实**。所有 hook 脚本(`hooks/*.py`)的实现都必须与本文件 +> 一致;若未来调整任何退出码或 JSON schema,**同一 commit 内**必须更新本文件。 +> +> 对应的 OpenSpec 规范是 `hook-protocol`(见 `openspec/specs/hook-protocol/spec.md`)。 + +--- + +## 1. 协议总表 + +| 事件 | Hook 脚本 | stdout | stderr | exit | 阻断? | +|---|---|---|---|---|---| +| SessionStart | `env_check.py` | 人类可读一行摘要 `codeguard 插件环境:...` | 仅内部错误时 | 0 | 否 | +| UserPromptSubmit | `user_prompt_validator.py` | JSON `{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"..."}}` | 仅内部错误时 | 0 | 否 | +| PreToolUse `Bash` | `pre_tool_git_guard.py` | 通过:空 | 通过:空;失败:完整修复指令 | 0(通过)/ 2(拦截) | 通过:否;失败:是 | +| PostToolUse `Write\|Edit\|MultiEdit` | `post_tool_lint.py` | JSON `{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"..."}, "systemMessage":"..."}` | 仅内部错误时 | 0 | 否 | +| Stop | `stop_summary.py` | 人类可读会话摘要 | 仅内部错误时 | 0 | 否 | + +实现定位(行号随 v0.6.2 锁定,下一次重构时核对): +- SessionStart 摘要:`hooks/env_check.py:34`。 +- UserPromptSubmit JSON:`hooks/user_prompt_validator.py:104-109` 与 `:117-125`。 +- PreToolUse stderr:`hooks/pre_tool_git_guard.py:159`。 +- PostToolUse JSON:`hooks/post_tool_lint.py:226-232`(通过)、`:253-259`(自动修复后通过)、`:275-281`(失败 additionalContext)。 +- Stop 摘要:`hooks/stop_summary.py:46`。 + +--- + +## 2. 三端兼容矩阵 + +| 宿主 | SessionStart | UserPromptSubmit | PreToolUse Bash | PostToolUse Write\|Edit\|MultiEdit | Stop | +|---|---|---|---|---|---| +| Codex CLI | stdout 注入 developer context | stdout JSON 注入 additionalContext | exit 2 走工具结果回给 AI;exit 0 静默 | stdout JSON 注入 additionalContext + systemMessage | stdout 摘要 | +| ZCode | 同 Codex | 同 Codex,additionalContext 进 systemMessage | 同 Codex | 同 Codex | 同 Codex | +| Kimi Code CLI | stdout 摘要注入会话上下文 | stdout JSON 注入上下文(观察型,**不阻断用户消息**) | exit 2 拦截 + stderr 指令;exit 0 静默 | stdout JSON 注入上下文 | stdout 摘要 | + +所有三端在所有事件上,**内部错误都走 fail-open**(见 §3),hook bug 永远不阻断工作流。 + +--- + +## 3. Fail-open 约定(强制) + +任何 hook 脚本在 `__main__` 处必须有: + +```python +if __name__ == "__main__": + try: + sys.exit(main()) + except SystemExit: + raise + except Exception as exc: + print(f"[codeguard] 内部错误已忽略(fail-open): {exc!r}", file=sys.stderr) + sys.exit(0) +``` + +含义: +- 钩子内部任何未捕获异常 → stderr 打印 traceback 摘要 → **exit 0**。 +- AI 与用户上下文永远收到「钩子无意见」的语义。 +- 同一约定适用于:`env_check.py:127-131`、`post_tool_lint.py:293-298`、`pre_tool_git_guard.py:166-170`、`user_prompt_validator.py:137-141`、`stop_summary.py:55-62`。 + +**禁止**把 fail-open 改为 exit 2——会破坏宿主工作流。 + +--- + +## 4. PreToolUse 硬拦截(exit 2)的语义边界 + +`pre_tool_git_guard.py` 是**唯一**会 exit 2 的 hook——且仅当: +1. 入参命令包含 `git commit` 或 `git push`(`is_guarded()` 命中);并且 +2. 仓库级 `git config codeguard.skipGate true` 未设置;并且 +3. 实际跑 linter 后存在非 skipped 的 failures。 + +出口内容(`pre_tool_git_guard.py:158-163`):stderr 报告块**首行必须是 `codeguard ❌ 提交门禁未通过:` 综述**,其后跟具体问题列表;这是 `tests/run_all.py:142-144` 守护的契约。 + +**禁止**在 lint skipped(工具未装/项目未接入)时 exit 2——这是「无法验证」而非「验证失败」。 + +--- + +## 5. 新增 hook checklist + +新加一个 hook 时,按顺序自检: + +1. **读本 cheat-sheet** + `openspec/specs/hook-protocol/spec.md`。 +2. 在 `hooks/hooks.json` 注册(事件 / matcher / command / timeout)。 +3. 脚本实现遵守 §1 协议总表的 exit 码与 stdout 形态。 +4. 脚本 `__main__` 加 §3 的 fail-open 套子。 +5. 在 `tests/run_all.py` 加子集触发:stdin 喂真实宿主协议 JSON,断言 exit 码与 stdout 形状。 +6. 在本文件 §1 协议总表加一行,标注实现位置(`hooks/.py:line`)。 +7. OpenSpec change:proposal / design / `hook-protocol/spec.md` 加 Scenario / tasks 勾选后 archive。 + +--- + +## 6. 协议变更策略 + +任何对 §1 协议总表字段的修改: + +- 同步改本文件 + 对应 `hooks/*.py` + 对应测试。 +- 走 OpenSpec change(`hook-protocol` spec 下新增 MODIFIED Requirements + Scenarios)。 +- 不允许「先改代码后补文档」的拆分 commit——契约一致是同一变更的原子单位。 + +--- + +## 7. 与 OpenSpec 的对应 + +| Cheat-sheet 节 | OpenSpec spec / Scenario | +|---|---| +| §1 协议总表 | `hook-protocol::Requirement: Hook-host contract SHALL live in hooks/__protocol__.md` | +| §3 Fail-open | `hook-protocol::Requirement: The cheat-sheet SHALL document fail-open` | +| §5 新增 hook checklist | `hook-protocol::Requirement: tests/run_all.py SHALL reference the cheat-sheet` | + +变更通过 `openspec archive add-host-protocol-cheatsheet` 入库;spec 在 `openspec/specs/hook-protocol/spec.md`。 \ No newline at end of file diff --git a/openspec/changes/archive/2026-09-22-add-host-protocol-cheatsheet/.openspec.yaml b/openspec/changes/archive/2026-09-22-add-host-protocol-cheatsheet/.openspec.yaml new file mode 100644 index 0000000..a9eca53 --- /dev/null +++ b/openspec/changes/archive/2026-09-22-add-host-protocol-cheatsheet/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-22 \ No newline at end of file diff --git a/openspec/changes/archive/2026-09-22-add-host-protocol-cheatsheet/design.md b/openspec/changes/archive/2026-09-22-add-host-protocol-cheatsheet/design.md new file mode 100644 index 0000000..5fc3047 --- /dev/null +++ b/openspec/changes/archive/2026-09-22-add-host-protocol-cheatsheet/design.md @@ -0,0 +1,39 @@ +## Context + +`grep -rn '协议\|protocol\|exit code\|JSON-RPC' hooks/*.py docs/*.md` 显示协议信息散布在 8 处(5 个 hook docstring + 2 份 docs/*.md + 1 处 `gate_lib.py:4-5` 钩子间互引)。无任何一处给「新增 hook 时」做端到端的清单。 + +实际行为契约(v0.6.2): +- SessionStart 钩子(`env_check.py`):stdout 人类可读一行摘要;exit 0;不阻断。 +- UserPromptSubmit 钩子(`user_prompt_validator.py`):stdout JSON `{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"..."}}`;exit 0(不阻断用户消息);失败时人工写明修复指令进 `additionalContext`。 +- PreToolUse Bash 钩子(`pre_tool_git_guard.py`):成功静默(exit 0、零输出);失败 exit 2 + stderr 反馈指令回给 AI。 +- PostToolUse Write|Edit|MultiEdit 钩子(`post_tool_lint.py`):stdout JSON `{"decision":"block","reason":"..."}` 或 `{"hookSpecificOutput":{...}}`;exit 0(注入上下文);失败 stderr + JSON 反馈。 +- Stop 钩子(`stop_summary.py`):stdout 人类可读;exit 0。 +- 全部 hook 内部错误 fail-open:`except Exception: print(... file=sys.stderr); sys.exit(0)`。 + +三端兼容矩阵(实测): +- Codex CLI:JSON stdout 注入 developer context;exit 2 阻断 + stderr 走工具结果回给 AI。 +- ZCode:同 Codex;`additionalContext` 进 systemMessage。 +- Kimi Code CLI:SessionStart/UserPromptSubmit 静默观察型,PostToolUse exit 0 时 stdout 注入上下文;Stop 静默。 + +## Decisions + +### 单源契约 = `hooks/__protocol__.md` + +文档分六节:协议总表(事件 × 端 × exit × 输出)/ 三端兼容矩阵 / fail-open 约定 / 新增 hook checklist / 协议变更策略 / 与 OpenSpec spec 的对应。 + +### 行为零变更 + +本文档仅复刻 v0.6.2 实际行为,不引入新行为;任何与现实现矛盾的描述都按现状修正,不重设契约。 + +### `run_all.py` 顶部 docstring 加引用 + +让 host-protocol 的发现面只有一个:从 README 到 hooks/ 时自然能看到。子集测试逻辑不变。 + +### 不锁实现细节 + +exit 码与 JSON schema 用「事实陈述」措辞("scripts/post_tool_lint.py:226 emits JSON with decision/reason fields"),便于读者回溯到代码;不抽象成「必须如何」。 + +## Risks / Trade-offs + +- **文档与实现漂移**:当前契约由 8 处分散文档描述,本身就有潜在不一致。**新文档是综合而非规范**——未来协议演化需要**先改代码 + 同步本 markdown + 提交**。在 OpenSpec spec `hook-protocol` 中作为 Requirement 加 Scenario 守卫。 +- **未来新增 hook 时漏读**:用 checklist 强制后续开发者先读本文件。 \ No newline at end of file diff --git a/openspec/changes/archive/2026-09-22-add-host-protocol-cheatsheet/proposal.md b/openspec/changes/archive/2026-09-22-add-host-protocol-cheatsheet/proposal.md new file mode 100644 index 0000000..44a2b62 --- /dev/null +++ b/openspec/changes/archive/2026-09-22-add-host-protocol-cheatsheet/proposal.md @@ -0,0 +1,26 @@ +## Why + +codeguard-plugin 的 hook 与宿主(Codex CLI / ZCode / Kimi Code)之间存在三套交互协议:SessionStart 静默启动 + 信息附加到上下文;UserPromptSubmit 软引导(exit 0 + stdout JSON additionalContext);PreToolUse 硬拦截(exit 2 + stderr 指令);PostToolUse 反馈(exit 0 + stdout JSON);Stop 静默总结。 + +这些契约目前分散在每个 hook 脚本 docstring(`post_tool_lint.py:3-7`、`pre_tool_git_guard.py:4-9`、`user_prompt_validator.py:4-5`、`gate_lib.py:4-5`)以及 `docs/partme-codeguard-plugin-Architecture.zh_CN.md` 与 `docs/5、partme-codeguard-plugin-技术方案与路线.md`。MEDIUM #7 评审已指出:未来加新 hook 时,契约无单源——容易在退出码语义(exit 0 vs 2 vs fail-open)与 JSON 协议(stdout / additionalContext / stderr 反馈)上走偏。 + +## What Changes + +- 新增 `hooks/__protocol__.md`:单源契约文档,覆盖 exit 码、JSON 输出格式、三端兼容矩阵、fail-open 约定、stale hook timeout 设定与新增 hook checklist。 +- `tests/run_all.py` 顶部 docstring 增加「host protocol cheat-sheet」引用,明确 `__protocol__.md` 为子集契约源。 +- 不修改任何 hook 脚本的实现(输出格式与退出码与 v0.6.2 完全一致);不修改 `hooks/hooks.json`。 +- 仅文档 + 引用;归档后 bump patch。 + +## Capabilities + +### New Capabilities + +- `hook-protocol`: Defines the canonical contract between codeguard-plugin hooks and host CLIs (Codex CLI / ZCode / Kimi Code). + +### Modified Capabilities + +None. + +## Impact + +新增 1 份 Markdown 文档(预计 ~120 行);修改 1 处 docstring 引用。无脚本/测试/manifest 行为变化。 \ No newline at end of file diff --git a/openspec/changes/archive/2026-09-22-add-host-protocol-cheatsheet/specs/hook-protocol/spec.md b/openspec/changes/archive/2026-09-22-add-host-protocol-cheatsheet/specs/hook-protocol/spec.md new file mode 100644 index 0000000..dfd16f7 --- /dev/null +++ b/openspec/changes/archive/2026-09-22-add-host-protocol-cheatsheet/specs/hook-protocol/spec.md @@ -0,0 +1,33 @@ +## ADDED Requirements + +### Requirement: Hook-host contract SHALL live in hooks/__protocol__.md + +The canonical description of how codeguard-plugin hooks communicate with Codex CLI, ZCode, and Kimi Code (exit codes, JSON output formats, fail-open convention, three-host compatibility) SHALL live in `hooks/__protocol__.md`. No hook script or docstring SHALL contradict it; any change to exit codes or JSON schema SHALL update the document in the same commit. + +#### Scenario: A future hook author consults the single source + +- **WHEN** an agent (human or AI) adds a new hook to `hooks/` +- **THEN** the README and `hooks/__protocol__.md` together are sufficient to design exit codes and JSON shape without further questions + +#### Scenario: A hook script's docstring contradicts the cheat-sheet + +- **WHEN** a reviewer or `git diff` notices a contradiction between a hook's docstring and `hooks/__protocol__.md` +- **THEN** the implementer updates whichever side is out of date so the document and implementation match in the merged commit + +### Requirement: tests/run_all.py SHALL reference the cheat-sheet + +The top-of-file docstring of `tests/run_all.py` SHALL contain a one-line reference to `hooks/__protocol__.md` describing its role as the canonical contract source for the host-protocol e2e suite. + +#### Scenario: A new contributor opens run_all.py to add a hook subset + +- **WHEN** the contributor reads the docstring +- **THEN** the reference to `hooks/__protocol__.md` is visible and points them at the single source for protocol questions + +### Requirement: The cheat-sheet SHALL document fail-open for uncaught exceptions + +`hooks/__protocol__.md` SHALL include a section explaining the fail-open convention: any uncaught exception inside a hook logs to stderr and exits 0, so a hook bug never blocks the host CLI workflow. This documents the existing behavior in `env_check.py`, `post_tool_lint.py`, `pre_tool_git_guard.py`, `user_prompt_validator.py`, and `stop_summary.py`. + +#### Scenario: A hook hits an unexpected exception + +- **WHEN** an uncaught `Exception` propagates to the top of a hook +- **THEN** the hook prints the traceback tail to stderr and exits 0; the host CLI receives no block signal and continues its workflow \ No newline at end of file diff --git a/openspec/changes/archive/2026-09-22-add-host-protocol-cheatsheet/tasks.md b/openspec/changes/archive/2026-09-22-add-host-protocol-cheatsheet/tasks.md new file mode 100644 index 0000000..9e62454 --- /dev/null +++ b/openspec/changes/archive/2026-09-22-add-host-protocol-cheatsheet/tasks.md @@ -0,0 +1,25 @@ +## 1. Document + +- [ ] 1.1 Create `hooks/__protocol__.md` with the 6 sections (protocol table / three-host compatibility / fail-open / new-hook checklist / change policy / spec mapping). +- [ ] 1.2 Verify every documented exit code / JSON schema matches the actual implementation in `hooks/*.py` (post_tool_lint.py:226 etc.) — record the matching line numbers. + +## 2. tests/run_all.py annotation + +- [ ] 2.1 Add a one-line reference to `hooks/__protocol__.md` in the top-of-file docstring of `tests/run_all.py`.. + +## 3. Validation + +- [ ] 3.1 `python3 -m unittest discover -s tests -p 'test_*.py' -v` — both tests pass. +- [ ] 3.2 `python3 tests/run_all.py` — full 141 / 0 / 0 must hold (hooks/manifests/vendored skills). +- [ ] 3.3 `git diff --check` — clean. +- [ ] 3.4 `openspec validate --all --strict` — pass (3 pre-existing + new `hook-protocol`). +- [ ] 3.5 `grep -c 'exit code\|additionalContext' hooks/__protocol__.md` ≥ 5 (sanity that document has substance). + +## 4. Release + +- [ ] 4.1 `node scripts/bump-plugin.mjs codeguard patch` — bump 0.6.2 → 0.6.3. +- [ ] 4.2 Fix in-repo `.agents/plugins/marketplace.json` ref/icon to `v0.6.3`. +- [ ] 4.3 Open PR, wait for vendor-check, merge. +- [ ] 4.4 Push tag `v0.6.3`. +- [ ] 4.5 Sync market repo catalog + README codeguard row to `0.6.3` and push. +- [ ] 4.6 `openspec archive add-host-protocol-cheatsheet` after all tasks above are checked. \ No newline at end of file diff --git a/openspec/specs/hook-protocol/spec.md b/openspec/specs/hook-protocol/spec.md new file mode 100644 index 0000000..6bfea9f --- /dev/null +++ b/openspec/specs/hook-protocol/spec.md @@ -0,0 +1,37 @@ +# hook-protocol Specification + +## Purpose +TBD - created by archiving change add-host-protocol-cheatsheet. Update Purpose after archive. +## Requirements +### Requirement: Hook-host contract SHALL live in hooks/__protocol__.md + +The canonical description of how codeguard-plugin hooks communicate with Codex CLI, ZCode, and Kimi Code (exit codes, JSON output formats, fail-open convention, three-host compatibility) SHALL live in `hooks/__protocol__.md`. No hook script or docstring SHALL contradict it; any change to exit codes or JSON schema SHALL update the document in the same commit. + +#### Scenario: A future hook author consults the single source + +- **WHEN** an agent (human or AI) adds a new hook to `hooks/` +- **THEN** the README and `hooks/__protocol__.md` together are sufficient to design exit codes and JSON shape without further questions + +#### Scenario: A hook script's docstring contradicts the cheat-sheet + +- **WHEN** a reviewer or `git diff` notices a contradiction between a hook's docstring and `hooks/__protocol__.md` +- **THEN** the implementer updates whichever side is out of date so the document and implementation match in the merged commit + +### Requirement: tests/run_all.py SHALL reference the cheat-sheet + +The top-of-file docstring of `tests/run_all.py` SHALL contain a one-line reference to `hooks/__protocol__.md` describing its role as the canonical contract source for the host-protocol e2e suite. + +#### Scenario: A new contributor opens run_all.py to add a hook subset + +- **WHEN** the contributor reads the docstring +- **THEN** the reference to `hooks/__protocol__.md` is visible and points them at the single source for protocol questions + +### Requirement: The cheat-sheet SHALL document fail-open for uncaught exceptions + +`hooks/__protocol__.md` SHALL include a section explaining the fail-open convention: any uncaught exception inside a hook logs to stderr and exits 0, so a hook bug never blocks the host CLI workflow. This documents the existing behavior in `env_check.py`, `post_tool_lint.py`, `pre_tool_git_guard.py`, `user_prompt_validator.py`, and `stop_summary.py`. + +#### Scenario: A hook hits an unexpected exception + +- **WHEN** an uncaught `Exception` propagates to the top of a hook +- **THEN** the hook prints the traceback tail to stderr and exits 0; the host CLI receives no block signal and continues its workflow + diff --git a/tests/run_all.py b/tests/run_all.py index 907b60b..c92c25c 100644 --- a/tests/run_all.py +++ b/tests/run_all.py @@ -10,6 +10,10 @@ 钩子模拟的原理 = 完全复刻宿主行为:把 ZCode/Claude 会发给钩子的 JSON payload 通过 stdin 喂给真实钩子脚本,断言退出码与输出协议(exit 0 JSON / exit 2 stderr)。 + +宿主契约的**单源事实**在 `hooks/__protocol__.md`(exit 码 / JSON 形态 / fail-open / +三端兼容矩阵 / 新增 hook checklist)。任何与本测试断言不一致的脚本改动必须**同 +commit**同步更新该文档与 `openspec/specs/hook-protocol/spec.md`。 runtime 用例按工具可用性自动 SKIP(shellcheck 已装则真跑,未装则结构通过)。 """ from __future__ import annotations