devmate 2.5.0__tar.gz → 2.5.2__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.5.0 → devmate-2.5.2}/PKG-INFO +1 -1
  2. {devmate-2.5.0 → devmate-2.5.2}/pyproject.toml +1 -1
  3. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/__init__.py +1 -1
  4. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/agent/validators.py +86 -0
  5. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/app/cli.py +24 -7
  6. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/app/web/__init__.py +15 -0
  7. devmate-2.5.2/src/devmate/sub_agent/__init__.py +237 -0
  8. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/workflow/__init__.py +104 -5
  9. {devmate-2.5.0 → devmate-2.5.2}/src/devmate.egg-info/PKG-INFO +1 -1
  10. {devmate-2.5.0 → devmate-2.5.2}/src/devmate.egg-info/SOURCES.txt +1 -0
  11. {devmate-2.5.0 → devmate-2.5.2}/src/devmate_agent/__init__.py +81 -17
  12. {devmate-2.5.0 → devmate-2.5.2}/README.md +0 -0
  13. {devmate-2.5.0 → devmate-2.5.2}/setup.cfg +0 -0
  14. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/__main__.py +0 -0
  15. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/agent_eval/__init__.py +0 -0
  16. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/ai_config/__init__.py +0 -0
  17. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/ai_scenarios/__init__.py +0 -0
  18. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/app/__init__.py +0 -0
  19. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/app/tui.py +0 -0
  20. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/app/web/cli_docs.py +0 -0
  21. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/bus/__init__.py +0 -0
  22. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/bus/bus.py +0 -0
  23. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/bus/command.py +0 -0
  24. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/core/__init__.py +0 -0
  25. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/core/config.py +0 -0
  26. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/core/logging.py +0 -0
  27. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/core/security.py +0 -0
  28. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/events/__init__.py +0 -0
  29. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/events/bus.py +0 -0
  30. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/events/domain_event.py +0 -0
  31. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/mcp/__init__.py +0 -0
  32. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/memory_store/__init__.py +0 -0
  33. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/module/__init__.py +0 -0
  34. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/module/registry.py +0 -0
  35. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/plugin/__init__.py +0 -0
  36. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/plugin_market/__init__.py +0 -0
  37. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/plugin_market/ratings.py +0 -0
  38. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/plugin_market/remote.py +0 -0
  39. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/rag/__init__.py +0 -0
  40. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/security/audit.py +0 -0
  41. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/security/vault.py +0 -0
  42. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/share/__init__.py +0 -0
  43. {devmate-2.5.0 → devmate-2.5.2}/src/devmate/skill/__init__.py +0 -0
  44. {devmate-2.5.0 → devmate-2.5.2}/src/devmate.egg-info/dependency_links.txt +0 -0
  45. {devmate-2.5.0 → devmate-2.5.2}/src/devmate.egg-info/entry_points.txt +0 -0
  46. {devmate-2.5.0 → devmate-2.5.2}/src/devmate.egg-info/requires.txt +0 -0
  47. {devmate-2.5.0 → devmate-2.5.2}/src/devmate.egg-info/top_level.txt +0 -0
  48. {devmate-2.5.0 → devmate-2.5.2}/src/devmate_apidev/__init__.py +0 -0
  49. {devmate-2.5.0 → devmate-2.5.2}/src/devmate_apihub/__init__.py +0 -0
  50. {devmate-2.5.0 → devmate-2.5.2}/src/devmate_dbadmin/__init__.py +0 -0
  51. {devmate-2.5.0 → devmate-2.5.2}/src/devmate_gitflow/__init__.py +0 -0
  52. {devmate-2.5.0 → devmate-2.5.2}/src/devmate_monitor/__init__.py +0 -0
  53. {devmate-2.5.0 → devmate-2.5.2}/src/devmate_notekeeper/__init__.py +0 -0
  54. {devmate-2.5.0 → devmate-2.5.2}/src/devmate_plugins/__init__.py +0 -0
  55. {devmate-2.5.0 → devmate-2.5.2}/src/devmate_plugins/code_stats.py +0 -0
  56. {devmate-2.5.0 → devmate-2.5.2}/src/devmate_plugins/env_switch.py +0 -0
  57. {devmate-2.5.0 → devmate-2.5.2}/src/devmate_plugins/file_watch.py +0 -0
  58. {devmate-2.5.0 → devmate-2.5.2}/src/devmate_plugins/pomodoro.py +0 -0
  59. {devmate-2.5.0 → devmate-2.5.2}/src/devmate_plugins/port_scan.py +0 -0
  60. {devmate-2.5.0 → devmate-2.5.2}/src/devmate_publisher/__init__.py +0 -0
  61. {devmate-2.5.0 → devmate-2.5.2}/src/devmate_regexlab/__init__.py +0 -0
  62. {devmate-2.5.0 → devmate-2.5.2}/src/devmate_reporter/__init__.py +0 -0
  63. {devmate-2.5.0 → devmate-2.5.2}/src/devmate_scaffold/__init__.py +0 -0
  64. {devmate-2.5.0 → devmate-2.5.2}/src/devmate_sshman/__init__.py +0 -0
  65. {devmate-2.5.0 → devmate-2.5.2}/src/devmate_toolkit/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devmate
3
- Version: 2.5.0
3
+ Version: 2.5.2
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.5.0"
7
+ version = "2.5.2"
8
8
  description = "程序员终端瑞士军刀 + AI 原生平台——替代 5 个独立工具,一个 CLI 搞定"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -1,6 +1,6 @@
1
1
  """
2
2
  DevMate — 程序员终端瑞士军刀
3
3
  """
4
- __version__ = "2.5.0"
4
+ __version__ = "2.5.2"
5
5
  __app_name__ = "DevMate"
6
6
  __description__ = "程序员终端瑞士军刀——替代 5 个独立工具,一个 CLI 搞定"
@@ -195,3 +195,89 @@ def summarize_validation(results: list[ValidatorResult]) -> str:
195
195
  return "无适用校验器"
196
196
  ok_count = sum(1 for r in results if r.ok)
197
197
  return f"{ok_count}/{len(results)} 校验通过"
198
+
199
+
200
+ # ═══════════════════════════════════════════════════
201
+ # 自动修复闭环(P0-②):校验失败 → 自动修复 → 重验
202
+ # ═══════════════════════════════════════════════════
203
+
204
+ def auto_fix(path: str, dry_run: bool = False) -> dict:
205
+ """校验失败时尝试自动修复
206
+
207
+ 当前支持:
208
+ - python_style: ruff check --fix(自动修风格问题)
209
+ - python_syntax: 无自动修复(需 Agent 介入)
210
+
211
+ Args:
212
+ path: 文件路径
213
+ dry_run: 仅预览修复(ruff --diff)
214
+
215
+ Returns:
216
+ {"fixed": bool, "before": str, "after": str, "details": [...]}
217
+ """
218
+ result = {"fixed": False, "before": "", "after": "", "details": []}
219
+ ext = Path(path).suffix.lower()
220
+
221
+ # ruff 自动修复(Python 风格问题)
222
+ if ext == ".py":
223
+ try:
224
+ cmd = ["ruff", "check", "--fix"]
225
+ if dry_run:
226
+ cmd = ["ruff", "check", "--diff"]
227
+ cmd.append(path)
228
+ r = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
229
+ if r.returncode in (0, 1):
230
+ # returncode 0 = 无问题;1 = 有问题但已修/有剩余
231
+ # 检测是否有修复
232
+ after = Path(path).read_text(errors="replace")
233
+ before = result["before"] # noqa: F841
234
+ if dry_run:
235
+ result["details"].append("dry-run: 预览修复差异")
236
+ result["after"] = r.stdout[:500]
237
+ else:
238
+ result["after"] = after[:200]
239
+ # 检查是否还有残留问题
240
+ remaining = validate_file_feedback(path)
241
+ if not remaining:
242
+ result["fixed"] = True
243
+ result["details"].append("ruff 自动修复完成,校验通过")
244
+ else:
245
+ result["details"].append(f"ruff 修复后仍有问题: {remaining[:100]}")
246
+ except FileNotFoundError:
247
+ result["details"].append("ruff 不可用,跳过自动修复")
248
+
249
+ # 记录修复前状态(用于对比)
250
+ try:
251
+ result["before"] = Path(path).read_text(errors="replace")[:200]
252
+ except Exception:
253
+ pass
254
+
255
+ return result
256
+
257
+
258
+ def validate_with_autofix(path: str) -> dict:
259
+ """校验 + 自动修复 + 重验 完整闭环
260
+
261
+ Returns:
262
+ {"feedback": str, "auto_fixed": bool, "results": [...]}
263
+ """
264
+ # 第一轮校验
265
+ feedback = validate_file_feedback(path)
266
+ auto_fixed = False
267
+ details = []
268
+
269
+ if feedback:
270
+ # 尝试自动修复
271
+ fix_result = auto_fix(path)
272
+ auto_fixed = fix_result["fixed"]
273
+ if fix_result["details"]:
274
+ details.extend(fix_result["details"])
275
+ # 重验
276
+ feedback = validate_file_feedback(path)
277
+
278
+ return {
279
+ "feedback": feedback,
280
+ "auto_fixed": auto_fixed,
281
+ "details": details,
282
+ "results": validate_file(path),
283
+ }
@@ -3524,20 +3524,29 @@ def wf_drift_check():
3524
3524
 
3525
3525
 
3526
3526
  @wf_group.command("daemon", help="调度守护进程")
3527
- @click.argument("action", type=click.Choice(["start", "stop", "status"]))
3527
+ @click.argument("action", type=click.Choice(["start", "stop", "status", "autostart", "no-autostart"])) # noqa: E501
3528
3528
  @click.option("--interval", default=30, type=int, help="检查间隔(秒)")
3529
- def wf_daemon(action, interval):
3529
+ @click.option("--retries", default=0, type=int, help="失败自动重试次数")
3530
+ def wf_daemon(action, interval, retries):
3530
3531
  """调度守护进程:常驻自动执行 cron 到期的调度任务
3531
3532
 
3532
3533
  用法:
3533
- devmate wf daemon start [--interval 30] # 启动(每 30s 检查)
3534
- devmate wf daemon status # 查询状态
3535
- devmate wf daemon stop # 停止
3534
+ devmate wf daemon start [--interval 30] [--retries 2] # 启动(重试 2 次)
3535
+ devmate wf daemon status # 查询状态
3536
+ devmate wf daemon stop # 停止
3537
+ devmate wf daemon autostart # 安装 launchd 开机自启(macOS)
3538
+ devmate wf daemon no-autostart # 卸载开机自启
3536
3539
  """
3537
- from devmate.workflow import daemon_start, daemon_status, daemon_stop
3540
+ from devmate.workflow import (
3541
+ daemon_install_launchd,
3542
+ daemon_start,
3543
+ daemon_status,
3544
+ daemon_stop,
3545
+ daemon_uninstall_launchd,
3546
+ )
3538
3547
 
3539
3548
  if action == "start":
3540
- result = daemon_start(interval=interval)
3549
+ result = daemon_start(interval=interval, retries=retries)
3541
3550
  console.print(f"[green]{result.data['message']}[/green]" if result.success
3542
3551
  else f"[red]❌ {result.error}[/red]")
3543
3552
  console.print(f"[dim]日志: {result.data.get('log', '')}[/dim]")
@@ -3552,6 +3561,14 @@ def wf_daemon(action, interval):
3552
3561
  result = daemon_stop()
3553
3562
  console.print(f"[green]{result.data['message']}[/green]" if result.success
3554
3563
  else f"[yellow]❌ {result.error}[/yellow]")
3564
+ elif action == "autostart":
3565
+ result = daemon_install_launchd()
3566
+ console.print(f"[green]{result.data['message']}[/green]" if result.success
3567
+ else f"[red]❌ {result.error}[/red]")
3568
+ elif action == "no-autostart":
3569
+ result = daemon_uninstall_launchd()
3570
+ console.print(f"[green]{result.data['message']}[/green]" if result.success
3571
+ else f"[red]❌ {result.error}[/red]")
3555
3572
 
3556
3573
 
3557
3574
  @wf_group.command("rm-step", help="删除工作流的第 N 个步骤")
@@ -781,6 +781,21 @@ async def ai_config_set(request: Request, _: str = Depends(require_auth)):
781
781
 
782
782
  # ── Skill 库页面 ─────────────────────────────────
783
783
 
784
+ @app.get("/plugins", response_class=HTMLResponse)
785
+ async def plugins_page(request: Request, _: str = Depends(require_auth),
786
+ message: str = ""):
787
+ """已安装插件管理页面"""
788
+ from devmate.plugin import list_installed, registry
789
+ installed = list_installed()
790
+ discovered = [p for p in registry.discover() if "error" not in p]
791
+ return templates.TemplateResponse(
792
+ "plugins.html",
793
+ {"request": request, "modules": MODULES, "active_module": "",
794
+ "installed": installed, "discovered": discovered,
795
+ "version": __version__, "message": message},
796
+ )
797
+
798
+
784
799
  @app.get("/skill", response_class=HTMLResponse)
785
800
  async def skill_page(request: Request, _: str = Depends(require_auth),
786
801
  message: str = ""):
@@ -0,0 +1,237 @@
1
+ """devmate.sub_agent — Sub-agent 隔离(P0-⑥ · Context Isolate)
2
+
3
+ 方法论:Context Engineering 五动作之一「隔离(Isolate)」
4
+ - 旁支任务委派给独立上下文的 Sub-agent
5
+ - 主 Agent 不承载子任务细节,只拿回结构化结果摘要
6
+ - 长任务 → 拆子任务 → 各子任务独立上下文 → 汇总
7
+ - 输出契约(多 Agent 工程三原则):每个子任务声明期望输出格式
8
+ 缺失字段 → 标记 schema_failed(关键节点校验)
9
+
10
+ 设计:
11
+ - Sub-agent 复用 AgentRuntime(独立 SessionMemory,互不污染)
12
+ - 结果自动截断/摘要,只回传关键信息给主上下文
13
+ - 支持并行执行多个子任务(ThreadPoolExecutor)
14
+ - 输出契约:expected_schema 声明字段名→类型,结果校验缺字段/类型错
15
+ """
16
+
17
+ import json
18
+ import time
19
+ from dataclasses import dataclass, field
20
+ from pathlib import Path # noqa: F401
21
+
22
+ from devmate.bus.command import CommandResult
23
+
24
+ # 允许的 schema 类型
25
+ _SCHEMA_TYPES = {"string": str, "int": int, "float": (int, float),
26
+ "bool": bool, "list": list, "dict": dict, "any": object}
27
+
28
+
29
+ @dataclass
30
+ class SubTask:
31
+ """子任务定义"""
32
+ name: str # 子任务名
33
+ prompt: str # 子任务指令(独立上下文)
34
+ max_steps: int = 5 # 最大步数
35
+ tool_whitelist: list[str] = field(default_factory=list) # 允许的工具白名单
36
+ expected_schema: dict | None = None # 输出契约 {字段: 类型}(如 {"host": "string"})
37
+ schema_required: bool = True # 契约校验失败是否算任务失败
38
+
39
+ def to_dict(self) -> dict:
40
+ return {"name": self.name, "prompt": self.prompt,
41
+ "max_steps": self.max_steps,
42
+ "tool_whitelist": self.tool_whitelist,
43
+ "expected_schema": self.expected_schema}
44
+
45
+
46
+ def validate_schema(output: str, schema: dict) -> tuple[bool, list[str]]:
47
+ """校验输出是否符合契约
48
+
49
+ 策略(宽容解析):
50
+ 1. 尝试 JSON 解析(输出可能是 JSON)
51
+ 2. 否则尝试 key: value 逐行解析
52
+ 3. 从文本中按字段名查找
53
+
54
+ Returns:
55
+ (是否通过, 缺失/错误字段列表)
56
+ """
57
+ if not schema:
58
+ return True, []
59
+
60
+ # 尝试 JSON 解析
61
+ obj = None
62
+ try:
63
+ obj = json.loads(output)
64
+ except Exception:
65
+ pass
66
+
67
+ issues = []
68
+ for field_name, field_type in schema.items():
69
+ # 定位值
70
+ value = None
71
+ if isinstance(obj, dict):
72
+ value = obj.get(field_name)
73
+ elif obj is not None:
74
+ continue # JSON 数组等,无法按字段校验
75
+ else:
76
+ # 行解析:key: value
77
+ for line in output.splitlines():
78
+ line = line.strip().lstrip("-• ")
79
+ if ":" in line:
80
+ k, _, v = line.partition(":")
81
+ if k.strip().strip('"').strip("'") == field_name:
82
+ value = v.strip()
83
+ break
84
+ if value is None:
85
+ # 全文包含字段名
86
+ if field_name in output:
87
+ continue # 出现但无法解析值,视为通过
88
+
89
+ if value is None or value == "":
90
+ issues.append(f"{field_name} 缺失")
91
+ continue
92
+
93
+ # 类型校验(宽容:去掉逗号/百分号/中文单位后缀)
94
+ expected_type = _SCHEMA_TYPES.get(field_type, object)
95
+ try:
96
+ clean = str(value).replace(",", "").replace("%", "")
97
+ # 去常见中英文单位后缀(核/GB/MB/秒/ms/元/%)
98
+ for unit in ("核", "个", "台", "GB", "MB", "KB", "秒", "ms", "元",
99
+ "字节", "张", "条"):
100
+ clean = clean.removesuffix(unit)
101
+ clean = clean.strip()
102
+ if field_type == "int":
103
+ int(clean)
104
+ elif field_type == "float":
105
+ float(clean)
106
+ elif field_type == "string":
107
+ str(value)
108
+ elif field_type in ("list", "dict"):
109
+ if not isinstance(value, expected_type):
110
+ issues.append(f"{field_name} 类型错误(期望 {field_type})")
111
+ elif field_type == "bool":
112
+ if str(value).lower() not in ("true", "false", "1", "0", "是", "否"):
113
+ issues.append(f"{field_name} 类型错误(期望 bool)")
114
+ elif field_type == "any":
115
+ pass
116
+ elif not isinstance(value, expected_type):
117
+ issues.append(f"{field_name} 类型错误(期望 {field_type})")
118
+ except (ValueError, TypeError):
119
+ issues.append(f"{field_name} 类型错误(期望 {field_type})")
120
+
121
+ return not issues, issues
122
+
123
+
124
+ @dataclass
125
+ class SubTaskResult:
126
+ """子任务结果"""
127
+ name: str
128
+ success: bool
129
+ summary: str = ""
130
+ duration_ms: float = 0.0
131
+ steps: int = 0
132
+ error: str = ""
133
+ schema_ok: bool = True # 输出契约校验是否通过
134
+ schema_missing: list[str] = field(default_factory=list) # 缺失/错误字段
135
+
136
+ def to_dict(self) -> dict:
137
+ return {"name": self.name, "success": self.success,
138
+ "summary": self.summary[:300],
139
+ "duration_ms": round(self.duration_ms, 1),
140
+ "steps": self.steps, "error": self.error[:200],
141
+ "schema_ok": self.schema_ok,
142
+ "schema_missing": self.schema_missing}
143
+
144
+
145
+ def run_subtask(task: SubTask, workdir: str = ".") -> SubTaskResult:
146
+ """执行单个子任务(独立上下文)
147
+
148
+ 用 AgentRuntime 跑,但:
149
+ - 独立 SessionMemory(不污染主 Agent)
150
+ - 独立工具环境(tool_whitelist 过滤)
151
+ - 结果只回传摘要 + 输出契约校验(expected_schema)
152
+ """
153
+ start = time.time()
154
+ try:
155
+ from devmate_agent import AgentRuntime
156
+
157
+ runtime = AgentRuntime()
158
+
159
+ # 工具白名单过滤(隔离:子任务只看到允许的工具)
160
+ if task.tool_whitelist:
161
+ allowed = set(task.tool_whitelist)
162
+ for tname in list(runtime.tools.list_names()):
163
+ if tname not in allowed:
164
+ runtime.tools.unregister(tname)
165
+
166
+ # 运行子任务(dry_run=False 真实执行)
167
+ result = runtime.run(task.prompt, max_steps=task.max_steps)
168
+
169
+ if result.success:
170
+ data = result.data
171
+ summary = str(data.get("final_output", data.get("output", "")))[:300]
172
+ # 输出契约校验(关键节点校验:缺失字段标记失败)
173
+ schema_ok, missing = True, []
174
+ if task.expected_schema:
175
+ schema_ok, missing = validate_schema(summary, task.expected_schema)
176
+ if not schema_ok and task.schema_required:
177
+ return SubTaskResult(
178
+ name=task.name, success=False,
179
+ error=f"输出契约校验失败: 缺 {', '.join(missing)}",
180
+ summary=summary, schema_ok=False,
181
+ schema_missing=missing,
182
+ duration_ms=(time.time() - start) * 1000,
183
+ steps=data.get("step_count", data.get("total_steps", 0)),
184
+ )
185
+ return SubTaskResult(
186
+ name=task.name, success=True, summary=summary,
187
+ duration_ms=(time.time() - start) * 1000,
188
+ steps=data.get("step_count", data.get("total_steps", 0)),
189
+ schema_ok=schema_ok, schema_missing=missing,
190
+ )
191
+ return SubTaskResult(
192
+ name=task.name, success=False,
193
+ error=str(result.error or "任务失败"),
194
+ duration_ms=(time.time() - start) * 1000,
195
+ )
196
+ except Exception as e:
197
+ return SubTaskResult(
198
+ name=task.name, success=False, error=str(e),
199
+ duration_ms=(time.time() - start) * 1000,
200
+ )
201
+
202
+
203
+ def run_subtasks(tasks: list[SubTask], workdir: str = ".",
204
+ parallel: bool = True) -> CommandResult:
205
+ """并行/串行执行多个子任务
206
+
207
+ Args:
208
+ tasks: 子任务列表
209
+ workdir: 工作目录
210
+ parallel: 是否并行(默认 True)
211
+ """
212
+ results = []
213
+ if parallel and len(tasks) > 1:
214
+ from concurrent.futures import ThreadPoolExecutor
215
+ with ThreadPoolExecutor(max_workers=min(len(tasks), 4)) as pool:
216
+ futures = {pool.submit(run_subtask, t, workdir): t.name
217
+ for t in tasks}
218
+ # 保持任务顺序
219
+ ordered = [futures[f] for f in futures]
220
+ for f, name in zip(futures, ordered):
221
+ results.append(f.result())
222
+ else:
223
+ for t in tasks:
224
+ results.append(run_subtask(t, workdir))
225
+
226
+ success = sum(1 for r in results if r.success)
227
+ total = len(results)
228
+ return CommandResult(success=success == total, data={
229
+ "results": [r.to_dict() for r in results],
230
+ "success_count": success,
231
+ "total": total,
232
+ "message": f"Sub-agents: {success}/{total} 成功",
233
+ "summary": "\n".join(
234
+ f"[{'✅' if r.success else '❌'}] {r.name}: "
235
+ f"{r.summary or r.error}" for r in results
236
+ )[:800],
237
+ })
@@ -1238,9 +1238,14 @@ def list_schedules() -> CommandResult:
1238
1238
  })
1239
1239
 
1240
1240
 
1241
- def run_due_workflows(now: time.struct_time | None = None) -> CommandResult:
1241
+ def run_due_workflows(now: time.struct_time | None = None,
1242
+ retries: int = 0) -> CommandResult:
1242
1243
  """执行所有到期的调度工作流(供 scheduler 守护进程调用)
1243
1244
 
1245
+ Args:
1246
+ now: 当前时间(测试可注入)
1247
+ retries: 失败自动重试次数(P2-⑪,默认 0 不重试)
1248
+
1244
1249
  返回: 已执行的调度列表
1245
1250
  """
1246
1251
  schedules = _load_schedules()
@@ -1256,15 +1261,27 @@ def run_due_workflows(now: time.struct_time | None = None) -> CommandResult:
1256
1261
  continue
1257
1262
  result = run(name)
1258
1263
  ok = result.success and result.data.get("fail_count", 0) == 0
1264
+ retry_count = 0
1265
+
1266
+ # 失败重试(指数退避:2s → 4s → 8s)
1267
+ while not ok and retry_count < retries:
1268
+ retry_count += 1
1269
+ import time as _t
1270
+ _t.sleep(min(2 ** retry_count, 10))
1271
+ result = run(name)
1272
+ ok = result.success and result.data.get("fail_count", 0) == 0
1273
+
1259
1274
  # 更新调度状态
1260
1275
  sched["last_run"] = time.strftime("%Y-%m-%d %H:%M:%S")
1261
1276
  sched["last_status"] = "success" if ok else "failed"
1277
+ sched["retries"] = retry_count
1262
1278
  executed.append({
1263
1279
  "name": name,
1264
1280
  "status": "success" if ok else "failed",
1265
1281
  "success_count": result.data.get("success_count", 0),
1266
1282
  "fail_count": result.data.get("fail_count", 0),
1267
1283
  "verified": result.data.get("verified"),
1284
+ "retries": retry_count,
1268
1285
  })
1269
1286
  # 失败告警(调度级 notify)
1270
1287
  if sched.get("notify") and not ok:
@@ -1365,9 +1382,14 @@ def daemon_status() -> CommandResult:
1365
1382
  })
1366
1383
 
1367
1384
 
1368
- def daemon_start(interval: int = POLL_INTERVAL) -> CommandResult:
1385
+ def daemon_start(interval: int = POLL_INTERVAL,
1386
+ retries: int = 0) -> CommandResult:
1369
1387
  """启动守护进程(后台)
1370
1388
 
1389
+ Args:
1390
+ interval: 检查间隔(秒)
1391
+ retries: 失败自动重试次数(P2-⑪)
1392
+
1371
1393
  每 interval 秒检查一次 cron 到期的工作流并执行。
1372
1394
  """
1373
1395
  import subprocess
@@ -1382,15 +1404,17 @@ import sys, time, json
1382
1404
  sys.path.insert(0, {str(Path(__file__).resolve().parent.parent.parent)!r})
1383
1405
  from devmate.workflow import run_due_workflows, daemon_status
1384
1406
  INTERVAL = {int(interval)}
1407
+ RETRIES = {int(retries)}
1385
1408
  def _log(msg):
1386
1409
  ts = time.strftime("%Y-%m-%d %H:%M:%S")
1387
1410
  print(f"[{{ts}}] {{msg}}", flush=True)
1388
1411
  while True:
1389
1412
  try:
1390
- r = run_due_workflows()
1413
+ r = run_due_workflows(retries=RETRIES)
1391
1414
  if r.data.get("ran"):
1392
1415
  for item in r.data["ran"]:
1393
- _log(f"执行 {{item['name']}}: {{item.get('status')}}")
1416
+ _log(f"执行 {{item['name']}}: {{item.get('status')}}"
1417
+ f"(重试{{item.get('retries', 0)}})")
1394
1418
  except Exception as e:
1395
1419
  _log(f"调度错误: {{e}}")
1396
1420
  time.sleep(INTERVAL)
@@ -1405,13 +1429,88 @@ while True:
1405
1429
  )
1406
1430
  DAEMON_PID_FILE.write_text(str(proc.pid))
1407
1431
  return CommandResult(success=True, data={
1408
- "message": f"✅ 守护进程已启动 (PID {proc.pid}, 每 {interval}s 检查)",
1432
+ "message": (f"✅ 守护进程已启动 (PID {proc.pid}, 每 {interval}s 检查, "
1433
+ f"重试 {retries} 次)"),
1409
1434
  "pid": proc.pid, "log": str(DAEMON_LOG_FILE),
1410
1435
  })
1411
1436
  except Exception as e:
1412
1437
  return CommandResult(success=False, error=f"启动失败: {e}")
1413
1438
 
1414
1439
 
1440
+ def daemon_install_launchd() -> CommandResult:
1441
+ """安装 launchd 开机自启(P2-⑫ · macOS)
1442
+
1443
+ 生成 ~/Library/LaunchAgents/com.devmate.scheduler.plist
1444
+ 开机自动启动 `devmate wf daemon start`
1445
+ """
1446
+ import shutil as _shutil
1447
+ import subprocess as _subprocess
1448
+
1449
+ label = "com.devmate.scheduler"
1450
+ plist_path = Path.home() / "Library" / "LaunchAgents" / f"{label}.plist"
1451
+
1452
+ # 找到 devmate 可执行文件
1453
+ devmate_bin = _shutil.which("devmate")
1454
+ if not devmate_bin:
1455
+ # 回退到 python -m devmate
1456
+ import sys
1457
+ devmate_bin = f"{sys.executable} -m devmate"
1458
+
1459
+ plist = f"""<?xml version="1.0" encoding="UTF-8"?>
1460
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1461
+ <plist version="1.0">
1462
+ <dict>
1463
+ <key>Label</key>
1464
+ <string>{label}</string>
1465
+ <key>ProgramArguments</key>
1466
+ <array>
1467
+ <string>bash</string>
1468
+ <string>-lc</string>
1469
+ <string>{devmate_bin} wf daemon start --interval 30 --retries 2</string>
1470
+ </array>
1471
+ <key>RunAtLoad</key>
1472
+ <true/>
1473
+ <key>KeepAlive</key>
1474
+ <true/>
1475
+ <key>StandardOutPath</key>
1476
+ <string>{DAEMON_LOG_FILE}</string>
1477
+ <key>StandardErrorPath</key>
1478
+ <string>{DAEMON_LOG_FILE}</string>
1479
+ </dict>
1480
+ </plist>
1481
+ """
1482
+ try:
1483
+ plist_path.parent.mkdir(parents=True, exist_ok=True)
1484
+ plist_path.write_text(plist)
1485
+ # 加载
1486
+ _subprocess.run(["launchctl", "load", str(plist_path)],
1487
+ capture_output=True, timeout=15)
1488
+ return CommandResult(success=True, data={
1489
+ "message": f"✅ 已安装开机自启: {plist_path}",
1490
+ "plist": str(plist_path),
1491
+ })
1492
+ except Exception as e:
1493
+ return CommandResult(success=False, error=f"安装失败: {e}")
1494
+
1495
+
1496
+ def daemon_uninstall_launchd() -> CommandResult:
1497
+ """卸载 launchd 开机自启"""
1498
+ import subprocess as _subprocess
1499
+
1500
+ label = "com.devmate.scheduler"
1501
+ plist_path = Path.home() / "Library" / "LaunchAgents" / f"{label}.plist"
1502
+ try:
1503
+ if plist_path.exists():
1504
+ _subprocess.run(["launchctl", "unload", str(plist_path)],
1505
+ capture_output=True, timeout=15)
1506
+ plist_path.unlink()
1507
+ return CommandResult(success=True, data={
1508
+ "message": "✅ 已卸载开机自启",
1509
+ })
1510
+ except Exception as e:
1511
+ return CommandResult(success=False, error=f"卸载失败: {e}")
1512
+
1513
+
1415
1514
  def daemon_stop() -> CommandResult:
1416
1515
  """停止守护进程"""
1417
1516
  st = daemon_status()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devmate
3
- Version: 2.5.0
3
+ Version: 2.5.2
4
4
  Summary: 程序员终端瑞士军刀 + AI 原生平台——替代 5 个独立工具,一个 CLI 搞定
5
5
  Author-email: DevMate Team <devmate@example.com>
6
6
  License: MIT
@@ -40,6 +40,7 @@ src/devmate/security/audit.py
40
40
  src/devmate/security/vault.py
41
41
  src/devmate/share/__init__.py
42
42
  src/devmate/skill/__init__.py
43
+ src/devmate/sub_agent/__init__.py
43
44
  src/devmate/workflow/__init__.py
44
45
  src/devmate_agent/__init__.py
45
46
  src/devmate_apidev/__init__.py
@@ -133,6 +133,10 @@ class ToolRegistry:
133
133
  def list_names(self) -> list[str]:
134
134
  return list(self._tools.keys())
135
135
 
136
+ def unregister(self, name: str):
137
+ """移除工具(Sub-agent 隔离用)"""
138
+ self._tools.pop(name, None)
139
+
136
140
  def register_builtin(self):
137
141
  """注册 16 个模块的核心工具"""
138
142
  self.register(Tool(
@@ -384,20 +388,25 @@ class ToolRegistry:
384
388
  p.parent.mkdir(parents=True, exist_ok=True)
385
389
  p.write_text(content, encoding="utf-8")
386
390
  result = f"已写入 {len(content)} 字节到 {path}"
387
- # Post-edit checkpoint:写文件后自动校验(验证信号注入)
391
+ # Post-edit checkpoint:写文件后自动校验 + 自动修复(验证信号注入)
388
392
  # 下沉到 handler —— Agent 主循环 / workflow / skill 所有路径统一受益
389
393
  try:
390
394
  from devmate.agent.validators import (
391
395
  summarize_validation,
392
- validate_file,
393
- validate_file_feedback,
396
+ validate_with_autofix,
394
397
  )
395
- results = validate_file(str(p))
396
- feedback = validate_file_feedback(str(p))
397
- if feedback:
398
- result += f"\n\n[校验反馈]\n{feedback}"
399
- elif results:
400
- result += f"\n\n[校验] {summarize_validation(results)}"
398
+ outcome = validate_with_autofix(str(p))
399
+ if outcome["auto_fixed"]:
400
+ result += ("\n\n[校验] 已自动修复风格问题,"
401
+ "重新校验通过")
402
+ elif outcome["feedback"]:
403
+ result += f"\n\n[校验反馈]\n{outcome['feedback']}"
404
+ else:
405
+ results = outcome.get("results", [])
406
+ if results:
407
+ result += f"\n\n[校验] {summarize_validation(results)}"
408
+ else:
409
+ result += "\n\n[校验] 通过"
401
410
  except Exception:
402
411
  pass # 校验失败不影响写入结果
403
412
  return result
@@ -556,21 +565,46 @@ class TraceStep:
556
565
 
557
566
  @dataclass
558
567
  class SessionMemory:
559
- """会话记忆(滑动窗口)"""
568
+ """会话记忆(滑动窗口 + 摘要压缩)"""
560
569
  messages: list[dict] = field(default_factory=list)
561
570
  max_turns: int = 10
571
+ compress: bool = False # 超窗口时压缩为摘要(P1-⑦),而非丢弃
572
+ summary: str = "" # 已压缩的早期历史摘要
562
573
 
563
574
  def add(self, role: str, content: str, tool_calls: list | None = None):
564
575
  msg = {"role": role, "content": content}
565
576
  if tool_calls:
566
577
  msg["tool_calls"] = tool_calls
567
578
  self.messages.append(msg)
568
- # 滑动窗口
579
+ # 滑动窗口(超窗时压缩或丢弃早期历史)
569
580
  if len(self.messages) > self.max_turns * 2:
570
- if self.messages[0]["role"] == "system":
571
- self.messages = [self.messages[0]] + self.messages[-(self.max_turns * 2 - 1):]
572
- else:
573
- self.messages = self.messages[-(self.max_turns * 2):]
581
+ overflow = self.messages[:-(self.max_turns * 2)]
582
+ keep = self.messages[-(self.max_turns * 2):]
583
+ if self.compress:
584
+ self._compress(overflow)
585
+ if keep and keep[0]["role"] == "system":
586
+ keep = keep[1:]
587
+ # 压缩摘要作为 system 消息注入最前
588
+ if self.summary:
589
+ keep.insert(0, {
590
+ "role": "system",
591
+ "content": f"[历史摘要] {self.summary[:500]}",
592
+ })
593
+ self.messages = keep
594
+
595
+ def _compress(self, overflow: list[dict]):
596
+ """把溢出的历史压缩为摘要(保留关键信息)"""
597
+ parts = []
598
+ for msg in overflow:
599
+ role = msg.get("role", "")
600
+ content = str(msg.get("content", ""))[:100]
601
+ if role == "user":
602
+ parts.append(f"用户说: {content}")
603
+ elif role == "assistant":
604
+ parts.append(f"助手: {content[:60]}")
605
+ elif role == "tool":
606
+ parts.append(f"工具({msg.get('name','')}): {content[:50]}")
607
+ self.summary = ";".join(parts[-10:]) # 只保留最近 10 条溢出
574
608
 
575
609
  def add_tool_result(self, tool_call_id: str, name: str, content: str):
576
610
  self.messages.append({
@@ -585,6 +619,7 @@ class SessionMemory:
585
619
 
586
620
  def clear(self):
587
621
  self.messages = []
622
+ self.summary = ""
588
623
 
589
624
 
590
625
  _DEFAULT_SYSTEM_PROMPT = """你是一个 DevMate 智能助手,可以调用工具来帮助用户完成开发任务。
@@ -603,10 +638,11 @@ class AgentRuntime:
603
638
  """Agent 运行时——真正的 ReAct 循环 + LLM 驱动"""
604
639
 
605
640
  def __init__(self, llm_config: dict | None = None,
606
- system_prompt: str = "", connect_mcp: bool = False):
641
+ system_prompt: str = "", connect_mcp: bool = False,
642
+ compress_memory: bool = True):
607
643
  self.tools = ToolRegistry()
608
644
  self.tools.register_builtin()
609
- self.memory = SessionMemory()
645
+ self.memory = SessionMemory(compress=compress_memory)
610
646
  self.trace: list[TraceStep] = []
611
647
  self.system_prompt = system_prompt or _DEFAULT_SYSTEM_PROMPT
612
648
 
@@ -803,6 +839,12 @@ class AgentRuntime:
803
839
  "duration_ms": round(duration_ms, 1),
804
840
  })
805
841
  self._save_audit()
842
+ # 行为信号自动采集(P1-⑨):成功工具调用 → 记忆观察计数自动递增
843
+ if success and action == "execute":
844
+ try:
845
+ self._collect_behavior_signal(tool, args)
846
+ except Exception:
847
+ pass
806
848
  # 同步到全局审计模块
807
849
  try:
808
850
  from devmate.security.audit import AuditEntry, audit
@@ -817,6 +859,28 @@ class AgentRuntime:
817
859
  except Exception:
818
860
  pass
819
861
 
862
+ def _collect_behavior_signal(self, tool: str, args: dict):
863
+ """行为信号自动采集(P1-⑨)
864
+
865
+ 成功工具调用 → 记忆观察计数自动递增(无需手动 set)
866
+ key 形如: tool:monitor_dashboard / tool:write_file
867
+ """
868
+ from devmate.memory_store import set_memory
869
+ key = f"tool:{tool}"
870
+ # 高频工具自动采集(排除敏感写操作)
871
+ if tool in ("write_file", "file_organize", "ssh_run"):
872
+ return
873
+ label = {"monitor_dashboard": "巡检系统资源",
874
+ "git_stats": "查看 Git 统计",
875
+ "http_request": "请求 API",
876
+ "note_search": "搜索笔记",
877
+ "rag_search": "检索知识库",
878
+ "file_size": "查看文件大小",
879
+ "read_file": "读取文件",
880
+ "skill_run": "执行 Skill",
881
+ "db_query": "查询数据库"}.get(tool, f"调用 {tool}")
882
+ set_memory(key, label, scope="session", category="behavior")
883
+
820
884
  def run(self, user_message: str, max_steps: int = 10,
821
885
  dry_run: bool = False) -> CommandResult:
822
886
  """运行 Agent ReAct 循环
File without changes
File without changes
File without changes
File without changes
File without changes