Agent 方法论框架 superpowers 的视觉伴侣:克制与零依赖
本文基于 superpowers 仓库 commit 44c9b2d(2026-07-27)梳理,该项目仍在持续迭代,具体行为以仓库 https://github.com/obra/superpowers 最新代码与文档为准。
**这块画布真正值钱的地方不是画布本身,而是那套「什么时候不许开画布」的规则。**把 HTML 渲染到浏览器里让人点两下,谁都能写;难的是让一个随时想表现自己的 Agent 忍住不去开它——不在会话一开始就开,不在每个问题上都开,甚至在用户已经同意之后仍然逐题重新判断一次。superpowers 的 visual-companion 就是围绕这个约束设计的,画布只是约束的载体。
先说清楚本篇和站内几篇的分工:AI 辅助方案设计 讲的是需求到方案的通用方法论,AI 建站工具横向对比 讲的是工具选型的通用判断,这两篇给的是「该怎么想」;本篇不重复那些原则,只看一个真实开源项目把「什么时候该给人看图」这件事落到了哪些文件、哪几行判断、哪些取舍上——具体到目录路径和函数名,你可以当场打开仓库核对。
一、它要解决的问题:有一类设计问题,文字讲不清
superpowers 的 brainstorming 技能是一条硬流程:探索项目上下文 → 逐个提澄清问题 → 给 2-3 个方案带取舍 → 分段呈现设计并逐段要用户确认 → 写 spec 文档 → 自查 → 用户复核 → 转交写计划的技能。它的技能文件里有一段 <HARD-GATE>,明确写着在用户批准设计之前不许调用任何实现技能、不许写代码、不许搭项目脚手架,而且这条规则对每个项目都生效,不管它看起来多简单。紧接着还有一节反模式叫「这个太简单了不需要设计」,说的是简单项目恰恰是未经检验的假设最容易造成返工的地方。
这条流程全程跑在终端里,靠文字一问一答。问题出在有一类问题上:终端里的纯文字问不清楚。
「你想要哪种向导?」这是个概念问题,用文字描述三种思路完全够用。但「这三种向导布局你觉得哪个对?」就不一样了——用户要看的是导航在左边还是顶部、步骤条长什么样、内容区多宽。你可以用文字描述一个布局,但用户读完在脑子里重建出来的那个东西,跟你想的多半不是同一个。等到实现完了才发现理解偏了,前面那一整轮对话就白问了。
视觉伴侣要解决的就是这一类问题。仓库文档里给出的判定标准只有一句话:**用户是看到它更容易理解,还是读到它更容易理解?**这句话是整个设计的轴心,后面所有的约束都是在防止这句话被滥用。
二、约束长什么样:三道闸门
「给 Agent 一块画布」这件事天然有个失控倾向——一旦有了新玩具,Agent 会倾向于处处使用它。superpowers 设了三道闸门。
第一道:不许提前提议。 brainstorming 技能的清单里,第 2 项写的是「just-in-time 地提议视觉伴侣——不要提前提」。它要求你等到第一次真正遇到「说出来不如画出来」的问题时才提。如果整场对话里从来没出现过视觉性的问题,那就永远不提。这一条把默认状态定成了「不开」,而不是「先开着备用」。
第二道:提议必须单独成一条消息。 技能文件里这条是加粗强调的:这条消息里只能有提议本身,不许夹带澄清问题、不许夹带小结、不许夹带任何其他内容,发完就停下来等用户回答。这个约束的用意很直白——如果提议混在一大段内容里,用户很可能顺手就「嗯」过去了,那不叫同意。文档甚至给了推荐话术,里面明说了这东西「还比较新,而且比较费 token」。用户拒绝之后就继续走纯文字,不再重复提,除非用户自己提起来。
第三道,也是最反直觉的一道:同意之后仍然逐题判断。 技能文件里写得很清楚——视觉伴侣是一个工具,不是一种模式;用户接受它,意味着它对那些适合视觉呈现的问题可用,不意味着从此每个问题都走浏览器。visual-companion.md 开头就把这一点又说了一遍:按问题决定,不是按会话决定。
配套的判定清单也写得很细。走浏览器的是内容本身就是视觉的:UI 原型(线框、布局、导航结构、组件设计)、架构图(系统组件、数据流、关系图)、并排视觉对比(两套布局、两套配色、两个设计方向)、观感打磨(间距、视觉层级)、空间关系(状态机、流程图、实体关系)。走终端的是内容本身是文字或表格的:需求与范围问题、概念性的 A/B/C 选择、取舍清单、技术决策(API 设计、数据建模、架构路线)、以及任何答案是「话」而不是「视觉偏好」的澄清问题。
最后它补了一句最容易被忽略的话:**一个关于 UI 话题的问题,不自动等于一个视觉问题。**这句话直接堵死了「反正在聊界面,那就开画布吧」这条捷径。这个思路和 人在环中的介入点设计 里讨论的判断是一致的——工具可用不等于工具该用,触发条件必须写死在规则里,而不是交给 Agent 临场发挥。
三、这块画布怎么跑起来
机制本身很朴素:一个进程盯着一个目录,把目录里最新的那个 HTML 文件端给浏览器。Agent「推一屏」的动作,就是往目录里写一个新文件。
服务端在 getNewestScreen() 里按修改时间排序取最新的 .html;文件监听用 Node 内置的 fs.watch,每个文件名带 100 毫秒防抖。发现的是一个没见过的新文件名时,它会把 state_dir/events 删掉、往标准输出打一条 screen-added;如果只是既有文件被改写,就打 screen-updated,不清事件。两种情况都会通过 WebSocket 向所有连着的浏览器广播一条 { type: 'reload' },页面自己刷新。
这里有个设计细节值得单独说:内容片段优先。服务端的 isFullDocument() 只看内容开头是不是 <!doctype 或 <html。是完整文档就原样端出去(只注入一段客户端脚本);不是,就自动塞进外壳模板的 <!-- CONTENT --> 位置,连带页头、主题 CSS、连接状态指示、以及全部交互基础设施一起给你。文档里的原话是「默认写内容片段」,只有在你确实需要完全控制整个页面时才写完整文档。
这意味着 Agent 每屏只需要写十几行 HTML。外壳模板里已经准备好了一批语义化的 class:.options/.option(A/B/C 选项,容器上加 data-multiselect 就变成多选)、.cards/.card(视觉方案卡)、.mockup(原型容器)、.split(并排视图)、.pros-cons(利弊两栏)、以及一组线框积木 .mock-nav、.mock-sidebar、.mock-content、.mock-button、.mock-input、.placeholder。少写 HTML 也就意味着少烧 token,这一点在 Agent 框架的 token 效率 的语境下是笔实账。
用户点击的回路同样简单。浏览器侧脚本在 document 上挂了一个点击监听,向上找最近的 [data-choice] 元素,把 {type, text, choice, id} 加上时间戳通过 WebSocket 发回去;服务端只把带 choice 字段的事件按行追加进 state_dir/events。Agent 下一轮读这个 JSONL 文件,跟用户在终端里说的话合起来看。文档特意提醒:终端里那句话是主反馈,事件文件提供的是结构化的交互数据;而且完整的事件流能看出用户的探索路径——他可能点了好几个才定下来,这个犹豫过程本身值得追问。
还有一个容易被跳过但很关键的动作:卸载。当下一步不再需要浏览器时(比如转去问一个澄清问题、讨论一段取舍),文档要求推一屏「等待页」把旧内容清掉,内容就是一句居中的「Continuing in terminal…」。理由是不要让用户对着一个已经解决完的选择发呆,而对话早就走远了。
| 组成部分 | 它负责什么 | 对应仓库位置 | 你什么时候会碰到它 |
|---|---|---|---|
| 提议时机规则 | 规定 just-in-time 提议、提议单独成一条消息、拒绝后不再提 | skills/brainstorming/SKILL.md 的 Visual Companion 一节 | 第一次遇到「说不清、得画」的问题时 |
| 逐题判定规则 | 每个问题该走浏览器还是终端,以及正反两份清单 | skills/brainstorming/visual-companion.md 的 When to Use | 每次准备推屏之前都要过一遍 |
| 启动脚本 | 建会话目录、按平台选前台/后台、写 PID 与实例 id、等待启动 JSON | skills/brainstorming/scripts/start-server.sh | 会话开始,以及服务掉了要原地重启 |
| 服务进程 | HTTP 路由、自实现 WebSocket、目录监听、鉴权、生命周期看门狗 | skills/brainstorming/scripts/server.cjs | 排查 403、端口冲突、进程自己退出 |
| 页面外壳模板 | 给内容片段套壳,提供主题 CSS、连接状态与那批语义 class | skills/brainstorming/scripts/frame-template.html | 写内容片段时查有哪些 class 能用 |
| 浏览器侧脚本 | 采集点击事件、断线重连退避、离线事件排队、暂停遮罩 | skills/brainstorming/scripts/helper.js | 用户点了但你没读到事件时 |
| 停止脚本 | 核实实例 id 后再杀进程,只删 /tmp 下的会话目录 | skills/brainstorming/scripts/stop-server.sh | 收尾,以及回头翻历史原型 |
四、零依赖是怎么换来的
服务端这块有份专门的设计文档:docs/superpowers/specs/2026-03-11-zero-dep-brainstorm-server-design.md。目标写得很直接——把 express、ws、chokidar 这三个被 vendored 进 git 仓库的依赖(文档里记的是 714 个受版本控制的文件)换成一个零依赖的单文件服务端,只用 Node 内置的 http、crypto、fs、path。
动机不是性能,是供应链。文档里的论证很老实:把 node_modules 提交进仓库意味着这些冻结的依赖拿不到安全补丁,714 个第三方文件未经审计就进了库,而且有人改了 vendored 代码,在提交历史里看起来跟正常提交没区别。它同时承认「实际风险很低(本来就是只跑 localhost 的开发服务器)」,但既然消除它并不难,那就消除。这是个挺克制的表述,没有把一个工程整洁度的改进包装成安全事故。
代价是 WebSocket 得自己写。server.cjs 里那段 RFC 6455 实现只覆盖文本帧:用 SHA-1 加上 RFC 6455 的魔术 GUID 算出 Sec-WebSocket-Accept,回 101;解帧处理三种带掩码的长度编码(小于 126 字节、16 位扩展、64 位扩展),用 4 字节掩码 XOR 还原载荷,缓冲区不完整就返回 null 等下一块数据,拒绝未加掩码的客户端帧;处理的操作码只有 TEXT、CLOSE、PING、PONG,遇到别的直接回一个状态码 1003 的关闭帧。
明确不做的部分在文档里单列了一条:二进制帧、分片消息、扩展(permessage-deflate)、子协议,全部不实现。理由写得很有说服力——这些对 localhost 之间传小段 JSON 文本毫无必要;而扩展和子协议是在握手阶段协商的,只要不主动声明,它们就永远不会被启用。这是把「不实现」变成「不可能被触发」,而不是留一个会在运行时炸的窟窿。代码里另外还压了一道上限:单帧载荷超过 10 MB 直接抛错。
这个文件还有个双重身份:直接 node server.cjs 跑起来是服务器,被 require 引入时导出 computeAcceptKey、encodeFrame、decodeFrame、browserLauncherForPlatform、OPCODES、MAX_FRAME_PAYLOAD_BYTES,专门给单元测试用。协议层的边界条件因此能脱离进程直接测,集成测试才去起真服务器——那里用了 ws 作为纯测试期的客户端依赖,不进最终交付。这种「同一个文件按入口分角色」的写法,让协议层的正确性不必依赖端到端环境就能守住。
安全这一块,后来的代码比这份文档走得更远。每个会话有一把随机密钥,跟在 URL 的 ?key= 上;首次加载时被镜像进一个以实际绑定端口命名的 HttpOnly、SameSite=Strict cookie,之后刷新页面和 /files/* 子资源就自带凭证。校验走 crypto.timingSafeEqual,长度不等直接判否。代码注释解释了为什么用密钥而不是 Host/Origin 白名单:这个服务对任何本地浏览器标签页都可达,绑到非回环地址时对任何能路由过来的主机都可达,密钥能在回环、隧道、远程三种情况下一致地认出真正的客户端,还能挡住 DNS 重绑定。WebSocket 升级请求额外走一道 Origin 校验。响应统一带上 X-Frame-Options: DENY、Content-Security-Policy: frame-ancestors 'none'、Referrer-Policy: no-referrer、Cache-Control: no-store、Cross-Origin-Resource-Policy: same-origin。静态文件那条路由用 isRegularFileInsideContentDir() 逐项排掉符号链接、非普通文件、硬链接数不为 1 的文件,再用 realpath 确认还在内容目录里面。
五、边界与代价
这套设计放弃的东西,比它得到的东西更值得先看清楚。
它明确不管的: 不管你画得好不好看。仓库里的建议只有几条粗线条——保真度匹配问题(布局问题给线框,打磨问题才给精细稿)、每屏 2-4 个选项、每页写清楚要问什么而不只是「选一个」、涉及真实感受时用真实内容(比如摄影作品集就该放真照片,占位内容会掩盖设计问题)。除此之外,画布上的东西质量如何,取决于 Agent 自己的产出,框架不兜底。
它不适用的场景: 纯远程或容器化环境需要额外配置。默认绑 127.0.0.1,要在别处访问得显式指定绑定地址和展示用的主机名。自动开浏览器这件事只在回环绑定时才做——代码里明确跳过非回环的情况,也跳过已经有客户端连着的情况。真正的无头环境更直接:browserLauncherForPlatform() 在既没有 DISPLAY 也没有 WAYLAND_DISPLAY 的 Linux 上返回 null,什么都不开。
没有优雅停机。 设计文档里写得很坦率:进程生命周期交给 shell 脚本用 SIGTERM 处理。取而代之的是两个兜底:一个看门狗按 60 秒的节奏检查宿主进程还在不在(用 process.kill(pid, 0) 探活),一个默认 4 小时的空闲超时。宿主没了或者闲太久,进程自己退出,并往 state_dir/server-stopped 写一条记录。
这条流程会让你变慢,这是设计意图。 硬门禁要求设计被批准之前不许动手,每段设计还要单独确认一次。对一个改配置项、改一行文案的小改动来说,这套完整流程是过度设计——技能文件自己承认设计可以很短,「简单项目几句话就够」,但仍然要求你把它说出来、要到批准。视觉伴侣叠在这上面,还要再加一层成本:提议本身要占一条消息,起服务、写每屏 HTML、读事件文件都要烧 token,文档里推荐的话术就直说了它「比较费 token」。你换来的是把理解偏差提前暴露,代价是每一轮都更慢、Agent 输出更啰嗦。这笔账值不值,取决于返工的代价有多大——一个要写两周的前端模块值,一个改 CSS 变量的活儿不值。
画布上的选择不是决策。 事件文件里记的是点击轨迹,不是结论。文档反复强调终端里那句话才是主反馈,浏览器事件是补充。也就是说这块画布没有替代对话,它只是给对话加了一个更高带宽的输入通道。
六、上手与避坑清单
别复用文件名。 服务端靠修改时间挑最新的文件,覆写同名文件只会打 screen-updated 而不清空事件文件——上一屏的点击记录会留下来,跟这一屏的混在一起。避法是每屏一个语义化的新文件名,迭代就加版本后缀(layout-v2.html、layout-v3.html)。
别用 cat/heredoc 写 HTML。 文档里这条是明令禁止的,理由是往终端里灌噪声。用你手上的文件写入工具。这条在长会话里影响的不只是观感——终端里那一大坨 HTML 会占掉上下文,具体的账可以对照 上下文预算怎么分配 来算。
推屏之前先确认服务还活着。 这是文档里标了「必须」的一条:检查 state_dir/server-info 存在、state_dir/server-stopped 不存在。会踩的原因是空闲超时和宿主进程看门狗都会让服务静默退出,而你手里还攥着那个 URL。正确的恢复动作是用同一个 --project-dir 重启:端口和密钥都持久化在项目下的 .superpowers/brainstorm/ 里,重启会复用,用户那个开着的标签页会自己重连(断开期间它显示一层「Companion paused」的遮罩),不用重新发 URL。
永远给完整 URL。 服务对不带密钥的请求一律回 403 加一个提示页。会踩的原因是顺手把 ?key=… 截掉,或者只报了 http://host:port。避法是原样转发启动 JSON 里 url 字段的完整值。
Windows 上要用后台方式起。 启动脚本会检测出 Git Bash / MSYS 环境并自动切到前台模式,而前台模式会阻塞整个工具调用。避法是在 Bash 工具上把后台执行打开,下一轮再去读 state_dir/server-info 拿 URL 和端口。同一份文档里给了 Codex、Gemini CLI、Copilot CLI 各自的起法,共同点是这个进程必须跨轮次活着。顺带一提,Windows 上启动脚本会主动把宿主 PID 清空——因为 Node 看不见 MSYS2 命名空间里的 POSIX PID,传进去反而会让服务在 60 秒的检查点误判自杀,清空之后只剩空闲超时这一个退出触发器。
加 .gitignore,并且知道文件会留下来。 用了 --project-dir 的会话,原型文件留在项目下的 .superpowers/brainstorm/ 里,停止脚本只删 /tmp 下的临时会话目录。会踩的原因是这些 HTML 会被 git 看见。文档要求提醒用户把 .superpowers/ 加进 .gitignore。好的一面是这些原型事后还能翻出来看。
别拿设计文档当当前行为的说明书。 那份 2026-03-11 的零依赖设计文档里写的是 server.js,事件和启动信息落在屏幕目录下的 .events、.server-info;今天仓库里的实现是 server.cjs,事件和启动信息都在独立的 state 目录里,还多出了会话密钥、空闲超时、宿主看门狗、端口复用这些当时没有的东西。这是正常的漂移——spec 记录的是当时那次决策,不是现在的接口。会踩的原因是照着 spec 里的路径去找文件然后找不到。避法很简单:路径以 visual-companion.md 和 server.cjs 为准,spec 只用来理解「为什么当初这么选」。
收束
如果只带走一件事:先写清楚什么时候不用它,再写它怎么用。 superpowers 这块画布的规格文字里,判定何时该用、何时不该用的篇幅,跟讲怎么用的篇幅是同一个量级的,而那句判定标准短到一行——用户是看到它更容易理解,还是读到它更容易理解。
想自己核一遍,建议按这个顺序读三个文件:先读 skills/brainstorming/SKILL.md,看视觉伴侣是怎么被塞进一条既有流程的第 2 步、以及那个硬门禁;再读 skills/brainstorming/visual-companion.md,看逐题判定的正反清单和那个「写文件即推屏」的回路;最后读 skills/brainstorming/scripts/server.cjs,看零依赖这个决定具体换来了多少自己要扛的代码。这个项目是 MIT 许可,仓库在 https://github.com/obra/superpowers ,三个文件里两份是规格文字、一份是服务端源码,加起来一千行出头,一个下午读得完。
本文属于 superpowers 方法论专题(共 30 篇,含三篇与其它开源 Agent 项目的对照)。想看把资产铺满的另一种取向,见 ECC 开源 Agent 套件专题。