niceeval 0.6.1 → 0.7.0

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 (378) hide show
  1. package/INDEX.md +23 -23
  2. package/README.zh.md +6 -6
  3. package/dist/agents/types.d.ts +69 -7
  4. package/dist/context/types.d.ts +32 -12
  5. package/dist/i18n/en.d.ts +54 -0
  6. package/dist/i18n/zh-CN.d.ts +57 -3
  7. package/dist/o11y/types.d.ts +16 -2
  8. package/dist/report/aggregate.d.ts +32 -24
  9. package/dist/report/aggregate.js +158 -50
  10. package/dist/report/built-in/index.d.ts +2 -0
  11. package/dist/report/built-in/index.js +8 -0
  12. package/dist/report/components.d.ts +93 -160
  13. package/dist/report/components.js +377 -114
  14. package/dist/report/compute.d.ts +87 -81
  15. package/dist/report/compute.js +597 -417
  16. package/dist/report/flag.d.ts +32 -6
  17. package/dist/report/flag.js +92 -4
  18. package/dist/report/format.d.ts +19 -11
  19. package/dist/report/format.js +30 -13
  20. package/dist/report/index.d.ts +16 -16
  21. package/dist/report/index.js +20 -21
  22. package/dist/report/load.js +3 -2
  23. package/dist/report/locale.d.ts +57 -33
  24. package/dist/report/locale.js +122 -56
  25. package/dist/report/metrics.d.ts +23 -4
  26. package/dist/report/metrics.js +110 -25
  27. package/dist/report/primitives.d.ts +48 -15
  28. package/dist/report/primitives.js +135 -26
  29. package/dist/report/react/AttemptList.d.ts +9 -7
  30. package/dist/report/react/AttemptList.js +17 -10
  31. package/dist/report/react/DeltaTable.js +19 -18
  32. package/dist/report/react/EvalList.d.ts +4 -4
  33. package/dist/report/react/EvalList.js +0 -0
  34. package/dist/report/react/ExperimentComparison.d.ts +10 -0
  35. package/dist/report/react/ExperimentComparison.js +12 -0
  36. package/dist/report/react/ExperimentList.d.ts +4 -3
  37. package/dist/report/react/ExperimentList.js +17 -18
  38. package/dist/report/react/MetricBars.js +5 -4
  39. package/dist/report/react/MetricLine.js +12 -5
  40. package/dist/report/react/MetricMatrix.js +1 -1
  41. package/dist/report/react/MetricScatter.js +59 -28
  42. package/dist/report/react/MetricTable.js +2 -12
  43. package/dist/report/react/ScopeSummary.d.ts +10 -0
  44. package/dist/report/react/ScopeSummary.js +17 -0
  45. package/dist/report/react/Scoreboard.js +6 -6
  46. package/dist/report/react/cell.js +2 -2
  47. package/dist/report/react/chart-math.d.ts +23 -6
  48. package/dist/report/react/chart-math.js +71 -19
  49. package/dist/report/react/fixtures.d.ts +5 -9
  50. package/dist/report/react/fixtures.js +110 -147
  51. package/dist/report/react/index.d.ts +15 -5
  52. package/dist/report/react/index.js +18 -7
  53. package/dist/report/report.d.ts +137 -16
  54. package/dist/report/report.js +259 -28
  55. package/dist/report/text/faces.d.ts +17 -19
  56. package/dist/report/text/faces.js +253 -184
  57. package/dist/report/text/plot.js +1 -1
  58. package/dist/report/text/table.js +38 -7
  59. package/dist/report/tree.d.ts +90 -40
  60. package/dist/report/tree.js +252 -94
  61. package/dist/report/types.d.ts +247 -284
  62. package/dist/report/types.js +4 -3
  63. package/dist/report/web.d.ts +21 -5
  64. package/dist/report/web.js +42 -16
  65. package/dist/results/select.d.ts +38 -16
  66. package/dist/results/select.js +73 -25
  67. package/dist/results/types.d.ts +49 -14
  68. package/dist/runner/feedback/sink.d.ts +110 -0
  69. package/dist/runner/types.d.ts +513 -22
  70. package/dist/sandbox/docker.d.ts +23 -2
  71. package/dist/sandbox/e2b.d.ts +15 -1
  72. package/dist/sandbox/errors.d.ts +30 -3
  73. package/dist/sandbox/io-retry.d.ts +17 -0
  74. package/dist/sandbox/registry.d.ts +2 -0
  75. package/dist/sandbox/resolve.d.ts +18 -5
  76. package/dist/sandbox/retry.d.ts +11 -1
  77. package/dist/sandbox/types.d.ts +39 -5
  78. package/dist/sandbox/vercel.d.ts +7 -1
  79. package/dist/scoring/coverage.d.ts +30 -0
  80. package/dist/scoring/display.d.ts +21 -0
  81. package/dist/scoring/display.js +120 -0
  82. package/dist/scoring/types.d.ts +103 -20
  83. package/dist/shared/aggregate.d.ts +4 -2
  84. package/dist/shared/aggregate.js +8 -7
  85. package/dist/shared/types.d.ts +28 -0
  86. package/dist/tty-line.d.ts +0 -4
  87. package/dist/util.d.ts +23 -0
  88. package/docs-site/zh/README.md +44 -0
  89. package/docs-site/zh/examples/ai-agent-application.mdx +63 -0
  90. package/docs-site/zh/examples/coding-agent-extensions.mdx +57 -0
  91. package/docs-site/zh/examples/index.mdx +50 -0
  92. package/docs-site/zh/{concepts → explanation}/adapter.mdx +31 -13
  93. package/docs-site/zh/{concepts → explanation}/assert.mdx +7 -7
  94. package/docs-site/zh/{concepts → explanation}/drive.mdx +8 -8
  95. package/docs-site/zh/{concepts → explanation}/evals.mdx +4 -4
  96. package/docs-site/zh/{concepts → explanation}/experiment.mdx +8 -8
  97. package/docs-site/zh/{concepts → explanation}/hitl.mdx +8 -8
  98. package/docs-site/zh/{concepts → explanation}/judge.mdx +5 -5
  99. package/docs-site/zh/{concepts → explanation}/overview.mdx +11 -11
  100. package/docs-site/zh/{guides → explanation}/runner.mdx +18 -8
  101. package/docs-site/zh/{concepts → explanation}/tier.mdx +6 -6
  102. package/docs-site/zh/{guides → how-to}/agent-feedback-loop.mdx +35 -33
  103. package/docs-site/zh/{guides → how-to}/authoring.mdx +35 -2
  104. package/docs-site/zh/{guides → how-to}/ci-integration.mdx +23 -12
  105. package/docs-site/zh/{guides → how-to}/connect-otel.mdx +6 -6
  106. package/docs-site/zh/{guides → how-to}/connect-your-agent.mdx +47 -21
  107. package/docs-site/zh/{guides → how-to}/custom-reports.mdx +34 -39
  108. package/docs-site/zh/{guides → how-to}/dataset-fanout.mdx +25 -3
  109. package/docs-site/zh/{guides → how-to}/experiments.mdx +12 -5
  110. package/docs-site/zh/how-to/publish-report.mdx +105 -0
  111. package/docs-site/zh/{guides → how-to}/reporters.mdx +2 -2
  112. package/docs-site/zh/{guides → how-to}/sandbox-agent.mdx +56 -7
  113. package/docs-site/zh/how-to/sandbox-providers.mdx +350 -0
  114. package/docs-site/zh/{guides → how-to}/scoring-guide.mdx +4 -4
  115. package/docs-site/zh/{guides → how-to}/viewing-results.mdx +82 -39
  116. package/docs-site/zh/{guides → how-to}/write-experiment.mdx +6 -4
  117. package/docs-site/zh/{guides → how-to}/write-send.mdx +30 -14
  118. package/docs-site/zh/index.mdx +24 -26
  119. package/docs-site/zh/introduction.mdx +8 -8
  120. package/docs-site/zh/reference/builtin-agents.mdx +32 -5
  121. package/docs-site/zh/reference/capabilities.mdx +8 -8
  122. package/docs-site/zh/reference/cli.mdx +40 -12
  123. package/docs-site/zh/reference/define-agent.mdx +58 -5
  124. package/docs-site/zh/reference/define-config.mdx +1 -1
  125. package/docs-site/zh/reference/define-eval.mdx +42 -9
  126. package/docs-site/zh/reference/events.mdx +3 -3
  127. package/docs-site/zh/reference/expect.mdx +26 -1
  128. package/docs-site/zh/{guides → reference}/official-adapters.mdx +32 -8
  129. package/docs-site/zh/{guides → reference}/report-components.mdx +45 -33
  130. package/docs-site/zh/{guides → reference}/results-data.mdx +21 -13
  131. package/docs-site/zh/troubleshooting/debug-sandbox.mdx +57 -0
  132. package/docs-site/zh/troubleshooting/debugging.mdx +212 -0
  133. package/docs-site/zh/{quickstart.mdx → tutorials/quickstart.mdx} +5 -17
  134. package/package.json +10 -2
  135. package/src/agents/ai-sdk-otel.test.ts +1 -0
  136. package/src/agents/ai-sdk.test.ts +3 -0
  137. package/src/agents/ai-sdk.ts +3 -0
  138. package/src/agents/bub-install-spec.test.ts +34 -0
  139. package/src/agents/bub-install-spec.ts +32 -0
  140. package/src/agents/bub.ts +31 -32
  141. package/src/agents/claude-code.test.ts +130 -9
  142. package/src/agents/claude-code.ts +76 -4
  143. package/src/agents/codex.test.ts +189 -40
  144. package/src/agents/codex.ts +155 -14
  145. package/src/agents/coding-cli-versions.test.ts +15 -0
  146. package/src/agents/coding-cli-versions.ts +3 -0
  147. package/src/agents/index.ts +13 -2
  148. package/src/agents/langgraph.test.ts +204 -0
  149. package/src/agents/langgraph.ts +495 -0
  150. package/src/agents/marketplace.ts +85 -0
  151. package/src/agents/native-config.test.ts +179 -0
  152. package/src/agents/native-config.ts +267 -0
  153. package/src/agents/openai-compat.test.ts +1 -0
  154. package/src/agents/openai-compat.ts +1 -1
  155. package/src/agents/openclaw.test.ts +31 -0
  156. package/src/agents/openclaw.ts +171 -0
  157. package/src/agents/plugin-config.test.ts +1 -0
  158. package/src/agents/sdk-streams.test.ts +79 -0
  159. package/src/agents/sdk-streams.ts +55 -10
  160. package/src/agents/skills.test.ts +1 -0
  161. package/src/agents/streaming.test.ts +3 -9
  162. package/src/agents/streaming.ts +2 -2
  163. package/src/agents/types.ts +71 -8
  164. package/src/agents/ui-message-stream.test.ts +3 -0
  165. package/src/cli.ts +446 -124
  166. package/src/context/context.test.ts +51 -12
  167. package/src/context/context.ts +162 -30
  168. package/src/context/session.test.ts +2 -1
  169. package/src/context/session.ts +115 -7
  170. package/src/context/types.ts +30 -12
  171. package/src/define.test.ts +13 -8
  172. package/src/define.ts +25 -4
  173. package/src/expect/index.ts +53 -23
  174. package/src/i18n/en.ts +81 -17
  175. package/src/i18n/zh-CN.ts +80 -17
  176. package/src/o11y/cost.test.ts +1 -0
  177. package/src/o11y/execution-tree.test.ts +1 -20
  178. package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
  179. package/src/o11y/otlp/parse.test.ts +1 -0
  180. package/src/o11y/otlp/turn-otel.test.ts +1 -0
  181. package/src/o11y/parsers/bub.test.ts +1 -0
  182. package/src/o11y/parsers/claude-code.test.ts +1 -34
  183. package/src/o11y/parsers/openclaw.test.ts +154 -0
  184. package/src/o11y/parsers/openclaw.ts +310 -0
  185. package/src/o11y/prices.json +746 -311
  186. package/src/o11y/tool-names.test.ts +1 -0
  187. package/src/o11y/types.ts +16 -2
  188. package/src/report/aggregate.ts +178 -61
  189. package/src/report/built-in/index.tsx +9 -0
  190. package/src/report/components.tsx +625 -279
  191. package/src/report/compute.ts +723 -491
  192. package/src/report/dual-render.test.tsx +741 -1024
  193. package/src/report/flag.ts +104 -12
  194. package/src/report/format.ts +32 -12
  195. package/src/report/index.ts +119 -46
  196. package/src/report/load.ts +3 -2
  197. package/src/report/locale.ts +136 -65
  198. package/src/report/metrics.ts +108 -25
  199. package/src/report/primitives.tsx +196 -45
  200. package/src/report/react/AttemptList.tsx +30 -43
  201. package/src/report/react/DeltaTable.tsx +63 -45
  202. package/src/report/react/EvalList.tsx +0 -0
  203. package/src/report/react/ExperimentComparison.tsx +73 -0
  204. package/src/report/react/ExperimentList.tsx +50 -32
  205. package/src/report/react/MetricBars.tsx +5 -4
  206. package/src/report/react/MetricLine.tsx +13 -8
  207. package/src/report/react/MetricMatrix.tsx +2 -2
  208. package/src/report/react/MetricScatter.tsx +86 -34
  209. package/src/report/react/MetricTable.tsx +4 -76
  210. package/src/report/react/ScopeSummary.tsx +86 -0
  211. package/src/report/react/Scoreboard.tsx +28 -10
  212. package/src/report/react/cell.tsx +2 -2
  213. package/src/report/react/chart-math.test.ts +85 -0
  214. package/src/report/react/chart-math.ts +101 -22
  215. package/src/report/react/enhance.js +89 -5
  216. package/src/report/react/fixtures.ts +114 -154
  217. package/src/report/react/index.tsx +24 -39
  218. package/src/report/react/render.test.tsx +138 -158
  219. package/src/report/react/styles.css +243 -82
  220. package/src/report/report.test.ts +779 -841
  221. package/src/report/report.ts +423 -41
  222. package/src/report/text/faces.ts +290 -193
  223. package/src/report/text/plot.ts +1 -1
  224. package/src/report/text/table.ts +44 -7
  225. package/src/report/tree.ts +362 -104
  226. package/src/report/types.ts +261 -271
  227. package/src/report/web.ts +63 -20
  228. package/src/results/annotated-source.test.ts +62 -9
  229. package/src/results/annotated-source.ts +64 -6
  230. package/src/results/attempt-evidence.test.ts +13 -11
  231. package/src/results/attempt-evidence.ts +20 -13
  232. package/src/results/attempt-source.ts +6 -3
  233. package/src/results/copy.ts +150 -60
  234. package/src/results/host-equivalence.test.ts +34 -20
  235. package/src/results/index.ts +12 -4
  236. package/src/results/locator.test.ts +1 -22
  237. package/src/results/open.ts +15 -5
  238. package/src/results/publish.ts +149 -0
  239. package/src/results/results.test.ts +89 -54
  240. package/src/results/select.ts +104 -34
  241. package/src/results/truncate.ts +90 -0
  242. package/src/results/types.ts +43 -14
  243. package/src/results/writer.ts +31 -13
  244. package/src/runner/attempt.test.ts +138 -7
  245. package/src/runner/attempt.ts +603 -104
  246. package/src/runner/discover.test.ts +47 -0
  247. package/src/runner/discover.ts +36 -2
  248. package/src/runner/eval-source.test.ts +1 -27
  249. package/src/runner/feedback/agent.test.ts +504 -0
  250. package/src/runner/feedback/agent.ts +409 -0
  251. package/src/runner/feedback/ci.test.ts +562 -0
  252. package/src/runner/feedback/ci.ts +401 -0
  253. package/src/runner/feedback/coordinator.test.ts +317 -0
  254. package/src/runner/feedback/coordinator.ts +397 -0
  255. package/src/runner/feedback/failure.ts +40 -0
  256. package/src/runner/feedback/human.test.ts +616 -0
  257. package/src/runner/feedback/human.ts +535 -0
  258. package/src/runner/feedback/index.ts +66 -0
  259. package/src/runner/feedback/io.ts +78 -0
  260. package/src/runner/feedback/profile.test.ts +50 -0
  261. package/src/runner/feedback/profile.ts +58 -0
  262. package/src/runner/feedback/reducer.test.ts +395 -0
  263. package/src/runner/feedback/reducer.ts +260 -0
  264. package/src/runner/feedback/renderer.ts +82 -0
  265. package/src/runner/feedback/sink.ts +203 -0
  266. package/src/runner/feedback/testing.ts +106 -0
  267. package/src/runner/ledger.test.ts +230 -0
  268. package/src/runner/ledger.ts +329 -0
  269. package/src/runner/report.test.ts +128 -3
  270. package/src/runner/report.ts +33 -9
  271. package/src/runner/reporters/artifacts.ts +8 -2
  272. package/src/runner/reporters/braintrust.test.ts +8 -7
  273. package/src/runner/reporters/braintrust.ts +9 -2
  274. package/src/runner/reporters/index.ts +2 -2
  275. package/src/runner/reporters/json.test.ts +162 -0
  276. package/src/runner/reporters/json.ts +35 -8
  277. package/src/runner/reporters/shared.ts +1 -5
  278. package/src/runner/run.test.ts +760 -3
  279. package/src/runner/run.ts +243 -37
  280. package/src/runner/sandbox-prep.ts +3 -42
  281. package/src/runner/timing.ts +158 -0
  282. package/src/runner/types.ts +518 -22
  283. package/src/sandbox/checkpoint.test.ts +55 -0
  284. package/src/sandbox/checkpoint.ts +29 -8
  285. package/src/sandbox/cli-commands.ts +407 -0
  286. package/src/sandbox/docker.ts +115 -16
  287. package/src/sandbox/e2b-agent-template.test.ts +56 -0
  288. package/src/sandbox/e2b-agent-template.ts +94 -0
  289. package/src/sandbox/e2b.ts +74 -9
  290. package/src/sandbox/errors.ts +111 -4
  291. package/src/sandbox/index.ts +2 -0
  292. package/src/sandbox/io-retry.test.ts +58 -0
  293. package/src/sandbox/io-retry.ts +45 -0
  294. package/src/sandbox/keep-registry.test.ts +86 -0
  295. package/src/sandbox/keep-registry.ts +142 -0
  296. package/src/sandbox/keep.ts +178 -0
  297. package/src/sandbox/paths.test.ts +1 -0
  298. package/src/sandbox/paths.ts +19 -8
  299. package/src/sandbox/registry.ts +20 -3
  300. package/src/sandbox/resolve.ts +76 -11
  301. package/src/sandbox/retry.test.ts +70 -0
  302. package/src/sandbox/retry.ts +46 -4
  303. package/src/sandbox/types.ts +44 -6
  304. package/src/sandbox/vercel.ts +43 -20
  305. package/src/scoring/collector.ts +60 -17
  306. package/src/scoring/coverage.ts +95 -0
  307. package/src/scoring/diff.ts +81 -0
  308. package/src/scoring/display.test.ts +121 -0
  309. package/src/scoring/display.ts +133 -0
  310. package/src/scoring/evidence.test.ts +189 -0
  311. package/src/scoring/judge.test.ts +142 -0
  312. package/src/scoring/judge.ts +15 -18
  313. package/src/scoring/scoped.ts +217 -50
  314. package/src/scoring/types.ts +117 -20
  315. package/src/scoring/verdict.ts +16 -4
  316. package/src/shared/aggregate.ts +8 -6
  317. package/src/shared/types.ts +31 -0
  318. package/src/show/compose.ts +50 -67
  319. package/src/show/index.ts +127 -56
  320. package/src/show/render.ts +662 -131
  321. package/src/show/report-host.test.ts +188 -0
  322. package/src/show/report-host.ts +375 -0
  323. package/src/show/show.test.ts +320 -54
  324. package/src/tty-line.ts +8 -26
  325. package/src/util.test.ts +1 -0
  326. package/src/util.ts +41 -0
  327. package/src/view/app/App.test.tsx +69 -0
  328. package/src/view/app/App.tsx +144 -48
  329. package/src/view/app/components/AttemptModal.tsx +423 -11
  330. package/src/view/app/components/CodeView.tsx +41 -14
  331. package/src/view/app/components/CopyControls.tsx +2 -2
  332. package/src/view/app/i18n.ts +37 -17
  333. package/src/view/app/lib/attempt-route.test.ts +1 -0
  334. package/src/view/app/lib/verdict.ts +7 -9
  335. package/src/view/app/main.tsx +13 -8
  336. package/src/view/app/pages/{RunsPage.tsx → AttemptsPage.tsx} +6 -6
  337. package/src/view/app/types.ts +4 -1
  338. package/src/view/artifact-serving.test.ts +2 -1
  339. package/src/view/client-dist/app.css +1 -1
  340. package/src/view/client-dist/app.js +17 -17
  341. package/src/view/data.test.ts +10 -3
  342. package/src/view/data.ts +155 -49
  343. package/src/view/index.ts +56 -41
  344. package/src/view/server.ts +37 -15
  345. package/src/view/shared/types.ts +34 -5
  346. package/src/view/styles.css +227 -0
  347. package/src/view/view-report.test.ts +167 -62
  348. package/dist/report/built-ins/experiment-comparison.d.ts +0 -1
  349. package/dist/report/built-ins/experiment-comparison.js +0 -13
  350. package/dist/report/built-ins/index.d.ts +0 -1
  351. package/dist/report/built-ins/index.js +0 -2
  352. package/dist/report/react/GroupSummary.d.ts +0 -8
  353. package/dist/report/react/GroupSummary.js +0 -8
  354. package/dist/report/react/RunOverview.d.ts +0 -8
  355. package/dist/report/react/RunOverview.js +0 -12
  356. package/docs-site/zh/example/ai-agent-application.mdx +0 -152
  357. package/docs-site/zh/example/claude-code-codex-plugin.mdx +0 -167
  358. package/docs-site/zh/example/claude-code-codex-skill.mdx +0 -152
  359. package/docs-site/zh/example/showcase.mdx +0 -39
  360. package/docs-site/zh/guides/publish-report.mdx +0 -91
  361. package/docs-site/zh/guides/sandbox-providers.mdx +0 -102
  362. package/src/report/built-in-user-parity.test.tsx +0 -640
  363. package/src/report/built-ins/experiment-comparison.tsx +0 -19
  364. package/src/report/built-ins/index.ts +0 -2
  365. package/src/report/react/GroupSummary.tsx +0 -66
  366. package/src/report/react/RunOverview.tsx +0 -109
  367. package/src/runner/reporters/console.ts +0 -70
  368. package/src/runner/reporters/live.test.ts +0 -56
  369. package/src/runner/reporters/live.ts +0 -247
  370. package/src/runner/reporters/quiet.test.ts +0 -66
  371. package/src/runner/reporters/quiet.ts +0 -49
  372. package/src/runner/reporters/table.ts +0 -277
  373. /package/docs-site/zh/{example/tier1-ai-sdk-v7.mdx → examples/integrations/ai-sdk-v7.mdx} +0 -0
  374. /package/docs-site/zh/{example/tier1-claude-sdk.mdx → examples/integrations/claude-sdk.mdx} +0 -0
  375. /package/docs-site/zh/{example/tier1-codex-sdk.mdx → examples/integrations/codex-sdk.mdx} +0 -0
  376. /package/docs-site/zh/{example/tier1-langgraph.mdx → examples/integrations/langgraph.mdx} +0 -0
  377. /package/docs-site/zh/{example/tier1-pi-sdk.mdx → examples/integrations/pi-sdk.mdx} +0 -0
  378. /package/docs-site/zh/{guides → how-to}/fixtures.mdx +0 -0
package/src/cli.ts CHANGED
@@ -5,23 +5,36 @@
5
5
  // niceeval clean 删除 .niceeval/ 历史运行 artifact
6
6
 
7
7
  import { spawn } from "node:child_process";
8
+ import { createHash } from "node:crypto";
8
9
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
9
10
  import { existsSync } from "node:fs";
10
- import { join, relative } from "node:path";
11
+ import { join, relative, resolve as resolvePath } from "node:path";
11
12
  import { pathToFileURL } from "node:url";
12
13
  import { parseArgs as nodeParseArgs } from "node:util";
13
14
  import { discoverEvals, discoverExperiments, makeFilter } from "./runner/discover.ts";
14
15
  import { runEvals, type AgentRun } from "./runner/run.ts";
15
- import { runWho } from "./runner/types.ts";
16
16
  import { planCarry } from "./runner/fingerprint.ts";
17
+ import { failureDetailFromResult } from "./runner/feedback/failure.ts";
17
18
  import { stopAllSandboxes, liveSandboxCount } from "./sandbox/registry.ts";
18
19
  import { evalLevelStats } from "./shared/verdict.ts";
19
20
  import { sandboxRecommendedConcurrency } from "./sandbox/resolve.ts";
20
- import { Console as ConsoleReporter } from "./runner/reporters/console.ts";
21
- import { Quiet as QuietReporter } from "./runner/reporters/quiet.ts";
22
21
  import { Json, JUnit } from "./runner/reporters/json.ts";
23
- import { Live as LiveReporter, type LiveRow } from "./runner/reporters/live.ts";
24
22
  import { Artifacts as ArtifactsReporter } from "./runner/reporters/artifacts.ts";
23
+ import {
24
+ resolveOutputProfile,
25
+ createFeedbackCoordinator,
26
+ createNodeFeedbackIO,
27
+ createHumanRenderer,
28
+ createAgentRenderer,
29
+ createCiRenderer,
30
+ renderAgentPlanEnvelope,
31
+ renderHumanDryPlan,
32
+ renderCiDryPlan,
33
+ computeCiExitCode,
34
+ reportActivity,
35
+ type OutputProfileFlag,
36
+ type AgentPlanRow,
37
+ } from "./runner/feedback/index.ts";
25
38
  import {
26
39
  buildView,
27
40
  startViewServer,
@@ -38,7 +51,17 @@ import { ReportLoadError } from "../dist/report/load.js";
38
51
  import { runShow } from "./show/index.ts";
39
52
  import { t } from "./i18n/index.ts";
40
53
  import { formatThrown, upsertManagedBlock } from "./util.ts";
41
- import type { Config, DiscoveredExperiment, Reporter } from "./types.ts";
54
+ import type {
55
+ CompletionStatus,
56
+ Config,
57
+ DiscoveredExperiment,
58
+ ReporterError,
59
+ ReporterRegistration,
60
+ RunCompletion,
61
+ RunFeedbackPlan,
62
+ RunFeedbackState,
63
+ RunSummary,
64
+ } from "./types.ts";
42
65
 
43
66
  /**
44
67
  * view 的可预期用户错误:版本不同的报告(npx 提示)、位置参数/组合语义错误、
@@ -60,7 +83,8 @@ interface Flags {
60
83
  timeout?: number;
61
84
  earlyExit?: boolean;
62
85
  dry: boolean;
63
- quiet: boolean;
86
+ /** 反馈 profile,已解析/校验(`auto` 默认值也算已解析——具体环境判定见 `resolveOutputProfile`)。 */
87
+ output: OutputProfileFlag;
64
88
  force: boolean;
65
89
  strict: boolean;
66
90
  budget?: number;
@@ -73,15 +97,24 @@ interface Flags {
73
97
  help: boolean;
74
98
  version: boolean;
75
99
  // ── show 专属(位置参数仍是 eval id 前缀 / `@<locator>`;这些 flag 选「怎么看」)──
76
- eval: boolean;
100
+ source: boolean;
77
101
  execution: boolean;
78
102
  diff: boolean;
79
103
  /** --diff=<路径>(必须 = 连写;空格形式会把路径当 eval id 前缀,按文档如此)。 */
80
104
  diffPath?: string;
105
+ timing?: "summary" | "full";
106
+ keepSandbox?: "failed" | "all";
107
+ all: boolean;
108
+ allowSensitiveArtifacts: boolean;
109
+ window?: string;
110
+ sandboxPath?: string;
111
+ leaveRunning: boolean;
81
112
  history: boolean;
82
113
  experiment?: string;
83
- run?: string;
114
+ results?: string;
115
+ snapshot?: string;
84
116
  report?: string;
117
+ page?: string;
85
118
  }
86
119
 
87
120
  // 表驱动的 flag 定义(node:util parseArgs)。--no-x 显式声明,不依赖 allowNegative(需 Node 20.14+,
@@ -103,6 +136,16 @@ const FLAG_OPTIONS = {
103
136
  timeout: { type: "string" },
104
137
  /** 整次运行的预算上限(美元)。 */
105
138
  budget: { type: "string" },
139
+ /** `exp` 命令专用:跑完留下 failed/errored attempt 的沙箱现场(= `--keep-sandbox=failed`);`--keep-sandbox=all` 连 passed 也留。事后用 `niceeval sandbox list/enter/stop` 查看与销毁。 */
140
+ "keep-sandbox": { type: "boolean" },
141
+ /** `sandbox stop` 专用:销毁全部留存沙箱。 */
142
+ all: { type: "boolean" },
143
+ /** `sandbox diff` 专用:只看某个 send 窗口(如 `--window s1/t2`);省略输出全部窗口的串联视图。 */
144
+ window: { type: "string" },
145
+ /** `sandbox diff` 专用:只看某个文件的 patch;省略输出该窗口的全部文件。 */
146
+ path: { type: "string" },
147
+ /** `sandbox enter` 专用:shell 退出后让现场保持运行,不送回休眠。 */
148
+ "leave-running": { type: "boolean" },
106
149
  /** 只运行带有该 tag 的 eval(见 `defineEval` 的 `tags`)。 */
107
150
  tag: { type: "string" },
108
151
  /** 额外写一份 JUnit XML 报告到指定路径,供 CI 消费。 */
@@ -113,29 +156,37 @@ const FLAG_OPTIONS = {
113
156
  out: { type: "string" },
114
157
  /** `view` 命令专用:指定本地服务器监听端口。 */
115
158
  port: { type: "string" },
116
- // show 的证据切面 / 时间轴 / 报告装载(docs-site/zh/guides/viewing-results.mdx)。
159
+ /** `view --out` 专用:对非发布根(快照没有 publish:{redaction:"applied"} 标记)导出时的显式确认——静态站会原样携带未消毒的证据文件。 */
160
+ "allow-sensitive-artifacts": { type: "boolean" },
161
+ // show 的证据切面 / 时间轴 / 报告装载(docs-site/zh/how-to/viewing-results.mdx)。
117
162
  // 证据切面只认 `@<locator>`(或收窄到单个 eval 的前缀)选出的那一个 attempt——不再有
118
163
  // 数字 `--attempt`,选哪个 attempt 由 locator 精确指名,不是「先选 eval 再挑第几次」。
119
164
  /** `show` 命令专用:该 attempt 运行时保存的 Eval 源码,gate/soft 断言标回源码行(证据切面)。 */
120
- eval: { type: "boolean" },
165
+ source: { type: "boolean" },
121
166
  /** `show` 命令专用:该 attempt 的标准执行事件流(消息、thinking、Skill load、工具调用/结果);有 OTel 时同一节点补时间(证据切面)。 */
122
167
  execution: { type: "boolean" },
168
+ /** `show` 命令专用:整个 Attempt 的统一时间树;裸 `--timing` 给有界诊断投影,`--timing=full` 逐节点展开全部 runner/已关联 OTel 节点。 */
169
+ timing: { type: "boolean" },
123
170
  // --diff 是布尔;--diff=<路径> 在 parseArgs 前预扫成 diffPath(路径必须 = 连写,
124
171
  // 空格形式的下一个 token 仍是位置参数 = eval id 前缀,与文档一致)。
125
172
  /** `show` 命令专用:sandbox 里的文件改动摘要;`--diff=<文件路径>` 看单个文件的完整改动(路径必须 `=` 连写)。 */
126
173
  diff: { type: "boolean" },
127
- /** `show` 命令专用:跨 run 时间轴,只列真实执行;与 `--report` 互斥。 */
174
+ /** `show` 命令专用:执行时间轴——对匹配的每个 experiment × eval 分节,逐 attempt 列时间 / verdict / 摘要 / 耗时 / 成本 / locator;与 `--report` 互斥。 */
128
175
  history: { type: "boolean" },
129
- /** `show` / `view` 命令专用:Selection 只留该实验。 */
176
+ /** `show` / `view` 命令专用:按路径段前缀收窄 experiment;组名会选中组内全部配置。 */
130
177
  experiment: { type: "string" },
131
- /** `show` / `view` 命令专用:钉死看某一个结果目录(某次快照或 `copySnapshots` 产物)。 */
132
- run: { type: "string" },
133
- /** `show` / `view` 命令专用:渲染你的报告文件(文件默认导出 `defineReport(...)`);show 用它替换 Attempt 索引,view 用它替换默认分析报告。 */
178
+ /** `show` / `view` / `sandbox enter|list|stop` 共用:结果根目录(`.niceeval` 之外的另一个根,如 `copySnapshots` 产出的发布根)。 */
179
+ results: { type: "string" },
180
+ /** `view` 命令专用:只打开这一份快照文件(`snapshot.json`);文件不可读时命令失败(扫描模式只跳过) */
181
+ snapshot: { type: "string" },
182
+ /** `show` / `view` 命令专用:用文件默认导出的 `defineReport(...)` 替换两者共用的默认报告。 */
134
183
  report: { type: "string" },
135
- /** 只打印本次会匹配到的 eval × 运行配置,不实际执行。 */
184
+ /** `show` / `view` 命令专用:选择多页报告的页(页 id 见 `show --report` 的页索引);`view` 里定初始页。 */
185
+ page: { type: "string" },
186
+ /** 只打印本次会匹配到的 eval × 运行配置,不实际执行(按下面 `--output` 选中的 profile 给出预览)。 */
136
187
  dry: { type: "boolean" },
137
- /** 关闭控制台 / live 的逐条结果与末尾汇总(attempt 进度行仍写 stderr);errored / failed 的结果各在 stderr 补一行摘要,passed / skipped 静默;reporter 仍会写 artifacts。 */
138
- quiet: { type: "boolean" },
188
+ /** 反馈 profile:`auto`(默认)按环境自动选择,`human` / `agent` / `ci` 强制指定;只改变终端展示,不改变选择、调度、判定、artifact 或退出码。`auto` 依次判定:stderr TTY human;否则 `CI`(或其它常见 CI 平台环境变量)存在 ci;否则 → agent。 */
189
+ output: { type: "string" },
139
190
  /** 忽略上次运行结果,不跳过已通过的 (experiment, eval) 组合,强制全部重跑。 */
140
191
  force: { type: "boolean" },
141
192
  /** CI 中推荐使用:让软阈值(`soft`)失败也计入整条 eval 的 verdict。 */
@@ -162,17 +213,50 @@ function numberFlag(name: string, raw: string | undefined): number | undefined {
162
213
  return n;
163
214
  }
164
215
 
216
+ const OUTPUT_PROFILE_VALUES = ["auto", "human", "agent", "ci"] as const;
217
+
218
+ /** `--output` 解析仅接受 `auto|human|agent|ci`;非法值直接报清晰用法并退出,不静默回退到某个默认值。 */
219
+ function outputFlag(raw: string | undefined): OutputProfileFlag {
220
+ if (raw === undefined) return "auto";
221
+ if ((OUTPUT_PROFILE_VALUES as readonly string[]).includes(raw)) return raw as OutputProfileFlag;
222
+ process.stderr.write(t("cli.flag.invalidOutput", { value: raw }));
223
+ process.exit(1);
224
+ }
225
+
165
226
  function parseArgs(argv: string[]): { command: string; positionals: string[]; flags: Flags } {
166
227
  if (argv[0] === "--") argv = argv.slice(1);
167
228
 
168
229
  // --diff=<路径> 预扫:diff 本体是布尔(裸 --diff = 文件级摘要),路径只接受 = 连写。
169
230
  let diffPath: string | undefined;
231
+ // --keep-sandbox[=failed|all] 预扫:本体是布尔(裸 = failed 档),档位只接受 = 连写。
232
+ let keepSandboxTier: "failed" | "all" | undefined;
233
+ // --timing[=summary|full] 预扫:node:util 的单个 option 不支持 boolean|string 联合,
234
+ // 所以 mode 在严格 parseArgs 前提取,再把两种形式统一成布尔 --timing。
235
+ let timingMode: "summary" | "full" | undefined;
170
236
  argv = argv.map((arg) => {
171
237
  if (arg.startsWith("--diff=")) {
172
238
  const path = arg.slice("--diff=".length);
173
239
  if (path) diffPath = path;
174
240
  return "--diff";
175
241
  }
242
+ if (arg.startsWith("--keep-sandbox=")) {
243
+ const tier = arg.slice("--keep-sandbox=".length);
244
+ if (tier !== "failed" && tier !== "all") {
245
+ process.stderr.write(`--keep-sandbox only accepts "failed" (default) or "all", got "${tier}".\n`);
246
+ process.exit(1);
247
+ }
248
+ keepSandboxTier = tier;
249
+ return "--keep-sandbox";
250
+ }
251
+ if (arg.startsWith("--timing=")) {
252
+ const mode = arg.slice("--timing=".length);
253
+ if (mode !== "summary" && mode !== "full") {
254
+ process.stderr.write(`--timing only accepts "summary" (default) or "full", got "${mode}".\n`);
255
+ process.exit(1);
256
+ }
257
+ timingMode = mode;
258
+ return "--timing";
259
+ }
176
260
  return arg;
177
261
  });
178
262
 
@@ -188,7 +272,7 @@ function parseArgs(argv: string[]): { command: string; positionals: string[]; fl
188
272
  }
189
273
 
190
274
  // 第一个位置参数若是已知命令,则为命令;其余是 eval id 前缀 / view 输入。
191
- const commands = new Set(["exp", "show", "list", "view", "clean", "init", "watch", "run"]);
275
+ const commands = new Set(["exp", "show", "list", "view", "clean", "init", "watch", "run", "sandbox"]);
192
276
  let command = "run";
193
277
  let positionals = rawPositionals;
194
278
  if (rawPositionals[0] && commands.has(rawPositionals[0])) {
@@ -209,25 +293,61 @@ function parseArgs(argv: string[]): { command: string; positionals: string[]; fl
209
293
  out: values.out as string | undefined,
210
294
  port: numberFlag("port", values.port as string | undefined),
211
295
  dry: values.dry === true,
212
- quiet: values.quiet === true,
296
+ output: outputFlag(values.output as string | undefined),
213
297
  force: values.force === true,
214
298
  strict: values.strict === true,
215
299
  earlyExit: values["no-early-exit"] === true ? false : values["early-exit"] === true ? true : undefined,
216
300
  open: values["no-open"] === true ? false : values.open === true ? true : undefined,
217
301
  help: values.help === true,
218
302
  version: values.version === true,
219
- eval: values.eval === true,
303
+ source: values.source === true,
220
304
  execution: values.execution === true,
221
305
  diff: values.diff === true && diffPath === undefined,
222
306
  diffPath,
307
+ timing: values.timing === true ? (timingMode ?? "summary") : undefined,
308
+ keepSandbox: values["keep-sandbox"] === true ? (keepSandboxTier ?? "failed") : undefined,
309
+ all: values.all === true,
310
+ allowSensitiveArtifacts: values["allow-sensitive-artifacts"] === true,
311
+ window: values.window as string | undefined,
312
+ sandboxPath: values.path as string | undefined,
313
+ leaveRunning: values["leave-running"] === true,
223
314
  history: values.history === true,
224
315
  experiment: values.experiment as string | undefined,
225
- run: values.run as string | undefined,
316
+ results: values.results as string | undefined,
317
+ snapshot: values.snapshot as string | undefined,
226
318
  report: values.report as string | undefined,
319
+ page: values.page as string | undefined,
227
320
  };
228
321
  return { command, positionals, flags };
229
322
  }
230
323
 
324
+ /**
325
+ * exp 只接受两类输入:位置参数选「跑哪些 eval」+ 调度/输出/机器出口 flag 选「对着哪个 agent、
326
+ * 怎么跑」。show / view 专属的证据切面(`--source`/`--execution`/`--diff`)、时间轴(`--history`)、
327
+ * Scope 收窄(`--experiment`/`--results`)、报告装载(`--report`/`--page`)、查看器
328
+ * (`--snapshot`/`--out`/`--port`/`--open`)不能被 exp 静默忽略(见 docs/feature/experiments/
329
+ * cli.md「用法错误」)。返回第一个被误用的 flag 及其归属命令(用于报错),没有误用返回 undefined。
330
+ */
331
+ function firstViewerOnlyFlag(flags: Flags): { flag: string; command: string } | undefined {
332
+ const SHOW = "show";
333
+ const BOTH = "show / view";
334
+ const VIEW = "view";
335
+ if (flags.source) return { flag: "--source", command: SHOW };
336
+ if (flags.execution) return { flag: "--execution", command: SHOW };
337
+ if (flags.timing !== undefined) return { flag: "--timing", command: SHOW };
338
+ if (flags.diff || flags.diffPath !== undefined) return { flag: "--diff", command: SHOW };
339
+ if (flags.history) return { flag: "--history", command: SHOW };
340
+ if (flags.experiment !== undefined) return { flag: "--experiment", command: BOTH };
341
+ if (flags.results !== undefined) return { flag: "--results", command: BOTH };
342
+ if (flags.report !== undefined) return { flag: "--report", command: BOTH };
343
+ if (flags.page !== undefined) return { flag: "--page", command: BOTH };
344
+ if (flags.snapshot !== undefined) return { flag: "--snapshot", command: VIEW };
345
+ if (flags.out !== undefined) return { flag: "--out", command: VIEW };
346
+ if (flags.port !== undefined) return { flag: "--port", command: VIEW };
347
+ if (flags.open !== undefined) return { flag: "--open", command: VIEW };
348
+ return undefined;
349
+ }
350
+
231
351
  /** 调度项的环境变量层(标志 > 环境变量 > config > 默认,见 docs/cli.md「配置优先级」)。 */
232
352
  function envNumber(name: string): number | undefined {
233
353
  const raw = process.env[name]?.trim();
@@ -283,7 +403,7 @@ const AGENT_RULES_CONTENT = [
283
403
  "the bundled Chinese docs are the authoritative version matching this installation.",
284
404
  "After a run, drill into failures with `niceeval show` — pick an `@<locator>` from the",
285
405
  "compact index it prints, then `niceeval show @<locator>` for a compact overview, or add",
286
- "`--eval` / `--execution` / `--diff` for evidence; the snapshot directories the CLI prints",
406
+ "`--source` / `--execution` / `--diff` for evidence; the snapshot directories the CLI prints",
287
407
  "are the structured source of truth: `snapshot.json` holds the run's metadata and each",
288
408
  "`<evalId>/a<attempt>/result.json` holds that attempt's verdict and assertions, next to",
289
409
  "its artifact files (`events.json` / `trace.json` / `diff.json`).",
@@ -357,6 +477,76 @@ function evalsFilterFromExperiment(
357
477
  return (id) => expFilter(id) && patternFilter(id);
358
478
  }
359
479
 
480
+ /**
481
+ * evals 过滤器的指纹(进 ExperimentRunInfo.evalFilterFingerprint,供「配置没变」判断):
482
+ * 数组按内容、函数按函数体哈希;CLI 追加的位置参数前缀一并计入。不存过滤器本身——
483
+ * 求值结果在 selectedEvalIds(见 runEvals)。
484
+ */
485
+ function fingerprintEvalsFilter(evals: DiscoveredExperiment["evals"], patterns: string[]): string {
486
+ const basis =
487
+ evals === undefined || evals === "*"
488
+ ? "*"
489
+ : Array.isArray(evals)
490
+ ? JSON.stringify([...evals].sort())
491
+ : evals.toString();
492
+ return createHash("sha256").update(JSON.stringify({ basis, patterns })).digest("hex").slice(0, 16);
493
+ }
494
+
495
+ /**
496
+ * run 结束后把 coordinator 累计的诊断折成 `RunCompletion`(见 docs/feature/experiments/cli.md
497
+ * 「运行完成状态不只看 verdict 计数」)。只读已经真实发生过的诊断,不额外发明信号:
498
+ * - `"interrupted"` 诊断只在 run.ts 判定为真·中断(Effect exit 真实标记中断,不是「signal 被
499
+ * abort 过」这种更弱的信号)时才会出现,见 `runner/run.ts` 的 `reportInterrupted()` 调用点。
500
+ * - `"budget-exhausted:<experimentId>"` 诊断的 `count` 就是该 experiment 因预算耗尽未派发的
501
+ * attempt 数(见 `runner/feedback/reducer.ts` 对 `budget-exhausted` 事件的注释),跨
502
+ * experiment 求和得到 `unstarted`。
503
+ * - `"reporter-error:<reporter>"` 诊断转成 `ReporterError[]`;`required` 字段来自事件自带的
504
+ * `data.required`,直接反映这个 reporter 注册时的真实 required/best-effort 分类(见上面
505
+ * 构造 `reporters: ReporterRegistration[]` 的地方——artifacts / --json / --junit 恒
506
+ * `required: true`,`config.reporters` 恒 `false`),不是一个统一写死的占位值。
507
+ * - `earlyExitUnstarted` 从反馈状态的 attempt:early-exit 计数派生(减去 fail-fast 的那部分——
508
+ * 那是「未完整覆盖」,进 unstarted,不是「省下的重复验证」)。
509
+ */
510
+ function assembleRunCompletion(state: RunFeedbackState): RunCompletion {
511
+ let unstarted = 0;
512
+ let failFastSkipped = 0;
513
+ let interrupted = false;
514
+ const reporterErrors: ReporterError[] = [];
515
+ for (const d of state.diagnostics) {
516
+ if (d.key === "interrupted") {
517
+ interrupted = true;
518
+ } else if (d.key.startsWith("budget-exhausted:")) {
519
+ unstarted += d.count;
520
+ } else if (d.key.startsWith("fail-fast:")) {
521
+ // run 级 fail-fast 造成的未派发同样计入 unstarted(结论落 incomplete,见
522
+ // docs/feature/experiments/architecture.md「Completion 与退出」)。
523
+ unstarted += d.count;
524
+ failFastSkipped += d.count;
525
+ } else if (d.key.startsWith("reporter-error:")) {
526
+ // required 决定这条错误是否写进 RunCompletion.reporterErrors 并让 completion 非 complete
527
+ // (见 docs/cli.md「required reporter」);best-effort reporter 的失败只保留为 diagnostic。
528
+ if (d.data?.required !== true) continue;
529
+ reporterErrors.push({
530
+ reporter: typeof d.data?.reporter === "string" ? d.data.reporter : d.key.slice("reporter-error:".length),
531
+ required: true,
532
+ message: d.message,
533
+ });
534
+ }
535
+ }
536
+ // 中断造成的未派发(仍在 queued 的 attempt)同样计入 unstarted(见 docs/feature/experiments/
537
+ // architecture.md「Completion 与退出」:budget 耗尽、fail-fast 或中断造成的未派发都不伪装成全绿)。
538
+ if (interrupted) unstarted += state.queued;
539
+ // attempt:early-exit 计数含 fail-fast 的未派发(反馈层同一事件驱动计数守恒);
540
+ // 「省下的重复验证」= 总数减去 fail-fast 那部分。
541
+ const earlyExitUnstarted = Math.max(0, state.earlyExitSkipped - failFastSkipped);
542
+ const status: CompletionStatus = interrupted
543
+ ? "interrupted"
544
+ : unstarted > 0 || reporterErrors.length > 0
545
+ ? "incomplete"
546
+ : "complete";
547
+ return { status, unstarted, earlyExitUnstarted, reporterErrors };
548
+ }
549
+
360
550
  /** package.json 的 version 字段;-v/--version 直接回显这个号。 */
361
551
  async function packageVersion(): Promise<string> {
362
552
  const raw = await readFile(new URL("../package.json", import.meta.url), "utf-8");
@@ -380,11 +570,17 @@ async function main(): Promise<void> {
380
570
  }
381
571
 
382
572
  if (command === "view") {
383
- // 位置参数 = eval id 前缀(收窄报告槽 Selection);存在的文件路径 = 单文件模式;
384
- // 结果目录经 --run 递入;--report 整槽替换报告槽(与 show --report 吃同一个文件)
573
+ // 位置参数只有一种含义:eval id 前缀(收窄报告槽 Scope)。结果根经 --results 递入,
574
+ // 单开一份快照经 --snapshot 递入;--report 整槽替换报告槽(与 show --report 吃同一个文件),
575
+ // --page 定初始页。文件与目录都不进位置参数(docs/feature/reports/view.md「打开与收窄」)。
576
+ // --out 与位置参数 / --experiment 的互斥在 buildView 内校验(单点,报错文案含 copySnapshots
577
+ // + filter 的下一步),经 exitOnViewUserError 按用法错误退出。
385
578
  let viewInput: { input?: string; patterns: string[] };
386
579
  try {
387
- viewInput = resolveViewInput(cwd, positionals, flags.run);
580
+ viewInput = resolveViewInput(cwd, positionals, {
581
+ ...(flags.results !== undefined ? { results: flags.results } : {}),
582
+ ...(flags.snapshot !== undefined ? { snapshot: flags.snapshot } : {}),
583
+ });
388
584
  } catch (e) {
389
585
  exitOnViewUserError(e);
390
586
  }
@@ -392,9 +588,10 @@ async function main(): Promise<void> {
392
588
  patterns: viewInput.patterns,
393
589
  ...(flags.experiment !== undefined ? { experiment: flags.experiment } : {}),
394
590
  ...(flags.report !== undefined ? { report: { path: flags.report, cwd } } : {}),
591
+ ...(flags.page !== undefined ? { page: flags.page } : {}),
395
592
  };
396
593
  if (flags.out) {
397
- const out = await buildView({ input: viewInput.input, out: flags.out, scan }).catch(exitOnViewUserError);
594
+ const out = await buildView({ input: viewInput.input, out: flags.out, allowSensitiveArtifacts: flags.allowSensitiveArtifacts, scan }).catch(exitOnViewUserError);
398
595
  process.stdout.write(t("cli.view.exportedDir", { out }));
399
596
  process.exit(0);
400
597
  }
@@ -410,17 +607,34 @@ async function main(): Promise<void> {
410
607
  await new Promise(() => {});
411
608
  }
412
609
 
610
+ if (command === "sandbox") {
611
+ // sandbox 命令组不读 niceeval.config.ts、不发现 eval:只操作留存注册表与 provider 的
612
+ // detached 能力(见 docs/feature/sandbox/cli.md)。
613
+ const { runSandboxCommand } = await import("./sandbox/cli-commands.ts");
614
+ const code = await runSandboxCommand(cwd, positionals, {
615
+ all: flags.all,
616
+ window: flags.window,
617
+ path: flags.sandboxPath,
618
+ leaveRunning: flags.leaveRunning,
619
+ // CLI flag 是 --results(结果根);sandbox 命令组的内部选项名保持 run,值语义相同。
620
+ run: flags.results,
621
+ });
622
+ process.exit(code);
623
+ }
624
+
413
625
  if (command === "show") {
414
- // show 不依赖 niceeval.config.ts:读的是 .niceeval/(或 --run 指定的某个快照目录)的落盘结果。
626
+ // show 不依赖 niceeval.config.ts:读的是 .niceeval/(或 --results 指定的结果根)的落盘结果。
415
627
  const code = await runShow(cwd, positionals, {
416
- eval: flags.eval,
628
+ source: flags.source,
417
629
  execution: flags.execution,
630
+ timing: flags.timing,
418
631
  diff: flags.diff,
419
632
  diffPath: flags.diffPath,
420
633
  history: flags.history,
421
634
  experiment: flags.experiment,
422
- run: flags.run,
635
+ results: flags.results,
423
636
  report: flags.report,
637
+ page: flags.page,
424
638
  });
425
639
  process.exit(code);
426
640
  }
@@ -453,15 +667,26 @@ async function main(): Promise<void> {
453
667
  }
454
668
 
455
669
  const agentRuns: AgentRun[] = [];
670
+ let experimentSelection = t("cli.all");
671
+ let availableExperimentGroups = t("cli.none");
456
672
 
457
673
  if (command === "exp") {
458
674
  if (flags.agent || flags.model) {
459
675
  process.stderr.write(t("cli.exp.agentModelFlagUnsupported"));
460
676
  process.exit(1);
461
677
  }
678
+ const viewerFlag = firstViewerOnlyFlag(flags);
679
+ if (viewerFlag) {
680
+ process.stderr.write(t("cli.exp.viewerFlagUnsupported", { flag: viewerFlag.flag, command: viewerFlag.command }));
681
+ process.exit(1);
682
+ }
462
683
  const experiments = await discoverExperiments(cwd);
463
684
  const expArg = positionals[0];
464
685
  const extraPatterns = positionals.slice(1);
686
+ experimentSelection = positionals.join(" ") || t("cli.all");
687
+ availableExperimentGroups = [...new Set(experiments.map((experiment) => experiment.group || experiment.id))]
688
+ .sort()
689
+ .join(", ") || t("cli.none");
465
690
  const selected = expArg
466
691
  ? experiments.filter((e) => e.group === expArg || e.id === expArg || e.id.startsWith(expArg + "/"))
467
692
  : experiments;
@@ -470,8 +695,21 @@ async function main(): Promise<void> {
470
695
  arg: expArg ?? t("cli.all"),
471
696
  experiments: experiments.map((e) => e.id).join(", ") || t("cli.none"),
472
697
  }));
698
+ // show / view 是顶层命令。只有同名 experiment 确实不存在时才纠错,不能抢占合法 id。
699
+ if (expArg === "show" || expArg === "view") {
700
+ process.stderr.write(t("cli.experiment.viewerCommandHint", {
701
+ command: expArg,
702
+ args: extraPatterns.length > 0 ? ` ${extraPatterns.join(" ")}` : "",
703
+ }));
704
+ }
473
705
  process.exit(1);
474
706
  }
707
+ // 残留提醒:注册表里还有上次留下的沙箱时打一行(不阻塞、不清理)。
708
+ {
709
+ const { keptSandboxReminder } = await import("./sandbox/cli-commands.ts");
710
+ const reminder = await keptSandboxReminder(cwd).catch(() => undefined);
711
+ if (reminder) process.stderr.write(reminder);
712
+ }
475
713
  for (const exp of selected) {
476
714
  // 一个实验 = 一个配置(单 model)。跨模型对比写多个实验文件,各钉一个 model。
477
715
  agentRuns.push({
@@ -486,6 +724,8 @@ async function main(): Promise<void> {
486
724
  budget: flags.budget ?? envNumber("NICEEVAL_BUDGET") ?? exp.budget,
487
725
  evalFilter: evalsFilterFromExperiment(exp.evals, extraPatterns),
488
726
  experimentId: exp.id,
727
+ description: exp.description,
728
+ evalFilterFingerprint: fingerprintEvalsFilter(exp.evals, extraPatterns),
489
729
  strict: flags.strict,
490
730
  // 实验级并发上限:随 AgentRun 进调度器按实验单独限流(runner 两级信号量),
491
731
  // 不再取所有选中实验的最小值钳全局——那会让一个串行实验拖慢整批基线。
@@ -513,69 +753,119 @@ async function main(): Promise<void> {
513
753
  process.exit(1);
514
754
  }
515
755
 
756
+ // profile 只改变反馈,不改变选择/调度/判定;显式值覆盖 auto 检测(见 resolveOutputProfile)。
757
+ // --dry 和真正开跑共用同一个已解析 profile。
758
+ const outputProfile = resolveOutputProfile({
759
+ explicit: flags.output,
760
+ isTTY: process.stderr.isTTY === true,
761
+ env: process.env,
762
+ });
763
+
764
+ // matchedByRun[i] 对应 agentRuns[i] 匹配到的 eval 集合;--dry 预览与真正开跑时的
765
+ // RunFeedbackPlan(总量、去重 eval 数)共用同一份计算,不重复过滤一遍。
766
+ const matchedByRun = agentRuns.map((run) => evals.filter((e) => run.evalFilter(e.id)));
767
+ const totalRuns = agentRuns.reduce((sum, run, i) => sum + matchedByRun[i]!.length * run.runs, 0);
768
+ const uniqueEvalIds = new Set(matchedByRun.flat().map((e) => e.id));
769
+
770
+ if (totalRuns === 0) {
771
+ process.stderr.write(t("cli.experiment.noEvalsSelected", {
772
+ selection: experimentSelection,
773
+ experiments: availableExperimentGroups,
774
+ }));
775
+ process.exit(1);
776
+ }
777
+
516
778
  if (flags.dry) {
517
- process.stdout.write(t("cli.dry.header", { evals: evals.length, configs: agentRuns.length }));
518
- for (const run of agentRuns) {
519
- const matched = evals.filter((e) => run.evalFilter(e.id));
520
- const who = run.model ? `${run.agent.name}/${run.model}` : run.agent.name;
521
- process.stdout.write(t("cli.dry.row", {
522
- who,
523
- experiment: run.experimentId ? ` (exp ${run.experimentId})` : "",
524
- evals: matched.map((e) => e.id).join(", ") || t("cli.dry.noMatches"),
525
- runs: run.runs,
526
- }));
779
+ // --dry 只按所选 profile 打印计划,不运行、不落盘 —— 三种 profile 各自的展示逻辑
780
+ // 都在 runner/feedback/{human,agent,ci}.ts 里,这里只负责拼数据、选函数、写流、退出。
781
+ if (outputProfile === "agent") {
782
+ const rows: AgentPlanRow[] = [];
783
+ for (let i = 0; i < agentRuns.length; i++) {
784
+ const run = agentRuns[i]!;
785
+ const label = run.experimentId ?? (run.model ? `${run.agent.name}/${run.model}` : run.agent.name);
786
+ for (const e of matchedByRun[i]!) rows.push({ label, evalId: e.id });
787
+ }
788
+ process.stdout.write(
789
+ renderAgentPlanEnvelope({
790
+ total: totalRuns,
791
+ evals: uniqueEvalIds.size,
792
+ configs: agentRuns.length,
793
+ runs: Math.max(1, ...agentRuns.map((r) => r.runs)),
794
+ rows,
795
+ }) + "\n",
796
+ );
797
+ } else if (outputProfile === "ci") {
798
+ process.stdout.write(
799
+ renderCiDryPlan({
800
+ total: totalRuns,
801
+ evals: uniqueEvalIds.size,
802
+ configs: agentRuns.length,
803
+ rows: agentRuns.map((run, i) => ({
804
+ experimentId: run.experimentId,
805
+ who: run.model ? `${run.agent.name}/${run.model}` : run.agent.name,
806
+ evalIds: matchedByRun[i]!.map((e) => e.id),
807
+ runs: run.runs,
808
+ })),
809
+ }),
810
+ );
811
+ } else {
812
+ process.stdout.write(
813
+ renderHumanDryPlan({
814
+ evals: evals.length,
815
+ configs: agentRuns.length,
816
+ rows: agentRuns.map((run, i) => ({
817
+ who: run.model ? `${run.agent.name}/${run.model}` : run.agent.name,
818
+ experimentSuffix: run.experimentId ? ` (exp ${run.experimentId})` : "",
819
+ evalIds: matchedByRun[i]!.map((e) => e.id),
820
+ runs: run.runs,
821
+ })),
822
+ }),
823
+ );
527
824
  }
528
825
  process.exit(0);
529
826
  }
530
827
 
531
- // 提前算好(而不是等 runEvals 内部算):live 表格要在第一帧就知道哪些行会被携入
532
- // (carry),直接渲染成已完成,不然会显示"waiting for a slot"直到进程退出——它们
533
- // 永远等不到 eval:start,run.ts 压根不会为携入的 (experimentId, evalId) 派发 attempt。
534
- // 两处必须共用同一份 planCarry() 判断,否则各自算一遍,一旦判断不一致,live 表格
535
- // 显示的"携入"和 run.ts 实际调度的"携入"就会对不上。
828
+ // 提前算好携入计划:coordinator 的 plan 事件与 runEvals 内部
829
+ // 实际调度必须共用同一份 planCarry() 判断,否则两边各自算一遍,一旦不一致,dashboard/
830
+ // envelope 展示的"携入"就会和 run.ts 真实调度的"携入"对不上( memory
831
+ // live-carry-row-shows-waiting-forever)
536
832
  const priorResults = flags.force ? undefined : await loadLatestResultsPerEval(join(cwd, ".niceeval"));
537
833
  const carryPlan = priorResults?.length ? await planCarry(evals, agentRuns, priorResults) : undefined;
538
- const carriedVerdictByKey = new Map<string, string>();
539
- for (const r of carryPlan?.carriedResults ?? []) {
540
- if (r.experimentId) carriedVerdictByKey.set(`${r.experimentId}|${r.id}`, r.verdict);
541
- }
834
+ const reusedFailures = (carryPlan?.carriedResults ?? [])
835
+ .map(failureDetailFromResult)
836
+ .filter((failure) => failure !== undefined);
542
837
 
543
- const reporters: Reporter[] = [];
544
- let onProgress: ((evalId: string, who: string, msg: string) => void) | undefined;
545
-
546
- if (!flags.quiet) {
547
- if (process.stderr.isTTY) {
548
- // TTY 模式:用 live display 替换 Console reporter,把 attempt log 路由到状态表行尾
549
- const liveRows: LiveRow[] = [];
550
- for (const agentRun of agentRuns) {
551
- // who 必须与 attempt.ts 的进度上报同源(runWho):曾用 agent/model,同 agent 同 model
552
- // 的实验变体(xxx 与 xxx--agents-md)会被折叠成一行,0/2 看起来像同一 eval 跑两次。
553
- const who = runWho({ agentName: agentRun.agent.name, model: agentRun.model, experimentId: agentRun.experimentId });
554
- const matched = evals.filter((e) => agentRun.evalFilter(e.id));
555
- for (const evalDef of matched) {
556
- const carriedVerdict = agentRun.experimentId
557
- ? carriedVerdictByKey.get(`${agentRun.experimentId}|${evalDef.id}`)
558
- : undefined;
559
- liveRows.push({ evalId: evalDef.id, who, total: agentRun.runs, carriedVerdict });
560
- }
561
- }
562
- const totalAttempts = liveRows.reduce((s, r) => s + r.total, 0);
563
- const live = LiveReporter(liveRows, totalAttempts);
564
- reporters.push(live);
565
- onProgress = (evalId, who, msg) => live.progress(evalId, who, msg);
566
- } else {
567
- reporters.push(ConsoleReporter());
568
- }
569
- } else {
570
- // --quiet:进度流照旧直写 stderr,结果流换成最小报告器 —— errored / failed 各补一行
571
- // stderr,passed / skipped 静默。没有它,attempt 出执行错时控制台会全程无声。
572
- reporters.push(QuietReporter());
573
- }
574
- const artifacts = ArtifactsReporter();
575
- reporters.push( artifacts);
576
- if (flags.junit) reporters.push(JUnit(flags.junit));
577
- if (flags.json) reporters.push(Json(flags.json));
578
- reporters.push(...(config.reporters ?? []));
838
+ // 无全局默认:并发上限由 sandbox provider 的推荐值决定(多个 agentRun 各有 sandbox 时取
839
+ // 最小值,最保守的 provider 决定上限)。同一个值既进 RunFeedbackPlan.shape,也传给 runEvals——
840
+ // 两处必须是同一个数字,dashboard 展示的并发上限不能和真实调度的并发上限对不上。
841
+ const sandboxRecs = agentRuns.map((r) => sandboxRecommendedConcurrency(r.sandbox));
842
+ const sandboxDefaultConcurrency = sandboxRecs.length > 0 ? Math.min(...sandboxRecs) : 10;
843
+ const maxConcurrency =
844
+ flags.maxConcurrency ??
845
+ envNumber("NICEEVAL_MAX_CONCURRENCY") ??
846
+ config.maxConcurrency ??
847
+ sandboxDefaultConcurrency;
848
+
849
+ const plan: RunFeedbackPlan = {
850
+ shape: { evals: uniqueEvalIds.size, configs: agentRuns.length, totalRuns, maxConcurrency },
851
+ reused: carryPlan?.carriedResults.length ?? 0,
852
+ reusedFailures,
853
+ };
854
+
855
+ // 一个 run 内只有一个终端协调者(见 docs/feature/experiments/cli.md「输出流和落盘节奏」):
856
+ // 三种 profile 各自的展示逻辑全部在 renderer 里,这里只按解析出的 profile 选一个构造好、
857
+ // 交给 coordinator。run:start 前(coordinator.start(plan) 之前)的一切都还没有活跃 sink,
858
+ // 出错走 bootstrap stderr;之后所有诊断都经它。
859
+ const io = createNodeFeedbackIO();
860
+ const commandLabel = ["niceeval", command, ...positionals].join(" ").trim();
861
+ const renderer =
862
+ outputProfile === "human"
863
+ ? createHumanRenderer({ io, command: commandLabel })
864
+ : outputProfile === "agent"
865
+ ? createAgentRenderer({ io })
866
+ : createCiRenderer({ io });
867
+ const coordinator = createFeedbackCoordinator({ profile: outputProfile, renderer, io });
868
+ coordinator.start(plan);
579
869
 
580
870
  // Ctrl+C / kill 的三级响应,核心目标:任何情况下都不留下孤儿沙箱。
581
871
  // 1 次:abort controller → runEvals 把它喂给 Effect signal → 各 attempt 的 Scope 跑 release
@@ -586,28 +876,29 @@ async function main(): Promise<void> {
586
876
  // 3 次:真不耐烦了,硬退(此时多半已无可清理的)。
587
877
  const ctrl = new AbortController();
588
878
  let signalCount = 0;
589
- // 兜底强清 + 退出:只跑一次,带超时(stopAllSandboxes 内每个 stop 各自有超时)
879
+ // 兜底强清 + 退出:只跑一次,带超时(stopAllSandboxes 内每个 stop 各自有超时)。先停 dashboard
880
+ // 的 tick/动态区域(coordinator.stopDynamic()),避免硬退时终端卡在半帧 ANSI 状态。
590
881
  let forcing = false;
591
882
  const forceCleanupAndExit = (code: number) => {
592
883
  if (forcing) return;
593
884
  forcing = true;
594
- void stopAllSandboxes().finally(() => process.exit(code));
885
+ void Promise.all([coordinator.stopDynamic(), stopAllSandboxes()]).finally(() => process.exit(code));
595
886
  };
596
887
  for (const sig of ["SIGINT", "SIGTERM"] as const) {
597
888
  process.on(sig, () => {
598
889
  signalCount += 1;
599
890
  if (signalCount === 1) {
600
- process.stderr.write(t("cli.interruptCleanup"));
891
+ reportActivity(t("cli.interruptCleanup").trimEnd());
601
892
  ctrl.abort();
602
893
  // 看门狗:graceful 清理 12s 还没让进程自己收口,就强清兜底。
603
894
  setTimeout(() => {
604
895
  if (liveSandboxCount() > 0) {
605
- process.stderr.write(t("cli.fallbackCleanupTimeout"));
896
+ reportActivity(t("cli.fallbackCleanupTimeout").trimEnd());
606
897
  forceCleanupAndExit(130);
607
898
  }
608
899
  }, 12_000).unref();
609
900
  } else if (signalCount === 2) {
610
- process.stderr.write(t("cli.forceCleanupExit"));
901
+ reportActivity(t("cli.forceCleanupExit").trimEnd());
611
902
  forceCleanupAndExit(130);
612
903
  } else {
613
904
  process.exit(130); // 第三次:硬退
@@ -615,47 +906,78 @@ async function main(): Promise<void> {
615
906
  });
616
907
  }
617
908
 
618
- // 无全局默认:并发上限由 sandbox provider 的推荐值决定。
619
- // 多个 agentRun 各有 sandbox 时取最小值(最保守的 provider 决定上限)。
620
- const sandboxRecs = agentRuns.map((r) => sandboxRecommendedConcurrency(r.sandbox));
621
- const sandboxDefaultConcurrency = sandboxRecs.length > 0 ? Math.min(...sandboxRecs) : 10;
622
-
623
- const summary = await runEvals({
624
- config,
625
- evals,
626
- agentRuns,
627
- reporters,
628
- maxConcurrency:
629
- flags.maxConcurrency ??
630
- envNumber("NICEEVAL_MAX_CONCURRENCY") ??
631
- config.maxConcurrency ??
632
- sandboxDefaultConcurrency,
633
- signal: ctrl.signal,
634
- onProgress,
635
- priorResults,
636
- carryPlan,
909
+ // reporter 只剩正交的机器/artifact 出口:human/agent/ci 的展示完全由上面的 coordinator +
910
+ // renderer 负责,不再有 Console/Live/Quiet 这类兼职当 reporter 的展示层( docs
911
+ // 「CLI 只负责解析 profile、构造 coordinator/reporters、运行和退出」)。每个 reporter 在这里
912
+ // 按来源分类 required/best-effort(见 `ReporterRegistration` 的字段注释):默认落盘的
913
+ // artifacts、显式指定的 --json/--junit 是 agent/CI 读结果的唯一入口,写失败必须让
914
+ // completion/退出码判红;用户 `config.reporters` 只是补充观测,失败只折成一条 diagnostic,
915
+ // 不影响 completion。
916
+ const reporters: ReporterRegistration[] = [];
917
+ const artifacts = ArtifactsReporter();
918
+ reporters.push({ reporter: artifacts, name: "artifacts", required: true });
919
+ if (flags.junit) reporters.push({ reporter: JUnit(flags.junit), name: "junit", required: true, target: flags.junit });
920
+ if (flags.json) reporters.push({ reporter: Json(flags.json), name: "json", required: true, target: flags.json });
921
+ (config.reporters ?? []).forEach((reporter, i) => {
922
+ reporters.push({ reporter, name: `config-reporter-${i}`, required: false });
637
923
  });
638
924
 
925
+ let summary: RunSummary;
926
+ try {
927
+ summary = await runEvals({
928
+ config,
929
+ evals,
930
+ agentRuns,
931
+ reporters,
932
+ maxConcurrency,
933
+ signal: ctrl.signal,
934
+ priorResults,
935
+ carryPlan,
936
+ keepSandbox: flags.keepSandbox,
937
+ niceevalRoot: resolvePath(cwd, ".niceeval"),
938
+ });
939
+ } catch (e) {
940
+ // 真崩溃前先撤下 dashboard,不让半帧 ANSI 状态和下面 main().catch 打印的错误交织。
941
+ await coordinator.stopDynamic();
942
+ throw e;
943
+ }
944
+
639
945
  // 正常返回(含被中断后走部分汇总)后再兜一刀:Scope finalizer 没停掉的残留沙箱在这里强清。
640
946
  // 跑顺利时登记表已空,是 no-op。
641
947
  await stopAllSandboxes();
642
948
 
643
- // agent 反馈闭环的入口:跑完直接给出每个已创建快照的目录,agent 读 snapshot.json 与各
644
- // attempt result.json / artifact(events/trace/diff),不必解析人类向的流式输出。
645
- // --quiet 下也输出。相对 cwd 的路径更友好;结果落在 cwd 外时(relative 路径以 .. 开头)
646
- // 原样打印绝对路径。
647
- for (const { dir } of artifacts.outputDirs()) {
949
+ // completion 要先算好,--json/--junit 是否"这次真的写出"才有依据(见下)。
950
+ const completion = assembleRunCompletion(coordinator.state);
951
+
952
+ // --json/--junit 是正交机器出口,只在这次运行真的写出对应文件时才把路径交给 coordinator
953
+ // (它转发给 ci renderer 打印独立的 json=/junit= 行,见 docs「CI 怎么用」)。判据是
954
+ // completion.reporterErrors 里有没有这次 required reporter("json"/"junit")的失败记录——
955
+ // 不能用 existsSync 探测磁盘:atomicWriteFile(json.ts)失败时原地保留上一次运行遗留的旧文件,
956
+ // existsSync 只会看到"文件存在"就误判成这次写成功,把上一轮的陈旧内容当成本次结果打印出去。
957
+ const jsonPath = flags.json && !completion.reporterErrors.some((e) => e.reporter === "json") ? flags.json : undefined;
958
+ const junitPath =
959
+ flags.junit && !completion.reporterErrors.some((e) => e.reporter === "junit") ? flags.junit : undefined;
960
+
961
+ // agent 反馈闭环的入口:跑完直接给出每个已创建快照的目录,agent/ci 读 snapshot.json 与各
962
+ // attempt 的 result.json / artifact(events/trace/diff),不必解析人类向的流式输出。相对 cwd
963
+ // 的路径更友好;结果落在 cwd 外时(relative 路径以 .. 开头)原样打印绝对路径。打印本身由
964
+ // renderer 的 "saved" 处理完成,这里只负责把路径交给 coordinator。
965
+ const paths = artifacts.outputDirs().map(({ dir }) => {
648
966
  const rel = relative(cwd, dir);
649
- process.stdout.write(t("cli.resultsPath", { path: rel && !rel.startsWith("..") ? rel : dir }));
650
- }
967
+ return rel && !rel.startsWith("..") ? rel : dir;
968
+ });
651
969
 
652
- // 退出码按 eval 级判定,不按 attempt:summary.failed/errored 统计的是每次 attempt,
653
- // 被 runs+earlyExit 重试吸收的失败(先挂一次、后来过了)不该把进程判红——否则
654
- // 「runs 吸收单次抖动」在 CI 退出码这层永远不成立。折叠口径与报表/view 共用
655
- // foldEvalVerdict(任一轮通过 eval 通过),粒度 experimentId|eval id。
656
- const stats = evalLevelStats(summary.results, (r) => `${r.experimentId ?? ""}|${r.id}`);
657
- const failedExit = stats.failed > 0 || stats.errored > 0;
658
- process.exit(failedExit ? 1 : 0);
970
+ await coordinator.finish({ summary, completion, paths, json: jsonPath, junit: junitPath });
971
+
972
+ // 退出码统一走 CompletionStatus 驱动的语义(interrupted 130、incomplete/required reporter
973
+ // 失败1),不再只看 verdict 计数;三种 profile 共用同一套退出码,不是 ci 专属。failed/errored
974
+ // 先按 (experiment, eval) 折叠再喂给 computeCiExitCode——它只认 RunSummary 原始字段,不知道
975
+ // 「同一 eval 的重试轮不该重复计红」这条 eval 级判定规则(被 runs+earlyExit 重试吸收的失败,
976
+ // 先挂一次、后来过了,不该把进程判红,否则 CI 判定与 evalLevelStats 报表口径不一致;
977
+ // 见 memory 的 cli-exit-code-attempt-level-not-eval-level)。
978
+ const foldedStats = evalLevelStats(summary.results, (r) => `${r.experimentId ?? ""}|${r.id}`);
979
+ const exitCode = computeCiExitCode({ ...summary, failed: foldedStats.failed, errored: foldedStats.errored }, completion);
980
+ process.exit(exitCode);
659
981
  }
660
982
 
661
983
  main().catch(async (e) => {