Skip to content

Commit 7b1a5d7

Browse files
authored
Merge pull request #81 from full-stack-plugins/portable-agent-plugins
feat(plugin): 迁移到 Agent Plugins v1.0.0 可移植布局
2 parents 9e4adb1 + af59438 commit 7b1a5d7

29 files changed

Lines changed: 2976 additions & 6 deletions

‎com.github.copilot/hooks/__protocol__.md‎

Lines changed: 231 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
#!/usr/bin/env python3
2+
"""SessionStart 宿主适配器:读取事件与本机插件配置,输出项目记忆。"""
3+
from __future__ import annotations
4+
5+
import contextlib
6+
import json
7+
import os
8+
import subprocess
9+
import sys
10+
from pathlib import Path
11+
12+
PLUGIN_ROOT = next((c for c in Path(__file__).resolve().parents if (c / "plugin.json").is_file()), Path(__file__).resolve().parents[1])
13+
sys.path.insert(0, str(PLUGIN_ROOT / "scripts"))
14+
15+
from codeguard import startup_application
16+
from codeguard.reporting import plugin_version
17+
from detect_lang import ( # 兼容原有 Python 导入接口
18+
LANG_COMMANDS,
19+
REGISTRY,
20+
detect_languages,
21+
ensure_user_path,
22+
find_project_root,
23+
load_user_config,
24+
)
25+
26+
__all__ = [
27+
"LANG_COMMANDS",
28+
"REGISTRY",
29+
"detect_languages",
30+
"detect_linter_config",
31+
"ensure_user_path",
32+
"find_project_root",
33+
"load_user_config",
34+
"main",
35+
"notify",
36+
"version_backlog_note",
37+
]
38+
39+
detect_linter_config = startup_application.detect_linter_config
40+
version_backlog_note = startup_application.version_backlog_note
41+
42+
43+
def notify(title: str, message: str) -> None:
44+
"""macOS 系统通知;其他宿主静默。"""
45+
if sys.platform != "darwin":
46+
return
47+
safe_title = title.replace('"', "'")
48+
safe_message = message.replace('"', "'")[:200]
49+
with contextlib.suppress(OSError):
50+
subprocess.Popen(
51+
["osascript", "-e",
52+
f'display notification "{safe_message}" with title "{safe_title}"'],
53+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
54+
)
55+
56+
57+
def _enabled_plugin_orgs() -> tuple[str, ...]:
58+
"""ZCode 宿主注册表只在适配器读取,其他宿主缺文件时无副作用。"""
59+
try:
60+
config = json.loads((Path.home() / ".zcode" / "cli" / "config.json").read_text(encoding="utf-8"))
61+
orgs = {
62+
key.split("@", 1)[1]
63+
for key in (config.get("plugins", {}) or {}).get("enabledPlugins", {})
64+
if key.startswith("codeguard@")
65+
}
66+
return tuple(sorted(orgs))
67+
except (OSError, ValueError, AttributeError):
68+
return ()
69+
70+
71+
def main(payload: dict | None = None) -> int:
72+
from codeguard.hook_state import observe_once, session_scope
73+
session_id = (payload or {}).get("session_id")
74+
with session_scope(payload or {}):
75+
return observe_once(f"start:{session_id}" if session_id else None, lambda: _main(payload))
76+
77+
78+
def _main(payload: dict | None = None) -> int:
79+
ensure_user_path(from_login_shell=True)
80+
project_root = find_project_root(os.getcwd()) or Path(os.getcwd())
81+
current = plugin_version()
82+
report = startup_application.build_startup_report(
83+
project_root,
84+
plugin_version="" if current == "unknown" else current,
85+
cache_root=Path.home() / ".zcode" / "cli" / "plugins" / "cache",
86+
enabled_plugin_orgs=_enabled_plugin_orgs(),
87+
)
88+
if report.notification:
89+
notify(*report.notification)
90+
if report.text:
91+
print(report.text)
92+
return 0
93+
94+
95+
if __name__ == "__main__":
96+
try:
97+
payload: dict = {}
98+
if not sys.stdin.isatty():
99+
try:
100+
data = json.loads(sys.stdin.read())
101+
if isinstance(data, dict):
102+
payload = data
103+
except (json.JSONDecodeError, ValueError):
104+
payload = {}
105+
sys.exit(main(payload))
106+
except SystemExit:
107+
raise
108+
except Exception as exc: # noqa: BLE001 — 内部错误 fail-open,不阻断宿主工作流
109+
print(f"[codeguard] 内部错误已忽略(fail-open): {exc!r}", file=sys.stderr)
110+
sys.exit(0)
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"""旧 Hook 导入兼容面;检查、仓库策略、审计与反馈均委托到独立服务。"""
2+
from __future__ import annotations
3+
4+
import sys
5+
from pathlib import Path
6+
7+
PLUGIN_ROOT = next((c for c in Path(__file__).resolve().parents if (c / "plugin.json").is_file()), Path(__file__).resolve().parents[1])
8+
sys.path.insert(0, str(PLUGIN_ROOT / "scripts"))
9+
10+
from codeguard.baseline import (
11+
_git,
12+
_mentioned_files,
13+
_stale_attribution,
14+
baseline_stale_finding,
15+
)
16+
from codeguard.gate import (
17+
GATE_CACHE_TTL,
18+
_gate_cache_key,
19+
_gate_cache_path,
20+
_worktree_fingerprint,
21+
run_gate,
22+
)
23+
from codeguard.gate import run_batch as _run_gate_uncached
24+
from codeguard.hook_state import (
25+
codeguard_home,
26+
record_gate_decision,
27+
record_skip_event,
28+
session_state_path,
29+
should_suppress_event,
30+
)
31+
from codeguard.path_policy import (
32+
GUARD_EXCLUDE_DIRS,
33+
GUARD_EXCLUDE_FILES,
34+
GUARD_ROOT_ONLY_FILES,
35+
)
36+
from codeguard.reporting import (
37+
CODEGUARD_VERSION,
38+
REPORT_MAX_CHARS,
39+
_dependency_resolution_hint,
40+
_failure_detail_blocks,
41+
_log_path,
42+
_rule_summary,
43+
_squeeze,
44+
_truncate_detail,
45+
format_failure_report,
46+
format_safety_report,
47+
gate_directive,
48+
summarize_failures,
49+
)
50+
from codeguard.repository_policy import check_commit_safety, skip_gate_via_git_config
51+
from codeguard.spec_validation import validate as _openspec_validate
52+
from detect_lang import (
53+
LANG_COMMANDS,
54+
detect_language,
55+
detect_languages,
56+
get_overrides,
57+
probe_toolchain,
58+
project_uses_linter,
59+
)
60+
from scope import changed_files
61+
62+
__all__ = [
63+
"CODEGUARD_VERSION",
64+
"GATE_CACHE_TTL",
65+
"GUARD_EXCLUDE_DIRS",
66+
"GUARD_EXCLUDE_FILES",
67+
"GUARD_ROOT_ONLY_FILES",
68+
"LANG_COMMANDS",
69+
"PLUGIN_ROOT",
70+
"REPORT_MAX_CHARS",
71+
"_dependency_resolution_hint",
72+
"_failure_detail_blocks",
73+
"_gate_cache_key",
74+
"_gate_cache_path",
75+
"_git",
76+
"_log_path",
77+
"_mentioned_files",
78+
"_openspec_validate",
79+
"_rule_summary",
80+
"_run_gate_uncached",
81+
"_squeeze",
82+
"_stale_attribution",
83+
"_truncate_detail",
84+
"_worktree_fingerprint",
85+
"baseline_stale_finding",
86+
"changed_files",
87+
"check_commit_safety",
88+
"codeguard_home",
89+
"detect_language",
90+
"detect_languages",
91+
"format_failure_report",
92+
"format_safety_report",
93+
"gate_directive",
94+
"get_overrides",
95+
"probe_toolchain",
96+
"project_uses_linter",
97+
"record_gate_decision",
98+
"record_skip_event",
99+
"run_gate",
100+
"session_state_path",
101+
"should_suppress_event",
102+
"skip_gate_via_git_config",
103+
"summarize_failures",
104+
]
105+
106+
107+
def _java_executable_from_plan(plan: dict) -> str | None:
108+
"""从 java_project.analyze 的产物里解析构建可执行文件。
109+
110+
两条来源按优先级:
111+
1. 顶层 ``executable`` 键(java_project 正常产出);
112+
2. 回退 ``commands[0].argv[0]``——历史版本的 java_project 只把
113+
wrapper 体现在计划命令里(实测 68c8a37 曾因此让 mvnw 感知成为
114+
死代码:消费者读的键生产者从不写)。
115+
非 mvn/gradle wrapper(如 codeguard.json 显式命令)不参与替换。
116+
"""
117+
exe = plan.get("executable")
118+
if exe:
119+
return exe
120+
for cmd in plan.get("commands", []) or []:
121+
argv = cmd.get("argv") or []
122+
if argv and Path(argv[0]).name in ("mvnw", "gradlew"):
123+
return argv[0]
124+
return None
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"hooks": {
3+
"SessionStart": [
4+
{
5+
"matcher": "startup|resume",
6+
"hooks": [
7+
{
8+
"type": "command",
9+
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/env_check.py\"",
10+
"timeout": 30,
11+
"statusMessage": "codeguard: 检测项目语言与现有 linter 配置…"
12+
}
13+
]
14+
}
15+
],
16+
"UserPromptSubmit": [
17+
{
18+
"matcher": "提交|commit|push|提交代码|部署|deploy",
19+
"hooks": [
20+
{
21+
"type": "command",
22+
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/user_prompt_validator.py\"",
23+
"timeout": 120
24+
}
25+
]
26+
}
27+
],
28+
"PreToolUse": [
29+
{
30+
"matcher": "Bash",
31+
"hooks": [
32+
{
33+
"type": "command",
34+
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/pre_tool_git_guard.py\"",
35+
"timeout": 120
36+
}
37+
]
38+
}
39+
],
40+
"PostToolUse": [
41+
{
42+
"matcher": "Write|Edit|MultiEdit",
43+
"hooks": [
44+
{
45+
"type": "command",
46+
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/post_tool_lint.py\"",
47+
"timeout": 180
48+
}
49+
]
50+
}
51+
],
52+
"Stop": [
53+
{
54+
"hooks": [
55+
{
56+
"type": "command",
57+
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/stop_summary.py\"",
58+
"timeout": 15
59+
}
60+
]
61+
}
62+
]
63+
}
64+
}

0 commit comments

Comments
 (0)