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
8 changes: 4 additions & 4 deletions .agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@
"source": {
"source": "url",
"url": "https://github.com/partme-ai/partme-codeguard-plugin.git",
"ref": "v0.13.0"
"ref": "v0.14.0"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_USE"
},
"category": "Developer Tools",
"version": "0.13.0",
"version": "0.14.0",
"description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.",
"icon": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.13.0/assets/official-logo.png",
"icon": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.14.0/assets/official-logo.png",
"interface": {
"displayName": "代码规范守卫",
"shortDescription": "Trustworthy code checks and Java impact analysis",
"logo": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.13.0/assets/official-logo.png"
"logo": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.14.0/assets/official-logo.png"
}
}
]
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codeguard",
"version": "0.13.0+codex.20260923",
"version": "0.14.0+codex.20260923",
"description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.",
"author": {
"name": "Full Stack Skills / PartMe.AI",
Expand Down
2 changes: 1 addition & 1 deletion .zcode-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"en": "CodeGuard",
"zh-CN": "代码规范检查"
},
"version": "0.13.0",
"version": "0.14.0",
"description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.",
"description_i18n": {
"en": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.",
Expand Down
8 changes: 6 additions & 2 deletions docs/current-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
```mermaid
flowchart TD
Host[Codex / ZCode / Kimi] --> Entry[CLI / MCP / 五类 Hook]
Entry --> HookApp[startup / session / prompt / git_guard / save applications]
Entry --> Check[check_application / language_check]
HookApp --> Gate
HookApp --> Plan
Entry --> Gate[gate / gate_checks]
Entry --> Scan[cve / dockerfile]
Check --> Plan[planning / Java 模块分析]
Expand All @@ -30,8 +33,9 @@ flowchart TD
| 位置 | 责任 | 扩展边界 |
|---|---|---|
| `bin/codeguard`、`scripts/run_check.py`、`scripts/fix.py` | CLI 参数、输出与 MCP stdio 注册 | 不复制扫描器或判定逻辑 |
| `hooks/` | 宿主 JSON、会话事件、通知与退出协议 | PreToolUse/PostToolUse 仍有待迁移的编排,不能称为纯适配器 |
| `hooks/` | 宿主 JSON、会话事件、通知与退出协议 | 五类 Hook 将检查编排委托应用服务;入口保留旧导入兼容面及 fail-open 协议 |
| `scripts/codeguard/check_application.py`、`language_check.py` | 语言选择、检查与修复请求、MCP 工具分发 | 不导入 MCP SDK;旧 `run_per_language.py` 仅再导出 |
| `startup_application.py`、`session_application.py`、`prompt_application.py`、`git_guard_application.py`、`save_application.py` | 五类 Hook 的盘点、状态消费、软提示、Git 守卫及保存反馈编排 | 返回结构化结果,不打印或调用宿主通知;Hook 控制输出、通知与 fail-open。软/硬提示与保存统计在成功输出后确认;Stop 的状态消费仍先于 stdout,输出故障时可能丢失总结 |
| `gate.py`、`gate_checks.py`、`repository_policy.py`、`baseline.py` | Git 门禁、单语言结果、安全规则及有证据的存量比较 | 只有准确基线失败且每条诊断及次数覆盖当前结果才可豁免 |
| `java_build.py`、`java_impact.py`、`java_planning.py` 等 | 构建读取、纯影响闭包、命令选择与环境观察 | 默认保留跳过测试的行为;复杂构建保守扩大检查范围 |
| `cve_reports.py`、`cve_policy.py`、`cve_scanners.py`、`cve.py` | 漏洞报告、阈值、进程适配与复扫编排 | 无有效结构化报告就没有安全通过结论 |
Expand Down Expand Up @@ -62,4 +66,4 @@ python3 scripts/vendor/skill_vendor.py check
openspec validate refactor-codeguard-architecture --strict
```

真实 Maven/Gradle 大型项目、联网漏洞数据库、57 种工具链、Windows 与三宿主已安装运行均需要独立证据。临时 Git 快照不是执行沙箱;构建和扫描命令以当前用户权限运行。当前重构的任务进度和本地证据见 `openspec/changes/refactor-codeguard-architecture/verification.md`,发布记录需待版本、CI、tag、市场和宿主分别核对。
真实 Maven/Gradle 大型项目、联网漏洞数据库、57 种工具链、Windows 与三宿主已安装运行均需要独立证据。临时 Git 快照不是执行沙箱;构建和扫描命令以当前用户权限运行。当前重构的任务进度和本地证据见 `openspec/changes/refactor-codeguard-architecture/verification.md`。v0.13.0 的源码、CI、tag、Release 和市场已核对;其后工作树变更仍需独立发布,宿主安装运行尚未验收。
215 changes: 51 additions & 164 deletions hooks/env_check.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
#!/usr/bin/env python3
"""SessionStart 钩子:检测项目语言,把规则注入 AI 上下文。
"""SessionStart 宿主适配器:读取事件与本机插件配置,输出项目记忆。"""
from __future__ import annotations

行为:
1. detect_lang 找到项目语言
2. 检查是否已有 linter 配置(.pre-commit-config.yaml、checkstyle.xml、.clippy.toml、eslint.config.js、ruff.toml)
3. 输出一段 AGENTS.md 风格的提示,让 AI 知道自己在一个被 codeguard 管理的项目里
"""
import contextlib
import json
import os
import subprocess
import sys
from pathlib import Path

PLUGIN_ROOT = Path(__file__).resolve().parents[1] # hooks/ 的上级 = 插件根(不依赖宿主环境变量)
PLUGIN_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PLUGIN_ROOT / "scripts"))

from detect_lang import ( # ensure_user_path/load_user_config 实际定义:scripts/paths.py、scripts/user_config.py
from codeguard import startup_application
from codeguard.reporting import plugin_version
from detect_lang import ( # 兼容原有 Python 导入接口
LANG_COMMANDS,
REGISTRY,
detect_languages,
Expand All @@ -25,79 +23,49 @@
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 系统通知(非 darwin 静默)"""
"""macOS 系统通知;其他宿主静默。"""
if sys.platform != "darwin":
return
safe_t = title.replace('"', "'")
safe_m = message.replace('"', "'")[:200]
safe_title = title.replace('"', "'")
safe_message = message.replace('"', "'")[:200]
with contextlib.suppress(OSError):
subprocess.Popen(
["osascript", "-e",
f'display notification "{safe_m}" with title "{safe_t}"'],
f'display notification "{safe_message}" with title "{safe_title}"'],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)


def detect_linter_config(project_root: Path) -> dict:
"""返回 {language: [存在的 linter 配置文件]},由 languages.json 驱动。

每条 languages.json 条目的 `linter_config_files` 字段声明该语言的 linter
配置文件路径(glob 模式允许,与 requiresConfig 同语义)。新增语言只需
在注册表里填字段,env_check 自动盘点——不再有硬编码常量的同步陷阱。
"""
found = {}
for lang_id, lang_def in REGISTRY.items():
files = lang_def.get("linter_config_files") or []
if not files:
continue
present = [f for f in files if (project_root / f).exists()]
if present:
found[lang_id] = present
return found


def _semver_tuple(v: str) -> tuple[int, ...]:
import re as _re
m = _re.match(r"(\d+(?:\.\d+)*)", v.strip())
if not m:
return (0,)
return tuple(int(x) for x in m.group(1).split("."))


def version_backlog_note(current: str, cache_root: Path) -> str | None:
"""纯函数:本地插件 cache 存在比 current 更新的版本 → 提示文案;否则 None。

检测面 = `~/.zcode/cli/plugins/cache/<vendor>/codeguard/<version>` 的**版本目录
集合**(离线可得的唯一权威)。抓两类漂移:同 vendor 升级后旧目录未清、跨
vendor 版本参差导致实际生效副本落后于本机已有副本。抓不到"本机最新 vs
远端仓库最新"(需网络,钩子不联网)——文案里明示提醒核对远端。
"""
newer: list[str] = []
installed: list[str] = []
if not cache_root.is_dir():
return None
def _enabled_plugin_orgs() -> tuple[str, ...]:
"""ZCode 宿主注册表只在适配器读取,其他宿主缺文件时无副作用。"""
try:
for vendor_dir in sorted(cache_root.glob("*/codeguard/*")):
if not vendor_dir.is_dir():
continue
v = vendor_dir.name
installed.append(v)
if _semver_tuple(v) > _semver_tuple(current):
newer.append(v)
except OSError:
return None
if not newer:
return None
newest = max(newer, key=_semver_tuple)
return (
f"- ⚠️ **版本积压**:当前生效 codeguard `v{current}`,本机 cache 已有更新版本 "
f"`v{newest}`(共 {len(installed)} 份: {', '.join(sorted(installed))})——"
"钩子行为以**生效副本**为准,修复可能已存在却没跑到;请更新插件并清理旧 "
"cache(`~/.zcode/cli/plugins/cache/*/codeguard/`),同时核对远端是否还有"
"更新版本(本检测离线,看不到仓库最新版)。"
)
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:
Expand All @@ -108,100 +76,19 @@ def main(payload: dict | None = None) -> int:


def _main(payload: dict | None = None) -> int:
ensure_user_path(from_login_shell=True) # GUI 宿主 PATH 不含用户级工具目录,先补齐再盘点
project_root = find_project_root(os.getcwd())
if project_root is None:
project_root = Path(os.getcwd())

languages = detect_languages(project_root)
if not languages:
return 0 # 未识别到代码项目,安静退出

linter_cfg = detect_linter_config(project_root)
cfg = load_user_config()
enabled = cfg.get("enabled_languages", [])
if enabled and enabled != ["auto"]:
languages = [lang for lang in languages if lang in enabled]

# linter 盘点:直接复用门禁的探活(比裸 which 准——能发现 npx 包未装、运行时损坏)
from codeguard.toolchain import ToolchainProbe
probe = ToolchainProbe(project_root)
ready, missing = [], []
for lang in languages:
cmd_def = LANG_COMMANDS.get(lang)
if not cmd_def:
continue
ok, reason = probe.probe(cmd_def)
if ok:
ready.append(lang)
else:
hint = cmd_def.get("install_hint") or "见 docs/LANGUAGES.md"
missing.append((lang, reason, hint))

# 输出注入到 AI 上下文的内容(追加模式)
lines = []
lines.append("# codeguard 项目记忆")
lines.append("")
lines.append(f"- 项目根: `{project_root}`")
lines.append(f"- 检测到语言: {', '.join(languages)}")

if missing:
lines.append("")
lines.append("**⚠️ 以下语言的检查未生效,这些语言的文件【不会】被检查(提交门禁也会跳过它们):**")
for lang, reason, hint in missing:
lines.append(f" - {lang}({reason})→ 修复: `{hint}`")
lines.append(" 请主动提示用户安装;安装后新写的文件才会被检查。")
notify(
"codeguard 部分检查未生效",
f"{len(missing)} 个语言未生效: " + ", ".join(lang for lang, _, _ in missing) +
";详情见对话",
)
if ready:
lines.append(f"- 检查已生效的语言: {', '.join(ready)}")

if linter_cfg:
lines.append(f"- 已有的 linter 配置: {linter_cfg}")
else:
lines.append("- 未配置任何 linter(建议运行 /init 接入 codeguard)")

# 双副本检测:partme-ai 与 full-stack-plugins 同时启用时每个钩子事件
# 跑两遍(历史实测双份触发);提示用户停用其一,而不是默默重复。
try:
import json as _json
_reg = _json.loads((Path.home() / ".zcode" / "cli" / "config.json").read_text(encoding="utf-8"))
_orgs = sorted({
key.split("@", 1)[1]
for key in (_reg.get("plugins", {}) or {}).get("enabledPlugins", {})
if key.startswith("codeguard@")
})
except (OSError, ValueError, AttributeError):
_orgs = []
if len(_orgs) > 1:
lines.append(
f"- ⚠️ 检测到 codeguard **双副本同时启用**({', '.join(_orgs)}):"
"每个钩子事件会执行两遍,报告与统计可能翻倍或分裂——建议只保留一个来源"
)

# 版本积压:本机 cache 里有比当前生效副本更新的版本(实测 8 份副本停留在
# 0.5.x 而源码已 0.8.x——"发布了但没跑到"的盲区,SessionStart 即可见)
try:
_mf = json.loads((PLUGIN_ROOT / ".zcode-plugin" / "plugin.json").read_text(encoding="utf-8"))
_current = str(_mf.get("version") or "")
except (OSError, ValueError, AttributeError):
_current = ""
if _current:
_note = version_backlog_note(
_current, Path.home() / ".zcode" / "cli" / "plugins" / "cache"
)
if _note:
lines.append(_note)

lines.append("- AI 写完代码会被 PostToolUse 钩子自动 lint,告警会出现在这里,按告警里的「怎么修」处理")
lines.append("- 用户要求「提交/push」时,UserPromptSubmit 钩子会再次确认所有 linter 通过,未通过会拦截提交")
lines.append("")
lines.append("**重要**:写代码前先阅读对应语言的规范(见 skills/codeguard-{language}/SKILL.md)")

print("\n".join(lines))
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


Expand All @@ -218,6 +105,6 @@ def _main(payload: dict | None = None) -> int:
sys.exit(main(payload))
except SystemExit:
raise
except Exception as exc: # noqa: BLE001 — 内部错误 fail-open:traceback 绝不进 AI 上下文/阻断工作流
except Exception as exc: # noqa: BLE001 — 内部错误 fail-open,不阻断宿主工作流
print(f"[codeguard] 内部错误已忽略(fail-open): {exc!r}", file=sys.stderr)
sys.exit(0)
Loading
Loading