niceeval 0.5.3 → 0.6.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 (344) hide show
  1. package/INDEX.md +55 -0
  2. package/dist/agents/types.d.ts +317 -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/execution-tree.d.ts +103 -0
  9. package/dist/o11y/otlp/parse.d.ts +3 -0
  10. package/dist/o11y/otlp/receiver.d.ts +21 -0
  11. package/dist/o11y/otlp/select.d.ts +22 -0
  12. package/dist/o11y/otlp/turn-otel.d.ts +43 -0
  13. package/dist/o11y/types.d.ts +190 -0
  14. package/dist/report/aggregate.d.ts +57 -0
  15. package/dist/report/aggregate.js +212 -0
  16. package/dist/report/built-ins/cost-pass-rate-comparison.d.ts +1 -0
  17. package/dist/report/built-ins/cost-pass-rate-comparison.js +17 -0
  18. package/dist/report/built-ins/index.d.ts +1 -0
  19. package/dist/report/built-ins/index.js +2 -0
  20. package/dist/report/components.d.ts +178 -0
  21. package/dist/report/components.js +132 -0
  22. package/dist/report/compute.d.ts +106 -0
  23. package/dist/report/compute.js +618 -0
  24. package/dist/report/flag.d.ts +6 -0
  25. package/dist/report/flag.js +8 -0
  26. package/dist/report/format.d.ts +32 -0
  27. package/dist/report/format.js +126 -0
  28. package/dist/report/index.d.ts +19 -0
  29. package/dist/report/index.js +30 -0
  30. package/dist/report/load.d.ts +12 -0
  31. package/dist/report/load.js +56 -0
  32. package/dist/report/locale.d.ts +83 -0
  33. package/dist/report/locale.js +160 -0
  34. package/dist/report/metrics.d.ts +21 -0
  35. package/dist/report/metrics.js +115 -0
  36. package/dist/report/primitives.d.ts +64 -0
  37. package/dist/report/primitives.js +113 -0
  38. package/dist/report/react/AttemptList.d.ts +23 -0
  39. package/dist/report/react/AttemptList.js +20 -0
  40. package/dist/report/react/DeltaTable.d.ts +8 -0
  41. package/dist/report/react/DeltaTable.js +24 -0
  42. package/dist/report/react/EvalList.d.ts +10 -0
  43. package/dist/report/react/EvalList.js +0 -0
  44. package/dist/report/react/ExperimentList.d.ts +10 -0
  45. package/dist/report/react/ExperimentList.js +15 -0
  46. package/dist/report/react/GroupSummary.d.ts +8 -0
  47. package/dist/report/react/GroupSummary.js +8 -0
  48. package/dist/report/react/MetricBars.d.ts +10 -0
  49. package/dist/report/react/MetricBars.js +48 -0
  50. package/dist/report/react/MetricLine.d.ts +9 -0
  51. package/dist/report/react/MetricLine.js +68 -0
  52. package/dist/report/react/MetricMatrix.d.ts +10 -0
  53. package/dist/report/react/MetricMatrix.js +23 -0
  54. package/dist/report/react/MetricScatter.d.ts +9 -0
  55. package/dist/report/react/MetricScatter.js +100 -0
  56. package/dist/report/react/MetricTable.d.ts +11 -0
  57. package/dist/report/react/MetricTable.js +26 -0
  58. package/dist/report/react/RunOverview.d.ts +8 -0
  59. package/dist/report/react/RunOverview.js +12 -0
  60. package/dist/report/react/Scoreboard.d.ts +8 -0
  61. package/dist/report/react/Scoreboard.js +26 -0
  62. package/dist/report/react/cell.d.ts +9 -0
  63. package/dist/report/react/cell.js +9 -0
  64. package/dist/report/react/chart-math.d.ts +14 -0
  65. package/dist/report/react/chart-math.js +68 -0
  66. package/dist/report/react/colors.d.ts +14 -0
  67. package/dist/report/react/colors.js +45 -0
  68. package/dist/report/react/fixtures.d.ts +19 -0
  69. package/dist/report/react/fixtures.js +393 -0
  70. package/dist/report/react/format.d.ts +3 -0
  71. package/dist/report/react/format.js +7 -0
  72. package/dist/report/react/index.d.ts +6 -0
  73. package/dist/report/react/index.js +14 -0
  74. package/dist/report/report.d.ts +24 -0
  75. package/dist/report/report.js +49 -0
  76. package/dist/report/text/faces.d.ts +29 -0
  77. package/dist/report/text/faces.js +503 -0
  78. package/dist/report/text/layout.d.ts +75 -0
  79. package/dist/report/text/layout.js +173 -0
  80. package/dist/report/text/plot.d.ts +37 -0
  81. package/dist/report/text/plot.js +81 -0
  82. package/dist/report/text/table.d.ts +8 -0
  83. package/dist/report/text/table.js +86 -0
  84. package/dist/report/tree.d.ts +99 -0
  85. package/dist/report/tree.js +228 -0
  86. package/dist/report/types.d.ts +420 -0
  87. package/dist/report/types.js +4 -0
  88. package/dist/report/web.d.ts +15 -0
  89. package/dist/report/web.js +37 -0
  90. package/dist/results/annotated-source.d.ts +61 -0
  91. package/dist/results/attempt-evidence.d.ts +69 -0
  92. package/dist/results/attempt-source.d.ts +15 -0
  93. package/dist/results/locator.d.ts +75 -0
  94. package/dist/results/locator.js +0 -0
  95. package/dist/results/select.d.ts +45 -0
  96. package/dist/results/select.js +230 -0
  97. package/dist/results/source-hash.d.ts +9 -0
  98. package/dist/results/types.d.ts +204 -0
  99. package/dist/runner/eval-source.d.ts +22 -0
  100. package/dist/runner/fingerprint.d.ts +23 -0
  101. package/dist/runner/types.d.ts +332 -0
  102. package/dist/sandbox/docker-stream.d.ts +24 -0
  103. package/dist/sandbox/docker.d.ts +84 -0
  104. package/dist/sandbox/e2b.d.ts +31 -0
  105. package/dist/sandbox/errors.d.ts +4 -0
  106. package/dist/sandbox/local-files.d.ts +6 -0
  107. package/dist/sandbox/paths.d.ts +4 -0
  108. package/dist/sandbox/registry.d.ts +13 -0
  109. package/dist/sandbox/resolve.d.ts +40 -0
  110. package/dist/sandbox/retry.d.ts +10 -0
  111. package/dist/sandbox/shell.d.ts +11 -0
  112. package/dist/sandbox/source-files.d.ts +13 -0
  113. package/dist/sandbox/types.d.ts +203 -0
  114. package/dist/sandbox/vercel.d.ts +37 -0
  115. package/dist/scoring/types.d.ts +70 -0
  116. package/dist/shared/aggregate.d.ts +29 -0
  117. package/dist/shared/aggregate.js +55 -0
  118. package/dist/shared/types.d.ts +27 -0
  119. package/dist/shared/verdict.d.ts +27 -0
  120. package/dist/shared/verdict.js +34 -0
  121. package/dist/tty-line.d.ts +6 -0
  122. package/dist/types.d.ts +7 -0
  123. package/dist/util.d.ts +22 -0
  124. package/docs-site/zh/concepts/adapter.mdx +9 -4
  125. package/docs-site/zh/concepts/assert.mdx +8 -8
  126. package/docs-site/zh/concepts/evals.mdx +5 -5
  127. package/docs-site/zh/concepts/experiment.mdx +3 -1
  128. package/docs-site/zh/concepts/judge.mdx +9 -9
  129. package/docs-site/zh/concepts/overview.mdx +2 -2
  130. package/docs-site/zh/concepts/tier.mdx +6 -6
  131. package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
  132. package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
  133. package/docs-site/zh/guides/agent-feedback-loop.mdx +235 -78
  134. package/docs-site/zh/guides/connect-otel.mdx +3 -3
  135. package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
  136. package/docs-site/zh/guides/custom-reports.mdx +196 -36
  137. package/docs-site/zh/guides/experiments.mdx +7 -0
  138. package/docs-site/zh/guides/fixtures.mdx +1 -1
  139. package/docs-site/zh/guides/official-adapters.mdx +31 -9
  140. package/docs-site/zh/guides/publish-report.mdx +4 -4
  141. package/docs-site/zh/guides/report-components.mdx +194 -55
  142. package/docs-site/zh/guides/reporters.mdx +2 -2
  143. package/docs-site/zh/guides/results-data.mdx +35 -34
  144. package/docs-site/zh/guides/runner.mdx +2 -2
  145. package/docs-site/zh/guides/sandbox-agent.mdx +10 -5
  146. package/docs-site/zh/guides/sandbox-providers.mdx +102 -0
  147. package/docs-site/zh/guides/viewing-results.mdx +199 -79
  148. package/docs-site/zh/guides/write-experiment.mdx +23 -1
  149. package/docs-site/zh/guides/write-send.mdx +2 -2
  150. package/docs-site/zh/index.mdx +4 -3
  151. package/docs-site/zh/introduction.mdx +2 -1
  152. package/docs-site/zh/quickstart.mdx +5 -3
  153. package/docs-site/zh/reference/builtin-agents.mdx +38 -13
  154. package/docs-site/zh/reference/capabilities.mdx +1 -1
  155. package/docs-site/zh/reference/cli.mdx +36 -13
  156. package/docs-site/zh/reference/define-agent.mdx +24 -12
  157. package/docs-site/zh/reference/define-config.mdx +1 -1
  158. package/docs-site/zh/reference/define-eval.mdx +3 -3
  159. package/docs-site/zh/reference/events.mdx +7 -1
  160. package/docs-site/zh/reference/expect.mdx +3 -3
  161. package/package.json +10 -6
  162. package/src/agents/bub.ts +134 -43
  163. package/src/agents/claude-code.test.ts +152 -0
  164. package/src/agents/claude-code.ts +127 -13
  165. package/src/agents/codex.test.ts +192 -0
  166. package/src/agents/codex.ts +151 -12
  167. package/src/agents/index.ts +11 -3
  168. package/src/agents/manifest.ts +41 -0
  169. package/src/agents/plugin-config.test.ts +94 -0
  170. package/src/agents/shared.ts +33 -2
  171. package/src/agents/skills.test.ts +214 -0
  172. package/src/agents/skills.ts +304 -0
  173. package/src/agents/streaming.ts +5 -5
  174. package/src/agents/types.ts +62 -2
  175. package/src/cli.ts +74 -47
  176. package/src/context/context.test.ts +20 -2
  177. package/src/context/context.ts +3 -0
  178. package/src/context/control-flow.ts +1 -1
  179. package/src/context/session.test.ts +92 -1
  180. package/src/context/session.ts +26 -1
  181. package/src/context/types.ts +4 -4
  182. package/src/define.test.ts +82 -0
  183. package/src/define.ts +72 -11
  184. package/src/expect/index.ts +5 -5
  185. package/src/i18n/en.ts +36 -15
  186. package/src/i18n/zh-CN.ts +39 -21
  187. package/src/index.ts +1 -1
  188. package/src/o11y/execution-tree.test.ts +452 -0
  189. package/src/o11y/execution-tree.ts +367 -0
  190. package/src/o11y/otlp/receiver.ts +1 -1
  191. package/src/o11y/otlp/select.ts +7 -3
  192. package/src/o11y/parsers/claude-code.test.ts +175 -0
  193. package/src/o11y/parsers/claude-code.ts +32 -0
  194. package/src/o11y/types.ts +6 -1
  195. package/src/report/aggregate.ts +26 -20
  196. package/src/report/built-in-user-parity.test.tsx +643 -0
  197. package/src/report/built-ins/cost-pass-rate-comparison.tsx +23 -0
  198. package/src/report/built-ins/index.ts +2 -0
  199. package/src/report/components.tsx +223 -50
  200. package/src/report/compute.ts +358 -53
  201. package/src/report/dual-render.test.tsx +1125 -0
  202. package/src/report/flag.ts +1 -1
  203. package/src/report/format.ts +50 -2
  204. package/src/report/index.ts +48 -14
  205. package/src/report/load.ts +1 -1
  206. package/src/report/locale.ts +206 -0
  207. package/src/report/metrics.ts +39 -15
  208. package/src/report/primitives.tsx +100 -2
  209. package/src/report/react/AttemptList.tsx +115 -0
  210. package/src/report/react/DeltaTable.tsx +9 -6
  211. package/src/report/react/EvalList.tsx +0 -0
  212. package/src/report/react/ExperimentList.tsx +108 -0
  213. package/src/report/react/GroupSummary.tsx +66 -0
  214. package/src/report/react/MetricBars.tsx +24 -13
  215. package/src/report/react/MetricLine.tsx +21 -21
  216. package/src/report/react/MetricMatrix.tsx +8 -4
  217. package/src/report/react/MetricScatter.tsx +136 -96
  218. package/src/report/react/MetricTable.tsx +115 -15
  219. package/src/report/react/RunOverview.tsx +48 -26
  220. package/src/report/react/Scoreboard.tsx +15 -7
  221. package/src/report/react/cell.tsx +18 -9
  222. package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
  223. package/src/report/react/colors.ts +24 -15
  224. package/src/report/react/enhance.js +129 -0
  225. package/src/report/react/fixtures.ts +163 -34
  226. package/src/report/react/format.ts +1 -1
  227. package/src/report/react/index.tsx +22 -6
  228. package/src/report/react/render.test.tsx +164 -31
  229. package/src/report/react/styles.css +405 -213
  230. package/src/report/report.test.ts +403 -94
  231. package/src/report/report.ts +29 -11
  232. package/src/report/text/faces.ts +303 -95
  233. package/src/report/text/layout.ts +76 -11
  234. package/src/report/text/table.ts +98 -0
  235. package/src/report/tree.ts +103 -20
  236. package/src/report/types.ts +196 -25
  237. package/src/report/web.ts +46 -19
  238. package/src/results/annotated-source.test.ts +146 -0
  239. package/src/results/annotated-source.ts +128 -0
  240. package/src/results/attempt-evidence.test.ts +158 -0
  241. package/src/results/attempt-evidence.ts +158 -0
  242. package/src/results/attempt-source.ts +48 -0
  243. package/src/results/copy.ts +154 -139
  244. package/src/results/format.ts +54 -47
  245. package/src/results/host-equivalence.test.ts +527 -0
  246. package/src/results/index.ts +56 -14
  247. package/src/results/locator.test.ts +198 -0
  248. package/src/results/locator.ts +0 -0
  249. package/src/results/open.ts +392 -175
  250. package/src/results/results.test.ts +943 -494
  251. package/src/results/select.ts +129 -23
  252. package/src/results/skipped-notice.ts +0 -0
  253. package/src/results/source-hash.ts +28 -0
  254. package/src/results/types.ts +98 -62
  255. package/src/results/writer.ts +304 -176
  256. package/src/runner/attempt.test.ts +170 -0
  257. package/src/runner/attempt.ts +97 -15
  258. package/src/runner/discover.test.ts +65 -0
  259. package/src/runner/discover.ts +6 -2
  260. package/src/runner/eval-source.test.ts +121 -0
  261. package/src/runner/eval-source.ts +45 -0
  262. package/src/runner/fingerprint.ts +54 -1
  263. package/src/runner/remote-sandbox.ts +1 -1
  264. package/src/runner/report.test.ts +6 -8
  265. package/src/runner/report.ts +5 -4
  266. package/src/runner/reporters/artifacts.ts +27 -41
  267. package/src/runner/reporters/braintrust.test.ts +3 -3
  268. package/src/runner/reporters/braintrust.ts +1 -1
  269. package/src/runner/reporters/console.ts +6 -6
  270. package/src/runner/reporters/json.ts +4 -4
  271. package/src/runner/reporters/live.test.ts +56 -0
  272. package/src/runner/reporters/live.ts +36 -10
  273. package/src/runner/reporters/quiet.test.ts +66 -0
  274. package/src/runner/reporters/quiet.ts +49 -0
  275. package/src/runner/reporters/shared.ts +5 -5
  276. package/src/runner/reporters/table.ts +17 -17
  277. package/src/runner/run.ts +61 -102
  278. package/src/runner/types.ts +43 -32
  279. package/src/sandbox/checkpoint.ts +1 -1
  280. package/src/sandbox/docker.ts +15 -2
  281. package/src/sandbox/e2b.ts +11 -5
  282. package/src/sandbox/errors.ts +13 -0
  283. package/src/sandbox/index.ts +3 -3
  284. package/src/sandbox/local-files.ts +1 -1
  285. package/src/sandbox/registry.ts +6 -5
  286. package/src/sandbox/resolve.ts +38 -23
  287. package/src/sandbox/retry.test.ts +68 -0
  288. package/src/sandbox/retry.ts +44 -0
  289. package/src/sandbox/types.ts +69 -28
  290. package/src/sandbox/vercel.ts +17 -3
  291. package/src/scoring/collector.ts +3 -3
  292. package/src/scoring/judge.ts +2 -2
  293. package/src/scoring/scoped.ts +13 -1
  294. package/src/scoring/types.ts +7 -7
  295. package/src/scoring/verdict.ts +4 -4
  296. package/src/shared/aggregate.ts +23 -3
  297. package/src/shared/types.ts +2 -2
  298. package/src/shared/{outcome.ts → verdict.ts} +15 -15
  299. package/src/show/compose.ts +12 -132
  300. package/src/show/index.ts +125 -94
  301. package/src/show/render.ts +660 -160
  302. package/src/show/show.test.ts +472 -253
  303. package/src/source-loc.ts +1 -1
  304. package/src/tty-line.ts +32 -0
  305. package/src/types.ts +2 -2
  306. package/src/util.test.ts +21 -1
  307. package/src/util.ts +5 -1
  308. package/src/view/app/App.tsx +33 -198
  309. package/src/view/app/components/AttemptModal.tsx +6 -6
  310. package/src/view/app/components/CodeView.tsx +2 -2
  311. package/src/view/app/components/CopyControls.tsx +32 -36
  312. package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
  313. package/src/view/app/components/ui/badge.tsx +1 -1
  314. package/src/view/app/components/ui/dialog.tsx +2 -2
  315. package/src/view/app/i18n.ts +40 -31
  316. package/src/view/app/lib/artifact-url.ts +1 -1
  317. package/src/view/app/lib/attempt-route.test.ts +37 -47
  318. package/src/view/app/lib/attempt-route.ts +26 -26
  319. package/src/view/app/lib/rows.ts +9 -112
  320. package/src/view/app/lib/verdict.ts +25 -0
  321. package/src/view/app/main.tsx +9 -12
  322. package/src/view/app/pages/RunsPage.tsx +4 -4
  323. package/src/view/app/pages/TracesPage.tsx +3 -3
  324. package/src/view/app/shared.ts +0 -1
  325. package/src/view/app/types.ts +6 -51
  326. package/src/view/artifact-serving.test.ts +136 -0
  327. package/src/view/client-dist/app.css +1 -1
  328. package/src/view/client-dist/app.js +21 -21
  329. package/src/view/data.test.ts +214 -143
  330. package/src/view/data.ts +166 -120
  331. package/src/view/index.ts +23 -11
  332. package/src/view/server.ts +52 -17
  333. package/src/view/shared/types.ts +22 -31
  334. package/src/view/styles.css +117 -327
  335. package/src/view/view-report.test.ts +146 -98
  336. package/docs-site/zh/guides/sandbox-backends.mdx +0 -75
  337. package/src/report/default-report.tsx +0 -222
  338. package/src/report/dual-face.test.tsx +0 -527
  339. package/src/report/react/CaseList.tsx +0 -70
  340. package/src/view/app/components/CostScoreChart.tsx +0 -147
  341. package/src/view/app/components/ExperimentTable.tsx +0 -284
  342. package/src/view/app/components/GroupSelector.tsx +0 -61
  343. package/src/view/app/components/primitives.tsx +0 -43
  344. package/src/view/app/lib/outcome.ts +0 -82
@@ -0,0 +1,192 @@
1
+ // Codex native plugin 安装(installPlugins)的单测:单 plugin 的命令构造、同名 marketplace
2
+ // 的去重、ref 钉定走 `--ref`(不像 claude-code 需要先 clone)、resolvedVersion 取不到时
3
+ // 优雅省略(含 `codex plugin list --json` 的真实输出形状 `{ installed: [...], available: [...] }`,
4
+ // 字段名 `pluginId`——实测 codex-cli 0.144.1,2026-07-13 native plugin e2e 复现过按裸数组 /
5
+ // `{ plugins: [...] }` 猜形状的旧版本恒返回 undefined,见
6
+ // memory/native-plugin-marketplace-name-not-caller-assignable.md)、marketplace/plugin 安装
7
+ // 失败的报错。风格与 src/agents/skills.test.ts、src/agents/claude-code.test.ts 一致,不另起一套。
8
+ // 定稿见 docs/feature/adapters/architecture/coding-agent-extensions.md。
9
+
10
+ import { describe, expect, it } from "vitest";
11
+ import { installPlugins, type CodexPluginSpec } from "./codex.ts";
12
+ import type { CommandResult, Sandbox, SandboxFile } from "../types.ts";
13
+
14
+ /** 内存沙箱:runShell 记命令(可按命令包含的子串打脚本化输出)。 */
15
+ class FakeSandbox implements Partial<Sandbox> {
16
+ readonly workdir = "/workspace";
17
+ readonly sandboxId = "fake";
18
+ readonly otlpHost = null;
19
+ readonly commands: string[] = [];
20
+ script: { match: string; result: (cmd: string) => Partial<CommandResult> }[] = [];
21
+
22
+ async runShell(script: string): Promise<CommandResult> {
23
+ this.commands.push(script);
24
+ const hit = this.script.find((s) => script.includes(s.match));
25
+ return { stdout: "", stderr: "", exitCode: 0, ...hit?.result(script) };
26
+ }
27
+ async writeFiles(): Promise<void> {}
28
+ async uploadFiles(_files: SandboxFile[]): Promise<void> {}
29
+ async fileExists(): Promise<boolean> {
30
+ return false;
31
+ }
32
+ async readFile(): Promise<string> {
33
+ throw new Error("not used in this test");
34
+ }
35
+ }
36
+
37
+ const sb = (s?: FakeSandbox["script"]): FakeSandbox => {
38
+ const box = new FakeSandbox();
39
+ if (s) box.script = s;
40
+ return box;
41
+ };
42
+ const asSandbox = (box: FakeSandbox): Sandbox => box as unknown as Sandbox;
43
+
44
+ describe("codex installPlugins · 命令构造", () => {
45
+ it("单 plugin:先连 marketplace(不带 --ref)再用 `plugin add` 装,manifest 记 marketplace/name", async () => {
46
+ const box = sb();
47
+ const plugins: CodexPluginSpec[] = [
48
+ { marketplace: { name: "acme", source: "acme/codex-plugins" }, name: "repo-map" },
49
+ ];
50
+ const out = await installPlugins(asSandbox(box), plugins);
51
+
52
+ expect(box.commands).toEqual([
53
+ "codex plugin marketplace add 'acme/codex-plugins'",
54
+ "codex plugin add 'repo-map@acme'",
55
+ "codex plugin list --json --marketplace 'acme'",
56
+ ]);
57
+ expect(out).toEqual([
58
+ { agent: "codex", marketplace: { name: "acme", source: "acme/codex-plugins" }, name: "repo-map" },
59
+ ]);
60
+ });
61
+
62
+ it("同名 marketplace 只连一次:两个 plugin 共用一个 marketplace.name → 只有一条 marketplace add,两条 plugin add", async () => {
63
+ const box = sb();
64
+ const plugins: CodexPluginSpec[] = [
65
+ { marketplace: { name: "acme", source: "acme/codex-plugins" }, name: "repo-map" },
66
+ { marketplace: { name: "acme", source: "acme/codex-plugins" }, name: "safe-shell" },
67
+ ];
68
+ const out = await installPlugins(asSandbox(box), plugins);
69
+
70
+ const marketplaceAdds = box.commands.filter((c) => c.startsWith("codex plugin marketplace add"));
71
+ expect(marketplaceAdds).toHaveLength(1);
72
+ const adds = box.commands.filter((c) => c.startsWith("codex plugin add"));
73
+ expect(adds).toEqual(["codex plugin add 'repo-map@acme'", "codex plugin add 'safe-shell@acme'"]);
74
+ expect(out.map((p) => p.name)).toEqual(["repo-map", "safe-shell"]);
75
+ expect(out.every((p) => p.marketplace.name === "acme")).toBe(true);
76
+ });
77
+
78
+ it("ref 钉定:直接走 `marketplace add --ref`,不像 claude-code 需要先 clone;manifest 保留 ref", async () => {
79
+ const box = sb();
80
+ const plugins: CodexPluginSpec[] = [
81
+ { marketplace: { name: "acme", source: "acme/codex-plugins", ref: "8f3c1a2" }, name: "repo-map" },
82
+ ];
83
+ const out = await installPlugins(asSandbox(box), plugins);
84
+
85
+ expect(box.commands.some((c) => c.includes("git clone"))).toBe(false);
86
+ const add = box.commands.find((c) => c.startsWith("codex plugin marketplace add"))!;
87
+ expect(add).toBe("codex plugin marketplace add 'acme/codex-plugins' --ref '8f3c1a2'");
88
+
89
+ expect(out).toEqual([
90
+ {
91
+ agent: "codex",
92
+ marketplace: { name: "acme", source: "acme/codex-plugins", ref: "8f3c1a2" },
93
+ name: "repo-map",
94
+ },
95
+ ]);
96
+ });
97
+
98
+ it("resolvedVersion:list 输出真实 `{ installed: [...] }` 形状、按 pluginId 命中 → manifest 记版本(实测 codex-cli 0.144.1)", async () => {
99
+ const box = sb([
100
+ {
101
+ match: "codex plugin list --json",
102
+ result: () => ({
103
+ stdout: JSON.stringify({
104
+ installed: [{ pluginId: "repo-map@acme", name: "repo-map", version: "2.0.0" }],
105
+ available: [],
106
+ }),
107
+ }),
108
+ },
109
+ ]);
110
+ const out = await installPlugins(asSandbox(box), [
111
+ { marketplace: { name: "acme", source: "acme/codex-plugins" }, name: "repo-map" },
112
+ ]);
113
+ expect(out[0]?.resolvedVersion).toBe("2.0.0");
114
+ });
115
+
116
+ it("resolvedVersion:`installed` 条目缺 pluginId、只有 name 时按 name 命中 → manifest 记版本", async () => {
117
+ const box = sb([
118
+ {
119
+ match: "codex plugin list --json",
120
+ result: () => ({ stdout: JSON.stringify({ installed: [{ name: "repo-map", version: "2.1.0" }] }) }),
121
+ },
122
+ ]);
123
+ const out = await installPlugins(asSandbox(box), [
124
+ { marketplace: { name: "acme", source: "acme/codex-plugins" }, name: "repo-map" },
125
+ ]);
126
+ expect(out[0]?.resolvedVersion).toBe("2.1.0");
127
+ });
128
+
129
+ it("resolvedVersion:list 输出裸数组(非当前实测形状,但保留兼容)、按 id 命中 → manifest 记版本", async () => {
130
+ const box = sb([
131
+ {
132
+ match: "codex plugin list --json",
133
+ result: () => ({ stdout: JSON.stringify([{ id: "repo-map@acme", version: "2.0.0" }]) }),
134
+ },
135
+ ]);
136
+ const out = await installPlugins(asSandbox(box), [
137
+ { marketplace: { name: "acme", source: "acme/codex-plugins" }, name: "repo-map" },
138
+ ]);
139
+ expect(out[0]?.resolvedVersion).toBe("2.0.0");
140
+ });
141
+
142
+ it("resolvedVersion 取不到时优雅省略:list 输出旧的 `{ plugins: [...] }` 猜测形状(实测证伪,不是真实 CLI 输出)不再命中", async () => {
143
+ const box = sb([
144
+ {
145
+ match: "codex plugin list --json",
146
+ result: () => ({ stdout: JSON.stringify({ plugins: [{ name: "repo-map", version: "2.1.0" }] }) }),
147
+ },
148
+ ]);
149
+ const out = await installPlugins(asSandbox(box), [
150
+ { marketplace: { name: "acme", source: "acme/codex-plugins" }, name: "repo-map" },
151
+ ]);
152
+ expect(out[0]).not.toHaveProperty("resolvedVersion");
153
+ });
154
+
155
+ it("resolvedVersion 取不到时优雅省略(不阻断安装):list 命令失败 → manifest 里没有 resolvedVersion 键", async () => {
156
+ const box = sb([{ match: "codex plugin list --json", result: () => ({ exitCode: 1 }) }]);
157
+ const out = await installPlugins(asSandbox(box), [
158
+ { marketplace: { name: "acme", source: "acme/codex-plugins" }, name: "repo-map" },
159
+ ]);
160
+ expect(out).toHaveLength(1);
161
+ expect(out[0]).not.toHaveProperty("resolvedVersion");
162
+ });
163
+
164
+ it("resolvedVersion 取不到时优雅省略:list 输出不是合法 JSON(如空 stdout)同样不阻断安装", async () => {
165
+ const box = sb(); // 默认 stdout 为空字符串,JSON.parse("") 抛错,installedVersion 内部吞掉
166
+ const out = await installPlugins(asSandbox(box), [
167
+ { marketplace: { name: "acme", source: "acme/codex-plugins" }, name: "repo-map" },
168
+ ]);
169
+ expect(out[0]).not.toHaveProperty("resolvedVersion");
170
+ });
171
+ });
172
+
173
+ describe("codex installPlugins · 失败语义", () => {
174
+ it("marketplace 连接失败:抛错并点名 marketplace 名与来源,不继续装 plugin", async () => {
175
+ const box = sb([{ match: "codex plugin marketplace add", result: () => ({ exitCode: 1, stderr: "boom" }) }]);
176
+ await expect(
177
+ installPlugins(asSandbox(box), [
178
+ { marketplace: { name: "acme", source: "acme/codex-plugins" }, name: "repo-map" },
179
+ ]),
180
+ ).rejects.toThrow(/acme/);
181
+ expect(box.commands.some((c) => c.startsWith("codex plugin add"))).toBe(false);
182
+ });
183
+
184
+ it("plugin 安装失败:抛错并点名 plugin 名", async () => {
185
+ const box = sb([{ match: "codex plugin add", result: () => ({ exitCode: 1, stderr: "boom" }) }]);
186
+ await expect(
187
+ installPlugins(asSandbox(box), [
188
+ { marketplace: { name: "acme", source: "acme/codex-plugins" }, name: "repo-map" },
189
+ ]),
190
+ ).rejects.toThrow(/repo-map/);
191
+ });
192
+ });
@@ -1,8 +1,16 @@
1
1
  import { defineSandboxAgent } from "../define.ts";
2
2
  import { requireEnv, getEnv } from "../util.ts";
3
3
  import { shared } from "./shared.ts";
4
+ import {
5
+ appendProjectInstruction,
6
+ installSkills,
7
+ installedSkillNames,
8
+ skillDiscoveryInstruction,
9
+ } from "./skills.ts";
10
+ import { writeAgentSetupManifest } from "./manifest.ts";
4
11
  import { mapCodexSpans } from "../o11y/otlp/mappers/codex.ts";
5
- import type { Agent, McpServer } from "../types.ts";
12
+ import { t } from "../i18n/index.ts";
13
+ import type { Agent, AgentSetupManifest, McpServer, Sandbox, SkillSpec } from "../types.ts";
6
14
 
7
15
  // ───────────────────────────────────────────────────────────────────────────
8
16
  // OpenAI Codex CLI 的 agent adapter(沙箱型)。
@@ -10,8 +18,30 @@ import type { Agent, McpServer } from "../types.ts";
10
18
  // 连接方式:在沙箱里 spawn `codex exec --json`,stdout JSONL → parseCodex → 标准事件流。
11
19
  // 配置:鉴权本地(config / env),模型交给实验(ctx.model),推理努力程度经 ctx.reasoningEffort
12
20
  // (兼容旧的 ctx.flags.effort),其余参数经 ctx.flags。
21
+ // 扩展(skill / plugin / MCP)是构造参数,setup 翻译成 codex 的原生形态并写 manifest。
13
22
  // ───────────────────────────────────────────────────────────────────────────
14
23
 
24
+ /** codex 的 skill 目录(`skills` 生态的「通用」目录);codex 不原生扫描它,靠下面的发现指引。 */
25
+ const SKILL_DIR = ".agents/skills";
26
+
27
+ /**
28
+ * Codex 的原生 Plugin —— **只属于 Codex**,不能传给 Claude Code(它有自己的
29
+ * {@link import("./claude-code.ts").ClaudeCodePluginSpec})。字段当前相似不等于同一个类型:
30
+ * 任一方的 Marketplace 鉴权、锁文件、选择规则或安装参数变化时,另一方不必接受无意义字段。
31
+ */
32
+ export interface CodexPluginSpec {
33
+ marketplace: {
34
+ /** Marketplace 在 Codex 配置中的连接名(`codex plugin add <plugin>@<name>` 里的那个名字)。 */
35
+ name: string;
36
+ /** Marketplace 来源:`owner/repo`、Git URL 或本地路径。 */
37
+ source: string;
38
+ /** 固定 Marketplace 的 Tag、Commit 或 Branch(→ `codex plugin marketplace add --ref`)。 */
39
+ ref?: string;
40
+ };
41
+ /** Marketplace 中的 Plugin 名。 */
42
+ name: string;
43
+ }
44
+
15
45
  export interface CodexConfig {
16
46
  /** 代理 / OpenAI API key。省略时读 CODEX_API_KEY env。 */
17
47
  apiKey?: string;
@@ -19,14 +49,17 @@ export interface CodexConfig {
19
49
  baseUrl?: string;
20
50
  /**
21
51
  * 额外 MCP server(每个沙箱 setup 时追加进 ~/.codex/config.toml)。
22
- * 格式对应 codex config.toml 的 [mcp_server.<name>] 表。
52
+ * 格式对应 codex config.toml 的 [mcp_servers.<name>] 表。
23
53
  */
24
54
  mcpServers?: McpServer[];
25
55
  /**
26
- * 额外安装的 skill,格式为 GitHub `"org/repo"`(如 `"Effect-TS/skills"`)。
27
- * setup 阶段执行 `npx skills add <org/repo>`,结果写进 skills-lock.json。
56
+ * 装进沙箱的 Skill(本地目录/文件,或 repo + 可钉 ref + 可选启用集)。
57
+ * 落在 `.agents/skills/<name>/`,并写一段发现指引进 AGENTS.md —— codex 没有 Claude Code 那种
58
+ * 原生 Skill 工具,只把文件装进去它不会自己去读(见 memory/codex-no-native-skill-tool.md)。
28
59
  */
29
- skills?: string[];
60
+ skills?: SkillSpec[];
61
+ /** Codex 原生 Plugin(先连 Marketplace,再从中装指定 Plugin)。 */
62
+ plugins?: CodexPluginSpec[];
30
63
  }
31
64
 
32
65
  export function codexAgent(config?: CodexConfig): Agent {
@@ -81,14 +114,28 @@ export function codexAgent(config?: CodexConfig): Agent {
81
114
  await sb.runShell(`cat >> ~/.codex/config.toml <<'MCPEOF'\n\n${mcpToml}\nMCPEOF\n`);
82
115
  }
83
116
 
117
+ const manifest: AgentSetupManifest = { skills: [] };
84
118
  if (config?.skills?.length) {
85
- for (const source of config.skills) {
86
- // 同 claude-code adapter:-y -a codex 避免无 tty 环境下卡在交互式 agent 选择框。
87
- // codex 没有 claude-code 那种原生 Skill 工具,装进的是 skills 包的"通用"目录
88
- // (`.agents/skills/<name>`)——codex CLI 本身不会主动去读它,要在 prompt 里
89
- // 显式提示"检查仓库里有没有 skill/guide 文件",agent 才会用 shell 命令读进上下文。
90
- await sb.runShell(`npx skills add ${shared.shellQuote(source)} -y -a codex`);
91
- }
119
+ manifest.skills = await installSkills(sb, config.skills, { dir: SKILL_DIR });
120
+ // 发现指引不是可选装饰:没有它,codex 连一次读 skill 文件的 shell 调用都不会发生。
121
+ await appendProjectInstruction(
122
+ sb,
123
+ skillDiscoveryInstruction(SKILL_DIR, installedSkillNames(manifest.skills)),
124
+ );
125
+ }
126
+ if (config?.plugins?.length) {
127
+ manifest.nativePlugins = await installPlugins(sb, config.plugins);
128
+ }
129
+ if (config?.mcpServers?.length) {
130
+ // manifest 只记「挂了哪个 server、怎么起」;env 里可能有 token,不落盘。
131
+ manifest.mcpServers = config.mcpServers.map((s) => ({
132
+ name: s.name,
133
+ command: s.command,
134
+ ...(s.args?.length ? { args: [...s.args] } : {}),
135
+ }));
136
+ }
137
+ if (manifest.skills.length || manifest.nativePlugins?.length || manifest.mcpServers?.length) {
138
+ await writeAgentSetupManifest(sb, manifest);
92
139
  }
93
140
  },
94
141
 
@@ -129,4 +176,96 @@ export function codexAgent(config?: CodexConfig): Agent {
129
176
  });
130
177
  }
131
178
 
179
+ /**
180
+ * 先按 `marketplace.name` 连 Marketplace(同名只连一次,`--ref` 钉版本),再装指定 Plugin。
181
+ * 只按 codex 自己的 marketplace / plugin 协议走 —— 与 claude-code 的实现不共用命令、不共用类型。
182
+ */
183
+ export async function installPlugins(
184
+ sb: Sandbox,
185
+ plugins: readonly CodexPluginSpec[],
186
+ ): Promise<NonNullable<AgentSetupManifest["nativePlugins"]>> {
187
+ const connected = new Set<string>();
188
+ const out: NonNullable<AgentSetupManifest["nativePlugins"]> = [];
189
+
190
+ for (const plugin of plugins) {
191
+ const { marketplace } = plugin;
192
+ if (!connected.has(marketplace.name)) {
193
+ const refFlag = marketplace.ref ? ` --ref ${shared.shellQuote(marketplace.ref)}` : "";
194
+ const add = await sb.runShell(
195
+ `codex plugin marketplace add ${shared.shellQuote(marketplace.source)}${refFlag}`,
196
+ );
197
+ if (add.exitCode !== 0) {
198
+ throw new Error(
199
+ t("plugin.marketplaceFailed", {
200
+ agent: "codex",
201
+ name: marketplace.name,
202
+ source: marketplace.source,
203
+ ref: marketplace.ref ?? "(default)",
204
+ tail: outputTail(add),
205
+ }),
206
+ );
207
+ }
208
+ connected.add(marketplace.name);
209
+ }
210
+
211
+ const install = await sb.runShell(
212
+ `codex plugin add ${shared.shellQuote(`${plugin.name}@${marketplace.name}`)}`,
213
+ );
214
+ if (install.exitCode !== 0) {
215
+ throw new Error(
216
+ t("plugin.installFailed", {
217
+ agent: "codex",
218
+ name: plugin.name,
219
+ marketplace: marketplace.name,
220
+ tail: outputTail(install),
221
+ }),
222
+ );
223
+ }
224
+
225
+ const resolvedVersion = await installedVersion(sb, plugin.name, marketplace.name);
226
+ out.push({
227
+ agent: "codex",
228
+ marketplace: {
229
+ name: marketplace.name,
230
+ source: marketplace.source,
231
+ ...(marketplace.ref !== undefined ? { ref: marketplace.ref } : {}),
232
+ },
233
+ name: plugin.name,
234
+ ...(resolvedVersion !== undefined ? { resolvedVersion } : {}),
235
+ });
236
+ }
237
+ return out;
238
+ }
239
+
240
+ /**
241
+ * `codex plugin list --json` 的版本回读;取不到不阻断安装(manifest 里 resolvedVersion 省略)。
242
+ * 真实输出(实测 codex-cli 0.144.1)是 `{ installed: [...], available: [...] }`,已安装的这条在
243
+ * `installed` 数组里,字段名是 `pluginId`(不是 `id`)——早前按裸数组 / `{ plugins: [...] }` 猜的
244
+ * 形状全部猜错,`installedVersion` 曾对任何真实安装恒返回 undefined(见
245
+ * memory/native-plugin-marketplace-name-not-caller-assignable.md 的姊妹发现,2026-07-13 e2e 复现)。
246
+ */
247
+ async function installedVersion(sb: Sandbox, name: string, marketplace: string): Promise<string | undefined> {
248
+ try {
249
+ const res = await sb.runShell(`codex plugin list --json --marketplace ${shared.shellQuote(marketplace)}`);
250
+ if (res.exitCode !== 0) return undefined;
251
+ const raw = JSON.parse(res.stdout) as unknown;
252
+ const list = (
253
+ Array.isArray(raw) ? raw : ((raw as { installed?: unknown[] })?.installed ?? [])
254
+ ) as {
255
+ pluginId?: string;
256
+ id?: string;
257
+ name?: string;
258
+ version?: string;
259
+ }[];
260
+ const hit = list.find((p) => p.pluginId === `${name}@${marketplace}` || p.id === `${name}@${marketplace}` || p.name === name);
261
+ return typeof hit?.version === "string" ? hit.version : undefined;
262
+ } catch {
263
+ return undefined;
264
+ }
265
+ }
266
+
267
+ function outputTail(res: { stdout: string; stderr: string }, n = 12): string {
268
+ return (res.stdout + res.stderr).trim().split("\n").slice(-n).join("\n");
269
+ }
270
+
132
271
  export default codexAgent();
@@ -71,18 +71,26 @@ export { BUILTIN_AGENTS } from "./builtin.ts";
71
71
  export { claudeCodeAgent } from "./claude-code.ts";
72
72
  export { codexAgent } from "./codex.ts";
73
73
  export { bubAgent } from "./bub.ts";
74
- export type { ClaudeCodeConfig } from "./claude-code.ts";
75
- export type { CodexConfig } from "./codex.ts";
76
- export type { BubConfig } from "./bub.ts";
74
+ export type { ClaudeCodeConfig, ClaudeCodePluginSpec } from "./claude-code.ts";
75
+ export type { CodexConfig, CodexPluginSpec } from "./codex.ts";
76
+ export type { BubConfig, PythonPluginSpec } from "./bub.ts";
77
+
78
+ // 安装 manifest 的落点:adapter 写(shared.writeAgentSetup),运行器读并抬成 attempt artifact。
79
+ export { AGENT_SETUP_MANIFEST_PATH } from "./manifest.ts";
77
80
 
78
81
  export type {
79
82
  Agent,
80
83
  AgentContext,
81
84
  AgentSession,
85
+ AgentSetup,
86
+ AgentSetupManifest,
87
+ AgentSetupSkill,
88
+ AgentTeardown,
82
89
  AgentTracing,
83
90
  SpanMapper,
84
91
  Telemetry,
85
92
  SandboxAgentDef,
86
93
  RemoteAgentDef,
87
94
  McpServer,
95
+ SkillSpec,
88
96
  } from "../types.ts";
@@ -0,0 +1,41 @@
1
+ // 安装 Manifest 的落点与读写(定稿见 docs/feature/adapters/architecture/coding-agent-extensions.md
2
+ // 「安装 Manifest」、docs/feature/results/architecture.md「agent-setup.json」)。
3
+ //
4
+ // 分工:**adapter 写**(setup 收尾,知道自己装了什么)、**运行器读**(把它抬成 attempt artifact)。
5
+ // 中间那层只是一个双方都认的沙箱路径 —— core 不解释 manifest 内容,也不按 agent 名字分支;
6
+ // 自定义沙箱 adapter 用同一个 `shared.writeAgentSetup()` 就能让自己的安装结果进落盘。
7
+
8
+ import type { AgentSetupManifest, Sandbox } from "../types.ts";
9
+
10
+ /**
11
+ * 沙箱内的 manifest 路径(相对 workdir)。`__niceeval__/` 已在 git 基线的 .gitignore 里
12
+ * (见 runner/sandbox-prep.ts),所以写它不会污染 agent 产出的 diff。
13
+ */
14
+ export const AGENT_SETUP_MANIFEST_PATH = "__niceeval__/agent-setup.json";
15
+
16
+ /** adapter 在 setup 收尾调:把这次实际安装的东西写进沙箱 manifest。 */
17
+ export async function writeAgentSetupManifest(sandbox: Sandbox, manifest: AgentSetupManifest): Promise<void> {
18
+ await sandbox.writeFiles({ [AGENT_SETUP_MANIFEST_PATH]: JSON.stringify(manifest, null, 2) });
19
+ }
20
+
21
+ /**
22
+ * 运行器在 agent.setup 之后调:沙箱里没有这个文件(adapter 什么都没装,或不是 coding agent)
23
+ * 就是 undefined —— 不生成空 artifact,与「某类数据为空就不落文件」的落盘规则一致。
24
+ * 文件在但不是合法 JSON 视为「manifest 无法完整反映安装结果」,按 setup 失败上抛。
25
+ */
26
+ export async function readAgentSetupManifest(sandbox: Sandbox): Promise<AgentSetupManifest | undefined> {
27
+ let raw: string;
28
+ try {
29
+ raw = await sandbox.readFile(AGENT_SETUP_MANIFEST_PATH);
30
+ } catch {
31
+ return undefined;
32
+ }
33
+ try {
34
+ return JSON.parse(raw) as AgentSetupManifest;
35
+ } catch {
36
+ throw new Error(
37
+ `Agent setup manifest ${AGENT_SETUP_MANIFEST_PATH} is not valid JSON. The adapter wrote a broken install manifest; ` +
38
+ "it must faithfully report what was installed (write it with shared.writeAgentSetup()).",
39
+ );
40
+ }
41
+ }
@@ -0,0 +1,94 @@
1
+ // 编译期类型测试:证明 Claude Code / Codex / Bub 的 Config 只能收各自合法的扩展组合——
2
+ // 不支持的能力在类型层就不存在,不留到运行期 fail fast(定稿见
3
+ // docs/feature/adapters/architecture/coding-agent-extensions.md「类型边界」)。
4
+ //
5
+ // 断言机制:每个 `@ts-expect-error` 断言「紧跟的这一行编译不过」。这些断言全部装在从不在
6
+ // 运行时调用的函数体里——`tsc --noEmit`(`pnpm run typecheck`)本就会类型检查未执行到的函数
7
+ // 体,不需要真的跑起来。断言是自证的:如果这一行其实编译得过,tsc 会反过来报
8
+ // "Unused '@ts-expect-error' directive",本文件本身就不再通过 typecheck,不会有「断言写错、
9
+ // 静默失效」的中间状态。风格参照 src/report/dual-render.test.tsx 的
10
+ // metricScatterPropsTypeChecks:整段包一个从不调用的函数,文件末尾 `void` 掉避免「未使用」噪音。
11
+ //
12
+ // 覆盖范围以真实类型定义为准(先读了 claude-code.ts / codex.ts / bub.ts 的 Config 接口,
13
+ // 不是凭空假设字段名):
14
+ // - BubConfig 没有 mcpServers / plugins 字段——MCP 与 native plugin 只属于 Claude Code / Codex。
15
+ // - ClaudeCodeConfig / CodexConfig 没有 pythonPlugins 字段——那是 Bub 专属。
16
+ // - PythonPluginSpec(`{ package }`)与 ClaudeCodePluginSpec / CodexPluginSpec
17
+ // (`{ marketplace, name }`)形状不同:塞错位置(PythonPluginSpec 塞进 `plugins`,或反过来把
18
+ // plugin spec 塞进 `pythonPlugins`)编译不过。
19
+ //
20
+ // **刻意不覆盖**:ClaudeCodePluginSpec ↔ CodexPluginSpec 互相塞进对方的 `plugins` 字段。
21
+ // 两者当前字段形状完全相同(`{ marketplace: { name, source, ref? }, name }`),结构类型系统
22
+ // 判不出「这是哪家的 Marketplace」——这是文档已裁决的非目标,见
23
+ // coding-agent-skills-plugins.md 设计规则 5 第二段:「类型系统不负责拦住『把只有 Codex 能读的
24
+ // Marketplace 递给 Claude Code』:那是 source 字段的值不合法,不是形状不合法,任何结构类型
25
+ // 系统都判不出来」。实测(见本文件同目录下 claude-code.test.ts / codex.test.ts 的开发过程)
26
+ // 直接给这个组合写 `@ts-expect-error` 会被 tsc 判成 "Unused directive"(因为它其实编译得过),
27
+ // 写这条断言本身就会让 `pnpm run typecheck` 失败——所以这里不写这一条,归属改由 Adapter 在
28
+ // 运行期按 `source` 值报错(见 claude-code.ts / codex.ts 的 installPlugins 失败语义)。
29
+
30
+ import { describe, expect, it } from "vitest";
31
+ import { claudeCodeAgent, type ClaudeCodeConfig } from "./claude-code.ts";
32
+ import { codexAgent, type CodexConfig } from "./codex.ts";
33
+ import { bubAgent, type BubConfig, type PythonPluginSpec } from "./bub.ts";
34
+ import type { McpServer } from "../types.ts";
35
+
36
+ /** BubConfig 没有 mcpServers / plugins 字段:两者只属于 Claude Code / Codex。 */
37
+ function bubConfigRejectsForeignExtensions(mcp: McpServer, python: PythonPluginSpec): void {
38
+ // 合法组合先落一遍,证明下面的报错真是因为多出的字段,不是因为 pythonPlugins 本身就不合法。
39
+ const legal: BubConfig = { pythonPlugins: [python] };
40
+ void legal;
41
+
42
+ // @ts-expect-error BubConfig 没有 mcpServers 字段——MCP 只属于 Claude Code / Codex。
43
+ bubAgent({ mcpServers: [mcp] });
44
+
45
+ // @ts-expect-error BubConfig 没有 plugins 字段——native plugin 只属于 Claude Code / Codex。
46
+ bubAgent({ plugins: [{ marketplace: { name: "acme", source: "x" }, name: "y" }] });
47
+ }
48
+
49
+ /** ClaudeCodeConfig / CodexConfig 没有 pythonPlugins 字段:那是 Bub 专属。 */
50
+ function codingAgentConfigsRejectPythonPlugins(python: PythonPluginSpec): void {
51
+ // @ts-expect-error CodexConfig 没有 pythonPlugins 字段——那是 Bub 专属(PythonPluginSpec)。
52
+ codexAgent({ pythonPlugins: [python] });
53
+
54
+ // @ts-expect-error ClaudeCodeConfig 没有 pythonPlugins 字段——那是 Bub 专属。
55
+ claudeCodeAgent({ pythonPlugins: [python] });
56
+ }
57
+
58
+ /** PythonPluginSpec 形状(`{ package }`)塞不进要求 `{ marketplace, name }` 的 native plugin 字段。 */
59
+ function nativePluginFieldsRejectPythonShape(python: PythonPluginSpec): void {
60
+ // @ts-expect-error PythonPluginSpec 缺 ClaudeCodePluginSpec 要求的 marketplace/name,形状不合法。
61
+ claudeCodeAgent({ plugins: [python] });
62
+
63
+ // @ts-expect-error 同上,Codex 侧(CodexPluginSpec 同样要求 marketplace/name)。
64
+ codexAgent({ plugins: [python] });
65
+ }
66
+
67
+ void bubConfigRejectsForeignExtensions;
68
+ void codingAgentConfigsRejectPythonPlugins;
69
+ void nativePluginFieldsRejectPythonShape;
70
+
71
+ describe("Agent Config 的编译期类型边界", () => {
72
+ it("正控:各自合法的配置组合能正常构造(证明上面的 @ts-expect-error 不是因为字段名打错才生效)", () => {
73
+ const claudeConfig: ClaudeCodeConfig = {
74
+ skills: [{ kind: "local", path: "skills/effect-ts/SKILL.md" }],
75
+ plugins: [
76
+ { marketplace: { name: "acme", source: "acme/claude-code-plugins", ref: "v1.3.0" }, name: "safe-shell" },
77
+ ],
78
+ mcpServers: [{ name: "browser", command: "npx", args: ["-y", "@modelcontextprotocol/server-browser"] }],
79
+ };
80
+ const codexConfig: CodexConfig = {
81
+ skills: [{ kind: "repo", source: "Effect-TS/skills", ref: "8f3c1a2", skills: ["effect"] }],
82
+ plugins: [{ marketplace: { name: "acme", source: "acme/codex-plugins", ref: "8f3c1a2" }, name: "repo-map" }],
83
+ mcpServers: [{ name: "browser", command: "npx" }],
84
+ };
85
+ const bubConfig: BubConfig = {
86
+ skills: [{ kind: "local", path: "skills/effect-ts/SKILL.md" }],
87
+ pythonPlugins: [{ package: "bub-plugin-memory==1.3.0" }],
88
+ };
89
+
90
+ expect(claudeCodeAgent(claudeConfig).name).toBe("claude-code");
91
+ expect(codexAgent(codexConfig).name).toBe("codex");
92
+ expect(bubAgent(bubConfig).name).toBe("bub");
93
+ });
94
+ });
@@ -10,9 +10,18 @@ import {
10
10
  parseClaudeCodeTranscript,
11
11
  parseBubTranscript,
12
12
  } from "../o11y/parsers/index.ts";
13
- import type { Sandbox, StreamEvent } from "../types.ts";
13
+ import type { AgentSetupManifest, AgentSetupSkill, Sandbox, SkillSpec, StreamEvent } from "../types.ts";
14
14
  import { t } from "../i18n/index.ts";
15
15
  import { shellQuote } from "../sandbox/shell.ts";
16
+ import { writeAgentSetupManifest } from "./manifest.ts";
17
+ import {
18
+ appendProjectInstruction,
19
+ excludeFromDiff,
20
+ installSkills,
21
+ installedSkillNames,
22
+ skillDiscoveryInstruction,
23
+ type InstallSkillsOptions,
24
+ } from "./skills.ts";
16
25
 
17
26
  /** 每个沙箱里「已经装过的全局包」去重,避免每轮 send 重复 npm i -g。 */
18
27
  const installedBySandbox = new WeakMap<Sandbox, Set<string>>();
@@ -165,9 +174,31 @@ export const shared = {
165
174
  extractJsonlFromStdout,
166
175
  codexThreadId,
167
176
  firstJsonField,
168
- /** 把文本包成 shell 单引号字面量(含转义)。与 sandbox 后端同一份实现,别在 adapter 里手写。 */
177
+ /** 把文本包成 shell 单引号字面量(含转义)。与 sandbox provider 同一份实现,别在 adapter 里手写。 */
169
178
  shellQuote,
170
179
  diagnoseFailure,
180
+ /**
181
+ * 把 SkillSpec 装进沙箱的 `opts.dir`(本地目录/文件按原字节上传;repo 按 ref clone、按
182
+ * 选择集拷贝),返回 manifest 用的 Skill 记录。装进 workspace 的东西自动排除出 diff。
183
+ */
184
+ installSkills(sandbox: Sandbox, specs: readonly SkillSpec[], opts: InstallSkillsOptions): Promise<AgentSetupSkill[]> {
185
+ return installSkills(sandbox, specs, opts);
186
+ },
187
+ /** manifest 里的 Skill 记录 → 沙箱里实际的目录名。 */
188
+ installedSkillNames,
189
+ /** 没有原生 Skill 加载机制的 agent 要写的发现指引段落(装了不提示 = 白装)。 */
190
+ skillDiscoveryInstruction,
191
+ /** 把一段 project instruction 追加进沙箱里的 AGENTS.md(新建的文件顺手排除出 diff)。 */
192
+ appendProjectInstruction,
193
+ /** 把 setup 自己写进 workspace 的路径排除出 agent diff(git 基线早于 agent.setup)。 */
194
+ excludeFromDiff,
195
+ /**
196
+ * setup 收尾写安装 manifest:落沙箱 `__niceeval__/agent-setup.json`,运行器抬成 attempt
197
+ * artifact `agent-setup.json`。什么都没装就别写(空 artifact 不落文件)。secret 不进 manifest。
198
+ */
199
+ writeAgentSetup(sandbox: Sandbox, manifest: AgentSetupManifest): Promise<void> {
200
+ return writeAgentSetupManifest(sandbox, manifest);
201
+ },
171
202
  /** 原始 codex JSONL → 标准事件流 + 用量 + 压缩计数。 */
172
203
  parseCodex(raw: string | undefined): ParsedTranscript {
173
204
  return parseCodexTranscript(raw);