Skip to content

Commit b8392aa

Browse files
committed
docs: 新增 hooks/__protocol__.md 契约 cheat-sheet + tests/run_all.py 引用
新增 hooks/__protocol__.md(7 节): - 协议总表:5 个事件 × exit 码 × stdout 形态 × 阻断语义 - 三端兼容矩阵:Codex / ZCode / Kimi - fail-open 约定:__main__ 必须 swallow Exception → exit 0 - PreToolUse 硬拦截边界:唯一 exit 2 的 hook + 综述行契约 - 新增 hook checklist:7 步自检清单 - 协议变更策略:同一 commit 改本文件 + 实现 + 测试 + OpenSpec - 与 OpenSpec spec 的对应映射 tests/run_all.py 顶部 docstring 增加单源引用 OpenSpec spec: hook-protocol (3 Requirements) 已合入 验证:unittest 14/14 + run_all 141/141/0 + openspec strict 6/6 + diff clean 实现定位行号锁在 v0.6.2: - SessionStart 摘要:env_check.py:34 - UserPromptSubmit JSON:user_prompt_validator.py:104,117 - PreToolUse stderr:pre_tool_git_guard.py:159 - PostToolUse JSON:post_tool_lint.py:226,253,275 - Stop 摘要:stop_summary.py:46
1 parent 25b0128 commit b8392aa

8 files changed

Lines changed: 277 additions & 0 deletions

File tree

‎hooks/__protocol__.md‎

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# hooks/ 与宿主 CLI 之间的契约(canonical cheat-sheet)
2+
3+
> 这份文档是 codeguard-plugin 与 Codex CLI / ZCode / Kimi Code 三端宿主
4+
> 交互契约的**单源事实**。所有 hook 脚本(`hooks/*.py`)的实现都必须与本文件
5+
> 一致;若未来调整任何退出码或 JSON schema,**同一 commit 内**必须更新本文件。
6+
>
7+
> 对应的 OpenSpec 规范是 `hook-protocol`(见 `openspec/specs/hook-protocol/spec.md`)。
8+
9+
---
10+
11+
## 1. 协议总表
12+
13+
| 事件 | Hook 脚本 | stdout | stderr | exit | 阻断? |
14+
|---|---|---|---|---|---|
15+
| SessionStart | `env_check.py` | 人类可读一行摘要 `codeguard 插件环境:...` | 仅内部错误时 | 0 | 否 |
16+
| UserPromptSubmit | `user_prompt_validator.py` | JSON `{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"..."}}` | 仅内部错误时 | 0 | 否 |
17+
| PreToolUse `Bash` | `pre_tool_git_guard.py` | 通过:空 | 通过:空;失败:完整修复指令 | 0(通过)/ 2(拦截) | 通过:否;失败:是 |
18+
| PostToolUse `Write\|Edit\|MultiEdit` | `post_tool_lint.py` | JSON `{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"..."}, "systemMessage":"..."}` | 仅内部错误时 | 0 | 否 |
19+
| Stop | `stop_summary.py` | 人类可读会话摘要 | 仅内部错误时 | 0 | 否 |
20+
21+
实现定位(行号随 v0.6.2 锁定,下一次重构时核对):
22+
- SessionStart 摘要:`hooks/env_check.py:34`。
23+
- UserPromptSubmit JSON:`hooks/user_prompt_validator.py:104-109` 与 `:117-125`。
24+
- PreToolUse stderr:`hooks/pre_tool_git_guard.py:159`。
25+
- PostToolUse JSON:`hooks/post_tool_lint.py:226-232`(通过)、`:253-259`(自动修复后通过)、`:275-281`(失败 additionalContext)。
26+
- Stop 摘要:`hooks/stop_summary.py:46`。
27+
28+
---
29+
30+
## 2. 三端兼容矩阵
31+
32+
| 宿主 | SessionStart | UserPromptSubmit | PreToolUse Bash | PostToolUse Write\|Edit\|MultiEdit | Stop |
33+
|---|---|---|---|---|---|
34+
| Codex CLI | stdout 注入 developer context | stdout JSON 注入 additionalContext | exit 2 走工具结果回给 AI;exit 0 静默 | stdout JSON 注入 additionalContext + systemMessage | stdout 摘要 |
35+
| ZCode | 同 Codex | 同 Codex,additionalContext 进 systemMessage | 同 Codex | 同 Codex | 同 Codex |
36+
| Kimi Code CLI | stdout 摘要注入会话上下文 | stdout JSON 注入上下文(观察型,**不阻断用户消息**) | exit 2 拦截 + stderr 指令;exit 0 静默 | stdout JSON 注入上下文 | stdout 摘要 |
37+
38+
所有三端在所有事件上,**内部错误都走 fail-open**(见 §3),hook bug 永远不阻断工作流。
39+
40+
---
41+
42+
## 3. Fail-open 约定(强制)
43+
44+
任何 hook 脚本在 `__main__` 处必须有:
45+
46+
```python
47+
if __name__ == "__main__":
48+
try:
49+
sys.exit(main())
50+
except SystemExit:
51+
raise
52+
except Exception as exc:
53+
print(f"[codeguard] 内部错误已忽略(fail-open): {exc!r}", file=sys.stderr)
54+
sys.exit(0)
55+
```
56+
57+
含义:
58+
- 钩子内部任何未捕获异常 → stderr 打印 traceback 摘要 → **exit 0**。
59+
- AI 与用户上下文永远收到「钩子无意见」的语义。
60+
- 同一约定适用于:`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`。
61+
62+
**禁止**把 fail-open 改为 exit 2——会破坏宿主工作流。
63+
64+
---
65+
66+
## 4. PreToolUse 硬拦截(exit 2)的语义边界
67+
68+
`pre_tool_git_guard.py` 是**唯一**会 exit 2 的 hook——且仅当:
69+
1. 入参命令包含 `git commit` 或 `git push`(`is_guarded()` 命中);并且
70+
2. 仓库级 `git config codeguard.skipGate true` 未设置;并且
71+
3. 实际跑 linter 后存在非 skipped 的 failures。
72+
73+
出口内容(`pre_tool_git_guard.py:158-163`):stderr 报告块**首行必须是 `codeguard ❌ 提交门禁未通过:` 综述**,其后跟具体问题列表;这是 `tests/run_all.py:142-144` 守护的契约。
74+
75+
**禁止**在 lint skipped(工具未装/项目未接入)时 exit 2——这是「无法验证」而非「验证失败」。
76+
77+
---
78+
79+
## 5. 新增 hook checklist
80+
81+
新加一个 hook 时,按顺序自检:
82+
83+
1. **读本 cheat-sheet** + `openspec/specs/hook-protocol/spec.md`。
84+
2. 在 `hooks/hooks.json` 注册(事件 / matcher / command / timeout)。
85+
3. 脚本实现遵守 §1 协议总表的 exit 码与 stdout 形态。
86+
4. 脚本 `__main__` 加 §3 的 fail-open 套子。
87+
5. 在 `tests/run_all.py` 加子集触发:stdin 喂真实宿主协议 JSON,断言 exit 码与 stdout 形状。
88+
6. 在本文件 §1 协议总表加一行,标注实现位置(`hooks/<new>.py:line`)。
89+
7. OpenSpec change:proposal / design / `hook-protocol/spec.md` 加 Scenario / tasks 勾选后 archive。
90+
91+
---
92+
93+
## 6. 协议变更策略
94+
95+
任何对 §1 协议总表字段的修改:
96+
97+
- 同步改本文件 + 对应 `hooks/*.py` + 对应测试。
98+
- 走 OpenSpec change(`hook-protocol` spec 下新增 MODIFIED Requirements + Scenarios)。
99+
- 不允许「先改代码后补文档」的拆分 commit——契约一致是同一变更的原子单位。
100+
101+
---
102+
103+
## 7. 与 OpenSpec 的对应
104+
105+
| Cheat-sheet 节 | OpenSpec spec / Scenario |
106+
|---|---|
107+
| §1 协议总表 | `hook-protocol::Requirement: Hook-host contract SHALL live in hooks/__protocol__.md` |
108+
| §3 Fail-open | `hook-protocol::Requirement: The cheat-sheet SHALL document fail-open` |
109+
| §5 新增 hook checklist | `hook-protocol::Requirement: tests/run_all.py SHALL reference the cheat-sheet` |
110+
111+
变更通过 `openspec archive add-host-protocol-cheatsheet` 入库;spec 在 `openspec/specs/hook-protocol/spec.md`。
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-09-22
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
## Context
2+
3+
`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 时」做端到端的清单。
4+
5+
实际行为契约(v0.6.2):
6+
- SessionStart 钩子(`env_check.py`):stdout 人类可读一行摘要;exit 0;不阻断。
7+
- UserPromptSubmit 钩子(`user_prompt_validator.py`):stdout JSON `{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"..."}}`;exit 0(不阻断用户消息);失败时人工写明修复指令进 `additionalContext`。
8+
- PreToolUse Bash 钩子(`pre_tool_git_guard.py`):成功静默(exit 0、零输出);失败 exit 2 + stderr 反馈指令回给 AI。
9+
- PostToolUse Write|Edit|MultiEdit 钩子(`post_tool_lint.py`):stdout JSON `{"decision":"block","reason":"..."}` 或 `{"hookSpecificOutput":{...}}`;exit 0(注入上下文);失败 stderr + JSON 反馈。
10+
- Stop 钩子(`stop_summary.py`):stdout 人类可读;exit 0。
11+
- 全部 hook 内部错误 fail-open:`except Exception: print(... file=sys.stderr); sys.exit(0)`。
12+
13+
三端兼容矩阵(实测):
14+
- Codex CLI:JSON stdout 注入 developer context;exit 2 阻断 + stderr 走工具结果回给 AI。
15+
- ZCode:同 Codex;`additionalContext` 进 systemMessage。
16+
- Kimi Code CLI:SessionStart/UserPromptSubmit 静默观察型,PostToolUse exit 0 时 stdout 注入上下文;Stop 静默。
17+
18+
## Decisions
19+
20+
### 单源契约 = `hooks/__protocol__.md`
21+
22+
文档分六节:协议总表(事件 × 端 × exit × 输出)/ 三端兼容矩阵 / fail-open 约定 / 新增 hook checklist / 协议变更策略 / 与 OpenSpec spec 的对应。
23+
24+
### 行为零变更
25+
26+
本文档仅复刻 v0.6.2 实际行为,不引入新行为;任何与现实现矛盾的描述都按现状修正,不重设契约。
27+
28+
### `run_all.py` 顶部 docstring 加引用
29+
30+
让 host-protocol 的发现面只有一个:从 README 到 hooks/ 时自然能看到。子集测试逻辑不变。
31+
32+
### 不锁实现细节
33+
34+
exit 码与 JSON schema 用「事实陈述」措辞("scripts/post_tool_lint.py:226 emits JSON with decision/reason fields"),便于读者回溯到代码;不抽象成「必须如何」。
35+
36+
## Risks / Trade-offs
37+
38+
- **文档与实现漂移**:当前契约由 8 处分散文档描述,本身就有潜在不一致。**新文档是综合而非规范**——未来协议演化需要**先改代码 + 同步本 markdown + 提交**。在 OpenSpec spec `hook-protocol` 中作为 Requirement 加 Scenario 守卫。
39+
- **未来新增 hook 时漏读**:用 checklist 强制后续开发者先读本文件。
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
## Why
2+
3+
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 静默总结。
4+
5+
这些契约目前分散在每个 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 反馈)上走偏。
6+
7+
## What Changes
8+
9+
- 新增 `hooks/__protocol__.md`:单源契约文档,覆盖 exit 码、JSON 输出格式、三端兼容矩阵、fail-open 约定、stale hook timeout 设定与新增 hook checklist。
10+
- `tests/run_all.py` 顶部 docstring 增加「host protocol cheat-sheet」引用,明确 `__protocol__.md` 为子集契约源。
11+
- 不修改任何 hook 脚本的实现(输出格式与退出码与 v0.6.2 完全一致);不修改 `hooks/hooks.json`。
12+
- 仅文档 + 引用;归档后 bump patch。
13+
14+
## Capabilities
15+
16+
### New Capabilities
17+
18+
- `hook-protocol`: Defines the canonical contract between codeguard-plugin hooks and host CLIs (Codex CLI / ZCode / Kimi Code).
19+
20+
### Modified Capabilities
21+
22+
None.
23+
24+
## Impact
25+
26+
新增 1 份 Markdown 文档(预计 ~120 行);修改 1 处 docstring 引用。无脚本/测试/manifest 行为变化。
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
## ADDED Requirements
2+
3+
### Requirement: Hook-host contract SHALL live in hooks/__protocol__.md
4+
5+
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.
6+
7+
#### Scenario: A future hook author consults the single source
8+
9+
- **WHEN** an agent (human or AI) adds a new hook to `hooks/`
10+
- **THEN** the README and `hooks/__protocol__.md` together are sufficient to design exit codes and JSON shape without further questions
11+
12+
#### Scenario: A hook script's docstring contradicts the cheat-sheet
13+
14+
- **WHEN** a reviewer or `git diff` notices a contradiction between a hook's docstring and `hooks/__protocol__.md`
15+
- **THEN** the implementer updates whichever side is out of date so the document and implementation match in the merged commit
16+
17+
### Requirement: tests/run_all.py SHALL reference the cheat-sheet
18+
19+
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.
20+
21+
#### Scenario: A new contributor opens run_all.py to add a hook subset
22+
23+
- **WHEN** the contributor reads the docstring
24+
- **THEN** the reference to `hooks/__protocol__.md` is visible and points them at the single source for protocol questions
25+
26+
### Requirement: The cheat-sheet SHALL document fail-open for uncaught exceptions
27+
28+
`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`.
29+
30+
#### Scenario: A hook hits an unexpected exception
31+
32+
- **WHEN** an uncaught `Exception` propagates to the top of a hook
33+
- **THEN** the hook prints the traceback tail to stderr and exits 0; the host CLI receives no block signal and continues its workflow
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
## 1. Document
2+
3+
- [ ] 1.1 Create `hooks/__protocol__.md` with the 6 sections (protocol table / three-host compatibility / fail-open / new-hook checklist / change policy / spec mapping).
4+
- [ ] 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.
5+
6+
## 2. tests/run_all.py annotation
7+
8+
- [ ] 2.1 Add a one-line reference to `hooks/__protocol__.md` in the top-of-file docstring of `tests/run_all.py`..
9+
10+
## 3. Validation
11+
12+
- [ ] 3.1 `python3 -m unittest discover -s tests -p 'test_*.py' -v` — both tests pass.
13+
- [ ] 3.2 `python3 tests/run_all.py` — full 141 / 0 / 0 must hold (hooks/manifests/vendored skills).
14+
- [ ] 3.3 `git diff --check` — clean.
15+
- [ ] 3.4 `openspec validate --all --strict` — pass (3 pre-existing + new `hook-protocol`).
16+
- [ ] 3.5 `grep -c 'exit code\|additionalContext' hooks/__protocol__.md` ≥ 5 (sanity that document has substance).
17+
18+
## 4. Release
19+
20+
- [ ] 4.1 `node scripts/bump-plugin.mjs codeguard patch` — bump 0.6.2 → 0.6.3.
21+
- [ ] 4.2 Fix in-repo `.agents/plugins/marketplace.json` ref/icon to `v0.6.3`.
22+
- [ ] 4.3 Open PR, wait for vendor-check, merge.
23+
- [ ] 4.4 Push tag `v0.6.3`.
24+
- [ ] 4.5 Sync market repo catalog + README codeguard row to `0.6.3` and push.
25+
- [ ] 4.6 `openspec archive add-host-protocol-cheatsheet` after all tasks above are checked.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# hook-protocol Specification
2+
3+
## Purpose
4+
TBD - created by archiving change add-host-protocol-cheatsheet. Update Purpose after archive.
5+
## Requirements
6+
### Requirement: Hook-host contract SHALL live in hooks/__protocol__.md
7+
8+
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.
9+
10+
#### Scenario: A future hook author consults the single source
11+
12+
- **WHEN** an agent (human or AI) adds a new hook to `hooks/`
13+
- **THEN** the README and `hooks/__protocol__.md` together are sufficient to design exit codes and JSON shape without further questions
14+
15+
#### Scenario: A hook script's docstring contradicts the cheat-sheet
16+
17+
- **WHEN** a reviewer or `git diff` notices a contradiction between a hook's docstring and `hooks/__protocol__.md`
18+
- **THEN** the implementer updates whichever side is out of date so the document and implementation match in the merged commit
19+
20+
### Requirement: tests/run_all.py SHALL reference the cheat-sheet
21+
22+
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.
23+
24+
#### Scenario: A new contributor opens run_all.py to add a hook subset
25+
26+
- **WHEN** the contributor reads the docstring
27+
- **THEN** the reference to `hooks/__protocol__.md` is visible and points them at the single source for protocol questions
28+
29+
### Requirement: The cheat-sheet SHALL document fail-open for uncaught exceptions
30+
31+
`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`.
32+
33+
#### Scenario: A hook hits an unexpected exception
34+
35+
- **WHEN** an uncaught `Exception` propagates to the top of a hook
36+
- **THEN** the hook prints the traceback tail to stderr and exits 0; the host CLI receives no block signal and continues its workflow
37+

‎tests/run_all.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
1111
钩子模拟的原理 = 完全复刻宿主行为:把 ZCode/Claude 会发给钩子的 JSON payload
1212
通过 stdin 喂给真实钩子脚本,断言退出码与输出协议(exit 0 JSON / exit 2 stderr)。
13+
14+
宿主契约的**单源事实**在 `hooks/__protocol__.md`(exit 码 / JSON 形态 / fail-open /
15+
三端兼容矩阵 / 新增 hook checklist)。任何与本测试断言不一致的脚本改动必须**同
16+
commit**同步更新该文档与 `openspec/specs/hook-protocol/spec.md`。
1317
runtime 用例按工具可用性自动 SKIP(shellcheck 已装则真跑,未装则结构通过)。
1418
"""
1519
from __future__ import annotations

0 commit comments

Comments
 (0)