homegraph 1.5.0 → 1.5.1

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 (214) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +305 -305
  3. package/dist/bin/command-supervision.d.ts.map +1 -1
  4. package/dist/bin/command-supervision.js +7 -4
  5. package/dist/bin/command-supervision.js.map +1 -1
  6. package/dist/bin/homegraph.js +9 -9
  7. package/dist/db/index.js +36 -36
  8. package/dist/db/migrations.js +37 -37
  9. package/dist/db/queries.js +156 -156
  10. package/dist/db/schema.sql +203 -203
  11. package/dist/directory.js +5 -5
  12. package/dist/extraction/languages/arkts-viewtree.d.ts +2 -4
  13. package/dist/extraction/languages/arkts-viewtree.d.ts.map +1 -1
  14. package/dist/extraction/languages/arkts-viewtree.js +6 -21
  15. package/dist/extraction/languages/arkts-viewtree.js.map +1 -1
  16. package/dist/extraction/languages/arkts.d.ts +16 -6
  17. package/dist/extraction/languages/arkts.d.ts.map +1 -1
  18. package/dist/extraction/languages/arkts.js +174 -21
  19. package/dist/extraction/languages/arkts.js.map +1 -1
  20. package/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
  21. package/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
  22. package/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
  23. package/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
  24. package/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
  25. package/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
  26. package/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
  27. package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
  28. package/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
  29. package/dist/installer/instructions-template.js +9 -9
  30. package/dist/mcp/liveness-watchdog.d.ts +18 -0
  31. package/dist/mcp/liveness-watchdog.d.ts.map +1 -1
  32. package/dist/mcp/liveness-watchdog.js +185 -73
  33. package/dist/mcp/liveness-watchdog.js.map +1 -1
  34. package/dist/mcp/server-instructions.js +47 -47
  35. package/dist/reasoning/reasoner.js +32 -32
  36. package/dist/spec/db/commit-node.js +4 -4
  37. package/dist/spec/db/fragment-node.js +10 -10
  38. package/dist/spec/db/fts.js +8 -8
  39. package/dist/spec/db/relations.js +88 -88
  40. package/dist/spec/db/schema.js +6 -6
  41. package/dist/spec/db/schema.sql +121 -121
  42. package/dist/spec/db/spec-node.js +4 -4
  43. package/dist/spec/llm/prompts.js +53 -53
  44. package/dist/spec/utils.d.ts +16 -4
  45. package/dist/spec/utils.d.ts.map +1 -1
  46. package/dist/spec/utils.js +58 -6
  47. package/dist/spec/utils.js.map +1 -1
  48. package/package.json +62 -62
  49. package/scripts/_tmp-cfwk-resolve.log +0 -0
  50. package/scripts/_tmp-cfwk-sig.log +0 -0
  51. package/scripts/_tmp-cfwk-vt.log +0 -0
  52. package/scripts/add-lang/bench.sh +60 -60
  53. package/scripts/add-lang/check-grammar.mjs +75 -75
  54. package/scripts/add-lang/dump-ast.mjs +103 -103
  55. package/scripts/add-lang/verify-extraction.mjs +70 -70
  56. package/scripts/agent-eval/ab-adoption.sh +91 -91
  57. package/scripts/agent-eval/ab-hook.sh +86 -86
  58. package/scripts/agent-eval/ab-impl.sh +78 -78
  59. package/scripts/agent-eval/ab-new-vs-baseline.sh +102 -102
  60. package/scripts/agent-eval/ab-sufficiency.sh +78 -78
  61. package/scripts/agent-eval/arms-F.sh +21 -21
  62. package/scripts/agent-eval/arms-matrix.sh +37 -37
  63. package/scripts/agent-eval/audit.sh +68 -68
  64. package/scripts/agent-eval/bench-readme.sh +28 -28
  65. package/scripts/agent-eval/bench-why-repo.sh +22 -22
  66. package/scripts/agent-eval/block-read-hook.sh +19 -19
  67. package/scripts/agent-eval/hook-settings.json +15 -15
  68. package/scripts/agent-eval/itrun.sh +120 -120
  69. package/scripts/agent-eval/offload-eval-3arm.sh +72 -72
  70. package/scripts/agent-eval/offload-eval-cost.mjs +133 -133
  71. package/scripts/agent-eval/offload-eval-effort.mjs +108 -108
  72. package/scripts/agent-eval/offload-eval-frontload-matrix.sh +25 -25
  73. package/scripts/agent-eval/offload-eval-frontload.sh +47 -47
  74. package/scripts/agent-eval/offload-eval-ground-truth.json +18 -18
  75. package/scripts/agent-eval/offload-eval-hook.mjs +84 -84
  76. package/scripts/agent-eval/offload-eval-judge.mjs +103 -103
  77. package/scripts/agent-eval/offload-eval-matrix.sh +20 -20
  78. package/scripts/agent-eval/offload-eval-metrics.mjs +94 -94
  79. package/scripts/agent-eval/offload-eval-refs1.sh +50 -50
  80. package/scripts/agent-eval/offload-eval-setup.sh +24 -24
  81. package/scripts/agent-eval/offload-eval-styles.sh +71 -71
  82. package/scripts/agent-eval/offload-eval-summarize.mjs +68 -68
  83. package/scripts/agent-eval/offload-eval.md +76 -76
  84. package/scripts/agent-eval/parse-arms.mjs +116 -116
  85. package/scripts/agent-eval/parse-bench-readme.mjs +84 -84
  86. package/scripts/agent-eval/parse-run.mjs +45 -45
  87. package/scripts/agent-eval/parse-session.mjs +93 -93
  88. package/scripts/agent-eval/probe-context.mjs +21 -21
  89. package/scripts/agent-eval/probe-explore.mjs +40 -40
  90. package/scripts/agent-eval/probe-node.mjs +20 -20
  91. package/scripts/agent-eval/probe-sweep.mjs +119 -119
  92. package/scripts/agent-eval/probe-trace.mjs +20 -20
  93. package/scripts/agent-eval/redirect-read-hook.sh +38 -38
  94. package/scripts/agent-eval/repro-concurrent-explore.mjs +119 -119
  95. package/scripts/agent-eval/repro-daemon-clients.mjs +125 -125
  96. package/scripts/agent-eval/run-agent.sh +34 -34
  97. package/scripts/agent-eval/run-all.sh +75 -75
  98. package/scripts/agent-eval/run-arms.sh +56 -56
  99. package/scripts/agent-eval/seq-matrix.mjs +137 -137
  100. package/scripts/bench-arkts-init-rss.log +0 -0
  101. package/scripts/build-bundle.sh +123 -123
  102. package/scripts/exp_boundary_eval/README.md +247 -247
  103. package/scripts/exp_boundary_eval/__pycache__/_utils.cpython-310.pyc +0 -0
  104. package/scripts/exp_boundary_eval/__pycache__/_utils.cpython-38.pyc +0 -0
  105. package/scripts/exp_boundary_eval/__pycache__/analyze.cpython-310.pyc +0 -0
  106. package/scripts/exp_boundary_eval/__pycache__/analyze.cpython-38.pyc +0 -0
  107. package/scripts/exp_boundary_eval/__pycache__/deveco_arm.cpython-38.pyc +0 -0
  108. package/scripts/exp_boundary_eval/__pycache__/run_all.cpython-310.pyc +0 -0
  109. package/scripts/exp_boundary_eval/__pycache__/run_all.cpython-38.pyc +0 -0
  110. package/scripts/exp_boundary_eval/__pycache__/run_one.cpython-310.pyc +0 -0
  111. package/scripts/exp_boundary_eval/__pycache__/run_one.cpython-38.pyc +0 -0
  112. package/scripts/exp_boundary_eval/__pycache__/run_session.cpython-310.pyc +0 -0
  113. package/scripts/exp_boundary_eval/__pycache__/run_session.cpython-38.pyc +0 -0
  114. package/scripts/exp_boundary_eval/__pycache__/setup.cpython-310.pyc +0 -0
  115. package/scripts/exp_boundary_eval/__pycache__/setup.cpython-38.pyc +0 -0
  116. package/scripts/exp_boundary_eval/__pycache__/win_mcp_launcher.cpython-38.pyc +0 -0
  117. package/scripts/exp_boundary_eval/_test_mcp_chain.py +78 -78
  118. package/scripts/exp_boundary_eval/_test_stdin.py +8 -8
  119. package/scripts/exp_boundary_eval/_utils.py +1116 -1116
  120. package/scripts/exp_boundary_eval/analyze.py +1313 -1313
  121. package/scripts/exp_boundary_eval/deveco_arm.py +519 -519
  122. package/scripts/exp_boundary_eval/run_all.py +378 -378
  123. package/scripts/exp_boundary_eval/run_one.py +165 -165
  124. package/scripts/exp_boundary_eval/run_session.py +158 -158
  125. package/scripts/exp_boundary_eval/setup.py +120 -120
  126. package/scripts/exp_boundary_eval/win_mcp_launcher.py +73 -73
  127. package/scripts/exp_boundary_eval/win_mcp_stdio_wrap.js +36 -36
  128. package/scripts/exp_boundary_eval/win_node_launcher.py +24 -24
  129. package/scripts/extract-release-notes.mjs +130 -130
  130. package/scripts/local-install.sh +41 -41
  131. package/scripts/npm-sdk.js +75 -75
  132. package/scripts/npm-shim.js +275 -275
  133. package/scripts/ohos-sdk-publish.mjs +133 -133
  134. package/scripts/pack-npm.sh +119 -119
  135. package/scripts/prepare-release.mjs +270 -270
  136. package/scripts/probe-arkts-mem-why-run.log +0 -0
  137. package/scripts/probe-banner-livecard-ir.log +0 -0
  138. package/scripts/probe-cfwk-ast.stderr.log +0 -0
  139. package/scripts/probe-cfwk-ast.stdout.log +0 -0
  140. package/scripts/probe-cfwk-attr-shape.log +0 -0
  141. package/scripts/probe-cfwk-cfgdump.stderr.log +0 -0
  142. package/scripts/probe-cfwk-cfgdump.stdout.log +0 -0
  143. package/scripts/probe-cfwk-cvc.stderr.log +0 -0
  144. package/scripts/probe-cfwk-cvc.stdout.log +0 -0
  145. package/scripts/probe-cfwk-diag2.log +0 -0
  146. package/scripts/probe-cfwk-diag3.log +0 -0
  147. package/scripts/probe-cfwk-fedbg.stderr.log +0 -0
  148. package/scripts/probe-cfwk-fedbg.stdout.log +0 -0
  149. package/scripts/probe-cfwk-fileresult.log +0 -0
  150. package/scripts/probe-cfwk-fix.stderr.log +0 -0
  151. package/scripts/probe-cfwk-fix.stdout.log +0 -0
  152. package/scripts/probe-cfwk-fix2.stderr.log +0 -0
  153. package/scripts/probe-cfwk-fix2.stdout.log +0 -0
  154. package/scripts/probe-cfwk-fix3.stderr.log +0 -0
  155. package/scripts/probe-cfwk-fix3.stdout.log +0 -0
  156. package/scripts/probe-cfwk-foreach.log +0 -0
  157. package/scripts/probe-cfwk-getmethod-throw.log +0 -0
  158. package/scripts/probe-cfwk-hg-extract.log +0 -0
  159. package/scripts/probe-cfwk-pr1003-noprior.stderr.log +0 -0
  160. package/scripts/probe-cfwk-pr1003-noprior.stdout.log +0 -0
  161. package/scripts/probe-cfwk-pr1003.stderr.log +0 -0
  162. package/scripts/probe-cfwk-pr1003.stdout.log +0 -0
  163. package/scripts/probe-cfwk-preroot-noprior.stderr.log +0 -0
  164. package/scripts/probe-cfwk-preroot-noprior.stdout.log +0 -0
  165. package/scripts/probe-cfwk-preroot-prior.stderr.log +0 -0
  166. package/scripts/probe-cfwk-preroot-prior.stdout.log +0 -0
  167. package/scripts/probe-cfwk-preroot-skipstate.stderr.log +0 -0
  168. package/scripts/probe-cfwk-preroot-skipstate.stdout.log +0 -0
  169. package/scripts/probe-cfwk-resolve-sim.log +0 -0
  170. package/scripts/probe-cfwk-tree-shape.log +0 -0
  171. package/scripts/probe-cfwk-walk-abort.log +0 -0
  172. package/scripts/probe-cfwk.log +0 -0
  173. package/scripts/probe-force-index.log +0 -0
  174. package/scripts/probe-no-force-index.log +0 -0
  175. package/scripts/probe-samefile-ir.stderr.log +0 -0
  176. package/scripts/probe-samefile-ir.stdout.log +224 -0
  177. package/scripts/probe-sdk-vs-project.log +0 -0
  178. package/scripts/probe-viewtree-downgrade.log +0 -0
  179. package/dist/arkts/ohos-api-index.d.ts +0 -15
  180. package/dist/arkts/ohos-api-index.d.ts.map +0 -1
  181. package/dist/arkts/ohos-api-index.js +0 -190
  182. package/dist/arkts/ohos-api-index.js.map +0 -1
  183. package/dist/arkts/ohos-sdk-input.d.ts +0 -36
  184. package/dist/arkts/ohos-sdk-input.d.ts.map +0 -1
  185. package/dist/arkts/ohos-sdk-input.js +0 -214
  186. package/dist/arkts/ohos-sdk-input.js.map +0 -1
  187. package/dist/extraction/languages/arkts-state-decorators.d.ts +0 -13
  188. package/dist/extraction/languages/arkts-state-decorators.d.ts.map +0 -1
  189. package/dist/extraction/languages/arkts-state-decorators.js +0 -26
  190. package/dist/extraction/languages/arkts-state-decorators.js.map +0 -1
  191. package/dist/extraction/languages/ohos-api-consumer.d.ts +0 -34
  192. package/dist/extraction/languages/ohos-api-consumer.d.ts.map +0 -1
  193. package/dist/extraction/languages/ohos-api-consumer.js +0 -283
  194. package/dist/extraction/languages/ohos-api-consumer.js.map +0 -1
  195. package/dist/spec/build/git-scanner.d.ts +0 -93
  196. package/dist/spec/build/git-scanner.d.ts.map +0 -1
  197. package/dist/spec/build/git-scanner.js +0 -254
  198. package/dist/spec/build/git-scanner.js.map +0 -1
  199. package/dist/spec/git-utils.d.ts +0 -8
  200. package/dist/spec/git-utils.d.ts.map +0 -1
  201. package/dist/spec/git-utils.js +0 -14
  202. package/dist/spec/git-utils.js.map +0 -1
  203. package/dist/spec/mine/clusterer.d.ts +0 -63
  204. package/dist/spec/mine/clusterer.d.ts.map +0 -1
  205. package/dist/spec/mine/clusterer.js +0 -904
  206. package/dist/spec/mine/clusterer.js.map +0 -1
  207. package/dist/spec/mine/progress-handler.d.ts +0 -22
  208. package/dist/spec/mine/progress-handler.d.ts.map +0 -1
  209. package/dist/spec/mine/progress-handler.js +0 -108
  210. package/dist/spec/mine/progress-handler.js.map +0 -1
  211. package/dist/spec/mine/progress.d.ts +0 -23
  212. package/dist/spec/mine/progress.d.ts.map +0 -1
  213. package/dist/spec/mine/progress.js +0 -12
  214. package/dist/spec/mine/progress.js.map +0 -1
@@ -1,378 +1,378 @@
1
- #!/usr/bin/env python3
2
- """
3
- Run boundary-eval experiments sequentially. Multi-agent + HomeGraph A/B arm support.
4
-
5
- Usage:
6
- python run_all.py --both-arms --agent deveco # 一条命令跑 baseline + homegraph
7
- python run_all.py --agent deveco --arm homegraph
8
- python run_all.py --agent deveco --arm baseline
9
- python run_all.py 1-1 1-2 1-3
10
- python run_all.py --repo-path /path/to/photos
11
- python run_all.py --both-arms --agent deveco --repo-path /path/to/photos --skip-index
12
- """
13
-
14
- import argparse
15
- import json
16
- import os
17
- import shutil
18
- import subprocess
19
- import sys
20
- from pathlib import Path
21
- from typing import Optional, Tuple
22
-
23
- import run_one
24
- import run_session
25
- import analyze
26
- from _utils import (DATA_DIR, EVAL_ROOT, LOG_DIR, OUTPUT_DIR, GREEN, NC, append_run_log,
27
- current_time_ms, create_output_dirs, error, get_agent, header,
28
- info, load_config, log, subprocess_no_window_kwargs, verify_agent_binary, warn)
29
- from deveco_arm import (ensure_homegraph_index, git_clean_repo,
30
- write_deveco_project_config)
31
-
32
- DEFAULT_EXPERIMENTS = ["1-1", "1-2", "1-3"]
33
- SHARED_CLONE_DIR = DATA_DIR / "clone"
34
-
35
-
36
- _NW = subprocess_no_window_kwargs()
37
-
38
-
39
- def clone_repo(url: str, branch: str, clone_dir: Path) -> bool:
40
- clone_dir = Path(clone_dir)
41
- if (clone_dir / ".git").is_dir():
42
- log(f"Clone exists, updating: {clone_dir}")
43
- subprocess.run(["git", "fetch", "origin", branch], cwd=clone_dir, capture_output=True, text=True, **_NW)
44
- subprocess.run(["git", "checkout", branch], cwd=clone_dir, capture_output=True, text=True, **_NW)
45
- subprocess.run(["git", "reset", "--hard", f"origin/{branch}"], cwd=clone_dir, capture_output=True, text=True, **_NW)
46
- return True
47
- log(f"Cloning {url} (branch: {branch})...")
48
- if clone_dir.exists():
49
- shutil.rmtree(clone_dir)
50
- clone_dir.parent.mkdir(parents=True, exist_ok=True)
51
- r = subprocess.run(
52
- ["git", "clone", "--depth", "1", "--branch", branch, url, str(clone_dir)],
53
- capture_output=True, text=True,
54
- **_NW,
55
- )
56
- if r.returncode != 0:
57
- error(f"Clone failed: {r.stderr}")
58
- return False
59
- r2 = subprocess.run(["git", "rev-parse", "--short", "HEAD"], capture_output=True, text=True, cwd=clone_dir, **_NW)
60
- log(f"Clone complete: {r2.stdout.strip()}")
61
- return True
62
-
63
-
64
- def reset_clone(clone_dir: Path, branch: str, arm: str = "baseline"):
65
- subprocess.run(["git", "checkout", branch], cwd=clone_dir, capture_output=True, text=True, **_NW)
66
- subprocess.run(["git", "checkout", "."], cwd=clone_dir, capture_output=True, text=True, **_NW)
67
- git_clean_repo(clone_dir, arm)
68
-
69
-
70
- def reset_local(work_dir: Path, arm: str = "baseline"):
71
- subprocess.run(["git", "checkout", "."], cwd=work_dir, capture_output=True, text=True, **_NW)
72
- git_clean_repo(work_dir, arm)
73
-
74
-
75
- def ensure_homegraph_index_for_arm(
76
- repo_root: Path, arm: str, *, skip_index: bool = False,
77
- ) -> dict:
78
- """Index repo when running the homegraph arm."""
79
- if arm != "homegraph":
80
- return {"homegraph_index_ms": 0, "homegraph_index_success": None}
81
- return ensure_homegraph_index(repo_root, skip=skip_index)
82
-
83
-
84
- def write_run_manifest(state_dir: Path, results_dir: Path, meta: dict):
85
- manifest = {
86
- **meta,
87
- "results_dir": str(results_dir),
88
- "state_dir": str(state_dir),
89
- }
90
- for d in (state_dir, results_dir):
91
- d.mkdir(parents=True, exist_ok=True)
92
- (d / "run_manifest.json").write_text(
93
- json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8"
94
- )
95
-
96
-
97
- def resolve_work_dir(args, state_dir: Path) -> Tuple[Path, Optional[Path], bool]:
98
- """Return (work_dir, ephemeral_clone_dir_or_none, owns_ephemeral_clone)."""
99
- if args.repo_path:
100
- work_dir = Path(args.repo_path).resolve()
101
- if not (work_dir / ".git").is_dir():
102
- error(f"--repo-path 不是 git 仓库: {work_dir}")
103
- sys.exit(1)
104
- os.environ["REPO_PATH"] = str(work_dir)
105
- os.environ.pop("REPO_CLONE", None)
106
- log(f"使用本地仓库: {work_dir}")
107
- return work_dir, None, False
108
-
109
- if args.both_arms:
110
- clone_dir = SHARED_CLONE_DIR
111
- if not clone_repo(args.repo_url, args.branch, clone_dir):
112
- error("Clone 失败。请检查网络,或改用 --repo-path。")
113
- sys.exit(1)
114
- os.environ["REPO_PATH"] = str(clone_dir)
115
- os.environ.pop("REPO_CLONE", None)
116
- log(f"A/B 共用仓库: {clone_dir}")
117
- return clone_dir, None, False
118
-
119
- clone_dir = state_dir / "clone"
120
- if not clone_repo(args.repo_url, args.branch, clone_dir):
121
- error("Clone 失败。请改用 --repo-path 指定本地 checkout。")
122
- sys.exit(1)
123
- os.environ["REPO_CLONE"] = str(clone_dir)
124
- os.environ.pop("REPO_PATH", None)
125
- return clone_dir, clone_dir, True
126
-
127
-
128
- def run_suite(
129
- args, arm: str, *, prebuilt_index_meta: Optional[dict] = None,
130
- ) -> dict:
131
- """Run one arm (baseline or homegraph). Returns summary dict."""
132
- agent = get_agent(args.agent)
133
- skip_perms = not args.no_skip_permissions
134
- experiments = args.experiments or DEFAULT_EXPERIMENTS
135
- total = len(experiments)
136
- passed = 0
137
- failed = 0
138
-
139
- results_dir, state_dir = create_output_dirs(args.agent or "default", arm)
140
- work_dir, ephemeral_clone, owns_clone = resolve_work_dir(args, state_dir)
141
-
142
- if arm == "baseline":
143
- hg_dir = work_dir / ".homegraph"
144
- if hg_dir.exists():
145
- log("Baseline: 保留 .homegraph(A/B 共用仓库;baseline 通过 deveco.jsonc deny homegraph_* 隔离)")
146
-
147
- if prebuilt_index_meta is not None:
148
- index_meta = prebuilt_index_meta
149
- else:
150
- index_meta = ensure_homegraph_index_for_arm(
151
- work_dir, arm, skip_index=getattr(args, "skip_index", False),
152
- )
153
-
154
- if arm == "homegraph" and not index_meta.get("homegraph_index_success"):
155
- error("HomeGraph 索引失败,homegraph 组无法评测。")
156
- err = index_meta.get("homegraph_index_error", "")
157
- if err:
158
- error(f" 原因: {err}")
159
- error(" 请先在 homegraph 仓库执行 npm run build,再重试 run_all.py --arm homegraph")
160
- append_run_log(state_dir, f"ABORT: homegraph index failed: {err}")
161
- write_run_manifest(state_dir, results_dir, {
162
- "arm": arm, "agent": agent["name"], "agent_key": args.agent,
163
- "branch": args.branch, "repo_url": args.repo_url,
164
- "repo_root": str(work_dir), "experiments": experiments,
165
- "aborted": True, **index_meta,
166
- })
167
- sys.exit(1)
168
-
169
- if agent.get("binary") == "deveco":
170
- write_deveco_project_config(work_dir, arm)
171
-
172
- write_run_manifest(state_dir, results_dir, {
173
- "arm": arm,
174
- "agent": agent["name"],
175
- "agent_key": args.agent,
176
- "branch": args.branch,
177
- "repo_url": args.repo_url,
178
- "repo_root": str(work_dir),
179
- "experiments": experiments,
180
- **index_meta,
181
- })
182
- append_run_log(state_dir, f"开始: arm={arm}, agent={agent['name']}, exps={experiments}")
183
-
184
- os.environ["EVAL_ARM"] = arm
185
-
186
- header(f"边界评测 — 档位切换 ({arm})")
187
- print(f"组别 : {arm}")
188
- print(f"Agent : {agent['name']} ({agent['binary']})")
189
- print(f"仓库 : {work_dir}")
190
- print(f"分支 : {args.branch}")
191
- print(f"权限 : {'跳过确认' if skip_perms else '需确认'}")
192
- print(f"实验数 : {total}")
193
- print(f"输出 : {results_dir}")
194
- print(f"日志 : {state_dir}\n")
195
-
196
- overall_start = current_time_ms()
197
-
198
- for exp_id in experiments:
199
- header(f"实验 {exp_id}")
200
- append_run_log(state_dir, f"开始实验 {exp_id}")
201
-
202
- if ephemeral_clone:
203
- log("重置 clone 到干净状态...")
204
- reset_clone(ephemeral_clone, args.branch, arm)
205
- else:
206
- log("重置本地仓库...")
207
- reset_local(work_dir, arm)
208
-
209
- if exp_id == "5":
210
- ok = run_session.run_session("5", skip_perms, args.agent,
211
- results_dir=results_dir, state_dir=state_dir)
212
- else:
213
- ok = run_one.run_experiment(exp_id, skip_perms, args.agent,
214
- results_dir=results_dir, state_dir=state_dir)
215
-
216
- if ok:
217
- passed += 1
218
- else:
219
- failed += 1
220
- error(f"实验 {exp_id} 失败")
221
- print()
222
-
223
- overall_end = current_time_ms()
224
- duration = (overall_end - overall_start) // 1000
225
-
226
- append_run_log(state_dir, f"完成: passed={passed}, failed={failed}, duration_s={duration}")
227
-
228
- header(f"组别完成 — {arm}")
229
- print(f" 通过 : {passed} / {total}")
230
- print(f" 失败 : {failed} / {total}")
231
- print(f" 耗时 : {duration // 60}m {duration % 60}s")
232
- print(f" 输出 : {results_dir}")
233
- print(f" 日志 : {state_dir}")
234
-
235
- if owns_clone and ephemeral_clone and not args.keep_clone and ephemeral_clone.exists():
236
- warn(f"删除临时 clone: {ephemeral_clone}")
237
- shutil.rmtree(ephemeral_clone)
238
-
239
- return {
240
- "arm": arm,
241
- "results_dir": results_dir,
242
- "state_dir": state_dir,
243
- "passed": passed,
244
- "failed": failed,
245
- "duration_s": duration,
246
- "homegraph_index_ms": index_meta.get("homegraph_index_ms", 0),
247
- }
248
-
249
-
250
- def _generate_reports(summaries: list, agent_key: str):
251
- """Run analyze.py logic after experiments; print report paths."""
252
- reports = []
253
- for s in summaries:
254
- try:
255
- path = analyze.analyze_run(s["results_dir"], verbose=False)
256
- reports.append((s["arm"], path))
257
- except ValueError as e:
258
- warn(f"分析 {s['arm']} 失败: {e}")
259
-
260
- compare_path = None
261
- if len(summaries) == 2 and len(reports) == 2:
262
- try:
263
- baseline_s = next(s for s in summaries if s["arm"] == "baseline")
264
- homegraph_s = next(s for s in summaries if s["arm"] == "homegraph")
265
- compare_path = analyze.compare_runs(
266
- baseline_s["results_dir"],
267
- homegraph_s["results_dir"],
268
- agent=agent_key,
269
- baseline_wall_s=baseline_s["duration_s"],
270
- homegraph_wall_s=homegraph_s["duration_s"],
271
- verbose=True,
272
- )
273
- except ValueError as e:
274
- warn(f"A/B 对比报告失败: {e}")
275
-
276
- baseline_report = next((p for arm, p in reports if arm == "baseline"), None)
277
- homegraph_report = next((p for arm, p in reports if arm == "homegraph"), None)
278
- single_report = reports[0][1] if len(reports) == 1 else None
279
-
280
- analyze.print_report_locations(
281
- baseline_report=baseline_report,
282
- homegraph_report=homegraph_report,
283
- compare_report=compare_path,
284
- single_report=single_report,
285
- )
286
- return reports, compare_path
287
-
288
-
289
- def _blocking_homegraph_index(work_dir: Path, *, skip_index: bool = False) -> dict:
290
- """Run full ``homegraph index`` and block until complete (or validate existing index)."""
291
- if skip_index:
292
- header("HomeGraph 索引检查(--skip-index,不执行 index)")
293
- else:
294
- header("HomeGraph 全量 index(阻塞,Photos 约 15–30 分钟)")
295
- log(f"仓库: {work_dir}")
296
- if skip_index:
297
- log("模式: 仅校验已有 .homegraph/(可从同 commit 的其他机器复制整个目录)")
298
- else:
299
- log("命令: node --stack-size=8192 … homegraph index(见 deveco_arm.HOMEGRAPH_NODE_STACK_SIZE_KB)")
300
- meta = ensure_homegraph_index(work_dir, skip=skip_index)
301
- if not meta.get("homegraph_index_success"):
302
- error("HomeGraph index 失败,中止。")
303
- err = meta.get("homegraph_index_error", "")
304
- if err:
305
- error(f" 原因: {err}")
306
- sys.exit(1)
307
- return meta
308
-
309
-
310
- def main():
311
- analyze._configure_stdout()
312
- cfg = load_config()
313
- meta = cfg.get("meta", {})
314
-
315
- parser = argparse.ArgumentParser(description="HomeGraph 档位切换边界评测")
316
- parser.add_argument("--agent", default=os.environ.get("EVAL_AGENT", "deveco"),
317
- help="Agent:claude / cursor / codex / opencode / deveco")
318
- parser.add_argument("--arm", default=os.environ.get("EVAL_ARM", "baseline"),
319
- choices=["baseline", "homegraph"],
320
- help="baseline=不带 HomeGraph;homegraph=带 HomeGraph")
321
- parser.add_argument("--both-arms", action="store_true",
322
- help="一条命令依次跑 baseline 和 homegraph 两组 A/B")
323
- parser.add_argument("--repo-path", default=os.environ.get("REPO_PATH", ""),
324
- help="使用已有本地 clone,跳过 git clone")
325
- parser.add_argument("--keep-clone", action="store_true")
326
- parser.add_argument(
327
- "--skip-index",
328
- action="store_true",
329
- help="不跑 homegraph init/index;要求仓库根目录已有健康的 .homegraph/(可复制自同 commit 的其他 checkout)",
330
- )
331
- parser.add_argument("--no-skip-permissions", action="store_true")
332
- parser.add_argument("--branch", default=os.environ.get("BRANCH", meta.get("default_branch", "weekly_20260601")))
333
- parser.add_argument("--repo-url", default=os.environ.get("REPO_URL", meta.get("repo_url", "")))
334
- parser.add_argument("experiments", nargs="*")
335
- args = parser.parse_args()
336
-
337
- if not args.experiments:
338
- args.experiments = DEFAULT_EXPERIMENTS
339
-
340
- agent = get_agent(args.agent)
341
- ok, resolved, err_msg = verify_agent_binary(agent)
342
- if not ok:
343
- error(err_msg)
344
- sys.exit(1)
345
- log(f"Agent CLI: {resolved}")
346
-
347
- if args.both_arms and args.arm != "baseline":
348
- warn("--both-arms 已指定,忽略 --arm")
349
-
350
- if args.both_arms:
351
- _rd, _sd = create_output_dirs(args.agent or "default", "baseline")
352
- work_dir, _, _ = resolve_work_dir(args, _sd)
353
-
354
- # Index FIRST so the user sees it immediately (15–30 min); baseline does not use MCP.
355
- index_meta = _blocking_homegraph_index(work_dir, skip_index=args.skip_index)
356
-
357
- summaries = []
358
- print(f"\n{'=' * 60}\n 开始 homegraph 组(先看 HomeGraph 是否生效)\n{'=' * 60}\n")
359
- summaries.append(run_suite(args, "homegraph", prebuilt_index_meta=index_meta))
360
-
361
- print(f"\n{'=' * 60}\n 开始 baseline 组(无 HomeGraph)\n{'=' * 60}\n")
362
- summaries.append(run_suite(args, "baseline"))
363
-
364
- header("A/B 全部完成")
365
- for s in summaries:
366
- print(f" [{s['arm']}] 通过 {s['passed']},失败 {s['failed']},"
367
- f"耗时 {s['duration_s'] // 60}m{s['duration_s'] % 60}s")
368
- print(f" 输出 → {s['results_dir']}")
369
- _generate_reports(summaries, args.agent or "default")
370
- sys.exit(0 if all(s["failed"] == 0 for s in summaries) else 1)
371
-
372
- summary = run_suite(args, args.arm)
373
- _generate_reports([summary], args.agent or "default")
374
- sys.exit(0 if summary["failed"] == 0 else 1)
375
-
376
-
377
- if __name__ == "__main__":
378
- main()
1
+ #!/usr/bin/env python3
2
+ """
3
+ Run boundary-eval experiments sequentially. Multi-agent + HomeGraph A/B arm support.
4
+
5
+ Usage:
6
+ python run_all.py --both-arms --agent deveco # 一条命令跑 baseline + homegraph
7
+ python run_all.py --agent deveco --arm homegraph
8
+ python run_all.py --agent deveco --arm baseline
9
+ python run_all.py 1-1 1-2 1-3
10
+ python run_all.py --repo-path /path/to/photos
11
+ python run_all.py --both-arms --agent deveco --repo-path /path/to/photos --skip-index
12
+ """
13
+
14
+ import argparse
15
+ import json
16
+ import os
17
+ import shutil
18
+ import subprocess
19
+ import sys
20
+ from pathlib import Path
21
+ from typing import Optional, Tuple
22
+
23
+ import run_one
24
+ import run_session
25
+ import analyze
26
+ from _utils import (DATA_DIR, EVAL_ROOT, LOG_DIR, OUTPUT_DIR, GREEN, NC, append_run_log,
27
+ current_time_ms, create_output_dirs, error, get_agent, header,
28
+ info, load_config, log, subprocess_no_window_kwargs, verify_agent_binary, warn)
29
+ from deveco_arm import (ensure_homegraph_index, git_clean_repo,
30
+ write_deveco_project_config)
31
+
32
+ DEFAULT_EXPERIMENTS = ["1-1", "1-2", "1-3"]
33
+ SHARED_CLONE_DIR = DATA_DIR / "clone"
34
+
35
+
36
+ _NW = subprocess_no_window_kwargs()
37
+
38
+
39
+ def clone_repo(url: str, branch: str, clone_dir: Path) -> bool:
40
+ clone_dir = Path(clone_dir)
41
+ if (clone_dir / ".git").is_dir():
42
+ log(f"Clone exists, updating: {clone_dir}")
43
+ subprocess.run(["git", "fetch", "origin", branch], cwd=clone_dir, capture_output=True, text=True, **_NW)
44
+ subprocess.run(["git", "checkout", branch], cwd=clone_dir, capture_output=True, text=True, **_NW)
45
+ subprocess.run(["git", "reset", "--hard", f"origin/{branch}"], cwd=clone_dir, capture_output=True, text=True, **_NW)
46
+ return True
47
+ log(f"Cloning {url} (branch: {branch})...")
48
+ if clone_dir.exists():
49
+ shutil.rmtree(clone_dir)
50
+ clone_dir.parent.mkdir(parents=True, exist_ok=True)
51
+ r = subprocess.run(
52
+ ["git", "clone", "--depth", "1", "--branch", branch, url, str(clone_dir)],
53
+ capture_output=True, text=True,
54
+ **_NW,
55
+ )
56
+ if r.returncode != 0:
57
+ error(f"Clone failed: {r.stderr}")
58
+ return False
59
+ r2 = subprocess.run(["git", "rev-parse", "--short", "HEAD"], capture_output=True, text=True, cwd=clone_dir, **_NW)
60
+ log(f"Clone complete: {r2.stdout.strip()}")
61
+ return True
62
+
63
+
64
+ def reset_clone(clone_dir: Path, branch: str, arm: str = "baseline"):
65
+ subprocess.run(["git", "checkout", branch], cwd=clone_dir, capture_output=True, text=True, **_NW)
66
+ subprocess.run(["git", "checkout", "."], cwd=clone_dir, capture_output=True, text=True, **_NW)
67
+ git_clean_repo(clone_dir, arm)
68
+
69
+
70
+ def reset_local(work_dir: Path, arm: str = "baseline"):
71
+ subprocess.run(["git", "checkout", "."], cwd=work_dir, capture_output=True, text=True, **_NW)
72
+ git_clean_repo(work_dir, arm)
73
+
74
+
75
+ def ensure_homegraph_index_for_arm(
76
+ repo_root: Path, arm: str, *, skip_index: bool = False,
77
+ ) -> dict:
78
+ """Index repo when running the homegraph arm."""
79
+ if arm != "homegraph":
80
+ return {"homegraph_index_ms": 0, "homegraph_index_success": None}
81
+ return ensure_homegraph_index(repo_root, skip=skip_index)
82
+
83
+
84
+ def write_run_manifest(state_dir: Path, results_dir: Path, meta: dict):
85
+ manifest = {
86
+ **meta,
87
+ "results_dir": str(results_dir),
88
+ "state_dir": str(state_dir),
89
+ }
90
+ for d in (state_dir, results_dir):
91
+ d.mkdir(parents=True, exist_ok=True)
92
+ (d / "run_manifest.json").write_text(
93
+ json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8"
94
+ )
95
+
96
+
97
+ def resolve_work_dir(args, state_dir: Path) -> Tuple[Path, Optional[Path], bool]:
98
+ """Return (work_dir, ephemeral_clone_dir_or_none, owns_ephemeral_clone)."""
99
+ if args.repo_path:
100
+ work_dir = Path(args.repo_path).resolve()
101
+ if not (work_dir / ".git").is_dir():
102
+ error(f"--repo-path 不是 git 仓库: {work_dir}")
103
+ sys.exit(1)
104
+ os.environ["REPO_PATH"] = str(work_dir)
105
+ os.environ.pop("REPO_CLONE", None)
106
+ log(f"使用本地仓库: {work_dir}")
107
+ return work_dir, None, False
108
+
109
+ if args.both_arms:
110
+ clone_dir = SHARED_CLONE_DIR
111
+ if not clone_repo(args.repo_url, args.branch, clone_dir):
112
+ error("Clone 失败。请检查网络,或改用 --repo-path。")
113
+ sys.exit(1)
114
+ os.environ["REPO_PATH"] = str(clone_dir)
115
+ os.environ.pop("REPO_CLONE", None)
116
+ log(f"A/B 共用仓库: {clone_dir}")
117
+ return clone_dir, None, False
118
+
119
+ clone_dir = state_dir / "clone"
120
+ if not clone_repo(args.repo_url, args.branch, clone_dir):
121
+ error("Clone 失败。请改用 --repo-path 指定本地 checkout。")
122
+ sys.exit(1)
123
+ os.environ["REPO_CLONE"] = str(clone_dir)
124
+ os.environ.pop("REPO_PATH", None)
125
+ return clone_dir, clone_dir, True
126
+
127
+
128
+ def run_suite(
129
+ args, arm: str, *, prebuilt_index_meta: Optional[dict] = None,
130
+ ) -> dict:
131
+ """Run one arm (baseline or homegraph). Returns summary dict."""
132
+ agent = get_agent(args.agent)
133
+ skip_perms = not args.no_skip_permissions
134
+ experiments = args.experiments or DEFAULT_EXPERIMENTS
135
+ total = len(experiments)
136
+ passed = 0
137
+ failed = 0
138
+
139
+ results_dir, state_dir = create_output_dirs(args.agent or "default", arm)
140
+ work_dir, ephemeral_clone, owns_clone = resolve_work_dir(args, state_dir)
141
+
142
+ if arm == "baseline":
143
+ hg_dir = work_dir / ".homegraph"
144
+ if hg_dir.exists():
145
+ log("Baseline: 保留 .homegraph(A/B 共用仓库;baseline 通过 deveco.jsonc deny homegraph_* 隔离)")
146
+
147
+ if prebuilt_index_meta is not None:
148
+ index_meta = prebuilt_index_meta
149
+ else:
150
+ index_meta = ensure_homegraph_index_for_arm(
151
+ work_dir, arm, skip_index=getattr(args, "skip_index", False),
152
+ )
153
+
154
+ if arm == "homegraph" and not index_meta.get("homegraph_index_success"):
155
+ error("HomeGraph 索引失败,homegraph 组无法评测。")
156
+ err = index_meta.get("homegraph_index_error", "")
157
+ if err:
158
+ error(f" 原因: {err}")
159
+ error(" 请先在 homegraph 仓库执行 npm run build,再重试 run_all.py --arm homegraph")
160
+ append_run_log(state_dir, f"ABORT: homegraph index failed: {err}")
161
+ write_run_manifest(state_dir, results_dir, {
162
+ "arm": arm, "agent": agent["name"], "agent_key": args.agent,
163
+ "branch": args.branch, "repo_url": args.repo_url,
164
+ "repo_root": str(work_dir), "experiments": experiments,
165
+ "aborted": True, **index_meta,
166
+ })
167
+ sys.exit(1)
168
+
169
+ if agent.get("binary") == "deveco":
170
+ write_deveco_project_config(work_dir, arm)
171
+
172
+ write_run_manifest(state_dir, results_dir, {
173
+ "arm": arm,
174
+ "agent": agent["name"],
175
+ "agent_key": args.agent,
176
+ "branch": args.branch,
177
+ "repo_url": args.repo_url,
178
+ "repo_root": str(work_dir),
179
+ "experiments": experiments,
180
+ **index_meta,
181
+ })
182
+ append_run_log(state_dir, f"开始: arm={arm}, agent={agent['name']}, exps={experiments}")
183
+
184
+ os.environ["EVAL_ARM"] = arm
185
+
186
+ header(f"边界评测 — 档位切换 ({arm})")
187
+ print(f"组别 : {arm}")
188
+ print(f"Agent : {agent['name']} ({agent['binary']})")
189
+ print(f"仓库 : {work_dir}")
190
+ print(f"分支 : {args.branch}")
191
+ print(f"权限 : {'跳过确认' if skip_perms else '需确认'}")
192
+ print(f"实验数 : {total}")
193
+ print(f"输出 : {results_dir}")
194
+ print(f"日志 : {state_dir}\n")
195
+
196
+ overall_start = current_time_ms()
197
+
198
+ for exp_id in experiments:
199
+ header(f"实验 {exp_id}")
200
+ append_run_log(state_dir, f"开始实验 {exp_id}")
201
+
202
+ if ephemeral_clone:
203
+ log("重置 clone 到干净状态...")
204
+ reset_clone(ephemeral_clone, args.branch, arm)
205
+ else:
206
+ log("重置本地仓库...")
207
+ reset_local(work_dir, arm)
208
+
209
+ if exp_id == "5":
210
+ ok = run_session.run_session("5", skip_perms, args.agent,
211
+ results_dir=results_dir, state_dir=state_dir)
212
+ else:
213
+ ok = run_one.run_experiment(exp_id, skip_perms, args.agent,
214
+ results_dir=results_dir, state_dir=state_dir)
215
+
216
+ if ok:
217
+ passed += 1
218
+ else:
219
+ failed += 1
220
+ error(f"实验 {exp_id} 失败")
221
+ print()
222
+
223
+ overall_end = current_time_ms()
224
+ duration = (overall_end - overall_start) // 1000
225
+
226
+ append_run_log(state_dir, f"完成: passed={passed}, failed={failed}, duration_s={duration}")
227
+
228
+ header(f"组别完成 — {arm}")
229
+ print(f" 通过 : {passed} / {total}")
230
+ print(f" 失败 : {failed} / {total}")
231
+ print(f" 耗时 : {duration // 60}m {duration % 60}s")
232
+ print(f" 输出 : {results_dir}")
233
+ print(f" 日志 : {state_dir}")
234
+
235
+ if owns_clone and ephemeral_clone and not args.keep_clone and ephemeral_clone.exists():
236
+ warn(f"删除临时 clone: {ephemeral_clone}")
237
+ shutil.rmtree(ephemeral_clone)
238
+
239
+ return {
240
+ "arm": arm,
241
+ "results_dir": results_dir,
242
+ "state_dir": state_dir,
243
+ "passed": passed,
244
+ "failed": failed,
245
+ "duration_s": duration,
246
+ "homegraph_index_ms": index_meta.get("homegraph_index_ms", 0),
247
+ }
248
+
249
+
250
+ def _generate_reports(summaries: list, agent_key: str):
251
+ """Run analyze.py logic after experiments; print report paths."""
252
+ reports = []
253
+ for s in summaries:
254
+ try:
255
+ path = analyze.analyze_run(s["results_dir"], verbose=False)
256
+ reports.append((s["arm"], path))
257
+ except ValueError as e:
258
+ warn(f"分析 {s['arm']} 失败: {e}")
259
+
260
+ compare_path = None
261
+ if len(summaries) == 2 and len(reports) == 2:
262
+ try:
263
+ baseline_s = next(s for s in summaries if s["arm"] == "baseline")
264
+ homegraph_s = next(s for s in summaries if s["arm"] == "homegraph")
265
+ compare_path = analyze.compare_runs(
266
+ baseline_s["results_dir"],
267
+ homegraph_s["results_dir"],
268
+ agent=agent_key,
269
+ baseline_wall_s=baseline_s["duration_s"],
270
+ homegraph_wall_s=homegraph_s["duration_s"],
271
+ verbose=True,
272
+ )
273
+ except ValueError as e:
274
+ warn(f"A/B 对比报告失败: {e}")
275
+
276
+ baseline_report = next((p for arm, p in reports if arm == "baseline"), None)
277
+ homegraph_report = next((p for arm, p in reports if arm == "homegraph"), None)
278
+ single_report = reports[0][1] if len(reports) == 1 else None
279
+
280
+ analyze.print_report_locations(
281
+ baseline_report=baseline_report,
282
+ homegraph_report=homegraph_report,
283
+ compare_report=compare_path,
284
+ single_report=single_report,
285
+ )
286
+ return reports, compare_path
287
+
288
+
289
+ def _blocking_homegraph_index(work_dir: Path, *, skip_index: bool = False) -> dict:
290
+ """Run full ``homegraph index`` and block until complete (or validate existing index)."""
291
+ if skip_index:
292
+ header("HomeGraph 索引检查(--skip-index,不执行 index)")
293
+ else:
294
+ header("HomeGraph 全量 index(阻塞,Photos 约 15–30 分钟)")
295
+ log(f"仓库: {work_dir}")
296
+ if skip_index:
297
+ log("模式: 仅校验已有 .homegraph/(可从同 commit 的其他机器复制整个目录)")
298
+ else:
299
+ log("命令: node --stack-size=8192 … homegraph index(见 deveco_arm.HOMEGRAPH_NODE_STACK_SIZE_KB)")
300
+ meta = ensure_homegraph_index(work_dir, skip=skip_index)
301
+ if not meta.get("homegraph_index_success"):
302
+ error("HomeGraph index 失败,中止。")
303
+ err = meta.get("homegraph_index_error", "")
304
+ if err:
305
+ error(f" 原因: {err}")
306
+ sys.exit(1)
307
+ return meta
308
+
309
+
310
+ def main():
311
+ analyze._configure_stdout()
312
+ cfg = load_config()
313
+ meta = cfg.get("meta", {})
314
+
315
+ parser = argparse.ArgumentParser(description="HomeGraph 档位切换边界评测")
316
+ parser.add_argument("--agent", default=os.environ.get("EVAL_AGENT", "deveco"),
317
+ help="Agent:claude / cursor / codex / opencode / deveco")
318
+ parser.add_argument("--arm", default=os.environ.get("EVAL_ARM", "baseline"),
319
+ choices=["baseline", "homegraph"],
320
+ help="baseline=不带 HomeGraph;homegraph=带 HomeGraph")
321
+ parser.add_argument("--both-arms", action="store_true",
322
+ help="一条命令依次跑 baseline 和 homegraph 两组 A/B")
323
+ parser.add_argument("--repo-path", default=os.environ.get("REPO_PATH", ""),
324
+ help="使用已有本地 clone,跳过 git clone")
325
+ parser.add_argument("--keep-clone", action="store_true")
326
+ parser.add_argument(
327
+ "--skip-index",
328
+ action="store_true",
329
+ help="不跑 homegraph init/index;要求仓库根目录已有健康的 .homegraph/(可复制自同 commit 的其他 checkout)",
330
+ )
331
+ parser.add_argument("--no-skip-permissions", action="store_true")
332
+ parser.add_argument("--branch", default=os.environ.get("BRANCH", meta.get("default_branch", "weekly_20260601")))
333
+ parser.add_argument("--repo-url", default=os.environ.get("REPO_URL", meta.get("repo_url", "")))
334
+ parser.add_argument("experiments", nargs="*")
335
+ args = parser.parse_args()
336
+
337
+ if not args.experiments:
338
+ args.experiments = DEFAULT_EXPERIMENTS
339
+
340
+ agent = get_agent(args.agent)
341
+ ok, resolved, err_msg = verify_agent_binary(agent)
342
+ if not ok:
343
+ error(err_msg)
344
+ sys.exit(1)
345
+ log(f"Agent CLI: {resolved}")
346
+
347
+ if args.both_arms and args.arm != "baseline":
348
+ warn("--both-arms 已指定,忽略 --arm")
349
+
350
+ if args.both_arms:
351
+ _rd, _sd = create_output_dirs(args.agent or "default", "baseline")
352
+ work_dir, _, _ = resolve_work_dir(args, _sd)
353
+
354
+ # Index FIRST so the user sees it immediately (15–30 min); baseline does not use MCP.
355
+ index_meta = _blocking_homegraph_index(work_dir, skip_index=args.skip_index)
356
+
357
+ summaries = []
358
+ print(f"\n{'=' * 60}\n 开始 homegraph 组(先看 HomeGraph 是否生效)\n{'=' * 60}\n")
359
+ summaries.append(run_suite(args, "homegraph", prebuilt_index_meta=index_meta))
360
+
361
+ print(f"\n{'=' * 60}\n 开始 baseline 组(无 HomeGraph)\n{'=' * 60}\n")
362
+ summaries.append(run_suite(args, "baseline"))
363
+
364
+ header("A/B 全部完成")
365
+ for s in summaries:
366
+ print(f" [{s['arm']}] 通过 {s['passed']},失败 {s['failed']},"
367
+ f"耗时 {s['duration_s'] // 60}m{s['duration_s'] % 60}s")
368
+ print(f" 输出 → {s['results_dir']}")
369
+ _generate_reports(summaries, args.agent or "default")
370
+ sys.exit(0 if all(s["failed"] == 0 for s in summaries) else 1)
371
+
372
+ summary = run_suite(args, args.arm)
373
+ _generate_reports([summary], args.agent or "default")
374
+ sys.exit(0 if summary["failed"] == 0 else 1)
375
+
376
+
377
+ if __name__ == "__main__":
378
+ main()