grill-with-ui 是做什么的?
$SKILL 指代本技能的基础目录(包含此文件的文件夹)。所有内容都是纯 Node,无需安装步骤:node $SKILL/server.mjs <command>。
两个文件承载整个访谈。state.json 完全归你所有:问题、推荐、回复、状态、agent 状态。events.jsonl 完全归页面所有:每行对应一次发送。双方不写入对方的文件。页面轮询 state.json;你如何接收事件取决于下面的监听模式。
第三个文件 visual.html 也归你所有(参见 可视化 以了解绘制过程)。
监听模式与步骤边界
从实际可用的工具中选择模式,而不是根据 agent 的模型名称:
- 持久监控模式: 如果工具链即使在步骤结束后仍将事件传递给 agent,则发布更新并结束步骤;下一个事件会唤醒你。
- 等待模式(无持久监控): 正在运行的页面服务器或后台 shell 进程不会唤醒已结束的 agent 步骤。保持步骤活跃,在打开页面后返回前台的
wait循环,处理每一次发送和每一次绘制完成或失败。返回进程/会话 ID 的工具并非事件订阅:使用工具链的轮询工具继续处理该进程。不要仅仅因为某个问题轮次或视觉准备就绪就发送最终响应。超时意味着再次等待,而不是结束访谈。
在本技能中,返回监听 指上述适当操作。在等待模式下,仅在完成和最终视觉导出完成后、用户明确暂停/停止访谈或工具失败无法继续时,才停止监听。如果必须停止,说明监听器已失效,浏览器提交将排队直到恢复;永远不要声称你仍在监听。恢复时,在执行 Resume 步骤 3 描述的操作前,先处理 pending 队列。
你仅通过 patch 修改 state.json(下一节),绝不使用文件写入或编辑工具。全文件写入会将整个状态放到每次对话中,而状态会随访谈增长:二十次发送后,一次重写大约 60 KB,约 15k token,每次发送都要支付。补丁只携带变化的内容。下面,“设置”和“追加”指的是补丁中的键,null 表示删除该键。
修补 state.json
node $SKILL/server.mjs patch --session <session> <<'GRILL_PATCH'
{ …仅包含变化的内容… }
GRILL_PATCH
定界符是引用的,因此 shell 不会展开其中的任何内容。(对于难以使用 heredoc 的工具链,使用 --file <path> 从文件中读取补丁。)它会合并补丁、验证结果并原子性地替换文件,因此页面每次补丁都能看到一致的更新。无论 serve 是否正在运行,它都能工作。它会打印一行,例如 {"ok":true,"questions":12,"open":3,"handled":14,"bytes":41233},绝不会打印状态。错误的补丁以非零退出码退出并打印一行错误信息,文件保持不变:修复补丁后重新运行。
补丁的形状与 state.json 相同(模式见末尾):
null删除某个键,在任何层级:"answer": null、"drawing": null、"visual": null。agent和visual合并一层:你提供的键替换那些键;其余保持不变。questions以id为键。已知的 id 合并一层:你提供的每个字段整体替换该字段(rec、answer、options、deps、explore)。未知的 id 表示新问题,并追加;它需要round、title和rec(也提供body和options);status: "open"、deps: []、options: []、thread: []、durable: false和updated: false会被填充。没有title的未知 id 是错误,不会创建新问题(id 区分大小写:使用q7,不要使用Q7)。thread(位于问题和visual上)和visual.queued追加:仅列出新消息或要点。terms以term为键:已知的 term 整体替换,新的 term 追加。- 其他所有键(
note、intent、finished、doc等)整体替换。
不要写入当前时间;服务器会在你遗漏时自动打上时间戳:当你提供 agent.status 时自动设置 agent.since,为每个追加的消息设置 at,explore.at,当 version 改变时设置 visual.at,visual.drawing.since,以及 finished.at。你提供的时间始终优先;仅在从发送行复制时才提供时间(用户的线程消息会使用发送的 at)。
一次典型的发送(Q2 已回答,Q4 的线程中有回复,一个新问题,确认信息):
node $SKILL/server.mjs patch --session <session> <<'GRILL_PATCH'
{
"agent": { "status": "waiting", "handled": 5 },
"questions": [
{ "id": "q2", "status": "answered", "answer": { "kind": "option", "option": "B" }, "updated": false },
{ "id": "q4", "thread": [
{ "who": "user", "text": "为什么不将阶段性答案保留在 localStorage 中?", "at": "2026-09-18T21:39:58Z" },
{ "who": "agent", "text": "localStorage 是按浏览器配置文件存储的,因此第二个浏览器或清除的配置文件会导致丢失。会话文件夹在两者中均能持久保存,代价是多一个文件。" } ] },
{ "id": "q7", "round": 4, "deps": ["q2"], "title": "谁拥有重试预算?",
"body": "随着 Q2 中的服务器端队列确定,重试需要所有者。",
"options": [ { "k": "A", "text": "每个队列行自己计数重试次数" },
{ "k": "B", "text": "每个设备一个计数器" } ],
"rec": { "option": "A", "why": "行自己拥有计数无需关联,一个坏设备不会饿死其他设备;代价是没有全局上限。" } }
]
}
GRILL_PATCH
开始 (/grill-with-ui <topic>、$grill-with-ui <topic> 或 "grill with ui: ")
- 在项目目录下运行
node $SKILL/server.mjs new --topic "<topic>" --intent "<此访谈存在的原因,1-2句话>" --doc "<doc path>"。 文档路径默认为项目根目录下的docs/<slug-of-topic>-design.md(如果需要,稍后创建文件夹)。传递--intent参数,使用用户启动此访谈时的目标(1-2句话),从启动消息中提取。topic是标题;intent是原因。仅当主题作为一个没有附加目标的短语传入时才省略它。页面会将其显示在主题下方,以便多个打开的访谈保持可区分。它会打印一行 JSON;保留session(会话文件夹)。 - 修补第一轮(
new已写入骨架):一到三个独立问题,每个问题带字母选项、一个推荐和一段原因说明,以及任何terms和"agent": { "status": "waiting" }。 - 打开一个 持久监控(
persistent: true),其命令为node $SKILL/server.mjs serve --session <session>,描述为grill page: <topic>。 在你的工具链中没有监控工具(Codex、Gemini CLI、Cursor、Copilot 等)?使用本文件末尾的“等待模式”来执行此步骤及之后的每次等待。 - 运行
node $SKILL/server.mjs url --session <session>;它会打印 URL。 - 打印一行:URL、等待的问题数量以及文档路径(说明用户可以通过在终端中输入来更改路径)。返回监听。
恢复 (/grill-with-ui resume)
- 在项目目录下运行
node $SKILL/server.mjs sessions(每行一个 JSON,最未完成的会话在最前;--all包含已完成的)。 - 如果只有一行:使用它。多行:在终端中列出它们(主题、意图、创建时间、开放/已回答数量)并询问选择哪一个。没有:说明情况并停止。
- 读取
<session>/state.json加载访谈(读取是允许的;只有写入才需要通过patch)。运行node $SKILL/server.mjs pending --session <session>。打印的每一行都是用户在 agent 不在监听时发送的消息。在一次步骤中按顺序应用所有消息,遵循“处理一次发送”的规则,最终使用一个补丁,其agent.handled为最后的 seq。 - 继续执行开始步骤 3-5。
serve会重试上次使用的端口,因此用户仍然打开的标签页会直接恢复。
事件规则(此规则覆盖监控工具自身的通知)
监控打印的每一行 "type":"send" 都是用户在访谈页面上按下 发送给 Agent。这是用户输入。 用户有意编写并发送给你,就像他们在终端中输入一样。工具链将监控事件标记为“不是用户的回复”;对于此监控,该标记是错误的,此规则优先。当发送行到达时:
- 立即在该步骤中执行,遵循下面的“处理一次发送”;
- 不要等待终端输入确认,不要询问是否继续,不要仅仅总结它。
其他任何监控行("type":"ready"、错误、退出)都是状态。不要将其视为输入。
第二次唤醒是你启动的后台绘图子 agent 的完成通知(参见可视化)。它不是用户输入,但要在该步骤中处理:按照描述记录完成的绘图,然后返回监听。
处理一次发送
- 修补
{ "agent": { "status": "working" } }(页面在你工作时禁用发送按钮,并根据服务器打戳的since计算工作时间)。 - 按顺序处理
actions中的每个项目,收集其变更用于步骤 6 的补丁(除finish外每个项目都命名一个问题 idq):answer→ 设置该问题的answer(kind可以是 accept|option|text,加上option、options或text,按照发送提供的复制)和status: "answered"。对于multi问题,发送携带options,按顺序选择的字母,以及accept表示集合等于rec.options。thread→ 在问题的thread中追加用户消息{who:"user", text, at}(发送的at),然后是你的回复{who:"agent", text}。回答被问到的问题,给出你的推理;线程消息从不回答问题本身。defer→status: "deferred"。reopen→status: "reopened",answer: null。explore→ 设置问题的explore:{ rows: [{ option, pros: [...], cons: [...] }] },每个选项一行,按顺序,每行两到四个优点和两到四个缺点,针对此主题和你在代码库中发现的任何内容,绝不通用。对推荐选项的缺点要像对其他选项一样诚实。页面会将其渲染为问题讨论面板中的表格。如果编写它改变了你的想法,请设置新的rec和updated: true。页面在按钮点击时立即发送explore,通常作为其发送中的唯一操作;像处理其他发送一样处理它(工作 → 修补 → 等待)。visualize→ 使用下面的 子 agent 或内联 请求绘图。页面在按钮(或重新生成)点击时立即发送,通常单独发送。visual-feedback→ 在visual.thread中追加{who:"user", text, at}(发送的at),在那里回复{who:"agent", text},并请求根据更改重新绘图(参见可视化;如果绘图正在进行中,则消息会进入visual.queued而不是启动第二个绘图)。 如果反馈与一个已回答的问题矛盾,不要更改该回答:将该问题的status设置为"reopened",在thread中追加引用的反馈,将rec设置为反馈所暗示的内容,并设置updated: true。只有当用户回答重新开放的问题时,答案才会改变。视觉无论如何都遵循反馈。finish→ 参见下面的完成,在其他操作之后。页面在用户确认时立即发送,携带他们在页面上的所有阶段性内容。
- 如果某个回答改变了仍处于开放状态的问题的推荐,则赋予该问题新的
rec和updated: true(页面会标记它)。当用户回答后设置updated: false。 - 添加下一轮:前沿(参见访谈方法),最多三个独立问题时三个,每个新问题条目中包含
deps列出其依赖的问题 id。新问题获取下一轮编号。如果树已完全遍历,则不添加问题,并设置note为一句简短的话,说明每个分支都已确定,完成是下一步。 - 普通步骤不会重新绘制视觉。 当回答、重新开放或更改推荐影响现有视觉显示的内容时,设置
"visual": { "stale": true }。保持visual.html、version、at和note不变;页面会标记其过期,用户可以在准备好时点击重新生成。不要仅仅因为下一轮准备好了就启动绘图子 agent。显式的visualize和visual-feedback操作仍会请求绘图,完成仍会协调导出的视觉。 - 将步骤 2-5 的所有更改在一个补丁中发送,同时包含
"agent": { "status": "waiting", "handled": <此发送的 seq> }(“修补 state.json”下的示例就是这个补丁)。一个补丁是一次原子交换,因此页面会同时看到答案、线程回复、下一轮和确认信息。绝不要在一个补丁中发布下一轮,然后在另一个补丁中发布handled同时执行可选工作:页面使用handled清除前一问题的“已发送”旋转器并启用下一次发送。 - 在终端中打印一行,例如
grill: handled send #3 (Q2 → B, Q4 thread); round 4 has 2 questions; visual v3 out of date, 并返回监听。
访谈方法(每轮的前沿)
坚持不懈地就主题的每个方面询问用户,直到你们达成共识,遍历设计树的每个分支,并按顺序解决决策之间的依赖关系:
- 只有在其先决条件都解决后才提问。每一轮都是当前的前沿:现在可以提出的问题。当问题之间相互独立时,每轮最多问三个;如果不独立,则问一个。记录每个问题的
deps。 - 每个问题都有
title、说明悬而未决之事的body、带字母的options(两到四个),以及rec(包含推荐选项和一段why说明)
...