mcpField
E
expo-skill-eval

expo-skill-eval

github.com/expo测试与质量3.5k22.3k 次安装今天更新
代码仓库

关于 expo-skill-eval

expo-skill-eval 是用于评估 Expo 插件仓库中技能的工具。它指导开发者选择提示词和验证方式,并检查技能触发准确性、生成代码或应用渲染结果及截图。

主要功能

  1. 01评估 Expo 技能
  2. 02选择内置、自定义或图片提示词
  3. 03检查技能触发准确性
  4. 04无设备运行代码检查
  5. 05捕获运行时截图

适用场景

  1. 01测量技能是否会响应相关提示词
  2. 02检查生成的应用代码是否符合预期
  3. 03将渲染界面与目标截图对比

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/expo/skills --skill expo-skill-eval

安装后在 Claude Code 中输入/expo-skill-eval确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

agents/
visual-grader.md
references/
design-rubric.md
runtime-matrix.md
scripts/
check-static.sh
clean-fixture.sh
generate_viewer.py
latest-sdk.sh
make-fixture.sh
make-workspace.sh
snapshot-android.sh
snapshot-ios.sh
snapshot-web.sh

详细介绍

Expo Skill Eval

评估 plugins/expo/skills/ 中各项技能的触发准确性、生成代码质量和/或在 Expo Go 中的运行时渲染效果。

要求:macOS、Xcode(iOS 模拟器)、至少有一个 AVD 的 Android SDK,以及 bun。不假定安装了其他设备工具。

工作区根目录:/private/tmp/expo-skill-eval-<skill-name>/iteration-N/(例如 /private/tmp/expo-skill-eval-expo-ui/iteration-4/)。

开始前——明确范围

在进行任何流水线工作之前,先确认以下所有事项——不可跳过任何一项(只有在请求中已明确说明某项选择时,才可跳过该项)。按以下顺序分批调用 AskUserQuestion,每次最多问 4 个问题:

  1. 要评估的技能(如果请求中不明确)。
  2. 提示词——用哪些提示词驱动评估。默认预先选中所有内置提示词(来自该技能的评估用例);你可以移除其中任意提示词、添加自定义文本提示词,或根据上传的屏幕截图构建(技能必须复现的目标 UI)。详见下文的提示词。
  3. 要验证的内容——从三个选项中多选:运行时 + 屏幕截图 / 触发准确性 / 代码检查(无设备)。详见要验证的内容。
  4. Expo SDK——最新版本(默认,自动检测)或指定版本。
  5. 运行器——Expo Go(默认)或开发构建。
  6. 平台——iOS / Android / web(始终提供全部三个选项)。
  7. claude -p 的权限标志——skip-permissions(默认)或 accept-edits。
  8. 查看器交付方式——仅本地(默认)或发布可分享的 Artifact。
  9. 如果选择了触发准确性——确认已禁用(或未安装)已发布的 expo 插件。

每项的详情见下文。第 4–6 项(SDK、运行器、平台)适合放在同一次 AskUserQuestion 调用中。

如果请求中不明确要评估哪项技能,请列出 plugins/expo/skills/ 中可用的技能,并询问要评估哪一项。

被测技能的加载方式——每个阶段各用一种,共两种机制(不要全局只选一种):执行器通过文件路径引用技能(SKILL_PATH = plugins/expo/skills/<skill>/SKILL.md,并显式读取);触发评估则将其作为插件加载(--plugin-dir plugins/expo,让模型根据描述自动选择)。两种方式都指向仓库内的本地版本——你要评估的就是这个版本。启动测试工具会话本身不需要任何特殊标志(测试工具会根据仓库路径找到技能);这些机制适用于它所启动的 claude -p 子进程。有关各阶段采用不同方式的原因,请参阅步骤 1 和 3。一项运行前检查(若涵盖触发评估则必须执行):如果已安装/启用已发布的 expo 插件,请在启动测试工具之前通过 /plugin 将其禁用,并在之后重新启用。禁用一次会更改全局配置,而当前会话及其启动的 claude -p 子进程都会继承该配置。触发评估必须执行此操作的原因:该阶段通过 --plugin-dir 加载本地技能,而另一个已安装的 expo 插件会与之冲突——模型可能触发已发布的 expo:expo-ui;由于检测只会检查工具调用名称,你可能会在不知情的情况下评估已发布的描述,而非本地修改(冲突也可能直接导致错误)。执行器/运行时/静态阶段不受影响——它们通过本地 SKILL_PATH 读取被测技能,不使用 --plugin-dir——因此不包含触发评估的运行可以跳过禁用步骤。禁用 expo 不会禁用 expo-skill-eval(这是一个独立的项目技能,不属于 expo 插件),因此测试工具仍然可用。 请在开始前明确向用户确认——与确认要评估哪个技能的方式相同。当触发器评估在范围内时,请在开始第 1 步之前,询问用户是否确认已禁用(或未安装)已发布的 expo 插件;如果插件仍处于启用状态,请暂停,并让用户通过 /plugin 将其禁用。未获用户确认前,不要运行触发器评估——测试框架无法可靠地自行检测已安装的插件(读取全局插件配置或运行 claude plugin list 会触发提示),因此这需要手动确认,而非自动检查。

选择提示词——内置、自定义或目标截图。 提示词是驱动执行器的输入(启用技能和未启用技能两种情况),与要验证的内容分开。通过 AskUserQuestion 确认提示词(如果请求中已指定提示词,则跳过):

  • 内置提示词——阅读待测技能(其 SKILL.md 和 references/)以及 references/runtime-matrix.md,生成涵盖该技能标准用例的代表性提示词。(如果技能已在 evals/evals.json 中附带评估用例,也要纳入其中的 prompt 字段——但大多数技能并没有,因此通常需要自行整理。)默认全选,让默认运行覆盖该技能的标准用例;允许用户取消选择。
  • 自定义文本提示词——用户输入的一次性提示词。不要为此单独占用一个选项:AskUserQuestion 会自动添加「输入内容」/「其他」选项,用户在此输入的任何内容都会成为自定义文本用例。
  • 根据上传的截图构建——用户提供目标截图(要复现的 UI)的路径。执行器会被要求打开它——claude -p 会通过其 Read 工具读取 PNG——并构建与之匹配的应用;该用例会将此路径记录为 reference_image,评分时会将生成的应用与该目标进行比较(第 6 步)。这是测试 UI 技能视觉效果最有力的方式:「构建这个。」

遵守 AskUserQuestion 每个问题最多 4 个选项的限制,并按以下优先级安排(要避免的问题:四个选项填满后,上传选项会被静默移除):

  1. 始终为「根据上传的截图构建」预留一个选项。 这是视觉评估的核心,绝不能成为被移除的选项。
  2. 不要添加明确的「自定义文本提示词」选项——自动添加的「输入内容」/「其他」选项已经涵盖此需求。
  3. 剩余的 ≤3 个选项用于内置/代表性提示词,默认选中。如果提示词超过 3 个,则将其合并为一个默认选中的**「所有内置提示词(默认)」**选项,并在简短的后续提问中提供选择子集的方式,以便仍能保留上传选项。

将其设为多选。选择「根据上传的截图构建」后,在后续提问中询问目标图片的路径。每个选中的提示词(内置、用户输入或图片)都会成为一个评估用例(分别运行启用技能和未启用技能的情况)。

除非请求已明确说明,否则始终确认要验证的内容。 展示以下选项,并允许用户选择一项或多项(根据技能在 references/runtime-matrix.md 中的条目,将默认项设为粗体):

选项功能何时建议设为默认选项
运行时 + 截图完整流程:fixture → 执行器 → 静态门禁 → 在 iOS/Android 上运行应用并截图。运行器(Expo Go 或开发构建)是另一个问题——此处不要提及。对任何会渲染应用屏幕的技能,均设为默认选项(references/runtime-matrix.md 中的 expo-go/dev-build 行)。需要已启动的模拟器/仿真器。
触发准确性通过 claude -p 运行逼真的提示词,检查是否读取了技能。衡量召回率(仅检查应触发的查询)。始终适合作为独立检查。
代码检查(无需设备)tsc --noEmit + 基于差异的 lint + expo export,此外,评分器还会根据你提供的自定义预期检查生成的代码。无需设备。对 static-only 和 n/a 技能设为默认选项;如果想验证代码模式(正确的导入路径、Host 封装等)但不运行应用,也设为默认选项。
将这些选项作为一个多选问题呈现——“你想验证什么?” 这些是评分维度(如何评判构建结果)——与 Prompts 阶段(构建什么)不同。用户可以选择任意组合。如果提示词是上传的截图(见 Prompts),请加入 “运行时 + 截图”,以便测试框架捕获生成的应用,评分器也能根据目标进行评分。

阅读 references/runtime-matrix.md,在提出建议前确定该技能的默认模式。如果请求已指定模式(例如“只检查是否触发”“在设备上运行”),则跳过问题并继续。

一次性提前选择 Expo SDK 版本。 使用 bash /abs/path/expo-skill-eval/scripts/latest-sdk.sh 检测最新版本(该脚本会打印主版本号,例如 56;内部使用 bun 运行 npm view expo dist-tags --json,并通过 JSON.parse/semver 读取主版本号,且符合 bash-scripts 规则——因此不要自行内联运行注册表查询,否则会触发提示)。然后通过 AskUserQuestion 确认:默认使用最新 SDK,或允许用户指定较旧版本(例如复现特定版本的问题)。在构建 fixture 的所有地方都使用选定版本——将其作为 <sdk> 参数传给 make-fixture.sh,并写入每个 eval case 的 runtime.sdk。如果请求已指定版本(“在 SDK 54 上评估”),则跳过检测并使用该版本。

默认使用最新版本——这样可以与 expo start 在设备上安装的 Expo Go 保持兼容。如果指定的 SDK 版本早于设备上安装的 Expo Go 版本,expo start 会尝试提示“安装推荐的 Expo Go 版本?”;在没有 TTY 的情况下(快照脚本会从 /dev/null 读取 stdin),程序会因 Input is required, but 'npx expo' is in non-interactive mode 而退出,导致所有快照都失败。因此,只有在模拟器/仿真器上也预先安装了匹配的 Expo Go 时,才指定较旧的 SDK;否则请使用最新版本。

选择运行器——Expo Go(默认)或开发构建。 通过 AskUserQuestion 询问(如果请求已说明选择哪种,则跳过):

  • Expo Go(默认) — 快照脚本会直接使用 expo start --ios / expo start --android 运行应用。速度快(无需原生编译),并且能运行 Expo Go 所包含的所有内容(包括 SDK 56+ 中的 @expo/ui)。无法运行自定义原生代码(expo-modules、配置插件,以及 Expo Go 未包含的原生依赖项)。
  • 开发构建 — 快照脚本会改用 expo run:ios / expo run:android,为每个 fixture 编译原生开发客户端。适用于输出需要自定义原生代码的技能(否则会是 static-only 的情况)。速度慢得多——expo run 会为每个 fixture 预构建并进行原生编译(耗时数分钟,尤其是首次运行),且需要完整的 iOS/Android 构建工具链,因此只有在技能确实需要原生代码时才选择此项。占用大量磁盘空间: 每个 fixture 的原生构建都占用数 GB。快照阶段会在每个 fixture 运行后执行 clean-fixture.sh,将峰值占用控制在约一次构建的大小,但仍建议开发构建运行时减少评测用例,并只选择一个平台,同时确保留有数 GB 的可用空间。clean-fixture.sh 会移除每个 fixture 的构建输出(node_modules、ios、android、.expo、dist 以及该 fixture 的 iOS DerivedData),并保留应用源代码和 git。控制开发构建磁盘占用的关键是减少评测用例并选择一个平台——它只会回收每个 fixture 的构建输出,不会触及共享依赖缓存,因此不会重新下载任何内容。

通过 EXPO_SKILL_EVAL_RUNNER 环境变量将所选模式传递给快照脚本(默认值为 expo-go,也可以是 dev-build),并在每个评测用例的 runtime.mode 中设置相应值(expo-go 或 dev-build)。请参阅步骤 5。

选择平台——无论技能是什么,都要询问。 使用 AskUserQuestion 提供 iOS / Android / Web(可多选);默认选择 iOS + Android,但始终要提供 Web 选项,不要根据技能预先筛选。对于大多数技能,Web 都是有效选择:@expo/ui 的通用组件(Host、Row、Column、Button、List 等)可在 Web 上渲染,expo-dom、NativeWind/Tailwind、API routes 和纯 React Native 也一样。唯一无法在 Web 上显示的是特定平台的原生视图树(@expo/ui/swift-ui 或 @expo/ui/jetpack-compose),它们会渲染为空白——而空白本身也是有用的信号,因此仍由用户决定。无论使用哪种运行器,Web 都通过 snapshot-web.sh(expo start --web + Playwright/Chromium)运行(expo run 仅支持原生平台;没有 Web 开发构建),而且 Web 是测试最少的路径。将用户选择的平台集合写入每个评测用例的 runtime.platforms,并让 run_snapshots.py 遍历这些平台。

确认 claude -p 子进程的运行方式——启动前询问一次。 使用 AskUserQuestion 询问是否可以使用 --dangerously-skip-permissions 运行,然后对本次运行中的所有子进程都采用相同的选择(运行过程中不要再次询问):

  • 跳过权限(推荐) — 传入 --dangerously-skip-permissions。每个子进程都会在 /private/tmp/expo-skill-eval-* 下的临时 fixture 中无人值守运行,并可写入文件、运行设置命令,而无需提示。
  • 仅接受编辑 — 改为传入 --permission-mode acceptEdits。Bash 命令和安装操作会自动拒绝(没有 TTY),因此部分评测可能只会生成不完整的输出。

如果不带任何一个标志,单独使用 claude -p 完全无法写入文件。如果请求中已经说明了偏好(例如“跳过权限”或“不要使用危险标志”),则跳过提问。

确认如何交付结果查看器——开始前询问一次。 发布到 claude.ai 会对外展示,因此绝不要在运行过程中突然发布;请在开始前的同一个 AskUserQuestion 中(与权限标志一并)询问:

  • 仅限本地(默认) — generate_viewer.py 会生成 viewer.html 并在本地浏览器中打开。不会有任何内容离开本机。
  • 发布可分享的 Artifact — 此外,在最后将查看器渲染为 claude.ai Artifact(默认设为私有的网页,用户可与团队成员分享)。只有用户在此选择同意时才执行。 如果请求中已经说明是否要分享/发布,请跳过提问。发布操作方式请参阅 Viewer 部分。

评估用例架构

你负责生成本次运行的评估用例——每个选定提示词对应一个用例——并将其写入 <workspace>/iteration-N/evals.json(查看器会从那里读取)。每个用例都在标准的 skill-creator 评估用例结构上扩展了一个 runtime 块和视觉预期:

{
  "id": 1,
  "prompt": "Build me a settings screen with a dark mode toggle and a list of options",
  "expected_output": "Working Expo Router screen",
  "expectations": [
    "Uses Expo Router file-based routing",
    "TypeScript compiles with no errors"
  ],
  "runtime": {
    "mode": "expo-go",
    "platforms": ["ios", "android"],
    "sdk": "56"
  },
  "visual_expectations": [
    "No red error screen or Expo Go error overlay on any platform",
    "A settings screen with a visible toggle control is rendered"
  ]
}
  • runtime.mode:静态检查通过后如何运行评估 —

    • "expo-go":在 Expo Go 中运行(expo start --<platform>)并截图。速度快,仅使用 JS。默认值。
    • "dev-build":构建原生开发客户端(expo run:<platform>)并截图。适用于输出使用自定义原生代码的技能;速度慢得多(每个 fixture 都要编译原生代码)。
    • "static-only":静态检查通过后停止 — 适用于不生成 UI 的技能,或完全不想运行设备的情况(CI)。

    请查阅 references/runtime-matrix.md,了解代码库中的技能分别支持哪些模式。(dev-build 可让你实际运行此前因需要原生代码而只能使用 static-only 的技能。)

  • runtime.platforms:从 ios、android、web 中选定的子集 — 提前选定(始终会提供这些选项,不受技能限制;请参阅 开始之前)。默认值为 ["ios", "android"]。

  • runtime.sdk:fixture 应用使用的 Expo SDK 主版本 — 设置为提前选定的版本(请参阅 开始之前 — 明确范围)。省略则使用最新模板。

  • reference_image(可选 — 图像提示词):技能必须复现的目标截图的绝对路径。设置后,执行器会被指示打开该图像(通过其 Read 工具)并构建匹配的应用,评分器会在常规预期之外,对生成的应用复现目标图像的相似度进行评分(第 6 步)。在 Prompts 阶段通过“根据上传的截图构建”进行设置。

图像提示词用例是设置了 reference_image 的普通用例;请启用“Runtime + screenshots”,以便测试框架捕获结果并与目标进行比较:

{
  "prompt": "Build an app whose UI matches the attached reference screenshot.",
  "reference_image": "/abs/path/to/target.png",
  "runtime": { "mode": "expo-go", "platforms": ["ios"], "sdk": "56" },
  "visual_expectations": ["Matches the reference's layout, components, and color treatment"]
}

每个评估用例的流程

编排模型 — 在主线程中,你只需运行 python3 <orchestrator>,几乎不做其他操作。 每个阶段都由一个小型 Python 编排器驱动:你先用 Write 将其写入工作区,再通过 python3 /private/tmp/expo-skill-eval-<skill>/<phase>.py 运行(适用 python3 规则)。编排器是调用 scripts/*.sh 文件的唯一位置 — 始终通过 subprocess.run(["bash", "<scripts>/<name>.sh", …]) 调用,作为 python3 的子进程运行,无需单独设置规则 — 并且是并行处理、日志记录和目录创建的唯一所在。因此,在主线程中你只需:编写编排器,用 python3 运行它们,用 Read/Glob/Grep 工具检查输出,以及启动评分器子 Agent。绝不要将命令放入串联、后台运行或管道连接的 shell 结构中,也绝不要临时运行 mkdir/ls/cat/tail/echo — 这些就是提示词的作用。(单独运行一次 bash …/scripts/<name>.sh …,用于手动调试是可以的,例如重新运行一个不稳定的快照;但整个流程必须通过编排器运行。)在前台运行每个编排器 — 让工具调用阻塞,直到它完成;编排器已在阶段内并行处理,因此无需让多个阶段重叠运行。不要通过 … & echo "$!" / wait 在 shell 中将阶段放到后台(这些 &、echo 和 wait 片段没有规则,会触发提示)。如果确实必须在继续其他工作时运行某个阶段,请在普通的 python3 <orchestrator> 2>&1 | tee <ws>/…log 调用中使用 Bash 工具的 run_in_background 参数 — 切勿手动编写 shell &。**预计一开始恰好会出现一次权限提示:**首次向工作区执行 Write 时。allowed-tools 可以跳过 Bash/Read 的权限提示,但不能跳过 Write/Edit 的提示,因此请在首次提示中选择“允许本会话对该目录进行所有编辑” — 这会覆盖整个运行过程中的所有编排器、evals.json 和查看器文件。

0. 工作区设置

使用工作区脚本一次性创建本次运行的目录树——切勿临时使用 mkdir(直接运行 mkdir 会提示:没有 mkdir 规则,而且 "$WORKSPACE/…" 变量也无法匹配路径 glob):

bash /abs/path/expo-skill-eval/scripts/make-workspace.sh /private/tmp/expo-skill-eval-<skill> iteration-N <num-evals>

这会为每个评估创建 trigger-evals/scratch 和 iteration-N/eval-<i>/{with_skill,without_skill}/outputs。该操作受 Bash(bash *expo-skill-eval/scripts/*) 规则覆盖,而脚本中的 mkdir 会作为脚本的子进程运行(无需单独规则)。之后,其他所有目录都由需要它们的脚本或编排器创建(make-fixture.sh、执行器编排器的 os.makedirs、快照脚本),或者由 Write 工具自动创建父目录——因此你无需再运行 mkdir。

1. 触发评估(仅评估应触发的情况)

在工作区的 trigger-evals/ 目录下编写 run_trigger_eval_real.py 脚本。仅使用 "should_trigger": true 查询——expo 插件是一组互补技能,因此多个技能在同一提示词下触发并不算失败。只衡量召回率:使用应该调用该技能的真实提示词,并按触发率评分。

脚本应针对每个查询运行 claude -p <query>(附带 --output-format=stream-json --verbose --include-partial-messages,从环境中移除 CLAUDECODE,并在开始前于 开始之前——明确范围 中确认权限标志),并通过监视流中的 Skill 或 Read 工具调用来检测目标技能是否触发。注意:--include-partial-messages 必须同时搭配 --output-format=stream-json 和 --verbose 使用——缺少其中任何一项都会导致 CLI 立即报错。

加载待测技能——为每个触发子进程传入 --plugin-dir。 触发评估衡量的是技能的描述是否能让模型调用它,因此子进程必须加载本地技能(即你修改后的版本)。claude -p 子进程不会继承父会话的 --plugin-dir,所以必须显式添加:--plugin-dir <plugin-root>,其中 <plugin-root> 是拥有该技能的插件目录的绝对路径——即包含 .claude-plugin/plugin.json 的 plugins/expo 上级目录(例如 --plugin-dir /Users/.../skills/plugins/expo)。路径必须是绝对路径:子进程从临时的 scratch/ 工作目录运行,因此相对路径 plugins/expo 无法解析——而缺少插件目录会静默地导致未加载任何内容,造成触发率为 0% 的假象。然后监视技能是否以其带插件限定的名称触发(<plugin>:<skill>,例如 expo:expo-ui)。另有两点需要注意:(1) 如果全局还安装了已发布的 expo 插件,请在本次运行期间禁用它(通过 /plugin),运行后再重新启用——否则两个 expo 副本会在子进程中发生冲突,模型可能触发已发布的 expo:expo-ui,导致评分静默地针对其描述而非你的本地修改(触发检测只能看到工具调用名称,无法区分这两个副本;开发检出通常不会安装该插件)。(2) 切勿制作技能的合成副本——始终会优先使用已加载的真实副本,因此合成测试框架的评分会是 0%。(已安装插件不会影响执行器:它们直接读取本地 SKILL_PATH,且不传入 --plugin-dir。)

每个查询的子进程都应从一个空的临时工作目录运行(例如 trigger-evals/scratch/),而不是从仓库根目录运行。诸如“帮我做一个设置界面”的应触发提示词可能会让子进程写入文件;如果使用 --dangerously-skip-permissions,这些文件就可能写入技能仓库。触发检测只需确认技能的 Skill/Read 调用出现在流中——不需要测试 fixture——因此任何意外写入都只会留在临时目录中。

每个查询的子进程超时至少应设为 300 秒。180 秒太短——某些查询会导致模型在触发技能前就开始生成代码,使总运行时间超过 3 分钟。

每个技能运行一次触发评估,而不是针对每个代码评估用例分别运行。

2. 夹具

每次执行器运行都会获得一个全新的 Expo 应用,由 scripts/make-fixture.sh <app-path> <sdk> [clean|full] 创建:

scripts/make-fixture.sh <workspace>/iteration-N/eval-X/<config>/app <sdk>          # blank app (default)
scripts/make-fixture.sh <workspace>/iteration-N/eval-X/<config>/app <sdk> full     # keep example tabs

该脚本会使用 bunx create-expo-app -t default@sdk-<version> 创建应用(未提供版本时使用最新模板),每个 SDK 版本和变体只创建一次,然后将其缓存到 ~/.cache/expo-skill-eval/fixtures/,并通过 APFS 写时复制克隆缓存。因此,每种变体的首次运行需要支付安装成本,之后的每次运行都几乎瞬间完成。默认的 clean 变体会运行模板的 reset-project 脚本,因此执行器从空白应用开始,输出中的每个屏幕都由它们创建——评分信号清晰得多。只有当评测提示词假设应用已经存在时(例如“我有一个包含两个标签页的应用……”),才使用 full。该脚本还会在克隆目录中重置 git,因此应用中的 git diff 会准确显示执行器所做的更改(这是评分器的实用依据)。

先按顺序构建夹具,再并行分发执行器——绝不要并发创建夹具。 make-fixture.sh 会共享 ~/.cache/expo-skill-eval/fixtures/ 下按 SDK 和变体区分的缓存。如果两次运行都发现缓存尚未创建,并同时调用 bunx create-expo-app,bun 的链接步骤就会发生冲突,其中一次运行会因 EEXIST / "could not determine executable to run for package create-expo-app" 而失败。因此,在执行器编排器(步骤 3)中,应先逐个创建所有夹具——用一个简单的 Python 循环调用 subprocess.run(["bash", "<scripts>/make-fixture.sh", app, sdk, variant])(其中 sdk 是预先选定的版本)——然后使用 ThreadPoolExecutor 并行分发 claude -p 执行器。按顺序创建很快:每个 SDK 和变体只有第一个夹具需要支付安装成本,其余都是约 1 秒的 APFS 克隆。(也绝不要用临时编写的 shell 命令并行创建夹具,例如 make-fixture.sh A & make-fixture.sh B & wait——&/wait 片段会触发提示;按顺序执行的 Python 循环既能避免竞争,也能避免提示。)

3. 生成(执行器子 Agent)

从 Python 脚本中以 claude -p 子进程调用的方式运行执行器,不要通过 Agent 工具运行。Agent 工具会在各自的权限上下文中启动子 Agent——在 fixture 应用中编辑文件时会提示用户。claude -p 子进程是完全独立于权限系统的进程(与触发评估测试框架使用的模式相同)。

编写 Python 脚本,保存到 /private/tmp/expo-skill-eval-<skill>/run_executors.py。首先在顺序循环中创建每次运行所需的所有 fixture——每次依次运行 subprocess.run(["bash", "<scripts>/make-fixture.sh", app, sdk, variant], …)(并发创建会与共享的 bun 缓存发生竞争——参见步骤 2)。然后通过 ThreadPoolExecutor 并行运行启用和未启用 skill 的 claude -p 调用。两个阶段都在 Python 内运行(受 python3 规则约束),因此主线程上不会执行临时 shell 命令。每个执行器的提示词必须包含:

  • skill 路径(仅启用 skill 的运行)和评估提示词。
  • **图片提示词用例(设置了 reference_image):**目标截图的绝对路径,以及类似这样的指令:“使用你的 Read 工具打开 <path> 处的参考截图,并尽可能还原其 UI——布局、组件、间距和颜色。”(claude -p 可以读取以这种方式提供的 PNG,因此执行器确实能看到目标。)
  • fixture 应用路径:“在 <app-path> 中进行更改。项目已存在且依赖已安装。所有文件操作都使用绝对路径。”
  • “写入任何文件之前,检查项目布局——运行 ls,读取 package.json 和 app.json——以找到正确的路由目录。近期的 SDK 默认模板会将 Expo Router 路由放在 src/app/;较早的模板会放在项目根目录下的 app/ 中——请检查并确认此 fixture 使用的是哪一种。”
  • “请勿启动开发服务器、启动模拟器或截屏——完成后由测试框架执行这些操作。”
  • 简短说明所构建内容的保存位置。

claude -p 子进程的标志:

  • 从环境变量中移除 CLAUDECODE(env = {k: v for k, v in os.environ.items() if k != "CLAUDECODE"})——否则,在运行中的 Claude Code 会话内嵌套运行时,claude -p 会无响应地挂起。
  • 一个事先与用户确认的权限标志(见 开始之前——明确范围):--dangerously-skip-permissions 或 --permission-mode acceptEdits。将选定的标志写入生成的脚本。单独使用 claude -p 而不加这两个标志时,无法写入文件——它没有 TTY 来批准编辑,只会将代码作为文本输出。
  • 请勿向执行器传递 --plugin-dir(与触发评估不同)。启用 skill 的运行已通过绝对路径 SKILL_PATH 读取 skill,因此会直接测试本地内容;未启用 skill 的运行则必须完全不提供 skill——加载插件可能会使 skill 自动触发,从而污染基线。使用基于路径的执行器还能清晰地区分两个问题:执行器衡量的是内容质量(读取 skill 后,它是否有用?),触发评估衡量的是触发效果(描述能否使它被选中?)。

将每次运行的 stdout/stderr 保存到 fixture 旁边的日志文件中,作为评分依据。每个执行器的超时时间设为 900 秒——启用 skill 的运行会在编码前读取多个参考文件,通常需要 5–10 分钟。

4. 静态检查

编写 run_static.py,并使用 python3 运行。对于每个 eval/config 应用,在 ThreadPoolExecutor 中调用 subprocess.run(["bash", "<scripts>/check-static.sh", app, "ios,android"], capture_output=True, …)(静态检查相互独立——在 Python 内并发运行,绝不要使用 shell 的 &/wait),并将每项结果(退出代码和捕获的输出)写入 eval-<i>/<config>/static.json,供评分器使用。

check-static.sh 会针对列出的平台运行 tsc --noEmit、expo lint 和 expo export。导出成功可以在不连接设备的情况下发现大多数导入、语法和缺少模块等问题;如果导出失败,第 5 步应干净地判定为 FAIL——记录失败,并让快照编排器跳过该应用。

5. 运行并截图(各 eval 串行执行)

编写 run_snapshots.py,并使用 python3 运行。模拟器和仿真器是共享资源,因此此编排器应串行运行(不要使用线程池):对于通过静态检查的每个应用,以及每个平台,先通过 os.makedirs 创建 outputs/ 目录,然后调用 subprocess.run(["bash", "<scripts>/snapshot-<platform>.sh", app, f"{outputs}/<platform>.png", port], env={**os.environ, "EXPO_SKILL_EVAL_RUNNER": runner}, …)。将端口作为位置参数传入:iOS 使用 8081,Android 使用 8082——expo run:ios/android --port N 受支持,使用不同端口可让你在需要并行运行两个平台时避免端口冲突。截图会保存到本次运行的 outputs/ 目录中,供查看器内嵌显示。

每个 fixture 完成后清理磁盘——对于 dev-build 运行至关重要。 捕获某个应用的所有选定平台后(并且在构建下一个 fixture 之前),调用 subprocess.run(["bash", "<scripts>/clean-fixture.sh", app])。每次 expo run:<platform> 都会留下数 GB 的原生构建输出(iOS Pods 和 DerivedData、Android Gradle build);如果不清理,eval × config × iteration 会不断累积并在运行中途占满磁盘(你会看到的故障正是磁盘占满)。clean-fixture.sh 会删除可重新生成的大型目录(node_modules、ios、android、.expo、dist)以及该 fixture 的 iOS DerivedData,同时保留应用源代码和 git,因此评分器的 git diff 仍可正常工作。串行截图并在每个 fixture 后清理,可将磁盘峰值控制在约一个 fixture 的构建所需空间,而非所有 fixture 的总和。(对 expo-go 运行也无害——只是可回收的空间很少。)

runner 是预先选择的运行方式(默认是 expo-go,也可以是 dev-build)。快照脚本会遵循 EXPO_SKILL_EVAL_RUNNER:expo-go 会通过 expo start --<platform> 启动(并执行安装 Expo Go 和深层链接的流程);dev-build 会通过 expo run:<platform> --port <port> 启动,编译并安装原生开发客户端,并跳过 Expo Go 的步骤。脚本已将 dev-build 的超时默认设为 900s,但如果首次原生编译需要更长时间,请增大 EXPO_SKILL_EVAL_BUNDLE_TIMEOUT。make-fixture.sh 会在每个 fixture 中预先安装 expo-dev-client,以便在 expo run 尝试通过深层链接打开应用之前注册开发客户端 URL scheme。

快照脚本始终截取初始路由 /。 它们会通过深层链接打开应用并截取一张图片——无法点击或导航。设计 eval 提示词时,应确保待测功能渲染在根路由。如果执行器将主 UI 放在导航操作之后(例如 index 页面上的“打开设置”按钮),快照就会完全错过该功能,所有视觉预期都会失败。 每个 snapshot-<platform>.sh 都会在启动时释放其 Metro 端口(终止上次运行崩溃后残留在该端口上的进程),并在退出时关闭 Metro — 因此你无需自行运行 lsof/kill/pkill 来清理端口(那样会触发提示,而且端口清理已自动处理)。随后,脚本会启动 Metro,等待 Metro 日志中出现“Bundled”行,等待界面稳定,截取屏幕截图,然后关闭 Metro。如果没有已启动的设备,iOS 会启动最新可用的 iPhone 模拟器;Android 会启动第一个 AVD(这是较慢的路径 — 只需启动一次,并在整个迭代过程中重复使用)。Android 会先回收卡死或处于 offline 状态的模拟器(先尝试优雅地运行 adb emu kill,再强制终止进程并重置 adb),以免半死不活的实例影响运行;然后使用硬件 GPU启动(-gpu host,在 Apple Silicon 上由 Metal 加速)。如果 host 模式在某台机器上导致模拟器自行中止(qemu 在 gfxstream/Metal 深处触发 SIGABRT — 在负载较高的 Apple Silicon 上可能发生),请在 snapshot-android.sh 中将 GPU_MODE 改为软件模式(guest 可以稳定渲染,但速度较慢 — 请增加等待稳定时间;避免使用 swiftshader_indirect,它会在 arm64 上卡死在启动阶段)。只有当 platforms 包含 web 时,才会运行 snapshot-web.sh。每个脚本都会在截图旁写入 Metro 日志(<name>.metro.log)— 请将其纳入评分器的输入。如果脚本以非零状态退出,仍会尽力截取屏幕截图(错误画面也是证据)。**dev-build 重新启动:**Metro 启动后,脚本会通过 xcrun simctl launch(iOS)和 adb shell am start -n <pkg>/.MainActivity(Android)重新启动应用 — 这样可以避免首次启动时通过 URL scheme 深度链接触发的“在 X 中打开?”系统对话框。

完成本次迭代的所有截图后,务必生成查看器 — 将工作区根目录传给仓库中已有的脚本:

python3 /abs/path/expo-skill-eval/scripts/generate_viewer.py /private/tmp/expo-skill-eval-<skill>

脚本会在工作区根目录(iteration-N/ 的上一级)生成 viewer.html,并自行在浏览器中打开(通过 webbrowser.open)— 因此无需单独运行 open 命令(也不需要 Bash(open:*) 规则)。参见下方的 查看器 部分。

6. 评分

在前台启动一个评分子 Agent。其提示词必须包含:

  • 评估用例中的评估提示词、预期列表和 visual_expectations。
  • agents/visual-grader.md 中的说明(截图评分、redbox 检测)。
  • 截图文件、Metro 日志,以及作为输入的第 4 步 static.json。
  • 图像提示词用例(用例包含 reference_image):还需包含目标截图(reference_image)、references/design-rubric.md 和 fixture 的 git diff。告知评分器将生成的截图与目标截图进行比较,并输出下方的 reference_match 和 quality 块。

评分器会在输出文件旁写入 grading.json,其结构如下:

{
  "score": 8.5,
  "max_score": 9,
  "expectations": [
    {"text": "...", "passed": true, "evidence": "..."}
  ],
  "reference_match": {
    "score": 7, "max": 10,
    "evidence": "ios.png vs target.png: same two-section grouped list + toggle; accent color differs (blue vs target's green); row spacing tighter than target"
  },
  "quality": {
    "dimensions": [
      {"name": "Layout & hierarchy", "score": 2, "max": 3, "evidence": "ios.png: …"}
    ],
    "subtotal": 17,
    "max": 24,
    "summary": "…"
  },
  "user_notes_summary": {"needs_review": false, "notes": ""}
}

视觉预期也放入同一个 expectations 数组,证据需注明截图文件名并描述画面内容。reference_match 块(生成的应用与目标截图的吻合程度)和 quality 块(references/design-rubric.md 中的设计评分)仅适用于图像提示词用例,或明确要求进行质量评分的情况。纯文本提示词运行时,请省略这两个块。

推出阶段

按以下顺序构建并调试流水线 — 每个阶段都可独立使用:

  1. 静态:仅执行步骤 1–4(所有内容均设置为 runtime.mode: "static-only")。无需设备;适合 CI。
  2. iOS:将 snapshot-ios.sh 加入循环。simctl 是最适合脚本控制的目标。
  3. Android:添加 snapshot-android.sh。模拟器启动最耗时 — 整个会话期间保持一个模拟器运行。
  4. Web:为面向 web 的技能添加 snapshot-web.sh(通过 bunx 使用 Playwright;首次运行会下载 Chromium)。

实用说明

  • 临时位置:所有评估工作区都位于 /private/tmp/expo-skill-eval-<skill-name>/iteration-N/ 下。本次运行中的所有操作——Read、Write、Edit 和 Bash——都受 allowed-tools 前置元数据约束,因此正确加载的技能无需提示。
  • 权限规则形式(为什么此技能无需提示):规则的语法很重要,而且两类工具的行为不同:
    • Bash(...) 规则——将路径限定为技能自身的代码(不允许宽泛的解释器)。 Bash(python3 /private/tmp/expo-skill-eval-*)(以及 /tmp 别名)可运行你在工作区中生成的 Python 编排脚本;Bash(python3 *expo-skill-eval/scripts/*) 可运行已签入的 scripts/generate_viewer.py;Bash(tee /private/tmp/expo-skill-eval-*)(以及 /tmp)允许 python3 … 2>&1 | tee <workspace>/…log 写入日志而不触发提示;Bash(bash *expo-skill-eval/scripts/*) 只能运行此技能的 scripts/*.sh。由于每条路径都受到限定,越权途径仍会被拒绝:python3 -c …、bash -c …、tee /etc/… 以及在其他任何位置运行代码都不匹配(经实测验证——限定规则允许 bash <dir>/run.sh,但会阻止 bash -c … 和任何其他路径)。脚本内部调用的命令——bunx、xcrun simctl、adb、git、mkdir、expo——是脚本的子进程,不是 Bash 工具调用,因此无需规则。不要在主线程中临时运行 mkdir/ls/find/cat/grep(它们没有规则,会触发提示——而且原始命令 mkdir "$WORKSPACE/…" 无法匹配路径 glob,因为路径是未展开的变量):使用 make-workspace.sh(步骤 0)创建目录树,让编排脚本创建自己的目录(os.makedirs),并使用 Read/Glob/Grep 工具检查结果(无需 Bash 规则)。
    • Bash 规则匹配(已测试,不易察觉):Bash 规则是针对命令字符串的 gitignore 风格 glob。* 匹配任意长度的字符序列,包括 / 和空格,且可用于模式中间——因此 Bash(python3 /private/tmp/expo-skill-eval-*) 可匹配 python3 /private/tmp/expo-skill-eval-x/run.py 2>&1,而 Bash(bash *expo-skill-eval/scripts/*) 可匹配 bash /any/abs/path/expo-skill-eval/scripts/foo.sh args。之前尝试时遇到两个易错点:** 会被按字面匹配(Bash 规则中切勿使用);后缀 :* 仅在紧跟命令令牌时有效(Bash(python3:*))——不能放在部分路径之后(Bash(python3 /path-:*) 不匹配)。复合命令会按 |、&&、||、; 和 & 拆分,每个片段都需要各自匹配的规则。
    • **Read 规则可抑制提示;Write/Edit 规则则不能。**这是 Claude Code 的行为不对称(不是模式错误,也不是未重新加载——即使在同一前置元数据中的 Bash/Read 规则明确生效的会话里,Write 仍会触发提示):无论 allowed-tools 如何设置,文件创建和编辑始终会经过 Claude Code 的编辑审批流程。前置元数据仍会将 Read/Write/Edit 限定在 …/expo-skill-eval-*/** 下(同时包含 /tmp 和 /private/tmp 两种形式,因为 macOS 不会自动解析符号链接),作为说明和防护栏,但这些 Write/Edit 条目本身无法消除提示。实际影响:每次运行开始时,工作区会触发一次 Write 提示——选择 “Yes, allow all edits in this directory for the session”,之后在该工作区中的所有编排脚本、evals.json 和 viewer 写入操作都会静默执行。让文件写入无需提示的,是这次目录授权,而不是规则。
    • 编辑前置元数据后重新加载——必须完全重启,而非使用 /reload-skills。技能在会话开始时只读取一次 allowed-tools;/reload-skills 会重新加载技能正文,但不能可靠地刷新权限规则。编辑此文件后,请完全退出 Claude Code 并启动新会话,然后重新运行技能——否则,即使磁盘上的文件正确,仍会因规则集过期(缓存)而不断触发提示。
    • Grader 子 Agent 使用自己的权限上下文访问文件时仍会触发提示——这是预期行为,与主线程的规则无关。
  • **调用评估脚本——每次单独执行一条命令,绝不串联。**使用绝对路径,通过单独的 Bash 调用运行每个脚本:bash /abs/path/expo-skill-eval/scripts/snapshot-ios.sh arg1 arg2(受 Bash(bash *expo-skill-eval/scripts/*) 规则覆盖)。不要将其与 &、&&、||、;、wait、tail、head 或 echo 组合——复合命令会逐段检查,而这些额外片段没有规则,因此即使 bash …/scripts/… 部分获准,整条命令仍会触发提示。(唯一允许的管道是 … 2>&1 | tee <workspace>/…log,因为限定的 tee 规则覆盖它。)需要并行执行或精简输出?将操作放进 Python 编排脚本(受 python3 /…/expo-skill-eval-* 覆盖),由它通过 ThreadPoolExecutor 使用 subprocess 运行脚本。使用 Read/Glob/Grep 工具检查结果,而不是 cat/ls/grep。**一般规则:**在此技能的严格限定下,Agent 临时编写的任何 shell 命令都会触发提示——解决方法是将其移入脚本/编排脚本(或使用受限的 tee),绝不要放宽规则。
  • **检查输出(截图、日志、文件)——使用工具,不要使用 shell。**使用 Glob 工具查找文件(例如 /private/tmp/expo-skill-eval-<skill>/iteration-N/**/ios.png);使用 Read 工具查看文件——Read 会以视觉方式呈现 PNG,这正是确认截图已渲染所需的方式。使用 Grep 搜索文件内容。切勿为此使用 find/ls/cat:它们会触发提示;并且有意禁止 find … -exec …,因为其 -exec 可以运行任何命令(例如 -exec rm)。这些工具都已限定范围且无需提示;每当你原本想输入 find/ls/cat 时,都应改用它们。
  • 生成的 Python 脚本:在工作区中编写编排/汇总脚本(例如 /private/tmp/expo-skill-eval-<skill>/aggregate.py),并使用 python3 运行(受 Bash(python3 /private/tmp/expo-skill-eval-*) 覆盖)。viewer 是个例外——它是已签入的 scripts/generate_viewer.py,应通过 Bash(python3 *expo-skill-eval/scripts/*) 运行。Write 会自动创建父目录,但首次操作会触发提示——请授权一次工作区目录(见上文 Write/Edit 说明)。可让脚本自行写入日志,或使用 python3 … 2>&1 | tee <workspace>/…log(受限定的 tee 规则覆盖)捕获输出;使用 Read 工具读取日志。请勿使用 python3 -c … 进行设置(限定规则只匹配工作区脚本路径,因此裸 -c 会触发提示)。
  • 针对已安装插件进行触发评测:在流中检测实际安装的技能名称(例如 expo:expo-ui)——如果已安装真实插件,使用合成副本的测试框架总是会得到 0% 的评分,因为模型会选择真实技能,而非合成副本。
  • 基准测试聚合:将每次运行的 grading.json + timing.json 保存到 eval-<N>/<config>/run-1/ 下。在工作区中编写 Python 聚合脚本,并使用 python3 运行。
  • Expo Go 上限:任何需要自定义原生代码的内容(expo-module、App Clips、brownfield)都无法在 Expo Go 中运行。此类情况使用 static-only 模式——为某项技能编写评测用例前,请参阅 references/runtime-matrix.md(注意:@expo/ui 确实可以在 SDK 56+ 的 Expo Go 中运行)。
  • API 路由类技能:不要使用截图,而是在 Metro 运行时通过 curl 验证路由;将响应记录为用于评分的输出文件。
  • 计时数据:每次执行器运行后,立即将令牌数和耗时记录到 timing.json 中——这些数据之后无法恢复。要捕获令牌数,请在执行器的 claude -p 调用中添加 --output-format=stream-json --verbose,并从日志中解析 message_start / message_delta 事件。没有这些标志,日志只包含文本,唯一可恢复的指标只有耗时秒数。
  • 首次启动对话框:全新模拟器上,Expo Go 偶尔会显示一次性提示。如果截图捕获到的是对话框而非应用,请重新运行快照脚本(它会重新打开 URL)并重新截图。

查看器

截取截图后,务必生成并打开 HTML 查看器,让用户立即查看结果,无需询问。该查看器是纳入版本控制的 scripts/generate_viewer.py——以工作区根目录为参数运行:

python3 /abs/path/expo-skill-eval/scripts/generate_viewer.py /private/tmp/expo-skill-eval-<skill>

它会生成独立的 /private/tmp/expo-skill-eval-<skill>/viewer.html,并自行在浏览器中打开(webbrowser.open)。它会呈现以下内容:

  • 每次迭代对应一个标签页(工作区根目录下的 iteration-*;会在 localStorage 中记住上次活动的标签页)。
  • 对于每个评测用例(从 <iteration>/evals.json 读取):并排显示 with_skill / without_skill 两列,每列展示静态门控状态、得分、平台截图(点击可放大;以 base64 data: URI 嵌入,使文件独立完整)、带有 PASS/FAIL 徽章的预期列表,以及审核备注。
  • 对于图像提示词用例(包含 reference_match / quality 的 grading.json):在生成的截图旁显示目标截图、reference_match 得分(生成结果与目标的匹配度)、每种配置的 quality 评分细则(每个维度显示一条得分/满分条及小计),以及摘要栏中的质量差值(with_skill − without_skill 小计),并与正确性差值并列。
  • 摘要栏显示 with_skill %、without_skill % 和差值。
  • 如果存在 trigger-evals/trigger_results.json,则显示触发准确率表。
  • 使用深色背景和颜色编码的得分(绿色 ≥85%,琥珀色 ≥65%,低于 65% 为红色)。
发布查看器(仅当事先选择加入时)

本地 viewer.html 始终会生成。只有当用户在事先确认时选择了“发布可分享的 Artifact”,才会在最后额外将其渲染为 claude.ai Artifact——未经用户选择加入,绝不发布(这是面向外部的内容,已发布的页面可能会被缓存或编入索引)。具体操作:

  • Artifact 工具会用自己的 <!doctype html>…<head></head><body> 骨架封装文件,因此交给它的文件必须仅包含页面内容——内联 <style>/<script>、base64 data: 图片和 <title>,但不得包含自己的 <!DOCTYPE>/<html>/<head>/<body> 标签(完整的独立文档会被重复封装,导致渲染错误)。
  • 添加 --artifact 后,脚本会生成适用于 Artifact 的版本:python3 /abs/path/expo-skill-eval/scripts/generate_viewer.py /private/tmp/expo-skill-eval-<skill> --artifact 会写入 viewer_artifact.html(内容相同,但移除了骨架,且不会打开浏览器)。将该文件传给 Artifact 工具(favicon: "📊"),而不是独立文档版本。
  • 查看器已自包含(截图采用 base64 编码,CSS/JS 均为内联),因此符合 Artifact CSP(不访问外部主机)。

参考资料

  • references/runtime-matrix.md — 各技能的运行时适用性(expo-go 与仅静态模式、平台说明)。
  • agents/visual-grader.md — 评分子代理的截图评分说明。

评论

支持 Markdown