agentprecept 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentprecept/__init__.py +1 -0
- agentprecept/cli.py +409 -0
- agentprecept/gnhf_task.py +93 -0
- agentprecept/mcp_server.py +146 -0
- agentprecept-0.3.0.dist-info/METADATA +10 -0
- agentprecept-0.3.0.dist-info/RECORD +10 -0
- agentprecept-0.3.0.dist-info/WHEEL +5 -0
- agentprecept-0.3.0.dist-info/entry_points.txt +3 -0
- agentprecept-0.3.0.dist-info/licenses/LICENSE +21 -0
- agentprecept-0.3.0.dist-info/top_level.txt +1 -0
agentprecept/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""AgentPrecept — AI 编码 Agent 方法论治理工具集"""
|
agentprecept/cli.py
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
"""AgentPrecept CLI — 项目初始化(6阶段) / 同步 / 审计(10维) / 诊断 / setup / hooks / gnhf"""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import subprocess
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
SCRIPTS = Path(__file__).parent.parent / "scripts"
|
|
9
|
+
ROOT = Path(__file__).parent.parent
|
|
10
|
+
|
|
11
|
+
# ===== init (6 阶段) =====
|
|
12
|
+
|
|
13
|
+
def _check_git(project):
|
|
14
|
+
git_dir = Path(project) / ".git"
|
|
15
|
+
if not git_dir.exists():
|
|
16
|
+
subprocess.run(["git", "init", project], capture_output=True)
|
|
17
|
+
return True, "git init done"
|
|
18
|
+
return False, "Git already initialized"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _install_hook(project):
|
|
22
|
+
hook_path = Path(project) / ".git" / "hooks" / "pre-commit"
|
|
23
|
+
hook_content = """#!/bin/bash
|
|
24
|
+
# AgentPrecept pre-commit gate
|
|
25
|
+
# Skip: git commit --no-verify
|
|
26
|
+
|
|
27
|
+
# --- Gate 1: branch policy ---
|
|
28
|
+
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
|
|
29
|
+
CHANGED_COUNT=$(git diff --cached --name-only 2>/dev/null | wc -l | tr -d ' ')
|
|
30
|
+
|
|
31
|
+
if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
|
|
32
|
+
if [ "$CHANGED_COUNT" -gt 10 ] 2>/dev/null; then
|
|
33
|
+
echo ""
|
|
34
|
+
echo "[AgentPrecept] WARNING: $CHANGED_COUNT files on $BRANCH branch"
|
|
35
|
+
echo "[AgentPrecept] Major changes should use feature branches."
|
|
36
|
+
echo "[AgentPrecept] Create: git checkout -b feature/your-change"
|
|
37
|
+
echo "[AgentPrecept] Skip: git commit --no-verify"
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# --- Gate 2: design gate (shared logic with MCP tool) ---
|
|
43
|
+
CHANGED_CODE=$(git diff --cached --name-only | grep -Ev '^docs/|\\.md$|\\.ya?ml$|\\.json$|\\.cfg$|\\.toml$')
|
|
44
|
+
if [ -n "$CHANGED_CODE" ]; then
|
|
45
|
+
python scripts/design_gate_check.py --files $CHANGED_CODE
|
|
46
|
+
if [ $? -eq 1 ]; then
|
|
47
|
+
echo ""
|
|
48
|
+
echo "[AgentPrecept] Design docs missing. Create them first."
|
|
49
|
+
echo "[AgentPrecept] Skip: git commit --no-verify"
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# --- Gate 3: commit size ---
|
|
55
|
+
CHANGED_CODE_FILES=$(git diff --cached --name-only | grep -Ev '^docs/|\\.md$|\\.ya?ml$|\\.json$|\\.cfg$|\\.toml$' | wc -l | tr -d ' ')
|
|
56
|
+
if [ "$CHANGED_CODE_FILES" -gt 15 ] 2>/dev/null; then
|
|
57
|
+
echo ""
|
|
58
|
+
echo "[AgentPrecept] WARNING: $CHANGED_CODE_FILES code files in one commit"
|
|
59
|
+
echo "[AgentPrecept] Consider splitting into smaller commits (1-3 files each)"
|
|
60
|
+
echo "[AgentPrecept] Skip: git commit --no-verify"
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# --- Gate 4: NEEDS_HUMAN_REVIEW ---
|
|
65
|
+
REVIEW_TAGGED=$(git diff --cached --name-only | xargs grep -l '\[NEEDS_HUMAN_REVIEW\]' 2>/dev/null)
|
|
66
|
+
if [ -n "$REVIEW_TAGGED" ]; then
|
|
67
|
+
echo ""
|
|
68
|
+
echo "[AgentPrecept] WARNING: staged files contain [NEEDS_HUMAN_REVIEW]"
|
|
69
|
+
echo "[AgentPrecept] Confirm design docs first, then remove NEEDS_HUMAN_REVIEW."
|
|
70
|
+
echo "[AgentPrecept] Skip: git commit --no-verify"
|
|
71
|
+
exit 1
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
exit 0
|
|
75
|
+
"""
|
|
76
|
+
hook_path.parent.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
hook_path.write_text(hook_content)
|
|
78
|
+
# Unix: chmod +x
|
|
79
|
+
try:
|
|
80
|
+
hook_path.chmod(0o755)
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _check_gnhf():
|
|
87
|
+
try:
|
|
88
|
+
result = subprocess.run(["gnhf", "--version"], capture_output=True, text=True, timeout=5)
|
|
89
|
+
return result.returncode == 0
|
|
90
|
+
except Exception:
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _setup_gnhf():
|
|
95
|
+
from agentprecept.gnhf_task import render_template
|
|
96
|
+
render_template()
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _check_ci(project):
|
|
101
|
+
workflows = Path(project) / ".github" / "workflows"
|
|
102
|
+
return workflows.exists() and any(workflows.glob("*.yml"))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _generate_ci_gate(project):
|
|
106
|
+
workflows = Path(project) / ".github" / "workflows"
|
|
107
|
+
workflows.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
gate_yml = workflows / "agentprecept-gate.yml"
|
|
109
|
+
content = """# AgentPrecept CI Gate — PR merge 前自动运行 10 维审计
|
|
110
|
+
name: AgentPrecept Gate
|
|
111
|
+
on:
|
|
112
|
+
pull_request:
|
|
113
|
+
types: [opened, synchronize, reopened]
|
|
114
|
+
jobs:
|
|
115
|
+
audit-gate:
|
|
116
|
+
runs-on: ubuntu-latest
|
|
117
|
+
steps:
|
|
118
|
+
- uses: actions/checkout@v4
|
|
119
|
+
- uses: actions/setup-python@v5
|
|
120
|
+
with:
|
|
121
|
+
python-version: '3.10'
|
|
122
|
+
- run: pip install agentprecept
|
|
123
|
+
- run: agentprecept audit --gate
|
|
124
|
+
"""
|
|
125
|
+
gate_yml.write_text(content)
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _mcp_config():
|
|
130
|
+
config = {
|
|
131
|
+
"mcpServers": {
|
|
132
|
+
"agentprecept": {
|
|
133
|
+
"command": "python",
|
|
134
|
+
"args": ["-m", "agentprecept.mcp_server"],
|
|
135
|
+
"env": {"PYTHONIOENCODING": "utf-8", "PYTHONUTF8": "1"}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return json.dumps(config, indent=2)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def cmd_init(project=".", yes=False, dry_run=False, status_only=False,
|
|
143
|
+
ci=None, gnhf_opt=None):
|
|
144
|
+
"""6 阶段项目接入"""
|
|
145
|
+
project = Path(project).resolve()
|
|
146
|
+
|
|
147
|
+
if status_only:
|
|
148
|
+
print_status(project)
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
if dry_run:
|
|
152
|
+
print(f"[dry-run] would init: {project}")
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
project.mkdir(parents=True, exist_ok=True)
|
|
156
|
+
docs = project / "docs"
|
|
157
|
+
docs.mkdir(exist_ok=True)
|
|
158
|
+
|
|
159
|
+
report = {}
|
|
160
|
+
|
|
161
|
+
# Phase 1: 骨架
|
|
162
|
+
if not (project / "AGENTS.md").exists() or yes:
|
|
163
|
+
(ROOT / "AGENTS.md").replace(project / "AGENTS.md")
|
|
164
|
+
for tmpl in ["INDEX.md", "L1_A02_naming-convention_命名规范.md",
|
|
165
|
+
"L1_B01_glossary_术语表.md", "HANDOFF.md",
|
|
166
|
+
"MEMORY.md", "project-graph.yaml",
|
|
167
|
+
"L4_O01_design-rationale_设计依据.md"]:
|
|
168
|
+
src = ROOT / "templates" / tmpl
|
|
169
|
+
dst = docs / tmpl
|
|
170
|
+
if src.exists() and not dst.exists():
|
|
171
|
+
src.replace(dst)
|
|
172
|
+
report["AGENTS.md"] = True
|
|
173
|
+
report["docs/ skeleton"] = True
|
|
174
|
+
|
|
175
|
+
# Phase 2: Git
|
|
176
|
+
report["Git"] = _check_git(project)[0]
|
|
177
|
+
|
|
178
|
+
# Phase 3: Git Hook
|
|
179
|
+
if report["Git"]:
|
|
180
|
+
report["Pre-commit Hook"] = _install_hook(project)
|
|
181
|
+
|
|
182
|
+
# Phase 3.5: gnhf
|
|
183
|
+
if report.get("Git") and gnhf_opt is not False:
|
|
184
|
+
if _check_gnhf():
|
|
185
|
+
if gnhf_opt or yes:
|
|
186
|
+
_setup_gnhf()
|
|
187
|
+
report["gnhf"] = "enabled"
|
|
188
|
+
else:
|
|
189
|
+
report["gnhf"] = "skipped (run: agentprecept gnhf setup)"
|
|
190
|
+
else:
|
|
191
|
+
report["gnhf"] = "not installed (pip install gnhf && agentprecept gnhf setup)"
|
|
192
|
+
|
|
193
|
+
# Phase 4: CI Gate
|
|
194
|
+
has_ci = _check_ci(project)
|
|
195
|
+
if has_ci and ci is not False:
|
|
196
|
+
if ci or yes:
|
|
197
|
+
_generate_ci_gate(project)
|
|
198
|
+
report["CI Gate"] = True
|
|
199
|
+
else:
|
|
200
|
+
report["CI Gate"] = "skipped (run: agentprecept init --ci)"
|
|
201
|
+
else:
|
|
202
|
+
report["CI Gate"] = "no CI detected"
|
|
203
|
+
|
|
204
|
+
# Phase 5: MCP
|
|
205
|
+
report["MCP"] = _mcp_config()
|
|
206
|
+
|
|
207
|
+
# Phase 6: 状态报告
|
|
208
|
+
print_status(project, report)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def print_status(project, report=None):
|
|
212
|
+
"""输出 AgentPrecept 接入状态"""
|
|
213
|
+
project = Path(project)
|
|
214
|
+
if report is None:
|
|
215
|
+
report = {}
|
|
216
|
+
|
|
217
|
+
print("AgentPrecept 接入状态")
|
|
218
|
+
print("-" * 44)
|
|
219
|
+
|
|
220
|
+
def line(icon, name, detail=""):
|
|
221
|
+
print(f" {icon} {name:<20} {detail}")
|
|
222
|
+
|
|
223
|
+
ag = (project / "AGENTS.md").exists()
|
|
224
|
+
idx = (project / "docs" / "INDEX.md").exists()
|
|
225
|
+
git = (project / ".git").exists()
|
|
226
|
+
hook = (project / ".git" / "hooks" / "pre-commit").exists()
|
|
227
|
+
ci_gate = (project / ".github" / "workflows" / "agentprecept-gate.yml").exists()
|
|
228
|
+
|
|
229
|
+
line("✅" if ag else "❌", "AGENTS.md", "Agent 行为规则" if ag else "缺失")
|
|
230
|
+
line("✅" if idx else "❌", "docs/ skeleton", "7 核心文档" if idx else "缺失")
|
|
231
|
+
line("✅" if git else "⚠", "Git", "已初始化" if git else "运行: git init")
|
|
232
|
+
line("✅" if hook else "⚠", "Pre-commit Hook", "设计文档门禁" if hook else "需 git init")
|
|
233
|
+
if g := report.get("gnhf", ""):
|
|
234
|
+
status = "✅" if g == "enabled" else "⚠"
|
|
235
|
+
line(status, "gnhf", g)
|
|
236
|
+
if ci := report.get("CI Gate", ""):
|
|
237
|
+
line("✅" if ci is True else "⚠", "CI Gate", ci if isinstance(ci, str) else "PR merge 强制审计")
|
|
238
|
+
line("✅" if ag else "⚠", "MCP", "6 tools 可用" if ag else "需 AGENTS.md")
|
|
239
|
+
|
|
240
|
+
print("-" * 44)
|
|
241
|
+
total = sum(1 for v in [ag, idx, git, hook] if v)
|
|
242
|
+
print(f" {total}/4 核心能力已接入")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# ===== hooks =====
|
|
246
|
+
|
|
247
|
+
def cmd_hooks(action="status", project="."):
|
|
248
|
+
"""Git hook 管理"""
|
|
249
|
+
hook_path = Path(project) / ".git" / "hooks" / "pre-commit"
|
|
250
|
+
|
|
251
|
+
if action == "install":
|
|
252
|
+
if not (Path(project) / ".git").exists():
|
|
253
|
+
print("error: git 未初始化")
|
|
254
|
+
return
|
|
255
|
+
_install_hook(project)
|
|
256
|
+
print("pre-commit hook installed")
|
|
257
|
+
elif action == "uninstall":
|
|
258
|
+
if hook_path.exists():
|
|
259
|
+
hook_path.unlink()
|
|
260
|
+
print("pre-commit hook removed")
|
|
261
|
+
elif action == "status":
|
|
262
|
+
if hook_path.exists():
|
|
263
|
+
print("pre-commit hook: installed")
|
|
264
|
+
else:
|
|
265
|
+
print("pre-commit hook: not installed")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# ===== gnhf =====
|
|
269
|
+
|
|
270
|
+
def cmd_gnhf(action="status"):
|
|
271
|
+
"""gnhf 集成"""
|
|
272
|
+
if action == "setup":
|
|
273
|
+
if _check_gnhf():
|
|
274
|
+
_setup_gnhf()
|
|
275
|
+
print("[gnhf] task template generated: .gnhf/sync-task.md")
|
|
276
|
+
print("[gnhf] run: gnhf --goal .gnhf/sync-task.md --verify 'agentprecept audit'")
|
|
277
|
+
else:
|
|
278
|
+
print("gnhf CLI not found. Install: pip install gnhf")
|
|
279
|
+
elif action == "task":
|
|
280
|
+
_setup_gnhf()
|
|
281
|
+
print("[gnhf] task template regenerated")
|
|
282
|
+
elif action == "status":
|
|
283
|
+
task_file = Path(".gnhf/sync-task.md")
|
|
284
|
+
if _check_gnhf():
|
|
285
|
+
print("gnhf CLI: available")
|
|
286
|
+
print(f"task template: {'exists' if task_file.exists() else 'missing'}")
|
|
287
|
+
else:
|
|
288
|
+
print("gnhf CLI: not installed")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# ===== sync / audit / doctor / setup =====
|
|
292
|
+
|
|
293
|
+
def cmd_sync(src="src", graph="docs/project-graph.yaml"):
|
|
294
|
+
subprocess.run([sys.executable, str(SCRIPTS / "sync-graph.py"), src, graph])
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def cmd_audit(docs="docs", gate=False, scope_args=None):
|
|
298
|
+
args = [sys.executable, str(SCRIPTS / "basic-audit.py"), docs]
|
|
299
|
+
if gate:
|
|
300
|
+
args.append("--gate")
|
|
301
|
+
if scope_args:
|
|
302
|
+
args.extend(scope_args)
|
|
303
|
+
subprocess.run(args)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def cmd_doctor():
|
|
307
|
+
root = Path.cwd()
|
|
308
|
+
checks = {
|
|
309
|
+
"AGENTS.md": root / "AGENTS.md",
|
|
310
|
+
"docs/INDEX.md": root / "docs" / "INDEX.md",
|
|
311
|
+
"docs/project-graph.yaml": root / "docs" / "project-graph.yaml",
|
|
312
|
+
"docs/HANDOFF.md": root / "docs" / "HANDOFF.md",
|
|
313
|
+
"docs/L4_O01": root / "docs" / "L4_O01_design-rationale_设计依据.md",
|
|
314
|
+
}
|
|
315
|
+
ok = 0
|
|
316
|
+
for name, path in checks.items():
|
|
317
|
+
status = "OK" if path.exists() else "MISSING"
|
|
318
|
+
if path.exists():
|
|
319
|
+
ok += 1
|
|
320
|
+
print(f" {status} {name}")
|
|
321
|
+
print(f"\n{ok}/{len(checks)} 项通过")
|
|
322
|
+
if ok < len(checks):
|
|
323
|
+
print("运行 agentprecept init . 修复缺失文件")
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def cmd_setup():
|
|
327
|
+
print("=== agentprecept setup ===\n")
|
|
328
|
+
print("[1/3] 初始化项目文档...")
|
|
329
|
+
cmd_init(".", yes=True)
|
|
330
|
+
print()
|
|
331
|
+
print("[2/3] MCP Server 配置")
|
|
332
|
+
print(f" {_mcp_config()}")
|
|
333
|
+
print(" 重启 Agent 后即可使用 MCP tools: query/audit/diff/decision/handoff/design_gate\n")
|
|
334
|
+
print("[3/3] 诊断环境...")
|
|
335
|
+
cmd_doctor()
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# ===== main =====
|
|
339
|
+
|
|
340
|
+
USAGE = """agentprecept — AI coding agent governance toolkit
|
|
341
|
+
|
|
342
|
+
用法: agentprecept <command> [options]
|
|
343
|
+
|
|
344
|
+
命令:
|
|
345
|
+
init [project] 一键接入(6 阶段: 骨架/Git/Hook/gnhf/CI/MCP)
|
|
346
|
+
--yes 全部 yes(非交互)
|
|
347
|
+
--dry-run 预览模式
|
|
348
|
+
--status 仅查看接入状态
|
|
349
|
+
--ci 追加 CI Gate
|
|
350
|
+
--no-ci 跳过 CI Gate
|
|
351
|
+
--no-gnhf 跳过 gnhf
|
|
352
|
+
sync [src] 从代码同步 project-graph
|
|
353
|
+
audit [docs] 8 维审计(--gate 开启 10 维)
|
|
354
|
+
doctor 诊断缺失文件
|
|
355
|
+
setup 一键安装(init + MCP + doctor)
|
|
356
|
+
hooks <action> Git hook 管理 (install/uninstall/status)
|
|
357
|
+
gnhf <action> gnhf 集成 (setup/task/status)
|
|
358
|
+
"""
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def main():
|
|
362
|
+
if len(sys.argv) < 2:
|
|
363
|
+
print(USAGE)
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
cmd = sys.argv[1]
|
|
367
|
+
args = sys.argv[2:]
|
|
368
|
+
|
|
369
|
+
if cmd == "init":
|
|
370
|
+
project = args[0] if args else "."
|
|
371
|
+
yes = "--yes" in args
|
|
372
|
+
dry_run = "--dry-run" in args
|
|
373
|
+
status_only = "--status" in args
|
|
374
|
+
ci = False if "--no-ci" in args else (True if "--ci" in args else None)
|
|
375
|
+
gnhf_opt = False if "--no-gnhf" in args else None
|
|
376
|
+
cmd_init(project, yes=yes, dry_run=dry_run, status_only=status_only,
|
|
377
|
+
ci=ci, gnhf_opt=gnhf_opt)
|
|
378
|
+
|
|
379
|
+
elif cmd == "sync":
|
|
380
|
+
cmd_sync(*(args[:2] if args else ["src", "docs/project-graph.yaml"]))
|
|
381
|
+
|
|
382
|
+
elif cmd == "audit":
|
|
383
|
+
gate = "--gate" in args
|
|
384
|
+
scope_next = [a for a in args if a.startswith("--scope")]
|
|
385
|
+
other = [a for a in args if a != "--gate" and not a.startswith("--scope")]
|
|
386
|
+
docs = other[0] if other else "docs"
|
|
387
|
+
cmd_audit(docs=docs, gate=gate, scope_args=scope_next)
|
|
388
|
+
|
|
389
|
+
elif cmd == "doctor":
|
|
390
|
+
cmd_doctor()
|
|
391
|
+
|
|
392
|
+
elif cmd == "setup":
|
|
393
|
+
cmd_setup()
|
|
394
|
+
|
|
395
|
+
elif cmd == "hooks":
|
|
396
|
+
action = args[0] if args else "status"
|
|
397
|
+
project = args[1] if len(args) > 1 else "."
|
|
398
|
+
cmd_hooks(action, project)
|
|
399
|
+
|
|
400
|
+
elif cmd == "gnhf":
|
|
401
|
+
action = args[0] if args else "status"
|
|
402
|
+
cmd_gnhf(action)
|
|
403
|
+
|
|
404
|
+
else:
|
|
405
|
+
print(USAGE)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
if __name__ == "__main__":
|
|
409
|
+
main()
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""gnHF 任务模板生成器
|
|
2
|
+
|
|
3
|
+
用法: python agentprecept/gnhf_task.py [output_path]
|
|
4
|
+
|
|
5
|
+
读取当前 project-graph 状态 + git diff 摘要,
|
|
6
|
+
渲染为 gnhf --goal 可消费的 markdown 任务模板。
|
|
7
|
+
"""
|
|
8
|
+
import sys
|
|
9
|
+
import subprocess
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def git_diff_summary() -> str:
|
|
14
|
+
"""获取自上次 sync 以来的代码变更摘要"""
|
|
15
|
+
try:
|
|
16
|
+
result = subprocess.run(
|
|
17
|
+
["git", "diff", "--stat", "HEAD"],
|
|
18
|
+
capture_output=True, text=True, timeout=10
|
|
19
|
+
)
|
|
20
|
+
return result.stdout.strip() or "(无变更)"
|
|
21
|
+
except Exception:
|
|
22
|
+
return "(无法获取 git diff)"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def current_graph_state() -> str:
|
|
26
|
+
"""读取 project-graph 的概要统计"""
|
|
27
|
+
graph_path = Path("docs/project-graph.yaml")
|
|
28
|
+
if not graph_path.exists():
|
|
29
|
+
return "project-graph.yaml 不存在——首次同步"
|
|
30
|
+
|
|
31
|
+
import yaml
|
|
32
|
+
doc = yaml.safe_load(graph_path.read_text(encoding="utf-8")) or {}
|
|
33
|
+
structure = doc.get("structure", {})
|
|
34
|
+
relations = doc.get("relations", [])
|
|
35
|
+
evolution = doc.get("evolution", [])
|
|
36
|
+
|
|
37
|
+
from collections import Counter
|
|
38
|
+
type_counts = Counter(r.get("type") for r in relations)
|
|
39
|
+
|
|
40
|
+
return f"""structure: {len(structure)} 个包/模块
|
|
41
|
+
relations: {len(relations)} 条依赖
|
|
42
|
+
types: {', '.join(f'{t}:{c}' for t, c in sorted(type_counts.items()))}
|
|
43
|
+
evolution: {len(evolution)} 条 ADR"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def render_template(output_path: str = ".gnhf/sync-task.md") -> str:
|
|
47
|
+
"""生成 gnhf 任务模板"""
|
|
48
|
+
diff = git_diff_summary()
|
|
49
|
+
state = current_graph_state()
|
|
50
|
+
|
|
51
|
+
template = f"""# agentprecept: Auto-Sync Task
|
|
52
|
+
|
|
53
|
+
## 目标
|
|
54
|
+
根据最新代码结构更新 `docs/project-graph.yaml`。
|
|
55
|
+
|
|
56
|
+
## 当前知识库状态
|
|
57
|
+
{state}
|
|
58
|
+
|
|
59
|
+
## 代码变更摘要
|
|
60
|
+
```
|
|
61
|
+
{diff}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 执行规则
|
|
65
|
+
1. 运行 `python scripts/sync-graph.py src docs/project-graph.yaml`
|
|
66
|
+
2. 同步完成后运行 `python scripts/basic-audit.py docs/`
|
|
67
|
+
3. 如果 audit 无 FAIL: 任务完成
|
|
68
|
+
4. 如果 audit 有 FAIL: 修复对应问题后重新 audit
|
|
69
|
+
5. structure 中的 stability 和 description 字段必须保留
|
|
70
|
+
6. relations 全量替换——代码 import 是唯一真实来源
|
|
71
|
+
7. evolution 追加新的 ADR,不动已有的
|
|
72
|
+
|
|
73
|
+
## 期望输出
|
|
74
|
+
- `docs/project-graph.yaml` 已更新
|
|
75
|
+
- `python scripts/basic-audit.py docs/` exit 0
|
|
76
|
+
- 提交信息: `auto-sync: update project-graph ({len(diff.splitlines())} files changed)`
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
out = Path(output_path)
|
|
80
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
out.write_text(template, encoding="utf-8")
|
|
82
|
+
return template
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def main():
|
|
86
|
+
output = sys.argv[1] if len(sys.argv) > 1 else ".gnhf/sync-task.md"
|
|
87
|
+
template = render_template(output)
|
|
88
|
+
print(f"[gnhf] 任务模板已生成: {output}")
|
|
89
|
+
print(f"[gnhf] 使用方法: gnhf --agent claude --goal {output} --verify 'python scripts/basic-audit.py docs/'")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
main()
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""agentprecept MCP Server
|
|
2
|
+
|
|
3
|
+
启动: agentprecept-mcp
|
|
4
|
+
提供 6 个 tool: query / audit / diff / decision / handoff / design_gate
|
|
5
|
+
"""
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from fastmcp import FastMCP
|
|
11
|
+
except ImportError:
|
|
12
|
+
print("需要安装 fastmcp: pip install fastmcp", file=sys.stderr)
|
|
13
|
+
sys.exit(1)
|
|
14
|
+
|
|
15
|
+
_scripts = Path(__file__).resolve().parent.parent / "scripts"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load_script(name):
|
|
19
|
+
path = _scripts / f"{name}.py"
|
|
20
|
+
spec = __import__("importlib.util", fromlist=["util"]).util
|
|
21
|
+
mod_spec = spec.spec_from_file_location(name, path)
|
|
22
|
+
mod = spec.module_from_spec(mod_spec)
|
|
23
|
+
mod_spec.loader.exec_module(mod)
|
|
24
|
+
return mod
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
basic_audit = _load_script("basic-audit")
|
|
28
|
+
sync_graph = _load_script("sync-graph")
|
|
29
|
+
|
|
30
|
+
mcp = FastMCP("agentprecept")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@mcp.tool
|
|
34
|
+
def project_graph_query(module: str = "", query_type: str = "relations") -> dict:
|
|
35
|
+
graph_path = Path("docs/project-graph.yaml")
|
|
36
|
+
if not graph_path.exists():
|
|
37
|
+
return {"error": "project-graph.yaml not found—run agentprecept sync first"}
|
|
38
|
+
import yaml
|
|
39
|
+
doc = yaml.safe_load(graph_path.read_text(encoding="utf-8")) or {}
|
|
40
|
+
result = {}
|
|
41
|
+
if query_type in ("relations", "all"):
|
|
42
|
+
rels = doc.get("relations") or []
|
|
43
|
+
if module:
|
|
44
|
+
rels = [r for r in rels if r.get("from","").startswith(module) or r.get("to","").startswith(module)]
|
|
45
|
+
result["relations"] = rels
|
|
46
|
+
result["count"] = len(rels)
|
|
47
|
+
if query_type in ("structure", "all"):
|
|
48
|
+
st = doc.get("structure") or {}
|
|
49
|
+
if module:
|
|
50
|
+
st = {k:v for k,v in st.items() if k.startswith(module)}
|
|
51
|
+
result["structure"] = st
|
|
52
|
+
if query_type in ("evolution", "all"):
|
|
53
|
+
result["evolution"] = doc.get("evolution") or []
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@mcp.tool
|
|
58
|
+
def audit_run(docs_dir: str = "docs") -> dict:
|
|
59
|
+
import io
|
|
60
|
+
old = sys.stdout
|
|
61
|
+
sys.stdout = io.StringIO()
|
|
62
|
+
results = []
|
|
63
|
+
for check, name in [
|
|
64
|
+
(basic_audit.check_naming, "naming"),
|
|
65
|
+
(basic_audit.check_broken_links, "broken_links"),
|
|
66
|
+
(basic_audit.check_numbering, "numbering"),
|
|
67
|
+
(basic_audit.check_skeleton, "skeleton"),
|
|
68
|
+
(basic_audit.check_graph_schema, "graph_schema"),
|
|
69
|
+
(basic_audit.check_design_trace, "design_trace"),
|
|
70
|
+
(basic_audit.check_coverage, "coverage"),
|
|
71
|
+
(basic_audit.check_dogfood, "dogfood"),
|
|
72
|
+
]:
|
|
73
|
+
findings = check(docs_dir)
|
|
74
|
+
has_fail = any(f["severity"] == "FAIL" for f in findings)
|
|
75
|
+
results.append({"dimension": name, "status": "PASS" if not findings else "FAIL" if has_fail else "WARN", "findings": findings})
|
|
76
|
+
sys.stdout = old
|
|
77
|
+
fail = sum(1 for r in results if r["status"] == "FAIL")
|
|
78
|
+
warn = sum(1 for r in results if r["status"] == "WARN")
|
|
79
|
+
return {"results": results, "summary": {"FAIL": fail, "WARN": warn, "PASS": 8 - fail - warn}}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@mcp.tool
|
|
83
|
+
def sync_diff(src_dir: str = "src") -> dict:
|
|
84
|
+
graph_path = Path("docs/project-graph.yaml")
|
|
85
|
+
old = {}
|
|
86
|
+
if graph_path.exists():
|
|
87
|
+
import yaml
|
|
88
|
+
existing = yaml.safe_load(graph_path.read_text(encoding="utf-8")) or {}
|
|
89
|
+
old = {"structure": existing.get("structure",{}), "relations": existing.get("relations",[])}
|
|
90
|
+
new_structure = sync_graph.build_structure(src_dir)
|
|
91
|
+
all_relations = []
|
|
92
|
+
for fn in [sync_graph.build_python_relations, sync_graph.build_js_relations, sync_graph.build_db_relations, sync_graph.build_api_relations, sync_graph.build_frontend_relations, sync_graph.build_external_relations]:
|
|
93
|
+
all_relations.extend(fn(src_dir))
|
|
94
|
+
old_keys = set(old.get("structure",{}).keys())
|
|
95
|
+
new_keys = set(new_structure.keys())
|
|
96
|
+
old_rels = {(r.get("from"), r.get("to"), r.get("type")) for r in old.get("relations",[])}
|
|
97
|
+
new_rels = {(r["from"], r["to"], r["type"]) for r in all_relations}
|
|
98
|
+
from collections import Counter
|
|
99
|
+
types = Counter(r["type"] for r in all_relations)
|
|
100
|
+
return {"structure": {"added": sorted(new_keys - old_keys), "removed": sorted(old_keys - new_keys)}, "relations": {"added": len(new_rels - old_rels), "removed": len(old_rels - new_rels), "total": len(all_relations)}, "type_counts": dict(types), "needs_sync": len(new_rels) != len(old_rels)}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@mcp.tool
|
|
104
|
+
def decision_search(query: str = "") -> list:
|
|
105
|
+
p = Path("docs/L4_O01_design-rationale_设计依据.md")
|
|
106
|
+
if not p.exists():
|
|
107
|
+
return []
|
|
108
|
+
return [l.strip() for l in p.read_text(encoding="utf-8").split("\n") if l.strip().startswith("| 为什么") and (query.lower() in l.lower() if query else True)]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@mcp.tool
|
|
112
|
+
def handoff_read() -> dict:
|
|
113
|
+
p = Path("docs/HANDOFF.md")
|
|
114
|
+
if not p.exists():
|
|
115
|
+
return {"status": "HANDOFF not found"}
|
|
116
|
+
content = p.read_text(encoding="utf-8")
|
|
117
|
+
status = [l.replace("> 状态:", "").strip() for l in content.split("\n") if l.startswith("> 状态:")]
|
|
118
|
+
next_lines = []
|
|
119
|
+
in_next = False
|
|
120
|
+
for l in content.split("\n"):
|
|
121
|
+
if "## 下一步" in l:
|
|
122
|
+
in_next = True
|
|
123
|
+
elif in_next and l.strip().startswith(("1.","2.","3.","4.")):
|
|
124
|
+
next_lines.append(l.strip())
|
|
125
|
+
return {"status": status[0] if status else "", "next_step": " ".join(next_lines)}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@mcp.tool
|
|
129
|
+
def design_gate(module: str = "", operation: str = "modify") -> dict:
|
|
130
|
+
"""Agent 准备修改代码前调用。返回模块的前置设计文档状态。"""
|
|
131
|
+
import json, subprocess, sys as _sys
|
|
132
|
+
result = subprocess.run(
|
|
133
|
+
[_sys.executable, str(_scripts / "design_gate_check.py"), "--module", module, "--json"],
|
|
134
|
+
capture_output=True, text=True, timeout=10
|
|
135
|
+
)
|
|
136
|
+
if result.returncode not in (0, 1, 2):
|
|
137
|
+
return {"status": "ERROR", "gates": [], "message": result.stderr}
|
|
138
|
+
return json.loads(result.stdout)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def main():
|
|
142
|
+
mcp.run()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
main()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentprecept
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: AgentPrecept — AI coding agent governance toolkit (precept, not monitoring)
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: PyYAML>=6.0
|
|
9
|
+
Requires-Dist: fastmcp>=2.0
|
|
10
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
agentprecept/__init__.py,sha256=YjweZwiuqRqpGUekhm8om30cW6nnFDXSIUSjaqo5djU,63
|
|
2
|
+
agentprecept/cli.py,sha256=gGtngxLBeVWHNHC__EwkKwJat6vjVeSgEMZKB7IvUD4,13406
|
|
3
|
+
agentprecept/gnhf_task.py,sha256=QqBh2aMXq_eV3PdGo3U_DSoe6K2uwpmRz8MtKroOB6E,2927
|
|
4
|
+
agentprecept/mcp_server.py,sha256=5k6e4nYa7AKYjVyyT7D6KU_tUsUMX-BvF9dhlpoTFBE,5853
|
|
5
|
+
agentprecept-0.3.0.dist-info/licenses/LICENSE,sha256=9k4RFMvDVpacIGuDfT5BF0W0C954KXA8sXMLJq47vU4,1083
|
|
6
|
+
agentprecept-0.3.0.dist-info/METADATA,sha256=gnjJPNDzeG30ByjtLpHa0lLtdS83dEywRIi-7r26g_E,289
|
|
7
|
+
agentprecept-0.3.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
8
|
+
agentprecept-0.3.0.dist-info/entry_points.txt,sha256=7KGD6lOycqexAaIHIy2e01OIRQcPX7ygprmx4gov2dg,103
|
|
9
|
+
agentprecept-0.3.0.dist-info/top_level.txt,sha256=gKDqQGj_Rtzv0mSj8CFNxyZNNzZ3q2v2T-ydokqE3MQ,13
|
|
10
|
+
agentprecept-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 agent-compass contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agentprecept
|