跳到正文

Reticle

一个 MCP 服务器加一个只在开发期生效的 SDK:让编码 agent 从应用内部去读、去操作一个正在运行的 web 或桌面应用,然后给出判词和该改的文件与行号,而不是一张截图。

Screenshot of Reticle
编辑截图, 30 Sep 2026Reticle ↗

这是什么

一个以 MCP 工具形式交给编码 agent 调用的验证工具。开发期,应用里嵌一个很小的 SDK,它通过本地 WebSocket 上报 DOM、网络、控制台、路由与框架状态;agent 先看、再操作、再观察,最后断言「应该成立的事」,拿回来的判词是 yes、no 或 unknown,附带证据,以及该改的源文件与行号。它不靠截图评分:读的是无障碍树、网络日志、控制台和 store,那些「到不了像素」的故障正落在那里。它提供 CLI、MCP 服务器、Vite/Next/Babel 构建插件,以及 Electron、Tauri 和命令行这几类被验证对象的适配器;一条流程录一次之后可以不再经过模型重放,CI 门禁则不允许一个被流程覆盖的文件在没有通过判词的情况下过关。

谁做的账号 divshekhar 写了仓库 2,602 次提交中的 2,226 次,占全部的 86%;人类贡献者里排在第二的有 56 次。README 请读者去「book a call with the founders」,用的是复数;路线图里说话用的是「we」。1,890 次提交带共同作者尾注,其中 934 条写的是带一百万 token 上下文的 Claude Opus 5,其次是 Claude Opus 4.8 的 661 条,Cursor 只有 32 条。

它是怎么搭起来的

组成 · 6

形状是三个活动部件加一份契约。页面里一个只在开发期存在的 SDK,负责监视 DOM、网络、控制台、路由与框架状态,并拨出一条 WebSocket;一个 Node 进程持有 bridge、MCP 服务器和 CLI;仓库里的 .reticle/ 目录把流程、基线和运行产物存成可评审的文件。整个 monorepo 按「每条边界强制一条规则」来切,而不是按偏好:浏览器包不许 import Node API,服务器包不许 import DOM API,契约位于依赖图最底层、只依赖一个 schema 库和那份协议,而决定判词的那部分代码身上不挂浏览器、不挂 daemon、也不挂 CLI——正因如此,同一套规则既可以被一致性运行器反复驱动,也可以被质疑结论的人直接读。这个选择有两个后果贯穿其余代码。其一,判词的价值上限就是它的出处,所以「被测对象」的身份是一等值——会死的 instance、热更新时会动的 epoch——每一条观察都必须挂在它上面。其二,真正有意思的失败是没人看到的那些,所以「诚实」被当成一个带测试的功能来做:一条断言必须说明自己用的是哪一档证据,而 unknown 是一个结果,不是缺失。

core/ 与 open-verification/
依赖图的最底层:凡是跨浏览器、bridge 和 agent 的常量与 schema 都住在 core(160 个文件,约 1 MB),它只依赖 zod 和那份协议包。协议本身是一件独立发布的东西——约 65 KB 的规范、永久不复用的需求编号、一份裁决向量文件、一个一致性运行器,以及自己的治理、版本与变更流程——因为决定判词的那些规则,正是持怀疑态度的读者最想审的部分。
adapters/realm/browser/
页面内的 SDK,也是最大的适配器,370 个文件:DOM、网络、控制台、路由、存储、动画、对话框、下载、焦点、性能与 frame 各自的观察器;动作层与查询层;无障碍名计算;ref 寻址与源码印记;遮挡与绘制上下文;以及一个可选的面板,让人能旁观、批注并接管。
server/src/
Node 那一半,按文件数也是仓库的中心:1,322 个文件、约 8.9 MB,涵盖 WebSocket bridge、MCP 面及其代理、reticle CLI、daemon 及其生命周期规则、.reticle/ 下的流程与运行存储、租用标签页的浏览器池、对「没有会话」的诊断,以及一份大到拥有自己 59 KB 文档和一条强制测试的遥测契约。
engine/src/
决定判词的规则,被隔在一切能够触达浏览器或 socket 的东西之外:180 个文件,做矛盾检测、证据处理、断言求值与时间窗,并由一条「这些规则可以独立成立」的测试和一条「它与 core 的耦合只许变小」的测试看着,服务器侧还有一条对称的中立性测试。
init/ 与 adapters/build/
一次安装的两半。init 是一个没有运行时的构建期改写器:识别框架与 dev 脚本、规划改动、改写 Vite/Next/Remix/Astro/Nuxt/Angular/CRA/Electron 配置、注册 MCP 客户端,并且在真的有会话连上之前拒绝宣布自己完成。构建适配器为 Babel、Next 和 Vite 盖源码位置,其中 Vite 那个还负责注入 connect 调用。
桌面 realm 与整套仪器
Electron 有主进程适配器和约 11 KB 的预加载脚本,Tauri 有按操作系统分开、位于所有 JavaScript 门禁之外的 Rust 截图模块,另有靠外部监督进程的命令行 realm。产品周围是它自己的测量仪器:bench/ 的 75 个 harness 文件与公开记分卡、break/ 的敌对环境、conformance/ 拿协议去驱动各种实现、apps/e2e/ 四十多条端到端用例、十五个写给 agent 的 skill 目录,以及 docs/ 下 64 份文档。

取舍,以及它替代了什么

  • 读结构化数据,而不是截图 替代 截一张图然后去看它

    一次「看」大约要 1,365 个图像 token,慢、不确定,而且对一切非视觉的东西完全失明——失败的那个请求、那条控制台报错、那个没有变的路由。架构文档主张改为读无障碍树、网络日志、控制台和框架状态:更便宜,而且能看到像素承载不了的 bug。

  • 断言后果,而不是断言外观 替代 断言匹配某个选择器的元素在场

    它被称作最弱的一种判据:一个错误的元素、或者一个「自愈到错误元素」的元素就能满足它,于是回归照样发出去。所以证据分成档,而只有「后果」这一档能换来一个 yes;协议再补一条:证明后果的证据不得来自执行动作的那条通道。

  • 把 wire 契约放在依赖图最底层,只有一份 替代 在浏览器和服务器里各写一遍同样的字符串

    两个运行时必须在每条消息上完全一致,而文中点名的诱惑就是在两边都写上像「网络请求事件名」这样的字面量,那「正是漂移和静默损坏开始的方式」。于是这类字符串和结构都只作为具名常量加 schema 存在一次,两边共同引用,服务器则对每条进来的消息先校验而不是先相信调用方。

  • 对外只挂一个很小的工具面 替代 把服务器能分发的工具全都挂出去

    MCP 每一轮都会把每个工具定义重新发给模型,所以这份列表是租金而不是一次性购买;面铺得太宽还会让模型到处乱试。退休的工具是被合并进一个 action 参数而不是删除;项目还会自己测这份工具面的体积,同时承认它早先公布的那组数字已经过时——同一份文档里仍有一节标题写着「The default 19」,底下列着十九个名字,而它第一行说的是九个。

  • 流程录一次,重放时不经过模型 替代 每一轮都用 agent 重新驱动整套流程

    重放会把每个元素的耐久锚点重新解析到现场 DOM 上,并重新断言声明的后果,于是一次重新验证只要几十个 token,而不是一次完整驱动——公开的重跑数据是 47 对大约 120,000,四条流程跑一百轮则是 128k 对 12.1M。自愈只在后果仍然成立时才重新绑定漂移的锚点,这正是它不会「自愈到错误元素」的原因。

  • 新增的守卫测试必须对应一次具名事故 替代 因为「以后可能会漂移」就加一条守卫

    这条规则连同它的理由一起写着:便宜的守卫会越积越多,很少以能教人新知的方式失败,而「可能会漂移」不算一个缺陷。某一个版本里 CI 找到的三个缺陷中有两个,就在已经有一条守卫盯着它的代码里,而两条守卫都通过了——它们断言的是一个路径存在、一个打包器被找到,而「存在」从来不是问题所在。

  • 规则开放,服务器源码可见 替代 整个仓库统一用一个许可证

    协议、wire 契约、引擎和 SDK 是 Apache-2.0,服务器与 CLI 采用 Functional Source License,每个版本在发布两年后转为 Apache-2.0。给出的理由是:决定判词的那一部分,正是持怀疑态度的读者最想审的部分,所以它恰好没有许可证挡在前面;而企业功能藏在一把密钥后面,开发与评估免费,激活过程离线完成。

依据docs/architecture.md(约 12 KB,讲活动部件与四条设计决策)、CLAUDE.md(约 19 KB,讲 monorepo 布局、服务边界与不可谈判的规则)、open-verification/SPEC.md(讲协议)、docs/tools/overview.mdx(讲工具面及其测量出来的成本)、README(讲许可证切分与安全性质),以及带文件体积的完整目录树。

制作过程

6 个阶段
  1. 01

    仓库建于 2026-06-11,三天后 bridge 就能跑通一个来回

    仓库建于 2026-06-11,第一次提交在 2026-06-13,信息是「M0: project scaffold + working bridge round-trip」——一次就把脚手架和一个能应答的 socket 落了下来。到 2026-09-30 已有 2,602 次提交,最后一次的标题是「a daemon skew is told to the connection that announced it」(issue #1136)。按月看:建仓那个月 395 次,7 月 466 次,8 月 1,252 次,9 月 489 次。列出的二十个 release 从 2026-07-18 的 v2.1.0 到 2026-09-30 的 v3.4.0,大约四天一个,其中四个在最后十二天里;标签列表里还有一个 release 列表没有的 v3.0.0,所以从 2.14.0 跳到 3.1.0 时跳过了一个号。最后一天值得按顺序读:18:16 到 18:20 之间,作者提了三十个 issue,每个都点名一个文件、附一条「Done when」验收条件;18:25 打了 v3.4.0 的标签;提交一直持续到 18:42。其中两个在一小时内被外部的人认领——仓库本身也备好了这套流量:issue 模板、code owners、DCO 检查、治理文档、行为准则,以及 hacktoberfest 标签。706,454 字节的 changelog 由脚本拼装,测试则保证发布包与 Rust crate 的版本同步,同时不把「新增字段」当成版本偏差。

  2. 02

    传感器放在页面里面,而不是站在旁边看

    定调的是一个 README 里的对照表:Playwright、DevTools 和浏览器类 agent 都站在浏览器外面往里看,这对你不拥有的站点是对的,对你自己在做的应用是错的,因为真正要命的 bug 根本到不了像素。所以应用自己带着传感器。开发期它调用 connect,默认连到 ws://localhost:4400/reticle 的 WebSocket,发送一条 HELLO,带上会话 id、协议版本,以及(如果配了)配对令牌;每个标签页各自生成 id,所以两个标签页不会撞车。SDK 随后装上 MutationObserver 看 DOM 变化、包住 fetch 与 XMLHttpRequest 看网络、挂上 console 钩子和 history 钩子看路由,再加上应用按名字主动注册的那几样——registerStore、registerCapabilities 与 signal——全部推进一个有界环形缓冲区:最近的历史随时可查,内存有上限。agent 调一个 MCP 工具,服务器把它变成一条走这条 socket 的命令,SDK 在真实页面里执行,再把结构化事件流回来。整条链上没有任何一处执行任意 JavaScript:固定的命令集只有看、操作、读状态和跳转,没有「执行这段 JS」这种工具。两个运行时的接缝是一个包而不是一句约定——凡是跨浏览器、bridge 和 agent 的字符串与结构,都只在 @reticlehq/core 里定义一次——而服务器会用这套 schema 校验每一条进来的消息,格式不对就断开连接,而不是让它流进逻辑里。

  3. 03

    同一套协议也架在 Electron、Tauri 和命令行上

    桌面这一块是这个仓库不再是纯浏览器工具的地方。Electron 拿到一个住在主进程里的适配器:IPC 观察器加主进程截图,预加载脚本约 11 KB——渲染进程与特权进程之间那道边界就是这样变成证据的,README 也把这道边界点名为「只做浏览器的工具看不到」的东西。Tauri 拿到一套 Rust 截图后端:一个在所有 JavaScript 门禁之外的 crate,由专门的 CI job 编译,按操作系统各一个 capture 模块;另有一份由脚本生成的桌面契约文件,由测试守着。第三类 realm 处理命令行对象:它把进程拉起来并盯着它,不往里面塞任何代码,还自渲染一个终端 HUD。三者都听同一个协议包的话,而这个协议包的「对象」词汇表是刻意开放的——web、desktop、mobile、service、game、device,或者别的什么——理由是:一个必须改协议才能容纳一类新计算机的协议,是有保质期的。为这份通用性付出的代价就写在旁边:当被命名的那件事物被替换时,instance 必须变化;导航会整体作废证据,热更新只作废一部分;而两者都必须可上报。

  4. 04

    文件与行号来自构建期,而 SDK 绝不会进生产包

    一句点名 src/checkout/PayButton.tsx 加行号的判词,价值不会超过那张映射表,而这张表是构建期盖上去的,不是运行时猜出来的:三个插件把 data-reticle-source 印进标记里——一个 Babel 插件、一个保留 SWC 并包住配置的 Next 适配器,以及 Vite 插件;Vite 这个还负责注入 connect 调用、探出开发服务器端口、写入配对令牌。@reticlehq/react 则把 DOM 节点映射到组件、再到源码位置,而它是刻意的可选项:项目自己写明,核心部分在没有源码映射那一半时也能工作。也正是同一个构建步骤让 SDK 永远不会随包发布——它只在开发服务器上生效,生产构建会把它换成一个惰性桩,运行时还有一道守卫,构建报告 NODE_ENV=production 时拒绝连接。这道守卫还顺带产出了一条维护者自己提给自己的错误建议:issue #1267 记录说,把 lease 指向一个已部署的 URL,会得到 sdk_never_dialled 以及「在那个目录里跑 reticle init」的提示,而这帮不上任何忙,因为那个桩是故意的。其余边界是以性质而非承诺写下的:bridge 只绑 127.0.0.1,应用用一个仅属主的令牌 ~/.reticle/pairing-token 与它配对,不带该令牌就拒绝绑到非回环地址,凡是能关掉某项安全控制的环节变量都只有一个具名常量,所以打错字不会静默地关掉鉴权;密码、令牌、API key 和卡号在到达 agent 之前就被换成 [REDACTED]。

  5. 05

    yes、no、unknown——而看不见的地方也一并公开

    判词有三个值,重点在第三个:unknown 的意思是证据不足以判定,README 明确写着判词绝不是「悄悄地算通过」。证据本身分档——应用自己发出的 signal 最强,因为错误的元素伪造不出来;网络加路由加状态次之,在多数单页应用上不需要任何准备;DOM 或文本的存在感是最弱的那一档,工具会把人往别处推——而回答会说明它用的是哪一档。协议包把这种直觉变成了带编号的要求:证明某个后果的证据,不得来自执行该动作的那条通道;只有两条通道里至少有一条是独立的,才允许把不一致报告为故障;实现必须在连接时声明自己的通道;而读取未声明通道的断言必须是 unknown 而不是失败,因为「没人在看」和「事情没发生」会产出完全相同的空证据,意思却相反。它举的例子是 MCAS:三取二的传感器表决是一套正确的原则,却被实现成了只架在一个迎角叶片上;于是「一个只用散文写出规则、却不允许实现去声明独立性的协议,造出的是同一架飞机」。同一种反射也生出了那些小字:标签页在后台时,每个响应都会警告定时器、rAF 和指针手势可能静默失效;事件缓冲区发生淘汰时,会返回保留数、丢弃数,并注明此时一个否定结果可能是假否定;能力边界与强项并列公开——IndexedDB、Web Worker、closed shadow root 和跨域 iframe 目前看不见,而围绕单次动作的竞态被明确标为「部分支持」。支撑这套分档的测量也是同样写法:在一个确定性语料上,一个解析到错误元素的定位器能满足「存在」检查,却满足不了「后果」检查,而这个差别就是「88 个里有 1 个假绿」与 29 个的距离。

  6. 06

    对外只挂九个工具,表里却有四十五个

    对外的工具面是一个预算决定,不是剩下的东西:MCP 会在每一轮把每个工具定义重新发给模型,所以工具列表是租金,不是买断的菜单。表里 45 个工具,agent 看得到九个,而其中只有两个能产出判词——一次驱动如果没有 reticle_act_and_wait 或 reticle_assert 收尾,就没有结果。其余大多数是被合并而不是被删掉,调用一个退休名字会得到替代它的那个调用。2026-09-16 从一个全新 daemon 上量到的数据是:默认工具面九个工具、17,663 字节;把全部工具都挂出去则是三十个工具、126,954 字节。有两个工具是被一次量出来的错误推回台前的:它们原本只能通过一个通用的 invoke 工具够到,于是一个登录表单被一次一个调用地驱动,因为「一个 agent 必须先知道它存在才会去调的工具,就是永远不会被调用的工具」。再往下削有地板:砍到八个工具时,真实 agent 的准确率可测量地掉了下去,而那条附注说这份读数已经旧到只能算「有日期的证据」而不是证明。预算是另一半,门禁则压在它上面:录下来的流程重放时不经过模型,47 个 token,而重新用模型驱动大约是 120,000 个;门禁要求每一次被改动波及的流程都有通过的产物覆盖,否则以非零退出——README 说这是唯一一个没人能靠「自己推理自己的 diff」满足的检查。

相关档案

全部档案 →