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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 231 additions & 0 deletions com.github.copilot/hooks/__protocol__.md

Large diffs are not rendered by default.

110 changes: 110 additions & 0 deletions com.github.copilot/hooks/env_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""SessionStart 宿主适配器:读取事件与本机插件配置,输出项目记忆。"""
from __future__ import annotations

import contextlib
import json
import os
import subprocess
import sys
from pathlib import Path

PLUGIN_ROOT = next((c for c in Path(__file__).resolve().parents if (c / "plugin.json").is_file()), Path(__file__).resolve().parents[1])
sys.path.insert(0, str(PLUGIN_ROOT / "scripts"))

from codeguard import startup_application
from codeguard.reporting import plugin_version
from detect_lang import ( # 兼容原有 Python 导入接口
LANG_COMMANDS,
REGISTRY,
detect_languages,
ensure_user_path,
find_project_root,
load_user_config,
)

__all__ = [
"LANG_COMMANDS",
"REGISTRY",
"detect_languages",
"detect_linter_config",
"ensure_user_path",
"find_project_root",
"load_user_config",
"main",
"notify",
"version_backlog_note",
]

detect_linter_config = startup_application.detect_linter_config
version_backlog_note = startup_application.version_backlog_note


def notify(title: str, message: str) -> None:
"""macOS 系统通知;其他宿主静默。"""
if sys.platform != "darwin":
return
safe_title = title.replace('"', "'")
safe_message = message.replace('"', "'")[:200]
with contextlib.suppress(OSError):
subprocess.Popen(
["osascript", "-e",
f'display notification "{safe_message}" with title "{safe_title}"'],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)


def _enabled_plugin_orgs() -> tuple[str, ...]:
"""ZCode 宿主注册表只在适配器读取,其他宿主缺文件时无副作用。"""
try:
config = json.loads((Path.home() / ".zcode" / "cli" / "config.json").read_text(encoding="utf-8"))
orgs = {
key.split("@", 1)[1]
for key in (config.get("plugins", {}) or {}).get("enabledPlugins", {})
if key.startswith("codeguard@")
}
return tuple(sorted(orgs))
except (OSError, ValueError, AttributeError):
return ()


def main(payload: dict | None = None) -> int:
from codeguard.hook_state import observe_once, session_scope
session_id = (payload or {}).get("session_id")
with session_scope(payload or {}):
return observe_once(f"start:{session_id}" if session_id else None, lambda: _main(payload))


def _main(payload: dict | None = None) -> int:
ensure_user_path(from_login_shell=True)
project_root = find_project_root(os.getcwd()) or Path(os.getcwd())
current = plugin_version()
report = startup_application.build_startup_report(
project_root,
plugin_version="" if current == "unknown" else current,
cache_root=Path.home() / ".zcode" / "cli" / "plugins" / "cache",
enabled_plugin_orgs=_enabled_plugin_orgs(),
)
if report.notification:
notify(*report.notification)
if report.text:
print(report.text)
return 0


if __name__ == "__main__":
try:
payload: dict = {}
if not sys.stdin.isatty():
try:
data = json.loads(sys.stdin.read())
if isinstance(data, dict):
payload = data
except (json.JSONDecodeError, ValueError):
payload = {}
sys.exit(main(payload))
except SystemExit:
raise
except Exception as exc: # noqa: BLE001 — 内部错误 fail-open,不阻断宿主工作流
print(f"[codeguard] 内部错误已忽略(fail-open): {exc!r}", file=sys.stderr)
sys.exit(0)
124 changes: 124 additions & 0 deletions com.github.copilot/hooks/gate_lib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""旧 Hook 导入兼容面;检查、仓库策略、审计与反馈均委托到独立服务。"""
from __future__ import annotations

import sys
from pathlib import Path

PLUGIN_ROOT = next((c for c in Path(__file__).resolve().parents if (c / "plugin.json").is_file()), Path(__file__).resolve().parents[1])
sys.path.insert(0, str(PLUGIN_ROOT / "scripts"))

from codeguard.baseline import (
_git,
_mentioned_files,
_stale_attribution,
baseline_stale_finding,
)
from codeguard.gate import (
GATE_CACHE_TTL,
_gate_cache_key,
_gate_cache_path,
_worktree_fingerprint,
run_gate,
)
from codeguard.gate import run_batch as _run_gate_uncached
from codeguard.hook_state import (
codeguard_home,
record_gate_decision,
record_skip_event,
session_state_path,
should_suppress_event,
)
from codeguard.path_policy import (
GUARD_EXCLUDE_DIRS,
GUARD_EXCLUDE_FILES,
GUARD_ROOT_ONLY_FILES,
)
from codeguard.reporting import (
CODEGUARD_VERSION,
REPORT_MAX_CHARS,
_dependency_resolution_hint,
_failure_detail_blocks,
_log_path,
_rule_summary,
_squeeze,
_truncate_detail,
format_failure_report,
format_safety_report,
gate_directive,
summarize_failures,
)
from codeguard.repository_policy import check_commit_safety, skip_gate_via_git_config
from codeguard.spec_validation import validate as _openspec_validate
from detect_lang import (
LANG_COMMANDS,
detect_language,
detect_languages,
get_overrides,
probe_toolchain,
project_uses_linter,
)
from scope import changed_files

__all__ = [
"CODEGUARD_VERSION",
"GATE_CACHE_TTL",
"GUARD_EXCLUDE_DIRS",
"GUARD_EXCLUDE_FILES",
"GUARD_ROOT_ONLY_FILES",
"LANG_COMMANDS",
"PLUGIN_ROOT",
"REPORT_MAX_CHARS",
"_dependency_resolution_hint",
"_failure_detail_blocks",
"_gate_cache_key",
"_gate_cache_path",
"_git",
"_log_path",
"_mentioned_files",
"_openspec_validate",
"_rule_summary",
"_run_gate_uncached",
"_squeeze",
"_stale_attribution",
"_truncate_detail",
"_worktree_fingerprint",
"baseline_stale_finding",
"changed_files",
"check_commit_safety",
"codeguard_home",
"detect_language",
"detect_languages",
"format_failure_report",
"format_safety_report",
"gate_directive",
"get_overrides",
"probe_toolchain",
"project_uses_linter",
"record_gate_decision",
"record_skip_event",
"run_gate",
"session_state_path",
"should_suppress_event",
"skip_gate_via_git_config",
"summarize_failures",
]


def _java_executable_from_plan(plan: dict) -> str | None:
"""从 java_project.analyze 的产物里解析构建可执行文件。

两条来源按优先级:
1. 顶层 ``executable`` 键(java_project 正常产出);
2. 回退 ``commands[0].argv[0]``——历史版本的 java_project 只把
wrapper 体现在计划命令里(实测 68c8a37 曾因此让 mvnw 感知成为
死代码:消费者读的键生产者从不写)。
非 mvn/gradle wrapper(如 codeguard.json 显式命令)不参与替换。
"""
exe = plan.get("executable")
if exe:
return exe
for cmd in plan.get("commands", []) or []:
argv = cmd.get("argv") or []
if argv and Path(argv[0]).name in ("mvnw", "gradlew"):
return argv[0]
return None
64 changes: 64 additions & 0 deletions com.github.copilot/hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume",
"hooks": [
{
"type": "command",
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/env_check.py\"",
"timeout": 30,
"statusMessage": "codeguard: 检测项目语言与现有 linter 配置…"
}
]
}
],
"UserPromptSubmit": [
{
"matcher": "提交|commit|push|提交代码|部署|deploy",
"hooks": [
{
"type": "command",
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/user_prompt_validator.py\"",
"timeout": 120
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/pre_tool_git_guard.py\"",
"timeout": 120
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/post_tool_lint.py\"",
"timeout": 180
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/stop_summary.py\"",
"timeout": 15
}
]
}
]
}
}
Loading
Loading