niceeval 0.5.4 → 0.6.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 (337) hide show
  1. package/INDEX.md +55 -0
  2. package/dist/agents/types.d.ts +321 -0
  3. package/dist/context/types.d.ts +335 -0
  4. package/dist/i18n/core.d.ts +11 -0
  5. package/dist/i18n/en.d.ts +181 -0
  6. package/dist/i18n/index.d.ts +5 -0
  7. package/dist/i18n/zh-CN.d.ts +183 -0
  8. package/dist/o11y/otlp/parse.d.ts +3 -0
  9. package/dist/o11y/otlp/receiver.d.ts +21 -0
  10. package/dist/o11y/otlp/turn-otel.d.ts +43 -0
  11. package/dist/o11y/types.d.ts +190 -0
  12. package/dist/report/aggregate.d.ts +57 -0
  13. package/dist/report/aggregate.js +212 -0
  14. package/dist/report/built-ins/experiment-comparison.d.ts +1 -0
  15. package/dist/report/built-ins/experiment-comparison.js +13 -0
  16. package/dist/report/built-ins/index.d.ts +1 -0
  17. package/dist/report/built-ins/index.js +2 -0
  18. package/dist/report/components.d.ts +180 -0
  19. package/dist/report/components.js +132 -0
  20. package/dist/report/compute.d.ts +106 -0
  21. package/dist/report/compute.js +600 -0
  22. package/dist/report/flag.d.ts +6 -0
  23. package/dist/report/flag.js +8 -0
  24. package/dist/report/format.d.ts +26 -0
  25. package/dist/report/format.js +109 -0
  26. package/dist/report/index.d.ts +19 -0
  27. package/dist/report/index.js +30 -0
  28. package/dist/report/load.d.ts +12 -0
  29. package/dist/report/load.js +56 -0
  30. package/dist/report/locale.d.ts +94 -0
  31. package/dist/report/locale.js +183 -0
  32. package/dist/report/metrics.d.ts +21 -0
  33. package/dist/report/metrics.js +115 -0
  34. package/dist/report/primitives.d.ts +64 -0
  35. package/dist/report/primitives.js +113 -0
  36. package/dist/report/react/AttemptList.d.ts +23 -0
  37. package/dist/report/react/AttemptList.js +19 -0
  38. package/dist/report/react/DeltaTable.d.ts +8 -0
  39. package/dist/report/react/DeltaTable.js +24 -0
  40. package/dist/report/react/EvalList.d.ts +10 -0
  41. package/dist/report/react/EvalList.js +0 -0
  42. package/dist/report/react/ExperimentList.d.ts +11 -0
  43. package/dist/report/react/ExperimentList.js +67 -0
  44. package/dist/report/react/GroupSummary.d.ts +8 -0
  45. package/dist/report/react/GroupSummary.js +8 -0
  46. package/dist/report/react/MetricBars.d.ts +10 -0
  47. package/dist/report/react/MetricBars.js +48 -0
  48. package/dist/report/react/MetricLine.d.ts +9 -0
  49. package/dist/report/react/MetricLine.js +68 -0
  50. package/dist/report/react/MetricMatrix.d.ts +10 -0
  51. package/dist/report/react/MetricMatrix.js +23 -0
  52. package/dist/report/react/MetricScatter.d.ts +9 -0
  53. package/dist/report/react/MetricScatter.js +96 -0
  54. package/dist/report/react/MetricTable.d.ts +11 -0
  55. package/dist/report/react/MetricTable.js +26 -0
  56. package/dist/report/react/RunOverview.d.ts +8 -0
  57. package/dist/report/react/RunOverview.js +12 -0
  58. package/dist/report/react/Scoreboard.d.ts +8 -0
  59. package/dist/report/react/Scoreboard.js +26 -0
  60. package/dist/report/react/cell.d.ts +9 -0
  61. package/dist/report/react/cell.js +9 -0
  62. package/dist/report/react/chart-math.d.ts +14 -0
  63. package/dist/report/react/chart-math.js +68 -0
  64. package/dist/report/react/colors.d.ts +14 -0
  65. package/dist/report/react/colors.js +45 -0
  66. package/dist/report/react/fixtures.d.ts +19 -0
  67. package/dist/report/react/fixtures.js +398 -0
  68. package/dist/report/react/format.d.ts +3 -0
  69. package/dist/report/react/format.js +7 -0
  70. package/dist/report/react/index.d.ts +6 -0
  71. package/dist/report/react/index.js +14 -0
  72. package/dist/report/report.d.ts +24 -0
  73. package/dist/report/report.js +49 -0
  74. package/dist/report/text/faces.d.ts +29 -0
  75. package/dist/report/text/faces.js +541 -0
  76. package/dist/report/text/layout.d.ts +75 -0
  77. package/dist/report/text/layout.js +173 -0
  78. package/dist/report/text/plot.d.ts +37 -0
  79. package/dist/report/text/plot.js +81 -0
  80. package/dist/report/text/table.d.ts +8 -0
  81. package/dist/report/text/table.js +86 -0
  82. package/dist/report/tree.d.ts +99 -0
  83. package/dist/report/tree.js +228 -0
  84. package/dist/report/types.d.ts +408 -0
  85. package/dist/report/types.js +4 -0
  86. package/dist/report/web.d.ts +15 -0
  87. package/dist/report/web.js +37 -0
  88. package/dist/results/locator.d.ts +75 -0
  89. package/dist/results/locator.js +0 -0
  90. package/dist/results/select.d.ts +45 -0
  91. package/dist/results/select.js +230 -0
  92. package/dist/results/source-hash.d.ts +9 -0
  93. package/dist/results/types.d.ts +204 -0
  94. package/dist/runner/eval-source.d.ts +22 -0
  95. package/dist/runner/fingerprint.d.ts +23 -0
  96. package/dist/runner/types.d.ts +332 -0
  97. package/dist/sandbox/docker-stream.d.ts +24 -0
  98. package/dist/sandbox/docker.d.ts +84 -0
  99. package/dist/sandbox/e2b.d.ts +31 -0
  100. package/dist/sandbox/errors.d.ts +4 -0
  101. package/dist/sandbox/local-files.d.ts +6 -0
  102. package/dist/sandbox/paths.d.ts +4 -0
  103. package/dist/sandbox/registry.d.ts +13 -0
  104. package/dist/sandbox/resolve.d.ts +40 -0
  105. package/dist/sandbox/retry.d.ts +10 -0
  106. package/dist/sandbox/shell.d.ts +11 -0
  107. package/dist/sandbox/source-files.d.ts +13 -0
  108. package/dist/sandbox/types.d.ts +203 -0
  109. package/dist/sandbox/vercel.d.ts +37 -0
  110. package/dist/scoring/types.d.ts +70 -0
  111. package/dist/shared/aggregate.d.ts +29 -0
  112. package/dist/shared/aggregate.js +55 -0
  113. package/dist/shared/types.d.ts +27 -0
  114. package/dist/shared/verdict.d.ts +27 -0
  115. package/dist/shared/verdict.js +34 -0
  116. package/dist/tty-line.d.ts +6 -0
  117. package/dist/types.d.ts +7 -0
  118. package/dist/util.d.ts +22 -0
  119. package/docs-site/zh/concepts/adapter.mdx +7 -7
  120. package/docs-site/zh/concepts/assert.mdx +19 -18
  121. package/docs-site/zh/concepts/evals.mdx +12 -11
  122. package/docs-site/zh/concepts/experiment.mdx +2 -2
  123. package/docs-site/zh/concepts/judge.mdx +9 -9
  124. package/docs-site/zh/concepts/overview.mdx +3 -3
  125. package/docs-site/zh/concepts/tier.mdx +6 -6
  126. package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
  127. package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
  128. package/docs-site/zh/guides/agent-feedback-loop.mdx +237 -78
  129. package/docs-site/zh/guides/connect-otel.mdx +3 -3
  130. package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
  131. package/docs-site/zh/guides/custom-reports.mdx +196 -36
  132. package/docs-site/zh/guides/experiments.mdx +7 -1
  133. package/docs-site/zh/guides/fixtures.mdx +4 -2
  134. package/docs-site/zh/guides/official-adapters.mdx +31 -9
  135. package/docs-site/zh/guides/publish-report.mdx +4 -4
  136. package/docs-site/zh/guides/report-components.mdx +194 -55
  137. package/docs-site/zh/guides/reporters.mdx +2 -2
  138. package/docs-site/zh/guides/results-data.mdx +61 -34
  139. package/docs-site/zh/guides/runner.mdx +2 -2
  140. package/docs-site/zh/guides/sandbox-agent.mdx +3 -2
  141. package/docs-site/zh/guides/{sandbox-backends.mdx → sandbox-providers.mdx} +12 -12
  142. package/docs-site/zh/guides/viewing-results.mdx +200 -79
  143. package/docs-site/zh/guides/write-experiment.mdx +9 -1
  144. package/docs-site/zh/guides/write-send.mdx +4 -3
  145. package/docs-site/zh/index.mdx +4 -3
  146. package/docs-site/zh/introduction.mdx +2 -1
  147. package/docs-site/zh/quickstart.mdx +5 -3
  148. package/docs-site/zh/reference/builtin-agents.mdx +38 -13
  149. package/docs-site/zh/reference/capabilities.mdx +1 -1
  150. package/docs-site/zh/reference/cli.mdx +36 -13
  151. package/docs-site/zh/reference/define-agent.mdx +15 -13
  152. package/docs-site/zh/reference/define-config.mdx +1 -1
  153. package/docs-site/zh/reference/define-eval.mdx +3 -3
  154. package/docs-site/zh/reference/events.mdx +9 -3
  155. package/docs-site/zh/reference/expect.mdx +13 -8
  156. package/package.json +10 -6
  157. package/src/agents/bub.ts +129 -42
  158. package/src/agents/claude-code.test.ts +152 -0
  159. package/src/agents/claude-code.ts +127 -13
  160. package/src/agents/codex.test.ts +192 -0
  161. package/src/agents/codex.ts +151 -12
  162. package/src/agents/index.ts +9 -3
  163. package/src/agents/manifest.ts +41 -0
  164. package/src/agents/plugin-config.test.ts +94 -0
  165. package/src/agents/shared.ts +33 -2
  166. package/src/agents/skills.test.ts +214 -0
  167. package/src/agents/skills.ts +304 -0
  168. package/src/agents/streaming.ts +5 -5
  169. package/src/agents/types.ts +59 -3
  170. package/src/cli.ts +74 -47
  171. package/src/context/context.test.ts +20 -2
  172. package/src/context/control-flow.ts +1 -1
  173. package/src/context/session.test.ts +92 -1
  174. package/src/context/session.ts +23 -1
  175. package/src/context/types.ts +4 -4
  176. package/src/define.test.ts +8 -8
  177. package/src/define.ts +11 -11
  178. package/src/expect/index.ts +5 -5
  179. package/src/i18n/en.ts +33 -15
  180. package/src/i18n/zh-CN.ts +37 -21
  181. package/src/index.ts +1 -1
  182. package/src/o11y/execution-tree.test.ts +452 -0
  183. package/src/o11y/execution-tree.ts +367 -0
  184. package/src/o11y/otlp/receiver.ts +1 -1
  185. package/src/o11y/otlp/select.ts +7 -3
  186. package/src/o11y/parsers/claude-code.test.ts +175 -0
  187. package/src/o11y/parsers/claude-code.ts +32 -0
  188. package/src/o11y/types.ts +6 -1
  189. package/src/report/aggregate.ts +26 -20
  190. package/src/report/built-in-user-parity.test.tsx +640 -0
  191. package/src/report/built-ins/experiment-comparison.tsx +19 -0
  192. package/src/report/built-ins/index.ts +2 -0
  193. package/src/report/components.tsx +225 -50
  194. package/src/report/compute.ts +335 -54
  195. package/src/report/dual-render.test.tsx +1129 -0
  196. package/src/report/flag.ts +1 -1
  197. package/src/report/format.ts +35 -2
  198. package/src/report/index.ts +47 -14
  199. package/src/report/load.ts +1 -1
  200. package/src/report/locale.ts +229 -0
  201. package/src/report/metrics.ts +39 -15
  202. package/src/report/primitives.tsx +100 -2
  203. package/src/report/react/AttemptList.tsx +113 -0
  204. package/src/report/react/DeltaTable.tsx +9 -6
  205. package/src/report/react/EvalList.tsx +0 -0
  206. package/src/report/react/ExperimentList.tsx +220 -0
  207. package/src/report/react/GroupSummary.tsx +66 -0
  208. package/src/report/react/MetricBars.tsx +24 -13
  209. package/src/report/react/MetricLine.tsx +21 -21
  210. package/src/report/react/MetricMatrix.tsx +8 -4
  211. package/src/report/react/MetricScatter.tsx +126 -96
  212. package/src/report/react/MetricTable.tsx +115 -15
  213. package/src/report/react/RunOverview.tsx +48 -26
  214. package/src/report/react/Scoreboard.tsx +15 -7
  215. package/src/report/react/cell.tsx +18 -9
  216. package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
  217. package/src/report/react/colors.ts +24 -15
  218. package/src/report/react/enhance.js +168 -0
  219. package/src/report/react/fixtures.ts +169 -34
  220. package/src/report/react/format.ts +1 -1
  221. package/src/report/react/index.tsx +21 -6
  222. package/src/report/react/render.test.tsx +180 -31
  223. package/src/report/react/styles.css +432 -213
  224. package/src/report/report.test.ts +398 -94
  225. package/src/report/report.ts +29 -11
  226. package/src/report/text/faces.ts +344 -96
  227. package/src/report/text/layout.ts +76 -11
  228. package/src/report/text/table.ts +98 -0
  229. package/src/report/tree.ts +103 -20
  230. package/src/report/types.ts +184 -25
  231. package/src/report/web.ts +46 -19
  232. package/src/results/annotated-source.test.ts +146 -0
  233. package/src/results/annotated-source.ts +128 -0
  234. package/src/results/attempt-evidence.test.ts +158 -0
  235. package/src/results/attempt-evidence.ts +158 -0
  236. package/src/results/attempt-source.ts +48 -0
  237. package/src/results/copy.ts +154 -139
  238. package/src/results/format.ts +54 -47
  239. package/src/results/host-equivalence.test.ts +527 -0
  240. package/src/results/index.ts +56 -14
  241. package/src/results/locator.test.ts +198 -0
  242. package/src/results/locator.ts +0 -0
  243. package/src/results/open.ts +392 -175
  244. package/src/results/results.test.ts +943 -494
  245. package/src/results/select.ts +129 -23
  246. package/src/results/skipped-notice.ts +0 -0
  247. package/src/results/source-hash.ts +28 -0
  248. package/src/results/types.ts +98 -62
  249. package/src/results/writer.ts +304 -176
  250. package/src/runner/attempt.test.ts +170 -0
  251. package/src/runner/attempt.ts +48 -13
  252. package/src/runner/discover.test.ts +65 -0
  253. package/src/runner/discover.ts +6 -2
  254. package/src/runner/eval-source.test.ts +121 -0
  255. package/src/runner/eval-source.ts +45 -0
  256. package/src/runner/fingerprint.ts +54 -1
  257. package/src/runner/remote-sandbox.ts +1 -1
  258. package/src/runner/report.test.ts +6 -8
  259. package/src/runner/report.ts +5 -4
  260. package/src/runner/reporters/artifacts.ts +27 -41
  261. package/src/runner/reporters/braintrust.test.ts +3 -3
  262. package/src/runner/reporters/braintrust.ts +1 -1
  263. package/src/runner/reporters/console.ts +6 -6
  264. package/src/runner/reporters/json.ts +4 -4
  265. package/src/runner/reporters/live.test.ts +56 -0
  266. package/src/runner/reporters/live.ts +36 -10
  267. package/src/runner/reporters/quiet.test.ts +66 -0
  268. package/src/runner/reporters/quiet.ts +49 -0
  269. package/src/runner/reporters/shared.ts +5 -5
  270. package/src/runner/reporters/table.ts +17 -17
  271. package/src/runner/run.ts +61 -102
  272. package/src/runner/types.ts +43 -32
  273. package/src/sandbox/checkpoint.ts +1 -1
  274. package/src/sandbox/docker.ts +15 -2
  275. package/src/sandbox/e2b.ts +11 -5
  276. package/src/sandbox/errors.ts +13 -0
  277. package/src/sandbox/index.ts +3 -3
  278. package/src/sandbox/local-files.ts +1 -1
  279. package/src/sandbox/registry.ts +6 -5
  280. package/src/sandbox/resolve.ts +38 -23
  281. package/src/sandbox/retry.test.ts +68 -0
  282. package/src/sandbox/retry.ts +44 -0
  283. package/src/sandbox/types.ts +24 -24
  284. package/src/sandbox/vercel.ts +17 -3
  285. package/src/scoring/collector.ts +3 -3
  286. package/src/scoring/judge.ts +2 -2
  287. package/src/scoring/scoped.ts +13 -1
  288. package/src/scoring/types.ts +7 -7
  289. package/src/scoring/verdict.ts +4 -4
  290. package/src/shared/aggregate.ts +23 -3
  291. package/src/shared/types.ts +2 -2
  292. package/src/shared/{outcome.ts → verdict.ts} +15 -15
  293. package/src/show/compose.ts +12 -132
  294. package/src/show/index.ts +119 -95
  295. package/src/show/render.ts +462 -159
  296. package/src/show/show.test.ts +471 -253
  297. package/src/source-loc.ts +1 -1
  298. package/src/tty-line.ts +32 -0
  299. package/src/types.ts +2 -2
  300. package/src/util.test.ts +21 -1
  301. package/src/util.ts +5 -1
  302. package/src/view/app/App.tsx +33 -198
  303. package/src/view/app/components/AttemptModal.tsx +6 -6
  304. package/src/view/app/components/CodeView.tsx +2 -2
  305. package/src/view/app/components/CopyControls.tsx +32 -36
  306. package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
  307. package/src/view/app/components/ui/badge.tsx +1 -1
  308. package/src/view/app/components/ui/dialog.tsx +2 -2
  309. package/src/view/app/i18n.ts +40 -31
  310. package/src/view/app/lib/artifact-url.ts +1 -1
  311. package/src/view/app/lib/attempt-route.test.ts +37 -47
  312. package/src/view/app/lib/attempt-route.ts +26 -26
  313. package/src/view/app/lib/rows.ts +9 -112
  314. package/src/view/app/lib/verdict.ts +25 -0
  315. package/src/view/app/main.tsx +9 -12
  316. package/src/view/app/pages/RunsPage.tsx +4 -4
  317. package/src/view/app/pages/TracesPage.tsx +3 -3
  318. package/src/view/app/shared.ts +0 -1
  319. package/src/view/app/types.ts +6 -51
  320. package/src/view/artifact-serving.test.ts +136 -0
  321. package/src/view/client-dist/app.css +1 -1
  322. package/src/view/client-dist/app.js +21 -21
  323. package/src/view/data.test.ts +214 -143
  324. package/src/view/data.ts +166 -120
  325. package/src/view/index.ts +23 -11
  326. package/src/view/server.ts +52 -17
  327. package/src/view/shared/types.ts +22 -31
  328. package/src/view/styles.css +117 -327
  329. package/src/view/view-report.test.ts +146 -98
  330. package/src/report/default-report.tsx +0 -222
  331. package/src/report/dual-face.test.tsx +0 -527
  332. package/src/report/react/CaseList.tsx +0 -70
  333. package/src/view/app/components/CostScoreChart.tsx +0 -147
  334. package/src/view/app/components/ExperimentTable.tsx +0 -284
  335. package/src/view/app/components/GroupSelector.tsx +0 -61
  336. package/src/view/app/components/primitives.tsx +0 -43
  337. package/src/view/app/lib/outcome.ts +0 -82
@@ -0,0 +1,214 @@
1
+ // SkillSpec 安装的单测:本地形状(目录 / 单文件 / 不支持)、repo 的 ref 钉定与选择规则、
2
+ // manifest 记录形状、以及「setup 装进 workspace 的东西不算 agent diff」这条护栏。
3
+ // 沙箱是内存 fake(记命令 + 记文件),不起容器:这里要验的是 adapter 侧的翻译规则,
4
+ // 不是 provider 的执行。
5
+
6
+ import { describe, expect, it } from "vitest";
7
+ import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { installSkills, installedSkillNames, skillDiscoveryInstruction } from "./skills.ts";
11
+ import { AGENT_SETUP_MANIFEST_PATH, readAgentSetupManifest, writeAgentSetupManifest } from "./manifest.ts";
12
+ import type { CommandResult, Sandbox, SandboxFile } from "../types.ts";
13
+
14
+ /** 内存沙箱:runShell 记命令(可按前缀脚本化输出),writeFiles/uploadFiles 记文件。 */
15
+ class FakeSandbox implements Partial<Sandbox> {
16
+ readonly workdir = "/workspace";
17
+ readonly sandboxId = "fake";
18
+ readonly otlpHost = null;
19
+ readonly commands: string[] = [];
20
+ readonly files = new Map<string, string>();
21
+ /** 命令 → 输出(按命令算,因为 clone 目录是随机的);没命中就是 exit 0、空输出。 */
22
+ script: { match: string; result: (cmd: string) => Partial<CommandResult> }[] = [];
23
+
24
+ async runShell(script: string): Promise<CommandResult> {
25
+ this.commands.push(script);
26
+ const hit = this.script.find((s) => script.includes(s.match));
27
+ return { stdout: "", stderr: "", exitCode: 0, ...hit?.result(script) };
28
+ }
29
+ async writeFiles(files: Record<string, string>, targetDir?: string): Promise<void> {
30
+ for (const [path, content] of Object.entries(files)) {
31
+ this.files.set(targetDir ? `${targetDir}/${path}` : path, content);
32
+ }
33
+ }
34
+ async uploadFiles(files: SandboxFile[], targetDir?: string): Promise<void> {
35
+ for (const f of files) {
36
+ this.files.set(targetDir ? `${targetDir}/${f.path}` : f.path, f.content.toString());
37
+ }
38
+ }
39
+ async fileExists(): Promise<boolean> {
40
+ return false;
41
+ }
42
+ async readFile(path: string): Promise<string> {
43
+ const hit = this.files.get(path);
44
+ if (hit === undefined) throw new Error(`no such file: ${path}`);
45
+ return hit;
46
+ }
47
+ }
48
+
49
+ const sb = (s?: FakeSandbox["script"]): FakeSandbox => {
50
+ const box = new FakeSandbox();
51
+ if (s) box.script = s;
52
+ return box;
53
+ };
54
+ const asSandbox = (box: FakeSandbox): Sandbox => box as unknown as Sandbox;
55
+
56
+ async function skillProject(): Promise<string> {
57
+ const root = await mkdtemp(join(tmpdir(), "niceeval-skills-"));
58
+ await mkdir(join(root, "skills", "effect-ts"), { recursive: true });
59
+ await writeFile(join(root, "skills", "effect-ts", "SKILL.md"), "# Effect\nuse Effect\n", "utf-8");
60
+ await writeFile(join(root, "skills", "effect-ts", "reference.md"), "layers\n", "utf-8");
61
+ await writeFile(join(root, "skills", "repository-guide.md"), "# Repo guide\n", "utf-8");
62
+ await mkdir(join(root, "skills", "broken"), { recursive: true });
63
+ await writeFile(join(root, "skills", "broken", "notes.md"), "no SKILL.md here\n", "utf-8");
64
+ return root;
65
+ }
66
+
67
+ describe("installSkills · 本地 Skill", () => {
68
+ it("目录形态按原样上传(名字取目录名),单文件形态落成 <name>/SKILL.md(SKILL.md 取所在目录名);manifest 记 sha256", async () => {
69
+ const root = await skillProject();
70
+ try {
71
+ const box = sb();
72
+ const skills = await installSkills(
73
+ asSandbox(box),
74
+ [
75
+ { kind: "local", path: "skills/effect-ts" },
76
+ { kind: "local", path: "skills/effect-ts/SKILL.md" },
77
+ { kind: "local", path: "skills/repository-guide.md", name: "repo-guide" },
78
+ ],
79
+ { dir: ".claude/skills", projectRoot: root },
80
+ );
81
+
82
+ expect(skills.map((s) => s.kind === "local" && s.name)).toEqual(["effect-ts", "effect-ts", "repo-guide"]);
83
+ expect(box.files.get(".claude/skills/effect-ts/SKILL.md")).toContain("use Effect");
84
+ expect(box.files.get(".claude/skills/effect-ts/reference.md")).toBe("layers\n");
85
+ expect(box.files.get(".claude/skills/repo-guide/SKILL.md")).toBe("# Repo guide\n");
86
+ // sha256 是内容哈希:目录(两个文件)与单文件(只有 SKILL.md)不是同一份内容 → 不同哈希
87
+ const hashes = skills.map((s) => (s.kind === "local" ? s.sha256 : ""));
88
+ expect(hashes[0]).toMatch(/^[0-9a-f]{64}$/);
89
+ expect(hashes[0]).not.toBe(hashes[1]);
90
+ // setup 装进 workspace 的目录排除出 agent diff(git 基线早于 agent.setup)
91
+ expect(box.commands.some((c) => c.includes(".git/info/exclude") && c.includes(".claude/skills/"))).toBe(true);
92
+ } finally {
93
+ await rm(root, { recursive: true, force: true });
94
+ }
95
+ });
96
+
97
+ it("路径不存在 / 目录里没有 SKILL.md / 不是 .md 文件:setup 失败并说明原因", async () => {
98
+ const root = await skillProject();
99
+ try {
100
+ await expect(
101
+ installSkills(asSandbox(sb()), [{ kind: "local", path: "skills/nope" }], { dir: ".claude/skills", projectRoot: root }),
102
+ ).rejects.toThrow(/does not exist|不存在/);
103
+
104
+ await expect(
105
+ installSkills(asSandbox(sb()), [{ kind: "local", path: "skills/broken" }], { dir: ".claude/skills", projectRoot: root }),
106
+ ).rejects.toThrow(/SKILL\.md/);
107
+
108
+ await expect(
109
+ installSkills(asSandbox(sb()), [{ kind: "local", path: "skills/broken/notes.txt" }], {
110
+ dir: ".claude/skills",
111
+ projectRoot: root,
112
+ }),
113
+ ).rejects.toThrow(/does not exist|不存在/);
114
+ } finally {
115
+ await rm(root, { recursive: true, force: true });
116
+ }
117
+ });
118
+ });
119
+
120
+ describe("installSkills · repo Skill", () => {
121
+ /** clone 目录是随机的:find 的假输出从命令里把它抠出来再拼 SKILL.md 路径。 */
122
+ const cloneDirOf = (cmd: string): string => /find '([^']+)'/.exec(cmd)![1]!;
123
+ // repo 根就有 SKILL.md → 整个 repo 是一个 skill(名字取 repo 名)
124
+ const oneSkill = [{ match: "find", result: (cmd: string) => ({ stdout: `${cloneDirOf(cmd)}/SKILL.md\n` }) }];
125
+ // 多 skill repo:每个含 SKILL.md 的目录一个
126
+ const manySkills = [
127
+ {
128
+ match: "find",
129
+ result: (cmd: string) => ({
130
+ stdout: `${cloneDirOf(cmd)}/skills/effect/SKILL.md\n${cloneDirOf(cmd)}/skills/effect-sql/SKILL.md\n`,
131
+ }),
132
+ },
133
+ ];
134
+
135
+ it("repo 只有一个 skill、省略 skills:装唯一那个(名字取 repo 名);ref 钉定走完整 clone + checkout", async () => {
136
+ const box = sb(oneSkill);
137
+ const skills = await installSkills(
138
+ asSandbox(box),
139
+ [{ kind: "repo", source: "Effect-TS/skills", ref: "8f3c1a2" }],
140
+ { dir: ".agents/skills" },
141
+ );
142
+
143
+ expect(skills).toEqual([{ kind: "repo", source: "Effect-TS/skills", ref: "8f3c1a2", skills: ["skills"] }]);
144
+ const clone = box.commands.find((c) => c.includes("git clone"))!;
145
+ expect(clone).toContain("https://github.com/Effect-TS/skills.git");
146
+ expect(clone).not.toContain("--depth 1"); // 任意 commit checkout 不到浅克隆里
147
+ expect(clone).toContain("checkout --quiet '8f3c1a2'");
148
+ expect(box.commands.some((c) => c.includes("cp -R") && c.includes(".agents/skills/skills"))).toBe(true);
149
+ });
150
+
151
+ it("省略 ref → 浅克隆;repo 有多个 skill 而没给 skills → 失败并列出可选集", async () => {
152
+ const box = sb(oneSkill);
153
+ await installSkills(asSandbox(box), [{ kind: "repo", source: "Effect-TS/skills" }], { dir: ".agents/skills" });
154
+ expect(box.commands.find((c) => c.includes("git clone"))).toContain("--depth 1");
155
+
156
+ await expect(
157
+ installSkills(asSandbox(sb(manySkills)), [{ kind: "repo", source: "Effect-TS/skills" }], { dir: ".agents/skills" }),
158
+ ).rejects.toThrow(/effect, effect-sql/);
159
+ });
160
+
161
+ it("选中的 skill 不存在 → 失败并报 source / ref / 可选集;选中子集 → 只装选中的", async () => {
162
+ await expect(
163
+ installSkills(
164
+ asSandbox(sb(manySkills)),
165
+ [{ kind: "repo", source: "Effect-TS/skills", ref: "v1", skills: ["effect", "nope"] }],
166
+ { dir: ".agents/skills" },
167
+ ),
168
+ ).rejects.toThrow(/nope/);
169
+
170
+ const box = sb(manySkills);
171
+ const skills = await installSkills(
172
+ asSandbox(box),
173
+ [{ kind: "repo", source: "Effect-TS/skills", skills: ["effect"] }],
174
+ { dir: ".agents/skills" },
175
+ );
176
+ expect(skills).toEqual([{ kind: "repo", source: "Effect-TS/skills", skills: ["effect"] }]);
177
+ const copies = box.commands.filter((c) => c.includes("cp -R"));
178
+ expect(copies).toHaveLength(1);
179
+ expect(copies[0]).toContain(".agents/skills/effect");
180
+ });
181
+ });
182
+
183
+ describe("发现指引 / manifest", () => {
184
+ it("skillDiscoveryInstruction 逐条点名装好的 skill 路径(没有它 codex/bub 不会去读)", () => {
185
+ const text = skillDiscoveryInstruction(".agents/skills", ["effect", "repo-guide"]);
186
+ expect(text).toContain(".agents/skills/effect/SKILL.md");
187
+ expect(text).toContain(".agents/skills/repo-guide/SKILL.md");
188
+ expect(text).toMatch(/read its `SKILL\.md`/);
189
+ });
190
+
191
+ it("installedSkillNames 把 local / repo 两种记录摊平成目录名", () => {
192
+ expect(
193
+ installedSkillNames([
194
+ { kind: "local", name: "repo-guide", path: "skills/repo.md", sha256: "x" },
195
+ { kind: "repo", source: "Effect-TS/skills", skills: ["effect", "effect-sql"] },
196
+ ]),
197
+ ).toEqual(["repo-guide", "effect", "effect-sql"]);
198
+ });
199
+
200
+ it("manifest 写进沙箱固定路径,读回同一份;没写过 → undefined(不生成空 artifact)", async () => {
201
+ const box = sb();
202
+ expect(await readAgentSetupManifest(asSandbox(box))).toBeUndefined();
203
+
204
+ await writeAgentSetupManifest(asSandbox(box), {
205
+ skills: [{ kind: "repo", source: "Effect-TS/skills", ref: "8f3c1a2", skills: ["effect"] }],
206
+ mcpServers: [{ name: "browser", command: "npx", args: ["-y", "@modelcontextprotocol/server-browser"] }],
207
+ });
208
+
209
+ expect(box.files.has(AGENT_SETUP_MANIFEST_PATH)).toBe(true);
210
+ const back = await readAgentSetupManifest(asSandbox(box));
211
+ expect(back?.skills).toEqual([{ kind: "repo", source: "Effect-TS/skills", ref: "8f3c1a2", skills: ["effect"] }]);
212
+ expect(back?.mcpServers?.[0]?.name).toBe("browser");
213
+ });
214
+ });
@@ -0,0 +1,304 @@
1
+ // SkillSpec 的安装实现 —— 三个内置沙箱型 adapter(claude-code / codex / bub)共用的那一半:
2
+ // 「从哪里取得哪份 Skill」(本地目录/文件、repo + ref + 选择集)。另一半——装到哪个目录、
3
+ // 要不要额外写发现指引——留在各 adapter 里,由它们传 `dir` 并自己写 project instruction。
4
+ // 定稿见 docs/feature/adapters/architecture/coding-agent-extensions.md。
5
+ //
6
+ // 为什么不调 `npx skills add`:那个 installer 没有钉 ref 的入口,也没有机器可读的「这个 repo
7
+ // 里有哪些 skill」输出(-l 只打人看的带 ANSI 的清单),而契约要求「来源必须可复现」(钉 ref)
8
+ // 与「多 Skill Repo 必须明确选择启用集合」(选不中要报出可选集)。所以 repo skill 走 git:
9
+ // clone → 枚举 SKILL.md → 按规则选 → 拷进 agent 的 skill 目录。顺带绕开了 memory 里
10
+ // npx-skills-add-headless-hang 那个交互式选择框(无 tty 卡死)的整类问题。
11
+
12
+ import { createHash } from "node:crypto";
13
+ import { readFile, readdir, stat } from "node:fs/promises";
14
+ import { basename, dirname, extname, join, posix, relative, resolve, sep } from "node:path";
15
+ import { shellQuote as q } from "../sandbox/shell.ts";
16
+ import { t } from "../i18n/index.ts";
17
+ import type { AgentSetupSkill, Sandbox, SkillSpec } from "../types.ts";
18
+
19
+ /** 沙箱里放临时 clone 的地方(装完即删;不在 workdir 下,不进 diff)。 */
20
+ const CLONE_ROOT = "/tmp/niceeval-git";
21
+
22
+ export interface InstallSkillsOptions {
23
+ /** 沙箱里的 Skill 安装根目录(相对 workdir):claude-code 用 `.claude/skills`,codex/bub 用 `.agents/skills`。 */
24
+ dir: string;
25
+ /** 本地 Skill 的解析根;省略用跑 niceeval 的项目根(process.cwd())。 */
26
+ projectRoot?: string;
27
+ }
28
+
29
+ /**
30
+ * 按配置顺序把 SkillSpec 装进沙箱,返回 manifest 里的 Skill 记录(与配置顺序一一对应)。
31
+ * 同名 Skill 来自多个来源时后装的覆盖磁盘、manifest 保留每一条(不静默合并)。
32
+ * 任何一条装不上(路径不存在 / 形状不支持 / clone 或 ref 解析失败 / 选择集不合法)直接抛错 ——
33
+ * setup 失败即 attempt errored,不伪装成断言失败。
34
+ */
35
+ export async function installSkills(
36
+ sandbox: Sandbox,
37
+ specs: readonly SkillSpec[],
38
+ opts: InstallSkillsOptions,
39
+ ): Promise<AgentSetupSkill[]> {
40
+ const out: AgentSetupSkill[] = [];
41
+ for (const spec of specs) {
42
+ out.push(
43
+ spec.kind === "local"
44
+ ? await installLocalSkill(sandbox, spec, opts)
45
+ : await installRepoSkill(sandbox, spec, opts),
46
+ );
47
+ }
48
+ if (out.length) await excludeFromDiff(sandbox, [`${opts.dir}/`]);
49
+ return out;
50
+ }
51
+
52
+ // ───────────────────────── 本地 Skill ─────────────────────────
53
+
54
+ async function installLocalSkill(
55
+ sandbox: Sandbox,
56
+ spec: Extract<SkillSpec, { kind: "local" }>,
57
+ opts: InstallSkillsOptions,
58
+ ): Promise<AgentSetupSkill> {
59
+ const root = opts.projectRoot ?? process.cwd();
60
+ const abs = resolve(root, spec.path);
61
+ const info = await stat(abs).catch(() => undefined);
62
+ if (!info) throw new Error(t("skill.localMissing", { path: spec.path, resolved: abs }));
63
+
64
+ if (info.isDirectory()) {
65
+ const files = await readDirFiles(abs);
66
+ if (!files.some((f) => f.path === "SKILL.md")) {
67
+ throw new Error(t("skill.localDirNoSkillFile", { path: spec.path }));
68
+ }
69
+ const name = spec.name ?? basename(abs);
70
+ const sha256 = hashFiles(files);
71
+ await sandbox.uploadFiles(
72
+ files.map((f) => ({ path: f.path, content: f.content })),
73
+ posix.join(opts.dir, name),
74
+ );
75
+ return { kind: "local", name, path: spec.path, sha256 };
76
+ }
77
+
78
+ if (!info.isFile() || extname(abs).toLowerCase() !== ".md") {
79
+ throw new Error(t("skill.localUnsupportedShape", { path: spec.path }));
80
+ }
81
+ const content = await readFile(abs, "utf-8");
82
+ const name = spec.name ?? localFileSkillName(abs);
83
+ await sandbox.writeFiles({ [posix.join(opts.dir, name, "SKILL.md")]: content });
84
+ return { kind: "local", name, path: spec.path, sha256: hashFiles([{ path: "SKILL.md", content }]) };
85
+ }
86
+
87
+ /** `skills/effect-ts/SKILL.md` → `effect-ts`(SKILL.md 的名字不含信息,取所在目录);`skills/guide.md` → `guide`。 */
88
+ export function localFileSkillName(absPath: string): string {
89
+ const file = basename(absPath);
90
+ return file.toLowerCase() === "skill.md" ? basename(dirname(absPath)) : basename(file, extname(file));
91
+ }
92
+
93
+ /** 目录内容的确定性哈希:按相对路径排序,逐条喂 `path\0content\0`(二进制附件按原字节)。 */
94
+ function hashFiles(files: { path: string; content: string | Buffer }[]): string {
95
+ const h = createHash("sha256");
96
+ for (const f of [...files].sort((a, b) => a.path.localeCompare(b.path))) {
97
+ h.update(f.path).update("\0").update(f.content).update("\0");
98
+ }
99
+ return h.digest("hex");
100
+ }
101
+
102
+ /** Skill 目录可以带脚本 / 图片等附件,一律按原字节读 + 上传,不做文本假设。 */
103
+ async function readDirFiles(dir: string): Promise<{ path: string; content: Buffer }[]> {
104
+ const out: { path: string; content: Buffer }[] = [];
105
+ const walk = async (current: string): Promise<void> => {
106
+ for (const entry of await readdir(current, { withFileTypes: true })) {
107
+ const full = join(current, entry.name);
108
+ if (entry.isDirectory()) {
109
+ if (entry.name === ".git") continue;
110
+ await walk(full);
111
+ } else if (entry.isFile()) {
112
+ out.push({ path: relative(dir, full).split(sep).join("/"), content: await readFile(full) });
113
+ }
114
+ }
115
+ };
116
+ await walk(dir);
117
+ return out;
118
+ }
119
+
120
+ // ───────────────────────── Repo Skill ─────────────────────────
121
+
122
+ async function installRepoSkill(
123
+ sandbox: Sandbox,
124
+ spec: Extract<SkillSpec, { kind: "repo" }>,
125
+ opts: InstallSkillsOptions,
126
+ ): Promise<AgentSetupSkill> {
127
+ const cloneDir = await cloneRepo(sandbox, spec.source, spec.ref);
128
+ try {
129
+ const available = await discoverSkills(sandbox, cloneDir, spec.source);
130
+ const selected = selectRepoSkills(available, spec);
131
+ for (const name of selected) {
132
+ const src = available.find((s) => s.name === name)!.dir;
133
+ const dest = posix.join(opts.dir, name);
134
+ const res = await sandbox.runShell(
135
+ `mkdir -p ${q(opts.dir)} && rm -rf ${q(dest)} && cp -R ${q(src)} ${q(dest)}`,
136
+ );
137
+ if (res.exitCode !== 0) {
138
+ throw new Error(t("skill.copyFailed", { name, dest, tail: tail(res.stdout + res.stderr) }));
139
+ }
140
+ }
141
+ return {
142
+ kind: "repo",
143
+ source: spec.source,
144
+ ...(spec.ref !== undefined ? { ref: spec.ref } : {}),
145
+ skills: selected,
146
+ };
147
+ } finally {
148
+ await sandbox.runShell(`rm -rf ${q(cloneDir)}`).catch(() => undefined);
149
+ }
150
+ }
151
+
152
+ /** repo 里可用的一个 Skill:名字 + 沙箱内 SKILL.md 所在目录。 */
153
+ export interface DiscoveredSkill {
154
+ name: string;
155
+ dir: string;
156
+ }
157
+
158
+ /**
159
+ * 枚举 clone 里的 Skill:根 `SKILL.md` 存在 → 整个 repo 就是一个 Skill(名字取 repo 名);
160
+ * 否则每个含 `SKILL.md` 的目录是一个 Skill(名字取该目录名),与 `skills` CLI 的默认口径一致。
161
+ */
162
+ async function discoverSkills(sandbox: Sandbox, cloneDir: string, source: string): Promise<DiscoveredSkill[]> {
163
+ const res = await sandbox.runShell(`find ${q(cloneDir)} -name SKILL.md -not -path '*/.git/*'`);
164
+ const paths = res.stdout.split("\n").map((l) => l.trim()).filter(Boolean).sort();
165
+ if (paths.length === 0) throw new Error(t("skill.repoNoSkills", { source }));
166
+
167
+ const rootFile = `${cloneDir}/SKILL.md`;
168
+ if (paths.includes(rootFile)) return [{ name: repoName(source), dir: cloneDir }];
169
+ return paths
170
+ .map((p) => {
171
+ const dir = posix.dirname(p);
172
+ return { name: posix.basename(dir), dir };
173
+ })
174
+ .sort((a, b) => a.name.localeCompare(b.name)); // 报错里的「可选集」按名字排,不按路径排
175
+ }
176
+
177
+ /**
178
+ * 选择规则(docs 的表):repo 只有一个 Skill 且省略 `skills` → 装唯一那个;多个且省略 →
179
+ * 失败并列出可选集;指定了不存在的 Skill → 失败并报 source / ref / 名字。
180
+ */
181
+ export function selectRepoSkills(
182
+ available: readonly DiscoveredSkill[],
183
+ spec: Extract<SkillSpec, { kind: "repo" }>,
184
+ ): string[] {
185
+ const names = available.map((s) => s.name);
186
+ if (!spec.skills?.length) {
187
+ if (available.length === 1) return [names[0]!];
188
+ throw new Error(t("skill.repoAmbiguous", { source: spec.source, available: names.join(", ") }));
189
+ }
190
+ for (const want of spec.skills) {
191
+ if (!names.includes(want)) {
192
+ throw new Error(
193
+ t("skill.repoUnknownSkill", {
194
+ skill: want,
195
+ source: spec.source,
196
+ ref: spec.ref ?? "(default)",
197
+ available: names.join(", "),
198
+ }),
199
+ );
200
+ }
201
+ }
202
+ return [...spec.skills];
203
+ }
204
+
205
+ // ───────────────────────── git ─────────────────────────
206
+
207
+ /**
208
+ * 把一个 repo clone 进沙箱临时目录,给了 ref 就钉到那个 tag/commit/branch,返回 clone 目录。
209
+ * ref 可能是任意 commit,所以钉 ref 时不能 `--depth 1`(浅克隆 checkout 不到历史 commit)。
210
+ * Skill 与 native plugin 的 marketplace(claude 侧 CLI 没有钉 ref 的入口)共用这一条。
211
+ */
212
+ export async function cloneRepo(sandbox: Sandbox, source: string, ref?: string): Promise<string> {
213
+ const dir = `${CLONE_ROOT}/${slug(source)}-${Math.random().toString(36).slice(2, 8)}`;
214
+ const url = gitUrlOf(source);
215
+ const script =
216
+ `rm -rf ${q(dir)} && mkdir -p ${q(CLONE_ROOT)} && ` +
217
+ `git clone --quiet ${ref ? "" : "--depth 1 "}${q(url)} ${q(dir)}` +
218
+ (ref ? ` && git -C ${q(dir)} checkout --quiet ${q(ref)}` : "");
219
+ const res = await sandbox.runShell(script);
220
+ if (res.exitCode !== 0) {
221
+ throw new Error(
222
+ t("skill.repoCloneFailed", { source, ref: ref ?? "(default)", tail: tail(res.stdout + res.stderr) }),
223
+ );
224
+ }
225
+ return dir;
226
+ }
227
+
228
+ /** `owner/repo` → GitHub HTTPS URL;已经是 URL(含 `://` 或 `git@`)则原样用。 */
229
+ export function gitUrlOf(source: string): string {
230
+ if (source.includes("://") || source.startsWith("git@")) return source;
231
+ return `https://github.com/${source}.git`;
232
+ }
233
+
234
+ function repoName(source: string): string {
235
+ const last = gitUrlOf(source).replace(/\/+$/, "").split("/").pop() ?? source;
236
+ return last.replace(/\.git$/, "");
237
+ }
238
+
239
+ function slug(source: string): string {
240
+ return source.replace(/[^\w.-]/g, "-");
241
+ }
242
+
243
+ function tail(output: string, n = 12): string {
244
+ return output.trim().split("\n").slice(-n).join("\n");
245
+ }
246
+
247
+ // ───────────────────────── 发现指引 ─────────────────────────
248
+
249
+ /** manifest 里的 Skill 记录 → 沙箱里实际的目录名(发现指引要逐条点名)。 */
250
+ export function installedSkillNames(skills: readonly AgentSetupSkill[]): string[] {
251
+ const names: string[] = [];
252
+ for (const skill of skills) {
253
+ if (skill.kind === "local") names.push(skill.name);
254
+ else names.push(...skill.skills);
255
+ }
256
+ return [...new Set(names)];
257
+ }
258
+
259
+ /**
260
+ * 给没有原生 Skill 加载机制的 agent(codex / bub)写的 project instruction 段落:
261
+ * 只把文件装到 skill 目录不足以让它们读到(见 memory/codex-no-native-skill-tool.md ——
262
+ * 不提示的话 codex 连一次 shell 读取都不会发生),必须在 AGENTS.md 里给出稳定的发现指引。
263
+ */
264
+ export function skillDiscoveryInstruction(dir: string, skills: readonly string[]): string {
265
+ const list = skills.map((name) => `- ${name}: ${posix.join(dir, name)}/SKILL.md`).join("\n");
266
+ return [
267
+ "",
268
+ "## Skills",
269
+ "",
270
+ `Skill files are installed in \`${dir}/\`:`,
271
+ "",
272
+ list,
273
+ "",
274
+ "Before answering a task, check whether one of these skills covers it and read its `SKILL.md`",
275
+ "(e.g. with a shell command). Follow the skill's instructions when it applies.",
276
+ "",
277
+ ].join("\n");
278
+ }
279
+
280
+ /**
281
+ * 把发现指引追加进沙箱里的一个 instruction 文件(不存在则创建)。落在 workspace 里时,
282
+ * 新建的文件顺手排除出 diff —— setup 装的东西不是 agent 的产出(见 excludeFromDiff)。
283
+ * 文件本来就存在(starter 自带 AGENTS.md)时不排除:那是 agent 也可能改的文件,
284
+ * 排掉会把真实改动一起藏起来;此时追加的这一段会如实出现在 diff 里。
285
+ */
286
+ export async function appendProjectInstruction(sandbox: Sandbox, text: string, file = "AGENTS.md"): Promise<void> {
287
+ const existed = await sandbox.fileExists(file).catch(() => false);
288
+ const delim = `NICEEVAL_EOF_${Math.random().toString(36).slice(2, 8)}`;
289
+ await sandbox.runShell(`cat >> ${q(file)} <<'${delim}'\n${text}\n${delim}\n`);
290
+ if (!existed) await excludeFromDiff(sandbox, [file]);
291
+ }
292
+
293
+ /**
294
+ * 把 setup 装进 workspace 的东西(skill 目录、adapter 新建的 instruction 文件)写进
295
+ * `.git/info/exclude`:git 基线是在 agent.setup **之前**打的,不排除的话这些文件会被
296
+ * `captureGeneratedFiles`(git add -A && git diff HEAD)当成「agent 生成的文件」记进
297
+ * diff.json —— 装了 skill 的实验,diff 里凭空多出几十个文件,基于 diff 的断言与展示全被污染。
298
+ * 排除只对未跟踪文件有效,这正是 adapter 自己创建的那一类。
299
+ */
300
+ export async function excludeFromDiff(sandbox: Sandbox, paths: readonly string[]): Promise<void> {
301
+ if (!paths.length) return;
302
+ const lines = paths.map((p) => `printf '%s\\n' ${q(p)} >> .git/info/exclude`).join(" && ");
303
+ await sandbox.runShell(`test -d .git && mkdir -p .git/info && ${lines}`).catch(() => undefined);
304
+ }
@@ -63,14 +63,14 @@ export async function driveFrameStream<Frame, RFrame = Frame>(
63
63
  const derived = reducer.add(frame as unknown as RFrame);
64
64
  events.push(...derived);
65
65
 
66
- const outcome = onFrame?.(frame, derived, ctx);
67
- if (outcome && "pause" in outcome) {
68
- events.push({ type: "input.requested", request: outcome.pause });
66
+ const verdict = onFrame?.(frame, derived, ctx);
67
+ if (verdict && "pause" in verdict) {
68
+ events.push({ type: "input.requested", request: verdict.pause });
69
69
  return { status: "waiting", events, usage: reducer.usage };
70
70
  }
71
- if (outcome && "fail" in outcome) {
71
+ if (verdict && "fail" in verdict) {
72
72
  transportFailed = true;
73
- events.push({ type: "error", message: outcome.fail });
73
+ events.push({ type: "error", message: verdict.fail });
74
74
  }
75
75
  }
76
76
 
@@ -7,8 +7,9 @@ import type { StreamEvent, TraceSpan, Usage } from "../o11y/types.ts";
7
7
  import type { Sandbox } from "../sandbox/types.ts";
8
8
 
9
9
  /**
10
- * MCP server 描述符 —— Claude Code Codex 共用的扩展插件单元。
11
- * agent factory config 里声明,setup 阶段写进各自的配置文件。
10
+ * MCP server 描述符 —— 支持 MCP 的 adapter(Claude Code / Codex)共用的工具服务单元,
11
+ * 不是 native plugin 的一种。在 agent factory config 里声明,setup 阶段写进各自的配置文件。
12
+ * 见 docs/feature/adapters/architecture/coding-agent-extensions.md「类型边界」。
12
13
  */
13
14
  export interface McpServer {
14
15
  /** 服务器唯一名(config key)。 */
@@ -21,6 +22,57 @@ export interface McpServer {
21
22
  env?: Record<string, string>;
22
23
  }
23
24
 
25
+ /**
26
+ * Skill 的来源描述 —— Claude Code / Codex / Bub 共用的**数据类型**:只统一「从哪里取得
27
+ * 哪份 Skill」,安装位置、发现机制、要不要额外写 project instruction 由各 Adapter 决定。
28
+ * 见 docs/feature/adapters/architecture/coding-agent-extensions.md「类型边界」。
29
+ */
30
+ export type SkillSpec =
31
+ | {
32
+ kind: "local";
33
+ /** 相对项目根(跑 niceeval 的目录)的 Skill 文件或目录:`SKILL.md`、含 `SKILL.md` 的目录,或单个 `.md`。 */
34
+ path: string;
35
+ /** 展示名;省略时由文件或目录名推导(`<dir>/SKILL.md` → `<dir>`,`foo.md` → `foo`)。 */
36
+ name?: string;
37
+ }
38
+ | {
39
+ kind: "repo";
40
+ /** GitHub `owner/repo` 或 Git URL。 */
41
+ source: string;
42
+ /** 多 Skill Repo 中要启用的 Skill;repo 只有一个 Skill 时可省略,多个时省略即 setup 失败。 */
43
+ skills?: string[];
44
+ /** Tag、Commit 或 Branch;省略表示 repo 默认 ref。 */
45
+ ref?: string;
46
+ };
47
+
48
+ /** Manifest 里的一条 Skill 安装记录:本地(带内容哈希)或 repo(带来源与 ref)。 */
49
+ export type AgentSetupSkill =
50
+ | { kind: "local"; name: string; path: string; sha256: string }
51
+ | { kind: "repo"; source: string; ref?: string; skills: string[] };
52
+
53
+ /**
54
+ * 一次 Agent setup 实际装了什么 —— 沙箱型 Coding Agent Adapter 在 setup 收尾写出的安装清单。
55
+ * 沙箱内落在 `__niceeval__/agent-setup.json`,运行器把它作为 attempt artifact 存成
56
+ * `agent-setup.json`(见 docs/feature/results/architecture.md)。不参与评分,只回答「这次实际
57
+ * 装了什么」;**环境变量值与 secret 不写进来**(所以 mcpServers 只记 name/command/args)。
58
+ */
59
+ export interface AgentSetupManifest {
60
+ /** 装进去的 Skill(按配置顺序;同名来自多个来源时逐条保留,不静默合并)。 */
61
+ skills: AgentSetupSkill[];
62
+ /** Agent 原生 Plugin(Claude Code / Codex 各自的 Marketplace 协议)。 */
63
+ nativePlugins?: Array<{
64
+ agent: "claude-code" | "codex";
65
+ marketplace: { name: string; source: string; ref?: string };
66
+ name: string;
67
+ /** 安装后 CLI 报告的版本;取不到时省略。 */
68
+ resolvedVersion?: string;
69
+ }>;
70
+ /** 挂上的 MCP server(不含 env:secret 不进 manifest)。 */
71
+ mcpServers?: Array<{ name: string; command: string; args?: string[] }>;
72
+ /** Bub 的 Python Plugin(规范化后的 package 串)。 */
73
+ pythonPlugins?: Array<{ package: string }>;
74
+ }
75
+
24
76
  /** 随一轮消息附带的文件(图片等多模态输入)。 */
25
77
  export interface InputFile {
26
78
  /** 文件名(可选,供 adapter / 模型参考)。 */
@@ -169,7 +221,11 @@ export interface AgentContext {
169
221
  * 具体取值)是两个维度——这里只是「跑的是哪个实验」的稳定标识,不携带条件内容。
170
222
  */
171
223
  readonly experimentId?: string;
172
- /** 仅沙箱型 agent 有(运行器按 --sandbox 备好)。 */
224
+ /**
225
+ * 所有 agent 都有:沙箱型是运行器按项目/experiment 配置备好的真实沙箱句柄,remote 型是
226
+ * `createRemoteSandbox()` 产出的 stub(仅含 `workdir`/`sandboxId`/`otlpHost`/`stop` 等
227
+ * 元信息,其余方法调用即抛错)。
228
+ */
173
229
  readonly sandbox: Sandbox;
174
230
  readonly session: AgentSession;
175
231
  /**