devmate 2.3.0__tar.gz → 2.5.0__tar.gz

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.
Files changed (65) hide show
  1. {devmate-2.3.0 → devmate-2.5.0}/PKG-INFO +1 -1
  2. {devmate-2.3.0 → devmate-2.5.0}/pyproject.toml +8 -1
  3. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/__init__.py +1 -1
  4. devmate-2.5.0/src/devmate/agent/validators.py +197 -0
  5. devmate-2.5.0/src/devmate/agent_eval/__init__.py +267 -0
  6. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/app/cli.py +1184 -3
  7. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/app/tui.py +176 -78
  8. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/app/web/__init__.py +440 -29
  9. devmate-2.5.0/src/devmate/memory_store/__init__.py +184 -0
  10. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/plugin/__init__.py +4 -3
  11. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/plugin_market/__init__.py +48 -2
  12. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/rag/__init__.py +68 -0
  13. devmate-2.5.0/src/devmate/share/__init__.py +284 -0
  14. devmate-2.5.0/src/devmate/skill/__init__.py +339 -0
  15. devmate-2.5.0/src/devmate/workflow/__init__.py +1436 -0
  16. {devmate-2.3.0 → devmate-2.5.0}/src/devmate.egg-info/PKG-INFO +1 -1
  17. {devmate-2.3.0 → devmate-2.5.0}/src/devmate.egg-info/SOURCES.txt +11 -0
  18. {devmate-2.3.0 → devmate-2.5.0}/src/devmate.egg-info/entry_points.txt +7 -0
  19. {devmate-2.3.0 → devmate-2.5.0}/src/devmate.egg-info/top_level.txt +1 -0
  20. {devmate-2.3.0 → devmate-2.5.0}/src/devmate_agent/__init__.py +497 -15
  21. {devmate-2.3.0 → devmate-2.5.0}/src/devmate_monitor/__init__.py +164 -0
  22. devmate-2.5.0/src/devmate_plugins/__init__.py +9 -0
  23. devmate-2.5.0/src/devmate_plugins/code_stats.py +104 -0
  24. devmate-2.5.0/src/devmate_plugins/env_switch.py +92 -0
  25. devmate-2.5.0/src/devmate_plugins/file_watch.py +69 -0
  26. devmate-2.5.0/src/devmate_plugins/pomodoro.py +42 -0
  27. devmate-2.5.0/src/devmate_plugins/port_scan.py +60 -0
  28. {devmate-2.3.0 → devmate-2.5.0}/src/devmate_toolkit/__init__.py +82 -0
  29. devmate-2.3.0/src/devmate/workflow/__init__.py +0 -315
  30. {devmate-2.3.0 → devmate-2.5.0}/README.md +0 -0
  31. {devmate-2.3.0 → devmate-2.5.0}/setup.cfg +0 -0
  32. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/__main__.py +0 -0
  33. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/ai_config/__init__.py +0 -0
  34. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/ai_scenarios/__init__.py +0 -0
  35. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/app/__init__.py +0 -0
  36. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/app/web/cli_docs.py +0 -0
  37. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/bus/__init__.py +0 -0
  38. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/bus/bus.py +0 -0
  39. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/bus/command.py +0 -0
  40. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/core/__init__.py +0 -0
  41. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/core/config.py +0 -0
  42. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/core/logging.py +0 -0
  43. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/core/security.py +0 -0
  44. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/events/__init__.py +0 -0
  45. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/events/bus.py +0 -0
  46. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/events/domain_event.py +0 -0
  47. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/mcp/__init__.py +0 -0
  48. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/module/__init__.py +0 -0
  49. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/module/registry.py +0 -0
  50. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/plugin_market/ratings.py +0 -0
  51. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/plugin_market/remote.py +0 -0
  52. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/security/audit.py +0 -0
  53. {devmate-2.3.0 → devmate-2.5.0}/src/devmate/security/vault.py +0 -0
  54. {devmate-2.3.0 → devmate-2.5.0}/src/devmate.egg-info/dependency_links.txt +0 -0
  55. {devmate-2.3.0 → devmate-2.5.0}/src/devmate.egg-info/requires.txt +0 -0
  56. {devmate-2.3.0 → devmate-2.5.0}/src/devmate_apidev/__init__.py +0 -0
  57. {devmate-2.3.0 → devmate-2.5.0}/src/devmate_apihub/__init__.py +0 -0
  58. {devmate-2.3.0 → devmate-2.5.0}/src/devmate_dbadmin/__init__.py +0 -0
  59. {devmate-2.3.0 → devmate-2.5.0}/src/devmate_gitflow/__init__.py +0 -0
  60. {devmate-2.3.0 → devmate-2.5.0}/src/devmate_notekeeper/__init__.py +0 -0
  61. {devmate-2.3.0 → devmate-2.5.0}/src/devmate_publisher/__init__.py +0 -0
  62. {devmate-2.3.0 → devmate-2.5.0}/src/devmate_regexlab/__init__.py +0 -0
  63. {devmate-2.3.0 → devmate-2.5.0}/src/devmate_reporter/__init__.py +0 -0
  64. {devmate-2.3.0 → devmate-2.5.0}/src/devmate_scaffold/__init__.py +0 -0
  65. {devmate-2.3.0 → devmate-2.5.0}/src/devmate_sshman/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devmate
3
- Version: 2.3.0
3
+ Version: 2.5.0
4
4
  Summary: 程序员终端瑞士军刀 + AI 原生平台——替代 5 个独立工具,一个 CLI 搞定
5
5
  Author-email: DevMate Team <devmate@example.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devmate"
7
- version = "2.3.0"
7
+ version = "2.5.0"
8
8
  description = "程序员终端瑞士军刀 + AI 原生平台——替代 5 个独立工具,一个 CLI 搞定"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -89,6 +89,13 @@ audit = "devmate_audit"
89
89
  plugin = "devmate_plugin_engine"
90
90
  publisher = "devmate_publisher"
91
91
 
92
+ [project.entry-points."devmate.plugins"]
93
+ code-stats = "devmate_plugins.code_stats:CodeStatsPlugin"
94
+ pomodoro = "devmate_plugins.pomodoro:PomodoroPlugin"
95
+ env-switch = "devmate_plugins.env_switch:EnvSwitchPlugin"
96
+ port-scan = "devmate_plugins.port_scan:PortScanPlugin"
97
+ file-watch = "devmate_plugins.file_watch:FileWatchPlugin"
98
+
92
99
  [tool.setuptools.packages.find]
93
100
  where = ["src"]
94
101
  include = ["devmate*", "devmate_*"]
@@ -1,6 +1,6 @@
1
1
  """
2
2
  DevMate — 程序员终端瑞士军刀
3
3
  """
4
- __version__ = "2.3.0"
4
+ __version__ = "2.5.0"
5
5
  __app_name__ = "DevMate"
6
6
  __description__ = "程序员终端瑞士军刀——替代 5 个独立工具,一个 CLI 搞定"
@@ -0,0 +1,197 @@
1
+ """devmate.agent.validators — 验证信号注入(P0)
2
+
3
+ 确定性校验器集合:写文件后自动校验,反馈给 Agent 自纠。
4
+ 遵循原则:能用计算型信号解决的优先交给确定性程序。
5
+
6
+ 校验器:
7
+ - python_syntax: py_compile 语法检查
8
+ - python_style: ruff check 代码风格(可用时)
9
+ - json_valid: json.loads 合法 JSON
10
+ - shell_syntax: bash -n 语法检查
11
+ - yaml_valid: yaml 解析(可用时)
12
+ """
13
+
14
+ import json
15
+ import subprocess
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+
19
+
20
+ @dataclass
21
+ class ValidatorResult:
22
+ """校验结果"""
23
+ ok: bool
24
+ validator: str
25
+ file: str
26
+ errors: list[str] = field(default_factory=list)
27
+ suggestions: list[str] = field(default_factory=list)
28
+
29
+ def to_feedback(self) -> str:
30
+ """转为反馈给 Agent 的文本(含修复建议)"""
31
+ if self.ok:
32
+ return f"✅ 校验通过 [{self.validator}]: {self.file}"
33
+ lines = [f"❌ 校验失败 [{self.validator}]: {self.file}"]
34
+ for e in self.errors[:10]:
35
+ lines.append(f" - {e}")
36
+ if self.suggestions:
37
+ lines.append(" 修复建议:")
38
+ for s in self.suggestions[:5]:
39
+ lines.append(f" * {s}")
40
+ return "\n".join(lines)
41
+
42
+
43
+ class BaseValidator:
44
+ """校验器基类"""
45
+ name = "base"
46
+
47
+ def validate(self, path: str) -> ValidatorResult:
48
+ raise NotImplementedError
49
+
50
+
51
+ class PythonSyntaxValidator(BaseValidator):
52
+ """Python 语法检查(py_compile,零依赖)"""
53
+ name = "python_syntax"
54
+
55
+ def validate(self, path: str) -> ValidatorResult:
56
+ import py_compile
57
+ try:
58
+ py_compile.compile(path, doraise=True)
59
+ return ValidatorResult(True, self.name, path)
60
+ except py_compile.PyCompileError as e:
61
+ return ValidatorResult(
62
+ False, self.name, path,
63
+ errors=[str(e).splitlines()[0] if str(e) else "语法错误"],
64
+ suggestions=["检查缩进、括号配对、引号闭合",
65
+ "可用 `python -m py_compile <file>` 本地复现"],
66
+ )
67
+ except Exception as e:
68
+ return ValidatorResult(False, self.name, path, errors=[str(e)])
69
+
70
+
71
+ class PythonStyleValidator(BaseValidator):
72
+ """Python 代码风格(ruff,可用时)"""
73
+ name = "python_style"
74
+
75
+ def validate(self, path: str) -> ValidatorResult:
76
+ try:
77
+ result = subprocess.run(
78
+ ["ruff", "check", path],
79
+ capture_output=True, text=True, timeout=15,
80
+ )
81
+ except (FileNotFoundError, subprocess.TimeoutExpired):
82
+ return ValidatorResult(True, self.name, path) # ruff 不可用跳过
83
+ if result.returncode == 0:
84
+ return ValidatorResult(True, self.name, path)
85
+ errors = [l for l in result.stdout.splitlines() if ":" in l][:10] # noqa: E741
86
+ return ValidatorResult(
87
+ False, self.name, path,
88
+ errors=errors,
89
+ suggestions=["运行 `ruff check --fix <file>` 自动修复",
90
+ "常见:未使用导入/过长行/命名不规范"],
91
+ )
92
+
93
+
94
+ class JsonValidator(BaseValidator):
95
+ """JSON 合法性"""
96
+ name = "json_valid"
97
+
98
+ def validate(self, path: str) -> ValidatorResult:
99
+ try:
100
+ Path(path).read_text(encoding="utf-8")
101
+ json.loads(Path(path).read_text(encoding="utf-8"))
102
+ return ValidatorResult(True, self.name, path)
103
+ except json.JSONDecodeError as e:
104
+ return ValidatorResult(
105
+ False, self.name, path,
106
+ errors=[f"行 {e.lineno} 列 {e.colno}: {e.msg}"],
107
+ suggestions=["检查逗号、引号、括号配对",
108
+ "可用 `python -m json.tool <file>` 定位错误"],
109
+ )
110
+ except Exception as e:
111
+ return ValidatorResult(False, self.name, path, errors=[str(e)])
112
+
113
+
114
+ class ShellSyntaxValidator(BaseValidator):
115
+ """Shell 语法检查(bash -n)"""
116
+ name = "shell_syntax"
117
+
118
+ def validate(self, path: str) -> ValidatorResult:
119
+ try:
120
+ result = subprocess.run(
121
+ ["bash", "-n", path],
122
+ capture_output=True, text=True, timeout=10,
123
+ )
124
+ except (FileNotFoundError, subprocess.TimeoutExpired):
125
+ return ValidatorResult(True, self.name, path) # bash 不可用跳过
126
+ if result.returncode == 0:
127
+ return ValidatorResult(True, self.name, path)
128
+ return ValidatorResult(
129
+ False, self.name, path,
130
+ errors=[l for l in result.stderr.splitlines()[:10] if l.strip()], # noqa: E741
131
+ suggestions=["检查引号/括号配对、if/for 语句闭合"],
132
+ )
133
+
134
+
135
+ class YamlValidator(BaseValidator):
136
+ """YAML 合法性(pyyaml 可用时)"""
137
+ name = "yaml_valid"
138
+
139
+ def validate(self, path: str) -> ValidatorResult:
140
+ try:
141
+ import yaml
142
+ except ImportError:
143
+ return ValidatorResult(True, self.name, path) # yaml 不可用跳过
144
+ try:
145
+ yaml.safe_load(Path(path).read_text(encoding="utf-8"))
146
+ return ValidatorResult(True, self.name, path)
147
+ except yaml.YAMLError as e:
148
+ return ValidatorResult(
149
+ False, self.name, path,
150
+ errors=[str(e).splitlines()[0] if str(e) else "YAML 错误"],
151
+ suggestions=["检查缩进一致性(YAML 对缩进敏感)"],
152
+ )
153
+
154
+
155
+ # 按扩展名注册校验器
156
+ EXT_VALIDATORS: dict[str, list[BaseValidator]] = {
157
+ ".py": [PythonSyntaxValidator(), PythonStyleValidator()],
158
+ ".json": [JsonValidator()],
159
+ ".sh": [ShellSyntaxValidator()],
160
+ ".bash": [ShellSyntaxValidator()],
161
+ ".yml": [YamlValidator()],
162
+ ".yaml": [YamlValidator()],
163
+ }
164
+
165
+
166
+ def get_validators(path: str) -> list[BaseValidator]:
167
+ """获取文件对应的校验器列表"""
168
+ ext = Path(path).suffix.lower()
169
+ return EXT_VALIDATORS.get(ext, [])
170
+
171
+
172
+ def validate_file(path: str) -> list[ValidatorResult]:
173
+ """校验单个文件,返回全部结果"""
174
+ results = []
175
+ for v in get_validators(path):
176
+ try:
177
+ results.append(v.validate(path))
178
+ except Exception as e:
179
+ results.append(ValidatorResult(False, v.name, path, errors=[str(e)]))
180
+ return results
181
+
182
+
183
+ def validate_file_feedback(path: str) -> str:
184
+ """校验文件并返回组合反馈(空 = 全部通过)"""
185
+ results = validate_file(path)
186
+ failed = [r for r in results if not r.ok]
187
+ if not failed:
188
+ return ""
189
+ return "\n".join(r.to_feedback() for r in failed)
190
+
191
+
192
+ def summarize_validation(results: list[ValidatorResult]) -> str:
193
+ """汇总校验结果(供审计)"""
194
+ if not results:
195
+ return "无适用校验器"
196
+ ok_count = sum(1 for r in results if r.ok)
197
+ return f"{ok_count}/{len(results)} 校验通过"
@@ -0,0 +1,267 @@
1
+ """devmate.agent_eval — Agent 评测体系(P0 补充)
2
+
3
+ 借鉴 Anthropic 对抗评估方法论:
4
+ - 行为清单(Checklist):把任务拆成 200+ 条具体行为描述(JSON)
5
+ - 每条标 pass/fail,禁止删条目或降标准
6
+ - Planner/Generator/Evaluator 三角色分工
7
+
8
+ 核心思路:用确定性信号(校验器/文件存在/命令输出)自动判定 pass/fail,
9
+ 不依赖 LLM 判断,保证评测可复现、可追踪。
10
+ """
11
+
12
+ import json
13
+ import time
14
+ from collections.abc import Callable
15
+ from dataclasses import dataclass, field # noqa: F401
16
+ from pathlib import Path
17
+
18
+ from devmate.bus.command import CommandResult
19
+
20
+ EVAL_DIR = Path.home() / ".devmate" / "eval"
21
+ REPORTS_DIR = EVAL_DIR / "reports"
22
+
23
+
24
+ @dataclass
25
+ class BehaviorItem:
26
+ """行为清单条目"""
27
+ id: str # 如 B-001
28
+ description: str # 行为描述
29
+ category: str = "general" # 分类
30
+ check: str = "" # 检查方式(shell 命令或特殊标识)
31
+ required: bool = True # 是否必过
32
+
33
+ def to_dict(self) -> dict:
34
+ return {"id": self.id, "description": self.description,
35
+ "category": self.category, "check": self.check,
36
+ "required": self.required}
37
+
38
+
39
+ @dataclass
40
+ class EvalResult:
41
+ """单条行为评测结果"""
42
+ item: BehaviorItem
43
+ passed: bool
44
+ detail: str = ""
45
+
46
+ def to_dict(self) -> dict:
47
+ return {"id": self.item.id, "description": self.item.description,
48
+ "category": self.item.category, "passed": self.passed,
49
+ "detail": self.detail, "required": self.item.required}
50
+
51
+
52
+ # ── 检查器(确定性验证信号) ─────────────────────
53
+
54
+ def _check_file_exists(path: str) -> bool:
55
+ return Path(path).expanduser().exists()
56
+
57
+
58
+ def _check_file_contains(path: str, text: str) -> bool:
59
+ p = Path(path).expanduser()
60
+ if not p.exists():
61
+ return False
62
+ try:
63
+ return text in p.read_text(errors="replace")
64
+ except Exception:
65
+ return False
66
+
67
+
68
+ def _check_command(cmd: str) -> tuple[bool, str]:
69
+ """执行 shell 命令检查(返回码 0 = pass)"""
70
+ import subprocess
71
+ try:
72
+ result = subprocess.run(
73
+ ["bash", "-c", cmd], capture_output=True, text=True, timeout=30)
74
+ return result.returncode == 0, (result.stdout + result.stderr)[:200]
75
+ except Exception as e:
76
+ return False, str(e)
77
+
78
+
79
+ def _check_python_syntax(path: str) -> bool:
80
+ import py_compile
81
+ try:
82
+ py_compile.compile(path, doraise=True)
83
+ return True
84
+ except Exception:
85
+ return False
86
+
87
+
88
+ def _check_json_valid(path: str) -> bool:
89
+ try:
90
+ json.loads(Path(path).read_text())
91
+ return True
92
+ except Exception:
93
+ return False
94
+
95
+
96
+ # ── 行为清单执行器 ──────────────────────────────
97
+
98
+ def run_checklist(items: list[BehaviorItem], workdir: str = ".") -> CommandResult:
99
+ """执行行为清单评测
100
+
101
+ Args:
102
+ items: 行为条目列表
103
+ workdir: 工作目录(相对路径基准)
104
+ """
105
+ results = []
106
+ passed = 0
107
+ failed = 0
108
+ skipped = []
109
+
110
+ for item in items:
111
+ check = item.check.strip()
112
+ ok = False
113
+ detail = ""
114
+
115
+ try:
116
+ if check.startswith("file_exists:"):
117
+ path = check.split(":", 1)[1].strip()
118
+ if not Path(path).is_absolute():
119
+ path = str(Path(workdir) / path)
120
+ ok = _check_file_exists(path)
121
+ detail = f"文件存在: {path}" if ok else f"文件不存在: {path}"
122
+ elif check.startswith("file_contains:"):
123
+ _, rest = check.split(":", 1)
124
+ path, _, text = rest.partition("::")
125
+ if not Path(path).is_absolute():
126
+ path = str(Path(workdir) / path)
127
+ ok = _check_file_contains(path.strip(), text.strip())
128
+ detail = f"含文本: {text.strip()}" if ok else f"缺文本: {text.strip()}"
129
+ elif check.startswith("python_syntax:"):
130
+ path = check.split(":", 1)[1].strip()
131
+ if not Path(path).is_absolute():
132
+ path = str(Path(workdir) / path)
133
+ ok = _check_python_syntax(path)
134
+ detail = f"Python 语法: {path}" if ok else f"语法错误: {path}"
135
+ elif check.startswith("json_valid:"):
136
+ path = check.split(":", 1)[1].strip()
137
+ if not Path(path).is_absolute():
138
+ path = str(Path(workdir) / path)
139
+ ok = _check_json_valid(path)
140
+ detail = f"JSON 合法: {path}" if ok else f"JSON 无效: {path}"
141
+ elif check.startswith("cmd:"):
142
+ ok, detail = _check_command(check.split(":", 1)[1].strip())
143
+ elif check:
144
+ # 未知检查方式 → 跳过(不降标准)
145
+ skipped.append(item.id)
146
+ continue
147
+ else:
148
+ # 无检查 → 视为描述性条目(由评估者人工/LLM 判定)
149
+ skipped.append(item.id)
150
+ continue
151
+ except Exception as e:
152
+ ok = False
153
+ detail = str(e)
154
+
155
+ results.append(EvalResult(item=item, passed=ok, detail=detail))
156
+ if ok:
157
+ passed += 1
158
+ else:
159
+ failed += 1
160
+
161
+ total = len(results)
162
+ score = round(passed / total * 100, 1) if total else 0.0
163
+ required_failed = [r for r in results if not r.passed and r.item.required]
164
+
165
+ report = {
166
+ "time": time.strftime("%Y-%m-%d %H:%M:%S"),
167
+ "total": total, "passed": passed, "failed": failed,
168
+ "skipped": skipped, "score": score,
169
+ "pass_rate": f"{score}%",
170
+ "required_failed": len(required_failed),
171
+ "status": "PASS" if not required_failed else "FAIL",
172
+ "results": [r.to_dict() for r in results],
173
+ }
174
+
175
+ # 保存报告
176
+ REPORTS_DIR.mkdir(parents=True, exist_ok=True)
177
+ report_path = REPORTS_DIR / f"eval-{time.strftime('%Y%m%d-%H%M%S')}.json"
178
+ report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2))
179
+
180
+ return CommandResult(success=not required_failed, data={
181
+ "report": report,
182
+ "report_path": str(report_path),
183
+ "message": (f"评测 {'✅ PASS' if not required_failed else '❌ FAIL'}: "
184
+ f"{passed}/{total} 通过 ({score}%)"),
185
+ })
186
+
187
+
188
+ # ── 内置评测套件 ────────────────────────────────
189
+
190
+ def _suite_write_python_file() -> list[BehaviorItem]:
191
+ """B1: Agent 写 Python 文件 → 语法/文件/内容正确"""
192
+ return [
193
+ BehaviorItem("B1-001", "生成 app.py 文件", "write",
194
+ "file_exists:app.py"),
195
+ BehaviorItem("B1-002", "app.py 语法正确", "write",
196
+ "python_syntax:app.py"),
197
+ BehaviorItem("B1-003", "包含 Flask 应用", "write",
198
+ "file_contains:app.py::Flask"),
199
+ BehaviorItem("B1-004", "包含路由定义", "write",
200
+ "file_contains:app.py::@app.route"),
201
+ BehaviorItem("B1-005", "包含主入口", "write",
202
+ "file_contains:app.py::__main__"),
203
+ ]
204
+
205
+
206
+ def _suite_generate_config() -> list[BehaviorItem]:
207
+ """B2: Agent 生成 JSON 配置 → 合法/字段正确"""
208
+ return [
209
+ BehaviorItem("B2-001", "生成 config.json", "write",
210
+ "file_exists:config.json"),
211
+ BehaviorItem("B2-002", "config.json 合法 JSON", "write",
212
+ "json_valid:config.json"),
213
+ BehaviorItem("B2-003", "含 server 字段", "write",
214
+ "file_contains:config.json::server"),
215
+ BehaviorItem("B2-004", "含 port 字段", "write",
216
+ "file_contains:config.json::port"),
217
+ ]
218
+
219
+
220
+ def _suite_git_operations() -> list[BehaviorItem]:
221
+ """B3: Agent 在 Git 仓库操作 → 仓库状态正确"""
222
+ return [
223
+ BehaviorItem("B3-001", "仓库存在", "git", "cmd:test -d .git"),
224
+ BehaviorItem("B3-002", "有提交记录", "git", "cmd:test -n \"$(git log --oneline -1 2>/dev/null)\""), # noqa: E501
225
+ BehaviorItem("B3-003", "工作区无未提交变更", "git",
226
+ "cmd:test -z \"$(git status --porcelain 2>/dev/null)\""),
227
+ ]
228
+
229
+
230
+ def _suite_report_generation() -> list[BehaviorItem]:
231
+ """B4: Agent 生成 Markdown 报告 → 结构完整"""
232
+ return [
233
+ BehaviorItem("B4-001", "生成 report.md", "report",
234
+ "file_exists:report.md"),
235
+ BehaviorItem("B4-002", "含标题", "report",
236
+ "file_contains:report.md::#"),
237
+ BehaviorItem("B4-003", "含结论", "report",
238
+ "file_contains:report.md::结论"),
239
+ ]
240
+
241
+
242
+ BUILTIN_SUITES: dict[str, Callable[[], list[BehaviorItem]]] = {
243
+ "write-python": _suite_write_python_file,
244
+ "generate-config": _suite_generate_config,
245
+ "git-operations": _suite_git_operations,
246
+ "report-generation": _suite_report_generation,
247
+ }
248
+
249
+
250
+ def list_suites() -> CommandResult:
251
+ """列出评测套件"""
252
+ return CommandResult(success=True, data={
253
+ "suites": [
254
+ {"name": name, "items": len(fn()), "checks": [
255
+ i.check.split(":")[0] for i in fn()]}
256
+ for name, fn in BUILTIN_SUITES.items()
257
+ ],
258
+ "total": len(BUILTIN_SUITES),
259
+ })
260
+
261
+
262
+ def run_suite(name: str, workdir: str = ".") -> CommandResult:
263
+ """运行指定评测套件"""
264
+ if name not in BUILTIN_SUITES:
265
+ return CommandResult(success=False, error=f"评测套件不存在: {name}")
266
+ items = BUILTIN_SUITES[name]()
267
+ return run_checklist(items, workdir=workdir)