merge: candidate cursor-governance-hooks into dev

This commit is contained in:
weichen
2026-09-24 09:30:49 +00:00
28 changed files with 1619 additions and 29 deletions
+37
View File
@@ -5,7 +5,44 @@
{
"command": ".cursor/hooks/guard-git.sh",
"matcher": "^git ",
"timeout": 10,
"failClosed": true
},
{
"command": ".cursor/hooks/guard-pi-shell.sh",
"matcher": "pi_subagents\\.py|pkill",
"timeout": 10,
"failClosed": true
}
],
"beforeMCPExecution": [
{
"command": ".cursor/hooks/guard-pi-dispatch.sh",
"matcher": "pi-subagents|pi_subagent_",
"timeout": 10,
"failClosed": true
}
],
"postToolUse": [
{
"command": ".cursor/hooks/pi-receipt-context.sh",
"matcher": "MCP: pi-subagents|pi_subagent_",
"timeout": 10,
"failClosed": false
}
],
"sessionStart": [
{
"command": ".cursor/hooks/audit-session.sh",
"timeout": 20,
"failClosed": false
}
],
"stop": [
{
"command": ".cursor/hooks/audit-session.sh",
"timeout": 10,
"failClosed": false
}
]
}
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""Shared helpers for project Cursor hooks (advisory gates + audit/probe)."""
from __future__ import annotations
import json
import os
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
KNOWN_ROLES = frozenset(
{
"research",
"planning",
"requirements_review",
"plan_review",
"code_review",
"diagnosis",
"test_run",
"acceptance_check",
"stage_notes",
"report",
"auxiliary",
}
)
TEST_ROLES = frozenset({"test_run", "acceptance_check"})
FULL_SHA = re.compile(r"^[0-9a-fA-F]{40}$")
REQUEST_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
def repo_root() -> Path:
env = os.environ.get("CURSOR_PROJECT_DIR") or os.environ.get("CURSOR_WORKSPACE_ROOT")
if env:
return Path(env).resolve()
# hooks run with cwd = project root
return Path.cwd().resolve()
def audit_path() -> Path:
return repo_root() / ".cursor" / "hook-audit.jsonl"
def probe_path() -> Path:
return repo_root() / ".cursor" / "hook-probe.jsonl"
def inventory_dir() -> Path:
return repo_root() / ".cursor" / "hook-audit"
def append_jsonl(path: Path, record: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
record = dict(record)
record.setdefault("ts", datetime.now(timezone.utc).isoformat())
with path.open("a", encoding="utf-8") as fh:
fh.write(json.dumps(record, ensure_ascii=False) + "\n")
def read_stdin() -> str:
return sys.stdin.read()
def maybe_probe(raw: str, hook: str) -> None:
if os.environ.get("CURSOR_HOOKS_PROBE", "").strip() not in ("1", "true", "TRUE", "yes"):
return
append_jsonl(
probe_path(),
{"hook": hook, "raw": raw[:65536], "raw_len": len(raw)},
)
def parse_json_loose(raw: str) -> dict[str, Any] | None:
raw = (raw or "").strip()
if not raw:
return None
try:
data = json.loads(raw)
except json.JSONDecodeError:
return None
return data if isinstance(data, dict) else None
def first_key(data: dict[str, Any], *names: str) -> Any:
for name in names:
if name in data and data[name] is not None:
return data[name]
return None
def tool_name(data: dict[str, Any]) -> str | None:
v = first_key(data, "tool_name", "mcp_tool_name", "name", "toolName")
return str(v) if v is not None else None
def mcp_server(data: dict[str, Any]) -> str | None:
v = first_key(data, "mcp_server_name", "server", "mcpServer", "server_name")
return str(v) if v is not None else None
def tool_input(data: dict[str, Any]) -> dict[str, Any] | None:
v = first_key(data, "tool_input", "arguments", "input", "toolInput", "params")
if isinstance(v, dict):
return v
if isinstance(v, str):
try:
parsed = json.loads(v)
return parsed if isinstance(parsed, dict) else None
except json.JSONDecodeError:
return None
return None
def shell_command(data: dict[str, Any]) -> str:
v = first_key(data, "command", "shell_command", "cmd")
return str(v) if v is not None else ""
def emit(obj: dict[str, Any]) -> None:
sys.stdout.write(json.dumps(obj, ensure_ascii=False))
sys.stdout.flush()
def allow() -> None:
emit({"permission": "allow"})
raise SystemExit(0)
def ask(user_message: str, agent_message: str) -> None:
emit(
{
"permission": "ask",
"user_message": user_message,
"agent_message": agent_message,
}
)
raise SystemExit(0)
def additional_context(text: str) -> None:
emit({"additional_context": text})
raise SystemExit(0)
def audit(hook: str, code: str, **extra: Any) -> None:
rec = {"hook": hook, "code": code}
rec.update(extra)
try:
append_jsonl(audit_path(), rec)
except OSError:
pass
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
DIR="$(cd "$(dirname "$0")" && pwd)"
exec python3 -B "$DIR/audit_session.py"
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""sessionStart / stop: audit + optional fail-open inventory (no context injection)."""
from __future__ import annotations
import json
import os
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import _lib as lib # noqa: E402
HOOK = "audit-session"
INVENTORY_TIMEOUT = 15
def _run_inventory(root: Path) -> dict:
out: dict = {"cleanup_report": None, "docs_drift": None, "errors": []}
if os.environ.get("CURSOR_HOOKS_NO_INVENTORY", "").strip() in ("1", "true", "TRUE", "yes"):
out["skipped"] = "CURSOR_HOOKS_NO_INVENTORY"
return out
agentctl = root / "scripts" / "agentctl.py"
if not agentctl.is_file():
out["errors"].append("agentctl.py missing")
return out
for label, args in (
("cleanup_report", ["cleanup", "report"]),
("docs_drift", ["docs", "drift"]),
):
try:
r = subprocess.run(
["python3", "-B", str(agentctl), *args],
cwd=str(root),
capture_output=True,
text=True,
timeout=INVENTORY_TIMEOUT,
check=False,
)
payload = None
for stream in (r.stdout, r.stderr):
for line in (stream or "").splitlines():
line = line.strip()
if line.startswith("{"):
try:
payload = json.loads(line)
break
except json.JSONDecodeError:
continue
if payload is not None:
break
out[label] = {
"exit_code": r.returncode,
"payload": payload,
"stdout_tail": (r.stdout or "")[-2000:],
"stderr_tail": (r.stderr or "")[-1000:],
}
except subprocess.TimeoutExpired:
out["errors"].append(f"{label}: timeout>{INVENTORY_TIMEOUT}s")
except OSError as exc:
out["errors"].append(f"{label}: {exc}")
return out
def main() -> None:
raw = lib.read_stdin()
lib.maybe_probe(raw, HOOK)
data = lib.parse_json_loose(raw) or {}
event = str(
lib.first_key(data, "hook_event_name", "event", "event_name", "type") or "unknown"
)
root = lib.repo_root()
if "start" in event.lower() or event == "unknown":
inv = _run_inventory(root)
inv_path = None
try:
inv_dir = lib.inventory_dir()
inv_dir.mkdir(parents=True, exist_ok=True)
ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
path = inv_dir / f"inventory-{ts}.json"
path.write_text(json.dumps(inv, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
inv_path = str(path)
except OSError:
pass
lib.audit(
HOOK,
"SESSION_AUDIT",
event=event,
inventory_path=inv_path,
inventory_errors=inv.get("errors"),
)
else:
lib.audit(HOOK, "SESSION_AUDIT", event=event)
lib.emit({})
raise SystemExit(0)
if __name__ == "__main__":
main()
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# Thin wrapper: preserve stdin for Cursor hook JSON (do not use heredoc).
set -euo pipefail
DIR="$(cd "$(dirname "$0")" && pwd)"
exec python3 -B "$DIR/guard_pi_dispatch.py"
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
DIR="$(cd "$(dirname "$0")" && pwd)"
exec python3 -B "$DIR/guard_pi_shell.py"
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""Advisory beforeMCPExecution gate for pi-subagents MCP tools."""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import _lib as lib # noqa: E402
HOOK = "guard-pi-dispatch"
def main() -> None:
raw = lib.read_stdin()
lib.maybe_probe(raw, HOOK)
data = lib.parse_json_loose(raw)
if data is None:
lib.audit(HOOK, "UNRECOGNIZED_INPUT", reason="non_json_or_empty")
lib.allow()
name = lib.tool_name(data)
server = lib.mcp_server(data)
if not name:
lib.audit(HOOK, "UNRECOGNIZED_INPUT", reason="missing_tool_name", keys=sorted(data.keys())[:40])
lib.allow()
if server and "pi-subagents" not in server and not name.startswith("pi_subagent_"):
if not name.startswith("pi_subagent_"):
lib.allow()
if name in ("pi_subagent_roles", "pi_subagent_result"):
lib.allow()
if name != "pi_subagent_run":
if name.startswith("pi_subagent_"):
lib.audit(HOOK, "UNRECOGNIZED_INPUT", reason="unknown_pi_tool", tool=name)
lib.allow()
args = lib.tool_input(data) or {}
req = args.get("request") if isinstance(args.get("request"), dict) else args
if not isinstance(req, dict):
lib.audit(HOOK, "UNRECOGNIZED_INPUT", reason="missing_request")
lib.ask(
"pi_subagent_run 缺少可识别的 request 对象。请核对 MCP 参数后再继续。",
"Hook: unrecognized request shape for pi_subagent_run.",
)
role = req.get("role_id")
request_id = req.get("request_id")
goal = req.get("goal")
auth = req.get("authorization_ref")
source_sha = req.get("source_sha")
input_files = req.get("input_files")
timeout = req.get("timeout_seconds")
problems: list[str] = []
if not role or not isinstance(role, str):
problems.append("role_id 缺失")
elif role not in lib.KNOWN_ROLES:
problems.append(f"role_id={role!r} 不在已知角色表")
if not request_id or not isinstance(request_id, str) or not lib.REQUEST_ID.match(request_id):
problems.append("request_id 非法(需字母数字开头,长度≤128)")
if not goal or not isinstance(goal, str) or not goal.strip():
problems.append("goal 为空")
if not auth or not isinstance(auth, str) or not auth.strip():
problems.append("authorization_ref 为空")
if not isinstance(source_sha, str) or not lib.FULL_SHA.match(source_sha):
problems.append("source_sha 必须是完整 40-hex 提交(禁止 HEAD/分支名/短 hash)")
if not isinstance(input_files, list):
problems.append("input_files 必须是数组")
if not isinstance(timeout, int) or isinstance(timeout, bool) or timeout <= 0:
if isinstance(timeout, float) and timeout.is_integer() and timeout > 0:
pass
else:
problems.append("timeout_seconds 必须是正整数")
if isinstance(role, str) and role in lib.TEST_ROLES:
cmds = req.get("approved_commands")
digest = req.get("plan_digest")
if not isinstance(cmds, list) or not cmds:
problems.append("test 角色需要 approved_commands(argv 数组的数组)")
elif any(not isinstance(c, list) or not all(isinstance(x, str) for x in c) for c in cmds):
problems.append("approved_commands 形状错误(应为字符串数组的数组)")
if not isinstance(digest, str) or len(digest) != 64 or any(
c not in "0123456789abcdefABCDEF" for c in digest
):
problems.append("plan_digest 必须是 64-hex SHA-256")
if "model_override" in req and req.get("model_override") is not None:
problems.append("出现 model_override(禁止静默换模型;请用任务 override)")
if problems:
detail = ";".join(problems)
lib.audit(HOOK, "SHAPE_ASK", tool=name, problems=problems, role=role, request_id=request_id)
lib.ask(
f"pi_subagent_run 形状检查未通过:{detail}。执行器仍会硬校验;确认后继续或修正参数。",
f"Hook advisory: {detail}",
)
lib.audit(HOOK, "ALLOW", tool=name, role=role, request_id=request_id)
lib.allow()
if __name__ == "__main__":
main()
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""Advisory beforeShellExecution: bare pi_subagents.py run / broad pkill."""
from __future__ import annotations
import re
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import _lib as lib # noqa: E402
HOOK = "guard-pi-shell"
def main() -> None:
raw = lib.read_stdin()
lib.maybe_probe(raw, HOOK)
data = lib.parse_json_loose(raw)
if data is None:
lib.audit(HOOK, "UNRECOGNIZED_INPUT", reason="non_json_or_empty")
lib.allow()
cmd = lib.shell_command(data)
if not cmd:
lib.audit(HOOK, "UNRECOGNIZED_INPUT", reason="missing_command", keys=sorted(data.keys())[:40])
lib.allow()
compact = re.sub(r"\s+", " ", cmd).strip()
if re.search(r"(^|[;&|]\s*)pkill(\s|$)", compact) and re.search(r"\s-f(\s|$)", compact):
lib.audit(HOOK, "PKILL_ASK", command=compact[:500])
lib.ask(
"检测到 pkill -f(广谱杀进程)。AGENTS/PI 规则禁止广谱杀;请确认范围后再继续。",
"Hook requires confirmation for pkill -f.",
)
if re.search(r"pi_subagents\.py(\s|$)", compact) and re.search(r"(^|\s)run(\s|$)", compact):
has_lease = (
re.search(r"agentctl(\.py)?\s+with\b", compact) is not None
and re.search(r"--resource\s+path:", compact) is not None
)
if re.search(r"pi_subagents\.py\s+(roles|result|status|config)\b", compact):
lib.allow()
if not has_lease:
lib.audit(HOOK, "BARE_RUN_ASK", command=compact[:500])
lib.ask(
"裸调 pi_subagents.py run 缺少 agentctl with --resource path:… 租约。"
"报告路径须在 path:.doc/test-reports/assets/pi-subagents/<task>/<request> 租约内。",
"Hook: bare PI run without lease resource.",
)
lib.allow()
if __name__ == "__main__":
main()
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
DIR="$(cd "$(dirname "$0")" && pwd)"
exec python3 -B "$DIR/pi_receipt_context.py"
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""postToolUse: inject receipt-check reminders after pi-subagents tools."""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import _lib as lib # noqa: E402
HOOK = "pi-receipt-context"
REMINDER = """[PI subagent receipt checklist]
- 核对 report/receipt 路径:.doc/test-reports/assets/pi-subagents/<task_id>/<request_id>/
- 记录 config_revision / config_sha256 / configured vs actual_model(不可观察填 unknown)
- run_status=succeeded 不等于阶段 PASS;review_state 仍可为 pending
- 测试 pass 只来自批准命令证据(approved_commands),不能用模型叙事冒充
- 推进门禁前用 subagent-stage-record skill 写回传与阶段收据
"""
def main() -> None:
raw = lib.read_stdin()
lib.maybe_probe(raw, HOOK)
data = lib.parse_json_loose(raw)
if data is None:
lib.audit(HOOK, "UNRECOGNIZED_INPUT", reason="non_json_or_empty")
lib.emit({})
raise SystemExit(0)
name = lib.tool_name(data) or ""
server = lib.mcp_server(data) or ""
blob = raw
is_pi = (
name.startswith("pi_subagent_")
or "pi-subagents" in server
or "pi_subagent_" in blob
)
if not is_pi:
lib.emit({})
raise SystemExit(0)
lib.audit(HOOK, "CONTEXT_INJECT", tool=name or "unknown")
lib.additional_context(REMINDER)
if __name__ == "__main__":
main()
+2 -2
View File
@@ -2,11 +2,11 @@
"mcpServers": {
"agentctl": {
"command": "node",
"args": ["${workspaceFolder}/scripts/mcp/agentctl-mcp.mjs"]
"args": ["scripts/mcp/agentctl-mcp.mjs"]
},
"jev": {
"command": "node",
"args": ["${workspaceFolder}/scripts/mcp/jev-mcp.mjs"]
"args": ["scripts/mcp/jev-mcp.mjs"]
},
"codegraph": {
"type": "stdio",
+5 -2
View File
@@ -11,7 +11,7 @@ alwaysApply: true
1. **写仓库**:必须 `agentctl with` / MCP `lease_run`。裸 `git commit` 会被 agentctl pre-commit hook 拒绝。
2. **共享文档**:`.doc/ops/CURRENT.md`、`.doc/status/FEATURE-MATRIX.md` 只能 `docs apply`(行级 CAS;MATRIX 键为 `record_id`)。
3. **合入 local dev**:`validate` 后 `queue enqueue`(daemon `--ff-only`)或受控 `integrate`;禁止直接改主目录 `dev` tip。
3. **合入 local dev**:`validate` 后 `queue enqueue`(daemon `--ff-only`)或受控 `integrate`;禁止直接改主目录 `dev` tip。
4. **禁止默认**:未授权 `git push`、晋升 test/main、`--force` 清理、卸载 launchd coordinator、改 gap-matrix / 升 MATRIX 状态值冒充发布。
5. **需求重叠**:跨对话新设计须 `catalog overlap`;硬重叠拒绝;软重叠须 `catalog ack` 留痕后方可编码(见 `.doc/ops/2026-09-24-governance-catalog-plan.md`)。
@@ -21,4 +21,7 @@ alwaysApply: true
- 建场 → `agentctl-worktree-setup`
- 写回 → `agentctl-lease-write`
- 合入/收口 → `agentctl-integrate-closeout`
- MCP 工具定义见 `.cursor/mcp.json` → `scripts/mcp/agentctl-mcp.mjs`
- PI 派发 → skill `pi-subagent-dispatch`(MCP `pi-subagents`)
- 阶段落盘 → skill `subagent-stage-record`
- MCP 工具定义见 `.cursor/mcp.json` → `scripts/mcp/agentctl-mcp.mjs` / `pi-subagents-mcp.mjs`
- 客户端 hook 边界见规则 `cursor-hooks`
+40
View File
@@ -0,0 +1,40 @@
---
description: Project Cursor hooks are advisory gates layered on agentctl/PI executor hard checks; scripts exist does not mean Cursor loaded them.
alwaysApply: true
---
# Cursor hooks(客户端门禁)
配置:`.cursor/hooks.json`;脚本:`.cursor/hooks/`。规范登记修订 `cursor-governance-2026-09-24-r1`。
## 分层
| 层 | 能力 | 说明 |
|---|---|---|
| 执行器 / agentctl | **enforced** | `scripts/pi_subagents.py`、租约、pre-commit 等硬拒绝 |
| hook 脚本行为 | **enforced**(有单元回归) | stdin→stdout 契约可测 |
| hook ↔ Cursor 接线 | **advisory / manual** | 官方事件字段未完全文档化;待 probe 采样后收紧 |
命中策略:与 `guard-git.sh` 一致,一律 `permission: ask`(不静默 deny)。
## 事件与脚本
- `beforeShellExecution`:`guard-git.sh`(破坏性 git);`guard-pi-shell.sh`(裸调 `pi_subagents.py run`、`pkill -f`)
- `beforeMCPExecution`:`guard-pi-dispatch.sh`(`pi_subagent_run` 形状)
- `postToolUse`:`pi-receipt-context.sh`(注入收据核对提醒)
- `sessionStart` / `stop`:`audit-session.sh`(审计 + 可选只读盘点;**不注入上下文**)
## 审计与 probe
- 审计:`.cursor/hook-audit.jsonl`(gitignore;勿提交)
- 盘点:`.cursor/hook-audit/inventory-*.json`
- 采样:`CURSOR_HOOKS_PROBE=1` → `.cursor/hook-probe.jsonl`
- 关闭盘点:`CURSOR_HOOKS_NO_INVENTORY=1`
异形/未知字段:**fail-open + 审计 `UNRECOGNIZED_INPUT`**,不因契约未知卡死会话。
## 红线
- 不把 hook 复刻成第二套执行器门禁
- 脚本存在 ≠ Cursor 已加载;改 hooks 后检查 Hooks 设置页或通道
- 审计日志不得放进被 git 跟踪的 `.cursor/hooks/` 目录
+2 -1
View File
@@ -5,7 +5,7 @@ alwaysApply: true
# PI subagents(工程工具)
本机 PI 子任务走 `scripts/pi_subagents.py` + 模型配置 `.doc/ops/2026-09-24-subagent-model-config.md`。
本机 PI 子任务走 `scripts/pi_subagents.py` + 模型配置 `.doc/ops/2026-09-24-subagent-model-config.md`。可执行步骤见 skill `pi-subagent-dispatch`;阶段记录见 `subagent-stage-record`。
## 自动派发(主 Agent)
@@ -21,3 +21,4 @@ alwaysApply: true
- **Cursor Auto**:唯一代码实施车道;本规则不把 implementation 派给 PI。
- **禁止**:自动换模型、M3 关键角色、伪造 lease、递归把本修复任务再派给 PI worker、`pkill -f` 广谱杀进程。
- **MCP**:`.cursor/mcp.json` → `pi-subagents`(roles/run/result);executor 来自 MCP 安装根。
- **Hook(advisory)**:`.cursor/hooks/guard-pi-dispatch.sh` / `guard-pi-shell.sh` 可在形状错误时 `ask`;**硬拒绝以执行器为准**,见规则 `cursor-hooks`。
+17 -21
View File
@@ -1,14 +1,13 @@
---
description: 规定项目中所有自动化测试、功能测试、集成测试报告的存放路径与结构标准规范
globs: ["**/*test*.go", ".doc/test-reports/**/*.md"]
description: Cursor-side test report path/naming conventions; validation scope comes from the validation contract.
alwaysApply: true
---
# 测试报告规范准则 (Test Reports Standard)
本仓库严格遵循标准化、可追溯的质量测试管理体系。所有测试报告必须归档于指定的文档目录下,并遵循固定结构。
本仓库严格遵循标准化、可追溯的质量测试管理体系。**验证范围、套件选择与模型证据的唯一来源**为 [验证契约](../../.doc/ops/2026-09-21-validation-contract.md);本规则只约定 Cursor 侧的报告路径、命名与结构提醒,不另设第二套测试门禁。
**范围判定**:纯工程规范/文档变更按 [PROJECT.md](../../PROJECT.md) §8 和 [worktree 工作规范](../../.doc/ops/2026-09-21-agent-worktree-workflow.md) 检查链接、规则一致性及 Git 差异;不适用下述产品 PI 全链路与全量 Go 回归要求。报告注明 NOT REQUIRED 及依据,不能记成产品测试 PASS。运行模板、提示词和业务变更不属于此例外。
**范围判定**:纯工程规范/文档变更按 [PROJECT.md 验证入口](../../PROJECT.md#验证入口) 与 [worktree 工作规范](../../.doc/ops/2026-09-21-agent-worktree-workflow.md) 检查链接、规则一致性及 Git 差异;不适用产品 PI 全链路。报告注明 NOT REQUIRED 及依据,不能记成产品测试 PASS。运行模板、提示词和业务变更不属于此例外——按验证契约选层级。
## 一、 测试报告归档路径规范
@@ -17,11 +16,9 @@ alwaysApply: true
- 例如:`.doc/test-reports/2026-09-07-three-branch-intent-framework-test-report.md`
- **证据附件**:运行产物(json/jsonl、workspace 快照、截图)放 `.doc/test-reports/assets/<report-slug>/`,**不得**与 `*-test-report.md` 混放在同一层级目录。
---
## 二、 测试报告固定结构模板
每份测试报告必须包含以下核心板块:
每份独立测试报告应包含以下核心板块(小型文档/脚本任务可在任务主文档设验证节,无需机械拆报告):
```markdown
# [功能/模块名称] 测试报告
@@ -30,38 +27,37 @@ alwaysApply: true
- 简述被测功能/特性的产生背景、业务痛点或上一轮测试中发现的缺陷与瓶颈。
## 二、 测试目标 (Test Objectives)
- 列举本次测试需要验证的核心功能点、质量指标(如通过率 100%、无流程卡点、非阻塞人机协同等)。
- 列举本次测试需要验证的核心功能点、质量指标。
## 三、 测试方案与环境 (Test Plan & Environment)
- 测试环境(OS、Go 版本、后端 Stub/Real 模型);
- 测试层级分层(单元测试、协同集成测试、CLI 真实交互测试);
- 测试环境(OS、工具版本、目标环境批准配置);
- 测试层级分层(按验证契约矩阵选行);
- 核心测试用例矩阵设计。
## 四、 预期结果 (Expected Results)
- 明确给出各用例输入下的理论判定分支、目标意图、提取槽位、参谋回复风格与状态机流转。
- 明确给出各用例输入下的理论判定。
## 五、 实际测试结果与判定 (Actual Results & Evaluation)
- 必须包含针对每一个典型测试用例的真实记录:
- **测试输入 (Input)**
- **实际输出 (Actual Output)**:贴出真实的终端/日志输出文本
- **评价判定 (Evaluation / Verdict)**:PASS / FAIL 标志及判定依据
- **实际输出 (Actual Output)**:贴出真实的终端/日志输出文本(可脱敏)
- **评价判定 (Evaluation / Verdict)**:PASS / FAIL / SKIP / 未运行
- **性能与耗时 (Duration & Latency)**
## 六、 自动化测试套件汇总 (Test Suite Execution)
- 附带 `go test -v ./...` 全量回归测试输出与通过率统计。
- 只列本次适用并**实际运行**的检查与通过率;不无条件要求全量 `go test`。
- 工程工具改动优先 `python3 -B -m unittest discover -s scripts/tests -p 'test_*.py' -v`(见验证契约「工程工具验收补充」)。
## 七、 测试结论与后续优化建议 (Conclusion & Next Steps)
- 总结测试是否达到上线/合并标准,提出后续演进或强化点。
- 总结测试是否达到授权交付终点,提出后续演进或强化点。
```
---
## 三、 执行要求与真实 Agent 交付铁律
1. **真实 Agent 交付铁律(红线)**:
- **开发实现阶段**:可以快速使用 `stub` 验证状态机、协议分流、数据落盘与逻辑契约,加速反馈;
- **交付前验收与测试报告生成**:**必须切回真实环境与真实 Agent(`pi-agent` + 实际运行的 `MiniMax-M2.7-highspeed` 容器)进行全链路端到端实测**;
- **严禁**使用 stub 的测试输出作为最终交付报告的验收数据。
2. **严禁散落存放**:测试报告统一归档至 `.doc/test-reports/` 目录下,禁止散落在根目录或临时沙箱中。
- **开发实现阶段**:可以快速使用 stub 验证状态机、协议分流、数据落盘与逻辑契约,加速反馈;
- **交付前验收与测试报告生成**:涉及产品 Agent/工作流时,必须按验证契约「产品 PI 基线」在获授权环境用真实产品 PI Agent Runner 全链路实测;模型以目标环境批准配置为准,不得在本规则中硬编码模型名;
- **严禁**使用 stub、SKIP、未运行输出作为最终交付报告的验收数据。
2. **严禁散落存放**:测试报告统一归档至 `.doc/test-reports/`,禁止散落在根目录或临时沙箱中。
3. **真实控制台取样**:测试报告中的输入、输出、耗时与判定必须取自实际环境运行的真实控制台输出与日志。
4. **Git 规范管理**:测试报告伴随对应阶段的 `docs(test)` 或 `test(...)` 规范提交入库。
@@ -0,0 +1,77 @@
---
name: pi-subagent-dispatch
description: Dispatch gmktec PI analysis/review/test subagents via project MCP pi-subagents (roles/run/result). Use when delegating research, planning, reviews, diagnosis, test_run, acceptance_check, or report work—not for code implementation.
---
# PI Subagent 派发
权威:[模型配置](../../../.doc/ops/2026-09-24-subagent-model-config.md)、[执行器说明](../../../.doc/ops/2026-09-24-pi-subagent-executor.md)、[流程规范](../../../.doc/ops/2026-09-24-subagent-workflow-standard.md)。客户端规则:[pi-subagents](../../rules/pi-subagents.mdc)。阶段落盘:[subagent-stage-record](../subagent-stage-record/SKILL.md)。
**Cursor Auto 独占代码实施与修复**;本 skill 只走本机 PI 子任务。
## 调用序列
1. `pi_subagent_roles` — 只读预览角色/模型(不调 PI)。
2. 选 `role_id`(research → planning → reviews → diagnosis → test_run / acceptance_check → report)。
3. `pi_subagent_run` — 在 agentctl 租约下异步启动。
4. `pi_subagent_result` — 取回收据;轮询至终态。
CLI 等价:`python3 -B scripts/pi_subagents.py roles|run|result`;`run` 必须包在 `agentctl with --resource path:.doc/test-reports/assets/pi-subagents/<task>/<request>` 内。
## 请求字段(MCP `pi_subagent_run`)
外层:`worktree`(已登记任务工作区绝对路径)、`task_id`、`request` 对象。
`request` 必填:
| 字段 | 约束 |
|---|---|
| `role_id` | 配置 §3 中 enabled 角色 |
| `request_id` | 任务内稳定唯一;同参重放不重复调模型 |
| `goal` | 具体目标;子任务不依赖主会话未传入历史 |
| `authorization_ref` | 可回链真实用户授权,禁止日志自授 |
| `source_sha` | **完整** 40-hex 提交;禁止 `HEAD`/分支名/短 hash |
| `input_files` | 已跟踪 UTF-8 普通文件相对路径数组 |
| `timeout_seconds` | 正整数 |
测试角色(`test_run` / `acceptance_check`)另需:
- `approved_commands`:argv 数组的数组(如 `[["go","test","-short","./"]]`)
- `plan_digest`:外部已批准计划的 SHA-256 hex
禁止静默 `model_override`;临时改模型写任务 override 并说明用户来源。
## 租约与输出
- 资源:`path:.doc/test-reports/assets/pi-subagents/<task_id>/<request_id>`
- 产物:`receipt.json`、`report.md`、命令日志(同目录)
- 裸调 `run`(无租约)被拒绝
## 状态分离(红线)
| 字段 | 含义 |
|---|---|
| `run_status` | running / succeeded / failed / cancelled / unknown |
| `verdict` | pass / changes_required / blocked / not_applicable |
| `review_state` | 阶段门禁;模型 succeeded **仅** pending |
测试 `commands_passed`/`pass` 只针对批准命令证据,不能直接赋给任务 `acceptance`。
## 常见拒绝码(执行器权威)
| 码 | 处置 |
|---|---|
| `INVALID_SOURCE_SHA` | 换完整提交 hash |
| `APPROVED_COMMANDS_REQUIRED` | 补 approved_commands + plan_digest |
| `MINIMAX_ROLE_FORBIDDEN` | 不把 M3 用于关键角色 |
| `INPUT_NOT_TRACKED` / `PATH_OUT_OF_SCOPE` | 收窄 input_files |
| `CONCURRENCY_LIMIT` | 串行或等空闲槽 |
| `ORPHAN_RUNNING` | 先核进程/副作用,勿盲目重派 |
Cursor hooks 对形状错误可能 `permission=ask`(advisory);**硬拒绝以执行器为准**。
## 禁止
- 把 implementation 派给 PI
- 自动换模型 / 伪造 lease / 递归把本修复再派给 PI worker
- 用模型叙事冒充 PASS;审查结论须带回报告路径与 receipt 字段
@@ -0,0 +1,56 @@
---
name: subagent-stage-record
description: Record PI/Cursor stage handoffs using the subagent stage template (dispatch packet, return, S0–S9 receipt, R-AC-T mapping). Use after PI results, before gate advances, or when closing a multi-stage task.
---
# Subagent 阶段记录
权威模板:[subagent-stage-record.md](../../../.doc/ops/templates/subagent-stage-record.md);流程:[协作规范](../../../.doc/ops/2026-09-24-subagent-workflow-standard.md)。派发入口:[pi-subagent-dispatch](../pi-subagent-dispatch/SKILL.md)。
原则:**一步一记录,不必一步一文件**。小任务可把适用段落并入任务主文档;跨模块才拆独立审查/测试/验收证据。
## 何时写
- 每次 `pi_subagent_run` 前后:派发包 + 回传
- 阶段门禁推进前:S0–S9 收据行
- 收口前:R→AC→改动→审查→T→运行验收映射
## 派发包(最小字段)
复制到任务文档并填实值:
- `parent_task_id` / `subtask_id` / `stage_id` / `role_id`
- 配置路径 / `config_revision` / `config_sha256`(来自 roles 或 receipt)
- 解析后的 provider/model;任务 override 与用户来源
- 目标、非目标、授权来源、交付终点
- 输入版本:R-ID / AC-ID / 方案版本 / `base_sha` / `head_sha` 或 `source_sha`
- cwd / branch / allowed_paths / 只读或写入 / mutex
- 完成条件、输出路径、预算与异常处理
## 子任务回传
- PI `run_id` / producer / 起止时间
- `configured_model` / `actual_model`(不可观察填 `unknown`)
- `run_status` 与 `verdict` **分列**(不得把退出成功当质量通过)
- 结论与依据、证据链接、脱敏范围
- 未覆盖项、风险、遗留进程/产物、接管建议
只读子 Agent 结论由主 Agent 转存时:保留 `agent_id` 与原结论,注明「由主 Agent 转存」。
## 阶段收据表
| 阶段 | 输入版本 | 产物/证据 | producer | reviewer/独立性 | 决定 | 下一步 |
|---|---|---|---|---|---|---|
| S0–S9 适用项 | | | | independent / self_review / not_required | pass / changes_required / blocked / not_applicable | |
未执行阶段写 `not_applicable` + 原因。报告存在不自动满足门禁。
## 需求追踪
| R-ID | AC-ID | 改动/版本 | 审查 | T-ID / 结果 | 运行验收 | acceptance_owner |
|---|---|---|---|---|---|---|
| | | | | | | |
## 与任务五态
继续用任务模板的 `status`、`review_state`、`integration_state`、`deployment`、`cleanup`、`remote_sync`;额外记 `phase`、`acceptance`、待回收 `subtask_id`。`done` 取决于授权交付终点,不因会话结束自动完成。
@@ -0,0 +1,41 @@
---
issue_id: ISSUE-20260924-cursor-hook-event-contract
issue_state: open
severity: medium
owner: Cursor
task_ids: [cursor-governance-hooks-2026-09-24]
created: 2026-09-24
occurred_at: 2026-09-24
updated: 2026-09-24
recurrence_count: 0
evidence_basis: current
lesson_ids: [LESSON-20260924-cursor-hook-advisory-gates]
---
# Cursor hook 事件字段契约未文档化
## 事实与证据
预期:`beforeMCPExecution` / `sessionStart` / `stop` 的 stdin 字段名与 matcher 匹配串在官方文档中明确,便于 failClosed 硬门禁。
实际:官方 create-hook skill 仅明确 `beforeShellExecution` 的 `command` 字段;`beforeMCPExecution` 不支持 `updated_input`;`sessionStart` 无文档化返回字段可注入上下文;`MCP: ...` matcher 形式只写给 `preToolUse` 族。实现层字符串探测存在 `mcp_server_name`/`tool_name`/`tool_input`,但非文档契约。
证据:本任务调研记录;单元测试用多名容错驱动;真实 Cursor IDE 事件采样待 `CURSOR_HOOKS_PROBE=1`。
## 根因与同类范围
客户端 hook API 文档滞后于实现。同类:任何依赖未文档化 hook 输入的 failClosed 门禁都有假阴性/假阳性风险。
## 修复思路与过程
| 日期 | 动作 | 结果 | 下一步 |
|---|---|---|---|
| 2026-09-24 | 落地 advisory hooks + 多名容错 + probe 开关;异形 fail-open | 单元回归 PASS | 用户触发真实 MCP/shell 事件采样后收紧 |
## 验收与关闭
- 静态/单元:已覆盖(见测试报告)
- 实机:打开 `CURSOR_HOOKS_PROBE=1`,触发一次 `pi_subagent_roles` 与一次 shell,归档 `.cursor/hook-probe.jsonl` 片段到测试报告 assets;确认字段名后可将 matcher/`failClosed` 收紧并将本 issue → verifying/resolved
## 经验与复发
见 [LESSON-20260924-cursor-hook-advisory-gates](../ops/lessons/2026-09-24-cursor-hook-advisory-gates.md)。
+4 -3
View File
@@ -2,7 +2,7 @@
维护规则:[问题记录与开发经验闭环](../ops/2026-09-23-issue-learning-workflow.md)。每个问题一个稳定 ID;主记录是事实与状态来源,本表为可检索投影。新发现及时登记,修复后保留记录;发布 blocker 仍见 [gap-matrix](../remediation/gap-matrix.md)。
更新:2026-09-24。登记管理员重置密码死功能、登录信封误导、无凭据僵尸账号;历史 resolved 不表示本日运行回归通过。
更新:2026-09-24。Cursor 治理层:测试规则漂移/MCP 路径改为 verifying;新登记 hook 事件契约 issue;历史 resolved 不表示本日产品回归通过。
| issue_id | 模块 / 检索词 | 问题 | 严重度 | issue_state | owner | 证据日期 / 范围 | 唯一主记录 | 经验 |
|---|---|---|---|---|---|---|---|---|
@@ -13,6 +13,7 @@
| ISSUE-20260924-admin-reset-dead | auth / adminResetPassword / T20-F4 / chenwei | 管理台重置密码死按钮;验收口令不可知后无法恢复 | high | resolved | Cursor | 2026-09-24;test 库实查 + API 单测 | [主记录](2026-09-24-admin-password-reset-issues.md#issue-20260924-admin-reset-dead) | 待补 LESSON |
| ISSUE-20260924-login-error-envelope | auth / AUTHZ / 无权访问 / auth.ts | 登录失败展示裸 JSON / 「无权访问」误导 | medium | resolved | Cursor | 2026-09-24;wire 复现 + 前端单测 | [主记录](2026-09-24-admin-password-reset-issues.md#issue-20260924-login-error-envelope) | 待补 LESSON |
| ISSUE-20260924-zombie-credentials | auth / user_credentials / LEFT JOIN / CreateUser | 空哈希静默建号导致永久无法登录 | high | resolved | Cursor | 2026-09-24;三环境只读盘点;机制修复不含存量清理 | [主记录](2026-09-24-admin-password-reset-issues.md#issue-20260924-zombie-credentials) | 待补 LESSON |
| ISSUE-20260924-cursor-test-rule-drift | Cursor / test-reports / 固定模型 / 规则副本 | Cursor 测试规则与验证契约存在固定模型及范围冲突 | medium | open | Codex(研究) | 2026-09-24;静态规则比对,客户端行为未复现 | [主记录](2026-09-24-subagent-workflow-research.md#4-issue-20260924-cursor-test-rule-drift) | 未修复验证,不新增 validated 经验 |
| ISSUE-20260924-cursor-test-rule-drift | Cursor / test-reports / 固定模型 / 规则副本 | Cursor 测试规则与验证契约存在固定模型及范围冲突 | medium | verifying | Cursor | 2026-09-24;规则文本已缩为验证契约指针;客户端实际读取行为待实机 | [主记录](2026-09-24-subagent-workflow-research.md#4-issue-20260924-cursor-test-rule-drift) | [LESSON-20260924-cursor-hook-advisory-gates](../ops/lessons/2026-09-24-cursor-hook-advisory-gates.md) |
| ISSUE-20260924-pi-executor-boundaries | PI executor / 路径规范化 / MCP / 子进程 / 幂等 | 未发布初版只读边界与执行可靠性缺陷 | high | resolved | Cursor/Codex | 2026-09-24;173 工程回归与真实 Cursor CLI/MCP/PI | [主记录](2026-09-24-pi-executor-boundaries.md) | [工具路径与输入隔离](../ops/lessons/2026-09-24-pi-tool-boundaries.md) |
| ISSUE-20260924-cursor-cli-mcp-path | Cursor CLI / MCP / workspaceFolder | 既有 agentctl/jev 项目 MCP 入口在本机 CLI 连接失败 | medium | deferred | 工程维护 | 2026-09-24;CLI list/list-tools,PI 入口已修复 | [主记录](2026-09-24-pi-executor-boundaries.md#cursor-cli-mcp-path) | 沿用工具路径经验 |
| ISSUE-20260924-cursor-cli-mcp-path | Cursor CLI / MCP / workspaceFolder | 既有 agentctl/jev 项目 MCP 入口在本机 CLI 连接失败 | medium | verifying | Cursor | 2026-09-24;mcp.json 已改相对路径;IDE/CLI list 证据待补 | [主记录](2026-09-24-pi-executor-boundaries.md#cursor-cli-mcp-path) | [LESSON-20260924-cursor-hook-advisory-gates](../ops/lessons/2026-09-24-cursor-hook-advisory-gates.md) |
| ISSUE-20260924-cursor-hook-event-contract | Cursor / hooks / beforeMCPExecution / probe | hook 事件 stdin 字段名与 matcher 官方未文档化 | medium | open | Cursor | 2026-09-24;单元容错已落地;实机 probe 待采样 | [主记录](2026-09-24-cursor-hook-event-contract.md) | [LESSON-20260924-cursor-hook-advisory-gates](../ops/lessons/2026-09-24-cursor-hook-advisory-gates.md) |
+9
View File
@@ -52,6 +52,15 @@ CURRENT 是带时间的活跃协调快照;Git/机器状态用于核验实际
用户要求研究 Cursor Auto 下的 subagent 分工、阶段流程及报告,授权与本次验收见[任务](short-term/2026-09-24-subagent-workflow-standard.md)。[规范草案](2026-09-24-subagent-workflow-standard.md)为拟议行为的唯一来源,[研究报告](../audit/2026-09-24-subagent-workflow-research.md)给出事实和待修冲突,[模板](templates/subagent-stage-record.md)承载交接。
用户后续明确采用 Cursor Auto 实施 + gmktec 本机 PI 子任务,并批准默认模型分工及可调整文档。[模型配置](2026-09-24-subagent-model-config.md)为模型路由唯一来源,decision_state=approved;流程整体仍 proposed;已批准的模型配置与执行器已合入本地 dev。原 Cursor 原生角色/inherit 方案被替代。执行器已在任务分支实现并验证 Cursor CLI→MCP→PI,代码 69ffaa677e23a1dd9aa0528bb09b3acfcd38b119;[运行说明](2026-09-24-pi-subagent-executor.md)与[验证报告](../test-reports/2026-09-24-pi-executor-test-report.md)限定已验证的机器边界。dev 集成/完整流程审批仍独立记录;不以派发器可用宣称全阶段自动门禁。
## Cursor 治理层局部修订
修订 ID:`cursor-governance-2026-09-24-r1`;owner:`Cursor`;state:合入 local_dev 后 `active`(effective_commit 以集成收据为准);工具能力分层:
- 规则/skill 文本:`manual`(Agent 读取执行)
- hook 脚本 stdin→stdout 行为:`enforced`(`scripts/tests/test_cursor_hooks.py`)
- hook ↔ Cursor IDE 接线:`advisory`/`manual`(事件字段待 probe 采样;见 ISSUE-20260924-cursor-hook-event-contract)
用户授权:在 PI 执行器合入后补齐 skills/rules/hooks;范围与验收见[任务](short-term/2026-09-24-cursor-governance-hooks.md)。客户端入口:`.cursor/rules/cursor-hooks.mdc`、`pi-subagents.mdc`、`test-reports-standard.mdc`(缩为验证契约指针);skills `pi-subagent-dispatch` / `subagent-stage-record`;hooks 见 `.cursor/hooks.json`。不改变产品 Runner、不扩大执行器硬门禁语义。
## 治理目录库与需求重叠检测拟议修订
修订 ID:`governance-catalog-2026-09-24-r1`;owner:`Cursor`;state:`proposed`;能力:schema/门禁落地后为 `enforced`,向量层 `fail-open`;effective_commit:无(随受控合入记录)。
+1
View File
@@ -18,6 +18,7 @@
| owner | task | branch | worktree | mutex | status |
|---|---|---|---|---|---|
| Cursor | cursor-governance-hooks-2026-09-24 | feature/cursor-governance-hooks-2026-09-24 | .worktrees/cursor-governance-hooks-2026-09-24 | cursor-governance | in_progress |
| Cursor | governance-catalog-2026-09-24 | feature/governance-catalog-2026-09-24 | .worktrees/governance-catalog-2026-09-24 | agent-workflow-governance+catalog | done |
| Codex | subagent-workflow-standard-2026-09-24-land | feature/subagent-workflow-standard-2026-09-24 | .worktrees/subagent-workflow-standard-2026-09-24 | pi-subagent-integration | done |
| Cursor/Codex | subagent-workflow-standard-2026-09-24-executor | feature/subagent-workflow-standard-2026-09-24 | .worktrees/subagent-workflow-standard-2026-09-24 | pi-subagent-executor | done |
@@ -0,0 +1,28 @@
---
lesson_id: LESSON-20260924-cursor-hook-advisory-gates
lesson_state: candidate
owner: Cursor
reviewer: Cursor(自审;实机采样后转 validated)
last_reviewed: 2026-09-24
reviewed_revision: cursor-governance-hooks-2026-09-24 worktree
source_issues:
- ISSUE-20260924-cursor-hook-event-contract
- ISSUE-20260924-cursor-test-rule-drift
- ISSUE-20260924-cursor-cli-mcp-path
keywords: [Cursor, hooks, advisory, fail-open, probe, stdin, MCP]
---
# 客户端 hook 与执行器门禁分层;契约未知时 fail-open
- 触发条件:为 Cursor 增加 hooks/rules,或发现 hook「写了但不生效/误拦」时。
- 机制与来源:执行器(`pi_subagents.py` / agentctl)已有权威硬拒绝;hook 事件字段官方未完全文档化。heredoc 包装 Python 会吞掉 hook stdin。审计日志若放进被 git 跟踪的 `.cursor/hooks/` 会污染仓库。
- 开发检查:
1. hook 只做形状/授权类 `ask`,不复刻执行器全部校验。
2. 用独立 `.py` + 薄 `.sh` `exec`,禁止 heredoc 占 stdin。
3. 多名容错(`tool_name|mcp_tool_name|name` 等);识别失败 → allow + `UNRECOGNIZED_INPUT` 审计。
4. 审计/probe 写 `.cursor/hook-audit.jsonl` / `hook-probe.jsonl`(被 `.cursor/*` ignore)。
5. 规则副本只做指针,不复制验证契约中的模型名与全量命令。
- 验证方式:`scripts/tests/test_cursor_hooks.py`;实机 `CURSOR_HOOKS_PROBE=1` 采样后升 validated。
- 适用边界:本仓库项目级 hooks;不声称覆盖所有 Cursor 版本或 CLI 加载行为。
- 防护落实:单元回归 enforced;hook↔Cursor 接线仍 advisory/manual。
- 修订与失效:实机字段确认后收紧 matcher 并更新本条状态。
+1
View File
@@ -8,5 +8,6 @@
| LESSON-20260923-content-hash | docrevision、canonicalize、hash、bootstrap、hard-break、404、水合 | 保持文本语义并检查账本/文件一致性;新数据通过不能覆盖旧数据迁移 | validated | [正文规范化与投影](2026-09-23-content-hash.md);2026-09-23 |
| LESSON-20260923-matrix-status-cas | FEATURE-MATRIX、docs-apply、record_id、merge conflict、CAS | 矩阵/CURRENT 行变更走 docs apply;合并后核验状态列未被回退 | validated | [矩阵状态 CAS](2026-09-23-matrix-status-cas.md);2026-09-23 |
| LESSON-20260924-pi-tool-boundaries | PI、MCP、readonly、path、snapshot、process-group | 路径语义与快照边界一致,合法路径和拒绝路径都验收 | validated | [文件工具边界](2026-09-24-pi-tool-boundaries.md);2026-09-24 |
| LESSON-20260924-cursor-hook-advisory-gates | Cursor、hooks、advisory、fail-open、probe、stdin | hook 与执行器分层;契约未知 fail-open;审计日志放 ignore 路径 | candidate | [客户端 hook 门禁](2026-09-24-cursor-hook-advisory-gates.md);2026-09-24 |
validated 的证据范围见各条目;本日复核为历史材料和源码阅读,未重跑产品回归。后续按机制补充,过时条目标 superseded/retired 并保留替代链接。
@@ -0,0 +1,80 @@
---
title: "Cursor 治理层扩展(rules / skills / hooks)"
task_id: cursor-governance-hooks-2026-09-24
status: in_progress
created: 2026-09-24
updated: 2026-09-24
owner: Cursor
task_type: implementation
task_tier: multi_stage
policy_revision: "agent-governance-2026-09-21-r1 + issue-learning-2026-09-23-r1 + codegraph-governance-2026-09-24-r1 + subagent-workflow-2026-09-24-r2(proposed); base_dev@cfc80b34a40e8de78e77a44839571bb684d405d5"
branch: feature/cursor-governance-hooks-2026-09-24
worktree: .worktrees/cursor-governance-hooks-2026-09-24
base_dev_sha: cfc80b34a40e8de78e77a44839571bb684d405d5
write_scope: [".cursor", "scripts/tests", ".doc"]
allowed_environments: []
mutex: cursor-governance
review_state: pending
integration_state: pending
deployment: not_required
cleanup: pending
cleanup_owner: "owner 退出后的独立维护者"
cleanup_trigger: "合入 local_dev、证据归档且 owner 退出后复查"
remote_sync: not_requested
---
# Cursor 治理层扩展(rules / skills / hooks)
## 用户目标与授权
- 用户目标:在已合入的 PI subagent 执行器基础上,为本项目 Cursor 补齐 skills、rules、hooks 等治理层。
- 授权来源:本会话确认计划 `cursor_governance_layer`,范围含 7 项全选、hook 命中一律 `ask`、session+postToolUse 双轨、字段识别 both_phases。
- 交付终点:任务分支实现并通过工程回归;受控合入 local `dev`;不 push;不重建产品服务。
- 禁止:未授权 push / 晋升 test/main;改产品 Runner;以 stub 冒充产品 PASS;覆盖他人 CURRENT/MATRIX。
## 范围与非目标
- 允许:`.cursor/rules|skills|hooks|mcp.json`、`scripts/tests/test_cursor_hooks.py`、相关 `.doc`(任务/台账/经验/规范登记/测试报告)。
- 非目标:改 `scripts/pi_subagents.py` 硬门禁语义;实现 S0–S9 自动状态机;产品 Go/前端/PG。
## 相关问题与经验
- ISSUE-20260924-cursor-test-rule-drift(open → verifying)
- ISSUE-20260924-cursor-cli-mcp-path(deferred → verifying)
- 新建 ISSUE-20260924-cursor-hook-event-contract(open)
- 采纳 LESSON-20260923-matrix-status-cas、LESSON-20260924-pi-tool-boundaries;新建 LESSON-20260924-cursor-hook-advisory-gates(candidate)
## 需求与验收
| R-ID | 要求 | AC-ID |
|---|---|---|
| R1 | 修复测试规则漂移 | AC1:无固定模型/无条件全量 Go/失效 §8;指向验证契约 |
| R2 | PI 派发 skill | AC2:roles→run→result、租约、拒绝码、run_status≠verdict |
| R3 | 阶段记录 skill | AC3:派发包/回传/收据/R-AC-T 可执行 |
| R4 | PI/会话/收据 hooks | AC4:ask;fail-open 异形;probe 开关;单元可测 |
| R5 | MCP 路径 | AC5:agentctl/jev 相对路径;CLI 证据或 verifying |
| R6 | rules 收敛与登记 | AC6:薄指针 + policy-registry 修订 |
| R7 | 工程回归 | AC7:unittest discover 全绿;文档静态检查;报告归档 |
## 步骤与验收
1. [ ] 建场与认领
2. [ ] 规则漂移修复
3. [ ] 两 skill
4. [ ] 四 hook + hooks.json
5. [ ] MCP 路径
6. [ ] rules 收敛
7. [ ] 测试与全量回归
8. [ ] ISSUES/lesson/policy-registry
9. [ ] 测试报告
10. [ ] probe 独立验收(需用户配合;未采样前 advisory)
11. [ ] 受控合入 local_dev
## 验证与证据
见后续测试报告与本记录更新。产品链路 NOT REQUIRED。
## 相关链接
- 计划:用户确认的 Cursor governance layer
- 权威:`.doc/ops/2026-09-24-subagent-model-config.md` · `2026-09-24-subagent-workflow-standard.md` · `2026-09-24-pi-subagent-executor.md` · `2026-09-21-validation-contract.md`
@@ -0,0 +1,79 @@
# Cursor 治理层扩展(rules / skills / hooks)测试报告
## 一、 测试背景 (Background)
PI subagent 执行器已合入 local `dev`(`cfc80b34`)。本任务补齐 Cursor 客户端治理层:修复 `test-reports-standard.mdc` 规则漂移、新增派发/阶段记录 skills、新增 advisory hooks、修正 agentctl/jev MCP 相对路径,并将能力分层登记进规范登记表。
## 二、 测试目标 (Test Objectives)
| AC | 目标 | 判定 |
|---|---|---|
| AC1 | 规则无硬编码模型/无条件全量 Go/失效 §8 | PASS(静态 grep) |
| AC2–AC3 | 两 skill 存在且指向权威文档 | PASS |
| AC4 | hooks ask + fail-open + probe;单元可测 | PASS(29 项) |
| AC5 | mcp.json agentctl/jev 相对路径 | PASS(结构);CLI 显示 registered/needs approval(未宣称 Connection 已绿) |
| AC6 | rules 收敛 + policy-registry 修订 | PASS |
| AC7 | unittest discover 全绿 | PASS(202) |
## 三、 测试方案与环境 (Test Plan & Environment)
- OS:Linux;Python 3.14;工作区 `.worktrees/cursor-governance-hooks-2026-09-24`
- 基线:`cfc80b34a40e8de78e77a44839571bb684d405d5`
- 层级:工程脚本单元 + 文档静态;**产品 Go/前端/PG/真实产品 Runner:NOT REQUIRED**(无产品代码变更)
## 四、 预期结果 (Expected Results)
- hook 合法放行;形状错误/裸跑/`pkill -f` → `permission=ask`
- 异形 JSON → allow + `UNRECOGNIZED_INPUT`
- 全量 `scripts/tests` 无回归
## 五、 实际测试结果与判定 (Actual Results & Evaluation)
### 5.1 Cursor hooks 专项
命令:`python3 -B -m unittest scripts.tests.test_cursor_hooks -v`
- 29 tests OK(结构、guard-git、guard-pi-dispatch、guard-pi-shell、receipt-context、audit-session、skills)
- 证据:[unittest-discover.txt](assets/2026-09-24-cursor-governance-hooks/unittest-discover.txt)
### 5.2 全量工程回归
命令:`python3 -B -m unittest discover -s scripts/tests -p 'test_*.py' -v`
- **Ran 202 tests in 111.351s — OK**(原 173 + 新增 29)
- 判定:PASS
### 5.3 文档/规则静态
- `grep` rules:无 `MiniMax-M2.7-highspeed`、无 `PROJECT.md) §8`、无 `go test -v ./...` → PASS
- `git diff --check`:PASS
- `agentctl docs drift`:`status=ok`,`regressions=[]`
### 5.4 MCP 路径
- `.cursor/mcp.json` 中 agentctl/jev/pi-subagents 均使用 `scripts/mcp/...` 相对路径
- CLI `agent mcp list`:四服务均显示 `not loaded (needs approval)`(已注册;加载需用户在客户端批准)。**不宣称已消除 Connection failed**;issue 保持 verifying
### 5.5 IDE probe(独立验收)
状态:`pending_ide_sample` — 见 [probe-status.json](assets/2026-09-24-cursor-governance-hooks/probe-status.json)
用户步骤:
1. 重载项目 MCP / Hooks
2. `export CURSOR_HOOKS_PROBE=1`(或在 Cursor 环境中设置)
3. 触发一次 `pi_subagent_roles` 与任意 shell
4. 将 `.cursor/hook-probe.jsonl` 脱敏片段归档到本 assets,再收紧 matcher
## 六、 自动化测试套件汇总 (Test Suite Execution)
| 套件 | 结果 |
|---|---|
| `unittest discover scripts/tests` | 202 OK |
| 产品 PI / Go 全量 | NOT REQUIRED |
## 七、 测试结论与后续优化建议 (Conclusion & Next Steps)
达到授权交付终点(工程治理层合入 local_dev)。后续:IDE probe 采样后关闭/推进 hook 契约 issue;CLI 批准 MCP 后复核 agentctl/jev;lesson 由 candidate → validated。
相关:任务 [2026-09-24-cursor-governance-hooks](../ops/short-term/2026-09-24-cursor-governance-hooks.md);ISSUE hook 契约 / 规则漂移 / MCP 路径。
@@ -0,0 +1,22 @@
{
"status": "pending_ide_sample",
"how_to": "CURSOR_HOOKS_PROBE=1 then trigger pi_subagent_roles and any shell; collect .cursor/hook-probe.jsonl",
"unit_samples": [
{
"event": "beforeShellExecution",
"synthetic_payload_keys": [
"command"
],
"note": "unit-driven; IDE probe pending"
},
{
"event": "beforeMCPExecution",
"synthetic_payload_keys": [
"mcp_server_name",
"tool_name",
"tool_input"
],
"note": "unit-driven; IDE probe pending"
}
]
}
@@ -0,0 +1,279 @@
test_linux_permission_error_path_remains_fail_closed (test_agent_worktree.ProcessVisibilityTest.test_linux_permission_error_path_remains_fail_closed) ... ok
test_unknown_same_uid_inaccessible_cwd_is_not_skipped (test_agent_worktree.ProcessVisibilityTest.test_unknown_same_uid_inaccessible_cwd_is_not_skipped) ... ok
test_branch_delete_refusal_preserves_branch_without_force_with_visibility_fixture (test_agent_worktree.WorktreeTest.test_branch_delete_refusal_preserves_branch_without_force_with_visibility_fixture) ... ok
test_cleanup_actual_branch_mapping_with_visibility_fixture (test_agent_worktree.WorktreeTest.test_cleanup_actual_branch_mapping_with_visibility_fixture) ... ok
test_cleanup_legacy_mapping_and_nonforced_branch_delete_with_visibility_fixture (test_agent_worktree.WorktreeTest.test_cleanup_legacy_mapping_and_nonforced_branch_delete_with_visibility_fixture) ... ok
test_cleanup_lifecycle_lock_prevents_a_new_run (test_agent_worktree.WorktreeTest.test_cleanup_lifecycle_lock_prevents_a_new_run) ... ok
test_cleanup_refuses_active_registry_operation_even_with_other_cwd (test_agent_worktree.WorktreeTest.test_cleanup_refuses_active_registry_operation_even_with_other_cwd) ... ok
test_cleanup_refuses_live_process_cwd (test_agent_worktree.WorktreeTest.test_cleanup_refuses_live_process_cwd) ... ok
test_cleanup_refuses_tracked_untracked_and_ignored_evidence (test_agent_worktree.WorktreeTest.test_cleanup_refuses_tracked_untracked_and_ignored_evidence) ... ok
test_cleanup_refuses_unfinished_git_operation (test_agent_worktree.WorktreeTest.test_cleanup_refuses_unfinished_git_operation) ... ok
test_cleanup_refuses_unmerged_commit (test_agent_worktree.WorktreeTest.test_cleanup_refuses_unmerged_commit) ... ok
test_create_from_feature_uses_current_dev_and_canonical_main (test_agent_worktree.WorktreeTest.test_create_from_feature_uses_current_dev_and_canonical_main) ... ok
test_create_with_main_on_unrelated_branch_uses_dev (test_agent_worktree.WorktreeTest.test_create_with_main_on_unrelated_branch_uses_dev) ... ok
test_forged_inherited_lease_is_refused (test_agent_worktree.WorktreeTest.test_forged_inherited_lease_is_refused) ... ok
test_inherited_worktree_lease_does_not_double_lock (test_agent_worktree.WorktreeTest.test_inherited_worktree_lease_does_not_double_lock) ... ok
test_list_is_read_only_without_init (test_agent_worktree.WorktreeTest.test_list_is_read_only_without_init) ... ok
test_rejects_traversal_nested_and_protected_branches (test_agent_worktree.WorktreeTest.test_rejects_traversal_nested_and_protected_branches) ... ok
test_same_task_id_and_existing_branch_do_not_collide (test_agent_worktree.WorktreeTest.test_same_task_id_and_existing_branch_do_not_collide) ... ok
test_stable_lock_refuses_concurrent_writer (test_agent_worktree.WorktreeTest.test_stable_lock_refuses_concurrent_writer) ... ok
test_symlink_parent_or_target_is_refused (test_agent_worktree.WorktreeTest.test_symlink_parent_or_target_is_refused) ... ok
test_write_requires_explicit_init (test_agent_worktree.WorktreeTest.test_write_requires_explicit_init) ... ok
test_admin_document_cannot_bypass_done_with_compact_cells (test_agentctl.AgentctlTests.test_admin_document_cannot_bypass_done_with_compact_cells) ... ok
test_admin_document_cannot_overwrite_human_sections (test_agentctl.AgentctlTests.test_admin_document_cannot_overwrite_human_sections) ... ok
test_admin_document_cannot_overwrite_integration_slot (test_agentctl.AgentctlTests.test_admin_document_cannot_overwrite_integration_slot) ... ok
test_admin_document_cas_preserves_slot_and_needs_capability (test_agentctl.AgentctlTests.test_admin_document_cas_preserves_slot_and_needs_capability) ... ok
test_admin_full_document_requires_explicit_admin_capability (test_agentctl.AgentctlTests.test_admin_full_document_requires_explicit_admin_capability) ... ok
test_claim_blocks_task_close_until_released (test_agentctl.AgentctlTests.test_claim_blocks_task_close_until_released) ... ok
test_claim_concurrent_twenty_exactly_one_held (test_agentctl.AgentctlTests.test_claim_concurrent_twenty_exactly_one_held) ... ok
test_claim_frozen_rejects_even_holder (test_agentctl.AgentctlTests.test_claim_frozen_rejects_even_holder) ... ok
test_claim_parent_child_overlap_and_sibling_ok (test_agentctl.AgentctlTests.test_claim_parent_child_overlap_and_sibling_ok) ... ok
test_claim_request_id_idempotent_and_reuse_guard (test_agentctl.AgentctlTests.test_claim_request_id_idempotent_and_reuse_guard) ... ok
test_claim_unrelated_resources_parallel_ok (test_agentctl.AgentctlTests.test_claim_unrelated_resources_parallel_ok) ... ok
test_commit_lease_must_cover_staged_files_not_only_task_scope (test_agentctl.AgentctlTests.test_commit_lease_must_cover_staged_files_not_only_task_scope) ... ok
test_daemon_does_not_grant_register_or_docs_apply (test_agentctl.AgentctlTests.test_daemon_does_not_grant_register_or_docs_apply) ... ok
test_daemon_single_instance_lock_busy_with_holder_pid (test_agentctl.AgentctlTests.test_daemon_single_instance_lock_busy_with_holder_pid) ... ok
test_daemon_stop_leaves_pending_queue_untouched (test_agentctl.AgentctlTests.test_daemon_stop_leaves_pending_queue_untouched) ... ok
test_descendant_resource_guard_accepts_only_held_resource (test_agentctl.AgentctlTests.test_descendant_resource_guard_accepts_only_held_resource) ... ok
test_docs_cannot_launder_unmanaged_other_task_changes (test_agentctl.AgentctlTests.test_docs_cannot_launder_unmanaged_other_task_changes) ... ok
test_docs_foreign_current_row_is_rejected (test_agentctl.AgentctlTests.test_docs_foreign_current_row_is_rejected) ... ok
test_docs_multi_file_preflight_failure_has_no_partial_writes (test_agentctl.AgentctlTests.test_docs_multi_file_preflight_failure_has_no_partial_writes) ... ok
test_docs_project_parallel_apply_keeps_both_rows (test_agentctl.AgentctlTests.test_docs_project_parallel_apply_keeps_both_rows) ... ok
test_docs_project_preview_apply_receipts_and_projections (test_agentctl.AgentctlTests.test_docs_project_preview_apply_receipts_and_projections) ... ok
test_docs_project_same_row_race_base_changed_no_partial (test_agentctl.AgentctlTests.test_docs_project_same_row_race_base_changed_no_partial) ... ok
test_docs_row_cas_preserves_unrelated_rows_and_is_idempotent (test_agentctl.AgentctlTests.test_docs_row_cas_preserves_unrelated_rows_and_is_idempotent) ... ok
test_env_guard_accepts_clean_main_and_requires_all_target_leases (test_agentctl.AgentctlTests.test_env_guard_accepts_clean_main_and_requires_all_target_leases) ... ok
test_env_guard_refuses_linked_worktree_even_with_capability (test_agentctl.AgentctlTests.test_env_guard_refuses_linked_worktree_even_with_capability) ... ok
test_failed_child_releases_lease_and_preserves_failure (test_agentctl.AgentctlTests.test_failed_child_releases_lease_and_preserves_failure) ... ok
test_failed_revalidation_revokes_old_success_receipt (test_agentctl.AgentctlTests.test_failed_revalidation_revokes_old_success_receipt) ... ok
test_guard_build_requires_exact_clean_commit (test_agentctl.AgentctlTests.test_guard_build_requires_exact_clean_commit) ... ok
test_hook_checks_reject_out_of_scope_and_secret_paths (test_agentctl.AgentctlTests.test_hook_checks_reject_out_of_scope_and_secret_paths) ... ok
test_hook_rejects_shared_document_without_publish_receipt (test_agentctl.AgentctlTests.test_hook_rejects_shared_document_without_publish_receipt) ... ok
test_hooks_allow_published_shared_document_receipt (test_agentctl.AgentctlTests.test_hooks_allow_published_shared_document_receipt) ... ok
test_hooks_allow_scoped_task_commit_under_live_lease (test_agentctl.AgentctlTests.test_hooks_allow_scoped_task_commit_under_live_lease) ... ok
test_hooks_refuse_overwriting_foreign_hook (test_agentctl.AgentctlTests.test_hooks_refuse_overwriting_foreign_hook) ... ok
test_init_cannot_steal_live_legacy_lock (test_agentctl.AgentctlTests.test_init_cannot_steal_live_legacy_lock) ... ok
test_init_preserves_existing_legacy_lock_inode (test_agentctl.AgentctlTests.test_init_preserves_existing_legacy_lock_inode) ... ok
test_installed_hooks_reject_direct_dev_commit (test_agentctl.AgentctlTests.test_installed_hooks_reject_direct_dev_commit) ... ok
test_interrupted_validation_stops_child_before_releasing_scope (test_agentctl.AgentctlTests.test_interrupted_validation_stops_child_before_releasing_scope) ... ok
test_invalid_document_request_shape_has_no_write_side_effects (test_agentctl.AgentctlTests.test_invalid_document_request_shape_has_no_write_side_effects) ... ok
test_linked_worktree_uses_only_main_coordination_root (test_agentctl.AgentctlTests.test_linked_worktree_uses_only_main_coordination_root) ... ok
test_live_and_stale_credentials_cannot_be_spoofed (test_agentctl.AgentctlTests.test_live_and_stale_credentials_cannot_be_spoofed) ... ok
test_live_run_cannot_be_recovered_or_task_closed (test_agentctl.AgentctlTests.test_live_run_cannot_be_recovered_or_task_closed) ... ok
test_matrix_done_without_evidence_refused (test_agentctl.AgentctlTests.test_matrix_done_without_evidence_refused) ... ok
test_matrix_duplicate_or_rename_record_id_rejected (test_agentctl.AgentctlTests.test_matrix_duplicate_or_rename_record_id_rejected) ... ok
test_matrix_record_id_layout_migration_without_new_dones (test_agentctl.AgentctlTests.test_matrix_record_id_layout_migration_without_new_dones)
Additive record_id column must not trip EVIDENCE_REQUIRED when statuses unchanged. ... ok
test_matrix_unknown_columns_and_lineage_preserved (test_agentctl.AgentctlTests.test_matrix_unknown_columns_and_lineage_preserved) ... ok
test_multi_resource_acquisition_is_atomic (test_agentctl.AgentctlTests.test_multi_resource_acquisition_is_atomic) ... ok
test_parent_child_path_conflict_and_disjoint_coexistence (test_agentctl.AgentctlTests.test_parent_child_path_conflict_and_disjoint_coexistence) ... ok
test_path_scope_and_capability_are_enforced (test_agentctl.AgentctlTests.test_path_scope_and_capability_are_enforced) ... ok
test_policy_invalid_json_rejects_write (test_agentctl.AgentctlTests.test_policy_invalid_json_rejects_write) ... ok
test_policy_missing_rejects_write_but_status_stays_readonly (test_agentctl.AgentctlTests.test_policy_missing_rejects_write_but_status_stays_readonly) ... ok
test_policy_protocol_too_low_rejects_write (test_agentctl.AgentctlTests.test_policy_protocol_too_low_rejects_write) ... ok
test_quarantine_sweep_keeps_live_child_and_release_refuses (test_agentctl.AgentctlTests.test_quarantine_sweep_keeps_live_child_and_release_refuses) ... ok
test_quarantine_ttl_demotes_never_auto_completes (test_agentctl.AgentctlTests.test_quarantine_ttl_demotes_never_auto_completes) ... /home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/email/errors.py:12: ResourceWarning: unclosed database in <sqlite3.Connection object at 0x7aad4a3727a0>
class MessageParseError(MessageError):
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/email/errors.py:12: ResourceWarning: unclosed database in <sqlite3.Connection object at 0x7aad4a373c40>
class MessageParseError(MessageError):
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/email/errors.py:12: ResourceWarning: unclosed database in <sqlite3.Connection object at 0x7aad493775b0>
class MessageParseError(MessageError):
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/email/errors.py:12: ResourceWarning: unclosed database in <sqlite3.Connection object at 0x7aad49376d40>
class MessageParseError(MessageError):
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/email/errors.py:12: ResourceWarning: unclosed database in <sqlite3.Connection object at 0x7aad493776a0>
class MessageParseError(MessageError):
ResourceWarning: Enable tracemalloc to get the object allocation traceback
{"demoted": [{"from": "running", "run_id": "hb-stale", "to": "suspect"}, {"from": "suspect", "run_id": "sus-stale", "to": "quarantine"}], "heartbeat_ttl_seconds": 300, "status": "ok", "suspect_ttl_seconds": 600}
ok
test_quarantine_unreadable_proc_stops_at_suspect (test_agentctl.AgentctlTests.test_quarantine_unreadable_proc_stops_at_suspect) ... {"demoted": [{"from": "running", "run_id": "run-unknown", "to": "suspect"}], "heartbeat_ttl_seconds": 300, "status": "ok", "suspect_ttl_seconds": 600}
ok
test_queue_enqueue_list_cancel_roundtrip (test_agentctl.AgentctlTests.test_queue_enqueue_list_cancel_roundtrip) ... ok
test_queue_schedule_promotes_validated_candidate (test_agentctl.AgentctlTests.test_queue_schedule_promotes_validated_candidate) ... ok
test_queue_subparser_registered_once (test_agentctl.AgentctlTests.test_queue_subparser_registered_once)
Duplicate queue registration must not shadow handler-compatible flags. ... usage: python3.14 -m unittest [-h] [--repo REPO]
{init,status,doctor,task,with,guard,worktree-check,docs,queue,cleanup,recover,operation,freeze,unfreeze,hooks,validate,integrate,deploy,claim,quarantine,daemon} ...
python3.14 -m unittest: error: unrecognized arguments: --head-sha deadbeef
ok
test_recover_git_dev_requires_verification_marker (test_agentctl.AgentctlTests.test_recover_git_dev_requires_verification_marker) ... ok
test_recovery_refuses_dead_supervisor_with_live_child (test_agentctl.AgentctlTests.test_recovery_refuses_dead_supervisor_with_live_child) ... ok
test_registered_active_task_must_close_before_cleanup (test_agentctl.AgentctlTests.test_registered_active_task_must_close_before_cleanup) ... ok
test_replaced_compatibility_inode_is_not_a_new_free_lock (test_agentctl.AgentctlTests.test_replaced_compatibility_inode_is_not_a_new_free_lock) ... ok
test_shared_lease_does_not_satisfy_write_guard (test_agentctl.AgentctlTests.test_shared_lease_does_not_satisfy_write_guard) ... ok
test_stale_fence_env_is_stale_generation (test_agentctl.AgentctlTests.test_stale_fence_env_is_stale_generation) ... ok
test_task_close_does_not_auto_promote_matrix_done (test_agentctl.AgentctlTests.test_task_close_does_not_auto_promote_matrix_done) ... ok
test_task_index_lease_serializes_index_but_not_disjoint_work (test_agentctl.AgentctlTests.test_task_index_lease_serializes_index_but_not_disjoint_work) ... ok
test_task_registration_rejects_protected_branch_and_invalid_scopes (test_agentctl.AgentctlTests.test_task_registration_rejects_protected_branch_and_invalid_scopes) ... ok
test_uninitialized_status_and_doctor_do_not_create_state (test_agentctl.AgentctlTests.test_uninitialized_status_and_doctor_do_not_create_state) ... ok
test_unset_fence_env_matches_default_zero (test_agentctl.AgentctlTests.test_unset_fence_env_matches_default_zero)
Self-bootstrap: hooks re-exec updated agentctl without AGENTCTL_FENCE set. ... ok
test_validated_linear_commit_cannot_bypass_candidate_entrypoint (test_agentctl.AgentctlTests.test_validated_linear_commit_cannot_bypass_candidate_entrypoint) ... ok
test_validated_merge_candidate_is_exactly_promoted_to_dev (test_agentctl.AgentctlTests.test_validated_merge_candidate_is_exactly_promoted_to_dev) ... ok
test_boot_and_self_proc_are_observable (test_agentctl.ProcessPlatformTests.test_boot_and_self_proc_are_observable) ... ok
test_descent_unreadable_is_unknown (test_agentctl.ProcessPlatformTests.test_descent_unreadable_is_unknown) ... ok
test_gone_pid_is_dead (test_agentctl.ProcessPlatformTests.test_gone_pid_is_dead) ... ok
test_lease_denies_when_supervisor_proc_unreadable (test_agentctl.ProcessPlatformTests.test_lease_denies_when_supervisor_proc_unreadable) ... ok
test_unreadable_proc_is_unknown_not_dead (test_agentctl.ProcessPlatformTests.test_unreadable_proc_is_unknown_not_dead) ... ok
test_zombie_is_not_alive (test_agentctl.ProcessPlatformTests.test_zombie_is_not_alive) ... ok
test_build_projection_row_canonical (test_agentctl_w4.AgentctlW4Tests.test_build_projection_row_canonical) ... ok
test_deploy_recover_loop_failure_sets_recovery_required (test_agentctl_w4.AgentctlW4Tests.test_deploy_recover_loop_failure_sets_recovery_required) ... ok
test_deploy_recover_loop_success_with_fake_command (test_agentctl_w4.AgentctlW4Tests.test_deploy_recover_loop_success_with_fake_command) ... ok
test_document_projection_cas_mismatch_refuses (test_agentctl_w4.AgentctlW4Tests.test_document_projection_cas_mismatch_refuses) ... ok
test_project_cleanup_markdown_header_is_report_only (test_agentctl_w4.AgentctlW4Tests.test_project_cleanup_markdown_header_is_report_only) ... ok
test_recover_deploy_from_recovery_required (test_agentctl_w4.AgentctlW4Tests.test_recover_deploy_from_recovery_required) ... ok
test_row_projection_ambiguous_record_refuses (test_agentctl_w4.AgentctlW4Tests.test_row_projection_ambiguous_record_refuses) ... ok
test_row_projection_cas_and_insert (test_agentctl_w4.AgentctlW4Tests.test_row_projection_cas_and_insert) ... ok
test_cwd_visibility_structured_on_host (test_agentctl_w4.DarwinCwdVisibilityTests.test_cwd_visibility_structured_on_host) ... ok
test_cleanup_report_never_deletes (test_codegraph_governance.CodegraphGovernanceTest.test_cleanup_report_never_deletes) ... ok
test_codegraph_sync_fail_open_without_binary (test_codegraph_governance.CodegraphGovernanceTest.test_codegraph_sync_fail_open_without_binary) ... ok
test_create_skips_codegraph_when_missing (test_codegraph_governance.CodegraphGovernanceTest.test_create_skips_codegraph_when_missing) ... ok
test_docs_drift_detects_done_regression (test_codegraph_governance.CodegraphGovernanceTest.test_docs_drift_detects_done_regression) ... ok
test_hooks_install_includes_codegraph_sync_hooks (test_codegraph_governance.CodegraphGovernanceTest.test_hooks_install_includes_codegraph_sync_hooks) ... ok
test_emits_empty_object (test_cursor_hooks.AuditSessionTest.test_emits_empty_object) ... ok
test_probe_writes_when_enabled (test_cursor_hooks.AuditSessionTest.test_probe_writes_when_enabled) ... ok
test_hooks_json_parseable_and_scripts_executable (test_cursor_hooks.CursorHooksStructureTest.test_hooks_json_parseable_and_scripts_executable) ... ok
test_mcp_relative_paths_for_agentctl_jev (test_cursor_hooks.CursorHooksStructureTest.test_mcp_relative_paths_for_agentctl_jev) ... ok
test_rules_no_drift_markers (test_cursor_hooks.CursorHooksStructureTest.test_rules_no_drift_markers) ... ok
test_allow_status (test_cursor_hooks.GuardGitTest.test_allow_status) ... ok
test_ask_branch_D (test_cursor_hooks.GuardGitTest.test_ask_branch_D) ... ok
test_ask_clean_f (test_cursor_hooks.GuardGitTest.test_ask_clean_f) ... ok
test_ask_force (test_cursor_hooks.GuardGitTest.test_ask_force) ... ok
test_ask_push (test_cursor_hooks.GuardGitTest.test_ask_push) ... ok
test_ask_reset_hard (test_cursor_hooks.GuardGitTest.test_ask_reset_hard) ... ok
test_allow_result (test_cursor_hooks.GuardPiDispatchTest.test_allow_result) ... ok
test_allow_roles (test_cursor_hooks.GuardPiDispatchTest.test_allow_roles) ... ok
test_allow_test_with_commands (test_cursor_hooks.GuardPiDispatchTest.test_allow_test_with_commands) ... ok
test_allow_valid_run (test_cursor_hooks.GuardPiDispatchTest.test_allow_valid_run) ... ok
test_alt_field_names_arguments (test_cursor_hooks.GuardPiDispatchTest.test_alt_field_names_arguments) ... ok
test_ask_model_override (test_cursor_hooks.GuardPiDispatchTest.test_ask_model_override) ... ok
test_ask_source_sha_head (test_cursor_hooks.GuardPiDispatchTest.test_ask_source_sha_head) ... ok
test_ask_source_sha_short (test_cursor_hooks.GuardPiDispatchTest.test_ask_source_sha_short) ... ok
test_ask_test_bad_digest (test_cursor_hooks.GuardPiDispatchTest.test_ask_test_bad_digest) ... ok
test_ask_test_missing_commands (test_cursor_hooks.GuardPiDispatchTest.test_ask_test_missing_commands) ... ok
test_fail_open_empty_json (test_cursor_hooks.GuardPiDispatchTest.test_fail_open_empty_json) ... ok
test_allow_leased_run (test_cursor_hooks.GuardPiShellTest.test_allow_leased_run) ... ok
test_allow_roles (test_cursor_hooks.GuardPiShellTest.test_allow_roles) ... ok
test_ask_bare_run (test_cursor_hooks.GuardPiShellTest.test_ask_bare_run) ... ok
test_ask_pkill_f (test_cursor_hooks.GuardPiShellTest.test_ask_pkill_f) ... ok
test_injects_for_pi_tool (test_cursor_hooks.PiReceiptContextTest.test_injects_for_pi_tool) ... ok
test_noop_for_unrelated (test_cursor_hooks.PiReceiptContextTest.test_noop_for_unrelated) ... ok
test_skills_exist (test_cursor_hooks.SkillsPresentTest.test_skills_exist) ... ok
test_both_expected_tests_pass (test_go_test_evidence.TestEvidence.test_both_expected_tests_pass) ... ok
test_missing_partial_skip_failure_and_duplicate_are_not_pass (test_go_test_evidence.TestEvidence.test_missing_partial_skip_failure_and_duplicate_are_not_pass) ... ok
test_subtests_and_case_mismatches_do_not_prove_parent_execution (test_go_test_evidence.TestEvidence.test_subtests_and_case_mismatches_do_not_prove_parent_execution) ... ok
test_all_profiles_have_noninteractive_flags_and_explicit_pi_routes (test_herdr_agents.HerdrTests.test_all_profiles_have_noninteractive_flags_and_explicit_pi_routes) ... ok
test_explicit_prewrite_retry_reaches_flash (test_herdr_agents.HerdrTests.test_explicit_prewrite_retry_reaches_flash) ... ok
test_implementation_fallback_is_explicit (test_herdr_agents.HerdrTests.test_implementation_fallback_is_explicit) ... ok
test_kimi_atomic_limit_across_eight_processes (test_herdr_agents.HerdrTests.test_kimi_atomic_limit_across_eight_processes) ... ok
test_kimi_reason_disabled_marker_and_cross_lane_budget (test_herdr_agents.HerdrTests.test_kimi_reason_disabled_marker_and_cross_lane_budget) ... ok
test_kimi_uncertain_send_consumes_budget (test_herdr_agents.HerdrTests.test_kimi_uncertain_send_consumes_budget) ... ok
test_main_or_wrong_target_pane_cannot_start (test_herdr_agents.HerdrTests.test_main_or_wrong_target_pane_cannot_start) ... ok
test_outside_herdr_and_wrong_task_cwd_never_contact_transport (test_herdr_agents.HerdrTests.test_outside_herdr_and_wrong_task_cwd_never_contact_transport) ... ok
test_report_routes_to_max_not_kimi_or_minimax (test_herdr_agents.HerdrTests.test_report_routes_to_max_not_kimi_or_minimax) ... ok
test_same_role_ambiguity_and_foreign_cwd_do_not_dispatch (test_herdr_agents.HerdrTests.test_same_role_ambiguity_and_foreign_cwd_do_not_dispatch) ... ok
test_uncertain_transport_error_and_blocked_do_not_fallback (test_herdr_agents.HerdrTests.test_uncertain_transport_error_and_blocked_do_not_fallback) ... ok
test_authorized_pinned_rebuild_preserves_no_build (test_ops_guards.OpsGuardsTest.test_authorized_pinned_rebuild_preserves_no_build) ... ok
test_build_accepts_nightly_evidence_excluded_from_build_context (test_ops_guards.OpsGuardsTest.test_build_accepts_nightly_evidence_excluded_from_build_context) ... ok
test_build_context_is_frozen_when_managed_source_changes (test_ops_guards.OpsGuardsTest.test_build_context_is_frozen_when_managed_source_changes) ... ok
test_build_dirty_source_refuses_before_docker_or_receipts (test_ops_guards.OpsGuardsTest.test_build_dirty_source_refuses_before_docker_or_receipts) ... ok
test_build_mismatched_commit_refuses_before_docker_or_receipts (test_ops_guards.OpsGuardsTest.test_build_mismatched_commit_refuses_before_docker_or_receipts) ... ok
test_build_untracked_source_refuses_before_docker_or_receipts (test_ops_guards.OpsGuardsTest.test_build_untracked_source_refuses_before_docker_or_receipts) ... ok
test_clean_build_records_exact_source_commit (test_ops_guards.OpsGuardsTest.test_clean_build_records_exact_source_commit) ... ok
test_environment_lease_cannot_deploy_dirty_main_source (test_ops_guards.OpsGuardsTest.test_environment_lease_cannot_deploy_dirty_main_source) ... ok
test_invalid_action_does_not_initialize_environment (test_ops_guards.OpsGuardsTest.test_invalid_action_does_not_initialize_environment) ... ok
test_linked_worktree_writes_refuse_and_queries_remain_read_only (test_ops_guards.OpsGuardsTest.test_linked_worktree_writes_refuse_and_queries_remain_read_only) ... ok
test_one_environment_lease_does_not_authorize_all (test_ops_guards.OpsGuardsTest.test_one_environment_lease_does_not_authorize_all) ... ok
test_queries_never_initialize_environment (test_ops_guards.OpsGuardsTest.test_queries_never_initialize_environment) ... ok
test_service_writes_without_lease_refuse_before_any_side_effect (test_ops_guards.OpsGuardsTest.test_service_writes_without_lease_refuse_before_any_side_effect) ... ok
test_valid_lease_cannot_hide_linked_caller_or_linked_script (test_ops_guards.OpsGuardsTest.test_valid_lease_cannot_hide_linked_caller_or_linked_script) ... ok
test_00_executor_module_present (test_pi_subagents.PiSubagentsTests.test_00_executor_module_present) ... ok
test_approved_commands_do_not_mutate_model_snapshot (test_pi_subagents.PiSubagentsTests.test_approved_commands_do_not_mutate_model_snapshot) ... ok
test_config_catalog_parses_known_pool_and_roles (test_pi_subagents.PiSubagentsTests.test_config_catalog_parses_known_pool_and_roles) ... ok
test_config_rejects_unknown_alias_and_cycle (test_pi_subagents.PiSubagentsTests.test_config_rejects_unknown_alias_and_cycle) ... ok
test_config_reload_snapshot_on_dispatch (test_pi_subagents.PiSubagentsTests.test_config_reload_snapshot_on_dispatch) ... ok
test_corrupt_receipt_fail_closed (test_pi_subagents.PiSubagentsTests.test_corrupt_receipt_fail_closed) ... ok
test_crash_before_receipt_terminal_is_unknown_on_status (test_pi_subagents.PiSubagentsTests.test_crash_before_receipt_terminal_is_unknown_on_status) ... ok
test_disabled_auxiliary_and_m3_critical_remap_denied (test_pi_subagents.PiSubagentsTests.test_disabled_auxiliary_and_m3_critical_remap_denied) ... ok
test_extension_outside_model_snapshot_inventory (test_pi_subagents.PiSubagentsTests.test_extension_outside_model_snapshot_inventory)
Model snapshot must contain only declared inputs; -e guard lives outside it. ... ok
test_failed_replay_stays_failure (test_pi_subagents.PiSubagentsTests.test_failed_replay_stays_failure) ... ok
test_forged_lease_env_refused (test_pi_subagents.PiSubagentsTests.test_forged_lease_env_refused) ... ok
test_global_concurrency_limit (test_pi_subagents.PiSubagentsTests.test_global_concurrency_limit) ... ok
test_idempotent_replay_and_digest_mismatch (test_pi_subagents.PiSubagentsTests.test_idempotent_replay_and_digest_mismatch) ... ok
test_input_path_traversal_symlink_secret_refusal (test_pi_subagents.PiSubagentsTests.test_input_path_traversal_symlink_secret_refusal) ... ok
test_leader_exit_reaps_orphan_child (test_pi_subagents.PiSubagentsTests.test_leader_exit_reaps_orphan_child) ... ok
test_malformed_and_empty_outputs_fail (test_pi_subagents.PiSubagentsTests.test_malformed_and_empty_outputs_fail) ... ok
test_mcp_from_worktree_uses_main_agentctl (test_pi_subagents.PiSubagentsTests.test_mcp_from_worktree_uses_main_agentctl)
Server INSTALL_ROOT may be a linked worktree; agentctl must resolve from main. ... /home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=7 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=4 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=5 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
ok
test_mcp_initialize_list_run_result (test_pi_subagents.PiSubagentsTests.test_mcp_initialize_list_run_result) ... /home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=7 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=4 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=5 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
ok
test_mcp_running_poll_is_not_error (test_pi_subagents.PiSubagentsTests.test_mcp_running_poll_is_not_error) ... /home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=7 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=4 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=5 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
ok
test_mcp_surfaces_role_disabled_and_scope_refusal (test_pi_subagents.PiSubagentsTests.test_mcp_surfaces_role_disabled_and_scope_refusal) ... /home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=10 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=6 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=8 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=7 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=4 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=5 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
ok
test_model_mismatch_fails (test_pi_subagents.PiSubagentsTests.test_model_mismatch_fails) ... ok
test_naked_run_without_lease_refused (test_pi_subagents.PiSubagentsTests.test_naked_run_without_lease_refused) ... ok
test_orphan_result_is_readonly (test_pi_subagents.PiSubagentsTests.test_orphan_result_is_readonly) ... ok
test_output_cap_on_stdout_flood (test_pi_subagents.PiSubagentsTests.test_output_cap_on_stdout_flood) ... ok
test_readonly_guard_allows_exact_root_listing (test_pi_subagents.PiSubagentsTests.test_readonly_guard_allows_exact_root_listing)
Exact-root ls/grep must pass when snapshot inventory is allowlisted-only. ... ok
test_readonly_guard_rejects_at_prefix (test_pi_subagents.PiSubagentsTests.test_readonly_guard_rejects_at_prefix)
Exercise extension path policy via Node strip-types if available. ... ok
test_replay_keeps_old_config_after_disable (test_pi_subagents.PiSubagentsTests.test_replay_keeps_old_config_after_disable) ... ok
test_roles_does_not_invoke_pi (test_pi_subagents.PiSubagentsTests.test_roles_does_not_invoke_pi) ... ok
test_run_emits_exact_pi_flags_and_receipt (test_pi_subagents.PiSubagentsTests.test_run_emits_exact_pi_flags_and_receipt) ... ok
test_scope_refusal_outside_report_path (test_pi_subagents.PiSubagentsTests.test_scope_refusal_outside_report_path) ... ok
test_scrub_env_drops_pi_dir_for_tests (test_pi_subagents.PiSubagentsTests.test_scrub_env_drops_pi_dir_for_tests) ... ok
test_source_sha_rejects_symbolic_refs (test_pi_subagents.PiSubagentsTests.test_source_sha_rejects_symbolic_refs) ... ok
test_test_run_requires_approved_commands_and_records_failure (test_pi_subagents.PiSubagentsTests.test_test_run_requires_approved_commands_and_records_failure) ... ok
test_timeout_kills_process_group (test_pi_subagents.PiSubagentsTests.test_timeout_kills_process_group) ... ok
----------------------------------------------------------------------
Ran 202 tests in 111.351s
OK
+352
View File
@@ -0,0 +1,352 @@
"""Unit tests for project Cursor hooks (advisory gates) and hooks.json structure."""
from __future__ import annotations
import json
import os
import shutil
import stat
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
SOURCE = Path(__file__).resolve().parents[1]
REPO = SOURCE.parent
HOOKS = REPO / ".cursor" / "hooks"
HOOKS_JSON = REPO / ".cursor" / "hooks.json"
RULES = REPO / ".cursor" / "rules"
def _run_hook(script: Path, payload: dict | str, *, env: dict | None = None, cwd: Path | None = None) -> tuple[int, dict | None, str]:
data = payload if isinstance(payload, str) else json.dumps(payload)
e = os.environ.copy()
e["CURSOR_HOOKS_NO_INVENTORY"] = "1"
if env:
e.update(env)
# Isolate audit/probe under temp repo root via CURSOR_PROJECT_DIR
r = subprocess.run(
[str(script)],
input=data,
text=True,
capture_output=True,
cwd=str(cwd or REPO),
env=e,
check=False,
)
out = (r.stdout or "").strip()
parsed = None
if out:
try:
parsed = json.loads(out)
except json.JSONDecodeError:
parsed = None
return r.returncode, parsed, r.stderr or ""
class CursorHooksStructureTest(unittest.TestCase):
def test_hooks_json_parseable_and_scripts_executable(self):
cfg = json.loads(HOOKS_JSON.read_text(encoding="utf-8"))
self.assertEqual(cfg.get("version"), 1)
hooks = cfg.get("hooks") or {}
self.assertTrue(hooks)
for event, entries in hooks.items():
self.assertIsInstance(entries, list, event)
for entry in entries:
cmd = entry.get("command")
self.assertTrue(cmd, f"{event} missing command")
path = REPO / cmd
self.assertTrue(path.is_file(), f"missing {path}")
mode = path.stat().st_mode
self.assertTrue(mode & stat.S_IXUSR, f"not executable: {path}")
matcher = entry.get("matcher")
if matcher:
# JavaScript RegExp must compile in Node
r = subprocess.run(
["node", "-e", f"new RegExp({json.dumps(matcher)})"],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(r.returncode, 0, f"bad matcher {matcher!r}: {r.stderr}")
def test_rules_no_drift_markers(self):
text = (RULES / "test-reports-standard.mdc").read_text(encoding="utf-8")
self.assertNotIn("MiniMax-M2.7-highspeed", text)
self.assertNotIn("PROJECT.md) §8", text)
self.assertNotIn("PROJECT.md](../../PROJECT.md) §8", text)
# No unconditional full go test requirement as acceptance
self.assertNotIn("go test -v ./...", text)
self.assertIn("验证契约", text)
def test_mcp_relative_paths_for_agentctl_jev(self):
mcp = json.loads((REPO / ".cursor" / "mcp.json").read_text(encoding="utf-8"))
for name in ("agentctl", "jev", "pi-subagents"):
args = mcp["mcpServers"][name]["args"]
self.assertTrue(any(a.startswith("scripts/") for a in args), name)
self.assertFalse(any("${workspaceFolder}" in a for a in args), name)
class GuardGitTest(unittest.TestCase):
SCRIPT = HOOKS / "guard-git.sh"
def _perm(self, command: str) -> str:
code, parsed, err = _run_hook(self.SCRIPT, {"command": command})
self.assertEqual(code, 0, err)
self.assertIsInstance(parsed, dict)
return parsed.get("permission")
def test_allow_status(self):
self.assertEqual(self._perm("git status"), "allow")
def test_ask_push(self):
self.assertEqual(self._perm("git push origin dev"), "ask")
def test_ask_force(self):
self.assertEqual(self._perm("git push --force origin main"), "ask")
def test_ask_reset_hard(self):
self.assertEqual(self._perm("git reset --hard HEAD~1"), "ask")
def test_ask_clean_f(self):
self.assertEqual(self._perm("git clean -fd"), "ask")
def test_ask_branch_D(self):
self.assertEqual(self._perm("git branch -D feature/x"), "ask")
class GuardPiDispatchTest(unittest.TestCase):
SCRIPT = HOOKS / "guard-pi-dispatch.sh"
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.root = Path(self.tmp.name)
(self.root / ".cursor").mkdir()
self.env = {"CURSOR_PROJECT_DIR": str(self.root), "CURSOR_HOOKS_NO_INVENTORY": "1"}
def tearDown(self):
self.tmp.cleanup()
def _run(self, payload: dict) -> dict:
code, parsed, err = _run_hook(self.SCRIPT, payload, env=self.env, cwd=self.root)
self.assertEqual(code, 0, err)
self.assertIsInstance(parsed, dict)
return parsed
def _valid_run(self, **overrides) -> dict:
req = {
"role_id": "research",
"request_id": "research-001",
"goal": "read PROJECT.md",
"authorization_ref": "task auth",
"source_sha": "cfc80b34a40e8de78e77a44839571bb684d405d5",
"input_files": ["PROJECT.md"],
"timeout_seconds": 120,
}
req.update(overrides)
return {
"mcp_server_name": "pi-subagents",
"tool_name": "pi_subagent_run",
"tool_input": {"worktree": "/tmp/wt", "task_id": "t1", "request": req},
}
def test_allow_valid_run(self):
self.assertEqual(self._run(self._valid_run()).get("permission"), "allow")
def test_allow_roles(self):
p = {
"mcp_server_name": "pi-subagents",
"tool_name": "pi_subagent_roles",
"tool_input": {"worktree": "/tmp/wt", "task_id": "t1"},
}
self.assertEqual(self._run(p).get("permission"), "allow")
def test_allow_result(self):
p = {
"tool_name": "pi_subagent_result",
"arguments": {"worktree": "/tmp/wt", "task_id": "t1", "request_id": "r1"},
}
self.assertEqual(self._run(p).get("permission"), "allow")
def test_ask_source_sha_head(self):
self.assertEqual(self._run(self._valid_run(source_sha="HEAD")).get("permission"), "ask")
def test_ask_source_sha_short(self):
self.assertEqual(self._run(self._valid_run(source_sha="cfc80b34")).get("permission"), "ask")
def test_ask_test_missing_commands(self):
p = self._valid_run(role_id="test_run")
self.assertEqual(self._run(p).get("permission"), "ask")
def test_ask_test_bad_digest(self):
p = self._valid_run(
role_id="acceptance_check",
approved_commands=[["true"]],
plan_digest="not-a-digest",
)
self.assertEqual(self._run(p).get("permission"), "ask")
def test_allow_test_with_commands(self):
digest = "a" * 64
p = self._valid_run(
role_id="test_run",
approved_commands=[["python3", "-c", "print(1)"]],
plan_digest=digest,
)
self.assertEqual(self._run(p).get("permission"), "allow")
def test_ask_model_override(self):
p = self._valid_run(model_override={"provider": "x", "model": "y"})
self.assertEqual(self._run(p).get("permission"), "ask")
def test_fail_open_empty_json(self):
code, parsed, err = _run_hook(self.SCRIPT, "", env=self.env, cwd=self.root)
self.assertEqual(code, 0, err)
self.assertEqual(parsed.get("permission"), "allow")
audit = (self.root / ".cursor" / "hook-audit.jsonl").read_text(encoding="utf-8")
self.assertIn("UNRECOGNIZED_INPUT", audit)
def test_alt_field_names_arguments(self):
# tool_input via "arguments"
payload = {
"name": "pi_subagent_run",
"arguments": {
"request": {
"role_id": "planning",
"request_id": "p-1",
"goal": "g",
"authorization_ref": "a",
"source_sha": "cfc80b34a40e8de78e77a44839571bb684d405d5",
"input_files": [],
"timeout_seconds": 30,
}
},
}
self.assertEqual(self._run(payload).get("permission"), "allow")
class GuardPiShellTest(unittest.TestCase):
SCRIPT = HOOKS / "guard-pi-shell.sh"
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.root = Path(self.tmp.name)
(self.root / ".cursor").mkdir()
self.env = {"CURSOR_PROJECT_DIR": str(self.root)}
def tearDown(self):
self.tmp.cleanup()
def _perm(self, command: str) -> str:
code, parsed, err = _run_hook(
self.SCRIPT, {"command": command}, env=self.env, cwd=self.root
)
self.assertEqual(code, 0, err)
return parsed.get("permission")
def test_allow_roles(self):
self.assertEqual(self._perm("python3 -B scripts/pi_subagents.py roles"), "allow")
def test_ask_bare_run(self):
self.assertEqual(
self._perm("python3 -B scripts/pi_subagents.py run --request /tmp/r.json"),
"ask",
)
def test_allow_leased_run(self):
cmd = (
"python3 scripts/agentctl.py with --task t1 "
"--resource path:.doc/test-reports/assets/pi-subagents/t1/r1 -- "
"python3 -B scripts/pi_subagents.py run --request /tmp/r.json"
)
self.assertEqual(self._perm(cmd), "allow")
def test_ask_pkill_f(self):
self.assertEqual(self._perm("pkill -f pi_subagents"), "ask")
class PiReceiptContextTest(unittest.TestCase):
SCRIPT = HOOKS / "pi-receipt-context.sh"
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.root = Path(self.tmp.name)
(self.root / ".cursor").mkdir()
self.env = {"CURSOR_PROJECT_DIR": str(self.root)}
def tearDown(self):
self.tmp.cleanup()
def test_injects_for_pi_tool(self):
code, parsed, err = _run_hook(
self.SCRIPT,
{"tool_name": "pi_subagent_result", "mcp_server_name": "pi-subagents"},
env=self.env,
cwd=self.root,
)
self.assertEqual(code, 0, err)
self.assertIn("additional_context", parsed)
self.assertIn("run_status", parsed["additional_context"])
def test_noop_for_unrelated(self):
code, parsed, err = _run_hook(
self.SCRIPT,
{"tool_name": "Read", "tool_input": {"path": "x"}},
env=self.env,
cwd=self.root,
)
self.assertEqual(code, 0, err)
self.assertEqual(parsed, {})
class AuditSessionTest(unittest.TestCase):
SCRIPT = HOOKS / "audit-session.sh"
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.root = Path(self.tmp.name)
(self.root / ".cursor").mkdir()
# Minimal fake repo so inventory finds no agentctl and skips cleanly
self.env = {
"CURSOR_PROJECT_DIR": str(self.root),
"CURSOR_HOOKS_NO_INVENTORY": "1",
}
def tearDown(self):
self.tmp.cleanup()
def test_emits_empty_object(self):
code, parsed, err = _run_hook(
self.SCRIPT,
{"hook_event_name": "sessionStart"},
env=self.env,
cwd=self.root,
)
self.assertEqual(code, 0, err)
self.assertEqual(parsed, {})
audit = (self.root / ".cursor" / "hook-audit.jsonl").read_text(encoding="utf-8")
self.assertIn("SESSION_AUDIT", audit)
def test_probe_writes_when_enabled(self):
env = dict(self.env)
env["CURSOR_HOOKS_PROBE"] = "1"
code, _, err = _run_hook(
self.SCRIPT, {"event": "stop"}, env=env, cwd=self.root
)
self.assertEqual(code, 0, err)
probe = self.root / ".cursor" / "hook-probe.jsonl"
self.assertTrue(probe.is_file())
self.assertIn("audit-session", probe.read_text(encoding="utf-8"))
class SkillsPresentTest(unittest.TestCase):
def test_skills_exist(self):
for name in ("pi-subagent-dispatch", "subagent-stage-record"):
p = REPO / ".cursor" / "skills" / name / "SKILL.md"
self.assertTrue(p.is_file(), p)
text = p.read_text(encoding="utf-8")
self.assertIn("name:", text)
if __name__ == "__main__":
unittest.main()