Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions hooks/__protocol__.md
Original file line number Diff line number Diff line change
@@ -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/<new>.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`。
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-09-22
Original file line number Diff line number Diff line change
@@ -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 强制后续开发者先读本文件。
Original file line number Diff line number Diff line change
@@ -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 行为变化。
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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.
37 changes: 37 additions & 0 deletions openspec/specs/hook-protocol/spec.md
Original file line number Diff line number Diff line change
@@ -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

4 changes: 4 additions & 0 deletions tests/run_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading