merge: candidate cursor-governance-hooks into dev
This commit is contained in:
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
exec python3 -B "$DIR/audit_session.py"
|
||||
@@ -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()
|
||||
Executable
+5
@@ -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"
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
exec python3 -B "$DIR/guard_pi_shell.py"
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
exec python3 -B "$DIR/pi_receipt_context.py"
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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/` 目录
|
||||
@@ -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`。
|
||||
|
||||
@@ -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)。
|
||||
@@ -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) |
|
||||
|
||||
@@ -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:无(随受控合入记录)。
|
||||
|
||||
@@ -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 并更新本条状态。
|
||||
@@ -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
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user